您现在的位置是:首页 > 文章详情

使用自定义注解实现接口参数校验

日期:2019-01-23点击:314

1.前言

在接口的开发中,我们有时会想让某个接口只可以被特定的人(来源)请求,那么就需要在服务端对请求参数做校验.

这种情况我们可以使用interceptor来统一进行参数校验,但是如果很多个接口,有不同的的设定值,我们总不能写很多个interceptor,然后按照path逐一添加吧?

面对这种情况,我们可以选择自定义一个注解,由注解来告诉我们,这个接口允许的访问者是谁.

注:在本文的示例中,仅实现了对某一个字段的校验,安全性并不高,实际项目中,可以采用多字段加密的方式,来保证安全性,原理和文中是一样的.

2.java 注解介绍

Java Annotation是JDK5.0引入的一种注释机制。

Annotation是代码里的特殊标记,这些标记可以在编译、类加载、运行时被读取,并执行相应的处理。

通过使用Annotation,程序员可以在不改变原有逻辑的情况下,在源文件中嵌入一些补充信息。

Annotation可以像修饰符一样被使用,可以用于package、class、interface、constructor、method、member variable(成员变量)、parameter、local variable(局部变量)、annotation(注解),jdk 1.8之后,只要出现类型(包括类、接口、注解、枚举)的地方都可以使用注解了。

我们可以使用JDK以及其它框架提供的Annotation,也可以自定义Annotation。

3.元注解(meta-annotation)

元注解是什么呢?在我的理解里,元注解是java官方提供的,用于修饰其他注解的几个属性.

因为开放了自定义注解,所以所有的注解必须有章可循,他们的一些属性必须要被定义.比如:这个注解用在什么地方?类上还是方法上还是字段上?这个注解的生命周期是什么?是保留在源码里供人阅读就好,还是会生成在class文件中,对程序产生实际的作用?这些都需要被提前定义好,因此就有了:

这四个元注解@Target、@Retation、@Inherited、@Documented.

接下来对四个元注解逐一说明

@Target

用于描述注解的使用范围(即:被描述的注解可以用在什么地方)

他的取值范围JDK定义了枚举类ElementType,他的值共有以下几种:

  1. CONSTRUCTOR:用于描述构造器
  2. FIELD:用于描述域即类成员变量
  3. LOCAL_VARIABLE:用于描述局部变量
  4. METHOD:用于描述方法
  5. PACKAGE:用于描述包
  6. PARAMETER:用于描述参数
  7. TYPE:用于描述类、接口(包括注解类型) 或enum声明

注:在JDK1.8,新加了两种类型,

  1. TYPE_PARAMETER:表示这个 Annotation 可以用在 Type 的声明式前,
  2. TYPE_USE 表示这个 Annotation 可以用在所有使用 Type 的地方

@Retention

表示需要在什么级别保存该注释信息,用于描述注解的生命周期(即:被描述的注解在什么范围内有效)

他的取值范围来自于枚举类RetentionPolicy,取值共以下几种:

  1. SOURCE:在源文件中有效(即源文件保留)
  2. CLASS:在class文件中有效(即class保留)
  3. RUNTIME:在运行时有效(即运行时保留)

@Documented

@Documented用于描述其它类型的annotation应该被作为被标注的程序成员的公共API,因此可以被例如javadoc此类的工具文档化。Documented是一个标记注解,没有成员。

@Inherited

@Inherited 元注解是一个标记注解,@Inherited阐述了某个被标注的类型是被继承的。如果一个使用了@Inherited修饰的annotation类型被用于一个class,则这个annotation将被用于该class的子类。

4.常见注解

常用的第三方框架实现了非常多的注解,比如MybatisParam,SpringComponent,Service,fastjsonJSONfield等等.

具体的实现方法这里不多解释了,有兴趣的朋友可以取看一下fastjson的源码,该项目相比spring等框架,简单一些也更容易理解.

看到这种注解或简单或复杂的功能之后,我们是否也可以自己来动手实现一个呢?

5.自定义注解

5.1.定义注解

首先我们来定义注解:

package com.huyan.demo.config; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * created by huyanshi on 2019/1/20 */ @Target(ElementType.METHOD) // 该注解使用在方法上 @Retention(RetentionPolicy.RUNTIME) //运行时注解 @Documented public @interface CheckSource { //该注解的参数,是一个string数组 String[] sources() default {"all"}; }

我们需要的注解用于校验参数,因此它的使用范围是方法,生命周期是运行时保留.此外,注解有一个类型为string数组的参数,用来表示当前方法允许的source列表.

5.2.编写注解解析器

其实一开始我在这里纠结了许久,因为我不能理解一个注解应该在哪里以什么方式调用.

按照我的思路,每个注解应该有一个字段(或者类似的东西),来指示应该去哪里调用这个注解的真正使用.

后来经过细细思考,发现这是不现实的,因为注解的作用完全没有规律可言,你可以实现任何你想要的功能,返回值可以使任意值,里面的逻辑也是任意的.

那么就意味着,你需要为你的注解负责,否则他没有任何作用.也就是说,你需要为自己的注解编写注解解析器,来定义什么时候用到这个注解,用它干什么?

@纯个人观点,慎看
经过在网上冲浪,我发现注解解析器的主要形式有三种:

1.interceptor

