SpringMVC学习笔记(二)

接着上一篇来说,这篇包括 RequestMapping 的进一步探索、与 Struts2 的主要区别、为什么用 JSTL、设置 JSON 格式相关、文件上传以及(自定义)拦截器的相关内容。
相比之下这篇的内容就少了,果然 SpringMVC 是轻量级的原因么,比 Struts 的篇幅要少得多,但依然做到高效开发,有一些源码级别的内容还没深入看,以后慢慢再继续补充。

深入RequestMapping

都知道它是一个处理请求地址的注解,来研究下当一个方法被 @RequestMapping 标记后(处理器方法),它所支持的方法参数和返回类型。

支持的方法参数

  1. HttpServlet 对象,主要包括 HttpServletRequest 、HttpServletResponse 和 HttpSession 对象。
    但是有一点需要注意的是在使用 HttpSession 对象的时候,如果此时 HttpSession 对象还没有建立起来的话就会有问题。
  2. Spring 自己的 WebRequest 对象。
    使用该对象可以访问到存放在 HttpServletRequest 和 HttpSession 中的属性值。
  3. InputStream 、OutputStream 、Reader 和 Writer 。
    InputStream 和 Reader 是针对 HttpServletRequest 而言的,可以从里面取数据;OutputStream 和 Writer 是针对 HttpServletResponse 而言的,可以往里面写数据。
  4. 使用 @PathVariable 、@RequestParam 、@CookieValue 和 @RequestHeader 标记的参数。
  5. 使用 @ModelAttribute 标记的参数。
  6. java.util.Map 、Spring 封装的 Model 和 ModelMap 。
    这些都可以用来封装模型数据,用来给视图做展示。
  7. 实体类。
    可以用来接收上传的参数。
  8. Spring 封装的 MultipartFile 。
    用来接收上传文件的。
  9. Spring 封装的 Errors 和 BindingResult 对象。
    这两个对象参数必须紧接在需要验证的实体对象参数之后,它里面包含了实体对象的验证结果。

仔细看看确实不算少,但大部分都已经看过了。

支持的返回类型

  1. 一个包含模型和视图的 ModelAndView 对象。
  2. 一个模型对象,这主要包括 Spring 封装好的 Model 和 ModelMap ,以及 java.util.Map ,当没有视图返回的时候视图名称将由 RequestToViewNameTranslator 来决定。
  3. 一个 View 对象。
    这个时候如果在渲染视图的过程中模型的话就可以给处理器方法定义一个模型参数,然后在方法体里面往模型中添加值。
  4. 一个 String 字符串。
    这往往代表的是一个视图名称。这个时候如果在渲染视图的过程中需要模型的话就可以给处理器方法一个模型参数,然后在方法体里面往模型中添加值就可以了。
  5. 返回值是 void 。
    这种情况一般是我们直接把返回结果写到 HttpServletResponse 中了,如果没有写的话,那么Spring 将会利用 RequestToViewNameTranslator 来返回一个对应的视图名称。
    如果视图中需要模型的话,处理方法与返回字符串的情况相同。
  6. 如果处理器方法被注解 @ResponseBody 标记的话,那么处理器方法的任何返回类型都会通过 HttpMessageConverters 转换之后写到 HttpServletResponse 中,而不会像上面的那些情况一样当做视图或者模型来处理。
  7. 除以上几种情况之外的其他任何返回类型都会被当做模型中的一个属性来处理;
    而返回的视图还是由 RequestToViewNameTranslator 来决定,
    添加到模型中的属性名称可以在该方法上用 @ModelAttribute(“attributeName”) 来定义,否则将使用返回类型的类名称的首字母小写形式来表示。
    使用 @ModelAttribute 标记的方法会在 @RequestMapping 标记的方法执行之前执行。

与Struts2的区别

