Spring注解驱动开发之MVC
1 SpringMVC注解驱动开发
1.1 基于Servlet3.0的环境搭建
1.1.1 导入坐标
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.23</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.2.8.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>5.2.8.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>5.2.8.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.2.8.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>5.2.8.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>5.2.8.RELEASE</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.21</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-instrument</artifactId>
<version>5.2.8.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>5.2.8.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.2.8.RELEASE</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.servlet.jsp</groupId>
<artifactId>jsp-api</artifactId>
<version>2.2</version>
</dependency>
1.1.2 编写控制器
- HelloController.java
package com.sunxiaping.web;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
public class HelloController {
@RequestMapping(value = "/hello")
public String sayHello() {
System.out.println("控制器执行了");
return "success";
}
}
1.1.3 编写配置类
- SpringConfig.java
package com.sunxiaping.config;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Controller;
/**
* Spring的配置类,替代了applicationContext.xml
*/
@Configuration
@ComponentScan(value = "com.sunxiaping",excludeFilters = @ComponentScan.Filter(value = Controller.class))
public class SpringConfig {
}
- SpringMvcConfig.java
package com.sunxiaping.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Controller;
import org.springframework.web.servlet.ViewResolver;
import org.springframework.web.servlet.view.InternalResourceViewResolver;
/**
* SpringMVC的配置类,用于替代springmvc.xml
*/
@Configuration
@ComponentScan(value = "com.sunxiaping", useDefaultFilters = false, includeFilters = @ComponentScan.Filter(value = Controller.class))
@EnableWebMvc
public class SpringMvcConfig {
@Bean
public ViewResolver viewResolver(){
InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
viewResolver.setPrefix("/WEB-INF/views/");
viewResolver.setSuffix(".jsp");
return viewResolver;
}
}
- config.java
package com.sunxiaping.config;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
import org.springframework.web.filter.CharacterEncodingFilter;
import org.springframework.web.servlet.support.AbstractDispatcherServletInitializer;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
/**
* 初始化Spring和SpringMVC 容器的配置类
*/
public class config extends AbstractDispatcherServletInitializer {
/**
* 注册字符集过滤器
*
* @param servletContext
* @throws ServletException
*/
@Override
public void onStartup(ServletContext servletContext) throws ServletException {
super.onStartup(servletContext);
CharacterEncodingFilter filter = new CharacterEncodingFilter();
filter.setEncoding("utf-8");
FilterRegistration.Dynamic filterRegistration = servletContext.addFilter("characterEncodingFilter", filter);
filterRegistration.addMappingForUrlPatterns(EnumSet.of(DispatcherType.INCLUDE, DispatcherType.FORWARD, DispatcherType.REQUEST), false, "/*");
}
/**
* 用于创建SpringMVC的IOC容器
*
* @return
*/
@Override
protected WebApplicationContext createServletApplicationContext() {
AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
context.register(SpringMvcConfig.class);
return context;
}
@Override
protected String[] getServletMappings() {
return new String[]{"/"};
}
/**
* 用于创建Spring的IOC容器
*
* @return
*/
@Override
protected WebApplicationContext createRootApplicationContext() {
AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
context.register(SpringConfig.class);
return context;
}
}
1.1.4 编写页面
- index.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>首页</title>
</head>
<body>
<a href="${pageContext.request.contextPath}/hello">SpringMVC基于Servlet3.0规范纯注解开发入门</a>
</body>
</html>
- success.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Title</title>
</head>
<body>
成功啦
</body>
</html>
1.2 入门案例执行过程分析
1.2.1 Servlet3.0规范加入的内容
- ServletContainerInitializer.java
/**
* Servlet3.0规范提供的接口
*/
public interface ServletContainerInitializer {
/**
* 启动容器时做一些初始化工作,比如注册Servlet、Filter以及Listener等
*/
public void onStartup(Set<Class<?>> c, ServletContext ctx)
throws ServletException;
}
- HandlesTypes.java
/**
* 用于指定要加载到ServletContainerInitializer接口实现类中的字节码
*/
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface HandlesTypes {
/**
* 指定要加载到ServletContainerInitializer实现类的onStartUp方法中类的字节码
*/
Class<?>[] value();
}
任何要使用Servlet3.0规范且脱离web.xml的配置,在使用的时候都必须在对应的jar包的MATA-INF/services目录创建一个名为javax.servlet.ServletContainerInitializer的文件,文件内容指定具体的ServletContainerInitializer的实现类,那么,当web容器启动的时候就会运行这个初始化器做一些组件内的初始化工作。
1.2.2 config类中的onStartUp方法
- config.java
package com.sunxiaping.config;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
import org.springframework.web.filter.CharacterEncodingFilter;
import org.springframework.web.servlet.support.AbstractDispatcherServletInitializer;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
/**
* 初始化Spring和SpringMVC 容器的配置类
*/
public class config extends AbstractDispatcherServletInitializer {
/**
* 注册字符集过滤器
*
* @param servletContext
* @throws ServletException
*/
@Override
public void onStartup(ServletContext servletContext) throws ServletException {
super.onStartup(servletContext);
CharacterEncodingFilter filter = new CharacterEncodingFilter();
filter.setEncoding("utf-8");
FilterRegistration.Dynamic filterRegistration = servletContext.addFilter("characterEncodingFilter", filter);
filterRegistration.addMappingForUrlPatterns(EnumSet.of(DispatcherType.INCLUDE, DispatcherType.FORWARD, DispatcherType.REQUEST), false, "/*");
}
/**
* 用于创建SpringMVC的IOC容器
*
* @return
*/
@Override
protected WebApplicationContext createServletApplicationContext() {
AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
context.register(SpringMvcConfig.class);
return context;
}
@Override
protected String[] getServletMappings() {
return new String[]{"/"};
}
/**
* 用于创建Spring的IOC容器
*
* @return
*/
@Override
protected WebApplicationContext createRootApplicationContext() {
AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
context.register(SpringConfig.class);
return context;
}
}
1.2.3 AbstractDispatcherServletInitializer中的onStartUp方法
- AbstractDispatcherServletInitializer.java
public abstract class AbstractDispatcherServletInitializer extends AbstractContextLoaderInitializer {
public static final String DEFAULT_SERVLET_NAME = "dispatcher";
@Override
public void onStartup(ServletContext servletContext) throws ServletException {
//执行父类的onStartup方法
super.onStartup(servletContext);
//注册DispatcherServlet
registerDispatcherServlet(servletContext);
}
/**
* 注册DispatcherServlet
*/
protected void registerDispatcherServlet(ServletContext servletContext) {
String servletName = getServletName();
Assert.hasLength(servletName, "getServletName() must not return null or empty");
//创建表现层的IOC容器
WebApplicationContext servletAppContext = createServletApplicationContext();
Assert.notNull(servletAppContext, "createServletApplicationContext() must not return null");
//创建DispatcherServlet对象
FrameworkServlet dispatcherServlet = createDispatcherServlet(servletAppContext);
Assert.notNull(dispatcherServlet, "createDispatcherServlet(WebApplicationContext) must not return null");
dispatcherServlet.setContextInitializers(getServletApplicationContextInitializers());
ServletRegistration.Dynamic registration = servletContext.addServlet(servletName, dispatcherServlet);
if (registration == null) {
throw new IllegalStateException("Failed to register servlet with name '" + servletName + "'. " +
"Check if there is another servlet registered under the same name.");
}
registration.setLoadOnStartup(1);
registration.addMapping(getServletMappings());
registration.setAsyncSupported(isAsyncSupported());
Filter[] filters = getServletFilters();
if (!ObjectUtils.isEmpty(filters)) {
for (Filter filter : filters) {
registerServletFilter(servletContext, filter);
}
}
customizeRegistration(registration);
}
/**
* Return the name under which the {@link DispatcherServlet} will be registered.
* Defaults to {@link #DEFAULT_SERVLET_NAME}.
* @see #registerDispatcherServlet(ServletContext)
*/
protected String getServletName() {
return DEFAULT_SERVLET_NAME;
}
/**
* 创建ServletApplicationContex
*/
protected abstract WebApplicationContext createServletApplicationContext();
/**
* 创建DispatcherServlet对象
*/
protected FrameworkServlet createDispatcherServlet(WebApplicationContext servletAppContext) {
return new DispatcherServlet(servletAppContext);
}
@Nullable
protected ApplicationContextInitializer<?>[] getServletApplicationContextInitializers() {
return null;
}
/**
* 设置Servlet映射
*/
protected abstract String[] getServletMappings();
//略
}
1.2.4 注册DispatcherServlet
- AbstractContextLoaderInitializer.java
public abstract class AbstractContextLoaderInitializer implements WebApplicationInitializer {
/** Logger available to subclasses. */
protected final Log logger = LogFactory.getLog(getClass());
@Override
public void onStartup(ServletContext servletContext) throws ServletException {
// 调用注册ContextLoaderListener方法
registerContextLoaderListener(servletContext);
}
/**
* 创建根容器,并注册ContextLoaderListener
*/
protected void registerContextLoaderListener(ServletContext servletContext) {
WebApplicationContext rootAppContext = createRootApplicationContext();
if (rootAppContext != null) {
ContextLoaderListener listener = new ContextLoaderListener(rootAppContext);
listener.setContextInitializers(getRootApplicationContextInitializers());
servletContext.addListener(listener);
}
else {
logger.debug("No ContextLoaderListener registered, as " +
"createRootApplicationContext() did not return an application context");
}
}
}
2 常用注解说明
2.1 基础注解
2.1.1 @Controller注解
源码
- @Controller注解:
//此注解是用来修饰表现层控制器的注解
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Controller {
/**
* 用于指定存入IOC容器的Bean的唯一标识
*/
@AliasFor(annotation = Component.class)
String value() default "";
}
应用示例
- 示例:
package com.sunxiaping.web;
import org.springframework.stereotype.Controller;
@Controller
public class HelloController {
}
2.1.2 @RequestMapping注解
源码
- @RequestMapping注解:
//此注解用于建立请求URLhe处理请求方法之间的对应关系
//属性只要出现2个或以上的时候,它们的关系是并列的关系。表示必须同时满足条件。
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Mapping
public @interface RequestMapping {
/**
* 用于给请求URL提供一个名称
*/
String name() default "";
/**
* 用于指定请求的URL。它和path属性的作用是一样的
*/
@AliasFor("path")
String[] value() default {};
/**
* 和value属性是一样的
*/
@AliasFor("value")
String[] path() default {};
/**
* 用于指定请求的方式
*/
RequestMethod[] method() default {};
/**
* 用于指定限制请求参数的条件。
*/
String[] params() default {};
/**
* 用于指定限制请求消息头的条件
*/
String[] headers() default {};
/**
* 用于指定可以接收的请求正文类型(MIME类型)
* 例如: consumes = "text/plain"
*/
String[] consumes() default {};
/**
* 用于指定可以生成的响应正文类型(MIME类型)
* 例如: produces = "text/plain"
*/
String[] produces() default {};
}
应用示例
- 示例:
package com.sunxiaping.web;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import java.util.List;
@Controller
@RequestMapping(value ="/sys/user")
public class SysUserController {
@RequestMapping(value = "/pageList")
public List<String> pageList(){
return null;
}
}
衍生注解
- @GetMapping注解:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@RequestMapping(method = RequestMethod.GET)
public @interface GetMapping {
/**
* Alias for {@link RequestMapping#name}.
*/
@AliasFor(annotation = RequestMapping.class)
String name() default "";
/**
* Alias for {@link RequestMapping#value}.
*/
@AliasFor(annotation = RequestMapping.class)
String[] value() default {};
/**
* Alias for {@link RequestMapping#path}.
*/
@AliasFor(annotation = RequestMapping.class)
String[] path() default {};
/**
* Alias for {@link RequestMapping#params}.
*/
@AliasFor(annotation = RequestMapping.class)
String[] params() default {};
/**
* Alias for {@link RequestMapping#headers}.
*/
@AliasFor(annotation = RequestMapping.class)
String[] headers() default {};
/**
* Alias for {@link RequestMapping#consumes}.
* @since 4.3.5
*/
@AliasFor(annotation = RequestMapping.class)
String[] consumes() default {};
/**
* Alias for {@link RequestMapping#produces}.
*/
@AliasFor(annotation = RequestMapping.class)
String[] produces() default {};
}
- @PostMapping注解:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@RequestMapping(method = RequestMethod.POST)
public @interface PostMapping {
/**
* Alias for {@link RequestMapping#name}.
*/
@AliasFor(annotation = RequestMapping.class)
String name() default "";
/**
* Alias for {@link RequestMapping#value}.
*/
@AliasFor(annotation = RequestMapping.class)
String[] value() default {};
/**
* Alias for {@link RequestMapping#path}.
*/
@AliasFor(annotation = RequestMapping.class)
String[] path() default {};
/**
* Alias for {@link RequestMapping#params}.
*/
@AliasFor(annotation = RequestMapping.class)
String[] params() default {};
/**
* Alias for {@link RequestMapping#headers}.
*/
@AliasFor(annotation = RequestMapping.class)
String[] headers() default {};
/**
* Alias for {@link RequestMapping#consumes}.
*/
@AliasFor(annotation = RequestMapping.class)
String[] consumes() default {};
/**
* Alias for {@link RequestMapping#produces}.
*/
@AliasFor(annotation = RequestMapping.class)
String[] produces() default {};
}
- @PutMapping注解:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@RequestMapping(method = RequestMethod.PUT)
public @interface PutMapping {
/**
* Alias for {@link RequestMapping#name}.
*/
@AliasFor(annotation = RequestMapping.class)
String name() default "";
/**
* Alias for {@link RequestMapping#value}.
*/
@AliasFor(annotation = RequestMapping.class)
String[] value() default {};
/**
* Alias for {@link RequestMapping#path}.
*/
@AliasFor(annotation = RequestMapping.class)
String[] path() default {};
/**
* Alias for {@link RequestMapping#params}.
*/
@AliasFor(annotation = RequestMapping.class)
String[] params() default {};
/**
* Alias for {@link RequestMapping#headers}.
*/
@AliasFor(annotation = RequestMapping.class)
String[] headers() default {};
/**
* Alias for {@link RequestMapping#consumes}.
*/
@AliasFor(annotation = RequestMapping.class)
String[] consumes() default {};
/**
* Alias for {@link RequestMapping#produces}.
*/
@AliasFor(annotation = RequestMapping.class)
String[] produces() default {};
}
- @DeleteMapping注解:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@RequestMapping(method = RequestMethod.DELETE)
public @interface DeleteMapping {
/**
* Alias for {@link RequestMapping#name}.
*/
@AliasFor(annotation = RequestMapping.class)
String name() default "";
/**
* Alias for {@link RequestMapping#value}.
*/
@AliasFor(annotation = RequestMapping.class)
String[] value() default {};
/**
* Alias for {@link RequestMapping#path}.
*/
@AliasFor(annotation = RequestMapping.class)
String[] path() default {};
/**
* Alias for {@link RequestMapping#params}.
*/
@AliasFor(annotation = RequestMapping.class)
String[] params() default {};
/**
* Alias for {@link RequestMapping#headers}.
*/
@AliasFor(annotation = RequestMapping.class)
String[] headers() default {};
/**
* Alias for {@link RequestMapping#consumes}.
*/
@AliasFor(annotation = RequestMapping.class)
String[] consumes() default {};
/**
* Alias for {@link RequestMapping#produces}.
*/
@AliasFor(annotation = RequestMapping.class)
String[] produces() default {};
}
- @PatchMapping注解:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@RequestMapping(method = RequestMethod.PATCH)
public @interface PatchMapping {
/**
* Alias for {@link RequestMapping#name}.
*/
@AliasFor(annotation = RequestMapping.class)
String name() default "";
/**
* Alias for {@link RequestMapping#value}.
*/
@AliasFor(annotation = RequestMapping.class)
String[] value() default {};
/**
* Alias for {@link RequestMapping#path}.
*/
@AliasFor(annotation = RequestMapping.class)
String[] path() default {};
/**
* Alias for {@link RequestMapping#params}.
*/
@AliasFor(annotation = RequestMapping.class)
String[] params() default {};
/**
* Alias for {@link RequestMapping#headers}.
*/
@AliasFor(annotation = RequestMapping.class)
String[] headers() default {};
/**
* Alias for {@link RequestMapping#consumes}.
*/
@AliasFor(annotation = RequestMapping.class)
String[] consumes() default {};
/**
* Alias for {@link RequestMapping#produces}.
*/
@AliasFor(annotation = RequestMapping.class)
String[] produces() default {};
}
2.1.3 @RequestParam注解
源码
- @RequestParam注解:
//此注解是从请求正文中获取请求参数,给控制器方法形参赋值的。
//当请求参数的名称和控制器方法形参变量名称一致的时候,无须使用此注解。
//当没有获取到请求参数的时候,此注解还可以给控制器方法形参提供默认值
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RequestParam {
/**
* Alias for {@link #name}.
*/
@AliasFor("name")
String value() default "";
/**
* 将请求参数绑定到修饰的属性上
* @since 4.2
*/
@AliasFor("value")
String name() default "";
/**
* 指定参数是否必须有值。如果为true,参数没有值会报错。
*/
boolean required() default true;
/**
* 在参数没有值时的默认值
*/
String defaultValue() default ValueConstants.DEFAULT_NONE;
}
应用示例
- 示例:
package com.sunxiaping.web;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import java.util.List;
@Controller
@RequestMapping(value = "/sys/user")
public class SysUserController {
/**
* 分页显示系统用户信息
*
* @param pageSize 页码
* @param pageNum 每页显示条数
* @return
*/
@RequestMapping(value = "/pageList")
public List<String> pageList(
@RequestParam(value = "pageSize", defaultValue = "1") Integer pageSize,
@RequestParam(value = "pageNum", defaultValue = "10") Integer pageNum
) {
return null;
}
}
2.1.4 @InitBinder注解
源码
- @InitBinder注解:
//此注解用于初始化表单请求参数的数据绑定器
//如果写在某个控制器里面,那么其作用范围就是这个控制器。
//如果想扩大范围,就需要写到@ControllerAdvice注解标注的类中
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface InitBinder {
/**
* 指定给那些参数进行绑定操作
*/
String[] value() default {};
}
应用示例
- User.java
package com.sunxiaping.spring5.domain;
import java.io.Serializable;
import java.util.Date;
public class User implements Serializable {
private Integer id;
private String username;
private String password;
private Integer age;
private String gender;
private Date birthday;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
public String getGender() {
return gender;
}
public void setGender(String gender) {
this.gender = gender;
}
public Date getBirthday() {
return birthday;
}
public void setBirthday(Date birthday) {
this.birthday = birthday;
}
}
- index.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Title</title>
</head>
<body>
<form action="${pageContext.request.contextPath}/sys/user/save" method="post">
姓名:<input type="text" name="username"/><br>
密码:<input type="password" name="password"><br>
年龄:<input type="text" name="age"><br>
性别:<input type="text" name="gender"><br>
出生年月:<input type="text" name="birthday"><br>
<input type="submit" value="提交"/>
</form>
</body>
</html>
- SysUserController.java
package com.sunxiaping.web;
import com.sunxiaping.domain.User;
import org.springframework.format.datetime.DateFormatter;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.InitBinder;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
@RequestMapping(value = "/sys/user")
public class SysUserController {
@RequestMapping(value = "/save")
public String save(User user){
System.out.println(user);
return "success";
}
@InitBinder(value = "user")
public void initBinder(WebDataBinder webDataBinder){
webDataBinder.addCustomFormatter(new DateFormatter("yyyy-MM-dd"),"birthday");
}
}
衍生注解
- 在实际开发中,@InitBinder并不能灵活的处理时间,这个时候可以使用@DateTimeFormat注解标注了实体类上,但是需要在SpringMvcConfig.java中加上@EnableMvc注解。
- User.java
package com.sunxiaping.domain;
import org.springframework.format.annotation.DateTimeFormat;
import java.io.Serializable;
import java.util.Date;
public class User implements Serializable {
private Integer id;
private String username;
private String password;
private Integer age;
private String gender;
@DateTimeFormat(pattern = "yyyy-MM-dd")
private Date birthday;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
public String getGender() {
return gender;
}
public void setGender(String gender) {
this.gender = gender;
}
public Date getBirthday() {
return birthday;
}
public void setBirthday(Date birthday) {
this.birthday = birthday;
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", username='" + username + '\'' +
", password='" + password + '\'' +
", age=" + age +
", gender='" + gender + '\'' +
", birthday=" + birthday +
'}';
}
}
- SpringMvcConfig.java
package com.sunxiaping.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Controller;
import org.springframework.web.servlet.ViewResolver;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.resource.VersionResourceResolver;
import org.springframework.web.servlet.view.InternalResourceViewResolver;
@Configuration
@ComponentScan(value = "com.sunxiaping", useDefaultFilters = false, includeFilters = @ComponentScan.Filter(classes = Controller.class))
@EnableWebMvc
public class SpringMvcConfig implements WebMvcConfigurer {
/**
* 配置视图解析器
*
* @return
*/
@Bean
public ViewResolver viewResolver() {
InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
viewResolver.setPrefix("/WEB-INF/views/");
viewResolver.setSuffix(".jsp");
return viewResolver;
}
/**
* 配置静态资源
*
* @param registry
*/
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/js/**")
.addResourceLocations("/js/")
.resourceChain(true)
.addResolver(new VersionResourceResolver().addContentVersionStrategy("/**"));
}
}
2.1.5 @ControllerAdvice注解
源码
- @ControllerAdvice注解:
//用于给控制器提供一个增强的通知,以保证可以在多个控制器之间实现增强共享。
//它可以配置@ExceptionHandler、@InitBinder和@ModelAttribute注解一起使用
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface ControllerAdvice {
/**
* 用于指定对那些包下的控制器进行增强
*/
@AliasFor("basePackages")
String[] value() default {};
/**
* 和value属性的作用一样
*/
@AliasFor("value")
String[] basePackages() default {};
/**
* 通过指定类的字节码的方式来指定增强作用
*/
Class<?>[] basePackageClasses() default {};
/**
* 用于指定给特定类型提供增强
*/
Class<?>[] assignableTypes() default {};
/**
* 用于指定给特定注解提供增强
*/
Class<? extends Annotation>[] annotations() default {};
}
应用示例
- 示例:
- index.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Title</title>
</head>
<body>
<form action="${pageContext.request.contextPath}/sys/user/save" method="post">
姓名:<input type="text" name="username"/><br>
密码:<input type="password" name="password"><br>
年龄:<input type="text" name="age"><br>
性别:<input type="text" name="gender"><br>
出生年月:<input type="text" name="birthday"><br>
<input type="submit" value="提交"/>
</form>
</body>
</html>
- SysUserController.java
package com.sunxiaping.web;
import com.sunxiaping.domain.User;
import org.springframework.format.datetime.DateFormatter;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.InitBinder;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
@RequestMapping(value = "/sys/user")
public class SysUserController {
@RequestMapping(value = "/save")
public String save(User user){
Integer age = user.getAge();
if(null == age){
throw new RuntimeException("age年龄必须填写");
}
if(age <=0 || age >= 200){
throw new RuntimeException("你不是人");
}
return "success";
}
}
- SpringMVC5ControllerAdvice.java
package com.sunxiaping.advice;
import org.springframework.format.datetime.DateFormatter;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.InitBinder;
import java.util.List;
@ControllerAdvice
public class SpringMVC5ControllerAdvice {
@ExceptionHandler(value = RuntimeException.class)
public void handleRunTimeException(RuntimeException ex) {
String message = ex.getMessage();
System.out.println("message = " + message);
}
@InitBinder
public void initBinder(WebDataBinder webDataBinder){
webDataBinder.addCustomFormatter(new DateFormatter("yyyy-MM-dd"),"birthday");
}
@ExceptionHandler(value = Exception.class)
public void handleException(Exception ex, BindingResult result) {
String message = ex.getMessage();
System.out.println("message = " + message);
List<FieldError> fieldErrors = result.getFieldErrors();
for (FieldError fieldError : fieldErrors) {
String defaultMessage = fieldError.getDefaultMessage();
System.out.println("defaultMessage = " + defaultMessage);
}
}
}
2.1.6 @RequestHeader注解
源码
- @RequestHeader注解:
//此注解是从请求消息头中获取消息头的信息,并把值赋值给控制器的方法的形参
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RequestHeader {
/**
* 用于指定请求消息头的名称,和name属性作用一样
*/
@AliasFor("name")
String value() default "";
/**
* 和value属性的作用一样
*/
@AliasFor("value")
String name() default "";
/**
* 用于指定是否必须有此消息头。如果为true,没有此消息头会报错
*/
boolean required() default true;
/**
* 用于指定消息头的默认值。
*/
String defaultValue() default ValueConstants.DEFAULT_NONE;
}
应用示例
- 示例:
package com.sunxiaping.web;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
public class RequestHeaderController {
@RequestMapping(value = "/test")
public String test(@RequestHeader(value = "Accept-Language",required = false) String requestHeader){
System.out.println("requestHeader = " + requestHeader);
return "success";
}
}
2.1.7 @CookieValue注解
源码
- @CookieValue注解:
//此注解是从请求消息头中获取Cookie的值,并把值赋给控制器方法形参
//此注解只能出现在方法形参上
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface CookieValue {
/**
* 指定Cookie的名称
*/
@AliasFor("name")
String value() default "";
/**
* 和value属性一样
*/
@AliasFor("value")
String name() default "";
/**
* 用于指定是否必须有此消息头。如果为true,没有此消息头会报错
*/
boolean required() default true;
/**
* 用于指定消息头的默认值
*/
String defaultValue() default ValueConstants.DEFAULT_NONE;
}
应用示例
- 示例:
package com.sunxiaping.web;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.CookieValue;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
public class CookieValueController {
@RequestMapping(value = "/test")
public String test(@CookieValue(value = "JSESSIONID",required = false) String jsessionId){
System.out.println("jsessionId = " + jsessionId);
return "success";
}
}
2.1.8 @ModelAttribute注解
源码
- @ModelAttribute注解:
//此注解可以用于修饰方法和参数。
//如果修饰的是方法,表示执行控制器方法之前,被此注解修饰的方法都会执行。
//当修饰参数的时候,用于获取指定的数据给参数赋值
@Target({ElementType.PARAMETER, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ModelAttribute {
/**
* 当注解写在方法上,则表示存入的名称。
* 当注解写在参数上,可以从ModelMap、Model、Map中获取数据。前提之前存入过。
* 指定的是存入的key。
*/
@AliasFor("name")
String value() default "";
/**
* 和value属性的作用一样。
*/
@AliasFor("value")
String name() default "";
/**
* 用于指定是否支持数据绑定
*/
boolean binding() default true;
}
应用示例
- 示例:
package com.sunxiaping.web;
import com.sunxiaping.domain.User;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import java.util.Date;
@Controller
@RequestMapping(value = "/sys/user")
public class SysUserController {
@ModelAttribute
public User findById(Integer id){
User user = new User();
user.setId(id);
user.setUsername("admin");
user.setPassword("123456");
user.setGender("男");
user.setAge(11);
user.setBirthday(new Date());
return new User();
}
@RequestMapping("/update")
public String update(@ModelAttribute User user){
System.out.println(user);
return "success";
}
}
2.1.9 @SessionAttribute和@SessionAttributes注解
源码
- @SessionAttribute注解:
//此注解是用于让开发者和Servlet的API进行解耦
//让开发者可以无需使用HttpSession的getAttribute方法即可从会话域中获取数据
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SessionAttribute {
/**
* 用于指定在会话域中数据的名称
*/
@AliasFor("name")
String value() default "";
/**
* 和value的属性作用一样
*/
@AliasFor("value")
String name() default "";
/**
* 用于指定是否必须从会话域中获取到数据。如果为true,表示指定名称不存在就报错
*/
boolean required() default true;
}
- @SessionAttributes注解:
//此注解是用于让开发者和Servlet的API进行解耦
//通过此注解即可实现把数据存入到会话域中,而无需再使用HttpSession的setAttribute方法
//当我们在控制器方法形参中加入Model或者ModelMap类型参数的时候,默认是存入到请求域中的
//但是当控制器中使用了此注解,就会向会话域中添加数据
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface SessionAttributes {
/**
* 指定可以存入会话域中的名称
*/
@AliasFor("names")
String[] value() default {};
/**
* 和value属性的作用是一样
*/
@AliasFor("value")
String[] names() default {};
/**
* 指定可以存入会话域中的数据类型
*/
Class<?>[] types() default {};
}
应用示例
- 略。
2.1.10 @ExceptionHandler注解
源码
- @ExceptionHandler注解:
//用于标注方法,表明当前方法是控制器执行产生异常后的处理方法
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ExceptionHandler {
/**
* 执行用于需要捕获的异常类型
*/
Class<? extends Throwable>[] value() default {};
}
应用示例
- 略。
2.2 JSON数据交互相关注解
2.2.1 @ResponseBody注解
源码
- @ResponseBody注解:
//用于用流输出响应正文
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ResponseBody {
}
应用示例
- 示例:
package com.sunxiaping.web;
import com.sunxiaping.domain.User;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
@RequestMapping(value = "/sys/user")
public class SysUserController {
@ResponseBody
public String update(User user){
System.out.println(user);
return "success";
}
}
2.2.2 @RequestBody注解
源码
- @RequestBody注解:
//用于获取全部的请求体,如果想将json封装到对应的实体类中,使用此注解时需要导入jackson的jar包
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RequestBody {
/**
* 用于指定是否必须有请求体
*/
boolean required() default true;
}
应用示例
- 示例:
package com.sunxiaping.web;
import com.sunxiaping.domain.User;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
@RequestMapping(value = "/sys/user")
public class SysUserController {
@PostMapping(value = "/save")
public String save(@RequestBody User user){
return "success";
}
}
2.2.3 @RestController注解
源码
- @RestController注解:
//此注解具备了@Controller注解的全部功能,同时多了@ResponseBody注解的功能
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Controller
@ResponseBody
public @interface RestController {
/**
* 用于指定存入IOC容器的Bean的id
*/
@AliasFor(annotation = Controller.class)
String value() default "";
}
应用示例
- 略。
2.2.4 @RestControllerAdvice注解
源码
- @RestControllerAdvice注解:
//此注解的作用和@ControllerAdvice一样,并且支持@ResponseBody的功能
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@ControllerAdvice
@ResponseBody
public @interface RestControllerAdvice {
@AliasFor(annotation = ControllerAdvice.class)
String[] value() default {};
@AliasFor(annotation = ControllerAdvice.class)
String[] basePackages() default {};
@AliasFor(annotation = ControllerAdvice.class)
Class<?>[] basePackageClasses() default {};
@AliasFor(annotation = ControllerAdvice.class)
Class<?>[] assignableTypes() default {};
@AliasFor(annotation = ControllerAdvice.class)
Class<? extends Annotation>[] annotations() default {};
}
应用示例
- 略。
2.3 Rest风格URL请求相关注解
2.3.1 @PathVariable注解
源码
- @PathVariable注解:
//此注解是SpringMVC框架支持REST风格URL的标识
//它可以用于获取请求URL映射中占位符对应的值
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface PathVariable {
/**
* 执行URL映射中占位符的名称
*/
@AliasFor("name")
String value() default "";
/**
* 和value属性的作用一样
*/
@AliasFor("value")
String name() default "";
/**
* 用于指定是否必须有此占位符,如果为true,没有会报错
*/
boolean required() default true;
}
应用示例
- 示例:
package com.sunxiaping.web;
import com.sunxiaping.domain.User;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping(value = "/sys/user")
public class SysUserController {
@GetMapping(value = "/view/{id}")
public User view(@PathVariable("id") Integer id) {
User user = new User();
user.setId(id);
user.setUsername("lisi");
user.setPassword("123456");
return user;
}
}
2.4 跨域访问
2.4.1 关于跨域访问
- 浏览器从一个域名的网页去请求另一个域名的资源时,域名、端口、协议任一不同,都是跨域访问。
2.4.2 自定义过滤器实现跨域访问
- CrossOriginFilter.java
package com.sunxiaping.filter;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class CrossOriginFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
try {
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse resp = (HttpServletResponse) response;
//允许跨域的主机地址
resp.setHeader("Access-Control-Allow-Origin","*");
//允许跨域的请求方法GET、PUT、POST、HEAD等
resp.setHeader("Access-Control-Allow-Methods","*");
//重新预检测跨域的缓存时间
resp.setHeader("Access-Control-Max-Age","3600");
//允许跨域的请求头
resp.setHeader("Access-Control-Allow-Headers","*");
//是否携带cookie
resp.setHeader("Access-Control-Allow-Credentials","true");
chain.doFilter(req, resp);
}catch (Exception e){
e.printStackTrace();
}
}
@Override
public void destroy() {
}
}
- 将CrossOriginFilter注册到SpringMVC的容器中:
package com.sunxiaping.config;
import com.sunxiaping.filter.CrossOriginFilter;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
import org.springframework.web.filter.CharacterEncodingFilter;
import org.springframework.web.servlet.support.AbstractDispatcherServletInitializer;
import javax.servlet.DispatcherType;
import javax.servlet.FilterRegistration;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import java.util.EnumSet;
/**
* 初始化Spring和SpringMVC 容器的配置类
*/
public class config extends AbstractDispatcherServletInitializer {
/**
* 注册字符集过滤器
*
* @param servletContext
* @throws ServletException
*/
@Override
public void onStartup(ServletContext servletContext) throws ServletException {
super.onStartup(servletContext);
//编码过滤器
CharacterEncodingFilter filter = new CharacterEncodingFilter();
filter.setEncoding("utf-8");
FilterRegistration.Dynamic filterRegistration = servletContext.addFilter("characterEncodingFilter", filter);
filterRegistration.addMappingForUrlPatterns(EnumSet.of(DispatcherType.INCLUDE, DispatcherType.FORWARD, DispatcherType.REQUEST), false, "/*");
//跨域过滤器
CrossOriginFilter crossOriginFilter = new CrossOriginFilter();
FilterRegistration.Dynamic filterRegistration2 = servletContext.addFilter("crossOriginFilter", crossOriginFilter);
filterRegistration2.addMappingForUrlPatterns(EnumSet.of(DispatcherType.INCLUDE, DispatcherType.FORWARD, DispatcherType.REQUEST), false, "/*");
}
/**
* 用于创建SpringMVC的IOC容器
*
* @return
*/
@Override
protected WebApplicationContext createServletApplicationContext() {
AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
context.register(SpringMvcConfig.class);
return context;
}
@Override
protected String[] getServletMappings() {
return new String[]{"/"};
}
/**
* 用于创建Spring的IOC容器
*
* @return
*/
@Override
protected WebApplicationContext createRootApplicationContext() {
AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
context.register(SpringConfig.class);
return context;
}
}
2.4.3 在SpringMVC中一次性开启跨域访问
- SpringMvcConfig.java
package com.sunxiaping.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Controller;
import org.springframework.web.servlet.ViewResolver;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.view.InternalResourceViewResolver;
/**
* SpringMVC的配置类,用于替代springmvc.xml
*/
@Configuration
@ComponentScan(value = "com.sunxiaping", useDefaultFilters = false, includeFilters = @ComponentScan.Filter(value = Controller.class))
@EnableWebMvc
public class SpringMvcConfig implements WebMvcConfigurer {
@Bean
public ViewResolver viewResolver() {
InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
viewResolver.setPrefix("/WEB-INF/views/");
viewResolver.setSuffix(".jsp");
return viewResolver;
}
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*")
.allowCredentials(true)
.allowedMethods("GET", "POST", "DELETE", "PUT", "PATCH")
.maxAge(3600);
}
}
2.4.4 使用@CrossOrigin注解开启跨域访问
源码
- @CrossOrigin注解:
//此注解用于指定是否支持跨域访问
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface CrossOrigin {
/** @deprecated as of Spring 5.0, in favor of {@link CorsConfiguration#applyPermitDefaultValues} */
@Deprecated
String[] DEFAULT_ORIGINS = {"*"};
/** @deprecated as of Spring 5.0, in favor of {@link CorsConfiguration#applyPermitDefaultValues} */
@Deprecated
String[] DEFAULT_ALLOWED_HEADERS = {"*"};
/** @deprecated as of Spring 5.0, in favor of {@link CorsConfiguration#applyPermitDefaultValues} */
@Deprecated
boolean DEFAULT_ALLOW_CREDENTIALS = false;
/** @deprecated as of Spring 5.0, in favor of {@link CorsConfiguration#applyPermitDefaultValues} */
@Deprecated
long DEFAULT_MAX_AGE = 1800;
/**
* 和origins的属性一样
*/
@AliasFor("origins")
String[] value() default {};
/**
* "*"代表所有域的请求都支持
* 默认所有请求的域都支持
*/
@AliasFor("value")
String[] origins() default {};
/**
* 允许请求头中的header,默认都支持
*/
String[] allowedHeaders() default {};
/**
* 响应头中允许访问的header,默认为空
*/
String[] exposedHeaders() default {};
/**
* 用于指定支持的HTTP请求方式列表
*/
RequestMethod[] methods() default {};
/**
* 是否允许cookie随请求发送,使用的时候必须指定具体的域
*/
String allowCredentials() default "";
/**
* 预请求的结果的有效期,默认是30分钟
*/
long maxAge() default -1;
}
应用示例
- 示例:
package com.sunxiaping.web;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@CrossOrigin
public class CrossOriginController {
@RequestMapping(value = "/hello")
public String sayHello() {
System.out.println("控制器执行了");
return "success";
}
}
3 SpringMVC中各组件详解及源码分析
3.1 前端控制器DispatcherServlet
3.1.1 作用
- 用户请求到达前端控制,它就相当于MVC模式中的C,DispatcherServlet是整个流程空旷感知的中心,由它调用其他组件处理用户的请求,DispatcherServlet的存在降低了组件之间的耦合性。
3.1.2 执行过程分析
3.1.2.1 DispatcherServlet的类图
从上图中,我们可以知道DispatcherServlet其实就是HttpServlet,用Tomcat等web容器启动,一旦浏览器发送了请求,会经过HttpServlet的service方法,这个方法内部会判断请求方法的请求方式,然后调用对应的方法,例如请求方式是GET请求,就调用doGet方法。DispatcherServlet的父类是FrameworkServlet,而FrameworkServlet必定会重写service方法,在重写的service方法中,调用了processRequest方法,而processRequest方法内部调用了doService方法,doService是个抽象方法,由子类实现,所以,DispatcherServlet会调用doService方法。
3.1.2.2 doService方法
- doService方法:
/**
* Exposes the DispatcherServlet-specific request attributes and delegates to {@link #doDispatch}
* for the actual dispatching.
*/
@Override
protected void doService(HttpServletRequest request, HttpServletResponse response) throws Exception {
logRequest(request);
// Keep a snapshot of the request attributes in case of an include,
// to be able to restore the original attributes after the include.
Map<String, Object> attributesSnapshot = null;
if (WebUtils.isIncludeRequest(request)) {
attributesSnapshot = new HashMap<>();
Enumeration<?> attrNames = request.getAttributeNames();
while (attrNames.hasMoreElements()) {
String attrName = (String) attrNames.nextElement();
if (this.cleanupAfterInclude || attrName.startsWith(DEFAULT_STRATEGIES_PREFIX)) {
attributesSnapshot.put(attrName, request.getAttribute(attrName));
}
}
}
// Make framework objects available to handlers and view objects.
request.setAttribute(WEB_APPLICATION_CONTEXT_ATTRIBUTE, getWebApplicationContext());
request.setAttribute(LOCALE_RESOLVER_ATTRIBUTE, this.localeResolver);
request.setAttribute(THEME_RESOLVER_ATTRIBUTE, this.themeResolver);
request.setAttribute(THEME_SOURCE_ATTRIBUTE, getThemeSource());
if (this.flashMapManager != null) {
FlashMap inputFlashMap = this.flashMapManager.retrieveAndUpdate(request, response);
if (inputFlashMap != null) {
request.setAttribute(INPUT_FLASH_MAP_ATTRIBUTE, Collections.unmodifiableMap(inputFlashMap));
}
request.setAttribute(OUTPUT_FLASH_MAP_ATTRIBUTE, new FlashMap());
request.setAttribute(FLASH_MAP_MANAGER_ATTRIBUTE, this.flashMapManager);
}
try {
doDispatch(request, response);
}
finally {
if (!WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted()) {
// Restore the original attribute snapshot, in case of an include.
if (attributesSnapshot != null) {
restoreAttributesAfterInclude(request, attributesSnapshot);
}
}
}
}
doService方法内部调用了doDispatch方法,这是处理请求分发的核心方法。
3.1.2.3 doDispatch方法
- doDispatch方法:
/**
* 处理请求分发的核心方法
* 它负责通过反射调用我们的控制器方法
* 负责执行拦截器
* 负责处理结果视图
*/
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null;
boolean multipartRequestParsed = false;
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
try {
ModelAndView mv = null;
Exception dispatchException = null;
try {
processedRequest = checkMultipart(request);
multipartRequestParsed = (processedRequest != request);
// Determine handler for the current request.
mappedHandler = getHandler(processedRequest);
if (mappedHandler == null) {
noHandlerFound(processedRequest, response);
return;
}
// Determine handler adapter for the current request.
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
// Process last-modified header, if supported by the handler.
String method = request.getMethod();
boolean isGet = "GET".equals(method);
if (isGet || "HEAD".equals(method)) {
long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {
return;
}
}
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}
// Actually invoke the handler.
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
if (asyncManager.isConcurrentHandlingStarted()) {
return;
}
applyDefaultViewName(processedRequest, mv);
mappedHandler.applyPostHandle(processedRequest, response, mv);
}
catch (Exception ex) {
dispatchException = ex;
}
catch (Throwable err) {
// As of 4.3, we're processing Errors thrown from handler methods as well,
// making them available for @ExceptionHandler methods and other scenarios.
dispatchException = new NestedServletException("Handler dispatch failed", err);
}
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
}
catch (Exception ex) {
triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
}
catch (Throwable err) {
triggerAfterCompletion(processedRequest, response, mappedHandler,
new NestedServletException("Handler processing failed", err));
}
finally {
if (asyncManager.isConcurrentHandlingStarted()) {
// Instead of postHandle and afterCompletion
if (mappedHandler != null) {
mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
}
}
else {
// Clean up any resources used by a multipart request.
if (multipartRequestParsed) {
cleanupMultipart(processedRequest);
}
}
}
}
3.2 处理器映射器HanderMapping
3.2.1 作用
- HandlerMapping负责根据用户请求找到Hander即处理器,SpringMVC提供了不同的映射器实现不同的映射方式,例如:配置文件方式、实现接口方式以及注解方式等。
3.2.2 执行过程分析
- Servlet启动的时候会调用init方法进行初始化。HttpServlet没有重写init方法。
- HttpServlet的子类HttpServletBean重写了init方法,其源码如下:
public abstract class HttpServletBean extends HttpServlet implements EnvironmentCapable, EnvironmentAware {
@Override
public final void init() throws ServletException {
// Set bean properties from init parameters.
PropertyValues pvs = new ServletConfigPropertyValues(getServletConfig(), this.requiredProperties);
if (!pvs.isEmpty()) {
try {
BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this);
ResourceLoader resourceLoader = new ServletContextResourceLoader(getServletContext());
bw.registerCustomEditor(Resource.class, new ResourceEditor(resourceLoader, getEnvironment()));
initBeanWrapper(bw);
bw.setPropertyValues(pvs, true);
}
catch (BeansException ex) {
if (logger.isErrorEnabled()) {
logger.error("Failed to set bean properties on servlet '" + getServletName() + "'", ex);
}
throw ex;
}
}
// 调用initServletBean方法
initServletBean();
}
//initServletBean方法是空方法,由子类实现
protected void initServletBean() throws ServletException {
}
}
- FrameworkServlet是HttpServletBean的子类,重写了initServletBean方法,其源码如下:
public abstract class FrameworkServlet extends HttpServletBean implements ApplicationContextAware {
/**
* Overridden method of {@link HttpServletBean}, invoked after any bean properties
* have been set. Creates this servlet's WebApplicationContext.
*/
@Override
protected final void initServletBean() throws ServletException {
getServletContext().log("Initializing Spring " + getClass().getSimpleName() + " '" + getServletName() + "'");
if (logger.isInfoEnabled()) {
logger.info("Initializing Servlet '" + getServletName() + "'");
}
long startTime = System.currentTimeMillis();
try {
//调用 initWebApplicationContext方法
this.webApplicationContext = initWebApplicationContext();
initFrameworkServlet();
}
catch (ServletException | RuntimeException ex) {
logger.error("Context initialization failed", ex);
throw ex;
}
if (logger.isDebugEnabled()) {
String value = this.enableLoggingRequestDetails ?
"shown which may lead to unsafe logging of potentially sensitive data" :
"masked to prevent unsafe logging of potentially sensitive data";
logger.debug("enableLoggingRequestDetails='" + this.enableLoggingRequestDetails +
"': request parameters and headers will be " + value);
}
if (logger.isInfoEnabled()) {
logger.info("Completed initialization in " + (System.currentTimeMillis() - startTime) + " ms");
}
}
/**
* Initialize and publish the WebApplicationContext for this servlet.
* <p>Delegates to {@link #createWebApplicationContext} for actual creation
* of the context. Can be overridden in subclasses.
* @return the WebApplicationContext instance
* @see #FrameworkServlet(WebApplicationContext)
* @see #setContextClass
* @see #setContextConfigLocation
*/
protected WebApplicationContext initWebApplicationContext() {
WebApplicationContext rootContext =
WebApplicationContextUtils.getWebApplicationContext(getServletContext());
WebApplicationContext wac = null;
if (this.webApplicationContext != null) {
// A context instance was injected at construction time -> use it
wac = this.webApplicationContext;
if (wac instanceof ConfigurableWebApplicationContext) {
ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) wac;
if (!cwac.isActive()) {
// The context has not yet been refreshed -> provide services such as
// setting the parent context, setting the application context id, etc
if (cwac.getParent() == null) {
// The context instance was injected without an explicit parent -> set
// the root application context (if any; may be null) as the parent
cwac.setParent(rootContext);
}
configureAndRefreshWebApplicationContext(cwac);
}
}
}
if (wac == null) {
// No context instance was injected at construction time -> see if one
// has been registered in the servlet context. If one exists, it is assumed
// that the parent context (if any) has already been set and that the
// user has performed any initialization such as setting the context id
wac = findWebApplicationContext();
}
if (wac == null) {
// No context instance is defined for this servlet -> create a local one
wac = createWebApplicationContext(rootContext);
}
if (!this.refreshEventReceived) {
// Either the context is not a ConfigurableApplicationContext with refresh
// support or the context injected at construction time had already been
// refreshed -> trigger initial onRefresh manually here.
synchronized (this.onRefreshMonitor) {
//调用onRefresh方法
onRefresh(wac);
}
}
if (this.publishContext) {
// Publish the context as a servlet context attribute.
String attrName = getServletContextAttributeName();
getServletContext().setAttribute(attrName, wac);
}
return wac;
}
//onRefresh是个空方法,有子类实现
protected void onRefresh(ApplicationContext context) {
// For subclasses: do nothing by default.
}
}
- DispatcherServlet是FrameworkServlet的子类,重写了onRefresh方法,其源码如下:
@SuppressWarnings("serial")
public class DispatcherServlet extends FrameworkServlet {
/**
* This implementation calls {@link #initStrategies}.
*/
@Override
protected void onRefresh(ApplicationContext context) {
initStrategies(context);
}
/**
* Initialize the strategy objects that this servlet uses.
* <p>May be overridden in subclasses in order to initialize further strategy objects.
*/
protected void initStrategies(ApplicationContext context) {
initMultipartResolver(context);
initLocaleResolver(context);
initThemeResolver(context);
initHandlerMappings(context);
initHandlerAdapters(context);
initHandlerExceptionResolvers(context);
initRequestToViewNameTranslator(context);
initViewResolvers(context);
initFlashMapManager(context);
}
//初始化HandlerMappings
private void initHandlerMappings(ApplicationContext context) {
this.handlerMappings = null;
if (this.detectAllHandlerMappings) {
// Find all HandlerMappings in the ApplicationContext, including ancestor contexts.
Map<String, HandlerMapping> matchingBeans =
BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerMapping.class, true, false);
if (!matchingBeans.isEmpty()) {
this.handlerMappings = new ArrayList<>(matchingBeans.values());
// We keep HandlerMappings in sorted order.
AnnotationAwareOrderComparator.sort(this.handlerMappings);
}
}
else {
try {
HandlerMapping hm = context.getBean(HANDLER_MAPPING_BEAN_NAME, HandlerMapping.class);
this.handlerMappings = Collections.singletonList(hm);
}
catch (NoSuchBeanDefinitionException ex) {
// Ignore, we'll add a default HandlerMapping later.
}
}
// Ensure we have at least one HandlerMapping, by registering
// a default HandlerMapping if no other mappings are found.
if (this.handlerMappings == null) {
this.handlerMappings = getDefaultStrategies(context, HandlerMapping.class);
if (logger.isTraceEnabled()) {
logger.trace("No HandlerMappings declared for servlet '" + getServletName() +
"': using default strategies from DispatcherServlet.properties");
}
}
}
}
3.3 处理器适配器
3.3.1 作用
- 适配器模式就是把一个类的接口变换成客户端所期待的另一种接口,从而使原本因为接口原因不匹配而无法一起工作的两个类能够一起工作。适配类可以根据参数返回一个合适的实例给客户端。
- 通过HandlerAdapter对处理器进行执行,这是适配器模式的应用,通过扩展适配器可以对更多类型的处理器进行执行。
3.3.2 执行过程分析
- 3.1.2.3分析的doDispatch方法中有如下的代码:
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
- 一般而言,我们现在是使用注解方式来标注控制器,使用@RequestMapping注解来映射请求,所以对应的HandlerAdapter是RequestMappingHandlerAdapter。
- HandlerAdapter的源码如下:
public interface HandlerAdapter {
/**
* Given a handler instance, return whether or not this {@code HandlerAdapter}
* can support it. Typical HandlerAdapters will base the decision on the handler
* type. HandlerAdapters will usually only support one handler type each.
* <p>A typical implementation:
* <p>{@code
* return (handler instanceof MyHandler);
* }
* @param handler the handler object to check
* @return whether or not this object can use the given handler
*/
boolean supports(Object handler);
/**
* Use the given handler to handle this request.
* The workflow that is required may vary widely.
* @param request current HTTP request
* @param response current HTTP response
* @param handler the handler to use. This object must have previously been passed
* to the {@code supports} method of this interface, which must have
* returned {@code true}.
* @throws Exception in case of errors
* @return a ModelAndView object with the name of the view and the required
* model data, or {@code null} if the request has been handled directly
*/
@Nullable
ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception;
/**
* Same contract as for HttpServlet's {@code getLastModified} method.
* Can simply return -1 if there's no support in the handler class.
* @param request current HTTP request
* @param handler the handler to use
* @return the lastModified value for the given handler
* @see javax.servlet.http.HttpServlet#getLastModified
* @see org.springframework.web.servlet.mvc.LastModified#getLastModified
*/
long getLastModified(HttpServletRequest request, Object handler);
}
- 通过上面的源码可知,HandlerAdapter是一个接口。而AbstractHandlerMethodAdapter是其子类。其源码如下:
public abstract class AbstractHandlerMethodAdapter extends WebContentGenerator implements HandlerAdapter, Ordered {
private int order = Ordered.LOWEST_PRECEDENCE;
public AbstractHandlerMethodAdapter() {
// no restriction of HTTP methods by default
super(false);
}
/**
* Specify the order value for this HandlerAdapter bean.
* <p>The default value is {@code Ordered.LOWEST_PRECEDENCE}, meaning non-ordered.
* @see org.springframework.core.Ordered#getOrder()
*/
public void setOrder(int order) {
this.order = order;
}
@Override
public int getOrder() {
return this.order;
}
/**
* This implementation expects the handler to be an {@link HandlerMethod}.
* @param handler the handler instance to check
* @return whether or not this adapter can adapt the given handler
*/
@Override
public final boolean supports(Object handler) {
return (handler instanceof HandlerMethod && supportsInternal((HandlerMethod) handler));
}
/**
* Given a handler method, return whether or not this adapter can support it.
* @param handlerMethod the handler method to check
* @return whether or not this adapter can adapt the given method
*/
protected abstract boolean supportsInternal(HandlerMethod handlerMethod);
/**
* 重写父类的handle方法
*/
@Override
@Nullable
public final ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
//handleInternal是抽象方法,由子类实现
return handleInternal(request, response, (HandlerMethod) handler);
}
@Nullable
protected abstract ModelAndView handleInternal(HttpServletRequest request,
HttpServletResponse response, HandlerMethod handlerMethod) throws Exception;
//略
}
- RequestMappingHandlerAdapter是AbstractHandlerMethodAdapter的子类,其源码如下:
public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter
implements BeanFactoryAware, InitializingBean {
@Override
protected ModelAndView handleInternal(HttpServletRequest request,
HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {
ModelAndView mav;
checkRequest(request);
// Execute invokeHandlerMethod in synchronized block if required.
if (this.synchronizeOnSession) {
HttpSession session = request.getSession(false);
if (session != null) {
Object mutex = WebUtils.getSessionMutex(session);
synchronized (mutex) {
mav = invokeHandlerMethod(request, response, handlerMethod);
}
}
else {
// No HttpSession available -> no mutex necessary
mav = invokeHandlerMethod(request, response, handlerMethod);
}
}
else {
// 这边会通过反射调用我们自己实现的Handler的方法
mav = invokeHandlerMethod(request, response, handlerMethod);
}
if (!response.containsHeader(HEADER_CACHE_CONTROL)) {
if (getSessionAttributesHandler(handlerMethod).hasSessionAttributes()) {
applyCacheSeconds(response, this.cacheSecondsForSessionAttributeHandlers);
}
else {
prepareResponse(response);
}
}
return mav;
}
}
3.4 视图解析器ViewResolver
3.4.1 View
- 视图的作用是渲染模型数据,将模型里的数据以某种形式呈现给用户。为了实现视图模型和具体实现技术的解耦,Spring在org.springframework.web.servlet包中定义了一个高度抽象的View接口。
- 视图是无状态的,所以不会有线程安全问题。
- 无状态是指对于每一个请求,都会创建一个View对象。
3.4.2 ViewResolver
- ViewResolver负责将处理结果生成View视图,View Resolver首先根据逻辑视图名解析成物理视图名即具体的页面地址,再生成View视图对象,最后对View进行渲染将处理结果通过页面展示给用户。视图对象是由视图解析器负责实例化的。
- 视图解析器的作用是将逻辑视图转为物理视图,所有的视图解析器都必须实现ViewResolver接口。
- SpringMVC为逻辑视图名的解析提供了不同的策略,可以在Spring WEB上下文配置一种或多种解析策略,并指定它们之间的先后顺序。每一个映射策略对应一个具体的视图解析器实现类。程序员可以选择一种或多种视图解析器。可以通过order属性指定解析器的优先顺序,order越小优先级越高,SpringMVC会按视图解析器的优先顺序对逻辑视图名进行解析,直到解析成功并返回视图对象,否则抛出异常。
3.4.3 执行过程分析
- 在DispatcherServlet中的doDispatch方法中,有调用processDispatchResult(处理分发请求)方法,其源码如下:
public class DispatcherServlet extends FrameworkServlet {
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null;
boolean multipartRequestParsed = false;
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
try {
ModelAndView mv = null;
Exception dispatchException = null;
try {
processedRequest = checkMultipart(request);
multipartRequestParsed = (processedRequest != request);
// Determine handler for the current request.
mappedHandler = getHandler(processedRequest);
if (mappedHandler == null) {
noHandlerFound(processedRequest, response);
return;
}
// Determine handler adapter for the current request.
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
// Process last-modified header, if supported by the handler.
String method = request.getMethod();
boolean isGet = "GET".equals(method);
if (isGet || "HEAD".equals(method)) {
long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {
return;
}
}
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}
// Actually invoke the handler.
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
if (asyncManager.isConcurrentHandlingStarted()) {
return;
}
applyDefaultViewName(processedRequest, mv);
mappedHandler.applyPostHandle(processedRequest, response, mv);
}
catch (Exception ex) {
dispatchException = ex;
}
catch (Throwable err) {
// As of 4.3, we're processing Errors thrown from handler methods as well,
// making them available for @ExceptionHandler methods and other scenarios.
dispatchException = new NestedServletException("Handler dispatch failed", err);
}
//处理分发请求
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
}
catch (Exception ex) {
triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
}
catch (Throwable err) {
triggerAfterCompletion(processedRequest, response, mappedHandler,
new NestedServletException("Handler processing failed", err));
}
finally {
if (asyncManager.isConcurrentHandlingStarted()) {
// Instead of postHandle and afterCompletion
if (mappedHandler != null) {
mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
}
}
else {
// Clean up any resources used by a multipart request.
if (multipartRequestParsed) {
cleanupMultipart(processedRequest);
}
}
}
}
/**
* Handle the result of handler selection and handler invocation, which is
* either a ModelAndView or an Exception to be resolved to a ModelAndView.
*/
private void processDispatchResult(HttpServletRequest request, HttpServletResponse response,
@Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv,
@Nullable Exception exception) throws Exception {
boolean errorView = false;
if (exception != null) {
if (exception instanceof ModelAndViewDefiningException) {
logger.debug("ModelAndViewDefiningException encountered", exception);
mv = ((ModelAndViewDefiningException) exception).getModelAndView();
}
else {
Object handler = (mappedHandler != null ? mappedHandler.getHandler() : null);
mv = processHandlerException(request, response, handler, exception);
errorView = (mv != null);
}
}
// Did the handler return a view to render?
if (mv != null && !mv.wasCleared()) {
//渲染视图并将结果返回给用户
render(mv, request, response);
if (errorView) {
WebUtils.clearErrorRequestAttributes(request);
}
}
else {
if (logger.isTraceEnabled()) {
logger.trace("No view rendering, null ModelAndView returned.");
}
}
if (WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted()) {
// Concurrent handling started during a forward
return;
}
if (mappedHandler != null) {
// Exception (if any) is already handled..
mappedHandler.triggerAfterCompletion(request, response, null);
}
}
/**
* Render the given ModelAndView.
* <p>This is the last stage in handling a request. It may involve resolving the view by name.
* @param mv the ModelAndView to render
* @param request current HTTP servlet request
* @param response current HTTP servlet response
* @throws ServletException if view is missing or cannot be resolved
* @throws Exception if there's a problem rendering the view
*/
protected void render(ModelAndView mv, HttpServletRequest request, HttpServletResponse response) throws Exception {
// Determine locale for request and apply it to the response.
Locale locale =
(this.localeResolver != null ? this.localeResolver.resolveLocale(request) : request.getLocale());
response.setLocale(locale);
View view;
String viewName = mv.getViewName();
if (viewName != null) {
//根据逻辑视图名解析物理视图
view = resolveViewName(viewName, mv.getModelInternal(), locale, request);
if (view == null) {
throw new ServletException("Could not resolve view with name '" + mv.getViewName() +
"' in servlet with name '" + getServletName() + "'");
}
}
else {
// No need to lookup: the ModelAndView object contains the actual View object.
view = mv.getView();
if (view == null) {
throw new ServletException("ModelAndView [" + mv + "] neither contains a view name nor a " +
"View object in servlet with name '" + getServletName() + "'");
}
}
// Delegate to the View object for rendering.
if (logger.isTraceEnabled()) {
logger.trace("Rendering view [" + view + "] ");
}
try {
if (mv.getStatus() != null) {
response.setStatus(mv.getStatus().value());
}
view.render(mv.getModelInternal(), request, response);
}
catch (Exception ex) {
if (logger.isDebugEnabled()) {
logger.debug("Error rendering view [" + view + "]", ex);
}
throw ex;
}
}
//解析视图
@Nullable
protected View resolveViewName(String viewName, @Nullable Map<String, Object> model,
Locale locale, HttpServletRequest request) throws Exception {
if (this.viewResolvers != null) {
//循环调用各种视图解析器,去解析视图,知道解析成功为止
for (ViewResolver viewResolver : this.viewResolvers) {
View view = viewResolver.resolveViewName(viewName, locale);
if (view != null) {
return view;
}
}
}
return null;
}
}
- 实际开发的时候,我们一般使用的是InternalResourceViewResolver视图解析器。而InternalResourceViewResolver是AbstractCachingViewResolver的子类。
- 在AbstractCachingViewResolver中的解析视图的方法:
public abstract class AbstractCachingViewResolver extends WebApplicationObjectSupport implements ViewResolver {
//解析视图的方法
@Override
@Nullable
public View resolveViewName(String viewName, Locale locale) throws Exception {
if (!isCache()) {
return createView(viewName, locale);
}
else {
Object cacheKey = getCacheKey(viewName, locale);
View view = this.viewAccessCache.get(cacheKey);
if (view == null) {
synchronized (this.viewCreationCache) {
view = this.viewCreationCache.get(cacheKey);
if (view == null) {
// Ask the subclass to create the View object.
view = createView(viewName, locale);
if (view == null && this.cacheUnresolved) {
view = UNRESOLVED_VIEW;
}
if (view != null && this.cacheFilter.filter(view, viewName, locale)) {
this.viewAccessCache.put(cacheKey, view);
this.viewCreationCache.put(cacheKey, view);
}
}
}
}
else {
if (logger.isTraceEnabled()) {
logger.trace(formatKey(cacheKey) + "served from cache");
}
}
return (view != UNRESOLVED_VIEW ? view : null);
}
}
//略
}
- 而View是视图的顶层接口,其子类AbstractView中,重写了render方法:
public abstract class AbstractView extends WebApplicationObjectSupport implements View, BeanNameAware {
@Override
public void render(@Nullable Map<String, ?> model, HttpServletRequest request,
HttpServletResponse response) throws Exception {
if (logger.isDebugEnabled()) {
logger.debug("View " + formatViewName() +
", model " + (model != null ? model : Collections.emptyMap()) +
(this.staticAttributes.isEmpty() ? "" : ", static attributes " + this.staticAttributes));
}
Map<String, Object> mergedModel = createMergedOutputModel(model, request, response);
prepareResponse(request, response);
//调用了Servlet底层的API,进行转发
renderMergedOutputModel(mergedModel, getRequestToExpose(request), response);
}
protected abstract void renderMergedOutputModel(
Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws Exception;
}
- InternalResourceView是AbstractView的子类,重写了renderMergedOutputModel方法,源码如下:
public class InternalResourceView extends AbstractUrlBasedView {
/**
* Render the internal resource given the specified model.
* This includes setting the model as request attributes.
*/
@Override
protected void renderMergedOutputModel(
Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws Exception {
// Expose the model object as request attributes.
exposeModelAsRequestAttributes(model, request);
// Expose helpers as request attributes, if any.
exposeHelpers(request);
// Determine the path for the request dispatcher.
String dispatcherPath = prepareForRendering(request, response);
// 此处是重点,
RequestDispatcher rd = getRequestDispatcher(request, dispatcherPath);
if (rd == null) {
throw new ServletException("Could not get RequestDispatcher for [" + getUrl() +
"]: Check that the corresponding file exists within your web application archive!");
}
// If already included or response already committed, perform include, else forward.
if (useInclude(request, response)) {
response.setContentType(getContentType());
if (logger.isDebugEnabled()) {
logger.debug("Including [" + getUrl() + "]");
}
rd.include(request, response);
}
else {
// Note: The forwarded resource is supposed to determine the content type itself.
if (logger.isDebugEnabled()) {
logger.debug("Forwarding to [" + getUrl() + "]");
}
//转发
rd.forward(request, response);
}
}
}
4 拦截器的执行时机和调用过程
4.1 拦截器的执行时机
- SpringMVC中的拦截器有三个方法,其中preHandle拦截方法是在控制器方法之前先执行的,preHandle方法做一些前置增强;postHandle拦截方法是在控制器方法执行之后,结果视图执行之前执行的,postHandle方法可以对响应数据进行增强;afterCompletion是在结果视图执行完成之后,响应之前执行的,可以实现一些清理操作。
4.2 注解开发中使用拦截器
- 定义拦截器:
package com.sunxiaping.interceptor;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class InterceptorDemo implements HandlerInterceptor {
/**
* 控制器的拦截方法,它是在控制器方法之前先执行的,可以做一些前置增强
*
* @param request
* @param response
* @param handler
* @return
* @throws Exception
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("InterceptorDemo....preHandle");
return true;
}
/**它是拦截器的后处理方法,执行时机在控制器方法执行之后,同时结果视图执行之前。它可以对响应数据进行增强
* @param request
* @param response
* @param handler
* @param modelAndView
* @throws Exception
*/
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
System.out.println("InterceptorDemo....postHandle");
}
/**它是拦截器最后指定的方法,执行时机在结果视图执行完成之后,响应之前,可以实现一些清理操作
* @param request
* @param response
* @param handler
* @param ex
* @throws Exception
*/
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
System.out.println("InterceptorDemo....afterCompletion");
}
}
- 向SpringMVC的容器中注册拦截器:
package com.sunxiaping.config;
import com.sunxiaping.interceptor.InterceptorDemo;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Controller;
import org.springframework.web.servlet.ViewResolver;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.resource.VersionResourceResolver;
import org.springframework.web.servlet.view.InternalResourceViewResolver;
@Configuration
@ComponentScan(value = "com.sunxiaping", useDefaultFilters = false, includeFilters = @ComponentScan.Filter(classes = Controller.class))
@EnableWebMvc
public class SpringMvcConfig implements WebMvcConfigurer {
/**
* 配置视图解析器
*
* @return
*/
@Bean
public ViewResolver viewResolver() {
InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
viewResolver.setPrefix("/WEB-INF/views/");
viewResolver.setSuffix(".jsp");
return viewResolver;
}
/**
* 配置静态资源
*
* @param registry
*/
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/js/**")
.addResourceLocations("/js/")
.resourceChain(true)
.addResolver(new VersionResourceResolver().addContentVersionStrategy("/**"));
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new InterceptorDemo());
}
}
4.3 拦截器的责任链模式
-
责任链模式是一种常见的行为模式。它是使得多个对象都有处理请求的机会,从而避免了请求的发送者和接收者之间的耦合关系。将这些对象串成一条链,并沿着这条链一直传递给请求,直到有对象处理它为止。
-
优势:
- 解耦了请求和处理。
- 请求处理者只需要关注自己感兴趣的请求进行处理即可,对于不感兴趣的请求,直接转发给下一级节点对象。
- 具备链式传递处理请求功能,请求发送者无需知晓链路结构,只需等待请求处理结果。
- 链路结构灵活,可以通过改变链路结构动态的新增或者删除责任。
- 易于扩展新的请求处理类(节点),符合开闭原则。
-
弊端:
- 责任链路过长时,可能对请求处理传递效率有影响。
- 如果节点对象存在循环引用时,会造成死循环,导致系统崩溃。
5 类型转换器和异常处理器
5.1 类型转换器(不建议使用)
5.1.1 Converter接口
- Converter接口:
@FunctionalInterface
public interface Converter<S, T> {
/**
* Convert the source object of type {@code S} to target type {@code T}.
* @param source the source object to convert, which must be an instance of {@code S} (never {@code null})
* @return the converted object, which must be an instance of {@code T} (potentially {@code null})
* @throws IllegalArgumentException if the source cannot be converted to the desired target type
*/
@Nullable
T convert(S source);
}
5.1.2 应用示例
- 示例:
- 自定义Converter:
package com.sunxiaping.converter;
import org.springframework.core.convert.converter.Converter;
import org.springframework.util.StringUtils;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
public class DateTimeConverter implements Converter<String, Date> {
@Override
public Date convert(String s) {
if (StringUtils.isEmpty(s)) {
throw new RuntimeException(s + "不能为null");
}
try {
return new SimpleDateFormat("yyyy-MM-dd").parse(s);
} catch (ParseException e) {
throw new RuntimeException("格式转换错误");
}
}
}
- 注册自定义Converter:
package com.sunxiaping.advice;
import com.sunxiaping.converter.DateTimeConverter;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.support.GenericConversionService;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.InitBinder;
@ControllerAdvice
public class SpringMVC5ControllerAdvice {
@InitBinder
public void initBinder(WebDataBinder webDataBinder) {
ConversionService conversionService = webDataBinder.getConversionService();
if(conversionService instanceof GenericConversionService){
GenericConversionService genericConversionService = (GenericConversionService) conversionService;
genericConversionService.addConverter(new DateTimeConverter());
}
}
}
5.2 异常处理器(不建议使用)
5.2.1 HandlerExceptionResolver接口
- HandlerExceptionResolver接口:
public interface HandlerExceptionResolver {
/**
* Try to resolve the given exception that got thrown during handler execution,
* returning a {@link ModelAndView} that represents a specific error page if appropriate.
* <p>The returned {@code ModelAndView} may be {@linkplain ModelAndView#isEmpty() empty}
* to indicate that the exception has been resolved successfully but that no view
* should be rendered, for instance by setting a status code.
* @param request current HTTP request
* @param response current HTTP response
* @param handler the executed handler, or {@code null} if none chosen at the
* time of the exception (for example, if multipart resolution failed)
* @param ex the exception that got thrown during handler execution
* @return a corresponding {@code ModelAndView} to forward to,
* or {@code null} for default processing in the resolution chain
*/
@Nullable
ModelAndView resolveException(
HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex);
}
5.2.2 应用示例
- 示例:
- 自定义异常处理器:
package com.sunxiaping.exception;
import org.springframework.web.servlet.HandlerExceptionResolver;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class CustomHandlerExceptionResolver implements HandlerExceptionResolver {
@Override
public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
ModelAndView mv = new ModelAndView();
if(ex instanceof RuntimeException){
System.out.println("这是运行时异常");
mv.addObject("errorMsg","这是运行时异常");
}else{
System.out.println("这是系统异常");
mv.addObject("errorMsg","这是系统异常");
}
mv.setViewName("error");
return mv;
}
}
- 注册自定义异常处理器:
package com.sunxiaping.config;
import com.sunxiaping.exception.CustomHandlerExceptionResolver;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Controller;
import org.springframework.web.servlet.HandlerExceptionResolver;
import org.springframework.web.servlet.ViewResolver;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.resource.VersionResourceResolver;
import org.springframework.web.servlet.view.InternalResourceViewResolver;
import java.util.List;
@Configuration
@ComponentScan(value = "com.sunxiaping", useDefaultFilters = false, includeFilters = @ComponentScan.Filter(classes = Controller.class))
@EnableWebMvc
public class SpringMvcConfig implements WebMvcConfigurer {
/**
* 配置视图解析器
*
* @return
*/
@Bean
public ViewResolver viewResolver() {
InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
viewResolver.setPrefix("/WEB-INF/views/");
viewResolver.setSuffix(".jsp");
return viewResolver;
}
/**
* 配置静态资源
*
* @param registry
*/
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/js/**")
.addResourceLocations("/js/")
.resourceChain(true)
.addResolver(new VersionResourceResolver().addContentVersionStrategy("/**"));
}
/**
* 注册自定义异常处理器
*
* @param resolvers
*/
@Override
public void configureHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) {
resolvers.add(new CustomHandlerExceptionResolver());
}
}
6 SpringMVC中的文件上传
6.1 MultipartFile
6.1.1 源码
- MultipartFile:
//SpringMVC中对上传文件的封装
public interface MultipartFile extends InputStreamSource {
/**
* 获取临时文件名称
*/
String getName();
/**
* 获取原始文件的名称
*/
@Nullable
String getOriginalFilename();
/**
* 获取上传文件的MIME类型
*/
@Nullable
String getContentType();
/**
* 是否是空文件
*/
boolean isEmpty();
/**
* 获取上传文件的字节大小
*/
long getSize();
/**
* 获取上传文件的字节数组
*/
byte[] getBytes() throws IOException;
/**
* 获取上传文件的字节输入流
*/
@Override
InputStream getInputStream() throws IOException;
/**
* 把上传文件转换成一个Resource对象
*/
default Resource getResource() {
return new MultipartFileResource(this);
}
/**
* 把临时文件移动到指定位置并重命名,参数是一个文件对象
*/
void transferTo(File dest) throws IOException, IllegalStateException;
/**
* 把临时文件移动到指定位置并重命名,参数是一个文件路径
*/
default void transferTo(Path dest) throws IOException, IllegalStateException {
FileCopyUtils.copy(getInputStream(), Files.newOutputStream(dest));
}
}
6.1.2 commons-fileupload的实现
- CommonsMultipartFile.java
public class CommonsMultipartFile implements MultipartFile, Serializable {
protected static final Log logger = LogFactory.getLog(CommonsMultipartFile.class);
private final FileItem fileItem;
private final long size;
private boolean preserveFilename = false;
/**
* Create an instance wrapping the given FileItem.
* @param fileItem the FileItem to wrap
*/
public CommonsMultipartFile(FileItem fileItem) {
this.fileItem = fileItem;
this.size = this.fileItem.getSize();
}
/**
* Return the underlying {@code org.apache.commons.fileupload.FileItem}
* instance. There is hardly any need to access this.
*/
public final FileItem getFileItem() {
return this.fileItem;
}
/**
* Set whether to preserve the filename as sent by the client, not stripping off
* path information in {@link CommonsMultipartFile#getOriginalFilename()}.
* <p>Default is "false", stripping off path information that may prefix the
* actual filename e.g. from Opera. Switch this to "true" for preserving the
* client-specified filename as-is, including potential path separators.
* @since 4.3.5
* @see #getOriginalFilename()
* @see CommonsMultipartResolver#setPreserveFilename(boolean)
*/
public void setPreserveFilename(boolean preserveFilename) {
this.preserveFilename = preserveFilename;
}
@Override
public String getName() {
return this.fileItem.getFieldName();
}
@Override
public String getOriginalFilename() {
String filename = this.fileItem.getName();
if (filename == null) {
// Should never happen.
return "";
}
if (this.preserveFilename) {
// Do not try to strip off a path...
return filename;
}
// Check for Unix-style path
int unixSep = filename.lastIndexOf('/');
// Check for Windows-style path
int winSep = filename.lastIndexOf('\\');
// Cut off at latest possible point
int pos = Math.max(winSep, unixSep);
if (pos != -1) {
// Any sort of path separator found...
return filename.substring(pos + 1);
}
else {
// A plain name
return filename;
}
}
@Override
public String getContentType() {
return this.fileItem.getContentType();
}
@Override
public boolean isEmpty() {
return (this.size == 0);
}
@Override
public long getSize() {
return this.size;
}
@Override
public byte[] getBytes() {
if (!isAvailable()) {
throw new IllegalStateException("File has been moved - cannot be read again");
}
byte[] bytes = this.fileItem.get();
return (bytes != null ? bytes : new byte[0]);
}
@Override
public InputStream getInputStream() throws IOException {
if (!isAvailable()) {
throw new IllegalStateException("File has been moved - cannot be read again");
}
InputStream inputStream = this.fileItem.getInputStream();
return (inputStream != null ? inputStream : StreamUtils.emptyInput());
}
@Override
public void transferTo(File dest) throws IOException, IllegalStateException {
if (!isAvailable()) {
throw new IllegalStateException("File has already been moved - cannot be transferred again");
}
if (dest.exists() && !dest.delete()) {
throw new IOException(
"Destination file [" + dest.getAbsolutePath() + "] already exists and could not be deleted");
}
try {
this.fileItem.write(dest);
LogFormatUtils.traceDebug(logger, traceOn -> {
String action = "transferred";
if (!this.fileItem.isInMemory()) {
action = (isAvailable() ? "copied" : "moved");
}
return "Part '" + getName() + "', filename '" + getOriginalFilename() + "'" +
(traceOn ? ", stored " + getStorageDescription() : "") +
": " + action + " to [" + dest.getAbsolutePath() + "]";
});
}
catch (FileUploadException ex) {
throw new IllegalStateException(ex.getMessage(), ex);
}
catch (IllegalStateException | IOException ex) {
// Pass through IllegalStateException when coming from FileItem directly,
// or propagate an exception from I/O operations within FileItem.write
throw ex;
}
catch (Exception ex) {
throw new IOException("File transfer failed", ex);
}
}
@Override
public void transferTo(Path dest) throws IOException, IllegalStateException {
if (!isAvailable()) {
throw new IllegalStateException("File has already been moved - cannot be transferred again");
}
FileCopyUtils.copy(this.fileItem.getInputStream(), Files.newOutputStream(dest));
}
/**
* Determine whether the multipart content is still available.
* If a temporary file has been moved, the content is no longer available.
*/
protected boolean isAvailable() {
// If in memory, it's available.
if (this.fileItem.isInMemory()) {
return true;
}
// Check actual existence of temporary file.
if (this.fileItem instanceof DiskFileItem) {
return ((DiskFileItem) this.fileItem).getStoreLocation().exists();
}
// Check whether current file size is different than original one.
return (this.fileItem.getSize() == this.size);
}
/**
* Return a description for the storage location of the multipart content.
* Tries to be as specific as possible: mentions the file location in case
* of a temporary file.
*/
public String getStorageDescription() {
if (this.fileItem.isInMemory()) {
return "in memory";
}
else if (this.fileItem instanceof DiskFileItem) {
return "at [" + ((DiskFileItem) this.fileItem).getStoreLocation().getAbsolutePath() + "]";
}
else {
return "on disk";
}
}
}
6.2 MultipartResolver
6.2.1 源码
- MultipartResolver:
//SpringMVC中文件解析器的标准
//通过一个接口规定了文件解析器中必须包含的方法
public interface MultipartResolver {
/**
* 判断是否支持文件上传
*/
boolean isMultipart(HttpServletRequest request);
/**
* 解析HttpServletRequest
*/
MultipartHttpServletRequest resolveMultipart(HttpServletRequest request) throws MultipartException;
/**
* 删除临时文件和一些清理操作
*/
void cleanupMultipart(MultipartHttpServletRequest request);
}
6.2.2 CommonsFileUploadResolver
- CommonsFileUploadSupport:
public abstract class CommonsFileUploadSupport {
protected final Log logger = LogFactory.getLog(getClass());
private final DiskFileItemFactory fileItemFactory;
private final FileUpload fileUpload;
private boolean uploadTempDirSpecified = false;
private boolean preserveFilename = false;
/**
* Instantiate a new CommonsFileUploadSupport with its
* corresponding FileItemFactory and FileUpload instances.
* @see #newFileItemFactory
* @see #newFileUpload
*/
public CommonsFileUploadSupport() {
this.fileItemFactory = newFileItemFactory();
this.fileUpload = newFileUpload(getFileItemFactory());
}
/**
* Return the underlying {@code org.apache.commons.fileupload.disk.DiskFileItemFactory}
* instance. There is hardly any need to access this.
* @return the underlying DiskFileItemFactory instance
*/
public DiskFileItemFactory getFileItemFactory() {
return this.fileItemFactory;
}
/**
* Return the underlying {@code org.apache.commons.fileupload.FileUpload}
* instance. There is hardly any need to access this.
* @return the underlying FileUpload instance
*/
public FileUpload getFileUpload() {
return this.fileUpload;
}
/**
* Set the maximum allowed size (in bytes) before an upload gets rejected.
* -1 indicates no limit (the default).
* @param maxUploadSize the maximum upload size allowed
* @see org.apache.commons.fileupload.FileUploadBase#setSizeMax
*/
public void setMaxUploadSize(long maxUploadSize) {
this.fileUpload.setSizeMax(maxUploadSize);
}
/**
* Set the maximum allowed size (in bytes) for each individual file before
* an upload gets rejected. -1 indicates no limit (the default).
* @param maxUploadSizePerFile the maximum upload size per file
* @since 4.2
* @see org.apache.commons.fileupload.FileUploadBase#setFileSizeMax
*/
public void setMaxUploadSizePerFile(long maxUploadSizePerFile) {
this.fileUpload.setFileSizeMax(maxUploadSizePerFile);
}
/**
* Set the maximum allowed size (in bytes) before uploads are written to disk.
* Uploaded files will still be received past this amount, but they will not be
* stored in memory. Default is 10240, according to Commons FileUpload.
* @param maxInMemorySize the maximum in memory size allowed
* @see org.apache.commons.fileupload.disk.DiskFileItemFactory#setSizeThreshold
*/
public void setMaxInMemorySize(int maxInMemorySize) {
this.fileItemFactory.setSizeThreshold(maxInMemorySize);
}
/**
* Set the default character encoding to use for parsing requests,
* to be applied to headers of individual parts and to form fields.
* Default is ISO-8859-1, according to the Servlet spec.
* <p>If the request specifies a character encoding itself, the request
* encoding will override this setting. This also allows for generically
* overriding the character encoding in a filter that invokes the
* {@code ServletRequest.setCharacterEncoding} method.
* @param defaultEncoding the character encoding to use
* @see javax.servlet.ServletRequest#getCharacterEncoding
* @see javax.servlet.ServletRequest#setCharacterEncoding
* @see WebUtils#DEFAULT_CHARACTER_ENCODING
* @see org.apache.commons.fileupload.FileUploadBase#setHeaderEncoding
*/
public void setDefaultEncoding(String defaultEncoding) {
this.fileUpload.setHeaderEncoding(defaultEncoding);
}
/**
* Determine the default encoding to use for parsing requests.
* @see #setDefaultEncoding
*/
protected String getDefaultEncoding() {
String encoding = getFileUpload().getHeaderEncoding();
if (encoding == null) {
encoding = WebUtils.DEFAULT_CHARACTER_ENCODING;
}
return encoding;
}
/**
* Set the temporary directory where uploaded files get stored.
* Default is the servlet container's temporary directory for the web application.
* @see org.springframework.web.util.WebUtils#TEMP_DIR_CONTEXT_ATTRIBUTE
*/
public void setUploadTempDir(Resource uploadTempDir) throws IOException {
if (!uploadTempDir.exists() && !uploadTempDir.getFile().mkdirs()) {
throw new IllegalArgumentException("Given uploadTempDir [" + uploadTempDir + "] could not be created");
}
this.fileItemFactory.setRepository(uploadTempDir.getFile());
this.uploadTempDirSpecified = true;
}
/**
* Return the temporary directory where uploaded files get stored.
* @see #setUploadTempDir
*/
protected boolean isUploadTempDirSpecified() {
return this.uploadTempDirSpecified;
}
/**
* Set whether to preserve the filename as sent by the client, not stripping off
* path information in {@link CommonsMultipartFile#getOriginalFilename()}.
* <p>Default is "false", stripping off path information that may prefix the
* actual filename e.g. from Opera. Switch this to "true" for preserving the
* client-specified filename as-is, including potential path separators.
* @since 4.3.5
* @see MultipartFile#getOriginalFilename()
* @see CommonsMultipartFile#setPreserveFilename(boolean)
*/
public void setPreserveFilename(boolean preserveFilename) {
this.preserveFilename = preserveFilename;
}
/**
* Factory method for a Commons DiskFileItemFactory instance.
* <p>Default implementation returns a standard DiskFileItemFactory.
* Can be overridden to use a custom subclass, e.g. for testing purposes.
* @return the new DiskFileItemFactory instance
*/
protected DiskFileItemFactory newFileItemFactory() {
return new DiskFileItemFactory();
}
/**
* Factory method for a Commons FileUpload instance.
* <p><b>To be implemented by subclasses.</b>
* @param fileItemFactory the Commons FileItemFactory to build upon
* @return the Commons FileUpload instance
*/
protected abstract FileUpload newFileUpload(FileItemFactory fileItemFactory);
/**
* Determine an appropriate FileUpload instance for the given encoding.
* <p>Default implementation returns the shared FileUpload instance
* if the encoding matches, else creates a new FileUpload instance
* with the same configuration other than the desired encoding.
* @param encoding the character encoding to use
* @return an appropriate FileUpload instance.
*/
protected FileUpload prepareFileUpload(@Nullable String encoding) {
FileUpload fileUpload = getFileUpload();
FileUpload actualFileUpload = fileUpload;
// Use new temporary FileUpload instance if the request specifies
// its own encoding that does not match the default encoding.
if (encoding != null && !encoding.equals(fileUpload.getHeaderEncoding())) {
actualFileUpload = newFileUpload(getFileItemFactory());
actualFileUpload.setSizeMax(fileUpload.getSizeMax());
actualFileUpload.setFileSizeMax(fileUpload.getFileSizeMax());
actualFileUpload.setHeaderEncoding(encoding);
}
return actualFileUpload;
}
/**
* Parse the given List of Commons FileItems into a Spring MultipartParsingResult,
* containing Spring MultipartFile instances and a Map of multipart parameter.
* @param fileItems the Commons FileItems to parse
* @param encoding the encoding to use for form fields
* @return the Spring MultipartParsingResult
* @see CommonsMultipartFile#CommonsMultipartFile(org.apache.commons.fileupload.FileItem)
*/
protected MultipartParsingResult parseFileItems(List<FileItem> fileItems, String encoding) {
MultiValueMap<String, MultipartFile> multipartFiles = new LinkedMultiValueMap<>();
Map<String, String[]> multipartParameters = new HashMap<>();
Map<String, String> multipartParameterContentTypes = new HashMap<>();
// Extract multipart files and multipart parameters.
for (FileItem fileItem : fileItems) {
if (fileItem.isFormField()) {
String value;
String partEncoding = determineEncoding(fileItem.getContentType(), encoding);
try {
value = fileItem.getString(partEncoding);
}
catch (UnsupportedEncodingException ex) {
if (logger.isWarnEnabled()) {
logger.warn("Could not decode multipart item '" + fileItem.getFieldName() +
"' with encoding '" + partEncoding + "': using platform default");
}
value = fileItem.getString();
}
String[] curParam = multipartParameters.get(fileItem.getFieldName());
if (curParam == null) {
// simple form field
multipartParameters.put(fileItem.getFieldName(), new String[] {value});
}
else {
// array of simple form fields
String[] newParam = StringUtils.addStringToArray(curParam, value);
multipartParameters.put(fileItem.getFieldName(), newParam);
}
multipartParameterContentTypes.put(fileItem.getFieldName(), fileItem.getContentType());
}
else {
// multipart file field
CommonsMultipartFile file = createMultipartFile(fileItem);
multipartFiles.add(file.getName(), file);
LogFormatUtils.traceDebug(logger, traceOn ->
"Part '" + file.getName() + "', size " + file.getSize() +
" bytes, filename='" + file.getOriginalFilename() + "'" +
(traceOn ? ", storage=" + file.getStorageDescription() : "")
);
}
}
return new MultipartParsingResult(multipartFiles, multipartParameters, multipartParameterContentTypes);
}
/**
* Create a {@link CommonsMultipartFile} wrapper for the given Commons {@link FileItem}.
* @param fileItem the Commons FileItem to wrap
* @return the corresponding CommonsMultipartFile (potentially a custom subclass)
* @since 4.3.5
* @see #setPreserveFilename(boolean)
* @see CommonsMultipartFile#setPreserveFilename(boolean)
*/
protected CommonsMultipartFile createMultipartFile(FileItem fileItem) {
CommonsMultipartFile multipartFile = new CommonsMultipartFile(fileItem);
multipartFile.setPreserveFilename(this.preserveFilename);
return multipartFile;
}
/**
* Cleanup the Spring MultipartFiles created during multipart parsing,
* potentially holding temporary data on disk.
* <p>Deletes the underlying Commons FileItem instances.
* @param multipartFiles a Collection of MultipartFile instances
* @see org.apache.commons.fileupload.FileItem#delete()
*/
protected void cleanupFileItems(MultiValueMap<String, MultipartFile> multipartFiles) {
for (List<MultipartFile> files : multipartFiles.values()) {
for (MultipartFile file : files) {
if (file instanceof CommonsMultipartFile) {
CommonsMultipartFile cmf = (CommonsMultipartFile) file;
cmf.getFileItem().delete();
LogFormatUtils.traceDebug(logger, traceOn ->
"Cleaning up part '" + cmf.getName() +
"', filename '" + cmf.getOriginalFilename() + "'" +
(traceOn ? ", stored " + cmf.getStorageDescription() : ""));
}
}
}
}
private String determineEncoding(String contentTypeHeader, String defaultEncoding) {
if (!StringUtils.hasText(contentTypeHeader)) {
return defaultEncoding;
}
MediaType contentType = MediaType.parseMediaType(contentTypeHeader);
Charset charset = contentType.getCharset();
return (charset != null ? charset.name() : defaultEncoding);
}
/**
* Holder for a Map of Spring MultipartFiles and a Map of
* multipart parameters.
*/
protected static class MultipartParsingResult {
private final MultiValueMap<String, MultipartFile> multipartFiles;
private final Map<String, String[]> multipartParameters;
private final Map<String, String> multipartParameterContentTypes;
public MultipartParsingResult(MultiValueMap<String, MultipartFile> mpFiles,
Map<String, String[]> mpParams, Map<String, String> mpParamContentTypes) {
this.multipartFiles = mpFiles;
this.multipartParameters = mpParams;
this.multipartParameterContentTypes = mpParamContentTypes;
}
public MultiValueMap<String, MultipartFile> getMultipartFiles() {
return this.multipartFiles;
}
public Map<String, String[]> getMultipartParameters() {
return this.multipartParameters;
}
public Map<String, String> getMultipartParameterContentTypes() {
return this.multipartParameterContentTypes;
}
}
}