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有两个目的(或其中之一):

  1. 不修改已有代码进行功能增强
  2. 剔除方法中的非核心逻辑,精简代码

二. 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是怎么利用代理做的呢?

  1. 用户编写增强类,做计时处理
  2. 利用Java的反射功能扫描com.baidu.waimai.service包下的所有类
  3. 对每一个类进行动态代理生成子类,覆写父类方法,在覆写方法中回调用户的增强类
  4. 返回代理子类
  5. 用户调用子类的覆写方法时,实际上会调用增强类的方法

下面以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实现了对RuntimeExceptionError异常进行重试,重试间隔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容器实现无修改或少量修改去增强已有代码,使得已有代码内容紧凑,降低代码耦合。十分推荐大家去使用!

posted @ 2016-12-19 00:56  -六月飞雪-  阅读(3079)  评论(0编辑  收藏  举报