Spring

摘要:Spring 框架包括 IoC 和 AOP,事务,设计模式等。

目录

[TOC]

Spring 概述

Spring 是轻量级的开源 J2EE(容器)框架和中间层框架(万能胶)。

在 SSM 中作用:Bean 工厂,用来管理 Bean 的生命周期和框架集成。

两大核心:IoC/DI(控制反转/依赖注入)和AOP(面向切面编程)。

优点

  1. 轻量:Spring 是轻量的,基本的版本大约 2MB;
  2. IoC 容器
    1. 通过 IoC 实现与代码松耦合:(IoC 解耦)用 IoC 容器管理对象的生命周期和配置;对象发生改变,只需在配置文件中修改,而无须修改 Java 代码;
    2. 非侵入式:对象可不依赖于 Spring 的 API;(不改变现有的类结构,就能增强 Java Bean 的功能,Struts2 等传统框架常要实现特定接口、继承特定类才能增强功能,改变了 Java 类的结构)。
  3. 支持AOP:面向切面编程,把应用业务逻辑和系统服务分开;
  4. 异常处理:提供了方便的 API 把具体技术相关的异常(如由 JDBC、Hibernate、JDO 抛出的)转化为一致的 unchecked 异常Spring 的 JDBC 抽象层提供了一个异常层次结构,简化了错误处理策略。
  5. 事务管理:提供了一个持续的事务管理接口,可扩展到上至本地事务下至全局事务(JTA);
    • 支持声明式事务:只通过配置就可完成事务管理,无须手动编程。
  6. 方便集成各种框架
    1. 支持 WEB 框架:MVC 设计模式
    2. 支持 Struts2、Hibernate、MyBatis 等;
    3. 为 JavaEE 开发中一些 API(JDBC、JavaMail、远程调用等)提供了封装。
  7. 单元测试支持比较好:框架中包含测试环境;支持 JUnit4,可用注解方便地测试。

Spring Framework 八大模块

img

  1. Core Container 核心容器:Spring Framework 的核心;
    1. Beans 模块:包括 IoC 和 DI,BeanFactory 接口,IoC 容器的实现
    2. Core 模块
    3. Context 上下文模块ApplicationContext 接口,IoC 容器的实现
    4. SpEL(Spring Express Language)表达式
  2. Data Access/Integration 数据访问/集成:提供与数据库交互的支持;
    1. JDBC:Spring使用的是javax.sql.DataSource接口获取数据库连接,DataSource代表一个数据源(获取数据库工厂),相较于直接通过java.sql.Driver获取数据库连接(通常是通过DriverManager查找并调用匹配的Driver创建连接),DataSource最大的优势就是可以实现连接池。
    2. ORM:提供对象-关系映射框架的集成 API,包括 JPA、JDO、Hibernate 和 MyBatis 等。
    3. OXM:提供了一个支持 Object/XML 映射的抽象层实现(Java 对象和 XML 数据间的映射),如 JAXB、Castor、XMLBeans、JiBX 和 XStream。
    4. JMS:Java 消息服务。
    5. Transactions 事务
  3. Web:提供了创建 Web 应用程序的支持;
    1. WebSocket
    2. Servlet
    3. Web
    4. Portlet
  4. AOP 面向切面编程
  5. Aspects:支持 AspectJ 的集成
  6. Instrumentation:为类检测和类加载器的实现提供支持;
  7. Messaging
  8. Test:支持 Junit 和 TestNG 测试框架,模拟 Http 请求的功能。

Spring 启动过程?分哪些步骤

https://www.cnblogs.com/lgx211/p/18535984

IoC 设计思想

IoC 设计思想

IoC 和 DI

在传统的 Java 应用中,一个类(调用者)想要调用(依赖)另一个类(被调用者)的成员变量或成员方法,通常会先通过 new Object() 创建对象。

  1. IoC(Inversion of Control,控制反转):new/创建对象的控制权,由开发者手动创建转移(反转)给(第三方) IoC 容器管理。
  2. DI(Dependency Injection,依赖注入):通过 IoC 容器管理对象间的依赖,容器在创建对象时,(根据依赖关系)将它依赖的对象自动注入当前对象。

二者含义相同,是从两个角度描述的同一概念。如,把 Dao 依赖注入到 Service 层,Service 层反转给 Controller 层,Spring 顶层容器为 BeanFactory。

优点:

  1. 把应用的代码量降到最低;
  2. 使应用容易测试,单元测试不再需要单例和 JNDI 查找机制;
  3. 最小的代价和最小的侵入性可实现松散耦合
  4. IOC 容器支持加载服务时的饿汉式初始化和懒加载

IoC 容器

IoC 容器:管理(Bean)对象(从创建到销毁的)整个生命周期,管理对象间的依赖及注入。

IoC 容器的两种实现

  1. BeanFactory 接口:Spring 内部用;懒加载(获取对象时才创建对象);简单,占内存少,启动快。最常用 XmlBeanFactory实现接口。

    1
    2
    3
    4
    5
    6
    7
     import  org.springframework.beans.factory.BeanFactory;
          
     Resource res = new ClassPathResource("appContext.xml"); 
     BeanFactory fact = new XmlBeanFactory(res); 
          
     Student stu = (Student) fact.getBean("student");
     stu.getMsg();
    
  2. ApplicationContext 接口:面向开发者,用的更多;继承并扩展 BeanFactory,功能更完整;即时加载(加载配置文件时创建并初始化所有对象);不管用没用到,容器启动一次性创建所有 Bean ;

    1. ClassPathXmlApplicationContext:从 classpath 加载配置文件,更常用;
    2. FileSystemXmlApplicationContext:从指定位置加载配置文件,不常用;
    3. XmlWebApplicationContext:从Web系统中的 XML 文件加载配置文件。
    1
      BeanFactory beanFactory = new ClassPathXmlApplicationContext("beans.xml");
    

