2024年8月

写在前面

博主最近在做一个数据服务的项目,而这个数据服务的核心就是对外暴露的API,值得高兴的这是一个从0开始的项目,所以终于不用受制于“某些历史”因素去续写各种风格的Controller,可以在项目伊始就以规范的技术和统一形式去搭建API。借此机会,梳理和汇总一下基于SpringBoot项目开发REST API的技术点和规范点。

接口服务主要由两部分组成,即参数(输入)部分,响应(输出)部分。其中在SpringBoot中主要是Controller层作为API的开发处,其实在架构层面来讲,
Controller
本身是一个最高的应用层,它的职责是调用、组装下层的interface服务数据,核心是组装和调用,不应该掺杂其他相关的逻辑。

但是往往很多项目里针对Controller部分的代码都是十分混乱,有的
Controller
兼顾各种if else的参数校验,有的甚至直接在Controller进行业务代码编写;对于
Controller
的输出,有的粗略的加个外包装,有的甚至直接把service层的结构直接丢出去;对于异常的处理也是各种各样。

以上对于
Controller
相关的问题,这里统一用一系列
Controller
的封装处理来提供优化思路。优雅且规范的开发REST API需要做以下几步:

  • 接口版本控制
  • 参数校验
  • 异常捕获处理
  • 统一响应封装
  • 接口文档的维护和更新

@RestController注解

直接来看@RestController源码

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Controller
@ResponseBody
public @interfaceRestController {
@AliasFor(
annotation
= Controller.class)
String value()
default "";
}

@RestController注解等价于@Controller和@@ResponseBody,@ResponseBody注解的作用是告诉Spring MVC框架,该方法的返回值应该直接写入HTTP响应体中,而不是返回一个视图(View)。当一个控制器方法被标记为
@ResponseBody
时,Spring MVC会将方法的返回值序列化成JSON或XML等格式,然后发送给客户端。更适用于REST API的构建。

所以针对Controller接口的开发,
直接使用@RestController为好。它会自动将Controller下的方法返回内容转为REST API的形式

例如:

@RestController
@RequestMapping(
"/dataserver/manage")public classDataServerController{

@PostMapping(
"/search")publicResponse searchData(@RequestBody SearchTaskDto param){returnResponse.success(taskScheduleManagerService.searchTaskForPage(param));
}
}

接口版本管理

对于API来讲,一般是对外服务的基础,不能随意变更,但是随着需求和业务不断变化,接口和参数也会发生相应的变化。此时尽可能保证“开闭原则”,以新增接口或增强接口功能来支撑,此时就需要对API的版本进行维护,以版本号来确定同一接口的不同能力,一般版本都基于url来控制

例如:

  • http://localhost:8080/dataserver/v1/queryAccount
  • http://localhost:8080/dataserver/v2/queryAccount:相比v1版本增强了参数查询的灵活性

进行API版本控制主要分三步:

  • 定义版本号注解
  • 编写版本号匹配逻辑处理器
  • 注册处理器

定义版本号注解

/*** API版本控制注解*/@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interfaceApiVersion {/***版本号,默认为1*/ int value() default 1;
}

该注解可直接使用在Controller类上

@RestController
@RequestMapping(
"dataserver/{version}/account")
@ApiVersion(
2)//输入版本号,对应{version} public classAccountController{
@GetMapping(
"/test")publicString test() {return "XXXX";
}
}

编写版本号匹配逻辑处理器

首先定义一个条件匹配类,对应解析Url中的version与ApiVersion注解

/***实现Request的条件匹配接口
*
*
*/ public class ApiVersionCondition implements RequestCondition<ApiVersionCondition>{private final static Pattern VERSION_PREFIX_PATTERN = Pattern.compile(".*v(\\d+).*");private intapiVersion;

ApiVersionCondition(
intapiVersion) {this.apiVersion =apiVersion;
}
private intgetApiVersion() {returnapiVersion;
}


@Override
publicApiVersionCondition combine(ApiVersionCondition apiVersionCondition) {return newApiVersionCondition(apiVersionCondition.getApiVersion());
}

@Override
publicApiVersionCondition getMatchingCondition(HttpServletRequest httpServletRequest) {
Matcher m
=VERSION_PREFIX_PATTERN.matcher(httpServletRequest.getRequestURI());if(m.find()) {
Integer version
= Integer.valueOf(m.group(1));if (version >= this.apiVersion) {return this;
}
}
return null;
}

@Override
public intcompareTo(ApiVersionCondition apiVersionCondition, HttpServletRequest httpServletRequest) {return apiVersionCondition.getApiVersion() - this.apiVersion;
}
}

这里补充一下
RequestCondition<ApiVersionCondition>
相关概念:

它是 Spring 框架中用于请求映射处理的一部分。在 Spring MVC 中,
RequestCondition
接口允许开发者定义自定义的请求匹配逻辑,这可以基于请求的任何属性,例如路径、参数、HTTP 方法、头部等。相关的应用场景包括:

  1. 路径匹配(Path Matching)
    :使用
    PatternsRequestCondition
    来定义请求的路径模式,支持 Ant 风格的路径模式匹配,如
    /api/*
    可以匹配所有
    /api
    开头的请求路径 。

  2. 请求方法匹配(Request Method Matching)
    :通过
    RequestMethodsRequestCondition
    来限制请求的 HTTP 方法,例如只允许 GET 或 POST 请求 。

  3. 请求参数匹配(Request Params Matching)
    :使用
    ParamsRequestCondition
    来定义请求必须包含的参数,例如某些接口可能需要特定的查询参数才能访问 。

  4. 请求头匹配(Request Headers Matching)

    HeadersRequestCondition
    允许定义请求头的条件,例如某些接口可能需要特定的认证头部才能访问 。

  5. 消费媒体类型匹配(Consumes Media Type Matching)

    ConsumesRequestCondition
    用来定义控制器方法能够处理的请求体媒体类型,通常用于 RESTful API 中,例如只处理
    application/json
    类型的请求体 。

  6. 产生媒体类型匹配(Produces Media Type Matching)

    ProducesRequestCondition
    定义了控制器方法能够返回的媒体类型,这通常与
    Accept
    请求头结合使用以确定响应的格式 。

  7. 自定义条件匹配
    :开发者可以通过实现
    RequestCondition
    接口来定义自己的匹配逻辑,例如根据请求中的版本号来路由到不同版本的 API,实现 API 的版本控制 。

  8. 组合条件匹配(Composite Conditions Matching)
    :在某些情况下,可能需要根据多个条件来匹配请求,
    CompositeRequestCondition
    可以将多个
    RequestCondition
    组合成一个条件来进行匹配 。

  9. 请求映射的优先级选择(Priority Selection for Request Mapping)
    :当存在多个匹配的处理器方法时,
    RequestCondition

    compareTo
    方法用于确定哪个条件具有更高的优先级,以选择最合适的处理器方法 。

创建一个版本映射处理器,使用
ApiVersionCondition
作为自定义条件来处理请求映射。当 Spring MVC 处理请求时,它会使用这个自定义的映射处理器来确定哪个版本的 API 应该处理请求。

public class ApiRequestMappingHandlerMapping extendsRequestMappingHandlerMapping {private static final String VERSION_FLAG = "{version}";/***检查类上是否有 @RequestMapping 注解,如果有,它会构建请求映射的 URL。如果 URL 中包含版本 
*标识 VERSION_FLAG,并且类上有 ApiVersion 注解,它将创建并返回一个 ApiVersionCondition
*实例,表示这个类关联的 API 版本。
*
*/ private static RequestCondition<ApiVersionCondition> createCondition(Class<?>clazz) {
RequestMapping classRequestMapping
= clazz.getAnnotation(RequestMapping.class);if (classRequestMapping == null) {return null;
}
StringBuilder mappingUrlBuilder
= newStringBuilder();if (classRequestMapping.value().length > 0) {
mappingUrlBuilder.append(classRequestMapping.value()[
0]);
}
String mappingUrl
=mappingUrlBuilder.toString();if (!mappingUrl.contains(VERSION_FLAG)) {return null;
}
ApiVersion apiVersion
= clazz.getAnnotation(ApiVersion.class);return apiVersion == null ? new ApiVersionCondition(1) : newApiVersionCondition(apiVersion.value());
}

@Override
protected RequestCondition<?>getCustomMethodCondition(Method method) {returncreateCondition(method.getClass());
}

