Star's Blog

Keep learning, Keep improving


  • 首页

  • 分类

  • 关于

  • 标签

  • 归档

  • 搜索

Java虚拟机的类加载机制2

发表于 2019-12-07 | 分类于 Java虚拟机

概念

类加载器把class文件中的二进制数据读入到内存中,存放在方法区,然后在堆区创建一个java.lang.Class对象,用来封装类在方法区内的数据结构。

加载:

查找并加载类的二进制数据(把class文件里面的信息加载到内存里面)

连接:

把内存中类的二进制数据合并到虚拟机的运行时环境中

  1. 验证:确保被加载的类的正确性,包括:
    1. 类文件的结构检查:检查是否满足Java类文件的固定格式
    2. 语义检查:确保类本身符合Java的语法规范
    3. 字节码验证:确保字节码流可以被Java虚拟机安全的执行。字节码流是操作码组成的序列。每一个操作码后面都会跟着一个或者多个操作数。字节码检查这个步骤会检查每一个操作码是否合法。
    4. 二进制兼容性验证:确保相互引用的类之间是协调一致的。
  2. 准备:为类的静态变量分配内存,并将其初始化为默认值
  3. 解析:把类中的符号引用转化为直接引用(比如说方法的符号引用,是由方法名和相关描述符组成。在解析阶段,JVM把符号引用替换成一个指针,这个指针就是直接引用,它指向该类的该方法在方法区中的内存位置)

初始化:

为类的静态变量赋予正确的初始值。当静态变量的等号右边的值是一个常量表达式时,不会调用static代码块进行初始化。只有等号右边的值是一个运行时运算出来的值,才会调用static初始化。

双亲委派模型

1、将类加载请求向上传递。当一个类加载器收到类加载请求时,它首先不会自己去加载这个类的信息,而是把该请求转发给父类加载器,依次向上。

所以所有的类加载请求都会被传递到父类加载器中,只有当父类加载器中没有找到所需的类,子类加载器才会自己尝试去加载该类。

当前类加载器和所有父类加载器都无法加载该类时,抛出ClassNotFindException异常。

2、意义:提高系统的安全性。用户自定义的类加载器不可能加载应该由父加载器加载的可靠类。

比如用户定义了一个恶意代码,自定义的类加载器首先让系统加载器去加载,系统加载器检查该代码不符合规范,于是就不继续加载了。

3、定义类加载器:如果某个类加载器能够加载一个类,那么这个类加载器就叫做定义类加载器

4、初始类加载器:定义类加载器及其所有子加载器都称作初始类加载器。

5、运行时包:

  • 由同一个类加载器加载并且拥有相同包名的类组成运行时包
  • 只有属于同一个运行时包的类,才能访问包可见(default)的类和类成员。
  • 其作用是限制用户自定义的类冒充核心类库的类去访问核心类库的包可见成员

6、加载两份相同class对象的情况:A 和 B 不属于父子类加载器关系,并且各自都加载了同一个类。

特点:

  1. 全盘负责:当一个类加载器加载一个类时,该类所依赖的其他类也会被这个类加载器加载到内存中。
  2. 缓存机制:所有的Class对象都会被缓存,当程序需要使用某个Class时,类加载器先从缓存中查找,找不到,才从class文件中读取数据,转化成Class对象,存入缓存中。

两种类型的类加载器

JVM自带的类加载器(3种)

(1) 根类加载器/启动类加载器(Bootstrap):

1
2
3
a、C++编写的,程序员无法在程序中获取该类
b、负责加载虚拟机的核心库,比如java.lang.Object
c、没有继承ClassLoader类

(2) 扩展类加载器(Extension):

1
2
3
4
a、Java编写的,从指定目录中加载类库
b、父加载器是根类加载器
c、是ClassLoader的子类
d、如果用户把创建的jar文件放到指定目录中,也会被扩展加载器加载。

(3) 系统加载器(System)或者应用类加载器(App):

1
2
3
4
5
a、Java编写的
b、父加载器是扩展类加载器
c、从环境变量或者class.path中加载类
d、是用户自定义类加载的默认父加载器
e、是ClassLoader的子类

用户自定义的类加载器:

(1)Java.lang.ClassLoader类的子类
(2)用户可以定制类的加载方式
(3)父类加载器是系统加载器
(4)编写步骤:

1
2
A、继承ClassLoader
B、重写findClass方法。从特定位置加载class文件,得到字节数组,然后利用defineClass把字节数组转化为Class对象

