【JavaEE】Spring IoC&DI详解

一.基本概念

1.Ioc基本概念

  • Ioc: Inversion of Control (控制反转), 也就是说 Spring 是⼀个"控制反转"的容器.

什么是控制反转呢?

也就是控制权反转. 什么的控制权发发了反转? 获得依赖对象的过程被反转了也就是说, 当需要某个对象时, 传统开发模式中需要自己通过 new 创建对象, 现在不需要再进行创建, 把创建对象的任务交给容器, 程序中只需要依赖注入(Dependency Injection,DI)就可以了. 这个容器称为:IoC容器. Spring是一个IoC容器,所以有时Spring也称为Spring容器.

  • 控制反转是一种思想, 在生活中也是处处体现.
    比如自动驾驶, 传统驾驶方式, 车辆的横向和纵向驾驶控制权由驾驶员来控制, 现在交给了驾驶自动化系统来控制, 这也是控制反转思想在生活中的实现.比如招聘, 企业的员工招聘,入职, 解雇等控制权. 老板转交给HR(人力资源)来处理

2.DI基本概念

  • DI : Dependency Injection(依赖注入)容器在运行期间, 动态的为应用程序提供运行时所依赖的资源,称之为依赖注入。
  • 程序运行时需要某个资源,此时容器就为其提供这个资源.从这点来看, 依赖注入(DI)和控制反转(IoC)是从不同的角度的描述的同⼀件事情,就是指通过引入 IoC 容器,利用依赖关系注入的方式,实现对象之间的解耦。
  • IoC 是⼀种思想,也是"目标", 而思想只是一种指导原则,最终还是要有可行的落地方案,而 DI 就属于具体的实现。所以也可以说, DI 是IoC的一种实现.
    • 比如说我今天心情比较好,吃一顿好的犒劳犒劳自己,那么"吃一顿好的"是思想和目标(是IoC),但最后我是吃海底捞还是杨国福?这就是具体的实现,就是 DI。

二.Ioc的使用.

  • IoC交给Spring管理共有两类注解可以实现:
    *
    1. 类注解:@Controller、@Service、@Repository、@Component、@Configuration.
      1. 方法注解:@Bean.

2.1类注解

2.1.1@Controller注解

使用@Controller存储 bean 的代码如下所示:

java 复制代码
@Controller // 将对象存储到 Spring 中
public class UserController {
 public void sayHi(){
 System.out.println("hi,UserController...");
 }
}

读取 bean 的代码:

java 复制代码
@SpringBootApplication
public class SpringIocDemoApplication {
 	public static void main(String[] args) {
 	//获取Spring上下⽂对象
 	ApplicationContext context = 
	SpringApplication.run(SpringIocDemoApplication.class, args);
 	//从Spring上下⽂中获取对象
 	UserController userController = context.getBean(UserController.class);
	 //使⽤对象
 	userController.sayHi();
 	}
 }

ApplicationContext 翻译过来就是: Spring 上下文因为对象都交给 Spring 管理了,所以获取对象要从 Spring 中获取,那么就得先得到 Spring 的上下文.

关于上下文的概念上学时, 阅读理解经常会这样问: 根据上下文, 说⼀下你对XX的理解, 在计算机领域, 上下文这个概念, 咱们最早是在学习线程时了解到过, 比如我们应用进行线程切换的时候,切换前都会把线程的状态信息暂时储存起来,这里的上下文就包括了当前线程的信息,等下次该线程又得到CPU时间的时候, 从上下文中拿到线程上次运行的信息这个上下文, 就是指当前的运行环境, 也可以看作是⼀个容器, 容器里存了很多内容, 这些内容是当前运行的环境

  • 如果把@Controller删掉, 再观察运行结果
  • 报错信息显示: 找不到类型是: com.example.demo.controller.UserController的bean
