sun jdk 与jdk_Sun过去的世界中的JDK 11和代理

sun jdk 与jdk

使用JDK 11后,就sun.misc.Unsafe的第一种方法。 其中, defineClass方法已删除。 代码生成框架通常使用此方法在现有的类加载器中定义新的类。 尽管此方法易于使用,但它的存在也使JVM本质上不安全,正如其定义类的名称所暗示的那样。 通过允许在任何类加载器和程序包中定义一个类,可以通过在其中定义一个类来获得对任何程序包的程序包范围访问,从而突破了原本封装的程序包或模块的边界。

为了删除sun.misc.Unsafe的目标,OpenJDK开始提供在运行时定义类的替代方法。 从版本9开始, MethodHandles.Lookup类提供了类似于不安全版本的方法defineClass 。 但是,类定义仅适用于与查找的宿主类位于同一包中的类。 由于模块只能解析对某个模块拥有或已打开的包的查找,因此无法将类再注入到不打算提供此类访问权限的包中。

使用方法句柄查找,可以在运行时定义类foo.Qux ,如下所示:

MethodHandles.Lookup lookup = MethodHandles.lookup();
MethodHandles.Lookup privateLookup = MethodHandles.privateLookupIn(foo.Bar.class, lookup);
byte[] fooQuxClassFile = createClassFileForFooQuxClass();
privateLookup.defineClass(fooQuxClassFile);

为了执行类定义,需要MethodHandles.Lookup的实例,可以通过调用MethodHandles::lookup方法来检索该MethodHandles::lookup 。 调用后一种方法对呼叫点敏感。 因此,返回的实例将代表从方法内部调用的类和包的特权。 要在另一个包中定义一个类,然后在当前包中定义一个类,则需要使用MethodHandles::privateLookupIn对此包中的类进行解析。 仅当此目标类的程序包与原始查找类位于同一模块中,或者此包显式打开到查找类的模块时,才有可能。 如果不满足这些要求,则尝试解决私有查找将引发IllegalAccessException ,从而保护JPMS隐含的边界。

当然,代码生成库也受此限制的约束。 否则,它们可能被用来创建和注入恶意代码。 而且由于方法句柄的创建对调用站点敏感,因此在不要求用户通过提供表示其模块特权的适当查找实例的情况下,不要求用户做一些额外工作的情况下就不可能合并新的类定义机制。

使用Byte Buddy时,所需的更改很小。 该库使用ClassDefinitionStrategy定义类,该类负责从其二进制格式加载类。 在Java 11之前,可以使用Reflection或sun.misc.Unsafe使用ClassDefinitionStrategy.Default.INJECTION定义一个类。 为了支持Java 11,此策略需要由ClassDefinitionStrategy.UsingLookup.of(lookup)代替,在ClassDefinitionStrategy.UsingLookup.of(lookup)中,提供的查找必须有权访问将驻留类的包。

将cglib代理迁移到Byte Buddy

到目前为止,其他代码生成库尚未提供这种机制,并且尚不确定何时以及是否添加了这种功能。 尤其是对于cglib而言,由于库的过时以及在不再更新且不会采用修改的遗留应用程序中的广泛使用,过去已证明API更改会带来问题。 对于希望采用Byte Buddy作为更现代且积极开发的替代产品的用户,因此以下部分将介绍可能的迁移。

例如,我们使用一个方法为以下示例类生成代理:

public class SampleClass {public String test() { return "foo"; }
}

为了创建代理,通常在所有方法都被覆盖以派发侦听逻辑的情况下对代理类进行子类化。 为此,作为示例,我们将一个值栏附加到原始实现的返回值上。

通常使用Enhancer类和MethodInterceptor一起定义cglib代理。 方法拦截器提供代理实例,代理方法及其参数。 最后,它还提供了MethodProxy的实例,该实例允许调用原始代码。

Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(SampleClass.class);
enhancer.setCallback(new MethodInterceptor() {@Overridepublic Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) {return proxy.invokeSuper(obj, method, args) + "bar";}
});
SampleClass proxy = (SampleClass) enhancer.create();
assertEquals("foobar", proxy.test());

