Spring+MyBaits+SpringBoot面试突击

Spring 相关


Spring 一般就问那几个问题,至于源码部分,这里是不做要求的,如果遇到问源码,就认栽吧,资料层面这里没有推荐,看文章学习吧,这里有几篇还不错的文章

Spring本质系列(1) — 依赖注入

Spring本质系列(2) — AOP

Spring,你为何中止我的事务?

“上帝视角”图解Spring事务的传播机制原理

正文

1、AOP 原理


Spring 的 AOP 是用动态代理实现的。如果我们为 Spring 的某个 bean 配置了切面,那么 Spring 在创建这个 Bean 时会直接创建这个 Bean 的代理对象。后续对 Bean 方法的调用,实际调用的是代理类重写的代理方法。Spring 的 AOP 采用了两种方法实现动态代理,分别是 JDK 动态代理和 CGLib 动态代理。

其中 Spring 默认使用 JDK 的动态代理实现 AOP,前提条件是类要实现了某个借口,然后借助 reflect 包下的 Proxy 类和 InvocationHandler 接口来动态生成代理对象。当我们通过代理对象调用方法时,底层将通过反射,去调用我们实现的代理方法。

正式由于 JDK 的动态代理存在类必须实现接口的限制,Spring 在其他情况下会使用 CGLib 动态代理来代理对象。CGLib 实现动态代理的原理是,底层采用了ASM 字节码生成框架,直接对需要代理的类的字节码进行操作,生成这个类的一个子类,并重写类中所有可以重写的方法,在重写的过程中,将我们定义的额外的逻辑织入到方法中,对方法进行增强。

两种方式各有优劣,JDK 动态代理是 JDK 原生的,不需要任何依赖即可使用。缺点是目标类必须实现了某个接口,才能用 JDK 动态代理。CGLib 动态代理不需要目标类必须实现接口,侵入性小。但 CGLib 是通过继承的方式,生成一个目标类的子类作为代理类,因此假如目标类是 final 类也无法使用 CGLib 代理。并且 CGLib 实现代理方法的方式是重写目标类的方法,所以无法对 final,private 方法进行代理。

2、 AOP 用到了 Java 中的哪些技术 ?


AOP 在使用时经常用注解来标记切面和切点,首先就用到了注解。

其次由于 AOP 使用动态代理实现的,Spring 又用两种方式实现动态代理,一种是 JDK 动态代理,这种方式用到了 Java 的反射。另一种是 CGLib 动态代理,主要是通过继承生成目标类的子类作为代理类。

还有,用到的设计模式也很多,比如代理模式实现动态代理,观察者模式实现通知功能,策略模式实现多种通知类型等等。

3、 IOC AOP 说说自己的看法


IOC 全称是 inversion of control,也就是控制反转。它的核心思想在于,资源不由使用资源的双方管理,而是由不使用资源的第三方管理,也就是 IOC 容器。在 Java 中,以前我们要使用某个对象,大多数时候都需要我们自己去 new 一个出来,而有了 Spring 的 IOC 只后,Bean 都交给了 IOC 容器统一管理,我们只需要在任何需要的时候从容器中获取就行。

IOC 使用时有很多优点,比如实现资源的统一配置和管理,降低了使用资源双方的耦合度。

AOP 是一种面向切面编程的思想,也就是通过将系统分解成不同的切面,从而使系统中不同的切面可以独立地进行开发和维护。

(可以结合自己项目中的运用说)就比如说项目中的日志系统,我当然把记录日志的操作写在要记录日志的每个方法中,但是这也太臃肿和难以维护了,每添加一个方法都要重新写记录日志的方法,想要更改记录日志的逻辑,所有的方法都要改。更进一步,我不仅想要对每个方法进行日志记录,还要对每个方法进行安全检测等等,那更麻烦了。

为了解决这些问题,我们可以使用设计模式中的模板模式或者装饰者模式,但它们或多或少都有缺陷,比如不够灵活,代码隔离性不强等。

这个时候不妨通过一种方法,将这些日志记录,安全监测等逻辑与实际的业务代码隔离开,将刚才那些逻辑写在一个个切面里,最后在程序运行时动态织入这些切面,并自动生成融合了原类和增强逻辑的代理类来完成功能。

总的来说,AOP 的主要解决的就是”非业务代码的抽取“问题,利用 AOP 可以很方便在某些方法前后增加一些功能,让开发者更专注于业务逻辑开发。