二者都是通过 XML 配置文件加载 Bean 的,通常用后者,只有在系统资源较少时,才考虑用 BeanFactory

区别是 BeanFactory 是懒加载,ApplicationContext 是一次性加载所有 Bean。

主要区别在于 Bean 的某一属性没有注入时:

  1. BeanFacotry 加载后,第一次调用 getBean()抛出异常
  2. ApplicationContext 在初始化时就自动检测所依赖的属性是否注入。

IoC 工作原理

IoC 底层通过工厂模式、XML 解析、Java 的反射机制等技术,降低代码耦合度,主要步骤有:

  1. 工厂模式:可把 IoC 容器当做一个工厂,产品就是 Spring Bean;
  2. XML 解析:在配置文件(如 Bean.xml)中,配置各个对象及对象间的依赖关系,容器启动时会加载并解析
    • 与代码松耦合、IoC 解耦原理:对象发生改变,只需在配置文件中修改,而无须修改 Java 代码。
  3. IoC 利用 Java 的反射机制:根据类名生成相应的对象,并根据依赖关系将此对象注入到依赖它的对象中。

工厂方法:分为无参和有参,静态工厂和实例工厂。

1
2
3
4
5
6
7
8
9
10
11
12
// 用静态工厂方法创建Bean
class UserFactory {
    public static UserDao getDao() {
        // xml解析
        String className = class属性值;
        // 通过反射创建对象
        Class clazz = Class.forName(className);
        return (UserDao)clazz.newInstance();
        // return new UserDao();
    }
}
UserDao dao = UserFactory.getDao();

循环依赖

循环依赖:是指 Bean 对象循环引用,是两个或多个 Bean 之间相互持有对方的引用,

  • 例如 DependencyA → DependencyB → DependencyA。
  • 单个对象的自我依赖也会出现循环依赖。

如何解决循环依赖

  1. 构造器的:直接抛出 BeanCurrentlylnCreationException 异常。
  2. 单例模式下的 setter(默认的单例 Bean 中,属性互相引用):通过三级缓存处理。
  3. 非单例的 bean 和@Async注解的 bean 无法支持循环依赖:无法处理。

Spring 如何解决三级缓存:

  • 如果发生循环依赖的话,就去三级缓存 singletonFactories 中拿到三级缓存中存储的 ObjectFactory ,并调用它的 getObject() 方法,来获取这个循环依赖对象的前期暴露对象(虽然还没初始化完成,但是可以拿到该对象在堆中的存储地址了),并且将这个前期暴露对象放到二级缓存中,这样在循环依赖时,就不会重复初始化了!

  • 缺点:比如增加了内存开销(需要维护三级缓存,也就是三个 Map),降低了性能(需要进行多次检查和转换)。

SpringBoot 允许循环依赖发生么?

  • SpringBoot 2.6.x 以前是默认允许循环依赖的,也就是说代码出现了循环依赖问题,一般情况下也不会报错。
  • SpringBoot 2.6.x 以后官方不再推荐编写存在循环依赖的代码。

SpringBoot 2.6.x 以后,如果不想重构循环依赖的代码的话,也可以采用下面这些方法:

  • 在全局配置文件中设置允许循环依赖存在:spring.main.allow-circular-references=true。最简单粗暴的方式,不太推荐。
  • 在导致循环依赖的 Bean 上添加 @Lazy 注解,比较推荐。@Lazy 用来标识类是否需要懒加载/延迟加载,可以作用在类上、方法上、构造器上、方法参数上、成员变量中。

依赖注入的方式

依赖注入本质上是 Spring Bean 属性注入的一种,只不过这个属性是一个对象。

Spring 配置

见 Spring Boot 文档中。

Spring Bean 定义

Spring Bean 定义

Bean:由 IoC 容器创建并管理的对象。

  • 把一个拥有对属性进行set和get方法的类,就可以称之为JavaBean。

Bean 属性注入的方式

  1. 构造器注入:XML 配置文件中,用 <constructor-arg> 标签给构造方法的参数赋值;
    • 缺点:没有部分注入;任意修改都会创建一个新实例。
  2. setter() 方法注入:通过调用默认无参构造器或无参 static 工厂方法,实例化 Bean 对象,然后调用 setXxx() 设置<property>属性;
    • 缺点:会覆盖 setter 属性;适用于设置少量属性。
  3. 接口注入/工厂方法;静态工厂注入、实例工厂;
  4. 短命名空间注入:
    1. c:<bean> 中嵌套的 <contructor> 元素,构造器注入的优化;
    2. p:<bean> 中嵌套的 <property> 元素,setter() 注入的优化;
  5. 泛型注入
  6. 基于Groovy DSL 配置(很少见)

基于 XML 配置文件的属性注入

配置文件:描述如何创建对象和哪些组件需要哪些服务。用于定义 Bean 的属性值、作用域、依赖关系。

格式有:

  1. Properties 配置文件:key-value 形式,只能赋值,不能进行其他操作;用于简单的属性配置。
  2. XML 配置文件:树形结构清晰灵活,内容繁琐,用于大型复杂的项目。
    • <bean>的常用属性:见下;
    • <bean>的其它属性:
      • parent 属性:指定继承的父 Bean。
      • 指定 abstarct="true" 而不指定 class 属性,表示为 Bean 定义模板(抽象类?),只能被继承,不能被实例化。
      • value:用于注入基本数据类型及字符串类型的值;
      • type:用来指定对应的构造函数;
      • index:指定参数位置;
    • <property>:给属性注入值,name 的名称取决于 setXxx() 后的参数;
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
// Beans.xml
<beans>
    // bean 标签: 要创建的对象,默认执行无参构造器
    // id: Bean 的唯一标识符
    // class: Bean的实现类,指定从 package 到 class name 的完全限定名
    // name: 
    // scope: Bean 的作用域
    <bean id="user" class="com.spring5.User" name="" scope="">
        <--! 1. 用(有参)构造器注入属性-->
        <constructor-arg name="oname" index=0 type="" value="China">
        </constructor-arg>
        
        <--! 2. 用set方法注入属性,通过property标签实现属性注入-->
        // 注入8大基本数据类类 + String
        <property name="bname" value="java"></property>
        // 注入外部bean属性
        <property name="userDao" ref="userDaoImpl"></property>
        // 注入集合,创建类,定义数组、List、Set、Map属性,生成set方法
        <property name="courses">
            // 注入数组类型属性
            // 注入List集合属性
            <list>
                <value>java</value>
            </list>
            // 注入Map属性
            // 在集合里设置对象类型的值
        </property>
    </bean>
