SpringMVC 进一步学习

@PathVariable

取得 URL 中的匹配的内容,适合 REST 的风格。

A @PathVariable argument can be of any simple type such as int, long, Date, etc. Spring automatically converts to the appropriate type or throws a TypeMismatchException if it fails to do so. You can also register support for parsing additional data types.

package controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class ParameterController {
    // {userId} 是 placeholder,里面的内容可以用 @PathVariable 取到
    @RequestMapping("/user/{userId}")
    @ResponseBody
    public String one(@PathVariable Integer userId) {
        return userId + "";
    }

    // 如果变量名和 {} 中的名字不一样,
    // 需要用 @PathVariable("nameInPath") 来指定变量要使用路径中的哪一个变量
    @RequestMapping("/category/{categoryName}/product/{productId}")
    @ResponseBody
    public String two(@PathVariable String categoryName,
                        @PathVariable("productId") Integer pId) {
        return "categoryName: " + categoryName + ", productId: " + pId;
    }
    
    // 还支持正则表达式的方式
    @RequestMapping("/regex/{text:[a-z]+}-{number:\\d+}")
    @ResponseBody
    public String three(@PathVariable String text, @PathVariable Integer number) {
        return "Text: " + text + ", Number: " + number;
    }
}
测试连接:

@RequestParam

取得 HTTP 请求中的参数。

package controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class ParameterController {
    // 取得名字为 id 的参数的值
    @RequestMapping("/user")
    @ResponseBody
    public String findUser(@RequestParam Integer id) {
        return "ID: " + id;
    }

    // required 默认是 true,参数是必要的,如果没有提供需要的参数,则报错
    // required 为 false 表示参数是可选的
    @RequestMapping("/product")
    @ResponseBody
    public String findProduct(@RequestParam(value="productId", required=true) Integer id,
                              @RequestParam(value="productName", required=false) String name) {
        return "ID: " + id + ", Name: " + name;
    }
}
测试连接:

@ModelAttribute

HTTP 请求的参数映射到对象,参数名和对象中的属性名匹配的就做映射,不匹配的就不管。

定义 controller.ParameterController
package controller;

import domain.User;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class ParameterController {
    // 表单中的参数被映射到对象 user
    @RequestMapping("/user")
    @ResponseBody
    public String findUser(@ModelAttribute User user,
                           @RequestParam(required=false) Integer age) {
        System.out.println("Age: " + age);
        return user.toString();
    }
}
定义 domain.User
package domain;

public class User {
    private int id;
    private String username;
    private String password;

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    @Override
    public String toString() {
        return "User{" +
                "id=" + id +
                ", username='" + username + '\'' +
                ", password='" + password + '\'' +
                '}';
    }
}
测试连接:

Forward and Redirect

Forwardforward:/URI Redirectredirect:/URI

package controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
public class ParameterController {
    @RequestMapping("/forward-test")
    public String forward() {
        return "forward:/helloworld-springmvc";
    }

    @RequestMapping("/redirect-test")
    public String redirect() {
        return "redirect:/helloworld-springmvc";
    }
}
测试连接:

RedirectAttributes

表单提交后一般都会 redirect 到另一个页面,防止表单重复提交。RedirectAttributes 的作用就是把处理 PageA 的结果存储起来,当 redirect 到 PageB 的时候显示 PageA 的结果。

Request 中的参数不能被传递给 redirect 的页面,因为 redirect 是从浏览器端发起一个新的请求。

Normally when we generate an http redirect request, the data stored in request is lost making it impossible for next GET request to access some of the useful information from request.

Flash attributes comes handy in such cases. Flash attributes provide a way for one request to store attributes intended for use in another. Flash attributes are saved temporarily (typically in the session) before the redirect to be made available to the request after the redirect and removed immediately.

防止表单重复提交的流程


view/ftl/user-form.htm
<!DOCTYPE html>
<html>
<head>
    <title>Update User</title>
</head>
<body>
    <form action="/update-user" method="post">
        Username: <input type="text" name="username"><br>
        Password: <input type="text" name="password"><br>
        <button type="submit">Update User</button>
    </form>
</body>
</html>

view/ftl/result.htm
Result: ${result}

controller/ParameterController.java
package controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;

@Controller
public class ParameterController {
    // 显示表单
    @RequestMapping("/user-form")
    public String showUserForm() {
        return "user-form.htm";
    }

