C# 使用Emit实现动态AOP框架 进阶篇之优化
目 录
在前几篇文章中,有几个遗留问题还没有处理:
1、切面特性对象的InterceptType属性没有处理,分别取值OnEntry(只触发入口)、OnExit(只触发出口)、All(都触发);
2、代理类中,各代理方法中的相关特性调用代码冗余,随着特性的增多,这些代码也成倍增加;
LogAttribute logAttribute = new LogAttribute();
logAttribute.OnEntry(aspectContext);
base.Age = value;
logAttribute.OnExit(aspectContext);
3、AspectExceptionAttribute我们只是修改了 ILGenerateProxyMethod,实现了异常处理,但是造成的结果时所有方法和属性(Get、Set方法)都进行了异常处理;
4、一般属性切入时,我们只切入Set方法,而现在Get、Set都进行了切入,灵活性不够;
下面我们针对这四个问题进行优化:
增加公共的切面特性调用方法( _OnEntryExit)
方法如下:
protected override void _OnEntryExit(string text, AspectContext context, AspectAttribute[] array)
{
for (int i = ; i < array.Length; i++)
{
InterceptType interceptType = array[i].InterceptType;
if (text.Equals("OnEntry") && interceptType.HasFlag(InterceptType.OnEntry))
{
array[i].OnEntry(context);
}
if (text.Equals("OnExit") && interceptType.HasFlag(InterceptType.OnExit))
{
array[i].OnExit(context);
}
}
}
方法调用代码如下:
public override int NYearLater(int num)
{
AspectContext aspectContext = new AspectContext(this, "NYearLater", new object[]
{
num
});
AspectAttribute[] array = new AspectAttribute[]
{
(AspectAttribute)new LogAttribute(InterceptType.All),
(AspectAttribute)new PerformanceAttribute(InterceptType.All)
};
int num2;
try
{
this._OnEntryExit("OnEntry", aspectContext, array);
num2 = base.NYearLater(num);
aspectContext.Result = num2;
this._OnEntryExit("OnExit", aspectContext, array);
}
catch (Exception ex)
{
aspectContext.Result = ex;
this._OnEntryExit("OnEntry", aspectContext, new AspectAttribute[]
{
(AspectAttribute)new AspectExceptionAttribute(InterceptType.All)
});
}
finally
{
this._OnEntryExit("OnExit", aspectContext, new AspectAttribute[]
{
(AspectAttribute)new AspectExceptionAttribute(InterceptType.All)
});
}
return num2;
}
上边标红的代码中看以看到,除去异常处理外的所有特性先存储到一个基于其父类的AspectAttribute[]数组中,调用通用方法_OnEntryExit管理特性切入的触发,并在_OnEntryExit中处理拦截类型,有效的解决了问题1、2,下边我们看一下生成AspectAttribute[]数组和_OnEntryExit方法的Emit代码:
生成AspectAttribute[]数组:
private static LocalBuilder CreateAspectAttributeArray(ILGenerator il_ProxyMethod, object[] aspectAttributes)
{
int aspectCount = aspectAttributes.Length; LocalBuilder array = il_ProxyMethod.DeclareLocal(typeof(AspectAttribute[]));
//数组长度入栈
il_ProxyMethod.Emit(OpCodes.Ldc_I4, aspectCount);
//生成新数组
il_ProxyMethod.Emit(OpCodes.Newarr, typeof(AspectAttribute));
//赋值给局部数组变量
il_ProxyMethod.Emit(OpCodes.Stloc, array); //遍历参数,并存入数组
for (int i = ; i < aspectCount; i++)
{
il_ProxyMethod.Emit(OpCodes.Ldloc, array);//数组入栈
il_ProxyMethod.Emit(OpCodes.Ldc_I4, i);//数组下标入栈 var aspectType = aspectAttributes[i].GetType(); ConstructorInfo constructor = aspectType.GetConstructor(new Type[] { typeof(InterceptType) }); //获取当前特性拦截类型
il_ProxyMethod.Emit(OpCodes.Ldc_I4, (int)(aspectAttributes[i] as AspectAttribute).InterceptType); //生成新的特性对象
il_ProxyMethod.Emit(OpCodes.Newobj, constructor);
il_ProxyMethod.Emit(OpCodes.Castclass, typeof(AspectAttribute));
//存入数组
il_ProxyMethod.Emit(OpCodes.Stelem_Ref);
}
return array;
}
_OnEntryExit方法:
private static MethodBuilder CreateOnEntryExit(TypeBuilder typeBuilder)
{
var result = typeBuilder.DefineMethod("_OnEntryExit", MethodAttributes.Family | MethodAttributes.HideBySig | MethodAttributes.Virtual,
typeof(void), new Type[] { typeof(string), typeof(AspectContext), typeof(AspectAttribute[]) }); ILGenerator gen = result.GetILGenerator();
//临时变量i
LocalBuilder locI = gen.DeclareLocal(typeof(int));
Label lbCondition = gen.DefineLabel();
Label lbTrue = gen.DefineLabel(); //if 标签
Label lbIfEntryRet = gen.DefineLabel(); Label lbIfExitRet = gen.DefineLabel(); //i=0
gen.Emit(OpCodes.Ldc_I4_0);
gen.Emit(OpCodes.Stloc, locI); //跳至判断
gen.Emit(OpCodes.Br, lbCondition); //标记True代码
gen.MarkLabel(lbTrue); //声明拦截类型变量,存储当前切面特性的拦截类型 InterceptType interceptType = array[i].InterceptType;
LocalBuilder interceptType = gen.DeclareLocal(typeof(InterceptType)); gen.Emit(OpCodes.Ldarg_3);
gen.Emit(OpCodes.Ldloc, locI);
gen.Emit(OpCodes.Ldelem_Ref);
gen.Emit(OpCodes.Callvirt, typeof(AspectAttribute).GetMethod("get_InterceptType"));
gen.Emit(OpCodes.Box, typeof(InterceptType));
gen.Emit(OpCodes.Stloc, interceptType); //if 条件 if (text.Equals("OnEntry")
gen.Emit(OpCodes.Ldarg_1);
gen.Emit(OpCodes.Ldstr, "OnEntry");
gen.Emit(OpCodes.Call, typeof(String).GetMethod("Equals", new Type[] { typeof(string) }));
gen.Emit(OpCodes.Brfalse, lbIfEntryRet); //if 条件 && interceptType.HasFlag(InterceptType.OnEntry))
gen.Emit(OpCodes.Ldloc, interceptType);
gen.Emit(OpCodes.Ldc_I4, );
gen.Emit(OpCodes.Box, typeof(InterceptType));
gen.Emit(OpCodes.Call, typeof(Enum).GetMethod("HasFlag"));
gen.Emit(OpCodes.Brfalse, lbIfEntryRet); //True
gen.Emit(OpCodes.Ldarg_3);
gen.Emit(OpCodes.Ldloc, locI);
gen.Emit(OpCodes.Ldelem_Ref);
gen.Emit(OpCodes.Ldarg_2);
gen.Emit(OpCodes.Callvirt, typeof(AspectAttribute).GetMethod("OnEntry")); gen.MarkLabel(lbIfEntryRet); //if 条件 if (text.Equals("OnExit"))
gen.Emit(OpCodes.Ldarg_1);
gen.Emit(OpCodes.Ldstr, "OnExit");
gen.Emit(OpCodes.Call, typeof(String).GetMethod("Equals", new Type[] { typeof(string) }));
gen.Emit(OpCodes.Brfalse, lbIfExitRet); //if 条件 (&& interceptType.HasFlag(InterceptType.OnExit))
gen.Emit(OpCodes.Ldloc, interceptType);
gen.Emit(OpCodes.Ldc_I4, );
gen.Emit(OpCodes.Box, typeof(InterceptType));
gen.Emit(OpCodes.Call, typeof(Enum).GetMethod("HasFlag"));
gen.Emit(OpCodes.Brfalse, lbIfExitRet); //True
gen.Emit(OpCodes.Ldarg_3);
gen.Emit(OpCodes.Ldloc, locI);
gen.Emit(OpCodes.Ldelem_Ref);
gen.Emit(OpCodes.Ldarg_2);
gen.Emit(OpCodes.Callvirt, typeof(AspectAttribute).GetMethod("OnExit")); gen.MarkLabel(lbIfExitRet); //追加代码 //i++
gen.Emit(OpCodes.Ldloc, locI);
gen.Emit(OpCodes.Ldc_I4_1);
gen.Emit(OpCodes.Add);
gen.Emit(OpCodes.Stloc, locI); //判断代码
gen.MarkLabel(lbCondition);
gen.Emit(OpCodes.Ldloc, locI);
gen.Emit(OpCodes.Ldarg_3);
gen.Emit(OpCodes.Ldlen);
gen.Emit(OpCodes.Conv_I4); gen.Emit(OpCodes.Clt);
//如果True,跳至true代码
gen.Emit(OpCodes.Brtrue, lbTrue); //********
gen.Emit(OpCodes.Nop); gen.Emit(OpCodes.Ret); return result;
}
#endregion
这个方法稍微复杂了一些,有一个循环和两个判断,比直接写C#要难于理解。
增加两个特性,并修改ILGenerateProxyMethod方法解决问题3、4
作用于类的切面范围特性,如下:
/// <summary>
/// 切面范围特性
/// </summary>
[AttributeUsage(AttributeTargets.Class)]
public class AspectRangeAttribute : Attribute
{
#region 构造函数
/// <summary>
/// 构造函数
/// </summary>
public AspectRangeAttribute()
{
Type = XAOP.AspectRangeType.Method | XAOP.AspectRangeType.Setter;
} /// <summary>
/// 构造函数
/// </summary>
/// <param name="aspectType">切面类型</param>
public AspectRangeAttribute(AspectRangeType aspectType)
{
Type = aspectType;
}
#endregion public AspectRangeType Type { get; set; } } /// <summary>
/// 切面类型
/// </summary>
public enum AspectRangeType
{
#region 枚举值 /// <summary>
/// 构造函数
/// </summary>
Constructor = , /// <summary>
/// 方法
/// </summary>
Method = , /// <summary>
/// 属性的Set方法
/// </summary>
Setter = , /// <summary>
/// 属性的Get方法
/// </summary>
Getter = , /// <summary>
/// 所有
/// </summary>
All = #endregion
}
作用于方法和属性的不切入特性:
/// <summary>
/// 不切入特性
/// </summary>
[AttributeUsage(AttributeTargets.Method| AttributeTargets.Property)]
public class NoAspectAttribute : Attribute
{ }
如果切面范围特性的取值是AspectRangeType.Method | AspectRangeType.Setter;那么我们看一个属性对应的代码
public override string Name
{
get
{
return base.Name;
}
set
{
AspectContext aspectContext = new AspectContext(this, "set_Name", new object[]
{
value
});
AspectAttribute[] array = new AspectAttribute[]
{
(AspectAttribute)new LogAttribute(InterceptType.OnExit)
};
this._OnEntryExit("OnEntry", aspectContext, array);
base.Name = value;
this._OnEntryExit("OnExit", aspectContext, array);
}
}
我们可以看到Get方法只是重写了一下,而Set方法进行了切入,这样我们就需要一个重写方法和一个切入后的代理方法,而ILGenerateProxyMethod只实现切入方法,下边我们将去除ILGenerateProxyMethod方法,将其拆分为两个类,一个类(ILOverrideMethod)实现重写方法,一个类(ILProxyMethod)实现切入后的代理方法(它继承ILOverrideMethod),而一个方法调用大概是三个阶段,执行方法体、获取返回值、返回;下边我们看两个类的实现:
ILOverrideMethod:
/// <summary>
/// 重写方法
/// </summary>
public class ILOverrideMethod
{
#region 构造函数
/// <summary>
/// 构造函数
/// </summary>
/// <param name="srcMethod">源方法</param>
/// <param name="overrideIL">override方法IL</param>
public ILOverrideMethod(MethodInfo srcMethod, ILGenerator overrideIL)
{
SrcMethod = srcMethod; OverrideMethodIL = overrideIL; ParamTypes = XDynamic.GetMethodParameterTypes(SrcMethod);
}
#endregion #region 属性 /// <summary>
/// 源方法
/// </summary>
public MethodInfo SrcMethod { get; set; } /// <summary>
/// 重写方法IL
/// </summary>
public ILGenerator OverrideMethodIL { get; set; } #endregion #region Protected /// <summary>
/// 返回值
/// </summary>
protected LocalBuilder Result; /// <summary>
/// 方法参数列表
/// </summary>
protected Type[] ParamTypes; #endregion #region 生成重写方法 Create
/// <summary>
/// 生成重写方法
/// </summary>
public virtual void Create()
{
CreateMethodBody();
57
58 GetResult();
59
60 Return();
}
#endregion #region 创建方法体 CreateMethodBody
/// <summary>
/// 创建方法体
/// </summary>
protected virtual void CreateMethodBody()
{
//类对象,参数值依次入栈并调用基类的方法
for (int i = ; i <= ParamTypes.Length; i++)
OverrideMethodIL.Emit(OpCodes.Ldarg, i); OverrideMethodIL.Emit(OpCodes.Call, SrcMethod);
}
#endregion #region 获取结果 GetResult
/// <summary>
/// 获取结果
/// </summary>
protected virtual void GetResult()
{
//如果有返回值,保存返回值到局部变量
if (SrcMethod.ReturnType != typeof(void))
{
Result = OverrideMethodIL.DeclareLocal(SrcMethod.ReturnType);
OverrideMethodIL.Emit(OpCodes.Stloc, Result);
}
}
#endregion #region 返回 Return
/// <summary>
/// 返回
/// </summary>
protected virtual void Return()
{
//如果有返回值,则把返回值压栈
if (Result != null)
OverrideMethodIL.Emit(OpCodes.Ldloc, Result); OverrideMethodIL.Emit(OpCodes.Ret);//返回
}
#endregion
}
ILProxyMethod:
/// <summary>
/// 代理方法
/// </summary>
public class ILProxyMethod : ILOverrideMethod
{
#region 构造函数
/// <summary>
/// 构造函数
/// </summary>
/// <param name="srcMethod">源方法</param>
/// <param name="il"></param>
/// <param name="aspectAttributes">切面特性集合</param>
/// <param name="onEntryExit">切面入口出口统一调用方法</param>
public ILProxyMethod(MethodInfo srcMethod, ILGenerator il, object[] aspectAttributes, MethodInfo onEntryExit)
: base(srcMethod, il)
{
OnEntryExit = onEntryExit; AspectExceptionAttributes = aspectAttributes.Where(p => p.GetType() == typeof(AspectExceptionAttribute)).ToArray(); HandleException = AspectExceptionAttributes.Length > ; //生成切面上下文
AspectContext = CreateAspectContext(OverrideMethodIL, SrcMethod.Name, ParamTypes);
//生成除去异常特性外的特性数组
AspectAttributes = CreateAspectAttributeArray(OverrideMethodIL,
aspectAttributes.Where(p => p.GetType() != typeof(AspectExceptionAttribute)).ToArray());
}
#endregion #region Protected /// <summary>
/// 切面入口出口统一调用方法
/// </summary>
protected MethodInfo OnEntryExit { get; set; } /// <summary>
/// 切面上下文
/// </summary>
protected LocalBuilder AspectContext; /// <summary>
/// 除去异常外的切面特性数组
/// </summary>
protected LocalBuilder AspectAttributes { get; set; } /// <summary>
/// 异常特性
/// </summary>
protected object[] AspectExceptionAttributes; /// <summary>
/// 是否处理异常
/// </summary>
protected bool HandleException = false; #endregion #region 创建方法体 CreateMethodBody
/// <summary>
/// 创建方法体
/// </summary>
protected override void CreateMethodBody()
{
if (HandleException)
OverrideMethodIL.BeginExceptionBlock(); //调用横切对象的OnEntryt方法
CallOn_Entry_Exit(OverrideMethodIL, true, AspectContext, AspectAttributes, OnEntryExit); base.CreateMethodBody();
}
#endregion #region 获取结果 GetResult
/// <summary>
/// 获取结果
/// </summary>
protected override void GetResult()
{
base.GetResult(); if (SrcMethod.ReturnType != typeof(void))
{
//给AspectContext的属性Result赋值
var resultSetMethod = typeof(AspectContext).GetMethod("set_Result");
OverrideMethodIL.Emit(OpCodes.Ldloc, AspectContext); //加载AspectContext局部变量
OverrideMethodIL.Emit(OpCodes.Ldloc, Result);//加载返回值
OverrideMethodIL.Emit(OpCodes.Box, SrcMethod.ReturnType);
OverrideMethodIL.Emit(OpCodes.Call, resultSetMethod);//赋值
}
}
#endregion #region 返回
/// <summary>
///
/// </summary>
protected override void Return()
{
//调用横切对象的OnExit方法
CallOn_Entry_Exit(OverrideMethodIL, false, AspectContext, AspectAttributes, OnEntryExit); if (HandleException)
{
AspectExceptionAttribute temp = AspectExceptionAttributes[] as AspectExceptionAttribute; OverrideMethodIL.BeginCatchBlock(typeof(Exception)); //保存Exception到临时变量
LocalBuilder exception = OverrideMethodIL.DeclareLocal(typeof(Exception)); OverrideMethodIL.Emit(OpCodes.Stloc, exception); //复制Exception到Result
var resultSetMethod = typeof(AspectContext).GetMethod("set_Result");
OverrideMethodIL.Emit(OpCodes.Ldloc, AspectContext); //加载AspectContext局部变量
OverrideMethodIL.Emit(OpCodes.Ldloc, exception);//错误信息
OverrideMethodIL.Emit(OpCodes.Box, typeof(Exception));
OverrideMethodIL.Emit(OpCodes.Call, resultSetMethod);//赋值 //调用OnEntry
LocalBuilder array = CreateAspectAttributeArray(OverrideMethodIL, AspectExceptionAttributes); CallOn_Entry_Exit(OverrideMethodIL, true, AspectContext, array, OnEntryExit); //触发Finally
if (temp.InterceptType.HasFlag(InterceptType.OnExit))
{
OverrideMethodIL.BeginFinallyBlock(); //调用Exit
array = CreateAspectAttributeArray(OverrideMethodIL, AspectExceptionAttributes); CallOn_Entry_Exit(OverrideMethodIL, false, AspectContext, array, OnEntryExit);
} OverrideMethodIL.EndExceptionBlock();
} base.Return();
}
#endregion
}
通过上边的代码我们可以看到,ILGenerateProxyMethod就拆分为一个类的三个方法,同时也灵活的处理了异常特性(if (HandleException),这里边新增了一个CallOn_Entry_Exit方法,其对应代码如下:
private static void CallOn_Entry_Exit(ILGenerator il_ProxyMethod, bool entry, LocalBuilder aspectContext, LocalBuilder array, MethodInfo onEntryExit)
{
il_ProxyMethod.Emit(OpCodes.Ldarg_0);
il_ProxyMethod.Emit(OpCodes.Ldstr, entry ? "OnEntry" : "OnExit");
il_ProxyMethod.Emit(OpCodes.Ldloc, aspectContext);
il_ProxyMethod.Emit(OpCodes.Ldloc, array); il_ProxyMethod.Emit(OpCodes.Call, onEntryExit);
}
#endregion
测试
测试类修改如下:
public class AopTest
{ public AopTest()
{
Name = "小明"; Age = ;
} public AopTest(string name, int age)
{
Name = name; Age = age;
} [Log(InterceptType.OnExit)]
public virtual string Name { get; set; } [Log(InterceptType.OnEntry)]
public virtual int Age { get; set; } [Log]
[AspectException]
public virtual int NYearLater(int a)
{
int larter = Age + a; return larter;
}
}
结果:
Log OnExit: set_Name Log OnEntry:set_Age() Log OnEntry:NYearLater()
Log OnExit: NYearLater Result:
Finally : NYearLater done...
比对结果,达到了我们的目的。
C# 使用Emit实现动态AOP框架 进阶篇之优化的更多相关文章
- C# 使用Emit实现动态AOP框架 进阶篇之异常处理
目 录 C# 使用Emit实现动态AOP框架 (一) C# 使用Emit实现动态AOP框架 (二) C# 使用Emit实现动态AOP框架 (三) C# 使用Emit实现动态AOP框架 进阶篇之异常处 ...
- C# 使用Emit实现动态AOP框架 (三)
目 录 C# 使用Emit实现动态AOP框架 (一) C# 使用Emit实现动态AOP框架 (二) C# 使用Emit实现动态AOP框架 (三) C# 使用Emit实现动态AOP框架 进阶篇之异常处 ...
- C# 使用Emit实现动态AOP框架 (二)
目 录 C# 使用Emit实现动态AOP框架 (一) C# 使用Emit实现动态AOP框架 (二) C# 使用Emit实现动态AOP框架 (三) C# 使用Emit实现动态AOP框架 进阶篇之异常处 ...
- C# 使用Emit实现动态AOP框架 (一)
目 录 C# 使用Emit实现动态AOP框架 (一) C# 使用Emit实现动态AOP框架 (二) C# 使用Emit实现动态AOP框架 (三) C# 使用Emit实现动态AOP框架 进阶篇之异常处 ...
- (6)MySQL进阶篇SQL优化(MyISAM表锁)
1.MySQL锁概述 锁是计算机协调多个进程或线程并发访问某一资源的机制.在数据库中,除传统的计算资源 (如 CPU.RAM.I/O 等)的抢占以外,数据也是一种供许多用户共享的资源.如何保证数 据并 ...
- hadoop之yarn详解(框架进阶篇)
前面在hadoop之yarn详解(基础架构篇)这篇文章提到了yarn的重要组件有ResourceManager,NodeManager,ApplicationMaster等,以及yarn调度作业的运行 ...
- 【Spring AOP】Spring AOP之如何通过注解的方式实现各种通知类型的AOP操作进阶篇(3)
一.切入点表达式的各种类型 切入点表达式的作用:限制连接点的匹配(满足时对应的aspect方法会被执行) 1)execution:用于匹配方法执行连接点.Spring AOP用户可能最经常使用exec ...
- (2)MySQL进阶篇SQL优化(show status、explain分析)
1.概述 在应用系统开发过程中,由于初期数据量小,开发人员写SQL语句时更重视功能上的实现,但是当应用系统正式上线后,随着生产数据量的急剧增长,很多SQL语句开始逐渐显露出性能问题,对生产环境的影响也 ...
- (3)MySQL进阶篇SQL优化(索引)
1.索引问题 索引是数据库优化中最常用也是最重要的手段之一,通过索引通常可以帮助用户解决大多数 的SQL性能问题.本章节将对MySQL中的索引的分类.存储.使用方法做详细的介绍. 2.索引的存储分类 ...
随机推荐
- session cookie傻傻分不清
做了这么多年测试,还是分不清什么是cookie,什么是session?很正常,很多初级开发工程师可能到现在都搞不清什么是session,cookie相对来说会简单很多. 下面这篇文章希望能够帮助大家分 ...
- ZT:在mybatis的Mapping文件写入表名 出现异常ORA-00903: 表名无效 的解决
简而言之,把#{tablename}换成${tablename}就能解决问题. 版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明.本文链接:htt ...
- 在Ubuntu 16.04 LTS下编译安装OpenCV 4.1.1
目录 一 安装前的准备 二 编译并安装OpenCV 4.1.1 注:原创不易,转载请务必注明原作者和出处,感谢支持! OpenCV目前(2019-8-1)的最新版本为4.1.1.本文将介绍如何在Ubu ...
- webpy简单入门---1
1. 2. 3. 4.
- Dubbo ==> 简介
一.架构发展过程 首先,我们先来看看上面的架构发展的线路图:单一应用架构 --> 垂直应用架构 --> 分布式服务架构 --> 流动计算架构 . 单一应用架构 在一些中小型的传统软件 ...
- java+ueditor word粘贴上传
最近公司做项目需要实现一个功能,在网页富文本编辑器中实现粘贴Word图文的功能. 我们在网站中使用的Web编辑器比较多,都是根据用户需求来选择的.目前还没有固定哪一个编辑器 有时候用的是UEditor ...
- php获取服务器ip方法
public static function getServerIp() { if(!empty($_SERVER['SERVER_ADDR'])) { return $_SERVER['SERVER ...
- TLC编程
NAND Flash可以划分为SLC.MLC和TLC SLC:单阶存储单元,读写速率快,可擦写次数高 MLC和TLC:多阶存储单元,MLC每个存储单元中存储2 bit数据,可以表示四种数据:SLC每个 ...
- C#中?的相关使用
C#中?的相关使用 今天看了几篇博客,学习了一下与?相关的使用,大致分为一下几种: 1. 可空类型 看标题就能够很好的理解这个概念:可以为空的类型.而在C#中可以为空也就是null的类型,都是引用类型 ...
- C语言递归之翻转二叉树
题目描述 翻转一棵二叉树. 示例 输入: / \ / \ / \ 输出: / \ / \ / \ 题目要求 /** * Definition for a binary tree node. * str ...