zl程序教程

您现在的位置是:首页 >  IT要闻

当前栏目

spring学习笔记

2023-03-31 10:45:26 时间

1、Spring概述


简介

Spring 是最流行的企业 Java 应用程序开发框架。 全球数以百万计的开发人员使用 Spring 框架来创建高性能、易于测试和可重用的代码。

Spring 框架是一个开源的 Java 平台。 它最初由 Rod Johnson 编写,并于 2003 年 6 月在 Apache 2.0 许可下首次发布。

在大小和透明度方面,Spring 是轻量级的。 Spring 框架的基本版本约为 2MB。

Spring 框架的核心特性可用于开发任何 Java 应用程序,但也有一些扩展可用于在 Java EE 平台之上构建 Web 应用程序。 Spring 框架旨在通过启用基于 POJO 的编程模型使 J2EE 开发更易于使用并促进良好的编程实践。

使用 Spring 框架的好处

以下是使用 Spring 框架的几个巨大好处的列表

  • Spring 使开发人员能够使用 POJO 开发企业级应用程序。 仅使用 POJO 的好处是您不需要 EJB 容器产品(例如应用程序服务器),但您可以选择仅使用健壮的 servlet 容器(例如 Tomcat 或某些商业产品)。

  • Spring 以模块化方式组织。 即使包和类的数量很多,您也只需要担心您需要的,而忽略其余的。

  • Spring 并没有重新发明,而是真正利用了一些现有的技术,如几个 ORM 框架、日志框架、JEE、Quartz 和 JDK 计时器以及其他视图技术。

  • 测试使用 Spring 编写的应用程序很简单,因为依赖于环境的代码已移至此框架。 此外,通过使用 JavaBeanstyle POJO,更容易使用依赖注入来注入测试数据。

  • Spring 的 web 框架是一个设计良好的 web MVC 框架,它为 Struts 等 web 框架或其他过度设计或不太流行的 web 框架提供了一个很好的替代方案。

  • Spring 提供了一个方便的 API 来将特定于技术的异常(例如,由 JDBC、Hibernate 或 JDO 抛出)转换为一致的、未经检查的异常。

  • 轻量级 IoC 容器往往是轻量级的,例如,与 EJB 容器相比时尤其如此。 这有利于在内存和 CPU 资源有限的计算机上开发和部署应用程序。

  • Spring 提供了一个一致的事务管理接口,可以缩小到本地事务(例如使用单个数据库)并扩展到全局事务(例如使用 JTA)。


依赖注入 (DI)

Spring 最受认同的技术是控制反转的依赖注入 (DI) 风格。 Inversion of Control (IoC) 控制反转是一个通用概念,可以用多种不同的方式表达。 依赖注入只是控制反转的一个具体例子。

在编写复杂的 Java 应用程序时,应用程序类应尽可能独立于其他 Java 类,以增加重用这些类的可能性,并在单元测试时独立于其他类对其进行测试。 依赖注入有助于将这些类粘合在一起,同时保持它们的独立性。

究竟什么是依赖注入? 我们分别来看这两个词。 这里依赖部分转化为两个类之间的关联。 例如,A 类依赖于 B 类。现在,我们来看第二部分,注入。 这意味着,B 类将被 IoC 注入到 A 类中。

依赖注入可以通过将参数传递给构造函数的方式发生,也可以通过使用 setter 方法的构造后发生。 由于依赖注入是 Spring 框架的核心,我们将在单独的章节中用相关示例解释这个概念。


面向方面的编程(AOP)

Spring 的关键组件之一是面向方面编程 (AOP) 框架。 跨越应用程序多个点的功能称为横切关注点,这些横切关注点在概念上与应用程序的业务逻辑分开。 有各种常见的好例子,包括日志记录、声明式事务、安全性、缓存等。

OOP 中模块化的关键单元是类,而 AOP 中模块化的单元是方面。 DI 可帮助您将应用程序对象彼此分离,而 AOP 可帮助您将横切关注点与它们影响的对象分离。

Spring 框架的 AOP 模块提供了面向方面的编程实现,允许您定义方法拦截器和切入点,以清晰地解耦实现应该分离的功能的代码。 我们将在单独的章节中讨论更多关于 Spring AOP 的概念。

2、Spring架构


Core 容器

Core 容器由 Core、Beans、Context 和 Expression Language 模块组成,详细信息如下 −

  • Core 模块提供了框架的基本部分,包括 IoC 和依赖注入功能。

  • Bean 模块提供了 BeanFactory,它是工厂模式的一个复杂的实现。

  • Context 模块建立在 Core 和 Beans 模块提供的坚实基础之上,它是访问任何已定义和配置的对象的媒介。 ApplicationContext 接口是上下文模块的焦点。

  • SpEL 模块提供了一种强大的表达式语言,用于在运行时查询和操作对象图。


数据访问/集成

数据访问/集成层由 JDBC、ORM、OXM、JMS 和 Transaction 事务模块组成,详细信息如下 −

  • JDBC 模块提供了一个 JDBC 抽象层,它消除了繁琐的 JDBC 相关编码的需要。

  • ORM 模块为流行的对象关系映射 API 提供集成层,包括 JPA、JDO、Hibernate 和 iBatis。

  • OXM 模块提供了一个抽象层,它支持 JAXB、Castor、XMLBeans、JiBX 和 XStream 的 Object/XML 映射实现。

  • Java 消息传递服务 JMS 模块包含用于生成和使用消息的功能。

  • Transaction 模块支持对实现特殊接口的类和所有 POJO 的编程和声明式事务管理。


Web

Web 层由 Web、Web-MVC、Web-Socket 和 Web-Portlet 模块组成,具体如下 −

  • Web 模块提供了基本的面向 Web 的集成功能,例如多部分文件上传功能以及使用 servlet 侦听器和面向 Web 的应用程序上下文初始化 IoC 容器。

  • Web-MVC 模块包含 Spring 的 Web 应用程序的 Model-View-Controller(模型-视图-控制器) (MVC) 实现。

  • Web-Socket 模块支持 Web 应用程序中客户端和服务器之间基于 WebSocket 的双向通信。

  • Web-Portlet 模块提供了在portlet 环境中使用的MVC 实现,并反映了Web-Servlet 模块的功能。


杂项

其他重要的模块很少,如 AOP、Aspects、Instrumentation、Web 和 Test 模块,具体如下 −

  • AOP 模块提供了一个面向方面的编程实现,允许您定义方法拦截器和切入点,以清晰地解耦实现应该分离的功能的代码。

  • Aspects 模块提供与 AspectJ 的集成,AspectJ 又是一个强大且成熟的 AOP 框架。

  • Instrumentation 模块提供类检测支持和类加载器实现以用于某些应用程序服务器。

  • Messaging 模块支持将 STOMP 作为 WebSocket 子协议在应用程序中使用。 它还支持注解编程模型,用于路由和处理来自 WebSocket 客户端的 STOMP 消息。

  • Test 模块支持使用 JUnit 或 TestNG 框架对 Spring 组件进行测试。


3、Spring快速入门

user类

@Data  
@AllArgsConstructor  
@NoArgsConstructor  
public class User {  
    private String name;  
    private int age;  
}

Spring配置文件

<?xml version="1.0" encoding="UTF-8"?>  
<beans xmlns="http://www.springframework.org/schema/beans"  
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"  
       xsi:schemaLocation="http://www.springframework.org/schema/beans  
        https://www.springframework.org/schema/beans/spring-beans.xsd"> 
         
    <bean id="user" class="com.zh.pojo.User">  
        <property name="age" value="18"/>  
        <property name="name" value="张三"/>  
    </bean>
</beans>

测试

public class Test01 {  
    public static void main(String[] args) {  
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("beans.xml");  
  
        User user = applicationContext.getBean("user", User.class);  
  
        System.out.println(user);  
    }  
}

4、IOC

构成应用程序主干并由 Spring IoC 容器管理的对象称为 beans。 bean 是由 Spring IoC 容器实例化、组装和管理的对象。 这些 bean 使用您提供给容器的配置元数据创建。 例如,以 XML 的形式。 前面章节中已经看到的定义。

Bean定义中包含了称为配置元数据的信息,容器需要知道以下信息 −

  • 如何创建一个 bean
  • Bean 的生命周期详情
  • Bean 的依赖关系

上述所有配置元数据都转换为一组构成每个 bean 定义的以下属性。

属性 & 描述

1 class

该属性是必需的,它指定了用于创建 bean 的 bean 类。

2 id

此属性唯一地指定 bean 标识符。 在基于 XML 的配置元数据中,您使用 id 或者 name 属性来指定 bean 标识符。

3 scope

此属性指定从特定 bean 定义创建的对象的范围,将在 bean 范围一章中讨论。

4 constructor-arg

这用于注入依赖关系,将在后续章节中讨论。

5 properties

这用于注入依赖关系,将在后续章节中讨论。

6 autowiring mode

这用于注入依赖关系,将在后续章节中讨论。

7 lazy-initialization mode

延迟初始化的 bean 告诉 IoC 容器在第一次被请求时创建一个 bean 实例,而不是在启动时。

8 initialization method

在容器设置了 bean 上的所有必要属性之后调用的回调。 它将在 bean 生命周期章节中讨论。

9 destruction method

当包含 bean 的容器被销毁时要使用的回调。 它将在 bean 生命周期章节中讨论。


5、Spring - Bean

序号

作用域 & 描述

1 singleton

这将 bean 定义范围限定为每个 Spring IoC 容器的单个实例(默认)。

2 prototype

这将单个 bean 定义的范围限定为具有任意数量的对象实例。

3 request

这将 bean 定义的范围限定为 HTTP 请求。 仅在 Web 感知 Spring ApplicationContext 的上下文中有效。

4 session

这将 bean 定义范围限定为 HTTP 会话。 仅在 Web 感知 Spring ApplicationContext 的上下文中有效。

5 global-session

这将 bean 定义范围限定为全局 HTTP 会话。 仅在 Web 感知 Spring ApplicationContext 的上下文中有效。

singleton 作用域

如果作用域设置为 singleton,则 Spring IoC 容器会创建该 bean 定义所定义的对象的一个实例。 此单个实例存储在此类单例 bean 的缓存中,并且该命名 bean 的所有后续请求和引用都返回缓存的对象。单例在容器初始化的时候创建

<bean id = "..." class = "..." scope = "singleton">

</bean

prototype 作用域

