概述

Spring 是分层的JavaSE/EE应用 full-stack 轻量级开源框架,以Ioc(Inverse Of Control:反转控制)和AOP(Aspect Oriented Programming:面向切面编程)为内核,提供了展现层Spring MVC和持久层Spring JDBC以及业务层事务管理等众多的企业级应用技术,还能整合开源世界众多著名的第三方框架和类库。

1、IoC的概念和作用

IoC(inversion of Control):其思想是反转资源获取的方向。传统的资源查找方式要求组件向容器发起请求查找资源。作为回应,容器适时的返回资源。而应用了IOC之后,则是容器主动地将资源推送给他所管理的组件,组件所要做的仅仅是选择一种合适的方式来接受资源。这种行为也被称为查找的被动形式。

DI(Dependency Injection)——IOC的另一种表述方式:即组件以一些预先定义好的方式(例如setter方法)接受来自如容器的资源注入

1.1、程序间的耦合和解耦

程序的耦合:

  • 耦合:程序间的依赖关系
    包括:
  • 类之间的依赖
  • 方法间的依赖
  • 解耦:
    降低程序间的依赖关系
    实际开发中:
    应该做到:编译期不依赖,运行时才依赖
    解耦的思路:
    第一步:使用反射来创建对象,而避免使用new关键字
    DriverManager.registerDriver(new oracle.jdbc.OracleDriver()); ==> Class.forName("oracle.jdbc.OracleDriver")
    第二步:通过读取配置文件来获取要创建的对象全限定类名

1.2、Spring的IoC原理

1.2.1、手动创建一个简单的Bean工厂解耦

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

JavaBean:用java编写的可重用组件。javaBean的范围远远大于实体类。
编写一个创建bean对象的工厂,利用Bean工厂创建service和dao对象:

  • 需要一个配置文件来配置我们的service和dao
    配置文件的内容为: 唯一标识=全限定类名 (key=value)
  • 通过读取配置文件中配置的内容,反射创建对象
    配置文件可以是xml或properties文件

示例程序:

public class BeanFactory {

    // 定义一个Properties对象
    private static Properties props;

    // 使用静态代码块为Properties对象赋值
    static {
        try {
            // 实例化对象
            props = new Properties();
            // 获取properties文件的流对象;
            // 此处使用BeanFactory.class.getClassLoader().getResourceAsStream("bean.properties")获取路径
            // 获取与class同级目录的resources中的bean.properties文件
            InputStream in = BeanFactory.class.getClassLoader().getResourceAsStream("bean.properties");
            props.load(in);
        } catch (Exception e) {
            // 抛出一个error错误
            throw new ExceptionInInitializerError("初始化properties失败");
        }
    }

    /**
     * 根据bean名称获取bean对象
     */
    public static Object getBean(String beanName) {
        Object bean = null;
        try {
            String beanPath = props.getProperty(beanName);
            bean = Class.forName(beanPath).newInstance();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return bean;
    }
}

在resource文件夹下创建bean.properties配置文件

accountService=com.study.service.impl.AccountServiceImpl
accountDao=com.study.dao.impl.AccountDaoImpl

在需要使用bean的位置,利用BeanFactory创建bean

private IAccountDao accountDao = (IAccountDao) BeanFactory.getBean("accountDao");
IAccountService as = (IAccountService) BeanFactory.getBean("accountService");

1.2.2、使用容器将工厂修改为单例模式

单例模式时:类对象只会被创建一次,从而类中的成员也就只会初始化一次。
多例模式时:类对象会被多次创建,执行效率没有单例对象高。

在bean工厂中创建一个容器。bean初始化执行静态代码块时,为Properties配置文件中所有配置的bean都初始化出一个对应的对象,存储在容器中,后续根据配置取对象时直接从容器中取这个单例的对象。

public class BeanFactory {

    // 定义一个Properties对象
    private static Properties props;

    // 定义一个map,用于存放我们创建的对象。我们把它称为容器
    private static Map<String, Object> beans;

    static {
        try {
            props = new Properties();
            InputStream in = BeanFactory.class.getClassLoader().getResourceAsStream("bean.properties");
            props.load(in);

            // 实例化容器
            beans = new HashMap<String, Object>();
            // 取出配置文件中所有的key
            Enumeration keys = props.keys();
            while(keys.hasMoreElements()) {
                String key = keys.nextElement().toString();
                String beanPath = props.getProperty(key);
                Object value = Class.forName(beanPath).newInstance();
                beans.put(key, value);
            }
        } catch (Exception e) {
            // 抛出一个error错误
            throw new ExceptionInInitializerError("初始化properties失败");
        }
    }

