6.3 数据格式化 2.使用FormatterRegistrar注册Formatter

注册Formatter的另一种方法是使用FormatterRegistrar

自定义注册器

实现FormatterRegistrar只需要实现一个方法,就是registerFormatters,在该方法中添加需要注册的Formatter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class MyFormatterRegistrar
implements FormatterRegistrar
{
private DateFormatter dateFormatter;
public void setDateFormatter(DateFormatter dateFormatter)
{
this.dateFormatter = dateFormatter;
}
@Override
public void registerFormatters(FormatterRegistry registry)
{
registry.addFormatter(dateFormatter);
}
}

配置文件中注册Registrar

配置文件中不需要再注册任何Formatter了,而是注册Registrar

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<!-- 装配自定义格式化 -->
<mvc:annotation-driven conversion-service="conversionService" />
<!-- 指定类全名,创建自定义格式化类 -->
<!-- 指定日期字符串表示格式 -->
<bean
id="dateFormatter"
class="org.fkit.formatter.DateFormatter"
c:_0="yyyy-MM-dd" />
<!-- 格式化 -->
<bean
id="conversionService"
class="org.springframework.format.support.FormattingConversionServiceFactoryBean">
<property name="formatterRegistrars">
<set>
<!-- 创建自定义注册器 -->
<bean
class="org.fkit.formatter.MyFormatterRegistrar"
p:dateFormatter-ref="dateFormatter" />
</set>
</property>
</bean>

6.3 数据格式化

Spring使用Converter转换器进行源类型对象到目标类型对象的转换,Spring的转换器并不承担输入以及输出信息格式化的工作。如果需要转换的源类型数据是从客户端界面中传过来的,比如日期、时间、数字、货币等数据,它们往往都拥有一定的格式。在不同的本地化环境中,同一类型的数据还会相应地呈现不同的显示格式

如何从格式化的数据中获取真正的数据以完成数据绑定,并将处理完成的数据输出为格式化的数据是Spring格式化框架需要解决的问题。Spring从3.0开始引入了格式化转换框架,这个框架位于org.springframework.format包中,其中最重要的是Formatter<T>接口

Converter和Formatter的区别

Converter完成任意ObjectObject之间的类型转换,
Formatter完成任意ObjectString之间的类型转换,即格式化和解析,
它和PropertyEditor功能类似,可以替代PropertyEditor来进行对象的解析和格式化,而且支持细粒度的字段级别的格式化解析。
Formatter只能将String转换成另一种Java类型。例如,将String转换成Date但它不能将Long转换成Date
因此Formatter更适用于Web层的数据转换。而Converter则可以用在任意层中
因此,在Spring MVC的应用程序当中,如果想转换表单中的用户输入,则建议选择Formatter,而不是Converter.

Spring MVC格式化接口

Formatter格式化转换是Spring通用的,定义在org.springframework.format包中,其不仅仅在Spring Web MVC场景下使用。在org.springframework.format包中定义的接口如下:

Printer接口

Printer<T>接口。格式化显示接口,其将T类型的对象根据 Locale信息以某种格式进行打印显示(即返回字符串形式)。该接口中定义了一个 print方法,其根据本地化信息将T类型的对象输出为不同格式的字符串

1
String print(T object,Locale locale)

Parser接口

Parser<T>接口。解析接口,其根据 Locale信息解析字符串到T类型的对象。该接口中定义了一个 parse方法,其参考本地化信息将一个格式化的字符串转换为T类型的对象

1
T parse(String text,Locale locale) throws ParseException

Formatter接口

Formatter<T>接口。格式化接口,继承自 Printer<T>Parser<T>接口,它完成T类型对象的格式化和解析功能。

FormatterRegistrar接口

FormatterRegistrar接口。注册格式化转换器。该接口中定义registerFormatters方法,其参数就是 FormatterRegistry对象,用于注册多个格式化转换器

1
void registerFormatters(FormatterRegistry registry)

AnnotationFormatterFactory接口

AnnotationFormatterFactory< A extends Annotation>接口。注解驱动的字段格式化工厂,用于创建带注解的对象字段的 PrinterParser,即用于格式化和解析带注解的对象字段。该接口中定义了以下几个方法:

  • Set<Class<?>> getFieldTypes()。注解A的应用范围,即哪些属性类可以标注A注解
  • Printer<?> getPrinter( a Annotation, Class<?> fieldType)。**根据注解A获取特定属性类型Printer**。
  • Parser<?> getParser( A annotation, Class<?> fieldType)。根据注解A获取特定属性类型的Parser

示例 使用Formatter格式化数据

使用自定义的格式化类

DateFormatter

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
// 实现Converter<S,T>接口
public class DateFormatter
implements Formatter<Date>
{
// 日期格式化对象
private SimpleDateFormat dateFormat;
// 构造器,通过依赖注入的日期类型创建日期格式化对象
public DateFormatter(String datePattern)
{
this.dateFormat = new SimpleDateFormat(datePattern);
}
// 显示Formatter<T>的T类型对象
@Override
public String print(Date date, Locale locale)
{
System.out.println("实现的print方法:Date转String");
return dateFormat.format(date);
}
// 解析文本字符串返回一个Formatter<T>的T类型对象。
@Override
public Date parse(String source, Locale locale) throws ParseException
{
try
{
System.out.println("实现的parse方法:String转Date");
return dateFormat.parse(source);
} catch (Exception e)
{
throw new IllegalArgumentException();
}
}
}

