Spring万字学习笔记整理

229 阅读27分钟

1、概述

Spring框架是一个开源的、松耦合的、分层的、可配置的一站式企业级 Java 开发框架,它的核心是IoC(控制反转)和AOP(面向切面编程) 。在Spring中,构成应用程序主干并由 Spring IoC 容器管理的对象称为bean。bean 是由IoC容器实例化,组装和以其他方式管理的对象。

IoC是一种思想,核心是将控制权转交出去,实际上指的就是本来由程序员手动创建bean的工作交由Spring进行创建,而放置和管理这些bean的就是IoC容器。程序员可以将对象的创建和相互之间的依赖关系写在配置文件中,也就是说bean及其之间的依赖关系反映在容器使用的配置元数据中,以此达到松耦合的效果。

DI即依赖注入,指的是组件以一些预先定义好的方式(如Setter 方法)接受来自于容器的资源注入。IoC也被称为是DI。

IoC is also known as dependency injection (DI). ---Spring5.1.3.RELEASE文档

AOP可以将与业务无关却被业务调用的逻辑封装起来,为应用业务做统一或特定的功能增强,能实现应用业务与增强逻辑的解耦,例如日志、事务等功能通过AOP实现。

Spring框架的核心模块:

image-20210727012641802

2、Spring快速上手

2.1 创建工程

环境为:jdk1.8,maven 3.6.3,idea2021.1

创建一个maven工程

image-20210725215408953

image-20210725215740314

2.2 添加项目依赖信息

在项目的pom.xml中添加spring依赖信息,快速上手项目只需要导入context包,这里使用的是Spring的5.1.3.RELEASE:

 <project ...>
 ...
     <dependencies>
         <dependency>
             <groupId>org.springframework</groupId>
             <artifactId>spring-context</artifactId>
             <version>5.1.3.RELEASE</version>
         </dependency>
     </dependencies>
 </project>

点击右上角的load maven changes按钮进行依赖安装

2.3 创建配置文件

在项目src/main/resources下创建spring配置文件

image-20210725221432685

初始化配置:

 <?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 http://www.springframework.org/schema/beans/spring-beans.xsd">
 ​
 </beans>

配置idea的应用上下文,直接点ok

image-20210725221556129

2.4 创建实体类

在src/main/java下创建名为Person的实体类,可自定义包结构(我的为top.jtszt.bean),快速上手只是感受控制反转的效果,因此先不用在类中定义信息

2.5 声明bean

在Spring配置文件中加入对于Person类的声明,在beans标签体中使用bean标签声明:

 <!-- id代表的是该bean的唯一标识 class为需要声明的类的全类名 -->
 <beans...>
     <bean id="person" class="top.jtszt.bean.Person"></bean>
 </beans>

2.6 创建启动类

在自定义包下创建一个启动类QuickStartApplication.java

 public class QuickStartApplication {
     public static void main(String[] args) {
          ApplicationContext ioc =  new ClassPathXmlApplicationContext("ioc.xml");
         Person person = (Person) ioc.getBean("person");
         System.out.println(person);
     }
 }

这里使用了ClassPathXmlApplicationContext来加载配置文件,加载完成后用ApplicationContext接口来接收,接着调用getBean方法,传入需要获取的bean的id,获取该bean并打印

 top.jtszt.bean.Person@523884b2

至此就完成了Spring框架的快速上手,可以发现我们并没有手动去new对象,而是把对象声明在配置文件中,之后通过工厂获取出这个对象。

3、IoC部分

3.1 IoC容器

org.springframework.context.ApplicationContext接口代表了Spring IoC容器,它负责实例化、配置、组装bean。通过快速上手了解到,可以将类的相关声明写在配置文件中,(此外还可以使用注解驱动开发,下面会细讲),之后将资源加载到IoC容器中,通过容器去管理。而在通过IoC容器读取Bean的实例之前,需要先将IoC容器本身实例化。在web应用中通常都是通过ClassPathXmlApplicationContext在classpath下加载配置文件(基于xml配置),生成一个IoC容器:

 ApplicationContext ioc = new ClassPathXmlApplicationContext("ioc.xml");

实际上这里还可以返回成BeanFactory,这是IoC容器的基本实现,它的特点是延迟载入所有Bean;而ApplicationContext是BeanFactory的子接口,它简化了与 AOP 的整合、消息机制、事件机制,以及对 Web 环境的扩展,同时保持基本特性的兼容。Spring官方文档中推荐使用这种方式。

You should use an ApplicationContext unless you have a good reason for not doing so.

除此之外还可以通过FileSystemXmlApplicationContext从文件系统或者URL装载XML配置文件;XmlWebApplicationContextXmlPortletApplicationContext用于Web和入口应用程序。

3.2 依赖查找(基于xml)

依赖查找指的是容器中的受控对象通过容器的API来查找自己所依赖的资源和协作对象。通过这种方式来查找对象需要调用相关的api。上面使用到的 ioc.getBean("person")即是依赖查找。

1)基于上下文的依赖查找:

①通过对象的id获取,传入配置在xml文件中的对象的id,也就是快速上手的那种获取方法;

②通过对象类型去查找,需要注意的是,如果同一个类型的bean在XML文件中配置了多个,则获取时会抛出异常,所以同一个类型的bean在容器中必须是唯一的:

 Person person = ioc.getBean(Person.class);

