java Compiler API (java编译api)-程序员宅基地

技术标签: java编译API  Java Basic  

在早期的版本中(Java SE5及以前版本)中只能通过tools.jar中的com.sun.tools.javac包来调用Java编译器,但由于tools.jar不是标准的Java库,在使用时必须要设置这个jar的路径。而在Java SE6中为我们提供了标准的包来操作Java编译器,这就是javax.tools包。

编译java文件

使用Java API来编译Java源代码有非常多方法,目前让我们来看一种最简单的方法,通过JavaCompiler进行编译。

使用ToolProvider.getSystemJavaCompiler来得到一个JavaCompiler接口的实例。

JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();

JavaCompiler中最核心的方法是run()。通过这个方法能编译java源代码。

int run(InputStream in, OutputStream out, OutputStream err, String... arguments)

参数分别用来为:

  1. java编译器提供参数
  2. 得到Java编译器的输出信息
  3. 接收编译器的错误信息,
  4. 一个或多个Java源程式文件

如果run编译成功,返回  0。

如果前3个参数传入的是null,那么run方法将以标准的输入、输出代替,即System.inSystem.outSystem.err。如果我们要编译一个test.java文件,并将使用标准输入输出,run的使用方法如下:

int results = tool.run(null, null, null, "F:\\demo\\Test.java");

完整的例子:

//CompileMain.java
import javax.tools.JavaCompiler;
import javax.tools.ToolProvider;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

public class CompileMain {

    public static void main(String[] args) throws IOException {
        JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
        int result = compiler.run(null, null, null, "F:\\demo\\Test.java");
        System.out.println(result == 0 ? "编译成功" : "编译失败");

//执行java 命令 , 空参数, 所在文件夹
        Process process = Runtime.getRuntime().exec("java Test",null,new File("F:\\demo\\"));
               

        BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(process.getInputStream()));
        String str;
        while ((str = bufferedReader.readLine()) != null) {
            System.out.println(str);
        }
    }
}
public class Test {

    public static void main(String[] args) {
        System.out.println("this is a test.java file ,thank you very much");
    }

}
$ javac CompileMain.java 

$ java CompileMain             
编译成功
this is a test.java file ,thank you very much

编译非文件形式源代码

JDK 6 的编译器 API 的另外一个强大之处在于,它可以编译的源文件的形式并不局限于文本文件JavaCompiler 类依靠文件管理服务可以编译多种形式的源文件。比如直接由内存中的字符串构造的文件,或者是从数据库中取出的文件。这种服务是由 JavaFileManager 类提供的。

在Java SE6中最佳的方法是使用StandardJavaFileManager类。这个类能非常好地控制输入、输出,并且能通过DiagnosticListener得到诊断信息,而DiagnosticCollector类就是listener的实现。新的 JDK 定义了 javax.tools.FileObject 和 javax.tools.JavaFileObject 接口。任何类,只要实现了这个接口,就可以被 JavaFileManager 识别。

编译流程

使用StandardJavaFileManager步骤:

  1. 建立一个DiagnosticCollector实例
  2. 通过JavaCompiler.getStandardFileManager()方法得到一个StandardFileManager对象。
  3. 使用StandardFileManager获取需要编译的源代码。从文件或者字符流中获取源代码。
  4. JavaCompiler.getTask()生成编译任务抽象。
  5. 通过CompilationTask.call()方法编译源代码。
  6. 关闭StandardFileManager

在使用这种方法调用Java编译时最复杂的方法就是getTask,下面让我们讨论一下getTask方法。这个方法有如下所示的6个参数。

getTask(Writer out,
        JavaFileManager fileManager,
        DiagnosticListener<? super JavaFileObject> diagnosticListener,
        Iterable<String> options,
        Iterable<String> classes,
        Iterable<? extends JavaFileObject> compilationUnits)

这些参数大多数都可为null。他们的含义所下。

  • out: 用于输出错误的流,默认是System.err
  • fileManager:标准的文件管理。
  • diagnosticListener: 编译器的默认行为。
  • options: 编译器的选项
  • classes:参和编译的class。
  • compilationUnits: 待编译的Java文件,不能为null

