Spring

Quick Start

Intro to IoC and DI

Inversion of Control is a principle in software engineering which transfers the control of objects or portions of a program to a container or framework.

Connecting objects with other objects, or “injecting” objects into other objects, is done by an assembler rather than by the objects themselves.

控制反转作为软件工程里的一项原则,能够将对象或部分程序的控制权转移到容器或者框架。依赖注入的核心是通过构造函数、属性或者方法参数的方式将依赖关系传递给对象,而不是让对象自己去创建和管理这些依赖关系。总的来说,依赖注入 DI 则正是这种松耦合编程思想 IoC 的具体实现方式。

Spring IoC Container

An IoC container is a common characteristic of frameworks that implement IoC.

The Spring container is responsible for instantiating, configuring & assembling objects known as beans, as well as managing their life cycles.

Spring 中 IoC 容器的两种获取方式:BeanFactory 和 ApplicationContext 接口。

Beans're special type of Pojos. There're some restrictions on POJO to be a bean.

In order to assemble beans, the container uses configuration metadata, which can be in the form of XML configuration or annotations.

在组装时,容器所使用的配置元数据是一个描述性文件,其中包含了与配置属性交互的必要信息。

在导入 Spring 坐标并定义 Spring 管理的类后,在 resources 目录创建 Spring 配置文件,配置对应类作为 Spring 管理的 bean。

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd">
    <bean id="helloComponent" class="com.xxx.HelloComponent"></bean>
</beans>
public class IOCTest {
    // 创建 IOC 容器对象
    private ApplicationContext iocContainer = new ClassPathXmlApplicationContext("helloComponent.xml");
    @Test
    public void testExperiment01() {
        // 根据 id 从 IoC 容器对象里获取 bean => 组件对象
        HelloComponent helloComponent = (helloComponent) iocContainer.getBean("helloComponent");
        helloComponent.HelloMethod();
    }
}

Spring Bean Scopes

The scope of a bean defines the life cycle and visibility in the contexts.

The latest version of the Spring framework defines 6 types of scopes: singleton, prototype, request, session, application and websocket. The last four scopes mentioned, request, session, application and websocket, are only available in a web-aware application.

bean 作用域默认为单例模式 singleton,即整个应用中只会创建一个 Bean 实例。

在 Spring 容器启动时,如果该 Bean 的作用域为 singleton 单例模式,那么 Spring 会自动创建该 Bean 实例并放入容器中,即在加载配置文件的过程中创建。而如果该 Bean 的作用域为 prototype 原型模式,那么 Spring 在加载配置文件时只会创建 Bean 的定义信息,而不会创建 Bean 实例。对于作用域为 singleton 的 Bean,当容器需要该 Bean 时,直接从容器中获取即可,不需要再创建新的 Bean 实例;而对于作用域为 prototype 的 Bean,每次从容器中获取时都会创建新的 Bean 实例。

Spring 管理的 bean 可以是可重用的组件,例如 service、dao layer objects 和工具对象等,这些组件的状态在整个应用程序生命周期中保持不变,因此适合使用单例模式。但对于一些有状态的组件,例如封装实体的域对象,不适合使用单例模式,因为它们的状态随着业务逻辑的不同而不断发生变化。如果在多个业务逻辑中共享同一个实例,可能会引发状态不一致等问题,因此这种情况下通常使用原型模式创建 bean。

需要注意的是,即使是有状态的 bean,有时也可以使用单例模式,前提是确保 bean 状态不会在不同的业务逻辑之间共享或交叉影响,可以通过使用线程本地变量等方式来实现。

<!--<bean id="myBean" class="com.<secret>.spring5.factorybean.MyBean"></bean>-->
<bean id="myBean" class="com.<secret>.spring5.factorybean.MyBean" scope="singleton"></bean>
@Test
public void testFBean(){
    ApplicationContext context = new ClassPathXmlApplicationContext("bean7.xml");
    MyBean myBean = context.getBean("myBean", MyBean.class);
    MyBean myBean1 = context.getBean("myBean", MyBean.class);
    System.out.println(myBean); // com.<secret>.spring5.factorybean.MyBean@42d8062c
    System.out.println(myBean1); // com.<secret>.spring5.factorybean.MyBean@42d8062c 
}

Instantiating Beans

在 Spring 中,可以通过以下几种方式实例化 bean:

构造函数创建:通过 <constructor-arg>@ConstructorProperties 指定构造函数的参数。

静态工厂方法创建:使用 <bean> 标签中的 factory-method 属性或者 @Bean 注解中的 factoryMethod 属性指定静态工厂方法。

实例工厂方法创建:使用 <bean> 标签中的 factory-bean 属性和 factory-method 属性或者 @Bean 注解中的 factoryBean 属性和 factoryMethod 属性指定实例工厂方法。

工厂 Bean 创建:使用 <bean> 标签中的 class 属性指定工厂 Bean,该工厂 Bean 会调用自己的 getObject 方法来创建 bean 实例。

通过注解实现:使用 @Component、@Service、@Repository、@Controller 等注解定义的类都可以通过 Spring 自动扫描并创建 bean 实例。

✔️ 通过实现 FactoryBean 接口:定义一个类实现 FactoryBean 接口,并在该类中实现 getObject()getObjectType() 方法。getObject() 方法用于返回实际的 bean 对象,getObjectType() 方法用于指定返回对象的类型。

FactoryBean 是一个接口,用于创建和管理其他 bean。例如,在集成 MyBatis 等第三方框架时,会使用 FactoryBean 来动态创建 Mapper 类的代理对象,或者用来创建动态数据源等。

对于实现了 FactoryBean 接口的类,Spring 在创建它的时候会将该类作为工厂来使用,而不是将该类的实例作为一个普通的 bean 来创建和管理。因此,在创建 FactoryBean 的时候并不会同时创建和管理它所产生的 bean,而是在实际使用该 bean 时才会触发产生。这种方式也被称为“延迟加载”。

但是并不是所有通过 FactoryBean 创建的 bean 都是延迟加载的。如果 FactoryBean 的作用是创建普通的 bean,而非代理或其他的工厂,那么它所创建的 bean 就不是延迟加载的。

public class XxxDaoFactoryBean implements FactoryBean<XxxDao>{
    // 代替原始实例工厂中创建对象的方法
    @Override
    public XxxDao getObject() throws Exception {
        return new XxxDaoImpl();
    }
    @Override
    public Class<?> getObjectType() {
        return XxxDao.class;
    }
}
<bean id="xxxDao" class="com.???.factory.XxxDaoFactoryBean"/>
public class TestFactoryBean {
    @Test
    public void testFBean(){
        ApplicationContext ctx = new ClassPathXmlApplicationContext("fbBeam.xml");
        XxxDao xxxDao = (XxxDao) ctx.getBean("xxxDao");
        xxxDao.yyy();
    }
}

Lifecycle Callbacks

在 Spring 容器中,每个 bean 都有一个生命周期,即 bean 实例化、初始化、使用和销毁等一系列过程。Spring 提供了多种方式可以让开发者在 bean 生命周期的不同阶段执行特定的逻辑,其中 Lifecycle Callbacks 是一种常用的方式。

Spring 提供了三种类型的 Lifecycle Callbacks:初始化方法、销毁方法和自定义方法。

  1. 初始化方法:初始化方法会在 bean 实例化后,依赖注入完成后执行

Spring 提供了三种方式定义初始化方法:在方法上添加 @PostConstruct 注解;在 XML 配置文件中配置 init-method 属性来指定无参数且无返回值的回调方法;实现 InitializingBean 接口的 afterPropertiesSet() 方法。

  1. 销毁方法:销毁方法会在容器销毁 bean 实例时执行

