@22221cjp
2016-09-18T12:17:59.000000Z
字数 17126
阅读 944
技术
先来一个接口Moveable和类Tank,Tank实现了Moveable接口
-----------------------------------------Moveable-----------------------------------package com.chen.proxy;public interface Moveable {void move();}-----------------------------------------Tank-----------------------------------package com.chen.proxy;import java.util.Random;public class Tank implements Moveable {@Overridepublic void move() {System.out.println("Tank moving...");try {Thread.sleep(new Random().nextInt(10000));} catch (InterruptedException e) {e.printStackTrace();}}}
现在有一个需求,我想记录一下Tank这个类中move方法的运行时间,常用的设计方法有两种:1.继承 2.组合。代码分别如下:
1. 继承(定义Tank1类,继承Tank,然后重写其中的move方法)
package com.chen.proxy;public class Tank1 extends Tank {@Overridepublic void move() {long start = System.currentTimeMillis();super.move();long end = System.currentTimeMillis();System.out.println("move time: " + (end - start));}}
这样,只需要new一个Tank1的对象,然后调用move方法就能知道Tank中的move方法运行的时间。
2. 组合(定义一个类TankTimeProxy,也实现Moveable接口,此接口中还持有一个Moveable接口的引用。
package com.chen.proxy;public class TankTimeProxy implements Moveable {Moveable m;@Overridepublic void move() {long start = System.currentTimeMillis();m.move();long end = System.currentTimeMillis();System.out.println("move time:" + (end - start));}public TankTimeProxy(Moveable moveable) {this.m = moveable;}}
要想知道Tank中的move方法运行的时间,只要以下测试代码即可。
package com.chen.proxy;public class Test {public static void main(String[] args) {Moveable ttp = new TankTimeProxy(new Tank());ttp.move();}}
以上两种方法哪种比较好?
第二种!只从语法上来说,第二种的TankTimeProxy实现了一个接口,但是还可以从其他的类继承,而第一种已经不能从其他的类继承了,不够灵活。但是这并不是主要的原因,想想更深层原因,如果我现在想再记录一下坦克move的日志,也就是当坦克移动开始的时候打印”move start”,坦克移动结束的时候打印”move stop”,如果采用继承的方法,就需要定义一个Tank2类继承Tank1,然后重写Tank1中的move方法,但是我若想调换这两个需求的位置,先记录坦克移动的日志,然后记录坦克的运行时间,这样就必须从Tank重新继承一个Tank3,先记录一下日志,然后再来一个Tank5继承Tank3,然后重写move方法,在开始和介绍的位置加上时间的记录,可以想象采用继承的方式是相当不灵活的,会导致“类爆炸”,那么采用接口的方式会如何呢?按照上面的思路,我们定义一个TankLogProxy类如下:
----------------------------TankLogProxy-----------------------------package com.chen.proxy;public class TankLogProxy implements Moveable {Moveable m;@Overridepublic void move() {System.out.println("tank start");m.move();System.out.println("tank stop");}public TankLogProxy(Moveable m) {this.m = m;}}
若我想先记录时间,然后记录日志,可以采用如下的代码:
package com.chen.proxy;public class Test {public static void main(String[] args) {Tank tank = new Tank();Moveable tlp = new TankLogProxy(tank);Moveable ttp = new TankTimeProxy(tlp);ttp.move();}}
若反过来,我先记录坦克日志,然后记录时间,则可以采用如下的代码:
package com.chen.proxy;public class Test {public static void main(String[] args) {Tank tank = new Tank();Moveable ttp = new TankTimeProxy(tank);Moveable tlp = new TankLogProxy(ttp);tlp.move();}}
因为TankTimeProxy和TankLogProxy中持有一个Moveable的引用,而TankTimeProxy和TankLogProxy都实现了Moveable接口,所以这两个类的对象都是可以初始化这两个类中的字段 m ,所以上面想调换一下这两个需求的位置只需要初始化的位置调换一下就可以了,这个是相当灵活的。观察一下TankTimeProxy和TankLogProxy这两个类,可以发现,这两个类和Tank都实现了同一个接口Moveable,而这两个类中又都有一个Moveable接口的引用 m ,且在实现move方法的时候,核心的代码逻辑只是简单的调用了 m.move()方法,若将 m 初始化为Tank的对象,那么核心的代码逻辑就是调用Tank对象的move方法,实际上TankTimeProxy对象此时就是一个Tank对象的代理,因为这个代理有名有姓,又是我们自己实现的,是写死了的,所以也叫静态代理。
注意一下静态代理产生的方式:
1. 被代理类必须实现了一个接口
2. 代理类也实现了与被代理类相同的接口,且有一个此接口的引用。
3. 代理类有一个构造函数,其中可以初始化这个引用。
现在只考虑TankTimeProxy这个类,这个类虽然能产生Tank对象的代理,记录Tank对象move方法的运行时间,但是还不够灵活,因为它只能产生Moveable接口对象中的move方法的运行时间,若我想产生任何对象中任何方法的运行时间,该怎么办呢?
刚才的TankTimeProxy是我们写死了的,若我们能将TankTimeProxy这个类动态的生成,然后动态的编译不就能动态的产生一个Tank的时间代理类了么,虽然此时还不能完全实现对任何对象产生时间的代理类,但是已经向这个目标迈进了一步。
定义一个类Proxy(JDK中类名也是这个),其中有一个静态方法getNewInstance,这个方法就用来实现对Tank对象的时间的代理类。
package com.chen.proxy;import java.io.File;import java.io.FileWriter;import java.io.IOException;import java.lang.reflect.Constructor;import java.lang.reflect.InvocationTargetException;import java.net.MalformedURLException;import java.net.URL;import java.net.URLClassLoader;import javax.tools.JavaCompiler;import javax.tools.StandardJavaFileManager;import javax.tools.ToolProvider;import javax.tools.JavaCompiler.CompilationTask;public class Proxy {public static Object getNewInstance() {String src = "package com.chen.proxy;" +"public class TankTimeProxy implements Moveable {" + " Moveable m;" +"@Override" + "\r\n" + "public void move() {"+ "long start = before();" + "m.move();" + "after(start);"+ "}" +"private void after(long start) {" + "long end = before();"+ "System.out.println(\"move time: \" + (end - start));" + "}" +"private long before() {"+ "long start = System.currentTimeMillis();" + "return start;"+ "}" +"public TankTimeProxy(Moveable tank) {" + "this.m = tank;"+ "}" + "}";System.out.println(System.getProperty("user.dir"));String fileName = System.getProperty("user.dir")+ "/src/com/chen/proxy/TankTimeProxy.java";File file = new File(fileName);try {FileWriter fileWriter = new FileWriter(file);fileWriter.write(src);fileWriter.flush();fileWriter.close();} catch (IOException e) {e.printStackTrace();}// 编译JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();StandardJavaFileManager fileManager = compiler.getStandardFileManager(null, null, null);Iterable iterable = fileManager.getJavaFileObjects(fileName);CompilationTask task = compiler.getTask(null, fileManager, null, null,null, iterable);task.call();try {fileManager.close();} catch (IOException e) {e.printStackTrace();}// 加载到内存并实例化URL[] urls;Moveable moveable = null;try {urls = new URL[] { new URL("file:/"+ System.getProperty("user.dir") + "/src/") };URLClassLoader urlClassLoader = new URLClassLoader(urls);Class clazz = urlClassLoader.loadClass("com.chen.proxy.TankTimeProxy");System.out.println(clazz);Constructor constructor = clazz.getConstructor(Moveable.class);moveable = (Moveable) constructor.newInstance(new Tank());} catch (MalformedURLException e) {e.printStackTrace();} catch (ClassNotFoundException e) {e.printStackTrace();} catch (SecurityException e) {e.printStackTrace();} catch (NoSuchMethodException e) {e.printStackTrace();} catch (IllegalArgumentException e) {e.printStackTrace();} catch (InstantiationException e) {e.printStackTrace();} catch (IllegalAccessException e) {e.printStackTrace();} catch (InvocationTargetException e) {e.printStackTrace();}return moveable;}}
这段代码有点长,我们把刚才的TankTimeProxy类中的完全代码拷贝到getNewInstance方法中,并定义成一个字符串,如果我们能把这个字符串动态的编译,那么我们就不需要刚才的TankTimeProxy类了,现在的问题是如何动态的编译一个类的字符串形式呢?有如下几种方法:
1. JDK1.6以上自带了动态编译的api
2. 使用第三方的类库,如:CGLib ASM等,这些工具直接可以生产java的二进制文件,不需要生产中间的XXX.java和XXX.class文件。
这里我们使用的就是第一种。我们把原来的TankTimeProxy删掉,调用getNewInstance方法就可以在src目录下重新生成一个TankTimeProxy类。
此时src目录下只需三个类即可,Moveable接口,Tank类,Proxy类(如上)。
通过以下测试类,可以测试运行结果:
package com.chen.proxy;public class Test {public static void main(String[] args) {Moveable m = (Moveable) Proxy.getNewInstance();m.move();}}
运行的截图如下:

运行完成后,刷新src目录,可以看到动态生成的TankTimeProxy类。需要注意的是,上面需要获得java的编译器,那么项目的运行环境不能为jre1.6,因为jre中是不带java的编译器的,所以需求将项目的运行环境改为jdk1.6,更改方法见博客。
评论:上面代码的确能够动态的生成Tank的时间代理类,但是在字符串src中,接口Moveable是写死了的,我们要产生实现任何接口的动态代理类就不能将接口写死,解决方案很简单,不写死就让接口成为一个参数传入getNewInstance方法,再getNewInstance内通过反射可以得到,这个是可以办到的,所以可以修改getNewInstance方法如下(下面的代码来自视频源码):
package com.bjsxt.proxy;import java.io.File;import java.io.FileWriter;import java.lang.reflect.Constructor;import java.lang.reflect.Method;import java.net.URL;import java.net.URLClassLoader;import javax.tools.JavaCompiler;import javax.tools.StandardJavaFileManager;import javax.tools.ToolProvider;import javax.tools.JavaCompiler.CompilationTask;public class Proxy {public static Object newProxyInstance(Class infce) throws Exception { //JDK6 Complier API, CGLib, ASMString methodStr = "";String rt = "\r\n";Method[] methods = infce.getMethods();//通过反射拿到方法名称,然后在前面和后面加上记录时间的代码for(Method m : methods) {methodStr += "@Override" + rt +"public void " + m.getName() + "() {" + rt +" long start = System.currentTimeMillis();" + rt +" System.out.println(\"starttime:\" + start);" + rt +" t." + m.getName() + "();" + rt +" long end = System.currentTimeMillis();" + rt +" System.out.println(\"time:\" + (end-start));" + rt +"}";}String src ="package com.bjsxt.proxy;" + rt +"public class $Proxy1 implements " + infce.getName() + "{" + rt +" public $Proxy1(Moveable t) {" + rt +" this.t = t;" + rt +" }" + rt +" Moveable t;" + rt +methodStr +"}";String fileName ="d:/src/com/bjsxt/proxy/$Proxy1.java";File f = new File(fileName);FileWriter fw = new FileWriter(f);fw.write(src);fw.flush();fw.close();//compileJavaCompiler compiler = ToolProvider.getSystemJavaCompiler();StandardJavaFileManager fileMgr = compiler.getStandardFileManager(null, null, null);Iterable units = fileMgr.getJavaFileObjects(fileName);CompilationTask t = compiler.getTask(null, fileMgr, null, null, null, units);t.call();fileMgr.close();//load into memory and create an instanceURL[] urls = new URL[] {new URL("file:/" + "d:/src/")};URLClassLoader ul = new URLClassLoader(urls);Class c = ul.loadClass("com.bjsxt.proxy.$Proxy1");System.out.println(c);Constructor ctr = c.getConstructor(Moveable.class);//这里传了Tank对象,返回了此对象的代理对象。Object m = ctr.newInstance(new Tank());return m;}}
这个getNewInstance和上面的区别仅仅是在前面拼字符串的时候通过反射拿到了传入进来接口的方法名,然后将每个方法的前面和后面都加上记录时间的代码。此外,此段代码会在d:/src/com/bjsxt/proxy/目录下生产$Proxy1.java文件,看一下这个文件中生产的代码如下:
package com.bjsxt.proxy;public class $Proxy1 implements com.bjsxt.proxy.Moveable{public $Proxy1(Moveable t) {this.t = t;}Moveable t;@Overridepublic void move() {long start = System.currentTimeMillis();System.out.println("starttime:" + start);t.move();long end = System.currentTimeMillis();System.out.println("time:" + (end-start));}}
虽然产生的代码和以前是一样的,但是这里是将接口传进来以后产生的代码,也就是说,只要把传入的接口一变,不论这个接口中有多少方法,都可以生产这个接口中所有方法记录时间的处理逻辑。
此外还要注意,这个中间代码的生成的文件我们可以写到任何地方,作为使用这个类的客户端,甚至都不知道中间产生了一个文件,如果借助CGLib等第三方的类库,可以完全不用生成这些中间文件,而直接编译这些代码产生对象。这件事情很神奇,我们只要将被代理类实现的接口传入getNewInstance方法,就能获得被代理类的一个代理对象。看如下测试代码:
package com.bjsxt.proxy;public class Client {public static void main(String[] args) throws Exception {Moveable moveable = (Moveable) Proxy.newProxyInstance(Moveable.class);moveable.move();}}
运行的截图如下:

评论:以上的程序虽然可以对任意的接口生成记录方法运行时间的代理类对象,但是此代理类还是不太灵活,因为getNewInstance只能产生记录时间的处理逻辑,也就是说代理的处理行为是写死的,如何随意地定义生成代理对象的处理逻辑呢?
要在哪个方向上保持灵活,就要在哪个方向上进行抽象,处理逻辑显然抽象成一个接口更合适,抽象成如下接口InvocationHandler(jdk中也是这个名称)
package com.bjsxt.proxy;import java.lang.reflect.Method;public interface InvocationHandler {public void invoke(Object o, Method m);}
这里的参数也是模仿jdk中的,o为最终生成的代理对象,m为接口中的方法,这里用的是反射(可以先不了解这两个参数的意义,往下看)。
那么如何使用这个接口来生成代理对象,假如还是记录时间的需求,代码如下:
package com.bjsxt.proxy;import java.lang.reflect.Method;public class TimeHandler implements InvocationHandler {private Object target; //用于产生传进来的这个对象的代理对象public TimeHandler(Object target) {super();this.target = target;}@Overridepublic void invoke(Object o, Method m) {long start = System.currentTimeMillis();System.out.println("starttime:" + start);System.out.println(o.getClass().getName());try {m.invoke(target);} catch (Exception e) {e.printStackTrace();}long end = System.currentTimeMillis();System.out.println("time:" + (end - start));}}
在invoke方法中,打印了 o 的名字是看一下这个参数是什么意思。m.invoke(target);实际上是调用了target这个对象的 m 方法,到底是如何调用的呢?接着看。
需要产生任意逻辑的代理对象,就需要修改getNewInstance方法,将处理逻辑的抽象InvocationHandler传进去。代码如下:
package com.bjsxt.proxy;import java.io.File;import java.io.FileWriter;import java.lang.reflect.Constructor;import java.lang.reflect.Method;import java.net.URL;import java.net.URLClassLoader;import javax.tools.JavaCompiler;import javax.tools.StandardJavaFileManager;import javax.tools.ToolProvider;import javax.tools.JavaCompiler.CompilationTask;public class Proxy {public static Object newProxyInstance(Class infce, InvocationHandler h) throws Exception { //JDK6 Complier API, CGLib, ASMString methodStr = "";String rt = "\r\n";Method[] methods = infce.getMethods();//对每个方法,都生成相同的处理逻辑。//这里的h.invoke(this,md)会调用具体的InvocationHandler实现类中的invoke方法,而传的两个参数分别为this,也就是代理类对象本身,而md是传进来接口中的方法。for(Method m : methods) {methodStr += "@Override" + rt +"public void " + m.getName() + "() {" + rt +" try {" + rt +" Method md = " + infce.getName() + ".class.getMethod(\"" + m.getName() + "\");" + rt +" h.invoke(this, md);" + rt +" }catch(Exception e) {e.printStackTrace();}" + rt +"}";}String src ="package com.bjsxt.proxy;" + rt +"import java.lang.reflect.Method;" + rt +"public class $Proxy1 implements " + infce.getName() + "{" + rt +" public $Proxy1(InvocationHandler h) {" + rt +" this.h = h;" + rt +" }" + rt +" com.bjsxt.proxy.InvocationHandler h;" + rt +methodStr +"}";String fileName ="d:/src/com/bjsxt/proxy/$Proxy1.java";File f = new File(fileName);FileWriter fw = new FileWriter(f);fw.write(src);fw.flush();fw.close();//compileJavaCompiler compiler = ToolProvider.getSystemJavaCompiler();StandardJavaFileManager fileMgr = compiler.getStandardFileManager(null, null, null);Iterable units = fileMgr.getJavaFileObjects(fileName);CompilationTask t = compiler.getTask(null, fileMgr, null, null, null, units);t.call();fileMgr.close();//load into memory and create an instanceURL[] urls = new URL[] {new URL("file:/" + "d:/src/")};URLClassLoader ul = new URLClassLoader(urls);Class c = ul.loadClass("com.bjsxt.proxy.$Proxy1");System.out.println(c);Constructor ctr = c.getConstructor(InvocationHandler.class);//这里创建一个对象,且将h传了进去,实际上这里返回的就是一个代理对象,h是构造函数的参数Object m = ctr.newInstance(h);return m;}}
这里和上面生成的中间类$Proxy1不同的地方是上面的类中只是Moveable接口的引用,如下:
package com.bjsxt.proxy;public class $Proxy1 implements com.bjsxt.proxy.Moveable{public $Proxy1(Moveable t) {this.t = t;}Moveable t;@Overridepublic void move() {long start = System.currentTimeMillis();System.out.println("starttime:" + start);t.move();long end = System.currentTimeMillis();System.out.println("time:" + (end-start));}}
处理逻辑也是在生成的中间类写死的。再看看这次生成的中间类:
package com.bjsxt.proxy;import java.lang.reflect.Method;public class $Proxy1 implements com.bjsxt.proxy.Moveable{public $Proxy1(InvocationHandler h) {this.h = h;}com.bjsxt.proxy.InvocationHandler h;@Overridepublic void move() {try {Method md = com.bjsxt.proxy.Moveable.class.getMethod("move");h.invoke(this, md);}catch(Exception e) {e.printStackTrace();}}}
类中有一个InvocationHandler引用,在方法的处理逻辑中通过这个引用调用invoke方法,注意上面Method md = com.bjsxt.proxy.Moveable.class.getMethod("move");中的”move”是根据public void move() 确定的,也就说用生成的代理对象调用接口中的哪个方法,这里的md就会是哪个方法,根据多态h.invoke(this,md)会调用InvocationHandler实现类中的invoke方法,看看上面的TimeHandler类:
package com.bjsxt.proxy;import java.lang.reflect.Method;public class TimeHandler implements InvocationHandler {private Object target; //用于产生传进来的这个对象的代理对象,此对象是被代理对象public TimeHandler(Object target) {super();this.target = target;}@Overridepublic void invoke(Object o, Method m) {long start = System.currentTimeMillis();System.out.println("starttime:" + start);System.out.println(o.getClass().getName());try {m.invoke(target);} catch (Exception e) {e.printStackTrace();}long end = System.currentTimeMillis();System.out.println("time:" + (end - start));}}
这时会调用这里面的invoke方法,所以这里的两个参数的意思我们也就能够理解了,o 为生成的代理对象的引用,而 m 是代理对象调用什么方法,这里的 m 就是什么,m 是什么下面的 m.invoke(target)当然就会调用被代理对象中相应的方法,而我们可以在调用被代理方法的前面和后面任意的加上我们想要的处理逻辑。这件事情更神奇了,我们要想拿到一个对象(实现了接口infac)的代理对象,并自由的定义代理对象的处理逻辑,我们只需要做如下几件事情就可以了。
1. 定义一个逻辑处理类(如TimeHandler)实现InvocationHandler,并在此类中定义一个Object引用(用来引用被代理的对象),然后实现invoke方法,其中m.invoke(target)是固定的,前后随便写,这里的 o参数只是一个代理对象的引用,可以不用。
2. new 一个TimeHandler的对象(h),并把被代理对象传进去。
3. 调用Proxy.getNewInstance(infac.class,h)就能返回一个代理对象。
看测试代码如下:
package com.bjsxt.proxy;public class Client {public static void main(String[] args) throws Exception {InvocationHandler h = new TimeHandler(new Tank());Moveable m = (Moveable) Proxy.newProxyInstance(Moveable.class, h);m.move();}}
运行的截图如下:

以上我们自己模拟实现了一个动态代理,接下来看看jdk的动态代理的api,以及如何使用这些api。

和我们自己实现的动态代理类相比,多了一个ClassLoader参数,注意这里的ClassLoader必须和被代理对象使用相同的ClassLoader,第二个参数是一个接口数组,因为一个类可能实现多个接口,那么可以对多个接口中的方法同时加上相同的处理逻辑。再看一下jdk中InvocationHandler接口是什么样子的,文档如下:

这里比我们实现的多了一个Object[]数组,作为方法调用的参数,但是我们不要去管这个args形参,因为此方法的调用是在自动生成的中间代理类中,可以想象,你调用了哪个方法,那么内部实现肯定是通过反射获得此方法的参数列表,然后传递此方法,若代理对象调用的接口方法中没有参数,则这里的args为null。我们上面的实现中为了简单没有考虑参数问题,但是原理是一样的。另外此方法还有一个返回值,这个返回值就是代理对象调用接口方法的返回值,若接口方法没有返回值,那么这里可以返回null。注意:不论是args还是返回值,都是Object的,若原来的接口方法中是基本类型,会自动装箱成对应的包装类对象。
使用jdk自带的api,实现坦克move时间的代理类代码如下:
-------------------------Moveable接口-------------------------package com.jdk.proxy;public interface Moveable {void move();}-----------------Tank类实现了Moveable接口-----------------------package com.jdk.proxy;import java.util.Random;public class Tank implements Moveable {@Overridepublic void move() {System.out.println("Tank Moving...");try {Thread.sleep(new Random().nextInt(10000));} catch (InterruptedException e) {e.printStackTrace();}}}------------定义代理类的处理逻辑(实现InvocationHandler接口)--------------package com.jdk.proxy;import java.lang.reflect.InvocationHandler;import java.lang.reflect.Method;public class TimeHandler implements InvocationHandler {private Moveable m;@Overridepublic Object invoke(Object proxy, Method method, Object[] args)throws Throwable {// 看看jdk自动生成的类的类名是什么System.out.println(proxy.getClass());System.out.println(args == null); // 这里应该输出true,因为接口方法move没有参数long start = System.currentTimeMillis();System.out.println("start time:" + start);Object obj = method.invoke(m, args); // 固定写法,不用管args参数是什么long end = System.currentTimeMillis();System.out.println("run time:" + (end - start));return obj; // move没有返回值,这里返回null}public TimeHandler(Moveable m) {this.m = m;}}------------------测试类Test---------------------package com.jdk.proxy;import java.lang.reflect.InvocationHandler;import java.lang.reflect.Proxy;public class Test {public static void main(String[] args) {Moveable tank = new Tank();InvocationHandler timeHandler = new TimeHandler(tank);Moveable m = (Moveable) Proxy.newProxyInstance(Test.class.getClassLoader(), new Class[] { Moveable.class },timeHandler);m.move();}}
输出的结果为:
因为被代理对象和代理对象必须是相同的ClassLoader,所以更好的测试代码可以这样写:
package com.jdk.proxy;import java.lang.reflect.InvocationHandler;import java.lang.reflect.Proxy;public class Test {public static void main(String[] args) {// 更好的测试代码可以这样写Moveable tank = new Tank();InvocationHandler timeHandler = new TimeHandler(tank);Moveable m = (Moveable) Proxy.newProxyInstance(tank.getClass().getClassLoader(), tank.getClass().getInterfaces(), timeHandler);m.move();}}
后记:动态代理是相当重要的设计模式,也是设计模式中比较难的,Spring中的两大核心思想之一AOP就是动态代理的一种应用,因为我们可以“神不知,鬼不觉”的在一个对象的方法运行的前面和后面做一些事情,而这个方法本身是完全不知觉的。如在一个方法调用前,我对调用的请求进行身份验证,还可以在插入用户的时候记录日志,如果插入用户和记录日志是在不同的数据库中进行的,那么我们就没有办法将这两个动作放到同一个事务中,因为事务的边界是基于session,而一个session只是与一个数据库实例的会话连接,若在不同的数据库中插入数据,通常我们没有办法将它们放到同一个事务中,但是有了动态代理,这事就好办了,我们可以将这两个方法调用之前做一点事情,之后也做一点事情,两个方法的调用中间也可以做一些事情,这样就可以在一个动作处理失败的时候,进行一些自定义的回滚操作,保证两个动作的事务性。事实上,Spring也的确提供了这样的功能,而此前仅仅凭借Hibernate是做不到的。