JavaEE--SpringIoC
目录
一、IoC概述
1. 传统程序开发
2. 问题分析
3. 解决方案
4. IoC程序开发
5. IoC优势
二、IoC详解
1. Bean的存储
1.1 @Controller(控制器存储)
1.2 @Service(服务存储)
1.3 @Repository(仓库存储)
1.4 @Component(组件存储)
1.5 @Configuration(配置存储)
2. 为什么需要这么多注解?
3. 方法注解@Bean
3.1 方法注解要配合类注解使用
3.2 定义多个对象
3.3 重命名Bean
3.4 扫描路径
一、IoC概述
控制反转(Inversion of Control,IoC)是一种设计原则,用于解耦组件之间的依赖关系。传统编程中,调用者主动创建和管理依赖对象;而IoC将控制权交给外部容器或框架,由容器负责创建和注入依赖对象。
接下来我们通过案例来了解一下什么是IoC。
需求:造一辆车
1. 传统程序开发
我们的设计思路是这样的:
先设计轮子(Tire),然后根据轮子的大小设计底盘(Bottom),接着根据底盘设计车身(Framework),最后根据车身设计好整个汽车(Car)。这里就出现了一个“依赖”关系:汽车依赖车身,车身依赖底盘,底盘依赖轮子。
最终程序的实现代码如下:
public class NewCarExample {public static void main(String[] args) {Car car = new Car();car.run();}/*** 汽车对象*/static class Car {private Framework framework;public Car() {framework = new Framework();System.out.println("Car init...");}public void run() {System.out.println("Car run...");}}/*** 车身类*/static class Framework {private Bottom bottom;public Framework() {bottom = new Bottom();System.out.println("Framework init...");}}/*** 底盘类*/static class Bottom {private Tire tire;public Bottom() {this.tire = new Tire();System.out.println("Bottom init...");}}/*** 轮胎类*/static class Tire {// 尺寸private int size;public Tire() {this.size = 17;System.out.println("轮胎尺寸: " + size);}}
}
2. 问题分析
这样的设计看起来没问题,但是可维护性却很低。
如果接下来需求有了变更:随着对车的需求量越来越大,个性化需求也会越来越多,我们需要加工多种尺寸的轮胎。
那这个时候就需要对上面的程序进行修改,修改后的代码如下所示:
修改之后,其他调用程序也会报错,我们需要继续修改:
完整代码如下:
public class NewCarExample {public static void main(String[] args) {Car car = new Car(20);car.run();}/*** 汽车对象*/static class Car {private Framework framework;public Car(int size) {framework = new Framework(size);System.out.println("Car init...");}public void run() {System.out.println("Car run...");}}/*** 车身类*/static class Framework {private Bottom bottom;public Framework(int size) {bottom = new Bottom(size);System.out.println("Framework init...");}}/*** 底盘类*/static class Bottom {private Tire tire;public Bottom(int size) {this.tire = new Tire(size);System.out.println("Bottom init...");}}/*** 轮胎类*/static class Tire {// 尺寸private int size;public Tire(int size) {this.size = size;System.out.println("轮胎尺寸: " + size);}}
}
从以上代码可以看出:当最底层代码改动之后,整个调用链上的所有代码都需要修改(程序的耦合度非常高)。
3. 解决方案
我们尝试换一种思路,我们先设计汽车的大概样子,然后根据汽车的样子来设计车身,根据车身来设计底盘,最后根据底盘来设计轮子。这时候,依赖关系就倒置过来了:轮子依赖底盘,底盘依赖车身,车身依赖汽车。
如何来实现呢:
我们可以尝试不在每个类中自己创建下级类,如果自己创建下级类就会出现当下级类发生改变操作,自己也要跟着修改。此时,我们只需要将原来由自己创建的下级类,改为传递的方式(也就是注入的方式),因为我们不需要在当前类中创建下级类了,所以下级类即使发生变化(创建或减少参数),当前类本身也无需修改任何代码,这样就完成了程序的解耦。
4. IoC程序开发
基于以上思路,我们把调用汽车的程序示例改造一下,把创建子类的方式,改为注入传递的方式。
具体实现代码如下:
public class IocCarExample {public static void main(String[] args) {Tire tire = new Tire(20);Bottom bottom = new Bottom(tire);Framework framework = new Framework(bottom);Car car = new Car(framework);car.run();}static class Car {private Framework framework;public Car(Framework framework) {this.framework = framework;System.out.println("Car init....");}public void run() {System.out.println("Car run...");}}static class Framework {private Bottom bottom;public Framework(Bottom bottom) {this.bottom = bottom;System.out.println("Framework init...");}}static class Bottom {private Tire tire;public Bottom(Tire tire) {this.tire = tire;System.out.println("Bottom init...");}}static class Tire {private int size;public Tire(int size) {this.size = size;System.out.println("轮胎尺寸: " + size);}}
}
代码经过以上调整,无论底层类如何变化,整个调用链是不用做任何改变的,这样就完成了代码之间的解耦,从而实现了更加灵活、通用的程序设计了。
5. IoC优势
- 降低耦合度:组件不直接依赖具体实现。
- 增强可测试性:便于模拟依赖进行单元测试。
- 提高可维护性:依赖关系集中管理,修改时影响小。
- 灵活性:运行时动态替换依赖实现。
Spring就是一种IoC容器,帮助我们来做了这些资源管理。
二、IoC详解
1. Bean的存储
Spring框架为了更好的服务web应用程序,提供了更丰富的注解。
共有两类注解类型可以实现:
- 类注解:@Controller、@Service、@Repository、@Component、@Configuration
- 方法注解:@Bean
1.1 @Controller(控制器存储)
使用@Controller存储bean的代码如下所示:
import org.springframework.stereotype.Controller;@Controller
public class HelloController {public void print() {System.out.println("Hello control");}
}
从Spring容器中获取这个对象:
@SpringBootApplication
public class SpringIocDemoApplication {public static void main(String[] args) {//获取Spring上下文对象ApplicationContext context = SpringApplication.run(SpringIocDemoApplication.class, args);//从Spring上下文中获取对象HelloController bean = context.getBean(HelloController.class);//使用对象bean.print();}
}
ApplicationContext:Spring上下文
因为对象都交给Spring管理了,所以获取对象要从Spring中获取,那么就得下得到Spring的上下文。
观察运行结果,发现成功从Spring中获取到Controller对象,并执行Controller的print方法。
若把@Controller删掉,观察运行结果:
报错信息显示:找不到bean。
ApplicationContext也提供了其他获取bean的方式,ApplicationContext获取bean对象的功能,是父类BeanFactory提供的功能。
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//package org.springframework.beans.factory;import org.springframework.beans.BeansException;
import org.springframework.core.ResolvableType;
import org.springframework.lang.Nullable;public interface BeanFactory {String FACTORY_BEAN_PREFIX = "&";char FACTORY_BEAN_PREFIX_CHAR = '&';//1Object getBean(String name) throws BeansException;//2<T> T getBean(String name, Class<T> requiredType) throws BeansException;//3Object getBean(String name, Object... args) throws BeansException;//4<T> T getBean(Class<T> requiredType) throws BeansException;//5<T> T getBean(Class<T> requiredType, Object... args) throws BeansException;//6<T> ObjectProvider<T> getBeanProvider(Class<T> requiredType);//7<T> ObjectProvider<T> getBeanProvider(ResolvableType requiredType);boolean containsBean(String name);boolean isSingleton(String name) throws NoSuchBeanDefinitionException;boolean isPrototype(String name) throws NoSuchBeanDefinitionException;boolean isTypeMatch(String name, ResolvableType typeToMatch) throws NoSuchBeanDefinitionException;boolean isTypeMatch(String name, Class<?> typeToMatch) throws NoSuchBeanDefinitionException;@NullableClass<?> getType(String name) throws NoSuchBeanDefinitionException;@NullableClass<?> getType(String name, boolean allowFactoryBeanInit) throws NoSuchBeanDefinitionException;String[] getAliases(String name);
}
常用的是上述1,2,4种,这三种方式获取到的bean是一样的。其中1,2种都涉及到根据名称来获取对象,Bean的名称可以参考官方文档。
程序开发人员不需要为bean指定名称(Beanld),如果没有显式的提供名称(Beanld),Spring容器将为该bean生成唯一的名称。
命名约定使用Java标准约定作为实例字段名。也就是说,bean名称以小写字母开头,然后使用驼峰式大小写。
比如
类名:UserController,Bean的名称为:userController
类名:AccountManager,Bean的名称为:accountManager
类名:AccountService,Bean的名称为:accountService
也有一些特殊情况,当有多个字符并且第一个和第二个字符都是大写时,将保留原始的大小写。这些规则与java.beans.Introspector.decapitalize(Spring在这里使用的)定义的规则相同。
比如
类名:UController,Bean的名称为:UController
类名:AManager,Bean的名称为:AManager
根据这个命名规则,我们来获取bean。
@SpringBootApplication
public class SpringIocDemoApplication {public static void main(String[] args) {ApplicationContext context = SpringApplication.run(SpringIocDemoApplication.class, args);HelloController bean = context.getBean(HelloController.class);bean.print();HelloController bean2 = (HelloController) context.getBean("helloController");bean2.print();HelloController bean3 = context.getBean("helloController", HelloController.class);bean3.print();System.out.println(bean);System.out.println(bean2);System.out.println(bean3);}
}
三种方法都成功从Spring中获取到Controller对象,并执行Controller的print方法。并且地址一样,说明是同一个对象。
ApplicationContext VS BeanFactory
- 从继承关系和功能方面来说:Spring容器有两个顶级的接口:BeanFactory和ApplicationContext。其中BeanFactory提供了基础的访问容器的能力,而ApplicationContext属于BeanFactory的子类,它除了继承了BeanFactory的所有功能之外,它还拥有独特的特性,还添加了对国际化支持、资源访问支持、以及事件传播等方面的支持。
- 从性能方面来说:ApplicationContext是一次性加载并初始化所有的Bean对象,而BeanFactory是需要那个才去加载那个,因此更加轻量。(空间换时间)
1.2 @Service(服务存储)
使用@Service存储的代码如下所示:
import org.springframework.stereotype.Service;@Service
public class UserService {public void print() {System.out.println("Hello service");}
}
读取bean的代码:
@SpringBootApplication
public class SpringIocDemoApplication {public static void main(String[] args) {ApplicationContext context = SpringApplication.run(SpringIocDemoApplication.class, args);UserService bean = context.getBean(UserService.class);bean.print();}
}
1.3 @Repository(仓库存储)
使用@Repository存储的代码如下所示:
import org.springframework.stereotype.Repository;@Repository
public class UserRepository {public void print() {System.out.println("Hello Repository");}
}
读取bean的代码:
@SpringBootApplication
public class SpringIocDemoApplication {public static void main(String[] args) {ApplicationContext context = SpringApplication.run(SpringIocDemoApplication.class, args);UserRepository bean = context.getBean(UserRepository.class);bean.print();}
}
1.4 @Component(组件存储)
使用@Component存储bean的代码如下所示:
import org.springframework.stereotype.Component;@Component
public class UserComponent {public void print() {System.out.println("Hello component");}
}
读取bean的代码:
@SpringBootApplication
public class SpringIocDemoApplication {public static void main(String[] args) {ApplicationContext context = SpringApplication.run(SpringIocDemoApplication.class, args);UserComponent bean = context.getBean(UserComponent.class);bean.print();}
}
1.5 @Configuration(配置存储)
使用@Configuration存储bean的代码如下所示:
import org.springframework.context.annotation.Configuration;@Configuration
public class UserConfig {public void print() {System.out.println("Hello configuration");}
}
读取bean的代码:
@SpringBootApplication
public class SpringIocDemoApplication {public static void main(String[] args) {ApplicationContext context = SpringApplication.run(SpringIocDemoApplication.class, args);UserConfig bean = context.getBean(UserConfig.class);bean.print();}
}
2. 为什么需要这么多注解?
这个和应用分层是呼应的,让程序员看到类注解之后,就能直接了解当前类的用途。
- @Controller:控制层,接收请求,对请求进行处理,并进行响应
- @Servie:业务逻辑层,处理具体的业务逻辑
- @Repository:数据访问层,也称为持久层.负责数据访问操作
- @Configuration:配置层.处理项目中的一些配置信息
程序的应用分层,调用流程如下:
查看@Controller /@Service /@Repository /@configuration等注解的源码发现:
其实这些注解里面都有一个注解@Component,说明它们本身就是属于@Component 的"子类"。@Component是一个元注解,也就是说可以注解其他类注解,如@Controller,@Service,@Repository等。这些注解被称为@Component的衍生注解。
@Controller,@Service和@Repository用于更具体的用例(分别在控制层,业务逻辑层,持久化层),在开发过程中,如果你要在业务逻辑层使用@Component或@Service,显然@Service是更好的选择。
3. 方法注解@Bean
类注解是添加到某个类上的,但是存在两个问题:
- 使用外部包里的类,没办法添加类注解
- 一个类,需要多个对象,比如多个数据源
这种场景,我们就需要使用方法注解@Bean。
下来看方法注解如何使用,创建一个Student类:
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;@NoArgsConstructor
@AllArgsConstructor
@Data
public class Student {private String name;private Integer age;
}
public class StudentComponent {@Beanpublic Student student() {return new Student("zhangsan", 18);}
}
尝试获取Bean对象中的student:
@SpringBootApplication
public class SpringIocDemoApplication {public static void main(String[] args) {ApplicationContext context = SpringApplication.run(SpringIocDemoApplication.class, args);Student student = context.getBean(Student.class);System.out.println(student);}
}
但是未能成功获取。
3.1 方法注解要配合类注解使用
在Spring框架的设计中,方法注解@Bean要配合类注解才能将对象正常的存储到Spring容器中,
如下代码所示:
@Component
public class StudentComponent {@Beanpublic Student student() {return new Student("zhangsan", 18);}
}
再次运行:
3.2 定义多个对象
对于同一个类,如何定义多个对象呢?(比如多数据源的场景,类是同一个,但是配置不同,志向不同的数据源)
@Bean的使用:
@Component
public class StudentComponent {@Beanpublic Student student1() {return new Student("zhangsan", 18);}@Beanpublic Student student2 () {return new Student("lisi", 20);}
}
根据类型获取到的是哪个对象呢?
@SpringBootApplication
public class SpringIocDemoApplication {public static void main(String[] args) {ApplicationContext context = SpringApplication.run(SpringIocDemoApplication.class, args);Student student = context.getBean(Student.class);System.out.println(student);}
}
报错信息显示:期望只有一个匹配,结果发现了两个。
从报错信息中,可以看出@Bean注解的bean,bean的名称就是它的方法名。
接下来我们根据名称来获取bean对象:
@SpringBootApplication
public class SpringIocDemoApplication {public static void main(String[] args) {ApplicationContext context = SpringApplication.run(SpringIocDemoApplication.class, args);//根据Bean名称,从Spring上下文获取对象Student student1 = (Student) context.getBean("student1");Student student2 = (Student) context.getBean("student2");System.out.println(student1);System.out.println(student2);}
}
3.3 重命名Bean
可以通过设置name属性给Bean对象进行重命名操作,如下代码所示:
@Bean(name = {"s","stu"})
public Student student() {return new Student("zhangsan", 18);
}
此时我们使用s或stu就可以获取到Student对象了:
@SpringBootApplication
public class SpringIocDemoApplication {public static void main(String[] args) {ApplicationContext context = SpringApplication.run(SpringIocDemoApplication.class, args);Student student1 = (Student) context.getBean("s");Student student2 = (Student) context.getBean("stu");System.out.println(student1);System.out.println(student2);}
}
“name=”可以省略:
@Bean({"s","stu"}) public Student student() {return new Student("zhangsan", 18); }
只有一个名称时,“{}”也可以省略:
@Bean("s") public Student student() {return new Student("zhangsan", 18); }
3.4 扫描路径
下面我们先修改启动类的代码:
@SpringBootApplication
public class SpringIocDemoApplication {public static void main(String[] args) {ApplicationContext context = SpringApplication.run(SpringIocDemoApplication.class, args);Student bean = (Student) context.getBean("s");System.out.println(bean);}
}
再修改项目工程的目录结构,测试bean对象是否生效:
使用五大注解声明的bean,要想生效,还需要配置扫描路径,让Spring扫描到这些注解,也就是通过@ComponentScan来配置扫描路径。
@ComponentScan("com.bite.springiocdemo")
@SpringBootApplication
public class SpringIocDemoApplication {public static void main(String[] args) {ApplicationContext context = SpringApplication.run(SpringIocDemoApplication.class, args);Student bean = (Student) context.getBean("s");System.out.println(bean);}
}
在之前的代码中,@ComponentScan注解虽然没有显式配置,但是实际上已经包含在了启动类声明注解@SpringBootApplication中了,默认扫描的范围是SpringBoot启动类所在包及其子包。在配置类上添加@ComponentScan注解,该注解默认会扫描该类所在的包下所有的配置类。
创作不易,给个三连支持一下吧