v前言:

  代理模式是常用的java设计模式,它的特征是代理类与委托类有同样的接口,代理类主要负责为委托类预处理消息、过滤消息、把消息转发给委托类,以及事后处理消息等。代理类与委托类之间通常会存在关联关系,一个代理类的对象与一个委托类的对象关联,代理类的对象本身并不真正实现服务,而是通过调用委托类的对象的相关方法,来提供特定的服务。

vJAVA各种动态代理实现的比较

v接口

interface AddInterface{int add(int a, int b);
}interface SubInterface{int sub(int a, int b);
}

v实现类

class Arithmetic implements AddInterface, SubInterface{@Overridepublic int sub(int a, int b) {return a-b;}@Overridepublic int add(int a, int b) {return a+b;}
}

v方式1: JDK自带的动态代理

v实现方式

  Java在JDK1.3后引入的动态代理机制,使我们可以在运行期动态的创建代理类。使用动态代理实现AOP需要有四个角色:被代理的类,被代理类的接口,织入器,和InvocationHandler,而织入器使用接口反射机制生成一个代理类,然后在这个代理类中织入代码。被代理的类是AOP里所说的目标,InvocationHandler是切面,它包含了Advice和Pointcut。

vInvocationHandler接口的实现

class JdkDPQueryHandler implements InvocationHandler{private Arithmetic real;public JdkDPQueryHandler(Arithmetic real){this.real = real;}@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {String methodName = method.getName();System.out.println(method);System.out.println("the method: " + methodName + "开始, 参数: "+Arrays.asList(args));Object result = method.invoke(real, args);System.out.println("the method: "+methodName+"结束, 结果: " + result);return result;}
}

v创建代理类并且调用代理类

public class Main{private static int a = 4, b = 2;public static Object createJDKProxy(Arithmetic real){Object proxyArithmetic = Proxy.newProxyInstance(real.getClass().getClassLoader(),real.getClass().getInterfaces(), new JdkDPQueryHandler(real)); return proxyArithmetic;}public static void main(String[] args){Arithmetic real = new Arithmetic();Object proxyArithmetic = createJDKProxy(real);((AddInterface)proxyArithmetic).add(a, b);((SubInterface)proxyArithmetic).sub(a, b);}
}

v方式2:动态字节码生成(cglib)

v实现方式

Enhancer和MethodInterceptor。Enhancer可以用来动态的生成一个类,这个类可以继承指定的一个类,实现指定的一些接口。同时,Enhancer在生成一个类之前需要指定一个Callback,当类方法调用时,方法的执行被分配给这个Callback,MethodInterceptor是一个使用比较多的继承自Callback的接口,它只有一个方法声明。

v接口InvocationHandler(jdk中)和接口MethodInterceptor(cglib中)对比

public interface MethodInterceptor extends Callback  {  public Object intercept(Object obj, java.lang.reflect.Method method, Object[] args,MethodProxy proxy) throws Throwable;
}  

public interface InvocationHandler {  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable;
}  

从参数构成上,methodInterceptor的输入参数比Invocationhandler多1个,其实前3个参数对象的含义与Invocationhandler的含义是相同的。
第一个参数表示调用方法来自哪个对象;
第二个参数表示调用方法的Method对象;
第三个参数表示此次调用的输入参数列表;
多出来的参数是MethodProxy 类型的,它应该是cglib生成用来代替Method对象的一个对象,使用MethodProxy比调用JDK自身的Method直接执行方法效率会有提升。

v实现1

  MethodInterceptor接口的实现

class CglibDPQueryInterceptor implements MethodInterceptor{private Arithmetic real;public CglibDPQueryInterceptor(Arithmetic real){this.real = real;}@Overridepublic Object intercept(Object target, Method method, Object[] args, MethodProxy proxy) throws Throwable {String methodName = method.getName();System.out.println(method);System.out.println("the method: " + methodName + "开始, 参数: "+Arrays.asList(args));//Object result = method.invoke(real, args);//两种方式都是可以得Object result = proxy.invoke(real, args);System.out.println("the method: "+methodName+"结束, 结果: " + result);return result;}
}

