Spring高频面试题

52 篇文章 1 订阅
8 篇文章 1 订阅


文章持续更新中…
我们知道java之所能够有如此美好的生态,spring绝对功不可没,因为完美的和spring兼容在一起,才有了那么多丰富好用的功能。

1.AOP有哪些实现方式?

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

1.1 静态代理

静态代理:代理类在编译阶段生成,在编译阶段将通知织入Java字节码中,也称编译时增强

AspectJ使用的是静态代理。

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

1.2 动态代理

动态代理:代理类在程序运行时创建,AOP框架不会去修改字节码,而是在内存中临时生成一个代理对象,在运行期间对业务方法进行增强,不会生成新类。

1.2.1 JDK动态代理和CGLIB动态代理的区别?

Spring AOP中的动态代理主要有两种方式:JDK动态代理和CGLIB动态代理。

JDK动态代理

如果目标类实现了接口,Spring AOP会选择使用JDK动态代理目标类。

代理类根据目标类实现的接口动态生成,不需要自己编写,生成的动态代理类和目标类都实现相同的接口。JDK动态代理的核心是InvocationHandler接口和Proxy类。

缺点:目标类必须有实现的接口。如果某个类没有实现接口,那么这个类就不能用JDK动态代理。

CGLIB来动态代理

通过继承实现。如果目标类没有实现接口,那么Spring AOP会选择使用CGLIB来动态代理目标类。CGLIB(Code Generation Library)可以在运行时动态生成类的字节码,动态创建目标类的子类对象,在子类对象中增强目标类。

CGLIB是通过继承的方式做的动态代理,因此如果某个类被标记为final,那么它是无法使用CGLIB做动态代理的。

优点:目标类不需要实现特定的接口,更加灵活。

两者的区别:

jdk动态代理使用jdk中的类Proxy来创建代理对象,它使用反射技术来实现,不需要导入其他依赖。cglib需要引入相关依赖:asm.jar,它使用字节码增强技术来实现。
当目标类实现了接口的时候,Spring Aop默认使用jdk动态代理方式来增强方法,没有实现接口的时候使用cglib动态代理方式增强方法。

2.Bean注入容器有哪些方式?

将普通类交给Spring容器管理,通常有以下方法:

1、使用@Configuration与@Bean注解;

2、使用@Controller、@Service、@Repository、@Component 注解标注该类,然后启用@ComponentScan自动扫描;

3、使用@Import 方法。使用@Import注解把Bean导入到容器中,代码如下:

@ComponentScan
@Import({Demo.class})
public class App {
    public static void main(String[] args) throws Exception {
        ConfigurableApplicationContext context = SpringApplication.run(App.class, args);
        System.out.println(context.getBean(Demo.class));
        context.close();
    }
}

3.@Autowired和@Resource的区别?

1、@Autowired注解是按照类型byType装配依赖对象的,但是如果存在多个类型⼀致的Bean,⽆法通过byType注⼊时,就会使⽤byName根据名称来注⼊。

byName是将bean的名字与被注入的成员变量的名称匹配,而不是与被注入的成员变量的类型匹配。以下代码中,App 类的成员变量 userService1 会使用名字为 userService1 的 Bean,成员变量 userService2 会使用名字为 userService2 的 Bean。

@Service
class UserService1 implements UserService {...}
@Service
class UserService2 implements UserService {...}

public class App {
    @Autowired
    private UserService userService1;
    @Autowired
    private UserService userService2;

    @Test
    public void test() {
        System.out.println(userService1.getClass().toString());
        System.out.println(userService2.getClass().toString());
    }
}

如果⽆法判断注⼊哪个Bean则会抛出UnsatisfiedDependencyException。

2、@Resource会⾸先按照名称byName来装配,如果找不到Bean,会⾃动byType再找⼀次。

4.@Qualifier 注解有什么作用

当需要创建多个相同类型的 Bean 并希望仅使用属性装配其中一个 Bean 时,可以使用@Qualifier注解和 @Autowired 通过指定应该装配哪个 Bean 来消除歧义。

@Component("bird")
public class Bird implements Fly {...}

@Component("plane")
public class Plane implements Fly {...}

@Component
public class FooService {
    @Autowired
    @Qualifier("bird")
    private Fly fly;
}

5.@Bean和@Component有什么区别?

都是使用注解定义 Bean。两者的区别:

1、@Bean 是使用 Java 代码装配 Bean,@Component 是自动装配 Bean。

