springAOP理解及事务

devtools/2024/11/15 6:17:03/

AOP:

springAOP是什么:

AOP:Aspect Oriented Programming(面向切面编程、面向方面编程),其实就是面向特定方法编程。

使用场景:

比如你想统计业务中每个方法的执行耗时,那我们最初的想法就是对每个方法一开始写一个获取方法运行开始时间,然后再来一个获取方法运行结束时间
然后相减,那这个做法思路简单,不过问题也很明显,就是如果都每个方法都这样做,我们需要写很多重复的代码,所以AOP就可以解决这样的问题

再比如你想知道谁在业务中调用了增,删,改方法,想将这个调用方法的信息存入到信息日志表中,我们也可以用AOP来解决这样的问题。

springAOP的代理设计思想:

代理模式:

二十三种设计模式中的一种,属于结构型模式。它的作用就是通过提供一个代理类,让我们在调用目标方法的时候,不再是直接对目标方法进行调用,而是通过代理类间接调用。让不属于目标方法核心逻辑的代码从目标方法中剥离出来——解耦。调用目标方法时先调用代理对象的方法,减少对目标方法的调用和打扰,同时让附加功能能够集中在一起也有利于统一维护。  

生活中的代理:

  • 广告商找大明星拍广告需要经过经纪人

  • 合作伙伴找大老板谈合作要约见面时间需要经过秘书

  • 房产中介是买卖双方的代理

  • 太监是大臣和皇上之间的代理 相关术语:

 静态代理:
public class CalculatorStaticProxy implements Calculator {// 将被代理的目标对象声明为成员变量private Calculator target;public CalculatorStaticProxy(Calculator target) {this.target = target;}@Overridepublic int add(int i, int j) {// 附加功能由代理类中的代理方法来实现System.out.println("参数是:" + i + "," + j);// 通过目标对象来实现核心业务逻辑int addResult = target.add(i, j);System.out.println("方法内部 result = " + result);return addResult;}

静态代理确实实现了解耦,但是由于代码都写死了,完全不具备任何的灵活性。就拿日志功能来说,将来其他地方也需要附加日志,那还得再声明更多个静态代理类,那就产生了大量重复的代码,日志功能还是分散的,没有统一管理

动态代理:

动态代理技术分类

  • JDK动态代理:JDK原生的实现方式,需要被代理的目标类必须实现接口!他会根据目标类的接口动态生成一个代理对象!代理对象和目标对象有相同的接口!(拜把子)

  • cglib:通过继承被代理的目标类实现代理,所以不需要目标类实现接口!(认干爹) JDK动态代理技术实现(了解)

代理工程:基于jdk代理技术,生成代理对象

AOP的核心概念和执行流程:

