【JAVA】Spring(下)AOP&Spring事务

目录

【Spring】

【AOP】—— Aspect Oriented Programming

【核心】

【AOP的使用】

【AOP流程】

【AOP切入点表达式】

【切入点表达式的标准格式】

【通配符描述切入点】

【AOP的通知类型】

【前置通知】

【后置通知】

【环绕通知】

【返回后通知】

【抛出异常后通知】

【案例】—— 测量业务层接口万次执行效率

【AOP通知获取数据】

【获取切入点方法的参数】

【获取切入点方法返回值】

【获取切入点方法运行异常信息】

【案例】—— 密码数据兼容处理

【Spring事务】

【Spring事务角色】

【Spring事务配置】

【事务传播行为】

【案例】—— 银行账户转账


【Spring】

【AOP】—— Aspect Oriented Programming

【概述】

面向切面编程,一种编程范式,指导开发者如何组织程序结构

【作用】

在不惊动原始设计的基础上为其进行功能增强

【核心】

1、连接点(JoinPoint):程序执行过程中的任意位置,粒度为执行方法、抛出异常、设置变量等

在SpringAOP中,理解为方法的执行

2、切入点(Pointcut):匹配连接点的式子

  • 在SpringAOP中,一个切入点可以只描述一个具体方法,也可以匹配多个方法
  • 1、一个具体方法:com.itheima.dao包下的BookDao接口中的无形参无返回值的save方法
  • 2、匹配多个方法:所有的save方法,所有的get开头的方法,所有以Dao结尾的接口中的任意方法,所有带有一个参数的方法

3、通知(Advice):在切入点处执行的操作,也就是共性功能

  • 在SpringAOP中,功能最终以方法的形式呈现

4、通知类:定义通知的类

  • 切面(Aspect):描述通知与切入点的对应关系

【AOP的使用】

1、导入aop相关坐标(aspectjweaver和spring-aop)

        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
            <version>1.9.4</version>
        </dependency>

【注意】

spring-aop坐标依赖与spring-context

2、实现dao接口和实现类

//接口
public interface BookDao {
    void save();
    void update();
}
//实现类
@Repository
public class BookDaoImpl implements BookDao {
    @Override
    public void save() {
        System.out.println(System.currentTimeMillis());
        System.out.println("book dao save");
    }

    @Override
    public void update() {
        System.out.println("book dao update");
    }
}

3、定义通知类和通知

public class MyAdvice {
    public void method() {
        System.out.println(System.currentTimeMillis());
}

4、定义切入点

public class MyAdvice {
    @Pointcut("execution(void com.itheima.dao.BookDao.update())")
    private void pt() {
    }
    public void method() {
        System.out.println(System.currentTimeMillis());

    }
}

【注意】

切入点定义依托一个不具有实际意义的方法进行,即无参数,无返回值,方法体无实际逻辑

5、绑定切入点与通知的关系,并指定通知添加到原始连接点的具体执行位置

public class MyAdvice {
    @Pointcut("execution(void com.itheima.dao.BookDao.update())")
    private void pt() {
    }
    @Before("pt()")
    public void method() {
        System.out.println(System.currentTimeMillis());
    }
}

6、定义通知类受Spring容器管理

@Component
@Aspect
public class MyAdvice {
    @Pointcut("execution(void com.itheima.dao.BookDao.update())")
    private void pt() {
    }
    @Before("pt()")
    public void method() {
        System.out.println(System.currentTimeMillis());
    }
}

7、开启Spring对AOP注解驱动支持

@Configuration
@ComponentScan("com.itheima")
@EnableAspectJAutoProxy        //开启Spring对AOP注解驱动支持
public class SpringConfig {
}

【AOP流程】

1、Spring容器启动

2、读取所有切面配置中的切入点

3、初始化bean,判定bean对应的类中的方法是否匹配到任意切入点

  • 匹配失败,创建对象
  • 匹配成功,创建原始对象(目标对象)的代理对象

4、获取bean执行方法

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

【注意】

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

【AOP切入点表达式】

切入点:要进行增强的方法

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

【切入点表达式的标准格式】

表达式:动作关键字(访问修饰符  返回值  包名.类/接口名.方法名(参数)异常名)

  • 动作关键字:描述切入点的行为动作,例如execution表示执行到指定切入点
  • 访问修饰符:public,private等,可以省略
  • 返回值
  • 包名
  • 类/接口名
  • 方法名
  • 参数
  • 异常名:方法定义中抛出指定异常,可以省略

例:execution(public User com.itheima.service.UserService.findById (int))

【通配符描述切入点】

1、* :单个独立的任意符号,可以独立出现,也可以作为前缀或者后缀的匹配符出现

例:匹配com.itheima包下的任意包中的UserService类或接口中所有find开头的带有一个参数的方法

execution(public User com.itheima.service.UserService.findById (int))

2、.. :多个连续的任意符号,可以独立出现,常用于简化包名与参数的书写

例:匹配com包下的任意包中的UserService类或接口中所有名称为findById的方法

execution(public User com..UserService.findById (..))

3、+:专用于匹配子类类型

例:

execution(* *..*Service+.*(..))

【书写注意】