@Override
protected RequestCondition<?> getCustomTypeCondition(Class<?>handlerType) {returncreateCondition(handlerType);
}
}

注册处理器

将上述的处理器注册到SpringMvc的处理流程中

@Configurationpublic class WebMvcRegistrationsConfig implementsWebMvcRegistrations {
@Override
publicRequestMappingHandlerMapping getRequestMappingHandlerMapping() {return newApiRequestMappingHandlerMapping();
}
}

验证:

@RestController
@RequestMapping(
"dataserver/{version}/account")
@ApiVersion(
1)public classAccountOneController {

@GetMapping(
"/test")publicString test() {return "测试接口,版本1";
}
@GetMapping(
"/extend")publicString extendTest() {return "版本1的测试接口延申";
}
}


@RestController
@RequestMapping(
"dataserver/{version}/account")
@ApiVersion(
2)public classAccountTwoController {
@GetMapping(
"/test")publicString test() {return "测试接口,版本2";
}
}

针对test接口进行不同版本的请求:

针对Account扩展版本调用上一版本接口

当请求对应的版本不存在接口时,会匹配之前版本的接口,即请求
/v2/account/extend
接口时,由于v2 控制器未实现该接口,所以自动匹配v1 版本中的接口。这就实现了
API版本继承

参数校验

@Validated注解

@Validated
是一个用于 Java 应用程序中的注解,特别是在 Spring 框架中,以指示目标对象或方法需要进行验证。这个注解通常与 JSR 303/JSR 380 规范的 Bean Validation API 结合使用,以确保数据的合法性和完整性。

@Validated注解的三种用法:

方法级别验证
:当
@Validated
注解用在方法上时,它指示 Spring 在调用该方法之前执行参数的验证。如果参数不符合指定的验证条件,将抛出
MethodArgumentNotValidException

@PostMapping("/user")
@Validated
publicResVo createUser(@RequestBody @Valid User user) {//方法实现 }

类级别验证
:将
@Validated
注解用在类上,表示该类的所有处理请求的方法都会进行验证。这可以减少在每个方法上重复注解的需要。

@RestController
@Validated
public classUserController {//类中的所有方法都会进行验证 }

组合注解
:Spring 还提供了
@Valid
注解,它是
@Validated
的一个更简单的形式,只触发验证并不指定特定的验证组(Validation Groups)。
@Validated
允许你指定一个或多个验证组,这在需要根据不同情况执行不同验证规则时非常有用。

@Validated(OnCreate.class)public voidcreateUser(User user) {//只使用 OnCreate 组的验证规则
}

使用注解进行参数校验

在REST API中进行参数验证一般使用方法级别验证即可,即对参数Dto的类内信息进行验证,例如一个分页的查询参数类:

@Datapublic class BaseParam implementsSerializable {

@NotNull(message
= "必须包含关键字")privateString keyFilter;

@Min(value
= 1,message = "页码不可小于1")private intpageNo;

@Max(value
= 100,message = "考虑性能问题,每页条数不可超过100")private intpageSize;

}

在Controller中配合@Validated使用:

  @PostMapping("/findProductByVo")publicPageData findByVo(@Validated ProductParam param) {//……业务逻辑
        returnPageData.success(data);
}

此时如果前端传入参数不合法,例如pageNo为0又或者productType不存在,则会抛出
MethodArgumentNotValidException
的异常。稍后对于异常进行处理即可完成参数的验证。

这里的@Max

@Min

@NotNull
注解属于 Bean Validation API 的一部分,这是一个 JSR 303/JSR 380 规范,用于在 Java 应用程序中提供声明式验证功能。这些注解用于约束字段值的范围和非空性。类似的注解还有:

注解 作用

@NotNull

验证注解的字段值不能为
null
@NotEmpty
@NotNull
类似,但用于集合或字符串,验证注解的字段值不能为
null
,且对于字符串,长度不能为 0。
@NotBlank 验证注解的字段值不能为
null
,且不能是空白字符串(空白包括空格、制表符等)。
@Min(value) 验证注解的字段值是否大于或等于指定的最小值。
value
参数接受一个整数。
@Max(value) 验证注解的字段值是否小于或等于指定的最大值。
value
参数接受一个整数。
@Size(min, max) 验证字符串或集合的大小在指定的最小值和最大值之间。
@Pattern(regex) 验证字段值是否符合指定的正则表达式。

注:SpringBoot 2.3.1 版本默认移除了校验功能,如果想要开启的话需要添加以上依赖

<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
        </dependency>

统一异常捕获

@RestControllerAdvice注解

@RestControllerAdvice
是 @ResponseBody+@ControllerAdvice的集合注解,用于定义一个控制器级别的异常处理类。一般用来进行全局异常处理,在
@RestControllerAdvice
类中处理异常后,可以直接返回一个对象,该对象会被转换为 JSON 或 XML 响应体,返回给客户端。

使用@RestControllerAdvice注解处理参数异常


使用@Validated和 Bean Validation API 的注解进行参数校验后,当出现不符合规定的参数会抛出
MethodArgumentNotValidException
异常

,这里就可以使用@RestControllerAdvice注解来创建一个全局Controller异常拦截类,来统一处理各类异常

@RestControllerAdvicepublic classControllerExceptionAdvice {

@ExceptionHandler({MethodArgumentNotValidException .
class})//此处可以根据参数异常的各类情况进行相关异常类的绑定 publicString MethodArgumentNotValidExceptionHandler(MethodArgumentNotValidException e) {//从异常对象中拿到ObjectError对象 ObjectError objectError = e.getBindingResult().getAllErrors().get(0);return "参数异常错误";
}
}

这里只以
MethodArgumentNotValidException 异常进行拦截,在
@RestControllerAdvice类内可以创建多个方法,通过@ExceptionHandler对不同的异常进行定制化处理,这样当Controller内发生异常,都可以在@RestControllerAdvice类内进行截获、处理、返回给客户端安全的信息。

@RestControllerAdvicepublic classControllerExceptionAdvice {//HttpMessageNotReadableException异常为webJSON解析出错
    @ExceptionHandler({HttpMessageNotReadableException.class})publicString MethodArgumentNotValidExceptionHandler(HttpMessageNotReadableException e) 
{
return "参数错误";
}

@ExceptionHandler({XXXXXException .
class})publicString otherExceptionHandler(Exception e) {

ObjectError objectError
= e.getBindingResult().getAllErrors().get(0);returnobjectError..getDefaultMessage();
}
}

统一响应封装

首先,进行统一的响应格式,这里需要封装一个固定返回格式的结构对象:
ResponseData

public class Response<T> implementsSerializable {privateInteger code;privateString msg;privateT data;publicResponse() {this.code = 200;this.msg = "ok";this.data = null;
}
publicResponse(Integer code, String msg, T data) {this.code =code;this.msg =msg;this.data =data;
}
publicResponse(String msg, T data) {this(200, msg, data);
}
publicResponse(T data) {this("ok", data);
}
public static <T> Response<T>success(T data) {return newResponse(data);
}
publicInteger getCode() {return this.code;
}
public voidsetCode(Integer code) {this.code =code;
}
publicString getMsg() {return this.msg;
}
public voidsetMsg(String msg) {this.msg =msg;
}
publicT getData() {return this.data;
}
public voidsetData(T data) {this.data =data;
}
publicString toJsonString() {
String out
= "";try{
out
= JSONUtil.toJsonPrettyStr(this);
}
catch(Exception var3) {this.setData(null);
var3.printStackTrace();
}
returnout;
}
}

统一状态码

其中对于相关的状态码最好进行统一的封装,便于以后的开发,创建状态枚举:

//面向接口开发,首先定义接口
public interfaceStatusCode {
Integer getCode();
String getMessage();
}
//创建枚举类 public enum ResponseStatus implementsStatusCode{//正常响应 SUCCESS(200, "success"),//服务器内部错误 FAILED(500, " Server Error"),//参数校验错误 VALIDATE_ERROR(400, "Bad Request");//……补充其他内部约定状态 private intcode;privateString msg;

ResponseStatus(
intcode, String msg) {this.code =code;this.msg =msg;
}

@Override
publicInteger getCode() {return this.code;
}

@Override
publicString getMessage() {return this.msg;
}
}

统一返回结构

将上述的
ResponseData
中状态类相关替换为枚举

