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

Posted1 by 呼延十 on January 20, 2019 Hot:

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

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