DateFormatter类实现了org. springframework. format. Formatter接口。实现了接口中的两个方法:

  • parse方法,使用指定的 Locale将一个 String解析成目标T类型;
  • print方法,用于返回T类型的字符串表示形式。

DateFormatter类中使用 SimpleDateFormat对象将 String转换成Date类型,日期类型模板yyy-M-dd会通过配置文件的依赖注入设置。

springmvc-config.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<!-- 装配自定义格式化 -->
<mvc:annotation-driven conversion-service="conversionService" />
<!-- 格式化 -->
<bean
id="conversionService"
class="org.springframework.format.support.FormattingConversionServiceFactoryBean">
<!-- formatters属性可以注册格式化类Fommatter -->
<!-- converters属性可以注册转换器Converter -->
<property name="formatters">
<list>
<!-- 使用自定义格式化类 -->
<!-- 指定格式化模板 -->
<bean
class="org.fkit.formatter.DateFormatter"
c:_0="yyyy-MM-dd" />
</list>
</property>
</bean>

Spring在格式化模块中定义了一个实现ConversionService接口的FormattingConversionService实现类,该类既具有类型转换功能,又具有格式化的功能
FormattingConversionServiceFactoryBean工厂类用于在 Spring上下文中构造一个FormattingConversionService对象,通过这个工厂类可以注册自定义的格式化转换器

以上配置使用FormattingConversionServiceFactoryBean对自定义的格式化转换器DateFormatter进行了注册。 FormattingConversionServiceFactoryBean类有一个属性converters,可以用它注册Converter;有一个属性formatters,可以用它注册Formatter
值得注意的是,在mvc:annotation-driven标签内部默认创建的conversionService实例就是一个FormattingConversionServiceFactoryBean,有了FormattingConversionServiceFactoryBean之后, Spring MVC对处理方法的参数就绑定格式化功能了。

User.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 域对象,实现序列化接口
public class User
implements Serializable
{
private static final long serialVersionUID = 1L;
private String loginname;
private Date birthday;
public User()
{
super();
// TODO Auto-generated constructor stub
}
// 此处省略getter和setter方法,请自己补上
@Override
public String toString()
{
return "User [loginname=" + loginname + ", birthday=" + birthday + "]";
}
}

UserController.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Controller
public class UserController {
@GetMapping(value = "/registerForm")
public String registerForm()
{
// 跳转到注册页面
return "registerForm";
}
@PostMapping(value = "/register")
public String register(@ModelAttribute
User user, Model model)
{
System.out.println(user);
model.addAttribute("user", user);
return "success";
}
}

registerForm.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
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>测试Formatter接口</title>
</head>
<body>
<h3>注册页面</h3>
<form action="register" method="post">
<table>
<tr>
<td><label>登录名: </label></td>
<td><input type="text" id="loginname"
name="loginname"></td>
</tr>
<tr>
<td><label>生日: </label></td>
<td><input type="text" id="birthday"
name="birthday"></td>
</tr>
<tr>
<td><input id="submit" type="submit" value="登录"></td>
</tr>
</table>
</form>
</body>
</html>

success.jsp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt"%>
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>测试Formatter</title>
</head>
<body>
登录名:${requestScope.user.loginname }
<br> 生日:
<fmt:formatDate value="${requestScope.user.birthday}"
pattern="yyyy年MM月dd日" />
<br>
</body>
</html>

测试

测试链接

1
<a href="registerForm">测试使用格式化类</a>

填写数据

这里有一张图片

转换效果

这里有一张图片

控制台输出

1
2
实现的parse方法:String转Date
User [loginname=xiaoming, birthday=Sat Feb 03 00:00:00 CST 2345]

使用Spring提供的格式化类

以上使用实现Formatter<T>接口的方式完成数据转换,而Spring本身提供了很多常用的Formatter实现。在org.springframework.format.datetime包中提供了一个用于时间对象格式化的DateFormatter实现类
org.springframework.format.number包中提供了3个用于数字对象格式化的实现类:

  • Numberformatter。用于数字类型对象的格式化
  • CurrencyFormatter。用于货币类型对象的格式化PercentFormatter。用于百分数数字类型对象的格式化

例如,如果要使用org.springframework.format.datetime包中提供的DateFormatter实现类完成字符串到日期对象的转换,则只需要在配置文件中配置就可以了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<bean
id="conversionService"
class="org.springframework.format.support.FormattingConversionServiceFactoryBean">
<!-- formatters属性可以注册格式化类Fommatter -->
<!-- converters属性可以注册转换器Converter -->
<property name="formatters">
<list>
<!-- 使用Spring提供的日期格式化类 -->
<!-- 指定日期格式为yyy-MM-dd -->
<bean
class="org.springframework.format.datetime.DateFormatter"
p:pattern="yyyy-MM-dd" />
</list>
</property>
</bean>

