0%

输入URL发生了什么?

  1. 浏览器的地址栏输入URL并按下回车。
  2. 浏览器查找当前URL是否存在缓存,并比较缓存是否过期。
  3. DNS解析URL对应的IP。
  4. 根据IP建立TCP(传输层)连接(三次握手)。
    • 客户端发送⼀个TCPSYN=1Seq=X的包到服务器端口
    • 服务器发回SYN=1ACK=X+1Seq=Y的响应包
    • 客户端发送ACK=Y+1Seq=Z
  5. HTTP发起请求。
  6. 服务器处理请求,浏览器接收HTTP响应。
  7. 渲染页面,构建DOM树。
  8. 关闭TCP连接(四次挥手)。
    • 主动⽅发送Fin=1Ack=ZSeq= X报⽂
    • 被动⽅发送ACK=X+1Seq=Z报⽂
    • 被动⽅发送Fin=1ACK=XSeq=Y报⽂
    • 主动⽅发送ACK=YSeq=X报⽂

输入URL发生了什么?为什么要三次握手而不是两次?

这是为了防止已经失效的请求报文突然又传到服务器产生错误。

HTTP协议介绍

超文本传输协议(HTTP)是一种应用层协议。

HTTP是一个客户端终端(用户)和服务器端(网站)请求和应答的标准(TCP)。通过使用网页浏览器、网络爬虫或者其它的工具,客户端发起一个HTTP请求到服务器上指定端口(默认端口为80)。我们称这个客户端为用户代理程序(user agent)。应答的服务器上存储着一些资源,比如HTML文件和图像。我们称这个应答服务器为源服务器(origin server)。在用户代理和源服务器中间可能存在多个“中间层”,比如代理服务器、网关或者隧道(tunnel)。

尽管TCP/IP协议是互联网上最流行的应用,HTTP协议中,并没有规定必须使用它或它支持的层。事实上,HTTP可以在任何互联网协议上,或其他网络上实现。HTTP假定其下层协议提供可靠的传输。因此,任何能够提供这种保证的协议都可以被其使用。因此也就是其在TCP/IP协议族使用TCP作为其传输层。

通常,由HTTP客户端发起一个请求,创建一个到服务器指定端口(默认是80端口)的TCP连接。HTTP服务器则在那个端口监听客户端的请求。一旦收到请求,服务器会向客户端返回一个状态,比如”HTTP/1.1 200 OK”,以及返回的内容,如请求的文件、错误消息、或者其它信息。

HTTP协议定义Web客户端如何从Web服务器请求Web页面,以及服务器如何把Web页面传送给客户端。HTTP协议采用了请求/响应模型。客户端向服务器发送一个请求报文,请求报文包含请求的方法、URL、协议版本、请求头部和请求数据。服务器以一个状态行作为响应,响应的内容包括协议的版本、成功或者错误代码、服务器信息、响应头部和响应数据。

以下是 HTTP 请求/响应的步骤:

  1. 客户端连接到Web服务器
    一个HTTP客户端,通常是浏览器,与Web服务器的HTTP端口(默认为80)建立一个TCP套接字连接。例如,http://www.simonf.cn。
  2. 发送HTTP请求
    通过TCP套接字,客户端向Web服务器发送一个文本的请求报文,一个请求报文由请求行、请求头部、空行和请求数据4部分组成。
  3. 服务器接受请求并返回HTTP响应
    Web服务器解析请求,定位请求资源。服务器将资源复本写到TCP套接字,由客户端读取。一个响应由状态行、响应头部、空行和响应数据4部分组成。
  4. 释放连接TCP连接
    若connection 模式为close,则服务器主动关闭TCP连接,客户端被动关闭连接,释放TCP连接;若connection 模式为keepalive,则该连接会保持一段时间,在该时间内可以继续接收请求;
  5. 客户端浏览器解析HTML内容
    客户端浏览器首先解析状态行,查看表明请求是否成功的状态代码。然后解析每一个响应头,响应头告知以下为若干字节的HTML文档和文档的字符集。客户端浏览器读取响应数据HTML,根据HTML的语法对其进行格式化,并在浏览器窗口中显示。