    /**
     * 根据bean名称获取单例的bean对象
     * @param beanName
     * @return
     */
    public static Object getBean(String beanName) {
        return beans.get(beanName);
    }
}

1.2.3、控制反转——Inversion Of Control

控制反转(Inversion Of Control,缩写Ioc):把创建对象的权利交给框架。包括依赖注入(Dependency Injection,缩写DI)和依赖查找(Dependency Lookup)。

在上面的工厂示例中:将原来在具体业务逻辑中可以自主new出来的service和dao对象,改成在工厂中进行控制,具体业务逻辑中直接读取bean工厂中生成的对象。控制权发生了转移,就叫控制反转。

IoC的作用:削减计算机程序间的耦合(解除代码中的依赖关系)。

2、Spring中的IoC

2.1、Spring-framework源码文件结构

  • docs
    spring-framework的说明文档
  • libs
    spring-framework的jar包。
    每个jar包都分为三个:
  • xxx.jar:可以直接引用的jar包
  • xxx-javadoc.jar:该jar的说明文档
  • xxx-sources.jar:源码
  • schema
    xml的schema约束

2.2、Bean工厂示例修改为spring方式

2.2.1、引入spring的pom依赖

引入spring-context的依赖

<dependencies>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>5.0.2.RELEASE</version>
    </dependency>
</dependencies>

引入maven依赖后,libraries中会出现6个jar包:

  • spring-aop
    使用注解时需要使用该jar包。如果是手动导入的jar包,在不使用注解时可以不导入该jar包。
  • spring-jcl
    commons-loggin.jar,内容为:org.apache.commons.logging
  • spring-beans、spring-context、spring-core、spring-expression
    spring的核心jar包

2.2.2、编写xml配置文件

在resources文件夹中创建bean.xml配置文件。
bean.xml可以为任意文件名,后期会使用applicationContext.xml。
xml引入的约束可以在spring-framework的doc文档中找到。

<?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
        http://www.springframework.org/schema/beans/spring-beans.xsd">

    <!-- 配置bean的唯一标识和全限定类名 -->
    <bean id="accountService" class="com.study.service.impl.AccountServiceImpl">
    </bean>
    <bean id="accountDao" class="com.study.dao.impl.AccountDaoImpl">
    </bean>
</beans>

2.2.3、在main方法中加载配置创建容器获取bean

使用ApplicationContext创建bean对象

public class Client {
    public static void main(String[] args) {

        // 获取spring的IoC核心容器
        ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");

        // 从容器中获取指定id的bean对象
        // 可以进行强制转换,也可以在第二个参数中传入指定要获取的bean的类型
        IAccountService as = (IAccountService) ac.getBean("accountService");
        IAccountDao ad = ac.getBean("accountDao", IAccountDao.class);

        System.out.println(as);
        System.out.println(ad);
    }
}

使用BeanFactory创建对象示例:

Resource resourse = new ClassPathResource("bean.xml");
BeanFactory factory = new XmlBeanFactory(resourse);
IAccountService as = factory.getBean("accountService", IAccountService.class);
IAccountDao ad = factory.getBean("accountDao", IAccountDao.class);
System.out.println(as);
System.out.println(ad);

ApplicationContext常用的三个实现类:

  • ClassPathXmlApplicationContext:从类路径下加载配置文件,要求配置文件必须在类路径下(最常用)
  • FileSystemXmlApplicationContext:从磁盘任意路径加载有访问权限的配置文件(即绝对路径)
  • AnnotationConfigApplicationContext:用于读取注解创建容器

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

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

BeanFactory是个顶层接口,实际中更经常使用ApplicationContext。ApplicationContext可以根据我们的配置去自动选择采用延迟或立即加载。

2.3、spring中bean的管理细节

2.3.1、bean的创建方式

  • 使用默认构造函数创建
    在spring的配置文件中使用标签,配以id和class属性之后,且没有其他属性和标签时。采用的就是默认构造函数创建bean对象,此时如果类中没有默认构造函数,则对象无法创建。
  • 使用普通工厂中的方法创建对象
    使用另一个类中的方法创建需要的类的对象,并存入spring容器。
    InstanceFactory.java
    bean.xml
  • 使用工厂中的静态方法创建对象
    使用另一个类中的静态方法创建需要的类的对象,并存入spring容器。
    StaticFactory.java
    bean.xml

2.3.2、bean对象的作用范围调整

bean标签的scope属性:

  • 作用:用于指定bean的作用范围
  • 取值:常用的值为:singleton、prototype
  • singleton:单例的(默认值)
  • prototype:多例的
  • request:作用于web应用的请求范围
  • session:作用于web应用的会话范围
  • global-session:作用于集群环境的会话范围(全局会话范围)。当不是集群环境时,它和session等效。
<!-- 使用多例方式 -->
<bean id="accountService" class="com.study.service.impl.AccountServiceImpl" scope="prototype">
</bean>

2.3.3、bean对象的生命周期

单例对象:

  • 出生:当容器创建时对象出生
  • 活着:只要容器还在,对象一直活着
  • 死亡:容器销毁,对象消亡
  • 总结:单例对象的生命周期和容器相同

多例对象:

  • 出生:当我们使用对象时spring框架为我们创建
  • 活着:对象在使用过程中就一直活着
  • 死亡:当对象长时间不用且没有别的对象引用时,由java的垃圾回收器回收。

示例程序:
AccountSercviceImpl.java

public void saveAccount() {
    System.out.println("service中的saveAccount执行了");
}

public void init() {
    System.out.println("init 方法执行了.");
}

public void  destroy() {
    System.out.println("destroy方法执行了");
}

bean.xml

<!-- init-method:对象创建时执行的方法
	 destroy-method:对象销毁时执行的方法 -->
<bean id="accountService"
      class="com.study.service.impl.AccountServiceImpl"
      init-method="init"
      destroy-method="destroy"
      >
</bean>

Client.java

public static void main(String[] args) {

    // ClassPathXmlApplicationContext有close()方法,
    // 但是接口ApplicationContext没有该方法
    ClassPathXmlApplicationContext ac = 
        new ClassPathXmlApplicationContext("bean.xml");
    IAccountService as = ac.getBean("accountService", IAccountService.class);
    System.out.println(as);

    // 如果不手动关闭容器,程序运行完时main方法进程就关闭了,就看不到容器关闭的打印语句了
    ac.close();
}

2.4、依赖注入

Dependency Injection,简称DI。

依赖关系的管理以后都交给spring维护。在当前类需要用到其他类的对象,由spring为我们提供,我们只需要在配置文件中说明。这种依赖关系的维护就叫做依赖注入。

依赖注入能注入的数据有三类:

  • 基本类型和String
  • 其他bean类型(在配置文件中或注解配置过的bean)
  • 复杂类型/集合类型

依赖注入的方式有三种:

  • 使用构造函数提供
  • 使用set方法提供
  • 使用注解提供

2.4.1、构造函数注入

AccountServiceImpl.java

public class AccountServiceImpl implements IAccountService {

    // 如果是经常变化的数据,并不适用于注入的方式
    private String name;  // String类型
    private Integer age;  // 基本数据类型的包装类型
    private Date birthday;  // 其他bean类型

    // 构造函数中需要传入相关变量
    public AccountServiceImpl(String name, Integer age, Date birthday) {
        this.name = name;
        this.age = age;
        this.birthday = birthday;
    }

    public void saveAccount() {
        System.out.println("service中的saveAccount执行了" + this.toString());
    }

    @Override
    public String toString() {
        return "AccountServiceImpl{" + "name='" + name + '\'' + ", age=" + age + ", birthday=" + birthday + '}';
    }
}

在bean.xml中的标签下使用标签通过构造函数注入相关变量的值:

<constructor-arg>标签:
 
● 出现的位置:<bean>标签的内部
● 标签中的属性: 
  ○ type:用于指定要注入的数据的数据类型,该数据类型也是构造函数中某个或某些参数的类型。(当构造函数中有多个类型相同的参数时,使用type无法区分)
  ○ index:用于给构造函数中指定索引位置的参数赋值,索引位置从0开始。(使用前要知道该类的构造函数中每个参数的位置,使用不太方方便)
  ○ name:用于给构造函数中指定名称的参数赋值。(经常用)
  ○ value:用于提供基本类型、基本类型包装类型和String类型的数据
  ○ ref:用于指定其他的bean类型数据(在Spring的IoC核心容器中出现过的bean对象)
 
● 也可以在constructor-arg标签内嵌入value标签来赋值
● 如果value值带有特殊符号,使用<![CDATA[]]>包裹起来
● 优势:在获取bean对象时,注入数据是必须的操作,否则对象无法创建。
● 弊端:改变了bean对象的实例化方式,使我们在创建对象时如果用不到这些数据也必须提供。


<bean id="accountService" class="com.study.service.impl.AccountServiceImpl">
    <!-- String类型的name变量可以直接注入值 -->
    <constructor-arg name="name" value="test"></constructor-arg>
    <!-- Integer类型的age变量,Spring会自动将值转换为需要的类型 -->
    <constructor-arg name="age" value="18"></constructor-arg>
    <!-- 复杂类型的变量,需要通过ref映射到容器中出现过的bean对象 -->
    <constructor-arg name="birthday" ref="nowDate"></constructor-arg>
    <constructor-arg name="desc">
        <!-- 属性值可以使用value节点进行配置 -->
        <!-- 赋值带有特殊符号的<shanghai^..>,需要使用<![CDATA[]]>包裹起来 -->
        <value><![CDATA[<shanghai^..>]]></value>
    </constructor-arg>
</bean>

<bean id="nowDate" class="java.util.Date"></bean>

2.4.2、set方法注入

<property>标签:
●  出现的位置:<bean>标签的内部
●  标签的属性:
  ○ name:用于指定注入时所调用的set方法名称(并不关注实际变量名称,只关注set方法名称。如果变量age的set方法为setUseAge(int age),则name应该为userAge)
  ○ value:用于提供基本类型和String类型的数据
  ○ ref:用于指定其他的bean类型数据(在Spring的IoC核心容器中出现过的bean对象)
●  null值可以使用专用的<null/>标签进行赋值 
●  引用类型的变量,可以在标签内再嵌入一个<bean>标签作为内部bean,内部Bean不能在其他地方调用
●  可以使用级联赋值
●  优势:创建对象时没有明确的限制,可以直接使用默认构造函数。
●  弊端:如果有某个成员必须有值,则获取对象时set方法无法保证该成员一定已经赋值。

<property name="car">
    <bean class="com.study.Person">
        <property name="name" value="张三" />
    </bean>
</property>

<!-- 需要先构造car,否则会出现空指针异常 -->
<property ref="car" />
<property name="car.maxSpeed" value="200"/>

2.4.3、注入集合数据

使用属性的setter方法给集合类型的属性注入值。

public class AccountServiceImpl3 implements IAccountService {

