Spring系列开篇 — 核心功能 IoC, AOP (附事务管理,spring与mybatis整合等)

6 篇文章 20 订阅
2 篇文章 1 订阅


这篇文章是Spring系列笔记的开篇,介绍了Spring的两大核心功能:IoC和AOP。以及基于AOP的事务管理,Spring与mybatis的整合等内容。代码示例比较详细,所以篇幅较长,后续会更新一篇总结性质的篇幅较短的文章,敬请关注(已更新, 传送门)。
(注:mybatis和spring整合的部分,在 事务支持小节)

概述

Spring是什么?

广义上讲,Spring指的是Spring技术栈,或Spring生态。这些技术栈涵盖了web应用,大数据,云计算等各个方面。

狭义地讲,Spring指的就是Spring生态中的 Spring Framework。这也是我们常说的Spring。而Spring Framework中最核心的部分就是IoC(控制反转)与AOP(面向切面编程)。

如果你在百度上搜索"Spring 是什么",得到的答案大概是:Spring是一个分层的JavaEE一站式轻量级开源框架。这是广义上的Spring,也是较为官方的定义。

根据官网(https://spring.io/projects)所述,从配置到安全,从web应用到大数据,Spring均有涉足。

Spring生态整体采用了模块化设计,针对不同的场景,可以选用Spring生态中的不同模块进行开发。如简单的Web应用我们可以用 Spring Framework ,微服务场景我们可以用SpringBoot,SpringCloud。

下图展示了Spring生态中所包含的各个模块。

而就算是Spring Framework 这个核心模块,它包含的内容也是非常丰富的。根据其官方文档,我们可以看到,Spring Framework 中包含了IoC容器,AOP等核心功能;测试相关内容;数据访问相关的 事务,JDBC,ORM等;Web部分(基于Servlet)的SpringMVCWebSocket,甚至还有支持响应式Web的WebFlux;以及JMS消息服务,邮件服务,任务调度等。

Spring Framework内部也采用了模块化设计,我们可以按需选择其中的模块进行使用。其结构如下图

其中最核心的部分,便是Core Container(Spring核心容器),可以看到,其他的如WebData Access都是以Core Container为基础的。

Core Container又分为4个子模块:BeansCoreContextSpEL,这4个子模块对应了4个jar包,在pom.xml中就对应了4个dependency

		<dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-core</artifactId>
            <version>5.0.10.RELEASE</version>
        </dependency>

        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-beans</artifactId>
            <version>5.0.10.RELEASE</version>
        </dependency>

        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>5.0.10.RELEASE</version>
        </dependency>

        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-expression</artifactId>
            <version>5.0.10.RELEASE</version>
        </dependency>

Spring的简单介绍就到这里,下面对Spring中的一些重要概念进行说明。

重要概念

  • IoC控制反转。它不是一种具体技术,而是一种编程思想。详见附录IoC
  • DI依赖注入。是对IoC从不同角度的描述,详见附录IoC
  • AOP面向切面编程。能够在不修改源代码的情况下,对类做一些增强(添加一些额外功能),详见附录aop
  • Spring容器:指的就是IoC容器,在Spring的底层源码中对应了BeanFactory这个接口,负责创建和管理对象bean实例)

快速入门

Spring最最核心的就是IoC容器(即Spring容器),Spring在启动时,会扫描相关配置,完成对象的创建和相关依赖的装配。Spring的快速入门demo如下

  1. 打开IDEA,创建一个maven项目,在pom.xml中添加spring的核心依赖包

    <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns="http://maven.apache.org/POM/4.0.0"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
        <modelVersion>4.0.0</modelVersion>
    
        <groupId>org.demo</groupId>
        <artifactId>spring-demo</artifactId>
        <version>1.0-SNAPSHOT</version>
    
        <dependencies>
            <dependency>
                <groupId>org.springframework</groupId>
                <artifactId>spring-core</artifactId>
                <version>5.0.10.RELEASE</version>
            </dependency>
    
            <dependency>
                <groupId>org.springframework</groupId>
                <artifactId>spring-beans</artifactId>
                <version>5.0.10.RELEASE</version>
            </dependency>
    
            <dependency>
                <groupId>org.springframework</groupId>
                <artifactId>spring-context</artifactId>
                <version>5.0.10.RELEASE</version>
            </dependency>
    
            <dependency>
                <groupId>org.springframework</groupId>
                <artifactId>spring-expression</artifactId>
                <version>5.0.10.RELEASE</version>
            </dependency>
    
            <dependency>
                <groupId>junit</groupId>
                <artifactId>junit</artifactId>
                <version>4.12</version>
                <scope>test</scope>
            </dependency>
        </dependencies>
    
    </project>
    
  2. 创建几个类

    package org.demo.dao;
    
    public class PetDao {
    	public String getPetName(int id) {
    		// 模拟访问数据库, 获取数据
    		return "dog00" + id;
    	}
    }
    
    package org.demo.service;
    
    import org.demo.dao.PetDao;
    
    public class PetService {
    
    	private PetDao dao;
    
    	public void feedPet(int id) {
    		// 先根据id从数据库查出宠物的名称
    		String petName = dao.getPetName(id);
    		// 给宠物喂食
    		System.out.println("给宠物 [" + petName +"] 喂食");
    	}
    
    	public void setDao(PetDao dao) {
    		this.dao = dao;
    	}
    }
    
  3. 创建Spring配置文件

    <!-- spring-demo.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"
           xsi:schemaLocation="http://www.springframework.org/schema/beans
                http://www.springframework.org/schema/beans/spring-beans.xsd">
    
        <bean id="petStore" class="org.demo.service.PetService">
            <property name="dao" ref="petDao"/>
        </bean>
    
        <bean id="petDao" class="org.demo.dao.PetDao"/>
    
    </beans>
    
  4. 编写测试类

    import org.demo.service.PetService;
    import org.junit.Test;
    import org.springframework.context.ApplicationContext;
    import org.springframework.context.support.ClassPathXmlApplicationContext;
    
    public class SpringTest {
    
    	@Test
    	public void test() {
    		ApplicationContext context = new ClassPathXmlApplicationContext("spring-demo.xml");
    		PetService petService = context.getBean(PetService.class);
    		petService.feedPet(7);
    	}
    }
    

    项目结构如下

    运行测试

可以看到,Spring做的无非就是提供了一个容器,在这个容器里帮你创建所需要的各种类的对象,并解析各个类对象之间的依赖关系,自动完成依赖注入(上面的示例将PetDao对象注入到了PetService对象中)。随后可以调用Spring容器的getBean方法,取出所需要的对象来使用即可。

下面对Spring的核心功能IoC,进行一个较为详细的说明。

基本使用

Spring的基本配置可以通过XML或者注解的方式进行。

纯XML

IoC配置

通过配置,告诉Spring,需要创建哪些对象(对象,bean实例,bean,说的都是同一个东西)到Spring容器中。

通过<bean>标签,即可将一个对象注册到Spring容器中,如

<!-- 完整的xml配置模板见前文【快速入门】  -->
<bean id="petDao" class="org.litespring.pojo.PetDao"/>

默认情况下Spring会调用类的无参构造函数来实例化对象,若没有无参构造函数则会实例化失败。(若<bean>标签中,有通过<constructor-arg>子标签配置构造器参数时,则会调用带参数的构造函数进行实例化。其他的不常用的实例化方式参考这里)。<bean>标签的相关属性说明如下

  • id

    对象在Spring容器中的唯一标识。也叫beanid,或者bean的名称。当不指定id时,默认使用简单类名的小写作为id

  • class

    唯一一个必填属性。指定类的全限定名。Spring会通过反射来创建一个该类的对象,默认调用无参构造函数。

  • init-method

    指定类中的初始化方法。在bean实例化完成(实例化指的是给这个对象分配内存空间,可以简单理解为,相当于代码中的new),依赖注入完成后,Spring会调用该属性指定的方法,进行一些自定义的初始化操作。可参见bean的生命周期

    注:通过注解配置bean时,可以通过@PostConstruct(在类的某个方法上打上该注解)完成类似的功能。只是@PostConstruct的执行顺序要早于init-method(若同时配置)

  • destroy-method

    有自定义初始化方法,也就有自定义的销毁方法。该属性指定了,当对象在Spring容器中被销毁时(准确地说是被销毁之前),需要执行的方法(一般是用来做一些资源的释放和清理)。

    对应注解中的@PreDestroy,只是@PreDestroy的执行顺序要早于destroy-method(若同时配置)

  • scope

    指定bean的作用范围。有如下几种取值

    • singleton默认值,单例。bean对象在整个Spring容器中只有1个。

      生命周期:当Spring容器初始化时,被创建,随后bean一直存活,直到应用关闭,Spring容器销毁时,bean才被销毁。

    • prototype:原型。每次从Spring容器中获取该bean时(每次调用BeanFactory中的getBean方法时),都会创建一个新的对象,也就是说bean是多例的。

      生命周期:当获取该bean时,创建一个新的对象实例,对象若被使用,就一直存活,当对象没有被任何地方引用,则会由垃圾回收器进行回收。

    • request仅在web环境下有该配置。每次请求会创建一个新的beanbean的存活周期和一次请求的周期一致。请求到来时,新建一个bean,请求结束时,bean被销毁。

      需要结合<aop:scoped-proxy/>标签使用,该值不常用

    • session仅在web环境下有该配置。每个会话(HttpSession)创建一个新的beanbean的存活周期和一次会话的周期一致。会话开启时,新建一个bean,会话结束时,bean被销毁。

      需要结合<aop:scoped-proxy/>标签使用,该值也不常用

    对于Spring中beanrequestsession作用域,想要进一步了解,可参考这篇文章

DI配置

依赖注入配置。通过配置,让Spring自动把一个类对象所需要的东西注入给它。

首先,什么是依赖?依赖是一个bean实例中的属性,比如下面这个类

package org.litespring.pojo;
public class PetStoreService {
	private PetDao dao;
	private String storeName;
    private List<String> customerNames;
}

这个类中的3个属性,都属于这个类的依赖。

依赖(或属性),根据其变量的类型,又可以分为3大类

  • 简单类型:8大Java基本类型和String
  • 对象类型:自定义的Java类(POJO)
  • 集合类型:jdk中自带的:ListSetMap

