IOC控制反转:究竟反转了什么

351 阅读4分钟

IOC控制反转的终极奥义

本文档配套视频:www.bilibili.com/video/BV1vx…

IOC的终极奥义:解耦。

对依赖创建的控制反转

先看反面例子:

public class TestService {
​
    public void save() {
        MySqlDao mysqlDao = new MySqlDao();
        mysqlDao.save();
    }
    
    //void update()...
}
​

Service中使用Dao中的方法操作数据库,这样写绝对线程安全,但难以扩展,比如要切换数据库类型为Progres,或者测试的时候传入一个模拟Dao(不会连接任何数据库),都需要修改Service类,违反了开闭原则。(讲解一下线程安全)

解决方案:依赖的对象的创建交给外部注入,这样就可以根据需要创建自己的DbDao。(讲解一下线程安全)

public class TestService {
​
​
    private DbDao Dbdao;
​
    public TestService(DbDao Dbdao) {
        this.Dbdao = Dbdao;
    }
    public void save() {
        Dbdao.save();
    }
​
    //通过setter方法注入
    /*public void setDbdao(DbDao Dbdao) {
        this.Dbdao = Dbdao;
    }*/
}
public class MySqlDao implements DbDao {
​
    public void save()
    {
        System.out.println("MySqlDao save user");
    }
}
​
public class ProgresDao implements DbDao {
​
    public void save()
    {
        System.out.println("ProgresDao save user");
    }
}
​
public class Application {
    public static void main(String[] args) {
        TestService testService = new TestService(new MySqlDao());
        testService.save();
​
        TestService testService1 = new TestService(new ProgresDao());
        testService1.save();
    }
}

控制反转的本质是将依赖的创建权和生命周期管理权从代码转移到外部容器。

注入的方式:构造函数注入(推荐,缺点是可能会有一个参数非常多的构造函数,可使用Lombok简化),setter注入(不推荐) , 字段注入(spring常见,通过反射)

IOC推荐的使用方式:

使用构造函数注入,并对字段使用final修饰,防止日后代码记不住了自己加一个setter方法导致原子性被破坏,逻辑混乱或线程安全问题

private final DbDao Dbdao;
​
public TestService(final DbDao Dbdao) {
    this.Dbdao = Dbdao;
}

Spring中的IOC通过注解强化了功能并简化了过程,不过Spring官方依然推荐使用构造函数进行注入

Spring实际应用中大多数的bean都是单例的,并且依赖关系在启动期就已经填充好,通常情况下是不会出现线程安全问题的,但是如果通过一些方法去修改bean实现的注入,依然存在线程安全问题。

对生命周期的控制反转

IoC 不仅管理依赖的创建,还控制对象的完整生命周期,包括:

  • 实例化:容器根据配置(如注解、XML)创建对象。
  • 依赖注入:将依赖对象注入到目标对象中。
  • 初始化:调用初始化方法(如@PostConstruct)。
  • 销毁:在对象不再需要时调用销毁方法(如@PreDestroy)。
维度传统编程IoC 模式
依赖创建代码主动new依赖对象容器根据配置创建并注入依赖
生命周期代码手动管理对象的创建、初始化、销毁容器自动管理对象的完整生命周期

示例(Spring 容器管理生命周期)

@Component
public class TestService {
    @Autowired
    private DbDao dbDao; // 依赖由容器注入
    
    @PostConstruct // 初始化回调
    public void init() {
        // 初始化逻辑
    }
    
    @PreDestroy // 销毁回调
    public void destroy() {
        // 清理资源
    }
}

Spring中使用set注入会存在改变状态导致线程安全问题的隐患,需要注意,如:

Controller:

@GetMapping("/{payType}/{amount}")
    public String pay(@PathVariable("payType") PayType payType, @PathVariable("amount") double amount) throws Exception{
        System.out.println(Thread.currentThread().getName() + payType.toString());
        Thread.sleep(1000);
        // 设置当前线程的支付方式
        payService.setPayType(payType);
        // 调用支付服务的支付方法
        payService.pay(amount);
        // 返回一个成功的消息
        return "支付成功";
    }

Service:

public class PayService {
    // 使用ThreadLocal来存储当前线程所选择的支付方式
    private ThreadLocal<PayType> payTypeThreadLocal = new ThreadLocal<>();
​
    // 使用@Autowired注解来自动注入支付策略工厂对象
    @Resource
    private PayStrategyFactory payStrategyFactory;
    private PayType payType;
    public static final String VAL = "abc";
    // 设置当前线程的支付方式
    public void setPayType(PayType payType) {
        payTypeThreadLocal.set(payType);
        //this.payType = payType;
    }
​
    // 获取当前线程的支付方式
    public PayType getPayType() {
        return payTypeThreadLocal.get();
        //return payType;
    }
​
    // 支付方法,根据当前线程的支付方式来调用对应的策略对象的支付方法
    public void pay(double amount) throws Exception{
        PayType payType = getPayType();
        PayStrategy payStrategy = payStrategyFactory.getPayStrategy(payType);
        // 打印当前线程的名称和选择的策略名称
        System.out.println(Thread.currentThread().getName() + "选择了" + payStrategy.getName() + amount + "元");
​
        payStrategy.pay(amount);
        //把ThreadLocal中的数据移除,否则...
        payTypeThreadLocal.remove();
    }
​
    public void pay(double amount,PayType payType) throws Exception{
        PayStrategy payStrategy = payStrategyFactory.getPayStrategy(payType);
        // 打印当前线程的名称和选择的策略名称
        System.out.println(Thread.currentThread().getName() + "选择了" + payStrategy.getName() + amount + "元");
​
        payStrategy.pay(amount);
        //把ThreadLocal中的数据移除,否则...
        payTypeThreadLocal.remove();
    }
​
}

上面的代码通过ThreadLocal解除了线程安全的隐患

在Mock测试中,使用构造器依然如此,通过构造器可以防止使用set方法导致bean的状态被改变的隐患:

@RunWith(SpringRunner.class)
@ContextConfiguration(classes = AppConfig.class)
public class TestServiceTest {
​
    @Mock
    private DbDao dbDao;
​
    private TestService testService;
​
    @Before
    public void setUp() {
        MockitoAnnotations.initMocks(this);
        testService = new TestService(dbDao);
    }
​
    @Test
    public void testSave() {
        testService.save();
        verify(dbDao).save();
    }
}

尽管使用构造器注入,可能会代码一个很多参数的构造器,但可以通过lombok解决:

@Service
@RequiredArgsConstructor
public class TestService {
​
    private final DbDao dbDao;
​
    
​
    public void save() {
        System.out.println("执行前置逻辑");
        dbDao.save();
        System.out.println("执行后置逻辑");
    }
}