Java面试准备之Spring框架系列一

hresh 612 0

Java面试准备之Spring框架系列一

什么是 Spring 框架?

Spring 是一种轻量级开发框架,旨在提高开发人员的开发效率以及系统的可维护性。Spring 官网:https://spring.io/。

我们一般说 Spring 框架指的都是 Spring Framework,它是很多模块的集合,使用这些模块可以很方便地协助我们进行开发。这些模块是:核心容器、数据访问/集成,、Web、AOP(面向切面编程)、工具、消息和测试模块。比如:Core Container 中的 Core 组件是Spring 所有组件的核心,Beans 组件和 Context 组件是实现 IOC 和依赖注入的基础,AOP组件用来实现面向切面编程。

Spring 官网列出的 Spring 的 6 个特征:

  • 核心技术 :依赖注入(DI),AOP,事件(events),资源,i18n,验证,数据绑定,类型转换,SpEL。
  • 测试 :模拟对象,TestContext框架,Spring MVC 测试,WebTestClient。
  • 数据访问 :事务,DAO支持,JDBC,ORM,编组XML。
  • Web支持 : Spring MVC和Spring WebFlux Web框架。
  • 集成 :远程处理,JMS,JCA,JMX,电子邮件,任务,调度,缓存。
  • 语言 :Kotlin,Groovy,动态语言。

优点:

轻量级的开源免费框架,非入侵式的;

控制反转IoC,面向切面编程AOP;

支持事务;

列举一些重要的Spring模块?

下图对应的是 Spring4.x 版本。目前最新的5.x版本中 Web 模块的 Portlet 组件已经被废弃掉,同时增加了用于异步响应式处理的 WebFlux 组件。

Java面试准备之Spring框架系列一

  • Spring Core: 基础,可以说 Spring 其他所有的功能都需要依赖于该类库。
  • Spring Aspects : 该模块为与AspectJ的集成提供支持。
  • Spring AOP :提供了面向切面的编程实现。
  • Spring JDBC : Java数据库连接。
  • Spring JMS :Java消息服务。
  • Spring ORM : 用于支持Hibernate等ORM工具。
  • Spring Web : 为创建Web应用程序提供支持。
  • Spring Test : 提供了对 JUnit 和 TestNG 测试的支持。

谈谈自己对于 Spring IoC 和 AOP 的理解

IoC

IoC(Inverse of Control:控制反转)是一种设计思想,就是将原本在程序中手动创建对象的控制权,交由Spring框架来管理。 IoC 在其他语言中也有应用,并非 Spirng 特有。 IoC 容器是 Spring 用来实现 IoC 的载体, IoC 容器实际上就是个Map(key,value),Map 中存放的是各种对象

要了解控制反转( Inversion of Control ), 我觉得有必要先了解软件设计的一个重要思想:依赖倒置原则(Dependency Inversion Principle )。

  • 高层模块不应该依赖于底层模块,两者应该依赖于其抽象。
  • 抽象不应该依赖具体实现,具体实现应该依赖抽象。

上面2点是依赖倒置原则的概念,也是核心。主要是说模块之间不要依赖具体实现,依赖接口或抽象。

其实依赖倒置原则的核心思想是面向接口编程。

Java面试准备之Spring框架系列一

将对象之间的相互依赖关系交给 IoC 容器来管理,并由 IoC 容器完成对象的注入。这样可以很大程度上简化应用的开发,把应用从复杂的依赖关系中解放出来。 IoC 容器就像是一个工厂一样,当我们需要创建一个对象的时候,只需要配置好配置文件/注解即可,完全不用考虑对象是如何被创建出来的。 在实际项目中一个 Service 类可能有几百甚至上千个类作为它的底层,假如我们需要实例化这个 Service,你可能要每次都要搞清这个 Service 所有底层类的构造函数,这可能会把人逼疯。如果利用 IoC 的话,你只需要配置好,然后在需要的地方引用就行了,这大大增加了项目的可维护性且降低了开发难度。

Spring 时代我们一般通过 XML 文件来配置 Bean,后来开发人员觉得 XML 文件来配置不太好,于是 SpringBoot 注解配置就慢慢开始流行起来。

