向万鹏的独立博客


  • 首页

  • 分类

  • 归档

  • 标签

在Spring的环境下使用SpringMVC

发表于 2016-11-15   |   分类于 SpringMVC   |  

是否需要用Spring整合SpringMVC

  是否需要用 Spring 整合 SpringMVC?或者更通俗地说,是否需要在 web.xml 文件中配置了SpringMVC IOC 容器的 DispatcherServlet 的同时,再去配置启动 Spring IOC 容器的 ContextLoaderListener?关于这个问题,答案通常是需要的。建议SpringMVC的配置文件只去配置SpringMVC需要的handler,而类似于数据源,事务,Service,Dao以及整合其他框架都是放在Spring的配置文件中(而不是放在 SpringMVC 的配置文件中)。

bean被创建两次?

  当我们同时配置了SpringMVC和Spring的IOC容器,并且指定扫描相同的包,那么同一个Bean会被创建两次。而正确的情况应该为,Spring的IOC 容器不应该扫描SpringMVC中的bean, 对应的SpringMVC的IOC 容器不应该扫描Spring中的bean。有两种解决方法:
  
  方法一:使Spring的IOC容器扫描的包和SpringMVC的IOC 容器扫描的包没有重合的部分。不过在实际情况下,项目的开发很可能是分模块进行的,所以这种方法并不总是适合。
  
  方法二:在SpringMVC的IOC容器中指定只扫描由@Controller和@ControllerAdvice注解标记的bean:

1
2
3
4
5
6
7
<context:component-scan base-package="com.MySpringMVC"
use-default-filters="false">
<context:include-filter type="annotation"
expression="org.springframework.stereotype.Controller" />
<context:include-filter type="annotation"
expression="org.springframework.web.bind.annotation.ControllerAdvice" />
</context:component-scan>

  同时在Spring的IOC容器中指定不扫描由@Controller和@ControllerAdvice注解标记的bean:

1
2
3
4
5
6
<context:component-scan base-package="com.MySpringMVC">
<context:exclude-filter type="annotation"
expression="org.springframework.stereotype.Controller" />
<context:exclude-filter type="annotation"
expression="org.springframework.web.bind.annotation.ControllerAdvice" />
</context:component-scan>

SpringMVC的IOC容器中的bean可以引用Spring的IOC容器中的bean,反之则不行

  SpringMVC的IOC容器中的bean可以引用SpringIOC容器中的bean,例如,我们现在在SpringMVC的IOC容器中注册了一个Controller,名为HelloWorld,在SpringIOC的容器中注册了一个Service,名为UserService,那么现在我可以在HelloWorld中引用UserService的bean:

1
2
3
4
5
6
7
8
9
10
11
12
@Controller
public class HelloWorld {
@Autowired
private UserService userService;
@RequestMapping("/hello")
public String hello(){
System.out.println("hello:"+userService);
return "success";
}
}

运行程序将输出:

image_1b1jj5u7e24rlmn75v1sj311n09.png-30.5kB

  但是我们却无法在UserService中引用HelloWorld的bean:

1
2
3
4
5
6
@Service
public class UserService {
@Autowired
private HelloWorld helloWorld;
}

运行程序会报错:

image_1b1jjguch11gk9c91eu1va11iskm.png-46.3kB

  这是因为多个Spring IOC 容器之间可以设置为父子关系,以实现良好的解耦。
  Spring MVC的IOC作为WEB层容器,是业务层容器Spring IOC的子容器:即WEB 层容器可以引用业务层容器的Bean,而业务层容器却访问不到WEB 层容器的Bean,如下图所示:
  image_1b1jjkmo9u94lo11ie11labntr13.png-28.6kB
  
  实际上,从软件设计的层面来讲这也是符合逻辑的——Controller可以依赖Service,而Service却不能依赖Controller。

SpringMVC的工作流程

发表于 2016-11-15   |   分类于 SpringMVC   |  

  SpringMVC的运行流程如下图所示:
  
  image_1b1j2u0oo66l1vmu11551abh7rq9.png-240.3kB
  
注意点:
1. 如果SringMVC中不存在请求的对应映射,会检查springmvc的配置文件中是否配置了 <mvc:default-servlet-handler />。
1.1 如果没有配置,则会跳转到404页面,并在控制台输出 No mapping found for HTTP request with URI[/XX/XX] in DispatcherServelt;
1.2 如果配置了,则会把该请求当作对静态资源的请求,并去访问相应的静态资源。如果找不到该静态资源,那么也会跳转到404页面,但是不会在控制台输出上述提示信息。

2. 如果SringMVC中存在请求的对应映射,那么在从执行控制器的目标方法到渲染视图并得到响应结果的过程中,会涉及到三个比较重要的类:

  • HandlerMapping:用于定义请求到处理器之间的映射。
  • HandlerExecutionChain:处理器调用链。包含了某一个处理器对象,以及和这个处理器相关的拦截器。
  • HandlerAdapter:处理器适配器。 用于实现目标方法调用过程中的适配工作(例如表单数据类型的校验、转换、格式化,HttpMessageConverter的使用),并调用目标方法。

SpringMVC中的异常处理

发表于 2016-11-14   |   分类于 SpringMVC   |  

  Spring MVC通过HandlerExceptionResolver处理程序的异常,包括Handler 映射、数据绑定以及目标方法执行时发生的异常。在Eclipse中可以查看到HandleExceptionResolver接口的实现类:
  
  image_1b1gosocup6i1ugoq3p119qdta9.png-34.4kB

  如果我们没有在springmvc配置文件中配置<mvc:annotation-driven/>,那么DispatcherServlet会默认装配AnnotationMethodHandlerExceptionResolver、DefaultHandlerExceptionResolver和ResponseStatusException三种解析器,但是其中AnnotationMethodHandlerExceptionResolver已经过期了。所以我们配置<mvc:annotation-driven/>,将会把AnnotationMethodHandlerExceptionResolver替换为ExceptionHandlerExceptionResolver。
  下面的例子中,我们都默认已经在spring配置文件中配置了<mvc:annotation-driven/>。

ExceptionHandlerExceptionResolver

  ExceptionHandlerExceptionResolver主要用于处理Handler 中用@ExceptionHandler注解标记的方法,示例如下:


页面
index.jsp

1
2
<a href="testExceptionHandlerExceptionResolver?i=0">
Test ExceptionHandlerExceptionResolver</a>

error.jsp

1
2
<h4>Error Page</h4>
${exception }


控制器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private final static String ERROR = "error";
//异常处理器
//value = ArithmeticException.class声明捕获数学异常
@ExceptionHandler(value = ArithmeticException.class)
public ModelAndView handleArithmeticException(Exception ex) {
ModelAndView mv = new ModelAndView(ERROR);
System.out.println("出异常了:" + ex);
mv.addObject("exception", ex);
return mv;
}
//定义一个可以抛出ArithmeticException异常的方法
@RequestMapping("/testExceptionHandlerExceptionResolver")
public String testExceptionHandlerExceptionResolver(@RequestParam("i") int i) {
System.out.println("result:" + 10 / i);
return SUCCESS;
}


运行程序:
点击index.jsp中的超链接,异常处理器会捕获到异常,在控制台输出:

image_1b1gqo9gavuv47c1s4lq8r1je9m.png-13.7kB

并跳转到error.jsp,且在该页面打印了异常信息:

image_1b1gqpbg41slr1e5dccbvk91oju13.png-12.6kB


需要注意的是:

  1. @ExceptionHandler标记的方法的入参中不能传入Map。 若希望把异常信息传导页面上, 需要使用 ModelAndView 作为返回值。
  2. @ExceptionHandler 方法标记的异常有优先级的问题。如果我们在上面的例子中同时声明了可以捕获ArithmeticException异常和RuntimeException异常的两个异常处理器,那么会使用匹配度更高的,即捕获ArithmeticException异常的异常处理器。
  3. 通过以上方式创建的异常处理器只能处理该控制器类内部的异常,若需要统一配置可以捕获所有控制器类抛出的异常,可以新建一个异常处理器类,在类的声明处用@ControllerAdvice注解标记即可。如果在当前 控制器类 中找不到 @ExceptionHandler 方法来处理当前方法出现的异常, 则将去 @ControllerAdvice 标记的类中查找 @ExceptionHandler 标记的方法来处理异常。例如,我们现在创建一个异常处理器类,并在其中定义一个可以处理数学异常的方法:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    @ControllerAdvice
    public class SpringMVCTestExceptionHandler {
    private static final String ERROR = "error";
    @ExceptionHandler(value=ArithmeticException.class)
    public ModelAndView handleArithmeticException1(Exception ex){
    ModelAndView mv = new ModelAndView(ERROR);
    System.out.println("[出异常了]:"+ex);
    mv.addObject("exception",ex);
    return mv;
    }
    }