Bean 命名约定
  • 官方文档关于Bean命名的叙述: 链接:link
  • 命名约定使用Java标准约定作为实例字段名. 也就是说,bean名称以小写字母开头,然后使用驼峰式大小写.比如
    类名: UserController, Bean的名称为: userController
    类名: AccountManager, Bean的名称为: accountManager
    类名: AccountService, Bean的名称为: accountService
  • 也有一些特殊情况, 当有多个字符并且第一个和第二个字符都是大写时, 将保留原始的大小写. 这些规则与java.beans.Introspector.decapitalize (Spring在这里使用的)定义的规则相同.
    比如
    类名: UController, Bean的名称为: UController
    类名: AManager

2.1.2@Server注解 ( 服务存储 )

使用@Service 存储 bean 的代码如下所示:

java 复制代码
@Service
public class UserService {
 	public void sayHi(String name) {
 		System.out.println("Hi," + name);
 	}
}

读取 bean 的代码:

java 复制代码
@SpringBootApplication
public class SpringIocDemoApplication {
 	public static void main(String[] args) {
 	//获取Spring上下⽂对象
 	ApplicationContext context = 
	SpringApplication.run(SpringIocDemoApplication.class, args);
 	//从Spring中获取UserService对象
 	UserService userService = context.getBean(UserService.class);
 	//使⽤对象
 	userService.sayHi();
	}
}

观察运行结果, 发现成功从Spring中获取到UserService对象, 并执行UserService的sayHi方法

2.1.3@Repository ( 仓库存储 )

使用@Repository 存储 bean 的代码如下所示:

java 复制代码
@Repository
public class UserRepository {
	public void sayHi() {
		System.out.println("Hi, UserRepository~");
	}
}

读取 bean 的代码:

java 复制代码
@SpringBootApplication
public class SpringIocDemoApplication {
 	public static void main(String[] args) {
 	//获取Spring上下⽂对象
	 ApplicationContext context = 
	SpringApplication.run(SpringIocDemoApplication.cla
 	//从Spring上下⽂中获取对象
 	UserRepository userRepository = context.getBean(UserRepository.class);
 	//使⽤对象
 	userRepository.sayHi();
 	}
}

观察运行结果, 发现成功从Spring中获取到UserRepository 对象, 并执行UserRepository 的sayHi方法

2.1.4@Component ( 组件存储 )

使用@Component 存储 bean 的代码如下所示:

java 复制代码
@Component
public class UserComponent {
 	public void sayHi() {
 		System.out.println("Hi, UserComponent~");
 	}
 }

读取 bean 的代码:

java 复制代码
@SpringBootApplication
public class SpringIocDemoApplication {
 	public static void main(String[] args) {
 	//获取Spring上下⽂对象
 	ApplicationContext context = 
	SpringApplication.run(SpringIocDemoApplication.class, args);
 	//从Spring上下⽂中获取对象
 	UserComponent userComponent = context.getBean(UserComponent.class);
 	//使⽤对象
 	userComponent.sayHi();
 	}
}

观察运行结果, 发现成功从Spring中获取到UserComponent 对象, 并执行UserComponent 的sayHi方法

2.1.5@Configuration( 组件存储 )

使用 @Configuration 存储 bean 的代码如下所示:

java 复制代码
@Configuration
public class UserConfiguration {
 	public void sayHi() {
 		System.out.println("Hi,UserConfiguration~");
 	}
 }

读取 bean 的代码:

java 复制代码
@SpringBootApplication
public class SpringIocDemoApplication {
 	public static void main(String[] args) {
 	//获取Spring上下⽂对象
 	ApplicationContext context = 
	SpringApplication.run(SpringIocDemoApplication.class, args);
 	//从Spring上下⽂中获取对象
 	UserConfiguration userConfiguration = 
	context.getBean(UserConfiguration.class);
 	//使⽤对象
 	userConfiguration.sayHi();
 	}
}

观察运行结果, 发现成功从Spring中获取到UserConfiguration 对象, 并执行UserConfiguration 的sayHi方法

2.2为什么要这么多类注解?