 连接点:JoinPoint,可以被AOP控制的方法(暗含方法执行时的相关信息)
通知:Advice,指哪些重复的逻辑,也就是共性功能(最终体现为一个方法)
切入点:PointCut,匹配连接点的条件,通知仅会在切入点方法执行时被应用
切面:Aspect,描述通知与切入点的对应关系(通知+切入点)
目标对象:Target,通知所应用的对象

 

连接点:

在Spring中用JoinPoint抽象了连接点,用它可以获得方法执行时的相关信息,如目标类名、方法名、方法参数等
对于 @Around 通知,获取连接点信息只能使用  ProceedingJoinPoint
对于其他四种通知,获取连接点信息只能使用 JoinPoint ,它是 ProceedingJoinPoint 的父类型

JoinPoint的基本方法:
String className = joinPoint.getTarget().getClass().getName(); //获取目标类名
Signature signature = joinPoint.getSignature(); //获取目标方法签名
String methodName = joinPoint.getSignature().getName(); //获取目标方法名
Object[] args = joinPoint.getArgs(); //获取目标方法运行参数 
Object res = joinPoint.proceed(); //执行原始方法,获取返回值(环绕通知)(最后一种只有环绕通知能使用)

通知类型:

1:@Around:环绕通知,此注解标注的通知方法在目标方法前、后都被执行(如果目标方法有异常,那环绕通知的第三格部分结束方法就不会执行)
2:@Before:前置通知,此注解标注的通知方法在目标方法前被执行
3:@After :后置通知,此注解标注的通知方法在目标方法后被执行,无论是否有异常都会执行
4:@AfterReturning : 返回后通知,此注解标注的通知方法在目标方法后被执行,有异常不会执行(这个通知程序必须正常执行才会运行)
5:@AfterThrowing : 异常后通知,此注解标注的通知方法发生异常后执行(这个只有程序有异常了才会运行)

从上面几种通知就能看出来,第四个和第五个是两个对立的通知。

注意:
@Around环绕通知需要自己调用 ProceedingJoinPoint.proceed() 来让原始方法执行,其他通知不需要考虑目标方法执行
@Around环绕通知方法的返回值,必须指定为Object,来接收原始方法的返回值。(拿不到返回值的话,界面上就不会显示数据)

切入点:

切入点表达式:描述切入点方法的一种表达式
作用:主要用来决定项目中的哪些方法需要加入通知 (Advice)
常见形式:
1:execution(……):根据方法的签名来匹配
2:@annotation(……) :根据注解匹配

切入点表达式的语法:
execution(访问修饰符?  返回值  包名.类名.?方法名(方法参数) throws 异常?)
例子:execution(public void com.springboottlias.service.impl.DeptServiceImpl.deletedept(java.lang.Integer))

访问修饰符:可省略(比如: public、protected)

切入点表达式中的通配符:* 和 ..

* :单个独立的任意符号,可以通配任意返回值、包名、类名、方法名、任意类型的一个参数,也可以通配包、类、方法名的一部分
例子:execution(* com.*.service.*.update*(*))
第一个*表示:任意返回值
第二个*表示:com包下的任意包
第三个*也表示service的包名或类名
第四个*表示方法名中开头是update的方法名,如果是*deptservice这样的,说明以deptservice结尾的方法名
第五个*表示update*方法中任意的一个参数

.. :多个连续的任意符号,可以通配任意层级的包,或任意类型、任意个数的参数
例子:execution(* com.itheima..DeptService.*(..))
表示这个函数里面可以有任意个参数。

AOP执行流程:


    一旦我们进行了AOP的方法开发,那我们运行得就不是原始对象得方法了,运行得就是一个代理对象
    这个代理对象如何理解呢:可以理解为是原始对象方法得加强版(这个加强得意思就是在这个代理对象中多了其它的方法)
    当我们想执行原来的原始对象方法,这个时候我们就不是执行原始对象,我们执行的就是这个代理对象。

