Spring源码学习 | 1 简单使用Spring与SpringMVC

前言

上一篇写了使用Servlet的方式进行web项目搭建,这一篇来看有Spring之后,web项目怎么做。本文将使用Spring + SpringMVC来搭建demo。

实战:Spring + SpringMVC

上一篇对于搭建web项目步骤介绍的比较详细了,本文就简单的列举一下关键的配置:

项目结构

spring-demo
    │
    ├─src
    │  ├─main
    │  │  ├─java
    │  │  │  └─cn
    │  │  │      └─dingyufan
    │  │  │          └─demo
    │  │  │              └─spring
    │  │  │                  ├─controller
    │  │  │                  │     └─HelloController.java
    │  │  │                  │
    │  │  │                  └─service
    │  │  │                        └─HelloService.java
    │  │  │
    │  │  ├─resources
    │  │  └─webapp
    │  │      └─WEB-INF
    │  │           ├─spring-beans.xml
    │  │           ├─spring-mvc.xml
    │  │           └─web.xml
    │  │
    │  └─test
    │      ├─java
    │      └─resources
    │
    └─pom.xml
复制代码

依赖包

日常使用都是springboot,现在回到自己动手找依赖包的时候,也是有些不习惯了= =。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>cn.dingyufan</groupId>
    <artifactId>spring-demo</artifactId>
    <version>1.0-SNAPSHOT</version>
    <name>spring-demo</name>
    <packaging>war</packaging>

    <properties>
        <maven.compiler.target>1.8</maven.compiler.target>
        <maven.compiler.source>1.8</maven.compiler.source>
        <spring.version>5.2.10.RELEASE</spring.version>
    </properties>

    <dependencies>

        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
            <version>4.0.1</version>
        </dependency>

        <!--spring核心依赖-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-core</artifactId>
            <version>${spring.version}</version>
        </dependency>

        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-beans</artifactId>
            <version>${spring.version}</version>
        </dependency>

        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>${spring.version}</version>
        </dependency>

        <!--spring web依赖-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-web</artifactId>
            <version>${spring.version}</version>
        </dependency>

        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-webmvc</artifactId>
            <version>${spring.version}</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-war-plugin</artifactId>
                <version>2.4</version>
            </plugin>
        </plugins>
    </build>
</project>
复制代码

编码

我们这里就简单的写一个Controller和一个Service。

package cn.dingyufan.demo.spring.controller;

import cn.dingyufan.demo.spring.service.HelloService;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;

@RestController
public class HelloController implements ApplicationContextAware {

    private ApplicationContext applicationContext;

    @Autowired
    private HelloService helloService;

    @GetMapping("/hello")
    public ResponseEntity<String> hello(HttpServletRequest request) {
        return ResponseEntity.ok("Hello Spring!");
    }

    @GetMapping("/check")
    public ResponseEntity<String> check(HttpServletRequest request) {
        return ResponseEntity.ok(String.valueOf(helloService.hashCode()));
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        System.out.println("HelloController注入applicationContext =>" + applicationContext.getDisplayName());
        this.applicationContext = applicationContext;
    }
}
复制代码
package cn.dingyufan.demo.spring.service;


import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Service;


public class HelloService implements ApplicationContextAware {

    private ApplicationContext applicationContext;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        System.out.println("HelloService注入applicationContext =>" + applicationContext.getDisplayName());
        this.applicationContext = applicationContext;
    }
}

复制代码

可以看到我们代码中都实现了ApplicationContextAware接口,简单解释一下这个接口的作用。这个接口其实是继承于Aware接口,作用就是可以通过set方法注入applicationContext实例。其实有一系列类似的接口,同理比如BeanFactoryAware,可以通过set方法注入beanFactory,还有很多在此不一一举例。在Controller和Service注入applicationContext的目的,就是对比一下两者获取到的容器的异同,后面会细说。

web.xml配置

上一篇中,我们在web.xml配置了listener、context-param、filter和servlet。同样引入Spring之后也需要配置这些内容,但是和之前的配置有些许差别。