注释掉之前定义的handleArithmeticException方法,运行程序,与之前的例子类似,仍然跳转到error.jsp,但是在控制台输出:

image_1b1hdanpd1bqcb3sdc215gporm1g.png-26.6kB

表示执行的是我们新定义的handleArithmeticException1方法,注意,如果不注释掉handleArithmeticException方法,那么会优先执行handleArithmeticException方法,因为handleArithmeticException方法和抛出异常的testExceptionHandlerExceptionResolver方法定义在同一个控制器类中,testExceptionHandlerExceptionResolver方法抛出异常后会先在当前 控制器类 中找 @ExceptionHandler方法来处理当前方法出现的异常,如果找不到,才会去 @ControllerAdvice 标记的类中查找 @ExceptionHandler 标记的方法来处理异常。

ResponseStatusExceptionResolver

  ResponseStatusExceptionResolver的作用是在异常及异常父类中找到@ResponseStatus注解,然后使用这个注解的属性进行处理。下面举例说明:


页面

1
2
<a href="testResponseStatusExceptionResolver?i=13">
Test ResponseStatusExceptionResolver</a>


定义一个异常类

1
2
3
4
5
6
7
8
9
@ResponseStatus(value=HttpStatus.FORBIDDEN,reason="用户名和密码不匹配")
public class UserNameNotMatchPasswordException extends RuntimeException{
/**
*
*/
private static final long serialVersionUID = 1L;
}


模拟一个可以抛出上述异常的方法

1
2
3
4
5
6
7
//定义一个可以抛出UserNameNotMatchPasswordException异常的方法
@RequestMapping("/testResponseStatusExceptionResolver")
public String testResponseStatusExceptionResolver(@RequestParam("i") int i) {
if(i==13)
throw new UserNameNotMatchPasswordException();
return SUCCESS;
}


运行程序,由于抛出了异常,并且我们没有定义任何异常处理器对该异常进行处理,那么会出现我们定制的异常页面:

image_1b1hfd8r917su1p7i1viie55ac1t.png-28.8kB

此外,@ResponseStatus注解还可以用来标记方法:

1
2
3
4
5
6
7
8
@ResponseStatus(reason="测试",value=HttpStatus.NOT_FOUND)
@RequestMapping("/testResponseStatusExceptionResolver")
public String testResponseStatusExceptionResolver(@RequestParam("i") int i) {
if(i==13)
throw new UserNameNotMatchPasswordException();
System.out.println("testResponseStatusExceptionResolver...");
return SUCCESS;
}

现在,我们在浏览器中令i=10,这样方法是不会抛出UserNameNotMatchPasswordException异常的,结果程序跳转到了如下页面:

image_1b1hg7ludgi3dqeadvea917mq2a.png-23.5kB

但是控制台上输出了如下信息,说明这个方法被正常地执行了:

image_1b1hg8ssm5goidn1f5m1rjqjm42n.png-27.9kB

DefaultHandlerExceptionResolver

  对一些特殊的异常进行处理,比如NoSuchRequestHandlingMethodException、HttpRequestMethodNotSupportedException等,具体处理哪些异常可以在DefaultHandlerExceptionResolver源码中查看doResolveException()方法。

SimpleMappingExceptionResolver

  SimpleMappingExceptionResolver可以将异常类名映射为视图名,即发生异常时使用对应的视图报告异常。下面进行测试:


页面

index.jsp

1
2
<a href="testSimpleMappingExceptionResolver?i=21">
Test SimpleMappingExceptionResolver</a>

error.jsp

1
2
<h4>Error Page</h4>
${exception }


控制器

1
2
3
4
5
6
7
// 定义一个可以抛出ArrayIndexOutOfBoundsException异常的方法
@RequestMapping("/testSimpleMappingExceptionResolver")
public String testSimpleMappingExceptionResolver(@RequestParam("i") int i) {
int[] values = new int[20];
System.out.println(values[i]);
return SUCCESS;
}


spring配置文件

1
2
3
4
5
6
7
8
9
<!-- 配置使用 SimpleMappingExceptionResolver 来映射异常 -->
<bean
class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">
<property name="exceptionMappings">
<props>
<prop key="java.lang.ArrayIndexOutOfBoundsException">error</prop>
</props>
</property>
</bean>


运行程序:

image_1b1hi0jq9bnd76gji918aa1pe234.png-14.7kB

由于目标方法抛出了数组下标越界异常,程序跳转到了error.jsp页面,并且在页面上打印了异常信息,这是因为在SimpleMappingExceptionResolver中有一个String类型的属性exceptionAttribute,其默认值为exception,所以我们在error.jsp中通过${exception }可以获取到。也可以在配置文件中配置exceptionAttribute的值,例如:

1
2
3
4
5
6
7
8
9
10
<!-- 配置使用 SimpleMappingExceptionResolver 来映射异常 -->
<bean
class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">
<property name="exceptionAttribute" value="ex"></property>
<property name="exceptionMappings">
<props>
<prop key="java.lang.ArrayIndexOutOfBoundsException">error</prop>
</props>
</property>
</bean>

那么,此时需要在error.jsp中通过如下方式在可以获取到异常信息:

1
2
<h4>Error Page</h4>
${ex }

     

深入理解SpringMVC中的拦截器

发表于 2016-11-14   |   分类于 SpringMVC   |  

  Spring MVC也可以使用拦截器对请求进行拦截处理。

自定义拦截器

  用户可以通过实现HandlerInterceptor接口,使用自定义的拦截器。
  下面展示如何配置自定义的拦截器:


创建拦截器类FirstInterceptor

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
public class FirstInterceptor implements HandlerInterceptor {
/**
* 该方法在目标方法之前被调用.
* 若返回值为 true, 则继续调用后续的拦截器和目标方法.
* 若返回值为 false, 则不会再调用后续的拦截器和目标方法.
*
* 可以考虑做权限. 日志, 事务等.
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
System.out.println("[FirstInterceptor] preHandle");
return true;
}
/**
* 调用目标方法之后, 但渲染视图之前.
* 可以对请求域中的属性或视图做出修改.
*/
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
ModelAndView modelAndView) throws Exception {
System.out.println("[FirstInterceptor] postHandle");
}
/**
* 渲染视图之后被调用. 释放资源
*/
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
throws Exception {
System.out.println("[FirstInterceptor] afterCompletion");
}
}


springmvc配置文件

1
2
3
4
5
<mvc:interceptors>
<!-- 配置自定义的拦截器 -->
<bean class="com.MySpringMVC.interceptors.FirstInterceptor">
</bean>
</mvc:interceptors>


现在只要有任何页面提交请求,拦截器就会工作:

image_1b1g8etan1egn13fj18ss70l1v089.png-15.9kB

注意:即使页面请求的是不通过控制器,而是通过配置<mvc:view-controller>直接跳转到页面,拦截器也会捕获到请求。

可以通过在<mvc:interceptors>用<mvc:interceptor>配置拦截器,从而指定拦截器对哪些请求起作用,对哪些请求不起作用,例如,我们现在定义另一个拦截器类SecondInterceptor:


springmvc配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
<mvc:interceptors>
<!-- 配置自定义的拦截器 FirstInterceptor -->
<bean class="com.MySpringMVC.interceptors.FirstInterceptor">
</bean>
<!-- 配置自定义的拦截器 SecondInterceptor -->
<mvc:interceptor>
<!-- 配置拦截器作用的路径 -->
<mvc:mapping path="/emps" />
<!-- 若要配置不起作用的路径,则使用 <mvc:exclude-mapping path=""/> -->
<bean class="com.MySpringMVC.interceptors.SecondInterceptor"></bean>
</mvc:interceptor>
</mvc:interceptors>


