1.Spring Boot引起的源码“堆外内存泄漏”排查及经验总结
2.2.创建SpringBoot项目
3.Spring Boot demo系列(九):Jasypt
4.Spring5源码分析之@Configuration注解的详解。希望读者能够耐着性子看完
5.Spring Security5后基于Oauth2.1的源码Authorization Server 实现 (官方demo,稍作修改)
6.Spring中ApplicationListener和ApplicationContext的源码使用
Spring Boot引起的“堆外内存泄漏”排查及经验总结
为了更好地实现对项目的管理,我们将组内一个项目迁移到MDP框架(基于Spring Boot),源码随后我们就发现系统会频繁报出Swap区域使用量过高的源码异常。笔者被叫去帮忙查看原因,源码苹果云网站源码发现配置了4G堆内内存,源码但是源码实际使用的物理内存竟然高达7G,确实不正常。源码JVM参数配置是源码“-XX:MetaspaceSize=M -XX:MaxMetaspaceSize=M -XX:+AlwaysPreTouch -XX:ReservedCodeCacheSize=m -XX:InitialCodeCacheSize=m, -Xssk -Xmx4g -Xms4g,-XX:+UseG1GC -XX:G1HeapRegionSize=4M”,实际使用的源码物理内存如下图所示:
使用Java层面的工具定位内存区域(堆内内存、Code区域或者使用unsafe.allocateMemory和DirectByteBuffer申请的源码堆外内存)。
笔者在项目中添加-XX:NativeMemoryTracking=detailJVM参数重启项目,源码使用命令jcmd pid VM.native_memory detail查看到的源码内存分布如下:
发现命令显示的committed的内存小于物理内存,因为jcmd命令显示的源码内存包含堆内内存、Code区域、通过unsafe.allocateMemory和DirectByteBuffer申请的内存,但是不包含其他Native Code(C代码)申请的堆外内存。所以猜测是使用Native Code申请内存所导致的问题。
为了防止误判,笔者使用了pmap查看内存分布,发现大量的M的地址;而这些地址空间不在jcmd命令所给出的地址空间里面,基本上就断定就是这些M的内存所导致。
使用系统层面的工具定位堆外内存。
因为已经基本上确定是Native Code所引起,而Java层面的工具不便于排查此类问题,只能使用系统层面的工具去定位问题。
首先,使用了gperftools去定位问题。
从上图可以看出:使用malloc申请的的内存最高到3G之后就释放了,之后始终维持在M-M。笔者第一反应是:难道Native Code中没有使用malloc申请,直接使用mmap/brk申请的?(gperftools原理就使用动态链接的方式替换了操作系统默认的内存分配器(glibc)。)
然后,使用strace去追踪系统调用。
因为使用gperftools没有追踪到这些内存,于是直接使用命令“strace -f -e"brk,mmap,munmap" -p pid”追踪向OS申请内存请求,但是并没有发现有可疑内存申请。
接着,使用GDB去dump可疑内存。javascript 源码解析
因为使用strace没有追踪到可疑内存申请;于是想着看看内存中的情况。就是直接使用命令gdp -pid pid进入GDB之后,然后使用命令dump memory mem.bin startAddress endAddressdump内存,其中startAddress和endAddress可以从/proc/pid/smaps中查找。然后使用strings mem.bin查看dump的内容,如下:
从内容上来看,像是解压后的JAR包信息。读取JAR包信息应该是在项目启动的时候,那么在项目启动之后使用strace作用就不是很大了。所以应该在项目启动的时候使用strace,而不是启动完成之后。
再次,项目启动时使用strace去追踪系统调用。
项目启动使用strace追踪系统调用,发现确实申请了很多M的内存空间,截图如下:
使用该mmap申请的地址空间在pmap对应如下:
最后,使用jstack去查看对应的线程。
因为strace命令中已经显示申请内存的线程ID。直接使用命令jstack pid去查看线程栈,找到对应的线程栈(注意进制和进制转换)如下:
这里基本上就可以看出问题来了:MCC(美团统一配置中心)使用了Reflections进行扫包,底层使用了Spring Boot去加载JAR。因为解压JAR使用Inflater类,需要用到堆外内存,然后使用Btrace去追踪这个类,栈如下:
然后查看使用MCC的地方,发现没有配置扫包路径,默认是扫描所有的包。于是修改代码,配置扫包路径,发布上线后内存问题解决。
为什么堆外内存没有释放掉呢?
虽然问题已经解决了,但是有几个疑问。带着疑问,直接看了一下 Spring Boot Loader那一块的源码。发现Spring Boot对Java JDK的InflaterInputStream进行了包装并且使用了Inflater,而Inflater本身用于解压JAR包的需要用到堆外内存。而包装之后的类ZipInflaterInputStream没有释放Inflater持有的堆外内存。于是以为找到了原因,立马向Spring Boot社区反馈了这个bug。虾客源码但是反馈之后,就发现Inflater这个对象本身实现了finalize方法,在这个方法中有调用释放堆外内存的逻辑。也就是说Spring Boot依赖于GC释放堆外内存。
使用jmap查看堆内对象时,发现已经基本上没有Inflater这个对象了。于是就怀疑GC的时候,没有调用finalize。带着这样的怀疑,把Inflater进行包装在Spring Boot Loader里面替换成自己包装的Inflater,在finalize进行打点监控,结果finalize方法确实被调用了。于是又去看了Inflater对应的C代码,发现初始化的使用了malloc申请内存,end的时候也调用了free去释放内存。
此时,怀疑free的时候没有真正释放内存,便把Spring Boot包装的InflaterInputStream替换成Java JDK自带的,发现替换之后,内存问题也得以解决了。
再次看gperftools的内存分布情况,发现使用Spring Boot时,内存使用一直在增加,突然某个点内存使用下降了好多(使用量直接由3G降为M左右)。这个点应该就是GC引起的,内存应该释放了,但是在操作系统层面并没有看到内存变化,那是不是没有释放到操作系统,被内存分配器持有了呢?
继续探究,发现系统默认的内存分配器(glibc 2.版本)和使用gperftools内存地址分布差别很明显,2.5G地址使用smaps发现它是属于Native Stack。内存地址分布如下:
到此,基本上可以确定是内存分配器在捣鬼;搜索了一下glibc M,发现glibc从2.开始对每个线程引入内存池(位机器大小就是M内存),原文如下:
按照文中所说去修改MALLOC_ARENA_MAX环境变量,发现没什么效果。查看tcmalloc(gperftools使用的内存分配器)也使用了内存池方式。
为了验证是内存池搞的鬼,就简单写个不带内存池的橙牛源码内存分配器。使用命令gcc zjbmalloc.c -fPIC -shared -o zjbmalloc.so生成动态库,然后使用export LD_PRELOAD=zjbmalloc.so替换掉glibc的内存分配器。其中代码Demo如下:
通过在自定义分配器当中埋点可以发现实际申请的堆外内存始终在M-M之间,gperftools监控显示内存使用量也是在M-M左右。但是从操作系统角度来看进程占用的内存差别很大(这里只是监控堆外内存)。
使用不同分配器进行不同程度的扫包,占用的内存如下:
为什么自定义的malloc申请M,最终占用的物理内存在1.7G呢?因为自定义内存分配器采用的是mmap分配内存,mmap分配内存按需向上取整到整数个页,所以存在着巨大的空间浪费。通过监控发现最终申请的页面数目在k个左右,那实际上向系统申请的内存等于k * 4k(pagesize) = 2G。
为什么这个数据大于1.7G呢?因为操作系统采取的是延迟分配的方式,通过mmap向系统申请内存的时候,系统仅仅返回内存地址并没有分配真实的物理内存。只有在真正使用的时候,系统产生一个缺页中断,然后再分配实际的物理Page。
整个内存分配的流程如上图所示。MCC扫包的默认配置是扫描所有的JAR包。在扫描包的时候,Spring Boot不会主动去释放堆外内存,导致在扫描阶段,堆外内存占用量一直持续飙升。当发生GC的时候,Spring Boot依赖于finalize机制去释放了堆外内存;但是glibc为了性能考虑,并没有真正把内存归返到操作系统,而是留下来放入内存池了,导致应用层以为发生了“内存泄漏”。所以修改MCC的配置路径为特定的JAR包,问题解决。在发表这篇文章时,发现Spring Boot的最新版本(2.0.5.RELEASE)已经做了修改,在ZipInflaterInputStream主动释放了堆外内存不再依赖GC;所以Spring Boot升级到最新版本,这个问题也可以得到解决。
2.创建SpringBoot项目
在完成上一章节的环境配置和lombok插件安装后,我们需要创建一个SpringBoot应用所需的父项目和三个子项目。
以下是创建过程的详细步骤。
1. 创建父项目:
打开IDEA,web商城源码点击“New Project”快速创建一个Maven项目。
创建完成后,删除src源码目录,只保留pom.xml文件。
2. 创建demo-api:
demo-api层负责提供接口和参数,只需要提供jar包即可。使用Maven创建该项目。
在项目上右键,选择“New” > “Module”,快速创建Maven项目,确保选择父项目。
3. 创建demo-core:
demo-core主要与数据库进行交互,不启动服务,只提供jar包。同样选择Maven项目。
在项目上右键,选择“New” > “Module”,快速创建Maven项目,注意选择父项目。
4. 创建demo-service:
5. 结果:
最终生成的文件目录结构如下:
6. pom文件结构如下:
父pom.xml
api/pom.xml
core/pom.xml
service/pom.xml
Spring Boot demo系列(九):Jasypt
Jasypt是一个加密库,提供了Spring Boot集成的库jasypt-spring-boot。本文演示如何使用该库对配置文件进行加密。 首先添加依赖,Gradle方式如下: 依赖添加后,进行简单加密。加密口令直接写在配置文件中,步骤包括: 在配置文件中添加加密口令参数。 在测试类中注入StringEncryptor,使用encrypt方法加密通过@Value获取的值。 运行测试,输出密文,完成属性加密。 可以自定义加密类,实现StringEncrypto接口,配置Bean名称,简化加密过程。 支持非对称加密,生成公钥与私钥,配置到jasypt.encryptor.public-key-string与jasypt.encryptor.private-key-string,进行加密与解密。 对于jasypt.encryptor.password,支持非明文口令传递,方式包括命令行参数、应用环境变量及系统环境变量。 打包时需注意配置Maven参数,Gradle无需额外添加。部署时,通过JAR直接部署,根据需要加上参数,Docker部署时,需在ENTRYPOINT或ENV指定参数。 参考源码提供Java及Kotlin版实现。Spring5源码分析之@Configuration注解的详解。希望读者能够耐着性子看完
Spring5源码中@Configuration注解详解,让你理解无需XML的Bean创建。在Spring 3.0以后,@Configuration注解的出现,允许开发者在运行时动态创建和初始化Bean,无需依赖XML配置。它实际上标记了@Component元注解,被@ComponentScan扫描并纳入Spring容器管理。
使用@Configuration时,Bean的默认名称与方法名称相同,可通过name属性指定。它不仅自身可以作为受管理的组件,还能通过@Autowired和@Inject注解注入其他Bean。例如,修改Demo,配置类可以作为服务组件被自动扫描。
@Configuration不仅支持@ComponentScan,还能与@Controller、@Service、@Repository等注解配合,这些注解本质上都有@Component,适合不同场景的管理。此外,@Configuration可以同@Import和@Profile注解组合,实现更灵活的配置导入和环境条件控制。
在配置类内部嵌套@Configuration,可以利用静态内部类简化@Import的使用。配置类的初始化可以通过@Lazy注解延迟,提供更细致的控制。配置类解析涉及@ConfigurationClassPostProcessor处理器,处理@Configuration类的@Bean、@ComponentScan和环境相关注解。
最后,@Configuration类的Bean定义信息由ConfigurationClassBeanDefinitionReader处理并注册到Spring容器,整个过程包括解析@Configuration类、扫描相关注解和Bean定义的加载。
理解@Configuration的解析流程,能帮助你更高效地利用Spring的动态配置能力。如果你对文章内容有所收获,别忘了分享和关注我们的更多内容。
Spring Security5后基于Oauth2.1的Authorization Server 实现 (官方demo,稍作修改)
QQ群 说明 Spring Security5之后,实现Oauth不再推荐使用spring-security-oauth2。官方推荐了一种新的方法(github.com/spring-proje...)。 需要注意的是,该方式支持的不是oauth2,而是oauth2.1。关于oauth2.1的详细介绍,可以参考:oauth.net/2.1/ 和 aaronparecki.com//1...。 摘自 oauth2.net 网站上对比 oauth2 的说明: 主要区别(以下使用翻译软件翻译,大致意思没错) 官方demo运行 根据 github.com/spring-proje... 上的说明,使用 gradle 命令运行即可。 复制官方代码(官网 userdetails 使用内存模式,我修改成了 jdbc,更加直观),使用 postman 进行测试。 代码位置:authorization-server 测试 查看自动创建的表结构: 浏览器访问:localhost:/h2-console... 点击 Connect: 我们看到创建了五张表,打开表会发现数据也被初始化进去了。 启动项目,请求授权码模式地址: 输入用户名密码(user1, password): 勾选授权 scope,点击 submit consent 按钮: 地址栏:baidu.com/?... 授权码 code=dPEZCnsiz2WPk5mWdnPImxbSQkbwK7-yPKmgJuR2NHbswtbXWZFjgZr6MEXfIqi8JhRourmlSSYVVfGuCN-ep8jbQwxHsqrUVeeY-1XRHkpqaQ2UM9-ulbTsU0mg 打开 postman 通过该 code 获取 access_token: 点击 send,返回: JSON 数据如下: 刷新 token 请求后结果: JSON 数据是: 简化模式(response_type=token)在 oauth2.1 被移除。 返回结果如图: JSON 结果: 之前以为是 bug,就提了 issue(github.com/spring-proje...),官方回复不支持。 但是有扩展方式。还没弄。具体可看我提出的 issue 的作者回复。 官方使用的是 in-memory 方式。本人通过查看源码发现还支持 jdbc 方式,redis 方式不支持。之后会根据 demo 修改一版 jdbc 方式,以及动态查询用户名密码的方式。 spring-security-oauth2 的实现方式,可查看我之前的笔记 知乎:zhuanlan.zhihu.com/p/... github 源码:github.com/xpp/spr... 本文内容 github 地址是:github.com/xpp/spri... 梦想越是美丽,就越是显得遥不可及。可奇怪的是,一旦你下定了决心,很快地,那些梦想就一一成为了现实!Spring中ApplicationListener和ApplicationContext的使用
关于Spring的源码相关功能1引入ApplicationContextApplicationContext是Spring的一个核心接口,允许容器通过应用程序上下文环境创建,获取,管理bean.
publicinterfaceApplicationContextextendsEnvironmentCapable,ListableBeanFactory,HierarchicalBeanFactory,MessageSource,ApplicationEventPublisher,ResourcePatternResolver{ /***Returntheuniqueidofthisapplicationcontext.*@returntheuniqueidofthecontext,or{ @codenull}ifnone*/@NullableStringgetId();/***Returnanameforthedeployedapplicationthatthiscontextbelongsto.*@returnanameforthedeployedapplication,ortheemptyStringbydefault*/StringgetApplicationName();/***Returnafriendlynameforthiscontext.*@returnadisplaynameforthiscontext(never{ @codenull})*/StringgetDisplayName();/***Returnthetimestampwhenthiscontextwasfirstloaded.*@returnthetimestamp(ms)whenthiscontextwasfirstloaded*/longgetStartupDate();/***Returntheparentcontext,or{ @codenull}ifthereisnoparent*andthisistherootofthecontexthierarchy.*@returntheparentcontext,or{ @codenull}ifthereisnoparent*/@NullableApplicationContextgetParent();AutowireCapableBeanFactorygetAutowireCapableBeanFactory()throwsIllegalStateException;}ApplicationContext提供的功能:
访问应用程序组件的Bean工厂方法.从org.springframework.beans.factory.ListableBeanFactory继承而来.
publicinterfaceListableBeanFactoryextendsBeanFactory{ ......}通用方式加载文件资源的能力.从org.springframework.core.io.support.ResourcePatternResolver继承而来.
packageorg.springframework.core.io.support;importjava.io.IOException;importorg.springframework.core.io.Resource;importorg.springframework.core.io.ResourceLoader;publicinterfaceResourcePatternResolverextendsResourceLoader{ StringCLASSPATH_ALL_URL_PREFIX="classpath*:";Resource[]getResources(Stringvar1)throwsIOException;}向注册监听器发布事件的能力.从org.springframework.context.ApplicationEventPublisher继承而来.
@FunctionalInterfacepublicinterfaceApplicationEventPublisher{ defaultvoidpublishEvent(ApplicationEventevent){ publishEvent((Object)event);}voidpublishEvent(Objectevent);}解析消息的能力,支持国际化.从org.springframework.context.MessageSource继承而来.
publicinterfaceMessageSource{ @NullableStringgetMessage(Stringcode,@NullableObject[]args,@NullableStringdefaultMessage,Localelocale);StringgetMessage(Stringcode,@NullableObject[]args,Localelocale)throwsNoSuchMessageException;StringgetMessage(MessageSourceResolvableresolvable,Localelocale)throwsNoSuchMessageException;}从父上下文继承,后代上下文中的定义总是优先级.单个父上下文可以被整个web应用程序使用,而每个servlet都有自己独立于任何其他servlet的子上下文.
2关于ApplicationListener的说明2.1ApplicationListener简介ApplicationContext事件机制是属于设计模式中的观察者设计模式,通过ApplicationEvent类和ApplicationListener接口实现事件处理.
当容器中有一个ApplicationListener对象,当ApplicationContext发布ApplicationEvent事件时,ApplicationListener对象会被自动触发,需要由程序来控制.此外Spring中也内置了一下事件.
内置事件说明ContextRefreshedEventApplicationContext被初始化或刷新时,该事件被发布。这也可以在ConfigurableApplicationContext接口中使用refresh()方法来发生。此处的初始化是指:所有的Bean被成功装载,后处理Bean被检测并激活,所有SingletonBean被预实例化,ApplicationContext容器已就绪可用ContextStartedEventConfigurableApplicationContext(ApplicationContext子接口)接口中的start()方法启动ApplicationContext时,该事件被发布。你可以调查你的数据库,或者你可以在接受到这个事件后重启任何停止的应用程序ContextStoppedEventConfigurableApplicationContext接口中的stop()停止ApplicationContext时,发布这个事件。你可以在接受到这个事件后做必要的清理的工作ContextClosedEventConfigurableApplicationContext接口中的close()方法关闭ApplicationContext时,该事件被发布。一个已关闭的上下文到达生命周期末端;它不能被刷新或重启RequestHandledEvent是web-specific事件,告诉所有beanHTTP请求已经被服务处理。只能应用于使用DispatcherServlet的Web应用。在使用Spring作为前端的MVC控制器时,当Spring处理用户请求结束后,系统会自动触发该事件2.2ApplicationListener案列1准备一个SpringBoot环境2创建一个自定义的监听器@ComponentpublicclassDemoApplicationListenerimplementsApplicationListener<ContextRefreshedEvent>{ @OverridepublicvoidonApplicationEvent(ContextRefreshedEventevent){ System.out.println(event);System.out.println("TestApplicationListener............................");}}根据上述可知,ContextRefreshedEvent内置事件,是ApplicationContext被初始化或刷新时会发布,即监听器可以收到回调信息.
3启动项目,查看日志3关于ApplicationContext的说明3.1ApplicationContext的简介从上述可知ApplicationContext具有发布事件的能力,是从ApplicationEventPublisher接口继承来的.而Spring中的事件使用,需要继承ApplicationEvent类或ApplicationContextEvent抽象类,抽象类中只有一个构造函数,且带有一个Object类型的参数作为事件源,且该事件源不能为null,因此我们需要在自己的构造函数中执行super(Object)。
publicclassEventObjectimplementsjava.io.Serializable{ privatestaticfinallongserialVersionUID=L;/***TheobjectonwhichtheEventinitiallyoccurred.*/protectedtransientObjectsource;/***ConstructsaprototypicalEvent.**@paramsourceTheobjectonwhichtheEventinitiallyoccurred.*@exceptionIllegalArgumentExceptionifsourceisnull.*/publicEventObject(Objectsource){ if(source==null)thrownewIllegalArgumentException("nullsource");this.source=source;}....}3.2ApplicationContext的案列3.2.1准备一个SpringBoot环境@SpringBootApplicationpublicclassApplication{ publicstaticvoidmain(String[]args){ SpringApplication.run(Application.class,args);testEvent();}//@Bean//publicFeignInterceptorfeignInterceptor(){ //returnnewFeignInterceptor();//}//测试事件publicstaticvoidtestEvent(){ ApplicationContextcontext=newAnnotationConfigApplicationContext(EventConfig.class);DemoEventdemoEvent=newDemoEvent(context,"小明",);context.publishEvent(demoEvent);}}3.2.2创建一个自定义的监听器@ComponentpublicclassDemo2ApplicationListenerimplementsApplicationListener<ApplicationEvent>{ @OverridepublicvoidonApplicationEvent(ApplicationEventevent){ //针对自定义事件做处理if(eventinstanceofDemoEvent){ System.out.println(event);DemoEventdemoEvent=(DemoEvent)event;System.out.println("姓名:"+demoEvent.getUsername()+",年龄:"+demoEvent.getAge());System.out.println("自定义DemoEvent事件............................");}}}3.2.3创建一个自定义的事件publicclassDemoEventextendsApplicationEvent{ privateStringusername;privateintage;/***CreateanewApplicationEvent.**@paramsourcetheobjectonwhichtheeventinitiallyoccurred(never{ @codenull})*/publicDemoEvent(Objectsource,Stringusername,intage){ super(source);this.username=username;this.age=age;}publicStringgetUsername(){ returnusername;}publicvoidsetUsername(Stringusername){ this.username=username;}publicintgetAge(){ returnage;}publicvoidsetAge(intage){ this.age=age;}}3.2.4启动项目,查看日志SpringCloud入门实战-Sleuth+Zipkin分布式请求链路跟踪详解
探索SpringCloud实战:Sleuth+Zipkin实现分布式请求链路跟踪详解 在SpringCloud入门实战系列中,我们将深入理解SpringCloud Sleuth如何协助解决微服务中的挑战。通过源码地址的项目demo,一步步掌握这一关键组件。Sleuth是Spring Cloud的分布式跟踪解决方案,它跟踪用户请求从数据采集到处理的全过程,构建调用链视图,对微服务监控至关重要。 Sleuth借鉴了Dapper的术语,核心概念包括:Span(跨度):一次请求的标识,每个微服务调用产生一个,由位ID唯一标识,包含摘要、时间戳等信息。
Trace(跟踪):调用链路集合,由一个请求产生的所有Span组成,每个跨度有各自的跟踪ID。
Annotation(标注):记录请求的开始和结束事件,如发送请求、接收请求等。
Sleuth与Zipkin紧密相关,通常一起使用进行可视化追踪。Sleuth特性包括将跟踪信息添加到日志、在应用程序边界自动插入跟踪、提供分布式跟踪数据模型抽象等。在项目集成时,可以搭建Zipkin服务,添加依赖,配置通过HTTP或消息传递方式发送跟踪数据,以及在业务代码中应用Sleuth。测试时,通过访问特定接口可以查看请求链路信息。 通过实践SpringCloud Sleuth,你可以更好地理解和应用它在微服务架构中的作用,提升监控和调试的效率。SpringBoot的CommandLineRunner和ApplicationRunner源码分析
深入探究SpringBoot中的ApplicationRunner和CommandLineRunner接口。这两个接口在启动SpringBoot应用时起到关键作用,下面将对它们进行源码分析。
首先,让我们聚焦于ApplicationRunner接口,其内部定义了一个名为run的方法,无需额外参数,源码如下所示,展示了接口的基本框架。
接着,审视CommandLineRunner接口,同样地,它也仅定义了一个run方法,同样没有额外参数,源码内容在此。接口设计简洁,旨在支持特定逻辑的执行。
为了更直观地理解这些接口的运行,让我们通过实际项目进行演示。具体操作是将SpringBoot项目打包为JAR文件并执行。
在项目执行过程中,观察并分析代码,可以揭示更多关于ApplicationRunner和CommandLineRunner接口如何在实际应用中运作的细节。
接下来,以ApplicationRunnerDemo和CommandLineRunnerDemo为例,深入探讨接口的使用。首先,审视ApplicationRunnerDemo类,了解如何定义实现ApplicationRunner接口的实例并注入应用上下文。然后,通过CommandLineRunnerDemo类,进一步探索实现CommandLineRunner接口的实例,关注参数传递的机制以及接口执行的时机。
至此,参数传递、参数解析以及获取参数的过程已经清晰呈现。此外,ApplicationRunner和CommandLineRunnerDemo的执行时机也已明确阐述,为理解SpringBoot启动过程中的关键逻辑提供了深入洞察。
江西景德镇:深化改革创新 激活服务监管“一池春水”
erp源码在线
关注线源码_关注代码
简单炒股源码_炒股指标的源码
江苏南京:召开老门东景区ODR企业宣贯培训会
社交im 源码_社交源码开源