    // 更新 User,把结果保存到 RedirectAttributes
    @RequestMapping("/update-user")
    public String updateUser(@RequestParam String username,
                             @RequestParam String password,
                             final RedirectAttributes redirectAttributes) {
        // Update user in database...
        System.out.println("Username: " + username + ", Password: " + password);

        // 操作结果显示给用户
        redirectAttributes.addFlashAttribute("result", "The user is already successfully updated");

        return "redirect:/result";
    }

    // 显示表单处理结果
    @RequestMapping("/result")
    public String result() {
        return "result.htm";
    }
}
测试连接:

访问 http://localhost/user-form,填写信息,提交表单,处理好后页面被 redirect 到 http://localhost/result 显示操作结果。


Redirect 到另一个 Controller 时获取 RedirectAttributes 里的属性使用 @ModelAttribute

    @RequestMapping("flash")
    public String flash(RedirectAttributes redirectAttributes) {
        redirectAttributes.addFlashAttribute("username", "Biao");
        return "redirect:flash2";
    }

    @RequestMapping("flash2")
    @ResponseBody
    public String flash2(@ModelAttribute("username") String username) {
        return "username: " + username;
    }

获取 Request and Response

想取得 HttpServletRequest 和 HttpServletResponse 很容易,只要在方法的参数里定义后,SpringMVC 会自动的注入 它们。

package controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@Controller
public class ParameterController {
    @RequestMapping("/request-response")
    @ResponseBody
    public String foo(HttpServletRequest request, HttpServletResponse response) {
        System.out.println(request);
        return "WoW";
    }
}
测试连接:

@RequestHeader: Read and write header

The @RequestHeader annotation allows a method parameter to be bound to a request header. Here is a sample request header:

Header 直接写到 HttpServletResponse

Header Value
Host localhost:8080
Accept text/html,application/xhtml+xml,application/xml;q=0.9
Accept-Language fr,en-gb;q=0.7,en;q=0.3
Accept-Encoding gzip,deflate
Accept-Charset ISO-8859-1,utf-8;q=0.7,*;q=0.7
Keep-Alive 300
package controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import javax.servlet.http.HttpServletResponse;

@Controller
public class ParameterController {
    @RequestMapping("/read-header")
    @ResponseBody
    public String readHeader(@RequestHeader("Host") String host,
                             @RequestHeader(value="Accept-Encoding", required=false) String encoding,
                             @RequestHeader(value="Accept-Charset", required=false) String charset) {
        return "Host: " + host + ", Encoding: " + encoding + ", Charset: " + charset;
    }

    @RequestMapping("/read-header-error")
    @ResponseBody
    public String readHeaderError(@RequestHeader("Host") String host,
                                  @RequestHeader("Accept-Charset") String charset) {
        return "Host: " + host + ", Charset: " + charset;
    }

    @RequestMapping("/write-header")
    @ResponseBody
    public String writeHeader(HttpServletResponse response) {
        response.setHeader("token", "D4BFCEC2-89E6-40CB-AF9A-B5513CB30FED");

        return "Header is wrote.";
    }
}
测试连接:

访问 http://localhost/read-header

访问 http://localhost/read-header-error 提示错误,因为 charset 是 required=true的,在 header 信息里没有 charset,所以报错。但是这个错误如果不了解的话会一头雾水,因为在控制台中没有输出错误的原因

这个错误是SpringMVC报出来的,见到它意味着html/jsp页面的控件名称和 controller 里函数的参数不符。 要看到错误原因,需要把 Spring 的日志调到 Debug 级别。

@CookieValue: Read and write Cookie

The @CookieValue annotation allows a method parameter to be bound to the value of an HTTP cookie.

Cookie 直接写到 HttpServletResponse

package controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.CookieValue;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletResponse;

@Controller
public class ParameterController {
    @RequestMapping("/read-cookie")
    @ResponseBody
    public String readCookie(@CookieValue("username") String cookie) {
        return "Cookie for username: " + cookie;
    }

    @RequestMapping("/write-cookie")
    @ResponseBody
    public String writeCookie(HttpServletResponse response) {
        Cookie cookie = new Cookie("username", "Don't tell you");
        cookie.setMaxAge(1000);
        response.addCookie(cookie); // Put cookie in response.

        return "Cookie is wrote.";
    }
}

