当前位置: 首页 > news >正文

JAVA学习之知识补充(下)

六:File类与IO流:

这里给出三种常见的初始化方法:
  1. 通过文件路径初始化:
File file = new File("C:/example/test.txt");
  • 这种方法用于创建一个文件对象,该文件对象表示指定路径的文件或目录。例如:File file = new File("C:/example/test.txt"); 表示创建一个指向 “C:/example/test.txt” 的文件对象。
2. 通过文件路径和文件名初始化:
File file = new File("C:/example", "test.txt");
3. 通过URI初始化:
URI uri = new URI("file:///C:/example/test.txt");
File file = new File(uri);
  • 这种方法用于创建一个文件对象,该文件对象表示指定URI的文件或目录。例如:URI uri = new URI("file:///C:/example/test.txt"); File file = new File(uri); 表示创建一个指向 “C:/example/test.txt” 的文件对象。
这些方法可用于创建Java文件类的实例并指定文件的路径、名称或URI。

在这里插入图片描述

要得到Java文件的相对路径和绝对路径,可以使用以下方法:
  1. 相对路径:相对路径是相对于当前工作目录的路径。可以使用以下代码来获取相对路径:
String relativePath = new File("yourFile.java").getPath();
2. 绝对路径:绝对路径是文件在文件系统中的完整路径。可以使用以下代码来获取绝对路径:
String absolutePath = new File("yourFile.java").getAbsolutePath();
以上代码中的"yourFile.java"是你要获取路径的Java文件名。通过这两种方法,你可以得到Java文件的相对路径和绝对路径。

File类的方法:

(1):基本方法:

在这里插入图片描述

在这里插入图片描述

举例:
  1. 使用list()方法列出目录下一级的文件和子目录名称,如下所示:
import java.io.File;public class ListFilesExample {public static void main(String[] args) {File directory = new File("path/to/directory");String[] files = directory.list();for (String file : files) {System.out.println(file);}}
}
  1. 使用listFiles()方法列出目录下一级的文件和子目录的File对象,如下所示:
import java.io.File;public class ListFilesExample {public static void main(String[] args) {File directory = new File("path/to/directory");File[] files = directory.listFiles();for (File file : files) {System.out.println(file.getName());}}
}
当然,以下是完整的输出示例:
假设目录下有文件 “file1.txt” 和子目录 “subdir”,使用上述两种方法的输出将如下所示:
file1.txt
subdir

(2):命名:

Java中的renameTo方法用于重命名文件或移动文件到另一个目录。它是File类的一个方法,接受一个File对象作为参数,表示文件的新路径或新名称。如果重命名或移动成功,renameTo方法将返回true;否则,返回false。

在这里插入图片描述

