SSM注解开发的高级知识讲解

经过前面两篇文章SSM的整合和一个案例带你快速入门SSM开发的讲解后,我们已经顺利的了解了利用SSM进行开发的基础知识,本篇文章将为你介绍更负责的高级知识,例如数据回显、参数绑定list数据、图片的上传、json数据的请求和响应、validation分组校验、统一异常处理、RESTful支持以及拦截器的应用。

当然此篇文章也是在前篇文章开发的工程基础上进行讲解。源码请点击这里前往我的github

1.数据回显

需求:当提交表单时出现错误,回到表单重新填写数据时需要将刚才提交的参数在页面回显。以往我们采用将数据放在request域中然后请求转发到表单页面,运用el表达式将数据回显。在springmvc中我们通过Model对象的addAttribute(“id”,id),将简单类型的数据传入request域(注意在进入修改页面的controller方法中和提交修改商品信息方法model.addAttribute方法设置的key一致)。通过Model的addAttribute(“pojo”,pojo)方法将pojo类型的数据也是传入到request域中,然后通过el表达式完成取出request域中的数据,这是pojo类型数据回显的第一种方法,如下:

1
2
3
4
5
6
7
8
9
10
@RequestMapping("/editItemSubmit")
public String editItemSubmit(Model model,Integer id,ItemsCustom itemsCustom) throws Exception
{

//进行数据回显
model.addAttribute("id",id);
model.addAttribute("item",itemsCustom);

itemsService.updateItems(id,itemsCustom);
return "editItem";
}

第二种方法是通过@ModelAttribute()注解的方式,括号内传入要回显的pojo对象,如我们这里要将itemsCustom传到request域所以我们这样写@ModelAttribute(value="itemsCustom"),然后便可以将代码中的将model添加到request域的代码model.addAttribute("item",itemsCustom);注释掉了。

@ModelAttribute()注解的另外一种用法:将方法的返回值填入到request域然后显示在页面上。在ItemsController.java中加入方法:

1
2
3
4
5
6
7
8
9
//单独将商品类型的方法提取出来,将方法返回值填充到request域,在页面提示
@ModelAttribute("itemsType")
public Map<String,String> getItemsType() throws Exception{

HashMap<String,String> itemsType=new HashMap<>();
itemsType.put("001","data type");
itemsType.put("002","clothes");
return itemsType;
}

然后我们在itemsList.jsp页面中加入如下标签进行页面的展示:

1
2
3
4
5
6
7
<td>
<select>
<c:forEach items="${itemsType}" var="item">
<option value="${item.key}">${item.value}</option>
</c:forEach>
</select>
</td>

成功使用@ModleAttribute注解将方法返回值返回到页面。其实这种注解的方式也是将数据填入到request域中,然后在页面中通过el表达式取出,同model.addAttribute()方法和modelAndView.addObject()方法。

使用@ModelAttribute将公用的取数据的方法返回值传到页面,不用在每一个controller方法通过Model将数据传到页面。

2.将页面信息绑定到参数集合类型中

前面的基础知识讲解中我们都只是将简单类型和pojo类型的数据绑定到了参数中。接下来讲解高级知识的绑定。

2.1将页面信息绑定到数组参数中

需求:在商品查询列表页面,用户选择要删除的商品,批量删除商品。

在controller方法中如何将批量提交的数据绑定成数组类型。

页面的定义,在itemsList.jsp中每件商品前面添加一个checkbox:

1
<td><input type="checkbox" name="delete_id" value="${item.id}"> </td>

在上方添加一个批量删除的按钮:

1
<input type="button" value="批量删除" onclick="deleteItems()">

因为我们form的action是点击查询后返回到itemsList.action,所以这里我们点击delete_id按钮应该返回到另一个action所以需要编写一个js脚本对该按钮进行事件监听。

以及点击这个按钮响应的事件:

1
2
3
4
5
6
7
8
9
10
<script type="text/javascript">
function deleteItems() {
//将form的action指向删除商品的地址
document.itemsForm.action="${pageContext.request.contextPath}/items/deleteItems.action";

//进行form提交
document.itemsForm.submit();

}
</script>

然后需要在contronller中添加deleteItems()方法:

1
2
3
4
5
6
7
8
9
//删除商品
@RequestMapping("/deleteItems")
public String deleteItems(Integer[] delete_id) throws Exception
{

//调用serive方法删除商品
//这里我们就是简单的介绍完成将页面的信息绑定到数组中,所以service的方法你可以自己去完成

return "success";
}

然后定义一个success.jsp页面输出成功删除的信息即可。

