Spring笔记(二)(黑马)(AOP面向切面编程)

01、AOP 简介

1.1 AOP的概念

AOP,Aspect Oriented Programming,面向切面编程,是对面向对象编程OOP的升华。OOP是纵向对一个事物的抽象,一个对象包括静态的属性信息,包括动态的方法信息等。而AOP是横向的对不同事物的抽象,属性与属性、方法与方法、对象与对象都可以组成一个切面,而用这种思维去设计编程的方式叫做面向切面编程
image.png

1.2 AOP思想的实现方案

动态代理技术,在运行期间,对目标对象的方法进行增强,代理对象同名方法内可以执行原有逻辑的同时嵌入执行其他增强逻辑或其他对象的方法
image.png

1.3 模拟AOP的基础代码

其实在之前学习BeanPostProcessor时,在BeanPostProcessor的after方法中使用动态代理对Bean进行了增 强,实际存储到单例池singleObjects中的不是当前目标对象本身,而是当前目标对象的代理对象Proxy,这样在调用目标对象方法时,实际调用的是代理对象Proxy的同名方法,起到了目标方法前后都进行增强的功能, 对该方式进行一下优化,将增强的方法提取出去到一个增强类中,且只对com.mem.service.impl包下的任何类的任何方法进行增强

  1. 引入坐标
  2. 编写目标对象userService
public interface UserService {
    void show1();
    void show2();
}

public class UserServiceImpl implements UserService {
    @Override
    public void show1() {
        System.out.println("show1 ...");
    }

    @Override
    public void show2() {
        System.out.println("show2 ...");
    }
}
  1. 编写增强(通知)对象myAdvice
// 增强类,内部提供增强方法
public class MyAdvice {
    public void beforeAdvice(){
        System.out.println("前置增强....");
    }

    public void afterReturningAdvice(){
        System.out.println("后置增强....");
    }
}
  1. 编写bean后处理器模拟AOP实现
public class MockAopBeanPostProcessor implements BeanPostProcessor , ApplicationContextAware {
    private ApplicationContext applicationContext = null;
    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        // 目的:对UserServiceImpl中的show1,show2方法进行增强,增强方法存在与MyAdvice中
        // 问题1:筛选service.impl包下所有类和所有方法多可以进行增强,解决方案:if-else
        // 问题2:MyAdvice怎么获取? 解决方案:从spring容器中获取
        if (bean.getClass().getPackage().getName().equals("com.mem.service.impl")){
            //生成当前Bean的代理对象
            Object proxyInstance = Proxy.newProxyInstance(
                    bean.getClass().getClassLoader(),
                    bean.getClass().getInterfaces(),
                    (Object proxy, Method method, Object[] args)->{
                        MyAdvice myAdvice = (MyAdvice) applicationContext.getBean("myAdvice");
                        //执行增强对象的前置方法
                        myAdvice.beforeAdvice();
                        //执行目标对象的目标方法
                        Object result = method.invoke(bean, args);
                        //执行增强对象的后置方法
                        myAdvice.afterReturningAdvice();
                        return result;
                    }
            );
            return proxyInstance;
        }else{
            return bean;
        }
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }
}
  1. 配置文件:载入容器
<bean id="myAdvice" class="com.mem.advice.MyAdvice"/>
<bean id="userService" class="com.mem.service.impl.UserServiceImpl"/>
<bean id="mockAopBeanPostProcessor" class="com.mem.processor.MockAopBeanPostProcessor"/>
  1. 测试
public class ApplicationContextTest {
    public static void main(String[] args) {
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
        UserService userService = (UserService) applicationContext.getBean("userService");
        userService.show1();
    }
}
// 输出
前置增强....
show1 ...
后置增强....

1.4 AOP相关概念

image.png

image.png

02、基于xml配置的AOP

2.1 xml方式AOP快速入门

前面我们自己编写的AOP基础代码还是存在一些问题的,主要如下:

  • 被增强的包名在代码写死了
  • 通知对象的方法在代码中写死了

image.png
通过配置文件的方式去解决上述问题

  • 配置哪些包、哪些类、哪些方法需要被增强 (涉及切点表达式的配置)
  • 配置目标方法要被哪些通知方法所增强,在目标方法执行之前还是之后执行增强(涉及织入的配置)

配置方式的设计、配置文件(注解)的解析工作,Spring已经帮我们封装好了

xml方式配置AOP的步骤:

  1. 导入AOP相关坐标;
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>1.9.6</version>
</dependency>

aspectj是实现AOP的一种实现方式

Spring-context坐标下已经包含spring-aop的包了,所以就不用额外导入了
image.png

  1. 准备目标类、准备增强(通知)类,并配置给Spring管理;
  • 目标类
public interface UserService {
    void show1();
    void show2();
}

public class UserServiceImpl implements UserService {
    @Override
    public void show1() {
        System.out.println("show1 ...");
    }

    @Override
    public void show2() {
        System.out.println("show2 ...");
    }
}
  • 通知类
// 增强类,内部提供增强方法
public class MyAdvice {
    public void beforeAdvice(){
        System.out.println("前置增强....");
    }

    public void afterReturningAdvice(){
        System.out.println("后置增强....");
    }
}
  • 配置文件(需引入aop的命名空间)
<bean id="myAdvice" class="com.mem.advice.MyAdvice"/>
<bean id="userService" class="com.mem.service.impl.UserServiceImpl"/>
  1. 配置切点表达式(哪些方法被增强);
  2. 配置织入(切点被哪些通知方法增强,是前置增强还是后置增强)。
  • 追加配置文件
