目录
[一、Spring Ioc](#一、Spring Ioc)
[什么是 IOC?](#什么是 IOC?)
[1. 构造方法注入](#1. 构造方法注入)
[2. Setter方法注入](#2. Setter方法注入)
[3. 字段/属性注入](#3. 字段/属性注入)
[4. 方法注入](#4. 方法注入)
[5. 接口注入](#5. 接口注入)
[6. 注解注入](#6. 注解注入)
[二、Spring Aop](#二、Spring Aop)
[什么是 AOP?](#什么是 AOP?)
[AOP 为什么叫面向切面编程?](#AOP 为什么叫面向切面编程?)
[AOP 解决了什么问题?](#AOP 解决了什么问题?)
[AOP 的应用场景有哪些?](#AOP 的应用场景有哪些?)
[AOP 实现方式有哪些?](#AOP 实现方式有哪些?)
[Spring AOP和Aspect有什么区别?](#Spring AOP和Aspect有什么区别?)
Spring 正如其名字,给开发者带来了春天,Spring 是为解决企业级应用开发的复杂性而设计的一款框架,其设计理念就是:简化开发。
Spring 框架中最核心思想就是:
- IOC(控制反转):即转移创建对象的控制权,将创建对象的控制权从开发者转移到了 Spring 框架。
- AOP(切面编程):将公共行为(如记录日志,权限校验等)封装到可重用的模块中,而使原本的模块内只需关注自身的个性化行为。
本文,将主要介绍 Spring 中 IOC 的依赖注入。
一、Spring Ioc
什么是 IOC?
就 IOC 本身而言,并不是什么新技术,IoC(Inversion of Control控制反转) 是一种设计思想,而不是一个具体的技术实现。是一种是面向对象编程中的一种设计原则,用来减低计算机代码之间的耦合度。
IoC 的思想就是借助于"第三方"实现具有依赖关系的对象之间的解耦。换句话说就是,将原本在程序中手动创建对象的控制权,交由 Spring 框架来管理。不过, IoC 并非 Spring 特有,在其他语言中也有应用。
依赖倒置原则
假设我们设计一辆汽车:先设计轮子,然后根据轮子大小设计底盘,接着根据底盘设计车身,最后根据车身设计好整个汽车。这里就出现了一个"依赖"关系:汽车依赖车身,车身依赖底盘,底盘依赖轮子。
这样的设计看起来没问题,但是可维护性却很低。假设设计完工之后,上司却突然说根据市场需求的变动,要我们把车子的轮子设计都改大一码。这下就很麻烦了:因为我们是根据轮子的尺寸设计的底盘,轮子的尺寸一改,底盘的设计就得修改;同样因为我们是根据底盘设计的车身,那么车身也得改,同理汽车设计也得改------整个设计几乎都得改!
由此我们可以看到,仅仅是为了修改轮胎的构造函数,这种设计却需要修改整个上层所有类的构造函数!在软件工程中,这样的设计几乎是不可维护的------在实际工程项目中,有的类可能会是几千个类的底层,如果每次修改这个类,我们都要修改所有以它作为依赖的类,那软件的维护成本就太高了。
我们现在换一种思路。
我们先设计汽车的大概样子,然后根据汽车的样子来设计车身,根据车身来设计底盘,最后根据底盘来设计轮子。这时候,依赖关系就倒置过来了:轮子依赖底盘, 底盘依赖车身, 车身依赖汽车。
这时候,上司再说要改动轮子的设计,我们就只需要改动轮子的设计,而不需要动底盘,车身,汽车的设计了。
这就是依赖倒置原则------把原本的高层建筑依赖底层建筑"倒置"过来,变成底层建筑依赖高层建筑。高层建筑决定需要什么,底层去实现这样的需求,但是高层并不用管底层是怎么实现的。这样就不会出现前面的"牵一发动全身"的情况。
所以我们需要进行控制反转(IoC),即**上层控制下层,而不是下层控制着上层。我们用依赖注入(Dependency Injection)这种方式来实现控制反转。**所谓依赖注入,就是把底层类作为参数传入上层类,实现上层类对下层类的"控制"。
控制反转(Inversion of Control)就是依赖倒置原则的一种代码设计的思路。具体采用的方法就是所谓的依赖注入(Dependency Injection)。
这几种概念的关系大概如下:
为什么叫控制反转?
- 控制:指的是对象创建(实例化、管理)的权力
- 反转:控制权交给外部环境(Spring 框架、IoC 容器)
将对象之间的相互依赖关系交给 IoC 容器来管理,并由 IoC 容器完成对象的注入。这样可以很大程度上简化应用的开发,把应用从复杂的依赖关系中解放出来。 IoC 容器就像是一个工厂一样,当我们需要创建一个对象的时候,只需要配置好配置文件/注解即可,完全不用考虑对象是如何被创建出来的。
在实际项目中一个 Service 类可能依赖了很多其他的类,假如我们需要实例化这个 Service,你可能要每次都要搞清这个 Service 所有底层类的构造函数,这可能会把人逼疯。如果利用 IoC 的话,你只需要配置好,然后在需要的地方引用就行了,这大大增加了项目的可维护性且降低了开发难度。
在 Spring 中, IoC 容器是Spring框架的核心组件之一,是 Spring 用来实现 IoC 的载体,它负责管理和容纳应用程序中的所有对象,并提供依赖注入的功能。IoC 容器实际上就是个 Map(key,value),Map 中存放的是各种对象。
Spring提供了多个IOC容器的实现,如常用的BeanFactory和ApplicationContext。容器从配置文件(如XML、注解或JavaConfig)中读取配置信息,实例化被容器管理的Bean,并通过DI将所需的依赖注入到Bean中。
Spring 时代我们一般通过 XML 文件来配置 Bean,后来开发人员觉得 XML 文件来配置不太好,于是 SpringBoot 注解配置就慢慢开始流行起来。
好处:
- 对象之间的耦合度或者说依赖程度降低。
- 资源变的容易管理;比如你用 Spring 容器提供的话很容易就可以实现一个单例。
两种实现方式
IOC 的核心就是原先创建一个对象,我们需要自己直接通过 new 来创建,而 IOC 就相当于有人帮们创建好了对象,需要使用的时候直接去拿就行。
IOC 主要有两种实现方式:
- DL(Dependency Lookup)依赖查找:容器帮我们创建好了对象,我们需要使用的时候自己再主动去容器中查找。
- DI(Dependency Inject):依赖注入:相比较依赖查找又是一种优化,也就是我们不需要自己去查找,只需要告诉容器当前需要注入的对象,容器就会自动将创建好的对象进行注入(赋值)。
依赖注入DI
对于spring配置一个bean时,如果需要给该bean提供一些初始化参数,则需要通过依赖注入方式,所谓的依赖注入就是通过spring将bean所需要的一些参数传递到bean实例对象的过程。
依赖注入是IoC的一种具体实现方式,依赖注入是指通过注入的方式将一个对象的依赖关系交给容器来管理。
以前是开发者自己在代码中通过new操作符创建对象并手动管理对象之间的关系,而现在通过DI的方式,可以通过配置文件或注解的方式告诉容器需要创建那些对象以及它们之间的关系,从而实现解耦和便于维护的效果。
它允许我们通过构造器、setter方法或字段注入等方式,将依赖对象直接注入到需要它们的组件中,而不是组件自己去创建依赖对象。
Spring有哪些注入方式?
通过 xml 的注入方式不做讨论。
Spring注入有以下几种方式:
- 构造方法注入
- Setter方法注入
- 字段注入
- 接口注入
- 方法注入
- 注解注入
1. 构造方法注入
当两个类属于强关联时,我们也可以通过构造方法实现依赖注入。在类的构造方法中使用@Autowired注解注入需要的依赖类。构造器注入是Spring推荐的依赖注入方式,适用于必需的依赖。
这种方式的注入是指带有参数的构造函数注入,看下面的例子,我创建了一个成员变量myDependency,但是并未设置对象的set方法。这里的注入方式是在SpringAction的构造函数中注入,也就是说在创建SpringAction对象时要将MyDependency参数值传进来。
java
@Component
public class MyService {
private final MyDependency myDependency;
@Autowired
public MyService(MyDependency myDependency) {
this.myDependency = myDependency;
}
}
2. Setter方法注入
通过setter方法实现依赖注入。在类中定义对应的setter方法,并使用@Autowired注解注入需要的依赖类。
Setter注入适用于可选的依赖。通过Setter方法,我们可以在Spring容器中注入依赖项。
看下面例子,假设有一个MyService,类中需要实例化一个myDependency对象,那么就可以定义一个private的myDependency成员变量,然后创建MyService的set方法(这是ioc的注入入口)。
java
@Component
public class MyService {
private MyDependency myDependency;
@Autowired
public void setMyDependency(MyDependency myDependency) {
this.myDependency = myDependency;
}
}
3. 字段/属性注入
通过属性注入的方式非常常用,这个应该是大家比较熟悉的一种方式,直接在类中定义依赖类的字段,并使用@Autowired注解注入需要的依赖类。
Spring不推荐使用字段注入,因为它可能会隐藏必需的字段,这些字段本应在构造器中被赋值。因为字段注入隐藏了类的依赖关系,这使得类的构造函数不够清晰,难以看出类的必要依赖项。
其次构造器注入允许你在编译时就确定所有必要的依赖都已经正确设置。而字段注入运行时才设置依赖可能会导致由于缺少必要的依赖而导致的问题,这些问题在编译阶段无法发现。
java
@Component
public class MyService {
@Autowired
private MyDependency myDependency;
}
4. 方法注入
在这种方式中,依赖关系通过方法参数进行注入。它通常用于特定方法的依赖的场景。
java
@Component
public class MyService {
public void performAction(@Autowired MyDependency myDependency) {
myDependency.doSomething();
}
}
5. 接口注入
接口注入是一种较少使用的依赖注入方式。在这种方式中,依赖关系通过接口方法来注入,将需要注入的依赖类定义为接口类型,在类中使用@Autowired注解注入实现该接口的类。
在接口注入中,首先定义一个接口,该接口包含一个或多个用于设置依赖关系的方法。然后,具体的实现类实现这个接口,并在实现中处理依赖关系的注入。
java
public interface DependencyAware {
void setDependency(Dependency dependency);
}
@Component
public class MyService implements DependencyAware {
private Dependency dependency;
@Override
public void setDependency(Dependency dependency) {
this.dependency = dependency;
}
}
接口定义了依赖关系的设置方法,使得依赖关系变得显式,而不是隐式的构造器或字段注入。并且可以根据不同的实现类提供不同的依赖,增强了代码的可扩展性。
尽管在Spring中,构造器注入和注解注入(如@Autowired
)更为常用,但接口注入在某些特定场景下可以提供更高的灵活性和可测试性。
6. 注解注入
注解注入是Spring中最常用的依赖注入方式之一。通过使用特定的注解,Spring可以自动处理依赖关系的注入。
注解注入是Spring中最常用和推荐的方式,提供了简洁和自动化的配置,Spring会自动扫描注解并进行依赖注入,无需手动配置,减少了开发者的负担。通过注解,开发者可以轻松管理依赖关系,提高代码的可读性和可维护性。
最常用的注解是@Autowired
,它可以用于构造器、setter方法或字段上。除了@Autowired注解,还可以使用@Inject、@Resource等注解,实现依赖注入。选择哪种注解要根据具体情况来决定。通常在Spring项目中,@Autowired
和@Inject
比较常用,而@Resource
主要用于资源相关的注入。
(1)@Autowired
是Spring框架提供的注解,用于自动注入依赖。Spring会根据类型自动查找并注入所需的依赖对象。
- 使用场景:可以用在构造器、setter方法和字段上。
- 特性:
- 默认是按类型注入,如果找到多个候选者,则会抛出异常。
- 可以使用
@Qualifier
注解指定具体的实现类。 - 允许设置
required
属性,如果为false
,则在未找到匹配的依赖时不会抛出异常。
java
@Component
public class MyService{
private final Dependency dependency;
@Autowired
public MyService(Dependency dependency) {
this.dependency = dependency;
}
}
(2)@Inject
是Java规范(JSR-330)中的注解,Spring支持它。它与@Autowired
具有类似的功能,用于进行依赖注入。
- 使用场景:可以用在构造器、setter方法和字段上。
- 特性 :
- 默认按类型注入,与
@Autowired
类似。 - 不支持
required
属性,必须有相应的依赖,否则会抛出异常。 - 更加通用,可以与其他依赖注入框架(如Guice)兼容。
- 默认按类型注入,与
java
@Component
public class MyService {
@Inject
private Dependency dependency;
}
(3)@Resource
是Java EE规范中的注解,通常用于进行资源注入。它可以注入Bean或其他资源(如JDBC数据源等)。
- 使用场景:可以用在字段或setter方法上。
- 特性 :
- 默认按名称注入,先查找与属性名称相同的Bean,如果找不到,再按类型查找。
- 不需要像
@Autowired
那样结合使用@Qualifier
来指定具体的Bean。
java
@Component
public class MyService {
@Resource
private Dependency dependency;
}
Spring框架提供了多种依赖注入方式,包括构造器注入、Setter方法注入、接口注入、自动装配、注解注入和Java配置等。不同的注入方式适用于不同的场景,开发人员可以根据具体需求选择合适的方式。依赖注入使得应用程序的组件之间解耦合,提高了代码的可测试性、可维护性和可扩展性。通过灵活使用Spring框架提供的依赖注入方式,可以更加方便地管理和维护应用程序的依赖关系,从而提升开发效率和代码质量。
总的来说,Spring的依赖注入机制可以灵活地应对不同种类的依赖注入需求,提高了代码的可维护性和可重用性。
二、Spring Aop
什么是 AOP?
AOP(Aspect Oriented Programming)即面向切面编程,AOP 是 OOP(面向对象编程)的一种延续,二者互补,并不对立。
AOP 的目的是将横切关注点(如日志记录、事务管理、权限控制、接口限流、接口幂等等)从核心业务逻辑中分离出来,通过动态代理、字节码操作等技术,实现代码的复用和解耦,提高代码的可维护性和可扩展性。OOP 的目的是将业务逻辑按照对象的属性和行为进行封装,通过类、对象、继承、多态等概念,实现代码的模块化和层次化(也能实现代码的复用),提高代码的可读性和可维护性。
AOP 为什么叫面向切面编程?
AOP 之所以叫面向切面编程,是因为它的核心思想就是将横切关注点从核心业务逻辑中分离出来,形成一个个的切面(Aspect)。
示例:
AOP 解决了什么问题?
OOP 不能很好地处理一些分散在多个类或对象中的公共行为(如日志记录、事务管理、权限控制、接口限流、接口幂等等),这些行为通常被称为横切关注点(cross-cutting concerns) 。如果我们在每个类或对象中都重复实现这些行为,那么会导致代码的冗余、复杂和难以维护。
AOP 可以将横切关注点(如日志记录、事务管理、权限控制、接口限流、接口幂等等)从**核心业务逻辑(core concerns,核心关注点)**中分离出来,实现关注点的分离。
AOP 的应用场景有哪些?
- 日志记录:自定义日志记录注解,利用 AOP,一行代码即可实现日志记录。
- 性能统计:利用 AOP 在目标方法的执行前后统计方法的执行时间,方便优化和分析。
- 事务管理:
@Transactional
注解可以让 Spring 为我们进行事务管理比如回滚异常操作,免去了重复的事务管理逻辑。@Transactional
注解就是基于 AOP 实现的。 - 权限控制:利用 AOP 在目标方法执行前判断用户是否具备所需要的权限,如果具备,就执行目标方法,否则就不执行。例如,SpringSecurity 利用
@PreAuthorize
注解一行代码即可自定义权限校验。 - 接口限流:利用 AOP 在目标方法执行前通过具体的限流算法和实现对请求进行限流处理。
- 缓存管理:利用 AOP 在目标方法执行前后进行缓存的读取和更新。
- ......
AOP 实现方式有哪些?
AOP 的常见实现方式有动态代理、字节码操作等方式。
- Spring AOP 就是基于动态代理
- 如果要代理的对象,实现了某个接口,那么 Spring AOP 会使用JDK Proxy,去创建代理对象。
- 而对于没有实现接口的对象,就无法使用 JDK Proxy 去进行代理了,这时候 Spring AOP 会使用 Cglib 生成一个被代理对象的子类来作为代理。
- 当然你也可以使用AspectJ !Spring AOP 已经集成了 AspectJ ,AspectJ 应该算的上是 Java 生态系统中最完整的 AOP 框架了。
- AspectJ 相比于 Spring AOP 功能更加强大,但是 Spring AOP 相对来说更简单。
- 如果我们的切面比较少,那么两者性能差异不大。但是,当切面太多的话,最好选择 AspectJ ,它比 Spring AOP 快很多。
Spring AOP和Aspect有什么区别?
SpringAOP:
1)主要通过动态代理实现切面。对于实现了接口的类,它使用JDK动态代理;对于没有实现接口的类,则使用CGLIB字节码生成技术。
2)运行时织入:
- 在运行时动态地将切面逻辑编织到目标对象的方法调用中。
- 当Spring容器初始化时,它会根据配置创建一个代理对象,这个代理对象包装了目标对象,并在方法调用前后执行切面逻辑。
Aspect:
1)是静态代理。是一个易用的功能强大的AOP框架,属于编译时增强,可以单独使用,也可以整合到其它框架中,是AOP编程的完全解决方案。
2)属于静态织入,通过修改代码来实现,在实际运行之前就完成了织入,所以说它生成的类是没有额外运行时开销的。
一般有如下几个织入的时机:
- 编译期织入:指在编译源代码时将切面(Aspect)逻辑编织到目标类中。通常需要使用Aspect的编译器(AJC)来完成
- 编译后织入:指在源代码已经编译成.class文件之后,通过工具修改这些字节码文件来添加切面逻辑。这种织入方式通常用于已有代码的增强。
- 类加载后织入:指在类被加载到Jvm 之前,通过JavaAgent来修改类的字节码。这种方式允许在运行时动态地增强类的行为。
3)Aspect提供完整的AOP解决方案,像SpringAOP只支持方法级别的织入,而Aspect支持字段、方法、构造函数等等,所以它更加强大,当然也更加复杂。
三、总结
控制反转是一种在软件工程中解耦合的思想,调用类只依赖接口,而不依赖具体的实现类,减少了耦合。控制权交给了容器,在运行的时候才由容器决定将具体的实现动态的"注入"到调用类的对象中。
依赖注入是一种设计模式,可以作为控制反转的一种实现方式。依赖注入就是将实例变量传入到一个对象中去(Dependency injection means giving an object its instance variables)。
通过IoC框架,类A依赖类B的强耦合关系可以在运行时通过容器建立,也就是说把创建B实例的工作移交给容器,类A只管使用就可以。