从依赖倒置原则看Spring(一)

简介: 从依赖倒置原则看Spring

前言

知其然,更应知其所以然。

案例

我们先来看一个案例:有一个小伙,有一辆吉利车, 平常就开吉利车上班

代码实现:

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接口,而之前的GeelyCarHongQiCarCar接口实现

代码实现:

定义出Car接口

public interface Car {
    void run();
}

将之前的GeelyCarHongQiCar改为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());

随着系统的复杂度增加,这样的问题就会越来越多,越来越难以维护,那么我们应当如何解决这个问题呢?

思考

首先,我们可以肯定:使用依赖倒置原则是没有问题的,它在一定程度上解决了我们的问题。

我们觉得出问题的地方是在传入参数的过程:程序需要什么我们就传入什么,一但系统中出现多重依赖的类关系,这个传入的参数就会变得极其复杂。

或许我们可以把思路反转一下:我们有什么,程序就用什么!

当我们只实现HongQiCarYoungBoy时,代驾就使用的是开着HongQiCarYoungBoy

当我们只实现GeelyCarOldBoy时,代驾自然而然就改变成了开着GeelyCarOldBoy

这其实就是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 {
}


目录
相关文章
|
设计模式 Java Spring
【设计模式】依赖倒置原则与工厂方法模式与spring
【设计模式】依赖倒置原则与工厂方法模式与spring
101 0
|
缓存 Java Spring
从依赖倒置原则看Spring(三)
从依赖倒置原则看Spring
159 0
|
XML 前端开发 Java
从依赖倒置原则看Spring(二)
从依赖倒置原则看Spring
142 0
|
8天前
|
SQL 监控 druid
springboot-druid数据源的配置方式及配置后台监控-自定义和导入stater(推荐-简单方便使用)两种方式配置druid数据源
这篇文章介绍了如何在Spring Boot项目中配置和监控Druid数据源,包括自定义配置和使用Spring Boot Starter两种方法。
|
2月前
|
缓存 Java Maven
Java本地高性能缓存实践问题之SpringBoot中引入Caffeine作为缓存库的问题如何解决
Java本地高性能缓存实践问题之SpringBoot中引入Caffeine作为缓存库的问题如何解决
|
3月前
|
Java 测试技术 数据库
Spring Boot中的项目属性配置
本节课主要讲解了 Spring Boot 中如何在业务代码中读取相关配置,包括单一配置和多个配置项,在微服务中,这种情况非常常见,往往会有很多其他微服务需要调用,所以封装一个配置类来接收这些配置是个很好的处理方式。除此之外,例如数据库相关的连接参数等等,也可以放到一个配置类中,其他遇到类似的场景,都可以这么处理。最后介绍了开发环境和生产环境配置的快速切换方式,省去了项目部署时,诸多配置信息的修改。
|
3月前
|
Java 应用服务中间件 开发者
Java面试题:解释Spring Boot的优势及其自动配置原理
Java面试题:解释Spring Boot的优势及其自动配置原理
101 0
|
8天前
|
XML Java 关系型数据库
springboot 集成 mybatis-plus 代码生成器
本文介绍了如何在Spring Boot项目中集成MyBatis-Plus代码生成器,包括导入相关依赖坐标、配置快速代码生成器以及自定义代码生成器模板的步骤和代码示例,旨在提高开发效率,快速生成Entity、Mapper、Mapper XML、Service、Controller等代码。
springboot 集成 mybatis-plus 代码生成器
|
16天前
|
Java 应用服务中间件 开发者
深入探索并实践Spring Boot框架
深入探索并实践Spring Boot框架
27 2
|
8天前
|
前端开发 Java Spring
springboot自定义拦截器的简单使用和一个小例子
本文介绍了如何在Spring Boot中创建和使用自定义拦截器,通过一个登录验证的示例,演示了拦截器在MVC流程中的preHandle、postHandle和afterCompletion三个环节的作用,并说明了如何在Spring Boot配置类中注册拦截器。
下一篇
无影云桌面