public class Response<T> implementsSerializable {privateInteger code;privateString msg;privateT data;publicResponse() {this.code = 200;this.msg = "success";this.data = null;
}
publicResponse(StatusCode status, T data) {this.code =status.getCode();this.msg =status.getMssage();this.data =data;
}
publicResponse(T data) {this(ResponseStatus.SUCCESS, data);
}
public static <T> Response<T>success(T data) {return newResponse(data);
}
publicInteger getCode() {return this.code;
}
public voidsetCode(Integer code) {this.code =code;
}
publicString getMsg() {return this.msg;
}
public voidsetMsg(String msg) {this.msg =msg;
}
publicT getData() {return this.data;
}
public voidsetData(T data) {this.data =data;
}
publicString toJsonString() {
String out
= "";try{
out
= JSONUtil.toJsonPrettyStr(this);
}
catch(Exception var3) {this.setData(null);
var3.printStackTrace();
}
returnout;
}
}

这样Controller的接口统一返回格式就是标准的结构了。

{"code":200,"msg":"success","data":{"total":123,"record":[]
}
}

统一封装Controller响应

有了统一响应体的Controller在返回时可以这样写:

    @PostMapping("/search")
@Operation(summary
= "分页查询任务")publicResponse searchData(@RequestBody SearchParam param){returnResponse.success(XXXXService.searchForPage(param));
}

即便如此,团队开发中可能还会出现换个人新写Controller不知道有统一返回体这回事,为了更保险,可以通过AOP进行统一对结果进行封装,不论Controller返回啥,到客户端的数据都包含一个包装体。

具体实现是使用
@RestControllerAdvice类
实现
ResponseBodyAdvice
接口来完成。

@RestControllerAdvicepublic class ControllerResponseAdvice implements ResponseBodyAdvice<Object>{
@Override
public boolean supports(MethodParameter methodParameter, Class<? extends HttpMessageConverter<?>>aClass) {//返回结构是Response类型都不进行包装 return !methodParameter.getParameterType().isAssignableFrom(Response.class);
}

@Override
public Object beforeBodyWrite(Object data, MethodParameter returnType, MediaType mediaType, Class<? extends HttpMessageConverter<?>>aClass, ServerHttpRequest request, ServerHttpResponse response) {//String类型不能直接包装 if (returnType.getGenericParameterType().equals(String.class)) {
ObjectMapper objectMapper
= newObjectMapper();try{//将数据包装在ResultVo里后转换为json串进行返回 returnobjectMapper.writeValueAsString(Response.success(data));
}
catch(JsonProcessingException e) {
e.printStackTrace();
}
}
//其他所有结果统一包装成Response返回 returnResponse.success(data);
}
}

我们以test接口为例,test接口原本返回的是String,而toint返回的是Integer

@RestController
@RequestMapping(
"dataserver/{version}/account")
@ApiVersion(
1)public classAccountOneController {

@GetMapping(
"/test")publicString test() {return "测试接口,版本1";
}

@GetMapping(
"/toint")publicInteger toint() {return 1;
}

}

但是页面返回是JSON字符串和返回体:

文档:调试维护API利器—Swagger

接口开发完成,调试时,大多数都是使用Postman模拟请求调试或者直接用前端代码调用调试,其实这两种都比较麻烦,尤其面对复制参数时,Postman要逐个接口的录入,十分费事,其实这里可以在SpringBoot中引入Swagger接口文档组件,接口文档和接口调试一并解决。

引入依赖

直接在maven中引入相关依赖:

     <!--swagger 3-->
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-boot-starter</artifactId>
            <version>3.0.0</version>
        </dependency>
        <dependency>
            <groupId>com.github.xiaoymin</groupId>
            <artifactId>knife4j-spring-boot-starter</artifactId>
            <version>3.0.3</version>
        </dependency>

标准的swagger3引入以上两个依赖即可,相关版本可自行选择

装配配置类

下面进行swagger的配置类和一些swagger相关页面的配置

@Configurationpublic classSwaggerConfig {


@Bean
publicDocket testApis(){return newDocket(DocumentationType.OAS_30)
.apiInfo(apidoc())
.select()
.apis(RequestHandlerSelectors.basePackage(
"net.gcc.webrestapi.controller"))
.paths(PathSelectors.any())
.build()
.groupName(
"测试服务")
.enable(
true);
}
privateApiInfo apidoc(){return newApiInfoBuilder()
.title(
"测试接口")
.description(
"接口文档")
.contact(
new Contact("GCC", "#", "XXXX"))
.version(
"1.0")
.build();
}
}

使用注解

Swagger相关注解明细

注解 使用位置 作用
@Api 作用在类上,
Controller
表示对类的说明,通常用于描述 Controller 的作用和分类,如
@Api(tags = "用户管理"),后续会在Swagger文档中以 目录形式 展示
@ApiOperation 作用在方法上,一般为
Controller中具体方法
描述 API 接口的具体操作和功能,例如
@ApiOperation(value = "获取用户列表", notes = "根据条件获取用户列表")
,在swagger文档中以目录内容体现
@ApiModel 作用于类上,一般是
参数实体类
表示这是一个模型类,通常与
@ApiModelProperty
结合使用来描述模型属性 。
@ApiModelProperty 用于模型类的属性上,
参数类的成员变量
描述属性的信息,如
@ApiModelProperty(value = "用户名", required = true)
@ApiImplicitParams

@ApiImplicitParam
用于方法上,一般为
Controller中具体方法
描述接口的隐含参数,例如请求参数或请求头信息
@ApiResponses

@ApiResponse
用于方法上,一般为
Controller中具体方法
描述接口的响应信息,可以指定不同的响应状态码和对应的描述信息 。
@ApiIgnore 用于类或方法上 表示忽略该类或方法,不将其显示在Swagger文档中。

@Api

@ApiOperation
使用

@RestController
@RequestMapping(
"/dataserver/{version}/manage")
@Api(tags
= "数据源管理服务", description = "用于管理数据源信息")
@ApiVersion
public classDataServerController {

@PostMapping(
"/search")
@ApiOperation(summary
= "分页查询数据源")public IPage<DataSourceEntity>searchData(@RequestBody SearchParam param){//XXXX逻辑 return new IPage<DataSourceEntity>();
}


}

@ApiMode

@ApiModelProperty

@Data
@ApiModel(value
= "基础参数")public class BaseParam implementsSerializable {

@ApiModelProperty(value
= "关键字", required = true)
@NotNull(message
= "必须包含关键字")privateString keyFilter;
@ApiModelProperty(value
= "页码", required = true)
@Min(value
= 1,message = "页码不可小于1")private intpageNo;
@ApiModelProperty(value
= "每页大小", required = true)
@Max(value
= 100,message = "考虑性能问题,每页条数不可超过100")private intpageSize;

}

@ApiImplicitParams

@ApiImplicitParam

与ApiMode和ApiModeProperty功能一致,一般用于get请求中的参数描述

   @GetMapping("/extend")
@ApiOperation(value
= "账号角色",notes = "测试版本1延申接口")
@ApiImplicitParams({
@ApiImplicitParam(value
= "accountId",name = "账号ID"),
@ApiImplicitParam(value
= "role",name = "角色")
}
)
publicString extendTest(String accountId,String role) {return new JSONObject().set("account",accountId).set("role",role).toJSONString(0);
}

效果

使用swagger后,直接在页面访问 http://127.0.0.1:8080/XXX/doc.html即可访问接口页面

不要复杂的postman调用,本地调试可以直接使用调试功能

补充:完整的Controller类代码模板

@RestController
@RequestMapping(
"/dataserver/{version}/manage")
@Api(tags
= "数据源管理服务V1")
@ApiVersion
public classDataServerController {

@PostMapping(
"/search")
@ApiOperation(value
= "分页查询数据源",notes = "测试")public PageVo<DataSourceVo>searchData(@RequestBody BaseParam param){//XXXX逻辑 return new PageVo<DataSourceVo>();
}
//get请求,使用ApiImplicitParams注解标明参数 @GetMapping("/searchAccountAndRole")
@ApiOperation(value
= "账号角色",notes = "查询账号角色")
@ApiImplicitParams({
@ApiImplicitParam(value
= "accountId",name = "账号ID"),
@ApiImplicitParam(value
= "role",name = "角色")
})
publicString extendTest(String accountId,String role) {return new JSONObject().set("account",accountId).set("role",role).toJSONString(0);
}

}
//部分参数代码: @Data
@ApiModel
public class BaseParam implementsSerializable {
@NotNull(message
= "必须包含关键字")
@ApiModelProperty(
"关键字过滤")privateString keyFilter;

@Min(value
= 1,message = "页码不可小于1")
@ApiModelProperty(
"分页页码")private intpageNo;

@Max(value
= 100,message = "考虑性能问题,每页条数不可超过100")
@ApiModelProperty(
"分页每页条数")private intpageSize;

}
//响应部分代码 @Data
@ApiModel
public class DataSourceVo implementsSerializable {
@ApiModelProperty(
"id")privateString id;
@ApiModelProperty(
"数据源名称")privateString name;
@ApiModelProperty(
"数据源url")privateString url;
}

