SpringAOP+SpringBoot事务管理

  • 项目搭建
  • SpringAOP
  • SpringBoot中管理事务
  • AOP案例实战-日志记录
  • 日志系统

一、项目搭建

第一步:构建项目

第二步:导入依赖

第三步:配置信息

  • 自动配置(项目自动生成的启动类)

    /**
     * 启动类:申明当前类是一个SpringBoot项目的启动类
     * 启动类会做一些自动配置,减少手动配置
     * 启动类启动时会扫描当前包及其子包下的某些注解
     */
    @MapperScan("cn.itsource.mapper") //扫描mapper接口 - 自动生成Mapper接口的实现类。-并交给Spring管理
    @SpringBootApplication
    public class AopApplication {
        public static void main(String[] args) {
            //使用启动类 运行 Spring程序或应用
            SpringApplication.run(AopApplication.class, args);
        }
    }
    
  • 手动配置(项目中的.yml配置)

    # 端口号配置
    server:
      port: 80
    # 连接数据库的四个必要参数=四大金刚
    spring:
      datasource:
        username: root  # 数据库连接账号
        password: 123456 # 数据库连接密码
        driver-class-name: com.mysql.cj.jdbc.Driver #数据库驱动类名称
        url: jdbc:mysql://localhost:3306/test # 数据库连接URL
    # 配置sql日志
    mybatis:
      configuration:
        # Mybatis日志配置,输出到控制台 
        log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
        # 开启驼峰自动转换 - 将数据库表的_下划线字段的数据自动映射到实体类的驼峰字段
        map-underscore-to-camel-case: true #注意配置了就会强制用驼峰转换,实体类必须写驼峰
        # 配置别名【只能配置实体类的包】-在xml中类型就可以使用三种写法:类名,类名首字母小写,完全限定名
      type-aliases-package: cn.itsource.domain
    

第四步:数据准备

后端数据:数据库,表,工具类等

第五步:项目开发

使用三层架构实现User表的基础方法,并使用Apifox测试

  1. domain
  2. Mapper接口和Sql文件
  3. Service接口和实现类
  4. Controller实现
  5. 测试

二. SpringAOP

概念 :

Spring两大核心机制:IOC控制反转、AOP面向切面编程

什么是AOP:

概念: 面向切面编程(面向方面编程) ,将共同的业务抽取出来,以xml或注解的方式作用到目标上

场景:抽取分散的公共代码就只有用AOP可以使用

    public void save(Product t) {
        try{
            EntityManager entityManager = JpaUtils.getEntityManager();
            //开启事务
            entityManager.getTransaction().begin();
            productDao.save(t); // 例如这种方法在中间,如果在多几个操作,下面就又要重新开启事务,就会有大量重复代码,就要想办法把他们公共的代码抽取出来
            //提交事务
            entityManager.getTransaction().commit();
        }catch{
            //回滚事务
            ..
        }finaly{..}
    }
  1. AOP入门

接下来我们使用AOP模拟事务控制。事务是把多个操作看成一个整体,三层架构中可以在Service层进行业务处理执行多个操作,所以事务都是控制在Service业务层。事务简单回顾:

  1. 事务是把多个操作看成一个整体,要么都成功,要么都不成功
  2. 事务的操作主要有三步:开启事务、提交事务、回滚事务、关闭事务
  3. 事务的四大特性ACID:原子性,一致性,隔离性,持久性

第一步: 导入AOP包

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

第二步:AOP核心业务类(建一个包,包名叫aop,里面写核心业务类)

这些方法中以后可以针对不同的业务编写大量的业务代码,以实现最终需求。这里只是用AOP做事务管理测试,里面的方法仅仅打印一些字符串

package cn.itsource.aop;

import org.aspectj.lang.ProceedingJoinPoint;
import org.springframework.stereotype.Component;

@Component //将当前类交给Spring管理,方便在service中注入使用
public class TxManager {
    public void begin(){
        System.out.println("开启事务"); // 注意这里只是模拟事务管理,主要理解AOP的作用
    }
    public void commit(){
        System.out.println("提交事务");
    }
    public void rollback(){
        System.out.println("回滚事务");
    }
    public void close(){
        System.out.println("关闭事务");
    }
    public void around(ProceedingJoinPoint joinPoint){
        try {
            begin();
            //执行切入点指定的方法 - service中的方法  //java.lang.Throwable
            joinPoint.proceed(); //底层会去执行切入点指定的方法 - service中的方法
            commit();
        } catch (Throwable e) {
            e.printStackTrace();
            rollback();
        } finally {
            close();
        }
    }
}

