👏 Hi! 我是 Yumuing,一个技术的敲钟人
👨💻 每天分享技术文章,永远做技术的朝拜者
📚 欢迎关注我的博客:Yumuing's blog
我们都知道,在使用Spring 框架时,常常遇到 Bean 概念,它其实是描述 Java 的软件组件模型,简单说,就是一个个组成一个可运行机器的小齿轮。而在 Spring 中存在一个专门管理这些齿轮的工厂,它是 IOC( Inversion of Control (IoC:控制反转)理论的实现,称为 DI(Dependency Injection:依赖注入),其中,IOC 是设计思想,DI 是实现方式,是同一个概念的不同角度描述。
在 Spring 框架中,DI 的具体实现类是称为容器,我们通过容器生成一个个小齿轮(Bean),从创建、实例化、销毁等等都交由容器去实现,进一步解耦合调用者与被调用者的代码,并且,去除了大量重复和无意义的代码,方便测试,利于功能复用。Spring 框架为了让用户更为简单的配置 Bean,使用了注解的形式进行配置,而无需去使用麻烦的 XML 进行一一对象配置。
概念性的说明就到这,更重要的是,我们去理解其运行原理,并实际使用它。在不使用 IOC 设计思想时,我们根据下图即可看出,由于某些情况下,以前的 A.class 被弃用了,经过修改变为 B.class ,此时调用 A.class 的对象都得修改代码,以适应变化,以防程序出错。当然,有人会说,那使用抽象接口不就行了,用什么 IOC 呢!其实,我们不能预料未来那些类会发生改变,只能尽可能做好抽象接口处理,总会有遗漏,况且,不是什么类都适合抽象接口,或者,如果每个类都适合并且也做了抽象接口的话,那将出现很多无意义的代码,变得臃肿。最重要的是,即使在存在抽象接口的情况下,你使用新的类,也必须对新的类进行实例化,这部分代码必须在调用者层面去修改,也就意味着调用双方还是得进行修改代码,重新编译源代码,不仅麻烦,还可能编译出错。
当能够使用 IOC 思想之后,就能完美解决,让容器工厂去创建、实例化对象,注入给调用者即可。即使同一个抽象接口下,需要指定哪一个优先注入给调用者也是无需修改调用者代码,除非,还需要使用旧对象的一些方法,其实也可以直接复制代码,也是无需修改调用者代码的,但如果,实在需要旧对象,也可在调用者处修改代码,在获取 Bean 时指定对象。
在 Spring 中,对象被大致分为四大类,与之相对应的也有四个注解,它们仅仅体现在语义上的不同,方便编写者阅读代码而已。当然,对于对象的属性也有相对应的注入注解,方便自定义对象初始属性。对应的注解说明如下:
IOC 思想在 Spring 框架中不仅仅体现在对象的创建、实例化上,还能够在对象销毁前自动化地为我们执行某些操作。在测试之前,我们先学会使用容器工厂。
当我们需要使用容器工厂时,就必须使其方法所属类继承 ApplicationContextAware 并实现 setApplicationContext 方法,其中,在 setApplicationContext 方法中必须设置一个属性对象来接受容器工厂类 ApplicationContext 。并且,为了保证与主配置类相同启动环境,test 类也得使用 ContextConfiguration(classes = xxxApplication.class 来配置环境。代码如下:
package top.yumuing.community;
import org.junit.jupiter.api.Test;
import org.springframework.beans.BeansException;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.test.context.ContextConfiguration;
@SpringBootTest
@ContextConfiguration(classes = CommunityApplication.class)
class CommunityApplicationTests implements ApplicationContextAware {
private ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
}
在方法前一行添加 @PostConstruct 即可 Bean 实例化后第一时间执行该方法,添加 @PreDestroy 即可在 Bean 销毁前最后执行。以上均为 Spring 框架的容器工厂自动执行,无需额外代码。测试类为 TestBeanManagement,代码如下:
package top.yumuing.community.test;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import org.springframework.stereotype.Component;
@Component
public class TestBeanManagement {
public TestBeanManagement(){
System.out.println("实例化");
}
@PostConstruct
public void init(){
System.out.println("初始化");
}
@PreDestroy
public void destroy(){
System.out.println("销毁了");
}
}
测试方法如下:
package top.yumuing.community;
import org.junit.jupiter.api.Test;
import org.springframework.beans.BeansException;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.test.context.ContextConfiguration;
@SpringBootTest
@ContextConfiguration(classes = CommunityApplication.class)
class CommunityApplicationTests implements ApplicationContextAware {
private ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
@Test
public void testBean(){
TestBeanManagement testBeanManagement = applicationContext.getBean(TestBeanManagement.class);
System.out.println(testBeanManagement);
}
}
有时,我们使用了某些 jar 包里的对象,需要被注入时,又没办法直接在 jar 里增加注解怎么办?毕竟,jar 包可能加密,或者是需要调用 SDK 中的对象,源代码不可获取。此时,可以采用配置类的方式,通过自行实例化对象,再注册成 Bean,进而通过容器工厂进行调用,无需增加注解在 jar 包里。这种方法是我们主动去获取 Bean ,还是显得较为麻烦,后续有说明,在 Spring 框架下,正确的依赖注入方式,主动获取的方法代码如下:
TestConfig 为配置 jar 包内的 SimpleDateFormat 注册成 Bean 的类,CommunityApplicationTests 为调用该注册为 Bean 的 SimpleDateFormat 类。当然,实际项目目录会按业务、数据库、控制三级平级创建service、dao、controller三个目录,为了方便演示,简化为 test 目录,目录结构如下:
TestConfig.java 代码如下:
package top.yumuing.community.test;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.text.SimpleDateFormat;
@Configuration
public class TestConfig {
@Bean
public SimpleDateFormat simpleDateFormat(){
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
}
}
CommunityApplicationTests.java 代码如下:
package top.yumuing.community;
import org.junit.jupiter.api.Test;
import org.springframework.beans.BeansException;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.test.context.ContextConfiguration;
import java.text.SimpleDateFormat;
import java.util.Date;
@SpringBootTest
@ContextConfiguration(classes = CommunityApplication.class)
class CommunityApplicationTests implements ApplicationContextAware {
private ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
@Test
public void testBeanConfig(){
SimpleDateFormat simpleDateFormat = applicationContext.getBean("simpleDateFormat",SimpleDateFormat.class);
System.out.println(simpleDateFormat.format(new Date()));
}
}
当然,这是我们利用容器工厂的特性进行主动获取 Bean 的方式,明显违背了 IOC 思想,我们将再次在相同的环境下,使用 IOC 思想实现的依赖注入方式解决问题。代码如下:
package top.yumuing.community;
import org.junit.jupiter.api.Test;
import org.springframework.beans.BeansException;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.test.context.ContextConfiguration;
import java.text.SimpleDateFormat;
import java.util.Date;
@SpringBootTest
@ContextConfiguration(classes = CommunityApplication.class)
class CommunityApplicationTests implements ApplicationContextAware {
private ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
@Autowired
public SimpleDateFormat simpleDateFormat;
@Test
public void testDI(){
System.out.println(simpleDateFormat.format(new Date()));
}
}
如果想要指定继承了某个接口的对象,不仅仅需要使用 @Autowired 进行属性注入,还要配合 @Qualifier("Bean 名") 来指定。这次我们模拟实际项目代码,由 controller 调用 service,再由 service 调用 dao 的方式实现。目录结构如下:其中 TestDaoOne 与 TestDaoTwo 继承 TestDao 接口。
TestDao 代码如下:
package top.yumuing.community.test;
public interface TestDao {
String select();
}
TestDaoOne 代码如下:
package top.yumuing.community.test;
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Repository;
@Repository
@Primary
public class TestDaoOne implements TestDao{
@Override
public String select() {
return "one";
}
}
TestDaoTwo 代码如下:
package top.yumuing.community.test;
import org.springframework.stereotype.Repository;
@Repository("testDaoTwo")
public class TestDaoTwo implements TestDao{
@Override
public String select() {
return "two";
}
}
TestService 代码如下:
package top.yumuing.community.test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;
@Service
public class TestService {
@Autowired
public TestDao testDaoOne;
public String getDateOne(){
return testDaoOne.select();
}
@Autowired
@Qualifier("testDaoTwo")
public TestDao testDaoTwo;
public String getDateTwo(){
return testDaoTwo.select();
}
}
HelloController 代码如下:
package top.yumuing.community.test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
@RequestMapping("/yumuing")
public class HelloController {
@RequestMapping("/hello")
@ResponseBody
public String hello(){
return "hello";
}
@Autowired
public TestService testService;
@RequestMapping("/dateOne")
@ResponseBody
public String oneDate(){
return testService.getDateOne();
}
@RequestMapping("/dateTwo")
@ResponseBody
public String twoDate(){
return testService.getDateTwo();
}
}
通过以上的了解,我们就可以简单的知道,何为 IOC ,它又帮助我们减轻了什么,并且该怎么去运用它。除此之外,也简单明白了在Spring 框架中进行测试的方法,以及依赖注入的相关注解。想要获得全部源代码,可见 认识 Spring IOC 及其应用