Spring注解驱动开发第54讲——Servlet 3.0整合Spring MVC

59 篇文章 131 订阅

写在前面

在上一讲中,我们分析清楚了Servlet 3.0整合Spring MVC的底层原理。接下来,我们就要以注解的方式将Spring MVC整合进来了。其实,大家完全可以参考Spring的官方文档的这一块的写法。

在这里插入图片描述

因为这块的写法跟我们的分析是一模一样的。

Servlet 3.0与Spring MVC的整合

首先,我们来编写一个类,例如MyWebAppInitializer,来继承AbstractAnnotationConfigDispatcherServletInitializer这个抽象类,一开始我们写成下面这样。

package com.meimeixia;

import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;

public class MyWebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {

	@Override
	protected Class<?>[] getRootConfigClasses() {
		// TODO Auto-generated method stub
		return null;
	}

	@Override
	protected Class<?>[] getServletConfigClasses() {
		// TODO Auto-generated method stub
		return null;
	}

	@Override
	protected String[] getServletMappings() {
		// TODO Auto-generated method stub
		return null;
	}

}

按照我们之前的分析,以上该类会在web容器启动的时候创建对象,而且在整个创建对象的过程中,会来调用相应方法来初始化容器以及前端控制器。

我们自己写的MyWebAppInitializer类继承了AbstractAnnotationConfigDispatcherServletInitializer抽象类之后,发现它里面需要重写三个抽象方法,下面我们就来详细说一下它们。

  • getRootConfigClasses方法:它是来获取根容器的配置类的,该配置类就类似于我们以前经常写的Spring的配置文件,而且我们以前是利用监听器的方式来读取Spring的配置文件的哟~,还记得吗?然后,就能创建出一个父容器了

  • getServletConfigClasses方法:它是来获取web容器的配置类的,该配置类就类似于我们以前经常写的Spring MVC的配置文件,而且我们以前是利用前端控制器来加载Spring MVC的配置文件的哟~,你还记得吗?然后,就能创建出一个子容器了

  • getServletMappings方法:它是来获取DispatcherServlet的映射信息的。该方法需要返回一个String[],如果我们返回的是这样一个new String[]{"/"}东东,即:

    @Override
    protected String[] getServletMappings() {
        // TODO Auto-generated method stub
        return new String[]{"/"};
    }
    

    那么DispatcherServlet就会来拦截所有请求,包括静态资源,比如xxx.js文件、xxx.png等等等等,但是不包括*.jsp,也即不会拦截所有的jsp页面。

    如果我们返回的是这样一个new String[]{"/*"}东东,即:

    @Override
    protected String[] getServletMappings() {
        // TODO Auto-generated method stub
        return new String[]{"/*"};
    }
    

    那么DispatcherServlet就是真正来拦截所有请求了,包括*.jsp,也就是说就连jsp页面都拦截,所以,我们切忌不可写成这样(即/*)。否则的话,jsp页面一旦被Spring MVC拦截,最终极有可能我们就看不到jsp页面了,因为jsp页面是由Tomcat服务器中的jsp引擎来解析的。

也就是说,我们最好是在getServletMappings方法中返回这样一个new String[]{"/"}东东,即:

package com.meimeixia;

import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;

/**
 * 在web容器启动的时候创建对象,而且在整个创建对象的过程中,会调用相应方法来初始化容器以及前端控制器
 * 编写好该类之后,就相当于是在以前我们配置好了web.xml文件
 * @author liayun
 *
 */
public class MyWebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {

	/*
	 * 获取根容器的配置类,该配置类就类似于我们以前经常写的Spring的配置文件,然后创建出一个父容器
	 */
	@Override
	protected Class<?>[] getRootConfigClasses() {
		// TODO Auto-generated method stub
		return null;
	}

	/*
	 * 获取web容器的配置类,该配置类就类似于我们以前经常写的Spring MVC的配置文件,然后创建出一个子容器
	 */
	@Override
	protected Class<?>[] getServletConfigClasses() {
		// TODO Auto-generated method stub
		return null;
	}

	// 获取DispatcherServlet的映射信息
	/*
	 * /:拦截所有请求,包括静态资源,比如xxx.js文件、xxx.png等等等等,但是不包括*.jsp,也即不会拦截所有的jsp页面
	 * /*:真正来拦截所有请求了,包括*.jsp,也就是说就连jsp页面都拦截
	 */
	@Override
	protected String[] getServletMappings() {
		// TODO Auto-generated method stub
		return new String[]{"/"};
	}

}

由于我们还需要在getRootConfigClasses和getServletConfigClasses这俩方法中指定两个配置类的位置,所以我们来创建上两个配置类,分别如下:

  • 根容器的配置类,例如RootConfig

    package com.meimeixia.config;
    
    public class RootConfig {
    
    }
    
  • web容器的配置类,例如AppConfig

    package com.meimeixia.config;
    
    public class AppConfig {
    
    }
    

以上这两个配置类最终需要形成父子容器的效果。还要有一点,我需要重点来说明,即AppConfig配置类只来扫描所有的控制器(即Controller),以及和网站功能有关的那些逻辑组件;RootConfig配置类只来扫描所有的业务逻辑核心组件,包括dao层组件、不同的数据源等等,反正它只扫描和业务逻辑相关的组件就哦了。

接下来,我们来完善以上两个配置类。首先,先来完善RootConfig配置类,我们可以使用@ComponentScan注解来指定扫描com.meimeixia包以及子包下的所有组件,而且为了形成父子容器,还必须得排除掉一些组件,那排除掉哪些组件呢?很显然,应该排除掉controller控制层组件,即Controller。所以,我们得通过@ComponentScan注解的excludeFilters()方法按照注解的方式来排除掉所有标注了@Controller注解的组件。

package com.meimeixia.config;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.FilterType;
import org.springframework.context.annotation.ComponentScan.Filter;
import org.springframework.stereotype.Controller;

// 该配置类相当于Spring的配置文件
// Spring容器不扫描Controller,它是一个父容器
@ComponentScan(value="com.meimeixia",excludeFilters={
		@Filter(type=FilterType.ANNOTATION, classes={Controller.class})
})
public class RootConfig {

}

然后,再来完善AppConfig配置类,我们同样使用@ComponentScan注解来指定扫描com.meimeixia包以及子包下的所有组件,但是呢,与上面正好相反,这儿只扫描controller控制层组件,即Controller,如此一来就能与上面形成互补配置了。OK,那我们就通过@ComponentScan注解的includeFilters()方法按照注解的方式来指定只扫描controller控制层组件。

package com.meimeixia.config;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.FilterType;
import org.springframework.context.annotation.ComponentScan.Filter;
import org.springframework.stereotype.Controller;

// 该配置类相当于Spring MVC的配置文件
// Spring MVC容器只扫描Controller,它是一个子容器
// useDefaultFilters=false:禁用默认的过滤规则
@ComponentScan(value="com.meimeixia",includeFilters={
		@Filter(type=FilterType.ANNOTATION, classes={Controller.class})
},useDefaultFilters=false)
public class AppConfig {

}

尤其要注意这一点,在以上配置类中通过@ComponentScan注解的includeFilters()方法来指定只扫描controller控制层组件时,需要禁用掉默认的过滤规则,即必须得加上useDefaultFilters=false这样一个配置。千万记得必须要禁用掉默认的过滤规则哟,否则扫描就不会生效了。

但是,在RootConfig配置类中通过@ComponentScan注解的excludeFilters()方法来指定排除哪些组件时,是不需要对useDefaultFilters进行设置的,因为其默认值就是true,表示默认情况下标注了@Component@Repository@Service以及@Controller这些注解的组件都会被扫描,即扫描所有。

接下来,我们就要分别来编写一个controller控制层组件和service业务层组件来进行测试了。首先,编写一个service业务层组件,例如HelloService,如下所示。

package com.meimeixia.service;

import org.springframework.stereotype.Service;

@Service
public class HelloService {
	
	public String sayHello(String name) {
		return "Hello, " + name;
	}
	
}

然后,编写一个controller控制层组件,例如HelloController,并且我们还可以在该HelloController中注入HelloService组件,来调用其方法。

package com.meimeixia.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import com.meimeixia.service.HelloService;

@Controller
public class HelloController {
	
	@Autowired
	HelloService helloService;

	@ResponseBody
	@RequestMapping("/hello")
	public String hello() {
		String hello = helloService.sayHello("tomcat...");
		return hello;
	}
	
}

从以上HelloController的代码中,我们可以看到它里面的hello方法是来处理hello请求的,而且通过@ResponseBody注解会直接将返回的结果(即字符串)写到浏览器页面中。

现在,我们能不能启动咱们的项目进行测试了呢?还不可以,因为我们还没有在我们自己编写的MyWebAppInitializer类中指定两个配置类的位置。OK,那我们来分别指定一下,如下所示。

package com.meimeixia;

import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;

import com.meimeixia.config.AppConfig;
import com.meimeixia.config.RootConfig;

/**
 * 在web容器启动的时候创建对象,而且在整个创建对象的过程中,会调用相应方法来初始化容器以及前端控制器
 * 编写好该类之后,就相当于是在以前我们配置好了web.xml文件
 * @author liayun
 *
 */
public class MyWebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {

	/*
	 * 获取根容器的配置类,该配置类就类似于我们以前经常写的Spring的配置文件,然后创建出一个父容器
	 */
	@Override
	protected Class<?>[] getRootConfigClasses() {
		// TODO Auto-generated method stub
		return new Class<?>[]{RootConfig.class};
	}

	/*
	 * 获取web容器的配置类,该配置类就类似于我们以前经常写的Spring MVC的配置文件,然后创建出一个子容器
	 */
	@Override
	protected Class<?>[] getServletConfigClasses() {
		// TODO Auto-generated method stub
		return new Class<?>[]{AppConfig.class};
	}

	// 获取DispatcherServlet的映射信息
	/*
	 * /:拦截所有请求,包括静态资源,比如xxx.js文件、xxx.png等等等等,但是不包括*.jsp,也即不会拦截所有的jsp页面
	 * /*:真正来拦截所有请求了,包括*.jsp,也就是说就连jsp页面都拦截
	 */
	@Override
	protected String[] getServletMappings() {
		// TODO Auto-generated method stub
		return new String[]{"/"};
	}

}

这就相当于分别来指定Spring配置文件和Spring MVC配置文件的位置。

最后,我们就可以启动项目来进行测试了。项目启动成功之后,我们在浏览器地址栏中输入http://localhost:8080/springmvc-annotation-liayun/hello进行访问,发现浏览器页面上确实打印出来了一串字符串,如下图所示。

在这里插入图片描述

这说明我们controller控制层组件和service业务层组件都起作用了。

至此,使用注解的方式(即配置类的方式)来整合Spring MVC,我们就算是彻底讲完了。

总结

结合前两讲中的内容,我想在这儿做一点总结,不然感觉脑子始终一团浆糊。我们知道web容器(即Tomcat服务器)在启动应用的时候,会扫描当前应用每一个jar包里面的META-INF/services/javax.servlet.ServletContainerInitializer文件中指定的实现类,然后再运行该实现类中的方法。

恰好在spring-web-4.3.11.RELEASE.jar中的META-INF/services/目录里面有一个javax.servlet.ServletContainerInitializer文件,并且在该文件中指定的实现类就是org.springframework.web.SpringServletContainerInitializer,打开该实现类,发现它上面标注了@HandlesTypes(WebApplicationInitializer.class)这样一个注解。

因此,web容器在启动应用的时候,便会来扫描并加载org.springframework.web.SpringServletContainerInitializer实现类,而且会传入我们感兴趣的类型(即WebApplicationInitializer接口)的所有后代类型,最终再运行其onStartup方法。

package org.springframework.web;

import java.lang.reflect.Modifier;
import java.util.LinkedList;
import java.util.List;
import java.util.ServiceLoader;
import java.util.Set;
import javax.servlet.ServletContainerInitializer;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.annotation.HandlesTypes;

import org.springframework.core.annotation.AnnotationAwareOrderComparator;

@HandlesTypes(WebApplicationInitializer.class)
public class SpringServletContainerInitializer implements ServletContainerInitializer {

	@Override
	public void onStartup(Set<Class<?>> webAppInitializerClasses, ServletContext servletContext)
			throws ServletException {

		List<WebApplicationInitializer> initializers = new LinkedList<WebApplicationInitializer>();

		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) 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);
		}
	}

}

