Dawn's Blogs

分享技术 记录成长

0%

Interceptor

Interceptor 是 Spring MVC 框架中的一个组件,用于拦截和处理HTTP请求。在 Java Spring 中,Interceptor 是通过实现 HandlerInterceptor 接口来实现的。Interceptor 可以对请求进行预处理,也可以对响应进行后处理。Interceptor 的主要作用是在请求到达 Controller 之前或响应离开 Controller 之后,对请求和响应进行处理。

使用开发方法

使用方法就是:定义拦截器类,实现 HandlerInterceptor 接口,并重写其中的方法。

  • preHandle:在调用 Handler 之前执行,返回值决定了是否会调用 Handler。如果返回值为 false,不会执行 Handler,也不会执行下面的两个方法。
  • postHandle:在 Handler 执行完成之后执行。
  • afterCompletion:在 DispatcherServlet 进行视图的渲染之后执行,多用于清理资源。

和 Filter 的区别

Interceptor 和 Filter 都可以拦截和处理请求,但是有以下区别:

  1. 实现方式。Filter 是通过实现 javax.servlet.Filter 接口来实现的,而 Interceptor 是通过实现 HandlerInterceptor 接口来实现的。因此,Filter 依赖于 Servlet API,而 Interceptor 依赖于 Spring MVC 框架。
  2. 使用范围。Filter 不仅可以应用于 Web 应用程序,还可以应用于其他基于 Java 的应用程序,如 Java SE、Java EE 等。而 Interceptor 仅适用于基于 Spring MVC 框架的 Web 应用程序。
  3. 生命周期。Filter 的生命周期比 Interceptor 更加灵活。Filter 可以在请求到达 Controller 之前或响应离开 Controller 之后的任何阶段进行拦截和处理。而 Interceptor 的拦截和处理只能在 Controller 方法执行之前或之后进行。

统一异常处理

统一的异常处理方式,就是定义 ExceptionHandler。具体来说,就是使用 @ControllerAdvice 和 @ExceptionHandler 两个注解。

这种异常处理下,会给所有或者指定的 Controller 织入异常处理的逻辑(AOP)。当 Controller 方法抛出异常后,由被 @ExceptionHandler 修饰的方法进行处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@ControllerAdvice
@ResponseBody
public class GlobalExceptionHandler {

@ExceptionHandler(BaseException.class)
public ResponseEntity<?> handleAppException(BaseException ex, HttpServletRequest request) {
//......
}

@ExceptionHandler(value = ResourceNotFoundException.class)
public ResponseEntity<ErrorReponse> handleResourceNotFoundException(ResourceNotFoundException ex, HttpServletRequest request) {
//......
}
}

原理

ExceptionHandlerMethodResolver 中 getMappedMethod 方法决定了异常具体被哪个被 @ExceptionHandler 注解修饰的方法处理异常。

具体来说,就是首先找到可以匹配处理异常的所有方法信息,然后对其进行从小到大的排序,最后取最小的那一个匹配的方法(匹配程度最高的那一个)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Nullable
private Method getMappedMethod(Class<? extends Throwable> exceptionType) {
List<Class<? extends Throwable>> matches = new ArrayList<>();
//找到可以处理的所有异常信息。mappedMethods 中存放了异常和处理异常的方法的对应关系
for (Class<? extends Throwable> mappedException : this.mappedMethods.keySet()) {
if (mappedException.isAssignableFrom(exceptionType)) {
matches.add(mappedException);
}
}
// 不为空说明有方法处理异常
if (!matches.isEmpty()) {
// 按照匹配程度从小到大排序
matches.sort(new ExceptionDepthComparator(exceptionType));
// 返回处理异常的方法
return this.mappedMethods.get(matches.get(0));
}
else {
return null;
}
}

请求

请求路径映射

通过 @RequestMapping 注解实现请求路径的映射。

请求参数

默认情况下 Controller 方法的参数名,就是请求参数的映射名字。

  • 也可以通过 @RequestParam(用于 URL 或者表单)和 @RequestBody(用于接收 json 数据)注解,指定请求参数。
  • 通过 @DateTimeFormat 注解,指定日期格式。
  • 通过 @PathVariable 注解,指定路径参数(@RequestMapping 中使用 {id} 指定路径参数)

响应

使用 @RequestBody 注解,设置 Controller 的返回值作为响应体。

  • 方法的返回值为字符串,会将其作为文本内容直接响应给前端。
  • 方法的返回值为对象,会将对象转换成 Json 文本响应给前端。

如果不适用 @RequestBody,返回的字符串代表 HTML 文件名,会返回对应的 HTML 文件。

介绍

SpringMVC 是一个轻量级的 MVC 框架,可以帮助我们进行更简洁的 Web 层的开发,并且它天生与 Spring 框架集成。

  • M(Model):数据层,也就是 dao 和 bean。
  • V(View):视图层,用于展示模型中的数据。
  • C(Controller):接受用户请求,并将请求发送至 Model,最后返回数据给请求方。

组件

SpringMVC 的核心组件包括:

  • DispatcherServlet:中央处理器,负责接收请求、分发,并给予客户端响应。
  • HandlerMapping:Handler 映射器,根据 URL 去匹配查找能处理的 Handler,并会将请求涉及到的拦截器和 Handler 一起封装。
  • HandlerAdapter:Handler 适配器,根据 HandlerMapping 找到的 Handler,适配器负责 Handler 的执行。
  • Handler:实际处理请求的组件。
  • ViewResolver:视图解析器,解析并渲染真正的视图,并传递给 DispatcherServlet 用于响应。

工作原理

SpringMVC 的工作流程如下:

  1. 客户端(浏览器)发送请求, DispatcherServlet 拦截请求
  2. DispatcherServlet 根据请求信息调用 HandlerMapping。HandlerMapping 根据 URL 去匹配查找能处理的 Handler(也就是 Controller 控制器) ,并会将请求涉及到的拦截器和 Handler 一起封装。
  3. DispatcherServlet 调用 HandlerAdapter 适配器执行 Handler
  4. Handler 完成对用户请求的处理后,会返回一个 ModelAndView 对象给 DispatcherServlet,包含了数据模型以及相应的视图的信息。Model 是返回的数据对象,View 是个逻辑上的 View。
  5. ViewResolver 会根据逻辑 View 查找实际的 View
  6. DispaterServlet 把返回的 Model 传给 View(视图渲染)。
  7. 把 View 返回给请求者(浏览器)。

img

本文总结 Spring 用到的一些设计模式。

工厂模式

