Spring

Spring 初识

官方学习文档:https://docs.spring.io/spring-framework/docs/5.0.0.RC3/spring-framework-reference/core.html#spring-core

优点:

  • 轻量级、非入侵式框架
  • 控制反转(IOC),面向切面编程(AOP)
  • 支持事务处理,支持框架整合

总结:Spring 就是一个轻量级的控制反转(IOC)和面向切面编程(AOP)的框架。

环境初步

依赖:或可导入 spring-context 依赖

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-webmvc</artifactId>
    <version>5.2.5.RELEASE</version>
</dependency>

beans.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="hello" class="com.luis.domain.Hello">
        <property name="str" value="hello spring"/>
    </bean>
</beans>

测试:

ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
Hello hello = (Hello)context.getBean("hello");
System.out.println(hello);

IOC 思想理解

  • IOC(控制反转)是一种思想,DI(依赖注入)是实现 IOC 的一种方式。
  • 控制:谁来控制对象的创建,传统应用程序的对象是由程序本身控制创建,使用 Spring 后,对象是由 Spring 来创建
  • 反转:程序本身不创建对象,而变成被动的接收对象
  • 依赖注入:就是利用 set 方法来进行注入的
  • IOC 是一种编程思想,由主动的编程编程被动的接收
  • 通过控制反转,我们彻底不用在程序中去改动了,要实现不同的操作只需要在 xml 配置文件中进行修改即可
  • 所谓的 IOC,一句话来说,就是对象由 Spring 来创建,管理,装配!
  • 个人认为,所谓控制反转就是获得依赖对象的方式反转了。
  • 采用 XML 的方式配置 Bean 的时候,Bean 的定义信息是和实现分离的,而采用注解的方式可以把两者合为一体,Bean 的定义信息直接以注解的形式定义在实现类中,从而达到零配置的目的。
  • 控制反转是一种通过描述(XML 或注解)并通过第三方去生产或获取特定对象的方式。在 Spring 中实现控制反转的是 IoC 容器,其实现方法是依赖注入(DI)。

IOC 创建对象的方式

  1. 默认是通过无参构造创建(set 赋值注入)

    <bean id="hello" class="com.luis.domain.Hello">
    	<property name="str" value="hello spring"/>
    </bean>
    
  2. 可以使用有参构造创建(构造赋值注入)

    1. 通过参数赋值创建(推荐+掌握)

      <bean id="hello" class="com.luis.domain.Hello">
          <constructor-arg name="str" value="test"/>
      </bean>
      
    2. 通过类型赋值创建(不建议)

      <bean id="hello" class="com.luis.domain.Hello">
      	<constructor-arg type="java.lang.String" value="tt"/>
      </bean>
      
    3. 通过下标赋值创建

      <bean id="hello" class="com.luis.domain.Hello">
      	<constructor-arg index="0" value="t"/>
      </bean>
      

    总结:在 Spring 容器加载配置文件时,容器中管理的对象就全部被实例化了。

Spring 主配置

Bean 的配置

<!--
        id:bean 的唯一标识符
        class:对象的全限定名,即包名 + 类型
        name:和 alias 标签作用一样,起别名,支持配置多个;容器中获取对象的时候可用此别名获取
-->
<bean id="hello" class="com.luis.domain.Hello" name="hello3,hello4">
    <property name="str" value="hello spring"/>
</bean>

别名

作用:给 bean 配置别名后,从容器中获取 bean 时,可以通过 bean 的 id,也可以通过自定义的别名

使用位置:和 bean 标签同级

<alias name="hello" alias="hello2"/>

import

使用位置:和 bean 标签同级

作用:一般用于团队开发使用,可以将多个配置文件,导入合并为一个

  • 主配置文件中一般只是导入其他配置,不定义对象
  • 其他副配置一般有两种分类方式:一种是按模块,一种是按类型(dao、service、controller 等)
举例:
假设 applicationContext.xml 为主配置,beans1.xml、beans2.xml、beans3.xml 均为副配置,可在主配置 applicationContext.xml 中使用 import 标签将其他副配置合并导入,这样,其他副配置中内容也可正常使用。

image-20220314155040205

<!--主配置 applicationContext.xml 中-->
<import resource="classpath:beans1.xml"/>
<import resource="classpath:beans2.xml"/>
<import resource="classpath:beans3.xml"/>

<!--也可以直接使用通配符的方式:注意需要放在同一个目录下,而且不要造成死循环-->
<import resource="classpath:beans*.xml"/>
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
Hello hello = (Hello)context.getBean("xxx"); // 其他副配置中注册的对象也可获取到
System.out.println(hello);

Spring 容器的对象管理

应该放容器中管理的对象

Spring 中的对象默认都是单例的,在容器中叫这个名称的对象只有一个

  • dao 类
  • service 类
  • controller 类
  • 工具类

不应该放容器中管理的对象

  • 实体类
  • servlet
  • listener
  • filter 等

DI 依赖注入 - 基于 XML

核心总览:

  1. set 注入
  2. 构造注入
  3. 引用类型自动注入

基于 xml 的DI 依赖注入主要有两种方式:set 注入和构造注入

Set 方式注入(重点)

  • 依赖注入:Set 注入!
    • 依赖:bean 对象的创建依赖于容器
    • 注入:也就是赋值的意思,指 bean 对象中的所有属性,由容器来注入

Set 注入下对象不同类型属性的注入格式示例:

// 实体类 Address
public class Address {
    private String addr;
    // getter和setter省略
    // toString省略
}
// 实体类 Student
public class Student {
    private String name;
    private Address address;
    private String[] books;
    private List<String> hobbys;
    private Map<String,String> card;
    private Set<String> games;
    private String wife;
    private Properties info;
    // getter和setter省略
    // toString省略
}
<bean id="address" class="com.luis.domain.Address">
    <property name="addr" value="苏州"/>
</bean>

<bean id="student" class="com.luis.domain.Student">
    <!-- 普通值注入:value -->
    <property name="name" value="林威"/>
    <!-- bean注入:ref -->
    <property name="address" ref="address"/>
    <!-- 数组注入:array -->
    <property name="books">
        <array>
            <value>天气之子</value>
            <value>斗破苍穹</value>
        </array>
    </property>
    <!-- List集合注入:list -->
    <property name="hobbys">
        <list>
            <value>看动漫</value>
            <value>编程</value>
            <value>阅读</value>
        </list>
    </property>
    <!-- Map集合注入:map -->
    <property name="card">
        <map>
            <entry key="身份证" value="123456789"/>
            <entry key="银行卡" value="987654321"/>
        </map>
    </property>
    <!-- Set集合注入:set -->
    <property name="games">
        <set>
            <value>LOL</value>
            <value>Steam</value>
        </set>
    </property>
    <!-- null注入:null -->
    <property name="wife">
        <null/>
    </property>
    <!-- 属性类Properties注入:props -->
    <property name="info">
        <props>
            <prop key="username">luis</prop>
            <prop key="password">123</prop>
        </props>
    </property>
</bean>
测试结果:
Student{
name='林威', 
address=Address{addr='苏州'}, 
books=[天气之子, 斗破苍穹], 
hobbys=[看动漫, 编程, 阅读], 
card={身份证=123456789, 银行卡=987654321}, 
games=[LOL, Steam], 
wife='null', 
info={password=123, username=luis}}

构造器方式注入

前面 IOC 通过有参构造创建对象时已经讲述,此处简要介绍

三种方式:

  1. 参数(推荐)

  2. 下标(不建议)

  3. 类型

<!--方式一:通过参数构造输入-->
<bean id="hello" class="com.luis.domain.Hello">
    <constructor-arg name="str" value="test"/>
