概述
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注解的就是主配置类,而导入的都是子配置类。
设置配置类的不同方式:
- 在AnnotationConfigApplicationContext构造方法中传参:
- 使用@Configuration注解声明配置类:(需要在主配置类中使用@ComponentScan注解扫描到该配置类所在的包
- 使用@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的逻辑无法进行事务控制,需要对程序作出修改。
- 创建连接工具类,用于从数据库获取数据库连接,并实现和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();
}
}
- 添加事务管理工具类,包括开启事务、提交事务、回滚事务、释放连接等操作。
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);
}
}
}
- 修改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);
}
}
}
- 为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示例
- 将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);
}
}
- 编写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();
}
}
});
}
}
- 在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>
- 将原来测试方法中注入的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);
}
}