AOP、spring事务管理

目录

AOP简介

AOP入门案例

AOP配置管理

AOP通知类型

业务层接口执行效率

AOP通知获取数据

百度网盘密码数据兼容处理

AOP事务管理


AOP简介

什么是AOP?

AOP(Aspect  Oriented  Programming)面向切面编程,一种编程范式,指导开发者如何组织程序结构。
OOP(Object  Oriented  Programming)面向对象编程
AOP是在不改原有代码的前提下对其进行增强。

AOP作用

作用:在不惊动原始设计的基础上为其进行功能增强
spring理念:无入侵式/无侵入式

AOP核心概念

(1)前面一直在强调,Spring的AOP是对一个类的方法在不进行任何修改的前提下实现增强。
对于上面的案例中BookServiceImpl中有save , update , delete 和select 方法,
这些方法起了一个名字叫连接点

(2)在BookServiceImpl的四个方法中,update 和delete 只有打印没有计算万次执行消耗时间,
但是在运行的时候已经有该功能,那也就是说update 和delete 方法都已经被增强,
所以对于需要增强的方法起一个名字叫切入点

(3)执行BookServiceImpl的update和delete方法的时候都被添加了一个计算万次执行消耗时间的功能,
将这个功能抽取到一个方法中,换句话说就是存放共性功能的方法,起了个名字叫通知

(4)通知是要增强的内容,会有多个,切入点是需要被增强的方法,也会有多个,
那哪个切入点需要添加哪个通知,就需要提前将它们之间的关系描述清楚,
那么对于通知和切入点之间的关系描述,我们给起了个名字叫切面

(5)通知是一个方法,方法不能独立存在需要被写在一个类中,这个类我们也给起了个名字叫通知类
连接点(JoinPoint):程序执行过程中的任意位置,粒度为执行方法、抛出异常、设置变量等
    在SpringAOP中,理解为方法的执行
切入点(Pointcut):匹配连接点的式子
    在SpringAOP中,一个切入点可以描述一个具体方法,也可也匹配多个方法
    一个具体的方法:如com.green.dao包下的BookDao接口中的无形参无返回值的save方法
    匹配多个方法:所有的save方法,所有的get开头的方法,所有以Dao结尾的接口中的任意方法,
    所有带有一个参数的方法
通知(Advice):在切入点处执行的操作,也就是共性功能
    在SpringAOP中,功能最终以方法的形式呈现
通知类:定义通知的类
切面(Aspect):描述通知与切入点的对应关系。

 AOP入门案例

需求分析
案例设定:测算接口执行效率
简化设定:在方法执行前输出当前系统时间。
思路分析
1.导入坐标(pom.xml)
2.制作连接点(原始操作,Dao接口与实现类) 
3.制作共性功能(通知类与通知)
4.定义切入点
5.绑定切入点与通知关系(切面)

环境准备

pom.xml添加Spring依赖

<?xml version="1.0" encoding="UTF-8"?>
<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.0</modelVersion>

    <groupId>org.example</groupId>
    <artifactId>spring_17_aop_demo</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>13</maven.compiler.source>
        <maven.compiler.target>13</maven.compiler.target>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>5.2.10.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
            <version>1.9.4</version>
        </dependency>
    </dependencies>

</project>
//添加BookDao和BookDaoImpl类
public interface BookDao {
    public void save();
    public void update();
    public void delete();
    public void select();
}

@Repository
public class BookDaoImpl implements BookDao {
    public void save() {
        //记录程序当前执行执行(开始时间)
        Long startTime = System.currentTimeMillis();
        //业务执行万次
        for (int i = 0; i < 10000; 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 ...");
    }
}
//创建Spring的配置类
@Configuration
@ComponentScan("com.green")
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();
    }
}

AOP实现步骤

定义通知类和通知

 定义切入点

要增强的是update方法,该如何定义呢?
//通知类
public class MyAdvice {
    //切入点
    @Pointcut("execution(void com.green.dao.BookDao.update())") //连接点方法
    private void pt(){}
    