(5)为什么要自定义类加载器?

  1. 可以从指定位置加载class文件,比如说从数据库、云端加载class文件
  2. 加密:Java代码可以被轻易的反编译,因此,如果需要对代码进行加密,那么加密以后的代码,就不能使用Java自带的ClassLoader来加载这个类了,需要自定义ClassLoader,对这个类进行解密,然后加载。
阅读全文 »

如何在Spring-Boot中做参数校验?

发表于 2019-12-07 | 分类于 Spring

数据的校验的重要性就不用说了,即使在前端对数据进行校验的情况下,我们还是要对传入后端的数据再进行一遍校验,避免用户绕过浏览器直接通过一些 HTTP 工具直接向后端请求一些违法数据。

我个人觉得这个和统一异常处理一样是后端很容易做好的一件事情,同时也是很有必要的事情。如果对后端如何统一异常处理不太清楚的朋友,也可以留言一下,我后面会分享自己在项目中学到的统一异常处理的方法。

本文结合自己在项目中的实际使用经验,可以说文章介绍的内容很实用,不了解的朋友可以学习一下,后面可以立马实践到项目上去。

下面我会通过实例程序演示如何在 Java 程序中尤其是 Spring 程序中优雅地的进行参数验证。

基础设施搭建

相关依赖

如果开发普通 Java 程序的的话,你需要可能需要像下面这样依赖:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
<version>6.0.9.Final</version>
</dependency>
<dependency>
<groupId>javax.el</groupId>
<artifactId>javax.el-api</artifactId>
<version>3.0.0</version>
</dependency>
<dependency>
<groupId>org.glassfish.web</groupId>
<artifactId>javax.el</artifactId>
<version>2.2.6</version>
</dependency>

使用 Spring Boot 程序的话只需要spring-boot-starter-web 就够了,它的子依赖包含了我们所需要的东西。除了这个依赖,下面的演示还用到了 lombok ,所以不要忘记添加上相关依赖。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

实体类

下面这个是示例用到的实体类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Person {

@NotNull(message = "classId 不能为空")
private String classId;

@Size(max = 33)
@NotNull(message = "name 不能为空")
private String name;

@Pattern(regexp = "((^Man$|^Woman$|^UGM$))", message = "sex 值不在可选范围")
@NotNull(message = "sex 不能为空")
private String sex;

@Email(message = "email 格式不正确")
@NotNull(message = "email 不能为空")
private String email;

}

正则表达式说明:

1
2
3
4
5
> - ^string : 匹配以 string 开头的字符串
> - string$ :匹配以 string 结尾的字符串
> - ^string$ :精确匹配 string 字符串
> - ((^Man$|^Woman$|^UGM$)) : 值只能在 Man,Woman,UGM 这三个值中选择
>

下面这部分校验注解说明内容参考自:https://www.cnkirito.moe/spring-validation/ ,感谢@徐靖峰。

JSR提供的校验注解:

  • @Null 被注释的元素必须为 null
  • @NotNull 被注释的元素必须不为 null
  • @AssertTrue 被注释的元素必须为 true
  • @AssertFalse 被注释的元素必须为 false
  • @Min(value) 被注释的元素必须是一个数字,其值必须大于等于指定的最小值
  • @Max(value) 被注释的元素必须是一个数字,其值必须小于等于指定的最大值
  • @DecimalMin(value) 被注释的元素必须是一个数字,其值必须大于等于指定的最小值
  • @DecimalMax(value) 被注释的元素必须是一个数字,其值必须小于等于指定的最大值
  • @Size(max=, min=) 被注释的元素的大小必须在指定的范围内
  • @Digits (integer, fraction) 被注释的元素必须是一个数字,其值必须在可接受的范围内
  • @Past 被注释的元素必须是一个过去的日期
  • @Future 被注释的元素必须是一个将来的日期
  • @Pattern(regex=,flag=) 被注释的元素必须符合指定的正则表达式

Hibernate Validator提供的校验注解:

  • @NotBlank(message =) 验证字符串非null,且长度必须大于0
  • @Email 被注释的元素必须是电子邮箱地址
  • @Length(min=,max=) 被注释的字符串的大小必须在指定的范围内
  • @NotEmpty 被注释的字符串的必须非空
  • @Range(min=,max=,message=) 被注释的元素必须在合适的范围内

验证Controller的输入

验证请求体(RequestBody)

Controller:

我们在需要验证的参数上加上了@Valid注解,如果验证失败,它将抛出MethodArgumentNotValidException。默认情况下,Spring会将此异常转换为HTTP Status 400(错误请求)。

