aaaaaaa a# 整合IoC和MVC:在Web环境中启动IoC容器 你好,我是郭屹。 通过上一节课的工作,我们已经初步实现了一个基础的MVC框架,并且引入了@RequestMapping注解以及对指定包进行全局扫描来简化XML配置。然而,这个MVC框架是独立运行的,并没有与之前创建的IoC(控制反转)容器整合在一起。 在这节课中,我们将把IoC容器与MVC框架结合,使MVC中的Controller能够引用IoC容器中的Bean,从而形成一个统一的整体。

Servlet服务器启动过程

IoC容器是自包含的服务,而MVC框架则需要遵守Web标准。为了将这两者结合起来,我们需要了解一些关于Web规范的知识。根据Servlet规范,当服务器启动时,它会依据web.xml文件来进行配置。

web.xml配置文件概述

  • web.xml 文件是Java Servlet规范定义的一个配置文件,它包含了Web应用的所有配置信息。
  • 每个Java Web应用都必须有一个位于WEB-INF目录下的web.xml文件。
  • web.xml的根元素是<web-app>,并且指定了命名空间和schema。
  • 常见的配置项包括:
  • context-param:用于设置上下文参数。
  • Listener:监听器,可以用来监听Web应用的各种事件。
  • Filter:过滤器,可以在请求到达Servlet之前或响应返回客户端之前处理请求或响应。
  • Servlet:声明Servlet及其映射路径。 理解这些配置对于我们在Web环境下正确地启动IoC容器至关重要。接下来,我们将探讨如何具体实施这一整合。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
<display-name></display-name>
声明WEB应用的名字
<description></description>
 声明WEB应用的描述信息
<context-param></context-param>
声明应用全局的初始化参数。
<listener></listener>
声明监听器,它在建立、修改和删除会话或servlet环境时得到事件通知。
<filter></filter>
声明一个实现javax.servlet.Filter接口的类。
<filter-mapping></filter-mapping>
声明过滤器的拦截路径。
<servlet></servlet>
声明servlet类。
<servlet-mapping></servlet-mapping>
声明servlet的访问路径,试一个方便访问的URL。
<session-config></session-config>
session有关的配置,超时值。
<error-page></error-page>
在返回特定HTTP状态代码时,或者特定类型的异常被抛出时,能够制定将要显示的页面。

当Servlet服务器如Tomcat启动的时候,要遵守下面的时序:

  1. 在启动Web项目时,Tomcat会读取web.xml中的context-param节点,获取这个Web应用的全局参数。
  2. Tomcat创建一个ServletContext实例,是全局有效的。
  3. 将context-param的参数转换为键值对,存储在ServletContext里。
  4. 创建listener中定义的监听类的实例,按照规定Listener要继承自ServletContextListener。监听器初始化方法是contextInitialized(ServletContextEvent event)。初始化方法中可以通过event.getServletContext().getInitParameter(“name”)方法获得上下文环境中的键值对。
  5. 当Tomcat完成启动,也就是contextInitialized方法完成后,再对Filter过滤器进行初始化。
  6. servlet初始化:有一个参数load-on-startup,它为正数的值越小优先级越高,会自动启动,如果为负数或未指定这个参数,会在servlet被调用时再进行初始化。init-param 是一个servlet整个范围之内有效的参数,在servlet类的init()方法中通过 this.getInitParameter(“param1”)方法获得。 规范中规定的这个时序,就是我们整合两者的关键所在。

Listener初始化启动IoC容器

由上述服务器启动过程我们知道,我们把web.xml文件里定义的元素加载过程简单归总一下:先获取全局的参数context-param来创建上下文,之后如果配置文件里定义了Listener,那服务器会先启动它们,之后是Filter,最后是Servlet。因此我们可以利用这个时序,把容器的启动放到Web应用的Listener中。 Spring MVC就是这么设计的,它按照这个规范,用ContextLoaderListener来启动容器。我们也模仿它同样来实现这样一个Listener。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package com.minis.web;

import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;

public class ContextLoaderListener implements ServletContextListener {
	public static final String CONFIG_LOCATION_PARAM = "contextConfigLocation";
	private WebApplicationContext context;

