使用spring security时经常会使用注解@AuthenticationPrincipal获取用户信息。在想要注入用户信息的Controller接口上加上注解,即可方便的获取访问用户的信息。而且Controller还“非常的智能”,比如接口上写上参数HttpServletRequest request,就能自动注入http请求信息

需求

现在有一种需求(案例简化的需求),很多接口都需要查询用户的部门Department。每次可能都有类似如下硬编码

1@GetMapping("/")
2public String getSomeThing(@AuthenticationPrincipal Account account) {
3    Department department = departmentManager.findOne(account.getDepartmentId());
4    // doSomeThing with department ...
5}

这样也不是不可以,但是总感觉不够优雅,特别是有时业务不像案例这么简单一个查询就OK,可能有一大段逻辑要处理才查询出部门信息。

解决

spring自身有如此智能的参数注入机制,猜想这个优秀的框架肯定为我们留下了可扩展的方法,让我们可以实现类似的功能。经过查找资料,找到了自定义的办法

自定义注解

我们写一个自定义注解@GetDepartment,稍后为所有有该注解的Controller接口方法参数上注入部门信息

1@Documented
2@Target({ElementType.PARAMETER})
3@Retention(RetentionPolicy.RUNTIME)
4public @interface GetDepartment {
5}

自定义http方法参数解析器

实现自定义解析器实现HandlerMethodArgumentResolver。这里因为需要用到@Autowired注入一个部门查询的Bean,所以加了@Component注解。其实不需要注入时不加@Component注解也是可以的。推荐加注解交给Spring管理。

 1@Component
 2public class GetDepartmentHandlerMethodArgumentResolver implements HandlerMethodArgumentResolver {
 3    @Autowired
 4    private DepartmentManager departmentManager;
 5
 6    private static final Class<?> commandClass = Department.class;
 7
 8
 9    @Override
10    public boolean supportsParameter(MethodParameter parameter) {
11        // 过滤出符合条件的参数,这里指的是加了GetDepartment注解的参数。
12        // 其他不符合条件的参数不会使用该解析器处理
13        return parameter.hasParameterAnnotation(GetDepartment.class);
14    }
15
16
17    
18    @Override
19    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) {
20        // 该方法解析器处理逻辑
21        Class<?> klass = parameter.getParameterType();
22        // 定义部门信息必须用Department类接收,不允许用其他的class接收
23        if (!klass.isAssignableFrom(commandClass)) {
24            throw new RuntimeException("部门获取失败");
25        }
26        // 获取用户信息
27        CurrentUserDetails account = (CurrentUserDetails) mavContainer.getDefaultModel().get("currentAccount");
28        // 查询部门
29        Department department = departmentManager.findOne(account.getDepartmentId());
30        return department;
31    }
32}

需要重写两个方法:

  1. supportsParameter方法主要过滤出需要注入部门的参数,放行不满足条件的参数
  2. resolveArgument方法是真正做自己业务逻辑,比如这里获取部门信息,并返回给框架,框架会帮我们注入到方法上

resolveArgument方法自带4个参数,经过debug,发现这从这4个参数中,我们可以获取大部分想要的信息,去继续下面的业务逻辑。

  1. ModelAndViewContainer参数中带有用户的登录信息,即和使用@AuthenticationPrincipal注解时获取的用户信息一样。

  2. MethodParameter是本次http请求的接口方法信息。

  3. NativeWebRequest里有HttpRequest一些信息,比如可以从中获取cookie,获取方法参数等。

  4. WebDataBinderFactory暂时没用上

把自定义解析器添加到spring管理

仅仅把解析器生成Spring Bean是不够的,还需要把该对象添加到特定的解析器队列中。需要自定义一个类继承WebMvcConfigurerAdapter,重写addArgumentResolvers方法,把刚才的Bean添加进去队列。

 1@Configuration
 2public class CustomWebMvcConfigurerAdapter extends WebMvcConfigurerAdapter {
 3    @Autowired
 4    private GetDepartmentHandlerMethodArgumentResolver getDepartmentHandlerMethodArgumentResolver;
 5
 6    @Override
 7    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
 8        resolvers.add(getDepartmentHandlerMethodArgumentResolver);
 9    }
10}

使用

上面自定义两个类,一个注解后,配置已经完成。在使用处使用@GetDepartment Department department接收即可,这样部门信息就自动注入进来了

1@GetMapping("/")
2public String getSomeThing(@AuthenticationPrincipal Account account,@GetDepartment Department department) {
3    // doSomeThing with department ...
4}

踩坑