CompilationTask 提供了 setProcessors(Iterable<? extends Processor>processors)方法,用户可以制定处理 annotation 的处理器。

在使用完getTask前,需要通过StandardJavaFileManager.getJavaFileObjectsFromFiles()StandardJavaFileManager.getJavaFileObjectsFromStrings方法得到待编译的compilationUnits对象。

也可以通过继承/实现SimpleJavaObject获取带编译的对象。

调用这两个方法的方式如下:

Iterable<? extends JavaFileObject> getJavaFileObjectsFromFiles(Iterable<? extends File> files)
Iterable<? extends JavaFileObject> getJavaFileObjectsFromStrings(Iterable<String> names)

String[] filenames = …;
Iterable<? extends JavaFileObject> compilationUnits =
fileManager.getJavaFileObjectsFromFiles(Arrays.asList(filenames));

JavaCompiler.CompilationTask task = compiler.getTask(null, fileManager,
diagnostics, options, null, compilationUnits);

最后需要关闭fileManager.close();

例如:

package win.hgfdodo.dynamic;

import javax.tools.JavaCompiler;
import javax.tools.JavaFileObject;
import javax.tools.StandardJavaFileManager;
import javax.tools.ToolProvider;
import java.util.Arrays;

public class JavaFileManagerMain {
    public static void main(String[] args) {
        String fullQuanlifiedFileName = "win.hgfdodo.dynamic.".replaceAll("\\.", java.io.File.separator) + "Calculator.java";
        JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
        StandardJavaFileManager fileManager =
                compiler.getStandardFileManager(null, null, null);

        Iterable<? extends JavaFileObject> files =
                fileManager.getJavaFileObjectsFromStrings(
                        Arrays.asList(fullQuanlifiedFileName));
        JavaCompiler.CompilationTask task = compiler.getTask(
                null, fileManager, null, null, null, files);

        Boolean result = task.call();
        if (result == true) {
            System.out.println("Succeeded");
        }
    }
}

package win.hgfdodo.dynamic;

public class Calculator {
    public int multiply(int multiplicand, int multiplier) {
        return multiplicand * multiplier;
    }
}

JavaFileObject获取java源程序

开发者希望生成 Calculator 的一个测试类,而不是手工编写。使用 compiler API,可以将内存中的一段字符串,编译成一个 CLASS 文件。

定制 JavaFileObject 对象:

package win.hgfdodo.dynamic;

import javax.tools.SimpleJavaFileObject;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;

public class StringObject extends SimpleJavaFileObject {
    private String content = null;

    protected StringObject(String className, String contents) throws URISyntaxException {
        super(new URI(className), Kind.SOURCE);
        this.content = contents;
    }

    @Override
    public CharSequence getCharContent(boolean ignoreEncodingErrors) throws IOException {
        return content;
    }
}

SimpleJavaFileObject 是 JavaFileObject 的子类,它提供了默认的实现。继承 SimpleJavaObject 之后,只需要实现 getCharContent 方法。

接下来,在内存中构造 Calculator 的测试类 CalculatorTest,并将代表该类的字符串放置到 StringObject 中,传递给 JavaCompiler.getTask 方法。

具体如下:

package win.hgfdodo.dynamic;

import javax.tools.JavaCompiler;
import javax.tools.JavaFileObject;
import javax.tools.StandardJavaFileManager;
import javax.tools.ToolProvider;
import java.net.URISyntaxException;
import java.util.Arrays;

public class StringClassCompilerMain {
    public static void main(String[] args) {
        JavaCompiler javaCompiler = ToolProvider.getSystemJavaCompiler();
        StandardJavaFileManager standardJavaFileManager = javaCompiler.getStandardFileManager(null, null, null);
        JavaFileObject testFile = generateTest();
        Iterable<? extends JavaFileObject> classes = Arrays.asList(testFile);
        JavaCompiler.CompilationTask task = javaCompiler.getTask(null, standardJavaFileManager, null, null, null, classes);
        if(task.call()){
            System.out.println("success");
        }else{
            System.out.println("failure!");
        }

    }

