回顾 Spring

什么是Spring?

spring是一个为了简化企业级开发,它是轻量级的、使用IoC、AOP等进行开发的一站式框架。比如, 控制反转&依赖注入面向切面编程spring事务管理、通过spring继承其他框架(Spring继承jdbc、mybatis等)。

什么是控制反转(IoC)和依赖注入(DI)?

依赖关系:当一个对象a的某些操作需要通过调用另一个对象b中的方法来实现时,说明a依赖于对象b,a与b是依赖关系。

IoC:控制反转

使用者之前使用对象的时候都需要自己手动的创建和组装,而现在这些创建和组装都交给了spring容器来进行管理,需要使用这个对象的时候,我们可以直接去spring容器中查找使用这个对象。之前创建和组装对象都是我们自己手动的进行创建,现在交由spring来进行创建和组装了,对象的构建被反转了,就叫做控制反转。IOC是面向对象编程的一种设计原则,它可以降低系统代码的耦合度,使系统更加有利于维护和扩展。

DI:依赖注入

依赖注入是Spring容器中创建对象时给其设置依赖对象的方式,比如给Spring一个清单,清单中列出了需要创建B对象以及其他的一些对象(可能包含B类型中需要依赖的对象),此时spring 在创建B的时候,会看B对象需要依赖哪些对象,然后去spring容器中查找看没有这些对象,如果有就去将其创建好,然后将其传递给B 对象;可能B 需要依赖于很多对象,B 创建之前完全不需要知道其他对象是否存在或者其他对象在哪里以及被他们是如何创建,而spring 容器会将B 依赖对象主动创建好并将 其注入到B 中去,比如spring 容器创建B 的时候,发现B 需要依赖于A ,那么spring 容器在清单中找到A 的定义并将其创建好之后,注入到B 对象中。

IOC容器

IOC容器是具有依赖注入功能的容器,负责对象的实例化、对象的初始化,对象和对象之间依赖关系配置,对象的销毁、对外提供对象的查找等操作,对象的整个生命周期都是由容器来控制。我们不需要我们手动的创建对象,需要使用的对象都保存在IOC容器进行管理,当我们需要使用的时候直接从IOC容器中直接获取即可。

总结

  1. IoC将对象创建和组装的主动控制权交给了spring容器去做,避免我们自己手动的创建对象,创建对象的过程被反转了,降低了系统的耦合度,利于系统的维护和扩展。
  2. DI依赖注入,表示spring容器中创建对象时给其设置依赖对象的方式,通过某些注入方式可以让系统更加灵活,比如自动注入等可以让系统变的很灵活,比如自动注入等可以让系统变的更加灵活。
  3. Spring容器(IOC容器):主要负责容器中对象的创建、组装、对象查找、对象生命周期的管理等操作。(IOC容器:主要对对象的整个生命周期进行管理,需要的时候直接从IOC容器中获取即可。)的IOC 容器也叫spring 容器。

Bean

Bean概念

由Spring容器管理的对象称为Bean对象。

创建Bean对象

  1. 通过反射机制创建bean对象
  2. 通过静态工厂创建bean对象(可以创建静态工厂,内部提供一些静态方法来生成所需要的对象,将这些静态方法创建的对象交给spring 以供使用。)
  3. 通过实例工厂创建bean对象(让spring 容器去调用某些对象的某些实例方法来生成bean 对象放在容器中以供使用。)
  4. 通过FactoryBean创建bean对象(FactoryBean 可以让spring 容器通过这个接口的实现来创建我们需要的bean对象。)