4、 Spring 中 Bean 的生命周期(Bean 的加载步骤)


对于 Spring 的 Bean 来说,整个生命周期可以分为四个阶段:实例化,属性注入,初始化和销毁。

具体的,Bean 在实例化之前其实还有一些预热操作,Spring 在启动时需扫描所有被 XML 或注解配置的 Bean,然后将这些 Bean 的元信息封装成 BeanDefination,通过 BeanDefinationReader 将它们以 key-value 的形式注册到 BeanDefinationRegistry 中,BeanDefinationRegistry 本质上是一个 Map。接着 Spring 会遍历这个 Map,可选择的执行 BeanFactoryPostProcessor 来对这些 BeanDefination 做最后的修改和替换,到此为 Bean 实例化预热的阶段结束。在真正的 Bean 实例化阶段,Spring 一般是通过反射的方式,选择合适的构造器来把对象创建出来,接着对其进行属性注入,属性注入完就进行 Bean 的初始化。在初始化阶段首先需要判断这个 Bean 是否实现了 Aware 相关的接口,如果存在就填空相关的资源。比如通常会制作一个工具类 ApplicationContextUtil 类,去实现 ApplicationContextAware 接口,在容器启动阶段获取到具体的 ApplicationContext 实例,进而通过它来获取 Bean。Aware 接口处理完之后接着执行 BeanPostProcessor 后置处理器,它是 AOP 实现的关键,具体有 before 和 after 两个方法,在这中间,也就是 BeanPostProcessor 的 before 方法执行后,接着会执行 Bean init 相关的方法,其中官方文档中定义的执行顺序为 @PostConstruct,实现了 InitializingBean 接口以及 init-method 方法。其中 @PostConstruct 方法经常用到,比如在 Bean 实例化之后立马去 MySQL 拿一些数据啥的。之后接着执行 after 方法,到此为止一个 Bean 就初始化完成,可以拿去用了。需要销毁的时候,需要先看看有没有定义具体的 destroy 方法,执行就行。

需要补充一点,就是如果某些 Bean 被配置成懒加载的模式,那么只有我们在需要它时才会对其进行真正的实例化。如果没有选择懒加载模式,在容器启动完之后,其中有一个步骤 finishBeanFactoryInitialization( ) 来启动这些 Bean 的实例化阶段,通过隐式调用所有依赖对象的 getBean( ) 来实例化这些 Bean,完成 Bean 加载。

实例化和初始化是两个完全不同的过程,实例化只是给 Bean 分配了内存空间,初始化则是实际的为 Bean 添加相应的业务代码。

5、 Spring 的常用注解


用于创建对象对象的注解 @Component,把当前类对象放入 Spring 容器中。为了更清晰有提供了更具体的 @Controller,@Service,@Repository 分别用于控制层,业务层和持久层。

用于注入数据的注解 @Autowired 和 @Resource,前者默认按照类型注入,后者是默认按照 Bean 的名称注入。

用于改变作用范围的注解 @Scope,来指定 Bean 的作用范围。

还有和 Bean 生命周期相关的注解 @PreDestory,@PostConstruct。前者用于指定 Bean 的销毁方法,后者用于指定 Bean 的初始化方法。

其它的还有 @Configuration 指定配置类,@ComponentScan 指定创建容器时要扫描的包,@PropertySource 指定 properties 文件位置等等。

6、Spring 注入的几种方式


Spring 中常见的一般有三种依赖注入方式:基于字段注入,基于 Setter 方式注入,基于构造器注入。

基于字段注入,就是直接在 Bean 的变量上使用注解进行依赖注入,本质上是通过反射的方式直接注入到字段中,这在平时的代码中很常见,但是 Spring 官方是不推荐这种方式的。优点是代码看起来简洁,通俗易懂,只需要把 @Autowired 扔在变量上就行了,不需要特殊的构造器或 Setter 方法。但这样就使字段注入的类变得越来越难以维护,难以理清依赖项之间的关系。并且所注入的字段不能是 final 的,编译器会直接报错,这种情况只能使用构造器注入。

基于 Setter 方法注入,就是在对应变量的 Set 方法上使用注解进行依赖注入。优点是完全符合类单一职责原则,每个 Setter 只针对一个对象,缺点是仍然不能注入 final 对象,并且由于 Setter 方法的操作,意味着被注入的对象可能随时会改变。