<?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_4_0.xsd"
         version="4.0">


    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>


    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>/WEB-INF/spring-beans.xml</param-value>
    </context-param>


    <filter>
        <filter-name>encodingFilter</filter-name>
        <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
        <init-param>
            <param-name>encoding</param-name>
            <param-value>UTF-8</param-value>
        </init-param>
        <init-param>
            <param-name>forceEncoding</param-name>
            <param-value>true</param-value>
        </init-param>
    </filter>

    <filter-mapping>
        <filter-name>encodingFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>


    <servlet>
        <servlet-name>dispatcherServlet</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>WEB-INF/spring-mvc.xml</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>

    <servlet-mapping>
        <servlet-name>dispatcherServlet</servlet-name>
        <url-pattern>/*</url-pattern>
    </servlet-mapping>

</web-app>
复制代码

在这个web.xml中,我们配置了同样的内容,但是相关的类都不是自己写的,而是Spring提供的。这里对各组件进行介绍。

ContextLoaderListener

ContextLoaderListener是spring-web包提供的一个监听器。打开来看它的源码,发现它和我们上一篇写的HelloListener一样,都实现了ServletContextListener接口,并重写了接口的contextInitialized()和contextDestroyed()方法。通过调用initWebApplicationContext(),创建并初始化一个applicationContext。

那么可以得出一个结论,服务器启动之后,ServletContext初始化后,Spring的容器(applicationContext)就会通过监听器(ContextLoaderListener)来构建并初始化。这里就是Web项目中Spring容器启动的入口。

public class ContextLoaderListener extends ContextLoader implements ServletContextListener {

	public ContextLoaderListener(WebApplicationContext context) {
		super(context);
	}

	@Override
	public void contextInitialized(ServletContextEvent event) {
                // 顾名思义,这里会创建并初始化一个applicationContext
		initWebApplicationContext(event.getServletContext());
	}

	@Override
	public void contextDestroyed(ServletContextEvent event) {
		closeWebApplicationContext(event.getServletContext());
		ContextCleanupListener.cleanupAttributes(event.getServletContext());
	}

}

复制代码

CharacterEncodingFilter

这个Filter并不是必须项。只是通常情况下会使用这个过滤器来统一request的编码。配置这个过滤器是为了和前文自己写的做个对比。

image.png

通过他的继承关系,可以看到他和我们前文RandomFilter一样,也实现了Filter接口。核心的doFilter()方法由父类OncePerRequestFilter实现。父类OncePerRequestFilter在doFilter()中写了自己的逻辑,并调用了doFilterInternal()方法。这个方法在OncePerRequestFilter是一个空方法,作用是留给子类实现子类自己的逻辑。这种做法是一种设计模式,模板方法模式,在Spring源码中很常见。

image.png

所以CharacterEncodingFilter的逻辑就是在自己的doFilterInternal()方法中。具体的逻辑就是根据局部变量,判断是否修改request和response的编码,并无拦截目的。

public class CharacterEncodingFilter extends OncePerRequestFilter {
	@Nullable
	private String encoding;
	private boolean forceRequestEncoding = false;
	private boolean forceResponseEncoding = false;

	public CharacterEncodingFilter() {
        }
	public CharacterEncodingFilter(String encoding) {
		this(encoding, false);
	}
	public CharacterEncodingFilter(String encoding, boolean forceEncoding) {
		this(encoding, forceEncoding, forceEncoding);
	}
	public CharacterEncodingFilter(String encoding, boolean forceRequestEncoding, boolean forceResponseEncoding) {
		Assert.hasLength(encoding, "Encoding must not be empty");
		this.encoding = encoding;
		this.forceRequestEncoding = forceRequestEncoding;
		this.forceResponseEncoding = forceResponseEncoding;
	}

	public void setEncoding(@Nullable String encoding) {
		this.encoding = encoding;
	}

	@Nullable
	public String getEncoding() {
		return this.encoding;
	}

	public void setForceEncoding(boolean forceEncoding) {
		this.forceRequestEncoding = forceEncoding;
		this.forceResponseEncoding = forceEncoding;
	}

	public void setForceRequestEncoding(boolean forceRequestEncoding) {
		this.forceRequestEncoding = forceRequestEncoding;
	}

	public boolean isForceRequestEncoding() {
		return this.forceRequestEncoding;
	}

	public void setForceResponseEncoding(boolean forceResponseEncoding) {
		this.forceResponseEncoding = forceResponseEncoding;
	}

	public boolean isForceResponseEncoding() {
		return this.forceResponseEncoding;
	}


	@Override
	protected void doFilterInternal(
			HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
			throws ServletException, IOException {
                // CharacterEncodingFilterj具体逻辑
		String encoding = getEncoding();
		if (encoding != null) {
			if (isForceRequestEncoding() || request.getCharacterEncoding() == null) {
				request.setCharacterEncoding(encoding);
			}
			if (isForceResponseEncoding()) {
				response.setCharacterEncoding(encoding);
			}
		}
		filterChain.doFilter(request, response);
	}

}
复制代码

还有一点需要简单解释一下,我们看到web.xml中filter的配置中有两个init-param。可以发现这个其实就是对应他源码中成员变量对应的。父类GenericFilterBean会在init()方法中,将配置的值赋值给对应的字段。

DispatcherServlet

DispatcherServlet是SpringMVC必不可少的组件,它是由spring-webmvc包提供。从它的类名就可以知道它就是一个Servlet,它也确实实现了Servlet接口。它可以算是SpringMVC所有请求的入口。它涉及的内容非常多,在这里我们只要先明白他的几个作用

  • 创建并初始化一个WebApplicationContext
  • 转发根据requestMapping转发请求给Handler(Controller)

image.png

DispatcherServlet相关的web.xml配置也有init-param,配置了SpringMVC配置文件的位置;还有一个load-on-startup配置,这个配置的作用就是表示加载顺序,能让Servlet在ServletContext初始化后就依次创建Servlet实例,如果不配置或配置负数,则是像上文说的有请求时再创建Servlet实例。

context-param

在web.xml中有一个context-param配置,这个配置的其实就是Spring的配置文件位置。context-param会加载到ServletContext中,ContextLoaderListener在方法中创建Spring容器applianceContext的时候,会读取ServletContext中这项配置的值,并给applicationContext赋值,这样后期Spring容器读取配置时就可以找到正确的位置。

在web.xml我们在context-param中配置了contextConfigLocation,DispatcherServlet中也配置了contextConfigLocation,两者有什么区别呢?

实际上,在使用Spring + SpringMVC的时候,Spring会创建一个Root WebApplicationContext,管理Spring的Bean;SpringMVC也会创建一个容器WebApplicationContext,它是Root WebApplicationContext的子容器,管理Controller相关Bean。为什么要这么做呢?我的理解是,这样可以很方便解耦。比如我们选择Spring + Structs2,Spring依然有一个Root WebApplicationContext容器用以管理Bean,而控制器层由Structs2管理。这样Spring或是SpringMVC都可以单独拿出来,灵活组合。

spring配置

我们使用手动定义、扫描两种方式来注册Bean。后面会讲解xml配置是怎么样变成Bean对象的。在这只要知道怎么样可以定义就可以了。

spring-beans.xml

使用原始的xml配置bean的方式。

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
    <!--定义一个bean-->
    <bean id="helloService" class="cn.dingyufan.demo.spring.service.HelloService"/>
    
</beans>
复制代码

spring-mvc.xml

使用扫描的方式配置bean

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:mvc="http://www.springframework.org/schema/mvc"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/mvc https://www.springframework.org/schema/mvc/spring-mvc.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">
    
    <mvc:annotation-driven/>
    <!--扫描base-package下的bean-->
    <context:component-scan base-package="cn.dingyufan.demo.spring.controller"/>
    
</beans>
复制代码

运行验证

启动tomcat,控制台打印了两个bean注入的applicationContext,可以看出是不同的Spring容器。

image.png

请求 localhost:8080/spring-demo/hello,成功返回

image.png

请求 localhost:8080/spring-demo/check,返回了hashcode,说明helloService注入成功。

image.png

结语

连贯起来看使用了Spring + SpringMVC的web项目的启动顺序:

  1. 服务器启动,创建ServletContext,并读取web.xml
  2. 创建listener实例,ContextLoaderListener得到通知后创建一个Spring容器并初始化
  3. 创建Filter实例
  4. 创建Servlet实例,DispatcherServlet创建一个Spring子容器并初始化,做mvc相关配置

本文的web服务,和前一篇相比有很多相似之处,也有不同之处。但是可以看出,Spring + SpringMVC底层其实也是Servlet那一套,它是在Servlet的基础上,扩充了许多功能,然后自身形成了一套概念、理论。所以Spring源码其实并不神秘,学习Spring源码,就是学习Spring是如何设计这样一套框架的。知其然,然后知其所以然。先了解源码是什么样,然后思考源码为什么是这样。下一篇我们来看Spring容器初始化到底做了什么。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享