Java配置Spring MVC

前言

在 Servlet 3.0 标准之前,配置Spring MVC 要在 web.xml 中配置 前端控制器DispatcherServlet 、Web容器启动监听器ContextLoaderListener 。配置的东西倒是不多,但是由于 XML 文件的特性,即使是简单的配置上述两项也会有一大堆的 XML 节点要写,例如下面这样

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
         version="3.1">

    <!--配置Spring IOC容器的启动监听器-->
    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>classpath:spring/springApplication.xml</param-value>
    </context-param>
    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>

    <!--配置Spring MVC的前端控制器-->
    <servlet>
        <servlet-name>SpringMVC</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>SpringMVC</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>

</web-app>

本来记住几个类名还不太难,可是要记住这么多XML节点,就有点费劲了。

Servlet 3.0 (及以上版本)

自从 Servlet 3.0 问世之后,我们可以通过 Java 的方式配置 Servlet 容器了,摆脱了繁琐的 XML 文件,瞬间起飞~~ 芜湖!!!

使用 Java 配置的原理

所有实现了 Servlet3.0 的 Web 容器,都会在 jar/war 包下面搜索一个文件,这个文件的名字必须叫 javax.servlet.ServletContainerInitializer ,而且必须放在 jar/war 包的类路径下面的 META-INF/services 文件夹里面。这个文件的内容也非常简单,是某个实现了 javax.servlet.ServletContainerInitializer 接口的实现类的全限定类名。
在这里插入图片描述
(PS : 在 IDEA 中,蓝色的文件夹就代表项目的类路径。)
现在看一下这个类的实现

@HandlesTypes(Color.class)
public class MyServletContainerInit implements ServletContainerInitializer {

    /**
     * Servlet 容器启动时调用该方法
     * @param set 上面的 @HandlersTypes 注解中 classes 方法指明的接口/抽象类的所有子类
     * @param servletContext Application 作用域,可以用来注册 Servlet 、Listener、Filter
     * @throws ServletException
     */
    @Override
    public void onStartup(Set<Class<?>> set, ServletContext servletContext) throws ServletException {
        //  注册 servlet
        ServletRegistration.Dynamic userServlet = servletContext.addServlet("userServlet", UserServlet.class);
        userServlet.addMapping("/users");
        // 注册 filter
        FilterRegistration.Dynamic characterFilter = servletContext.addFilter("characterFilter", MyFilter.class);
        // 第一个参数是设置过滤器拦截哪种请求
        characterFilter.addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST),true, "/*");
        // 添加 Listener
        servletContext.addListener(MyListener.class);
    }
}

可以看到,onStartup 方法中所做的事情,和 web.xml 文件中做的事情是一样的,但是简化了很多。是不是很方便!而且要配什么,都是通过调用方法的方式进行配置,而不是像 XML 中一样要记住那些标签的名字。
这里要特别的注意一下 @HandlesTypes 注解,这个注解就是后面要说的,Spring MVC 基于 Java 配置的原理。

使用 Java 配置 Spring MVC

讲了 Servlet 3.0 之后,我们再来理解基于 Java 配置的 Spring MVC,就会轻车熟路了。因为说到底,Spring MVC的核心:前端控制器 DispatcherServlet 只是一个普通的 Servlet ,它也需要注入到 Web 容器中才能发挥 Spring MVC 的前端控制器功能。你刀法再好,我不给你刀,你也练不出来。

有了 Servlet 3.0 的基础,我们知道了所有实现了 Servlet 3.0 标准的 Web 容器在启动的时候都会去项目类路径下的 META-INF/services 下面查找 javax.servlet.ServletContainerInitializer 文件,那我们就来看看 Spring MVC 的 jar 包下面有没有这个文件。
在这里插入图片描述
果然被我们找到了,只不过这里的实现类,是一个名为 SpringServletContainerInitializer 的类,点进去看看里面写了啥

/**
 * 注意这里,非常重要,这就是 Spring-MVC 留给我们的扩展接口
 */
@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...
                // 如果传入的webAppInitializerClasses中某个成员不是WebApplicationInitializer的子接口
                // 并且不是抽象实现类,就要创建实例
                if (!waiClass.isInterface() && !Modifier.isAbstract(waiClass.getModifiers()) &&
                        WebApplicationInitializer.class.isAssignableFrom(waiClass)) {
                    try {
                        // 将所有 WebApplicationInitializer 实现类收集到 list 中
                        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);
        // 循环调用所有实现类的 onStartup 方法
        for (WebApplicationInitializer initializer : initializers) {
            initializer.onStartup(servletContext);
        }
    }
}