@Data
@ApiModel
public class PageVo<V>{

@ApiModelProperty(
"总数量")private inttotal;
@ApiModelProperty(
"具体内容")private List<V>rows;
}

补充:完整的@RestControllerAdvice类代码模板

关于参数验证的异常处理和统一返回结构,可以使用一个类来完成,以下是完整模板:

@RestControllerAdvice(basePackages = "net.gcc.webrestapi")public class ControllerExceptionAdvice implements ResponseBodyAdvice<Object>{


@Override
public boolean supports(MethodParameter methodParameter, Class<? extends HttpMessageConverter<?>>aClass) {//返回结构是Response类型都不进行包装 return !methodParameter.getParameterType().isAssignableFrom(Response.class);
}

@Override
public Object beforeBodyWrite(Object data, MethodParameter returnType, MediaType mediaType, Class<? extends HttpMessageConverter<?>>aClass, ServerHttpRequest request, ServerHttpResponse response) {//String类型不能直接包装 if (returnType.getGenericParameterType().equals(String.class)) {
ObjectMapper objectMapper
= newObjectMapper();try{//将数据包装在ResultVo里后转换为json串进行返回 returnobjectMapper.writeValueAsString(Response.success(data));
}
catch(JsonProcessingException e) {
e.printStackTrace();
}
}
//系统特殊错误 if(data instanceofLinkedHashMap&& ((LinkedHashMap<?, ?>) data).containsKey("status")&& ((LinkedHashMap<?, ?>) data).containsKey("message")&&((LinkedHashMap<?, ?>) data).containsKey("error")){int code = Integer.parseInt(((LinkedHashMap<?, ?>) data).get("status").toString());
String mssage
= ((LinkedHashMap<?, ?>) data).get("error").toString();return new Response<>(code,mssage,null);
}
//其他所有结果统一包装成Response返回 returnResponse.success(data);
}


@ExceptionHandler({MethodArgumentNotValidException.
class})publicResponse MethodArgumentNotValidExceptionHandler(MethodArgumentNotValidException e)
{
//默认统一返回响应体,填写参数错误编码, 从异常对象中拿到错误信息 return new Response(401,e.getBindingResult().getAllErrors().get(0).getDefaultMessage(),"");
}
//HttpMessageNotReadableException异常为webJSON解析出错 @ExceptionHandler({HttpMessageNotReadableException.class})publicResponse HttpNotReqadableExceptionHandler(HttpMessageNotReadableException e)
{
//默认统一返回响应体,填写参数错误编码, 从异常对象中拿到错误信息 return new Response(401,"参数解析错误","");
}

}

《软件性能测试分析与调优实践之路》(第2版)  是清华大学出版社出版的一本图书,作者为张永清,全书共分为9章,如下图所示

图书介绍:
《软件性能测试分析与调优实践之路》(第2版)

1、为什么需要性能测试与分析

1)、了解系统的各项性能指标,通过性能压测来了解系统能承受多大的并发访问量、系统的平均响应时间是多少、系统的TPS是多少等。
2)、发现系统中存在的性能问题,常见的性能问题如下:

  • 系统中是否存在负载均衡不均的情况。负载均衡不均匀一般指的是在并发的情况下,每台服务器接收的并发压力不均匀,从而导致部分服务器因为压力过大而出现性能急剧下降,以及部分服务器因为并发过小而出现资源浪费的情况。
  • 系统中是否存在内存泄漏问题。内存泄漏是指应用程序代码在每次执行完后,不会主动释放内存资源而导致内存使用一直增加,最终会使服务器物理内存全部耗光,程序运行逐渐变慢,最终因为无法申请到内存而退出运行。内存泄漏多数情况下是非常缓慢的增加,不容易被发现,一般需要通过高并发性能压测才能暴露。
  • 系统中是否存在连接泄漏问题。连接泄漏种类非常广泛,可以是数据库连接泄漏、HTTP连接泄漏或者其他的TCP/UDP连接泄漏等。除了系统实际情况需要建立长连接外,一般短连接都应该是用完就需要关闭和释放。
  • 系统中是否存在线程安全问题。线程安全问题是在高并发访问的多线程处理系统中经常会出现的问题,如果系统中存在线程安全问题,就会出现多个线程先后更改数据,造成所得到的数据全部是脏数据,有时候甚至会造成巨大的经济损失。
  • 系统中是否存在死锁问题。死锁问题也是多线程系统中经常会遇到的一个经典问题,一般常见的有系统死锁、数据库死锁等。
  • 系统中是否存在网络架构或者应用架构扩展性问题。扩展性问题一般是指在性能指标无法满足预期的情况下,通过横向或者纵向扩展硬件资源后,系统性能指标无法按照一定的线性规律进行快速递增。
  • 发现系统的性能瓶颈在何处。性能瓶颈一般是指因为某些因素而造成系统的性能无法持续上升。

3)解决性能压测中存在的问题和性能瓶颈,通过不断的性能调优,使得系统可以满足预期的性能指标。

2、性能分析与调优实践(流程)

《软件性能测试分析与调优实践之路》(第2版) 一书中总结如下:

3、性能分析与调优实践(调优模型)

《软件性能测试分析与调优实践之路》(第2版) 一书中总结如下:

4、性能分析与调优实践(分层分析)

下图展示了《软件性能测试分析与调优实践之路》(第2版) 一书中如何从上到下或者从下到上来分层分析性能问题

5、性能分析与调优实践(推理论证)

下图展示了如何通过推理论证的方式来分析性能问题

6、常见的性能调优方式:

1)、性能分析与调优实践(缓存调优)

《软件性能测试分析与调优实践之路》(第2版) 一书中详细论证了如何来解决如下缓存中面临的问题:

  • (1)如何让缓存的命中率更高?
  • (2)如何注意防止缓存穿透?
  • (3)如何控制好缓存的失效时间和失效策略?
  • (4)如何做好缓存的监控分析?
  • (5)如何防止缓存雪崩?

2)、性能分析与调优实践(同步转异步)

3)、性能分析与调优实践(削峰填谷)

4)、性能分析与调优实践(拆分)

5)、性能分析与调优实践(任务分解与并行计算)

6)、性能分析与调优实践(索引与分库分表)

  • 按照冷热数据分离的方式:一般将使用频率较高的数据称为热数据,查询频率较低或者几乎不被查询的数据称为冷数据。冷热数据分离后,热数据单独存储,这样数据量就会下降下来,查询的性能自然也就提升了,而且还可以更方便地单独针对热数据进行I/O性能调优。
  • 按照时间维度的方式:比如可以按照实时数据和历史数据分库分表,也可以按照年份、月份等时间区间进行分库分表,目的是尽可能地减少每个库表中的数据量。
  • 按照一定的算法计算的方式:此种方式一般适用于数据都是热数据的情况,比如数据无法做冷热分离,所有的数据都经常被查询到,而且数据量又非常大。此时就可以根据数据中的某个字段执行算法(注意:这个字段一般是数据查询时的检索条件字段),使得数据插入后能均匀地落到不同的分表中去(由算法决定每条数据是进入哪个分表),查询时再根据查询条件字段执行同样的算法,就可以快速定位到是需要到哪个分表中去进行数据查询。

前言

在上一篇
给我5分钟,保证教会你在vue3中动态加载远程组件
文章中,我们通过
defineAsyncComponent
实现了动态加载远程组件。这篇文章我们将通过debug源码的方式来带你搞清楚
defineAsyncComponent
是如何实现异步组件的。注:本文使用的vue版本为
3.4.19

欧阳写了一本开源电子书
vue3编译原理揭秘
,这本书初中级前端能看懂。完全免费,只求一个star。