Spring 可以通过 BeanFactory 或 ApplicationContext 创建 bean 对象,ApplicationContext 继承自 BeanFactory。

  • BeanFactory:延迟注入,所以占用更少的内存,程序启动速度更快。
  • ApplicationContext:容器启动的时候,不管你用没用到,一次性创建所有 bean 。并且 ApplicationContext 继承自 BeanFactory,扩展了功能。

ApplicationContext 有但不限于以下实现类:

  • ClassPathXmlApplicationContext:从 ClassPath 中加载 XML 配置文件,创建 ApplicationContext。
  • FileSystemXmlApplicationContext:从文件系统中加载 XML 配置文件,创建 ApplicationContext。
  • XmlWebApplicationContext:从 Web 系统中加载 XML 配置文件,创建 ApplicationContext。
  • AnnotationConfigApplicationContext:通过注解配置类,创建 ApplicationContext。

单例模式

bean 的作用域默认就是单例模式的,Spring 通过 ConcurrentHashMap 作为单例注册表来实现单例模式,key 为 bean name,value 为单例对象。

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
// 通过 ConcurrentHashMap(线程安全) 实现单例注册表
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<String, Object>(64);

public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) {
Assert.notNull(beanName, "'beanName' must not be null");
synchronized (this.singletonObjects) {
// 检查缓存中是否存在实例
Object singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null) {
//...省略了很多代码
try {
singletonObject = singletonFactory.getObject();
}
//...省略了很多代码
// 如果实例对象在不存在,我们注册到单例注册表中。
addSingleton(beanName, singletonObject);
}
return (singletonObject != NULL_OBJECT ? singletonObject : null);
}
}
//将对象添加到单例注册表
protected void addSingleton(String beanName, Object singletonObject) {
synchronized (this.singletonObjects) {
this.singletonObjects.put(beanName, (singletonObject != null ? singletonObject : NULL_OBJECT));

}
}
}

代理模式

Spring AOP 就是基于动态代理的,Spring AOP 根据被代理对象是否有实现接口,分别使用 JDK Proxy 和 CGLIB 实现动态代理。

模板方法

TransactionTemplate.execute 使用到了模板方法,实现事务。Spring 使用 Callback 模式与模板方法模式配合,既达到了代码复用的效果,同时增加了灵活性。

以 Template 结尾的对数据库操作的类,都使用到了模板方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Autowired
private TransactionTemplate transactionTemplate;
public void testTransaction() {

transactionTemplate.execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus transactionStatus) {

try {

// .... 业务代码
} catch (Exception e){
//回滚
transactionStatus.setRollbackOnly();
}

}
});
}

观察者模式

观察者模式是一种对象行为型模式。它表示的是一种对象与对象之间具有依赖关系,当一个对象发生改变的时候,依赖这个对象的所有对象也会做出反应。Spring 事件驱动模型就是观察者模式很经典的一个应用。Spring 事件驱动模型非常有用,在很多场景都可以解耦代码。比如我们每次添加商品的时候都需要重新更新商品索引,这个时候就可以利用观察者模式来解决这个问题。

Spring 事件驱动模型

Spring 事件驱动模型有三种角色:

  • 事件:ApplicationEvent 是一个抽象类,继承自 java.util.EventObject 并实现了 java.io.Serializable 接口。
  • 事件监听者:ApplicationListener 是一个接口,定义了 onApplicationEvent 方法来处理事件。只要实现了 ApplicationListener 接口,就可以完成对事件的监听。
  • 事件发布者:ApplicationEventPublisher 作为事件的发布者,也是一个接口。这个接口在 AbstractApplicationContext 中实现。

流程

Spring 中完成对事件的订阅和发布,流程如下:

  1. 定义一个事件,继承自 ApplicationEvent。
  2. 定义一个事件监听者,实现 ApplicationListener 接口,重写 onApplicationEvent 方法。
  3. 使用事件发布者发布消息,通过 ApplicationEventPublisher 的 publishEvent 方法发布消息。
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
// 定义一个事件,继承自ApplicationEvent并且写相应的构造函数
public class DemoEvent extends ApplicationEvent{
private static final long serialVersionUID = 1L;

private String message;

public DemoEvent(Object source,String message){
super(source);
this.message = message;
}

public String getMessage() {
return message;
}
}

// 定义一个事件监听者,实现ApplicationListener接口,重写 onApplicationEvent() 方法;
@Component
public class DemoListener implements ApplicationListener<DemoEvent>{

//使用onApplicationEvent接收消息
@Override
public void onApplicationEvent(DemoEvent event) {
String msg = event.getMessage();
System.out.println("接收到的信息是:"+msg);
}

}
// 发布事件,可以通过ApplicationEventPublisher 的 publishEvent() 方法发布消息。
@Component
public class DemoPublisher {

@Autowired
ApplicationContext applicationContext;

public void publish(String message){
//发布事件
applicationContext.publishEvent(new DemoEvent(this, message));
}
}

适配器模式

适配器模式将一个接口转换成客户希望的另一个接口,适配器模式使接口不兼容的那些类可以一起工作。

Spring AOP 通知

Spring AOP:通知用到了适配器模式,与之相关的接口是 AdvisorAdapter

Advice 常用的类型有:BeforeAdvice(目标方法调用前,前置通知)、AfterAdvice(目标方法调用后,后置通知)、AfterReturningAdvice(目标方法执行结束后,return 之前)等等。每个类型 Advice(通知)都有对应的拦截器:MethodBeforeAdviceInterceptorAfterReturningAdviceInterceptorThrowsAdviceInterceptor 等等。

Spring 预定义的通知要通过对应的适配器,适配成 MethodInterceptor 接口(方法拦截器)类型的对象(如:MethodBeforeAdviceAdapter 通过调用 getInterceptor 方法,将 MethodBeforeAdvice 适配成 MethodBeforeAdviceInterceptor )。

Spring MVC

Spring MVC:SpringMVC 中有 HandlerAdapter,用于适配执行 Handler。

为什么 SpringMVC 需要适配器?

Spring MVC 中的 Controller 种类众多,不同类型的 Controller 通过不同的方法来对请求进行处理。如果不利用适配器模式的话,DispatcherServlet 直接获取对应类型的 Controller,需要的自行来判断,像下面这段代码。新增的 Controller 类型就违反了设计模式中的开闭原则。

1
2
3
4
5
6
7
if(mappedHandler.getHandler() instanceof MultiActionController){
((MultiActionController)mappedHandler.getHandler()).xxx
}else if(mappedHandler.getHandler() instanceof XXX){
...
}else if(...){
...
}