<!--配置aop-->
<aop:config>
    <!--配置切点表达式,目的:指定哪些方法要被增强-->
    <aop:pointcut id="myPointcut" expression="execution(void com.mem.service.impl.UserServiceImpl.show*())"/>
    <!--配置织入,目的:指定哪些切点与哪些通知进行结合-->
    <aop:aspect id="" ref="myAdvice">
        <aop:before method="beforeAdvice" pointcut-ref="myPointcut"/>
        <aop:after-returning method="afterReturningAdvice" pointcut-ref="myPointcut"/>
    </aop:aspect>
</aop:config>
  1. 测试结果和上面模拟的效果一样

2.2 xml方式AOP配置详解

xml配置AOP的方式还是比较简单的,下面看一下AOP详细配置的细节:

  • 切点表达式的配置方式
  • 切点表达式的配置语法
  • 通知的类型
  • AOP的配置的两种方式

2.2.1 切点表达式的配置方式:

有两种

  • 直接将切点表达式配置在通知上
  • 可以将切点表达式抽取到外面,在通知上进行引用

image.png

2.2.2 切点表达式的配置语法

切点表达式是配置要对哪些连接点(哪些类的哪些方法)进行通知的增强,语法如下:
image.png
其中,

  • 访问修饰符可以省略不写;
  • 返回值类型、某一级包名、类名、方法名 可以使用 * 表示任意;
  • 包名与类名之间使用单点 . 表示该包下的类,使用双点..表示该包及其子包下的类;
  • 参数列表可以使用两个点 .. 表示任意参数。

切点表达式举几个例子方便理解
image.png

2.2.3 通知的类型

AspectJ的通知由以下五种类型
image.png

  • 前置和后置如上
  • 环绕通知

image.png

  • 异常通知,当目标方法抛出异常时,异常通知方法执行,且后置通知和环绕后通知不在执行

image.png

  • 最终通知,类似异常捕获中的finally,不管目标方法有没有异常,最终都会执行的通知

image.png
通知方法在被调用时,Spring可以为其传递一些必要的参数
image.png

  • JoinPoint 对象

image.png

  • ProceedingJoinPoint对象

image.png

  • Throwable对象

image.png

2.2.4 AOP的配置的两种方式

AOP的xml有两种配置方式,如下:

  • 使用<advisor>配置切面(详见之前的配置)
  • 使用<aspect>配置切面(如下)

Spring定义了一个Advice接口,实现了该接口的类都可以作为通知类出现
image.png
Advice的子功能接口
image.png

前置通知和后置通知接口
通知类实现了前置通知和后置通知接口

public class MyAdvice2 implements MethodBeforeAdvice, AfterReturningAdvice {
    @Override
    public void afterReturning(Object o, Method method, Object[] objects, Object o1) throws Throwable {
        System.out.println("后置通知*****");
    }

    @Override
    public void before(Method method, Object[] objects, Object o) throws Throwable {
        System.out.println("前置通知*****");
    }
}

切面使用advisor标签配置

<!--配置aop-->
<aop:config>
    <!--配置切点表达式,目的:指定哪些方法要被增强-->
    <aop:pointcut id="myPointcut" expression="execution(void com.mem.service.impl.UserServiceImpl.show*())"/>
    <!--配置织入,目的:指定哪些切点与哪些通知进行结合-->
    <aop:advisor advice-ref="myAdvice2" pointcut-ref="myPointcut"/>
</aop:config>

环绕通知
通知类实现了方法拦截器接口

public class MyAdvice3 implements MethodInterceptor {

    @Override
    public Object invoke(MethodInvocation methodInvocation) throws Throwable {
        System.out.println("环绕前*****");
        // 执行目标方法
        Object res = methodInvocation.getMethod().invoke(methodInvocation.getThis(), methodInvocation.getArguments());
        System.out.println("环绕后*****");
        return res;
    }
}

切面使用advisor标签配置

<!--配置aop-->
<aop:config>
    <!--配置切点表达式,目的:指定哪些方法要被增强-->
    <aop:pointcut id="myPointcut" expression="execution(void com.mem.service.impl.UserServiceImpl.show*())"/>
    <!--配置织入,目的:指定哪些切点与哪些通知进行结合-->
    <aop:advisor advice-ref="myAdvice3" pointcut-ref="myPointcut"/>
</aop:config>

两种方式的比较<advisor><aspect>
1)配置语法不同:
image.png

2)通知类的定义要求不同,advisor 需要的通知类需要实现Advice的子功能接口:
image.png
aspect 不需要通知类实现任何接口,在配置的时候指定哪些方法属于哪种通知类型即可,更加灵活方便:
image.png

3)可配置的切面数量不同:

  • 一个advisor只能配置一个固定通知和一个切点表达式;
  • 一个aspect可以配置多个通知和多个切点表达式任意组合,粒度更细。

4)使用场景不同:

  • 如果通知类型多、允许随意搭配情况下可以使用aspect进行配置;
  • 如果通知类型单一、且通知类中通知方法一次性都会使用到的情况下可以使用advisor进行配置;
  • 在通知类型已经固定,不用人为指定通知类型时,可以使用advisor进行配置,例如后面要学习的Spring事务 控制的配置;

由于实际开发中,自定义aop功能的配置大多使用aspect的配置方式,所以我们后面主要讲解aspect的配置, advisor是为了后面Spring声明式事务控制做铺垫,此处大家了解即可。

2.3 xml方式AOP原理刨析