  1. 所有代码按照标准规范开发,否则以下技巧全部失效
  2. 描述切入点通常描述接口,而不描述实现类
  3. 访问控制修饰符针对接口开发均采用public描述(可省略访问控制修饰符描述
  4. 返回值类型对于增删改类使用精准类型加速匹配,对于查询类使用*通配快速描述
  5. 包名书写尽量不使用..匹配,效率过低,常用*做单个包描述匹配,或精准匹配
  6. 接口名/类名书写名称与模块相关的采用*匹配,例如UserService书写成*Service,绑定业务层接口名
  7. 方法名书写以动词进行精准匹配,名词采用*匹配,例如getById书写成getBy*,selectAll书写成selectAll
  8. 参数规则较为复杂,根据业务方法灵活调整
  9. 通常不使用异常作为匹配规则
  10. *与com之间需要有空格

【AOP的通知类型】

【概述】

AOP通知描述了抽取的共性功能,根据共性功能抽取的位置不同,最终运行代码时要将其加入到合理的位置

【分类】

  • 前置通知
  • 后置通知
  • 环绕通知
  • 返回后通知
  • 抛出异常后通知

【前置通知】

  • 名称:@Before
  • 类型:方法注解
  • 作用:设置当前通知方法与切入点之间的绑定关系,当前通知方法在原始切入点方法前运行
  • 相关属性:value(默认):切入点方法名,格式为类名.方法名()

例:

@Before("pt()")
public void before() {
    System.out.println("before advice ...");
}

【后置通知】

  • 名称:@After
  • 类型:方法注解
  • 作用:设置当前通知方法与切入点之间的绑定关系,当前通知方法在原始切入点方法后运行
  • 相关属性:value(默认):切入点方法名,格式为类名.方法名()

例:

@After("pt()")
public void after() {
    System.out.println("after advice ...");
}

【环绕通知】

  • 名称:@Around(重点,常用)
  • 类型:方法注解
  • 作用:设置当前通知方法与切入点之间的绑定关系,当前通知方法在原始切入点方法前后运行

例:

@Around("pt()")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
    System.out.println("around before advice ...");
    Object ret = pjp.proceed();
    System.out.println("around after advice ...");
    return ret;
}

【注意】

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

【返回后通知】

  • 名称:@AfterReturning
  • 类型:方法注解
  • 作用:设置当前通知方法与切入点之间的绑定关系,当前通知方法在原始切入点方法正常执行完毕后运行
  • 相关属性:value(默认):切入点方法名,格式为类名.方法名()

例:

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

【抛出异常后通知】

  • 名称:@AfterThrowing
  • 类型:方法注解
  • 作用:设置当前通知方法与切入点之间的绑定关系,当前通知方法在原始切入点方法运行抛出异常后执行
  • 相关属性:value(默认):切入点方法名,格式为类名.方法名()

例:

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

【案例】—— 测量业务层接口万次执行效率

例:

@Component
@Aspect
public class ProjectAdvice {
    @Pointcut("execution(* com.itheima.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:适用于前置、后置、返回后、抛出异常后通知
  • ProceedJointPoint:适用于环绕通知

例:

@Before("pt()")
public void before(JoinPoint jp) {
    Object[] args = jp.getArgs();
    System.out.println(Arrays.toString(args));
}
@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;
}

【注意】

  • JoinPoint对象描述了连接点方法的运行状态,可以获取到原始方法的调用参数
  • ProceedJointPoint是JoinPoint的子类

【获取切入点方法返回值】

  • 返回后通知
  • 环绕通知

例:

@AfterReturning(value = "pt()",returning = "ret")
public void afterReturning(String ret) {
    System.out.println("afterReturning advice ..."+ret);
}
@Around("pt()")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
    Object ret = pjp.proceed();
    return ret;
}

【注意】

  • 抛出异常后通知可以获取切入点方法中出现的异常信息,使用形参可以接收对应的异常对象
  • 环绕通知中可以手工书写对原始方法的调用,得到的结果即为原始方法的返回值

【获取切入点方法运行异常信息】

  • 抛出异常后通知
  • 环绕通知

例:

@AfterThrowing(value = "pt()",throwing = "t")
public void afterThrowing(Throwable t) {
    System.out.println("afterThrowing advice ..."+ t);
}
@Around("pt()")
public Object around(ProceedingJoinPoint pjp)  {
    Object ret = null;
    try {
        ret = pjp.proceed();
    } catch (Throwable t) {
        t.printStackTrace();
    }
    return ret;
}

【注意】

  • 抛出异常后通知可以获取切入点方法中出现的异常信息,使用形参可以接收对应的异常对象
  • 抛出异常后通知可以获取切入点方法运行的异常信息,使用形参可以接收运行时抛出的异常对象

【案例】—— 密码数据兼容处理

例:

@Component
@Aspect
public class DataAdvice {
    @Pointcut("execution(boolean com.itheima.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;
    }
}

【Spring事务】

【作用】

