一、AOP
这是一个Java面试题老生常谈的问题,下面我就来简单说一下什么是AOP。
1.1 什么是AOP
AOP(Aspect Oriented Programming)是一个面向切面编程的思想,是对OOM(Object-Oriented Model)的一种补充,它可以不修改源码的方式来增强代码。这样说可能有点抽象,下面来举个例子
比如,现在有上面这么多模块的代码,我们现在需要在每个代码中来增加日志和事务的功能。
传统的方法,肯定是在每个模块的对应的方法中添加对应的功能,而AOP则是在模块中找到切点,把日志和事务的方法织入到对应的方法中。这就是AOP
1.2 名词解释
AOP 有自己的一套术语,我们必须了解一下这些行话,才能更好地理解 AOP。
1.通知 (Advice)
AOP在特定的切入点上具体执行哪些方法,什么时候执行。大致分为before、after、afterReturning、afterThrowing、around
2.连接点 (JoinPoint)
哪些方法需要被AOP增强,这些方法就叫做连接点。
3.切点(PointCut)
切点用于定义切面的位置,也就是捕获哪些连接点的调用然后执行"通知"的操作(什么地点)。
4.切面(Aspect)
AOP核心就是切面,它将多个类的通用行为封装成可重用的模块,该模块含有一组API提供横切功能。比如,一个日志模块可以被称作日志的AOP切面。根据需求的不同,一个应用程序可以有若干切面。在Spring AOP中,切面通过带有@Aspect注解的类实现。
5.目标对象( Target )
就是被增强的对象
6.织入(Weaving)
织入是把切面应用到切点对应的连接点的过程。切面在指定连接点被织入到目标对象中。
1.3 通知类型
通知(advice)是你在你的程序中想要应用在其他模块中的横切关注点的实现。Advice主要有以下5种类型:
- 前置通知(Before Advice):在连接点之前执行的Advice,不过除非它抛出异常,否则没有能力中断执行流。使用@Before注解使用这个Advice。
- 返回之后通知(After Retuning Advice):在连接点正常结束之后执行的Advice。例如,如果一个方法没有抛出异常正常返回。通过 @AfterReturning注解使用它。
- 抛出(异常)后执行通知(After Throwing Advice):如果一个方法通过抛出异常来退出的话,这个Advice就会被执行。通过 @AfterThrowing注解来使用。
- 后置通知(After Advice):无论连接点是通过什么方式退出的(正常返回或者抛出异常)都会执行在结束后执行这些Advice。通过 @After注解使用。
- 围绕通知(Around Advice):围绕连接点执行的Advice,就你一个方法调用。这是最强大的Advice。通过@Around注解使用。
1.4 AOP底层原理
它是基于代理设计模式,而代理设计模式又分为静态代理和动态代理,静态代理比较简单就是一个接口,分别由一个真实实现和一个代理实现,而动态代理分为基于接口的JDK动态代理和基于类的CGLIB的动态代理。
二、自定义注解
下面就以我们实际开发为例,我们先需要给每个接口加上注解,并获取接口的信息。
2.1 创建注解
/**
* 统一日志注解
*/
@Target({ ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
public @interface LogAnnotation {
/**
* value : 目标url,如:“/open/api/cif/sync/lessee”
* @return
*/
String value() default "";
/**
* type : 接口类型,如:“in”
* @return
*/
String type() default "";
/**
* target : 目标资方 获取资方枚举
* @return
*/
String target() default "";
}
@Retention: 表示该注解的生命周期,是RetentionPolicy类型的,该类型是一个枚举类型,可提供三个值选择,分别是:CLASS、RUNTIME、SOURCE
- RetentionPolicy.CLASS: 注解被保留到class文件,但jvm加载class文件时候被遗弃,这是默认的生命周期;
- RetentionPolicy.RUNTIME: 注解不仅被保存到class文件中,jvm加载class文件之后,仍然存在;
- RetentionPolicy.SOURCE: 注解只保留在源文件,当Java文件编译成class文件的时候,注解被遗弃; 由此可见生命周期关系:SOURCE < CLASS < RUNTIME,我们一般用RUNTIME
@Target: 表示该注解的作用范围,是ElementType类型的,该类型是一个枚举类型,一共提供了10个值选择,我们最常用的几个:FIELD、TYPE、PARAMETER、METHOD - ElementType.FIELD:用于字段、枚举的常量
- ElementType.TYPE:用于接口、类、枚举、注解
- ElementType.PARAMETER:用于方法形参:值传递不是简单的把实参传递给形参,而是,实参建立了一个副本,然后把副本传递给了形参,所以形参改变并不会影响到实参
- ElementType.METHOD:用于方法
2.2 定义注解行为
这一步就是我们需要如何去处理我们的注解,这里面有五个方法,分别是@Before、@after、@Around、AfterReturning、AfterThrowing,我们常用的一般是前三个,看具体需求选择适合自己的方式。
import com.alibaba.fastjson.JSONObject;
import com.alibaba.nacos.shaded.com.google.gson.Gson;
import com.midea.mhpdp.mbf.api.request.CommonReq;
import com.midea.mhpdp.mbf.api.response.Result;
import com.midea.mhpdp.mic.api.constant.common.aop.LogAnnotation;
import com.midea.mhpdp.mic.app.aop.request.LogAnnotationCmdReq;
import com.midea.mhpdp.mic.app.token.command.service.LogAdviceCmdAppService;
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.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.util.*;
import java.util.stream.Collectors;
/**
* @author LayTao
* @date 2023/11/28
* @description 日志注解类的AOP实现
*/
@Component
@Aspect
@Slf4j
public class LogAdvice
{
@Resource
private LogAdviceCmdAppService logAdviceCmdAppService;
@Around(value = "execution(* com.midea.mhpdp.mic.facade..*.*(..))")
public Object savaLog(ProceedingJoinPoint joinPoint) throws Throwable{
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
LogAnnotation logAnnotation = methodSignature.getMethod().getDeclaredAnnotation(LogAnnotation.class);
// 获取请求
RequestAttributes ra = RequestContextHolder.getRequestAttributes();
ServletRequestAttributes sra = (ServletRequestAttributes) ra;
assert sra != null;
HttpServletRequest request = sra.getRequest();
String url = request.getRequestURL().toString();
String method = request.getMethod();
Enumeration<String> headerNames = request.getHeaderNames();
List<String> headList = new ArrayList<>();
while (headerNames.hasMoreElements()){
String headValue = request.getHeader(headerNames.nextElement());
headList.add(headerNames.nextElement()+":"+headValue);
}
// 过滤掉部分请求参数,方法参数为HttpServletRequest HttpServletResponse MultipartFile
List<String> filterList = Arrays.asList(joinPoint.getArgs()).stream().map(obj ->{
String objStr = parseParams(obj);
return objStr;
}).collect(Collectors.toList());
// 请求参数
String queryString = filterList.size() > 1 ? filterList.stream().collect(Collectors.joining(";")) :
(filterList.size() == 1 ? filterList.get(0) : "");
// 请求头
String queryHead = headList.size() > 1 ? headList.stream().collect(Collectors.joining(";")) :
(headList.size() == 1 ? headList.get(0) : "");
//执行方法
Object object = joinPoint.proceed();
// 响应结果
int response = parseResponse(object);
LogAnnotationCmdReq cmdReq = null;
if(Objects.isNull(logAnnotation)){
// 如果没有加@LogAnnotation 不记录日志 直接执行方法
cmdReq = LogAnnotationCmdReq.builder()
.object(object.toString())
.joinPoint(joinPoint)
.queryString(queryString)
.method(method)
.head(queryHead)
.isSuccess(response)
.build();
}else{
cmdReq = LogAnnotationCmdReq.builder()
.url(logAnnotation.value())
.type(logAnnotation.type())
.targetSys(logAnnotation.target())
.object(object.toString())
.joinPoint(joinPoint)
.queryString(queryString)
.method(method)
.head(queryHead)
.isSuccess(response)
.build();
}
// 保存接口日志信息
logAdviceCmdAppService.saveIntegrateLogInfo(cmdReq);
return object;
}
/**
* 解析响应结果
* @return
*/
public int parseResponse (Object obj){
if(obj == null){
return 1;
}
String json = "";
Gson gson = new Gson();
JSONObject parseObject = JSONObject.parseObject(gson.toJson(obj, Result.class));
String code = parseObject.getString("code");
JSONObject data = parseObject.getJSONObject("data");
if(!StringUtils.isEmpty(code) && code.startsWith("9")){
return 1;
}
if( !StringUtils.isEmpty(data) && !code.startsWith("9") && data.getString("code").equals("0")){
return 2;
}
return 1;
}
/**
* 解析参数
* @return
*/
public String parseParams (Object obj){
if(obj == null){
return "";
}
String json = "";
// 如果参数是CommonReq封装参数
if(obj.toString().contains("CommonReq")){
Gson gson = new Gson();
JSONObject parseObject = JSONObject.parseObject(gson.toJson(obj, CommonReq.class));
json = parseObject.getString("restParams");
}else{
json = obj.toString();
}
return json;
}
}
2.3 实现日志落库
/**
* @author LayTao
* @date 2023/11/28
* @description 接口日志信息Ability
*/
@Slf4j
@DomainService(id="LogAdviceAbility",domainServiceName = "LogAdviceAbility",domainServiceDes = "接口日志信息Ability")
public class LogAdviceAbility
{
@Value("${hxfl.gateway:https://dcms-test.hxfl.com.cn:38080}")
private String gateway;
@Autowired
private LogAdviceOps logAdviceOps;
/**
* 保存接口日志信息
*
* dmReq
*/
public void saveIntegrateLogInfo(LogAdviceInfoDmReq dmReq)
{
// 接口URL
String url = gateway + dmReq.getUrl();
IntegrateInoutRecordPo recordPo = IntegrateInoutRecordPo.builder()
.recordType(StringUtils.isEmpty(dmReq.getType()) ? "" : dmReq.getType())
.bizCode(StringUtils.isEmpty(dmReq.getUrl()) ? "" : dmReq.getUrl())
.reqProtocol("Http")
.httpMethod(dmReq.getMethod())
.httpHeader(dmReq.getHead())
.targetUrl(StringUtils.isEmpty(dmReq.getUrl()) ? "" : url)
.paramJsonStr(dmReq.getQueryString())
.responseJsonStr(dmReq.getObject())
.isSuccess(dmReq.getIsSuccess())
.targetSys(dmReq.getTargetSys())
.createdBy("system")
.createTime(DateUtil.getDateTime())
.build();
logAdviceOps.saveLogInfo(recordPo);
}
}
这样就实现了 接口日志功能