装饰器模式

装饰者模式可以动态地给对象添加一些额外的属性或行为。相比于使用继承,装饰者模式更加灵活。Spring 中配置 DataSource 的时候,DataSource 可能是不同的数据库和数据源,可以通过装饰器模式动态的切换不同的数据源。

Spring 中用到的装饰器在类名上含有 Wrapper 或者 Decorator。

事务的 ACID 特性中,只有保证了事务的持久性、原子性、隔离性之后,一致性才能得到保障。也就是说 A、I、D 是手段,C 是目的。

Spring 事务

事务管理方式

编程式事务管理

编程式事务管理,通过 TransactionTemplate 或者 TransactionManager 手动管理事务。

  • 使用 TransactionTemplate,调用 execute 方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Autowired
private TransactionTemplate transactionTemplate;
public void testTransaction() {

transactionTemplate.execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus transactionStatus) {

try {

// .... 业务代码
} catch (Exception e){
//回滚
transactionStatus.setRollbackOnly();
}

}
});
}
  • 使用 TransactionManager,调用 getTransaction 获取一个新的事务。
1
2
3
4
5
6
7
8
9
10
11
12
13
@Autowired
private PlatformTransactionManager transactionManager;

public void testTransaction() {

TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
try {
// .... 业务代码
transactionManager.commit(status);
} catch (Exception e) {
transactionManager.rollback(status);
}
}

声明式事务管理

通过 @Transaction 注解,代码的侵入性最小,原理是 AOP。

事务管理接口

Spring 框架中,事务管理相关最重要的 3 个接口如下:

  • **PlatformTransactionManager**:(平台)事务管理器,Spring 事务策略的核心。
  • **TransactionDefinition**:事务定义信息(事务隔离级别、传播行为、超时、只读、回滚规则)。
  • **TransactionStatus**:事务运行状态。

可以把 PlatformTransactionManager 接口可以被看作是事务上层的管理者,而 TransactionDefinitionTransactionStatus 这两个接口可以看作是事务的描述。

PlatformTransactionManager 会根据 TransactionDefinition 的定义比如事务超时时间、隔离级别、传播行为等来进行事务管理 ,而 TransactionStatus 接口则提供了一些方法来获取事务相应的状态比如是否新事务、是否可以回滚等等。

事务管理接口

Spring 并不直接管理事务,而是提供了多种事务管理器 。Spring 事务管理器的接口是 PlatformTransactionManager,通过这个接口 Spring 可以为各个平台如 JDBC(DataSourceTransactionManager)、Hibernate(HibernateTransactionManager)等提供了对应的事务管理器。

img

PlatformTransactionManager 接口中定义了三个方法:

1
2
3
4
5
6
7
8
9
10
11
12
package org.springframework.transaction;

import org.springframework.lang.Nullable;

public interface PlatformTransactionManager {
//获得事务
TransactionStatus getTransaction(@Nullable TransactionDefinition var1) throws TransactionException;
//提交事务
void commit(TransactionStatus var1) throws TransactionException;
//回滚事务
void rollback(TransactionStatus var1) throws TransactionException;
}

事务属性

事务属性 TransactionDefinition,定义了基本的事务属性,包括五个方面:

  • 隔离级别。
  • 传播行为。
  • 回滚规则。
  • 是否只读。
  • 事务超时。

TransactionDefinition 接口定义如下。

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
package org.springframework.transaction;

import org.springframework.lang.Nullable;

public interface TransactionDefinition {
int PROPAGATION_REQUIRED = 0;
int PROPAGATION_SUPPORTS = 1;
int PROPAGATION_MANDATORY = 2;
int PROPAGATION_REQUIRES_NEW = 3;
int PROPAGATION_NOT_SUPPORTED = 4;
int PROPAGATION_NEVER = 5;
int PROPAGATION_NESTED = 6;
int ISOLATION_DEFAULT = -1;
int ISOLATION_READ_UNCOMMITTED = 1;
int ISOLATION_READ_COMMITTED = 2;
int ISOLATION_REPEATABLE_READ = 4;
int ISOLATION_SERIALIZABLE = 8;
int TIMEOUT_DEFAULT = -1;
// 返回事务的传播行为,默认值为 REQUIRED。
int getPropagationBehavior();
//返回事务的隔离级别,默认值是 DEFAULT
int getIsolationLevel();
// 返回事务的超时时间,默认值为-1。如果超过该时间限制但事务还没有完成,则自动回滚事务。
int getTimeout();
// 返回是否为只读事务,默认值为 false
boolean isReadOnly();

@Nullable
String getName();
}

事务状态

TransactionStatus 用于描述事务状态,接口定义如下:

1
2
3
4
5
6
7
public interface TransactionStatus{
boolean isNewTransaction(); // 是否是新的事务
boolean hasSavepoint(); // 是否有恢复点
void setRollbackOnly(); // 设置为只回滚
boolean isRollbackOnly(); // 是否为只回滚
boolean isCompleted; // 是否已完成
}

事务属性详解

事务传播行为

事务传播行为是为了解决业务层方法之间互相调用的事务问题。当事务方法被另一个事务方法调用时,必须指定事务应该如何传播。在 TransactionDefinition 定义中包括了如下几个表示传播行为的常量,Spring 也相应地定义了一个枚举类 Propagation。

传播行为如下:

  • PROPAGATION_REQUIRED:默认的传播行为,如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。
  • PROPAGATION_REQUIRES_NEW:不管外部是否有事务,修饰的内部方法会新开启自己的事务,且开启的事务相互独立,互不干扰。
  • PROPAGATION_NESTED:在外部方法开启事务时的情况下,在内部开启一个新的事务,作为嵌套事务存在。如果外部方法无事务,则单独开启一个事务。
  • PROPAGATION_MANDATORY:如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。

以下事务传播行为不常用,导致事务不会发生回滚:

  • PROPAGATION_SUPPORTS:如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。
  • PROPAGATION_NOT_SUPPORTED:以非事务方式运行,如果当前存在事务,则把当前事务挂起。
  • PROPAGATION_NEVER:以非事务方式运行,如果当前存在事务,则抛出异常。

隔离级别

隔离级别与数据库中定义的隔离级别一致(读未提交,读已提交,可重复读,穿行化),这里不再赘述。

事务超时

所谓事务超时,就是指一个事务所允许执行的最长时间,如果超过该时间限制但事务还没有完成,则自动回滚事务。TransactionDefinition 中以 int 的值来表示超时时间,其单位是秒,默认值为 -1,这表示事务的超时时间取决于底层事务系统或者没有超时时间。