看个demo

还是一样的套路,我们来看个
defineAsyncComponent
异步组件的demo。

本地子组件
local-child.vue
代码如下:

<template>
  <p>我是本地组件</p>
</template>

异步子组件
async-child.vue
代码如下:

<template>
  <p>我是异步组件</p>
</template>

父组件
index.vue
代码如下:

<template>
  <LocalChild />
  <button @click="showAsyncChild = true">load async child</button>
  <AsyncChild v-if="showAsyncChild" />
</template>

<script setup lang="ts">
import { defineAsyncComponent, ref } from "vue";
import LocalChild from "./local-child.vue";

const AsyncChild = defineAsyncComponent(() => import("./async-child.vue"));
const showAsyncChild = ref(false);
</script>

我们这里有两个子组件,第一个
local-child.vue
,他和我们平时使用的组件一样,没什么说的。

第二个子组件是
async-child.vue
,在父组件中我们没有像普通组件
local-child.vue
那样在最上面import导入,而是在
defineAsyncComponent
接收的回调函数中去动态import导入
async-child.vue
文件,这样定义的
AsyncChild
组件就是异步组件。

在template中可以看到,只有当点击
load async child
按钮后才会加载异步组件
AsyncChild

我们先来看看执行效果,如下gif图:
demo

从上面的gif图可以看到,当我们点击
load async child
按钮后,在network面板中才会去加载异步组件
async-child.vue

defineAsyncComponent
除了像上面这样直接接收一个返回Promise的回调函数之外,还可以接收一个对象作为参数。demo代码如下:

const AsyncComp = defineAsyncComponent({
  // 加载函数
  loader: () => import('./async-child.vue'),

  // 加载异步组件时使用的组件
  loadingComponent: LoadingComponent,
  // 展示加载组件前的延迟时间,默认为 200ms
  delay: 200,

  // 加载失败后展示的组件
  errorComponent: ErrorComponent,
  // 如果提供了一个 timeout 时间限制,并超时了
  // 也会显示这里配置的报错组件,默认值是:Infinity
  timeout: 3000
})

其中对象参数有几个字段:

  • loader
    字段其实对应的就是前面那种写法中的回调函数。

  • loadingComponent
    为加载异步组件期间要显示的loading组件。

  • delay
    为显示loading组件的延迟时间,默认200ms。这是因为在网络状况较好时,加载完成得很快,加载组件和最终组件之间的替换太快可能产生闪烁,反而影响用户感受。

  • errorComponent
    为加载失败后显示的组件。

  • timeout
    为超时时间。

在接下来的源码分析中,我们还是以前面那个接收一个返回Promise的回调函数为例子进行debug调试源码。

开始打断点

我们在浏览器中接着来看父组件
index.vue
编译后的代码,很简单,在浏览器中可以像vscode一样使用command(windows中是control)+p就可以唤起一个输入框,然后在输入框中输入
index.vue
点击回车就可以在source面板中打开编译后的
index.vue
文件了。如下图:
command

我们看到编译后的
index.vue
文件代码如下:

import { defineComponent as _defineComponent } from "/node_modules/.vite/deps/vue.js?v=868545d8";
import {
  defineAsyncComponent,
  ref,
} from "/node_modules/.vite/deps/vue.js?v=868545d8";
import LocalChild from "/src/components/defineAsyncComponentDemo/local-child.vue?t=1723193310324";
const _sfc_main = _defineComponent({
  __name: "index",
  setup(__props, { expose: __expose }) {
    __expose();
    const showAsyncChild = ref(false);
    const AsyncChild = defineAsyncComponent(() =>
      import("/src/components/defineAsyncComponentDemo/async-child.vue")
    );
    const __returned__ = { showAsyncChild, AsyncChild, LocalChild };
    return __returned__;
  },
});

function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
  // ...省略
}

export default _export_sfc(_sfc_main, [["render", _sfc_render]]);

从上面的代码可以看到编译后的
index.vue
主要分为两块,第一块为
_sfc_main
对象中的
setup
方法,对应的是我们的
script
模块。第二块为
_sfc_render
,也就是我们常说的render函数,对应的是template中的内容。

我们想要搞清楚
defineAsyncComponent
方法的原理,那么当然是给setup方法中的
defineAsyncComponent
方法打断点。刷新页面,此时代码将会停留在断点
defineAsyncComponent
方法处。

defineAsyncComponent
方法

然后将断点走进
defineAsyncComponent
函数内部,在我们这个场景中简化后的
defineAsyncComponent
函数代码如下:

function defineAsyncComponent(source) {
  if (isFunction(source)) {
    source = { loader: source };
  }
  const { loader, loadingComponent, errorComponent, delay = 200 } = source;
  let resolvedComp;

  const load = () => {
    return loader()
      .catch(() => {
        // ...省略
      })
      .then((comp) => {
        if (
          comp &&
          (comp.__esModule || comp[Symbol.toStringTag] === "Module")
        ) {
          comp = comp.default;
        }
        resolvedComp = comp;
        return comp;
      });
  };

  return defineComponent({
    name: "AsyncComponentWrapper",
    setup() {
      const instance = currentInstance;
      const loaded = ref(false);
      const error = ref();
      const delayed = ref(!!delay);
      if (delay) {
        setTimeout(() => {
          delayed.value = false;
        }, delay);
      }
      load()
        .then(() => {
          loaded.value = true;
        })
        .catch((err) => {
          onError(err);
          error.value = err;
        });
      return () => {
        if (loaded.value && resolvedComp) {
          return createInnerComp(resolvedComp, instance);
        } else if (error.value && errorComponent) {
          return createVNode(errorComponent, {
            error: error.value,
          });
        } else if (loadingComponent && !delayed.value) {
          return createVNode(loadingComponent);
        }
      };
    },
  });
}

从上面的代码可以看到
defineAsyncComponent
分为三部分。

  • 第一部分为:处理传入的参数。

  • 第二部分为:
    load
    函数用于加载异步组件。

  • 第三部分为:返回
    defineComponent
    定义的组件。

第一部分:处理传入的参数

我们看第一部分:处理传入的参数。代码如下:

function defineAsyncComponent(source) {
  if (isFunction(source)) {
    source = { loader: source };
  }
  const { loader, loadingComponent, errorComponent, delay = 200 } = source;
  let resolvedComp;
  // ...省略
}

首先使用
isFunction(source)
判断传入的
source
是不是函数,如果是函数,那么就将
source
重写为包含
loader
字段的对象:
source = { loader: source }
。然后使用
const { loader, loadingComponent, errorComponent, delay = 200 } = source
解构出对应的loading组件、加载失败组件、延时时间。

看到这里我想你应该明白了为什么
defineAsyncComponent
函数接收的参数可以是一个回调函数,也可以是包含
loader

loadingComponent

errorComponent
等字段的对象。因为如果我们传入的是回调函数,在内部会将传入的回调函数赋值给
loader
字段。不过loading组件、加载失败组件等参数不会有值,只有
delay
延时时间默认给了200。

接着就是定义了
load
函数用于加载异步组件,这个函数是在第三部分的
defineComponent
中调用的,所以我们先来讲
defineComponent
函数部分。

第三部分:返回defineComponent定义的组件

我们来看看
defineAsyncComponent
的返回值,是一个
defineComponent
定义的组件,代码如下:

function defineAsyncComponent(source) {
  // ...省略

  return defineComponent({
    name: "AsyncComponentWrapper",
    setup() {
      const instance = currentInstance;
      const loaded = ref(false);
      const error = ref();
      const delayed = ref(!!delay);
      if (delay) {
        setTimeout(() => {
          delayed.value = false;
        }, delay);
      }
      load()
        .then(() => {
          loaded.value = true;
        })
        .catch((err) => {
          onError(err);
          error.value = err;
        });
      return () => {
        if (loaded.value && resolvedComp) {
          return createInnerComp(resolvedComp, instance);
        } else if (error.value && errorComponent) {
          return createVNode(errorComponent, {
            error: error.value,
          });
        } else if (loadingComponent && !delayed.value) {
          return createVNode(loadingComponent);
        }
      };
    },
  });
}

defineComponent
函数的接收的参数是一个vue组件对象,返回值也是一个vue组件对象。他其实没有做什么事情,单纯的只是提供ts的类型推导。

我们接着来看vue组件对象,对象中只有两个字段:
name
属性和
setup
函数。