</bean>
<!--方式二:通过下标构造输入-->
<bean id="hello" class="com.luis.domain.Hello">
	<constructor-arg index="0" value="t"/>
</bean>
<!--方式三:通过类型构造输入-->
<bean id="hello" class="com.luis.domain.Hello">
	<constructor-arg type="java.lang.String" value="tt"/>
</bean>

引入类型的自动注入

Bean 的自动装配

  • 在 Spring 中有三种装配的方式
    1. 在 XML 中显示配置
    2. 在 Java 中显示配置
    3. 隐式的自动装配 bean【重要】(即引入类型的自动注入)
  • 自动装配是 Spring 满足 bean 依赖的一种方式
  • Spring 会在上下文中自动寻找,并自动给 bean 装配属性
  • 在说自动装配之前,我们先聊一聊什么是手动装配。
  • 手动装配:就是我们自己给定属性,然后赋值
  • Spring IOC 容器可以自动装配 Bean,需要做的仅仅是在 bean 的 autowire 属性里指定自动装配的模式
  • byName(按名称注入)和byType(按类型注入)两种模式可实现引用类型的自动注入/装配
引用类型的自动注入: spring框架根据某些规则可以给引用类型赋值,不用你再给引用类型赋值了。
使用的规则常用的是byName, byType.

1.byName(按名称注入) : java类中引用类型的属性名和spring容器中(配置文件)<bean> 的id名称一样,
                       且数据类型是一致的,这样的容器中的bean,spring能够赋值给引用类型。
  语法:
  <bean id="xx" class="yyy" autowire="byName">
     简单类型属性赋值
  </bean>

 2.byType(按类型注入) : java类中引用类型的数据类型和spring容器中(配置文件)<bean>的class属性
                       是同源关系的,这样的bean能够赋值给引用类型
  同源就是一类的意思:
   1.java类中引用类型的数据类型和bean的class的值是一样的。
   2.java类中引用类型的数据类型和bean的class的值父子类关系的。
   3.java类中引用类型的数据类型和bean的class的值接口和实现类关系的

测试环境:

// 实体类
public class Cat {
    public void shout() {
        System.out.println("mmm~");
    }
}

public class Dog {
    public void shout() {
        System.out.println("www~");
    }
}

public class Person {
    private Dog dog;
    private Cat cat;
    private String name;
    // getter and setter 省略
}    

byName 自动注入

byName(按名称注入):

java 类中引用类型的属性名和 spring 容器中(配置文件)bean 的 id 名称一样,
且数据类型是一致的,这样的容器中的 bean,spring 能够赋值给引用类型。

<bean id="dog" class="com.luis.domain.Dog"/>
<bean id="cat" class="com.luis.domain.Cat"/>
	<!--
        byName:java类中引用类型的属性名和spring容器中(配置文件)<bean> 的id名称一样,
                且数据类型是一致的,这样的容器中的bean,spring能够赋值给引用类型。
  	-->
<bean id="person" class="com.luis.domain.Person" autowire="byName">
    <property name="name" value="luis"/>
</bean>

byType 自动注入

byType(按类型注入):

java 类中引用类型的数据类型和 spring 容器中(配置文件)bean 的 class 属性
是同源关系的,这样的bean能够赋值给引用类型。
同源关系:相同类型、父子继承、接口和实现类。

注意:使用 byType 按类型自动注入,在 xml 配置文件中声明 bean 的时候就只能有一个符合要求的,多了就会报错!

<bean id="dog" class="com.luis.domain.Dog"/>
<bean id="cat" class="com.luis.domain.Cat"/>
	<!--
        byType:java类中引用类型的数据类型和spring容器中(配置文件)<bean>的class属性
                是同源关系的,这样的bean能够赋值给引用类型
        同源关系:相同类型、父子继承、接口和实现类
    -->
<bean id="person" class="com.luis.domain.Person" autowire="byType">
    <property name="name" value="luis"/>
</bean>

测试:

ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
Person person = context.getBean("person", Person.class);
person.getCat().shout();
person.getDog().shout();

拓展方式注入(了解)

image-20220314172058374

Bean 的作用域

设置方法:在 bean 标签中作为 scope 属性进行设置

  1. 单例模式(Spring 默认机制):每次 get 的都是同一个对象

    <bean id="address" class="com.luis.domain.Address" scope="singleton">
        <property name="addr" value="苏州"/>
    </bean>
    
  2. 原型模式:每次从容器中 get 的时候,都会产生一个新对象

    <bean id="address" class="com.luis.domain.Address" scope="prototype">
        <property name="addr" value="苏州"/>
    </bean>
    
  3. 其余的 request、session、application 这些只能在 web 开发中使用到,作用域如其名

DI 依赖注入 - 基于注解

  • 使用注解的方式完成对象创建和属性赋值

以后开发,注解为主,配置为辅!

使用步骤:

  1. 添加 spring-context 依赖(主要是使用 spring-context 依赖中包含的 spring-aop 依赖)

    <dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>5.2.5.RELEASE</version>
    </dependency>
    
  2. 在类中使用 Spring 的注解(有多个不同功能的注解)

  3. 在 Spring 的配置文件中添加组件扫描器标签,说明注解在项目中的位置

    使用技巧:在 IDEA 中直接打 <comp... 就可以有提示!!!

    <!--声明组件扫描器(component-scan),组件就是Java对象,需要指定包名-->
    <context:component-scan base-package="com.luis"/>
    
    <!--指定多个包的三种方式-->
    <!--方式一:使用多次组件扫描器,每次指定不同的包-->
    <context:component-scan base-package="com.luis.ba01"/>
    <context:component-scan base-package="com.luis.ba02"/>
    <!--方式二:使用分隔符(;或,)分隔多个包名-->
    <context:component-scan base-package="com.luis.ba01;com.luis.ba02"/>
    <!--方式三:直接指定父包-->
    <context:component-scan base-package="com.luis"/>
    

常用注解

  1. @Component:创建除 dao,service、controller 之外的对象专用
  2. @Repository:创建 dao 类对象专用
  3. @Service:创建 service 类对象专用
  4. @Controller:创建 controller 类对象专用
  5. @Value:给简单类型属性赋值(推荐直接在属性上使用)
  6. @Autowired:给引用类型属性赋值(推荐直接在属性上使用,使用的是自动注入原理,默认 byType)
  7. @Resource:给引用类型属性赋值(推荐直接在属性上使用,使用的是自动注入原理,默认 byName)

@Component

作用:创建对象

特点:创建除 dao,service、controller 之外的对象专用

// 正规完全写法
// @Component(value = "myStudent")
// 常用省略value写法(开发常用)
@Component("myStudent")
// 不指定value,系统自动指定为类名首字母小写
// @Component
public class Student {}

@Repository

作用:创建对象

用法同 @Component

特点:放在持久层 dao 类上,表示创建 dao 对象,dao 对象可访问数据库

@Service

作用:创建对象

用法同 @Component

特点:放在业务层 service 类上,表示创建 service 对象,service 对象做业务处理,有事务等功能。

@Controller

作用:创建对象

用法同 @Component

特点:放在控制器 controller 类上,表示创建 controller 对象,controller 对象可接收用户参数,显示请求处理结果。

@Value

作用:给简单类型属性赋值

使用方式:

  1. 直接在属性定义上使用(无需set方法,开发常用,推荐使用)
  2. 在 set 方法上使用(不建议)
@Value("luis")
private String name;

@Value("22")
public void setAge(Integer age) {
    this.age = age;
}

@Autowired

作用:给引用类型属性赋值