采用 XML 方式配置 Bean 的时候,Bean 的定义信息是和实现分离的,而采用注解的方式可以把两者合为一体,Bean 的定义信息直接以注解的形式定义在实现类中,从而达到了零配置的目的。

推荐阅读:https://www.zhihu.com/question/23277575/answer/169698662

IoC 解决了什么问题

IoC 的思想就是两方之间不互相依赖,由第三方容器来管理相关资源。这样有什么好处呢?

  1. 对象之间的耦合度或者说依赖程度降低,便于程序扩展;
  2. 资源变的容易管理;比如你用 Spring 容器提供的话很容易就可以实现一个单例。

AOP

AOP(Aspect-Oriented Programming:面向切面编程)能够将那些与业务无关,却为业务模块所共同调用的逻辑或责任(例如事务处理、日志管理、权限控制等)封装起来,便于减少系统的重复代码,降低模块间的耦合度,并有利于未来的可拓展性和可维护性。

Spring AOP就是基于动态代理的,如果要代理的对象,实现了某个接口,那么 Spring AOP 会使用 JDK Proxy,去创建代理对象,而对于没有实现接口的对象,就无法使用 JDK Proxy 去进行代理了,这时候Spring AOP会使用 Cglib 生成一个被代理对象的子类来作为代理,如下图所示:

AOP

关于上述两种动态代理模式,不懂的朋友可以阅读 Spring 系列文章:代理模式

动态代理总结:

JDK 动态代理必须实现接口,通过反射来动态生成代理方法,消耗系统性能,此外生成的代理类也只能代理某个类接口定义的方法 。Cglib 动态代理无需实现接口,通过生成子类字节码来实现,比反射快一点,没有性能问题。但是由于 Cglib 会继承被代理类,需要重写被代理方法,所以被代理类不能是 final 类,被代理方法不能是 final 标识。因此,Cglib 应用更加广泛一些。

当然你也可以使用 AspectJ ,Spring AOP 已经集成了AspectJ ,AspectJ 应该算的上是 Java 生态系统中最完整的 AOP 框架了。

当 Spring 需要使用 @AspectJ 注解支持时,需要在 Spring 配置文件中如下配置:

<aop:aspectj-autoproxy/>

而关于强制使用 Cglib,可以通过在 Spring 配置文件如下实现:

<aop:aspectj-autoproxy proxy-target-class="true"/>

proxy-target-class 属性值决定是基于接口的还是基于类的代理被创建,默认为 false。如果 proxy-target-class 属性值被设置为 true,那么基于类的代理将有效(这时需要 Cglib 库)。反之是基于接口的代理(JDK的动态代理)。

使用 AOP 之后我们可以把一些通用功能抽象出来,在需要用到的地方直接使用即可,这样大大简化了代码量。我们需要增加新功能时也方便,这样也提高了系统扩展性。日志功能、事务管理等等场景都用到了 AOP 。

Spring AOP 和 AspectJ AOP 有什么区别?

Spring AOP 属于运行时增强,而 AspectJ 是编译时增强。 Spring AOP 基于代理(Proxying),而 AspectJ 基于字节码操作(Bytecode Manipulation)。

Spring AOP 已经集成了 AspectJ ,AspectJ 应该算的上是 Java 生态系统中最完整的 AOP 框架了。AspectJ 相比于 Spring AOP 功能更加强大,但是 Spring AOP 相对来说更简单,

如果我们的切面比较少,那么两者性能差异不大。但是,当切面太多的话,最好选择 AspectJ ,它比 Spring AOP 快很多。

Spring AOP相关概念

连接点:JoinPoint,就是要被拦截的方法

能够被拦截的地方,每个成员方法都可以称之为连接点

切入点:PointCut,定义在哪些类,哪些方法上切入(拦截)

Pointcut 的作用就是提供一组规则(使用 AspectJ pointcut expression language 来描述 )来匹配 JoinPoint,给满足规则的 JoinPoint 添加 Advice。

通知:Advice,在拦截到连接点之后要执行的代码,通知主要有五种,前置通知,后置通知,异常通知,最终通知和环绕通知。

通知 Advice 即我们定义的横切逻辑,比如我们可以定义一个用于监控方法性能的通知,也可以定义一个事务处理的通知等。

