文章目录
- 🍃前言
- 🎍统一数据返回格式
- 🚩快速入门
- 🚩存在问题
- 🎈问题原因
- 🎈代码修改
- 🚩统一格式返回的优点
- 🍀统一异常处理
- 🌲前端代码的修改
- 🚩登录页面
- 🚩图书列表
- 🚩删除图书
- 🚩批量删除图书
- 🚩添加图书
- 🚩获取图书详情
- 🚩修改图书
- ⭕总结
🍃前言
今天我们将对图书管理系统进行收尾工作,今天的开发任务有两个
- 实现统一数据返回格式
- 实现统一异常的处理
🎍统一数据返回格式
在【JavaEE进阶】图书管理系统开发日记——柒实现拦截器时,博主对数据的返回格式其实已经进行了封装
但是如果每一个接口都这样写,岂不太麻烦了一点儿。
其实spring boot为我们提供了统一数据格式返回的功能
🚩快速入门
统⼀的数据返回格式使⽤ @ControllerAdvice 和ResponseBodyAdvice 的⽅式实现@ControllerAdvice 表⽰控制器通知类
添加类 ResponseAdvice ,实现 ResponseBodyAdvice 接⼝,并在类上添加@ControllerAdvice 注解
@ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice {
@Override
public boolean supports(MethodParameter returnType, Class converterType) {
return true;
}
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType,
MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest
request, ServerHttpResponse response) {
return Result.success(body);
}
}
- supports方法:判断是否要执行beforeBodyWrite方法.true为执行,false不执行.通过该⽅法可以选择哪些类或哪些方法的response要进行处理,其他的不进行处理
从returnType获取类名和⽅法名
//获取执⾏的类
Class<?> declaringClass = returnType.getMethod().getDeclaringClass();
//获取执⾏的⽅法
Method method = returnType.getMethod();
- beforeBodyWrite方法:对response方法进行具体操作处理
接下来我们进行测试一下,我们先来看一下没有添加统一功能的时候的登录返回
添加统一功能返回后
但是此时如果运用到我们的项目中,是会出现问题的
🚩存在问题
这里我就不演示错误的情况了
这里直接给出结论,返回结果为String或i为Result类型时会出现错误
🎈问题原因
那么是什么原因造成的呢?
SpringMVC默认会注册⼀些⾃带的 HttpMessageConverter (从先后顺序排列分别为ByteArrayHttpMessageConverter ,
StringHttpMessageConverter , SourceHttpMessageConverter ,SourceHttpMessageConverter,AllEncompassingFormHttpMessageConverter )
public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter
implements BeanFactoryAware, InitializingBean {
//...
public RequestMappingHandlerAdapter() {
this.messageConverters = new ArrayList<>(4);
this.messageConverters.add(new ByteArrayHttpMessageConverter());
this.messageConverters.add(new StringHttpMessageConverter());
if (!shouldIgnoreXml) {
try {
this.messageConverters.add(new SourceHttpMessageConverter<>());
}
catch (Error err) {
// Ignore when no TransformerFactory implementation is available
}
}
this.messageConverters.add(new
AllEncompassingFormHttpMessageConverter());
}
//...
}
其中AllEncompassingFormHttpMessageConverter会根据项⽬依赖情况添加对应的HttpMessageConverter
public AllEncompassingFormHttpMessageConverter() {
if (!shouldIgnoreXml) {
try {
addPartConverter(new SourceHttpMessageConverter<>());
}
catch (Error err) {
// Ignore when no TransformerFactory implementation is available
}
if (jaxb2Present && !jackson2XmlPresent) {
addPartConverter(new Jaxb2RootElementHttpMessageConverter());
}
}
if (kotlinSerializationJsonPresent) {
addPartConverter(new KotlinSerializationJsonHttpMessageConverter());
}
if (jackson2Present) {
addPartConverter(new MappingJackson2HttpMessageConverter());
}
else if (gsonPresent) {
addPartConverter(new GsonHttpMessageConverter());
}
else if (jsonbPresent) {
addPartConverter(new JsonbHttpMessageConverter());
}
if (jackson2XmlPresent && !shouldIgnoreXml) {
addPartConverter(new MappingJackson2XmlHttpMessageConverter());
}
if (jackson2SmilePresent) {
addPartConverter(new MappingJackson2SmileHttpMessageConverter());
}
}
在依赖中引⼊jackson包后,容器会把MappingJackson2HttpMessageConverter 自动注册到
messageConverters 链的末尾.
Spring会根据返回的数据类型,从 messageConverters 链选择合适的HttpMessageConverter .
当返回的数据是非字符串时,使用的MappingJackson2HttpMessageConverter 写⼊返回对象.
当返回的数据是字符串时,StringHttpMessageConverter 会先被遍历到,这时会认为StringHttpMessageConverter 可以使用
public abstract class AbstractMessageConverterMethodProcessor extends
AbstractMessageConverterMethodArgumentResolver
implements HandlerMethodReturnValueHandler {
//...代码省略
protected <T> void writeWithMessageConverters(@Nullable T value,
MethodParameter returnType,
ServletServerHttpRequest inputMessage, ServletServerHttpResponse
outputMessage)
throws IOException, HttpMediaTypeNotAcceptableException,
HttpMessageNotWritableException {
//...代码省略
if (selectedMediaType != null) {
selectedMediaType = selectedMediaType.removeQualityValue();
for (HttpMessageConverter<?> converter : this.messageConverters) {
GenericHttpMessageConverter genericConverter = (converter
instanceof GenericHttpMessageConverter ?
(GenericHttpMessageConverter<?>) converter : null);
if (genericConverter != null ?
((GenericHttpMessageConverter)
converter).canWrite(targetType, valueType, selectedMediaType) :
converter.canWrite(valueType, selectedMediaType)) {
//getAdvice().beforeBodyWrite 执⾏之后, body转换成了Result类型的
结果
body = getAdvice().beforeBodyWrite(body, returnType,
selectedMediaType,
(Class<? extends HttpMessageConverter<?>>)
converter.getClass(),
inputMessage, outputMessage);
if (body != null) {
Object theBody = body;
LogFormatUtils.traceDebug(logger, traceOn ->
"Writing [" + LogFormatUtils.formatValue(theBody,
!traceOn) + "]");
addContentDispositionHeader(inputMessage, outputMessage);
if (genericConverter != null) {
genericConverter.write(body, targetType,
selectedMediaType, outputMessage);
}
else {
//此时cover为StringHttpMessageConverter
((HttpMessageConverter) converter).write(body,
selectedMediaType, outputMessage);
}
}
else {
if (logger.isDebugEnabled()) {
logger.debug("Nothing to write: null body");
}
}
return;
}
}
}
//...代码省略
}
//...代码省略
}
在 ((HttpMessageConverter) converter).write(body, selectedMediaType,outputMessage) 的处理中,调用父类的write方法
由于 StringHttpMessageConverter 重写了addDefaultHeaders方法,所以会执行⼦类的⽅法
然⽽⼦类 StringHttpMessageConverter 的addDefaultHeaders⽅法定义接收参数为String,此时t为Result类型,所以出现类型不匹配"Result cannot be cast to java.lang.String"的异常
🎈代码修改
如果⼀些⽅法返回的结果已经是Result类型了,那就直接返回Result类型的结果即可
如果是String类型,进行处理一下即可
@ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice {
@Autowired
private ObjectMapper objectMapper;
@Override
public boolean supports(MethodParameter returnType, Class converterType) {
return true;
}
@SneakyThrows
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
//在返回之前, 需要做的事情
//body 是返回的结果
if (body instanceof Result){
return body;
}
if (body instanceof String){
return objectMapper.writeValueAsString(Result.success(body));
}
return Result.success(body);
}
}
🚩统一格式返回的优点
- ⽅便前端程序员更好的接收和解析后端数据接⼝返回的数据
- 降低前端程序员和后端程序员的沟通成本,按照某个格式实现就可以了,因为所有接⼝都是这样返回的.
- 有利于项⽬统⼀数据的维护和修改.
- 有利于后端技术部⻔的统⼀规范的标准制定,不会出现稀奇古怪的返回内容
🍀统一异常处理
统⼀异常处理使⽤的是 @ControllerAdvice+@ExceptionHandler 来实现的,
- @ControllerAdvice 表⽰控制器通知类
- @ExceptionHandler 是异常处理器,
两个结合表示当出现异常的时候执行某个通知,也就是执行某个方法事件
简单使用如下:
@ControllerAdvice
@ResponseBody
public class ErrorAdvice {
@ExceptionHandler
public Object handler(Exception e) {
return Result.fail(e.getMessage());
}
}
以上代码表⽰,如果代码出现Exception异常(包括Exception的⼦类),就返回⼀个Result的对象,
Result对象的设置参考博主对返回消息的包装Result.fail(e.getMessage())
public static Result fail(String msg) {
Result result = new Result();
result.setStatus(ResultStatus.FAIL);
result.setErrorMessage(msg);
result.setData("");
return result;
}
现在我们将它应用于项目中,我们需要针对不同的异常进行返回不同的结果
@ResponseBody
@ControllerAdvice
public class ErrorAdvice {
@ExceptionHandler
public Object handler(Exception e) {
return Result.fail(e.getMessage());
}
@ExceptionHandler
public Object handler(NullPointerException e) {
return Result.fail("发⽣NullPointerException:"+e.getMessage());
}
@ExceptionHandler
public Object handler(ArithmeticException e) {
return Result.fail("发⽣ArithmeticException:"+e.getMessage());
}
}
至此我们的图书管理系统后端开发已经完毕了,接下来由于我们统一了数据返回格式,所以我们需要对前端代码进行修改
🌲前端代码的修改
🚩登录页面
登录界⾯没有拦截,只是返回结果发⽣了变化,所以只需要根据返回结果修改对应代码即可
登录结果代码修改
function login() {
$.ajax({
type: "post",
url: "/user/login",
data: {
name: $("#userName").val(),
password: $("#password").val()
},
success: function (result) {
if (result.status=="SUCCESS" && result.data==true) {
location.href = "book_list.html";
} else {
alert("账号或密码不正确!");
}
}
});
}
🚩图书列表
针对图书列表⻚有两处变化
- 拦截器进行了强制登录校验,如果校验失败,则http状态码返回401,此时会⾛ajax的error逻辑处理
- 接⼝返回结果发生了变化
图书列表代码修改:
function getBookList() {
$.ajax({
type: "get",
url: "/book/getListByPage"+location.search,
success: function (result) {
console.log(result);
if (result == null || result.data == null) {
location.href = "login.html";
return;
}
if (result != null) {
var finalHtml = "";
var data = result.data;
for (var book of data.records) {
finalHtml += '<tr>';
finalHtml += '<td><input type="checkbox" name="selectBook" value="'+book.id+'" id="selectBook" class="book-select"></td>'
finalHtml += '<td>' + book.id + '</td>';
finalHtml += '<td>' + book.bookName + '</td>';
finalHtml += '<td>' + book.author + '</td>';
finalHtml += '<td>' + book.count + '</td>';
finalHtml += '<td>' + book.price + '</td>';
finalHtml += '<td>' + book.publish + '</td>';
finalHtml += '<td>' + book.statusCN + '</td>';
finalHtml += '<td><div class="op">';
finalHtml += '<a href="book_update.html?bookId=' + book.id + '">修改</a>'
finalHtml += '<a href="javascript:void(0)" onclick="deleteBook(' + book.id + ')">删除</a>'
finalHtml += '</div></td>';
finalHtml += "</tr>";
}
$("tbody").html(finalHtml);
//翻页信息
$("#pageContainer").jqPaginator({
totalCounts: data.total, //总记录数
pageSize: 10, //每页的个数
visiblePages: 5, //可视页数
currentPage: data.pageRequest.currentPage, //当前页码
first: '<li class="page-item"><a class="page-link">首页</a></li>',
prev: '<li class="page-item"><a class="page-link" href="javascript:void(0);">上一页<\/a><\/li>',
next: '<li class="page-item"><a class="page-link" href="javascript:void(0);">下一页<\/a><\/li>',
last: '<li class="page-item"><a class="page-link" href="javascript:void(0);">最后一页<\/a><\/li>',
page: '<li class="page-item"><a class="page-link" href="javascript:void(0);">{{page}}<\/a><\/li>',
//页面初始化和页码点击时都会执行
onPageChange: function (page, type) {
console.log("第" + page + "⻚, 类型:" + type);
if (type != 'init') {
location.href = "book_list.html?currentPage=" + page;
}
}
});
}
},
error(error) {
if(error.status == 401) {
location.href = "login.html";
}
}
});
}
🚩删除图书
function deleteBook(id) {
//...代码省略
success: function (result) {
if(result.status=="SUCCESS" || result.data==""){
//重新刷新⻚⾯
location.href = "book_list.html"
}else{
alert(result.data);
}
},
error: function (error) {
if (error != null && error.status == 401) {
//⽤⼾未登录
location.href = "login.html";
}
}
//...代码省略
}
🚩批量删除图书
function batchDelete() {
var isDelete = confirm("确认批量删除?");
if (isDelete) {
//获取复选框的id
var ids = [];
$("input:checkbox[name='selectBook']:checked").each(function () {
ids.push($(this).val());
});
console.log(ids);
//批量删除
$.ajax({
type: "post",
url: "/book/batchDeleteBook?ids="+ids,
success: function (result) {
if (result.status=="SUCCESS" || result.data==true) {
alert("删除成功");
//重新刷新⻚⾯
location.href = "book_list.html"
}
},
error: function (error) {
if (error != null && error.status == 401) {
//⽤⼾未登录
location.href = "login.html";
}
}
});
}
}
🚩添加图书
function add() {
$.ajax({
type: "post",
url: "/book/addBook",
data: $("#addBook").serialize(),
success: function (result) {
console.log(result);
console.log(result.data);
if (result.status == "SUCCESS" && result.data == "") {
location.href = "book_list.html"
} else {
console.log(result);
alert("添加失败:" + result.data);
}
},
error: function (error) {
if (error != null && error.status == 401) {
//⽤⼾未登录
alert("⽤⼾未登录");
location.href = "login.html";
}
}
});
}
🚩获取图书详情
$.ajax({
type:"get",
url: "/book/queryBookById"+location.search,
success:function(book){
if (result.status == "SUCCESS" && result.data != null) {
var book = result.data;
if (book != null) {
$("#bookId").val(book.id);
$("#bookName").val(book.bookName);
$("#bookAuthor").val(book.author);
$("#bookStock").val(book.count);
$("#bookPrice").val(book.price);
$("#bookPublisher").val(book.publish);
$("#bookStatus").val(book.status);
}
}
},
error: function (error) {
if (error != null && error.status == 401) {
//⽤⼾未登录
alert("⽤⼾未登录");
location.href = "login.html";
}
}
});
🚩修改图书
function update() {
$.ajax({
type: "post",
url: "/book/updateBook",
data: $("#updateBook").serialize(),
success: function (result) {
if (result.status == "SUCCESS" || result.data == "") {
location.href = "book_list.html"
} else {
console.log(result);
alert("修改失败:" + result.data);
}
},
error: function (error) {
if (error != null && error.status == 401) {
//⽤⼾未登录
location.href = "login.html";
}
}
});
}
⭕总结
关于《【JavaEE进阶】 图书管理系统开发日记——捌》就讲解到这儿,图书管理系统到此也就开发完毕了,感谢大家的支持,欢迎各位留言交流以及批评指正,如果文章对您有帮助或者觉得作者写的还不错可以点一下关注,点赞,收藏支持一下!