1
2
3
4
5
6
7
8
9
@RestController
@RequestMapping("/api")
public class PersonController {

@PostMapping("/person")
public ResponseEntity<Person> getPerson(@RequestBody @Valid Person person) {
return ResponseEntity.ok().body(person);
}
}

ExceptionHandler:

自定义异常处理器可以帮助我们捕获异常,并进行一些简单的处理。如果对于下面的处理异常的代码不太理解的话,可以查看这篇文章 《SpringBoot 处理异常的几种常见姿势》。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@ControllerAdvice(assignableTypes = {PersonController.class})
public class GlobalExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Map<String, String>> handleValidationExceptions(
MethodArgumentNotValidException ex) {
Map<String, String> errors = new HashMap<>();
ex.getBindingResult().getAllErrors().forEach((error) -> {
String fieldName = ((FieldError) error).getField();
String errorMessage = error.getDefaultMessage();
errors.put(fieldName, errorMessage);
});
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errors);
}
}
阅读全文 »

在浏览器输入 URL 回车之后发生了什么?

发表于 2019-10-01 | 分类于 网络

前言

这个问题已经是老生常谈了,更是经常被作为面试的压轴题出现,网上也有很多文章,但最近闲的无聊,然后就自己做了一篇笔记,感觉比之前理解更透彻了。

这篇笔记是我这两天看了数十篇文章总结出来的,所以相对全面一点,但由于我是做前端的,所以会比较重点分析浏览器渲染页面那一部分,至于其他部分我会罗列出关键词,感兴趣的可以自行查阅.

注意:本文的步骤是建立在,请求的是一个简单的 HTTP 请求,没有 HTTPS、HTTP2、最简单的 DNS、没有代理、并且服务器没有任何问题的基础上,尽管这是不切实际的。

大致流程

  1. URL 解析
  2. DNS 查询
  3. TCP 连接
  4. 处理请求
  5. 接受响应
  6. 渲染页面

URL 解析

地址解析:

首先判断你输入的是一个合法的 URL 还是一个待搜索的关键词,并且根据你输入的内容进行自动完成、字符编码等操作。

HSTS

由于安全隐患,会使用 HSTS 强制客户端使用 HTTPS 访问页面。详见:你所不知道的 HSTS[1]。

其他操作

浏览器还会进行一些额外的操作,比如安全检查、访问限制(之前国产浏览器限制 996.icu)。

检查缓存

DNS 查询

基本步骤

1. 浏览器缓存

浏览器会先检查是否在缓存中,没有则调用系统库函数进行查询。

2. 操作系统缓存

操作系统也有自己的 DNS缓存,但在这之前,会向检查域名是否存在本地的 Hosts 文件里,没有则向 DNS 服务器发送查询请求。

3. 路由器缓存

路由器也有自己的缓存。

4. ISP DNS 缓存

ISP DNS 就是在客户端电脑上设置的首选 DNS 服务器,它们在大多数情况下都会有缓存。

根域名服务器查询

在前面所有步骤没有缓存的情况下,本地 DNS 服务器会将请求转发到互联网上的根域,下面这个图很好的诠释了整个流程:

需要注意的点

  1. 递归方式:一路查下去中间不返回,得到最终结果才返回信息(浏览器到本地DNS服务器的过程)
  2. 迭代方式,就是本地DNS服务器到根域名服务器查询的方式。
  3. 什么是 DNS 劫持
  4. 前端 dns-prefetch 优化

TCP 连接

TCP/IP 分为四层,在发送数据时,每层都要对数据进行封装:

应用层:发送 HTTP 请求

在前面的步骤我们已经得到服务器的 IP 地址,浏览器会开始构造一个 HTTP 报文,其中包括:

  • 请求报头(Request Header):请求方法、目标地址、遵循的协议等等
  • 请求主体(其他参数)

其中需要注意的点:

  • 浏览器只能发送 GET、POST 方法,而打开网页使用的是 GET 方法

传输层:TCP 传输报文

传输层会发起一条到达服务器的 TCP 连接,为了方便传输,会对数据进行分割(以报文段为单位),并标记编号,方便服务器接受时能够准确地还原报文信息。

在建立连接前,会先进行 TCP 三次握手。

关于 TCP/IP 三次握手,网上已经有很多段子和图片生动地描述了。

相关知识点:

  1. SYN 泛洪攻击

网络层:IP协议查询Mac地址

将数据段打包,并加入源及目标的IP地址,并且负责寻找传输路线。