从以上onStartup方法中,我们可以看到它会遍历感兴趣的类型(即WebApplicationInitializer接口)的所有后代类型,然后利用反射技术创建WebApplicationInitializer类型的对象,而我们自定义的MyWebAppInitializer就是WebApplicationInitializer这种类型的。而且创建完之后,都会存储到名为initializers的LinkedList<WebApplicationInitializer>集合中。接着,又会遍历该集合,并调用每一个WebApplicationInitializer对象的onStartup方法。

遍历到每一个WebApplicationInitializer对象之后,调用其onStartup方法,实际上就是先调用其(例如我们自定义的MyWebAppInitializer)最高父类的onStartup方法,创建根容器;然后再调用其次高父类的onStartup方法,创建web容器以及DispatcherServlet;接着,根据其重写的getServletMappings方法来为DispatcherServlet配置映射信息,差不多就是这样了。

如有总结不对的地方,还望指出,我来修改。创作不易,还请多多点赞与评论哟😀

  • 3
    点赞
  • 1
    评论
  • 0
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

打赏
文章很值,打赏犒劳作者一下
相关推荐
©️2020 CSDN 皮肤主题: 猿与汪的秘密 设计师:白松林 返回首页

打赏

李阿昀

你的鼓励将是我创作的最大动力

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值