Spring 提供了三种方式定义销毁方法:在方法上添加 @PreDestroy 注解;在 XML 配置文件中配置 destroy-method 属性来指定无参数且无返回值的回调方法;实现 DisposableBean 接口的 destroy() 方法。

  1. 使用自定义方法实现 Lifecycle Callbacks

自定义方法需要在 bean 定义中配置 MethodInvokingFactoryBean 和自定义方法的信息。

<bean id="myBean" class="com.example.MyBean">
    <property name="myProperty" value="someValue"/>
    <property name="myCallback">
        <bean class="org.springframework.beans.factory.config.MethodInvokingFactoryBean">
            <property name="targetObject" ref="myBean"/>
            <property name="targetMethod" value="myCustomMethod"/>
        </bean>
    </property>
</bean>

Spring IoC 容器提供扩展接口 BeanPostProcessor,用于在 bean 实例化之后和初始化之前,对 bean 对象进行一些自定义的操作,该接口中定义了两个回调方法:

  • 方法 postProcessBeforeInitialization 在 bean 实例初始化前调用
  • 方法 postProcessAfterInitialization 在 bean 实例初始化后调用

需要注意的是,后置处理器 BeanPostProcessor 会对当前配置文件中的所有 bean 都生效。

<bean id="myBeanPost" class="com.<secret>.spring5.lifeCycle.MyBeanPost"/>
public class MyBeanPost implements BeanPostProcessor {
    @Nullable
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        System.out.println("在初始化之前执行的方法");
        return bean;
    }
    @Nullable
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        System.out.println("在初始化之后执行的方法");
        return bean;
    }
}

Dependency Injection

Dependency Injection in Spring can be done through constructors, setters or fields.

依赖注入分别存在通过 setter 和构造器注入简单或引用类型的四种方式。

Setter-based DI

setter 注入是通过容器在实例化 bean 后,调用其 setter 方法来实现的。

setter 注入引用类型步骤:

  • 定义一个类并声明一个属性,同时提供一个对应的 setter 方法
  • 在 Spring 配置文件中配置该 Bean,并在配置中指定对另一个 Bean 的引用
  • Spring 容器实例化该 Bean 且调用其 setter 方法,将对另一个 Bean 的引用注入到 Bean 中
public class XxxServiceImpl implements XxxService {
    private XxxDao xxxDao;
    public void setXxxDao(XxxDao xxxDao) {
      this.xxxDao = xxxDao;
    }
}
<bean id="xxxDao" class="com.xxx.dao.impl.XxxDaoImpl">
<bean id="xxxService" class="com.xxx.service.impl.XxxServiceImpl">
    <property name="xxxDao" ref="xxxDao" />
</bean>

setter 注入简单类型步骤:

  • 定义一个类并声明一个属性,同时提供一个对应的 setter 方法
  • 在 Spring 配置文件中配置该 Bean,并在配置中指定该属性的值
  • Spring 容器实例化该 Bean,并调用该 Bean 的 setter 方法,将属性值注入到 Bean 中
public class XxxServiceImpl implements XxxService {
    private int connectionNumber;
    public void setConnectionNumber(int connectionNumber) {
      this.connectionNumber = connectionNumber;
    }
}
<bean id="xxxService" class="com.xxx.service.impl.XxxServiceImpl">
    <property name="connectionNumber" value="6" />
</bean>

Constructor-Based DI

如果一个类没有定义任何的构造函数,那么编译器会默认提供一个无参的构造函数。但是,如果定义了有参构造器,则编译器不再提供默认的无参构造函数。因此,在进行依赖注入时,如果使用构造函数注入,且类中只定义了有参构造器,那么需要在类中显式地提供一个无参构造器,否则会抛出异常。如果使用 setter 注入,则无需提供无参构造器。

构造器注入是通过在 bean 定义中指定参数类型和值来实现的。

构造器注入引用类型:在 bean 定义中使用 <constructor-arg> 指定参数引用的 bean,可以使用 ref 属性来指定引用的 bean 的 id。

public class XxxServiceImpl implements XxxService {
    private XxxDao xxxDao;
    public void setXxxDao(XxxDao xxxDao) {
      this.xxxDao = xxxDao;
    }
}
<bean id="xxxDao" class="com.xxx.dao.impl.XxxDaoImpl">
<bean id="xxxService" class="com.xxx.service.impl.XxxServiceImpl">
    <constructor-arg name="xxxDao" ref="xxxDao" />
</bean>

构造器注入简单类型:在 bean 定义中使用 <constructor-arg> 指定参数的值,可以使用 value 属性或 type 属性和 value 属性组合来指定参数的类型和值。

public class XxxServiceImpl implements XxxService {
    private int connectionNumber;
    public void setConnectionNumber(int connectionNumber) {
      this.connectionNumber = connectionNumber;
    }
}
<bean id="xxxService" class="com.xxx.service.impl.XxxServiceImpl">
    <constructor-arg name="xxxDao" value="6" />
</bean>

需要注意的是,当一个 bean 定义中存在多个构造函数时,可以通过 index 属性或 type 属性和 name 属性组合来指定使用哪个构造函数进行注入。

<bean id="exampleBean3" class="com.example.ExampleBean3">
    <constructor-arg index="0" type="java.lang.String" value="example"/>
    <constructor-arg index="1" name="exampleBean2" ref="exampleBean2"/>
</bean>

Field-Based DI

在需要注入 bean 的类中定义需要注入的字段,并使用 @Autowired@Resource 注解标记需要注入的字段。使用 Field-Based DI 的依赖注入时,需要在类中使用注解或 XML 配置显式地告诉 Spring 容器要注入哪个 bean 对象,否则会抛出 NullPointerException。

基于 XML 配置的 Field-Based DI 的步骤:

  • 在 XML 配置文件中给需要自动装配的 bean 添加值为 byTypebyName 的 autowire 属性
  • 在需要自动装配的属性上使用 @Autowired 注解或 @Resource 注解
public class MyBean {
  @Autowired
  private MyDependency myDependency;
 
  // bean methods
}
<bean id="myDependency" class="com.example.MyDependency">
  <!-- bean properties -->
</bean>

<bean id="myBean" class="com.example.MyBean" autowire="byType">
  <!-- bean properties -->
</bean>

注意:需确保被自动装配的属性是非私有的(即 public 或 protected),且存在相应的 setter 方法。否则将无法自动装配该属性。

使用纯注解进行 Field-Based DI 的步骤如下:

  • 在类上加上 @Component 或其他相关注解,将该类交由 Spring IoC 容器进行管理
  • 在需要注入的字段上加上 @Autowired 或其他相关注解,表示该字段需要被自动装配

注意:如果有多个实现类,可以在该字段上再加上 @Qualifier 注解,指定要注入的实现类;如果该字段允许为 null,可以在该字段上加上 @Nullable@Autowired(required = false) 注解。

CData section − Characters between these two enclosures are interpreted as characters, and not as markup. This section may contain markup characters (<, >, and &), but they are ignored by the XML processor.

在 XML 里,如果 bean 中的属性值有包含特殊符号,如 <、>、& 等,那么需要对其进行转义处理。可以使用 <![CDATA[ ]]> 标记将特殊符号包裹起来,从而避免转义。

<bean id="myBean" class="com.example.MyBean">
    <property name="myProperty" value="Hello <![CDATA[&]]> World"/>
</bean>

p & c-namespace

在 Spring 的 XML 配置文件中,p-namespace 和 c-namespace 都是用于简化配置的命名空间。

The p-namespace lets you use the bean element’s attributes (instead of nested <property/> elements) to describe your property values collaborating beans, or both.

p-namespace 可以用来简化 <bean> 元素中的属性配置。通常在 XML 配置文件中,配置一个属性需要使用 <property> 标签,而使用 p-namespace,则可以通过 p:属性 替代 <property> 标签。

<bean id="exampleBean" class="com.example.ExampleBean">
    <property name="prop1" value="value1" />
    <property name="prop2" value="value2" />
    <property name="prop3" value="value3" />