如果我们希望拦截请求,做自己的业务逻辑,Spring Cloud Gateway中默认的过滤器就没办法实现。此时,就需求使用全局过滤器,全局过滤器的作用也是处理一切进入网关的请求和微服务响应,与GatewayFilter的作用一样。区别在于GatewayFilter通过配置定义,处理逻辑是固定的;而GlobalFilter的逻辑需要自己写代码实现。

以下是个配置自定义全局过滤器并在过滤器内部处理业务逻辑的示例:

  • 需求:定义全局过滤器,拦截请求,判断请求的参数是否满足下面条件:

请求参数中是否有username,如果满足则放行,否则拦截

  • 步骤分析:

1、定义一个类实现GlobalFilter接口

2、重写filter方法

3、将该类纳入到spring容器中

4、实现Ordered接口定义该过滤器的顺序

  • 实现代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Component
public class AuthorizationFilter implements GlobalFilter, Ordered {

//实现过滤器逻辑
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String username = exchange.getRequest().getQueryParams().getFirst("username");
if(!StringUtils.hasText(username)){
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
}
return chain.filter(exchange);
}

//定义该过滤器的顺序
@Override
public int getOrder() {
return 0;
}
}

在写一个后台用户模块时,为了区分系统管理员和用户,命名时用使用了sUsername来命名系统管理员的名字,但是数据库注册管理员时一直没法注册成功。

1
2
3
4
5
6
7
8
9
10
11
@Data
@Schema(description = "系统管理员")
public class SystemUser {

@Schema(description = "管理员姓名")
private String sUsername;

@Schema(description = "管理员手机号码")
private String phone;

}

经查源码发现sUsername的set方法在loombook中被命名为setSUsername,也就是说loombook中所有的属性的前两个字母必须小写!

枚举和包装类

阅读代码,分析运行结果:

1
2
3
4
5
6
7
8
9
public static void main(String[] args) {
Integer i1 = 128;
Integer i2 = 128;
int i3 = 128;
int i4 = 128;
System.out.println(i1 == i2);
System.out.println(i3 == i4);
System.out.println(i1 == i3);
}

这段代码涉及到了Java中的自动装箱和拆箱的概念。

首先,我们需要了解Java中的整数缓存机制。Java中对于整数类型的缓存范围是在-128到127之间。当我们使用自动装箱将一个整数赋值给一个 Integer 对象时,如果该整数在缓存范围内,那么会直接返回缓存中的对象,而不会创建新的对象。

现在来分析代码:

1
2
3
4
5
6
7
Integer i1 = 128;
Integer i2 = 128;
int i3 = 128;
int i4 = 128;
System.out.println(i1 == i2);
System.out.println(i3 == i4);
System.out.println(i1 == i3);

在这段代码中,我们分别创建了两个 Integer 对象 i1i2,并将它们都赋值为 128。然后,我们创建了两个 int 类型的变量 i3i4,并同样赋值为 128

接下来,我们分别使用 == 运算符进行比较。

第一个比较 i1 == i2,由于 i1i2 都是通过自动装箱得到的 Integer 对象,而且 128 不在整数缓存范围内,所以 i1i2 引用的是不同的对象,因此比较结果为 false

第二个比较 i3 == i4,由于 i3i4 都是基本类型的 int,直接进行数值比较,所以比较结果为 true

第三个比较 i1 == i3,由于 i1Integer 对象,而 i3 是基本类型的 int,在比较时会进行自动拆箱,将 i1 转换为 int 类型。然后进行数值比较,所以比较结果为 true

总结起来,i1 == i2 的结果是 falsei3 == i4i1 == i3 的结果都是 true。这是因为 Integer 对象的比较是通过自动拆箱后的数值比较来实现的。

阅读代码,分析运行结果:

1
2
3
4
5
6
7
8
9
public static void main(String[] args) {
double a = 2.0;
double b = 2.0;
Double c = 2.0;
Double d = 2.0;
System.out.println(a == b);
System.out.println(c == d);
System.out.println(a == d);
}