③通过使用类型和id参数搭配的方式查找

 Person person = ioc.getBean("person", Person.class);

④通过getBeansOfType方法获取传入接口/抽象类的实现类/子类(假设MyInterface是接口)

 Map<String, MyInterface> beans = ioc.getBeansOfType(MyInterface.class);

2)查找IoC容器所有bean

如果需要查找出IoC容器中的所有bean,还可以使用getBeanDefinitionNames方法,这个方法会获取所有bean的id,接着可以根据id去找出对应的bean

 String[] beanNames = ioc.getBeanDefinitionNames();

3)延迟查找

对于一些特殊的场景,需要依赖容器中的某些特定的 Bean ,但当它们不存在时需要有方法去处理对应的情况,而不是抛异常。假设我们现在Person类没有在IoC容器中,那么如果直接getBean会报NoSuchBeanDefinitionException异常,我们需要的是让他延迟查找这个bean,怎么做?

ApplicationContext中有一个方法叫 getBeanProvider,传入bean的类信息,得到的是一个ObjectProvider<>对象,只有在调用该对象的getObject方法试图取出里面的bean对象时才会抛异常。

再进一步,如果bean不存在,我不想让它抛异常,怎么做?

ObjectProvider中还有一个方法叫getIfAvailable,在bean存在时会获取到bean,而获取bean失败时返回null而非异常。

3.3 依赖注入(基于xml)

现在我们已经可以从IoC容器中获取到对象了,但可以发现这些对象都是没有具体的属性的,而依赖注入是一个过程,也就是说对于这些属性我们不用自己手动去传,而是交给IoC容器去处理,让容器通过反射的方式去进行对象的注入。

对Person对象进行改造:

 public class Person {
     private String name;
     private Integer age;
     private String phone;
     private Car car;
     private List<Car> CarList;
     private Map<String, Object> testMap;
     
     public Person(String name, Integer age, String phone) {
         this.name = name;
         this.age = age;
         this.phone = phone;
     }
     
     public void setTestMap(Map<String, Object> testMap) {
         this.testMap = testMap;
     }
     public void setCar(Car car) {this.car = car;}
     public void setCarList(List<Car> carList) {CarList = carList;}
     public void setnameForPerson(String name) {this.name = name;}
     public void setAge(Integer age) {this.age = age;}
     public void setPhone(String phone) {this.phone = phone;}
     //重写toString方法
 }

依赖注入有两个主要的形式,一个是基于构造函数的依赖注入,一个是基于 Setter 的依赖注入。

1)基于Setter的依赖注入;

在xml文件中进行配置,使用property标签,name属性为对应的setXx方法的这个Xx,而不是在类中定义的属性,value为属性值:

 <bean id="person1" class="top.jtszt.bean.Person">
     <!-- 可以看到这里写的是nameForPerson并非name -->
     <property name="nameForPerson" value="zhangsan"/>
     <property name="age" value="16"/>
     <property name="phone" value="135000"/>
 </bean>

2)基于构造器的依赖注入;

在xml文件中,使用constructor-arg标签进行赋值,name为构造器参数名,value为值:

 <bean id="person2" class="top.jtszt.bean.Person">
     <!-- 调用有参构造器进行创建对象并赋值 -->
     <constructor-arg name="name" value="lisi"/>
     <constructor-arg name="age" value="18"/>
     <constructor-arg name="phone" value="135111"/>
 </bean>

这里也可以省略constructor-arg的name属性,但是必须按照构造器顺序写value

3)为bean属性赋值的其他情况

①为属性赋值null;

 <bean id="person3" class="top.jtszt.bean.Person">
     <property name="name">
         <null/>
     </property>
 </bean>

②为引用类型的属性赋值;

创建一个Car类:

 public class Car {
     private String carName;
     private Integer price;
 ​
     public void setCarName(String carName) {this.carName = carName;}
     public void setPrice(Integer price) {this.price = price;}
     //重写toString
 }

方式1:使用bean标签赋值

 <bean id="person4" class="top.jtszt.bean.Person">
     <property name="car">
         <bean class="top.jtszt.bean.Car">
             <property name="carName" value="benz"/>
             <property name="price" value="8800"/>
         </bean>
     </property>
 </bean>

方式2:引用外部bean

 <bean id="car1" class="top.jtszt.bean.Car">
     <property name="carName" value="bmw"/>
     <property name="price" value="3380"/>
 </bean>
 <bean id="person4" class="top.jtszt.bean.Person">
     <property name="car" ref="car1"/>
 </bean>

③为集合类型属性赋值;

1)为List属性赋值:

 <bean id="person5" class="top.jtszt.bean.Person">
     <property name="carList">
         <!-- 这里的list标签相当于 list = new ArrayList<>(); -->
         <list>
         <!-- 方式一:直接写内部bean -->
             <bean class="top.jtszt.bean.Car">
                 <property name="carName" value="byd"/>
                 <property name="price" value="6600"/>
             </bean>
         <!-- 方法二:引用外部bean -->
             <ref bean="car1"/>
         <!-- list中还有value标签可以赋单值 -->
         </list>
     </property>
 </bean>