通过xml方式配置AOP时,我们引入了AOP的命名空间,根据讲解的,要去找spring-aop包下的META-INF,在去找spring.handlers文件
image.png
最终加载的是 AopNamespaceHandler,该Handler的init方法中注册了config标签对应的解析器(ConfigBeanDefinitionParser)

public class AopNamespaceHandler extends NamespaceHandlerSupport {
    public AopNamespaceHandler() {
    }

    public void init() {
        this.registerBeanDefinitionParser("config", new ConfigBeanDefinitionParser());
        this.registerBeanDefinitionParser("aspectj-autoproxy", new AspectJAutoProxyBeanDefinitionParser());
        this.registerBeanDefinitionDecorator("scoped-proxy", new ScopedProxyBeanDefinitionDecorator());
        this.registerBeanDefinitionParser("spring-configured", new SpringConfiguredBeanDefinitionParser());
    }
}

以ConfigBeanDefinitionParser作为入口进行源码剖析,最终会注册一个AspectJAwareAdvisorAutoProxyCreator 进入到Spring容器中

public abstract class AopConfigUtils {
    @Nullable
    public static BeanDefinition registerAspectJAutoProxyCreatorIfNecessary(BeanDefinitionRegistry registry, @Nullable Object source) {
        return registerOrEscalateApcAsRequired(AspectJAwareAdvisorAutoProxyCreator.class, registry, source);
    }
}

那该类作用是什么呢?看一下继承体系图
image.png

AspectJAwareAdvisorAutoProxyCreator 的上上级父类AbstractAutoProxyCreator中的 postProcessAfterInitialization方法
image.png
通过断点方式观察,当bean是匹配切点表达式时,this.wrapIfNecessary(bean, beanName, cacheKey)返回的是 一个**JDKDynamicAopProxy **
image.png

可以在深入一点,对wrapIfNecessary在剖析一下,看看是不是我们熟知的通过JDK的 Proxy.newProxyInstance(ClassLoader loader, Class[] interfaces,InvocationHandler h) 的方式创建的代理 对象呢?经过如下一系列源码跟踪
image.png

动态代理实现:
动态代理的实现的选择,在调用getProxy() 方法时,我们可选用的 AopProxy接口有两个实现类,如上图,这两种都是动态生成代理对象的方式,一种就是基于JDK的,一种是基于Cglib的
image.png

配置<aop:config proxy-target-class=“true”> 后
image.png
删除proxy-target-class="true"(默认)
image.png

  • JDK动态代理 生成的代理类与目标对象更偏向兄弟关系
  • Cglib动态代理生成的代理类为目标对象的子类

image.png

JDK的动态代理代码,之前已经写过了,下面看一下Cglib基于超类的动态代理

  1. 目标类
public class Target {
    public void show(){
        System.out.println("show.....");
    }
}
  1. 通知类
public class MyAdvice4 {

    public void beforeAdvice(){
        System.out.println("前置增强....");
    }

    public void afterReturningAdvice(){
        System.out.println("后置增强....");
    }
}
  1. CgLib代理测试
/**
 * CGLib基于父类的动态代理
 */
public class MyCglibProxyTest {
    public static void main(String[] args) {
        // 目标对象
        Target target = new Target();
        // 通知对象
        MyAdvice4 myAdvice4 = new MyAdvice4();
        // 编写CGLib的代码
        Enhancer enhancer = new Enhancer();
        // 设置父类
        enhancer.setSuperclass(Target.class); // 生成的代理对象就是Target的子类
        // 设置回调
        enhancer.setCallback(new MethodInterceptor() {
            @Override
            // intercept方法相当于JDK的Proxy的invoke方法
            public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
                myAdvice4.beforeAdvice();
                Object res = method.invoke(target, objects);
                myAdvice4.afterReturningAdvice();
                return res;
            }
        });

        // 生成代理对象
        Target proxy = (Target) enhancer.create();
        proxy.show();
        /**
         * 前置增强....
         * show.....
         * 后置增强....
         */
    }
}

03、基于注解配置的AOP

3.1 注解方式AOP基本使用

Spring的AOP也提供了注解方式配置,使用相应的注解替代之前的xml配置,xml配置AOP时,我们主要配置了三部分:目标类被Spring容器管理、通知类被Spring管理、通知与切点的织入(切面),如下:

<!--配置通知 -->
<bean id="myAdvice" class="com.mem.advice.MyAdvice"/>
<!--配置目标 -->
<bean id="userService" class="com.mem.service.impl.UserServiceImpl"/>
<!--配置aop-->
<aop:config>
    <!--配置织入,目的:指定哪些切点与哪些通知进行结合-->
    <aop:aspect id="" ref="myAdvice">
        <aop:before method="beforeAdvice" pointcut="execution(void com.mem.service.impl.UserServiceImpl.show*())"/>
        <aop:after-returning method="afterReturningAdvice" pointcut="execution(void com.mem.service.impl.UserServiceImpl.show*())"/>
    </aop:aspect>
</aop:config>
  1. 用注解替代 通知目标
@Service  // 第一步
public interface UserService {
    void show1();
}
@Component  // 第二步
public class MyAdvice {

    public void beforeAdvice(){
        System.out.println("前置增强....");
    }

    public void afterReturningAdvice(){
        System.out.println("后置增强....");
    }
}
  1. 用注解替代配置aop

配置aop,其实配置aop主要就是配置通知类中的哪个方法(通知类型)对应的切点表达式是什么

@Component
@Aspect  // 第三步
public class MyAdvice {