name
属性大家都很熟悉,表示当前vue组件的名称。

大家平时
<script setup>
语法糖用的比较多,这个语法糖经过编译后就是
setup
函数,当然vue也支持让我们自己手写
setup
函数。

提个问题:
setup
函数对应的是
<script setup>
,我们平时写代码都有template模块对应的是视图部分,也就是熟悉的render函数。为什么这里没有render函数呢?


setup
函数打个断点,当渲染异步组件时会去执行这个
setup
函数。代码将会停留在
setup
函数的断点处。


setup
函数中首先使用
ref
定义了三个响应式变量:
loaded

error

delayed

  • loaded
    是一个布尔值,作用是记录异步组件是否加载完成。

  • error
    记录的是加载失败时记录的错误信息,如果同时传入了
    errorComponent
    组件,在加载异步组件失败时就会显示
    errorComponent
    组件。

  • delayed
    也是一个布尔值,由于loading组件不是立马就显示的,而是延时一段时间后再显示。这个
    delayed
    布尔值记录的是是当前是否还在延时阶段,如果是延时阶段那么就不显示loading组件。

接下来判断传入的参数中设置设置了
delay
延迟,如果是就使用
setTimeout
延时
delay
毫秒才将
delayed
的值设置为false,当
delayed
的值为false后,在loading阶段才会去显示loading组件。代码如下:

if (delay) {
  setTimeout(() => {
    delayed.value = false;
  }, delay);
}

接下来就是执行
load
函数,这个
load
函数就是我们前面说的
defineAsyncComponent
函数中的第二部分代码。代码如下:

load()
  .then(() => {
    loaded.value = true;
  })
  .catch((err) => {
    onError(err);
    error.value = err;
  });

从上面的代码可以看到
load
函数明显返回的是一个Promise,所以才可以在后面使用
.then()

.catch()
。并且这里在
.then()
中将
loaded
的值设置为true,将断点走进
load
函数,代码如下:

const load = () => {
  return loader()
    .catch(() => {
      // ...省略
    })
    .then((comp) => {
      if (
        comp &&
        (comp.__esModule || comp[Symbol.toStringTag] === "Module")
      ) {
        comp = comp.default;
      }
      resolvedComp = comp;
      return comp;
    });
};

这里的
load
函数代码也很简单,在里面直接执行
loader
函数。还记得这个
loader
函数是什么吗?

defineAsyncComponent
函数可以接收一个异步加载函数,这个异步加载函数可以在运行时去import导入组件。这个异步加载函数就是这里的
loader
函数,执行
loader
函数就会去加载异步组件。在我们这里是异步加载
async-child.vue
组件,代码如下:

const AsyncChild = defineAsyncComponent(() => import("./async-child.vue"));

所以这里执行
loader
函数就是在执行
() => import("./async-child.vue")
,执行了
import()
后就可以在
network
面板看到加载
async-child.vue
文件的网络请求。
import()
返回的是一个Promise,等import的文件加载完了后就会触发Promise的
then()
,所以这里的
then()
在此时不会触发。

接着将断点走出
load
函数回到
setup
函数的最后一个return部分,代码如下:

setup() {
  // ...省略
  return () => {
    if (loaded.value && resolvedComp) {
      return createInnerComp(resolvedComp, instance);
    } else if (error.value && errorComponent) {
      return createVNode(errorComponent, {
        error: error.value,
      });
    } else if (loadingComponent && !delayed.value) {
      return createVNode(loadingComponent);
    }
  };
},

注意看,这里的
setup
的返回值是一个函数,不是我们经常看见的对象。由于这里返回的是函数,此时代码将不会走到返回的函数里面去,给return的函数打个断点。我们暂时先不看函数中的内容,让断点走出
setup
函数。发现
setup
函数是由vue中的
setupStatefulComponent
函数调用的,在我们这个场景中简化后的
setupStatefulComponent
函数代码如下:

function setupStatefulComponent(instance) {
  const Component = instance.type;
  const { setup } = Component;
  const setupResult = callWithErrorHandling(setup, instance, 0, [
    instance.props,
    setupContext,
  ]);
  handleSetupResult(instance, setupResult);
}

上面的
callWithErrorHandling
函数从名字你应该就能看出来,调用一个函数并且进行错误处理。在这里就是调用
setup
函数,然后将调用
setup
函数的返回值丢给
handleSetupResult
函数处理。

将断点走进
handleSetupResult
函数,在我们这个场景中
handleSetupResult
函数简化后的代码如下:

function handleSetupResult(instance, setupResult) {
  if (isFunction(setupResult)) {
    instance.render = setupResult;
  }
}

在前面我们讲过了我们这个场景
setup
函数的返回值是一个函数,所以
isFunction(setupResult)
的值为true。代码将会走到
instance.render = setupResult
,这里的
instance
是当前vue组件实例,执行这个后就会将
setupResult
赋值给
render
函数。

我们知道render函数一般是由template模块编译而来的,执行render函数就会生成虚拟DOM,最后由虚拟DOM生成对应的真实DOM。


setup
的返回值是一个函数时,这个函数就会作为组件的render函数。这也就是为什么前面
defineComponent
中只有
name
熟悉和
setup
函数,却没有
render
函数。

在执行render函数生成虚拟DOM时就会去执行
setup
返回的函数,由于我们前面给返回的函数打了一个断点,所以代码将会停留在
setup
返回的函数中。回顾一下
setup
返回的函数代码如下:

setup() {
  // ...省略
  return () => {
    if (loaded.value && resolvedComp) {
      return createInnerComp(resolvedComp, instance);
    } else if (error.value && errorComponent) {
      return createVNode(errorComponent, {
        error: error.value,
      });
    } else if (loadingComponent && !delayed.value) {
      return createVNode(loadingComponent);
    }
  };
},

由于此时还没将异步组件加载完,所以
loaded
的值也是false,此时代码不会走进第一个
if
中。

同样的组件都还没加载完也不会有error,代码也不会走到第一个
else if
中。

如果我们传入了loading组件,此时代码也不会走到第二个
else if
中。因为此时的
delayed
的值还是true,代表还在延时阶段。只有等到前面
setTimeout
的回调执行后才会将
delayed
的值设置为false。

并且由于
delayed
是一个ref响应式变量,所以在
setTimeout
的回调中改变了
delayed
的值就会重新渲染,也就是再次执行render函数。前面讲了这里的render函数就是
setup
中返回的函数,代码就会重新走到第二个
else if
中。

此时
else if (loadingComponent && !delayed.value)
,其中的
loadingComponent
是loading组件,并且
delayed.value
的值也是false了。代码就会走到
createVNode(loadingComponent)
中,执行这个函数就会将loading组件渲染到页面上。

加载异步组件

前面我们讲过了在渲染异步组件时会执行
load
函数,在里面其实就是执行
() => import("./async-child.vue")
加载异步组件
async-child.vue
,我们也可以在network面板中看到多了一个
async-child.vue
文件的请求。

我们知道
import()
的返回值是一个Promise,当文件加载完成后就会触发Promise的
then()
。此时代码将会走到第一个
then()
中,回忆一下代码:

const load = () => {
  return loader()
    .catch(() => {
      // ...省略
    })
    .then((comp) => {
      if (
        comp &&
        (comp.__esModule || comp[Symbol.toStringTag] === "Module")
      ) {
        comp = comp.default;
      }
      resolvedComp = comp;
      return comp;
    });
};


then()
中判断加载进来的文件是不是一个es6的模块,如果是就将模块的
default
导出重写到
comp
组件对象中。并且将加载进来的vue组件对象赋值给
resolvedComp
变量。

执行完第一个
then()
后代码将会走到第二个
then()
中,回忆一下代码:

load()
  .then(() => {
    loaded.value = true;
  })

第二个
then()
代码很简单,将
loaded
变量的值设置为true,也就是标明已经将异步组件加载完啦。由于
loaded
是一个响应式变量,改变他的值就会导致页面重新渲染,将会再次执行render函数。前面我们讲了这里的render函数就是
setup
中返回的函数,代码就会重新走到第二个
else if
中。

再来回顾一下
setup
中返回的函数,代码如下:

setup() {
  // ...省略
  return () => {
    if (loaded.value && resolvedComp) {
      return createInnerComp(resolvedComp, instance);
    } else if (error.value && errorComponent) {
      return createVNode(errorComponent, {
        error: error.value,
      });
    } else if (loadingComponent && !delayed.value) {
      return createVNode(loadingComponent);
    }
  };
},

