怎么自定义一个注解?
先来看一个自定义注解的示例,如下
import java.lang.annotation.*;
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SysLog {
/**
* 内容描述
*/
String value() default "";
}
可以看到,@SysLog 主要由 @Target、@Retention、@Documented 三个元注解组成。定义该注解只能用在方法上,可以被保留到运行期。
Java 的注解分为元注解和自定义注解,元注解是专门用来描述注解的注解类。在 JDK 中只有 4 个元注解,分别是
@Target //描述注解的使用范围。可以用在包、类、成员变量、局部变量、成员方法、构造方法、注解类等,支持的类型都定义在 ElementType 枚举类中
@Retention //描述注解保留的时间范围。
RetentionPolicy 枚举类中有三种策略,
SOURCE // 源文件保留
CLASS // 编译期保留(默认)
RUNTIME // 运行期保留
@Documented //描述在使用 javadoc 工具为类生成帮助文档时是否要保留其注解信息
@Inherited //如果某个类使用了被 @Inherited 修饰的注解,则其子类将自动具有该注解
所以除了这四个元注解之外所有的注解都是自定义注解,像我们在 Spring、SpringBoot 开发中使用的 @Controller、@Service、@Mapper、@Resources…等这些注解其实都是自定义注解,只不过 Spring 框架已经帮我们封装好了,不需要我们再去手动封装了。
execution 表达式
Spring 官网中 Spring AOP 切入点表达式支持以下的 AspectJ 切入方式:
execution:用于匹配方法执行的连接点。这是在使用Spring AOP时要使用的主要切入点指示符。
within:将匹配限制为某些类型内的连接点。
this:在bean引用(Spring AOP代理)是给定类型的实例的情况下,将匹配限制为连接点。
target:在目标对象是给定类型的实例的情况下,将匹配限制为连接点。
args:将匹配限制为连接点,其中参数是给定类型的实例。
@target:在执行对象的类具有给定类型的注释的情况下,将匹配限制为连接点。
@args:限制匹配的连接点,其中传递的实际参数的运行时类型具有给定类型的注释。
@within:将匹配限制为具有给定注释的类型内的连接点。
@annotation:将匹配限制在连接点的对象具有给定注解的连接点处。
bean(BeanName):匹配限制为指定命名的 Bean
execution(<修饰符模式>?<返回类型模式><方法名模式>(<参数模式>)<异常模式>?)
其中除了返回类型模式、方法名模式和参数模式外,其它的修饰符模式、异常模式是可选的。
execution(* com.execution.test..*.*(..))
对应
execution(返回值 全包名.类名.方法名 (参数集合))
1、 execution() 是表达式的主体
2、 第一个 * 号表示返回值可以为任意类型
3、 com.execution.test 指的是 AOP 切面所切的包名
4、 包名后面的 .. 符号表示当前包及其子包,如果只有一个 . 号表示当前包下的所有类,不包含子包
5、 第二个 * 号表示类名,* 即代表所有类,也可以用 User* 这样去指定以 User 开头的类名,用 *User 指定以 User 结尾的类名
6、 最后的".*(..)" 表示任何方法名,括号表示参数,两个点表示任意参数类型
其实平常就 execution 和 @annotation 用的多点,其他的也很少用,再看看一些不同的示例:
// 指定注解类
@annotation(com.execution.test.SysLog)
// 匹配所有目标类所有类型的方法
execution(* *(..))
// 匹配所有目标类的 private 方法
execution(private * *(..))
// 匹配SysLogController所有的方法
execution(* * com.execution.test.SysLogController.*(..))
// 匹配指定的 com.execution.test 包中的所有 public 方法,并且第一个参数是 String 类型,第二个参数是 int类型,返回值是 int 类型,后面可以有任意个且类型不限的参数的方法
execution(public int com.execution.test.*(String, int, ..))
// 匹配指定类中的所有方法
within(TestService)
// 匹配以指定名字结尾的类中的所有方法
bean(*Service)
// 匹配一个接口的所有实现类中的实现方法
within(UserDao+)
// 切点表达式组合 &&、||、!
// 匹配在 com.execution.test 包及其子包下以 Service 结尾的类
within(com.execution.test..*) && within(*Service)
增强的使用示例
示例代码环境的版本:JDK1.8,SpringBoot 2.0.0,Spring 5.0.4。
切面类 SysLogAspect ,记得需要加上 @Component、@Aspect 这两个注解。
@Component// 交给 Spring 管理
@Aspect// 标识为切面
public class SysLogAspect {
private Logger logger = LoggerFactory.getLogger(getClass());
/**
* 以所有添加了 SysLog 注解的方法为切点
*/
@Pointcut("@annotation(com.jsd.zhaopin.web.component.SysLog)")
public void logAspect() {
}
/**
* 前置增强
*/
@Before("logAspect()")
public void doBefore() {
logger.info("==========doBefore==========");
}
/**
* 正常返回增强
*/
@AfterReturning("logAspect()")
public void doAfterReturning() {
logger.info("==========doAfterReturning==========");
}
/**
* 异常返回增强
*/
@AfterThrowing("logAspect()")
public void doAfterThrowing() {
logger.info("==========doAfterThrowing==========");
}
/**
* 后置通知
*/
@After("logAspect()")
public void doAfter() {
logger.info("==========doAfter==========");
}
/**
* 环绕增强
*/
@Around("logAspect()")
public Object doAround(ProceedingJoinPoint proceedingJoinPoint) {
try {
logger.info("==========doAround before==========");
// 调用proceed()方法目标方法才会执行,这是环绕增强控制目标方法执行的关键
Object o = joinPoint.proceed();
logger.info("==========doAround after==========");
return o;
} catch (Throwable e) {
e.printStackTrace();
}
return null;
}
}
目标方法SysLogController.test()
@RestController
@RequestMapping("/api/aop")
public class SysLogController {
private Logger logger = LoggerFactory.getLogger(getClass());
@SysLog("AOP测试")
@GetMapping("test")
public void test() {
logger.info("==========test aop==========");
}
}
测试结果
==========doAround before==========
==========doBefore==========
==========test aop==========
==========doAround after==========
==========doAfter==========
==========doAfterReturning==========(异常时==========doAfterThrowing==========)
可以看出,不同增强的执行顺序如下:
@Around 环绕增强是前面四个增强的结合体,当需要处理的数据需要强一致性的时候那就必须得使用 @Around 去实现,如果只是打印请求 URL、请求用户 IP、接口入参这些那就直接 @Before 就可以了。另外,当使用 @Around 来定义环绕增强时,形参必须要一个及以上而且第一个形参必须是 ProceedingJoinPoint 类型。
利用AOP实现日志记录
@Around("@annotation(sysLog)")
public Object doAround(ProceedingJoinPoint joinPoint, SysLog sysLog) throws Throwable {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
// 获取请求用户IP
String ip = request.getRemoteAddr();
// 获取请求接口URL
String url = request.getRequestURL().toString();
// 获取签名
Signature signature = joinPoint.getSignature();
MethodSignature methodSignature = (MethodSignature) signature;
// 获取接口入参参数名称
String argumentNames = StringUtils.join(methodSignature.getParameterNames(), ",");
// 获取接口入参参数值
String params = StringUtils.join(joinPoint.getArgs(), ",");
// 获取包名+类名
String packageName = signature.getDeclaringTypeName();
// 获取方法名
String methodName = signature.getName();
// 返回被增强处理的目标对象
Object target = joinPoint.getTarget();
// 返回AOP框架为目标对象生成的代理对象
Object proxy = joinPoint.getThis();
try {
logger.info("==========doAround before==========");
long start = System.currentTimeMillis();
Object proceed = joinPoint.proceed();
long end = System.currentTimeMillis();
// 接口执行时间,单位毫秒
long time = end - start;
logger.info("==========doAround after==========");
return proceed;
} catch (Throwable e) {
// 异常信息
String message = e.getMessage();
throw e;
}
// 以上注释的变量可以选择性的保存到数据库中,这样就利用AOP实现了日志记录的功能
SystemLog systemLog = new SystemLog();
systemLog.setIp(ip);
systemLog.setUrl(url);
...
systemLogMapper.insert(systemLog);
}
aop应用场景也很丰富,比如日志记录,认证鉴权,全局异常处理等,只要是在横向扩展一些局部或全局功能的时候就可以考虑能否使用aop。不过实际业务可能复杂多变,可按具体情况去实现。