只读属性

MySQL 默认对每一个新建立的连接都启用了 autocommit 模式。在该模式下,每一个发送到 MySQL 服务器的 SQL 语句都会在一个单独的事务中进行处理,执行结束后会自动提交事务,并开启一个新的事务。

对于只有读取数据查询的事务,可以指定事务类型为 readonly,即只读事务。只读事务不涉及数据的修改,数据库会提供一些优化手段,适合用在有多条数据库查询操作的方法中。对于更新操作,只读事务中会抛出异常。

如果给方法加上了 Transactional 注解的话,这个方法执行的所有 SQL 会被放在一个事务中。如果声明了只读事务的话,数据库或者 ORM 框架就会去优化它的执行。

例如 Oracle 对于只读事务,不启动回滚段,不记录回滚log。

事务回滚规则

定义了哪些异常会导致事务回滚而哪些不会,默认情况下,事务只有遇到运行异常以及 Error 才会回滚;遇到受检异常时,不会回滚。可以使用 rollbackFor 属性定义需要回滚的异常类型。

@Transaction

@Transaction 的作用范围:

方法:推荐将注解使用于方法上,不过需要注意的是:该注解只能应用到 public 方法上,否则不生效。

:如果这个注解使用在类上的话,表明该注解对该类中所有的 public 方法都生效。

接口:不推荐在接口上使用。

定义

@Transaction 的定义如下,常用配置有:

  • propagation:传播行为,默认为 REQUIRED。
  • isolation:隔离级别,默认为 DEFAULT。
  • timeout:事务超时事件,默认为 -1(不会超时)。
  • readOnly:是否是只读事务,默认为 false。
  • rollbackFor:用于指定能够触发事务回滚的异常类型,并且可以指定多个异常类型。
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
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Transactional {

@AliasFor("transactionManager")
String value() default "";

@AliasFor("value")
String transactionManager() default "";

Propagation propagation() default Propagation.REQUIRED;

Isolation isolation() default Isolation.DEFAULT;

int timeout() default TransactionDefinition.TIMEOUT_DEFAULT;

boolean readOnly() default false;

Class<? extends Throwable>[] rollbackFor() default {};

String[] rollbackForClassName() default {};

Class<? extends Throwable>[] noRollbackFor() default {};

String[] noRollbackForClassName() default {};

}

原理

@Transaction 基于 AOP,AOP 又是基于动态代理实现的。如果目标对象实现了接口,默认情况下会采用 JDK 的动态代理,如果目标对象没有实现了接口,会使用 CGLIB 动态代理。

如果一个类或者一个类中的 public 方法上被标注 @Transactional 注解的话,Spring 容器就会在启动的时候为其创建一个代理类,在调用被 @Transactional 注解的 public 方法的时候,实际调用的是,TransactionInterceptor 类中的 invoke() 方法。这个方法的作用就是在目标方法之前开启事务,方法执行过程中如果遇到异常的时候回滚事务,方法调用完成之后提交事务。

自调用问题

当一个方法被标记了 @Transactional 注解的时候,Spring 事务管理器只会在被其他类方法调用的时候生效,而不会在一个类中方法调用生效。

这是因为 Spring AOP 工作原理决定的。因为 Spring AOP 使用动态代理来实现事务的管理,它会在运行的时候为带有 @Transactional 注解的方法生成代理对象,并在方法调用的前后应用事物逻辑。如果该方法被其他类调用代理对象就会拦截方法调用并处理事务。但是在一个类中的其他方法内部调用的时候,代理对象就无法拦截到这个内部调用,因此事务也就失效了。

解决办法就是避免同一类中自调用或者使用 AspectJ 取代 Spring AOP 代理

1
2
3
4
5
6
7
8
9
10
11
12
@Service
public class MyService {

private void method1() {
((MyService)AopContext.currentProxy()).method2(); // 先获取该类的代理对象,然后通过代理对象调用method2。
//......
}
@Transactional
public void method2() {
//......
}
}

AOP

面向切面编程也是一种设计思想,核心思想就是将横切关注点从核心业务逻辑中分离出来,形成一个个的切面(Aspect)

Spring AOP 就是基于动态代理的,如果要代理的对象,实现了某个接口,那么 Spring AOP 会使用 JDK Proxy,去创建代理对象;对于没有实现接口的对象,Spring AOP 会使用 CGLIB 生成一个被代理对象的子类来作为代理。

SpringAOPProcess

也可以使用 AspectJ,AspectJ 算的上是 Java 生态系统中最完整的 AOP 框架了。

Spring AOP 和 AspectJ AOP 的区别?

  • 实现原理不同:Spring AOP 属于运行时增强,而 AspectJ 是编译时增强。** Spring AOP 基于代理,而 AspectJ 基于字节码操作。

  • Spring AOP 集成了 AspectJ AOP:Spring AOP 已经集成了 AspectJ ,AspectJ 应该算的上是 Java 生态系统中最完整的 AOP 框架了。AspectJ 相比于 Spring AOP 功能更加强大,但是 Spring AOP 相对来说更简单,

  • AspectJ 更快:如果我们的切面比较少,那么两者性能差异不大。但是,当切面太多的话,最好选择 AspectJ ,它比 Spring AOP 快很多。

AOP 的应用场景:

  • 日志记录:自定义日志记录注解,利用 AOP,一行代码即可实现日志记录。
  • 性能统计:利用 AOP 在目标方法的执行前后统计方法的执行时间,方便优化和分析。
  • 事务管理:@Transactional 注解基于 AOP 实现的,可以让 Spring 进行事务管理比如回滚异常操作,免去了重复的事务管理逻辑。
  • 权限控制:利用 AOP 在目标方法执行前判断用户是否具备所需要的权限,如果具备,就执行目标方法,否则就不执行。
  • 接口限流:利用 AOP 在目标方法执行前通过具体的限流算法和实现对请求进行限流处理。
  • 缓存管理:利用 AOP 在目标方法执行前后进行缓存的读取和更新。
  • 。。。。。。

核心概念