2.2绑定页面信息到List

需求:批量修改商品信息提交。先进入批量修改商品页面,填写信息,点击提交。

页面的定义,新建一个editItemsList.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
61
62
63
64
65
66
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>

<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="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>批量修改商品查询</title>
<script type="text/javascript">
function updateItems() {
//将form的action指向删除商品的地址
document.itemsForm.action="${pageContext.request.contextPath}/items/deleteItems.action";

//进行form提交
document.itemsForm.submit();

}
</script>

</head>
<body>
<form name="itemsForm" action="${pageContext.request.contextPath }/items/queryItem.action" method="post">
查询条件:
<table width="100%" border=1>
<tr>
<td>
<select>
<c:forEach items="${itemsType}" var="item">

<option value="${item.key}">${item.value}</option>

</c:forEach>

</select>
</td>
<td><input type="submit" value="查询"/>
<input type="button" value="批量修改提交" onclick="updateItems()">
</td>
</tr>
</table>
商品列表:
<table width="100%" border=1>
<tr>
<td>商品名称</td>
<td>商品价格</td>
<td>生产日期</td>
<td>商品描述</td>
<td>操作</td>
</tr>
<c:forEach items="${itemsList }" var="item" varStatus="s">
<tr>
<td><input type="text" name="itemsList[${s.index}].name" value="${item.name}"> </td>
<td><input type="text" name="itemsList[${s.index}].price" value="${item.price}"> </td>
<td><fmt:formatDate value="${item.createtime}" pattern="yyyy-MM-dd HH:mm:ss"/></td>
<td>${item.detail }</td>

<td><a href="${pageContext.request.contextPath }/items/editItems.action?id=${item.id}">修改</a></td>

</tr>
</c:forEach>

</table>
</form>
</body>

</html>

在controller中加入如下两个方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//批量修改商品查询
@RequestMapping("/editItemsList")
public ModelAndView editItemsList() throws Exception {
//调用servie来查询商品列表
List<ItemsCustom> itemsList=itemsService.findItemsList(null);

ModelAndView modelAndView=new ModelAndView();
modelAndView.addObject("itemsList",itemsList);
//指定逻辑视图名itemsList.jsp
modelAndView.setViewName("editItemsList");

return modelAndView;
}

//批量修改商品的提交

@RequestMapping("/editItemsListSubmit")
public String editItemsListSubmit(ItemsQueryVo itemsQueryVo) throws Exception{
return "success";
}

在xml页面中的name属性值的定义是很重要的,因为在我们提交表单后该数据是会绑定到后台中的方法参数中的,这里我们使用itemsList[${s.index}].name作为name的属性值,所以我们在controller的submit方法中添加了一个ItemsQueryVo的参数,若想完成绑定则需要在ItemsQueryVo中定义一个itemsList的属性,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class ItemsQueryVo {

//商品信息
private ItemsCustom itemsCustom;

//定义一个list
private List<ItemsCustom> itemsList;

public List<ItemsCustom> getItemsList() {
return itemsList;
}

public void setItemsList(List<ItemsCustom> itemsList) {
this.itemsList = itemsList;
}

public ItemsCustom getItemsCustom() {
return itemsCustom;
}

public void setItemsCustom(ItemsCustom itemsCustom) {
this.itemsCustom = itemsCustom;
}
}

注释:
itemsList:controller方法形参包装类型中list的属性名。
itemsList[0]或itemsList[1]...的[]中是序号,从0开始。
itemsList[].name:中name就是controller方法形参包装类型中list中pojo的属性名。(有点绕口)

3.商品图片的上传

需求:在商品修改页面,增加图片上传的功能。