如果作用域设置为prototype,则每次对特定 bean 发出请求时,Spring IoC 容器都会创建对象的新 bean 实例。 通常,对所有状态完整的 bean 使用 prototype 作用域,对无状态 bean 使用 singleton 作用域。在取对象的时候创建

<bean id = "..." class = "..." scope = "prototype">
   
</bean>

bean的初始化回调和销毁回调

在基于 XML 的配置元数据的情况下,您可以使用 init-method 和destroy-method属性来指定具有 void 无参数签名的方法的名称。

另外还有实现接口的方式

public class User {  
    private String name;  
    private int age;  
  
    public void init(){  
        System.out.println("创建了一个user对象");  
    }  
}
<bean id="user" class="com.zh.pojo.User" init-method="init">  
    <property name="age" value="18"/>  
    <property name="name" value="张三"/>  
</bean>

默认初始化和销毁方法

如果您有太多具有相同名称的初始化和/或销毁方法的 bean,则无需在每个 bean 上声明 init-method 和 destroy-method . 相反,框架提供了使用beans 元素上的 default-init-method 和 default-destroy-method 属性配置这种情况的灵活性,如下所示

<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-3.0.xsd"
   default-init-method = "init" 
   default-destroy-method = "destroy">

   <bean id = "..." class = "...">
      <!-- collaborators and configuration for this bean go here -->
   </bean>
   
</beans>

bean的继承


一个 bean 定义可以包含很多配置信息,包括构造函数参数、属性值以及容器特定的信息,例如初始化方法、static factory 方法名称等。

子 bean 定义继承父定义的配置数据。 子定义可以根据需要覆盖某些值或添加其他值。

Spring Bean 定义继承与 Java 类继承无关,但继承概念是相同的。 您可以将父 bean 定义定义为模板,其他子 bean 可以从父 bean 继承所需的配置。

当您使用基于 XML 的配置元数据时,您可以通过使用 parent 属性来指示子 bean 定义,并将父 bean 指定为该属性的值

例如:

<?xml version = "1.0" encoding = "UTF-8"?>

<beans xmlns = "http://www.springframework.org/schema/beans"
   xmlns:xsi = "http://www.w3.org/2001/XMLSchema-instance"
   xsi:schemaLocation = "http://www.springframework.org/schema/beans
   http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">

   <bean id = "helloWorld" class = "com.tutorialspoint.HelloWorld">
      <property name = "message1" value = "Hello World!"/>
      <property name = "message2" value = "Hello Second World!"/>
   </bean>

   <bean id ="helloIndia" class = "com.tutorialspoint.HelloIndia" parent = "helloWorld">
      <property name = "message1" value = "Hello India!"/>
      <property name = "message3" value = "Namaste India!"/>
   </bean>
</beans>
public class MainApp {
   public static void main(String[] args) {
      ApplicationContext context = new ClassPathXmlApplicationContext("Beans.xml");
      
      HelloWorld objA = (HelloWorld) context.getBean("helloWorld");
      objA.getMessage1();
      objA.getMessage2();

      HelloIndia objB = (HelloIndia) context.getBean("helloIndia");
      objB.getMessage1();
      objB.getMessage2();
      objB.getMessage3();
   }
}
World Message1 : Hello World!
World Message2 : Hello Second World!
India Message1 : Hello India!
India Message2 : Hello Second World!
India Message3 : Namaste India!

如果您在此处观察到,我们在创建"helloIndia"bean 时没有传递 message2,但由于 Bean 定义继承,它通过了

Bean 定义模板

您可以创建一个 Bean 定义模板,它可以被其他子 bean 定义使用,而无需花费太多精力。 在定义 Bean 定义模板时,不应指定 class 属性,而应指定 abstract 属性,并应指定值为 true 的 abstract 抽象属性,如以下代码片段所示

<?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-3.0.xsd">

   <bean id = "beanTeamplate" abstract = "true">
      <property name = "message1" value = "Hello World!"/>
      <property name = "message2" value = "Hello Second World!"/>
      <property name = "message3" value = "Namaste India!"/>
   </bean>

   <bean id = "helloIndia" class = "com.tutorialspoint.HelloIndia" parent = "beanTeamplate">
      <property name = "message1" value = "Hello India!"/>
      <property name = "message3" value = "Namaste India!"/>
   </bean>
   
</beans>

6、Spring依赖注入

构造器注入

<?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-3.0.xsd">

   <!-- Definition for textEditor bean -->
   <bean id = "textEditor" class = "com.tutorialspoint.TextEditor">
      <constructor-arg ref = "spellChecker"/>
   </bean>

   <!-- Definition for spellChecker bean -->
   <bean id = "spellChecker" class = "com.tutorialspoint.SpellChecker"></bean>

</beans>
<beans>

   <bean id = "exampleBean" class = "examples.ExampleBean">
      <constructor-arg type = "int" value = "2001"/>
      <constructor-arg type = "java.lang.String" value = "Zara"/>
   </bean>

</beans>
<beans>

   <bean id = "exampleBean" class = "examples.ExampleBean">
      <constructor-arg index = "0" value = "2001"/>
      <constructor-arg index = "1" value = "Zara"/>
   </bean>

</beans>

内部注入beans

<?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-3.0.xsd">

   <bean id = "outerBean" class = "...">
      <property name = "target">
         <bean id = "innerBean" class = "..."/>
      </property>
   </bean>

</beans>

注入集合

<?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-3.0.xsd">

   <!-- Definition for javaCollection -->
   <bean id = "javaCollection" class = "com.tutorialspoint.JavaCollection">
      
      <!-- results in a setAddressList(java.util.List) call -->
      <property name = "addressList">
         <list>
            <value>INDIA</value>
            <value>Pakistan</value>
            <value>USA</value>
            <value>USA</value>
         </list>
      </property>

      <!-- results in a setAddressSet(java.util.Set) call -->
      <property name = "addressSet">
         <set>
            <value>INDIA</value>
            <value>Pakistan</value>
            <value>USA</value>
            <value>USA</value>
         </set>
      </property>

      <!-- results in a setAddressMap(java.util.Map) call -->
      <property name = "addressMap">
         <map>
            <entry key = "1" value = "INDIA"/>
            <entry key = "2" value = "Pakistan"/>
            <entry key = "3" value = "USA"/>
            <entry key = "4" value = "USA"/>
         </map>
      </property>
      
      <!-- results in a setAddressProp(java.util.Properties) call -->
      <property name = "addressProp">
         <props>
            <prop key = "one">INDIA</prop>
            <prop key = "one">INDIA</prop>
            <prop key = "two">Pakistan</prop>
            <prop key = "three">USA</prop>
            <prop key = "four">USA</prop>
         </props>
      </property>
   </bean>

</beans>

注入 Bean 引用

<?xml version = "1.0" encoding = "UTF-8"?>

<beans xmlns = "http://www.springframework.org/schema/beans"
   xmlns:xsi = "http://www.w3.org/2001/XMLSchema-instance"
   xsi:schemaLocation = "http://www.springframework.org/schema/beans
   http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">

   <!-- Bean Definition to handle references and values -->
   <bean id = "..." class = "...">

      <!-- Passing bean reference  for java.util.List -->
      <property name = "addressList">
         <list>
            <ref bean = "address1"/>
            <ref bean = "address2"/>
            <value>Pakistan</value>
         </list>
      </property>
      
      <!-- Passing bean reference  for java.util.Set -->
      <property name = "addressSet">
         <set>
            <ref bean = "address1"/>
            <ref bean = "address2"/>
            <value>Pakistan</value>
         </set>
      </property>
      
      <!-- Passing bean reference  for java.util.Map -->
      <property name = "addressMap">
         <map>
            <entry key = "one" value = "INDIA"/>
            <entry key = "two" value-ref = "address1"/>
            <entry key = "three" value-ref = "address2"/>
         </map>
      </property>
   </bean>

</beans>

注入 null 和空字符串值

如果你需要传递一个NULL值,那么你可以按如下方式传递它

<bean id = "..." class = "exampleBean">
   <property name = "email"><null/></property>
</bean>

如果你需要传递一个空字符串作为一个值,那么你可以按如下方式传递它

<bean id = "..." class = "exampleBean">
   <property name = "email" value = ""/>
</bean>

7、Spring - Beans 自动装配

自动装配模式

以下是自动装配模式,可用于指示 Spring 容器使用自动装配进行依赖注入。 您可以使用 元素的 autowire 属性来为 bean 定义指定 autowire 模式。

模式 & 描述

1 no

这是默认设置,这意味着没有自动装配,您应该使用显式 bean 引用进行装配。 您无需为此连接做任何特别的事情。 这是你在依赖注入一章中已经看到的。

2 byName

按属性名称自动装配。 Spring 容器查看在 XML 配置文件中 autowire 属性设置为 byName 的 bean 的属性。 然后,它会尝试将其属性与配置文件中由相同名称定义的 bean 进行匹配和连接。

3 byType

按属性数据类型自动装配。 Spring 容器查看在 XML 配置文件中 autowire 属性设置为 byType 的 bean 的属性。 然后,如果它的 type 与配置文件中的一个 beans 名称完全匹配,它会尝试匹配和连接一个属性。 如果存在多个此类 bean,则会引发致命错误。

4 constructor

与 byType 类似,但类型适用于构造函数参数。 如果容器中没有一个构造函数参数类型的 bean,则会引发致命错误。

5 autodetect

Spring 首先尝试使用 constructor 的自动装配,如果它不起作用,Spring 会尝试通过 byType 自动装配。

您可以使用 byType 或 constructor 自动装配模式来连接数组和其他类型化的集合。

Spring 自动装配 byName

此模式通过属性名称指定自动装配。 Spring 容器查看 XML 配置文件中 auto-wire 属性设置为 byName 的 bean。 然后,它会尝试将其属性与配置文件中由相同名称定义的 bean 进行匹配和连接。 如果找到匹配项,它将注入这些 bean。 否则,bean 将不会被连接。

例如,如果一个 bean 定义在配置文件中设置为自动装配 byName,并且它包含一个 spellChecker 属性(即,它有一个 setSpellChecker(...) 方法),Spring 查找名为 spellChecker 的 bean 定义,并使用它来设置属性。 您仍然可以使用 property 标签连接其余属性。 下面的例子将说明这个概念。

public class Pet {  
    private String Type;  
    private String name;  
}