使用方式:

  1. 在引用类型属性上直接使用(无需 set 方法,开发常用,推荐使用)
  2. 在 set 方法上使用(不建议)

注意:

  • 给引用类型属性赋值,使用的是自动注入原理,支持 byName 和 byType

  • 默认使用的是 byType 自动注入

  • 如果不使用默认的 byType 自动注入,需要使用 byName 自动注入,需要在添加 @Autowired 注解的前提下,再添加一个 @Qualifier 注解,并为其指定 bean 的 id。

关于 @Autowired 的属性 required:

  • 添加 @Autowired 属性时,其有一个 required 属性,是 boolean 类型,默认为 true
  • required = true:表示若引用类型赋值失败,程序报错,并终止执行
  • required = false:表示若引用类型赋值失败,程序正常执行,引用类型赋值 null
  • 建议:不要设置为 false,建议使用默认的 true 值,可提前暴露问题!
// 使用默认的 byType 自动注入方式
@Autowired
private Address address;

// 使用 byName 自动注入方式
@Autowired
@Qualifier("myAddr")
private Address address;

@Resource

来源:来自 JDK 中,不是来自 Spring,Spring 提供对其的支持

作用:给引用类型属性赋值

使用方式:

  1. 在引用类型属性上直接使用(无需 set 方法,开发常用,推荐使用)
  2. 在 set 方法上使用(不建议)

注意:

  • 给引用类型属性赋值,使用的是自动注入原理,支持 byName 和 byType
  • 默认先使用的是 byName 自动注入,若 byName 自动注入方式失败,则再使用 byType 自动注入
  • 如果希望只使用 byName 方式,只需要为其指定 name 属性值即可
// 默认先使用 byName,若 byName 失败,则使用 byType
@Resource
private Address address;

// 指定只使用 byName 方式
@Resource(name = "myAddr")
private Address address;

关于注解和 XML 配置方式选择

  • 对于不经常改变的使用注解,经常改变的,使用配置文件方式

AOP

什么时候考虑使用 AOP ?

  1. 需要给项目中一个类保留其原有功能前提下,进行功能完善,可以使用 AOP 实现。
  2. 需要给项目中多个类增加一个相同的功能,可以使用 AOP 实现。
  3. 需要给业务方法增加事务,日志输出时,可以使用 AOP 实现。

动态代理

两种实现方式

  1. jdk动态代理
    • 使用 jdk 中的 Proxy,Method,InvocaitonHanderl 创建代理对象
    • jdk 动态代理要求目标类必须实现接口
  2. cglib 动态代理
    • 第三方的工具库,创建代理对象
    • 原理是继承,通过继承目标类,创建子类,子类就是代理对象
    • 要求目标类不能是 final的, 方法也不能是 final 的

动态代理作用(掌握)

  1. 在目标类源代码不改变的情况下,增加功能
  2. 减少代码的重复
  3. 专注业务逻辑代码
  4. 解耦合,让你的业务功能和日志,事务非业务功能分离

理解 AOP

概念

  • AOP(Aspect Orient Programming)面向切面编程

  • 面向切面编程, 基于动态代理的,可以使用 jdk,cglib 两种代理方式 。

  • Aspect: 切面,给你的目标类增加的功能,就是切面。 像我们用的日志,事务都是切面。

    • 切面的特点: 一般都是非业务方法,独立使用的。
  • Orient:面向,对着。

  • Programming:编程

总结:AOP 就是动态代理的规范化, 把动态代理的实现步骤,方式都定义好了,让开发人员用一种统一的方式,使用动态代理。作用:

  1. 在目标类不改变源代码的前提下,增强功能
  2. 减少重复的代码
  3. 专注业务功能的实现
  4. 解耦合:将业务功能和日志,事务这些非业务功能分离开,解耦合

怎么理解面向切面编程 ?

面试常问!

  1. 需要在分析项目功能时,找出切面
  2. 合理的安排切面的执行时间(在目标方法前, 还是目标方法后)
  3. 合理的安排切面执行的位置,在哪个类,哪个方法增加增强功能

相关术语

  1. Aspect:切面,表示增强的功能,就是一堆代码,完成某个一个功能。非业务功能,常见的切面功能有日志,事务,统计信息,参数检查,权限验证。
  2. JoinPoint:连接点,连接业务方法和切面的位置,就是某类中的业务方法。
  3. Pointcut:切入点,指多个连接点方法的集合,多个方法;表示切面功能执行的位置。
  4. 目标对象:给哪个类的方法增加功能,这个类就是目标对象。
  5. Advice:通知,通知表示切面功能执行的时间。

切面三个关键的要素:

  1. 切面的功能代码,切面干什么
  2. 切面的执行位置,使用 Pointcut 表示切面执行的位置
  3. 切面的执行时间,使用 Advice 表示时间,在目标方法之前,还是目标方法之后

AOP 的实现

AOP 是一个规范,是动态代理的一个规范化,一个标准 AOP 的技术实现框架有以下两种:

  1. Spring (不用此方式)
    • spring 在内部实现了 aop 规范,能做 aop 的工作。
    • spring 主要在事务处理时使用 aop。
    • 我们项目开发中很少使用 spring 的 aop 实现!因为 spring 的 aop 比较笨重。
  2. aspectJ(掌握)
    • 一个开源的专门做 aop 的框架。
    • spring 框架中集成了 aspectj 框架,通过 spring 就能使用 aspectj 的功能。

aspectJ 框架实现 aop 有两种方式:

  1. 使用 xml 的配置文件 :配置全局事务
  2. 使用注解

我们在项目中要做 aop 功能,一般都使用注解,aspectj 有5个注解。

重点:一般使用 aspectJ 的 AOP 实现,不用 Spring 的。

aspectJ 框架实现 AOP

AOP 主要通过注解方式实现!做事务则通过配置的方式!

明确切面三个要素

  1. 执行时间:Advice(通知,增强)
  2. 执行位置:Pointcut(切入点)
  3. 功能代码:切面功能(自己写的非业务功能代码)

切面三个关键的要素复述:

  1. 切面的功能代码,切面干什么(我们自己写的功能代码)
  2. 切面的执行位置,使用 Pointcut 表示切面执行的位置
  3. 切面的执行时间,使用 Advice 表示时间,在目标方法之前,还是目标方法之后

切面的执行时间:这个执行时间在规范中叫做 Advice (通知,增强),在 aspectj 框架中使用注解表示的;也可以使用 xml 配置文件中的标签。

  1. @Before:前置通知【掌握】
  2. @AfterReturning:后置通知【掌握】
  3. @Around:环绕通知【掌握】
  4. @AfterThrowing:异常通知【了解】
  5. @After:最终通知【了解】

切入点表达式:表示执行位置(理解下列举例的五个使用即可)

注解:@Pointcut 定义切入点【掌握】

image-20220316123046528

image-20220316123124780

image-20220316123149371

简述实现步骤

此处大概演示前置通知的使用,其他通知或切入点表达式的写法见后续笔记。

主要看下固定的实现步骤!