    // <aop:before method="beforeAdvice" pointcut="execution(void com.mem.service.impl.UserServiceImpl.show*())"/>
    @Before("execution(void com.mem.service.impl.UserServiceImpl.show*())")  // 第四步
    public void beforeAdvice(){
        System.out.println("前置增强....");
    }
    // <aop:after-returning method="afterReturningAdvice" pointcut="execution(void com.mem.service.impl.UserServiceImpl.show*())"/>
    @AfterReturning("execution(void com.mem.service.impl.UserServiceImpl.show*())")  // 第四步
    public void afterReturningAdvice(){
        System.out.println("后置增强....");
    }
}
  1. 配置文件或配置类的修改
  • 配置文件: 注解@Aspect、@Around需要被Spring解析,所以在Spring核心配置文件中需要配置aspectj的自动代理 <aop:aspectj-autoproxy/>
<!-- aspectj 自动代理-->
<aop:aspectj-autoproxy/>
<!-- 容器包扫描 -->
<context:component-scan base-package="com.mem"/>
  • 配置类:需要加上@EnableAspectJAutoProxy
@Configuration
@ComponentScan("com.mem")
@EnableAspectJAutoProxy
public class ApplicationConfig {
}
  1. 测试
public class ApplicationContextTest {
    public static void main(String[] args) {
        // 配置文件
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext2.xml");
        UserService userService = (UserService) applicationContext.getBean("userService");
        userService.show1();
        // 配置类
//        ApplicationContext applicationContext = new AnnotationConfigApplicationContext(ApplicationConfig.class);
//        UserService userService = (UserService) applicationContext.getBean("userService");
//        userService.show1();
    }
}
// 两种方式的打印结果
前置增强....
show1 ...
后置增强....

3.2 注解方式AOP配置详解

各种注解方式通知类型
image.png
切点表达式的抽取,使用一个空方法,将切点表达式标注在空方法上,其他通知方法引用即可
image.png

3.3 注解方式AOP原理刨析

  • 半注解方式:使用aspectj-autoproxy标签

之前在使用xml配置AOP时,是借助的Spring的外部命名空间的加载方式完成的,使用注解配置后,就抛弃了 标签,而该标签最终加载了名为AspectJAwareAdvisorAutoProxyCreator的BeanPostProcessor , 最终,在该BeanPostProcessor中完成了代理对象的生成。
image.png
同样,从aspectj-autoproxy标签的解析器(AspectJAutoProxyBeanDefinitionParser)入手

public class AopNamespaceHandler extends NamespaceHandlerSupport {
    public AopNamespaceHandler() {
    }

    public void init() {
        this.registerBeanDefinitionParser("config", new ConfigBeanDefinitionParser());
        this.registerBeanDefinitionParser("aspectj-autoproxy", new AspectJAutoProxyBeanDefinitionParser());
        this.registerBeanDefinitionDecorator("scoped-proxy", new ScopedProxyBeanDefinitionDecorator());
        this.registerBeanDefinitionParser("spring-configured", new SpringConfiguredBeanDefinitionParser());
    }
}

而AspectJAutoProxyBeanDefinitionParser代码内部,最终也是执行了和xml方式AOP一样的代码(注册一个AspectJAwareAdvisorAutoProxyCreator 进入到Spring容器)

registerOrEscalateApcAsRequired(AspectJAwareAdvisorAutoProxyCreator.class, registry, source);
  • 全注解方式:使用@EnableAspectJAutoProxy注解

查看@EnableAspectJAutoProxy源码,使用的也是@Import导入相关解析类

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import({AspectJAutoProxyRegistrar.class})
public @interface EnableAspectJAutoProxy {
    boolean proxyTargetClass() default false;

    boolean exposeProxy() default false;
}

使用@Import导入的AspectJAutoProxyRegistrar源码,一路追踪下去,最终还是注册了 AnnotationAwareAspectJAutoProxyCreator 这个类
image.png

三种方式总结:
image.png

04、基于AOP的声明式事务控制

4.1 Spring事务编程概述

事务是开发中必不可少的东西,使用JDBC开发时,我们使用connnection对事务进行控制,使用MyBatis时,我们使用SqlSession对事务进行控制,缺点显而易见,当我们切换数据库访问技术时,事务控制的方式总会变化, Spring 就将这些技术基础上,提供了统一的控制事务的接口。Spring的事务分为:编程式事务控制声明式事务控制
image.png

Spring事务编程相关的类主要有如下三个
image.png
虽然编程式事务控制我们不学习,但是编程式事务控制对应的这些类我们需要了解一下,因为我们在通过配置的方式进行声明式事务控制时也会看到这些类的影子

4.2 搭建测试环境

搭建一个转账的环境,dao层一个转出钱的方法,一个转入钱的方法,service层一个转账业务方法,内部分别调 用dao层转出钱和转入钱的方法,准备工作如下:

  • 数据库准备一个账户表tb_account;
DROP TABLE IF EXISTS `account`;