</bean>
<bean id="exampleBean" class="com.example.ExampleBean" 
      p:prop1="value1" p:prop2="value2" p:prop3="value3" />

The c-namespace, introduced in Spring 3.1, allows inlined attributes for configuring the constructor arguments rather then nested constructor-arg elements.

c-namespace 是 Spring 4.1 引入的新特性,用于简化 <constructor-arg> 中的构造函数参数配置。

<bean id="exampleBean" class="com.example.ExampleBean">
    <constructor-arg index="0" value="value1" />
    <constructor-arg index="1" value="value2" />
    <constructor-arg index="2" value="value3" />
</bean>
<bean id="exampleBean" class="com.example.ExampleBean"
      c:_0="value1" c:_1="value2" c:_2="value3" />

Injecting Collection

在 Spring 中,可以通过 XML 配置或注解的方式,将集合类型(数组、List、Map 和 Set)的属性注入到 Bean 中。

注入集合类型属性的步骤:

  • 声明集合属性:在 Bean 中定义一个集合类型的属性,并提供一个对应的 setter 方法
  • 注入集合元素:可以使用 <list><set><map> 等标签来注入集合元素
    • 在 XML 配置文件中,使用 <property> 标签来注入集合类型的属性
    • 使用注解时,可以使用 @Value@Resource 等注解来注入集合元素

注入集合类型属性的注意点:

  • 集合类型的属性必须要提供一个对应的 setter 方法,否则 Spring 会在注入时抛出异常
  • 集合元素可以使用 <value> 注入简单类型的元素,也可以使用 <ref> 来注入 Bean 对象
  • 通过注解注入集合类型属性时,集合元素的注入可通过 @Value@Resource 注解来完成
  • 集合类型属性可以通过 <util:list><util:set><util:map> 等标签来定义
public class XxxDaoImpl implements XxxDao {
    private int[] array;
    private List<String> list;
    private Map<String, String> map;
    private Set<String> set;
    private Properties properties;
    public void setArray(int[] array) { this.array = array; }
    public void setList(List<String> list) { this.list = list; }
    public void setMap(Map<String, String> maps) { this.maps = maps; }
    public void setSet(Set<String> sets) { this.sets = sets; }
    public void setProperties(Properties properties) { this.properties = properties; }
    public void test(){
        System.out.println(Arrays.toString(array));
        System.out.println(list);
        System.out.println(map);
        System.out.println(set);
        System.out.println(properties);
    }
}
<bean id="xxxDao" class="com.???.dao.impl.XxxDaoImpl">
    <property name="array">
        <array>
            <value>1</value>
            <value>2</value>
            <value>3</value>
        </array>
    </property>
    <property name="list">
        <list>
            <value>yes</value>
            <value>ok</value>
        </list>
    </property>
    <property name="map">
        <map>
            <entry key="JAVA" value="java"/>
            <entry key="JAVASCRIPT" value="javascript"/>
        </map>
    </property>
    <property name="set">
        <set>
            <value>MySQL</value>
            <value>Redis</value>
        </set>
    </property>
    <property name="properties">
        <props>
            <prop key="msg">msgcontext</prop>
        </props>
    </property>
</bean>
public class TestL {
    @Test
    public void testCollection1(){
        ApplicationContext ctx = new ClassPathXmlApplicationContext("beanDI.xml");
        XxxDao xxxDao = (XxxDao) ctx.getBean("xxxDao");
        xxxDao.test();
    }
}

在注入 bean 集合类型属性时,对于存在可复用的集合,应抽取单独封装。

/* 集合注入提取 */
public class BookExtract {
    private List<String> list;
    public void setList(List<String> list) { this.list = list; }
    public void test(){ System.out.println(list); }
}
<!-- 集合注入部分提取 -->
<util:list id="bookList">
  <value>js</value>
  <value>java</value>
  <value>nodejs</value>
</util:list>
<bean id="bookExtract" class="com.<secret>.spring5.bean.BookExtract">
  <property name="list" ref="bookList"></property>
</bean>
public class TestL {
    // 集合注入部分抽取测试
    @Test
    public void testCollection2(){
        ApplicationContext context = new ClassPathXmlApplicationContext("bean6.xml");
        BookExtract be = context.getBean("bookExtract", BookExtract.class);
        be.test();
    }
}

Properties File DI

当需要在 Spring 中注入外部文件时,可以使用 context:property-placeholder 元素实现。

Properties File DI 主要步骤如下:

  • 将外部文件放置在应用程序的 classpath 中,例如 classpath:config.properties
  • 在 Spring 配置文件中,添加 context 命名空间
  • 使用 context:property-placeholder 元素配置外部文件的位置和属性占位符的格式
  • 在需要使用属性的 bean 中,通过 ${propertyName} 形式引用属性值
jdbc.driverClass=com.mysql.cj.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/???
jdbc.userName=root
jdbc.password=yesok
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
<context:property-placeholder location="classpath:config.properties" />
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
    <property name="driverClassName" value="${jdbc.driverClass}"/>
    <property name="url" value="${jdbc.url}"/>
    <property name="username" value="${jdbc.userName}"/>
    <property name="password" value="${jdbc.password}"/>
</bean>

Spring Annotations

An alternative to XML setup is provided by annotation-based configuration, which relies on bytecode metadata for wiring up components instead of XML declarations. Instead of using XML to describe a bean wiring, the developer moves the configuration into the component class itself by using annotations on the relevant class, method, or field declaration. - docs.spring

Spring 2.5 之后的版本引入了对注解的支持,使用注解的方式配置 Bean,可以更加直观地了解 Bean 的属性和依赖关系,减少冗杂的 XML 配置(类名、属性、依赖关系)。

和配置文件一样,注解本身并不执行,仅做为标记使用。具体的功能是通过框架来检测到注解所标记的位置,然后针对这个位置按照注解标记的功能来执行对应的操作。

Core Annotations

This @Required annotation is applied on bean setter methods. Consider a scenario where you need to enforce a required property. The @Required annotation indicates that the affected bean must be populated at configuration time with the required property. Otherwise an exception of type BeanInitializationException is thrown.

The @Autowired annotation is applied on fields, setter methods, and constructors. This annotation injects object dependency implicitly.

上述中 "implicitly" 指的是 @Autowired 注解的自动注入,即无需显式地指定依赖对象,Spring 容器会根据类型和名称等信息自动寻找并注入对应的依赖对象,这样就不需要手动编写依赖注入的代码。简单来说,就是当一个类中的属性被 @Autowired 注解所标记时,Spring 容器会自动寻找匹配的依赖对象,并将其注入到该属性中。

Note: As of Spring 4.3, an @Autowired annotation on such a constructor is no longer necessary if the target bean defines only one constructor to begin with.

The @Qualifier is used to avoid conflicts in bean mapping and we need to provide the bean name that will be used for autowiring. This way we can avoid issues where multiple beans are defined for same type. This annotation usually works with the @Autowired annotation. For constructors with multiple arguments, we can use this annotation with the argument names in the method.

Consider an example where an interface BeanInterface is implemented by two beans BeanB1 and BeanB2.

@Component
public class BeanB1 implements BeanInterface {
  //
}
@Component
public class BeanB2 implements BeanInterface {
  //
}

Now if BeanA autowires this interface, Spring will not know which one of the two implementations to inject. One solution to this problem is the use of the @Qualifier annotation.

@Component
public class BeanA {
  @Autowired
  @Qualifier("beanB2")
  private BeanInterface dependency;
  ...
}

With the @Qualifier annotation added, Spring will know which bean to autowire where beanB2 is the name of BeanB2.

The @Configuration annotation is used on classes which define beans. The @Configuration is an analog for XML configuration file – it is configuration using Java class. Java class annotated with @Configuration is a configuration by itself and will have methods to instantiate and configure the dependencies.