	public ContextLoaderListener() {
	}
	public ContextLoaderListener(WebApplicationContext context) {
		this.context = context;
	}
	@Override
	public void contextDestroyed(ServletContextEvent event) {
	}
	@Override
	public void contextInitialized(ServletContextEvent event) {
		initWebApplicationContext(event.getServletContext());
	}
	private void initWebApplicationContext(ServletContext servletContext) {
		String sContextLocation = servletContext.getInitParameter(CONFIG_LOCATION_PARAM);
		WebApplicationContext wac = new AnnotationConfigWebApplicationContext(sContextLocation);
		wac.setServletContext(servletContext);
		this.context = wac;
		servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.context);
	}
}

ContextLoaderListener类中声明了一个常量CONFIG_LOCATION_PARAM,其默认值为contextConfigLocation。这个常量代表配置文件路径,即IoC容器的配置文件路径。因此,Listener期望在web.xml文件中有一个参数用于指定配置文件路径。我们可以查看web.xml文件以了解具体配置。

1
2
3
4
5
6
7
8
9
  <context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>applicationContext.xml</param-value>
  </context-param>
  <listener>
    <listener-class>
	        com.minis.web.ContextLoaderListener
	    </listener-class>
  </listener>

Listener 类的定义

在上述文件中定义了一个 Listener 类,该类负责加载全局参数并指定配置文件的路径。

ContextLoaderListener 类的作用

ContextLoaderListener 类中定义了 WebApplicationContext 对象,虽然目前该对象还不存在,但通过其名称我们可以推断出 WebApplicationContext 是一个上下文接口,主要用于 Web 项目中。

定义 WebApplicationContext

为了定义 WebApplicationContext,我们需要了解其在 Web 项目中的作用和实现方式。以下是定义 WebApplicationContext 的步骤和要点:

  1. 理解上下文接口WebApplicationContext 是 Spring 框架中用于管理 Web 应用程序上下文的接口。

  2. 配置文件加载ContextLoaderListener 通过监听特定的事件来加载配置文件,为应用程序提供必要的上下文信息。

  3. 上下文初始化:在 Web 应用程序启动时,ContextLoaderListener 会初始化 WebApplicationContext,确保所有的 Bean 都被正确加载和配置。

  4. 上下文使用:在应用程序运行过程中,WebApplicationContext 提供了访问和管理应用程序上下文的接口,包括 Bean 的查找和事件的发布等。

结论

通过上述步骤,我们可以定义 WebApplicationContext 并利用 ContextLoaderListener 在 Web 项目中管理和维护应用程序的上下文环境。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
package com.minis.web;

import javax.servlet.ServletContext;
import com.minis.context.ApplicationContext;

public interface WebApplicationContext extends ApplicationContext {
	String ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE = WebApplicationContext.class.getName() + ".ROOT";

	ServletContext getServletContext();
	void setServletContext(ServletContext servletContext);
}

ServletContext与ContextLoaderListener

在Web应用开发中,ServletContext是Servlet容器提供的用于存储全局信息的接口。ContextLoaderListener是一个特殊的监听器,用于在Web应用启动时初始化Spring的WebApplicationContext,并将其存储在ServletContext的属性中,以便在整个应用中共享和访问。

1. ServletContext的作用

ServletContext提供了一种方式,允许各个Servlet之间共享信息。它是一个域对象,可以存储属性,这些属性在整个Web应用的生命周期内都是可访问的。

2. ContextLoaderListener的职责

ContextLoaderListener的主要任务是在Web应用启动时加载Spring的WebApplicationContext。这个上下文包含了应用的所有Spring管理的Bean,是Spring应用的核心。

3. 初始化WebApplicationContext

ContextLoaderListenercontextInitialized方法中,我们初始化WebApplicationContext。这个过程通常涉及到加载Spring的配置文件,并创建应用上下文。

4. 存储WebApplicationContext

一旦WebApplicationContext被创建,我们需要将其存储在ServletContext的属性中。这样,其他组件就可以通过ServletContext获取到这个上下文,进而访问Spring管理的Bean。

5. 使用ServletContext的Attribute存储上下文

我们将WebApplicationContext存储在ServletContext的属性中,通常使用一个特定的属性名,如' ',来标识这个上下文。这样,任何需要访问Spring上下文的Servlet或Listener都可以通过这个属性名来获取上下文。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public void contextInitialized(ServletContextEvent event) {
    initWebApplicationContext(event.getServletContext());
}
private void initWebApplicationContext(ServletContext servletContext) {
    String sContextLocation =
servletContext.getInitParameter(CONFIG_LOCATION_PARAM);
    WebApplicationContext wac = new
AnnotationConfigWebApplicationContext(sContextLocation);
    wac.setServletContext(servletContext);
    this.context = wac;
    servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ ATTRIBUTE, this.context);