AOP中的术语:

  • 横切关注点(cross-cutting concerns) :多个类或对象中的公共行为(如日志记录、事务管理、权限控制、接口限流、接口幂等等)。
  • 切面(Aspect):对横切关注点进行封装的类,一个切面是一个类。切面可以定义多个通知,用来实现具体的功能。
  • 连接点(JoinPoint):连接点是方法调用或者方法执行时的某个特定时刻(如方法调用、异常抛出等)。
  • 通知(Advice):通知就是切面在某个连接点要执行的操作。通知有五种类型,分别是前置通知(Before)、后置通知(After)、返回通知(AfterReturning)、异常通知(AfterThrowing)和环绕通知(Around)。前四种通知都是在目标方法的前后执行,而环绕通知可以控制目标方法的执行过程。
  • 切点(Pointcut):一个切点是一个表达式,它用来匹配哪些连接点需要被切面所增强。切点可以通过注解、正则表达式、逻辑运算等方式来定义。比如 execution(* com.xyz.service..*(..))匹配 com.xyz.service 包及其子包下的类或接口。
  • 织入(Weaving):织入是将切面和目标对象连接起来的过程,也就是将通知应用到切点匹配的连接点上。

通知类型

Spring AOP 中定义的通知类型:

  • Before(前置通知):目标对象的方法调用之前触发。
  • After (后置通知):目标对象的方法调用之后触发。
  • AfterReturning(返回通知):目标对象的方法调用完成,在返回结果值之后触发。
  • AfterThrowing(异常通知):目标对象的方法运行中抛出 / 触发异常后触发。AfterReturning 和 AfterThrowing 两者互斥。如果方法调用成功无异常,则会有返回值;如果方法抛出了异常,则不会有返回值。
  • Around (环绕通知):编程式控制目标对象的方法调用。环绕通知是所有通知类型中可操作范围最大的一种,因为它可以直接拿到目标对象,以及要执行的方法,所以环绕通知可以任意的在目标对象的方法调用前后搞事,甚至不调用目标对象的方法

切面的执行顺序

多个切面的执行顺序如何控制,有两种方法:

  • @Order 注解,值越小优先级越高。
  • 通过实现 Ordered 接口,重新 getOrder 方法实现。

IOC

IOC(Inversion Of Control,控制反转)是一种设计思想,就是将原本在程序中手动创建对象的控制权,交由 Spring 框架来管理,被管理

  • 控制:指的是对象创建(实例化、管理)的权力。
  • 反转:控制权交给外部环境(Spring 框架中的 IOC 容器)。

IOC 的目的就是解耦,将对象之间的相互依赖关系交给 IoC 容器来管理,并由 IoC 容器完成对象的注入。也就是说,IOC 有两个好处:

  1. 对象之间的耦合性降低。
  2. 资源变的容易管理,由手动管理变为 Spring IOC 容器进行管理,比如可以很容易的通过 IOC 容器实现单例模式。

Bean

Bean 是指被 IOC 容器管理的 Java 对象。

声明 Bean

作用于类的注解

声明为 Bean 的注解:

  • @Component:通用的注解,可标注任意类为 Spring 组件。如果一个 Bean 不知道属于哪个层,可以使用该注解。
  • @Repository:持久化层,用于数据库相关的操作。
  • @Service:对应服务层,主要涉及一些复杂的逻辑,需要用到 Dao 层。
  • @Controller:对应 Spring MVC 控制层,主要用于接受用户请求并调用 Service 层,返回数据给前端。

可以通过 @ComponentScan 注解定义要扫描的路径。

@Bean 注解

@Bean 注解修饰方法,表示在这个方法中定义产生这个 bean。使用示例如下:

1
2
3
4
5
6
7
8
@Configuration
public class AppConfig {
@Bean
public TransferService transferService() {
return new TransferServiceImpl();
}

}

@Bean 和 @Component 的区别如下:

  • @Component 作用于类,@Bean 作用于方法。
  • @Component 修饰类,表示这个类需要自动装配到 Spring 容器中。@Bean 注解修饰方法,表示在这个方法中定义产生这个 bean。
  • @Bean 注解比 @Component 的自定义性更强。比如当我们引用第三方库中的类需要装配到 Spring 容器中时,通过 @Bean 实现。

注入 Bean

用于注入 Bean 的注解:

  • @Autowired:来自于 org.springframework.bean.factory
  • @Resource:来自于 javax.annotation
  • @Inject:来自于 javax.inject

@Autowired 和 @Resource 的区别

@Autowired

@Autowired 是 Spring 内置的注解,默认注入方式为 byType(根据类型进行匹配),也就是说会优先根据接口类型去匹配并注入 Bean (接口的实现类)。

当接口存在多个实现类时,Spring 可以通过被注入的变量名称选择注入的 bean,变量名称等于首字母小写的实现类的名称。可以通过 @Qualifier 注解来显式指定名称而不是依赖变量的名称。

当一个接口存在多个实现类的话,byType 就无法正确注入对象了,因为这个时候 Spring 会同时找到多个满足条件的选择,默认情况下它自己不知道选择哪一个。

这种情况下,注入方式变为 byName(根据名称进行匹配),这个名称通常就是类名(首字母小写)。

@Resource

@Resource 是 JDK 提供的注解,默认注入方式为 byName。如果无法通过名称匹配到对应的 Bean 的话,注入方式会变为 byType。@Resource 有两个属性,name 和 type。

1
2
3
4
public @interface Resource {
String name() default "";
Class<?> type() default Object.class;
}

如果仅指定 name 属性则注入方式为 byName,如果仅指定 type 属性则注入方式为 byType,如果同时指定 name 和 type 属性(不建议这么做)则注入方式为 byType+byName。

Bean 的作用域

Spring 中 Bean 的作用域通常有下面几种:

  • singleton : IoC 容器中只有唯一的 bean 实例。Spring 中的 bean 默认都是单例的,是对单例设计模式的应用。
  • prototype : 每次获取都会创建一个新的 bean 实例。也就是说,连续 getBean() 两次,得到的是不同的 Bean 实例。
  • request (仅 Web 应用可用): 每一次 HTTP 请求都会产生一个新的 bean(请求 bean),该 bean 仅在当前 HTTP request 内有效。
  • session (仅 Web 应用可用) : 每一次来自新 session 的 HTTP 请求都会产生一个新的 bean(会话 bean),该 bean 仅在当前 HTTP session 内有效。
  • application/global-session (仅 Web 应用可用):每个 Web 应用在启动时创建一个 Bean(应用 Bean),该 bean 仅在当前应用启动时间内有效。
  • websocket (仅 Web 应用可用):每一次 WebSocket 会话产生一个新的 bean。

可以通过配置或者 @Scope 注解来声明 Bean 的作用域:@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)

Bean 是线程安全的吗?