由于此时
loaded
的值为true,并且
resolvedComp
的值为异步加载vue组件对象,所以这次render函数返回的虚拟DOM将是
createInnerComp(resolvedComp, instance)
的执行结果。

createInnerComp函数

接着将断点走进
createInnerComp
函数,在我们这个场景中简化后的代码如下:

function createInnerComp(comp, parent) {
  const { ref: ref2, props, children } = parent.vnode;
  const vnode = createVNode(comp, props, children);
  vnode.ref = ref2;
  return vnode;
}

createInnerComp
函数接收两个参数,第一个参数为要异步加载的vue组件对象。第二个参数为使用
defineAsyncComponent
创建的vue组件对应的vue实例。

然后就是执行
createVNode
函数,这个函数大家可能有所耳闻,vue提供的
h()
函数其实就是调用的
createVNode
函数。

在我们这里
createVNode
函数接收的第一个参数为子组件对象,第二个参数为要传给子组件的props,第三个参数为要传给子组件的children。
createVNode
函数会根据这三个参数生成对应的异步组件的虚拟DOM,将生成的异步组件的虚拟DOM进行return返回,最后就是根据虚拟DOM生成真实DOM将异步组件渲染到页面上。如下图(图后还有一个总结):
progress

总结

本文讲了
defineAsyncComponent
是如何实现异步组件的:


  • defineAsyncComponent
    函数中会返回一个vue组件对象,对象中只有
    name
    属性和
    setup
    函数。

  • 当渲染异步组件时会执行
    setup
    函数,在
    setup
    函数中会执行内置的一个
    load
    方法。在
    load
    方法中会去执行由
    defineAsyncComponent
    定义的异步组件加载函数,这个加载函数的返回值是一个Promise,异步组件加载完成后就会触发Promise的
    then()


  • setup
    函数中会返回一个函数,这个函数将会是组件的render函数。

  • 当异步组件加载完了后会走到前面说的Promise的
    then()
    方法中,在里面会将
    loaded
    响应式变量的值修改为true。

  • 修改了响应式变量的值导致页面重新渲染,然后执行render函数。前面讲过了此时的render函数是
    setup
    函数中会返回的回调函数。执行这个回调函数会调用
    createInnerComp
    函数生成异步组件的虚拟DOM,最后就是根据虚拟DOM生成真实DOM,从而将异步子组件渲染到页面上。

关注公众号:【前端欧阳】,给自己一个进阶vue的机会

另外欧阳写了一本开源电子书
vue3编译原理揭秘
,这本书初中级前端能看懂。完全免费,只求一个star。

首先看一下效果:

任意控件
可以附加一个文字在控件的右上角,并带有
红色
背景

第一步,新建一个空的wpf项目:

第二步,创建一个类,取名为badge:

第三步,将badge的父类设置成
System.Windows.Documents.Adorner

    public classBadge : Adorner
{
public Badge(UIElement adornedElement) : base(adornedElement)
{

}
}

里面的adornedElement表示badge后面附加的对象

关于Adorner这个类的说明,微软给了相应的教程
https://learn.microsoft.com/zh-cn/dotnet/desktop/wpf/controls/adorners-overview?view=netframeworkdesktop-4.8

也可以F11查看Adorner类的说明.

第4步,给badge添加一个Content的附加属性:

        public static readonlyDependencyProperty ContentProperty;staticBadge()
{
ContentProperty
= DependencyProperty.RegisterAttached("Content", typeof(string), typeof(Badge),new FrameworkPropertyMetadata(string.Empty, newPropertyChangedCallback(ContentChangedCallBack)));
}

public static string GetContent(DependencyObject obj)
{
return (string)obj.GetValue(ContentProperty);
}

public static void SetContent(DependencyObject obj, string value)
{
obj.SetValue(ContentProperty, value);
}

第5步,实现content的回调方法:

private static voidContentChangedCallBack(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var target = d asFrameworkElement;if (target != null)
{
if(target.IsLoaded)
{
var layer =AdornerLayer.GetAdornerLayer(target);if (layer != null)
{
var Adorners =layer.GetAdorners(target);if (Adorners != null)
{
foreach (var adorner inAdorners)
{
if (adorner isBadge)
{
layer.Remove(adorner);
}
}
}
layer.Add(
newBadge(target));
}
}
else{
target.Loaded
+= (sender, ae) =>{var layer =AdornerLayer.GetAdornerLayer(target);if (layer != null)
{
var Adorners =layer.GetAdorners(target);if (Adorners != null)
{
foreach (var adorner inAdorners)
{
if (adorner isBadge)
{
layer.Remove(adorner);
}
}
}
layer.Add(
newBadge(target));
}
};
}
}
}

第6步,重写一下OnRender方法:

        protected override voidOnRender(DrawingContext drawingContext)
{
var element = this.AdornedElement asFrameworkElement;
Rect adornedElementRect
= newRect(element.DesiredSize);var point =adornedElementRect.TopRight;
point.X
= adornedElementRect.Right - element.Margin.Left -element.Margin.Right;

SolidColorBrush renderBrush
= newSolidColorBrush(Colors.Red);
Pen renderPen
= new Pen(new SolidColorBrush(Colors.Red), 0.5);double renderRadius = 5;var content = this.AdornedElement.GetValue(Badge.ContentProperty).ToString();
FormattedText formattedText
= new FormattedText(content, CultureInfo.GetCultureInfo("zh-cn"), FlowDirection.LeftToRight, new Typeface("Verdana"), 10, Brushes.White);var textWidth =formattedText.Width;var textHeight =formattedText.Height;var rectangleSizeWidth = textWidth < 15 ? 15: textWidth;var rectangleSizeHeight = textHeight < 15 ? 15: textHeight;var size = newSize(rectangleSizeWidth, rectangleSizeHeight);
Rect rect
= new Rect(new Point(point.X - rectangleSizeWidth / 2, point.Y - rectangleSizeHeight / 2), size);

drawingContext.DrawRoundedRectangle(renderBrush, renderPen, rect, renderRadius, renderRadius);
drawingContext.DrawText(formattedText,
new Point(point.X - textWidth / 2, point.Y - textHeight / 2));
}

这段代码就是在目标控件的右上角绘制一个带圆角的rectangle,背景色为红色,再绘制一个文本用来显示content.

第7步,运用到项目中:

    <StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
        <cc:CornerButton ButtonType="OutLine" Width="200" Height="30"cc:Badge.Content="{Binding ElementName=textbox1, Path=Text, UpdateSourceTrigger=PropertyChanged}" Margin="10"/>
        <cc:CornerTextBox x:Name="textbox1" Width="200" Height="30" Text="12"VerticalContentAlignment="Center" WaterText="BadgeContent"/>
    </StackPanel>

cc是表示badge所在的命名空间,然后你就会发现,你改变textbox的值的时候,badge会跟着textbox的值发生变化哦.

项目github地址:
bearhanQ/WPFFramework: Share some experience (github.com)

QQ技术交流群:332035933;

前言

接续上一篇的optimizer的学习。

optimizer

代码和上一篇文章的一样,如下:

import torch
import numpy as np
import torch.nn as nn

X = torch.tensor([1, 2, 3, 4], dtype=torch.float32)
Y = torch.tensor([2, 4, 6, 8], dtype=torch.float32)
w2 = torch.tensor(0.0, requires_grad=True)
 
def forward(_x):
    return w2* _x


learning_rate = 0.01
n_iter = 100  # 循环次数
loss =nn.MSELoss()
optimizer =torch.optim.SGD([w2],lr=learning_rate)

 
for epoch in range(n_iter):
    y_pred = forward(X)# 
    l = loss(Y, y_pred) 
    l.backward() #执行完反向传播后,w2里就已经有w2.grad了
    optimizer.step() #optimizer初始化时就接收了w2,现在w2有了grad,就可以执行step进行优化了,优化时会使用w2的梯度grad属性和学习率learning_rate
    optimizer.zero_grad() #梯度清零

   
    if epoch % 1 == 0:
        print(f'epoch {epoch+1}:w2= {w2:.3f} ,loss = {l:.8f}')
print(f'f(5)={forward(5):.3f}')