在Java Web应用中,通过web.xml配置文件获取到如applicationContext.xml这样的配置文件路径。接着,利用该路径创建一个AnnotationConfigWebApplicationContext对象,我们称之为WAC。这个WAC就是新的Spring应用上下文。

随后,通过调用servletContext.setAttribute()方法,并按照默认的属性值,将WAC设置到servletContext中。这样一来,AnnotationConfigWebApplicationContextservletContext之间就可以互相引用,这为程序内部组件之间的交互提供了便利。

AnnotationConfigWebApplicationContext是Spring框架中的一个类,它继承自GenericApplicationContext并实现了ConfigurableApplicationContext接口。此类主要用于支持基于注解的配置,允许开发者使用纯Java代码来定义和配置Spring容器中的Bean,而无需XML配置文件。这种方式不仅简化了配置过程,也使得应用程序更加易于维护。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
package com.minis.web;

import javax.servlet.ServletContext;
import com.minis.context.ClassPathXmlApplicationContext;

public class AnnotationConfigWebApplicationContext
					extends ClassPathXmlApplicationContext implements WebApplicationContext{
	private ServletContext servletContext;

	public AnnotationConfigWebApplicationContext(String fileName) {
		super(fileName);
	}
	@Override
	public ServletContext getServletContext() {
		return this.servletContext;
	}
	@Override
	public void setServletContext(ServletContext servletContext) {
		this.servletContext = servletContext;
	}
}

AnnotationConfigWebApplicationContext 与 IoC 容器的关系

AnnotationConfigWebApplicationContext 类继承自 IoC 容器中的 ClassPathXmlApplicationContext,它在 ClassPathXmlApplicationContext 的基础上增加了对 servletContext 的支持,使其适用于 Web 应用场景。

配置文件 applicationContext.xml

在 Web 应用中,我们使用 applicationContext.xml 作为配置文件,该文件由 web.xml 中定义的一个参数指定。

配置文件的作用

  • applicationContext.xml 文件用于定义 Spring 框架中的 Bean 和相关的配置。

  • 它允许开发者通过 XML 格式定义应用的配置信息,包括但不限于 Bean 的定义、属性注入、以及 Bean 之间的依赖关系。

在 web.xml 中的配置

  • 在 web.xml 文件中,我们通过指定参数来引入 applicationContext.xml 文件,从而将 Spring 的配置集成到 Web 应用中。

  • 这允许 Web 应用利用 Spring 框架提供的 IoC 容器功能,实现依赖注入和 Bean 管理。

1
2
3
4
  <context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>applicationContext.xml</param-value>
  </context-param>

这个配置文件就是我们现在的IoC容器的配置文件,主要作用是声明Bean,如:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
<?xml version="1.0" encoding="UTF-8"?>
<beans>
	<bean id="bbs" class="com.test.service.BaseBaseService">
	    <property type="com.test.service.AServiceImpl" name="as" ref="aservice"/>
	</bean>
	<bean id="aservice" class="com.test.service.AServiceImpl">
		<constructor-arg type="String" name="name" value="abc"/>
		<constructor-arg type="int" name="level" value="3"/>
        <property type="String" name="property1" value="Someone says"/>
        <property type="String" name="property2" value="Hello World!"/>
        <property type="com.test.service.BaseService" name="ref1" ref="baseservice"/>
	</bean>
	<bean id="baseservice" class="com.test.service.BaseService">
	</bean>
</beans>

回顾一下,现在完整的过程是:当Servlet服务器启动时,Listener会优先启动,读配置文件路径,启动过程中初始化上下文,然后启动IoC容器,这个容器通过refresh()方法加载所管理的Bean对象。这样就实现了Tomcat启动的时候同时启动IoC容器。

好了,到了这一步,IoC容器启动了,我们回来再讨论MVC这边的事情。我们已经知道,在服务器启动的过程中,会注册Web应用上下文,也就是WAC。这样方便我们通过属性拿到启动时的WebApplicationContext。

1
this.webApplicationContext = (WebApplicationContext) this.getServletContext().getAttribute(WebApplicationContext.ROOT_WEB_APPLICATION _CONTEXT_ATTRIBUTE);

