• 自定义注解!绝对是程序员装逼的利器!!


    基本知识

    在Java中,注解分为两种,元注解自定义注解
    很多人误以为自定义注解就是开发者自己定义的,而其它框架提供的不算,但是其实上面我们提到的那几个注解其实都是自定义注解。
    关于"元"这个描述,在编程世界里面有都很多,比如"元注解"、"元数据"、"元类"、"元表"等等,这里的"元"其实都是从meta翻译过来的。
    一般我们把元注解理解为描述注解的注解,元数据理解为描述数据的数据,元类理解为描述类的类…
    所以,在Java中,除了有限的几个固定的"描述注解的注解"以外,所有的注解都是自定义注解。
    在JDK中提供了4个标准的用来对注解类型进行注解的注解类(元注解),他们分别是:

    @Target
    
    @Retention
    
    @Documented
    
    @Inherited

    除了以上这四个,所有的其他注解全部都是自定义注解。
    这里不准备深入介绍以上四个元注解的作用,大家可以自行学习。
    本文即将提到的几个例子,都是作者在日常工作中真实使用到的场景,这例子有一个共同点,那就是都用到了Spring的AOP技术。
    什么是AOP以及他的用法相信很多人都知道,这里也就不展开介绍了。

    使用自定义注解做日志记录

    不知道大家有没有遇到过类似的诉求,就是希望在一个方法的入口处或者出口处做统一的日志处理,比如记录一下入参、出参、记录下方法执行的时间等。
    如果在每一个方法中自己写这样的代码的话,一方面会有很多代码重复,另外也容易被遗漏。
    这种场景,就可以使用自定义注解+切面实现这个功能。
    假设我们想要在一些web请求的方法上,记录下本次操作具体做了什么事情,比如新增了一条记录或者删除了一条记录等。
    首先我们自定义一个注解:

    /**
    
     * Operate Log 的自定义注解
    
     */
    
    @Target(ElementType.METHOD)
    
    @Retention(RetentionPolicy.RUNTIME)
    
    public @interface OpLog {
    
        /**
    
         * 业务类型,如新增、删除、修改
    
         * @return
    
         */
    
        public OpType opType();
    
        /**
    
         * 业务对象名称,如订单、库存、价格
    
         * @return
    
         */
    
        public String opItem();
    
        /**
    
         * 业务对象编号表达式,描述了如何获取订单号的表达式
    
         * @return
    
         */
    
        public String opItemIdExpression();
    
    }

    因为我们不仅要在日志中记录本次操作了什么,还需要知道被操作的对象的具体的唯一性标识,如订单号信息。
    但是每一个接口方法的参数类型肯定是不一样的,很难有一个统一的标准,那么我们就可以借助Spel表达式,即在表达式中指明如何获取对应的对象的唯一性标识。
    有了上面的注解,接下来就可以写切面了。主要代码如下:

    /**
    
     * OpLog的切面处理类,用于通过注解获取日志信息,进行日志记录
    
     * @author Hollis
    
     */
    
    @Aspect
    
    @Component
    
    public class OpLogAspect {
    
        private static final Logger LOGGER = LoggerFactory.getLogger(OpLogAspect.class);
    
        @Autowired
    
        HttpServletRequest request;
    
        @Around("@annotation(com.hollis.annotation.OpLog)")
    
        public Object log(ProceedingJoinPoint pjp) throws Exception {
    
            Method method = ((MethodSignature)pjp.getSignature()).getMethod();
    
            OpLog opLog = method.getAnnotation(OpLog.class);
    
            Object response = null;
    
            try {
    
                // 目标方法执行
    
                response = pjp.proceed();
    
            } catch (Throwable throwable) {
    
                throw new Exception(throwable);
    
            } 
    
            if (StringUtils.isNotEmpty(opLog.opItemIdExpression())) {
    
                SpelExpressionParser parser = new SpelExpressionParser();
    
                Expression expression = parser.parseExpression(opLog.opItemIdExpression());
    
                EvaluationContext context = new StandardEvaluationContext();
    
                // 获取参数值
    
                Object[] args = pjp.getArgs();
    
                // 获取运行时参数的名称
    
                LocalVariableTableParameterNameDiscoverer discoverer
    
                    = new LocalVariableTableParameterNameDiscoverer();
    
                String[] parameterNames = discoverer.getParameterNames(method);
    
                // 将参数绑定到context中
    
                if (parameterNames != null) {
    
                    for (int i = 0; i < parameterNames.length; i++) {
    
                        context.setVariable(parameterNames[i], args[i]);
    
                    }
    
                }
    
                // 将方法的resp当做变量放到context中,变量名称为该类名转化为小写字母开头的驼峰形式
    
                if (response != null) {
    
                    context.setVariable(
    
                        CaseFormat.UPPER_CAMEL.to(CaseFormat.LOWER_CAMEL, response.getClass().getSimpleName()),
    
                        response);
    
                }
    
                // 解析表达式,获取结果
    
                String itemId = String.valueOf(expression.getValue(context));
    
                // 执行日志记录
    
                handle(opLog.opType(), opLog.opItem(), itemId);
    
            }
    
            return response;
    
        }
    
    
        private void handle(OpType opType,  String opItem, String opItemId) {
    
          // 通过日志打印输出
    
          LOGGER.info("opType = " + opType.name() +",opItem = " +opItem + ",opItemId = " +opItemId);
    
        }
    
    }

    以上切面中,有几个点需要大家注意的:

    • 1、使用@Around注解来指定对标注了OpLog的方法设置切面。
    • 2、使用Spel的相关方法,通过指定的表示,从对应的参数中获取到目标对象的唯一性标识。
    • 3、再方法执行成功后,输出日志。


    有了以上的切面及注解后,我们只需要在对应的方法上增加注解标注即可,如:

    @RequestMapping(method = {RequestMethod.GET, RequestMethod.POST})
    
    @OpLog(opType = OpType.QUERY, opItem = "order", opItemIdExpression = "#id")
    
    public @ResponseBody
    
    HashMap view(@RequestParam(name = "id") String id)
    
        throws Exception {
    
    }

    上面这种是入参的参数列表中已经有了被操作的对象的唯一性标识,直接使用#id指定即可。
    如果被操作的对象的唯一性标识不在入参列表中,那么可能是入参的对象中的某一个属性,用法如下:

    @RequestMapping(method = {RequestMethod.GET, RequestMethod.POST})
    
    @OpLog(opType = OpType.QUERY, opItem = "order", opItemIdExpression = "#orderVo.id")
    
    public @ResponseBody
    
    HashMap update(OrderVO orderVo)
    
        throws Exception {
    
    }

    以上,即可从入参的OrderVO对象的id属性的值获取。
    如果我们要记录的唯一性标识,在入参中没有的话,应该怎么办呢?最典型的就是插入方法,插入成功之前,根本不知道主键ID是什么,这种怎么办呢?
    我们上面的切面中,做了一件事情,就是我们把方法的返回值也会使用表达式进行一次解析,如果可以解析得到具体的值,也是可以。如以下写法:

    @RequestMapping(method = {RequestMethod.GET, RequestMethod.POST})
    
    @OpLog(opType = OpType.QUERY, opItem = "order", opItemIdExpression = "#insertResult.id")
    
    public @ResponseBody
    
    InsertResult insert(OrderVO orderVo)
    
        throws Exception {
    
        return orderDao.insert(orderVo);
    
    }

    以上,就是一个简单的使用自定义注解+切面进行日志记录的场景。下面我们再来看一个如何使用注解做方法参数的校验。

    使用自定义注解做前置检查

    当我们对外部提供接口的时候,会对其中的部分参数有一定的要求,比如某些参数值不能为空等。大多数情况下我们都需要自己主动进行校验,判断对方传入的值是否合理。
    这里推荐一个使用HibernateValidator + 自定义注解 + AOP实现参数校验的方式。
    首先我们会有一个具体的入参类,定义如下:

    public class User {
    
        private String idempotentNo;
    
        @NotNull(
    
            message = "userName can't be null"
    
        )
    
        private String userName;
    
    }

    以上,对userName参数注明不能为null。
    然后再使用Hibernate Validator定义一个工具类,用于做参数校验。

    /**
    
     * 参数校验工具
    
     * @author Hollis
    
     */
    
    public class BeanValidator {
    
        private static Validator validator = Validation.byProvider(HibernateValidator.class).configure().failFast(true)
    
            .buildValidatorFactory().getValidator();
    
        /**
    
         * @param object object
    
         * @param groups groups
    
         */
    
        public static void validateObject(Object object, Class<?>... groups) throws ValidationException {
    
            Set<ConstraintViolation<Object>> constraintViolations = validator.validate(object, groups);
    
            if (constraintViolations.stream().findFirst().isPresent()) {
    
                throw new ValidationException(constraintViolations.stream().findFirst().get().getMessage());
    
            }
    
        }
    
    }

    以上代码,会对一个bean进行校验,一旦失败,就会抛出ValidationException。
    接下来定义一个注解:

    /**
    
     * facade接口注解, 用于统一对facade进行参数校验及异常捕获
    
     * <pre>
    
     *      注意,使用该注解需要注意,该方法的返回值必须是BaseResponse的子类
    
     * </pre>
    
     */
    
    @Target(ElementType.METHOD)
    
    @Retention(RetentionPolicy.RUNTIME)
    
    public @interface Facade {
    
    }

    这个注解里面没有任何参数,只用于标注那些方法要进行参数校验。
    接下来定义切面:

    /**
    
     * Facade的切面处理类,统一统计进行参数校验及异常捕获
    
     * @author Hollis
    
     */
    
    @Aspect
    
    @Component
    
    public class FacadeAspect {
    
        private static final Logger LOGGER = LoggerFactory.getLogger(FacadeAspect.class);
    
        @Autowired
    
        HttpServletRequest request;
    
        @Around("@annotation(com.hollis.annotation.Facade)")
    
        public Object facade(ProceedingJoinPoint pjp) throws Exception {
    
            Method method = ((MethodSignature)pjp.getSignature()).getMethod();
    
            Object[] args = pjp.getArgs();
    
            Class returnType = ((MethodSignature)pjp.getSignature()).getMethod().getReturnType();
    
            //循环遍历所有参数,进行参数校验
    
            for (Object parameter : args) {
    
                try {
    
                    BeanValidator.validateObject(parameter);
    
                } catch (ValidationException e) {
    
                    return getFailedResponse(returnType, e);
    
                }
    
            }
    
            try {
    
                // 目标方法执行
    
                Object response = pjp.proceed();
    
                return response;
    
            } catch (Throwable throwable) {
    
                return getFailedResponse(returnType, throwable);
    
            }
    
        }
    
        /**
    
         * 定义并返回一个通用的失败响应
    
         */
    
        private Object getFailedResponse(Class returnType, Throwable throwable)
    
            throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
    
            //如果返回值的类型为BaseResponse 的子类,则创建一个通用的失败响应
    
            if (returnType.getDeclaredConstructor().newInstance() instanceof BaseResponse) {
    
                BaseResponse response = (BaseResponse)returnType.getDeclaredConstructor().newInstance();
    
                response.setSuccess(false);
    
                response.setResponseMessage(throwable.toString());
    
                response.setResponseCode(GlobalConstant.BIZ_ERROR);
    
                return response;
    
            }
    
            LOGGER.error(
    
                "failed to getFailedResponse , returnType (" + returnType + ") is not instanceof BaseResponse");
    
            return null;
    
        }
    
    }

    以上代码,和前面的切面有点类似,主要是定义了一个切面,会对所有标注@Facade的方法进行统一处理,即在开始方法调用前进行参数校验,一旦校验失败,则返回一个固定的失败的Response。
    特别需要注意的是,这里之所以可以返回一个固定的BaseResponse,是因为我们会要求我们的所有对外提供的接口的response必须继承BaseResponse类,这个类里面会定义一些默认的参数,如错误码等。
    之后,只需要对需要参数校验的方法增加对应注解即可:

    @Facade
    
    public TestResponse query(User user) {
    
    }

    这样,有了以上注解和切面,我们就可以对所有的对外方法做统一的控制了。

    其实,以上这个facadeAspect我省略了很多东西,我们真正使用的那个切面,不仅仅做了参数检查,还可以做很多其他事情。比如异常的统一处理、错误码的统一转换、记录方法执行时长、记录方法的入参出参等等。
    总之,使用切面+自定义注解,我们可以统一做很多事情。除了以上的这几个场景,我们还有很多相似的用法,比如:
    统一的缓存处理。如某些操作需要在操作前查缓存、操作后更新缓存。这种就可以通过自定义注解+切面的方式统一处理。
    代码其实都差不多,思路也比较简单,就是通过自定义注解来标注需要被切面处理的累或者方法,然后在切面中对方法的执行过程进行干预,比如在执行前或者执行后做一些特殊的操作。
    使用这种方式可以大大减少重复代码,大大提升代码的优雅性,方便我们使用。
    但是同时也不能过度使用,因为注解看似简单,但是其实内部有很多逻辑是容易被忽略的。无脑的使用切面和注解,可能会引入一些不必要的问题。
    不管怎么说,自定义注解确实是一个很好的发明,可以减少很多重复代码。快快在你的项目中用起来吧。

     https://www.cnblogs.com/phyger/p/14073383.html

     

    故乡明
  • 相关阅读:
    【Robot Framework】List 的相关使用方法
    robot framework ——关键字run keyword if 如何在一个条件下接多个执行语句,以及如何写复杂条件句-关键字Run Keywords和and
    Robotframework之页面元素操作功能
    selenium之 下拉选择框Select
    selenium修改readonly属性的元件
    Robotframework之Python的自定义库
    python 元类详解
    从<<JavaScript权威指南>>抄下来的一个例子
    链接
    Python+selenium之获取文本值和下拉框选择数据
  • 原文地址:https://www.cnblogs.com/luweiweicode/p/14211362.html
Copyright © 2020-2023  润新知