【小家Spring】Spring注解驱动开发---Servlet 3.0整合Spring MVC(不使用web.xml部署描述符,使用ServletContainerInitializer)(中)

简介: 【小家Spring】Spring注解驱动开发---Servlet 3.0整合Spring MVC(不使用web.xml部署描述符,使用ServletContainerInitializer)(中)

整合Spring MVC



整合Spring MVC是重中之重。前面已经说过ServletContainerInitializer了,相信大家能够想到Spring是怎么做的了吧?直接参照Spring官方文档先看看:


image.png

链接如下:https://docs.spring.io/spring/docs/5.1.5.RELEASE/spring-framework-reference/web.html#mvc-servlet


我们看看Spring-web包jar包内:


image.png


很显然的发现,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);
    }
  }
}



步骤分析:


  1. spring的应用一启动会加载感兴趣的WebApplicationInitializer接口的下的所有组件;
  2. 为WebApplicationInitializer组件创建对象(组件不是接口,不是抽象类

image.png

image.png


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容器推荐使用父子容器的概念:

image.png从上面分析得知,我们要使用注解驱动的话。只需要我们自己实现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是正常的:


image.png

但是在Service层注入,启动的时候就会报错了


image.png


报错如下:

image.png这里面说个结论:

1、父子容器的关系就行内部类的关系一样。子容器能得到父容器的Bean,但是父容器得不到子容器的Bean

2、父子容器中,属性值都不是互通的。@Value注入的时候需要注意一下子~

定制Spring MVC


之前我们使用xml文件的时候,我们可以配置Spring MVC等相关选项。

比如视图解析器、视图映射、静态资源映射、拦截器。。。


首先:在配置文件里加上注解@EnableWebMvc:开启SpringMVC定制配置功能;

其次: 实现WebMvcConfigurer接口。通过这个接口我们可以发现,里面有很多方法,但大多数情况下我们并不需要配置这么多项,因此Spring MVC也考虑到了这一点,提供给我们一个WebMvcConfigurerAdapter来extends就行,Adapter都是空实现~,这样我们需要配置什么,复写对应方法就行

image.png


从上可以看出,如果你的项目是构建在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,所以使用起来请自行举一反三

相关文章
|
3天前
|
Java Spring 容器
Spring注解开发,bean的作用范围及生命周期、Spring注解开发依赖注入
Spring注解开发,bean的作用范围及生命周期、Spring注解开发依赖注入
12 1
Spring注解开发,bean的作用范围及生命周期、Spring注解开发依赖注入
|
4天前
|
缓存 NoSQL Java
Spring Cache之本地缓存注解@Cacheable,@CachePut,@CacheEvict使用
SpringCache不支持灵活的缓存时间和集群,适合数据量小的单机服务或对一致性要求不高的场景。`@EnableCaching`启用缓存。`@Cacheable`用于缓存方法返回值,`value`指定缓存名称,`key`定义缓存键,可按SpEL编写,`unless`决定是否不缓存空值。当在类上使用时,类内所有方法都支持缓存。`@CachePut`每次执行方法后都会更新缓存,而`@CacheEvict`用于清除缓存,支持按键清除或全部清除。Spring Cache结合Redis可支持集群环境。
32 5
|
5天前
|
Java Python Spring
小唐开始学 Spring Boot——(2)Spring Boot核心配置与注解
小唐开始学 Spring Boot——(2)Spring Boot核心配置与注解
|
11天前
|
Java
Springboot 使用自定义注解结合AOP方式校验接口参数
Springboot 使用自定义注解结合AOP方式校验接口参数
Springboot 使用自定义注解结合AOP方式校验接口参数
|
11天前
|
前端开发 JavaScript 开发者
深入理解MVC和MVVM:构建现代Web应用的利器
深入理解MVC和MVVM:构建现代Web应用的利器
|
12天前
|
存储 缓存 Java
【JavaEE】Spring中注解的方式去获取Bean对象
【JavaEE】Spring中注解的方式去获取Bean对象
3 0
|
13天前
|
Java 应用服务中间件 Maven
SpringBoot 项目瘦身指南
SpringBoot 项目瘦身指南
65 0
|
13天前
|
缓存 安全 Java
Spring Boot 面试题及答案整理,最新面试题
Spring Boot 面试题及答案整理,最新面试题
144 0
|
13天前
|
存储 JSON Java
SpringBoot集成AOP实现每个接口请求参数和返回参数并记录每个接口请求时间
SpringBoot集成AOP实现每个接口请求参数和返回参数并记录每个接口请求时间
50 2
|
13天前
|
前端开发 搜索推荐 Java
【Spring底层原理高级进阶】基于Spring Boot和Spring WebFlux的实时推荐系统的核心:响应式编程与 WebFlux 的颠覆性变革
【Spring底层原理高级进阶】基于Spring Boot和Spring WebFlux的实时推荐系统的核心:响应式编程与 WebFlux 的颠覆性变革