public class Person {  
    private String name;  
    private int age;  
    private Pet pet;  
}
<?xml version="1.0" encoding="UTF-8"?>  
<beans xmlns="http://www.springframework.org/schema/beans"  
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"  
       xsi:schemaLocation="http://www.springframework.org/schema/beans  
        https://www.springframework.org/schema/beans/spring-beans.xsd">  
  
    <bean id="pet" class="com.zh.pojo.Pet">  
        <property name="type" value="猫"/>  
        <property name="name" value="小花"/>  
    </bean>    
    <bean id="person" class="com.zh.pojo.Person" autowire="byName">  
        <property name="name" value="张三"/>  
        <property name="age" value="18"/>  
    </bean>
</beans>
public class Test01 {  
    public static void main(String[] args) {  
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("beans.xml");  
  
        Person person = applicationContext.getBean("person", Person.class);  
  
        System.out.println(person);  
    }  
}


Person(name=张三, age=18, pet=Pet(Type=猫, name=小花))

在bean配置中没有设置person的pet属性,容器自动装配了

注意:byname是根据属性名和bean的id来识别的,如果属性名和bean的id不符,则自动装配失效

<bean id="pet1" class="com.zh.pojo.Pet">  
    <property name="type" value="猫"/>  
    <property name="name" value="小花"/>  
</bean>  
<bean id="person" class="com.zh.pojo.Person" autowire="byName">  
    <property name="name" value="张三"/>  
    <property name="age" value="18"/>  
</bean>

Person(name=张三, age=18, pet=null)

Spring 自动装配 byType

此模式指定按属性类型自动装配。 Spring 容器查看 XML 配置文件中 autowire 属性设置为 byType 的 bean。 然后,如果它的 type 与配置文件中的一个 beans 名称完全匹配,它会尝试匹配和连接一个属性。 如果找到匹配项,它将注入这些 bean。 否则,bean 将不会被连接。
上述例子中,改变bean的id之后,自动装配失效,这时可以通过buType来实现自动装配

<bean id="pet1" class="com.zh.pojo.Pet">  
    <property name="type" value="猫"/>  
    <property name="name" value="小花"/>  
</bean>  
<bean id="person" class="com.zh.pojo.Person" autowire="byType">  
    <property name="name" value="张三"/>  
    <property name="age" value="18"/>  
</bean>


Person(name=张三, age=18, pet=Pet(Type=猫, name=小花))

byType自动装配有两个相同的类型的bean时,编译器会报错,因为此时容器无法确定需要哪个bean ----如果存在多个此类 bean,则会引发致命错误

自动装配的限制
您仍然可以使用始终覆盖自动装配的 constructor-arg 和 property 设置来指定依赖项。
您不能自动装配所谓的简单属性,例如原始数据、字符串和类。

8、常用注解

要使用注解,必须先在配置文件中引入约束并开启

注解注入在 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-3.0.xsd
   http://www.springframework.org/schema/context
   http://www.springframework.org/schema/context/spring-context-3.0.xsd">

   <context:annotation-config/>
   <!-- bean definitions go here -->

</beans>

1、注册bean的相关注解

  • @Component :标准一个普通的spring Bean类。
    • @Repository:标注一个DAO组件类。
    • @Service:标注一个业务逻辑组件类。
    • @Controller:标注一个控制器组件类。

@Component可以代替@Repository、@Service、@Controller,因为这三个注解是被@Component标注的。

注册bean注解需要导入约束

<?xml version="1.0" encoding="UTF-8"?>  
<beans xmlns="http://www.springframework.org/schema/beans"  
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"  
       xmlns:context = "http://www.springframework.org/schema/context"  
       xsi:schemaLocation="http://www.springframework.org/schema/beans  
        https://www.springframework.org/schema/beans/spring-beans.xsd        http://www.springframework.org/schema/context        http://www.springframework.org/schema/context/spring-context-3.0.xsd">  
    <context:annotation-config/>  <!--开启注解不是必须的-->
    <context:component-scan base-package="com.zh.pojo"/>  
</beans>
@Data  
@NoArgsConstructor  
@AllArgsConstructor  
@Component  
public class Person {  
    @Value("张三")  
    private String name;  
    @Value("18")  
    private int age;  
    @Autowired  
    @Qualifier("pet")  
    private Pet pet;  
}

@Data  
@NoArgsConstructor  
@AllArgsConstructor  
@Component  
public class Pet {  
    @Value("猫")  
    private String Type;  
    @Value("大白")  
    private String name;  
}

2、自动装载相关注解

@Autowired:属于Spring 的org.springframework.beans.factory.annotation包下,可用于为类的属性、构造器、方法进行注值

required属性:默认为不为null,改为false可为null

Spring @Autowired 注解

@Autowired 注解提供了对自动装配的位置和方式进行更细粒度的控制。 @Autowired 注解可用于在 setter 方法上自动装配 bean,就像 @Required 注解、构造函数、具有任意名称和/或多个参数的属性或方法一样。

@Autowired 在 Setter 方法上

您可以在 setter 方法上使用 @Autowired 注解来摆脱 XML 配置文件中的 property 元素。 当 Spring 发现与 setter 方法一起使用的 @Autowired 注解时,它会尝试对方法执行 byType 自动装配。

@Autowired 属性

您可以在属性上使用 @Autowired 注解来摆脱 setter 方法。 当您使用 a1 传递自动装配属性的值时,Spring 将自动为这些属性分配传递的值或引用。

@Autowired 在构造函数上

您也可以将 @Autowired 应用于构造函数。 构造函数 @Autowired 注解指示在创建 bean 时应该自动装配构造函数,即使在 XML 文件中配置 bean 时没有使用 constructor-arg元素。 让我们检查以下示例。

@Autowired with (required = false) 选项

默认情况下,@Autowired 注解暗示依赖项是必需的,类似于 @Required 注解,但是,您可以通过使用带有 @Autowired 的 (required=false) 选项来关闭默认行为。

public class Person {  
    private String name;  
    private int age;  
    @Autowired  
    private Pet pet;  
}
<bean id="pet1" class="com.zh.pojo.Pet">  
    <property name="type" value="狗"/>  
    <property name="name" value="小花1"/>  
</bean>  
<bean id="person" class="com.zh.pojo.Person">  
    <property name="name" value="张三"/>  
    <property name="age" value="20"/>  
</bean>


Person(name=张三, age=20, pet=Pet(Type=狗, name=小花1))

**@Autowired 默认是byType,如果存在多个相同类型的bean,则报错

Spring @Qualifier 注解

当您创建多个相同类型的 bean 并希望仅将其中一个与属性关联时,可能会出现这种情况。 在这种情况下,您可以将 @Qualifier 注解与 @Autowired 一起使用,通过指定要连接的确切 bean 来消除混淆。

上述例子中,在添加一个bean,则会报错

<bean id="pet1" class="com.zh.pojo.Pet">  
    <property name="type" value="狗"/>  
    <property name="name" value="小花1"/>  
</bean>  
<bean id="pet2" class="com.zh.pojo.Pet">  
    <property name="type" value="狗"/>  
    <property name="name" value="小花2"/>  
</bean>  
<bean id="person" class="com.zh.pojo.Person">  
    <property name="name" value="张三"/>  
    <property name="age" value="20"/>  
</bean>
public class Person {  
    private String name;  
    private int age;  
    @Autowired  
    private Pet pet;  
}

这时可以使用@Qualifier注解来区分

@Data  
@NoArgsConstructor  
@AllArgsConstructor  
public class Person {  
    private String name;  
    private int age;  
    @Autowired  
    @Qualifier("pet1")  
    private Pet pet;  
}


Person(name=张三, age=20, pet=Pet(Type=狗, name=小花1))

Spring @Resource注解

不属于spring的注解,而是来自于JSR-250位于java.annotation包下,使用该annotation为目标bean指定协作者Bean。

@Target({TYPE, FIELD, METHOD})
@Retention(RUNTIME)
public @interface Resource {
 String name() default "";
 Class type() default java.lang.Object.class;

}


public class AnotationExp {

    @Resource(name = "HappyClient")
    private HappyClient happyClient;
    
    @Resource(type = HappyPlayAno .class)
    private HappyPlayAno happyPlayAno;
}

@Resource和@Autowired的对比

相同点 @Resource的作用相当于@Autowired,均可标注在字段或属性的setter方法上。

不同点

  1. 提供方 @Autowired是Spring的注解,@Resource是javax.annotation注解,而是来自于JSR-250,J2EE提供,需要JDK1.6及以上。

  2. 注入方式 @Autowired只按照Type 注入;@Resource默认按Name自动注入,也提供按照Type 注入;

  3. 属性

@Autowired注解可用于为类的属性、构造器、方法进行注值。默认情况下,其依赖的对象必须存在(bean可用),如果需要改变这种默认方式,可以设置其required属性为false。

还有一个比较重要的点就是,@Autowired注解默认按照类型装配,如果容器中包含多个同一类型的Bean,那么启动容器时会报找不到指定类型bean的异常,解决办法是结合@Qualifier注解进行限定,指定注入的bean名称。

@Resource有两个中重要的属性:name和type。name属性指定byName,如果没有指定name属性,当注解标注在字段上,即默认取字段的名称作为bean名称寻找依赖对象,当注解标注在属性的setter方法上,即默认取属性名作为bean名称寻找依赖对象。

需要注意的是,@Resource如果没有指定name属性,并且按照默认的名称仍然找不到依赖对象时, @Resource注解会回退到按类型装配。但一旦指定了name属性,就只能按名称装配了。

  1. @Resource注解的使用性更为灵活,可指定名称,也可以指定类型 ;@Autowired注解进行装配容易抛出异常,特别是装配的bean类型有多个的时候,而解决的办法是需要在增加@Qualifier进行限定。

Spring @Required 注解

@Required 注解适用于 bean 属性设置器方法,它指示受影响的 bean 属性必须在配置时填充到 XML 配置文件中。 否则,容器会抛出 BeanInitializationException 异常。 下面是一个例子来展示@Required 注解的使用。

public class Student {
   private Integer age;
   private String name;

   @Required
   public void setAge(Integer age) {
      this.age = age;
   }
   public Integer getAge() {
      return age;
   }
   
   @Required
   public void setName(String name) {
      this.name = name;
   }
   public String getName() {
      return name;
   }
}

@Required 注解的属性,必须在xml中注入,次注解以过时

9、Spring - 基于 Java 的配置

@Data  
@NoArgsConstructor  
@AllArgsConstructor  
public class Pet {  
    private String Type;  
    private String name;  
}