创建拦截器类 SecondInterceptor

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
public class SecondInterceptor implements HandlerInterceptor {
/**
* 该方法在目标方法之前被调用.
* 若返回值为 true, 则继续调用后续的拦截器和目标方法.
* 若返回值为 false, 则不会再调用后续的拦截器和目标方法.
*
* 可以考虑做权限. 日志, 事务等.
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
System.out.println("[SecondInterceptor] preHandle");
return true;
}
/**
* 调用目标方法之后, 但渲染视图之前.
* 可以对请求域中的属性或视图做出修改.
*/
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
ModelAndView modelAndView) throws Exception {
System.out.println("[SecondInterceptor] postHandle");
}
/**
* 渲染视图之后被调用. 释放资源
*/
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
throws Exception {
System.out.println("[SecondInterceptor] afterCompletion");
}
}


现在,如果我们在页面上请求访问的路径为emps,那么两个拦截器FirstInterceptor和SecondInterceptor都会起作用,如果请求访问emps路径以外的路径,那么只有FirstInterceptor拦截器会起作用。

拦截器方法及其执行顺序

1、单个拦截器各方法的执行顺序

  HandlerInterceptor接口定义了三个方法:

  • preHandle():这个方法在业务处理器处理请求之前被调用,在该方法中对用户请求request 进行处理。如果程序员决定该拦截器对请求进行拦截处理后还要调用其他的拦截器,或者是业务处理器去进行处理,则返回true;如果程序员决定不需要再调用其他的组件去处理请求,则返回false。
  • postHandle():这个方法在业务处理器处理完请求后,但是DispatcherServlet 向客户端返回响应前被调用,在该方法中对用户请求request进行处理。
  • afterCompletion():这个方法在DispatcherServlet 完全处理完请求后被调用,可以在该方法中进行一些资源清理的操作。

  例如FirstInterceptor拦截器各方法的执行顺序为:

image_1b1gadk6fun63kl8um1rfn1kvsm.png-51.1kB

2、多个拦截器之间各方法的执行顺序

  当有多个拦截器同时工作时,则它们的preHandle()方法按照拦截器的配置的顺序执行,它们的postHandle()方法和afterCompletion()方法按照配置顺序的反序执行。例如我们在上面的springmvc配置文件中,先配置了FirstInterceptor,后配置了SecondInterceptor,那么当请求两个拦截器都起作用的emps路径时,拦截器的执行顺序为:
  
  image_1b1garmstfhptsineq44h1ngv13.png-20.5kB

即:

image_1b1gbv3be1vjd134g2uveju16b71p.png-65.4kB

如果当SecondInterceptor的preHandle方法返回false时,程序会怎样执行呢?
我们令SecondInterceptor的preHandle方法返回false,运行程序:

image_1b1gfcuvh2rbocd1gh7u06qvt26.png-30.6kB

即SecondInterceptor的preHandle方法返回false后,仅执行了FirstInterceptor的afterCompletion()方法,查看Spring的源码,我了解到了其工作流程是这样的:

1、当页面发送请求后,DispatcherServlet会通过其中的HandlerExecutionChain对象依次调用各拦截器的的preHandle()方法;
2、查看该HandlerExecutionChain对象,发现其中的interceptors属性记录了当前项目配置的所有拦截器:
image_1b1gg7ussuub1qfnukt9221ggo2j.png-8.6kB
其中除了我们自定义的两个拦截器以外,还有一个SpringMVC内置的ConversionServiceExposingInterceptor拦截器。此外,还有一个int型参数interceptorIndex用于记录当前时刻 preHandle()方法已经返回true的拦截器的最大下标,例如现在我们的SecondInterceptor拦截器的preHandle()方法返回了false,那么interceptorIndex参数便为1,代表拦截器ConversionServiceExposingInterceptor和FirstInterceptor的preHandle()方法返回了true:
image_1b1ggq9g91e2kgkjhie16ulhmt3d.png-9.8kB
3、当HandlerExecutionChain对象发现SecondInterceptor拦截器的preHandle()方法返回了false后,便执行triggerAfterCompletion()方法用于从下标值为interceptorIndex开始执行各拦截器的afterCompletion()方法,直到下标值减为-1为止:
image_1b1ggn7saenb1hiq1pn01s9u8km30.png-13kB

那么,我们就很好理解了为什么当SecondInterceptor拦截器的preHandle()方法返回了false后,程序运行结果为上述截图的那样了,运行流程如下:
image_1b1ggvjvfse5ce1hhqn8a1ict3q.png-154.2kB

SpringMVC的文件上传功能

发表于 2016-11-14   |   分类于 SpringMVC   |  

  spring MVC为文件上传提供了直接的支持,这种支持是通过即插即用的MultipartResolver(接口)实现的。Spring 用Jakarta Commons FileUpload技术实现了一个MultipartResolver实现类:CommonsMultipartResovler。
  Spring MVC 上下文中默认没有装配MultipartResovler,因此默认情况下不能处理文件的上传工作,如果想使用Spring 的文件上传功能,需先在上下文中配置MultipartResolver。
  下面展示如何配置CommonsMultipartResovler,以及如何使用上传文件功能:
  
  
导入jar包:

image_1b1g674tqj56o151iba18fu137r9.png-4.4kB
配置CommonsMultipartResovler:

1
2
3
4
5
6
7
8
<bean id="multipartResolver"
class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
<!-- defaultEncoding: 必须和用户JSP 的pageEncoding属性一致,以便正确解析表单的内容 -->
<property name="defaultEncoding" value="UTF-8"></property>
<!-- 指定上传的文件最大不能超过1000KB -->
<property name="maxInMemorySize" value="1024000"></property>
<!-- 还可以指定其他属性... -->
</bean>

上传示例:


index.jsp

1
2
3
4
5
<form action="testUpload" method="post" enctype="multipart/form-data">
File:<input type="file" name="file" />
Description:<input type="text" name="des" />
<input type="submit" value="Upload" />
</form>


controller

1
2
3
4
5
6
7
8
9
@RequestMapping("/testUpload")
public String testUpload(@RequestParam("file") MultipartFile file, @RequestParam("des") String des)
throws IllegalStateException, IOException {
if (!file.isEmpty()) {
System.out.println("des : " + des);
file.transferTo(new File("G:/UploadTest/" + file.getOriginalFilename()));
}
return SUCCESS;
}


运行程序:

image_1b1g6gesri3q1dsp50svkapq2m.png-11.4kB

在index.jsp选择一个文本文件test.txt,文本中输入MY TEXT,点击Upload:

控制台输出:
image_1b1g6j1e71b9tiaepd1fs0142c13.png-2.7kB

且 G:/UploadTest/ 目录下获取到了该文件:

image_1b1g6k63212tci7e145h19gu1f5h1g.png-10.3kB

关于SpringMVC中的国际化

发表于 2016-11-13   |   分类于 SpringMVC   |  

国际化概述

  为了让Web项目支持国际化,需要识别每个用户的首选区域,并根据这个区域显示内容。在spring MVC应用程序中,用户的区域是通过区域解析器来识别的,它必须实现LocaleResolver接口。
  Spring MVC提供了几个LocaleResolver的实现,我们可以根据不同的需求来解析区域。如果SpringMVC提供的解析器不能满足需求,我们可以实现LocaleResolver接口,创建自定义的区域解析器。
  要定义一个区域解析器,只需在web应用程序上下文中注册一个LocaleResolver类型的Bean就可以了。必须将区域解析器的Bean名称设置为localeResolver,这样DispatcherServlet才能自动侦测到它。注意,每DispatcherServlet只能注册一个区域解析器。
  
  Spring采用的默认区域解析器是AcceptHeaderLocaleResolver。它通过检验HTTP请求头的accept-language属性来解析区域。   

默认区域解析器AcceptHeaderLocaleResolver的示例

  下面通过两个例子展示默认的区域解析器AcceptHeaderLocaleResolver的效果:
  
  示例一:在页面上能够根据浏览器语言设置的情况对文本(不是内容), 时间, 数值进行本地化处理。
  
  要实现上述功能,使用 JSTL 的 fmt 标签即可。(假设请求不经过控制器,直接跳转到jsp页面)


