Spring参数校验和全局异常处理

2023-10-27

目录

一、前言

二、Validation

1、JSR-303

2、Spring Validation

3、@Validated和@Valid的区别

三、全局异常处理

1、为何要处理异常

2、@RestControllerAdvice

3、返回自定义消息

四、全局异常原理解析

1、ControllerAdvice的加载

2、Controller接口异常如何进入exceptionHandler方法

五、总结

参考


一、前言

        数据校验在业务代码中经常用到,比如前端传过来的用户名、年龄等数据,可能要求用户名非空、不能包含emoji,年龄的数值必须在1到110之间。如果不使用数据校验框架,那么就需要在业务代码中专门写一些代码用来校验数据的合法性。例如:

if (StringUitls.isBlank(userName)) {
    throw new XXXException(500, "用户名不能为空");
}
if (age > 110 || age < 1) {
    throw new XXXException(500, "年龄非法");
}

        从上述代码可以看出,数据合法性的校验不复杂,但是嵌入到业务代码就比较凌乱,不但影响代码整洁,还需要开发自己去写校验数据合法性的代码,这种做法不够优雅,而且也容易出错产生bug。如果使用Spring的validation框架,就能够避免上述的烦恼。

二、Validation

1、JSR-303

        JSR是Java Specification Requests的缩写,意思是Java规范提案。它是向JCP(Java Community Process)提出新增一个标准化技术规范的正式请求。任何人都可以提交JSR,为Java平台新增API和服务。JSR对于Java来说是一个重要的标准。

        JSR-303是Java EE 6中的一项子规范,称为Bean Validation。Hibernate Validator是Bean Validation的参考实现。提供了JSR-303的所有内置constraint的实现。JSR-303的常用注解如下表所示:

约束注解名称

约束注解说明

@Null

被注释的元素必须为null

@NotNull

被注释的元素必须不为null

@AssertTrue

被注释的元素必须为true

@AssertFalse

被注释的元素必须为false

@Min(value)

被注释的元素必须是一个数字,其值必须大于等于指定的最小值

@Max(value)

被注释的元素必须是一个数字,其值必须小于等于指定的最大值

@DecimalMin(value)

被注释的元素必须是一个数字,其值必须大于等于指定的最小值

@DecimalMax(value)

被注释的元素必须是一个数字,其值必须小于等于指定的最大值

@Size(max, min)

被注释的元素的大小必须在指定的范围内

@Digits (integer, fraction)

被注释的元素必须是一个数字,其值必须在可接受的范围内

@Past

被注释的元素必须是一个过去的日期

@Future

被注释的元素必须是一个将来的日期

@Pattern(value)

被注释的元素必须符合指定的正则表达式

2、Spring Validation

        Spring Validation对Hibernate Validation进行了二次封装,在Spring MVC模块中添加了自动校验,并将校验信息封装在特定的类中。Spring Validation框架对参数的校验提供了@Validated(Spring's JSR-303规范,是JSR-303的一个变种),javax提供了@Valid。

3、@Validated和@Valid的区别

        @Validated和@Valid在基本验证上没太大区别,但是在分组、注解位置、嵌套验证等方便表现不太相同。

功能点

@Validated

@Valid

分组

支持分组功能,不同的分组采用不同的分组校验

不支持分组

位置

可以用在类、方法、方法参数上

在方法、构造函数、方法参数和成员属性(字段)上

嵌套验证

不支持嵌套

支持嵌套

三、全局异常处理

1、为何要处理异常

        在日常开发过程中,为了不将后端异常堆栈抛给前端页面,经常需要在controller层、service层、dao层等进行异常捕获,这样做的话代码不太美观,后期维护也不方便。而且如果有些异常没有捕获住,就可能将一些莫名其妙的异常抛给前端,用户看到此异常就非常不友好。比如用户插入数据的时候,可能他插入的数据违法了数据库的唯一性约束,那么如果没有对这种场景进行捕获并且转化成用户可读的异常,就可能会直接抛出下面异常:

com.mysql.jdbc.exceptions.jdbc4.MySQLIntegrityConstraintViolationException: Duplicate entry '1010640164' for key 'uk_user_id'
        at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
        at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
        at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
        at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
        at com.mysql.jdbc.Util.handleNewInstance(Util.java:425)
        at com.mysql.jdbc.Util.getInstance(Util.java:408)
        at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:936)
        at com.mysql.jdbc.MysqlIO.checkErrorPacket(MysqlIO.java:3978)
        at com.mysql.jdbc.MysqlIO.checkErrorPacket(MysqlIO.java:3914)
        at com.mysql.jdbc.MysqlIO.sendCommand(MysqlIO.java:2530)
        at com.mysql.jdbc.MysqlIO.sqlQueryDirect(MysqlIO.java:2683)
        at com.mysql.jdbc.ConnectionImpl.execSQL(ConnectionImpl.java:2495)
        at com.mysql.jdbc.PreparedStatement.executeInternal(PreparedStatement.java:1903)
        at com.mysql.jdbc.PreparedStatement.execute(PreparedStatement.java:1242)
        省略多余异常...

        为了解决上述问题,可以将Controller层的异常信息进行统一封装处理,解决方案可以使用@RestControllerAdvice(或者ControllerAdvice) + @ExceptionHandler。用@RestControllerAdvice表示开启了全局异常处理,我们需要自定义一个方法,使用@ExceptionHandler注解修饰的方法进行统一异常处理。简单例子如下代码所示:

