• SpringMVC之四:渲染Web视图


    理解视图解析

    在前面的例子中,我们看到控制器返回的都是一个逻辑视图的名称,然后把这个逻辑视图名称交给view resolver,然后返回渲染后的 html 页面给 client。

    将控制器中请求处理的逻辑和视图中的渲染实现解耦是Spring MVC的一个重要特性。如果控 制器中的方法直接负责产生HTML的话,就很难在不影响请求处理逻辑的前提下,维护和更新 视图。控制器方法和视图的实现会在模型内容上达成一致,这是两者的最大关联,除此之外, 两者应该保持足够的距离。但是,如果控制器只通过逻辑视图名来了解视图的话,那Spring该如何确定使用哪一个视图实 现来渲染模型呢?这就是Spring视图解析器的任务了。

    优点:通过在控制器和视图之间传递 model 数据,可以使代码分离,逻辑清晰,更利于维护等优点。

    Spring MVC 定义了一个 ViewResolver 的接口:

    package org.springframework.web.servlet;
    
    import java.util.Locale;
    public interface ViewResolver {
        View resolveViewName(String viewName, Locale locale) throws Exception;
    }

    ViewResolver 方法,当提供一个 viewName 和 locale 以后,返回一个 View 实体。View 是另外一个接口:

    package org.springframework.web.servlet;
    public interface View {
        String getContentType();
    
        void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) throws Exception;
    
    }

    View 接口的作用就是利用 model 数据,还有 request 和 response 对象渲染视图内容,然后返回给 response。

    当然你在实际使用的过程中不会接触到这些内部的接口,因为 Spring 提供了很多视图技术的支持:FreeMarkerViewResolverInternalResourceViewResolverVelocityViewResolver等。

    Spring 4和Spring 3.2支持表中的所有视图解析器。Spring 3.1支持除Tiles 3 TilesViewResolver之外的所有视图解析器。

    对于表中的大部分视图解析器来讲,每一项都对应Java Web应用中特定的某种视图技术。InternalResourceViewResolver一般会用于JSP,TilesViewResolver用于 Apache Tiles视图,而FreeMarkerViewResolver和VelocityViewResolver分别对应 FreeMarker和Velocity模板视图。

    我们将会关注与大多数Java开发人员最息息相关的视图技术。因为大多数Java Web 应用都会用到JSP,我们首先将会介绍InternalResourceViewResolver,这个视图解析 器一般会用来解析JSP视图。接下来,我们将会介绍TilesViewResolver,控制JSP页面的布 局。

    二、InternalResourceViewResolver解析JSP视图

    Spring提供了两种支持JSP视图的方式:

    • InternalResourceViewResolver会将视图名解析为JSP文件。另外,如果在你的JSP 页面中使用了JSP标准标签库(JavaServer Pages Standard Tag Library,JSTL)的 话,InternalResourceViewResolver能够将视图名解析为JstlView形式的JSP文件, 从而将JSTL本地化和资源bundle变量暴露给JSTL的格式化(formatting)和信息(message)标 签。
    • Spring提供了两个JSP标签库,一个用于表单到模型的绑定,另一个提供了通用的工具类 特性。

    不管你使用JSTL,还是准备使用Spring的JSP标签库,配置解析JSP的视图解析器都是非常重要 的。尽管Spring还有其他的几个视图解析器都能将视图名映射为JSP文件,但就这项任务来 讲,InternalResourceViewResolver是最简单和最常用的视图解析器。

    2.1、配置适用于JSP的视图解析器

    有一些视图解析器,如ResourceBundleViewResolver会直接将逻辑视图名映射为特定 的View接口实现,而InternalResourceViewResolver所采取的方式并不那么直接。它 遵循一种约定,会在视图名上添加前缀和后缀,进而确定一个Web应用中视图资源的物理路径。

    作为样例,考虑一个简单的场景,假设逻辑视图名为home。通用的实践是将JSP文件放到Web 应用的WEB-INF目录下,防止对它的直接访问。如果我们将所有的JSP文件都放在“/WEBINF/views/”目录下,并且home页的JSP名为home.jsp,那么我们可以确定物理视图的路径就是 逻辑视图名home再加上“/WEB-INF/views/”前缀和“.jsp”后缀。如图所示。

     当使用@Bean注解的时候,我们可以按照如下的方式配置Internal-ResourceView Resolver,使其在解析视图时,遵循上述的约定。

        // 配置JSP视图解析器
        @Bean
        public ViewResolver viewResolver() {
            InternalResourceViewResolver resolver = new InternalResourceViewResolver();
            resolver.setPrefix("/WEB-INF/views/");
            resolver.setSuffix(".jsp");
            return resolver;
        }

    作为替代方案,如果你更喜欢使用基于XML的Spring配置,那么可以按照如下的方式配 置InternalResourceViewResolver:

    InternalResourceViewResolver配置就绪之后,它就会将逻辑视图名解析为JSP文件, 如下所示:

    • home将会解析为“/WEB-INF/views/home.jsp”
    • productList将会解析为“/WEB-INF/views/productList.jsp”
    • books/detail将会解析为“/WEB-INF/views/books/detail.jsp”

    2.2、解析JSTL视图

    到目前为止,我们对InternalResourceViewResolver的配置都很基础和简单。它最终会将逻辑视图名解析为InternalResourceView实例,这个实例会引用JSP文件。但是如果这 些JSP使用JSTL标签来处理格式化和信息的话,那么我们会希 望InternalResourceViewResolver将视图解析为JstlView。 JSTL的格式化标签需要一个Locale对象,以便于恰当地格式化地域相关的值,如日期和货币。信息标签可以借助Spring的信息资源和Locale,从而选择适当的信息渲染到HTML之中。 通过解析JstlView,JSTL能够获得Locale对象以及Spring中配置的信息资源。 如果想让InternalResourceViewResolver将视图解析为JstlView,而不是InternalResourceView的话,那么我们只需设置它的viewClass属性即可:

    同样,我们也可以使用XML完成这一任务:

     不管使用Java配置还是使用XML,都能确保JSTL的格式化和信息标签能够获得Locale对象 以及Spring中配置的信息资源。

    使用Spring的JSP库

    当为JSP添加功能时,标签库是一种很强大的方式,能够避免在脚本块中直接编写Java代码。 Spring提供了两个JSP标签库,用来帮助定义Spring MVC Web的视图。其中一个标签库会用来渲染HTML表单标签,这些标签可以绑定model中的某个属性。另外一个标签库包含了一些工 具类标签,我们随时都可以非常便利地使用它们。

      在这两个标签库中,你可能会发现表单绑定的标签库更加有用。所以,我们就从这个标签库开 始学习Spring的JSP标签。我们将会看到如何将Spittr应用的注册表单绑定到模型上,这样表单 就可以预先填充值,并且在表单提交失败后,能够展现校验错误。 将表单绑定到模型上 Spring的表单绑定JSP标签库包含了14个标签,它们中的大多数都用来渲染HTML中的表单标 签。但是,它们与原生HTML标签的区别在于它们会绑定模型中的一个对象,能够根据模型中 对象的属性填充值。标签库中还包含了一个为用户展现错误的标签,它会将错误信息渲染到 最终的HTML之中。

    异常处理

    在应用中抛出异常,最后还是需要写入到 response 中,Spring 提供如下方式将异常转化为 response

    • 特定的 Spring 异常自动映射为 HTTP 状态码
    • 异常可映射为 HTTP 状态码通过在异常上使用 @ResponseStatus 注解
    • 一个方法可用来处理异常通过在其上使用 @ExceptionHandler 注解

    异常映射 HTTP 状态码

    通过 @ResponseStatus 注解将异常和 HTTP 状态码对应:

    package spittr.web;
    
    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 RuntimeException {
    }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    现在只需要在控制器中抛出异常,就会被映射为指定的 HTTP 状态码:

    @RequestMapping(value="/{spittleId}", method=RequestMethod.GET)
    public String spittle(@PathVariable("spittleId") long spittleId,
            Model model) {
        Spittle spittle = spittleRepository.findOne(spittleId);
        if (spittle == null) {
            throw new SpittleNotFoundException();
        }
        model.addAttribute(spittle);
        return "spittle";
    }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    使用 exception-handling 方法

    映射异常到 HTTP 状态码的方式简单高效,但是如果需要返回更多的信息就不行了(比如返回一个 view 视图)。所以,Spring 提供了 @ExceptionHandler 注解,可像处理请求那样处理异常。

    @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";
      }
    }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    如上,我们在控制器中既包含了业务处理代码,也包含了异常处理代码。使用 @ExceptionHandler 就可以让你专注于业务逻辑代码,而在另外的方法中专门处理异常。

    @ExceptionHandler(DuplicateSpittleException.class)
    public String handleDuplicateSpittle() {
        return "error/duplicate";
    }
    • 1
    • 2
    • 3
    • 4

    注意,异常处理的方法必须放在会抛出该异常的控制器类中才行,也就是说只能捕获当前控制器类抛出的指定的异常。该异常处理返回了一个逻辑视图的名称,Spring 会根据这个名称返回相应的 html 页面。通过 @ExceptionHandler 注解,我们可以定义一个方法处理所有该控制器中任意 handler 抛出的 DuplicateSpittleException 异常,简化了代码。

    // 新的业务逻辑 handler
    @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";
    }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    但是,需要为每个控制器都定义一个 @ExceptionHandler 方法是不是有点重复。所以,一般情况下会把 exception-handler 方法放在 BaseController,或者使用 @ControllerAdvice

    Advising controllers

    为了使某些方法(如 exception-handler 方法)在全部的控制器中都能够发挥作用,Spring 3.2 引入了 controller adviceController advice 是一个被 @ControllerAdvice 注解的类,它包含一个或多个如下类型的方法:

    • 被 @ExceptionHandler 注解的方法
    • 被 @InitBinder 注解的方法
    • 被 @ModelAttribute 注解的方法

    在被 @ControllerAdvice 注解的类中的这些方法能够应用到所有被 @RequestMapping 注解的方法。

    因为,@ControllerAdvice 本身被 @Component 注解,所以能够被 component-scan 扫描被注入,就像 @Controller 注解一样。

    如下,就是使用 @ControllerAdvice 为所有的控制器定义 @ExceptionHandler 异常处理方法。

    package spitter.web;
    import org.springframework.web.bind.annotation.ControllerAdvice;
    import org.springframework.web.bind.annotation.ExceptionHandler;
    
    @ControllerAdvice
    public class AppWideExceptionHandler {
    
        @ExceptionHandler(DuplicateSpittleException.class)
        public String duplicateSpittleHandler() {
            return "error/duplicate";
        }
    
    }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    现在,然后被 @RequestMapping 注解的控制器方法如果抛出 DuplicateSpittleException 异常,都可以被该方法捕获到,从而进行处理。


    面向资源的控制器

    因为 Javascript 在客户端的大量使用,现在服务器端很多时候需要传回 XML 或 JSON 格式的数据,那么 Spring MVC 如何返回不同类型的数据,而不是返回 html 页面呢?

    • Content negotiation:一种把 model 数据渲染成客户端需要的格式的视图
    • Message conversion:能够把控制器返回的对象转换为 client 需要的格式的转换器,没有了视图渲染的环节

    因为,Content negotiation 只能转换 model 数据,而 model 本质上来说是 map 类型的数据格式。所以,转换后的数据可能并不是理想的 client 需要的格式。出于这个原因,我们倾向于使用 Message conversion。

    使用 HTTP message converters

    Spring MVC 自带多种 message converters:Jaxb2RootElementHttpMessageConverterMappingJacksonHttpMessageConverterMappingJackson2HttpMessageConverterResourceHttpMessageConverter, 其他的 converters 请参考官方文档 HTTP Message Conversion

    那么如何使用这些 converters:

    • 向 client 发送数据:根据 request’s Accept header 确定
    • 从 client 接收数据:根据 Content-Type header 确定

    很多 converter 都是默认注册的,所以你不需要格外的配置,但是可能需要添加额外的依赖到项目的 classpath 中以便使用这些 converters。比如,如果需要使用 MappingJacksonHttpMessageConverter 在 JSON messages 和 Java 对象之间相互转换,你需要添加 Jackson JSON Processor 依赖到 classpath 中。自定义 Jackson converter 参考 Latest Jackson integration improvements in Spring

    自定义 Message converters

    除了使用 Spring MVC 默认提供的 message converters 外,我们还可以自定义 converter。比如自定义 MappingJackson2HttpMessageConverter,让其可以处理其他的 media types。

    @Configuration
    @EnableWebMvc
    @ComponentScan("org.acherie.demo.web")
    public class WebConfig extends WebMvcConfigurerAdapter {
    
        ...
    
        @Override
        public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
            converters.add(jacksonConverter());
        }
    
        @Bean
        public MappingJackson2HttpMessageConverter jacksonConverter() {
            List<MediaType> mediaTypes = new ArrayList<>();
            mediaTypes.add(MediaType.APPLICATION_XML);
    
            MappingJackson2HttpMessageConverter converter = 
                    new MappingJackson2HttpMessageConverter();
            converter.setSupportedMediaTypes(mediaTypes);
            return converter;
        }
    }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    @ResponseBody 返回资源

    如果你需要返回 JSON 或 XML 到 client,你需要告诉 Spring 略过通常的 model/view 流程,并且使用 message converter。而这个东西就是 @ResponseBody 注解:

    @RequestMapping(method=RequestMethod.GET, produces="application/json")
    public @ResponseBody List<Spittle> spittles(
        @RequestParam(value="max", defaultValue=MAX_LONG_AS_STRING) long max,
        @RequestParam(value="count", defaultValue="20") int count) {
      return spittleRepository.findSpittles(max, count);
    }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    @ResponseBody 告诉 Spring 你想将控制器返回的 Java object 转换(使用 converter)为资源返回 client。更近一步,DispatcherServlet 会通过请求的 Accept header 确定 client 想要的格式(比如 JSON),然后寻找合适的 message converter 去做转换。

    @RequestBody 接收 client 的资源

    使用 @RequestBody 告诉 Spring 根据 Content-Type header 确定合适的 message converter 转换资源(比如 JSON,XML)为 Java Objects。

    @RequestMapping(method=RequestMethod.POST, consumes="application/json")
    @ResponseStatus(HttpStatus.CREATED)
    public ResponseEntity<Spittle> saveSpittle(@RequestBody Spittle spittle, UriComponentsBuilder ucb) {
      Spittle saved = spittleRepository.save(spittle);
    
      HttpHeaders headers = new HttpHeaders();
      URI locationUri = ucb.path("/spittles/")
          .path(String.valueOf(saved.getId()))
          .build()
          .toUri();
      headers.setLocation(locationUri);
    
      ResponseEntity<Spittle> responseEntity = new ResponseEntity<Spittle>(saved, headers, HttpStatus.CREATED);
      return responseEntity;
    }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    错误状态返回

    当控制器出现错误时,我们可能需要返回不同的 HTTP 状态码。在 Spring MVC 中有如下两种方式可以选择:

    • ResponseEntity
    • @ResponseStatus

    使用 ResponseEntity 可以不用使用 @ResponseBody,Spring MVC 知道会使用 message converter 去转换。

    我们首先定义 Error class:

    public class Error {
        private int code;
        private String message;
    
        public Error(int code, String message) {
            this.code = code;
            this.message = message;
        }
        public int getCode() {
            return code;
        }
        public String getMessage() {
            return message;
        }
    }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    然后使用 ResponseEntity 返回对于信息:

    @RequestMapping(value="/{id}", method=RequestMethod.GET)
    public ResponseEntity<?> spittleById(@PathVariable long id) {
        Spittle spittle = spittleRepository.findOne(id);
        if (spittle == null) {
            Error error = new Error(4, "Spittle [" + id + "] not found");
            return new ResponseEntity<Error>(error, HttpStatus.NOT_FOUND);
        }
        return new ResponseEntity<Spittle>(spittle, HttpStatus.OK);
    }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    而如果要使用 @ResponseStatus 的话,我们需要使用把错误处理 的代码提出来放在其他地方,因为一个方法只能有一个 @ResponseStatus 注解。我们使用 @ExceptionHandler 注解来统一处理控制器中抛出的异常@ExceptionHandler 需使用在控制器方法上:

    @ExceptionHandler(SpittleNotFoundException.class)
    public ResponseEntity<Error> spittleNotFound(SpittleNotFoundException e) {
        long spittleId = e.getSpittleId();
        Error error = new Error(4, "Spittle [" + spittleId + "] not found");
        return new ResponseEntity<Error>(error, HttpStatus.NOT_FOUND);
    }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    // SpittleNotFoundException类
    public class SpittleNotFoundException extends RuntimeException {
        private long spittleId;
    
        public SpittleNotFoundException(long spittleId) {
            this.spittleId = spittleId;
        }
        public long getSpittleId() {
            return spittleId;
        }
    }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    这样所有控制器抛出的 SpittleNotFoundException 异常都会被该处理器处理,看到我们还是使用的 ResponseEntity 返回的 404 状态码。现在原来的控制器就变得简单了:

    @RequestMapping(value="/{id}", method=RequestMethod.GET)
    public ResponseEntity<Spittle> spittleById(@PathVariable long id) {
        Spittle spittle = spittleRepository.findOne(id);
        if (spittle == null) { throw new SpittleNotFoundException(id); }
        return new ResponseEntity<Spittle>(spittle, HttpStatus.OK);
    }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    接下来,我们使用 @ResponseStatus 注解:

    @RequestMapping(value="/{id}", method=RequestMethod.GET)
    @ResponseStatus(HttpStatus.OK)
    public @ResponseBody Spittle spittleById(@PathVariable long id) {
        Spittle spittle = spittleRepository.findOne(id);
        if (spittle == null) { throw new SpittleNotFoundException(id); }
        return spittle;
    }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    其中,因为控制器默认会返回 200(OK),所以这里的 @ResponseStatus 注解是可以省略的。

    我们也可以对异常处理方法做同样的简化:

    @ExceptionHandler(SpittleNotFoundException.class)
    @ResponseStatus(HttpStatus.NOT_FOUND)
    public @ResponseBody Error spittleNotFound(SpittleNotFoundException e) {
        long spittleId = e.getSpittleId();
        return new Error(4, "Spittle [" + spittleId + "] not found");
    }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    这样就能够返回 404(NOT_FOUND)状态码,而不需要使用 @ResponseEntity 了。

  • 相关阅读:
    单机部署Fastfds+nginx
    day_ha配置文件
    day_1_登录接口

    表(list)
    Java基础01 ------ 从HelloWorld到面向对象
    测试V模型
    360极速模式和兼容模式区别
    初识VBS
    Bug描述规范
  • 原文地址:https://www.cnblogs.com/duanxz/p/3803705.html
Copyright © 2020-2023  润新知