注意:如果切入点表达式无法匹配到目标方法,则既不会创建动态代理对象,也不会有功能增强!

  1. 添加 spring-context,spring-aspects、junit 依赖

    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.11</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>5.2.5.RELEASE</version>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-aspects</artifactId>
        <version>5.2.5.RELEASE</version>
    </dependency>
    
  2. 创建目标接口以及目标接口实现类

    public interface SomeService {
        void doSome(String name,Integer age);
    }
    
    public class SomeServiceImpl implements SomeService {
        @Override
        public void doSome(String name, Integer age) {
            System.out.println("doSome method execute!");
        }
    }
    
  3. 创建切面类,加 @Aspect 注解;创建切面方法,添加相关 Advice 通知,并指定切入点表达式;在方法中写切面功能

    @Aspect
    public class MyAspect {
        // 完全格式的切入点表达式示例[访问权限 返回值 包名+类名+方法名(参数)] 必须:返回值、方法名(参数)
        // @Before("execution(public void com.luis.service.impl.SomeServiceImpl.doSome(String,Integer))")
        // @Before("execution(void com.luis.service.impl.SomeServiceImpl.doSome(String,Integer))")
        // @Before("execution(void *..SomeServiceImpl.doSome(String,Integer))")
        // @Before("execution(* *..SomeServiceImpl.do*(..))")
        @Before("execution(* do*(..))") // 必须有:返回值、方法名(参数)
        public void addDate() {
            System.out.println("前置通知:时间 " + new Date());
        }
    }
    
  4. 在配置文件中将对象交给容器创建或使用注解创建(使用注解的话记得声明组件扫描器)

  5. 在配置文件中声明 aspectj 的自动代理生成器

    <bean id="someService" class="com.luis.service.impl.SomeServiceImpl"/>
    <bean id="myAspect" class="com.luis.aspect.MyAspect"/>
    <!--声明自动代理生成器-->
    <aop:aspectj-autoproxy/>
    
  6. 测试,获取修改后的目标类对象(也是动态代理类)执行相关方法

    @Test
    public void test() {
        ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
        SomeService proxy = (SomeService) context.getBean("someService");
        System.out.println(proxy.getClass().getName());
        proxy.doSome("name", 22);
    }
    

    测试结果:

    com.sun.proxy.$Proxy8
    前置通知:时间 Wed Mar 16 14:12:22 CST 2022
    doSome method execute!
    

JoinPoint(掌握)

注意:JoinPoint 只可使用在切面类中切面方法/通知方法的第一个形参位置上

使用位置:只可使用在切面类中切面方法/通知方法的第一个形参位置上

作用:可以在通知方法中获取方法执行时的信息,例如方法名,方法实参等

注意:

  • 如果切面功能中需要用到方法的信息,就可以通过加入 JoinPoint 参数获取
  • JoinPoint 参数的值是由框架赋予,必须是第一个位置的参数!

使用示例:

@Aspect
@Component("myAspect")
public class MyAspect {
    @Before("execution(* do*(..))")
    public void addDate(JoinPoint jp) {
        System.out.println("前置通知:时间 " + new Date());
        System.out.println("方法签名:" + jp.getSignature());
        System.out.println("方法名:" + jp.getSignature().getName());
        Object[] args = jp.getArgs();
        for (Object arg : args) {
            System.out.println("方法参数:" + arg);
        }
    }
}

打印内容:

com.sun.proxy.$Proxy14
前置通知:时间 Wed Mar 16 15:35:37 CST 2022
方法签名:void com.luis.service.SomeService.doSome(String,Integer)
方法名:doSome
方法参数:name
方法参数:22
doSome method execute!

@Before(掌握)

前置通知方法定义格式:

  1. 公共方法 public
  2. 方法没返回值
  3. 方法名称自定义
  4. 方法可以有参数,也可以没有参数;如果有参数,参数不是自定义的,有几个参数可以使用;如必须放在第一个位置上的 JoinPoint 参数,可以获取方法名,方法参数等信息。

前置通知注解:@Before

属性:value,是切入点表达式,表示切面的功能执行的位置

使用位置:在切面方法上面

特点:

  1. 在目标方法之前先执行
  2. 不会改变目标方法的执行结果
  3. 不会影响目标方法的执行

示例:

@Before("execution(* doSome(..))")
public void addDate(JoinPoint jp) {
    System.out.println("前置通知:时间 " + new Date());
    System.out.println("方法签名:" + jp.getSignature());
    System.out.println("方法名:" + jp.getSignature().getName());
    Object[] args = jp.getArgs();
    for (Object arg : args) {
        System.out.println("方法参数:" + arg);
    }
}

@AfterReturning(掌握)

后置通知方法定义格式:

  1. 公共方法 public
  2. 方法没返回值
  3. 方法名称自定义
  4. 方法必须有一个参数,参数名自定义,推荐写 Object!
  5. 可以添加必须放在第一个位置上的 JoinPoint 参数,可以获取方法名,方法参数等信息。

后置通知注解:@AfterReturning

属性:

  1. value:切入点表达式
  2. returning:自定义的变量,表示目标方法的返回值的;该自定义变量名必须和通知方法的形参名一样。

位置:在方法定义的上面

特点:

  1. 在目标方法之后执行的
  2. 能够获取到目标方法的返回值,可以根据这个返回值做不同的处理功能
  3. 可以修改这个返回值

示例:

@AfterReturning(value = "execution(* *..SomeServiceImpl.doOther(..))", returning = "res")
public void myAfterReturning(JoinPoint jp, Object res) {
    System.out.println("后置通知:执行相关事务");
    if (res instanceof Student) {
        Student s = (Student) res;
        s.setName("newName");
        s.setAge(11);
        res = s;
    }
}

@Around(掌握)

环绕通知是功能最强的通知!

环绕通知方法定义格式:

  1. public
  2. 必须有一个返回值,推荐使用Object
  3. 方法名称自定义
  4. 方法有参数,固定的参数: ProceedingJoinPoint(JoinPoint 是其父接口)

环绕通知注解:@Around

属性:value,切入点表达式

位置:在方法的定义上面

特点:

  1. 它是功能最强的通知
  2. 在目标方法的前和后都能增强功能
  3. 可控制目标方法是否被调用执行
  4. 可修改原来的目标方法的执行结果,直接影响最后的调用结果

环绕通知,等同于 jdk 动态代理的 InvocationHandler 接口

参数: ProceedingJoinPoint

  • 等同于 Method
  • 作用就是执行目标方法

返回值: 就是目标方法的执行结果,可以被修改。

环绕通知: 经常做事务, 在目标方法之前开启事务,执行目标方法, 在目标方法之后提交事务。

示例:

@Around(value = "execution(* *..SomeServiceImpl.doAround(..))")
public Object myAround(ProceedingJoinPoint pjp) throws Throwable {

    System.out.println("环绕通知:目标方法执行前开启事务");
    // 执行目标方法;相当于动态代理的 Method.invoke()
    Object result = pjp.proceed(); // 在此处可以控制目标方法是否可调用
    System.out.println("环绕通知:目标方法执行后关闭事务");

    // 可以修改返回结果
    if (result != null) {
        result = "new modify";
    }
    return result;
}

测试结果:

com.sun.proxy.$Proxy18
环绕通知:目标方法执行前开启事务
目标方法 doAround 执行了
环绕通知:目标方法执行后关闭事务
new modify

@AfterThrowing

了解即可,实际开发中很少用!

/**
 *  @Aspect : 是aspectj框架中的注解。
 *     作用:表示当前类是切面类。
 *     切面类:是用来给业务方法增加功能的类,在这个类中有切面的功能代码
 *     位置:在类定义的上面
 */
@Aspect
public class MyAspect {

    /**
     * 异常通知方法的定义格式
     *  1.public
     *  2.没有返回值
     *  3.方法名称自定义
     *  4.方法有个一个Exception, 如果还有是JoinPoint
     */

