需求
一个 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;
}
以上~