依赖注入则是对这些属性,进行赋值,这也是交给Spring去完成的。

依赖注入有如下2种方式

构造函数注入

通过类的构造函数,来对类的属性进行赋值。示例如下

package org.litespring.pojo;
public class PetStoreService {
	private PetDao dao;
	private String storeName;
	public PetStoreService(PetDao dao, String storeName) {
		this.dao = dao;
		this.storeName = storeName;
	}
	// 省略了get方法
}
<!-- spring-demo.xml -->
	<bean id="petStore" class="org.litespring.pojo.PetStoreService">
        <constructor-arg index="0" ref="petDao"/>
        <constructor-arg index="1" value="大黄"/>
    </bean>

	<bean id="petDao" class="org.litespring.pojo.PetDao"/>

测试

	@Test
	public void testConstruct() {
		ApplicationContext context = new ClassPathXmlApplicationContext("spring-demo.xml");
		PetStoreService petStore = (PetStoreService) context.getBean("petStore");
		System.out.println(petStore.getStoreName());
		System.out.println(petStore.getDao());
	}

主要是使用<bean>标签的子标签<constructor-arg>来进行的,<constructor-arg>标签有2个关键点:

  1. 指定要注入到构造函数中的哪个参数

  2. 要注入的值是什么

针对1,可以通过属性index指定构造函数中参数的位置下标,或通过name指定参数的名称,或通过type指定参数的类型。或者,当多个<constructor-arg>标签的顺序与构造函数的参数顺序一致,则可以不显式指定。

针对2,当注入的是简单类型变量,用value属性;当注入的是POJO类型(注入bean),用ref属性指定beanid(这个bean也需要注册到了Spring容器中);当注入的是集合类型,在下面的set方法注入部分再做说明

set方法注入

Spring会通过反射,调用类中的setXxx等方法,完成属性的注入。示例如下

首先,在类中,对需要注入的属性,需要有对应的set方法

package org.litespring.pojo;
public class PetStoreService {

	private PetDao dao;

	private String storeName;

	public void setDao(PetDao dao) {
		this.dao = dao;
	}

	public void setStoreName(String storeName) {
		this.storeName = storeName;
	}
    //省略get
}
	<!-- spring-demo.xml -->
	<bean id="petStore" class="org.litespring.pojo.PetStoreService">
        <property name="dao" ref="petDao"/>
        <property name="storeName" value="大黄"/>
    </bean>

    <bean id="petDao" class="org.litespring.pojo.PetDao"/>

测试

	@Test
	public void testProperty() {
		ApplicationContext context = new ClassPathXmlApplicationContext("spring-demo.xml");
		PetStoreService petStore = (PetStoreService) context.getBean("petStore");
		System.out.println(petStore.getStoreName());
		System.out.println(petStore.getDao());
	}

当需要注入的是集合类型时,采用如下方式,比如PetStoreService中有一个List类型的属性

package org.litespring.pojo;
import java.util.List;

public class PetStoreService {

	private List<String> customerNames;

	public void setCustomerNames(List<String> customerNames) {
		this.customerNames = customerNames;
	}
    // 省略get
}

则在XML文件中使用如下配置即可

   <bean id="petStore" class="org.litespring.pojo.PetStoreService">
        <property name="customerNames">
            <list>
                <value>大黄</value>
                <value>王二狗</value>
                <value>王狗蛋</value>
            </list>
        </property>
    </bean>

测试

	@Test
	public void testList() {
		ApplicationContext context = new ClassPathXmlApplicationContext("spring-demo.xml");
		PetStoreService petStore = (PetStoreService) context.getBean("petStore");
		petStore.getCustomerNames().forEach(System.out::println);
	}

其他的集合类型,如数组,setmapproperties等,分别按照如下标签进行配置即可

<!-- array -->
<bean id="b" class="B">
   <property name="arrs">
         <array>
            <value>数学</value>
           <value>英语</value>
         </array>
   </property>
</bean>
<!-- set -->
<bean id="b" class="B">
   <property name="arrs">
         <set>
            <value>数学</value>
           <value>英语</value>
         </set>
   </property>
</bean>
<!-- map -->
<bean id="b" class="B">
   <property name="arrs">
       <map>
           <entry key="age" value="18"/>
           <entry key="name" value="Bob"/>
       </map>
   </property>
</bean>
<!-- properties -->
<bean id="b" class="B">
   <property name="pro">
         <props>
              <prop key="id" value="root"/>
              <prop key="pw" value="root" />
         </props>
   </property>
</bean>
c命名空间和p命名空间

针对基于XML的构造器注入set方法注入,可以使用简化版的c命名空间注入p命名空间注入。(c代表constructor,即构造器注入;p代表property,即set方法注入)。

c命名空间注入的示例如下

假设有2个类

package com.food;
public class HotDog {
	public String getFoodName() {
		return "热狗";
	}
}
package com.food;
public class FoodStore {
	private HotDog food;
	private Integer price;
	public FoodStore(HotDog food, Integer price) {
		this.food = food;
		this.price = price;
	}
    //省略get
}

XML中进行如下配置(XML中记得先引入c命名空间,下面展示完整的XML文件)

<!--
c命名空间, 用于简化<constructor-arg>标签
格式: 
可以指定字段名,也可以用构造函数中形参的位置下标,分为引用注入和值注入,如
c:name-ref="anotherbean"
c:name="10"
c:_0-ref="anotherbean"
c:_0="10"
p命名空间, 用于简化<property>标签, 格式与上面类似
-->
<?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:c="http://www.springframework.org/schema/c"
       xmlns:p="http://www.springframework.org/schema/p"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
            http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="hotdog" class="com.food.HotDog"/>
    <bean id="store" class="com.food.FoodStore" c:food-ref="hotdog" c:price="10" />
</beans>

测试

	@Test
	public void TestCNameSpace() {
		ApplicationContext context = new ClassPathXmlApplicationContext("spring-demo.xml");
		FoodStore foodStore = context.getBean(FoodStore.class);
		System.out.println(foodStore.getFood().getFoodName());
		System.out.println(foodStore.getPrice());
	}

XML中进行配置时,IDEA也会进行自动提示

p命名空间注入类似,在类中添加set方法,随后在XML中进行配置(记得XML中先引入p命名空间)

XML+注解

IoC配置
  1. 首先,在Spring的XML配置文件中,添加<component-scan>标签,指定需要扫描的包

  2. 然后,在类上打上注解@Component,表示需要创建一个该类的对象到Spring容器中。其中@Component注解还有3个衍生注解:@Controller@Service@Repository。这3个注解提供了更强的语义,但效果和@Component是一样的。

    @Controller 一般用于表现层

    @Service一般用于业务层

    @Repository一般用于持久层

示例如下

package org.pet;
import org.springframework.stereotype.Component;
@Component
public class PetDao { }
package org.pet;
import org.springframework.stereotype.Component;
@Component
public class PetStoreService { }
<!-- spring-demo.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"
       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="org.pet"/> <!-- 扫描 org.pet包下所有带@Component注解的类 -->
</beans>

测试

package spring.test;

import org.junit.Test;
import org.pet.PetDao;
import org.pet.PetStoreService;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import static org.junit.Assert.*;
public class SpringTest {

	@Test
	public void test() {
		ApplicationContext context = new ClassPathXmlApplicationContext("spring-demo.xml");
		PetStoreService petStore = context.getBean(PetStoreService.class);
		PetDao petDao = context.getBean(PetDao.class);
		assertNotNull(petStore);
		assertNotNull(petDao);
	}
}

注:@Component注解中可以指定beanid,跟XML配置时<bean>标签中的id是一样的,当不指定时,beanid默认是当前类的简单类名(首字母小写)

DI配置

依赖注入有2步操作:1是从IoC容器中查找对象,2是将找到的对象进行注入