创建国际化资源文件

i18n_zh_CN.properties:

1
2
i18n.username=\u7528\u6237\u540D
i18n.password=\u5BC6\u7801

注:\u7528\u6237\u540D为“用户名”,\u5BC6\u7801为“密码”

i18n_en_US.properties:

1
2
i18n.username=username
i18n.password=password


配置国际化资源文件

spring配置文件:

1
2
3
4
5
6
7
8
9
<!-- 配置国际化资源文件 -->
<bean id="messageSource"
class="org.springframework.context.support.ResourceBundleMessageSource">
<property name="basename" value="i18n"></property>
</bean>
<!--同时配置jsp页面的直接跳转-->
<mvc:view-controller path="/i18n" view-name="i18n"/>
<mvc:view-controller path="/i18n2" view-name="i18n2" />


页面

i18n.jsp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Insert title here</title>
</head>
<body>
<fmt:message key="i18n.username"></fmt:message>
<br><br>
<a href="i18n2">I18N2 Page</a>
</body>
</html>

i18n2.jsp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Insert title here</title>
</head>
<body>
<fmt:message key="i18n.password"></fmt:message>
<br><br>
<a href="i18n">I18N Page</a>
</body>
</html>


运行结果:
  当设置浏览器语言为zh-cn时,i18n.jsp显示“用户名”,i18n2.jsp显示“密码”;
  当设置浏览器语言为en-us时,i18n.jsp显示“username”,i18n2.jsp显示“password”。


  示例二:在 bean 中获取国际化资源文件 对应于 Locale 的信息。
  
  要实现上述功能,在 bean 中注入 ResourceBundleMessageSource 的实例, 使用其对应的 getMessage 方法即可:(创建国际化资源文件、配置国际化资源文件同示例一,但是令/i18n映射到控制器,而非直接跳转)


页面

index.jsp:

1
<a href="i18n">I18N Page</a>


控制器:

1
2
3
4
5
6
7
8
9
@Autowired
private ResourceBundleMessageSource messageSource;
@RequestMapping("/i18n")
public String testI18N(Locale locale) {
String val = messageSource.getMessage("i18n.username", null, locale);
System.out.println(val);
return "i18n";
}


运行结果:
  当设置浏览器语言为zh-cn时,点击index.jsp中的超链接,控制台输出“用户名”;
  当设置浏览器语言为en-us时,点击index.jsp中的超链接,控制台输出“username”。

使用SessionLocaleResolver实现通过超链接切换 Locale

  示例三:现在我们希望可以通过超链接切换 Locale, 而不再依赖于浏览器的语言设置情况
  
  实现方法为配置 SessionLocalResolver 和 LocaleChangeInterceptor。(创建、配置国际化资源文件同上面的例子)


配置SessionLocaleResolver和LocaleChangeInterceptor

spring配置文件:

1
2
3
4
5
6
7
8
9
10
<mvc:interceptors>
<!-- 配置 LocaleChanceInterceptor 拦截器
作用: 获取name=locale的请求参数 -->
<bean class="org.springframework.web.servlet.i18n.LocaleChangeInterceptor"></bean>
</mvc:interceptors>
<!-- 配置 SessionLocalResolver
作用:将Locale对象设置为session的属性;从session中获取Locale对象 -->
<bean id="localeResolver"
class="org.springframework.web.servlet.i18n.SessionLocaleResolver"></bean>


页面:

1
2
3
<a href="i18n?locale=zh_CN">Chinese</a>
<br><br>
<a href="i18n?locale=en_US">English</a>


控制器: (同示例二)

1
2
3
4
5
6
7
8
9
@Autowired
private ResourceBundleMessageSource messageSource;
@RequestMapping("/i18n")
public String testI18N(Locale locale) {
String val = messageSource.getMessage("i18n.username", null, locale);
System.out.println(val);
return "i18n";
}


运行结果:
  当点击页面的Chinese时,控制台输出“用户名”;
  当点击页面的English时,控制台输出“username”。   

SessionLocaleResolver&LocaleChangeInterceptor的工作原理

  通过查看源码,我了解到利用SessionLocaleResolver和LocaleChangeInterceptor实现示例三功能的工作流程为:

  1. 在目标方法执行之前,LocaleChangeInterceptor拦截器的preHandle方法会先被调用,该方法获取请求参数中的locale属性,如果locale不为空,则获取到当前的区域解析器(此时为SessionLocaleResolver),然后调用SessionLocaleResolver对象的setLocale方法,其中把 根据locale值生成的Locale对象 作为参数传递到该方法中。
  2. 在SessionLocaleResolver对象的setLocale方法中,会将Locale对象设置到Session中。
  3. DispatchServlet通过SessionLocaleResolver区域解析器在session中获取到Locale对象,将其传入目标方法,然后即可在目标方法中使用Locale对象。   

使用HttpMessageConverter处理JSON

发表于 2016-11-13   |   分类于 SpringMVC   |  

HttpMessageConverter<T>工作原理:

  HttpMessageConverter<T> 是 Spring3.0 新添加的一个接口,负责将请求信息转换为一个对象(类型为 T)并绑定到处理方法的入参中,或将对象(类型为T)输出为响应信息。对此SpringMVC提供了两种实现途径:

  1. 使用@RequestBody/@ResponseBody注解对处理方法进行标注。
  2. 使用HttpEntity<T>/ResponseEntity<T>作为处理方法的入参或返回值。

  当控制器处理方法使用到@RequestBody/@ResponseBody 或者 HttpEntity<T>/ResponseEntity<T>时,Spring首先根据请求头或响应头的Accept属性选择匹配的HttpMessageConverter,进而根据参数类型或泛型类型的过滤得到匹配的HttpMessageConverter,若找不到可用的HttpMessageConverter将会报错。
  
  下图展示了HttpMessageConverter<T>的工作原理:
image_1b1e5tcdhui21hgpn6eqo3ba01t.png-204.3kB

  DispatcherServlet默认装配了RequestMappingHandlerAdapter,我们查看Spring源码发现RequestMappingHandlerAdapter默认装配了如下的HttpMessageConverter:
  image_1b1eccehf393hvg135teo0105o3u.png-210.6kB

HttpMessageConverter<T>使用示例

  在开始介绍HttpMessageConverter<T>的用法之前,需要先导入jar包:
  
image_1b1e4vo451r1j1pc1qmm1sc5127n9.png-3.9kB

  在导入以上jar包后,RequestMappingHandlerAdapter会自动额外装配一个HttpMessageConverter对象,用于在Java对象和JSON对象之间进行转换:
  image_1b1ecs9khgn721bln28qrao04b.png-248.1kB
  
  下面的三个示例分别展示了@ResponseBody、@RequestBody和ResponseEntity<T>的使用方法,HttpEntity<T>用法类似。
  
示例一:通过@RequestBody注解将目标方法返回的Java对象转为HttpOutputMessage,并在页面中通过AJAX获取:

jsp:

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
&lt;%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%&gt;
&lt;!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"&gt;
&lt;html&gt;
&lt;head&gt;
&lt;meta http-equiv="Content-Type" content="text/html; charset=UTF-8"&gt;
&lt;title&gt;index jsp&lt;/title&gt;
&lt;script type="text/javascript"
src="${pageContext.request.contextPath }/scripts/jquery-1.9.1.min.js"&gt;&lt;/script&gt;
&lt;script type="text/javascript"&gt;
$(function() {
$("#testJson").click(function() {
var url = $(this).attr("href");
var arg = {
"time" : new Date()
};
$.get(url, arg, function(data) {
for (var i = 0; i &lt; data.length; i++)
alert(data[i].id + ":" + data[i].lastName);
});
return false;
});
})
&lt;/script&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href="testJson" id="testJson"&gt;Test Json&lt;/a&gt;
&lt;/body&gt;
&lt;/html&gt;

controller:

1
2
3
4
5
@ResponseBody
@RequestMapping("/testJson")
public Collection&lt;Employee&gt; testJson(){
return employeeDao.getAll();
}