 AOP案例:

将案例中 增、删、改 相关接口的操作日志记录到数据库表中
日志信息包含:操作人、操作时间、执行方法的全类名、执行方法名、方法运行时参数、返回值、方法执行时长

1:准备:

这个准备包括引入AOP的依赖,然后建一个日志记录表还有一个对呀日志记录表的实体类

1:在案例工程中引入AOP的起步依赖
<!--        AOP依赖        --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId></dependency>2:导入资料中准备好的数据库表结构,并引入对应的实体类
下面是实体类:
@Data
@AllArgsConstructor
@NoArgsConstructor
public class OperateLog {private Integer id; //IDprivate Integer operateUser; //操作人IDprivate LocalDateTime operateTime; //操作时间private String className; //操作类名private String methodName; //操作方法名private String methodParams; //操作方法参数private String returnValue; //操作方法返回值private Long costTime; //操作耗时
}Sql脚本:
-- 操作日志表
create table operate_log(id int unsigned primary key auto_increment comment 'ID',operate_user int unsigned comment '操作人ID',operate_time datetime comment '操作时间',class_name varchar(100) comment '操作的类名',method_name varchar(100) comment '操作的方法名',method_params varchar(1000) comment '方法参数',return_value varchar(2000) comment '返回值',cost_time bigint comment '方法执行耗时, 单位:ms'
) comment '操作日志表';

2:自定义注解 @Log:

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

3:定义切面类,完成记录操作日志的逻辑

@Slf4j
@Component//将这个类交给IOC容器管理
@Aspect//声明这个类是一个AOP类
public class OperateLogAspect{@Autowiredprivate HttpServletRequest request;@Autowiredprivate OperateLogMapper operateLogMapper;@Pointcut("@annotation(com.springboottlias.anno.Log)")private void pt(){}@Around("pt()")public Object RecordtoDatebase(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {//操作人ID - 当前登录员工ID//获取请求头中的jwt令牌,解析令牌String jwt = request.getHeader("token");Claims claims = JwtUtils.parseJWT(jwt);Integer operateUserId = (Integer) claims.get("id");//操作时间LocalDateTime operateTime = LocalDateTime.now();//操作类名String classname = proceedingJoinPoint.getTarget().getClass().getName();//操作方法名String methodname = proceedingJoinPoint.getSignature().getName();//操作方法参数Object[] args = proceedingJoinPoint.getArgs();String methodParams = Arrays.toString(args);long begin = System.currentTimeMillis();//原始方法执行之前的时间//调用原始目标方法运行Object res = proceedingJoinPoint.proceed();//方法返回值String returnValue = JSONObject.toJSONString(res);//操作耗时long end = System.currentTimeMillis();//原始方法执行之后的时间long costtime = end-begin;//记录操作日志OperateLog operateLog = new OperateLog(null,operateUserId,operateTime,classname,methodname,methodParams,returnValue,costtime);operateLogMapper.insert(operateLog);log.info("AOP操作日志:{}",operateLog);return res;}
}
这一步有几个注意点:

1:你想要获得操作人的ID,就得用到JWT令牌功能:你得先从请求头中获得jwt令牌,并且解析token
现在的问题就转化为了,你怎么得到这个令牌,我们知道令牌是在请求头中的,请求头的对象是Request
所以,我们可以从IOC容器中获得Request对象,然后获取令牌。
    @Autowired
        private HttpServletRequest request;

//操作人ID - 当前登录员工ID
        //获取请求头中的jwt令牌,解析令牌
        String jwt = request.getHeader("token");
        Claims claims = JwtUtils.parseJWT(jwt);
        Integer operateUserId = (Integer) claims.get("id");


2:第二个处理的点就是这个方法返回值这边的处理:
用到了之前的一个工具类,将对象转化成json格式的数据。

 4:在需要的方法上添加注解@Log

事务:

简单理解:事务就是不可拆分的最小事件

Spring事务管理(底层是AOP)

位置:业务(service)层的方法上、类上、接口上
作用:将当前方法交给spring进行事务管理,方法执行前,开启事务;成功执行完毕,提交事务;出现异常,回滚事务

@Transactional这个注解可以作用在方法,接口和类上,不过我们一般作用在业务层的增删改业务上,也就是需要访问多次数据访问的方法上。

@Transactional@Overridepublic void deletedept(Integer id) {deptmapper.deletedept(id);//根据id删除部门int i = 1/0;empMapper.deleteByDeptid(id);//根据部门id删除部门下的员工}

通过这个方法可以解决方法中出现RuntimeError,也就是运行时错误
但是,如果你去特意抛一个异常,那还是解决不了,比如下面

@Transactional@Overridepublic void deletedept(Integer id) throws Exception {deptmapper.deletedept(id);//根据id删除部门if(true){throw new Exception("错误");}empMapper.deleteByDeptid(id);//根据部门id删除部门下的员工}

这个时候就需要用到spring事务中的一个回滚事务注释@Transactional(rollbackFor = Exception.class)

@Transactional(rollbackFor = Exception.class)@Overridepublic void deletedept(Integer id) throws Exception {deptmapper.deletedept(id);//根据id删除部门if(true){throw new Exception("错误");}empMapper.deleteByDeptid(id);//根据部门id删除部门下的员工}

这样处理之后,所有的异常都会回滚

事务属性-传播行为:

事务传播行为:指的就是当一个事务方法被另一个事务方法调用时,这个事务方法应该如何进行事务控制。

设想一个场景:在一个事务里,又开启了另一个事务,那这个时候,这两个事务的回滚或者叫传播行为是怎么样的呢
 这个注解可以解释@Transactional(propagation = Propagation.REQUIRED)
    REQUIRED    【默认值】需要事务,有则加入,无则创建新事务
    REQUIRES_NEW    需要新事务,无论有无,总是创建新事务

当这个注解的值是这个的时候,就说明,两个事务时绑定在一起的,比如在delete这个方法中又新建了一个事务,然后delete方法因为
异常rollback了,那这个事务也得rollback。

但如果你设置的值是这个:@Transactional(propagation = Propagation.REQUIRES_NEW)
那这个时候,就会新开一个事务,就算上面的delete方法rollback了,你这个事务还是会接着执行。

propagation = Propagation.REQUIRES_NEW:比如:下订单前需要记录日志,不论订单保存成功与否,都需要保证日志记录能够记录成功。

举个例子:

@Transactional  
public void removeDepartmentAndEmployees(Integer deptId) {  deletedept(deptId); // 调用配置了 REQUIRES_NEW 的方法  
}  @Transactional(propagation = Propagation.REQUIRES_NEW)  
@Override  
public void deletedept(Integer id) {  deptMapper.deletedept(id); // 如果这条操作成功  empMapper.deleteByDeptid(id); // 假设这条操作失败  
}  

 在上面的例子中,假设 deletedept 方法中的 deptMapper.deletedept(id) 调用成功,但 empMapper.deleteByDeptid(id) 调用失败:

如果 deletedept 使用了 REQUIRES_NEW,则 deptMapper.deletedept(id) 将成功提交,即使后面的 empMapper.deleteByDeptid(id) 失败,部门的删除操作仍然会被提交成功。

这个功能还是挺强大的,不过也需要主要是否会发生数据不一致的问题


http://www.ppmy.cn/devtools/85563.html

相关文章

在 Windows 搭建 flink 运行环境并模拟流数据处理

一、引入 在大数据场景中,开发者追求高效与灵活,Linux 系统以其稳定性成为众多组件的首选,但在资源有限的情况下,在本机搭建一个 Linux 虚拟机集群却显得过于笨重,启动、运行占资源,需要配置网络,无法和windows共享资源,尤其是对只有 8GB 内存的 Windows 系统用户来说…

【MR】现代机器人学-算法库函数解析(C++版截止2024.4.7)

算法库提供以下函数的实现 NearZero: 判断一个值是否可以忽略为0。ad: 计算给定6维向量的6x6矩阵[adV]。Normalize: 返回输入向量的归一化版本。VecToso3: 返回角速度向量的反对称矩阵表示。so3ToVec: 返回由反对称矩阵表示的角速度向量。AxisAng3: 将指数旋转转换为其单独的分…

Spark RPC框架详解

文章目录 前言Spark RPC模型概述RpcEndpointRpcEndpointRefRpcEnv 基于Netty的RPC实现NettyRpcEndpointRefNettyRpcEnv消息的发送消息的接收RpcEndpointRef的构造方式直接通过RpcEndpoint构造RpcEndpointRef通过消息发送RpcEndpointRef Endpoint的注册Dispatcher消息的投递消息…

SpringMvc有几个上下文

你好&#xff0c;我是柳岸花明。 SpringMVC作为Spring框架的重要组成部分&#xff0c;其启动流程和父子容器机制是理解整个框架运行机制的关键。本文将通过一系列详细的流程图&#xff0c;深入剖析SpringMVC的启动原理与父子容器的源码结构。 SpringMVC 父子容器 父容器的创建 …

超级详细的SpringSecurity

文章目录 概述与shiro对比快速入门底层原理FilterDelegatingFilterProxyFilterChainProxySecurityFilterChainMultiple SecurityFilterChain 自定义登录流程解析基于内存的用户认证实现基于数据库的用户登录 实现用户新增功能controllerservice修改配置关闭csrf攻击防御修改默认…

linux下使用yum安装mysql

本文使用常规方式手动安装mysql 第一步 下载mysql的repo源 wget http://repo.mysql.com/mysql-community-release-el7-5.noarch.rpm第二步 安装mysql-community-release-el7-5.noarch.rpm包 rpm -ivh mysql-community-release-el7-5.noarch.rpm第三步 安装mysql-server yum -y…

mac 如何给默认终端(zsh)装插件,使得更美观易用

要给 Zsh 安装插件并使其更美观和易用&#xff0c;最常用的方法是使用 Oh My Zsh&#xff0c;这是一款开源的 Zsh 配置管理框架&#xff0c;提供了大量的插件和主题。以下是安装 Oh My Zsh 并配置插件和主题的步骤&#xff1a; 安装 Oh My Zsh 安装 Zsh&#xff1a; 确保你已经…

linux进程控制——进程创建、运行、exit终止——详解解析!

前言&#xff1a;本篇进入新章节——进程控制。 本章节和上一章节同样都是讲解进程&#xff0c; 但是内容上却比上一章内容好理解的多。上一章内容都是进程的概念性相关&#xff0c; 那个时候我们对于进程的理解还处于小白状态&#xff0c; 所以很多东西很抽象&#xff0c; 不好…