可以看到,我们这里引用增加了一个import torch.nn as nn。
这里只是简单的使用了nn.MSELoss(),我们就不用手写这个计算平均值的步骤了。
然后我们定义了一个optimizer,接收了两个参数,一个是权重w2,一个是学习率learning_rate。
这里我们的传递的是一个Tensors数组,而不是w2。
上篇文章已经介绍的w是根据梯度x.grad生成的,所以,按理应该是一个跟x同类型的矩阵,这里[w2]跟x不同型,但这里他只是一个数,这是因为计算时,会自动把这种一个数的矩阵变形为跟x同型的矩阵。
正确的写法应该是下面这样。

import torch
import numpy as np
import torch.nn as nn

X = torch.tensor([1, 2, 3, 4], dtype=torch.float32)
Y = torch.tensor([2, 4, 6, 8], dtype=torch.float32)
w2 = torch.tensor([0.0,0.0,0.0,0.0], requires_grad=True)
def forward(_x):
    return w2* _x #如果w2不是1个元素或者4个元素,这里就无法相乘
learning_rate = 0.01
n_iter = 100  # 循环次数
loss =nn.MSELoss()
optimizer =torch.optim.SGD([w2],lr=learning_rate)
for epoch in range(n_iter):
    y_pred = forward(X)# 
    l = loss(Y, y_pred) 
    l.backward()  
    optimizer.step() 
    optimizer.zero_grad() 
    if epoch % 1 == 0:
        print(f'epoch {epoch+1}:w21= {w2[0]:.3f} w22= {w2[1]:.3f}  ,loss = {l:.8f}')

思维逻辑重述

回忆一下前面将的青蛙例子,我们重新叙述一下这个计算逻辑,首先我们有个y,这个是我们的目标,然后有个x,这个是我们的输入数据。然后通过w和b这俩参数,结合公式y=wx+b,一次一次的尝试求出w和b,然后通过求出的w和b修正x,然后我们得到了一个新的矩阵——修正x;我们令y_predict=x修正矩阵,就形成了x通过变化得到了预测y,即y_predict。然后我们就可以比较y和y_predict了。

torch.nn简介

torch.nn 是 PyTorch 的一个核心模块,专门用于构建和训练神经网络。这个模块提供了各种类和函数,使你可以方便地定义神经网络模型、实现前向传播和反向传播、定义损失函数,以及进行参数优化等任务。

Linear

torch.nn.Linear的概念是PyTorch 中用于实现线性变换(全连接层)的模块。这里我们先无视他的定义。
先看几个变量的含义。
X.shape:返回的是一个 torch.Size 对象 形状信息分别是 行数(样本数)和列数(特征数),这里特别注意样本和特征这俩词,这是俩干扰我们学习非常狠的名词,注意这个特征并不是我们线性代数里通过计算得到的特征值或特征向量,他就是矩阵的列数量。
nn.Linear(input_size, output_size):这是实例化Linear,入参是俩数,分别叫做input_size, output_size,这俩参数的含义如下。

反人类的定义

input_size:是输入特征的数量,也就是每个输入样本的维度。
output_size:是输出特征的数量,也就是模型希望输出的特征维度。

正常定义

input_size:表示输入数据 X 的列数。
output_size:表示模型预测输出 y_predict 的列数。
注:这里要把反人类的定义多看几遍,因为,如果你学习人工智能,你会在各种视频和文章中看到别人用反人类定义来描述操作与问题。如果不能很好的转换这个理解,就只能等着被恶心吧。
这里,我们可以稍微思考一下,就可以根据input_size和output_size的分别传入推测出一个结论,那就是我们可以输入3 * 3的矩阵x,然后利用torch.nn这个库,输出成4 * 4的矩阵,然后再和4 * 4矩阵y比较。
不过Linear的话,要求输入和输出矩阵的维度必须匹配,所以,这里我们是不用这么做的,不过稍微联想一下即可得出结论,多层神经网络或其他层(如卷积层)肯定可以做这样复杂的映射。
Linear使用代码如下:

import torch
import numpy as np
import torch.nn as nn

X = torch.tensor([[1], [2], [3], [4]], dtype=torch.float32)
Y = torch.tensor([[2], [4], [6], [8]], dtype=torch.float32)

n_samples, n_features = X.shape  # x是4行1列矩阵,这里返回4和1
print("n_samples", n_samples, "n_features", n_features)
input_size = n_features
output_size = n_features
model = nn.Linear(input_size, output_size)

learning_rate = 0.01
n_iter = 100  # 循环次数
loss = nn.MSELoss()
[w, b]= model.parameters()
optimizer = torch.optim.SGD([w, b], lr=learning_rate)

for epoch in range(n_iter):
    y_pred = model(X)  # 这里的 model(X) 调用的是 model 的 forward 方法
    l = loss(Y, y_pred)
    l.backward()
    optimizer.step()
    optimizer.zero_grad()
    if epoch % 1 == 0:
        [w, b] = model.parameters()
        print(f'epoch {epoch+1}:w2= {w[0][0].item():.3f} ,loss = {l:.8f}')

如上代码,我们使用model = nn.Linear(input_size, output_size)定义了一个线性模型对象。
然后使用 torch.optim.SGD时,传入了model.parameters()的返回值。
model.parameters()的返回值就是w和b。model.parameters()在被调用后,会在内部创建一个w和一个b。
权重矩阵 w:形状为 [output_size, input_size]。
偏置向量 b:形状为 [output_size]。
然后我们使用model(x)调用这个实例,这里Linear类里应该是实现了__call__方法,所以类的实例可以像函数一样被调用。
这里我们传递了x,有了x它就可以前向传播了,即,model(x)里我们传递了x,同时触发了前向传播。
所以,model(x)的返回值是一个预测的y值。
然后我们使用我们通过nn.MSELoss()定义的[标量函数/损失函数]来进行计算标量。
然后这个标量就可以使用反向传播了。
然后,我们就得到了模型参数w和b的值了。

nn.Module简介

nn.Module 是 PyTorch 中所有神经网络模块的基类。所有的神经网络层(如线性层、卷积层、LSTM 等)都继承自这个类。
通过继承 nn.Module,可以定义自己的网络层或模型,并利用 PyTorch 的自动微分功能来进行训练。
nn.Linear 是 nn.Module 的子类,是一个特定的神经网络层类,继承自 nn.Module。它实现了一个最简单的线性变换层,也叫全连接层。
通过继承 nn.Module,nn.Linear 能够利用 nn.Module 提供的所有功能,比如注册参数、前向传播、保存和加载模型等。
结构如下:

# nn.Module
#    |
#    |-- nn.Linear
#    |-- nn.Conv2d
#    |-- nn.RNN
#    |-- (Other Modules)

下面自定义一个类,继承Module实现Linear的代码:

X = torch.tensor([[1], [2], [3], [4]], dtype=torch.float32)  # 4行1列矩阵
Y = torch.tensor([[2], [4], [6], [8]], dtype=torch.float32)

n_samples, n_features = X.shape
print("n_samples", n_samples, "n_features", n_features)
input_size = n_features
output_size = n_features

# model = nn.Linear(input_size, output_size) 
class LinearRegression(nn.Module):
    def __init__(self, input_dim, output_dim):
        super(LinearRegression,self).__init__()
        # define layers
        self.lin = nn.Linear(input_dim, output_dim)
    def forward(self, x):return self.lin(x)
model =LinearRegression(input_size, output_size)

learning_rate = 0.01
n_iter = 100  # 循环次数
loss = nn.MSELoss()
[w, b]= model.parameters()

optimizer = torch.optim.SGD([w, b], lr=learning_rate)

for epoch in range(n_iter):
    y_pred = model(X)
    l = loss(Y, y_pred)
    l.backward()
    optimizer.step()
    optimizer.zero_grad()  # 梯度清零

    if epoch % 1 == 0:
        [w, b] = model.parameters()
        print(f'epoch {epoch+1}:w2= {w[0][0].item():.3f} ,loss = {l:.8f}')

传送门:
零基础学习人工智能—Python—Pytorch学习(一)
零基础学习人工智能—Python—Pytorch学习(二)
零基础学习人工智能—Python—Pytorch学习(三)
零基础学习人工智能—Python—Pytorch学习(四)
学习就先到这。


注:此文章为原创,任何形式的转载都请联系作者获得授权并注明出处!



若您觉得这篇文章还不错,请点击下方的【推荐】,非常感谢!

https://www.cnblogs.com/kiba/p/18354543