运行效果与上面的类似.

6.2.3 多种转换器的优先顺序

对于同一个类型的对象来说,如果既在ConversionService中装配了自定义的转换器,又通过WebBindingInitializer接口装配了全局的自定义编辑器,同时还在控制器中通过@initBinder装配了自定义的编辑器,此时 Spring MVC将按照以下的优先顺序查找对应的编辑器
(1)查询通过@initBinder装配的自定义编辑器
(2)查询通过ConversionService装配的自定义转换器。
(3)查询通过WebBindingInitializer接口装配的全局自定义编辑器。

6.2.2 Spring支持的转换器 示例 使用WebBindingInitializer注册全局自定义编辑器转换数据

如果希望在全局范围内使用自定义的编辑器,则可以通过实现WebBindingInitializer接口并在该实现类中注册自定义编辑器完成.

注册自定义编辑器

1
2
3
4
5
6
7
8
9
10
11
// 实现WebBindingInitializer接口
public class DateBindingInitializer
implements WebBindingInitializer
{
@Override
public void initBinder(WebDataBinder binder)
{
// 注册自定义编辑器
binder.registerCustomEditor(Date.class, new DateEditor());
}
}

DateBindingInitializer类实现WebBindingInitializer接口,并在 initBinder()方法中注册自定义编辑器DateEditorr类.

自定义编辑器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 自定义属性编辑器
public class DateEditor
extends PropertyEditorSupport
{
// 将传如的字符串数据转换成Date类型
@Override
public void setAsText(String text) throws IllegalArgumentException
{
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
try
{
Date date = dateFormat.parse(text);
setValue(date);
} catch (ParseException e)
{
e.printStackTrace();
}
}
}

在Spring配置文件中装配自定义编辑器

UserController类中不需要再使用@initBinder注解的方法来注册编辑器,而是在springmvc-config.xml配置文件中配置全局的自定义编辑器。

1
2
3
4
5
6
7
<!-- 通过RequestMappingHandlerAdapter装配自定义编辑器 -->
<bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter">
<property name="webBindingInitializer">
<!-- 指定自定义的编辑器所在的全路径名 -->
<bean class="org.fkjava.binding.DateBindingInitializer" />
</property>
</bean>

不能再使用默认装配方案

注意,这里使用了RequestMappingHandlerAdapter装配自定义编辑器,不能再使用默认配置方案<mvc:annotation-driven />

领域对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class User
implements Serializable
{
private static final long serialVersionUID = 1L;
private String loginname;
private Date birthday;
public User()
{
super();
// TODO Auto-generated constructor stub
}
// 此处省略getter和setter方法,请自己补上
@Override
public String toString()
{
return "User [loginname=" + loginname + ", birthday=" + birthday + "]";
}
}

测试连接

1
<a href="registerForm">registerForm</a>

registerForm请求处理方法

1
2
3
4
5
6
@GetMapping(value = "/registerForm")
public String registerForm()
{
// 跳转到注册页面
return "registerForm";
}

registerForm.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
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>测试WebBindingInitializer</title>
</head>
<body>
<h3>测试WebBindingInitializer</h3>
<form action="register" method="post">
<table>
<tr>
<td><label>登录名: </label></td>
<td><input type="text" id="loginname"
name="loginname"></td>
</tr>
<tr>
<td><label>生日: </label></td>
<td><input type="text" id="birthday"
name="birthday"></td>
</tr>
<tr>
<td><input id="submit" type="submit" value="登录"></td>
</tr>
</table>
</form>
</body>
</html>

register请求处理方法

1
2
3
4
5
6
7
8
@PostMapping(value = "/register")
public String register(@ModelAttribute
User user, Model model)
{
System.out.println(user);
model.addAttribute("user", user);
return "success";
}

测试

填写表单

这里有一张图片

显示效果

这里有一张图片

控制台输出

1
User [loginname=小明, birthday=Sat May 06 00:00:00 CST 1234]

自定义的字符串转Date转换器运行成功。

7.3 拦截器

Interceptor拦截器是SpringMVC中相当重要的功能,它的主要作用是拦截用户的请求并进行相应的处理。比如**通过拦截器来进行用户权限验证,或者用来判断用户是否已经登录**等。

Spring MVC拦截器可插拔

Spring MVC拦截器是可插拔式的设计。如果需要使用某个拦截器,只需要在配置文件中应用该拦截器即可;如果不需要使用该拦截器,只需要在配置文件中取消应用该拦截器。不管是否应用某个拦截器,对Spring MVC框架不会有任何影响。

7.3.1 HandlerInterceptor接口

Spring MVC中的Interceptor拦截器拦截请求是通过实现HandlerInterceptor接口来完成的。在Spring MVC中定义一个Interceptor拦截器非常简单,通常在要定义的Interceptor拦截器类中实现SpringHandlerInterceptor接口,或者继承抽象类HandlerInterceptorAdapter
HandlerInterceptor接口中定义了三个方法,SpringMVC就是通过这三个方法来对用户的请求进行拦截处理的。