因此我们改造一下DispatcherServlet这个核心类里的init()方法。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
public void init(ServletConfig config) throws ServletException {          super.init(config);
    this.webApplicationContext = (WebApplicationContext)
this.getServletContext().getAttribute(WebApplicationContext.ROOT_WEB_APPLICATION _CONTEXT_ATTRIBUTE);
    sContextConfigLocation = config.getInitParameter("contextConfigLocation");
    URL xmlPath = null;
	try {
		xmlPath = this.getServletContext().getResource(sContextConfigLocation);
	} catch (MalformedURLException e) {
		e.printStackTrace();
	}
    this.packageNames = XmlScanComponentHelper.getNodeValue(xmlPath);        Refresh();
}

Servlet初始化过程

1. 获取WebApplicationContext

在Servlet初始化阶段,首先从ServletContext中获取属性,以获得Listener在启动时注册的WebApplicationContext

2. 获取配置文件路径

接着,获取Servlet配置参数contextConfigLocation,该参数表示配置文件的路径,通常指向MVC使用的配置文件,例如minisMVC-servlet.xml

3. 扫描包和加载Bean

在获取到配置文件路径后,扫描指定路径下的包,并调用refresh()方法来加载Bean,完成DispatcherServlet的初始化。

构建URL与后端程序映射关系

1. 改造initMapping()方法

按照新的方法构建URL和后端程序之间的映射关系,需要查找所有使用了@RequestMapping注解的方法。

2. 存储URL和映射信息

将URL存放到urlMappingNames中,映射的对象存放到mappingObjs中,映射的方法存放到mappingMethods中。这种方法替代了过去通过解析Bean得到的映射关系,减少了XML文件中的手工配置。

3. 查看相关代码

可以通过查看相关代码来进一步了解这一过程的具体实现。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
protected void initMapping() {
    for (String controllerName : this.controllerNames) {
        Class<?> clazz = this.controllerClasses.get(controllerName);             Object obj = this.controllerObjs.get(controllerName);
        Method[] methods = clazz.getDeclaredMethods();
        if (methods != null) {
            for (Method method : methods) {
                boolean isRequestMapping =
method.isAnnotationPresent(RequestMapping.class);
                if (isRequestMapping) {
                    String methodName = method.getName();
                    String urlMapping =
method.getAnnotation(RequestMapping.class).value();
                    this.urlMappingNames.add(urlMapping);
                    this.mappingObjs.put(urlMapping, obj);
                    this.mappingMethods.put(urlMapping, method);
                }
            }
        }
    }
}

最后稍微调整一下 doGet() 方法内的代码,去除不再使用的结构。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    String sPath = request.getServletPath();
	if (!this.urlMappingNames.contains(sPath)) {
		return;
	}

    Object obj = null;
    Object objResult = null;
    try {
        Method method = this.mappingMethods.get(sPath);
        obj = this.mappingObjs.get(sPath);
        objResult = method.invoke(obj);
    } catch (Exception e) {
		e.printStackTrace();
	}
    response.getWriter().append(objResult.toString());
}

整合了IoC容器的MVC实现

在我们的Web应用中,doGet()方法承担着处理HTTP GET请求的任务。它会从请求中解析出访问路径,并根据这个路径与后端程序之间的映射关系来决定调用哪个对象的哪个方法。执行完相应的方法之后,将结果直接返回给客户端。

doGet() 方法详解

  • 获取访问路径:首先,doGet()方法会读取请求中的URL路径部分。
  • 映射到后端逻辑:然后,通过预定义好的映射规则(通常是配置文件或注解的形式),确定要调用的具体服务类和服务方法。
  • 调用对应方法:接着,使用IoC容器管理的服务实例来调用对应的方法。
  • 返回结果:最后,将方法执行的结果包装成合适的格式并通过response对象发送回客户端。

完整流程示例

假设有一个简单的用户信息查询接口,其URL为/user/info,则整个处理过程可能如下:

  1. doGet()接收到请求并识别出请求路径是/user/info。2. 根据映射规则,找到应该调用UserService类下的getUserInfo()方法。3. IoC容器提供UserService的一个实例。4. 调用getUserInfo()方法获取用户信息。5. 将得到的信息转换成JSON字符串形式。6. 设置响应头信息,并将JSON字符串写入response对象,从而完成响应。

验证

