作者 l Hollis

来源 l Hollis(ID:hollischuang)

相信很多人对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);@AutowiredHttpServletRequest 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);@AutowiredHttpServletRequest 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事务,为啥我不建议使用!》中提到的观点一样,无脑的使用切面和注解,可能会引入一些不必要的问题。

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

推 荐

程序人生公众号全新搜索技能上线啦!

只要在公众号后台回复消息

就能自动回复想搜索的内容啦!

简直是程序员必备的搜索神器!

猜猜回复“Mysql安装”会出现什么

点分享点点赞点在看

自定义注解!绝对是程序员装大佬的利器!!相关推荐

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

    GitHub 18k Star 的Java工程师成神之路,不来了解一下吗! GitHub 18k Star 的Java工程师成神之路,真的不来了解一下吗! GitHub 18k Star 的Java工 ...

  2. 程序员装B小技巧——管理你的桌面

    程序员装B小技巧--管理你的桌面 引言 想不想拥有一个和下方截图一样简单快捷的桌面?随着工作学习的时间推移,我们电脑里面的各种软件和文档日渐增多,导致我们的桌面变得杂乱且不美观.身为一个程序员怎么能容 ...

  3. java装逼的话_程序员装逼指南(语言篇)

    原标题:程序员装逼指南(语言篇) 语言:千万不要说自己是做Java或者.Net的,一下子就屌丝了.PHP现在也不行,Python稍微有点烂大街,但还是明显要强过前几个.剩下的可以说自己是做Ruby的, ...

  4. 揭开程序员装 13 行为的面具

    为什么80%的码农都做不了架构师?>>>    核心提示] 程序员一直都是很善良的IT工种,勤勤恳恳不辞辛苦的工作,不过今天可不是为了夸程序员.来 818 程序员有哪些装 13 的行 ...

  5. 高级程序员装逼指南,是高级哦!(转)

    高级程序员装逼指南,是高级哦! 下面这个才是高级版本 ;; 这是注释 ;; 0.9版本 ;; 2011年8月22日 ** 前言 ** 最近网上出了一个<程序员装逼指南>,觉得这个东西其实图 ...

  6. 高级程序员装逼指南,是高级哦!

    下面这个才是高级版本 ;; 这是注释 ;; 0.9版本 ;; 2011年8月22日 ** 前言 ** 最近网上出了一个<程序员装逼指南>,觉得这个东西其实图样图森破 然后在下跟微博上的一些 ...

  7. 21条精通程序员装逼技术

    1. 电脑一定不能盖机箱盖,用来表示自己拆装机小CASE且经常升级自己用了四五年的破电脑<br><br> 2. 桌上必须散放各种内存条 优盘 或者拆开的光驱硬盘报废的CPU等 ...

  8. java程序员装黑苹果_黑苹果教程(四)———MAC OS 10.11+固态硬盘+自定义引导

    为了使黑苹果的体验效果更佳,楼主购买了一块固态硬盘,为了黑苹果还把主板搞出问题了,,哎,方法没有掌握好,为了今后的同志少走弯路,同时给自己留个笔记,所以分享一下自己的经验. 楼主黑苹果也经历了三个版本 ...

  9. 程序员装逼被怼,决定用面试证明自己,结果......

    黑客技术点击右侧关注,了解黑客的世界! Java开发进阶点击右侧关注,掌握进阶之路! Linux编程点击右侧关注,免费入门到精通! 作者丨三级狗 https://www.zhihu.com/quest ...

最新文章

  1. MyEclipse Enterprise Workbench 9.0 破解及注册机 注册码
  2. Node.js 将Json文件数据转为SQL可执行的insert语句
  3. 【leetcode】3Sum
  4. 对SqlCommandBuilder类理解
  5. python中的异常处理 try..except
  6. 但有很多的HTML5朋友
  7. go和python性能对比_Go和Python Web服务器性能对比
  8. Citrix Xendesktop5中创建虚拟机失败的2个案例分析
  9. 12.9日个人工作总结
  10. 安全浏览器版本过低?该升级浏览器内核了
  11. c语言中的汉字编码,【C语言学习】C语言汉字编码。。。C语言中汉字的输入
  12. window系统查找占用端口号进程并杀死
  13. 软件工程专业和网络工程专业的区别
  14. 中国大学排行榜网站的爬取
  15. UE4骨骼动画新手入门
  16. 【LeetCode每日一题】【2021/12/8】689. 三个无重叠子数组的最大和
  17. 树莓派3B通过U盘启动系统
  18. [Python] 黑白棋(翻转棋)小游戏
  19. 2017.10.23 模拟考试 (题目来自:学大伟业)
  20. Primavera P6或项目管理站点/论坛地址,交流群组

热门文章

  1. 一句话总结英国新规:不欢迎AI专业的中国留学生、学者与研究员
  2. 献给新一代AI后浪们----《后丹》
  3. 下一代 MES,智能制造的骨架
  4. Science评论:量子计算目前最大的挑战,在0和1之间
  5. SAP SD基础知识之维护中央信贷主数据
  6. 神秘的Waymo一反常态,CVPR现场发布大型自动驾驶数据集
  7. AI公司为什么集体造“芯”?寻求商业变现新途径
  8. GANs笔记(1) - 初步了解 GANs
  9. 量子领域、人工智能都是佼佼者,中国科技实力详解
  10. ERP与EWM集成配置---ERP 端(一)