preHandle方法

preHandle方法的方法声明如下

1
boolean preHandle(HttpServletRequest request,HttpServletResponse response,Object handle)

顾名思义,该方法将在请求处理之前被调用
SpringMVC中的Interceptor实行的是链式调用,即在一个应用中或者说在一个请求中可以同时存在多个Interceptor。每个Interceptor的调用会依据它的声明顺序依次执行,而且最先执行的是Interceptor中的preHandle方法,所以可以在这个方法中进行一些前置的初始化操作或者是对当前请求的一个预处理,也可以在这个方法中进行一些判断来决定请求是否要继续进行下去。该方法的返回值是boolean类型的,

  • 当返回值为false时,表示请求结束,后续的InterceptorController都不会再执行;
  • 当返回值为true时就会继续调用下一个InterceptorpreHandle方法;

如果已经是最后一个Interceptor,就会调用当前请求的Controller方法。

postHandle方法

1
2
void postHandle (HttpServletRequest request,HttpServletResponse response,
Object handler,ModelAndView mv)

该方法和之后的afterCompletion方法都只能在当前所属的InterceptorpreHandle方法的返回值为true时才能被调用。
postHandle方法,顾名思义,就是在当前请求被处理之后,也就是在Controller的请求处理方法被调用之后执行,但是它会DispatcherServlet进行视图返回渲染之前被调用,所以我们可以在这个方法中对Controller处理之后的ModelAndView对象进行操作。postHandle方法被调用的方向跟preHandle是相反的,也就是说先声明的InterceptorpostHandle方法反而会后执行,这和Struts2里面的Interceptor的执行过程类似。

afterCompletion方法

1
2
void afterCompletion(HttpServletRequest request,HttpServletResponsere sponse,
Object handler,Exception exception)

该方法也是在当前所属的InterceptorpreHandle方法的返回值为true时才会执行。

顾名思义,该方法将在整个请求结束之后,也就是在DispatcherServlet渲染了对应的视图之后执行。这个方法的主要作用是进行资源清理

示例 拦截器实现用户权限验证

目录结构

展开/折叠
G:\Desktop\随书源码\Spring+Mybatis企业应用实战(第2版)\codes\07\InterceptorTest
├─src\
│ └─org\
│   └─fkit\
│     ├─controller\
│     │ ├─BookController.java
│     │ ├─FormController.java
│     │ └─UserController.java
│     ├─domain\
│     │ ├─Book.java
│     │ └─User.java
│     └─interceptor\
│       └─AuthorizationInterceptor.java
└─WebContent\
  ├─images\
  │ ├─ajax.jpg
  │ ├─android.jpg
  │ ├─basic.jpg
  │ ├─ee.jpg
  │ ├─fkjava.jpg
  │ ├─framework.jpg
  │ ├─java.jpg
  │ ├─javaee.jpg
  │ ├─struts.jpg
  │ └─xml.jpg
  ├─META-INF\
  │ └─MANIFEST.MF
  └─WEB-INF\
    ├─content\
    │ ├─loginForm.jsp
    │ └─main.jsp
    ├─lib\
    │ ├─commons-logging-1.2.jar
    │ ├─javax.servlet.jsp.jstl-1.2.1.jar
    │ ├─javax.servlet.jsp.jstl-api-1.2.1.jar
    │ ├─spring-aop-5.0.1.RELEASE.jar
    │ ├─spring-aspects-5.0.1.RELEASE.jar
    │ ├─spring-beans-5.0.1.RELEASE.jar
    │ ├─spring-context-5.0.1.RELEASE.jar
    │ ├─spring-context-indexer-5.0.1.RELEASE.jar
    │ ├─spring-context-support-5.0.1.RELEASE.jar
    │ ├─spring-core-5.0.1.RELEASE.jar
    │ ├─spring-expression-5.0.1.RELEASE.jar
    │ ├─spring-instrument-5.0.1.RELEASE.jar
    │ ├─spring-jcl-5.0.1.RELEASE.jar
    │ ├─spring-jdbc-5.0.1.RELEASE.jar
    │ ├─spring-jms-5.0.1.RELEASE.jar
    │ ├─spring-messaging-5.0.1.RELEASE.jar
    │ ├─spring-orm-5.0.1.RELEASE.jar
    │ ├─spring-oxm-5.0.1.RELEASE.jar
    │ ├─spring-test-5.0.1.RELEASE.jar
    │ ├─spring-tx-5.0.1.RELEASE.jar
    │ ├─spring-web-5.0.1.RELEASE.jar
    │ ├─spring-webflux-5.0.1.RELEASE.jar
    │ ├─spring-webmvc-5.0.1.RELEASE.jar
    │ └─spring-websocket-5.0.1.RELEASE.jar
    ├─springmvc-config.xml
    └─web.xml

本小节通过拦截器完成一个用户权限验证的功能。即用户必须登录之后才可以访问网站首页,如果没有登录就直接访问网站首页,则拦截器会拦截请求,并将请求重新转发到登录页面,同时提示用户需要先登录再访问网站。