为了确保上述功能正确无误地工作,接下来我们将查看Tomcat服务器使用的web.xml配置文件以确认相关设置是否恰当。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:web="http://xmlns.jcp.org/xml/ns/javaee" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" id="WebApp_ID">
  <context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>applicationContext.xml</param-value>
  </context-param>
  <listener>
    <listener-class>
	        com.minis.web.ContextLoaderListener
	    </listener-class>
  </listener>
  <servlet>
    <servlet-name>minisMVC</servlet-name>
    <servlet-class>com.minis.web.DispatcherServlet</servlet-class>
    <init-param>
      <param-name>contextConfigLocation</param-name>
      <param-value> /WEB-INF/minisMVC-servlet.xml </param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
  </servlet>
  <servlet-mapping>
    <servlet-name>minisMVC</servlet-name>
    <url-pattern>/</url-pattern>
  </servlet-mapping>
</web-app>

然后是IoC容器使用的配置文件applicationContext.xml。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
<?xml version="1.0" encoding="UTF-8"?>
<beans>
	<bean id="bbs" class="com.test.service.BaseBaseService">
	    <property type="com.test.service.AServiceImpl" name="as" ref="aservice"/>
	</bean>
	<bean id="aservice" class="com.test.service.AServiceImpl">
		<constructor-arg type="String" name="name" value="abc"/>
		<constructor-arg type="int" name="level" value="3"/>
        <property type="String" name="property1" value="Someone says"/>
        <property type="String" name="property2" value="Hello World!"/>
        <property type="com.test.service.BaseService" name="ref1" ref="baseservice"/>
	</bean>
	<bean id="baseservice" class="com.test.service.BaseService">
	</bean>
</beans>

MVC扫描的配置文件minisMVC-servlet.xml。

1
2
3
4
<?xml version="1.0" encoding="UTF-8" ?>
<components>
<component-scan base-package="com.test"/>
</components>

最后,在com.minis.test.HelloworldBean内的测试方法上,增加@RequestMapping注解。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
package com.test;

import com.minis.web.RequestMapping;

public class HelloWorldBean {
    @RequestMapping("/test")
    public String doTest() {
        return "hello world for doGet!";
    }
}

Tomcat测试与MVC和IoC整合

在本节课中,我们成功地将MVC设计模式与IoC(控制反转)容器进行了整合,并通过Tomcat服务器启动了我们的应用程序。当你在浏览器中输入 localhost:8080/test 并看到预期的结果时,这标志着我们的配置是正确的。这个端口号可以根据需要自定义,同时请求路径可能还需要根据实际的应用上下文进行调整。

整合过程详解

  • Tomcat启动阶段

  • 首先读取context-param参数。

  • 初始化监听器(Listener),在这个过程中会创建IoC容器并构建WebApplicationContext (WAC)。

  • WAC加载被管理的Bean对象,并将其关联到servlet context中。

  • DispatcherServlet初始化

  • 从servletContext获取属性以获得WAC,并将其设置为servlet的属性之一。

  • 根据servlet的配置读取路径参数。

  • 扫描指定路径下的包来发现组件。

  • 调用refresh()方法加载更多Bean。

  • 最后配置URL映射关系。

关键点理解

能够实现MVC与IoC的整合,关键在于遵循了Servlet规范所规定的生命周期顺序:从监听器、过滤器到最后的servlet。这种顺序为我们提供了干预程序执行流程的机会,使得我们可以插入自己的逻辑代码。 在开发软件时,重要的是学习如何建立一个可扩展的系统结构。这意味着不应该把程序的流程固定得太死板,而应该留有足够的接口和扩展点,让其他开发者也能方便地加入他们自己的业务逻辑。 框架之所以成为框架而不是单纯的应用程序,正是因为它提供了一套高度可扩展的体系结构,给后续的程序员留下了极大的发展空间。阅读如Rodd Johnson等大师级人物编写的源代码,就像是品鉴一部文学杰作,让人不禁感叹其优雅的设计。

完整源码

如果你想查看完整的源代码示例,可以访问 https://github.com/YaleGuo/minis

课后思考题

既然我们知道可以通过DispatcherServlet访问WebApplicationContext中管理的Bean,那么反过来是否可以通过WebApplicationContext访问到由DispatcherServlet管理的Bean呢?欢迎你在讨论区分享你的想法。如果你觉得这节课对你有所帮助,请不要犹豫把它推荐给你认为需要的朋友。期待下次课程与你再次相见!aaaaaa