操作流程:用户进入商品修改页面,上传图片,点击提交(提交的是图片和商品信息,再次进入修改页面,图片在商品修改页面展示。

3.1图片存储问题

切记:不要把图片上传到工程目录 ,不方便进行工程维护。实际电商项目中使用专门图片服务器(http,比如apache、tomcat)。本教程使用图片虚拟目录,通过虚拟目录访问硬盘上存储的图片目录。

我用的是开发工具为IDEA,虚拟目录的设置为,1.打开tomcat的设置:

2.点击External Source:

3.进入到如下页面:

选择硬盘上的一个路径作为虚拟目录的位置,以后上传的图片都会放在此路径上。并在Application context中添加一个在服务器上访问的路径别名,我这里设置的为/pic。若想访问该目录下的图片,则在浏览器网站上输入:http://localhost:8080/pic/图片名称,(不需要写项目名称),即可在浏览器中成功访问到该图片。

注意:图片目录中尽量进行目录分级存储,提高访问速度(提交i/o)

3.2配置图片上传解析器

springmvc使用commons-fileupload进行图片上传。

需要导入的jar包为:commons-fileupload-1.2.2.jar和依赖包commons-io-2.4.jar。

然后在springmvc中配置图片上传解析器:

1
2
3
4
5
6
7
8
9
<!-- 文件上传
CommonsMultipartResolver依赖我们传入的fileupload jar包-->

<bean id="multipartResolver"
class="org.springframework.web.multipart.commons.CommonsMultipartResolver">

<!-- 设置上传文件的最大尺寸为5MB -->
<property name="maxUploadSize">
<value>5242880</value>
</property>
</bean>

3.3编写上传图片的页面

在editItem.jsp中添加图片的文件表单项:

1
2
3
4
5
6
7
8
9
10
<tr>
<td>商品图片</td>
<td>
<c:if test="${itemsCustom.pic}!=null">
<img src="/pic/${itemsCustom.pic}" width="100" height="100">
<br/>
</c:if>
<input type="file" name="pictureFile">
</td>
</tr>

另外涉及到文件表单项的地方需要在form标签中添加enctype="multipart/form-data"的属性。

3.4编写controller方法

修改edititemSubmit方法,添加参数图片进行页面的图片信息与参数的绑定:

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
 @RequestMapping("/editItemSubmit")
public String editItemSubmit(Model model,Integer id,
@Validated(value = {ValidGroup1.class}) @ModelAttribute(value = "itemsCustom") ItemsCustom itemsCustom,

BindingResult bindingResult,
//上传图片
MultipartFile pictureFile
) throws Exception
{

//进行数据回显
model.addAttribute("id",id);
// model.addAttribute("item",itemsCustom);

//进行图片的上传
if (pictureFile!=null&&pictureFile.getOriginalFilename()!=null&&pictureFile.getOriginalFilename().length()>0)
{
//图片上传成功后,将图片的地址写到数据库
String filePath="/Users/codingBoy/Pictures/";//它的值要同你设置虚拟目录时涉及的目录路径一致,
String originalFilename=pictureFile.getOriginalFilename();

String newFileName= UUID.randomUUID()+originalFilename.substring(originalFilename.lastIndexOf("."));

//新文件
File file=new File(filePath+newFileName);

//将内存中的文件写入磁盘
pictureFile.transferTo(file);

//图片上传成功
itemsCustom.setPic(newFileName);
}


itemsService.updateItems(id,itemsCustom);
//请求转发
// return "forward:queryItems.action";


return "editItem";
//重定向
//return "redirect:queryItems.action";
}

然后运行程序,在修改商品信息的页面上传图片,成功上传后便可在你设置的虚拟目录中找到你上传的图片。

4.json数据的交互

需求:json数据格式是比较简单容易理解,json数据格式常用于远程接口传输,http传输json数据,非常方便页面进行提交/请求结果解析,对json数据的解析。

4.1springmvc解析json需要加入的json解析包

Springmvc默认用MappingJacksonHttpMessageConverter对json数据进行转换,需要加入jackson的包:jackson-core-sal-1.9.11.jar和jackson-mapper-asl-1.9.11.jar。

4.2在处理器适配器中加入MappingJacksonHttpMessageConverter配置

在springmvc的处理器适配器的配置中加入如下配置信息:

1
2
3
4
5
6
7
<!--加入json数据的消息转换器
MappingJacksonHttpMessageConverter依赖jackson的两个jar包-->

<property name="messageConverters">
<list>
<bean class="org.springframework.http.converter.json.MappingJacksonHttpMessageConverter"></bean>
</list>
</property>

4.3@RequestBody和@ResponseBody注解

@RequestBody:将请求的json数据转成java对象。
@ResponseBody:将java对象转成json数据输出。

图解:

4.4测试1:请求的是json数据,响应的也是json数据

在controller包下写一个json测试类的controller叫jsonTest.java,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
@Controller
public class JsonTest
{


//请求的json响应json,请求商品信息(该信息为json格式,在页面中通过ajax向写入用户请求的json信息,需要加入@RequestBody注解),商品信息用json格式输出商品信息(请求的url实际返回的是itemsCustom对象,但由于使用了@ResponseBody就将返回的pojo对象转换成了json格式的数据)
@RequestMapping("/requestJson")
public @ResponseBody ItemsCustom requestJson(@RequestBody ItemsCustom itemsCustom) throws Exception{

return itemsCustom;
}

}

在web包下创建一个请求数据的页面jsonTest.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
<%@ 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>json测试</title>
<script type="text/javascript" src="${pageContext.request.contextPath }/js/jquery-1.4.4.min.js"></script>
<script type="text/javascript">

//请求json响应json
function requestJson(){
$.ajax({
url:"${pageContext.request.contextPath }/requestJson.action",
type:"post",
contentType:"application/json;charset=utf-8",
//请求json数据,使用json表示商品信息
data:'{"name":"手机","price":1999}',
success:function(data){

alert(data.name);
}


});


}

</script>

</head>
<body>

<input type="button" value="请求json响应json" onclick="requestJson()"/>
<input type="button" value="请求key/value响应json" onclick="responseJson()"/>
</body>
</html>

测试追踪,在浏览器中输入:http://localhost:8080/SpringMvcMybatis/jsonTest.jsp直接访问改页面,点击请求json响应json按钮,通过抓包的方式,我们看到服务器上放回的信息为:

4.5测试2:请求key/value数据,响应的json数据

在jsonTest.java中加入方法:

1
2
3
4
5
6
//请求key/value(在页面中通过ajax写入用户想要请求的key/value信息,不需要加@RequestBody注解),响应json(由于action返回的是itemsCustom对象,所以需要加入@ResponseBody注解将pojo对象转换为json格式响应给用户)
@RequestMapping("/responseJson")
public @ResponseBody ItemsCustom responseJson(ItemsCustom itemsCustom) throws Exception{

return itemsCustom;
}

在页面中加入如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//请求key/value响应json
function responseJson(){

$.ajax({
url:"${pageContext.request.contextPath }/responseJson.action",
type:"post",
//contentType:"application/json;charset=utf-8",
//请求key/value数据
data:"name=手机&price=1999",
success:function(data){

alert(data.name);
}


});
}

测试追踪,在浏览器中输入网址,点击请求key/value响应json按钮,通过抓包的方式我们发现丛服务器上返回过来的信息为:

可以看到,我们请求的是key/value数据,而服务器返回的信息是json格式的数据。

4.6小结

如果前端处理没有特殊要求建议使用第二种,请求key/value,响应json,方便客户端解析请求结果。

5validation校验(了解)

对前端的校验大多数通过js在页面校验,这种方法比较简单,如果对安全性考虑,还要在后台校验。

springmvc使用JSR-303(javaEE6规范的一部分)校验规范,springmvc使用的是Hibernate Validator(和Hibernate的ORM无关)。

5.1加入Hibernate Validator的jar

5.2在处理器适配器中配置校验器

在springmvc.xml中的自定义webBinder标签中加入如下标签:

1
<property name="validator" ref="validator"/>

这里引入了一个validator,所以我们需要在springmvc.xml中还需要加入validator的配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<bean id="validator"
class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean">

<property name="providerClass" value="org.hibernate.validator.HibernateValidator" />
<!-- 如果不指定则默认使用classpath下的ValidationMessages.properties -->
<property name="validationMessageSource" ref="messageSource" />
</bean>
<!-- 校验错误信息配置文件 -->
<bean id="messageSource"
class="org.springframework.context.support.ReloadableResourceBundleMessageSource">

<property name="basenames">
<list>
<value>CustomValidationMessages</value>
</list>
</property>
<property name="fileEncodings" value="utf-8" />
<property name="cacheSeconds" value="120" />
</bean>

然后在src包下创建一个CustomValidationMessages.properties文件,内容为:

1
2
3
#校验提示信息:items.name.length.error要写在java代码中
items.name.length.error=商品名称的长度请限制在1到30个字符
items.createtime.is.notnull=请输入商品的生产日期

5.3校验规则

需求:商品信息提交时校验 ,商品生产日期不能为空,商品名称长度在1到30字符之间。

另外首先需要知道的是规则应该在pojo对象的相应属性上规定。

修改pojo的Items.java内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Items {
private Integer id;

@Size(min = 1,max = 30,message = "{items.name.length.error}")
private String name;

private Float price;

private String pic;
@NotNull(message="{items.createtime.is.notnull}")
private Date createtime;

private String detail;
}
省略了相应的setter和getter方法。

通过上面的操作我们定义了校验的规则,那么当违反了上述规则后我们该如何捕获错误呢?需要修改controller方法,在要校验的pojo前边加上@Validated注解,在editItemSubmit()方法中添加如下内容:

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
  //商品提交页面
//itemsQueryVo是包装类型的pojo
//在@Validated中定义使用ValidGroup1组下边的校验
@RequestMapping("/editItemSubmit")
public String editItemSubmit(Model model,Integer id,
@Validated @ModelAttribute(value = "itemsCustom") ItemsCustom itemsCustom,

BindingResult bindingResult,
//上传图片
MultipartFile pictureFile
) throws Exception
{

//输出校验错误信息
//如果参数绑定时出错
if (bindingResult.hasErrors())
{
//获取错误
List<ObjectError> errors=bindingResult.getAllErrors();

model.addAttribute("errors",errors);

for (ObjectError error:errors)
{
//输出错误信息
System.out.println(error.getDefaultMessage());
}

//如果校验错误,仍然回到商品修改页面
return "editItem";

}

....
}

然后是在页面editItem.jsp中展示错误:

1
2
3
4
<!--错误信息-->
<c:forEach items="${errors}" var="error">
<div color="red">${error.defaultMessage}<br/></div>
</c:forEach>

运行程序,在修改商品信息的页面当我们没有输入日起或是将商品名称超过了20个字符时便不能完成页面的跳转而是在该修改页面显示错误信息。

以上我们便通过Validation完成了校验。下面我们来谈谈分组校验:针对不同的controller方法通过分组校验达到个性化校验的目的。

5.4分组校验

需求:修改商品修改功能,只校验生产日期不能为空。

第一步:创建分组接口,创建一个ValidGroup1.java接口:

1
2
3
4
public interface ValidGroup1
{

//接口不定义方法,相当于接口就是个类型,只标识哪些校验规则属于该ValidGroup1分组
}

第二步:定义校验规则属于哪个校验分组:

1
2
3
//通过groups指定此校验属于哪个分组,当然可以指定多个分组
@NotNull(message="{items.createtime.is.notnull}",groups = {ValidGroup1.class})
private Date createtime;

而属性name的校验规则没有添加groups属性。

第三步:在controller方法定义使用校验的分组。

1
2
3
4
5
6
7
public String editItemSubmit(Model model,Integer id,
@Validated(value = {ValidGroup1.class}) @ModelAttribute(value = "itemsCustom") ItemsCustom itemsCustom,

BindingResult bindingResult,
//上传图片
MultipartFile pictureFile
) throws Exception
{}

此时运行程序,在编辑商品信息页面中只会进行日期是否为空的校验,而不能进行商品名称信息的校验。因为在pojo的name属性中我们没有指定该校验规则属于哪个分组,所以在controller的方法中就无法定义使用该校验规则的分组,也就无法进行商品名称的校验。

6.统一异常处理

需求:一般项目中都需要作异常处理,基于系统架构的设计考虑,使用统一的异常处理方法。

系统中异常类型有哪些?

包括预期可能发生的异常、运行时异常(RuntimeException),运行时异常不是预期会发生的。

针对预期可能发生的异常,在代码手动处理异常可以try/catch捕获,可以向上抛出。

针对运行时异常,只能通过规范代码质量、在系统测试时详细测试等排除运行时异常。

6.1统一异常处理解决方案

6.1.2自定义系统异常类

针对预期可能发生的异常,定义很多异常类型,这些异常类型通常继承于Exception。这里定义一个系统自定义异常类CustomException.java,用于测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class CustomException extends Exception
{


//异常信息
private String message;

public CustomException(String message)
{

super(message);
this.message=message;

}

@Override
public String getMessage() {
return message;
}

public void setMessage(String message) {
this.message = message;
}
}

6.1.2异常处理

要在一个统一异常处理的类中要处理系统抛出的所有异常,根据异常类型来处理。

统一异常处理的类是什么?

前端控制器DispatcherServlet在进行HandlerMapping、调用HandlerAdapter执行Handler过程中,如果遇到异常,都会调用这个统一异常处理类。当然该类只有实现HandlerExceptionResolver接口才能叫统一异常处理类。

6.1.2.1定义统一异常处理器类

创建一个CustomExceptionResolver.java文件并实现HandlerExceptionResolver接口,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
public class CustomExceptionResolver implements HandlerExceptionResolver
{

//前端控制器DispatcherServlet在进行HandlerMapping、
// 调用HandlerAdapter执行Handler过程中,如果遇到异常就会执行此方法
//参数中的handler是最终要执行的Handler,它的真实身份是HandlerMethod
//ex就是接受到的异常信息
@Override
public ModelAndView resolveException(HttpServletRequest request,
HttpServletResponse response,
Object handler, Exception ex)
{

return null;
}

6.1.2.2配置统一异常处理器

在springmvc.xml文件中加入统一异常处理器的配置:

1
2
<!--定义统一异常处理器-->
<bean class="exception.CustomExceptionResolver"></bean>

6.1.2.3统一异常处理逻辑

根据不同的异常类型进行异常处理。

系统自定义的异常类是CustomException ,在controller方法中、service方法中手动抛出此类异常。

针对系统自定义的CustomException异常,就可以直接从异常类中获取异常信息,将异常处理在错误页面展示。
针对非CustomException异常,对这类重新构造成一个CustomException,异常信息为“未知错误”,此类错误需要在系统测试阶段去排除。

在统一异常处理器CustomExceptionResolver中实现上边的逻辑。在CustomExceptionResolver.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
//输出异常
ex.printStackTrace();


//统一异常处理代码
//针对系统自定义的CustomException异常,就可以直接从一场中获取一场信息,将异常处理在错误页面展示
//异常信息
String message=null;
CustomException customException=null;
//如果ex是系统自定义的异常,我们就直接取出异常信息
if (ex instanceof CustomException)
{
customException= (CustomException) ex;
}else {
customException=new CustomException("未知错误");
}

//错误信息
message=customException.getMessage();

request.setAttribute("message",message);


try {
//转向到错误页面
request.getRequestDispatcher("/WEB-INF/jsp/error.jsp").forward(request,response);
} catch (ServletException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}

return new ModelAndView();
}

6.1.2.4测试抛出异常由统一异常处理器捕获

可以在controller方法、service方法、dao实现类中抛出异常,要求dao、service、controller遇到异常全部向上抛出异常,方法向 上抛出异常throws Exception。

在ItemsServiceImpl.java的findItemsById方法中添加如下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Override
public ItemsCustom findItemsById(int id) throws Exception {


Items items=itemsMapper.selectByPrimaryKey(id);

//如果查询的商品信息为空,抛出系统自定义的异常
if (items==null)
{
throw new CustomException("修改商品信息不存在");
}

//在这里以后随着需求的变化,需要查询商品的其它相关信息,返回到controller
//所以这个时候用到扩展类更好,如下
ItemsCustom itemsCustom=new ItemsCustom();
//将items的属性拷贝到itemsCustom
BeanUtils.copyProperties(items,itemsCustom);

return itemsCustom;
}

图解:

测试:运行程序,在流浪器中我们输入http://localhost:8080/SpringMvcMybatis/items/editItems.action?id=111,由于没有哪个商品信息的id为111,所以出现异常,一旦出现异常,系统会立马走向统一异常处理器,执行统一异常处理器中的程序。

7.RESTful支持

7.1什么RESTful

RESTful是一种软件开发理念,RESTful对http进行非常好的诠释。RESTful即Representational State Transfer的缩写。关于RESTful我建议大家去看看这篇文章理解RESTful架构–阮一峰的网络日志

7.2url的RESTful实现

非RESTful的http的url:http://localhost:8080/items/editItems.action?id=1&....

RESTful的url是简洁的:http:// localhost:8080/items/editItems/1

参数通过url传递,rest接口返回json数据

7.2.1需求

根据id查看商品信息,商品信息查看的连接使用RESTful方式实现,商品信息以json返回。

7.2.2第一步更改DispatcherServlet配置

在web.xml中再添加一个前端控制器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<!--RESTful的配置-->
<servlet>
<servlet-name>springmvc_rest</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<!--加载springmvc配置文件-->
<init-param>
<param-name>contextConfigLocation</param-name>
<!--配置文件的地址
如果不配置contextConfigLocation,默认查找的配置文件名称是classpath下的:servlet名称+"-servlet.xml"即springmvc-servlet.xml-->

<param-value>classpath:config/spring/springmvc.xml</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>springmvc_rest</servlet-name>
<!--可以配置/:此工程所有的请求全部由springmvc解析,此种方式可以实现RESTful方式,需要特殊处理对静态文件的解析不能由springmvc解析
可以配置*.do或者*.action,所有请求的url扩展名为.do或.action由springmvc解析,此中方法常用
不可以配置/*,如果配置/*,返回jsp也由springmvc解析,这是不对的-->


<!--reft方式配置为/-->
<url-pattern>/</url-pattern>
</servlet-mapping>

7.2.3第二步参数通过url传递

在controller中添加一个方法:

1
2
3
4
5
6
7
8
9
10
11
12
//更具商品id查看商品信息rest接口
//@requestMapping中指定restful方式的url中的参数,参数需要用{}包起来
//@PathVariable将url中的参数和形参进行绑定
@RequestMapping("/viewItems/{id}")
public @ResponseBody ItemsCustom viewItems(@PathVariable("id") Integer id) throws Exception
{

//调用service查询商品的信息
ItemsCustom itemsCustom=itemsService.findItemsById(id);


return itemsCustom;
}

此时我们便可以在浏览器中输入http://localhost:8080/SpringMvcMybatis/items/viewItems/1来查看商品信息。

但是此时会有个bug,我们在web.xml中将访问所有路径的方式设置为:<url-pattern>/</url-pattern>即访问只要是该路径下的资源时都会经过前端控制器,这是不对的。当我们访问web包下的静态资源时也会经过前端控制器由springmvc解析,这当然是不正确的。所以我们需要设置静态资源的解析,在springmvc.xml中添加如下配置信息:

1
<mvc:resources location="/js/" mapping="/js/**"/>

表示访问web/js/包下的静态资源时不会被前端控制器拦截也就不会被springmvc.xml解析。此时便可以正常访问静态资源。

8.springmvc拦截器

8.1拦截器的应用场合

用户请求到DispatherServlet中,DispatherServlet调用HandlerMapping查找Handler,HandlerMapping返回一个拦截的链儿(多个拦截),springmvc中的拦截器是通过HandlerMapping发起的。

在企业开发,使用拦截器实现用户认证(用户登陆后进行身份校验拦截),用户权限拦截。

8.2springmvc拦截器方法

定义一个拦截器HandlerInterceptor1.java实现HandlerInterceptor接口:

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
public class HandlerInterceptor1 implements HandlerInterceptor
{

//在执行handler之前来执行的
//用于用户认证校验、用户权限校验
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//返回false表示进行拦截,不继续执行handler。返回true表示不进行拦截,继续执行handler
return false;
}


//在执行handerl但是返回modelandview之前来执行
//如果需要向页面提供一些公用的数据或配置一些视图信息,使用此方法实现从modelAndView入手
@Override
public void postHandle(HttpServletRequest request,
HttpServletResponse response, Object handler,
ModelAndView modelAndView) throws Exception
{


}


//执行handler之后执行此方法
//做系统统一异常处理,进行方法执行性能监控,在prehandler中设置一个时间点,在afterCompletion设置一个时间点,两个时间点的差就是执行时长
//实现系统统一日志记录
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,
Exception ex) throws Exception
{



}
}

8.3测试拦截器

再定义一个拦截器HandlerInterceptor2.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
public class HandlerInterceptor2 implements HandlerInterceptor
{

//在执行handler之前来执行的
//用于用户认证校验、用户权限校验
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {


System.out.println("HandlerInterceptor2....postHandle");

//返回false表示拦截不继续执行handler,返回true表示放行
return false;
}


//在执行handerl但是返回modelandview之前来执行
//如果需要向页面提供一些公用的数据或配置一些视图信息,使用此方法实现从modelAndView入手
@Override
public void postHandle(HttpServletRequest request,
HttpServletResponse response, Object handler,
ModelAndView modelAndView) throws Exception
{


System.out.println("HandlerInterceptor2....postHandler");

}


//执行handler之后执行此方法
//做系统统一异常处理,进行方法执行性能监控,在prehandler中设置一个时间点,在afterCompletion设置一个时间点,两个时间点的差就是执行时长
//实现系统统一日志记录
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,
Exception ex) throws Exception
{


System.out.println("HandlerInterceptor2....afterCompletion");

}
}

8.3.1配置拦截器

配置全局的拦截器,DispatcherServlet将配置的全局拦截器加载到所有的HandlerMapping。在springmvc.xml中配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
<!--拦截器 -->
<mvc:interceptors>
<!--多个拦截器,顺序执行 -->
<mvc:interceptor>
<mvc:mapping path="/**"/>
<bean class="controller.interceptor.HandlerInterceptor1"></bean>
</mvc:interceptor>
<mvc:interceptor>
<mvc:mapping path="/**"/>
<bean class="controller.interceptor.HandlerInterceptor2"></bean>
</mvc:interceptor>

</mvc:interceptors>

8.3.2测试1:拦截器1与2都放行

测试结果为:

1
2
3
4
5
6
7
8
HandlerInterceptor1...preHandle
HandlerInterceptor2...preHandle

HandlerInterceptor2...postHandle
HandlerInterceptor1...postHandle

HandlerInterceptor2...afterCompletion
HandlerInterceptor1...afterCompletion

总结:执行preHandle是顺序执行。执行postHandle、afterCompletion是倒序执行。

8.3.3测试2:拦截器1放行,2不放行

此时访问界面,界面是空白的但是不会出现报错信息,控制台打印测试结果:

1
2
3
HandlerInterceptor1...preHandle
HandlerInterceptor2...preHandle
HandlerInterceptor1...afterCompletion

总结:如果preHandle不放行,postHandle、afterCompletion都不执行。只要有一个拦截器不放行,controller不能执行完成。

8.3.3测试2:拦截器1和2都不放行

测试结果:

1
HandlerInterceptor1...preHandle

将这个测试结果和测试2结果进行比较,该测试结果只有拦截器1的preHandle方法执行而2的prehandle方法没有执行;而测试2结果拦截器1和2的preHandle都执行了,说明只有前边的拦截器放行时后面拦截器的preHandle方法才会执行。

8.4拦截器的应用

需求:用户访问系统的资源(url),如果用户没有进行身份认证,进行拦截,系统跳转登陆页面,如果用户已经认证通过,用户可以继续访问系统的资源。

8.4.1用户登录及退出功能开发

新建一个controller,LoginController.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
@Controller
public class LoginController
{


//用户登陆提交方法
@RequestMapping("/login")
public String login(HttpSession session,String usercode, String password) throws Exception
{

//调用service校验用户帐号和密码的正确性
//这个东西我们讲shiro的时候再写



//如果service校验通过,将用户身份记录到session
session.setAttribute("usercode",usercode);

//重定向到商品查询页面
return "redirect:/items/queryItems.action";
}

//用户退出
@RequestMapping("/logout")
public String logout(HttpSession session) throws Exception
{

//session失效
session.invalidate();

//重定向到商品查询页面
return "redirect:/items/queryItems.action";
}
}

8.4.2用户身份认证校验拦截器

拦截实现的思路:

8.4.3拦截器的编写

新建一个拦截器LoginInterceptor.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
public class LoginInterceptor implements HandlerInterceptor
{

//在执行handler之前来执行的
//用于用户认证校验、用户权限校验
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

String url=request.getRequestURI();

//判断是否是公开地址
//世纪开发中需要将公开地址配置在配置文件中

if (url.indexOf("login.action")>=0)
{
//如果是公开地址则放行
return true;
}

//判断用户身份在session中是否存在
HttpSession session=request.getSession();
String usercode= (String) session.getAttribute("usercode");

//如果用户身份在session中存在则放行
if (usercode!=null)
{
return true;
}

//执行到这里就拦截,跳转到登陆页面,用户进行登陆
request.getRequestDispatcher("/WEB-INF/jsp/login.jsp").forward(request,response);

return false;
}


//在执行handerl但是返回modelandview之前来执行
//如果需要向页面提供一些公用的数据或配置一些视图信息,使用此方法实现从modelAndView入手
@Override
public void postHandle(HttpServletRequest request,
HttpServletResponse response, Object handler,
ModelAndView modelAndView) throws Exception
{


System.out.println("HandlerInterceptor1....postHandle");
}


//执行handler之后执行此方法
//做系统统一异常处理,进行方法执行性能监控,在prehandler中设置一个时间点,在afterCompletion设置一个时间点,两个时间点的差就是执行时长
//实现系统统一日志记录
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,
Exception ex) throws Exception
{


System.out.println("HandlerInterceptor1....afterCompletion");

}
}

然后在springmvc.xml中配置该拦截器,需要注视掉我们之前配置的拦截器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<!--拦截器 -->
<mvc:interceptors>
<!--多个拦截器,顺序执行 -->
<!--<mvc:interceptor>-->
<!--<mvc:mapping path="/**"/>-->
<!--<bean class="controller.interceptor.HandlerInterceptor1"></bean>-->
<!--</mvc:interceptor>-->
<!--<mvc:interceptor>-->
<!--<mvc:mapping path="/**"/>-->
<!--<bean class="controller.interceptor.HandlerInterceptor2"></bean>-->
<!--</mvc:interceptor>-->


<mvc:interceptor>
<!--/**可以拦截路径不管有多少层-->
<mvc:mapping path="/**"/>
<bean class="controller.interceptor.LoginInterceptor"></bean>
</mvc:interceptor>
</mvc:interceptors>

然后运行程序,当用户访问的不是公开url且用户没有登录时,该拦截器就会将用户的请求拦截并使用户跳转到登录页面进行登录。

2018.3.19更

欢迎加入我的Java交流1群:659957958。

2018.4.21更:如果群1已满或者无法加入,请加Java学习交流2群:305335626

9.联系

If you have some questions after you see this article,you can tell your doubts in the comments area or you can find some info by clicking these links.

记得扫一扫领一下红包再走哦