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

代码传送门

需求

一个 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;
}

以上~

  • 8
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 6
    评论
Spring AOP(面向切面编程)是Spring框架中的一个组件,它允许您以一种声明性的方式来处理横切关注点(如事务管理,日志记录等)。 通过使用AOP,可以将这些关注点从应用程序的主体中分离出来,从而实现代码的复用和灵活性。 在使用Spring框架中实现多数据源的切换时,可以使用自定义注解的形式来实现。首先,首先在应用程序的主体中定义两个数据源。 然后,可以定义一个自定义注解,用于标识哪些方法应该使用哪个数据源。例如,使用“@Primary”注解标记主要数据源,使用“@Secondary”注解标记辅助数据源。 然后,在Spring配置中定义一个AOP切面,该切面使用上述自定义注解来切换数据源。下面是这种方法的一个示例: ```java @Aspect @Component public class DataSourceAspect { @Around("@annotation(Primary)") public Object primaryDataSource(ProceedingJoinPoint proceedingJoinPoint) throws Throwable { // 切换到主要数据源 DynamicDataSourceContextHolder.setDataSource(DynamicDataSourceContextHolder.DATA_SOURCE_PRIMARY); try { return proceedingJoinPoint.proceed(); } finally { // 切换回默认数据源 DynamicDataSourceContextHolder.clearDataSource(); } } @Around("@annotation(Secondary)") public Object secondaryDataSource(ProceedingJoinPoint proceedingJoinPoint) throws Throwable { // 切换到辅助数据源 DynamicDataSourceContextHolder.setDataSource(DynamicDataSourceContextHolder.DATA_SOURCE_SECONDARY); try { return proceedingJoinPoint.proceed(); } finally { // 切换回默认数据源 DynamicDataSourceContextHolder.clearDataSource(); } } } ``` 在上面的代码中,我们可以看到“@Around”注解被用于定义一个环绕通知,该通知基于使用“@Primary”或“@Secondary”注解的方法进行拦截。 在方法执行之前,我们使用“DynamicDataSourceContextHolder”来将数据源设置为主要或辅助数据源。 在方法执行完成之后,我们将数据源切换回默认数据源。 最后,我们可以将“@Primary”和“@Secondary”注解带到相应的方法上,以切换不同的数据源,例如: ```java @Service public class UserServiceImpl implements UserService { @Autowired private UserDao userDao; @Override @Primary public User getUserById(long id) { return userDao.getUserById(id); } @Override @Secondary public List<User> getAllUsers() { return userDao.getAllUsers(); } } ``` 在上面的代码中,我们可以看到“@Primary”注解被用于getUserById()方法,表示这个方法应该从主要数据源中读取数据。相反,getAllUsers()方法被标记为“@Secondary”注解,表示这个方法应该从辅助数据源中读取数据。 通过这种方式,我们可以很容易地切换应用程序中的不同数据源,并且代码的重复率很低。这种方法适用于需要在应用程序的不同部分使用不同数据源的多租户应用程序。
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值