Spring: IOC & DI

张天宇 on 2020-03-04

Spring中的控制反转和依赖注入。

耦合与解耦

耦合:程序之间的依赖关系,包括类之间的依赖和方法中的依赖。

解耦:降低程序之间的依赖关系。

实际开发中应该做到:编译器不依赖,运行时才依赖。

解耦思路:

  • 1.使用反射来创建对象,避免使用new关键字。

  • 2.通过读取配置文件来读取要创建的对象全限定类名。

实例1:JDBC驱动注册

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//常规
//1.注册驱动(独立性差)
DriverManager.registerDriver(new com.mysql.jdbc.Driver());
//2.获取连接
Connection conn=DriverManager.getConnection("jdbc:mysql://localhost:3306/test""root""root");
//3.获取数据库的预处理对象
PreparedStatement pstm=conn.prepareStatement("select * from account");
//4.执行SQL语句,得到结果集
ResultSet rs=pstm.executeQuery();
//5.遍历结果集
while (rs.next()){
System.out.println(rs.getString("name"));
}
//6.释放资源
rs.close();
pstm.close();
conn.close();
1
2
//通过反射加载驱动类
Class.forName("com.mysql.jdbc.Driver");

实例2:三层框架解耦

表现层调用业务层,业务层调用持久层。

1
2
3
4
5
6
public class Client {
public static void main(String[] args) {
IAccountService as=new AccountServiceImpl();
as.saveAccount();
}
}

业务层依赖持久层的接口和实现类,若编译时不存在没有持久层实现类,则编译期不能通过,造成了编译期依赖。

使用工厂模式解耦

Bean:在计算机英语中有可重用组件的含义。

JavaBean:用Java语言编写的可重用组件。不等于实体类,远大于实体类。

Bean工厂:创建service和dao对象。

  • 1.需要一个配置文件配置service和dao。配置文件内容:唯一标识=全限定类名(key-value),xml或者properties。
  • 2.通过读取配置文件中的内容,反射创建对象。

在实际开发中可以把三层的对象的全类名都使用配置文件保存起来,当启动服务器应用加载的时候,创建这些对象的实例并保存在容器中。在获取对象时,不使用new的方式,而是直接从容器中获取,这就是工厂设计模式

Spring中的IOC

降低程序间的耦合性(依赖关系)

Spring Framework Core:https://docs.spring.io/spring/docs/5.2.4.RELEASE/spring-framework-reference/core.html#spring-core

项目创建

  1. 创建Maven项目,配置pom.xml。
1
2
3
4
5
6
7
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.0.2.RELEASE</version>
</dependency>
</dependencies>
  1. 创建框架,三层接口类和实现类。

  2. 配置bean文件。XML格式,可通过上述文档查找:

1
2
3
4
5
6
7
8
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd">
<!--把对象的创建交给Spring来管理-->
<bean id="accountService" class="com.itheima.service.impl.AccountServiceImpl"></bean>
</beans>
  1. 在表现层中通过容器创建对象,核心容器的getBean()方法获取具体对象。
1
2
3
4
5
6
7
8
9
10
11
12
public class Client {
public static void main(String[] args) {
// 获取核心容器对象
ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
// 根据id获取Bean对象
IAccountService as = (IAccountService)ac.getBean("accountService");
//或者不强转,传入字节码
IAccountService as = ac.getBean("accountService", IAccountService.class);
// 执行as的具体方法
// ...
}
}

常用容器

  • ClassPathXmlApplicationContext: 它是从类的根路径下加载配置文件(常用)
  • FileSystemXmlApplicationContext: 它是从磁盘路径上加载配置文件(必须有访问权限)
  • AnnotationConfigApplicationContext: 读取注解创建容器

核心容器两个接口引发出的问题

  • ApplicationContext: 它在构建核心容器时,创建对象采取的策略是采用立即加载的方式。也就是说一读取完配置文件马上创建配置文件中配置的对象。单例对象适用。
  • BeanFactory: 它在构建核心容器时,创建对象采取的策略是延迟加载的方式,也就是说什么时候根据ID获取对象了什么时候才真正的创建对象。多例对象适用。

Bean的细节

