1、创建bean的三种方式
1.1 方式一:
<bean id="accountService" class="com.itheima.service.impl.IAccountServiceImpl">
在spring的配置文件中使用bean标签,仅仅以id和class属性。采用的就是默认构造函数创建bean对象。
如果此类中没有默认构造方法,则对象无法创建。
Failed to instantiate [com.itheima.service.impl.IAccountServiceImpl]: No default constructor found;
1.2 方式二:
使用普通工厂中的方法创建对象(使用某个类中的方法创建对象,并存入spring容器)
普通工厂:普通方法
```
public class InstanceFactory {
public IAccountService getAccountService(){
return new IAccountServiceImpl();
}
}
```
* 看到这个类,我们可能会出现这样的困惑:
getAccountService()方法中也使用了new,这不也是耦合了吗? 我们要知道可能存在的这种情况:这是一个模拟的工厂类,该类可能是存在jar包中的,他是一个class文件我们无法通过修改其源码的方式来提供默认构造函数。 所以既然是jar包中的类,我们没必要着眼与方法中的具体实现,是要我们不new这个类即可。我们要的是。将其中的方法的返回值return new IAccountServiceImpl()放到我们的spring容器中。
```
<bean id="instanceFactory" class="com.itheima.factory.InstanceFactory"></bean>
<bean id="accountService" factory-bean="instanceFactory" factory-method="getAccountService"></bean>
```
* 分析一下这两句配置:
首先,public IAccountService getAccountService()这是一个普通的方法,需要对象调用。
1.先指定这个工厂的默认构造函数:得到instanceFactory对象
<bean id="instanceFactory" class="com.itheima.factory.InstanceFactory"></bean>
2.怎么得到这个方法呢?
bean id="accountService":指定要得到的对象的id——IAccountService
factory-bean="instanceFactory":指定那个工厂bean
factory-method="getAccountService":指定这个工厂类中我们要用到的方法
1.2 方式三:
使用静态工厂中的静态方法(使用某个类中的静态方法创建对象,并存入spring容器)
* 静态工厂:静态方法
```
public class StaticFactory {
public static IAccountService getAccountService(){
return new IAccountServiceImpl();
}
}
```
* 我们要意识到,需要调用的是静态方法,所以不用实例化那个工厂类。
即我们直接指定对应的静态方法即可。
```
<bean id="accountService" class="com.itheima.factory.StaticFactory"
factory-method="getAccountService"></bean>
```
* 分析一下这句配置
我要用id="accountService"来取StaticFactory中的getAccountService得到的结果。
而且我们没有创建StaticFactory对象,所以getAccountService是一个静态方法。
2 bean对象的生命周期及作用范围
* 那么我们怎么判断要创建的对象是单例还是多例呢?
* bean标签的scope属性:
作用:用于指定bean的作用范围
取值:
singleton:单例(默认值)
prototype:多例
request:作用于web应用的请求范围
session:作用于web应用的会话范围
global-session:作用于集群环境的会话范围(全局会话范围,当不是集群环境就只是session)
* 相应的配置:
```
<bean id="accountService" class="com.itheima.service.impl.IAccountServiceImpl" scope="singleton"></bean>
```
* 扩展:global-session全局session
由于实际情况下后台的服务器是多个的。我们需要多台服务器都能共享一些数据,那么就将这些数据,在global中,这是一个全局会话,这几台服务器都能得到里面的信息。
2.1 单例对象
1.基本概念
出生:当容器创建时,对象出生。
活着:只要容器还在,对象一直活着
死亡:容器销毁,对象消亡
```
<bean id="accountService" class="com.itheima.service.impl.IAccountServiceImpl"
scope="prototype" init-method="init" destroy-method="destroy"></bean>
```
init-method:指定类中的初始化方法名称
destroy-method:指定类中的销毁方法名称
2. 手动关闭容器
main是一切应用程序的入口,当main方法结束之后,当前应用中线程占据的内存全部释放。
当然也包含创建的容器,但是此时并没有调用销毁方法destroy,就已经把内存释放了。所以说还没有来得及调用销毁,容器就已经消失了。要是想要调用销毁方法,怎么做呢?———— 手动关闭容器。
* 注意:
close()方法存在于ApplicationContext的子类中,ApplicationContext本类没有该方法
前面applicationContext对象我们是通过多态的方式实例化的。
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("bean.xml");
此时applicationContext被看成了父类的对象,那么调用的方法编译时看的是父类的方法。如果父类有这个方法,那么可以编译通过,执行的是子类的方法。
close()方法存在于ApplicationContext的子类ClassPathXmlApplicationContext中,所以我们要想用.close()方法,实例化ClassPathXmlApplicationContext,就不要用多态
ClassPathXmlApplicationContext cac = new ClassPathXmlApplicationContext("bean.xml");
classApplicationContext.close();
3. 多例对象
出生:当使用对象时,spring框架才为我们创建对象
活着:对象在使用过程中一直活着。
死亡:spring虽然很强大,但是它仍然无法知道我们这个对象要用到什么时候,什么时候销毁。所以它不会轻易的将这个对象销毁的。
* 当对象长时间不用且没有别的对象引用时,由java的垃圾回收器回收
2.2 依赖注入
* 依赖注入的方式:
1. 使用构造方法提供
2. 使用set方法提供
3. 使用注解提供
* 能注入的数据:
基本数据类型和String
其他的bean类型(在配置文件中/注解 配置过的bean,即在spring容器中体现过的bean)
复杂类型/集合类型
2.2.1 构造方法注入
```
public IAccountServiceImpl(String name, Integer age, Date birthday){
this.name = name;
this.age = age;
this.birthday = birthday;
}
```
使用的标签: constructor-arg
标签出现的位置:bean标签内部
标签中的属性:
type:用于指定要注入的数据的数据类型,该数据类型也是构造函数中某个或某些参数的类型
如果有两个String属性的参数,type就无法指定哪个第一个哪个放第二个
index:用于指定要注入的参数在构造函数中的索引位置。参数索引从0开始
name:用于指定,给构造函数中指定名称的参数赋值(常用)
* 以上三个用于指定给构造函数中哪个参数赋值
value:用于提供基本类型和String类型的数据的值
ref:用于指定其他的bean类型数据,它指的就是在spring的IOC核心容器中出现过的bean对象。
就是在xml或注解中配置过这个bean,那都可以通过ref来引用。
配置文件bean.xml代码
```
<bean id="accountService" class="com.itheima.service.impl.IAccountServiceImpl">
<constructor-arg name="age" value="18"></constructor-arg>
<constructor-arg name="name" value="test"></constructor-arg>
<constructor-arg name="birthday" ref="now"></constructor-arg>
</bean>
<bean id="now" class="java.util.Date"></bean>
```
除非必要,一般不用。
2.2.2 set方法
1. 需要注入的Bean类
```
private String name;
private Integer age;
private Date birthday;
public void setQQName(String name) {
this.name = name;
}
public void setAge(Integer age) {
this.age = age;
}
public void setBirthday(Date birthday) {
this.birthday = birthday;
}
```
2. bean.xml配置:
使用的标签:property
出现的位置:bean标签的位置
标签的属性:
name:用于指定,给构造函数中指定名称的参数赋值(常用)
value:用于提供基本类型和String类型的数据的值
ref:用于指定其他bean类型数据,它指的就是在spring的IOC核心容器中出现过的bean对象。就是在xml或注解中配置过这个bean,那都可以通过ref来引用。
3. bean.xml代码
```
<bean id="accountService2" class="com.itheima.service.impl.IAccountServiceImpl2">
<property name="age" value="21"></property>
<property name="birthday" ref="now2"></property>
<property name="QQName" value="撕撕撕"></property>
</bean>
<bean id="now2" class="java.util.Date"></bean>
```
4. 优势:
很灵活,没有明确的限制,不影响使用默认构造方法
2.2.3 复杂类型注入
1.要注入的Bean类:成员变量都是数组,集合,map等。
```
private String[] myStr;
private List<String> myList;
private Set<String> mySet;
private Map<String, String> myMap;
private Properties myProps;
public void setMyStr(String[] myStr) {
this.myStr = myStr;
}
public void setMyList(List<String> myList) {
this.myList = myList;
}
public void setMySet(Set<String> mySet) {
this.mySet = mySet;
}
public void setMyMap(Map<String, String> myMap) {
this.myMap = myMap;
}
public void setMyProps(Properties myProps) {
this.myProps = myProps;
}
```
2. 相应的bean.xml配置
```
<bean id="accountService3" class="com.itheima.service.impl.IAccountServiceImpl3">
<property name="myStr" >
<array>
<value>AAA</value>
<value>BBB</value>
<value>CCC</value>
</array>
</property>
<property name="myList" >
<list>
<value>qweq</value>
<value>q55q</value>
<value>888q</value>
</list>
</property>
<property name="myMap">
<map>
<entry key="testa" value="aaaa"></entry>
<entry key="testb">
<value>bbbb</value>
</entry>
</map>
</property>
<property name="mySet">
<set>
<value>12..as</value>
<value>%$^.as</value>
<value>omg</value>
</set>
</property>
<property name="myProps">
<props>
<prop key="testC">cccc</prop>
<prop key="testD">dddd</prop>
</props>
</property>
</bean>
```
3 注解的使用
3.1 Aurowired Qualifier Resource Value
1.Autowired:
作用:自动按照类型注入。
只要容器中有唯一的一个bean对象类型和要注入的变量类型匹配就可以注入成功。
*注意这里是唯一。如果没有匹配的就会报错。
2.@Qualifier
作用:在按照类中注入的基础上在按照名称注入。它在给成员注入时不能单独使用(搭配@Autowired)。
*但是在给方法参数注入时可以单独使用。 value:用于指定注入bean的id
```
@Autowired
@Qualifier("accountDao1")
private IAccountDao accountDao1 = null;
```
3.@Resource
作用:直接按照bean的id注入。它可以独立使用
属性:name用于指定bean的id
```
@Resource(name="accountDao1")
private IAccountDao accountDao1 = null;
```
***以上三个注入都只能注入其他bean类型的数据,而基本类型和String类型无法使用上述注解实现,另外集合类型的注入只能通过XML来实现。
4.@Value:
作用:用于注入基本类型和String类型的数据
属性:value用于指定数据的值,它可以使用spring中的SpEl(也就是spring的el表达式)
SpEL的写法:$(表达式)
3.2 Scope PostConstruct PreDestroy
1.用于指定ApplicationContext接口是创建单例还是多例对象的:
ApplicationContext接口可以根据我们的需要来创建单例/多例:我们怎么发出需求,
通过scope。如果配置的scope取值为singleton那么单例创建,如果配置的scope取值为
prototype那么多例创建。
属性:value
指定范围的取值,常用取值:singleton prototype(默认情况下是单例的)
2.PostConstruct:用于指定初始化方法
3.PreDestroy:用于指定销毁方法
---IOC容器关闭时.close()才会执行该方法。
注意:其关闭容器的close()方法,是ApplicationContext子类的方法,@PreDestroy要想起作用。容器对象必须是ClassPathXmlApplicationContext对象。
注意:如果注解的方法所在的类,是通过构造器多例创建的那么容器不能通过close()方法关闭,spring不负责销毁。因为它不知道你什么时候用完,所以只有单例时才可以用这个注解。
```
public static void main(String[] args) {
ClassPathXmlApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
IAccountService as = (IAccountService)ac.getBean("iAccountServiceImpl");
as.saveAccount();
ac.close();
}
@Scope(value = "singleton")
public class IAccountServiceImpl implements IAccountService {
@Resource(name="accountDao1")
private IAccountDao accountDao1 = null;
@PostConstruct
public void init(){
System.out.println("初始化方法执行");
}
@PreDestroy
public void destroy(){
System.out.println("销毁方法执行了");
}
}
```
4.单表crud操作
4.1 使用xml方式
1.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"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<!--配置service对象-->
<bean id="accountService" class="com.itheima.service.impl.AccountServiceImpl">
<!--通过set方法:注入dao-->
<property name="accountDao" ref="accountDao"></property>
</bean>
<!--配置Dao对象-->
<bean id="accountDao" class="com.itheima.dao.impl.AccountDaoImpl">
<!--通过set方法:注入QueryRunner-->
<property name="runner" ref="queryRunner"></property>
</bean>
<!--配置注入QueryRunner对象:由于现在是单表的CRUD操作,所以可以选择传入数据源(连接池)。
注意这里获取的QueryRunner默认是单例的,这样就可能出现线程问题,所以用多例模式创建
即:每次使用QueryRunner对象都会创建一个新的QueryRunner对象。
-->
<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://localhost:3306/txl_spring"></property>
<property name="user" value="root"></property>
<property name="password" value="root"></property>
</bean>
</beans>
```
注意:
1.配置AccountServiceImpl Bean对象
2.通过set属性方法(property),注入AccountServiceImpl中的Bean类型的成员变量:AccountDaoImpl
3.配置AccountDaoImpl Bean对象
4.通过set属性方法(property),注入AccountDaoImpl的Bean类型的成员变量:QueryRunner
5.配置QueryRunner Bean对象
*6.通过构造方法(constructor-arg)注入DataSource
```
public QueryRunner(DataSource ds) {
super(ds);
}
```
*7注入(property)DataSource的4个属性
2.注入QueryRunner对象:
1.由于现在是单表的CRUD操作,所以可以选择传入数据源(连接池)。(考虑到事务)
2.注意这里获取的QueryRunner默认是单例的,这样就可能出现线程问题,所以用多例模式创建每次使用QueryRunner对象都会创建一个新的QueryRunner对象。
```
<bean id="queryRunner" class="org.apache.commons.dbutils.QueryRunner" scope="prototype">
<!--注入数据源-->
<constructor-arg name="ds" ref="dataSource"></constructor-arg>
</bean>
```
2. Test
```
public void testfindAll() {
//1.获取容器
ApplicationContext container = new ClassPathXmlApplicationContext("bean.xml");
/**
* 2.得到业务层对象:
* 由于IAccountServiceImpl有IAccountDaoImpl成员属性
* IAccountDaoImpl有QueryRunner成员属性
* 所以同时得到了IAccountServiceImpl对象,IAccountDaoImpl对象,QueryRunner对象
*/
IAccountService accountService = container.getBean("accountService", IAccountService.class);
//3.执行方法
List<Account> accounts = accountService.findAllAccount();
for (Account account : accounts) {
System.out.println(account);
}
}
```
4.2 使用注解方式
1. bean.xml
* 文件头要改:context
* 告知spring:在创建容器时要扫描的包
<context:component-scan base-package="com.itheima"></context:component-scan>
* 只留下注入QueryRunner和数据源
```
<?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/>
<!--告知spring:在创建容器时要扫描的包-->
<context:component-scan base-package="com.itheima"></context:component-scan>
<!--配置注入QueryRunner对象:由于现在是单表的CRUD操作,所以可以选择传入数据源(连接池)。注意这里获取的QueryRunner默认是单例的,这样就可能出现线程问题,所以用多例模式创建
即:每次使用QueryRunner对象都会创建一个新的QueryRunner对象。
-->
<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://localhost:3306/txl_spring"></property>
<property name="user" value="root"></property>
<property name="password" value="root"></property>
</bean>
</beans>
```
2. AccountServiceImpl和AccountDaoImpl
* 需要注入的属性就不需要set方法了。
* 创建Bean:
@Service("accountService")
@Repository("accountDao")
* 注入(引用类型)属性
@Resource(name="accountDao")
@Resource(name="queryRunner")
4.2 使用注解有哪些瓶颈
1. 我们目前学习的注解:
*1 只能加到自己写的类上面,所以如果将其他jar包中的类也交给spring来创建或注入的话,
只能用xml来配置,这是我们目前面临的一个问题。
比如此例中:QueryRunner是外部jar包中的一个类,显然不能在上面加注解。
*2 还有告知spring要扫描的包,我们也要在xml中写。
4.3 实现纯注解的步骤
想要去掉XML中的配置,全部用注解Anno来完成这个案例:
我们可以手动创建一个配置类来完成之前XML配置中剩下的内容。
SpringConfiguration类
```
package config; //在java资源文件下建立config包
@Configuration
@ComponentScan(value={"com.itheima"})
public class SpringConfiguration {
}
```
1.来看一下这两个注解
* 1.@Configuration
作用:指定当前类是一个配置类 SpringConfiguration
* 2.ComponentScan
作用:通过该注解指定spring在创建容器时要扫描的包
属性:value和basePackages互为别名,所以是一样的。
都是数组,所以标准写法:
value={"com.itheima", "xxx"}
@ComponentScan(value={"com.itheima"}):用于指定创建容器时要扫描的包。
* 那么现在我们就解决了遗留的第一个XML配置
```
<!--告知spring:在创建容器时要扫描的包-->
<context:component-scan base-package="com.itheima"></context:component-scan>
```
2. 第二个问题:怎么通过注解的方式让其他jar包中的类也交给spring来创建或注入
<bean id="queryRunner" class="org.apache.commons.dbutils.QueryRunner" scope="prototype">
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
发现:可以通过调用构造函数来实例化QueryRunner和DataSource,QueryRunner是有参构造,
DataSource是无参构造。
1 继续完善SpringConfiguration
我们分析一下:
```
1.<bean id="queryRunner" class="org.apache.commons.dbutils.QueryRunner" scope="prototype">
```
```
2.public QueryRunner creatQueryRunner(DataSource dataSource){
return new QueryRunner(dataSource);
}
```
这两者都能创建一个quertRunner对象,但是前者创建后会放到IOC容器中,而后者不会放在IOC中。
2 新的注解:@Bean 标注在方法上,将返回值放到容器中。
作用:用于把当前方法中的返回值bean对象存入springIOC容器中。
属性:name用于指定bean的id。默认值:当不指定时:当前方法的名称
细节:
1.当我们使用注解配置时,如果方法有参数,spring框架会去容器中
查找有没有可用的bean对象。查找的方式和AutoWired的注解方式
是一样的:自动按照类型注入,如果有唯一的类型直接注入,如果
有多个再按照变量名注入。
```
@Bean(name="queryRunner")
@Scope(value="prototype") //注意QueryRunner我们需要多例创建
public QueryRunner creatQueryRunner(DataSource dataSource){
return new QueryRunner(dataSource);
}
```
即参数DataSource dataSource也会去容器中匹配相应的bean对象,匹配
方式同@AutoWerid的引用类型注入。
2.注意这里指定的@Bean(name="queryRunner"),也就是要用queryRunner取
容器中的QueryRunner对象,所以如果其他类中要注入QueryRunner对象时,
要注意指明name是queryRunner。
3.这种也是调用构造方法创建的QueryRunner对象,而且我们没有指明是多例创建,
所以默认也是单例创建,针对这个获取数据源连接池的QueryRunner对象多例
创建,再加上:
@Scope(value="prototype")
```
@Bean(name="queryRunner")
@Scope(value="prototype") //注意QueryRunner我们需要多例创建
public QueryRunner creatQueryRunner(DataSource dataSource){
return new QueryRunner(dataSource);
}
@Bean("dataSource")
public DataSource creatDataSource(){
ComboPooledDataSource ds = new ComboPooledDataSource();
try {
ds.setDriverClass("com.mysql.jdbc.Driver");
ds.setJdbcUrl("jdbc:mysql://localhost:3306/txl_spring");
ds.setUser("root");
ds.setPassword("root");
return ds;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
```
3. 这个时候我们就完全用注解实现了这个案例:可以不用xml配置文件了
最后:修改一下获取容器的方式
用注解方式获容器,不用加载配置文件的方式。
Test方法中:
* 之前的获取方式:
//获取容器
ApplicationContext container = new ClassPathXmlApplicationContext("bean.xml");
* 现在的方式
//注解方式:获取容器的方法也变了,参数是被@Configuration注解过的类(可以使多个)。
ApplicationContext container = new AnnotationConfigApplicationContext(SpringConfiguration.class);
注意:参数是被@Configuration注解过的类(可以有多个)。
4.4 配置类的写法
@ComponentScan("com.itheima")
@Import(JdbcConfig.class)
@PropertySource(value="classpath:jdbcConfig.properties")
public class SpringConfiguration {
}
public class JdbcConfig {
@Value("${jdbc.driver}")
private String driver;
@Value("${jdbc.url}")
private String url;
@Value("${jdbc.username}")
private String username;
@Value("${jdbc.password}")
private String password;
/**
* 用于创建一个QueryRunner对象
* @param dataSource
* @return
*/
@Bean(name="runner")
@Scope("prototype")
public QueryRunner createQueryRunner(@Qualifier("ds2") DataSource dataSource){
return new QueryRunner(dataSource);
}
/**
* 创建数据源对象
* @return
*/
@Bean(name="ds2")
public DataSource createDataSource(){
try {
ComboPooledDataSource ds = new ComboPooledDataSource();
ds.setDriverClass(driver);
ds.setJdbcUrl(url);
ds.setUser(username);
ds.setPassword(password);
return ds;
}catch (Exception e){
throw new RuntimeException(e);
}
}
@Bean(name="ds1")
public DataSource createDataSource1(){
try {
ComboPooledDataSource ds = new ComboPooledDataSource();
ds.setDriverClass(driver);
ds.setJdbcUrl("jdbc:mysql://localhost:3306/eesy02");
ds.setUser(username);
ds.setPassword(password);
return ds;
}catch (Exception e){
throw new RuntimeException(e);
}
}
}
4.5 容器中有多个相同类型的参数吻合方法参数时:
同@Autowired注解规则一样。我们想到@Qualifier,
但是@Qualifier在给类成员标注时必须搭配@Autowired。我们又发现:
@Qualifier是可以标注在方法参数中的:ElementType.PARAMETER,
而且此时不需要搭配同@Autowired。所以我们为形参指定id(key):
4.6 spring和junit整合
1.导入spring整合junit的jar包(坐标)
```
<!--spring整合junit的jar包-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>5.0.2.RELEASE</version>
</dependency>
```
2.使用JUNIT提供的一个注解把原有的main方法替换掉,替换成spring提供的
@RunWith(SpringJUnit4ClassRunner.class)
Class<? extends Runner> value(); 里面的值必须是一个继承Runner的字节码类。
SpringJUnit4ClassRunner extends BlockJUnit4ClassRunner
SpringJUnit4ClassRunner是Spring提供的,它一定会为我们提供容器。它还得知道
你有的是XML配置文件还是注解。配置文件放哪了,配置类是谁?
3. 告知Spring的运行器:
spring的IOC创建是基于 是XML配置文件还是注解,并且说明位置。
@ContextConfiguration
locations:指定xml文件的位置,加上classpath关键字,表示在类路径下。
classes:指定注解类所在的位置
@ContextConfiguration(locations = "classpath:bean.xml")
@ContextConfiguration(classes = SpringConfiguration.class)
4. 小细节:版本要求
当我们使用spring 5.x版本时,要求junit的jar必须是4.12及以上。这样
在运行测试方法时,SpringJUnit4ClassRunner才会为我们创建IOC容器。
/**
* 使用Junit单元测试:测试我们的配置
*/
@RunWith(SpringJUnit4ClassRunner.class)//改变main方法
@ContextConfiguration(locations = "classpath:bean.xml")//创建容器
public class AccountServiceTest {
@Autowired
private IAccountService as;
@Test
public void testTransfer(){
as.transfer("aaa","bbb",100f);
}
}
<!--以下是bean.xml的配置-->
<context:component-scan base-package="com.itheima"></context:component-scan>
<!--配置QueryRunner-->
<bean id="runner" class="org.apache.commons.dbutils.QueryRunner" scope="prototype"></bean>
<!-- 配置数据源 -->
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
<!--连接数据库的必备信息-->
<property name="driverClass" value="com.mysql.cj.jdbc.Driver"></property>
<property name="jdbcUrl" value="jdbc:mysql://localhost:3306/eesy?useSSL=false&serverTimezone=GMT"></property>
<property name="user" value="root"></property>
<property name="password" value="root"></property>
</bean>
5 事务
5.1 的示例:
*我在业务层中加入一个方法:模拟转账
```
public void transfer(String sourceName, String targetName, float money) {
//1.根据名称查询转出账户
Account source = accountDao.findAccountByName(sourceName);
//2.根据名称查询转入账户
Account target = accountDao.findAccountByName(targetName);
//3.转出账户减钱
source.setMoney(source.getMoney() - money);
//4.转入账户加钱
target.setMoney(target.getMoney() + money);
//5.更新转出账户
accountDao.updateAccount(source);
int i = 1/0; //模拟转账异常
//6.更新转入账户
accountDao.updateAccount(target);
}
```
2.执行之后发现:
由于有异常,转账失败。
但是转出账户的钱减少了,转入账户的钱却没有增加。
3. 分析:
1.事务管理的问题
首先应该确定,我们的代码中是有事务提交的,因为其中的增删改方法都能够正常执行且没有回滚的。
如果没有事务,那么增删改显然是无法提交的。
换句话说,不是没有事务造成的而是事务的管理出现了问题。
# 5.2.分析QueryRunner对象
1.前面提到每次用QueryRunner时,都会再次创建一个新的对象,同时也会从数据池中拿出一个新的连接。
2.整个过程,模拟转账代码和数据库交互了4次。那么这样就会有4个connection对象。
而每个connection也都有自己的独立事务,那么情况就是:
第一个connection成功执行了---事务提交
第二个connection成功执行了---事务提交
第三个connection成功执行了---事务提交
第四个connection没成功----------事务不提交
3.解决方式:使用ThreadLocal获取的Connection对象,来控制事务使用唯一一个Connection
*** 明确一点:之前的代码中是有事务提交的,因为其中的增删改方法都能够正常执行且没有回滚的。如果没有事务,那么增删改显然是无法提交的。换句话说,不是没有事务造成的而是事务的管理出现了问题。
5.1 手写一个事务管理器
```连接池工具类
public class ConnectionUtils {
private ThreadLocal<Connection> tl = new ThreadLocal<Connection>();
@Autowired
private DataSource dataSource;
<!--这个set方法是为了在xml配置ConnectionUtils时可通过set方法注入-->
public void setDataSource(DataSource dataSource) {
this.dataSource = dataSource;
}
/**
* 获取当前线程上的连接
* 控制事务,是靠connection把手动提交改成自动提交
* 再通过commit和rollback来控制事务。
*/
public Connection getThreadConnection(){
try{
//1.先从ThreadLocal上获取
Connection coon = tl.get();
//2.判断当前线程上是否有连接
if(coon == null){
//3.从数据源中获取一个连接,并且存入ThreadLocal中
coon = dataSource.getConnection();
//4.存入ThreadLocal
tl.set(coon);
}
//5.返回当前线程上的连接
return coon;
}catch (Exception e){
throw new RuntimeException(e);
}
}
/**
* 把连接和线程解绑
*/
public void removeConnection(){
tl.remove();
}
}
```
```事务管理器代码
public class TransactionManager {
@Autowired
private ConnectionUtils connectionUtils;
//等待Spring将其注入
public void setConnectionUtils(ConnectionUtils connectionUtils) {
this.connectionUtils = connectionUtils;
}
//开启事务
public void beginTransaction(){
try{
connectionUtils.getThreadConnection().setAutoCommit(false);
} catch (Exception e){
e.printStackTrace();
}
}
//提交事务
public void commit(){
try{
connectionUtils.getThreadConnection().commit();
} catch (Exception e){
e.printStackTrace();
}
}
//回滚事务
public void rollback(){
try{
connectionUtils.getThreadConnection().rollback();
} catch (Exception e){
e.printStackTrace();
}
}
//释放连接
public void release(){
try{
connectionUtils.getThreadConnection().close();
//解绑
connectionUtils.removeConnection();
} catch (Exception e){
e.printStackTrace();
}
}
}
```
``` service层代码
public void transfer(String sourceName, String targetName, float money) {
try {
//1.开启事务
txManager.beginTransaction();
//2.执行操作
//1.根据名称查询转出账户
Account source = accountDao.findAccountByName(sourceName);
//2.根据名称查询转入账户
Account target = accountDao.findAccountByName(targetName);
//3.转出账户减钱
source.setMoney(source.getMoney() - money);
//4.转入账户加钱
target.setMoney(target.getMoney() + money);
//5.更新转出账户
accountDao.updateAccount(source);
int i = 1/0; //模拟转账异常
//6.更新转入账户
accountDao.updateAccount(target);
//3.提交事务
txManager.commit();
} catch (Exception e){
//4.回滚操作
txManager.rollback();
e.printStackTrace();
}finally {
//5.释放连接
txManager.release();
}
}
```dao层代码
public List<Account> findAllAccount() {
try {
return runner.query(connectionUtils.getThreadConnection(), "select * from account", new BeanListHandler<Account>(Account.class));
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public Account findAccountById(Integer accountId) {
try {
return runner.query(connectionUtils.getThreadConnection(),"select * from account where id = ?", new BeanHandler<Account>(Account.class), accountId);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
5.2 改进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) {
System.out.println("transfer...");
//1.根据名称查询转出账户
Account source = accountDao.findAccountByName(sourceName);
//2.根据名称查询转入账户
Account target = accountDao.findAccountByName(targetName);
//3.转出账户减钱
source.setMoney(source.getMoney() - money);
//4.转入账户加钱
target.setMoney(target.getMoney() + money);
//5.更新转出账户
accountDao.updateAccount(source);
//int i = 1/0; //模拟转账异常
//6.更新转入账户
accountDao.updateAccount(target);
}
}
```
2. 创建AccountServiceImpl的代理对象的工厂: BeanFactory
```
public class BeanFactory {
private IAccountService accountService;
public final void setAccountService(IAccountService accountService) {
this.accountService = accountService;
}
private TransactionManager txManager;
public void setTxManager(TransactionManager txManager) {
this.txManager = txManager;
}
public IAccountService getAccountService(){
IAccountService proxyAccountService =
(IAccountService)Proxy.newProxyInstance(accountService.getClass().getClassLoader(),accountService.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 rtValue = null;
try {
//1.开启事务
txManager.beginTransaction();
//2.执行操作
rtValue = method.invoke(accountService, args);
//3.提交事务
txManager.commit();
//4.返回结果
return rtValue;
} catch (Exception e){
//5.回滚操作
txManager.rollback();
throw new RuntimeException(e);
}finally {
//6.释放连接
txManager.release();
}
}
});
return proxyAccountService;
}
}
```
6 spring之aop
6.1基本术语
1.Joinpoint(连接点):
所谓连接点是指那些被拦截到的点,在spring中,这些点指的是方法,因为
spring只支持方法类型的连接点。
2.Pointout(切入点)
所谓切入点是指我们要对哪些Joinpoint进行增强。
我们可以指定Service层哪些方法被增强,哪些方法不被增强。
```
if("test".equals(method.getName())){
// 如果是test函数:按照真实对象中的方法执行:没有事务支持
// 调用method对象的invoke方法,并且指明该method是accountService中的method,且传递的参数args也不改变
return method.invoke(accountService, args);
}
```
3. Advice(通知/增强)
所谓通知是指拦截到的Joinpoint之后要做的事情就是通知。
通知的类型:
前置通知
后置通知
异常通知
最终通知
环绕通知
4.Introduction(引介)
引介是一种特殊的通知,在不修改类代码的前提下,Introduction可以在运行期为
类动态地添加一些方法或Field
5.Target(目标对象)
代理的目标对象:被代理对象
6.Weaving(织入)
是指把增强应用到目标对象来创建新的代理对象的过程。
7. Proxy(代理)
一个类被AOP织入增强后,就产生的一个结果代理类。
8.Aspect(切面):
是切入点和通知(引介)的结合。
例如:
被增强的方法:transfer()
通知:就是那些提供了公共代码的类中的方法。如之前写的transactionManager中的开启事务,提交事务,回滚事务,释放连接的方法
切面:建立切入点方法和通知方法在执行调用的先后关系
6.2 Spring基于XML的AOP配置步骤
* 要用AOP的配置,所以要在bean.xml中将AOP的约束放进来。ctrl+F : 搜索xmlns:aop
* 导入aspectjweaver依赖
aspectj是一个软件联盟:能解析出我们在bean.xml中配置的切入点表达式。
1.把通知类(Logger)bean也交给spring来管理
2.使用aop:config标签表明开始AOP的配置
3.使用aop:aspect标签开始配置切面
id属性:给切面提供一个唯一标识 随便取:logAdvice()表明是,log通知
ref属性:使用指定通知类(Logger)的bean的id。
4.在aop:aspect标签的内部使用对应标签来配置通知的类型
我们现在的示例是让通知类(Logger)中的printLog方法在切入点之前执行,所以是前置通知。
aop:before:表示配置前置通知
method属性:用于指定Logger类中的哪个方法是前置通知
5.pointcut属性:用于指定对业务层中哪些方法增强,
切入点表达式的写法:
关键字:execution(表达式)
表达式:访问修饰符 返回值 包名.包名.包名... 类名.方法名(参数列表)
标准的表达式写法 public void com.itheima.service.impl.AccountServiceImpl.saveAccount()
6.3 四种通知类型
1. aop:before :配置前置通知的类型
在切入点方法之前执行
2. aop:after-returning :配置后置通知的类型----代表着切入点方法成功执行。
在切入点方法成功的执行之后才会执行后置通知
3. after-throwing:配置异常通知的类型
切入点方法出现异常后,会执行异常通知
4. aop:after :配置最终通知的类型
在finally()里面的通知,无论切入点方法是否正常之心,一定会执行。
最多执行三个:
后置通知和异常通知只有一个能执行成功。
6.4 环绕通知
如下代码没有明确的切入点调用
我们也要在环绕通知里调用切入点方法
Spring框架为我们提供了一个接口:ProceedingJoinPoint。
该接口有一个方法proceed()
此方法就相当于明确调用切入点方法。该接口可以作为环绕通知的方法参数,
在程序执行时,spring框架会为我们提供该接口的实现类供我们使用。
简单来说:拿去用就行了,其他的spring都给你弄好了。
修改原来的环绕通知方法:aroundPrintLog()
* 传入参数 ProceedingJoinPoint pjp
* 调用其中的方法:pjp.proceed()
* 增强方法:通知
System.out.println("Logger类中的环绕通知aroundPrintLog()开始记录日志了");
写在pjp.proceed()之前就是前置
写在pjp.proceed()之后就是后置
写在catch里面就是异常
写在finally里面就是最终
最后这整个aroundPrintLog()就变成了环绕通知。
*注意:注意:此处异常必须写Throwable t ; Exception拦不住它
6.5 spring中基于XML和注解的AOP配置
1.改成IOC的注解:改成context的名称空间---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: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/aop
http://www.springframework.org/schema/aop/spring-aop.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
```
2.bean.xml的其他配置
<!--配置spring创建容器时要扫描的包-->
<context:component-scan base-package="com.itheima"></context:component-scan>
<!--配置springAOP的支持-->
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
3. 也可以把一点XML都不写:写一个配置类 SpringConfiguration
4. 注解通知类
@Component("logger")
@Aspect//表示当前类是一个切面类
public class Logger {
@Around("pt1()")
public Object aroundPringLog(ProceedingJoinPoint pjp){
Object rtValue = null;
try{
Object[] args = pjp.getArgs();//得到方法执行所需的参数
System.out.println("Logger类中的aroundPringLog方法开始记录日志了。。。前置");
rtValue = pjp.proceed(args);//明确调用业务层方法(切入点方法)
System.out.println("Logger类中的aroundPringLog方法开始记录日志了。。。后置");
return rtValue;
}catch (Throwable t){
System.out.println("Logger类中的aroundPringLog方法开始记录日志了。。。异常");
throw new RuntimeException(t);
}finally {
System.out.println("Logger类中的aroundPringLog方法开始记录日志了。。。最终");
}
}
@Pointcut("execution(* com.itheima.service.impl.*.*(..))")
private void pt1(){}
/**
* 前置通知
*/
@Before(value = "pt1()")
public void beforePrintLog(){
System.out.println("前置通知Logger类中的beforePrintLog方法开始记录日志了。。。");
}
/**
* 后置通知
*/
@AfterReturning("pt1()")
public void afterReturningPrintLog(){
System.out.println("后置通知Logger类中的afterReturningPrintLog方法开始记录日志了。。。");
}
/**
* 异常通知
*/
@AfterThrowing("pt1()")
public void afterThrowingPrintLog(){
System.out.println("异常通知Logger类中的afterThrowingPrintLog方法开始记录日志了。。。");
}
/**
* 最终通知
*/
@After("pt1()")
public void afterPrintLog(){
System.out.println("最终通知Logger类中的afterPrintLog方法开始记录日志了。。。");
}
**顺序错乱问题:如图
7 spring中基于XML和注解的AOP配置
7.1 纯配置实现事务控制
1.实体类: Account
```
@Data
public class Account implements Serializable {
private Integer id;
private String name;
private Float money;
...
}
```
2.账户的业务层实现类:AccountServiceImpl
```
public class AccountServiceImpl implements IAccountService {
private IAccountDao accountDao;
public void setAccountDao(IAccountDao accountDao){
this.accountDao = accountDao;
}
public void transfer(String sourceName, String targetName, float money) {
System.out.println("transfer...");
//1.根据名称查询转出账户
Account source = accountDao.findAccountByName(sourceName);
//2.根据名称查询转入账户
Account target = accountDao.findAccountByName(targetName);
//3.转出账户减钱
source.setMoney(source.getMoney() - money);
//4.转入账户加钱
target.setMoney(target.getMoney() + money);
//5.更新转出账户
accountDao.updateAccount(source);
//int i = 1/0; //模拟转账异常
//6.更新转入账户
accountDao.updateAccount(target);
}
}
```
3.账户的持久层实现类
```
public class AccountDaoImpl implements IAccountDao {
private QueryRunner runner;
private ConnectionUtils connectionUtils;
public void setConnectionUtils(ConnectionUtils connectionUtils) {
this.connectionUtils = connectionUtils;
}
public void setRunner(QueryRunner runner){
this.runner = runner;
}
public void updateAccount(Account account) {
try {
runner.update(connectionUtils.getThreadConnection(),"update account set name = ?, money = ? where id = ?", account.getName(),account.getMoney(), account.getId());
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public Account findAccountByName(String accountName) {
try {
List<Account> accounts = runner.query(connectionUtils.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 (Exception e) {
throw new RuntimeException(e);
}
}
}
```
4.连接工具类
从数据源中获取一个连接,并且实现和线程的绑定
```
public class ConnectionUtils {
//1.ThreadLocal对象,用泛型指定管理对象是java.sql.Connection
private ThreadLocal<Connection> tl = new ThreadLocal<Connection>();
@Autowired
private DataSource dataSource;
public void setDataSource(DataSource dataSource) {
this.dataSource = dataSource;
}
/**
* 获取当前线程上的连接
* 控制事务,是靠connection把手动提交改成自动提交
* 再通过commit和rollback来控制事务。
* @return
*/
public Connection getThreadConnection(){
try{
//1.先从ThreadLocal上获取
Connection coon = tl.get();
/**
* 2.判断当前线程上是否有连接:
*/
if(coon == null){
//3.从数据源中获取一个连接,并且存入ThreadLocal中
coon = dataSource.getConnection();
//4.存入ThreadLocal
tl.set(coon);
}
//5.返回当前线程上的连接
return coon;
}catch (Exception e){
throw new RuntimeException(e);
}
}
/**
* 把连接和线程解绑
*/
public void removeConnection(){
tl.remove();
}
}
```
5.事务管理类
```
public class TransactionManager {
@Autowired
private ConnectionUtils connectionUtils;
//等待Spring将其注入
public void setConnectionUtils(ConnectionUtils connectionUtils) {
this.connectionUtils = connectionUtils;
}
//开启事务
public void beginTransaction(){
try{
connectionUtils.getThreadConnection().setAutoCommit(false);
} catch (Exception e){
e.printStackTrace();
}
}
// 提交事务
public void commit(){
try{
connectionUtils.getThreadConnection().commit();
} catch (Exception e){
e.printStackTrace();
}
}
//回滚事务
public void rollback(){
try{
connectionUtils.getThreadConnection().rollback();
} catch (Exception e){
e.printStackTrace();
}
}
// 释放连接
public void release(){
try{
connectionUtils.getThreadConnection().close();
//解绑
connectionUtils.removeConnection();
} catch (Exception e){
e.printStackTrace();
}
}
}
```
6.配置AOP切面。(添加事务/为切入点方法添加通知)
```
<?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">
<!--配置service对象-->
<bean id="accountService" class="com.itheima.service.impl.AccountServiceImpl">
<property name="accountDao" ref="accountDao"></property>
</bean>
<!--配置Dao对象-->
<bean id="accountDao" class="com.itheima.dao.impl.AccountDaoImpl">
<property name="runner" ref="queryRunner"></property>
<property name="connectionUtils" ref="connectionUtils"></property>
</bean>
<!--配置QueryRunner对象-->
<bean id="queryRunner" class="org.apache.commons.dbutils.QueryRunner" scope="prototype">
</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://localhost:3306/txl_spring"></property>
<property name="user" value="root"></property>
<property name="password" value="root"></property>
</bean>
<!--配置Connection的工具类:ConnectionUtils-->
<bean id="connectionUtils" class="com.itheima.utils.ConnectionUtils">
<property name="dataSource" ref="dataSource"></property>
</bean>
<!--配置事务管理类:TransactionManager
也就是提供了重复代码的通知类,将其交给spring来管理。
-->
<bean id="transactionManager" class="com.itheima.utils.TransactionManager">
<property name="connectionUtils" ref="connectionUtils"></property>
</bean>
<!--配置AOP-->
<aop:config>
<!--配置一个通用的切入点表达式-->
<aop:pointcut id="pt1" expression="execution(* com.itheima.service.impl.*.*(..))"/>
<aop:aspect id="txAdvice" ref="transactionManager">
<!--配置前置通知:开启事务-->
<aop:before method="beginTransaction" pointcut-ref="pt1"></aop:before>
<!--配置后置通知:提交事务-->
<aop:after-returning method="commit" pointcut-ref="pt1"></aop:after-returning>
<!--配置异常通知:回滚事务-->
<aop:after-throwing method="rollback" pointcut-ref="pt1"></aop:after-throwing>
<!--配置最终通知:释放连接-->
<aop:after method="release" pointcut-ref="pt1"></aop:after>
</aop:aspect>
</aop:config>
```
7.2 配置和注解实现事务控制
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: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/aop
http://www.springframework.org/schema/aop/spring-aop.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<!--配置spring创建容器时要扫描的包-->
<context:component-scan base-package="com.itheima"></context:component-scan>
<!--配置QueryRunner对象-->
<bean id="queryRunner" class="org.apache.commons.dbutils.QueryRunner" scope="prototype">
</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://localhost:3306/txl_spring"></property>
<property name="user" value="root"></property>
<property name="password" value="root"></property>
</bean>
<!--开启spring对注解AOP的支持-->
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
</beans>
```
3.事务管理类
```
@Component("transactionManager")
@Aspect
public class TransactionManager {
@Resource(name="connectionUtils")
private ConnectionUtils connectionUtils;
@Pointcut("execution(* com.itheima.service.impl.*.*(..))")
private void pt1(){
}
/**
* 开启事务
*/
@Before("pt1()")
public void beginTransaction(){
try{
connectionUtils.getThreadConnection().setAutoCommit(false);
} catch (Exception e){
e.printStackTrace();
}
}
/**
* 提交事务
*/
@AfterReturning("pt1()")
public void commit(){
try{
connectionUtils.getThreadConnection().commit();
} catch (Exception e){
e.printStackTrace();
}
}
/**
* 回滚事务
*/
@AfterThrowing("pt1()")
public void rollback(){
try{
connectionUtils.getThreadConnection().rollback();
} catch (Exception e){
e.printStackTrace();
}
}
/**
* 释放连接
*/
@After("pt1()")
public void release(){
try{
connectionUtils.getThreadConnection().close();
//解绑
connectionUtils.removeConnection();
} catch (Exception e){
e.printStackTrace();
}
}
}
```
4.连接工具类
```
@Component("connectionUtils")
public class ConnectionUtils {
private ThreadLocal<Connection> tl = new ThreadLocal<Connection>();
@Autowired
private DataSource dataSource;
public Connection getThreadConnection(){
try{
//1.先从ThreadLocal上获取
Connection coon = tl.get();
if(coon == null){
//3.从数据源中获取一个连接,并且存入ThreadLocal中
coon = dataSource.getConnection();
coon.setAutoCommit(false);
//4.存入ThreadLocal
tl.set(coon);
}
//5.返回当前线程上的连接
return coon;
}catch (Exception e){
throw new RuntimeException(e);
}
}
public void removeConnection(){
tl.remove();
}
}
```
5.业务层
```
@Service("accountService")
public class AccountServiceImpl implements IAccountService {
@Autowired
private IAccountDao accountDao;
public List<Account> findAllAccount() {
return accountDao.findAllAccount();
}
public Account findAccountById(Integer accountId) {
return accountDao.findAccountById(accountId);
}
public void saveAccount(Account account) {
accountDao.saveAccount(account);
}
public void updateAccount(Account account) {
accountDao.updateAccount(account);
}
public void deleteAccount(Integer accountId) {
accountDao.deleteAccount(accountId);
}
public void transfer(String sourceName, String targetName, float money) {
System.out.println("transfer...");
//1.根据名称查询转出账户
Account source = accountDao.findAccountByName(sourceName);
//2.根据名称查询转入账户
Account target = accountDao.findAccountByName(targetName);
//3.转出账户减钱
source.setMoney(source.getMoney() - money);
//4.转入账户加钱
target.setMoney(target.getMoney() + money);
//5.更新转出账户
accountDao.updateAccount(source);
//int i = 1/0; //模拟转账异常
//6.更新转入账户
accountDao.updateAccount(target);
}
}
```
6.持久层...
7. 分析:
先回想一下,注解实现AOP的一个小问题:先调用最终通知,再调用异常通知/后置通知。
所以先调用最终通知就会将coon关闭了,再将coon与当前线程解绑。
```
@After("pt1()")
public void release(){
try{
connectionUtils.getThreadConnection().close();
//解绑
connectionUtils.removeConnection();
} catch (Exception e){
e.printStackTrace();
}
}
```
那么再调用后置通知时:会先调用connectionUtils.getThreadConnection().commit();
发现没有连接了,它会再从数据源中拿一个新的coon然后绑上去。
为什么可以拿新的coon,因为我们在ConnectionUtils类中写的方法:
connectionUtils.getThreadConnection()
前置通知执行完了(因为前置通知会将autocommit置false),才从数据源中拿一个新的
coon绑上去。注意从数据池中得到的新的coon的autocommit=true,所以再提交commit就会报错:
Can't call commit when autocommit=true
那我们在ConnectionUtils.getThreadConnection()获取新的coon时,将其AutoCommit设为false。
设置成false之后,那么可以提交了,但是数据库中的数据会改吗?
因为刚才操作的那个coon被先关闭了,而现在这个新的coon虽然你提交,没有异常产生了。但是 新的coon里面没有任何的执行操作,什么都提交不了。
8. 所以这就是Spring基于注解的配置AOP时:通知的执行调用是有问题的,
如果非要用注解:我们用环绕通知。
7.3 Spring提供的工具来进行事务控制
*概述
1. JavaEE体系进行分层开发,事务处理位于业务层,Spring提供了分层设计业务层的事务处理解决方案。
2. spring框架为我们提供了一组事务控制的接口。具体在后面的第二小节介绍。这组接口是在
spring-tx-5.0.2.RELEASE.jar中。
所以要先导入依赖
3. spring的事务控制都是基于AOP的,它既可以使用编程的方式实现,也可以使用配置的方式实现。
4. 注意:我们学习的重点是使用配置的方式实现。
7.3.1 Spring基于XML的事务控制
步骤:
1. 配置spring的事务管理器
这个里面有提交和回滚等方法。然后在第2步,指明其是通知。
2. 配置事务的通知
此时我们要导入事务的约束: tx的名称空间和约束,同时也需要aop的约束 xmlns:tx
使用tx:advice配置事务通知
属性:
id:给事务通知起一个唯一标志
transaction-manager:给事务通知提供一个事务管理器引用
3. 配置AOP中的通用切入点表达式
4. 建立事务通知和切入点表达式的对应关系
5. 配置事务的属性
是在事务的通知tx:advice标签的内部
明确:因为用的是spring自带的事务管理器,里面有提交和回滚等方法。所以我们只要指定管理器和切入点方法的联系即可。不用我们在设置里面具体的执行步骤。
<!--1.配置事务管理器--> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource"></property> </bean> <!--2.配置事务的通知--> <tx:advice id="txAdvice" transaction-manager="transactionManager"> <tx:attributes> <!--4.为service中的所有方法,配置事务的属性 isolation="":指定事务的隔离级别,默认值DEFAULT表示使用数据库的默认隔离级别 no-rollback-for="":用于指定一个异常,产生该异常时不回滚,其他异常会滚。没有默认值,表示任何异常都会滚。 propagation="":指定事务的传播级别:默认REQUIRED:没有就新建一个,有就添加进去。适合增删改。SUPPORTS:没事务就用,没有就不用。适合查询 read-only="":事务是否只读:查询方法可以设为只读true,增删改设为读写。 rollback-for="":用于指定一个异常,产生该异常时回滚,其他异常不会滚。没有默认值,表示任何异常都会滚。 timeout="" :指定事务的超时时间:,默认-1,永不超时。指定数值以秒为单位。 --> <tx:method name="*" propagation="REQUIRED" read-only="false"/> <!--为service中的findxxx()方法,配置事务的属性 这个优先级是高于上面的全通配的 --> <tx:method name="find*" propagation="SUPPORTS" read-only="true"/> </tx:attributes> </tx:advice> <!--3.配置AOP--> <aop:config> <!--3.1配置通用点表达式--> <aop:pointcut id="pt1" expression="execution(*com.itheima.service.impl.*.*(..))"/> <!--3.2建立切入点表达式和事务通知的对应关系--> <aop:advisor advice-ref="txAdvice" pointcut-ref="pt1"></aop:advisor> </aop:config>
7.3.2 Spring基于注解和XML结合的事务控制
1.配置事务管理器
```
<!--1.配置事务管理器-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"></property>
</bean>
```
2.开启spring对注解事务的支持
```
<tx:annotation-driven transaction-manager="transactionManager"></tx:annotation-driven>
```
3.在需要事务支持的地方使用@Transactional注解
不仅仅在类上,可以指定到具体的方法
7.3.2 Spring基于注解的事务控制 (没有bean.xml)
1.账户的持久层实现类
```
@Repository("accountDao")
public class AccountDaoImpl implements IAccountDao {
@Autowired
private JdbcTemplate jdbcTemplate;
//1.按方法名查询一个
public Account findAccountByName(String accountName) {
}
//2.更新账户
public void updateAccount(Account account) {
}
```
2.账户的业务层实现类:让事务控制和业务层的方法分离
在需要事务支持的地方使用@Transactional注解
```
@Service("accountService")
@Transactional //事务的属性全在里面:read-only/timeout/isolation...
public class AccountServiceImpl implements IAccountService {
@Transactional(propagation = Propagation.REQUIRED, readOnly = false)
public void transfer(String sourceName, String targetName, Float money) {
Account source = accountDao.findAccountByName(sourceName);
Account target = accountDao.findAccountByName(targetName);
source.setMoney(source.getMoney() - money);
target.setMoney(target.getMoney() + money);
accountDao.updateAccount(source);
//int i = 1/0; //模拟转账异常
accountDao.updateAccount(target);
}
}
```
3. 连接资源配置文件:jdbcConfig.properties
```
jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/txl_spring
jdbc.username=root
jdbc.password=root
```
4. 3个配置类
主配置类:public class SpringConfiguration,引入子配置类
子配置类1:public class JdbcConfig,和连接数据库相关的配置类
子配置类2:public class TransactionConfig,和事务相关的配置类
4.1: 子配置类1:public class JdbcConfig,和连接数据库相关的配置类
```
public class JdbcConfig {
@Value("${jdbc.driver}")
private String driver;
@Value("${jdbc.url}")
private String url;
@Value("${jdbc.username}")
private String username;
@Value("${jdbc.password}")
private String password;
//创建JdbcTemplate对象
@Bean(name="jdbcTemplate")
public JdbcTemplate createJdbcTemplate(DataSource dataSource){
return new JdbcTemplate(dataSource);
}
//创建数据源对象
@Bean(name="dataSource")
public DataSource createDataSource(){
DriverManagerDataSource ds = new DriverManagerDataSource();
ds.setDriverClassName(driver);
ds.setUrl(url);
ds.setPassword(password);
ds.setUsername(username);
return ds;
}
}
```
4.2: 子配置类2:public class JdbcConfig,public class TransactionConfig,和事务相关的配置类
```
public class TransactionConfig {
/**
* 创建管理器对象
* PlatformTransactionManager 接口
* new DataSourceTransactionManager(dataSource); 返回它的子类
* @param dataSource
* @return
*/
@Bean(name="transactionManager")
public PlatformTransactionManager createTransactionManager(DataSource dataSource){
return new DataSourceTransactionManager(dataSource);
}
}
```
4.3 : 主配置类:public class SpringConfiguration
```
/**
* spring的配置类,相当于bean.xml
*/
@Configuration //这个注解可写可不写:因为可以把这个类当参数传递给测试方法
@ComponentScan("com.itheima")
@Import({JdbcConfig.class,TransactionConfig.class})
@PropertySource("classpath:jdbcConfig.properties") //指明连接资源
@EnableTransactionManagement //开启注解支持
public class SpringConfiguration {
}
```
5. 测试方法---spring整合的junit
```
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = SpringConfiguration.class)
public class AccountServiceTest {
@Autowired
private IAccountService accountService;
@Test
public void testTransfer(){
accountService.transfer("bbb", "ccc", 100f);
}
}
```