1
自定义注解
1.元注解:java.lang.annotation中提供了元注解,可以使用这些注解来定义自己的注解。主要使用的是Target和Retention注解。
2.常用元注解
@Target:描述了注解修饰的对象范围,取值在java.lang.annotation.ElementType定义,常用的包括:
- METHOD:应用于描述方法
- TYPE:应用于描述类、接口或enum类型
- PACKAGE:应用于描述包
- FIELD:应用于属性(包括枚举中的常量)
@Retention: 表示注解保留时间长短。取值在java.lang.annotation.RetentionPolicy中,取值为:
- SOURCE:编译时被丢弃,不包含在类文件中
- CLASS:JVM加载时被丢弃,包含在类文件中,默认值
- RUNTIME:由JVM 加载,包含在类文件中,在运行时可以被获取到
注:只有定义为RetentionPolicy.RUNTIME时,我们才能通过注解反射获取到注解
@Document:表明该注解标记的元素可以被Javadoc 或类似的工具文档化
例:
@Retention(RetentionPolicy.RUNTIME)@Target({ElementType.METHOD, ElementType.TYPE})@Documentedpublic @interface ApiOperateLog {}
2
Aop理解
AOP为何那么重要呢?在我们的程序中经常会存在一些系统性的需求,比如权限校验、日志记录、统计等,这些代码会散落穿插在各个业务逻辑中,非常冗余且不利于维护。AOP将这些非业务代码完全提取出来,与业务代码分离并寻找节点切入业务代码中。
简单地去理解,其实AOP要做三类事:
- 在哪里切入,也就是这些非业务操作在哪些业务代码中执行
- 在什么时候切入,是业务代码执行前还是执行后
- 切入后做什么事,比如做权限校验、日志记录、统计等
简单画了个体系图梳理下
- Pointcut:切点,决定处理如日志记录等在何处切入业务代码中(即织入切面)。切点分为execution方式和annotation方式。前者可以用路径表达式指定哪些类织入切面,后者可以指定被哪些注解修饰的代码织入切面。
- Advice:处理,包括处理时机和处理内容。处理内容就是要做什么事,比如校验权限和记录日志。处理时机就是在什么时机执行处理内容,分为前置处理(即业务代码执行前)、后置处理(业务代码执行后)等。
- Aspect:切面,即Pointcut和Advice。
- Joint point:连接点,是程序执行的一个点。例如,一个方法的执行或者一个异常的处理。在 Spring AOP 中,一个连接点总是代表一个方法执行。
- Weaving:织入,就是通过动态代理,在目标对象方法中执行处理内容的过程。
3
日志记录的实现
1.自定义注解
@Retention(RetentionPolicy.RUNTIME)@Target({ElementType.METHOD, ElementType.TYPE})@Documentedpublic @interface ApiOperateLog { String logName() default "操作日志"; String logType(); String logModule();}
2.注解用到的类型
public interface LogConst { /** * 日志类型 * 增 add * 删 del * 改 update * 查 select */ interface LogType{ String LogType_ADD = "add"; String LogType_DEL = "del"; String LogType_UPDATE = "update"; String LogType_SELECT = "select"; } /** * 模块名称常量_评论管理 */ interface LogModule { /** * 用户信息管理 */ String LOG_USER_INFO = "userInfo"; }}
3.核心切入类
@Slf4j@Aspect@Componentpublic class LogAspect { /** * 此处的切点是注解的方式(注解类路径),也可以用包名的方式达到相同的效果 * '@Pointcut("execution(* com.diku.provider.service.impl.*.*(..))")' */ @Pointcut("@annotation(com.diku.provider.annotion.ApiOperateLog)") public void execute(){ } /** * 后置返回通知 * 处理完请求,返回内容 */ @AfterReturning(returning = "result", value = "execute()") public void afterReturning(JoinPoint point, Object result) throws Throwable { Object[] args = point.getArgs(); //得到注解对象 ApiOperateLog annotation = ((MethodSignature)point.getSignature()).getMethod().getAnnotation(ApiOperateLog.class); String name = annotation.logName(); String type = annotation.logType(); String logModule = annotation.logModule(); SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); String createTime = simpleDateFormat.format(new Date()); //获取方法返回值 log.info(" @AfterReturning:{}",result); saveLog(name, type, logModule, args, createTime); } private void saveLog(String name, String type, String logModule, Object[] args, String createTime) { //保存相关日志操作,调用kafka(其他消息中间件),通知Es保存相关日志记录 log.info("{}->类型:{}->模块:{}->参数:{}->创建时间:{}", name,type,logModule,Arrays.toString(args), createTime); } /** * 前置通知 */ @Before("execute()") public void doBeforeAdvice(JoinPoint joinPoint){ log.info("测试@Before....."); } /** * 后置异常通知 */ @AfterThrowing("execute()") public void afterThrowing(JoinPoint joinPoint){ log.info("测试@AfterThrowing....."); } /** * 后置通知 */ @After("execute()") public void after(JoinPoint joinPoint){ log.info("测试@After....."); } /** * 环绕通知 */ @Around("execute()") public Object doAround(ProceedingJoinPoint point) throws Throwable { log.info("进入环绕通知"); long start=System.currentTimeMillis(); Object proceed = point.proceed(point.getArgs()); log.info("环绕通知.....统计方法执行的时间为:{}ms",String.valueOf(System.currentTimeMillis()-start)); log.info("退出环绕通知"); return proceed; }}
注:@Around功能虽然强大,但通常需要在线程安全的环境下使用。因此,如果使用普通的Before、AfterReturning就能解决的问题,就没有必要使用Around了。如果需要目标方法执行之前和之后共享某种状态数据,则应该考虑使用Around。尤其是需要使用增强处理阻止目标的执行,或需要改变目标方法的返回值时,则只能使用Around增强处理了。例如案例中我们统计方法执行的时间。
4.接口使用注解测试
@RestController@RequestMapping("/user")public class UserController { @Autowired private UserMapper userMapper; @ApiOperateLog(logName = "查询日志",logType = LogConst.LogType.LogType_SELECT,logModule = LogConst.LogModule.LOG_USER_INFO) @GetMapping("/{id}") public User selectById(@PathVariable("id") int id) { try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } return userMapper.selectById(id); }}
浏览器访问:
控制台打印日志:
通过控制台打印的日志我们还可以看出这几种通知的执行顺序:
进入环绕通知--->前置通知--->执行ProceedingJoinPoint.proceed--->退出环绕通知--->后置通知--->后置返回通知
喜欢就加个关注吧,