这段代码涉及到了Java中的自动装箱和拆箱的概念,和包装类的缓存机制。Double和Float包装类没有缓存对象。所以(a==b)结果为true,(c==d)结果为false,(a==d)结果为true。

路由设置路径中出现大写,浏览器将路径自动转换为小写导致跳过路由守卫

1
2
3
4
5
6
7
8
let router = createRouter({
history:createWebHashHistory(),

routes:[
{path:'/ShowSchedule',component:ShowSchedule},
{path:'/Login',component:Login},
]
})

浏览器输入URL:view-source:localhost:5174/#/showschedule路径自动全部转换为小写导致跳过了路由守卫,建议以后Path全小写,component首字母大写。

由于文件没有被设置成Resoureces Root导致的空指针异常

在前端输入信息后,点击提交按钮,控制台报错:

1
java.lang.NullPointerException: inStream parameter is null

检查后发现输入流没有读取到properties文件,将类加载器改为当前类的加载器(当前类为JDBCTools.class),将properties文件移入到resources文件夹目录下(一定记得将resources文件夹右键Mark Directory as Resources Root),该问题解决。以下是Java代码:

1
pro.load((JDBCTools.class.getClassLoader().getResourceAsStream("db.properties")));

泛型使用

从前端获取的数据用UserDao工具类的getBean语句在数据库内查询,但是返回值是Object类。

通过在BaseBao后面加泛型可以获取查询到对象是User对象,代码如下:

1
2
public class UserDao extends BaseDao<User>
User user = this.getBean(User.class,"select password from userreg where id = ?" ,id);

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class test1 {
public static void main(String[] args) {
Employee[] employees = new Employee[3];
System.out.println("-----------------" + "添加第1个员工" + "--------");
Scanner sc = new Scanner(System.in);
for (int i = 0; i < 3; i++) {
int[] arr = new int[3];
employees[i].setName(sc.next());
employees[i].setGender(sc.next().charAt(0));
employees[i].setAge(sc.nextInt());
employees[i].setSalary(sc.nextDouble());
employees[i].setPhoneNumber(sc.next());
employees[i].setEmail(sc.next());
}
System.out.println("--------------"+"添加完成"+"--------");
System.out.println("--------------"+"员工列表"+"--------");
for (int i = 0; i < 3; i++) {
System.out.println(employees[i].toString());
System.out.println("--------------------------------");
}
System.out.println("--------------"+"员工列表完成"+"--------");
}
}

张三(控制台输入)
报错:

1
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "src.com.simonf.Employee.setName(String)" because "employees[i]" is null at src.com.simonf.test1.main(test1.java:11

这段代码报错的原因是在循环中没有为 employees[i] 分配内存空间,导致其为 null。因此,在使用 employees[i] 的方法时会出现NullPointerException。

要解决这个问题,你需要为 employees[i] 分配内存空间,即创建 Employee 对象并将其赋值给 employees[i]。可以在循环之前为每个employees[i] 创建一个新的 Employee 对象,如下所示:

1
2
3
4
5
6
7
8
9
for (int i = 0; i < 3; i++) {  
employees[i] = new Employee(); // 创建新的 Employee 对象
employees[i].setName(sc.next());
employees[i].setGender(sc.next().charAt(0));
employees[i].setAge(sc.nextInt());
employees[i].setSalary(sc.nextDouble());
employees[i].setPhoneNumber(sc.next());
employees[i].setEmail(sc.next());
}

note1

以下合法的标识符是?(A,E,F,G)
A.flag_3
B.my code
C.discount%
D.234rate
E.$name
F.println
G.main

解释:Java的关键字不能用做标识符,但是println,main不属于标识符(标识符一般在编辑器中会被高亮成紫色)。

note2

标识符的命名规范
1)见名知意

(2)类名、接口名等:每个单词的首字母都大写(大驼峰法则),形式:XxxYyyZzz,

例如:HelloWorld,String,System等

(3)变量、方法名等:从第二个单词开始首字母大写(小驼峰法则),其余字母小写,形式:xxxYyyZzz,

例如:age,name,bookName,main