基于构造器注入是目前比较推荐的注入方式,它可以注入 final 对象,也不会像 Setter 注入那样会导致被注入对象被修改,并且由于依赖对象是在构造方法中执行的,在真正使用时能确保依赖对象完全初始化完成了。

除了上面三种,别的还有基于方法注入,接口回调注入等等。

7、 Spring 装配 Bean 的几种方式


Spring 中装配 Bean 的方式常见的有三种,在 XML 中配置,在 Java 代码中使用注解和隐式的 Bean 组件扫描和自动装配。

Spring 早期都是通过 XML 文件来配置 Bean 的,在 XML 文件中利用各种标签来描述一个 Bean。这样的好处在于对 Bean 的修改只需去改相应的配置文件,无需重新编译代码,重新启动程序就能加载最新的配置。缺点也有,就是配置起来有点繁琐。

Java 5 之后引入了注解,Spring 在此之后就可以利用注解来简化 XML 的配置,比如生命一个配置类用 @Configuration,声明一个 Bean 可以用 @Bean 注解,同时也可以用 name 属性来指定 Bean 的名字。

再往后 Spring 提供了 @ComponentScan 和 @Autowired 两个注解,一个用来扫描应用上下文中所创建的 Bean,一个用来自动装配 Bean。具体的,当需要声明一个 Bean 时,只需要在对应的类上加上 @Service,@Controller 等等注解,Spring 上下文就可以在启动时发现这些 Bean。当需要注入 Bean 时,可以使用 @Autowired 注解在字段,方法或者构造器上,方便的完成注入。

这些配置 Bean 的方式都是可以相互搭配的,不过一般情况下若注解与 XML 同时使用,XML 的优先级要高于注解。因为这样可以对 Bean 做修改后不用重新编译整个工程,效率很高。

8、 如何使用 IOC


Spring 提供了两种 IOC 容器类型,分别为 BeanFactory 和 ApplicationContext。

BeanFactory 只提供了最基本的依赖注入,而 ApplicationContext 作为 BeanFactory 的子接口,还提供了更高级的功能比如:资源访问,事务传播,国际化等等特性,通常使用 ApplicationContext 居多。

使用的话,可以通过 XML 文件配置 Bean,然后使用 ClassPathXmlApplicationContext 类读取,然后从容器中 getBean 即可。也可以使用注解搭配 @ComponentScan,需要使用到 AnnotationConfigApplicationContext 容器,对所需 Bean 进行注册,然后调用 refresh( ) 启动容器并进行初始化和装配工作。

例:

// 使用 XML 文件配置
public class Main {
    public static void main(String[] args) {
        // 读取 XML 文件
        ApplicationContext context = new ClassPathXmlApplicationContext("person.xml");

        // 获取 Bean
        Person person = (Person) context.getBean("person");

        // 使用 person 对象进行操作
        // ...
    }
}
// 使用注解

// 一个配置类
@Configuration
@ComponentScan(basePackages = "com.example.package")
public class AppConfig {
    // ...
}

// 一个 Component(Service) 类
@Component
public class UserService {
    // ...
}

// main
public class Main {
    public static void main(String[] args) {

        // 使用 AppConfig 配置类来初始化 Spring 应用上下文
        ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);

        // 获取 Bean
        UserService userService = context.getBean(UserService.class);

        // 使用 userService 对象进行操作
        // ...
    }
}

9、 通常如何获取 Bean


在早期使用 XML 配置文件的时候,一般都是先获得 BeanFactory,再从中获取 getBean( )。

不过现在都推荐使用 ApplicationContext,在项目启动时获取 ApplicationContext 对象并将其封装到一个工具类中,使用时直接从工具类中 getBean。

除此之外,还可以通过继承 BeanFactoryAware 或 ApplicationContextAware 接口,在容器初始化时就会调用 setBeanFactoryAware 或 setApplicationContextAware 方法设置将对应的实例。与上面直接使用 BeanFactory 或 ApplicationContext 不同的是,Aware 接口可以减少 Bean 与容器的耦合度,Bean 不需要知道容器的具体类型,只需要知道如何与它们交互即可,而且这种方式可以在获取容器引用前后执行某些自定义操作。