判断目标地址是否与当前地址处于同一网络中,是的话直接根据 Mac 地址发送,否则使用路由表查找下一跳地址,以及使用 ARP 协议查询它的 Mac 地址。

注意:在 OSI 参考模型中 ARP 协议位于链路层,但在 TCP/IP 中,它位于网络层。

链路层:以太网协议

以太网协议

根据以太网协议将数据分为以“帧”为单位的数据包,每一帧分为两个部分:

  • 标头:数据包的发送者、接受者、数据类型
  • 数据:数据包具体内容

Mac 地址

以太网规定了连入网络的所有设备都必须具备“网卡”接口,数据包都是从一块网卡传递到另一块网卡,网卡的地址就是 Mac 地址。每一个 Mac 地址都是独一无二的,具备了一对一的能力。

阅读全文 »

new一个对象的过程中发生了什么?

发表于 2019-10-01 | 分类于 Java虚拟机

java在new一个对象的时候,会先查看对象所属的类有没有被加载到内存,如果没有的话,就会先通过类的全限定名来加载。加载并初始化类完成后,再进行对象的创建工作。

我们先假设是第一次使用该类,这样的话new一个对象就可以分为两个过程:加载并初始化类和创建对象。

类加载过程(第一次使用该类)

java是使用双亲委派模型来进行类的加载的,所以在描述类加载过程前,我们先看一下它的工作过程:

双亲委托模型的工作过程是:如果一个类加载器(ClassLoader)收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委托给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父类加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需要加载的类)时,子加载器才会尝试自己去加载。

使用双亲委托机制的好处是:能够有效确保一个类的全局唯一性,当程序中出现多个限定名相同的类时,类加载器在执行加载时,始终只会加载其中的某一个类。

加载

由类加载器负责根据一个类的全限定名来读取此类的二进制字节流到JVM内部,并存储在运行时内存区的方法区,然后将其转换为一个与目标类型对应的java.lang.Class对象实例

验证

格式验证:验证是否符合class文件规范

语义验证:检查一个被标记为final的类型是否包含子类;检查一个类中的final方法是否被子类进行重写;确保父类和子类之间没有不兼容的一些方法声明(比如方法签名相同,但方法的返回值不同)

操作验证:在操作数栈中的数据必须进行正确的操作,对常量池中的各种符号引用执行验证(通常在解析阶段执行,检查是否可以通过符号引用中描述的全限定名定位到指定类型上,以及类成员信息的访问修饰符是否允许访问等)

准备

为类中的所有静态变量分配内存空间,并为其设置一个初始值(由于还没有产生对象,实例变量不在此操作范围内)

被final修饰的static变量(常量),会直接赋值;

解析

将常量池中的符号引用转为直接引用(得到类或者字段、方法在内存中的指针或者偏移量,以便直接调用该方法),这个可以在初始化之后再执行。
解析需要静态绑定的内容。 // 所有不会被重写的方法和域都会被静态绑定

以上2、3、4三个阶段又合称为链接阶段,链接阶段要做的是将加载到JVM中的二进制字节流的类数据信息合并到JVM的运行时状态中。

初始化(先父后子)

  • 1 为静态变量赋值
  • 2 执行static代码块

注意:static代码块只有jvm能够调用

如果是多线程需要同时初始化一个类,仅仅只能允许其中一个线程对其执行初始化操作,其余线程必须等待,只有在活动线程执行完对类的初始化操作之后,才会通知正在等待的其他线程。

因为子类存在对父类的依赖,所以类的加载顺序是先加载父类后加载子类,初始化也一样。不过,父类初始化时,子类静态变量的值也有有的,是默认值。

最终,方法区会存储当前类类信息,包括类的静态变量、类初始化代码(定义静态变量时的赋值语句 和 静态初始化代码块)、实例变量定义、实例初始化代码(定义实例变量时的赋值语句实例代码块和构造方法)和实例方法,还有父类的类信息引用。

创建对象

在堆区分配对象需要的内存

分配的内存包括本类和父类的所有实例变量,但不包括任何静态变量

对所有实例变量赋默认值

将方法区内对实例变量的定义拷贝一份到堆区,然后赋默认值

执行实例初始化代码

初始化顺序是先初始化父类再初始化子类,初始化时先执行实例代码块然后是构造方法

如果有类似于Child c = new Child()形式的c引用的话,在栈区定义Child类型引用变量c,然后将堆区对象的地址赋值给它

需要注意的是,每个子类对象持有父类对象的引用,可在内部通过super关键字来调用父类对象,但在外部不可访问