Bean实例化的三种方式
  • 使用默认无参构造函数创建对象:默认情况下会根据默认无参构造函数来创建类对象,若Bean类中没有默认无参构造函数,将会创建失败。

    1
    <bean id="accountService" class="cn.maoritian.service.impl.AccountServiceImpl"></bean>
  • 使用普通工厂的方法创建对象(使用某个类中的方法创建对象并存入Spring容器):

    1
    2
    3
    4
    5
    6
    //工厂,即我们需要使用createAccountService()来实例化一个IAccountService
    public class InstanceFactory {
    public IAccountService createAccountService(){
    return new AccountServiceImpl();
    }
    }

    先创建实例工厂对象instanceFactory,通过调用其createAccountService()方法创建对象。

    1
    2
    3
    4
    5
    6
    7
    8
    <bean id="instancFactory" class="cn.maoritian.factory.InstanceFactory"></bean>
    <bean id="accountService"
    factory-bean="instancFactory"
    factory-method="createAccountService"></bean>
    <!--
    factory-bean属性: 指定实例工厂的id
    factory-method属性: 指定实例工厂中生产对象的方法
    -->
  • 使用工厂的静态方法创建对象:

    1
    2
    3
    4
    5
    public class StaticFactory {
    public static IAccountService createAccountService(){
    return new AccountServiceImpl();
    }
    }

使用StaticFactory中的静态方法createAccountServic()创建对象。

1
<bean id="accountService" class="cn.maoritian.factory.StaticFactory" factory-method="createAccountService"></bean>
Bean的作用范围和生存周期
单例对象
1
<bean id="accountService" class="cn.maoritian.factory.StaticFactory" factory-method="createAccountService" scope="singleton"></bean>
  • 出生:当容器创建时对象出生
  • 活着:只要容器还在,对象一直活着
  • 死亡:容器销毁,对象消亡
  • 总结:单例对象的生命周期和容器相同
多例对象
1
<bean id="accountService" class="cn.maoritian.factory.StaticFactory" factory-method="createAccountService" scope="prototype"></bean>
  • 出生:当使用对象时,Spring框架创建
  • 活着:对象在使用过程中一直活着
  • 死亡:当对象长时间不用,且没有别的对象引用时,由Java的垃圾回收器回收
Bean标签

配置托管给spring的对象,默认情况下调用类的无参构造函数,如果没有无参构造函数则创建失败。

  • id:指定对象在容器中的标识,将其作为参数传入getBean()方法可以获取获取对应对象。
  • class:指定类的全类名,默认情况下调用无参构造函数。
  • scope:指定对象的作用范围,可选值:
    • singleton:单例对象,默认值
    • prototype:多例对象
    • request:将对象存入到web项目的request域
    • session:将对象存入到web项目的session域
    • global session:将对象存入到web项目集群的session域中,若不存在集群,则global session相当于session
  • init-method:指定类中的初始化方法名称,在对象创建成功之后执行
  • destroy-method:指定类中销毁方法名称,对prototype多例对象没有作用,因为多利对象的销毁时机不受容器控制

Spring中的DI

依赖关系的维护,叫依赖注入。

注入的数据

  • 基本类型和String(经常变化的数据并不适用注入的方式,如数据库的账号和密码)
  • 其他Bean类型(在配置文件中或者注释配置过的Bean)
  • 复杂类型/集合类型

