- 项目搭建
- 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测试
- domain
- Mapper接口和Sql文件
- Service接口和实现类
- Controller实现
- 测试
二. 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{..}}
- AOP入门
接下来我们使用AOP模拟事务控制。事务是把多个操作看成一个整体,三层架构中可以在Service层进行业务处理执行多个操作,所以事务都是控制在Service业务层。事务简单回顾:
- 事务是把多个操作看成一个整体,要么都成功,要么都不成功
- 事务的操作主要有三步:开启事务、提交事务、回滚事务、关闭事务
- 事务的四大特性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.ThrowablejoinPoint.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();}
}
- 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();}}
}
- Aop相关术语
3.1. 核心概念
名称 | 说明 |
---|---|
Joinpoint:连接点 | 连接点指的是可以被Aop控制的方法,例如:入门程序当中所有的Service层方法都是可以被Aop控制的 |
Pointcut:切入点 | 切入点指的是哪些类、方法要被拦截,也就是哪些连接点要被拦截,例如:入门程序中切入点就是Service层所有方法 |
Advice:通知 | 通知指的是要作用到连接点的功能,例如:入门程序中的通知 |
Target:目标 | 目标指的是被代理的对象,例如:入门案例中的UserService就是目标对象 |
Aspect:切面 | 切面指的是切入点和通知的结合,例如:入门案例中的TxManager就被定义为切面 |
Proxy:代理 | 代理指的是被增强后的对象,也就是织入了增强处理的类,在程序运行时看的到 |
3.2. 通知分类
通知 | 说明 |
---|---|
before:前置通知 | 通知方法在目标方法调用之前执行 |
after:最终通知 | 通知方法在目标方法执行后执行,核心方法是否异常都会执行 |
after-returning:后置通知 | 通知方法会在目标方法执行后执行,核心方法异常后不执行 |
after-throwing:异常通知 | 通知方法会在目标方法抛出异常后执行 |
around:环绕通知 | 通知方法会将目标方法封装起来 |
- 代理模式
概念:代理模式的英文叫做Proxy或Surrogate,中文都可译为代理。所谓代理,就是一个人或者一个机构代表另一个人或者另一个机构采取行动。在一些情况下,一个客户不想或者不能够直接引用一个对象,而代理对象可以在客户端和目标对象之间起到中介的作用
分类:
静态代理(在执行之前就要为业务类生成代理类,业务类是否运行都会生成代理类,非常不灵活,添加一个业务类,也需要添加相应的代理类。)
动态代理(Java1.3就提供了动态代理,让咱们可以在代码运行期动态生成代理类。)
jdk的动态代理只允许完成有接口的类的代理,如果没有就需要用第三发的CGLIB的方式实现
1. 如果代理的类有接口,默认采用原生JDK的方式实现动态代理 2. 如果代理的类没有接口,只能采用第三方的CGLIB的方式实现动态代理 注意:其实有接口的也可以强制采用使用CGLIB的动态代理模式,不过需要单独配置
AOP的代理模式:
概念:Aop底层是通过动态代理实现,而动态代理底层可以通过原生的JDK方式和第三方cglib的方式实现
- Aop的使用场景
日志记录:记录用户的所有操作到数据库中
事务管理:在Service层管理事务
权限验证:在执行业务代码前执行权限校验
性能监控:记录Service层方法执行耗时
注意:AOP可以拦截指定的方法,并且对方法增强,比如:事务、日志、权限、性能监测等增强,而且无需侵入到业务代码中,使业务与非业务处理逻辑分离
三、SpringBoot中管理事务
概念:Spring事务管理分为编程式和声明式的两种方式
- 编程式:是指在写业务代码中将事务代码也写进去,这是很古老的做法了(几乎不用了)
- 声明式:基于AOP将具体业务逻辑与事务处理解耦,声明式事务管理使业务代码逻辑不受污染 (现在都用这个声明式)
3.1 声明式事务有两种方式
- 配置文件中做相关的事务规则声明
- 基于@Transactional 注解的方式(主流)
3.2 Transactional注解作用
概念:Spring提供的用来控制事务回滚/提交的一个注解,属于声明式事务的实现。
作用域:@Transactional可以写在类和方法上
当标注在类上的时候,表示给该类所有的public方法添加上@Transactional注解
当标注在方法上的时候,事务的作用域就只在该方法上生效,并且如果类及方法上都配置@Transactional注解时,方法的注解会覆盖类上的注解
注意:Transactional注解的底层实现原理基于AOP和代理模式
3.3 Transactional注解使用&事务传播机制
- 注解使用:
第一步:在Service层实现类上加上@Transaction注解
第二步:直接测试即可
- 事务传播机制
概念:以后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 {@Autowiredprivate HttpServletRequest request; // 用来获取ip的@Autowiredprivate LogsMapper logsMapper; // 给类中属性赋值//只有有@Logs注解的方法才作用 - around方法作用到使用了@Logs注解的方法@Around("@annotation(cn.itsource.anno.Logs)") // @annotationpublic 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));//返回值 @TODOSignature 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@Logpublic 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@Logpublic void add(User user) {userMapper.add(user);}