    /**
     * @AfterThrowing:异常通知
     *     属性:1. value 切入点表达式
     *          2. throwinng 自定义的变量,表示目标方法抛出的异常对象。
     *             变量名必须和方法的参数名一样
     * 特点:
     *   1. 在目标方法抛出异常时执行的
     *   2. 可以做异常的监控程序, 监控目标方法执行时是不是有异常。
     *      如果有异常,可以发送邮件,短信进行通知
     *
     *  执行就是:
     *   try{
     *       SomeServiceImpl.doSecond(..)
     *   }catch(Exception e){
     *       myAfterThrowing(e);
     *   }
     */
    @AfterThrowing(value = "execution(* *..SomeServiceImpl.doSecond(..))", throwing = "ex")
    public void myAfterThrowing(Exception ex) {
        System.out.println("异常通知:方法发生异常时,执行:"+ex.getMessage());
        //发送邮件,短信,通知开发人员
    }
}

@After

了解即可,实际开发中很少用!

/**
 *  @Aspect : 是aspectj框架中的注解。
 *     作用:表示当前类是切面类。
 *     切面类:是用来给业务方法增加功能的类,在这个类中有切面的功能代码
 *     位置:在类定义的上面
 */
@Aspect
public class MyAspect {

    /**
     * 最终通知方法的定义格式
     *  1.public
     *  2.没有返回值
     *  3.方法名称自定义
     *  4.方法没有参数,  如果还有是JoinPoint,
     */

    /**
     * @After :最终通知
     *    属性: value 切入点表达式
     *    位置: 在方法的上面
     * 特点:
     *  1.总是会执行
     *  2.在目标方法之后执行的
     *
     *  try{
     *      SomeServiceImpl.doThird(..)
     *  }catch(Exception e){
     *
     *  }finally{
     *      myAfter()
     *  }
     *
     */
    @After(value = "execution(* *..SomeServiceImpl.doThird(..))")
    public  void  myAfter(){
        System.out.println("执行最终通知,总是会被执行的代码");
        //一般做资源清除工作的。
    }
}

@Pointcut

辅助功能注解,定义管理切入点表达式,可实现切入点表达式复用。

/**
 *  @Aspect : 是aspectj框架中的注解。
 *     作用:表示当前类是切面类。
 *     切面类:是用来给业务方法增加功能的类,在这个类中有切面的功能代码
 *     位置:在类定义的上面
 */
@Aspect
public class MyAspect {

    @After(value = "mypt()")
    public  void  myAfter(){
        System.out.println("执行最终通知,总是会被执行的代码");
        //一般做资源清除工作的。
    }

    @Before(value = "mypt()")
    public  void  myBefore(){
        System.out.println("前置通知,在目标方法之前执行~");
    }

    /**
     * @Pointcut: 定义和管理切入点, 如果你的项目中有多个切入点表达式是重复的,可以复用的。
     *            可以使用@Pointcut
     *    属性:value 切入点表达式
     *    位置:在自定义的方法上面
     * 特点:
     *   当使用@Pointcut定义在一个方法的上面 ,此时这个方法的名称就是切入点表达式的别名。
     *   其它的通知中,value属性就可以使用这个方法名称,代替切入点表达式了
     */
    @Pointcut(value = "execution(* *..SomeServiceImpl.doThird(..))")
    private void mypt(){
        //无需代码
    }
}

关于使用的是 jdk 动态代理还是 cglib 动态代理

声明自动代理生成器:使用aspectj框架内部的功能,创建目标对象的代理对象。
创建代理对象是在内存中实现的, 修改目标对象的内存中的结构。 创建为代理对象,
所以目标对象就是被修改后的代理对象。
aspectj-autoproxy:会把spring容器中的所有的目标对象,一次性都生成代理对象。

<aop:aspectj-autoproxy /> 
目标对象有接口时,默认使用jdk动态代理方式
目标对象无接口时,会使用cglib动态代理方式

目标对象有接口,想强制使用cglib动态代理方式,
声明自动代理生成器时这样设置即可:(这种方式,程序执行时效率会稍微高一些)
<aop:aspectj-autoproxy proxy-target-class="true" />

Spring 集成 Mybatis

前言:

把 mybatis 框架和 spring 集成在一起,像一个框架一样使用。

用的技术是:ioc

为什么 ioc 能把 mybatis 和 spring 集成在一起,像一个框架, 是因为 ioc 能创建对象。 可以把 mybatis 框架中的对象交给spring 统一创建, 开发人员从 spring 中获取对象。 开发人员就不用同时面对两个或多个框架了, 就面对一个 spring 即可。

mybatis 中要使用 dao 对象,需要使用 getMapper() 方法, 怎么能使用 getMapper() 方法,需要哪些条件 ?

  1. 获取SqlSession 对象, 需要使用 SqlSessionFactory 的 openSession() 方法。
  2. 创建 SqlSessionFactory 对象,通过读取 mybatis 的主配置文件,才能创建 SqlSessionFactory 对象 。

需要 SqlSessionFactory 对象, 使用 Factory 能获取 SqlSession ,有了 SqlSession 就能有 dao , 目的就是获取 dao 对象 ,Factory 创建需要读取主配置文件。

我们会使用独立的连接池类替换 mybatis 默认自己带的, 把连接池类也交给 spring 创建。

通过以上的说明,我们需要让 spring 创建以下对象

  1. 独立的连接池类的对象,使用阿里的 druid 连接池
  2. SqlSessionFactory 对象
  3. 创建出 dao 对象

需要学习就是上面三个对象的创建语法,使用 xml 的 bean 标签。

<语法都是固定的,没有什么技术含量,容易学!!!>