@RestControllerAdvice
public class UnionExceptionHandler {
    
    @ExceptionHandler(value = Exception.class)
    public String exceptionHandler(Exception e) {
        System.out.println("未知异常!原因是:"+e);
        return e.getMessage();
    }
}

        上述实例代码,对捕获的异常进行简单的二次处理,返回异常的信息。此例子较为简单,返回的错误信息也比较有限,而且不够人人性化。一般线上的响应都比较复杂,可以使用自定义的Result类型返回。例如下面代码所示:

@RestControllerAdvice
public class CustomizeResponseExceptionHandler {
    
    @ExceptionHandler(value = CustomizeException.class)
    public Result exceptionHandler(CustomizeException e) {
        System.out.println("未知异常!原因是:" + e.getErrorMsg());
        return Result.buildError(e.getErrorCode(), e.getErrorMsg());
    }
}

public class Result<T> implements Serializable {
 
    private int code;

    private String msg;

    private T data;
    
    private List<Error> errors;
   
    public Result() {}
    
    public Result(int code, String msg, T data, List<Error> errors) {
        this.code = code;
        this.msg = msg;
        this.data = data;
        this.errors = errors;
    }
    
    public static Result<Void> buildError(int code, String message) {
        return new Result<>(code, msg, null, null);
    }
}

2、@RestControllerAdvice

        @RestControllerAdvice是Spring框架里的一个Component注解,具体代码如下所示:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@ControllerAdvice
@ResponseBody
public @interface RestControllerAdvice {

    @AliasFor("basePackages")
	String[] value() default {};
    
    @AliasFor("value")
	String[] basePackages() default {};
    
    Class<?>[] basePackageClasses() default {};
    
    Class<?>[] assignableTypes() default {};
    
    Class<? extends Annotation>[] annotations() default {};
}

        上述代码可以看出RestControllerAdvice只能修饰类,并且是运行时才生效。值得注意的是RestControllerAdvice类上方还有@ControllerAdvice和@ResponseBody注解修饰。如果一个类是ControllerAdvice注解修饰,想要返回json数据的话就得额外添加@ResponseBody注解。

        有了@RestControllerAdvice,被@ExceptionHandler、@InitBinder和@ModelAttribute注解的方法,都会作用到@RequestMapping注解的方法上。

3、返回自定义消息

        在实际业务开发过程中,经常遇到数据校验的场景,比如在新增一个用户的基本信息时,需要校验这个人的基本信息的合法性。那么需要在UserDto实体类中加上一些校验注解,代码如下:

public class UserDto implements Serializable {

    private static final long serialVersionUID = -8412446300622080614L;

    @NotBlank(message = "用户姓名不能为空")
    private String name;

    @Range(min = 1, max = 110, message = "年龄必须在1到110之间")
    private int age;

    @NotBlank(message = "昵称不能为空")
    @Pattern(regexp = "^.{2,20}$", message = "昵称2到20个字符")
    private String nickname;
}


@Slf4j
@RestController
public class UserController {

    @RequestMapping(value = "/user/add")
    public Result<Boolean> addUser(@RequestBody @Validated UserDto userDto) {
        boolean success = userService.addUser(userDto);
        return Result.ofSuccess(success);
    }
}

public class Result<T> implements Serializable {
 
    private int code;

    private String msg;

    private T data;
    
    private List<Error> errors;
   
    public Result() {}
    
