当前位置: 首页 > 科技观察

自定义注释!绝对是程序员的利器!!

时间:2023-03-22 11:21:41 科技观察

Java中的注解相信很多人都不陌生,比如我们经常使用的@Override、@Autowired、@Service等,都是JDK或者Spring等框架提供的。在之前的采访中,我发现很多程序员只知道使用层面的注解。很少有人知道注解是如何实现的,更不用说使用自定义注解来解决实际问题了。但其实我觉得一个好的程序员的标准就是懂得如何优化自己的代码。在代码优化方面,如何精简代码,去除重复代码是一个至关重要的课题。在这个话题领域,自定义注解绝对算得上是功臣了。所以,在我看来,使用自定义注解≈优秀的程序员。所以,在这篇文章中,我将介绍几个笔者在开发中实际使用过的例子,向大家介绍如何使用注解来提升代码的风格。基础知识在Java中,注解分为元注解和自定义注解两种。很多人误以为自定义注解是开发者自己定义的,其他框架提供的不算,但实际上,我们上面说的注解其实就是自定义注解。编程界对“元”的描述有很多,如“元注解”、“元数据”、“元类”、“元表”等。这里的“meta”其实是从metaof翻译过来的。一般我们把元注解理解为描述注解的注解,元数据理解为描述数据的数据,元类理解为描述类的类……所以,在Java中,除了数量有限的固定“描述注解的注解”外,所有的注解都是自定义注解.在JDK中,有4种标准的注解类(meta-annotations)用于注解注解类型。它们是:@Target@Retention@Documented@Inherited除了上面四个注解外,其他注解都是自定义注解。以上四种元注解的作用我这里就不深入介绍了,大家可以自行学习。本文将要提到的几个例子,都是作者在日常工作中实际使用的场景。这些例子有一个共同点,就是都使用了Spring的AOP技术。什么是AOP,它的用法相信很多人都知道,这里就不介绍了。使用自定义注释进行日志记录。不知道大家有没有遇到过类似的需求。我只想在一个方法的入口或出口做统一的日志处理,比如记录入参、出参、记录方法执行等。时间等。如果在每个方法中自己写这样的代码,一方面会出现大量的代码重复,另一方面也很容易遗漏。在这种场景下,可以使用自定义注解+切面来实现这个功能。假设我们想在一些web请求方法中记录本次操作做了什么,比如新增一条记录或者删除一条记录等。首先,我们自定义一个注解:/***OperateLog的自定义注解*/@Target(ElementType.METHOD)@Retention(RetentionPolicy.RUNTIME)public@interfaceOpLog{/***业务类型,如增、删、改*@return*/publicOpTypeopType();/***业务对象名称,如订单,inventory,price*@return*/publicStringopItem();/***业务对象编号表达式,描述了如何获取订单编号的表达式公式*@return*/publicStringopItemIdExpression();}因为我们不仅需要记录日志中的操作,还需要知道被操作对象的具体唯一标识,比如订单号信息。但是每个接口方法的参数类型肯定是不一样的,很难有一个统一的标准。那么我们就可以使用Spel表达式来说明如何获取表达式中对应对象的唯一标识。有了上面的注解,接下来的部分就可以写了。主要代码如下:/***OpLog的切面处理类,用于通过注解获取日志信息,记录日志*@authorHollis*/@Aspect@ComponentpublicclassOpLogAspect{privatestaticfinalLoggerLOGGER=LoggerFactory.getLogger(OpLogAspect.class);@AutowiredHttpServletRequestrequest;@AutowiredHttpServletRequestrequest;@Around("@annotation(com.hollis.annotation.OpLog)")publicObjectlog(ProceedingJoinPointpjp)throwsException{Methodmethod=((MethodSignature)pjp.getSignature()).getMethod();OpLogopLog=method.getAnnotation(OpLog.class);Objectresponse=null;try{//目标方法执行response=pjp.proceed();}catch(Throwablethrowable){thrownewException(throwable);}if(StringUtils.isNotEmpty(opLog.opItemIdExpression())){SpelExpressionParserparser=newSpelExpressionParser();Expressionexpression=parser.parseExpression(opLog.opItemIdExpression());EvaluationContextcontext=newStandardEvaluationContext();//获取参数值Object[]args=pjp.getArgs();//获取运行时参数的名称LocalVariableTableParameterNameDiscovererdiscoverer=newLocalVariableTableParameterNameDiscoverer();String[]parameterNames=discoverer.getParameterNames(method);//将参数绑定到上下文中}//将方法的resp作为变量放在上下文中,变量名转换为小写字母开头的驼峰式if(response!=null){context.setVariable(CaseFormat.UPPER_CAMEL.to(CaseFormat.LOWER_CAMEL,response.getClass().getSimpleName()),response);}//解析表达式得到结果StringitemId=String.valueOf(expression.getValue(context));//执行日志记录handle(opLog.opType(),opLog.opItem(),itemId);}returnresponse;}privatevoidhandle(OpTypeopType,StringopItem,StringopItemId){//打印出LOGGER.info("opType="+opType.name()+",opItem="+opItem+",opItemId="+opItemId);}}在以上几个方面中,有几点需要大家注意:1、使用@Around注解指定标有OpLog的方法来设置方面2.使用Spel的相关方法,通过gh指定的representation,目标对象的唯一标识符是从相应的参数中获取的。3、方法执行成功后,输出日志。有了以上的方面和注解,我们只需要在相应的方法上加上注解即可,如:@RequestMapping(method={RequestMethod.GET,RequestMethod.POST})@OpLog(opType=OpType.QUERY,opItem="order",opItemIdExpression="#id")public@ResponseBodyHashMapview(@RequestParam(name="id")Stringid)throwsException{}上面是入参ID的参数列表中已经操作过的对象的唯一性,只是使用#id来指定它。如果被操作对象的唯一标识不在入参列表中,则可能是入参对象中的属性。用法如下:@RequestMapping(method={RequestMethod.GET,RequestMethod.POST})@OpLog(opType=OpType.QUERY,opItem="order",opItemIdExpression="#orderVo.id")public@ResponseBodyHashMapupdate(OrderVOorderVo)throwsException{},可以从作为参数输入的OrderVO对象的id属性值中获取。入参中没有包含我们要记录的唯一标识怎么办?最典型的是insert方法。插入成功之前,不知道主键ID是多少。我应该怎么办?在上面这方面,我们做了一件事情,就是我们也用表达式来分析方法的返回值。如果能分析得出具体的值,也是可以的。比如下面这样写:@RequestMapping(method={RequestMethod.GET,RequestMethod.POST})@OpLog(opType=OpType.QUERY,opItem="order",opItemIdExpression="#insertResult.id")public@ResponseBodyInsertResultinsert(OrderVOorderVo)throwsException{returnorderDao.insert(orderVo);}以上是使用自定义注解+切面进行日志记录的简单场景。我们来看看如何使用注解来验证方法参数。使用自定义注释进行预检查。我们在对外提供接口的时候,会对一些参数有一定的要求,比如某些参数值不能为空等。大多数情况下,我们需要自己主动验证,判断值是否为空。对方传入的是合理的。这里推荐使用HibernateValidator+自定义注解+AOP的方式来实现参数校验。首先,我们会有一个具体的入口类,定义如下:publicclassUser{privateStringidempotentNo;@NotNull(message="userNamecan'tbenull")privateStringuserName;}上面,userName参数不能为null。然后使用HibernateValidator定义一个工具类来进行参数验证。/***参数验证工具*@authorHollis*/publicclassBeanValidator{privatestaticValidatorvalidator=Validation.byProvider(HibernateValidator.class).configure().failFast(true).buildValidatorFactory().getValidator();/***@paramobjectobject*@paramgroupsgroups*/publicstaticvoidvalidateObject(Objectobject,Class...groups)throwsValidationException{Set>constraintViolations=validator.validate(object,groups);if(constraintViolations.stream().findFirst().isPresent()){thrownewValidationException(constraintViolations.stream().findFirst().get().getMessage());}}}上面的代码会验证一个bean,如果验证失败,会抛出ValidationException。接下来定义一个注解:/***facade接口注解,用于对facade统一进行参数校验和异常捕获。*

*注意使用这个注解需要注意。这个方法的返回值必须是BaseResponse的子类*
*/@Target(ElementType.METHOD)@Retention(RetentionPolicy.RUNTIME)public@interfaceFacade{}这个注解没有参数,只有用于标记那些需要进行参数校验的方法。接下来定义切面:/***Facade的切面处理类,参数校验和异常捕获统一统计*@authorHollis*/@Aspect@ComponentpublicclassFacadeAspect{privatestaticfinalLoggerLOGGER=LoggerFactory.getLogger(FacadeAspect.class);@AutowiredHttpServletRequestrequest;@Around("@annotation(com.hollis.annotation.Facade)")publicObjectfacade(ProceedingJoinPointpjp)throwsException{Methodmethod=((MethodSignature)pjp.getSignature()).getMethod();Object[]args=pjp.getArgs();ClassreturnType=((MethodSignature)pjp.getSignature()).getMethod().getReturnType();//循环遍历所有参数,进行参数校验for(Objectparameter:args){try{BeanValidator.validateObject(parameter);}catch(ValidationExceptione){returngetFailedResponse(returnType,e);}}try{//目标方法执行Objectresponse=pjp.proceed();returnresponse;}catch(Throwablethrowable){returngetFailedResponse(returnType,throwable);}}/***定义并返回一个泛型失败响应*/privateObjectgetFailedResponse(ClassreturnType,Throwablethrowable)throwsNoSuchMethodException,IllegalAccessException,InvocationTargetException,InstantiationException{//如果返回值的类型是BaseResponse的子类,则创建一个通用的失败响应;response.setSuccess(false);response.setResponseMessage(throwable.toString());response.setResponseCode(GlobalConstant.BIZ_ERROR);returnresponse;}LOGGER.error("failedtogetFailedResponse,returnType("+returnType+")isnotinstancesennoll"BaseResp;}上面的代码和前面的aspect有些类似,主要定义了一个aspect,它会统一处理所有@Facade标记的方法,即在开始方法调用前进行参数校验,校验失败则返回一个固定的failedResponse需要特别注意,这里之所以可以返回一个固定的BaseResponse,是因为我们会要求我们对外提供的接口的所有response都必须继承BaseResponse类,并且会在这个类中定义一些默认参数,比如错误码等,之后只需要在需要参数校验的方法上加上相应的注解:@FacadepublicTestResponsequery(Useruser){}这样,有了上面的注解和方面,我们就可以统一控制所有外部方法。其实上面的facadeAspect我省略了很多东西。我们真正用到的切面不只是做参数检查,还可以做很多其他的事情。比如异常的统一处理,错误码的统一转换,记录方法的执行时间,记录方法的输入输出参数等。总之,使用切面+自定义注解,我们可以在一个时间里做很多事情统一方式。除了以上场景,我们还有很多类似的用法,比如:统一缓存处理。比如有些操作需要在操作前检查缓存,操作后更新缓存。这可以通过自定义注解+切面来统一处理。其实代码都差不多,思路也比较简单。就是通过自定义注解来标记切面需要处理的累点或者方法,然后在切面中干预方法的执行过程,比如在执行前或者执行后做一些特殊的操作。.使用这种方式可以大大减少重复代码,大大提高代码的优雅度,方便我们的使用。但同时也不能过度使用,因为注解看似简单,其实里面有很多容易被忽略的逻辑。就像我在上篇文章《Spring官方都推荐使用的@Transactional事务,为啥我不建议使用!》中提到的观点一样,无脑地使用aspects和annotation可能会引入一些不必要的问题。不管怎样,自定义注解是一个很好的发明,可以减少很多重复的代码。在您的项目中快速使用它。