2)为map属性赋值:

 <bean id="person6" class="top.jtszt.bean.Person">   
     <property name="testMap">
         <!-- 1.map标签相当于 map = new LinkedHashMap<>(); -->
         <map>
             <!-- 方式一:在value中直接写值 -->
             <entry key="key01" value="zhangsan"/>
             <!-- 方式二:value是一个引用bean的内容 -->
             <entry key="key02" value-ref="car1"/>
             <!-- 方式三:在entry标签内部直接写bean去作为它的值 -->
             <entry key="key03">
                 <bean class="top.jtszt.bean.Car">
                     <property name="carName" value="honda"/>
                     <property name="price" value="5200"/>
                 </bean>
             </entry>
         </map>
     </property>
 </bean>

3.4 注解驱动开发

从Spring Framework3.0之后引入了大量注解,可以使用注解驱动的方式来代替原来的xml配置,这也是后来比较常见的一种配置方式。

1)配置类

注解驱动需要的是配置类,一个配置类就可以理解为一个 xml ,只要在类上标注一个 @Configuration ,这个类即是配置类。而在xml中我们使用的是bean标签来声明bean,而在注解驱动中只需要在方法上标注@Bean即可代表bean的声明。

 @Configuration
 public class ConfClass {
     //默认方法名为bean的id,如果使用 @Bean("xx") 则代表xx为bean的id
     //返回值类型为bean的类型
     @Bean
     public Person person(){
         return new Person();
     }
 }

这就相当于创建了一个xml文件,并在IoC容器中注册了id为person的Person类。

2)依赖查找

在完成配置类之后,依赖的查找需要用到AnnotationConfigApplicationContext来初始化加载容器。

 ApplicationContext context = new AnnotationConfigApplicationContext(ConfClass.class);
 Person person = (Person)context.getBean("person");
 System.out.println(person);

3)依赖注入

方式一:基于Setter方法的注入

只需要在方法中使用set方法对属性进行赋值,将赋值属性之后的对象返回即可。

 @Bean
 public Person person(){
     Person person = new Person();
     person.setName("zs");
     person.setAge(11);
     return person;
 }

方式二:基于构造函数的注入

 @Bean
 public Person person(){
     return new Person("zs",11,"135111");
 }

4)组件注册

当我们需要注册的bean越来越多的时候,写很多方法标注很多的bean注解是不现实的。因此Spring中也提供了一些模式注解,可以实现对组件的快速注册。最基本的就是@Component注解,代表的是被注解的类会被注册到IoC容器中作为一个 Bean ,也就是说我们可以直接在Person实体类上标注该注解,那么就不用手动去写bean注解标注的方法进行注册。

 @Component
 public class Person {
     private String name;
     ....
 }

这样配置之后默认是将Person类注册到IoC容器中,id为类名的小驼峰式,如果想指定id,可以使用 @Component("xx")声明。

5)组件扫描

在声明组件之后,还需要进行组件的扫描才能让IoC容器感知到组件的存在。我们可以直接在配置类上配置多一个@ComponentScan("xx")注解,xx为包路径,指定需要扫描的包及其子包。

 @Configuration
 @ComponentScan("top.jtszt.bean")
 public class ConfClass { }

还可以在创建AnnotationConfigApplicationContext对象时传入包路径,这样也可以进行组件扫描。

 ApplicationContext context = new AnnotationConfigApplicationContext("top.jtszt.bean");

除了使用注解驱动有组件扫描之外,基于xml配置也可以进行组件扫描

 <beans>
     <context:component-scan base-package="top.jtszt.bean"/>
 </beans>

6)声明bean的几个注解

@Component :通用的注解,可标注任意类为 Spring 组件。

@Repository : 对应持久层即 Dao 层,主要用于数据库相关操作。

@Service : 对应服务层,主要涉及一些复杂的逻辑,需要用到 Dao层。

@Controller : 对应 Spring MVC 控制层,主要用户接受用户请求并调用 Service 层返回数据。

7) 注解与xml梦幻联动

在xml中开启注解配置,并声明配置类

 <beans>
     <context:annotation-config/>
     <bean class="top.jtszt.annotation.ConfClass"/>
 </beans>

在配置类上使用注解@ImportResource引入xml文件

 @Configuration
 @ImportResource("classpath:applicationcontext.xml")
 public class ConfClass { }

3.5 依赖注入的其他姿势(基于注解)

1)注解式属性注入

在对@Component扫描进去的组件进行属性注入时,可以使用@Value("xx")注解标在对应属性上对属性进行信息注入,并且该实体类可以不带Setter方法。

 @Component
 public class Person {
     @Value("zs")
     private String name;
     @Value("12")
     private Integer age;
     @Value("132111")
     private String phone;
     //重写toString
 }

2)从外部配置文件注入

在Spring中从外部配置文件读取信息之后注入给属性也是被允许的。首先需要使用@PropertySource("xx")注解标注在类上,其中xx代表配置文件的位置。之后在属性上使用@Value("${yy}")注解进行注入,其中yy代表该值对应在配置文件中的key。

①在类路径下创建一个配置文件my.properties

 person.name=ls
 person.age=16
 person.phone=135123

②在实体类上标注@PropertySource("classpath:my.properties")