它们两者的实现机制是不同的,所以说区分是很大的,主要是下面几点:

  • SpringMVC 的入口是 Servlet,Struts2 是基于 Filter,所以就说它们的实现机制是不同的
  • SpringMVC 是基于方法的设计,也就是说传递参数是通过方法的形参,实现是单例(Spring 实例化对象默认就是单例),也推荐单例,这样就省去了创建、销毁对象的过程,提高效率,并且使用方法的形参传值,也不需要担心并发问题。
    Struts2 是基于类设计,传递参数通过类的属性,只能设置为多例。
  • 参数传递方面,Struts2 因为是用类属性接收,所以不同方法可以共享;但是在 SpringMVC 中多个方法直接不能共享参数,因为是基于方法的嘛

使用JSTL

前面学过,JSTL 是个标签库,为什么要使用 JSTL 呢?SpringMVC 没有么?
答案是 SpringMVC 有自己的标签库,但是并没有 Struts2 的那么强大,所以就没有必要用它的了,使用经典成熟的 JSTL 或许是更好的选择。
在讲框架流程的时候也说了,最终模型里的数据会填充到 Request 域,所以 JSTL 可以直接获取数据来用的。
只要你还记得导 C 标签库和会用 JSTL 就行….

输出&输入为JSON

在上一篇的 @ResponseBody 注解中提到了,使用了这个注解就意味着是不正常的输出(其他视图),默认就是 JSON。

1
2
3
@RequestMapping(value = "/userlist")
@ResponseBody
public List<User> show () {...}

返回值是 List,当加入了 Jackson 的依赖后,会自动注册其对应的转换器,最终响应的就是 JSON 数据格式了,会自动把 List 对象序列化为 JSON 格式的数据。
当输入为 JSON 的时候就需要用 @RequestBody 来处理了,它们一个是从对象到 JSON,一个是 JSON 到对象(这里以 JSON 这种格式为例)

1
2
3
4
@RequestMapping(value = "/user")
public ModelAndView show (@RequestBody User user) {...}
// 获取原始 JSON 数据
public ModelAndView show (@RequestBody String user) {...}

这个栗子就是接收一个 JSON 数据,然后反序列为 user 对象。

文件上传

这里就用比较简单的方式了,Spring 自带的上传功能效率好像更好些,比较复杂,稍后再说;
首先要添加 Apache 的 commons-fileupload 依赖,上传功能依赖于它;然后在 SpringMVC 的配置文件中做相应的配置:

1
2
3
4
5
 <!-- 上传文件的设置 ,maxUploadSize=-1,表示无穷大。uploadTempDir为上传的临时目录 -->  
<bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver"
p:defaultEncoding="UTF-8"
p:maxUploadSize="5400000"
p:uploadTempDir="fileUpload/temp"/>

使用了 p 命名空间,不记得?去看 spring 笔记二。可以配合 Spring 提供的 CharacterEncodingFilter 来简单处理下乱码问题。
如果怕出异常(如超出限制),可以配下 exceptionResolver 这个 bean。
然后定义处理上传文件的控制器即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@Controller
public class TestController {

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

@RequestMapping(value="/uploadfile",method=RequestMethod.POST)
public String upLoadFile(@RequestParam("file") MultipartFile[] myfiles, HttpServletRequest request) throws IOException {
//如果只是上传一个文件,则只需要MultipartFile类型接收文件即可
for(MultipartFile myfile : myfiles) {
if(myfile.isEmpty()) {
System.out.println("文件未上传");
continue;
} else {
System.out.println("文件长度: " + myfile.getSize());
System.out.println("文件类型: " + myfile.getContentType());
System.out.println("文件名称: " + myfile.getName());
System.out.println("文件原名: " + myfile.getOriginalFilename());
System.out.println("===================");

String realPath = request.getSession().getServletContext().getRealPath("/WEB-INF/upload");
// FileUtils.copyInputStreamToFile() 会自动关流
FileUtils.copyInputStreamToFile(myfile.getInputStream(), new File(realPath + "/" +myfile.getOriginalFilename()));
// 或者使用下面的方式保存
// myfile.transferTo(new File(realPath + "/" +myfile.getOriginalFilename()));
}
}
return "redirect:/success.html";
}
}

