整合Spring MVC
整合Spring MVC是重中之重。前面已经说过ServletContainerInitializer
了,相信大家能够想到Spring是怎么做的了吧?直接参照Spring官方文档先看看:
链接如下:https://docs.spring.io/spring/docs/5.1.5.RELEASE/spring-framework-reference/web.html#mvc-servlet
我们看看Spring-web包jar包内:
很显然的发现,Spring MVC也是通过这种方式和Servlet容器进行整合的。web容器在启动的时候,就会自动去加载org.springframework.web.SpringServletContainerInitializer这个类。源码如下(它关心的是WebApplicationInitializer的子类们):
@HandlesTypes(WebApplicationInitializer.class) public class SpringServletContainerInitializer implements ServletContainerInitializer { @Override public void onStartup(@Nullable Set<Class<?>> webAppInitializerClasses, ServletContext servletContext) throws ServletException { List<WebApplicationInitializer> initializers = new LinkedList<>(); if (webAppInitializerClasses != null) { for (Class<?> waiClass : webAppInitializerClasses) { // Be defensive: Some servlet containers provide us with invalid classes, // no matter what @HandlesTypes says... if (!waiClass.isInterface() && !Modifier.isAbstract(waiClass.getModifiers()) && WebApplicationInitializer.class.isAssignableFrom(waiClass)) { try { initializers.add((WebApplicationInitializer) ReflectionUtils.accessibleConstructor(waiClass).newInstance()); } catch (Throwable ex) { throw new ServletException("Failed to instantiate WebApplicationInitializer class", ex); } } } } if (initializers.isEmpty()) { servletContext.log("No Spring WebApplicationInitializer types detected on classpath"); return; } servletContext.log(initializers.size() + " Spring WebApplicationInitializers detected on classpath"); AnnotationAwareOrderComparator.sort(initializers); for (WebApplicationInitializer initializer : initializers) { initializer.onStartup(servletContext); } } }
步骤分析:
- spring的应用一启动会加载感兴趣的WebApplicationInitializer接口的下的所有组件;
- 为WebApplicationInitializer组件创建对象(组件不是接口,不是抽象类)
1)、AbstractContextLoaderInitializer:创建根容器;createRootApplicationContext();
2)、AbstractDispatcherServletInitializer:
创建一个web的ioc容器;createServletApplicationContext();
创建了DispatcherServlet;createDispatcherServlet();
将创建的DispatcherServlet添加到ServletContext中;
-----------抽象方法:getServletMappings();
3)、AbstractAnnotationConfigDispatcherServletInitializer:注解方式配置的DispatcherServlet初始化器(本文重点)
创建根容器:createRootApplicationContext()
------------getRootConfigClasses();传入一个配置类(用户自定义)
创建web的ioc容器: createServletApplicationContext();
------------获取配置类;getServletConfigClasses();
说明:虽然父类只有两个abstract抽象方法要求子类必须实现。但是父类的设计都是可以扩展的,若你想定制化自己的需求,都是可以通过复写父类的protected的方式扩展的
比如:你想定制化自己的DispatcherServlet(父类默认值是单纯的new一下),那么你就可以通过复写createDispatcherServlet()去定制~
Spring容器推荐使用父子容器的概念:
从上面分析得知,我们要使用注解驱动的话。只需要我们自己实现AbstractAnnotationConfigDispatcherServletInitializer这个抽象类就行了,这样web容器启动的时候就能处理我们实现的这个类的内容。示例如下(采用父子容器):
父子容器的配置类:
// 备注:此处@ControllerAdvice、RestControllerAdvice 这个注解不要忘了,属于Controller层处理全局异常的,应该交给web去扫描 @ComponentScan(value = "com.fsx", excludeFilters = { @Filter(type = FilterType.ANNOTATION, classes = {Controller.class, ControllerAdvice.class, RestControllerAdvice.class}) }) @Configuration //最好标注上,本人亲测若不标准,可能扫描不生效 public class RootConfig { } // 此处记得排除掉@Controller和@ControllerAdvice、@RestControllerAdvice @ComponentScan(value = "com.fsx", useDefaultFilters = false, includeFilters = {@Filter(type = FilterType.ANNOTATION, classes = {Controller.class, ControllerAdvice.class, RestControllerAdvice.class})} ) @Configuration //最好标注上,本人亲测若不标准,可能扫描不生效 public class AppConfig { }
@RestControllerAdvice是Spring4.3后提供的注解。@ControllerAdvice是Spring3.2提供的
它俩的区别就像@Controller和@RestController的区别。(也就是说@RestControllerAdvice``可以省略@ResponseBody`不用写了~~~)
useDefaultFilters默认值为true,表示默认情况下@Component、@Repository、@Service、@Controller都会扫描
useDefaultFilters=false加上includeFilters我们就可以只扫描指定的组件了,比如Spring MVC的web子容器只扫描Controller组件
excludeFilters的时候,就不需要去设置useDefaultFilters=false,这样子我们直接排除掉即可哟~
特别注意:useDefaultFilters的正确使用,不要造成重复扫描。否则很有可能造成事务不生效,并且你还非常不好定位这个错误。
然后我们自己来实现AbstractAnnotationConfigDispatcherServletInitializer一个初始化实体类:
/** * 自己实现 基于注解驱动的ServletInitializer来初始化DispatcherServlet */ public class MyWebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer { /** * 根容器的配置类;(Spring的配置文件) 父容器; */ @Override protected Class<?>[] getRootConfigClasses() { return new Class<?>[]{RootConfig.class}; } /** * web容器的配置类(SpringMVC配置文件) 子容器; */ @Override protected Class<?>[] getServletConfigClasses() { return new Class<?>[]{AppConfig.class}; } //获取DispatcherServlet的映射信息 // 注意: /:拦截所有请求(包括静态资源(xx.js,xx.png)),但是不包括*.jsp; // /*:拦截所有请求;连*.jsp页面都拦截;jsp页面是tomcat的jsp引擎解析的; @Override protected String[] getServletMappings() { return new String[]{"/"}; } // 若你想定制化父类的一些默认行为 这里都是可以复写父类的protected方法的~~~~ // Spring MVC也推荐你这么干~ @Override protected FrameworkServlet createDispatcherServlet(WebApplicationContext servletAppContext) { DispatcherServlet dispatcherServlet = (DispatcherServlet) super.createDispatcherServlet(servletAppContext); // dispatcherServlet.setDetectAllHandlerAdapters(false); return dispatcherServlet; } }
我们写个测试类试试:
@Controller public class HelloController { @Autowired HelloService helloService; @ResponseBody @RequestMapping("/hello") public String hello() { System.out.println(helloService); //com.fsx.service.HelloServiceImpl@512663b0 return "hello..."; } }
这样我们就可以正常访问controller的请求了。大功告成~
web容器中的Spring的应用 一启动就会 加载感兴趣的WebApplicationInitializer接口的下的所有组件,并且为WebApplicationInitializer组件创建对象(组件不是接口,不是抽象类)。
特别注意的是:
按照上面的配置,我偶然的发现了,RootConfig仍然还是去扫描了我的controller,导致我的controller被扫描了两次,怎么回事呢???
找了好久,终于找到原因了,并不是@ComponentScan或者excludeFilters的问题,而是因为咱们在执行RootConfig的时候,虽然不去扫描Controller注解了,但是它会扫描AppConfig.java这个配置类,从而间接的又去扫描了@Controller了,因此最正确的做法应该如下:
@ComponentScan(value = "com.fsx", excludeFilters = { @Filter(type = FilterType.ANNOTATION, classes = {Controller.class}), //排除掉web容器的配置文件,否则会重复扫描 @Filter(type = FilterType.ASSIGNABLE_TYPE, classes = {AppConfig.class}) }) @Configuration public class RootConfig { }
这样子,我们的Controller就只会被扫描一次了,容器也就非常的干净了,强烈建议这么干。
当然,如果你还是当初xml的方式来做的,分别是两个配置文件下,只要不自己import之类,就不会出现此种问题~
容器完全隔离后的好处是非常明显的,比如我们的web组件,就放在AppConig里,其它的放在Root里,不要什么都往RootConfig里面塞,比如如下:
//web子容器里注册一个Child public class AppConfig { @Bean public Child child() { return new Child(); } } //父容器里注册一个Parent public class RootConfig { @Bean public Parent parent() { return new Parent(); } }
然后我们会发现在Controller层注入这两个Bean是正常的:
但是在Service层注入,启动的时候就会报错了
报错如下:
这里面说个结论:
1、父子容器的关系就行内部类的关系一样。子容器能得到父容器的Bean,但是父容器得不到子容器的Bean
2、父子容器中,属性值都不是互通的。@Value注入的时候需要注意一下子~
定制Spring MVC
之前我们使用xml文件的时候,我们可以配置Spring MVC等相关选项。
比如视图解析器、视图映射、静态资源映射、拦截器。。。
首先:在配置文件里加上注解@EnableWebMvc:开启SpringMVC定制配置功能;
其次: 实现WebMvcConfigurer接口。通过这个接口我们可以发现,里面有很多方法,但大多数情况下我们并不需要配置这么多项,因此Spring MVC也考虑到了这一点,提供给我们一个WebMvcConfigurerAdapter来extends就行,Adapter都是空实现~,这样我们需要配置什么,复写对应方法就行
从上可以看出,如果你的项目是构建在Spring5.0(基于java8)以上的,直接实现接口即可。不用再继承此Adapter了~
@EnableWebMvc @Configuration //一定要说明这个文件是个配置文件 public class WebMvcConfig implements WebMvcConfigurer { //视图解析器 @Override public void configureViewResolvers(ViewResolverRegistry registry) { //默认所有的页面都从 /WEB-INF/ xxx .jsp //registry.jsp(); registry.jsp("/WEB-INF/views/", ".jsp"); } // 开启静态资源的请求转发到默认servlet上,不配置页面报错404,(默认servlet不是DispatcherServlet!理解的) @Override public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) { configurer.enable(); } //自定义添加拦截器=========这个比较常用 @Override public void addInterceptors(InterceptorRegistry registry) { //registry.addInterceptor(new MyFirstInterceptor()).addPathPatterns("/**"); } }
这样我们就可以通过此配置文件,个性化定制我们的Spring MVC了。
Spring MVC中WebMvcConfigurerAdapter、WebMvcConfigurationSupport与WebMvcConfigurer
最后需要多一句嘴,我们还能看到还有一个类:WebMvcConfigurationSupport。小伙伴们查看很多文章,但此处我只推荐一个老铁的文章,说到了点上:WebMvcConfigurationSupport与WebMvcConfigurer的关系
结论可以摆在此处:最佳实践还是继承WebMvcConfigurerAdapter(或直接实现接口WebMvcConfigurer),只不过要多加一个@EnableWebMvc注解而已。备注:若是SpringBoot环境,请不要加@EnableWebMvc注解,因为springboot已经实例化了WebMvcConfigurationSupport,如果添加了该注解,默认的WebMvcConfigurationSupport配置类是不会生效的
HandlerInterceptor与WebRequestInterceptor的异同
WebRequestInterceptor间接实现了HandlerInterceptor,只是他们之间使用WebRequestHandlerInterceptorAdapter适配器类联系。
这两个Spring MVC的拦截器接口比较就比较简单了。直接给结论吧:
两个接口都可用于Contrller层请求拦截,接口中定义的方法作用也是一样的。
1.WebRequestInterceptor的入参WebRequest是包装了HttpServletRequest 和
2.HttpServletResponse的,通过WebRequest获取Request中的信息更简便直接
3.WebRequestInterceptor的preHandle是没有返回值的,说明该方法中的逻辑并不影响后续的方法执行,所以这个接口实现就是为了获取Request中的信息,没有阻止请求的作用
4.HandlerInterceptor的功能更强大也更基础,可以在preHandle方法中就直接拒绝请求进入controller方法。
最佳实践:HandlerInterceptor能够实现所有WebRequestInterceptor做的事,更偏底层些。因此建议使用HandlerInterceptor
最后需要注意一点的是,关于自定义视图解析的自定义配置。此处还有一种方法是直接向容器里面注册Bean即可,如下:
//自定义一个视图解析器 @Bean public ViewResolver viewResolver() { InternalResourceViewResolver resolver = new InternalResourceViewResolver(); resolver.setPrefix("WEB-INF/views/"); resolver.setSuffix(".html"); resolver.setExposeContextBeansAsAttributes(true); return resolver; }
关于其中缘由原理。接下来相关文章讲解到Spring MVC深层原理剖析的时候,会精讲这一块,有兴趣的小伙伴可以持续关注
由于Spring Boot环境下并不建议启用
@EnableWebMvc
,所以使用起来请自行举一反三