③在对应属性上标注@Value进行取值

 @Component
 @PropertySource("classpath:my.properties")
 public class Person {
     @Value("${person.name}")
     private String name;
     @Value("${person.age}")
     private Integer age;
     @Value("${person.phone}")
     private String phone;
     //重写toString
 }

3)SpEL实现注入

SpEL也就是Spring表达式语言 ,它从Spring3.0开始被支持,SpEL支持调用属性值、属性参数以及方法调用、数组存储、逻辑计算等功能。SpEL 的语法统一用 #{xx} 表示,xx为表达式。

最常见的是使用SpEL对bean属性的引用或者方法的调用。

例:现在有两个实体类Bird与Dog,我们可以使用SpEL将Bird的age属性注入到Dog的age属性中。

 @Component
 public class Bird {
     @Value("16")
     private String age;
     ...
 }
 @Component
 public class Dog {
     @Value("#{bird.age}")
     private String age;
     ...
 }

3.6 自动注入

1)@Autowired

在 bean 中直接在属性或者Setter方法上标注 @Autowired 注解,IoC 容器会按照属性对应的类型,从容器中找对应类型的 bean 赋值到对应的属性上,实现自动注入。

如果容器中找不到对应类型的bean,那么会抛出NoSuchBeanDefinitionException异常,也可以在该注解上加上required = false ,那么找不到bean时会注入null。

假设有一个Car类,现在Person类有一个属性叫car,要对它实现自动注入:

 @Component
 public class Person {
     private String name;
     @Autowired
     private Car car;
     //重写toString
 }

★Autowired注入的原理:

首先会拿到该属性的类型去IoC容器中找,如果找到一个则返回;如果找不到就抛异常(找不到bean);如果找到多个,那么会根据属性id去容器中找有没有对应的id,如果有就返回,没有就抛异常(该类型bean不唯一)。

2)@Qualifier

如果容器中存在多个类型相同的bean(且id不匹配)将会注入失败。这时可以使用@Qualifier注解显式地指定要注入哪一个bean。

 @Autowired
 @Qualifier("bmw")
 private Car car;

3)@Primary

除了使用@Qualifier之外,还可以在被注入的 Bean 上标注@Primary,可以指定默认注入的Bean。

 @Configuration
 @ComponentScan("top.jtszt.bean")
 public class ConfClass {
     @Bean
     @Primary
     public Car bmw(){
         return new Car();
     }
 }

4)@Resource

@Resource 来自JSR250规范,它与 @Autowired 的不同之处在于:@Autowired 是按照类型注入,@Resource 是直接按照属性名 / Bean的名称注入,相当于@Autowired@Qualifier的结合。

 @Resource(name="benz")
 private Car carCar;

5)@Inject

@Inject来自JSR330规范,使用前需要导入javax.inject依赖。使用上等同于@Autowired,但由于它是JSR的规范,因此不受Spring框架的限制。

3.7 DI与DL的异同

  • 作用目标不同

    • 依赖注入的作用目标通常是类成员
    • 依赖查找的作用目标可以是方法体内,也可以是方法体外
  • 实现方式不同

    • 依赖注入通常借助一个上下文被动的接收
    • 依赖查找通常主动使用上下文搜索

3.8 bean高级

1)bean的作用域

singleton: 单实例的bean在容器创建好之前就已经完成创建,并且在容器中只有单一的对象;

prototype: 多实例的bean只有在调用相关方法获取bean时才会被创建,并且获取一次创建一个;

request : 每一次HTTP请求都会产生一个新的bean,该bean仅在当前HTTP request内有效;

session : 每一次HTTP请求都会产生一个新的 bean,该bean仅在当前 HTTP session 内有效;

global-session: 全局session作用域,仅仅在基于portlet的web应用中才有意义,Spring5已经没有了。Portlet是能够生成语义代码(例如:HTML)片段的小型Java Web插件。它们基于portlet容器,可以像servlet一样处理HTTP请求。但是,与 servlet 不同,每个 portlet 都有不同的会话。

在bean的配置中可以通过scope属性指定单实例/多实例。

 <!--bean的作用域默认是单实例(singleton) -->
 <bean id="car1" class="top.jtszt.bean.Car" scope="prototype"/>

2)bean的类型

①普通bean

类似前面创建的那些bean都是普通bean

 @Component
 public class Person { ... }

②工厂bean

所谓工厂bean就是在bean对象创建过于复杂,或者有一些特殊策略时借助FactoryBean 使用工厂方法来创建bean。FactoryBean 接口是一个创建对象的工厂,如果 Bean 实现了 FactoryBean 接口,则它本身将不再是一个普通的 Bean ,不会在实际的业务逻辑中起作用,而是由创建的对象来起作用。

FactoryBean接口有三个方法:

 public interface FactoryBean<T> {
     // 返回创建的对象
     @Nullable
     T getObject() throws Exception;
     
     // 返回创建的对象的类型(即泛型类型)
     @Nullable
     Class<?> getObjectType();
 ​
     // 创建的对象是单实例Bean还是原型Bean,默认单实例
     default boolean isSingleton() {
         return true;
     }
 }