(4)包名等:每一个单词都小写,单词之间使用点.分割,形式:xxx.yyy.zzz,

例如:java.lang

(5)常量名等:每一个单词都大写,单词之间使用下划线_分割,形式:XXX_YYY_ZZZ,

例如:MAX_VALUE,PI

note3

Java报错: 类class 是公共的, 应在名为 .java 的文件中声明
原因:public修饰的class必须与.java的文件名一致

note4

分析以下代码:
short s1 = 120;
short s2 = 8;
byte s3 = (byte)(s1+s2);

输出s3结果为-128(两个short做运算结果是int)。因为s1+s2=128。int(占四个字节,32个2进制位) 128的二进制为00000000 00000000 00000000 10000000,若要转换成byte(1个字节,8个二进制位)则为10000000(-128),因为有符号二进制存储第一个数为1,代表负数,先求10000000的反码为01111111,在加1得到10000000,该数的十进制数为128。则s3对应的数为-128。
同理 byte b = (byte)129;
b的输出值为-127,因为int 129的二进制数为00000000 00000000 00000000 10000001,转换为byte类型,其二进制数为10000001,第一个数字为1表明该二进制数对应的十进制数字为负数,按照负数的转换规则,先求10000001的反码为01111110,再加1,得到01111111,该数为127,则b的值为-127。

note5

byte b1 = 3;
byte b2 = 4;
byte b3 = b1 + b2;
byte b4 = 3 + 4;
运行以上的代码,b3会报错,因为b1 + b2默认是int类型,(byte,short,char运算的值默认为int类型),存储范围大的变量不能直接赋值给存储范围小的变量。
b4不会报错,因为3和4是两个常量值,会自动运算为7,与变量不一样,可以直接赋值给b3。

note6

int i = 10;
i = i++;
System.out.println(“i =”+i);
输出结果i = 10,因为i的值一开始为10,先把10这个值取出来找个地方存储,然后i++,i的值变为11,再把10这个值赋给等号左侧的i,所以i的输出值为10。

note7

分析以下代码:
byte b= 127;
b = b + 1;
b += 1;

b = b + 1; 这行代码会报错,因为b + 1的运算结果是int类型,不能赋值给byte类型的b。但是如果运行 b += 1则可以运行,但是输出的结果b = -128。因为 += 操作实际上进行了强制类型转换,但是byte的取值范围为-128到127,强制类型转换为byte就导致128变成了-128。

同理
int i = 1;
i *=0.1;
System.out.println(i);
输出的结果为i = 0,因为i *= 0.1;实际上的操作为i = (int) i * 0.1; 即(int)1 * 0.1 = 0;

note8

右移运算符 :一个数右移n位,结果为这个数除以2的n次幂,除不尽向下取整。
System.out.println(-9>>2);输出的数为-3,因为-9/2^2 = -2.25,向下取整得到的就是-3。
System.out.println(-9>>>2);输出的数为1073741821,因为>>>为无符号右移,最高为补0,结果为正数,该数的求法是通过算补码得到的。
System.out.println(3<<33);输出的数为6,因为int类型4个字节,32位,移动33位相当于移动一位。每超过32位,得到的数相当于3 * 2^(33%32) 。

note9

int x = 5;
int y = 7;
x = x^y;
y = x^y;
x = x^y;
System.out.println(x);
System.out.println(y);
该运算操作相当于交换两个变量的值。

note10

数组的声明:
数据类型[] 数组名;
数组的几种定义方式:

  1. 静态初始化:
    数据类型[] 数组名 = new 数据类型[]{元素1,元素2,…};
    数据类型[] 数组名 = {元素1,元素2,…};
  2. 动态初始化:
    数据类型[] 数组名 = new 数据类型[长度];

note11

Java虚拟机的内存划分
常用到的三部分:方法区、堆内存、虚拟机栈。
方法区:存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
堆内存:存储对象(包括数组对象),new来创建的,都存储在堆内存。
虚拟机栈:用于存储正在执行的每个Java方法的局部变量表等。
局部变量表存放了编译期可知长度的各种基本数据类型、对象引用,方法执行完,自动释放。