目录
2.1 根SpringWeb容器(Root WebApplication)
2.2 Servlet SpringWeb容器(Servlet WebApplicationContext)
4.SpringMVC REST服务端:@RestController
一、静态资源的放行
中央控制器DispatcherServlet配置在应用的入口配置中(一般配置在web.xml)在Servlet3.0之后,可以实现WebApplicationInitializer接口替代。DispatcherServlet中需要配置拦截的请求匹配,出啊同配置可拦截*.do和*.action后缀的请求,结合参数可以实现多层级的匹配,比如在控制器类中配置.do的请求映射,在方法中通过参数进行第二层匹配。如下:
@Controller
@RequestMapping("user.do")
public class UserController {
@RequestMapping(params = "mytype=create")
public ModelAndView create() {
return new ModelAndView("user");
}
}
浏览器请求user.do?mytype=create即可访问user.jsp,这种配置方式对.jsp及其他静态文件是不拦截的,但是存在安全隐患。在前后端分离框架中,特别是REST风格的资源请求方式流行后,推荐使用“/”作为DispatcherServlet拦截的地址匹配,改配置方式不会拦截,jsp文件和.jspx文件,不过这种配置方式会对js,*.png等静态文件的访问进行拦截。虽然会拦截静态资源,但是这种配置方式会先在应用入口配置(比如web.xml)中查找静态资源匹配的Servlet处理,如果没有找到,才会将请求交给DispatcherServlet处理。所以使用“/”作为DispatcherServlet的映射匹配,才可以配合其他设置对静态资源放行,比如使用服务器本身的静态资源处理的defaultServlet或者使用SpringMVC提供的方式。
1.配置Servlet处理静态资源
Java Web服务器本身维护了一个默认的Servlet用来处理请求,配置路径映射让静态资源交由默认的Servlet处理,而不经由DispatcherServlet拦截处理。以运行在Tomcat服务器的应用配置为例,在web.xml中增加<servlet-mapping>配置,servlet-name为default,url-pattern配置为需要放行的静态资源的后缀名匹配,如下:
<servlet-mapping>
<servlet-name>default</servlet-name><!--默认的Servlet名字-->
<url-pattern>*.jpg</url-pattern> <!--静态资源后缀名匹配-->
<url-pattern>*.js</url-pattern>
<url-pattern>*.html</url-pattern>
<url-pattern>*.css</url-pattern>
<url-pattern>*.gif</url-pattern>
</servlet-mapping>
<!--该配置需要配置在DispatcherServlet前面,让defaultServlet先拦截请求-->
这种配置方式比较直接,由Web服务器直接处理,不经过Spring,性能也好。但不同的服务器,默认Servlet名字不同。常见的Web服务器默认的Servlet名字如下:
- Tomcat、Jetty、JBoss和GlassFish:default
- Google App Engine:ah_default
- Resin:resin-file
- WebLogic:FileServlet
- WebSphere:SimpleFileServlet
如果应用程序需要部署在不同类型的服务器上,或者可能出现服务器转换,那么这种配置方式存在兼容性问题。为了兼容不同的服务器,SpringMVC提供了统一的处理方式。
2.配置<mvc:default-servlet-handler />放行动态资源
SpringMVC为了兼容不同的服务器,对defaultServlet封装了统一的接口,直接在SpringMVC配置文件中增加这一条配置即可。
<mvc:default-servlet-handler />
上面的这种配置方式会把“/**”的URL注册到SimpleUrlHandlerMapping的URLMap中,静态资源的访问由HandlerMapping转到DefaultServletHttpRequestHandler处理并返回。这种方式其实最终也是由DefaultServlet来处理,只是统一使用Spring提供的DefaultServletHttpRequestHandler来查找对应服务器默认defaultServlet。通过这种方式,就不用担心应用部署在不同服务器上时会出现兼容性问题了。
3.配置<mvc:resources>放行动态资源
<mvc:resources location="/images/" mapping="/images/**"></mvc:resources>
<mvc:resources location="/css/" mapping="/css/**"></mvc:resources>
<mvc:resources location="/js/" mapping="/js/**"></mvc:resources>
从Spring3.0.4开始提供了<mvc:resources>标签来解决静态资源无法访问的问题,这种方式请求会交予ResourceHttpRequestHandler类来处理,这种方式更加灵活、细化,而且对资源访问路径和实际路径做了一层映射(location指定的是资源的实际的路径,mapping为映射路径,就是url访问时的路径),更加安全。
二、父子容器
在基于Spring核心框架的应用中,使用不同的配置文件可以初始化多个容器对象,并且可以将某个容器对象作为另一个容器创建的参数来设定两者之间的父子层关系。子容器能够获取父容器中管理的Bean,父容器则无法使用子容器的Bean。在SpringMVC中默认提供了父子容器的设定方式,用来实现不同层的Bean进行管理。
1.Spring的父子容器
Spring容器的主要作用是对Bean进行生命周期的管理。在同一个应用中可以同时存在多个容器对象。以ClassPathXmlApplicationContext初始化容器的方式为例:
public static void main(String[] args) {
ApplicationContext applicationContext1 = new ClassPathXmlApplicationContext("applicationContext1.xml");
ApplicationContext applicationContext2 = new ClassPathXmlApplicationContext("applicationContext2.xml");
}
以上两个容器是平行关系,也可以设置容器的层级关系。可通过如下构造方法构造父子层级结构的容器:
public ClassPathXmlApplicationContext(String[] configLocations, @Nullable ApplicationContext parent) throws BeansException {
this(configLocations, true, parent);
}
设定父子容器可以实现应用上下文的隔离。在桌面应用中,重写第三方库是父子容器的应用场景之一。
下面的内容为XXX.jar中的内容
@Service
public class UserService {
@Autowired
private UserDao userDao;
public void setUserDao(UserDao userDao) {
this.userDao = userDao;
}
public String get() {
return userDao.getUserName("");
}
}
public interface UserDao {
String getUserName(String id);
}
@Repository
public class ParentUsrDao implements UserDao {
@Override
public String getUserName(String id) {
return "Parent User Name";
}
}
如下为该XXX.jar文件中的Spring核心文件配置:
<beans> <!--省略命名空间及文档位置定义的配置-->
<context:component-scan base-package="com.mec.spring"></context:component-scan>
</beans>
如果我们现在需要使用该第三方包,只需要在当前项目中使用import导入第三方包的Spring配置文件即可,如下:
<import resource="classpath*:parent.xml"/>
<!--通过classpath查找.jar文件时需要加星号,也就是classpath*-->
如果此时需要改写UserService中的get()方法的逻辑,也就是修改ParentUsrDao中的get()方法的逻辑。在一般应用中有如下两种处理方式:
- 如果有源码,修改ParentUsrDao类后,将修改后的类编译后替换原.jar文件中的ParentUsrDao.class文件。
- 如果没有源码,则解压.jar文件并反编译后取得源码,然后再进行修改和替换。
如果是基于Spring依赖注入的容器应用中,自定义一个继承自UserDao接口的实现类,让UserService使用这个类的Bean,则问题就可以得到解决。但是如果直接定义一个UserDao接口的实现类然后启动容器,那么容器可能会启动失败。(因为在UserService中是使用@Autowired自动装配UserDao接口的实现类,@Autowired是通过byType的方式注入的,之前只有一个实现类ParentUsrDao的时候注入没问题,当我们又自定义了一个UserDao实现类的时候,Spring就会报错,因为他不知道该注入哪一个实现类)。我们不更改第三方库中的代码,此时就可以使用父子容器来解决。
因为子容器对父容器不可视,所以在子容器中注册的组件不会和父容器中发生冲突。这里需要对配置文件做一些调整,当前Spring项目的核心配置文件中不再使用import导入parent.xml,而是通过容器对象初始化代码设置容器的层级关系,在子容器中获取UserService的Bean之后,设置其依赖的UserDao的Bean。如下:
public static void main(String[] args) {
ApplicationContext parentContext = new ClassPathXmlApplicationContext("classpath*:parent.xml");
ApplicationContext childContext = new ClassPathXmlApplicationContext(new String[] { "child.xml" }, parentContext);
UserService userService = (UserService) childContext.getBean("userService");
UserDao childUserDao = (UserDao) childContext.getBean("childUserDao");
userService.setUserDao(childUserDao);
}
容器本身也是对象,也可以通过配置Bean的方式进行配置。父子容器也可以通过XML进行配置,比如配置文件名为parent-child.xml,配置示例如下:
<bean id="parentContext" class="org.springframework.context.support.ClassPathXmlApplicationContext">
<constructor-arg>
<value>
<!-- 构造器方式指定配置文件 -->
classpath*:parent.xml
</value>
</constructor-arg>
</bean>
<bean id="childContext" class="org.springframework.context.support.ClassPathXmlApplicationContext">
<constructor-arg>
<value>
<!-- 构造器方式指定配置文件 -->
classpath:child.xml
</value>
</constructor-arg>
<constructor-arg>
<!-- 注入父容器 -->
<ref bean="parentContext" />
</constructor-arg>
</bean>
2.Spring MVC的父子容器
在平常练习开发中对于SSM项目我们可以只是用一个Spring核心配置文件,对应一个容器实例来初始化即可,但是有时候我们可能需要两个Spring核心配置文件,其中一个来配置管理控制层的Bean,另一个来配置管理其他层的Bean,这两个配置文件,就可以对应两个容器对象实例,那么这两个容器对象就可以构建父子关系,所以这个时候我们就需要SpringMVC的父子容器。在SpringMVC项目中,通过DispatcherServlet的contextConfigLocation或contextClass参数指定配置文件或配置类来初始化Spring Web容器(这个可以用于管理控制层的Bean),还可以在web.xml中使用<listener>元素来初始化容器(这个可以用于管理其他层的Bean)。这两个容器都是WebApplicationContext类的实例(SpringMVC项目中我们都是使用它的实例来加载配置初始化容器)。为了区分这两个容器,将它们分别取名为Servlet Spring Web容器和根 Spring Web容器。Spring MVC父子容器的关系如下图(下图展示的是依靠web.xml来配置两个容器的方式,当然还有不使用web.xml的配置方式):
2.1 根SpringWeb容器(Root WebApplication)
根SpringWeb容器也叫SpringWeb父容器。该容器通过在项目入口配置(比如web.xml)中添加监听器(<listener>)进行配置。监听器类使用的是ContextLoaderListener,该类由Spring提供,继承自ServletContextListener标准接口(用于监听ServletContext的生命周期,该接口定义了ServletContext初始化和销毁的回调方法)和上下文加载类ContextLoader。ContextLoaderListener在ServletContext初始化时进行容器初始化,初始化配置文件使用contextConfigLocation参数配置,多个配置文件使用逗号分隔,如果不进行该参数的配置,则默认会查找/WEB-INF/applicationContext.xml。
<web-app version="3.0" xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaeehttp://java.sun.com/xml/ns/javaee/web-app_3_0.xsd">
<!-- 上下文加载监听器 -->
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<!-- 创建Root WebApplicationContext -->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/spring/applicationContext.xml</param-value>
</context-param>
</web-app>
容器被加载后,会被放入ServletContext对象中,键的值为org.springframework.web.context.WebApplicationContext.ROOT
2.2 Servlet SpringWeb容器(Servlet WebApplicationContext)
Servlet Spring Web容器也称为Spring Web子容器,在中央控制器DispatcherServlet加载时初始化。DispatcherServlet在创建该容器时,会先从ServletContext查找根容器。如果找到的话,则设为父容器;如果没有找到,则不设置父容器。该容器对象默认维护在DispatcherServlet中,也可以维护在ServletContext中。
维护在DispatcherServlet对象中,DispatcherServlet在处理请求时,会把这个子上下文保存到Request对象中,键:org.springframework.web.servlet.DispatcherServlet.CONTEXT。通过设置publishContext属性的值可以将其放入ServletContext对象中,键:org.springframework.web.servlet.FrameworkServlet.CONTEXT.{Servlet名字}
DispatcherServlet同样使用contextConfigLocation参数设置XML的Spring配置文件,如果不指定,默认使用/WEB-INF/{dispatcherServletName}-servlet.xml,而dispatcherServletName是在web.xml中配置的<servlet-name>的值。在web.xml中配置示例如下:
<servlet>
<servlet-name>dispatcher</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/spring/springmvc.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup> <!--默认启动-->
</servlet>
<servlet-mapping>
<servlet-name>dispatcher</servlet-name>
<url-pattern>/*</url-pattern>
</servlet-mapping>
3.Spring MVC父子容器的配置
Spring MVC的父子容器都可以用来配置组件Bean,遵循子容器可以调用父容器的Bean,但父容器不可以调用子容器的Bean的规则。通过父子容器的上下文隔离特性,实现分层解耦。基于Spring+SpringMVC的应用,也可以不使用父子容器,而只在子容器。为了保持简洁性,中小型项目使用DispatcherServlet层级的容器即可;大型项目则进行父子容器的拆分。
SpringMVC中父子容器根据Web层和非Web层进行设定,两个容器管理不同类型的Bean。
- 父容器主要存放数据源、Dao层、服务层和事务等非Web的组件;
- 子容器存放控制器、处理器映射,处理器适配等Web层的组件。
可以通过<context:component-scan />的组件扫描配置元素结合<context:exclude-filter>和<context:include-filter> 的子元素对组件类进行筛选。以基于注解的开发为例,在父容器的核心配置文件(默认名为applicationContext.xml)中可以进行如下配置:
<context:component-scan base-package="com.mec.springmvc">
<!-- 排除@Controller注解组件 -->
<context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
</context:component-scan>
在子容器的配置(默认名是{dispatcherServletName}-servlet.xml)中,仅包含@Controller注解的控制类,配置如下:
<context:component-scan base-package="com.mec.springmvc">
<!-- 仅包含@Controller注解组件 -->
<context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
</context:component-scan>
<mvc:annotation-driven /> <!--web注解驱动开启,处理请求映射等注解-->
在使用Java替换web.xml的入口配置方式中,通过继承AbstractDispatcherServletInitializer类可以分别实现父子容器的初始化方法,如下:
//入口配置类
public class MvcWebApplicationInitializer extends AbstractDispatcherServletInitializer {
// 初始化子容器
@Override
protected WebApplicationContext createServletApplicationContext() {
XmlWebApplicationContext applicationContext = new XmlWebApplicationContext();
applicationContext.setConfigLocation("/WEB-INF/springdispatcherconfig.xml");
return applicationContext;
}
// 路径拦截匹配
@Override
protected String[] getServletMappings() {
return new String[] { "/" };
}
// 初始化父容器
@Override
protected WebApplicationContext createRootApplicationContext() {
XmlWebApplicationContext applicationContext = new XmlWebApplicationContext();
applicationContext.setConfigLocation("/WEB-INF/spring/applicationContext.xml");
return applicationContext;
}
}
如果应用的入口配置也使用Java类,也就是实现零XML配置,则入口配置类继承类AbstractAnnotationConfigDispatcherServletInitializer,对应父子容器的方法返回容器配置类即可,如下:
public class MyAnnoWebApplicationInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
// 返回父容器配置类集合
@Override
protected Class<?>[] getRootConfigClasses() {
return new Class<?>[] { MyAppConfig.class };
}
@Override
protected Class<?>[] getServletConfigClasses() {
return new Class<?>[] { MyAppWebConfig.class };
}
@Override
protected String[] getServletMappings() {
return new String[] { "/" };
}
}
三、SpringMVC与REST
REST是Resource Representational State Transfer这几个单词的缩写,表述起来就是资源在网络中以某种表现形式进行状态转移,该概念在前后端分离概念中非常适用:
- Resource:资源,也就是数据
- Representational:表现层,也就是表现形式。类似于JSON和XML等格式的表现形式
- State Transfer:状态变化对应到HTTP的GET、POST、PUT和DELETE等请求方法
REST是一种软件架构风格,其本质上是使用URL来访问资源的风格定义。我们先来看一下HTTP的请求方法,HTTP协议定义了不同类型的请求方法(HTTP动词),包括GET、POST、HEAD、OPTIONS、PUT、DELETE、TRACE和CONNECT。目的在于用这些不同的请求方法来表示不同的数据(资源)操作,比如增、删、改、查等操作。在基于REST风格的应用中,我们需要正确使用HTTP动词,来表明我们要如何操作资源。常用的请求方法及数据库方法的对应关系如下:
HTTP请求方法 | 方法名 | 数据库操作方法 | 描述 |
GET | 查 | REDA | 从服务器取出资源(一项或多项) |
POST | 增 | CREATE | 在服务器上新建一个资源 |
PUT | 改 | UPDATE | 在服务器上更新资源(某个资源的完整更新) |
PATCH | 改 | UPDATE | 在服务器上更新资源(某个资源的局部更新,比如部分属性) |
DELETE | 删 | DELETE | 从服务器上删除指定资源 |
相比数据库使用SQL语句对资源进行读取等操作,REST定义的是URL来访问资源的风格,但REST不是强制的规范和接口,它不是协议,也不是规范,而是一种基于URL资源的访问风格,也就是想让我们按照这种风格去合理使用URL。
1.REST风格定义
REST很好地利用了HTTP请求类型和URL地址定义对资源进行访问。URL路径中的资源名使用复数,路径中不使用动词。以某个企业应用中对公司部门数据的操作为例,REST风格的服务URL如下表:
方法 | URL | 描述 |
GET | /depts | 查找所有部门 |
GET | /depts/{deptId} | 获取某个部门的信息 |
POST | /depts | 创建一个新的部门 |
PATCH | /depts/{deptId} | 更新某个指定部门的部分信息 |
PUT | /depts/{deptId} | 更新某个部门的所有信息 |
DELETE | /depts/{deptId} | 删除某个部门 |
注意:PUT和PATCH虽然都是更新资源,但PUT方法是更新整个资源,而PATCH方法是更新资源的局部信息。例如,上表中,PUT需要更新的话,则需要将Dept的所有信息都传入,没有的字段就应该被清空,而PATCH则只会更新传入的字段,没有的不变。
有时需要对该资源关联的子资源进行操作,则可以定义关联资源操作的URL。以部门下的用户操作为例,如下表:
方法 | URL | 描述 |
GET | /depts/{deptId}/users | 查找某个部门的所有用户 |
GET | /depts/{deptId}/users/{userId} | 获取某个部门的某个用户 |
POST | /depts/{deptId}/users | 创建某个部门的新用户 |
PATCH | /depts/{deptId}/users/{userId} | 局部更新某个部门的某个用户 |
PUT | /depts/{deptId}/users/{userId} | 更新某个部门中某个用户的所有信息 |
DELETE | /depts/{deptId}/users/{userId} | 删除某个部门的某个用户 |
2.请求方法的幂等性
幂等性这个概念对应HTTP的请求操作就是不管执行多少次,最后资源的结果都是一样的。
方法 | 是否幂等 | 说明 |
GET | 是 | 获取资源,不管调用多少次接口,结果的内容都是一样的 |
POST | 否 | 每次调用都将产生新的资源 |
PATCH | 否 | 比如部门有一个栏位是更新时间,这个栏位是由系统自动设置为当前时间,每次调用PATCH方法时这个栏位的结果不一样 |
PUT | 是 | 多次调用,所有栏位都全部更新 |
DELETE | 是 | 对某个资源删除多少次,结果都是一样的 |
3.请求响应状态码
200是常见的操作成功的响应状态码。除200之外,不同的请求方法成功返回的状态码是不一样的。REST常见的请求方法返回的成功状态码如下表:
请求方法 | 成功状态码 |
GET | 200 |
POST | 201 |
PATCH | 201 |
PUT | 200 |
DELETE | 204 |
除了成功的状态码,不同类型的请求在发生客户端或服务端错误时,也会返回不同的响应状态码和状态信息,由此可以快速地对问题进行定位和解决。常见地HTTP请求状态码和状态信息如下表
4.SpringMVC REST服务端:@RestController
在控制器类上使用@RestController注解来定义REST风格地请求服务,该注解是@Controller和@ResponseBody的组合注解,除了请求方法返回JSON格式数据外,开发中最好做到控制器类的请求映射地址符合REST风格,即每个控制器类对应一种资源类型,请求映射方法包括查询、添加、修改和删除。以用户操作的控制类为例,如下:
@RestController
public class PersonController {
public List<Person> personList = new ArrayList<>();
// 构造方法中初始化数据
public PersonController() {
personList.add(new Person(1, "Zhang"));
}
@GetMapping("/persons")
public Object getAll() {
return personList;
}
@GetMapping("/persons/{id}")
public Object getOne(@PathVariable("id") Integer id) {
Person person = null;
for (Person p : personList) {
if (p.getId() == id) {
person = p;
break;
}
}
return person;
}
@PostMapping("/persons")
public Object addPerson(@RequestBody Person p) {
personList.add(p);
return personList;
}
@PutMapping("/persons/{id}") // 修改该用户的所有属性
public Object modify(@PathVariable("id") Integer id, @RequestBody Person person) {
for (Person p : personList) {
if (p.getId() == id) {
p.setName(person.getName());
}
}
return person;
}
@DeleteMapping("/persons/{id}")
public Object delete(@PathVariable("id") Integer id) {
for (Person p : personList) {
if (p.getId() == id) {
personList.remove(p);
}
}
return personList;
}
}