• JAVA与.NET的相互调用——TCP/IP相互调用基本架构


    TCP/IP套接字的概念

    TCP/IP(传输控制协议/网际协议)是网络互连的通信协议,通过它可以实现各种异构网络或异种机之间的互联通信。TCP/IP是Transmission Control Protocol/Internet Protocol的简写,中文译名为传输控制协议/因特网互联协议,又叫网络通讯协议,这个协议是Internet最基本的协议、Internet国际互联网的基础,简单地说,就是由网络层的IP协议和传输层的TCP协议组成的。TCP/IP 定义了电子设备(比如计算机)如何连入因特网,以及数据如何在它们之间传输的标准。TCP/IP是一个四层的分层体系结构。高层为传输控制协议,它负责聚集信息或把文件拆分成更小的包。低层是网际协议,它处理每个包的地址部分,使这些包正确的到达目的地。 TCP/IP已成为当今计算机网络最成熟、应用最广的互联协议。Internet采用的就是 TCP/IP协议,网络上各种各样的计算机上只要安装了TCP/IP协议,它们之间就能相互通信。

      

    TCP/IP套接字通讯的开发

    在众多的开发语言中,绝大部分的开发语言都支持TCP/IP协议通讯,开发过程也十分相像,先设置好Socket,然后由客户端发送请求信息,服务器连接客户端接收到请求后再返还信息。而在.NET系统当中则稍有不同,系统把Socket对象包装在TcpClient对象内,对Socket对象的生命周期进行管理。在开发过程当中,服务器与客户端的开发语言有所不同的情况经常发生,服务器是在JDK1.6的环境下进行开发的,客户却要求使用.NET开发客户端,这往往会令开发人员感到困惑!下面在下使用JAVA为服务器,.NET为客户端为例子,为大家介绍一下如何使用TCP/IP协议进行JAVA  .NET之间的相互调用。像TCP/IP实现聊天室这样的例子很多,开发起来也比较简单,因为通讯双方都是使用String来传送信息。而在真正建立ERP、OA、CRM等系统的时候,通讯双方都必须先建立一套统一的通讯契约,才能实现TCP/IP通讯,下面将为大家介绍一个比较典型的企业信息通讯实例。

     

    信息传送方式

    因为.NET与JAVA各有不同的特性,双方不可能直接通过的序列化对象来传输信息,常用的信息交换方式有以下三种:

    1. 最 笨拙也是最复杂的一种传息方式,就是直接使用“头文件说明+字段属性”的方式。 这是一个既原始又麻烦的通讯方式,因为每个契约都要以二进制的方式发送一个请求,就算是同一类契约,随着参数的不同,每个请求的长度也会发生改变。这样的 传息方式虽然是麻烦,但在不同开发语言相互调用的时候却经常会看到,这可能是因为开发人员对两种开发语言未能完全熟悉,所以倒置使用这最原始最简单的开发 方式。


     

    2. 使用XML的信息传送方式,这是最常见,使用最广的信息传递方式。在绝大多数的开发平台都会支持XML,所以XML在Web网络传讯过程中最为常见。但XML最大的一个缺点就是过于堪舆,耗费大量的传输流量。

     

    3. 对 于XML的缺点,JSON应运而生而且发展迅速,JSON本是源于Javascript的,多数只用于B/S的页面开发,但随着技术的发展和多个开发语言 的支持,现今到处都可以看JSON的身影。因为JSON既提供一套跨平台的通讯方式,也免去XML复杂特性,受到各类型开发人员的欢迎。

     

    服务器端开发

    • 通讯契约

    首先建立一套服务器与客户端同时接受通讯契约, Contract 的name特性是契约的名称,服务器会通过此名称在Contracts.xml文件中找到该契约,然后根据output的package属性,class属性,method属性找到该契约的包名称,类名,调用的方法等属性。

     

    Contracts.XML
    <Contracts>

    <Contract name="GetPersonByAge"> //name为契约名,服务器与客户端必须同时遵守此契约
    <Input>
    <Description>获取Age等于此值的People对象集</Description> //说明此契约内容
    </Input>
    <Output>
    <Package>Manager</Package> //接收到GetPersonByAge请求时所调用的包名称
    <Class>PersonManager</Class> //接收到GetPersonByAge请求时所调用的类名称
    <Method>GetListByAge</Method> //接收到GetPersonByAge请求时所调用的处理方法名称
    </Output>
    </Contract>

    <Contract name="GetPersonByID">
    <Input>
    <Description>获取ID等于此值的People对象</Description>
    </Input>
    <Output>
      <Package >Manager</Package>
    <Class>PersonManager</Class>
    <Method>GetListByID</Method>
    </Output>
    </Contract>

    </Contracts>
    • 以JSON方式实现信息传送

    尽管目前在C/S的开发当中大部分还是使用序列化对象和分节字段的方式进行双方通讯,但在这个实例当中,在下想以JSON通讯方式为例子来实现。首先,客户端会使用额定格式的JSON向服务器发送请求: 

    {“ContractName”:“GetPeopleByAge”,“Params”:[23]}

    ContractName代表着契约名称,系统会根据此名称在Contracts.xml文件中找到Name等于GetPeopleByAge的Contract项。然后在对应Output的子项Package,Class,Method中查找到对应的包,类型和方法。

    Params是客户端传输过来的参数,服务器端会调用对象的方法输入参数23后,得到计算结果,最后把结果返还到客户端。

    在 这里有两点是值得注意的,第一点是JSON中的契约格式是固定的,服务器与客户端都必须遵守此契约,在ContractName中输入是必须对应的契约名 称,而在Params中输入的必输是一个参数的集合,哪怕里面只包含有一个参数。第二点是在Contracts.xml文件,Output里面的 Package,Class,Method是服务器端自定义的,它只是绑定了服务器端实现GetPersonByAge契约的方法,而这些方法并不是固 定,服务器可以根据系统的需要而修改。这个做法有点像Struts里面的Struts.xml文件,其意义就是在于使服务器的处理方法与客户端发送的请求实现分离。

    • 基本结构

    系统的基本结构如图,客户端会以JSON方式{“ContractName”:“GetPeopleByAge”,“Params”:[23]}发送请求到服务器,服务器会利用“数据转换层”把接收到的请求转换成Contract对象。然后逻辑转换层会根据该Contract对象调用对应的方法,最后把计算结果以JSON方式返回到客户端。

    注意在服务器与客户端信息交换的过程中,都是使用JSON格式。

    • JSON数据转换

    在服务器端,当接到到客户端请求后,Transfer类负责把接收到的JSON数据转换成Contract对象。在Transfer里面使用org.json工具包作为JSON的转化工具,org.json工具包可于以下网址下载http://www.json.org/java/index.html

    而Implement类包含GetResult(Contract contract )方法,其作就是根据contract对象Package,Class,Method等属性,调用“逻辑转换层”的对应方法,最后把计算结果返还给InputControl。

    服务器端接收请求后就会直接调用InputControl对输入的数据进行处理。

    代码
    //Contract实体类,包含契约的package,class,method,params等多个属性
    package Model;

    import org.json.JSONArray;

    publicclass Contract {
    private String package1;
    private String class1;
    private String method;
    private JSONArray params;
    publicvoid setPackage1(String package1) {
    this.package1 = package1;
    }
    public String getPackage1() {
    return package1;
    }
    publicvoid setClass1(String class1) {
    this.class1 = class1;
    }
    public String getClass1() {
    return class1;
    }
    publicvoid setMethod(String method) {
    this.method = method;
    }
    public String getMethod() {
    return method;
    }
    publicvoid setParams(JSONArray params) {
    this.params = params;
    }
    public JSONArray getParams() {
    return params;
    }

    }

    //把输入的String字符串转化为Contract对象
    //在这里使用org.json工具包作为JSON的转化工具,org.json工具包可于以下网址下载http://www.json.org/java/index.html
    package Common;
    import java.io.File;
    import java.io.IOException;
    import javax.xml.parsers.DocumentBuilder;
    import javax.xml.parsers.DocumentBuilderFactory;
    import javax.xml.parsers.ParserConfigurationException;
    import Model.Contract;
    import org.json.*;
    import org.w3c.dom.Document;
    import org.w3c.dom.Element;
    import org.w3c.dom.NodeList;
    import org.xml.sax.SAXException;

    publicclass Transfer {
    private Transfer(){}

    privatestatic String contractName=null;
    privatestatic Contract contract=new Contract();
    privatestatic JSONObject jsonObject=null;

    publicstatic Contract GetContract(String data) throws Exception, JSONException, ParserConfigurationException, SAXException, IOException{
    jsonObject
    =new JSONObject(data); //把字符串转化为JSONOject对象
    GetContractName(); //获取契约名称
    GetProperty(); //获取契约的package,class,method属性
    GetParams(); //获取契约的参数集
    return contract;
    }

    /*
    * 获取契约对应的包名称,类名称,方法名称
    */
    privatestaticvoid GetProperty() throws Exception{
    File file
    =new File("Contracts.xml");
    DocumentBuilderFactory factory
    = DocumentBuilderFactory.newInstance();
    DocumentBuilder builder
    = factory.newDocumentBuilder();
    Document doc
    = builder.parse(file);
    NodeList nodeList
    = doc.getElementsByTagName("Contract");
    Element contractElement
    =null;
    for (int i =0; i < nodeList.getLength(); i++) {
    if(nodeList.item(i).getAttributes().item(0).getNodeValue().equals(contractName)){
    contractElement
    =(Element)nodeList.item(i);
    break;
    }
    }
    if(contractElement!=null){
    Element outputElement
    =(Element)contractElement.getElementsByTagName("Output").item(0);
    contract.setPackage1(outputElement.getElementsByTagName(
    "Package").item(0).getTextContent());
    //获取包名称
    contract.setClass1(outputElement.getElementsByTagName("Class").item(0).getTextContent());
    //获取类名称
    contract.setMethod(outputElement.getElementsByTagName("Method").item(0).getTextContent());
    //获取方法名
    }
    else
    thrownew Exception("未能找到对象的契约");
    }

    /*
    * 获取契约名称
    */
    privatestaticvoid GetContractName() throws JSONException{
    contractName
    =jsonObject.getString("ContractName");
    }

    /*
    * 获取输入参数
    */
    privatestaticvoid GetParams() throws JSONException{
    contract.setParams(jsonObject.getJSONArray(
    "Params"));
    }
    }

    //调用Contract对象里面包中的类的某个方法,获取计算结果
    package Common;
    import java.lang.reflect.Method;
    import java.util.HashMap;
    import java.util.Map;
    import org.json.JSONArray;
    import Model.*;

    publicclass Implement {
    private Contract contract;
    private String fullName;
    privatestatic Map<String,Object> objects=new HashMap<String,Object>(); //保存对象实体
    privatestatic Map<String,Class> classes=new HashMap<String,Class>(); //保存类名

    /*
    * 先获取对应的对象,再用反射模式调用对象的方法,获取计算结果
    */
    public Object GetResult(Contract contract){
    this.contract=contract;
    this.fullName=contract.getPackage1()+"."+contract.getClass1();

    try {
    Object manager
    =GetObject();
    Class theClass
    =classes.get(fullName);
    Method method
    =theClass.getDeclaredMethod(contract.getMethod(),JSONArray.class);
    return method.invoke(manager, contract.getParams());
    }
    catch (Exception e) {
    // TODO Auto-generated catch block
    e.printStackTrace();
    returnnull;
    }
    }

    /*
    * 多次使用反射创建获取对象会损耗一定性能,所以此处使用单体模式获取对应的对象
    */
    private Object GetObject() throws InstantiationException, IllegalAccessException, ClassNotFoundException{
    if(!objects.containsKey(fullName)){
    Class theClass
    = Class.forName(fullName);
    classes.put(fullName,theClass);
    Object manager
    =theClass.newInstance();
    objects.put(fullName, manager);
    }
    return objects.get(fullName);
    }
    }

    //直接把接收到的二进制数据转换成String,然后通过Transfer把String转化为Contract对象,最后通过Implement获取运算结果
    package Common;

    import java.io.DataInputStream;
    import Model.Contract;

    publicclass InputControl {
    private DataInputStream inputStream;

    public InputControl(DataInputStream inputStream){
    this.inputStream=inputStream;
    }
    /*
    * 直接把接收到的二进制数据转换成String,然后通过Transfer把String转化为Contract对象,最后通过Implement对象获取运算结果
    */
    public Object GetResult(){
    byte[] byteMessage=newbyte[1024]; //在此处只获取测试数据,在真正运行时应使用分批缓存的方式
    try{
    int n=inputStream.read(byteMessage);
    String message
    =new String(byteMessage,"ASCII");
    Contract contract
    =Transfer.GetContract(message);
    Implement implement
    =new Implement();
    Object result
    =implement.GetResult(contract);
    return result;
    }
    catch(Exception ex){
    ex.printStackTrace();
    returnnull;
    }
    }
    }

    最后,系统通过OutputControl类把计算结果返还给客户端。

    代码
    //把计算结果返回到客户端
    package Common;

    import java.io.DataOutputStream;

    publicclass OutputControl {
    private DataOutputStream outputStream;

    public OutputControl(DataOutputStream outputStream){
    this.outputStream=outputStream;
    }

    publicvoid Output(Object data){
    try{
    outputStream.writeBytes(data.toString());
    outputStream.flush();
    }
    catch(Exception ex){
    ex.printStackTrace();
    }
    }
    }

    //运行系统进行测试
    package Common;

    import java.io.*;
    import java.net.*;

    publicclass Program {
    privatestatic ServerSocket serverSocket;

    publicstaticvoid main(String[] args) throws ClassNotFoundException {
    // TODO Auto-generated method stub
    Socket socket;
    try {
    serverSocket
    =new ServerSocket(5100); //激活5100端口
    while(true){ //循环捕捉请求
    socket=serverSocket.accept();
    DataOutputStream outStream
    =new DataOutputStream(socket.getOutputStream()); //获取DataOutputStream输出流
    DataInputStream inputStream=new DataInputStream(socket.getInputStream()); //获取DataInputStream输入流

    //调用InputControl对象获取运算结果
    InputControl inputControl=new InputControl(inputStream);
    Object result
    =inputControl.GetResult();
    //调用OutputControl对象输入运算结果
    OutputControl outputControl=new OutputControl(outStream);
    outputControl.Output(result);
    }
    }
    catch (Exception e) {
    // TODO Auto-generated catch block
    e.printStackTrace();

    }
    }
    }
    • 逻辑转换层

    现在先开发一个例子作为参考,在完成客户端开发的时候就可以进行测试。这个例子是先在Manager包里面设置好一个类PersonManager,PersonManager类中包含一个名为GetListByAge的方法。在Contracts.xml文件设置一个名为GetPersonByAge的契约,客户端就可以通过这个契约在远程调用此方法获取计算结果。

    代码
    //设置Person对象
    package Model;

    publicclass Person {
    privateint id;
    private String name;
    privateint age;
    publicvoid setId(int id) {
    this.id = id;
    }
    publicint getId() {
    return id;
    }
    publicvoid setName(String name) {
    this.name = name;
    }
    public String getName() {
    return name;
    }
    publicvoid setAge(int age) {
    this.age = age;
    }
    publicint getAge() {
    return age;
    }
    }

    //开发PersonManager
    package Manager;
    import java.util.ArrayList;
    import java.util.List;
    import org.json.JSONArray;
    import org.json.JSONException;
    import Model.*;

    publicclass PersonManager {
    /*
    * 测试数据
    */
    private List<Person> GetList(){
    List
    <Person> personList=new ArrayList<Person>();
    Person person1
    =new Person();
    person1.setId(
    0);
    person1.setAge(
    23);
    person1.setName(
    "Mike");
    personList.add(person1);

    Person person2
    =new Person();
    person2.setId(
    1);
    person2.setAge(
    29);
    person2.setName(
    "Leslie");
    personList.add(person2);

    Person person3
    =new Person();
    person3.setId(
    2);
    person3.setAge(
    21);
    person3.setName(
    "Jack");
    personList.add(person3);

    Person person4
    =new Person();
    person4.setId(
    3);
    person4.setAge(
    23);
    person4.setName(
    "Rose");
    personList.add(person4);
    return personList;
    }

    /*
    * 获取年龄等于age参数的Person,因为数据将返还给客户端,所以这时把输出数据转化为JSONArray
    */
    public JSONArray GetListByAge(JSONArray jsonList) throws JSONException{
    int age=jsonList.getInt(0); //因为输入参数为一个集合params,所以即使只包括一个参数,也是通过要jsonList的第一个参数来获取的。
    List<Person> personList=GetList();
    List
    <Person> resultList=new ArrayList<Person>();

    for(int n=0;n<personList.size();n++){
    if(personList.get(n).getAge()==age)
    resultList.add(personList.get(n));
    }

    JSONArray jsonArray
    =new JSONArray(resultList);
    return jsonArray;
    }
    }

    然后在Contracts.xml设置绑定

    XML
    <Contracts>
    <Contract name="GetPersonByAge"> //契约名称
    <Input>
    <Description>获取Age等于此值的People对象集</Description> //文字说明
    </Input>
    <Output>
    <Package>Manager</Package> //绑定包
    <Class>PersonManager</Class> //绑定类
    <Method>GetListByAge</Method> //绑定处理方法
    </Output>
    </Contract>
    </Contracts>

    绑定以后,在完成客户端开发的时候就可以进行测试。使用这开发模式的好处在于利用JSON作用数据传输的桥梁,解决不同开发平台之间数据难以同步的问题。使用JSON比XML更容易操作,可以减少传输流量,而且受到各开发语言的支持。使用Contracts.xml在服务器绑定处理方式,使服务器的处理方法与客户端发送的请求实现分离。下面开始介绍一下客户端的开发。

    客户端开发

    客户端的开发的开发相对简单,因为契约是使用   {“ContractName”:“GetPeopleByAge”,“Params”:[23]}    JSON方式进行传送,所以先开发一个MessageEntity实体类来承载契约。

    代码
    namespace Model
    {
    [DataContract]
    publicclass MessageEntity
    {
    //契约名称
    [DataMember]
    publicstring ContractName
    {
    get;
    set;
    }

    //注意参数使用集合的方式来传送
    [DataMember]
    public IList<Object> Params
    {
    get;
    set;
    }
    }
    }

    然后开发一个MessageManager信息管理器来管理契约的传送过程,因为Framework4.0里面,未能对JSON数据中集合的转换提供一个简单函数,所以在MessageManager里面使用了一个Newtonsoft.Json工具包,该工具包里面对JSON的操作有着强大支持,可以在http://www.codeplex.com/官方网站下载

    代码
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Net.Sockets;
    using System.Runtime.Serialization.Json;
    using System.IO;
    using System.Threading;
    using Model;
    using Newtonsoft.Json;

    namespace Common
    {
    publicclass MessageManager
    {
    privatestatic TcpClient _tcpClient;

    //设置tcpClient对象
    publicstatic TcpClient TcpClient
    {
    set { _tcpClient = value; }
    }

    //此处只使用静态方法实现数据传送,发送请求后使用Thread.Sleep等待运算结果,这样存在一定风险,也会降低效率
    //在大型的开发当中应该进一步改善,把信息发送与信息接收分开处理
    publicstaticobject GetMessage(MessageEntity message, Type type)
    {
    NetworkStream networkStream
    = _tcpClient.GetStream();
    //利用DataContractJsonSerializer将MessageEntity对象实现序列化,发送到服务器
    DataContractJsonSerializer jsonSerializer =new DataContractJsonSerializer(typeof(MessageEntity));
    lock (networkStream)
    {
    jsonSerializer.WriteObject(networkStream, message);
    networkStream.Flush();
    }
    Thread.Sleep(
    500);

    //获取回传信息,这里设置接收值1024个字节
    //在实际的开发当中应该使用分批缓存的方式实现数据接收
    byte[] messageByte =newbyte[1024];
    int n =0;
    lock (networkStream)
    n
    = networkStream.Read(messageByte, 0, 1024);
    if (n ==0)
    returnnull;

    //根据输入的type对象,把二进制信息转化为对应的对象
    string jsonMessage = Encoding.ASCII.GetString(messageByte);
    //利用Netonsoft.Json工具集将获取的JSON数据转化对象
    object returnValue = JavaScriptConvert.DeserializeObject(jsonMessage, type);
    return returnValue;
    }
    }
    }

    下面开发一个GetPersonByAge 契约作为例子

    代码
    using System;
    using System.Collections.Generic;
    using System.Text;
    using Model;
    using Common;

    namespace DAL
    {
    publicclass PersonDAL
    {
    ///<summary>
    /// 建立MessageEntity对象,注意输入额定契约名称及数据参数,获取查询结果
    ///</summary>
    ///<param name="age">Person的年龄</param>
    ///<returns>获取年龄等于此值的Person对象集</returns>
    public IList<Person> GetPersonByAge(int age)
    {
    //先建立一个MessageEntity对象,设定其ContractName及参数集合
    //注意ContractName的值必须与服务器端Contracts.xml文件中Contract 项的 name 特性相对应
    MessageEntity messageEntity =new MessageEntity();
    messageEntity.ContractName
    ="GetPersonByAge";
    messageEntity.Params
    =new List<Object> { age };
    //调用MessageManager的GetMessage方法获取计算结果
    IList<Person> personList = (List<Person>)MessageManager.GetMessage(messageEntity, typeof(List<Person>));
    return personList;
    }
    }
    }

    PersonDAL类中的GetPersonByAge方法就是把契约封装在MessageEntity当中,再利用MessageManager把契约发送到服务器端获取运行结果,然后把结果转换为JSON,最后利用Netonsoft.Json工具集的JavaScriptConvert类,把JSON转换成Person对象。

    测试

    代码
    Person实体例
    namespace Model
    {
    publicclass Person
    {
    privateint _id;
    privatestring _name;
    privateint _age;

    publicint id
    {
    get { return _id; }
    set { _id = value; }
    }

    publicint age
    {
    get { return _age; }
    set { _age = value; }
    }

    publicstring name
    {
    get { return _name; }
    set { _name = value; }
    }

    }
    }

    直接调用DAL层
    namespace BLL
    {
    publicclass PersonBLL
    {
    private PersonDAL personDal;

    public PersonBLL()
    {
    personDal
    =new PersonDAL();
    }

    public IList<Person> GetPersonByAge(int age)
    {
    IList
    <Person> personList=personDal.GetPersonByAge(age);
    if (personList.Count !=0)
    return personList;
    else
    returnnew List<Person>();
    }
    }
    }

    测试
    class Program
    {
    privatestatic TcpClient tcpClient =new TcpClient();

    staticvoid Main(string[] args)
    {
    tcpClient.Connect(
    "127.0.0.1", 5100);
    MessageManager.TcpClient
    = tcpClient;

    PersonBLL personBll
    =new PersonBLL();
    IList
    <Person> personList=personBll.GetPersonByAge(23);
    if (personList.Count !=0)
    Console.WriteLine(personList.Count.ToString());
    Console.ReadKey();
    }
    }

    注意测试是输入的查询条件转换成JSON后是 {“ContractName”:“GetPeopleByAge”,“Params”:[23]},而这种  “ContractName":  "契约名","Params": {参数,参数,...}    传送格式是固定不可改变的。当获取查询结果  "[{\"id\":0,\"age\":23,\"name\":\"Mike\"},{\"id\":3,\"age\":23,\"name\":\"Rose\"}] 后 ,MessageManager将通过Newtonsoft.Json把返还值转换为List<Person>。

    到此处,在下为大家介绍了利用JSON数据实现JAVA与.NET之间TCP/IP相互调用,其实以JSON的方式实现并不是唯一的选择,只是在下想在惯常的用法之上,利用一下这个另类的方法,至于在开发结构上有不够周全的地方敬请各位点评。至于以.NET为服务器,JAVA为客户端的TCP/IP通讯实例与此例子极为相像,在此就不作介绍了。

    原代码 (由于上传空间有限,未能将JAVA项目的.metadata一并上传,请运行时先建立JAVA Project项目,再加入原代码即可以成功运行)

    下载

     

    JAVA与.NET的相互调用——利用JNBridge桥接模式实现远程通讯

    JAVA与.NET的相互调用——TCP/IP套接字相互调用的基本架构

    JAVA与.NET的相互调用——通过Web服务实现相互调用

    对 JAVA 开发有兴趣的朋友欢迎加入QQ群:174850571 共同探讨!
    对 .NET  开发有兴趣的朋友欢迎加入QQ群:162338858 共同探讨 !

    cnblogs博客园:http://www.cnblogs.com/leslies2/
    csdn博客:http://blog.csdn.net/leslies2
    原创作品,转载时请注明作者及出处



  • 相关阅读:
    【技术博客】利用handler实现线程之间的消息传递
    BUAA软件工程个人作业-软件案例分析
    BUAA软件工程结对项目作业
    BUAA软件工程个人项目作业
    BUAA软件工程个人博客作业
    BUAA-软件工程第一次作业
    BUAA-OO-最后单元总结
    BUAA-OO-第三单元总结
    BUAA-OO-第二单元总结
    第四单元总结&&OO总结
  • 原文地址:https://www.cnblogs.com/leslies2/p/2035399.html
Copyright © 2020-2023  润新知