    public void method(){
        System.out.println(System.currentTimeMillis());
    }
}

 步骤5:作切面

切面是用来描述通知和切入点之间的关系
绑定切入点与通知关系,并指定通知添加到原始连接点的具体执行位置
说明:@Before翻译过来是之前,也就是说通知会在切入点方法执行之前执行
public class MyAdvice {
    //切入点
    @Pointcut("execution(void com.green.dao.BookDao.update())") //连接点方法
    private void pt(){}

    @Before("pt()") //在切入点前面执行
    public void method(){
        System.out.println(System.currentTimeMillis());
    }
}

步骤6:将通知类配给容器并标识其为切面类

@Component
@Aspect
public class MyAdvice {
    //定义切入点
    @Pointcut("execution(void com.green.dao.BookDao.update())") //连接点方法
    private void pt(){}

    @Before("pt()") //在切入点前面执行
    public void method(){
        System.out.println(System.currentTimeMillis());
    }
}

步骤7:开启注解格式AOP功能

@Configuration
@ComponentScan("com.green")
@EnableAspectJAutoProxy//注解开发AOP
public class SpringConfig {
}

步骤8:行程序

看到在执行update方法之前打印了系统时间戳,说明对原始方法进行了增强,AOP编程成功。

名称

@EnableAspectJAutoProxy

@Aspect

@Pointcut

类型

配置类注解

类注解

方法注解

位置

配置类定义上方

切面类定义上方

切入点方法定义上方

作用

开启注解格式AOP功能

设置当前类为AOP切面类

设置切入点方法

属性

value(默认):切入点表达式

名称

@Before

类型

方法注解

位置

知方法定义上方

作用

设置当前通知方法与切入点之间的绑定关系,当前通知方法在原始切入点方法前运行

AOP工作流程

由于AOP是基于Spring容器管理的bean做的增强,所以整个工作过程需要从Spring加载bean说起:

 流程1:Spring容器启动

容器启动就需要去加载bean,哪些类需要被加载呢?
需要被增强的类,如:BookServiceImpl
通知类,如:MyAdvice
注意此时bean对象还没有创建成功

流程2:读取所有切面配置中的切入点

流程3:初始化bean

判定bean对应的类中的方法是否匹配到任意切入点
    注意第1步在容器启动的时候,bean对象还没有被创建成功。
    要被实例化bean对象的类中的方法和切入点进行匹配

匹配失败,创建原始对象,如UserDao
    匹配失败说明不需要增强,直接调用原始对象的方法即可。
匹配成功,创建原始对象(目标对象)的代理对象,如: BookDao
    匹配成功说明需要对其进行增强
    对哪个类做增强,这个类对应的对象就叫做目标对象
    因为要对目标对象进行功能增强,而采用的技术是动态代理,所以会为其创建一个代理对象
    最终运行的是代理对象的方法,在该方法中会对原始方法进行功能增强

流程4:获取bean执行方法

获取的bean是原始对象时,调用方法并执行,完成操作
获取的bean是代理对象时,根据代理对象的运行模式运行原始方法与增强的内容,完成操作

验证容器中是否为代理对象

如果目标对象中的方法会被增强,那么容器中将存入的是目标对象的代理对象
如果目标对象中的方法不被增强,那么容器中将存入的是目标对象本身。

验证思路

1.要执行的方法,不被定义的切入点包含,即不要增强,打印当前类的getClass()方法
2.要执行的方法,被定义的切入点包含,即要增强,打印出当前类的getClass()方法
3.观察两次打印的结果

步骤1:修改App,取类的类型 

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());
    }
}

步骤2:修改MyAdvice类,不增强

@Component
@Aspect
public class MyAdvice {
    //定义切入点
    @Pointcut("execution(void com.green.dao.BookDao.update1())") //连接点方法
    private void pt(){}

    @Before("pt()") //在切入点前面执行
    public void method(){
        System.out.println(System.currentTimeMillis());
    }
}
因为定义的切入点中,被修改成update1 ,所以BookDao中的update方法在执行的时候,就不会被增强
所以容器中的对象应该是目标对象本身。