第三步:在service层手动管理事务

@Override
public void delete(Integer id) {
    try {
        txManager.begin();
        userMapper.delete(id);   // 代码跟下面的重复
        txManager.commit();
    } catch (Exception e) {
        e.printStackTrace();
        txManager.rollback();
    } finally {
        txManager.close();
    }
}
@Override
public void add(User user) {
    try {
        txManager.begin();
        userMapper.add(user);
        txManager.commit();
    } catch (Exception e) {
        e.printStackTrace();
        txManager.rollback();
    } finally {
        txManager.close();
    }
}
  1. AOP入场

2.1有两个核心注解:

  • @Aspect:定义切面,作用在Aop核心业务类上

  • @Pointcut:定义切点,指定此切面作用在哪些方法上

2.2 核心业务类改造

package cn.itsource.aop;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

@Aspect//申明当前类AOP的切面类-AOP的核心业务类
@Component  // 将此类交给spring管理
public class TxManager {
    //作用到cn.itsource.service.I*Service的所有方法上
    //第一个*表示任意返回值,最后一个*表示所有方法,(..)任意参数
    @Pointcut("execution(* cn.itsource.service.I*Service.*(..))") // execution是方法限定表达式
    public void pointcut(){}

    @Before("pointcut()") //作用在业务方法之前-【前置通知】
    public void begin(){
        System.out.println("开启事务");
    }
    @AfterReturning("pointcut()") //作用在业务方法之后-【后置通知】
    public void commit(){
        System.out.println("提交事务");
    }
    @AfterThrowing("pointcut()") //除了异常之后作用-【异常通知】
    public void rollback(){
        System.out.println("回滚事务");
    }
    @After("pointcut()") //无论是否出异常最终都会执行-【最终通知】
    public void close(){
        System.out.println("关闭事务");
    }

    
    @Around // 环绕通知   如果写了这个,上面的before和其他几个注解就不用写了
    public void around(ProceedingJoinPoint joinPoint){  //这个参数非常重要
        try {
            begin();
            //执行切入点指定的目标方法 - service中的方法都是目标方法   
            joinPoint.proceed(); //底层会去执行切入点指定的方法 - service中的方法 
            commit();
        } catch (Throwable e) {
            e.printStackTrace();
            rollback();
        } finally {
            close();
        }
    }
}
  1. Aop相关术语

3.1. 核心概念

名称说明
Joinpoint:连接点连接点指的是可以被Aop控制的方法,例如:入门程序当中所有的Service层方法都是可以被Aop控制的
Pointcut:切入点切入点指的是哪些类、方法要被拦截,也就是哪些连接点要被拦截,例如:入门程序中切入点就是Service层所有方法
Advice:通知通知指的是要作用到连接点的功能,例如:入门程序中的通知
Target:目标目标指的是被代理的对象,例如:入门案例中的UserService就是目标对象
Aspect:切面切面指的是切入点和通知的结合,例如:入门案例中的TxManager就被定义为切面
Proxy:代理代理指的是被增强后的对象,也就是织入了增强处理的类,在程序运行时看的到

3.2. 通知分类

通知说明
before:前置通知通知方法在目标方法调用之前执行
after:最终通知通知方法在目标方法执行后执行,核心方法是否异常都会执行
after-returning:后置通知通知方法会在目标方法执行后执行,核心方法异常后不执行
after-throwing:异常通知通知方法会在目标方法抛出异常后执行
around:环绕通知通知方法会将目标方法封装起来
  1. 代理模式

概念:代理模式的英文叫做Proxy或Surrogate,中文都可译为代理。所谓代理,就是一个人或者一个机构代表另一个人或者另一个机构采取行动。在一些情况下,一个客户不想或者不能够直接引用一个对象,而代理对象可以在客户端和目标对象之间起到中介的作用

在这里插入图片描述

分类:

  • 静态代理(在执行之前就要为业务类生成代理类,业务类是否运行都会生成代理类,非常不灵活,添加一个业务类,也需要添加相应的代理类。)

  • 动态代理(Java1.3就提供了动态代理,让咱们可以在代码运行期动态生成代理类。)

    • jdk的动态代理只允许完成有接口的类的代理,如果没有就需要用第三发的CGLIB的方式实现

      1. 如果代理的类有接口,默认采用原生JDK的方式实现动态代理
      2. 如果代理的类没有接口,只能采用第三方的CGLIB的方式实现动态代理
      注意:其实有接口的也可以强制采用使用CGLIB的动态代理模式,不过需要单独配置
      