Java面试准备之Spring框架系列一

切面:AspectJ,就是切入点加上通知

切面 Aspect 整合了切点和通知两个模块,切点解决了 where 问题,通知解决了 when 和 how 问题。切面把两者整合起来,就可以解决 对什么方法(where)在何时(when - 前置还是后置,或者环绕)执行什么样的横切逻辑(how)的三连发问题。在 AOP 中,切面只是一个概念,并没有一个具体的接口或类与此对应。

织入:weaving,把切面加入对象,并创建出代理对象的过程。

织入是把切面应用到目标对象并创建新的代理对象的过程。切面在指定的连接点被织入到目标对象中。在目标对象的生命周期里有很多个点可以织入:

  • 编译期:切面在目标类编译时被织入。这种方式需要特殊的编译器。AspectJ 的织入编译器就是以这种方式织入切面的 。
  • 类加载期:切面在目标类加载到 JVM 时被织入。这种方式需要特殊的类加载器(ClassLoader),它可以在目标类被引入应用之前 增强该目标类的字节码。
  • 运行期:切面在应用运行的某个时刻被织入。一般情况下,在织入切面时,AOP 容器会为目标对象动态地创建一个代理对象。Spring AOP 就是以这种方式织入切面的。

Spring IoC涉及到的重要组件

  1. Resource 主要负责对资源的抽象,它的每一个实现类都代表了一种资源的访问策略,如 ClasspathResource 、 URLResource ,FileSystemResource 等。
  2. 有了资源,就需要有资源加载模块,Spring 利用 ResourceLoader 来进行统一资源加载。
  3. 资源加载完毕之后就需要 BeanFactory 来进行加载解析,它是一个 bean 容器,其中 BeanDefinition 是它的基本结构,它内部维护着一 个 BeanDefinition map ,并可根据 BeanDefinition 的描述进行 bean 的创建和管理。
  4. BeanDefinition 用来描述 Spring 中的 Bean 对象。
  5. BeanDefinitionReader 的作用是读取 Spring 配置文件中的内容,将其转换为 IoC 容器内部的数据结构:BeanDefinition。
  6. ApplicationContext 是个 Spring 容器,也叫做应用上下文。它继承 BeanFactory,同时也是 BeanFactory 的扩展升级版。由于 ApplicationContext 的结构就决定了它与 BeanFactory 的不同,其主要区别有:
    • 继承 MessageSource ,提供国际化的标准访问策略;
    • 继承 ApplicationEventPublisher,提供强大的事件机制;
    • 扩展 ResourceLoader,可以用来加载多个 Resource,可以灵活访问不同的资源;
    • 对 Web 应用的支持。

推荐阅读:Spring之IoC理论

谈一谈ClassPathResource

AbstractResource 为 Resource 的默认实现类,它对 Resource 接口做了统一的实现,子类继承该类后只需要覆盖相应的方法即可,同时对于自定义的 Resource 我们也是继承该类。

ClassPathResource 类是对 classpath 下资源的封装,或者是说对 ClassLoader.getResource()方法或 Class.getResource()方法的封装 ,它支持在当前 classpath 中读取资源文件。可以传入相对 classpath 的文件全路径名和 ClassLoader 构建 ClassPathResource,或忽略 ClassLoader 采用默认ClassLoader(即DefaultResourceLoader),此时在 getInputStream()方法 实现时会使用 ClassLoader.getSystemResourceAsStream(path)方法。 由于使用 ClassLoader 获取资源时默认相对于 classpath 的根目录,因而构造函数会忽略开头的“/”字符。ClassPathResource 还可以使用文件路径和 Class 作为参数构建,此时文件路径需要以“/”开头,表示该文件为相对于classpath 的绝对路径,否则为相对 Class 实例的相对路径,然后程序会报错,在 getInputStream()方法实现时使用 Class.getResourceAsStream()方法。

如下代码:

@Test
public void getResource() throws IOException {
    //ClassPathResource
    ClassPathResource resource = new ClassPathResource("application_context.xml");
    //        ClassPathResource resource = new ClassPathResource("/application_context.xml", User.class);
    InputStream input = resource.getInputStream();
    Assert.assertNotNull(input);
    System.out.println(resource.getClassLoader());
    System.out.println(resource.getPath());

}