The @ComponentScan annotation is used with @Configuration annotation to allow Spring to know the packages to scan for annotated components. @ComponentScan is also used to specify base packages using basePackageClasses or basePackage attributes to scan. If specific packages are not defined, scanning will occur from the package of the class that declares this annotation.

注意,@ComponentScan 注解可以替代 Spring XML 配置文件中的 <context:component-scan> 标签,用于指定要扫描的包,并将扫描到的带有 @Component@Service@Repository@Controller 等注解的类注册为 Spring 的 bean。与 <context:component-scan> 标签相比,@ComponentScan 注解更加简洁、直观,且可以避免手写 XML 配置文件的繁琐。

A @Bean annotation is used at the method level. @Bean annotation works with @Configuration to create Spring beans. As mentioned earlier, @Configuration will have methods to instantiate and configure dependencies. Such methods will be annotated with @Bean. The method annotated with this annotation works as bean ID and it creates and returns the actual bean.

The @Lazy annotation is used on component classes. By default all autowired dependencies are created and configured at startup. But if you want to initialize a bean lazily, you can use @Lazy annotation over the class. This means that the bean will be created and initialized only when it is first requested for. You can also use this annotation on @Configuration classes. This indicates that all @Bean methods within that @Configuration should be lazily initialized.

A @Value annotation is used at the field, constructor parameter, and method parameter level. The @Value annotation indicates a default value expression for the field or parameter to initialize the property with. As the @Autowired annotation tells Spring to inject object into another when it loads your application context, you can also use @Value annotation to inject values from a property file into a bean’s attribute. It supports both #{...} and ${...} placeholders.

Stereotype Annotations

The @Component annotation is used on classes to indicate a Spring component. The @Component annotation marks the Java class as a bean or say component so that the component-scanning mechanism of Spring can add into the application context.

The @Controller annotation is used to indicate the class is a Spring controller. This annotation can be used to identify controllers for Spring MVC or Spring WebFlux.

The @Service annotation is used on a class. The @Service marks a Java class that performs some service, such as execute business logic, perform calculations and call external APIs. This annotation is a specialized form of the @Component annotation intended to be used in the service layer.

The @Repository annotation is used on Java classes which directly access the DB. This annotation works as marker for any class that fulfills the role of repository or Data Access Object. This annotation has an automatic translation feature.

For example, when an exception occurs in the @Repository there's a handler for that exception and there is no need to add a try catch block.

在组件扫描机制中,被扫描的组件应添加特定的注解(如 @Component),以便被 Spring 容器识别为组件并进行注册。此外,组件扫描配置通常需要在配置文件中进行指定,可以通过在配置文件中添加元素 <context:component-scan> 来启用组件扫描机制。

@Component(value = "userAnnot")
public class UserAnnot {
    public void add(){ System.out.println("Annot add..."); }
}

上述代码定义了一个名为 UserAnnot 的类,并使用 @Component 注解来将该类声明为一个组件,且通过 value = "userAnnot" 指定了该组件的名称(该名称将被用于在容器中注册此组件)。

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
    <context:component-scan base-package="com.???.spring5.wkAnnot"/>
</beans>
/* 注解测试类 */
public class TestAnno {
    @Test
    public void testAnno1(){
        ApplicationContext ctx = new ClassPathXmlApplicationContext("bbean1.xml");
        UserAnnot ua = ctx.getBean("userAnnot", UserAnnot.class);
        System.out.println(ua);
        ua.add();
    }
}

当在其他地方需要使用到注册在容器中的组件类时,可以通过 @Autowired 等注解来自动注入该组件的实例,从而完成对象的依赖注入。

常用的 <context:component-scan> 元素属性如下:

  • base-package 属性指定了需要扫描的组件包路径
  • use-default-filters 属性用于指定是否使用默认的过滤器
    • exclude-filter 元素用于配置需要排除的组件
      • type 属性指定过滤器类型
      • expression 属性指定需要排除的注解
    • include-filter 元素用于配置需要包含的组件
      • type 属性指定过滤器类型
      • expression 属性指定需要包含的组件的正则表达式
<context:component-scan base-package="com.example.app" use-default-filters="false">
    <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
    <context:include-filter type="regex" expression="com\.example\.app\.service\..*Service"/>
</context:component-scan>

上述示例通过控制扫描的范围,来避免将不必要的组件注册到 Spring 容器,代码解释如下:

  • <context:exclude-filter> 使用 annotation 类型的过滤器,排除 @Controller 注解
  • <context:include-filter> 使用 regex 类型的过滤器,指定要包括 service 包下的特定类

Using @PropertySource

The @PropertySource annotation provides a convenient and declarative mechanism for adding a PropertySource to Spring’s Environment. Any ${…} placeholders present in a @PropertySource resource location are resolved against the set of property sources already registered against the environment.

Spring 3 中加入的 @PropertySource 注解可以指定属性文件的位置,从而将属性文件中的键值对注入到 Spring 管理的 bean 中。

以下代码中,@Configuration 用于表明这是一个 Spring 的配置类,等价于传统 XML 配置文件中的 <beans> 元素。注解 @ComponentScan 用于扫描指定包下的组件,并将其中含有特定注解的类注册为 Spring 的 bean。@PropertySource 指定属性文件的位置,可以通过 valuelocation 指定属性文件的路径。在本例中,classpath:jdbc.properties 表示属性文件在类路径下。

public class TestPureAnnot {
    @Test
    public void TestPureAnnot(){
        ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
        UserService userservice = ctx.getBean("userService", UserService.class);
        userservice.usMethod();
    }
}
@Configuration
@ComponentScan(basePackages = {"com.<secret>.spring5.annoDev"})
@PropertySource("classpath:jdbc.properties")
public class SpringConfig {}

classpath(类路径)是 Java 编程中指定 JVM 查找 class 文件的路径。当 JVM 加载 class 文件时,会从 classpath 中查找对应的 class 文件,如果找到则加载该类。classpath 可以包含多个路径,路径之间使用操作系统的路径分隔符(Windows 上为 ";",Unix/Linux 上为 ":")进行分隔。

Spring Autowiring

自动装配是指 Spring 容器在需要时自动地将某个 bean 注入到另一个 bean 中的过程,在该过程中,无需手动调用 set 方法或构造函数进行显式的指定。

自动装配通过使用 @Autowired@Qualifier@Resource@Value 等注解实现,可以大大减少手动配置 bean 的工作量。

  • @Autowired 根据 bean 类型进行装配
  • @Qualifier 根据 bean 名称进行装配
  • @Resource 可以根据类型或名称进行装配
  • @Value 可以注入各种类型的值,包括简单类型、Spring EL 表达式、属性文件中的值等

在自动装配过程中,Spring 容器根据类型或名称等信息自动寻找合适的 bean 进行注入。如果存在多个类型相同或名称相同的 bean,则需要使用 @Qualifier 进行进一步地指定。

需要注意的是,自动装配依赖于 Spring 容器中的 bean 配置信息,如果没有配置或者配置不正确,则可能会出现装配失败的情况。同时,使用自动装配也需要注意 bean 之间的循环依赖问题,避免出现死锁或其他异常情况。自动装配基于反射设计创建对象并初始化属性值,因此无需提供 setter 方法。

对于构造方法,虽然自动装配建议使用无参构造方法,但如果没有无参构造方法,也可以通过有参构造方法进行自动装配,只要在构造方法上标注 @Autowired 就可以了。

