在接口的开发中,我们有时会想让 某个接口只可以被特定的人(来源)请求 ,那么就需要在服务端对请求参数做校验.
这种情况我们可以使用 interceptor
来统一进行参数校验,但是如果很多个接口,有不同的的设定值,我们总不能写很多个 interceptor
,然后按照path逐一添加吧?
面对这种情况,我们可以选择自定义一个注解,由注解来告诉我们,这个接口允许的访问者是谁.
注:在本文的示例中,仅实现了对某一个字段的校验,安全性并不高,实际项目中,可以采用多字段加密的方式,来保证安全性,原理和文中是一样的.
Java Annotation是JDK5.0引入的一种注释机制。
Annotation是代码里的特殊标记,这些标记可以在编译、类加载、运行时被读取,并执行相应的处理。
通过使用Annotation,程序员可以在不改变原有逻辑的情况下,在源文件中嵌入一些补充信息。
Annotation可以像修饰符一样被使用,可以用于package、class、interface、constructor、method、member variable(成员变量)、parameter、local variable(局部变量)、annotation(注解),jdk 1.8之后,只要出现类型(包括类、接口、注解、枚举)的地方都可以使用注解了。
我们可以使用JDK以及其它框架提供的Annotation,也可以自定义Annotation。
元注解是什么呢?在我的理解里,元注解是java官方提供的,用于修饰其他注解的几个属性.
因为开放了自定义注解,所以所有的注解必须有章可循,他们的一些属性必须要被定义.比如:这个注解用在什么地方?类上还是方法上还是字段上?这个注解的生命周期是什么?是保留在源码里供人阅读就好,还是会生成在class文件中,对程序产生实际的作用?这些都需要被提前定义好,因此就有了:
这四个元注解@Target、@Retation、@Inherited、@Documented.
接下来对四个元注解逐一说明
用于描述注解的使用范围(即:被描述的注解可以用在什么地方)
他的取值范围JDK定义了枚举类 ElementType
,他的值共有以下几种:
注:在JDK1.8,新加了两种类型, 8. TYPE_PARAMETER:表示这个 Annotation 可以用在 Type 的声明式前, 9. TYPE_USE 表示这个 Annotation 可以用在所有使用 Type 的地方
表示需要在什么级别保存该注释信息,用于描述注解的生命周期(即:被描述的注解在什么范围内有效)
他的取值范围来自于枚举类 RetentionPolicy
,取值共以下几种:
@Documented用于描述其它类型的annotation应该被作为被标注的程序成员的公共API,因此可以被例如javadoc此类的工具文档化。Documented是一个标记注解,没有成员。
@Inherited 元注解是一个标记注解,@Inherited阐述了某个被标注的类型是被继承的。如果一个使用了@Inherited修饰的annotation类型被用于一个class,则这个annotation将被用于该class的子类。
常用的第三方框架实现了非常多的注解,比如 Mybatis
的 Param
, Spring
的 Component
, Service
, fastjson
的 JSONfield
等等.
具体的实现方法这里不多解释了,有兴趣的朋友可以去看一下 fastjson
的源码,该项目相比 spring
等框架,简单一些也更容易理解.
看到这种注解或简单或复杂的功能之后,我们是否也可以自己来动手实现一个呢?
首先我们来定义注解:
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列表.
其实一开始我在这里纠结了许久,因为我不能理解 一个注解应该在哪里以什么方式调用
.
按照我的思路,每个注解应该有一个字段(或者类似的东西),来指示应该去 哪里
调用这个注解的真正使用.
后来经过细细思考,发现这是不现实的,因为注解的作用完全没有规律可言,你可以实现任何你想要的功能,返回值可以使任意值,里面的逻辑也是任意的.
那么就意味着,你需要为你的注解负责,否则他没有任何作用.也就是说,你需要为自己的注解编写 注解解析器
,来定义 什么时候用到这个注解,用它干什么
?
@纯个人观点,慎看
经过在网上冲浪,我发现注解解析器的主要 形式
有三种:
这种方式比较方便,可以直接拦截所有的请求,检查该请求进入的类及方法上有没有特定的注解,如果有怎么怎么操作一波.
但是局限性比较大,我们又不是只在controller里面会用到注解.
这种方式也比较方便,扩展性比较好,当你需要在新的地方用到该注解,新增一个切点就好.
这种是大部分人喜闻乐见的(其实最喜闻乐见的是每次用到就写一遍呗),但是如果不经常重构一下代码,你会发现导出充满了你对某一个注解的使用代码,那就很崩溃了,你需要尽量将其封装一下,放在统一的工具类,每次需要的时候调用即可.
由于我们这次的需求是拦截不合法的请求,所以当然是第一种方式比较靠谱,因此我们写了一个拦截器:
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是否在允许的列表中.
在这个拦截器中,我们定义了:
在我们配置的,使用这个拦截器的时候,进入controller层的某一个方法时.
拿传入的 source
参数和这个注解的属性 sources
列表一一匹配,有匹配上的则允许请求,无匹配值则返回错误信息.
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"); } } 复制代码
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 "哈哈哈"; } } 复制代码
好,编码全部完成了.
启动项目,看一下结果.
source
参数 source
参数 source
参数 java的注解机制并不算太难理解,但是重点是,我们日常中很难想到去应用他,一来是因为我们对其不够熟悉,二来是我们的 业务
,没有那么通用的逻辑.
注解机制被大量的使用在各种框架中,足以证明他是一种优秀的机制,值得我们去学习并努力的应用在自己的工作中.
2019-01-20 完成