步骤3:行程序

 步骤4:修改MyAdvice,增强

因为定义的切入点中,被修改成update ,所以BookDao中的update方法在执行的时候,就会被增强
所以容器中的对象应该是目标对象的代理对象
@Component
@Aspect
public class MyAdvice {
    //定义切入点
    @Pointcut("execution(void com.green.dao.BookDao.update())") //连接点方法
    private void pt(){}

    @Before("pt()") //在切入点前面执行
    public void method(){
        System.out.println(System.currentTimeMillis());
    }
}

步骤5:行程序

 AOP核心概念

目标对象(Target):原始功能去掉共性功能对应的类产生的对象,这种对象是无法直接完成最终工作的
代理(Proxy):目标对象无法直接完成工作,需要对其进行功能回填,通过原始对象的代理对象实现

AOP配置管理

AOP切入点表达式

对于AOP中切入点表达式,总共会学习三个内容,分别是 语法格式 、 通配符和书写技巧 

语法格式

切入点:要进行增强的方法
切入点表达式:要进行增强的方法的描述方式

描述方式一:执行com.green.dao包下的BookDao接口中的无参数update方法

 

描述方式二:执行com.green.dao.impl包下的BookDaoImpl类中的无参数update方法

 

因为调用接口方法的时候最终运行的还是其实现类的方法,所以上面两种描述方式都是可以的。
对于切入点表达式的语法为:
切入点表达式标准格式:动作关键字(访问修饰符 返回值 包名.类/接口名.方法名(参数)  异常名)
  execution(public User com.green.service.UserService.findById(int))

execution:动作关键字,描述切入点的行为动作,例如execution表示执行到指定切入点
public:访问修饰符,还可以是public,private等,可以省略
User:返回值,写返回值类型
com.green.service:包名,多级包使用点连接
UserService:类/接口名称
findById:方法名
int:参数,直接写参数的类型,多个类型用逗号隔开异常名:方法定义中抛出指定异常,可以省略

 通配符

* :单个独立的任意符号,可以独立出现,也可以作为前缀或者后缀的匹配符出现
execution(public * com.green.*.UserService.find*(*))
匹配com.green包下的任意包中的UserService类或接口中所有find开头的带有一个参数的方法

.. :多个连续的任意符号,可以独立出现,常用于简化包名与参数的书写
execution(public User com..UserService.findById(..))
匹配com包下的任意包中的UserService类或接口中所有名称为findById的方法

+ :专用于匹配子类类型
execution(* *..*Service+.*(..))
*Service+,表示所有以Service结尾的接口的子类。

 

 

 书写技巧

所有代码按照标准规范开发,否则以下技巧全部失效