FormController.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 动态页面跳转控制器
*/
@Controller
public class FormController {

@GetMapping(value = "/loginForm")
public String loginForm()
{
System.out.println("请求处理方法 loginForm");
// 跳转到登录页面
return "loginForm";
}
}

loginForm.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
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>登录页面</title>
</head>
<body>
<h3>登录页面</h3>
<form action="login" method="post">
<!-- 提示信息 -->
<font color="red">${requestScope.message }</font>
<table>
<tr>
<td><label>登录名: </label></td>
<td><input type="text" id="loginname" name="loginname"></td>
</tr>
<tr>
<td><label>密码: </label></td>
<td><input type="password" id="password" name="password"></td>
</tr>
<tr>
<td><input type="submit" value="登录"></td>
</tr>
</table>
</form>
</body>
</html>

UserController.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
package org.fkit.controller;

import javax.servlet.http.HttpSession;
import org.fkit.domain.User;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.servlet.ModelAndView;

/**
* 处理用户请求控制器
*/
@Controller
public class UserController
{

/**
* 处理/login请求
*/
@PostMapping(value = "/login")
public ModelAndView login(String loginname, String password,
ModelAndView mv, HttpSession session)
{
// 模拟数据库根据登录名和密码查找用户,判断用户登录
if (loginname != null && loginname.equals("fkit") && password != null
&& password.equals("123456"))
{
// 模拟创建用户
User user = new User();
user.setLoginname(loginname);
user.setPassword(password);
user.setUsername("管理员");
// 登录成功,将user对象设置到HttpSession作用范围域
session.setAttribute("user", user);
// 转发到main请求
mv.setViewName("redirect:main");
} else
{
// 登录失败,设置失败提示信息,并跳转到登录页面
mv.addObject("message", "登录名或密码错误,请重新输入!");
mv.setViewName("loginForm");
}
return mv;
}

}

UserController类的login方法用来处理登录请求,本示例没有使用数据库存储数据,只是简单地模拟了用户登录,只要用户输入的登录名是”xiaoming“,密码是”xiaoming”,则验证通过。

  • 如果验证通过,创建一个User对象保存到HttpSession当中,同时将请求使用客户端跳转到main请求;
  • 如果验证失败,则设置失败提示信息到ModelAndView对象,同时将请求使用客户端跳转到loginForm请求,即跳回登录页面。

BookController.java

BookController类的main方法用来处理网站首页的请求,该方法获得所有图书信息,并将它们设置到Model当中,然后传递到main页面。本示例没有使用数据库存储数据,只是简单地创建了一个集合模拟从数据库获取图书信息。

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
package org.fkit.controller;

import java.util.ArrayList;
import java.util.List;
import org.fkit.domain.Book;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;

/**
* 处理图书请求控制器
*/
@Controller
public class BookController
{

/**
* 处理/main请求
*/
@RequestMapping(value = "/main")
public String main(Model model)
{
// 模拟数据库获得所有图书集合
List<Book> book_list = new ArrayList<Book>();
book_list.add(new Book("java.jpg", "疯狂Java讲义(附光盘)", "李刚 编著", 74.2));
book_list.add(new Book("ee.jpg", "轻量级Java EE企业应用实战", "李刚 编著", 59.2));
book_list.add(
new Book("android.jpg", "疯狂Android讲义(附光盘)", "李刚 编著", 60.6));
book_list.add(new Book("ajax.jpg", "疯狂Ajax讲义(附光盘)", "李刚 编著", 66.6));
// 将图书集合添加到model当中
model.addAttribute("book_list", book_list);
// 跳转到main页面
return "main";
}

}

main.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
<%@ 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>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>首页</title>
<style type="text/css">
table {
border-collapse: collapse;
border-spacing: 0;
border-left: 1px solid #888;
border-top: 1px solid #888;
background: #efefef;
}

th, td {
border-right: 1px solid #888;
border-bottom: 1px solid #888;
padding: 5px 15px;
}

th {
font-weight: bold;
background: #ccc;
}
</style>
</head>
<body>
<h3>欢迎[${sessionScope.user.username }]访问</h3>
<br>
<table border="1">
<tr>
<th>封面</th>
<th>书名</th>
<th>作者</th>
<th>价格</th>
</tr>
<c:forEach items="${requestScope.book_list }" var="book">
<tr>
<td><img src="images/${book.image }" height="60"></td>
<td>${book.name }</td>
<td>${book.author }</td>
<td>${book.price }</td>
</tr>
</c:forEach>
</table>
</body>
</html>

