前言
知其然,更应知其所以然。
案例
我们先来看一个案例:有一个小伙,有一辆吉利车, 平常就开吉利车上班
代码实现:
public class GeelyCar { public void run(){ System.out.println("geely running"); } }
public class Boy { // 依赖GeelyCar private final GeelyCar geelyCar = new GeelyCar(); public void drive(){ geelyCar.run(); } }
有一天,小伙赚钱了,又买了辆红旗,想开新车。
简单,把依赖换成HongQiCar
代码实现:
public class HongQiCar { public void run(){ System.out.println("hongqi running"); } }
public class Boy { // 修改依赖为HongQiCar private final HongQiCar hongQiCar = new HongQiCar(); public void drive(){ hongQiCar.run(); } }
新车开腻了,又想换回老车,这时候,就会出现一个问题:这个代码一直在改来改去
很显然,这个案例违背了我们的依赖倒置原则(DIP):程序不应依赖于实现,而应依赖于抽象
优化
现在我们对代码进行如下优化:
img
Boy
依赖于Car
接口,而之前的GeelyCar
与HongQiCar
为Car
接口实现
代码实现:
定义出Car接口
public interface Car { void run(); }
将之前的GeelyCar
与HongQiCar
改为Car
的实现类
public class GeelyCar implements Car { @Override public void run(){ System.out.println("geely running"); } }
HongQiCar相同
Person此时依赖的为Car
接口
public class Boy { // 依赖于接口 private final Car car; public Boy(Car car){ this.car = car; } public void drive(){ car.run(); } }
此时小伙想换什么车开,就传入什么参数即可,代码不再发生变化。
局限性
以上案例改造后看起来确实没有什么毛病了,但还是存在一定的局限性,如果此时增加新的场景:
有一天小伙喝酒了没法开车,需要找个代驾。代驾并不关心他给哪个小伙开车,也不关心开的是什么车,小伙就突然成了个抽象,这时代码又要进行改动了,代驾依赖小伙的代码可能会长这个样子:
private final Boy boy = new YoungBoy(new HongQiCar());
随着系统的复杂度增加,这样的问题就会越来越多,越来越难以维护,那么我们应当如何解决这个问题呢?
思考
首先,我们可以肯定:使用依赖倒置原则是没有问题的,它在一定程度上解决了我们的问题。
我们觉得出问题的地方是在传入参数的过程:程序需要什么我们就传入什么,一但系统中出现多重依赖的类关系,这个传入的参数就会变得极其复杂。
或许我们可以把思路反转一下:我们有什么,程序就用什么!
当我们只实现HongQiCar
和YoungBoy
时,代驾就使用的是开着HongQiCar
的YoungBoy
!
当我们只实现GeelyCar
和OldBoy
时,代驾自然而然就改变成了开着GeelyCar
的OldBoy
!
这其实就是Spring的控制反转思想
而应该如何实现这个反转的需求呢?
需求分析
需求描述:我们有什么,程序就用什么。
分析:
- 程序怎么知道我们有什么?或者我们应该如果告知程序我们有什么?
- 程序怎么去使用?
借鉴生产消费模型看这个问题。
生产者把生产的物品放入容器中,消费中从容器中将物品取出。
我们是否也可以使用这样的模式?
我们将对象放入容器里,程序在容器里面取这个对象,有啥对象就用啥对象。
比如我们往容器里放HongQiCar
, 那程序就用HongQiCar
, 往容器里放GeelyCar
, 那程序就用GeelyCar
。
需求实现:
- 程序怎么知道我们有什么?
我们往一个容器里面放对象,放了什么就是什么。 - 程序怎么去使用?
程序从容器里面取对象,拿到什么就用什么。
简单的实现:
容器的选定:容器可以用Map,key为接口的名称,value为对应的实现。如:car:HongQiCar
代码:
public class SimpleMain { private static Map<String, Object> map = new ConcurrentHashMap<>(); public static void main(String[] args) { // 放 map.put("car", new HongQiCar()); // 使用 new Boy((Car) map.get("car")).drive(); } }
这样的实现在系统简单的时候还好,但是对象多了,那改起来就非常复杂了。
有没有一种方法,能让程序自己去寻找使用的对象,我们只给要用的对象打个标识。
比如这个注解@JsonIgnore
大家肯定都用过,它表示被标识的字段是否进行json序列化, 标识了就不进行序列化
@JsonIgnore private String name;
if(field.isAnnotationPresent(JsonIgnore.class)){ // 不序列化 }
显然这是一份通用的代码,我们只需要给字段上加
@JsonIgnore
注解就好
借鉴这个方法,我们同样可以给类加上注解,让程序去找寻项目里有该注解的类,有就自己把它put到Map中。
分析:
Q:给类加上注解,什么注解?
A:注解我们就用@Component
注解
Q:让程序去找寻项目里有该注解的类,怎么找?
A:指定一个包路径,让程序去扫描包下的类,判断类是否被@Component
注解标识
Q:有就自己把它put到Map中,怎么put?
A:通过反射将该类进行实例化,然后以类名为key, 实例为value,put到Map中
Q:如何扫描包下的类?
A:其实就是遍历文件目录
流程:
由于代码比较复杂,这里不做展示,见源码中:com.my.spring.auto.AutoMain
现在,我们可以随意的通过更换注解的方式,让程序使用不同的类,但是还剩下一个问题,就是这个
new Boy((Car) map.get("car")).drive();
回到案例的问题,此时加上代驾小哥,代码就会变成这个样子
Boy boy = map.get("boy"); boy.setCar(map.get("car")); new DaiJia(boy).drive();
还是挺复杂的,所以能不能让程序把这个组装过程也完成了呢?
相当于让程序自己帮我检测包下的类之间的依赖关系,并且帮我组装好,我最后只要这样写代码:
map.get("daijia").drive();
岂不美哉!
现在,让我们结合之前的需求做一个整体的分析
需求:扫描指定包下面的类,进行实例化,并根据依赖关系组合好
步骤分解:
扫描指定包下面的类 -> 如果这个类标识了Component注解(是个Bean) -> 把这个类的信息存起来
进行实例化 -> 遍历存好的类信息 -> 通过反射把这些类进行实例化
根据依赖关系组合 -> 解析类信息 -> 判断类中是否有需要进行依赖注入的字段 -> 对字段进行注入
Q:为什么现在需要把类存起来?之前的实现都是直接把类实例化后放到map里的。
A:这是因为要查找依赖关系,比如A类依赖了B类,那么在实例化A时,去哪里找B呢,一个办法是将包再扫描一次,但是这样效率太低了,所以只要在第一次扫描时将类都存起来,后面找的时候只要从map找就行了。
Q:怎么判断类中是否有需要进行依赖注入的字段?
A:还是用注解,这里用@Autowired
注解
为了更灵活,这里将扫描指定包下面的类也通过注解@ComponentScan
实现
流程:
方案实现
定义注解
首先我们需要定义出需要用到的注解:ComponentScan
,Component
,Autowired
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) public @interface ComponentScan { String basePackages() default ""; }
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) public @interface Component { String value() default ""; }
@Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) public @interface Autowired { }