描述切入点通常描述接口,而不描述实现类,如果描述到实现类,就出现紧耦合了
访问控制修饰符针对接口开发均采用public描述(可省略访问控制修饰符描述
返回值类型对于增删改类使用精准类型加速匹配,对于查询类使用*通配快速描述
包名书写尽量不使用..匹配,效率过低,常用*做单个包描述匹配,或精准匹配

接口名/类名书写名称与模块相关的采用*匹配,例如UserService书写成*Service,绑定业务层接口名

方法名书写以动词进行精准匹配,名词采用匹配,例如getById书写成getBy,selectAll书写成selectAll

参数规则较为复杂,根据业务方法灵活调整
通常不使用异常作为匹配规则

 AOP通知类型

类型介绍

前置通知
后置通知
环绕通知(重点)
返回后通知(了解)
抛出异常后通知(了解)

(1)前置通知,追加功能到方法执行前,类似于在代码1或者代码2添加内容
(2)后置通知,追加功能到方法执行后,不管方法执行的过程中有没有抛出异常都会执行,类似于在代码5添加内容
(3)返回后通知,追加功能到方法执行后,只有方法正常执行结束后才进行,类似于在代码3添加内容,
如果方法执行抛出异常,返回后通知将不会被添加
(4)抛出异常后通知,追加功能到方法抛出异常后,只有方法执行出异常才进行,类似于在代码4添加内容,
只有方法抛出异常后才会被添加
(5)环绕通知,环绕通知功能比较强大,它可以追加功能到方法执行的前后

 环境准备

pom.xml添加Spring依赖

    <dependencies>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>5.2.10.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
            <version>1.9.4</version>
        </dependency>
    </dependencies>

添加BookDaoBookDaoImpl

 创建Spring的配置类

@Configuration
@ComponentScan("com.green")
@EnableAspectJAutoProxy//注解开发AOP
public class SpringConfig {
}

创建通知类

@Component
@Aspect
public class MyAdvice {
    @Pointcut("execution(void com.green.dao.BookDao.update())")
    private void pt(){}

    @Pointcut("execution(int com.green.dao.BookDao.select())")
    private void pt2(){}

    //切入点之前
//    @Before("pt()")
    public void before(){
        System.out.println("before advice...");
    }

    //切入点之后
//    @After("pt()")
    public void after(){
        System.out.println("after advice...");
    }

    //环绕
//    @Around("pt()")
    public void around(ProceedingJoinPoint pjp) throws Throwable {
        System.out.println("around before advice...");
        //表示对原始操作的调用
        pjp.proceed();
        System.out.println("around after advice...");
    }

//    @Around("pt2()")
    public Object aroundSelect(ProceedingJoinPoint pjp) throws Throwable {
        System.out.println("around before advice...");
        //表示对原始操作的调用
        Object result = pjp.proceed();
        System.out.println("around after advice...");
        //需要将返回值扔回去
        return result;
    }

//    @AfterReturning("pt2()")
    public void afterReturning(){
        System.out.println("afterReturning advice...");
    }

    @AfterThrowing("pt2()")
    public void afterThrowing(){
        System.out.println("afterThrowing advice...");
    }
}

编写App运行类

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();
        //int i = 1 / 0;
        System.out.println(num);
    }
}

名称

@After

类型

方法注解

位置

知方法定义上方

作用

设置当前通知方法与切入点之间的绑定关系,当前通知方法在原始切入点方法后运行

@AfterReturning

类型

方法注解

位置

知方法定义上方

作用

设置当前通知方法与切入点之间绑定关系,当前通知方法在原始切入点方法正常执行完毕后执行

名称

@AfterThrowing

类型

方法注解

位置

通知方法定义上方

作用

设置当前通知方法与切入点之间绑定关系,当前通知方法在原始切入点方法运行抛出异常执行

名称

@Around

类型

方法注解

位置

知方法定义上方

作用

设置当前通知方法与切入点之间的绑定关系,当前通知方法在原始切入点方法前后运行

 环绕通知注意事项

1.  环绕通知必须依赖形参ProceedingJoinPoint才能实现对原始方法的调用,
进而实现原始方法调用前后同时添加通知
2.  通知中如果未使用ProceedingJoinPoint对原始方法进行调用将跳过原始方法的执行
3.  对原始方法的调用可以不接收返回值,通知方法设置成void即可,如果接收返回值,最好设定为Object类型
4.  原始方法的返回值如果是void类型,通知方法的返回值类型可以设置成void,也可以设置成Object
5.  由于无法预知原始方法运行后是否会抛出异常,因此环绕通知方法必须要处理Throwable异常

业务层接口执行效率

需求分析

需求:任意业务层接口执行均可显示其执行效率(执行时长)
具体实现的思路:
(1)  开始执行方法之前记录一个时间
(2)  执行方法
(3)  执行完方法之后记录一个时间
(4)  用后一个时间减去前一个时间的差值,就是我们需要的结果。

pom.xml添加Spring依赖

    <dependencies>
        <!--spring依赖-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>5.2.10.RELEASE</version>
        </dependency>
        <!--数据库连接池-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.1.16</version>
        </dependency>

        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis</artifactId>
            <version>3.5.6</version>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.47</version>
        </dependency>

        <!--spring-jdbc整合-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-jdbc</artifactId>
            <version>5.2.10.RELEASE</version>
        </dependency>

        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis-spring</artifactId>
            <version>1.3.0</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.2.10.RELEASE</version>
        </dependency>

        <!--解析切入点表达式依赖-->
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
            <version>1.9.4</version>
        </dependency>
    </dependencies>

