- 控制反转(IoC):通过控制反转技术,实现了解耦合。对象给出它们的依赖,而不是创建或查找依赖的对象。
- 面向切面(AOP):Spring支持面向切面的编程,并将应用程序业务逻辑与系统服务分离。(AOP是对IoC功能的拓展)
IoC
1、能讲一下 IoC 的设计模式吗?
IoC(Inversion of Control:控制反转) ,也被称为依赖注入,是一种设计模式,而不是一个具体的技术实现。把创建对象的权力交给了框架,然后Spring通过反射就会创建一个对象。当我创建的一个类要用到这个对象,Spring就会把这个对象交给我。IoC 并非 Spring 特有,在其他语言中也有应用。
为什么叫控制反转?
- 控制:指的是对象创建(实例化、管理)的权力
- 反转:控制权交给外部环境(Spring 框架、IoC 容器)
将对象之间的相互依赖关系交给 IoC 容器来管理,并由 IoC 容器完成对象的注入。这样可以很大程度上简化应用的开发,把应用从复杂的依赖关系中解放出来。 IoC 容器就像是一个工厂一样,当我们需要创建一个对象的时候,只需要配置好配置文件/注解即可,完全不用考虑对象是如何被创建出来的。
在实际项目中一个 Service 类可能依赖了很多其他的类,假如我们需要实例化这个 Service,你可能要每次都要搞清这个 Service 所有底层类的构造函数,这可能会把人逼疯。如果利用 IoC 的话,你只需要配置好,然后在需要的地方引用就行了,这大大增加了项目的可维护性且降低了开发难度。
在 Spring 中, IoC 容器是 Spring 用来实现 IoC 的载体, IoC 容器实际上就是个 Map(key,value),Map 中存放的是各种对象。
Spring 时代我们一般通过 XML 文件来配置 Bean,后来开发人员觉得 XML 文件来配置不太好,于是 SpringBoot 注解配置就慢慢开始流行起来。
IoC模式的优点包括:
- 降低代码的耦合度,提高代码的可维护性和可扩展性。
- 提高代码的可测试性,因为依赖关系被解耦出来,可以更容易地进行单元测试。
- 提高代码的复用性,因为对象的创建和依赖关系的管理被封装在容器中,可以在不同的应用程序中重复使用。
- 提高代码的灵活性,因为容器可以动态地管理对象的创建和依赖关系,可以根据不同的需求进行配置和调整。
总之,IoC模式是一种非常有用的设计模式,可以帮助我们编写更加灵活、可维护和可扩展的代码。
补充:
DI(Dependency Injection)依赖注入,是 IoC 容器装配、注入对象的一种方式。
通过依赖注入机制,简单的配置即可注入需要的资源,完成自身的业务逻辑,不需要关心资源的出处和具体实现。
2、IoC原理
Spring中的IoC的实现原理就是工厂模式 + 反射机制。
3、什么是Spring Bean?
由 Spring IoC 容器管理的对象称为 Bean,Bean 根据 Spring 配置文件中的信息创建。
我们可以把 Spring IoC 容器看作是一个大工厂,Bean 相当于工厂的产品。如果希望这个大工厂生产和管理 Bean,就需要告诉容器需要哪些 Bean,以哪种方式装配。
我们需要告诉 IoC 容器帮助我们管理哪些对象,这个是通过配置元数据来定义的。配置元数据可以是 XML 文件、注解或者 Java 配置类。
<!-- Constructor-arg with 'value' attribute -->
<bean id="..." class="...">
<constructor-arg value="..."/>
</bean>
下图简单地展示了 IoC 容器如何使用配置元数据来管理对象。
org.springframework.beans 和 org.springframework.context 这两个包是 IoC 实现的基础
spring 提供了三种主要的方式来配置 IoC 容器中的 bean:
- 基于 XML 文件配置
- 基于注解配置
- 基于注解 + java 代码显式配置
4、将一个类声明为 Bean 的注解有哪些?
@Component:通用的注解,可标注任意类为 Spring 组件。如果一个 Bean 不知道属于哪个层,可以使用@Component 注解标注。@Repository:对应持久层即 Dao 层,主要用于数据库相关操作。@Service:对应服务层,主要涉及一些复杂的逻辑,需要用到 Dao 层。@Controller:对应 Spring MVC 控制层,主要用于接受用户请求并调用 Service 层返回数据给前端页面。
5、什么数据结构存储Spring给我们管理的bean?
Spring容器会将所有的Bean对象存储在一个名为BeanFactory的容器中。在BeanFactory中,Bean对象是以 “Bean名称-Bean实例” 的形式存储的,其中Bean名称就是一个字符串,Bean实例是一个Java对象。Spring中默认使用的是HashMap结构来存储Bean对象,其中Bean名称作为key,Bean实例作为value。所以可以说,Spring中存储Bean对象的数据结构就是一个HashMap,其中key存储的是Bean的名称,value存储的是Bean的实例。
6、@Component 和 @Bean 的区别是什么?
@Component注解作用于类,而@Bean注解作用于方法。@Component通常是通过类路径扫描来自动侦测以及自动装配到 Spring 容器中(我们可以使用@ComponentScan注解定义要扫描的路径从中找出标识了需要装配的类自动装配到 Spring 的 bean 容器中)。@Bean注解通常是我们在标有该注解的方法中定义产生这个 bean,@Bean告诉了 Spring 这是某个类的实例,当我需要用它的时候还给我。@Bean注解比@Component注解的自定义性更强,而且很多地方我们只能通过@Bean注解来注册 bean。比如当我们引用第三方库中的类需要装配到Spring容器时,则只能通过@Bean来实现。
@Bean注解使用示例:
@Configuration
public class AppConfig {
@Bean
public TransferService transferService() {
return new TransferServiceImpl();
}
}
上面的代码相当于下面的 xml 配置
<beans>
<bean id="transferService" class="com.acme.TransferServiceImpl"/>
</beans>
下面这个例子是通过 @Component 无法实现的。
@Bean
public OneService getService(status) {
case (status) {
when 1:
return new serviceImpl1();
when 2:
return new serviceImpl2();
when 3:
return new serviceImpl3();
}
}
7、注入 Bean 的注解有哪些?
Spring 内置的 @Autowired 以及 JDK 内置的 @Resource 和 @Inject 都可以用于注入 Bean。
| Annotaion | Package | Source |
|---|---|---|
| @Autowired | org.springframework.bean.factory | Spring 2.5+ |
| @Resource | javax.annotation | Java JSR-250 |
| @Inject | javax.inject | Java JSR-330 |
@Autowired 和 @Resource使用的比较多一些。
8、bean的几种注入(创建/实例化)方式
- 使用构造器创建Bean实例
- 使用静态工厂方法创建Bean
- 调用实例工厂方法创建Bean
1、使用构造器创建Bean实例
使用构造器来创建Bean实例是最常见的情况,如果不采用构造注入,Spring底层会调用Bean类的无参数构造器来创建实例,因此要求该Bean类提供无参数的构造器。
采用默认的构造器创建Bean实例,Spring对Bean实例的所有属性执行默认初始化,即所有的基本类型的值初始化为0或false;所有的引用类型的值初始化为null。
2、使用静态工厂方法创建Bean
使用静态工厂方法创建Bean实例时,class属性也必须指定,但此时class属性并不是指定Bean实例的实现类,而是静态工厂类,Spring通过该属性知道由哪个工厂类来创建Bean实例。
除此之外,还需要使用factory-method属性来指定静态工厂方法,Spring将调用静态工厂方法返回一个Bean实例,一旦获得了指定Bean实例,Spring后面的处理步骤与采用普通方法创建Bean实例完全一样。如果静态工厂方法需要参数,则使用<constructor-arg.../>元素指定静态工厂方法的参数。
3、调用实例工厂方法创建Bean
实例工厂方法与静态工厂方法只有一个不同:调用静态工厂方法只需使用工厂类即可,而调用实例工厂方法则需要工厂实例。使用实例工厂方法时,配置Bean实例的<bean.../>元素无须class属性,配置实例工厂方法使用factory-bean指定工厂实例。
采用实例工厂方法创建Bean的<bean.../>元素时需要指定如下两个属性:
- factory-bean: 该属性的值为工厂Bean的id。
- factory-method: 该属性指定实例工厂的工厂方法。
若调用实例工厂方法时需要传入参数,则使用<constructor-arg.../>元素确定参数值。
补充:BeanFactory是所有Spring Bean的容器根接口,其给IoC容器提供了一套完整的规范。FactoryBean是 一种创建Bean的方式,是对Bean的一种扩展。
9、@Autowired 和 @Resource 的区别是什么?
Autowired 属于 Spring 内置的注解,默认的注入方式为byType(根据类型进行匹配),也就是说会优先根据接口类型去匹配并注入 Bean (接口的实现类)。
这会有什么问题呢? 当一个接口存在多个实现类的话,byType这种方式就无法正确注入对象了,因为这个时候 Spring 会同时找到多个满足条件的选择,默认情况下它自己不知道选择哪一个。
这种情况下,注入方式会变为 byName(根据名称进行匹配),这个名称通常就是类名(首字母小写)。就比如说下面代码中的 smsService 就是我这里所说的名称,这样应该比较好理解了吧。
// smsService 就是我们上面所说的名称
@Autowired
private SmsService smsService;
举个例子,SmsService 接口有两个实现类: SmsServiceImpl1和 SmsServiceImpl2,且它们都已经被 Spring 容器所管理。
// 报错,byName 和 byType 都无法匹配到 bean
@Autowired
private SmsService smsService;
// 正确注入 SmsServiceImpl1 对象对应的 bean
@Autowired
private SmsService smsServiceImpl1;
// 正确注入 SmsServiceImpl1 对象对应的 bean
// smsServiceImpl1 就是我们上面所说的名称
@Autowired
@Qualifier(value = "smsServiceImpl1")
private SmsService smsService;
我们还是建议通过 @Qualifier 注解来显式指定名称而不是依赖变量的名称。
@Resource属于 JDK 提供的注解,默认注入方式为 byName。如果无法通过名称匹配到对应的 Bean 的话,注入方式会变为byType。
@Resource 有两个比较重要且日常开发常用的属性:name(名称)、type(类型)。
public @interface Resource {
String name() default "";
Class<?> type() default Object.class;
}
如果仅指定 name 属性则注入方式为byName,如果仅指定type属性则注入方式为byType,如果同时指定name 和type属性(不建议这么做)则注入方式为byType+byName。
// 报错,byName 和 byType 都无法匹配到 bean
@Resource
private SmsService smsService;
// 正确注入 SmsServiceImpl1 对象对应的 bean
@Resource
private SmsService smsServiceImpl1;
// 正确注入 SmsServiceImpl1 对象对应的 bean(比较推荐这种方式)
@Resource(name = "smsServiceImpl1")
private SmsService smsService;
简单总结一下:
@Autowired是 Spring 提供的注解,@Resource是 JDK 提供的注解。Autowired默认的注入方式为byType(根据类型进行匹配),@Resource默认注入方式为byName(根据名称进行匹配)。- 当一个接口存在多个实现类的情况下,
@Autowired和@Resource都需要通过名称才能正确匹配到对应的 Bean。Autowired可以通过@Qualifier注解来显式指定名称,@Resource可以通过name属性来显式指定名称。
10、Bean 的作用域有哪些?
Spring 中 Bean 的作用域通常有下面几种:
- singleton : IoC 容器中只有唯一的 bean 实例。Spring 中的 bean 默认都是单例的,是对单例设计模式的应用。
- prototype : 每次获取都会创建一个新的 bean 实例。也就是说,连续
getBean()两次,得到的是不同的 Bean 实例。 - request (仅 Web 应用可用): 每一次 HTTP 请求都会产生一个新的 bean(请求 bean),该 bean 仅在当前 HTTP request 内有效。
- session (仅 Web 应用可用) : 每一次来自新 session 的 HTTP 请求都会产生一个新的 bean(会话 bean),该 bean 仅在当前 HTTP session 内有效。
- application/global-session (仅 Web 应用可用):每个 Web 应用在启动时创建一个 Bean(应用 Bean),该 bean 仅在当前应用启动时间内有效。
- websocket (仅 Web 应用可用):每一次 WebSocket 会话产生一个新的 bean。
如何配置 bean 的作用域呢?
xml 方式:
<bean id="..." class="..." scope="singleton"></bean>
注解方式:
@Bean
@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public Person personPrototype() {
return new Person();
}
11、单例 Bean 的线程安全问题了解吗?
(Spring的bean默认都是单例的)
Spring框架中有一个@Scope注解,默认的值就是singleton。因为一般在Spring的bean中都是注入无状态的对象,没有线程安全问题,如果在bean中定义了可修改的成员变量,是要考虑线程安全问题的。
单例 Bean 存在线程问题,主要是因为当多个线程操作同一个对象的时候是存在资源竞争的。常见的有两种解决办法:
- 在 Bean 中尽量避免定义可变的成员变量。
- 在类中定义一个
ThreadLocal成员变量,将需要的可变成员变量保存在ThreadLocal中(推荐的一种方式)。
不过,大部分 Bean 实际都是无状态(没有实例变量)的(比如 Dao、Service),这种情况下, Bean 是线程安全的。
12、Bean 的生命周期了解么?
Spring Bean 生命周期大致分为4个阶段:
- 实例化,实例化该 Bean 对象
- 填充属性,给该 Bean 的全局变量字段 赋值
- 初始化
- 如果实现了 Aware 接口,会通过其接口获取容器资源
- 如果实现了 BeanPostProcessor 接口,则会回调该接口的前置和后置处理增强
- 如果配置了 init-method 方法,会执行该方法
- 销毁
- 如果实现了 DisposableBean 接口,则会回调该接口的 destroy 方法
- 如果配置了 destroy-method 方法,则会执行 destroy-method 配置的方法
13、Spring的工作流程
- 客户端发出请求,请求被DispatcherServlet接收;
- DispatcherServlet根据请求的URL,在配置文件中查找对应的处理器;
- DispatcherServlet将请求发送给处理器,处理器根据请求参数,调用业务逻辑层的服务;
- 业务逻辑层的服务处理完请求,将处理结果返回给处理器;
- 处理器将处理结果封装成ModelAndView对象,返回给DispatcherServlet;
- DispatcherServlet将ModelAndView对象传递给视图解析器,视图解析器根据ModelAndView对象中的信息,解析出对应的视图;
- 视图解析器将视图返回给DispatcherServlet;
- DispatcherServlet将视图返回给客户端,客户端收到响应。
AOP
1、谈谈你对Spring AOP的理解?
得分点:AOP概念、AOP作用、AOP的实现方式
标准回答:AOP(Aspect-Oriented Programming:面向切面编程)是一种编程思想,是通过 预编译方式 和 运行期动态代理 的方式实现(不修改源代码的情况下)程序动态统一添加功能的技术。
AOP技术利用一种称为“横切”的技术,剖解开封装对象的内部,将影响多个类的公共行为封装到一个可重用的模块中,并将其命名为切面。所谓的切面,简单来说,就是将那些与业务逻辑无关的公共功能(比如日志记录、事务处理等)分离出来,形成一个个独立的切面(Aspect),然后在程序运行时将这些切面动态地应用到业务逻辑中,达到增强程序功能、减少程序复杂度、提高程序可维护性的目的。
AOP可以有多种实现方式,而Spring AOP支持如下两种实现方式。
- JDK动态代理:这是Java提供的动态代理技术,可以 在运行时创建接口的代理实例 。Spring AOP默认采用这种方式,在接口的代理实例中织入代码。
- CGLib动态代理:采用底层的字节码技术,在运行时创建子类代理的实例。当目标对象不存在接口时,Spring AOP就会采用这种方式,在子类实例中织入代码。
加分回答:在应用场景方面,Spring AOP为IoC的使用提供了更多的便利,一方面,应用可以直接使用AOP的功能,设计应用的横切关注点,把跨越应用程序多个模块的功能抽象出来,并通过简单的AOP的使用,灵活地编制到模块中,比如可以通过AOP实现应用程序中的日志功能。另一方面,在Spring内部,例如事务处理之类的一些支持模块也是通过Spring AOP来实现的。 AOP不能增强的类: 1. Spring AOP只能对IoC容器中的Bean进行增强,对于不受容器管理的对象不能增强。 2. 由于CGLib采用动态创建子类的方式生成代理对象,所以不能对final修饰的类进行代理。
AOP是怎么实现的?
AOP就是基于动态代理的,如果要代理的对象,实现了某个接口,那么Spring AOP会使用JDK Proxy,去创建代理对象,而对于没有实现接口的对象,就无法使用 JDK Proxy去进行代理了,这时候Spring AOP会使用Cglib ,这时候Spring AOP会使用 Cglib 生成一个被代理对象的子类来作为代理。
2、AOP实现类
AOP实现类主要包括以下几个方面
| 术语 | 含义 |
|---|---|
| 通知(Advice) | 指的是具体要执行哪些切面逻辑,可以是在目标方法执行前、后或者异常抛出时执行 |
| 切点(Pointcut) | 用于定义哪些方法需要执行切面逻辑,可以基于方法的名称、注解、参数类型等进行定义 |
| 切面(Aspect) | 将通知和切点结合起来(通知+切点),指明在哪些切点上应该执行哪些通知。切面定义可以是一个单独的类,也可以是一个由通知和切点组成的类 |
| 连接点(Join Point) | 指的是在应用程序中执行的特定点,例如方法调用或异常抛出等。切点会将连接点和切面联系起来,形成一个完整的切面逻辑 |
| 织入(Weaving) | 指的是在目标类的代码中插入切面逻辑的过程,可以通过编译时织入、装载时织入和运行时织入等方式实现 |
| 目标(target) | 被通知的对象 |
| 代理(Proxy) | 向目标对象应用通知之后创建的代理对象 |
在Spring AOP中,可以通过编写通知类和切点表达式,以及在配置文件中定义切面来实现AOP。同时Spring也提供了多种通知类型,包括前置通知、后置通知、环绕通知、异常通知和引入通知等,开发人员可以根据具体需求选择适合自己的AOP实现方式和相关类。
3、AOP中有哪种设计模式?怎么实现的?
AOP(Aspect Oriented Programming,面向切面编程)是一种用于解决分散在多个组件中的横切关注点的问题,是一种程序设计范式。AOP中实现切面的方式有多种。其中,比较常用的方式是利用代理模式和装饰器模式。
- 代理模式:代理模式是指为另一个对象提供一个替身,以便控制它的访问。在AOP中,代理模式可以被用来实现一个横切关注点的代码注入。一个切面可以编写一个代理,拦截方法的调用,并执行特定的操作。代理模式可以被静态实现或动态实现。
- 静态代理:在编译时期已经确定好目标对象,在程序运行前代理类的class文件就已经生成。比如Spring AOP中的AspectJ。
- 动态代理:在程序运行时,动态生成代理对象。比如JDK自带的动态代理机制。
- 装饰器模式:装饰器模式是指在运行时给对象添加某些功能,而不需要修改原来的类。在AOP中,装饰器模式可以被用来实现一个横切关注点的代码注入。通过该模式,一个切面可以包装目标对象,拦截方法的调用,并在方法调用前后执行特定的操作。
通常,AOP的实现都需要借助于字节码操作或者反射技术来动态创建代理或者修改类的字节码,在目标方法执行前或者执行后,执行切面中编写的代码。在Java语言中,常见的AOP框架包括Spring AOP、AspectJ等,它们都是基于代理模式或者装饰器模式实现的。
4、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 快很多。
5、JDK代理和Cglib代理的区别?
- CGLib所创建的动态代理对象在实际运行时候的性能要比JDK动态代理高(1.6和1.7的时候,CGLib更快;1.8的时候,jdk更快)
- CGLib在创建对象的时候所花费的时间却比JDK动态代理多
- singleton的代理对象或者具有实例池的代理,因为无需频繁的创建代理对象,所以比较适合采用CGLib动态代理,反之,则适合用JDK动态代理
- JDK生成的代理类类型是Proxy(因为继承的是Proxy),CGLIB生成的代理类类型是Enhancer类型
- JDK动态代理是面向接口的,CGLib动态代理是通过字节码底层继承代理类来实现(如果被代理类被final关键字所修饰,那么会失败)
- 如果要被代理的对象是个实现类,那么Spring会使用JDK动态代理来完成操作(Spirng默认采用JDK动态代理实现机制);如果要被代理的对象不是实现类,那么Spring会强制使用CGLib来实现动态代理。
6、你的项目有没有使用AOP?(考察有没有真的用过aop)
常见的AOP使用场景:
- 记录操作日志
- 缓存处理
- Spring中内置的事务处理
我当时在后台管理系统中,就是使用AOP来记录了系统的操作日志
主要思路是这样的,使用aop中的环绕通知+切点表达式,这个表达式就是要找到要记录日志的方法,然后通过环绕通知的参数获取请求方法的参数,比如类信息、方法信息、注解、请求方式等,获取到这些参数以后,保存到数据库。
获取请求的用户名、请求方式、访问地址、模块名称、登录ip、操作时间,需要记录到数据库的日志表中
比如我们的用户请求来了以后,假如后台有4个请求的接口,分别是登录、新增用户、更新用户、删除用户。当有这些操作的时候,我们就需要记录日志了。
我们以新增用户为例:
我们可以使用AOP提供的环绕通知,在这里面去做一个切面。切面就相当于一个通用的代码,在这个保存用户、修改用户或者其他请求的时候,都会去执行的这个代码。
AOP把一些公共的代码给抽出来了。……(ps:我还是不太明白)