AOP的代理模式:

概念:Aop底层是通过动态代理实现,而动态代理底层可以通过原生的JDK方式和第三方cglib的方式实现

  1. Aop的使用场景
  • 日志记录:记录用户的所有操作到数据库中

  • 事务管理:在Service层管理事务

  • 权限验证:在执行业务代码前执行权限校验

  • 性能监控:记录Service层方法执行耗时

    注意:AOP可以拦截指定的方法,并且对方法增强,比如:事务、日志、权限、性能监测等增强,而且无需侵入到业务代码中,使业务与非业务处理逻辑分离

三、SpringBoot中管理事务

概念:Spring事务管理分为编程式和声明式的两种方式

  • 编程式:是指在写业务代码中将事务代码也写进去,这是很古老的做法了(几乎不用了)
  • 声明式:基于AOP将具体业务逻辑与事务处理解耦,声明式事务管理使业务代码逻辑不受污染 (现在都用这个声明式)

3.1 声明式事务有两种方式

  • 配置文件中做相关的事务规则声明
  • 基于@Transactional 注解的方式(主流)

3.2 Transactional注解作用

概念:Spring提供的用来控制事务回滚/提交的一个注解,属于声明式事务的实现。

作用域:@Transactional可以写在类和方法上

  • 当标注在类上的时候,表示给该类所有的public方法添加上@Transactional注解

  • 当标注在方法上的时候,事务的作用域就只在该方法上生效,并且如果类及方法上都配置@Transactional注解时,方法的注解会覆盖类上的注解

    注意:Transactional注解的底层实现原理基于AOP和代理模式

3.3 Transactional注解使用&事务传播机制

  1. 注解使用:

第一步:在Service层实现类上加上@Transaction注解

第二步:直接测试即可

  1. 事务传播机制

概念:以后Service业务都会控制事务,但是当一个方法调用其他方法时就会设计到事务的传播,因为一个业务方法中只能有一个事务

事务传播机制有如下几种:

  • Propagation.REQUIRED: 默认,支持当前事务,如果当前没有事务,就创建一个事务,保证一定有事务 – 增删改方法使用
  • Propagation.SUPPORTS: 支持当前事务,如果当前没有事务,就不使用事务 – 查询方法使用
  • Propagation.REQUIRES_NEW:新建事务,如果当前有事务,就挂起 – 不常用
  • Propagation.NAVEN: 不支持事务,如果当前有事务,就抛出异常 – 不常用

@Transactional的属性

  • propagation:事务传播机制,通常与readOnly搭配配置,默认值是Propagation.REQUIRED
  • readOnly:事务是否是只读,通常与propagation搭配使用,默认值是false
    • false:不只读、可修改
    • true:只读、不可修改

3.4 事务最终配置

在真实开发中,一个类的查询方法占比最多,所以在类上使用查询的全局配置,增删改在方法上单独配置:

1. 在类上配置查询的事务控制方式:@Transactional(readOnly = true, propagation = Propagation.SUPPORTS) //事务注解:指定为只读并且是否有事务都可以
2. 在方法上配置增删改的事务控制方式:@Transactional  // 由于就近原则,所以方法上的事务控制,是听这个的

四. Aop案例实战-日志记录

4.1 需求分析

1. 将用户对于Service层的所有增删改操作记录在数据库中,不用记录查询操作,因为查询不对系统造成影响,无需后期进行追踪
2. 记录的操作日志当中包括:操作人、操作时间,访问的是哪个类、哪个方法、方法运行时参数、方法的返回值、方法的运行时长
  • 使用什么通知:要记录目标方法的返回值,只有环绕通知可以获取到,所以采用环绕通知
  • 切入点如何编写:在模拟事务案例中切入点我们使用的是execution切入点表达式的方式,这种方式特点是比较简单。但是目前的需求是只对增删改操作进行增强,execution切入点表达式就无法很方便的编写,所以要使用切入点的第二种写法叫做annotation切入点表达式
  • @Annotation切入点表达式:用于匹配标识有特定注解的方法,也就是我们可以在需要增强的方法上加上指定注解,然后annotation切入点表达式指定扫描这个注解即可

4.2 功能实战