基本整合步骤参考:

  1. 创建 maven 项目,完善目录结构

  2. 导入所需依赖和资源插件并 reload 项目

    <dependencies>
        <!--单元测试依赖-->
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.11</version>
            <scope>test</scope>
        </dependency>
        <!--spring核心ioc-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>5.2.5.RELEASE</version>
        </dependency>
        <!--spring做事务用到的-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-tx</artifactId>
            <version>5.2.5.RELEASE</version>
        </dependency>
        <!--spring做事务用到的-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-jdbc</artifactId>
            <version>5.2.5.RELEASE</version>
        </dependency>
        <!--mybatis依赖-->
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis</artifactId>
            <version>3.5.1</version>
        </dependency>
        <!--mybatis和spring集成的依赖-->
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis-spring</artifactId>
            <version>1.3.1</version>
        </dependency>
        <!--mysql驱动的依赖-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.9</version>
        </dependency>
        <!--阿里公司的数据库连接池(实际开发常用)(替代mybatis自带的连接池)-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.1.12</version>
        </dependency>
    </dependencies>
    
    <build>
        <!--目的是把crc/main/java目录中的.xml和.properties文件包含到输出结果中,输出到classes目录中-->
        <resources>
            <resource>
                <directory>src/main/java</directory><!--所在的目录-->
                <includes><!--包括目录下的.properties,.xml 文件都会扫描到-->
                    <include>**/*.properties</include>
                    <include>**/*.xml</include>
                </includes>
                <filtering>false</filtering>
            </resource>
            <resource>
                <directory>src/main/resources</directory><!--所在的目录-->
                <includes><!--包括目录下的.properties,.xml 文件都会扫描到-->
                    <include>**/*.properties</include>
                    <include>**/*.xml</include>
                </includes>
                <filtering>false</filtering>
            </resource>
        </resources>
    </build>
    
  3. 准备数据库测试用表,创建对应实体类

    public class User {
        private Integer id;
        private String name;
        private String pwd;
        // 以下省略
    }
    
  4. 创建 dao 接口和对应 mapper 文件

    public interface UserDao {
        int insertUser(User user);
        List<User> selectUsers();
    }
    
    <?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.luis.dao.UserDao">
        <insert id="insertUser">
            insert into user values(#{id},#{name},#{pwd})
        </insert>
        <select id="selectUsers" resultType="com.luis.domain.User">
            select id,name,pwd from user order by id desc
        </select>
    </mapper>
    
  5. 创建 service 接口以及其实现类

    public interface UserService {
        int addUser(User user);
        List<User> queryUsers();
    }
    
    public class UserServiceImpl implements UserService {
        private UserDao userDao;
        // 必须有此set方法,set注入会用到
        public void setUserDao(UserDao userDao) {
            this.userDao = userDao;
        }
        @Override
        public int addUser(User user) {
            return userDao.insertUser(user);
        }
        @Override
        public List<User> queryUsers() {
            return userDao.selectUsers();
        }
    }
    
  6. 创建并配置 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>
        <!--settings:控制mybatis全局行为-->
        <settings>
            <!--设置mybatis输出日志-->
            <setting name="logImpl" value="STDOUT_LOGGING"/>
        </settings>
        <!--设置别名-->
        <typeAliases>
            <!--这样设置后,实体类的类名就是该类全限定名称的别名,用别名代表了包名+类名-->
            <package name="com.luis.domain"/>
        </typeAliases>
        <!-- sql mapper(sql映射文件)的位置-->
        <mappers>
            <!--name:代表包名,这个包中所有mapper.xml一次都能加载,不用一个个配置了-->
            <package name="com.luis.dao"/>
        </mappers>
    </configuration>
    
  7. 创建并配置 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"
           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
           https://www.springframework.org/schema/context/spring-context.xsd">
    
        <!--
            把数据库的配置信息,写到一个独立的文件,编译修改数据库的配置内容。
            让spring知道jdbc.properties文件的位置!
        -->
        <context:property-placeholder location="classpath:jdbc.properties"/>
        
        <!--声明数据源DateSource,作用是连接数据库的(替代mybatis中的数据源)-->
        <bean id="myDataSource" class="com.alibaba.druid.pool.DruidDataSource"
              init-method="init" destroy-method="close">
            <!--
                使用属性配置文件的数据,语法:
            -->
            <!--set注入给DruidDataSource提供连接数据库信息-->
            <property name="url" value="${jdbc.url}"/><!--setUrl-->
            <property name="username" value="${jdbc.username}"/><!--setUsername-->
            <property name="password" value="${jdbc.password}"/><!--setPassword-->
            <property name="maxActive" value="${jdbc.max}"/><!--最大连接数20-->
        </bean>
    
        <!--声明mybatis中提供的SqlSessionFactoryBean类,这个类内部创建SqlSessionFactory对象-->
        <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
            <!--set注入,把数据库连接池赋给了dateSource属性-->
            <property name="dataSource" ref="myDataSource"/>
            <!--
                mybatis主配置文件的位置
                configLocation属性是Resource类型,读取配置文件
                它的赋值,使用value,指定文件的路径,使用classpath:表示文件的位置
            -->
            <property name="configLocation" value="classpath:mybatis.xml"/>
        </bean>
    
        <!--
            创建dao对象,使用SqlSession的getMapper(StudentDao.class)
            MapperScannerConfigurer:在内部调用getMapper()生成每个dao接口的代理对象。
        -->
        <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
            <!--指定SqlSessionFactory对象的id-->
            <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/>
            <!--
                指定包名,包名是dao接口所在的包名。
                MapperScannerConfigurer会扫描这个包中所有接口,把每个接口都执行
                一次getMapper()方法,得到每个接口的dao对象。
                创建好的dao对象放入到spring的容器中。
                dao对象的默认名称:是接口名的首字母小写。
            -->
            <property name="basePackage" value="com.luis.dao"/>
        </bean>
    
        <!--声明service-->
        <bean id="userService" class="com.luis.service.impl.UserServiceImpl">
            <property name="userDao" ref="userDao"/>
        </bean>
    </beans>
    
  8. 创建连接数据库的属性配置文件

    jdbc.url=jdbc:mysql://localhost:3306/mybatis
    jdbc.username=root
    jdbc.password=luis
    jdbc.max=20
    
  9. 创建测试类,测试

    @Test
    public void test() {
        ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
        UserService userService = (UserService) context.getBean("userService");
        // 插入数据测试:
        int res = userService.addUser(new User(222, "luis", "223"));
        System.out.println(res);
        // 查询数据测试:
        // List<User> users = userService.queryUsers();
        // for (User user : users) {
        //     System.out.println(user);
        // }
    }
    

Spring 的事务处理

前言

回答问题
1.什么是事务
  讲mysql的时候,提出了事务。 事务是指一组sql语句的集合, 集合中有多条sql语句
  可能是insert , update ,select ,delete, 我们希望这些多个sql语句都能成功,
  或者都失败, 这些sql语句的执行是一致的,作为一个整体执行。

2.在什么时候想到使用事务
  当我的操作,涉及得到多个表,或者是多个sql语句的insert,update,delete。需要保证
  这些语句都是成功才能完成我的功能,或者都失败,保证操作是符合要求的。

  在java代码中写程序,控制事务,此时事务应该放在那里呢? 
     service类的业务方法上,因为业务方法会调用多个dao方法,执行多个sql语句

3.通常使用JDBC访问数据库, 还是mybatis访问数据库怎么处理事务
	jdbc访问数据库,处理事务  Connection conn ; conn.commit(); conn.rollback();
	mybatis访问数据库,处理事务, SqlSession.commit();  SqlSession.rollback();
	hibernate访问数据库,处理事务, Session.commit(); Session.rollback();

4.3问题中事务的处理方式,有什么不足
  1) 不同的数据库访问技术,处理事务的对象,方法不同,需要了解不同数据库访问技术使用事务的原理
  2) 掌握多种数据库中事务的处理逻辑。什么时候提交事务,什么时候回顾事务
  3) 处理事务的多种方法。

总结: 就是多种数据库的访问技术,有不同的事务处理的机制,对象,方法。
    
5.怎么解决不足
  spring提供一种处理事务的统一模型, 能使用统一步骤,方式完成多种不同数据库访问技术的事务处理。

  使用spring的事务处理机制,可以完成mybatis访问数据库的事务处理
  使用spring的事务处理机制,可以完成hibernate访问数据库的事务处理。