添加AccountService、AccountServiceImpl、AccountDao与Account类
public interface AccountService {

    void save(Account account);//添加

    void delete(Integer id);//删除

    void update(Account account);//修改

    List<Account> findAll();//查询所有

    Account findById(Integer id);//根据id查询数据
}

@Service
public class AccountServiceImpl implements AccountService {

    //自动装配
    @Autowired
    private AccountDao accountDao;

    @Override
    public void save(Account account) {
        accountDao.save(account);
    }

    @Override
    public void delete(Integer id) {
        accountDao.delete(id);
    }

    @Override
    public void update(Account account) {
        accountDao.update(account);
    }

    @Override
    public List<Account> findAll() {
        return accountDao.findAll();
    }

    @Override
    public Account findById(Integer id) {
        return accountDao.findById(id);
    }
}

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);

    //根据id修改数据
    @Update("update tbl_account set name = #{name} , money = #{money} where id = #{id} ")
    void update(Account account);

    //查询所有数据
    @Select("select * from tbl_account")
    List<Account> findAll();

    //根据id查询数据
    @Select("select * from tbl_account where id = #{id} ")
    Account findById(Integer id);
}

public class Account implements Serializable {

    private Integer id;
    private String name;
    private Double money;
//setter..getter..toString方法省略
}

resources下提供一个jdbc.properties

 创建相关配置类

Spring配置类:SpringConfig 
@Configuration
@ComponentScan("com.green") //包扫描
@PropertySource("classpath:jdbc.properties")
@Import({JdbcConfig.class,MybatisConfig.class}) //导入配置类
@EnableAspectJAutoProxy
public class SpringConfig {
}

//JdbcConfig配置类

public class JdbcConfig {
    @Value("${jdbc.driver}")
    private String driver;
    @Value("${jdbc.url}")
    private String url;
    @Value("${jdbc.username}")
    private String userName;
    @Value("${jdbc.password}")
    private String password;

    @Bean
    public DataSource dataSource(){
        DruidDataSource ds = new DruidDataSource();
        ds.setDriverClassName(driver);
        ds.setUrl(url);
        ds.setUsername(userName);
        ds.setPassword(password);
        return ds;
    }
}

//mybatis配置类
public class MybatisConfig {
    //定义bean,SqlSessionFactoryBean,用于产生SqlSessionFactory对象
    @Bean
    public SqlSessionFactoryBean sqlSessionFactoryBean(DataSource dataSource){
        SqlSessionFactoryBean ssfb = new SqlSessionFactoryBean();
        //设置模型类的别名扫描
        ssfb.setTypeAliasesPackage("com.green.domain");
        //设置数据源
        ssfb.setDataSource(dataSource);
        return ssfb;
    }

    //定义bean,返回MapperScannerConfigurer对象
    @Bean
    public MapperScannerConfigurer mapperScannerConfigurer(){
        MapperScannerConfigurer msc = new MapperScannerConfigurer();
        msc.setBasePackage("com.green.dao");
        return msc;
    }
}

编写Spring整合Junit的测试类

 功能开发

创建AOP通知类

该类要被Spring管理,需要添加@Component
要标识该类是一个AOP的切面类,需要添加@Aspect
配置切入点表达式,需要添加一个方法,并添加@Pointcut
@Component
@Aspect
public class ProjectAdvice {

    //匹配业务层的所有接口
    @Pointcut("execution(* com.green.service.*Service.*(..))")
    private void servicePt() {}
    
    @Around("ProjectAdvice.servicePt()")
    public void runSpeed(ProceedingJoinPoint pjp) throws Throwable {
        //一次执行的签名时间
        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();
        }

        long end = System.currentTimeMillis();
        System.out.println("万次执行: " + className + "." + methodName + "===>" + (end - start) + "ms");
    }
}

AOP通知获取数据

获取参数
获取返回值 
获取异常

