0、基本介绍
Spring
是对业务层的操作,同时可以整合Mybats
框架和Spring MVC
框架。下图是MVC
结构:
**耦合:**简单理解为程序间的依赖关系
- 类之间的依赖
- 方法间的依赖
**解耦:**降低程序间的依赖关系
实际开发中编译期不依赖,运行时才依赖。
解耦的思路:
第一步:使用反射来创建对象,而避免使用new
关键字。
第二步:通过读取配置文件来获取要创建的对象全限定类名。
一个创建Bean
对象的工厂。
Bean
:含有可重用组件的含义。
JavaBean
:用Java
语言编写的可重用组件。
JavaBean > 实体类
JavaBean
就是创建service
和dao
对象的。
第一个:需要一个配置文件来配置我们的service
和dao
配置的内容:唯一标识=全限定类名(key=value
)
第二个:通过读取配置文件中配置的内容,反射创建对象。
配置文件可以是xml
,也可以是properties
。
使用步骤:
- 创建
Properties
对象,读取配置文件。 - 通过类加载器读取流。
以上两步通过static
静态代码块加载。
加载文件代码块:
/**
* 模拟工厂进行解耦,一个创建Bean对象的工厂
*/
public class BeanFactory {
//定义一个Properties对象
private static Properties props;
//定义一个Map,用于存放我们要创建的对象。我们把它称之为容器
private static Map<String,Object> beans;
//使用静态代码块为Properties对象赋值
static {
try {
//实例化对象
props = new Properties();
//获取properties文件的流对象
InputStream in = BeanFactory.class.getClassLoader().getResourceAsStream("bean.properties");
props.load(in);
//实例化容器
beans = new HashMap<String,Object>();
//取出配置文件中所有的Key
Enumeration keys = props.keys();
//遍历枚举 -- 用来获取全限定类名的唯一标识符(key)
while (keys.hasMoreElements()){
//取出每个Key
String key = keys.nextElement().toString();
//根据key获取value -- 全限定类名
String beanPath = props.getProperty(key);
//反射创建对象
Object value = Class.forName(beanPath).newInstance();
//把key和value存入容器中
beans.put(key,value);
}
}catch(Exception e){
throw new ExceptionInInitializerError("初始化properties失败!");
}
}
/**
* 根据bean的名称获取对象
* @param beanName
* @return
*/
public static Object getBean(String beanName){
return beans.get(beanName);
}
// private static Properties pro; //
// private static Map<String, Object> beans;// 定义一个map,用于存放我们要创建的对象,将之称之为容器。
//
// static {
// try {
// pro = new Properties();
// InputStream is = BeanFactory.class.getClassLoader().getResourceAsStream("bean.properties"); // 配置文件加载进内存
// pro.load(is);
// beans = new HashMap<String, Object>();
// // 取出配置文件中的所有的key
// Enumeration keys = pro.keys();
// // 将全限定类名和反射创建的对象组成key-value,存放到集合中,这样我们对同一个类对象,就是只是从bean的集合中获取,始终获取的都是同一个对象。
// while (keys.hasMoreElements()) {
// String key = keys.nextElement().toString();
// String beanPath = pro.getProperty(key);
// Object value = Class.forName(beanPath).newInstance();
// beans.put(key, value);
// }
// } catch (IOException e) {
// e.printStackTrace();
// } catch (IllegalAccessException e) {
// e.printStackTrace();
// } catch (InstantiationException e) {
// e.printStackTrace();
// } catch (ClassNotFoundException e) {
// e.printStackTrace();
// }
//
// }
//
// /**
// * 根据bean的名称获取对象
// *
// * @param beanName
// * @return
// */
// public static Object getBean(String beanName) {
// Object bean = null; // 创建一个对象引用
// bean = beans.get(beanName);
// return bean;
// }
//
}
单例对象:从始至终只有一个对象。
- 只被创建一次,从而类中的成员也就只会初始化一次。
多例对象:对象被创建多次,执行效率没有单例对象高。
目的:我们需要创建单例对象,只会初始化一次。
在创建工厂时,我们创建一个容器,将配置文件中的所有对象都提前创建好,存入到容器中,这样我们每次获取对象时,都是从这个容器中获取对象,保证我们始终获取的都是同一个对象。这里的容器就可以是Map
集合。
- 在
BeanFactory
中,以容器装载对象。 – 当获取对象时,是从容器中获取对象 - 获取对象,使用工厂获取对象,避免了
new
关键字创建出的对象,并由于工厂中,对象存储在容器中,保证了这里的对象的单例的。
一、IOC – 控制反转
将new
的自主控制权交给了工厂。工厂再通过全限定类名决定得到获取到的对象。此时类无法再确定所获得到对象是否是自己所需要的(降低了耦合)。
使用步骤一:
-
创建配置文件
<?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"> <!-- 将对象的创建交给spring来管理 --> <bean id="accountService" class="cn.lihzi.service.impl.AccountServiceImpl"></bean> <bean id="accountDao" class="cn.lihzi.dao.impl.AccountDaoImpl"></bean> </beans>
-
获取容器 –
spring
的核心容器,并根据id
获取对象public class Client { /** * 获取spring的IOC核心容器,并根据id获取对象 * * @param args * @throws IllegalAccessException * @throws InstantiationException * @throws ClassNotFoundException */ public static void main(String[] args) { // 1. 获取核心容器对象 ClassPathXmlApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml"); // 2. 根据id获取Bean对象 AccountService service = (AccountService) ac.getBean("accountService"); AccountDao dao = ac.getBean("accountDao", AccountDao.class); System.out.println(service); System.out.println(dao); // service.save(); } }
1.1 ApplicationContext的三个常用实现类:
-
ClassPathXmlApplicationContext
它可以加载类路径下的配置文件,要求配置文件必须在类路径下。不在的话,加载不了。
-
FileSystemXmlApplicationContext
它可以加载磁盘任意路径下的配置文件(必须有访问权限)
-
AnnotationConfigApplicationContext
它用于读取注解创建的容器
1.2 核心容器的两个接口引发出的问题
-
ApplicationContext
它在构建核心容器时,创建对象采取的策略是采用立即加载的方法。也就是说,只要一读取完配置文件马上就创建配置文件中配置的对象。
试用情况:单例对象适用,更多采用此接口
-
它在构建核心容器时,创建对象采取的策略是采用延迟加载的方式。也就是说,什么时候根据
id
获取对象,什么时候才是真正的创建对象。试用情况:多例对象适用。
1.3 Bean对象的细节
- 创建
Bean
的三种方式:
- 第一种方式
使用默认构造函数创建。
在spring
的配置文件中使用bean标签,配以id
和class
属性之后,且没有其他属性和标签时。
采用的就是默认构造函数创建bean对象,此时如果类中没有默认构造函数,则对象无法创建。
<bean id="accountService" class="cn.lihzi.service.impl.AccountServiceImpl"></bean>
<bean id="accountDao" class="cn.lihzi.dao.impl.AccountDaoImpl"></bean>
- 第二种方式
使用工厂中的普通方法创建对象(使用某个类中的方法创建对象,并存入spring容器)
<bean id="instanceFactory" class="cn.lihzi.factory.InstanceFactory"></bean>
<bean id="accountService" factory-bean="instanceFactory" factory-method="getAccountService"/>
通过factory-bean
找到id
得到其对象,再通过factory-method
得到其方法对象。而其中的id="accountService"
是对应到容器中的key
。
- 第三种方式
使用工厂中的静态方法创建对象(使用某个类中的静态方法创建对象,并存入spring容器)。
<bean id="accountService" class="cn.lihzi.factory.StaticInstanceFactory" factory-method="getAccountService"/>
其中,第二种、第三种方式可以用来获取到jar
包中的方法对象。
Bean
的作用范围
默认的作用范围是单例。
bean
标签的scope
属性:
作用:用于指定bean
的作用范围
取值:常用的就是单例的和多例的
singleto
:单例的(默认值)prototype
:多例的request
:作用于web
应用的请求范围session
:作用于web
应用的会话范围global-session
:作用于集群环境的会话范围(全局会话范围),当不会集群环境时,它就是session
bean
对象的生命周期
-
单例对象
出生:当容器创建时对象出生
活着:只要容器还在,对象一直活着
死亡:容器销毁,对象消亡
总结:单例对象的生命周期和容器相同
-
多例对象
出生:当我们使用对象时
spring
框架为我们创建活着:对象只要是在使用过程中就一直活着
死亡:当对象长时间不用,且没有别的对象引用时,由
Java
的垃圾回收器回收
bean
配置文件中分别是,init-method
;destory-method
指定初始化和销毁时使用的对象方法。
1.4 Spring的依赖注入
依赖注入: Dependency Injection
IOC
作用:降低程序间的耦合(依赖关系)
依赖关系的管理
- 交给
spring
来维护
在当前类需要用到其他类的对象,由spring
为我们提供,我们只需要在配置文件中说明
依赖关系的维护就称之为依赖注入。
依赖注入:
- 能注入的数据:三类
- 基本数据类型和
String
- 其他
bean
类型(在配置文件中或者注解配置过的bean
) - 复杂类型/集合类型
- 基本数据类型和
- 注入的方式:三种
- 使用构造函数提供
- 使用
set
方法提供 - 使用注解提供
1.4.1 构造函数注入
<!-- 构造函数注入:
使用的标签:constructor-arg
标签出现的位置:bean标签的内部
标签中的属性
type:用于指定要注入的数据的数据类型,该数据类型也是构造函数中某个或某些参数的类型(当构造函数中同时含有多个相同的数据类型时,就无法分别)
index:用于指定要注入的数据给构造函数中指定索引位置的参数赋值。索引的位置是从0开始
name:用于指定给构造函数中指定名称的参数赋值。(最常用,也是最直接的方式)
=============以上三个用于指定给构造函数中哪个参数赋值================
value:用于提供基本类型和String类型的数据
ref:用于指定其他的bean类型数据。它指的就是在spring的Ioc核心容器中出现过的bean对象。
特点:
在获取bean对象时,注入数据是必须的操作,否则对象无法创建成功。
缺点:
改变了bean对象的实例化方法,使我们在创建对象时,如果用不到这些数据,也必须提供。
-->
类的代码:
public class AccountServiceImpl implements AccountService {
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 save() {
System.out.println("service方法执行.."+name+":"+age+":"+birthday);
}
}
配置文件:
<bean id="accountService" class="cn.lihzi.service.impl.AccountServiceImpl">
<constructor-arg name="name" value="Tom"></constructor-arg>
<constructor-arg name="age" value="18"></constructor-arg>
<constructor-arg name="birthday" ref="now"></constructor-arg>
</bean>
<!-- 以下是配置一个日期对象 反射方式创建Date的对象,再由id指定赋值-->
<bean id="now" class="java.util.Date"></bean>
1.4.2 set方法注入 – 适用于存在空参的构造方法(更常用的set方法注入)
涉及的标签:property
出现的位置:bean
标签的内部
标签的属性
name
:用于指定注入时所调用的set
方法名称value
:用于提供基本类型和String
类型的数据ref
:用于指定其他的bean
类型数据。它指的就是在spring
的Ioc
核心容器中出现的bean
对象。
优势:
- 创建对象时没有明确的限制,可以直接使用默认构造函数
弊端:
- 如果有某个成员必须有值,则获取对象是有可能
set
方法没有执行。
配置文件:
<!-- set -->
<bean id="accountService1" class="cn.lihzi.service.impl.AccountServiceImpl1">
<property name="age" value="18"></property>
<property name="name" value="Tom"></property>
<!-- <property name="birthday" ref="now"></property>-->
</bean>
1.4.3 复杂类型的注入/集合类型的注入
用于给List
结构集合注入的标签:list
、array
、set
用于给Map
结构集合注入的标签:map
、props
结构相同,标签可以互换。
实体类:
public class AccountServiceImpl2 implements AccountService {
private Map<String, String> map;
private List<String> list;
private Set<String> set;
private Properties properties;
private String[] str;
/*
省略了getter和setter方法
*/
public void save() {
System.out.println("str:"+Arrays.toString(str));
System.out.println("map:"+map);
System.out.println("list:"+list);
System.out.println("set:"+set);
System.out.println("properties:"+properties);
}
}
Bean
配置文件:
<!-- 复杂对象的封装使用 -->
<bean id="accountService2" class="cn.lihzi.service.impl.AccountServiceImpl2">
<property name="list">
<list>
<value>a</value>
<value>b</value>
<value>c</value>
</list>
</property>
<property name="str">
<list>
<value>a</value>
<value>b</value>
<value>c</value>
</list>
</property>
<property name="set">
<list>
<value>a</value>
<value>b</value>
<value>c</value>
</list>
</property>
<property name="map">
<map>
<entry key="name">
<value>Tom</value>
</entry>
<entry key="age" value="18"></entry>
</map>
</property>
<property name="properties">
<!-- <map>-->
<!-- <entry key="name">-->
<!-- <value>Tom</value>-->
<!-- </entry>-->
<!-- <entry key="age" value="18"></entry>-->
<!-- -->
<!-- </map>-->
<props>
<prop key="name">Tom</prop>
<prop key="age">18</prop>
</props>
</property>
</bean>
二、基于注解的方式 – IOC
xml
起始的配置
<bean id="accountService" class="cn.lihzi.service.impl.AccountServiceImpl" scope="" init-method="" destory-method="">
<property name="" value="" ref=""></property>
</bean>
注解类别:
-
用于创建对象的(创建的对象存放至
Spring
容器中)-
其作用就和在
XML
配置文件中编写一个<bean>
标签实现的功能是一样的 -
@Component
-
-
作用:用于把当前类对象写入
spring
容器中。 -
属性:
-value
:用于指定bean
的id
。当我们不写时,它的默认值是当前类名,且首字母小写。
-
@Controller
:一般用于表现层 -
@Service
:一般用在业务层 -
@Respository
:一般用于持久层
以上三个注解他们的作用和属性与Component
是一模一样的。
他们三个是Spring
框架为我们提供明确的三层使用的注解,使我们的三层对象更加清晰。
-
用于注入数据的(注入的对象是从
Spring
容器中获取)-
其作用就和在
xml
配置文件中的bean
标签中写一个<property>
标签的作用是一样的 -
@Autowired
- 作用:自动按照类型注入。只要容器中有唯一的一个
bean
对象类型和要注入的变量类型匹配,就可以注入成功。 - 出现位置:可以是变量上,也可以是方法上。
- 细节:在使用注解注入时,
set
方法就不是必须的了。 - 如果
IOC
容器中有多个类型匹配时:先找到同类别的,再根据变量名称进行注入。
通过在Spring
容器中符合数据类型的value
,并将value
值赋给该引用,当出现同类型的引用时,再根据变量名称进行赋值。
- 作用:自动按照类型注入。只要容器中有唯一的一个
-
@Qualifier
- 作用:在按照类中注入的基础之上再按照名称注入。它在给类成员注入时不能单独使用。但是在给方法参数注入时可以。(要和
@Autowired
配合使用) - 属性
value
:用于指定注入bean
的id
。
- 作用:在按照类中注入的基础之上再按照名称注入。它在给类成员注入时不能单独使用。但是在给方法参数注入时可以。(要和
-
@Resource
- 作用:直接按照
bean
的id
注入。它可以独立使用。 - 属性
name
:用于指定bean
的id
。
- 作用:直接按照
以上三个注入都只能注入其他
bean
类型的数据,而基本类型和String
类型无法使用上述注解实现。另外集合只能通过
xml
来实现。@Value
- 作用:用于注入基本类型和
String
类型的数据。 - 属性:
value
:用于指定数据的值。它可以使用spring
中SpEL
(也就是spring
的el
表达式)SpEL
的写法:${表达式}
。注意:其表达式写在哪里(JSP
、Mybatis
、Spring
…),就是从哪里获值。
- 作用:用于注入基本类型和
-
-
用于改变作用范围的
- 其作用就和在
bean
标签中使用scope
属性实现的功能是一样的 @Scope
- 作用:用于指定
bean
的作用范围 - 属性:
value
:指定范围的取值。常用取值:singleton
、prototype
(默认为singleton
)
- 作用:用于指定
- 其作用就和在
-
生命周期相关
- 其作用就和在
bean
标签中使用init-method
和destory-method
的作用是一样的。 @PreDestroy
- 作用:用于指定销毁方法
@PostConstruct
- 作用:用于指定初始化方法
- 其作用就和在
**注意:**需要告知spring
在创建容器时要扫描的包、配置所需要的标签不是在bean
的约束中,而是一个名称为context
名称空间和约束中。其配置文件形式为:
<?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:annotation-config/>
<context:component-scan base-package="cn.lihzi"></context:component-scan> <!-- 扫描这个包下的所有包及其子包 -->
</beans>
2.1 实例:简单的数据库增删改查
XML
方式
service
层中,提供setter
方法,共xml
配置使用。
配置:
- 业务层对象
- 持久层对象
JDBC
对象- 数据库连接池
注意数据源的单例、多例造成的线程混乱问题
<?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">
<!-- 创建service对象,并将service对象中的变量注入数据dao -->
<bean id="accountService" class="cn.lizhi.service.impl.AccountServiceImpl">
<property name="dao" ref="accountDao"></property>
</bean>
<!-- 创建dao对象,并给变量注入数据,runner -->
<bean id="accountDao" class="cn.lizhi.dao.impl.AccountDaoImpl">
<!-- 注入runner -->
<property name="runner" ref="queryRunner"></property>
</bean>
<!-- 创建QueryRunner对象 并采用构造方法的方式,注入数据源(这里采用的是有参构造方法,其参数就是dataSource数据源,故进而再创建dataSource对象) -->
<!-- QueryRunner对象,是单例对象。为了让多个dao在调用这个对象时,互不干扰,故采取多例的方法 -->
<bean id="queryRunner" class="org.apache.commons.dbutils.QueryRunner" scope="prototype">
<!-- 注入数据源,方便sql语句的复用,不用每次都传入数据 -->
<constructor-arg name="ds" ref="dataSource"></constructor-arg>
</bean>
<!-- 配置数据源dataSource,给dataSource对象的变量注入数据-->
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
<property name="driverClass" value="com.mysql.jdbc.Driver"></property>
<property name="jdbcUrl" value="jdbc:mysql://url:3306/draft"></property>
<property name="user" value="username"></property>
<property name="password" value="password"></property>
</bean>
</beans>
测试方法:
public class AccountServiceTest {
// 1. 获取对象
private ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
// 2. 得到业务层对象
private AccountService service = ac.getBean("accountService", AccountService.class);
@Test
public void findAll() {
List<Account> accounts = service.findAll();
for (Account account : accounts) {
System.out.println(account);
}
}
@Test
public void findById() {
Account account = service.findById(1);
System.out.println(account);
}
@Test
public void insert() {
Account account = new Account();
account.setName("Tom");
account.setMoney(800.8f);
service.insert(account);
}
}
- 基于注解的
IOC
配置
注意一点:当用注解方式对变量进行注入时,setter
方法就不是必要的了。
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="cn.lizhi"></context:component-scan> <!-- 扫描这个包下的所有包及其子包 -->
<!-- service和dao的对象通过注解的方法创建以及注入-->
<bean id="queryRunner" 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="com.mysql.jdbc.Driver"></property>
<property name="jdbcUrl" value="jdbc:mysql://url/draft"></property>
<property name="user" value="username"></property>
<property name="password" value="password"></property>
</bean>
</beans>
2.2 使用注解不带有xml
配置文件的使用(纯注解)
-
创建配置类 —
config
例如:
SpringConfiguration
:其作用同bean.xml
相同spring
中的新注解:-
@Configuration
- 作用:指定当前类是一个配置类
- 细节:当配置类作为
AnnotationConfigApplicationContext
对象创建的参数时,该注解可以不写。(原因是参数传递的是配置的Class
对象,就能够读取到这个类)
-
-
@ComponentScan
-
作用:用于通过注解指定
spring
在创建容器时要扫描的包 -
属性:
-
value
:它和basePackages
的作用是一样的,都是用于指定创建容器时要扫描的包。我们使用此注解就等同于在xml
中配置了:<context:component-scan base-package="cn.lizhi"></context:component-scan>
-
-
-
@Bean
-
作用:用于把当前方法的返回值作为
bean
对象存入spring
的ioc
容器中-
属性:
name
:用于指定bean
的id
。当不写时,默认值是当前方法的名称
-
细节:
当我们使用注解配置方法时,如果方法需要传递参数,
Spring
框架会去容器中查找有没有可用的对应类型的bean
对象。查找的方式和Autowired
注解的作用是一样的。
-
-
-
@Import
- 作用:用于导入其他的配置类
- 属性
value
:用于指定其他配置类的字节码。当我们使用import
的注解之后,有Import
注解的类就是父配置类,而导入的都是子配置类。
在主配置类下配置@import
,@import
中的参数为value
数组,内容填写子配置类。这样也可以不用在子配置类下配置@Configuration
注解。
-
@PropertySource
- 作用:用于指定
properties
文件的位置。 - 属性:
value
:指定文件的名称和路径。- 关键字:
classpath
,表示类路径下。- 例如:
@PropertySource(classpath:jdbcConfig.properties)
- 例如:
注解位置在主配置文件下。
xml
和注解配置选择问题:自己写的类,选择采用注解
的方式;存在于jar
包中的选择用xml
方式,两者可以配合着使用。注解类:
@Configuration @ComponentScan("cn.lizhi") public class SpringConfiguration { @Bean("runner") // 用于将返回值存入容器中 -- 其中id设置为runner @Scope("prototype") // 将数据源设置成多例 public QueryRunner getRunner(DataSource dataSource) { // 获取QueryRunner对象 -- 参数为dataSource,故进一步再得到dataSource对象,见下方 return new QueryRunner(dataSource); } @Bean("dataSource") -- 容器中id为dataSource public DataSource getDataSource() { ComboPooledDataSource cpds = new ComboPooledDataSource(); try { cpds.setDriverClass("com.mysql.jdbc.Driver"); cpds.setJdbcUrl("jdbc:mysql://url:3306/draft"); cpds.setUser("username"); cpds.setPassword("password"); } catch (PropertyVetoException e) { e.printStackTrace(); } return cpds; } }
测试用例:
public class AccountServiceTest { // 1. 获取对象 // private ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml"); private ApplicationContext ac = new AnnotationConfigApplicationContext(SpringConfiguration.class); // 2. 得到业务层对象 private AccountService service = ac.getBean("accountService", AccountService.class); @Test public void findAll() { List<Account> accounts = service.findAll(); for (Account account : accounts) { System.out.println(account); } } @Test public void findById() { Account account = service.findById(1); System.out.println(account); } @Test public void insert() { Account account = new Account(); account.setName("Tom"); account.setMoney(800.8f); service.insert(account); } @Test public void update() { Account account = service.findById(4); account.setMoney(1000.0f); service.update(account,account.getId()); } @Test public void delete() { service.delete(4); } }
**注意:**此时接口的实现方法使用的是
AnnotationConfigApplicationContext
。 - 作用:用于指定
-
抽取子配置类
-
JdbcConfig
- 子配置类public class JdbcConfig { @Value("${jdbc.driver}") private String driver; @Value("${jdbc.url}") private String url; @Value("${jdbc.user}") private String user; @Value("${jdbc.password}") private String password; @Bean("runner") @Scope("prototype") // 将数据源设置成多例 public QueryRunner getRunner(DataSource dataSource) { return new QueryRunner(dataSource); } @Bean("dataSource") public DataSource getDataSource() { ComboPooledDataSource cpds = new ComboPooledDataSource(); try { cpds.setDriverClass(driver); cpds.setJdbcUrl(url); cpds.setUser(user); cpds.setPassword(password); } catch (PropertyVetoException e) { e.printStackTrace(); } return cpds; } }
-
SpringConfiguration
- 父配置类@Import(JdbcConfig.class) @ComponentScan("cn.lizhi") @PropertySource("classpath:jdbcConfig.properties") public class SpringConfiguration { }
-
jdbcConfig.properties
- 配置文件jdbc.driver=com.mysql.jdbc.Driver jdbc.url=jdbc:mysql://url:3306/draft jdbc.user=username jdbc.password=password
-
2.3 spring整合junit问题
整合原因:
- 应用程序的入口 – main 方法
junit
单元测试中,没有main
方法也能执行
junit
集成了一个main
方法- 该方法就会判断当前测试类中哪些方法有
@Test
注解junit
就让有Test
注解的方法执行junit
中无法探测出是否存在spring
框架
- 在执行测试方法时,
junit
无法得知我们是否使用了spring
框架- 因此也就不会为我们读取配置文件/配置类创建
spring
核心容器综上:在执行测试方式时,没有
IOC
容器,就算谢了Autowired
注解,也无法实现注入
Spring
整合junit
的配置
-
导入
spring
整合junit
的jar
(坐标) -
使用
junit
提供的一个注解把原有的main
方法替换了,替换成spring
提供的@RunWith
注解配置–SpringJUnit4ClassRunner.class
-
告知
spring
的运行器,spring
和ioc
创建是基于xml
还是基于注解
的,并且说明位置。-
@ContextConfiguration
-
locations
:指定xml
文件的位置,加上classpath
关键字,表示在类路径下。 -
classes
:指定注解所在地位置。例如:@ContextConfiguration(classes=SpringConfiguration.class)
当我们使用
spring 5.x
版本的时候,要求junit
的jar
包必须是4.12
以上。 -
-
三、AOP – 导读
3.1 案例-transfer(银行转账案例)
在service
接口中定义转账方法(其参数列表为转出用户,转入用户,转账金额),dao
接口中定义根据用户名称查找用户的方法。
当在数据库数据进行更新时(转账过程中),如果出现异常,可能就会出现破坏数据库的一致性操作。故下面要进行对事物的控制。
事务控制在service
层。
以上的问题引发了一个思考,就是获取的connection
应该全部由同一个connection
进行控制,要成功就一起成功,如果失败就一起失败。
**解决办法:**需要使用ThreadLocal
对象把Connection
与当前线程绑定,从而使一个线程中只有一个能控制事务的对象。
事务控制应该都是在业务层。
注意:在web
工程中,当tomcat
服务器启动时,会初始化线程池,当我们对tomcat
服务器进行访问时,便会从线程池中获取线程,同时当使用数据库连接池时,在获取连接以后,当我们对线程访问完毕以后,需要对线程进行归还,此时归还到线程池中的线程还在绑定着数据库的连接,所以在归还连接(线程)前(无论是线程或数据库连接),都需要将线程与数据库连接进行解绑,否则当我们再获取这个线程时,因为它绑定着数据库连接池中的那个连接,再使用时是无法使用的,因为这个数据连接已经被close
(归还)了,需要我们重新获取连接并进行绑定。
通过创建service
的代理对象的工厂解决事务上方法的耦合问题。即对service
类中的方法进行增强(增强的内容就是加入事务的控制)。
事务解决的整个思路:
-
创建
ConnectionUtils
工具类,通过ThreadLocal
绑定数据库连接。/** * 线程绑定 */ public class ConnectionUtils { private ThreadLocal<Connection> tl = new ThreadLocal<Connection>(); // 绑定的对象 -- Connection private DataSource dataSource; // 获取数据源 public void setDataSource(DataSource dataSource) { this.dataSource = dataSource; } public Connection getThreadConnection() { Connection conn = tl.get(); if (conn == null) { // 如果TreadLocal未绑定有连接,则从连接池中获取连接,并对其进行绑定 try { conn = dataSource.getConnection(); tl.set(conn); } catch (SQLException e) { e.printStackTrace(); } } return conn; } /** * 对其进行解绑 */ public void remove() { Connection conn = tl.get(); if (conn != null) { tl.remove(); } } }
-
创建事务管理类,用于在业务层对
SQL
进行事务管理/** * 对事物进行管理的工具类 */ public class TransactionManager { private ConnectionUtils connectionUtils; public void setConnectionUtils(ConnectionUtils connectionUtils) { this.connectionUtils = connectionUtils; } /** * 开启事务 */ public void beginTransaction() { try { connectionUtils.getThreadConnection().setAutoCommit(false); // 关闭自动提交 -- 即使用此方法为开启事务(关闭了自动提交事务) } catch (SQLException e) { e.printStackTrace(); } } /** * 提交事务 */ public void commit() { try { connectionUtils.getThreadConnection().commit(); } catch (SQLException e) { e.printStackTrace(); } } /** * 回滚事务 */ public void rollback() { try { connectionUtils.getThreadConnection().rollback(); } catch (SQLException e) { e.printStackTrace(); } } /** * 释放连接 */ public void release() { try { connectionUtils.getThreadConnection().close(); connectionUtils.remove(); // 释放连接时,将线程与数据库连接池中的连接进行解绑 } catch (SQLException e) { e.printStackTrace(); } } }
-
dao
层代码public class AccountDaoImpl implements AccountDao { // 通过spring配置获取QueryRunner对象 private QueryRunner runner; // 通过spring配置获取连接 private ConnectionUtils connectionUtils; public void setConnectionUtils(ConnectionUtils connectionUtils) { this.connectionUtils = connectionUtils; } public void setRunner(QueryRunner runner) { this.runner = runner; } public void update(Account account, Integer id) { try { runner.update(connectionUtils.getThreadConnection(), "update account set name=?,money=? where id=?", account.getName(), account.getMoney(), id); } catch (SQLException e) { e.printStackTrace(); } } public Account findByName(String name) { Account account = null; try { List<Account> accounts = runner.query( "select * from account where name=?", new BeanListHandler<Account>(Account.class), name); if (accounts == null || accounts.size() == 0) { throw new RuntimeException("该用户不存在"); } else if (accounts.size() > 1) { throw new RuntimeException("用户存在异常,存在两个异常"); } else { account = accounts.get(0); } } catch (SQLException e) { e.printStackTrace(); } return account; }
connectionUtils.getThreadConnection()
用于获取连接。 -
创建工厂类-用于创建
service
的代理对象的工厂public class BeanFactory { private AccountService accountService; private TransactionManager tsManager; public final void setTsManager(TransactionManager tsManager) { this.tsManager = tsManager; } public void setAccountService(AccountService accountService) { this.accountService = accountService; } /** * 获取代理对象,对方法进行增强 * @return */ public AccountService getAccountService() { AccountService proxy_accountService = (AccountService) Proxy.newProxyInstance(accountService.getClass().getClassLoader(), accountService.getClass().getInterfaces(), new InvocationHandler() { /** * 增强对事务的控制 **/ public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { Object obj = null; try { // 1. 开启事务 tsManager.beginTransaction(); // 2. 执行语句 obj = method.invoke(accountService, args); // 3. 提交事务 tsManager.commit(); return obj; } catch (Exception e) { // 4. 回滚事务 tsManager.rollback(); throw new RuntimeException(e); } finally { // 5. 关闭连接 tsManager.release(); } } }); return proxy_accountService; } }
这里被代理的对象为
accountService
,当我们使用它的方法时,我们其实是在使用其代理对象(通过BeanFactory
中getAccountService
方法创建出的对象)中增强后的方法。其中,创建工厂中普通方法的配置方法:<!-- 创建工厂类对象 --> <bean id="beanFactory" class="cn.lizhi.factory.BeanFactory"> <!-- 被代理对象 --> <property name="accountService" ref="accountService"></property> <!-- 注入事务管理器 --> <property name="tsManager" ref="transaction"></property> </bean> <!-- 创建工厂类方法对象 --> <bean id="proxyAccountService" factory-bean="beanFactory" factory-method="getAccountService"></bean>
例如被代理对象(
AccountService
)中的一处方法为:public void transfer(String startName, String endName, Float money) { Account startAccount = dao.findByName(startName); // 出款人账号 -- 会获取连接 Account endAccount = dao.findByName(endName); // 收款人账号 -- 会获取连接 Float startMoney = startAccount.getMoney(); startAccount.setMoney(startMoney - money); Float endMoney = endAccount.getMoney(); endAccount.setMoney(endMoney + money); dao.update(startAccount, startAccount.getId()); // 会获取连接 int a = 3 / 0; dao.update(endAccount, endAccount.getId()); // 会获取连接 }
即通过代理对象增强后,对其方法进行了事务管理,即对以下方法的执行,在其上下添加事务的管理。
obj = method.invoke(accountService, args);
增强的方法等价于:
public void transfer(String startName, String endName, Float money) { try { // 1. 开启事务 tsManager.beginTransaction(); // 2. 执行操作 Account startAccount = dao.findByName(startName); // 出款人账号 -- 会获取连接 Account endAccount = dao.findByName(endName); // 收款人账号 -- 会获取连接 Float startMoney = startAccount.getMoney(); startAccount.setMoney(startMoney - money); Float endMoney = endAccount.getMoney(); endAccount.setMoney(endMoney + money); dao.update(startAccount, startAccount.getId()); // 会获取连接 int a = 3 / 0; dao.update(endAccount, endAccount.getId()); // 会获取连接 // 3. 提交事务 tsManager.commit(); } catch (Exception e) { // 4. 回滚事务 tsManager.rollback(); throw new RuntimeException(e); } finally { // 5. 关闭连接 tsManager.release(); } }
由以上的比较,故能够很清晰的看到,通过动态代理的方法,可以简化代码量以及降低方法间的耦合问题。
如果,采用上面一般的方式,即在业务层中所有的方法都要加入事务管理的相关代码,这样就增加了代码的冗余;其次,如果我们对
TransactionManager
类中关于事务管理的方法名进行修改,那么在业务层中相应调用事务的方法名也都要修改。如果,采用动态代理的方式:
- 我们关于事务管理的代码只需要写一次(工厂类中的代理对象)。
TransactionManage
类中事务管理相关的方法名修改后,只需要在代理对象中对增强的方法进行修改即可。
-
最后依赖注入的对象是代理对象
@Autowired @Qualifier("proxyAccountService") private AccountService service = null;
四、Spring AOP
4.1 AOP基本介绍
定义:
在软件业,AOP为Aspect Oriented Programming的缩写,意为:面向切面编程,通过预编译方式和运行期间动态代理实现程序功能的统一维护的一种技术。AOP是OOP的延续,是软件开发中的一个热点,也是Spring框架中的一个重要内容,是函数式编程的一种衍生范型。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。
作用:
- 在程序运行期间,不修改源码对已有方法进行增强。
优势:
- 减少重复代码
- 提高开发效率
- 维护方便
实现方式:
- 使用动态代理技术
4.2 Spring中的AOP介绍
4.2.1 AOP相关术语介绍及使用介绍
-
Joinpoint(连接点)
所谓连接点是指那些被拦截的点。在
Spring
中,这些点指的是方法,因为Spring
只支持方法类型的连接点。即:在业务逻辑中的全部方法。 -
Pointcut(切入点)
所谓切入点是指我们要对哪些
Joinpoint
进行拦截的定义。即:在业务逻辑中,那些被增强的方法。所以,有些方法是连接点,但不是切入点,因为没有被增强。所以得出:所有的切入点都是连接点,但并不一定所有的连接点都是切入点。(只有被增强的连接点,才是切入点)
-
Advice(通知/增强)
所谓通知,是指拦截到
Joinpoint
之后所要做的事情就是通知。(即,想要增加的功能,事先定义好,然后在想要用的地方添加通知即可)通知的类型:
- 前置通知:在
method.invoke()
之前执行的方法 - 后置通知:在
method.invoke()
之后执行的方法 - 异常通知:
catch
中的代码 - 最终通知:
finally
中的代码 - 环绕通知:整个
invoke
(public Object invoke..
)方法在执行就是环绕通知,即在环绕通知中有明确的切入点方法调用。 – 最强大的一个通知
- 前置通知:在
-
Introduction(引介)
引介是一种特殊的通知在不修改类代码的前提下,
Introduction
可以在运行期为类动态地添加一些方法或Field
。 -
Target(目标对象)
代理的目标对象。(被代理对象)
-
Weaving(织入)
是指把增强应用到目标对象来创建新的代理对象的过程。
Spring
采用动态代理织入,而AspectJ
采用编译期织入和类装载期织入。 -
Proxy(代理)
一个类被
AOP
织入增强后,就产生一个结果代理类。 -
Aspect(切面)
是切入点和通知(引介)的结合。
Spring
框架监控切入点方法的执行。一旦监控到切入点方法被运行,使用代理机制,动态创建目标对象的代理对象,根据通知类型,在代理对象的对应位置,将通知对应的功能织入,完成完整的代码逻辑运行。
将公共代码作为通知(即把通知Bean
交给Spring
来管理),由AOP
配置织入到切入点。
Logger
类作为通知:
/**
* 模拟记录日志的工具类,里面提供了公共的代码
*/
public class Logger {
/**
* 用于打印日志,计划让其在切入点方法执行之前执行(这里切入点方法就是业务层方法)
*/
public void printLog() {
System.out.println("记录了日志...");
}
}
以Logger
类为例,aop
相关配置步骤:
-
配置
Spring
的IOC
,把service
对象配置进来。 -
spring
中基于XML
的aop
配置步骤:-
把通知
Bean
交给spring
来管理 -
使用
aop:config
标签表明开始AOP
的配置 -
使用
aop:aspect
标签表明配置切面id
属性:是给切面提供一个唯一的标识ref
属性:是指定通知类bean
的Id
-
在
aop:aspect
标签的内部使用对应标签来配置通知的类型在
Logger
类中的pringLog
方法是在切入点方法执行之前,所以是前置通知aop:before
表示配置前置通知method
属性:用于指定Logger
类中哪个方法是前置通知。pointcut
属性:用于指定切入点表达式,该表达式的含义是对业务层中哪些方法增强
切入点表达式的写法:
-
关键字:
execution(表达式)
-
表达式:
访问修饰符 返回值 包名.包名.包名...类名.方法名(参数列表)
-
根据以上说明,见下方配置:
<?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:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd">
<!-- 配置Spring的IOC,把Service对象配置进来 -->
<bean id="accountService" class="cn.lizhi.service.impl.AccountServiceImpl"></bean>
<!-- 配置Logger类(通知) -->
<bean id="logger" class="cn.lizhi.utlis.Logger"></bean>
<!-- 配置AOP -->
<aop:config>
<!-- 配置切面 -->
<aop:aspect id="logAdvice" ref="logger">
<!-- 配置通知的类型,并且建立通知方法和切入点方法的关联-->
<aop:before method="printLog" pointcut="execution(public void cn.lizhi.service.impl.AccountServiceImpl.saveAccount())"></aop:before>
</aop:aspect>
</aop:config>
</beans>
-
切入点表达式的写法
-
全通配写法
* *..*.*(..)
原因:
-
访问修饰符可以省略
void cn.lizhi.service.impl.AccountServiceImpl.saveAccount()
-
返回值可以改成任意类型的返回值
* cn.lizhi.service.impl.AccountServiceImpl.saveAccount()
-
包名可以使用通配符,表示任意包。但是有几级包,就需要几个
*.
* *.*.*.*.AccountServiceImpl.saveAccount()
-
包名可以使用
..
表示当前包及其子包* *..AccountServiceImpl.saveAccount()
-
类名和方法名都可以使用
*
来实现通配* *..*.*()
-
参数列表:
-
可以直接写数据类型:
- 基本类型直接写名称 例如:
int
- 引用类型写包名.类名的方式 例如:
java.lang.String
- 基本类型直接写名称 例如:
-
可以使用通配符表示任意类型,但是必须有参数
-
可以使用
..
表示有无参数均可,有参数可以是任意类型* *..*.*(..)
-
实际开发中切入点表达式的通常写法:
-
切到业务层实现类下的所有方法
* cn.lizhi.service.impl.*.*(..)
总结:以上是正则表达式的应用,正则表达式中
*
代表0次或无限次扩展;.
代表任何单个字符。 -
-
4.1.2 四种常用通知类型
- 前置通知:在切入点方法执行之前执行,类比动态代理中的
method.invoke
执行之前的开启事务方法 –before
- 后置通知:在切入点方法正常执行之后执行。它和异常通知永远只能执行一个,类比事务中的
commit
–after-returning
- 异常通知:在切入点方法执行产生异常之后执行。它和后置通知永远只能执行一个,类比
catch
代码块中的rollback
。--after-throwing
- 最终通知:无论切入点方法是否正常执行,它都会在其后面执行,类比
finally
代码块。 –after
最终配置文件:
<?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:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd">
<!-- 配置Spring的IOC,把Service对象配置进来 -->
<bean id="accountService" class="cn.lizhi.service.impl.AccountServiceImpl"></bean>
<!-- 配置Logger类(通知) -->
<bean id="logger" class="cn.lizhi.utlis.Logger"></bean>
<!-- 配置AOP -->
<aop:config>
<aop:pointcut id="pt1" expression="execution(* cn.lizhi.service.impl.*.*(..))"/>
<!-- 配置切面 -->
<aop:aspect id="logAdvice" ref="logger">
<!-- 配置通知的类型,并且建立通知方法和切入点方法的关联-->
<aop:before method="beforePrintLog" pointcut-ref="pt1"></aop:before>
<aop:after-returning method="afterReturningPrintLog" pointcut-ref="pt1"></aop:after-returning>
<aop:after-throwing method="afterThrowingPrintLog" pointcut-ref="pt1"></aop:after-throwing>
<aop:after method="afterPrintLog" pointcut-ref="pt1"></aop:after>
</aop:aspect>
</aop:config>
</beans>
其中这个切入点表达式的配置
<aop:pointcut id="pt1" expression="execution(* cn.lizhi.service.impl.*.*(..))"/>
id
属性用于指定表达式的唯一标识。
expression
属性用于指定表达式内容。
同时,此标签写在aop:aspect
标签内部只能当前切面使用。
当它写在aop:aspect
外面时,此时就变成了所有切面可用。
4.1.3 环绕通知
4.1.3.1 问题
当我们配置了环绕通知之后,切入点方法没有执行,而通知方法执行了。
4.1.3.2 分析
通过对比动态代理中的环绕通知代码,发现动态代理的环绕通知有明确的切入点方法调用,而我们的代码代码中没有。
4.1.3.3 解决
Spring
框架为我们提供了一个接口:ProceedingJoinPoint
。该接口有一个方法proceed()
,此方法就相当于明确调用切入点方法。
该接口可以作为环绕通知的方法参数,在程序执行时,spring
框架会为我们提供该接口的实现类供我们使用。
使用:
public Object aroundPrintLog(ProceedingJoinPoint pjp) {
Object obj = null;
try {
System.out.println("Logger类中aroundPrintLog方法开始记录日志了...前置"); // 前置通知
Object[] args = pjp.getArgs(); // 得到方法执行所需要的参数
obj = pjp.proceed(args);// 明确调用业务层方法(切入点方法)
System.out.println("Logger类中aroundPrintLog方法开始记录日志了...后置"); // 后置通知
return obj;
} catch (Throwable throwable) {
System.out.println("Logger类中aroundPrintLog方法开始记录日志了...异常"); // 异常通知
throw new RuntimeException(throwable);
}finally {
System.out.println("Logger类中aroundPrintLog方法开始记录日志了...结束"); // 结束通知
}
}
从上面可以看出,环绕通知是Spring
框架为我们提供的一种可以在代码中手动控制增强方法何时执行的方式。
4.2.2 基于注解的AOP配置
-
在
bean.xml
配置Spring
创建容器时要扫描的包<!-- 配置扫描的包 --> <context:component-scan base-package="cn.lizhi"></context:component-scan> <!-- 配置Spring开启注解AOP的支持 --> <aop:aspectj-autoproxy></aop:aspectj-autoproxy>
-
在通知类中,开启注解
@Before
@AfterReturning
@AfterThrowing
@After
@Around
另外需要额外在通知类中,建立通知方法和切入点的关联,以上的通知注解中的参数就写关联方法的函数名:
@Pointcut("execution(* cn.lizhi.service.impl.*.*(..))") private void pt1() {} @Before("pt1()") public void beforePrintLog() { System.out.println("beforePrintLog记录了日志..."); }
最后需要在配置文件中,开启
Spring
对注解AOP
的支持:<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
或者采用纯注解的方式(定义配置类):
@Configuration @ComponentScan(basePackages="cn.lizhi") @EnableAspectJAutoProxy public class springConfiguration(){ }
**注意:**使用注解的方式,操作四个普通类型的通知,可能会带来顺序错误的问题。例如:
beforePrintLog记录了日志…前置
模拟用户保存了…
afterPrintLog记录了日志…最终
afterReturningPrintLog记录了日志…后置**注意:**这里出现了
最终通知
在后置通知
之前出现的问题。这样会在事务控制中产生一个问题,以上面的银行转账为例,就是最终通知–release
操作会在后置通知
–commit
操作之前执行,那么会造成获取的连接不会是同一个连接(每次调用dao
方法时,都会获取连接,由于前面我们使用了ThreadLocal
对连接进行了绑定,所以此时获取的连接都是同一个连接,当对其进行release
之后,再次获取连接时,那么和之前的连接就都不是同一个连接,因为在release
方法中首先对数据库连接进行归还连接池操作,然后再将ThreadLocal
和数据库连接进行解绑),就没有对事务进行相应的控制。因为当你commit
时,会从连接池中重新获取连接再与线程进行绑定,而此时的连接并没有做任何的操作,所以commit
就是一个空的提交。因此,在使用注解的方式时,尽量使用
环绕通知
代替以上四个普通通知
,因为环绕通知中的代码执行一定是由我们自己控制编写的。
五、Spring事务相关
5.1 Spring中JdbcTemplate 介绍
5.1.1 Spring JdbcTemplate的内置数据源
// 准备数据源,spring的内置数据源
DriverManagerDataSource ds = new DriverManagerDataSource();
ds.setDriverClassName("com.mysql.jdbc.Driver"); // 注入数据库驱动
ds.setUrl("jdbc:mysql://url:3306/draft"); // 连接地址
ds.setUsername("root"); // 用户名称
ds.setPassword("1234"); // 密码
// 1.创建JdbcTemplate对象
JdbcTemplate template = new JdbcTemplate();
// 注入数据源
template.setDataSource(ds);
通过配置文件,利用IOC
思想简化以上操作:
<?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">
<!-- 创建JdbcTemplate对象 -->
<bean id="template" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dateSource"></property>
</bean>
<!-- dataSource数据源 -->
<bean id="dateSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName" value="com.mysql.jdbc.Driver"></property>
<property name="url" value="jdbc:mysql://url/draft"></property>
<property name="username" value="username"></property>
<property name="password" value="password"></property>
</bean>
</beans>
代码实现:
public static void main(String[] args) {
ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");// 获取容器
JdbcTemplate template = ac.getBean("template", JdbcTemplate.class);// 获取template对象
template.execute("insert into account (name,money) values('Lisa',900)");// 执行操作
}
5.1.2 JdbcTemplate的使用 – CRUD
public static void main(String[] args) {
ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
JdbcTemplate template = ac.getBean("template", JdbcTemplate.class);
// template.execute("insert into account (name,money) values('Lisa',900)");
// 1.保存操作
template.update("insert into account(name,money) values(?,?)", "Ben", 800f);
// 2.更新操作
template.update("update account set name=?,money=? where id=?", "Gu", 900f, 5);
// 3.删除操作
template.update("delete from account where id=?", 6);
// 4.查询所有
List<Account> accounts = template.query("select * from account", new BeanPropertyRowMapper<Account>(Account.class));
// 5.查询单个
Account account = template.queryForObject("select * from account where id = ?", new BeanPropertyRowMapper<Account>(Account.class), 10);
}
5.1.3 JdbcDaoSupport的使用
可以用于抽取dao
层中的重复代码块。
例如:
dao
层的接口:
public interface AccountDao {
//更新数据
void updateAccount(Account account);
//查询所有
List<Account> findAll();
}
实现类:
public class AccountDaoImpl implements AccountDao {
private JdbcTemplate template = null;
public void setTemplate(JdbcTemplate template) {
this.template = template;
}
public void updateAccount(Account account) {
template.update("update account set name=?,money=? where id = ?", account.getName(), account.getMoney(), account.getId());
}
public List<Account> findAll() {
List<Account> lists = null;
try {
lists = template.query("select * from account", new BeanPropertyRowMapper<Account>(Account.class));
} catch (DataAccessException e) {
e.printStackTrace();
}
return lists;
}
}
其配置文件bean.xml
中bean
配置为:
<!--配置账户的持久层-->
<bean id="accountDao" class="cn.lizhi.dao.impl.AccountDaoImpl">
<property name="template" ref="template"></property>
</bean>
<!-- JdbcTemplate对象 -->
<bean id="template" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dateSource"></property>
</bean>
<!-- DataSource数据源 -->
<bean id="dateSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName" value="com.mysql.jdbc.Driver"></property>
<property name="url" value="jdbc:mysql:/url:3306/draft"></property>
<property name="username" value="username"></property>
<property name="password" value="password"></property>
</bean>
当我们有多个dao
的实现方法时,那么这里的重复代码块:
private JdbcTemplate template = null;
public void setTemplate(JdbcTemplate template) {
this.template = template;
}
所以这里就引发了我们的一个思考,是否可以把这些重复的代码块进行抽取,作为一个单独的类,然后当我们的实现类去继承这个类,进而简化我们的代码。答案是可以的。
现在我们对这个类进行抽取:
public class JdbcDaoSupport {
private JdbcTemplate jdbcTemplate = null;
public JdbcTemplate getJdbcTemplate() {
return jdbcTemplate;
}
public void setJdbcTemplate(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
}
那么我们原有的实现类就需要继承这个类:
public class AccountDaoImpl extends JdbcDaoSupport implements AccountDao {
public void updateAccount(Account account) {
super.getJdbcTemplate().update("update account set name=?,money=? where id = ?", account.getName(), account.getMoney(), account.getId());
}
public List<Account> findAll() {
List<Account> lists = null;
try {
lists = super.getJdbcTemplate().query("select * from account", new BeanPropertyRowMapper<Account>(Account.class));
} catch (DataAccessException e) {
e.printStackTrace();
}
return lists;
}
}
从上面的bean.xml
配置文件中,我们看到需要单独对JdbcTemplate
进行配置,我们是否可以将JdbcTemplate
和DataSource
进行统一配置呢。我们将DataSource
同样创建在JdbcDaoSupport
类中:
public class JdbcDaoSupport {
private JdbcTemplate jdbcTemplate = null;
private DataSource dataSource = null;
public JdbcTemplate getJdbcTemplate() {
return jdbcTemplate;
}
public void setDataSource(DataSource dataSource) {
if (jdbcTemplate == null) {
jdbcTemplate = createJdbcTemplate(dataSource);
}
}
private JdbcTemplate createJdbcTemplate(DataSource dataSource) {
return new JdbcTemplate(dataSource);
}
}
在这里我们创建了一个DataSource
对象,并改写了其set
方法。**作用:**如果我们直接传递template
对象,那么template
理所应当有值(有set
方法);如果我们没有传递template
对象,我们可以通过DataSource
来获取template
对象,由于我们改写了其set
方法,我们同样可以使template
有值(创建了有参构造函数的template
,并向其传递了dataSource
)。
此时bean.xml
配置文件中bean
的配置为:
<!--配置账户的持久层-->
<bean id="accountDao" class="cn.lizhi.dao.impl.AccountDaoImpl">
<property name="dataSource" ref="dateSource"></property>
</bean>
<bean id="dateSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName" value="com.mysql.jdbc.Driver"></property>
<property name="url" value="jdbc:mysql://url:3306/draft"></property>
<property name="username" value="username"></property>
<property name="password" value="password"></property>
</bean>
从上面的bean.xml
文件也可以看出,在创建accountDao
时,我们是向其注入了dataSource
,所以进而就直接触发其set
方法(因为accountDaoImpl
继承了JdbcDaoSupport
),判断template
是否有值,如果没有值,就调用其父类中的createJdbcTemplate
方法,其中传递的参数dataSource
是来自于bean.xml
配置文件中的id=dataSource
的bean
依赖注入。
以上的操作其实Spring
已经帮我们实现了,不需要我们自己手动构建(也算是自己练习了一下源码)。截取其部分源码:
public abstract class JdbcDaoSupport extends DaoSupport {
@Nullable
private JdbcTemplate jdbcTemplate;
public JdbcDaoSupport() {
}
public final void setDataSource(DataSource dataSource) {
if (this.jdbcTemplate == null || dataSource != this.jdbcTemplate.getDataSource()) {
this.jdbcTemplate = this.createJdbcTemplate(dataSource);
this.initTemplateConfig();
}
}
protected JdbcTemplate createJdbcTemplate(DataSource dataSource) {
return new JdbcTemplate(dataSource);
}
@Nullable
public final JdbcTemplate getJdbcTemplate() {
return this.jdbcTemplate;
}
}
是不是同样的存在createJdbcTemplate(DataSource dataSource)
和getJdbcTemplate()
方法。从上面的源码中也可以看出Spring
在使用set
依赖注入时,可以不用定义其变量,只要定义其set
属性方法即可。
最后,以上这种继承的方法适用于xml
配置文件的方法,不适用于注解开发的方式(因为无法在jar
包中的JdbcTemplate
上加注解,注入我们的数据类型)。
5.2 Spring中事务控制的API
5.2.1 Spring事务控制
JavaEE
体系进行分层开发,事务处理处于业务层,Spring
提供了分层设计业务层的事务处理解决方案。Spring
框架为我们提供了一组事务控制的接口Spring
的事务控制都是基于AOP
的,它既可以使用编程的方式实现,也可以使用配置的方法实现。
依赖导入:
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<version>5.0.2.RELEASE</version>
</dependency>
5.2.2 Spring中事务控制的API介绍
-
PlatformTransactionManager
public interface PlatformTransactionManager { TransactionStatus getTransaction(@Nullable TransactionDefinition var1) throws TransactionException; void commit(TransactionStatus var1) throws TransactionException; void rollback(TransactionStatus var1) throws TransactionException; }
此接口存在
commit
、rollback
方法,即为通知bean
。PlatformTransactionManager
接口提供事务操作的方法,包含有3个具体的操作:
-
获取事务状态信息
-
TransactionStatus getTransaction(@Nullable TransactionDefinition var1)
-
提交事务
void commit(TransactionStatus var1)
-
回滚事务
void rollback(TransactionStatus var1)
我们需要使用的是其实现类:
-
org.springframework.jdbc.datasource.DataSourceTransactionManager
使用
Spring JDBC
或iBatis
进行持久化数据时使用 -
org.springframework.orm.hibernate5.HibernateTransactionManager
使用
Hibernate
版本进行持久化数据时使用
-
-
TransactionDefinition
-
获取事务对象名称
String getName()
-
获取事务隔离级别:有四个隔离级别,
Spring
默认使用的是数据库的隔离级别int getIsolationLevel()
-
获取事务传播行为:定义什么时候需要用事务(增、删、改),什么时候事务可有可无(查询)
int getPropagationBehavior()
-
获取事务超时时间
int getTimeout()
-
获取事务是否只读(建议查询方法下为只读)
boolean isReadOnly()
读写型事务:增加、删除、修改开启事务
只读型事务:执行查询时,也会开启事务
-
5.2.3 事务的隔离级别
事务隔离级别反映事务提交并发访问时的处理态度
ISOLATION_DEFAULT
- 默认级别,归属下列某一种
ISOLATION_READ_UNCOMMITTED
- 可以读取未提交数据
ISOLATION_READ_COMMITTED
- 只能读取已提交数据,解决脏读问题(
Oracle
默认级别)
- 只能读取已提交数据,解决脏读问题(
ISOLATION_REPEATABLE_READ
- 是否读取其他事务提交修改后的数据,解决不可重复读问题(
MySQL
默认级别)
- 是否读取其他事务提交修改后的数据,解决不可重复读问题(
ISOLATION_SERIALIZABLE
- 是否读取其他事务提交添加后的数据,解决幻读问题
5.2.4 事务的隔离级别
REQUIRED
:如果当前没有事务,就新建一个事务,如果已经存在一个事务中,加入到这个事务中。一般的选择(默认值)SUPPORTS
:支持当前事务,如果当前没有事务,就以非事务方式执行(没有事务)MANDATORY
:使用当前的事务,如果当前没有事务,就抛出异常REQUERS_NEW
:新建事务,如果当前在事务中,把当前事务挂起。NOT_SUPPORTED
:以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。NEVER
:以非事务方式运行,如果当前存在事务,抛出异常。NESTED
:如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行REQUIRED
类似的操作。
5.2.5 TransactionStatus接口
此接口提供的是事务具体的运行状态,TransacitonStatus
接口描述了某个时间点上事务对象的状态信息,包含有6个具体的操作:
- 刷新事务
void flush()
- 获取是否存在存储点(相当于回滚的断点,回滚时不是回滚到最初的开始,而是回滚到设置的存储点处)
boolean hasSavepoint()
- 获取事务是否完成
boolean isCompleted()
- 获取事务是否为新的事务
boolean isNewTransaction()
- 获取事务是否回滚
boolean isRollbackOnly()
- 设置事务回滚
void setRollbackOnly()
5.3 Spring中基于XML
的声明式事务控制配置步骤
-
配置事务管理器
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dateSource"></property> </bean>
-
配置事务的通知
此时我们需要导入事务的约束
tx
名称空间和约束,同时也需要aop
使用
tx:advice
标签配置事务通知- 属性:
id
:给事务通知起一个唯一标识transaction-manager
:给事务通知提供一个事务管理器引用
<!-- 配置事务通知 --> <tx:advice id="txAdvice" transaction-manager="transactionManager"> </tx:advice>
- 属性:
-
配置
AOP
中的通用切入点表达式<aop:config> <!-- 通用表达式配置 --> <aop:pointcut id="pt1" expression="execution(* cn.lizhi.service.*.*(..))"/> </aop:config>
-
建立事务通知和切入点表达式的对应关系
<aop:config> <!-- 通用表达式配置 --> <aop:pointcut id="pt1" expression="execution(* cn.lizhi.service.*.*(..))"/> <aop:advisor advice-ref="txAdvice" pointcut-ref="pt1"></aop:advisor> </aop:config>
-
配置事务的属性 –
tx:attributes
是在事务的通知
tx:advice
标签的内部tx:method
name
属性:表明业务层中哪个方法需要被事务控制。
配置事务的属性:
isolation
:用于指定事务的隔离级别。默认值是DEFAULT
,表示使用数据库的默认隔离级别。propagation
:用于指定事务的传播行为。默认值是REQUIRED
,表示一定会有事务,增删改的选择。查询方法可以选择SUPPORTS
。read-only
:用于指定事务是否只读。只有查询方法才能设置为true
。默认值是false
,表示读写。timeout
:用于指定事务的超时时间,默认值是-1,表示永不超时。如果指定了数值,以秒为单位。rollback-for
:用于指定一个异常。当产生该异常时,事务回滚,产生其他异常时,事务不回滚。没有默认值,表示任何异常都回滚。no-rollback-for
:用于指定一个异常,当产生异常时,事务不回滚,产生其他异常时事务回滚。没有默认值,表示任何异常都回滚。
<!-- 配置事务通知 --> <tx:advice id="txAdvice" transaction-manager="transactionManager"> <tx:attributes> <!-- 增删改事务的控制 --> <tx:method name="transfer" propagation="REQUIRED" read-only="false"/> <!-- 查询方法事务的控制 --> <tx:method name="find*" propagation="SUPPORTS" read-only="true"/> </tx:attributes> </tx:advice>
需要命名规范,通过通配符配置方法名。
最终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:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd">
<!--配置账户的持久层-->
<bean id="accountDao" class="cn.lizhi.dao.impl.AccountDaoImpl">
<property name="dataSource" ref="dateSource"></property>
</bean>
<!-- 配置账户的业务层 -->
<bean id="accountService" class="cn.lizhi.service.impl.AccountServiceImpl">
<property name="dao" ref="accountDao"/>
</bean>
<!-- 配置数据源 -->
<bean id="dateSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName" value="com.mysql.jdbc.Driver"></property>
<property name="url" value="jdbc:mysql://url:3306/draft"></property>
<property name="username" value="username"></property>
<property name="password" value="password"></property>
</bean>
<!-- 配置事务管理器 -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dateSource"></property>
</bean>
<!-- 配置事务通知 -->
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<tx:attributes>
<tx:method name="transfer" propagation="REQUIRED" read-only="false"/>
<tx:method name="find*" propagation="SUPPORTS" read-only="true"/>
</tx:attributes>
</tx:advice>
<!-- AOP配置 -->
<aop:config>
<!-- 通用表达式配置 -->
<aop:pointcut id="pt1" expression="execution(* cn.lizhi.service.*.*(..))"/>
<aop:advisor advice-ref="txAdvice" pointcut-ref="pt1"></aop:advisor>
</aop:config>
</beans>
5.4 Spring中基于注解的声明式事务控制配置步骤
-
配置事务管理器
<!-- 配置事务管理器 --> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dateSource"></property> </bean>
-
开启
spring
对注解事务的支持<tx:annotation-driven transaction-manager="transactionManager"></tx:annotation-driven>
-
在需要事务支持的地方使用
@Transaction
注解
以上中的注意事项有:因为是使用注解式配置,所以在dao
层,不能通过继承JdbcDaoSupport
的方式简化我们的代码,故需要在bean.xml
中对SpringJdbcTemplate
进行配置。
5.4.1 基于纯注解的声明式事务控制
通过配置类的方式实现。
-
SpringConfig
配置类@ComponentScan("cn.lizhi") @Import({JdbcConfig.class, TransactionConfig.class}) @PropertySource("classpath:jdbcConfig.properties") @EnableTransactionManagement public class SpringConfig { }
-
JdbcConfig
配置类public class JdbcConfig { @Value("${driver}") private String driver; @Value("${username}") private String username; @Value("${password}") private String password; @Value("${url}") private String url; @Bean("template") public JdbcTemplate getJdbcTemplate(DataSource dataSource) { return new JdbcTemplate(dataSource); } @Bean("dataSource") public DataSource getDataSource() { DriverManagerDataSource dataSource = new DriverManagerDataSource(); dataSource.setDriverClassName(driver); dataSource.setUsername(username); dataSource.setPassword(password); dataSource.setUrl(url); return dataSource; } }
-
TransactionConfig
配置类public class TransactionConfig { @Bean("transactionManager") public DataSourceTransactionManager getTransactionManager(DataSource dataSource) { DataSourceTransactionManager transactionManager = new DataSourceTransactionManager(); transactionManager.setDataSource(dataSource); return transactionManager; } }
5.5 Spring编程式事务控制
因为我们的事务管理都是由Spring
进行控制,所以我们都需要进行事务管理器的配置。
<!-- 配置事务管理器 -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dateSource"></property>
</bean>
当有了事务管理器之后,Spring
同样为我们提供了事务模板,供我们具体使用事务控制的相关方法(相当于代理对象,对业务层方法进行增强):
<!-- 事务模板对象 -->
<bean id="transactionTemplate" class="org.springframework.transaction.support.TransactionTemplate">
<property name="transactionManager" ref="transactionManager"/>
</bean>
查看具体的TransactionTemplate
源码,其中一段为:
@Nullable
public <T> T execute(TransactionCallback<T> action) throws TransactionException {
Assert.state(this.transactionManager != null, "No PlatformTransactionManager set");
if (this.transactionManager instanceof CallbackPreferringPlatformTransactionManager) {
return ((CallbackPreferringPlatformTransactionManager)this.transactionManager).execute(this, action);
} else {
TransactionStatus status = this.transactionManager.getTransaction(this);
Object result;
try {
result = action.doInTransaction(status);
} catch (Error | RuntimeException var5) {
this.rollbackOnException(status, var5);
throw var5;
} catch (Throwable var6) {
this.rollbackOnException(status, var6);
throw new UndeclaredThrowableException(var6, "TransactionCallback threw undeclared checked exception");
}
this.transactionManager.commit(status);
return result;
}
}
在上面的异常代码块中,可以看出,当执行不通过时,执行this.rollbackOnException(status, var5)
,进行事务回滚;当执行通过时,try
代码块中执行事务状态result = action.doInTransaction(status)
,最后执行this.transactionManager.commit(status)
,进行事务的提交。
具体使用:
模仿TransactionTemplate
中的execute
方法,在业务层中需要事务控制的方法中,执行该方法,该方法中的参数是一个接口,需要我们自己实现,其内容就填写我们需要控制的业务具体代码块。
public void transfer(final String startName, final String endName, final Float money) {
template.execute(new TransactionCallback<Object>() {
public Object doInTransaction(TransactionStatus transactionStatus) {
Account startAccount = dao.findByName(startName); // 出款人账号 -- 会获取连接
Account endAccount = dao.findByName(endName); // 收款人账号 -- 会获取连接
Float startMoney = startAccount.getMoney();
startAccount.setMoney(startMoney - money);
Float endMoney = endAccount.getMoney();
endAccount.setMoney(endMoney + money);
dao.update(startAccount, startAccount.getId()); // 会获取连接
// int a = 3 / 0;
dao.update(endAccount, endAccount.getId()); // 会获取连接
return null;
}
});
}
由于编程式的事务控制,又增加了代码的冗余,违背了AOP
的初心,所以实际中很少使用这种事务控制方式。