    public Result(int code, String msg, T data, List<Error> errors) {
        this.code = code;
        this.msg = msg;
        this.data = data;
        this.errors = errors;
    }
    
    public static Result<Void> buildError(int code, String message) {
        return new Result<>(code, msg, null, null);
    }
    
    public static Result<Void> buildError(String message) {
        return new Result<>(500, msg, null, null);
    }
    
    public static <T> Result<T> ofSuccess(T data) {
        return new Result<>(data);
    }
}

        如果在添加用户的时候,传的参数有误,比如age传了0,那么上述代码的错误响应结果如下所示:

{
    "code": 10086,
    "msg": "系统异常,请稍后重试",
    "errors": [
        {
            "name": "age",
            "message": "年龄必须在1到110之间"
        }
    ]
}

        假如想将所有的人性化异常信息都放在msg里,这样前端直接报msg里的异常,用户看见就容易懂了。改造后的响应结果如下所示:

{
    "code": 500,
    "msg": "年龄必须在1到110之间"
}

        想实现上述的要求,应该如何做呢?可以用上述提到的,用@RestControllerAdvice + @ExceptionHandler解决。代码如下:
 

@RestControllerAdvice
public class ControllerAdvice {

    @ExceptionHandler(value = {MethodArgumentNotValidException.class})
    public Result<Void> exceptionHandler(Exception exception, HttpServletRequest request, HttpServletResponse response) {
        Result<Void> result = new Result<>();
        StringBuilder errorBuilder = new StringBuilder();
        Iterator iterator;
        FieldError fieldError;
        if (exception instanceof MethodArgumentNotValidException) {
            iterator = ((MethodArgumentNotValidException) exception).getBindingResult().getFieldErrors().iterator();
            while (iterator.hasNext()) {
                fieldError = (FieldError) iterator.next();
                errorBuilder.append(fieldError.getDefaultMessage()).append("\n");
            }
        }
        return ResultUtils.buildError(errorBuilder.toString());
    }

}

        上述代码的@ExceptionHandler里的value是MethodArgumentNotValidException.class,这样当@Validated修饰的对象报错时,就可以被捕捉到,然后就可以组装自定义的响应格式。

四、全局异常原理解析

        @RestControllerAdvice是Spring4.3新增的注解,可以用于定义@ExceptionHandler、@InitBinder、@ModelAttribute,并且作用到所有被@RequestMapping注解的方法上。那么为什么Controller接口抛出异常后,会自动进入@ExceptionHandler修饰的ControllerAdvice#exceptionHandler方法里呢?接下来通过源码来分析具体原理。

1、ControllerAdvice的加载

        上述代码可以看到,ControllerAdvice被@RestControllerAdvice修饰。ExceptionHandlerExceptionResolver类实现了InitializingBean接口,因此重写了afterPropertiesSet()方法。具体代码如下:

	@Override
	public void afterPropertiesSet() {
		//初始化ExceptionHandlerAdvice缓存,添加ResponseBodyAdvice到responseBodyAdvice中
		initExceptionHandlerAdviceCache();

		if (this.argumentResolvers == null) {
			List<HandlerMethodArgumentResolver> resolvers = getDefaultArgumentResolvers();
			this.argumentResolvers = new HandlerMethodArgumentResolverComposite().addResolvers(resolvers);
		}
		if (this.returnValueHandlers == null) {
			List<HandlerMethodReturnValueHandler> handlers = getDefaultReturnValueHandlers();
			this.returnValueHandlers = new HandlerMethodReturnValueHandlerComposite().addHandlers(handlers);
		}
	}

    private void initExceptionHandlerAdviceCache() {
		if (getApplicationContext() == null) {
			return;
		}
        //查找所有被@ControllerAdvice注解的类
		List<ControllerAdviceBean> adviceBeans = ControllerAdviceBean.findAnnotatedBeans(getApplicationContext());
		AnnotationAwareOrderComparator.sort(adviceBeans);

		for (ControllerAdviceBean adviceBean : adviceBeans) {
			Class<?> beanType = adviceBean.getBeanType();
			if (beanType == null) {
				throw new IllegalStateException("Unresolvable type for ControllerAdviceBean: " + adviceBean);
			}
			ExceptionHandlerMethodResolver resolver = new ExceptionHandlerMethodResolver(beanType);
			if (resolver.hasExceptionMappings()) {
				this.exceptionHandlerAdviceCache.put(adviceBean, resolver);
			}
			if (ResponseBodyAdvice.class.isAssignableFrom(beanType)) {
				this.responseBodyAdvice.add(adviceBean);
			}
		}
        ...省略代码
	}
    
    /**
     * 查询被ControllerAdvice注解的类,并且转化成ControllerAdviceBean列表
     */
	public static List<ControllerAdviceBean> findAnnotatedBeans(ApplicationContext context) {
		return Arrays.stream(BeanFactoryUtils.beanNamesForTypeIncludingAncestors(context, Object.class))
				.filter(name -> !ScopedProxyUtils.isScopedTarget(name))
                 //此处可以看到需要过滤出被ControllerAdvice注解修饰类
				.filter(name -> context.findAnnotationOnBean(name, ControllerAdvice.class) != null)
				.map(name -> new ControllerAdviceBean(name, context))
				.collect(Collectors.toList());
	}

        上面的initExceptionHandlerAdviceCache会从容器中找到@ControllerAdvice注解的类,因为@RestControllerAdvice是@ControllerAdvice注解的类,因此被@RestControllerAdvice注解的类也会被扫描到。具体可以看findAnnotatedBeans方法。

        当扫描出所有被@RestControllerAdvice注解的类后,放到List<ControllerAdviceBean> adviceBeans中,遍历adviceBeans,将这些ControllerAdviceBean放到exceptionHandlerAdviceCache,exceptionHandlerAdviceCache的类型是Map<ControllerAdviceBean, ExceptionHandlerMethodResolver>。放exceptionHandlerAdviceCache过程中,会扫描每个带有@ExceptionHandler注解的方法,ExceptionHandler的value可能有多个,然后以exceptionType为key,method为value放到mappedMethods中。具体方法在ExceptionHandlerMethodResolver构造函数里,具体代码如下所示:

public class ExceptionHandlerMethodResolver{
    
    private final Map<Class<? extends Throwable>, Method> mappedMethods = new HashMap<>(16);
    
    //被@ExceptionHandler注解的方法
    public static final MethodFilter EXCEPTION_HANDLER_METHODS = method ->
			AnnotatedElementUtils.hasAnnotation(method, ExceptionHandler.class);
	
    /**
     * 构造方法
     */
    public ExceptionHandlerMethodResolver(Class<?> handlerType) {
        //遍历被@ExceptionHandler注解的方法
		for (Method method : MethodIntrospector.selectMethods(handlerType, EXCEPTION_HANDLER_METHODS)) {
			for (Class<? extends Throwable> exceptionType : detectExceptionMappings(method)) {
				addExceptionMapping(exceptionType, method);
			}
		}
	}

    /**
     * 提取@ExceptionHandler里的value,然后放到result里
     */
    private void detectAnnotationExceptionMappings(Method method, List<Class<? extends Throwable>> result) {
		ExceptionHandler ann = AnnotatedElementUtils.findMergedAnnotation(method, ExceptionHandler.class);
		Assert.state(ann != null, "No ExceptionHandler annotation");
		result.addAll(Arrays.asList(ann.value()));
	}

}

        上面的afterPropertiesSet执行完后,就将adviceBean放到了exceptionHandlerAdviceCache中。在后续Controller接口异常的时候会用到。

2、Controller接口异常如何进入exceptionHandler方法

        可以从DispatcherServlet#doDispatch方法入手,具体代码如下所示:

protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
		HttpServletRequest processedRequest = request;
		HandlerExecutionChain mappedHandler = null;
		boolean multipartRequestParsed = false;

		WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);

		try {
			ModelAndView mv = null;
			Exception dispatchException = null;

			try {
			    ...省略代码

				// 
				mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

				if (asyncManager.isConcurrentHandlingStarted()) {
					return;
				}

				applyDefaultViewName(processedRequest, mv);
				mappedHandler.applyPostHandle(processedRequest, response, mv);
			}
			catch (Exception ex) {
				dispatchException = ex;
			}
            ...省略代码
			processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
		}
		...省略代码
	}

        当执行Controller过程产生异常时,会进入catch方法,将异常赋值给dispatchException,具体代码是dispatchException = ex; 然后执行processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);processDispatchResult代码如下所示:

private void processDispatchResult(HttpServletRequest request, HttpServletResponse response,
			@Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv,
			@Nullable Exception exception) throws Exception {

		boolean errorView = false;

		if (exception != null) {
			if (exception instanceof ModelAndViewDefiningException) {
				logger.debug("ModelAndViewDefiningException encountered", exception);
				mv = ((ModelAndViewDefiningException) exception).getModelAndView();
			}
			else {
				Object handler = (mappedHandler != null ? mappedHandler.getHandler() : null);
				mv = processHandlerException(request, response, handler, exception);
				errorView = (mv != null);
			}
		}
        ...省略代码
	}

        上述代码如果Exception不是ModelAndViewDefiningException,就会执行processHandlerException方法。processHandlerException方法会遍历所有HandlerExceptionResolver,找到一个不为空的ModelAndView,processHandlerException代码如下所示:

	@Nullable
	protected ModelAndView processHandlerException(HttpServletRequest request, HttpServletResponse response,
			@Nullable Object handler, Exception ex) throws Exception {

		// Success and error responses may use different content types
		request.removeAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE);

		// Check registered HandlerExceptionResolvers...
		ModelAndView exMv = null;
		if (this.handlerExceptionResolvers != null) {
			for (HandlerExceptionResolver resolver : this.handlerExceptionResolvers) {
                //遍历所有HandlerExceptionResolver
				exMv = resolver.resolveException(request, response, handler, ex);
				if (exMv != null) {
					break;
				}
			}
		}
		...省略代码
	}

        上述代码resolver.resolveException(request, response, handler, ex);最终会调用到ExceptionHandlerExceptionResolver的doResolveHandlerMethodException方法。具体代码如下所示:

	@Override
	@Nullable
	protected ModelAndView doResolveHandlerMethodException(HttpServletRequest request,
			HttpServletResponse response, @Nullable HandlerMethod handlerMethod, Exception exception) {
        //从缓存中获取该异常对应的handle方法
		ServletInvocableHandlerMethod exceptionHandlerMethod = getExceptionHandlerMethod(handlerMethod, exception);
		if (exceptionHandlerMethod == null) {
			return null;
		}
        ...省略代码

		try {
			...省略代码
			else {
				//执行具体的handle方法,也就是被@ExceptionHandler修饰的方法
				exceptionHandlerMethod.invokeAndHandle(webRequest, mavContainer, exception, handlerMethod);
			}
		}
		...省略代码
	}

        exceptionHandlerMethod.invokeAndHandle(webRequest, mavContainer, exception, handlerMethod);最终会执行异常对应的处理方法,也就是上文中的exceptionHandler方法。然后exceptionHandler方法就可以根据自定义的逻辑组装返回的数据结构,可以实现将验证框架里的message返回给前端。

五、总结

        本文首先介绍数据校验框架的标准JSR-303,然后介绍Spring validation,接着简单对比了@Validated和@Valid的差异。对于全局异常处理使用具体的代码例子进行阐述,最后深入代码对全局异常的原理进行解析。

参考

  1. notnull注解_参数校验注解Validated和Valid的区别,这次终于有人说清楚了 - 百度文库
  2. @Valid与@Validated区别_凯里欧文的博客-CSDN博客_valid和validated的区别
  3. @RestControllerAdvice与@ControllerAdvice的区别_假装Java大神的博客-CSDN博客

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Spring参数校验和全局异常处理 的相关文章