测试:
  1. 访问 http://localhost/read-cookie 报错,因为还没有 cookie,
    可以给 cookie 一个默认值,这样即使没有 cookie 也不会报错了:
    @CookieValue(value="username", defaultValue="") String cookie
  2. 访问 http://localhost/write-cookie 写入 cookie
  3. 访问 http://localhost/read-cookie 输出 cookie

@SessionAttributes and HttpSession

可以使用 HttpSession 来直接操作 session,同时 SpringMVC 也提供了操作 session 的注解 @SessionAttributes

HttpSession 的方式什么时候都生效,但是 @SessionAttributes 有时候就不行,如返回 AJAX 请求时设置的 session 无效。所以推荐使用注入 HttpSession 来读写 session。

@SessionAttributes annotation indicates that in the controller’s methods can be assigned some values to arguments of the annotation. In this example I declared just one session attribute with the name “thought“. That’s mean I can put some object into modelAndView using addObject() method, and it will be added to the session if the name of the object will be the same as the name of argument in @SessionAttributes.

package controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.SessionAttributes;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpSession;

@Controller
@SessionAttributes({"result"})
public class SessionController {
    @RequestMapping("/write-session")
    @ResponseBody
    public String writeSession(HttpSession session) {
        session.setAttribute("username", "座山雕");
        session.setAttribute("password", "天王盖地虎");

        return "Session wrote...";
    }

    @RequestMapping("/read-session")
    public String readSession() {
        return "helloworld-freemarker.htm";
    }

    @RequestMapping("/write-session2")
    public ModelAndView writeSession2() {
        ModelAndView mav = new ModelAndView();
        mav.addObject("result", "Save session");
        mav.setViewName("user-form.htm");

        return mav;
    }

    @RequestMapping("/read-session2")
    public String readSession2() {
        return "result.htm";
    }
}
测试:
  1. 访问 http://localhost/write-session
  2. 访问 http://localhost/read-session

@Valid 参数验证

使用 JSR-303 Validation 进行验证,比 SpringMVC 自己提供的验证方式简单很多。

当我们在SpringMVC 中需要使用到 JSR-303 的时候就需要我们提供一个对 JSR-303 规范的实现,Hibernate Validator 是实现了这一规范的,这里我将以它作为 JSR-303 的实现来讲解 SpringMVC 对 JSR-303 的支持。

参考 http://haohaoxuexi.iteye.com/blog/1812584

Annocation Description
@Null 限制只能为null
@NotNull 限制必须不为null
@AssertFalse 限制必须为false
@AssertTrue 限制必须为true
@DecimalMax(value) 限制必须为一个不大于指定值的数字
@DecimalMin(value) 限制必须为一个不小于指定值的数字
@Digits(integer,fraction) 限制必须为一个小数,且整数部分的位数不能超过integer,小数部分的位数不能超过fraction
@Future 限制必须是一个将来的日期
@Max(value) 限制必须为一个不大于指定值的数字
@Min(value) 限制必须为一个不小于指定值的数字
@Past 限制必须是一个过去的日期
@Pattern(value) 限制必须符合指定的正则表达式
@Size(max,min) 限制字符长度必须在min到max之间
添加依赖
<!-- For Validate -->
<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-validator</artifactId>
    <version>5.1.3.Final</version>
</dependency>

controller.ValidateController
package controller;

import domain.User;
import org.springframework.stereotype.Controller;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import javax.validation.Valid;

@Controller
public class ValidateController {
    @RequestMapping("/validate-user")
    @ResponseBody
    public String validateUser(@ModelAttribute @Valid User user, BindingResult result) {
        System.out.println(result.hasErrors());

        if (result.hasErrors()) {
            return result.getFieldErrors().toString();
        }

        return "名字: " + user.getUsername() + ", Password: " + user.getPassword();
    }
}

domain.User
package domain;

import org.hibernate.validator.constraints.NotBlank;

import javax.validation.constraints.NotNull;

public class User {
    private int id;
    private String username;
    private String password;

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    @NotBlank(message="用户名不能为空") // 进行参数验证
    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    @NotNull(message="密码不能为null") // 进行参数验证
    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    @Override
    public String toString() {
        return "User{" +
                "id=" + id +
                ", username='" + username + '\'' +
                ", password='" + password + '\'' +
                '}';
    }
}

spring-mvc.xml
</mvc:annotation-driven>

测试连接:
  1. http://localhost/validate-user
  2. http://localhost/validate-user?username=
  3. http://localhost/validate-user?username=biao
  4. http://localhost/validate-user?username=biao&password=1234