取决于其作用域和状态

  • prototype 作用域下,每次获取都会创建一个新的 bean 实例,不存在资源竞争问题,所以不存在线程安全问题。
  • singleton 作用域下,IoC 容器中只有唯一的 bean 实例,可能会存在资源竞争问题(取决于 Bean 是否有状态)。如果这个 bean 是有状态的话,那就存在线程安全问题(有状态 Bean 是指包含可变的成员变量的对象)。无状态则不存在线程安全问题(事实上,大部分 Bean 都是无状态的,如 Dao、Service)。

对于有状态单例 Bean 的线程安全问题,常见的有两种解决办法:

  1. 在 Bean 中尽量避免定义可变的成员变量。
  2. 使用 ThreadLocal,将有状态的变量保存在 ThreadLocal 中。

Bean 的生命周期

Bean 的声明周期分为四步:

  1. 创建 Bean 的实例:Bean 容器首先会找到配置文件中的 Bean 定义,然后使用 Java 反射 API 来创建 Bean 的实例。
  2. Bean 属性赋值/填充:为 Bean 设置相关属性和依赖,例如 @Autowired 等注解注入的对象、@Value 注入的值、setter 方法或构造函数注入依赖和值、@Resource 注入的各种资源。
  3. Bean 的初始化
    • 如果实现了 *.Aware 接口,调用相应的方法,如 BeanNameAware、BeanClassLoaderAware、BeanFactoryAware 等接口。
    • 如果实现了 BeanPostProcessor 接口,执行 postProcessBeforeInitialization() 方法。
    • 如果 Bean 实现了 InitializingBean 接口,执行 afterPropertiesSet() 方法。
    • 如果 Bean 在配置文件中的定义包含 init-method 属性,执行指定的方法。
    • 如果实现了 BeanPostProcessor 接口,执行 postProcessAfterInitialization() 方法。
  4. 销毁 Bean:注册相关销毁回调接口,将来需要销毁 Bean 或者销毁容器的时候,就调用这些方法去释放 Bean 所持有的资源。
    • 如果 Bean 实现了 DisposableBean 接口,执行 destroy 方法。
    • 如果 Bean 在配置文件中的定义了 destory-method 属性,执行指定的 Bean 销毁方法。或者,也可以直接通过 @PreDestory 注解标记 Bean 销毁之前执行的方法。

img

Spring 中的循环依赖

三级缓存

Spring 通过三级缓存来解决循环依赖问题,确保即使在循环依赖的情况下也能正确创建 Bean。Spring 中的三级缓存其实就是三个 Map,如下:

1
2
3
4
5
6
7
8
9
10
11
// 一级缓存
/** Cache of singleton objects: bean name to bean instance. */
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);

// 二级缓存
/** Cache of early singleton objects: bean name to bean instance. */
private final Map<String, Object> earlySingletonObjects = new HashMap<>(16);

// 三级缓存
/** Cache of singleton factories: bean name to ObjectFactory. */
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);

简单来说,Spring 的三级缓存包括:

  1. 一级缓存(singletonObjects):存放最终形态的 Bean(已经实例化、属性填充、初始化),单例池。一般情况获取 Bean 都是从这里获取的,但是并不是所有的 Bean 都在单例池里面,例如原型 Bean 就不在里面。
  2. 二级缓存(earlySingletonObjects):存放过渡 Bean(半成品,尚未属性填充),也就是三级缓存中 ObjectFactory 产生的对象,与三级缓存配合使用的,可以防止 AOP 的情况下,每次调用 ObjectFactory#getObject() 都是会产生新的代理对象的。
  3. 三级缓存(singletonFactories):存放 ObjectFactoryObjectFactorygetObject() 方法(最终调用的是 getEarlyBeanReference() 方法)可以生成原始 Bean 对象或者代理对象(如果 Bean 被 AOP 切面代理)。三级缓存只会对单例 Bean 生效。

Spring 创建 bean 的过程

Spring 创建 Bean 的流程:

  1. 先去一级缓存 singletonObjects 中获取,存在就返回;
  2. 如果不存在或者对象正在创建中,于是去 二级缓存 earlySingletonObjects 中获取;
  3. 如果还没有获取到,就去 三级缓存 singletonFactories 中获取,通过执行 ObjectFacotrygetObject() 就可以获取该对象,获取成功之后,从三级缓存移除,并将该对象加入到二级缓存中。

Spring 在创建 Bean 的时候,如果允许循环依赖的话,Spring 就会将刚刚实例化完成,但是属性还没有初始化完的 Bean 对象给提前暴露出去,这里通过 addSingletonFactory 方法,向三级缓存中添加一个 ObjectFactory 对象:

1
2
3
4
5
6
7
8
9
// AbstractAutowireCapableBeanFactory # doCreateBean #
public abstract class AbstractAutowireCapableBeanFactory ... {
protected Object doCreateBean(...) {
//..

// 支撑循环依赖:将 ()->getEarlyBeanReference 作为一个 ObjectFactory 对象的 getObject() 方法加入到三级缓存中
addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
}
}

如何解决循环依赖

如 A 和 B 发生了循环依赖,则会发生如下:

  • 当 Spring 创建 A 之后,发现 A 依赖了 B ,又去创建 B,B 依赖了 A ,又去创建 A;
  • 在 B 创建 A 的时候,那么此时 A 就发生了循环依赖,由于 A 此时还没有初始化完成,因此在 一二级缓存 中肯定没有 A;
  • 那么此时就去三级缓存中调用 getObject() 方法去获取 A 的 前期暴露的对象 ,也就是调用 getEarlyBeanReference() 方法,生成一个 A 的 前期暴露对象
  • 然后就将这个 ObjectFactory 从三级缓存中移除,并且将前期暴露对象放入到二级缓存中,那么 B 就将这个前期暴露对象注入到依赖,来支持循环依赖。

只有两级缓存够吗

在没有 AOP 的情况下,确实可以只使用一级和三级缓存来解决循环依赖问题。但是,当涉及到 AOP 时,二级缓存就显得非常重要了,因为它确保了即使在 Bean 的创建过程中有多次对早期引用的请求,也始终只返回同一个代理对象,从而避免了同一个 Bean 有多个代理对象的问题。

有 AOP 的流程:

  1. 生成一个 A 原始对象,A 对象在三级缓存中注册 ObjectFactory(一个回调函数),用于创建 A 的代理对象。
  2. A 引用 B 对象,B 又引用 A 对象。B 回调 ObjectFactory 创建 A 的代理对象,B 引用 A 的代理对象。
  3. B 将 A 的 ObjectFactory 移出三级缓存,将 A 的代理对象放入二级缓存
  4. A 填充 B 对象,完成属性填充。
  5. A 在 BeanPostProcessor 接口的 postProcessAfterInitialization 的方法中,A 尝试生成代理对象,首先检查二级缓存,若二级缓存中有代理则不会生成新的代理对象。

