当客户端向服务器提交请求时,如果请求数据出现明显的问题(例如关键数据为null
、字符串的长度不在可接受范围内、其它格式错误),应该直接响应错误,而不是将明显错误的请求参数传递到Service!
关于判断错误,只有涉及数据库中的数据才能判断出结果的,都由Service进行判断,而基本的格式判断,都由Controller进行判断。
Validation框架是专门用于解决检查数据基本格式有效性的,最早并不是Spring系列的框架,目前,Spring Boot提供了更好的支持,所以,通常结合在一起使用。
在Spring Boot项目中,需要添加spring-boot-starter-validation
依赖项,例如:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency>
在控制器中,首先,对需要检查数据格式的请求参数添加@Valid
或@Validated
注解(这2个注解没有区别),例如:
@RequestMapping("/add-new") public JsonResult<Void> addNew(@Validated AdminAddNewDTO adminAddNewDTO) { adminService.addNew(adminAddNewDTO); return JsonResult.ok(); }
真正需要检查的是AdminAddNewDTO
中各属性的值,所以,接下来需要在此类的各属性上通过注解来配置检查的规则,例如:
@Data public class AdminAddNewDTO implements Serializable { @NotNull // 验证规则为:不允许为null private String username; // ===== 原有其它代码 ===== }
重启项目,通过不提交用户名的URL(例如:http://localhost:8080/admins/add-new)进行访问,在浏览器上会出现400错误页面,并且,在IntelliJ IDEA的控制台会出现以下警告:
2022-06-07 11:37:53.424 WARN 6404 --- [nio-8080-exec-8] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [ org.springframework.validation.BindException: org.springframework.validation.BeanPropertyBindingResult: 1 errors<EOL>Field error in object 'adminAddNewDTO' on field 'username': rejected value [null]; codes [NotNull.adminAddNewDTO.username,NotNull.username,NotNull.java.lang.String,NotNull]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [adminAddNewDTO.username,username]; arguments []; default message [username]]; default message [不能为null]]
从警告信息中可以看到,当验证失败时(不符合所使用的注解对应的规则时),会出现org.springframework.validation.BindException
异常,则自行处理此异常即可!
首先,在State
中添加新的枚举:
public enum State { OK(200), ERR_USERNAME(201), ERR_PASSWORD(202), ERR_BAD_REQUEST(400), // 新增 ERR_INSERT(500); // ===== 原有其它代码 ===== }
然后,在GlobalExceptionHandler
中添加新的处理异常的方法:
@ExceptionHandler(BindException.class) public JsonResult<Void> handleBindException(BindException e) { return JsonResult.fail(State.ERR_BAD_REQUEST, e.getMessage()); }
完成后,再次重启项目,继续使用为null
的用户名提交请求时,可以看到异常已经被处理,此时,响应的JSON数据例如:
{ "state":400, "message":"org.springframework.validation.BeanPropertyBindingResult: 2 errors\nField error in object 'adminAddNewDTO' on field 'username': rejected value [null]; codes [NotNull.adminAddNewDTO.username,NotNull.username,NotNull.java.lang.String,NotNull]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [adminAddNewDTO.username,username]; arguments []; default message [username]]; default message [不能为null]\nField error in object 'adminAddNewDTO' on field 'password': rejected value [null]; codes [NotNull.adminAddNewDTO.password,NotNull.password,NotNull.java.lang.String,NotNull]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [adminAddNewDTO.password,password]; arguments []; default message [password]]; default message [不能为null]" }
关于错误提示信息,以上内容中出现了不能为null
的字样,是默认的提示文本,可以通过@NotNull
注解的message
属性进行配置,例如:
@Data public class AdminAddNewDTO implements Serializable { @NotNull(message = "添加管理员失败,请提交用户名!") private String username; @NotNull(message = "添加管理员失败,请提交密码!") private String password; // ===== 原有其它代码 ===== }
然后,在处理异常时,通过异常信息获取自定义的提示文本:
@ExceptionHandler(BindException.class) public JsonResult<Void> handleBindException(BindException e) { BindingResult bindingResult = e.getBindingResult(); String defaultMessage = bindingResult.getFieldError().getDefaultMessage(); return JsonResult.fail(State.ERR_BAD_REQUEST, defaultMessage); }
再次运行,在不提交用户名和密码的情况下,会随机的提示用户名或密码验证失败的提示文本中的某1条。
在Validation框架中,还有其它许多注解,用于进行不同格式的验证,例如:
@NotEmpty
:只能添加在String
类型上,不许为空字符串,例如""
即视为空字符串
@NotBlank
:只能添加在String
类型上,不允许为空白,例如普通的空格可视为空白,使用TAB键输入的内容也是空白,(虽然不太可能在此处出现)换行产生的空白区域也是空白
@Size
:限制大小
@Min
:限制最小值
@Max
:限制最大值
@Range
:可以配置min
和max
属性,同时限制最小值和最大值
@Pattern
:只能添加在String
类型上,自行指定正则表达式进行验证
其它
以上注解,包括@NotNull
是允许叠加使用的,即允许在同一个参数属性上添加多个注解!
以上注解均可以配置message
属性,用于指定验证失败的提示文本。
通常:
对于必须提交的属性,都会添加@NotNull
对于数值类型的,需要考虑是否添加@Range
(则不需要使用@Min
和@Max
)
对于字符串类型,都添加@Pattern
注解进行验证
因为现在开发大部分都是前后端分离开发,两个作为独立的项目,独立开发与部署。所以前端直接向后端发送异步请求,前端会出现以下错误
Access to XMLHttpRequest at 'http://localhost:8080/admins/add-new' from origin 'http://localhost:8081' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
这种问题被称为跨域问题,关键字是CORS
基于SpringMVC项目框架中,需要一个SpringMVC的配置类(实现了WebMvcConfigurer接口的类),并重新方法,以允许指定符合条件的进行跨域
package cn.tedu.boot.demo.config; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.CorsRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @Configuration public class SpringMvcConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOriginPatterns("*") .allowedMethods("*") .allowedHeaders("*") .allowCredentials(true) .maxAge(3600); } }
&
拼接各参数与值// username=root&password=123456&nickname=jackson&phone=13800138001&email=jackson@baidu.com&description=none let data = 'username=' + this.ruleForm.username + '&password=' + this.ruleForm.password + '&nickname=' + this.ruleForm.nickname + '&phone=' + this.ruleForm.phone + '&email=' + this.ruleForm.email + '&description=' + this.ruleForm.description;
let data = { 'username': this.ruleForm.username, // 'root' 'password': this.ruleForm.password, // '123456' 'nickname': this.ruleForm.nickname, // 'jackson' 'phone': this.ruleForm.phone, // '13800138001' 'email': this.ruleForm.email, // 'jackson@baidu.com' 'description': this.ruleForm.description // 'none' };
具体使用哪种做法,取决于服务器端的设计:
如果服务器端处理请求的方法中,在参数前添加了@RequestBody
,则允许使用以上第2种做法(JSON数据)提交请求参数,不允许使用以上第1种做法(使用&
拼接)
如果没有使用@RequestBody
,则只能使用以上第1种做法
例如:电商类包含用户、商品、购物车、订单等
例如:用户类型涉及注册、登陆等
简单的易于实现,且可以积累经验
只有先增,还可能查、删、改
只有查了以后,才能明确有哪些数据,才便于实现删、改
删和改相比,删一般更加简单,所以先开发删,再开发改
配置(Spring Boot项目)
使用@MapperScan配置接口所在的根包
在配置文件中通过mybatis.mapper-locations配置XML文件的位置
处理某种类型数据的持久层访问,需要:
创建接口
创建XML文件
SQL大致语句:
select * from ams_admin where username=?
由于不允许使用*号,因此细分为:
select id,username,password,nickname,avatar,is_enable from ams_admin username=?
提示:理论上,还应该查出login_count
,当登录成功后,还应该更新login_count
、gmt_last_login
等数据,此次暂不考虑。
不要使用实体类,根据阿里的开发规范,每张数据表中都应该有id
、gmt_create
、gmt_modified
这3个字段,而gmt_create
、gmt_modified
这2个字段都是用于特殊情况下排查问题的,一般情况下均不会使用,所以,如果使用实体类,必然存在多余的属性,同时,由于不使用星号作为字段列表,则一般也不会查询这2个字段的值,会导致实体类对象中永远至少存在2个属性为null
。
根据以上提示,以前已经写好的getByUsername()
是不规范的,应该调整已存在此方法,本次并不需要添加新的抽象方法。
创建cn.tedu.boot.demo.pojo.vo.AdminSimpleVO
类,添加此次查询时需要的属性:
package cn.tedu.boot.demo.pojo.vo; @Data public class AdminSimpleVO implements Serializable { private Long id; private String username; private String password; private String nickname; private String avatar; private Integer isEnable; }
将Admin getByUsername(String username)改为
AdminSimpleVO getByUsername(String username);
因为修改了源代码,所以调用了原方法的代码都会出现错误,包括:
测试
业务逻辑层的实现类
因及时修改,但是由于未完成SQL配置,相关代码暂时不能运行
删除<sql>
中不必查询的字段,注意:此字段不要有多余的逗号
修改<resultMap>
节点的type属性值
在<resultMap>
节点下,删除不必要的配置
<select id="getByUsername" resultMap="BaseResultMap"> select <include refid="BaseQueryFields" /> from ams_admin where username=#{username} </select> <sql id="BaseQueryFields"> <if test="true"> id, username, password, nickname, avatar, is_enable </if> </sql> <resultMap id="BaseResultMap" type="cn.tedu.boot.demo.pojo.vo.AdminSimpleVO"> <id column="id" property="id" /> <result column="username" property="username" /> <result column="password" property="password" /> <result column="nickname" property="nickname" /> <result column="avatar" property="avatar" /> <result column="is_enable" property="isEnable" /> </resultMap>
此次并不需要编写新的测试,使用原有的测试即可!
注意:由于本次是修改了原“增加管理员”就已经使用的功能,应该检查原功能是否可以正常运行。
如果第1次处理某种类型数据的业务逻辑层访问,需要:
创建接口
创建类,实现接口,并在类上添加@Service
注解
本次需要开发的“管理员登录”并不需要再做以上操作
如果是整个项目第1次开发控制器层,需要:
创建统一处理异常的类
添加@RestControllerAdvice
创建统一的响应结果类型及相关类型
例如:JsonResult
及State
如果第1次处理某种类型数据的控制器层访问,需要:
创建控制器类
添加@RestController
添加@RequestMapping
本次需要开发的“管理员登录”并不需要再做以上操作