CREATE TABLE `account` (
  `id` int NOT NULL AUTO_INCREMENT,
  `account_name` varchar(20) COLLATE utf8_unicode_ci DEFAULT NULL,
  `money` int DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;

insert  into `account`(`id`,`account_name`,`money`) values (1,'tom',5000),(2,'lucy',5000);
  • dao层准备一个AccountMapper,包括incrMoney和decrMoney两个方法;
public interface AccountMapper {
    // 加钱
    @Update("update account set money=money+#{money} where account_name=#{accountName}")
    void incrMoney(@Param("accountName") String accountName, @Param("money") Integer money);
    // 减钱
    @Update("update account set money=money-#{money} where account_name=#{accountName}")
    void decrMoney(@Param("accountName") String accountName, @Param("money") Integer money);
}
  • service层准备一个transferMoney方法,分别调用incrMoney和decrMoney方法;
public interface AccountService {

    void transferMoney(String outAccount,String inAccount,Integer money);
}

@Service("accountService")
public class AccountServiceImpl implements AccountService {
    @Autowired
    private AccountMapper accountMapper;
    @Override
    public void transferMoney(String outAccount, String inAccount, Integer money) {
        accountMapper.decrMoney(outAccount,money);
//        int i = 1/0;
        accountMapper.incrMoney(inAccount,money);
    }
}
  • 在applicationContext文件中进行Bean的管理配置;
<!--组件扫描-->
<context:component-scan base-package="com.mem"/>
<!--加载properties文件-->
<context:property-placeholder location="classpath:jdbc.properties"/>
<!-- 配置数据源信息 -->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
    <property name="driverClassName" value="${jdbc.driver}"></property>
    <property name="url" value="${jdbc.url}"></property>
    <property name="username" value="${jdbc.username}"></property>
    <property name="password" value="${jdbc.password}"></property>
</bean>

<!-- 配置SqlSessionFactoryBean,作用将SqlSessionFactory存储到Spring容器中-->
<bean class="org.mybatis.spring.SqlSessionFactoryBean">
    <property name="dataSource" ref="dataSource"/>
</bean>

<!--MapperScannerConfigurer,作用扫描指定的包,产生Mapper对象,存储到Spring容器中-->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
    <property name="basePackage" value="com.mem.mapper"/>
</bean>
jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://192.168.91.135:3306/mybatis?useSSL=false
jdbc.username=root
jdbc.password=123456
  • 测试正常转账与异常转账。
public class AccountTest {
    public static void main(String[] args) {
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
        AccountService accountService = (AccountService) applicationContext.getBean("accountService");
        accountService.transferMoney("tom","lucy",500);
        System.out.println("转账操作成功");
    }
}
// 打印结果
转账操作成功

打开AccountServiceImpl类中的错误代码,报错:java.lang.ArithmeticException: / by zero
此时数据库,tom用户的余额减了500,而lucy用户的余额却没有增加500
下面通过spring的声明式事务进行控制

4.3 基于xml声明式事务控制

结合上面我们学习的AOP的技术,很容易就可以想到,可以使用AOP对Service的方法进行事务的增强。

  • 目标类: 自定义的AccountServiceImpl,内部的方法是切点
  • 通知类: Spring提供的,通知方法已经定义好,只需要配置即可

我们分析要进行的操作:

  • 通知类是Spring提供的,需要导入Spring事务的相关的坐标;
  • 配置目标类AccountServiceImpl;
  • 使用advisor标签配置切面。

详情:

  1. 导入Spring事务的相关的坐标,spring-jdbc坐标已经引入的spring-tx坐标
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-jdbc</artifactId>
    <version>5.3.12</version>
</dependency>

image.png

  1. 配置目标类AccountServiceImpl (注解方式已完成)
<context:component-scan base-package="com.mem"/>
  1. 使用advisor标签配置切面
<!-- 事务增强的aop -->
<aop:config>
    <!--配置切点表达式-->
    <aop:pointcut id="myPointcut" expression="execution(void com.mem.service.impl.*.*(..))"/>
    <!--配置织入关系,通知是谁? spring提供好的-->
    <aop:advisor advice-ref="" pointcut-ref="myPointcut"/>
</aop:config>

疑问:Spring提供的通知类是谁?是spring-tx包下的advice标签配置提供的
配置详情:

<!-- 引入tx的命名空间 -->
<beans xmlns:tx="http://www.springframework.org/schema/tx" 
       xsi:schemaLocation="http://www.springframework.org/schema/tx https://www.springframework.org/schema/tx/spring-tx.xsd"
>
<!--配置平台事务管理器 PlatformTransactionManager-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <property name="dataSource" ref="dataSource"/>
</bean>
<!--配置Spring提供好的Advice 内部transaction-manager 需要一个平台事务管理器-->
<tx:advice id="txAdvice" transaction-manager="transactionManager">
    <tx:attributes>
        <tx:method name="*"/>
    </tx:attributes>
</tx:advice>
<!-- 事务增强的aop -->
<aop:config>
    <!--配置切点表达式-->
    <aop:pointcut id="myPointcut" expression="execution(void com.mem.service.impl.*.*(..))"/>
    <!--配置织入关系,通知是谁? spring提供好的-->
    <aop:advisor advice-ref="txAdvice" pointcut-ref="myPointcut"/>
</aop:config>

对上述配置进行详解一下
平台事务管理器:
首先,平台事务管理器PlatformTransactionManager是Spring提供的封装事务具体操作的规范接口,封装了事 务的提交和回滚方法
image.png
继承关系图
image.png
不同的持久层框架事务操作的方式有可能不同,所以不同的持久层框架有可能会有不同的平台事务管理器实现, 例如,

  • MyBatis作为持久层框架时,使用的平台事务管理器实现是DataSourceTransactionManager。
  • Hibernate作为持久层框架时,使用的平台事务管理器是HibernateTransactionManager。

事务定义信息:
其次,事务定义信息配置,每个事务有很多特性,例如:隔离级别、只读状态、超时时间等,这些信息在开发时 可以通过connection进行指定,而此处要通过配置文件进行配置
image.png

  • name属性名称指定哪个方法要进行哪些事务的属性配置

方法名在配置时,也可以使用*进行模糊匹配,例如:
image.png

此处需要区分的是切点表达式指定的方法与此处指定的方法的区别?
切点表达式,是过滤哪些方法可以进行事务增强;
事务属性信息的name,是指定哪个方法要进行哪些事务属性的配置
image.png

  • isolation属性:指定事务的隔离级别

事务并发存在三大问题:脏读、不可重复读、幻读/虚读。可以通过设置事务的隔离级别来保证并发问题的出现,常用的是READ_COMMITTED 和 **REPEATABLE_READ **
image.png

  • read-only属性:设置当前的只读状态

如果是查询则设置为true,可以提高查询性能,如果是更新(增删改)操作则设置为false
image.png

  • timeout属性:设置事务执行的超时时间,单位是秒

如果超过该时间限制但事务还没有完成,则自动回滚事务 ,不在继续执行。默认值是-1,即没有超时时间限制
image.png

  • propagation属性:设置事务的传播行为

主要解决是A方法调用B方法时,事务的传播方式问题的,例如:使用单方的事务,还是A和B都使用自己的事务等。事务的传播行为有如下七种属性值可配置
image.png

xml方式声明式事务控制的原理浅析一下

<tx:advice>标签使用的命名空间处理器是TxNamespaceHandler,内部注册的是解析器是 TxAdviceBeanDefinitionParser

public class TxNamespaceHandler extends NamespaceHandlerSupport {
    static final String TRANSACTION_MANAGER_ATTRIBUTE = "transaction-manager";
    static final String DEFAULT_TRANSACTION_MANAGER_BEAN_NAME = "transactionManager";

    public TxNamespaceHandler() {
    }

    static String getTransactionManagerName(Element element) {
        return element.hasAttribute("transaction-manager") ? element.getAttribute("transaction-manager") : "transactionManager";
    }

    public void init() {
        this.registerBeanDefinitionParser("advice", new TxAdviceBeanDefinitionParser());
        this.registerBeanDefinitionParser("annotation-driven", new AnnotationDrivenBeanDefinitionParser());
        this.registerBeanDefinitionParser("jta-transaction-manager", new JtaTransactionManagerBeanDefinitionParser());
    }
}

TxAdviceBeanDefinitionParser中指定了要注册的BeanDefinition
image.png
TxAdviceBeanDefinitionParser二级父类AbstractBeanDefinitionParser的parse方法将TransactionInterceptor 以配置的名称注册到了Spring容器中
image.png

那么TransactionInterceptor是啥?

public class TransactionInterceptor extends TransactionAspectSupport implements MethodInterceptor, Serializable {
}

从上面的实现来看,他实现了**MethodInterceptor**内部有个invoke方法(相当于环绕通知)
TransactionInterceptor中的invoke方法会被执行,跟踪invoke方法,最终会看到事务的开启和提交
根据targetClass的debug信息:可以证明目标对象是AccountServiceImpl
image.png

接着调用this.invokeWithinTransaction(..)方法,在TransactionAspectSupport类中的invokeWithinTransaction方法内完成相应的处理(环绕前,目标方法,环绕后)

public abstract class TransactionAspectSupport implements BeanFactoryAware, InitializingBean {
    
	protected Object invokeWithinTransaction(Method method, @Nullable Class<?> targetClass, final TransactionAspectSupport.InvocationCallback invocation) throws Throwable {
        // 在createTransactionIfNecessary方法内部开启事务
        TransactionAspectSupport.TransactionInfo txInfo = this.createTransactionIfNecessary(ptm, txAttr, joinpointIdentification);
        // 执行目标方法
        retVal = invocation.proceedWithInvocation();
        // commitTransactionAfterReturning方法 提交事务
        this.commitTransactionAfterReturning(txInfo);
        return retVal;
    }
}

4.4 基于注解声明式事务控制

注解就是对xml的替代

<!--配置Spring提供好的Advice 内部transaction-manager 需要一个平台事务管理器-->
<tx:advice id="txAdvice" transaction-manager="transactionManager">
    <tx:attributes>
        <tx:method name="*" isolation="REPEATABLE_READ" timeout="3" read-only="false" propagation="REQUIRED"/>
    </tx:attributes>
</tx:advice>
<!-- 事务增强的aop -->
<aop:config>
    <!--配置切点表达式-->
    <aop:pointcut id="myPointcut" expression="execution(void com.mem.service.impl.*.*(..))"/>
    <!--配置织入关系,通知是谁? spring提供好的-->
    <aop:advisor advice-ref="txAdvice" pointcut-ref="myPointcut"/>
</aop:config>
  1. @Transactional代替上面两个配置
@Service("accountService")
public class AccountServiceImpl implements AccountService {
    @Autowired
    private AccountMapper accountMapper;
    @Override
    @Transactional(isolation = Isolation.READ_COMMITTED,propagation = Propagation.SUPPORTS,timeout = 3)
    public void transferMoney(String outAccount, String inAccount, Integer money) {
        accountMapper.decrMoney(outAccount,money);
        int i = 1/0;
        accountMapper.incrMoney(inAccount,money);
    }
}
  1. 同样,使用的事务的注解,平台事务管理器仍然需要配置,还需要进行事务注解开关的开启
<!--配置平台事务管理器 PlatformTransactionManager-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <property name="dataSource" ref="dataSource"/>
</bean>
<!-- 配置事务的注解驱动 transaction-manager:默认值为transactionManager 可以不设置-->
<tx:annotation-driven transaction-manager="transactionManager"/>

改成全注解的方式需要替换成@Bean@EnableTransactionManagement
原来的xml(完整版):

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:aop="http://www.springframework.org/schema/aop"
       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/aop https://www.springframework.org/schema/aop/spring-aop.xsd
       http://www.springframework.org/schema/tx https://www.springframework.org/schema/tx/spring-tx.xsd">
    <!--组件扫描-->
    <context:component-scan base-package="com.mem"/>
    <!--加载properties文件-->
    <context:property-placeholder location="classpath:jdbc.properties"/>

    <!-- 配置数据源信息 -->
    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
        <property name="driverClassName" value="${jdbc.driver}"></property>
        <property name="url" value="${jdbc.url}"></property>
        <property name="username" value="${jdbc.username}"></property>
        <property name="password" value="${jdbc.password}"></property>
    </bean>

    <!-- 配置SqlSessionFactoryBean,作用将SqlSessionFactory存储到Spring容器中-->
    <bean class="org.mybatis.spring.SqlSessionFactoryBean">
        <property name="dataSource" ref="dataSource"/>
    </bean>

    <!--MapperScannerConfigurer,作用扫描指定的包,产生Mapper对象,存储到Spring容器中-->
    <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
        <property name="basePackage" value="com.mem.mapper"/>
    </bean>

    <!-- 配置事务的注解驱动 transaction-manager:默认值为transactionManager 可以不设置-->
    <tx:annotation-driven transaction-manager="transactionManager"/>

    <!--配置平台事务管理器 PlatformTransactionManager-->
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource"/>
    </bean>

    <!--配置Spring提供好的Advice 内部transaction-manager 需要一个平台事务管理器-->
    <tx:advice id="txAdvice" transaction-manager="transactionManager">
        <tx:attributes>
            <tx:method name="*" isolation="REPEATABLE_READ" timeout="3" read-only="false" propagation="REQUIRED"/>
        </tx:attributes>
    </tx:advice>
    <!-- 事务增强的aop -->
    <aop:config>
        <!--配置切点表达式-->
        <aop:pointcut id="myPointcut" expression="execution(void com.mem.service.impl.*.*(..))"/>
        <!--配置织入关系,通知是谁? spring提供好的-->
        <aop:advisor advice-ref="txAdvice" pointcut-ref="myPointcut"/>
    </aop:config>
</beans>

替换成全注解方式,如下:

@Configuration
@ComponentScan("com.mem") // <context:component-scan base-package="com.mem"/>
@PropertySource("classpath:jdbc.properties")  // <context:property-placeholder location="classpath:jdbc.properties"/>
/**
 * <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
 *     <property name="basePackage" value="com.mem.mapper"/>
 * </bean>
 */
@MapperScan("com.mem.mapper")
@EnableTransactionManagement // <tx:annotation-driven transaction-manager="transactionManager"/>
public class AccountConfig {

    @Bean
    public DataSource dataSource(
            @Value("${jdbc.driver}") String driver,
            @Value("${jdbc.url}") String url,
            @Value("${jdbc.username}") String username,
            @Value("${jdbc.password}") String password
    ){
        DruidDataSource dataSource = new DruidDataSource();
        dataSource.setDriverClassName(driver);
        dataSource.setUrl(url);
        dataSource.setUsername(username);
        dataSource.setPassword(password);
        return dataSource;
    }

    @Bean
    public SqlSessionFactoryBean sqlSessionFactoryBean(DataSource dataSource){
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(dataSource);
        return sqlSessionFactoryBean;
    }

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/123310.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

spdk用户态块层详解

先通过回顾内核态的通用块层来详细介绍SPDK通用块层&#xff0c;包括通用块层的架构、核心数据结构、数据流方面的考量等。最后描述基于通用块层之上的两个特性&#xff1a;一是逻辑卷的支持&#xff0c;基于通用块设备的Blobstore和各种逻辑卷的特性&#xff0c;精简配置&…

C# OpenCvSharp 去除文字中的线条

效果 中间过程效果 项目 代码 using OpenCvSharp; using System; using System.Drawing; using System.Windows.Forms; using static System.Net.Mime.MediaTypeNames;namespace OpenCvSharp_Demo {public partial class frmMain : Form{public frmMain(){InitializeComponent…

Spring面试题:(四)Spring Bean生命周期

Bean生命周期的阶段 实例化初始化完成销毁 IoC容器实例化Bean的流程 Bean定义 Bean工厂处理 反射实例化Bean 初始化 完成存储到单例池 Bean生命周期 Bean初始化话过程 属性填充aware接口BeanPostProcessor前置处理InitialzingBean接口初始化方法自定义init方法BeanPost…

Oracle(15)Managing Users

目录 一、基础知识 1、Users and Security 用户和安全 2、Database Schema 3、Checklist for Creating Users创建用户步骤 二、基础操作 1、创建一个用户 2、OS Authentication 操作系统身份验证 3、Dropping a User 删除用户 4、Getting User Information 获取用户信…

搭建自己的MQTT服务器,实现设备上云(Ubuntu+EMQX)

一、EMQX介绍 这篇文章教大家在ECS云服务器上部署EMQX,搭建自己私有的MQTT服务器,配置EMQX实现设备上云,设备数据转发,存储;服务器我采用的华为云的ECS服务器,系统选择Ubuntu系统。 Windows版本的看这里: https://blog.csdn.net/xiaolong1126626497/article/details/1…

经验模态分解(Empirical Mode Decomposition,EMD)(附代码)

代码原理 EMD&#xff08;Empirical Mode Decomposition&#xff09;&#xff0c;也称为经验模态分解&#xff0c;是一种将非线性和非平稳信号分解成多个本征模态函数&#xff08;Intrinsic Mode Functions&#xff0c;简称IMF&#xff09;的方法。 EMD的基本原理是通过一系列…

掌动智能:UI自动化测试工具产品功能和优势

UI自动化测试工具是一种软件工具&#xff0c;用于模拟用户与应用程序的交互&#xff0c;检查应用程序的用户界面是否按预期工作。这些工具允许开发人员编写测试脚本&#xff0c;以模拟用户操作&#xff0c;例如点击按钮、输入文本、导航菜单等&#xff0c;然后验证应用程序的响…

哪款手机便签软件支持存储录音文件并支持转文字?

手机便签类软件带有存储录音转文字功能是比较实用的&#xff0c;很多人通常会整理很多录音类型的文件&#xff0c;录音文件整合在一起后&#xff0c;后续有需要可以逐条点开播放收听。尤其是在工作中&#xff0c;当领导说一些重点时&#xff0c;大家无法借助灵活的大脑来成功的…

如何判断被DDoS攻击

当网络和设备正常的情况下&#xff0c;服务器突然出现连接断开、访问卡顿、用户掉线等情况;服务器CPU或内存占用率出现明显增长;网络出入流量出现明显增长;网站或应用程序突然出现大量的未知访问;登录服务器失败或者登录过慢等等。以上是最为常见的服务器被 DDoS攻击后出现的几…

2.3 - 网络协议 - ICMP协议工作原理,报文格式,抓包实战

「作者主页」&#xff1a;士别三日wyx 「作者简介」&#xff1a;CSDN top100、阿里云博客专家、华为云享专家、网络安全领域优质创作者 「推荐专栏」&#xff1a;对网络安全感兴趣的小伙伴可以关注专栏《网络安全入门到精通》 ICMP协议 1、ICMP协议工作原理2、ICMP协议报文格式…

el-date-picker精确到分钟

0 效果 1 代码 使用format、value-format属性格式化即可 :clearable“false” // 取消删除图标 注意&#xff1a; format&#xff1a;“yyyy-MM-dd HH:mm” 小时默认是从00:00开始 format&#xff1a;“yyyy-MM-dd hh:mm” 小时默认是从12:00开始

【爬虫】Java爬虫爬取某招聘网站招聘信息

目录 前言 一、爬虫程序的基本架构 二、如何获取目标网站的页面内容 三、解析HTML页面&#xff0c;提取所需信息 四、代理IP的使用 五、完整代码 总结 前言 随着互联网的普及&#xff0c;越来越多的人开始关注网络上的招聘信息&#xff0c;而传统的求职方式愈发显得不够…

docker部署mysql

docker部署mysql Docker部署MySQL配置远程登录持久化数据创建容器时配置挂载卷后置配置挂载卷 Docker卸载MySQL部署特定版本mysql【出现1251错误】远程连接docker容器中的mysql8.0数据库时出现连接不上 Docker部署MySQL 要使用Docker部署MySQL&#xff0c;你可以使用官方的MyS…

伊朗黑客对以色列科技和教育领域发起破坏性网络攻击

导语 近期&#xff0c;以色列的高等教育和科技领域遭受了一系列破坏性的网络攻击。这些攻击始于2023年1月&#xff0c;旨在部署以前未记录的数据清除恶意软件。在最近的攻击中&#xff0c;攻击者试图窃取个人身份信息和知识产权等敏感数据。本文将介绍这些攻击的具体细节&#…

Go语言开发环境安装,hello world!

1. Go开发包SDK https://golang.google.cn/dl/&#xff08;国内也可以安装&#xff09; 根据自己电脑下载对应的安装包&#xff0c;我懒下载了msi安装 然后一路点确定安装Go 2.安装GoLand https://www.jetbrains.com/go/download/#sectionwindows 下载安装包 一路确定安装完…

大数据疫情分析及可视化系统 计算机竞赛

文章目录 0 前言2 开发简介3 数据集4 实现技术4.1 系统架构4.2 开发环境4.3 疫情地图4.3.1 填充图(Choropleth maps)4.3.2 气泡图 4.4 全国疫情实时追踪4.6 其他页面 5 关键代码最后 0 前言 &#x1f525; 优质竞赛项目系列&#xff0c;今天要分享的是 &#x1f6a9; 大数据疫…

Linux 安装 Nginx 并配置为系统服务(超详细)

目录 前言安装 Nginx安装依赖项下载Nginx解压Nginx编译和安装防火墙设置启动Nginx 配置 Nginx 为系统服务配置 Nginx 服务文件启动 Nginx 服务设置开机自启动检查 Nginx 状态停止 Nginx 服务重启 Nginx 服务 卸载 Nginx结语 前言 Nginx是一款卓越的高性能Web服务器&#xff0c…

STM32 IIC 实验

1. 可以选择I2C1&#xff0c;也可以选择I2C2&#xff0c;或者同时选择&#xff0c;同时运行 配置时钟信号 为节约空间&#xff0c;选择这两个&#xff0c;然后选择GENERATE CODE 二、HAL_I2C_Mem_Write I2C_HandleTypeDef *hi2c&#xff1a;I2C设备句柄 uint16_t DevAddress&am…