跟依赖注入相关的注解有如下几个

  • @Autowired

    • 可以注解在类的字段(Field),方法(Method),构造器(Constructor)上。

      当注解在类的字段上时,Spring会先根据该字段的变量类型,在IoC容器中查找对象,随后将找到的对象通过反射的方式,注入到这个字段上(这种方式不需要有set方法)。

      当注解在方法构造器上时,会根据方法的形参类型,在IoC容器中查找对象,随后通过反射的方式,调用这个方法,并把找到的对象传进去。示例如下

      package org.pet;
      
      import org.litespring.beans.factory.annotation.Autowired;
      import org.springframework.stereotype.Component;
      
      @Component
      public class PetStoreService {
          
      	@Autowired
      	private PetDao dao; // 会在Spring容器中查找类型为 PetDao 的对象, 并进行注入
      }
      
      package org.pet;
      
      import org.litespring.beans.factory.annotation.Autowired;
      import org.springframework.stereotype.Component;
      
      @Component
      public class PetStoreService {
          
      	private PetDao dao;
          
      	@Autowired
      	public PetStoreService(PetDao dao) {
      		this.dao = dao;
      	}
      }
      
    • @Autowired由Spring提供

    • @Autowired默认要求被注入的对象必须存在(不存在则报错),如果允许注入的值为null,则可以设置其require属性为false,如@Autowired(required = false)

    • @Autowired按照类型进行装配的

      @Autowired
      private PetDao dao;
      /** 
      像这种, 是会去Spring容器中找到一个类型为 PetDao的对象,然后进行注入。
      而不是根据名称, 去Spring容器中找一个id为 dao 的 bean
      **/
      
    • @Autowired在Spring源码中是通过AutowiredAnnotationBeanPostProcessor这个类实现的

  • @Qualifier

    由于@Autowired按照类型进行装配,若Spring容器中该类型的bean有多个,则会报错。此时,可以结合@Qualifier注解(注意这里的@Qualifier是Spring中定义的@Qualifier),指定beanid。示例如下

    package org.pet.service;
    
    public interface IPetService {
    	void doService();
    }
    
    package org.pet.service;
    
    import org.springframework.stereotype.Service;
    
    @Service
    public class GoodPetService implements IPetService {
    	@Override
    	public void doService() {
    		System.out.println("serve well");
    	}
    }
    
    package org.pet.service;
    
    import org.springframework.stereotype.Service;
    
    @Service
    public class BadPetService implements IPetService {
    	@Override
    	public void doService() {
    		System.out.println("not serve well");
    	}
    }
    
    package org.pet.controller;
    
    import org.pet.service.IPetService;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.beans.factory.annotation.Qualifier;
    import org.springframework.stereotype.Controller;
    
    @Controller
    public class PetController {
    
    	@Autowired
    	@Qualifier("goodPetService")
    	private IPetService petService;
    
    	public void servePet() {
    		petService.doService();
    	}
    }
    
    

    若在PetController中不加@Qualifier注解指定beanid,则Spring框架会报错NoUniqueBeanDefinitionException,因为有多个类型为IPetService的对象,Spring不知道该注入哪一个,所以此时需要添加@Qualifier注解,指定beanid

  • @Resource

    @Autowired类似。可以注解在类的字段上,方法上(构造方法不行)。当指定了name属性时,按照名称进行注入(按照beanid进行查找),当不指定name属性时,按照类型进行注入。

    @Resource注解属于JavaEE规范(JSR250),不属于Spring。该注解的全限定名为javax.annotation.Resource

    使用@Resource而不是@Autowired,便可以和Spring解耦。这样,若以后使用了另外的IoC框架,则无需修改代码。

    使用示例如下

    package org.pet.controller;
    
    import org.pet.service.IPetService;
    import org.springframework.stereotype.Controller;
    import javax.annotation.Resource;
    
    @Controller
    public class PetController {
    
    	@Resource(name = "goodPetService")
    	private IPetService petService;
    
    	public void servePet() {
    		petService.doService();
    	}
    }
    
    package org.pet.service;
    
    import org.springframework.stereotype.Service;
    
    @Service
    public class GoodPetService implements IPetService {
    	@Override
    	public void doService() {
    		System.out.println("serve well");
    	}
    }
    
  • @Inject

    @Autowired@Resource功能相同。它可以注解在类的字段方法构造器上。

    按照类型进行注入。

    @Inject属于JavaEE规范(JSR330),需要导入jar包依赖

            <dependency>
                <groupId>javax.inject</groupId>
                <artifactId>javax.inject</artifactId>
                <version>1</version>
            </dependency>
    

    @Inject属于同一规范的,还有个注解@Named,也在这个jar包中。由于@Inject按照类型进行注入,当存在多个类型相同的bean时,需要使用@Named指定beanid。这一点上,@Inject@Named(2个注解都是javax.inject包里面的)的组合类似于@Autowired@Qualifier(2个注解都是spring包里面的)。

    特别的,用于将一个对象注册到Spring容器中的@Component注解,也可以用@Named注解替换,Spring框架同样会将对象注册到IoC容器中。

    package org.pet.service;
    
    import javax.inject.Named;
    @Named
    public class BadPetService implements IPetService {
    	@Override
    	public void doService() {
    		System.out.println("not serve well");
    	}
    }
    
  • @Value

    前面的注解都是用于注入对象的。而@Value注解则是用来注入基本类型和String类型的。

    它支持用${}占位符来获取属性文件中的值。

    注意:当占位符中带有$,表示取这个变量的值;不带$时,直接注入字面量,说明如下

    // 带 $ 表示取 properties 文件中 key 为 user.name 的变量的值
    @Value("${user.name}")
    private String name;
    
    // 不带$, 则直接将 "user.name" 作为一个字符串,注入给name字段
    @Value("{user.name}")
    private String name;
    

    比如,在src/main/resources下新建一个db.properties

    db.user=eminem
    db.password=loseyourself
    db.age=20
    

    新建一个配置类

    package org.pet.properties;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.context.annotation.PropertySource;
    import org.springframework.stereotype.Component;
    
    @Component
    @PropertySource("classpath:db.properties") // 使用该注解加载properties配置文件
    public class DbConfig {
    
    	@Value("${db.user}")
    	private String user;
    
    	@Value("${db.password}")
    	private String password;
        
        @Value("${db.age}")
    	private int age;
        //省略get方法
    }
    

    XML中配置扫描org.pet

    <!-- 省略其余部分 -->
    <context:component-scan base-package="org.pet"/>
    

    测试代码

    	@Test
    	public void test2() {
    		ApplicationContext context = new ClassPathXmlApplicationContext("spring-demo.xml");
    		DbConfig dbConfig = context.getBean(DbConfig.class);
    		System.out.println(dbConfig.getUser());
    		System.out.println(dbConfig.getPassword());
            System.out.println(dbConfig.getAge());
    	}
    

    上面演示的是通过@PropertySource注解和@Value注解完成属性的注入,若在XML中要如何使用呢

    在XML中,通过配置一个PropertyPlaceholderConfigurer对象,即可在XML配置文件中使用${}获取其中的属性了,示例如下

    在项目的src/main/resources资源目录下,新建一个properties文件

    # user.properties
    user=eminem
    password=loseyourself
    

    新建一个类

    package org.demo.pet.config;
    
    public class UserConfig {
    
    	private String user;
    
    	private String password;
        
        // 省略get/set方法
    }
    
    

    创建XML配置文件

    <!-- spring-demo2.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:aop="http://www.springframework.org/schema/aop"
           xmlns:context="http://www.springframework.org/schema/context"
           xmlns:tx="http://www.springframework.org/schema/tx"
           xsi:schemaLocation="http://www.springframework.org/schema/beans
                               http://www.springframework.org/schema/beans/spring-beans.xsd
                               http://www.springframework.org/schema/aop
                               http://www.springframework.org/schema/aop/spring-aop.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">
    
        <bean id="propertyConfigurer" class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
            <property name="locations">
                <list>
                    <value>user.properties</value>
                </list>
            </property>
        </bean>
    
        <bean id="userConfig" class="org.demo.pet.config.UserConfig">
            <property name="user" value="${user}"/>
            <property name="password" value="${password}"/>
        </bean>
    
    </beans>
    

    测试

    package org.demo.test;
    
    import org.demo.pet.config.UserConfig;
    import org.junit.Test;
    import org.junit.runner.RunWith;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.test.context.ContextConfiguration;
    import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
    
    @RunWith(SpringJUnit4ClassRunner.class)
    @ContextConfiguration(locations = "classpath:spring-demo2.xml")
    public class PropertiesTest {
    
    	@Autowired
    	private UserConfig userConfig;
    
    	@Test
    	public void test() {
    		System.out.println(userConfig.getUser());
    		System.out.println(userConfig.getPassword());
    	}
    }
    

    其中,PropertyPlaceholderConfigurer对象,也可以用一个简化的注解<<context:property-placeholder>进行替换,效果相同,如下

    <context:property-placeholder location="user.properties"/>
    

其他的一些注解:

  • @Scope:注解在类上,与@Component配套使用,指定bean的作用范围,跟<bean>标签中的scope属性作用一致。可以设置为singletonprototype
  • @PostConstruct:注解在方法上,约等于<bean>标签中的init-method,用于在bean完成实例化和依赖注入后,进行自定义的初始化操作
  • @PreDestroy:注解在方法上,约等于<bean>标签中的destroy-method,用于在bean销毁之前,进行一些资源释放和清理操作

纯注解

上面的XML+注解的方式,还是需要有一个XML配置文件,来配置要扫描的包。

其实,若看XML不顺眼,则可以完全干掉它,全部采用纯注解进行配置。(在使用SpringBoot进行开发时,几乎都是采用纯注解的方式,并且其约定优于配置的特点,大大降低了开发的成本)

IoC配置
  • @Configuration

    在一个类上打上该注解,则该类就是一个Spring的配置类(相当于Spring的XML配置文件)

  • @Bean

    在配置类中,使用@Bean注解一个方法,方法的返回值是一个对象,即可把方法返回值的对象注册到Spring容器。可以配合@Scope注解来指定bean的作用范围。这个注解主要用来创建一些第三方jar包中的对象到Spring容器。(因为第三方jar包中的代码我们无法修改,无法在其源码上添加@Component等注解)

  • @ComponentScan

    通常和@Configuration一起使用,注解在类上,指定要扫描的包。可以通过basePackages属性,用字符串指定要扫描的包名(多个包直接用逗号分隔);也可以使用basePackageClasses属性指定某个类,则会扫描该类所在的包。

  • @Import

    用于组合多个配置类,比如在某个总配置类中,需要引入其他配置类

示例如下

package org.pet.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

import java.util.Arrays;
import java.util.List;

@Configuration
@ComponentScan(basePackages = {"org.pet.controller","org.pet.service"})
public class SpringConfig {

	@Bean
	public List<Integer> list() {
		return Arrays.asList(1,2,3);
	}
}

其中

package org.pet.controller;
import org.pet.service.IPetService;
import org.springframework.stereotype.Controller;
import javax.inject.Inject;

@Controller
public class PetController {

	@Inject
	private IPetService petService;

	public void servePet() {
		petService.doService();
	}
}
package org.pet.service;
import javax.inject.Named;

@Named
public class BadPetService implements IPetService {
	@Override
	public void doService() {
		System.out.println("not serve well");
	}
}

测试

	@Test
	public void test3() {
        // 使用AnnotationConfigApplicationContext, 传入配置类的class
		ApplicationContext context = new AnnotationConfigApplicationContext(SpringConfig.class);
		PetController petController = context.getBean(PetController.class);
		petController.servePet();
		List list = context.getBean(List.class);
		list.forEach(System.out::println);
	}

我们上面的单元测试,都是采用new的方式,传入XML配置文件或者配置类的class信息,来创建一个Spring的上下文,那么其实还有一种方式更为方便,那就是与JUnit整合

Spring整合JUnit4

需要引入依赖

        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-test</artifactId>
            <version>5.0.10.RELEASE</version>
        </dependency>

        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
            <scope>test</scope>
        </dependency>

在JUnit测试类上用@RunWith注解,传入SpringJUnit4ClassRunner.class,并用@ContextConfiguration注解申明要加载的Spring配置(可以指定加载XML文件或者配置类的class),随后就可以用@Autowired等DI相关的注解来注入需要测试的bean

package org.demo.test;

import org.demo.pet.service.PetService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:spring-demo.xml")
public class SpringTest {

	@Autowired
	private PetService petService;

	@Test
	public void testXmlAop() {
		petService.servePet();
	}
}

SpringAop

AOP的基本介绍参见附录aop部分

对于AOP的实现,最完备的有AspectJ框架,其功能非常强大,提供了完整的AOP实现;而Spring的AOP,旨在提供一个轻量级的实现,以便解决一些最常见的问题。

术语介绍

首先,对AOP相关的术语进行说明

  • JoinPoint

    连接点。即系统中,可以应用通知(参见下文Advice)的时机。也就是可以被AOP拦截下来的地方。

    连接点可以分为3类

    • 方法连接点:可以在方法执行时,进行拦截并添加一些功能
    • 字段连接点:可以在字段发生变化时,进行拦截并添加一些功能
    • 构造器连接点:可以在构造器执行时,进行拦截并添加一些功能

    SpringAOP仅支持方法级别的连接点,而AspectJ则支持全部的3种。

  • PointCut

    切入点。可以理解为连接点的匹配条件。满足这个匹配条件的一个或多个连接点,将会被应用通知。

    即,通过PointCut指定一个规则,满足这个规则的地方,会被AOP拦截下来进行功能增强

  • Advice

    通知,或者叫增强。AOP拦截下JointPoint之后要做的工作。比如我通过AOP拦截下一个方法,在这个方法执行前打印一下当前时间,那么打印时间这个动作,就是Advice,并且此时是前置通知,因为是在连接点执行之前做的增强。

    Advice描述了2个点

    • 要做什么?

      即拦截下JointPoint后要添加的额外功能

    • 在何时做?

      JointPoint之前,之后或者环绕。

      根据何时做,将通知分为了

      • 前置通知(Before)
      • 后置通知(After):无论JoinPoint是否正常执行完毕。在JoinPoint执行后,执行增强。可以理解为Java中的finally
      • 环绕通知(Around)
      • 返回通知(After-Returning):JoinPoint正常执行结束后,执行增强。
      • 异常通知(After-Throwing):JoinPoint抛出异常后,执行增强。
  • Aspect

    切面

    切面 = 切入点(PointCut) + 通知(Advice)

    • 切入点定义了切面在何处(在哪些连接点)被应用
    • 通知定义了切面需要做什么,以及何时

    总结起来就是,在何处何时做什么

  • Introduction

    引介

    通过提供一个接口和接口的实现类,向目标对象中添加新的方法。使用示例见下文。

  • Target

    目标对象。被AOP拦截下来的原对象。

  • Proxy

    代理对象。AOP实际是通过对目标对象进行拦截,生成一个代理对象,在代理对象中执行额外操作(增强)。

    代理对象中会持有目标对象,在代理对象完成增强后,会调用目标对象执行连接点的方法。

  • Weaving

    织入

    切面应用到目标对象,并创建代理对象的过程

    切面在指定的连接点,被织入到目标对象中。在目标对象的生命周期中有多个时间点可以进行织入:

    • 编译期

      在目标类编译时织入。需要特殊的编译器,AspectJ的织入即是这种方式。

    • 类加载期

      在目标类被加载到JVM时织入。需要特殊的类加载器,在目标类被加载时,增强该目标类的字节码

    • 运行期

      切面在运行时被织入,通过动态代理的方式创建一个代理对象(动态代理,分为JDK原生动态代理CGLib动态代理)。Spring AOP即是这种方式

    根据织入时机进行分类,前两种的叫做静态织入,最后一种叫动态织入。AspectJ采用的是静态织入,且主要在编译期织入,而SpringAOP采用的是动态织入

    两种动态代理的实现方式说明如下:

    JDK原生动态代理。基于接口,和反射技术。涉及到JDK中的2个类:java.lang.reflect.Proxyjava.lang.reflect.InvocationHandler。被代理的目标对象,必须要实现某一接口,以这个接口为纽带,来完成AOP。

    CGLib动态代理。基于继承,和字节码修改技术(asm),需要导入cglib依赖包,使用其中的EnhancerMethodInterceptor。被代理的目标对象,不能是final(那样会导致无法继承,无法创建子类),CGLib会创建一个目标对象的子类对象,来完成AOP。

JDK动态代理示例

package dynamic.proxy;

public interface IService {
	void doService();
}
package dynamic.proxy;

public class GoodService implements IService {
	@Override
	public void doService() {
		System.out.println("serve well");
	}
}
package dynamic.proxy;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

public class ProxyFactory implements InvocationHandler {

	private Object target;

	private ProxyFactory(Object target) {
		this.target = target;
	}

	@Override
	public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
		System.out.println("这是前置通知");
		Object returnVal = method.invoke(target, args);
		System.out.println("这是后置通知");
		return returnVal;
	}

	public static <T> T getProxy(T target) {
		ProxyFactory proxyFactory = new ProxyFactory(target);
		return (T) Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), proxyFactory);
	}
}

测试

package dynamic.proxy;

public class Test {
	public static void main(String[] args) {
		IService service = new GoodService();
		IService proxy = ProxyFactory.getProxy(service);
		proxy.doService();
	}
}

CGLib动态代理示例

先导入依赖

        <dependency>
            <groupId>cglib</groupId>
            <artifactId>cglib</artifactId>
            <version>3.3.0</version>
        </dependency>
package dynamic.proxy;

public class MyService {
	public void doService() {
		System.out.println("我的服务手法可好了!客官,试试?");
	}
}
package dynamic.proxy;

import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;

public class CgLibProxyFactory {

	public static <T> T getProxy(T target) {
		Enhancer enhancer = new Enhancer();
		enhancer.setSuperclass(target.getClass());
		enhancer.setCallback(new MethodInterceptor() {
			@Override
			public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
				System.out.println("前置通知");
				Object returnVal = methodProxy.invokeSuper(o, args);
				System.out.println("后置通知");
				return returnVal;
			}
		});
		return (T) enhancer.create();
	}
}

测试

package dynamic.proxy;

public class Test {

	public static void main(String[] args) {
		MyService service = new MyService();
		MyService proxy = CgLibProxyFactory.getProxy(service);
		proxy.doService();
	}
}

使用

因为现在基本都采用SpringBoot做快速开发了,所以这里先讲解纯注解的SpringAOP使用(这也是最简单的),至于XML的形式,会在本小节的最后部分略微提及。

快速上手

注意,除了有spring最基本的几个依赖,(contextbeancore),还需要有如下maven依赖

        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-aop</artifactId>
            <version>5.0.10.RELEASE</version>
        </dependency>

        <!-- aspectj -->
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
            <version>1.9.6</version>
        </dependency>

首先要有个目标类

package org.demo.pet.service;

import org.springframework.stereotype.Service;

@Service
public class PetService {
	public void servePet() {
		System.out.println("开始撸猫!");
	}
}

然后,进行切面配置

package org.demo.pet.aop;

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class LogAop {

    // 切入点, 这里指定切入 PetService类中的任意方法, 方法参数为空
	@Pointcut("execution(* org.demo.pet.service.PetService.*())")
	private void pointcut() {}

    // 当然, 也可以不使用@Pointcut注解单独定义切入点
    // 可以直接将切入点表达式(@Pointcut后面那部分字符串),写到下面的@Before里
	@Before("pointcut()")
	public void before() {
		System.out.println("你好, 我是前置通知, 负责记录日志的~");
	}
}

最后,通过@Configuration进行Spring配置,记得加上@EnableAspectJAutoProxy

package org.demo.pet;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;

@Configuration
@ComponentScan
@EnableAspectJAutoProxy
public class SpringConfig {
}

测试

package org.demo.test;

import org.demo.pet.SpringConfig;
import org.demo.pet.service.PetService;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class SpringTest {

	@Test
	public void testAop() {
		ApplicationContext context = new AnnotationConfigApplicationContext(SpringConfig.class);
		PetService petService = context.getBean(PetService.class);
		petService.servePet();
	}
}

使用AOP的核心内容,是AOP的配置类,下面进行详细说明。

首先,AOP配置类需要注册到Spring容器中,所以要打上@Component注解,并且要打上@Aspect注解,以表示这个类是切面配置类。

随后,就是配置AOP中最重要的切入点(PointCut)和通知(Advice)。

切入点+通知,构成了一个切面(Aspect)

定义切入点,用@Pointcut注解,注解中通过指示器(designators)来编写切入点的匹配条件。有如下的几种指示器

