SpringMVC @RequestMapping

参考

  • 官方文档

关于DispatcherServlet的url-pattern问题

待解决问题:
如果设定为/, 即代替容器的default Servelt. 解析规则会变得很奇怪, 深层原因有待考究.
先贴上Tomcat的默认的default Servlet配置:
apache-tomcat-8.0.32/conf/web.xml


    <servlet>
        <servlet-name>default</servlet-name>
        <servlet-class>org.apache.catalina.servlets.DefaultServlet</servlet-class>
        <init-param>
            <param-name>debug</param-name>
            <param-value>0</param-value>
        </init-param>
        <init-param>
            <param-name>listings</param-name>
            <param-value>false</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>

    <!-- The mapping for the default servlet -->
    <servlet-mapping>
        <servlet-name>default</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>

@RequestMapping

可以注解在class上, 也可以注解在method上.
如果不指定@RequestMapping(method = HTTP.METHOD), 那么默认是支持所有方法.

示例:

@Controller
@RequestMapping("/echo")
public class EchoController {

    @RequestMapping(path = "/{name}", method = RequestMethod.GET)
    @ResponseBody
    public String get(@PathVariable String name){
        return "get method: " + name;
    }

    @RequestMapping(method = RequestMethod.POST)
    @ResponseBody
    public String post(){
        return "post method";
    }
}

method字段:

/**
* The HTTP request methods to map to, narrowing the primary mapping:
* GET, POST, HEAD, OPTIONS, PUT, PATCH, DELETE, TRACE.
* <p><b>Supported at the type level as well as at the method level!</b>
* When used at the type level, all method-level mappings inherit
* this HTTP method restriction (i.e. the type-level restriction
* gets checked before the handler method is even resolved).
* <p>Supported for Servlet environments as well as Portlet 2.0 environments.
*/

查看RequestMapping的源码, 可以发现pathvalueServlet环境下都是对方的别名, 所以是等价的.

/**
     * The primary mapping expressed by this annotation.
     * <p>In a Servlet environment this is an alias for {@link #path}.
     * For example {@code @RequestMapping("/foo")} is equivalent to
     * {@code @RequestMapping(path="/foo")}.
     * <p>In a Portlet environment this is the mapped portlet modes
     * (i.e. "EDIT", "VIEW", "HELP" or any custom modes).
     * <p><b>Supported at the type level as well as at the method level!</b>
     * When used at the type level, all method-level mappings inherit
     * this primary mapping, narrowing it for a specific handler method.
     */
    @AliasFor("path")
    String[] value() default {};

    /**
     * In a Servlet environment only: the path mapping URIs (e.g. "/myPath.do").
     * Ant-style path patterns are also supported (e.g. "/myPath/*.do").
     * At the method level, relative paths (e.g. "edit.do") are supported within
     * the primary mapping expressed at the type level. Path mapping URIs may
     * contain placeholders (e.g. "/${connect}")
     * <p><b>Supported at the type level as well as at the method level!</b>
     * When used at the type level, all method-level mappings inherit
     * this primary mapping, narrowing it for a specific handler method.
     * @see org.springframework.web.bind.annotation.ValueConstants#DEFAULT_NONE
     * @since 4.2
     */
    @AliasFor("value")
    String[] path() default {};

@PathVariable

    @RequestMapping(path = "/{name}", method = RequestMethod.GET)
    @ResponseBody
    public String get(@PathVariable String name){
        return "get method: " + name;
    }

可以绑定一个URI template variable到方法中的某个参数中.其中URI里面的变量用{}表示.
@PathVariable绑定变量的两种方式:

  1. 如果被注解的参数名和{name}一致, 那么@PathVariable可以不用指定URI中的变量名
  2. 如果不同可以使用@PathVariable("name")指定
    例如:
@RequestMapping(path="/owners/{ownerId}", method=RequestMethod.GET)
public String findOwner(@PathVariable("ownerId") String theOwner, Model model) {
// implementation omitted
}

方法中可以有多个@PathVariable注解,

@RequestMapping(path="/owners/{ownerId}/pets/{petId}", method=RequestMethod.GET)
public String findPet(@PathVariable String ownerId, @PathVariable String petId, Model model) {
  Owner owner = ownerService.findOwner(ownerId);
  Pet pet = owner.getPet(petId);
  model.addAttribute("pet", pet);
  return "displayPet";
}

当注解到Map<String, String>类型的参数上时: map会被填充全部变量.

@PathVariable可以从typepath level的@RequestMapping注解中取得URI template:

@Controller
@RequestMapping("/owners/{ownerId}")
public class RelativePathUriTemplateController {
    @RequestMapping("/pets/{petId}")
    public void findPet(@PathVariable String ownerId, @PathVariable String petId, Model model) {
    // implementation omitted
    }
}