注入的方式

  • 使用构造函数注入

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    //对有参构造函数进行注入
    public class AccountServiceImpl implements IAccountService{
    private String name;
    private Integer age;
    private Date birthday;

    public AccountServiceImpl(String name, Integer age, Date birthday) {
    this.name = name;
    this.age = age;
    this.birthday = birthday;
    }

    public void saveAccount() {
    System.out.println("service中save"+name+","+age+","+birthday);
    }
    }

    使用的标签constructor-arg,属性有:

    • type:指定要注入的数据类型,该数据类型同时也是构造函数中某个或者某些参数的类型
    • index:用于指定要注入的数据给构造函数中指定索引位置的参数赋值,从0开始
    • name:用于指定给构造函数中指定名称的参数赋值(常用)
    • value:用于提供基本类型和String类型的数据
    • ref:用于指定其他的Bean类型数据,在Spring的IOC核心容器中出现过的Bean对象
    1
    2
    3
    4
    5
    6
    7
    <bean id="accountService" class="factory.service.AccountServiceImpl">
    <constructor-arg name="name" value="test"></constructor-arg>
    <constructor-arg name="age" value="15"></constructor-arg>
    <constructor-arg name="birthday" ref="now"></constructor-arg>
    </bean>
    <!-- 配置一个Bean -->
    <bean id="now" class="java.util.Date"></bean>

    优势:在获取Bean对象时,注入数据是必须的,否则无法创建成功。

    弊端:改变了Bean对象的实例化方式,使我们在创建对象时,用不到这些数据也必须提供。

    在无可奈何时候才使用。

  • 使用Set方法注入(更常用)

    使用的标签property,属性有:

    • name:用于指定注入时所调用的Set方法名称
    • value:用于提供基本类型和String类型的数据
    • ref:用于指定其他的Bean类型数据,在Spring的IOC核心容器中出现过的Bean对象
    1
    2
    3
    4
    5
    6
    <!-- name后面接setter函数名字,如setUsername的username,与变量名无关 -->
    <bean id="accountService2" class="factory.service.AccountServiceImpl2">
    <property name="name" value="test"></property>
    <property name="age" value="21"></property>
    <property name="birthday" ref="now"></property>
    </bean>

    优势:创建对象时没有明确的限制,可以直接使用默认构造函数。

    弊端:如果有某个成员必须有值,但获取对象时set方法没有执行。

    注入集合数据:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    public class AccountServiceImpl3 implements IAccountService{
    private String[] myStrs;
    private List<String> myList;
    private Set<String> mySet;
    private Map<String,String> myMap;
    private Properties myProps;

    public void setMyStrs(String[] myStrs) {
    this.myStrs = myStrs;
    }

    public void setMyList(List<String> myList) {
    this.myList = myList;
    }

    public void setMySet(Set<String> mySet) {
    this.mySet = mySet;
    }

    public void setMyMap(Map<String, String> myMap) {
    this.myMap = myMap;
    }

    public void setMyProps(Properties myProps) {
    this.myProps = myProps;
    }

    public void saveAccount() {
    System.out.println(Arrays.toString(myStrs));
    System.out.println(myList);
    System.out.println(mySet);
    System.out.println(myMap);
    System.out.println(myProps);
    }
    }

    用于给List结构集合注入的标签:list, array, set

    用于Map结构集合注入的标签:map, props

    结构相同,标签可以互换。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    <bean id="accountService3" class="factory.service.AccountServiceImpl3">
    <property name="myStrs">
    <array>
    <value>AAA</value>
    <value>BBB</value>
    <value>CCC</value>
    </array>
    </property>
    <property name="myList">
    <list>
    <value>AAA</value>
    <value>BBB</value>
    <value>CCC</value>
    </list>
    </property>
    <property name="mySet">
    <set>
    <value>AAA</value>
    <value>BBB</value>
    <value>CCC</value>
    </set>
    </property>
    <property name="myMap">
    <map>
    <entry key="testaA" value="aaa"></entry>
    <entry key="testB">
    <value>bbb</value>
    </entry>
    </map>
    </property>
    <property name="myProps">
    <props>
    <prop key="testC">ccc</prop>
    <prop key="testD">ddd</prop>
    </props>
    </property>
    </bean>
  • 使用注解提供

基于注解的IOC配置

注解和XML的选择原则:使用哪个最方便就是用哪种。

配置bean.xml

1
2
3
4
5
6
7
8
9
10
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
https://www.springframework.org/schema/context/spring-context.xsd">
<context:component-scan base-package="factory"></context:component-scan>
</beans>