请注意,如果在代理实例上调用了诸如hashCodeequalstoString类的任何其他方法,则上述代码将引起问题。 前两个方法也将由拦截器分派,因此当cglib尝试返回字符串类型的返回值时,将导致类强制转换异常。 相反, toString方法可以工作,但是会返回意外的结果,因为原始实现的前缀是bar作为返回值。

在Byte Buddy中,代理不是专门的概念,但是可以使用库的通用代码生成DSL进行定义。 对于与cglib最相似的方法,使用MethodDelegation提供了最简单的迁移路径。 这样的委派以用户定义的拦截器类为目标,方法调用将调度到该类:

public class SampleClassInterceptor {public static String intercept(@SuperCall Callable<String> zuper) throws Exception {return zuper.call() + "bar";}
}

上面的拦截器首先通过由Byte Buddy提供的帮助程序实例来调用原始代码。 使用Byte Buddy的代码生成DSL来实现对此拦截器的委托,如下所示:

SampleClass proxy = new ByteBuddy().subclass(SampleClass.class).method(ElementMatchers.named("test")).intercept(MethodDelegation.to(SampleClassInterceptor.class)).make().load(someClassLoader, ClassLoadingStrategy.UsingLookup.of(MethodHandles.privateLookupIn(SampleClass.class, MethodHandles.lookup())).getLoaded().getDeclaredConstructor().newInstance();
assertEquals("foobar", proxy.test());

除了cglib之外,Byte Buddy还需要使用ElementMatcher指定方法过滤器。 尽管在cglib中完全可以进行过滤,但是它非常麻烦并且没有明确要求,因此很容易被遗忘。 在Byte Buddy中,仍然可以使用ElementMatchers.any()匹配器拦截所有方法,但是通过要求指定这样的匹配器,希望提醒用户做出有意义的选择。

使用上述匹配器,每当调用名为test的方法时,都会使用所讨论的方法委派将调用委派给指定的拦截器。

但是,引入的拦截器将无法分派不返回字符串实例的方法。 实际上,代理创建会产生由Byte Buddy发出的异常。 但是,完全有可能定义一个更通用的拦截器,该拦截器可应用于与cglib的MethodInterceptor提供的方法类似的任何方法:

public class SampleClassInterceptor {@RuntimeTypepublic static Object intercept(@Origin Method method,@This Object self,@AllArguments Object[] args,@SuperCall Callable<String> zuper) throws Exception {return zuper.call() + "bar";}
}

当然,由于在这种情况下不使用拦截器的其他参数,因此可以省略它们,从而使代理更有效。 Byte Buddy仅在需要时才提供论据,如果实际需要的话。

由于上述代理是无状态的,因此将拦截方法定义为静态。 同样,这是一个简单的优化,因为Byte Buddy否则需要在代理类中定义一个字段,该字段包含对拦截器实例的引用。 但是,如果需要实例,则可以使用MethodDelegation.to(new SampleClassInterceptor())将委托定向到实例的成员方法。

缓存代理类以提高性能

使用Byte Buddy时,不会自动缓存代理类。 这意味着每次运行上述代码时,都会生成并加载一个新类。 由于代码生成和类定义是昂贵的操作,因此这当然效率低下,如果可以重复使用代理类,则应避免这种情况。 在cglib中,如果两次增强的输入相同,则返回先前生成的类,这通常在两次运行同一代码段时是正确的。 然而,由于通常可以更容易地计算缓存密钥,因此该方法相当容易出错并且通常效率低下。 使用字节伙伴,可以使用专用的缓存库(如果已有的话)。 另外,Byte Buddy还提供了TypeCache ,它通过用户定义的缓存键为类实现了简单的缓存。 例如,可以使用以下代码使用基类作为键来缓存以上类的生成:

TypeCache<Class<?>> typeCache = new TypeCache<>(TypeCache.Sort.SOFT);
Class<?> proxyType = typeCache.findOrInsert(classLoader, SampleClass.class, () -> new ByteBuddy().subclass(SampleClass.class).method(ElementMatchers.named("test")).intercept(MethodDelegation.to(SampleClassInterceptor.class)).make().load(someClassLoader, ClassLoadingStrategy.UsingLookup.of(MethodHandles.privateLookupIn(SampleClass.class, MethodHandles.lookup())).getLoaded()
});

不幸的是,Java中的缓存类带来了一些警告。 如果创建了代理,则它当然会继承它所代理的类的子类,该类使该基类不适合进行垃圾收集。 因此,如果代理类被强引用,则密钥也将被强引用。 这将使高速缓存无用,并因内存泄漏而打开。 因此,必须通过构造函数参数指定的内容来轻而易举地引用代理类。 将来,如果Java引入了星历作为参考类型,则可能会解决此问题。 同时,如果不存在代理类垃圾回收的问题,则可以使用ConcurrentMap在不存在时计算值。

拓宽代理类的可用性

为了包含代理类的重用,将代理类重构为无状态并将状态隔离到实例字段中通常是有意义的。 然后,可以在侦听期间使用提到的依赖项注入机制来访问此字段,例如,以使后缀值可针对每个代理实例进行配置:

public class SampleClassInterceptor {public static String intercept(@SuperCall Callable<String> zuper, @FieldValue("qux") String suffix) throws Exception {return zuper.call() + suffix;}
}

上面的拦截器现在接收字段qux的值作为第二个参数,可以使用Byte Buddy的类型创建DSL声明它:

TypeCache<Class<?>> typeCache = new TypeCache<>(TypeCache.Sort.SOFT);
Class<?> proxyType = typeCache.findOrInsert(classLoader, SampleClass.class, () -> new ByteBuddy().subclass(SampleClass.class).defineField(“qux”, String.class, Visibility.PUBLIC).method(ElementMatchers.named("test")).intercept(MethodDelegation.to(SampleClassInterceptor.class)).make().load(someClassLoader, ClassLoadingStrategy.UsingLookup.of(MethodHandles.privateLookupIn(SampleClass.class, MethodHandles.lookup())).getLoaded()
});

现在,可以使用Java反射在每个实例创建后在每个实例上设置该字段值。 为了避免反射,DSL还可以用于实现一些接口,该接口声明用于所提及字段的设置方法,可以使用Byte Buddy的FieldAccessor实现来实现。

加权代理运行时和创建性能

最后,在使用Byte Buddy创建代理时,需要考虑一些性能。 在生成代码时,需要在代码生成本身的性能与所生成代码的运行时性能之间进行权衡。 与cglib或其他proxing库相比,Byte Buddy通常旨在创建尽可能高效地运行的代码,这可能需要更多时间来创建此类代码。 这是基于这样的假设,即大多数应用程序运行时间很长,但是一次只能创建代理,但是代理不适用于所有类型的应用程序。

与cglib的一个重要区别是,Byte Buddy为每个方法生成一个专用的超级调用委托,该方法被拦截,而不是单个MethodProxy 。 这些额外的类需要花费更多的时间来创建和加载,但是使这些类可用可以为每个方法执行带来更好的运行时性能。 如果在循环中调用代理方法,则这种差异很快就很关键。 但是,如果运行时性能不是主要目标,并且在短时间内创建代理类更重要,则以下方法可避免完全创建其他类:

public class SampleClassInterceptor {public static String intercept(@SuperMethod Method zuper, @This Object target, @AllArguments Object[] arguments) throws Exception {return zuper.invoke(target, arguments) + "bar";}
}

模块化环境中的代理

对拦截器使用简单形式的依赖注入,而不是依赖于特定于库的类型,例如cglib的
MethodInterceptor ,Byte Buddy在模块化环境中提供了另一个优势:由于生成的代理类将直接引用拦截器类,而不是引用库特定的调度程序类型(例如cglib的MethodInterceptor ,因此被代理类的模块不需要读取Byte Buddy的模块。 对于cglib,代理的类模块必须读取cglib的模块,该模块定义了MethodInterceptor接口,而不是实现该接口的模块。 对于使用cglib作为传递依赖的库的用户,这很可能是不直观的,尤其是如果将后者依赖视为不应公开的实现细节时,尤其如此。

在某些情况下,代理类的模块读取提供拦截器的框架模块甚至是不可能或不希望的。 对于这种情况,Byte Buddy还提供了一种解决方案,通过使用它来完全避免这种依赖性
Advice组件。 该组件可用于以下示例中的代码模板:

public class SampleClassAdvice {@Advice.OnMethodExitpublic static void intercept(@Advice.Returned(readOnly = false) String returned) {returned += "bar";}
}

上面的代码看起来似乎没有多大意义,实际上,它将永远不会执行。 该类仅用作Byte Buddy的字节代码模板,后者可读取带注释的方法的字节代码,然后将其内联到生成的代理类中。 为此,必须对上述方法的每个参数进行注释,以代表代理方法的值。 在上述情况下,注释定义了参数,以定义方法的返回值,在给定模板的情况下,将bar添加为后缀。 给定此建议类,可以如下定义代理类:

new ByteBuddy().subclass(SampleClass.class).defineField(“qux”, String.class, Visibility.PUBLIC).method(ElementMatchers.named(“test”)).intercept(Advice.to(SampleClassAdvice.class).wrap(SuperMethodCall.INSTANCE)).make()

通过将建议包装在SuperMethodCall周围,​​在对重写方法进行调用之后,将内联上述建议代码。 要在原始方法调用之前内联代码,可以使用OnMethodEnter批注。

9和10之前的Java版本上的支持代理

在为JVM开发应用程序时,通常可以依赖在特定版本上运行的应用程序,也可以在更高版本上运行。 即使使用了内部API,也已经有很长时间了。 但是,由于删除了此内部API,从Java 11开始,情况不再如此,依赖于sun.misc.Unsafe代码生成库将不再起作用。 同时,通过MethodHandles.Lookup类定义MethodHandles.Lookup用于版本9之前的JVM。

对于Byte Buddy,用户有责任使用与当前JVM兼容的类加载策略。 为了支持所有JVM,需要进行以下选择:

ClassLoadingStrategy<ClassLoader> strategy;
if (ClassInjector.UsingLookup.isAvailable()) {Class<?> methodHandles = Class.forName("java.lang.invoke.MethodHandles");Object lookup = methodHandles.getMethod("lookup").invoke(null);Method privateLookupIn = methodHandles.getMethod("privateLookupIn", Class.class, Class.forName("java.lang.invoke.MethodHandles$Lookup"));Object privateLookup = privateLookupIn.invoke(null, targetClass, lookup);strategy = ClassLoadingStrategy.UsingLookup.of(privateLookup);
} else if (ClassInjector.UsingReflection.isAvailable()) {strategy = ClassLoadingStrateg.Default.INJECTION;
} else {throw new IllegalStateException(“No code generation strategy available”);
}

上面的代码使用反射来解决方法句柄查找并加以解决。 这样做,可以在Java 9之前的JDK上编译和加载代码。不幸的是,由于MethodHandles::lookup是调用站点敏感的,因此Byte Buddy无法实现此代码,因此必须在驻留在其中的类中定义以上内容。用户模块,而不在Byte Buddy中。

最后,值得考虑的是完全避免类注入。 代理类也可以使用ClassLoadingStrategy.Default.WRAPPER策略在自己的类加载器中定义。 此策略不使用任何内部API,并且可以在任何JVM版本上使用。 但是,必须牢记创建专用类加载器的性能成本。 最后,即使代理类的软件包名称与代理类相同,通过在不同的类加载器中定义代理,JVM将不再将其运行时软件包视为相等,因此不允许覆盖任何软件包,私人方法。

最后的想法

最后一点,我想表达我的观点,尽管迁移成本很高,但退出sun.misc.Unsafe是朝着更安全,模块化的JVM迈出的重要一步。 在删除此非常强大的类之前,可以使用sun.misc.Unsafe仍然提供的特权访问来绕过JPMS设置的任何边界。 如果不进行此删除,则JPMS会付出额外封装带来的所有不便,而无法依赖它。

JVM上的大多数开发人员很可能永远不会遇到这些附加限制的任何问题,但是如上所述,代码生成和代理库需要适应这些更改。 对于cglib,不幸的是,这确实意味着道路的尽头。 Cglib最初被建模为Java内置代理API的更强大版本,在该版本中,Cglib要求代理类引用其自己的调度程序API,类似于Java API要求引用其类型的方式。 但是,这些后面的类型驻留在java.base模块中,该模块始终由任何模块读取。 因此,Java代理API仍然可以正常工作,而cglib模型则无法修复。 过去,这已经使cglib成为OSGi环境的一个困难候选者,但是对于JPMS,作为库的cglib不再起作用。 Javassist提供的相应代理API存在类似问题。

这种变化的好处是,JVM最终提供了一个稳定的API,用于在应用程序运行时定义类,这是一种依赖内部API二十多年的常见操作。 除了我认为仍然需要更灵活方法的Javaagents之外,这意味着在所有代理用户完成此最终迁移之后,可以保证将来的Java版本始终能够正常工作。 鉴于cglib的开发多年来一直处于Hibernate状态,并且该库受到许多限制,因此无论如何,如今的库用户最终迁移都是不可避免的。 Javassist代理可能也是如此,因为后者的库在近半年内也没有提交。

翻译自: https://www.javacodegeeks.com/2018/04/jdk-11-and-proxies-in-a-world-past-sun-misc-unsafe.html

sun jdk 与jdk

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/333639.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

php mysql上传多张图片_PHP开发之多个文件上传到MySql数据库(一)

前面的章节我们介绍了用PHP实现上传一个文件的教程。朋友们就会有疑问&#xff0c;怎么样才能上传多张多个文件到数据库&#xff1f;重点在于放入几个文件以后点击提交上传之后所有的文件一起上传&#xff0c;并且每个文件都给一个新的路径。提供一种思路&#xff1a;先获取每个…

jdk只有一个java进程_JDK 10:从Java访问Java应用程序的进程ID

jdk只有一个java进程StackOverflow.com上一个普遍的问题是&#xff1a;“ Java程序如何获得自己的进程ID&#xff1f; 与该问题相关的几个答案包括解析ManagementFactory返回的String 。 getRuntimeMXBean&#xff08;&#xff09; 。 getName&#xff08;&#xff09; [但是可…

Shell 脚本生成不重复的随机数

#!/bin/bash #AUTHOR:AN #DATE:2019-3-24 #Describe:Generate No-Repeat Random-Number #Method:如生成3~7的随机数&#xff0c;先生成0~4&#xff08;7-3&#xff09;的随机数&#xff0c;再加上3&#xff08;起始值&#xff09;来修正 #####################################…

python if语句能否判断中文_Python“if”语句被忽略

如果满足多个条件(“or”)中的一个&#xff0c;我将触发Python中的循环。脚本似乎跳过了“if”语句并在不满足所需条件的情况下进入内部循环。在编码# Begin TestCase# Main logic: execute RemoteController macro, if expected state true, set Success, else: Failfor macr…

payara 创建 集群_Payara Micro在Oracle应用容器云上

payara 创建 集群在此博客文章中&#xff0c;我将描述如何将打包在Payara Microber -jar中的CloudEE Duke应用程序部署到Oracle Application Container Cloud 。 在Oracle Application Container Cloud中进行部署所需的部署工件是一个ZIP归档文件&#xff0c;其中包含应用程序…

shell 脚本书写规范

表达式中的运算符之间不允许空格&#xff0c;例如&#xff1a; [roothtlwk0001host ~]# sum 200 300 sum: : 没有那个文件或目录 sum: 200: 没有那个文件或目录 sum: : 没有那个文件或目录 sum: 300: 没有那个文件或目录从上面的输出结果可知&#xff0c;每项前后出现空格&a…

python可以用来整理表格吗_Python将多份excel表格整理成一份表格

利用Python将多份excel表格整理成一份表格&#xff0c;抛弃过去逐份打开复制粘贴的方式。直接附上代码&#xff1a; import xlrdimport xlwtimport osfrom xlutils.copy import copyimport os.pathfrom xlwt import *dir input("输入文件路径\n");start_row input(…

Linux 环境变量启动过程/配置文件的读取过程

环境变量配置文件 对所有用户都起作用 /etc/profile/etc/profile.d/*.sh/etc/bashrc 对当前用户起作用 配置文件在用户家目录下&#xff0c;即用户的主目录下。 ~/.bash_profile~/.bashrc 环境变量启动过程 新进程启动后先将父进程的全局性的环境变量复制一份到自己的栈…

azure blob_使用Azure Blob存储托管Maven工件

azure blob如果您使用Microsoft Azure并且将Java用于项目&#xff0c;则Azure Blob存储是托管团队工件的理想场所。 它很容易设置&#xff0c;而且很便宜。 如果您对它们的功能不特别感兴趣&#xff0c;那么它比设置现有存储库选项&#xff08;jfrog&#xff0c;nexus&#xf…

angular ngoninit 刷新html页面_web前端入门到实战:实现html页面自动刷新

使用场景&#xff1a;页面需要定时刷新&#xff0c;实时加载数据&#xff0c;需要实时查看监控数据&#xff08;H5中的WebSocket和SSE可以实现局部刷新&#xff09;一定时间之后跳转到指定页面&#xff08;登录注册之类&#xff09;前端开发使用伪数据调试html页面&#xff08;…

什么是超越数

超越数&#xff0c;数学概念&#xff0c;指不是代数数的数。超越数的存在是由法国数学家刘维尔&#xff08;Joseph Liouville&#xff0c;1809 ~ 1882&#xff09;在1844年最早证明的。关于超越数的存在&#xff0c;刘维尔写出了下面这样一个无限小数&#xff1a;a0.1100010000…

认识JSON补丁:JSON-P 1.1概述系列

Java EE 8包括对JSON处理API的更新&#xff0c;并使其与JSON的最新IEFT标准保持同步。 他们是&#xff1a; JSON指针 RFC 6901 JSON补丁 RFC 6902 JSON合并修补程序RFC 7396 我将在这个迷你系列中涵盖这些主题。 入门 要开始使用JSON-P&#xff0c;您将需要Maven中央存储库…

mysql驱动连接不了mariadb_无法从振动应用程序连接到MySQL / MariaDB数据库

如果我使用自定义main(void main()而不是shared static this()),一切正常.使用默认主菜单时出现“访问冲突”错误.它看起来像MySQL不允许从localhost连接到它,但在my.ini中我添加了字符串&#xff1a;bind-address 127.0.0.1代码,如果有帮助&#xff1a;import std.stdio;impo…

什么是自然数

自然数是指用以计量事物的件数或表示事物次序的数。即用数码 0&#xff0c;1&#xff0c;2&#xff0c;3&#xff0c;4……所表示的数。自然数由 0 开始&#xff0c;一个接一个&#xff0c;组成一个无穷的集体。自然数有有序性&#xff0c;无限性。分为偶数和奇数&#xff0c;合…

angular jwt_Angular5 JWT身份验证(Spring Boot安全性)

angular jwt欢迎使用带有Spring Security的angular5 jwt身份验证。在本教程中&#xff0c;我们将在一个angular5单页应用程序中使用jwt身份验证创建一个全栈应用程序&#xff0c;该应用程序具有由spring boot支持并支持spring security集成的后备服务器。带有集成了HttpInterce…

impacket安装 python_Impacket网络协议工具包介绍

Impacket是一个Python类库&#xff0c;用于对SMB1-3或IPv4 / IPv6 上的TCP、UDP、ICMP、IGMP&#xff0c;ARP&#xff0c;IPv4&#xff0c;IPv6&#xff0c;SMB&#xff0c;MSRPC&#xff0c;NTLM&#xff0c;Kerberos&#xff0c;WMI&#xff0c;LDAP等协议进行低级编程访问。…

什么是实数?

文章目录什么是实数有理数无理数实数的运算什么是实数 实数就是所有的有理数与无理数的集合&#xff0c;在数轴上&#xff0c;我们可以一一找到与实数相对应的点&#xff0c;更加具体化来说&#xff0c;实数也就是有限小数(整数也可以被看成是小数部位为零的小数)和无限小数的…

朝着理想坚实迈进_坚实原则:接口隔离原则

朝着理想坚实迈进以前&#xff0c;我们研究了liskov替代原理。 下一个原则是接口隔离 。 接口隔离原则&#xff08;ISP&#xff09;指出&#xff0c;不应强迫任何客户端依赖其不使用的方法。 想象一下&#xff0c;在我们的代码库中有很多方法的接口&#xff0c;尽管只有一部分…

mysql动态标签可以嵌套吗_Mysql动态嵌套游标

欢迎进入Linux社区论坛&#xff0c;与200万技术人员互动交流 >>进入 前提&#xff1a; 表一、 ddm_demand system_id demand_id quotate_end_team 1 1 team1,team5,team2 1 2 tea3,team1,team,4 pk:quotate_end_team、demand_id; 表二、 mc_team team_nam view_order tea…