在配置添加解析器到Spring中,最开始是自定义类继承WebMvcConfigurationSupport而不是继承WebMvcConfigurerAdapter,其实同样可以做到自定义注入参数的功能。但是后来发现自定义实现WebMvcConfigurationSupport后,原本使用@AuthenticationPrincipal注解获取用户信息的地方,现在都无法获取了。

 1@Configuration
 2public class CustomWebMvcConfigurerAdapter extends WebMvcConfigurationSupport {
 3    @Autowired
 4    private GetDepartmentHandlerMethodArgumentResolver getDepartmentHandlerMethodArgumentResolver;
 5
 6    @Override
 7    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
 8        resolvers.add(getDepartmentHandlerMethodArgumentResolver);
 9    }
10}

经过查看源码发现Spring提供了DelegatingWebMvcConfiguration去实现WebMvcConfigurationSupport。我们称代理类

 1@Configuration
 2public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport {
 3
 4    private final WebMvcConfigurerComposite configurers = new WebMvcConfigurerComposite();
 5
 6
 7    @Autowired(required = false)
 8    public void setConfigurers(List<WebMvcConfigurer> configurers) {
 9        if (!CollectionUtils.isEmpty(configurers)) {
10            this.configurers.addWebMvcConfigurers(configurers);
11        }
12    }
13
14    @Override
15    protected void configurePathMatch(PathMatchConfigurer configurer) {
16        this.configurers.configurePathMatch(configurer);
17    }
18    // ...
19}

代理类中有个WebMvcConfigurerComposite对象,通过set注入mvc配置类List<WebMvcConfigurer>,并add进去。我们查看WebMvcConfigurerComposite类,下面称之为聚合类。

 1class WebMvcConfigurerComposite implements WebMvcConfigurer {
 2
 3    private final List<WebMvcConfigurer> delegates = new ArrayList<WebMvcConfigurer>();
 4
 5
 6    public void addWebMvcConfigurers(List<WebMvcConfigurer> configurers) {
 7        if (!CollectionUtils.isEmpty(configurers)) {
 8            this.delegates.addAll(configurers);
 9        }
10    }
11
12    @Override
13    public void configurePathMatch(PathMatchConfigurer configurer) {
14        for (WebMvcConfigurer delegate : this.delegates) {
15            delegate.configurePathMatch(configurer);
16        }
17    }
18	// ...
19}

我们发现对代理类的方法调用,都会转为对聚合类中相同方法的调用,而聚合类会把配置类集合List<WebMvcConfigurer>循环,一一对每个WebMvcConfigurer调用相同的方法。比如上面的configurePathMatch方法

现在找寻代理类的使用地方,找到了WebMvcAutoConfiguration,可以看出这个类是SpringMvc缺省的自动配置类,而这个类有个很重要的一行@ConditionalOnMissingBean(WebMvcConfigurationSupport.class)。意思只有不存在WebMvcConfigurationSupportbean实例的时候,才会初始化下面这些bean,包括spring mvc 缺省一个自动配置WebMvcAutoConfigurationAdapter,还有实现了刚才代理类的子类EnableWebMvcConfiguration和下面其他的一些配置。这些统统不生效,实现了WebMvcConfigurationSupport的bean只能存在一个

 1@Configuration
 2@ConditionalOnWebApplication
 3@ConditionalOnClass({ Servlet.class, DispatcherServlet.class,WebMvcConfigurerAdapter.class })
 4@ConditionalOnMissingBean(WebMvcConfigurationSupport.class)
 5@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)
 6@AutoConfigureAfter({ DispatcherServletAutoConfiguration.class,ValidationAutoConfiguration.class })
 7public class WebMvcAutoConfiguration {
 8
 9    @Configuration
10    @Import({ EnableWebMvcConfiguration.class, MvcValidatorRegistrar.class })
11    @EnableConfigurationProperties({ WebMvcProperties.class, ResourceProperties.class })
12    public static class WebMvcAutoConfigurationAdapter extends WebMvcConfigurerAdapter {
13        // ...
14    }
15
16    @Configuration
17    public static class EnableWebMvcConfiguration extends DelegatingWebMvcConfiguration	implements InitializingBean {
18        // ...
19    }
20    // ...
21}

总结

WebMvcConfigurationSupport注释如下

1/**
2 * This is the main class providing the configuration behind the MVC Java config...
3 */

WebMvcConfigurationSupport是spring mvc主要配置类,所以一开始我们自定义实现WebMvcConfigurationSupport时,原先的spring mvc一些缺省配置都不会自动配置,也不会配置代理类,自然不会有 调用代理类->调用聚合类->聚合类调用每一个配置类这样一个调用链条,相当于所有的WebMvcConfigurer实现类都不生效。相关的mvc一些缺省配置自然不生效。

参考

https://www.jianshu.com/p/c5c1503f5367