Spring实战 七 SpringMVC的高级技术

概述

先自己搭个项目回顾一下子上一章的内容,我这里采用的是Java方式配置SpringMVC并且使用了thymeleaf模板技术展示一个简单的首页。

Spring实战 七 SpringMVC的高级技术

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。

如下我们通过指定contextClasscontextConfigLocation来进行这项操作,把之前的两个配置文件WebConfig.javaRootConfig.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类型的数据示例

Spring实战 七 SpringMVC的高级技术

每个------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进行设置。

Spring实战 七 SpringMVC的高级技术

对于大多数情况下的继承AbstractAnnotationConfigDispatcherServletInitializer,我们只需要重写其中的customizeRegistration方法来设置参数就好了。

@Override
protected void customizeRegistration(ServletRegistration.Dynamic registration) {
    registration.setMultipartConfig(
            new MultipartConfigElement("/tmp/uploads")
    );
}

我们只使用了一个参数的构造器来指定上传的暂存路径,还可以有其它的构造器用来指定更多的参数。

Spring实战 七 SpringMVC的高级技术

假如我们不希望用户上传大于2MB的文件,不希望整个请求的数据高达4MB,所有文件都写入到磁盘中,那么可以使用如下设置。

@Override
protected void customizeRegistration(ServletRegistration.Dynamic registration) {
    registration.setMultipartConfig(
            new MultipartConfigElement("/tmp/uploads", 2097152, 4194304, 0)
    );
}

XML配置如下

Spring实战 七 SpringMVC的高级技术

处理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直接以文件的形式保存到本地。

Spring实战 七 SpringMVC的高级技术

@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状态码,并打印所有的异常堆栈。

Spring实战 七 SpringMVC的高级技术

这样不仅对用户不友好,而且将系统的异常堆栈暴露给用户对系统来说也很不安全。

如果我们在异常上使用@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();
}

Spring实战 七 SpringMVC的高级技术

编写处理异常的方法

大部分发生异常的时候我们都要进行处理然后返回一个自定义页面,而不只是返回一个默认页面给用户。

在Controller中定义一个方法,方法上声明一个@ExceptionHandler的注解并指定要捕获的异常类型。

@ExceptionHandler(UserNotFoundException.class)
public String userNotFoundHandler() {
    return "errors/user-not-found";
}

这个方法和普通的控制器方法没什么区别,也可以返回一个逻辑视图名,主要是该控制器内所有未捕获的UserNotFoundException异常都会被这个函数处理。

如下, 我们定义的异常页面被渲染。

Spring实战 七 SpringMVC的高级技术

控制器通知

如果很多个控制器中都可能会出现同种类型的异常,那么把处理异常的代码放到控制器中就不合适了。

控制器通知可以将这种异常的处理抽离出来,我们先写一个看看。

@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";
}
上一篇:SpringMVC 的工作原理


下一篇:Spring+SpringMvc常见面试题总结