SpringBoot參數(shù)校驗(yàn)及原理全面解析
前言
平時(shí)服務(wù)端開(kāi)發(fā)過(guò)程中,不可避免的需要對(duì)接口參數(shù)進(jìn)行校驗(yàn),比較常見(jiàn)的比如用戶(hù)名不能為空、年齡必須大于0、郵箱格式要合規(guī)等等。
如果通過(guò)if else去校驗(yàn)參數(shù),校驗(yàn)代碼會(huì)跟業(yè)務(wù)耦合,且顯得很冗長(zhǎng)。
SpringBoot提供了一種簡(jiǎn)潔、高效的方式,通過(guò)@Validated/@Valid注解來(lái)做參數(shù)校驗(yàn),大大提高了工作效率
一、基本用法
總共三種方式:
- Controller的@RequestBody參數(shù)校驗(yàn)
- Controller的@RequestParam/@PathVariable參數(shù)校驗(yàn)
- 編程式校驗(yàn),直接調(diào)用hibernate的validate方法
三種方式都需要加上以下依賴(lài)。里面有所需的jakarta.validation-api和hibernate-validator包
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>@RequestBody參數(shù)校驗(yàn)
該方式適用于Controller中POST/PUT方法的參數(shù)校驗(yàn),校驗(yàn)失敗會(huì)拋MethodArgumentNotValidException
1.首先在參數(shù)類(lèi)的屬性上聲明約束注解,比如@NotBlank、@Email等
@Data
public class UserVo implements Serializable {
@NotBlank(message = "名字不能為空")
@Size(min = 2, max = 50, message = "名字長(zhǎng)度的范圍為2~50")
private String name;
@Email(message = "郵箱格式不對(duì)")
private String email;
@NotNull(message = "年齡不能為空")
@Min(18)
@Max(100)
private Integer age;
@NotEmpty(message = "照片不能為空")
private List<String> photoList;
}2.接著在Controller方法@RequestBody旁加上@Validated注解
@Slf4j
@RestController
public class UserController {
@ApiOperation("保存用戶(hù)")
@PostMapping("/save/user")
public Result<Boolean> saveUser(@RequestBody @Validated UserVo user) {
return Result.ok();
}
}@RequestParam/@PathVariable參數(shù)校驗(yàn)
該方式適用于Controller中GET方法的參數(shù)校驗(yàn),校驗(yàn)失敗會(huì)拋ConstraintViolationException。它是通過(guò)類(lèi)上加@Validated注解,方法參數(shù)前加@NotBlank等約束注解的方式來(lái)實(shí)現(xiàn)的,所以其它Spring Bean的方法也適用
1.Controller類(lèi)上加@Validated注解;@RequestParam/@PathVariable旁加上@NotBlank、@Max等注解
@Slf4j
@RestController
@Validated
public class UserController {
@ApiOperation("查詢(xún)用戶(hù)")
@GetMapping("/list/user")
public Result<List<UserVo>> listUser(
@Min(value = 100, message = "id不能小于100") @RequestParam("id") Long id,
@NotBlank(message = "名稱(chēng)不能為空") @RequestParam("name") String name,
@Max(value = 90, message = "年齡不能大于90") @RequestParam("age") Integer age) {
List<UserVo> list = new ArrayList<>();
return Result.ok(list);
}
}編程式校驗(yàn)
該方式適用于Service參數(shù)的校驗(yàn),校驗(yàn)失敗手動(dòng)拋ValidationException
1.通過(guò)@bean注解初始化Validator對(duì)象
public class ValidatorConfig {
@Bean
public Validator validator() {
return Validation.byProvider(HibernateValidator.class)
.configure()
// 快速失敗模式
.failFast(true)
.buildValidatorFactory()
.getValidator();
}
}2.在Service方法中調(diào)用hibernate的validate方法對(duì)參數(shù)進(jìn)行校驗(yàn)
@Service
@Slf4j
public class UserService {
@Autowired
private Validator validator;
public boolean editUser(UserVo user) {
Set<ConstraintViolation<UserVo>> validateSet = validator.validate(user);
if (CollectionUtils.isNotEmpty(validateSet)) {
StringBuilder errorMessage = new StringBuilder();
for (ConstraintViolation<UserVo> violation : validateSet) {
errorMessage.append("[").append(violation.getPropertyPath().toString()).append("]")
.append(violation.getMessage()).append(";");
}
throw new ValidationException(errorMessage.toString());
}
return Boolean.TRUE;
}
}二、進(jìn)階用法
自定義驗(yàn)證注解
jakarta.validation-api和hibernate-validator包中內(nèi)置的注解有些場(chǎng)景可能不支持,比如添加用戶(hù)時(shí),需要校驗(yàn)用戶(hù)名是否重復(fù),這時(shí)可以通過(guò)自定義注解來(lái)實(shí)現(xiàn)
1.首先自定義注解
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})
@Retention(RUNTIME)
@Documented
@Repeatable(UniqueName.List.class)
@Constraint(validatedBy = {UniqueNameValidator.class})
public @interface UniqueName {
String message() default "用戶(hù)名重復(fù)了";
// 分組
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})
@Retention(RUNTIME)
@Documented
public @interface List {
UniqueName[] value();
}
}2.接著給自定義注解添加驗(yàn)證器
- 實(shí)現(xiàn)ConstraintValidator接口,并指定自定義注解<UniqueName>和驗(yàn)證的數(shù)據(jù)類(lèi)型 <String>
- 重寫(xiě)isValid方法,實(shí)現(xiàn)驗(yàn)證邏輯
@Component
public class UniqueNameValidator implements ConstraintValidator<UniqueName, String> {
@Autowired
private UserService userService;
@Override
public boolean isValid(String name, ConstraintValidatorContext context) {
if (StringUtils.isBlank(name)) {
return true;
}
UserVo user = userService.getByName(name);
if (user == null) {
return true;
}
return false;
}
}3.使用自定義注解
@Data
public class UserVo implements Serializable {
@UniqueName
private String name;
}多屬性聯(lián)合校驗(yàn)
當(dāng)一個(gè)字段的校驗(yàn)依賴(lài)另一個(gè)字段的值時(shí),需要用到多屬性聯(lián)合校驗(yàn),或者叫分組校驗(yàn)。
舉個(gè)例子,某個(gè)系統(tǒng)提交用戶(hù)信息時(shí)需要做校驗(yàn),當(dāng)性別為女時(shí),照片信息不能為空。這時(shí),照片信息能否為空,依賴(lài)于性別的取值。
hibernate-validator提供了DefaultGroupSequenceProvider接口供我們自定義分組,具體使用如下:
1.首先定義兩個(gè)組,Boy和Girl
public interface Boy {
}
public interface Girl {
}2.分組邏輯實(shí)現(xiàn),當(dāng)性別為女時(shí),將用戶(hù)分到Girl組
public class CustomGroupSequenceProvider implements DefaultGroupSequenceProvider<UserVo> {
@Override
public List<Class<?>> getValidationGroups(UserVo user) {
List<Class<?>> defaultGroupSequence = new ArrayList<>();
defaultGroupSequence.add(UserVo.class);
if (user != null) {
String sex = user.getSex();
if ("女".equals(sex)) {
defaultGroupSequence.add(Girl.class);
}
}
return defaultGroupSequence;
}
}3.使用分組校驗(yàn)photoList字段
- 實(shí)體類(lèi)上添加@GroupSequenceProvider(CustomSequenceProvider.class)注解
- 字段上添加@NotEmpty(message = "性別為女時(shí)照片不能為空", groups = {Girl.class})注解
@Data
@GroupSequenceProvider(CustomSequenceProvider.class)
public class UserVo implements Serializable {
@NotBlank(message = "性別不能為空")
private String sex;
@NotEmpty(message = "性別為女時(shí)照片不能為空", groups = {Girl.class})
private List<String> photoList;
}嵌套校驗(yàn)
當(dāng)VO對(duì)象中存在對(duì)象屬性需要校驗(yàn)時(shí),可以使用嵌套校驗(yàn),
1.在對(duì)象屬性上加@Valid注解
@Data
public class UserVo implements Serializable {
@Valid
@NotNull(message = "地址不能為空")
private Address address;
}2.然后在內(nèi)嵌對(duì)象中聲明約束注解
@Data
public class Address implements Serializable {
@NotBlank(message = "地址名稱(chēng)不能為空")
private String name;
private String longitude;
private String latitude;
}三、實(shí)現(xiàn)原理
@RequestBody參數(shù)校驗(yàn)實(shí)現(xiàn)原理
所有@RequestBody注釋的參數(shù)都要經(jīng)過(guò)RequestResponseBodyMethodProcessor類(lèi)處理,該類(lèi)主要用于解析@RequestBody注釋方法的參數(shù),以及處理@ResponseBody注釋方法的返回值。其中,resolveArgument()方法是解析@RequestBody注釋參數(shù)的入口
public class RequestResponseBodyMethodProcessor extends AbstractMessageConverterMethodProcessor {
@Override
public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
parameter = parameter.nestedIfOptional();
Object arg = readWithMessageConverters(webRequest, parameter, parameter.getNestedGenericParameterType());
String name = Conventions.getVariableNameForParameter(parameter);
if (binderFactory != null) {
WebDataBinder binder = binderFactory.createBinder(webRequest, arg, name);
if (arg != null) {
validateIfApplicable(binder, parameter);
if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) {
throw new MethodArgumentNotValidException(parameter, binder.getBindingResult());
}
}
if (mavContainer != null) {
mavContainer.addAttribute(BindingResult.MODEL_KEY_PREFIX + name, binder.getBindingResult());
}
}
return adaptArgumentIfNecessary(arg, parameter);
}
}resolveArgument方法中的validateIfApplicable(binder, parameter)會(huì)對(duì)帶有@valid/@validate注解的參數(shù)進(jìn)行校驗(yàn)
protected void validateIfApplicable(WebDataBinder binder, MethodParameter parameter) {
Annotation[] annotations = parameter.getParameterAnnotations();
for (Annotation ann : annotations) {
Object[] validationHints = ValidationAnnotationUtils.determineValidationHints(ann);
if (validationHints != null) {
binder.validate(validationHints);
break;
}
}
}
//會(huì)對(duì)@Validated注解或者@Valid開(kāi)頭的注解進(jìn)行校驗(yàn)
public static Object[] determineValidationHints(Annotation ann) {
Class<? extends Annotation> annotationType = ann.annotationType();
String annotationName = annotationType.getName();
if ("javax.validation.Valid".equals(annotationName)) {
return EMPTY_OBJECT_ARRAY;
}
Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class);
if (validatedAnn != null) {
Object hints = validatedAnn.value();
return convertValidationHints(hints);
}
if (annotationType.getSimpleName().startsWith("Valid")) {
Object hints = AnnotationUtils.getValue(ann);
return convertValidationHints(hints);
}
return null;
}Spring通過(guò)一圈適配轉(zhuǎn)換后,會(huì)把參數(shù)校驗(yàn)邏輯落到hibernate-validator中,在ValidatorImpl#validate(T object, Class<?>... groups)中做校驗(yàn)
public class ValidatorImpl implements Validator, ExecutableValidator {
@Override
public final <T> Set<ConstraintViolation<T>> validate(T object, Class<?>... groups) {
Contracts.assertNotNull( object, MESSAGES.validatedObjectMustNotBeNull() );
sanityCheckGroups( groups );
@SuppressWarnings("unchecked")
Class<T> rootBeanClass = (Class<T>) object.getClass();
BeanMetaData<T> rootBeanMetaData = beanMetaDataManager.getBeanMetaData( rootBeanClass );
if ( !rootBeanMetaData.hasConstraints() ) {
return Collections.emptySet();
}
BaseBeanValidationContext<T>
validationContext = getValidationContextBuilder().forValidate( rootBeanClass, rootBeanMetaData, object );
ValidationOrder validationOrder = determineGroupValidationOrder( groups );
BeanValueContext<?, Object> valueContext = ValueContexts.getLocalExecutionContextForBean(
validatorScopedContext.getParameterNameProvider(),
object,
validationContext.getRootBeanMetaData(),
PathImpl.createRootPath()
);
return validateInContext( validationContext, valueContext, validationOrder );
}
}具體校驗(yàn)過(guò)程在validateConstraintsForSingleDefaultGroupElement方法中,它會(huì)遍歷@NotNull、@NotBlank、@Email這些約束注解,看參數(shù)是否符合限制
public class ValidatorImpl implements Validator, ExecutableValidator {
private <U> boolean validateConstraintsForSingleDefaultGroupElement(BaseBeanValidationContext<?> validationContext, ValueContext<U, Object> valueContext, final Map<Class<?>, Class<?>> validatedInterfaces,
Class<? super U> clazz, Set<MetaConstraint<?>> metaConstraints, Group defaultSequenceMember) {
boolean validationSuccessful = true;
valueContext.setCurrentGroup( defaultSequenceMember.getDefiningClass() );
//metaConstraints是@NotNull、@NotBlank、@Email這些約束注解的集合,一個(gè)個(gè)驗(yàn)證
for ( MetaConstraint<?> metaConstraint : metaConstraints ) {
final Class<?> declaringClass = metaConstraint.getLocation().getDeclaringClass();
if ( declaringClass.isInterface() ) {
Class<?> validatedForClass = validatedInterfaces.get( declaringClass );
if ( validatedForClass != null && !validatedForClass.equals( clazz ) ) {
continue;
}
validatedInterfaces.put( declaringClass, clazz );
}
boolean tmp = validateMetaConstraint( validationContext, valueContext, valueContext.getCurrentBean(), metaConstraint );
if ( shouldFailFast( validationContext ) ) {
return false;
}
validationSuccessful = validationSuccessful && tmp;
}
return validationSuccessful;
}
}validator.isValid()是所有驗(yàn)證器的入口,包括hibernate-validator內(nèi)置的,以及自定義的
public abstract class ConstraintTree<A extends Annotation> {
protected final <V> Optional<ConstraintValidatorContextImpl> validateSingleConstraint(
ValueContext<?, ?> valueContext,
ConstraintValidatorContextImpl constraintValidatorContext,
ConstraintValidator<A, V> validator) {
boolean isValid;
try {
@SuppressWarnings("unchecked")
V validatedValue = (V) valueContext.getCurrentValidatedValue();
isValid = validator.isValid( validatedValue, constraintValidatorContext );
}
catch (RuntimeException e) {
if ( e instanceof ConstraintDeclarationException ) {
throw e;
}
throw LOG.getExceptionDuringIsValidCallException( e );
}
if ( !isValid ) {
//We do not add these violations yet, since we don't know how they are
//going to influence the final boolean evaluation
return Optional.of( constraintValidatorContext );
}
return Optional.empty();
}
}以下是@NotBlank約束注解驗(yàn)證器的具體實(shí)現(xiàn)
public class NotBlankValidator implements ConstraintValidator<NotBlank, CharSequence> {
/**
* Checks that the character sequence is not {@code null} nor empty after removing any leading or trailing
* whitespace.
*
* @param charSequence the character sequence to validate
* @param constraintValidatorContext context in which the constraint is evaluated
* @return returns {@code true} if the string is not {@code null} and the length of the trimmed
* {@code charSequence} is strictly superior to 0, {@code false} otherwise
*/
@Override
public boolean isValid(CharSequence charSequence, ConstraintValidatorContext constraintValidatorContext) {
if ( charSequence == null ) {
return false;
}
return charSequence.toString().trim().length() > 0;
}
}@RequestParam/@PathVariable參數(shù)校驗(yàn)實(shí)現(xiàn)原理
該方式本質(zhì)是通過(guò)類(lèi)上加@Validated注解,方法參數(shù)前加@NotBlank等約束注解來(lái)實(shí)現(xiàn)的。底層使用的是Spring AOP,具體來(lái)說(shuō)是通過(guò)MethodValidationPostProcessor動(dòng)態(tài)注冊(cè)AOP切面,然后使用MethodValidationInterceptor對(duì)切點(diǎn)方法織入增強(qiáng)。
以下是容器啟動(dòng)時(shí)初始化@Validated切點(diǎn),以及MethodValidationInterceptor增強(qiáng)
public class MethodValidationPostProcessor extends AbstractBeanFactoryAwareAdvisingPostProcessor
implements InitializingBean {
private Class<? extends Annotation> validatedAnnotationType = Validated.class;
@Nullable
private Validator validator;
@Override
public void afterPropertiesSet() {
Pointcut pointcut = new AnnotationMatchingPointcut(this.validatedAnnotationType, true);
this.advisor = new DefaultPointcutAdvisor(pointcut, createMethodValidationAdvice(this.validator));
}
protected Advice createMethodValidationAdvice(@Nullable Validator validator) {
return (validator != null ? new MethodValidationInterceptor(validator) : new MethodValidationInterceptor());
}
}具體增強(qiáng)邏輯在MethodValidationInterceptor中
public class MethodValidationInterceptor implements MethodInterceptor {
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
// Avoid Validator invocation on FactoryBean.getObjectType/isSingleton
if (isFactoryBeanMetadataMethod(invocation.getMethod())) {
return invocation.proceed();
}
Class<?>[] groups = determineValidationGroups(invocation);
// Standard Bean Validation 1.1 API
ExecutableValidator execVal = this.validator.forExecutables();
Method methodToValidate = invocation.getMethod();
Set<ConstraintViolation<Object>> result;
try {
result = execVal.validateParameters(
invocation.getThis(), methodToValidate, invocation.getArguments(), groups);
}
catch (IllegalArgumentException ex) {
// Probably a generic type mismatch between interface and impl as reported in SPR-12237 / HV-1011
// Let's try to find the bridged method on the implementation class...
methodToValidate = BridgeMethodResolver.findBridgedMethod(
ClassUtils.getMostSpecificMethod(invocation.getMethod(), invocation.getThis().getClass()));
result = execVal.validateParameters(
invocation.getThis(), methodToValidate, invocation.getArguments(), groups);
}
if (!result.isEmpty()) {
throw new ConstraintViolationException(result);
}
Object returnValue = invocation.proceed();
result = execVal.validateReturnValue(invocation.getThis(), methodToValidate, returnValue, groups);
if (!result.isEmpty()) {
throw new ConstraintViolationException(result);
}
return returnValue;
}
}其中execVal.validateParameters()方法是用來(lái)做參數(shù)校驗(yàn)的,最終會(huì)進(jìn)到hibernate-validator中。后面的邏輯跟上面類(lèi)似,此處就不再贅述
public class ValidatorImpl implements Validator, ExecutableValidator {
@Override
public <T> Set<ConstraintViolation<T>> validateParameters(T object, Method method, Object[] parameterValues, Class<?>... groups) {
Contracts.assertNotNull( object, MESSAGES.validatedObjectMustNotBeNull() );
Contracts.assertNotNull( method, MESSAGES.validatedMethodMustNotBeNull() );
Contracts.assertNotNull( parameterValues, MESSAGES.validatedParameterArrayMustNotBeNull() );
return validateParameters( object, (Executable) method, parameterValues, groups );
}
}項(xiàng)目源碼
https://github.com/layfoundation/spring-param-validate
附件
jakarta.validation-api(版本2.0.1)所有注解
| 注解 | 說(shuō)明 |
|---|---|
| @AssertFalse | 驗(yàn)證 boolean 類(lèi)型值是否為 false |
| @AssertTrue | 驗(yàn)證 boolean 類(lèi)型值是否為 true |
| @DecimalMax(value) | 驗(yàn)證數(shù)字的大小是否小于等于指定的值,小數(shù)存在精度 |
| @DecimalMin(value) | 驗(yàn)證數(shù)字的大小是否大于等于指定的值,小數(shù)存在精度 |
| @Digits(integer, fraction) | 驗(yàn)證數(shù)字是否符合指定格式 |
| 驗(yàn)證字符串是否符合電子郵件地址的格式 | |
| @Future | 驗(yàn)證一個(gè)日期或時(shí)間是否在當(dāng)前時(shí)間之后 |
| @FutureOrPresent | 驗(yàn)證一個(gè)日期或時(shí)間是否在當(dāng)前時(shí)間之后或等于當(dāng)前時(shí)間 |
| @Max(value) | 驗(yàn)證數(shù)字的大小是否小于等于指定的值 |
| @Min(value) | 驗(yàn)證數(shù)字的大小是否大于等于指定的值 |
| @Negative | 驗(yàn)證數(shù)字是否是負(fù)整數(shù),0無(wú)效 |
| @NegativeOrZero | 驗(yàn)證數(shù)字是否是負(fù)整數(shù) |
| @NotBlank | 驗(yàn)證字符串不能為空null或"",只能用于字符串驗(yàn)證 |
| @NotEmpty | 驗(yàn)證對(duì)象不得為空,可用于Map和數(shù)組 |
| @NotNull | 驗(yàn)證對(duì)象不為 null |
| @Null | 驗(yàn)證對(duì)象必須為 null |
| @past | 驗(yàn)證一個(gè)日期或時(shí)間是否在當(dāng)前時(shí)間之前。 |
| @PastOrPresent | 驗(yàn)證一個(gè)日期或時(shí)間是否在當(dāng)前時(shí)間之前或等于當(dāng)前時(shí)間。 |
| @Pattern(value) | 驗(yàn)證字符串是否符合正則表達(dá)式的規(guī)則 |
| @Positive | 驗(yàn)證數(shù)字是否是正整數(shù),0無(wú)效 |
| @PositiveOrZero | 驗(yàn)證數(shù)字是否是正整數(shù) |
| @Size(max, min) | 驗(yàn)證對(duì)象(字符串、集合、數(shù)組)長(zhǎng)度是否在指定范圍之內(nèi) |
hibernate-validator(版本6.0.17.Final)補(bǔ)充的常用注解
| 注解 | 說(shuō)明 |
|---|---|
| @Length | 被注釋的字符串的大小必須在指定的范圍內(nèi) |
| @Range | 被注釋的元素必須在合適的范圍內(nèi) |
| @SafeHtml | 被注釋的元素必須是安全Html |
| @URL | 被注釋的元素必須是有效URL |
總結(jié)
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
Spring AOP實(shí)現(xiàn)復(fù)雜的日志記錄操作(自定義注解)
Spring AOP實(shí)現(xiàn)復(fù)雜的日志記錄操作(自定義注解),具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-09-09
在Spring Boot中實(shí)現(xiàn)多環(huán)境配置的方法
在SpringBoot中,實(shí)現(xiàn)多環(huán)境配置是一項(xiàng)重要且常用的功能,它允許開(kāi)發(fā)者為不同的運(yùn)行環(huán)境,這種方式簡(jiǎn)化了環(huán)境切換的復(fù)雜度,提高了項(xiàng)目的可維護(hù)性和靈活性,本文給大家介紹在Spring Boot中實(shí)現(xiàn)多環(huán)境配置的方法,感興趣的朋友跟隨小編一起看看吧2024-09-09
IDEA中Maven依賴(lài)包下載不了的問(wèn)題解決方案匯總
這篇文章主要介紹了IDEA中Maven依賴(lài)包下載不了的問(wèn)題解決方案匯總,文中通過(guò)圖文示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-08-08
@ConfigurationProperties加載外部配置方式
這篇文章主要介紹了@ConfigurationProperties加載外部配置方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-03-03
利用Java+OpenCV實(shí)現(xiàn)拍照功能
網(wǎng)上大多是利用C語(yǔ)言或者Python實(shí)現(xiàn)拍照功能,本文將為大家介紹另一種方法,即在Java中調(diào)用OpenCV實(shí)現(xiàn)拍照功能,感興趣的可以了解一下2022-01-01
Java異常處理Guava?Throwables類(lèi)使用實(shí)例解析
這篇文章主要為大家介紹了Java異常處理神器Guava?Throwables類(lèi)使用深入詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-12-12