  • 事务作用:在数据层保障一系列的数据库操作同成功同失败
  • Spring事务作用:在数据层或业务层保障一系列的数据库操作同成功同失败

【Spring事务角色】

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

【Spring事务配置】

属性作用示例
readOnly设置是否为只读事务readOnly=true 只读事务
timeout设置事务超时时间timeout=-1 永不超时
rollbackFor

设置事务回滚异常(class)

rollbackFor={NullPointException.class}
rollbackForClassName设置事务回滚异常(String)rollbackFor={NullPointException.String}
noRollbackForFor

设置事务不回滚异常(class)

noRollbackFor={NullPointException.class}
noRollbackForClassName设置事务不回滚异常(String)

noRollbackFor={NullPointException.Stiring}

propagation设置事务传播行为

【事务传播行为】

传播属性事务管理员事务协调员
REQUIRED开启T加入T
新建T2
REQUIRES_NEW开启T新建T2
新建T2
SUPPORTS开启T加入T
NOT_SUPPORTED开启T
MANDATORY开启T加入T
ERROR
NEVER开启TERROR
NESTED设置savePoint,一旦事务回滚,事务回滚到savePoint处,交由客户响应提交/回滚

【案例】—— 银行账户转账

需求:实现任意两个账户间转账操作,并对每次转账操作在数据库进行留痕

实现:

  1. 在业务层接口上添加Spring事务管理
  2. 设置事务管理
  3. 开启注解式事务驱动
  4. 数据层提供基础操作,指定账户减钱(outMoney),指定账户加钱(inMoney)
  5. 业务层提供转账操作(transfer),调用减钱与加钱的操作
  6. 提供2个账号和操作金额执行转账操作
  7. 基于Spring整合MyBatis环境搭建上述操作
  8. 基于转账操作案例添加日志模块,实现数据库中记录日志
  9. 业务层转账操作(transfer),调用减钱、加钱与记录日志功能

结果分析:

  1. 程序正常执行时,账户金额A减B加,没有问题
  2. 程序出现异常后,转账失败,但是异常之前操作成功,异常之后操作失败,整体业务失败
  3. 日志的记录与转账操作隶属同一个事务,同成功同失败

优化:

  1. 添加Spring事务,以确保整个操作同时成功或同时失败
  2. 无论转账操作是否成功,日志必须保留

例:

//1、转账数据层操作
public interface AccountDao {
    @Update("update account set money = money + #{money} where name = #{name}")
    void inMoney(@Param("name") String name, @Param("money") Double money);

    @Update("update account set money = money - #{money} where name = #{name}")
    void outMoney(@Param("name") String name, @Param("money") Double money);
}
//2、提供2个账号操作执行转账
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = SpringConfig.class)
public class AccountServiceTest {
    @Autowired
    private AccountService accountService;

    @Test
    public void testTransfer(){
        accountService.transfer("Tom","Jerry",500D);
    }
}
//3、日志数据层操作
public interface LogDao {
    @Insert("insert into tb_log (id,info,createData) VALUES (null,#{info},now())")
    void log(String info);
}
//4、日志
//提供日志添加接口
public interface LogService {
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    void log(String out, String in, Double money);
}

//实现类
@Service
public class LogServiceImpl implements LogService {
    @Autowired
    private LogDao logDao;
    @Override
    public void log(String out, String in, Double money) {
        logDao.log("转账操作由" + out + "到" + in + ",金额:" + money);
    }
}
//5、业务层
//提供转账操作接口
public interface AccountService {
    /**
     * 转账操作
     * @param out 转出方
     * @param in 转入方
     * @param money 金额
     */
    @Transactional
    void transfer(String out, String in, Double money);
}

//转账接口实现类
@Service
public class AccountServiceImpl implements AccountService {
    @Autowired
    private AccountDao accountDao;
    @Autowired
    private LogService logService;
    public void transfer(String out, String in, Double money) {
        try {
            accountDao.outMoney(out, money);
            accountDao.inMoney(in, money);
        } finally {
            logService.log(out, in, money);
        }
    }
}
//6、在Jdbcconfig中添加事务
@Bean
public PlatformTransactionManager transactionManager(DataSource dataSource){
    DataSourceTransactionManager transactionManager=new DataSourceTransactionManager();
    transactionManager.setDataSource(dataSource);
    return transactionManager;
}
//7、开启注解式事务驱动
@Configuration
@ComponentScan("com.itheima")
@PropertySource("classpath:jdbc.properties")
@Import({JdbcConfig.class,MybatisConfig.class})
@EnableTransactionManagement        //开启注解式事务驱动
public class SpringConfig {
}

【注意】

  • Spring注解式事务通常添加在业务层接口中而不会添加到业务层实现类中,降低耦合
  • 注解式事务可以添加到业务方法上表示当前方法开启事务,也可以添加到接口上表示当前接口所有方法开启事务
  • 事务管理器要根据实现技术进行选择
  • MyBatis框架使用的是JDBC事务

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Lx_Hy_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值