Spring实战第七章————SpringMVC配置的替代方案

2021-07-04 08:06

阅读:604

标签:app   使用   pre   rtu   position   创建   max   模版   post   

SpringMVC配置的替代方案

自定义DispatherServlet配置

我们之前在SpittrWebAppInitializer所编写的三个方法仅仅是必须要重载的abstract方法。但还有更多的方法可以进行重载,从而实现额外的配置。

例如customizeRegistration()。在AbstractAnnotationConfigDispatcherServletInitializer将DispatcherServlet主车道Servlet容器后,就会调用该方法,并将Servlet注册后得到的Registration.Dynamic传递进来。例如稍后我们将要计划使用Servlet3.0对multipart的支持,那么需要使用DispatcherServlet的registration来启用multipart请求。

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

借助customizeRegistration()方法的ServletRegistration.Dynamic来设置MultipartConfigElement。

添加其它的Servlet和Filter

基于Java的初始化器(initializer)的一个好处就在于我们可以定义任意数量的初始化器类。
因此,如果需要定义额外的组件,只需新建相应的初始化类即可。最简单的方法就是实现Spring的WebApplicationInitializer接口。

import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletRegistration.Dynamic;
import org.springframework.web.WebApplicationInitializer;
import com.myapp.MyServlet;
public class MyServletInitializer implements WebApplicationInitializer {
    @Override
    public void onStartup(ServletContext servletContext) throws ServletException {
        // 定义servlet
        Dynamic myServlet = servletContext.addServlet("myServlet", MyServlet.class);
        // 映射servlet
        myServlet.addMapping("/custom/**");
    }
}

这段代码注册了一个Servlet并将其映射到一个路径上。我们也可以用这个方式来手动注册DispatcherServlet。类似的我们也可以这样注册Filter和Listener。

@Override
public void onStartup(ServletContext servletContext) throws ServletException {
    // 注册一个filter
    javax.servlet.FilterRegistration.Dynamic filter = servletContext.addFilter("myFilter", MyFilter.class);
    // 添加映射
    filter.addMappingForUrlPatterns(null, false, "/custom/*");
}

WebApplicationInitializer是一个在注册servlet、filter、listener时比较推荐的方式,当然你是使用基于Java的配置方式并将应用部署在Servlet3.0容器上的。如果你仅仅需要注册一个filter并将其映射到DispatcherServlet,那么也可以使用AbstractAnnotationConfigDispatcherServletInitializer。要注册多个filter并将它们映射到DispatcherServlet,你所要做的仅仅是重写getServletFilters()方法。比如:

@Override
protected Filter[] getServletFilters() {
    return new Filter[] { new MyFilter() };
}

如你所见,该方法返回了一个javax.servlet.Filter的数组,这里仅仅返回了一个filter,但是它可以返回很多个。同时这里不再需要为这些filter去声明映射,因为通过getServletFilters()返回的filter会自动地映射到DispatcherServlet。

在web.xml中声明DispatcherServlet

在之前我们是使用AbstractAnnoatationConfigDispatcherServletInitializer自动注册DispatcherServlet和ContextLoaderListener。但也可以按传统方法在web.xml中注册。

contextConfigLocation/WEB-INF/spring/root-context.xmlorg.springframework.web.context.ContextLoaderListenerappServletorg.springframework.web.servlet.DispatcherServlet1appServlet/

设置web.xml使用基于Java的配置

contextClass
            org.springframework.web.context.support.AnnotationConfigWebApplicationContext
        contextConfigLocationspittr.config.RootConfig
            org.springframework.web.context.ContextLoaderListener
        appServletorg.springframework.web.servlet.DispatcherServletcontextClass
                org.springframework.web.context.support.AnnotationConfigWebApplicationContext
            contextConfigLocation
                spittr.config.WebConfigConfig
            1appServlet/

处理multipart形式的数据

在WEB应用中用户经常会上传内容。而Spittr应用在两个地方需要文件上传。当新用户注册应用的时候,会需要他们上传一张图片。而当他们提交新的Spittle时可能会上传图片。一般表单提交形成的请求结果很简单,就是以&为分割符的多个name-value。尽管这种编码形式很简单,但对像图片这样的二进制数据就不合适了。而multipart格式的数据会将一个表单后拆分为多个部分,每个部分对应一个输入域。下面展现mulrtipart的请求体:

------WebKitFormBoundaryqgkaBn8IHJCuNmiW
Content-Disposition: form-data; name="firstName"
Charles
------WebKitFormBoundaryqgkaBn8IHJCuNmiW
Content-Disposition: form-data; name="lastName"
Xavier
------WebKitFormBoundaryqgkaBn8IHJCuNmiW
Content-Disposition: form-data; name="email"
charles@xmen.com
------WebKitFormBoundaryqgkaBn8IHJCuNmiW
Content-Disposition: form-data; name="username"
professorx
------WebKitFormBoundaryqgkaBn8IHJCuNmiW
Content-Disposition: form-data; name="password"
letmein01
------WebKitFormBoundaryqgkaBn8IHJCuNmiW
Content-Disposition: form-data; name="profilePicture"; filename="me.jpg"
Content-Type: image/jpeg
[[ Binary image data goes here ]]
------WebKitFormBoundaryqgkaBn8IHJCuNmiW--

尽管multipart看起来复杂,但在SpringM中处理却很容易。而首先需要要我们配置一个multipart解析器。

配置multipart解析器

DispatcherServlet并没有任何实现解析multipart请求数据的功能。他将这任务委托给Spring中MultipartResolver策略接口的实现,从Spring3.1开始,Spring内置了两个MultipartResolver的实现供我们选择:

  • CommonMultipartResolver:使用Jakarta Commons FileUpload解析multipart请求;
  • StandardServletMultipartResolver:依赖于Servlet3.0对multipart请求的支持(始于Spring3.1)

一般选用StandardServletMultipartResolver。兼容Servlet3.0的StandardServletMultipartResolver没有构造器参数,也没有要设的属性。因此在Spring上下文中将其声明为bean会非常简单,如下所示:

 @Bean
    public MultipartResolver multipartResolver() throws IOException {
        return new StandardServletMultipartResolver();
    }

那么如何配置StandardServletMultipartResolver的限制条件呢?我们会在Servlet中指定multipart的限定条件。至少也要写入文件上传的过程中所写入得临时文件路径。如果不设定这个最基本配置的话,StandardServletMultipartResolver就无法正常工作。所以我们会在web.xml或Servlet初始化类中将multipart的具体细节作为DispatcherServlet配置的一部分。采用Servlet初始化类的方式来配置:

DispatcherServlet ds=new DispatcherServlet();
Dynamic registration=context.addServlet("appServlet",ds);
registration.addMapping("/");
registration.asetMultipartConfig(
    new MultipartCofigElement("/tmp/spittr/upolads"));

如果配置的Servlet初始化类继承了AbstractAnnotationConfigDispatcherServletInitializer或AbstractDispatcherServletInitializer的话,可以重载customizeRegistration()方法来配置multipart的具体细节。

//设置multipart上传配置,路径,文件不超过2MB,请求不超过4MB
    @Override
    protected void customizeRegistration(ServletRegistration.Dynamic registration){
        registration.setMultipartConfig(
                new MultipartConfigElement("C:/test",2097152,4194304,0));
    }

MultipartConfigElement构造器也可以进行其他一些设置:

  • 文件上传的最大值(byte),默认没有限制;
  • 所有multipart请求的文件最大值(byte),不管有多少个请求,默认无限制;
  • 直接上传文件(不需存储到临时目录)的最大值(byte),默认是0,也就是所有的文件都要写入硬盘;

如果你是使用的传统的web.xml的方式来设置的DispatcherServlet,那么就需要使用多个

appServletorg.springframework.web.servlet.DispatcherServlet1/tmp/spittr/uploads20971524194304

处理multipart请求

当配置好了对multipart请求的处理,接下来要编写控制器方法来接受上传的文件。实现这点最常见的方法就是在某个控制器方法参数上添加@Requestpart注解。
假设你想让用户可以在注册时上传图像,那么就需要对注册表单进行更改从而用户可以选择一个图片,同时还需要更改SpitterController中的processRegistration()方法以获取上传的文件。现在所需做的就是更新processRegistration()方法:

@RequestMapping(value = "/register", method = RequestMethod.POST)
public String processRegistration(@RequestPart("profilePicture") byte[] profilePicture, @Valid Spitter spitter,
        Errors errors) {
            ···
        }

当注册表单提交时,请求部分的数据就会赋予到profilePicture属性中,如果用户没有选中一个文件,那么该数组就会是一个空值(不是null)。既然已经获取到上传的文件,下面所需要的就是将文件保存。

处理异常

不管发生什么事情Servlet请求的输出都是一个Servlet响应,所以如果出现异常,那么它的输出依旧是Servlet响应,一场必须是以某种方式转换为响应。Spring提供了多种方式将一场转换为响应:

  • 某些Spring异常会自动的映射为特定的HTTP状态码;
  • 使用@ResponseStatus注解将一个异常映射为HTTP状态码;
  • 使用ExceptionHandler注解的方法可以用来处理异常

将异常映射为HTTP状态码

在默认情况下,Spring将自身的一些异常转换为合适的状态码。

Spring异常 HTTP状态码
BindException 400 - Bad Request
ConversionNotSupportedException 500 - Internal Server Error
HttpMediaTypeNotAcceptableException 406 - Not Acceptable
HttpMediaTypeNotSupportedException 415 - Unsupported Media Type
HttpMessageNotReadableException 400 - Bad Request
HttpMessageNotWritableException 500 - Internal Server Error
HttpRequestMethodNotSupportedException 405 - Method Not Allowed
MethodArgumentNotValidException 400 - Bad Request
MissingServletRequestParameterException 400 - Bad Request
MissingServletRequestPartException 400 - Bad Request
NoSuchRequestHandlingMethodException 404 - Not Found
TypeMismatchException 400 - Bad Request

将异常映射为特定的状态码

import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;

@ResponseStatus(value=HttpStatus.NOT_FOUND, reason="Spittle Not Found")
public class SpittleNotFoundException extends Exception {

}

编写异常处理的方法

将异常映射为状态码大多数情况下是比较简单有效的,但是如果想让响应不仅仅只有一个状态码呢?也许你想对异常进行一些处理,就行处理请求一样。

例如,SpittleRepository的save()方法在用户重复创建Spittle时抛出了一个DuplicateSpittleException,那么SpittleController的saveSpittle()方法就需要处理该异常。如下面的代码所示,saveSpittle()方法可以直接处理该异常:

@RequestMapping(method = RequestMethod.POST)
public String saveSpittle(SpittleForm form, Model model) {
    try {
        spittleRepository.save(new Spittle(null, form.getMessage(), 
                new Date(), form.getLongitude(), form.getLatitude()));
        return "redirect:/spittles";
    } catch (DuplicateSpittleException e) {
        return "error/duplicate";
    }
}

这个方法有两个路径,我们可以用别的方法处理异常,那这个方法可以简单点。首先处理正确路径的saveSpittle方法:

@RequestMapping(method = RequestMethod.POST)
public String saveSpittle(SpittleForm form, Model model) {
        spittleRepository.save(new Spittle(null, form.getMessage(), 
                new Date(), form.getLongitude(), form.getLatitude()));
        return "redirect:/spittles";
}

现在在SpittleController中添加一个新的方法:

@ExceptionHandler(DuplicateSpittleException.class)
public String handleDuplicateSpittle() {
    return "error/duplicate";
}

@ExceptionHandler注解应用在handleDuplicateSpittle()方法上,用来指定在有DuplicateSpittleException异常抛出时执行。而值得注意的是,@ExceptionHandler注解的方法在同一个控制器里是通用的额,即无论SpittleController的哪一个方法抛出DuplicateSpittleException异常,handleDuplicateSpittle()方法都可以对其进行处理,而不再需要在每一个出现异常的地方进行捕获。那么,@ExceptionHandler注解的方法能不能捕获其他controller里的异常啊?在Spring3.2里是可以的,但仅仅局限于定义在控制器通知类里的方法。
那什么是控制器通知类呢?这就是接下来要介绍的

为控制器添加通知

如果controller类的特定切面可以跨越应用的所有controller进行使用,那么这将会带来极大的便捷。例如,@ExceptionHandler方法就可以处理多个controller抛出的异常了。如果多个controller类都抛出同一个异常,也许你会在这些controller进行重复的@ExceptionHandler方法编写。或者,你也可以编写一个异常处理的基类,供其他@ExceptionHandler方法进行继承。