  • 这个也是和应用分层是呼应的. 让程序员看到类注解之后,就能直接了解当前类的用途.
    • @Controller:控制层, 接收请求, 对请求进⾏处理, 并进⾏响应.
    • @Servie:业务逻辑层, 处理具体的业务逻辑.
    • @Repository:数据访问层,也称为持久层. 负责数据访问操
    • @Configuration:配置层. 处理项⽬中的⼀些配置信息

这和每个省/市都有自己的车牌号是一样的.车牌号都是唯一的, 标识⼀个车辆的. 但是为什么还需要设置不同的车牌开头呢.比如陕西的车牌号就是:陕X:XXXXXX,北京的车牌号:京X:XXXXXX,甚至一个省不同的县区也是不同的,比如西安就是,陕A:XXXXX,咸阳:陕B:XXXXXX,宝鸡,陕C:XXXXXX,一样.这样做的好处除了可以节约号码之外,更重要的作用是可以直观的标识一辆车的归属地.

这样做的好处除了可以节约号码之外,更重要的作⽤是可以直观的标

类注解之间的关系

查看 @Controller / @Service / @Repository / @Configuration 等注解的源码发

现:

其实这些注解里面都有⼀个注解 @Component ,说明它们本⾝就是属于 @Component 的"子类". @Component 是⼀个元注解,也就是说可以注解其他类注解,如 @Controller , @Service ,@Repository 等. 这些注解被称为 @Component 的衍生注解.@Controller , @Service 和 @Repository ⽤于更具体的用例(分别在控制层, 业务逻辑层, 持久化层), 在开发过程中, 如果你要在业务逻辑层使用 @Component 或@Service,显然@Service是更好的选择

2.3方法注解 @Bean

类注解是添加到某个类上的, 但是存在两个问题:

  1. 使用外部包里的类, 没办法添加类注解
  2. ⼀个类, 需要多个对象, 比如多个数据源, 这种场景, 我们就需要使用方法注解 @Bean
  • 方法注解如何使用
java 复制代码
public class BeanConfig {
 	@Bean
 	public User user(){
 	User user = new User();
 	user.setName("zhangsan");
 	user.setAge(18);
 	return user;
 	}
}

然而,当我们写完以上代码,尝试获取 bean 对象中的 user 时却发现,根本获取不到:

java 复制代码
@SpringBootApplication
public class SpringIocDemoApplication {
 	public static void main(String[] args) {
 	//获取Spring上下⽂对象
 	ApplicationContext context = 
	SpringApplication.run(SpringIocDemoApplication.class, args);
 	//从Spring上下⽂中获取对象
 	User user = context.getBean(User.class);
 	//使⽤对象
 	System.out.println(user);
 	}
}

以上程序的执行结果如下:

2.3.1 方法注解要配合类注解使用

java 复制代码
@Component
public class BeanConfig {
 @Bean
 public User user(){
 User user = new User();
 user.setName("zhangsan");
 user.setAge(18);
 return user;
 }
}

再次执行以上代码,运行结果如下:

2.3.2 定义多个对象

java 复制代码
@Component
public class BeanConfig {
 	@Bean
 	public User user1(){
 		User user = new User();
 		user.setName("zhangsan");
 		user.setAge(18);
 		return user;
 	}
 	@Bean
 	public User user2(){
 		User user = new User();
 		user.setName("李四");
 		user.setAge(20);
 		return user;
 	}
}
java 复制代码
@SpringBootApplication
public class SpringIocDemoApplication {
	public static void main(String[]
 		//获取Spring上下⽂对象
 		ApplicationContext context = 
		SpringApplication.run(SpringIocDemo
 		//从Spring上下⽂中获取对象
 		User user = context.getBean(U
 		//使⽤对象
 		System.out.println(user);
 	}
}

运行结果:

报错信息显示: 期望只有⼀个匹配, 结果发现了两个, user1, user2从报错信息中, 可以看出来, @Bean 注解的bean, bean的名称就是它的方法名

接下来我们根据名称来获取bean对象

java 复制代码
@SpringBootApplication
public class SpringIocDemoApplication {
 	public static void main(String[] args) {
 		//获取Spring上下⽂对象
 		ApplicationContext context = 
		SpringApplication.run(SpringIocDemoApplication.class, args);
 		//根据bean名称, 从Spring上下⽂中获取对象
 		User user1 = (User) context.getBean("user1");
 		User user2 = (User) context.getBean("user2");
 		System.out.println(user1);
 		System.out.println(user2);
 	}
 }

运行结果:

2.3.3 重命名 Bean

java 复制代码
@Bean(name = {"u1","user1"})
public User user1(){
 	User user = new User();
 	user.setName("zhangsan");
 	user.setAge(18);
 	return user;
}

此时我们使⽤ u1 就可以获取到 User对象了,代码如下:

java 复制代码
@SpringBootApplication
public class SpringIocDemoApplication {
 	public static void main(String[] args) {
 		//获取Spring上下⽂对象
 		ApplicationContext context = 
		SpringApplication.run(SpringIocDemoApplication.class, args);
 		//从Spring上下⽂中获取对象
 		User u1 = (User) context.getBean("u1");
 		//适用对象
 		System.out.println(u1);
 	}
 }

2.3.4 扫描路径

再运行代码:

java 复制代码
public class SpringIocDemoApplication {
 	public static void main(String[]
 		//获取Spring上下⽂对象
 		ApplicationContext context = 
		SpringApplication.run(SpringIocDemo
	 	//从Spring上下⽂中获取对象
 		User u1 = (User) context.getB
 		//使⽤对象
 		System.out.println(u1);
 	}
}

运行结果:

使用五大注解声明的bean,要想生效, 还需要配置扫描路径, 让Spring扫描到这些注解. 也就是通过 @ComponentScan 来配置扫描路径.

java 复制代码
@ComponentScan({"com.example.demo"})
@SpringBootApplication
public class SpringIocDemoApplication {
 	public static void main(String[] args) {
 		//获取Spring上下⽂对象
 		ApplicationContext context =
		SpringApplication.run(SpringIocDemoApplication.class, args);
 		//从Spring上下⽂中获取对象
 		User u1 = (User) context.getBean("u1");
 		//使⽤对象
 		System.out.println(u1);
 	}
 }
  • 默认扫描的范围是SpringBoot启动类所在包及其子包

三.DI的使用.

  • 关于依赖注入, Spring也给我们提供了三种方式:
    *
    1. 属性注入(Field Injection)
      1. 构造方法注⼊(Constructor Injection)
      1. Setter 注入

3.1属性注入

  • 属性注入是使用 @Autowired 实现的,将 Service 类注入到 Controller 类中.
  • Service 类的实现代码如下
java 复制代码
import org.springframework.stereotype.Service;
@Service
public class UserService {
 	public void sayHi() {
 		System.out.println("Hi,UserService");
 	}
}

Controller 类的实现代码如下:

java 复制代码
@Controller
public class UserController {
 	//注⼊⽅法1: 属性注⼊
 	@Autowired
 	private UserService userService;
 	public void sayHi(){
 		System.out.println("hi,UserController...");
 		userService.sayHi();
 	}
}

获取 Controller 中的 sayHi方法:

java 复制代码
@SpringBootApplication
public class SpringIocDemoApplication {
 	public static void main(String[] args) {
 		//获取Spring上下⽂对象
 		ApplicationContext context = 
		SpringApplication.run(SpringIocDemoApplication.class, args);
 		//从Spring上下⽂中获取对象
 		UserController userController = (UserController) 
		context.getBean("userController");
 		//使⽤对象
 		userController.sayHi();
 		}
 }

最终运行结果:

去掉@Autowired , 再运行一下程序看看结果

3.2构造方法注入

  • 构造方法注入是在类的构造方法中实现注入,如下代码所示:
java 复制代码
@Controller
public class UserController2 {
 	//注⼊⽅法2: 构造⽅法
 	private UserService userService;
 	@Autowired
 	public UserController2(UserService userService) {
		this.userService = userService;
 	}
 	public void sayHi(){
		System.out.println("hi,UserController2...");
		userService.sayHi();
	}
}

注意事项:如果类只有一个构造方法,那么 @Autowired 注解可以省略;如果类中有多个构造方法,那么需要添加上 @Autowired 来明确指定到底使用哪个构造方法。

3.3Setter注入

Setter 注入和属性的 Setter 方法实现类似,只不过在设置 set ⽅法的时候需要加上 @Autowired 注解 ,如下代码所示:

java 复制代码
@Controller
public class UserController3 {
 		//注⼊⽅法3: Setter⽅法注⼊
 		private UserService userService;
 		@Autowired
 		public void setUserService(UserService userService) {
 			this.userService = userService;
 		}
 		public void sayHi(){
 			System.out.println("hi,UserController3...");
 			userService.sayHi();
 		}
}

3.4三重注入方式的优缺点分析.

属性注入

  • 优点: 简洁,使用方便;
  • 缺点:
    • 只能用于 IoC 容器,如果是非 IoC 容器不可用,并且只有在使用的时候才会出现 NPE(空指针异常)
    • 不能注入一个Final修饰的属性.

构造函数注入(Spring 4.X推荐)

  • 优点:

    • 可以注入final修饰的属性
    • 注入的对象不会被修改
    • 依赖对象在使用前一定会被完全初始化,因为依赖是在类的构造方法中执行的,而构造方法是在类加载阶段就会执行的方法.
    • 通⽤性好, 构造方法是JDK支持的, 所以更换任何框架,他都是适用的
  • 缺点:

    • 注入多个对象时, 代码会比较繁琐

Setter注⼊(Spring 3.X推荐)

  • 优点: 方便在类实例之后, 重新对该对象进行配置或者注入
  • 缺点:
    • 不能注入一个Final修饰的属性
    • 注入对象可能会被改变, 因为setter方法可能会被多次调用,就有被修改的风险

3.5@Autowired存在问题

当同一类型存在多个bean时, 使用@Autowired会存在问题.

java 复制代码
@Component
public class BeanConfig {
 	@Bean("u1")
 	public User user1(){
 		User user = new User();
 		user.setName("zhangsan");
 		user.setAge(18);
 		return user;
 	}
 	@Bean
 	public User user2() {
 		User user = new User();
 		user.setName("lisi");
 		user.setAge(19);
 		return user;
 	}
}
java 复制代码
@Controller
public class UserController {
 
 	@Autowired
 	private UserService userService;
 	//注⼊user
 	@Autowired
 	private User user;
	public void sayHi(){
 		System.out.println("hi,UserController...");
 		userService.sayHi();
 		System.out.println(user);
 	}
}

运行结果:

报错的原因是,非唯一的 Bean 对象。

如何解决上述问题呢?Spring提供了以下几种解决方案:

  1. @Primary

    使用@Primary注解:当存在多个相同类型的Bean注入时,加上@Primary注解,来确定默认的实现.

  2. @Qualifier

    • 使用@Qualifier注解:指定当前要注入的bean对象。 在@Qualifier的value属性中,指定注入的bean的名称。
    • @Qualifier注解不能单独使⽤,必须配合@Autowired使⽤
  3. @Resource

    • 使用@Resource注解:是按照bean的名称进行注入。通过name属性指定要注入的bean的名称。

@Autowird 与 @Resource的区别

  • @Autowired 是spring框架提供的注解,而@Resource是JDK提供的注解
  • @Autowired 默认是按照类型注⼊,而@Resource是按照名称注入. 相比于@Autowired 来说,@Resource 支持更多的参数设置,例如 name 设置,根据名称获取 Bean。
相关推荐
csdn5659738507 分钟前
Java 实现 Elasticsearch 查询当前索引全部数据
java·elasticsearch·jenkins
一弓虽15 分钟前
java基础学习——IO流详细介绍
java·学习·io流
黑客老陈19 分钟前
JAVA XXE 学习总结
java·服务器·开发语言·python·学习·安全·web安全
杂货铺的小掌柜20 分钟前
spring mvc源码学习笔记之七
学习·spring·mvc
杂货铺的小掌柜20 分钟前
spring mvc源码学习笔记之九
学习·spring·mvc
风清云淡_A22 分钟前
【JAVA基础】Collections方法的具体使用方法
java·后端
华东设计之美31 分钟前
java集合类有哪些?
java·开发语言
heath ceTide33 分钟前
Java项目中集成Github登录
java·开发语言·github
sin220136 分钟前
springmvc--对日期类型如何处理
java·开发语言
java熊猫1 小时前
Ruby语言的正则表达式
开发语言·后端·golang