SpringMVC 学习(二)控制器

控制器的主要作用就是处理特定URL发过来的HTTP请求,然后进行业务逻辑处理,将结果返回给某个特定的视图。

处理请求

在Spring中定义控制器非常简单,新建一个类然后应用@Controller注解即可,当然一般习惯上将控制器类也命名为XXController。

每个控制器可以有若干方法,分别处理不同的请求。要指定处理请求的URL,使用@RequestMapping注解。控制器的方法处理之后,返回一个字符串,指定要使用的视图名称,然后该名称交给视图解析器转换成真正的视图,然后返回给客户端。

@RequestMapping还可以注解到控制器类上,这样一来每个方法处理的URL就是控制器和方法上URL的组合。

@Controller
@RequestMapping("/mvc")
public class MainController {

    @RequestMapping("/hello")
    public String hello(Model model) {
        model.addAttribute("name", "name");
        return "hello";
    }

    @RequestMapping("/index")
    public String index() {
        return "index";
    }

}

默认情况下@RequestMapping会处理所有请求,如果希望只处理GET或者POST等请求,可以使用@RequestMapping的method属性。

@RequestMapping(value = "/index", method = {RequestMethod.GET})
public String index() {
    return "index";
}

当然也可以直接使用Spring定义的几个Mapping注解,包括了GET、POST、DELETE、PUT等。需要注意这几个注解只能应用于方法上。

路径参数

有些网站的URL很特别,类似<code>http://mysite.com/list/yitian</code>这样的。Spring也支持这样的路径参数。

这时候路径模式中相应部分需要用花括号括起来,然后在方法中使用@PathVariable注解(注解中的名称需要和花括号中的参数相同)。
这样对应的路径参数就会由Spring自动赋给方法中的参数,我们直接在方法中使用即可。

@RequestMapping("/hello/{name}")
public String hey(@PathVariable("name") String username, Model model) {
    model.addAttribute("name", username);
    return "hello";
}

如果方法参数和路径中花括号部分相同,那么@PathVariable中的名称可以省略。

@RequestMapping("/hello/{name}")
public String hey(@PathVariable String name, Model model) {
    model.addAttribute("name", name);
    return "hello";
}

另外,方法中可以有多个路径参数。而且路径参数并不一定只能是字符串,也可以是<code>int</code>、<code>long</code>、<code>Date</code>这样的简单类型,Spring会自动进行转换,如果转换失败,就会抛出<code>TypeMismatchException</code>。

正则表达式匹配

有时候可能需要匹配一个比较复杂的路径,这时候可以使用正则表达式,语法是{varName:regex}。

例如为了匹配"/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) {
    // ...
}