切入点指示器
  • execution()

    execution指示器匹配的粒度最细,是方法级别

    格式:[修饰符] 返回值类型 包名.类名.方法名(方法参数) [异常声明]

    其中修饰符异常声明是可选的

    示例

    @Pointcut("execution(public * org.demo.pet.service.PetService.*())")
    private void pointcut() {}
    // 上面的指示器,public指的是匹配public方法,第一个*表示返回值是任意类型,中间表示匹配org.demo.pet.service.PetService这个类,后面的*表示PetSerivce中的任意方法,再后面的()是方法的形参列表,为空表示匹配参数为空的方法
    
    @Pointcut("execution(* org.demo..*.*())")
    private void pointcut() {}
    // 上面省略了方法修饰符,第一个*表示返回值是任意类型,后面的是包名org.demo,在后面的2个点号..表示当前包及其下的所有子孙包(org.demo下面的全部)
    // 后面的*表示包下的任意类,再后面的*表示任意方法,最后的()表示参数为空
    
    @Pointcut("execution(* org.demo.pet.service.PetService.*(String,Long))")
    private void pointcut() {}
    //上面的表示匹配org.demo.pet.service.PetService类的任意方法,且方法接收2个参数,第1个参数为String,第2个参数为Long
    
    @Pointcut("execution(* org.demo.pet.service.PetService.*(Integer,Long,..))")
    private void pointcut() {}
    //上面的表示匹配org.demo.pet.service.PetService类的任意方法,且方法接收至少2个参数,前2个参数分别为Integer和Long
    
    @Pointcut("execution(* *..*Service.*(..))")
    private void pointcut() {}
    //上面的表示匹配类名以Service结尾的类中的任意方法,方法的参数任意
    
    @Pointcut("execution(* *..*Service.do*(..))")
    private void pointcut() {}
    //上面的表示匹配类名以Service结尾的类中的以do开头的方法,方法的参数任意
    
  • within()

    匹配粒度是类级别,类中全部方法都会被匹配上。(注意,若指定匹配某一接口,则接口的实现类并不会被AOP拦截到)

    格式跟execution略有不同,仅仅需要指定类的全限定名即可,当然,中间也可以使用通配符*,以及表示多级包的..

    示例

    //匹配com.service包下的所有类, 最后一个*表示这个包下的所有类
    @Pointcut("within(com.service.*)")
    private void service(){}
    	
    //匹配com.service包下所有类名以Service结尾的类
    @Pointcut("within(com.service.*Service)")
    private void service(){}
    
    // 匹配com.service包及其子孙包下的全部类
    @Pointcut("within(com.service..*)")
    private void service(){}
    
    //注意within用来匹配接口是无效的
    //假设我们期望匹配com.service下所有实现了SomeService接口的类
    //如下的写法并不能达成目标,将within换成this,target,或者使用更通用的execution即可
    //或者在within里面的接口后面加上一个加号+ ,表示匹配类及其子类
    @Pointcut("within(com.service.SomeService)")
    public void service(){}
    
  • this()

    匹配目标对象的AOP代理对象,可以拦截到introduction中的方法

    	//拦截SomeService接口的所有实现类(就算实现类不在com.service包下)
    	@Pointcut("this(com.service.SomeService)")
        public void service(){}
    
        @Before("service()")
        public void aop(){
            System.out.println("aop take effect");
        }
    
  • target

    匹配目标对象,不能拦截introduction中的方法,在没有引入introductio时,和this是一样的

  • bean

    匹配Spring IoC容器里的bean(通过beanid)

    //匹配bean名称为dinnerService的bean对象中只有一个参数且为Long的方法
    // 表达式可以用 && || 等连接符来做运算
    @Pointcut("bean(dinnerService) && args(Long)")
    public void service(){}
    
  • args()

    匹配参数

    	//匹配SomeService接口的所有实现类中,只有一个参数,且参数类型为Long的方法
    	@Pointcut("this(service.SomeService) && args(Long)")
        public void service(){}
    	//匹配SomeService接口的所有实现类中,只有一个参数且为Long,或者第一个参数为Long的方法
    	@Pointcut("this(service.SomeService) && args(Long,..)")
        public void service2(){}
    
        @Before("service()")
        public void aop(){
            System.out.println("aop take effect");
        }
    
  • @annotation()

    匹配带有某一注解的方法。下面示例额外演示了AOP中的参数绑定

    先定义一个注解

    package org.demo.pet.annotations;
    
    import java.lang.annotation.ElementType;
    import java.lang.annotation.Retention;
    import java.lang.annotation.RetentionPolicy;
    import java.lang.annotation.Target;
    
    @Target(ElementType.METHOD)
    @Retention(RetentionPolicy.RUNTIME)
    public @interface Secure {
    	String apiName() default "";
    }
    

    再定义一个Service

    package org.demo.pet.service;
    
    import org.demo.pet.annotations.Secure;
    import org.springframework.stereotype.Service;
    
    @Service
    public class ConfidentialService {
    
    	@Secure(apiName = "withdraw")
    	public void withdrawMoney(Long amount) {
    		System.out.println("取了" + amount + "钱");
    	}
    }
    
    

    定义一个AOP

    package org.demo.pet.aop;
    
    import org.aspectj.lang.annotation.Aspect;
    import org.aspectj.lang.annotation.Before;
    import org.demo.pet.annotations.Secure;
    import org.springframework.stereotype.Component;
    
    @Aspect
    @Component
    public class SecureAop {
    	@Before("@annotation(secure)")
    	public void before(Secure secure) {
    		System.out.println("我是Secure前置通知, 你所调用的api是" + secure.apiName());
    	}
    }
    

    使用配置类进行配置

    package org.demo.pet;
    
    import org.springframework.context.annotation.ComponentScan;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.context.annotation.EnableAspectJAutoProxy;
    
    @Configuration
    @ComponentScan
    @EnableAspectJAutoProxy
    public class SpringConfig {
    }
    

    测试

    package org.demo.test;
    
    import org.demo.pet.SpringConfig;
    import org.demo.pet.service.ConfidentialService;
    import org.junit.Test;
    import org.junit.runner.RunWith;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.test.context.ContextConfiguration;
    import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
    
    @RunWith(SpringJUnit4ClassRunner.class)
    @ContextConfiguration(classes = SpringConfig.class)
    public class SpringTest {
    
    	@Autowired
    	private ConfidentialService confidentialService;
    
    	@Test
    	public void testAnnotationAop() {
    		confidentialService.withdrawMoney(1000L);
    	}
    }
    
    

  • @within

    匹配带有某一注解的类

    @Pointcut("@within(anno.CheckThis)")
    public void check(){}
    
    @Component
    @CheckThis
    public class DinnerService implements SomeService {
        @Override
        public void doService() {
            System.out.println("Serve dinner");
        }
    }
    
  • @target

    匹配带有某一注解的类

  • @args()

    匹配带某一注解的参数,示例如下

    // 错误的使用如下
    //如,有一个这样的方法
    public void service(@CheckThis Args args){
    }
    //下面的切入点,并不能拦截到上面的service方法
    @Pointcut("@args(com.anno.CheckThis)")
    
    //正确的使用如下
    //定义一个类, 类上带注解
    @CheckThis
    public class Args{
    }
    //某个方法的入参是这个类
    public void service(Args args){ }
    //下面的切入点,能够拦截到service方法了
    @Pointcut("@args(com.anno.CheckThis)")
    
  • 切入点表达式中的通配符

    • * 匹配任意数量字符
    • + 匹配指定类及子类
    • .. 匹配包及子包,或任意参数
  • 切入点表达式中的逻辑运算符

    • && :逻辑与。在XML配置中,使用and
    • || :逻辑或。在XML配置中,使用or
    • ! :逻辑非。在XML配置中,使用 not
AOP参数传递

示例

先定义一个POJO

package org.demo.pet.po;

public class Pet {

	private Integer petNo;

	private String petName;

	private Integer petAge;

	private String masterName;
    // 省略构造函数, get/set函数
	
}

定义一个Service

package org.demo.pet.service;

import org.demo.pet.po.Pet;
import org.springframework.stereotype.Service;

@Service
public class PetService {
	public void introduce(Pet pet) {
		System.out.printf("我叫%s, 我%d岁了, 我的主人是%s\n", pet.getPetName(), pet.getPetAge(), pet.getMasterName());
	}
}

定义AOP

package org.demo.pet.aop;

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.demo.pet.po.Pet;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class LogAop {

    // 匹配任意包下类名以Service结尾的类中的方法名以intro开头的方法, 且具有一个Pet类型的参数
	@Pointcut("execution(* *..*Service.intro*(..)) && args(pet)")
	private void pointcut(Pet pet) {}

	@Before("pointcut(pet)")
	public void before(Pet pet) {
		System.out.println("下面有请第" + pet.getPetNo() + "号选手进行自我介绍~~鼓掌~~");
	}
}

-----2022/06/09更新—begin
如果使用@Around,并进行参数绑定,要保证ProceedingJoinPoint放在第一个位置,如下

	@Pointcut("@annotation(com.test.aop.RequestLimit) && args(request)")
	private void pointcut(CommonRequest request) {}
	
	@Around("pointcut(request)")
	private Object requestLimitControl(ProceedingJoinPoint joinPoint, CommonRequest request) throws Throwable {
	
	}

我一开始写的

	@Around("pointcut(request)")
	private Object requestLimitControl(CommonRequest request, ProceedingJoinPoint joinPoint) throws Throwable {
	
	}

spring启动时会报错 error at ::0 formal unbound in pointcut

-----2022/06/09更新—end

Spring配置类

package org.demo.pet;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;

@Configuration
@ComponentScan
@EnableAspectJAutoProxy  // 开启AOP自动代理
public class SpringConfig {
}

测试

package org.demo.test;

import org.demo.pet.SpringConfig;
import org.demo.pet.po.Pet;
import org.demo.pet.service.PetService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = SpringConfig.class)
public class SpringTest {

	@Autowired
	private PetService petService;

	@Test
	public void  test() {
		Pet pet = new Pet(23, "小柴柴", 1, "大黄");
		petService.introduce(pet);
	}
}

AOP引介的使用

我们知道AOP实际是通过生成一个代理对象来完成的。引介可以为被代理的目标对象添加新的属性或新的方法。其原理大概是:将新的方法或属性,用一个新的bean去实现,然后代理对象同时包装了目标对象和这个新的bean。调用代理对象的方法时,会先判断方法是原始目标对象的,还是通过引介引入的新方法。代理对象再委托给其内部的不同bean去执行。

食用示例如下

定义一个接口以及实现类

package org.demo.pet.intro;

public interface IntroFunction {
	void fun();
}
package org.demo.pet.intro;

public class IntroFunctionImpl implements IntroFunction {
	@Override
	public void fun() {
		System.out.println("this is a new function added by introduction");
	}
}

配置AOP

package org.demo.pet.aop;

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.DeclareParents;
import org.demo.pet.intro.IntroFunction;
import org.demo.pet.intro.IntroFunctionImpl;
import org.springframework.stereotype.Component;

@Component
@Aspect
public class IntroAop {

	@DeclareParents(value = "org.demo.pet.service.PetService", defaultImpl = IntroFunctionImpl.class)
	public IntroFunction introFunction;

}

配置类

package org.demo.pet;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;

@Configuration
@ComponentScan(basePackages = {"org.demo.pet.aop", "org.demo.pet.service"})
@EnableAspectJAutoProxy
public class IntroConfig {
}

测试

package org.demo.test;

import org.demo.pet.IntroConfig;
import org.demo.pet.intro.IntroFunction;
import org.demo.pet.service.PetService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;


@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = IntroConfig.class)
public class IntroTest {

	@Autowired
	private PetService petService;

	@Test
	public void test() {
		IntroFunction introFunction = (IntroFunction) petService;
		introFunction.fun();
	}
}

基于XML的AOP使用

准备一个service

package org.demo.pet.service;