关于获取资源的方式有两种:Class 获取和 ClassLoader 获取。

  • ClassLoader.getResource("")获取的是 classpath 的根路径
  • Class.getResource("")获取的是相对于当前类的相对路径
  • Class.getResource("/")获取的是 classpath 的根路径
  • System.getProperty("user.dir")获取的是项目的路径

推荐阅读:Spring IoC资源管理之Resource

关于 ResourceLoader的相关了解

DefaultResourceLoader 同样也是 ResourceLoader 的默认实现,在自定义 ResourceLoader 的时候我们除了可以继承该类外还可以实现 ProtocolResolver 接口来实现自定义资源加载协议。

DefaultResourceLoader 每次只能返回单一的资源,所以 Spring 针对该情况提供了另一个接口 ResourcePatternResolver,该接口提供了根据指定的 locationPattern 返回多个资源的策略。其子类 PathMatchingResourcePatternResolver 是一个集大成者的 ResourceLoader,因为它既实现 了 Resource getResource(String location) 也实现了 Resource[] getResources(String locationPattern)

推荐阅读:Spring IoC资源管理之ResourceLoader

你知道BeanFactoryPostProcessor和BeanPostProcessor吗?

BeanFactoryPostProcessor

BeanFactoryPostProcessor 和 BeanPostProcessor 这两个接口,都是 Spring 初始化 bean 时对外暴露的扩展点,一般叫做 Spring 的 Bean 后置处理器接口,作用是为 Bean 的初始化前后 提供可扩展的空间。两个接口名称看起来很相似,但作用和使用场景却略有不同。对比 bean 的生命周期图可以发现:

Java面试准备之Spring框架系列一

BeanFactoryPostProcessor 是在 Spring 容器加载了定义 bean 的 XML 文件之后,在 bean 实例化之前执行的。接口方法的入参是 ConfigurrableListableBeanFactory 类型,使用该参数可以获取到相关的 bean 的定义信息。

BeanPostProcessor

BeanPostProcessor 可以在 spring 容器实例化 bean 之后,在执行 bean 的初始化方法前后,添加一些自己的处理逻辑。 这里说的初始化方法,指的是以下两种:

  1. bean 实现 了 InitializingBean 接口,对应的方法为 afterPropertiesSet 。

  2. 在 XML 文件中定义 bean 的时候,标签有个属性叫做 init-method,来指定初始化方法。

    注意:BeanPostProcessor 是在 spring 容器加载了 bean 的定义文件并且实例化 bean 之后执行的。BeanPostProcessor 的执行顺序是在 BeanFactoryPostProcessor 之后。

推荐阅读:Spring之BeanFactoryPostProcessor和BeanPostProcessor

ClassPathXmlApplicationContext相关知识

ClassPathXmlApplicationContext 作为 ApplicationContext 最经常使用的子类,关于它的学习尤为重要。该类分为两种构造方法:构造方法之 configLocations构造方法之 paths,对应下述代码:

public ClassPathXmlApplicationContext(String[] configLocations, boolean refresh, @Nullable ApplicationContext parent) throws BeansException {
    super(parent);
    this.setConfigLocations(configLocations);
    if (refresh) {
        this.refresh();
    }

}

public ClassPathXmlApplicationContext(String[] paths, Class<?> clazz, @Nullable ApplicationContext parent) throws BeansException {
    super(parent);
    Assert.notNull(paths, "Path array must not be null");
    Assert.notNull(clazz, "Class argument must not be null");
    this.configResources = new Resource[paths.length];

    for(int i = 0; i < paths.length; ++i) {
        this.configResources[i] = new ClassPathResource(paths[i], clazz);
    }

    this.refresh();
}

两者获取资源的方式不同,前者通过 ClassLoader 获取,后者通过 Class 获取。

前者关于设置文件路径有自己的实现方法 setConfigLocations,而后者中无此代码实现,没法使用占位符设置配置文件路径。

推荐阅读:Spring IoC之ClassPathXmlApplicationContext