@PathVariable参数可以是任何简单类型, 比如int, long, Date等, Spring负责转换. 如果无法转换则会报错, 如果需要更复杂的转换规则, 可以参考“Method Parameters And Type Conversion” 以及 “Customizing WebDataBinder initialization”.

URI template对正则表达式的支持.
比如说要匹配/spring-web/spring-web-3.0.5.jar. 那么可以使用下面这种规则书写正则表达式:
{变量名: 正则表达式}

@RequestMapping("/spring-web/{symbolicName:[a-z-]+}-{version:\\d\\.\\d\\.\\d}{extension:\\.[a-z]+}")
public void handle(@PathVariable String version, @PathVariable String extension) {
  // ...
}

Path Patterns

Ant-style path patterns:

  • /myPath/*.do
  • /owners/*/pets/{petId}

当请求的URL匹配多个模式的时候的路径选择:

  • A pattern with a lower count of URI variables and wild cards is considered more specific.

    /hotels/{hotel}/* has 1 URI variable and 1 wild card and is considered more specific than /
    hotels/{hotel}/** which as 1 URI variable and 2 wild cards.

  • If two patterns have the same count, the one that is longer is considered more specific.
    For example /
    foo/bar* is longer and considered more specific than /foo/*.

  • When two patterns have the same count and length, the pattern with fewer wild cards is considered more specific.
    For example /hotels/{hotel} is more specific than /hotels/*.

There are also some additional special rules:

  • The default mapping pattern /** is less specific than any other pattern.
    For example /api/{a}/{b}/{c} is more specific.
  • A prefix pattern such as /public/** is less specific than any other pattern that doesn’t contain double wildcards.
    For example /public/path3/{a}/{b}/{c} is more specific.

Path Patterns with Placeholders

Patterns in @RequestMapping annotations support ${...} placeholders against local properties and/or system properties and environment variables. This may be useful in cases where the path a controller is mapped to may need to be customized through configuration. For more information on placeholders, see the javadocs of the PropertyPlaceholderConfigurer class.

@RequestParam

从Query参数中获取各个参数.
@RequestParam("") 默认表示这个参数是required的.可以通过设置这个字段来设定是否是必须的: @RequestParam(name="id", required=false).

注意: 如果设定某个参数required=false, 那么参数类型请使用引用类型, 因为如果请求中没有包含这个参数, 会传递null过去, 原始类型不能被赋值为null会报错.

@RequestParam默认参数的设置方法:

// 因为defaultValue只能是String, 直接传false是不行的, 只能弄成字符串
// 如果是数字类型也是一样, 需要用引号引起来
@RequestParam(value = "echo", required = false, defaultValue = "false") Boolean echo

简单例子

    // 注意echo的类型用了Boolean
    // 因为如果RequestParam不是required的话, 那么query中没这个参数的时候
    // 会传递null
    @RequestMapping(path = "/query")
    @ResponseBody
    public String echoQuery(@RequestParam("purpose") String purpose, @RequestParam(value = "echo", required = false) Boolean echo){
        String message;
        if (echo != null && echo)
            message = purpose;
        else
            message = "set to not echo.";
        return message;
    }

测试:

curl "http://localhost:8080/app/echo/query?purpose=test&echo=false" -i
HTTP/1.1 200 OK
Server: Apache-Coyote/1.1
Content-Type: text/plain;charset=ISO-8859-1
Content-Length: 16
Date: Sun, 12 Nov 2017 11:35:21 GMT

set to not echo.
-----------------
smallfly@Ubuntu:~$ curl "http://localhost:8080/app/echo/query?purpose=test&echo=true" -i
HTTP/1.1 200 OK
Server: Apache-Coyote/1.1
Content-Type: text/plain;charset=ISO-8859-1
Content-Length: 4
Date: Sun, 12 Nov 2017 11:36:04 GMT

test
-----------------
curl "http://localhost:8080/app/echo/query" -iHTTP/1.1 400 Bad Request
Server: Apache-Coyote/1.1
Content-Type: text/html;charset=utf-8
Content-Language: en
Content-Length: 1111
Date: Sun, 12 Nov 2017 11:36:34 GMT
Connection: close

数组参数的例子

    @RequestMapping(path = "/array")
    @ResponseBody
    public String echoArray(@RequestParam("values") String[] values){
        return Arrays.toString(values);
    }

测试:

curl "http://localhost:8080/app/echo/array?values=tomorrow&values=is&values=Monday" -i
HTTP/1.1 200 OK
Server: Apache-Coyote/1.1
Content-Type: text/plain;charset=ISO-8859-1
Content-Length: 22
Date: Sun, 12 Nov 2017 11:42:48 GMT

[tomorrow, is, Monday]

@RequestParam 用在Map参数上

When an @RequestParam annotation is used on a Map<String, String> or MultiValueMap<String, String> argument, the map is populated with all request parameters.

Java代码:

    @RequestMapping(path = "/map", method = RequestMethod.GET)
    @ResponseBody
    public String echoMap(@RequestParam Map<String, String> map){
        Set<Map.Entry<String, String>> set =  map.entrySet();
        Iterator<Map.Entry<String, String>> iterator = set.iterator();
        StringBuilder stringBuilder = new StringBuilder();
        while (iterator.hasNext()){
            Map.Entry<String, String> entry = iterator.next();
            stringBuilder.append(String.format("%s: %s\n", entry.getKey(), entry.getValue()));
        }
        return stringBuilder.toString();
    }

测试:

curl "http://localhost:8080/app/echo/map?name=xiaofu&age=22" -i
HTTP/1.1 200 OK
Server: Apache-Coyote/1.1
Content-Type: text/plain;charset=ISO-8859-1
Content-Length: 21
Date: Sun, 12 Nov 2017 11:54:20 GMT

name: xiaofu
age: 22

@RequestParam 用在MultiValueMap参数上

测试:
Java:

    @RequestMapping(path = "/multiValue", method = RequestMethod.GET)
    @ResponseBody
    public String echoMap(@RequestParam MultiValueMap<String, String> map){
        Set<Map.Entry<String, List<String>>> set =  map.entrySet();
        Iterator<Map.Entry<String, List<String>>> iterator = set.iterator();
        StringBuilder stringBuilder = new StringBuilder();
        while (iterator.hasNext()){
            Map.Entry<String, List<String>> entry = iterator.next();
            stringBuilder.append(String.format("%s: %s\n", entry.getKey(), entry.getValue()));
        }
        return stringBuilder.toString();
    }

注意URL匹配是大小写有关的

curl "http://localhost:8080/app/echo/multiValue?name=xiaofu&age=22" -i
HTTP/1.1 200 OK
Server: Apache-Coyote/1.1
Content-Type: text/plain;charset=ISO-8859-1
Content-Length: 25
Date: Sun, 12 Nov 2017 11:57:15 GMT

name: [xiaofu]
age: [22]

处理form参数

结合javax.validationhibernate-validator实现bean的验证.
首先在pom.xml中添加依赖: 注意两个依赖的版本是互相有关的, 否则可能会出现问题.
依赖:

        <!--需要注意javax.validation和Hibernate Validator的版本匹配问题-->
        <!--https://stackoverflow.com/questions/34966160/hibernate-validations-->

        <!-- https://mvnrepository.com/artifact/javax.validation/validation-api -->
        <dependency>
            <groupId>javax.validation</groupId>
            <artifactId>validation-api</artifactId>
            <version>2.0.0.Final</version>
        </dependency>

        <!-- Hibernate Validator -->
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-validator</artifactId>
            <version>6.0.4.Final</version>
        </dependency>

定义Bean:

package me.xiaofud.spring101.spittr.domainobject;


import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;

/**
 * Created by xiaofu on 17-11-12.
 */
public class Post {

    // 设置max为10, 方便调试
    @NotNull
    @NotEmpty
    @Size(min = 1, max = 10)
    private String title;

    @NotNull
    @NotEmpty
    @Size(min = 1, max = 140)
    private String content;


    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public String getContent() {
        return content;
    }

    public void setContent(String content) {
        this.content = content;
    }

}

Controller:
addPost()方法的参数都会被注入. 其中Errors对象可以得知验证bean过程中是否出现了错误, 以及具体的错误原因.

@Controller
public class PostController {

    @RequestMapping(value = "/post/add", method = RequestMethod.POST)
    @ResponseBody
    public String addPost(@Valid Post post, Errors errors){
        if (errors.hasErrors()){
            return errors.getAllErrors().toString();
        }
        return "saved:" + post.getTitle();
    }

}

测试:

curl -i -X POST "http://localhost:8080/app/post/add" --data "title=123456789" --data "content=Hello!"
HTTP/1.1 200 OK
Server: Apache-Coyote/1.1
Content-Type: text/plain;charset=ISO-8859-1
Content-Length: 15
Date: Sun, 12 Nov 2017 13:42:42 GMT

saved:123456789

# 不合法参数
curl -i -X POST "http://localhost:8080/app/post/add" --data "title=12345678901" --data "content=Hello"
HTTP/1.1 200 OK
Server: Apache-Coyote/1.1
Content-Type: text/plain;charset=ISO-8859-1
Content-Length: 339
Date: Sun, 12 Nov 2017 13:45:39 GMT

[Field error in object 'post' on field 'title': rejected value [12345678901]; codes [Size.post.title,Size.title,Size.java.lang.String,Size]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [post.title,title]; arguments []; default message [title],10,1]; default message [size must be between 1 and 10]]

推荐阅读更多精彩内容