运行后浏览器会依次弹出“data[i].id + “:” + data[i].lastName”的提示信息:
image_1b1e5htql1qbvqkm45h1br01pi4m.png-8.8kB
image_1b1e5iuc215auou3m7164dubo13.png-9.8kB
image_1b1e5j6681i2i1jtk1t71fi010mv1g.png-10.2kB

          …
          
示例二:通过@RequestBody注解实现实现文件上传的功能:

jsp:

1
2
3
4
5
&lt;form action="testRequestBody" method="post" enctype="multipart/form-data"&gt;
File:&lt;input type="file" name="file" /&gt;
Description:&lt;input type="text" name="des" /&gt;
&lt;input type="submit" value="Submit" /&gt;
&lt;/form&gt;

controller:

1
2
3
4
5
6
7
8
@ResponseBody
@RequestMapping("/testRequestBody")
public String testRequestBody(@RequestBody String body){
System.out.println(body);
//return "helloworld!"+new Date()保证了方法执行完后不会匹配任何一个页面
return "helloworld!"+new Date();
}

运行程序,我们上传一个test.txt文件,其中存储了某一jsp页面的信息,并在Description中输入文本值“MY TEXT”:

image_1b1eatgr614ev90sl1a1bvqlo134.png-4.4kB

提交之后,HttpMessageConverter会把上传的文件和文本值转换为一个String类型的对象,并在控制台输出:

image_1b1ea9rkce0gv63gt42mbdum2a.png-51.8kB

而且浏览器上也显示了helloworld!+时间:

image_1b1eab44h162210eaalel771c802n.png-11.8kB

注意,如果在spring的配置文件中注册了CommonsMultipartResolver类型的bean实例,如下:

1
2
3
&lt;bean id="multipartResolver"
class="org.springframework.web.multipart.commons.CommonsMultipartResolver"&gt;
&lt;/bean&gt;

那么@RequestBody就不能正常将上传的文件类型转换为String类型在控制台输出了,原因暂时我还不太清楚。

示例三:通过@ResponseEntity<T>实现文件下载的功能:

jsp:

1
&lt;a href="testResponseEntity"&gt;Test ResponseEntity&lt;/a&gt;

在根目录下的file目录下保存了示例二中的test.txt:

image_1b1ec0e011pq61o0v1nipd41j6f3h.png-3.2kB

controller:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@ResponseBody
@RequestMapping("/testResponseEntity")
public ResponseEntity&lt;byte[]&gt; testResponseEntity(HttpSession session) throws IOException{
byte[] body = null;
ServletContext context = session.getServletContext();
InputStream in = context.getResourceAsStream("/file/test.txt");
body = new byte[in.available()];
in.read(body);
HttpHeaders headers = new HttpHeaders();
headers.add("Content-Disposition", "attachment;filename=test.txt");
HttpStatus statusCode = HttpStatus.OK;
ResponseEntity&lt;byte[]&gt; entity = new ResponseEntity&lt;byte[]&gt;(body, headers, statusCode);
return entity;
}

运行程序,点击超链接Test ResponseEntity后发现可以正确下载test.txt文件。

使用SpringMVC实现REST风格的CRUD功能

发表于 2016-11-13   |   分类于 SpringMVC   |  

  这篇文章介绍如何使用SpringMVC实现简单的、REST风格的员工信息的增删改查操作。项目源码附在我的github中。   

需求

① 查看所有员工信息:(url:emps,method:GET)

image_1b1dlbuog1jhq1u2dfi010uf153v9.png-49.9kB

② 添加员工信息,添加后重定向到显示员工信息页面:(url:emp,method:POST)

image_1b1dlh5bik4316kl11kq75jqvi13.png-16.4kB

image_1b1dlfl0q1i2l18751ts2m4c1nldm.png-16.1kB

③ 更新员工信息。LastName字段不可修改,要求能够回显表单,更新后重定向到显示员工信息页面:(url:emp/{id},method:PUT)

④ 删除员工信息,删除后重定向到显示员工信息页面。(url:emp/{id},method:DELETE)

实现

Employee.java:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Employee {
private Integer id;
@NotEmpty
private String lastName;
@Email
private String email;
// 1 male, 0 female
private Integer gender;
private Department department;
//方法略
}

Department.java:

1
2
3
4
5
6
7
public class Department {
private Integer id;
private String departmentName;
//方法略
}