</beans>

// 类中
public Orders(String oname) {
    this.oname = oname;
}

Bean 作用域

Bean 作用域:指 Spring IoC 容器创建的 Bean 对象相对于其他 Bean 的请求可见范围。在装配 Bean 时就必须指明,可通过 XML 或注解方式配置。

1
2
3
4
5
6
7
8
9
10
1. XML 方式:
<bean id="" class="" scope="singleton"></bean>

2. 注解方式:
@Bean
//@Scope("singleton")
@Scope(value = Con'figurableBeanFactory.SCOPE_PROTO'TYPE)
public Person personPrototype() {
    return new Person();
}

基本作用域

  1. 'singleton:单例模式,默认值,在整个Spring IoC 容器中只有一个共享的 Bean 实例,由 BeanFactory 维护。一旦创建成功,可重复使用。存储在高速缓存中,用于无会话状态的 Bean(如 DAO 层、Service 层)。
  2. prototype:原型模式,每次获取 Bean 时,容器都会创建一个新的 Bean 实例;创建成功后不再跟踪和维护 Bean 实例的状态。用于需保持会话状态的 Bean(如 Struts2 的 Action 类)。

Spring 框架并没有对单例 Bean 进行任何多线程的封装处理。大部分的 Spring bean 并没有可变的状态(如 Service 类和 DAO 类),所以在某种程度上说 Spring 的单例 bean 是线程安全的。如果 bean 有多种状态的话,就需自行保证线程安全。最浅显的解决办法就是将多态 bean 的作用域由 singleton 变更为 prototype

Web 作用域:只能在 Web 环境(XmlWebApplicationContext)下用,如果用 ClassPathXmlApplicationContext 加载这些作用域中的任意一个的 Bean,会抛出异常。

  1. request:每次 HTTP request 请求都会产生一个新的(不同的)Bean 实例,仅在当前 request 内有效(在请求完成后,Bean 会被 GC 回收)。
  2. session:每个 HTTP Session 会产生一个新的 Bean,仅在当前 session 内有效。同一个 Session 共享一个 Bean 实例。
  3. global-session:每个全局的 HTTP Session、用 session 定义的 Bean 都将产生一个新实例。典型情况下,仅在用 portlet context 时有效。
  4. application:类似于 singleton,同一个 Web 应用(可能有多个 IoC 容器)共享一个 Bean 实例。
  5. websocket:作用域是 WebSocket。

Bean 生命周期

img

spring bean 在初始化和销毁时可触发自定义回调操作。

  1. 实例化 Bean 对象(3步);
  2. 初始化 Bean 对象(4步),自定义处理;
    1. 初始化顺序:类构造器 > @PostConstruct > InitializingBean > init-method
    2. 初始化时实现的方法:
      1. 通过 Java 提供的 @PostConstruct 注解;
      2. 通过实现 Spring 提供的 InitializingBean 接口,并重写其 afterPropertiesSet() 方法;
      3. 通过 Spring 的 xml bean 配置或 bean 注解指定初始化方法,如自定义 initMethod方法通过 @Bean 注解指定。
  3. 销毁 Bean 对象(4步);
    1. 销毁顺序:@PreDestroy > DisposableBean 接口> destroyMethod
    2. 销毁时实现的方法:
      1. 通过 Java 提供的 @PreDestroy 注释;
      2. 通过实现 spring 提供的 DisposableBean 接口,并重写其 destroy 方法;
      3. 通过 spring 的 xml bean 配置或 bean 注解指定销毁方法,如下面实例的 destroyMethod 方法通过 @bean 注解指定。

同名 Bean 的优先级

  1. 同一个配置文件内,以最早定义的为准;
  2. 不同配置文件中,后解析的配置文件会覆盖先解析的;
  3. 同文件中 @Bean的优先级(最先注册)> @ComponentScan

Bean 自动装配

Bean 装配/依赖注入(装配方式即依赖注入方式):在 IoC 容器中,Bean 与 Bean 间建立依赖关系。

装配方式

  1. 手动装配 Bean:XML 配置文件中,通过 <constructor-arg><property> 中的 ref 属性,手动维护 Bean 间的依赖关系。
  2. 自动装配 Bean:Spring 容器依据规则,为指定的 Bean 从应用的上下文AppplicationContext 容器)中查找所依赖的 Bean,并自动建立 Bean 间的依赖关系。对象无需自己查找或创建与其关联的其他对象,由容器负责把(需相互协作的)对象引用赋予各个对象。
    1. 基于 XML 配置文件自动装配 Bean;
    2. 基于注解装配 Bean:最常用 @Autowire

自动装配的五种规则

1
2
<!-- 通过 autowire 属性设置自动装配的规则-->
<bean id="employee" class="net.bian.Employee" autowire="byName" ref="">
  1. no:默认设置,表示不用自动装配,必须通过 ref 属性手动设置依赖的 Bean;
  2. byName:根据 Bean 中 <Property>name 属性(查找对应的 Bean)自动装配/注入对象依赖;
  3. byType:根据 Property 兼容的数据类型自动装配;
  4. constructor:类似 byType,根据构造方法参数的数据类型,进行 byType 模式的自动装配;
  5. autodetect:自动探测,如果 Bean 中有默认的构造方法,则用 constructor 模式,否则用 byType 模式。

基于 XML 自动装配 Bean

