Spring基础学习笔记 学习来源:
学习时间:2021年2月4日,2023年6月12日
1 Spring简介
2002年,首次推出了Spring框架的雏形,interface21框架
2004年3月24日,正式发布了1.0版本
创始人Rod Johnson
Spring maven依赖
1 2 3 4 5 6 7 8 9 10 <dependency > <groupId > org.springframework</groupId > <artifactId > spring-webmvc</artifactId > <version > 5.2.0.RELEASE</version > </dependency > <dependency > <groupId > org.springframework</groupId > <artifactId > spring-jdbc</artifactId > <version > 5.2.0.RELEASE</version > </dependency >
优点:
Spring是一个开源的免费的容器
Spring是一个轻量级的、非入侵的框架
控制反转IOC,面向切面编程AOP
支持事务的处理,对框架整合的支持
Spring 的核心是一个 容器 ,通常称为 *Spring 应用程序上下文(Application Context)*,用于创建和管理应用程序组件。这些组件(或 bean)在 Spring 应用程序上下文中连接在一起以构成一个完整的应用程序。
将 bean 连接在一起的行为是基于一种称为 依赖注入 (DI)的模式。依赖项注入的应用程序不是由组件自身创建和维护它们依赖的其他 bean 的生命周期,而是依赖于单独的实体(容器)来创建和维护所有组件,并将这些组件注入需要它们的 bean。通常通过构造函数参数或属性访问器方法完成此操作。
例如,假设在应用程序的许多组件中,要处理两个组件:inventory service(用于获取库存级别)和 product service(用于提供基本产品信息)。product service 取决于 inventory service,以便能够提供有关产品的完整信息。下图说明了这些 bean 与 Spring 应用程序上下文之间的关系。
2 Spring的组成及拓展
3 IOC控制反转 3.1 程序的耦合 3.1.1 概念 耦合性(Coupling),也叫耦合度,是对模块间关联程度的度量 。耦合的强弱取决于模块间接口的复杂性、调用模块的方式以及通过界面传送数据的多少。模块间的耦合度是指模块之间的依赖关系,包括控制关系、调用关系、数据传递关系 。模块间联系越多,其耦合性越强 ,同时表明其独立性越差( 降低耦合性,可以提高其独立性)。耦合性存在于各个领域,而非软件设计中独有的,但是我们只讨论软件工程中的耦合在软件工程中,耦合指的就是就是对象之间的依赖性 。对象之间的耦合越高,维护成本越高。因此对象的设计应使类和构件之间的耦合最小。软件设计中通常用耦合度和内聚度作为衡量模块独立程度的标准。划分模块的一个准则就是高内聚低耦合。
1) 耦合的分类 (1)内容耦合 。当一个模块直接修改或操作另一个模块的数据时,或一个模块不通过正常入口而转入另一个模块时,这样的耦合被称为内容耦合。内容耦合是最高程度的耦合 ,应该避免使用之。
(2)公共耦合 。两个或两个以上的模块共同引用一个全局数据项,这种耦合被称为公共耦合。在具有大量公共耦合的结构中,确定究竟是哪个模块给全局变量赋了一个特定的值是十分困难的。
(3)外部耦合 。一组模块都访问同一全局简单变量而不是同一全局数据结构,而且不是通过参数表传递该全局变量的信息,则称之为外部耦合。
(4)控制耦合 。一个模块通过接口向另一个模块传递一个控制信号,接受信号的模块根据信号值进行适当的动作,这种耦合被称为控制耦合。
(5)标记耦合 。若一个模块 A 通过接口向两个模块 B 和 C 传递一个公共参数,那么称模块 B 和 C 之间存在一个标记耦合。
(6)数据耦合 。模块之间通过参数来传递数据,那么被称为数据耦合。数据耦合是最低的一种耦合形式,系统中一般都存在这种类型的耦合 ,因为为了完成一些有意义的功能,往往需要将某些模块的输出数据作为另一些模块的输入数据。
(7)非直接耦合 。两个模块之间没有直接关系,它们之间的联系完全是通过主模块的控制和调用来实现的。
总结
耦合是影响软件复杂程度和设计质量的一个重要因素,在设计上我们应采用以下原则 :
如果模块间必须存在耦合,就尽量使用数据耦合,少用控制耦合,限制公共耦合的范围,尽量避免使用内容耦合 。
2) 内聚与耦合 内聚标志一个模块内各个元素彼此结合的紧密程度,它是信息隐蔽和局部化概念的自然扩展。内聚是从功能角度来度量模块内的联系,一个好的内聚模块应当恰好做一件事 。它描述的是模块内的功能联系。耦合是软件结构中各模块之间相互连接的一种度量,耦合强弱取决于模块间接口的复杂程度、进入或访问一个模块的点以及通过接口的数据。 程序讲究的是低耦合,高内聚。就是同一个模块内的各个元素之间要高度紧密,但是各个模块之间的相互依存度却要不那么紧密。
内聚和耦合是密切相关的,同其他模块存在高耦合的模块意味着低内聚,而高内聚的模块意味着该模块同其他模块之间是低耦合。在进行软件设计时,应力争做到高内聚,低耦合 。
3) 解耦思路 耦合:程序之间的依赖关系。包括类之间的依赖和方法之间的依赖。
解耦:降低程序之间的依赖关系。
实际开发中应该做到:编译期不依赖,运行时才依赖 。
解耦思路:
第一步:使用反射 来创建对象,而避免使用new关键字
第二步:通过读取配置文件 ,来获取要创建的对象全限定类名
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 public class JdbcDemo { public static void main (String[] args) throws SQLException { Class.forName("com.mysql.jdbc.Driver" ); Connection con = DriverManager.getConnection("jdbc:mysql://localhost:3306/test" ,"root" ,"123456" ); PreparedStatement preparedStatement= con.prepareStatement("select * from account" ); ResultSet resultSet=preparedStatement.executeQuery(); while (resultSet.next()){ System.out.println(resultSet.getString("name" )); } resultSet.close(); preparedStatement.close(); con.close(); } }
3.1.2 程序实例
账户持久层接口
1 2 3 4 5 6 public interface IAccountDao { void saveAccount () ; }
账户持久层实现类
1 2 3 4 5 public class AccountDaoImpl implements IAccountDao { public void saveAccount () { System.out.println("保存了账户" ); } }
业务层接口
1 2 3 4 5 6 public interface IAccountService { void saveAccount () ; }
业务层实现类
1 2 3 4 5 6 7 8 9 public class AccountServiceImpl implements IAccountService { private IAccountDao accountDao = new AccountDaoImpl (); public void saveAccount () { accountDao.saveAccount(); } }
表现层
1 2 3 4 5 6 7 public class Client { public static void main (String[] args) { IAccountService iAccountService = new AccountServiceImpl (); iAccountService.saveAccount(); } }
3.1.3 工厂模式解耦 1) bean Bean有可重用组件 的含义。Java bean >> 实体类。Java bean是用Java语言编写的可重用组件,它就是创建service和dao对象的。它:
需要一个配置文件 来配置service和dao,该配置文件的内容至少应包含:唯一标志-全限定类名
(key-value形式)
通过读取配置文件中配置的内容,反射创建对象
配置文件可以是xml
或properties
2) 工厂模式实例
配置文件bean.properties
1 2 accountService = com.hongyi.service.impl.AccountServiceImpl accountDao = com.hongyi.dao.impl.AccountDaoImpl
BeanFactory.class
类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 public class BeanFactory { private static Properties props; static { try { props = new Properties (); InputStream in = BeanFactory.class.getClassLoader().getResourceAsStream("bean.properties" ); props.load(in); } catch (IOException e) { throw new ExceptionInInitializerError ("初始化Properties失败" ); } } public static Object getBean (String beanName) { Object bean = null ; try { String beanPath = props.getProperty(beanName); bean = Class.forName(beanPath).newInstance(); } catch (Exception e){ e.printStackTrace(); } return bean; } }
表现层代码改造
1 2 3 4 5 6 public class Client { public static void main (String[] args) { IAccountService iAccountService = (IAccountService) BeanFactory.getBean("accountService" ); iAccountService.saveAccount(); } }
业务层实现类代码改造
1 2 3 4 5 6 7 public class AccountServiceImpl implements IAccountService { private IAccountDao accountDao = (IAccountDao) BeanFactory.getBean("accountDao" ); public void saveAccount () { accountDao.saveAccount(); } }
3) 工厂模式实例问题分析及改造
单例和多例对象的区别
单例:对象只被创建一次,从而类中的成员只会初始化一次。会有线程安全问题。
多例:对象被创建多次,执行效率没有单例高,但没有线程安全问题。
前述的工厂模式是多例模式 ,但可采用单例模式进行创建对象(原因?)。
改造为单例模式的BeanFactory.class
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 public class BeanFactory { private static Properties props; private static Map<String, Object> beans; static { try { props = new Properties (); InputStream in = BeanFactory.class.getClassLoader().getResourceAsStream("bean.properties" ); props.load(in); beans = new HashMap <String, Object>(); Enumeration keys = props.keys(); while (keys.hasMoreElements()) { String key = keys.nextElement().toString(); String beanPath = props.getProperty(key); Object value = Class.forName(beanPath).newInstance(); beans.put(key,value); } } catch (Exception e) { throw new ExceptionInInitializerError ("初始化Properties失败" ); } } public static Object getBean (String beanName) { return beans.get(beanName); } }
3.2 IOC简介 3.2.1 概念 控制反转 (Inversion of Control,缩写为IoC ),是面向对象编程中的一种设计原则,可以用来减低计算机代码之间的耦合度。其中最常见的方式叫做依赖注入 (Dependency Injection,简称DI ),还有一种方式叫“依赖查找 ”(Dependency Lookup)。通过控制反转,对象在被创建的时候,由一个调控系统内所有对象的外界实体将其所依赖的对象的引用传递给它 。也可以说,依赖被注入到对象中。
3.2.2 作用 削减计算机程序的耦合,降低代码中的依赖关系。
3.3 Spring中的IOC 采用配置的方式 替代之前的编码工作。
3.3.1 IOC基本依赖 1 2 3 4 5 <dependency > <groupId > org.springframework</groupId > <artifactId > spring-context</artifactId > <version > 5.0.2.RELEASE</version > </dependency >
依赖关系图
3.3.2 核心容器的创建
1 2 3 4 5 6 7 8 9 10 11 <?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 id ="accountService" class ="com.hongyi.service.impl.AccountServiceImpl" > </bean > <bean id ="accountDao" class ="com.hongyi.dao.impl.AccountDaoImpl" > </bean > </beans >
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public class Client { public static void main (String[] args) { ApplicationContext ac = new FileSystemXmlApplicationContext ("E:\\develop\\ioc_demo1\\src\\main\\resources\\bean.xml" ); IAccountService accountService = (IAccountService) ac.getBean("accountService" ); IAccountDao accountDao = ac.getBean("accountDao" ,IAccountDao.class); System.out.println(accountService); } }
ApplicationContext
三个常用实现类
ClassPathXmlApplicationContext
:它可以加载类路径下的配置文件,要求配置文件必须在类路径下,否则加载不了。更常用 。
FileSystemXMLApplicationContext
:它可以加载磁盘任意路径下的配置文件(前提是必须有访问权限)。采用绝对路径。不常用 。
AnnotationConfigApplicationContext
:它是用于读取注解创建容器的。最常用 。
ApplicationContext继承关系
核心容器的两个接口引发出的问题
实际开发中,采用ApplicationContext更多 ,Spring会自动的选择是否是即时加载还是延时加载。
3.3.3 Bean对象管理的细节 1) 创建bean的三种方式
使用默认构造函数创建:在spring的配置文件中,使用bean标签,配以id和class属性之后,且没有其他属性和标签时,采用的就是默认构造函数 创建bean对象。此时如果类中没有默认构造函数,则对象无法创建。
1 <bean id ="accountService" class ="com.hongyi.service.impl.AccountServiceImpl" > </bean >
使用普通工厂中的方法创建对象:使用某个类中的方法创建对象,并存入spring容器。
1 2 <bean id ="instanceFactory" class ="com.hongyi.factory.InstanceFactory" > </bean > <bean id ="accountService" factory-bean ="instanceFactory" factory-mothod ="getAccountService" > </bean >
InstanceFactory.java
代码
1 2 3 4 5 public class instanceFactory { public IAccountService getAccountService () { return new AccountServiceImpl (); } }
使用静态工厂中的静态方法创建对象:使用某个类中的静态方法创建对象,并存入spring容器中。
1 <bean id ="accountService" class ="com.hongyi.factory.StaticFactory" factory-mothod ="getAccountService" > </bean >
StaticFactory.java
代码
1 2 3 4 5 public class StaticFactory { public static IAccountService getAccountService () { return new AccountServiceImpl (); } }
2) bean的作用范围 bean标签的scope
属性:用于指定bean的作用范围。取值有:常用的就是前两个。spring 5.x已不支持后三种了。
1 <bean id ="accountService" class ="com.hongyi.service.impl.AccountServiceImpl" scope ="prototype" > </bean >
singleton
:默认,单例
prototype
:多例
request
:作用于web应用的请求范围
session
:作用于web应用的会话范围
global-session
:作用于集群环境的会话范围(全局会话范围),当不是集群环境时,作用同session一样。
测试代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public class Client { public static void main (String[] args) { ApplicationContext ac = new ClassPathXmlApplicationContext ("bean.xml" ); IAccountService as1 = (IAccountService) ac.getBean("accountService" ); IAccountService as2 = (IAccountService) ac.getBean("accountService" ); System.out.println(as1 == as2); } }
3) bean的生命周期
单例对象
出生:当容器创建时出生
活着:只要容器还在,就活着
死亡:容器一销毁,对象就死亡
总结:单例对象的生命周期同容器的一致
编码测试
1 <bean id ="accountService" class ="com.hongyi.service.impl.AccountServiceImpl" init-method ="init" destroy-method ="destroy" > </bean >
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public class AccountServiceImpl implements IAccountService { public void saveAccount () { System.out.println("service中的saveAccount方法执行了" ); } public void init () { System.out.println("对象初始化了" ); } public void destroy () { System.out.println("对象销毁了" ); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 public class Client { public static void main (String[] args) { ClassPathXmlApplicationContext ac = new ClassPathXmlApplicationContext ("bean.xml" ); IAccountService as = (IAccountService) ac.getBean("accountService" ); as.saveAccount(); ac.close(); } }
执行结果:
多例对象
出生:当我们要使用对象时,spring为我们创建
活着:在使用过程中,就一直活着
死亡:当对象长时间不被使用,且没有别的对象引用时,则由Java的垃圾回收机制回收
3.3.4 依赖注入 DI 1) 概念 依赖注入:Dependency Injection
。依赖关系的管理都交由spring来管理。在当前类需要用到其他类的对象,由spring为我们提供,我们只需要在配置文件中说明 。依赖关系的维护就叫做依赖注入。
依赖注入的数据
基本类型和String
其他bean类型(在配置文件中或注解配置过的bean)
复杂类型/集合类型
注入方式
使用构造函数 提供
使用set方法 提供
使用注解 提供
2) 构造函数注入
注入示例代码
1 2 3 4 5 6 7 <bean id ="accountService" class ="com.hongyi.service.impl.AccountServiceImpl" > <constructor-arg name ="name" value ="hongyi" > </constructor-arg > <constructor-arg name ="age" value ="18" > </constructor-arg > <constructor-arg name ="birthday" ref ="now" > </constructor-arg > </bean > <bean id ="now" class ="java.util.Date" > </bean >
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public class AccountServiceImpl implements IAccountService { private String name; private Integer age; private Date birthday; public AccountServiceImpl (String name, Integer age, Date birthday) { this .name = name; this .age = age; this .birthday = birthday; } public void saveAccount () { System.out.println("service中的saveAccount方法执行了" +name+"," +age+"," +birthday); } }
方法:使用的标签是constructor-arg
;位于bean标签的内部;
标签中的属性:
type:用于指定要注入的数据的数据类型,该数据类型也是构造函数中某个或某些参数的类型
index:用于指定要注入的数据给构造函数中指定索引位置的参数赋值。索引的位置从零开始
name:用于指定给构造函数中指定名称的参数赋值。以上三个用于指定给构造函数中哪个参数赋值。常用的是name。
value:用于提供基本类型和String类型的数据
ref:用于指定其他bean类数据。他指的就是在spring的ioc容器中出现的bean对象。
优势
在获取bean对象时,注入数据是必须的操作,否则对象无法创建成功。
弊端
改变了bean对象的实例化方式,使我们在创建对象时,如果用不到这些数据,也必须提供。
这种注入方式不常用 。
3) set方法注入
编码测试
1 2 3 4 5 6 7 8 <bean id ="now" class ="java.util.Date" > </bean > <bean id ="accountService2" class ="com.hongyi.service.impl.AccountServiceImpl2" > <property name ="age" value ="21" > </property > <property name ="name" value ="hongyi" > </property > <property name ="birthday" ref ="now" > </property > </bean >
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public class AccountServiceImpl2 implements IAccountService { private String name; private Integer age; private Date birthday; public void setName (String name) { this .name = name; } public void setAge (Integer age) { this .age = age; } public void setBirthday (Date birthday) { this .birthday = birthday; } public void saveAccount () { System.out.println("service中的saveAccount方法执行了" +name+"," +age+"," +birthday); } }
涉及的标签:property
;出现的位置:bean标签的内部;属性有:
name:用于指定注入时所调用的set方法名称
value:用于提供基本类型和String类型的数据
ref:用于指定其他bean类数据。他指的就是在spring的ioc容器中出现的bean对象。
优势
创建对象时,没有明确的限制,可以直接使用默认构造函数。
弊端
如果有某个成员必须有值,则获取对象是有可能set方法没有执行。
这种方式更常用
采用set方法注入复杂类型
说明:用于给List结构集合注入的标签有list,array,set。用于给Map结构集合注入的标签有map,props。即结构相同,标签可以互换。
bean.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 <bean id ="accountService" class ="com.hongyi.service.impl.AccountServiceImpl" > <property name ="myStrs" > <array > <value > AAA</value > <value > BBB</value > <value > CCC</value > </array > </property > <property name ="myList" > <list > <value > AAA</value > <value > BBB</value > <value > CCC</value > </list > </property > <property name ="mySet" > <set > <value > AAA</value > <value > BBB</value > <value > CCC</value > </set > </property > <property name ="myMap" > <map > <entry key ="k1" value ="v1" > </entry > <entry key ="k2" value ="v2" > </entry > <entry key ="k3" > <value > v3</value > </entry > </map > </property > <property name ="myProps" > <props > <prop key ="k1" > prop-v1</prop > <prop key ="k2" > prop-v2</prop > </props > </property > </bean >
AccountServiceImpl.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 public class AccountServiceImpl implements IAccountService { private String[] myStrs; private List<String> myList; private Set<String> mySet; private Map<String,String> myMap; private Properties myProps; public void setMyStrs (String[] myStrs) { this .myStrs = myStrs; } public void setMyList (List<String> myList) { this .myList = myList; } public void setMySet (Set<String> mySet) { this .mySet = mySet; } public void setMyMap (Map<String, String> myMap) { this .myMap = myMap; } public void setMyProps (Properties myProps) { this .myProps = myProps; } public void saveAccount () { System.out.println(Arrays.toString(myStrs)); System.out.println(myList); System.out.println(mySet); System.out.println(myMap); System.out.println(myProps); } }
Client.java
1 2 3 4 5 6 7 8 public class Client { public static void main (String[] args) { ApplicationContext ac = new ClassPathXmlApplicationContext ("bean.xml" ); IAccountService as = (IAccountService) ac.getBean("accountService" ); as.saveAccount(); } }
执行结果
4) 基于注解的IOC配置
bean标签的主要形式
1 2 3 <bean id ="" class ="" scope ="" init-method ="" destroy-method ="" > <property name ="" value ="" |ref ="" > </property > </bean >
用于创建对象的注解 作用就和在xml配置文件中编写一个bean标签实现的作用是一样的。
@Component
作用:用于把当前类对象存入spring容器中 。
属性:value:用于指定bean的id,当不写时,默认值是当前类名首字母改小写。当只有一个value时,可以省略
bean.xml
代码:用于扫描注解存在的包
1 2 3 4 5 6 7 8 9 10 11 12 <?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.hongyi" > </context:component-scan > </beans >
AccountServiceImpl.java
代码
1 2 3 4 5 6 7 8 9 @Component(value = "accountService") public class AccountServiceImpl implements IAccountService { private IAccountDao accountDao; public void saveAccount () { accountDao.saveAccount(); } }
Client.java
代码
1 2 3 4 5 6 7 public class Client { public static void main (String[] args) { ApplicationContext ac = new ClassPathXmlApplicationContext ("bean.xml" ); IAccountService as = (IAccountService) ac.getBean("accountService" ); System.out.println(as); } }
@Controller,@Service,@Repository
以上三个注解,作用和属性与Component是相同的 。它们三个是spring框架为我们提供明确的三层使用的注解,使我们的三层对象更加清晰。各用于表现层,业务层,持久层。
用于注入数据的注解 作用就和在bean标签中编写一个property标签实现的作用是一样的。
@Autowired
示例代码:
1 2 3 4 5 6 7 8 9 @Service(value = "accountService") public class AccountServiceImpl implements IAccountService { @Autowired private IAccountDao accountDao; public void saveAccount () { accountDao.saveAccount(); } }
IOC容器和注入图示:
如果IOC容器中没有任何bean的类型和要注入的变量类型匹配,则报错。
1 2 3 4 5 6 7 @Repository("accountDao") public class AccountDaoImpl { public void saveAccount () { System.out.println("保存账户" ); } }
如果IOC容器中有多个类型匹配时:
首先按照类型(红色部分)进行匹配,再按照id(蓝色部分)进行匹配。若都不对应,则报错。
@Qualifier
1 2 3 4 5 6 7 8 9 @Service(value = "accountService") public class AccountServiceImpl implements IAccountService { @Autowired @Qualifier("accountDao") private IAccountDao accountDao; public void saveAccount () { accountDao.saveAccount(); } }
@Resource
作用:直接按照bean的id
注入。可以独立使用。
属性:name:用于指定bean的id。
1 2 3 4 5 6 7 8 @Service(value = "accountService") public class AccountServiceImpl implements IAccountService { @Resource(name = "accountDao") private IAccountDao accountDao; public void saveAccount () { accountDao.saveAccount(); } }
以上三个注解都只能注入其他bean类型的数据,而基本类型和String类型无法使用上述注解实现。此外,集合类型的注入只能通过XML来实现 。
@Value
1 2 @Value("${jdbc.driver}") private String driver;
用于改变作用范围的注解 作用就和在bean标签中使用scope属性是一样的。
@Scope
作用:用于指定bean的作用范围
属性:value:指定范围的取值,取值有:singleton,prototype,对应单例和多例,默认单例 。
1 2 3 4 5 6 7 8 9 10 @Service(value = "accountService") @Scope(value = "prototype") public class AccountServiceImpl implements IAccountService { @Autowired private IAccountDao accountDao; public void saveAccount () { accountDao.saveAccount(); } }
和生命周期相关的注解 作用就和在bean标签中使用init-method和destroy-method是一样的。了解。
3.3.5 采用xml配置的案例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 <?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 id ="accountService" class ="com.hongyi.service.impl.AccountServiceImpl" > <property name ="accountDao" ref ="accountDao" > </property > </bean > <bean id ="accountDao" class ="com.hongyi.dao.impl.AccountDaoImpl" > <property name ="runner" ref ="runner" > </property > </bean > <bean id ="runner" class ="org.apache.commons.dbutils.QueryRunner" scope ="prototype" > <constructor-arg name ="ds" ref ="dataSource" > </constructor-arg > </bean > <bean id ="dataSource" class ="com.mchange.v2.c3p0.ComboPooledDataSource" > <property name ="driverClass" value ="com.mysql.jdbc.Driver" > </property > <property name ="jdbcUrl" value ="jdbc:mysql://localhost:3306/test" > </property > <property name ="user" value ="root" > </property > <property name ="password" value ="123456" > </property > </bean > </beans >
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 public class AccountDaoImpl implements IAccountDao { private QueryRunner runner; public void setRunner (QueryRunner runner) { this .runner = runner; } public Account findAccountById (Integer id) { try { return runner.query("select * from account where id = ?" ,new BeanHandler <Account>(Account.class),id); } catch (Exception e) { throw new RuntimeException (e); } } public List<Account> findAllAccount () { try { return runner.query("select * from account" ,new BeanListHandler <Account>(Account.class)); } catch (Exception e) { throw new RuntimeException (e); } } public void saveAccount (Account account) { try { runner.update("insert into account(name,money) values(?,?)" ,account.getName(),account.getMoney()); } catch (Exception e) { throw new RuntimeException (e); } } public void updateAccount (Account account) { try { runner.update("update account set name = ?,money = ? where id = ?" ,account.getName(),account.getMoney(),account.getId()); } catch (Exception e) { throw new RuntimeException (e); } } public void deleteAccount (Integer id) { try { runner.update("delete from account where id = ?" ,id); } catch (Exception e) { throw new RuntimeException (e); } } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 public class AccountServiceTest { @Test public void testFindAll () { ApplicationContext ac = new ClassPathXmlApplicationContext ("bean.xml" ); IAccountService as = (IAccountService) ac.getBean("accountService" ); List<Account> allAccount = as.findAllAccount(); for (Account account : allAccount){ System.out.println(account); } } @Test public void testFindOne () { ApplicationContext ac = new ClassPathXmlApplicationContext ("bean.xml" ); IAccountService as = (IAccountService) ac.getBean("accountService" ); Account account = as.findAccountById(1 ); System.out.println(account); } @Test public void testSave () { } @Test public void testUpdate () { } @Test public void testDelete () { } }
3.3.6 采用注解配置的案例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 <?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.hongyi" > </context:component-scan > <bean id ="runner" class ="org.apache.commons.dbutils.QueryRunner" scope ="prototype" > <constructor-arg name ="ds" ref ="dataSource" > </constructor-arg > </bean > <bean id ="dataSource" class ="com.mchange.v2.c3p0.ComboPooledDataSource" > <property name ="driverClass" value ="com.mysql.jdbc.Driver" > </property > <property name ="jdbcUrl" value ="jdbc:mysql://localhost:3306/test" > </property > <property name ="user" value ="root" > </property > <property name ="password" value ="123456" > </property > </bean > </beans >
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 @Repository("accountDao") public class AccountDaoImpl implements IAccountDao { @Autowired private QueryRunner runner; public Account findAccountById (Integer id) { try { return runner.query("select * from account where id = ?" ,new BeanHandler <Account>(Account.class),id); } catch (Exception e) { throw new RuntimeException (e); } } public List<Account> findAllAccount () { try { return runner.query("select * from account" ,new BeanListHandler <Account>(Account.class)); } catch (Exception e) { throw new RuntimeException (e); } } public void saveAccount (Account account) { try { runner.update("insert into account(name,money) values(?,?)" ,account.getName(),account.getMoney()); } catch (Exception e) { throw new RuntimeException (e); } } public void updateAccount (Account account) { try { runner.update("update account set name = ?,money = ? where id = ?" ,account.getName(),account.getMoney(),account.getId()); } catch (Exception e) { throw new RuntimeException (e); } } public void deleteAccount (Integer id) { try { runner.update("delete from account where id = ?" ,id); } catch (Exception e) { throw new RuntimeException (e); } } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 @Service("accountService") public class AccountServiceImpl implements IAccountService { @Autowired private IAccountDao accountDao; public Account findAccountById (Integer id) { return accountDao.findAccountById(id); } public List<Account> findAllAccount () { return accountDao.findAllAccount(); } public void saveAccount (Account account) { accountDao.saveAccount(account); } public void updateAccount (Account account) { accountDao.updateAccount(account); } public void deleteAccount (Integer id) { accountDao.deleteAccount(id); } }
3.3.7 Spring的其它注解
@Configuration
1 ApplicationContext ac = new AnnotationConfigApplicationContext (SpringConfiguration.class);
@ComponentScan
1 <context:component-scan base-package ="com.hongyi" > </context:component-scan >
示例代码:
1 2 3 4 5 6 7 8 9 10 11 @Configuration @ComponentScan(basePackages = {"com.hongyi"}) public class SpringConfiguration { }
@Bean
作用:用于把当前方法的返回值作为bean对象,存入spring的IOC容器中。
属性:name:用于指定bean的id,当不注明时默认是当前方法的名称。
细节:当我们使用注解配置方法时,如果方法有参数,spring框架会去容器中查找有没有可用的bean对象,查找方式同autowired注解的作用是一样的。当有以上三个注解组合使用时,xml配置文件中的所有内容都可以拿掉了。
示例代码:
SpringConfiguration.java代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 @Configuration @ComponentScan(basePackages = {"com.hongyi"}) public class SpringConfiguration { @Bean(name = "runner") public QueryRunner createQueryRunner (DataSource dataSource) { return new QueryRunner (dataSource); } @Bean(name = "dataSource") public DataSource createDataSource () { ComboPooledDataSource ds = null ; try { ds = new ComboPooledDataSource (); ds.setDriverClass("com.mysql.jdbc.Driver" ); ds.setJdbcUrl("jdbc:mysql://localhost:3306/test" ); ds.setUser("root" ); ds.setPassword("12345678" ); return ds; } catch (Exception e) { e.printStackTrace(); } return ds; } }
注意:当不加bean注解时,14行代码new出的对象不会加入到ioc容器当中,当加上bean注解后,spring会自动地将其加入到容器,效果同以下xml配置相同:
1 2 3 4 5 <bean id ="runner" class ="org.apache.commons.dbutils.QueryRunner" scope ="prototype" > <constructor-arg name ="ds" ref ="dataSource" > </constructor-arg > </bean >
通过注解获取容器AnnotationConfigApplicationContext
代码示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 @Test public void testFindAll () { ApplicationContext ac = new AnnotationConfigApplicationContext (SpringConfiguration.class); IAccountService ac = (IAccountService) ac.getBean("accountService" ); List<Account> allAccount = as.findAllAccount(); for (Account account : allAccount){ System.out.println(account); } }
@Import
@PropertySource
代码示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 @Configuration @ComponentScan(basePackages = {"com.hongyi"}) @PropertySource("classpath:jdbcConfig.properties") public class SpringConfiguration { @Value("${jdbc.driver}") private String driver; @Value("${jdbc.url}") private String url; @Value("${jdbc.username}") private String username; @Value("${jdbc.password}") private String password; @Bean(name = "runner") public QueryRunner createQueryRunner (DataSource dataSource) { return new QueryRunner (dataSource); } @Bean(name = "dataSource") public DataSource createDataSource () { ComboPooledDataSource ds = null ; try { ds = new ComboPooledDataSource (); ds.setDriverClass(driver); ds.setJdbcUrl(url); ds.setUser(username); ds.setPassword(password); return ds; } catch (Exception e) { e.printStackTrace(); } return ds; } }
3.3.8 Spring整合JUnit 1) 问题分析
应用程序的入口:main方法
junit单元测试中,没有main方法也能执行:junit中集成了一个main方法,该方法就会判断当前测试类中哪些方法有@Test注解,junit就会让有Test注解的方法执行。
junit不会管是否采用了spring框架。在执行测试方法时,junit不知道是否采用了spring框架。所以不会读取配置文件/配置类来创建ioc容器
由以上三点可知,当测试方法执行时,没有核心容器,即使写了autowired也无法实现注入。
2) 整合配置
导入spring整合junit的坐标
1 2 3 4 5 <dependency > <groupId > org.springframework</groupId > <artifactId > spring-test</artifactId > <version > 5.1.5.RELEASE</version > </dependency >
使用junit提供的一个注解把原有的main方法替换,替换成spring提供的
@RunWith
1 2 3 4 @RunWith(SpringJUnit4ClassRunner.class) public class AccountServiceTest { }
告知spring的运行器,spring的ioc创建时基于xml还是注解的,并说明位置
@ContextConfiguration
locations
:指定xml文件的位置,加上classpath
关键字,表示在类路径下
classes
:指定注解类所在的位置
1 2 3 4 5 @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes = SpringConfiguration.class) public class AccountServiceTest { }
代码示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes = SpringConfiguration.class) public class AccountServiceTest { @Autowired private IAccountService as; @Test public void testFindAll () { List<Account> allAccount = as.findAllAccount(); for (Account account : allAccount){ System.out.println(account); } } @Test public void testFindOne () { Account account = as.findAccountById(1 ); System.out.println(account); } }
2 AOP面向切面 2.1 添加转账方法并演示事务问题
持久层IAccountDao.java
新增findAccountByName()
方法,并在实现类中实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public Account findAccountByName (String accountName) { try { List<Account> accounts = runner.query("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); } }
业务层IAccountService.java
新增transfer()
方法,并在实现类中实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 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); accountDao.updateAccount(target); }
测试代码:
1 2 3 4 5 6 7 8 @Test public void testTransfer () { ApplicationContext ac = new ClassPathXmlApplicationContext ("bean.xml" ); IAccountService as = (IAccountService) ac.getBean("accountService" ); as.transfer("aaa" ,"bbb" ,100f ); }
以上操作不满足事务的一致性。
2.1.1 问题分析
事务的控制都应当在业务层中完成。
2.1.2 连接的工具类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 public class ConnectionUtils { private ThreadLocal<Connection> tl = new ThreadLocal <Connection>(); private DataSource dataSource; public void setDataSource (DataSource dataSource) { this .dataSource = dataSource; } public Connection getConnection () { 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(); } }
2.1.3 管理事务的工具类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 public class TransactionManager { private ConnectionUtils connectionUtils; public void setConnectionUtils (ConnectionUtils connectionUtils) { this .connectionUtils = connectionUtils; } public void beginTransaction () { try { connectionUtils.getConnection().setAutoCommit(false ); } catch (SQLException e) { e.printStackTrace(); } } public void commit () { try { connectionUtils.getConnection().commit(); } catch (SQLException e) { e.printStackTrace(); } } public void rollback () { try { connectionUtils.getConnection().rollback(); connectionUtils.removeConnection(); } catch (SQLException e) { e.printStackTrace(); } } public void release () { try { connectionUtils.getConnection().close(); } catch (SQLException e) { e.printStackTrace(); } } }
2.1.4 改造业务层和持久层实现类代码 业务层AccountServiceImpl
部分代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 private TransactionManager txManager;public void setTxManager (TransactionManager txManager) { this .txManager = txManager; } public List<Account> findAllAccount () { try { txManager.beginTransaction(); List<Account> accounts = accountDao.findAllAccount(); txManager.commit(); return accounts; }catch (Exception e){ txManager.rollback(); throw new RuntimeException (); }finally { txManager.release(); } } public void transfer (String sourceName, String targetName, Float money) { try { txManager.beginTransaction(); Account source = accountDao.findAccountByName(sourceName); Account target = accountDao.findAccountByName(targetName); source.setMoney(source.getMoney()-money); target.setMoney(target.getMoney()+money); accountDao.updateAccount(source); accountDao.updateAccount(target); txManager.commit(); }catch (Exception e){ txManager.rollback(); }finally { txManager.release(); } }
bean.xml
代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 <?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 id ="accountService" class ="com.hongyi.service.impl.AccountServiceImpl" > <property name ="accountDao" ref ="accountDao" > </property > <property name ="txManager" ref ="txManger" > </property > </bean > <bean id ="accountDao" class ="com.hongyi.dao.impl.AccountDaoImpl" > <property name ="runner" ref ="runner" > </property > <property name ="connectionUtils" ref ="connectionUtils" > </property > </bean > <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.jdbc.Driver" > </property > <property name ="jdbcUrl" value ="jdbc:mysql://localhost:3306/test" > </property > <property name ="user" value ="root" > </property > <property name ="password" value ="123456" > </property > </bean > <bean id ="connectionUtils" class ="com.hongyi.utils.ConnectionUtils" > <property name ="dataSource" ref ="dataSource" > </property > </bean > <bean id ="txManger" class ="com.hongyi.utils.TransactionManager" > <property name ="connectionUtils" ref ="connectionUtils" > </property > </bean > </beans >
问题:耦合度太高
2.2 动态代理 2.2.1 特点
特点:字节码随用随创建,随用随加载。
作用:不修改源码的基础上,对方法增强 。
分类:
基于接口的动态代理,JDK代理
基于子类的动态代理,cglib代理
2.2.2 基于接口的动态代理
(1)ClassLoader
:类加载器;用于加载代理对象的字节码,和被代理对象使用相同的类加载器。固定写法:代理谁,就写谁的类加载器,如:instance.getClass().getClassLoader()
(2)Class[]
:字节码数组;它是用于让代理对象和被代理对象有相同的方法。固定写法:代理谁,就写谁的getClass().getInterfaces()
(3)InvocationHandler
:用于提供增强的代码。它是让我们写如何代理。一般写该接口的实现类,通常情况下为匿名内部类,但不是必须的。此接口的实现类,都是谁用,谁写。
代码示例
IProducer.java
代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public interface IProducer { public void saleProduct (float money) ; public void afterService (float money) ; }
Producer.java
代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public class Producer implements IProducer { public void saleProduct (float money) { System.out.println("销售产品,并拿到钱" + money); } public void afterService (float money) { System.out.println("提供售后服务,并拿到钱" + money); } }
Client.java
代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 public class Client { public static void main (String[] args) { final Producer producer = new Producer (); IProducer proxyProducer = (IProducer) Proxy.newProxyInstance(producer.getClass().getClassLoader(), producer.getClass().getInterfaces(), new InvocationHandler () { public Object invoke (Object proxy, Method method, Object[] args) throws Throwable { Object returnValue = null ; Float money = (Float)args[0 ]; if ("saleProduct" .equals(method.getName())){ returnValue = method.invoke(producer,money*0.8f ); } return returnValue; } }); proxyProducer.saleProduct(10000f ); } }
2.2.3 基于子类的动态代理
导入坐标
1 2 3 4 5 <dependency > <groupId > cglib</groupId > <artifactId > cglib</artifactId > <version > 3.3.0</version > </dependency >
(1)Class
:字节码;用于指定被代理对象的字节码。固定写法。
(2)Callback
:用于提供增强的代码。它是让我们写如何代理。一般写该接口的子接口的实现类:MethodInterceptor
,通常情况下为匿名内部类,但不是必须的。此接口的实现类,都是谁用,谁写。
代码示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 public class Client { public static void main (String[] args) { final Producer producer = new Producer (); Producer cglibProducer = (Producer) Enhancer.create(producer.getClass(), new MethodInterceptor () { public Object intercept (Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable { Object returnValue = null ; Float money = (Float)objects[0 ]; if ("saleProduct" .equals(method.getName())){ returnValue = method.invoke(producer,money*0.8f ); } return returnValue; } }); cglibProducer.saleProduct(10000 ); } }
2.2.4 利用动态代理实现事务控制 BeanFactory.java
代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 public class BeanFactory { private IAccountService accountService; private TransactionManager txManager; public void setTxManager (TransactionManager txManager) { this .txManager = txManager; } public final void setAccountService (IAccountService accountService) { this .accountService = accountService; } public IAccountService getAccountService () { Proxy.newProxyInstance(accountService.getClass().getClassLoader(), accountService.getClass().getInterfaces(), new InvocationHandler () { public Object invoke (Object proxy, Method method, Object[] args) throws Throwable { Object rtValue = null ; try { txManager.beginTransaction(); rtValue = method.invoke(accountService, args); txManager.commit(); return rtValue; } catch (Exception e) { txManager.rollback(); throw new RuntimeException (); } finally { txManager.release(); } } }); return null ; } }
bean.xml
代码
1 2 3 4 5 6 7 8 9 10 <bean id ="proxyAccountService" factory-bean ="beanFactory" factory-method ="getAccountService" > </bean > <bean id ="beanFactory" class ="com.hongyi.factory.BeanFactory" > <property name ="accountService" ref ="accountService" > </property > <property name ="txManager" ref ="txManger" > </property > </bean >
2.3 面向切面编程 2.3.1 AOP概念 在软件业,AOP为Aspect Oriented Programming的缩写,意为:面向切面编程,通过预编译方式和运行期间动态代理实现程序功能的统一维护的一种技术。AOP是OOP的延续,是软件开发中的一个热点,也是Spring框架中的一个重要内容,是函数式编程 的一种衍生范型。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。
简单来说,就是把程序中重复的代码抽取出来,在需要执行的时候,使用动态代理技术,在不修改源码的基础上,对已有的方法进行增强 。目的:解耦。
示例
先来看一个例子, 如何给如下UserServiceImpl
中所有方法添加进入方法的日志:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public class UserServiceImpl implements IUserService { @Override public List<User> findUserList () { System.out.println("execute method: findUserList" ); return Collections.singletonList(new User ("pdai" , 18 )); } @Override public void addUser () { System.out.println("execute method: addUser" ); } }
我们将记录日志功能解耦为日志切面,它的目标是解耦。进而引出AOP的理念:就是将分散在各个业务逻辑代码中相同的代码通过横向切割 的方式抽取到一个独立的模块中。
OOP面向对象编程,针对业务处理过程的实体及其属性和行为进行抽象封装,以获得更加清晰高效的逻辑单元划分。而AOP则是针对业务处理过程中的切面进行提取,它所面对的是处理过程的某个步骤或阶段,以获得逻辑过程的中各部分之间低耦合的隔离效果。
2.3.2 AOP相关术语
连接点Joinpoint:告诉AOP在哪里干 ,指的是那些被拦截的点,在spring中指的是方法,因为spring只支持方法类型的连接点。业务层接口中的方法都是连接点。
切入点Pointcut:连接点的集合。
通知Advice:告诉AOP干什么 。指的是拦截到连接点之后要做的事情。通知类型有前置通知,后置通知,异常通知,最终通知,环绕通知。
引入Introduction:是一种特殊的通知,在不修改代码的前提下,引入可以在运行期间为类动态地添加一些方法或字段。
目标对象Target:代理的目标对象,即被代理对象 。
织入Weaving:告诉AOP如何实现 。把增强应用到目标对象来创建新的代理对象的过程。
代理Proxy:一个类被AOP织入增强后,就产生了一个结果代理类。即代理对象。
切面Aspect:是切入点和通知(引介)的结合。
2.3.3 基于XML配置的AOP 1) 必要的代码 业务层接口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public interface IAccountService { public void saveAccount () ; public void updateAccount (int i) ; int deleteAccount () ; }
业务层实现类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public class AccountService implements IAccountService { public void saveAccount () { System.out.println("执行了保存账户" ); } public void updateAccount (int i) { System.out.println("执行了更新账户" +i); } public int deleteAccount () { System.out.println("执行了删除账户" ); return 0 ; } }
记录日志的工具类
1 2 3 4 5 6 7 8 9 10 11 12 public class Logger { public void beforePrintLog () { System.out.println("Logger类中的printLog方法开始记录日志" ); } }
2) 配置步骤
把通知bean也交给spring来管理
使用aop:config
标签表明开始aop配置
使用aop:aspect
标签表明开始配置切面,属性有:
id
:给切面配置一个唯一标识
ref
:指定通知类bean的id
在aop:aspect
标签的内部,使用对应的标签来配置通知的类型,示例中是让pringLog方法在切入点执行之前执行,所以是前置通知。属性有:
method
:用于指定Logger类中哪个方法是前置的。
pointcut
:用于指定切入点表达式,该表达式的含义是指对业务层中哪些方法增强。写法:
关键字:execution(表达式)
表达式:访问修饰符+返回值+包名.包名…..类名.方法名(参数)
标准的表达式写法
:(只对某个方法增强)
1 public void com.hongyi.service.impl.AccountService.saveAccount()
其中访问修饰符可以省略;
返回值可以使用通配符,表示任意返回值;
包名可以使用通配符,表示任意包,但有几级包就要有几级通配符;也可以使用..
代表当前包及其子包;
类名和方法名都可以使用*
号进行通配;
参数列表用..
表示任意参数和任意类型;
于是有了全通配写法:
全通配写法
:(对所有方法增强)
实际开发中
,切入点表达式的通常写法:切到业务层实现类下的所有方法,即:
1 * com.hongyi.service.impl.*.*(..)
bean.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 <?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" > <bean id ="accountService" class ="com.hongyi.service.impl.AccountService" > </bean > <bean id ="logger" class ="com.hongyi.utils.Logger" > </bean > <aop:config > <aop:aspect id ="logAdvice" ref ="logger" > <aop:before method ="printLog" pointcut ="execution(public void com.hongyi.service.impl.AccountService.saveAccount())" > </aop:before > <aop:before method ="printLog" pointcut ="execution(* com.hongy i.service.impl.*.*(..))" > </aop:before > </aop:aspect > </aop:config > </beans >
测试代码
1 2 3 4 5 6 7 8 9 10 11 12 13 public class AOPTest { public static void main (String[] args) { ApplicationContext ac = new ClassPathXmlApplicationContext ("bean.xml" ); IAccountService as = (IAccountService) ac.getBean("accountService" ); as.saveAccount(); } }
执行结果:
3) 四种常用的通知类型 bean.xml
代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 <?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" > <bean id ="accountService" class ="com.hongyi.service.impl.AccountService" > </bean > <bean id ="logger" class ="com.hongyi.utils.Logger" > </bean > <aop:config > <aop:aspect id ="logAdvice" ref ="logger" > <aop:before method ="beforePrintLog" pointcut ="execution(* com.hongyi.service.impl.*.*(..))" > </aop:before > <aop:after-returning method ="afterReturningPrintLog" pointcut ="execution(* com.hongyi.service.impl.*.*(..))" > </aop:after-returning > <aop:after-throwing method ="afterThrowingPrintLog" pointcut ="execution(* com.hongyi.service.impl.*.*(..))" > </aop:after-throwing > <aop:after method ="afterPrintLog" pointcut ="execution(* com.hongyi.service.impl.*.*(..))" > </aop:after > </aop:aspect > </aop:config > </beans >
执行结果:
4) 通用化切入点表达式 采用标签<aop:pointcut>
,此标签写在aop:aspect
内部,只能在当前切面中使用。它还可以写在aop:aspect
的外面,此时就变成了所有切面可用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 <aop:config > <aop:aspect id ="logAdvice" ref ="logger" > <aop:before method ="beforePrintLog" pointcut-ref ="pt1" > </aop:before > <aop:after-returning method ="afterReturningPrintLog" pointcut-ref ="pt1" > </aop:after-returning > <aop:after-throwing method ="afterThrowingPrintLog" pointcut-ref ="pt1" > </aop:after-throwing > <aop:after method ="afterPrintLog" pointcut-ref ="pt1" > </aop:after > <aop:pointcut id ="pt1" expression ="execution(* com.hongyi.service.impl.*.*(..))" /> </aop:aspect > </aop:config >
5) 环绕通知
测试代码
1 2 3 4 5 6 public void aroundPrintLog () { System.out.println("Logger类中的aroundPrintLog方法开始记录日志" ); }
1 2 3 4 <aop:pointcut id ="pt1" expression ="execution(* com.hongyi.service.impl.*.*(..))" /> <aop:around method ="aroundPrintLog" pointcut-ref ="pt1" > </aop:around >
1 2 3 4 5 6 7 8 9 10 public class AOPTest { public static void main (String[] args) { ApplicationContext ac = new ClassPathXmlApplicationContext ("bean.xml" ); IAccountService as = (IAccountService) ac.getBean("accountService" ); as.saveAccount(); } }
测试结果
问题:当配置了环绕通知后,切入点方法没有执行,而通知方法执行了。
分析:通过对比动态代理中的环绕通知代码,发现动态代理中的环绕通知中有明确的切入点方法调用,而上面的代码中没有。
解决:Spring框架为我们提供了一个接口:ProceedingJoinPoint。该接口有一个方法proceed(),此方法就相当于明确调用切入点方法,该接口可以作为环绕通知的方法参数,在程序执行时,Spring框架会为我们提供该接口的实现类供我们使用。
spring中的环绕通知:一种可以在代码中手动控制增强方法何时执行的方式。相当于自定义何时来执行通知方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public Object aroundPrintLog (ProceedingJoinPoint pjp) { Object rtValue = null ; try { Object[] args = pjp.getArgs(); System.out.println("Logger类中的aroundPrintLog方法开始记录日志,前置" ); rtValue = pjp.proceed(args); System.out.println("Logger类中的aroundPrintLog方法开始记录日志,后置" ); return rtValue; } catch (Throwable throwable) { System.out.println("Logger类中的aroundPrintLog方法开始记录日志,异常" ); throw new RuntimeException (throwable); } finally { System.out.println("Logger类中的aroundPrintLog方法开始记录日志,最终" ); } }
执行结果
2.3.4 基于注解配置的AOP
代码演示
bean.xml
代码
1 2 3 4 5 <context:component-scan base-package ="com.hongyi" > </context:component-scan > <aop:aspectj-autoproxy > </aop:aspectj-autoproxy >
Logger.java
代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 @Component("logger") @Aspect public class Logger { @Pointcut("execution(* com.hongyi.service.impl.*.*(..))") private void pt1 () {} @Before("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方法开始记录日志" ); } @Around("pt1()") public Object aroundPrintLog (ProceedingJoinPoint pjp) { Object rtValue = null ; try { Object[] args = pjp.getArgs(); System.out.println("Logger类中的aroundPrintLog方法开始记录日志,前置" ); rtValue = pjp.proceed(args); System.out.println("Logger类中的aroundPrintLog方法开始记录日志,后置" ); return rtValue; } catch (Throwable throwable) { System.out.println("Logger类中的aroundPrintLog方法开始记录日志,异常" ); throw new RuntimeException (throwable); } finally { System.out.println("Logger类中的aroundPrintLog方法开始记录日志,最终" ); } } }
执行结果
注意最终通知和后置通知的顺序有变化
2.4 AOP日志 3 JdbcTemplate的使用 3.1 基本使用
依赖
1 2 3 4 5 <dependency > <groupId > org.springframework</groupId > <artifactId > spring-jdbc</artifactId > <version > 5.0.2.RELEASE</version > </dependency >
代码示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public class JdbcTemplateDemo1 { public static void main (String[] args) { DriverManagerDataSource ds = new DriverManagerDataSource (); ds.setDriverClassName("com.mysql.jdbc.Driver" ); ds.setUrl("jdbc:mysql://localhost:3306/test" ); ds.setUsername("root" ); ds.setPassword("123456" ); JdbcTemplate jt = new JdbcTemplate (); jt.setDataSource(ds); jt.execute("insert into account(id,name,money) values (4,'ddd',1000)" ); } }
3.2 JdbcTemplate在Spring的ioc容器中的使用
代码示例
bean.xml
代码
1 2 3 4 5 6 7 8 9 10 11 12 <bean id ="jdbcTemplate" class ="org.springframework.jdbc.core.JdbcTemplate" > <property name ="dataSource" ref ="dataSource" > </property > </bean > <bean id ="dataSource" class ="org.springframework.jdbc.datasource.DriverManagerDataSource" > <property name ="driverClassName" value ="com.mysql.jdbc.Driver" > </property > <property name ="url" value ="jdbc:mysql://localhost:3306/test?useSSL=false" > </property > <property name ="username" value ="root" > </property > <property name ="password" value ="123456" > </property > </bean >
JdbcTemplateDemo2.java
代码
1 2 3 4 5 6 7 8 9 10 public class JdbcTemplateDemo2 { public static void main (String[] args) { ApplicationContext ac = new ClassPathXmlApplicationContext ("bean.xml" ); JdbcTemplate jt = (JdbcTemplate) ac.getBean("jdbcTemplate" ,JdbcTemplate.class); jt.execute("insert into account(id,name,money) values (10,'ddd',1000)" ); } }
3.3 JdbcTemplate的CRUD操作
代码示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 public class JdbcTemplateDemo2 { public static void main (String[] args) { ApplicationContext ac = new ClassPathXmlApplicationContext ("bean.xml" ); JdbcTemplate jt = (JdbcTemplate) ac.getBean("jdbcTemplate" ,JdbcTemplate.class); jt.update("insert into account (id,name,money) values (8,'hongyi',999)" ); jt.update("update account set name=?,money=? where id=?" ,"test" ,100 ,1 ); jt.update("delete from account where id=?" ,1 ); List<Account> accounts = jt.query("select * from account where money > ?" ,new AccountRowMapper (),999f ); List<Account> accounts = jt.query("select * from account where money > ?" ,new BeanPropertyRowMapper <Account>(Account.class),999f ); for (Account account : accounts){ System.out.println(account.getId()+" " +account.getName()+" " +account.getMoney()); } List<Account> accounts = jt.query("select * from account where id = ?" ,new BeanPropertyRowMapper <Account>(Account.class),2 ); System.out.println(accounts.isEmpty()?"没有该用户" :accounts.get(0 )); Long count = jt.queryForObject("select count(*) from account where money > ?" ,Long.class,100 ); System.out.println(count); } } class AccountRowMapper implements RowMapper <Account>{ public Account mapRow (ResultSet resultSet, int i) throws SQLException { Account account = new Account ();; account.setId(resultSet.getInt("id" )); account.setName(resultSet.getString("name" )); account.setMoney(resultSet.getFloat("money" )); return account; } }
3.4 JdbcTemplate的在DAO中的使用
代码示例
AccountDaoImpl.java
代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 public class AccountDaoImpl implements IAccountDao { private JdbcTemplate jdbcTemplate; public void setJdbcTemplate (JdbcTemplate jdbcTemplate) { this .jdbcTemplate = jdbcTemplate; } public Account findAccountById (Integer id) { List<Account> accountList = jdbcTemplate.query("select * from account where id = ?" ,new BeanPropertyRowMapper <Account>(Account.class),id); return accountList.isEmpty()?null :accountList.get(0 ); } public Account findAccountByName (String name) { List<Account> accountList = jdbcTemplate.query("select * from account where name = ?" ,new BeanPropertyRowMapper <Account>(Account.class),name); if (accountList.isEmpty()){ return null ; }else if (accountList.size()>1 ){ throw new RuntimeException ("结果集不唯一" ); } return accountList.get(0 ); } public void updateAccount (Account account) { jdbcTemplate.update("update account set name = ?,money = ? where id = ?" ,account.getName(),account.getMoney(),account.getId()); } }
bean.xml
1 2 3 4 <bean id ="accountDao" class ="com.hongyi.dao.impl.AccountDaoImpl" > <property name ="jdbcTemplate" ref ="jdbcTemplate" > </property > </bean >
测试代码
1 2 3 4 5 6 7 8 public class JdbcTemplateDemo3 { public static void main (String[] args) { ApplicationContext ac = new ClassPathXmlApplicationContext ("bean.xml" ); IAccountDao accountDao = (IAccountDao) ac.getBean("accountDao" ); System.out.println(accountDao.findAccountById(2 )); System.out.println(accountDao.findAccountByName("bbb" )); } }
注意当有多个Dao实现类时,可继承JdbcDaoSupport类,去除重复代码。
代码示例
1 2 3 public class AccountDaoImpl2 extends JdbcDaoSupport implements IAccountDao { }
4 Spring事务 4.1 要点
JavaEE体系进行分层开发,事务处理位于业务层,Spring为业务层的事务处理解决方案。
Spring的事务控制都是基于AOP的,它既可以通过编程的方式实现(编程式),也可以使用配置的方式实现(声明式)。重点是采用配置的方式实现AOP 。
4.2 事务的四个特性 ACID
原子性(Atomicity):事务是一个原子操作,由一系列动作组成。事务的原子性确保动作要么全部完成,要么完全不起作用。
一致性(Consistency):一旦事务完成(不管成功还是失败),系统必须确保它所建模的业务处于一致的状态,而不会是部分完成部分失败。在现实中的数据不应该被破坏。
隔离性(Isolation):可能有许多事务会同时处理相同的数据,因此每个事务都应该与其他事务隔离开来,防止数据损坏。
持久性(Durability):一旦事务完成,无论发生什么系统错误,它的结果都不应该受到影响,这样就能从任何系统崩溃中恢复过来。通常情况下,事务的结果被写到持久化存储器中。
Spring事务管理涉及的接口的联系如下:
4.3 基于XML的声明式事务控制 4.3.1 项目搭建
需求
用户aaa向用户bbb转账100元,事务开始前,两者都有初始资金1000:
项目结构
导入相关依赖
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 <dependencies > <dependency > <groupId > org.springframework</groupId > <artifactId > spring-context</artifactId > <version > 5.3.9</version > </dependency > <dependency > <groupId > org.springframework</groupId > <artifactId > spring-jdbc</artifactId > <version > 5.2.13.RELEASE</version > </dependency > <dependency > <groupId > org.springframework</groupId > <artifactId > spring-tx</artifactId > <version > 5.2.13.RELEASE</version > </dependency > <dependency > <groupId > mysql</groupId > <artifactId > mysql-connector-java</artifactId > <version > 5.1.47</version > </dependency > <dependency > <groupId > org.aspectj</groupId > <artifactId > aspectjweaver</artifactId > <version > 1.9.6</version > </dependency > <dependency > <groupId > junit</groupId > <artifactId > junit</artifactId > <version > 4.12</version > <scope > test</scope > </dependency > <dependency > <groupId > org.springframework</groupId > <artifactId > spring-test</artifactId > <version > 5.3.9</version > <scope > test</scope > </dependency > </dependencies >
重要类代码
AccountServiceImpl.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 public class AccountServiceImpl implements IAccountService { private IAccountDao accountDao; public void setAccountDao (IAccountDao accountDao) { this .accountDao = accountDao; } @Override public Account findAccountById (Integer accountId) { return accountDao.findAccountById(accountId); } @Override public void transfer (String sourceName, String targetName, Float money) { System.out.println("transfer...." ); 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); } }
AccountServiceTest.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(locations = "classpath:bean.xml") public class AccountServiceTest { @Autowired private IAccountService as; @Test public void testTransfer () { as.transfer("aaa" ,"bbb" ,100f ); } }
4.3.2 XML配置的声明式事务 1) 配置步骤
配置事务管理器
配置事务通知 :
配置AOP中的通用切入点表达式
配置事务的属性:是在事务的通知tx:advice标签的内部
2) 事务的属性
isolation:用于指定事务的隔离级别。默认是DEFAULT,表示使用数据库的默认隔离级别
propagation:用于指定事务的传播行为,默认为REQUIRED,表示一定会有事务,增删改的选择。查询方法可以选择SUPPORTS
read-only:用于指定事务是否只读。只有查询方法才能设置为true,默认为false,表示读写
timeout:用于指定事务的超时时间,默认为-1,表示永不超时。如果指定了数值,则以秒为单位
rollback-for:用于指定一个异常,当产生该异常时,事务回滚,产生其他异常时,不回滚。没有默认值,表示任何异常都回滚
no-rollback-for:与上述相反
3) bean.xml整体配置代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 <?xml version="1.0" encoding="UTF-8" ?> <beans xmlns ="http://www.springframework.org/schema/beans" xmlns:xsi ="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop ="http://www.springframework.org/schema/aop" xmlns:tx ="http://www.springframework.org/schema/tx" xsi:schemaLocation =" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd" > <bean id ="accountService" class ="com.itheima.service.impl.AccountServiceImpl" > <property name ="accountDao" ref ="accountDao" > </property > </bean > <bean id ="accountDao" class ="com.itheima.dao.impl.AccountDaoImpl" > <property name ="dataSource" ref ="dataSource" > </property > </bean > <bean id ="dataSource" class ="org.springframework.jdbc.datasource.DriverManagerDataSource" > <property name ="driverClassName" value ="com.mysql.jdbc.Driver" > </property > <property name ="url" value ="jdbc:mysql://localhost:3306/eesy" > </property > <property name ="username" value ="root" > </property > <property name ="password" value ="12345678" > </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" > <tx:attributes > <tx:method name ="*" propagation ="REQUIRED" read-only ="false" /> <tx:method name ="find*" propagation ="SUPPORTS" read-only ="true" /> </tx:attributes > </tx:advice > <aop:config > <aop:pointcut id ="pt1" expression ="execution(* com.itheima.service.impl.*.*(..))" /> <aop:advisor advice-ref ="txAdvice" pointcut-ref ="pt1" > </aop:advisor > </aop:config > </beans >
4.4 基于注解的声明式事务控制 项目搭建同4.3节
4.4.1 xml引入注解依赖 1 2 3 4 5 6 7 8 9 10 11 12 13 14 <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" >
4.4.2 配置步骤
配置事务管理器
开启spring对注解事务的支持
在需要事务支持的地方使用@Transactional
注解
bean.xml代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 <context:component-scan base-package ="com.itheima" > </context:component-scan > <bean id ="jdbcTemplate" class ="org.springframework.jdbc.core.JdbcTemplate" > <property name ="dataSource" ref ="dataSource" > </property > </bean > <bean id ="accountService" class ="com.itheima.service.impl.AccountServiceImpl" > <property name ="accountDao" ref ="accountDao" > </property > </bean > <bean id ="accountDao" class ="com.itheima.dao.impl.AccountDaoImpl" > <property name ="dataSource" ref ="dataSource" > </property > </bean > <bean id ="dataSource" class ="org.springframework.jdbc.datasource.DriverManagerDataSource" > <property name ="driverClassName" value ="com.mysql.jdbc.Driver" > </property > <property name ="url" value ="jdbc:mysql://localhost:3306/eesy" > </property > <property name ="username" value ="root" > </property > <property name ="password" value ="12345678" > </property > </bean > <bean id ="transactionManager" class ="org.springframework.jdbc.datasource.DataSourceTransactionManager" > <property name ="dataSource" ref ="dataSource" > </property > </bean > <tx:annotation-driven transaction-manager ="transactionManager" > </tx:annotation-driven >
AccountDaoImpl.java
1 2 3 4 5 6 7 8 9 10 @Repository("accountDao") public class AccountDaoImpl implements IAccountDao { @Autowired private JdbcTemplate jdbcTemplate; }
AccountServiceImpl.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 @Service("accountService") @Transactional(propagation = Propagation.SUPPORTS, readOnly = true) public class AccountServiceImpl implements IAccountService { @Autowired private IAccountDao accountDao; public void setAccountDao (IAccountDao accountDao) { this .accountDao = accountDao; } @Override public Account findAccountById (Integer accountId) { return accountDao.findAccountById(accountId); } @Transactional(propagation = Propagation.REQUIRED, readOnly = false) @Override public void transfer (String sourceName, String targetName, Float money) { } }
4.4.3 基于纯注解的声明式事务控制
项目架构
SpringConfiguration.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @Configuration @ComponentScan("com.itheima") @Import({JdbcConfig.class, TransactionConfig.class}) @PropertySource("jdbcConfig.properties") @EnableTransactionManagement public class SpringConfiguration {}
TransactionConfig.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public class TransactionConfig { @Bean("transactionManager") public PlatformTransactionManager createTransactionManager (DataSource dataSource) { return new DataSourceTransactionManager (dataSource); } }
JdbcConfig.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 public class JdbcConfig { @Value("${jdbc.driver}") private String driver; @Value("${jdbc.username}") private String username; @Value("${jdbc.password}") private String password; @Value("${jdbc.url}") private String url; @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.setUsername(username); ds.setPassword(password); return ds; } }
jdbcConfig.properties
1 2 3 4 jdbc.driver = com.mysql.jdbc.Driver jdbc.url = jdbc:mysql://localhost:3306/eesy jdbc.username = root jdbc.password = 12345678
其余dao层和service层的代码不变,这样就删除了bean.xml配置,采用了纯注解的形式来控制事务。
4.5 @Transactional注解 4.5.1 作用域
当标注在类上的时候:表示给该类所有的 public
方法添加上 @Transactional注解
当标注在方法上的时候:事务的作用域就只在该方法上生效,并且如果类及方法上都配置 @Transactional注解时,方法的注解会覆盖类上的注解
4.5.2 属性 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 @Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Inherited @Documented public @interface Transactional { @AliasFor("transactionManager") String value () default "" ; @AliasFor("value") String transactionManager () default "" ; Propagation propagation () default Propagation.REQUIRED; Isolation isolation () default Isolation.DEFAULT; int timeout () default -1 ; boolean readOnly () default false ; Class<? extends Throwable >[] rollbackFor() default {}; String[] rollbackForClassName() default {}; Class<? extends Throwable >[] noRollbackFor() default {}; String[] noRollbackForClassName() default {}; }
① 传播行为 事务传播:当一个事务方法被另一个事务方法调用时,这个事务方法应该如何进行。
例如:methodA事务方法调用methodB事务方法时,methodB是继续在调用者methodA的事务中运行呢,还是为自己开启一个新事务运行,这就是由methodB的事务传播行为决定的。
1 2 3 4 5 6 7 8 9 10 @Transactional(Propagation=XXX) public void methodA () { methodB(); } @Transactional(Propagation=XXX) public void methodB () { }
事务传播行为有7种,默认是 REQUIRED
传播机制。
如果A方法调用B方法:
值
含义
REQUIRED
如果A存在事务,则B加入该事务;如果A不存在事务,则B创建一个新的事务;
SUPPORTS
如果A存在事务,则加入该事务;如果A不存在事务,则B以非事务的方式继续运行;
MANDATORY
如果A存在事务,则加入该事务;如果A不存在事务,则B抛出异常;
REQUIRES_NEW
如果A不存在事务,重新创建一个新的事务;如果A存在事务,则B暂停当前的事务;
NOT_SUPPORTED
以非事务的方式运行;如果A存在事务,则B暂停当前的事务
NEVER
以非事务的方式运行;如果A存在事务,则B抛出异常
NESTED
如果A存在事务,则B在该事务内嵌套事务运行;如果A不存在事务,则B创建一个新的事务;
REQUIRED和NESTED区别
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @Service public class ServiceA { @Autowired private ServiceB serviceB; @Transactional public void A () { try { serviceB.B(); }catch (Exception e){ e.printStackTrace(); } } }
1 2 3 4 5 6 7 @Service public class ServiceB { @Transactional(propagation = Propagation.REQUIRED) public void B () { } }
情况一:A方法中出现了异常,结果A、B方法修改操作都会被回滚
情况二:B方法中出现了异常,结果A、B方法修改操作都会被回滚(一荣俱荣,一损俱损)
如果将B方法修改为NESTED
:
1 2 3 4 5 6 7 @Service public class ServiceB { @Transaction(propagation = Propagation.NESTED) public void B () { } }
情况一:A方法中出现了异常,结果A、B方法修改操作都会被回滚
情况二:B方法中出现了异常,结果B方法修改操作被回滚,A方法修改操作提交
② 隔离级别 Spring事务隔离级别对应的事务隔离级别与 MySQL 一致。
③ 读写事务
设置为true:表示只读,如果该方法内存在增、删、改操作则会抛出异常;
设置为false(默认):表示读写,增、删、改、查操作都允许;
④ 回滚异常类型
rollbackFor
:通过类进行指定,如@Transactional(rollbackFor = {Exception.class})
rollbackForClassName
:通过类名进行指定,如@Transactional(rollbackForClassName = {"java.lang.Exception"})
Spring默认抛出了未检查unchecked异常(继承自 RuntimeException 的异常)或者 Error才回滚事务;其他异常不会触发回滚事务。如果在事务中抛出其他类型的异常,但却期望 Spring 能够回滚事务,就需要指定 rollbackFor 或者 rollbackForClassName 属性。
4.6 事务失效的场景 在某些业务场景下,如果一个请求中,需要同时写入多张表的数据 。为了保证操作的原子性(要么同时成功,要么同时失败),避免数据不一致的情况,我们一般都会用到spring事务。
4.6.1 没有被 Spring 管理 1 2 3 4 5 6 7 @Service public class OrderServiceImpl implements OrderService { @Transactional public void updateOrder (Order order) { } }
如果此时把@Service
注解注释掉,那么这个类就不会被加载成一个Bean,这个类就不会Spring管理了,事务自然就失效了。
4.6.2 没有在Spring配置文件中启用事务管理器 如果是Spring Boot项目,它默认会自动配置事务管理器并开启事务支持。
4.6.3 数据库引擎不支持事务 这里以 MySQL为例,MyISAM引擎是不支持事务操作的,一般要支持事务都会使用InnoDB引擎,根据MySQL 的官方文档说明,从MySQL 5.5.5 开始的默认存储引擎是 InnoDB,之前默认的都是 MyISAM,所以这一点要值得注意,如果底层引擎不支持事务,那么再怎么设置也没有用。
4.6.4 方法不是 public @Transactional
注解只能用于public 的方法上,否则事务不会生效,如果要用在非public的方法上,则可以开启基于 AspectJ 框架的静态代理模式。
4.6.5 在类中方法内部调用(类方法自调用)* 1 2 3 4 5 6 7 8 9 10 11 @Service public class OrderServiceImpl implements OrderService { public void update (Order order) { updateOrder(order); } @Transactional public void updateOrder (Order order) { } }
事务不生效的原因 : 事务是通过Spring AOP代理来实现的,而在同一个类中,一个方法调用另一个方法时,调用方法直接调用目标方法的代码,而不是通过代理类进行调用 。即以上代码,调用目标updateOrder
方法不是通过代理类进行的,因此事务不生效。
此外:
1 2 3 4 5 6 7 8 9 10 11 12 @Service public class OrderServiceImpl implements OrderService { @Transactional public void update (Order order) { updateOrder(order); } @Transactional(propagation = Propagation.REQUIRES_NEW) public void updateOrder (Order order) { } }
这次在 update 方法上加了 @Transactional
,如果在 updateOrder 上加了 REOUIRES_NEW
新开启一个事务,也不会生效。因为它们发生了自身调用,调用了该类自己的方法,而没有经过Spring的代理类,默认只有调用外部代理类的方法,事务才会生效。
Spring之所以可以对开启@Transactional的方法进行事务管理,是因为Spring为当前类生成了一个代理类,然后在执行相关方法时,会判断这个方法有没有@Transactional注解,如果有的话,则会开启一个事务。
代码模拟——类方法自调用
1 2 3 4 5 6 7 8 public interface OrderService { void verifyOrderParameters () ; void saveOrder () ; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public class OrderServiceImpl implements OrderService { @Override public void verifyOrderParameters () { System.out.println("校验订单参数" ); saveOrder(); } @Override @Transactional public void saveOrder () { System.out.println("保存订单信息到DB" ); } }
一个静态代理类,模拟Spring在事务中动态生成的代理类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public class OrderServiceImplProxy implements OrderService { private OrderServiceImpl orderServiceImpl; public OrderServiceImplProxy (OrderServiceImpl orderServiceImpl) { this .orderServiceImpl = orderServiceImpl; } @Override public void verifyOrderParameters () { orderServiceImpl.verifyOrderParameters(); } @Override public void saveOrder () { System.out.println("开启事务..." ); orderServiceImpl.saveOrder(); System.out.println("提交事务..." ); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 public class Client { public static void main (String[] args) { OrderServiceImpl orderServiceImpl = new OrderServiceImpl (); OrderServiceImplProxy orderServiceImplProxy = new OrderServiceImplProxy (orderServiceImpl); orderServiceImplProxy.verifyOrderParameters(); System.out.println("--------------------------------------------" ); orderServiceImplProxy.saveOrder(); } }
1 2 3 4 5 6 校验订单参数 保存订单信息到DB -------------------------------------------- 开启事务... 保存订单信息到DB 提交事务...
可以看到,类方法自调用并没有走代理对象(第1,2行输出);
解决方案:获取本对象的代理对象,再进行调用。
在Spring 中可以在当前线程中暴露并获取当前代理类,通过在启动类上添加以下注解来启用暴露代理类,如下面的示例所示。
1 2 @EnableAspectJAutoProxy(exposeProxy = true)
然后通过以下代码获取当前代理类,并调用代理类的事务方法:
1 2 3 4 5 6 7 8 9 10 11 12 @Service public class OrderServiceImpl implements OrderService { public void update (Order order) { ((OrderService) AopContext.currentProxy()).updateOrder(); } @Transactional public void updateOrder (Order order) { } }
4.6.6 异常类型不匹配 @Transactional
注解默认的回滚异常类型是运行时异常(RuntimeException)。
1 2 3 4 5 6 7 8 9 10 11 @Service public class OrderServiceImpl implements OrderService { @Transactional public void update (Order order) { try { }catch { throw new Exception ("更新失败" ); } } }
因为 Spring 默认回滚的是 RuntimeException 异常,和程序抛出的 Exception 异常不匹配,所以事务也是不生效的。如果要触发默认RuntimeException之外异常的回滚,则需要在 @Transactional 事务注解上指定异常类,示例如下:
1 2 3 4 5 6 7 8 9 10 11 @Service public class OrderServiceImpl implements OrderService { @Transactional(rollbackFor = Exception.class) public void update (Order order) { try { }catch { throw new Exception ("更新失败" ); } } }
4.6.7 异常被捕获但未抛出 当抛出的异常被try-catch捕获时,事务也会失效,具体看代码:
1 2 3 4 5 6 7 8 9 10 11 12 @Service public class OrderServiceImpl implements OrderService { @Transactional(rollbackFor = Exception.class) public void update (Order order) { try { }catch (Exception e){ e.printStackTrace(); } } }
这个方法把异常给捕获了,但没有抛出来,所以事务不会回滚。
需要修改如下:
1 2 3 4 5 6 7 8 9 10 11 12 @Service public class OrderServiceImpl implements OrderService { @Transactional(rollbackFor = Exception.class) public void update (Order order) { try { }catch (Exception e){ e.printStackTrace(); throw new Exception ("更新失败" ); } } }