  创建代理类并调用代理类

public class Main{private static int a = 4, b = 2;public static Object createCglibProxy(Arithmetic real){Enhancer enhancer = new Enhancer();enhancer.setCallback(new CglibDPQueryInterceptor(real));enhancer.setInterfaces(real.getClass().getInterfaces());return enhancer.create();}public static void main(String[] args){Arithmetic real = new Arithmetic();      Object proxyArithmetic = createCglibProxy(real);        ((AddInterface)proxyArithmetic).add(a, b);((SubInterface)proxyArithmetic).sub(a, b);}
}

  注意了,MethodProxy在对执行函数的时候,提供了2个方法

public Object invoke (Object obj, Object[] args) throws Throwable
public Object invokeSuper(Object obj, Object[] args) throws Throwable

  其中,javadoc上说这个invoke()方法可以用于相同类中的其他对象的方法执行,也就是说这个方法中的obj需要传入相同一个类的另一个对象(上述方法中就是传入了Arithmetic类的不同对象),否则会进入无限递归循环(测试之后还真是出现了StackOverflowError)。仔细的想一想就会发现,public Object intercept(Object target, Method method, Object[] args, MethodProxy proxy)中的target是实现的代理类对象,通过target调用add()方法时会触发intercept()方法被调用,如果在intercept()方法中再调用method.invoke(target, args),就相当于add()方法中又调用add()方法,导致无限的递归循环。但是如果执行method.invoke(real, args)则不会,因为real和target是同一个类不同对象,real是真实逻辑主题,target是真实主题real的代理。

  下面一个例子来模拟一下:

interface SolveInterface{void solve();
}class Real implements SolveInterface{public void solve(){System.out.println("Real Solve!");}
}class Target extends Real{private Object obj;public void setObject(Object obj){this.obj = obj;}private void invoke(){try {Method method = SolveInterface.class.getMethod("solve", new Class[]{});method.invoke(obj, new Class[]{});} catch (Exception e) {e.printStackTrace();}}public void solve(){System.out.println("Target Solve!");invoke();}
}

  

public class Main{public static void main(String[] args) throws Exception{     Target target = new Target();target.setObject(new Real());//正确
//target.setObject(target);//发生循环调用target.solve();}
}

  其实Method的invoke()方法会根据obj的类型去调用对应的solve()方法,也就是多态性。

v实现2

  MethodInterceptor接口的实现

class CglibDPQueryInterceptor implements MethodInterceptor{@Overridepublic Object intercept(Object target, Method method, Object[] args, MethodProxy proxy) throws Throwable {String methodName = method.getName();System.out.println(method);System.out.println("the method: " + methodName + "开始, 参数: "+Arrays.asList(args));//  打印类信息 :target.getClass();省略Object result = proxy.invokeSuper(target, args);System.out.println("the method: "+methodName+"结束, 结果: " + result);return result;}
}

  创建代理类并调用代理类

public class Main{private static int a = 4, b = 2;
public static Object createCglibProxy(){Enhancer enhancer = new Enhancer();enhancer.setCallback(new CglibDPQueryInterceptor());enhancer.setSuperclass(Arithmetic.class);return enhancer.create();}public static void main(String[] args){Arithmetic real = new Arithmetic();
Object proxyArithmetic = createCglibProxy();((AddInterface)proxyArithmetic).add(a, b);((SubInterface)proxyArithmetic).sub(a, b);}
}

  注意了,实现2中Enhancer 没有设置接口,因为设置了Superclass了(也就是代理类的父类是Arithmetic),我们的代理类会继承它,而Arithmetic已经实现了我们的接口。为了证明这一点,可以在MethodInterceptor的 intercept方法中打印 target.getClass()的类信息,你会发现cglib的两种方式代理类的父类是不同的。如下:

  (如果需要打印类信息代码,请参考:http://www.cnblogs.com/hujunzheng/p/5132943.html)

  实现1:

public class com.test.Arithmetic$$EnhancerByCGLIB$$4fa786eb extends java.lang.Object

  实现2:

public class com.test.Arithmetic$$EnhancerByCGLIB$$4fa786eb extends com.test.Arithmetic

v方式3:javassist生成动态代理(代理工厂创建 或者 动态代码创建)  

  Javassist是一个编辑字节码的框架,可以让你很简单地操作字节码。它可以在运行期定义或修改Class。使用Javassist实现AOP的原理是在字节码加载前直接修改需要切入的方法。这比使用Cglib实现AOP更加高效,并且没太多限制,实现原理如下图:

v实现1:

接口的实现

class JavassistDPQueryHandler implements MethodHandler{@Overridepublic Object invoke(Object target, Method method, Method proxy, Object[] args) throws Throwable {String methodName = method.getName();System.out.println(method);System.out.println("the method: " + methodName + "开始, 参数: "+Arrays.asList(args));Object result = proxy.invoke(target, args);System.out.println("the method: "+methodName+"结束, 结果: " + result);return result;}
}

创建代理类并调用代理类

public class Main{private static int a = 4, b = 2;
public static Object createJavassistProxy() throws Exception{ProxyFactory factory = new ProxyFactory();factory.setSuperclass(Arithmetic.class);factory.setHandler(new JavassistDPQueryHandler());return factory.createClass().newInstance();}public static void main(String[] args) throws Exception{Arithmetic real = new Arithmetic();Object proxyArithmetic = createJavassistProxy();((AddInterface)proxyArithmetic).add(a, b);((SubInterface)proxyArithmetic).sub(a, b);}
}

  注意:MethodHandler接口中invoke方法的定义,如下:

public Object invoke(Object target, Method method, Method proxy, Object[] args)

  method代表调用方法的Method对象,proxy是代理类产生并代替method的对象,否则用method.invoke(target, args)会产生无限循环调用。

v实现2:

  (来自:http://cuishen.iteye.com/blog/421464),代码注释很详细,仔细研究一下就会懂了!

  javassist使用动态java代码常见代理过程和前文的方法略有不同。javassist内部可以通过动态java代码,生成字节码。这种方式创建的动态代理可以非常灵活,甚至可以在运行时产生业务逻辑。

//自定义拦截器接口
interface InterceptorHandler {  /** * 调用动态代理对象的方法将反射本方法,可在本方法实现中添加类似AOP的事前事后操作,只有在本方法体中加入如下代码 * 被代理的方法才会被执行,返回值将返回给代理最后返回给程序 * @param obj Object 被代理的对象 * @param method Method 被代理对象的方法 * @param args Object[] 被代理对象的方法的参数 * @return Object 被代理对象的方法执行后的返回值 * @throws Throwable */  public Object invoke(Object obj, Method method, Object[] args) throws Throwable;
}

//拦截器的实现
class InterceptorHandlerImpl implements InterceptorHandler{@Overridepublic Object invoke(Object obj, Method method, Object[] args) throws Throwable {String methodName = method.getName();System.out.println(method);System.out.println("the method: " + methodName + "开始, 参数: "+Arrays.asList(args));Object result = method.invoke(obj, args);System.out.println("the method: "+methodName+"结束, 结果: " + result);return result;}
}class MyProxyImpl {  /** 动态代理类的类名后缀 */  private final static String PROXY_CLASS_NAME_SUFFIX = "$MyProxy_";  /** 拦截器接口 */  private final static String INTERCEPTOR_HANDLER_INTERFACE = "com.test.InterceptorHandler";  /** 动态代理类的类名索引,防止类名重复 */  private static int proxyClassIndex = 1;  /** * 暴露给用户的动态代理接口,返回某个接口的动态代理对象,注意本代理实现需和com.cuishen.myAop.InterceptorHandler拦截器配合 * 使用,即用户要使用本动态代理,需先实现com.cuishen.myAop.InterceptorHandler拦截器接口 * @param interfaceClassName String 要动态代理的接口类名, e.g test.StudentInfoService * @param classToProxy String 要动态代理的接口的实现类的类名, e.g test.StudentInfoServiceImpl * @param interceptorHandlerImplClassName String 用户提供的拦截器接口的实现类的类名 * @return Object 返回某个接口的动态代理对象 * @throws InstantiationException * @throws IllegalAccessException * @throws NotFoundException * @throws CannotCompileException * @throws ClassNotFoundException * @see com.cuishen.myAop.InterceptorHandler */  public static Object newProxyInstance(String interfaceClassName, String classToProxy, String interceptorHandlerImplClassName) throws InstantiationException, IllegalAccessException, NotFoundException, CannotCompileException, ClassNotFoundException {  Class interfaceClass = Class.forName(interfaceClassName);  Class interceptorHandlerImplClass = Class.forName(interceptorHandlerImplClassName);  return dynamicImplementsInterface(classToProxy, interfaceClass, interceptorHandlerImplClass);  }  /** * 动态实现要代理的接口 * @param classToProxy String 要动态代理的接口的实现类的类名, e.g test.StudentInfoServiceImpl * @param interfaceClass Class 要动态代理的接口类, e.g test.StudentInfoService * @param interceptorHandlerImplClass Class 用户提供的拦截器接口的实现类 * @return Object 返回某个接口的动态代理对象 * @throws NotFoundException * @throws CannotCompileException * @throws InstantiationException * @throws IllegalAccessException */  private static Object dynamicImplementsInterface(String classToProxy, Class interfaceClass, Class interceptorHandlerImplClass) throws NotFoundException, CannotCompileException, InstantiationException, IllegalAccessException {  ClassPool cp = ClassPool.getDefault();  String interfaceName = interfaceClass.getName();  //动态指定代理类的类名  String proxyClassName = interfaceName + PROXY_CLASS_NAME_SUFFIX + proxyClassIndex++;  //要实现的接口的包名+接口名  String interfaceNamePath = interfaceName;  CtClass ctInterface = cp.getCtClass(interfaceNamePath);  CtClass cc = cp.makeClass(proxyClassName);  cc.addInterface(ctInterface);  Method [] methods = interfaceClass.getMethods();  for(int i = 0; i < methods.length; i++) {  Method method = methods[i];  dynamicImplementsMethodsFromInterface(classToProxy, cc, method, interceptorHandlerImplClass, i);  }  return (Object)cc.toClass().newInstance();  }  /** * 动态实现接口里的方法 * @param classToProxy String 要动态代理的接口的实现类的类名, e.g test.StudentInfoServiceImpl * @param implementer CtClass 动态代理类的包装 * @param methodToImpl Method 动态代理类里面要实现的接口方法的包装 * @param interceptorClass Class 用户提供的拦截器实现类 * @param methodIndex int 要实现的方法的索引 * @throws CannotCompileException */  private static void dynamicImplementsMethodsFromInterface(String classToProxy, CtClass implementer, Method methodToImpl, Class interceptorClass, int methodIndex) throws CannotCompileException {  String methodCode = generateMethodCode(classToProxy, methodToImpl, interceptorClass, methodIndex);  CtMethod cm = CtNewMethod.make(methodCode, implementer);  implementer.addMethod(cm);  }  /** * 动态组装方法体,当然代理里面的方法实现并不是简单的方法拷贝,而是反射调用了拦截器里的invoke方法,并将接收到的参数进行传递 * @param classToProxy String 要动态代理的接口的实现类的类名, e.g test.StudentInfoServiceImpl * @param methodToImpl Method 动态代理类里面要实现的接口方法的包装 * @param interceptorClass Class 用户提供的拦截器实现类 * @param methodIndex int 要实现的方法的索引 * @return String 动态组装的方法的字符串 */  private static String generateMethodCode(String classToProxy, Method methodToImpl, Class interceptorClass, int methodIndex) {  String methodName = methodToImpl.getName();  String methodReturnType = methodToImpl.getReturnType().getName();  Class[] parameters = methodToImpl.getParameterTypes();  Class[] exceptionTypes = methodToImpl.getExceptionTypes();  StringBuffer exceptionBuffer = new StringBuffer();  //组装方法的Exception声明  if(exceptionTypes.length > 0) exceptionBuffer.append(" throws ");  for(int i = 0; i < exceptionTypes.length; i++) {  if(i != exceptionTypes.length - 1) exceptionBuffer.append(exceptionTypes[i].getName()).append(",");  else exceptionBuffer.append(exceptionTypes[i].getName());  }  StringBuffer parameterBuffer = new StringBuffer();  //组装方法的参数列表  for(int i = 0; i < parameters.length; i++) {  Class parameter = parameters[i];  String parameterType = parameter.getName();  //动态指定方法参数的变量名  String refName = "a" + i;  if(i != parameters.length - 1) parameterBuffer.append(parameterType).append(" " + refName).append(",");  else parameterBuffer.append(parameterType).append(" " + refName);  }  StringBuffer methodDeclare = new StringBuffer();  //方法声明,由于是实现接口的方法,所以是public  methodDeclare.append("public ").append(methodReturnType).append(" ").append(methodName).append("(").append(parameterBuffer).append(")").append(exceptionBuffer).append(" {\n");  String interceptorImplName = interceptorClass.getName();  //方法体  methodDeclare.append(INTERCEPTOR_HANDLER_INTERFACE).append(" interceptor = new ").append(interceptorImplName).append("();\n");  //反射调用用户的拦截器接口  methodDeclare.append("Object returnObj = interceptor.invoke(Class.forName(\"" + classToProxy + "\").newInstance(), Class.forName(\"" + classToProxy + "\").getMethods()[" + methodIndex + "], ");  //传递方法里的参数  if(parameters.length > 0) methodDeclare.append("new Object[]{");   for(int i = 0; i < parameters.length; i++) {  //($w) converts from a primitive type to the corresponding wrapper type: e.g.  //Integer i = ($w)5;  if(i != parameters.length - 1) methodDeclare.append("($w)a" + i + ",");  else methodDeclare.append("($w)a" + i);  }  if(parameters.length > 0) methodDeclare.append("});\n");  else methodDeclare.append("null);\n");  //对调用拦截器的返回值进行包装  if(methodToImpl.getReturnType().isPrimitive()) {  if(methodToImpl.getReturnType().equals(Boolean.TYPE)) methodDeclare.append("return ((Boolean)returnObj).booleanValue();\n");  else if(methodToImpl.getReturnType().equals(Integer.TYPE)) methodDeclare.append("return ((Integer)returnObj).intValue();\n");  else if(methodToImpl.getReturnType().equals(Long.TYPE)) methodDeclare.append("return ((Long)returnObj).longValue();\n");  else if(methodToImpl.getReturnType().equals(Float.TYPE)) methodDeclare.append("return ((Float)returnObj).floatValue();\n");  else if(methodToImpl.getReturnType().equals(Double.TYPE)) methodDeclare.append("return ((Double)returnObj).doubleValue();\n");  else if(methodToImpl.getReturnType().equals(Character.TYPE)) methodDeclare.append("return ((Character)returnObj).charValue();\n");  else if(methodToImpl.getReturnType().equals(Byte.TYPE)) methodDeclare.append("return ((Byte)returnObj).byteValue();\n");  else if(methodToImpl.getReturnType().equals(Short.TYPE)) methodDeclare.append("return ((Short)returnObj).shortValue();\n");  } else {  methodDeclare.append("return (" + methodReturnType + ")returnObj;\n");  }  methodDeclare.append("}");  System.out.println(methodDeclare.toString());  return methodDeclare.toString();  }  }  public class Main{     public static void main(String[] args) throws Exception{
//分别对应 代理类要实现的接口类名, 需要代理类的类名, 用户自定义拦截器实现类的类名Object proxyArithmetic = MyProxyImpl.newProxyInstance("com.test.ArithmeticInterface", "com.test.Arithmetic",
"com.test.InterceptorHandlerImpl");((ArithmeticInterface)proxyArithmetic).add(a, b);((ArithmeticInterface)proxyArithmetic).sub(a, b);        }
}

  打印一下动态实现接口的代码如下:

public int add(int a0,int a1) {
com.test.InterceptorHandler interceptor = new com.test.InterceptorHandlerImpl();
Object returnObj = interceptor.invoke(Class.forName("com.test.Arithmetic").newInstance(), Class.forName("com.test.Arithmetic").getMethods()[0], new Object[]{($w)a0,($w)a1});
return ((Integer)returnObj).intValue();
}
public int sub(int a0,int a1) {
com.test.InterceptorHandler interceptor = new com.test.InterceptorHandlerImpl();
Object returnObj = interceptor.invoke(Class.forName("com.test.Arithmetic").newInstance(), Class.forName("com.test.Arithmetic").getMethods()[1], new Object[]{($w)a0,($w)a1});
return ((Integer)returnObj).intValue();
}

java中动态代理实现机制相关推荐

  1. Java中动态代理的两种方式JDK动态代理和cglib动态代理以及区别

    视频功能审核通过了,可以看视频啦!记得点关注啊~ 注意:因为网络原因,视频前一两分钟可能会比较模糊,过一会儿就好了 记得点关注啊,视频里的wx二维码失效了,wx搜索:"聊5毛钱的java&q ...

  2. java中动态代理的使用

    代理模式是常用的java设计模式,他的特征是代理类与委托类有同样的接口,代理类主要负责为委托类预处理消息.过滤消息.把消息转发给委托类,以及事后处理消息等.代理类与委托类之间通常会存在关联关系,一个代 ...

  3. [Java|面试] 面试被问Java的动态代理机制,能说说吗

    Java的动态代理机制 文章目录 Java的动态代理机制 0. 什么是代理 1.动态代理和静态代理的区别 2. 使用代理的情况 3. 动态代理的构成 4. JDK中的动态代理 5. 手写一个JDK动态 ...

  4. java的动态代理机制详解

    2019独角兽企业重金招聘Python工程师标准>>> 参考资料 1.java的动态代理机制详解 转载于:https://my.oschina.net/Howard2016/blog ...

  5. 代理模式及Java实现动态代理

    代理模式 定义:给某个对象提供一个代理对象,并由代理对象控制对于原对象的访问,即客户不直接操控原对象,而是通过代理对象间接地操控原对象. 代理模式UML图 在上图中: RealSubject 是原对象 ...

  6. 深入理解Java反射+动态代理,java开发面试笔试题

    我总结出了很多互联网公司的面试题及答案,并整理成了文档,以及各种学习的进阶学习资料,免费分享给大家. 扫描二维码加VX好友,拉你进[程序员面试学习交流群]免费领取.也欢迎各位一起在群里探讨技术. 答: ...

  7. 【java】深入分析Java反射-动态代理 proxy

    1.概述 转载:深入分析Java反射(四)-动态代理 [Java]Java 反射机制浅析 [Java]java代理 静态代理 动态代理 proxy [java]静态代理 proxy 2.动态代理的简介 ...

  8. java CGLIB动态代理

    CGLIB动态代理 一:CGLIB(Code Generation Library)是一个基于ASM的字节码生成库,它允许我们在运行时对字节码进行修改和动态生成.CGLIB通过继承方式实现代理. 二: ...

  9. java反射-动态代理

    转自:https://www.cnblogs.com/throwable/p/12272269.html 动态代理的简介# Java动态代理机制的出现,使得Java开发人员不用手工编写代理类,只要简单 ...

最新文章

  1. 天天动听 悬浮歌词(迷你歌词)效果解读
  2. MATLAB常用的学习的网站
  3. JUC系列(九)| ThreadPool 线程池
  4. PHP 数组遍历 foreach 语法结构
  5. 让数字保持在整数范围内
  6. latex-bib参考文献人名特殊字符
  7. c++设计成员变量可动态调整的动态类结构
  8. indesign入门教程,如何添加文字?
  9. Qt_MsgBox 非常简单 自定义实现类似QMessageBox的弹窗 静态调用
  10. 腾讯专访 | 子芽:代码疫苗技术,赋能数字化应用内生安全自免疫
  11. 北京大学计算机系在线编程题库,北京大学acm试题题库及解答 完整版
  12. 金蝶 K3cloud 安装说明教程
  13. flutter web 微信授权和微信支付
  14. 中国紧急警报系统(EAS)行业发展现状与前景动态预测报告2022-2027
  15. 黑科技VNET——最好用的Android抓包神器
  16. 如何让excel图表根据当前月份自动延伸
  17. JavaScript实现Sleep效果
  18. Android开发系列7——icon图标和开机启动页
  19. 我对光学相干层析成像的理解
  20. 佩伯尔幻像_幻像类型提高了编译时的安全性

热门文章

  1. 用html编写ASCII表,[html_css]ASCII编码表
  2. SpringBoot 45个注解
  3. 【Linux】【服务器】 CentOS7下卸载MySQL详细过程步骤
  4. was连接oracle rac集群,Oracle集群(RAC)及 jdbc 联接双机数据库
  5. mysql 分区表优化_Sql优化之Mysql表分区
  6. vue中组件在不同页面中渲染出错
  7. rpython求随机数代码4段5个字符_如何创建一个4位数的随机数并将其存储为变量...
  8. 洲际的merlin怎么用_天问一号是怎么拍摄地月合影的?
  9. dump分析工具_iOS逆向分析和注入微信防撤回
  10. 数据结构实验之图论八:欧拉回路