Spring3.2带来了另外一种解决方法:控制器通知。控制器通知是任意带有@ControllerAdvice注解的类,这个类会包含一个或多个如下类型的方法:

  • @ExceptionHandler注解的
  • @InitBinder注解的
  • @ModelAttribute注解的

@ControllerAdvice注解的类中的这些方法会在整个应用中的所有controller的所有@RequestMapping注解的方法上应用。
@ControllerAdvice注解本身是使用了@Component注解的,因此,使用@ControllerAdvice注解的类会在组件扫描时进行提取,就行使用@Controller注解的类一样。@ControllerAdvice的最实用的一个功能就是将所有的@ExceptionHandler方法集成在一个类中,从而可以在一个地方处理所有controller中的异常。例如,假设你想处理应用中所有的DuplicateSpittleException异常,可以采用下面的方法:

import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;

// 声明控制器增强
@ControllerAdvice
public class AppWideExceptionHandler {

    // 定义异常处理方法
    @ExceptionHandler(DuplicateSpittleException.class)
    public String handleDuplicateSpittle() {
        return "error/duplicate";
    }

    @ExceptionHandler(SpittleNotFoundException.class)
    public String handleSpittleNotFound() {
        return "error/duplicate";
    }

}

跨重定向请求传递数据

在处理完POST请求过后通常应该执行重定向。这样可以避免用户点击浏览器的刷新按钮或后退按钮时,客户端重新执行危险的POST请求。在第五章中,已经在控制器方法返回的视图名称中使用了redirect:前缀,这时返回的String不是用来寻找视图,而是浏览器进行跳转的路径:

return "redirect:/spitter/" + spitter.getUsername();

也许你认为Spring处理重定向只能这样了,但是:Spring还可以做得更多。
特别是一个重定向方法如何向处理重定向的方法发送数据呢?一般的,当一个处理函数结束后,方法中的model数据都会作为request属性复制到request中,并且request会传递到视图中进行解析。因为控制器和视图面对的是同一个request,因此request属性在forward时保留了下来。
但是,当一个控制器返回的是一个redirect时,原来的request会终止,并且会开启一个新的HTTP请求。原来request中所有的model数据都会清空。新的request不会有任何的model数据。
技术分享图片

明显的,现在不能再redirect时使用model来传递数据了。但是还有其他方法用来从重定向的方法中获取数据:

  • 将数据转换为路径参数或者查询参数
  • 在flash属性中发送数据

通过URL模版进行重定向

@RequestMapping(value="/",method=POST)
public String processRegistration(Spitter spitter,Model model){
  spitterRepository.save(spitter);
  model.addAttribute("username",spitter.getUsername());
  model.addAttribute("spitterId",spitter.getId());
  return "redirect:/spitter/{username}";
}

返回的重定向String并没有什么变化,但是由于model中的spitterId属性并没有映射到URL中的占位符,它会自动作为查询参数。
如果username是habuma,spitterId是42,那么返回的重定向路径将是/spitter/habuma?spitterId=42。
使用路径参数和查询参数传递数据比较简单,但是它也有局限性。它只适用于传递简单值,比如String和数字,不能传递比较复杂的东西,那么我们就需要flash属性来帮忙。

使用flash属性

@RequestMapping(value="/",method=POST)
public String processRegistration(Spitter spitter,RedirectAttributes model){
  spitterRepository.save(spitter);
  model.addAttribute("username",spitter.getUsername());
  model.addFlashAttribute("spitter",spitter);
  return "redirect:/spitter/{username}";
}

我们传递了一个Spitter对象给addFlashAttribute()方法,在重定向之前,所有的flash属性都会复制到会话中,在重定向之后,存在会话中的flash属性会被取出,并从会话转移到模型之中。处理重定向的方法就能从模型中访问Spitter对象了

Spring实战第七章————SpringMVC配置的替代方案

标签:app   使用   pre   rtu   position   创建   max   模版   post   

原文地址:https://www.cnblogs.com/wbw2621/p/9614105.html


评论


亲,登录后才可以留言!