2、@Component 注解用在类上,表明一个类会作为组件类,并告知Spring要为这个类创建Bean,每个类对应一个 Bean。
而@Bean 注解用在方法上,表示这个方法会返回一个 Bean。@Bean 需要在配置类中使用,即类上需要加上@Configuration注解。

@Component
public class Student {...}

@Configuration
public class WebSocketConfig {
    @Bean
    public Student student(){
        return new Student();
    }
}

@Bean 注解更加灵活。当需要将第三方类装配到 Spring 容器中,因为没办法源代码上添加@Component注解,只能使用@Bean注解的方式,当然也可以使用 xml 的方式。

6.@Component、@Controller、@Repositor和@Service 的区别?

  • @Component:最普通的组件,可以被注入到spring容器进行管理。

  • @Controller:将类标记为 Spring Web MVC 控制器。

  • @Service:业务层组件。

  • @Repository:数据访问组件,即DAO组件。

7.Spring 事务实现方式有哪些?

事务就是一系列的操作原子执行。Spring事务机制主要包括声明式事务和编程式事务。

  • 编程式事务:通过编程的方式管理事务,这种方式带来了很大的灵活性,但很难维护。
  • 声明式事务:将事务管理代码从业务方法中分离出来,通过aop进行封装。
    Spring声明式事务使得我们无需要去处理获得连接、关闭连接、事务提交和回滚等这些操作。使用 @Transactional 注解开启声明式事务。
    @Transactional相关属性如下:
    在这里插入图片描述

上表字段说明:

  • value :主要用来指定不同的事务管理器;主要用来满足在同一个系统中,存在不同的事务管理器。比如在Spring中,声明了两种事务管理器txManager1, txManager2.然后,用户可以根据这个参数来根据需要指定特定的txManager.
    value 适用场景:在一个系统中,需要访问多个数据源或者多个数据库,则必然会配置多个事务管理器的

  • REQUIRED_NEW:内部的事务独立运行,在各自的作用域中,可以独立的回滚或者提交;而外部的事务将不受内部事务的回滚状态影响。

  • ESTED 的事务,基于单一的事务来管理,提供了多个保存点。这种多个保存点的机制允许内部事务的变更触发外部事务的回滚。而外部事务在混滚之后,仍能继续进行事务处理,即使部分操作已经被混滚。由于这个设置基于 JDBC 的保存点,所以只能工作在 JDB C的机制。

  • rollbackFor:让受检查异常回滚;即让本来不应该回滚的进行回滚操作。
  • noRollbackFor:忽略非检查异常;即让本来应该回滚的不进行回滚操作。

下面我们就@Transactional来 探讨一下:

7.1 @Transactional

7.1.1 @Transactional的注意事项

1.添加位置
接口实现类或接口实现方法上,而不是接口类中。


2.访问权限

public 的方法才起作用。

@Transactional 注解应该只被应用到 public 方法上,这是由 Spring AOP 的本质决定的。

3.系统设计

将标签放置在需要进行事务管理的方法上,而不是放在所有接口实现类上:只读的接口就不需要事务管理,由于配置了@Transactional就需要AOP拦截及事务的处理,可能影响系统性能。

4.错误使用

接口中A、B两个方法,A无@Transactional标签,B有,上层通过A间接调用B,此时事务不生效。

默认配置下,spring 只有在抛出的异常为运行时 unchecked 异常时才回滚该事务,也就是抛出的异常为RuntimeException 的子类(Errors也会导致事务回滚),而抛出 checked 异常则不会导致事务回滚 。
可通过 @Transactional rollbackFor进行配置。

多线程下事务管理因为线程不属于 spring 托管,故线程不能够默认使用 spring 的事务, 也不能获取spring 注入的 bean 。
在被 spring 声明式事务管理的方法内开启多线程,多线程内的方法不被事务控制。 一个使用了@Transactional 的方法,如果方法内包含多线程的使用,方法内部出现异常, 不会回滚线程中调用方法的事务。

7.1.2 @Transactional 实现原理

@Transactional 实质是使用了JDBC 的事务来进行事务控制的

1.事务开始时,通过AOP机制,生成一个代理connection对象,并将其放入 DataSource 实例的某个与 DataSourceTransactionManager 相关的某处容器中。

在接下来的整个事务中,客户代码都应该使用该 connection 连接数据库,执行所有数据库命令。
不使用该 connection 连接数据库执行的数据库命令,在本事务回滚的时候得不到回滚(物理连接connection逻辑上新建一个会话session;
DataSource 与 TransactionManager 配置相同的数据源)