主要的实现逻辑如下:

  1. 通过注解@HandlesTypes 来获取所有WebApplicationInitializer 类型的实现类
  2. 如果实现类 Set 不为空,则遍历所有实现类
  3. 找到其中的不是子接口的、不是抽象实现类的 实现类
  4. 创建实例,存到 LinkedList
  5. 循环调用每个实现类的 onStartup 方法。

既然要加载的是WebApplicationInitializer接口的实现类,那是不是我们创建一个类来实现该接口就可以对Web 容器进行初始化和添加组件了呢?如果真的这么写了,就和上面的普通 Web 工程一样了,我们无法通过这种形式创建 Spring MVC父子容器。

Spring MVC父子容器

先了解下 Spring MVC 父子容器的概念,打开–> Spring 官网<-- 可以看到,Spring 推荐我们使用父子容器的方式创建 Spring MVC上下文环境。其中 Servlet 应用上下文 注入所有与 Web 开发相关的功能,如所有 Controller 、视图解析器和处理器映射器。
而根应用上下文则负责管理所有与业务相关的组件,如 Service 层和 Repositories 数据访问层。
在这里插入图片描述

开始配置

说了半天,终于开始配置 Spring MVC 了。我们先打开WebApplicationInitializer 接口,看看 Spring 为我们提供的注释。
注释太长了,里面写了详细的 xml 配置方式。我主要翻译并截取了重要的部分
在这里插入图片描述
在这里插入图片描述
真的就跟写论文一样······。老外做程序员太吃香了,这些文档看完还有不会的道理么?

屁话不多说,这里主要强调了该接口的一个抽象实现,也是本文的重头戏:AbstractAnnotationConfigDispatcherServletInitializer。Spring-MVC官方推荐我们实现这个类,来对 Web 容器进行初始化的工作。先来看看这个类。

public abstract class AbstractAnnotationConfigDispatcherServletInitializer
        extends AbstractDispatcherServletInitializer {

    /**
     * {@inheritDoc}
     * <p>This implementation creates an {@link AnnotationConfigWebApplicationContext},
     * providing it the annotated classes returned by {@link #getRootConfigClasses()}.
     * Returns {@code null} if {@link #getRootConfigClasses()} returns {@code null}.
     */
    @Override
    @Nullable
    protected WebApplicationContext createRootApplicationContext() {
        Class<?>[] configClasses = getRootConfigClasses();
        if (!ObjectUtils.isEmpty(configClasses)) {
            AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
            context.register(configClasses);
            return context;
        }
        else {
            return null;
        }
    }

    /**
     * {@inheritDoc}
     * <p>This implementation creates an {@link AnnotationConfigWebApplicationContext},
     * providing it the annotated classes returned by {@link #getServletConfigClasses()}.
     */
    @Override
    protected WebApplicationContext createServletApplicationContext() {
        AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
        Class<?>[] configClasses = getServletConfigClasses();
        if (!ObjectUtils.isEmpty(configClasses)) {
            context.register(configClasses);
        }
        return context;
    }

    /**
     * 模板方法1:通过子类重写该方法,来返回 Root 应用上下文的配置类类型
     * @return the configuration for the root application context, or {@code null}
     * if creation and registration of a root context is not desired
     */
    @Nullable
    protected abstract Class<?>[] getRootConfigClasses();

    /**
     * 模板方法2:通过子类重写该方法,来返回 Servlet 应用上下文的配置类类型
     * {@code null} if all configuration is specified through root config classes.
     */
    @Nullable
    protected abstract Class<?>[] getServletConfigClasses();

}

可以看到 AbstractAnnotationConfigDispatcherServletInitializer 中应用了模板方法设计模式,上面两个方法分别创建了一个 WebApplicationContext 实例。顾名思义,createRootApplicationContext 方法负责创建根应用上下文环境,而createServletApplicationContext 负责创建 Servlet 应用上下文实例。这里的 WebApplicationContext 实例都是通过 new 关键字创建的,也就是说不存在单例模式的可能,两个方法都是实打实的创建出两个上下文,地址绝对不相同。

  1. createRootApplicationContext 方法调用了模板方法 getRootConfigClasses 来获取根应用的配置类,这个类里面可以注入一些 Bean ,指定包扫描等操作,也就是相当于一个 springApplication.xml 配置文件。
  2. createServletApplicationContext 方法调用了模板方法getServletConfigClasses 来获取 Servlet 容器的配置类,可以在这里配置前端控制器,视图解析器等组件。

我们只需要实现getRootConfigClasses 方法和 getServletConfigClasses 方法就可以分别配置 根应用上下文和 Servlet 上下文,再实现 getServletMappings 方法即可设置前端控制器的拦截路径。

代码实现

