1 什么是Spring?
Spring
是分层的Java SE/EE
应用full-stack
轻量级开源框架,以IOC
(Inverse Of Control
:反转控制)和AOP
(Aspect Oriented Programming
:面向切面编程)为内核,提供了展现层Spring MVC
和持久层Spring JDBC
以及业务层事务管理等众多的企业级应用技术。
Spring
的优势:
- 便于解耦,简化开发:通过
Spring
提供的IOC
容器,可以将对象间的依赖关系交由Spring
进行控制,避免硬编码所造成的过度程序耦合。 AOP
编程:通过Spring
的AOP
功能,方便进行面向切面的编程,许多不容易用传统OOP
实现的功能可以通过AOP
轻松应付。- 声明式事务的支持:将我们从单调烦闷的事务管理代码中解脱出来,通过声明式方式灵活的进行事务的管理,提高开发效率和质量。
2 IOC(控制反转)
在了解IOC
之前,首先需要了解什么是程序之间的耦合以及工程模式解耦。
2.1 程序的耦合
耦合指的就是对象之间的依赖性。对象之间的耦合越高,维护成本就越高。因此,对象的设计应使类和构件之间的耦合最小。下面从三层模式看程序间的耦合,如下:
显然,业务层调用持久层的代码需要在业务层中采用UserDao userDao = new UserDaoImpl();
的形式实现,如果没有持久层的实现类,编译就会失败(直接编译就报错,连运行期都到不了),说明业务层类和持久层类之间的依赖过高。解决该问题的方法其实在JDBC
中已经体现过,即采取工厂模式。
2.2 工厂模式解耦
将需要创建的对象存入bean.properties
中,然后用户读取配置文件、创建和获取该对象的类即为工厂。
bean.properties
配置文件:
UserDao=com.hc.dao.impl.UserDaoImpl
UserService=com.hc.service.impl.UserServiceImpl
- 工厂类:
package com.hc.utils;
import java.io.InputStream;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
public class Factory {
// 定义Properties对象
private static Properties prop;
// 定义一个Map容器,存放需要创建的对象
private static Map<String, Object> beanMaps;
// 使用静态代码块读取Properties文件并创建对象
static {
try {
// 实例化Properties对象
prop = new Properties();
// 获取Properties文件的流对象
InputStream in = Factory.class.getClassLoader().getResourceAsStream("bean.properties");
// 加载该文件
prop.load(in);
beanMaps = new HashMap<>();
// 取出配置文件中所有的Key,即beanPath。然后,依据beanPath创建对象并存入Map
Enumeration<Object> keys = prop.keys();
while (keys.hasMoreElements()) {
String key = keys.nextElement().toString();
String beanPath = prop.getProperty(key);
// 反射创建对象
Object value = Class.forName(beanPath).newInstance();
beanMaps.put(key, value);
}
} catch (Exception e) {
throw new ExceptionInInitializerError("读取properties配置文件失败!!!");
}
}
/**
* 根据bean的名称获取对象
*
* @param beanName
* @return
*/
public static Object getBean(String beanName) {
return beanMaps.get(beanName);
}
}
很明显,调用工厂类中的getBean
方法即可获取事先创建好的类。测试如下:
2.3 IOC(控制反转)的概念
上图test01
方法中采取new
关键字去获取对象,是主动的创建对象,如下:
然而,在工厂模式下获取对象是跟工厂要是被动的,如下:
因此,IOC
的概念就是:把创建对象的权利交给容器(框架)。它包括依赖注入(Dependency Injection
)和依赖查找(Dependency Lookup
),作用就是为了削减计算机程序之间的耦合。
2.4 Spring IOC削减程序的耦合(基于XML)
Spring IOC
实现很简单,只需要在类的根路径下配置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">
<!--
bean标签:用于配置让spring创建对象,并且存入ioc容器之中
id属性:对象的唯一标识
class属性:指定要创建对象的全限定类名
-->
<bean id="userService" class="com.hc.service.impl.UserServiceImpl"></bean>
</beans>
测试如下:
2.5 Spring工程类结构图
从结构图可以看出,创建工厂有两种不同的方式:
BeanFactory
:什么使用什么时候创建对象,有点像延迟加载。其实现类包括XMLBeanFactory
等。ApplicationContext
:只要一读取配置文件,默认情况下就会创建对象。其实现类分为①ClassPathXmlApplicationContext
从类的根路径下加载配置文件。②FileSystemXmlApplicationContext
从磁盘路径上加载配置文件,配置文件可以在磁盘的任意位置。③AnnotationConfigApplicationContext
基于注解配置容器对象。
2.6 bean标签
2.6.1 bean标签属性
作用:
bean标签用于配置对象让spring来创建的。
默认情况下它调用的是类中的无参构造函数。如果没有无参构造函数则不能创建成功。
属性:
id:给对象在容器中提供一个唯一标识。用于获取对象。
class:指定类的全限定类名。用于反射创建对象。默认情况下调用无参构造函数。
scope:指定对象的作用范围。
singleton:默认值,单例的.
prototype:多例的.
request:WEB项目中,Spring创建一个Bean的对象,将对象存入到request域中.
session:WEB项目中,Spring创建一个Bean的对象,将对象存入到session域中.
global session:WEB项目中,应用在Portlet环境.如果没有Portlet环境那么globalSession相当于session.
init-method:指定类中的初始化方法名称。
destroy-method:指定类中销毁方法名称。
2.6.2 实例化bean的三种方式
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<!--
第一种方式:使用默认无参构造函数
根据默认无参构造函数来创建类对象,如果bean中没有默认无参构造函数,将会创建失败。
-->
<!--<bean id="userService" class="com.hc.service.impl.UserServiceImpl"></bean>-->
<!--
第二种方式:spring管理静态工厂-使用静态工厂的方法创建对象
使用StaticFactory类中的静态方法createAccountService创建对象,并存入spring容器
id属性:指定bean的id,用于从容器中获取
class属性:指定静态工厂的全限定类名
factory-method属性:指定生产对象的静态方法
-->
<!--<bean id="userService" class="com.hc.utils.StaticFactory" factory-method="createUserService"></bean>-->
<!--
第三种方式:spring管理实例工厂-使用实例工厂的方法创建对象
先把工厂的创建交给spring来管理,然后在使用工厂的bean来调用里面的方法
factory-bean属性:用于指定实例工厂bean的id。
factory-method属性:用于指定实例工厂中创建对象的方法。
-->
<bean id="instanceFactory" class="com.hc.utils.InstanceFactory"></bean>
<bean id="userService" factory-bean="instanceFactory" factory-method="createUserService"></bean>
</beans>
2.6.3 bean的作用范围和生命周期
单例对象:scope="singleton"
一个应用只有一个对象的实例。它的作用范围就是整个引用。
生命周期:
对象出生:当应用加载,创建容器时,对象就被创建了。
对象活着:只要容器在,对象一直活着。
对象死亡:当应用卸载,销毁容器时,对象就被销毁了。
多例对象:scope="prototype"
每次访问对象时,都会重新创建对象实例。
生命周期:
对象出生:当使用对象时,创建新的对象实例。
对象活着:只要对象在使用中,就一直活着。
对象死亡:当对象长时间不用时,被java的垃圾回收器回收了。
2.7 Spring的依赖注入
2.7.1 构造函数注入
<?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框架来注入)
标签:constructor-arg
属性:
index:指定参数在构造函数参数列表的索引位置
type:指定参数在构造函数中的数据类型
name:指定参数在构造函数中的名称
=======上面三个都是找给谁赋值,下面两个指的是赋什么值的==============
value:它能赋的值是基本数据类型和String类型
ref:它能赋的值是其他bean类型,必须得是在配置文件中配置过的bean
-->
<bean id="userService" class="com.hc.service.impl.UserServiceImpl">
<constructor-arg name="name" value="HC"></constructor-arg>
<constructor-arg name="age" value="25"></constructor-arg>
<constructor-arg name="birthday" ref="date"></constructor-arg>
<!--复杂类型赋值:list-->
<constructor-arg name="list">
<list>
<value>1</value>
<value>2</value>
<value>3</value>
</list>
</constructor-arg>
<!--复杂类型赋值:map-->
<constructor-arg name="map">
<map>
<entry key="A">
<value>
AA
</value>
</entry>
<entry key="B">
<value>
BB
</value>
</entry>
</map>
</constructor-arg>
</bean>
<bean id="date" class="java.util.Date"></bean>
</beans>
2.7.2 set方法注入
<?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">
<!--
set方法注入:使用类中提供需要注入成员的set方法。(通过配置的方式,让spring框架来注入)
标签:property
属性:
name:类中set方法后面的部分
=======上面这个都是找给谁赋值,下面两个指的是赋什么值的==============
value:它能赋的值是基本数据类型和String类型
ref:它能赋的值是其他bean类型,必须得是在配置文件中配置过的bean
-->
<bean id="userService" class="com.hc.service.impl.UserServiceImpl">
<property name="name" value="HC"></property>
<property name="age" value="25"></property>
<property name="birthday" ref="date"></property>
<property name="list">
<list>
<value>4</value>
<value>5</value>
<value>6</value>
</list>
</property>
<property name="map">
<map>
<entry key="AA">
<value>A</value>
</entry>
<entry key="BB">
<value>B</value>
</entry>
<entry key="CC">
<value>C</value>
</entry>
</map>
</property>
</bean>
<bean id="date" class="java.util.Date"></bean>
<!--
第二种方式:spring管理静态工厂-使用静态工厂的方法创建对象
使用StaticFactory类中的静态方法createAccountService创建对象,并存入spring容器
id属性:指定bean的id,用于从容器中获取
class属性:指定静态工厂的全限定类名
factory-method属性:指定生产对象的静态方法
-->
<!--<bean id="userService" class="com.hc.utils.StaticFactory" factory-method="createUserService"></bean>-->
<!--
第三种方式:spring管理实例工厂-使用实例工厂的方法创建对象
先把工厂的创建交给spring来管理,然后在使用工厂的bean来调用里面的方法
factory-bean属性:用于指定实例工厂bean的id。
factory-method属性:用于指定实例工厂中创建对象的方法。
-->
<!--<bean id="instanceFactory" class="com.hc.utils.InstanceFactory"></bean>-->
<!--<bean id="userService" factory-bean="instanceFactory" factory-method="createUserService"></bean>-->
</beans>
2.8 Spring IOC削减程序的耦合(基于注解)
与XML
的配置一致,首先在根路径下配置bean.xml
文件如下:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<!-- 告知spring创建容器时扫描的包 -->
<context:component-scan base-package="com.hc"></context:component-scan>
<!-- 需要注入的属性 -->
<bean id="date" class="java.util.Date"></bean>
</beans>
然后在需要交给Spring
容器的类UserServiceImpl
和UserDaoImpl
配置如下:
package com.hc.service.impl;
import com.hc.dao.UserDao;
import com.hc.domain.User;
import com.hc.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.util.Date;
import java.util.List;
import java.util.Map;
@Service(value = "userService")
public class UserServiceImpl implements UserService {
@Value(value = "HHC")
private String name;
@Value(value = "23")
private Integer age;
@Autowired
private Date birthday;
private List<Integer> list;
private Map<String, String> map;
@Autowired
private UserDao userDao = null;
public UserServiceImpl() {
}
@Override
public List<User> findAll() {
System.out.println("UserServiceImpl running!!!");
System.out.println("UserServiceImpl{" + "name='" + name + ", age=" + age + ", birthday=" + birthday + ", list=" + list + ", map=" + map + '}');
return userDao.findAll();
}
}
测试如下:
从测试的结果发现一个问题:复杂类型的注入为null
。
2.9 Spring IOC常用注解说明
2.9.1 创建bean
@Component
:把资源让Spring
来管理,相当于在xml
中配置一个bean
。@Controller
、@Service
和@Repository
是@Component
的衍生注解,@Controller
:一般用于表现层的注解;@Service
:一般用于业务层的注解;@Repository
:一般用于持久层的注解。
2.9.2 数据注入
@Autowired
:自动按照类型注入。当使用注解注入属性时,set
方法可以省略。它只能注入其他bean
类型。当有多个类型匹配时,使用要注入的对象变量名称作为bean
的id
,在Spring
容器查找,找到了也可以注入成功,找不到就报错。
@Qualifier
:在自动按照类型注入的基础之上,再按照bean
的id
注入。它在给字段注入时不能独立使用,必须和@Autowire
一起使用;但是给方法参数注入时,可以独立使用。@Resource
:直接按照bean
的id
注入。它也只能注入其他bean
类型。@Value
:注入基本数据类型和String
类型数据的。
2.9.3 作用范围
@Scope
:指定bean
的作用范围。取值与XML
的scope
属性一致。
2.9.4 生命周期
@PostConstruct
:用于指定初始化方法。@PreDestroy
:用于指定销毁方法,注意多例对象不受Spring
容器管理。
3 Spring IOC
采用Spring IOC
实现User
表的CRUD
操作,持久层使用DBUtils
和c3p0
数据源。
3.1 Spring IOC案例(XML)
3.1.1 三层架构
- 持久层及其实现类
package com.hc.dao.impl;
import com.hc.dao.UserDao;
import com.hc.domain.User;
import org.apache.commons.dbutils.QueryRunner;
import org.apache.commons.dbutils.handlers.BeanHandler;
import org.apache.commons.dbutils.handlers.BeanListHandler;
import java.util.List;
public class UserDaoImpl implements UserDao {
private QueryRunner runner;
/**
* 便于构造函数注入runner
*
* @param runner
*/
public UserDaoImpl(QueryRunner runner) {
this.runner = runner;
}
@Override
public void saveOne(User user) {
try {
runner.update("insert into user(username,birthday,sex,address) values (?,?,?,?)", user.getUsername(), user.getBirthday(), user.getSex(), user.getAddress());
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@Override
public void deleteOneById(Integer id) {
try {
runner.update("delete from user where id = ?", id);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@Override
public void updateOne(User user) {
try {
runner.update("update user set username = ?,birthday = ?,sex = ?,address = ? where id = ?", user.getUsername(), user.getBirthday(), user.getSex(), user.getAddress(), user.getId());
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@Override
public User findOneById(Integer id) {
try {
return runner.query("select * from user where id = ?", new BeanHandler<User>(User.class), id);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@Override
public List<User> findAll() {
try {
return runner.query("select * from user", new BeanListHandler<User>(User.class));
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
注意,QueryRunner
对象的创建和注入都需要交给Spring
容器。
- 业务层及其实现类
package com.hc.service.impl;
import com.hc.dao.UserDao;
import com.hc.domain.User;
import com.hc.service.UserService;
import java.util.List;
public class UserServiceImpl implements UserService {
private UserDao userDao;
/**
* 便于set方法注入
*
* @param userDao
*/
public void setUserDao(UserDao userDao) {
this.userDao = userDao;
}
@Override
public void saveOne(User user) {
userDao.saveOne(user);
}
@Override
public void deleteOneById(Integer id) {
userDao.deleteOneById(id);
}
@Override
public void updateOne(User user) {
userDao.updateOne(user);
}
@Override
public User findOneById(Integer id) {
return userDao.findOneById(id);
}
@Override
public List<User> findAll() {
return userDao.findAll();
}
}
注意,userDao
对象的创建和注入也需要交给Spring
容器。
3.1.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"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<!--配置userService,并通过set方法注入userDao-->
<bean id="userService" class="com.hc.service.impl.UserServiceImpl">
<property name="userDao" ref="userDao"></property>
</bean>
<!--配置userDao,并通过构造方法注入runner(runner内部封装着简便的mysql操作方法)-->
<bean id="userDao" class="com.hc.dao.impl.UserDaoImpl">
<constructor-arg name="runner" ref="runner"></constructor-arg>
</bean>
<!--
配置runner,并通过构造方法注入c3p0数据源
注意:可能同时有多个对象访问数据源,故需要每一个连接给出一个ds
-->
<bean id="runner" class="org.apache.commons.dbutils.QueryRunner" scope="prototype">
<constructor-arg name="ds" ref="dataSource"></constructor-arg>
</bean>
<!--配置数据源,并通过set方法注入数据库连接的必要信息-->
<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/SSM?useUnicode=true&characterEncoding=utf-8&serverTimezone=GMT"></property>
<property name="user" value="root"></property>
<property name="password" value="1234"></property>
</bean>
</beans>
3.1.3 测试以及存在的问题
package com.hc;
import com.hc.domain.User;
import com.hc.service.UserService;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import java.util.Date;
import java.util.List;
public class testUserCRUD {
@Test
public void testSaveUser() {
ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
UserService userService = (UserService) ac.getBean("userService");
User user = new User();
user.setUsername("HPeng");
user.setBirthday(new Date());
user.setSex("男");
user.setAddress("湖南岳阳");
userService.saveOne(user);
}
@Test
public void testDeleteOneById() {
ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
UserService userService = (UserService) ac.getBean("userService");
userService.deleteOneById(51);
}
@Test
public void testUpdateOne() {
ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
UserService userService = (UserService) ac.getBean("userService");
User user = userService.findOneById(50);
user.setUsername("HPeng");
user.setBirthday(new Date());
user.setSex("男");
user.setAddress("湖南岳阳");
userService.updateOne(user);
}
@Test
public void testFindOneById() {
ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
UserService userService = (UserService) ac.getBean("userService");
User user = userService.findOneById(41);
System.out.println(user);
}
@Test
public void testFindAll() {
ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
UserService userService = (UserService) ac.getBean("userService");
List<User> users = userService.findAll();
for (User u : users) {
System.out.println(u);
}
}
}
很明显,缺点在于:每个测试方法都重新获取了一次Spring
的核心容器。
3.2 Spring IOC案例(注解)
3.2.1 三层架构
使用注解时,仅需要修改业务层和持久层的以下位置:
3.2.3 纯注解配置
bean.xml
中的内容可以以纯注解的形式配置如下:
package com.hc.dao.config;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.PropertySource;
/**
* 主配置类,作用和bean.xml是一样
* spring中的新注解
* Configuration
* 作用:指定当前类是一个配置类
* 细节:当配置类作为AnnotationConfigApplicationContext对象创建的参数时,该注解可以不写。
* ComponentScan
* 作用:用于通过注解指定spring在创建容器时要扫描的包
* 属性:
* value:它和basePackages的作用是一样的,都是用于指定创建容器时要扫描的包。
* 我们使用此注解就等同于在xml中配置了:
* <context:component-scan base-package="com.itheima"></context:component-scan>
* Bean
* 作用:用于把当前方法的返回值作为bean对象存入spring的ioc容器中
* 属性:
* name:用于指定bean的id。当不写时,默认值是当前方法的名称
* 细节:
* 当我们使用注解配置方法时,如果方法有参数,spring框架会去容器中查找有没有可用的bean对象。
* 查找的方式和Autowired注解的作用是一样的
* Import
* 作用:用于导入其他的配置类
* 属性:
* value:用于指定其他配置类的字节码。
* 当我们使用Import的注解之后,有Import注解的类就父配置类,而导入的都是子配置类
* PropertySource
* 作用:用于指定properties文件的位置
* 属性:
* value:指定文件的名称和路径。
* 关键字:classpath,表示类路径下
*/
@Configuration
@ComponentScan(basePackages = "com.hc")
@Import(JdbcConfig.class)
@PropertySource("classpath:jdbcConfig.properties")
public class SpringConfiguration {
}
package com.hc.dao.config;
import com.mchange.v2.c3p0.ComboPooledDataSource;
import org.apache.commons.dbutils.QueryRunner;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Scope;
import javax.sql.DataSource;
/**
* spring连接数据库相关的配置类
* Bean
* 作用:用于把当前方法的返回值作为bean对象存入spring的ioc容器中
* 属性:
* name:用于指定bean的id。当不写时,默认值是当前方法的名称
* 细节:
* 当我们使用注解配置方法时,如果方法有参数,spring框架会去容器中查找有没有可用的bean对象。
* 查找的方式和Autowired注解的作用是一样的
*/
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对象并存入Spring容器
*
* @param dataSource
* @return
*/
@Bean(name = "runner")
@Scope(value = "prototype")
public QueryRunner createQueryRunner(DataSource dataSource) {
return new QueryRunner(dataSource);
}
/**
* 创建数据源并存入Spring容器
*
* @return
*/
@Bean(name = "dataSource")
public DataSource createDataSource() {
try {
ComboPooledDataSource ds = new ComboPooledDataSource();
ds.setDriverClass(driver);
ds.setJdbcUrl(url);
ds.setUser(username);
ds.setPassword(password);
return ds;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
3.2.3 测试
注意,此时要使用AnnotationConfigApplicationContext
。
3.2.4 注解补充
@Configuration
:用于指定当前类是一个Spring
配置类,当创建容器时会从该类上加载注解。获取容器时需要使用AnnotationApplicationContext
(有@Configuration
注解的类.class
)。@ComponentScan
:用于指定Spring
在初始化容器时要扫描的包。作用和在Spring
的xml
配置文件中的:<context:component-scan base-package="com.itheima"/>
是一样的。属性:basePackages
:用于指定要扫描的包。和该注解中的value
属性作用一样。@Bean
:该注解只能写在方法上,表明使用此方法创建一个对象,并且放入Spring
容器。属性:name
:给当前@Bean
注解方法创建的对象指定一个名称(即bean
的id
)。@PropertySource
:用于加载.properties
文件中的配置。属性:value[]
用于指定properties
文件位置。如果是在类路径下,需要写上classpath
。@Import
:用于导入其他配置类,在引入其他配置类时,可以不用再写@Configuration
注解。例如,上述的JdbcConfig
。
3.3 XML开发和纯注解开发的选择
4 AOP
在了解AOP
之前,先来看看之前代码中出现的事务问题。
4.1 事务控制问题
之前关于User
表的操作(增删改查)每次都只执行一条语句,事务被自动控制。关注Account
表中的转账问题如下:
public void transfer(Integer sourceId, Integer targetId, Double money) {
// 根据名称查询两个账户信息
Account source = accountDao.findOneByUid(sourceId);
Account target = accountDao.findOneByUid(targetId);
// 转出账户减钱,转入账户加钱
source.setMoney(source.getMoney() - money);
target.setMoney(target.getMoney() + money);
// 更新两个账户
accountDao.updateOne(source);
int i = 1 / 0; // 模拟转账异常
accountDao.updateOne(target);
}
测试如上方法如下:
@Test
public void testTransfer() {
accountService.transfer(45,50,100.0);
}
即:45号账户向50号账户转账100。然而,由于由于执行的异常,转账失败,存在如下问题:
45号账户钱减少了,50号账户的钱却没有增加,不符合事务的一致性。
4.2 事务控制问题解决方法之一业务层控制事务的提交和回滚
- 定义事务管理的类
TransactionManager
如下:
package com.hc.utils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.sql.DataSource;
import java.sql.Connection;
/**
* 连接的工具类,它用于从数据源中获取一个连接,并且实现和线程的绑定
*/
@Component(value = "connectionUtils")
public class ConnectionUtils {
private ThreadLocal<Connection> tl = new ThreadLocal<Connection>();
@Autowired
private DataSource dataSource;
/**
* 获取当前线程上的连接
*
* @return
*/
public Connection getThreadConnection() {
try {
Connection conn = tl.get();
if (conn == null) {
conn = dataSource.getConnection();
tl.set(conn);
}
return conn;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* 把连接和线程解绑
*/
public void removeConnection() {
tl.remove();
}
}
package com.hc.utils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* 和事务管理相关的工具类,它包含了,开启事务,提交事务,回滚事务和释放连接
*/
@Component(value = "transactionManager")
public class TransactionManager {
@Autowired
private 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();
}
}
}
- 然后在
AccountServiceImpl
中进行事务管理:
package com.hc.service.impl;
import com.hc.dao.AccountDao;
import com.hc.dao.UserDao;
import com.hc.domain.Account;
import com.hc.domain.UserAccount;
import com.hc.service.AccountService;
import com.hc.utils.TransactionManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
@Service(value = "accountService")
public class AccountServiceImpl implements AccountService {
@Autowired
private AccountDao accountDao;
@Autowired
private TransactionManager txManager;
@Override
public void transfer(Integer sourceId, Integer targetId, Double money) {
try {
// 1.开启事务
txManager.beginTransaction();
// 2.执行操作
// 根据名称查询两个账户信息
Account source = accountDao.findOneByUid(sourceId);
Account target = accountDao.findOneByUid(targetId);
// 转出账户减钱,转入账户加钱
source.setMoney(source.getMoney() - money);
target.setMoney(target.getMoney() + money);
// 更新两个账户
accountDao.updateOne(source);
int i = 1 / 0; // 模拟转账异常
accountDao.updateOne(target);
// 3.提交事务
txManager.commit();
} catch (Exception e) {
//4.回滚操作
txManager.rollback();
e.printStackTrace();
} finally {
//5.释放连接
txManager.release();
}
}
}
- 注意,需要在
AccountDaoImpl
中进行相应的修改
package com.hc.dao.impl;
import com.hc.dao.AccountDao;
import com.hc.domain.Account;
import com.hc.domain.User;
import com.hc.utils.ConnectionUtils;
import org.apache.commons.dbutils.QueryRunner;
import org.apache.commons.dbutils.handlers.BeanHandler;
import org.apache.commons.dbutils.handlers.BeanListHandler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository(value = "accountDao")
public class AccountDaoImpl implements AccountDao {
@Autowired
private QueryRunner runner;
@Autowired
private ConnectionUtils connectionUtils;
@Override
public void updateOne(Account account) {
try {
runner.update(connectionUtils.getThreadConnection(), "update account set money = ? where id = ?", account.getMoney(), account.getId());
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@Override
public Account findOneByUid(Integer uid) {
try {
List<Account> accounts = runner.query(connectionUtils.getThreadConnection(), "select * from account where uid = ?", new BeanListHandler<Account>(Account.class), uid);
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);
}
}
- 最后修改
bean.xml
文件如下:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<context:component-scan base-package="com.hc"></context:component-scan>
<!--
配置runner,并通过构造方法注入c3p0数据源
注意:可能同时有多个对象访问数据源,故需要每一个连接给出一个ds
-->
<bean id="runner" class="org.apache.commons.dbutils.QueryRunner" scope="prototype">
</bean>
<!--配置数据源,并通过set方法注入数据库连接的必要信息-->
<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/SSM?useUnicode=true&characterEncoding=utf-8&serverTimezone=GMT"></property>
<property name="user" value="root"></property>
<property name="password" value="1234"></property>
</bean>
</beans>
- 测试转账功能如下:
注:通过对业务层改造,可以实现事务控制。然而,也产生了很多新问题。例如,业务层方法变得臃肿了,里面充斥着很多重复代码。其次就是业务层方法和事务控制方法耦合。
4.3 动态代理解决事务控制问题
4.3.1 动态代理
动态代理参见这里。
4.3.2 动态代理解决事务控制问题
从4.2可以看出,在业务层控制事务会使得业务层的代码极其臃肿。借鉴于动态代理,可以代理实现AccountService
,避免这些问题。
- 首先,创建动态代理
AccountService
对象,实现方法增强(增加事务控制):
package com.hc.utils;
import com.hc.service.AccountService;
import org.springframework.beans.factory.annotation.Autowired;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
public class BeanFactory {
@Autowired
private AccountService accountService;
@Autowired
private TransactionManager transactionManager;
public AccountService getAccountService() {
AccountService proxyAccountService = (AccountService) Proxy.newProxyInstance(accountService.getClass().getClassLoader(), accountService.getClass().getInterfaces(), new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if ("test".equals(method.getName())) {
return method.invoke(accountService, args);
}
Object returnValue = null;
try {
transactionManager.beginTransaction();
returnValue = method.invoke(accountService, args);
transactionManager.commit();
return returnValue;
} catch (Exception e) {
transactionManager.rollback();
throw new RuntimeException(e);
} finally {
transactionManager.release();
}
}
});
return proxyAccountService;
}
}
- 其次,在
beam.xml
中配置生产增强后AccountService
对象proxyAccountService
如下:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<context:component-scan base-package="com.hc"></context:component-scan>
<!--配置proxyAccountService对象-->
<bean id="proxyAccountService" factory-bean="beanFactory" factory-method="getAccountService"></bean>
<bean id="beanFactory" class="com.hc.utils.BeanFactory"></bean>
<!--
配置runner,并通过构造方法注入c3p0数据源
注意:可能同时有多个对象访问数据源,故需要每一个连接给出一个ds
-->
<bean id="runner" class="org.apache.commons.dbutils.QueryRunner" scope="prototype">
</bean>
<!--配置数据源,并通过set方法注入数据库连接的必要信息-->
<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/SSM?useUnicode=true&characterEncoding=utf-8&serverTimezone=GMT"></property>
<property name="user" value="root"></property>
<property name="password" value="1234"></property>
</bean>
</beans>
- 测试如下:
package com.hc;
import com.hc.domain.Account;
import com.hc.service.AccountService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import java.util.List;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:bean.xml")
public class testAccountCRUD {
@Autowired
@Qualifier(value = "proxyAccountService")
private AccountService accountService;
@Test
public void testFindOneById() {
Account account = accountService.findOneById(1);
System.out.println(account);
}
@Test
public void testFindAllByUid() {
List<Account> accounts = accountService.findAllByUid(50);
for (Account account : accounts) {
System.out.println(account);
}
}
@Test
public void testFindAll() {
List<Account> accounts = accountService.findAll();
for (Account account : accounts) {
System.out.println(account);
}
}
@Test
public void testTransfer() {
accountService.transfer(45, 50, 100.0);
}
}
注:上面测试代码中一个关键的语句是@Qualifier(value = "proxyAccountService")
,需要准确的在Spring
容器中寻找到动态代理后的对象。
4.4 AOP的概念
AOP
为Aspect Oriented Programming
的缩写,意为:面向切面编程,通过预编译方式和运行期间动态代理实现程序功能的统一维护的一种技术。AOP
是OOP
的延续,是软件开发中的一个热点,也是Spring
框架中的一个重要内容,是函数式编程的一种衍生范型。利用AOP
可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。
简单理解:将程序中重复的代码抽取出来,在需要执行的时候,使用动态代理的技术,在不修改源码的基础上,对已有方法进行增强。
实现基础:动态代理。
5 Spring AOP
5.1 AOP相关术语
Joinpoint
(连接点):指那些被拦截到的点。在Spring
中,这些点指的是方法,因为Spring
只支持方法类型的连接点。即可能需要增强的方法。Pointcut
(切入点):指我们要对哪些Joinpoint
进行拦截的定义。在Spring
中,指的是要对哪些方法进行拦截。即实际被增强的方法。Advice
(通知/增强):知是指拦截到Joinpoint
之后所要做的事情就是通知。通知的类型:前置通知,后置通知,异常通知,最终通知,环绕通知。即动态代理中的增强代码部分。Target
(目标对象):代理的目标对象。Weaving
(织入):指把增强应用到目标对象来创建新的代理对象的过程。- Proxy(代理):代理类。
- Aspect(切面):是切入点和通知(引介)的结合。
5.2 AOP的实际工作
- 在编写完核心的业务代码后,将公共代码抽取出来,制作成通知。最后在配置文件中,声明切入点与通知点的关系,即切面。
- Spring框架监控切入点方法的执行。一旦监控到切入点方法被运行,使用代理机制,动态创建目标对象的代理对象,根据通知类别,在代理对象的对应位置,将通知对应的功能织入,完成完整的代码逻辑运行。
5.3 Spring AOP解决事务控制(基于XML)
5.3.1 三层架构
AccountDaoImpl
如下:
package com.hc.dao.impl;
import com.hc.dao.AccountDao;
import com.hc.domain.Account;
import com.hc.domain.User;
import com.hc.utils.ConnectionUtils;
import org.apache.commons.dbutils.QueryRunner;
import org.apache.commons.dbutils.handlers.BeanHandler;
import org.apache.commons.dbutils.handlers.BeanListHandler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository(value = "accountDao")
public class AccountDaoImpl implements AccountDao {
@Autowired
private QueryRunner runner;
@Autowired
private ConnectionUtils connectionUtils;
@Override
public void updateOne(Account account) {
try {
runner.update(connectionUtils.getThreadConnection(), "update account set money = ? where id = ?", account.getMoney(), account.getId());
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@Override
public Account findOneById(Integer id) {
try {
return runner.query(connectionUtils.getThreadConnection(), "select * from account where id = ?", new BeanHandler<Account>(Account.class), id);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@Override
public Account findOneByUid(Integer uid) {
try {
List<Account> accounts = runner.query(connectionUtils.getThreadConnection(), "select * from account where uid = ?", new BeanListHandler<Account>(Account.class), uid);
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);
}
}
@Override
public List<Account> findAllByUid(Integer uid) {
try {
return runner.query(connectionUtils.getThreadConnection(), "select * from account where uid = ?", new BeanListHandler<Account>(Account.class), uid);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@Override
public List<Account> findAll() {
try {
return runner.query(connectionUtils.getThreadConnection(), "select * from account", new BeanListHandler<Account>(Account.class));
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
AccountServiceImpl
如下:
package com.hc.service.impl;
import com.hc.dao.AccountDao;
import com.hc.domain.Account;
import com.hc.service.AccountService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
@Service(value = "accountService")
public class AccountServiceImpl implements AccountService {
@Autowired
private AccountDao accountDao;
@Override
public Account findOneById(Integer id) {
return accountDao.findOneById(id);
}
@Override
public List<Account> findAllByUid(Integer uid) {
return accountDao.findAllByUid(uid);
}
@Override
public List<Account> findAll() {
return accountDao.findAll();
}
@Override
public void transfer(Integer sourceId, Integer targetId, Double money) {
// 根据名称查询两个账户信息
Account source = accountDao.findOneByUid(sourceId);
Account target = accountDao.findOneByUid(targetId);
// 转出账户减钱,转入账户加钱
source.setMoney(source.getMoney() - money);
target.setMoney(target.getMoney() + money);
// 更新两个账户
accountDao.updateOne(source);
int i = 1 / 0; // 模拟转账异常
accountDao.updateOne(target);
}
}
- 事务管理类见4.1和4.2
5.3.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">
<context:component-scan base-package="com.hc"></context:component-scan>
<!--
配置runner
-->
<bean id="runner" class="org.apache.commons.dbutils.QueryRunner" scope="prototype">
</bean>
<!--配置数据源,并通过set方法注入数据库连接的必要信息-->
<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/SSM?useUnicode=true&characterEncoding=utf-8&serverTimezone=GMT"></property>
<property name="user" value="root"></property>
<property name="password" value="1234"></property>
</bean>
<!-- 配置AOP -->
<aop:config>
<!-- 配置切面 -->
<!--
配置切入点表达式 id属性用于指定表达式的唯一标识。expression属性用于指定表达式内容
此标签写在aop:aspect标签内部只能当前切面使用。
它还可以写在aop:aspect外面,此时就变成了所有切面可用
-->
<aop:pointcut id="pt" expression="execution(* com.hc.service.impl.*.*(..))"/>
<aop:aspect id="transactionManagerAdvice" ref="transactionManager">
<!-- 配置通知的类型,并且建立通知方法和切入点方法的关联-->
<!--
aop:before:前置通知
method属性:用于指定transactionManager类中哪个方法是前置通知
pointcut属性:用于指定切入点表达式,该表达式的含义指的是对业务层中哪些方法增强
execution():切入点表达式
-->
<aop:before method="beginTransaction" pointcut-ref="pt"></aop:before>
<!--后置通知-->
<aop:after-returning method="commit" pointcut-ref="pt"></aop:after-returning>
<!--异常通知-->
<aop:after-throwing method="rollback" pointcut-ref="pt"></aop:after-throwing>
<!--最终通知-->
<aop:after method="release" pointcut-ref="pt"></aop:after>
</aop:aspect>
</aop:config>
</beans>
5.3.3 测试
package com.hc;
import com.hc.domain.Account;
import com.hc.service.AccountService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import java.util.List;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:bean.xml")
public class testAccountCRUD {
@Autowired
private AccountService accountService;
@Test
public void testFindOneById() {
Account account = accountService.findOneById(1);
System.out.println(account);
}
@Test
public void testFindAllByUid() {
List<Account> accounts = accountService.findAllByUid(50);
for (Account account : accounts) {
System.out.println(account);
}
}
@Test
public void testFindAll() {
List<Account> accounts = accountService.findAll();
for (Account account : accounts) {
System.out.println(account);
}
}
@Test
public void testTransfer() {
accountService.transfer(45, 50, 100.0);
}
}
5.4 XML中配置AOP的细节
5.4.1 依赖管理
配置AOP
需要引入如下依赖:
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.8.13</version>
</dependency>
5.4.2 aop的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">
</beans>
5.4.3 aop标签配置说明
aop:config
:用于声明开始aop
的配置。aop:aspect
:用于配置切面。id
:给切面提供一个唯一标识。ref
:引用配置好的通知类bean
的id
。aop:pointcut
:用于配置切入点表达式。就是指定对哪些类的哪些方法进行增强。expression
:用于定义切入点表达式。aop:before
:用于配置前置通知。指定增强的方法在切入点方法之前执行。method
:用于指定通知类中的增强方法名称。ponitcut-ref
:用于指定切入点的表达式的引用。poinitcut
:用于指定切入点表达式。aop:after-returning
:用于配置后置通知。aop:after-throwing
:用于配置异常通知。aop:after
:用于配置最终通知。aop:around
:用于配置环绕通知。
5.4.4 切入点表达式
execution
:匹配方法的执行。语法:execution
([修饰符] 返回值类型 包名.类名.方法名(参数))
通配细节如下:
关键字:execution(表达式)
表达式:
访问修饰符 返回值 包名.包名.包名...类名.方法名(参数列表)
标准的表达式写法:
public void com.itheima.service.impl.AccountServiceImpl.saveAccount()
访问修饰符可以省略
void com.itheima.service.impl.AccountServiceImpl.saveAccount()
返回值可以使用通配符,表示任意返回值
* com.itheima.service.impl.AccountServiceImpl.saveAccount()
包名可以使用通配符,表示任意包。但是有几级包,就需要写几个*.
* *.*.*.*.AccountServiceImpl.saveAccount())
包名可以使用..表示当前包及其子包
* *..AccountServiceImpl.saveAccount()
类名和方法名都可以使用*来实现通配
* *..*.*()
参数列表:
可以直接写数据类型:
基本类型直接写名称 int
引用类型写包名.类名的方式 java.lang.String
可以使用通配符表示任意类型,但是必须有参数
可以使用..表示有无参数均可,有参数可以是任意类型
全通配写法:
* *..*.*(..)
5.5 Spring AOP解决事务控制(基于注解)
5.5.1 事务管理类的修改
package com.hc.utils;
import org.aspectj.lang.annotation.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* 和事务管理相关的工具类,它包含了,开启事务,提交事务,回滚事务和释放连接
*/
@Component(value = "transactionManager")
@Aspect // 切面
public class TransactionManager {
@Autowired
private ConnectionUtils connectionUtils;
@Pointcut("execution(* com.hc.service.impl.*.*(..))")
public void pt() {
}
/**
* 开启事务
*/
@Before("pt()")
public void beginTransaction() {
try {
connectionUtils.getThreadConnection().setAutoCommit(false);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 提交事务
*/
@AfterReturning("pt()")
public void commit() {
try {
connectionUtils.getThreadConnection().commit();
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 回滚事务
*/
@AfterThrowing("pt()")
public void rollback() {
try {
connectionUtils.getThreadConnection().rollback();
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 释放连接
*/
@After("pt()")
public void release() {
try {
connectionUtils.getThreadConnection().close();//还回连接池中
connectionUtils.removeConnection();
} catch (Exception e) {
e.printStackTrace();
}
}
}
5.5.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">
<context:component-scan base-package="com.hc"></context:component-scan>
<!--
配置runner
-->
<bean id="runner" class="org.apache.commons.dbutils.QueryRunner" scope="prototype">
</bean>
<!--配置数据源,并通过set方法注入数据库连接的必要信息-->
<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/SSM?useUnicode=true&characterEncoding=utf-8&serverTimezone=GMT"></property>
<property name="user" value="root"></property>
<property name="password" value="1234"></property>
</bean>
<!--开启注解支持-->
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
</beans>
5.5.3 测试结果
出现该问题的原因在于,使用注解时不能保证提交事务方法在开启事务方法之后执行,建议使用环绕注解。
6 Spring的事务控制
之前基于Spring AOP
的事务控制来自于自己配置的事务管理类TransactionManager
,而Spring
同样给我们提供了一组事务控制的接口,更方便简洁。
6.1 Spring事务控制API
6.1.1 Spring事务控制API的UML
主要接口和类说明:
PlatformTransactionManager接口:Spring的事务控制接口,定义了事务管理的规则。
getTransaction(TransactionDefinition): 根据事务的定义信息类,获取事务。
commit(): 提交事务。该方法完成开启事务,并提交事务。(setAutoCommit(false)+commit)
rollback(): 事务回滚。该方法完成事务的回滚并释放资源。(rollback+release)
DataSourceTransationManager类: 使用JDBC或iBatis操作数据库时的事务控制
HibernateTransactionManager类: 使用Hibernate操作数据库时的事务控制
TransactionDefinition接口:定义事务的属性
getIsolationLevel(): 获取事务的隔离级别
getPropagationBehavior():获取事务传播行为
getTimeout(): 获取事务的超时事件
isReadOnly(): 事务是否只读
6.1.2 事务的隔离级别
ISOLATION_DEFAULT: 默认级别,使用数据库中的默认级别
ISOLATION_READ_UNCOMMITTED = 1:可以读取未提交数据
ISOLATION_READ_COMMITTED = 2:只能读取已提交数据,解决脏读问题(Oracle默认级别)
ISOLATION_REPEATABLE_READ = 4:是否读取其他事务提交修改后的数据,解决不可重复读问题(MySQL默认级别)
ISOLATION_SERIALIZABLE = 8:是否读取其他事务提交添加后的事务,解决幻影读问题。
6.1.3 事务的传播行为
当一个方法被另一个方法调用时,这个方法是否被事务控制。
分为7种:
REQUIRED:如果当前没有事务,就新建一个事务,如果已经存在一个事务中,加入到这个事务中。一般的选择(默认值),增删改时选择
SUPPORTS:支持当前事务,如果当前没有事务,就以非事务方式执行(没有事务),查询时选择
MANDATORY:表示当前方法必须由事务控制。如果当前没有事务,就抛出异常
REQUERS_NEW: 表示当前方法必须由它自己的事务控制。如果存在事务,把当前事务挂起。
NOT_SUPPORTED:以非事务方式执行操作。如果存在事务,就把当前事务挂起
NEVER:以非事务方式运行,如果当前存在事务,抛出异常
NESTED:如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行 REQUIRED 类似的操作。
6.2 Spring的事务控制(基于XML)
6.2.1 三层架构
AccountDaoImpl
package com.hc.dao.impl;
import com.hc.dao.AccountDao;
import com.hc.domain.Account;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository(value = "accountDao")
public class AccountDaoImpl implements AccountDao {
@Autowired
private JdbcTemplate jdbcTemplate;
@Override
public Account findOneByUid(Integer uid) {
List<Account> accounts = jdbcTemplate.query("select * from account where uid = ?", new BeanPropertyRowMapper<Account>(Account.class), uid);
if (accounts == null || accounts.size() == 0)
return null;
if (accounts.size() > 1)
throw new RuntimeException("结果集不唯一!");
return accounts.get(0);
}
@Override
public void update(Account account) {
jdbcTemplate.update("update account set money = ? where id = ?", account.getMoney(), account.getId());
}
}
AccountServiceImpl
package com.hc.service.impl;
import com.hc.dao.AccountDao;
import com.hc.domain.Account;
import com.hc.service.AccountService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service(value = "accountService")
public class AccountServiceImpl implements AccountService {
@Autowired
private AccountDao accountDao;
@Override
public Account findOneByUid(Integer uid) {
return accountDao.findOneByUid(uid);
}
@Override
public void transfer(Integer sourceId, Integer targetId, Double money) {
// 根据名称查询两个账户信息
Account source = accountDao.findOneByUid(sourceId);
Account target = accountDao.findOneByUid(targetId);
// 转出账户减钱,转入账户加钱
source.setMoney(source.getMoney() - money);
target.setMoney(target.getMoney() + money);
// 更新两个账户
accountDao.update(source);
int i = 1 / 0; // 模拟转账异常
accountDao.update(target);
}
}
6.2.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:tx="http://www.springframework.org/schema/tx"
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/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
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<!--开启注解支持-->
<context:component-scan base-package="com.hc"></context:component-scan>
<!--引入外部properties文件中的数据库连接信息-->
<context:property-placeholder location="jdbcConfig.properties"></context:property-placeholder>
<!--创建jdbcTemplate提交给Spring管理,并注入数据源-->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource"></property>
</bean>
<!--创建数据源,采取Spring-jdbc自身提供的-->
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName" value="${jdbc.driver}"></property>
<property name="url" value="${jdbc.url}"></property>
<property name="username" value="${jdbc.username}"></property>
<property name="password" value="${jdbc.password}"></property>
</bean>
<!-- 配置事务管理器 -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"></property>
</bean>
<!-- 配置事务的通知-->
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<!--
配置事务的属性
isolation:用于指定事务的隔离级别。默认值是DEFAULT,表示使用数据库的默认隔离级别。
propagation:用于指定事务的传播行为。默认值是REQUIRED,表示一定会有事务,增删改的选择。查询方法可以选择SUPPORTS。
read-only:用于指定事务是否只读。只有查询方法才能设置为true。默认值是false,表示读写。
timeout:用于指定事务的超时时间,默认值是-1,表示永不超时。如果指定了数值,以秒为单位。
rollback-for:用于指定一个异常,当产生该异常时,事务回滚,产生其他异常时,事务不回滚。没有默认值。表示任何异常都回滚。
no-rollback-for:用于指定一个异常,当产生该异常时,事务不回滚,产生其他异常时事务回滚。没有默认值。表示任何异常都回滚。
-->
<tx:attributes>
<tx:method name="find*" propagation="SUPPORTS" read-only="true"/>
<tx:method name="*" propagation="REQUIRED" read-only="false"/>
</tx:attributes>
</tx:advice>
<!-- 配置aop-->
<aop:config>
<!-- 配置切入点表达式-->
<aop:pointcut id="pt" expression="execution(* com.hc.service.impl.*.*(..))"/>
<!--建立切入点表达式和事务通知的对应关系 -->
<aop:advisor advice-ref="txAdvice" pointcut-ref="pt"></aop:advisor>
</aop:config>
</beans>
6.2.3 测试(整合JUnit)
package com.hc;
import com.hc.service.AccountService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:bean.xml")
public class TestSpringTransactionManager {
@Autowired
private AccountService accountService;
@Test
public void testTransfer() {
accountService.transfer(45, 50, 500.0);
}
}
6.3 Spring的事务控制(基于注解)
6.3.1 业务层代码修改
仅需要添加@Transactional
注解即可。
6.3.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:tx="http://www.springframework.org/schema/tx"
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/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
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<!--开启注解支持-->
<context:component-scan base-package="com.hc"></context:component-scan>
<!--引入外部properties文件中的数据库连接信息-->
<context:property-placeholder location="jdbcConfig.properties"></context:property-placeholder>
<!--创建jdbcTemplate提交给Spring管理,并注入数据源-->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource"></property>
</bean>
<!--创建数据源,采取Spring-jdbc自身提供的-->
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName" value="${jdbc.driver}"></property>
<property name="url" value="${jdbc.url}"></property>
<property name="username" value="${jdbc.username}"></property>
<property name="password" value="${jdbc.password}"></property>
</bean>
<!-- 配置事务管理器 -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"></property>
</bean>
<!-- 开启spring对注解事务的支持 -->
<tx:annotation-driven transaction-manager="transactionManager"></tx:annotation-driven>
</beans>
7 Spring整合
7.1 Spring整合JUnit
问题:在
3.1.3
中提到了一个测试中存在的问题,那就是每个测试方法都重新获取了一次Spring
的核心容器。
针对该问题,我们需要的是程序自动的创建容器。然而,如果我们仅在userService
上加入@Autowired
注解是无法创建成功的,因为JUnit
根本不知道我们是否使用了Spring
框架。因此,需要将Spring
和JUnit
做进一步的整合,使得在进行JUnit
测试时,自动创建Spring
容器。
7.1.1 导入依赖
- 导入
Spring
整合JUnit
的坐标
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>5.0.2.RELEASE</version>
</dependency>
7.1.2 整合JUnit
- 使用
JUnit
提供的一个注解@RunWith(SpringJUnit4ClassRunner.class)
把原有的main
方法替换了,替换成Spring
提供的。 - 注解
@ContextConfiguration
告知Spring
的运行器Spring
和IOC
创建是基于XML
还是注解的,并且说明位置。若是基于XML
创建IOC
,则使用参数locations
:指定XML
文件的位置,加上classpath
关键字,表示在类路径下。反之,参数classes
:指定注解类所在地位置。
package com.hc;
import com.hc.config.SpringConfiguration;
import com.hc.domain.User;
import com.hc.service.UserService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import java.util.List;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = SpringConfiguration.class)
public class testUserCRUD {
@Autowired
private UserService userService;
@Test
public void testFindAll() {
List<User> users = userService.findAll();
for (User u : users) {
System.out.println(u);
}
}
}