6.处理事务,需要怎么做,做什么
  spring处理事务的模型,使用的步骤都是固定的。把事务使用的信息提供给spring就可以了

  1)spring内部提交,回滚事务,使用的事务管理器对象,代替你完成commit,rollback
     事务管理器是一个接口和他的众多实现类。
	  接口:PlatformTransactionManager ,定义了事务重要方法 commit ,rollback
	  实现类:spring把每一种数据库访问技术对应的事务处理类都创建好了。
    	mybatis访问数据库---spring创建好的是DataSourceTransactionManager
    	hibernate访问数据库----spring创建的是HibernateTransactionManager

     怎么使用:你需要告诉spring 你用是那种数据库的访问技术,怎么告诉spring呢?
	  声明数据库访问技术对应的事务管理器实现类, 在spring的配置文件中使用<bean>声明就可以了
	  例如,你要使用mybatis访问数据库,你应该在xml配置文件中
	  <bean id=“xxx" class="...DataSourceTransactionManager"> 

    2)你的业务方法需要什么样的事务,说明需要事务的类型。
         说明方法需要的事务:
            1)事务的隔离级别:有4个值。
            DEFAULT:采用 DB 默认的事务隔离级别。
    		MySql 的默认为 REPEATABLE_READ; Oracle默认为 READ_COMMITTED。
            ➢ READ_UNCOMMITTED:读未提交。未解决任何并发问题。
            ➢ READ_COMMITTED:读已提交。解决脏读,存在不可重复读与幻读。【oracle默认】
            ➢ REPEATABLE_READ:可重复读。解决脏读、不可重复读,存在幻读【mysql默认】
            ➢ SERIALIZABLE:串行化。不存在并发问题。

          2) 事务的超时时间: 表示一个方法最长的执行时间,如果方法执行时超过了时间,事务就回滚。
              单位是秒, 整数值, 默认是 -1. (-1表示没有时间限制)

           3)事务的传播行为 : 控制业务方法是不是有事务的, 是什么样的事务的。
                7个传播行为,表示你的业务方法调用时,事务在方法之间是如果使用的。

                PROPAGATION_REQUIRED
                PROPAGATION_REQUIRES_NEW
                PROPAGATION_SUPPORTS
                以上三个需要掌握的
       --------------------------------------
               以上三个的小写形式参考:
                propagation_required
                propagation_requires_new
                propagation_supports
    --------------------------------------------

                PROPAGATION_MANDATORY
                PROPAGATION_NESTED
                PROPAGATION_NEVER
                PROPAGATION_NOT_SUPPORTED

    1) PROPAGATION_REQUIRED:
    指定的方法必须在事务内执行。若当前存在事务,就加入到当前事务中;若当前没有事务,
    则创建一个新事务。这种传播行为是最常见的选择,也是 Spring 默认的事务传播行为。 
    如该传播行为加在 doOther()方法上。若 doSome()方法在调用 doOther()方法时就是在事务内运行的,
    则 doOther()方法的执行也加入到该事务内执行。若 doSome()方法在调用 doOther()方法时没有在事务内执行,
    则 doOther()方法会创建一个事务,并在其中执行。
    2) PROPAGATION_SUPPORTS
    指定的方法支持当前事务,但若当前没有事务,也可以以非事务方式执行。
    3)PROPAGATION_REQUIRES_NEW
    总是新建一个事务,若当前存在事务,就将当前事务挂起,直到新事务执行完毕。
    
3)事务提交事务,回滚事务的时机
     1)当你的业务方法,执行成功,没有异常抛出,当方法执行完毕,spring在方法执行后提交事务。
         调用事务管理器的commit
	 
	  2)当你的业务方法抛出运行时异常或ERROR, spring执行回滚,调用事务管理器的rollback
	     运行时异常的定义: RuntimeException  和他的子类都是运行时异常,
       例如NullPointException , NumberFormatException
	  
	  3) 当你的业务方法抛出非运行时异常, 主要是受查异常时,提交事务
        受查异常:在你写代码中,必须处理的异常。例如IOException, SQLException

---------------------------------------------------------------------------------
总结spring的事务:
  1.管理事务的是:事务管理器和他的实现类
  2.spring的事务是一个统一模型
      1)指定要使用的事务管理器实现类,使用<bean>
	  2)指定哪些类,哪些方法需要加入事务的功能
	  3)指定方法需要的隔离级别,传播行为,超时

  你需要告诉spring,你的项目中类信息,方法的名称,方法的事务传播行为。

例子演示参见链接:https://www.bilibili.com/video/BV1nz4y1d7uy?p=85

下面直接写 Spring 中事务的处理步骤。


事务处理实现-注解方式

Spring 自己的事务处理——通过注解方式

注解方式使用场景:中小型项目

实际项目都比较大,则用的比较少:方法多,需要加的注解多,而且容易遗漏。

通过事务回滚可以撤销对数据库的操作,从主键设置成自动增长的,可以看出事务是否回滚。
它会生成你的代理对象,它的机制是aop的环绕通知。

    spring使用aop机制,创建@Transactional所在的类代理对象,给方法加入事务的功能。
    spring给业务方法加入事务:
	    在你的业务方法执行之前,先开启事务,在业务方法之后提交或回滚事务,使用aop的环绕通知
       底层实现原理:
		 @Around("你要增加的事务功能的业务方法名称")
		 Object myAround(){
           开启事务,spring给你开启
			  try{
			     buy(1001,10);
				  spring的事务管理器.commit();
			  }catch(Exception e){
             spring的事务管理器.rollback();
			  }	 
		 }
============================重点=======================================   
1、@Transactional需要加在公共方法public上
2、可以使用在类上,表示该类中所有公共方法都具有事务(但意义不大,一般开发中都是使用在需要有事务的public方法上)
2、直接写 @Transactional 即可,属性全使用默认值,最简单!!!(开发中常用此方法,简单快捷!)
3、加上@Transactional事务注解后,该注解所在的类就变成了代理了(spring内部实现)
4、适合中小项目
========================================================================

注解方式使用场景:中小型项目

使用步骤:

  1. 在 Spring 主配置中声明事务管理器对象

    注意:

    1. 不同数据库需要声明的事务管理器对象不同,MySQL 需要声明 DataSourceTransactionManager 对象
    2. 在声明事务管理器中,还需要指定数据源
    3. IDEA 中可通过输入 DSTM,快速找到 DataSourceTransactionManager
  2. 在 Spring 主配置中开启事务注解驱动

    注意:

    1. annotation-driven 注解来源有好几个,需要使用 tx 结尾的,即事务专用的!
    2. 需要配置 transaction-manager,事务管理器对象
<!--  1.声明事务管理器对象:DataSourceTransactionManager 快捷简拼 DSTM  -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <!--需配置数据源-->
    <property name="dataSource" ref="myDataSource"/>
</bean>
<!--  2.开启事务注解驱动:注意使用 tx 结尾事务专用注解  -->
<tx:annotation-driven transaction-manager="transactionManager"/>
  1. 在需要使用事务处理的方法上添加 @Transactional 注解即可

    使用要求:该方法需要是 public

注意:直接在需要事务处理的方法上加@Transactional注解即可,不用配置,使用默认参数即可!

下面附 @Transactional 配置格式,仅供参考,其实不用配置,直接添加注解即可,全部使用默认的配置。

@Transactional(
    propagation = Propagation.REQUIRED, // 传播行为
    isolation = Isolation.DEFAULT, // 隔离级别
    readOnly = false, // 是否只读
    rollbackFor = { // 遇到下列异常类自动回滚
        NullPointerException.class,
        NotEnoughException.class
            }
)
public void buy(Integer goodsId, Integer nums) {
}        

再附 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"
       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/context
       https://www.springframework.org/schema/context/spring-context.xsd 
       http://www.springframework.org/schema/tx 
       http://www.springframework.org/schema/tx/spring-tx.xsd">

    <!--声明组件扫描器,扫描注解所在的包-->
    <context:component-scan base-package="com.luis.service.impl"/>

    <!--
        把数据库的配置信息,写到一个独立的文件,编译修改数据库的配置内容。
        让spring知道jdbc.properties文件的位置!
    -->
    <context:property-placeholder location="classpath:jdbc.properties"/>

    <!--声明数据源DateSource,作用是连接数据库的(替代mybatis中的数据源)-->
    <bean id="myDataSource" class="com.alibaba.druid.pool.DruidDataSource"
          init-method="init" destroy-method="close">
        <!--
            使用属性配置文件的数据,语法:
        -->
        <!--set注入给DruidDataSource提供连接数据库信息-->
        <property name="url" value="${jdbc.url}"/><!--setUrl-->
        <property name="username" value="${jdbc.username}"/><!--setUsername-->
        <property name="password" value="${jdbc.password}"/><!--setPassword-->
        <property name="maxActive" value="${jdbc.max}"/><!--最大连接数20-->
    </bean>

    <!--声明mybatis中提供的SqlSessionFactoryBean类,这个类内部创建SqlSessionFactory对象-->
    <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
        <!--set注入,把数据库连接池赋给了dateSource属性-->
        <property name="dataSource" ref="myDataSource"/>
        <!--
            mybatis主配置文件的位置
            configLocation属性是Resource类型,读取配置文件
            它的赋值,使用value,指定文件的路径,使用classpath:表示文件的位置
        -->
        <property name="configLocation" value="classpath:mybatis.xml"/>
    </bean>

    <!--
        创建dao对象,使用SqlSession的getMapper(StudentDao.class)
        MapperScannerConfigurer:在内部调用getMapper()生成每个dao接口的代理对象。
    -->
    <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
        <!--指定SqlSessionFactory对象的id-->
        <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/>
        <!--
            指定包名,包名是dao接口所在的包名。
            MapperScannerConfigurer会扫描这个包中所有接口,把每个接口都执行
            一次getMapper()方法,得到每个接口的dao对象。
            创建好的dao对象放入到spring的容器中。
            dao对象的默认名称:是接口名的首字母小写。
        -->
        <property name="basePackage" value="com.luis.dao"/>
    </bean>

    <!--声明service-->
