不优雅的参数校验
后端对前端传过来的参数也是需要进行校验的,如果在controller中直接校验需要用大量的if else做判断,那么该校验手段就是不优雅的
针对这个普遍的问题,Java开发者在Java API规范 (JSR303) 定义了Bean校验的标准validation-api,但没有提供实现。hibernate validation是对这个规范的实现,并增加了校验注解如@Email、@Length等。
Spring Validation是对hibernate validation的二次封装,用于支持spring mvc参数自动校验。
接下来,我们以springboot项目为例,介绍Spring Validation的使用。
添加pom依赖
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-validation -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
请求参数封装
单一职责,所以将查询用户的参数封装到Param中, 而不是(数据库实体)本身。
对每个参数字段添加validation注解约束和message。
例如一个User类如下:
@Data
@TableName("tb_user")
@ApiModel(value = "TbUser对象", description = "")
@Document(indexName = "TbUserInfo")
public class TbUser implements Serializable {
/*
* type : 字段数据类型
* analyzer : 分词器类型
* index : 是否索引(默认:true)
* Keyword : 短语,不进行分词
*/
/**
* 商品唯一标识 必须有 id,这里的 id 是全局唯一的标识,等同于 es 中的"_id"
*/
private static final long serialVersionUID = 1L;
@TableId(value = "id", type = IdType.AUTO)
private Integer id;
private Integer tenantId;
@Field(type = FieldType.Keyword)
private String userName;
private String password;
private String email;
private Integer phoneNumber;
@Field(type = FieldType.Text, analyzer = "ik_max_word")
private String description;
@JsonDeserialize(using = LocalDateTimeDeserializer.class)
@JsonSerialize(using = LocalDateTimeSerializer.class)
private LocalDateTime createTime;
@JsonDeserialize(using = LocalDateTimeDeserializer.class)
@JsonSerialize(using = LocalDateTimeSerializer.class)
private LocalDateTime updateTime;
@Override
public String toString() {
return "TbUser{" +
"id=" + id +
", tenantId=" + tenantId +
", userName=" + userName +
", password=" + password +
", email=" + email +
", phoneNumber=" + phoneNumber +
", description=" + description +
", createTime=" + createTime +
", updateTime=" + updateTime +
"}";
}
}
为了遵守单一职责规范,创建一个新类UserParam,将查询用户的参数封装到UserParam中
@Data
@Builder
@ApiModel(value = "UserParam")
public class TbUserParam implements Serializable {
@NotNull(message = "id could not be null", groups = {EditValidationGroup.class})
private Integer id;
@NotNull(message = "tenantId could not be null", groups = {EditValidationGroup.class})
private Integer tenantId;
@NotEmpty(message = "email could not be empty")
@Email(message = "invalid email")
private String email;
@NotEmpty(message = "phoneNumber could not be empty")
// @Pattern(regexp = "^(\\d{6})(\\d{4})(\\d{2})(\\d{2})(\\d{3})([0-9]|X)$", message = "invalid identification number")
@Pattern(regexp = "^1[3456789]\\d{9}$", message = "invalid phone")
private String phoneNumber;
@NotEmpty(message = "userName could not be empty")
@Length(min = 1, max = 10, message = "user name should be 1-10")
private String userName;
@NotEmpty(message = "password could not be empty")
@Length(min = 8, max = 16, message = "password should be 8-16")
private String password;
private String description;
public interface AddValidationGroup { }
public interface EditValidationGroup { }
public TbUserParam() {}
@JsonCreator
public TbUserParam(@JsonProperty("id") Integer id,
@JsonProperty("tenantId") Integer tenantId,
@JsonProperty("email") String email,
@JsonProperty("phoneNumber") String phoneNumber,
@JsonProperty("userName") String userName,
@JsonProperty("password") String password,
@JsonProperty("description") String description) {
this.id = id;
this.tenantId = tenantId;
this.email = email;
this.phoneNumber = phoneNumber;
this.userName = userName;
this.password = password;
this.description = description;
}
public void encryptPassword() {
// 使用适当的加密方法对密码进行加密
this.password = BCrypt.hashpw(this.password, BCrypt.gensalt());
}
private static final long SERIALIZATION_ID = 123456789L;
private static final long serialVersionUID = SERIALIZATION_ID;
}
Controller中获取参数绑定结果
@PostMapping("add")
public ResponseEntity<String> add(@Valid @RequestBody UserParam userParam, BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
List<ObjectError> errors = bindingResult.getAllErrors();
errors.forEach(p -> {
FieldError fieldError = (FieldError) p;
log.error("Invalid Parameter : object - {},field - {},errorMessage - {}", fieldError.getObjectName(), fieldError.getField(), fieldError.getDefaultMessage());
});
return ResponseEntity.badRequest().body("invalid parameter");
}
return ResponseEntity.ok("success");
}
但是这样操作,是否依旧感到很繁杂?如果有多个需要校验的接口,那么就需要进行多次if (bindingResult.hasErrors())判断,这样代码很是冗余,因此我们采用AOP思想,将这段代码再次封装。
校验切面处理
@Aspect
@Component
public class ValidAspect {
// 定义一个切点,表示所有被 @PostMapping 注解修饰的方法
@Pointcut("@annotation(org.springframework.web.bind.annotation.PostMapping)")
public void postMapping() {}
// 声明一个增强方法,用于拦截校验出错时的处理逻辑
@Around(value = "postMapping() && args(dto, result)", argNames = "joinPoint,dto,result")
public Object handleValid(ProceedingJoinPoint joinPoint, Object dto, BindingResult result) throws Throwable {
if (result.hasErrors()) {
List<ObjectError> errors = result.getAllErrors();
ErrorResponse errorResponse = new ErrorResponse(errors.stream().map(ObjectError::getDefaultMessage).collect(Collectors.toList()));
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errorResponse);
}
return joinPoint.proceed(); // 继续执行被拦截的方法
}
}
进而就可以优雅的创建多个校验接口
@Slf4j
@Api(value = "User Interfaces", tags = "User Interfaces")
@Controller
@RequestMapping("/details/tbUser")
public class TbUserController {
@Autowired
private ITbUserService iTbUserService;
@PostMapping("add")
@ResponseBody
public ResponseEntity<?> add(@Valid @RequestBody TbUserParam tbuserParam) {
return iTbUserService.addUser(tbuserParam);
}
@GetMapping("selectById/{userId}")
@ResponseBody
public ResponseResult<TbUser> selectById(@PathVariable("userId") String userId) {
return iTbUserService.selectById(userId);
}
@GetMapping("find/{userId}")
@ResponseBody
public ResponseResult<TbUser> findUserById(@PathVariable("userId") String userId) {
return iTbUserService.findUserById(userId);
}
@GetMapping("search/{userName}")
@ResponseBody
public ResponseResult<TbUser> SearchUserInfo(@PathVariable("userName") String userName) {
return iTbUserService.searchUserInfo(userName);
}
@PostMapping("addToRedis")
@ResponseBody
public ResponseEntity<?> addToRedis(@Valid @RequestBody TbUserParam tbuserParam) {
return iTbUserService.addToRedis(tbuserParam);
}
@GetMapping("/sendMessage")
public ResponseEntity<?> sendDirectMessage(@Valid @RequestBody TbUserParam tbuserParam){
return iTbUserService.sendDirectMessage(tbuserParam);
}
@GetMapping("/getMessage")
public ResponseEntity<?> getDirectMessage(){
return iTbUserService.getDirectMessage();
}
}
但是这样又有人会问了,传值过来写入校验的是UserParam类,但我要真正写入和查询的是User类,这又该如何解决呢?因此,我们还需要在Impl层进行copyProperties操作。
但是Impl又要进行copy,又要进行response修改和回应,还可能需要进行redis,rabbitmq,es等的相关操作,岂不是又变的很繁杂了?因此,我们就让Impl专门进行ResponseEntity和ResponseResult的回应,在创建一个新类进行进一步解耦操作。
示例代码如下:
@Service
public class TbUserServiceImpl extends ServiceImpl<TbUserMapper, TbUser> implements ITbUserService {
@Autowired
private TbUserDAO tbUserDAO;
@Override
public ResponseEntity<TbUser> addUser(TbUserParam tbuserParam) {
TbUser user = tbUserDAO.insert(tbuserParam);
return ResponseEntity.ok(user);
}
@Override
public ResponseEntity<TbUser> addToRedis(TbUserParam tbuserParam) {
TbUser user = tbUserDAO.addToRedis(tbuserParam);
return ResponseEntity.ok(user);
}
@Override
public ResponseResult<TbUser> findUserById(String userId) {
TbUser user = tbUserDAO.findUserById(userId);
return ResponseResult.success(user);
}
@Override
public ResponseResult<TbUser> selectById(String userId) {
TbUser user = tbUserDAO.selectById(userId);
return ResponseResult.success(user);
}
@Override
public ResponseResult<TbUser> searchUserInfo(String userName) {
TbUser user = tbUserDAO.searchUserInfo(userName);
return ResponseResult.success(user);
}
@Override
public ResponseEntity<?> sendDirectMessage(TbUserParam tbuserParam) {
TbUser user = tbUserDAO.sendDirectMessage(tbuserParam);
return ResponseEntity.ok(user);
}
@Override
public ResponseEntity<?> getDirectMessage() {
String info = tbUserDAO.getDirectMessage();
if(info == null){
return ResponseEntity.status(HttpStatus.NOT_FOUND).body("没有消息");
}
return ResponseEntity.ok(info);
}
}
现在只需要TbUserDAO将TbUserParam转化为User类进行相应的操作即可。
在TbUserDAO中将具体实现细节设置为private,将必须公开的设置为public。
public TbUser insert(TbUserParam tbuserParam) {
TbUser user = createUser(tbuserParam);
insert(user);
return user;
}
public TbUser addToRedis(TbUserParam tbuserParam){
TbUser user = createUser(tbuserParam);
addToRedis(user);
return user;
}
public TbUser selectById(String userId) {
return findById(userId);
}
public TbUser searchUserInfo(String userName) {return null;}
public TbUser sendDirectMessage(TbUserParam tbuserParam) {
TbUser user = createUser(tbuserParam);
sendDirectMessage(user);
return user;
}
public TbUser findUserById(String userId) {
TbUser user = findUserFromRedis(userId);
if (user == null) {
user = findById(userId);
if (user != null) {
addToRedis(user);
}
}
return user;
}
具体实现用private
private TbUser createUser(TbUserParam tbuserParam) {
TbUser user = new TbUser();
UUID uuid = UUID.randomUUID();
long mostSigBits = uuid.getMostSignificantBits();
//根据UUID的最高位,生成一个1-10的随机数
int uuidInteger = (int) (mostSigBits >> 32);
uuidInteger = uuidInteger>0?uuidInteger:uuidInteger*-1;
tbuserParam.encryptPassword();
BeanUtils.copyProperties(tbuserParam, user);
user.setCreateTime(LocalDateTime.now());
user.setUpdateTime(LocalDateTime.now());
user.setId(uuidInteger);
user.setTenantId(uuidInteger);
return user;
}
private TbUser findById(String userId) {return tbUserMapper.selectById(userId);}
private TbUser findUserFromRedis(String userId) {
return redisTemplate.opsForValue().get(userId);
}
private void insert(TbUser user) {
tbUserMapper.insert(user);
}
private void addToRedis(TbUser user) {
redisTemplate.opsForValue().set(String.valueOf(user.getId()), user);
}
private void sendDirectMessage(TbUser user) {
rabbitTemplate.convertAndSend("TestDirectExchange", "test.topic.a", user, new CorrelationData(UUID.randomUUID().toString()));
}
将业务拆分,解耦,构建中间件等,设置一个优雅的架构和代码风范。
将业务拆分、解耦是指在软件设计、开发过程中,将复杂的业务模块分解成小而简单的模块,使其易于理解和维护。
为了实现业务拆分、解耦,常常需要使用设计模式,如工厂模式、策略模式等,来帮助我们把业务模块逐步分解、组合。拆分后的业务部件或服务之间应该是相互独立的,它们可以被独立开发和测试,并且可以独立部署、运行和维护。
构建中间件是指在系统开发过程中考虑通用性,试图将一些功能性的组件、工具封装起来,并让它们在应用程序中多次重用。这样可以降低系统开发的复杂性,并且提高代码的可重用性和可维护性。例如,数据持久化处理可以使用ORM(对象关系映射)框架,消息传递可以使用MQ(消息队列)等中间件。
设置一个优雅的架构和代码风范是指对于系统架构和代码风格进行规范化的设计和管理。通过统一的编码规范、代码组织方式、模块化的分层架构和松耦合的服务接口设计等方式以提高代码的可读性和可维护性,同时能够方便开发团队进行协作开发,降低项目的维护成本。在这个过程中,需要使用一些常规的设计模式、重构技巧,利用各种先进框架来简化代码编写,以实现系统的高效、稳定运行和快速迭代开发等目标。
如何在springboot中优雅的进行参数的统一校验