import org.demo.pet.po.Pet;

public class PetService {
	public void introduce(Pet pet) {
		System.out.printf("我叫%s, 我%d岁了, 我的主人是%s\n", pet.getPetName(), pet.getPetAge(), pet.getMasterName());
	}
}

准备一个切面类

package org.demo.pet.aop;

public class XmlAspect {

	public void log() {
		System.out.println("我是XML的AOP");
	}
}

配置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: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/aop
                           http://www.springframework.org/schema/aop/spring-aop.xsd">

    <bean id="petService" class="org.demo.pet.service.PetService"/>

    <bean id="xmlAspect" class="org.demo.pet.aop.XmlAspect"/>

    <aop:config>
        <aop:pointcut id="pointcut" expression="within(org..*.PetService)"/>
        <aop:aspect ref="xmlAspect"> <!-- 使用xmlAspect这个对象作为切面对象 -->
             <!-- 调用切面对象中的log方法, 对指定的切入点, 应用前置通知 -->
            <aop:before method="log" pointcut-ref="pointcut"/>
        </aop:aspect>
    </aop:config>

</beans>

测试

package org.demo.test;

import org.demo.pet.po.Pet;
import org.demo.pet.service.PetService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:spring-demo2.xml")
public class XmlAopTest {

	@Autowired
	private PetService petService;

	@Test
	public void test() {
		Pet pet = new Pet(1, "小柴", 2, "大黄");
		petService.introduce(pet);
	}
}

结果

事务支持

Spring并不直接管理事务,而是提供了一系列接口。

事务相关的核心接口有3个:PlatformTransactionManagerTransactionDefinitionTransactionStatus

接口的具体实现类由各个平台自己去完成,如JDBC有DataSourceTransactionManager(spring-jdbc包下),Hibernate有HibernateTransactionManager

  • PlatformTransactionManager

    平台事务管理器。根据不同的持久层框架,选择不同的实现类。如使用的持久层框架是Mybatis或者Spring JDBC Template,需要选用DataSourceTransactionManager实现类(在spring-jdbc这个jar包中);若使用的是Hibernate,需要选用HibernateTransactionManager实现类

    该接口提供了3个方法:

    • getTransaction
    • commit
    • rollback

    分别用来获取当前事务提交事务回滚事务

  • TransactionDefinition

    事务的定义信息。如,事务的隔离级别,传播行为,超时,只读等。

  • TransactionStatus

    事务的状态信息。如,是否是新事务,是否已提交,是否有保存点,是否回滚等。

XML方式

首先,事务管理是针对持久层数据库访问的,所以需要先配置一个数据源DataSource;随后,配置一个PlatformTransactionManager的对象,用来管理事务。接着,需要定义,哪些方法需要进行事务管理,以及具体的事务配置(隔离级别,传播行为等)。最后,配置aop,将PlatformTransactionManager对象切入目标类。