    private static JavaFileObject generateTest() {
        String contents = new String(
                "package win.hgfdodo.dynamic;" +
                        "class CalculatorTest {\n" +
                        "  public void testMultiply() {\n" +
                        "    Calculator c = new Calculator();\n" +
                        "    System.out.println(c.multiply(2, 4));\n" +
                        "  }\n" +
                        "  public static void main(String[] args) {\n" +
                        "    CalculatorTest ct = new CalculatorTest();\n" +
                        "    ct.testMultiply();\n" +
                        "  }\n" +
                        "}\n");
        StringObject so = null;
        try {
            so = new StringObject("win.hgfdodo.dynamic.CalculatorTest", contents);
        } catch (URISyntaxException e) {
            e.printStackTrace();
        }

        return so;

    }
}

采集编译器的诊断信息

收集编译过程中的诊断信息是JDK6新增的内容。诊断信息,通常指错误、警告或是编译过程中的详尽输出。

JDK 6 通过 Listener 机制,获取这些信息。如果要注册一个 DiagnosticListener,必须使用 CompilationTask 来进行编译,因为 Tool.run 方法没有办法注册 Listener

步骤:

  1. 构造一个 Listener
  2. 传递给 JavaFileManager 的构造函数;
  3. 编译完成后,获取Diagnostic列表;
  4. 输出诊断信息。

例子:

package win.hgfdodo.dynamic;

import javax.tools.*;
import java.net.URISyntaxException;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;

public class StringClassCompilerMain {
    public static void main(String[] args) {
        JavaCompiler javaCompiler = ToolProvider.getSystemJavaCompiler();
        DiagnosticCollector<JavaFileObject> collector = new DiagnosticCollector<>();
        StandardJavaFileManager standardJavaFileManager = javaCompiler.getStandardFileManager(null, null, null);
        JavaFileObject testFile = generateTest();
        Iterable<? extends JavaFileObject> classes = Arrays.asList(testFile);
        JavaCompiler.CompilationTask task = javaCompiler.getTask(null, standardJavaFileManager, collector, null, null, classes);
        if(task.call()){
            System.out.println("success");
        }else{
            System.out.println("failure!");
        }

        List<Diagnostic<? extends JavaFileObject>> diagnostics = collector.getDiagnostics();
        for (Diagnostic<? extends JavaFileObject> diagnostic: diagnostics){
            System.out.println("line:"+ diagnostic.getLineNumber());
            System.out.println("msg:"+ diagnostic.getMessage(Locale.ENGLISH));
            System.out.println("source:"+ diagnostic.getSource());

        }
    }

    private static JavaFileObject generateTest() {
        String contents = new String(
                "package win.hgfdodo.dynamic;" +
                        "class CalculatorTest {\n" +
                        "  public void testMultiply() {\n" +
                        "    Calculator c = new Calculator()\n" +
                        "    System.out.println(c.multiply(2, 4));\n" +
                        "  }\n" +
                        "  public static void main(String[] args) {\n" +
                        "    CalculatorTest ct = new CalculatorTest();\n" +
                        "    ct.testMultiply();\n" +
                        "  }\n" +
                        "}\n");
        StringObject so = null;
        try {
            so = new StringObject("win.hgfdodo.dynamic.CalculatorTest", contents);
        } catch (URISyntaxException e) {
            e.printStackTrace();
        }

        return so;

    }
}

generateTest方法在构造Calculator时,将行尾;去掉,造成java 源文件错误,在编译时,会输出:

line:3
msg:需要';'
source:win.hgfdodo.dynamic.StringObject[win.hgfdodo.dynamic.CalculatorTest]

运行时编译和运行java类

运行时编译运行图解

CharSequenceJavaFileObject -- 存储源代码

package win.hgfdodo.compiler;

import javax.tools.SimpleJavaFileObject;
import java.io.IOException;
import java.net.URI;

/**
 * 字符串java源代码。JavaFileObject表示
 */
public class CharSequenceJavaFileObject extends SimpleJavaFileObject {

    //表示java源代码
    private CharSequence content;

    protected CharSequenceJavaFileObject(String className, String content) {
        super(URI.create("string:///" + className.replaceAll("\\.", "/") + Kind.SOURCE.extension), Kind.SOURCE);
        this.content = content;
    }