    // 数组、Set集合、List集合
    private String[] myStrs;
    private List<String> myList;
    private Set<String> mySet;
    
    // Map集合、Properties键值对
    private Map<String,String> myMap;
    private Properties myProperties;

    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 setMyProperties(Properties myProperties) {
        this.myProperties = myProperties;
    }

    public void saveAccount() {}

}
<bean id="accountService3" class="com.study.service.impl.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>
            <!-- map标签下的entry标签,value可以作为标签属性,也可以作为子标签 -->
            <entry key="key1" value="aaa"></entry>
            <entry key="key2">
                <value>bbb</value>
            </entry>
            <entry key="key3" value="ccc"></entry>
        </map>
    </property>
    <property name="myProperties">
        <props>
            <prop key="key1">aaa</prop>
            <prop key="key2">bbb</prop>
            <prop key="key3">ccc</prop>
        </props>
    </property>
</bean>
<set>、<list>、<array>三种类型的数据的结构相同,三个标签可以任意互换;
<map>、<props>标签的数据结构相同,这两个标签可以互换使用。

3、基于注解的IoC配置

注解配置和xml配置实现的功能是一样的,都是为了降低程序间的耦合,只是配置的形式不一样。

3.1、配置xml扫描指定包

要使用注解,需要在xml中配置容器扫描指定包里面的类上的注解
该项配置所需的标签不在beans的约束中,而是一个名称为context的名称空间的约束中。该约束可以从spring官方文档中复制
bean.xml

<?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
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context.xsd">

    <!--  告知spring在创建容器时要扫描的包,但是这个配置所需的标签不在beans的约束中,而是一个名称为context名称空间约束中  -->
    <context:component-scan base-package="com.study"></context:component-scan>
</beans>

3.2、用于创建对象的注解

这些注解的作用和在xml中配置标签的作用一样。

  • @Component
    作用: 用于将当前类对象存入Spring容器中
    属性:value属性用于指定存入spring容器中的bean的id,不配置时默认为:当前类的类名首字母转小写
  • @Controller
    一般用在表现层。
    作用、属性同@Component完全一样。
  • @Service
    一般用在业务层。
    作用、属性同@Component完全一样。
  • @Repository
    一般用在持久层。
    作用、属性同@Component完全一样。

@Controller、@Service、@Repository三个注解的作用、属性同@Component完全一样。
他们三个只是Spring框架为我们提供明确的三层,使我们的三层对象更清晰。

@Service("accountService")
public class AccountServiceImpl implements IAccountService {

    private IAccountDao accountDao;

    public void saveAccount() {
        accountDao.saveAccount();
    }
}

3.3、用于注入数据的注解

这些注解的作用和xml配置的标签下的标签的作用是一样的。

