AOP
向切面编程通过提供另一种思考程序结构的方式来补充面向对象编程(OOP)。OOP中模块化的关键单元是类,而AOP中模块化的单元是切面。切面支持跨多个类型和对象的关注点(如事务管理)的模块化。(在AOP教程中,此类关注点通常被称为"横切"关注点)
1.AOP 概念
对AOP中Aspect(切面)、JoinPoint(连接点)、PointCut(切入点)、Weaving(织入)、Advice(通知)等关键的术语和概念进行讲解
2.AOP使用(注解方式)
1、开启AOP
@Configuration
@ComponentScan("com.spring.study.springfx.aop")
// 开启AOP
@EnableAspectJAutoProxy
public class Config {
}
核心点就是在配置类上添加@EnableAspectJAutoProxy
,这个注解中有两个属性如下:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(AspectJAutoProxyRegistrar.class)
public @interface EnableAspectJAutoProxy {
// 是否使用CGLIB代理,默认不使用。默认使用JDK动态代理
boolean proxyTargetClass() default false;
// 是否将代理类作为线程本地变量(threadLocal)暴露(可以通过AopContext访问)
// 主要设计的目的是用来解决内部调用的问题
boolean exposeProxy() default false;
}
2、申明切面
@Aspect // 申明是一个切面
@Component // 切记,一定要将切面交由Spring管理,否则不起作用
public class DmzAnnotationAspect {
//......
}
3、申明切点
我们一般都会通过切点表达式来申明切点,切点表达式一般可以分为以下几种
切点表达式
excecution表达式
语法:
execution(modifiers-pattern? ret-type-pattern declaring-type-pattern?name-pattern(param-pattern) throws-pattern?)
这里问号表示当前项是非必填的,其中各项的语义如下:
- modifiers-pattern(非必填):方法的可见性,如public,protected;
- ret-type-pattern**(必填)**:方法的返回值类型,如int,void等;
- declaring-type-pattern(非必填):方法所在类的全路径名,如com.spring.Aspect;
- name-pattern**(必填)**:方法名类型,如buisinessService();
- param-pattern**(必填)**:方法的参数类型,如java.lang.String;
- throws-pattern(非必填):方法抛出的异常类型,如java.lang.Exception;
可以看到,必填的参数只有三个,返回值,方法名,方法参数。
示例
按照上面的语法,我们可以定义如下的切点表达式
// 1.所有权限为public的,返回值不限,方法名称不限,方法参数个数及类型不限的方法,简而言之,所有public的方法
execution(public * *(..))
// 2.所有权限为public的,返回值限定为String的,方法名称不限,方法参数个数及类型不限的方法
execution(public java.lang.String *(..))
// 3.所有权限为public的,返回值限定为String的,方法名称限定为test开头的,方法参数个数及类型不限的方法
execution(public java.lang.String test*(..))
// 4.所有权限为public的,返回值限定为String的,方法所在类限定为com.spring.study.springfx.aop.service包下的任意类,方法名称限定为test开头的,方法参数个数及类型不限的方法
execution(public java.lang.String com.spring.study.springfx.aop.service.*.test*(..))
// 5.所有权限为public的,返回值限定为String的,方法所在类限定为com.spring.study.springfx.aop.service包及其子包下的任意类,方法名称限定为test开头的,方法参数个数及类型不限的方法
execution(public java.lang.String com.spring.study.springfx.aop.service..*.test*(..))
// 6.所有权限为public的,返回值限定为String的,方法所在类限定为com.spring.study.springfx.aop.service包及其子包下的Dmz开头的类,方法名称限定为test开头的,方法参数个数及类型不限的方法
execution(public java.lang.String com.spring.study.springfx.aop.service..Dmz*.test*(..))
// 7.所有权限为public的,返回值限定为String的,方法所在类限定为com.spring.study.springfx.aop.service包及其子包下的Dmz开头的类,方法名称限定为test开头的,方法参数限定第一个为String类,第二个不限但是必须有两个参数
execution(public java.lang.String com.spring.study.springfx.aop.service..Dmz*.test*(String,*))
// 8.所有权限为public的,返回值限定为String的,方法所在类限定为com.spring.study.springfx.aop.service包及其子包下的Dmz开头的类,方法名称限定为test开头的,方法参数限定第一个为String类,第二个可有可无并且不限定类型
execution(public java.lang.String com.spring.study.springfx.aop.service..Dmz*.test*(String,..))
看完上面的例子不知道大家有没有疑问,比如为什么修饰符一直是public
呢?其它修饰符行不行呢?修饰符的位置能不能写成*
这种形式呢?
答:
如果使用的是JDK动态代理,这个修饰符必须是public,因为JDK动态代理是针对于目标类实现的接口进行的,接口的实现方法必定是public的。
如果不使用JDK动态代理而使用CGLIB代理(@EnableAspectJAutoProxy(proxyTargetClass = true)
)那么修饰符还可以使用protected或者默认修饰符。但是不能使用private修饰符,因为CGLIB代理生成的代理类是继承目标类的,private方法子类无法复写,自然也无法代理。基于此,修饰符是不能写成*这种格式的。
@annotation表达式
语法
@annotation(annotation-type)
示例
// 代表所有被DmzAnnotation注解所标注的方法
// 使用注解的方法定义切点一般会和自定义注解配合使用
@annotation(com.spring.study.springfx.aop.annotation.DmzAnnotation)
within表达式
语法
within(declaring-type-pattern)
示例
// within表达式只能指定到类级别,如下示例表示匹配com.spring.service.BusinessObject中的所有方法
within(com.spring.service.BusinessObject)
// within表达式能够使用通配符,如下表达式表示匹配com.spring.service包(不包括子包)下的所有类
within(com.spring.service.*)
// within表达式能够使用通配符,如下表达式表示匹配com.spring.service包及子包下的所有类
within(com.spring.service..*)
arg表达式
语法
args(param-pattern)
示例
// 匹配所有只有一个String类型的方法
args(String)
// 匹配所有有两个参数并且第一个参数为String的方法
args(String,*)
// 匹配所有第一个参数是String类型参数的方法
args(String,..)
@args表达式
语法
@args(annotation-type)
示例
@args(com.spring.annotation.FruitAspect)
跟@annotation表达式
以及@within表达式
类似,@annotation表达式
表示匹配使用了指定注解的方法,@within表达式
表达式表示匹配了使用了指定注解的类,而@args表达式
则代表使用了被指定注解标注的类作为方法参数
this表达式
// 代表匹配所有代理类是AccountService的类
this(com.xyz.service.AccountService)
target表达式
// 代表匹配所有目标类是AccountService的类
target(com.xyz.service.AccountService)
this跟target很鸡肋,基本用不到
官网中一共给出了9中切点表达式的定义方式,如下:
- execution:用于匹配方法执行连接点。这是使用Spring AOP时要使用的主要切入点指示器。
- within:限制匹配到特定类内的连接点(使用Spring AOP时在匹配类内声明的方法的执行)。
- this:限制匹配到连接点(使用SpringAOP时方法的执行),其中bean引用(SpringAOP代理)是给定类型的实例。
- target:限制匹配到连接点(使用SpringAOP时方法的执行),其中目标对象(被代理的应用程序对象)是给定类型的实例。
- args:限制匹配到连接点(使用SpringAOP时方法的执行),其中参数是给定类型的实例。
- @target:限制匹配到连接点(使用SpringAOP时方法的执行),其中执行对象的类具有给定类型的注释。
- @args:限制匹配到连接点(使用SpringAOP时方法的执行),其中传递的实际参数的运行时类型具有给定类型的注释。
- @within:限制匹配到具有给定注释的类型内的连接点(使用Spring AOP时,在具有给定注释的类型中声明的方法的执行)。
- @annotation:限制匹配到连接点的主题(在SpringAOP中运行的方法)具有给定注释的连接点。
组合切入点表达式
你可以使用&&,| |和!组合切入点表达式。您还可以按名称引用切入点表达式。以下示例显示了三个切入点表达式:
@Pointcut("execution(public * *(..))")
private void anyPublicOperation() {}
@Pointcut("within(com.xyz.myapp.trading..*)")
private void inTrading() {}
@Pointcut("anyPublicOperation() && inTrading()")
private void tradingOperation() {}
示例
@Aspect
@Component
public class DmzAnnotationAspect {
@Pointcut("execution(public * *(..))")
private void executionPointcut() {}
@Pointcut("@annotation(com.spring.study.springfx.aop.annotation.DmzAnnotation)")
private void annotationPointcut() { }
// 可以组合使用定义好的切点
// 表示同时匹配满足两者
@Pointcut("executionPointcut() && annotationPointcut()")
private void annotationPointcutAnd() {}
// 满足其中之一即可
@Pointcut("executionPointcut() || annotationPointcut()")
private void annotationPointcutOr() {}
// 不匹配即可
@Pointcut("!executionPointcut()")
private void annotationPointcutNot() {}
}
4、申明通知
通知的类型
Before
在目标方法之前执行,如果发生异常,会阻止业务代码的执行
AfterReturning
跟Before对应,在目标方法完全执行后(return后)再执行
AfterThrowing
方法抛出异常这个通知仍然会执行(这里的方法既可以是目标方法,也可以是我们定义的通知)
After(Finally)
切记,跟Before对应的是AfterReturning,一个在目标方法还没执行前执行,一个在目标方法完全执行后(return后)再执行,这个After类型的通知类型我们在编写代码时的Finally,即使方法抛出异常这个通知仍然会执行(这里的方法既可以是目标方法,也可以是我们定义的通知)。
一般我们使用After类型的通知都是为了完成资源的释放或者其它类似的目的
Around
最强大的通知类型,可以包裹目标方法,其可以传入一个ProceedingJoinPoint用于调用业务模块的代码,无论是调用前逻辑还是调用后逻辑,都可以在该方法中编写,甚至其可以根据一定的条件而阻断业务模块的调用,可以更改目标方法的返回值
实际应用
@Aspect
@Component
public class DmzAspect {
// 申明的切点
@Pointcut("execution(public * *(..))")
private void executionPointcut() {}
@Pointcut("@annotation(com.spring.study.springfx.aop.annotation.DmzAnnotation)")
private void annotationPointcut() {}
// 前置通知,在目标方法前调用
@Before("executionPointcut()")
public void executionBefore() {
System.out.println("execution aspect Before invoke!");
}
// 后置通知,在目标方法返回后调用
@AfterReturning("executionPointcut()")
public void executionAfterReturning() {
System.out.println("execution aspect AfterReturning invoke!");
}
// 最终通知,正常的执行时机在AfterReturning之前
@After("executionPointcut()")
public void executionAfter() {
System.out.println("execution aspect After invoke!");
}
// 异常通知,发生异常时调用
@AfterThrowing("executionPointcut()")
public void executionAfterThrowing() {
System.out.println("execution aspect AfterThrowing invoke!");
}
// 环绕通知,方法调用前后都能进行处理
@Around("executionPointcut()")
public void executionAround(ProceedingJoinPoint pjp) throws Throwable{
System.out.println("execution aspect Around(before) invoke!");
System.out.println(pjp.proceed());
System.out.println("execution aspect Around(after) invoke!");
}
}
通知中的参数
在上面应用的例子中,只有在环绕通知的方法上我添加了一个
ProceedingJoinPoint
类型的参数。这个ProceedingJoinPoint
意味着当前执行中的方法,它继承了JoinPoint
接口。
JoinPoint
JoinPoint可以在任意的通知方法上作为第一个参数申明,代表的时候通知所应用的切点(也就是目标类中的方法),它提供了以下几个方法:
getArgs()
: 返回当前的切点的参数getThis()
: 返回代理对象getTarget()
: 返回目标对象getSignature()
: 返回这个目标类中方法的描述信息,比如修饰符,名称等
ProceedingJoinPoint
ProceedingJoinPoint在JoinPoint的基础上多提供了两个方法
proceed()
:直接执行当前的方法,基于此,我们可以在方法的执行前后直接加入对应的业务逻辑proceed(Object[] args)
:可以改变当前执行方法的参数,然后用改变后的参数执行这个方法
通知的排序
当我们对于一个切点定义了多个通知时,例如,在一个切点上同时定义了两个before类型的通知。这个时候,为了让这两个通知按照我们期待的顺序执行,我们需要在切面上添加org.springframework.core.annotation.Order
注解或者让切面实现org.springframework.core.Ordered
接口。如下:
@Aspect
@Component
@Order(-1)
public class DmzFirstAspect {
// ...
}
@Aspect
@Component
@Order(0)
public class DmzSecondAspect {
// ...
}
3.AOP的应用
@RestControllerAdvice
简单介绍
@RestControllerAdvice是在Spring框架3.2新增的的注解
可以用来定义@ExceptionHandler,@InitBinder,@ModelAttribute,并应用到@RequestMapping中
使用原因
在统一异常处理时,如果每一个controller的每一个RequestMapping都进行异常的捕捉和处理,会造成代码的冗余,不方便维护,所以基于面向切面编程的思想,给这些处理异常的代码抽取出来.
具体实现
- 创建一个类,在类上加上@RestControllerAdvice或者@ControllerAdvice(这两者间有区别)实现一个切面,加上@Component作为一个组件加入ioc容器.
- 创建捕捉和处理统一异常的方法,方法上加上@ExceptionHandler,顾名思义,异常处理器,此注解需要value值,值是你想捕捉的异常的类型的字节码,并且方法也有参数,参数是此异常类的对象
- 可以处理多种异常,因为异常的种类,可以选择性的捕捉和处理异常,不同的异常给出不同的处理了,在处理异常时有一点需要注意,如果Exception异常和它的子类,更加范围小的异常同时存在时,会走范围小更精确的异常,并且处理掉后不会由Exception异常处理.
package com.chif.goingplus.aop;
import org.springframework.jdbc.BadSqlGrammarException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
import javax.servlet.http.HttpServletRequest;
//AOP实现
//该注解定义全局异常处理类
//@ControllerAdvice
//@ResponseBody
// 使用@RestControllerAdvice可以替代上面两个注解
@RestControllerAdvice
//@ControllerAdvice(basePackages ="com.example.demo.controller") 可指定包
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler{
//自定义异常
@ExceptionHandler(value=GlobalException.class) //该注解声明异常处理方法
public Result<String> exceptionHandler(HttpServletRequest request, Exception e){
e.printStackTrace();
// 在这里针对异常做自己的处理
return Result.error(ErrorCode.INTERNAL_SERVICE_ERROR);
}
//处理SQL异常
@ExceptionHandler(value = BadSqlGrammarException.class)
public Result<String> sqlException(BadSqlGrammarException badSqlGrammarException){
return Result.error(ErrorCode.BAD_SQL_GRAMMAR_ERROR);
}
//处理Controller异常
@ExceptionHandler(value = Exception.class)
public Result<String> handleException(Exception exception){
return Result.error(ErrorCode.INTERNAL_SERVICE_ERROR);
}
}
AOP的实际应用非常多,我这里就给出两个例子
- 全局异常处理器
- 利用AOP打印接口日志
全局异常处理器
需要用到两个注解:
@RestControllerAdvice
及@ExceptionHandler
,总共分为以下几步:
- 定义自己项目中用到的错误码及对应异常信息
- 封装自己的异常
- 申明全局异常处理器并针对业务中的异常做统一处理
定义错误码及对应异常信息
@AllArgsConstructor
@Getter
public enum ErrorCode {
INTERNAL_SERVICE_ERROR(500100, "服务端异常"),
PASSWORD_CAN_NOT_BE_NULL(500211, "登录密码不能为空"),
PASSWORD_ERROR(500215, "密码错误");
private int code;
private String msg;
}
// 统一返回的参数
@Data
public class Result<T> {
private int code;
private String msg;
private T data;
public static <T> Result<T> success(T data){
return new Result<T>(data);
}
public static <T> Result<T> error(ErrorCode cm){
return new Result<T>(cm.getMsg);
}
}
封装对应异常
public class GlobalException extends RuntimeException {
private static final long serialVersionUID = 1L;
private int errorCode;
public CreativeArtsShowException(int errorCode) {
this.errorCode = errorCode;
}
public CreativeArtsShowException(ErrorCode errorCode) {
super(errorCode.getMsg());
this.errorCode = errorCode.getCode();
}
}
申明异常处理器
//该注解定义全局异常处理类
//@ControllerAdvice
//@ResponseBody
// 使用@RestControllerAdvice可以替代上面两个注解
@RestControllerAdvice
//@ControllerAdvice(basePackages ="com.example.demo.controller") 可指定包
public class GlobalExceptionHandler {
@ExceptionHandler(value=GlobalException.class) //该注解声明异常处理方法
public Result<String> exceptionHandler(HttpServletRequest request, Exception e){
e.printStackTrace();
// 在这里针对异常做自己的处理
}
}
其实SpringMVC中提供了一个异常处理的基类(org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler
)。我们只需要将自定义的异常处理类继承这个ResponseEntityExceptionHandler
然后复写对应的方法即可完成全局异常处理。这个类中的方法很简单,所以这里就不放代码了。
这个类中已经定义了很多的异常处理方法,如下:
@ExceptionHandler({
HttpRequestMethodNotSupportedException.class,
HttpMediaTypeNotSupportedException.class,
HttpMediaTypeNotAcceptableException.class,
MissingPathVariableException.class,
MissingServletRequestParameterException.class,
ServletRequestBindingException.class,
ConversionNotSupportedException.class,
TypeMismatchException.class,
HttpMessageNotReadableException.class,
HttpMessageNotWritableException.class,
MethodArgumentNotValidException.class,
MissingServletRequestPartException.class,
BindException.class,
NoHandlerFoundException.class,
AsyncRequestTimeoutException.class
})
所以我们只需要复写对应异常处理的方法即可完成自己在当前业务场景下异常的处理。但是需要注意的是,它只会对上面这些框架抛出的异常进行处理,对于我们自定义的异常还是会直接抛出,所以我们自定义的异常处理还是需要在其中进行定义。
接口日志
我们在开发中经常会打印日志,特别是接口的入参日志,如下:
@RestController
@RequestMapping("/test/simple")
@Validated
@Slf4j
public class ValidationController {
@GetMapping("/valid")
public String testValid(
@Max(10) int age, @Valid @NotBlank String name) {
log.info("接口入参:" + age + " " + name);
return "OK";
}
}
如果每一个接口都需要添加这样一句代码的话就显得太LOW了,基于此我们可以使用AOP来简化代码,按照以下几步即可:
- 自定义一个注解
- 申明切面
定义一个注解
@Target({ElementType.TYPE,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface Log {
}
申明切面
@Aspect
@Component
@Slf4j
public class LogAspect {
@Pointcut("@annotation(com.spring.study.springfx.aop.annotation.Log)")
private void pointcut() {
}
@Before("pointcut()")
public void before(JoinPoint joinPoint) {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
Parameter[] parameters = method.getParameters();
Object[] args = joinPoint.getArgs();
String methodName = method.getName();
Class<?> declaringClass = method.getDeclaringClass();
String simpleName = declaringClass.getSimpleName();
StringBuilder sb = new StringBuilder();
sb.append(simpleName).append(".").append(methodName).append(" [");
for (int i = 0; i < parameters.length; i++) {
String name = parameters[i].getName();
sb.append(name);
sb.append(":");
sb.append(args[i]);
sb.append(";");
}
sb.setLength(sb.length() - 1);
sb.append("]");
log.info(sb.toString());
}
}
基于上面的例子测试:
@RestController
@RequestMapping("/test/simple")
@Validated
@Slf4j
public class ValidationController {
@Log
@GetMapping("/valid")
public String testValid(
@Max(10) int age, @Valid @NotBlank String name) {
log.info("接口入参:" + age + " " + name);
return "OK";
}
}
// 控制台输出日志:
// ValidationController.testValid [age:0;name:11]