学习笔记02-260930-Spring Boot 入门笔记: AOP、SpringBoot入门

发布时间:2026/10/1 8:23:05
学习笔记02-260930-Spring Boot 入门笔记: AOP、SpringBoot入门 该文章为ai润色原文笔记在最后。该文章仅为个人记录所写。1. 一次创建草稿的请求怎么走后端启动后日志里能看到Tomcat started on port 8080 (http) with context path /所以接口地址是http://localhost:8080/api/item。客户端发送 POST 请求请求体里放标题、内容和用户 ID{ title: 第一条练习草稿, content: 练习, ownerId: 1 }按我现在的理解这个过程可以先这样记客户端发出 HTTP 请求 ↓ Tomcat 接收请求经过过滤器链 ↓ Spring MVC 的 DispatcherServlet 找到对应的处理方法 ↓ 已配置的拦截器进行前置处理 ↓ 读取 JSON 请求体转换成 CreateItemRequest ↓ DemoController.createItem() ↓ ItemServiceImpl.createItem() ↓ 检查数据创建 ServiceItem保存到 Map ↓ 返回 ServiceItem转换为 JSON ↓ 客户端收到 HTTP 响应刚开始我写成“JSON 先进入 Controller然后由RequestBody转换”。更准确一点是 Spring MVC 根据RequestBody读取请求体通过消息转换器完成转换再把CreateItemRequest传给 Controller 方法。Controller 接着调用itemService.createItem(request)。这个itemService是 Spring 注入的不需要在 Controller 里自己new ItemServiceImpl()。Service 里的流程比较容易看懂检查标题和内容是否为空创建ServiceItem放入标题、内容、ownerId设置DRAFT状态和创建时间再生成 ID 并保存。我把保存数据的工具类命名为simpleDatabase但它里面其实是一个静态HashMap没有连接数据库。新增和查询分别是items.put(item.getId(), item); items.get(id);ID 也是在 Java 里通过id生成的。以后换成真实数据库主键可能用数据库自增、序列或 UUID不能笼统地说“主键都是数据库自动自增”。查询则从DemoController.selectById()进入ItemServiceImpl.selectById()最后调用simpleDatabase.select(id)从 Map 里取出数据。2. 顺便弄清 Spring Boot 的几个基础概念启动类上的注解启动类上常见的SpringBootApplication是一个组合注解主要包含这三个职责注解用来做什么SpringBootConfiguration表明这是一个 Spring Boot 配置类EnableAutoConfiguration根据依赖、配置和已有 Bean 等条件进行自动配置ComponentScan默认扫描启动类所在包及其子包寻找组件我先记住它们各自负责什么。尤其是“自动配置”有条件并不是加一个注解就把所有功能都开启了。配置文件和默认值application.properties使用keyvalueYAML 使用缩进表达层级。例如下面两段配置表达的是同一件事spring.application.name${APP_NAME:unnamed}spring: application: name: ${APP_NAME:unnamed}${APP_NAME:unnamed}表示读取APP_NAME没有时用unnamed。这里读取的是 Spring 环境中的配置值来源可以包括系统环境变量和命令行参数。读教程时我还遇到了“缺省配置”这个词。其实“缺省”就是默认某个配置项有默认值时省略配置就按默认值来。主动设置后通常会覆盖默认值具体还要看配置优先级。如果必要的配置没有默认值省略后仍然可能报错。Logback 是什么Logback 是日志实现用来记录启动信息、业务过程和异常等内容。我们在代码里通过 SLF4J 的接口写日志Logback 可以负责实际输出。Spring Boot 常用 starter 默认带有 Logback所以没有自己写日志配置文件控制台也能看到日志。如果要输出到文件、修改格式或调整日志级别再进行配置。教程里的defaults.xml则提供了一些预设日志配置。Maven 会不会自动挑一个能用的版本我最初以为额外依赖可以让 Maven 自动选版本如果某个版本不可用它还会换一个可用的。后来发现这个理解不对。我的项目通过pom.xml声明依赖。Boot 父项目或 BOM 已经管理版本的依赖通常可以省略单独的版本号没有被管理的依赖需要确定版本。dependencyManagement是管理规则并不等于已经引入这个库。多个依赖间接引入同一个库的不同版本时Maven 通常先看依赖路径的距离离当前项目更近的优先距离相同才看声明顺序依赖管理也会影响结果。如果选中的版本无法解析构建通常会报错不会自动换成任意其他版本。3. AOP 为什么能把日志单独拿出来假如创建、查询、修改这些业务都要打印日志最直接的写法就是在每个方法里加日志。以后改日志格式就需要挨个改。AOP也就是面向切面可以把这些横跨多个方法的处理集中起来。先用切点选出要处理的方法再用通知决定什么时候做什么。日志、权限检查、事务都是常见的例子。学到这里我又顺着问了“耦合”和“内聚”。耦合可以理解成代码模块之间依赖的程度。依赖本身很正常但如果一个模块知道另一个模块太多内部细节一点变化就影响很多地方修改就会比较困难。内聚则是一个模块内部的内容是否围绕同一个职责。业务方法集中处理业务日志模块集中处理日志都可以有比较好的内聚。所以把合适的日志处理提取成切面可以让业务方法更集中也方便统一修改日志。不过重复的业务代码不一定都适合做成 AOP还是要看它承担的职责。4. 我对动态代理的误解这是今天绕得最久的地方。我原来以为每个类都先生成一个动态代理 ↓ 需要 AOP 时代理再进入 AOP 接受增强 ↓ 使用时先调用 AOP再调用代理对象现在我这样理解 Spring AOPSpring 判断某个 Bean 需要代理 ↓ 为它创建代理组织目标对象和通知 ↓ 调用者拿到代理引用 ↓ 调用方法时执行匹配的通知再调用目标方法这里的增强不是把日志代码复制进原类源码。代理负责把通知与目标调用连接起来通知在方法被调用时执行。代理也是针对对象创建的。一个 Bean 已经有代理调用它另一个不匹配切点的方法仍然可能经过同一个代理只是不执行这组通知。动态代理还可以用于 AOP 以外的场景。JDK 动态代理和 CGLIB方式与目标类的关系容易记错的地方JDK 动态代理生成实现指定接口的代理类不继承目标实现类不会复制目标类的所有方法和字段也不会复制私有方法CGLIB生成目标类的子类通过可重写的方法拦截调用final类不能继承final、private方法不能按这种方式增强在日志里我看到了这两个类名目标对象com.example.demo.service.serviceImpl.ItemServiceImpl 代理对象com.example.demo.service.serviceImpl.ItemServiceImpl$$SpringCGLIB$$0Spring Boot 4.0 的 AOP 自动配置默认使用 CGLIB但不能把它说成所有 Spring 环境都统一默认 CGLIB。Spring AOP 基于代理目标对象内部的this.方法()调用通常会绕过代理这也是以后排查切面为什么没执行时要注意的地方。5. 给创建草稿加一个日志切面我先写了一个LogAspect在创建草稿前打印日志Slf4j Aspect Component public class LogAspect { ​ Before(execution(* com.example.demo.service.serviceImpl.ItemServiceImpl.createItem(..))) public void beforeCreate() { log.info(准备创建草稿); } }Aspect声明切面Component让它在组件扫描时成为 Spring BeanSlf4j提供日志字段。项目还需要相应的 AOP 依赖和支持。发送创建请求后控制台出现了“准备创建草稿”接口也照常返回。切点表达式怎么读第一次看到这一长串我基本看不懂execution(* com.example.demo.service.serviceImpl.ItemServiceImpl.createItem(..))拆开就清楚一些了execution匹配方法执行。最前面的*任意返回类型。com.example.demo.service.serviceImpl包名。ItemServiceImpl类名。createItem方法名。(..)零个或多个任意类型参数。调整匹配范围时可以这样写# 匹配 ItemServiceImpl 的方法 execution(* com.example.demo.service.serviceImpl.ItemServiceImpl.*(..)) ​ # 匹配 service 包及其子包中的方法 execution(* com.example.demo.service..*.*(..))参数位置里()表示无参数(*)表示恰好一个任意类型参数(..)表示零个或多个参数。这些表达式选出的范围还要受 Spring AOP 代理能力的限制。几个术语放回代码里理解术语我的简单理解对应例子Aspect切面集中放一类额外处理LogAspectJoin Point连接点可以被增强的执行位置Spring AOP 中是方法执行一次createItem()执行Pointcut切点筛选这些执行位置的规则execution(...)Advice通知或增强选中之后具体做什么创建前打印日志Target Object目标对象真正处理原业务的对象ItemServiceImplAOP Proxy代理对象接收调用、执行通知并调用目标的对象日志里带SpringCGLIB的对象Weaving织入把切面和原调用联系起来的过程Spring AOP 通过代理组织调用Interceptor拦截器在调用前后加入处理的一种实现代理里的拦截调用链与 MVC 请求拦截器不是同一个概念Introduction引介让代理额外实现某个接口原类源码不用增加该接口6.Around让我踩了一个返回值的坑常见通知有这几种通知执行时机Before方法执行前AfterReturning方法正常返回后AfterThrowing方法抛出异常后After方法结束时正常返回或异常都会触发Around围绕调用可以决定是否继续、最终返回什么我开始用Around时预期很简单开始时打印一条日志执行原来的创建方法结束时再打印一条日志。结果日志从around-Start一直打印到了around-EndPOST 的响应体却是空的。状态码为 200响应头里写着Content-Length: 0当时我还把它描述成“没有返回 HTTP 连接”。仔细看才发现HTTP 响应已经回来了只是没有响应体。GET 也还能查到已有草稿于是我一直在找为什么 POST 没有把对象返回。问题出在环绕通知我调用了proceed()却没有把它的结果返回而且通知方法写成了void。改成下面的处理后响应恢复正常Around(execution(* com.example.demo.service.serviceImpl.ItemServiceImpl.createItem(..))) public Object aroundCreate(ProceedingJoinPoint joinPoint) throws Throwable { log.info(around-Start); Object result joinPoint.proceed(); log.info(around-End); return result; }所以继续调用和返回调用结果是两件事。proceed()让调用继续return result才把结果交回调用者。为什么 Before 和 After 不用这样返回Before、After是框架在调用前后安排的附加动作它们的返回值不负责传递目标方法的结果。Around则包住了这次调用代理调用最终拿到的返回值来自环绕通知。想保留原来的行为就要把继续调用得到的结果返回。常用Object是为了适配不同返回类型原方法为void时得到的结果可以是null。如果不调用proceed()目标方法就不会继续执行但不代表请求一定卡住。环绕通知也可以直接返回一个结果只是它改变了原来的调用行为。另外我原来在After里打印“创建草稿完成”。这个通知异常时也会执行所以这句日志容易误导。要表达成功应该考虑AfterReturningAfter更适合表达“执行结束”。7. JoinPoint 到底是不是原方法我最开始把JoinPoint理解成原方法本身后来又不明白既然是原方法怎么还能拿到代理对象的信息它其实是描述当前方法执行的上下文对象可以从中取得不同的信息调用可以得到什么getSignature()方法签名信息例如名称getArgs()本次调用的参数getTarget()目标对象getThis()Spring AOP 代理对象ProceedingJoinPoint扩展了JoinPoint用于环绕通知额外提供proceed()。它会继续剩余通知链最终调用目标方法。对应到我的例子getTarget().getClass().getName()打印原来的ItemServiceImplgetThis().getClass().getName()打印带SpringCGLIB的代理类名。8. 自定义注解写一个标记再让切面识别它前面是按照包名、类名和方法名匹配。另一种常见方式是在方法上加一个注解再让切面匹配带有这个注解的方法。这让我想到了事务方法上写Transactional框架就会根据它做相应处理。自定义注解也可以提供这种标记方式但注解本身不会自动执行处理逻辑。我用的注解叫MetricTimeRetention(RetentionPolicy.RUNTIME) Target(ElementType.METHOD) public interface MetricTime { String value(); }它可以放在方法上例如MetricTime(select) public ServiceItem selectById(Long id) { return simpleDatabase.select(id); }注解是什么我现在把注解理解成贴在类、方法等位置上的结构化说明也叫元数据。它可以携带一些额外信息但要由编译器、工具或框架读取并处理才会产生相应作用。例如MetricTime(select)表示这个方法带有MetricTime标记其中value是select。切面可以根据这个标记选中方法也可以读取value用它作为日志里的名称。Target可以写在哪里我原来把它理解成“作用域”现在觉得说“允许标记的位置”更直观。值允许的位置TYPE类、接口、枚举、record、注解类型等类型声明METHOD方法声明FIELD字段包括枚举常量PARAMETER方法或构造器的参数CONSTRUCTOR构造器声明LOCAL_VARIABLE局部变量声明ANNOTATION_TYPE注解类型声明PACKAGE包声明通常写在package-info.javaTYPE_PARAMETER泛型类型参数声明TYPE_USE使用类型的位置例如泛型实参MODULE模块声明RECORD_COMPONENTrecord 的组件允许多个位置时可以写Target({ElementType.METHOD, ElementType.TYPE})。Retention保留到什么时候它不负责决定“什么时候执行”而是决定注解信息保留到哪个阶段。值含义SOURCE只保留在源码编译时丢弃例如OverrideCLASS写入 class 文件运行时不能通过普通反射读取RUNTIME写入 class 文件运行时仍能通过反射读取没有声明Retention时默认是CLASS。如果希望切面在运行时读取注解信息这里就需要保留到RUNTIME。value()是不是可选参数不是。是否可选要看有没有defaultpublic interface MetricTime { String value(); String unit() default ms; }这个示例里的value没有默认值使用时必填unit有默认值可以省略。MetricTime(select)是MetricTime(value select)的简写。名字叫value只是让写法可以更简短不会让它自动变成可选属性。9.annotation的两种写法这里又卡了一下因为表达式里看起来都是注解却有两种写法。按注解类型匹配Around(annotation(com.example.demo.aspect.MetricTime))它匹配带有MetricTime的方法通知方法不必额外声明MetricTime参数。如果想读取注解属性可以从方法信息中获取注解MethodSignature signature (MethodSignature) joinPoint.getSignature(); Method method signature.getMethod(); MetricTime metricTime method.getAnnotation(MetricTime.class); String name metricTime.value();我之前只写了MetricTime metricTime;接着就调用metricTime.value()IDE 提示变量可能尚未初始化。现在看原因很直接声明变量不会让它自动变成方法上的注解实例需要先取得这个实例。这里的MethodSignature来自org.aspectj.lang.reflectMethod来自java.lang.reflect。还要注意使用接口代理时得到的Method可能来自接口注解写在实现方法上时需要定位到目标方法不能直接假设getAnnotation()一定能读到。匹配并绑定注解Around(annotation(metricTime)) public Object time(ProceedingJoinPoint joinPoint, MetricTime metricTime) throws Throwable { String name metricTime.value(); return joinPoint.proceed(); }这个片段展示的是参数绑定表达式中的metricTime对应通知方法的同名参数Spring 会把匹配到的注解传进来方法里就能直接读取它的属性。两种写法都使用切点表达式。区别是一种按类型匹配另一种同时把注解绑定到参数。Spring 通知参数绑定如果要做耗时统计思路是在proceed()前记录开始时间在finally中计算耗时这样正常返回和异常结束都可以记录。计时之外也要注意保留返回值和异常的行为。原文内容如下原文内容可能有错误仅供参考AOP面向切面可以将在多个不同业务方法中但是又相同的部分与业务逻辑本身关联不大但是又不可或缺的部分单独提取出来制作成切面通过切点选出要处理的方法再通过通知决定在方法执行前、执行后或执行前后做什么。这样业务方法可以更集中地表达自己的业务逻辑共用处理也比较方便统一修改。例如安全检查、日志、事务等代码。如图假如一个业务逻辑是从上到下的但是一些与业务逻辑不直接相关的代码降低了内聚但是有了切面被切面覆盖的代码可以从业务逻辑中切出去业务逻辑只需要关心具体业务提高内聚对于业务之外但是又不得不用的代码可以通过切面来配装也可以通过修改切面代码来一键修改所有日志等实现降低直接耦合AOP原理Spring AOP 可以通过代理对象在符合切入点规则的方法调用前后加入额外处理。切入点负责选方法代理负责在调用时把通知和原方法联系起来。Spring AOP默认使用CGLIB动态代理也可以手动切换。JDK动态代理生成的是实现指定接口的代理类不继承原来的实现类CGLIB动态代理生成的是原类的子类通过重写可重写的方法来进行代理因此final、private等不能被重写的方法不能用这种方式增强我的误解和改正我之前一直以为每个类都会先默认创建一个动态代理。某个方法需要 AOP 时代理对象再“进入 AOP”接受增强使用时先调用 AOP再由 AOP 调用代理对象。但是实际上应该是Spring AOP 不是一个存放代理对象的地方。Spring 判断某个 Bean 需要 AOP 处理后才会为它准备代理对象调用者实际调用的是代理对象。代理会在符合规则的方法被调用时执行相应的通知再调用原对象的方法。这里的“增强”不是把通知代码复制进原类也不是修改原类源码。代理对象承载了这套调用流程通知是在方法被调用时才真正执行的。不是每个类都会因为 Spring AOP 而自动生成代理动态代理也可以用于 AOP 以外的场景装配AOPAspect切面即一个横跨多个核心逻辑的功能或者称之为系统关注点Joinpoint连接点即定义在应用程序流程的何处插入切面的执行Pointcut切入点即一组连接点的集合Advice增强指特定连接点上执行的动作Introduction引介指为一个已有的Java对象动态地增加新的接口Weaving织入指将切面整合到程序的执行流程中Interceptor拦截器是一种实现增强的方式Target Object目标对象即真正执行业务的核心逻辑对象AOP ProxyAOP代理是客户端持有的增强后的对象引用。先在SpringBoot中引入AOP依赖然后定义一个切面类LogAspectSlf4j Aspect Component public class LogAspect { // 方法执行前通知 Before((execution(* com.example.demo.service.serviceImpl.ItemServiceImpl.createItem(..)))) public void beforeCreate(){ log.info(准备创建草稿); } ​ // 方法执行后通知 After((execution(* com.example.demo.service.serviceImpl.ItemServiceImpl.createItem(..)))) public void afterCreate(){ log.info(创建草稿完成); } ​ }其中先通过Aspect注解声明这是一个AOP切面然后通过Component将其注册为Bean在AOP中有这几个通知注解通知何时执行Before方法执行前AfterReturning方法正常返回后AfterThrowing方法抛出异常后After方法结束时无论正常还是异常Around围绕整个调用切点表达式而注解中需要添加切点表达式来规定AOP的范围最常用的切点表达式如下execution(返回类型 包名.类名.方法名(参数))execution(* com.example.demo.service.serviceImpl.ItemServiceImpl.createItem(..))从右往左读依次为creatItem方法(..)允许任意个、任意类型的参数ItemServiceImpl类com.example.demo.service.serviceImpl包最前面的*允许返回任意类型范围的变化:表达式匹配范围execution(* com.example.demo.service.serviceImpl.ItemServiceImpl.createItem(..))这个类的createItem方法execution(* com.example.demo.service.serviceImpl.ItemServiceImpl.*(..))这个类的所有方法execution(* com.example.demo.service..*.*(..))service包及其子包中各类的所有方法()表示无参数(*)表示恰好一个任意类型的参数(..)表示零个或多个参数AOP术语与我的代码例子结合的解释术语简单理解在你的例子里Aspect切面把一类额外处理集中放在一起LogAspect集中放日志规则Join Point连接点程序运行中一个可以被处理的位置Spring AOP 中主要是方法执行一次createItem()的执行Pointcut切入点筛选连接点的规则选哪些方法execution(...ItemServiceImpl.createItem(..))Advice通知增强选中方法后具体做什么执行前打印“准备创建草稿”Target Object目标对象真正完成原业务的对象ItemServiceImpl对象AOP Proxy代理对象调用者接触的“中间人”负责执行通知并继续调用目标对象你看到的ItemServiceImpl$$SpringCGLIB$$0Weaving织入把切面与目标调用联系起来的过程Spring AOP 在运行时准备代理使日志能参与方法调用Interceptor拦截器实现“调用前后做额外处理”的一种方式可以把代理里的通知调用链理解为一层层拦截Introduction引介让代理对象额外表现为“实现了某个新接口”它不修改原类的源码Joinpoint和ProceedingJoinPoint名称是什么常见用途JoinPoint描述当前被通知处理的方法调用查看方法名getSignature()、参数getArgs()、目标对象getTarget()、代理对象getThis()ProceedingJoinPointJoinPoint的扩展供Around使用除了查看调用信息还能用proceed()继续调用JoinPoint提供“这次调用的信息”ProceedingJoinPoint允许你决定“是否继续这次调用”。在 Spring AOP 中这里的调用点是方法执行注解装配AOP除了通过添加切点表达式来定义装配AOP还可以通过使用注解的方式快速、灵活地定义装配AOP。首先先定义一个注解Retention(RetentionPolicy.RUNTIME) Target(ElementType.METHOD) public interface MetricTime { String value(); }Target()规定这个注解可以写在哪里值允许标记的位置TYPE类、接口、枚举、record 的声明包括注解类型METHOD方法声明FIELD成员字段包括枚举常量PARAMETER方法或构造器的参数CONSTRUCTOR构造器声明LOCAL_VARIABLE方法中的局部变量ANNOTATION_TYPE注解类型的声明例如你定义的MetricTimePACKAGE包声明通常写在package-info.javaTYPE_PARAMETER泛型参数声明例如class BoxT中的TTYPE_USE使用某个类型的位置例如List某注解 StringMODULE模块声明RECORD_COMPONENTrecord 的组件例如record User(String name)中的name可以允许多个位置Target({ElementType.METHOD, ElementType.TYPE})Retention()规定注解信息保留到哪个阶段值注解信息保留到哪里SOURCE只留在源码中编译时丢弃例如OverrideCLASS写入.class文件运行时不能通过普通反射读取RUNTIME写入.class文件运行时仍能通过反射读取注解中定义的内容叫属性是否必填只看它有没有defaultpublic interface MetricTime { String value(); // 没有默认值必填 String unit() default ms; // 有默认值可选 }在切面中通过将注解信息传给通知来使用注解实现AOP切面写法作用通知方法需要什么参数annotation(com.example.demo.aspect.MetricTime)选中带有MetricTime的方法不需要额外声明MetricTime参数annotation(metricTime)选中方法同时把它的注解传给通知需要声明MetricTime metricTime名字必须对应如果使用第一种方法需要通过反射使用Joinpoint来获取原注解中的元数据而使用第二种则直接可以获取到元数据SpringBoot一个请求到后端的简单流程浏览器客户端发起HTTP请求-请求经过过滤器-请求进入SpringMVC找到对应方法-请求经过拦截器-请求进入Controller自动格式转化为相应类型-请求进入Service处理相应的业务需要调用数据库时一般由Service调用Mapper调用数据库-业务处理完毕后返回Controller返回结果并生成HTTP响应-处理好的数据返回客户端配置文件application.properties采用keyvalue的格式因为必须写出完整的key而配置中key又有很大一部分前缀是相同的导致可读性困难# application.properties ​ spring.application.name${APP_NAME:unnamed} ​ spring.datasource.urljdbc:hsqldb:file:testdb spring.datasource.usernamesa spring.datasource.password spring.datasource.driver-class-nameorg.hsqldb.jdbc.JDBCDriver ​ spring.datasource.hikari.auto-commitfalse spring.datasource.hikari.connection-timeout3000 spring.datasource.hikari.validation-timeout3000 spring.datasource.hikari.max-lifetime60000 spring.datasource.hikari.maximum-pool-size20 spring.datasource.hikari.minimum-idle1 ​application.yml采用层级结构key的重复前缀可省略可读性好# application.yml ​ spring: application: name: ${APP_NAME:unnamed} datasource: url: jdbc:hsqldb:file:testdb username: sa password: driver-class-name: org.hsqldb.jdbc.JDBCDriver hikari: auto-commit: false connection-timeout: 3000 validation-timeout: 3000 max-lifetime: 60000 maximum-pool-size: 20 minimum-idle: 1 ​环境变量可以使用${}来使用环境变量环境变量可以是系统变量也可以是在启动时传入命令配置启动时的必要注解SpringBootApplication组合注解里面包含了三个必要的注解1、SpringBootConfiguration表明这是一个Spring配置类2、EnableAutoConfiguration:根据已有依赖自动配置需要的功能3、ComponentScan:扫描启动包以及其子包寻找要交给Spring管理的类依赖管理pom.xmlSpringBoot项目需要从pom.xml中配置依赖并交由Maven管理。项目指定了 Spring Boot 父项目的版本对于它已经管理版本的依赖添加依赖时通常可以省略单独的版本号。如果要加入未被管理的依赖就需要确认并指定版本。当不同依赖间接引入同一个库的不同版本时Maven 会按依赖管理和依赖树规则确定最终版本一般优先采用离当前项目更近的版本距离相同才考虑声明顺序。如果选定版本无法解析构建通常会报错不会自动换成任意其他版本。