补充:

通过实例引用调用实例方法的时候,先从方法区中对象的实际类型信息找,找不到的话再去父类类型信息中找。

如果继承的层次比较深,要调用的方法位于比较上层的父类,则调用的效率是比较低的,因为每次调用都要经过很多次查找。这时候大多系统会采用一种称为虚方法表的方法来优化调用的效率。

所谓虚方法表,就是在类加载的时候,为每个类创建一个表,这个表包括该类的对象所有动态绑定的方法及其地址,包括父类的方法,但一个方法只有一条记录,子类重写了父类方法后只会保留子类的。当通过对象动态绑定方法的时候,只需要查找这个表就可以了,而不需要挨个查找每个父类。

参考

原文出处:https://www.cnblogs.com/JackPn/p/9386182.html

Java8的Stream集合操作

发表于 2019-09-07 | 分类于 Java

简介

java8也出来好久了,接口默认方法,lambda表达式,函数式接口,Date API等特性还是有必要去了解一下。比如在项目中经常用到集合,遍历集合可以试下lambda表达式,经常还要对集合进行过滤和排序,Stream就派上用场了。用习惯了,不得不说真的很好用。
Stream作为java8的新特性,基于lambda表达式,是对集合对象功能的增强,它专注于对集合对象进行各种高效、便利的聚合操作或者大批量的数据操作,提高了编程效率和代码可读性。
Stream的原理:将要处理的元素看做一种流,流在管道中传输,并且可以在管道的节点上处理,包括过滤筛选、去重、排序、聚合等。元素流在管道中经过中间操作的处理,最后由最终操作得到前面处理的结果。
集合有两种方式生成流:

  • stream() − 为集合创建串行流
  • parallelStream() - 为集合创建并行流

上图中是Stream类的类结构图,里面包含了大部分的中间和终止操作。

  • 中间操作主要有以下方法(此类型方法返回的都是Stream):map (mapToInt, flatMap 等)、 filter、 distinct、 sorted、 peek、 limit、 skip、 parallel、 sequential、 unordered
  • 终止操作主要有以下方法:forEach、 forEachOrdered、 toArray、 reduce、 collect、 min、 max、 count、 anyMatch、 allMatch、 noneMatch、 findFirst、 findAny、 iterator

举例说明

首先为了说明Stream对对象集合的操作,新建一个Student类(学生类),覆写了equals()和hashCode()方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
public class Student {

private Long id;

private String name;

private int age;

private String address;

public Student() {}

public Student(Long id, String name, int age, String address) {
this.id = id;
this.name = name;
this.age = age;
this.address = address;
}

@Override
public String toString() {
return "Student{" +
"id=" + id +
", name='" + name + '\'' +
", age=" + age +
", address='" + address + '\'' +
'}';
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Student student = (Student) o;
return age == student.age &&
Objects.equals(id, student.id) &&
Objects.equals(name, student.name) &&
Objects.equals(address, student.address);
}

@Override
public int hashCode() {
return Objects.hash(id, name, age, address);
}

public Long getId() {
return id;
}

public void setId(Long id) {
this.id = id;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}

public String getAddress() {
return address;
}

public void setAddress(String address) {
this.address = address;
}

}

filter(筛选)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public static void main(String [] args) {

Student s1 = new Student(1L, "肖战", 15, "浙江");
Student s2 = new Student(2L, "王一博", 15, "湖北");
Student s3 = new Student(3L, "杨紫", 17, "北京");
Student s4 = new Student(4L, "李现", 17, "浙江");
List<Student> students = new ArrayList<>();
students.add(s1);
students.add(s2);
students.add(s3);
students.add(s4);

List<Student> streamStudents = testFilter(students);
streamStudents.forEach(System.out::println);
}
/**
* 集合的筛选
* @param students
* @return
*/
private static List<Student> testFilter(List<Student> students) {
//筛选年龄大于15岁的学生
// return students.stream().filter(s -> s.getAge()>15).collect(Collectors.toList());
//筛选住在浙江省的学生
return students.stream().filter(s ->"浙江".equals(s.getAddress())).collect(Collectors.toList());
}

运行结果:

这里我们创建了四个学生,经过filter的筛选,筛选出地址是浙江的学生集合。

阅读全文 »
1234…20
Morning Star

Morning Star

100 日志
14 分类
37 标签
GitHub
© 2021 Morning Star
由 Hexo 强力驱动
|
主题 — NexT.Pisces v5.1.4