简介

AOP(Aspect Oriented Programming),即面向切面编程

这是对面向对象思想的一种补充。

面向切面编程,就是在程序运行时,不改变程序源码的情况下,动态的增强方法的功能。

常见的使用场景有:

  • 日志
  • 事务
  • 数据库操作

这些操作中,无一例外,都有很多模板化的代码,而解决模板化代码,消除臃肿就是 AOP 的强项。

在 AOP 中,有几个常见的概念

概念 说明
切点 要添加代码的地方,称作切点
通知(增强) 通知就是向切点动态添加的代码
切面 切点+通知
连接点 切点的定义

AOP的实现

AOP 实际上是基于 Java 动态代理来实现的。

Java 中的动态代理有两种实现方式:cglibjdk

动态代理

基于 JDK 的动态代理

定义一个计算器接口

public interface MyCalculator {

    void add(int a, int b);

}

定义计算机接口的实现

@Service
public class MyCalculatorImpl implements MyCalculator{
@Override
public void add(int a, int b) {
System.out.println(a + "+" + b + "=" + (a + b));;
}
}

定义代理类

public class CalculatorProxy {

    public static Object getInstance(final MyCalculatorImpl myCalculator) {
return Proxy.newProxyInstance(CalculatorProxy.class.getClassLoader(), myCalculator.getClass().getInterfaces(), new InvocationHandler() {
/**
* @param proxy 代理对象
* @param method 代理的方法
* @param args 方法的参数
* @return
* @throws Throwable
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("方法开始执行");
Object invoke = method.invoke(myCalculator, args);
System.out.println("方法结束执行");
return invoke;
}
});
} }

Proxy.newProxyInstance 方法接收三个参数

  • 第一个是一个 classloader,即类加载器。
  • 第二个是代理多项实现的接口。
  • 第三个是代理对象方法的处理器,所有要额外添加的行为都在 invoke 方法中实现。

方法调用的 invoke 方法有两个参数

  • 第一个是执行方法的对象。
  • 第二个是方法的参数数组。

调用方法

MyCalculatorImpl myCalculatorImpl = new MyCalculatorImpl();
MyCalculator myCalculator = (MyCalculator) CalculatorProxy.getInstance(myCalculatorImpl);
myCalculator.add(1, 2);

打印结果

方法开始执行
1+2=3
方法结束执行

五种通知

Spring 中的 Aop 的通知类型有 5 种

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

在项目中,引入 Spring 依赖(这次需要引入 Aop 相关的依赖)

<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
</dependency>

定义切点,这里介绍两种切点的定义方式

  • 使用自定义注解
  • 使用规则

其中,使用自定义注解标记切点,是侵入式的,所以这种方式在实际开发中不推荐,仅作为了解。

另一种使用规则来定义切点的方式,无侵入,一般推荐使用这种方式。

自定义注解

自定义一个注解

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Action {
}

在需要拦截的方法上,添加该注解

在 reduce 方法上添加了 @Action 注解,表示该方法将会被 Aop 拦截,而其他未添加该注解的方法则不受影响

@Service
public class MyCalculatorImpl implements MyCalculator{ @Override
public void add(int a, int b) {
System.out.println(a + "+" + b + "=" + (a + b));;
} @Override
@Action
public int reduce(int a, int b) {
int result = a - b;
System.out.println(a + "-" + b + "=" + result);
return result;
} }

接下来,定义增强(通知、Advice)

@Component
@Aspect //标注这是一个切面
public class LogAspect { /**
* @param joinPoint 包含了目标方法的关键信息
* @Before 注解表示这是一个前置通知,即在目标方法执行之前执行,注解中,需要填入切点
*/
@Before(value = "@annotation(Action)")
public void before(JoinPoint joinPoint) {
Signature signature = joinPoint.getSignature();
String methodName = signature.getName();
System.out.println(methodName + "方法要执行了");
} /**
* 后置通知
* @param joinPoint 包含了目标方法的所有关键信息
* @After 表示这是一个后置通知,即在目标方法执行之后执行
*/
@After(value = "@annotation(Action)")
public void after(JoinPoint joinPoint) {
Signature signature = joinPoint.getSignature();
String methodName = signature.getName();
System.out.println(methodName + "方法执行结束了");
} /**
* @param joinPoint
* @param r 返回值
* @AfterReturning 表示这是一个返回通知,即有目标方法有返回值的时候才会触发
* 该注解中的 returning 属性表示目标方法返回值的变量名,这个需要和参数一一对应
* 注意:目标方法的返回值类型要和这里方法返回值参数的类型一致,否则拦截不到,如果想拦截所有(包括返回值为 void),则方法返回值参数可以为 Object
*/
@AfterReturning(value = "@annotation(Action)", returning = "r")
public void afterReturning(JoinPoint joinPoint, Integer r) {
Signature signature = joinPoint.getSignature();
String methodName = signature.getName();
System.out.println(methodName + "方法返回了" + r);
} /**
* 异常通知
* @param joinPoint
* @param e 目标方法所抛出的异常
* 注意,这个参数必须是目标方法所抛出的异常或者所抛出的异常的父类,只有这样,才会捕获
* 如果想拦截所有,参数类型声明为 Exception
*/
@AfterThrowing(value = "@annotation(Action)", throwing = "e")
public void afterThrowing(JoinPoint joinPoint, Exception e) {
Signature signature = joinPoint.getSignature();
String methodName = signature.getName();
System.out.println(methodName + "方法抛出了异常:" + e.getMessage()); } /**
* 环绕通知
*
* 环绕通知是集大成者,可以用环绕通知实现上面的四个通知,这个方法的核心有点类似于在这里通过反射执行方法
* @param pjp
* @return 注意这里的返回值类型最好是 Object ,和拦截到的方法相匹配
*/
@Around(value = "@annotation(Action)")
public Object around(ProceedingJoinPoint pjp) {
Object proceed = null;
try {
//这个相当于 method.invoke 方法,我们可以在这个方法的前后分别添加日志,就相当于是前置/后置通知
proceed = pjp.proceed();
} catch (Throwable throwable) {
throwable.printStackTrace();
}
return proceed;
} }

在配置类中,开启包扫描和自动代理

@Configuration
@ComponentScan
@EnableAspectJAutoProxy // 开启自动代理
public class JavaConfig {
}

开启调用

@Resource
private MyCalculator myCalculator; @Test
public void testAutoProxy() {
myCalculator.add(1, 2);
myCalculator.reduce(5, 2);
}

打印结果

1+2=3
reduce方法要执行了
5-2=3
reduce方法返回了3
reduce方法执行结束了

LogAspect 切面中,切点的定义是不够灵活的,切点直接写在注解里边,如果要修改切点,每个方法的注解都要修改。

我们可以将切点统一定义,然后统一调用。

@Pointcut("@annotation(Action)")
public void pointCut() {
} @Before("pointCut()")
public void before(JoinPoint joinPoint) {
}

不过,使用注解是侵入式的,还可以改为非侵入式的。

下面这种方式是更为通用的拦截方式

/**
* 可以统一定义切点
* 第一个 * 表示要拦截的目标方法返回值任意(也可以明确指定返回值类型)
* 第二个 * 表示包中的任意类(也可以明确指定类)
* 第三个 * 表示类中的任意方法
* 最后面的两个点表示方法参数任意,个数任意,类型任意
*/
@Pointcut("execution(* cn.sail.training.spring.aop.*.*(..))")
public void pointCut() { }

XML 配置 AOP

定义切面,不再需要注解

public class LogAspectXml {

    public void before(JoinPoint joinPoint) {
Signature signature = joinPoint.getSignature();
String methodName = signature.getName();
System.out.println(methodName + "方法要执行了");
} public void after(JoinPoint joinPoint) {
Signature signature = joinPoint.getSignature();
String methodName = signature.getName();
System.out.println(methodName + "方法执行结束了");
} public void afterReturning(JoinPoint joinPoint, Integer r) {
Signature signature = joinPoint.getSignature();
String methodName = signature.getName();
System.out.println(methodName + "方法返回了" + r);
} public void afterThrowing(JoinPoint joinPoint, Exception e) {
Signature signature = joinPoint.getSignature();
String methodName = signature.getName();
System.out.println(methodName + "方法抛出了异常:" + e.getMessage()); } public Object around(ProceedingJoinPoint pjp) {
Object proceed = null;
try {
//这个相当于 method.invoke 方法,我们可以在这个方法的前后分别添加日志,就相当于是前置/后置通知
proceed = pjp.proceed();
} catch (Throwable throwable) {
throwable.printStackTrace();
}
return proceed;
} }

在 XML 中配置 AOP

<bean class="cn.sail.aop.LogAspectXml" id="logAspectXml"/>
<aop:config>
<aop:pointcut id="pointCut1" expression="execution(* cn.sail.aop.*.*(..))"/>
<aop:aspect ref="logAspectXml">
<aop:before method="before" pointcut-ref="pointCut1"/>
<aop:after method="after" pointcut-ref="pointCut1"/>
<aop:after-returning method="afterReturning" pointcut-ref="pointCut1" returning="r"/>
<aop:after-throwing method="afterThrowing" pointcut-ref="pointCut1" throwing="e"/>
<aop:around method="around" pointcut-ref="pointCut1"/>
</aop:aspect>
</aop:config>

JdbcTemplate

JdbcTemplate 是 Spring 利用 AOP 思想封装的 JDBC 操作工具。

添加依赖

<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
</dependency> <dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>

准备数据库

create table grade
(
id int auto_increment comment 'ID'
primary key,
name varchar(30) null comment '名称'
);

准备实体类

public class Grade implements Serializable {

    private static final long serialVersionUID = 5882495741692079263L;

    private int id;

    private String name;

    public int getId() {
return id;
} public void setId(int id) {
this.id = id;
} public String getName() {
return name;
} public void setName(String name) {
this.name = name;
} @Override
public String toString() {
return "Grade{" +
"id=" + id +
", name='" + name + '\'' +
'}';
} public Grade() {
} public Grade(int id, String name) {
this.id = id;
this.name = name;
}
}

Java 配置

@Configuration
public class JdbcConfig { @Bean
DataSource dataSource() {
DriverManagerDataSource driverManagerDataSource = new DriverManagerDataSource();
driverManagerDataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
driverManagerDataSource.setUrl("jdbc:mysql://localhost:3306/test");
driverManagerDataSource.setUsername("username");
driverManagerDataSource.setPassword("password");
return driverManagerDataSource;
} @Bean
JdbcTemplate jdbcTemplate() {
return new JdbcTemplate(dataSource());
} }

调用

public class TestDemo {

    private JdbcTemplate jdbcTemplate;

    @Before
public void before() {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(JdbcConfig.class);
jdbcTemplate = ctx.getBean(JdbcTemplate.class);
} @Test
public void insert() {
jdbcTemplate.update("insert into grade (name) values (?);", "高一");
} @Test
public void update() {
jdbcTemplate.update("update grade set name = ? where id = ?", "大五", 5);
} @Test
public void delete() {
jdbcTemplate.update("delete from grade where id = ?", 5);
} @Test
public void select() {
Grade grade = jdbcTemplate.queryForObject("select * from grade where id = ?", new BeanPropertyRowMapper<>(Grade.class), 1);
System.out.println(grade);
}

在查询时,如果使用了 BeanPropertyRowMapper,要求查出来的字段必须和 Bean 的属性名一一对应。

如果不一样,则不要使用 BeanPropertyRowMapper,此时需要自定义 RowMapper 或者给查询的字段取别名。

@Test
public void select1() {
Grade grade = jdbcTemplate.queryForObject("select * from grade where id = ?", new RowMapper<Grade>() {
@Override
public Grade mapRow(ResultSet resultSet, int i) throws SQLException {
int id = resultSet.getInt("id");
String name = resultSet.getString("name");
return new Grade(id, name);
}
}, 2);
System.out.println(grade);
}

如果要查询多条记录,方式如下

@Test
public void select2() {
List<Grade> gradeList = jdbcTemplate.query("select * from grade", new BeanPropertyRowMapper<>(Grade.class));
System.out.println(gradeList);
}

以上配置,也可以通过 XML 文件来实现。

通过 XML 文件实现只是提供 JdbcTemplate 实例,剩下的代码还是 Java 代码,就是 JdbcConfig 被 XML 文件代替而已。

<bean class="org.springframework.jdbc.datasource.DriverManagerDataSource" id="dataSource2">
<property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/test"/>
<property name="username" value="root"/>
<property name="password" value="Asailing648735"/>
</bean> <bean class="org.springframework.jdbc.core.JdbcTemplate" id="jdbcTemplate">
<property name="dataSource" ref="dataSource2"/>
</bean>

事务

Spring 中的事务主要是利用 AOP 思想,简化事务的配置,可以通过 Java 配置也可以通过 XML 配置。

建表

CREATE TABLE `account` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL,
`money` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;

配置JdbcTemplate

<!--配置数据源-->
<bean class="org.springframework.jdbc.datasource.DriverManagerDataSource" id="dataSource2">
<property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/test"/>
<property name="username" value="root"/>
<property name="password" value="Asailing648735"/>
</bean> <!--配置数据访问模板-->
<bean class="org.springframework.jdbc.core.JdbcTemplate" id="jdbcTemplate">
<property name="dataSource" ref="dataSource2"/>
</bean>

开启自动化扫描

<context:component-scan base-package="cn.sail.transactional"/>

编写持久层

@Repository
public class UserDao { @Resource
private JdbcTemplate jdbcTemplate; public void addMoney(String username, Integer money) {
jdbcTemplate.update("update account set money = money + ? where username = ?", money, username);
} public void reduceMoney(String username, Integer money) {
jdbcTemplate.update("update account set money = money - ? where username = ?", money, username);
} }

编写服务层

@Service
public class UserService { @Resource
private UserDao userDao; public void updateMoney() {
userDao.addMoney("zhangsan", 100);
int i = 1 / 0;
userDao.reduceMoney("lisi", 100);
} }

这里的int i = 1 / 0;是为了让方法报错才写的。

XML配置

配置事务管理器

<bean class="org.springframework.jdbc.datasource.DataSourceTransactionManager" id="dataSourceTransactionManager">
<property name="dataSource" ref="dataSource2"/>
</bean>

配置事务要处理的方法

<tx:advice transaction-manager="dataSourceTransactionManager" id="txAdvice">
<tx:attributes>
<tx:method name="add*"/>
<tx:method name="delete*"/>
<tx:method name="update*"/>
<tx:method name="get*"/>
</tx:attributes>
</tx:advice>

一旦配置了方法名称规则之后,service 中的方法一定要按照这里的名称规则来,否则事务配置不会生效。

配置 Aop

<aop:config>
<aop:pointcut id="pointCut2" expression="execution(* cn.sail.transactional.*.*(..))"/>
<aop:advisor advice-ref="txAdvice" pointcut-ref="pointCut2"/>
</aop:config>

测试

public class TestDemo {

    private JdbcTemplate jdbcTemplate;

    private UserService userService;

    @Before
public void before() {
ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
jdbcTemplate = ctx.getBean(JdbcTemplate.class);
userService = ctx.getBean(UserService.class);
} @Test
public void testTransactional() {
userService.updateMoney();
} }

由于UserService中的updateMoney方法会报错,addMoney的执行结果会被回滚,表的数据保持不变,即实现了事务的效果。

Java 配置

在 XML 中添加如下配置

<tx:annotation-driven transaction-manager="dataSourceTransactionManager"/>

这行代码可以代替如下代码

<tx:advice transaction-manager="dataSourceTransactionManager" id="txAdvice">
<tx:attributes>
<tx:method name="add*"/>
<tx:method name="delete*"/>
<tx:method name="update*"/>
<tx:method name="get*"/>
</tx:attributes>
</tx:advice> <aop:config>
<aop:pointcut id="pointCut2" expression="execution(* cn.sail.transactional.*.*(..))"/>
<aop:advisor advice-ref="txAdvice" pointcut-ref="pointCut2"/>
</aop:config>

添加 @Transactional 注解

此注解表示该方法开启事务。

这个注解也可以放在类上,表示这个类中的所有方法都开启事务。

@Service
public class UserService { @Resource
private UserDao userDao; @Transactional
public void updateMoney1() {
userDao.addMoney("zhangsan", 100);
int i = 1 / 0;
userDao.reduceMoney("lisi", 100);
} }

测试

@Test
public void testTransactional() {
userService.updateMoney1();
}

执行方法,会得到与 XML 配置一样的结果。

Spring 03 切面编程的更多相关文章

  1. 快速高效掌握企业级项目中的Spring面向切面编程应用,外带讲面试技巧

    Spring面向切面编程(AOP)是企业级应用的基石,可以这样说,如果大家要升级到高级程序员,这部分的知识必不可少. 这里我们将结合一些具体的案例来讲述这部分的知识,并且还将给出AOP部分的一些常见面 ...

  2. Spring AOP 切面编程记录日志和接口执行时间

    最近客户现在提出系统访问非常慢,需要优化提升访问速度,在排查了nginx.tomcat内存和服务器负载之后,判断是数据库查询速度慢,进一步排查发现是因为部分视图和表查询特别慢导致了整个系统的响应时间特 ...

  3. Spring 面向切面编程(AOP)

    Spring 系列教程 Spring 框架介绍 Spring 框架模块 Spring开发环境搭建(Eclipse) 创建一个简单的Spring应用 Spring 控制反转容器(Inversion of ...

  4. Spring面向切面编程(AOP)

    1 spring容器中bean特性 Spring容器的javabean对象默认是单例的. 通过在xml文件中,配置可以使用某些对象为多列. Spring容器中的javabean对象默认是立即加载(立即 ...

  5. Spring AOP 切面编程的方法

    spring aop的使用分为两种,一种是使用注解来实现,一种是使用配置文件来实现. 先来简单的介绍一下这两种方法的实现,接下来详细的介绍各处的知识点便于查阅.目录如下: 1.基于注解实现spring ...

  6. 再学习之Spring(面向切面编程)

    一.概念 1.理论 把横切关注点和业务逻辑相分离是面向切面编程所要解决的问题.如果要重用通用功能的话,最常见的面向对象技术是继承(inheritance)或 组成(delegation).但是,如果在 ...

  7. spring aop 切面编程中获取具体方法的方法

    spring 切面编程中获取具体方法的方法 工作中,使用环绕通知,用来捕获异常,然后通过获取方法的返回值,返回不同的数据给到调用方. 由于方法的返回值不同,我们处理异常时,也需要返回不同的格式. 这时 ...

  8. Spring AOP 切面编程实战Demo项目

    为什么会有此项目?在某日,我看博客时,看到了讲面向切面编程的内容,之前也知道spring是面向切面编程的,只是自己没有写过相关的代码,于是决定自己写一个test.但是url拦截器从外部看,和AOP有相 ...

  9. Spring面向切面编程

    在使用面向切面编程时,我们可以在一个地方定义通用的共鞥,但是可以通过声明的方式定义这个功能要以何种方式在何处应用,而无需修改受影响的类.横切关注点可以被模块化为特殊的类,这些类被称为切面.这样的优点是 ...

随机推荐

  1. AI趋势量化系统(Binance升级版)

    更多精彩内容,欢迎关注公众号:数量技术宅,也可添加技术宅个人微信号:sljsz01,与我交流. B圈大跌行情,如何应对? 近期,伴随着美联储持续的加息进程,数字货币市场不论是市场焦点LUNA,还是BT ...

  2. PX4配置过程与踩坑

    0.前言 由于需要在GitHub下载代码,而国内访问受限,可能会出现一些问题,这里建议使用github国内镜像,参看:GitHub国内镜像网站,当然下面会给出具体解决方案. 1.步骤 1.1下载源码: ...

  3. VS Code官方插件集与工具

    如果您也使用VS Code作为CabloyJS项目开发的主编辑器,那么可以参考官方使用的插件集,此外也提供了一些周边工具 这是官方亲测可用的最简插件集,再也不必东奔西走了 插件集 插件名称 用途 Vi ...

  4. 如何利用 RPA 实现自动化获客?

    大家好,我是二哥.前高级技术专家 & 增长黑客,现一枚爱折腾的小小创业者,专注于 RPA & SaaS 软件这块.这次给大家带来如何利用 RPA 实现自动化获客 一.RPA 是什么?难 ...

  5. Idea创建文件夹自动合成一个

    在idea中创建文件夹时,它们总是自动合成一个,如下图: 文件夹自动折叠真的很影响效率,可能会引发一些不经意的失误 解决方法: 取消这个地方的勾选 这样就可以正常创建文件夹了

  6. DAST 黑盒漏洞扫描器 第五篇:漏洞扫描引擎与服务能力

    0X01 前言 转载请标明来源:https://www.cnblogs.com/huim/ 本身需要对外有良好的服务能力,对内流程透明,有日志.问题排查简便. 这里的服务能力指的是系统层面的服务,将扫 ...

  7. 【万字长文】从零配置一个vue组件库

    简介 本文会从零开始配置一个monorepo类型的组件库,包括规范化配置.打包配置.组件库文档配置及开发一些提升效率的脚本等,monorepo 不熟悉的话这里一句话介绍一下,就是在一个git仓库里包含 ...

  8. 「BUAA OO Unit 4 HW16」第四单元总结与课程回顾

    「BUAA OO Unit 4 HW16」第四单元总结与课程回顾 目录 「BUAA OO Unit 4 HW16」第四单元总结与课程回顾 Part 0 第四单元作业架构设计 架构设计概要 AppRun ...

  9. vue在Docker上运行

    Dockerfile # 设置基础镜像 FROM nginx:latest # 定义作者 MAINTAINER test # 将dist文件中的内容复制到 /etc/nginx/html/ 这个目录下 ...

  10. 『现学现忘』Git后悔药 — 27、版本回退介绍

    目录 1.什么版本回退 2.需要了解两个知识点 (1)HEAD是什么 (2)HEAD指针用法 3.git reflog命令介绍 1.什么版本回退 版本回退也可以叫回滚. 若修改过的文件,不仅添加到了暂 ...