前言
上一篇写了使用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的编码。配置这个过滤器是为了和前文自己写的做个对比。
通过他的继承关系,可以看到他和我们前文RandomFilter一样,也实现了Filter
接口。核心的doFilter()方法由父类OncePerRequestFilter实现。父类OncePerRequestFilter在doFilter()中写了自己的逻辑,并调用了doFilterInternal()方法。这个方法在OncePerRequestFilter是一个空方法,作用是留给子类实现子类自己的逻辑。这种做法是一种设计模式,模板方法模式,在Spring源码中很常见。
所以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)
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容器。
请求 localhost:8080/spring-demo/hello,成功返回
请求 localhost:8080/spring-demo/check,返回了hashcode,说明helloService注入成功。
结语
连贯起来看使用了Spring + SpringMVC的web项目的启动顺序:
- 服务器启动,创建ServletContext,并读取web.xml
- 创建listener实例,ContextLoaderListener得到通知后创建一个Spring容器并初始化
- 创建Filter实例
- 创建Servlet实例,DispatcherServlet创建一个Spring子容器并初始化,做mvc相关配置
本文的web服务,和前一篇相比有很多相似之处,也有不同之处。但是可以看出,Spring + SpringMVC底层其实也是Servlet那一套,它是在Servlet的基础上,扩充了许多功能,然后自身形成了一套概念、理论。所以Spring源码其实并不神秘,学习Spring源码,就是学习Spring是如何设计这样一套框架的。知其然,然后知其所以然。先了解源码是什么样,然后思考源码为什么是这样。下一篇我们来看Spring容器初始化到底做了什么。