    /**
     * 获取需要编译的源代码
     * @param ignoreEncodingErrors
     * @return
     * @throws IOException
     */
    @Override
    public CharSequence getCharContent(boolean ignoreEncodingErrors) throws IOException {
        return content;
    }
}

JavaClassObject 保存编译结果

package win.hgfdodo.compiler;

import javax.tools.SimpleJavaFileObject;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.URI;

/**
 * 存储编译后的字节码
 */
public class JavaClassObject extends SimpleJavaFileObject {

    /**
     * Compiler编译后的byte数据会存在这个ByteArrayOutputStream对象中,
     * 后面可以取出,加载到JVM中。
     */
    private ByteArrayOutputStream byteArrayOutputStream;

    public JavaClassObject(String className, Kind kind) {
        super(URI.create("string:///" + className.replaceAll("\\.", "/") + kind.extension), kind);
        this.byteArrayOutputStream = new ByteArrayOutputStream();
    }

    /**
     * 覆盖父类SimpleJavaFileObject的方法。
     * 该方法提供给编译器结果输出的OutputStream。
     * 
     * 编译器完成编译后,会将编译结果输出到该 OutputStream 中,我们随后需要使用它获取编译结果
     *
     * @return
     * @throws IOException
     */
    @Override
    public OutputStream openOutputStream() throws IOException {
        return this.byteArrayOutputStream;
    }

    /**
     * FileManager会使用该方法获取编译后的byte,然后将类加载到JVM
     */
    public byte[] getBytes() {
        return this.byteArrayOutputStream.toByteArray();
    }
}

JavaFileManager 处理编译结果

JavaFileManager提供了编译结果存储编译类的加载

package win.hgfdodo.compiler;

import javax.tools.FileObject;
import javax.tools.ForwardingJavaFileManager;
import javax.tools.JavaFileManager;
import javax.tools.JavaFileObject;
import java.io.IOException;
import java.security.SecureClassLoader;

/**
 * 输出字节码到JavaClassFile
 */
public class ClassFileManager extends ForwardingJavaFileManager {

    /**
     * 存储编译后的代码数据
     */
    private JavaClassObject classJavaFileObject;

    protected ClassFileManager(JavaFileManager fileManager) {
        super(fileManager);
    }

    /**
     * 编译后加载类
     * <p>
     * 返回一个匿名的SecureClassLoader:
     * 加载由JavaCompiler编译后,保存在ClassJavaFileObject中的byte数组。
     */
    @Override
    public ClassLoader getClassLoader(Location location) {
        return new SecureClassLoader() {
            @Override
            protected Class<?> findClass(String name) throws ClassNotFoundException {
                byte[] bytes = classJavaFileObject.getBytes();
                return super.defineClass(name, bytes, 0, bytes.length);
            }
        };
    }

    /**
     * 给编译器提供JavaClassObject,编译器会将编译结果写进去
     */
    @Override
    public JavaFileObject getJavaFileForOutput(Location location, String className, JavaFileObject.Kind kind, FileObject sibling) throws IOException {
        this.classJavaFileObject = new JavaClassObject(className, kind);
        return this.classJavaFileObject;
    }

}

DynamicCompiler -- 自定义编译器

DynamicCompiler实现将源代码编译并加载的功能。

package win.hgfdodo.compiler;

import javax.tools.*;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

/**
 * 运行时编译
 */
public class DynamicCompiler {
    private JavaFileManager fileManager;

    public DynamicCompiler() {
        this.fileManager = initManger();
    }

    private JavaFileManager initManger() {
        if (fileManager != null) {
            return fileManager;
        } else {
            JavaCompiler javaCompiler = ToolProvider.getSystemJavaCompiler();
            DiagnosticCollector diagnosticCollector = new DiagnosticCollector();
            fileManager = new ClassFileManager(javaCompiler.getStandardFileManager(diagnosticCollector, null, null));
            return fileManager;
        }
    }