第一步:准备日志记录表

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for t_logs
-- ----------------------------
DROP TABLE IF EXISTS `t_logs`;
CREATE TABLE `t_logs`  (
  `id` bigint UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键',
  `user_id` bigint NULL DEFAULT NULL COMMENT '操作人ID',
  `user_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '操作人名称',
  `create_time` datetime NULL DEFAULT NULL COMMENT '操作时间',
  `class_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '操作的类名',
  `method_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '操作的方法名',
  `method_params` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '方法参数',
  `return_value` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '返回值',
  `cost_time` bigint NULL DEFAULT NULL COMMENT '方法执行耗时, 单位:ms',
  `ip` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '操作Ip',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '操作日志表' ROW_FORMAT = Dynamic;

SET FOREIGN_KEY_CHECKS = 1;

第二步:编写日志domain

package cn.zy.domain;

import lombok.Data;

import java.util.Date;

@Data
public class Logs {
    private Long id;
    private Long userId;
    private String userName;
    private Date createTime;
    private String className;
    private String methodName;
    private String methodParams;
    private String returnValue;
    private Long costTime;
    private String ip;
}

第三步:,编写mapper中的新增方法&编写日志表新增方法

新增方法:

package cn.zy.mapper;

import cn.zy.domain.Logs;

public interface LogsMapper {
    void add(Logs logs);
}

新增方法

<?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="cn.zy.mapper.LogsMapper">


    <insert id="add">
        insert into t_logs(user_id, user_name, create_time, class_name,
                           method_name, method_params, return_value, cost_time, ip)
        VALUES (#{userId},#{userName},#{createTime},#{className},#{methodName},
                #{methodParams},#{returnValue},#{costTime},#{ip})
    </insert>
</mapper>

第四步:自定义@Log注解

package cn.zy.anno;

import org.springframework.stereotype.Component;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Component
public @interface Log {

}

第四步:定义日志记录切面类,切面类一般放在aop包下,类以Aspect结尾

package cn.itsource.aop;

import cn.itsource.domain.Logs;
import cn.itsource.mapper.LogsMapper;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;
import java.util.Arrays;
import java.util.Date;

@Component
@Aspect
public class LogManager {

    @Autowired
    private HttpServletRequest request;  // 用来获取ip的
    @Autowired
    private LogsMapper logsMapper;  // 给类中属性赋值

    //只有有@Logs注解的方法才作用 - around方法作用到使用了@Logs注解的方法
    @Around("@annotation(cn.itsource.anno.Logs)")  // @annotation
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {  //注意这里需要返回值,否则
        Logs logs = new Logs();
        logs.setUserId(1L);
        logs.setUserName("张三");
        logs.setCreateTime(new Date());
        //获取类名
        String className = joinPoint.getTarget().getClass().getName();
        logs.setClassName(className);
        //通过方法签名获取方法名
        String methodName = joinPoint.getSignature().getName();
        logs.setMethodName(methodName);
        //获取方法参数
        Object[] args = joinPoint.getArgs();
        logs.setMethodParams(Arrays.toString(args));
        //返回值 @TODO
        Signature signature = joinPoint.getSignature();
        if (signature instanceof MethodSignature) {
            MethodSignature methodSignature = (MethodSignature) signature;
            // 实例化
            String returnType = methodSignature.getReturnType().getName();
            logs.setReturnValue(returnType);
        }

        //操作业务方法时间
        long start = System.currentTimeMillis();
        //执行目标方法 - 如果不返回一个对象,调用方就会接收到一个null值
        Object result = joinPoint.proceed();
        long end = System.currentTimeMillis();
        logs.setCostTime(end-start);
        //获取ip地址
        String ip = request.getRemoteAddr();
        logs.setIp(ip);
        logsMapper.add(logs);
        return result;  // 这里返回值给调用方
    }
}

第五步:改造Service层方法

  • 给增删改方法增加@Logs注解
  • 注释掉前面写的TxManager切面,以免影响测试
    @Override
    @Log
    public void add(User user) {
        userMapper.add(user);
    }

= System.currentTimeMillis();
//执行目标方法 - 如果不返回一个对象,调用方就会接收到一个null值
Object result = joinPoint.proceed();
long end = System.currentTimeMillis();
logs.setCostTime(end-start);
//获取ip地址
String ip = request.getRemoteAddr();
logs.setIp(ip);
logsMapper.add(logs);
return result; // 这里返回值给调用方
}
}


第五步:改造Service层方法

> - 给增删改方法增加@Logs注解
> - 注释掉前面写的TxManager切面,以免影响测试

~~~java
    @Override
    @Log
    public void add(User user) {
        userMapper.add(user);
    }
  • 18
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Zyangxsir

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值