Spring MVC 筆記 (四) Working with View Resolver

1 Resolving Views

According to Spring MVC, a View is identifiable as an implementation of the org.springframework.web.servlet.View interface. The render method should render proper content as a response based on the given Model and request.

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

Spring MVC does not encourage you to couple the view object with the Controller as it will lead the controller method to tightly couple with one specific View technology. Instead, what we can do is return a logical View name and configure an appropriate view resolver of our choice in our web app context to create a view object.

Spring comes with quite a few view resolvers to resolve various type of Views. InternalResourceViewResolver will resolve the actual View file path by prepending the configured prefix value and appending the suffix value with the logical View name; the logical View name is the value usually returned by the Controller’s method.

2 RedirectView

Spring MVC has a special View object that handles redirection and forwarding. To use a RedirectView with our Controller, we simply need to return the target URL string with the redirection prefix from the Controller. There are two redirection prefixes available in Spring MVC: redirect adn forward.

POST/Forward/GET pattern

Traditionally, if we handle this scenario via the POST/Forward/GET pattern, then it may sometimes cause multiple form submission issues. The user might press F5 and the same form will be submitted again.

POST/Redirect/GET pattern

To resolve this issue, the POST/Redirect/GET pattern is used in many web applications. Thus if the user even presses F5 multiple times, the GET request gets loaded instead of submitting the form again and again.

Flash attributes
Usually, when we perform an HTTP request redirection, the data stored in the original request is lost, making it impossible for the next GET request to access it after redirection. Flash attributes can help in such cases.

Flash attributes provide a way for us to store information that is intended to be used in another request. Flash attributes are saved temporarily in a session to be available for an immediate request after redirection.

In order to use Flash attributes in your Spring MVC application, just add the RedirectAttributes parameter to your Spring Controller’s method as follows:

@Controller
public class HomeController {

    //爲了做實驗,把 Model去掉,直接訪問/ 時,頁面沒有內容
    //但如果是下面的方法重定向過來就有內容,因爲它帶了redirectAttributes
    @RequestMapping("/")
    public String welcome() {
        return "welcome";
    }

    //當訪問 /welcome/greeting 重定向到 / , 並帶了redirectAttributes
    @RequestMapping("/welcome/greeting")
    public String welcome(RedirectAttributes redirectAttributes) {      
        redirectAttributes.addFlashAttribute("greeting", "Welcome to Web Store!");
        redirectAttributes.addFlashAttribute("tagline", "The one and only amazing web store");
        return "redirect:/";
    }
}

3 Serving static resources