    /**
     * 编译源码并加载,获取Class对象
     * @param fullName
     * @param sourceCode
     * @return
     * @throws ClassNotFoundException
     */
    public Class compileAndLoad(String fullName, String sourceCode) throws ClassNotFoundException {
        JavaCompiler javaCompiler = ToolProvider.getSystemJavaCompiler();
        List<JavaFileObject> javaFileObjectList = new ArrayList<JavaFileObject>();
        javaFileObjectList.add(new CharSequenceJavaFileObject(fullName, sourceCode));
        boolean result = javaCompiler.getTask(null, fileManager, null, null, null, javaFileObjectList).call();
        if (result) {
            return this.fileManager.getClassLoader(null).loadClass(fullName);
        } else {
            return Class.forName(fullName);
        }
    }

    /**
     * 关闭fileManager
     * @throws IOException
     */
    public void closeFileManager() throws IOException {
        this.fileManager.close();
    }

}

测试

package win.hgfdodo.compiler;

import java.io.IOException;
import java.lang.reflect.InvocationTargetException;

public class DynamicCompilerTest {
    public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, IOException {
        StringBuilder src = new StringBuilder();
        src.append("package win.hgfdodo.compiler;");
        src.append("public class DynaClass {\n");
        src.append("    public String toString() {\n");
        src.append("        return \"Hello, I am \" + ");
        src.append("this.getClass().getSimpleName();\n");
        src.append("    }\n");
        src.append("}\n");

        String fullName = "win.hgfdodo.compiler.DynaClass";

        DynamicCompiler compiler = new DynamicCompiler();
        Class clz = compiler.compileAndLoad(fullName, src.toString());

        System.out.println(clz.getConstructor().newInstance());
        compiler.close();
    }
}

编译加载win.hgfdodo.compiler.DynaClass后,创建新的对象,并调用toString()输出:

Hello, I am DynaClass

参考

  1. javafile compiler, classloader and run

 

本文根据博客https://my.oschina.net/hgfdoing/blog/3052263

修改而来,注意, 编译.java源码, 和执行.class 都需要制定文件夹,这里我使用了绝对路径

2019年5月23日09:59:23

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/u010398771/article/details/90474813

智能推荐

Java数据库连接API(JDBC)_java连接数据库用什么api好-程序员宅基地

文章浏览阅读323次。JDBC的PreparedStatement是预编译的Statement,防止SQL注入,由于是预编译的,查询一次之后放到数据库的缓存,下次执行时发现相同,所以执行效率高。_java连接数据库用什么api好

JavaScript增强AJAX基础-程序员宅基地

文章浏览阅读69次。<title>js类型</title> <meta http-equiv="content-type" content="text/html; charset=UTF-8"> </head> <body> <script type="text/javascript"> //num为number类型 v..._ajax增强

Linux驱动开发——串口设备驱动_linux串口驱动开发-程序员宅基地

文章浏览阅读5k次,点赞6次,收藏43次。串口驱动开发_linux串口驱动开发

安装确认书模板_房屋租赁合同模板及审查要点-程序员宅基地

文章浏览阅读292次。公司成立后,选择办公场所成为一项重要工作。一般情况下,公司的办公场所都不是股东的自有房屋,而是需要通过租赁来选择合适的场地用来办公。因此,房屋租赁合同将不可避免的出现在公司经营过程之中,关于审核租赁合同需要关注哪些风险点呢?笔者将在本文中对于一些重点条款进行梳理。(注:本文租赁合同模版来源于深圳市住房和建设局2019年11月制《深圳市房屋租赁合同书(非住宅)》)模板1.1甲方出租给乙方的..._安装完成确认书

JUC学习(五):ArrayList的线程安全问题分析与解决方案(vector、Collections、写时复制技术)_arraylist和vector的线程安全-程序员宅基地