获取切入点方法的参数,所有的通知类型都可以获取参数
    JoinPoint:适用于前置、后置、返回后、抛出异常后通知
    ProceedingJoinPoint:适用于环绕通知
获取切入点方法返回值
    返回后通知
    环绕通知
获取切入点方法运行异常信息
    抛出异常后通知
    环绕通知

环境准备

pom.xml添加Spring依赖

添加BookDao和BookDaoImpl类
public interface BookDao {
    String findName(int id,String password);
}

@Repository
public class BookDaoImpl implements BookDao {
    @Override
    public String findName(int id,String password) {
        System.out.println("id:" + id);
        return "ok";
    }
}
创建Spring的配置类
@Configuration
@ComponentScan("com.green")
@EnableAspectJAutoProxy
public class SpringConfig {
}
编写App运行类
public class App {
    public static void main(String[] args) {
        ApplicationContext context = new AnnotationConfigApplicationContext(SpringConfig.class);
        BookDao bookDao = context.getBean(BookDao.class);
        String name = bookDao.findName(100,"root");
//        if (true)throw new NullPointerException();//异常测试
        System.out.println(name);
    }
}
编写通知类
@Component
@Aspect
public class MyAdvice {
    @Pointcut("execution(* com.green.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 ...");
    }

    //    @After("pt()")
    public void after(JoinPoint jp) {
        Object[] args = jp.getArgs();
        System.out.println(Arrays.toString(args));
        System.out.println("after advice ...");
    }

    //    @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;
    }

    //    @AfterReturning(value = "pt()", returning = "ret")
    public void afterReturning(Object ret) {
        System.out.println("afterReturning advice ..." + ret);
    }
    
    @AfterThrowing(value = "pt()", throwing = "t")
    public void afterThrowing(Throwable t) {
        System.out.println("afterThrowing advice ..." + t);
    }
}
使用JoinPoint的方式获取参数适用于前置 、 后置 、 返回后 、 抛出异常后 通知

获取返回值

 

 获取异常

百度网盘密码数据兼容处理

需求分析
需求:  对百度网盘分享链接输入密码时尾部多输入的空格做兼容处理。
①:在业务方法执行之前对所有的输入参数进行格式处理—— trim()  
②:使用处理后的参数调用原始方法——环绕通知中存在对原始方法的调用

环境准备

pom.xml添加Spring依赖

<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.2.10.RELEASE</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.4</version>
</dependency>
</dependencies>
添加ResourcesService,ResourcesServiceImpl,ResourcesDao和ResourcesDaoImpl类
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);
    }
}

public interface ResourcesDao {
    boolean readResources(String url, String password);
}

@Repository
public class ResourcesDaoImpl implements ResourcesDao {
    public boolean readResources(String url, String password) {
        System.out.println(password.length());
        //模拟校验
        return password.equals("root");
    }
}

创建Spring的配置类
@Configuration
@ComponentScan("com.green")
@EnableAspectJAutoProxy
public class SpringConfig {
}
运行类
public class AppCase {
    public static void main(String[] args) {
        ApplicationContext cxt = new AnnotationConfigApplicationContext(SpringConfig.class);
        ResourcesService resourcesService = cxt.getBean(ResourcesService.class);
        boolean flag = resourcesService.openURL("http://pan.baidu.com/haha", "root");
        System.out.println(flag);
    }
}

通知类
@Component
@Aspect
public class DataAdvice {
    @Pointcut("execution(boolean com.green.service.*Service.*(*,*))")
    private void servicePt(){}

    @Around("DataAdvice.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;
    }
}

AOP事务管理

Spring事务简介

事务作用:在数据层保障一系列的数据库操作同成功同失败
Spring事务作用:在数据层或业务层保障一系列的数据库操作同成功同失败
举个简单的例子,
转账业务会有两次数据层的调用,一次是加钱一次是减钱
把事务放在数据层,加钱和减钱就有两个事务
没办法保证加钱和减钱同时成功或者同时失败
这个时候就需要将事务放在业务层进行处理。
Spring为了管理事务,提供了一个平台事务管理器PlatformTransactionManager

