1.JSON
1.1. 转发或重定向的不足
对于发出的请求,最终需要给出“成功/失败”的结果的话,转发的方式来处理,给用户的体验就非常不好!即使用户填错了一项数据不达标,都需要回退至此前的页面重新填写,而原本填写的其它数据可能需要再填写一次!
并且,当用户发出请求后,如果是转发或者重定向,一定会发生页面的跳转!无法在页面中局部来提示信息。
从服务端的角度出来,其实,还存在浪费流量的问题!如果只是为了表达“成功/失败”,也许只需要1个“1/0”就可以了,根本就不需要使用整个网页来表达!
所以,当服务器需要向客户端响应某些数据,而不是响应整个页面时,可以使用JSON来组织数据!
1.2. JSON简介
早期,会使用XML来组织服务器响应的数据,例如:
<result>
<state>-1</state>
<message>修改密码失败!原密码错误!</message>
</result>
不过,这种做法导致的问题就是:解析比较麻烦,传输的数据量略大!后来,改用了JSON格式:
{
"state": -1,
"message":"修改密码失败!原密码错误!"
}
JSON是一种组织数据的格式,JavaScript天生就支持解析JSON数据,并且,这种格式相对于XML更加简洁,在传输过程中产生的流量将更小!
1.3. 语法格式
JSON是在JavaScript中的一种对象型数据,其数据的组成方式大致是:使用大括号框住所有内容,每项数据的名称都是字符串,需要使用引号(单/双均可)框住数据的名称,然后使用冒号与值分隔,值可以是各种数据类型,例如:数值型的值可以直接写,而字符串型的值需要引号,各个数据之间使用逗号进行分隔:
<script type="text/javascript">
var obj = { "state": -1, "message": "error" };
</script>
在JavaScript中,可以直接访问JSON对象中的任意数据,语法为:
var state = obj.state;
JSON中的数组
在JSON中的数据的值也可以是数组,例如:
var obj = { "state": -1, "message": "error", "skill": ["Java", "MySQL", "AJAX"] };
即使用[]
表示数组,各数组元素之间使用逗号分隔。
则通过obj.skill
可以访问到整个数组,通过obj.skill[下标]
可以访问到数组中的某个元素,和其它语言一样,数组的下标是从0开始编号的。
JSON中的对象
在JSON中的某个数据的值也可以是对象型的,例如:
var obj = { "state": -1, "message": "error", "skill": ["Java", "MySQL", "AJAX"], "user": { "username": "Jack", "age": 18 } };
则使用obj.user
就可以访问到整个对象,使用obj.user.username
就可以访问到"Jack"
这个值!
其实,在JSON中,可以无限的嵌套下去,即:JSON对象中包括某个子级对象,子级对象中还可以继续包含另一个子级对象,一直包含下去……
JSON对象与JSON字符串
在实际应用中,JSON对象往往并不是直接编写出来的,而是通过服务器端返回的!而服务器端返回的一定是字符串,而不会是一个JSON对象!即服务端返回的会是:
'{ "state": -1, "message": "error" }'
即:内容本身是JSON语法所组织的,但是,数据类型却是字符串,而不是JSON对象!
在JavaScript中,可以通过JSON.parse(str)
函数将参数str
进行转换,得到JSON对象!
1.4. 小结
JSON是一种组织数据结构的对象,支持在对象中存放若干个属性,属性的类型可以是各种类型,包括常用数据类型、数组和其它对象。
学习JSON重点掌握如何通过JSON组织数据。
JavaScript天生支持直接解析JSON对象,获取其中的数据。
当获取到的是JSON格式的字符串时,可以通过JSON.parse(str)
函数将字符串转换为JSON对象!
2. 服务器向客户端响应数据
当服务器向客户端响应的需要是某些数据,而不是转发或者重定向时,需要在处理请求的方法之前添加@ResponseBody
,而该注解默认是不识别的,在使用该注解之前,需要在Spring的配置文件中添加注解驱动:
<mvc:annotation-driven />
例如处理请求时:
@RequestMapping("/login.do")
@ResponseBody
public String showLogin() {
return "hello, login"; // /WEB-INF/login.jsp
}
则在浏览器中得到的将是"hello, login"
字符串,而不是网页的源代码!
注意:在没有进一步的配置之前,返回的内容中并不支持中文!
以“处理登录”为例,可以为处理请求的方法添加@ResponseBody
注解,然后,调整各种情况下的返回值,无论是哪种情况(成功或失败),都使用JSON语法组织数据:
@RequestMapping(value="/handle_login.do",
method=RequestMethod.POST)
@ResponseBody
public String handleLogin(
@RequestParam("username") String username,
@RequestParam("password") String password,
HttpSession session) {
try {
User user
= userService.login(username, password);
session.setAttribute("uid", user.getId());
session.setAttribute("username", user.getUsername());
return "{ \"state\": 1 }";
} catch (UserNotFoundException e) {
return "{ \"state\": -1, \"message\": \"" + e.getMessage() + "\" }";
} catch (PasswordNotMatchException e) {
return "{ \"state\": -2, \"message\": \"" + e.getMessage() +"\" }";
}
}
3. Jackson框架
使用Jackson框架,可以解决响应的数据可能乱码的问题(本质上是设置了响应头中的响应字符编码),并且,当响应某个对象时,会自动装响应头中的响应类型设置为json类型。
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.9.7</version>
</dependency>
除此以外,Jackson框架还可以将响应的对象自动转换为JSON字符串格式!通常,可以在项目中添加ResponseResult
类:
public class ResponseResult {
private Integer state;
private String message;
public Integer getState() {
return state;
}
public void setState(Integer state) {
this.state = state;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
}
SpringMVC框架对Jackson框架的支持性非常好,添加了Jackson依赖后,无须添加任何配置,也不需要在项目的任何位置显式的使用它。
4. AJAX
4.1. 简介
AJAX是一种异步提交请求并处理响应结果的做法,它本身是基于JavaScript实现的!
通常使用基于jQuery的AJAX处理方式,可以解决不同浏览器的版本兼容问题,并且使得代码的可读性变得更高,所以,通常需要:
<script type="text/javascript" src="${pageContext.request.contextPath }/jquery-3.3.1.min.js"></script>
然后调用jQuery中的ajax()函数来处理AJAX。
在ajax()函数的参数中,至少配置5个属性:
- url:将请求提交到哪个URL
- data:提交的参数
- type:请求的类型
- dataType:服务端响应的数据类型,可以是text、xml、json
- success:当服务器正确响应(200)时如何处理,值是匿名函数,函数的参数就是服务端响应的JSON字符串转换得到的JSON对象
4.2. 小结
使用这种模式开发时,关于控制器端,应该:
1 处理请求的方法需要添加@ResponseBody
注解;
2 处理请求的方法的返回值应该是ResponseResult
;
3 在处理请求的方法内部,无论是成功还是失败,当决定执行响应时,都应该在ResponseResult
对象中封装必要的数据,然后返回ResponseResult
对象。
关于前端,应该:
1 由于需要使用jQuery,所以,必须引入jQuery框架;
2 前端页面不强制需要<form>
,并且,用户的提交操作触发的不是传统的提交表单,而应该触发JS事件,例如:可以将提交按钮的类型调整为type="button"
,并配置onclick="doReg()"
,表示当点击这个按钮时,执行JavaScript中的doReg()
方法;
3 自定义JavaScript函数,且函数名与以上步骤中点击事件对应一致:
<script type="text/javascript">
function doReg() {
}
</script>
4 然后编写函数体,主要任务是调用jQuery框架中的ajax()函数来实现提交请求并获取响应结果:
function doReg() {
// 获取用户输入的数据
var u = $("#username").val();
var p = $("#password").val();
// 处理请求的URL
var url = "handle_reg.do";
// 提交到服务端的参数
var data = "username=" + u + "&password=" + p;
// 发出请求,并获取响应结果
$.ajax({
"url": url,
"data": data,
"type": "post",
"dataType": "json",
"success": function(json) {
// 由于服务端使用了Jackson
// 所以响应头中由Jackson自动设置了响应正文是json
// 所以dataType的值就是"json"
// 且当前成功时的处理函数的参数就是服务端响应的json对象
if (json.state == 1) {
alert("注册成功!");
} else {
alert("注册失败!" + json.message);
}
}
});
}
1. jQuery AJAX
1.1. jQuery框架中的ajax()函数
在应用了jQuery框架后,调用ajax()
函数即可发出AJAX请求,并获取响应结果,该函数的参数必须是JSON对象,通常,在JSON对象中封装的属性有:
-
url:处理请求的URL,也可以理解为将请求发送到哪个URL,取值可以是绝对路径,或相对路径,但是,不可以跨域
-
data:请求的数据,通常是
username=xx&password=xx&phone=xx
类似的格式,可以通过字符串拼接得到该值,也可以通过jQuery中的$("#form-id").serialize()
函数将整个表单中所有控件的值都封装起来,使用这个函数时,是以控件的名称与控件中的值进行封装的,所以,需要注意各控件的名称 -
type:请求的类型,可以是
get
或post
-
dataType:响应的数据的类型,可以是
text
、xml
、json
,主流做法多使用json
,但是,具体使用哪一种,取决于服务器响应时给出的Response Headers中的Content-Type -
beforeSend:发出请求之前如何处理,取值是函数,用于决于具体执行的代码,多用于例如:将发出请求的按钮禁用,以避免在没有响应之前却反复提交请求
-
success:成功响应后如何处理,取值是函数,success对应的函数只在正确的响应情况下被执行,如果服务端的响应码是302、4xx、5xx,并不会导致success对应的函数被执行!该处理函数可以添加参数,参数即是服务端响应的数据,如果
dataType
已经设置为json
,则此处的函数参数类型就是JSON对象 -
error:响应出错如何处理,取值是函数,只要响应码是3xx、4xx、5xx,都会导致error对应的函数被执行!
2. 异常的处理
2.1. 设计案例
创建简单的案例,发出/test/null.do
请求,服务端将产生NullPointerException
,发出/test/array.do
请求,服务端将产生ArrayIndexOutOfBoundsException
。
2.2. 创建并完成案例
创建Maven Project,Group Id使用cn.tedu.spring
,Artifact Id使用SPRINGMVC-04-EXCEPTION
。
检查spring-mvc.xml
的配置,在组件扫描的包下创建TestController
,使用@Controller
和@RequestMapping("/test")
注解:
@Controller
@RequestMapping("/test")
public class TestController {
}
然后添加2个方法处理2个请求,并在处理过程中,使得程序出现对应的异常:
@RequestMapping("/null.do")
public String handleNull() {
String str = null;
str.length();
return null;
}
@RequestMapping("/array.do")
public String handleArray() {
int[] arr = { 0 };
System.out.println(arr[10]);
return null;
}
2.3. 【不推荐】使用SimpleMappingExceptionResolver
如果没有对异常进行处理,默认的处理方式下,会把异常的跟踪信息显示在页面中,这种做法会导致较差的用户体验,并且可能对外泄露了当前的业务逻辑甚至是项目机密。但是,频繁的使用try...catch
语法来处理异常,则很大程度上增加了编码的难度,也不易于统一规划和处理!
SpringMVC中提供了SimpleMappingExceptionResolver
,可以配置异常与转发到的视图的对应关系,也就是说:只要出现了某种异常,就直接转发到某个视图,而不需要编写try...catch
相关代码:
<!-- 处理异常 -->
<bean class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">
<!-- 异常与转发到的视图的映射 -->
<property name="exceptionMappings">
<props>
<prop key="java.lang.NullPointerException">error_null</prop>
<prop key="java.lang.ArrayIndexOutOfBoundsException">error_array</prop>
</props>
</property>
</bean>
使用这种做法最大的问题:无法针对某个问题进行详细的处理!也无法将具体的错误信息显示在页面中!
2.4. 【推荐】使用@ExceptionHandler
在控制器类中自定义某个方法,用于统一处理异常,这个方法需要使用@ExceptionHandler
注解,并且,可以使用Exception
作为方法的参数:
@ExceptionHandler
public String handleException(Exception e) {
return null;
}
每当出现异常时,SpringMVC就会自动调用以上方法,并且将捕获的异常对象作为调用该方法的参数,则处理时:
@ExceptionHandler
public String handleException(
Exception e,
HttpServletRequest request) {
String message = e.getMessage();
if (e instanceof ArrayIndexOutOfBoundsException) {
request.setAttribute("msg", message);
return "err_array";
}
return null;
}
注意:尝试转发数据时,可以使用HttpServletRequest参数,也可以使用ModelAndView返回值,却不可以使用ModelMap参数。
2.5. Q&A
Q:使用@ExceptionHandler和配置SimpleMappingExceptionResolver这2个做法来处理异常会冲突吗?
A:两者同时使用,代码并不会出现错误,但是,对于同一个异常,注解方式优先。
Q:使用@ExceptionHandler这种方式处理异常时,可以不使用转发作为处理方式,而是直接响应JSON数据吗?
A:可以!在处理异常的方法之前添加@ResponseBody
注解,并且将方法的返回值调整为ResponseResult
即可(需要事先添加Jackson依赖,配置注解驱动)
Q:使用@ExceptionHandler这种方式处理异常时,处理异常的方法可以作用于其它控制器类中处理请求方法吗?
A:不可以!常规做法是定义BaseController
类,在这个类中添加处理异常的方法,然后,项目中所有其它的控制器类都继承自这个类!例如:
public abstract class BaseController {
@ExceptionHandler(ServiceException.class)
@ResponseBody
public ResponseResult handleException(
ServiceException e) {
// 准备返回值
ResponseResult rr = new ResponseResult();
// 向返回结果中封装错误信息
rr.setMessage(e.getMessage());
// 判断异常
if (e instanceof UserNotFoundException) {
rr.setState(401);
} else if (e instanceof PasswordNotMatchException) {
rr.setState(402);
} else if (e instanceof UpdateException) {
rr.setState(500);
} else {
rr.setState(600);
}
// 返回
return rr;
}
}
2.6. 小结
在开发项目时,业务层会在出现各种业务错误的情况下抛出不同的业务异常,例如此前项目中的UserNotFoundException
、PasswordNotMatchException
……而控制器层调用业务层的方法时,就需要处理这些异常,而某些异常出现的次数可能较多,反复的使用try...catch
不便于统一处理,代码也显得非常复杂!
SpringMVC提供的统一处理异常的方式有2种,分别是使用SimpleMappingExceptionResolver
和使用@ExceptionHandler
注解,前者在处理方面非常有局限性!一般对于业务异常的处理,推荐使用后者!
关于业务异常的处理,通常会在项目中创建BaseController
,然后在这个类中编写处理异常的方法,项目中实际使用的其它控制器类都应该继承自BaseController
。