可以看到这些方法的返回值都是 String 类型的,根据前面所讲,对应的就是视图名了,如果字符串是以 redirect 开头,那就表示是以重定向的方式跳转到后面的地址。
然后,可以看到文件上传多数是通过 MultipartFile 这个对象来处理的,能够理解为什么前面在配置文件中配了个 MultipartResolver 解析器了吧。
Spring使用 Jakarta Commons FileUpload 技术实现了一个 MultipartResolver 实现类:CommonsMultipartResolver;
如果上传的是超大文件,还是使用流式传输比较好(使用 CommonsMultipartFile 对象来接收),虽然慢,但是省资源。
因为 transferTo() 函数调用 write() 函数,而这个函数要求上传文件已经要在内存中,或者是在磁盘里,才能成功执行。

拦截器

这一块的学习成本应该是比较低的,据说 SpringMVC 的拦截器思想来源就是 Struts2,所以应该差不多。
SpringMVC 拦截器接口(HandlerInterceptor)定义了三个方法:

  • preHandle
    该方法在目标方法之前被调用(调用 Handler 之前),在该方法中对用户请求 request 进行处理
    若返回值为 true, 则继续调用后续的拦截器和目标方法.
    若返回值为 false, 则不会再调用后续的拦截器和目标方法.
    可以考虑做权限. 日志, 事务等。
    方法中的 Object 对象,对于静态资源是 ResourceHttpRequestHandler 类型(一般都会进行排除不会拦截的);对于动态资源是 HandlerMethod 类型,通过它可以获得 Method 等对象。
  • postHandle
    调用目标方法之后, 但渲染视图之前.
    可以对请求域中的属性或视图做出修改,也就是说:
    这个方法在业务处理器处理完请求后,但是 DispatcherServlet 向客户端返回响应前被调用,在该方法中对用户请求 request 进行处理。
  • afterCompletion
    渲染视图之后被调用(在 DispatcherServlet 完全处理完请求后). 可用来释放资源

自定义拦截器就不贴了,建个类,实现 HandlerInterceptor 接口就行了,就是上面的那三个方法,光写完了是没卵用的,还需要配置进 springmvc 的配置文件,这样它才知道你写了,然后才会执行(还记得框架流程图里的执行链?):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<mvc:interceptors>
<!-- 配置自定义的拦截器,这样写所有请求都会生效 -->
<bean class="com.MySpringMVC.interceptors.FirstInterceptor"/>
</mvc:interceptors>