2.事务结束时,回滚在第1步骤中得到的代理connection对象上执行的数据库命令,然后关闭该代理connection对象。

值得我们注意的是:事务结束后,回滚操作不会对已执行完毕的SQL操作命令起作用

7.1.3 事务的两种开启方式

  • 开启事务:start transaction | begin
  • 结束事务:commit | rollback
  • 关闭数据库中自动提交 autocommit set autocommit = 0;
  • MySQL 默认开启自动提交;通过手动提交或执行回滚操作来结束事务

关闭自动提交后,若事务一直未完成,即未手动执行 commit 或 rollback 时如何处理已经执行过的SQL操作?

C3P0 默认的策略是回滚任何未提交的事务

7.1.4 spring 事务特性

spring 所有的事务管理策略类都继承自

org.springframework.transaction.PlatformTransactionManager 接口

public interface PlatformTransactionManager {
  TransactionStatus getTransaction(TransactionDefinition definition)
  throws TransactionException;
  void commit(TransactionStatus status) throws TransactionException;
  void rollback(TransactionStatus status) throws TransactionException;
}

事务的隔离级别:是指若干个并发的事务之间的隔离程度

  1. @Transactional(isolation = Isolation.READ_UNCOMMITTED):读取未提交数据(会出现脏读, 不可重复读) 基本不使用
  2. @Transactional(isolation = Isolation.READ_COMMITTED):读取已提交数据(会出现不可重复读和幻读)
  3. @Transactional(isolation = Isolation.REPEATABLE_READ):可重复读(会出现幻读)
  4. @Transactional(isolation = Isolation.SERIALIZABLE):串行化

7.1.5 有哪些事务传播行为?

在TransactionDefinition接口中定义了七个事务传播行为:

1. TransactionDefinition.PROPAGATION_REQUIRED:
   如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。这是默认值。
 
2. TransactionDefinition.PROPAGATION_REQUIRES_NEW:
   创建一个新的事务,如果当前存在事务,则把当前事务挂起。
 
3. TransactionDefinition.PROPAGATION_SUPPORTS:
   如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。
 
4. TransactionDefinition.PROPAGATION_NOT_SUPPORTED:
   以非事务方式运行,如果当前存在事务,则把当前事务挂起。
 
5. TransactionDefinition.PROPAGATION_NEVER:
   以非事务方式运行,如果当前存在事务,则抛出异常。
 
6. TransactionDefinition.PROPAGATION_MANDATORY:
   如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。
 
7. TransactionDefinition.PROPAGATION_NESTED:
   如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;
   如果当前没有事务,则该取值等价于TransactionDefinition.PROPAGATION_REQUIRED。

PROPAGATION_NESTED 与PROPAGATION_REQUIRES_NEW的区别:

使用PROPAGATION_REQUIRES_NEW时,内层事务与外层事务是两个独立的事务。一旦内层事务进行了提交后,外层事务不能对其进行回滚。两个事务互不影响。

使用PROPAGATION_NESTED时,外层事务的回滚可以引起内层事务的回滚。而内层事务的异常并不会导致外层事务的回滚,它是一个真正的嵌套事务。

8.Spring怎么解决循环依赖的问题?

要解决这个问题,我们首先了解一下什么是循环依赖!

8.1 什么是循环依赖?

在这里插入图片描述

上图就很完美的诠释了循环依赖。
A依赖B,B依赖A,或者像C一样依赖自己本身。体现到代码层次就是这个样子:

@Component
public class A {
 // A中注入了B
 @Autowired
 private B b;
}
---
@Component
public class B {
 // B中注入了A
 @Autowired
 private A a;
}
---
// 自己依赖自己
@Component
public class C {
 // C中注入了C
 @Autowired
 private C c;
}

其次我们要了解第二个问题:什么情况下循环依赖可以被处理?

8.2 什么情况下循环依赖可以被处理

Spring解决循环依赖是有前置条件的:

  • 出现循环依赖的Bean必须要是单例(singleton),如果依赖prototype则完全不会有此需求。
  • 依赖注入的方式不能全是构造器注入的方式

第二点我们需要注意一下:不能全收构造器注入并不是意味着只能解决setter方法的循环依赖。

8.3 对象实例化