@Service
public class UserService {
//    @Autowired // 根据类型进行注入
//    @Qualifier(value="userDaoImpl1")
//    private UserService userDao;
//    @Resource // 根据类型进行注入
    @Resource(name = "userDaoImpl1") // 根据名称进行注入
    private UserDao userDao;
    @Value(value="hz") // 注入普通类型属性
    private String name;
    public void usMethod(){
        System.out.println("service usMethod");
        userDao.udiMethod();
        System.out.println(name);
    }
}
@Repository(value="userDaoImpl1")
public class UserDaoImpl implements UserDao{
    @Override
    public void udiMethod() { System.out.println("impl ado udiMethod"); }
}
@RunWith(SpringRunner.class)
@ContextConfiguration(classes = {UserService.class, UserDaoImpl.class})
public class UserServiceTest {
    @Autowired
    private UserService userService;
    @Test
    public void testUsMethod() {
        userService.usMethod();
    }
}

注意,在上述代码中,@ContextConfiguration@Configuration 是有区别的。

@Configuration 是一个用于定义配置类的注解,其中包含 @Bean 注解的方法,用于定义和配置 bean。通过 @Configuration@Bean 定义的 bean 可以被 Spring 容器管理和使用。

@ContextConfiguration 是用于指定一个或多个用于测试的配置类或 XML 文件的注解。在上述的测试代码中,@ContextConfiguration 用于加载 Spring 配置,以便能够使用 Spring 的依赖注入和其他功能来测试应用程序的不同部分。

Using a Third-Party Bean

Spring 中的第三方 bean 可以采取独立的配置类进行管理,但这个配置类必须使用 @Configuration 注解来标记,可以使用 @Bean 注解声明返回的对象是一个 bean。这些 bean 需要手动添加到 Spring 的核心配置中。

另外,也可以使用导入式注解 @Import 或扫描式注解 @ComponentScan 来管理第三方 bean。

  • @Import 注解用于引入其他配置类
  • @ComponentScan 注解用于自动扫描指定包中的类,找到标记为 @Component 或其他相关注解的类并将其注册为 bean

但是,在使用 @ComponentScan 注解时,需要将其放置在另一个 @Configuration 注解中。

public class JdbcConfig {
    @Bean
    public DataSource dataSource(){
        DruidDataSource ds = new DruidDataSource();
        ds.setDriverClassName("com.mysql.jdbc.Driver");
        ds.setUrl("jdbc:mysql://localhost:3306/???");
        ds.setUsername("root");
        ds.setPassword("yesok");
        return ds;
    }
}
@Configuration
@Import({JdbcConfig.class})
public class SpringConfig {}

上述中,JdbcConfig 类使用 @Bean 标注了一个返回值为 DataSource 的方法,表示这是一个 Bean 定义,Spring 容器将会负责创建这个 Bean,然后放到容器中供其他对象使用。SpringConfig 类通过注解 @Configuration 表明这是一个配置类,同时使用 @Import 导入了 JdbcConfig 类。JdbcConfig 中的 Bean 会被纳入 SpringConfig 配置类管理的范围,从而可以在 Spring 容器中被使用。

要注意,如果使用 @ComponentScan 替换 @Import,则需要在配置类上添加 @Configuration 注解,并在注解中指定需要扫描的包路径。这是因为在使用 @Import 导入配置类时,Spring 会自动将该类识别为一个配置类。

如果将配置类作为一个单独的类使用,如:XML 配置文件中使用 <bean> 标签声明一个配置类,那么需要在类上加上 @Configuration 注解来告诉 Spring 这是一个配置类。

@Configuration
public class JdbcConfig {
    @Bean
    public DataSource dataSource(){
        DruidDataSource ds = new DruidDataSource();
        ds.setDriverClassName("com.mysql.jdbc.Driver");
        ds.setUrl("jdbc:mysql://localhost:3306/???");
        ds.setUsername("root");
        ds.setPassword("yesok");
        return ds;
    }
}
@Configuration
@ComponentScan({"com.xxx.config"})
public class SpringConfig {}

第三方 bean 的依赖注入通常有两种方式:

  • 简单类型注入:注解 @Value 用于将配置文件中的属性值注入到 bean 中的属性里
  • 引用类型注入:可以通过为返回 bean 的方法设置相应的形参进行注入
public class JdbcConfig {
    @Value("com.mysql.jdbc.Driver");
    private String driver;
    @Value("jdbc:mysql://localhost:3306/???");
    private String url;
    @Value("root");
    private String userName;
    @Value("password")
    private String password;
    @Bean
    public DataSource dataSource(XxxService xxxService){
        System.out.println(xxxService);
        DruidDataSource ds = new DruidDataSource();
        ds.setDriverClassName(driver);
        ds.setUrl(url);
        ds.setUsername(userName);
        ds.setPassword(password);
        return ds;
    }
}

上述代码定义了一个被 @Configuration 注解标记的 JdbcConfig 配置类。该类中的 dataSource 方法使用 @Bean 注解,表示返回的对象应该被 Spring 容器管理为一个 bean。该方法返回 DataSource 接口的实现类 DruidDataSource 对象,并对其进行了一些初始化配置,例如数据库连接的 URL、用户名和密码等。Spring 容器会在初始化时自动调用该方法,并将其返回的对象添加到容器中。

@Configuration
@ComponentScan({"com..???..config","com.???.service"})
@Import({JdbcConfig.class})
public class SpringConfig {}

Aspect Oriented Programming

AOP Concepts & Terminology

Cross-cutting concerns are implementation details that need to be kept separate from business logic.

面向切面编程是一种编程范式,与面向对象编程类似,但是面向切面编程会将关注点从对象的内部状态和行为转移到对象之外的横切关注点上。横切关注点是指在应用程序的不同模块中,会涉及到的一些与核心业务逻辑无关但又必须被处理的逻辑,例如事务、安全、日志、缓存等。这些逻辑通常散布在不同的类和方法中,导致代码复杂性和重复性的增加。

AOP 的术语是一个通用的概念,不仅适用于 Spring,也适用于其他实现 AOP 的框架。

切面是一个横切关注点的抽象,通常由一组通知和切点组成。通过将切面织入到程序的不同执行点,可以在不改变原有代码的基础上对程序进行功能的增强。连接点是指在应用的执行期间,可以插入一个切面的点。例如,在方法的执行期间以及调用的前后。通知是切面在特定连接点处所执行的动作。通知有多种类型,如前置通知、后置通知、异常通知、最终通知等。切点是一组连接点的集合,用来定义通知将被应用到哪些连接点上。织入是将切面应用到目标对象并创建代理对象的过程。织入可在编译期、类加载期、运行期进行。

Advice is an action taken by an aspect at a particular join point. Different types of advice're as follows.

  • Before Advice (@Before): These advices runs before the execution of join point methods.
  • After (finally) Advice (@After): An advice that gets executed after the join point method finishes executing, whether normally or by throwing an exception.
  • After Returning Advice (@AfterReturning): Sometimes we want advice methods to execute only if the join point method executes normally.
  • After Throwing Advice (@AfterThrowing): This advice gets executed only when join point method throws exception, we can use it to rollback the transaction declaratively.
  • Around Advice (@Around): This advice surrounds the join point method and we can also choose whether to execute the join point method or not. We can write advice code that gets executed before and after the execution of the join point method. It is the responsibility of around advice to invoke the join point method and return values if the method is returning something.

@AspectJ Support

AOP 的常见实现包括 Spring AOP 和 AspectJ:

  • Spring AOP 是基于代理的 AOP 实现,即通过动态代理来实现横向切面的添加
  • AspectJ 是一种编译时的 AOP 实现,在编译时将切面逻辑直接编织到目标代码中

Spring 中的 AOP 是基于 Java 动态代理和 CGLIB 字节码增强来实现的,且遵循了通用原则和术语。

AspectJ 是一个流行的 AOP 框架,能与 Spring 很好的集成。以下代码是往 Maven 项目中引入 AspectJ 编译器和运行时的依赖。

<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>1.9.19</version>
</dependency>

