当前位置: 首页 > news >正文

网站建设的课件汽车网站的建设方向

网站建设的课件,汽车网站的建设方向,网站首页快照应该怎么,django企业网站开发实例Spring#xff08;3#xff09;Spring从零到入门 - Spring整合技术及AOP事务管理 文章目录 Spring#xff08;3#xff09;Spring从零到入门 - Spring整合技术及AOP事务管理4 Spring整合技术示例4.1 Spring整合Mybatis4.1.1 Mybatis开发回顾4.1.2 整合Spring分析4.1.3 Spri…Spring3Spring从零到入门 - Spring整合技术及AOP事务管理 文章目录 Spring3Spring从零到入门 - Spring整合技术及AOP事务管理4 Spring整合技术示例4.1 Spring整合Mybatis4.1.1 Mybatis开发回顾4.1.2 整合Spring分析4.1.3 Spring整合Mybatis 4.2 Spring整合Junit4.2.1 环境准备4.2.2 整合Junit步骤 5 AOP5.1 AOP简介5.2 AOP入门案例5.2.1 环境准备5.2.2 实现步骤 5.3 AOP工作流程5.3.1 AOP工作流程5.3.2 验证容器中是否为代理对象5.3.2.1 不被增强5.3.2.2 被增强 5.3.3 AOP核心概念 5.4 AOP配置管理5.4.1 AOP切入点表达式5.4.1.1 语法格式5.4.1.2 通配符5.4.1.3 书写技巧 5.4.2 AOP通知增强类型5.4.2.1 类型介绍**前置通知****后置通知****环绕通知****返回后通知****异常后通知****环绕通知扩展** 通知类型总结 5.4.3 AOP获取数据5.4.3.1 获取参数5.4.3.2 获取返回值5.4.3.3 获取异常 5.5 AOP总结5.5.1 AOP的核心概念5.5.2 切入点表达式5.5.3 五种通知类型5.5.4 通知中获取参数 5.6 AOP事务管理5.6.1 Spring事务简介5.6.1.1 转账案例 5.6.2 Spring事务角色5.6.2.1 未开启Spring事务之前5.6.2.2 开启了Spring事务之后 5.6.3 Spring事务属性5.6.3.1 事务配置5.6.3.2 事务传播行为**5.6.3.2.1 事务传播行为**5.6.3.2.2 事务传播行为属性说明**5.6.3.2.1 事务传播行为**5.6.3.2.2 事务传播行为属性说明 4 Spring整合技术示例 4.1 Spring整合Mybatis 4.1.1 Mybatis开发回顾 步骤1:准备数据库表 Mybatis是来操作数据库表所以先创建一个数据库及表 create database spring_db character set utf8; use spring_db; create table tbl_account(id int primary key auto_increment,name varchar(35),money double );步骤2:创建项目导入jar包 项目的pom.xml添加相关依赖 dependenciesdependencygroupIdorg.springframework/groupIdartifactIdspring-context/artifactIdversion5.2.10.RELEASE/version/dependencydependencygroupIdcom.alibaba/groupIdartifactIddruid/artifactIdversion1.1.16/version/dependencydependencygroupIdorg.mybatis/groupIdartifactIdmybatis/artifactIdversion3.5.6/version/dependencydependencygroupIdmysql/groupIdartifactIdmysql-connector-java/artifactIdversion5.1.47/version/dependency /dependencies步骤3:根据表创建模型类 public class Account implements Serializable {private Integer id;private String name;private Double money;//setter...getter...toString...方法略 }回顾**Serializable** Serializable 是一个标记接口继承了这个接口相关对象可实现序列化 什么场景中会涉及到将对象进行**序列化Serializable** 分布式系统 在分布式系统中不同的服务可能运行在不同的物理机器上。为了在这些服务之间传递对象需要将对象转换成字节序列以便在网络上传输。这是一种常见的序列化应用场景。缓存 在将对象存储到缓存中时有些缓存系统要求存储的对象是可序列化的。这样可以在需要时将对象保存到缓存中以提高数据访问性能。消息队列 在消息队列系统中消息通常需要被序列化以进行传输。生产者将对象序列化为消息而消费者则负责将消息反序列化为对象。 步骤4:创建Dao接口 public interface AccountDao {Insert(insert into tbl_account(name,money)values(#{name},#{money}))void save(Account account);Delete(delete from tbl_account where id #{id} )void delete(Integer id);Update(update tbl_account set name #{name} , money #{money} where id #{id} )void update(Account account);Select(select * from tbl_account)ListAccount findAll();Select(select * from tbl_account where id #{id} )Account findById(Integer id); }步骤5:创建Service接口和实现类 public interface AccountService {void save(Account account);void delete(Integer id);void update(Account account);ListAccount findAll();Account findById(Integer id);}Service public class AccountServiceImpl implements AccountService {Autowiredprivate AccountDao accountDao;public void save(Account account) {accountDao.save(account);}public void update(Account account){accountDao.update(account);}public void delete(Integer id) {accountDao.delete(id);}public Account findById(Integer id) {return accountDao.findById(id);}public ListAccount findAll() {return accountDao.findAll();} }步骤6:添加jdbc.properties文件 resources目录下添加用于配置数据库连接四要素 jdbc.drivercom.mysql.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/spring_db?useSSLfalse jdbc.usernameroot jdbc.password1234useSSL:关闭MySQL的SSL连接 在MySQL中**SSLSecure Sockets Layer连接**是通过使用SSL协议来加密和保护数据库连接的一种方式。SSL是一种用于在计算机网络上进行安全通信的协议它使用加密算法来确保数据在传输过程中的机密性和完整性。对于数据库连接使用SSL可以有效地保护敏感信息防止在传输过程中被窃听或篡改。 使用SSL连接MySQL的过程包括以下步骤 生成SSL证书和私钥配置MySQL服务器配置MySQL客户端 步骤7:添加Mybatis核心配置文件 ?xml version1.0 encodingUTF-8? !DOCTYPE configurationPUBLIC -//mybatis.org//DTD Config 3.0//ENhttp://mybatis.org/dtd/mybatis-3-config.dtd configuration!--读取外部properties配置文件--properties resourcejdbc.properties/properties!--别名扫描的包路径--typeAliasespackage namecom.ibaidu.domain//typeAliases!--数据源--environments defaultmysqlenvironment idmysqltransactionManager typeJDBC/transactionManagerdataSource typePOOLEDproperty namedriver value${jdbc.driver}/propertyproperty nameurl value${jdbc.url}/propertyproperty nameusername value${jdbc.username}/propertyproperty namepassword value${jdbc.password}/property/dataSource/environment/environments!--映射文件扫描包路径--mapperspackage namecom.ibaidu.dao/package/mappers /configuration第一行读取外部properties配置文件Spring有提供具体的解决方案PropertySource,需要交给Spring 第二行起别名包扫描为SqlSessionFactory服务的需要交给Spring 别名配置允许你给Java类设置一个别名 typeAliasestypeAlias aliasUser typecom.example.User/ /typeAliases上述配置将com.example.User类设置别名为User之后在映射文件中可以使用resultMap、parameterMap等元素时可以使用User代替完整的类名 包扫描Type Aliases Package typeAliasespackage namecom.example.domain/ /typeAliases包扫描配置允许你指定一个包名MyBatis将会自动扫描该包下的所有类并将这些类设置为别名。这样你在映射文件中引用类时只需使用类名而不必配置每个类的别名 第三行主要用于做连接池Spring之前我们已经整合了Druid连接池这块也需要交给Spring 前面三行一起都是为了创建SqlSession对象用的SqlSession是由SqlSessionFactory创建出来的所以只需要将SqlSessionFactory交给Spring管理即可。 第四行是Mapper接口和映射文件[如果使用注解就没有该映射文件]这个是在获取到SqlSession以后执行具体操作的时候用所以它和SqlSessionFactory创建的时机都不在同一个时间可能需要单独管理。 这段XML配置是MyBatis中用于配置映射器mappers的一部分。具体来说它通过package元素指定了映射器接口所在的包名MyBatis将会扫描该包下的所有映射器接口并自动加载它们 步骤8:编写应用程序 public class App {public static void main(String[] args) throws IOException {// 1. 创建SqlSessionFactoryBuilder对象SqlSessionFactoryBuilder sqlSessionFactoryBuilder new SqlSessionFactoryBuilder();// 2. 加载SqlMapConfig.xml配置文件InputStream inputStream Resources.getResourceAsStream(SqlMapConfig.xml);// 3. 创建SqlSessionFactory对象SqlSessionFactory sqlSessionFactory sqlSessionFactoryBuilder.build(inputStream);// 4. 获取SqlSessionSqlSession sqlSession sqlSessionFactory.openSession();// 5. 执行SqlSession对象执行查询获取结果UserAccountDao accountDao sqlSession.getMapper(AccountDao.class);Account ac accountDao.findById(1);System.out.println(ac);// 6. 释放资源sqlSession.close();} }SqlSessionFactoryBuilder的主要作用是读取MyBatis的配置信息并构建出一个SqlSessionFactory对象 SqlSessionFactory是“生产”SqlSession的“工厂” SqlSession代表Java程序和数据库之间的会话。HttpSession是Java程序和浏览器之间的会话 工厂模式如果创建某一个对象使用的过程基本固定那么我们就可以把创建这个对象的相关代码封装到一个“工厂类”中以后都使用这个工厂类来“生产”我们需要的对象。 步骤9:运行程序 4.1.2 整合Spring分析 Mybatis的基础环境我们已经准备好了接下来就得分析下在上述的内容中哪些对象可以交给Spring来管理? 真正需要交 给Spring管理的是**SqlSessionFactory** 4.1.3 Spring整合Mybatis 前面我们已经分析了Spring与Mybatis的整合大体需要做两件事 第一件事是:Spring要管理MyBatis中的SqlSessionFactory 第二件事是:Spring要管理Mapper接口的扫描 步骤1:项目中导入整合需要的jar包 dependency!--Spring操作数据库需要该jar包--groupIdorg.springframework/groupIdartifactIdspring-jdbc/artifactIdversion5.2.10.RELEASE/version /dependency dependency!--Spring与Mybatis整合的jar包这个jar包mybatis在前面是Mybatis提供的--groupIdorg.mybatis/groupIdartifactIdmybatis-spring/artifactIdversion1.3.0/version /dependency步骤2:创建Spring的主配置类 //配置类注解 Configuration //包扫描主要扫描的是项目中的AccountServiceImpl类 ComponentScan(com.ibaidu) public class SpringConfig { } 步骤3:创建数据源的配置类 在配置类中完成数据源的创建 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;Beanpublic DataSource dataSource(){DruidDataSource ds new DruidDataSource();ds.setDriverClassName(driver);ds.setUrl(url);ds.setUsername(userName);ds.setPassword(password);return ds;} }步骤4:主配置类中读properties并引入数据源配置类 Configuration ComponentScan(com.ibaidu) PropertySource(classpath:jdbc.properties) Import(JdbcConfig.class) public class SpringConfig { } 步骤5:创建Mybatis配置类并配置SqlSessionFactory public class MybatisConfig {//定义beanSqlSessionFactoryBean用于产生SqlSessionFactory对象Beanpublic SqlSessionFactoryBean sqlSessionFactory(DataSource dataSource){SqlSessionFactoryBean ssfb new SqlSessionFactoryBean();//设置模型类的别名扫描ssfb.setTypeAliasesPackage(com.ibaidu.domain);//设置数据源ssfb.setDataSource(dataSource);return ssfb;}//定义bean返回MapperScannerConfigurer对象Beanpublic MapperScannerConfigurer mapperScannerConfigurer(){MapperScannerConfigurer msc new MapperScannerConfigurer();msc.setBasePackage(com.ibaidu.dao);return msc;} }说明: 使用SqlSessionFactoryBean封装SqlSessionFactory需要的环境信息 SqlSessionFactoryBean是前面我们讲解FactoryBean的一个子类在该类中将SqlSessionFactory的创建进行了封装简化对象的创建我们只需要将其需要的内容设置即可。 使用MapperScannerConfigurer加载Dao接口创建代理对象保存到IOC容器中 这个**MapperScannerConfigurer对象也是MyBatis提供的专用于整合的jar包中的类**用来处理原始配置文件中的mappers相关配置加载数据层的Mapper接口类MapperScannerConfigurer有一个核心属性basePackage就是用来设置所扫描的包路径 步骤6:主配置类中引入Mybatis配置类 Configuration ComponentScan(com.ibaidu) PropertySource(classpath:jdbc.properties) Import({JdbcConfig.class,MybatisConfig.class}) public class SpringConfig { }Import中的加载顺序 JdbcConfig.class和MybatisConfig.class的代码顺序并不会影响它们的加载顺序Spring容器会根据配置类的依赖关系和其他条件以一种合适的顺序加载它们。 步骤7:编写运行类 在运行类中从IOC容器中获取Service对象调用方法获取结果 public class App2 {public static void main(String[] args) {ApplicationContext ctx new AnnotationConfigApplicationContext(SpringConfig.class);AccountService accountService ctx.getBean(AccountService.class);Account ac accountService.findById(1);System.out.println(ac);} } 步骤8:运行程序 支持Spring与Mybatis的整合就已经完成了其中主要用到的两个类分别是: SqlSessionFactoryBeanMapperScannerConfigurer 4.2 Spring整合Junit Junit是一个搞单元测试用的工具它不是我们程序的主体也不会参加最终程序的运行从作用上来说就和之前的东西不同它不是做功能的更像是一个辅助工具 4.2.1 环境准备 步骤1:引入依赖 pom.xml dependencygroupIdjunit/groupIdartifactIdjunit/artifactIdversion4.12/versionscopetest/scope /dependencydependencygroupIdorg.springframework/groupIdartifactIdspring-test/artifactIdversion5.2.10.RELEASE/version /dependency4.2.2 整合Junit步骤 在test\java下创建一个AccountServiceTest,这个名字任意 //设置类运行器 RunWith(SpringJUnit4ClassRunner.class) //设置Spring环境对应的配置类 ContextConfiguration(classes {SpringConfiguration.class}) //加载配置类 //ContextConfiguration(locations{classpath:applicationContext.xml})//加载配置文件 public class AccountServiceTest {//支持自动装配注入beanAutowiredprivate AccountService accountService;Testpublic void testFindById(){System.out.println(accountService.findById(1));}Testpublic void testFindAll(){System.out.println(accountService.findAll());} }注意: 单元测试如果测试的是注解配置类则使用ContextConfiguration(classes 配置类.class) 单元测试如果测试的是配置文件则使用ContextConfiguration(locations{配置文件名,...}) 虽然配置文件本身不包含业务逻辑但在测试中仍然有一些可以考虑的方面 配置项的正确性测试配置文件的存在性检查环境变量和配置文件的协同测试等 Junit运行后是基于Spring环境运行的所以Spring提供了一个专用的类运行器这个务必要设置这个类运行器就在Spring的测试专用包中提供的导入的坐标就是这个东西SpringJUnit4ClassRunner 上面两个配置都是固定格式当需要测试哪个bean时使用自动装配加载对应的对象下面的工作就和以前做Junit单元测试完全一样了 知识点1RunWith 名称RunWith类型测试类注解位置测试类定义上方作用设置JUnit运行器属性value默认运行所使用的运行期 知识点2ContextConfiguration 名称ContextConfiguration类型测试类注解位置测试类定义上方作用设置JUnit加载的Spring核心配置属性classes核心配置类可以使用数组的格式设定加载多个配置类locations:配置文件可以使用数组的格式设定加载多个配置文件名称 关于测试类的文件位置 在Java项目中通常会有一些约定俗成的目录结构用于存放源代码和测试代码。这种目录结构有助于开发者更容易地组织和管理代码。一般而言测试类Test Classes通常存放在与源代码相对应的测试目录中 src ├── main │ └── java │ └── com │ └── example │ └── MyClass.java └── test└── java└── com└── example└── MyClassTest.java按照这个结构存放确实一下就可以识别到相关的测试类和相关的注解 运行后报错Class not found: “com.baidu.test.AccountServiceTest” 原因pom.xml配置中配置了其他的测试类的地址注释掉之后就不会出错了 运行结果 如果想要自定义测试类那么 ① 在 pom.xml 文件中将 testSourceDirectory 标签设置为新的测试目录路径。 buildtestSourceDirectorysrc/test2/java/testSourceDirectory!-- 其他构建配置 -- /build② 确保在你的测试类上使用了适当的测试框架如 JUnit并在类上添加 RunWith 注解。 RunWith(SpringJUnit4ClassRunner.class) ContextConfiguration(classes {SpringConfig.class}) public class AccountServiceTest {Autowiredprivate AccountService accountService;Testpublic void test01(){System.out.println(this.accountService.findById(1));} }**但是**识别不到这个文件无法测试 建议还是将测试类及其代码放在约定俗成相应的文件夹中 对于接口的注入报错 接口AccountDao采用注解的方式实现了具体的方法所以没有AccountDaoImpl这样的实现类 所以相关注入会报错 但这种情况下不用管这个报错程序能够正常执行因为MybatisConfig中扫描了相关文件加载Dao接口创建代理对象保存到IOC容器中 public class MybatisConfig {Beanpublic SqlSessionFactoryBean sqlSessionFactory(DataSource dataSource){SqlSessionFactoryBean ssfb new SqlSessionFactoryBean();ssfb.setTypeAliasesPackage(com.baidu.domain);ssfb.setDataSource(dataSource);return ssfb;}Beanpublic MapperScannerConfigurer mapperScannerConfigurer(){MapperScannerConfigurer msc new MapperScannerConfigurer();msc.setBasePackage(com.baidu.dao);return msc;} }5 AOP Spring有两个核心的概念一个是IOC/DI一个是AOP AOP是在不改原有代码的前提下对其进行增强 5.1 AOP简介 AOP(Aspect Oriented Programming)面向切面编程一种编程范式指导开发者如何组织程序结构 OOP(Object Oriented Programming)面向对象编程 OOP是一种编程思想那么AOP也是一种编程思想编程思想主要的内容就是指导程序员该如何编写程序所以它们两个是不同的编程范式Programming paradigm 作用: 在不惊动原始设计的基础上为其进行功能增强前面咱们有技术就可以实现这样的功能即代理模式。 如何理解这里的**功能增强**呢 Repository public class BookDaoImpl implements BookDao {public void save() {//记录程序当前执行执行开始时间Long startTime System.currentTimeMillis();//业务执行万次for (int i 0;i10000;i) {System.out.println(book dao save ...);}//记录程序当前执行时间结束时间Long endTime System.currentTimeMillis();//计算时间差Long totalTime endTime-startTime;//输出信息System.out.println(执行万次消耗时间 totalTime ms);}public void update(){System.out.println(book dao update ...);}public void delete(){System.out.println(book dao delete ...);}public void select(){System.out.println(book dao select ...);} }于计算万次执行消耗的时间只有save方法有可不可以让delete和update方法也有呢可不可以让select方法保持原来的效果即没有呢 用Spring的AOP就可以实现。 在不惊动(改动)原有设计(代码)的前提下想给谁添加功能就给谁添加。 这个也就是Spring的理念无入侵式/无侵入式。前面的注入也具有这样的思想提供了一个变量相关的对象就有值了 背后的原理是什么样的呢核心概念 连接点类里面哪些方法可以被增强这些方法称为连接点 连接点(JoinPoint)程序执行过程中的任意位置粒度为执行方法、抛出异常、设置变量等 在SpringAOP中理解为方法的执行 切入点实际被真正增强的方法称为切入点 切入点(Pointcut): 匹配连接点的式子 在SpringAOP中一个切入点可以描述一个具体方法也可也匹配多个方法 一个具体的方法:如com.baidu.dao包下的BookDao接口中的无形参无返回值的save方法匹配多个方法:所有的save方法所有的get开头的方法所有以Dao结尾的接口中的任意方法所有带有一个参数的方法 连接点范围要比切入点范围大是切入点的方法也一定是连接点但是是连接点的方法就不一定要被增强所以可能不是切入点。 通知增强实际增强的逻辑部分称为通知增强通知有多种类型前置通知后置通知环绕通知异常通知最终通知 通知(Advice): 在切入点处执行的操作也就是共性功能 在SpringAOP中功能最终以方法的形式呈现 通知类通知是一个方法方法不能独立存在需要被写在一个类中即通知类 切面是动作把通知应用到切入点的过程个切入点需要添加哪个通知就需要提前将它们之间的关系描述清楚 5.2 AOP入门案例 使用SpringAOP的注解方式完成在方法执行前添加打印出当前系统时间的功能 5.2.1 环境准备 pom.xml添加Spring依赖 dependenciesdependencygroupIdorg.springframework/groupIdartifactIdspring-context/artifactIdversion5.2.10.RELEASE/version/dependency /dependencies添加BookDao和BookDaoImpl类 public interface BookDao {public void save();public void update(); }Repository public class BookDaoImpl implements BookDao {public void save() {System.out.println(System.currentTimeMillis());System.out.println(book dao save ...);}public void update(){System.out.println(book dao update ...);} }目前打印save方法的时候因为方法中有打印系统时间所以运行的时候是可以看到系统时间 对于update方法来说就没有该功能我们要使用SpringAOP的方式在不改变update方法的前提下让其具有打印系统时间的功能。 创建Spring的配置类 Configuration ComponentScan(com.baidu) public class SpringConfig { }编写App运行类 public class App {public static void main(String[] args) {ApplicationContext ctx new AnnotationConfigApplicationContext(SpringConfig.class);BookDao bookDao ctx.getBean(BookDao.class);bookDao.save();} }5.2.2 实现步骤 步骤1:添加依赖 pom.xml dependencygroupIdorg.aspectj/groupIdartifactIdaspectjweaver/artifactIdversion1.9.4/version /dependency因为spring-context中已经导入了spring-aop,所以不需要再单独导入spring-aop导入AspectJ的jar包AspectJ是AOP思想的一个具体实现Spring有自己的AOP实现但是相比于AspectJ来说比较麻烦所以我们直接采用Spring整合ApsectJ的方式进行AOP开发。 步骤2:定义接口与实现类 public interface BookDao {public void save();public void update(); }Repository public class BookDaoImpl implements BookDao {public void save() {System.out.println(System.currentTimeMillis());System.out.println(book dao save ...);}public void update(){System.out.println(book dao update ...);} }步骤3:定义通知类和通知 通知就是将共性功能抽取出来后形成的方法共性功能指的就是当前系统时间的打印。 public class MyAdvice {public void method(){System.out.println(System.currentTimeMillis());} }类名和方法名没有要求可以任意。 步骤4:定义切入点 BookDaoImpl中有两个方法分别是save和update我们要增强的是update方法该如何定义呢? public class MyAdvice {Pointcut(execution(void com.baidu.dao.BookDao.update()))private void pt(){}public void method(){System.out.println(System.currentTimeMillis());} }说明: 切入点定义依托一个不具有实际意义的方法进行即无参数、无返回值、方法体无实际逻辑。execution及后面编写的内容指定了一个方法的签名规定了哪些方法会被选定作为切入点。后面会详细讲解 步骤5:制作切面 切面是用来描述通知和切入点之间的关系如何进行关系的绑定? public class MyAdvice {Pointcut(execution(void com.baidu.dao.BookDao.update()))private void pt(){}Before(pt())public void method(){System.out.println(System.currentTimeMillis());} }绑定切入点与通知关系并指定通知添加到原始连接点的具体执行位置 说明:Before翻译过来是之前也就是说通知会在切入点方法执行之前执行除此之前还有其他四种类型。 切入点的定义和切面的制作都是在通知类中进行的。因为不能改动原有设计代码 步骤6将通知类配给容器并标识其为切面类 Component Aspect public class MyAdvice {Pointcut(execution(void com.baidu.dao.BookDao.update()))private void pt(){}Before(pt())public void method(){System.out.println(System.currentTimeMillis());} }步骤7开启注解格式AOP功能 Configuration ComponentScan(com.baidu) EnableAspectJAutoProxy public class SpringConfig { }步骤8运行程序 public class App {public static void main(String[] args) {ApplicationContext ctx new AnnotationConfigApplicationContext(SpringConfig.class);BookDao bookDao ctx.getBean(BookDao.class);bookDao.update();} }执行结果 知识点1EnableAspectJAutoProxy 名称EnableAspectJAutoProxy类型配置类注解位置配置类定义上方作用开启注解格式AOP功能 知识点2Aspect 名称Aspect类型类注解位置切面类定义上方作用设置当前类为AOP切面类 知识点3Pointcut 名称Pointcut类型方法注解位置切入点方法定义上方作用设置切入点方法属性value默认切入点表达式定义切入点定义依托一个不具有实际意义的方法进行即无参数、无返回值、方法体无实际逻辑 知识点4Before 名称Before类型方法注解位置通知方法定义上方作用设置当前通知方法与切入点之间的绑定关系当前通知方法在原始切入点方法前运行 5.3 AOP工作流程 5.3.1 AOP工作流程 核心代理模式 这得从Spring加载bean说起… 流程1: Spring容器启动 容器启动就需要去加载bean,哪些类会被加载呢?需要被增强的类如:BookServiceImpl通知类如:MyAdvice注意此时bean对象还没有创建成功 流程2: 读取所有切面配置中的切入点 上面这个例子中有两个切入点的配置但是第一个切入点定义时依托的方法ptx()并没有被使用所以不会被读取。如何看有没有被使用就看切面注解上有没有这个方法即可 流程3:初始化bean 判定bean对应的类中的方法是否匹配到任意切入点流程2中已经读取了切面中所有的切入点 注意第1步在容器启动的时候bean对象还没有被创建成功 要对实例化bean对象的类中的方法和切入点进行匹配 匹配失败创建原始对象, 如UserDao 匹配失败说明不需要增强直接调用原始对象的方法即可。 匹配成功创建原始对象目标对象的代理对象如:BookDao 匹配成功说明需要对其进行增强对哪个类做增强这个类对应的对象就叫做目标对象因为要对目标对象进行功能增强而采用的技术是动态代理所以会为其创建一个代理对象最终运行的是代理对象的方法在该方法中会对原始方法进行功能增强 流程4:获取bean执行方法 获取的bean是原始对象时调用方法并执行完成操作获取的bean是代理对象时根据代理对象的运行模式运行原始方法与增强的内容完成操作 5.3.2 验证容器中是否为代理对象 如果目标对象中的方法不被增强那么容器中将存入的是目标对象本身如果目标对象中的方法会被增强那么容器中将存入的是目标对象的代理对象 5.3.2.1 不被增强 Component Aspect public class MyAdvice {Pointcut(execution(void com.baidu.dao.BookDao.update1()))private void pt(){}Before(pt())public void method(){System.out.println(System.currentTimeMillis());} }切入点中update1这个方法是不存在的 运行代码 public class App {public static void main(String[] args) {ApplicationContext ctx new AnnotationConfigApplicationContext(SpringConfig.class);BookDao bookDao ctx.getBean(BookDao.class);System.out.println(bookDao);System.out.println(bookDao.getClass());} }运行结果 5.3.2.2 被增强 Component Aspect public class MyAdvice {Pointcut(execution(void com.baidu.dao.BookDao.update1()))private void pt(){}Before(pt())public void method(){System.out.println(System.currentTimeMillis());} }同样的运行代码运行出的结果 类型是代理类。 为什么打印出来的对象仍是BookDaoImpl 因为Spring的AOP对其toString方法进行了重写所以打印出来的对象会感觉是BookDaoImpl类型 5.3.3 AOP核心概念 在上面介绍AOP的工作流程中我们提到了两个核心概念分别是: 目标对象(Target)原始功能去掉共性功能对应的类产生的对象这种对象是无法直接完成最终工作的代理(Proxy)目标对象无法直接完成工作需要对其进行功能回填通过原始对象的代理对象实现 简单来说 目标对象就是要增强的类[如:BookServiceImpl类]对应的对象也叫原始对象不能说它不能运行只能说它在运行的过程中对于要增强的内容是缺失的。 SpringAOP是在不改变原有设计(代码)的前提下对其进行增强的它的底层采用的是代理模式实现的所以要对原始对象进行增强就需要对原始对象创建代理对象在代理对象中的方法把通知[如:MyAdvice中的method方法]内容加进去就实现了增强,这就是我们所说的代理(Proxy)。 5.4 AOP配置管理 5.4.1 AOP切入点表达式 形如execution(void com.baidu.dao.BookDao.update())即为切入点表达式 对于切入点表达式重点关注其语法格式、通配符和书写技巧。 5.4.1.1 语法格式 切入点: 要进行增强的方法 因为调用接口方法的时候最终运行的还是其实现类的方法所以有两种描述方式 描述方式一执行com.baidu.dao包下的BookDao接口中的无参数update方法 execution(void com.baidu.dao.BookDao.update())描述方式二执行com.baidu.dao.impl包下的BookDaoImpl类中的无参数update方法 execution(void com.baidu.dao.impl.BookDaoImpl.update())切入点表达式要进行增强的方法的描述方式 对于**切入点表达式的语法**为: 切入点表达式标准格式动作关键字(访问修饰符 返回值 包名.类/接口名.方法名(参数) 异常名 对于这个格式我们不需要硬记通过一个例子理解它: execution(public User com.baidu.service.UserService.findById(int))execution动作关键字描述切入点的行为动作例如execution表示执行到指定切入点public: 访问修饰符,还可以是publicprivate等可以省略User返回值写返回值类型com.baidu.service包名多级包使用点连接UserService: 类/接口名称findById方法名int: 参数直接写参数的类型多个类型用逗号隔开异常名方法定义中抛出指定异常可以省略 但如果每一个方法对应一个切入点表达式编写起来会比较麻烦有没有更简单的方式呢? 就需要用到下面的通配符。 5.4.1.2 通配符 我们使用通配符描述切入点主要的目的就是简化之前的配置 *:单个独立的任意符号可以独立出现也可以作为前缀或者后缀的匹配符出现 executionpublic * com.baidu.*.UserService.find*(*))匹配com.baidu包下的任意包中的UserService类或接口中所有find开头的带有一个参数的方法 ..多个连续的任意符号可以独立出现常用于简化包名与参数的书写 executionpublic User com..UserService.findById(..))匹配com包下的任意包中的UserService类或接口中所有名称为findById的方法 专用于匹配子类类型 execution(* *..*Service.*(..))这个使用率较低描述子类的咱们做JavaEE开发继承机会就一次使用都很慎重所以很少用它。*Service表示所有以Service结尾的接口的子类。 实际案例分析 execution(void com.baidu.dao.BookDao.update()) 匹配接口能匹配到 execution(void com.baidu.dao.impl.BookDaoImpl.update()) 匹配实现类能匹配到 execution(* com.baidu.dao.impl.BookDaoImpl.update()) 返回值任意能匹配到 execution(* com.baidu.dao.impl.BookDaoImpl.update(*)) 返回值任意但是update方法必须要有一个参数无法匹配要想匹配需要在update接口和实现类添加参数 execution(void com.*.*.*.*.update()) 返回值为void,com包下的任意包三层包下的任意类的update方法匹配到的是实现类能匹配 execution(void com.*.*.*.update()) 返回值为void,com包下的任意两层包下的任意类的update方法匹配到的是接口能匹配 execution(void *..update()) 返回值为void方法名是update的任意包下的任意类能匹配 execution(* *..*(..)) 匹配项目中任意类的任意方法能匹配但是不建议使用这种方式影响范围广 execution(* *..u*(..)) 匹配项目中任意包任意类下只要以u开头的方法update方法能满足能匹配 execution(* *..*e(..)) 匹配项目中任意包任意类下只要以e结尾的方法update和save方法能满足能匹配 execution(void com..*()) 返回值为voidcom包下的任意包任意类任意方法能匹配*代表的是方法 execution(* com.baidu.*.*Service.find*(..)) 将项目中所有业务层方法的以find开头的方法匹配 execution(* com.baidu.*.*Service.save*(..)) 将项目中所有业务层方法的以save开头的方法匹配后面两种更符合我们平常切入点表达式的编写规则 5.4.1.3 书写技巧 切入点表达式的编写其实是很灵活的常用的书写技巧所有代码按照标准规范开发否则以下技巧全部失效 描述切入点通常描述接口而不描述实现类, 如果描述到实现类就出现紧耦合了 访问控制修饰符针对接口开发均采用public描述可省略访问控制修饰符描述 返回值类型对于**增删改类使用精准类型加速匹配**对于**查询类使用*通配快速描述** 包名书写尽量不使用…匹配效率过低常用*做单个包描述匹配或精准匹配 接口名/类名书写名称与模块相关的采用*匹配例如UserService书写成*Service绑定业务层接口名 方法名书写以动词进行精准匹配名词采用匹配例如getById书写成getBy,selectAll书写成selectAll 参数规则较为复杂根据业务方法灵活调整 通常不使用异常作为匹配规则 指定在目标方法抛出异常时执行的通知advice Aspect Component public class ExceptionAspect {AfterThrowing(pointcut execution(* com.example.service.*.*(..)), throwing exception)public void handleException(Exception exception) {// 在方法抛出异常时执行此通知System.out.println(Exception caught: exception.getMessage());// 可以在这里执行异常处理逻辑例如记录日志、发送通知等} }5.4.2 AOP通知增强类型 回顾 它所代表的含义是将通知添加到切入点方法执行的前面 除了加在前面的类型还有没有加在其他地方的类型 5.4.2.1 类型介绍 前置通知后置通知环绕通知(重点)返回后通知 (了解)抛出异常后通知 (了解) (1) 前置通知追加功能到方法执行前,类似于在代码1或者代码2添加内容 (2) 后置通知, 追加功能到方法执行后,不管方法执行的过程中有没有抛出异常都会执行类似于在代码5添加内容 (3) 返回后通知, 追加功能到方法执行后只有方法正常执行结束后才进行,类似于在代码3添加内容如果方法执行抛出异常返回后通知将不会被添加 (4) 抛出异常后通知,追加功能到方法抛出异常后只有方法执行出异常才进行,类似于在代码4添加内容只有方法抛出异常后才会被添加 (5) 环绕通知, 环绕通知功能比较强大它可以追加功能到方法执行的前后这也是比较常用的方式它可以实现其他四种通知类型的功能 环境准备 dependenciesdependencygroupIdorg.springframework/groupIdartifactIdspring-context/artifactIdversion5.2.10.RELEASE/version/dependencydependencygroupIdorg.aspectj/groupIdartifactIdaspectjweaver/artifactIdversion1.9.4/version/dependency /dependenciespublic interface BookDao {public void update();public int select(); }Repository public class BookDaoImpl implements BookDao {public void update(){System.out.println(book dao update ...);}public int select() {System.out.println(book dao select is running ...);return 100;} }Configuration ComponentScan(com.baidu) EnableAspectJAutoProxy public class SpringConfig { }Component Aspect public class MyAdvice {Pointcut(execution(void com.baidu.dao.BookDao.update()))private void pt(){}public void before() {System.out.println(before advice ...);}public void after() {System.out.println(after advice ...);}public void around(){System.out.println(around before advice ...);System.out.println(around after advice ...);}public void afterReturning() {System.out.println(afterReturning advice ...);}public void afterThrowing() {System.out.println(afterThrowing advice ...);} }public class App {public static void main(String[] args) {ApplicationContext ctx new AnnotationConfigApplicationContext(SpringConfig.class);BookDao bookDao ctx.getBean(BookDao.class);bookDao.update();} }通知类型的使用 前置通知 修改MyAdvice,在before方法上添加Before注解 Component Aspect public class MyAdvice {Pointcut(execution(void com.baidu.dao.BookDao.update()))private void pt(){}Before(pt())//此处也可以写成 Before(MyAdvice.pt()),不建议public void before() {System.out.println(before advice ...);} }后置通知 Component Aspect public class MyAdvice {Pointcut(execution(void com.baidu.dao.BookDao.update()))private void pt(){}Before(pt())public void before() {System.out.println(before advice ...);}After(pt())public void after() {System.out.println(after advice ...);} }运行结果 环绕通知 试着编写这样的通知并调用 Component Aspect public class MyAdvice {Pointcut(execution(void com.baidu.dao.BookDao.update()))private void pt(){}Around(pt())public void around(){System.out.println(around before advice ...);System.out.println(around after advice ...);} }运行结果 结果通知的内容打印出来但是原始方法的内容却没有被执行并且之前的前置后置通知也没显示结果。 因为环绕通知需要在原始方法的前后进行增强所以环绕通知就必须要能对原始操作进行调用 环绕通知示例如下 Component Aspect public class MyAdvice {Pointcut(execution(void com.baidu.dao.BookDao.update()))private void pt(){}Around(pt())public void around(ProceedingJoinPoint pjp) throws Throwable{System.out.println(around before advice ...);//表示对原始操作的调用pjp.proceed();System.out.println(around after advice ...);} }提示proceed()为什么要抛出异常? 原因查看源码 运行结果 注意事项 原始方法有返回值的处理 select方法带有一个int的返回值 public interface BookDao {public void update();public int select(); }对select方法添加环绕通知 Component Aspect public class MyAdvice {Pointcut(execution(void com.baidu.dao.BookDao.update()))private void pt(){}Pointcut(execution(int com.baidu.dao.BookDao.select()))private void pt2(){}Around(pt2())public void aroundSelect(ProceedingJoinPoint pjp) throws Throwable {System.out.println(around before advice ...);//表示对原始操作的调用pjp.proceed();System.out.println(around after advice ...);} }修改App类调用select方法 public class App {public static void main(String[] args) {ApplicationContext ctx new AnnotationConfigApplicationContext(SpringConfig.class);BookDao bookDao ctx.getBean(BookDao.class);int num bookDao.select();System.out.println(num);} }会报错 错误大概的意思是:空的返回不匹配原始方法的int返回 void就是返回Null原始方法就是BookDao下的select方法 所以如果我们使用环绕通知的话要根据原始方法的返回值来设置环绕通知的返回值具体解决方案为 Component Aspect public class MyAdvice {Pointcut(execution(void com.baidu.dao.BookDao.update()))private void pt(){}Pointcut(execution(int com.baidu.dao.BookDao.select()))private void pt2(){}Around(pt2())public Object aroundSelect(ProceedingJoinPoint pjp) throws Throwable {System.out.println(around before advice ...);//表示对原始操作的调用Object ret pjp.proceed();System.out.println(around after advice ...);return ret;} }说明: 为什么返回的是Object而不是int的主要原因是Object类型更通用。 在环绕通知中是可以对原始方法返回值就行修改的。 如何修改 Aspect Component public class ModifyIntReturnValueAspect {Around(execution(* com.example.service.*.*(..)))public Object modifyIntReturnValue(ProceedingJoinPoint joinPoint) throws Throwable {// 调用目标方法并获取原始的返回值int originalReturnValue (int) joinPoint.proceed();// 修改返回值这里演示简单地加上一个固定的值int modifiedReturnValue originalReturnValue 10;// 返回修改后的返回值return modifiedReturnValue;} }Object是所有类的根类因此它可以接受任何非基本数据类型比如int的对象。这是由于自动装箱Autoboxing的特性 返回后通知 Component Aspect public class MyAdvice {Pointcut(execution(void com.baidu.dao.BookDao.update()))private void pt(){}Pointcut(execution(int com.baidu.dao.BookDao.select()))private void pt2(){}AfterReturning(pt2())public void afterReturning() {System.out.println(afterReturning advice ...);} }运行结果这里注释掉了around但保留了Before和After 注意返回后通知是需要在原始方法select正常执行后才会被执行如果select()方法执行的过程中出现了异常那么返回后通知是不会被执行。后置通知是不管原始方法有没有抛出异常都会被执行。 异常后通知 Component Aspect public class MyAdvice {Pointcut(execution(void com.baidu.dao.BookDao.update()))private void pt(){}Pointcut(execution(int com.baidu.dao.BookDao.select()))private void pt2(){}AfterThrowing(pt2())public void afterThrowing() {System.out.println(afterThrowing advice ...);} }//可以在select()方法中添加一行代码int i 1/0来产生异常运行结果 环绕通知扩展 思考下环绕通知是如何实现其他通知类型的功能的 因为环绕通知是**可以控制原始方法执行 pjp.proceed();**的所以我们把增强的代码写在调用原始方法的不同位置就可以实现不同的通知类型的功能 通知类型总结 知识点1After 名称After类型方法注解位置通知方法定义上方作用设置当前通知方法与切入点之间的绑定关系当前通知方法在原始切入点方法后运行 知识点2AfterReturning 名称AfterReturning类型方法注解位置通知方法定义上方作用设置当前通知方法与切入点之间绑定关系当前通知方法在原始切入点方法正常执行完毕后执行 知识点3AfterThrowing 名称AfterThrowing类型方法注解位置通知方法定义上方作用设置当前通知方法与切入点之间绑定关系当前通知方法在原始切入点方法运行抛出异常后执行 知识点4Around 名称Around类型方法注解位置通知方法定义上方作用设置当前通知方法与切入点之间的绑定关系当前通知方法在原始切入点方法前后运行 环绕通知注意事项 环绕通知必须依赖形参ProceedingJoinPoint才能实现对原始方法的调用进而实现原始方法调用前后同时添加通知通知中如果未使用ProceedingJoinPoint对原始方法进行调用的话将跳过原始方法的执行对原始方法的调用可以不接收返回值通知方法设置成void即可如果接收返回值最好设定为Object类型原始方法的返回值如果是void类型通知方法的返回值类型可以设置成void,也可以设置成Object由于无法预知原始方法运行后是否会抛出异常因此环绕通知方法必须要处理Throwable异常 介绍完这么多种通知类型具体该选哪一种呢? 我们可以通过一些案例加深下对通知类型的学习。 5.4.3 AOP获取数据 通知增强中获取切入点的数据如获取参数获取返回值获取异常 获取切入点方法的参数所有的通知类型都可以获取参数 JoinPoint类和ProceedingJoinPoint类都是通知中函数参数的数据类型 如 JoinPoint适用于前置、后置、返回后、抛出异常后通知 ProceedingJoinPoint适用于环绕通知 获取切入点方法返回值前置和抛出异常后通知是没有返回值后置通知可有可无所以不做研究 回顾为什么要获取返回值 如果使用**返回后通知或者环绕通知要根据原始方法的返回值来设置通知的返回值**否则会报错 返回后通知 环绕通知 获取切入点方法运行异常信息前置和返回后通知是不会有后置通知可有可无所以不做研究 抛出异常后通知环绕通知 5.4.3.1 获取参数 非环绕通知获取方式 在方法上添加JoinPoint通过JoinPoint来获取参数 Component Aspect public class MyAdvice {Pointcut(execution(* com.baidu.dao.BookDao.findName(..)))private void pt(){}Before(pt())public void before(JoinPoint jp) Object[] args jp.getArgs();System.out.println(Arrays.toString(args));System.out.println(before advice ... );}//...其他的略 }运行方法 public class App {public static void main(String[] args) {ApplicationContext ctx new AnnotationConfigApplicationContext(SpringConfig.class);BookDao bookDao ctx.getBean(BookDao.class);String name bookDao.findName(100,baidu);System.out.println(name);} }运行结果 说明: 使用JoinPoint的方式获取参数适用于前置、后置、返回后、抛出异常后通知 环绕通知获取方式 环绕通知使用的是ProceedingJoinPoint因为ProceedingJoinPoint是JoinPoint类的子类所以对于ProceedingJoinPoint类中应该也会有对应的getArgs()方法 Component Aspect public class MyAdvice {Pointcut(execution(* com.baidu.dao.BookDao.findName(..)))private void pt(){}Around(pt())public Object around(ProceedingJoinPoint pjp)throws Throwable {Object[] args pjp.getArgs();System.out.println(Arrays.toString(args));Object ret pjp.proceed();return ret;}//其他的略 }运行结果 注意: pjp.proceed()方法是有两个构造方法分别是: 调用无参数的proceed当原始方法有参数会在调用的过程中自动传入参数 所以调用这两个方法的任意一个都可以完成功能 但是当需要修改原始方法的参数时就只能采用带有参数的方法, 如下: Component Aspect public class MyAdvice {Pointcut(execution(* com.baidu.dao.BookDao.findName(..)))private void pt(){}Around(pt())public Object around(ProceedingJoinPoint pjp) throws Throwable{Object[] args pjp.getArgs();System.out.println(Arrays.toString(args));args[0] 666;Object ret pjp.proceed(args);return ret;}//其他的略 }有了这个特性后我们就可以在环绕通知中对原始方法的参数进行拦截过滤避免由于参数的问题导致程序无法正确运行保证代码的健壮性。 例如百度网盘在获取提取码时会将传入的参数去掉一些空格之后再将其传入 5.4.3.2 获取返回值 对于返回值只有返回后AfterReturing和环绕Around这两个通知类型可以获取 返回后通知获取返回值 Component Aspect public class MyAdvice {Pointcut(execution(* com.baidu.dao.BookDao.findName(..)))private void pt(){}AfterReturning(value pt(),returning ret)public void afterReturning(Object ret) {System.out.println(afterReturning advice ...ret);}//其他的略 }注意 : (1) 参数名的问题 (2) afterReturning方法参数类型的问题 参数类型可以写成String但是为了能匹配更多的参数类型建议写成Object类型 (3) afterReturning方法参数的顺序问题 运行App后查看运行结果说明返回值已经被获取到 环绕通知获取返回值 Component Aspect public class MyAdvice {Pointcut(execution(* com.baidu.dao.BookDao.findName(..)))private void pt(){}Around(pt())public Object around(ProceedingJoinPoint pjp) throws Throwable{Object[] args pjp.getArgs();System.out.println(Arrays.toString(args));args[0] 666;Object ret pjp.proceed(args);return ret;}//其他的略 }上述代码中ret就是方法的返回值我们是可以直接获取不但可以获取如果需要还可以进行修改。 5.4.3.3 获取异常 对于获取抛出的异常只有抛出异常后AfterThrowing和环绕Around这两个通知类型可以获取 环绕通知获取异常 在catch方法中就可以获取到异常至于获取到异常以后该如何处理和业务需求有关 Component Aspect public class MyAdvice {Pointcut(execution(* com.baidu.dao.BookDao.findName(..)))private void pt(){}Around(pt())public Object around(ProceedingJoinPoint pjp){Object[] args pjp.getArgs();System.out.println(Arrays.toString(args));args[0] 666;Object ret null;try{ret pjp.proceed(args);}catch(Throwable throwable){t.printStackTrace();}return ret;}//其他的略 }抛出异常后通知获取异常 Component Aspect public class MyAdvice {Pointcut(execution(* com.baidu.dao.BookDao.findName(..)))private void pt(){}AfterThrowing(value pt(),throwing t)public void afterThrowing(Throwable t) {System.out.println(afterThrowing advice ...t);}//其他的略 }注意 运行结果 5.5 AOP总结 AOP的知识就已经讲解完了接下来对于AOP的知识进行一个总结: 5.5.1 AOP的核心概念 概念AOP(Aspect Oriented Programming)面向切面编程一种编程范式作用在不惊动原始设计的基础上为方法进行功能增强核心概念 代理ProxySpringAOP的核心本质是采用代理模式实现的连接点JoinPoint在SpringAOP中理解为任意方法的执行切入点Pointcut匹配连接点的式子切入点一定是连接点通知Advice若干个方法的共性功能在切入点处执行最终体现为一个方法切面Aspect描述通知与切入点的对应关系目标对象Target被代理的原始对象成为目标对象 5.5.2 切入点表达式 切入点表达式标准格式动作关键字(访问修饰符 返回值 包名.类/接口名.方法名参数异常名) execution(* com.baidu.service.*Service.*(..))切入点表达式描述通配符 作用用于快速描述范围描述*匹配任意符号常用.. 匹配多个连续的任意符号常用匹配子类类型 切入点表达式书写技巧 按标准规范开发 查询操作的返回值建议使用*匹配 减少使用…的形式描述包 对接口进行描述使用*表示模块名例如UserService的匹配描述为*Service 方法名书写保留动词例如get使用*表示名词例如getById匹配描述为getBy* 参数根据实际情况灵活调整 5.5.3 五种通知类型 前置通知后置通知环绕通知重点 环绕通知依赖形参ProceedingJoinPoint才能实现对原始方法的调用环绕通知可以隔离原始方法的调用执行即不使用ProceedingJoinPoint进行对原始方法的调用环绕通知返回值设置为Object类型环绕通知中可以对原始方法调用过程中出现的异常进行处理 返回后通知抛出异常后通知 5.5.4 通知中获取参数 获取切入点方法的参数所有的通知类型都可以获取参数 JoinPoint适用于前置、后置、返回后、抛出异常后通知ProceedingJoinPoint适用于环绕通知 获取切入点方法返回值前置和抛出异常后通知是没有返回值后置通知可有可无所以不做研究 返回后通知环绕通知 获取切入点方法运行异常信息前置和返回后通知是不会有后置通知可有可无所以不做研究 抛出异常后通知环绕通知 5.6 AOP事务管理 5.6.1 Spring事务简介 事务作用在数据层保障一系列的数据库操作同成功同失败 Spring事务作用在**数据层或业务层保障一系列的数据库操作同成功同失败** 数据层有事务我们可以理解为什么业务层也需要处理事务呢? 因为会出现这种情况**业务层中有事务事务是在数据层中不同事务对应着各自的数据操作要保障一系列的数据库操作同成功同失败最终也就是要保障业务层一系列的数据库操作同成功同失败** Spring为了管理事务提供了一个平台事务管理器PlatformTransactionManager commit是用来提交事务rollback是用来回滚事务。 PlatformTransactionManager只是一个接口Spring还为其提供了一个具体的实现: 从名称上可以看出我们只需要给它一个DataSource对象它就可以帮你去在业务层管理事务。 其内部采用的是JDBC的事务。 所以说如果你持久层采用的是JDBC相关的技术就可以采用这个事务管理器来管理你的事务。 而Mybatis内部采用的就是JDBC的事务所以后期我们Spring整合Mybatis就采用的这个DataSourceTransactionManager事务管理器。 持久层Persistence Layer是指应用程序中负责处理数据持久化即数据在应用程序和数据库之间的存储和检索的部分 即常见的Dao层 5.6.1.1 转账案例 需求: 实现任意两个账户间转账操作 需求微缩: A账户减钱B账户加钱 实现步骤 ①数据层提供基础操作指定账户减钱outMoney指定账户加钱inMoney ②业务层提供转账操作transfer调用减钱与加钱的操作 ③提供2个账号和操作金额执行转账操作 ④基于Spring整合MyBatis环境搭建上述操作 public interface AccountDao {Update(update tbl_account set money money #{money} where name #{name})void inMoney(Param(name) String name, Param(money) Double money);Update(update tbl_account set money money - #{money} where name #{name})void outMoney(Param(name) String name, Param(money) Double money); }正常情况程序正常执行时账户金额A减B加。 引入如果在转账的过程中出现了异常如 Service public class AccountServiceImpl implements AccountService {Autowiredprivate AccountDao accountDao;public void transfer(String out,String in ,Double money) {accountDao.outMoney(out,money);int i 1/0;accountDao.inMoney(in,money);}}出现的问题金额减少之后并没有相关账户金额的增加如果交换执行顺序相关账户金额的增加但没有账户金额减少 程序出现异常后转账失败但是异常之前操作成功异常之后操作失败整体业务失败 当程序出问题后我们需要让事务进行回滚而且这个事务应该是加在业务层上而Spring的事务管理就是用来解决这类问题的。 Spring事务管理具体的实现步骤为: 步骤1:在需要被事务管理的方法上添加注解 Transactional public interface AccountService {/*** 转账操作* param out 传出方* param in 转入方* param money 金额*///配置当前接口方法具有事务public void transfer(String out,String in ,Double money) ; }Service public class AccountServiceImpl implements AccountService {Autowiredprivate AccountDao accountDao;Transactionalpublic void transfer(String out,String in ,Double money) {accountDao.outMoney(out,money);int i 1/0;accountDao.inMoney(in,money);}}注意: Transactional可以写在接口类上、接口方法上、实现类上和实现类方法上 写在接口类上该接口的**所有实现类的所有方法**都会有事务写在接口方法上该接口的**所有实现类的该方法**都会有事务写在实现类上该**类中的所有方法**都会有事务写在实现类方法上该方法上有事务建议写在实现类或实现类的方法上 步骤2:在JdbcConfig类中配置事务管理器 并注入Spring容器中Bean 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;Beanpublic DataSource dataSource(){DruidDataSource ds new DruidDataSource();ds.setDriverClassName(driver);ds.setUrl(url);ds.setUsername(userName);ds.setPassword(password);return ds;}//配置事务管理器mybatis使用的是jdbc事务Beanpublic PlatformTransactionManager transactionManager(DataSource dataSource){DataSourceTransactionManager transactionManager new DataSourceTransactionManager();transactionManager.setDataSource(dataSource);return transactionManager;} }注意事务管理器要根据使用技术进行选择Mybatis框架使用的是JDBC事务可以直接使用DataSourceTransactionManager 步骤3开启事务注解 EnableTransactionManagement 在SpringConfig的配置类中开启 Configuration ComponentScan(com.baidu) PropertySource(classpath:jdbc.properties) Import({JdbcConfig.class,MybatisConfig.class //开启注解式事务驱动 EnableTransactionManagement public class SpringConfig { } 步骤4:运行测试类 在测试类中测试 RunWith(SpringJUnit4ClassRunner.class) ContextConfiguration(classes SpringConfig.class) public class AccountServiceTest {Autowiredprivate AccountService accountService;Testpublic void testTransfer() throws IOException {accountService.transfer(Tom,Jerry,100D);}}会发现在转换的业务出现错误后事务就可以控制回滚保证数据的正确性。 知识点1EnableTransactionManagement 名称EnableTransactionManagement类型配置类注解位置配置类定义上方作用设置当前Spring环境中开启注解式事务支持 知识点2Transactional 名称Transactional类型接口注解 类注解 方法注解位置业务层接口上方 业务层实现类上方 业务方法上方作用为当前业务层方法添加事务如果设置在类或接口上方则类或接口中所有方法均添加事务 5.6.2 Spring事务角色 两个角色分别是事务管理员和事务协调员 事务管理员发起事务方在Spring中通常指代业务层开启事务的方法事务协调员加入事务方在Spring中通常指代数据层方法也可以是业务层方法 5.6.2.1 未开启Spring事务之前 未开启Spring事务之前也就是如果在转账的过程中出现了异常会导致数据不一致时: AccountDao的outMoney因为是修改操作会开启一个事务T1AccountDao的inMoney因为是修改操作会开启一个事务T2AccountService的transfer没有事务 运行过程中如果没有抛出异常则T1和T2都正常提交数据正确如果在两个方法中间抛出异常T1因为执行成功提交事务T2因为抛异常不会被执行就会导致数据出现错误 5.6.2.2 开启了Spring事务之后 transfer上添加了Transactional注解在该方法上就会有一个事务TAccountDao的outMoney方法的**事务T1加入到transfer的事务T中**AccountDao的inMoney方法的**事务T2加入到transfer的事务T中**这样就保证他们在同一个事务中当业务层中出现异常整个事务就会回滚保证数据的准确性。 注意 目前的事务管理是基于DataSourceTransactionManager用于管理数据库事务和SqlSessionFactoryBean配置和创建 MyBatis 的 SqlSessionFactory 的工厂Bean使用的是同一个数据源。 回顾数据源在Spring中的创建过程 Bean public DataSource dataSource(){DruidDataSource ds new DruidDataSource();ds.setDriverClassName(driver);ds.setUrl(url);ds.setUsername(userName);ds.setPassword(password);return ds; }5.6.3 Spring事务属性 事务的管理员和事务的协同员这两个概念具体做什么除了这两个概念事务的其他相关配置都有哪些 5.6.3.1 事务配置 上面这些属性都可以在Transactional注解的参数上进行设置。 readOnlytrue只读事务false读写事务增删改要设为false, 查询设为true。 timeout: 设置超时时间单位秒在多长时间之内事务没有提交成功就自动回滚-1表示不设置超时时间。 rollbackFor: 当出现指定异常进行事务回滚即也侧面说明了并不是所有的异常都会回滚事务 Spring的事务只会对Error异常和RuntimeException异常及其子类进行事务回顾其他的异常类型是不会回滚的如IOException不符合条件不会回滚 Service public class AccountServiceImpl implements AccountService {Autowiredprivate AccountDao accountDao;Transactionalpublic void transfer(String out,String in ,Double money) throws IOException{accountDao.outMoney(out,money);//int i 1/0; //这个异常事务会回滚if(true){throw new IOException(); //这个异常事务就不会回滚}accountDao.inMoney(in,money);} }运行上面的事务会发现虽然抛出了错误但是数据库的数据仍然发生了变化 此时就可以使用rollbackFor属性来设置出现IOException异常产生回滚操作 Service public class AccountServiceImpl implements AccountService {Autowiredprivate AccountDao accountDao;Transactional(rollbackFor {IOException.class})public void transfer(String out,String in ,Double money) throws IOException{accountDao.outMoney(out,money);//int i 1/0; //这个异常事务会回滚if(true){throw new IOException(); //这个异常事务就不会回滚}accountDao.inMoney(in,money);} }结果为抛出了IOException的异常并且数据库的数据没有发生变化 noRollbackFor:当出现指定异常不进行事务回滚 rollbackForClassName等同于rollbackFor,只不过属性为异常的类全名字符串 noRollbackForClassName等同于noRollbackFor只不过属性为异常的类全名字符串 isolation设置事务的隔离级别一个事务的执行不受其他事务的影响程度 DEFAULT: 默认隔离级别, 会采用数据库的隔离级别 READ_UNCOMMITTED : 读未提交允许一个事务读取另一个事务未提交的数据 READ_COMMITTED : 读已提交保证一个事务不会读取到另一个事务未提交的数据 REPEATABLE_READ : 重复读取保证一个事务在执行期间多次读取相同的数据时会得到相同的结果 SERIALIZABLE: 串行化 串行化 最高的隔离级别确保事务之间的完全隔离。避免了脏读、不可重复读和幻读但可能导致性能下降因为事务需要等待锁的释放。 5.6.3.2 事务传播行为 需求引入 在前面的转案例的基础上添加新的需求完成转账后记录日志。 需求实现任意两个账户间转账操作并对每次转账操作在数据库进行留痕需求微缩A账户减钱B账户加钱数据库记录日志无论转账操作是否成功均进行转账操作的日志留痕 分析 在业务层转账操作transfer调用减钱、加钱与记录日志功能 环境准备 步骤1: 创建日志表 create table tbl_log(id int primary key auto_increment,info varchar(255),createDate datetime )步骤2: 添加LogDao接口 now() 是一个数据库函数用于获取当前的日期和时间 public interface LogDao {Insert(insert into tbl_log (info,createDate) values(#{info},now()))void log(String info); }步骤3: 添加LogService接口与实现类 public interface LogService {void log(String out, String in, Double money); } Service public class LogServiceImpl implements LogService {Autowiredprivate LogDao logDao;Transactionalpublic void log(String out,String in,Double money ) {logDao.log(转账操作由out到in,金额money);} }步骤4: 在转账的业务中添加记录日志 public interface AccountService {/*** 转账操作* param out 传出方* param in 转入方* param money 金额*///配置当前接口方法具有事务public void transfer(String out,String in ,Double money)throws IOException ; } Service public class AccountServiceImpl implements AccountService {Autowiredprivate AccountDao accountDao;Autowiredprivate LogService logService;Transactionalpublic void transfer(String out,String in ,Double money) {try{accountDao.outMoney(out,money);accountDao.inMoney(in,money);}finally {logService.log(out,in,money);}} }步骤5: 运行程序发现问题 当转账业务之间出现异常(int i 1/0),转账失败tbl_account成功回滚但是tbl_log表未添加数据失败原因:日志的记录与转账操作隶属同一个事务同成功同失败 log方法、inMoney方法和outMoney方法都属于增删改分别有事务T1,T2,T3transfer因为加了Transactional注解也开启了事务T前面我们讲过Spring事务会把T1,T2,T3都加入到事务T中所以当转账失败后所有的事务都回滚导致日志没有记录下来这和我们的需求不符我们希望log方法单独是一个事务 5.6.3.2.1 事务传播行为 事务传播行为事务协调员对事务管理员所携带事务的处理态度。需要用到之前我们没有说的propagation属性。 代码修改 Service public class LogServiceImpl implements LogService {Autowiredprivate LogDao logDao;//propagation设置事务属性传播行为设置为当前操作需要新事务Transactional(propagation Propagation.REQUIRES_NEW)public void log(String out,String in,Double money ) {logDao.log(转账操作由out到in,金额money);} }运行后就能实现我们想要的结果不管转账是否成功都会记录日志。 5.6.3.2.2 事务传播行为属性说明 对于我们开发实际中使用的话因为默认值需要事务是常态的。 根据开发过程选择其他的就可以了例如案例中需要新事务就需要手工配置。 其实入账和出账操作上也有事务采用的就是默认值。 获取当前的日期和时间** public interface LogDao {Insert(insert into tbl_log (info,createDate) values(#{info},now()))void log(String info); }步骤3: 添加LogService接口与实现类 public interface LogService {void log(String out, String in, Double money); } Service public class LogServiceImpl implements LogService {Autowiredprivate LogDao logDao;Transactionalpublic void log(String out,String in,Double money ) {logDao.log(转账操作由out到in,金额money);} }步骤4: 在转账的业务中添加记录日志 public interface AccountService {/*** 转账操作* param out 传出方* param in 转入方* param money 金额*///配置当前接口方法具有事务public void transfer(String out,String in ,Double money)throws IOException ; } Service public class AccountServiceImpl implements AccountService {Autowiredprivate AccountDao accountDao;Autowiredprivate LogService logService;Transactionalpublic void transfer(String out,String in ,Double money) {try{accountDao.outMoney(out,money);accountDao.inMoney(in,money);}finally {logService.log(out,in,money);}} }步骤5: 运行程序发现问题 当转账业务之间出现异常(int i 1/0),转账失败tbl_account成功回滚但是tbl_log表未添加数据失败原因:日志的记录与转账操作隶属同一个事务同成功同失败 log方法、inMoney方法和outMoney方法都属于增删改分别有事务T1,T2,T3transfer因为加了Transactional注解也开启了事务T前面我们讲过Spring事务会把T1,T2,T3都加入到事务T中所以当转账失败后所有的事务都回滚导致日志没有记录下来这和我们的需求不符我们希望log方法单独是一个事务 5.6.3.2.1 事务传播行为 事务传播行为事务协调员对事务管理员所携带事务的处理态度。需要用到之前我们没有说的propagation属性。 代码修改 Service public class LogServiceImpl implements LogService {Autowiredprivate LogDao logDao;//propagation设置事务属性传播行为设置为当前操作需要新事务Transactional(propagation Propagation.REQUIRES_NEW)public void log(String out,String in,Double money ) {logDao.log(转账操作由out到in,金额money);} }运行后就能实现我们想要的结果不管转账是否成功都会记录日志。 [外链图片转存中…(img-wVRbTG7l-1703410601946)] 5.6.3.2.2 事务传播行为属性说明 对于我们开发实际中使用的话因为默认值需要事务是常态的。 根据开发过程选择其他的就可以了例如案例中需要新事务就需要手工配置。 其实入账和出账操作上也有事务采用的就是默认值。
http://www.zqtcl.cn/news/617601/

