提供了展现层 SpringMVC 和持久层 Spring JDBCTemplate 以及业务层事务管理等众多的企业级应用技术
,还能整合开源世界众多著名的第三方框架和类库,逐渐成为使用最多的Java EE 企业应用开源框架。
Spring Framework 基础的框架
Spring Boot 加速开发,提高开发的速度
Spring Cloud 分布式开发
是最基础的项目,是其他项目的根基
Core Container(核心容器)
AOP(面向切面编程)
Aspects(AOP思想实现)
Data Access / Integration
Web:web开发
Test:单元测试与集成测试
核心概念:IOC / DI Ioc核心容器 Bean
要追求程序的低耦合度。
使用对象时, 在程序中不要主动使用 new 产生对象,转为由外部提供对象
IoC(Inversion of Control)控制反转
Spring技术对IoC思想进行了实现
DI(Dependency Injection)依赖注入
目标:充分解耦
最终效果:
在pom文件中导入spring的坐标spring-context对应版本是5.2.1.RELEASE,先要将spring管理的类(接口)写好
类似如下代码:
public interface UserService{
public void save();
}
public static UserServiceImpl implements UserService{
private UserDao userDao = new UserDaoImpl();
public void save(){
userDao.save();
}
}
然后在资源文件夹中创建applicationContext.xml文件,在文件中配置bean,bean标签表示配置bean,id属性表示给bean起名字,class属性表示给bean定义类型
<bean id="userDao" class="com.itheima.dao.impl.UserDaoImpl"/>
<bean id="userService" class="com.itheima.service.impl.UserServiceImpl"/>
创建一个demo文件进行测试,先要获取IoC容器,获取过容器后获取bean,最后调取方法。
package com;
import com.itheima.dao.UserDao;
import com.itheima.service.UserService;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class app01 {
public static void main(String[] args) {
//3.获取IoC容器
ApplicationContext ioc = new ClassPathXmlApplicationContext("applicationContext.xml");
//4.获取bean
/* UserDao userDao = (UserDao) ioc.getBean("userDao");
userDao.save();*/
UserService userService = (UserService) ioc.getBean("userService");
userService.save();
}
}
将ServiceImpl中,new的userDao方法修改;
package com.itheima.service.impl;
import com.itheima.dao.UserDao;
import com.itheima.service.UserService;
public class UserServiceImpl implements UserService {
//5.删除业务层中使用new方法创建的对象
//private UserDao userDao = new UserDaoImpl();
private UserDao userDao;
public void setUserDao(UserDao userDao) {
this.userDao = userDao;
}
public void save() {
System.out.println("Hello service");
userDao.save();
}
}
在applicationContext.xml 文件中修改,配置service和dao之间的关系
<bean id="userService" class="com.itheima.service.impl.UserServiceImpl">
<property name="userDao" ref="userDao">
property>
bean>
当在测试的时候,获取bean时,里面的参数和applicationContext.xml文件中的bean标签中的对应的id不同时,会报错NoSuchBeanDefinitionException No bean named '获取bean时里面的参数' available
错误信息
这时候需要考虑的是
1、要么是自己在获取bean时里面的参数错误
2、要么是在配置文件中name中值的错误
定义bean的别名,可以定义多个,使用逗号(,)分号(;)空格( )分隔
默认创建的bean是的bean被成为单例对象,每次使用bean对象都会重新调用,不用重新创建,可以测试一下,在测试代码中创建两个BookService的bookService然后分别输入,得到两个一样的地址值。
public class app03 {
public static void main(String[] args) {
ApplicationContext ioc = new ClassPathXmlApplicationContext("AapplicationContext.xml");
BookService bookService1 = (BookService) ioc.getBean("BookService");
BookService bookService2 = (BookService) ioc.getBean("BookService");
System.out.println(bookService1);
System.out.println(bookService2);
}
}
这样的单例对象是在配置文件中bean标签里,用scope定义的,默认为singleton,可以改为prototype,这是非单例对象。
继续用上面那个测试代码进行测试会发现生成的地址不同。
默认为bean:
每次使用的时候都可以直接调用。
适合使用单例:
不适合单例:
bean实际上就是对象,创建bean使用构造方法完成。
在Bookdaompl中写constructor构造梦方法,权限修饰符无论是public还是private都能创建出对象,因为反射,里面有参数则没办法找到并创建对象,所以可知,spring创建bean的时候调用的是无参的构造方法。
正确的构造方法:
private BookDaoImpl() {
System.out.println("book dao constructor... ");
}
错误的构造方法:
private BookDaoImpl(int i) {
System.out.println("book dao constructor... ");
}
对于spring的报错信息一般都是从下向上读的,
.NoSuchMethodException: com.itheima.dao.impl.BookDaoImpl.
没有这样的一个方法,无参构造方法
配置
<bean id="bookDao" class="com.itheima.dao.impl.BookDaoImpl"/>
首先需要创建工厂,并在工厂中写创建BookDaoImpl的方法。
public class BookDaoFactory {
public static BookDao getBookDao(){
System.out.println("factory ...");
return new BookDaoImpl();
}
}
配置:
<bean id="BookDao" class="com.itheima.factory.BookDaoFactory" factory-method="getBookDao"/>
用测试代码测试,发现依旧创建了Dao对象。
在工厂的代码中进行修改,将static删除,变为非静态的方法。
然后在测试代码中,先要创建实例工厂对象,使用工厂的对象调用getUserDao方法,最后调用save()方法。
//创建实例化对象
BookDaoFactory bookDaoFactory = new BookDaoFactory();
//通过实例工厂对象创建对象
BookDao bookDao = bookDaoFactory.getBookDao();
bookDao.save();
在配置中想要创建实例化bean需要先创建出工厂的对象,即先配置工厂的bean
<bean id="bookFactory" class="com.itheima.factory.BookDaoFactory" />
<bean id="BookDao" factory-method="getBookDao" factory-bean="bookFactory"/>
进行测试发现能成功创建工厂对象和Dao对象。
在工厂中创建一个bean,实现的FactoryBean接口,这是一个泛型,所以需要在<>中写入需要创建的对象。
然后实现接口中所有方法,代码如下:
public class BookDaoFactoryBean implements FactoryBean<BookDao> {
//代替原始实例工厂中创建对象的方法
public BookDao getObject() throws Exception {
return new BookDaoImpl();
}
//
public Class<?> getObjectType() {
return BookDao.class;
}
}
在配置中:
<bean id="BookDao" class="com.itheima.factory.BookDaoFactoryBean"/>
继续测试发现依旧能创建出对象。
这样创建的对象是单例的,想要变为非单例的,需要在FactoryBean中加上isSingleton的方法,返回true是单例的,返回false则是非单例的。这里改bean的scope一样能起到这种作用,会以isSingleton为主。
在BookDaoImpl中写初始化代码和销毁代码:
//表示bean初始化对应的操作
public void init(){
System.out.println("dao init..");
}
//表示destory销毁对应的操作
public void destory(){
System.out.println("dao destory...");
}
写好方法以后需要在配置中进行添加:
<bean id="bookDao" class="com.itheima.dao.impl.BookDaoImpl " init-method="init" destroy-method="destory"/>
<bean id="BookService" class="com.itheima.service.impl.BookServiceImpl" scope="singleton">
<property name="BookDao" ref="bookDao">
property>
bean>
进行测试后会发现销毁操作并没有执行,我们的java文件是用虚拟机运行的,初始化创建的时候会执行初始化方法,但是虚拟机运行完退出的时候没有进行销毁的操作。
销毁的方式:
1、在虚拟机退出前将容器手动关闭,ApplicationContext接口不具备close()方法,但是在里面的ClassPathXmlApplicationContext接口有close()方法,所以需要将代码改为:
//3.获取IoC容器
ClassPathXmlApplicationContext ioc = new ClassPathXmlApplicationContext("applicationContext.xml");
//4.获取bean
UserDao userDao = (UserDao) ioc.getBean("userDao");
userDao.save();
ioc.close();
2、设计钩子,是在虚拟机退出之前将所有的容器关闭才退出。
ClassPathXmlApplicationContext ioc = new ClassPathXmlApplicationContext("applicationContext.xml");
//注册关闭钩子
ioc.registerShutdownHook();
UserDao userDao = (UserDao) ioc.getBean("userDao");
userDao.save();
在实现接口的类中实现spring提供的接口,InitializingBean和DisposableBean
public class BookServiceImpl implements BookService,InitializingBean,DisposableBean{
public void save() {
System.out.println("bookService save..");
bookDao.save();
userDao.save();
}
public void destroy() throws Exception {
System.out.println("service destory...");
}
public void afterPropertiesSet() throws Exception {
System.out.println("service init...");
}
}
这种方式就不需要在配置文件中写初始化和销毁属性;
依赖注入方式:
setter注入—引用类型
public class BookServiceImpl implements BookService{
private BookDao bookDao;
private UserDao userDao;
public void setUserDao(UserDao userDao) {
this.userDao = userDao;
}
public void setBookDao(BookDao bookDao) {
this.bookDao = bookDao;
}
public void save() {
System.out.println("bookService save..");
bookDao.save();
userDao.save();
}
<bean id="bookDao" class="com.itheima.dao.impl.BookDaoImpl"/>
<bean id="BookService" class="com.itheima.service.impl.BookServiceImpl">
<property name="bookDao" ref="bookDao"/>
<property name="userDao" ref="userDao"/>
bean>
setter注入—简单类型
public class BookDaoImpl implements BookDao{
private int connectNum;
private String databaseName;
public void setConnectNum(int connectNum) {
this.connectNum = connectNum;
}
public void setDatabaseNan(String databaseNan) {
this.databaseName = databaseNan;
}
public void save() {
System.out.println("bookDao save..."+connectNum+","+databaseName);
}
}
<bean id="bookDao" class="com.itheima.dao.impl.BookDaoImpl">
<property name="connectNum" value="100"/>
<property name="databaseNan" value="mysql"/>
bean>
因为是构造器注入,所以需要将setter方法删除,直接生成构造方法。
public class BookServiceImpl implements BookService {
private BookDao bookDao;
public BookServiceImpl(BookDao bookDao) {
this.bookDao = bookDao;
}
public void save() {
System.out.println("book Service save..");
bookDao.save();
}
}
在配置文件中的配置(标准书写):因为修改变量的名称也需要在配置文件中修改,耦合度高不符合初衷。
<bean id="bookDao1" class="com.itheima.dao.impl.BookDaoImpl">
<constructor-arg name="connectionNum" value="100"/>
<constructor-arg name="databaseName" value="mysql"/>
bean>
<bean id="userDao" class="com.itheima.dao.impl.UserDaoImpl"/>
<bean id="BookService" class="com.itheima.service.impl.BookServiceImpl">
<constructor-arg name="bookDao" ref="bookDao1"/>
<constructor-arg name="userDao" ref="userDao"/>
bean>
可以将配置文件中的变量名字删除,采用type的格式来写。
<bean id="bookDao1" class="com.itheima.dao.impl.BookDaoImpl">
<constructor-arg type="int" value="100"/>
<constructor-arg type="java.lang.String" value="mysql"/>
bean>
这样解决了形参名称改变的问题,但是当如果出现相同的类型就无法用这个方法。
可以利用构造器中定义的顺序的下标来表示,index=0,则是第一个变量
<bean id="bookDao1" class="com.itheima.dao.impl.BookDaoImpl">
<constructor-arg index="1" value="100"/>
<constructor-arg index="0" value="mysql"/>
bean>
这样解决了参数类型重复的问题,使用位置来解决参数匹配。
IoC容器根据bean所依赖的资源在容器中自动查找并注入到bean中的过程称为自动装配
自动装配的方式
在bookService中的bean里添加autowire属性,里面的值为byTpe,这是自动装配,
<bean id="bookService" class="com.itheima.service.impl.BookServiceImpl" autowire="byType"/>
但是进行测试会出错,说明没自动装配成功。如果bookDao创建两个bean,满足dao接口的有两个bean,自动装配已经无法完成。
当autuwire属性的值为byName,bookDao的id不符合时,会报空指针的错误。NullPointerException
一般使用按类型,配置为:
<bean id="bookDao" class="com.itheima.dao.impl.BookDaoImpl"/>
<bean id="bookService" class="com.itheima.service.impl.BookServiceImpl" autowire="byType"/>
测试发现可以正常输出。
依赖自动装配特征
在DaoImpl里面定义各种类型的集合,并在使用setter方法,最后在配置中赋值。
<bean id="bookDao" class="com.itheima.dao.impl.BookDaoImpl">
<property name="array">
<array>
<value>100value>
<value>200value>
<value>300value>
array>
property>
<property name="list">
<list>
<value>zpdvalue>
<value>lloovalue>
<value>sjyvalue>
list>
property>
<property name="set">
<set>
<value>qfvalue>
<value>pxvalue>
<value>itheimavalue>
<value>itheimavalue>
set>
property>
<property name="map">
<map>
<entry key="001" value="zpd"/>
<entry key="002" value="sjy"/>
map>
property>
<property name="properties">
<props>
<prop key="country">henanprop>
<prop key="province">henanprop>
props>
property>
bean>
如果是引用类型的话使用
<ref bean="beanId"/>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>druidartifactId>
<version>1.1.16version>
dependency>
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="driverClassName" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/spring_db"/>
<property name="username" value="root"/>
<property name="password" value="root"/>
bean>
c3p0连接池的使用:
1、导入坐标
<dependency>
<groupId>c3p0groupId>
<artifactId>c3p0artifactId>
<version>0.9.1.2version>
dependency>
2、配置数据源对象作为spring管理的bean
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
<property name="driverClass" value="com.mysql.jdbc.Driver"/>
<property name="jdbcUrl" value="jdbc:mysql://localhost:3306/spring_db"/>
<property name="user" value="root"/>
<property name="password" value="root"/>
bean>
3、测试发现有异常显示com.mysql.jdbc.Driver未找到,导入mysql
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<version>5.1.6version>
dependency>
1、开启context命名空间,
<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">
beans>
2、使用context空间加载properties文件
<context:property-placeholder location="jdbc.properties"/>
3、使用属性占位符${ },读取properties文件中的属性
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
<property name="driverClass" value="${jdbc.driver}"/>
<property name="jdbcUrl" value="${jdbc.url}"/>
<property name="user" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
bean>
注:在jdbc:properties中添加 username属性
输出后会出现itcast
因为内部有username属性,想要输出的话需要在加载文件的时候,添加system-properties-mode=“NEVER”
这样再输出后就会出现username的属性值。
当想加载多个properties文件时,可以用逗号隔开。也可以在里面写***.properties**,代表所有的properties文件都会被加载。但是最标准的书写方式是在前面加classpath:,加载的是本工程的文件,不会含有jar包里面的。*classpath*代表可以从jar包中读取。
<context:property-placeholder location="jdbc.properties" system-properties-mode="NEVER"/>
<context:property-placeholder location="jdbc.properties,jdbc2.properties"/>
<context:property-placeholder location="*.properties"/>
<context:property-placeholder location="classpath:*.properties"/>
<context:property-placeholder location="classpath*:*.properties"/>
ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
ApplicationContext ctx1 = new FileSystemXmlApplicationContext("D:\\大学\\前后端\\后\\中级\\spring\\manage\\src\\main\\resources\\applicationContext.xml");
加载多个配置文件
ApplicationContext ctx = new ClassPathXmlApplicationContext( "bean1.xml","bean2.xml");
使用bean名称获取
BookDao bookDao = ctx.getBean("bookDao",BookDao.class);
使用bean名称获取并指定类型
BookDao bookDao = (BookDao) ctx.getBean("BookDao");
使用bean类型获取
BookDao bookDao = (BookDao) ctx.getBean(BookDao.class);
BeanFactory是延迟加载bean,ApplicationContext是立即加载bean,想要让ApplicationContext延迟加载则需要在bean中添加lazy-init属性
<bean name="bookDao" class="com.zpd.dao.impl.BookDaoImpl" lazy-init="true"/>
Resource resources = new ClassPathResource("applicationContext.xml");
BeanFactory bf= new XmlBeanFactory(resources);
BookDao dao = bf.getBean(BookDao.class);
dao.save();
在一个原始模块中进行修改,BookDaoImpl中只有sava()方法,BookServiceImpl中调用了BookDao,使用了setter方法,还有一个save()方法,测试代码中只打印bookDao。
@Component 是通用的,如果有表现层,数据层,业务层的开发
@Controller 表现层
@Service 业务层
@Reposotory 数据层
需要配合组件扫描才可以读取
在配置中将bookDao的bean改造成注解开发,配置的是BookDao所以在实现类中添加注解,
@Component("bookDao")
public class BookDaoImpl implements BookDao {
}
@Component
public class BookServiceImpl implements BookService {
}
<context:component-scan base-package="com.zpd"/>
进行测试:
public class app {
public static void main(String[] args) {
ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
BookDao bookDao = (BookDao) ctx.getBean("bookDao");
System.out.println(bookDao);
BookService bookService = (BookService) ctx.getBean(BookService.class);
System.out.println(bookService);
}
}
BookDao是按照名称访问参数是id名,BookService是按类型访参数是class类型。
Spring3.0升级了纯注解开发模式,使用Java类替代配置文件,开启了Spring快速开发赛道
Java类代替Spring核心配置文件,
@Configuration
@ComponentScan("com.zpd")
public class SpringConfig {
}
@ComponentScan({"com.zpd.service","com.zpd.dao"})
//加载配置文件初始化容器
ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
//加载配置类初始化容器
ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
在测试代码中生成两个BookDao类型,都输入打印会发现地址相同,说明生成的是单例。想要生成非单例需要在BookDaoImpl上方添加@Scope注解,里面的内容是singleton就是单例,是prototype就是非单例。
@Scope("singleton")
public class BookDaoImpl implements BookDao {
}
在BookDao中定义初始化和销毁的方法,采用注解的方法,初始化用@PostConstruct,销毁用@PreDestroy注解。
@PostConstruct
public void init(){
System.out.println("bookDao init...");
}
@PreDestroy
public void destroy(){
System.out.println("bookDao destroy...");
}
在测试的时候需要调用close方法,或者新建钩子。
调用close方法时,将类型改为AnnotationConfigApplicationContext
public class AppForAnnotation {
public static void main(String[] args) {
//加载配置类初始化容器
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
BookDao bookDao1 = (BookDao) ctx.getBean("bookDao");
BookDao bookDao2 = (BookDao) ctx.getBean("bookDao");
System.out.println(bookDao1);
System.out.println(bookDao2);
ctx.close();
}
}
在BookService定义的BookDao上方加一个@Autowired注解即可,甚至可以不用写setter方法,因为这里用的是暴力反射方法。
@Autowired
private BookDao bookDao;
并且是按类型装配,但是如果有两个相同类型的文件,则需要为两个不同文件区分,在BookServiceImpl中使用@Qualifier区分。
//BookDaoImpl1文件
@Repository("bookDao1")
public class BookDaoImpl implements BookDao {
public void save(){
System.out.println("book dao save...1");
}
}
//BookDaoImpl2文件
@Repository("bookDao2")
public class BookDaoImpl2 implements BookDao {
public void save(){
System.out.println("book dao save...1");
}
}
使用@Qualifier注解区分。
@Autowired
@Qualifier("bookDao")
private BookDao bookDao;
注意: @Qualifier注解无法单独使用,必须配合@Autowired注解使用
使用@Value实现简单类型注入
@Value("zpd")
private String name;
@Value("20")
private int age;
从properties文件中取值:
先在jdbc.properties中对应的属性和值
jdbc.username=root
jdbc.password=root
@Configuration
@ComponentScan("com.zpd")
@PropertySource("jdbc.properties")
public class SpringConfig {
}
在调用的时候应该使用${ }符号
@Value("${jdbc.username}")
private String name;
@Value("${jdbc.password}")
private String pass;
第三方bean管理
在SpringConfig配置类中写,需要在pom文件中导入druid依赖。
在配置文件中写:
@Configuration
public class SpringConfig {
//1.定义一个方法获得要管理的对象
//2.添加@bean,表示当前方法的返回值是一个bean
@Bean("dataSource")
public DataSource dataSource(){
DruidDataSource ds = new DruidDataSource();
ds.setDriverClassName("com.mysql.jdbc.Driver");
ds.setUrl("jdbc:mysql://127.0.0.1:3306/spring_db");
ds.setUsername("root");
ds.setPassword("root");
return ds;
}
}
进行测试,通过获取类型进行测试
//加载配置类初始化容器
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
DataSource dataSource = ctx.getBean(DataSource.class);
System.out.println(dataSource);
优化:
创建一个jdbcConfig,将获得要管理的对象写入jdbc配置类中:
public class jdbcConfig {
//1.定义一个方法获得要管理的对象
//2.添加@bean,表示当前方法的返回值是一个bean
@Bean("dataSource")
public DataSource dataSource(){
DruidDataSource ds = new DruidDataSource();
ds.setDriverClassName("com.mysql.jdbc.Driver");
ds.setUrl("jdbc:mysql://127.0.0.1:3306/spring_db");
ds.setUsername("root");
ds.setPassword("root");
return ds;
}
}
@Configuration
@Import(jdbcConfig.class)
public class SpringConfig {
}
第三方bean依赖注入
导入BookDao,在配置类中添加@ComponentScan标签,里面写BookDao目录
@ComponentScan("com.zpd.dao")
在jdbcConfig文件中的dataSource方法中添加BookDao参数
@Bean
public DataSource dataSource(BookDao bookDao){
//自动装配
System.out.println(bookDao);
DruidDataSource ds = new DruidDataSource();
//属性设置
return ds;
}
XML配置与注解配置比较
第一步:在pom中导入坐标
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0modelVersion>
<groupId>com.itheimagroupId>
<artifactId>spring_15_spring_mybatisartifactId>
<version>1.0-SNAPSHOTversion>
<dependencies>
<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>spring-contextartifactId>
<version>5.2.10.RELEASEversion>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>druidartifactId>
<version>1.1.16version>
dependency>
<dependency>
<groupId>org.mybatisgroupId>
<artifactId>mybatisartifactId>
<version>3.5.6version>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<version>5.1.47version>
dependency>
<dependency>
<groupId>org.springframeworkgroupId>
<artifctId>spring-jdbcartifactId>
<version>5.2.10.RELEASEversion>
dependency>
<dependency>
<groupId>org.mybatisgroupId>
<artifactId>mybatis-springartifactId>
<version>1.3.0version>
dependency>
dependencies>
project>
创建Config工具,在里面有jdbcConfig,MyBatisConfig(整合MyBatis),springConfig三个工具类。jdbcConfig和springConfig以前都写过,但是在springConfig里面需要注意的是注释需要添加完全。
@Configuration
@ComponentScan("com.itheima")
@PropertySource("classpath:jdbc.properties")
@Import({jdbcConfig.class,MybatisConfig.class})
public class springConfig {package com.itheima.config;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.mapper.MapperScannerConfigurer;
import org.springframework.context.annotation.Bean;
import javax.sql.DataSource;
/**
* @Description
* @Author zpd
* @Date 2022/9/18
*/
public class MybatisConfig {
@Bean
public SqlSessionFactoryBean sqlSessionFactory(DataSource dataSource){
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setTypeAliasesPackage("com.itheima.domain");
sqlSessionFactoryBean.setDataSource(dataSource);
return sqlSessionFactoryBean;
}
@Bean
public MapperScannerConfigurer mapperScannerConfigurer(){
MapperScannerConfigurer mapperScannerConfigurer = new MapperScannerConfigurer();
mapperScannerConfigurer.setBasePackage("com.itheima.dao");
return mapperScannerConfigurer;
}
}
}
在MyBatisConfig里面写MyBaties创建sqlSessionFactory工厂,然后MyBatiesConfig.xml文件中标签的写入。
package com.itheima.config;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.mapper.MapperScannerConfigurer;
import org.springframework.context.annotation.Bean;
import javax.sql.DataSource;
public class MybatisConfig {
@Bean
//创建sqlSessionFactory工厂
public SqlSessionFactoryBean sqlSessionFactory(DataSource dataSource){
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setTypeAliasesPackage("com.itheima.domain");
sqlSessionFactoryBean.setDataSource(dataSource);
return sqlSessionFactoryBean;
}
@Bean
//原来Mybatis整合的文件现在用spring整合
public MapperScannerConfigurer mapperScannerConfigurer(){
MapperScannerConfigurer mapperScannerConfigurer = new MapperScannerConfigurer();
mapperScannerConfigurer.setBasePackage("com.itheima.dao");
return mapperScannerConfigurer;
}
}
固定格式!!!
1、引入依赖pom.xml
<dependency>
<groupId>junitgroupId>
<artifactId>junitartifactId>
<version>4.12version>
<scope>testscope>
dependency>
<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>spring-testartifactId>
<version>5.2.10.RELEASEversion>
dependency>
2、编写测试类
在test\java下创建一个AccountServiceTest,这个名字任意
//设置类运行器
@RunWith(SpringJUnit4ClassRunner.class)
//设置Spring环境对应的配置类
@ContextConfiguration(classes = {SpringConfiguration.class}) //加载配置类
//@ContextConfiguration(locations={"classpath:applicationContext.xml"})//加载配置文件
public class AccountServiceTest {
//支持自动装配注入bean
@Autowired
private AccountService accountService;
@Test
public void testFindById(){
System.out.println(accountService.findById(1));
}
@Test
public void testFindAll(){
System.out.println(accountService.findAll());
}
}
运行后发现可以正常运行。
需要记
//设置类运行器
@RunWith(SpringJUnit4ClassRunner.class)
//设置Spring环境对应的配置类
@ContextConfiguration(classes = {SpringConfiguration.class}) //加载配置类
AOP是在不改原有代码的前提下对其进行增强。
作用:在不惊动原始设计的基础上为其功能进行增强。
spring理念:无侵入式编程。
1、追加功能的点被称为切入点
2、追加的功能被称为通知
3、通知和切入点的连接被称为切面
思路分析:
1、导入坐标(pom.xml)
Spring-context坐标里面包含有AOP
2、制作连接点方法(原始操作,dao接口及其实现类)
Dao里面的sava方法或者定义的updata方法等;
3、制作共性功能(通知类与通知)
新建一个MyAdvice类用来制作共性功能,新建一个方法来表示共性功能
public void method(){
System.out.println(System.currentTimeMillis());
System.out.println("book dao sava...")
}
4、定义切入点
在上方随便添加一个方法,然后加入切入的注解。
说明︰切入点定义依托一个不具有实际意义的方法进行,即无参数,无返回值,方法体无实际逻辑
@Pointcut("execution(void com.itheima.dao.BookDao.updata())")
private void pt(){}
5、绑定切入点与通知的关系(切面)
在共性功能的方法上添加注解进行绑定。
@Before("pt()")
public void method(){
//内容
}
这样就会在执行方法前执行切入点。
切面制作完毕,但是不受spring控制,所以在类名上方添加注解:
//受spring控制
@Component
//告诉spring扫描到以后按照aop处理而不是按bean处理,去找下面的@Pointcut注解,和执行先后的注解
@Aspect
public class MyAdvice { }
最后在总的springConfig上添加注解:让spring清楚aop开发是注解开发。
@Configuration
@ComponentScan("com.itheima")
//告诉spring这里面有注解开发的aop,去找@Aspect注解
@EnableAspectJAutoProxy
public class SpringConfig {
}
归根到底修改的只有springConfig和新建了一个切面方法、切入点。
1、spring容器启动
2、读取所有切面配置中的切入点
3、初始化bean,判定bean对应的类中的方法是否匹配到切入点
4、 获取bean执行方法
AOP核心概念
描述方式一:执行com.itheima.dao包下的BookDao接口中的无参数update方法
execution(void com.itheima.dao.BookDao.update())
描述方式二︰执行com.itheima.dao.impl包下的BookDaoImpl类中的无参数update方法
execution(void com.itheima.dao.impl.BookDaoImpl.update())
execution:动作关键字,描述切入点的行为动作,例如execution表示执行到指定切入点
public:访问修饰符,还可以是public,private等,可以省略
User:返回值,写返回值类型
com.itheima.service:包名,多级包使用点连接
UserService:类/接口名称
findById:方法名
int:参数,直接写参数的类型,多个类型用逗号隔开
异常名:方法定义中抛出指定异常,可以省略
execution(public * com.itheima.*.UserService.find*(*))
匹配com.itheima包下的任意包中的UserService类或接口中所有find开头的带有一个参数的
方法
execution(public User com..UserService.findById(..))
匹配com包下的任意包中的UserService类或接口中所有名称为findById的方法
execution(* *..*Service+.*(..))
这个使用率较低,描述子类的,咱们做JavaEE开发,继承机会就一次,使用都很慎重,所以很少
用它。*Service+,表示所有以Service结尾的接口的子类。
execution(void com.itheima.dao.BookDao.update())
匹配接口,能匹配到
execution(void com.itheima.dao.impl.BookDaoImpl.update())
匹配实现类,能匹配到
execution(* com.itheima.dao.impl.BookDaoImpl.update())
返回值任意,能匹配到
execution(* com.itheima.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..*())
返回值为void,com包下的任意包任意类任意方法,能匹配,*代表的是方法
execution(* com.itheima.*.*Service.find*(..))
将项目中所有业务层方法的以find开头的方法匹配
execution(* com.itheima.*.*Service.save*(..))
将项目中所有业务层方法的以save开头的方法匹配
后面两种更符合我们平常切入点表达式的编写规则。
所有代码按照标准规范开发,否则以下技巧全部失效
描述切入点通常描述接口,而不描述实现类,如果描述到实现类,就出现紧耦合了
访问控制修饰符针对接口开发均采用public描述(可省略访问控制修饰符描述)
返回值类型对于增删改类使用精准类型加速匹配,对于查询类使用通配快速描述
包名书写==尽量不使用…匹配==,效率过低,常用做单个包描述匹配,或精准匹配
接口名**/类名书写名称与模块相关的采用匹配,例如UserService书写成Service,绑定业务
层接口名
方法名书写以动词进行精准匹配**,名词采用匹配,例如getById书写成getBy,selectAll书写成
selectAll
参数规则较为复杂,根据业务方法灵活调整
通常不使用异常作为匹配规则
@Before
@After
@Around 中间有表示对原始操作的调用
@Around("pt()")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
//调用原始操作
pjp.proceed();
}
需要抛出异常!
原始方法select如果有int返回值:
@Component
@Aspect
public class MyAdvice {
@Pointcut("execution(void com.itheima.dao.BookDao.update())")
private void pt(){}
@Pointcut("execution(int com.itheima.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 ...");
}
}
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);
}
}
运行后会报错,错误内容为:
Exception in thread “main” org.springframework.aop.AopInvocationException:
Null return value from advice does not match primitive return type for:
public abstract int com.itheima.dao.BookDao.select() at
org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopP
roxy.java:226) at com.sun.proxy.$Proxy19.select(Unknown Source) at
com.itheima.App.main(App.java:12)
错误大概的意思是:空的返回不匹配原始方法的int 返回
所以如果我们使用环绕通知的话,要根据原始方法的返回值来设置环绕通知的返回值,具体解决方案
为:
@Component
@Aspect
public class MyAdvice {
@Pointcut("execution(void com.itheima.dao.BookDao.update())")
private void pt(){}
@Pointcut("execution(int com.itheima.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类型更通用。
在环绕通知中是可以对原始方法返回值就行修改的。
@AfterReturning
注意:返回后通知是需要在原始方法select正常执行后才会被执行,如果select()方法执行的过程
中出现了异常,那么返回后通知是不会被执行。后置通知是不管原始方法有没有抛出异常都会被执
行。
@AfterThrowing
注意:异常后通知是需要原始方法抛出异常,可以在select()方法中添加一行代码int i = 1/0即
可。如果没有抛异常,异常后通知将不会被执行。
1、在springConfig配置文件上添加@EnableAspectJAutoProxy 注解
2、创建AOP的通知类
@Component
@Aspect
public class ProjectAdvice {
//配置业务层的所有方法
@Pointcut("execution(* com.itheima.service.*Service.*(..))")
private void servicePt(){}
public void runSpeed(){
}
}
3、添加环绕
在runSpeed()方法上添加@Around
@Component
@Aspect
public class ProjectAdvice {
//配置业务层的所有方法
@Pointcut("execution(* com.itheima.service.*Service.*(..))")
private void servicePt(){}
//@Around("ProjectAdvice.servicePt()") 可以简写为下面的方式
@Around("servicePt()")
public Object runSpeed(ProceedingJoinPoint pjp){
Object ret = pjp.proceed();
return ret;
}
}
注意:目前并没有做任何增强!!!
4、完成核心业务,记录万次执行的时间
@Component
@Aspect
public class ProjectAdvice {
//配置业务层的所有方法
@Pointcut("execution(* com.itheima.service.*Service.*(..))")
private void servicePt(){}
//@Around("ProjectAdvice.servicePt()") 可以简写为下面的方式
@Around("servicePt()")
public void runSpeed(ProceedingJoinPoint pjp){
long start = System.currentTimeMillis();
for (int i = 0; i < 10000; i++) {
pjp.proceed();
} l
ong end = System.currentTimeMillis();
System.out.println("业务层接口万次执行时间: "+(end-start)+"ms");
}
}
5、运行单元测试类
会显示运行10000次的时长。
6、程序优化
目前程序所面临的问题是,多个方法一起执行测试的时候,控制台都打印的是:
业务层接口万次执行时间:xxxms
我们没有办法区分到底是哪个接口的哪个方法执行的具体时间。
@Component
@Aspect
public class ProjectAdvice {
//配置业务层的所有方法
@Pointcut("execution(* com.itheima.service.*Service.*(..))")
private void servicePt(){}
//@Around("ProjectAdvice.servicePt()") 可以简写为下面的方式
@Around("servicePt()")
public void runSpeed(ProceedingJoinPoint pjp){
//获取执行签名信息
Signature signature = pjp.getSignature();
//通过签名获取执行操作名称(接口名)
String className = signature.getDeclaringTypeName();
//通过签名获取执行操作名称(方法名)
String methodName = signature.getName();
long start = System.currentTimeMillis();
for (int i = 0; i < 10000; i++) {
pjp.proceed();
} l
ong end = System.currentTimeMillis();
System.out.println("万次执行:"+ className+"."+methodName+"---->" +
(end-start) + "ms");
}
}
7、运行单元测试
能清楚的看到不管运行的哪种方法,都可以清楚的看到所用的时间。只是一个理论值,不是完整的过程。
通过测试来获取数据:
在方法上添加JoinPoint,通过JoinPoint来获取参数
@Component
@Aspect
public class MyAdvice {
@Pointcut("execution(* com.itheima.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 ..." );
} /
/...其他的略
}
参数的个数是不固定的,所以使用数组更通配些,当将获取的参数改为两个的时候返回的也是数组的格式。
环绕通知获取方式
环绕通知使用的是ProceedingJoinPoint,因为ProceedingJoinPoint是JoinPoint类的子
类,所以对于ProceedingJoinPoint类中应该也会有对应的getArgs()方法。
@Component
@Aspect
public class MyAdvice {
@Pointcut("execution(* com.itheima.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;
} /
/其他的略
}
@AfterReturning
@AfterReturning(value = "pt()",returning = "ret")
public void afterReturning(String ret) {
System.out.println("afterReturning advice ..."+ret);
}
returning 返回的返回值和方法里面的参数必须一致。如果在方法中JoinPoint和他都存在的话都错在,必须的JoinPont在前。
@Around 上面已经说过,甚至可以修改返回值;
public Object around(ProceedingJoinPoint pjp)throws Throwable {
Object ret = pjp.proceed(args);
return ret;
}
对于Around来说调用完就可以得到返回值。
@Around
@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();
} r
eturn ret;
}
try…catch获取异常
@AfterThrowing
@Component
@Aspect
public class MyAdvice {
@Pointcut("execution(* com.itheima.dao.BookDao.findName(..))")
private void pt(){}
@AfterThrowing(value = "pt()",throwing = "t")
//形参接异常的返回对象
public void afterThrowing(Throwable t)
{
System.out.println("afterThrowing advice ..."+t);
} /
/其他的略
}
让原始方法抛出异常:
@Repository
public class BookDaoImpl implements BookDao {
public String findName(int id,String password) {
System.out.println("id:"+id);
if(true){
throw new NullPointerException();
}
return "itcast";
}
}
需求: 对百度网盘分享链接输入密码时尾部多输入的空格做兼容处理。
分析:
1、在业务方法执行之前对所有的输入参数进行格式处理——trim()。
2、使用处理后的参数调用原始方法——环绕通知中存在对原始方法的调用
public interface ResourcesDao {
boolean readResources(String url, String password);
}
@Repository
public class ResourcesDaoImpl implements ResourcesDao {
public boolean readResources(String url, String password) {
//模拟校验
return password.equals("root");
}
}
public interface ResourcesService {
public boolean openURL(String url ,String password);
}
@Service
public class ResourcesServiceImpl implements ResourcesService {
@Autowired
private ResourcesDao resourcesDao;
public boolean openURL(String url, String password) {
return resourcesDao.readResources(url,password);
}
}
@Configuration
@ComponentScan("com.itheima")
public class SpringConfig {
}
public class App {
public static void main(String[] args) {
ApplicationContext ctx = new
AnnotationConfigApplicationContext(SpringConfig.class);
ResourcesService resourcesService = ctx.getBean(ResourcesService.class);
boolean flag = resourcesService.openURL("http://pan.baidu.com/haha", "root");
System.out.println(flag);
}
}
现在项目的效果是,当输入密码为"root"控制台打印为true,如果密码改为"root "控制台打印的是
false
需求是使用AOP将参数进行统一处理,不管输入的密码root前后包含多少个空格,最终控制台打印的
都是true。
实现:
1、开启SpringAop注解功能
@Configuration
@ComponentScan("com.itheima")
@EnableAspectJAutoProxy
public class SpringConfig {
}
2、编写通知类
@Component
@Aspect
public class DataAdvice {
@Pointcut("execution(boolean com.itheima.service.*Service.*(*,*))")
private void servicePt(){}
}
3、添加环绕通知
@Component
@Aspect
public class DataAdvice {
@Pointcut("execution(boolean com.itheima.service.*Service.*(*,*))")
private void servicePt(){}
@Around("DataAdvice.servicePt()")
//@Around("servicePt()")这两种写法都对
public Object trimStr(ProceedingJoinPoint pjp) throws Throwable {
Object ret = pjp.proceed();
return ret;
}
}
4、完成核心业务,处理空格
@Component
@Aspect
public class DataAdvice {
@Pointcut("execution(boolean com.itheima.service.*Service.*(*,*))")
private void servicePt(){}
@Around("DataAdvice.servicePt()")
// @Around("servicePt()")这两种写法都对
public Object trimStr(ProceedingJoinPoint pjp) throws Throwable {
//获取原始方法的参数
Object[] args = pjp.getArgs();
for (int i = 0; i < args.length; i++) {
//判断参数是不是字符串
if(args[i].getClass().equals(String.class)){
args[i] = args[i].toString().trim();
}
}
//将修改后的参数传入到原始方法的执行中
Object ret = pjp.proceed(args);
return ret;
}
}
5、运行程序
发现不管有没有空格都能返回true
概念:AOP(Aspect Oriented Programming)面向切面编程,一种编程范式
作用:在不惊动原始设计的基础上为方法进行功能增强
核心概念
切入点表达式标准格式:动作关键字(访问修饰符 返回值 包名**.类/接口名.**方法名(参数)异常名)
切入点表达式描述通配符
切入点表达式书写技巧
1.按标准规范开发
2.查询操作的返回值建议使用*匹配
3.减少使用**…**的形式描述包
4.对接口进行描述,使用 ***** 表示模块名,例如UserService的匹配描述为 ***** Service
5.方法名书写保留动词,例如get,使用 ***** 表示名词,例如getById匹配描述为getBy *****
6.参数根据实际情况灵活调整
通知类型
获取切入点方法的参数,所有的通知类型都可以获取参数
获取切入点方法返回值,前置和抛出异常后通知是没有返回值,后置通知可有可无,所以不做研究
获取切入点方法运行异常信息,前置和返回后通知是不会有,后置通知可有可无,所以不做研究
接口:
public interface PlatformTransactionManager{
void commit(Transactionstatus status) throws TransactionException;
void rollback(Transactionstatus status) throws TransactionException;
}
实现类:
public class DataSourceTransactionManager{
...
}
需求: 实现任意两个账户间转账操作
需求微缩: A账户减钱,B账户加钱
为了实现上述的业务需求,我们可以按照下面步骤来实现下:
①:数据层提供基础操作,指定账户减钱(outMoney),指定账户加钱(inMoney)
②:业务层提供转账操作(transfer),调用减钱与加钱的操作
③:提供2个账号和操作金额执行转账操作
④:基于Spring整合MyBatis环境搭建上述操作
事务实用开发
1、在业务层接口上添加spring事务管理
public interface AccountService {
@Transactional
public void transfer(String out,String in ,Double money) ;
}
Spring注解式事务通常添加在业务层接口中而不会添加到业务层实现类中,降低耦合
注解式事务可以添加到业务方法上表示当前方法开启事务,也可以添加到接口上表示当前接口所有方法开启事务
2、设置事务管理器
@Bean
public PlatformTransactionManager transactionManager(DataSource dataSource){
DataSourceTransactionManager transactionManager = new DataSourceTransactionManager();
transactionManager.setDataSource(dataSource);
return transactionManager;
}
3、开启注解式事务驱动
//开启注解式事务驱动
@EnableTransactionManagement
public class SpringConfig {
}
事务角色
接口中的transfer(String out,String in ,Double money)
方法
accountDao.outMoney(out,money); //int i = 1/0; accountDao.inMoney(in,money);
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Xleo3hC7-1663986030425)(C:%5CUsers%5Czpd%5CAppData%5CRoaming%5CTypora%5Ctypora-user-images%5Cimage-20220920164113644.png)]
并不是所有的异常都会回滚事务,
@Service
public class AccountServiceImpl implements AccountService {
@Autowired
private AccountDao accountDao;
public void transfer(String out,String in ,Double money) throws IOException {
accountDao.outMoney(out,money);
if (true){
throw new IOException();
}
accountDao.inMoney(in,money);
}
}
这时进行转账测试的话A减少钱,但是B没收到钱
@Transactional(rollbackFor = {IOException.class})
public void transfer(String out,String in ,Double money) throws IOException;
这时进行转账测试的话A不减少钱,B也没收到钱
在前面的转案例的基础上添加新的需求,完成转账后记录日志。
需求:实现任意两个账户间转账操作,并对每次转账操作在数据库进行留痕
需求微缩:A账户减钱,B账户加钱,数据库记录日志
分析:
①:基于转账操作案例添加日志模块,实现数据库中记录日志
②:业务层转账操作(transfer),调用减钱、加钱与记录日志功能
实现预期效果为:
无论转账操作是否成功,均进行转账操作的日志留痕
1、在业务层接口上添加spring事务,设置事务传播行为REQUIRES_NEW(需要新事务)
在事务管理员的方法里,@Transactional注解中有一个事务属性用来做事务传播行为。
@Transactional(propagation = Propagation.REQUIRES_NEW)
void log(String out, String in, Double money);
接口中而不会添加到业务层实现类中,降低耦合
注解式事务可以添加到业务方法上表示当前方法开启事务,也可以添加到接口上表示当前接口所有方法开启事务
2、设置事务管理器
@Bean
public PlatformTransactionManager transactionManager(DataSource dataSource){
DataSourceTransactionManager transactionManager = new DataSourceTransactionManager();
transactionManager.setDataSource(dataSource);
return transactionManager;
}
3、开启注解式事务驱动
//开启注解式事务驱动
@EnableTransactionManagement
public class SpringConfig {
}
事务角色
接口中的transfer(String out,String in ,Double money)
方法
accountDao.outMoney(out,money); //int i = 1/0; accountDao.inMoney(in,money);
[外链图片转存中…(img-Xleo3hC7-1663986030425)]
并不是所有的异常都会回滚事务,
@Service
public class AccountServiceImpl implements AccountService {
@Autowired
private AccountDao accountDao;
public void transfer(String out,String in ,Double money) throws IOException {
accountDao.outMoney(out,money);
if (true){
throw new IOException();
}
accountDao.inMoney(in,money);
}
}
这时进行转账测试的话A减少钱,但是B没收到钱
@Transactional(rollbackFor = {IOException.class})
public void transfer(String out,String in ,Double money) throws IOException;
这时进行转账测试的话A不减少钱,B也没收到钱
在前面的转案例的基础上添加新的需求,完成转账后记录日志。
需求:实现任意两个账户间转账操作,并对每次转账操作在数据库进行留痕
需求微缩:A账户减钱,B账户加钱,数据库记录日志
分析:
①:基于转账操作案例添加日志模块,实现数据库中记录日志
②:业务层转账操作(transfer),调用减钱、加钱与记录日志功能
实现预期效果为:
无论转账操作是否成功,均进行转账操作的日志留痕
1、在业务层接口上添加spring事务,设置事务传播行为REQUIRES_NEW(需要新事务)
在事务管理员的方法里,@Transactional注解中有一个事务属性用来做事务传播行为。
@Transactional(propagation = Propagation.REQUIRES_NEW)
void log(String out, String in, Double money);