一、问题来源
在进行软件设计的时候往往会将一个大的系统按照业务模块拆分成一个个的低耦合、高内聚的模块,分而治之。
如图:
拆分后发现一些问题,这些问题是通用的,跨模块的。比如:
1.日志:对执行某些操作分支判断时输出日志。
2.安全:在执行某些操作前进行权限检查。
3.事务:在方法开始前开始事务,方法结束时提交或者回滚事务。
4.性能统计:统计某些方法的执行时间。
以上这些属于非功能型需求,是多个业务模块都需要的,那么应该如何去写呢?
1.常规解决方法
public class Command {
public void execute(){
Logger logger = Logger.getLogger("");
//1.记录日志
logger.info("XXXX");
//2.性能统计
Long startTime = System.nanoTime();
//3.权限检查
if(!user.hasPreviledge()){
//抛出异常阻断流程
}
//4.开始事务,进行增删改操作
------业务代码-------
//提交事务
commitTransaction();
Long endTime = System.nanoTime();
logger.info("xxxxx");
}
}
上面的代码虽然解决了问题,但是使用起来十分不爽,日志、安全、性能、事务等代码都耦合到了业务代码中,使一个简单的业务逻辑模块代码都异常的复杂,且重复代码充斥在各个业务逻辑中。
那么如何解决解决痛点呢?
1.设计模式:模版方法
首先先来回顾下模版方法:
模版模式是当不变的和可变的行为混在一起的时候通过把不变的行为提取到超类并且定义一个算法的骨架,去除子类中的重复代码,以此摆脱重复不变行为的纠缠。
一堆啰嗦的话,还是看下具体实现:
//定义超类模版公共部分
public abstract class Command {
public void execute(){
Logger logger = Logger.getLogger("");
//1.记录日志
logger.info("XXXX");
//2.性能统计
Long startTime = System.nanoTime();
//3.权限检查
if(!user.hasPreviledge()){
//抛出异常阻断流程
}
//4.开始事务,进行增删改操作
//5.业务代码操作交由具体子类独自实现
doBusiness();
//提交事务
commitTransaction();
Long endTime = System.nanoTime();
logger.info("xxxxx");
}
//具体业务实现,交给对应模块的子类
public abstract void doBusiness();
}
class PlaceOrderCommand extends Command{
public void doBusiness() {
//执行订单操作
}
}
class PaymentCommand extends Command{
public void doBusiness() {
//执行支付操作
}
}
OK,这样一来,代码看起来清爽了好多,各种无关业务的代码在子类中也不复存在了。
调用也很简单,例如:
Command cmd = new PlaceOrderCommand();
cmd.execute();
总结:
仔细观察可以发现,使用这个方法会导致超类会定义所有的操作,要执行哪些非功能性代码,以什么顺序,等等。子类只能默默接受,那么有没有什么方法可以让子类达到定制化的效果,比如只需要进行事务的支持,其他的不需要。
2.设计模式:装饰者模式
内容复习:
装饰者模式可以动态的给一个对象添加一些额外的职责,有效的区分类的核心职责就增加功能而言,装饰者模式比生成子类更加灵活。
废话不多说,上代码:
public interface Command {
public void execute();
}
/**
* 日志类
*/
public class LoggerDecorator implements Command {
Command cmd;
public LoggerDecorator(Command cmd) {
this.cmd = cmd;
}
public void execute() {
Logger logger = Logger.getLogger("");
logger.info("记录日志start");
this.cmd.execute();
logger.info("记录日志end");
}
}
/**
* 性能统计
*/
class PerformanceDecorator implements Command {
Command cmd;
public PerformanceDecorator(Command cmd) {
this.cmd = cmd;
}
public void execute() {
PerformanceUtil.startTimer();
this.cmd.execute();
PerformanceUtil.endTimer();
}
}
/**
* 执行订单操作,具体业务代码
*/
class PlaceOrderDecorator implements Command {
Command cmd;
public PlaceOrderDecorator(Command cmd) {
this.cmd = cmd;
}
public void execute() {
//执行支付
}
}
现在就像递归一样层层调用,可以自由的决定让PlaceOrderDecorator可以选择打印日志或者性能统计等等。
Command cmd = new LoggerDecorator(
new PerformanceDecorator(new PlaceOrderDecorator()));
cmd.execute();
如果只需要其中部分功能只需要实例化对应的模块即可,例如只需要订单操作时打印日志
Command cmd = new LoggerDecorator(
new PlaceOrderDecorator());
cmd.execute();
总结:
装饰者模式虽然可以通过装饰器灵活的使用各种方法,但是由此也带出了两个问题:
1.一个处理日志/安全/事务/性能统计的类为什么要去实现业务接口呢?
2.如果其他模块没有实现业务接口,但是想使用日志/安全/事务/性能统计等功能,那该怎么办呢?
3.代理模式
Ⅰ.介绍:
为其他对象提供一种代理以此来控制这个对象的访问,使客户端不直接引用对象,很好的在客户端和目标对象之间起到中介的作用。
Ⅱ.适用场景:
- 远程代理,为一个对象在不同的地址空间提供局部代表。这样即使远程方法在服务器,但是本地调用其代理类,内部屏蔽了远程调用过程,对于客户端而言与本地方法无异。(如:webService)
- 虚拟代理,占用系统资源较多或者加载时间较长的对象,可以给这些对象提供一个虚拟代理。在真实对象创建成功之前虚拟代理扮演真实对象的替身,而当真实对象创建之后,虚拟代理将用户的请求转发给真实对象。(比如:HTML加载时,有很多图片,为了及时的响应页面,可以先将图片位置展示代理对象,待缓存完成时,再展示出来;当一个对象加载耗费资源很大时,利用虚拟代理,将那些耗费资源的操作直到具体使用时再加载,在此之前使用虚拟代理对象来代替。可达到资源重用,节省内存,以时间换空间。;参考浏览器加载可在因io阻塞导致对象加载缓慢的时候,结合多线程技术先行一步创建加载较快的代理对象,另起线程加载真实对象,先行返回代理对象,当调用真实对象时,此时资源可能已经加载完毕,以此来加快相应时间。)
- 安全代理, 也叫保护代理,用来控制真实对象访问时的权限,如果有必要的话,可以给不同调用者提供不同的权限。
- **智能指引,**是指当调用真实对象时,代理处理另外一些事,比如记录对此对象的调用次数等。
了解了代理模式及使用场景后,此处我们使用其中的智能指引的这种场景,既在代理类中对被代理的类添加一些功能。
代码:
public interface Command {
public void execute();
}
public class PlaceOrderDecorator implements Command {
public void execute() {
System.out.println("执行支付");
}
}
/**
* 代理类
*/
class CommandProxy implements Command {
Command cmd;
public CommandProxy(Command cmd) {
this.cmd = cmd;
}
public void execute() {
System.out.println("记录日志start");
this.cmd.execute();
System.out.println("记录日志end");
}
}
class Client {
public static void main(String[] args) {
Command cmd = new CommandProxy(new PlaceOrderDecorator());
cmd.execute();
}
}
总结:
1、代理模式可以在不修改初始类的前提下对其功能进行了增强。
2、接口中有多少方法,在proxy层就得实现多少方法,有多少方法就要开启和提交多少事务,代码繁琐复杂。
3、如果一个proxy实现了多个接口,如果其中的一个接口发生变化(添加了一个方法),那么proxy也要做改变。
4、静态代理模式并没有做到日志的重用
由此引出今天的主角,动态代理。
4.动态代理
Ⅰ、JDK动态代理
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
public class Interceptor implements InvocationHandler{
Transaction transaction;
Object target;
public Interceptor(Transaction transaction, Object target) {
this.transaction = transaction;
this.target = target;
}
/**
* @param proxy 目标对象的代理类实例
* @param method 对应于在代理实例上调用接口方法的Method实例
* @param args 传入到代理实例上方法参数值的对象数组
* @return 方法的返回值,没有返回值是null
* @throws Throwable
*/
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
String methodName = method.getName();
if("execute".equals(methodName)){
transaction.beginTransaction();
method.invoke(target);
transaction.commit();
}else{
method.invoke(target);
}
return null;
}
}
/**
* 事务类
*/
class Transaction{
void beginTransaction(){
System.out.println("******** 开启事务 **************");
}
void commit(){
System.out.println("********* 提交事务 ************");
}
}
/*
* 测试类
*/
@Test
public void testSave(){
/**
* 1、创建一个目标对象
* 2、创建一个事务
* 3、创建一个拦截器
* 4、动态产生一个代理对象
*/
Object target = new PlaceOrderDecorator();
Transaction transaction = new Transaction();
Interceptor proxy = new Interceptor(transaction,target);
/**
* 参数一:设置代码使用的类加载器,一般采用跟目标类相同的类加载器
* 参数二:设置代理类实现的接口,跟目标类使用相同的接口
* 参数三:设置回调对象,当代理对象的方法被调用时,会调用该参数指定对象的invoke方法
*/
Command com = (Command) Proxy.newProxyInstance(
target.getClass().getClassLoader(),
target.getClass().getInterfaces(),proxy);
com.execute();
}
基于上述流程进行优化:
public class ProxyFactory {
Transaction transaction = new Transaction();
Object target;
public ProxyFactory(Object target) {
this.target = target;
}
/**
* 生成代理类
*/
public Object getProxyInstance (){
return Proxy.newProxyInstance(target.getClass().getClassLoader(),
target.getClass().getInterfaces(), new InvocationHandler() {
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
String methodName = method.getName();
if("execute".equals(methodName)){
transaction.beginTransaction();
method.invoke(target,args);
transaction.commit();
}else{
method.invoke(target);
}
return null;
}
});
}
}
@Test
public void testProxy(){
Object target = new PlaceOrderDecorator();
Command com = (Command)new ProxyFactory(target).getProxyInstance();
com.execute();
}
总结:
静态代理与动态代理的区别:
- 静态代理在编译时就已经实现,编译完成后代理类是一个实际的class文件。
- 动态代理是在运行时动态生成的,即编译完成后没有实际的class文件,而是在运行时动态生成类字节码,并加载到JVM中。
特点:
- 因为使用了JDKProxy生成的代理类实现了接口,所以目标类中所有的方法在代理类中会有。
- 动态代理对象不需要实现接口,但是要求目标对象必须实现接口,否则不能使用动态代理。
- invoke方法中的三个参数可以访问目标类的被调用方法的API、被调用方法的参数、被调用方法的返回类型。
缺点:
1.在拦截器中除了能调用目标对象的目标方法之外功能比较单一。
2.在使用拦截器中的invoke方法的if判断语句进行功能增强的时候具体实施时因为要支持日志/安全/事务/性能统计等功能,if语句需要写很多,这无疑是个灾难。
Ⅰ、CGLIB动态代理
public class CglibProxyFactory implements MethodInterceptor{
private Object target;
Transaction transaction;
public CglibProxyFactory(Object target, Transaction transaction) {
this.target = target;
this.transaction = transaction;
}
public Object getProxyInstance(){
//工具类
Enhancer en = new Enhancer();
//设置父类
en.setSuperclass(target.getClass());
//设置回调函数 拦截调用intercept函数
en.setCallback(this);
//创建子类对象代理
return en.create();
}
public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
transaction.beginTransaction();
// 执行目标对象的方法
Object returnValue = method.invoke(target, args);
transaction.commit();
return null;
}
}
//测试类
@Test
public void testCGlibProxy(){
PlaceOrderDecorator target = new PlaceOrderDecorator();
System.out.println(target.getClass());
PlaceOrderDecorator proxy = (PlaceOrderDecorator)new CglibProxyFactory(target,new Transaction())
.getProxyInstance();
System.out.println(proxy.getClass());
proxy.execute();
}
输出结果:
class PlaceOrderDecorator
class PlaceOrderDecorator$$EnhancerByCGLIB$$640bbc4f
******** 开启事务 **************
执行支付
********* 提交事务 ************
总结:
1.JDK动态代理需要目标对象和代理对象实现业务接口,拦截器需要实现InvocationHandler接口,其中invoke方法体中内容是具体代理对象方法体内容。
2.CGlib代理的目标类不需要实现接口,代理类是目标类的子类,拦截器需要实现MethodInterceptor接口,其中重写intercept方法时,进行具体功能增强操作。但是因为cglib是依靠继承目标对象重写其方法,所以目标对象不得是final类!!!