和 if else说再见,SpringBoot 这样做参数校验才足够优雅!

大家好,我是老赵!

一、概述

当我们想提供可靠的 API 接口,对参数的校验,以保证最终数据入库的正确性,是 必不可少 的活。比如下图就是 我们一个项目里 新增一个菜单校验 参数的函数,写了一大堆的 if else 进行校验,非常的不优雅,比起枯燥的CRUD来说,参数校验更是枯燥。

这只是一个创建菜单的校验,只需要判断菜单,菜单url 以及菜单的父类id是否为空,上级菜单是否挂载正确,这样已经消耗掉了30,40行代码了,更不要说,管理后台创建商品这种参数贼多的接口。估计要写几百行校验代码了。

/*** 验证参数是否正确*/private void verifyForm(SysMenuEntity menu){if(StringUtils.isBlank(menu.getName())){throw new RRException("菜单名称不能为空");}if(menu.getParentId() == null){throw new RRException("上级菜单不能为空");}//菜单if(menu.getType() == Constant.MenuType.MENU.getValue()){if(StringUtils.isBlank(menu.getUrl())){throw new RRException("菜单URL不能为空");}}//上级菜单类型int parentType = Constant.MenuType.CATALOG.getValue();if(menu.getParentId() != 0){SysMenuEntity parentMenu = sysMenuService.getById(menu.getParentId());parentType = parentMenu.getType();}//目录、菜单if(menu.getType() == Constant.MenuType.CATALOG.getValue() ||menu.getType() == Constant.MenuType.MENU.getValue()){if(parentType != Constant.MenuType.CATALOG.getValue()){throw new RRException("上级菜单只能为目录类型");}return ;}//按钮if(menu.getType() == Constant.MenuType.BUTTON.getValue()){if(parentType != Constant.MenuType.MENU.getValue()){throw new RRException("上级菜单只能为菜单类型");}return ;}}

可能小伙伴会说不加参数校验行不行?或者把参数校验放到前端不就行了?那你可是想的太简单了,世界比我们想象中的不安全,可能有“黑客”会绕过浏览器,直接使用 HTTP 工具,模拟请求向后端 API 接口传入违法的参数,以达到它们 “不可告人” 的目的。比如 sql 注入攻击,我相信,很多时候并不是我们不想添加,而是没有统一方便的方式,让我们快速的添加实现参数校验的功能。

世界上大多数碰到的困难,大多已经有了解决方案,特别是开发生态非常完整的java来说,早在 Java 2009 年就提出了 Bean Validation 规范,并且已经历经 JSR303、JSR349、JSR380 三次标准的置顶,发展到了 2.0 。

8e3060ec4526485b3570c6b426655f72.png

有细心的小伙伴可能发现 Jakarta Bean Validation 3.0 里,这里3.0变化并不大,只是更改了一下包名 和命名空间而已。实际上还是对 Bean Validation 2.0 的实现。

d35dfe53204ffeb04fde257cc7598732.png

Bean Validation 和我们很久以前学习过的 JPA 一样,只提供规范,不提供具体的实现,目前实现 Bean Validation 规范的数据校验框架,主要有:

  • Hibernate Validator

  • Apache BVal

可能有小伙伴就要说 Hibernate 不就是个老掉牙的ORM框架吗?现在不都没人用了吗?其实不然 Hibernate 可是打着 Everything data 口号的,它还提供了 Hibernate Search、Hibernate OGM 等等解决方案的。

Hibernate 只是在国内的用的少了,国内主要是还是用 mybatis 这种 半orm框架的多。我们可以通过 google 的 trends 来看一下:

在中国的 mybatis, jpa, Hibernate 搜索热度:

68658133d2b3aac647392b99497fb008.png

在全球的 mybatis, jpa, Hibernate 搜索热度:

32e244cec0c47da895106a46522e8a6d.png

由于国内的开发环境可以说 99.99% 的开发者肯定都在用 spring,且正好 Spring Validation 提供了对 Bean Validation 的内置封装支持,可以使用 @Validated 注解,实现声明式校验,而无需直接调用 Bean Validation 提供的 API 方法。

而在实现原理上,也是基于 Spring AOP 拦截,最终还是调用不同的 Bean Validation 的实现框架。例如说,Hibernate Validator 。实现校验相关的操作这一点,类似 Spring Transaction 事务,通过 @Transactional 注解,实现声明式事务。下面,让我们开始学习如何在 Spring Boot 中,实现参数校验。

二、注解

在开始入门之前,我们先了解下本文可能会涉及到的注解。javax.validation.constraints 包下,定义了一系列的约束( constraint )注解。共 22个,如下:

4d39dab2c7d03a88f35fc71f28a9830b.png

大致可以分为以下几类:

2.1 空和非空检查

  • @NotBlank :只能用于字符串不为 null ,并且字符串 #trim() 以后 length 要大于 0 。

  • @NotEmpty :集合对象的元素不为 0 ,即集合不为空,也可以用于字符串不为 null 。

  • @NotNull :不能为 null 。

  • @Null :必须为 null 。

2.2 数值检查

  • @DecimalMax(value) :被注释的元素必须是一个数字,其值必须小于等于指定的最大值。

  • @DecimalMin(value) :被注释的元素必须是一个数字,其值必须大于等于指定的最小值。

  • @Digits(integer, fraction) :被注释的元素必须是一个数字,其值必须在可接受的范围内。

  • @Positive :判断正数。

  • @PositiveOrZero :判断正数或 0 。

  • @Max(value) :该字段的值只能小于或等于该值。

  • @Min(value) :该字段的值只能大于或等于该值。- @Negative :判断负数。

  • @NegativeOrZero :判断负数或 0 。

2.3 Boolean 值检查

  • @AssertFalse :被注释的元素必须为 true 。

  • @AssertTrue :被注释的元素必须为 false 。

2.4 长度检查

  • @Size(max, min) :检查该字段的 size 是否在 min 和 max 之间,可以是字符串、数组、集合、Map 等。

2.5 日期检查

  • @Future :被注释的元素必须是一个将来的日期。

  • @FutureOrPresent :判断日期是否是将来或现在日期。

  • @Past :检查该字段的日期是在过去。

  • @PastOrPresent :判断日期是否是过去或现在日期。

2.6 其它检查

  • @Email :被注释的元素必须是电子邮箱地址。

  • @Pattern(value) :被注释的元素必须符合指定的正则表达式。

2.7 Hibernate Validator 附加的约束注解

org.hibernate.validator.constraints 包下,定义了一系列的约束( constraint )注解。如下:

  • @Range(min=, max=) :被注释的元素必须在合适的范围内。

  • @Length(min=, max=) :被注释的字符串的大小必须在指定的范围内。

  • @URL(protocol=,host=,port=,regexp=,flags=) :被注释的字符串必须是一个有效的 URL 。

  • @SafeHtml :判断提交的 HTML 是否安全。例如说,不能包含 javascript 脚本等等。

3c6390812a979e0926e5a9f5652549ef.png

2.8 @Valid 和 @Validated

@Valid 注解,是 Bean Validation 所定义,可以添加在普通方法、构造方法、方法参数、方法返回、成员变量上,表示它们需要进行约束校验。

@Validated 注解,是 Spring Validation 锁定义,可以添加在类、方法参数、普通方法上,表示它们需要进行约束校验。同时,@Validated 有 value 属性,支持分组校验。属性如下:

@Target({ElementType.TYPE, ElementType.METHOD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Validated {/*** Specify one or more validation groups to apply to the validation step* kicked off by this annotation.* <p>JSR-303 defines validation groups as custom annotations which an application declares* for the sole purpose of using them as type-safe group arguments, as implemented in* {@link org.springframework.validation.beanvalidation.SpringValidatorAdapter}.* <p>Other {@link org.springframework.validation.SmartValidator} implementations may* support class arguments in other ways as well.*/Class<?>[] value() default {};}

对于初学的胖友来说,很容易搞混 @Valid(javax.validation包下) 和 @Validated (org.springframework.validation.annotation包下)注解。两者大致有以下的区别:

db7cdb51202504380e978a0610b5196f.png

@Valid 有嵌套对象校验功能 例如说:如果不在 User.profile 属性上,添加 @Valid 注解,就会导致 UserProfile.nickname 属性,不会进行校验。

// User.java
public class User {private String id;@Validprivate UserProfile profile;}// UserProfile.java
public class UserProfile {@NotBlankprivate String nickname;}

总的来说,绝大多数场景下,我们使用 @Validated 注解即可。而在有嵌套校验的场景,我们使用 @Valid 注解添加到成员属性上。

三、快速入门

3.1 引入依赖

<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.2.4.RELEASE</version><relativePath/> <!-- lookup parent from repository --></parent><groupId>com.ratel</groupId><artifactId>java-validation</artifactId><version>0.0.1-SNAPSHOT</version><name>java-validation</name><description>java validation action</description><properties><java.version>1.8</java.version></properties><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><!--在一些高版本springboot中默认并不会引入这个依赖,需要手动引入-->
<!--        <dependency><groupId>org.hibernate.validator</groupId><artifactId>hibernate-validator</artifactId><scope>compile</scope></dependency>--><!-- 保证 Spring AOP 相关的依赖包 --><dependency><groupId>org.springframework</groupId><artifactId>spring-aspects</artifactId></dependency><!--lombok相关 方便开发--><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><scope>provided</scope></dependency><!--knife4j接口文档 方便待会进行接口测试--><dependency><groupId>com.github.xiaoymin</groupId><artifactId>knife4j-spring-boot-starter</artifactId><version>3.0.3</version></dependency></dependencies><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build></project>

在 Spring Boot 体系中,也提供了 spring-boot-starter-validation 依赖。在这里,我们并没有引入。为什么呢?因为 spring-boot-starter-web 已经引入了 spring-boot-starter-validation ,而 spring-boot-starter-validation 中也引入了 hibernate-validator 依赖,所以无需重复引入。三者的依赖引入关系可见下图

73cb6d49674ffdfd1bd05c0894fe64b3.png

3.2 创建基本的类

UserAddDTO 实体类:

package com.ratel.validation.entity;import lombok.Data;
import org.hibernate.validator.constraints.Length;import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.Pattern;/*** @Description* @Author ratelfu* @Date 2023/04/07* @Version 1.0*/
@Data
public class UserAddDTO {/*** 账号*/@NotEmpty(message = "登录账号不能为空")@Length(min = 5, max = 16, message = "账号长度为 5-16 位")@Pattern(regexp = "^[A-Za-z0-9]+$", message = "账号格式为数字以及字母")private String username;/*** 密码*/@NotEmpty(message = "密码不能为空")@Length(min = 4, max = 16, message = "密码长度为 4-16 位")private String password;
}

UserController 用来写接口,在类上,添加 @Validated 注解,表示 UserController 是所有接口都需要进行参数校验。

package com.ratel.validation.cotroller;import com.ratel.validation.entity.UserAddDTO;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;import javax.validation.Valid;
import javax.validation.constraints.Min;@RestController
@RequestMapping("/users")
@Validated
public class UserController {private Logger logger = LoggerFactory.getLogger(getClass());@GetMapping("/get")public UserAddDTO get(@RequestParam("id") @Min(value = 1L, message = "编号必须大于 0") Integer id) {logger.info("[get][id: {}]", id);UserAddDTO userAddDTO = new UserAddDTO();userAddDTO.setUsername("张三");userAddDTO.setPassword("123456");return userAddDTO;}@PostMapping("/add")public void add(@Valid @RequestBody UserAddDTO addDTO) {logger.info("[add][addDTO: {}]", addDTO);}}

3.3 启动程序,进行测试

启动程序,然后再浏览器里我们就可以进行输入: swagger访问地址: http://localhost:8080/doc.html#/home 打开swagger文档 就可以进行测试了:

af408115e7bcb36c2b21847d931c405f.png

首先我们访问 http://localhost:8080/users/get?id=-1 进行测试,查看返回结果,果然对我们的 id 进行校验。

617accda74534c3f0b15c5c7ecb004d8.png

接下来我们访问 http://localhost:8080/users/add 进行新增用户的校验:请求体我们写成:

{"password": "233","username": "33"
}

然后返回结果的如下:

{"timestamp": "2023-04-09T13:33:58.864+0000","status": 400,"error": "Bad Request","errors": [{"codes": ["Length.userAddDTO.password","Length.password","Length.java.lang.String","Length"],"arguments": [{"codes": ["userAddDTO.password","password"],"arguments": null,"defaultMessage": "password","code": "password"},16,4],"defaultMessage": "密码长度为 4-16 位","objectName": "userAddDTO","field": "password","rejectedValue": "233","bindingFailure": false,"code": "Length"},{"codes": ["Length.userAddDTO.username","Length.username","Length.java.lang.String","Length"],"arguments": [{"codes": ["userAddDTO.username","username"],"arguments": null,"defaultMessage": "username","code": "username"},16,5],"defaultMessage": "账号长度为 5-16 位","objectName": "userAddDTO","field": "username","rejectedValue": "33","bindingFailure": false,"code": "Length"}],"message": "Validation failed for object='userAddDTO'. Error count: 2","path": "/users/add"
}

返回结果的json串中的 errors 字段,参数错误明细数组。每一个数组元素,对应一个参数错误明细。这里,username 违背了 账号长度为 5-16 位规定。password违反了 密码长度为 4-16 位的规定。

返回结果示意图:

6feaac3c652f0b323575d08191d0c5c7.png

3.4 一些疑问

在这里 细心的小伙伴可能会有几个疑问:

3.4.1 疑问一

#get(id) 方法上,我们并没有给 id 添加 @Valid 注解,而 #add(addDTO) 方法上,我们给 addDTO 添加 @Valid 注解。这个差异,是为什么呢?

因为 UserController 使用了 @Validated 注解,那么 Spring Validation 就会使用 AOP 进行切面,进行参数校验。而该切面的拦截器,使用的是 MethodValidationInterceptor 。

  • 对于 #get(id) 方法,需要校验的参数 id ,是平铺开的,所以无需添加 @Valid 注解。

  • 对于 #add(addDTO) 方法,需要校验的参数 addDTO ,实际相当于嵌套校验,要校验的参数的都在 addDTO 里面,所以需要添加 @Valid (其实实测加@Validated也行,暂时不知道为啥 为了好区分就先用 @Valid 吧 )注解。

3.4.2 疑问二

#get(id) 方法的返回的结果是 status = 500 ,而 #add(addDTO) 方法的返回的结果是 status = 400 。

  • 对于 #get(id) 方法,在 MethodValidationInterceptor 拦截器中,校验到参数不正确,会抛出 ConstraintViolationException 异常。

  • 对于 #add(addDTO) 方法,因为 addDTO 是个 POJO 对象,所以会走 SpringMVC 的 DataBinder 机制,它会调用 DataBinder#validate(Object... validationHints) 方法,进行校验。在校验不通过时,会抛出 BindException 。

在 SpringMVC 中,默认使用 DefaultHandlerExceptionResolver 处理异常。

  • 对于 BindException 异常,处理成 400 的状态码。

  • 对于 ConstraintViolationException 异常,没有特殊处理,所以处理成 500 的状态码。

这里,我们在抛个问题,如果 #add(addDTO) 方法,如果参数正确,在走完 DataBinder 中的参数校验后,会不会在走一遍 MethodValidationInterceptor 的拦截器呢?思考 100 毫秒…

答案是会。这样,就会导致浪费。所以 Controller 类里,如果 只有 类似的 #add(addDTO) 方法的 嵌套校验,那么我可以不在 Controller 类上添加 @Validated 注解。从而实现,仅使用 DataBinder 中来做参数校验。

3.4.3 返回提示很不友好,太长了

第三点,无论是 #get(id) 方法,还是 #add(addDTO) 方法,它们的返回提示都非常不友好,那么该怎么办呢?我们将在 第四章节通过 处理校验异常 进行处理。

四、处理校验异常

4.1 校验不通过的枚举类

package com.ratel.validation.enums;/*** 业务异常枚举*/
public enum ServiceExceptionEnum {// ========== 系统级别 ==========SUCCESS(0, "成功"),SYS_ERROR(2001001000, "服务端发生异常"),MISSING_REQUEST_PARAM_ERROR(2001001001, "参数缺失"),INVALID_REQUEST_PARAM_ERROR(2001001002, "请求参数不合法"),// ========== 用户模块 ==========USER_NOT_FOUND(1001002000, "用户不存在"),// ========== 订单模块 ==========// ========== 商品模块 ==========;/*** 错误码*/private final int code;/*** 错误提示*/private final String message;ServiceExceptionEnum(int code, String message) {this.code = code;this.message = message;}public int getCode() {return code;}public String getMessage() {return message;}}

4.2 统一返回结果实体类

package com.ratel.validation.common;import com.fasterxml.jackson.annotation.JsonIgnore;
import org.springframework.util.Assert;import java.io.Serializable;/*** 通用返回结果** @param <T> 结果泛型*/
public class CommonResult<T> implements Serializable {public static Integer CODE_SUCCESS = 0;/*** 错误码*/private Integer code;/*** 错误提示*/private String message;/*** 返回数据*/private T data;/*** 将传入的 result 对象,转换成另外一个泛型结果的对象** 因为 A 方法返回的 CommonResult 对象,不满足调用其的 B 方法的返回,所以需要进行转换。** @param result 传入的 result 对象* @param <T> 返回的泛型* @return 新的 CommonResult 对象*/public static <T> CommonResult<T> error(CommonResult<?> result) {return error(result.getCode(), result.getMessage());}public static <T> CommonResult<T> error(Integer code, String message) {Assert.isTrue(!CODE_SUCCESS.equals(code), "code 必须是错误的!");CommonResult<T> result = new CommonResult<>();result.code = code;result.message = message;return result;}public static <T> CommonResult<T> success(T data) {CommonResult<T> result = new CommonResult<>();result.code = CODE_SUCCESS;result.data = data;result.message = "";return result;}public Integer getCode() {return code;}public void setCode(Integer code) {this.code = code;}public String getMessage() {return message;}public void setMessage(String message) {this.message = message;}public T getData() {return data;}public void setData(T data) {this.data = data;}@JsonIgnorepublic boolean isSuccess() {return CODE_SUCCESS.equals(code);}@JsonIgnorepublic boolean isError() {return !isSuccess();}@Overridepublic String toString() {return "CommonResult{" +"code=" + code +", message='" + message + '\'' +", data=" + data +'}';}}

4.3 增加全局异常处理类 GlobalExceptionHandler

package com.ratel.validation.exception;import com.ratel.validation.common.CommonResult;
import com.ratel.validation.enums.ServiceExceptionEnum;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.validation.BindException;
import org.springframework.validation.ObjectError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;import javax.servlet.http.HttpServletRequest;
import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;@ControllerAdvice(basePackages = "com.ratel.validation.cotroller")
public class GlobalExceptionHandler {private Logger logger = LoggerFactory.getLogger(getClass());/*** 处理 MissingServletRequestParameterException 异常** SpringMVC 参数不正确*/@ResponseBody@ExceptionHandler(value = MissingServletRequestParameterException.class)public CommonResult missingServletRequestParameterExceptionHandler(HttpServletRequest req, MissingServletRequestParameterException ex) {logger.error("[missingServletRequestParameterExceptionHandler]", ex);// 包装 CommonResult 结果return CommonResult.error(ServiceExceptionEnum.MISSING_REQUEST_PARAM_ERROR.getCode(),ServiceExceptionEnum.MISSING_REQUEST_PARAM_ERROR.getMessage());}@ResponseBody@ExceptionHandler(value = ConstraintViolationException.class)public CommonResult constraintViolationExceptionHandler(HttpServletRequest req, ConstraintViolationException ex) {logger.error("[constraintViolationExceptionHandler]", ex);// 拼接错误StringBuilder detailMessage = new StringBuilder();for (ConstraintViolation<?> constraintViolation : ex.getConstraintViolations()) {// 使用 ; 分隔多个错误if (detailMessage.length() > 0) {detailMessage.append(";");}// 拼接内容到其中detailMessage.append(constraintViolation.getMessage());}// 包装 CommonResult 结果return CommonResult.error(ServiceExceptionEnum.INVALID_REQUEST_PARAM_ERROR.getCode(),ServiceExceptionEnum.INVALID_REQUEST_PARAM_ERROR.getMessage() + ":" + detailMessage.toString());}@ResponseBody@ExceptionHandler(value = BindException.class)public CommonResult bindExceptionHandler(HttpServletRequest req, BindException ex) {logger.info("========进入了 bindException======");logger.error("[bindExceptionHandler]", ex);// 拼接错误StringBuilder detailMessage = new StringBuilder();for (ObjectError objectError : ex.getAllErrors()) {// 使用 ; 分隔多个错误if (detailMessage.length() > 0) {detailMessage.append(";");}// 拼接内容到其中detailMessage.append(objectError.getDefaultMessage());}// 包装 CommonResult 结果return CommonResult.error(ServiceExceptionEnum.INVALID_REQUEST_PARAM_ERROR.getCode(),ServiceExceptionEnum.INVALID_REQUEST_PARAM_ERROR.getMessage() + ":" + detailMessage.toString());}@ResponseBody@ExceptionHandler(value = MethodArgumentNotValidException.class)public CommonResult MethodArgumentNotValidExceptionHandler(HttpServletRequest req, MethodArgumentNotValidException ex) {logger.info("-----------------进入了 MethodArgumentNotValidException-----------------");logger.error("[MethodArgumentNotValidException]", ex);// 拼接错误StringBuilder detailMessage = new StringBuilder();for (ObjectError objectError : ex.getBindingResult().getAllErrors()) {// 使用 ; 分隔多个错误if (detailMessage.length() > 0) {detailMessage.append(";");}// 拼接内容到其中detailMessage.append(objectError.getDefaultMessage());}// 包装 CommonResult 结果return CommonResult.error(ServiceExceptionEnum.INVALID_REQUEST_PARAM_ERROR.getCode(),ServiceExceptionEnum.INVALID_REQUEST_PARAM_ERROR.getMessage() + ":" + detailMessage.toString());}/*** 处理其它 Exception 异常* @param req* @param e* @return*/@ResponseBody@ExceptionHandler(value = Exception.class)public CommonResult exceptionHandler(HttpServletRequest req, Exception e) {// 记录异常日志logger.error("[exceptionHandler]", e);// 返回 ERROR CommonResultreturn CommonResult.error(ServiceExceptionEnum.SYS_ERROR.getCode(),ServiceExceptionEnum.SYS_ERROR.getMessage());}
}

4.4 测试

访问:http://localhost:8080/users/add 可以看到异常结果已经被拼接成一个字符串,相比之前清新 易懂了不少。

7584a9e4d9139c71ab3c80d5cd9884b7.png

a010e6fb8a216787f5ebbb92652d245f.png

作者:T-OPEN

来源:blog.csdn.net/weter_drop/article/

details/130046637

 
 

精品推荐

1.jenkins 真得很牛逼!只是大部分人不会用而已~(保姆级教程)
2.这才是 SpringBoot 统一登录鉴权、异常处理、数据格式 的正确姿势
3.这次被 foreach 坑惨了,再也不敢乱用了....
4.优雅的接口防刷处理方案
5.重磅官宣:阿里版 ChatGPT 突然发布!
6.我们公司用了 5 年的单点登录方案!从实现到部署实战详解(稳的一批),绝了!
7.从阿里跳槽来的工程师,写个try catch的方式都这么优雅!
8.MyBatis-Plus 还手写 Join 联表查询?一个依赖轻松搞定,真香

d376ce55041450accc13aef3dae10a26.png

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/49866.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

全球诺贝尔奖得主最多的30所大学

自1901年以来&#xff0c;诺贝尔奖得主全球最多的30所大学&#xff0c;这些大学堪称是真正的世界一流大学。世界一流大学的指标很多&#xff0c;但是有一项重要指标不可缺失&#xff0c;那就是至少有10位以上诺贝尔奖得主。以下是笔者根据维基百科整理的1901年至2018年间&#…

【娜家花园养花小记】

种花的话&#xff0c;看花开花落&#xff0c;经历寒冬酷暑&#xff0c;都是生命的一个体验的过程。月季花很坚强&#xff0c;酷暑来了&#xff0c;寒冬来了&#xff0c;它休眠一下。然后在其他时间呢&#xff0c;它就尽情的拿生命去绽放。种花更多的感受它带给你的快乐。带给你…

一生必看的 100 幅世界名画

智慧与美&#xff0c;是我之最爱。 从早期的叙事性绘画&#xff0c;直至后期更加侧重抒情与抽象的现代派绘画。希望这篇用心的长文&#xff0c;可以成为你开启艺术之们的钥匙。如果有幸有一幅画面&#xff0c;能够触及你内心柔软的角落抑或隐秘的激情&#xff0c;也请你静下心来…

2022-09-11 stonedb-宣讲-第二讲-一条SQL在Tianmu引擎中的运行

摘要: 记录列存储引擎第二讲的绸缪规划。 宣传语: 标题: 一条查询SQL在Tianmu引擎中的代码实现 宣讲语: 你是否只读过数据库理论的书籍, 但是一遇到代码就头疼呢? 你是否只会在理论上和数学公式上推导数据库内核, 但是从没亲自做过数据库内核的实现呢? 你是否对于数据库…

「上帝粒子」发现10周年

来源&#xff1a;FUTURE | 远见 选编&#xff1a;闵青云 2012年7月4日&#xff0c;欧洲核子研究中心&#xff08;CERN&#xff09;宣布发现了「上帝粒子」&#xff08;希格斯玻色子&#xff09;。希格斯玻色子是粒子物理学标准模型预言的一种玻色子&#xff0c;正是它的存在&…

全球诺贝尔奖得主最多的30所大学排名

上一篇&#xff1a;再见了阿里巴巴&#xff0c;希望以后不再有福报 100多年来&#xff0c;诺贝尔奖&#xff0c;尤其是自然科学领域的几种奖项&#xff0c;始终是全球范围内最受瞩目的科学荣誉。我们为您盘点1901-2019年全球诺贝尔获得者&#xff08;包括毕业生及职员&#xff…

SignalPlus 2023宏观经济展望:洞见机遇,迎接挑战

1. 2022 年市场回顾 (Markets in Review) 虽然理由不尽相同&#xff0c;但对于宏观资产和加密资产而言&#xff0c; 2022 年都是值得铭记的一年&#xff0c;Luna、FTX、Genesis 等机构的崩塌对加密货币生态系造成了灾难性的破坏&#xff0c;同时宏观市场也见证了一个强硬的美联…

python-微信自动发送信息2

《《由于女朋友最近打算考编&#xff0c;作为一名合格的男票肯定要天天督促啦。》》 实现目标&#xff1a;利用python实现微信自动发送教育学or心理学题目 实现思路&#xff1a; 1.从本地读取教育学心理学题目&#xff0c;并随机抽取一题。 2.使用python自带模块os.system模…

20145237 《Java程序设计》第七周学习总结

20145237 《Java程序设计》第七周学习总结 教材学习内容总结 第十三章 一、认识时间与日期 1.时间的度量 在正式认识Java提供了哪些时间处理API之前&#xff0c;得先来了解一些时间、日期的历史问题&#xff0c;这样你才会知道&#xff0c;时间日期确实是个很复杂的问题&…

GAMES101-现代计算机图形学入门-闫令琪 - lecture2 线性代数基础 - 课后笔记

向量的点乘 在图形学中&#xff0c;点乘的作用&#xff1a; 能够计算两个向量之间的角度&#xff0c;例如计算曲面和曲线之间的角度&#xff0c;用于计算两个方向向量之间距离有多近&#xff0c;越近其cos值越大&#xff0c;越小则越远&#xff0c;值为-1~1.能够将一个向量投影…

20155304 2016-2017-2 《Java程序设计》第七周学习总结

20155304 2016-2017-2 《Java程序设计》第七周学习总结 教材学习内容总结 1.时间的度量&#xff1a; 格林威治标准时间&#xff08;GMT&#xff09;通过观察太阳而得&#xff0c;其正午是太阳抵达天空最高点之时&#xff0c;因地球的公转与自传&#xff0c;会造成越来越大的时间…

ChineseGLUE:为中文NLP模型定制的自然语言理解基准

机器之心整理 参与&#xff1a;张倩、郑丽慧 GLUE 是一个用于评估通用 NLP 模型的基准&#xff0c;其排行榜可以在一定程度上反映 NLP 模型性能的高低。然而&#xff0c;现有的 GLUE 基准针对的是英文任务&#xff0c;无法评价 NLP 模型处理中文的能力。为了填补这一空白&…

全球诺贝尔奖得主最多的30所大学排名!

Datawhale分享 信息&#xff1a;诺贝尔奖&#xff0c;整理&#xff1a;图灵人工智能 100多年来&#xff0c;诺贝尔奖&#xff0c;尤其是自然科学领域的几种奖项&#xff0c;始终是全球范围内最受瞩目的科学荣誉。我们为您盘点1901-2019年全球诺贝尔获得者&#xff08;包括毕业…

GAMES101-现代计算机图形学入门-闫令琪 - lecture13 光线追踪1(Ray Tracing 1 - Whitted-Style Ray Tracing) - 课后笔记

光线追踪1 &#xff08;Ray Tracing 1 - Whitted-Style Ray Tracing&#xff09; 课程一共分为四个大的板块&#xff0c;目前已经学习了光栅化和几何&#xff0c;可以实现图1和2的效果&#xff0c;下面要来学习第三个大的板块&#xff0c;光线追踪。 为什么要使用光线追踪&…

GAMES101-现代计算机图形学入门-闫令琪 - lecture14 光线追踪2 - 加速结构(Ray Tracing 2 - Acceleration) - 课后笔记

光线追踪2 - 加速结构&#xff08;Ray Tracing 2 - Acceleration&#xff09; 对AABB结构优化来加速光线追踪的速度 均匀网格&#xff08;Uniform grids&#xff09;空间划分&#xff08;Spatial partitions&#xff09; 均匀空间划分&#xff08;Uniform Spatial Partition…

GAMES101-现代计算机图形学入门-闫令琪 - lecture15 光线追踪3 - 辐射度量学、渲染方程(Ray Tracing 3) - 课后笔记

光线追踪3 - 辐射度量学、渲染方程和全局光照 内容&#xff1a; 辐射度量学光线传输&#xff08;Light transport&#xff09; 反射方程&#xff08;The reflection equation&#xff09;渲染方程&#xff08;The rendering equation&#xff09; 全局光照&#xff08;Global…

GAMES101-现代计算机图形学入门-闫令琪 - lecture8 着色2(Shading 2) - 课后笔记

着色2&#xff08;Shading 2&#xff09; Blinn - Phong 光照模型包括 &#xff1a; 漫反射、镜面反射、环境光。上一节讲了漫反射&#xff0c;下面讲一下镜面反射和环境光。 镜面反射&#xff08;Specular reflection&#xff09; 镜面反射&#xff1a;当物体的表面很光滑的…

GAMES101-现代计算机图形学入门-闫令琪 - lecture11 几何3(Geometry 3) - 课后笔记

几何2 - 曲线和曲面&#xff08;Geometry 2 - Curves and Surface&#xff09; 上一节提到&#xff0c;要表现一些复杂的几何模型有两种方法&#xff1a; 隐式几何显式几何 本节课讲的为显式几何 显式几何&#xff08;Explicit Representations&#xff09; 显式几何有两种…

GAMES101-现代计算机图形学入门-闫令琪 - lecture4 观测变换(viewing transformation) - 课后笔记

观测变换&#xff08;Viewing transformation&#xff09; 视图 / 相机变换&#xff08;View / Camera transformation&#xff09;投影变换&#xff08;Projection transformation&#xff09; 正交投影&#xff08;Orthographic projection&#xff09;透视投影&#xff08;…

GAMES101-现代计算机图形学入门-闫令琪 - lecture9 着色3(Shading 3) - 课后笔记

着色3&#xff08;Shading 3&#xff09; 重心坐标纹理查询纹理应用 插值 - 重心坐标 &#xff08;Barycentric Coordinates&#xff09; 为什么要插值&#xff1f; 能够获得三角形三个固定顶点的属性&#xff0c;但是不知道三角形内部的属性希望三角形内部属性能有一个平滑…