下面的演示需要持久层的参与,故通过spring整合mybatis,进行演示

  1. 数据库建表及初始化

    CREATE TABLE `pet` (
      `pet_no` int NOT NULL AUTO_INCREMENT,
      `pet_name` varchar(255) NOT NULL,
      `pet_age` int NOT NULL,
      `master_name` varchar(255) NOT NULL,
      PRIMARY KEY (`pet_no`)
    );
    INSERT INTO pet (`pet_no`, `pet_name`, `pet_age`, `master_name`) VALUES (1, '小柴柴', 1, '大黄');
    

  2. 添加maven依赖

            <dependency>
                <groupId>org.springframework</groupId>
                <artifactId>spring-core</artifactId>
                <version>5.0.10.RELEASE</version>
            </dependency>
    
            <dependency>
                <groupId>org.springframework</groupId>
                <artifactId>spring-beans</artifactId>
                <version>5.0.10.RELEASE</version>
            </dependency>
    
            <dependency>
                <groupId>org.springframework</groupId>
                <artifactId>spring-context</artifactId>
                <version>5.0.10.RELEASE</version>
            </dependency>
    
            <dependency>
                <groupId>org.springframework</groupId>
                <artifactId>spring-aop</artifactId>
                <version>5.0.10.RELEASE</version>
            </dependency>
    
            <!-- aspectj -->
            <dependency>
                <groupId>org.aspectj</groupId>
                <artifactId>aspectjweaver</artifactId>
                <version>1.9.6</version>
            </dependency>
    
            <dependency>
                <groupId>junit</groupId>
                <artifactId>junit</artifactId>
                <version>4.12</version>
                <scope>test</scope>
            </dependency>
    
           <dependency>
                <groupId>org.springframework</groupId>
                <artifactId>spring-test</artifactId>
                <version>5.0.10.RELEASE</version>
            </dependency>
    
            <!-- spring 事务管理 -->
            <dependency>
                <groupId>org.springframework</groupId>
                <artifactId>spring-tx</artifactId>
                <version>5.0.10.RELEASE</version>
            </dependency>
    
            <dependency>
                <groupId>org.springframework</groupId>
                <artifactId>spring-jdbc</artifactId>
                <version>5.0.10.RELEASE</version>
            </dependency>
    
            <!-- mysql 驱动 -->
            <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
                <version>8.0.21</version>
                <scope>runtime</scope>
            </dependency>
    
            <!-- mybatis -->
            <dependency>
                <groupId>org.mybatis</groupId>
                <artifactId>mybatis</artifactId>
                <version>3.4.6</version>
            </dependency>
    
            <!-- spring - mybatis 桥梁包 -->
            <dependency>
                <groupId>org.mybatis</groupId>
                <artifactId>mybatis-spring</artifactId>
                <version>2.0.1</version>
            </dependency>
    
    		<!-- 阿里巴巴的 druid 连接池 -->
            <dependency>
                <groupId>com.alibaba</groupId>
                <artifactId>druid</artifactId>
                <version>1.1.10</version>
            </dependency>
    
  3. 创建PO类

    package org.demo.pet.po;
    
    public class Pet {
    
    	private Integer petNo;
    
    	private String petName;
    
    	private Integer petAge;
    
    	private String masterName;
        //省略构造函数, get/set函数
    }
    
  4. 创建mybatis的XML配置文件,创建包含SQL语句的mapper.xml文件(注意mybatis的XML配置文件中,不需要再配置数据源信息,数据源将交由Spring管理)

    <!-- mybatis-config.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <!DOCTYPE configuration
            PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
            "http://mybatis.org/dtd/mybatis-3-config.dtd">
    <configuration>
    
        <settings>
            <setting name="mapUnderscoreToCamelCase" value="true"/> <!-- 开启下划线-驼峰命名映射 -->
        </settings>
    
        <typeAliases>
            <package name="org.demo.pet.po"/>
        </typeAliases>
    
        <mappers>
            <mapper resource="PetMapper.xml"/>
        </mappers>
    
    </configuration>
    
    <!-- PetMapper.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <!DOCTYPE mapper
            PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
            "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
    
    <mapper namespace="org.demo.pet.mapper.PetMapper">
    
        <insert id="insert" parameterType="pet">
            insert into pet (pet_name, pet_age, master_name) VALUES (#{petName}, #{petAge}, #{masterName})
        </insert>
    
        <select id="findAll" resultType="pet">
            select * from pet;
        </select>
    
        <update id="changeMasterName" parameterType="pet">
            update pet set master_name = #{masterName} where pet_no = #{petNo}
        </update>
    </mapper>
    
  5. 创建PetMapper.xml对应的mapper接口

    package org.demo.pet.mapper;
    
    import org.demo.pet.po.Pet;
    
    import java.util.List;
    
    public interface PetMapper {
    	int insert(Pet pet);
    
    	List<Pet> findAll();
    
    	int changeMasterName(Pet pet);
    
    }
    
  6. 创建PetDao类

    package org.demo.pet.dao;
    
    import org.demo.pet.mapper.PetMapper;
    import org.demo.pet.po.Pet;
    
    public class PetDao {
    
    	private PetMapper mapper;
    
    	public void setMapper(PetMapper mapper) {
    		this.mapper = mapper;
    	}
    
    	public void changeMasterName(int petNo, String masterName) {
    		Pet pet = new Pet();
    		pet.setPetNo(petNo);
    		pet.setMasterName(masterName);
    		mapper.changeMasterName(pet); // 先进行数据库更新
    		System.out.println(1/0); // 再抛出异常
    	}
    }
    
  7. 配置数据库信息

    # db.properties
    db.url=jdbc:mysql://localhost:3306/yogurt?serverTimezone=Asia/Shanghai
    db.user=root
    db.password=root
    db.driver=com.mysql.cj.jdbc.Driver
    
  8. 配置Spring的XML文件(在这个XML文件里配置数据源, 以及mybatis的SqlSessionFactory等)

    <!-- spring-demo.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:aop="http://www.springframework.org/schema/aop"
           xmlns:context="http://www.springframework.org/schema/context"
           xmlns:tx="http://www.springframework.org/schema/tx"
           xsi:schemaLocation="http://www.springframework.org/schema/beans 
                               http://www.springframework.org/schema/beans/spring-beans.xsd
                               http://www.springframework.org/schema/aop 
                               http://www.springframework.org/schema/aop/spring-aop.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">
    
    
        <!-- 加载 db.properties -->
        <context:property-placeholder location="db.properties"/>
    
        
        <!-- 创建 DataSource 数据库连接池 -->
        <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
            <property name="driverClassName" value="${db.driver}"/>
            <property name="url" value="${db.url}"/>
            <property name="username" value="${db.user}"/>
            <property name="password" value="${db.password}"/>
            <property name="initialSize" value="5"/>
            <property name="maxActive" value="20"/>
            <property name="minIdle" value="5"/>
        </bean>
    
        <!-- 配置mybatis的SqlSessionFactory -->
        <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
            <property name="dataSource" ref="dataSource"/> <!-- 传入上面的 DataSource 数据源 -->
            <property name="configLocation" value="mybatis-config.xml"/>  <!-- 传入mybatis的XML配置文件 -->
        </bean>
    
       	<!-- 对 PetMapper 接口, 生成代理对象 -->
        <bean id="petMapper" class="org.mybatis.spring.mapper.MapperFactoryBean">
            <property name="mapperInterface" value="org.demo.pet.mapper.PetMapper"/>
            <property name="sqlSessionFactory" ref="sqlSessionFactory"/>
        </bean>
    
        <!-- 创建PetDao -->
        <bean id="petDao" class="org.demo.pet.dao.PetDao">
            <property name="mapper" ref="petMapper"/>
        </bean>
    
        <!-- 事务管理器, 由于用的是mybatis, 所以这里用 spring-jdbc 中的 DataSourceTransactionManager -->
        <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="change*"/> <!-- 对命名以change开头的方法, 进行事务管理, 这里还可以配置事务隔离级别, 传播行为等 -->
            </tx:attributes>
        </tx:advice>
    
        <!-- 通过 aop, 将上面的事务配置, 切入到具体的目标类 -->
        <aop:config>
            <!-- 事务aop必须用 <aop:advisor> -->
            <aop:advisor advice-ref="txAdvice" pointcut="execution(* org.demo.pet.dao.PetDao.change*(..))"/>
        </aop:config>
    
    </beans>
    
  9. 测试

    package org.demo.test;
    
    import org.demo.pet.dao.PetDao;
    import org.junit.Test;
    import org.junit.runner.RunWith;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.test.context.ContextConfiguration;
    import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
    
    @RunWith(SpringJUnit4ClassRunner.class)
    @ContextConfiguration(locations = "classpath:spring-demo.xml")
    public class SpringMybatisXmlTest {
    
    	@Autowired
    	private PetDao petDao;
    
    	@Test
    	public void testTransactional() {
    		int petNo = 1;
    		String newMasterName = "小李";
    		petDao.changeMasterName(petNo, newMasterName);
    	}
    }
    
    

    在执行PetDao中的changeMasterName这个方法时,我们先进行了数据库更新,随后抛出了一个异常。我们希望把changeMasterName这整个方法作为一个事务,若这个方法执行过程中出现了异常,则应该进行事务回滚。为了更清晰地看到SQL执行情况,我们通过如下的配置打开mybatis的SQL语句打印功能

    1. 添加log4j的maven依赖

              <!-- 日志 -->
              <dependency>
                  <groupId>log4j</groupId>
                  <artifactId>log4j</artifactId>
                  <version>1.2.17</version>
              </dependency>
      
              <dependency>
                  <groupId>org.slf4j</groupId>
                  <artifactId>slf4j-log4j12</artifactId>
                  <version>1.7.7</version>
              </dependency>
      
              <dependency>
                  <groupId>org.slf4j</groupId>
                  <artifactId>slf4j-api</artifactId>
                  <version>1.7.30</version>
              </dependency>
      
    2. mybatis的XML配置文件中开启日志打印

      <?xml version="1.0" encoding="UTF-8" ?>
      <!DOCTYPE configuration
              PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
              "http://mybatis.org/dtd/mybatis-3-config.dtd">
      <configuration>
      
          <settings>
              <setting name="mapUnderscoreToCamelCase" value="true"/>
              <setting name="logImpl" value="LOG4J"/>  <!-- 开启日志打印, 也就是这个配置 -->
          </settings>
      
          <typeAliases>
              <package name="org.demo.pet.po"/>
          </typeAliases>
      
          <mappers>
              <mapper resource="PetMapper.xml"/>
          </mappers>
      
      </configuration>
      
    3. 添加log4j.properties配置文件到src/main/resources目录下

      # 开启DEBUG级别的日志, 才能看到SQL语句
      log4j.rootLogger=DEBUG, CONSOLE
      ## for console
      log4j.appender.CONSOLE=org.apache.log4j.ConsoleAppender
      log4j.appender.CONSOLE.layout=org.apache.log4j.PatternLayout
      log4j.appender.CONSOLE.layout.ConversionPattern=%d %p %t %c - %m%n
      log4j.appender.CONSOLE.Encoding=UTF-8
      
      

    此时运行测试代码,控制台的日志情况如下

    根据日志,确实发生了事务回滚

    我们观察数据库,这一行数据并没有发生修改,说明事务管理生效

    若随便进行一下改动,比如在Spring的XML配置中,将AOP的相关配置去掉,即去掉下面的XML配置

        <aop:config>
            <!-- 事务aop必须用 <aop:advisor> -->
            <aop:advisor advice-ref="txAdvice" pointcut="execution(* org.demo.pet.dao.PetDao.change*(..))"/>
        </aop:config>
    

    再次执行,结果如下,没有发生事务回滚,数据更新成功

    再查看数据库,发现信息已经被修改,说明事务管理未生效

XML+注解

该种方式与XML方式大同小异,唯一的区别是,Spring的XML文件中无需配置AOP了。在需要事务管理的方法上打上@Transactional注解即可,具体的事务隔离级别,传播行为,可以在@Transactional注解中进行配置,示例如下

  • Spring的XML配置文件,去掉<tx:advice><aop-config>标签,添加<tx:annotation-driven>标签

    <?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:aop="http://www.springframework.org/schema/aop"
           xmlns:context="http://www.springframework.org/schema/context"
           xmlns:tx="http://www.springframework.org/schema/tx"
           xsi:schemaLocation="http://www.springframework.org/schema/beans
                               http://www.springframework.org/schema/beans/spring-beans.xsd
                               http://www.springframework.org/schema/aop
                               http://www.springframework.org/schema/aop/spring-aop.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:property-placeholder location="db.properties"/>
    
        <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
            <property name="driverClassName" value="${db.driver}"/>
            <property name="url" value="${db.url}"/>
            <property name="username" value="${db.user}"/>
            <property name="password" value="${db.password}"/>
            <property name="initialSize" value="5"/>
            <property name="maxActive" value="20"/>
            <property name="minIdle" value="5"/>
        </bean>
    
        <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
            <property name="dataSource" ref="dataSource"/>
            <property name="configLocation" value="mybatis-config.xml"/>
        </bean>
    
        <bean id="petMapper" class="org.mybatis.spring.mapper.MapperFactoryBean">
            <property name="mapperInterface" value="org.demo.pet.mapper.PetMapper"/>
            <property name="sqlSessionFactory" ref="sqlSessionFactory"/>
        </bean>
    
        <bean id="petDao" class="org.demo.pet.dao.PetDao">
            <property name="mapper" ref="petMapper"/>
        </bean>
    
        <!-- 事务管理器 -->
        <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
            <property name="dataSource" ref="dataSource"/>
        </bean>
    
        <!-- XML+注解混合, 需要有一个 transactionManager 对象, 但不用配置aop, 只需要在需要事务控制的方法上打上 @Transactional -->
        <tx:annotation-driven transaction-manager="transactionManager"/>
    
    </beans>
    
  • PetDao

    package org.demo.pet.dao;
    
    import org.demo.pet.mapper.PetMapper;
    import org.demo.pet.po.Pet;
    import org.springframework.transaction.annotation.Isolation;
    import org.springframework.transaction.annotation.Transactional;
    
    public class PetDao {
    
    	private PetMapper mapper;
    
    	public void setMapper(PetMapper mapper) {
    		this.mapper = mapper;
    	}
    
    	@Transactional(isolation = Isolation.REPEATABLE_READ) // RR隔离级别
    	public void changeMasterName(int petNo, String masterName) {
    		Pet pet = new Pet();
    		pet.setPetNo(petNo);
    		pet.setMasterName(masterName);
    		mapper.changeMasterName(pet);
    		System.out.println(1/0); // 为了抛出异常
    	}
    }
    

测试代码和上面相同,测试结果如下

可见发生了事务回滚,数据库情况不再赘述

纯注解

若实在看XML不顺眼,那么也有纯注解的使用方式,如下

  1. 创建一个properties类,读取并加载db.properties中的数据库配置信息

    package org.demo.pet.properties;
    
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.context.annotation.PropertySource;
    import org.springframework.stereotype.Component;
    
    @Component
    @PropertySource("classpath:db.properties")
    public class DbProperties {
    
    	@Value("${db.url}")
    	private String url;
    
    	@Value("${db.driver}")
    	private String driverClassName;
    
    	@Value("${db.user}")
    	private String user;
    
    	@Value("${db.password}")
    	private String password;
        // 省略get方法
    }
    
    
  2. @Component系列的注解,将PetDao注册到Spring容器,并在方法上打上@Transactional

    package org.demo.pet.dao;
    
    import org.demo.pet.mapper.PetMapper;
    import org.demo.pet.po.Pet;
    import org.springframework.stereotype.Repository;
    import org.springframework.transaction.annotation.Isolation;
    import org.springframework.transaction.annotation.Transactional;
    
    @Repository
    public class PetDao {
    
        @Autowired  // 改用 @Autowired 自动注入
    	private PetMapper mapper;
    
    	public void setMapper(PetMapper mapper) {
    		this.mapper = mapper;
    	}
    
    	@Transactional(isolation = Isolation.REPEATABLE_READ)
    	public void changeMasterName(int petNo, String masterName) {
    		Pet pet = new Pet();
    		pet.setPetNo(petNo);
    		pet.setMasterName(masterName);
    		mapper.changeMasterName(pet);
    		System.out.println(1/0); // 为了抛出异常
    	}
    }
    
  3. 创建Spring配置类

    package org.demo.pet;
    
    
    import com.alibaba.druid.pool.DruidDataSource;
    import org.demo.pet.properties.DbProperties;
    import org.mybatis.spring.SqlSessionFactoryBean;
    import org.mybatis.spring.annotation.MapperScan;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.ComponentScan;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.core.io.ClassPathResource;
    import org.springframework.jdbc.datasource.DataSourceTransactionManager;
    import org.springframework.transaction.annotation.EnableTransactionManagement;
    
    import javax.sql.DataSource;
    
    @Configuration
    @ComponentScan // 扫描当前包
    @MapperScan(basePackages = "org.demo.pet.mapper")  // 扫描mybatis的mapper接口, 生成代理对象
    @EnableTransactionManagement  // 开启事务管理
    public class SpringMybatisConfig {
    
    	/**
    	 * 创建DataSource,形参列表中的 DbProperties会自动注入, 因为DbProperties被@ComponentScan扫描到了Spring容器中
    	 * **/
    	@Bean
    	public DataSource dataSource(DbProperties dbProperties) {
    		DruidDataSource dataSource = new DruidDataSource();
    		dataSource.setUrl(dbProperties.getUrl());
    		dataSource.setUsername(dbProperties.getUser());
    		dataSource.setPassword(dbProperties.getPassword());
    		dataSource.setDriverClassName(dbProperties.getDriverClassName());
    		return dataSource;
    	}
    
    	/**
    	 * mybatis 的 SqlSessionFactory, 形参中的 dataSource 也会自动注入, 因为上面的方法创建了
    	 * **/
    	@Bean
    	public SqlSessionFactoryBean sqlSessionFactoryBean(DataSource dataSource) {
    		SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
    		sqlSessionFactoryBean.setDataSource(dataSource);
            // 这里传入mybatis的xml配置文件进行配置
            // 当然,不想用XML的话,也可以创建 mybatis 中的 Configuration 对象, 在代码中对Configuration对象进行配置,
    		sqlSessionFactoryBean.setConfigLocation(new ClassPathResource("mybatis-config.xml"));
    		return sqlSessionFactoryBean;
    	}
    
    	/**
    	 * 事务管理器 transactionManager
    	 * **/
    	@Bean
    	public DataSourceTransactionManager dataSourceTransactionManager(DataSource dataSource) {
    		DataSourceTransactionManager transactionManager = new DataSourceTransactionManager();
    		transactionManager.setDataSource(dataSource);
    		return transactionManager;
    	}
    }
    
  4. 测试

    这次,我们换一个宠物,先往数据库里插入一条数据

    然后尝试将编号2的宠物的主人改为小李

    package org.demo.test;
    
    import org.demo.pet.SpringMybatisConfig;
    import org.demo.pet.dao.PetDao;
    import org.junit.Test;
    import org.junit.runner.RunWith;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.test.context.ContextConfiguration;
    import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
    
    @RunWith(SpringJUnit4ClassRunner.class)
    @ContextConfiguration(classes = SpringMybatisConfig.class)
    public class SpringMybatisAnnoTest {
    
    	@Autowired
    	private PetDao petDao;
    
    	@Test
    	public void testTransaction() {
    		petDao.changeMasterName(2, "小李");
    	}
    }
    

    测试执行结果如下,可以看到事务成功回滚

    数据库中的2号宠物大橘的主人仍然是大黄

小结

使用Spring的事务控制功能,注意如下几点即可

  1. 最重要的。需要创建一个TransactionManager对象(即spring提供的接口PlatformTransactionManager的实现类对象),它负责事务管理(这个TransactionManager对象又需要一个DataSource数据源对象)。人如其名,Spring正是通过这个对象完成的事务管理功能。

    所以这个对象必须有,无论是XML方式还是注解方式。

  2. 定义哪些方法需要进行事务管理,并对每个方法设置具体的事务配置(事务隔离级别,传播行为,是否只读等)

  3. 对这些方法应用AOP切面

无论是XML方式,还是注解方式,都需要创建一个TransactionManager对象到Spring容器中。

若是纯XML,配置<tx:advice>(对应上面的2)和<aop-config>(对应上面的3)。

若使用@Transactional注解。若采用XML+注解混合使用的方式,XML中,不需要<tx:advice><aop-config>,只需要加上<tx:annotation-driven>标签;若采用纯注解的方式,则要在Spring配置类上加上@EnableTransactionManagement注解

实际,上面的演示中,mybatis和spring的整合占了很大一部分内容。

附录

IoC

Inversion of Control,控制反转。

是面向对象编程的一个重要思想,能够指导我们设计出松散耦合的良好程序。

传统编程中,若我们在一个类A中需要使用另一个类B的方法,通常的做法是通过new关键字等手段,创建一个B的对象。这就导致了类与类之间的高度耦合,使得整个系统变得脆弱,对此简单的理解是:若类B发生了一些变化,很有可能需要修改类A。而当系统中类的规模较大时,类与类之间的依赖关系,将交织形成一张大网,网中任意一个类的修改,将引起连锁反应,使得多个类要进行同步修改,这无疑是一种灾难。

于是,IoC思想应运而生。有了IoC,当A中需要B时,A无需自己去寻找并创建B,而是告诉IoC容器:“我需要B”,IoC容器便会找到一个合适的B对象(有必要的话会进行创建),交给A使用。这样,即使B发生了变化,A也无需关心,A只需要问IoC容器要一个B对象即可。

如果没有IoC,当A想要B时,只能A自己主动去创建;而有了IoCA只需要乖乖坐好等待(被动),IoC容器便会自动把B带过来给他。

这种【把对象的创建和管理的职责,由原先的类,转交给IoC容器】的思想,是为控制反转

当没有IoC时,A需要自己去创建B,还需要管理B,并且B只在A的作用范围内,无法被其他的CDE复用,可谓金屋藏娇了。而有了IoC之后,所有对象的创建和管理都由IoC容器统一负责,大大减轻了类的负担,降低了类之间的耦合。需要什么,只要跟IoC说一声,就能得到,无需每个类自己去承担创建和管理对象的职责。(如果找对象也能这么方便那就好了)。并且,IoC容器就像一个公共资源池,同一个对象,我用完了你还能用,你用完了他还能用,对象变得可复用了,而不必每次使用都重新创建。

经常和IoC同时出现的,还有个兄弟,叫依赖注入(Dependency Injection,或简称DI)。

A中需要用到B,则称A依赖于B,或者BA的依赖。IoC容器找到B,并把B注入到A中,此过程便为依赖注入

IoCDI在本质上是同一个东西,只是从不同的角度进行描述。IoC是从整体思想上的描述,而DI是从具体的操作细节上的描述。

AOP

Aspect Oriented Programming,面向切面编程。

一个系统中,有的【通用功能】需要被应用到各个业务模块当中,但是我们又不想在每个业务模块里明确地调用它们。比如日志,安全,事务管理性能监控。它们都很重要,但不应该由各个业务模块来主动参与它们的行为。

这些【通用功能】,从概念上讲,应该是和【业务逻辑】相分离的,属于通用的【系统逻辑】。

要进行功能的重用,最常见的技术是继承,但是如果使所有负责业务逻辑的类继承一个相同的基类,会导致形成一个脆弱的对象体系。并且继承表达的是一种is a的关系,而这种场景下,【业务逻辑】和【系统逻辑】并不是is a 的关系。

AOP采取横向抽取机制,补充了传统的纵向继承体系无法解决的重复性代码优化的问题。将代码中的【业务逻辑】和【系统逻辑】解耦,提高了程序的维护性和健壮性。

假设有一个电商网站,其内部分为了【订单,物流,支付,用户】这4个模块,若我们想对这4个模块添加【性能监控】的功能。采用纵向继承机制,则图示如下

同一份【性能监控】代码重复的分布在了4个业务模块中,而当【性能监控】的代码需要改动时,则需要同步修改4个模块。这样的纵向继承形式的代码复用,将【系统逻辑】和【业务逻辑】紧紧地耦合在了一起,非常不便于系统的维护和扩展。(虽然可以将【性能监控】的代码写在一个父类中,然后让各个负责业务的类继承它,但是子类中还是需要显式地调用其中的方法以完成性能监控的功能,业务逻辑的代码还是会对系统逻辑的代码有所感知,并且此时的场景不满足is a的关系,不适合采用继承

于是,AOP出现了,它采用横向抽取机制,将【系统逻辑】单独抽取出来,随后通过代码织入(静态织入,动态织入)的方式,对【业务逻辑】进行功能增强。图示如下

各个业务模块对【性能监控】一无所知。【性能监控】只需要编写一份代码,随后通过自动代码织入的方式神不知鬼不觉地将自己的代码逻辑添加到各个业务模块中,实现对业务模块的功能增强。

这样,【业务逻辑】和【系统逻辑】便可以独立开来,极大的降低了系统的耦合性。

bean的其他实例化方式

  • 静态工厂

    通过一个工厂类的静态方法来实例化,示例如下

    package org.litespring.pojo;
    public class StaticFactory {
    	public static PetStoreService createPetStore() {
    		return new PetStoreService();
    	}
    }
    
    <!-- spring的配置文件 -->
    <bean id="petStore2" class="org.litespring.pojo.StaticFactory" factory-method="createPetStore"/>
    <!-- class指定的是工厂类的全限定名, factory-method指定创建对象的静态方法 -->
    
  • 实例工厂

    通过一个工厂类的普通方法(非静态)来实例化,示例如下

    package org.litespring.pojo;
    public class InstanceFactory {
    	public PetStoreService createPetStore() {
    		return new PetStoreService();
    	}
    }
    
    <!-- spring的配置文件 -->
    <bean id="instanceFactory" class="org.litespring.pojo.InstanceFactory"/>
        <bean id="petStore2" factory-bean="instanceFactory" factory-method="createPetStore"/>
    
    <!-- factory-bean : 指定一个工厂对象的 beanId,
    	 factory-method : 指定创建对象的方法
     -->
    

bean的生命周期

由于Spring中提供了诸多接口,可以由开发者自定义地介入Spring容器中的各个过程。比如一个类实现了ApplicationContextAware方法,则会通过setApplicationContext方法将ApplicationContext注入到该bean中。

一个bean的较为完整的生命周期如下(读者可自行验证)
(在Spring源码中的BeanFactory接口的注释中可以找到完整的周期)

  1. 实例化

  2. bean的依赖注入

  3. BeanNameAware

  4. BeanFactoryAware

  5. ApplicationContextAware

  6. BeanPostProcessor的postProcessBeforeInitialization

    注意:若一个bean,实现了BeanPostProcessor接口,并且该bean被纳入spring容器,那么这个bean中的postProcessBeforeInitializationpostProcessAfterInitialization会对所有bean的创建进行拦截,统一进行前置后置处理

  7. @PostConstruct方法

  8. InitializingBean 的afterPropertiesSet方法

  9. <bean>标签中的init-method

  10. BeanPostProcessor的postProcessAfterInitialization

  11. (bean正常存活)

  12. @PreDestroy方法

  13. <bean>标签中的destroy-method

  14. DisposableBean 的destroy方法

在这里插入图片描述

评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值