★关于FactoryBean的几个注意点:

  • 它创建的bean直接放在 IoC 容器中
  • 它的加载是伴随IoC容器的初始化时机一起的,也就是在容器生成好之前就创建
  • 它生产bean的机制是延迟生产,只有调用方法获取bean时才会创建
  • 使用它生产出来的bean默认是单实例的

★BeanFactory 和FactoryBean 的区别:

BeanFactory:从类的继承结构上看,它是实现Spring最顶层的容器实现;从类的组合结构上看,它是最深层次的容器,ApplicationContext 在最底层组合了 BeanFactory 。

FactoryBean :创建对象的工厂 bean ,可以使用它来直接创建一些初始化流程比较复杂的对象。

3)bean的生命周期

  • Bean 容器找到配置文件中 Spring Bean 的定义。

  • Bean 容器利用 Java Reflection API 创建一个Bean的实例。

  • 如果涉及到一些属性值 利用 set()方法设置一些属性值。

  • 如果 Bean 实现了 BeanNameAware 接口,调用 setBeanName()方法,传入Bean的名字。

  • 如果 Bean 实现了 BeanClassLoaderAware 接口,调用 setBeanClassLoader()方法,传入 ClassLoader对象的实例。

  • 与上面的类似,如果实现了其他 *.Aware接口,就调用相应的方法。

  • 如果有和加载这个 Bean 的 Spring 容器相关的 BeanPostProcessor 对象,执行postProcessBeforeInitialization() 方法

  • 如果Bean实现了InitializingBean接口,执行afterPropertiesSet()方法。

  • 如果 Bean 在配置文件中的定义包含 init-method 属性,执行指定的方法。

  • 如果有和加载这个 Bean的 Spring 容器相关的 BeanPostProcessor 对象,执行postProcessAfterInitialization() 方法

  • 当要销毁 Bean 的时候,如果 Bean 实现了 DisposableBean 接口,执行 destroy() 方法。

  • 当要销毁 Bean 的时候,如果 Bean 在配置文件中的定义包含 destroy-method 属性,执行指定的方法。引用

    image-20210727232633008

4)bean的实例化方式

①直接通过 <bean>标签 或者 @Bean@Component注解 注册 bean 后实例化

②借助 FactoryBean 实例化 bean

③使用静态工厂方法 factory-method实例化 bean

④使用实例工厂方法factory-bean + factory-method 实例化 bean

5)单实例bean的线程安全问题

默认情况下单实例 bean 的线程是不安全的( 只要有可能变动的成员属性 ),也就是说如果在单实例bean的全局层面上定义有数据存储功能的对象 / 基本类型变量 / static变量,那么就会存在线程不安全的问题。

解决方法:

  • 通过声明Bean的 Scope="prototype",将bean变成多实例
  • 使用本地变量 ThreadLocal 进行定义

4、AOP部分

4.1 AOP概述

在OOP的开发中,对于一些重复的操作可以抽离成模块,这可以减少代码量,但还是无法从根本上解决代码的冗余。在这种情况下我们可以把这些重复的操作抽离成切面,通过在运行时动态代理组合进原有的对象,这就是AOP,它是对OOP的补充。

AOP即面向切面编程,实际上就是对一些方法进行业务上面的按需增强,将一些与业务逻辑无关的业务方法(如:日志打印、权限校验、数据缓存等)抽离开来作为增强器,再利用动态代理进行增强,从这我们也可以体会到AOP也有实现解耦的作用,并且AOP 可以实现组件化、可插拔式的功能扩展。

AOP的设计原理是对原有业务逻辑的横切增强,底层是运行时动态代理机制

不同于OOP以对象为关注的核心,AOP的核心是切面

4.2 术语

①目标对象:指的是被代理对象,也就是那个需要被增强的对象;

②连接点:在Spring中,连接点指的是目标对象中的所有方法;

③切入点:指的是对目标对象进行增强的连接点,目标对象中的连接点可能很多,但需要增强的可能不是全部,所以切入点一定是连接点,但连接点不一定是切入点;

④通知:用来增强对象的那些代码(如:日志打印、记录等);

⑤代理对象:指的是目标对象和通知的组合

⑥切面:指的是切入点和通知的组合

4.3 通知类型

前置通知 Before: 在目标方法(切入点方法)调用之前触发;

后置通知 After: 在目标方法(切入点方法)调用之后触发;

返回通知 AfterReturing: 在目标方法(切入点方法)成功返回之后触发;

异常通知 AfterThrowing: 在目标方法(切入点方法)出现/抛出异常之后触发;

环绕通知 Around: 它可以直接拿到目标对象,以及要执行的方法,所以可以在程序执行的任意位置进行切入。

 try{
     //前置通知
     Object res = pjp.proceed();
     //返回通知
 }catch(e){
     //异常通知
 }finally{
     //后置通知
 }

4.4 切入点表达式

Spring中的AOP配置是根据切入点表达式去找到特定的方法进行切入(增强) ,因此在实现AOP之前,我们需要了解切入点表达式的各种写法。

1)切入点表达式的语法:

execution(访问限定符 方法返回值类型 方法全类名(参数列表类型) [throws] 异常全类名 )

2)通配符

如果包名为 .. 则表示所有下级包(递归),如果参数为 .. 则表示不限制参数,如果包名/方法名为*表明全部包/方法,同时表达式中支持|| && 操作符

