看一下前面的代码,这一段:
@RestController
public class ArticleApi {
@PostMapping("/api/savearticle")
public ResponseEntity<?> saveArticle(HttpServletRequest request) {
String title = null, content = null, catalog = null;
Map<String, String[]> map = request.getParameterMap();
if (map.containsKey("title")) {
title = map.get("title")[0];
}
if (map.containsKey("content")) {
content = map.get("content")[0];
}
if (map.containsKey("catalog")) {
catalog = map.get("catalog")[0];
}
Document document = new Document("title", title)
.append("content", content)
.append("catalog", catalog);
DatabaseMan.Instance().GetCollection("article").insertOne(document);
return ResponseEntity.ok("ok");
}
}
这个方法里面,我们用了底层的HttpServletRequest来得到浏览器上传的数据。其中用了3个if判断,做相应的参数检测。这里可以修改的更Spring Boot一点:
- 可以让Spring Boot拿到数据后,直接把数据封装成想要的类的实例,比如用一个数据模型,放置title、content、catalog。而不是现在比较底层的HttpServletRequest。因为这里事实上我们只需要这些数据。
- 可以让Spring Boot帮我们在底层就验证好数据的有效性,这样几个if都不用再写。
来,跟我做!
创建一个Model,对应文章的字段
- 工程中,java目录下建立一个包(文件夹),名字“Model”,用来放置后面的数据类。
- Model文件夹中,创建一个java类,名字"Article"。里面对应的代码全部:
package Model;
public class Article {
private String title;
private String content;
private String catalog;
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
public String getCatalog() {
return catalog;
}
public void setCatalog(String catalog) {
this.catalog = catalog;
}
}
api中使用Model
打开ArticleApi.java,清理成这样:
package API;
import Model.Article;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class ArticleApi {
@PostMapping("/api/savearticle")
public ResponseEntity<?> saveArticle(@RequestBody Article article) {
return ResponseEntity.ok("ok");
}
}
代码清理主要使用了@RequestBody这个注解,这个会让Spring Boot默认把浏览器传过来的数据当做json数据来装配成后端对应的类。
注解修饰了一个Article类,结合刚说的理论,Article中,对应的字段名字必须能和前端送过来的json的名字能对应。这样Spring Boot接收到浏览器的数据后,才能知道哪个数据对应到对象的那个字段。
前端ajax发送数据修改
打开newarticle.js,把原来的$ajax一段有效内容用下面的替换:
var data = {'title': title, 'content': content, 'catalog': catalog};
$.ajax({
type: 'post',
async: true,
data: JSON.stringify(data),
url: document.location.origin + '/api/savearticle',
dataType:'json',
contentType: "application/json; charset=utf-8",
success: function(data) {
console.log("保存成功");
},
error: function () {
console.log("Ajax 发生错误!");
}
});
我们在原来的ajax代码中,
- 添加了conentType字段,指定数据格式是json,编码用utf-8
- dataType从原来的text改成现在的json
- data部分原来的内容用一个data变量存储起来,然后这个data最后必须是json格式,所以用js的stringify()方法处理
这三步完成后,我们就可以调试一下。如果你都跟我一样做的话,放个断点到java的saveArticle()中,能看到参数article能带上自动绑定好的数据。如图一:
图一:自动绑定的数据
到此我们解决掉了第一个问题,数据自动绑定。
剩下的问题是,让Spring Boot在调用我们的方法前,自动验证数据有效性。
利用Maven添加验证库
pom.xml文件中,加入一个依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
整体pom.xml内容:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.shixue</groupId>
<artifactId>PersonalBlog</artifactId>
<version>1.0-SNAPSHOT</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.1.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/org.mongodb/mongo-java-driver -->
<dependency>
<groupId>org.mongodb</groupId>
<artifactId>mongo-java-driver</artifactId>
<version>3.12.6</version>
</dependency>
</dependencies>
</project>
利用注解让Spring Boot帮我们验证
验证库添加好以后,就是考虑怎么验证?Spring Boot的方法是:在要验证的数据上添加对应的注解。
在我们原来的代码中,我们会判断title是不是null,如果不是null,在判断是不是空。这些有了验证库,一个注解就能搞定,而且你只要写注解,验证过程自动由Spring Boot在调用你的方法前就帮你做完(只有验证通过才会调用你的方法,否则就返回错误给浏览器)。
打开Article.java,修改内容成下面:
package Model;
import javax.validation.constraints.NotBlank;
public class Article {
@NotBlank(message = "title cannot be empty")
private String title;
@NotBlank(message = "content cannot be empty")
private String content;
@NotBlank(message = "catalog cannot be empty")
private String catalog;
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
public String getCatalog() {
return catalog;
}
public void setCatalog(String catalog) {
this.catalog = catalog;
}
}
相比较修改前的代码,只是多了三个注解。@NotBlank的意思是“不是null,trim()后长度不是0”。跟着的括弧中的表示如果验证不通过,报告的消息内容。
这是一步,解决哪些字段需要验证。
还有一步,我们打开ArticleApi.java,在@RequestBody前,必须加上@Valid,所以整体的代码会变成:
package API;
import Model.Article;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import javax.validation.Valid;
@RestController
public class ArticleApi {
@PostMapping("/api/savearticle")
public ResponseEntity<?> saveArticle(@Valid @RequestBody Article article) {
return ResponseEntity.ok("ok");
}
}
也就是说,参数article有了@Valid修饰后,Spring Boot就会知道,构造该实例的时候,需要先验证一下里面的字段。而需要验证的字段,都有类似@NotBlank这样的注解修饰。
测试自动验证
按我们原来的代码,测试肯定是没有问题的。客户端会先验证三个字段非空,只有通过才向服务器提交(这是多么好的全栈!)。
为了验证,我们在Article类中添加一个字段,因此整体代码会变成:
package Model;
import javax.validation.constraints.NotBlank;
public class Article {
@NotBlank(message = "title cannot be empty")
private String title;
@NotBlank(message = "content cannot be empty")
private String content;
@NotBlank(message = "catalog cannot be empty")
private String catalog;
@NotBlank(message = "test cannot be empty")
private String test;
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
public String getCatalog() {
return catalog;
}
public void setCatalog(String catalog) {
this.catalog = catalog;
}
public String getTest() {
return test;
}
public void setTest(String test) {
this.test = test;
}
}
运行服务器,然后浏览器在编辑页面都输入点内容,点击保存。结果大致如图二(浏览器)、图三(服务器):
图二:浏览器报的错误
图三:服务器报的错误
浏览器收到了400错误,服务器是一个异常,参数0验证出错(因为test字段为空)。说明我们的验证起了作用。
唯一的问题是,这样在浏览器(客户端)中,知道400错误,但是不知道详细的错误信息,我们给到的@NotBlank信息没有能送到客户端。下面来解决这个事情。
自定义出错信息
- 工程中,java目录下创建一个包(文件夹),名字“Exception”
- Exception包中,创建一个java类文件,名字“CustomExceptionHandler”,里面所有内容如下:
package Exception;
import java.util.ArrayList;
import java.util.List;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.ObjectError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
@SuppressWarnings({"unchecked","rawtypes"})
@ControllerAdvice
public class CustomExceptionHandler extends ResponseEntityExceptionHandler
{
@ExceptionHandler(Exception.class)
public final ResponseEntity<Object> handleAllExceptions(Exception ex, WebRequest request) {
List<String> details = new ArrayList<>();
details.add(ex.getLocalizedMessage());
ErrorResponse error = new ErrorResponse("Server Error", details);
return new ResponseEntity(error, HttpStatus.INTERNAL_SERVER_ERROR);
}
@Override
protected ResponseEntity<Object> handleMethodArgumentNotValid(MethodArgumentNotValidException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
List<String> details = new ArrayList<>();
for(ObjectError error : ex.getBindingResult().getAllErrors()) {
details.add(error.getDefaultMessage());
}
ErrorResponse error = new ErrorResponse("Validation Failed", details);
return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST);
}
}
这个类里面用了一个自定义的ErrorResponse类,马上来创建它。同样的包,创建一个ErrorResponse.java文件,里面内容如下:
package Exception;
import java.util.List;
public class ErrorResponse {
private String message;
private List<String> details;
public ErrorResponse(String message, List<String> details) {
super();
this.message = message;
this.details = details;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public List<String> getDetails() {
return details;
}
public void setDetails(List<String> details) {
this.details = details;
}
}
CustomExceptionHandler继承自ResponseEntityExceptionHandler,并且重写了它的handleMethodArgumentNotValid()方法,Spring Boot如果看到这样的实例,就会在参数验证不通过的时候,调用该方法。由此我们可以做自己想做的事情,比如把自定义的出错信息回给浏览器。
要让Spring Boot看到这样的配置,除了里面的@ControllerAdvice注解外,我们还需要让Spring Boot扫描当前添加的Exception包,所以还得修改Program.java,修改后整体的代码如下:
package DefaultMain;
import Database.DatabaseMan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ComponentScan;
@SpringBootApplication
@ComponentScan({"API", "Exception", "DefaultMain"})
public class Program {
public static void main(String[] args) {
DatabaseMan.Instance().Init();
System.out.println("Spring boot application start");
SpringApplication.run(Program.class, args);
}
}
这样服务器端的工作就准备好了。着急的话,你可以自行先调试一把,结果是客户端还是只能看到400错误。
很正常!因为我们没有在客户端的代码中,把错误信息打印出来。跟我做!
打开newarticle.js,修改error: function() 这里的代码成为这样:
error: function (xhr) {
console.log("Ajax 发生错误: " + xhr.responseText);
}
saveArticle()整体的代码是这样:
function saveArticle() {
var title = $('#article-title').val();
var catalog = $("#catlog-selection").val();
var content = window.editor.getData();
if (title == null || title.trim() == '') {
console.log("题目不能是空的");
return;
}
if (catalog == null || catalog.trim() == '') {
console.log("分类不能是空的");
return;
}
if (content == null || content.trim == '') {
console.log("内容不能是空的");
return;
}
var data = {'title': title, 'content': content, 'catalog': catalog};
$.ajax({
type: 'post',
async: true,
data: JSON.stringify(data),
url: document.location.origin + '/api/savearticle',
dataType:'json',
contentType: "application/json; charset=utf-8",
success: function(data) {
console.log("保存成功");
},
error: function (xhr) {
console.log("Ajax 发生错误: " + xhr.responseText);
}
});
}
好了。一切都准备完毕。运行服务器,调试。我这边客户端的结果如图四:
图四:自定义信息给到了客户端
这一节就到这里。保存功能后面我们再重新添加进来。休息了~