实战: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,现在回到自己动手找依赖包的时候,也是有些不风俗了= =。
4.0.0 cn.dingyufan spring-demo 1.0-SNAPSHOT spring-demo war 1.8 1.8 5.2.10.RELEASE javax.servlet javax.servlet-api 4.0.1 org.springframework spring-core ${spring.version} org.springframework spring-beans ${spring.version} org.springframework spring-context ${spring.version} org.springframework spring-web ${spring.version} org.springframework spring-webmvc ${spring.version} org.apache.maven.plugins maven-war-plugin 2.4 复制代码编码
我们这里就简朴的写一个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;@RestControllerpublic class HelloController implements ApplicationContextAware { private ApplicationContext applicationContext; @Autowired private HelloService helloService; @GetMapping("/hello") public ResponseEntity hello(HttpServletRequest request) { return ResponseEntity.ok("Hello Spring!"); } @GetMapping("/check") public ResponseEntity 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之后也必要配置这些内容,但是和之前的配置有些许差别。
org.springframework.web.context.ContextLoaderListener contextConfigLocation /WEB-INF/spring-beans.xml encodingFilter org.springframework.web.filter.CharacterEncodingFilter encoding UTF-8 forceEncoding true encodingFilter /* dispatcherServlet org.springframework.web.servlet.DispatcherServlet contextConfigLocation WEB-INF/spring-mvc.xml 1 dispatcherServlet /* 复制代码在这个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的方式。
复制代码spring-mvc.xml
使用扫描的方式配置bean
复制代码运行验证
启动tomcat,控制台打印了两个bean注入的applicationContext,可以看出是不同的Spring容器。
请求 localhost:8080/spring-demo/hello,乐成返回
请求 localhost:8080/spring-demo/check,返回了hashcode,阐明helloService注入乐成。
结语
本文我们使用Spring + SpringMVC来搭建了一个web服务,和前一篇相比有很多相似之处,也有很多不同之处。但是可以看出,Spring + SpringMVC底层实在也是Servlet那一套,它是在Servlet的基础上,扩充了许多功能,然后自身形成了一套概念、理论。所以Spring源码实在并不神秘,学习Spring源码,就是学习Spring是如何设计这样一套框架的。知其然,然后知其所以然。先了解源码是什么样,然后思考源码为什么是这样。 |