拦截器

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
/**
* 拦截器必须实现HandlerInterceptor接口
*/
public class AuthorizationInterceptor
implements HandlerInterceptor
{
// 不拦截"/loginForm"和"/login"请求
private static final String[] IGNORE_URI = {"/loginForm", "/login"};
/**
* 该方法将在整个请求完成之后执行, 主要作用是用于清理资源的,
* 该方法也只能在当前Interceptor的preHandle方法的返回值为true时才会执行。
*/
@Override
public void afterCompletion(HttpServletRequest request,
HttpServletResponse response, Object handler,
Exception exception)
throws Exception
{
System.out.println("拦截器的 afterCompletion方法\n-----------------------------");
}
/**
* 该方法将在Controller的方法调用之后执行, 方法中可以对ModelAndView进行操作 ,
* 该方法也只能在当前Interceptor的preHandle方法的返回值为true时才会执行。
*/
@Override
public void postHandle(HttpServletRequest request,
HttpServletResponse response, Object handler,
ModelAndView mv)
throws Exception
{
System.out.println("拦截器的 postHandle方法");
}
/**
* preHandle方法是进行处理器拦截用的,该方法将在Controller处理之前进行调用,
* 该方法的返回值为true拦截器才会继续往下执行,该方法的返回值为false的时候整个请求就结束了。
*/
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response, Object handler)
throws Exception
{
System.out.println("拦截器的 preHandle方法");
// flag变量用于判断用户是否登录,默认为false
boolean flag = false;
// 获取请求的路径进行判断
String servletPath = request.getServletPath();
// 判断请求是否需要拦截
for (String s : IGNORE_URI)
{
if (servletPath.contains(s))
{
flag = true;
break;
}
}
// 拦截请求
if (!flag)
{
// 1.获取session中的用户
User user = (User) request.getSession().getAttribute("user");
// 2.判断用户是否已经登录
if (user == null)
{
// 如果用户没有登录,则设置提示信息,跳转到登录页面
System.out.println(" |----验证不通过,重新登录");
request.setAttribute("message", "请先登录再访问网站");
// 转发到登录页面
request.getRequestDispatcher("loginForm").forward(request,
response);
} else
{
// 如果用户已经登录,则验证通过,放行
System.out.println(" |----验证通过,放行/main请求");
flag = true;
}
}
return flag;
}
}

测试

1
<a href="main">直接请求/main</a>

如果没有登录,直接访问main请求,拦截器会拦截请求,验证用户是否登录,此时用户若没有登录,则跳转到登录页面,如下图所示:
这里有一张图片
从控制台的输出可以看到各个方法的执行顺序,如下所示.

1
2
3
4
5
6
7
拦截器的 preHandle方法
|----验证不通过,重新登录
拦截器的 preHandle方法
请求处理方法 loginForm
拦截器的 postHandle方法
拦截器的 afterCompletion方法
-----------------------------

此时填写正确的用户名xiaoming和密码xiaoming,点击登录
这里有一张图片
此时才可以正确得到main请求的资源,如下图所示:
这里有一张图片
控制台新增内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
拦截器的 preHandle方法
请求处理方法 login
|----->登陆验证通过,转发到main请求
拦截器的 postHandle方法
拦截器的 afterCompletion方法
-----------------------------
拦截器的 preHandle方法
|----验证通过,放行/main请求
请求处理方法 main
拦截器的 postHandle方法
拦截器的 afterCompletion方法
-----------------------------

可以看到执行顺序是,表单提交到login请求,login方法验证成功后,转发到main请求,当请求main时,拦截器的preHandle方法先验证是否登录过,现在的情况是已经登录了,preHandle方法放行main请求,main方法得到执行.

6.2.2 Spring支持的转换器 示例 使用InitBinder注解添加自定义编辑器转换数据

域对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import java.io.Serializable;
import java.util.Date;
public class User
implements Serializable
{
private static final long serialVersionUID = 1L;
private String loginname;
private Date birthday;
public User()
{
super();
// TODO Auto-generated constructor stub
}
// 此处省略getter和setter方法,请自己补上
@Override
public String toString()
{
return "User [loginname=" + loginname + ", birthday=" + birthday + "]";
}
}

测试链接

1
<a href="registerForm">registerForm</a>

请求处理方法

1
2
3
4
5
6
@GetMapping(value = "/registerForm")
public String registerForm()
{
// 跳转到注册页面
return "registerForm";
}

表单

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
<form action="register" method="post">
<table>
<tr>
<td>
<label>登录名: </label>
</td>
<td>
<input type="text" id="loginname" name="loginname">
</td>
</tr>
<tr>
<td>
<label>生日: </label>
</td>
<td>
<input type="text" id="birthday" name="birthday">
</td>
</tr>
<tr>
<td>
<input id="submit" type="submit" value="登录">
</td>
</tr>
</table>
</form>

填写表单并提交

这里有一张图片

表单处理方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 在控制器初始化时注册属性编辑器
@InitBinder
public void initBinder(WebDataBinder binder)
{
// 注册自定义编辑器
binder.registerCustomEditor(Date.class, new DateEditor());
}
@PostMapping(value = "/register")
//@ModelAttribute注解:把提交的请求数据赋值给user参数
public String register(@ModelAttribute
User user, Model mode)
{
System.out.println(user);
mode.addAttribute("user", user);
return "success";
}

并使用@InitBinde注解,该注解会在控制器初始化时注册属性编辑器。WebDataBinder对象用于处理请求消息和处理方法的绑定工作。binder.registerCustomEditor()方法对传入的Date类型使用DateEditor类进行转换。
使用了@InitBinder注解,不再需要在mvc:annotation-driven标签中装配自定义转换器。

