AOP
1.1 AOP简介和作用【理解】
- AOP(Aspect Oriented Programming)面向切面编程,一种编程范式,指导开发者如何组织程序结构
- OOP(Object Oriented Programming)面向对象编程
- 作用:在不惊动原始设计的基础上为其进行功能增强。简单的说就是在不改变方法源代码的基础上对方法进行功能增强。
- Spring理念:无入侵式/无侵入式
面向对象(OOP)->面向切面(AOP)
面向切面(AOP): 是一种编程思想
动态代理
动态代理: 其实就是一个(Proxy)类,使用这个类可以在不修改源代码的基础上对功能进行增强!!!
面向切面
面向切面(AOP): 底层就是使用动态代理实现的!!!
面向切面编程思想: 在不修改源代码的基础上对功能进行增强!!!
其他概念
连接点: 所有方法
切入点: 被增强的方法
通知: 增强的代码
通知类: 工具类
切面: 绑定切入点和通知的关系
1.2 AOP入门
开发步骤
1)导包
2)开启AOP注解功能
3)编写工具类(通知类)
4)其他
【第一步】导入aop相关坐标
<dependencies>
<!--spring核心依赖,会将spring-aop传递进来-->
<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>
【第二步】定义dao接口与实现类
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 ...");
}
}
【第三步】定义通知类,制作通知方法
//通知类必须配置成Spring管理的bean
@Component
public class MyAdvice {
public void method(){
System.out.println(System.currentTimeMillis());
}
}
【第四步】定义切入点表达式、配置切面(绑定切入点与通知关系)
//通知类必须配置成Spring管理的bean
@Component
//设置当前类为切面类类
@Aspect
public class MyAdvice {
//设置切入点,@Pointcut注解要求配置在方法上方
@Pointcut("execution(void com.itheima.dao.BookDao.update())")
private void pt(){}
//设置在切入点pt()的前面运行当前操作(前置通知)
@Before("pt()")
public void method(){
System.out.println(System.currentTimeMillis());
}
}
【第五步】在配置类中进行Spring注解包扫描和开启AOP功能
@Configuration
@ComponentScan("com.itheima")
//开启注解开发AOP功能
@EnableAspectJAutoProxy
public class SpringConfig {
}
测试类和运行结果
public class App {
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
BookDao bookDao = ctx.getBean(BookDao.class);
bookDao.update();
}
}
1.3 AOP工作流程【理解】
疑难点
1)为什么要导入AspectJ包
spring 实现了面向切面这种编程思想
AspectJ 也实现了面向切面这种编程思想
AspectJ实现的比较好
2)@EnableAspectJAutoProxy的作用
3)强调什么是切面
4)切入点只能指定一个方法么?
//不是的, 可以使用通配符
@Pointcut("execution(void com.itheima.dao.BookDao.*())")
AOP工作流程
1.4 AOP详解
AOP切入点表达式
//1.严格匹配
@Pointcut("execution(void com.itheima.service.OrderService.placeOrder())")
//2.使用*匹配, 代表匹配单个 (返回值、包名、类名、方法名、参数 都可以匹配)
//还可以匹配 add* *Dao 以xxx开头/ xxx结尾
@Pointcut("execution(* com.itheima.service.OrderService.*())")
//3.使用..匹配,代表可以匹配多级, 0个、1个、多个 (一般用来匹配 包名、参数)
@Pointcut("execution(void com..service.OrderService.placeOrder(..))")
//4.常见写法
@Pointcut("execution(* com.itheima.service.*.*(..))")
AOP通知类型
AOP通知分类
- AOP通知描述了抽取的共性功能,根据共性功能抽取的位置不同,最终运行代码时要将其加入到合理的位置
- AOP通知共分为5种类型
- 前置通知:在切入点方法执行之前执行
- 后置通知:在切入点方法执行之后执行,无论切入点方法内部是否出现异常,后置通知都会执行。
- **环绕通知(重点):**手动调用切入点方法并对其进行增强的通知方式。
- 返回后通知(了解):在切入点方法执行之后执行,如果切入点方法内部出现异常将不会执行。
- 抛出异常后通知(了解):在切入点方法执行之后执行,只有当切入点方法内部出现异常之后才执行。
AOP通知详解
前置通知
- 名称:@Before
- 类型:方法注解
- 位置:通知方法定义上方
- 作用:设置当前通知方法与切入点之间的绑定关系,当前通知方法在原始切入点方法前运行
- 范例:
@Before("pt()")
public void before() {
System.out.println("before advice ...");
}
后置通知
- 名称:@After
- 类型:方法注解
- 位置:通知方法定义上方
- 作用:设置当前通知方法与切入点之间的绑定关系,当前通知方法在原始切入点方法后运行
- 范例:
@After("pt()")
public void after() {
System.out.println("after advice ...");
}
返回后通知
- 名称:@AfterReturning(了解)
- 类型:方法注解
- 位置:通知方法定义上方
- 作用:设置当前通知方法与切入点之间的绑定关系,当前通知方法在原始切入点方法正常执行完毕后运行
- 范例:
@AfterReturning("pt()")
public void afterReturning() {
System.out.println("afterReturning advice ...");
}
抛出异常后通知
- 名称:@AfterThrowing(了解)
- 类型:方法注解
- 位置:通知方法定义上方
- 作用:设置当前通知方法与切入点之间的绑定关系,当前通知方法在原始切入点方法运行抛出异常后执行
- 范例:
@AfterThrowing("pt()")
public void afterThrowing() {
System.out.println("afterThrowing 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;
}
环绕通知注意事项
- 环绕通知方法形参必须是ProceedingJoinPoint,表示正在执行的连接点,使用该对象的proceed()方法表示对原始对象方法进行调用,返回值为原始对象方法的返回值。
- 环绕通知方法的返回值建议写成Object类型,用于将原始对象方法的返回值进行返回,哪里使用代理对象就返回到哪里。
AOP通知位置
脑海中要清楚try{}catch(){}代码块
执行时机
前置:在原始方法运行之前执行
后置:在原始方法运行之后执行 - 不管原始方法有没有出现异常都会执行
异常:在原始方法运行出现异常的情况下才会执行
返回后:在原始方法运行之后执行 - 但是原始方法出现异常的情况下就不会执行了
环绕(重点)
@Around("pt()")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
Object ret = null;
System.out.println("前置通知 ...");
try {
//表示对原始操作的调用
ret = pjp.proceed();
System.out.println("返回后通知...");
} catch (Throwable throwable) {
System.out.println("异常通知...");
}
System.out.println("后置通知...");
return ret;
}
pjp 就是对原始操作的一个封装, 比直接使用反射方便很多!!!
1.5 AOP案例
案例-业务接口执行效率
@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();
Object[] args = pjp.getArgs(); //参数
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");
}
}
pjp对原始操作进行了封装, 封装了原始方法的类名、方法名、参数、返回值 还可以使用pjp执行原始方法
AOP通知获取数据
1)ProceedingJoinPoint 只能在环绕通知中使用
2)每一种通知都可以拿到参数
3)返回值,只有环绕通知和返回后通知可以拿到返回值
4)异常,只有环绕通知和异常可以拿到异常信息
//JoinPoint:用于描述切入点的对象,必须配置成通知方法中的第一个参数,可用于获取原始方法调用的参数
@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 ...");
}
//ProceedingJoinPoint:专用于环绕通知,是JoinPoint子类,可以实现对原始方法的调用
@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 t) {
t.printStackTrace();
}
return ret;
}
//设置返回后通知获取原始方法的返回值,要求returning属性值必须与方法形参名相同
@AfterReturning(value = "pt()",returning = "ret")
public void afterReturning(JoinPoint jp,String ret) {
System.out.println("afterReturning advice ..."+ret);
}
//设置抛出异常后通知获取原始方法运行时抛出的异常对象,要求throwing属性值必须与方法形参名相同
@AfterThrowing(value = "pt()",throwing = "t")
public void afterThrowing(Throwable t) {
System.out.println("afterThrowing advice ..."+t);
}
案例-百度网盘密码数据兼容处理
@Component
@Aspect
public class DataAdvice {
@Pointcut("execution(boolean com.itheima.service.*Service.*(*,*))")
private void 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;
}
}
1)理解AOP思想: 在不修改源代码的基础上对功能进行增强!!!
2)理解入门案例: 为什么导入aspectj包, @EnableAspectJAutoProxy注解的作用, @Aspect注解的作用
3)环绕通知的应用
切入点表达式 、 其他的通知类型 了解就行!!! 百度查资料能用就行了!!!
Spring事务
什么是事务?
mysql事务,是mysql提供的一个功能
功能:可以保证多条SQL语句同时成功同时失败!!!
spring事务
spring事务 : 就是对mysql事物的一个封装
spring事务开发步骤
步骤:
1, 开启事务功能
2, 配置事务管理器
3, 在需要事务的方法上面添加注解
1, 开启事务功能
@Configuration
@ComponentScan("com.itheima")
@PropertySource("classpath:jdbc.properties")
@Import({JdbcConfig.class,MybatisConfig.class})
//开启注解式事务驱动
@EnableTransactionManagement
public class SpringConfig {
}
2, 配置事务管理器
//配置事务管理器,mybatis使用的是jdbc事务
@Bean
public PlatformTransactionManager transactionManager(DataSource dataSource) {
DataSourceTransactionManager transactionManager = new DataSourceTransactionManager();
transactionManager.setDataSource(dataSource);
return transactionManager;
}
3, 在需要事务的方法上面添加注解
@Transactional
public void transfer(String out,String in ,Double money) ;
事务管理器
1)事务管理器是个什么东西
2)事务管理器的作用
spring事务角色
事务控制是在service层
事务管理员: 外层的事务方法,我们起了一名字事务管理员
事务协调员: 里层的事务方法,我们起了一名字事务协调员
spring事务属性
@Transactional注解的属性
@Transactional(readOnly = true,timeout = -1,rollbackFor = IOException.class)
//1)readOnly = true 只读,适用于查询操作
//2)timeout = 2 和数据库连接的超时时间,单位S. 默认-1,代表永不超时,永久等待
//3)rollbackFor 指定回滚的异常
// 默认并不是所有的异常都会回滚,默认RuntimeException本身和RuntimeException子类的异常
// 才会回滚
事务的传播行为(了解)
案例:转账业务追加日志
需求和分析
- 需求:实现任意两个账户间转账操作,并对每次转账操作在数据库进行留痕
- 需求微缩:A账户减钱,B账户加钱,数据库记录日志
- 分析:
①:基于转账操作案例添加日志模块,实现数据库中记录日志
②:业务层转账操作(transfer),调用减钱、加钱与记录日志功能 - 实现效果预期:
无论转账操作是否成功,均进行转账操作的日志留痕 - 存在的问题:
日志的记录与转账操作隶属同一个事务,同成功同失败 - 实现效果预期改进:
无论转账操作是否成功,日志必须保留 - 事务传播行为:事务协调员对事务管理员所携带事务的处理态度
【准备工作】环境整备
USE spring_db;
CREATE TABLE tbl_log(
id INT PRIMARY KEY AUTO_INCREMENT,
info VARCHAR(255),
createDate DATE
);
public interface LogService {
//propagation设置事务属性:传播行为设置为当前操作需要新事务
@Transactional
void log(String out, String in, Double money);
}
@Service
public class LogServiceImpl implements LogService {
@Autowired
private LogDao logDao;
public void log(String out,String in,Double money ) {
logDao.log("转账操作由"+out+"到"+in+",金额:"+money);
}
}
public interface LogDao {
@Insert("insert into tbl_log (info,createDate) values(#{info},now())")
void log(String info);
}
【第一步】在AccountServiceImpl中调用logService中添加日志的方法
@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);
int i = 1/0;
accountDao.inMoney(in,money);
}finally {
logService.log(out,in,money);
}
}
}
【第二步】在LogService的log()方法上设置事务的传播行为
public interface LogService {
//propagation设置事务属性:传播行为设置为当前操作需要新事务
@Transactional(propagation = Propagation.REQUIRES_NEW)
void log(String out, String in, Double money);
}
【第三步】运行测试类,查看结果
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = SpringConfig.class)
public class AccountServiceTest {
@Autowired
private AccountService accountService;
@Test
public void testTransfer() throws IOException {
accountService.transfer("Tom","Jerry",50D);
}
}