PlatformTransactionManager只是一个接口,Spring还为其提供了一个具体的实现:

 

 转账案例-需求分析

需求:  实现任意两个账户间转账操作
需求微缩:  A账户减钱,B账户加钱
实现步骤:
①:数据层提供基础操作,指定账户减钱(outMoney),指定账户加钱(inMoney)
②:业务层提供转账操作(transfer),调用减钱与加钱的操作
③:提供2个账号和操作金额执行转账操作
④:基于Spring整合MyBatis环境搭建上述操作

转账案例-环境搭建

步骤1:备数据库表

 步骤2:创建项目导入jar

    <dependencies>
        <!--spring依赖-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>5.2.10.RELEASE</version>
        </dependency>
        <!--数据库连接池-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.1.16</version>
        </dependency>

        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis</artifactId>
            <version>3.5.6</version>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.47</version>
        </dependency>

        <!--spring-jdbc整合-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-jdbc</artifactId>
            <version>5.2.10.RELEASE</version>
        </dependency>

        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis-spring</artifactId>
            <version>1.3.0</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.2.10.RELEASE</version>
        </dependency>
    </dependencies>

步骤3:据表创建模型类

 步骤4:创建Dao接口

 步骤5:创建Service接口和实现类

 步骤6:添加jdbc.properties文件

 步骤7:创建JdbcConfig配置类

 步骤8:创建MybatisConfig配置类

 

 步骤9:创建SpringConfig配置类

 步骤10:编写测试类

 事务管理

上述环境,运行单元测试类,会执行转账操作, Tom 的账户会减少100,Jerry 的账户会加100。
这是正常情况下的运行结果,但是如果在转账的过程中出现了异常,如:
@Service
public class AccountServiceImpl implements AccountService {

    //自动装配
    @Autowired
    private AccountDao accountDao;

    @Override
    public void transfer(String out, String in, Double money) {
        accountDao.inMoney(in, money);
//        int i = 1 / 0;
        accountDao.outMoney(out, money);
    }
}

①:程序正常执行时,账户金额A减B加,没有问题
②:程序出现异常后,转账失败,但是异常之前操作成功,异常之后操作失败,整体业务失败

 步骤1:在需要被事务管理的方法上添加注解

@Service
public class AccountServiceImpl implements AccountService {

    //自动装配
    @Autowired
    private AccountDao accountDao;
    @Transactional
    public void transfer(String out, String in, Double money) {
        accountDao.inMoney(in, money);
//        int i = 1 / 0;
        accountDao.outMoney(out, money);
    }
}
注意:
写在接口类上,该接口的所有实现类的所有方法都会有事务
写在接口方法上,该接口的所有实现类的该方法都会有事务
写在实现类上,该类中的所有方法都会有事务
写在实现类方法上,该方法上有事务
建议写在实现类或实现类的方法上

步骤2:JdbcConfig类中配置事务管理器

public class JdbcConfig {
    @Value("${jdbc.driver}")
    private String driver;
    @Value("${jdbc.url}")
    private String url;
    @Value("${jdbc.username}")
    private String userName;
    @Value("${jdbc.password}")
    private String password;

    @Bean
    public DataSource dataSource(){
        DruidDataSource ds = new DruidDataSource();
        ds.setDriverClassName(driver);
        ds.setUrl(url);
        ds.setUsername(userName);
        ds.setPassword(password);
        return ds;
    }

    //事务管路
    @Bean
    public PlatformTransactionManager transactionManager(DataSource dataSource){
        DataSourceTransactionManager transactionManager = new DataSourceTransactionManager();
        transactionManager.setDataSource(dataSource);
        return transactionManager;
    }
}

步骤3:开启事务注解

@Configuration
@ComponentScan("com.green") //包扫描
//@PropertySource:加载类路径jdbc.properties文件
@PropertySource("classpath:jdbc.properties")
@Import({JdbcConfig.class,MybatisConfig.class}) //导入配置类
@EnableTransactionManagement    //事务管理
public class SpringConfig {
}