再有的话就是使用 Spring 基于注解的自动装配,为 Bean 类加上 @Component 注解,表示将它交由 IOC 容器管理,在需要使用的地方使用 @Autowired 或 @Resource 进行自动装配。当然还带在启动类上加上 @ComponentScan 注解,能让 Spring 找到带有 @Component 注解的类。

10、 Bean 的扫描路径


Spring 主要通过 @ComponentScan 的 basePackageClasses 属性来指定 Bean 的扫描路径,也可以指定 includeFilters 和 excludeFilters 来包含和过滤带有特定注解的类。

11、 AutoWired 是按照什么规则来获取 Bean 的 (Resource 呢?)


Autowired 默认按照“类型”进行注入,如果 IOC 容器中存在两个及以上的相同类型的 Bean 时,就会根据 Bean 的名称进行注入,如果没有指定名称,就会直接报错。当然也可以设置 Autowired 的 required 属性,为 false 时若找不到就会直接忽略,不会报错。

Resource 有两个 name 和 type 两个属性,如果都不指定默认是按照 byName 方式进行查找,指定的话就查找指定的 Bean。

12、 Spring 如何解决循环依赖


Spring 中的循环依赖就是说,两个或多个 Bean 之间相互依赖,比如 A 依赖 B,同时 B 也依赖 A,此时就会形成一个循环引用的情况,会导致应用程序启动失败。

Spring 为了应对这种情况,引入了三级缓存,分别是用于存储完整对象的 singletonObjects,存储半成品对象的 earlySingletonObjects,存储对象的创建工厂的 singletonFactories。具体的使用在 Spring 对 Bean 进行实例化之后,属性注入之前,也就是对 A 进行实例化之后,会立马通过 A 创建一个工厂放在三级缓存。等到 A 属性注入时,发现了依赖 B,此时又开始实例化 B,但在 B 的属性注入时又需要获取 A,这里是从三级缓存中拿出来刚放进去的 A 工厂,通过它来获取 A,接着将三级缓存中的 A 工厂给删除,把还未进行完属性注入的半成品 A 对象放在二级缓存中,接着继续进行 B 的属性注入和初始化,B 初始化完成后再回过头来对 A 继续进行别的属性注入和初始化,等到 A 也初始化完成时,会将存储在二级缓存中的 A 删除掉,将初始化好的 A 放在一级缓存中,结束。我们自己去 getBean 时,实际上拿到的就是一级缓存中存储的 Bean。

需要补充的是,Spring 只能解决“单例对象”且 A 必须是通过“ setter 注入”产生的循环依赖问题。因为只有单例对象的创建和初始化只执行一次,在容器运行时,单例对象的属性依赖关系不会改变,因此可以通过提前暴露半成品对象的方式来解决循环依赖。类似的,如果是使用“构造器”注入,由于对象在实例化过程中最先调用构造器方法,此时还未完成实例化的 Bean 是无法提前暴露的,就会导致 A 创建时需要创建 B,B 创建时需要创建 A 的死锁问题,这种情况就会直接报错。

12.5、为什么要用三级缓存,二级缓存不行?


如果没有三级缓存,并且我们对 A 进行了 AOP 代理,那么就必须在 A 实例化之后立马完成 AOP 代理产生代理对象,这样才能在实例化 B 时,B 能正确拿到代理之后的 A 对象。但这样违背了 Spring 的设计原则,Spring 原先是在对 Bean 初始化过程中通过 BeanPostProcessor 后置处理器来完成 AOP 代理,而不是在实例化之后就立马进行 AOP 代理。

所以三级缓存的存在就是为了应对 Bean 需要被 AOP 代理的情况,而二级缓存则是为了性能,从三级缓存中的工厂创建出对象,再扔到二级缓存,就避免了每次都要从工厂中拿。

13、BeanFactory 和 FactoryBean 的区别?


BeanFactory 是一个顶层的工厂接口,定义了一些 IOC 容器最基本的功能,比如如何对 Bean 进行实例化和配置,建立 Bean 之间的依赖关系等等。ApplicationContext 就实现了 BeanFactory 接口并在此基础上扩展了资源访问,事件传播等功能。

FactoryBean 是一个 Bean 接口,但它的设计类似于工厂模式,能够根据自定义的 getBean( ) 产生或修饰其它 Bean 实例,通常帮助实现一些复杂的 Bean 初始化逻辑。比如典型的应用场景就是实现了 FactoryBean 的 ProxyFactoryBean,它的作用就是用来生成 Bean 的 AOP 代理对象,在对原始 Bean 的基础上额外织入了一些方法等等。