相关文章:

  • 嘉兴做网站的公司网红营销价值
  • scala做网站广州化妆品网站制作
  • 网站建设小组五类成员在线购物网站功能模块
  • 网站建设开发详细步骤流程图网站建设与管理实训报告总结
  • 网站设计的素材旅游网站建设标书
  • 做网站还得备案大企业网站建设多少钱
  • 一般做网站空间大概多少钱电商网站开发公司
  • 海报模板在线制作免费网站如何建设个人网站
  • 网站集群建设的意义如何优化推广网站
  • 怎么给公司做免费网站服装品牌网页设计图片
  • 中国通信建设协会网站新手建网站教程
  • 做网站页面的需要哪些技巧wordpress 网址导航
  • 如何做美食网站设计广州网页设计招聘
  • 中国商标网商标查询官方网站页面模板怎么添加文章
  • 建设基础化学网站的经验如何建设网站pdf下载
  • 外贸公司网站设计公司做网站能挣钱不
  • 免费网站ppt模板下载济南建设网站公司
  • 网站建设技术托管免费空间域名注册免备案
  • 威海住房建设部官方网站专科网站开发就业方向
  • 做外贸网站多少钱成都网页设计专业
  • 北京比较好的网站公司在线医生免费咨询
  • 免费的个人网站怎么做企业网站管理系统软件
  • 枣庄住房和城乡建设局网站如何注册国外域名
  • 满洲里建设局网站网页设计公司的目标客户有哪些
  • 英文书 影印版 网站开发怀化组织部网站
  • 网站建设领域的基本五大策略要学会网站细节
  • dede做英文网站优化cms建站系统哪个好
  • eclipse sdk做网站邯郸技术服务类
  • 汕头网站网站建设西安网约车租车公司哪家好
  • 网站空间域名维护协议网络推广软件平台