Spring相关面试题记录

1. Spring Boot的启动流程?

  1. 加载Spring Boot核心配置文件application.properties或application.yml。

  2. 创建Spring应用上下文ApplicationContext。

  3. 执行Spring的自动配置AutoConfiguration,根据依赖及条件,为Spring应用程序自动配置各种组件。

  4. 执行Spring Boot的启动监听器SpringApplicationRunListeners,通知外部应用程序Spring Boot开始启动。

  5. 执行Spring Boot的运行器SpringApplicationRunners,执行应用程序的自定义代码。

  6. 启动Spring Boot Web应用程序,创建Tomcat或Jetty等Web服务器,并将请求转发到Spring MVC控制器。

  7. Spring Boot Web应用程序启动后,向外部应用程序发送应用程序已启动的消息,并返回应用程序的状态。


2. Springboot常用注解有什么? Java代码是如何理解注解的?

Springboot常用注解包括:

  1. @RestController:将Controller类中的所有方法都默认添加了@ResponseBody注解,使得返回的结果都是JSON格式的数据。

  2. @RequestMapping:用于映射请求的URL路径和HTTP请求方法。

  3. @RequestParam:用于将请求参数绑定到Controller方法的参数上。

  4. @PathVariable:用于将URL路径上的变量值绑定到Controller方法的参数上。

  5. @Autowired:用于自动注入依赖对象。

  6. @Service:用于标记业务逻辑层的类。

  7. @Repository:用于标记数据访问层的类。

Java代码中的注解是通过反射机制来实现的。在程序运行时,Java虚拟机会读取注解信息,并根据注解中定义的信息来执行相应的操作。例如,@Autowired注解会告诉Spring容器要自动注入一个依赖对象,Spring容器会根据依赖对象的类型来查找并注入对应的实例。在编写Java代码时,我们可以通过注解来描述程序中的各种元素,从而让程序更加清晰和易于维护。


3. Spring用到了哪些设计模式?

1. 简单工厂模式:BeanFactory简单工厂模式的体现,根据传入一个唯一标识来获得Bean对象

@Override
public Object getBean(String name) throws BeansException {
    assertBeanFactoryActive();
    return getBeanFactory().getBean(name);
}

 在Spring中,BeanFactory就是一个典型的工厂模式的实例,通过BeanFactory可以创建各种Bean对象。其思想体现在Spring的配置文件中,通过配置不同的Bean,可以创建不同的实例对象。

public interface Animal {
    void say();
}

public class Cat implements Animal {

    @Override
    public void say() {
        System.out.println("喵喵喵");
    }
}

public class Dog implements Animal {

    @Override
    public void say() {
        System.out.println("汪汪汪");
    }
}

public class AnimalFactory {

    public static Animal getAnimal(String type) {
        if (type.equals("cat")) {
            return new Cat();
        } else if (type.equals("dog")) {
            return new Dog();
        } else {
            return null;
        }
    }
}

public class Main {

    public static void main(String[] args) {
        Animal cat = AnimalFactory.getAnimal("cat");
        cat.say();
        Animal dog = AnimalFactory.getAnimal("dog");
        dog.say();
    }
}

2. 工厂方法模式:FactoryBean是一个典型的工厂方法模式,这也是三级缓存可以顺利运行的重要一环。Spring在使用getBean()时获得该bean,自动调用getObject()方法。

注意,每一个Bean都会对应一个FactoryBean,例如SqlSession对应SqlSessionFactoryBean

3. 单例模式:一个类仅有一个实例,提供一个访问它的全局访问点。Spring创建Bean实例默认是单例的。在Spring中,很多对象都是单例的,比如ApplicationContext就是单例的。其思想体现在Spring的配置文件中,通过配置Scope为singleton即可创建单例对象。

这里会引出一个常见问题,双重检验锁的单例模式会写吗?

public class Singleton {
    private volatile static Singleton instance;

    private Singleton() {}

    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

这里使用了双重检验锁,即在getInstance()方法中先判断instance是否为空,如果为空再进入同步块进行实例化。这样做的好处是只有在instance为空的情况下才会进行同步,避免了不必要的同步,提高了效率。同时,使用volatile关键字保证了线程间的可见性,避免了由于指令重排序导致的线程不安全问题。

如果不使用双重检验锁,直接在getInstance()方法上加synchronized关键字,会导致每次获取实例都要进行同步,即使实例已经被创建了,这样会浪费很多资源,影响效率。

4. 适配器模式:SpringMVC中的适配器HandlerAdapter。由于应用本身会有多个Controller实现,如果需要直接使用Controller方法,需要判断是由哪一个Controller处理请求,然后调用相应的方法。如果增加新的Controller,则需要修改原来的逻辑,违反了开闭原则。

所以Spring提供了一种适配器接口,每一种Controller对应一种HandlerAdapter实现类。请求到来时,SpringMVC会调用getHandler()获取相应的Controller,并获取该Controller对应的HandlerAdapter,最后调用HandlerAdapter的handle()方法处理请求,这实际上调用的是Controller的handleRequest()。 这样,我们每次添加新的Controller,只需要新增一个适配器类,无需修改原有controller的逻辑。

例如,对注解进行处理,对http请求进行处理

5. 代理模式:Spring的AOP使用了动态代理,两种方式分别是基于JDK和CGLIB。(对于代理的说明,会在其他文章中继续记录),通过代理可以在方法前后添加一些额外的逻辑。

public interface Animal {
    void say();
}

public class Cat implements Animal {

