JavaWeb(后端进阶)
AOP 概述:
AOP:Aspect Oriented Programming (面向切面编程、面向方法编程),是一种思想
优点:减少重复代码,代码无侵入,提高开发效率,维护方便
AOP 入门:
需求:统计部门管理各个业务层方法执行耗时
在 pom.xml 文件中导入 AOP 的依赖:
<!-- AOP依赖 -->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId>
</dependency>编写 AOP 程序:针对于特定方法根据业务需要进行编程
@Component
@Aspect//当前类为切面类
@Slf4j
public class RecordTimeAspect{@Around("execution(* org.example.service.impl.DeptServiceImpl.*(..))")public Object recordTime(ProceedingJoinPoint pjp) throws Throwable{//记录方法执行开始时间long begin = System.currentTimeMillis();//执行原始方法Object result = pjp.proceed();//记录方法执行结束时间long end = System.currentTimeMillis();//计算方法执行耗时log.info("方法执行耗时: {}毫秒",end-begin);return result;}
}AOP 详解:
连接点:JoinPoint
指的是可以被 AOP 控制的方法
在 SpringAOP 提供的 JoinPoint 当中,封装了连接点方法在执行时的相关信息
通知:Advice
指的是重复的逻辑,也就是共性的功能
切入点:PointCut
匹配连接点的条件,通知仅会在切入点方法被执行时被应用
切面:Aspect
描述通知与切入点的对应关系
切面所在的类,为切面类(被 @Aspect 注解标识的类)
目标对象:Target
指的是通知所应用的对象
通知类型:
Spring AOP 通知类型 | |
@Around | 环绕通知,此注解标注的通知方法在目标方法前、后都被执行 |
@Before | 前置通知,此注解标注的通知方法在目标方法前被执行 |
@After | 后置通知,此注解标注的通知方法在目标方法后被执行,无论是否有异常都会执行 |
@AfterReturning | 返回后通知,此注解标注的通知方法在目标方法后被执行,有异常不会执行 |
@AfterThrowing | 异常后通知,此注解标注的通知方法发生异常后执行 |
代码测试:
@Slf4j
@Component
@Aspect
public class MyAspect1{//前置通知@Before("execution(* org.example.service.*.*(..))")public void before(JoinPoint joinPoint){log.info("before ...");}//环绕通知@Around("execution(* org.example.service.*.*(..))")public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable{log.info("around before ...");//调用目标对象的原始方法执行Object result = proceedingJoinPoint.proceed();//原始方法如果执行时有异常,环绕通知中的后置代码将不会执行log.info("around after ...");return result;}//后置通知@After("execution(* org.example.service.*.*(..))")public void after(JoinPoint joinPoint){log.info("after ...");}//返回后通知(程序在正常执行的情况下,会执行的后置通知)@AfterReturning("execution(* org.example.service.*.*(..))")public void afterReturning(JoinPoint joinPoint){log.info("afterReturning ...");}//异常通知(程序在出现异常的情况下,执行的后置通知)@AfterThrowing("execution(* org.example.service.*.*(..))")public void afterThrowing(JoinPoint joinPoint){log.info("afterThrowing ...");}
}
程序发生异常的情况下:
@AfterReturning 标识的通知方法不会执行,@AfterThrowing 标识的通知方法执行
@Around 环绕通知中原始方法调用时有异常,通知中的环绕后的代码逻辑将不会执行(原始方法调用已经出现异常)
在使用通知时的注意事项:
@Around 环绕通知需要自己调用 ProceedingJoinPoint.proceed() 来让原始方法执行,其他通知不需要考虑目标方法执行
@Around 环绕通知方法的返回值,必须指定为 Object,来接收原始方法的返回值,否则原始方法执行完毕,将获取不到返回值
抽取:
Spring 提供了 @Pointcut 注解,该注解的作用是将公共的切入点表达式抽取出来,需要用到时引用该切入点表达式即可
@Slf4j
@Component
@Aspect
public class MyAspect1{//切入点方法(公共的切入点表达式)@Pointcut("execution(* org.example.service.*.*(..))")private void pt(){}//前置通知@Before("pt()")public void before(JoinPoint joinPoint){log.info("before ...");}//环绕通知@Around("pt()")public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable{log.info("around before ...");//调用目标对象的原始方法执行Object result = proceedingJoinPoint.proceed();//原始方法如果执行时有异常,环绕通知中的后置代码将不会执行log.info("around after ...");return result;}//后置通知@After("pt()")public void after(JoinPoint joinPoint){log.info("after ...");}//返回后通知(程序在正常执行的情况下,会执行的后置通知)@AfterReturning("pt()")public void afterReturning(JoinPoint joinPoint){log.info("afterReturning ...");}//异常通知(程序在出现异常的情况下,执行的后置通知)@AfterThrowing("pt()")public void afterThrowing(JoinPoint joinPoint){log.info("afterThrowing ...");}
}注意:当切入点方法使用 private 修饰时,仅能在当前切面类中引用该表达式, 当外部其他切面类中也要引用当前类中的切入点表达式,就需要把 private 改为 public,而在引用的时候,具体的语法为:
@Slf4j
@Component
@Aspect
public class MyAspect2{//引用 MyAspect1 切面类中的切入点表达式@Before("org.example.aop.MyAspect1.pt()")public void before(){log.info("MyAspect2 -> before ...");}
}通知顺序:
当在项目开发当中定义了多个切面类,而多个切面类中多个切入点都匹配到了同一个目标方法,此时当目标方法在运行的时候,这多个切面类当中的这些通知方法都会运行
在不同切面类中,默认按照切面类的类名字母排序:
目标方法前的通知方法:字母排名靠前的先执行
目标方法后的通知方法:字母排名靠前的后执行
控制通知的执行顺序:修改切面类的类名 或 使用 Spring 提供的 @Order 注解(推荐)
切入点表达式:
切入点表达式:描述切入点方法的一种表达式
作用:主要用来决定项目中哪些方法需要加入通知
常见形式:
execution(...):根据方法的签名来匹配
@annotation(...):根据注解匹配
execution:
execution 主要根据方法的返回值、包名、类名、方法名、方法参数等信息来匹配,语法为:
execution([访问修饰符] 返回值 [包名.类名.]方法名(方法参数) [throws 异常])使用通配符描述切入点:
* :单个独立的任意符号,可以通配任意返回值、包名、类名、方法名、任意类型的一个参数,也可以通配包、类、方法名的一部分
.. :多个连续的任意符号,可以通配任意层级的包,或任意类型、任意个数的参数
切入点表达式的语法规则:
方法的访问修饰符可以省略
返回值可以使用 * 号代替(任意返回值类型)
包名可以使用 * 号代替,代表任意包(一层包使用一个 * )
使用 ..配置包名,标识此包以及此包下的所有子包
类名可以使用 * 号代替,标识任意类
方法名可以使用 * 号代替,表示任意方法
可以使用 * 配置参数,一个任意类型的参数
可以使用 .. 配置参数,任意个任意类型的参数
根据业务需要,可以使用 且 (&&) 、或 (||) 、非 (!) 来组合比较复杂的切入点表达式:
execution(* org.example.service.DeptService.findAll(..)) || execution(* org.example.service.DeptService.deleteById(..))切入点表达式书写建议:
所有业务方法名在命名时尽量规范,方便切入点表达式快速匹配(如:findXxx,updateXxx)
描述切入点方法通常基于接口描述,而不是直接描述实现类,增强拓展性
在满足业务需要的前提下,尽量缩小切入点的匹配范围(如:包名尽量不使用 .. ,使用
*匹配单个包)
@annotation:
基于注解的方式来匹配切入点方法
自定义注解:LogOperation
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LogOperation{
}DetpServiceImpl:
@Service
public class DeptServiceImpl implements DeptService{@Autowiredprivate DeptMapper deptMapper;@Override@LogOperation//自定义注解:表示当前方法属于目标方法public List<Dept> findAll(){return deptMapper.findAll();}@Override@LogOperationpublic void deleteById(Integer id){deptMapper.deleteById(id);}/*...*/
}切面类:
@Slf4j
@Component
@Aspect
public class MyAspect{//前置通知@Before("@annotation(org.example.anno.LogOperation)")public void before(){log.info("MyAspect -> before ...");}//后置通知@After("@annotation(org.example.anno.LogOperation)")public void after(){log.info("MyAspect -> after ...");}
}AOP 案例:
需求:
将案例中增、删、改相关接口的操作日志记录到数据库表中
操作日志信息包含:
操作人、操作时间、执行方法的全类名、执行方法名、方法运行时参数、返回值、方法执行时长
代码实现:
准备工作:
在 pom.xml 中引入 AOP 的依赖:
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId>
</dependency>创建数据库表结构:
-- 操作日志表
create table operate_log(id int unsigned primary key auto_increment comment 'ID',operate_emp_id 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 '返回值, 存储json格式',cost_time int comment '方法执行耗时, 单位:ms'
) comment '操作日志表';创建实体类 OperateLog:
@Data
@NoArgsConstructor
@AllArgsConstructor
public class OperateLog{private Integer id; //IDprivate Integer operateEmpId; //操作人IDprivate LocalDateTime operateTime; //操作时间private String className; //操作类名private String methodName; //操作方法名private String methodParams; //操作方法参数private String returnValue; //操作方法返回值private Long costTime; //操作耗时
}创建日志操作 Mapper 接口:
@Mapper
public interface OperateLogMapper{//插入日志数据@Insert("insert into operate_log (operate_emp_id, operate_time, class_name, method_name, method_params, return_value, cost_time) " +"values (#{operateEmpId}, #{operateTime}, #{className}, #{methodName}, #{methodParams}, #{returnValue}, #{costTime});")void insert(OperateLog log);
}自定义注解 @LogOperation:
//自定义注解,用于标识哪些方法需要记录日志
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LogOperation{
}定义 AOP 记录日志的切面类:
@Aspect
@Component
public class OperationLogAspect{@Autowiredprivate OperateLogMapper operateLogMapper;//环绕通知@Around("@annotation(log)")public Object around(ProceedingJoinPoint joinPoint, LogOperation log) throws Throwable{//记录开始时间long startTime = System.currentTimeMillis();//执行方法Object result = joinPoint.proceed();//当前时间long endTime = System.currentTimeMillis();//耗时long costTime = endTime - startTime;//构建日志对象OperateLog operateLog = new OperateLog();operateLog.setOperateEmpId(getCurrentUserId());//需要实现 getCurrentUserId 方法operateLog.setOperateTime(LocalDateTime.now());operateLog.setClassName(joinPoint.getTarget().getClass().getName());operateLog.setMethodName(joinPoint.getSignature().getName());operateLog.setMethodParams(Arrays.toString(joinPoint.getArgs()));operateLog.setReturnValue(result.toString());operateLog.setCostTime(costTime);//插入日志operateLogMapper.insert(operateLog);return result;}//获取当前用户IDprivate int getCurrentUserId(){return 1;}
}在 Controller 层需要记录日志的方法上加上注解 @LogOperation:
@Slf4j
@RequestMapping("/depts")
@RestController
public class DeptController{@Autowiredprivate DeptService deptService;//查询部门列表@GetMappingpublic Result list(){log.info("查询部门列表");List<Dept> deptList = deptService.findAll();return Result.success(deptList);}//根据ID删除部门@LogOperation@DeleteMappingpublic Result delete(Integer id){log.info("根据Id删除部门, id: {}" , id);deptService.deleteById(id);return Result.success();}//新增部门@LogOperation@PostMappingpublic Result add(@RequestBody Dept dept){log.info("新增部门, dept: {}" , dept);deptService.add(dept);return Result.success();}//根据ID查询部门@GetMapping("/{id}")public Result getById(@PathVariable Integer id){log.info("根据ID查询部门, id: {}" , id);Dept dept = deptService.getById(id);return Result.success(dept);}//修改部门@LogOperation@PutMappingpublic Result update(@RequestBody Dept dept){log.info("修改部门, dept: {}" , dept);deptService.update(dept);return Result.success();}
}
@Slf4j//在类中自动生成日志对象
@RequestMapping("/clazzs")
@RestController//标识 RESTful 风格的控制器类
public class ClazzController{@Autowiredprivate ClazzService clazzService;//条件分页查询班级@GetMapping//绑定 HTTP GET 请求public Result page(String name,@DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate begin,//请求参数绑定@DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate end,@RequestParam(defaultValue = "1") Integer page,//接收前端传递的 URL 查询参数@RequestParam(defaultValue = "10") Integer pageSize){PageResult pageResult = clazzService.page(name,begin,end,page,pageSize);return Result.success(pageResult);}//查询全部班级@GetMapping("/list")public Result findAll(){List<Clazz> clazzList = clazzService.findAll();return Result.success(clazzList);}//新增班级@LogOperation@PostMappingpublic Result add(@RequestBody Clazz clazz){//将请求体中的json数据自动转换为指定的对象clazzService.add(clazz);return Result.success();}//根据ID查询班级@GetMapping("/{id}")public Result getInfo(@PathVariable Integer id){Clazz clazz = clazzService.getInfo(id);return Result.success(clazz);}//修改班级@LogOperation@PutMappingpublic Result update(@RequestBody Clazz clazz){clazzService.update(clazz);return Result.success();}//删除班级@LogOperation@DeleteMapping("/{id}")public Result delete(@PathVariable Integer id){clazzService.deleteById(id);return Result.success();}
}测试操作日志记录功能:

连接点:
在 Spring 中用 JoinPoint 抽象了连接点,用它可以获得方法执行时的相关信息,如目标类名、方法名、方法参数等
对于 @Around 通知,获取连接点信息只能使用 ProceedingJoinPoint 类型
@Around("execution(* org.example.service.DeptService.*(..))")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable{String className = joinPoint.getTarget().getClass().getName();//获取目标类名Signature signature = joinPoint.getSignature();//获取目标方法签名String methodName = joinPoint.getSignature().getName();//获取目标方法名Object[] args = joinPoint.getArgs();//获取目标方法运行参数Object res = joinPoint.proceed();//执行原始方法,获取返回值(环绕通知)return res;
}对于其他四种通知,获取连接点信息只能使用 JoinPoint,是 ProceedingJoinPoint 的父类型
@Before("execution(* org.example.service.DeptService.*(..))")
public void before(JoinPoint joinPoint) {String className = joinPoint.getTarget().getClass().getName();//获取目标类名Signature signature = joinPoint.getSignature();//获取目标方法签名String methodName = joinPoint.getSignature().getName();//获取目标方法名Object[] args = joinPoint.getArgs();//获取目标方法运行参数
}获取当前登录员工:
ThreadLocal:
ThreadLocal 并不是一个 Thread,而是 Thread 的局部变量
ThreadLocal 为每个线程提供一份单独的存储空间,具有线程隔离的效果,不同的线程之间不会相互干扰

常见方法:
public void set(T value) 设置当前线程的线程局部变量的值
public T get() 返回当前线程所对应的线程局部变量的值
public void remove() 移除当前线程的线程局部变量
记录当前登录员工:

定义 ThreadLocal 操作的工具类,用于操作当前登录员工 ID:
package org.example.utils;public class CurrentHolder{private static final ThreadLocal<Integer> CURRENT_lOCAL = new ThreadLocal<>();public static void setCurrentId(Integer employeeId){CURRENT_lOCAL.set(employeeId);}public static Integer getCurrentId(){return CURRENT_lOCAL.get();}public static void remove(){CURRENT_lOCAL.remove();}
}在 TokenFilter 中,解析完当前登录员工 ID,将其存入 ThreadLocal (用完后需将其删除)
//令牌校验过滤器
@Slf4j
//@WebFilter(urlPatterns = "/*")
public class TokenFilter implements Filter{@Overridepublic void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException {HttpServletRequest request = (HttpServletRequest) req;//转换为HTTP请求对象HttpServletResponse response = (HttpServletResponse) resp;//转换为HTTP响应对象//获取请求urlString url = request.getRequestURL().toString();//判断是否是登录请求if(url.contains("login")){//登录请求log.info("登录请求 , 放行");chain.doFilter(request, response);return;}//获取请求头中的令牌(token)String jwt = request.getHeader("token");//判断令牌是否存在if(!StringUtils.hasLength(jwt)){//jwt为空log.info("获取到jwt令牌为空, 返回错误结果");response.setStatus(HttpStatus.SC_UNAUTHORIZED);//设置响应状态码为401return;}//解析tokentry{Claims claims = JwtUtils.parseJWT(jwt);Integer empId = Integer.valueOf(claims.get("id").toString());CurrentHolder.setCurrentId(empId);}catch(Exception e){//解析失败e.printStackTrace();log.info("解析令牌失败, 返回错误结果");response.setStatus(HttpStatus.SC_UNAUTHORIZED);//设置响应状态码为401return;}//放行log.info("令牌合法, 放行");chain.doFilter(request , response);//允许请求继续访问目标接口//清空当前线程绑定的IDCurrentHolder.remove();}
}在 AOP 程序中,从 ThreadLocal 中获取当前登录员工的 ID:
@Aspect
@Component
public class OperationLogAspect{@Autowiredprivate OperateLogMapper operateLogMapper;//环绕通知@Around("@annotation(log)")public Object around(ProceedingJoinPoint joinPoint, LogOperation log) throws Throwable{//记录开始时间long startTime = System.currentTimeMillis();//执行方法Object result = joinPoint.proceed();//当前时间long endTime = System.currentTimeMillis();//耗时long costTime = endTime - startTime;//构建日志对象OperateLog operateLog = new OperateLog();operateLog.setOperateEmpId(getCurrentUserId());//需要实现 getCurrentUserId 方法operateLog.setOperateTime(LocalDateTime.now());operateLog.setClassName(joinPoint.getTarget().getClass().getName());operateLog.setMethodName(joinPoint.getSignature().getName());operateLog.setMethodParams(Arrays.toString(joinPoint.getArgs()));operateLog.setReturnValue(result.toString());operateLog.setCostTime(costTime);//插入日志operateLogMapper.insert(operateLog);return result;}//获取当前用户IDprivate int getCurrentUserId(){return CurrentHolder.getCurrentId();}
}在同一个线程/同一个请求中,进行数据共享就可以使用 ThreadLocal