这种方式比较方便,可以直接拦截所有的请求,检查该请求进入的类及方法上有没有特定的注解,如果有怎么怎么操作一波.

但是局限性比较大,我们又不是只在controller里面会用到注解.

2.AOP

这种方式也比较方便,扩展性比较好,当你需要在新的地方用到该注解,新增一个切点就好.

3.封装成方法,随时调用

这种是大部分人喜闻乐见的(其实最喜闻乐见的是每次用到就写一遍呗),但是如果不经常重构一下代码,你会发现导出充满了你对某一个注解的使用代码,那就很崩溃了,你需要尽量将其封装一下,放在统一的工具类,每次需要的时候调用即可.

@个人观点结束!

由于我们这次的需求是拦截不合法的请求,所以当然是第一种方式比较靠谱,因此我们写了一个拦截器:

package com.huyan.demo.config; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.web.method.HandlerMethod; import org.springframework.web.servlet.handler.HandlerInterceptorAdapter; /** * created by huyanshi on 2019/1/20 */ public class CheckSourceInterceptor extends HandlerInterceptorAdapter { private static Logger LOG = LoggerFactory.getLogger(CheckSourceInterceptor.class); @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (!(handler instanceof HandlerMethod)) { LOG.warn("UnSupport handler"); throw new IllegalArgumentException("Interceptor only supports HandlerMethod handler"); } //拿到请求参数里面的source参数 String source = request.getParameter("source"); String errorMsg = null; //如果source为空,返回错误 if (null == source || "".equals(source)) { errorMsg = "No source in params"; } if (errorMsg != null) { response.setStatus(500); LOG.info(errorMsg); response.getWriter().write(errorMsg); return false; } //拿到该方法上的注解对象 CheckSource checkSource = getCheckSource((HandlerMethod) handler); //如果拿到的对象为空,说明没有此注解,直接放行 if (checkSource != null) { //拿到注解对象的属性,即允许通行的source列表 String[] sources = checkSource.sources(); if (sources.length == 0 || sources[0].equals("all")) { //列表为空或者为默认值,放行 return true; } //遍历列表,如果传入的参数在其中,则放行 for (String s : sources) { if (s.equals(source)) { return true; } } //如果传入的source参数不在允许的参数列表中,则拦截请求,并返回错误信息 errorMsg = "source is not support"; response.getWriter().write(errorMsg); return false; } return true; } /** * 拿到该方法上的checksource注解对象 */ private CheckSource getCheckSource(HandlerMethod handlerMethod) { if (handlerMethod.getBeanType().isAnnotationPresent(CheckSource.class)) { return handlerMethod.getBeanType().getAnnotation(CheckSource.class); } else if (handlerMethod.getMethod().isAnnotationPresent(CheckSource.class)) { return handlerMethod.getMethod().getAnnotation(CheckSource.class); } return null; } } 

代码中添加了比较详细的注释,这里只写一下大概的思路:

通过拦截器的机制,拿到该方法上的CheckSource对象,该对象可能为空,不为空的时候拿到它的sources属性,之后依次遍历,判断传入的source是否在允许的列表中.

在这个拦截器中,我们定义了:

1.何时使用这个注解?

在我们配置的,使用这个拦截器的时候,进入controller层的某一个方法时.

2.怎么使用这个注解?

拿传入的source参数和这个注解的属性sources列表一一匹配,有匹配上的则允许请求,无匹配值则返回错误信息.

5.3.实际使用注解

5.3.1.首先配置这个拦截器,拦截status接口

package com.huyan.demo.config; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; /** * created by huyanshi on 2019/1/20 */ @Configuration public class WebMvcConfig implements WebMvcConfigurer { CheckSourceInterceptor checkSourceInterceptor = new CheckSourceInterceptor(); @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(checkSourceInterceptor).addPathPatterns("/status"); } } 

5.3.2.status接口

package com.huyan.demo.controller; import com.huyan.demo.config.CheckSource; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; /** * created by pfliu on 2018/9/2 */ @RestController public class StatusController { private Logger logger = LoggerFactory.getLogger(this.getClass()); @CheckSource(sources = {"huyan", "huihui"}) @GetMapping(value = "/status") public Object status(@RequestParam("source") String source) { return "哈哈哈"; } }

好,编码全部完成了.

启动项目,看一下结果.

5.3.3.测试结果

  • 不带source参数

  • 错误的source参数

  • 正确的source参数

6.总结

java的注解机制并不算太难理解,但是重点是,我们日常中很难想到去应用他,一来是因为我们对其不够熟悉,二来是我们的业务,没有那么通用的逻辑.

注解机制被大量的使用在各种框架中,足以证明他是一种优秀的机制,值得我们去学习并努力的应用在自己的工作中.

7.参考链接

https://josh-persistence.iteye.com/blog/2226493
https://www.ibm.com/developerworks/cn/java/j-lo-java8annotation/index.html

完。









ChangeLog


2019-01-20 完成



以上皆为个人所思所得,如有错误欢迎评论区指正。

欢迎转载,烦请署名并保留原文链接。

联系邮箱:huyanshi2580@gmail.com

更多学习笔记见个人博客------>呼延十

原文链接:https://yq.aliyun.com/articles/688753
关注公众号

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。

持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。

转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。

文章评论

共有0条评论来说两句吧...

文章二维码

扫描即可查看该文章

点击排行

推荐阅读

最新文章