    @Override
    public void say() {
        System.out.println("喵喵喵");
    }
}

public class AnimalProxy implements Animal {

    private Animal animal;

    public AnimalProxy(Animal animal) {
        this.animal = animal;
    }

    @Override
    public void say() {
        System.out.println("开始说话");
        animal.say();
        System.out.println("结束说话");
    }
}

public class Main {

    public static void main(String[] args) {
        Animal cat = new Cat();
        Animal proxy = new AnimalProxy(cat);
        proxy.say();
    }
}

6. 观察者模式:Spring中的观察者模式常用于Listener的实现,例如ApplicationListener。

7. 模板模式:Spring中的jdbcTemplate,hibernateTemplate等使用了模板模式。


4. 什么是AOP和IOC,谈谈你的理解

AOP(面向切面编程)和IOC(控制反转)是两种不同的编程思想和实践方式。

AOP是一种面向切面的编程思想,它将应用程序的关注点(如日志、事务、安全等)从主要业务逻辑中分离出来,形成一种横向的切面结构,以此来增强程序的可维护性和可扩展性。AOP的实现方式主要有基于继承的动态代理和基于组合的静态代理两种。

IOC是一种控制反转的编程思想,它将应用程序中对象的依赖关系由程序员手动管理变为由容器自动管理。这种依赖关系的管理方式被称为“注入”,它是通过容器来实现的。IOC主要有三种实现方式:依赖注入(DI)、控制反转(CI)和依赖查找(DL)。

总的来说,AOP和IOC两种编程思想都是为了提高程序的可维护性和可扩展性,但它们的实现方式和应用场景不同。AOP主要用于解决代码中的横向关注点问题,而IOC主要用于解决对象之间的依赖关系问题。


5. 在Spring中,AOP常用来实现什么?用具体的代码进行说明?

AOP主要用于实现以下模块:

  1. 日志记录:通过AOP可以在方法的执行前、执行后或者抛出异常时记录相关的日志信息,方便开发人员对系统进行调试和排错。

  2. 权限控制:通过AOP可以对系统中的某些操作进行权限控制,例如只有管理员才能够进行敏感操作等。

  3. 缓存管理:通过AOP可以对系统中的缓存进行统一管理,例如在方法执行前先查询缓存中是否存在所需数据,如果存在则直接返回缓存中的数据,避免重复查询数据库。

  4. 性能监控:通过AOP可以对系统中的某些方法进行性能监控,例如记录方法的执行时间、调用次数等信息用于后续的优化。

以下是一个简单的AOP模板代码,用于记录方法执行时间:

@Aspect
@Component
public class LogAspect {
    // 定义切点,拦截所有公共方法
    @Pointcut("execution(public * com.example.demo.service.*.*(..))")
    public void log() {}

    // 在方法执行前记录日志信息
    @Before("log()")
    public void before(JoinPoint joinPoint) {
        System.out.println("方法开始执行:" + joinPoint.getSignature().getName());
    }

    // 在方法执行后记录日志信息
    @After("log()")
    public void after(JoinPoint joinPoint) {
        System.out.println("方法执行结束:" + joinPoint.getSignature().getName());
    }

    // 在方法执行后返回结果时记录日志信息
    @AfterReturning(returning = "result", pointcut = "log()")
    public void afterReturning(Object result) {
        System.out.println("方法返回结果:" + result);
    }

    // 在方法抛出异常时记录日志信息
    @AfterThrowing(throwing = "exception", pointcut = "log()")
    public void afterThrowing(Exception exception) {
        System.out.println("方法抛出异常:" + exception.getMessage());
    }
}

通过以上代码,我们可以对所有位于com.example.demo.service包下的公共方法进行拦截,并在方法执行前、执行后、返回结果时以及抛出异常时记录相应的日志信息。


6. AOP有哪些实现方式?

AOP有两种实现方式:静态代理和动态代理。

静态代理

静态代理:由程序员创建或特定工具自动生成源代码,也就是在编译时就已经将接口,被代理类,代理类等确定下来。在程序运行之前,代理类的.class文件就已经生成。

缺点:代理对象需要与目标对象实现一样的接口,并且实现接口的方法,会有冗余代码。同时,一旦接口增加方法,目标对象与代理对象都要维护。

动态代理

动态代理:代理类在程序运行时创建,AOP框架不会去修改字节码,而是在内存中临时生成一个代理对象,在运行期间对业务方法进行增强,不会生成新类。动态代理的对象是在运行时期创建的,随用随加载。 代理类并不是在Java代码中定义的,而是在运行时根据我们在Java代码中的“指示”动态生成的。相比于静态代理, 动态代理的优势在于可以很方便的对代理类的函数进行统一的处理,而不用修改每个代理类中的方法。

比如说,想要在每个代理的方法前都加上一个处理方法:

 public void giveMoney() {
        //调用被代理方法前加入处理方法
        beforeMethod();
        stu.giveMoney();
    }

这里只有一个giveMoney方法,就写一次beforeMethod方法,但是如果出了giveMonney还有很多其他的方法,那就需要写很多次beforeMethod方法,麻烦。

动态代理的解决方法:所有被代理执行的方法,都是通过在InvocationHandler中的invoke方法调用的,所以我们只要在invoke方法中统一处理,就可以对所有被代理的方法进行相同的操作了


7. Autowired 和 Resource两者有什么区别?

@Resource和@Autowired都是用于依赖注入的注解,但是有以下区别:

  1. 来源不同:@Resource是Java EE标准的注解,而@Autowired是Spring自己的注解。

  2. 自动装配方式不同:@Resource默认按名称装配,@Autowired默认按类型装配。当按名称装配的时候,@Resource需要指定名称,而@Autowired不需要。

  3. 匹配规则不同:@Resource只匹配名称,不匹配类型;@Autowired先按类型匹配,如果有多个相同类型的Bean,则再按名称匹配。

在使用场景上,@Resource适合用于Java EE环境中,而@Autowired适合用于Spring环境中。如果需要按名称装配Bean,可以使用@Resource;如果需要按类型匹配或者同时按名称和类型匹配Bean,可以使用@Autowired。


8. 什么是依赖注入?为什么要依赖注入?

在Spring创建对象的过程中,把对象依赖的属性注入到对象中。依赖注入(Dependency Injection,DI)是一种编程模式,它的主要目的是将类之间的依赖关系从代码中分离出来,使得这些依赖关系可以在运行时动态地被注入到类中,而不是由类自己去创建或者查找依赖对象。(不用人手工去找到合适的实际new,而是需要的时候再创建再使用)

依赖注入的主要优点是可以提高代码的可复用性、可测试性和可维护性。通过将依赖关系从代码中分离出来,我们可以更轻松地替换、升级或者删除依赖对象,而无需修改类的代码。同时,通过将依赖对象注入到类中,我们可以更容易地模拟依赖对象的行为,从而更方便地进行单元测试。

Java中实现依赖注入的方法有很多种,其中比较常用的有以下几种:

且如果我们使用XML的配置形式,最常用的两种注入方法及setter注入和构造器注入。

1. 构造函数注入

public class MyClass {
    private MyDependency myDependency;

    public MyClass(MyDependency myDependency) {
        this.myDependency = myDependency;
    }
}

2. Setter方法注入

public class MyClass {
    private MyDependency myDependency;

    public void setMyDependency(MyDependency myDependency) {
        this.myDependency = myDependency;
    }
}

3. 接口注入

public interface MyDependencyInjector {
    MyDependency getMyDependency();
}

public class MyClass {
    private MyDependency myDependency;

    public MyClass(MyDependencyInjector injector) {
        this.myDependency = injector.getMyDependency();
    }
}

4. 注解注入

public class MyClass {
    @Autowired
    private MyDependency myDependency;
}

以上只是一些常用的依赖注入方法,实际上还有很多其他的方法可以实现依赖注入,例如工厂模式、模板方法模式等等。不同的方法适用于不同的场景和需求,开发者可以根据自己的实际情况选择最适合自己的方法来实现依赖注入。


9. Spring三级缓存知道吗?为什么要设计三级缓存模式?两级缓存下会有什么问题?能解释吗?

如果采用二级缓存,一个放早期bean 一个放成熟bean,可以解决问题,但是考虑到spring aop的特性,不能一开始就创建所有的类,这样会消耗性能和内容,所以在增强的时候,进行创建,需要用到工厂的缓存

9.1

三级缓存解决的问题:解决循环依赖。

循环依赖是指两个或多个类之间存在相互依赖的情况,即每个类都需要引用另一个类才能被正确实现。在Java中,这种情况通常会导致编译错误或者运行时异常。

//ClassA.java
public class ClassA {
    private ClassB b;

    public void setB(ClassB b) {
        this.b = b;
    }

    public void doSomething() {
        b.doSomethingElse();
    }
}

//ClassB.java
public class ClassB {
    private ClassA a;

