使用JSR 303和AOP简化你的接口开发
本文出处:http://blog.csdn.net/chaijunkun/article/details/44854071,转载请注明。由于本人不定期会整理相关博文,会对相应内容作出完善。因此强烈建议在原始出处查看此文。
前言
如今互联网项目都采用HTTP接口形式进行开发。无论是Web调用还是智能设备APP调用,只要约定好参数形式和规则就能够协同开发。返回值用得最多的就是JSON形式。服务端除了保证正常的业务功能,还要经常对传进来的参数进行验证,例如某些参数不能为空,字符串必须含有可见字符,数值必须大于0等这样的要求。那么如何做到最佳实践,让接口开发的效率提升呢?今天我们就来聊一聊JSR 303和AOP的结合。
什么是JSR 303
首先JSR 303是Java的标准规范,根据官方文档的描述(https://jcp.org/en/jsr/proposalDetails?id=303):在一个应用的不同层面(例如呈现层到持久层),验证数据是一个是反复共同的任务。许多时候相同的验证要在每一个独立的验证框架中出现很多次。为了提升开发效率,阻止重复造轮子,于是形成了这样一套规范。该规范定义了一个元数据模型,默认的元数据来源是注解(annotation)。针对该规范的验证API不是为某一个编程模型来开发的,因此它不束缚于Web或者持久化。也就是说不仅仅是服务端应用编程可以用它,甚至富客户端swing应用开发也可以用它。相关的入门参考资料可以参见我之前的博文:http://blog.csdn.net/chaijunkun/article/details/9083171,也可以参阅IBM开发者社区的一篇文章:http://www.ibm.com/developerworks/cn/java/j-lo-jsr303/。
什么是AOP
然后再聊聊AOP。AOP就是Aspect Oriented Programming(面向切面编程)的缩写。AOP 是一个概念,一个规范,本身并没有设定具体语言的实现,这实际上提供了非常广阔的发展的空间。AspectJ就是AOP的一个很悠久的实现,在Java语言中,他使用的范围很广。到底什么是切面呢?举个例子吧。在Spring MVC中,开发了若干个Controller(控制器),并且这些控制器负责不同的模块。每一个控制器中都有若干个public的方法来对应各自的@RequestMapping。现在我想增加一个日志,记录调用每个URL请求后端处理的时间。如果只有一两个public的方法还好一些,无非在方法开头加个开始时间startTime,在末尾加个结束时间endTime。endTime - startTime=执行时间,最后输出就好了。可是如果一个系统有几十上百个控制器方法呢?挨个写吗?老板说要改下日志格式呢?整个人会崩溃的!那我们就把这个描述抽象出来:public * net.csdn.blog.chaijunkun.controller.*.*(..),在包net.csdn.blog.chaijunkun.controller下面的所有类,所有public的,无论有没有返回值,也无论参数是什么样的方法,全部聚合在一起。就像划定了一个满足特定条件的“圈”,那么这个“圈”就是切面,所有满足这个条件的方法都是”切点“。我们的编程就建立在这之上。只要在这个切面上加上开始时间,调用切点,再记录结束时间,最后输出就可以了。只写一次,改也很方便。
BTW,面向切面编程是基于动态代码织入的。也就是说通过预先配置切面规则,类加载器在加载目标类时会将切面方法加入到切点周围,简单来说就是“动态修改字节码”。如果你的项目在发布时使用了代码混淆,那么有些时候面向切面的代码将会失效,原因就在于混淆后的方法名可能已经不满足你的配置了。
实例
接下来我们就来个实例,说明一下Web项目中JSR 303为什么要和AOP结合。该实例的场景是返回JSON数据的接口,功能是对Student实体和Teacher实体进行CRUD操作:
建立Web项目及其依赖
为了简化描述,使用maven来建立Web项目。使用JSR 303需要引入一个该规范实现使用的框架,这里使用Hibernate Validator。另外针对AOP,需要引入AspectJ相关依赖。具体如下:
<dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-validator</artifactId> <version>5.1.3.Final</version> </dependency> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjrt</artifactId> <version>1.6.8</version> </dependency> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> <version>1.6.8</version> </dependency>另外还需要引入Spring Web和MVC相关的包,这里就不赘述了
配置Spring Servlet
这里我们要启用注解,并且打开Spring对JSR 303的支持,另外扫描指定包下的Controller进行实例化:
<bean id="validator" class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean" /> <mvc:annotation-driven /> <context:component-scan base-package="net.csdn.blog.chaijunkun.controller" />
编写持久化对象
在本例中,为了简化代码,将传参VO与持久化PO共用一个。
Student实体:
** * 学生对象 * @author chaijunkun * @since 2015年4月3日 */ public class Student { @NotNull(groups = {Get.class, Del.class, Update.class}) private Integer id; @NotBlank(groups = {Add.class, Update.class}) private String name; @NotNull(groups = {Add.class, Update.class}) private Boolean male; private Integer teacherId; //getters and setters }在Student实体约束中引入了groups。主要是针对不同场景下验证的字段不同。该参数必须是interface类型,不用实现,就是一个标记而已。声明如下:
/** * 学生验证分组 * @author chaijunkun * @since 2015年4月3日 */ public interface StudentGroup { public static interface Add{} public static interface Del{} public static interface Get{} public static interface Update{} }
Teachers实体:
/** * 教师对象 * @author chaijunkun * @since 2015年4月3日 */ public class Teacher { @NotNull(groups = {Get.class, Del.class, Update.class}) private Integer id; @NotBlank(groups = {Add.class, Update.class}) private String name; @NotNull(groups = {Add.class, Update.class}) private Boolean male; //getters and setters }同Student实体类似,需要定义一个Teacher实体专用的验证编组:
/** * 教师验证分组 * @author chaijunkun * @since 2015年4月3日 */ public interface TeacherGroup { public static interface Add{} public static interface Del{} public static interface Get{} public static interface Update{} }
编写虚拟的持久化服务
Student实体持久化服务:
/** * 学生持久化服务 * @author chaijunkun * @since 2015年4月3日 */ @Service public class StudentService { private static Map<Integer, Student> vDB = new HashMap<Integer, Student>(); private static int counter = 1; public Integer add(Student student){ student.setId(counter); vDB.put(counter, student); counter++; return student.getId(); } public boolean del(Integer id){ Student student = vDB.remove(id); return student != null ? true : false; } public Student get(Integer id){ return vDB.get(id); } public boolean update(Student student){ Student dbObj = vDB.get(student.getId()); if (dbObj==null){ return false; }else{ vDB.put(student.getId(), student); return true; } } }Teacher实体持久化服务
/** * 教师持久化服务 * @author chaijunkun * @since 2015年4月3日 */ @Service public class TeacherService { private static Map<Integer, Teacher> vDB = new HashMap<Integer, Teacher>(); private static int counter = 1; public Integer add(Teacher teacher){ teacher.setId(counter); vDB.put(counter, teacher); counter++; return teacher.getId(); } public boolean del(Integer id){ Teacher teacher = vDB.remove(id); return teacher != null ? true : false; } public Teacher get(Integer id){ return vDB.get(id); } public boolean update(Teacher teacher){ Teacher dbObj = vDB.get(teacher.getId()); if (dbObj==null){ return false; }else{ vDB.put(teacher.getId(), teacher); return true; } } }
规定接口返回数据结构
返回数据结构为JSON。当出现错误时,格式为:{"code":-1,"msg":"必选参数丢失"},当成功时,格式为:{"code":0,"msg":{返回数据}}
/** * 响应对象 * @author chaijunkun * @since 2015年4月3日 */ @JsonPropertyOrder(alphabetic = false) public class Resp<T> { /** * 生成成功返回对象 * @param msg * @return */ public static <T> Resp<T> success(T msg){ Resp<T> resp = new Resp<T>(); resp.setCode(0); resp.setMsg(msg); return resp; } /** * 生成失败返回对象 * @param msg * @return */ public static Resp<String> fail(String msg){ Resp<String> resp = new Resp<String>(); resp.setCode(-1); resp.setMsg(msg); return resp; } /** 响应代码 */ private Integer code; /** 响应消息 */ private T msg; //getters and setters }
编写API接口
由于Teacher接口与Student接口类似,本文只给出一个接口代码
/** * 学生控制器 * @author chaijunkun * @since 2015年4月3日 */ @Controller @RequestMapping(value = "student") public class StudentController { @Autowired private StudentService studentService; @ResponseBody @RequestMapping(value = "add", method = {RequestMethod.GET}) public Resp<?> add(@Validated(StudentGroup.Add.class) Student student, BindingResult result){ Integer id = studentService.add(student); if (id == null){ return Resp.fail("添加学生信息失败"); }else{ return Resp.success(id); } } @ResponseBody @RequestMapping(value = "del", method = {RequestMethod.GET}) public Resp<?> del(@Validated(StudentGroup.Del.class) Student student, BindingResult result){ if (studentService.del(student.getId())){ return Resp.success(true); }else{ return Resp.fail("删除学生信息失败"); } } @ResponseBody @RequestMapping(value = "get", method = {RequestMethod.GET}) public Resp<?> get(@Validated(StudentGroup.Get.class) Student student, BindingResult result){ Student data = studentService.get(student.getId()); if (data == null){ return Resp.fail("未找到指定学生"); }else{ return Resp.success(data); } } @ResponseBody @RequestMapping(value = "update", method = {RequestMethod.POST}) public Resp<?> update(@Validated(StudentGroup.Update.class) Student student, BindingResult result){ if (studentService.update(student)){ return Resp.success(true); }else{ return Resp.fail("更新学生信息失败"); } } }
使用JSR 303进行验证,需要在Controller参数前加入@Validated注解。如果指定特别的编组,需要将编组class作为参数附加给该注解。最后一个参数定义为BindingResult类型。这样,在进入该Controller方法后使用result.hassErrors()方法来判断参数是否通过了约束验证。若没通过,可以通过BindingResult对象来获取详细的错误信息。当然,这不是我们本文的用法,我们要突破这种麻烦的写法。
针对Controller方法的切面编程
由于例子总的所有Controller都放在net.csdn.blog.chaijunkun.controller包下,因此切面的配置应该是这样(在dispatcher-servlet.xml中配置):
<!-- JSR 303验证切面 --> <bean id="jsrValidationAdvice" class="net.csdn.blog.chaijunkun.aop.JSRValidationAdvice" /> <aop:config> <aop:pointcut id="jsrValidationPC" expression="execution(public * net.csdn.blog.chaijunkun.controller.*.*(..))" /> <aop:aspect id="jsrValidationAspect" ref="jsrValidationAdvice"> <aop:around method="aroundMethod" pointcut-ref="jsrValidationPC" /> </aop:aspect> </aop:config>重点来了,我们来看看JSRValidationAdvice是如何实现的:
/** * JSR303验证框架统一处理 * @author chaijunkun * @since 2015年4月1日 */ public class JSRValidationAdvice { Logger logger = LoggerFactory.getLogger(JSRValidationAdvice.class); /** * 判断验证错误代码是否属于字段为空的情况 * @param code 验证错误代码 */ private boolean isMissingParamsError(String code){ if (code.equals(NotNull.class.getSimpleName()) || code.equals(NotBlank.class.getSimpleName()) || code.equals(NotEmpty.class.getSimpleName())){ return true; }else{ return false; } } /** * 切点处理 * @param joinPoint * @return * @throws Throwable */ public Object aroundMethod(ProceedingJoinPoint joinPoint) throws Throwable { BindingResult result = null; Object[] args = joinPoint.getArgs(); if (args != null && args.length != 0){ for (Object object : args) { if (object instanceof BindingResult){ result = (BindingResult)object; break; } } } if (result != null && result.hasErrors()){ FieldError fieldError = result.getFieldError(); String targetName = joinPoint.getTarget().getClass().getSimpleName(); String method = joinPoint.getSignature().getName(); logger.info("验证失败.控制器:{}, 方法:{}, 参数:{}, 属性:{}, 错误:{}, 消息:{}", targetName, method, fieldError.getObjectName(), fieldError.getField(), fieldError.getCode(), fieldError.getDefaultMessage()); String firstCode = fieldError.getCode(); if (isMissingParamsError(firstCode)){ return Resp.fail("必选参数丢失"); }else{ return Resp.fail("其他错误"); } } return joinPoint.proceed(); } }该切面处理方法属于围绕Controller方法的形式,在进入Controller方法前会先调用该切面的aroundMethod(别问为什么,看上文中这个配置:<aop:around method="aroundMethod" pointcut-ref="jsrValidationPC" />),切面方法要求第一个参数类型必须为org.aspectj.lang.ProceedingJoinPoint。进入切面方法后,遍历Controller的所有参数类型,看下有没有BindingResult类型的参数。如果有,就调用它,判断是否有错误。如果有错误,通过日志将详细信息输出。并且返回错误信息。如果没有错误,执行切点的proceed()方法,按预定Controller逻辑进行计算。
实例总结
通过上面的例子,可以看到最终业务逻辑并没有验证代码,只需要注意参数前使用@Validated注解,在最后加入BindingResult类型参数即可。切面会自动帮你做验证检查。今后的接口开发只需要关注业务即可,恭喜你,再也不用为验证的事情烦心了。
本文代码已上传至资源分享。下载地址:
郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。