另外路径模式中还支持通配符,例如<code>/myPath/*.do</code>。

最后一个问题就是这些路径优先级的问题。如果一个请求匹配了多个路径模式,那么最具体的那个会被使用。规则如下:

  • 路径中路径参数和通配符越少,路径越具体。
  • 路径参数和通配符个数相同的话,路径越长越具体。
  • 个数和长度都相同的话,通配符个数越少路径越具体。
  • 默认匹配/**优先级最低。
  • 前缀模式例如<code>/public/**</code>比其他两个通配符的模式优先级更低。
矩阵变量(Matrix Variables)

可以在路径中添加键值对,这样的键值对叫做矩阵变量

Spring默认没有启用矩阵变量。要启用它,在dispatcher-servlet.xml中添加或修改如下一行:

<mvc:annotation-driven enable-matrix-variables="true"/>

矩阵变量可以用在路径的任何部分,需要和路径之间使用分号<code>;</code>分隔开,每个矩阵变量之间也是用分号分隔。
如果一个矩阵变量有多个值,使用逗号<code>,</code>分隔,
例如<code>"/matrix/42;colors=red,blue,yellow;year=2012"</code>。

对应的控制器方法如下:

// 处理请求 /matrix/42;colors=red,blue,yellow;year=2012
@RequestMapping("/matrix/{count}")
public String matrix(@PathVariable int count, @MatrixVariable String[] colors, @MatrixVariable int year, Model model) {
    model.addAttribute("colors", colors);
    model.addAttribute("year", year);
    return "matrix";
}

还可以将所有矩阵变量映射成一个Map。

// 处理请求 /matrix2/42;colors=red,blue,yellow;year=2012
@RequestMapping("/matrix2/{count}")
public String matrix2(@PathVariable int count, @MatrixVariable MultiValueMap<String, String> map, Model model) {
    model.addAttribute("map", map);
    return "matrix";
}

对应的视图文件是<code>matrix.jsp</code>。

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>矩阵变量</title>
    <meta charset="utf-8"/>
</head>
<body>
<p>颜色是:
    <c:forEach var="color" items="${colors}">
        ${color},
    </c:forEach>
</p>

<p>年份是:${year}</p>

<p>矩阵变量:${map}</p>
</body>
</html>
媒体类型

通过使用@RequestMapping的consumes属性,还可以指定某个处理方法只处理某个或某些媒体类型的请求。

下面的请求方法只处理<code>Content-Type</code>是<code>application/json</code>的请求。

@RequestMapping(path = "/pets", consumes = "application/json")
public void addPet(@RequestBody Pet pet, Model model) {
    // implementation omitted
}

另外comsumes部分还可以写为非的形式,表示匹配不是某种类型的请求。

例如<code>comsumes="!text/html"</code>表示处理Content-Type不是text/html的请求。

除了直接指定字符串,还可以指定org.springframework.http.MediaType提供的一组常量。

另外@RequestMapping还有一个produces属性,指定匹配Accept是某种类型的请求,并且使用指定的类型来编码返回的响应。

@GetMapping(path = "/pets/{petId}", produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
@ResponseBody
public Pet getPet(@PathVariable String petId, Model model) {
    // implementation omitted
}

定义处理方法

前面我们通过@RequestMapping和另外几个注解,来匹配特定的请求。下面来学习一下如何定义处理方法。

方法参数

处理方法的参数并不是任意的,Spring处理方法支持的参数列表很长,可以参考Spring文档

这些参数可以分为几类:

  • 一是Servlet相关的类,例如HttpServletRequest、HttpServletResponse、HttpSession等;
  • 二是应用程序相关的,例如Timezone、Locale等;
  • 三是Spring提供的各类注解;
  • 四是输入输出流,用于直接操作HTTP请求和响应。
返回类型

处理方法的返回类型也不是任意的。详细的返回类型参见Spring官方文档

常用的一些返回类型如下:

  • String,表示要返回视图的名称。
  • View,会由RequestToViewNameTranslator翻译实际视图的名称。
  • void,表示方法会自己生成响应,不需要视图支持。
  • Callable<?>,表示异步请求的返回。

绑定请求参数

我们还记得直接使用Servlet API中getParameter方法的恐惧吧,对于每个Servlet我们都要调用多次getParameter方法获取参数,而且获取到的是字符串,我们需要手动转换类型。

在Spring中就非常简单了,我们可以将请求参数绑定到方法参数上,使用@RequestParam即可。

该注解有三个属性:

  • name表示请求参数的名称;
  • defaultValue表示请求参数的默认值;
  • required表示请求参数是否是必需的。

如果请求参数的名称和方法参数相同,那么name还可以省略。

// GET /hello?name=yitian
@RequestMapping("/hello")
public String hello(@RequestParam(defaultValue = "苟") String name, Model model) {
    model.addAttribute("name", name);
    return "hello";
}

向视图传递数据

如果处理方法的拥有一个org.springframework.ui.Model类型参数,那么我们就可以调用该参数的addAttribute方法添加属性,然后在视图中就可以访问这些属性了。

绑定请求体和响应体

绑定请求体使用@RequestBody注解。

下面的例子将请求体直接返回给响应。这里的处理方法用到了Writer参数直接输出HTTP响应,不需要视图,因此这里返回空。为了运行这个例子,需要一个表单,发送到该控制器上,然后我们就可以看到表单对应的请求体了。

@PostMapping("/requestBody")
public void handle(@RequestBody String body, Writer writer) throws IOException {
    writer.write(body);
}

另外,与绑定响应体类似,我们需要使用@ResponseBody注解到方法上,这会告诉Spring直接将该方法的返回结果作为响应返回给客户端。

@GetMapping("/something")
@ResponseBody
public String helloWorld() {
    return "Hello World";
}

在底层,Spring会使用<code>HttpMessageConverter</code>来将请求信息转换成我们需要的类型。Spring Web MVC为我们自动注册了一些<code>HttpMessageConverter</code>,详细情况参见Spring 参考文档 Section 22.16.1, “Enabling the MVC Java Config or the MVC XML Namespace”

Rest控制器

@RestController会向所有@RequestMapping方法添加@ResponseBody注解。如果控制器需要实现REST API,那么这时候就很方便。

使用HttpEntity

HttpEntity和请求体、响应体这两个类似,可以在一个地方同时处理请求和响应。

下面是Spring官方的一个例子,获取了请求HttpEntity,处理之后返回一个响应HttpEntity。Spring会使用<code>HttpMessageConverter</code>做必要的转换。

@RequestMapping("/something")
public ResponseEntity<String> handle(HttpEntity<byte[]> requestEntity) throws UnsupportedEncodingException {
    String requestHeader = requestEntity.getHeaders().getFirst("MyRequestHeader"));
    byte[] requestBody = requestEntity.getBody();

    // do something with request header and body

    HttpHeaders responseHeaders = new HttpHeaders();
    responseHeaders.set("MyResponseHeader", "MyValue");
    return new ResponseEntity<String>("Hello World", responseHeaders, HttpStatus.CREATED);
}

使用ModelAttribute

@ModelAttribute注解用于向模型添加属性。可以作用到方法,这时候该方法会在该控制器的所有处理方法前执行。在方法中可以接受多个参数和一个模型参数,然后将这些参数处理之后添加到模型中。这样每次处理方法执行前都会先执行一次该方法。因此如果控制器中有多个处理方法要小心使用这个注解。

@ModelAttribute
public void addModel(@RequestParam String name, Model model) {
    model.addAttribute("name",name);
    // add more ...
}

@ModelAttribute还可以作用到方法参数上。这种情况更常见也更加有用。这时候Spring会先从model中寻找@ModelAttribute参数,如果没找到则实例化一个(因此这个类必须有无参构造函数),然后添加到model中。然后将请求参数(下面例子中是name=易天&age=24&gender=男)添加到模型中。这样当我们查看视图的时候,一个完整的实体类已经准备就绪了。

//  请求 /modelAttribute?name=易天&age=24&gender=男
@RequestMapping("/modelAttribute")
public String modelAttribute(@ModelAttribute User user, Model model) {
    model.addAttribute("user", user);
    return "modelAttribute";
}

User类的内容如下, 各种方法已省略。

public class User {
    private String name;
    private int age;
    private String gender;
}

使用SessionAttribute

@SessionAttribute可以用于控制器上,这时候它会将上面介绍的ModelAttribute保存到Session中,方便多个方法间使用。

@Controller
@SessionAttributes("user")
public class MainController {
    //  请求 /modelAttribute?name=易天&age=24&gender=男
    @RequestMapping("/modelAttribute")
    public String modelAttribute(@ModelAttribute User user, Model model) {
        model.addAttribute("user", user);
        return "modelAttribute";
    }
}

@SessionAttributes还可以用到处理方法的参数上,这时候可以获取到Session中相应名称的属性,需要注意这个属性必须是已存在的。如果该属性不存在Spring就会抛出异常,然后将我们导向400页面。

@RequestMapping("/accessSession")
public String accessSession(@SessionAttribute User user, Model model) {
    model.addAttribute("user", user);
    return "accessSession";
}

如果需要还可以直接使用HttpSession,方法很简单,将方法参数定义为HttpSession,然后Spring就会将session对象注入到方法中。我们可以直接进行操作。

@RequestMapping("/httpSession")
public String httpSession(HttpSession session, Model model) {
    User user = new User("林妹妹", 24, "女");
    session.setAttribute("user", user);
    model.addAttribute("session", session);
    return "accessSession";
}

使用@RequestAttribute

@RequestAttribute用于获取RequestAttribute,这些请求属性可能是由过滤器或拦截器产生的。

@RequestMapping("/")
public String handleInfo(@RequestAttribute String info) {
    // ...
}

处理application/x-www-form-urlencoded数据

浏览器会使用GET或者POST方法发送数据,非浏览器客户端可以使用PUT方法发送数据。

但是PUT发送过来的数据,不能被Servlet系列方法<code>ServletRequest.getParameter()</code>获取到。

Spring提供了一个过滤器<code>HttpPutFormContentFilter</code>,用于支持非浏览器的PUT信息发送。

<code>HttpPutFormContentFilter</code>需要在web.xml中配置。<servlet-name>配置的是Spring的DispatcherServlet的名称。

<filter>
    <filter-name>httpPutFormFilter</filter-name>
    <filter-class>org.springframework.web.filter.HttpPutFormContentFilter</filter-class>
</filter>

<filter-mapping>
    <filter-name>httpPutFormFilter</filter-name>
    <servlet-name>dispatcherServlet</servlet-name>
</filter-mapping>

这个过滤器会拦截Content-Type是<code>application/x-www-form-urlencoded</code>的PUT请求,读取其请求体然后包装到ServletRequest中,以便<code>ServletRequest.getParameter()</code>可以获取到参数。

使用@CookieValue

@CookieValue可以获取某个Cookie的值。如果该cookie不存在,就会抛出异常,可以使用required和defaultValue指定是否必须和默认值相等。

@RequestMapping("/cookie")
public void cookie(@CookieValue("JSESSIONID") String cookie) {
    //...
}

@RequestHeader

@RequestHeader注解可以获取RequestHeader的信息,可以使用required和defaultValue指定是否必须和默认值相等。

@RequestMapping("/header")
public String headerInfo(@RequestHeader("Accept-Encoding") String encoding, Model model) {
    model.addAttribute("encoding", encoding);
    return "info";
}

控制器通知

先来介绍一下@InitBinder注解,它可以放到控制器的一个方法上,这个方法有一个WebDataBinder参数,用它可以对控制器进行定制,添加格式转换、验证等功能。

@InitBinder
protected void initBinder(WebDataBinder binder) {
//添加功能
}

然后就可以介绍@ControllerAdvice和@RestControllerAdvice这两个注解了。

它们可以定义控制器通知,这个和AOP中的Advice概念是一样的。这些注解需要应用到类上,这些类可以包括@ExceptionHandler, @InitBinder和 @ModelAttribute注解的方法,然后这些方法就会在恰当的时机来执行。

// 注解了RestController的控制器
@ControllerAdvice(annotations = RestController.class)
public class AnnotationAdvice {}

// 特定包下的控制器
@ControllerAdvice("org.example.controllers")
public class BasePackageAdvice {}

// 特定类型的控制器
@ControllerAdvice(assignableTypes = {MainController.class})
public class AssignableTypesAdvice {}

拦截请求

我们可以使用拦截器拦截请求并进行处理,这一点有点像Servlet的过滤器。

我们需要实现org.springframework.web.servlet.HandlerInterceptor接口,不过更好的方法是继承HandlerInterceptorAdapter类,这个类将几个拦截方法进行了默认实现。我们只需要重写需要的方法即可。

下面定义了一个简单的拦截器,作用仅仅是输出拦截时间。我们可以看到有四个拦截时机,处理请求前,处理请求后,完成请求后和异步处理开始后,这些拦截方法的参数是Http请求和响应,使用很方便。

public class LogInterceptor extends HandlerInterceptorAdapter {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        System.out.println("preHandle:" + LocalTime.now());
        return super.preHandle(request, response, handler);
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        System.out.println("postHandle:" + LocalTime.now());
        super.postHandle(request, response, handler, modelAndView);
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        System.out.println("afterCompletion:" + LocalTime.now());
        super.afterCompletion(request, response, handler, ex);
    }

    @Override
    public void afterConcurrentHandlingStarted(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        System.out.println("afterConcurrentHandlingStarted:" + LocalTime.now());
        super.afterConcurrentHandlingStarted(request, response, handler);
    }
}

有了拦截器之后,我们还需要注册它。首先将其注册为一个Spring Bean。然后定义一个RequestMappingHandlerMapping并将拦截器传递给它。

<beans>
    <bean id="handlerMapping"
            class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping">
        <property name="interceptors">
            <list>
                <ref bean="logInterceptor"/>
            </list>
        </property>
    </bean>

    <bean id="logInterceptor"
            class="samples.LogInInterceptor"/>

</beans>

定义拦截器的这部分配置可以使用mvc命名空间简化。

<mvc:interceptors>
    <ref bean="logInterceptor"/>

</mvc:interceptors>

默认情况下拦截器针对所有处理方法。如果希望只匹配某些URL,可以定义一个org.springframework.web.servlet.handler.MappedInterceptor,使用它的构造方法设置映射。

<bean class="org.springframework.web.servlet.handler.MappedInterceptor">
    <constructor-arg index="0">
        <list>
            <value>/</value>
        </list>
    </constructor-arg>
    <constructor-arg index="1" ref="logInterceptor"/>
</bean>

也可以直接使用mvc命名空间。

<mvc:interceptors>
    <mvc:interceptor>
        <mvc:mapping path="/"/>
        <ref bean="logInterceptor"/>
    </mvc:interceptor>
</mvc:interceptors>

拦截器可能不适用@ResponseBody和ResponseEntity方法,因为这些方法会使用HttpMessageConverter来输出响应。这时候我们可以实现ResponseBodyAdvice接口,然后使用@ControllerAdvice注解或者直接在RequestMappingHandlerAdapter.配置它们来拦截这些方法。

转至 http://www.jianshu.com/p/042aa5b14d90 感谢!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容