从依赖倒置原则看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
273 0
|
XML 前端开发 Java
从依赖倒置原则看Spring(二)
从依赖倒置原则看Spring
254 0
|
设计模式 Java Spring
【设计模式】依赖倒置原则与工厂方法模式与spring
【设计模式】依赖倒置原则与工厂方法模式与spring
235 0
|
8月前
|
Java Spring 容器
SpringBoot自动配置的原理是什么?
Spring Boot自动配置核心在于@EnableAutoConfiguration注解,它通过@Import导入配置选择器,加载META-INF/spring.factories中定义的自动配置类。这些类根据@Conditional系列注解判断是否生效。但Spring Boot 3.0后已弃用spring.factories,改用新格式的.imports文件进行配置。
1198 0
|
9月前
|
人工智能 Java 测试技术
Spring Boot 集成 JUnit 单元测试
本文介绍了在Spring Boot中使用JUnit 5进行单元测试的常用方法与技巧,包括添加依赖、编写测试类、使用@SpringBootTest参数、自动装配测试模块(如JSON、MVC、WebFlux、JDBC等),以及@MockBean和@SpyBean的应用。内容实用,适合Java开发者参考学习。
977 0
|
5月前
|
JavaScript Java Maven
【SpringBoot(二)】带你认识Yaml配置文件类型、SpringMVC的资源访问路径 和 静态资源配置的原理!
SpringBoot专栏第二章,从本章开始正式进入SpringBoot的WEB阶段开发,本章先带你认识yaml配置文件和资源的路径配置原理,以方便在后面的文章中打下基础
489 4
|
5月前
|
Java 测试技术 数据库连接
【SpringBoot(四)】还不懂文件上传?JUnit使用?本文带你了解SpringBoot的文件上传、异常处理、组件注入等知识!并且带你领悟JUnit单元测试的使用!
Spring专栏第四章,本文带你上手 SpringBoot 的文件上传、异常处理、组件注入等功能 并且为你演示Junit5的基础上手体验
1034 3
|
12月前
|
前端开发 Java 数据库
微服务——SpringBoot使用归纳——Spring Boot集成Thymeleaf模板引擎——Thymeleaf 介绍
本课介绍Spring Boot集成Thymeleaf模板引擎。Thymeleaf是一款现代服务器端Java模板引擎,支持Web和独立环境,可实现自然模板开发,便于团队协作。与传统JSP不同,Thymeleaf模板可以直接在浏览器中打开,方便前端人员查看静态原型。通过在HTML标签中添加扩展属性(如`th:text`),Thymeleaf能够在服务运行时动态替换内容,展示数据库中的数据,同时兼容静态页面展示,为开发带来灵活性和便利性。
490 0
|
12月前
|
XML Java 数据库连接
微服务——SpringBoot使用归纳——Spring Boot集成MyBatis——基于 xml 的整合
本教程介绍了基于XML的MyBatis整合方式。首先在`application.yml`中配置XML路径,如`classpath:mapper/*.xml`,然后创建`UserMapper.xml`文件定义SQL映射,包括`resultMap`和查询语句。通过设置`namespace`关联Mapper接口,实现如`getUserByName`的方法。Controller层调用Service完成测试,访问`/getUserByName/{name}`即可返回用户信息。为简化Mapper扫描,推荐在Spring Boot启动类用`@MapperScan`注解指定包路径避免逐个添加`@Mapper`
699 0
|
12月前
|
Java 测试技术 微服务
微服务——SpringBoot使用归纳——Spring Boot中的项目属性配置——少量配置信息的情形
本课主要讲解Spring Boot项目中的属性配置方法。在实际开发中,测试与生产环境的配置往往不同,因此不应将配置信息硬编码在代码中,而应使用配置文件管理,如`application.yml`。例如,在微服务架构下,可通过配置文件设置调用其他服务的地址(如订单服务端口8002),并利用`@Value`注解在代码中读取这些配置值。这种方式使项目更灵活,便于后续修改和维护。
266 0