自定义属性编辑器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 自定义属性编辑器
public class DateEditor
extends PropertyEditorSupport
{
// 将传入的字符串数据转换成Date类型
@Override
public void setAsText(String text) throws IllegalArgumentException
{
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
try
{
Date date = dateFormat.parse(text);
setValue(date);
} catch (ParseException e)
{
e.printStackTrace();
}
}
}

显示效果

这里有一张图片

6.2.2 Spring支持的转换器

Springorg.springframework.core.convert.converter包中定义了3种类型的转换器接口,我们可以实现其中任意一种转换器接口,并将它作为自定义转换器注册到ConversionServiceFactoryBean当中。这3种类型转换器接口如下所示:

Converter接口

Converter<S,T>接口是Spring中最简单的一个转换器接口,该接口中只有一个方法:

1
此处有代码后面补上

该方法负责S类型的对象转换为T类型的对象

ConverterFactory接口

如果希望将一种类型的对象转换为另一种类型及其子类对象,比如将String转换为Number以及Number的子类IntegerDouble等对象,就需要一系列的Converter,如StringToIntegerStringToDouble等。**ConverterFactory<S,R>接口的作用就是将相同系列的多个Converter封装在一起**。该接口中也只有一个方法:

1
此处有代码后面补上

S为转换的源类型,R为目标类型的基类,TR的子类。

GenericConverter

Converter<S,T>接口只是负责将一个类型对象转换为另一个类型的对象,它并没有考虑类型对象上下文信息,因此并不能完成”复杂”类型的转换工作。**GenericConverter接口会根据源类对象及目标类对象的上下文信息进行类型转换**。该接口中定义了两个方法:

1
此处有代码后面补上

ConvertiblePair封装了源类型和目标类型,而TypeDescriptor包含了需要转换的类型对象的上下文信息,因此GenericConverter接口的convert()方法可以利用这些上下文信息完成类型转换的工作。

示例 使用ConversionService转换数据

域对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 域对象,实现序列化接口
public class User
implements Serializable
{
private static final long serialVersionUID = 1L;
private String loginname;
private Date birthday;
public User()
{
super();
// TODO Auto-generated constructor stub
}
// 此处省略getter和setter方法,请自己补上
@Override
public String toString()
{
return "User [loginname=" + loginname + ", birthday=" + birthday + "]";
}
}

User提供了loginnamebirthday属性,用于接收JSP页面传入的数据。注意,birthday属性的类型是一个java.util.Date,而JSP页面传入的控件数据类型都是String,这里就需要将String转换成Date对象

测试链接

1
<a href="registerForm">测试registerForm</a>

请求处理方法

1
2
3
4
5
6
@GetMapping(value = "/registerForm")
public String registerForm()
{
// 跳转到注册页面
return "registerForm";
}

登录表单

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

<form action="register" method="post">
<table>
<tr>
<td>
<label>登录名: </label>
</td>
<td>
<input type="text" id="loginname" name="loginname">
</td>
</tr>
<tr>
<td>
<label>生日: </label>
</td>
<!-- 将会提交给请求处理方法中与name属性值同名的域对象的属性 -->
<td>
<input type="text" id="birthday" name="birthday">
</td>
</tr>
<tr>
<td>
<input id="submit" type="submit" value="登录">
</td>
</tr>
</table>
</form>

表单处理方法

1
2
3
4
5
6
7
8
9
@PostMapping(value = "/register")
// 前台页面的控件的值,将会自动赋值给user对象中的同名属性中
public String register(@ModelAttribute
User user, Model model)
{
System.out.println(user);
model.addAttribute("user", user);
return "success";
}

register方法只是简单地接收请求数据,并将数据转换并设置到User对象当中,转换工作使用自定义的转换器来完成,转换器的使用方法如下所示:

自定义转换器

这里的转换器通过实现Converter<S,T>接口的方法实现,Converter<String, Date>可以将String类型的数据转换为Date类型

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
// 实现Converter<S,T>接口
//Converter<String, Date>可以将String类型的数据转换为Date类型
public class StringToDateConverter
implements Converter<String, Date>
{
// 日期类型模板:如yyyy-MM-dd
private String datePattern;
public void setDatePattern(String datePattern)
{
this.datePattern = datePattern;
}
// Converter<S,T>接口的类型转换方法
@Override
public Date convert(String date)
{
try
{
SimpleDateFormat dateFormat = new SimpleDateFormat(
this.datePattern);
// 将日期字符串转换成Date类型返回
return dateFormat.parse(date);
} catch (Exception e)
{
e.printStackTrace();
System.out.println("日期转换失败!");
return null;
}
}
}

使用自定义转换器

有了自定义转换器,接下来就要使用这个转换器了,在Spring配置文件中配置转换器的bean就行了.

在Sping配置文件中创建自定义转换器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!-- 自定义的类型转换器 -->
<bean
id="conversionService"
class="org.springframework.context.support.ConversionServiceFactoryBean">
<!-- 使用converters属性来注册自定义转换器 -->
<property name="converters">
<list>
<!-- 给自定义的转换器StringToDateConverter的bean赋值`yyyy-MM-dd` -->
<bean
class="org.fkit.converter.StringToDateConverter"
p:datePattern="yyyy-MM-dd"></bean>
</list>
</property>
</bean>