配置标签

  • 用于创建对象

    @Component:把当前类对象存入Spring容器中。属性value用于指定bean的id,不写时默认当前类名首字母改小写。

    以下三个注解的作用和属性和Component一模一样,为Spring框架提供的明确的三层使用的注释,使三层对象更加清晰。

    @Controller:一般用在表现层

    @Service:一般用在业务层

    @Repository:一般用在持久层

  • 用于注入对象

    @Autowired:自动按照类型注入。只要容器中有唯一的一个bean对象类型和要注入的变量类型匹配,就可以诸如成功。出现位置可以是成员变量,也可以是方法上。如果IOC有多个类型匹配时,首先按照类型框定匹配的对象,再按照变量名称作为bean的id继续查找,如果有一样的也可以注入成功,没有注入失败。使用注解时,set方法不是必须的。

    @Qualifier:在按照类中注入的基础上再按照名称注入。它在给类成员注入时不能单独使用要和@Autowired配合使用,但是给方法参数注入时可以。value用于指定注入bean的id。

    @Resource:直接按照bean的id注入,可以直接使用,不依托@Autowired使用。name用于指定注入的bean的id。

    以上三个注入都只能注入其他bean类型的数据,而基本类型和String类型无法使用上述注解实现。此外,集合类型的注入只能通过XML来实现。

    @Value:用于注入基本类型和String类型的数据。属性value用于指定数据的值,可以使用Spring中的SpEL,即Spring的el表达式(${表达式})。

  • 用于改变作用范围

    @Scope:用于指定bean的作用范围。属性value指定范围的取值,常用取值:singletonprototype

  • 和生命周期相关

    @PreDestroy:指定销毁方法。

    @PostConstruct:指定初始化方法。

示例:

1
2
3
4
5
6
7
8
9
10
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
https://www.springframework.org/schema/context/spring-context.xsd">
<context:component-scan base-package="factory"></context:component-scan>
</beans>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
@Repository
public class AccountDaoImpl implements IAccountDao {
public void saveAccount() {
System.out.println("保存了账户");
}
}
@Service
public class AccountServiceImpl implements IAccountService{
@Resource(name="accountDaoImpl")
private IAccountDao accountDao;
public void saveAccount() {
accountDao.saveAccount();
}
@PostConstruct
public void init(){
System.out.println("初始化方法执行了");
}
@PreDestroy
public void destroy(){
System.out.println("销毁方法执行了");
}
}
public class Client {
public static void main(String[] args) {
ClassPathXmlApplicationContext ac= new ClassPathXmlApplicationContext("bean.xml");
IAccountService as= (IAccountService) ac.getBean("accountServiceImpl");
as.saveAccount();
ac.close();
}
}
/**
* 结果:初始化方法执行了
* 保存了账户
* 销毁方法执行了
**/

Spring中的新注解

  • @Configuration:指定当前类是一个配置类,作用和bean.xml是一样的。当配置类作为AnnotationConfigApplicationContext对象创建的参数时,该注解可以不写,同理下面的扫包也不用写了。

    1
    ApplicationContext ac=new AnnotationConfigApplicationContext(SpringConfig.class);
  • @ComponentScan:用于通过注解指定Spring在创建容器时要扫描的包。value属性和basePackage的作用是一样的。都是用于指定创建容器时要扫描的包。使用此注解等同于在xml中配置了:

    1
    <context:component-scan base-package="com.ztygalaxy"></context:component-scan>
  • @Bean:用于把当前方法的返回值作为bean对象存入spring的IOC容器中。name属性用于指定bean的id,不写时默认值是当前方法的名称。当使用注解配置方法时,如果方法有参数,Spring框架会去容器中查找是否有可用的bean对象。查找方式和Autowired注解的作用是一样的,如果唯一类型匹配,多个按名称匹配。如示例中的dataSource。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
//配置类
@Configuration
@ComponentScan(basePackages = {"com.ztygalaxy"})
public class SpringConfig {
@Bean(name="runner")
@Scope("prototype")//单例
public QueryRunner createQueryRunner(DataSource dataSource){
return new QueryRunner(dataSource);
}
@Bean(name="dataSource")
public DataSource createDataSource(){
try {
ComboPooledDataSource ds = new ComboPooledDataSource();
ds.setDriverClass("com.mysql.jdbc.Driver");
ds.setJdbcUrl("jdbc:mysql://localhost:3306/test");
ds.setUser("root");
ds.setPassword("root");
return ds;
}catch(Exception e){
throw new RuntimeException(e);
}
}
}

