切面+自定义注解的一些玩法

文章内容

这篇文章主要记录一下切面+自定义注解在实际中的一些玩法。切面+自定义注解的玩法可能有很多,这篇主要说一下实现以下两个功能:

1. @HttpLog自动记录Http请求日志

2. @TimeStamp自动注入时间戳

源码 is here:切面+自定义注解的一些玩法

如何运行这个例子

  1. 创建数据库:
CREATE TABLE `t_user` (
  `id` varchar(32) NOT NULL COMMENT 'id',
  `username` varchar(16) DEFAULT NULL COMMENT '用户名',
  `password` varchar(16) DEFAULT NULL COMMENT '密码',
  `del_flag` int(1) DEFAULT NULL COMMENT '删除标识:0:已删除 1:未删除',
  `created_at` datetime DEFAULT NULL COMMENT '创建时间',
  `updated_at` datetime DEFAULT NULL COMMENT '更新时间',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4

2 . 修改配置文件application.properties,配置合适的端口和正确的数据库信息。

3 . 运行 DemoApplication.java,启动项目,成功启动后浏览器打开http://localhost:8008/swagger-ui.html,便可以看到swagger API展示界面,进行API调试

前置知识

  1. 自定义注解:如何自定义注解
  2. AOP:什么是AOP

适合人群

编程新手或者刚接触这块的老手

可能会了解

  1. 如何自定义注解
  2. 如何使用AOP
  3. 自定义注解+AOP组合使用,自定义你想要的注解
  4. 内含了 mybatis plus 使用的 demo,简化mabatis的使用
  5. 内置了一个全局唯一ID生成器,了解ID生成器
  6. Swagger 管理调试API
  7. 其他

实现解析

  • @HttpLog自动记录Http日志

在很多时候我们要把一些接口的Http请求信息记录到日志里面。通常原始的做法是利用日志框架如log4j,slf4j等,在方法里面打日志log.info("xxxx")。但是这样的工作无疑是单调而又重复的,我们可以采用自定义注解+切面的来简化这一工作。通常的日志记录都在Controller里面进行的比较多,我们可以实现这样的效果:

我们自定义@HttpLog注解,作用域在类上,凡是打上了这个注解的Controller类里面的所有方法都会自动记录Http日志。实现方式也很简单,主要写好切面表达式:

 // 切面表达式,描述所有所有需要记录log的类,所有有@HttpLog 并且有 @Controller 或 @RestController 类都会被代理
    @Pointcut("@within(com.example.vzard.annotation.HttpLog) && (@within(org.springframework.web.bind.annotation.RestController) || @within(org.springframework.stereotype.Controller))")
    public void httpLog() {
    }

    @Before("httpLog()")
    public void preHandler(JoinPoint joinPoint) {
        startTime.set(System.currentTimeMillis());
        ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest httpServletRequest = servletRequestAttributes.getRequest();
        log.info("Current Url: {}", httpServletRequest.getRequestURI());
        log.info("Current Http Method: {}", httpServletRequest.getMethod());
        log.info("Current IP: {}", httpServletRequest.getRemoteAddr());
        Enumeration<String> headerNames = httpServletRequest.getHeaderNames();
        log.info("=======http headers=======");
        while (headerNames.hasMoreElements()) {
            String nextName = headerNames.nextElement();
            log.info(nextName.toUpperCase() + ": {}", httpServletRequest.getHeader(nextName));
        }
        log.info("======= header end =======");
        log.info("Current Class Method: {}", joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint.getSignature().getName());
        log.info("Parms: {}", null != httpServletRequest.getQueryString() ? JSON.toJSONString(httpServletRequest.getQueryString().split("&")) : "EMPTY");

    }

    @AfterReturning(returning = "response", pointcut = "httpLog()")
    public void afterReturn(Object response) {
        log.info("Response: {}", JSON.toJSONString(response));
        log.info("Spend Time: [ {}", System.currentTimeMillis() - startTime.get() + " ms ]");

    }
  • @TimeStamp自动注入时间戳

我们的很多数据需要记录时间戳,最常见的就是记录created_atupdated_at,通常我们可以通常实体类中的setCreatedAt()方法来写入当前时间,然后通过ORM来插入到数据库里,但是这样的方法比较重复枯燥,给每个需要加上时间戳的类都要写入时间戳很麻烦而且不小心会漏掉。另一个思路是在数据库里面设置默认值,插入的时候由数据库自动生成当前时间戳,但是理想很丰满,现实很骨感,在MySQL如果时间戳类型是datetime里即使你设置了默认值为当前时间也不会在时间戳为空时插入数据时自动生成,而是会在已有时间戳记录的情况下更新时间戳为当前时间,这并不是我们所需要的,比如我们不希望created_at每次更改记录时都被刷新,另外的方法是将时间戳类型改为timestamp,这样第一个类型为timestamp的字段会在值为空时自动生成,但是多个的话,后面的均不会自动生成。再有一种思路是,直接在sql里面用now()函数生成,比如created_at = now()。但是这样必须要写sql,如果使用的不是主打sql流的orm不会太方便,比如hibernate之类的,并且也会加大sql语句的复杂度,同时sql的可移植性也会降低,比如sqlServer中就不支持now()函数。为了简化这个问题,我们可以自定义@TimeStamp注解,打上该注解的方法的入参里面的所有对象或者指定对象里面要是有setCreatedAtsetUpdatedAt这样的方法,便会自动注入时间戳,而无需手动注入,同时还可以指定只注入created_atupdated_at。实现主要代码如下:

//所有打上@TimeStamp注解的方法作为切点
    @Pointcut("@annotation(com.example.vzard.annotation.TimeStamp)")
    public void pointCut() {

    }

    @Before("pointCut() && @annotation(timeStamp)")
    public void before(JoinPoint joinPoint, TimeStamp timeStamp) {
        Long currentTime = System.currentTimeMillis();
        Class type = timeStamp.type();

        List argList = Arrays.stream(joinPoint.getArgs())
                .filter(t -> (t != null))
                .filter(t -> (t.getClass().getName().equals(type.getName())))
                .collect(Collectors.toList());

        for (Object arg : argList) {
            Method[] methods = arg.getClass().getMethods();
            for (Method m : methods) {

                if (timeStamp.rank().equals(TimeStampRank.FULL)) {
                    setCurrentTime(m, arg, "setUpdatedAt", "setCreatedAt");
                }

                if (timeStamp.rank().equals(TimeStampRank.UPDATE)) {
                    setCurrentTime(m, arg, "setUpdatedAt");
                }

                if (timeStamp.rank().equals(TimeStampRank.CREATE)) {
                    setCurrentTime(m, arg, "setCreatedAt");
                }

            }

        }

    }

    private void setCurrentTime(Method method, Object o, String... methodNames) {
        for (String name : methodNames) {
            if (method.getName().equals(name)) {
                try {
                    method.setAccessible(true);
                    method.invoke(o, new Date(System.currentTimeMillis()));

                } catch (IllegalAccessException | InvocationTargetException e) {
                    e.printStackTrace();
                }
            }
        }

    }

自定义注解+AOP可以做很多事,比如根据注解动态切换数据源,根据注解做接口鉴权等等,读者可以自己尝试去实现。

详细源码戳这:切面+自定义注解的一些玩法

点赞