Java面向切面原理与实践
Java面向切面原理与实践
一. 面向切面编程是什么
首先用一句话概括:面向切面编程(AOP)就是对某些具有相似点的代码进行增强。
相似点可以是同一个包、使用相同的注解、public的方法、以Impl结尾的类名等等。这些相似点也叫切点,我们可以想象一堆密密麻麻的切点在二维空间上排列,组成了一个面,这个面就叫切面,所以切面也是一堆相似代码的集合。
我们在开发时经常因为业务变更去修改已有的代码,这样做不满足设计模式的封闭-开放原则。修改已有代码可能有风险,也可能会让已有代码变得不好维护、逻辑变得复杂,因此我们不想去修改已有代码,同时还想要对已有代码进行功能性的增强。AOP就是要解决这类问题出现的。
举个例子:现在有个注册用户的方法如下所示
public boolean regist(String username, String password) {
return userDao.regist(username, password);
}
假如业务变更,我们需要去对参数进行校验,于是封装了一个Assert类:
public boolean regist(String username, String password) {
Assert.notEmpty(username);
Assert.notEmpty(password);
Assert.regexMatch(username, "^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+$");
return userDao.regist(username, password);
}
假如以后需要加上事务、分布式锁功能都在regist方法中写的话就会导致业务逻辑复杂,实际上真正做业务逻辑的只是调用userDao的regist方法一行代码而已。
AOP有两个目的(或其中之一):
- 不修改已有代码进行功能增强
- 剔除方法中的非核心逻辑,精简代码
二. Java面向切面原理
Java实现AOP一般是有两种实现方式,一种是静态代理,一种是动态代理。
(一) 静态代理
静态代理指的是通过与某个类或接口强绑定从而去实现代理模式。根据下面的代码,可以发现继承可以天然地实现增强,类似的门面模式也是属于静态代理。
public CheckUserService extends UserService {
@Override
public boolean regist(String username, String password) {
check(username, password); // 前置增强
boolean success = super.regist(username, password);
logger.info("注册结果: " + success); // 后置增强
return success;
}
...
}
静态代理一般用来实现拦截器,通常出现在表现层框架中。
(二) 动态代理
动态代理可以不与某个类或接口强绑定,要说明动态代理首先得了解一下Java类加载相关的原理。
Java是个编译型的语言,首先会把Java代码编译成class字节码,然后JVM去加载、解释字节码,通过ClassLoader类可以在程序运行期间动态的加载字节码生成一个类。
动态代理的原理就是在程序运行期间动态的生成一个比特数组,这个数组能够表示为目标类的子类,然后把数组交给ClassLoader进行解析,并返回子类的实例,这个子类的实例实际上可以看做目标类的代理类。以静态代理中的代码例子来说,动态代理根据UserService类在运行时生成了CheckUserService,而增强的代码其实就是子类实现的regist方法。
当前Java实现动态代理有两种方式,一种是JDK自带的动态代理,要求目标类必须实现一个接口,接口方法就是增强方法;另一种是CGLIB动态代理,要求目标类不能为final,否则不能生成子类。

简单来说,所谓代理就是利用面向对象的多态性去生成一个子类,把子类当作父类来使用,同时子类覆写了父类的方法,从而达到对父类方法进行增强或改变行为的效果。
(三) 面向切面编程与代理
假如现在要对com.baidu.waimai.service包下的所有文件增加计时日志,AOP是怎么利用代理做的呢?
- 用户编写增强类,做计时处理
- 利用Java的反射功能扫描
com.baidu.waimai.service包下的所有类 - 对每一个类进行动态代理生成子类,覆写父类方法,在覆写方法中回调用户的增强类
- 返回代理子类
- 用户调用子类的覆写方法时,实际上会调用增强类的方法

下面以CGLIB简单展示计时增强:
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(UserService.class);
enhancer.setCallback(new MethodInterceptor() {
@Override
public Object intercept(Object o, Method method, Object[] args,
MethodProxy methodProxy) throws Throwable {
long startTime = System.currentTimeMillis();
Object result = methodProxy.invokeSuper(o, args);
long endTime = System.currentTimeMillis();
logger.info("costTime: " + (endTime - startTime));
return result;
}
});
其中Enhancer就是增强类,当UserService的代理子类的public方法被调用时,都会走上面的intercept方法,然后由methodProxy的invokeSuper方法去真正调用父类的方法。
(四) 面向切面编程与Bean容器
只是拥有代理还不能实现不修改已有代码进行增强,我们还得在实例化父类的地方改成实例化代理子类,因此AOP经常与Bean容器结合使用。例如使用Spring框架时,我们会在XML中配置切面、增强,获取Bean的时候当作父类来处理就行,当切面、增强需要修改的时候可以只需要修改XML配置和增强类,不需要修改已有的业务代码。如下代码所示定义了切点为com.baidu.waimai.service包下的所有public方法,增强为costTimeAdvice。
<aop:config proxy-target-class="true">
<aop:pointcut id="costTimePointCut" expression="execution(public * com.baidu.waimai.service.*(..))"/>
<aop:advisor pointcut-ref="costTimePointCut" advice-ref="costTimeAdvice"/>
</aop:config>
三. 实践
(一) 日志
此功能已展示,不再赘述。
(二) 重试
通常在连接数据库或者调用远程服务时,可能由于各种原因会失败,因此我们想要在这些代码加上重试功能,可能还要判断哪些异常需要重试哪些不需要重试、重试次数、重试睡眠时间等等操作,这些代码都写在一个方法里就会大大增加耦合。
如下代码只通过增加一个@Retryable实现了对RuntimeException、Error异常进行重试,重试间隔1秒的重试功能,这种方式使得adhocQuery的方法体的业务代码更加清晰。
@Retryable(include = {RuntimeException.class, Error.class}, backoff = @Backoff(1000))
public void adhocQuery(String sql) {
...
}
另一种方式是在XML配置<aop:config>标签指明重试的切面和增强,这种方式能不修改adhocQuery方法。
这两种方式的好坏就是见仁见智了,个人认为增加注解的方式要更加方便一些。
(三) 缓存
缓存也是一个绝佳的需要AOP改造的功能!想想假如当前我们用ConcurrentHashMap,我们想要改成Redis或者Hbase做缓存,缓存判断代码和获取数据的代码挤在一个方法里,我们就不得不改一大段代码了。有了AOP就会十分简单。
@Cacheable(value = "sqlResultCache", cacheManager = "redisCacheManager")
public void adhocQuery(String sql) {
...
}
在cacheManager 中可以定义数据存储时间、并发数、垃圾回收策略等等,不影响adhocQuery的核心逻辑。
(四) 参数校验
如下代码,参数校验修改到了User bean中,regist方法增加了一个@Valid即可,推荐在Bean中加上校验注解。
public boolean regist(@Valid User user) {
...
}
public class User {
@NotEmpty("{\"status\": 301, \"msg\": \"用户名不能为空\"}")
@Pattern(
regexp = "^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+$",
message = "{\"status\": 201, \"msg\": \"用户名必须是邮箱格式\"}"
)
private String username;
@NotEmpty("{\"status\": 302, \"msg\": \"密码不能为空\"}")
private String password;
...
}
(五) 异常处理
用AOP处理异常通常是为了防止异常抛出到前端界面、统一记录异常日志。
(六) SQL映射
MyBatis框架使用AOP来处理SQL映射,把数据访问对象层的方法映射到配置文件的SQL,这样做的好处是SQL和Java文件分开容易DBA对SQL进行优化,当SQL需要变更时不需要修改代码。
(七) 事务
最普通的情况下使用事务时可能是这样的:
public void regist(String username) {
Connection connection = null;
try {
connection = connectionPool.getConnection();
// do Something
connection.commit(); // 提交
} catch (Throwable t) {
if (connection != null) {
connection.rollback(); // 回滚
}
throw t;
} finally {
if (connection != null) {
connection.close(); // 归还连接池
}
}
}
事务通常从进入业务逻辑层开始,退出业务逻辑层结束,通过Spring XML是这么做:
<!-- 事务增强 -->
<tx:advice id="txAdvice" transaction-manager="txManager">
<tx:attributes>
<tx:method name="*" propagation="REQUIRED" />
</tx:attributes>
</tx:advice>
<!-- 事务切面 -->
<aop:config proxy-target-class="true">
<aop:pointcut id="txPointCut" expression="execution(public * com.baidu.waimai.service.*(..))"/>
<aop:advisor pointcut-ref="txPointCut" advice-ref="txAdvice"/>
</aop:config>
这样能够自动在service包中的所有public方法打开事务、提交、回滚、归还连接。当然这样一刀切的打开事务并不好,因此要注意配置好切点。
改造后的regist方式是这样:
public void regist(String username) {
// do Something
}
(八) HTTP客户端
由于AOP通常是利用动态代理实现的,因此我们可以只定义接口,让增强去实现具体的子类。如下代码,增强代码将会根据注解去发送HTTP请求,自动处理类型转换和异常捕获,大幅度减少代码量。
@SophieClient(
value = "adhocService",
proxy = "adhocSohpieProxy",
url = "http://aaaa:8288/bbb/rest"
)
public interface AdhocService {
@RequestMapping(value = "sql", method = RequestMethod.POST)
JSONArray query(
@RequestParam("username") String username,
@RequestParam("sql") String sql,
@RequestParam("queryName") String queryName,
@RequestParam("useHive") Boolean useHive,
@RequestParam("useGPDB") Boolean useGPDB);
}
四. 总结
面向切面编程是在运行时生成代理子类覆写父类的方法去回调增强方法,结合Bean容器实现无修改或少量修改去增强已有代码,使得已有代码内容紧凑,降低代码耦合。十分推荐大家去使用!
Java面向切面原理与实践的更多相关文章
- Java 面向切面 AOP
参考: :http://www.blogjava.net/supercrsky/articles/174368.html AOP: Aspect Oriented Programming 即面向切面编 ...
- Java 面向切面编程(Aspect Oriented Programming,AOP)
本文内容 实例 引入 原始方法 装饰者模式 JDK 动态代理和 cglib 代理 直接使用 AOP 框架--AspectWerkz 最近跳槽了,新公司使用了 AOP 相关的技术,于是查点资料,复习一下 ...
- java面向切面编程总结-面向切面的本质
面向切面的本质:定义切面类并将切面类的功能织入到目标类中: 实现方式:将切面应用到目标对象从而创建一个新的代理对象的过程.替换: 使用注解@Aspect来定义一个切面,在切面中定义切入点(@Point ...
- 深入理解JVM虚拟机11:Java内存异常原理与实践
本文转自互联网,侵删 本系列文章将整理到我在GitHub上的<Java面试指南>仓库,更多精彩内容请到我的仓库里查看 https://github.com/h2pl/Java-Tutori ...
- Java实战之03Spring-03Spring的核心之AOP(Aspect Oriented Programming 面向切面编程)
三.Spring的核心之AOP(Aspect Oriented Programming 面向切面编程) 1.AOP概念及原理 1.1.什么是AOP OOP:Object Oriented Progra ...
- Atitit 表达式原理 语法分析 原理与实践 解析java的dsl 递归下降是现阶段主流的语法分析方法
Atitit 表达式原理 语法分析 原理与实践 解析java的dsl 递归下降是现阶段主流的语法分析方法 于是我们可以把上面的语法改写成如下形式:1 合并前缀1 语法分析有自上而下和自下而上两种分析 ...
- Method Swizzling和AOP(面向切面编程)实践
Method Swizzling和AOP(面向切面编程)实践 参考: http://www.cocoachina.com/ios/20150120/10959.html 上一篇介绍了 Objectiv ...
- Java基于自定义注解的面向切面的实现
目的:实现在任何想要切的地方添加一个注解就能实现面向切面编程 自定义注解类 @Target({ElementType.PARAMETER, ElementType.METHOD}) @Retentio ...
- C# 中使用面向切面编程(AOP)中实践代码整洁
1. 前言 最近在看<架构整洁之道>一书,书中反复提到了面向对象编程的 SOLID 原则(在作者的前一本书<代码整洁之道>也是被大力阐释),而面向切面编程(Aop)作为面向对象 ...
随机推荐
- HTML、CSS规范
作为一名前端开发者,至少要对HTML.CSS规范有个了解,然后尝试在项目是使用,以便别人阅读你代码的时候,也相对轻松点. HTML.CSS规范,参见:编码规范 by @mdo JavaScript 参 ...
- servlet关于转发用法
# 1.转发 ## (1)什么是转发? 一个web组件将未完成的处理交给另外一个web组件继续做. 注: web组件(servlet/jsp) 最常见的情况: ...
- luogu P4430 小猴打架(prufer编码与Cayley定理)
题意 n个点问有多少种有顺序的连接方法把这些点连成一棵树. (n<=106) 题解 了解有关prufer编码与Cayley定理的知识. 可知带标号的无根树有nn-2种.然后n-1条边有(n-1) ...
- SSIS安装以及安装好找不到商业智能各种坑
原文:SSIS安装以及安装好找不到商业智能各种坑 这两天为了安装SSIS,各种头疼.记录一下,分享给同样遇到坑的.. 安装SSIS需要几个步骤. 先说一下我的情况,安装SQL的时候,一直默认下一步,没 ...
- 常用Java开源库(新手必看)
Jakarta common: Commons LoggingJakarta Commons Logging (JCL)提供的是一个日志(Log)接口(interface),同时兼顾轻量级和不依赖于具 ...
- 不安装Oracle客户端,用plsql连接远程Oracle数据库(绝对解决你的问题)
1,首先准备下载两个软件,一个是instantclient.zip,另一个是plsql安装包.但是得确定您的电脑是32位还是64位,我这边提供了32位和64位的供您下载: 百度网盘:https://p ...
- tomcat怎样禁止显示文件夹和文件列表
查看原文:http://www.ibloger.net/article/300.html Tomcat禁止显示文件夹和文件列表 打开 tomcat的安装文件夹/conf/web.xml 文件 &l ...
- Swift环境下实现UILabel居上 居中 居下对齐
首先在Xcode中新建.h文件,将下面代码复制进去 // // myUILabel.h // // // Created by yexiaozi_007 on 3/4/13. // Copyright ...
- NSTimer解除循环引用
NSTimer作为一个经常使用的类,却有一个最大的弊病,就是会强引用target.造成调用timer很麻烦.稍有不慎就造成内存泄漏. 下面就是为解决问题做的封装. 直接上代码: #import < ...
- UVA10491 - Cows and Cars(概率)
UVA10491 - Cows and Cars(概率) 题目链接 题目大意:给你n个门后面藏着牛.m个门后面藏着车,然后再给你k个提示.在你作出选择后告诉你有多少个门后面是有牛的,如今问你作出决定后 ...