What if we have some static content that we want to serve to the client? We have a directory (/resources/images/) that contains some product images and we want to serve those images upon request.

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/img/**").addResourceLocations("/resources/images/");
    }

然後把圖片放在 src/main/webapp/resources/images/ 目錄下就可以了.
訪問 http://localhost:8080/webstore/img/P1234.png 會返回該圖片.

adding images to the product detail page

<img src="<c:url value="/img/${product.productId}.png"></c:url>" alt="image" style="width: 100%" />

4 Multipart requests

What if we were able to upload the images to the image directory? How can we do this? Here comes the multipart request. A multipart request is a type of HTTP request to send files and data to the server.

step1, 註冊一個 multipartResolver

@Bean
public CommonsMultipartResolver multipartResolver() {
CommonsMultipartResolver resolver=new
CommonsMultipartResolver();
resolver.setDefaultEncoding("utf-8");
return resolver;
}

step2 ,使用 commons-fileupload 實現

        <dependency>
            <groupId>commons-fileupload</groupId>
            <artifactId>commons-fileupload</artifactId>
            <version>1.3.3</version>
        </dependency>

step3 , domain 類加一個屬性,並getter ,setter

private MultipartFile productImage;

step4, addProduct.jsp 添加一個文件上傳UI,並添加 form 表單的enctype屬性

<form:input id="productImage" path="productImage" type="file" class="form:input-large" />
<form:form modelAttribute="newProduct" class="formhorizontal" enctype="multipart/form-data">

step5, 在 ProductController 的方法中添加處理多文件上傳的邏輯

    @RequestMapping(value = "/products/add", method = RequestMethod.POST)
    public String processAddNewProductForm(@ModelAttribute("newProduct") Product newProduct, BindingResult result,
            HttpServletRequest request) {
        String[] suppressedFields = result.getSuppressedFields();
        if (suppressedFields.length > 0) {
            throw new RuntimeException("Attempting to bind disallowed fields: "
                    + StringUtils.arrayToCommaDelimitedString(suppressedFields));
        }

        MultipartFile productImage = newProduct.getProductImage();
        String rootDirectory = request.getSession().getServletContext().getRealPath("/");
        if (productImage != null && !productImage.isEmpty()) {
            try {
                productImage
                        .transferTo(new File(rootDirectory + "resources\\images" + newProduct.getProductId() + ".png"));
            } catch (Exception e) {
                throw new RuntimeException("Product Image saving failed", e);
            }
        }

        productService.addProduct(newProduct);
        return "redirect:/market/products";
    }

方法多了一個參數:HttpServletRequest request ,用來獲取 ServletContext

String rootDirectory = request.getSession().getServletContext().getRealPath("/");

step6, initialiseBinder 添加白名單:

@InitBinder
    public void initialiseBinder(WebDataBinder binder) {
        binder.setAllowedFields("productId", "name", "unitPrice", "description", "manufacturer", "category",
                "unitsInStock", "condition","productImage");
    }

5 ContentNegotiatingViewResolver

Content negotiation is a mechanism that makes it possible to serve different representations of the same resource. ContentNegotiatingViewResolver does not resolve Views itself, but rather delegates to other view resolvers based on the request. Using ContentNegotiatingViewResolver, we can incorporate many Views such as MappingJacksonJsonView (for JSON) and MarshallingView (for XML) to represent the same product information in a XML or JSON format.

step1, spring-oxm 和 jackson-databind

        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-oxm</artifactId>
            <version>${spring-version}</version>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>2.9.5</version>
        </dependency>

step2 , 配置

    @Bean
    public ViewResolver contentNegotiatingViewResolver(ContentNegotiationManager manager) {
        ContentNegotiatingViewResolver resolver = new ContentNegotiatingViewResolver();
        resolver.setContentNegotiationManager(manager);
        ArrayList<View> views = new ArrayList<>();
        views.add(jsonView());
        views.add(xmlView());
        resolver.setDefaultViews(views);
        return resolver;
    }

    @Bean
    public MappingJackson2JsonView jsonView() {
        MappingJackson2JsonView jsonView = new MappingJackson2JsonView();
        jsonView.setPrettyPrint(true);
        return jsonView;
    }

    @Bean
    public MarshallingView xmlView() {
        Jaxb2Marshaller marshaller = new Jaxb2Marshaller();
        marshaller.setClassesToBeBound(Product.class);
        MarshallingView xmlView = new MarshallingView(marshaller);
        return xmlView;
    }

step3 , databinding

@XmlRootElement   --- 實體類爲根節點
@XmlTransient   --- 不綁定某個屬性
@JsonIgnore     --- 不綁定某個屬性

step4 , 修改訪問鏈接:

//正常的,即 JSP view resolver 返回的頁面
http://localhost:8080/webstore/market/product?id=P1235 

// xml,返回的是 MarshallingView
http://localhost:8080/webstore/market/product.xml?id=P1235 

// json,返回的是 MappingJackson2JsonView
http://localhost:8080/webstore/market/product.json?id=P1235 

6 HandlerExceptionResolver

In Spring, one of the main exception handling constructs is the HandlerExceptionResolver. . Any objects that implement this interface can resolve exceptions thrown during Controller mapping or execution. HandlerExceptionResolver implementers are typically registered as beans in the web application context.

Spring MVC creates two such HandlerExceptionResolver implementations by default to facilitate exception handling.

  • ResponseStatusExceptionResolver is created to support the @ResponseStatus annotation.
  • ExceptionHandlerExceptionResolver is created to support the @ExceptionHandler annotation.

    adding a ResponseStatus exception
    @ResponseStatus, value 對應status code , reason 對應 message

package com.webstore.exception;

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

@ResponseStatus(value = HttpStatus.NOT_FOUND, reason = "No products found under this category")
public class NoProductsFoundUnderCategoryException extends RuntimeException {

}

沒有查到此類型的商品是拋出這個異常

    @RequestMapping("/products/{category}")
    public String getProductsByCategory(Model model, @PathVariable("category") String productCategory) {

        List<Product> products = productService.getProductsByCategory(productCategory);

        if (products == null || products.isEmpty()) {
            throw new NoProductsFoundUnderCategoryException();
        }

        model.addAttribute("products", productService.getProductsByCategory(productCategory));
        return "products";
    }

adding an exception handler
step1 , 定製錯誤時返回的頁面:productNotFound.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%@ taglib prefix="spring" uri="http://www.springframework.org/tags"%>
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<link rel="stylesheet"
    href="//netdna.bootstrapcdn.com/bootstrap/3.0.0/css/bootstrap.min.css">
<title>Welcome</title>
</head>
<body>
    <section>
        <div class="jumbotron">
            <div class="container">
                <h1 class="alert alert-danger">There is no product found with
                    the Product id ${invalidProductId}</h1>
            </div>
        </div>
    </section>
    <section>
        <div class="container">
            <p>${url}</p>
            <p>${exception}</p>
        </div>
        <div class="container">
            <p>
                <a href="<spring:url value="/market/products" />"
                    class="btn btn-primary"> <span
                    class="glyphicon-hand-left glyphicon"> </span> products
                </a>
            </p>
        </div>
    </section>
</body>
</html>

step2, ProductController 添加一個 handleError 方法

    @ExceptionHandler(ProductNotFoundException.class)
    public ModelAndView handleError(HttpServletRequest req, ProductNotFoundException exception) {
        ModelAndView mav = new ModelAndView();
        mav.addObject("invalidProductId", exception.getProductId());
        mav.addObject("exception", exception);
        mav.addObject("url", req.getRequestURL() + "?" + req.getQueryString());
        mav.setViewName("productNotFound");
        return mav;
    }

step3 , 創建 ProductNotFoundException 類

package com.webstore.exception;

public class ProductNotFoundException extends RuntimeException {

    private String productId;

    public ProductNotFoundException(String productId) {
        this.productId = productId;
    }

    public String getProductId() {
        return productId;
    }
}

step4, 修改 InMemoryProductRepository 方法

    @Override
    public Product getProductById(String productID) {
        String SQL = "SELECT * FROM PRODUCTS WHERE ID = :id";
        Map<String, Object> params = new HashMap<String, Object>();
        params.put("id", productID);
        try {
            return jdbcTemplate.queryForObject(SQL, params, new ProductMapper());
        } catch (DataAccessException e) {
            throw new ProductNotFoundException(productID);
        }
    }
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章