    public void setA(ClassA a) {
        this.a = a;
    }

    public void doSomethingElse() {
        a.doSomething();
    }
}

//Main.java
public class Main {
    public static void main(String[] args) {
        ClassA a = new ClassA();
        ClassB b = new ClassB();

        a.setB(b);
        b.setA(a);

        a.doSomething();
    }
}

在这个示例中,ClassA和ClassB互相依赖,因为ClassA中的方法需要调用ClassB中的方法,而ClassB中的方法又需要调用ClassA中的方法。

当我们尝试运行Main类时,会发生栈溢出错误(StackOverflowError),因为程序陷入了无限循环,无法正常结束。

因此,循环依赖会导致程序无法正常运行,应该尽量避免。可以通过重构代码,将相互依赖的部分拆分成一个独立的类,或者引入一个中间层来解决循环依赖问题。

9.2 Spring中的循环依赖

@Service
public class A {
    @Autowired
    private B b;
}

@Service
public class B {
    @Autowired
    private A a;
}

在上述代码中,类A依赖类B,类B又依赖类A,形成了循环依赖。当Spring容器启动时,会尝试创建A和B两个bean。由于A依赖B,所以先创建B,并将其注入到A中。但是,由于B又依赖A,所以在创建A的时候又需要注入B,这时就形成了循环依赖,导致Spring无法完成bean的创建和注入,从而抛出循环依赖异常。

9.3 Spring如何解决循环依赖

Spring为了提高性能和减少内存消耗,将Bean的创建过程分为了三个阶段,并在每个阶段中都设置了缓存:

  • singletonObjects:单例缓存,存储完全初始化好的bean对象。
  • earlySingletonObjects:早期单例缓存,存储早期创建但未完全初始化的bean对象。
  • singletonFactories:单例工厂缓存,存储bean工厂对象,用于创建bean。

如果只有两级缓存,即只有单例缓存和单例工厂缓存,可能会出现以下问题
对于循环依赖的情况,只有两级缓存无法解决。当A依赖B,B依赖A时,在单例缓存中,A和B都无法完全初始化,而在单例工厂缓存中,A和B都没有被创建,因此无法解决循环依赖。
对于延迟加载的情况,只有两级缓存也无法解决。在单例缓存中,如果一个bean需要延迟加载,它会被完全初始化并放入单例缓存,这会造成内存浪费。而在单例工厂缓存中,延迟加载的bean需要等到第二次访问时才会被创建,无法满足延迟加载的需求。

问:B中提前注入了一个没有经过初始化的A类型对象不会有问题吗?

答:从下图中我们可以看到,虽然在创建B时会提前给B注入了一个还未初始化的A对象,但是在创建A的流程中一直使用的是注入到B中的A对象的引用,之后会根据这个引用对A进行初始化,所以这是没有问题的。

image-20200706161709829
因此,Spring设计了三级缓存来解决这些问题,早期单例缓存用于解决循环依赖的问题,同时可以解决延迟加载的问题。单例工厂(BeanFactory)缓存仍然用于创建bean,单例缓存则存储完全初始化好的bean对象。这样,Spring可以通过三级缓存来提高性能和解决循环依赖和延迟加载的问题


10. Bean的生命周期了解吗?

10.1 Bean启动的过程

Spring启动过程大致如下:

  1. 创建beanFactory,加载配置文件
  2. 解析配置文件转化beanDefination,获取到bean的所有属性、依赖及初始化用到的各类处理器等
  3. 刷新beanFactory容器,初始化所有单例bean
  4. 注册所有的单例bean并返回可用的容器,一般为扩展的applicationContext

使用到三级缓存的地方就是上述的第三步,创建bean的时候。

Bean 的生命周期是指在 Spring 应用程序中 Bean 对象的从创建到销毁的过程。生命周期主要分为以下几个阶段:

  1. 实例化:在这个阶段,Spring IoC 容器通过 Bean 的配置信息创建一个 Bean 对象。

  2. 属性赋值:在这个阶段,Spring IoC 容器将 Bean 的属性赋值为预先配置好的值或通过依赖注入获得的值。

  3. 对象初始化:在这个阶段,Spring IoC 容器调用 Bean 的初始化方法,以便进一步配置 Bean。

  4. 对象使用:在这个阶段,Bean 对象可以在应用程序中使用。

  5. 对象销毁:在这个阶段,Spring IoC 容器销毁 Bean 对象。

请注意,对于某些特定类型的 Bean,生命周期阶段可能不同,但以上是一般情况的生命周期。


参考资料:

csdn的chitgpt;

Spring高频面试题_牛客网 (nowcoder.com)

面试必杀技,讲一讲Spring中的循环依赖-阿里云开发者社区 (aliyun.com)

Spring的三级缓存各自的作用 - 掘金 (juejin.cn)

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值