在Spring配置文件中装配自定义转换器

1
2
<!-- 装配自定义的类型转换器 -->
<mvc:annotation-driven conversion-service="conversionService" />

springmvc-config.xml配置文件中,使用了mvc:annotation-driven标签,该标签可以简化Spring MVC的相关配置,使用mvc:annotation-driven标签可以自动注册RequestMappingHandlerMappingRequestMappingHandlerAdapter两个Bean,这是SpringMVC@Controllers注解分发请求所必需的。
除此之外,mvc:annotation-driven标签还会注册一个默认的ConversionService(FormattingConversionServiceFactoryBean),以满足大多数类型转换的需求。现在由于需要注册一个自定义的StringToDateConverter转换类,因此,需要显式定义一个ConversionService覆盖mvc:annotation-driven中的默认实现类,而这一步需要在,mvc:annotation-driven标签设置converters属性来完成。

填写表单

这里有一张图片
输入登录名和生日信息,单击”登录”按钮,转换器会自动将输入的日期字符串转换成Date类型,并将其设置到User对象的birthday属性当中。控制台输出如下所示:

1
User [loginname=小明, birthday=Thu Sep 06 00:00:00 CST 126]

可以看到,User对象的birthday属性已经获得JSP页面传入的日期值。

成功页面

1
2
3
4
5
登录名:${requestScope.user.loginname }
<br> 生日:
<fmt:formatDate value="${requestScope.user.birthday}"
pattern="yyyy年MM月dd日" />
<br>

成功显示效果

这里有一张图片

6.2 数据转换

Java语言中,在java.beans包中提供了一个ProperyEditor接口来进行数据转换。**ProperyEditor的核心功能是将一个字符串转换为一个Java对象**,以便根据界面的输入或配置文件中的配置字符串构造出一个Java对象。但是**ProperyEditor存在以下不足**:
只能用于字符串和Java对象的转换,不适用于任意两个Java类型直接的转换
对源对象及目标对象所在的上下文信息(如注解等)不敏感,在类型转换时不能利用这些上下文信息实施高级转换逻辑

6.2.1 ConversionService

Spring从3.0开始,添加了一个通用的类型转换模块,该类型转换模块位于org.springframework.core.convert包中。Spring希望用这个类型转换体系替换Java标准的ProperyEditor接口。但是由于历史原因,Spring还是同时支持两者。我们可以在SpringMVC处理方法的参数绑定中使用它们进行数据转换。
org.springframework.core.convert.ConversionServiceSpring类型转换体系的核心接口,在该接口中定义了以下4个方法:

方法 描述
boolean canConvert(Class<?> sourceType,Class<?> targetType) 判断是否可以将一个Java类转换为另一个Java
boolean canConvert(TypeDescriptor sourceType,TypeDescriptor targetType) 需要转换的类将以成员变量的方式出现,TypeDescriptor不但描述了需要转换类的信息,还描述了类的上下文信息,例如成员变量上的注解成员变量是否以数组、集合或Map的方式呈现等。类型转换逻辑可以利用这些信息做出各种灵活的控制。
<T>T convert(Object source,Class<T> targetType) 将源类型对象转换为目标类型对象
Object convert(Object source ,TypeDescriptor sourceType,TypeDescriptor targetType) 将对象从源类型对象转换为目标类型对象,通常会利用到类中的上下文信息。

在Spring配置文件中配置

可以利用org.springframework.context.support.ConversionServiceFactoryBeanSpring的上下文中定义一个ConversionServiceSpring将自动识别出上下文中的ConversionService,并在SpringMVC处理方法的参数绑定中使用它进行数据转换。示例配置代码如下:

1
此处有代码后面补上

默认支持转换类型

ConversionServiceFactoryBean中可以内置很多的类型转换器,使用它们可以完成大多数Java类型的转换工作,其除了包括将Spring对象转换为各种基础类型的对象外,还包括StringNumberArrayCollectionMapPropertiesObject之间的转换器。

注册自定义的类型转换器

可以通过ConversionServiceFactoryBeanconverters属性注册自定义的类型转换器,示例配置代码如下:

1
此处有代码后面补上

6.1 数据绑定流程

Spring MVC通过反射机制对目标处理方法的签名进行分析,并将请求消息绑定到处理方法的参数上,数据绑定的核心部件是DataBinder.

  • Spring MVC框架将ServletRequest对象及处理方法的参数对象实例传递给DataBinder,
  • DataBinder调用装配在SpringWeb上下文中的ConversionService组件进行数据类型转换、数据格式化工作,并将ServletRequest中的消息填充到参数对象中。
  • 然后再调用Validator组件对已经绑定了请求消息数据的参数对象进行数据合法性校验,并最终生成数据绑定结果BindingResult对象。

其运行机制如下图所示
这里有一张图片
BindingResult包含已完成数据绑定的参数对象,还包含相应的校验错误对象,Spring MVC抽取BindingResult中的参数对象校验错误对象,将它们赋给处理方法的相应参数