如果你只想改变文件的名称而不改变其路径,你可以使用 renameTo() 方法。以下是一个示例,展示如何仅仅重命名文件而不移动它:
import java.io.File;public class FileRenameExample {public static void main(String[] args) {File originalFile = new File("F:\\oldfile.txt"); // 原始文件的路径File newFile = new File("F:\\newfile.txt"); // 新文件的路径if (originalFile.renameTo(newFile)) {System.out.println("文件已重命名");} else {System.out.println("文件无法重命名");}}
}

(3):判断:

在这里插入图片描述

(4):创建、删除功能:

public boolean createNewFile() :创建文件。若文件存在,则不创建,返回false。
• public boolean mkdir() :创建文件目录。如果此文件目录存在,就不创建了。如果此文件目录的上层目录不存在,也不创建。
• public boolean mkdirs() :创建文件目录。如果上层文件目录不存在,一并创建。
• public boolean delete() :删除文件或者文件夹 删除注意事项:① Java 中的删除不走回收站。② 要删除一个文件目录,请注意该文件目录内不能包含文件或者文件目录。
练习 :判断指定目录下是否有后缀名为.jpg 的文件。如果有,就输出该文件名称:
//方法 1:
@Test
public void test1(){
File srcFile = new File("d:\\code");
String[] fileNames = srcFile.list();
for(String fileName : fileNames){
if(fileName.endsWith(".jpg")){System.out.println(fileName);}}
}
//方法 2:
@Test
public void test2(){
File srcFile = new File("d:\\code");
File[] listFiles = srcFile.listFiles();
for(File file : listFiles){
if(file.getName().endsWith(".jpg")){System.out.println(file.getAbsolutePath());}}
}
//方法 3:
/*
* File 类提供了两个文件过滤器方法
* public String[] list(FilenameFilter filter)
* public File[] listFiles(FileFilter filter)
*/
@Test
public void test3(){
File srcFile = new File("d:\\code");
File[] subFiles = srcFile.listFiles(new FilenameFilter() {
@Overridepublic boolean accept(File dir, String name) {return name.endsWith(".jpg");}});for(File file : subFiles){System.out.println(file.getAbsolutePath());}}

拓展:计算大小/删除所有/遍历目录下面所有文件:

以遍历为例子:
import java.io.File;public class FileTraversal {public static void main(String[] args) {File directory = new File("path_to_directory");traverseDirectory(directory);}public static void traverseDirectory(File directory) {if (directory.isDirectory()) {File[] files = directory.listFiles();if (files != null) {for (File file : files) {if (file.isDirectory()) {traverseDirectory(file);} else {System.out.println(file.getName());}}}}}
}

在这里插入图片描述

流:

在这里插入图片描述

在这里插入图片描述

FileReader与FileWriter:

注意:FileReader是输入流,而FileWriter是输出流,输入流是读取数据的,不要搞反了。输入流是从数据源(如文件、网络连接等)读取数据的流,而输出流是向数据目标(如文件、网络连接等)写入数据的流。
当我们使用FileReader来读取文件时,我们创建一个FileReader对象,并使用它来读取文件中的数据。这意味着数据从文件流向我们的程序,因此FileReader是输入流。
另一方面,如果我们想要向文件中写入数据,我们会使用FileWriter,它是输出流。通过FileWriter,我们可以将数据从程序流向文件,因此FileWriter是输出流。

FileReader:

下面是一个简单的示例,演示如何使用FileReader来读取文件中的数据:
import java.io.FileReader;
import java.io.IOException;public class FileReaderExample {public static void main(String[] args) {try {// 创建一个FileReader对象,指定要读取的文件路径FileReader fileReader = new FileReader("example.txt");// 读取文件中的字符数据int character;while ((character = fileReader.read()) != -1) {System.out.print((char) character);}// 关闭文件读取流fileReader.close();} catch (IOException e) {e.printStackTrace();}}
}
在上面的示例中,我们首先创建了一个FileReader对象,指定要读取的文件路径(“example.txt”)。然后,我们使用while循环和read()方法来逐个读取文件中的字符数据,并将其打印到控制台上。最后,我们关闭了文件读取流。
需要注意的是,在使用FileReader时,需要处理可能抛出的IOException异常。在示例中,我们使用了try-catch块来捕获并处理异常。最好用finally,确保一定会关闭流。可以用ctrl+AIt+T快捷操作。
比较规范的写法:
@Test
public void test2() {
FileReader fr = null;
try {
//1. 创建 File 类的对象,对应着物理磁盘上的某个文件
File file = new File("hello.txt");
//2. 创建 FileReader 流对象,将 File 类的对象作为参数传递到FileReader 的构造器中
fr = new FileReader(file);
//3. 通过相关流的方法,读取文件中的数据
/*
* read():每次从对接的文件中读取一个字符。并将此字符返回。
* 如果返回值为-1,则表示文件到了末尾,可以不再读取。
* */
int data;
while ((data = fr.read()) != -1) {
System.out.println((char) data);}
} catch (IOException e) {e.printStackTrace();
} finally {
//4. 关闭相关的流资源,避免出现内存泄漏try {if (fr != null)fr.close();} catch (IOException e) {e.printStackTrace();}}
}
if (fr != null)可以防止流没有创建成功的问题。
当然,read()一个一个读太慢了,可以用其他的方法读取数据:

read(char[] cbuf)

是Java中的一个方法,用于从输入流中读取数据到字符数组cbuf中。该方法会尝试从输入流中读取数据,并将读取的数据存储到cbuf数组中,返回实际读取的字符数。如果输入流中没有数据可供读取,则返回-1。
使用该方法时,需要先创建一个字符数组cbuf,并指定要读取的字符数目。然后调用read(cbuf)方法,将数据读取到数组中。通常在循环中使用该方法,直到返回-1表示数据读取完毕。
例如:
@Test
public void test3() {
FileReader fr = null;
try {
//1. 创建 File 类的对象,对应着物理磁盘上的某个文件
File file = new File("hello.txt");
//2. 创建 FileReader 流对象,将 File 类的对象作为参数传递到FileReader 的构造器中
fr = new FileReader(file);
//3. 通过相关流的方法,读取文件中的数据
char[] cbuf = new char[5];
/*
* read(char[] cbuf) : 每次将文件中的数据读入到 cbuf 数组中,并返回读入到数组中的字符的数。
* */
int len; //记录每次读入的字符的个数
while ((len = fr.read(cbuf)) != -1) {
//处理 char[]数组即可
//错误:
// for(int i = 0;i < cbuf.length;i++){
// System.out.print(cbuf[i]);
// }
//错误:
// String str = new String(cbuf);
// System.out.print(str);
//正确:
// for(int i = 0;i < len;i++){
// System.out.print(cbuf[i]);
// }
//正确:
String str = new String(cbuf, 0, len);
System.out.print(str);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
//4. 关闭相关的流资源,避免出现内存泄漏
try {
if (fr != null)
fr.close();
} catch (IOException e) {e.printStackTrace();}}}
}

FileWriter:

文件不在,会自动创建。文件如果存在,会根据构造器的声明判断,比如:fw=new FileWriter(file),fw=new FileWriter(file,false),都是覆盖,fw=new FileWriter(file,true)是追加等。
下面是 FileWriter 的一些常用方法:
  1. public void write(int c) throws IOException:将单个字符 c 写入文件。
  2. public void write(char[] c, int offset, int len):将字符数组中从索引 offset 开始、长度为 len 的一部分写入文件。
  3. public void write(String s, int offset, int len):将字符串中从索引 offset 开始、长度为 len 的一部分写入文件。
当然,还有一些其他的方法,比如直接写入String或者char []。

在这里插入图片描述

FileInputStream与FileOutStream:

以上均为字节流:

字节流是以字节为单位进行读写操作的流,它主要用于处理二进制数据,如图像、音频、视频等。在Java中,字节流的基本类是InputStream和OutputStream,它们提供了读取和写入字节的方法。
字符流是以字符为单位进行读写操作的流,它主要用于处理文本数据。在Java中,字符流的基本类是Reader和Writer,它们提供了读取和写入字符的方法。
另外,字符流通常会使用指定的字符编码来处理文本数据,而字节流则不会考虑字符编码的问题。因此,在处理文本数据时,通常会使用字符流来避免出现乱码等问题。

也就是说,字符流无法实现图片的拷贝。

举例:
import java.io.*;public class ImageCopy {public static void main(String[] args) {FileInputStream inputStream = null;FileOutputStream outputStream = null;try {File inputFile = new File("input.jpg");File outputFile = new File("output.jpg");inputStream = new FileInputStream(inputFile);outputStream = new FileOutputStream(outputFile);byte[] buffer = new byte[1024];int length;while ((length = inputStream.read(buffer)) > 0) {outputStream.write(buffer, 0, length);}System.out.println("图片复制成功");} catch (IOException e) {System.out.println("发生IO异常:" + e.getMessage());} finally {try {if (inputStream != null) {inputStream.close();}if (outputStream != null) {outputStream.close();}} catch (IOException e) {System.out.println("关闭流时发生异常:" + e.getMessage());}}}
}

在这里插入图片描述

以上都是节点流,下面来看看几种处理流 :

处理流:

(1):缓冲流:
缓冲流的作用是在数据传输过程中提供一个缓冲区,可以暂时存储数据并进行批量处理,从而提高数据传输的效率和性能。缓冲流可以减少对底层资源的频繁访问,减少I/O操作的次数,减少数据传输的延迟,提高数据传输的速度。
另外,缓冲流还可以提供一种更方便的方式来处理数据,比如可以使用缓冲流的缓冲区来进行数据的预读取和预写入,从而提高数据的读写效率。同时,缓冲流还可以提供一种更灵活的方式来控制数据的传输,比如可以设置缓冲区的大小,调整数据传输的速度,实现数据的流控制等功能。

在这里插入图片描述

注意:同一基类具有对应关系。

举例:
import java.io.*;public class ImageCopyExample {public static void main(String[] args) throws IOException {FileInputStream in = null;FileOutputStream out = null;try {File inputFile = new File("input.jpg");File outputFile = new File("output.jpg");in = new FileInputStream(inputFile);out = new FileOutputStream(outputFile);BufferedInputStream bufferedInput = new BufferedInputStream(in);BufferedOutputStream bufferedOutput = new BufferedOutputStream(out);int byteRead;while ((byteRead = bufferedInput.read()) != -1) {bufferedOutput.write(byteRead);}System.out.println("Image copied successfully.");} finally {if ( bufferedInput!= null) {bufferedInput.close();}if (bufferedOutput != null) {bufferedOutput.close();}}}
}
注意,事实上,就是在后面把字节流的名字改为缓冲流的名字,注意,在进行流的关闭时,要先关闭外层流,再关闭内层流,即先缓冲后字节,注意,外层流关闭会自动关闭内层,所以也可以忽略内层流。
补充:readLine():
readLine() 是 Java 中用于读取文本行的方法。它通常与 BufferedReader 类一起使用。以下是使用 readLine() 方法来读取文件内容的示例代码:
import java.io.*;public class Main {public static void main(String[] args) {try {BufferedReader in = new BufferedReader(new FileReader("test.log"));String str;while ((str = in.readLine()) != null) {System.out.println(str);}// Note: The last value of 'str' will be null.} catch (IOException e) {// Handle any exceptions here.}}
}
注意,该方法会忽略掉换行,所以如果需要输出显示器或者写入其他文件要加上换行符或者调用BufferedWriter对象的newLine()方法。
补充:flush():
因为内置缓冲区的原因,如果 FileWriter 不关闭输出流,无法写出字符到文件中。但是关闭的流对象,是无法继续写出数据的。如果我们既想写出数据,又想继续使用流,就需要 flush() 方法了。
• flush() :刷新缓冲区,流对象可以继续使用。
• close():先刷新缓冲区,然后通知系统释放资源。流对象不可以再被使用了。
注意:即便是 flush()方法写出了数据,操作的最后还是要调用 close 方法,释放系统资源。flush相当于把缓冲区已经有的内容全部写入文件,相当于保存了。注意,只用reader类才有该方法。

(2):转换流:

转换流是指将一个流转换成另一个流的过程。在Java中,可以通过InputStreamReader和OutputStreamWriter来进行流的转换。
InputStreamReader可以将字节流转换成字符流,它接受一个字节流作为输入,并根据指定的字符编码将其转换成字符流。这样就可以方便地进行字符的读取和处理。
OutputStreamWriter则是将字符流转换成字节流,它接受一个字符流作为输入,并根据指定的字符编码将其转换成字节流。这样就可以方便地将字符输出到文件或网络中。
通过使用转换流,可以在不改变原有流的情况下,对流进行字符编码的转换,从而方便地进行字符的读写操作。这在处理文件和网络数据时非常有用。

在这里插入图片描述

要先解码,再进行编码。

在这里插入图片描述

举例:

在这里插入图片描述

同理,只用关闭外层即可。
注意,InPutStreamReader,OutPutStreamReader,还可以接受一个字符集参数,在本例中,输出正常,因为IDEA默认就是UTF-8,不需要显式的写出来。

在这里插入图片描述

举例:GBK->UTF-8编码的转换:
/**
\* @author 
\* @create 9:06
*/
public class InputStreamReaderDemo {
@Test
public void test() {
InputStreamReader isr = null;
OutputStreamWriter osw = null;try {isr = new InputStreamReader(new FileInputStream("康师傅的话.txt"),"gbk");osw = new OutputStreamWriter(newFileOutputStream("C:\\Users\\shkstart\\Desktop\\寄语.txt"),"utf-8");
char[] cbuf = new char[1024];
int len;
while ((len = isr.read(cbuf)) != -1) {osw.write(cbuf, 0, len);osw.flush();
}
System.out.println("文件复制完成");
} catch (IOException e) {e.printStackTrace();
} finally {
try {if (isr != null)
isr.close();
} catch (IOException e) {
e.printStackTrace();}
try {
if (osw != null)osw.close();
} catch (IOException e) {e.printStackTrace();}}}
}
注意,最初的文件编码方式为GBK,所以只能使用GBK解码。

字符总结:

在这里插入图片描述

(3):数据流与对象流:

数据流:

数据流是Java中用于读写基本数据类型和字符串的一种流。它提供了一种方便的方式来读写Java的基本数据类型,如int、double、boolean等,以及字符串。
数据流包括DataInputStream和DataOutputStream两个类。DataOutputStream可以将基本数据类型和字符串写入到输出流中,而DataInputStream可以从输入流中读取基本数据类型和字符串。
使用数据流进行数据的读写时,可以方便地将各种基本数据类型和字符串写入到输出流中,并且可以从输入流中读取这些数据。这样可以方便地在不同的系统之间传输数据,而不用担心数据类型的兼容性问题。

在这里插入图片描述

但在实际中,我们更喜欢用对象流,因为它还可以读取引用类型变量。

对象流:

对象流是Java中用于读写对象的一种流。它可以将对象以二进制的形式进行读写,可以用于在网络上传输对象,或者将对象保存到文件中。
对象流包括ObjectInputStream和ObjectOutputStream两个类。ObjectOutputStream可以将对象写入到输出流中,而ObjectInputStream可以从输入流中读取对象。
使用对象流进行对象的读写时,需要注意对象的序列化和反序列化。对象需要实现Serializable接口,以便可以被序列化成二进制形式,并且可以被反序列化还原成对象。
对象流的使用非常灵活,可以用于传输各种类型的对象,包括自定义的对象。它为Java中对象的读写提供了方便的方式,同时也可以用于实现对象的持久化和网络传输。

在这里插入图片描述

以下是一个简单的Java序列化和反序列化的示例。这个例子中,我们有一个名为Person的类,它实现了Serializable接口。然后我们创建了一个Person对象,将其序列化到一个文件中,然后再从该文件中反序列化该对象。
import java.io.*;class Person implements Serializable {public String name;public int age;public Person(String name, int age) {this.name = name;this.age = age;}
}public class Main {public static void main(String[] args) {Person person = new Person("John Doe", 30);// 序列化try {FileOutputStream fileOut = new FileOutputStream("./person.ser");ObjectOutputStream out = new ObjectOutputStream(fileOut);out.writeObject(person);out.close();fileOut.close();System.out.printf("Serialized data is saved in ./person.ser");} catch (IOException i) {i.printStackTrace();}// 反序列化Person deserializedPerson = null;try {FileInputStream fileIn = new FileInputStream("./person.ser");ObjectInputStream in = new ObjectInputStream(fileIn);deserializedPerson = (Person) in.readObject();in.close();fileIn.close();} catch (IOException i) {i.printStackTrace();return;} catch (ClassNotFoundException c) {System.out.println("Person class not found");c.printStackTrace();return;}System.out.println("Deserialized Person...");System.out.println("Name: " + deserializedPerson.name);System.out.println("Age: " + deserializedPerson.age);}
}
ObjectOutputStream out = new ObjectOutputStream(fileOut); 这行代码只是创建了一个 ObjectOutputStream 对象,这个对象可以用来将Java对象序列化并写入到一个输出流中。在这个例子中,输出流是一个文件输出流,它指向 ./person.ser 文件。
真正的序列化过程发生在 out.writeObject(person); 这行代码执行时。这行代码将 person 对象序列化,并将序列化后的数据写入到 ObjectOutputStream 关联的输出流中,也就是 ./person.ser 文件。
所以,当 out.writeObject(person); 这行代码执行完成后,person 对象才被序列化并写入到 ./person.ser 文件中。在这之前,./person.ser 文件要么不存在,要么是空的(或者包含了之前写入的数据)。

注意,ObjectOutputStream也是输出流,所以也有flush方法,道理同上。

在这里插入图片描述

注意:类中静态的属性或者是声明为transient的属性,不会序列化。

(4):其他流:

4.1.标准输入,输出流:

在这里插入图片描述

如图:

在这里插入图片描述

即不用Scanner方法也实现了读入。
4.2.打印流:
如图:

在这里插入图片描述

即向io.txt文件中写入,注意,调用了setOut方法,让输出不在控制台上面显示,而是直接写进文件中。

apache-common包:

Apache Commons是一个可重复使用的Java组件集合,为Java应用程序提供常用功能。Apache Commons项目是Apache软件基金会的一部分,旨在提供高质量、可重复使用的Java组件,这些组件是免费提供的。
Apache Commons包括各种组件,如集合、配置、IO、语言、数学等。这些组件提供了常见编程问题的解决方案,并帮助开发人员编写更干净、更高效的代码。
Apache Commons中的组件通常被打包成JAR(Java Archive)文件,以便于在Java应用程序中使用。JAR文件是一种用于打包Java类、资源和元数据的标准文件格式。当开发人员需要使用Apache Commons中的组件时,他们通常会将相应的JAR文件添加到他们的项目中,并在代码中引用这些组件。
因此,Apache Commons包实际上是一组JAR文件的集合,每个JAR文件包含一个或多个相关的组件。开发人员可以通过将这些JAR文件添加到他们的Java项目中,来使用Apache Commons中的组件,从而为他们的应用程序提供各种常用功能和解决方案。

七:网络编程:

7.1.软件架构:

客户端-服务器(CS)架构是一种传统的架构模式,其中客户端应用程序通过网络连接到服务器,客户端负责处理用户界面和用户输入,而服务器负责处理业务逻辑和数据存储。这种架构适用于需要复杂业务逻辑和数据处理的应用,如企业级应用、数据库系统等。
浏览器-服务器(BS)架构是一种Web应用程序的架构模式,其中浏览器作为客户端,通过HTTP协议向服务器请求页面和资源,服务器端负责处理业务逻辑和数据存储,并将结果以HTML等格式返回给客户端。这种架构适用于互联网应用、电子商务网站等Web应用。
两种架构的主要区别在于CS架构更加灵活和复杂,适用于需要处理复杂业务逻辑和数据的应用,而BS架构更加简单和易于部署,适用于互联网应用和Web网站。同时,CS架构需要安装客户端应用程序,而BS架构只需要浏览器即可访问应用。

7.2.计算机网络:

计算机网络是指将多台计算机通过通信设备连接起来,使它们之间可以相互传输数据和共享资源的系统。计算机网络可以是局域网(LAN)、城域网(MAN)、广域网(WAN)或者因特网(Internet)。它们通过各种通信协议和技术来实现数据的传输和通信。计算机网络的应用非常广泛,包括文件共享、打印共享、电子邮件、网上购物、在线游戏等。

在这里插入图片描述

三大要素:

7.2.1.IP地址:
IP是Internet Protocol的缩写,是互联网协议的一种。它是一种用于在网络上进行数据传输的协议,通过它,数据包可以在网络上进行传输。IP地址是用来标识网络上的设备的唯一地址,类似于人类的家庭地址。
IP地址根据不同的特征可以分为IPv4和IPv6两种类型。IPv4地址是32位的地址,通常用四个十进制数表示,每个数的取值范围是0-255,例如192.168.1.1。而IPv6地址是128位的地址,通常用八组十六进制数表示,每组数之间用冒号分隔,例如2001:0db8:85a3:0000:0000:8a2e:0370:7334。IPv6地址是128位的,128位等于16字节,因为1字节等于8位,所以128位就是16字节。
根据IP地址的分配方式,IP地址又可以分为公网IP(万维网)和私网IP(局域网)。公网IP是可以直接被互联网访问的IP地址,而私网IP是在局域网内使用的IP地址,不能直接被互联网访问。
补充:本地回路地址:127.0.0.1。
补充:域名:

在这里插入图片描述

7.2.2.端口号:
端口号是用于标识不同网络应用程序的数字标识符。在计算机网络中,端口号被用于区分不同的网络应用程序或服务,以便数据包可以被正确地路由到目标应用程序。
端口号的范围是从0到65535,其中0到1023是被称为“系统端口”或“众所周知的端口”,通常用于一些常见的网络服务,比如HTTP(端口号80)、FTP(端口号21)、SSH(端口号22)等。而1024到49151之间的端口号被称为“注册端口”,用于一些常见的应用程序。而49152到65535之间的端口号则被称为“动态或私有端口”,通常用于客户端应用程序或临时服务。
端口号的作用是确保数据包能够被正确地传送到目标应用程序,从而实现网络通信和数据交换。在网络通信中,发送端和接收端的应用程序需要使用相同的端口号才能进行通信。因此,端口号在网络通信中起着非常重要的作用。

在这里插入图片描述

7.2.3.网络通信协议:

网络通信协议是计算机网络中用于在不同设备之间传输数据的规则和约定。它定义了数据的格式、传输方式、错误检测和纠正等方面的规范,以确保不同设备之间能够相互通信。
常见的网络通信协议包括TCP/IP协议、HTTP协议、FTP协议、SMTP协议、POP3协议等。其中,TCP/IP协议是最为重要和基础的网络通信协议,它是互联网的基础协议,用于在不同设备之间进行数据传输和通信。

在这里插入图片描述

7.3.InetAddress:

在这里插入图片描述

getByName()getLocalHost()是Java中两个不同的方法,它们在不同的类和场景中使用。
  1. getByName()方法
    • 这是java.net.InetAddress类中的一个方法,用于获取与指定主机名或IP地址对应的InetAddress对象。
    • 该方法可以接受一个主机名或IP地址字符串作为参数,并返回一个表示该地址的InetAddress对象。
    • 如果传入的参数为null,则返回表示本地主机地址的InetAddress对象。
try {InetAddress ia = InetAddress.getByName("46.21.29.40");System.out.println(ia.getHostName());
} catch (Exception ex) {System.err.println(ex);
}
注意,也可以传入域名。
2.getLocalHost()方法:这个方法用于获取表示本地主机的InetAddress对象。例如:
InetAddress localHost = InetAddress.getLocalHost();
System.out.println("本机的IP地址:" + localHost.getHostAddress());
再来看看两个常用方法:
getHostName()getHostAddress() 是 Java 中 InetAddress 类的两个方法。
  • getHostName() 方法用于获取主机名,例如,如果我们有一个 InetAddress 对象 ip,我们可以通过调用 ip.getHostName() 来获取该主机的主机名。
  • getHostAddress() 方法用于获取 IP 地址,同样,如果我们有一个 InetAddress 对象 ip,我们可以通过调用 ip.getHostAddress() 来获取该主机的 IP 地址。
import java.net.InetAddress;
import java.net.UnknownHostException;public class Address {public static void main(String[] args) {InetAddress ip;try {ip = InetAddress.getLocalHost();String localname = ip.getHostName(); // 获取本机名String localip = ip.getHostAddress(); // 获取本机地址System.out.println("本机名:" + localname);System.out.println("本机地址:" + localip);} catch (UnknownHostException e) {System.out.println("主机不存在或网络连接错误");e.printStackTrace();}}
}

7.4.传输层协议:TCP与UDP协议:

TCP(传输控制协议)是一种面向连接的、可靠的、基于字节流的传输层协议。它提供了数据传输的可靠性和顺序性,通过三次握手建立连接和四次挥手断开连接来确保数据的可靠传输。TCP还具有拥塞控制和流量控制的功能,可以根据网络状况调整数据传输的速率,以保证网络的稳定性和效率。TCP适用于对数据传输可靠性要求较高的应用场景,如网页浏览、文件传输等。
UDP(用户数据报协议)是一种面向无连接的、不可靠的传输层协议。它通过数据包的形式进行数据传输,不保证数据的可靠性和顺序性。UDP的优点是传输速度快,适用于对数据传输实时性要求较高的应用场景,如音视频传输、实时游戏等。但由于UDP不提供可靠性保证,因此在数据传输过程中可能会丢包或出现乱序,需要应用层自行处理。UDP适用于对数据传输实时性要求较高、对数据丢失和乱序容忍度较高的应用场合。

在这里插入图片描述

7.5.Socket类:

在这里插入图片描述

7.5.1.Socket 相关类 API:

7.5.1.1.ServerSocket 类:
//ServerSocket 类的构造方法:ServerSocket(int port) :创建绑定到特定端口的服务器套接字。
//ServerSocket 类的常用方法:Socket accept():侦听并接受到此套接字的连接。
7.5.1.2.Socket 类
//Socket 类的常用构造方法:public Socket(InetAddress address,int port):创建一个流套接字并将其连接到指定IP地址的指定端口号。
• public Socket(String host,int port):创建一个流套接字并将其连接到指定主机上的指定端口号。
//Socket 类的常用方法:public InputStream getInputStream():返回此套接字的输入流,可以用于接收消息
• public OutputStream getOutputStream():返回此套接字的输出流,可以用于发送消息
• public InetAddress getInetAddress():此套接字连接到的远程 IP 地址;如果套接字是未连接的,则返回 null。
• public InetAddress getLocalAddress():获取套接字绑定的本地地址。
• public int getPort():此套接字连接到的远程端口号;如果尚未连接套接字,则返回0。
• public int getLocalPort():返回此套接字绑定到的本地端口。如果尚未绑定套接字,则返回 -1。
• public void close():关闭此套接字。套接字被关闭后,便不可在以后的网络连接中使用(即无法重新连接或重新绑定)。需要创建新的套接字对象。 关闭此套接字也将会关闭该套接字的 InputStreamOutputStream。
• public void shutdownInput():如果在套接字上调用 shutdownInput() 后从套接字输入流读取内容,则流将返回 EOF(文件结束符)。 即不能在从此套接字的输入流中接收任何数据。
• public void shutdownOutput():禁用此套接字的输出流。对于 TCP 套接字,任何以前写入的数据都将被发送,并且后跟 TCP 的正常连接终止序列。 如果在套接字上调用 shutdownOutput() 后写入套接字输出流,则该流将抛出 IOException。 即不能通过此套接字的输出流发送任何数据。
注意:先后调用 Socket 的 shutdownInput()和 shutdownOutput()方法,仅仅关闭了输入流和输出流,并不等于调用 Socket 的 close()方法。在通信结束后,仍然要调用 Scoket 的 close()方法,因为只有该方法才会释放 Socket 占用的资源,比如占用的本地端口号等。

7.6.TCP网络编程:

在这里插入图片描述

客户端程序包含以下四个基本的步骤 :
• 创建 Socket :根据指定服务端的 IP 地址或端口号构造 Socket 类对象。若服务器端响应,则建立客户端到服务器的通信线路。若连接失败,会出现异常。
• 打开连接到 Socket 的输入/ 出流: 使用 getInputStream()方法获得输入流,使用getOutputStream()方法获得输出流,进行数据传输
• 按照一定的协议对 Socket 进行读/ 写操作:通过输入流读取服务器放入线路的信息(但不能读取自己放入线路的信息),通过输出流将信息写入线路。
• 关闭 Socket :断开客户端到服务器的连接,释放线路
服务器端程序包含以下四个基本的 步骤:
• 调用 ServerSocket(int port) :创建一个服务器端套接字,并绑定到指定端口上。用于监听客户端的请求。
• 调用 accept() :监听连接请求,如果客户端请求连接,则接受连接,返回通信套接字对象。
• 调用 该 Socket 类对象的 getOutputStream() 和 getInputStream () :获取输出流和输入流,开始网络数据的发送和接收。
• 关闭 Socket 对象:客户端访问结束,关闭通信套接字。
举例:客户端发送内容给服务端,服务端将内容打印到控制台上。
客户端代码:
public class Client {
public static void main(String[] args) throws Exception {
// 1、准备 Socket,连接服务器,需要指定服务器的 IP 地址和端口号
Socket socket = new Socket("127.0.0.1", 8888);
// 2、获取输出流,用来发送数据给服务器
OutputStream out = socket.getOutputStream();
// 发送数据
out.write("lalala".getBytes());
//会在流末尾写入一个“流的末尾”标记,对方才能读到-1,否则对方的读取方法会一致阻塞
socket.shutdownOutput();
//3、获取输入流,用来接收服务器发送给该客户端的数据
InputStream input = socket.getInputStream();
// 接收数据
byte[] data = new byte[1024];
StringBuilder s = new StringBuilder();
int len;
while ((len = input.read(data)) != -1) {
s.append(new String(data, 0, len));
}
System.out.println("服务器返回的消息是:" + s);
//4、关闭 socket,不再与服务器通信,即断开与服务器的连接
//socket 关闭,意味着 InputStream 和 OutputStream 也关闭了
socket.close();
}
}
注意,socket.shutdownOutput();必须要有这一步,防止客户端仍在继续尝试发送数据。
服务端代码:
public class Server {
public static void main(String[] args)throws Exception {
//1、准备一个 ServerSocket 对象,并绑定 8888 端口
ServerSocket server = new ServerSocket(8888);
System.out.println("等待连接....");
//2、在 8888 端口监听客户端的连接,该方法是个阻塞的方法,如果没有客户端连接,将一直等待
Socket socket = server.accept();
InetAddress inetAddress = socket.getInetAddress();
System.out.println(inetAddress.getHostAddress() + "客户端连接成功!!");
//3、获取输入流,用来接收该客户端发送给服务器的数据
InputStream input = socket.getInputStream();
//接收数据
byte[] data = new byte[1024];
StringBuilder s = new StringBuilder();
int len;
while ((len = input.read(data)) != -1) {
s.append(new String(data, 0, len));
}
System.out.println(inetAddress.getHostAddress() + "客户端发送的消息是:" + s);
//4、获取输出流,用来发送数据给该客户端
OutputStream out = socket.getOutputStream();
//发送数据
out.write("欢迎登录".getBytes());
out.flush();
//5、关闭 socket,不再与该客户端通信
//socket 关闭,意味着 InputStream 和 OutputStream 也关闭了
socket.close();
//6、如果不再接收任何客户端通信,可以关闭 ServerSocket
server.close();}
}
注意:可以通过socket.getInetAddress().getHostAddress()获取到是谁在进行连接。
注意,有些边读边输出的方法可能会出现乱码,比如字节流存入数组对原来的文本有截断,则会出现乱码,可以通过ByteArrayOutputStream解决,代码如下:
byte[] data = new byte[1024];
int len;
ByteArrayOutputStream baos = new ByteArrayOutputStream();
while ((len = input.read(data)) != -1) {baos.write(data,0,len);
}
System.out.println(inetAddress.getHostAddress() + "客户端发送的消息是:" + baos.toString());
相当于把每部分写入baos中,最后一起输出。

7.7.UDP网络编程:

UDP(User Datagram Protocol,用户数据报协议):是一个无连接的传输层协议、提供面向事务的简单不可靠的信息传送服务,类似于短信。
发送端程序包含以下四个基本的步骤:
• 创建 DatagramSocket :默认使用系统随机分配端口号。
• 创建 DatagramPacket:将要发送的数据用字节数组表示,并指定要发送的数据长度,接收方的IP 地址和端口号。
• 调用 该 DatagramSocket 类对象的 send 方法 :发送数据报 DatagramPacket 对象。
• 关闭 DatagramSocket 对象:发送端程序结束,关闭通信套接字。
接收端程序包含以下四个基本的步骤 :
• 创建 DatagramSocket :指定监听的端口号。
• 创建 DatagramPacket:指定接收数据用的字节数组,起到临时数据缓冲区的效果,并指定最大可以接收的数据长度。
• 调用 该 DatagramSocket 类对象的 receive 方法 :接收数据报 DatagramPacket 对象。
• 关闭 DatagramSocket :接收端程序结束,关闭通信套接字。
举例:
发送端:
DatagramSocket ds = null;
try {ds = new DatagramSocket();byte[] by = "hello,atguigu.com".getBytes();DatagramPacket dp = new DatagramPacket(by, 0, by.length,InetAddress.getByName("127.0.0.1"), 10000);
ds.send(dp);
} catch (Exception e) {e.printStackTrace();
} finally {
if (ds != null)ds.close();
}
接收端:
DatagramSocket ds = null;
try {
ds = new DatagramSocket(10000);
byte[] by = new byte[1024*64];
DatagramPacket dp = new DatagramPacket(by, by.length);
ds.receive(dp);
String str = new String(dp.getData(), 0, dp.getLength());
System.out.println(str + "--" + dp.getAddress());
} catch (Exception e) {
e.printStackTrace();
} finally {
if (ds != null)
ds.close();
}

7.8.URL网络编程:

URL(Uniform Resource Locator):统一资源定位符,它表示 Internet 上某一资源的地址。

• 通过 URL 我们可以访问 Internet 上的各种网络资源,比如最常见的 www,ftp 站点。浏览器通过解析给定的 URL 可以在网络上查找相应的文件或其他资源。
• URL 的基本结构由 5 部分组成:<传输协议>://<主机名>:<端口号>/<文件名>#片段名?参数列表
• 例如:http://192.168.1.100:8080/helloworld/index.jsp#a?username=shkstart&password=123

URL 类常用方法:

一个 URL 对象生成后,其属性是不能被改变的,但可以通过它给定的方法来获取这些属性:
public String getProtocol( ) 获取该 URL 的协议名
• public String getHost( ) 获取该 URL 的主机名
• public String getPort( ) 获取该 URL 的端口号
• public String getPath( ) 获取该 URL 的文件路径
• public String getFile( ) 获取该 URL 的文件名
• public String getQuery( ) 获取该 URL 的查询名

八:反射机制:

Java 程序中,所有的对象都有两种类型:
编译时类型和运行时类型,而很多时候对象的编译时类型和运行时类型不一致。 Object obj = new String(“hello”);obj.getClass()。
例如:某些变量或形参的声明类型是 Object 类型,但是程序却需要调用该对象运行时类型的方法,该方法不是 Object 中的方法,那么如何解决呢?
解决这个问题,有两种方案:
方案 1:在编译和运行时都完全知道类型的具体信息,在这种情况下,我们可以直接先使用 instanceof 运算符进行判断,再利用强制类型转换符将其转换成运行时类型的变量即可。
方案 2:编译时根本无法预知该对象和类的真实信息,程序只能依靠运行时信息来发现该对象和类的真实信息,这就必须使用反射。

反射概述

Reflection(反射)是被视为动态语言的关键,反射机制允许程序在运行期间借助于 Reflection API 取得任何类的内部信息,并能直接操作任意对象的内部属性及方法。

反射可以打破封装性。

Java 反射机制提供的功能:

• 在运行时判断任意一个对象所属的类
• 在运行时构造任意一个类的对象
• 在运行时判断任意一个类所具有的成员变量和方法
• 在运行时获取泛型信息
• 在运行时调用任意一个对象的成员变量和方法
• 在运行时处理注解
• 生成动态代理

下面都可以获取Class类:

(1)class:外部类,成员(成员内部类,静态内部类),局部内部类,匿名内部
类 (2)interface:接口 (3)[]:数组 (4)enum:枚举 (5)annotation
:注解@interface (6)primitive type:基本数据类型 (7)void

8.1.获取 Class 类的实例(四种方法):

方式 1:要求编译期间已知类型:
前提:若已知具体的类,通过类的 class 属性获取,该方法最为安全可靠,程序性能最高。
实例:
Class clazz = String.class;

方式 2:获取对象的运行时类型:

前提:已知某个类的实例,调用该实例的 getClass()方法获取 Class 对象
实例:
Class clazz = "www.atguigu.com".getClass();

方式 3:可以获取编译期间未知的类型:

前提:已知一个类的全类名,且该类在类路径下,可通过 Class 类的静态方法forName()获取,可能抛出 ClassNotFoundException:
实例:
Class clazz = Class.forName("java.lang.String");
当使用Class.forName()方法时,我们需要传入一个字符串参数,该参数是类的全限定名(包括包名)。该方法会返回一个Class对象,该对象代表了对应类的运行时类型信息。

方式 4:其他方式(不做要求):

前提:可以用系统类加载对象或自定义加载器对象加载指定路径下的类型
实例:
ClassLoader cl = this.getClass().getClassLoader();
Class clazz4 = cl.loadClass("类的全类名");
注意,两个相同的类的Class对象是相等的,因为类只会加载一次。

8.2.Class类的理解:

在这里插入图片描述

使用了类的加载器加载到内存的方法区。

8.3.类的加载过程与类的加载器:

在这里插入图片描述

同样的加载器只能加载一次,不同的加载器可以加载多次。
查看某个类的类加载器对象:
(1)获取默认的系统类加载器
ClassLoader classloader = ClassLoader.getSystemClassLoader();
(2)查看某个类是哪个类加载器加载的
ClassLoader classloader =Class.forName("exer2.ClassloaderDemo").getClassLoader();
//如果是根加载器加载的类,则会得到 null
ClassLoader classloader1 =
Class.forName("java.lang.Object").getClassLoader();
(3)获取某个类加载器的父加载器
ClassLoader parentClassloader = classloader.getParent();
使用 ClassLoader 获取流
关于类加载器的一个主要方法:getResourceAsStream(String str):获取类路径下的指定文件的输入流:
@Test
public void test5() throws IOException {
Properties pros = new Properties();
//方式 1:此时默认的相对路径是当前的 module
// FileInputStream is = new FileInputStream("info.properties");
// FileInputStream is = new FileInputStream("src//info1.properties");
//方式 2:使用类的加载器
//此时默认的相对路径是当前 module 的 src 目录
InputStream is =
ClassLoader.getSystemClassLoader().getResourceAsStream("info1.properties");
pros.load(is);
//获取配置文件中的信息
String name = pros.getProperty("name");
String password = pros.getProperty("password");
System.out.println("name = " + name + ", password = " + password);
}

8.4.反射的运用:

4.1 应用 1:创建运行时类的对象:

这是反射机制应用最多的地方。创建运行时类的对象有两种方式:
方式 1:直接调用 Class 对象的 newInstance()方法:
要 求: 1)类必须有一个无参数的构造器。2)类的构造器的访问权限需要足够。
方式 2:通过获取构造器对象来进行实例化:
方式一的步骤:
1)获取该类型的 Class 对象 2)调用 Class 对象的 newInstance()方法创建对象
方式二的步骤:
1)通过 Class 类的 getDeclaredConstructor(Class … parameterTypes)取得本类的指定形参类型的构造器 2)向构造器的形参中传递一个对象数组进去,里面包含了构造器中所需的各个参数。 3)通过 Constructor 实例化对象。如果构造器的权限修饰符修饰的范围不可见,也可以调setAccessible(true)
示例代码:
package com.atguigu.reflect;
import org.junit.Test;
import java.lang.reflect.Constructor;
public class TestCreateObject {
@Test
public void test1() throws Exception{
// AtGuiguClass obj = new AtGuiguClass();//编译期间无法创建
Class<?> clazz = Class.forName("com.atguigu.ext.demo.AtGuiguClass");
//clazz 代表 com.atguigu.ext.demo.AtGuiguClass 类型
//clazz.newInstance()创建的就是 AtGuiguClass 的对象
Object obj = clazz.newInstance();
System.out.println(obj);
}
@Test
public void test2()throws Exception{
Class<?> clazz = Class.forName("com.atguigu.ext.demo.AtGuiguDemo");
//java.lang.InstantiationException:
com.atguigu.ext.demo.AtGuiguDemo
//Caused by: java.lang.NoSuchMethodException:
com.atguigu.ext.demo.AtGuiguDemo.<init>()//即说明 AtGuiguDemo 没有无参构造,就没有无参实例初始化方法<init>
Object stu = clazz.newInstance();
System.out.println(stu);
}
@Test
public void test3()throws Exception{
//(1)获取 Class 对象
Class<?> clazz = Class.forName("com.atguigu.ext.demo.AtGuiguDemo");
/*
\* 获取 AtGuiguDemo 类型中的有参构造
\* 如果构造器有多个,我们通常是根据形参【类型】列表来获取指定的一个
构造器的
\* 例如:public AtGuiguDemo(String title, int num)
*/
//(2)获取构造器对象
Constructor<?> constructor =
clazz.getDeclaredConstructor(String.class,int.class);
//(3)创建实例对象
// T newInstance(Object... initargs) 这个 Object...是在创建对象时,给有参构造的实参列表
Object obj = constructor.newInstance("尚硅谷",2022);
System.out.println(obj);
}
}

4.2. 应用 2:获取运行时类的完整结构:

可以获取:包、修饰符、类型名、父类(包括泛型父类)、父接口(包括泛型父接口)、成员(属性、构造器、方法)、注解(类上的、方法上的、属性上的)。
//1.实现的全部接口
public Class<?>[] getInterfaces()
//确定此对象所表示的类或接口实现的接口。
//2.所继承的父类
public Class<? Super T> getSuperclass()
//返回表示此 Class 所表示的实体(类、接口、基本类型)的父类的 Class。
//3.全部的构造器
public Constructor<T>[] getConstructors()
//返回此 Class 对象所表示的类的所有 public 构造方法。
public Constructor<T>[] getDeclaredConstructors()
//返回此 Class 对象表示的类声明的所有构造方法。
//Constructor 类中:
//取得修饰符:
public int getModifiers();
//取得方法名称:
public String getName();
//取得参数的类型:
public Class<?>[] getParameterTypes();
//4.全部的方法
public Method[] getDeclaredMethods()
//返回此 Class 对象所表示的类或接口的全部方法
public Method[] getMethods()
//返回此 Class 对象所表示的类或接口的 public 的方法
//Method 类中:
public Class<?> getReturnType()
//取得全部的返回值
public Class<?>[] getParameterTypes()
//取得全部的参数
public int getModifiers()
//取得修饰符
public Class<?>[] getExceptionTypes()
//取得异常信息
//5.全部的 Field
public Field[] getFields()
//返回此 Class 对象所表示的类或接口的 public 的 Field。
public Field[] getDeclaredFields()
//返回此 Class 对象所表示的类或接口的全部 Field。
//Field 方法中:
public int getModifiers()
//以整数形式返回此 Field 的修饰符
public Class<?> getType()
//得到 Field 的属性类型
public String getName()
//返回 Field 的名称。
//6. Annotation 相关
get Annotation(Class<T> annotationClass)
getDeclaredAnnotations()
//7.泛型相关
//获取父类泛型类型:
Type getGenericSuperclass()
//泛型类型:ParameterizedType
//获取实际的泛型类型参数数组:
getActualTypeArguments()
//8.类所在的包
Package getPackage()

4.3. 应用 3:调用运行时类的指定结构:

4.3.1 调用指定的属性
在反射机制中,可以直接通过 Field 类操作类中的属性,通过 Field 类提供的set()和 get()方法就可以完成设置和取得属性内容的操作。
(1)获取该类型的 Class 对象
Class clazz = Class.forName("包.类名");
(2)获取属性对象
Field field = clazz.getDeclaredField("属性名");
getDeclaredField可以获取所以属性,而getField只能获取public属性。
(3)如果属性的权限修饰符不是 public,那么需要设置属性可访问
field.setAccessible(true);
(4)创建实例对象:如果操作的是非静态属性,需要创建实例对象
Object obj = clazz.newInstance(); //有公共的无参构造
Object obj = 构造器对象.newInstance(实参...);//通过特定构造器对象创建实例对象
(4)设置指定对象 obj 上此 Field 的属性内容
field.set(obj,"属性值");
如果操作静态变量,那么实例对象可以省略,用 null 表示
(5)取得指定对象 obj 上此 Field 的属性内容
Object value = field.get(obj);
如果操作静态变量,那么实例对象可以省略,用 null 表示

4.3.2 调用指定的方法:

(1)获取该类型的 Class 对象
Class clazz = Class.forName("包.类名");
(2)获取方法对象
Method method = clazz.getDeclaredMethod("方法名",方法的形参类型列表);
形参列表要写成String.class等。
(3)创建实例对象
Object obj = clazz.newInstance();
(4)调用方法
Object result = method.invoke(obj, 方法的实参值列表);
如果方法的权限修饰符修饰的范围不可见,也可以调用setAccessible(true),如果方法是静态方法,实例对象也可以省略,用 null 代替。

4.3.3.调用指定构造器:

在Java的反射机制中,可以使用Class类的newInstance()方法来调用指定构造器来创建对象。newInstance()方法会调用类的默认构造器来创建对象,如果需要调用指定的构造器,可以使用Constructor类的newInstance()方法来实现。
常用的方法包括:
  1. getConstructor(Class… parameterTypes):根据参数类型获取指定的构造器。
  2. getConstructors():获取所有公共的构造器。
  3. getDeclaredConstructor(Class… parameterTypes):根据参数类型获取指定的构造器,包括私有构造器。
  4. getDeclaredConstructors():获取所有的构造器,包括私有构造器。
  5. newInstance(Object… initargs):使用指定的参数调用构造器来创建对象。
  6. setAccessible(boolean flag):设置构造器的可访问性,如果构造器是私有的,需要设置为true才能调用。
用指定构造器时,可以通过以下示例代码来实现:
import java.lang.reflect.Constructor;public class ReflectionExample {public static void main(String[] args) {try {// 获取Class对象Class<?> clazz = Class.forName("com.example.MyClass");// 获取指定参数类型的构造器Constructor<?> constructor = clazz.getConstructor(String.class, int.class);// 使用构造器创建对象Object obj = constructor.newInstance("example", 123);// 打印对象System.out.println(obj);} catch (Exception e) {e.printStackTrace();}}
}
在上面的示例中,我们首先获取了MyClass类的Class对象,然后通过getConstructor()方法获取了一个带有String和int参数的构造器。接着使用newInstance()方法调用该构造器来创建一个MyClass对象,并将其打印出来。

4.3.4.调用注解:

一个完整的注解应该包含三个部分: (1)声明 (2)使用 (3)读取。
(1)声明自定义注解:
package com.atguigu.annotation;
import java.lang.annotation.*;
@Inherited
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Table {
String value();
}
让我们来分解这段代码:
  1. @Inherited: 这个注解表明该注解可以被子类继承。如果一个类使用了带有@Inherited注解的注解,那么它的子类也会继承这个注解。
  2. @Target(ElementType.TYPE): 这个注解指定了注解的作用目标。在这里,ElementType.TYPE表示这个注解可以用在类、接口、枚举或注解类型上。
  3. @Retention(RetentionPolicy.RUNTIME): 这个注解指定了注解的生命周期。在这里,RetentionPolicy.RUNTIME表示这个注解在运行时仍然可用,这意味着可以通过反射来获取这个注解的信息。
  4. public @interface Table { String value(); }: 这是注解的声明部分。它使用了关键字@interface来声明一个注解,并定义了一个名为Table的注解。在这个注解中,有一个名为value的成员,它没有指定默认值,因此在使用这个注解时,需要为value成员提供值。
总的来说,这段代码定义了一个可以用在类上的注解@Table,它可以被子类继承,在运行时可用,并且需要提供一个value值。这个注解可以用来为类添加表名的元数据信息,例如:
@Table("user_table")
public class User {// 类的定义
}
• 自定义注解可以通过四个元注解@Retention,@Target,@Inherited,@Documented,分别说明它的声明周期,使用位置,是否被继承,是否被生成到 API 文档中。
• Annotation 的成员在 Annotation 定义中以无参数有返回值的抽象方法的形式来声明,我们又称为配置参数。返回值类型只能是八种基本数据类型、String 类型、Class 类型、enum 类型、Annotation 类型、以上所有类型的数组。
• 可以使用 default 关键字为抽象方法指定默认返回值。
• 如果定义的注解含有抽象方法,那么使用时必须指定返回值,除非它有默认值。格式是“方法名 = 返回值”,如果只有一个抽象方法需要赋值,且方法名为 value,可以省略“value=”,所以如果注解只有一个抽象方法成员,建议使用方法名 value。

(2)使用注解:

@Table("t_stu")
public class Student {
@Column(columnName = "sid",columnType = "int")
private int id;
@Column(columnName = "sname",columnType = "varchar(20)")
private String name;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "Student{" +"id=" + id +", name='" + name + '\'' +'}';}
}
这段代码展示了一个使用自定义注解的Java类。让我们逐步解释这段代码的含义:
@Table("t_stu")
public class Student {
在这里,@Table(“t_stu”)注解被应用在Student类上,它为Student类添加了一个名为"t_stu"的元数据信息,可能表示这个类对应数据库中的"t_stu"表。
@Column(columnName = "sid", columnType = "int")
private int id;
@Column(columnName = "sname", columnType = "varchar(20)")
private String name;
在这里,@Column注解被应用在类的成员变量上,它为id和name成员变量添加了元数据信息,包括字段名和字段类型。

(3):读取和处理自定义注解

自定义注解必须配上注解的信息处理流程才有意义。我们自己定义的注解,只能使用反射的代码读取。所以自定义注解的声明周期必须是 RetentionPolicy.RUNTIME。
package com.atguigu.annotation;
import java.lang.reflect.Field;
public class TestAnnotation {
public static void main(String[] args) {
Class studentClass = Student.class;
Table tableAnnotation = (Table) studentClass.getAnnotation(Table.class);
String tableName = "";
if(tableAnnotation != null){tableName = tableAnnotation.value();
}
Field[] declaredFields = studentClass.getDeclaredFields();
String[] columns = new String[declaredFields.length];
int index = 0;
for (Field declaredField : declaredFields) {
Column column = declaredField.getAnnotation(Column.class);
if(column!= null) {
columns[index++] = column.columnName();}
}
String sql = "select ";
for (int i=0; i<index; i++) {
sql += columns[i];
if(i<index-1){sql += ",";
}
}
sql += " from " + tableName;
System.out.println("sql = " + sql);
}
}
这段代码是一个简单的示例,演示了如何使用Java的反射机制和注解来生成SQL查询语句。
首先,通过反射获取了Student类的Class对象,并通过该对象获取了@Table注解的值,即表名。然后再通过反射获取了Student类的所有字段,并遍历每个字段,获取其对应的@Column注解的值,即列名。最后根据获取到的表名和列名,拼接成了一个简单的SQL查询语句。
这个示例展示了如何使用注解来标记表名和列名,然后通过反射来动态地获取这些注解的值,从而生成相应的SQL语句。这种方式可以使代码更加灵活和可维护,同时也提高了代码的复用性。

相关文章:

JAVA学习之知识补充(下)

六&#xff1a;File类与IO流&#xff1a; 这里给出三种常见的初始化方法&#xff1a; 通过文件路径初始化: File file new File("C:/example/test.txt");这种方法用于创建一个文件对象&#xff0c;该文件对象表示指定路径的文件或目录。例如&#xff1a;File fil…...

qt生成一幅纯马赛克图像

由于项目需要&#xff0c;需生成一幅纯马赛克的图像作为背景&#xff0c;经过多次测试成功&#xff0c;记录下来。 方法一&#xff1a;未优化方法 1、代码&#xff1a; #include <QImage> #include <QDebug> #include <QElapsedTimer>QImage generateMosa…...

python循环——九九乘法表(更加轻松的理解循环结构)

感受 首先&#xff0c;得明确意识到这个问题&#xff0c;就是我的循环结构学的一塌糊涂&#xff0c;完全不能很好的使用这个循环来实现各种九九乘法表达输出&#xff0c;这样的循环结构太差了&#xff0c;还需要我自己找时间来补充一下循环的使用&#xff0c;来拓宽自己的思考方…...

UDS诊断系列之十八故障码的状态掩码

在谈19服务的子功能之前&#xff0c;先说一下故障码&#xff08;DTC&#xff09;的状态掩码是什么。 一、状态掩码 状态掩码由八个状态位构成&#xff0c;客户端利用它向服务器请求与其状态相匹配的DTC信息。当服务器接收到来自客户端的请求时&#xff0c;它会通过过滤匹配的…...

【jvm】直接引用

目录 1. 说明2. 形式3. 特点4. 生成过程5. 作用 1. 说明 1.在Java虚拟机&#xff08;JVM&#xff09;中&#xff0c;直接引用&#xff08;Direct Reference&#xff09;是相对于符号引用&#xff08;Symbolic Reference&#xff09;而言的&#xff0c;它是指向内存中实际存在的…...

PythonStudio 控件使用常用方式(二十七)TActionList

PythonStudio是一个极强的开发Python的IDE工具&#xff0c;官网地址是&#xff1a;https://glsite.com/ &#xff0c;在官网可以下载最新版的PythonStudio&#xff0c;同时&#xff0c;在使用PythonStudio时&#xff0c;它也能及时为用户升到最新版本。它使用的是Delphi的控件&…...

PDF 转Word 开源库

1. Apache PDFBox Apache PDFBox 是一个开源的 Java 库&#xff0c;用于创建和操作 PDF 文档。虽然 PDFBox 本身没有直接支持 PDF 转 Word 的功能&#xff0c;但它可以提取 PDF 内容&#xff0c;你可以结合其他方法将这些内容写入 Word。 添加依赖 <dependency><gr…...

Docker - 深入理解Dockerfile中的 RUN, CMD 和 ENTRYPOINT

RUN docker file 中的 RUN 命令相对来教容易理解 RUN 指令用于在构建镜像时执行命令&#xff0c;这些命令会在 Docker 镜像的构建过程中执行。常用于安装软件包、设置环境变量、创建目录等。RUN 指令会在镜像构建中创建新的镜像层&#xff0c;每个 RUN 指令都会创建一个新的镜…...

Python 函数式编程 内置高阶函数及周边【进阶篇 3】推荐

前面我们已经总结并实践了用python获取到了数据。也介绍了python中http网络请求的几种方式&#xff0c;正在学习python开发语言或者对python3知识点生疏需要回顾的请点这里 &#xff0c;本章主要总结了函数式编程及特点 和 python中内置的高阶函数及周边知识&#xff0c;方便自…...

【Rust光年纪】探秘Rust GUI库:从安装配置到API概览

Rust语言GUI库全方位比较&#xff1a;选择适合你的工具 前言 在现代软件开发中&#xff0c;图形用户界面&#xff08;GUI&#xff09;库扮演着至关重要的角色。随着Rust语言的不断发展&#xff0c;越来越多的优秀的GUI库也相继问世&#xff0c;为Rust开发者提供了更多选择。本…...

Element plus部分组件样式覆盖记录

文章目录 一、el-button 样式二、Popconfirm 气泡确认框三、Popover 气泡卡片四、Checkbox 多选框五、Pagination 分页六、Form 表单七、Table 表格 一、el-button 样式 html&#xff1a; <el-button class"com_btn_style">button</el-button>样式覆盖…...

重塑业务生态,Vatee万腾平台:引领行业变革的新引擎

在数字经济浪潮汹涌的今天&#xff0c;传统行业的边界正被不断模糊与重塑&#xff0c;新兴技术如云计算、大数据、人工智能等正以前所未有的速度改变着商业世界的面貌。在这一背景下&#xff0c;Vatee万腾平台应运而生&#xff0c;以其独特的创新模式和强大的技术实力&#xff…...

标准术语和定义中的【架构】应该如何描述

一、参考国家标准和国际标准中对“架构”的描述 &#xff08;1&#xff09;GB/T 8566-2022 国家标准 架构的术语描述&#xff1a;(系统)在其环境中的一些基本概念或性质,体现在其元素关系,以及设计与演进原则中。 &#xff08;2&#xff09;ISO/IEC/IEEE 42010 国际标准 架构的…...

华为鸿蒙Core Vision Kit 骨骼检测技术

鸿蒙Core Vision Kit 是华为鸿蒙系统中的一个图像处理框架&#xff0c;旨在提供各种计算机视觉功能&#xff0c;包括物体检测、人脸识别、文本识别等。骨骼检测是其中的一项功能&#xff0c;主要用于检测和识别人类身体的骨骼结构。 骨骼检测的关键点 骨骼点检测&#xff1a;通…...

Table API SQL系统(内置)函数System (Built-in) Function详解

目录 函数类型 引用函数 函数精确引用 函数模糊引用 函数解析顺序 精确的函数引用 模糊的函数引用 系统函数 标量函数(Scalar Functions) 比较函数(Comparison Functions) 逻辑函数(Logical Functions) 算术函数(Arithmetic Functions) 字符串函数(Strin…...

一键运行RocketMQ5.3和Dashboard

一键运行RocketMQ5.3和Dashboard 目录 一键运行RocketMQ5.3和Dashboard通过Docker Compose 来一键启动运行的容器包括docker-compose.yml文件运行命令启动本地效果查看 参考信息 通过Docker Compose 来一键启动 运行的容器包括 NameServerBrokerProxyDashBoard docker-compo…...

HAL STM32 SG90舵机驱动控制

HAL STM32 SG90舵机驱动控制 &#x1f516;测试对象&#xff1a;STM32F103SG90舵机 &#x1f33c;功能实现&#xff1a;通过串口指令&#xff0c;控制SG90舵机转动到指定角度。 ✨在实际硬件舵机驱动过程中&#xff0c;使用SG90普通舵机空载运转情况下&#xff0c;电流在180mA…...

【Kubernetes】k8s集群图形化管理工具之rancher

目录 一.Rancher概述 1.Rancher简介 2.Rancher与k8s的关系及区别 3.Rancher具有的优势 二.Rancher的安装部署 1.实验准备 2.安装 rancher 3.rancher的浏览器使用 一.Rancher概述 1.Rancher简介 Rancher 是一个开源的企业级多集群 Kubernetes 管理平台&#xff0c;实…...

AI编程系列一1小时完成链家房价爬虫程序

背景 AI编程实在太火&#xff0c;写了很多年的Java&#xff0c;现在Python 和Go 简单好用&#xff0c;今天结合智谱清言快速完成一个程序爬虫程序&#xff0c;没有任何Python 编程经验&#xff0c;只需要会提问&#xff0c;熟悉简单HTML结构即可。未来一定是有业务能力者的福…...

【JavaEE初阶】文件内容的读写—数据流

目录 &#x1f4d5; 引言 &#x1f334; 数据流的概念 &#x1f6a9; 数据流分类 &#x1f333; 字节流的读写 &#x1f6a9; InputStream&#xff08;从文件中读取字节内容) &#x1f6a9; OutputStream&#xff08;向文件中写内容&#xff09; &#x1f384; 字符流的…...

Spring Boot项目中使用Sharding-JDBC实现读写分离

Sharding-JDBC是一个分布式数据库中间件&#xff0c;它不仅支持数据分片&#xff0c;还可以轻松实现数据库的读写分离。下面是如何在Spring Boot项目中集成Sharding-JDBC并实现读写分离的详细步骤&#xff1a; 目录 1. 引入依赖 2. 配置数据源 3. 配置Sharding-JDBC相关参数…...

【网络安全】SSO登录过程实现账户接管

未经许可,不得转载。 文章目录 正文正文 登录页面展示了“使用 SSO 登录”功能: 经分析,单点登录(SSO)系统的身份验证过程如下: 1、启动SSO流程:当用户点击按钮时,浏览器会发送一个GET请求到指定的URL: /idp/auth/mid-oidc?req=[UNIQUE_ID]&redirect_uri=[REDI…...

Admin.NET源码学习(3:LazyCaptcha使用浅析)

Admin.NET项目前端登录页面的验证码图片默认使用动态图&#xff0c;且图形内容为阿拉伯数字运算&#xff08;如下图所示&#xff09;&#xff0c;用户输入正确的计算结果才能正常登录。项目采用LazyCaptcha模块生成验证码及动态图。   在Admin.NET.Core项目中添加了Lazy.Cap…...

在原生未启用kdump的BCLinux 8系列服务器上启用kdump及报错处理

本文记录了在原生未启用kdump的BCLinux 8系列操作系统的服务器上手动启用kdump服务及报错处理的过程。 一、问题描述 BCLinux 8系列操作系统&#xff0c;系统初始化安装时未启用kdump服务&#xff0c;手动启动时报以下“No memory reserved for crash kernel”或“ConditionK…...

Android架构组件中的MVVM

Android架构组件中的MVVM&#xff08;Model-View-ViewModel&#xff09;模式是一种广泛应用的设计模式&#xff0c;它通过将应用程序分为三个主要部分&#xff08;Model、View、ViewModel&#xff09;来分离用户界面和业务逻辑&#xff0c;从而提高代码的可维护性、可扩展性和可…...

走向绿色:能源新选择,未来更美好

当前&#xff0c;全球范围内可再生能源正经历着从辅助能源向核心能源的深刻转型&#xff0c;绿色能源日益渗透至居住、出行、日常应用等多个领域&#xff0c;深刻影响着我们的生活方式&#xff0c;使我们能够更加充分地体验清洁能源所带来的优质生活。 一、绿色能源与“住” …...

鸿蒙装饰器的介绍

State装饰器&#xff0c; State装饰的变量&#xff0c;称为状态变量&#xff0c;与声明式范式中的其他被装饰变量一样&#xff0c;是私有的&#xff0c;只能从组件内部访问&#xff0c;在声明时&#xff0c;必须指定其类型和本地初始化。 Provide装饰器和Consume装饰器&#…...

零基础5分钟上手亚马逊云科技核心云架构知识 - 权限管理最佳实践

简介&#xff1a; 欢迎来到小李哥全新亚马逊云科技AWS云计算知识学习系列&#xff0c;适用于任何无云计算或者亚马逊云科技技术背景的开发者&#xff0c;通过这篇文章大家零基础5分钟就能完全学会亚马逊云科技一个经典的服务开发架构方案。 我会每天介绍一个基于亚马逊云科技…...

[数据库][知识]SQL Server、MySQL 和 Oracle 的默认端口和数据库链接

SQL Server、MySQL 和 Oracle 的默认端口号、连接 URL 和驱动类名。以下是对每个数据库连接信息的简要说明&#xff1a; SQL Server 默认端口号&#xff1a;1433JDBC URL 格式&#xff1a;jdbc:sqlserver://localhost:1433;DatabaseNamedbnameJDBC 驱动类名&#xff1a;com.mic…...

【Unity教程】使用 Animation Rigging实现IK制作程序化的动画

在 Unity 开发中&#xff0c;为角色创建逼真且自适应的动画是提升游戏体验的关键。在本教程中&#xff0c;我们将结合 Animation Rigging 工具和 IK&#xff08;Inverse Kinematics&#xff0c;反向运动学&#xff09;插件来实现程序化的动画。 视频教程可以参考b战大佬的视频 …...

OBS混音器(Mixers)的重要性和配置指南

在进行直播或录制时,音频管理是非常关键的一环,特别是在需要同时处理多个音频源的复杂设置中。OBS Studio提供了强大的音频管理工具,其中“混音器”功能扮演了核心角色。混音器(Mixers)在OBS中用于控制不同音频源的输出路由,允许用户精确控制哪些音源出现在最终的直播或录…...

Ubuntu安装Anaconda3

本文详细阐述了在 Ubuntu 系统中安装 Anaconda3 的完整流程。包括 Anaconda3 安装包的获取途径&#xff0c;具体安装过程中的每一个步骤及注意事项&#xff0c;还有安装后的环境变量设置和安装成功的验证方法。旨在为 Ubuntu 用户提供清晰、易懂且准确的 Anaconda3 安装指南&am…...

数据类型解码:INT、VARCHAR、DATETIME的深度解析与实践

标题&#xff1a;数据类型解码&#xff1a;INT、VARCHAR、DATETIME的深度解析与实践 在软件开发和数据库设计中&#xff0c;数据类型是构建数据模型的基础。准确理解和使用数据类型&#xff0c;如INT、VARCHAR、DATETIME&#xff0c;对于确保数据的完整性、性能和安全性至关重…...

基于单片机的智能晾衣系统设计

摘 要 &#xff1a;在网络信息技术的推动下&#xff0c;智能家居得到了广泛应用&#xff0c;文章根据当前的市场动态&#xff0c;针对基于单片机的智能晾衣系统设计展开论述&#xff0c;具体包括两个方面的内容———硬件设计和软件设计。 关键词 &#xff1a;单片机&#xff…...

Python实战项目:天气数据爬取+数据可视化(完整代码)

一、选题的背景 随着人们对天气的关注逐渐增加&#xff0c;天气预报数据的获取与可视化成为了当今的热门话题&#xff0c;天气预报我们每天都会关注&#xff0c;天气情况会影响到我们日常的增减衣物、出行安排等。每天的气温、相对湿度、降水量以及风向风速是关注的焦点。通过…...

知识改变命运 数据结构【链表面试题】

1. 删除链表中等于给定值 val 的所有节点。 OJ链接 public ListNode removeElements(ListNode head, int val) {if (headnull) {return null;}ListNode curhead.next;ListNode prehead;while(cur!null) {if(cur.valval) {pre.nextcur.next;curcur.next;}else {precur;curcur.ne…...

计算机毕业设计 医院问诊系统 Java+SpringBoot+Vue 前后端分离 文档报告 代码讲解 安装调试

&#x1f34a;作者&#xff1a;计算机编程-吉哥 &#x1f34a;简介&#xff1a;专业从事JavaWeb程序开发&#xff0c;微信小程序开发&#xff0c;定制化项目、 源码、代码讲解、文档撰写、ppt制作。做自己喜欢的事&#xff0c;生活就是快乐的。 &#x1f34a;心愿&#xff1a;点…...

掌握CSS的:any-link伪类:统一链接样式的高效方法

在网页设计中&#xff0c;链接是用户导航和交互的重要组成部分。CSS提供了多种伪类选择器来定义链接的不同状态&#xff0c;例如:link用于选择未访问的链接&#xff0c;:visited用于选择已访问的链接。然而&#xff0c;有时候我们需要同时为所有状态的链接设置统一的样式&#…...

虚幻5|角色武器装备的数据库学习(不只是用来装备武器,甚至是角色切换也很可能用到)

虚幻5|在连招基础上&#xff0c;给角色添加武器并添加刀光|在攻击的时候添加武器并返回背后&#xff08;第一部分&#xff0c;下一部分讲刀光&#xff09;_unreal 如何给角色添加攻击-CSDN博客 目的&#xff1a;捡起各种不同的武器&#xff0c;捡起的武器跟装备的武器相匹配 …...

防火墙技术与地址转换

文章目录 前言一、四种区域二、实验拓扑图基础配置防火墙配置测试结果 前言 防火墙是计算机网络中的一种安全设备或软件功能&#xff0c;旨在监控和控制进出网络的网络流量。其核心目的是保护内部网络免受外部攻击或不必要的访问。防火墙通过设定一系列安全规则&#xff0c;允…...

C++11中的Lambda表达式

文章目录 C11中的Lambda表达式1.lambda表达式形式2.向lambda传递参数3.使用捕获列表4.lambda捕获和返回1.值捕获2.引用捕获3.隐式捕获4.可变lambda5.指定lambda的返回类型 C11中的Lambda表达式 1.lambda表达式形式 lambda表达式具有以下形式 [capture list] (parameter list)…...

Unity图形系统

Unity的图形系统是一个复杂且功能强大的模块&#xff0c;它支持多种渲染技术和API&#xff0c;能够满足从移动设备到高端游戏机和桌面平台的各种需求。以下是关于Unity图形系统的详细解析&#xff1a; 渲染流程与技术 Unity的渲染流程可以分为应用程序阶段&#xff08;CPU&…...

Ceph篇之利用shell脚本实现批量创建bucket桶

Ceph创建bucket桶 在 Ceph 中创建桶&#xff08;bucket&#xff09;需要使用 Ceph 对象网关&#xff08;RGW&#xff09;。 注&#xff1a;如果查看shell批量创建脚本请直接参见目录3 1. 利用radosgw-admin工具创建桶 确保 Ceph 集群和对象网关已正确配置 确保你的 Ceph 集群…...

周末总结(2024/08/17)

工作 人际关系核心实践&#xff1a; 要学会随时回应别人的善意&#xff0c;执行时间控制在5分钟以内 坚持每天早会打招呼 遇到接不住的话题时拉低自己&#xff0c;抬高别人(无阴阳气息) 朋友圈点赞控制在5min以内&#xff0c;职场社交不要放在5min以内 职场的人际关系在面对利…...

SQL高级编程:掌握自定义函数和过程的艺术

标题&#xff1a;SQL高级编程&#xff1a;掌握自定义函数和过程的艺术 在SQL的世界里&#xff0c;数据操作不仅仅局限于简单的查询和更新。通过自定义函数&#xff08;User-Defined Functions, UDFs&#xff09;和存储过程&#xff08;Stored Procedures&#xff09;&#xff…...

python监听环境内是否有声音

python监听环境内是否有声音 首先使用pyaudio打开麦克风&#xff0c;并开始录音。然后使用一个while循环来不断读取麦克风录取的音频数据&#xff0c;然后使用numpy来分析音频数据是否有声音。当检测到有声音时&#xff0c;会打印"有声音"并退出循环。最后关闭录音流…...

合并两个有序链表--力扣

题目如下: 将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。 示例如下: 示例 1&#xff1a; 输入&#xff1a;l1 [1,2,4], l2 [1,3,4] 输出&#xff1a;[1,1,2,3,4,4]示例 2&#xff1a; 输入&#xff1a;l1 [], l2 …...

【自用】Python爬虫学习(三):图片下载、使用代理、防盗链视频下载、多线程与多进程

Python爬虫学习&#xff08;三&#xff09; 使用BeautifulSoup解析网页并下载图片模拟用户登录处理使用代理视频下载&#xff0c;防盗链的处理多线程与多进程 使用BeautifulSoup解析网页并下载图片 目的&#xff1a;对某网站的某个专栏页面的图片进行下载得到高清图。 思路&am…...

#Datawhale AI夏令营第4期#AIGC方向Task3

在之前的任务中&#xff0c;我们已经对baseline进行了精读&#xff0c;并生成了&#xff0c;我们自己的八图故事。 在Task3中&#xff0c;我们的主要任务有两个&#xff1a;part1&#xff1a;工具初探一ComfyUI应用场景探索&#xff1b;Part2&#xff1a;Lora微调。 微调是一…...

【docker综合篇】关于我用docker搭建了6个应用服务的事

最近一直在捣鼓docker&#xff0c;利用测试服务器&#xff0c;本着犯错就重来(重装系统)的大无畏精神&#xff0c;不断尝试&#xff0c;总结经验&#xff0c;然后在网上搜寻一些关于docker有关的服务镜像&#xff0c;并搭建起来。看着一个个服务在我的服务器跑起来&#xff0c;…...