名称

@EnableTransactionManagement

类型

配置类注解

位置

配置类定义上方

作用

设置当前Spring环境中开启注解式事务支持

名称

@Transactional

类型

接口注解 类注解 方法注解

位置

业务层接口上方 业务层实现类上方 业务方法上方

作用

为当前业务层方法添加事务(如果设置在类或接口上方则类或接口中所有方法均添加事务)

 Spring事务角色

事务管理员 和 事务协调员 
事务管理员:发起事务方,在Spring中通常指代业务层开启事务的方法
事务协调员:加入事务方,在Spring中通常指代数据层方法,也可以是业务层方法

1. 未开启Spring事务之前:

AccountDao的outMoney因为是修改操作,会开启一个事务T1 
AccountDao的inMoney因为是修改操作,会开启一个事务T2
AccountService的transfer没有事务
    运行过程中如果没有抛出异常,则T1和T2都正常提交,数据正确
    如果在两个方法中间抛出异常,T1因为执行成功提交事务,T2因为抛异常不会被执行就会导致数据出现错误

2.  开启Spring的事务管理后

transfer上添加了@Transactional注解,在该方法上就会有一个事务T 
AccountDao的outMoney方法的事务T1加入到transfer的事务T中
AccountDao的inMoney方法的事务T2加入到transfer的事务T中
这样就保证他们在同一个事务中,当业务层中出现异常,整个事务就会回滚,保证数据的准确性。
注意:
目前的事务管理是基于DataSourceTransactionManager和SqlSessionFactoryBean使用的是同一个数据源

 Spring事务属性

事务配置

 上面这些属性都可以在@Transactional 注解的参数上进行设置。

出现这个问题的原因是,Spring的事务只会对Error异常 和RuntimeException异常及其子类进行事务回滚
其他的异常类型是不会回滚的,对应IOException不符合上述条件所以不回滚
此时就可以使用rollbackFor属性来设置出现IOException异常不回滚
@Service
public class AccountServiceImpl implements AccountService {
    @Autowired
    private 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);
    }
}
rollbackForClassName等同于rollbackFor,只不过属性为异常的类全名字符串
noRollbackForClassName等同于noRollbackFor,只不过属性为异常的类全名字符串
isolation设置事务的隔离级别
DEFAULT  :默认隔离级别,  会采用数据库的隔离级别
READ_UNCOMMITTED  :  读未提交
READ_COMMITTED  :  读已提交
REPEATABLE_READ  :  重复读取
SERIALIZABLE:  串行化

 转账业务追加日志案例

需求分析
在前面的转案例的基础上添加新的需求,完成转账后记录日志。
需求:实现任意两个账户间转账操作,并对每次转账操作在数据库进行留痕
需求微缩:A账户减钱,B账户加钱,数据库记录日志

①:基于转账操作案例添加日志模块,实现数据库中记录日志
②:业务层转账操作(transfer),调用减钱、加钱与记录日志功能
无论转账操作是否成功,均进行转账操作的日志留痕

步骤1:创建日志表

 步骤2:添加LogDao接口

 步骤3:添加LogService接口与实现类

 步骤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 {

    @Autowired
    private AccountDao accountDao; 
    @Autowired
    private LogService logService; 

    @Transactional
    public void transfer(String out, String in, Double money) {
        try {
            accountDao.outMoney(out, money);
            accountDao.inMoney(in, money);
        } finally {
            logService.log(out, in, money); }
    }
}

步骤5:行程序

当程序正常运行,tbl_account表中转账成功,tbl_log表中日志记录成功
当转账业务之间出现异常(int i =1/0),转账失败,tbl_account成功回滚,但是tbl_log表未添加数据
这个结果和我们想要的不一样,什么原因?该如何解决?
失败原因:日志的记录与转账操作隶属同一个事务,同成功同失败最终效果:无论转账操作是否成功,日志必须保留

事务传播行为

 1.修改logService改变事务的传播行为

 2.事务传播行为的可选值

猜你喜欢

转载自blog.csdn.net/weixin_52270382/article/details/130564051