applicationContext.xml 配置文件

  1. 根据名字装配;
  2. 根据类型装配;

局限:

  1. <constructor-arg><property> 设置指定依赖项,将覆盖自动装配;
  2. 基本元数据类型(简单属性,如原数据类型、字符串和类)无法自动装配;
  3. 不精确。

声明/注册 Bean 的注解

Spring Boot 彻底抛弃了 XML 配置,推荐基于 Java API 配置 Bean,在 Java 类中用注解设置依赖关系。

1
2
3
4
// Spring 配置文件(xml或properties)中启用注解装配
<beans>
	<context:annotation-config/>
</beans>

常用注册 Bean 的注解:

  1. @Component:通用的注解,可标注任意类Spring 组件(Java Bean 对象),并添加到容器中。用于不知道属于哪层的 Bean,如用于切面类等。同 @Name,较少用。
  2. @Bean:替代 <bean /> 元素;将该方法返回的 Bean 对象加载到 Spring 容器,如 Config 类等。可用 name 属性自定义,默认为方法名。
    • @Order (或者接口Ordered)的作用是定义Spring IOC容器中Bean的执行顺序的优先级,而不是定义Bean、类的加载顺序。参数值越小优先级越高。
  3. @Repository:用于 Dao(数据持久化)层,数据库相关操作。仅仅是一个标识,表明为仓库接口类,方便 Spring 自动扫描识别。JPA 具体的注解有 @CrudRepository、@PagingAndSortingRepository、@JpaRepository
  4. @Service: 用于 Service(业务逻辑)层,主要涉及一些复杂的逻辑。
  5. @Controller:用于控制层,接收并转发用户请求、流程控制等。
  6. @Aspect:用于切面。

@Component VS @Bean

  1. 位置@Component 注解作用于,而 @Bean 作用于方法
  2. 作用对象@Component (常通过类路径扫描来)自动装配当前类的对象到容器;@Bean 将该方法返回的对象加载到 Spring 容器。
  3. @Bean 自定义性更强,多处只能用 @Bean 来注册 Bean。如引用第三方库中的类需装配到 Spring 容器时。

基于注解装配/注入 Bean