例如:

 ①execution(public int top.jtszt.impl.MyCalculator.*(int,int))
 ②execution(int top.jtszt..*.*(..))

①表示切入的是 top.jtszt.impl.MyCalculator 类下的所有 返回值为int型带有两个int型参数公有 的方法

②表示切入的是 top.jtszt所有下级子包 中的 所有类 下的所有返回值为int型公有方法

4.5 AOP实现(基于xml)

背景:目标对象为top.jtszt.impl.MyCalculator,它是对top.jtszt.inter.Calculator接口的实现,其中有add、sub、div、multi这四个连接点,而切面类为top.jtszt.utils.LogUtils,其中有logStart、logReturn、logException、logEnd、logAround 这五个通知方法,现需要使用切面类对目标对象进行切入。

首先需要在maven导入AOP所需的依赖,包括spring-aop(被spring-context依赖)、aopalliance、 aspectjweaver 、cglib。接着在spring的配置文件中声明AOP配置,这里需要导入aop名称空间。

①切面类注入IoC:为切面类配置bean;

②配置切入点表达式:接着配置aop使用的是<aop:config>标签,为了达到切入点表达式复用的效果,我们可以先使用<aop:pointcut>标签声明切入点,它的expression属性即是切入点表达式,在下面我们只需要根据其id就可以复用这个表达式了; (注意:被切入的类必须注入IoC容器)

③定义切面类:使用<aop:aspect>标签进行定义,ref属性指向的是切面类bean,接着在标签体内定义各种通知方法;

④定义通知方法:有五个标签可以定义通知方法,在标签体内 method 属性为通知方法名,pointcut-ref属性 指向上面定义的切入点表达式。<aop:before>代表前置通知;<aop:after-returning>代表返回通知,可以使用returning属性定义接收return值的变量名,在切入方法中作为参数传入;<aop:after-throwing>代表异常通知,可以使用throwing属性定义接收异常信息的变量名,在切入方法中作为参数传入;<aop:after>代表后置通知;<aop:around>代表环绕通知。

 <beans>
     <!-- 首先需要为切面类配置bean -->
      <bean id="logUtils2" class="top.jtszt.utils.LogUtils"/>
      <!-- 在配置文件中配置AOP -->
      <aop:config>
          <!-- 定义切入点表达式 -->
          <aop:pointcut id="myPoint" expression="execution(public * top.jtszt.impl.MyCalculator.*(int,int))"/>
          <!-- 定义一个切面类 -->
          <aop:aspect ref="logUtils2">
              <!--定义前置通知方法-->
              <aop:before method="logStart" pointcut-ref="myPoint"/>
              <!--定义返回通知方法-->
              <aop:after-returning method="logReturn" pointcut-ref="myPoint" returning="result"/>
              <!--定义异常通知方法-->
              <aop:after-throwing method="logException" pointcut-ref="myPoint" throwing="exception"/>
              <!--定义后置通知方法-->
              <aop:after method="logEnd" pointcut-ref="myPoint"/>
              <!--定义环绕通知方法-->
              <aop:around method="logAround" pointcut-ref="myPoint" />
          </aop:aspect>
      </aop:config>
 </beans>

4.6 AOP实现(基于注解)

1)切面类注入IoC:为切面类加上@Component与@Aspect注解

2)配置切入点表达式:在切面类中定义一个空方法,使用@Pointcut 注解声明切入点表达式,以便在下面复用这个表达式;

 @Pointcut("execution(public int top.jtszt.impl.MyCalculator.*(int,int))")
 public void pointcutExpression(){}

3)定义通知方法

① @Before() :表明是在方法开始前切入;

② @AfterReturning() :表明是在方法正常返回后切入,后可声明接收返回值的参数名;

③ @AfterThrowing() :表明是在方法抛出异常后切入,后可声明接收异常的参数名;

④ @After() :表明是在方法最终结束时切入(如try..catch中的finally);

⑤ @Around() :表明这是一个环绕通知方法,环绕方法会先于其他四个通知方法执行,这个方法的返回值代表的就是调用实际方法的返回值。

 @Before("pointcutExpression()")
 public static void logStart(){}
 ​
 @AfterReturning(value="pointcutExpression()",returning = "result")
 public static void logReturn(Object result){}
 ​
 @AfterThrowing(value="pointcutExpression()",throwing = "exception")
 public static void logException(Exception exception){}
 ​
 @After("pointcutExpression()")
 public static void logEnd(){}

4)开启注解AOP

如果使用xml+注解,可以在xml中配置<aop:aspectj-autoproxy/>开启注解aop。

如果使用纯注解,可以在配置类加上@EnableAspectJAutoProxy注解开启注解aop 。

4.7 通知方法参数

像在使用原生的动态代理一样,如果需要在通知方法中获取切入方法的参数与方法名等信息,需要传入JoinPoint类型的参数。其中有几个比较常用的方法:

  • Object JoinPoint.getTarget():获取未代理的目标对象
  • Object JoinPoint.getThis():获取代理对象
  • Object[] JoinPoint.getArgs(): 获取切入方法的参数列表
  • Signature JoinPoint.getSignature():获取方法签名
  • String Signature.getName(): 获取方法名
  • Method (MethodSignature)Signature.getMethod():获取方法信息