refresh()核心方法通读

  • 方法是加锁的,这么做的原因是避免多线程同时刷新 Spring 上下文;
  • 尽管加锁可以看到是针对整个方法体的,但是没有在方法前加 synchronized 关键字,而使用了对象锁 startUpShutdownMonitor,这样做有两个好处:
    • (1)refresh()方法和 close()方法都使用了 startUpShutdownMonitor 对象锁加锁,这就保证了在调用 refresh()方法的时候无法调用 close()方法,反之依然,这样就避免了冲突。
    • (2)使用对象锁可以减小同步的范围,只对不能并发的代码块进行加锁,提高了整体代码运行的速率。
  • 在 refresh()方法中整合了很多个子方法,使得整个方法流程清晰易懂。这样一来,方便代码的可读性和可维护性。

推荐阅读:Spring IoC之ApplicationContext中refresh过程

FactoryBean

一般情况下,Spring 通过反射机制利用 class 属性指定实现类实例化 Bean,在某些情况下,实例化 Bean 过程比较复杂,如果按照传统的定义,则需要在配置文件中提供大量的配置信息。

如下述配置文件所示,如果 Car 类有十几个甚至更多的属性时,我们需要配置很多个 property,该过程比较麻烦。

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:p="http://www.springframework.org/schema/p"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="car" class="com.msdn.bean.Car">
        <property name="maxSpeed" value="120" />
        <property name="brand" value="BMW" />
        <property name="price" value="2500.5" />
    </bean>

</beans>

新建一个实现 FactoryBean 接口的 Bean 类,与普通的 Bean 类不同的是: 根据该 Bean 的 ID 从 BeanFactory 中获取的实际上是 FactoryBean 的 getObject() 返回的对象,而不是 FactoryBean 本身,如果要获取 FactoryBean 对象,请在 ID 前面加一个&符号来获取。

推荐阅读:Spring IoC之BeanFactory

AbstractBeanFactory中的考点

doGetBean 方法中优先执行 getSingleton 方法的原因?

protected <T> T doGetBean(String name, @Nullable Class<T> requiredType, @Nullable Object[] args, boolean typeCheckOnly) throws BeansException {
    String beanName = this.transformedBeanName(name);
    Object sharedInstance = this.getSingleton(beanName);
    ..........
}

目的是为了检查缓存中的实例工程是否存在对应的实例。因为在创建单例 bean 的时候会存在依赖注入的情况,而在创建依赖的时候为了避免循环依赖。Spring 创建 bean 的原则是在不等 bean 创建完就会将创建 bean 的objectFactory 提前曝光,即将其加入到缓存中,一旦下个 bean 创建时依赖上个 bean 则直接使用 objectFactory ,直接从缓存中或 singletonFactories 中获取 objectFactory。就算没有循环依赖,只是单纯的依赖注入,如B依赖A,如果A已经初始化完成,B进行初始化时,需要递归调用 getBean 获取A,这是A已经在缓存里了,直接可以从这里取到。

单例模式下的 getSingleton 方法做了哪些操作?

public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory)
  1. 再次检查缓存是否已经加载过,如果已经加载了则直接返回,否则开始加载过程。
  2. 调用 beforeSingletonCreation() 记录加载单例 bean 之前的加载状态,即前置处理。
  3. 调用参数传递的 ObjectFactory 的 getObject() 实例化 bean。
  4. 调用 afterSingletonCreation() 进行加载单例后的后置处理。
  5. 将结果记录并加入值缓存中,同时删除加载 bean 过程中所记录的一些辅助状态。

Spring 中的 bean 的作用域有哪些?

  • singleton : 唯一 bean 实例,Spring 中的 bean 默认都是单例的。
  • prototype : 每次请求都会创建一个新的 bean 实例。
  • request : 每一次HTTP请求都会产生一个新的bean,该bean仅在当前HTTP request内有效。
  • session : 每一次HTTP请求都会产生一个新的 bean,该bean仅在当前 HTTP session 内有效。
  • global-session: 全局session作用域,仅仅在基于portlet的web应用中才有意义,Spring5已经没有了。Portlet是能够生成语义代码(例如:HTML)片段的小型Java Web插件。它们基于portlet容器,可以像servlet一样处理HTTP请求。但是,与 servlet 不同,每个 portlet 都有不同的会话