注入 Bean 的注解:

  1. @Autowired:用于 setter()、构造函数或字段上。步骤:
    1. 默认byType 的方式(根据对象属性所属的类型自动注入 Bean 到(同样被 Spring 容器管理的)当前 Bean 中(即自动导入对象到类中),如:Service 对象作为属性注入到当前 Controller 类中;
    2. 同一个实体类有多个实现类(配置多个 Bean)时类型相同,IoC 不知该注入哪个实现类,这时改为 byName 的方式注入;默认根据标注的成员变量名作为 id,查找 Bean、进行装配;
    3. 仍失败,则通过 @Qualifier 手动指定目标 Bean 的 id(变量名)
    4. 检查属性是否正常装配(设置),无法找到匹配的 Bean 装配会抛出异常;设置 required=false 允许属性不被设置,则不抛出异常。
    5. @Autowired注解使用的自动装配方式
  2. @Resource默认值是 byName,自动用标注处的变量或方法名作为 Bean 名,找不到与名称匹配的 Bean(或该属性为空、不指定值)时用 byType;相同类型在 IoC 容器中只能有一个。如,PermissionService、RoleService 属性注入时。
    • @Autowired 加载的 Bean 相同类型可能有多个,通过 ByName 加载并区分;
    • @Resource 加载的 Bean 相同类型在 IoC 容器中只能有一个
  3. @Qualifier("userDAO"):限定要自动注入的 Bean 的 id,一般和 @Autowired 联用,指定 Bean的名称。

@Controller

用于控制器类上。

@Controller VS @ResposeBody VS @RestController

  1. @Controller:常用于 Controller 控制器类上,作用见 Controler 控制层。单独使用时(不加 @ResponseBody)返回一个视图/页面,用于(传统 Spring MVC)前后端不分离的情况。
    • 若扫描该类下有 @RequestMapping 方法,根据注解信息生成对应的处理器对象。
  2. @ResponseBody:常用于 Controller 方法上,返回具体数据类型而非跳转;将返回的对象转换为指定格式,并填充到 HTTP (响应对象的)响应 body 中,常用于构建 RESTful 的 api,返回 JSON 或 XML 数据。请求体另见@RequestBody
  3. @RestController = @Controller + @ResponseBody:表示 REST 风格的控制器 Bean。Springboot 会把这个类当成 controller 进行处理,把所有返回的参数放到 ResponseBody 中。返回 JSON 或 XML 格式(由客户端的 ACCEPT 请求头决定)的对象数据,并直接写入 HTTP 响应体中。属于 RESTful Web/API 服务,最常用的前后端分离框架的情况。

AOP

AOP(面向切面编程):将与业务无关却被业务模块所共同调用的(交叉业务)逻辑封装成切面。

主要作用是:

  • 通过不修改源码的方式、将非核心功能代码织入来实现对方法的增强。
  • 通过预编译方式和运行期动态代理实现程序功能的统一维护。可对业务逻辑的各部分进行隔离,从而降低耦合度,提高程序的可重用性。

应用场景:异常处理事务处理、日志记录、性能统计、安全控制,统一参数验证、统一返回对象等。

img

AOP 实现原理

基于代理模式,主要分为两种方式:

  1. Eclipse AspectJ 基于静态代理/编译时增强;
    1. 用 Java 编程语言的扩展实现;基于字节码操作;
    2. 可以在所有域对象上实现。像final的方法和静态方法,无法通过动态代理来改变,所以Spring AOP无法支持;
    3. 因为织入方式的区别,两者所支持的 Joinpoint 也是不同的;支持所有切入点
    4. AspectJ是直接在运行前织入实际的代码,所以功能会强大很多。
    5. 依赖 org.aspectj
  2. Spring AOP 基于 动态代理/运行时增强;对于接口使用 JDK Proxy 动态代理,而继承的使用 CGLIB 动态代理。
    1. 使用纯Java代码实现;
    2. 要依赖IOC容器来管理,并且只能作用于Spring容器;
    3. 仅支持方法执行切入点;
    4. 在容器启动时需要生成代理实例,在方法调用上也会增加栈的深度,使得性能不如AspectJ的那么好。
    5. 依赖 spring-boot-starter-aoporg.aspectj。在springAOP的实现中,借用了AspectJ的一些功能,比如@AspectJ、@Before、@PonitCut这些注解,都是AspectJ中的注解。在使用springAOP的时候需要引入AspectJ的依赖。

AOP 实现原理代码示例

声明式 AspectJ

  1. 基于 XML 的声明式 AspectJ / 开发AOP:指通过 Spring 配置文件定义切面、切入点及通知,都必须定义在 <aop:config> 元素(将定义好的 Bean 转换为切面 Bean)中。

    • 在 XML 文件中添加<aop:aspectj-autoproxy> 启用 @AspectJ
  2. 基于注解的声明式 AspectJ:启用 @AspectJ 注解有以下两种方法:

    1. @Configuration@EnableAspectJAutoProxy 注解;

      1
      2
      3
       @Configuration
       @EnableAspectJAutoProxy
       public class Appconfig {}
      
    2. @EnableAspectJAutoProxy 注解也不是必须的,如果引入了 spring-boot-starter-web 依赖则会自动启用AOP。

代理工厂(静态方法)

  1. 把 AOP 加入 IoC 容器中;
  2. 把 UserDao 放入容器中;
  3. 在配置文件中开启注解扫描,用静态工厂方法来创建代理类对象。

通知

img

@Aspect:用于定义切面;切面是通知和切点的结合,分别定义了何时、何处应用通知功能。

  • Aspect切面类必须使用@Component@Aspect注解才有效。

  • 通知(Advice):描述了切面要完成的工作何时执行。如,日志切面需在接口调用前后分别记录当前时间,取差值计算调用时长。

  • @Pointcut 切点:是对连接点(被拦截的 Target 方法)进行拦截的条件定义。作用是提供切点表达式来匹配连接点,给满足条件的连接点添加通知,定义了在何处做

    • 如,日志切面的应用范围是所有 Controller 层的接口方法。格式:
1
2
3
4
    // execution(方法修饰符 返回类型 方法所属的包.类名.方法名称(方法参数)
    @Pointcut("execution(public * com.macro.mall.controller.*.*(..)) || execution(public * com.macro.mall.*.controller.*.*(..))")
    public void webLog() {
    }
  • 连接点(JoinPoint):通知功能被应用的时机(方法被拦截的时机)。如,接口方法被调用时就是日志切面的连接点。

    • JoinPoint 类:返回目标对象,即被代理的对象。
    • ProceedingJoinPoint 类:继承了 JoinPoint。是在JoinPoint的基础上暴露出 proceed() 方法(用于启动目标方法执行的)。是aop代理链执行的方法。是环绕通知独有的, 能决定是否走代理链还是走自己拦截的其他逻辑。
  • 引入(Introduction):用来声明额外的方法和属性,可以给目标对象引入新的接口及其实现,在无需修改现有类的情况下,向现有类添加新方法或属性。

  • 织入(Weaving):是把切面应用到目标对象、并创建新的代理对象的过程。切面在指定的连接点织入到目标对象中。

    • 织入可以在编译期、类加载期和运行期完成,在编译期进行织入就是静态代理,而在运行期进行织入则是动态代理

常见的通知类型

  • @Around:环绕通知,通知包裹了目标方法,在目标方法调用前和调用后执行。通知方法会将目标方法封装起来。
  • @Before("webLog()"):前置通知,通知方法会在目标方法调用前执行。
  • @After:后置通知,通知方法会在目标方法返回或抛出异常执行;
    1. @AfterReturning返回通知,通知方法会在目标方法返回后执行;
    2. @AfterThrowing:异常通知,通知方法会在目标方法抛出异常后执行。

AspectJ 切入点语法

切入点指示符:用来指示切入点表达式目的,在Spring AOP中目前只有执行方法这一个连接点。

Spring AOP支持的AspectJ切入点指示符:

  1. execution:用于匹配方法切入点;
    • @annotation:匹配方法带有指定注解。
  2. within:用于匹配指定类的任意方法,不能匹配接口;
    • @within匹配类中带有指定注解。当定义类时使用了注解,该类的方法会被匹配,但在接口上使用注解不匹配。
  3. this:用于匹配当前AOP代理对象实例的类型的执行方法;匹配在运行时对象的类型。
  4. target:用于匹配当前目标对象类型的执行方法,匹配 AOP 被代理对象的类型;注意是目标对象的类型匹配,这样就不包括引入接口也类型匹配;
    • @target用于匹配目标对象实例的类型是否含有注解。当运行时对象实例的类型使用了注解,该类的方法会被匹配,在接口上使用注解不匹配。
  5. args:用于匹配(当前执行的)方法传入的参数为指定类型的执行方法;
    • @args:匹配方法参数类型是否含有注解。
  6. bean:通过 bean 的 id 或名称匹配,支持 * 通配符。Spring AOP扩展的,AspectJ没有对于指示符,用于匹配特定名称的Bean对象的执行方法;

AspectJ切入点支持的切入点指示符还有: call、get、set、preinitialization、staticinitialization、initialization、handler、adviceexecution、withincode、cflow、cflowbelow、if、@this、@withincode

但Spring AOP目前不支持这些指示符,使用这些指示符将抛出IllegalArgumentException异常。这些指示符Spring AOP可能会在以后进行扩展。

AspectJ类型匹配的通配符:

  1. *:匹配任何数量字符;
  2. ` ..`:匹配任何数量字符的重复,如在类型模式中匹配任何数量子包;而在方法参数模式中匹配任何数量参数。
  3. ` +`:匹配指定类型的子类型;仅能作为后缀放在类型模式后边。

创建切面的步骤

  1. 添加日志信息封装类 WebLog(POJO):用于封装需记录的日志信息,包括操作的描述、时间、消耗时间、URL、请求参数和返回结果等信息。
  2. 添加切面类 WebLogAspect:定义了一个日志切面,在环绕通知中获取日志需要的信息,并应用到 Controller 层中所有的 public 方法中。
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
@Component
@Aspect
@Slf4j
public class TradeOrderLogAspect {

    /**
     * 用户编号
     * 目前的使用场景:支付回调时,需要强制设置下用户编号
     */
    private static final ThreadLocal<Long> USER_ID = new ThreadLocal<>();
 	....

    @Resource
    private TradeOrderLogService orderLogService;

    @AfterReturning("@annotation(orderLog)")
    public void doAfterReturning(JoinPoint joinPoint, TradeOrderLog orderLog) {
        try {
            // 1.1 操作用户
            Integer userType = getUserType();
            Long userId = getUserId();
            // 1.2 订单信息
            Long orderId = ORDER_ID.get();
            if (orderId == null) { // 如果未设置,只有注解,说明不需要记录日志
                return;
            }
            Integer beforeStatus = BEFORE_STATUS.get();
            Integer afterStatus = AFTER_STATUS.get();
            Map<String, Object> exts = ObjectUtil.defaultIfNull(EXTS.get(), emptyMap());
            String content = StrUtil.format(orderLog.operateType().getContent(), exts);

            // 2. 记录日志
            TradeOrderLogCreateReqBO createBO = new TradeOrderLogCreateReqBO()
                    .setUserId(userId).setUserType(userType)
                    .setOrderId(orderId).setBeforeStatus(beforeStatus).setAfterStatus(afterStatus)
                    .setOperateType(orderLog.operateType().getType()).setContent(content);
            orderLogService.createOrderLog(createBO);
        } catch (Exception ex) {
            log.error("[doAfterReturning][orderLog({}) 订单日志错误]", toJsonString(orderLog), ex);
        } finally {
            clear();
        }
    }

异常

常见 web 请求和参数异常

  1. MethodArgumentNotValidException:参数校验异常;
  2. MethodArgumentTypeMismatchException, HttpMessageNotReadableException:参数格式有误;
  3. MissingServletRequestParameterException:缺少参数;
  4. HttpClientErrorException
  5. HttpRequestMethodNotSupportedException:不支持的请求类型;
  6. HttpMessageNotReadableException:请求体格式错误;
  7. ConstraintViolationException:约束异常;
  8. JsonMappingException:JSON格式错误;
  9. ServiceEx:业务层异常;

异常捕获和统一异常处理

当前端传来不合规范的数据时,Controller 就会报错,并显示对应的错误信息。

异常捕获:可定义一个全局异常的捕获类(GlobalExceptionHandler.java),当出现某种异常时就捕获,对(错误信息)数据进行封装,返回到前端页面显示。

注解:

  1. @ControllerAdvice :定义全局异常处理类;Spring3.2中新增的注解,Controller增强器,作用是给Controller控制器添加统一的操作或处理。用在类上。比较熟知的用法是结合@ExceptionHandler用于全局异常的处理.
  2. @ExceptionHandler :声明异常处理方法;

这种异常处理方式下,会给所有或者指定的 Controller 织入异常处理 AOP,当 Controller 中的方法抛出异常时,由被@ExceptionHandler 注解修饰的方法进行处理。

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
79
80
81
82
83
84
85
86
87
88
89
90
91
92
package com.macro.mall.common.api;

/**
 * 常用API返回对象接口
 */
public interface IErrorCode {
    /**
     * 返回码
     */
    long getCode();

    /**
     * 返回信息
     */
    String getMessage();
}

package com.macro.mall.common.exception;
import com.macro.mall.common.api.IErrorCode;

/**
 * 自定义API异常
 */
public class ApiException extends RuntimeException {
    private IErrorCode errorCode;

    public ApiException(IErrorCode errorCode) {
        super(errorCode.getMessage());
        this.errorCode = errorCode;
    }

    public ApiException(String message) {
        super(message);
    }

    public ApiException(Throwable cause) {
        super(cause);
    }

    public ApiException(String message, Throwable cause) {
        super(message, cause);
    }

    public IErrorCode getErrorCode() {
        return errorCode;
    }
}

package com.macro.mall.common.exception;
/**
 * 全局异常处理
 */
@ControllerAdvice
public class GlobalExceptionHandler {

    @ResponseBody
    @ExceptionHandler(value = ApiException.class)
    public CommonResult handle(ApiException e) {
        if (e.getErrorCode() != null) {
            return CommonResult.failed(e.getErrorCode());
        }
        return CommonResult.failed(e.getMessage());
    }

    @ResponseBody
    @ExceptionHandler(value = MethodArgumentNotValidException.class)
    public CommonResult handleValidException(MethodArgumentNotValidException e) {
        BindingResult bindingResult = e.getBindingResult();
        String message = null;
        if (bindingResult.hasErrors()) {
            FieldError fieldError = bindingResult.getFieldError();
            if (fieldError != null) {
                message = fieldError.getField()+fieldError.getDefaultMessage();
            }
        }
        return CommonResult.validateFailed(message);
    }

    @ResponseBody
    @ExceptionHandler(value = BindException.class)
    public CommonResult handleValidException(BindException e) {
        BindingResult bindingResult = e.getBindingResult();
        String message = null;
        if (bindingResult.hasErrors()) {
            FieldError fieldError = bindingResult.getFieldError();
            if (fieldError != null) {
                message = fieldError.getField()+fieldError.getDefaultMessage();
            }
        }
        return CommonResult.validateFailed(message);
    }
}

Filter 过滤器

结合项目说下,是怎么用 AOP,通过拦截器拦截非法请求。

Filter 过滤器(重点理解):主要用来过滤用户请求,允许对用户请求进行前置和后置处理,如实现 URL 级别的权限控制、过滤非法请求等。

  • 面向切面编程(AOP)的具体实现。
  • 依赖于 Servlet 容器,属于 Servlet 规范的一部分。

Listener 监听器

(简单过一下)

监听器:是 Servlet 规范中定义的一种特殊类。用于监听 servletContext、servletRequest、HttpSession 等域对象的创建和销毁事件、属性发生修改的事件。

  • Spring Boot 里的监听器实际上底层就是 Spring 和 Spring MVC 相关的东西,再下面就是 Servlet;
  • @Configuration 创建配置类。
  • @RabbitListener(queues = "mall.order.cancel")

用于在事件发生前、后做一些必要的处理。主要用于:

  1. 系统启动时加载初始化信息;
  2. 统计在线人数和用户;
  3. 统计网站访问量;
  4. 记录用户访问路径。

事件的发布与监听属于观察者模式;和 MQ 相比,偏向于处理“体系内”的某些逻辑。

步骤:

  1. 自定义事件;
  2. 通过 @EventListener 自定义(用于处理某种事件的)监听器类;
  3. 通过 @Component 注册监听器类;
  4. 发布事件(监听到该事件的监听器自动进行相关逻辑处理)。

在这里插入图片描述

Spring 事务

事务管理:按照给定的事务规则来执行提交或回滚操作。

见数据库文档中的事务。

事务实现

AOP

事务分类

  1. 编程式事务:使用 TransactionTemplate 编写代码(硬编码)实现管理事务,有极大灵活性,难维护;
  2. 声明式事务:分离事务管理和业务逻辑代码,本质是通过 AOP,对方法前后进行拦截,将事务处理(开启、提交或回滚操作)的功能编织到拦截的方法中(即在目标方法开始前加入一个事务,在执行完方法后根据执行情况提交或回滚事务)。不需编程,只需通过在 XML 文件中配置、或直接基于注解实现。实现声明式事务的方式:
    1. 基于 XML(<tx><aop> 命名空间)的声明式事务管理:推荐,最大特点是与 Spring AOP 结合紧密,可充分利用切点表达式的强大支持,更灵活。
    2. 基于 @Transactional 的全注解方式的:简化,使用最多。

@Transactional

  1. 作用于类:表示该类所有的 public 方法都配置相同的事务属性信息;
  2. 只能作用于 public 方法:方法的事务会覆盖类的事务配置信息。如 Service 方法。
1
2
3
4
5
6
public interface PmsProductService {
    /**
     * 创建商品
     */
    @Transactional(isolation = Isolation.DEFAULT, propagation = Propagation.REQUIRED, rollbackFor = {Exception.class, 其它异常})
    int create(PmsProductParam productParam);

@Transactional 的可选属性有:

  • propagation
  • isolation
  • rollbackFor:需要回滚的异常类。默认情况下,遇到 RuntimeException 时会回滚,遇到受检查的异常不会回滚。
    • 要想所有异常都回滚,要加上 @Transactional(rollbackFor = {Exception.class, 其它异常})

image-20221009222037746

事务传播行为/规则/属性

Spring 事务的传播行为:当多个事务同时存在时,Spring 如何处理这些事务的行为。

enum Propagation 中,枚举取值有:

  1. REQUIRED:支持当前事务,如果当前没有事务,新建一个事务。最常见,默认的方式;
  2. SUPPORTS:支持当前事务,如果当前没有事务,以非事务方式执行;事务将不会发生回滚
  3. 'MANDATORY:支持当前事务,如果当前没有事务,抛出异常;
  4. REQUIRES_NEW:新建事务,如果当前存在事务,把当前事务挂起
  5. NOT_SUPPORTED:以非事务方式执行操作,如果当前存在事务,把当前事务挂起;事务将不会发生回滚
  6. NEVER:以非事务方式执行,如果当前存在事务,抛出异常;事务将不会发生回滚
  7. NESTED:如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,进行与 REQUIRED类似的操作。

事务隔离级别

见数据库 MySQL 中。

enum Isolation 中,枚举值有:

1
2
3
4
5
DEFAULT(-1), // 采用数据库默认隔离级别
READ_UNCOMMITTED(1),
READ_COMMITTED(2),
REPEATABLE_READ(4),
SERIALIZABLE(8);
  1. ISOLATION_DEFAULT默认的隔离级别,使用数据库默认的事务隔离级别。MYSQL 默认为 REPEATABLE_READ,SQLSERVER、Oracle 默认为 READ_COMMITTED
  2. ISOLATION_READ_UNCOMMITTED:读未提交,允许另外一个事务可看到这个事务未提交的数据;最低级别,可能有脏读、不可重复读、幻读
  3. ISOLATION_READ_COMMITTED:读已提交,保证一个事务修改的数据提交后才能被另一事务读取,而且能看到该事务对已有记录的更新;新解决了脏读
  4. ISOLATION_REPEATABLE_READ:可重复读,保证一个事务修改的数据提交后才能被另一事务读取,但不能看到该事务对已有记录的更新;新解决了不可重复读
  5. ISOLATION_SERIALIZABLE:一个事务在执行的过程中完全看不到其他事务对数据库所做的更新。新解决幻读

Spring 事务管理

Spring框架提供了强大的事务管理功能,允许在应用程序中轻松管理数据库事务

  • 在Spring的事务管理中,TransactionSynchronizationManager类扮演着重要的角色,而registerSynchronization方法是其中一个关键方法。

  • 通过注册事务同步回调可以确保在事务的不同阶段执行特定的操作,从而更好地管理资源、记录日志、发送通知等。

TransactionSynchronizationManager

事务同步管理器是Spring框架中用于管理事务同步的工具类。让我们能监听 Spring 的事务操作。

  • 在一个事务中,可能涉及到多个资源,如数据库连接、消息队列等。
  • TransactionSynchronizationManager允许注册事务同步回调,以便在事务提交、回滚或完成时执行特定的操作。
  • registerSynchronization方法:用于向事务中注册一个同步回调对象,以便在事务的不同生命周期阶段执行特定的逻辑。

为什么要使用?

  • 主要目的是在事务管理中执行与事务状态相关的操作。

以下是一些常见的使用情景:

  1. 清理资源:在事务提交后,清理一些资源,如关闭数据库连接、释放文件句柄等。注册一个事务同步回调可以确保这些清理操作在事务成功提交后执行,而不会在事务回滚时执行。
  2. 发送通知:可以在事务成功提交后发送通知,通知其他系统或服务,事务已成功完成。
  3. 记录日志:在事务完成后,记录一些事务相关的日志信息,以便后续的审计或故障排除。
  4. 资源协调:在某些情况下,协调多个资源的操作,例如在事务提交后触发消息发布。

总之提供了一种在Spring事务中注册自定义操作的机制,以便更好地管理事务的生命周期。

方法

TransactionSynchronizationManager.registerSynchronization方法

  1. .registerSynchronization():注册一个监听器,需要传入一个对象TransactionSynchronization。
  2. .isSynchronizationActive():获取到当前是否存在事务。

回调方法

TransactionSynchronization接口定义了不同类型的回调方法,允许在不同的事务生命周期阶段执行操作。

  1. beforeCommit(boolean readOnly):在事务提交前执行。可以在这里执行一些与事务相关的操作,也可以根据readOnly参数判断事务是否为只读。
  2. beforeCompletion():在事务完成前执行。提供了一个在事务最后一刻执行操作的机会,可以用来执行一些清理工作或其他必要的操作。
  3. afterCommit():在事务成功提交后执行。是执行一些清理操作或发送通知的好地方。
  4. afterCompletion(int status):在事务完成后执行,参数status表示事务的状态。可以根据不同的状态执行不同的操作,如记录日志或触发其他业务逻辑。

使用示例

下面的示例包含多个回调方法,展示了如何在Spring事务管理中注册和使用事务同步回调:

  • 首先使用@Transactional注解标记了一个方法,表示这个方法是一个事务方法。
  • 在方法内部,执行了一些数据库操作,
  • 然后通过TransactionSynchronizationManager.registerSynchronization方法注册了一个事务同步回调对象
  • 这个回调对象实现了TransactionSynchronization接口,并在不同的回调方法中定义了在不同事务生命周期阶段要执行的逻辑。
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
@Transactional
@Service
@Valid
@Slf4j
public class MemberUserServiceImpl implements MemberUserService {

    public void performSomeDatabaseOperations() {
        // 执行数据库操作
        
        // 注册事务同步回调
        TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
            @Override
            public void afterCommit() {
                // 在事务提交后执行的逻辑
                // 可以进行一些清理工作或发送通知
            }

            @Override
            public void afterCompletion(int status) {
                // 在事务完成后执行的逻辑
                // status表示事务的状态,可以根据不同状态执行不同的操作
            }

            @Override
            public void beforeCommit(boolean readOnly) {
                // 在事务提交前执行的逻辑
                // readOnly表示事务是否为只读
            }

            @Override
            public void beforeCompletion() {
                // 在事务完成前执行的逻辑
            }
        });
    }
    
    //当前事务提交后方可进行异步任务,防止异步任务先于未提交的事务执行
    private void callBack(Invoice invoice){ // private
        boolean synchronizationActive = TransactionSynchronizationManager.isSynchronizationActive();
        if (synchronizationActive) { // 当前存在事务,在事务提交后执行
            TransactionSynchronizationManager.registerSynchronization(
                    new TransactionSynchronizationAdapter() {
                        @Override
                        public void afterCommit() { // 监听事务提交完成
                            doCall(invoice);
                        }
                    }
            );
        } else {
             // 当前不存在事务,直接执行
            doCall(invoice);
        }
    }
    
    @Override
    @Transactional(rollbackFor = Exception.class) //
    public MemberUserDO createUserIfAbsent(String mobile, String registerIp, Integer terminal) {
        // 用户已经存在
        MemberUserDO user = memberUserMapper.selectByMobile(mobile);
        if (user != null) {
            return user;
        }
        // 用户不存在,则进行创建
        return createUser(mobile, null, null, registerIp, terminal);
    }
}

Spring 框架中的设计模式

  1. 工厂模式IoC 容器的两种实现:通过 BeanFactoryApplicationContext 接口创建 Bean 对象。
  2. 单例模式:Bean 默认为单例模式,只有一个实例;Spring 中 Bean 的默认作用域就是 singleton。Spring 可通过 XML 或注解方式配置 Bean 的作用域来实现单例。
    1. Spring 通过 ConcurrentHashMap (线程安全)实现单例注册表的特殊方式实现单例模式。
      1. xml : <bean id="userService" class="top.UserService" scope="singleton"/>
      2. 注解:@Scope(value = "singleton")
    2. private final static AreaUtils INSTANCE = new AreaUtils();
  3. 代理模式AOP 的实现用到 JDK 动态代理和 CGLIB 字节码生成技术;
  4. 模板模式:用来解决代码重复的问题。如 RestTemplate, JmsTemplate, JpaTemplate、jdbcTemplate、hibernateTemplate 等以 Template 结尾的对数据库操作的类。
  5. 包装器(Wrapper)模式 : 可根据需求动态切换不同的数据源,使接口不兼容的类一起工作。项目需要连接多个数据库,而且不同的客户在每次访问中根据需要会去访问不同的数据库。
  6. 适配器模式 : 如 Spring AOP 的通知(Advice),spring MVC 中也是用到了适配器模式适配 Controller
  7. 装饰者模式:
  8. 观察者模式:定义对象间的依赖关系,一个对象发生改变时,所有依赖它的对象都会被动更新。
    • 如,事件驱动模型,每次添加商品时都需重新更新索引,Spring 中 listener 的实现 ApplicationListener
0%