啰嗦一堆,重头戏终于来了!!!我会在方法的注释上贴上对应的 xml 配置方式,方便大家对比差异。

  1. 先看 AbstractAnnotationConfigDispatcherServletInitializer 的子类,也就是我们的配置类
public class WebMvcConfig extends AbstractAnnotationConfigDispatcherServletInitializer {
    /**
     * 指明跟容器的配置类的类型
     * 对应 XML 配置,这里可能不太对,在 xml 配置中。根应用上下文是在容器启动监听器中创建的
     * 	<context-param>
     *  	<param-name>contextConfigLocation</param-name>
     *  	<param-value>classpath:spring/springApplication.xml</param-value>
   	 *  </context-param>
     */
    @Override
    protected Class<?>[] getRootConfigClasses() {
        return new Class[]{RootContext.class};
    }

    /**
     * 指明 Web 容器的配置类的类型
     * 	<servlet>
	 *      <servlet-name>SpringMVC</servlet-name>
	 *     <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
	 *      <load-on-startup>1</load-on-startup>
     *	</servlet>
     */
    @Override
    protected Class<?>[] getServletConfigClasses() {
        return new Class[]{ServletContext.class};
    }

    /**
     * 为 DispatcherServlet 添加 URL 映射,例如:/ 或者 /app
     * 对应 XML 的配置
     * <servlet-mapping>
	 *      <servlet-name>SpringMVC</servlet-name>
	 *      <url-pattern>/</url-pattern>
     * </servlet-mapping>
     */
    @Override
    protected String[] getServletMappings() {
        return new String[]{"/"};
    }
}
  1. 根应用上下文配置类,只是用于演示配置,就只配了个数据源做做样子
/**
 * Spring 容器只扫描和 web 无关的组件,所以要排除 Controller
 */
@ComponentScan(basePackages = "com.mvc.demo",
excludeFilters = {
        @ComponentScan.Filter(classes = Controller.class)
    }
)

public class RootContext {

    @Bean
    public DataSource dataSource() {
        DruidDataSource dataSource = new DruidDataSource();
        dataSource.setUsername("root");
        dataSource.setPassword("123");
        dataSource.setDriverClassName("com.mysql.jdbc.Driver");
        dataSource.setUrl("jdbc:mysql://localhost:3306");
        return dataSource;
    }
}
  1. Servlet 应用上下文配置类,配置了前端控制器、视图解析器。还应该配置一下静态资源拦截器,这里就不写那么多了。
/**
 * Dispatcher 容器,只负责扫描与 Web 容器相关的组件,这里以Controller为例。要排除其他注解标注的类
 * 默认就会扫描 Spring 所有 Component 的衍生注解标注的类,所以要禁用
 * 默认规则
 */
@ComponentScan(basePackages = "com.mvc.demo", useDefaultFilters = false,
includeFilters = @ComponentScan.Filter(classes = Controller.class)
)
// 相当于 dispatcher-servlet.xml 中的 <mvc:annotation-driven />
@EnableWebMvc
public class ServletContext {
	/**
	* 对应的 dispatcher-servlet.xml 中的前端控制器配置
	*<servlet>
	        <servlet-name>SpringMVC</servlet-name>
	        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
	        <load-on-startup>1</load-on-startup>
	  </servlet>
	*/
    @Bean
    public DispatcherServlet dispatcherServlet() {
        DispatcherServlet dispatcherServlet = new DispatcherServlet();
        return dispatcherServlet;
    }
	
	/**
	 * 相当于dispatcher-servlet.xml 中的 <mvc:default-servlet-handler/>
	 */
	@Override
    public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
        configurer.enable();
    }
	/**
     * 对应 dispatcher-servlet.xml 中对视图解析器的配置
     * @param registry
     */
    @Override
    public void configureViewResolvers(ViewResolverRegistry registry){
    	// 这里以 FreeMarker 模板引擎为例
        FreeMarkerViewResolver viewResolver = new FreeMarkerViewResolver();
        viewResolver.setPrefix("/views/");
        viewResolver.setSuffix(".html");
        registry.viewResolver(viewResolver);
    }
}

好了,这就是用 Java 配置 Spring MVC的全部过程,是不是很方便?比起 xml 繁琐的配置,我还是更喜欢用 Java 来配置。
如果使用 SpringBoot ,这些配置都不用写了,更加方便。但是理解了这种 Java 配置形式之后,再去理解 SpringBoot 的自动配置原理会容易许多。苦尽甘来,写过 XML 的配置形式,才能体验 Java 配置的简单。配过 Spring MVC 才能知道 SpringBoot 是多么的方便。如果大家喜欢我的文章,觉得讲的还算通俗易懂,麻烦点个赞支持下!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值