1 依赖注入
1.1 什么是依赖注入
依赖注入(dependency injection)的意思为,给予调用方它所需要的事物。“依赖”是指可被方法调用的事物。依赖注入形式下,调用方不再直接指使用“依赖”,取而代之是“注入”。"注入”是指将“依赖”传递给调用方的过程。
要想搞清楚这个“依赖注入”就必须理解什么是“依赖”,依赖就是指一个业务功能执行期间所需要的前提,比如:做米饭以米为前提,则做米饭功能依赖为米,汽车需要汽油才能跑起来,则汽车跑依赖于汽油,再比如光头强砍树需要用到电锯,则光头强砍树依赖于电锯。
注入是指被依赖的前提对象获得方式,如果是自己去创建就是“主动获得”,如果是被动得到,就是“注入”得到,比如光头强砍树依赖的电锯是李老板给的,这就是李老板将“电锯”注入给光头强,光头强去砍树。
1.2 @Bean注入
Spring IOC容器提供了依赖注入功能,可以将一个组件依赖的对象,在使用之前注入到合适位置。比如,如下关系模拟了光头强砍树时候依赖电锯:
Spring IOC解决依赖注入,在配置类Config中为Bean组件初始化方法增加参数,Spring IOC容器会在初始化时候自动根据类型注入Bean组件对象,解决“依赖注入”问题:
1.2.1 案例:
创建新的spring项目:
- 创建Saw
package cn.tedu.demo;
import java.io.Serializable;
public class Saw implements Serializable{
private String name = "寒冰锯";
@Override
public String toString() {
// TODO Auto-generated method stub
return name;
}
}
- 创建Worker
package cn.tedu.demo;
import java.io.Serializable;
public class Worker implements Serializable{
private String name = "光头强";
public Saw saw;
public void work() {
System.out.println(name + "使用" + saw + "砍树");
}
}
- 创建配置文件
package cn.tedu.context;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import cn.tedu.demo.Saw;
import cn.tedu.demo.Worker;
@Configuration
public class config {
@Bean
public Saw saw() {
return new Saw();
}
@Bean
/*
* spring会自动根据变量类型匹配bean组件类型,如果匹配到就将bean组件注入到方法参数中
*/
public Worker worker(Saw s) {
Worker worker = new Worker();
worker.saw = s;
return worker;
}
}
- 测试案例
package cn.tedu.test;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import cn.tedu.context.config;
import cn.tedu.demo.Worker;
public class TestCase {
AnnotationConfigApplicationContext ctx;
@Before
public void init() {
ctx = new AnnotationConfigApplicationContext(config.class);
}
@After
public void destroy() {
ctx.close();
}
@Test
public void testWorker() {
Worker worker = ctx.getBean("worker",Worker.class);
worker.work();
}
}
- 运行结果
光头强使用寒冰锯砍树
1.2.2 Spring IOC组件注入时候组件自动匹配规则:
- 首先按照注入参数类型查找相对应类型的Bean组件,如果没有直接报错
- 如果在Spring容器中能够匹配上唯一类型的Bean组件,如果没有直接报错
- 如果按照类型匹配到俩个Bean组件,则在查找组件ID和变量名是否匹配,如果匹配则注入成功
- 如果组件类型和组件ID都不能很好匹配则报错
1.2.3 注入失败案例:
- 更新Config.java
package cn.tedu.context;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import cn.tedu.demo.Saw;
import cn.tedu.demo.Worker;
@Configuration
public class config {
@Bean
public Saw saw() {
return new Saw();
}
@Bean
public Saw saw2() {
return new Saw();
}
@Bean
/*
* spring会自动根据变量类型匹配bean组件类型,如果匹配倒就将bean组件注入倒方法参数中
*/
public Worker worker(Saw s) {
Worker worker = new Worker();
worker.saw = s;
return worker;
}
}
- 重新运行测试结果
No qualifying bean of type 'cn.tedu.demo.Saw' available: expected single matching bean but found 2: saw,saw2
利用名字匹配案例:
package cn.tedu.context;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import cn.tedu.demo.Saw;
import cn.tedu.demo.Worker;
@Configuration
public class config {
@Bean
public Saw saw1() {
return new Saw();
}
@Bean
public Saw saw2() {
return new Saw();
}
@Bean
/*
* spring会自动根据变量类型匹配bean组件类型,如果匹配倒就将bean组件注入倒方法参数中
*/
public Worker worker(Saw saw1) {
Worker worker = new Worker();
worker.saw = saw;
return worker;
}
}
- 重新运行测试结果
光头强使用寒冰锯砍树
2 @Autowired
Spring提供的组件扫描功能,在扫描时候也可以完成依赖注入,这样可以减少编码提高编程效率。
如何使用@Autowired:
2.1 案例:
2.1.1 创建项目
- 创建一个新的Spring项目
- 电锯类Saw
2.1.2 创建Worker类
package cn.tedu.demo;
import java.io.Serializable;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class Worker implements Serializable{
private String name = "光头强";
@Autowired
public Saw saw;
public void work() {
System.out.println(name + "使用" + saw + "砍树");
}
}
2.1.3 创建Saw类
package cn.tedu.demo;
import java.io.Serializable;
import org.springframework.stereotype.Component;
@Component
public class Saw implements Serializable{
private String name = "寒冰锯";
@Override
public String toString() {
// TODO Auto-generated method stub
return name;
}
}
2.1.4 创建配置类
package cn.tedu.context;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import cn.tedu.demo.Saw;
import cn.tedu.demo.Worker;
@Configuration
@ComponentScan(basePackages = "cn.tedu.demo")
public class config {
}
2.1.5 测试案例
package cn.tedu.test;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import cn.tedu.context.config;
import cn.tedu.demo.Worker;
public class TestCase {
AnnotationConfigApplicationContext ctx;
@Before
public void init() {
ctx = new AnnotationConfigApplicationContext(config.class);
}
@After
public void destroy() {
ctx.close();
}
@Test
public void testWorker() {
Worker worker = ctx.getBean("worker",Worker.class);
worker.work();
}
}
2.1.6 测试结果运行
光头强使用寒冰锯砍树
3 set方法注入
刚刚学过@Autowired可以实现对象属性注入,@Autowired也可以标注在set方法上实现set方法注入。set方法也称为Bean属性访问方法,所以set方法注入也称为Bean属性注入。
Spring提供set方法注入的目的是给程序员以更多可以选择的注入方式,程序员可以根据实际情况下选择一种方式注入对象。
使用set方法注入原理:
案例:
- 更新Worker
package cn.tedu.demo;
import java.io.Serializable;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class Worker implements Serializable{
private String name = "光头强";
public Saw saw;
@Autowired
public void setSaw(Saw saw) {
this.saw = saw;
System.out.println("setSaw()");
}
public void work() {
System.out.println(name + "使用" + saw + "砍树");
}
}
- 重新测试
setSaw()
光头强使用寒冰锯砍树
测试结果中出现setSaw则说明确实是执行了setSaw方法注入的,在实际工作中,可以在此过程中实现一些其他逻辑,而不是直接注入输出,可以使用此方法。
4 IOC/DI解耦
4.1 利用接口解耦
IOC/DI和接口配合可以实现软件组件“解耦”。
要想理解“解耦”,就需要理解什么是耦合性,我们将一个组件依赖另外一个组件的现象称为耦合。组件连接紧密的称为“紧耦合”,反之松散连接关系称为“松耦合”。比如将手机和电池制造为一个整体,则手机和电池是紧密连接关系,是紧耦合;将手机和电池做成可拆卸组合的零件,则手机和电池是松散连接关系,是松耦合。松耦关系带来的好处是显而易见的,可以使组件之间可以重构,重组,重新搭配,就像手机和电池如果是松耦合关系,就可以在电池没有电的时候进行更换,生活中,松耦合关系比比皆是。
将紧密耦合关系改变为松散耦合关系称为“解耦”。
软件组件之间也存在耦合关系,将紧耦合转化为松耦合使软件之间能够更灵活的组合是很有必要的,上述的案例中“工人”只能依赖“电锯”砍树就是一种紧耦合关系,这种方式造成工人无法更换工具砍树。解决办法是利用接口进行抽象设计,这样组件依赖与接口就“解耦”了。
- 抽象设计工具接口,作为电锯和斧子的父类型
- 电锯和斧子实现工具接口
- 工人依赖于工具接口,工人不依赖于具体的工具
- 利用IOC管理组件,为工人注入适当的工具
利用Spring IOC容易提供的DI可以将工具对象注入给工人对象,继而解决对象之间的依赖关系,并且由于依赖接口,所以利用Spring IOC就可以控制组件的组合关系,实现松耦合。
案例:
- 创建一个新Spring项目
- 声明工具接口
package cn.tedu.demo;
/*
* 代表抽象的工具类型
*/
public interface Tool {
}
- 声明工人类
package cn.tedu.demo;
import java.io.Serializable;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class Worker implements Serializable{
private String name = "光头强";
@Autowired
private Tool tool;
public void work() {
System.out.println(name + "使用" + tool + "砍树");
}
}
- 声明电锯类
package cn.tedu.demo;
import java.io.Serializable;
import org.springframework.stereotype.Component;
//@Component
public class Saw implements Tool{
private String name = "寒冰锯";
@Override
public String toString() {
// TODO Auto-generated method stub
return name;
}
}
- 声明斧子类
package cn.tedu.demo;
import org.springframework.stereotype.Component;
@Component
public class Axe implements Tool{
private String name = "开天斧";
@Override
public String toString() {
// TODO Auto-generated method stub
return name;
}
}
- 配置类
package cn.tedu.context;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;
@Configuration
@ComponentScan(basePackages = "cn.tedu.demo")
public class Config {
}
- 测试类
package cn.tedu.text;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import cn.tedu.context.Config;
import cn.tedu.demo.Worker;
public class TestCase {
AnnotationConfigApplicationContext ctx;
@Before
public void init() {
ctx = new AnnotationConfigApplicationContext(Config.class);
}
@After
public void destroy() {
ctx.close();
}
@Test
public void testWorker() {
Worker worker = ctx.getBean("worker",Worker.class);
worker.work();
}
}
- 测试结果
光头强使用开天斧砍树
删除斧子类上的@Component注解,在电锯类上增加@Component注解,再次测试:
光头强使用寒冰锯砍树
说明只需要简单更改配置就可以轻松解决注解之间的耦合关系
4.2 @Autowired注入规则
上述案例中,如果将斧子类和电锯类都加上@Component注解将出现运行错误,原因是违反类@Autowired注解的注解注规则,@Autowired注入规则与@Bean组件注入规则类似:
- 首先按照注入参数类型查找相应类型的Bean组件,如果没有直接报错误
- 如果在Spring容器中能够匹配上唯一类型的Bean组件,则进行注入成功
- 如果按照类型匹配到俩个bean组件,则在查找组件ID和变量名是否匹配,如果匹配则注入成功
- 如果组件类型的组件ID都不能很好匹配则报错
- 如何解决斧子类和电锯类都加上@Component注解将出现运行错误?
-
- 利用组件自定义的ID,当组件的ID设置为too时,@Autowired注解就会根据ID匹配成功
-
- 利用注解@Qualifier(“BeanID”)指定注入Bean组件的ID
案例:将斧子类和电锯类都加上@Component注解
- 在斧子类上设置自定义BeanID
package cn.tedu.demo;
import org.springframework.stereotype.Component;
@Component("tool")
public class Axe implements Tool{
private String name = "开天斧";
@Override
public String toString() {
// TODO Auto-generated method stub
return name;
}
}
测试结果:
光头强使用开天斧砍树
- 重构Worker类,设定@Qualifier(“saw”)
package cn.tedu.text;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import cn.tedu.context.Config;
import cn.tedu.demo.Worker;
public class TestCase {
AnnotationConfigApplicationContext ctx;
@Before
public void init() {
ctx = new AnnotationConfigApplicationContext(Config.class);
}
@After
public void destroy() {
ctx.close();
}
@Test
public void testWorker() {
Worker worker = ctx.getBean("worker",Worker.class);
worker.work();
}
}
测试结果:
光头强使用寒冰锯砍树
以上方式都可以解决同时注入冲突问题,日常使用中根据实际情况使用即可。
4.3 同时使用@Bean和@Component
@Bean组件和@Component注解可以同时使用,都可以在spring容器中创建Bean组件。运行结果没有差异,并且注入时候俩种方式创建的组件也可以相互注入,@Bean声明组件可以注入到@Component声明的组件,反之也可以。
案例:@Bean声明的组件注入到@Component声明的组件
- 利用@Bean在配置类Config中声明组件
package cn.tedu.context;
import java.util.Date;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;
@Configuration
@ComponentScan(basePackages = "cn.tedu.demo")
public class Config {
@Bean
public Date currentDate() {
return new Date();
}
}
- 利用@Component声明组件,注入Date类型组件
package cn.tedu.demo;
import java.util.Date;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class Employee {
private String name = "Tom";
@Autowired
private Date date;
@Override
public String toString() {
// TODO Auto-generated method stub
return "Employee [name = "+ name + ",date =" + date + "]";
}
}
- 测试案例
@Test
public void testEmployee() {
/*
* 测试@Bean声明的组件注入到@Component声明的组件
*/
Employee employee = ctx.getBean("employee",Employee.class);
System.out.println(employee);
}
测试结果:输出当前系统时间,表示Date注入成功
Employee [name = Tom,date =Mon Jan 03 22:46:10 CST 2022]
案例:@Component声明的组件注入到@Bean声明的组件
- 声明Bean类型
package cn.tedu.demo;
public class Dept {
private String name = "教研部";
private Employee manager;
public void serManager(Employee manager) {
this.manager = manager;
}
@Override
public String toString() {
return "Dept [name=" + name + ", manager=" + manager + "]";
}
}
- 在配置类Config中利用@Bean声明Bean组件
@Bean
public Dept dept(Employee employee) {
Dept dept = new Dept();
dept.serManager(employee);
return dept;
}
- 测试案例
@Test
public void testDept() {
/*
* 测试@Componet 声明的Bean组件注入到@Bean声明的组件中
*/
Dept dept = ctx.getBean("dept",Dept.class);
System.out.println(dept);
}
- 测试结果
Dept [name=教研部, manager=Employee [name = Tom,date =Mon Jan 03 22:59:11 CST 2022]]
5 使用Properties
5.1 Druid连接池
Druid连接池是阿里巴巴提供开源数据库连接池(https://github.com/alibaba/druid/wiki/%E5%B8%B8%E8%A7%81%E9%97%AE%E9%A2%98),是目前被广泛使用的数据库连接池,官方网站是https://github.com/alibaba/druid。
连接池本身也是对象,可以利用Spring IOC容器进行管理,利用Spring IOC管理Druid连接池的步骤是:
- 利用Maven导入Druid连接池API和MySql数据库驱动API
- 将连接池作为JavaBean在Config.java中配置
- 测试连接池是否可用
案例:
- 导入连接池
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.8</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.27</version>
</dependency>
- 配置Config.java
@Bean(initMethod = "init",destroyMethod = "close")
public DataSource dataSource() {
DruidDataSource ds = new DruidDataSource();
ds.setDriverClassName("com.mysql.jdbc.Driver");
ds.setUrl("jdbc:mysql://localhost:3306/mysql?characterEncoding=utf8&useSSL=false&rewriteBatchedStatements=true");
ds.setUsername("root");
ds.setPassword("root");
ds.setMaxActive(10);
ds.setInitialSize(2);
return ds;
}
- 测试
@Test
public void testDataSource() {
/*
* 测试数据是否可以成功连接数据库
*/
String sql = "select 'Hello World!'";
DataSource ds = ctx.getBean("dataSource",DataSource.class);
try (Connection conn = ds.getConnection()){
java.sql.Statement st = conn.createStatement();
ResultSet rs = st.executeQuery(sql);
while (rs.next()) {
System.out.println(rs.getString(1));
}
} catch (Exception e) {
e.printStackTrace();
}
}
测试结果:
信息: {dataSource-1} inited
Hello World!
一月 08, 2022 8:01:50 上午 com.alibaba.druid.support.logging.JakartaCommonsLoggingImpl info
信息: {dataSource-1} closing ...
一月 08, 2022 8:01:50 上午 com.alibaba.druid.support.logging.JakartaCommonsLoggingImpl info
信息: {dataSource-1} closed
5.2 @PropertiesSource和Environment
程序参数经常保存在Properties文件中,Spring提供了@PropertiesSource注解用于读取Properties文件,读取后保存到Environment对象中,可以在程序中注入Environment对象,就可以获得配置文件中的信息了。
案例:
- 在resource文件夹中编写jdbc.properties文件
db.driver=com.mysql.jdbc.Driver
db.url=jdbc:mysql://localhost:3306/mysql?characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true
db.username=root
db.password=root
db.maxActive=10
db.initialSize=2
- 配置Config.java
@Bean(initMethod = "init",destroyMethod = "close")
public DataSource dataSource2() {
DruidDataSource ds = new DruidDataSource();
ds.setDriverClassName(env.getProperty("jdbc.driver"));
ds.setUrl(env.getProperty("db.url"));
ds.setUsername(env.getProperty("db.username"));
ds.setPassword(env.getProperty("db.password"));
ds.setMaxActive(env.getProperty("db.maxActive",Integer.class));
ds.setInitialSize(env.getProperty("db.initialSize",Integer.class));
return ds;
}
- 测试案例
@Test
public void testDataSource2() {
/*
* 测试@PropertiesSource 读取Properties文件
*/
String sql = "select 'Hello World!!'";
DataSource ds = ctx.getBean("dataSource2",DataSource.class);
try (Connection conn = ds.getConnection()){
java.sql.Statement st = conn.createStatement();
ResultSet rs = st.executeQuery(sql);
while (rs.next()) {
System.out.println(rs.getString(1));
}
} catch (Exception e) {
e.printStackTrace();
}
}
测试结果:
Loading class `com.mysql.jdbc.Driver'. This is deprecated. The new driver class is `com.mysql.cj.jdbc.Driver'. The driver is automatically registered via the SPI and manual loading of the driver class is generally unnecessary.
一月 08, 2022 8:39:43 上午 com.alibaba.druid.support.logging.JakartaCommonsLoggingImpl info
信息: {dataSource-1} inited
一月 08, 2022 8:39:43 上午 com.alibaba.druid.support.logging.JakartaCommonsLoggingImpl info
信息: {dataSource-2} inited
Hello World!!
一月 08, 2022 8:39:43 上午 com.alibaba.druid.support.logging.JakartaCommonsLoggingImpl info
信息: {dataSource-1} closing ...
一月 08, 2022 8:39:43 上午 com.alibaba.druid.support.logging.JakartaCommonsLoggingImpl info
信息: {dataSource-1} closed
一月 08, 2022 8:39:43 上午 com.alibaba.druid.support.logging.JakartaCommonsLoggingImpl info
信息: {dataSource-2} closing ...
一月 08, 2022 8:39:43 上午 com.alibaba.druid.support.logging.JakartaCommonsLoggingImpl info
信息: {dataSource-2} closed
5.3 @Value
利用@Value可以读取当前系统环境Environment中的信息,注入到变量中,这个方式更加灵活方便。
${}是Spring提供的表达式,这里可以得到配置文件中的属性信息。
案例:
@Bean(initMethod = "init",destroyMethod = "close")
public DataSource dataSource3(
@Value("${db.driver}") String driver,
@Value("${db.url}") String url,
@Value("${db.username}") String username,
@Value("${db.password}") String password,
@Value("${db.maxActive}")int maxActive,
@Value("${db.initialSize}") int initialSize) {
DruidDataSource ds = new DruidDataSource();
ds.setDriverClassName(driver);
ds.setUrl(url);
ds.setUsername(username);
ds.setPassword(password);
ds.setMaxActive(maxActive);
ds.setInitialSize(initialSize);
return ds;
}
测试案例:
Loading class `com.mysql.jdbc.Driver'. This is deprecated. The new driver class is `com.mysql.cj.jdbc.Driver'. The driver is automatically registered via the SPI and manual loading of the driver class is generally unnecessary.
一月 08, 2022 9:01:08 上午 com.alibaba.druid.support.logging.JakartaCommonsLoggingImpl info
信息: {dataSource-1} inited
一月 08, 2022 9:01:08 上午 com.alibaba.druid.support.logging.JakartaCommonsLoggingImpl info
信息: {dataSource-2} inited
Hello World!!
一月 08, 2022 9:01:08 上午 com.alibaba.druid.support.logging.JakartaCommonsLoggingImpl info
信息: {dataSource-2} closing ...
一月 08, 2022 9:01:08 上午 com.alibaba.druid.support.logging.JakartaCommonsLoggingImpl info
信息: {dataSource-2} closed
一月 08, 2022 9:01:08 上午 com.alibaba.druid.support.logging.JakartaCommonsLoggingImpl info
信息: {dataSource-1} closing ...
一月 08, 2022 9:01:08 上午 com.alibaba.druid.support.logging.JakartaCommonsLoggingImpl info
信息: {dataSource-1} closed
一月 08, 2022 9:01:08 上午 com.alibaba.druid.support.logging.JakartaCommonsLoggingImpl info
信息: {dataSource-0} closing ...