@Data  
@NoArgsConstructor  
@AllArgsConstructor  
public class Person {  
    private String name;  
    private int age;  
    @Autowired  
    private Pet pet;  
}


@Configuration  
public class Config {  
    @Bean  
    public Pet pet(){  
        return new Pet("猫","小花");  
    }  
}


public class Test01 {  
    public static void main(String[] args) {  
        ApplicationContext applicationContext = new AnnotationConfigApplicationContext(Config.class);  
  
        Pet pet = applicationContext.getBean("pet", Pet.class);  
  
        System.out.println(pet);  
    }  
}


Pet(Type=猫, name=小花)

@Configuration & @Bean 注解

@Configuration 注解一个类表明该类可以被 Spring IoC 容器用作 bean 定义的来源。 
@Bean 注解告诉 Spring,使用 @Bean 注解的方法将返回一个对象,该对象应在 Spring 应用程序上下文中注册为 bean。

注入 Bean 依赖项

@Configuration  
public class Config {  
  
    @Bean  
    public Person person(){  
        return new Person("张三",18,pet());  
    }  
    @Bean  
    public Pet pet(){  
        return new Pet("猫","小花");  
    }  
}

就是调用需要的依赖方法

指定 Bean 作用域

@Configuration
public class AppConfig {
   @Bean
   @Scope("prototype")
   public Foo foo() {
      return new Foo();
   }
}

生命周期回调

@Bean 注解支持指定任意初始化和销毁回调方法,很像 Spring XML 的 bean 元素上的 init-method 和 destroy-method 属性 −

public class Foo {
   public void init() {
      // initialization logic
   }
   public void cleanup() {
      // destruction logic
   }
}
@Configuration
public class AppConfig {
   @Bean(initMethod = "init", destroyMethod = "cleanup" )
   public Foo foo() {
      return new Foo();
   }
}

@Import 注解

@Configuration  
@Import(ConfigB.class)  
public class ConfigA {  
    @Bean  
    public Person person(){  
        return new Person("张三",19,null);  
    }  
}

@Configuration  
public class ConfigB {  
    @Bean  
    public Pet pet(){  
        return new Pet("狗","小花");  
    }  
}


public class Test01 {  
    public static void main(String[] args) {  
        ApplicationContext applicationContext = new AnnotationConfigApplicationContext(ConfigA.class);  
  
        Person person = applicationContext.getBean("person", Person.class);  
        Pet pet = applicationContext.getBean("pet", Pet.class);  
  
        System.out.println(person);  
        System.out.println(pet);  
    }  
}


Person(name=张三, age=19, pet=Pet(Type=狗, name=小花))
Pet(Type=狗, name=小花)

10、Spring AOP

1、概述

Spring Framework 的关键组件之一是面向方面编程 (AOP) 框架。 面向方面的编程需要将程序逻辑分解成不同的部分,称为所谓的关注点。跨越应用程序多个点的功能称为横切关注点。 这些横切关注点在概念上与应用程序的业务逻辑是分开的。有各种常见的良好示例,例如日志记录、审计、声明性事务、安全性、缓存等。

OOP 中模块化的关键单元是类,而 AOP 中模块化的单元是方面。 依赖注入可帮助您将应用程序对象彼此分离,而 AOP 可帮助您将横切关注点与它们影响的对象分离。 AOP 就像 Perl、.NET、Java 等编程语言中的触发器。

Spring AOP 模块让拦截器拦截应用程序。 例如,在执行方法时,您可以在方法执行之前或之后添加额外的功能。

2、Spring AOP 核心概念

1 Aspect

具有一组提供横切需求的 API 的模块。 例如,日志记录模块将被称为 AOP 方面的日志记录。 根据要求,应用程序可以具有任意数量的方面。

2 Join point

这代表了您的应用程序中的一个点,您可以在其中插入 AOP 方面。 您也可以说,它是应用程序中使用 Spring AOP 框架执行操作的实际位置。

3 Advice

这是在方法执行之前或之后要采取的实际操作。 这是 Spring AOP 框架在程序执行期间调用的实际代码。

4 PointCut

这是应该执行建议的一组一个或多个连接点。 正如我们将在 AOP 示例中看到的那样,您可以使用表达式或模式指定切入点。

5 Introduction

介绍允许您向现有类添加新方法或属性。

6 Target object

一个或多个方面所建议的对象。 该对象将始终是代理对象。 也称为建议对象。

7 Weaving

Weaving 是将方面与其他应用程序类型或对象链接以创建建议对象的过程。 这可以在编译时、加载时或运行时完成。

Spring 方面可以使用以下中提到的五种建议。

1 before

在方法执行之前运行建议。

2 after

在方法执行后运行通知,无论其结果如何。

3 after-returning

方法执行后运行通知,仅当方法成功完成时。

4 after-throwing

在方法执行后运行通知,仅当方法通过抛出异常退出时。

5 around

在调用建议方法之前和之后运行建议

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