单例bean(scope = singleton

当scope 的值设置为singleton 的时候,整个spring 容器中只会存在一个bean 实例,通过容器多次查找 bean的时候(调用BeanFactory 的getBean 方法或者bean 之间注入依赖的bean 对象的时候),返回的 都是同一个bean 对象,singleton 是scope 的默认值,所以spring 容器中默认创建的bean 对象是单例的, 通常spring 容器在启动的时候,会将scope 为singleton 的bean 创建好放在容器中(有个特殊的情况,当 bean的lazy 被设置为true 的时候,表示懒加载,那么使用的时候才会创建),用的时候直接返回。

使用注意:

单例bean是整个应用共享的,所以需要考虑到线程安全问题,之前在Spring MVC的时候,spring MVC中controller默认是单例的,有些开发者在controller中创建了一些变量,那么 这些变量实际上是共享的,controllet可能被多个线程同时访问,这些线程并发去修改controller中的共享变量,可能会出现数据错乱的问题,所以使用的时候需要特别注意。

scope =prototype

如果 scope 被设置为 prototype 类型的了,表示这个 bean 是多例的,通过容器每次获取的 bean

都是不同 的实例,每次获取都会重新创建一个bean 实例对象。

使用注意:

多例 bean 每次获取的时候都会重新创建,如果这个 bean 比较复杂,创建时间比较长,会影响 系统的性 能,这个地方需要注意。

总结:

  1. spring容器自带的2种作用域 ,分别是singleton 和 prototype ;还有3种分别是spring web容器环境中才支持的request 、 session 、 application
  2. singleton是spring容器默认的作用域,一个Spring容器中同名的bean实例只有一个,多次获得到的是同一个bean,单例的bean需要考虑线程安全问题。
  3. prototype是多例的,每次从容器中获取同名的bean,都会重新创建一个;多例bean使用的时候需要考虑创建bean 对性能的影响
  4. 一个应用中有多个spring容器
  5. 自定义scope3个步骤,实现Scope接口,将实现类注册到spring容器中,使用自定义的scope

CGLIB和Java动态代理的区别

  1. Java动态代理只能够对接口进行代理,不能对普通的类进行代理(因为所有生成的代理类都有共同的父类Proxy,Java类继承机制中不允许多继承);CGLIB能够代理普通类
  2. Java动态代理使用Java原生的反射机制进行处理,在生成类上比较高效; CGLIB 使用是 直接 对字节码进行操作,在类的执行过程中比较高效

注解

什么是注解?

注解: 可以标记包,类,方法,属性,参数等。和 Javadoc 不同, Java 标注可以通过反射获取标注内容。 注解是对代码的一种增强,可以在代码编译或者程序运行期间获取注解的信息,然后根据这 些信息做各种牛逼的事情。

@Configration和 @Bean

@Configuration这个注解可以加在类上,让这个类的功能等同于一个bean.xml配置文件。

@Configuration使用步骤:

  1. 在类上使用@Configuration注解
  2. 通过 AnnotationConfigApplicationContext容器来加 @Configuration注解修饰的类

@Bean:类似于bean.xml配置文件中的bean元素,用来在spring容器中注册一个bean。 @Bean 注解用在方法上,表示通过方法来定义一个 bean ,默认将方法名称作为 bean 名称,将方法返回 值作为 bean 对象,注册到 spring 容器中。

@ComponentScan

用于去扫描某些包及其子包中所有的类,然后将满足一定条件的类作为bean 注册到 spring容器容器中。

@ComponentScan 工作的过程:

  1. Spring会扫描指定的包,且会递归下面子包,得到一批类的数组
  2. 然后这些类会经过各种过滤器,最后剩下的类会被注册到容器中

第一个:需要扫描哪些包?

通过 value、backPackages、basePackageClasses 3 个参数来控制

第二个:过滤器有哪些?

通过 useDefaultFilters、includeFilters、excludeFilters 3 个参数来 控制过滤器

默认情况下,任何参数都不设置的情况下,此时,会将 @ComponentScan 修饰的类所在的包作为扫描包;默认情况下 useDefaultFilters 为 true ,这个为 true 的时候, spring 容器内部会使用默认过滤器, 规则是:凡是类上有 @Repository、@Service、@Controller、@Component 这几个注解中的任何一 个的,那么这个类就会被作为 bean 注册到 spring 容器中,所以默认情况下,只需在类上加上这几个注解 中的任何一个,这些类就会自动交给 spring 容器来管理了。

@Component、@Repository、@Service、 @Controller

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Indexed
public @interface Component {
    String value() default "";
}

从定义中可以看出,这个注解可以用在任何类型上面。

通常情况下将这个注解用在类上面,标注这个类为一个组件,默认情况下,被扫描的时候会被作为bean注册到容器中。value参数,用来指定被注册为bean时的,用来指定bean的名称。(不指定,默认为类名首字母小写)

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Repository {
    @AliasFor(annotation = Component.class)
    String value() default "";
}
/*
Repository上面有@Component注解。
value参数上面有 @AliasFor(annotation = Component.class) ,设置value参数的时候,也相
当于给 @Component 注解中的value设置值。
*/

其他两个注解@Service 、@Controller 源码和@Repository 源码类似。

这4 个注解本质上是没有任何差别,都可以用在类上面,表示这个类被spring 容器扫描的时候,可以作为 一个bean组件注册到spring容器中。@controller通常用来标注controller 层组件,@service 注解标注service 层的组件,@Repository 标注 dao层的组件,这样可以让整个系统的结构更清晰,当看到这些注解的时候,会和清晰的知道属于哪个层,对于spring 来说,将这3 个注解替换成@Component 注解,对系统没有任何影响,产生的效果是一样的。

@Import

按模块的方式进行导入,需要哪个导入哪个,不需要的时候,直接修改一下总的配置类,调整一下@Import就可以了,非常方便。 @Import 可以用来批量导入任何普通的组件、配置类,将这些类中定义的所有bean 注册到容器。

@Conditional:注解可以标注在spring 需要处理的对象上(配置类、@Bean 方法),相当于加了个条件判断,通过判断的结果,让spring 觉得是否要继续处理被这个注解标注的对象。

@Autowired、@Resource、 @Primary、@Qulifier

@Autowired:注入依赖对象

作用:实现依赖注入,spring容器会对bean中所有字段、方法进行遍历,标注有@Autowied注解的,都会进行注入。

@Target({ElementType.CONSTRUCTOR, ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Autowired {
    boolean required() default true;
}

@Resource:注入依赖对象

作用:实现依赖注入,spring容器会对bean中所有字段、方法进行遍历,标注有@Resourse注解的,都会进行注入。

@Documented
@Retention(RUNTIME)
@Target(TYPE)
public @interface Resources {
   /**
    * Array used for multiple resource declarations.

    */
   Resource[] value();
}

@Qualifier:限定符

作用:可以在依赖注入查找候选者的过程中对后选择进行过滤;可以用在类上;也可以用在依赖注入的地方,可以对候选者的查找进行过滤

@Primary :设置为主要候选者

作用: 注入依赖的过程中,当有多个候选者的时候,可以指定哪个候选者为主要的候选者。

@Autowired和@Resource的区别?

  • @Autowired注解是spring提供的,而@Resource是由J2EE提供的
  • @Autowired注解默认是通过byType类型注入,而@Resourse默认是通过byName类型注入
  • @Autowired注解注入的对象需要在IOC容器中存在,否则需要加上属性required = false,表示忽略当前到注入的bean,如果有直接注入,没有跳过,不会报错。
  • *@Autowired:先byType再byName;@Resource:先byName再byType(当指定@Resource name属性时,只会byName)

@Scope、@DependsOn、@ImportResource、@Lazy

  • @Scope:用来定义bean 的作用域;2 种用法:第1 种:标注在类上;第2 种:和@Bean 一起标注在方法上
  • @DependsOn:用来指定当前bean 依赖的bean ,可以确保在创建当前bean 之前,先将依赖的 bean创建好;2 种用法:第1 种:标注在类上;第2 种:和@Bean 一起标注在方法上
  • @ImportResource:标注在配置类上,用来引入bean 定义的配置文件
  • @Lazy:让bean 延迟初始化;常见3 种用法:第1 种:标注在类上;第2 种:标注在配置类上,会对配置类中所有的@Bean 标注的方法有效;第3 种:和@Bean 一起标注在方法上

父子容器

假设系统中有两个模块:module1和module2,两个模块是独立开发的,module2会用到module1中的一些类,module1会将自己打包为jar,提供给module2使用。

在mondule1中有三个类:Service1、Service2(Service2中需要用到Service1)、 Module1Confi(spring配置类)

在mondule2中有三个类:Service1、Service2(使用module2 中的Service1,使用module1 中的Service2)、 Module1Config(spring配置类)

测试运行报错,原因在于因为两个模块中都有Service1 ,被注册到 spring容器的时候,bean 名称会冲突,导致注册失败。

如何解决?

对module1 中的Service1 进行修改?

这个估计是行不通的,module1是别人以jar 的方式提供给我们的, 源码我们是无法修改的。 而module2 是我们自己的开发的,里面的东西我们可以随意调整,那么我们可以去修改一下module2 中 的Service1 ,可以修改一下类名,或者修改一下这个bean 的名称,此时是可以解决问题的。 不过大家有没有想过一个问题:如果我们的模块中有很多类都出现了这种问题,此时我们一个个去重构,还是比较痛苦的,并且代码重构之后,还涉及到重新测试的问题,工作量也是蛮大的,这些都是风险。 而spring 中的父子容器就可以很好的解决上面这种问题。

什么是父子容器?

创建spring 容器的时候,可以给当前容器指定一个父容器。

BeanFactory 的方式

//创建父容器
parentFactory DefaultListableBeanFactory parentFactory = new DefaultListableBeanFactory(); //创建一个子容器
childFactory DefaultListableBeanFactory childFactory = new DefaultListableBeanFactory();
//调用setParentBeanFactory指定父容器
childFactory.setParentBeanFactory(parentFactory);

ApplicationContext 的方式

//创建父容器
AnnotationConfigApplicationContext parentContext = new AnnotationConfigApplicationContext();
//启动父容器
parentContext.refresh();
//创建子容器
AnnotationConfigApplicationContext childContext = new AnnotationConfigApplicationContext(); //给子容器设置父容器
childContext.setParent(parentContext);
//启动子容器
childContext.refresh();

特点:

  1. 父容器和子容器是相互隔离的,他们内部可以存在名称相同的bean
  2. 子容器可以访问父容器中的bean,而父容器不能访问子容器中的bean
  3. 调用子容器的getBean方法获取Bean的时候,会沿着当前容器开始 向上面的容器进行查找,直到 找到对应的bean 为止。
  4. 子容器中可以通过任何注入方式注入父容器中的bean, 而父容器中是无法注入子容器中的 bean

Springmvc中只使用一个容器是否可以?

只使用一个容器是可以正常运行的

那么springmvc中为什么需要用到父子容器?

我们通常使用Spring mvc 的时候,采用3层结构,controller、service、dao;父容器中会包含dao层和service层,而子容器中包含的只有Controller,这两个容器组成了父子容器的结构,controller层通常注册service层的bean。

采用父子容器可以避免有些人在service1层去注入controller层的bean,导致整个依赖层次是比较混乱的。

父容器和子容器的需求也是不一样的,比如父容器中需要有事务的支持,会注入一些支持事务的组件,而子容器中controller完全用不到这些,对这些并不关心,子容器中需要注入一下springmvc相关的bean,而这些bean 父容器中同样是不会用到的,也是不关心一些东西,将这些相互不关心的东西隔开,可以有效的避免一些不必要的错误,而父子容器加载的速度也会快一些。

@Value注解及动态刷新实现

用法:

@Value 可以标注在字段上面,可以将外部配置文件中的数据,比如可以将数据库的一些配置信息放在配置文件中,然后通过@Value 的方式将其注入到 bean 的一些字段中

数据来源: 通常情况下我们 @Value 的数据来源于配置文件,不过,还可以用其他方式,比如我们可以将配置文件的内容放在数据库,这样修改起来更容易一些。 容器启动的时候,可以将这些信息加载到 Environment 中, @Value 中应用的值最终是通过 Environment 来解析的,所以只需要扩展一下 Environment 就可以实现了。

动态刷新: @Scope 中 proxyMode 参数,值为 ScopedProxyMode.DEFAULT ,会生成一 个代理,通

过这个代理来实现@Value 动态刷新的效果

Spring国际化

spring 中对国际化支持挺好的,比较简单,只需要按照语言配置几个 properties 文件,然后主要注册一个国际化的相关的bean ,同时需指定一下配置文件的位置,基本上就可以了

Spring 中国际化怎么用?

spring 中国际化是通过MessageSource 这个接口来支持的 ,spring国际化这块有个实现类,可以检测到配置文件的变化,就可以解决你这个问题。

循环依赖

什么是循环依赖?

多个bean之前相互依赖,形成了一个闭环

A依赖B、B依赖C、C依赖于A

public class A{
    B b;
}
public class B{
    C c;
}
public class C{
    A a;
}

如何检测是否存在循环依赖?

使用一个列表来记录 正在创建中的bean,bean创建之前,先去记录中看一下自己是否存在列表中,存在,说明存在循环依赖,如否,就加入到这个列表中,bean创建完成后,将其从列表中移出。

Spring如何解决循环依赖的问题?

使用spring内部的三级缓存来解决循环依赖问题。 它只针对属性注入,而构造方法注入,无法解决循环依赖。

三级缓存,也可以这样理解:把每一级缓存当做3个存储不同对象的Map。

  • 一级缓存(SingleTonObjects):用来存储实例化,属性赋值,初始化完成的bean。
  • 二级缓存(earlySingleTonObjects):用来存储实例化完成的bean。(半成品的bean,)
  • *三级缓存(SingleTonFactories):保存bean工厂,创建bean的工厂对象,以便后期需要进行添加其他功能。例如:事务管理中的代理对象的生成。

A,B 循环依赖,先初始化 A,先暴露一个半成品 A,再去初始化依赖的 B,初始化 B 时如果发现 B 依赖 A,也就是循环依赖,就注入半成品 A,之后初始化完毕 B,再回到 A 的初始化过程时就解决了循环依赖,在这里只需要一个 Map 能缓存半成品 A 就行了,也就是二级缓存就够了,但是这个二级缓存存的是 Bean 对象,如果这个对象存在代理,那应该注入的是代理,而不是 Bean,此时二级缓存无法及缓存 Bean,又缓存代理,因此三级缓存做到了缓存工厂 ,也就是生成代理,这总结起来: 二级缓存就能解决缓存依赖,三级缓存解决的是代理。

回顾 Spring

单例bean解决了循环依赖,还存在什么问题?

循环依赖的情况下,由于注入的是早期的bean,此时早期的bean中还未被填充属性,初始化等各种操作,也就是说,此时bean并没有被完全初始化完毕,此时若直接拿去,可能存在有问题的风险。

如果只使用2级缓存,直接将刚实例化好的bean暴露给二级缓存出是否可以否?

先下个结论吧:不行。 原因 这样做是可以解决:早期暴露给其他依赖者的 bean 和最终暴露的 bean

不一致的问题。

若将刚刚实例化好的bean直接丢到二级缓存中暴露出去,如果后期这个bean对象被更改了,比如可能在上面加了一些拦截器,将其包装为一个代理了,那么暴露出去的bean和最终的这个bean就不一样的,将自己暴露出去的时候是一个原始对象,而自己最终却是一个代理对象,最终会导致被暴露出去的 和最终的bean 不是同一个 bean 的,将产生意向不到的效果,而三级缓存就可以发现这个问题,会报错。

循环依赖无法解决的情况

只有单例的bean会通过三级缓存提前暴露来解决循环依赖的问题,而非单例的bean ,每次从容器中获 取都是一个新的对象,都会重新创建,所以非单例的 bean 是没有缓存的,不会将其放到三级缓存中。

什么是AOP?

将业务之外的代码,例如事务提交,获得sqlsession,获得接口代理,事务提交,关闭sqlsession等管理起来。使用预编译的方式和动态代理的方式,使用程序可以统一维护的一种技术。

原理:使用jdk代理和动态代理来创建代理对象,通过代理对象来访问目标对象,而代理对象融入增强的代码,最终起到对目标对象增强的效果。

作用:可以将业务逻辑之间进行隔离,降低耦合度,使得开发维护方便。

AOP到底为程序带来了哪些好处?

可以将一些公共的,重复出现的非业务代码进行提取,然后使用动态代理的方式,为我们的业务代码横切添加额外提取的功能,而且不需要显示的在业务代码中调用,可以通过一个代理对象,来帮助我们调用这些抽取出来的方法。

Spring中AOP一些概念

目标对象(target):目标对象指将要被增强的对象,即包含主业务逻辑的类对象

连接点(JoinPoint):连接点,程序执行的某一个点,比如执行某个方法,在spring AOP中Join Point总是表示一个方法的执行。

代理对象(Proxy):AOP会通过代理的凡是,对目标对象生成一个代理对象,代理对象中会加入需要增强功能,通过代理对象来间接的方式目标对象,起到增强目标对象的效果。

通知(Advice):需要在目标对象中增强的功能,如上面说的:业务方法前验证用户的功能、方法执行之后打印方法的执行日志。

通知中有连个重要的信息:方法的什么地方,执行什么操作,这两个信息通过通知来指定。

方法的什么地方?之前、之后、包裹目标方法、方法抛出异常后等。

如:

  • 在方法执行之前验证用户是否有效
  • 在方法执行之后,打印方法的执行耗时
  • 在方法抛出异常后,记录异常信息发送到mq.

切入点(Pointcut):用来指定需要使用到哪些地方,比如需要用在哪些类的方法上,切入点就是做这个配置的

切面(Aspect):通知和切入点的组合。切面来定义在哪些地方执行什么操作

顾问(Advisor):Advisor其实就是Pointcut和Advice的组合,Advice是要增强的逻辑,而增强的逻辑要在什么地方执行时通过Pointcut来指定的,所以Advice必须与Pointcut组合在一起,就诞生了Advisor这个类,spring AOP中提供了一个Advisor接口将Pointcut与Advice的组合起来。

@Aspect有5种通知

  • @Before:前置通知,在方法执行之前执行
  • @Around:环绕通知,围绕着方法执行
  • @After:后置通知,在方法执行之后执行
  • @AfterReturning:返回通知,在方法返回结果之后执行
  • *@AfterThrowing:异常通知,在方法抛出异常之后
@Aspect
public class BeforeAspect {

    @Before("execution(* com.Service1.*(..))")
    public void before(JoinPoint joinPoint) {
        System.out.println("我是前置通知!");
    }
}
/*
1. 类上需要使用@Aspect
2. 任意方法上使用@Before标注,将这个方法作为前置通知,目标方法被调用之前,会自动回调
这个方法
3. 被@Before标注的方法参数可以为空,或者为JoinPoint类型,当为JointPoint类型时,必须
为第一个参数
4. 被@Before标注的方法名称可以随意命名,符合java规范就行,其他通知也类型
*/

@Around :环绕通知

环绕通知会包裹目标方法的执行,可以在通知内部调用ProceedingJoinPoint.process方法继续执行下一个拦截器
用起来和@Before类似,但是有两点不一样
1. 若需要获取目标方法的信息,需要将ProceedingJoinPoint作为第一个参数
2. 通常使用Object类型作为方法的返回值,返回值也可以为void
特点:环绕通知比较特殊,其他4种类型的通知都可以用环绕通知来实现。

@After:后置通知

后置通知,在方法执行之后执行,用法和前置通知类似。

特点:

  • 不管目标方法是否有异常,后置通知都会执行
  • 这种通知无法获取方法返回值
  • 可以使用JoinPoint作为方法的第一个参数,用来获取连接点的信息。

@AfterReturning:返回通知

返回通知,在方法返回结果之后执行

特点:

  • 可以获取到方法的返回值
  • 当目标方法返回异常的时候,这个通知不会被调用,这点和@After通知是有区别的

@AfterThrowing:异常通知

在方法抛出异常之后会回调@AfterThrowing标注的方法。

@AfterThrowing标注的方法可以指定异常的类型,当被调用的方法触发该异常及其子类型的异常之后,会触发异常方法的回调,也可以不指定异常类型,此时会匹配所有异常。

特点: 不论异常是否被异常通知捕获,异常还会继续向外抛出。
通知类型 执行时间点 可获取返回值 目标方法异常时是否会执行 @Before

方法执行之前否是

@Around

环绕方法执行是自己控制
@After

方法执行之后否是
@AfterReturning

方法执行之后是否
@AfterThrowing

方法发生异常之后否是

回顾 Spring

@ComponentScan : 注解的作用会扫描当前包中的类,将标注有 @Component 的类注册到spring容器;

@EnableAspectJAutoProxy:用来启用自动代理的创建,简单点理解:会找到容器中所有标注有@Aspect注解的bean以及Advisor类型的bean,会将他们转换为Advisor 集合,spring会通过Advisor集合对容器中满足切入点表达式的bean生成代理对象,整个都是 spring容器启动的过程中自动完成的。

@EnableAsync & @Async 实现方法异步调用

@EnableAsync & @Async

Original: https://blog.csdn.net/m0_61470267/article/details/125585785
Author: 追梦的烟火
Title: 回顾 Spring

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/656950/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球