首先,Spring bean 的创建,其本质上还是一个对象的创建,既然是对象,一定要明白一点就是,一个完整的对象包含两部分:

  • 当前对象实例化
  • 对象属性的实例化

在Spring中,对象的实例化是通过反射实现的,而对象的属性则是在对象实例化之后通过一定的方式设置的。这个过程可以按照如下方式进行:
在这里插入图片描述

对于整体实例化对象的过程只要理解两点:

  • Spring是通过递归的方式获取目标bean及其所依赖的bean的;
  • Spring实例化一个bean的时候,是分两步进行的,首先实例化目标bean,然后为其注入属性。

结合这两点,也就是说,Spring在实例化一个bean的时候,是首先递归的实例化其所依赖的所有bean,直到某个bean没有依赖其他bean,此时就会将该实例返回,然后反递归的将获取到的bean设置为各个上层bean的属性的。

说完bean实例化,再看看一个对象的创建要经历哪些?

8.4 单例循环依赖的解决

一个对象一般创建过程有3部分组成:

  • createBeanInstance:实例化Bean,使用构造方法创建对象,为对象分配内存。
  • populateBean:进行依赖注入。
  • initializeBean:初始化Bean。

Spring为了解决单例的循环依赖问题,使用了三级缓存:

  • singletonObjects:完成了初始化的单例对象map
  • earlySingletonObjects:完成实例化未初始化的单例对象map
  • singletonFactories:单例对象工厂map,单例对象实例化完成之后会加入singletonFactories

说了这么多,循环依赖到底是怎么解决的呢?
Spring通过三级缓存解决了循环依赖,其中:

  • 一级缓存为单例池(singletonObjects)
  • 二级缓存为早期曝光对象earlySingletonObjects
  • 三级缓存为早期曝光对象工厂

当A、B两个类发生循环引用时,在A完成实例化后,就使用实例化后的对象去创建一个对象工厂,添加到三级缓存中,如果A被AOP代理,那么通过这个工厂获取到的就是A代理后的对象,如果A没有被AOP代理,那么这个工厂获取到的就是A实例化的对象。

当A进行属性注入时,会去创建B,同时B又依赖了A,所以创建B的同时又会去调用getBean(a)来获取需要的依赖,此时的getBean(a)会从缓存中获取:


第一步:先获取到三级缓存中的工厂;
第二步:调用对象工工厂的getObject方法来获取到对应的对象,得到这个对象后将其注入到B中。紧接着B会走完它的生命周期流程,包括初始化、后置处理器等。
第三步:当B创建完后,会将B再注入到A中,此时A再完成它的整个生命周期。至此,循环依赖结束!

上面提到了对象是否被AOP代理,那么普通的循环依赖和带AOP的依赖有什么不同吗?
普通循环依赖图
在这里插入图片描述

没有进行AOP的Bean间的循环依赖 从上图分析可以看出,这种情况下三级缓存根本没用,所以不存在什么提高了效率的说法。

带AOP循环依赖
带AOP的跟不带AOP的其实几乎一样,只是在三级缓存中存放的是函数式接口,在需要调用时直接返回代理对象。三级缓存存在的意义:


只有真正发生循环依赖的时候,才去提前生成代理对象,否则只会创建一个工厂并将其放入到三级缓存中,但是不会去通过这个工厂去真正创建对象

在这里插入图片描述

那么问题又来了,是否可以用二级缓存而不用三级缓存?


答案:不可以,违背Spring在结合AOP跟Bean的生命周期的设计!Spring结合AOP跟Bean的生命周期本身就是通过AnnotationAwareAspectJAutoProxyCreator这个后置处理器来完成的,在这个后置处理的postProcessAfterInitialization方法中对初始化后的Bean完成AOP代理。如果出现了循环依赖,那没有办法,只有给Bean先创建代理,但是没有出现循环依赖的情况下,设计之初就是让Bean在生命周期的「最后一步完成代理而不是在实例化后就立马完成代理」。

在这里插入图片描述

使用了三级缓存的情况下,A、B的创建流程
在这里插入图片描述

不使用三级缓存,直接在二级缓存中
在这里插入图片描述

结论:上面两个流程的唯一区别在于为A对象创建代理的时机不同,使用三级缓存的情况下为A创建代理的时机是在B中需要注入A的时候。
而不使用三级缓存的话在A实例化后就需要马上为A创建代理然后放入到二级缓存中去。三级缓存是无法提速的!

上面长篇大论这么多,总结起来就是:

如果要使用二级缓存解决循环依赖,意味着所有Bean在实例化后就要完成AOP代理,这样违背了Spring设计的原则,Spring在设计之初就是通过AnnotationAwareAspectJAutoProxyCreator这个后置处理器来在Bean生命周期的最后一步来完成AOP代理,而不是在实例化后就立马进行AOP代理。

好了,现在我们用简短的语言总结一下:
1.什么叫循环依赖

a类中有一个b属性,同时b类中又有一个a属性,两者之间属于一个相互依赖的关系

2.三级缓存的概念

上面内容也提到,Spring解决循环依赖的方法就是通过三级缓存来实现

  • 三级缓存:存放的是一个对象的半成品
  • 二级缓存:存放的是一个提前暴露出来的对象
  • 一级缓存:存放的是一个完全初始化的对象
    当我们构建a的时候,a会作为一个半成品放到三级缓存中,此时发现a中有一个b属性需要注入,这个时候就会去Spring容器中找有没有b属性,如果有b属性就直接拿出来;如果没有,这个时候就会再去初始化b类。在初始化b时,它和a一样,也要放到三级缓存中,但是呢发现初始化b时需要a属性,此时还是一样会去Spring容器中去找,会发现在三级缓存中有a,于是就把a从三级缓存提前暴露到二级缓存,交给b去做一个注入,这是a注入好了之后,b就可以从三级缓存放到一级缓存了。同时我们最开始创建b的原因就是a需要这个b,所以说,二级缓存中的a,就拿到了放在一级缓存里的b,这样,Spring就很完美的解决了循环依赖问题。

9.Spring 的单例 Bean 是否有线程安全问题?

当多个用户同时请求一个服务时,容器会给每一个请求分配一个线程,这时多个线程会并发执行该请求对应的业务逻辑,如果业务逻辑有对单例状态的修改(单例的成员属性),则必须考虑线程安全问题。

若每个线程中对全局变量、静态变量只有读操作,而无写操作,那么不会有线程安全问题;若有多个线程同时执行写操作,一般都需要考虑线程同步,否则就可能影响线程安全。

9.1 无状态Bean和有状态Bean

有实例变量的Bean,可以保存数据,是非线程安全的。

没有实例变量的对象。不能保存数据,是线程安全的。

在Spring中无状态的Bean适合用单例模式,这样可以共享实例提高性能。

有状态的Bean在多线程环境下不安全,可以用设置Bean为Prototype模式或者使用ThreadLocal解决线程安全问题。

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

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

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

2.工厂方法模式:FactoryBean就是典型的工厂方法模式。spring在使用getBean()调用获得该Bean时,会自动调用该Bean的getObject()方法。每个 Bean 都会对应一个 FactoryBean,如 SqlSessionFactory 对应 SqlSessionFactoryBean。

3、单例模式:一个类仅有一个实例。Spring 创建 Bean 实例默认是单例的。

4、适配器模式:SpringMVC中的适配器HandlerAdatper。由于应用会有多个Controller实现,如果需要直接调用Controller方法,那么需要先判断是由哪一个Controller处理请求,然后调用相应的方法。当增加新的 Controller,需要修改原来的逻辑,违反了开闭原则(对修改关闭,对扩展开放)。

为此,Spring提供了一个适配器接口,每一种 Controller 对应一种 HandlerAdapter 实现类,当请求过来,SpringMVC会调用getHandler()获取相应的Controller,然后获取该Controller对应的 HandlerAdapter,最后调用HandlerAdapter的handle()方法处理请求,实际上调用的是Controller的handleRequest()。每次添加新的 Controller 时,只需要增加一个适配器类就可以,无需修改原有的逻辑。源码如下:

// Determine handler for the current request.
mappedHandler = getHandler(processedRequest);

HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());

// Actually invoke the handler.
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

public class HttpRequestHandlerAdapter implements HandlerAdapter {

    @Override
    public boolean supports(Object handler) {//handler是被适配的对象,这里使用的是对象的适配器模式
        return (handler instanceof HttpRequestHandler);
    }

    @Override
    @Nullable
    public ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler)
        throws Exception {

        ((HttpRequestHandler) handler).handleRequest(request, response);
        return null;
    }
}

5、代理模式:spring 的 aop 使用了动态代理,有两种方式JdkDynamicAopProxy和Cglib2AopProxy。

6、模板模式:Spring 中 jdbcTemplate、hibernateTemplate 等使用了模板模式。

  • 0
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

ZNineSun

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值