注解操作 AspectJ 进行 AOP 开发:

  • 导入 AspectJ 相关坐标;spring-context 坐标默认依赖 spring-aop 坐标
  • 定义 AOP 接口与实现类
  • 定义通知类抽取通知,并通过 @Pointcut 定义切入点
  • 绑定切入点与通知关系,并指定通知添加到原始连接点的具体执行位置
  • 定义通知类接受 Spring 容器管理,且定义当前类为切面类
  • 在 Spring 核心配置中开启 Spring 对 AOP 注解的驱动支持
// 被增强类
@Component
public class User {
    public void uMethod(){ System.out.println("user method"); }
}

切入点的定义依托一个不具有实际意义的方法进行,即无参数,无返回值,无实际逻辑。AOP 通知类型查看 => Declaring Advice

With Spring, you can declare advice using AspectJ annotations, but you must first apply the @EnableAspectJAutoProxy annotation to your configuration class, which will enable support for handling components marked with AspectJ's @Aspect annotation.

// 增强类 or 通知类 => 不同方法代表不同通知内容
@Component // 生成 bean
@Aspect // 生成代理对象 & 作为 AOP 处理
@Order(3) // 多个增强类设置优先级
public class UserProxy {
    // 相同切入点抽取
    @Pointcut(value="execution(* com.<secret>.spring5.aopanno.User.uMethod())")
    public void pointExtraction(){

    }
    // 前置通知
    @Before(value="execution(* com.<secret>.spring5.aopanno.User.uMethod())")
    public void before(){
        // 前置通知
        System.out.println("before...");
    }
    // 后置通知 => 方法之后 => 异常也执行
    @After(value="pointExtraction()") // 重用切入点定义
    public void after(){
        System.out.println("after...");
    }
    // 返回通知 => 返回值之后执行
    @AfterReturning(value="pointExtraction()")
    public void afterReturning(){
        System.out.println("afterReturning...");
    }
    @AfterThrowing(value="execution(* com.<secret>.spring5.aopanno.User.uMethod())")
    public void afterThrowing(){
        System.out.println("afterThrowing...");
    }
    // 环绕通知
    @Around(value="execution(* com.<secret>.spring5.aopanno.User.uMethod())")
    public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable{
        System.out.println("around before...");
        Object ret = proceedingJoinPoint.proceed();
        System.out.println("around after...");
        return ret;
    }
}

The @AspectJ support can be enabled with XML- or Java-style configuration. In either case, you also need to ensure that AspectJ’s aspectjweaver.jar library is on the classpath of your application (version 1.9 or later). This library is available in the lib directory of an AspectJ distribution or from the Maven Central repository.

To enable @AspectJ support with Java @Configuration, add the @EnableAspectJAutoProxy annotation.

@Configuration
@EnableAspectJAutoProxy(proxyTargetClass = true)
public class ConfigAop {}

To enable @AspectJ support with XML-based configuration, use the aop:aspectj-autoproxy element.

<!-- 开启 Aspect 生成代理对象 -->
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>

xml 操作 AspectJ 进行 AOP 开发:

// 被增强类
public class Coffee { public void drink(){ System.out.println("drinking coffee..."); } }
// 代理增强类
public class CoffeeProxy { public void before(){ System.out.println("coffee with ice"); } }
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
                           http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
                           http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">
    <!-- 创建对象 -->
    <bean id="coffee" class="com.<secret>.spring5.aopxml.Coffee"></bean>
    <bean id="coffeeProxy" class="com.<secret>.spring5.aopxml.CoffeeProxy"></bean>
    <!-- 配置aop增强 -->
    <aop:config>
        <!-- 切入点 -->
        <aop:pointcut id="p" expression="execution(* com.???.spring5.aopxml.Coffee.drink(..))"/>
        <!-- 配置切面 -->
        <aop:aspect ref="coffeeProxy">
            <!-- 增强作用在具体的方法上 -->
            <aop:before method="before" pointcut-ref="p"/>
        </aop:aspect>
    </aop:config>
</beans>

Pointcut Expressions

Spring AOP 标签 aop:pointcut 中的表达式语言用于描述切点。切点在 AOP 中定义哪些方法应该被拦截或者增强。

A pointcut expression starts with a pointcut designator (PCD), which is a keyword telling Spring AOP what to match.

切入点指示符可以描述一个方法的修饰符、返回类型、方法名、参数等特征,从而定义出一个具有特定特征的方法集合。在 Spring AOP 中,切入点指示符通常是使用 "execution" 关键字来定义的。

// The format of an execution expression follows:
execution(modifiers-pattern? ret-type-pattern declaring-type-pattern? name-pattern(param-pattern) throws-pattern?)

在 XML 文件中通过 <aop:pointcut> 元素定义切入点的方式如下。匹配 com.example.MyService 类中所有名为 doSomething 的方法,并且这些方法的参数可以是任意类型和任意个数。

<aop:pointcut id="myPointcut" expression="execution(* com.example.MyService.doSomething(..))"/>

将上述写法转换为在 Java 中通过 @Pointcut 注解定义的切入点方式如下。

@Pointcut("execution(* com.example.MyService.doSomething(..))")

除上述 execution 之外,常见的 Spring PCD 如下:

  • execution:最主要的表示式,用来匹配方法执行的连接点。
  • within 指定匹配某个包或类中的所有方法
  • thistarget 都用于匹配某个对象或类的方法,前者匹配代理对象,后者匹配目标对象
  • args 用于匹配方法参数
  • @target 指定匹配带有特定注解的对象
  • @args 限制实参的运行时类型需具有指定的注释
  • @within 匹配拥有指定注解的类型
  • @annotation 用于匹配带有特定注解的方法

Note: Pointcut expressions can be combined using &&, ||, and ! operators.

AOP Workflow

Spring 容器启动时会读取所有在切面配置中定义的切入点,并初始化所有的 bean。对于每个 bean,容器会检查其所对应类中的方法是否匹配到任何一个切入点。如果没有匹配到任何一个切入点,容器会直接获取这个 bean 并调用它的方法执行。但是,如果匹配到了一个或多个切入点,容器会创建这个目标对象的代理对象,并根据代理对象的运行模式来运行原始方法和增强内容。这样,切面就可以在目标对象的方法执行前、执行后或执行前后进行增强处理,而不需要修改目标对象的代码。

通过动态代理增强类的方法:

  • 在有接口情况下使用 JDK 动态代理:创建接口实现类代理对象
  • 在没有接口情况下使用 CGLIB 动态代理:创建子类的代理对象
public interface UserDao {
    public int add(int a,int b);
    public String update(String str);
}
public class UserDaoImpl implements UserDao{
    @Override
    public int add(int a, int b) {
        System.out.println("add方法执行了");
        return a+b;
    }

    @Override
    public String update(String str) {
        System.out.println("update方法执行了");
        return str;
    }
}

在 JDKProxy 类中,使用 Proxy.newProxyInstance 方法创建一个 UserDao 接口的代理对象。

静态方法 Proxy.newProxyInstance 方法用于创建一个动态代理对象:

  • ClassLoader loader 是该代理类的类加载器
  • Class<?>[] interfaces 是代理类要实现的接口列表,代理对象会实现这些接口所有方法
  • InvocationHandler h 是一个实现了 invoke 方法的类,用于实现代理类的具体逻辑

在 UserDaoProxy 类中实现 InvocationHandler 接口的主要作用是提供代理对象的具体实现逻辑。

接口 InvocationHandler 中只有一个方法 invoke,该方法会在代理对象的方法被调用时被调用。