@Lazy

@Lazy 用来标识类是否需要懒加载/延迟加载,可以作用在类上、方法上、构造器上、方法参数上、成员变量中。

Spring Boot 2.2 新增了全局懒加载属性,开启后全局 bean 被设置为懒加载,需要时再去创建。

1
2
#默认false
spring.main.lazy-initialization=true

如非必要,尽量不要用全局懒加载。全局懒加载会让 Bean 第一次使用的时候加载会变慢,并且它会延迟应用程序问题的发现(当 Bean 被初始化时,问题才会出现)。

如果一个 Bean 没有被标记为懒加载,那么它会在 Spring IoC 容器启动的过程中被创建和初始化。如果一个 Bean 被标记为懒加载,那么它不会在 Spring IoC 容器启动时立即实例化,而是在第一次被请求时才创建。这可以帮助减少应用启动时的初始化时间,也可以用来解决循环依赖问题。

如何解决循环依赖

如 A 和 B 发生了循环依赖,A 的构造器上添加 @Lazy 注解后(延迟 Bean B 的实例化),加载的流程如下:

  • 首先 Spring 会去创建 A 的 Bean,创建时需要注入 B 的属性;

  • 由于在 A 上标注了 @Lazy 注解,因此 Spring 会去创建一个 B 的代理对象,将这个代理对象注入到 A 中的 B 属性;

    之后开始执行 B 的实例化、初始化,在注入 B 中的 A 属性时,此时 A 已经创建完毕了,就可以将 A 给注入进去。

关键点就在于对 A 中的属性 B 进行注入时,注入的是 B 的代理对象,因此不会循环依赖。

Spring 介绍

Spring 是一款开源 Java 开发框架,旨在提高开发人员的开发效率以及系统的可维护性。Spring 指的是 Spring Framework,包含很多模块,这些模块可以协助开发。如 Spring 支持 IOC 和 AOP,可以很方便地对数据库进行访问、可以很方便地集成第三方组件电子邮件,任务,调度,缓存等等)、对单元测试支持比较好、支持 RESTful Java 应用程序的开发。

Spring 最核心的思想就是不重新造轮子,开箱即用,提高开发效率。

Spring 模块

Spring 1 仅仅支持配置文件,Spring 2 开始引入注解,Spring 3 引入纯注解模式(Spring 配置类代替配置文件)。

Spring 4 的模块架构图:

Spring4.x主要模块

Spring 5 的模块架构图:

Spring5.x主要模块

Spring5.x 版本中 Web 模块的 Portlet 组件已经被废弃掉,同时增加了用于异步响应式处理的 WebFlux 组件。

从模块架构图中可以看到,Spring 的模块组织如下:

  • Core Container(核心容器):提供了对 IOC 控制反转的支持,即对象管理的支持,是 Spring 的基础模块。
  • AOP 和 Aspects:AOP 为面向切面的编程,Aspects 提供对 AOP 思想的实现。
  • Data Access/Integration:提供数据访问,数据集成的功能。
  • Web:Web 开发。
  • Test:单元测试与集成测试。
阅读全文 »

Java 类在加载时,完成以下事情:

  1. 通过全类名读取获取二进制字节流。
  2. 将字节流所代表的静态结构转为方法区的运行时数据结构。
  3. 生成一个该类对应的 Class 对象。

类加载器

ClassLoader 类加载器的主要作用就是加载 Java 类的字节码(.class 文件)到 JVM 中,在内存中生成一个代表该类的 Class 对象。除了加载类之外,类加载器还可以加载 Java 应用所需的资源如文本、图像、配置文件、视频等等文件资源。

  • 类加载器是一个负责加载类的对象,用于实现类加载过程中的加载这一步。
  • 每个 Java 类都有一个引用指向加载它的 ClassLoader。
  • 数组不是通过 ClassLoader 创建的,是由 JVM 直接生成的。

JVM 启动的时候,并不会一次性加载所有的类,而是根据需要去动态加载。也就是说,大部分类在具体用到的时候才会去加载,这样对内存更加友好。对于已经加载的类会被放在 ClassLoader 中。在类加载的时候,系统会首先判断当前类是否被加载过。已经被加载的类会直接返回,否则才会尝试加载。也就是说,对于一个类加载器来说,相同二进制名称的类只会被加载一次。

1
2
3
4
5
6
7
8
9
10
11
public abstract class ClassLoader {
...
private final ClassLoader parent;
// 由这个类加载器加载的类。
private final Vector<Class<?>> classes = new Vector<>();
// 由VM调用,用此类加载器记录每个已加载类。
void addClass(Class<?> c) {
classes.addElement(c);
}
...
}

类加载器类型

JVM 中内置了三个重要的 ClassLoader

  • BootstrapClassLoader(启动类加载器):最顶层的加载类,由 C++实现,通常表示为 null,并且没有父级,主要用来加载 JDK 内部的核心类库( %JAVA_HOME%/lib目录下的 rt.jarresources.jarcharsets.jar等 jar 包和类)以及被 -Xbootclasspath参数指定的路径下的所有类。
  • **ExtensionClassLoader(扩展类加载器)**:主要负责加载 %JRE_HOME%/lib/ext 目录下的 jar 包和类以及被 java.ext.dirs 系统变量所指定的路径下的所有类。
  • **AppClassLoader(应用程序类加载器)**:面向我们用户的加载器,负责加载当前应用 classpath 下的所有 jar 包和类。

除了这三种类加载器之外,用户还可以加入自定义的类加载器来进行拓展,以满足自己的特殊需求。

获取父类加载器

除了 BootstrapClassLoader 外,其他所有的类加载器都继承自抽象类 ClassLoader。类加载中 BootstrapClassLoader 时 ExtensionClassLoader 的父加载器,ExtensionClassLoader 是 AppClassLoader 的父加载器,而 AppClassLoader 是用户自定义加载器的父加载器。

通过组合而不是继承实现加载器之间的父子关系。

在面向对象编程中,有一条非常经典的设计原则:组合优于继承,多用组合少用继承。(合成复用原则)

所以,加载器之间都是“父子关系”,类加载器中有一个 private final ClassLoader parent 属性,指向这个类加载器的父加载器,可以通过 getParent() 方法获取。如果获取到 parent 为 null 的话,那么父 ClassLoader 为 BootstrapClassLoader。