随机推荐

  • RT-Thread uart2串口dma idle接收不断帧

    硬件STM32F407 IDE使用RT Thread Studio uart2串口使用这两个引脚 功能 IO端口 UART2 TX PA2 UART2 RX PA3 UART2 DMA接收配置 先使能DMA接收 RX缓冲区可以稍微调大些 b
  • Vuetify笔记(5):data-tables组件

    v data table 用于显示表格数据 功能包括排序 搜索 分页 行内编辑 头部提示以及行选择 而我们在实际应用中使用最多的就是服务端分页和排序 如果你从后台加载数据 并希望显示结果之前进行分页和排序 你可以使用 total items
  • 对象的构造和析构

    对象的构造和析构 1 对象的初始化和清理 构造函数 和 析构函数 被编译器自动调用完成对象初始化和对象清理工作 2 构造函数 和 析构函数 构造函数写法 与类名相同 没有返回值 不写void 可以有参数 可以发生重载 构造函数由编译器自动调
  • 神经网络学习——图像篡改

    记录 这是课堂上做的一个关于图像篡改识别的题目 因为前后花的时间比较多 虽然最后实现的效果也不怎么行 但是这个过程踩了很多坑 这里记录一下 文章目录 记录 前提 题目分析 网络搭建 依赖包 数据读取处理 网络搭建 训练参数 预测函数 模型保
  • linux——read和write函数实现cp、用户级缓冲预读入缓冲的简单认识

    用read和write实现cp 1 注意头文件 2 fd1是源文件 传入参数中的第一个 fd2是目标文件 传入参数中的第二个 要写入的文件至少可写 如果没有当前文件就创建文件并设置权限 如果已经有文件就截断为0再重新写 3 定义一个缓冲区
  • vue2和vue3的响应式原理

    vue2和vue3的响应式原理 vue2 的响应式 vue3 的响应式 vue2 的响应式 使用 Object 构造函数上 defineProperty 实现 存在的问题 对象 新增的属性没有响应式 数组 部分操作没有响应式 解决办法 1
  • [推荐系统] 1. 深度学习与推荐系统

    文章目录 1 推荐系统 1 1 推荐系统的作用和意义 1 2 推荐系统架构 1 2 1 推荐系统的逻辑架构 1 2 2 推荐系统的技术架构 2 前置知识 2 1 传统推荐模型的演化 2 2 协同过滤 2 2 1 概述 2 2 2 用户相似度
  • uniapp nfc读写

    1 先添加权限
  • MySQL中show语法

    1 show tables或show tables from database name 显示当前数据库中所有表的名称 2 show databases 显示mysql中所有数据库的名称 3 show columns from table
  • Git Rebase与Merge

    在 Git 中整合来自不同分支的修改主要有两种方法 merge 以及 rebase 两种观点 有一种观点认为 仓库的提交历史即是记录实际发生过什么 它是针对历史的文档 本身就有价值 不能乱改 从这个角度看来 改变提交历史是一种亵渎 你使用
  • [Android]系统启动时序

    最近公司的群里面有大佬分享了一张自己总结的系统启动时序 觉得画的非常清晰简练 因此在这里分享一下
  • I - Intense Bit Wheel (二进制,bitset)

    SDUT 2022 Summer Individual Contest 2 for 21 Virtual Judge There is a new intense giant wheel in UNAL town in UNAL town
  • uniapp整包apk更新下载后安装完删除下载的apk,避免占用app内存

    一 内存大 app版本更新后内存变的好大 更新多几次版本更大 小编便发现是每次更新后都把下载下来的apk大小增加到app里面去了 那么我们如何版本更新完之后删除下载下来的apk文件呢 避免占用内存 二 解决方案 使用html5 功能IO模块
  • NLP预训练模型-GPT-3

    NLP预训练模型 系列文章 1 BERT 2 GPT 3 GPT 2 4 GPT 3 目录 NLP预训练模型系列文章 文章目录 前言 1 Abstract 2 Introduction 3 Approach 3 1 模型和架构 3 2 训练
  • oracle11g 导出表报EXP-00011:table不存在。

    转自 https blog csdn net mingzaiwang article details 52608991 depth 1 utm source distribute pc relevant none task utm sour
  • 8种提升程序猿编程能力的方法+编程思维四个核心:分解、抽象、模式识别和算法

    8种提升程序猿编程能力的方法 对于程序员来说 提高自己的编程能力 算是给自己定的职业发展目标之一 不过定一个成为编程大神的目标很容易 具体做起来可能就不是一件简单的事了 首先 既然决定 我要变得更好 得先知道 更好 是什么样子的 另外 不能
  • C++ 单链表节点交换

    这里提供两种方法 一种是只交换对应的数据 另一种是通过更改指针来交换节点 而更改指针中又可以分为新建节点与不新建节点的方法 1 不更改指针 这个没啥好说的 直接将对应的data交换即可 这里的a c节点都为被交换节点的上一个节点 因为不更改
  • el-form之表单校验自动定位到报错位置

    1 背景 表单校验大多数的表单都会用到 一般情况下只是提示当前哪些项校验不通过 但是如果表单比较需要用户自己去找是哪项校验不通过 这样的用户体验不太好 如果能自动定位到当前校验不通过的表单项体验会更好一些 这里是以elementui 的 e
  • 主线剧情-番外02-设备树详解

    设备树详解 本文 续接 主线剧情03 NXP i MX 系列 u boot 移植基础详解 一文中移植过程小节中有关设备树的内容 编辑整理 By Staok 如有错误恭谢指出 侵删 CC BY NC SA 4 0 注意 本文适合学习设备树的一
  • Spring参数校验和全局异常处理

    目录 一 前言 二 Validation 1 JSR 303 2 Spring Validation 3 Validated和 Valid的区别 三 全局异常处理 1 为何要处理异常 2 RestControllerAdvice 3 返回自