面试说道AOP都会说作用是对每层记录日志。但从来都没真正实践过。
场景:编写的业务,每个函数,特别是Controller和Dao层 想把 各个函数的参数记录到日志中,方便排查之后生产上的问题。
解决:使用AOP技术。具体实践如下:
文件目录结构:
第一步:编写注解
记得引入aspect的依赖,文章最下方贴出来了。
package com.example.demo.anno;
import java.lang.annotation.*;
/*创建日志注解*/
@Retention(RetentionPolicy.RUNTIME) //指定注解的生命周期 在运行时
@Target(ElementType.METHOD) // 指定注解只作用在 方法(函数)上
@Documented
@Inherited //父类写了该注解 则子类默认继承的话
public @interface Logs { //@interface 注解的格式
String action() default ""; //注解 可以接受的参数。
}
第二步:编写切入点的逻辑
package com.example.demo.anno;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
/*切入每个函数的的入口且有@log注解标记的函数 调用该部分:功能打印函数参数*/
@Aspect
@Component
public class AOPOfLog {
@Around(value = "within(com.example.demo.anno..*) && @annotation(logs)")
public Object doLogs(ProceedingJoinPoint joinPoint,Logs logs) throws Throwable {
System.out.println("打印注解的参数:"+logs.action());
Object[] args = joinPoint.getArgs();//函数有几个参数,agrs这个数组就会有几个元素
System.out.println("打印切入点的函数,传入的参数是:");
for (int i = 0; i < args.length; i++) {
//如果是字符串或者数字则直接打印,否则打印 对象.toStirng()
if(args[i] instanceof String || args[i] instanceof Integer) {
System.out.print(" " + args[i]);
}else {
System.out.print(" "+args[i].toString());
}
}
//调用切入点 的函数 即:放行拦截
Object obj = joinPoint.proceed();
return obj; //如果返回void 则相当于没放行
}
}
第三步:使用注解
package com.example.demo.anno;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
@RequestMapping("/")
@ResponseBody
public class TestController {
// post请求测试 http://localhost:8080/test1?parm1=1&parm2=2
@Logs(action = "我是test1的注解")
@RequestMapping("/test1")
public Object test1(String parm1,String parm2){
return "成功了";
}
//post请求测试 http://localhost:8080/test2?name=王建伟&age=18
@Logs(action = "我是test2的注解")
@RequestMapping("/test2")
public Object test2(User user){
return "成功了";
}
}
class User{
private String name;
private int age;
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
现象结果:在访问包下的 com.example.demo.anno 的所有标记了 @Logs的函数 都会打印传入该函数的 参数值。
前端:
后台打印:
总结:AOP就是在执行每个函数前、后、中 都可以“插入某个代码段”,也就是面试中常说的,将每个相同业务的公共部分抽取出来 进行统一处理,不仅提高了开发效率,还提高了日后代码可维护性(实现一处修改全局修改)
本项目的完整pom依赖如下:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.3</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.mybatis</groupId>
<artifactId>demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>demo</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<repositories>
<repository>
<id>com.e-iceblue</id>
<name>e-iceblue</name>
<url>http://repo.e-iceblue.com/nexus/content/groups/public/</url>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.12</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.1</version>
</dependency>
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>itextpdf</artifactId>
<version>5.5.13</version>
</dependency>
<!-- <dependency>-->
<!-- <groupId>com.itextpdf</groupId>-->
<!-- <artifactId>itext-asian</artifactId>-->
<!-- <version>5.2.0</version>-->
<!-- </dependency>-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>5.2.7.RELEASE</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjtools</artifactId>
<version>1.9.5</version>
</dependency>
<dependency>
<groupId>aopalliance</groupId>
<artifactId>aopalliance</artifactId>
<version>1.0</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.0</version>
</dependency>
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>3.3.0</version>
</dependency>
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>itext7-core</artifactId>
<version>7.1.12</version>
<type>pom</type>
</dependency>
<dependency>
<groupId> e-iceblue </groupId>
<artifactId>spire.pdf</artifactId>
<version>3.11.6</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.mybatis.generator</groupId>
<artifactId>mybatis-generator-maven-plugin</artifactId>
<version>1.3.2</version>
</plugin>
</plugins>
</build>
</project>
另一个版本的日志打印(使用:joinPoint.proceed() 存在bug。会多调用一次。)
借助:swagger的注解,在方法头上加 标题。形如
日志切面:
package com.XXX.product.aspect;
import com.patpat.product.helpers.LogAspectHelper;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
import org.springframework.util.StopWatch;
@Slf4j
@Aspect
@Component
public class LogAspect {
@Pointcut("execution(public * com.XX.product.controller..*.*(..)) " +
"&& !execution(public * com.XX.product.controller.XXController.*(..))")
public void pointcut() {
}
@Around("pointcut()")
public Object handle(ProceedingJoinPoint joinPoint) throws Throwable {
String title = LogAspectHelper.getMethodTitle(joinPoint);
String param = LogAspectHelper.getMethodParam(joinPoint);
String paramReturn = LogAspectHelper.getReturnResult(joinPoint);
log.info("[Controller start], {}, param->{},", title, param);
StopWatch stopWatch = new StopWatch();
stopWatch.start();
try {
return joinPoint.proceed();
} finally {
stopWatch.stop();
log.info("[Controller end], {}, elapsed time->{}ms,resultParam->{}", title, stopWatch.getTotalTimeMillis(),paramReturn);
}
}
}
//切面拦截后所做的工作:
package com.XX.product.helpers;
import cn.hutool.json.JSONUtil;
import com.fasterxml.jackson.databind.json.JsonMapper;
import io.swagger.annotations.ApiOperation;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.stream.Collectors;
public class LogAspectHelper {
private LogAspectHelper() {
}
public static String getMethodParam(ProceedingJoinPoint joinPoint) {
try {
JsonMapper jsonMapper = new JsonMapper();
return jsonMapper.writeValueAsString(Arrays.stream(joinPoint.getArgs())
.filter(param -> !(param instanceof HttpServletRequest)
&& !(param instanceof HttpServletResponse)
&& !(param instanceof MultipartFile)
&& !(param instanceof MultipartFile[])
).collect(Collectors.toList()));
} catch (Exception e) {
return "";
}
}
public static String getMethodTitle(ProceedingJoinPoint joinPoint) {
String title = "";
try {
Method[] methods = joinPoint.getSignature().getDeclaringType().getMethods();
for (Method method : methods) {
if (StringUtils.equalsIgnoreCase(method.getName(), joinPoint.getSignature().getName())) {
ApiOperation annotation = method.getAnnotation(ApiOperation.class);
if (ObjectUtils.isNotEmpty(annotation)) {
title = annotation.value();
break;
}
}
}
if (StringUtils.isBlank(title)) {
title = getMethodName(joinPoint);
}
} catch (Exception e) {
title = "";
}
return title;
}
private static String getMethodName(ProceedingJoinPoint joinPoint) {
return joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint.getSignature().getName();
}
public static String getReturnResult(ProceedingJoinPoint joinPoint) {
Object proceed = null;
try {
proceed = joinPoint.proceed();//会导致 方法多调用一次
} catch (Throwable e) {
throw new RuntimeException(e);
}
return JSONUtil.toJsonStr(proceed);
}
}
使用方式:
@ApiOperation("搜索-list")
@PostMapping("/list")
public ResponseDTO<OpenSearchQueryResult> XX(@RequestBody XXXhReq req){
return openSearchService.XXX(req);
}
每次调用/list 会打印:
[Controller start], openSearch搜索-list, param->[{XXX}]
。。。。。。其他业务日志
[Controller end], openSearch搜索-list, elapsed time->818ms,resultParam->{XXX}
可参考:(118条消息) springboot项目获取请求中的参数和返回值_小小逗比唐的博客-CSDN博客
AOP的5个注解:
本质:运用AOP的原理:5大方式
- 方法前执行 @Before
- 方法后执行 @AfterReturning
- 方法前后执行 @Around
- 方法执行抛出异常时执行 @AfterThrowing
- 方法执行后一定执行 @After
其中,@AfterReturning是方法正常执行完后会执行,如果方法抛出异常则不会再执行
@After类似于try{}finally{}的finally一定会被执行
以上注解 都是针对"切面"进行的,所以我们还需要了解如何找切面,我们这里找切面类似于,正则表达式,匹配到的包、类、方法、权限修饰符 才被算 切点
如何写切点表达式呢?使用@PointCut("exexcution(XXXX)")
并且运用上面这5个注解呢?基本模板如下:
@Aspect
@Component
public class logUtils {
@Pointcut("execution(public int caculator.Interface.MyCaculator.*(int,int))")
public void test(){}
@Before("test()")
public static void logStart(JoinPoint joinPoint){
System.out.println("logUtil前置方法【"+joinPoint.getSignature().getName()+"】开始执行" + "调用的参数是:" + Arrays.asList(joinPoint.getArgs()));
}
@AfterReturning(value = "test()",returning = "result")
public void logReturn(JoinPoint joinPoint,Object result){
System.out.println("logUtil返回方法【"+joinPoint.getSignature().getName()+"】执行完毕" + "返回的结果是:" + result);
}
@AfterThrowing(value= "test()",throwing = "exception")
public void logException(JoinPoint joinPoint,Exception exception){
System.out.println("logUtil异常方法【"+joinPoint.getSignature().getName()+"】出现异常" + "异常的原因是:" + exception);
}
@After("test()")
public static void logAfter(JoinPoint joinPoint){
System.out.println("logUtil结束方法【"+joinPoint.getSignature().getName()+"】结束执行");
}
}