//使用
ApplicationContext ac=new AnnotationConfigApplicationContext(SpringConfig.class);
IAccountService as= (IAccountService) ac.getBean("accountService");
  • @Import:导入其他的配置类,使用import之后,有import注解的类就是主配置类(父配置类),而导入的都是子配置类。

    1
    @Import(JdbcConfig.class)
  • @PropertySource:用于指定properties文件的位置。属性value指定文件名称和文件路径,关键字classpath表示类路径下。

    1
    2
    @PropertySource("classpath:config/spring/jdbcConfig.properties")//有包就写包名
    @PropertySource("classpath:jdbcConfig.properties")
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    public class JdbcConfig {
    @Value("${jdbc.driver}")
    private String driver;
    @Value("${jdbc.url}")
    private String url;
    @Value("${jdbc.username}")
    private String username;
    @Value("${jdbc.password}")
    private String password;
    @Bean(name="runner")
    @Scope("prototype")
    public QueryRunner createQueryRunner(DataSource dataSource){
    return new QueryRunner(dataSource);
    }
    @Bean(name="dataSource")
    public DataSource createDataSource(){
    try {
    ComboPooledDataSource ds = new ComboPooledDataSource();
    ds.setDriverClass(driver);
    ds.setJdbcUrl(url);
    ds.setUser(username);
    ds.setPassword(password);
    return ds;
    }catch(Exception e){
    throw new RuntimeException(e);
    }
    }
    }
    1
    2
    3
    4
    5
    #jdbc.properties
    jdbc.driver=com.mysql.jdbc.Driver
    jdbc.url=jdbc:mysql://localhost:3306/test
    jdbc.username=root
    jdbc.password=root

Junit

Junit单元测试中,没有main方法也能执行。Junit集成了一个main方法,该方法会自动判断当前测试类中有哪些方法有@Test注解,Junit就让有@Test注解的方法执行。在执行测试方法时,Junit不知道是否使用了Spring框架,所以也就不会为我们读取配置文件或者配置类创建Spring核心容器。因此,当测试方法执行时,没有IOC容器,就算写了@Autowired也无法实现注入。

Spring整合Junit的配置

  1. 导入Spring整合Junit的Jar包(坐标)
1
2
3
4
5
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>5.0.2.RELEASE</version>
</dependency>
  1. 使用Junit提供的注解把原有的main方法替换成spring提供的
1
@RunWith(SpringJUnit4ClassRunner.class)
  1. 告知Spring的运行器,spring合IOC创建是基于xml还是注解的,并告诉位置。
1
2
3
4
//1. location:指定xml文件位置,加上classpath表示在类路径下
@ContextConfiguration(locations = "classpath:bean.xml")
//2. classes:指定注解类所在位置
@ContextConfiguration(classes = SpringConfig.class)

当使用Spring 5.x版本时,要求Junit的jar包是4.1.2及以上。

示例:

  • 版本1

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    public class AccountService {
    @Test
    public void testFindAll(){
    ApplicationContext ac=new AnnotationConfigApplicationContext(SpringConfig.class);
    IAccountService as= (IAccountService) ac.getBean("accountService");
    //上面两行在每个函数中都重复出现
    List<Account> accounts=as.findAllAccount();
    for (Account account:accounts)
    System.out.println(account);
    }
    //省略多个函数
    }
  • 版本2

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    public class AccountService {
    private ApplicationContext ac;
    private IAccountService as;
    @Before
    public void init(){//抽成init函数
    ac=new AnnotationConfigApplicationContext(SpringConfig.class);
    as= (IAccountService) ac.getBean("accountService");
    }
    @Test
    public void testFindAll(){
    List<Account> accounts=as.findAllAccount();
    for (Account account:accounts)
    System.out.println(account);
    }
    //省略多个函数
    }
  • 版本3

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
     使用Spring装填
    @RunWith(SpringJUnit4ClassRunner.class)
    @ContextConfiguration(classes = SpringConfig.class)
    public class AccountService {
    @Autowired
    private IAccountService as;
    @Test
    public void testFindAll(){
    List<Account> accounts=as.findAllAccount();
    for (Account account:accounts)
    System.out.println(account);
    }
    //省略多个函数
    }