</beans>`

需要导入依赖

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

基于@AspectJ

@AspectJ 指的是一种将方面声明为使用 Java 5 注解进行注解的常规 Java 类的风格。 通过在基于 XML 模式的配置文件中包含以下元素来启用 @AspectJ 支持。

<aop:aspectj-autoproxy/>

3、Spring AOP 实现

基于 XML 架构

声明 Aspect

aspect 使用 元素声明,支持 bean 使用 ref 属性引用,如下所示

<aop:config>
   <aop:aspect id = "myAspect" ref = "aBean">
   ...
   </aop:aspect>
</aop:config>

<bean id = "aBean" class = "...">
   ...
</bean>

声明 Pointcut

Pointcut 有助于确定要使用不同建议执行的感兴趣的连接点(即方法)。 在使用基于 XML Schema 的配置时,Pointcut 将定义如下

<aop:config>
   <aop:aspect id = "myAspect" ref = "aBean">

   <aop:pointcut id = "businessService"
      expression = "execution(* com.xyz.myapp.service.*.*(..))"/>
      ...
   </aop:aspect>
</aop:config>

<bean id = "aBean" class = "...">
   ...
</bean>

声明 Advices

您可以使用 <aop:{ADVICE NAME}> 元素在 aop:aspect 中声明五个建议中的任何一个,如下所示。

<aop:config>
   <aop:aspect id = "myAspect" ref = "aBean">
      <aop:pointcut id = "businessService"
         expression = "execution(* com.xyz.myapp.service.*.*(..))"/>

      <!-- a before advice definition -->
      <aop:before pointcut-ref = "businessService" 
         method = "doRequiredTask"/>

      <!-- an after advice definition -->
      <aop:after pointcut-ref = "businessService" 
         method = "doRequiredTask"/>

      <!-- an after-returning advice definition -->
      <!--The doRequiredTask method must have parameter named retVal -->
      <aop:after-returning pointcut-ref = "businessService"
         returning = "retVal"
         method = "doRequiredTask"/>

      <!-- an after-throwing advice definition -->
      <!--The doRequiredTask method must have parameter named ex -->
      <aop:after-throwing pointcut-ref = "businessService"
        throwing = "ex"
         method = "doRequiredTask"/>

      <!-- an around advice definition -->
      <aop:around pointcut-ref = "businessService" 
         method = "doRequiredTask"/>
   ...
   </aop:aspect>
</aop:config>

<bean id = "aBean" class = "...">
   ...
</bean>

基于@AspectJ

@AspectJ 指的是一种将方面声明为使用 Java 5 注解进行注解的常规 Java 类的风格。 通过在基于 XML 模式的配置文件中包含以下元素来启用 @AspectJ 支持。

<aop:aspectj-autoproxy/>

声明 Aspect

Aspects 类与任何其他普通 bean 一样,并且可以像任何其他类一样具有方法和字段,除了它们将使用 @Aspect 注解如下。

package org.xyz;

import org.aspectj.lang.annotation.Aspect;

@Aspect
public class AspectModule {
}

声明 Pointcut

Pointcut 有助于确定要使用不同建议执行的感兴趣的连接点(即方法)。 在使用基于 @AspectJ 的配置时,Pointcut 声明有两个部分 −

  • 一个切入点表达式,它准确地确定我们感兴趣的方法执行。

  • 一个切入点签名,包括名称和任意数量的参数。 该方法的实际主体是无关紧要的,实际上应该是空的。

以下示例定义了一个名为 "businessService" 的切入点,它将匹配 com.xyz.myapp.service 包下的类中可用的每个方法的执行。

import org.aspectj.lang.annotation.Pointcut;

@Pointcut("execution(* com.xyz.myapp.service.*.*(..))") // expression 
private void businessService() {}  // signature

声明 Advices

@Before("execution(* *.*(..))")
@After("execution(* *.*(..))")

4、实现AOP的三种方式-快速入门

  • 实现接口实现

public class Before implements MethodBeforeAdvice {  
    @Override  
    public void before(Method method, Object[] args, Object target) throws Throwable {  
        System.out.println(target.getClass().getSimpleName()+"的"+method.getName()+"方法执行了");  
    }  
}


public class After implements AfterReturningAdvice {  
    @Override  
    public void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable {  
        System.out.println(target.getClass().getSimpleName()+"的"+method.getName()+"方法执行结束了");  
    }  
}

配置AOP

<bean id="userDao" class="com.zh.pojo.UserDao"/>  
<bean id="after" class="com.zh.log.After"/>  
<bean id="before" class="com.zh.log.Before"/>  
<aop:aspectj-autoproxy/>  
<aop:config>  
    <aop:pointcut id="pointcut" expression="execution(* *.*(..))"/>  
    <aop:advisor advice-ref="after" pointcut-ref="pointcut"/>  
    <aop:advisor advice-ref="before" pointcut-ref="pointcut"/>  
</aop:config>

运行结果:

public class Test01 {  
    public static void main(String[] args) {  
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");  
        Dao dao = (Dao) applicationContext.getBean("userDao");  
  
        dao.select();  
  
        dao.del();  
    }  
}

com.zh.pojo.UserDao的select方法执行了
查询用户
com.zh.pojo.UserDao的select方法执行结束了
com.zh.pojo.UserDao的del方法执行了
删除了一个用户
com.zh.pojo.UserDao的del方法执行结束了
  • 自定义切面实现
public class Log {  
    private SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");  
    public void before(){  
        System.out.println("方法执行前:"+simpleDateFormat.format(new Date(System.currentTimeMillis())));  
    }  
    public void after(){  
        System.out.println("方法执行后:"+simpleDateFormat.format(new Date(System.currentTimeMillis())));  
    }  
}

配置AOP

<bean id="log" class="com.zh.log.Log"/>  
<bean id="userDao" class="com.zh.pojo.UserDao"/>  
<aop:aspectj-autoproxy/>  
<aop:config>  
    <aop:aspect id="log" ref="log">  
        <!-- 在切面外面定义的切点,可以给所有切面引用 -->  
        <aop:pointcut id="pointcut" expression="execution(* *.*(..))"/>  
        <aop:before method="before" pointcut-ref="pointcut"/>  
        <aop:after method="after" pointcut-ref="pointcut"/>  
    </aop:aspect>  
</aop:config>

运行结果:

public class Log {  
    private SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss SSS");  
    public void before(){  
        System.out.println("方法执行前:"+simpleDateFormat.format(new Date(System.currentTimeMillis())));  
    }  
    public void after(){  
        System.out.println("方法执行后:"+simpleDateFormat.format(new Date(System.currentTimeMillis())));  
    }  
}

方法执行前:2022-10-12 19:57:10 421
查询用户
方法执行后:2022-10-12 19:57:10 422
方法执行前:2022-10-12 19:57:10 423
删除了一个用户
方法执行后:2022-10-12 19:57:10 423
  • 注解实现
<bean id="log" class="com.zh.log.Log"/>  
<bean id="userDao" class="com.zh.pojo.UserDao"/>  
<aop:aspectj-autoproxy/>
@Aspect  
public class Log {  
    private SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss SSS");  
    @Before("execution(* *.*(..))")  
    public void before(){  
        System.out.println("方法执行前:"+simpleDateFormat.format(new Date(System.currentTimeMillis())));  
    }  
    @After("execution(* *.*(..))")  
    public void after(){  
        System.out.println("方法执行后:"+simpleDateFormat.format(new Date(System.currentTimeMillis())));  
    }  
}

public class Test01 {  
    public static void main(String[] args) {  
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");  
        Dao dao = (Dao) applicationContext.getBean("userDao");  
  
        dao.select();  
  
        dao.del();  
    }  
}


方法执行前:2022-10-12 20:07:49 099
查询用户
方法执行后:2022-10-12 20:07:49 100
方法执行前:2022-10-12 20:07:49 101
删除了一个用户
方法执行后:2022-10-12 20:07:49 101

5、AOP的一些细节

Spring中execution语法

execution(修饰符 返回类型 切入点类 切入点方法(参数) 异常抛出)

修饰符: 可选,支持通配符,(public/private/protected)

返回类型: 必填,支持通配符,可以使用 * 来匹配所有的返回值类型

切入点类: 可选,支持通配符,指定切入点类

切入点方法: 必填,支持通配符,指定要匹配的方法名,可以使用"*"通配符来匹配所有方法

参数: 若无可不填,指定方法声明中的形参列表,支持两个通配符,即*和..

其中*代表一个任意类型的参数,而…代表零个或多个任意类型的参数

() 匹配一个不接受任何参数的方法

(..) 匹配一个接受任意数量参数的方法,可以是零个或多个

(*) 匹配一个接受一个任何类型的参数的方法,只能是一个

(*,String) 匹配一个接受两个参数的方法,其中第一个参数是任意类型,第二个参数必须是String类型

常用实例

<!-- 【1、拦截所有public方法】 -->
<aop:pointcut expression="execution(public * *(..))" id="pt"/>  

<!-- 【2、拦截所有save开头的方法】 -->
<aop:pointcut expression="execution(* save*(..))" id="pt"/> 

<!-- 【3、拦截指定类的指定方法, 拦截时候一定要定位到方法】 -->
<aop:pointcut expression="execution(* com.shore.dao.impl.UserDao.save(..))" id="pt"/>

<!-- 【4、拦截指定类的所有方法】 -->
<aop:pointcut expression="execution(* com.shore.dao.impl.UserDao.*(..))" id="pt"/>

<!-- 【5、拦截指定包,以及其自包下所有类的所有方法】 -->
<aop:pointcut expression="execution(* com..*.*(..))" id="pt"/>

<!-- 【6、多条件】 -->
<!-- 或:||   or -->
<aop:pointcut expression="execution(* com.shore.dao.impl.UserDao.save(..)) || execution(* com.shore.dao.impl.MessageDao.save(..))" id="pt" />

<aop:pointcut expression="execution(* com.shore.dao.impl.UserDao.save(..)) or execution(* com.shore.dao.impl.MessageDao.save(..))" id="pt" />

<!-- 且:&&   and -->  <!-- 语法虽然没错,但,没意义 -->
<aop:pointcut expression="execution(* com.shore.dao.impl.UserDao.save(..)) && execution(* com.shore.dao.impl.MessageDao.save(..))" id="pt" />

<aop:pointcut expression="execution(* com.shore.dao.impl.UserDao.save(..)) and execution(* com.shore.dao.impl.MessageDao.save(..))" id="pt" />

<!-- 【7、取非值:not  !  不拦截指定的规则,拦截除此之外的所有类的方法】 -->
<aop:pointcut expression="!execution(* com.shore.dao.impl.UserDao.save(..))" id="pt"/> <!-- 注意not前必须有空格 -->

<aop:pointcut expression=" not execution(* com.shore.dao.impl.UserDao.save(..))" id="pt"/>

连接点和切入点

JoinPoint

JoinPoint 连接点代表应用程序中的一个点,您可以在其中插入 AOP 方面。 您也可以说,它是应用程序中使用 Spring AOP 框架执行操作的实际位置。 考虑以下示例 −

  • 包中包含的所有方法类。
  • 类的特定方法。

PointCut

PointCut 切入点是一组一个或多个 JoinPoints,应该在其中执行建议。 正如我们将在 AOP 示例中看到的那样,您可以使用表达式或模式指定切入点。 在 Spring 中,PointCut 有助于使用特定的 JoinPoints 来应用建议。 考虑以下示例 −

  • expression = "execution(* com.tutorialspoint..(..))"
  • expression = "execution(* com.tutorialspoint.Student.getName(..))"

切面中可以设置joinpoint参数更加详细的信息

@Aspect
public class Log {
    private SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss SSS");
    @Before("execution(* *.*(..))")
    public void before(JoinPoint jp){
        System.out.println(jp.getKind());
        System.out.println(jp.getSignature());
        System.out.println(jp.toLongString());
        System.out.println(jp.getTarget());
        System.out.println(jp.getClass());
        System.out.println("方法执行前:"+simpleDateFormat.format(new Date(System.currentTimeMillis())));
    }
    public void after(){
        System.out.println("方法执行后:"+simpleDateFormat.format(new Date(System.currentTimeMillis())));
    }
}


method-execution
void com.zh.pojo.Dao.select()
execution(public abstract void com.zh.pojo.Dao.select())
com.zh.pojo.UserDao@738dc9b
class org.springframework.aop.aspectj.MethodInvocationProceedingJoinPoint
方法执行前:2022-10-12 20:29:34 187
查询用户

pointcut注解复用:

@Aspect  
public class Log {  
    private SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss SSS");  
  
    @Pointcut("execution(* *.*(..))")  
    public void p1(){}  
  
  
    @Before("p1()")  
    public void before(){  
        System.out.println("方法执行前:"+simpleDateFormat.format(new Date(System.currentTimeMillis())));  
    }  
    @After("p1()")  
    public void after(){  
        System.out.println("方法执行后:"+simpleDateFormat.format(new Date(System.currentTimeMillis())));  
    }  
    @Around("p1()")  
    public void around(ProceedingJoinPoint jp) throws Throwable {  
        System.out.println("方法环绕前:"+simpleDateFormat.format(new Date(System.currentTimeMillis())));  
        jp.proceed();  
        System.out.println("方法环绕后:"+simpleDateFormat.format(new Date(System.currentTimeMillis())));  
    }  
}

详解通知的位置

  • Before :Before 是一种通知类型,可确保通知在方法执行之前运行。
@Before("pointcutAll()")  
public void before(){  
    System.out.println("执行方法之前");  
}
  • After:After 是一种通知类型,可确保通知在方法执行后运行。即方法执行不管抛出异常或报错,都会执行
@After("pointcutAll()")  
public void after(){  
    System.out.println("执行方法之后");  
}

public void print() {  
    for (int i = 0; i < 5; i++) {  
        System.out.println(i);  
    }  
  
    /*int i = 1/0;  
    System.out.println(i);*/
}

执行方法之前
0
1
2
3
4
执行方法之后

注释另外一部分之后

执行方法之前
执行方法之后
Exception in thread "main" java.lang.ArithmeticException: / by zero
  • After Returning:After Returning 是一种通知类型,它确保只有在方法成功完成时才会在方法执行之后运行通知
@AfterReturning("pointcutAll()")  
public void afterReturning(){  
    System.out.println("方法正常执行完成");  
}

public void print() {  
    for (int i = 0; i < 5; i++) {  
        System.out.println(i);  
    }  
  
    /*int i = 1/0;  
    System.out.println(i);*/
}

执行方法之前
0
1
2
3
4
方法正常执行完成

注释另外一部分之后

执行方法之前
Exception in thread "main" java.lang.ArithmeticException: / by zero
  • After Throwing:After Throwing 是一种通知类型,它确保通知在方法执行后运行,仅当方法通过抛出异常退出时。 以下是抛后通知的语法。
@AfterThrowing(value = "pointcutAll()",throwing = "ex")  
public void afterThrowing(Throwable ex){  
    System.out.println("方法抛出异常:"+ex);  
}

public void print() {  
    for (int i = 0; i < 5; i++) {  
        System.out.println(i);  
    }  
  
    /*int i = 1/0;  
    System.out.println(i);*/
}

执行方法之前
0
1
2
3
4

注释另外一部分之后

执行方法之前
方法抛出异常:java.lang.ArithmeticException: / by zero
Exception in thread "main" java.lang.ArithmeticException: / by zero

@AfterThrowing(value = "pointcutAll()",throwing = "ex"),要捕捉异常需指定throwing参数,并且参数名字要和方法参数名字相同
方法参数设置为:Throwable类型则对抛出的异常不做限制

Throwable implements Serializable
Exception extends Throwable
  • Around:Around 是一种通知类型,可确保通知在方法执行之前和之后运行。重点

1、环绕会把before和after环绕进去

@Aspect  
public class Log {  
    private SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss SSS");  
    @Before("execution(* *.*(..))")  
    public void before(){  
        System.out.println("方法执行前:"+simpleDateFormat.format(new Date(System.currentTimeMillis())));  
    }  
    @After("execution(* *.*(..))")  
    public void after(){  
        System.out.println("方法执行后:"+simpleDateFormat.format(new Date(System.currentTimeMillis())));  
    }  
    @Around("execution(* *.*(..))")  
    public void around(ProceedingJoinPoint jp) throws Throwable {  
        System.out.println("方法环绕前:"+simpleDateFormat.format(new Date(System.currentTimeMillis())));  
        jp.proceed();  
        System.out.println("方法环绕后:"+simpleDateFormat.format(new Date(System.currentTimeMillis())));  
    }  
}

public class Test01 {  
    public static void main(String[] args) {  
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");  
        Dao dao = (Dao) applicationContext.getBean("userDao");  
  
        dao.select();  
    }  
}


方法环绕前:2022-10-12 21:14:14 893
方法执行前:2022-10-12 21:14:14 894
查询用户
方法执行后:2022-10-12 21:14:14 894
方法环绕后:2022-10-12 21:14:14 895

2、在目标方法执行之前之后执行。被注解为环绕增强的方法要有返回值,Object 类型。并且方法可以包含-一个ProceedingJoinPoint 类型的参数。接口ProceedingJoinPoint 其有一 个proceed()方法,用于执行目标方法。若目标方法有返回值,则该方法的返回值就是目标方法的返回值。最后,环绕增强方法将其返回值返回。该增强方法实际是拦截了目标方法的执行。

@Around("pointcutAll()")  
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {  
    Object proceed = joinPoint.proceed();  
    return proceed;  
}

3、常见方法的使用

@Around("pointcutAll()")  
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {  
    //获得参数  
    Object[] args = joinPoint.getArgs();  
    System.out.print("方法参数:");  
    for (Object arg : args) {  
        System.out.print(arg+"	");  
    }  
    System.out.println();  
    //获得签名,强转为方法签名  
    MethodSignature signature = (MethodSignature)joinPoint.getSignature();  
  
    //获得参数类型  
    System.out.print("参数类型:");  
    Class[] parameterTypes = signature.getParameterTypes();  
    for (Class parameterType : parameterTypes) {  
        System.out.print(parameterType+"	");  
    }  
    System.out.println();  
  
    //获得返回类型  
    Class returnType = signature.getReturnType();  
    System.out.println("返回类型:"+returnType);  
  
    //动态的修改参数  
    args[0] = "李四";  
  
    //执行方法有两个方法  
    //Object proceed() throws Throwable;执行这个方法改变的参数不生效  
    //Object proceed(Object[] var1) throws Throwable;  
  
    //此方法的返回值是目标方法的返回值。若不返回,则方法的调用者收到的返回值为空  
    Object proceed = joinPoint.proceed(args);  
    System.out.println("响应的结果为:"+proceed);  
    return proceed;  
}

环绕通知就是四合一的通知:

@Around(value = "myPointCut()")

    public Object myAround(ProceedingJoinPoint proceedingJoinPoint)

    {

        Object[] args = proceedingJoinPoint.getArgs();

        Object result=null;

        try {

            //前置通知@Before

            System.out.println("环绕前置通知");

            //目标方法执行

            result = proceedingJoinPoint.proceed(args);

            //环绕返回通知@AfterReturning

            System.out.println("环绕返回通知");

        } catch (Throwable throwable) {

            //环绕异常通知@AfterThrowing

            System.out.println("环绕异常通知");

            throw new RuntimeException(throwable);

        } finally {

            //最终通知@After

            System.out.println("环绕最终通知");

        }

        return result;

    }

Around的特别

  1. 它是功能最强的通知
  2. 在目标方法的前和后都能增强功能
  3. 控制目标方法是否被调用执行
  4. 修改原来的目标方法的执行结果。 影响最后的调用结果
    环绕通知,等同于jdk动态代理的, Invocat ionHandler接口

注解实现AOP

1、before:@Before("execution(* .(..))")

2、after:@After("execution(* .(..))")

3、afterReturning:@AfterReturning("execution(* .(..))")

4、afterThrowing:@AfterThrowing(value = "execution(* .(..))",throwing = "ex")(方法参数名需要和这里定义的一样)

5、around:@Around("execution(* .(..))")

注解的切点表达式复用:定义一个空方法,返回值为void,用@Pointcut注释

@Pointcut("execution(* *.*(..))")  
public void pointcutAll(){  
}  
  
@Before("pointcutAll()")  
public void before(){  
    System.out.println("执行方法之前");  
}

11、Spring-Mybatis

Spring整合Mybatis需要导入的依赖

<!-- 数据源 -->  
<dependency>  
    <groupId>com.alibaba</groupId>  
    <artifactId>druid</artifactId>  
    <version>1.2.13-SNSAPSHOT</version>  
</dependency>  
  
<!--  mybatis -->  
<dependency>  
    <groupId>org.mybatis</groupId>  
    <artifactId>mybatis</artifactId>  
    <version>3.5.11</version>  
</dependency>  
  
<!--  mybatis提供的spring整合包 -->  
<dependency>  
    <groupId>org.mybatis</groupId>  
    <artifactId>mybatis-spring</artifactId>  
    <version>2.0.7</version>  
</dependency>  
  
<!--  连接数据库的驱动 -->  
<dependency>  
    <groupId>mysql</groupId>  
    <artifactId>mysql-connector-java</artifactId>  
    <version>8.0.30</version>  
</dependency>  
  
<!--  spring的jdbc -->  
<dependency>  
    <groupId>org.springframework</groupId>  
    <artifactId>spring-jdbc</artifactId>  
    <version>5.3.23</version>  
</dependency>

以上是整合mybatis需要导入的依赖,除此之外,还需导入spring的相关包

实体类

@Data  
@AllArgsConstructor  
@NoArgsConstructor  
public class User {  
    private int id;  
    private String name;  
    private int age;  
    private String addr;  
    private String hobby;  
}

dao接口以及实现类

public interface UserMapper {  
    List<User> getUsers();  
}

public class UserMapperImpl implements UserMapper{  

	//这个属性由spring注入
    private SqlSessionFactory sqlSessionFactory;  
  
    public void setSqlSessionFactory(SqlSessionFactory sqlSessionFactory) {  
        this.sqlSessionFactory = sqlSessionFactory;  
    }  
  
    @Override  
    public List<User> getUsers() {  
        return sqlSessionFactory.openSession().getMapper(UserMapper.class).getUsers();  
    }  
}

Spring配置文件

<?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-3.0.xsd">  
  
    <!-- 数据源 -->  
    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">  
        <!-- 基本属性 url、user、password -->  
        <property name="driverClassName" value="com.mysql.cj.jdbc.Driver" />  
        <property name="url" value="jdbc:mysql://localhost:3306/db01" />  
        <property name="username" value="root" />  
        <property name="password" value="123456" />  
    </bean>  
    <!-- SqlSessionFactoryBean,相当于SqlSessionFactoryBuilder,返回的是sqlSessionFactory -->  
    <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">  
        <property name="dataSource" ref="dataSource" />  
        <!--  加载mybatis的配置文件-->
        <property name="configLocation" value="classpath:mybatis-config.xml"/>  
    </bean>    <!--  实现业务的实体类-->  
    <bean id="userDAO" class="com.zh.mapper.UserMapperImpl">  
        <property name="sqlSessionFactory" ref="sqlSessionFactory"/>  
    </bean>
</beans>

SqlSessionFactoryBean并不是SqlSessionFactory

<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
  <property name="dataSource" ref="dataSource" />
</bean>

相当于

@Configuration
public class MyBatisConfig {
  @Bean
  public SqlSessionFactory sqlSessionFactory() {
    SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
    factoryBean.setDataSource(dataSource());
    return factoryBean.getObject();
  }
}

mybatis的配置文件

<?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>  
    <!-- 开启延迟加载 该项默认为false,即所有关联属性都会在初始化时加载  
        true表示延迟按需加载 -->  
    <settings>  
        <setting name="lazyLoadingEnabled" value="true"/>  
        <!-- 开启二级缓存 -->  
        <setting name="cacheEnabled" value="true"/>  
    </settings>  
    <typeAliases>
            <package name="com.zh.pojo"/>  
    </typeAliases>
    <mappers>
            <mapper resource="com/zh/mapper/UserMapper.xml"/>  
    </mappers>
</configuration>

UserMapper.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="com.zh.mapper.UserMapper">  
  
    <select id="getUsers" resultType="user">  
        select * from user  
    </select>  
</mapper>

测试类

public class Test01 {  
    public static void main(String[] args) throws SQLException {  
  
        ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");  
  
        UserMapperImpl userDAO = context.getBean("userDAO", UserMapperImpl.class);  
  
        List<User> users = userDAO.getUsers();  
  
        for (User user : users) {  
            System.out.println(user);  
        }  
  
    }  
}

User(id=1, name=张三, age=18, addr=北京, hobby=听歌)
User(id=2, name=李四, age=19, addr=南昌, hobby=玩游戏)
User(id=3, name=王五, age=20, addr=上海, hobby=敲代码)
User(id=4, name=赵六, age=18, addr=成都, hobby=追剧)

可以看到,mybatis以及被spring内部集成,已经看不到mybatis的代码了

Mapper自动代理

在spring配置文件中,我们每写一个接口,就用写一个实现类,然后把sqlSessionFactory属性注入,这繁琐了点,spring为了解决这个问题,提供了mapper动态代理

原来的配置

<bean id="userDAO" class="com.zh.mapper.UserMapperImpl">  
    <property name="sqlSessionFactory" ref="sqlSessionFactory"/>  
</bean>

MapperFactoryBean

<bean id="userMapper" class="org.mybatis.spring.mapper.MapperFactoryBean">  
    <property name="mapperInterface" value="com.zh.mapper.UserMapper" />  
    <property name="sqlSessionFactory" ref="sqlSessionFactory" />  
</bean>

自动生成mapper的代理类

public class Test01 {  
    public static void main(String[] args) throws SQLException {  
  
        ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");  
  
  
        UserMapper userMapper = context.getBean("userMapper", UserMapper.class);  
  
        List<User> users = userMapper.getUsers();  
  
        for (User user : users) {  
            System.out.println(user);  
        }  
          
    }  
}

这种方法不需要自己写实现类,但是还是需要一个接口配置一个bean

MapperScannerConfigurer:自动代理包下所有的接口

<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">  
    <property name="basePackage" value="com.zh.mapper"/>  
</bean>

UserService类:

public class UserService {  
  
    private UserMapper userMapper;  
  
    public void setUserMapper(UserMapper userMapper) {  
        this.userMapper = userMapper;  
    }  
  
    public List<User> getUsers(){  
        return userMapper.getUsers();  
    }  
}

注册bean,自动注入Mapper属性

<bean id="userService" class="com.zh.service.UserService" autowire="byType"/>

测试

public class Test01 {  
    public static void main(String[] args) throws SQLException {  
  
        ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");  
        UserService userService = context.getBean("userService", UserService.class);  
  
        for (User user : userService.getUsers()) {  
            System.out.println(user);  
        }  
    }  
}

User(id=1, name=张三, age=18, addr=北京, hobby=听歌)
User(id=2, name=李四, age=19, addr=南昌, hobby=玩游戏)
User(id=3, name=王五, age=20, addr=上海, hobby=敲代码)
User(id=4, name=赵六, age=18, addr=成都, hobby=追剧)

12、Spring事务

1、jdbc事务控制

不管你现在使用的是那一种ORM开发框架,只要你的核心是JDBC,那么所有的事务处理都是围绕着JDBC开展的,而JDBC之中的事务控制是由Connection接口提供的方法:

  • 1、关闭自动事务提交:connection.setAutoCommit(false);
  • 2、事务手工提交: connection.commit();
  • 3、事务回滚: connection.rollback();

1.1、ACID事务原则

ACID主要指的是事务的四种特点:原子性(Atomicity)、一致性(Consistency)、隔离性或独立性(lsolation)、持久性(Durabilily)四个特征

  • 原子性(Atomicity):整个事务中的所有操作,要么全部完成,要么全部不完成,不可能停滞在中间某个环节。事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样;
  • 一致性(Consistency):一个事务可以封装状态改变(除非它是一个只读的)。事务必须始终保持系统处于一致的状态,不管在任何给定的时间并发事务有多少;
  • 隔离性(lsolation):隔离状态执行事务,使它们好像是系统在给定时间内执行的唯一操作。如果有两个事务,运行在相同的时间内,执行相同的功能,事务的隔离性将确保每一事务在系统中认为只有该事务在使用系统;
  • 持久性(Durability):在事务完成以后,该事务对数据库所作的更改便持久的保存在数据库之中,并不会被回滚**

2、Spring事务架构

Spring事务是对已有JDBC事务的进一步的包装型处理,所以底层依然是JDBC事务控制,而后在这之上进行了更加合理的二次开发与设计,首先先来看一下Spring 与JDBC事务之间的结构图。

​ 只要是说到了事务的开发,那么就必须考虑到ORM组件的整合问题各类的ORM开发组件实在是太多了,同时Spring在设计的时候无法预知未来,那么这个时候在Spring 框架里面就针对于事务的接入提供了一个开发标准

​ Spring事务的核心实现关键是在于:PlatformTransactionManager

下图就是Spring事务的整体架构。

3、编程式事务控制(不常用)

spirng中使用事务的两种方式

  1. 通过PlatformTransactionManager控制事务
  2. 通过TransactionTemplate控制事务

latfornTransactionManager
这种是最原始的方式,代码量较大,后面其他方式都是针对这种方式的封装

@Test
    public void test1() throws Exception {
        //定义一个数据源
        DataSource dataSource = new DataSource();
        dataSource.setDriverClassName("com.mysql.jdbc.Driver");
        dataSource.setUrl("jdbc:mysql://localhost:3306/int?characterEncoding=UTF-8&serverTimezone=UTC");
        dataSource.setUsername("root");
        dataSource.setPassword("123456");
        dataSource.setInitialSize(5);
        //定义一个JdbcTemplate,用来方便执行数据库增删改查
        JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
        //1.定义事务管理器,给其指定一个数据源(可以把事务管理器想象为一个人,这个人来负责事务的控制操作)
        PlatformTransactionManager platformTransactionManager = new DataSourceTransactionManager(dataSource);
        //2.定义事务属性:TransactionDefinition,
        // TransactionDefinition可以用来配置事务的属性信息,比如事务隔离级别、事务超时时间、事务传播方式、是否是只读事务等等。
        TransactionDefinition transactionDefinition = new DefaultTransactionDefinition();
        //3.开启事务:调用platformTransactionManager.getTransaction开启事务操作,得到事务状态(TransactionStatus)对象
        TransactionStatus transactionStatus = platformTransactionManager.getTransaction(transactionDefinition);
        //4.执行业务操作,下面就执行2个插入操作
        try {
            System.out.println("before:" + jdbcTemplate.queryForList("SELECT * from t_user"));
            jdbcTemplate.update("insert into t_user (name) values (?)", "test1-1");
            jdbcTemplate.update("insert into t_user (name) values (?)", "test1-2");
            //5.提交事务:platformTransactionManager.commit
            platformTransactionManager.commit(transactionStatus);
        } catch (Exception e) {
            //6.回滚事务:platformTransactionManager.rollback
            platformTransactionManager.rollback(transactionStatus);
        }
        System.out.println("after:" + jdbcTemplate.queryForList("SELECT * from t_user"));
    }

代码中主要有5个步骤

步骤一:定义事务管理器PlatformTransactionManager

事务管理器相当于一个管理员,这个管理员就是用来帮你控制事务的,比如开启事务,提交事务,回滚事务等等。

spring中使用PlatformTransactionManager这个接口来表示事务管理器

public interface PlatformTransactionManager {

 //获取一个事务(开启事务)
 TransactionStatus getTransaction(@Nullable TransactionDefinition definition)
   throws TransactionException;

 //提交事务
 void commit(TransactionStatus status) throws TransactionException;


 //回滚事务
 void rollback(TransactionStatus status) throws TransactionException;

}

PlatformTransactionManager多个实现类,用来应对不同的环境

JpaTransactionManager:如果你用jpa来操作db,那么需要用这个管理器来帮你控制事务。
DataSourceTransactionManager:如果你用是指定数据源的方式,比如操作数据库用的是:JdbcTemplate、mybatis、ibatis,那么需要用这个管理器来帮你控制事务。
HibernateTransactionManager:如果你用hibernate来操作db,那么需要用这个管理器来帮你控制事务。
JtaTransactionManager:如果你用的是java中的jta来操作db,这种通常是分布式事务,此时需要用这种管理器来控制事务。

步骤二:定义事务属性TransactionDefinition

定义事务属性,比如事务隔离级别、事务超时时间、事务传播方式、是否是只读事务等等。
spring中使用TransactionDefinition接口来表示事务的定义信息,有个子类比较常用:DefaultTransactionDefinition。

步骤三:开启事务

调用事务管理器的getTransaction方法,即可以开启一个事务

这个方法会返回一个TransactionStatus表示事务状态的一个对象,通过TransactionStatus提供的一些方法可以用来控制事务的一些状态,比如事务最终是需要回滚还是需要提交。

执行了getTransaction后,spring内部会执行一些操作,为了方便大家理解,咱们看看伪代码:

/有一个全局共享的threadLocal对象 resources
static final ThreadLocal<Map<Object, Object>> resources = new NamedThreadLocal<>("Transactional resources");
//获取一个db的连接
DataSource datasource = platformTransactionManager.getDataSource();
Connection connection = datasource.getConnection();
//设置手动提交事务
connection.setAutoCommit(false);
Map<Object, Object> map = new HashMap<>();
map.put(datasource,connection);
resources.set(map);

上面代码,将数据源datasource和connection映射起来放在了ThreadLocal中,ThreadLocal大家应该比较熟悉,用于在同一个线程中共享数据;后面我们可以通过resources这个ThreadLocal获取datasource其对应的connection对象。

步骤四:执行业务操作

我们使用jdbcTemplate插入了2条记录。
大家看一下创建JdbcTemplate的代码,需要指定一个datasource

JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);

再来看看创建事务管理器的代码

PlatformTransactionManager platformTransactionManager = new DataSourceTransactionManager(dataSource);

2者用到的是同一个dataSource,而事务管理器开启事务的时候,会创建一个连接,将datasource和connection映射之后丢在了ThreadLocal中,而JdbcTemplate内部执行db操作的时候,也需要获取连接,JdbcTemplate会以自己内部的datasource去上面的threadlocal中找有没有关联的连接,如果有直接拿来用,若没找到将重新创建一个连接,而此时是可以找到的,那么JdbcTemplate就参与到spring的事务中了。

步骤五:提交 or 回滚

方式2:TransactionTemplate

方式1中部分代码是可以重用的,所以spring对其进行了优化,采用模板方法模式就其进行封装,主要省去了提交或者回滚事务的代码。

@Test
    public void test2() throws Exception {
        //定义一个数据源
        DataSource dataSource = new DataSource();
        dataSource.setDriverClassName("com.mysql.jdbc.Driver");
        dataSource.setUrl("jdbc:mysql://localhost:3306/int?characterEncoding=UTF-8&serverTimezone=UTC");
        dataSource.setUsername("root");
        dataSource.setPassword("123456");
        dataSource.setInitialSize(5);
        //定义一个JdbcTemplate,用来方便执行数据库增删改查
        JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
        //1.定义事务管理器,给其指定一个数据源(可以把事务管理器想象为一个人,这个人来负责事务的控制操作)
        PlatformTransactionManager platformTransactionManager = new DataSourceTransactionManager(dataSource);
        //2.定义事务属性:TransactionDefinition,TransactionDefinition可以用来配置事务的属性信息,比如事务隔离级别、事务超时时间、事务传播方式、是否是只读事务等等。
        DefaultTransactionDefinition  transactionDefinition = new DefaultTransactionDefinition();
        transactionDefinition.setTimeout(10);
        //3.创建TransactionTemplate对象
        TransactionTemplate transactionTemplate = new TransactionTemplate(platformTransactionManager,transactionDefinition);/**
         * 4.通过TransactionTemplate提供的方法执行业务操作
         * 主要有2个方法:
         * (1).executeWithoutResult(Consumer<TransactionStatus> action):没有返回值的,需传递一个Consumer对象,在accept方法中做业务操作
         * (2).<T> T execute(TransactionCallback<T> action):有返回值的,需要传递一个TransactionCallback对象,在doInTransaction方法中做业务操作
         * 调用execute方法或者executeWithoutResult方法执行完毕之后,事务管理器会自动提交事务或者回滚事务。
         * 那么什么时候事务会回滚,有2种方式:
         * (1)transactionStatus.setRollbackOnly();将事务状态标注为回滚状态
         * (2)execute方法或者executeWithoutResult方法内部抛出异常
         * 什么时候事务会提交?
         * 方法没有异常 && 未调用过transactionStatus.setRollbackOnly();
         */
        transactionTemplate.executeWithoutResult(new Consumer<TransactionStatus>() {
            @Override
            public void accept(TransactionStatus transactionStatus) {
                jdbcTemplate.update("insert into t_user (name) values (?)", "transactionTemplate-1");
                jdbcTemplate.update("insert into t_user (name) values (?)", "transactionTemplate-2");
            }
        });
        System.out.println("after:" + jdbcTemplate.queryForList("SELECT * from t_user"));
    }

代码分析

TransactionTemplate,主要有2个方法:

executeWithoutResult:无返回值场景,需传递一个Consumer对象,在accept方法中做业务操作

transactionTemplate.executeWithoutResult(new Consumer<TransactionStatus>() {
    @Override
    public void accept(TransactionStatus transactionStatus) {
        //执行业务操作
    }
});

execute:有返回值场景,需要传递一个TransactionCallback对象,在doInTransaction方法中做业务操作

Integer result = transactionTemplate.execute(new TransactionCallback<Integer>() {
    @Nullable
    @Override
    public Integer doInTransaction(TransactionStatus status) {
        return jdbcTemplate.update("insert into t_user (name) values (?)", "executeWithoutResult-3");
    }
});

什么时候事务会回滚,有2种方式

  1. 在execute或者executeWithoutResult内部执行transactionStatus.setRollbackOnly();将事务状态标注为回滚状态,spring会自动让事务回滚
  2. execute方法或者executeWithoutResult方法内部抛出任意异常即可回滚

什么时候事务会提交?

方法没有异常 && 未调用过transactionStatus.setRollbackOnly();

编程式事务正确的使用姿势

如果大家确实想在系统中使用编程式事务,那么可以参考下面代码,使用spring来管理对象,更简洁一些。

先来个配置类,将事务管理器PlatformTransactionManager、事务模板TransactionTemplate都注册到spring中,重用。

@Configuration
@ComponentScan
public class MainConfig3 {
    @Bean
    public DataSource dataSource() {
        org.apache.tomcat.jdbc.pool.DataSource dataSource = new org.apache.tomcat.jdbc.pool.DataSource();
        dataSource.setDriverClassName("com.mysql.jdbc.Driver");
        dataSource.setUrl("jdbc:mysql://localhost:3306/javacode2018?characterEncoding=UTF-8");
        dataSource.setUsername("root");
        dataSource.setPassword("root123");
        dataSource.setInitialSize(5);
        return dataSource;
    }

    @Bean
    public JdbcTemplate jdbcTemplate(DataSource dataSource) {
        return new JdbcTemplate(dataSource);
    }

    @Bean
    public PlatformTransactionManager transactionManager(DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }

    @Bean
    public TransactionTemplate transactionTemplate(PlatformTransactionManager transactionManager) {
        return new TransactionTemplate(transactionManager);
    }
}

通常我们会将业务操作放在service中,所以我们也来个service:UserService。

@Component
public class UserService {
    @Autowired
    private JdbcTemplate jdbcTemplate;
    @Autowired
    private TransactionTemplate transactionTemplate;

    //模拟业务操作1
    public void bus1() {
        this.transactionTemplate.executeWithoutResult(transactionStatus -> {
            //先删除表数据
            this.jdbcTemplate.update("delete from t_user");
            //调用bus2
            this.bus2();
        });
    }

    //模拟业务操作2
    public void bus2() {
        this.transactionTemplate.executeWithoutResult(transactionStatus -> {
            this.jdbcTemplate.update("insert into t_user (name) VALUE (?)", "java");
            this.jdbcTemplate.update("insert into t_user (name) VALUE (?)", "spring");
            this.jdbcTemplate.update("insert into t_user (name) VALUE (?)", "mybatis");
        });
    }

    //查询表中所有数据
    public List userList() {
        return jdbcTemplate.queryForList("select * from t_user");
    }
}

4、声明式事务

声明式事务管理有两种常用的方式,一种是基于tx和aop命名空间的xml配置文件,一种是基于@Transactional注解,随着Spring和Java的版本越来越高,大家越趋向于使用注解的方式,下面我们两个都说。

1.基于tx和aop命名空间的xml配置文件
配置文件

<tx:advice id="tx" transaction-manager="transactionManager" >  
    <tx:attributes>  
        <tx:method name="add*"/>  
        <tx:method name="del*"/>  
        <tx:method name="update*"/>  
        <tx:method name="get*" read-only="true"/>  
    </tx:attributes>  
</tx:advice>  
  
<aop:config>  
    <aop:pointcut id="txPointcut" expression="execution(* com.zh.services.*.*(..))"/>  
    <aop:advisor advice-ref="tx" pointcut-ref="txPointcut"/>  
</aop:config>

测试方法

public int add(User user){  
    int i = userMapper.addUser(user);  
    return i;  
}

测试

@Test  
public void t3(){  
    UserServices userServices = context.getBean("userServices", UserServices.class);  
    int i = userServices.add(new User(8,"张三",18,"北京","看电视"));  
    System.out.println(i);  
}

结果:数据正确插入数据库

模拟抛出异常

public int add(User user){  
    int i = userMapper.addUser(user);  
    //为了查看插入之后的数据的  
    for (User newUser : userMapper.getUsers()) {  
        System.out.println(newUser);  
    }  
    int j = 1/0;  
    return i;  
}

再次运行

@Test  
public void t3(){  
    UserServices userServices = context.getBean("userServices", UserServices.class);  
    int i = userServices.add(new User(9,"张三",18,"北京","看电视"));  
    System.out.println(i);  
}

结果:

User(id=1, name=张三, age=18, addr=北京, hobby=听歌)
User(id=2, name=李四, age=19, addr=南昌, hobby=玩游戏)
User(id=3, name=王五, age=20, addr=上海, hobby=敲代码)
User(id=4, name=赵六, age=18, addr=成都, hobby=追剧)
User(id=5, name=jake, age=21, addr=瑞金, hobby=看书)
User(id=8, name=张三, age=18, addr=北京, hobby=看电视)
User(id=9, name=张三, age=18, addr=北京, hobby=看电视)

java.lang.ArithmeticException: / by zero

可以看到这里查询出来的结果已经有id为9的用户,而后面有异常,产生异常之后回滚了数据,再看数据库中确实没有用户id为9的用户

2.基于@Transactional注解

这种方式最简单,也是最为常用的,只需要在配置文件中开启对注解事务管理的支持。

<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">  
    <property name="dataSource" ref="dataSource"/>  
</bean>   
  
<tx:annotation-driven transaction-manager="transactionManager"/>

然后在需要事务管理的地方加上@Transactional注解,如:

@Transactional  
public int add(User user){  
    int i = userMapper.addUser(user);  
    //为了查看插入之后的数据的  
    for (User newUser : userMapper.getUsers()) {  
        System.out.println(newUser);  
    }  
    int j = 1/0;  
    return i;  
}

执行结果与上面的一样

5、Spring事务隔离级别

Spring面试之中隔离级别的面试问题是最为常见的,也是一个核心的基础所在,但是所谓的隔离级别一定要记住,是在并发环境访问下才会存在的问题。数据库是一个项目应用中的公共存储资源,所以在实际的项目开发过程中,很有可能会有两个不同的线程(每个线程拥有各自的数据库事务),要进行同一条数据的读取以及更新操作。

5.1、脏读

脏读(Dirty reads):事务A在读取数据时,读取到了事务B未提交的数据,由于事务B有可能被回滚,所以该数据有可能是一个无效数据

5.2、不可重复读

不可重复读(Non-repeatable Reads):事务A对一个数据的两次读取返回了不同的数据内容,有可能在两次读取之间事务B对该数据进行了修改,一般此类操作出现在数据修改操作之中;

5.3、幻读

幻读(Phantom Reads):事务A在进行数据两次查询时产生了不一致的结果,有可能是事务B在事务A第二次查询之前增加或删除了数据内容所造成的.

Spring最大的优势是在于将所有的配置过程都进行了标准化的定义,于是在TransactionDefintion接口里面就提供了数据库隔离级别的定义常量

从正常的设计角度来讲,在进行Spring事务控制的时候,不要轻易的去随意修改隔离级别(需要记住这几个隔离级别的概念),因为一般都使用默认的隔离级别,由数据库自己来实现的控制。

6、Spring事务传播机制

事务开发是和业务层有直接联系的,在进行开发的过程之中,很难出现业务层之间不互相调用的场景,例如:存在有一个A业务处理,但是A业务在处理的时候有可能会调用B业务,那么如果此时A和B之间各自都存在有事务的机制,那么这个时候就需要进行事务有效的传播管理。

1、TransactionDefinition.PROPAGATION_REQUIRED:默认事务隔离级别,子业务直接支持当前父级事务,如果当前父业务之中没有事务,则创建一个新的事务,如果当前父业务之中存在有事务,则合并为一个完整的事务。简化的理解:不管任何的时候,只要进行了业务的调用,都需要创建出一个新的事务,这种机制是最为常用的事务传播机制的配置。

2、TransactionDefinition.PROPAGATION_SUPPORTS:如果当前父业务存事务,则加入该父级事务。如果当前不存在有父级事务,则以非事务方式运行;

3、TransactionDefinition.PROPAGATION_NOT_SUPPORTED:以非事务的方式运行,如果当前存在有父级事务,则先自动挂起父级事务后运行;

4、TransactionDefinition.PROPAGATION_MANDATORY:如果当前存在父级事务,则运行在父级事务之中,如果当前无事务则抛出异常(必须存在有父级事务);

5、TransactionDefinition.PROPAGATION_REQUIRES_NEW:建立一个新的子业务事务,如果存在有父级事务则会自动将其挂起,该操作可以实现子事务的独立提交,不受调用者的事务影响,即便父级事务异常,也可以正常提交;

6、TransactionDefinition.PROPAGATION_NEVER:以非事务的方式运行,如果当前存在有事务则抛出异常;

7、TransactionDefinition.PROPAGATION_NESTED:如果当前存在父级事务,则当前子业务中的事务会自动成为该父级事务中的一个子事务,只有在父级事务提交后才会提交子事务。如果子事务产生异常则可以交由父级调用进行异常处理,如果父级事务产生异常,则其也会回滚。