- 项目搭建
- 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.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();
}
}
- 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 {
@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);
}