Spring实战 七 SpringMVC的高级技术
概述
先自己搭个项目回顾一下子上一章的内容,我这里采用的是Java方式配置SpringMVC并且使用了thymeleaf模板技术展示一个简单的首页。
DispatcherServlet高级配置
AbstractAnnotationConfigDispatcherServletInitializer`来进行自动配置,这可以应付大部分情况下的应用了,但是总有特殊情况。
通过重写三个抽象方法之外的其他的方法,我们可以完成更加细致的配置。
在Spring初始化,注册DispatcherServlet到Servlet容器后,就会调用customizeRegistration
方法,传入一个ServletRegistration.Dynamic
对象,可以通过这个对象对DispatcherServlet进行额外的配置。
@Override
protected void customizeRegistration(ServletRegistration.Dynamic registration) {
registration.setMultipartConfig(
new MultipartConfigElement("/tmp/useruploads/"));
registration.setLoadOnStartup(1);
}
如上,我们通过customizeRegistration
设置了多文件上传、DispatcherServlet的启动时加载,通过这个方法,我们还可以其它的一些配置。
添加其他Servlet和Filter
SpringMVC接管了大部分我们该做的事,而且能做的很好,但有时候我们也需要创建自己的Servlet和Filter。
实现WebApplicationInitializer接口
EMMMM...第一个办法就是实现WebApplicationInitializer
接口。在第五章的笔记中介绍过这个接口。总之,这是Spring提供的一个接口,实现了这个接口的类会在Servlet容器启动时被扫描到并自动调用其中的onStartup
方法,我们之前继承的AbstractAnnotationConfigDispatcherServletInitializer
也是这个接口的实现类。
我们可以随便创建一个初始化器然后在onStartup
中调用servletContext
中的addServlet
或者addFilter
来实现添加filter和servlet。
public class ServletAndFilterInitializer implements WebApplicationInitializer {
@Override
public void onStartup(ServletContext servletContext) throws ServletException {
servletContext.addServlet("myServlet", MyServlet.class)
.addMapping("/myservlet");
servletContext.addFilter("myFilter", MyFilter.class)
.addMappingForUrlPatterns(null,false,"/myservlet");
}
}
下面贴上MyServlet和MyFilter代码:
public class MyServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.getWriter().println("MyServlet");
}
}
public class MyFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
System.out.println("init....");
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletResponse response = (HttpServletResponse) servletResponse;
response.getWriter().println("MyFilter");
filterChain.doFilter(servletRequest,servletResponse);
}
}
使用这个办法创建出来的Servlet和Filter就相当于独立于Spring的DispatcherServlet工作了。大部分时候我们还是要工作在Spring中,只是想注册一个映射到DispatcherServlet上的Filter的话,不用这么大动作。只需要回到AbstractAnnotationConfigDispatcherServletInitializer
中,重写内部的getServletFilters
方法即可。
重写getServletFilters
public class AppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
@Override
protected Filter[] getServletFilters() {
return new Filter[]{new DispatcherServletFilter()};
}
// ...
}
public class DispatcherServletFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
((HttpServletResponse) response).getWriter().println("Hello, DispatcherServletFilter!");
chain.doFilter(request,response);
}
}
这样你就可以把任意多个Filter映射到DispatcherServlet上了。
其他方法的说明
其实我试了在web.xml
下声明Servlet或者开启容器的自动扫描基于注解配置的Servlet,也都是可以正常配置的,但是我不知道和在Spring中配置有啥区别,在WebApplicationInitializer
中的ServletContext
和直接注册时的是一个对象,所以也不存在Spring会包装了那个Context对象在我们通过WebApplicationInitializer
对象注册时会做一些额外操作的事。
Web.xml中配置DispatcherServlet
如果我们必须在web.xml
中配置的话,我们可以这样配置。
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0" metadata-complete="false">
<!-- 设置根上下文配置文件位置 -->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/spring/root-context.xml</param-value>
</context-param>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<servlet>
<servlet-name>appServlet</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>appServlet</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
</web-app>
像在Java中配置一样ContextLoaderListener和DispatcherServlet各需要一个配置,经过如上设置,ContextLoaderListener会加载/WEB-INF/spring/root-context.xml
,从这里读取Bean定义。
而DispatcherServlet需要从servlet定义的init-param
中设置这个文件的位置,要不然默认是从/WEB-INF/{Servlet名字}-context.xml
中读取,这个示例中也就是/WEB-INF/appServlet-context.xml
。
经过如下设置,DispatcherServlet就会从/WEB-INF/spring/appServlet/servlet-context.xml
中读取定义。
<servlet>
<servlet-name>appServlet</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/spring/appServlet/servlet-context.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>appServlet</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
我们还是想通过Java来进行Bean的定义,而不是XML,我们只用XML来注册DispatcherServlet和ContextLoaderListener。
如下我们通过指定contextClass
和contextConfigLocation
来进行这项操作,把之前的两个配置文件WebConfig.java
和RootConfig.java
给使用起来。
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0" metadata-complete="false">
<context-param>
<param-name>contextClass</param-name>
<param-value>org.springframework.web.context.support.AnnotationConfigWebApplicationContext</param-value>
</context-param>
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>io.lilpig.springlearn.springlearn04.config.RootConfig</param-value>
</context-param>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<servlet>
<servlet-name>appServlet</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextClass</param-name>
<param-value>org.springframework.web.context.support.AnnotationConfigWebApplicationContext</param-value>
</init-param>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>io.lilpig.springlearn.springlearn04.config.WebConfig</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>appServlet</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
</web-app>
行,删了,换Java配置。。。。
处理文件上传
文字类型的表单通常有限制,遇到上传文件的需求就没有办法了,multipart类型是一个解决办法。
如下是一个multipart类型的数据示例
每个------WebKitFormBoundary...
是一个字段,下面一行是字段名啥的,然后一个空行,再往下一行是这个字段的数据。
如果这个字段是一个文件类型或者其他类型的数据,就会有一个Content-Type
头,这个头指定了它的类型,服务器端会根据这个头来取数据。
设置MultipartResolver
Spring提供了对应的Resolver来解析multipart类型的字段。
CommonsMultipartResolver
:使用Jakarta Commons FileUpload解析multipart请求StandardServletMultipartResolver
:依赖于Spring3.0对multipart请求的支持(始于Spring3.1)
我们使用第二种方案来处理文件上传
在WebConfig中创建这样一个Bean。
@Bean
public MultipartResolver multipartResolver() {
return new StandardServletMultipartResolver();
}
那么用户上传的文件该保存到哪里呢?大小、格式限制在哪里做呢?
如果是XML方式,需要配置DispatcherServlet
中的一部分来指定如何处理文件。
如果是实现WebApplicationInitializer则可以在DispatcherServlet的Servlet Registration上调用setMutipartConfig
进行设置。
对于大多数情况下的继承AbstractAnnotationConfigDispatcherServletInitializer
,我们只需要重写其中的customizeRegistration
方法来设置参数就好了。
@Override
protected void customizeRegistration(ServletRegistration.Dynamic registration) {
registration.setMultipartConfig(
new MultipartConfigElement("/tmp/uploads")
);
}
我们只使用了一个参数的构造器来指定上传的暂存路径,还可以有其它的构造器用来指定更多的参数。
假如我们不希望用户上传大于2MB的文件,不希望整个请求的数据高达4MB,所有文件都写入到磁盘中,那么可以使用如下设置。
@Override
protected void customizeRegistration(ServletRegistration.Dynamic registration) {
registration.setMultipartConfig(
new MultipartConfigElement("/tmp/uploads", 2097152, 4194304, 0)
);
}
XML配置如下
处理Multipart请求
下面就是编写Controller来处理Multipart请求了
我先写一个表单页面
下面使用enctype
指定请求体格式为multipart/form-data
,然后使用了一个file类型的input,除此之外没啥好说的。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<form method="post" th:object="${user}" enctype="multipart/form-data">
username: <input type="text" th:field="*{username}" name="username"> <br>
age: <input type="number" th:field="*{age}" name="age"> <br>
icon: <input type="file" name="profilePicture" accept="image/gif,image/jpeg,image/png"><br>
<button type="submit">注册</button>
</form>
</body>
</html>
下面就是Controller,我们使用了@RequestPart
注解,并通过一个byte数组来接收文件。用户上传的文件先会被服务器保存到之前配置的暂存目录下,然后再将文件的字节码传递给我们。
@Controller
@RequestMapping("/user")
public class UserController {
@GetMapping("/register")
public String register(Model model) {
model.addAttribute("user", new User());
return "register";
}
@PostMapping("/register")
public String processRegistration(
@RequestPart("profilePicture") byte[] profilePicture,
@Valid User user,
Errors errors
) {
if (errors.hasErrors()) {
return "register";
}else{
return "redirect:/";
}
}
}
使用byte[]
这种原始的数据类型,一切都得我们自己来处理,比如保存文件。
使用Spring提供的MultipartFile接口,我们可以更加方便地对用户上传的数据进行操作。
如下是MultipartFile提供的方法,我们可以获取源文件名,类型,大小,甚至可以用transferTo
直接以文件的形式保存到本地。
@PostMapping("/register")
public String processRegistration(
@RequestPart("profilePicture")MultipartFile multipartFile,
@Valid User user,
Errors errors
) {
try {
multipartFile.transferTo(
new File("D:/tmp/springlearn/userpictures/"+multipartFile.getOriginalFilename()));
} catch (IOException e) {
e.printStackTrace();
}
return "redirect:/";
}
还可以使用Part
类型来接收文件,它更简单,甚至无需配置MultipartResolver
。
异常处理
在这之前我们处理异常都是通过在Controller中使用try-catch
语句然后返回不同的逻辑视图名。这样Controller中就会产生不同的实际视图路径。如下:
try{
doSomething();
return "successed";
} catch(SomeException e){
return "redirect:/someErrorPage";
}
如果我们把这部分内容从Controller中剥离,让Controller就关注成功的状态,让失败的状态由其他模块处理,会更好一些。
ResponseStatus
对于未捕获的异常,默认情况下Spring会将其捕获然后返回500状态码,并打印所有的异常堆栈。
这样不仅对用户不友好,而且将系统的异常堆栈暴露给用户对系统来说也很不安全。
如果我们在异常上使用@ResponseStatus
注解,那么我们可以绑定一个错误代码和错误消息。
@ResponseStatus(
value = HttpStatus.NOT_FOUND,
reason = "user not found"
)
public class UserNotFoundException extends Exception{
}
然后在controller的方法中抛出对应异常
@GetMapping("/{username}")
public String getUser(@PathVariable String username) throws UserNotFoundException {
// 就是假装没找到
throw new UserNotFoundException();
}
编写处理异常的方法
大部分发生异常的时候我们都要进行处理然后返回一个自定义页面,而不只是返回一个默认页面给用户。
在Controller中定义一个方法,方法上声明一个@ExceptionHandler
的注解并指定要捕获的异常类型。
@ExceptionHandler(UserNotFoundException.class)
public String userNotFoundHandler() {
return "errors/user-not-found";
}
这个方法和普通的控制器方法没什么区别,也可以返回一个逻辑视图名,主要是该控制器内所有未捕获的UserNotFoundException
异常都会被这个函数处理。
如下, 我们定义的异常页面被渲染。
控制器通知
如果很多个控制器中都可能会出现同种类型的异常,那么把处理异常的代码放到控制器中就不合适了。
控制器通知可以将这种异常的处理抽离出来,我们先写一个看看。
@ControllerAdvice
public class UserExceptionHandler {
@ExceptionHandler(UserNotFoundException.class)
public String userNotFoundHandler() {
return "errors/user-not-found";
}
}
@ControllerAdvice
上被声明了一个@Component
注解,也就是说它也可以被Spring扫描。接下来只要是控制器中未处理的@ExcepitonHandler
中定义的异常都会执行对应的方法。
跨重定向请求数据
之前如果有重定向并传递数据的需求,我们都是通过"redirect:/url/"+参数
来传递的。
这样传递有一些限制,首先它不安全,直接拼接URL可能被用户注入一些恶意代码,其次它只能存储字符串。
Spring提供了两个办法来解决这个问题。
URL模板
我们可以通过URL模板来解决掉直接拼接参数产生的不安全问题,URL模板会对拼接的值进行转义。
@PostMapping("/register")
public String processRegistration(
User user,
Model model
) {
model.addAttribute("username",user.getUsername());
return "redirect:/user/showuser/{username}";
}
将model中的属性放到返回的逻辑视图名中。
然后跳转到的处理器方法可以通过PathVariable接收它
@GetMapping("/showuser/{username}")
public String showUser(@PathVariable String username, Model model) {
model.addAttribute("username",username);
return "showuser";
}
flash属性
flash属性解决了传递的值只能是字符串的问题。
flash使用session技术实现,但它被取出之后就不在session中存在了,也许是因为转瞬即逝,所以叫flash????
使用flash属性就不能使用普通的Model
了,需要RedirectAttributes
,下面是使用案例。
@PostMapping("/register")
public String processRegistration(
User user,
RedirectAttributes model
) {
model.addAttribute("username",user.getUsername());
model.addFlashAttribute("user", user);
return "redirect:/user/showuser/{username}";
}
flash属性会被传到转发请求的模型中,并且从session中移除。下面的控制器方法检测了模型里是否存在对应对象,如果存在就直接返回视图名,然后视图进行渲染。
@GetMapping("/showuser/{username}")
public String showUser(@PathVariable String username, Model model) throws Exception {
if(!model.containsAttribute("user"))
throw new Exception("bad paramter");
return "showuser";
}