面试还不懂这10道Spring问题,回去等通知了

5,494 阅读11分钟

最近有一位朋友和我说,他做了开发 3 年了,最近去面试时,Spring 被面试官问得哑口无言,他总结了下面几道被问到的关于 Spring 的面试题,可以参考下。

参考问题

  • Spring IoC、AOP 原理
  • Spring Bean 生命周期
  • Spring Bean 注入是如何解决循环依赖问题的
  • 怎样用注解的方式配置 Spring?
  • Spring 事务为何失效了
  • SpringMVC 的流程?
  • Springmvc 的优点:
  • Spring 通知类型使用场景分别有哪些?
  • IoC 控制反转设计原理?
  • Spring 如何处理线程并发问题?
  • 参考解析

    1.Spring IoC、AOP 原理

    1.1.定义

    1.1.1.IoC

    Inversion of Control,控制反转。是面向对象编程中的一种设计原则,可以用来减低计算机代码之间的耦合度。其中最常见的方式叫做依赖注入(DependencyInjection,简称 DI),这也是 Spring 的实现方式。通过控制反转,对象在被创建的时候,由一个调控系统内所有对象的外界实体将其所依赖的对象的引用传递给它。也可以说,依赖被注入到对象中。

    1.1.2.AOP

    Aspect Oriented Programming,面向切面编程。通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。AOP 是 OOP 的延续,是软件开发中的一个热点,也是 Spring框架中的一个重要内容,是函数式编程的一种衍生范型。利用 AOP 可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。其中,最常用的使用场景一般有日志模块、权限模块、事物模块。

    1.2.原理

    1.2.1.IoC

    IoC 内部核心原理就是反射技术,当然这里面还涉及到 Bean 对象的初始化构建等步骤,这个在后面的生命周期中讲,这里我们需要了解 Java 中反射是如何做的就好。这里主要说明下主要的相关类和可能面试问题转向,具体的 API 实现需要自己去看。

    还有其他的类不一一列举出来,都在 java.lang.reflect 包下。说到这个模块的时候,那么面试官可能会考察相关的知识,主要是考察你是否真的有去了解过反射的使用。举两个例子:

    利用反射获取实例的私有属性值怎么做

    这里其实就是里面的重要考察点就是反射对私有属性的处理。

    /**
     * 通过反射获取私有的成员变量.
     */
    private Object getPrivateValue(Person person, String fieldName)
    {
        try
        {
            Field field = person.getClass().getDeclaredField(fieldName);
            // 主要就是这里,需要将属性的 accessible 设置为 true 
            field.setAccessible(true);
            return field.get(person);
        }
        catch(Exception e)
        {
            e.printStackTrace();
        }
        return null;
    }

    如何通过反射构建对象实例?

    使用默认构造函数(无参)创建的话:

    Class.newInstance() Constroctor constroctor = clazz.getConstructor(String.class,Integer.class); Object obj = constroctor.newInstance("name", 18);

    1.2.2.AOP

    AOP 的内部原理其实就是动态代理和反射了。主要涉及到的反射类:


    动态代理相关原理的话,你需要了解什么是代理模式、静态代理的不足、动态代理的实现原理。Spring 中实现动态代理有两种方式可选,这两种动态代理的实现方式的一个对比也
    是面试中常问的。

    JDK 动态代理

    必须实现 InvocationHandler 接口,然后通过 Proxy.newProxyInstance(ClassLoader
    loader, Class<?>[] interfaces, InvocationHandler h) 获得动态代理对象。
    CGLIB 动态代理

    使用 CGLIB 动态代理,被代理类不需要强制实现接口。CGLIB 不能对声明为 final的方法进行代理,因为 CGLIB 原理是动态生成被代理类的子类。

    OK,AOP 讲了。其实讲到这里,可能会有一个延伸的面试问题。我们知道,Spring事物也是 通 过 AOP 来 实 现的 , 我们使用的时候 一 般就是在方法上 加@Tranactional 注解,那么你有没有遇到过事物不生效的情况呢?这是为什么?这个问题我们在后面的面试题中会讲。

    2.Spring Bean 生命周期


    这只是个大体流程,内部的具体行为太多,需要自行去看看代码。

    3.Spring Bean 注入是如何解决循环依赖问题的

    3.1. 什么是循环依赖,有啥问题?

    循环依赖就是 N 个类中循环嵌套引用,这样会导致内存溢出。循环依赖主要分两种:

    • 构造器循环依赖
    • setter 循环依赖
    3.2. Spring 解决循环依赖问题
    • 构造器循环依赖问题
    无解,直接抛出 BeanCurrentlyInCreatingException 异常。
    • setter 循环依赖问题
    单例模式下,通过“三级缓存”来处理。非单例模式的话,问题无解。

    Spring 初始化单例对象大体是分为如下三个步骤的:

    • createBeanInstance:调用构造函数创建对象
    • populateBean:调用类的 setter 方法填充对象属性
    • initializeBean:调用定义的 Bean 初始化 init 方法
    可以看出,循环依赖主要发生在 1、2 步,当然如果发生在第一步的话,Spring 也是无法解决该问题的。那么就剩下第二步 populateBean 中出现的循环依赖问题。通过“三级缓存”来处理,三级缓存如下:

    • singletonObjects:Cache of singleton objects: bean name --> bean instance,完成初始化的单例对象的 cache(一级缓存)
    • earlySingletonObjects:Cache of early singleton objects: bean name--> bean instance ,完成实例化但是尚未初始化的,提前暴光的单例对象的 cache (二级缓存)
    • singletonFactories : Cache of singleton factories: bean name -->ObjectFactory,进入实例化阶段的单例对象工厂的 cache (三级缓存)

    我们看下获取单例对象的方法:

    protected Object getSingleton(String beanName, boolean allowEarlyReference)
    {
        Object singletonObject = this.singletonObjects.get(beanName);
        // isSingletonCurrentlyInCreation:判断当前单例 bean 是否正在创建中 
        if(singletonObject == null && isSingletonCurrentlyInCreation(beanName))
        {
            synchronized(this.singletonObjects)
            {
                singletonObject = this.earlySingletonObjects.get(beanName);
                // allowEarlyReference:是否允许从 singletonFactories 中通过 getObject 拿到 
                对象
                if(singletonObject == null && allowEarlyReference)
                {
                    ObjectFactory <? > singletonFactory = this.singletonFactories.get(beanName);
                    if(singletonFactory != null)
                    {
                        singletonObject = singletonFactory.getObject();
                        this.earlySingletonObjects.put(beanName, singletonObject);
                        this.singletonFactories.remove(beanName);
                    }
                }
            }
        }
        return(singletonObject != NULL_OBJECT ? singletonObject : null);
    }

    其中解决循环依赖问题的关键点就在 singletonFactory.getObject() 这一步,getObject 这是 ObjectFactory<T> 接口的方法。Spring 通过对该方法的实现,在createBeanInstance 之后,populateBean 之前,通过将创建好但还没完成属性设置和初始化的对象提前曝光,然后再获取 Bean 的时候去看是否有提前曝光的对象实例来判断是否要走创建流程。

    protected void addSingletonFactory(String beanName, ObjectFactory <? > singletonFactory)
    {
        Assert.notNull(singletonFactory, "Singleton factory must not be null");
        synchronized(this.singletonObjects)
        {
            if(!this.singletonObjects.containsKey(beanName))
            {
                this.singletonFactories.put(beanName, singletonFactory);
                this.earlySingletonObjects.remove(beanName);
                this.registeredSingletons.add(beanName);
            }
        }
    }

    4.Spring 事务为何失效了

    可能的原因:

    1. MySQL 使用的是 MyISAM 引擎,而 MyISAM 是不支持事务的。需要支持使用可以使用 InnoDB 引擎
    2. 如果使用了 Spring MVC ,context:component-scan 重复扫描问题可能会引起事务失败
    3. @Transactional 注解开启配置放到 DispatcherServlet 的配置里了。
    4. @Transactional 注解只能应用到 public 可见度的方法上。 在其他可见类型上声明,事务会失效。
    5. 在接口上使用 @Transactional 注解,只能当你设置了基于接口的代理时它才生效。所以如果强制使用了 CGLIB,那么事物会实效。
    6. @Transactional 同一个类中无事务方法 a() 内部调用有事务方法 b(),那么此时事物不生效。
    按 理 说 , 如 果 按 照 Spring 说 的 事 物 传 播 级 别 去 配 置 其 事 物 级 别 为REQUIRES_NEW 的话,那么应该是在调用 b() 的时候会新生成一个事物。实际上却没有。



    NOT_SUPPORTED 总是非事务地执行,并挂起任何存在的事务其实,这是由于 Spring 的事物实现是通过 AOP 来实现的。此时,当这个有注解的方法 b() 被调用的时候,实际上是由代理类来调用的,代理类在调用之前就会启动 transaction。然而,如果这个有注解的方法是被同一个类中的其他方法 a() 调用的,那么该方法的调用并没有通过代理类,而是直接通过原来的那个 bean,所以就不会启动 transaction,我们看到的现象就是 @Transactional 注解无效。

    5.怎样用注解的方式配置 Spring?

    Spring 在 2.5 版本以后开始支持用注解的方式来配置依赖注入。可以用注解的方式来替代XML 方式的 bean 描述,可以将 bean 描述转移到组件类的内部,只需要在相关类上、方法上或者字段声明上使用注解即可。注解注入将会被容器在 XML 注入之前被处理,所以后者会覆盖掉前者对于同一个属性的处理结果。注解装配在 Spring 中是默认关闭的。所以需要在 Spring 文件中配置一下才能使用基于注解的装配模式。如果你想要在你的应用程序中使用关于注解的方法的话,请参考如下的配置。

    <beans>
        <context:annotation-config/>
        <!-- bean definitions go here -->
    </beans>

    在标签配置完成以后,就可以用注解的方式在 Spring 中向属性、方法和构造方法中自动装配变量。

    下面是几种比较重要的注解类型:

    • @Required:该注解应用于设值方法。
    • @Autowired:该注解应用于有值设值方法、非设值方法、构造方法和变量。
    • @Qualifier:该注解和@Autowired 注解搭配使用,用于消除特定 bean 自动装配的歧义。
    • JSR-250 Annotations:Spring 支持基于 JSR-250 注解的以下注解,@Resource、@PostConstruct 和@PreDestroy。

    6、SpringMVC 的流程?

    1. 用户发送请求至前端控制器 DispatcherServlet;
    2. DispatcherServlet 收到请求后,调用 HandlerMapping 处理器映射器,请求获取Handle
    3. 处理器映射器根据请求 url 找到具体的处理器,生成处理器对象及处理器拦截器(如果有则生成)一并返回给 DispatcherServlet;
    4. DispatcherServlet 调用 HandlerAdapter 处理器适配器;
    5. HandlerAdapter 经过适配调用 具体处理器(Handler,也叫后端控制器);
    6. Handler 执行完成返回 ModelAndView;
    7. HandlerAdapter 将 Handler 执 行 结 果 ModelAndView 返 回 给DispatcherServlet ;
    8. DispatcherServlet 将 ModelAndView 传 给ViewResolver 视图解析器进行解析;
    9. ViewResolver 解析后返回具体View;
    10. DispatcherServlet 对 View 进行渲染视图(即将模型数据填充至视图中)
    11. DispatcherServlet 响应用户。


    7、Springmvc 的优点:

    1. 可以支持各种视图技术,而不仅仅局限于 JSP;
    2. 与 Spring 框架集成(如 IoC 容器、AOP 等);
    3. 清 晰 的 角 色 分 配 : 前 端 控 制 器 (dispatcherServlet) , 请 求 到处理器映射(handlerMapping), 处理器适配器(HandlerAdapter), 视图解析器(ViewResolver)。
    4. 支持各种请求资源的映射策略。

    8. Spring 通知类型使用场景分别有哪些?

    9.IoC 控制反转设计原理?

    具体设计原理如下图:


    10.Spring 如何处理线程并发问题?

    Spring 使用 ThreadLocal 解决线程安全问题。我们知道在一般情况下,只有无状态的 Bean 才可以在多线程环境下共享,在Spring 中,绝大部分 Bean 都可以声明为 singleton 作用域。就是因为Spring 对 一 些 Bean ( 如 RequestContextHolder 、
    TransactionSynchronizationManager、LocaleContextHolder 等)中非线程安全状态采用 ThreadLocal 进行处理,让它们也成为线程安全的状态,因为有状态的 Bean 就可以在多线程中共享了。

    ThreadLocal 和线程同步机制都是为了解决多线程中相同变量的访问冲突问题。在同步机制中,通过对象的锁机制保证同一时间只有一个线程访问变量。这时该变量是多个线程共享的,使用同步机制要求程序慎密地分析什么时候对变量进行读写,什么时候需要锁定某个对象,什么时候释放对象锁等繁杂的问题,程序设计和编写难度相对较大。而 ThreadLocal 则从另一个角度来解决多线程的并发访问。 ThreadLocal 会为每一个线程提供一个独立的变量副本,从而隔离了多个线程对数据的访问冲突。因为每一个线程都拥有自己的变量副本,从而也就没有必要对该变量进行同步了。 ThreadLocal 提供了线程安全的共享对象,在编写多线程代码时,可以把不安全的变量封装进 ThreadLocal。

    由于 ThreadLocal 中可以持有任何类型的对象,低版本 JDK 所提供的 get()返回的是 Object 对象,需要强制类型转换。但 JDK 5.0 通过泛型很好的解决了这个问题,在一定程度地简化 ThreadLocal 的使用。概括起来说,对于多线程资源共享的问题,同步机制采用了“以时间换空间”的方式,而 ThreadLocal采用了“以空间换时间”的方式。前者仅提供一份变量,让不同的线程排队访问,而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响。

    最后

    欢迎大家一起交流,喜欢文章记得关注我点个赞哟,感谢支持!

    欢迎大家关注我的公众号【以Java架构赢天下】,2019年多家公司java面试笔记整理了500多页pdf文档,文章都会在里面更新,整理的资料也会放在里面。


    image.png