需要注意的是,由于环绕通知方法的返回值代表的就是调用实际方法的返回值,因此其中需要传入一个ProceedingJoinPoint类型的参数,通过这个对象调用proceed()方法可以得到实际方法的返回值,这个语句也相当于动态代理中调用invoke()方法。

4.8 多切面执行顺序

如果有多个切面类对同一个方法进行切入,遵循从外到内的规则(按切面类名的 unicode 编码的十六进制顺序执行)。

如:外层切面类A为AspectOne,内层切面类B为AspectTwo。

执行顺序为: A前置通知方法→B前置通知方法→实际方法→B返回/异常通知方法→B后置通知方法→A返回/异常通知方法→A后置通知方法

如果想改变切面的执行顺序,可以通过@Order注解设置切面类优先级,传入一个int型参数,数值越小优先值越高,默认为最低优先级。

此外,同切面中的相同类型通知方法的执行顺序也是按照unicode编码顺序来。

4.9 用AOP做事务控制

背景信息:书店进行图书销售活动,并且会员在系统中存有余额信息,在用户购买图书之后系统需要减图书库存同时减用户余额,这是一个整体(一个事务)。现在需要用AOP做事务控制,保证两个操作的一致性。

流程:让Spring管理数据库连接池以及jdbcTemplate,DAO利用自动装配的jdbcTemplate进行数据库操作,Service做具体的结账方法;之后让Spring利用AOP对这个结账方法做事务控制。

1)环境准备

①添加maven依赖,包括mysql-connector-java、spring-tx、c3p0、spring-jdbc以及ioc、aop相关的依赖;

②准备数据库表

用户信息表:

 CREATE TABLE t_account(
     username VARCHAR(50) PRIMARY KEY,
     balance INT
 )

图书信息表:

 CREATE TABLE t_book(
     isbn VARCHAR(50) PRIMARY KEY,
     book_name VARCHAR(50),
     price INT
 )

图书库存表:

 CREATE TABLE t_book_stock(
     isbn VARCHAR(50),
     stock INT,
     CONSTRAINT fk_isbn FOREIGN KEY(isbn) REFERENCES t_book(isbn)
 )

操作数据库:

 @Repository
 public class BookDAO {
     @Autowired
     JdbcTemplate jdbcTemplate;
 ​
     //减少余额的方法
     public void updateBalance(String userName, int price){
         String sql = "UPDATE t_account SET balance=balance-? WHERE username=?";
         jdbcTemplate.update(sql,price,userName);
     }
     
     // 获取图书价格的方法
     public int getPrice(String isbn){
         String sql = "SELECT price FROM t_book WHERE isbn=?";
         return jdbcTemplate.queryForObject(sql, Integer.class, isbn);
     }
 ​
     // 减库存的方法
     public void updateStock(String isbn){
         String sql = "UPDATE t_book_stock SET stock=stock-1 WHERE isbn=?";
         jdbcTemplate.update(sql,isbn);
     }
 }

服务方法(为方便不写接口):

 @Service
 public class BookService {
 ​
     @Autowired
     private BookDAO bookDAO;
 ​
     public void checkout(String username,String isbn){
         //减库存
         bookDAO.updateStock(isbn);
         //减余额
         bookDAO.updateBalance(username, bookDAO.getPrice(isbn));
     }
 }

xml中还有包扫描等操作这里就不贴了。

2)配置声明式事务(基于xml)

上面的Service方法是没有做事务管理的,一旦减库存方法执行完毕之后出现异常,那么该库存将被成功减去1,但是用户余额却没扣除,这显然是不行的。接着我们要对它进行事务的管理,首先是基于xml的配置,它依赖于tx和aop名称空间。

首先需要配置数据源,并且由于上文使用了jdbcTemplate自动装配,这里顺便配置它。

 <beans>
     <!-- 配置写在db.properties中 -->
     <context:property-placeholder location="db.properties"/>
     <!-- c3p0连接池 -->
     <bean id="ds" class="com.mchange.v2.c3p0.ComboPooledDataSource">
         <property name="user" value="${jdbc.user}"/>
         <property name="password" value="${jdbc.password}"/>
         <property name="jdbcUrl" value="${jdbc.jdbcUrl}"/>
         <property name="driverClass" value="${jdbc.driverClass}"/>
         <property name="maxPoolSize" value="${jdbc.maxPoolSize}"/>
         <property name="minPoolSize" value="${jdbc.minPoolSize}"/>
     </bean>
     <!-- 配置jdbcTemplate -->
     <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
         <property name="dataSource" ref="ds"/>
     </bean>
 </beans>

接着配置Spring提供的事务管理器,当使用JDBC/MyBatis进行持久化时可以使用DataSourceTransactionManager做事务管理器。

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

接着需要告诉Spring哪些方法是事务方法,使用的是tx名称空间下的advice标签,其中transaction-manager指向事务管理器,该标签下的attributes只有一个标签methodname属性用于匹配事务方法(可以使用通配符),此外还有其他的一些属性:

  • timeout 设置超时自动终止事务并回滚;
  • read-only 设置事务为只读;
  • no-rollback-for 指定哪些异常不回滚,传入异常全类名,默认为空;
  • rollback-for 当方法触发异常时回滚,传入异常全类名;默认是捕捉所有运行时异常和错误;
  • isolation 修改事务的隔离级别;
  • propagation 指定事务的传播行为;

