您当前的位置:首页 > 电脑百科 > 程序开发 > 程序员

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

时间:2020-11-10 10:18:07  来源:  作者:
自定义注解!绝对是程序员装X的利器

 

相信很多人对JAVA中的注解都很熟悉,比如我们经常会用到的一些如@Override、@Autowired、@Service等,这些都是JDK或者诸如Spring这类框架给我们提供的。

在以往的面试过程中,我发现,关于注解的知识很多程序员都仅仅停留在使用的层面上,很少有人知道注解是如何实现的,更别提使用自定义注解来解决实际问题了。

但是其实,我觉得一个好的程序员的标准就是懂得如何优化自己的代码,那在代码优化上面,如何精简代码,去掉重复代码就是一个至关重要的话题,在这个话题领域,自定义注解绝对可以算得上是一个大大的功臣。

所以,在我看来,会使用自定义注解 ≈ 好的程序员。

那么,本文,就来介绍几个,作者在开发中实际用到的几个例子,向你介绍下如何使用注解来提升你代码的逼格。

基本知识

在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我省略了很多东西,我们真正使用的那个切面,不仅仅做了参数检查,还可以做很多其他事情。比如异常的统一处理、错误码的统一转换、记录方法执行时长、记录方法的入参出参等等。

总之,使用切面+自定义注解,我们可以统一做很多事情。除了以上的这几个场景,我们还有很多相似的用法,比如:

统一的缓存处理。如某些操作需要在操作前查缓存、操作后更新缓存。这种就可以通过自定义注解+切面的方式统一处理。

代码其实都差不多,思路也比较简单,就是通过自定义注解来标注需要被切面处理的或者方法,然后在切面中对方法的执行过程进行干预,比如在执行前或者执行后做一些特殊的操作。

使用这种方式可以大大减少重复代码,大大提升代码的优雅性,方便我们使用。

但是同时也不能过度使用,因为注解看似简单,但是其实内部有很多逻辑是容易被忽略的。就像我之前写过一篇《Spring官方都推荐使用的@Transactional事务,为啥我不建议使用!》中提到的观点一样,无脑的使用切面和注解,可能会引入一些不必要的问题。

不管怎么说,自定义注解确实是一个很好的发明,可以减少很多重复代码。快快在你的项目中用起来吧。



Tags:自定义注解   点击:()  评论:()
声明:本站部分内容及图片来自互联网,转载是出于传递更多信息之目的,内容观点仅代表作者本人,如有任何标注错误或版权侵犯请与我们联系(Email:2595517585@qq.com),我们将及时更正、删除,谢谢。
▌相关推荐
对项目的基本介绍 1.整个框架主要是给MVVM框架使用的,自己写完interface接口后,通过自定义的注解就能自动生成接口方法 2.用Kotlin的Flow去代替Rxjava,因为我发现RxJava功能很...【详细内容】
2021-12-08  Tags: 自定义注解  点击:(16)  评论:(0)  加入收藏
相信很多人对Java中的注解都很熟悉,比如我们经常会用到的一些如@Override、@Autowired、@Service等,这些都是JDK或者诸如Spring这类框架给我们提供的。在以往的面试过程中,我...【详细内容】
2020-11-10  Tags: 自定义注解  点击:(112)  评论:(0)  加入收藏
▌简易百科推荐
JAVA开发工程师(北京)本科 3-5年经验 面议 (招1人)岗位职责:1.负责我行应用系统的设计,完成软件编码工作,负责管理代码设计规范等工作;2.根据应用需求分析说明书,评估需求研发的可行...【详细内容】
2021-12-27  just do丶IT公众号    Tags:国企   点击:(1)  评论:(0)  加入收藏
今天聊聊编程的本质。程序就是数据结构+控制+逻辑,程序员编程工作的本质是翻译,翻译机要来了,程序员怎么办?黑客帝国中的程序黑客帝国4就要上映了,不知道前三部你看懂了么?值得多...【详细内容】
2021-12-17  博士聊IT    Tags:程序员   点击:(8)  评论:(0)  加入收藏
梦醒之后,每个人对于这份职业的未来、互联网行业的未来,以及更重要的,自己的未来都有了更现实的判断 文 | 祝颖丽编辑 | 黄俊杰一个生于 1986 年的人,他所走过的前半生:从出生起,...【详细内容】
2021-12-03    财经杂志  Tags:程序员   点击:(15)  评论:(0)  加入收藏
前些天在头条看到一个八二年的哥们,述说自己找工作屡次被拒的问题,在网上引起了广泛的讨论,这件事给我留下了很深的印象,因为这哥们和我同是程序员,都人到中年,上有老下有小。唯一...【详细内容】
2021-12-01  云南贤哥在深圳    Tags:程序员   点击:(20)  评论:(0)  加入收藏
很多读者都问过一个问题:程序员如何实现高速成长?之前也写过相关的文章,强调的主要是夯实计算机体系基础知识。 再说另一个诀窍:多看经典开源项目,这些项目大多是众多顶尖程序员...【详细内容】
2021-11-30  findyi    Tags:程序员   点击:(14)  评论:(0)  加入收藏
近日,一位45岁的网民在中国政府网留言求职,引发关注。该网民自称是一名软件开发人员,今年45岁,精通各种技术体系,“而我辞职回家半年后再回来寻找工作机会的时候,却发现连个面试...【详细内容】
2021-11-17  郭主任    Tags:程序员   点击:(42)  评论:(0)  加入收藏
即使在安全技术取得进步之后,网络犯罪仍在不断增加。据统计,网络犯罪每分钟给企业造成约 290 万美元的损失。主要是因为新技术不断涌现,难以维护安全。随着网络威胁的增加,网络...【详细内容】
2021-11-04  章大千    Tags:编程语言   点击:(40)  评论:(0)  加入收藏
北漂小伙李强(化名),在北京互联网大厂工作7年,月薪3万,离职回老家开摄影店,亏了200万。李强出生于山西一座名不经传的小城市,互联网专业大学毕业的他,没有听父母的劝言回到家乡考公...【详细内容】
2021-10-29  霸王课  今日头条  Tags:程序员   点击:(51)  评论:(0)  加入收藏
程序员是青春饭,这在国内似乎是公认的。所以很多公司不愿招大龄程序员,很多程序员也“知趣”地及早转型。有的做管理,有的做架构,我还见过改行卖保险的。总之,年龄大了不想敲代码...【详细内容】
2021-10-27  编程的艺术    Tags:   点击:(29)  评论:(0)  加入收藏
软件工程专业有Web开发、移动终端开发、大数据开发、计算机系统工程师、视频开发工程师、计算机软件应用工程师等就业方向。第一,Web开发。Web开发包括前端开发和后端开发。...【详细内容】
2021-10-19  辰新   企鹅号  Tags:软件工程   点击:(79)  评论:(0)  加入收藏
相关文章
    无相关信息
最新更新
栏目热门
栏目头条