public class JDKProxy {
    public static void main(String[] args){
        Class[] interfaces = {UserDao.class};
        // 创建接口实现类的代理对象
        // 第三参数使用匿名内部类
//        Proxy.newProxyInstance(JDKProxy.class.getClassLoader(), interfaces, new InvocationHandler() {
//            @Override
//            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//                return null;
//            }
//        })
        UserDaoImpl userDao = new UserDaoImpl();
        // 接口等于实现类的代理对象
        UserDao dao = (UserDao)Proxy.newProxyInstance(JDKProxy.class.getClassLoader(), interfaces, new UserDaoProxy(userDao));
        int result = dao.add(1, 2);
        System.out.println(result);
    }
}

// 创建代理对象代码
class UserDaoProxy implements InvocationHandler{
    // 创建的是谁的代理对象,就将谁传递过来
    // 有参构造传递
    private Object obj;
    public UserDaoProxy(Object obj){
        this.obj = obj;
    }
    // 对象一创建,方法就调用
    // 增强的逻辑
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // 方法之前
        System.out.println("方法前执行 => "+method.getName()+" => 传递参数 => "+ Arrays.toString(args));
        // 被增强的方法执行
        Object res = method.invoke(obj, args);
        // 方法之后
        System.out.println("方法后执行 => "+obj);
        return res;
    }
}

当使用 Spring AOP 时,可能需要处理切入点相关的一些数据,如切入点方法的参数,返回值,以及异常。可以通过以下方式进行相关操作。

在通知方法中可以通过 JointPoint 参数获取切入点方法的参数,适用于前置、后置、返回、抛出异常后通知。

@Before("execution(public * com.example.demo.service.UserService.addUser(..))")
public void logBefore(JoinPoint joinPoint) {
    Object[] args = joinPoint.getArgs();
    // ...
}

可以在环绕通知中使用 ProceedingJoinPoint 对象来获取切入点方法的参数。这个对象允许在环绕通知中控制切入点方法的执行,且访问其参数。

@Aspect
public class MyAspect {
 
    @Around("execution(* com.example.service.MyService.*(..))")
    public Object aroundAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
 
        // 获取切入点方法的参数
        Object[] args = joinPoint.getArgs();
 
        // 对参数进行处理
        for (Object arg : args) {
            // ...
        }
 
        // 控制切入点方法的执行
        Object result = joinPoint.proceed();
 
        // 对返回值进行处理
        // ...
 
        return result;
    }
}

如果通知方法是在方法返回后执行的,可以使用 @AfterReturning 注解来获取返回值。此时需要使用 returning 属性来指定接收返回值的参数名。

@AfterReturning(value = "execution(public * com.example.demo.service.UserService.addUser(..))", returning = "result")
public void logAfterReturning(JoinPoint joinPoint, Object result) {
    // ...
}

使用 @Around 注解标记的环绕通知,可以直接调用 proceed() 方法获取原始方法的返回值。

@Around("execution(public * com.example.demo.service.UserService.addUser(..))")
public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
    Object result = joinPoint.proceed();
    // ...
    return result;
}

如果通知方法是在方法抛出异常后执行的,那么可以使用 @AfterThrowing 注解来获取异常信息。注意,需要使用 throwing 参数来指定接收异常信息的参数名。

@AfterThrowing(value = "execution(public * com.example.demo.service.UserService.addUser(..))", throwing = "ex")
public void logAfterThrowing(JoinPoint joinPoint, Exception ex) {
    // ...
}

如果是使用 @Around 注解标记的环绕通知,需要使用 try...catch 语句块捕获原始方法可能抛出的异常,并进行处理。

@Around("execution(public * com.example.demo.service.UserService.addUser(..))")
public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
    try {
        Object result = joinPoint.proceed();
        // ...
        return result;
    } catch (Exception ex) {
        // ...
        throw ex;
    }
}

Transactions with Spring

Spring Transactions Basics

事务方法是指在数据库事务中被调用的方法。在一个事务中,可能会涉及多个数据库操作,而这些操作需要保证原子性、一致性、隔离性和持久性,以保证数据库的完整性。事务方法可以确保这些操作被作为一个单独的逻辑单元执行,要么全部成功提交,要么全部失败回滚。

事务的隔离性是为解决在并发操作中产生的脏读、不可重复读、幻读等问题。脏读是一个未提交的事务读取了另一个未提交事务的数据;不可重复读表示一个未提交的事务读取到了另一个提交了事务修改的数据(数据不准确);幻读是一个未提交事务读取到了另一个提交事务添加的数据。

事务传播行为是指在事务方法被调用时,事务的范围如何传播。当一个事务方法被调用时,如果当前上下文中已经存在一个事务,那么事务传播行为规定了新的事务应该如何与当前事务进行交互。例如,如果一个事务方法被调用时当前上下文中已经存在一个事务,那么新的事务可以选择加入当前事务,或者开启一个新的独立事务。

常见的事务传播行为包括:

  • PROPAGATION_REQUIRED:默认选项,如果当前存在事务,则加入该事务,否则新开启一个事务
  • PROPAGATION_REQUIRES_NEW:新开启一个独立的事务,如果当前已经存在事务,则挂起当前事务
  • PROPAGATION_SUPPORTS:如果当前存在事务,则加入该事务,否则以非事务的方式执行
  • PROPAGATION_NOT_SUPPORTED:以非事务的方式执行,如果当前存在事务,则挂起该事务
  • PROPAGATION_MANDATORY:必须在当前事务的上下文中执行,否则抛出异常
  • PROPAGATION_NEVER:必须以非事务的方式执行,否则抛出异常
  • PROPAGATION_NESTED:如果当前存在事务,则在该事务的嵌套事务中执行,否则新开启一个事务

Spring 事务可以使用注解 @Transactional 来标记需要进行事务管理的方法。可以将注解添加到具体的业务方法上,也可以添加到接口上来表示当前接口的所有方法都需要进行事务管理。

public interface XxxService{
    @Transactional
    public void transfer(...)
}
@Service
// 可以加在类上或方法上 => 前者表示所有方法都添加上了事务,后者只是此方法添加事务.
// 默认传播行为就是propagation.REQUIRED
// mysql 默认隔离级别是 REPEATABLE_READ
@Transactional(propagation = Propagation.REQUIRED,isolation = Isolation.REPEATABLE_READ,timeout = -1)
public class UserService {}

在 Spring 中,使用事务管理器来协调和管理数据库事务的提交与回滚操作,以确保数据的一致性和完整性。Spring 提供的标准接口 PlatformTransactionManager 可以设置事务管理器。

@Bean
public PlatformTransactionManager transactionManager(DataSource dataSource){
    DataSourceTransactionManager dstm = new DataSourceTransactionManager();
    dstm.setDataSource(dataSource);
    return dstm;
}

上述代码中,使用 @Bean 注解来标识这个方法是一个 Spring Bean 的定义,表示可以被容器管理和使用。方法的名称 transactionManager 是 Bean 的名称,可以在其他 Bean 的定义中使用该名称来引用这个 Bean。方法中的参数 DataSource dataSource 是一个 DataSource 类型的对象,表示要使用的数据源。通过此方法可以创建一个 DataSourceTransactionManager 类型的事务管理器对象 dstm。

注解声明式事务管理

Spring 3.1 introduces the @EnableTransactionManagement annotation that we can use in a @Configuration class to enable transactional support.

However, if we're using a Spring Boot project and have a spring-data-* or spring-tx dependencies on the classpath, then transaction management will be enabled by default.