为什么 获取到 ClassLoadernull就是 BootstrapClassLoader 加载的呢? 这是因为BootstrapClassLoader 由 C++ 实现,由于这个 C++ 实现的类加载器在 Java 中是没有与之对应的类的,所以拿到的结果是 null。

自定义类加载器

除了 BootstrapClassLoader 其他类加载器均由 Java 实现且全部继承自java.lang.ClassLoader。如果我们要自定义自己的类加载器,很明显需要继承 ClassLoader抽象类。

ClassLoader 类有两个关键的方法:

  • protected Class loadClass(String name, boolean resolve):加载指定二进制名称的类,实现了双亲委派机制 。name 为类的二进制名称,resolve 如果为 true,在加载时调用 resolveClass(Class<?> c) 方法解析该类。
  • protected Class findClass(String name):根据类的二进制名称来查找类,默认实现是空方法。

如果不想打破双亲委派模型,就重写 ClassLoader 类中的 findClass() 方法即可,无法被父类加载器加载的类最终会通过这个方法被加载。但是,如果想打破双亲委派模型则需要重写 loadClass() 方法。

双亲委派模型

双亲委派模型简而言之就是:

  1. 除了顶层的启动类加载器外,其余的类加载器都应有自己的父类加载器
  2. 在加载类是,首先委托父类加载器进行加载,父类如果发现已经被加载则直接返回,没有加载则进行加载(自底向上查找类是否被加载)。
  3. 当父类无法加载时,子类进行加载(自顶向下尝试加载类)。

类加载器层次关系图

执行流程

双亲委派模型的执行流程:

  • 在类加载的时候,系统会首先判断当前类是否被加载过。已经被加载的类会直接返回,否则才会尝试加载(每个父类加载器都会走一遍这个流程)。
  • 类加载器在进行类加载的时候,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成(调用父加载器 loadClass()方法来加载类)。这样的话,所有的请求最终都会传送到顶层的启动类加载器 BootstrapClassLoader 中。
  • 只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载(调用自己的 findClass() 方法来加载类)。
  • 如果子类加载器也无法加载这个类,那么它会抛出一个 ClassNotFoundException 异常。

JVM 判定两个 Java 类是否相同的具体规则:JVM 不仅要看类的全名是否相同,还要看加载此类的类加载器是否一样。只有两者都相同的情况,才认为两个类是相同的。即使两个类来源于同一个 Class 文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相同。

双亲委派模型的实现代码都集中在 java.lang.ClassLoaderloadClass() 中,相关代码如下所示。

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
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
//首先,检查该类是否已经加载过
Class c = findLoadedClass(name);
if (c == null) {
//如果 c 为 null,则说明该类没有被加载过
long t0 = System.nanoTime();
try {
if (parent != null) {
//当父类的加载器不为空,则通过父类的loadClass来加载该类
c = parent.loadClass(name, false);
} else {
//当父类的加载器为空,则调用启动类加载器来加载该类
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
//非空父类的类加载器无法找到相应的类,则抛出异常
}

if (c == null) {
//当父类加载器无法加载时,则调用findClass方法来加载该类
//用户可通过覆写该方法,来自定义类加载器
long t1 = System.nanoTime();
c = findClass(name);

//用于统计类加载器相关的信息
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
//对类进行link操作
resolveClass(c);
}
return c;
}
}

好处

双亲委派模型保证了 Java 程序的稳定运行,可以避免类的重复加载(JVM 区分不同类的方式不仅仅根据类名,相同的类文件被不同的类加载器加载产生的是两个不同的类),也保证了 Java 的核心 API 不被篡改。

如果没有使用双亲委派模型,而是每个类加载器加载自己的话就会出现一些问题,比如我们编写一个称为 java.lang.Object 类的话,那么程序运行的时候,系统就会出现两个不同的 Object 类。双亲委派模型可以保证加载的是 JRE 里的那个 Object 类,而不是你写的 Object 类。这是因为 AppClassLoader 在加载你的 Object 类时,会委托给 ExtClassLoader 去加载,而 ExtClassLoader 又会委托给 BootstrapClassLoaderBootstrapClassLoader 发现自己已经加载过了 Object 类,会直接返回,不会去加载你写的 Object 类。

打破双亲委派模型

自定义类加载器,需要继承 ClassLoader,如果我们不想打破双亲委派模型,就重写 findClass 方法。如果想打破双亲委派模型,则重写 loadClass 方法。

上下文类加载器

单纯依靠自定义类加载器没办法满足某些场景的要求,例如,有些情况下,高层的类加载器需要加载低层的加载器才能加载的类。

比如,SPI 中,SPI 的接口(如 java.sql.Driver)是由 Java 核心库提供的,由BootstrapClassLoader 加载。而 SPI 的实现(如com.mysql.cj.jdbc.Driver)是由第三方供应商提供的,它们是由应用程序类加载器或者自定义类加载器来加载的。默认情况下,一个类及其依赖类由同一个类加载器加载。所以,加载 SPI 的接口的类加载器(BootstrapClassLoader)也会用来加载 SPI 的实现。按照双亲委派模型,BootstrapClassLoader 是无法找到 SPI 的实现类的,因为它无法委托给子类加载器去尝试加载。

再比如,假设我们的项目中有 Spring 的 jar 包,由于其是 Web 应用之间共享的,因此会由 SharedClassLoader 加载(Web 服务器是 Tomcat)。我们项目中有一些用到了 Spring 的业务类,比如实现了 Spring 提供的接口、用到了 Spring 提供的注解。所以,加载 Spring 的类加载器(也就是 SharedClassLoader)也会用来加载这些业务类。但是业务类在 Web 应用目录下,不在 SharedClassLoader 的加载路径下,所以 SharedClassLoader 无法找到业务类,也就无法加载它们。

如何解决这个问题呢? 这个时候就需要用到 线程上下文类加载器(ThreadContextClassLoader 了。

拿 Spring 这个例子来说,当 Spring 需要加载业务类的时候,它不是用自己的类加载器,而是用当前线程的上下文类加载器。还记得我上面说的吗?每个 Web 应用都会创建一个单独的 WebAppClassLoader,并在启动 Web 应用的线程里设置线程线程上下文类加载器为 WebAppClassLoader。这样就可以让高层的类加载器(SharedClassLoader)借助子类加载器( WebAppClassLoader)来加载业务类,破坏了 Java 的类加载委托机制,让应用逆向使用类加载器。

线程线程上下文类加载器的原理是将一个类加载器保存在线程私有数据里,跟线程绑定,然后在需要的时候取出来使用。