<!-- 下面看比较理性的写法 -->
<mvc:interceptors>
<mvc:interceptor>
<!-- 配置拦截器作用的路径;/** 为所有请求 -->
<mvc:mapping path="/emps" />
<!-- 若要配置不起作用的路径,则使用 <mvc:exclude-mapping path=""/> -->
<!-- 如果使用了 @Component 注解,可以用 ref 来指定,id 默认类名首字母小写 -->
<bean class="com.MySpringMVC.interceptors.SecondInterceptor"></bean>
</mvc:interceptor>
</mvc:interceptors>

前面说过吧,/** 是指所有目录,也可以认为是所有请求了。
注意:即使配置了 mvc:resources 或者 mvc:default-servlet-handler 排除静态资源,但是除了视图(jsp、html)其他静态资源也会被拦截,需要再配置放行规则。


然后下面就是重点的顺序问题,和 Struts 相似,preHandle 按照配置文件的顺序执行,剩余两个按照配置顺序的反序执行,应该能想得通吧。
工作流程(建议参考源码):

  1. 当页面发送请求后,DispatcherServlet 会通过其中的 HandlerExecutionChain 对象依次调用各拦截器的的 preHandle() 方法。
    该 HandlerExecutionChain 对象,其中的 interceptors 属性记录了当前项目配置的所有拦截器,除了我们自定义的两个拦截器以外,还有一个 SpringMVC 内置的 ConversionServiceExposingInterceptor 拦截器。
    此外,还有一个 int 型参数 interceptorIndex 用于记录当前时刻 preHandle() 方法已经返回 true 的拦截器的最大下标;
    例如现在我们的 SecondInterceptor 拦截器的 preHandle() 方法返回了 false,那么 interceptorIndex 参数便为1,代表拦截器 ConversionServiceExposingInterceptor 和 FirstInterceptor 的 preHandle() 方法返回了 true
  2. 当 HandlerExecutionChain 对象发现某拦截器的 preHandle() 方法返回了 false 后,便执行 triggerAfterCompletion() 方法用于从下标值为 interceptorIndex 开始执行各拦截器的 afterCompletion() 方法,直到下标值减为 -1 为止
    然后执行链直接 return,终止执行。
  3. 如果 HandlerExecutionChain 对象发现某拦截器的 preHandle() 方法返回的是 true ,那么会继续执行下一个的 preHandle,全部执行完后再执行 Handler 的具体逻辑
  4. Handler 执行完毕后,开始倒序执行 postHandle() 各拦截器的后置方法
  5. 后置方法调用完成后开始渲染视图(利用 ModelAndView 对象),渲染完成后倒序调用拦截器的完成方法 afterCompletion()

也就是说,无论第二个拦截器的 preHandle 返回的是什么,第一个(已经通过的拦截器)的 afterCompletion 都会执行;但是如果返回的是 false,Handler 和 postHandle 就不会执行了。

和过滤器的区别:
简单来说,过滤器什么都能拦下来,而拦截器则不一定,看框架的实现,比如 SpringMVC 的拦截器就不拦视图。
其次,过滤器是 Servlet API 里的,其中不能注入 Spring IoC 的对象;拦截器是 Spring 自己的(SpringMVC 中),就可以使用了。

使用RESTful

前面最开始的时候页介绍了,SpringMVC 支持 RESTful 风格,原来是使用 @ResponseBody 注解来转成相关的 json 格式,其实不需要这么的复杂。
这里必须要说下 ResponseEntity< T> 这个对象了:

ResponseEntity 意味着表示整个 HTTP 响应。你可以控制任何进入它:状态码,标题和正文。
@ResponseBody 是 HTTP 响应正文的标记,@ResponseStatus 声明 HTTP 响应的状态代码。

来看几个栗子就知道是怎么回事了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
@Controller
@RequestMapping("rest/girl")
public class RestfulController {
private static final Logger LOGGER = LoggerFactory.getLogger(RestfulController.class);

@Resource
private GirlService girlService;

@RequestMapping(value = "{id}", method = RequestMethod.GET)
@ResponseBody
private ResponseEntity<Girl> getGirl(@PathVariable("id") Integer id) {
LOGGER.info("----------------->" + id);
try {
Girl girl = girlService.queryGirl(id);
if (girl == null) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(null);
}
// 200 可以进行简写
return ResponseEntity.ok(girl);
} catch (Exception e) {
e.printStackTrace();
}
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(null);
}

@RequestMapping(method = RequestMethod.POST)
public ResponseEntity<Void> saveGirl(Girl girl) {
LOGGER.debug("-------------->" + girl.getName());
try {
if (girl.getName().isEmpty() || girl.getAge().toString().isEmpty()) {
// 400 错误,参数不正确
return ResponseEntity.status(HttpStatus.BAD_REQUEST).build();
}
Boolean flag = girlService.saveGirl(girl);
if (flag) {
// 新增成功,响应 201,如果返回是 void 可以使用 build 方法
return ResponseEntity.status(HttpStatus.CREATED).build();
}
} catch (Exception e) {
e.printStackTrace();
}
// 新增失败
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}

@RequestMapping(method = RequestMethod.PUT)
public ResponseEntity<Void> updateGirl(Girl girl) {
LOGGER.debug("-------------->" + girl.getName());
try {
if (girl.getName().isEmpty() || girl.getId().toString().isEmpty()) {
// 400 错误,参数不正确
return ResponseEntity.status(HttpStatus.BAD_REQUEST).build();
}
Boolean flag = girlService.updateGirl(girl);
if (flag) {
// 修改成功,响应 204,如果返回是 void 可以使用 build 方法
return ResponseEntity.status(HttpStatus.NO_CONTENT).build();
}
} catch (Exception e) {
e.printStackTrace();
}
// 新增失败
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}


@RequestMapping(method = RequestMethod.DELETE)
public ResponseEntity<Void> deleteGirl(@RequestParam(value = "id",defaultValue = "0") Integer id) {
try {
if (id == 0) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).build();
}
Boolean flag = girlService.deleteGirl(id);
if (flag) {
// 删除成功,响应 204
return ResponseEntity.status(HttpStatus.NO_CONTENT).build();
}
} catch (Exception e) {
e.printStackTrace();
}
// 新增失败
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
}

返回的时候会自动把实体里的属性(凡是有 getter 方法的)转换成 JSON 类型的格式(默认)返回,同时可以设置状态码之类的,总之就是非常的全面了,先说这些,等以后再补充。

其他相关的注解:

  • @RestController:
    Spring 4 的新注解。 她自动为每个方法加上 @ResponseBody ;
    @RestController 可以看做是 @Controller 和@ResponseBody 两个注解的组合。
  • @RequestBody:
    如果一个方法申明了 @RequestBody 注解, Spring 会基于请求头中的 Content-Type 使用 HTTP Message converters 将 request body 反序列化成对象。
  • @ResponseBody:
    是在方法的上注解,如果一个方法申明了@ResponseBody, Spring 会基于请求头中的 Accept 使用 HTTP Message converters 将对象序列化成 response body。
    在 Spring 4 中如果使用了 @RestController,则可以不用再声明此注解。

关于父/子容器

当我们的项目中引入了 Spring + SpringMVC 后,在启动后其实有两个 Spring 容器,一个是 Spring 的容器(处理 Bean),一个是 SpringMVC 的容器(处理 Controller),它们是父子关系,Spring 就是父容器了!
它们的关系:

  • 子容器能够访问父容器的资源(Bean)
    比如说在 Controller 中可以引用(注入) Service 中的 Bean
  • 父容器不能访问子容器中的资源
    这也就说明了为什么在 Spring 的配置文件中配置了扫描包,而在 SpringMVC 的配置文件中却还要再配置

然后再说下 @Value 这个注解,它就是用来获取配置文件中的值的;时机就是在所有 Bean 初始完成后从当前所在容器中获取值,然后注入;所以说如果在 Controller 中使用可能获取不到(假设配置在 Spring 文件中读取),即使子容器能够访问父容器,但是优先级最高的还是注解自身的特性。
解决方案可以采用在 Service 中写一个 propertiesService 专门用来注入配置文件的值,然后在 Controller 中注入这个 Service 来间接的使用配置文件中的值。
在它们整合的时候,SpringMVC 其实会扫描是否存在 Spring 容器(存在 Application 域,所以还算好找),如果存在就把它作为父容器,如果不存在它会自己搞一个(具体不是很清楚)

路径匹配

在 SpringMVC 中遵循:路径匹配,先最长路径匹配,再最短路径匹配;优先级最高的精确匹配就不说了。
所以说 /* 可以匹配所有,如果设置为 <url-pattern>/user/*</url-pattern> 那么 /abc/def/user/list 也是可以匹配到的,我想说明的就是这个,在以前的文章中应该提到过,不过我没找到呢。

关于放行静态资源

当 SpringMVC 拦截 / 的时候,会拦截所有请求,这时候需要配置让其放行静态资源,方式有几种:

  • 激活Tomcat的defaultServlet来处理静态文件
  • XML 中使用 mvc:resources 标签
  • XML 中使用 mvc:default-servlet-handler 标签

对于方案一,要写在 DispatcherServlet 的前面, 让 defaultServlet 先拦截,这个就不会进入 Spring 了:

1
2
3
4
5
6
7
8
9
10
11
12
<servlet-mapping>     
<servlet-name>default</servlet-name>
<url-pattern>*.jpg</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>default</servlet-name>
<url-pattern>*.js</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>default</servlet-name>
<url-pattern>*.css</url-pattern>
</servlet-mapping>

关于默认 servlet 是专门处理静态资源的,名字也不是乱写的:

Tomcat, Jetty, JBoss, and GlassFish 默认 Servlet的名字 – “default”
Google App Engine 默认 Servlet的名字 – “_ah_default”
Resin 默认 Servlet的名字 – “resin-file”
WebLogic 默认 Servlet的名字 – “FileServlet”
WebSphere 默认 Servlet的名字 – “SimpleFileServlet”


方案二配置:<mvc:resources mapping="/images/**" location="/images/" />

  • location 指定静态资源的位置,可以是 web application 根目录下、jar 包里;多个路径可使用 , 分割
  • mapping 指的是 location 映射成的路径,前端请求访问的就是这个路径,** 表示的是包含多层目录。

由 Spring MVC 框架自己处理静态资源,并添加一些有用的附加值功能。

<mvc:resources /> 允许静态资源放在任何地方,如 WEB-INF 目录下、类路径下等,你甚至可以将JavaScript等静态文件打到JAR包中。
通过 location 属性指定静态资源的位置,由于location属性是Resources类型,因此可以使用诸如”classpath:”等的资源前缀指定资源位置。
传统 Web 容器的静态资源只能放在 Web 容器的根路径下,<mvc:resources /> 完全打破了这个限制。

其次,<mvc:resources /> 依据当前著名的 Page Speed、YSlow 等浏览器优化原则对静态资源提供优化。你可以通过cacheSeconds属性指定静态资源在浏览器端的缓存时间,一般可将该时间设置为一年,以充分利用浏览器端的缓存。在输出静态资源时,会根据配置设置好响应报文头的Expires 和 Cache-Control值。
在接收到静态资源的获取请求时,会检查请求头的Last-Modified值,如果静态资源没有发生变化,则直接返回303相应状态码,提示客户端使用浏览器缓存的数据,而非将静态资源的内容输出到客户端,以充分节省带宽,提高程序性能 。


方案三是用的最多的一种,将静态资源的处理经由 Spring MVC 框架交回Web应用服务器处理
定义此标签后,会在 Spring MVC 上下文中定义一个 org.springframework.web.servlet.resource.DefaultServletHttpRequestHandler,它会像一个检查员,对进入 DispatcherServlet 的 URL 进行筛查,如果发现是静态资源的请求,就将该请求转由 Web 应用服务器默认的 Servlet 处理,如果不是静态资源的请求,才由 DispatcherServlet 继续处理。
一般 Web 应用服务器默认的 Servlet 名称是 “default”,因此 DefaultServletHttpRequestHandler 可以找到它。如果你所有的 Web 应用服务器的默认 Servlet 名称不是 “default”,则需要通过 default-servlet-name 属性显示指定:
<mvc:default-servlet-handler default-servlet-name="所使用的Web服务器默认使用的 Servlet 名称" />

其他

接收数据时用到日期时,默认 SpringMVC 是不会处理的,如果使用的是 po 来接收,那么肯定会转换异常,这时可以使用 @DateTimeFormat 注解来格式化,它有一个 pattern 参数可以指定日期的格式,比如:@DateTimeFormat(pattern="yyyy-MM-dd")
类似的,SpringMVC 提供其他多个 format,比如 NumberFormat 需要时了解吧


如果只是简单的跳转视图逻辑,那么可以使用 <mvc:view-controller path="" view-name=""/> 来配置,省的单独写一个 controller 方法了。

参考

http://www.cnblogs.com/fangjian0423/p/springMVC-interceptor.html
http://blog.csdn.net/xiangwanpeng/article/details/53157756
http://www.jianshu.com/p/58cf4936c523
https://www.cnblogs.com/fangqi/archive/2012/10/28/2743108.html

喜欢就请我吃包辣条吧!

评论框加载失败,无法访问 Disqus

你可能需要魔法上网~~