Spring AOP 自定义注解检查请求头(示例)

2023-10-30

代码传送门

需求

一个 Controller 可以处理 HTTP 请求

@RestController
public class DemoController {

    @GetMapping("/hello")
    public String hello(@RequestParam String name) {
        return "Hello, " + name;
    }
}

请求

$ curl "localhost:8080/hello?name=Mark"
Hello, Mark

现在要求 请求中必需包含特定的请求头,否则需要抛出异常。

自定义注解

自定义注解

@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface HeaderChecker {

    /**
     * Without default value means this argument is required
     *
     * @return Header names
     */
    String[] headerNames();
}

备注:注解参数不设置 default 默认值时,这个参数在使用注解时,便是必填的,由编译时保证。

定义 Aspect

@Slf4j
@Aspect
@Component
public class HeaderCheckerAspect {

    @Before("@annotation(headerChecker)")
    public void doBefore(HeaderChecker headerChecker) {
        HttpServletRequest request = currentRequest();
        if (Objects.isNull(request)) {
            log.info("without request, skip");
            return;
        }

        String[] headerNames = headerChecker.headerNames();
        for (String headerName : headerNames) {
            String value = request.getHeader(headerName);
            if (StringUtils.hasText(value)) {
                continue;
            }
            log.error("Header {} is required", headerName);
            throw new IllegalArgumentException("Header " + headerName + " is required");
        }

        log.info("checked");
    }

    /**
     * Return request current thread bound or null if none bound.
     *
     * @return Current request or null
     */
    private HttpServletRequest currentRequest() {
        // Use getRequestAttributes because of its return null if none bound
        ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        return Optional.ofNullable(servletRequestAttributes).map(ServletRequestAttributes::getRequest).orElse(null);
    }
}

通过 RequestContextHolder 获取当前线程对应的 Servlet Request。

检查请求的 header 中是否包含必需的 header name,否则抛出异常。

为 Controller 添加注解

@RestController
public class DemoController {

    @HeaderChecker(headerNames = {"Ni-Hao", "Do"})
    @GetMapping("/hello")
    public String hello(@RequestParam String name) {
        return "Hello, " + name;
    }
}

再次请求,可以发现,只有携带指定请求头才能请求成功。

$ curl "localhost:8080/hello?name=Mark"                                               
{"timestamp":"2018-11-14T07:53:08.426+0000","status":500,"error":"Internal Server Error","message":"Header Ni-Hao is required","path":"/hello"}

$ curl "localhost:8080/hello?name=Mark" --header "Ni-Hao: World"                      
{"timestamp":"2018-11-14T07:53:29.106+0000","status":500,"error":"Internal Server Error","message":"Header Do is required","path":"/hello"}

$ curl "localhost:8080/hello?name=Mark" --header "Ni-Hao: World" --header "Do: World" 
Hello, Mark

自定义类注解

上面的自定义注解,每个方法都要添加注解才能起到作用,貌似不太方便诶。

自定义类注解

@Documented
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface HeaderChecker {

    /**
     * Without default value means this argument is required
     *
     * @return Header names
     */
    String[] headerNames();
}
  • ElementType.METHOD 注解作用目标:方法
  • ElementType.TYPE 注解作用目标:接口、类、枚举、注解

但是,仅仅这样之后,虽然注解可以加在类上了,像下面这样,但是,不太对诶~

@HeaderChecker(headerNames = {"Hello"})
@RestController
public class DemoController {

    @HeaderChecker(headerNames = {"Ni-Hao", "Do"})
    @GetMapping("/hello")
    public String hello(@RequestParam String name) {
        return "Hello, " + name;
    }

    @GetMapping("/do")
    public String doSomething(@RequestParam String thing) {
        return "Do, " + thing;
    }
}

启动并请求会发现,注解无法真正的生效,没有 Hello 请求头依然可以请求成功,显然存在错误

$ curl "localhost:8080/do?thing=Noting"
Do, Noting

这涉及到 Aspect 类中,切面方法定义时 @annotation@within 的区分,先给出可以实现需求的 Aspect 类,如下,

@Slf4j
@Aspect
@Component
public class HeaderCheckerAspect {
    @Before("@within(headerChecker)")
    public void doBeforeForClass(HeaderChecker headerChecker) {
        doBefore(headerChecker);
    }

    @Before("@annotation(headerChecker)")
    public void doBeforeForMethod(HeaderChecker headerChecker) {
        doBefore(headerChecker);
    }

    private void doBefore(HeaderChecker headerChecker) {
        HttpServletRequest request = currentRequest();
        if (Objects.isNull(request)) {
            log.info("without request, skip");
            return;
        }

        String[] headerNames = headerChecker.headerNames();
        for (String headerName : headerNames) {
            String value = request.getHeader(headerName);
            if (StringUtils.hasText(value)) {
                continue;
            }
            log.error("Header {} is required", headerName);
            throw new IllegalArgumentException("Header " + headerName + " is required");
        }

        log.info("checked");
    }

    /**
     * Return request current thread bound or null if none bound.
     *
     * @return Current request or null
     */
    private HttpServletRequest currentRequest() {
        // Use getRequestAttributes because of its return null if none bound
        ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        return Optional.ofNullable(servletRequestAttributes).map(ServletRequestAttributes::getRequest).orElse(null);
    }
}

执行请求

$ curl "localhost:8080/do?thing=noting"                                                  
{"timestamp":"2018-11-14T09:39:11.842+0000","status":500,"error":"Internal Server Error","message":"Header Hello is required","path":"/do"}

$ curl "localhost:8080/do?thing=noting" --header "Hello: ww"
Do, noting

可以发现,类注解生效了,必须要有 Hello 请求头。继续测试,

$ curl "localhost:8080/hello?name=Mark"  --header "Hello: ww"
{"timestamp":"2018-11-14T09:38:41.670+0000","status":500,"error":"Internal Server Error","message":"Header Ni-Hao is required","path":"/hello"}

$ curl "localhost:8080/hello?name=Mark" --header "Ni-Hao: World" --header "Do: World"    
{"timestamp":"2018-11-14T09:38:47.028+0000","status":500,"error":"Internal Server Error","message":"Header Hello is required","path":"/hello"}

$ curl "localhost:8080/hello?name=Mark" --header "Ni-Hao: World" --header "Do: World" --header "Hello: ww"
Hello, Mark

可以发现,类注解和方法注解同时生效,必需同时具有 Hello, Ni-Hao, Do 三个请求头。


而实现类注解,自然归功于 @within 这个指示符啦~(可参考 AspectJ语法详解:execution,within,this,@Aspect

  • @within:用于匹配,持有指定注解的,类型内的“所有方法”;
  • @annotation:用于匹配,持有指定注解的,方法;

当然 @within 所指代的 “所有方法” 是指被 Spring Bean 所调用的方法,而不是真的所有方法。

如下面栗子中的 doSomething 被 Controller 的 Bean 调用时,会匹配切面方法。

然而,如果 doSomething 仅仅是被普通调用,或者像 realDoSomething 这样被 doSomething 调用,是不会匹配切面方法的。

原因 嘛,自然是与 AOP 相关的一个概念:动态代理

个人理解:上面提到的那个 Bean 实际是动态代理实例,所以被 Bean 实例调用才会匹配;不过不确定,就不展开描述了,藏~

@GetMapping("/do")
public String doSomething(@RequestParam String thing) {
    return realDoSomething(thing);
}

private String realDoSomething(String thing) {
    return "Do, " + thing;
}

以上~

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Spring AOP 自定义注解检查请求头(示例) 的相关文章

随机推荐