<!--    <bean id="buyGoodsService" class="com.luis.service.impl.BuyGoodsServiceImpl">-->
<!--        <property name="saleDao" ref="saleDao"/>-->
<!--        <property name="goodsDao" ref="goodsDao"/>-->
<!--    </bean>-->

    <!--  1.声明事务管理器对象:DSTM  -->
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="myDataSource"/>
    </bean>
    <!--  2.开启事务注解驱动:注意使用 tx 结尾事务专用注解  -->
    <tx:annotation-driven transaction-manager="transactionManager"/>

</beans>

AspectJ 事务处理-XML

AspectJ 事务处理方式——通过配置文件实现

  • 适合大型项目,有很多的类,方法,需要大量的配置事务,使用 aspectj 框架功能,在 spring 配置文件中声明类,方法需要的事务。
  • 这种方式业务方法和事务配置完全分离。

此配置方式使用场景:大型项目

实际开发中,用得非常多!!!

使用步骤:

  1. 添加 aspectJ 依赖

    <!--aspectj依赖-->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-aspects</artifactId>
        <version>5.2.5.RELEASE</version>
    </dependency>
    
  2. 声明事务管理器对象

  3. 声明事务方法的事务属性(隔离级别,传播行为,超时时间)

  4. 配置 aop

    <!--声明式事务管理,和源代码完全分离的-->
    <!--1、声明事务管理器对象-->
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="myDataSource" />
    </bean>
    
    <!--
        2、声明事务方法的事务属性(隔离级别,传播行为,超时时间)
        id:自定义名称,表示 <tx:advice> 和 </tx:advice> 之间的配置内容的。
        transaction-manager:事务管理器对象的id
    -->
    <tx:advice id="myAdvice" transaction-manager="transactionManager">
        <!--tx:attributes 配置事务属性-->
        <tx:attributes>
            <!--
                tx:method 给具体的方法配置事务属性,method可以有多个,分别给不同的方法设置事务属性
                name:写需要配置事务的方法名称
                    1)完整的方法名称,不带有包和类
                    2)方法可以使用通配符,* 表示任意字符
                propagation:传播行为,枚举值
                isolation:隔离级别
                rollback-for:你指定的异常类名,全限定类名。发生异常一定会回滚。
            -->
            <tx:method name="buy" propagation="REQUIRED" isolation="DEFAULT"
                       rollback-for="java.lang.NullPointerException,com.linwei.excep.NotEnoughException" />
            
            <!--使用通配符,可以指定很多的方法-->
            <!--指定添加方法-->
            <tx:method name="add*" propagation="REQUIRES_NEW" />
            <!--指定修改方法-->
            <tx:method name="modify*" />
            <!--指定删除方法-->
            <tx:method name="remove*" />
            <!--指定查询方法,query,search,find-->
            <!--此处 * 是指定以上方法之外的方法-->
            <tx:method name="*" propagation="SUPPORTS" read-only="true" />
    
            <!--spring中进行事务匹配优先级:先匹配有完全方法名的,再匹配带通配符的,再匹配只有* 的。-->
        </tx:attributes>
    </tx:advice>
    
    <!--3、配置aop 以上1和2步骤指定了哪些方法需要配置事务,并无法指定哪些类,哪些包,所以需要再配置以下内容-->
    <aop:config>
        <!--
            配置切入点表达式:指定哪些包中的类要使用事务
            id:切入点表达式的名称,唯一值。
            expression:切入点表达式,指定哪些类要使用事务,aspectj会创建代理对象
    
            com.linwei.service
            com.crm.service
            com.service
            。。。
        -->
        <aop:pointcut id="servicePt" expression="execution(* *..service..*.*(..))" />
    
        <!--配置增强器:关联advice和pointcut-->
        <aop:advisor advice-ref="myAdvice" pointcut-ref="servicePt" />
    </aop:config>
    

Spring 中 web 项目处理

核心就是将 Spring 容器对象在全局作用域对象ServletContext创建时,进行创建,并放入全局作用域中,实现只创建一次!

web 项目是运行在 Tomcat 服务器上的,Tomcat 一启动,项目就一直运行。

如果是 web 项目,则需要根据不同请求,由 Servlet 自动从 Spring 容器中获取相应对象,进行相关方法调用。

而每次请求都会调用一次 Servlet 中的方法,也就会使容器对象重复创建,所以需要实现容器对象只创建一次!

重点总结:web 项目中容器对象只需要创建一次,需要将容器对象放到全局作用域 ServletContext 中。

如何实现 ?

  • 使用监听器,在全局作用域对象被创建的同时,创建容器对象,将其存入 ServletContext 中 !

监听器作用:

  1. 创建容器对象
  2. 将容器对象放入全局作用域对象 ServletContext

监听器可以自己创建,也可以使用框架提供的 ContextLoderLister

框架中的ContextLoderLister监听器对象已经实现了上述功能,它会在全局作用域对象被创建的同时,为我们创建容器对象,并将其存入 ServletContext 中 !

下面是配置和使用框架提供的ContextLoderLister监听器的步骤。

使用步骤:

  1. 添加使用监听器必要的依赖

    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-web</artifactId>
        <version>5.2.5.RELEASE</version>
    </dependency>
    
  2. 在 web.xml 中注册监听器 ContextLoderLister

    <!--注册监听器ContextLoaderListener
        监听器被创建对象后,会读取/WEB-INF/spring.xml
        为什么要读取文件:因为在监听器中要创建ApplicationContext对象,需要加载配置文件。
        /WEB-INF/applicationContext.xml就是监听器默认读取的spring配置文件路径
    
        可以修改默认的文件位置,使用context-param重新指定文件的位置
    
        配置监听器:目的是创建容器对象,创建了容器对象, 就能把spring.xml配置文件中的所有对象都创建好。
        用户发起请求就可以直接使用对象了。
    -->
    <context-param>
        <!--contextConfigLocation:表示配置文件的路径-->
        <param-name>contextConfigLocation</param-name>
        <!--自定义配置文件路径-->
        <param-value>classpath:applicationContext.xml</param-value>
    </context-param>
    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>
    
  3. 使用工具类获取容器对象

    ServletContext sc = getServletContext(); // 获取全局作用域对象
            // 通过工具类获取容器对象:WebApplicationContext extends ApplicationContext
            WebApplicationContext context = WebApplicationContextUtils.getRequiredWebApplicationContext(sc);
    

配置监听器:目的是创建容器对象,创建了容器对象, 就能把 spring.xml 配置文件中的所有对象都创建好, 用户发起请求就可以直接使用对象了。

posted @ 2022-11-01 23:22  luis林  阅读(18)  评论(0编辑  收藏  举报