什么是AOP
AOP是Aspect Oriented Programming的缩写,即:面向切面编程,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。
相关概念
通知(Advice)
通知描述了切面要完成的工作以及何时执行。
- 前置通知(Before):在目标方法调用前调用通知功能;
- 后置通知(After):在目标方法调用之后调用通知功能,不关心方法的返回结果;
- 返回通知(AfterReturning):在目标方法成功执行之后调用通知功能;
- 异常通知(AfterThrowing):在目标方法抛出异常后调用通知功能;
- 环绕通知(Around):通知包裹了目标方法,在目标方法调用之前和之后执行自定义的行为。
连接点(JoinPoint)
通知功能被应用的时机,也就是目标方法被调用的时候。
切点(Pointcut)
切点定义了通知功能被应用的范围。
切面(Aspect)
切面是通知和切点的结合,定义了何时、何地应用通知功能。
引入(Introduction)
在无需修改现有类的情况下,向现有的类添加新方法或属性。
织入(Weaving)
把切面应用到目标对象并创建新的代理对象的过程。
Spring AOP
引入
<!--SpringBoot项目导入AOP-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
相关注解
- @Aspect:用于定义切面
- @Before:通知方法会在目标方法调用之前执行
- @After:通知方法会在目标方法返回或抛出异常后执行
- @AfterReturning:通知方法会在目标方法返回后执行
- @AfterThrowing:通知方法会在目标方法抛出异常后执行
- @Around:通知方法会将目标方法封装起来
- @Pointcut:定义切点表达式
切点表达式
指定通知被应用的范围
格式:
execution(方法修饰符 返回类型 方法所属的包.类名.方法名称(方法参数)
举例:
// com.lppnb.controller包中所有类的public方法都应用切面里的通知
execution(public * com.lppnb.controller.*.*(..))
示例代码
/**
* 统一日志处理切面
*/
@Aspect
@Component
@Order(1)
@Slf4j
public class WebLogAspect {
@Pointcut("execution(public * com.lppnb.controller.*.*(..))")
public void webLog() {
}
@Before("webLog()")
public void doBefore(JoinPoint joinPoint) throws Throwable {
}
@AfterReturning(value = "webLog()", returning = "ret")
public void doAfterReturning(Object ret) throws Throwable {
}
@Around("webLog()")
public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
long startTime = System.currentTimeMillis();
//获取当前请求对象
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
//记录请求信息
WebLog webLog = new WebLog();
Object result = joinPoint.proceed();
Signature signature = joinPoint.getSignature();
MethodSignature methodSignature = (MethodSignature) signature;
Method method = methodSignature.getMethod();
if (method.isAnnotationPresent(ApiOperation.class)) {
ApiOperation apiOperation = method.getAnnotation(ApiOperation.class);
webLog.setDescription(apiOperation.value());
}
long endTime = System.currentTimeMillis();
String urlStr = request.getRequestURL().toString();
webLog.setBasePath(StrUtil.removeSuffix(urlStr, URLUtil.url(urlStr).getPath()));
webLog.setIp(request.getRemoteUser());
webLog.setMethod(request.getMethod());
webLog.setParameter(getParameter(method, joinPoint.getArgs()));
webLog.setResult(result);
webLog.setSpendTime((int) (endTime - startTime));
webLog.setStartTime(startTime);
webLog.setUri(request.getRequestURI());
webLog.setUrl(request.getRequestURL().toString());
log.info("{}", JSONUtil.parse(webLog));
return result;
}
/**
* 根据方法和传入的参数获取请求参数
*/
private Object getParameter(Method method, Object[] args) {
List<Object> argList = new ArrayList<>();
Parameter[] parameters = method.getParameters();
for (int i = 0; i < parameters.length; i++) {
//将RequestBody注解修饰的参数作为请求参数
RequestBody requestBody = parameters[i].getAnnotation(RequestBody.class);
if (requestBody != null) {
argList.add(args[i]);
}
//将RequestParam注解修饰的参数作为请求参数
RequestParam requestParam = parameters[i].getAnnotation(RequestParam.class);
if (requestParam != null) {
Map<String, Object> map = new HashMap<>();
String key = parameters[i].getName();
if (!StringUtils.isEmpty(requestParam.value())) {
key = requestParam.value();
}
map.put(key, args[i]);
argList.add(map);
}
}
if (argList.size() == 0) {
return null;
} else if (argList.size() == 1) {
return argList.get(0);
} else {
return argList;
}
}
}
自定义注解
自定义Spring注解是AOP的典型应用
创建一个注解主要分两部分,一部分是创建注解类,一部分是创建一个切面类。
元注解
自定义注解类需要用元注解修饰以定义该类的一些基本特征。何谓元注解?就是注解的注解。
4个元注解:@Target、@Retention、@Documented、@Inherited。
@Target
@Target注解用于定义注解的使用位置,如果没有该项,表示注解可以用于任何地方。
@Target的ElementType取值有以下类型:
- TYPE:类,接口(包括注解类型)或枚举声明
- FIELD:字段(包括枚举常量)
- METHOD:方法
- PARAMETER:参数
- CONSTRUCTOR:构造方法
- LOCAL_VARIABLE:局部变量
- ANNOTATION_TYPE:注解类型
- PACKAGE:包
@Retention
@Retention注解用于指明修饰的注解的生命周期,即会保留到哪个阶段。
@Retention的RetentionPolicy的取值包含以下三种:
- SOURCE:源码级别保留,编译后即丢弃。
- CLASS:编译级别保留,编译后的class文件中存在,在jvm运行时丢弃,这是默认值。
- RUNTIME:运行级别保留,编译后的class文件中存在,在jvm运行时保留,可以被反射调用。
@Documented
指明修饰的注解可以被例如javadoc此类的工具文档化,只负责标记,没有成员取值。
@Inherited
@Inherited注解用于标注一个父类的注解是否可以被子类继承
创建注解类
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAnn {
String value() default "d";
}
创建切面类
@Aspect
@Component
public class MyAnnAop {
private Logger logger= LoggerFactory.getLogger(MyAnnAop.class);
@Pointcut("@annotation(com.example.demo.annotation.MyAnn)")
public void ann(){
}
@Before("ann()")
public void before(JoinPoint joinPoint){
logger.info("打印:开始前");
}
@AfterReturning(value = "ann()",returning = "res")
public Object dochange(JoinPoint joinPoint,Object res){
logger.info("AfterReturning通知开始-获取数据:{}",res);
//获取数据
Map<String,String> map= (Map<String, String>) res;
//添加新值
map.put("s1","我是在AOP中添加的新值");
return map;
}
}
注意事项
内部方法调用,调用的是具体方法,并没有调用使用AOP后生成的代理方法,所以AOP会失效。