  • @Autowired
    作用:
    自动按照类型注入,只要容器中有唯一的一个bean对象类型和要注入的变量类型匹配,就可以注入成功。
    如果IoC容器中没有任何bean的类型和要注入的变量的类型匹配,则报错。
    如果IoC容器中该类型的bean存在多个,则将要注入的变量的变量名作为bean的id进行二次匹配:
    如果根据变量名可以找到唯一的bean,则进行注入。
    如果根据变量名匹配不到,则报错。
    出现位置:可以使变量上,也可以是方法上。
    细节:在使用注解进行注入时,变量的setter方法就不是必须的了。
  • @Qualifier
    作用:
    在按照类型匹配的基础上,再按照名称匹配注入。
    它在给类的成员变量注入时,不能单独使用,要和@Autowired配合使用。
    它在给方法参数进行注入时,可以单独使用。
    属性:
    value:用于指定要注入的bean的id。
  • @Resource
    作用:直接按照bean的id进行注入。它可以独立使用。
    属性:
    name:用于指定bean的id。
  • @Value
    作用:用于注入基本类型和String类型的变量
    属性:
    value:用于指定数据的值。可以配置:字面量、${key}(从环境变量、配置文件中获取值)、使用Spring中的SpEL(Spring中的EL表达式:#{表达式})。

@Autowired、@Qualifier、@Resource三个注解只能注入bean类型的数据,不能注入基本数据类型和String类型。

集合类型的注入只能使用xml来实现。

@Service("accountService")
public class AccountServiceImpl implements IAccountService {

    @Resource(name = "accountDao")
    private IAccountDao accountDao;

    public void saveAccount() {
        accountDao.saveAccount();
    }
}

3.4、用于改变作用范围的注解

这些注解的作用和xml配置的标签的scope属性的作用一样。

  • @Scope
    作用:用于指定bean的作用范围。
    属性:
    value:指定范围的取值。常用取值:singleton、prototype。(不配置时默认为singleton)
@Service("accountService")
@Scope("prototype")
public class AccountServiceImpl implements IAccountService {

    @Resource(name = "accountDao")
    private IAccountDao accountDao;

    public void saveAccount() {
        accountDao.saveAccount();
    }
}

3.5、和生命周期相关的注解

这些注解的作用和xml配置的标签的init-method、destroy-method属性作用相同。

  • @PreDestroy
    作用:用于指定销毁方法。
  • @PostConstruct
    作用:用于指定初始化方法。
@Service("accountService")
public class AccountServiceImpl implements IAccountService {

    @Resource(name = "accountDao")
    private IAccountDao accountDao;

    public void saveAccount() {
        accountDao.saveAccount();
    }

    @PostConstruct
    public void init() {
        System.out.println("初始化方法");
    }

    @PreDestroy
    public void destroy() {
        System.out.println("销毁方法");
    }
}

3.6、在配置类中利用java方法创建bean

当我们需要在程序中使用commons-dbutils去操作数据库时,需要创建DataSource、QueryRunner对象存入Spring的容器。创建对象、存入容器的过程可以放在一个配置类中,将创建对象的方法返回值作为bean存入容器。

  • @Configuration
    作用:指定当前类为一个配置类
    细节:当该类作为AnnotationConfigApplicationContext对象创建的参数时,该注解可以不写。
  • @ComponentScan
    作用:指定Spring在创建容器时要扫描的包。作用和xml中配置的context:component-scan一样。
    属性:value/basePackages:两个属性的作用一样,都是指定创建容器时要扫描的包。属性的值为数组。
  • @Bean
    作用:将方法的返回值作为一个bean对象,存入Spring的IoC容器中。
    属性:value/name:两个属性的作用一样,用于指定bean的id。不配置该属性时,默认以方法名作为id。
    细节:当我们给方法配置@Bean注解时,如果方法有参数,Spring会去容器中查找有无可用的bean对象。查找的方式和@Autowired相同。
  • @Import
    作用:用于导入其他的配置类。
    属性:value:用于指定要导入的其他配置类的字节码文件。当使用@Import的注解之后,有@Import注解的就是主配置类,而导入的都是子配置类。

设置配置类的不同方式:

  1. 在AnnotationConfigApplicationContext构造方法中传参:
  2. 使用@Configuration注解声明配置类:(需要在主配置类中使用@ComponentScan注解扫描到该配置类所在的包
  3. 使用@Import注解导入其他配置
ApplicationContext ac = new AnnotationConfigApplicationContext(SpringConfiguration.class,JdbcConfiguration.class);

@Configuration
public class SpringConfiguration {
  // ....
}
@Configuration
public class JdbcConfiguration {
  // ....
}

@Configuration
@Import(JdbcConfig.class)
public class SpringConfiguration {
	// ...
}

示例:
使用dbutils需要在pom中指定依赖

<dependencies>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>5.0.2.RELEASE</version>
    </dependency>
    <dependency>
        <groupId>commons-dbutils</groupId>
        <artifactId>commons-dbutils</artifactId>
        <version>1.4</version>
    </dependency>
    <dependency>
        <groupId>com.oracle</groupId>
        <artifactId>ojdbc6</artifactId>
        <version>12.1.0.1-atlassian-hosted</version>
    </dependency>
    <dependency>
        <groupId>c3p0</groupId>
        <artifactId>c3p0</artifactId>
        <version>0.9.1.2</version>
    </dependency>
</dependencies>

xml的配置方式:

<?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
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context.xsd">

    <context:component-scan base-package="com.study"></context:component-scan>

    <!-- QueryRunner配置成多例模式,保证每次使用的都是重新创建的。避免相互干扰 -->
    <bean id="runner" class="org.apache.commons.dbutils.QueryRunner" scope="prototype">
        <constructor-arg name="ds" ref="dataSource"></constructor-arg>
    </bean>

    <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
        <property name="driverClass" value="oracle.jdbc.driver.OracleDriver">
        </property>
        <property name="jdbcUrl" value="jdbc:oracle:thin:@127.0.0.1:1521/orcl">
        </property>
        <property name="user" value="springtest"></property>
        <property name="password" value="tiger"></property>
    </bean>
</beans>

改成java配置类的形式:

@Configuration
@ComponentScan("com.study")
public class SpringConfiguration {

    @Bean("runner")
    @Scope("prototype")  // Spring容器的bean默认为单例,为避免不同数据库操作之间的干扰,此处应该使用Scope将runner指定为多例
    public QueryRunner createQueryRunner(DataSource dataSource) {
        return new QueryRunner(dataSource);
    }

    @Bean("dataSource")
    public DataSource createDataSource() {
        try {
            ComboPooledDataSource dataSource = new ComboPooledDataSource();
            dataSource.setDriverClass("oracle.jdbc.driver.OracleDriver");
            dataSource.setJdbcUrl("jdbc:oracle:thin:@127.0.0.1:1521/orcl");
            dataSource.setUser("springtest");
            dataSource.setPassword("tiger");
            return dataSource;
        } catch (PropertyVetoException e) {
            throw new RuntimeException(e);
        }
    }
}

将main方法由读取xml配置改为读取配置类的配置:

@Test
    public void testFindAll(){
        // 读取配置类中的配置(如果有多个配置类,也可以传入多个配置类)
        ApplicationContext ac = new AnnotationConfigApplicationContext(SpringConfiguration.class);
        IAccountService as = ac.getBean("accountService", IAccountService.class);
        as.findAllAccount().forEach(System.out::println);
    }

3.7、从properties配置文件中获取配置

将jdbc的相关信息配置在properties配置文件中。
在SpringConfig.java中声明properties文件的位置。
在JdbcConfig.java中获取properties中配置的信息。

如果使用xml配置,则用<context:property-placeholder location="classpath:person.properties">配置

  • @PropertySource
    作用:用于指定properties配置文件的位置。
    属性:value:指定文件的名称和路径。value中需要使用classpath关键字声明该配置文件位于class路径的resources文件夹中。如果properties文件位于包里,可以带上包名(value = "classpath:com/study/jdbcConfig.properties")。

编写jdbcConfig.properties配置文件:

jdbc.driver=oracle.jdbc.driver.OracleDriver
jdbc.url=jdbc:oracle:thin:@127.0.0.1:1521/orcl
jdbc.username=springtest
jdbc.password=tiger

在SpringConfig.java中配置properties文件的位置:

@Configuration
@ComponentScan({"com.study","com.config"})
@PropertySource("classpath:jdbcConfig.properties")
public class SpringConfiguration {

}

在JdbcConfig.java中获取properties文件中的配置信息

@Configuration
public class JdbcConfig {

    @Value("${jdbc.driver}")
    private String jdbcDriver;
    @Value("${jdbc.url}")
    private String jdbcUrl;
    @Value("${jdbc.username}")
    private String jdbcUser;
    @Value("${jdbc.password}")
    private String jdbcPassword;

    @Bean("dataSource")
    public DataSource createDataSource() {
        try {
            ComboPooledDataSource dataSource = new ComboPooledDataSource();
            dataSource.setDriverClass(jdbcDriver);
            dataSource.setJdbcUrl(jdbcUrl);
            dataSource.setUser(jdbcUser);
            dataSource.setPassword(jdbcPassword);
            return dataSource;
        } catch (PropertyVetoException e) {
            throw new RuntimeException(e);
        }
    }
}

4、动态代理分析

4.1、为service层的QueryRunner添加事务支持

QueryRunner在构造方法中传入数据源,在每次执行sql时不再指定数据源,此时在每次执行sql时都会创建一个数据库连接并在执行完之后提交。对于转账交易这类需要执行多条更新sql的逻辑无法进行事务控制,需要对程序作出修改。

  1. 创建连接工具类,用于从数据库获取数据库连接,并实现和ThreadLocal线程的绑定
public class ConnectionUtil {

    private ThreadLocal<Connection> threadLocal = new ThreadLocal<Connection>();
    // 添加set方法,使用xml配置Spring注入
    private DataSource dataSource;
    public void setDataSource(DataSource dataSource) {
        this.dataSource = dataSource;
    }

    /**
     * 获取当前线程上的连接
     */
    public Connection getThreadConnection() {
        try {
            Connection connection = threadLocal.get();
            if (connection == null) {
                connection = dataSource.getConnection();
                threadLocal.set(connection);
            }
            return connection;
        }catch (SQLException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * 把连接和线程解绑
     */
    public void removeConnection() {
        threadLocal.remove();
    }
}
  1. 添加事务管理工具类,包括开启事务、提交事务、回滚事务、释放连接等操作。
public class TransactionManager {

    // 添加set方法,使用xml配置Spring注入
    private ConnectionUtil connectionUtil;
    public void setConnectionUtil(ConnectionUtil connectionUtil) {
        this.connectionUtil = connectionUtil;
    }

    /**
     * 开启事务
     */
    public void beginTransaction() {
        try {
            connectionUtil.getThreadConnection().setAutoCommit(false);
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * 提交事务
     */
    public void commit() {
        try {
            connectionUtil.getThreadConnection().commit();
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * 回滚事务
     */
    public void rollback() {
        try {
            connectionUtil.getThreadConnection().rollback();
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * 释放连接
     */
    public void release() {
        try {
            // close并不会真正把连接关闭,只是把该连接还回了连接池中
            connectionUtil.getThreadConnection().close();
            // 该ThreadLocal中的连接已经被还回连接池,下次该线程如果还使用当前连接对象就用不了了。
            // 所以在线程绑定的连接关闭后,线程要和连接进行解绑。下次该线程要使用连接时重新从连接池获取连接绑定到线程。
            connectionUtil.removeConnection();
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
    }
}
  1. 修改dao层的QueryRunner操作,为每次sql执行指定数据库连接。
public class AccountDaoImpl implements IAccountDao {

    private QueryRunner runner;
    private ConnectionUtil connectionUtil;

    public void setConnectionUtil(ConnectionUtil connectionUtil) {
        this.connectionUtil = connectionUtil;
    }

    public void setRunner(QueryRunner runner) {
        this.runner = runner;
    }

    public List<Account> findAllAccount() {
        try {
            // 调用QueryRunner重载的带有数据源连接参数的query方法
            return runner.query(connectionUtil.getThreadConnection(), "select * from account", new BeanListHandler<Account>(Account.class));
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
    }

    public Account findAccountByName(String accountName) {
        try {
            List<Account> accounts = runner.query(connectionUtil.getThreadConnection(), "select * from account where name = ?", new BeanListHandler<Account>(Account.class), accountName);
            if(accounts == null || accounts.size() == 0) {
                return null;
            }
            if(accounts.size() > 1) {
                throw new RuntimeException("查询的账户数量不止一个");
            }
            return accounts.get(0);
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
    }
}
  1. 为service层的代码包裹事务。
public class AccountServiceImpl implements IAccountService {

    private IAccountDao accountDao;
    private TransactionManager transactionManager;
    public void setTransactionManager(TransactionManager transactionManager) {
        this.transactionManager = transactionManager;
    }
    public void setAccountDao(IAccountDao accountDao) {
        this.accountDao = accountDao;
    }

    public List<Account> findAllAccount() {
        try {
            transactionManager.beginTransaction();
            List<Account> accounts = accountDao.findAllAccount();
            transactionManager.commit();
            return accounts;
        } catch (Exception e) {
            transactionManager.rollback();
            throw new RuntimeException(e);
        } finally {
            transactionManager.release();
        }
    }

    public void transfer(String sourceName, String targetName, Float money) {
        try {
            transactionManager.beginTransaction();
            Account sourceAccount = accountDao.findAccountByName(sourceName);
            Account targetAccount = accountDao.findAccountByName(targetName);
            sourceAccount.setMoney(sourceAccount.getMoney() - money);
            targetAccount.setMoney(targetAccount.getMoney() + money);
            accountDao.updateAccount(sourceAccount);
            // int i = 1 / 0;
            accountDao.updateAccount(targetAccount);
            transactionManager.commit();
        } catch (Exception e) {
            e.printStackTrace();
            transactionManager.rollback();
        } finally {
            transactionManager.release();
        }
    }
}

4.2、基于接口的动态代理

基于接口的动态代理,要求被代理对象的类最少实现一个接口,否则不能创建代理对象。
动态代理:

  • 特点:字节码随用随创建,随用随加载
  • 作用:不修改源码的基础上对方法增强
  • 分类:
  • 基于接口的动态代理
  • 基于子类的动态代理

基于接口的动态代理:

  • 涉及的类:java.lang.reflect.Proxy
  • 提供者:jdk官方

如何创建代理对象:使用Proxy的newProxyInstance方法

创建代理对象的要求:被代理的类最少实现一个接口,如果没有则不能创建。

newProxyInstance方法的参数:

  • classLoader:类加载器
    它是用于加载代理对象字节码文件的,和被代理对象使用相同的类加载器。是固定写法。
  • Class[]:字节码数组
    它是用于让代理对象和被代理对象有相同的方法。固定的写法。
  • invocationHandler:用于提供增强的代码
    它是让我们写如何代理。一般都是写一个该接口的实现类。通常情况下都是匿名内部类,但不是必须。此接口的实现类都是谁用谁写。

示例:
IProducer接口:

public interface IProducer {
    Float sellProduct(Float money);
    void afterSell(Float money);
}

IProducer的实现类Produer类:

// 基于接口代理要求被代理类最少实现一个接口。如果此处不实现IProducer接口则无法创建被代理对象
public class Producer implements IProducer {
    public Float sellProduct(Float money) {
        System.out.println("销售商品");
        return money;
    }
    public void afterSell(Float money) {
        System.out.println("售后服务");
    }
}

编写main方法测试,并在main方法中加入代理:

public static void main(String[] args) {
        final Producer producer = new Producer();

        // 返回的是Object类型对象,需要强转
        IProducer proxyProducer = 
            (IProducer) Proxy.newProxyInstance(producer.getClass().getClassLoader(),
                producer.getClass().getInterfaces(),
                new InvocationHandler() {
                    /**
                     * 执行被代理对象的任何方法都会经过此方法
                     * @param proxy 代理对象的引用(一般用不上)
                     * @param method 当前执行的方法
                     * @param args 当前执行方法的参数
                     * @return 和被代理对象方法有相同类型的返回值
                     * @throws Throwable
                     */
                    public Object invoke(Object proxy, Method method, Object[] args) 
                        throws Throwable {
                        // 编写增强的代码
                        Object result = null;
                        // 获取方法的传入参数
                        Float money = (Float) args[0];
                        // 判断方法名
                        if("sellProduct".equals(method.getName())) {
                            // 执行方法(此处代理将原本传入的金额打了8折)
                            result = method.invoke(producer, money * 0.8f);
                        }
                        return result;
                    }
                });

        // 通过创建的代理对象执行对应的方法
        Float result = proxyProducer.sellProduct(1000f);
        System.out.println(result);
    }

4.3、基于子类的动态代理

基于子类的动态代理:

  • 涉及的类:Enhancer
  • 提供者:第三方cglib

如何创建对象:使用Enhancer的create方法

创建代理对象的要求:被代理类不能是最终类

create方法的参数:

  • class:字节码
    指定被代理对象的字节码
  • callback:提供增强的代码
    我们一般写的都是该接口的子接口实现类:MethodInterceptor

示例:
pom中引入cglib:

<dependency>
    <groupId>cglib</groupId>
    <artifactId>cglib</artifactId>
    <version>2.1_3</version>
</dependency>

编写Producer类:

// cglib不要求Producer实现接口,但是Producer类不能是final类
public class Producer{
    public Float sellProduct(Float money) {
        System.out.println("销售商品");
        return money;
    }

    public void afterSell(Float money) {
        System.out.println("售后服务");
    }
}

编写main方法测试,并在main方法中加入代理:

public static void main(String[] args) {
        final Producer producer = new Producer();
		// 使用cglib创建代理对象,返回Object类型需要强转
        Producer cglibProducer = (Producer) Enhancer.create(producer.getClass(), new MethodInterceptor() {
            /**
             * 执行被代理对象的任何方法都会经过该方法
             * @param proxy 代理对象的引用(一般用不上)
             * @param method 当前执行的方法
             * @param args 当前执行方法传入的参数
             * @param methodProxy 当前执行方法的代理对象(一般用不上)
             * @return 和被代理对象有相同类型的返回值
             * @throws Throwable
             */
            public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
                // 编写增强的代码
                Object result = null;
                // 获取方法的传入参数
                Float money = (Float) args[0];
                // 判断方法名
                if("sellProduct".equals(method.getName())) {
                    // 执行方法(此处代理将原本传入的金额打了8折)
                    result = method.invoke(producer, money * 0.8f);
                }
                return result;
            }
        });
        float money = cglibProducer.sellProduct(1000f);
        System.out.println(money);
    }

4.4、使用动态代理重构带有事务的service示例

  1. 将AccountServiceImpl中每个方法因为事务管理添加的重复性代码移除
public class AccountServiceImpl implements IAccountService {

    private IAccountDao accountDao;
    public void setAccountDao(IAccountDao accountDao) {
        this.accountDao = accountDao;
    }

    public List<Account> findAllAccount() {
        return accountDao.findAllAccount();
    }

    public void transfer(String sourceName, String targetName, Float money) {
        Account sourceAccount = accountDao.findAccountByName(sourceName);
        Account targetAccount = accountDao.findAccountByName(targetName);
        sourceAccount.setMoney(sourceAccount.getMoney() - money);
        targetAccount.setMoney(targetAccount.getMoney() + money);
        accountDao.updateAccount(sourceAccount);
        int i = 1 / 0;
        accountDao.updateAccount(targetAccount);
    }
}
  1. 编写BeanFactory类,作为service的代理对象的创建工厂
public class BeanFactory {
    private IAccountService accountService;
    private TransactionManager transactionManager;
    public void setTransactionManager(TransactionManager transactionManager) {
        this.transactionManager = transactionManager;
    }
    public final void setAccountService(IAccountService accountService) {
        this.accountService = accountService;
    }

    // 创建service的代理对象
    public IAccountService getAccountService() {
        return (IAccountService) Proxy.newProxyInstance(
            accountService.getClass().getClassLoader(), 
            accountService.getClass().getInterfaces(), 
            new InvocationHandler() {
            public Object invoke(Object proxy, Method method, Object[] args) 
                throws Throwable {
                Object returnValue = null;
                try {
                    transactionManager.beginTransaction();
                    returnValue = method.invoke(accountService, args);
                    transactionManager.commit();
                    return returnValue;
                } catch (Exception e) {
                    transactionManager.rollback();
                    throw new RuntimeException(e);
                } finally {
                    transactionManager.release();
                }
            }
        });
    }
}
  1. 在bean.xml中配置BeanFacotry需要注入的对象,以及生成的service的代理对象的bean
<bean id="beanFactory" class="com.study.factory.BeanFactory">
    <property name="transactionManager" ref="transactionManager"></property>
    <property name="accountService" ref="accountService"></property>
</bean>

<bean id="proxyAccountService" 
      factory-bean="beanFactory" 
      factory-method="getAccountService">
</bean>
  1. 将原来测试方法中注入的service对象修改为注入代理的对象
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:bean.xml")
public class AccountTest {
    @Resource(name = "proxyAccountService")
    IAccountService as;
    @Test
    public void testTransfer() {
        as.transfer("bbb", "ccc", 100f);
    }
}