这些属性也可以在@Transactional 注解中配置。

 <tx:advice id="myAdvice" transaction-manager="tm">
     <!-- 指明哪些方法是事务方法-->
     <tx:attributes>
         <tx:method name="*"/>
         <tx:method name="checkout" timeout="-1" read-only="false"/>
         <tx:method name="get*" read-only="true"/>
     </tx:attributes>
 </tx:advice>

上面只是声明事务方法,但实际上还需要设置切入点才能进行事务管理,只有成功切入了才有后面的事务管理。也就是说事务方法一定是切入点,但切入点不一定是事务方法。

 <aop:config>
     <aop:pointcut id="txPoint" expression="execution(* top.jtszt.*.*.*(..))"/>
     <!-- advice-ref:指向事务管理器的配置 -->
     <aop:advisor advice-ref="myAdvice" pointcut-ref="txPoint"/>
 </aop:config>

3)配置声明式事务(基于注解)

首先需要在配置类加上@EnableTransactionManagement 表示开启事务管理器,也可以在xml文件中开启基于注解的声明式事务。

 @Configuration
 @EnableTransactionManagement
 @ComponentScan("top.jtszt")
 public class ConfClass {}
 <tx:annotation-driven transaction-manager="tm"/>

之后在配置类中配置上数据源与事务管理器

 @Configuration
 @EnableTransactionManagement
 @ComponentScan("top.jtszt")
 public class ConfClass {
     //读配置文件
     @Bean
     public Properties properties() throws IOException {
         Properties properties = new Properties();
         properties.load(new FileReader("db.properties"));
         return properties;
     }
     //配置数据源
     @Bean
     public ComboPooledDataSource dataSource(Properties properties) throws PropertyVetoException {
         ComboPooledDataSource ds = new ComboPooledDataSource();
         ds.setUser(properties.getProperty("jdbc.user"));
         ds.setPassword(properties.getProperty("jdbc.password"));
         ds.setJdbcUrl(properties.getProperty("jdbc.jdbcUrl"));
         ds.setDriverClass(properties.getProperty("jdbc.driverClass"));
         return ds;
     }
     //配置jdbcTemplate
     @Bean
     public JdbcTemplate jdbcTemplate(DataSource dataSource){
         JdbcTemplate jdbcTemplate = new JdbcTemplate();
         jdbcTemplate.setDataSource(dataSource);
         return jdbcTemplate;
     }
     //配置事务管理器
     @Bean
     public DataSourceTransactionManager dataSourceTransactionManager(DataSource dataSource){
         DataSourceTransactionManager tm = new DataSourceTransactionManager();
         tm.setDataSource(dataSource);
         return tm;
     }
 }

接着还需要告诉Spring哪些方法是事务方法,使用的是@Transactional,并且最好设置rollbackFor属性,这个注解还有其他的属性可以设置,与<tx:method>相似。

 @Transactional(rollbackFor = {Exception.class})
 public void checkout(String username,String isbn){...}

此外@Transactional还可以设置在类上,表示全部方法都是事务方法。

4)事务的传播行为

上面讲到了事务方法设置中有一个属性可以设置事务的传播行为,那么事务的传播行为是啥?

事务传播行为指的是一个事务方法被另一个事务方法调用时运行的方式。Spring中定义了其中传播行为,分别是:

  • REQUIRED:如果当前有事务则在其中运行,否则新开一个事务,在自己的事务里运行 (事务的属性都继承于大事务);
  • REQUIRES_NEW: 当前方法必须开启新事务,并在自己的事务里运行,如果有事务正在运行则挂起;
  • SUPPORTS:如果有事务在运行则方法在这个事务中运行,否则可以不运行在事务中;
  • NOT_SUPPORTED:当前方法不应运行在事务中,如果有运行的事务则将其挂起;
  • MANDATORY:当前方法必须运行在事务内部,否则抛出异常;
  • NEVER:当前方法不应运行在事务内部,否则抛出异常;
  • NESTED: 如果有事务在运行则当前方法应该在这个事务的嵌套事务中运行,否则启动一个新事务,并在自己的事务中运行。

5)事务失效

一般情况下,事务失效会有如下场景:

  • 在SSM开发中Spring和SpringMVC是分管两个容器,这时如果SpringMVC扫描了@Service那么对于@controller注入的则是没有事务的方法,这会导致事务失效。因此声明式事务的配置必须由Spring容器加载。
  • 如果@Transactional注解标注在接口上,但实现类使用 Cglib 代理,则事务会失效。你标注的是接口,但是Cglib代理时直接拿到实现类去构建代理对象,也就绕过了接口的事务管理。
  • 事务默认捕捉RuntimeException,如果抛出Exception,默认不捕捉,事务失效,所以一般情况下都是显式声明捕捉Exception。
  • 在Service 方法中自行 try-catch 异常处理,那么呈现给事务拦截器的就是没有异常的情况,自然也会导致事务失效。
  • 同一个类中,一个方法调用了自身另一个带有事务控制的方法,直接调用时也会导致事务失效。

参考资料: