Spring MVC 学习总结(二)——控制器定义与@RequestMapping详解
一、控制器定义
控制器提供访问应用程序的行为,通常通过服务接口定义或注解定义两种方法实现。 控制器解析用户的请求并将其转换为一个模型。在Spring MVC中一个控制器可以包含多个Action(动作、方法)。
1.1、实现接口Controller定义控制器
Controller是一个接口,处在包org.springframework.web.servlet.mvc下,接口中只有一个未实现的方法,具体的接口如下所示:
package org.springframework.web.servlet.mvc; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.springframework.web.servlet.ModelAndView; //实现该接口的类获得控制器功能与类型, 解析用户的请求并将其转换为一个模型 public interface Controller { //处理请求且返回一个模型与视图对象 ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception; }
在自定义控制器前先创建一个基于maven的web项目,添加包的依赖,pom.xml文件如下:
如果不配置scope,会把jar包发布,会跟容器里的jar包冲突、scope要设置为provided,由容器提供,不会发布(或者不配这两个依赖,在项目的Java BuildPath的Libraries里添加Server Runtime)目前scope可以使用5个值:
compile:缺省值,适用于所有阶段,会随着项目一起发布。
provided:类似compile,期望JDK、容器或使用者会提供这个依赖。如servlet.jar。
runtime:只在运行时使用,如JDBC驱动,适用运行和测试阶段。test,只在测试时使用,用于编译和运行测试代码。不会随项目发布。
system:类似provided,需要显式提供包含依赖的jar,Maven不会在Repository中查找它。
创建一个名为Foo的类,实现接口Controller,重写handleRequest方法,代码如下:
package com.zhangguo.springmvc02.controllers; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.springframework.web.servlet.ModelAndView; import org.springframework.web.servlet.mvc.Controller; /* * 定义控制器 */ public class FooController implements Controller { @Override public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception { //返回一个模型视图对象,指定路径,指定模型的名称为message,值为一段字符串 return new ModelAndView("foo/index", "message", "Hello,我是通过实现接口定义的一个控制器"); } }
在WEB-INF/views/foo目录下创建一个名为index.jsp的视图,内容如下:
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%> <!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <title>Foo</title> </head> <body> ${message} </body> </html>
修改springmvc-servlet.xml配置文件,增加一个控制器bean的声明,具体内容如下:
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:mvc="http://www.springframework.org/schema/mvc" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.3.xsd http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-4.3.xsd"> <!-- 自动扫描包,实现支持注解的IOC --> <context:component-scan base-package="com.zhangguo.springmvc02" /> <!-- Spring MVC不处理静态资源 --> <mvc:default-servlet-handler /> <!-- 支持mvc注解驱动 --> <mvc:annotation-driven /> <!-- 视图解析器 --> <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver" id="internalResourceViewResolver"> <!-- 前缀 --> <property name="prefix" value="/WEB-INF/views/" /> <!-- 后缀 --> <property name="suffix" value=".jsp" /> </bean> <bean name="/foo" class="com.zhangguo.springmvc02.controllers.FooController"></bean> </beans>
基中name是访问路径,class是自定义的控制器的全名称。运行后的结果如下:
小结:实现接口Controller定义控制器是较老的办法,缺点是:一个控制器中只有一个Action,如果要多个Action则需要定义多个Controller;定义的方式比较麻烦;Spring 2.5以后采用注解的方式定义解决这引起问题。
1.2、使用注解@Controller定义控制器
org.springframework.stereotype.Controller注解类型用于声明Spring类的实例是一个控制器(在讲IOC时还提到了另外3个注解);Spring可以使用扫描机制来找到应用程序中所有基于注解的控制器类,为了保证Spring能找到你的控制器,需要在配置文件中声明组件扫描。
创建一个名了Bar的类,定义为一个控制器,类的具体实现如下:
package com.zhangguo.springmvc02.controllers; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.RequestMapping; /** * 定义控制器 */ //BarController类的实例是一个控制器,会自动添加到Spring上下文中 @Controller public class BarController { //映射访问路径 @RequestMapping("/bar") public String index(Model model){ //Spring MVC会自动实例化一个Model对象用于向视图中传值 model.addAttribute("message", "这是通过注解定义的一个控制器中的Action"); //返回视图位置 return "foo/index"; } }
还要需要修改Spring mvc配置文件,启用自动组件扫描功能,在beans中增加如下配置:
<!-- 自动扫描包,实现支持注解的IOC --> <context:component-scan base-package="com.zhangguo.springmvc02" />
base-package属性用于指定扫描的基础包,可以缩小扫描的范围。运行结果如下:
小结:从代码与运行结果可以看出BarController与FooController同时都指定了一个视图foo/index.jsp,但是页面结果的结果是不一样的,从这里可以看出视图是被复用的,而控制器与视图之间是弱偶合关系。
二、@RequestMapping详解
@RequestMapping注释用于映射url到控制器类或一个特定的处理程序方法。可用于类或方法上。用于类上,表示类中的所有响应请求的方法都是以该地址作为父路径。该注解共有8个属性,注解源码如下:
package org.springframework.web.bind.annotation; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.util.concurrent.Callable; import org.springframework.core.annotation.AliasFor; /** * 用于映射url到控制器类或一个特定的处理程序方法. */ //该注解只能用于方法或类型上 @Target({ ElementType.METHOD, ElementType.TYPE }) @Retention(RetentionPolicy.RUNTIME) @Documented @Mapping public @interface RequestMapping { /** * 指定映射的名称 */ String name() default ""; /** * 指定请求的路径映射,指定的地址可以是uri模板,别名为path */ @AliasFor("path") String[] value() default {}; /** 别名为value,使用path更加形象 * 只有用在一个Servlet环境:路径映射URI(例如“/myPath.do”)。 * Ant风格的路径模式,同时也支持(例如,“/myPath/*.do”)。在方法层面,在主要的映射在类型级别表示相对路径(例如,“edit.do”) * 的支持。路径映射的URI可能包含占位符(例如“/$ {}连接”) */ @AliasFor("value") String[] path() default {}; /** * 指定请求谓词的类型如GET, POST, HEAD, OPTIONS, PUT, PATCH, DELETE, TRACE. 收窄请求范围 The * HTTP request methods to map to, narrowing the primary mapping: GET, POST, * HEAD, OPTIONS, PUT, PATCH, DELETE, TRACE. */ RequestMethod[] method() default {}; /** * 映射请求的参数,收窄请求范围 The parameters of the mapped request, narrowing the * primary mapping. */ String[]params() default {}; /** * 映射请求头部,收窄请求范围 The headers of the mapped request, narrowing the primary * mapping. RequestMapping(value = "/something", headers = * "content-type=text/*") */ String[] headers() default {}; /** * 指定处理请求的提交内容类型(Content-Type),例如application/json, text/html,收窄请求范围 The * consumable media types of the mapped request, narrowing the primary * mapping. */ String[] consumes() default {}; /** * 指定返回的内容类型,仅当request请求头中的(Accept)类型中包含该指定类型才返回 The producible media types * of the mapped request, narrowing the primary mapping. produces = * "text/plain" produces = {"text/plain", "application/*"} produces = * "application/json; charset=UTF-8" */ String[] produces() default {}; }
从上面的源码可以发现除了name基本都是数组类型,在设置时我们可以指定单个值,如@RequestMapping(value="/foo");也可以同时指定多个值如:@RequestMapping(value={"/foo","/bar"})。
2.1、value 属性指定映射路径或URL模板
指定请求的实际地址,指定的地址可以是URL模板,正则表达式或路径占位,该属性与path互为别名关系,@RequestMapping("/foo")} 与 @RequestMapping(path="/foo")相同。该属性是使用最频繁,最重要的一个属性,如果只指定该属性时可以把value略去。Spring Framework 4.2引入了一流的支持声明和查找注释属性的别名。@AliasFor注解可用于声明一双别名属性,来给注解的属性起别名, 让使用注解时, 更加的容易理解(比如给value属性起别名, 更容易让人理解)。先看一个官网的示例:
@Controller @RequestMapping("/appointments") public class AppointmentsController { private final AppointmentBook appointmentBook; @Autowired public AppointmentsController(AppointmentBook appointmentBook) { this.appointmentBook = appointmentBook; } @RequestMapping(method = RequestMethod.GET) public Map<String, Appointment> get() { return appointmentBook.getAppointmentsForToday(); } @RequestMapping(value = "/{day}", method = RequestMethod.GET) public Map<String, Appointment> getForDay(@PathVariable @DateTimeFormat(iso = ISO.DATE) Date day, Model model) { return appointmentBook.getAppointmentsForDay(day); } @RequestMapping(value = "/new", method = RequestMethod.GET) public AppointmentForm getNewForm() { return new AppointmentForm(); } @RequestMapping(method = RequestMethod.POST) public String add(@Valid AppointmentForm appointment, BindingResult result) { if (result.hasErrors()) { return "appointments/new"; } appointmentBook.addAppointment(appointment); return "redirect:/appointments"; } }
2.1.1、指定具体路径字符
2.1.1.1 只注解方法
@Controller public class FooBarController { @RequestMapping("/action1") public String action1(){ return "foo/index"; } }
访问路径:http://localhost:8087/SpringMVC02/action1
2.1.1.2 同时注解类与方法
@Controller @RequestMapping("/foobar") public class FooBarController { @RequestMapping("/action1") public String action1(){ return "foo/index"; } }
访问路径:http://localhost:8087/SpringMVC02/foobar/action1
需要先指定类的路径再指定方法的路径
2.1.1.3 当value为空值
注解在方法上时,如果value为空则表示该方法为类下默认的Action。
@Controller @RequestMapping("/foobar") public class FooBarController { @RequestMapping("/action1") public String action1(Model model){ //在模型中添加属性message值为action1,渲染页面时使用 model.addAttribute("message", "action1"); return "foo/index"; } @RequestMapping public String action2(Model model){ //在模型中添加属性message值为action2,渲染页面时使用 model.addAttribute("message", "action2"); return "foo/index"; } }
访问action2的路径是:http://localhost:8087/SpringMVC02/foobar,如果加上action2就错误了。
注解在类上时,当value为空值则为默认的控制器,可以用于设置项目的起始页。
@Controller @RequestMapping public class FooBarController { @RequestMapping("/action1") public String action1(Model model){ //在模型中添加属性message值为action1,渲染页面时使用 model.addAttribute("message", "action1"); return "foo/index"; } @RequestMapping public String action2(Model model){ //在模型中添加属性message值为action2,渲染页面时使用 model.addAttribute("message", "action2"); return "foo/index"; } }
访问路径:http://localhost:8087/SpringMVC02/,同时省去了控制器名与Action名称,可用于欢迎页。
访问action1的路径是:http://localhost:8087/SpringMVC02/action1
2.1.2、路径变量占位,URI模板模式
在Spring MVC可以使用@PathVariable 注释方法参数的值绑定到一个URI模板变量。
@RequestMapping("/action3/{p1}/{p2}") public String action3(@PathVariable int p1,@PathVariable int p2,Model model){ model.addAttribute("message", p1+p2); return "foo/index"; }
运行结果:
使用路径变量的好处:使路径变得更加简洁;获得参数更加方便,框架会自动进行类型转换。通过路径变量的类型可以约束访问参数,如果类型不一样,则访问不到action,如这里访问是的路径是/action3/1/a,则路径与方法不匹配,而不会是参数转换失败。
2.1.3、正则表达式模式的URI模板
@RequestMapping(value="/action4/{id:\d{6}}-{name:[a-z]{3}}") public String action4(@PathVariable int id,@PathVariable String name,Model model){ model.addAttribute("message", "id:"+id+" name:"+name); return "foo/index"; }
正则要求id必须为6位的数字,而name必须为3位小写字母,访问结果如下:
2.1.4、矩阵变量@MatrixVariable
矩阵变量可以出现在任何路径段,每个矩阵变量用“;”分隔。例如:“/汽车;颜色=红;年=2012”。多个值可以是“,”分隔“颜色=红、绿、蓝”或变量名称可以重复“颜色=红;颜色=绿色;颜色=蓝”,如下所示:
// GET /pets/42;q=11;r=22 @RequestMapping(value = "/pets/{petId}") public void findPet(@PathVariable String petId, @MatrixVariable int q) { // petId == 42 // q == 11 }
// 矩阵变量 @RequestMapping(value = "/action5/{name}") public String action5(Model model, @PathVariable String name, //路径变量,用于获得路径中的变量name的值 @MatrixVariable String r, @MatrixVariable(required = true) String g, //参数g是必须的 @MatrixVariable(defaultValue = "99", required = false) String b) { //参数b不是必须的,默认值是99 model.addAttribute("message", name + " is #" + r + g + b); return "foo/index"; }
//Get http://localhost:8087/SpringMVC02/action5/the%20book%20color;r=33;g=66
//the book color is #336699
默认是不允许使用矩阵变量的,需要设置配置文中的RequestMappingHandlerMapping的属性removeSemicolonContent为false;在annotation-driven中增加属性enable-matrix-variables="true",修改后的springmvc-servlet.xml文件如下:
<!-- 支持mvc注解驱动 --> <mvc:annotation-driven enable-matrix-variables="true" /> <!-- 配置映射媒体类型的策略 --> <bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping"> <property name="removeSemicolonContent" value="false" /> </bean>
访问结果如下:
2.1.5、Ant风格路径模式
@RequestMapping注解也支持ant风格的路径模式,如/myPath/*.do,/owners/*/pets/{petId},示例代码如下:
//Ant风格路径模式 @RequestMapping(value = "/action6/*.do") public String action6(Model model){ model.addAttribute("message","Ant风格路径模式"); return "foo/index"; }
运行结果:
当然还有关于路径匹配的规则,特殊的优先级高过一般的,更多规则可以参考官方帮助。
2.2、method属性指定谓词类型
用于约束请求的谓词类型,可以收窄请求范围。指定请求谓词的类型如GET, POST, HEAD, OPTIONS, PUT, PATCH, DELETE, TRACE,如下代码所示:
//谓词类型 @RequestMapping(value = "/action6",method={RequestMethod.POST,RequestMethod.DELETE}) public String action6(Model model) { model.addAttribute("message", "请求谓词只能是POST与DELETE"); return "foo/index"; }
要访问action7请求谓词类型必须是POST或者为DELETE,当我们从浏览器的URL栏中直接请求时为一个GET请求,则结果是405,如下所示:
如果将POST修改为GET则正常了,如下所示:
//谓词类型 @RequestMapping(value = "/action6",method=RequestMethod.GET) public String action6(Model model) { model.addAttribute("message", "请求谓词只能是GET"); return "foo/index"; }
2.3、consumes属性指定请求的Content-Type
指定处理请求的提交内容类型(Content-Type),例如application/json, text/html,收窄请求范围,如果用户发送的请求内容类型不匹配则方法不会响应请求,具体使用如下代码所示:
package com.zhangguo.springmvc02.controllers; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.RequestMapping; @Controller @RequestMapping("/home") public class HomeController { // 请求内容类型必须为text/html,注意浏览器默认没有指定Content-type @RequestMapping(value = "/action8",consumes="text/html") public String action8(Model model) { model.addAttribute("message", "请求的提交内容类型(Content-Type)是text/html"); return "foo/index"; } }
在action8的注解中约束发送到服务器的Content-Type必须是text/html类型,如果类型不一致则会报错(415),测试结果如下:
从两个图的对比可以看出当内容类型为text/plain时报客户端错误415,当内容类型为text/html时则响应正常,响应的结果如下:
请求的提交内容类型(Content-Type)是text/html
注意:可以使用!号,如consumes="!text/html"
2.4、produces属性指定响应的Content-Type
指定返回的内容类型,仅当request请求头中的(Accept)类型中包含该指定类型才返回,方法才处理客户端的请求否则会报406错误,常用设置如下:
produces = "text/plain" //客户端只接收纯文本
produces = {"text/plain", "application/*"} //客户端接收纯文本与application/*类型的内容
produces = "application/json; charset=UTF-8" //客户端接收json且编码为utf-8
//客户端接收json且编码为utf-8,多数浏览器Accept设置的为*/*,接收任意类型 @RequestMapping(value = "/action9",produces="application/json; charset=UTF-8") public String action9(Model model) { model.addAttribute("message", "客户端可以接收的类型是application/json; charset=UTF-8"); return "foo/index"; }
运行结果:
注意:可以使用!号,如produces="!text/html"
2.5、params属性指定请求中必须有特定参数与值
映射请求的参数,收窄请求范围。可以限制客户端发送到服务器的请求参数为某些特定值或不为某些值,如下代码所示:
//请求的参数必须包含id=215与name不等于abc @RequestMapping(value = "/action10",params={"id=215","name!=abc"}) public String action10(Model model) { model.addAttribute("message", "请求的参数必须包含id=215与name不等于abc"); return "foo/index"; }
运行结果如下:
name的值如没有指定也是通过的;可以使用不等于;
2.6、headers属性指定请求中必须有特定header值
映射请求头部,收窄请求范围。约束客户端发送的请求头部信息中必须包含某个特定的值或不包含某个值,作用范围明显大于前面讲过的几种,示例代码如下:
//请求头部信息中必须包含Host=localhost:8088 @RequestMapping(value = "/action11",headers="Host=localhost:8088") public String action11(Model model) { model.addAttribute("message", "请求头部信息中必须包含Host=localhost:8088"); return "foo/index"; }
运行结果:
修改Host为8087时运行就正常了:
这里同样可以使用!号;可以使用通配符如:Content-Type="application/*"
2.7、name属性指定名称
为当前映射指定一个名称,不常用,一般不会指定。
2.8、path属性指定路径
先看源码中的path与value,定义如下:
@AliasFor("path") String[] value() default {}; @AliasFor("value") String[] path() default {};
从Spring 4.2开始引入了@AliasFor注解,可以实现属性的别名,如value本身并没有特定的含义,而path会更加具体,能见名知义,通俗说可以认为两者在使用中是一样的如:@RequestMapping("/foo")} 与 @RequestMapping(path="/foo")相同。示例代码如下:
//映射访问路径为/action12或/myaction,指定映射名称为actionTest @RequestMapping(path ={"/action12","/myaction"},name="actionTest") public String action12(Model model) { model.addAttribute("message", "映射访问路径为/action12或/myaction,指定映射名称为actionTest"); return "foo/index"; }
运行结果: