IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> Java知识库 -> Java通信方式总结 -> 正文阅读

[Java知识库]Java通信方式总结

Java通信方式总结

1 Java对象、方法间通信方式

1.1 值传递、引用传递

(1)值传递:将副本传递给方法,调用方法改变副本的值,但是并不改变原值

(2)引用传递:传递的是对象(或者变量)的引用,对其修改,会改变原值

内存分配的角度讲解Java中只存在值传递,不存在引用传递

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vfFVxPbt-1647765238072)(F:\XY\software\Typora\Document\Java通信方式总结.assets\image-20211015153112564.png)]

1.2 浅拷贝、深拷贝

(1)浅拷贝:对基本数据类型进行值传递,对引用数据类型进行引用传递般的拷贝,此为浅拷贝。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EPPb5F18-1647765238137)(F:\XY\software\Typora\Document\Java通信方式总结.assets\1460000010648519.jpeg)]
例程图解:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fSq6KYZB-1647765238145)(F:\XY\software\Typora\Document\Java通信方式总结.assets\image-20211023153246429.png)]
(2)深拷贝:对基本数据类型进行值传递,对引用数据类型,创建一个新的对象,并复制其内容,此为深拷贝。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CDkDCgVo-1647765238150)(F:\XY\software\Typora\Document\Java通信方式总结.assets\1460000010648520.jpeg)]
例程图解:
1647765238154)(F:\XY\software\Typora\Document\Java通信方式总结.assets\image-20211023160536863.png)]

2 Java进程间通信方式

2.1 进程通信的目的

(1)数据传输:一个进程需要将它的数据发送给另一个进程。
(2)资源共享:多个进程之间共享同样的资源。
(3)通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件
(4)进程控制:有些进程希望完全控制另一个进程的执行(如 Debug 进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。

2.2 进程间通信的方式

2.2.1 常用方式

(1)管道(pipe):管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用,进程的亲缘关系通常是指父子进程关系。

(2) 消息队列(messagequeue) : 消息队列是由消息的链表,存放在内核中并由消息队列标识符标识、消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。

(3)信号(sinal): 信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。

(4)共享内存(shared memory) :共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号量,配合使用,来实现进程间的同步和通信。

(5)内存映射(mapped memory)

(6)套接字(socket ): 套接字也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同机器间的进程通信。

2.2.2 其他通信方式(远程通信)

(1)RPC(Remote Procedure Call,RPC,远程过程调用)

(2)Web service

(3)RMI(Remote Method Invocation,RMI,远程方法调用)

(4)JMS等

2.2.2.1 RPC

概念:远程过程调用(Remote Procedure Call,缩写为RPC),是一种用于构建基于C/S(客户端/服务器)的分布式应用程序技术。调用者与被调用者可能在同一台服务器上,也可能在由网络连接的不同服务器上,对于他们来说,网络通信是透明的,就是像调用本地方法一样调用远程方法。

简单来说:RPC(remote procedure call)是指远程过程调用,比如两台服务器A和B,A服务器上部署一个应用,B服务器上部署一个应用,A服务器上的应用想调用B服务器上的应用提供的接口,由于不在一个内存空间,不能直接调用,所以需要通过网络来表达调用的语义和传达调用的数据。RPC就是用来传输这些远程调用的语义和数据的框架。RPC只是一种编程模型而非一种规范或协议,并没有规定具体怎样实现。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-anl96KZq-1647765238168)(F:\XY\software\Typora\Document\Java通信方式总结.assets\da7b559cd8b52e1083860b9421122c71.png)]
关键步骤

  • 首先,要解决通讯的问题。主要是通过客户端和服务器端之间建立TCP连接,远程过程调用的所有交换的数据都在这个连接里传输。连接可以是按需连接,调用结束后就断掉,也可以是长连接,多个远程过程调用共享一个连接。
  • 第二,要解决寻址的问题。A服务器上的应用要调用B服务器上的应用,A服务器上的应用需要通过底层RPC框架得知:如何连接到B服务器(主机或IP地址)以及特定的端口,方法的名称等信息,这样才能完成调用。(如函数对应表)
  • 第三,发起调用需要将调用语义和数据进行序列化。A服务器上的应用发起远程调用时,方法的参数需要通过底层的网络协议如TCP传递到B服务器,由于网络协议是基于二进制的,内存中的参数需要序列化成二进制形式,然后再通过寻址和传输将序列化的二进制发送给B服务器。
  • 第四:B服务器收到请求后,需要进行反序列化。恢复为内存中的表达方式,然后找到对应的方法进行本地调用并返回,序列化返回值并发送给A服务器。
  • 第五:处理返回值。A服务器收到B服务器的返回值后,进行反序列化,恢复为内存中的表达方式,然后交给A服务器上的应用进行处理。

例程系统调用过程:
在这里插入图片描述

2.2.2.2 WebService

1、概念:WebService是一种跨编程语言和跨操作系统平台的远程调用技术。

2、基础:WebService提供的服务是基于web容器的,底层使用http协议,类似一个远程的服务提供者。比如天气预报服务,对各地客户端提供天气预报,是一种请求应答的机制,是跨系统跨平台的,就是通过一个servlet,提供服务给其他应用请求。

3、传输:WebService采用HTTP协议传输数据,采用XML格式封装数据(即XML中说明调用远程服务对象的哪个方法,传递的参数是什么,以及服务对象的返回结果是什么)。XML是WebService平台中表示数据的格式。除了易于建立和易于分析外,XML主要的优点在于它既是平台无关的,又是厂商无关的。无关性是比技术优越性更重要的:软件厂商是不会选择一个由竞争对手所发明的技术的。但是XML虽然解决了数据表达问题,却留下了数据格式类型问题,XSD(XML Schema)就是专门解决这个问题的一套标准。它定义了一套标准的数据类型,并给出了一种语言来扩展这套数据类型。

4、WebService三种基本元素:

  • SOAP
  • WSDL
  • UDDI

1)SOAP

SOAP 基于XML 和 HTTP ,其通过XML 来实现消息描述,然后再通过 HTTP 实现消息传输。

Web Service通过HTTP协议发送请求和接收结果时,发送的请求内容和结果内容都采用XML格式封装,并增加了一些特定的HTTP消息头,以说明HTTP消息的内容格式,这些特定的HTTP消息头和XML内容格式就是SOAP协议。SOAP提供了标准RPC方法来调用Web Service。

SOAP协议 = HTTP协议 + XML数据格式

  • SOAP 是XML Web Service 的通信协议。
  • SOAP 是一种规范,用来定义消息的XML 格式 。包含在一对SOAP元素中的、结构正确的XML 段就是SOAP 消息。

2)WSDL(Web Services Description Language,对外的服务描述语言,就是广告)

类似于我们去商店买东西,首先要知道商店里有什么东西可买,然后再来购买,商家的做法就是张贴广告海报。

WebService也一样,WebService客户端要调用一个WebService服务,首先要知道这个服务的地址在哪,以及这个服务里有什么方法可以调用,所以,WebService务器端首先要通过一个WSDL文件来说明自己家里有啥服务可以对外调用,服务是什么(服务中有哪些方法,方法接受的参数是什么,返回值是什么),服务的网络地址用哪个url地址表示,服务通过什么方式来调用。

WSDL(Web Services Description Language)就是这样一个基于XML的语言,用于描述Web Service及其函数、参数和返回值。它是WebService客户端和服务器端都能理解的标准格式。因为是基于XML的,所以WSDL既是机器可阅读的,又是人可阅读的,这将是一个很大的好处。一些最新的开发工具既能根据你的Web service生成WSDL文档(生成广告),又能导入WSDL文档(阅读广告),生成调用相应WebService的代理类代码。

WSDL文件保存在Web服务器上,通过一个url地址就可以访问到它。客户端要调用一个WebService服务之前,要知道该服务的WSDL文件的地址。WebService服务提供商可以通过两种方式来暴露它的WSDL文件地址

  1. 注册到UDDI服务器,以便被人查找
  2. 直接告诉给客户端调用者

3)UDDI(介绍)

UDDI 目录条目是介绍所提供的业务和服务的XML文件,UDDI 目录条目包括三个部分。

“白页”介绍提供服务的公司:名称、地址、联系方式等等;

“黄页”包括基于标准分类法的行业类别;

“绿页”详细介绍了访问服务的接口,以便用户能够编写应用程序以使用 Web 服务。

服务的定义是通过一个称为类型模型(或 tModel)的 UDDI文档来完成的。多数情况下,tModel包含一个WSDL 文件,用于说明访问 XMLWeb Service 的SOAP 接口,但是tModel非常灵活,可以说明几乎所有类型的服务。

2.2.2.3 RMI

Java RMI,一种用于实现远程过程调用(RPC)的Java API
RMI交互图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LjIZIJjG-1647765238179)(F:\XY\software\Typora\Document\Java通信方式总结.assets\20181023090617906.png)]
特点: 能直接传输序列化后的Java对象和分布式垃圾收集。它的实现依赖于Java虚拟机(JVM),因此它仅支持从一个JVM到另一个JVM的调用。甚至,它可以直接看成RPC的java版本。

总结起来,RMI的工作原理大致可以理解为:

1、服务器端提供服务,服务中要暴露可以调用的远程方法,以接口的形式表现。这样在客户端可以通过服务接口来调用远程方法,实现复杂的业务逻辑。在服务器端,首先要对接口中提供的方法实现,以便客户端调用能够完成一定的业务逻辑;

2、接着需要生成Skeleton,在Skeleton中真正地实现了对商业方法的调用,完成了客户请求的调用的过程,将获取到的调用方法的结果通过序列化机制返回给客户端,进行应答。

3、在客户端,通过Stub来接收服务器返回的数据(对象),即在这里进行了反序列化,也就是读取网络传输的字节流,进而进行重构。在Skeleton和Stub中,都对网络通信进行了处理,例如建立套接字,建立网络连接,为实际的业务需要做好准备。

  • RPC与RMI区别

(1)方法调用方式不同:

RMI中是通过在客户端的Stub对象作为远程接口进行远程方法的调用。每个远程方法都具有方法签名。如果一个方法在服务器上执行,但是没有相匹配的签名被添加到这个远程接口(stub)上,那么这个新方法就不能被RMI客户方所调用。

RPC中是通过网络服务协议向远程主机发送请求,请求包含了一个参数集和一个文本值,通常形成“classname.methodname(参数集)”的形式。RPC远程主机就去搜索与之相匹配的类和方法,找到后就执行方法并把结果编码,通过网络协议发回。

(2)适用语言范围不同:

RMI只用于Java

RPC是网络服务协议,与操作系统和语言无关

(3)调用结果的返回形式不同:

Java是面向对象的,所以RMI的调用结果可以是对象类型或者基本数据类型

RMI的结果统一由外部数据表示 (External Data Representation, XDR) 语言表示,这种语言抽象了字节序类和数据类型结构之间的差异

2.2.2.4 JMS

JMS是Java的消息服务,JMS的客户端之间可以通过JMS服务进行异步的消息传输。JMS支持两种消息模型:Point-to-Point(P2P)和Publish/Subscribe(Pub/Sub),即点对点和发布订阅模型。在JMS API出现之前,大部分产品使用“点对点”和“发布/订阅”中的任一方式来进行消息通讯。JMS定义了这两种消息发送模型的规范,它们相互独立。任何JMS的提供者可以实现其中的一种或两种模型,这是它们自己的选择。JMS规范提供了通用接口保证我们基于JMS API编写的程序适用于任何一种模型。

(1)Point-to-Point Messaging Domain(点对点通信模型)

a、模式图:
在这里插入图片描述
b、涉及到的概念:
在点对点通信模式中,应用程序由消息队列,发送方,接收方组成。每个消息都被发送到一个特定的队列,接收者从队列中获取消息。队列保留着消息,直到他们被消费或超时。

c、特点:

  • 每个消息只要一个消费者
  • 发送者和接收者在时间上是没有时间的约束,也就是说发送者在发送完消息之后,不管接收者有没有接受消息,都不会影响发送方发送消息到消息队列中。
  • 发送方不管是否在发送消息,接收方都可以从消息队列中取到消息(The receiver can fetch message whether it is running or not when the sender sends the message)
  • 接收方在接收完消息之后,需要向消息队列应答成功

(2)Publish/Subscribe Messaging Domain(发布/订阅通信模型)

a、模式图:
在这里插入图片描述 
b、涉及到的概念:

在发布/订阅消息模型中,发布者发布一个消息,该消息通过topic传递给所有的客户端。该模式下,发布者与订阅者都是匿名的,即发布者与订阅者都不知道对方是谁。并且可以动态的发布与订阅Topic。Topic主要用于保存和传递消息,且会一直保存消息直到消息被传递给客户端。

c、特点:

  • 一个消息可以传递个多个订阅者(即:一个消息可以有多个接受方)
  • 发布者与订阅者具有时间约束,针对某个主题(Topic)的订阅者,它必须创建一个订阅者之后,才能消费发布者的消息,而且为了消费消息,订阅者必须保持运行的状态。
  • 为了缓和这样严格的时间相关性,JMS允许订阅者创建一个可持久化的订阅。这样,即使订阅者没有被激活(运行),它也能接收到发布者的消息。

JMS、AMQP、MQTT三者区别:

JMS:异步的消息机制,面向企业级应用,只应用在基于JVM的语言,没有标准的底层协议

AMQP:需要跨平台或语言或需要分布式事务的场景下可以使用

MQTT:专为小型无声设备之间通过低带宽发送短消息而设计,多应用于物联网设备,协议简单、流量小、计算量小、能耗低

3 Java线程间通信方式

3.1 同步

这里讲的同步是指多个线程通过synchronized关键字这种方式来实现线程间的通信。

参考示例:

public class MyObject {

    synchronized public void methodA() {
        //do something....
    }

    synchronized public void methodB() {
        //do some other thing
    }
}

public class ThreadA extends Thread {

    private MyObject object;
	//省略构造方法
    @Override
    public void run() {
        super.run();
        object.methodA();
    }
}

public class ThreadB extends Thread {

    private MyObject object;
	//省略构造方法
    @Override
    public void run() {
        super.run();
        object.methodB();
    }
}

public class Run {
    public static void main(String[] args) {
        MyObject object = new MyObject();

        //线程A与线程B 持有的是同一个对象:object
        ThreadA a = new ThreadA(object);
        ThreadB b = new ThreadB(object);
        a.start();
        b.start();
    }
}

由于线程A和线程B持有同一个MyObject类的对象object,尽管这两个线程需要调用不同的方法,但是它们是同步执行的,比如:线程B需要等待线程A执行完了methodA()方法之后,它才能执行methodB()方法。这样,线程A和线程B就实现了通信。

这种方式,本质上就是“共享内存”式的通信。多个线程需要访问同一个共享变量,谁拿到了锁(获得了访问权限),谁就可以执行。

3.2 while轮询的方式

代码如下:

import java.util.ArrayList;
import java.util.List;

public class MyList {

    private List<String> list = new ArrayList<String>();
    public void add() {
        list.add("elements");
    }
    public int size() {
        return list.size();
    }
}


import mylist.MyList;

public class ThreadA extends Thread {

    private MyList list;

    public ThreadA(MyList list) {
        super();
        this.list = list;
    }