@Configuration // @Configuration => 代表配置类
@ComponentScan(basePackages = "com.<secret>.spring5.txdemo") // @ComponentScan => 组件扫描
@EnableTransactionManagement // @EnableTransactionManagement => 开启事务
public class TxConfig {
    // 创建数据库连接池
    @Bean
    public DruidDataSource getDuridDataSource(){
        DruidDataSource dataSource = new DruidDataSource();
        dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
        dataSource.setUrl("jdbc:mysql://localhost:3306/user_db");
        dataSource.setUsername("<username>");
        dataSource.setPassword("<password>");
        return dataSource;
    }
    // 创建jdbc模板对象
    @Bean
    public JdbcTemplate getJdbcTemplate(DataSource dataSource){
        JdbcTemplate jdbcTemplate = new JdbcTemplate();
        // 注入 dataSource
        // IOC 容器已存在 dataSource 对象,根据类型找到 dataSource 对象比创建对象更好
        // jdbcTemplate.setDataSource(getDuridDataSource());
        jdbcTemplate.setDataSource(dataSource);
        return jdbcTemplate;
    }
    // 创建事务管理器对象
    @Bean
    public DataSourceTransactionManager getDataSourceTransactionManager(DataSource dataSource){
        DataSourceTransactionManager transactionManager = new DataSourceTransactionManager();
        transactionManager.setDataSource(dataSource);
        return transactionManager;
    }
}
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
                           http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
                           http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd
">
    <!-- 开启组件扫描 -->
    <context:component-scan base-package="com.<secret>.spring5.txdemo"></context:component-scan>
    <!-- 数据库连接池 -->
    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" destroy-method="close">
        <property name="url" value="jdbc:mysql://localhost:3306/{yourdb}" />
        <property name="username" value={yourusername} />
        <property name="password" value={yourpassword} />
        <property name="driverClassName" value="com.mysql.cj.jdbc.Driver" />
    </bean>
    <!-- jdbctemplate对象 -->
    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
        <!-- 注入datasource -->
        <property name="dataSource" ref="dataSource"></property>
    </bean>
    <!-- 创建事务管理器 -->
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <!-- 注入数据源 => 指定对哪个数据库进行事务管理 -->
        <property name="dataSource" ref="dataSource"></property>
    </bean>
    <!-- 开启事务注解 => 指定哪个事务管理器开启 -->
    <tx:annotation-driven transaction-manager="transactionManager"></tx:annotation-driven>
</beans>

XML 声明式事务管理

在下列配置中,使用 DataSourceTransactionManager 作为事务管理器,并为不同的方法设置了不同的事务传播属性。对于以 saveupdatedelete 开头的方法,都将使用 REQUIRED 的事务传播属性来启用事务。对于以 get 开头的方法,将其定义为只读的方法,并不需要启用事务。对于其他所有的方法,也使用了 REQUIRED 的事务传播属性,表示这些方法将使用与调用方相同的事务。

<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <property name="dataSource" ref="dataSource"/>
</bean>

<tx:advice id="txAdvice" transaction-manager="transactionManager">
    <tx:attributes>
        <tx:method name="save*" propagation="REQUIRED"/>
        <tx:method name="update*" propagation="REQUIRED"/>
        <tx:method name="delete*" propagation="REQUIRED"/>
        <tx:method name="get*" read-only="true"/>
        <tx:method name="*" propagation="REQUIRED"/>
    </tx:attributes>
</tx:advice>

注意,如果要在 XML 文件中使用 Spring 的事务管理与 AOP 功能,需要在 XML 文件中添加命名空间声明,命名空间声明是告诉 XML 解析器,txaop 前缀代表的命名空间,从而让 XML 解析器能够正确地解析 XML 文件中的 Spring 事务管理与 AOP 相关的元素和属性。

xmlns:tx="http://www.springframework.org/schema/tx
xmlns:aop="http://www.springframework.org/schema/aop

此外,还需要在 XML 文件中指定相应的 XSD 文件来定义这些命名空间所代表的元素和属性的结构和语法。

http://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/aop/spring-aop.xsd

接下来需要启用 AOP,并将切入点设置为 com.example.service 包中的所有方法。这意味着在这个包中的所有方法都将受到事务通知的影响。

<aop:config>
    <aop:advisor advice-ref="txAdvice" pointcut="execution(* com.example.service.*.*(..))"/>
</aop:config>

@Transactional 注解添加到需要事务管理的方法上,这将告诉 Spring 在这些方法中启用事务管理器。

@Service
public class ExampleServiceImpl implements ExampleService {

    @Autowired
    private ExampleDao exampleDao;

    @Override
    @Transactional
    public void saveExample(Example example) {
        exampleDao.saveExample(example);
    }

    @Override
    @Transactional
    public void updateExample(Example example) {
        exampleDao.updateExample(example);
    }

    @Override
    @Transactional
    public void deleteExample(Example example) {
        exampleDao.deleteExample(example);
    }

    @Override
    public Example getExampleById(Long id) {
        return exampleDao.getExampleById(id);
    }
}

配置事务管理器、通知、切入点与切面的整体 XML 代码如下所示。

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
                           http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
                           http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd
                           http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd
">
    <!-- 开启组件扫描 -->
    <context:component-scan base-package="com.<secret>.spring5.txdemo"></context:component-scan>
    <!-- 数据库连接池 -->
    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" destroy-method="close">
        <property name="url" value="jdbc:mysql://localhost:3306/user_db" />
        <property name="username" value="<username>" />
        <property name="password" value="<password>"/>
        <property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>
    </bean>
    <!-- jdbctemplate对象 -->
    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
        <!-- 注入datasource -->
        <property name="dataSource" ref="dataSource"></property>
    </bean>
    <!-- 创建事务管理器 -->
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <!-- 注入数据源 => 指定对哪个数据库进行事务管理 -->
        <property name="dataSource" ref="dataSource"></property>
    </bean>
    <!-- 配置通知 -->
    <tx:advice id="txadvice">
        <!-- 配置事务相关参数 -->
        <tx:attributes>
            <!-- 指定哪种规则的方法上添加事务 -->
            <!-- <tx:method name="account*"/> -->
            <tx:method name="accountMoney" propagation="REQUIRED"></tx:method>
        </tx:attributes>
    </tx:advice>
    <!-- 配置切入点和切面 -->
    <aop:config>
        <!-- 配置切入点 -->
        <aop:pointcut id="pt" expression="execution(* com.<secret>.spring5.txdemo.service.UserService.*(..))"/>
        <!-- 配置切面 => 事务加到具体方法上 -->
        <aop:advisor advice-ref="txadvice" pointcut-ref="pt"></aop:advisor>
    </aop:config>
</beans>

Bugs 解决

  • BeanDefinitionStoreException
// 报错原文
org.springframework.beans.factory.BeanDefinitionStoreException: Unexpected exception parsing XML document from class path resource.

在开启组件扫描的 base-package 下,是否存在多个需要扫描的文件。若存在则需要进一步缩小扫描范围。

  • Exception encountered during context initialization
// 报错原文
WARNING: Exception encountered during context initialization - cancelling refresh attempt: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'xxxService': Unsatisfied dependency expressed through field 'xxxDao';

在测试类中进行进一步验证;概率性加载 xml 文件错误。

  • UnsatisfiedDependencyException
org.springframework.context.support.AbstractApplicationContext refresh
WARNING: Exception encountered during context initialization - cancelling refresh attempt: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'userDaoImpl': Unsatisfied dependency expressed through field 'jdbcTemplate'; nested exception is org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'getJdbcTemplate' defined in com.<secret>.spring5.txdemo.config.TxConfig: Unsatisfied dependency expressed through method 'getJdbcTemplate' parameter 0; nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'javax.sql.DataSource' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {}
Core Technologies
docs.spring.io/spring-framework
Spring AOP Example Tutorial - Aspect, Advice, Pointcut, JoinPoint, Annotations, XML Configuration | DigitalOcean
Technical tutorials, Q&A, events — This is an inclusive place where developers can find or lend support and discover new ways to contribute to the community.
https://www.digitalocean.com/community/tutorials/spring-aop-example-tutorial-aspect-advice-pointcut-joinpoint-annotations
Spring Framework Annotations - Spring Framework Guru
In this post I review the Java annotations which are commonly used to configure behaviors in the Spring Framework.
https://springframework.guru/spring-framework-annotations/

结束

本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议,转载请注明出处!