文章浏览阅读5.2k次。目录一、异常演示二、解决方案 1、vector 2、Collections工具类 3、CopyOnWriteArrayList 写时复制技术三、写时复制技术 1、特性 2、原理一、异常演示循环创建线程,将数据放入集合的同时,从集合中读取数据。/** * list集合线程不安全问题 */public class ThreadDemo04 {......_arraylist和vector的线程安全

Typo: In word 'xxx' less... (Ctrl+F1) 去掉错误拼写检查提示_typo: in word 'bookindex' less... (ctrl+f1) inspec-程序员宅基地

文章浏览阅读7.4k次。解决步骤:File -> SettingsEditor -> Code Style -> inspections -> Spelling -Typo 把勾选状态去掉,点击 OK即可_typo: in word 'bookindex' less... (ctrl+f1) inspection info: spellchecker in

随便推点

泛微Ecology9.0流程Ecode实践:通过修改Store对象隐藏流程明细表列实例_泛微oa根据主表选择按钮隐藏明细表列-程序员宅基地

文章浏览阅读570次,点赞7次,收藏9次。Ecology9复写组件,E9隐藏明细表列,E9隐藏明细表栏,泛微隐藏明细表列_泛微oa根据主表选择按钮隐藏明细表列

趋势预测算法大PK!_趋势预测算法 csdn-程序员宅基地

文章浏览阅读1w次,点赞5次,收藏61次。https://blog.csdn.net/dQCFKyQDXYm3F8rB0/article/details/106368395趋势预测在很多应用场景中都会起到至关重要的作用,比如淘宝商家会考虑库存量应该保持在多少才能够满足客户需求,商场希望得知假期会迎来多大的客流量以安排系列活动,机场想要预测五一黄金周会有多大的客运量来做相应的应急部署等。在智能运维领域,趋势预测同样具有一定的理论意义和实际应用价值。趋势预测在运维场景中的应用背景在实时监控系统中会采集到大量的数据,有些数据具有周期性等时_趋势预测算法 csdn

【无人车路径跟踪】基于神经网络的数据驱动迭代学习控制(ILC)算法,用于具有未知模型和重复任务的非线性单输入单输出(SISO)离散时间系统的无人车的路径跟踪(Matlab代码实现)-程序员宅基地

文章浏览阅读713次,点赞23次,收藏21次。摘要本文提出了一种基于神经网络的(NN-based)数据驱动迭代学习控制(ILC)算法,用于具有未知模型和重复任务的非线性单输入单输出(SISO)离散时间系统的跟踪问题。控制目标是使系统的输出在每次迭代过程中跟踪参考轨迹。因此,在每次迭代过程的每个相对时间点上,使用广义回归神经网络(GRNN)作为估计器来解决系统的关键参数,并使用径向基函数神经网络(RBFNN)作为控制器来解决控制输入。

Flask核心机制_runtimeerror: working outside of application conte-程序员宅基地

文章浏览阅读6.3k次,点赞2次,收藏4次。python编程快速上手(持续更新中…)python实战网上书店项目(Flask技术点More))1.首先写一段测试代码我们通过db.create_all(app=app)的方式解决了working outside application context的错误,下面我们来深究,这个错误出现的具体原因是什么。from flask import Flask, current_appapp = Flask(name)断点调试这里显示current_app=[LocalProxy]a = cur_runtimeerror: working outside of application context.

java中间件 - redis其他问题_master最好不要做任何持久化工作-程序员宅基地

文章浏览阅读1.1k次。Redis常见性能问题和解决方案?Master最好不要做任何持久化工作,包括内存快照和AOF日志文件,特别是不要启用内存快照做持久化。如果数据比较关键,某个Slave开启AOF备份数据,策略为每秒同步一次。为了主从复制的速度和连接的稳定性,Slave和Master最好在同一个局域网内。尽量避免在压力较大的主库上增加从库Master调用BGREWRITEAOF重写AOF文件,AOF在重写的时候会占大量的CPU和内存资源,导致服务load过高,出现短暂服务暂停现象。为了Master的稳定性,主_master最好不要做任何持久化工作

美赛备赛资料大全_美赛资料-程序员宅基地

文章浏览阅读4.2w次,点赞246次,收藏2.1k次。目录1、美赛比赛网址及其介绍2、美赛摘要页说明3、美赛常用词语与语句4、美赛翻译注意事项5、美赛论文写作一些建议5.1 团队方面准备5.2 摘要表部分5.3 评委关注点6、组队要求7、软件与一些建模网址参考(1)写一篇建模文章大致需要如下技能:(2)数学建模算法总结(3) word小白教程数据资料:(4)1982—2018中国统计年鉴大全链接(5)美国人口普查数据大全链接(6)美国城市数据大全链接(7)全球统计数..._美赛资料

推荐文章

热门文章

相关标签