    @Override
    public void run() {
        try {
            for (int i = 0; i < 10; i++) {
                list.add();
                System.out.println("添加了" + (i + 1) + "个元素");
                Thread.sleep(1000);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}


import mylist.MyList;

public class ThreadB extends Thread {

    private MyList list;

    public ThreadB(MyList list) {
        super();
        this.list = list;
    }

    @Override
    public void run() {
        try {
            while (true) {
                if (list.size() == 5) {
                    System.out.println("==5, 线程b准备退出了");
                    throw new InterruptedException();
                }
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}


import mylist.MyList;
import extthread.ThreadA;
import extthread.ThreadB;

public class Test {

    public static void main(String[] args) {
        MyList service = new MyList();

        ThreadA a = new ThreadA(service);
        a.setName("A");
        a.start();

        ThreadB b = new ThreadB(service);
        b.setName("B");
        b.start();
    }
}

在这种方式下,线程A不断地改变条件,线程ThreadB不停地通过while语句检测这个条件(list.size()==5)是否成立 ,从而实现了线程间的通信。但是这种方式会浪费CPU资源。之所以说它浪费资源,是因为JVM调度器将CPU交给线程B执行时,它没做什么“有用”的工作,只是在不断地测试某个条件是否成立。就类似于现实生活中,某个人一直看着手机屏幕是否有电话来了,而不是: 在干别的事情,当有电话来时,响铃通知TA电话来了。关于线程的轮询的影响。

这种方式还存在另外一个问题:

线程都是先把变量读取到本地线程栈空间,然后再去再去修改的本地变量。因此,如果线程B每次都在取本地的条件变量,那么尽管另外一个线程已经改变了轮询的条件,它也察觉不到,这样也会造成死循环

3.3 wait/notify机制

代码如下:

import java.util.ArrayList;
import java.util.List;

public class MyList {

    private static List<String> list = new ArrayList<String>();

    public static void add() {
        list.add("anyString");
    }

    public static int size() {
        return list.size();
    }
}


public class ThreadA extends Thread {

    private Object lock;

    public ThreadA(Object lock) {
        super();
        this.lock = lock;
    }

    @Override
    public void run() {
        try {
            synchronized (lock) {
                if (MyList.size() != 5) {
                    System.out.println("wait begin "
                            + System.currentTimeMillis());
                    lock.wait();
                    System.out.println("wait end  "
                            + System.currentTimeMillis());
                }
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}


public class ThreadB extends Thread {
    private Object lock;

    public ThreadB(Object lock) {
        super();
        this.lock = lock;
    }

    @Override
    public void run() {
        try {
            synchronized (lock) {
                for (int i = 0; i < 10; i++) {
                    MyList.add();
                    if (MyList.size() == 5) {
                        lock.notify();
                        System.out.println("已经发出了通知");
                    }
                    System.out.println("添加了" + (i + 1) + "个元素!");
                    Thread.sleep(1000);
                }
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

public class Run {

    public static void main(String[] args) {

        try {
            Object lock = new Object();

            ThreadA a = new ThreadA(lock);
            a.start();
            Thread.sleep(50);
            ThreadB b = new ThreadB(lock);
            b.start();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

线程A要等待某个条件满足时(list.size()==5),才执行操作。线程B则向list中添加元素,改变list 的size。

A,B之间如何通信的呢?也就是说,线程A如何知道 list.size() 已经为5了呢?

这里用到了Object类的 wait() 和 notify() 方法。

当条件未满足时(list.size() !=5),线程A调用wait() 放弃CPU,并进入阻塞状态。——不像while轮询那样占用CPU

当条件满足时,线程B调用 notify()通知线程A,所谓通知线程A,就是唤醒线程A,并让它进入可运行状态。

这种方式的一个好处就是CPU的利用率提高了。

但是也有一些缺点:比如,线程B先执行,一下子添加了5个元素并调用了notify()发送了通知,而此时线程A还执行;当线程A执行并调用wait()时,那它永远就不可能被唤醒了。因为,线程B已经发了通知了,以后不再发通知了。这说明:通知过早,会打乱程序的执行逻辑。

3.4 管道通信

使用java.io.PipedInputStream 和 java.io.PipedOutputStream进行通信,分布式系统中说的两种通信机制:共享内存机制和消息通信机制。synchronized关键字和while轮询 “属于” 共享内存机制,由于是轮询的条件使用了volatile关键字修饰时,这就表示它们通过判断这个“共享的条件变量“是否改变了,来实现线程间的交流。而管道通信,更像消息传递机制,也就是说:通过管道,将一个线程中的消息发送给另一个。

4 总结

4.1 广播机制

4.1.1 观察者模式

观察者模式:在对象之间定义了一对多的依赖,这样一来,当一个对象改变状态,依赖它的对象会收到通知并自动更新。

观察者模式是广播机制,消息订阅和推送的核心设计。

开发中常见的场景:

1.手机中消息推送

2.后台群发信息

观察者模式主要有两个角色

  • Subject 观察主题对象,也可以叫被观察或者被订阅对象
  • Observer 观察者或者订阅者对象,当Subject有变动,就会通知到每一个Observer

上述2.2.2.4中的JMS就是采用观察者模式实现的。

4.2.2 安卓应用(补充)

4.2.2.1 标准广播

完全异步执行的广播,当发出广播后,广播接收器几乎会在同一时刻接收到广播消息,所以没有先后顺序可言,效率比较高,无法被截断。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-97RcfQcT-1647765238197)(F:\XY\software\Typora\Document\Java通信方式总结.assets\image-20211023161452401.png)]

4.2.2.2 有序广播

同步执行的广播,广播发出后,会有一个广播接收器接收广播消息,当这个广播接收器中的逻辑执行完毕后广播才会继续传递。有先后顺序,优先级较高的接收器先收到广播消息并且可以截断正在传递的广播,使得后面的接收器无法收到广播消息。
在这里插入图片描述

4.2.2.3 系统广播

Android内置很多系统级别广播,如手机开机后发一条广播,电池电量发生变化发一条广播等等

4.2.2.4 自定义广播

4.2.2.5 本地广播

(出于安全性考虑,只能在程序内部传递)

优势:正在发送的广播不会离开我们的程序,不必担心数据泄露。其他程序无法将广播发送到我们程序内部,不必担心安全漏洞。发送本地广播比系统全局广播更高效。

4.2 共享存储、长轮询

如3.1 线程同步和3.2 while轮询中的内容

4.3 管道模式

如3.4 管道通信

4.4 等待、通知机制

如3.3 wait/notify机制

4.5 远程通信

如2.2.2所示RPC、WebService、RMI以及JMS

1.文章中所有例程均已上传,资源链接:https://download.csdn.net/download/qq_38233258/85008235
2.文中部分图片、代码引用于其他博主博客,仅用作学习用途,在此表示感谢,如有侵权,可联系我删除。

  Java知识库 最新文章
计算距离春节还有多长时间
系统开发系列 之WebService(spring框架+ma
springBoot+Cache(自定义有效时间配置)
SpringBoot整合mybatis实现增删改查、分页查
spring教程
SpringBoot+Vue实现美食交流网站的设计与实
虚拟机内存结构以及虚拟机中销毁和新建对象
SpringMVC---原理
小李同学: Java如何按多个字段分组
打印票据--java
上一篇文章      下一篇文章      查看所有文章
加:2022-03-21 20:35:37  更:2022-03-21 20:40:27 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/24 8:40:32-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码