14、 SpringBoot 自动配置怎么实现的?


SpringBoot 的自动配置是指 springboot 能根据一些规则来自动配置应用程序所需的 Bean,服务和其他组件等等。原理就是 @SpringBootApplication 注解中包含的 @EnableAutoConfiguration 注解实现的。

@EnableAutoConfiguration 注解又包含了 @AutoConfigurationPackage 并且导入了一个 ImportSelector 类。@AutoConfigurationPackage 中又导入了一个 Register 类。整体上看,是先由 Register 类将应用程序类所在的包以及子包下的组件导入,然后再由下层注解的 ImportSelector 类负责获取所有默认的配置类,加载所有 jar 包下的 classpath 路径下 META-INF/spring.factories 文件中给出的所有自动配置类,完成整体的自动配置。

不过还需要注意的是,在加载自动配置类时,并不是一股脑的加载 spring.factories 中的全部配置,而是会通过 @Conditional 类注解的属性注解过滤一下,根据条件决定是否加载。

15、 Spring 事务管理怎么实现的?


Spring 总体上提供了”声明式事务”和”编程式事务”两种事务管理方式。

编程式事务就是使用硬编码的方式,使用 Spring 中提供的事务相关的类来控制事务。而编程式事务通常又有两种用法,一种是使用 PlatfromTransactionmanager,一种是使用 TransactionTemplate 来控制事务。PlatformTransactionManager 是最原始的方式,代码有点大,它是一个 Spring 事务管理的核心接口,往下有很多具体的数据源如JDBC,Hibernate 等等都有相对应的实现类。其中我们最常用的就是 JdbcTemplate。在使用时,我们一般利用配置的 jdbc 数据源初始化一个 PlatformTransactionManager,然后定义事务的属性例如事务隔离级别,事务传播方式等等到 TransactionDefinition 中,最后调用 getTransaction( ) 开启一个事务,返回一个 TransactionStatus,这个对象用于确定当前事务的对台,比如最终是要提交 commit 还是回滚 rollback 等等。getTransaction( ) 内部简单来说就是创建一个全局共享的 ThreadLocal 对象用来存储当前线程的 datasource 到 connection 的映射,然后就建立一个 DB 连接,设置是否自动提交事务,最后将其放到 ThreadLocal 对象中。第二种方式就是使用 TransactionTemplate,它在 PlatformTransactionManager 的基础上使用模板方法进行了一些封装,主要省去了提交或者回滚事务的代码。一般是使用它的 execute( ),并传递一个 TransactionCallback 对象,在里面做业务操作。

与编程式事务不同,Spring 的声明式事务是建立在 AOP 机制上的,其本质是对目标方法的前后进行拦截,在方法开始前创建或加入一个事务,等到方法执行完时根据情况选择提交事务或者回滚事务。(AOP 的具体实现等等补充)

16、 Spring 哪里用到了反射?


在 Spring IOC 容器模块,创建 Bean 的实例和依赖注入都使用到了反射。具体的,创建 Bean 实例时会通过类加载器,根据类路径所在路径获取到类型为 Class 的实例,然后通过 newInstance( ) 创建对象。而 newInstance( ) 的执行过程,就是先通过反射获取到类的构造方法信息,然后执行默认的构造方法生成实例。利用构造器注入和 setter 注入时也类似,先通过反射获取到当前类的所有方法信息或构造方法信息,然后通过 invoke 调用 setter( ) 或通过反射获取到的构造器进行带参数的实例创建。对于设置了 @Autowired 的字段注入,则需要在获取到类的所有字段信息后,判断字段是否有 @Autowired 注解,然后通过反射设置字段的所依赖的 Bean。

在 Spring AOP 模块,Spring 可以在运行期,动态的利用反射为被代理类生成代理对象,并将其包装成一个切面,从而实现对被代理对象的增强操作。

更多的 Spring 可以看这里:0. Spring 面试题阅读指南

MyBatis

可以先将就看下面这些:0. MyBatis面试题阅读指南,这块一般问的不怎么难,主要考察你的实操,大家平时做项目用到的一些操作,要大致记住一下,特别是一些注解,然后有个大致的印象就可以了。

SpringBoot

和mybatis同理,可以先将就看下面这些:SpringBoot面试题阅读指南

发表评论

后才能评论