EmployeeDao.java: (其中关于数据库的操作仅采用模拟方式实现)

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
@Repository
public class EmployeeDao {
private static Map<Integer, Employee> employees = null;
@Autowired
private DepartmentDao departmentDao;
static{
employees = new HashMap<Integer, Employee>();
try {
employees.put(1001, new Employee(1001, "E-AA", "aa@163.com", 1, new Department(101, "D-AA"),new SimpleDateFormat("yyyy-MM-dd").parse("1994-03-21")));
employees.put(1002, new Employee(1002, "E-BB", "bb@163.com", 1, new Department(102, "D-BB"),new SimpleDateFormat("yyyy-MM-dd").parse("1994-03-21")));
employees.put(1003, new Employee(1003, "E-CC", "cc@163.com", 0, new Department(103, "D-CC"),new SimpleDateFormat("yyyy-MM-dd").parse("1994-03-21")));
employees.put(1004, new Employee(1004, "E-DD", "dd@163.com", 0, new Department(104, "D-DD"),new SimpleDateFormat("yyyy-MM-dd").parse("1994-03-21")));
employees.put(1005, new Employee(1005, "E-EE", "ee@163.com", 1, new Department(105, "D-EE"),new SimpleDateFormat("yyyy-MM-dd").parse("1994-03-21")));
} catch (ParseException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
private static Integer initId = 1006;
public void save(Employee employee){
if(employee.getId() == null){
employee.setId(initId++);
}
employee.setDepartment(departmentDao.getDepartment(employee.getDepartment().getId()));
employees.put(employee.getId(), employee);
}
public Collection<Employee> getAll(){
return employees.values();
}
public Employee get(Integer id){
return employees.get(id);
}
public void delete(Integer id){
employees.remove(id);
}
}

DepartmentDao.java: (其中关于数据库的操作仅采用模拟方式实现)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Repository
public class DepartmentDao {
private static Map<Integer, Department> departments = null;
static{
departments = new HashMap<Integer, Department>();
departments.put(101, new Department(101, "D-AA"));
departments.put(102, new Department(102, "D-BB"));
departments.put(103, new Department(103, "D-CC"));
departments.put(104, new Department(104, "D-DD"));
departments.put(105, new Department(105, "D-EE"));
}
public Collection<Department> getDepartments(){
return departments.values();
}
public Department getDepartment(Integer id){
return departments.get(id);
}
}

控制器EmployeeHandler.java:

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
@Controller
public class EmployeeHandler {
@Autowired
private EmployeeDao employeeDao;
@Autowired
private DepartmentDao departmenDao;
@ModelAttribute
public void getEmployee(@RequestParam(value = "id", required = false) Integer id, Map<String, Object> map) {
if (id != null)
map.put("employee", employeeDao.get(id));
}
@RequestMapping(value = "/emp/{id}", method = RequestMethod.DELETE)
public String delete(@PathVariable("id") Integer id) {
employeeDao.delete(id);
return "redirect:/emps";
}
@RequestMapping(value = "/emp", method = RequestMethod.PUT)
public String update(Employee employee) {
employeeDao.save(employee);
return "redirect:/emps";
}
@RequestMapping(value = "/emp", method = RequestMethod.POST)
public String save(@Valid Employee employee, BindingResult result, Map<String, Object> map) {
if (result.getErrorCount() > 0) {
System.out.println("出错了!");
for (FieldError error : result.getFieldErrors()) {
System.out.println(error.getField() + ":" + error.getDefaultMessage());
}
map.put("departments", departmenDao.getDepartments());
return "input";
}
employeeDao.save(employee);
return "redirect:/emps";
}
@RequestMapping(value = "/emp", method = RequestMethod.GET)
public String input(Map<String, Object> map) {
map.put("departments", departmenDao.getDepartments());
/*
* 在input.jsp中使用了SpringMVC的form标签,SpringMVC认为 表单是一定要进行回显的,即便是“第一次来”,
* 所以要new一个 Employee对象放入request域 。
*/
map.put("employee", new Employee());
return "input";
}
@RequestMapping(value = "/emp/{id}", method = RequestMethod.GET)
public String input(@PathVariable("id") Integer id, Map<String, Object> map) {
map.put("departments", departmenDao.getDepartments());
/*
* 在input.jsp中使用了SpringMVC的form标签,SpringMVC认为
* 表单是一定要进行回显的,即便是“第一次来”,所以要获取一个 Employee对象放入request域 。
*/
map.put("employee", employeeDao.get(id));
return "input";
}
@RequestMapping("/emps")
public String list(Map<String, Object> map) {
map.put("employees", employeeDao.getAll());
return "list";
}
}

index.jsp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>index jsp</title>
</head>
<body>
<a href="${pageContext.request.contextPath }/emps">List All
Employees</a>
<br><br>
</body>
</html>

WEB-INF/views/input.jsp:

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
<%@page import="java.util.HashMap"%>
<%@page import="java.util.Map"%>
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%@taglib prefix="form" uri="http://www.springframework.org/tags/form"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Insert title here</title>
</head>
<body>
<form:form action="${pageContext.request.contextPath }/emp"
method="POST" modelAttribute="employee">
<c:if test="${employee.id == null }">
LastName:<form:input path="lastName" />
&nbsp;<form:errors path="lastName"></form:errors>
<br>
<br>
</c:if>
<c:if test="${employee.id != null }">
<form:hidden path="id" />
<input type="hidden" name="_method" value="PUT">
<%-- 对于 _method 不能使用 form:hidden 标签, 因为 modelAttribute 对应的 bean 中没有 _method 这个属性 --%>
<%--
<form:hidden path="_method" value="PUT"/>
--%>
</c:if>
Email:<form:input path="email" />
&nbsp;<form:errors path="email"></form:errors>
<br>
<br>
<%
Map<String, String> genders = new HashMap<String, String>();
genders.put("0", "Female");
genders.put("1", "Male");
request.setAttribute("genders", genders);
%>
Gender:<form:radiobuttons path="gender" items="${genders }" />
<br>
<br>
Birth:<form:input path="birth" />
&nbsp;<form:errors path="birth"></form:errors>
<br>
<br>
Department:<form:select path="department.id" items="${departments }"
itemLabel="departmentName" itemValue="id"></form:select>
<input type="submit" value="Submit" />
</form:form>
</body>
</html>

WEB-INF/views/list.jsp

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
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Insert title here</title>
<script type="text/javascript"
src="${pageContext.request.contextPath }/scripts/jquery-1.9.1.min.js"></script>
<script type="text/javascript">
$(function() {
$(".delete").click(function() {
var href = $(this).attr("href");
$("form").attr("action", href).submit();
return false;
});
})
</script>
</head>
<body>
<form action="" method="POST">
<input type="hidden" name="_method" value="DELETE">
</form>
<c:if test="${empty requestScope.employees }">
no employees information.
</c:if>
<c:if test="${!empty requestScope.employees }">
<table border="1" cellpadding="10" cellspacing="0">
<tr>
<th>ID</th>
<th>LastName</th>
<th>Email</th>
<th>Gender</th>
<th>Birth</th>
<th>Department</th>
<th>Edit</th>
<th>Delete</th>
</tr>
<c:forEach items="${requestScope.employees }" var="emp">
<tr>
<td>${emp.id }</td>
<td>${emp.lastName }</td>
<td>${emp.email }</td>
<td>${emp.gender == 0 ? 'Female' : 'male' }</td>
<td>${emp.birth }</td>
<td>${emp.department.departmentName }</td>
<td><a
href="${pageContext.request.contextPath }/emp/${emp.id }">Edit</a></td>
<td><a class="delete"
href="${pageContext.request.contextPath }/emp/${emp.id }">Delete</a></td>
</tr>
</c:forEach>
</table>
</c:if>
<a href="${pageContext.request.contextPath }/emp">Add New Employee</a>
</body>
</html>

web.xml:

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
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
id="WebApp_ID" version="3.1">
<!-- 配置 org.springframework.web.filter.HiddenHttpMethodFilter: 可以把 POST
请求转为 DELETE 或 PUT 请求 -->
<filter>
<filter-name>HiddenHttpMethodFilter</filter-name>
<filter-class>org.springframework.web.filter.HiddenHttpMethodFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>HiddenHttpMethodFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!-- 配置 DispatcherServlet -->
<servlet>
<servlet-name>springDispatcherServlet</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:springmvc.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<!-- Map all requests to the DispatcherServlet for handling -->
<servlet-mapping>
<servlet-name>springDispatcherServlet</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
</web-app>

springmvc.xml:

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
<?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.0.xsd
http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-4.0.xsd">
<!-- 配置要自动扫描的包 -->
<context:component-scan base-package="com.MySpringMVC"></context:component-scan>
<!-- 配置视图解析器: 如何把 handler 方法返回值解析为实际的物理视图 -->
<bean
class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/views/"></property>
<property name="suffix" value=".jsp"></property>
</bean>
<!-- default-servlet-handler 将在 SpringMVC 上下文中定义一个 DefaultServletHttpRequestHandler,
它会对进入 DispatcherServlet 的请求进行筛查, 如果发现是没有经过映射的请求, 就将该请求交由 WEB 应用服务器默认的 Servlet
处理. 如果不是静态资源的请求,才由 DispatcherServlet 继续处理 一般 WEB 应用服务器默认的 Servlet 的名称都是 default.
若所使用的 WEB 服务器的默认 Servlet 名称不是 default,则需要通过 default-servlet-name 属性显式指定 -->
<mvc:default-servlet-handler />
<!-- 解决 配置了 mvc:default-servlet-handler 或者 mvc:view-controller 后,控制器不能正常映射请求
的问题 还会会默认装配好一个LocalValidatorFactoryBean,用于数据校验 -->
<mvc:annotation-driven></mvc:annotation-driven>
</beans>

lib目录:
image_1b1dmlo7b1cnc1f3sn7fv4cb171g.png-60.7kB

核心知识点

1、SpringMVC的form标签:

  1.1 通过SpringMVC的form标签可以实现将模型数据中的属性和HTML 表单元素相绑定,以实现表单数据更便捷编辑和表单值的回显。
  1.2 可以通过SpringMVC的form标签的modelAttribute属性指定绑定的模型属性以进行表单回显。
  1.3 SpringMVC的form标签默认一定要回显表单,若没有指定modelAttribute属性,则默认从request 域对象中读取command的表单bean。如果在 指定的modelAttribute 或者 command 中无法获取到某一属性,则会发生错误。
  
2、处理静态资源:

  2.1 优雅的REST 风格的资源URL 不希望带.html 或.do 等后缀,若将DispatcherServlet请求映射配置为/,则Spring MVC 将捕获WEB 容器的所有请求,包括静态资源的请求(例如使用jQuery),SpringMVC会将他们当成一个普通请求处理,因找不到对应处理器将导致错误。
  
  2.2 上述问题的解决方法为:在SpringMVC的配置文件中添加配置:

1
<mvc:default-servlet-handler/>

其原理是:default-servlet-handler 将在 SpringMVC 上下文中定义一个 DefaultServletHttpRequestHandler,它会对进入 DispatcherServlet 的请求进行筛查,如果发现是没有经过映射的请求, 就将该请求交由 WEB 应用服务器默认的 Servlet 处理。如果不是静态资源的请求,才由 DispatcherServlet 继续处理。一般情况下,WEB 应用服务器默认的 Servlet 的名称都是 default。若所使用的 WEB 服务器的默认 Servlet 名称不是 default,则需要通过 default-servlet-name 属性显式指定。

3.数据转换&数据格式化&数据校验

  3.1 数据绑定流程:
  ① Spring MVC主框架将ServletRequest对象及目标方法的入参实例传递给WebDataBinderFactory实例,以创建DataBinder实例对象;
  ② DataBinder调用装配在Spring MVC上下文中的ConversionService组件进行数据类型转换、数据格式化工作。将Servlet 中的请求信息填充到入参对象中;
  ③ 调用Validator组件对已经绑定了请求消息的入参对象进行数据合法性校验,并最终生成数据绑定结果BindingData对象;
  ④ Spring MVC抽取BindingResult中的入参对象和校验错误对象,将它们赋给处理方法的响应入参
  Spring MVC通过反射机制对目标处理方法进行解析,将请求消息绑定到处理方法的入参中。数据绑定的核心部件是DataBinder,运行机制如下:
  
  image_1b1doigq71hd01kg01e6cqbd150q2j.png-77.5kB
  
  实现源码:
  image_1b1dohstp1v2tjt8rstfue1n9o26.png-236.9kB
  
  3.2 数据转换
  Spring MVC 上下文中内建了很多转换器,可完成大多数Java 类型的转换工作,如下图:
  image_1b1dp9ap5105bn081nmlna5bnj30.png-215.9kB
  
  如果内建的转换器不能满足要求,可以自定义转换器。方法是利用 ConversionServiceFactoryBean 在Spring 的IOC 容器中定义一个 ConversionService,并通过ConversionServiceFactoryBean的 converters 属性注册自定义的类型转换器。Spring 将自动识别出IOC 容器中的ConversionService,并在Bean 属性配置及Spring MVC 处理方法入参绑定等场合使用它进行数据的转换。例如现在要实现一个将String转为Employee对象的转换器:(这里的Employee没有添加birth属性)
  
springmvc.xml:

1
2
3
4
5
6
7
8
9
10
11
12
<!-- 将自定义的ConversionService注册到Spring MVC 的上下文中 -->
<mvc:annotation-driven conversion-service="conversionService"></mvc:annotation-driven>
<!-- 配置 ConversionService -->
<bean id="conversionService"
class="org.springframework.format.support.FormattingConversionServiceFactoryBean">
<property name="converters">
<set>
<ref bean="employeeConverter"/>
</set>
</property>
</bean>

jsp:

1
2
3
4
5
<form action="testConversionServiceConverer" method="POST" >
<!-- lastname-email-gender-department.id 例如: GG-gg@atguigu.com-0-105 -->
Employee: <input type="text" name="employee"/>
<input type="submit" value="Submit"/>
</form>

controller:

1
2
3
4
5
6
@RequestMapping("/testConversionServiceConverer")
public String testConverter(@RequestParam("employee") Employee employee){
System.out.println("save: " + employee);
employeeDao.save(employee);
return "redirect:/emps";
}

Converter:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Component
public class EmployeeConverter implements Converter<String, Employee> {
@Override
public Employee convert(String source) {
if(source != null){
String [] vals = source.split("-");
//GG-gg@atguigu.com-0-105
if(vals != null && vals.length == 4){
String lastName = vals[0];
String email = vals[1];
Integer gender = Integer.parseInt(vals[2]);
Department department = new Department();
department.setId(Integer.parseInt(vals[3]));
Employee employee = new Employee(null, lastName, email, gender, department);
System.out.println(source + "--convert--" + employee);
return employee;
}
}
return null;
}
}

  3.3 数据格式化
  对属性对象的输入/输出进行格式化,从其本质上讲依然属于“类型转换”的范畴。Spring 在格式化模块中定义了一个实现ConversionService接口的FormattingConversionService实现类,该实现类扩展了GenericConversionService,因此它既具有类型转换的功能,又具有格式化的功能。FormattingConversionService拥有一个FormattingConversionServiceFactroyBean工厂类,后者用于在Spring 上下文中构造前者。
  FormattingConversionServiceFactroyBean内部已经注册了:

  • NumberFormatAnnotationFormatterFactroy:支持对数字类型的属性使用@NumberFormat注解

  • JodaDateTimeFormatAnnotationFormatterFactroy:支持对日期类型的属性使用@DateTimeFormat注解

  装配了FormattingConversionServiceFactroyBean后,就可以在Spring MVC 入参绑定及模型数据输出时使用注解驱动了。mvc:annotation-driven标签 默认创建的ConversionService 实例即为 FormattingConversionServiceFactroyBean。
  例如,我们在Employee.java中的birth属性上添加了注解:@DateTimeFormat(pattern=”yyyy-MM-dd”),即指定了日期输入的格式。此外,还可通过@NumberFormat注解对类似数字类型的属性进行格式化标注,例如

1
2
  @NumberFormat(pattern="#,###,###.#")
private Float salary;

  3.4 数据校验
  JSR 303是Java 为Bean 数据合法性校验提供的标准框架,它已经包含在JavaEE 6.0中。JSR 303通过在Bean 属性上标注类似于@NotNull、@Max等标准的注解指定校验规则,并通过标准的验证接口对Bean 进行验证。详细注解如下图:
  image_1b1ds94ej1m5d1c9h2oo118g13vc3t.png-208.7kB
  
  hibernate Validator是JSR 303 的一个参考实现,除支持所有标准的校验注解外,它还支持以下的扩展注解:
  image_1b1dsabituub1vpsv23grg1f2f4a.png-159.3kB
  
Spring MVC 数据校验:

  Spring 4.0拥有自己独立的数据校验框架,同时支持JSR 303标准的校验框架。
  Spring 在进行数据绑定时,可同时调用校验框架完成数据校验工作。在Spring MVC 中,可直接通过注解驱动的方式进行数据校验。
  Spring 的LocalValidatorFactroyBean既实现了Spring 的Validator 接口,也实现了JSR 303 的Validator接口。只要在Spring 容器中定义了一个LocalValidatorFactoryBean,即可将其注入到需要数据校验的Bean 中。mvc:annotation-driven标签 会默认装配好一个LocalValidatorFactoryBean,通过在处理方法的入参上标注@valid注解即可让Spring MVC 在完成数据绑定后执行数据校验的工作。
  Spring 本身并没有提供JSR303 的实现,所以必须将JSR303 的实现者的jar 包放到类路径下:
  image_1b1dsi76s5j875d19k014n2nao4n.png-10.6kB
  
  Spring MVC 是通过对处理方法签名的规约来保存校验结果的:前一个表单/命令对象的校验结果保存到随后的入参中,这个保存校验结果的入参必须是BindingResult或Errors类型,这两个类都位于org.springframework.validation包中。需校验的Bean 对象和其绑定结果对象或错误对象时成对出现的,它们之间不允许声明其他的入参。
  例如,在上述的EmployeeHandler类的save方法中即声明了需要对employee入参进行校验,并将校验结果存入result中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@RequestMapping(value = "/emp", method = RequestMethod.POST)
public String save(@Valid Employee employee, BindingResult result, Map<String, Object> map) {
if (result.getErrorCount() > 0) {
System.out.println("出错了!");
for (FieldError error : result.getFieldErrors()) {
System.out.println(error.getField() + ":" + error.getDefaultMessage());
}
map.put("departments", departmenDao.getDepartments());
return "input";
}
employeeDao.save(employee);
return "redirect:/emps";
}

其中对employee的哪些字段进行何种校验,Employee.java中已经标注了,例如

1
2
@NotEmpty
private String lastName;

标注了lastName字段不能为null。

在目标方法中获取校验结果:
  上面save方法的代码已经介绍了如何利用校验结果的信息在目标方法中进行打印。
  
在页面上显示错误:
  Spring MVC 除了会将表单/命令对象的校验结果保存到对应的BindingResult或Errors 对象中外,还会将所有校验结果保存到“隐含模型”,即implicitModel。隐含模型中的所有数据最终将通过HttpServletRequest的属性列表暴露给JSP 视图对象,因此在JSP 中可以获取错误信息。例如,如果上述的lastName字段为null,则可以在跳转的页面input.jsp中通过如下代码获取错误信息。

1
  <form:errors path="lastName"></form:errors>

提示消息的国际化:
   每个属性在数据绑定和数据校验发生错误时,都会生成一个对应的FieldError对象。当一个属性校验失败后,校验框架会为该属性生成4 个消息代码,这些代码以校验注解类名为前缀,结合modleAttribute、属性名及属性类型名生成多个对应的消息代码:例如User 类中的password 属性标准了一个@Pattern 注解,当该属性值不满足@Pattern 所定义的规则时, 就会产生以下4 个错误代码:

  • Pattern.user.password
  • Pattern.password
  • Pattern.java.lang.String
  • Pattern

  当使用Spring MVC 标签显示错误消息时,Spring MVC 会查看WEB 上下文是否装配了对应的国际化消息,如果没有,则显示默认的错误消息,否则使用国际化消息。例如,我们要定制lastName字段的验证出错的错误消息:
  
创建国际化资源文件 i18n.properties:(其中“LastName\u4E0D\u80FD\u4E3A\u7A7A”是输入汉字“LastName不能为空”自动转换为ISO-8859-1编码的结果)

1
NotEmpty.employee.lastName=LastName\u4E0D\u80FD\u4E3A\u7A7A

在springmvc.xml中配置国际化资源文件

1
2
3
4
5
<!-- 配置国际化资源文件 -->
<bean id="messageSource"
class="org.springframework.context.support.ResourceBundleMessageSource">
<property name="basename" value="i18n"></property>
</bean>

那么如果lastName字段为null,错误信息将会是:LastName不能为空。
  
  若数据类型转换或数据格式转换时发生错误,或该有的参数不存在,或调用处理方法时发生错误,都会在隐含模型中创建错误消息。其错误代码前缀说明如下:

  • required:必要的参数不存在。如@RequiredParam(“param1”) 标注了一个入参,但是该参数不存在。
  • typeMismatch:在数据绑定时,发生数据类型不匹配的问题
  • methodInvocation:Spring MVC。 在调用处理方法时发生了错误–注册国际化资源文件。

4、关于mvc:annotation-driven
  mvc:annotation-driven 会自动注册RequestMappingHandlerMapping、RequestMappingHandlerAdapter与ExceptionHandlerExceptionResolver三个bean。还将提供以下支持:

  • 支持使用ConversionService实例对表单参数进行类型转换(不加的话就没有ConversionService)
  • 支持使用@NumberFormat、@DateTimeFormat注解完成数据类型的格式化
  • 支持使用@Valid注解对JavaBean 实例进行JSR 303验证
  • 支持使用@RequestBody和@ResponseBody注解

5、关于@InitBinder注解
  由@InitBinder标识的方法,可以对WebDataBinder对象进行初始化。WebDataBinder是DataBinder的子类,用于完成由表单字段到JavaBean 属性的绑定。
  @InitBinder方法不能有返回值,它必须声明为void。
  @InitBinder方法的参数通常是WebDataBinder。
  例如,如下代码声明了不将请求参数中的lastName属性绑定到WebDataBinder中:

1
2
3
4
5
//lastName不进行赋值,(不自动绑定对象中的lastName属性,另行处理) user的角色赋值的时候可能会用到
@InitBinder
public void initBinder(WebDataBinder binder){
binder.setDisallowedFields("lastName");
}

SpringMVC中的视图和视图解析器

发表于 2016-11-12   |   分类于 SpringMVC   |  

  对于控制器的目标方法,无论其返回值是String、View、ModelMap或是ModelAndView,SpringMVC都会在内部将它们封装为一个ModelAndView对象进行返回。
   Spring MVC 借助视图解析器(ViewResolver)得到最终的视图对象(View),最终的视图可以是JSP也可是Excell、
JFreeChart等各种表现形式的视图。

SpringMVC的视图解析流程

  SpringMVC的视图解析流程为:
  
1、调用目标方法,SpringMVC将目标方法返回的String、View、ModelMap或是ModelAndView都转换为一个ModelAndView对象;

2、然后通过视图解析器(ViewResolver)对ModelAndView对象中的View对象进行解析,将该逻辑视图View对象解析为一个物理视图View对象;

3、最后调用物理视图View对象的render()方法进行视图渲染,得到响应结果。

视图(View)

  视图的作用是渲染模型数据,将模型里的数据以某种形式呈现给客户。
  为了实现视图模型和具体实现技术的解耦,Spring在org.springframework.web.servlet包中定义了一个高度抽象的View接口。
  视图对象由视图解析器负责实例化。由于视图是无状态的,所以他们不会有线程安全的问题。所谓视图是无状态的,是指对于每一个请求,都会创建一个View对象。
  JSP是最常见的视图技术。
  
  常用的视图实现类:
  image_1b1ce221b11hn6kv3pv1jj4kcm9.png-351.2kB

视图解析器(ViewResolver)

  视图解析器的作用是将逻辑视图转为物理视图,所有的视图解析器都必须实现ViewResolver接口。
   SpringMVC为逻辑视图名的解析提供了不同的策略,可以在Spring WEB上下文中配置一种或多种解析策略,并指定他们之间的先后顺序。每一种映射策略对应一个具体的视图解析器实现类。程序员可以选择一种视图解析器或混用多种视图解析器。可以通过order属性指定解析器的优先顺序,order越小优先级越高,SpringMVC会按视图解析器顺序的优先顺序对逻辑视图名进行解析,直到解析成功并返回视图对象,否则抛出ServletException异常。
  
   常用的视图解析器实现类:
   image_1b1ce6cmvcq619dl1fv19i3fg29.png-264.4kB
  
   以下是通过InternalResourceViewResolver作为视图解析器解析,将逻辑视图解析为JSP视图的实例:

1
2
3
4
5
<bean
class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/views/"></property>
<property name="suffix" value=".jsp"></property>
</bean>

  若项目中使用了JSTL,则SpringMVC会自动把视图由InternalResourceView转为JstlView。   

使用mvc:view-controller不经控制器直接跳转到页面

  若希望直接响应通过SpringMVC渲染的页面,可以使用mvc:view-controller标签实现:

1
2
3
  <!-- 配置直接转发的页面 -->
<!-- 可以直接相应转发的页面, 而无需再经过 Handler 的方法. -->
<mvc:view-controller path="/success" view-name="success"/>

  那么现在可以直接在某一页面中通过请求路径”success”访问到/WEB-INF/views/success.jsp页面(因为我们上面配置了视图解析器将逻辑视图解析为前缀为/WEB-INF/views/,后缀为.jsp的物理视图)。但是,这种情况下通过控制器就无法映射到请求了,需要再进行如下配置:

1
2
<!-- 在实际开发中通常都需配置 mvc:annotation-driven 标签, 之前的页面才不会因为配置了直接转发页面而受到影响 -->
<mvc:annotation-driven></mvc:annotation-driven>

  

关于重定向

  一般情况下,控制器方法返回字符串类型的值会被当成逻辑视图名处理,但如果返回的字符串中带forward:或redirect:前缀时,SpringMVC会对它们进行特殊处理:将forward: 和redirect: 当成指示符,其后的字符串作为URL 来处理。示例如下:
  
index.jsp:

1
<a href="${pageContext.request.contextPath }/springmvc/testRedirect">Test Redirect</a>

controller:

1
2
3
4
5
6
7
8
9
10
@Controller
@RequestMapping("/springmvc")
public class SpringMVCTest {
@RequestMapping("/testRedirect")
public String testRedirect() {
System.out.println("testRedirect");
return "redirect:/index.jsp";
}
}

即可重定向到index.jsp。也可在redirect:/后添加控制器方法的映射路径,重定向到该目标方法。

  

在JSP中引用JS文件的三种方法

发表于 2016-11-11   |   分类于 Jquery   |  

在JSP中引用JS文件的三种方法:

1、如果是直接访问JSP,则使用相对于JSP页面的相对路径:

image_1b18hnotn96o1psa12es191r1ma69.png-7.1kB

当项目目录如图所示时,则在NewFile.jsp中访问jquery-1.9.1.min.js的方法为:

1
<script type="text/javascript" src="../scripts/jquery-1.9.1.min.js"></script>

2、如果是从action跳转至JSP,则使用相对于WEB工程的相对路径:

image_1b18hnotn96o1psa12es191r1ma69.png-7.1kB

如果是从某一个action跳转到NewFile.jsp,则在NewFile.jsp中访问jquery-1.9.1.min.js的方法为:

1
<script type="text/javascript" src="scripts/jquery-1.9.1.min.js"></script>

3、适用于以上两种情况的方法——使用WEB工程的绝对路径:

1
<script type="text/javascript" src="${pageContext.request.contextPath }/scripts/jquery-1.9.1.min.js"></script>

1…345…7
Alan

Alan

Better late than never.

61 日志
11 分类
74 标签
GitHub weibo
© 2016 Alan
由 Hexo 强力驱动
主题 - NexT.Pisces