其中比较常用的是singleton和prototype两种作用域。对于singleton作用域的Bean,每次请求该Bean都将获得相同的实例。容器负责跟踪Bean实例的状态,负责维护Bean实例的生命周期行为;如果一个Bean被设置成prototype作用域,程序每次请求该id的Bean,Spring都会新建一个Bean实例,然后返回给程序。在这种情况下,Spring容器仅仅使用new 关键字创建Bean实例,一旦创建成功,容器不在跟踪实例,也不会维护Bean实例的状态。

如果不指定Bean的作用域,Spring默认使用singleton作用域。Java在创建Java实例时,需要进行内存申请;销毁实例时,需要完成垃圾回收,这些工作都会导致系统开销的增加。因此,prototype作用域Bean的创建、销毁代价比较大。而singleton作用域的Bean实例一旦创建成功,可以重复使用。因此,除非必要,否则尽量避免将Bean被设置成prototype作用域。

Spring bean之间的关系

在 Spring 容器中,两个 Bean 之间除了注入关系外,还存在继承、依赖和引用关系

  • 继承关系:在 Spring 容器当中允许使用 abstract 标签来定义一个父 bean,parent 标签来定义一个子 bean。子 bean 将自动继承父 bean 的配置信息。
  • 依赖关系:Spring 允许用户通过 depends-on 标签来设定 bean 的前置依赖 bean,前置依赖的 bean 会在本 bean 实例化之前创建好,供本 bean 使用。
  • 引用关系:不光可以通过 ref 标签来引用其他的 bean,而且可以通过 idref 标签来引用其他 bean 的名字。它的主要作用是:在 Spring 容器启动的时候就可以检查引用关系的正确性,从而可以提前发现配置信息是否存在错误。

推荐阅读:Spring bean之间的关系

循环依赖

什么是循环依赖?

循环依赖其实是循环引用,也就是两个或则两个以上的 bean 互相持有对方,最终形成闭环。比如A依赖于B,B依赖于C,C又依赖于A。如下图所示:

Java面试准备之Spring框架系列一

注意,这里不是函数的循环调用,是对象的相互依赖关系。循环调用其实就是一个死循环,除非有终结条件。
Spring 中循环依赖场景有:

  • 构造器的循环依赖
  • field 属性的循环依赖

对于构造器的循环依赖,Spring 是无法解决的,只能抛出 BeanCurrentlyInCreationException 异常表示循环依赖,所以下面我们分析的都是基于 field 属性的循环依赖。

Spring 只解决 scope 为 singleton 的循环依赖,对于scope 为 prototype 的 bean Spring 无法解决,直接抛出 BeanCurrentlyInCreationException 异常。

如何解决循环依赖?

Spring 解决 singleton bean 的关键因素所在,被称为三级缓存,第一级为 singletonObjects,第二级为 earlySingletonObjects,第三级为 singletonFactories。

Spring 在创建 bean 的时候并不是等它完全完成,而是在创建过程中将创建中的 bean 的 ObjectFactory 提前曝光(即加入到 singletonFactories 缓存中),这样一旦下一个 bean 创建的时候需要依赖 bean ,则直接使用 ObjectFactory 的 getObject() 获取了,也就是 getSingleton()中的代码片段了。

示意图:

Java面试准备之Spring框架系列一

Spring 中的单例 bean 的线程安全问题了解吗?

Spring 容器中的 Bean 是否线程安全,容器本身并没有提供 Bean 的线程安全策略,因此可以说 spring 容器中的 Bean 本身不具备线程安全的特性,但是具体还是要结合具体 scope 的Bean去研究。

单例 bean 存在线程问题,主要是因为当多个线程操作同一个对象的时候,对这个对象的非静态成员变量的写操作会存在线程安全问题。

常见的有两种解决办法:

  • 在Bean对象中尽量避免定义可变的成员变量(不太现实)。
  • 在类中定义一个ThreadLocal成员变量,将需要的可变成员变量保存在 ThreadLocal 中(推荐的一种方式)。

发表评论 取消回复
表情 图片 链接 代码

分享