-
前言:在 Java 中,输入输出(I/O)是常见的操作,字节流和字符流是处理文件和数据的核心类,本文将介绍 InputStream、OutputStream、Reader 和 Writer 类的基本用法。这里是秋刀鱼不做梦的BLOG想要了解更多内容可以访问我的主页秋刀鱼不做梦-CSDN博客在正式开始讲解之前,先让我们看一下本文大致的讲解内容:目录1.File 类 (1)构造方法 【1】File(String pathname):最简单的方式,直接传入文件的路径 【2】File(String parent, String child):我们也可以先给出父目录,然后再给出子文件或子目录的名称 【3】File(File parent, String child):首先先创建一个 File 对象表示父目录,然后用它来构建子文件或子目录 (2)File中的方法【1】获取文件信息类【2】判断文件状态类【3】文件与目录操作【4】列出目录内容2.数据流类 (1)InputStream 类 (2)OutputStream 类 (3)Reader 类 (4)Writer 类1.File 类 首先先让我们了解一下Java中的File 类,在 Java 中,File 类是我们用来操作文件和目录的工具,虽然它并不能直接读取或写入文件的内容,但它提供了很多方法,让我们能够管理文件系统,比如检查文件是否存在、获取文件信息、创建文件和目录、删除文件等等,简而言之,它是与文件打交道时的一个基础类。 (1)构造方法 初步了解了File类是个什么东西之后,那么我们在Java中如何去创建File类呢?常见的创建方式有如下三种: 【1】File(String pathname):最简单的方式,直接传入文件的路径File file = new File("path/to/file.txt");AI生成项目java运行 【2】File(String parent, String child):我们也可以先给出父目录,然后再给出子文件或子目录的名称File file = new File("path/to", "file.txt");AI生成项目java运行 【3】File(File parent, String child):首先先创建一个 File 对象表示父目录,然后用它来构建子文件或子目录File parentDir = new File("path/to");File file = new File(parentDir, "file.txt");AI生成项目java运行 通过上述方法,我们就可以方便地根据路径创建 File 对象,之后就可以对这些文件或目录进行各种操作了!!! (2)File中的方法 File类中提供了很多方法,能帮助我们做各种文件和目录的操作,比如获取文件信息、检查文件状态、创建和删除文件等等,我们来看看其中一些最常用的方法:【1】获取文件信息方法 getName():返回文件的名称,不包括路径。File file = new File("path/to/file.txt");System.out.println(file.getName()); // 输出 file.txtAI生成项目java运行 getAbsolutePath():返回文件的绝对路径,这个路径不管你在哪个目录下都能访问到文件。System.out.println(file.getAbsolutePath());AI生成项目java运行 getPath():返回文件的路径,可能是相对路径,也可能是绝对路径,取决于创建 File 对象时传入的是什么路径System.out.println(file.getPath());AI生成项目java运行 getParent():返回文件的父目录,如果文件在根目录或者没有父目录,这个方法会返回null。System.out.println(file.getParent());AI生成项目【2】判断文件状态方法 exists():检查文件或目录是否存在,如果存在,返回 true,否则返回 false。if (file.exists()) { System.out.println("文件存在");} else { System.out.println("文件不存在");}AI生成项目java运行 isFile():判断它是否是一个文件,如果是文件,返回 true,如果是目录,返回 false。if (file.isFile()) { System.out.println("是文件");}AI生成项目java运行 isDirectory():判断它是否是一个目录,如果是目录,返回 true,如果是文件,返回 false。if (file.isDirectory()) { System.out.println("是目录");}AI生成项目java运行【3】文件与目录操作方法 createNewFile():用来创建一个新文件,如果文件已经存在,这个方法会返回 false。try { if (file.createNewFile()) { System.out.println("文件创建成功"); } else { System.out.println("文件已存在"); }} catch (IOException e) { e.printStackTrace();}AI生成项目java运行 mkdir():创建一个目录。如果目录已存在,返回 false,如果成功创建,返回 true。File dir = new File("path/to/directory");if (dir.mkdir()) { System.out.println("目录创建成功");}AI生成项目java运行【4】列出目录内容方法 listFiles():返回一个 File 数组,包含目录下所有的文件和子目录,如果这个 File 对象代表的不是目录,返回 null。File dir = new File("path/to/directory");File[] files = dir.listFiles();if (files != null) { for (File f : files) { System.out.println(f.getName()); }}AI生成项目java运行 这样我们就大致的了解了File中的常用方法了!!!2.数据流类 在 Java 中,处理文件和数据流的输入输出是非常常见的操作,为了让这些操作更加高效,Java 提供了字节流和字符流的不同方式,这些类分为 InputStream 和 OutputStream,以及 Reader 和 Writer,接下来让我们一一讲解一下: (1)InputStream 类 InputStream 是所有字节输入流的超类,它负责从外部读取字节数据,你可以把它想象成一个“读取器”,它帮助你从磁盘文件、网络连接或者内存等地方读取数据。常用方法: ——read():从输入流中读取一个字节并返回它,如果流的末尾已经到达,则返回 -1InputStream inputStream = new FileInputStream("file.txt");int byteData = inputStream.read();while (byteData != -1) { System.out.print((char) byteData); // 转换为字符并输出 byteData = inputStream.read(); // 继续读取下一个字节}inputStream.close(); // 别忘了关闭流AI生成项目java运行 代码解释:这个例子逐个字节读取文件内容,直到到达文件末尾。我们将每个字节转换为字符并打印出来 ——read(byte[] b):一次性读取多个字节到字节数组 b 中,返回实际读取的字节数,如果已经到达流的末尾,它返回 -1。byte[] buffer = new byte[1024];int bytesRead = inputStream.read(buffer);System.out.println("读取了 " + bytesRead + " 个字节");inputStream.close();AI生成项目java运行 代码解释:这个例子中我们使用read方法将数据读到了buffer这个数组中,并返回了读取到的字节数。 ——read(byte[] b, int off, int len):从字节数组 b 的 off 偏移量开始,最多读取 len 个字节,返回实际读取的字节数。byte[] buffer = new byte[1024];int bytesRead = inputStream.read(buffer, 0, 100); // 从数组开头读取 100 字节inputStream.close();AI生成项目java运行 代码解释:和read(byte[] b)方法类似,只不过我们只读取了0到100字节而已至此,我们就了解了InputStream类的常用方法了!!! (2)OutputStream 类 与 InputStream 类相对应,OutputStream 负责将数据写入输出流,它可以用于文件、网络或内存等目标的写入。常用方法: ——write(int b):将一个字节的数据写入输出流,需要注意,write() 方法接受的是一个 int 类型的参数,它会自动转换为字节。OutputStream outputStream = new FileOutputStream("output.txt");outputStream.write(65); // 写入字节 'A'(ASCII 码为 65)outputStream.close();AI生成项目java运行 代码解释:这个例子将字节 65 写入文件。 ——write(byte[] b):将字节数组中的数据写入输出流。byte[] data = "Hello".getBytes();outputStream.write(data);outputStream.close();AI生成项目java运行 代码解释:我们将字符串 "Hello" 转换成字节数组,然后写入文件。 ——write(byte[] b, int off, int len):从字节数组 b 的 off 偏移量开始,最多写入 len 个字节。byte[] data = "HelloWorld".getBytes();outputStream.write(data, 0, 5); // 写入 "Hello"outputStream.close();AI生成项目java运行 代码解释:这段代码将 "HelloWorld" 字符串中的前 5 个字节写入文件 ——flush():在某些情况下,写入的数据会被暂时保存在缓冲区中,直到缓冲区满了才会被写入,如果你需要强制将缓冲区中的数据立即写入目标,可以使用 flush()。outputStream.flush();AI生成项目java运行以上就是OutputStream 类的常见方法了!!! (3)Reader 类 如果你要处理文本文件中的字符数据,Reader 类是一个非常方便的选择,它是所有字符输入流的超类,专门用来处理字符而不是字节。常用方法: ——read():读取一个字符并返回它的 Unicode 值。如果已到文件末尾,返回 -1。Reader reader = new FileReader("file.txt");int charData = reader.read();while (charData != -1) { System.out.print((char) charData); // 转换为字符并输出 charData = reader.read(); // 继续读取下一个字符}reader.close();AI生成项目java运行 代码解释:这个例子将逐字符读取文件内容,直到遇到文件的末尾。 ——read(char[] cbuf):一次性读取多个字符并将它们存入字符数组 cbuf 中,返回实际读取的字符数。char[] buffer = new char[1024];int charsRead = reader.read(buffer);System.out.println("读取了 " + charsRead + " 个字符");reader.close();AI生成项目java运行 代码解释:我们使用一个char数组来接收读取到的数据,并且read方法返回了读取到的个数 ——read(char[] cbuf, int off, int len):从字符数组 cbuf 的 off 偏移量开始,最多读取 len 个字符。char[] buffer = new char[1024];int charsRead = reader.read(buffer, 0, 100); // 从数组开头读取 100 个字符reader.close();AI生成项目java运行 代码解释:我们使用了char数组来接收读取的数据,只读取了前100个字节至此,我们就了解了Reader类的常用方法了!!! (4)Writer 类 与 Reader 类相对应,Writer 类用于将字符数据写入目标输出流,它专门用于处理字符数据,避免了字节流处理文本时的编码问题。常用方法: ——write(int c):将一个字符的 Unicode 值写入到输出流。Writer writer = new FileWriter("output.txt");writer.write(65); // 写入字符 'A'writer.close();AI生成项目java运行 代码解释:这段代码将字符 'A' 写入到文件中。 ——write(char[] cbuf):将字符数组中的数据一次性写入输出流。char[] data = "Hello".toCharArray();writer.write(data);writer.close();AI生成项目java运行 代码解释:这里我们将字符串 "Hello" 转换为字符数组,然后写入文件 ——write(char[] cbuf, int off, int len):从字符数组 cbuf 的 off 偏移量开始,最多写入 len 个字符。char[] data = "HelloWorld".toCharArray();writer.write(data, 0, 5); // 写入 "Hello"writer.close();AI生成项目java运行 代码解释: 这段代码将 "HelloWorld" 字符串中的前 5 个字符写入文件 ——flush():与字节流类似,Writer 类也有 flush() 方法,可以将缓冲区中的数据强制写入文件。writer.flush();AI生成项目java运行以上就是Writer类的常见方法了————————————————原文链接:https://blog.csdn.net/2302_80198073/article/details/144897687
-
简介:解析WSDL文件是开发基于Web服务的应用程序中的关键任务。本文将详细介绍如何使用Java以及相关库如Apache CXF和Axis2来解析WSDL文档,并实现SOAP请求与响应处理。文章首先会指导如何引入必要的库,然后深入解析WSDL文件获取服务接口、操作和消息结构。接着,通过示例代码展示如何创建服务代理,发送SOAP请求,并接收与解析SOAP响应。最后,本文也会解释如何处理主流Web服务框架生成的WSDL,并提供详细的示例代码。掌握这些知识能够帮助开发者在Java环境中构建健壮的Web服务客户端。1. WSDL文件解析的必要性WSDL(Web Services Description Language)文件是Web服务的标准描述语言,它定义了服务接口和绑定方式,使得开发者能够理解和使用远程服务。随着SOA(Service Oriented Architecture)的普及,WSDL文件的解析成为了软件开发中不可或缺的一环。1.1 WSDL文件的作用WSDL文件在Web服务交互中扮演着至关重要的角色。它不仅定义了服务的网络地址(即endpoint),还详细描述了服务可以执行的操作,包括每个操作的输入输出消息格式。服务提供者通过WSDL文件对外公开其服务的接口,服务消费者则通过解析WSDL文件来了解如何与服务进行交互,这样双方就能够基于共同的理解进行通信。1.2 解析WSDL文件的必要性解析WSDL文件的必要性体现在以下几个方面:接口透明性 :通过解析WSDL文件,开发者无需访问服务的源代码,即可了解服务的功能和如何调用,这大大提高了系统的封装性和模块化。服务的自动化 :开发工具可以自动读取WSDL文件,生成客户端代理代码,从而加快开发进程,并减少因手动编码导致的错误。跨平台交互 :WSDL基于XML,它是一种可跨平台、跨语言的描述语言,有助于实现不同系统之间的互操作性。灵活性和可扩展性 :WSDL文件可以描述复杂的网络服务协议和结构,使得服务可以灵活地添加新的功能而不影响现有的客户端实现。总之,WSDL文件是Web服务沟通的桥梁,而解析WSDL文件是理解和实现这一沟通的基础。接下来的章节将详细介绍如何使用Apache CXF和Axis2等工具来解析WSDL文件,并进一步探讨WSDL的结构及其在实际开发中的应用。2. 使用Apache CXF解析WSDLApache CXF是一个功能强大的开源服务框架,它提供了全面的Web服务支持,包括对WSDL的解析。在本章节中,我们将深入了解Apache CXF框架如何帮助我们解析WSDL文件,并在实践中应用它。我们不仅会覆盖基本的使用方法,还会讨论在解析过程中可能遇到的挑战,并提供解决方案。2.1 Apache CXF概述2.1.1 CXF框架的架构和特点Apache CXF(从前称为 Celtix 和 XFire)是一个全功能的开源服务框架,它使得Web服务的创建和消费变得简单。CXF的核心特点包括其服务总线架构,支持多种传输协议和数据格式,以及对多种行业标准的支持,特别是SOAP和WSDL。CXF的关键组件包括:Frontend API :用于配置和启动Web服务。Backend API :用于处理服务调用的底层细节。DataBinding :处理XML和Java对象之间的映射。JBI Container :提供对Java Business Integration的支持。CXF的架构被设计为模块化和可扩展,允许开发者根据需要轻松地添加或替换组件。2.1.2 CXF与WSDL的关系及其作用WSDL文件对于Web服务的描述至关重要,Apache CXF提供了读取和解析WSDL文件的能力。CXF利用WSDL文件来识别服务操作、消息格式、传输协议等关键信息,进而在客户端和服务端之间建立通信机制。CXF支持WSDL的最新版本,它能够:生成WSDL文件,基于Java接口和服务实现。读取WSDL文件,用于在不直接访问代码的情况下了解服务的结构和功能。提供WSDL的动态解析功能,用于在运行时动态创建代理和客户端。2.2 CXF解析WSDL的实践2.2.1 解析WSDL的代码实现接下来,让我们通过一个实际的代码例子来展示如何使用Apache CXF来解析一个WSDL文件。假设我们有一个名为 example.wsdl 的WSDL文件,我们希望了解如何获取这个WSDL文件中的服务定义和操作。首先,你需要在你的项目中包含Apache CXF的依赖库。如果你使用Maven,可以添加以下依赖到你的 pom.xml 文件中:<dependency> <groupId>org.apache.cxf</groupId> <artifactId>cxf-rt-frontend-jaxws</artifactId> <version>3.4.4</version></dependency><dependency> <groupId>org.apache.cxf</groupId> <artifactId>cxf-rt-transports-http</artifactId> <version>3.4.4</version></dependency>AI生成项目xml然后,你可以使用以下Java代码来解析WSDL文件:import org.apache.cxf.helpers.IOUtils;import org.apache.cxf.wsdl.WSDLManager;import org.apache.cxf.wsdl.model.WSDLModel;import org.apache.cxf.wsdl.model.WSDLModelFactory;import org.apache.cxf.wsdl.model.WSDLPart; import java.io.StringReader;import java.util.Map; public class WSDLParserExample { public static void main(String[] args) { try { // 加载WSDL文件 String wsdlContent = IOUtils.toString( WSDLParserExample.class.getResourceAsStream("/example.wsdl"), "UTF-8"); WSDLModelFactory factory = WSDLManager.getInstance().getModelFactory(); WSDLModel model = factory.newWSDLModel(new StringReader(wsdlContent)); // 获取服务和端口类型 Map<String, Object> services = model.getServiceMap(); System.out.println("Service(s):"); services.keySet().forEach(System.out::println); Map<String, Object> portTypes = model.getPortTypeMap(); System.out.println("\nPortType(s):"); portTypes.keySet().forEach(System.out::println); // 打印所有消息 model.getMessagingMap().keySet().forEach(System.out::println); } catch (Exception e) { e.printStackTrace(); } }}AI生成项目java运行2.2.2 分析WSDL结构和生成的服务接口在上述代码中,我们首先通过 IOUtils.toString() 方法读取了WSDL文件的内容,并创建了一个 WSDLModel 实例。这个实例代表了整个WSDL文档的结构和内容。通过调用 model.getServiceMap() 和 model.getPortTypeMap() ,我们可以检索到定义在WSDL中的所有服务和端口类型。此外, model.getMessagingMap().keySet() 帮助我们获取了WSDL中定义的所有消息。解析WSDL文件后,我们可以进一步生成服务接口。Apache CXF提供了一个代码生成工具,可以基于WSDL文件自动生成Java接口和实现类。这允许开发者快速开始使用Web服务,而无需手动编写大量的绑定和代理代码。解析和代码生成之后,下一步是创建服务代理类实例,用于调用远程Web服务。这个过程将在下一章详细介绍。3. 使用Axis2解析WSDLAxis2是一个功能强大且灵活的Web服务平台,用于创建、部署和管理Web服务。与旧版本的Axis相比,Axis2对WSDL解析和Web服务的实现进行了优化,提高了性能和可用性。深入了解Axis2框架如何解析WSDL文件,并通过API与WSDL元素进行交互,将有助于开发者更好地利用这一工具。3.1 Axis2框架解析WSDL机制3.1.1 Axis2的架构和工作原理Axis2架构建立在模块化的设计理念之上,可以灵活地添加或移除组件来扩展功能。其核心组件包括:Axis2 Engine :处理消息的接收与发送,并协调整个请求处理流程。ServiceRepository :存储Web服务的信息,包括WSDL文件解析结果。Transport Sender/Receiver :负责与底层传输层通信,如HTTP、SMTP等。在解析WSDL文件时,Axis2首先加载WSDL文档,并将其内容解析为内部数据结构。这些数据结构随后可用于生成服务端代码或客户端代理。3.1.2 Axis2对WSDL文件的处理流程Axis2处理WSDL文件的过程可以分为以下几个步骤:WSDL文件加载 :Axis2读取WSDL文件内容。WSDL解析 :利用其提供的解析器将WSDL文档解析为DOM结构。服务和绑定生成 :从解析的DOM结构中提取服务描述,包括端点、消息和绑定。服务类生成 :Axis2可使用内置的代码生成器根据WSDL描述生成服务类的Java代码。3.2 Axis2解析WSDL的实战演练3.2.1 配置和初始化Axis2环境在开始解析WSDL之前,需要先配置好Axis2环境。这通常包括添加Axis2的核心库到项目的构建路径中,配置XML解析器和相关的依赖库。<!-- pom.xml --><dependency> <groupId>org.apache.axis2</groupId> <artifactId>axis2</artifactId> <version>1.8.2</version></dependency>AI生成项目xml接下来,初始化一个Axis2服务的 Service 对象:import org.apache.axis2.context.ServiceContext;import org.apache.axis2.service.Service; // ... ServiceContext serviceContext = ServiceContext.createContext();Service service = new Service(serviceContext);AI生成项目java运行3.2.2 Axis2解析WSDL实例代码解析假设我们有一个WSDL文件 MyService.wsdl ,以下是如何使用Axis2来解析这个WSDL文件并获取其服务和绑定信息的示例:import org.apache.axis2.description.AxisService;import org.apache.axis2.description.AxisEndpoint;import org.apache.axis2.description.WSDL2JavaAxisOperation;import org.apache.axis2.description.WSDL2JavaAxisMessage;import org.apache.axis2.transport.http.HTTPConstants;import org.apache.axis2.util.MatrixUtil;import org.apache.axiom.om.OMElement;import org.apache.axiom.om.OMAbstractDocument;import org.apache.axiom.om.util.AXIOMUtil;import org.apache.axiom.soap.SOAPFactory;import org.apache.axiom.soap.SOAPFactoryImpl;import org.apache.axiom.soap.SOAPFault;import org.apache.axiom.soap.SOAPHeader;import org.apache.axiom.soap.SOAPEnvelope;import org.apache.axiom.soap.SOAPBody;import org.w3c.dom.Document; // 加载WSDL文档Document wsdlDoc = AXIOMUtil.stringToOM("wsdl_file_path").getDocument(); // 创建WSDL2JavaAxisOperation对象WSDL2JavaAxisOperation operation = new WSDL2JavaAxisOperation();operation.setWSDLDocument((OMAbstractDocument) wsdlDoc);operation.setServiceName("MyService");operation.setPortName("MyPort");operation.setOperationName("myOperation");operation.setAxis2ServiceDescription((AxisService) service.getDescription()); // 获取操作的输入消息OMElement inputMessage = operation.getInputMessage();// 获取操作的输出消息OMElement outputMessage = operation.getOutputMessage(); // 解析输入消息WSDL2JavaAxisMessage inputMessageParser = new WSDL2JavaAxisMessage(inputMessage, operation);inputMessageParser.setAxis2ServiceDescription((AxisService) service.getDescription());inputMessageParser.setAxis2OperationDescription((WSDL2JavaAxisOperation) operation);inputMessageParser.parse(); // 解析输出消息WSDL2JavaAxisMessage outputMessageParser = new WSDL2JavaAxisMessage(outputMessage, operation);outputMessageParser.setAxis2ServiceDescription((AxisService) service.getDescription());outputMessageParser.setAxis2OperationDescription((WSDL2JavaAxisOperation) operation);outputMessageParser.parse(); // 获取服务端点AxisEndpoint endpoint = service.getAxis2Endpoint(operation); // 设置消息格式SOAPFactory factory = new SOAPFactoryImpl();SOAPEnvelope env = factory.getDefaultEnvelope();SOAPHeader header = env.getHeader();SOAPBody body = env.getBody();AI生成项目java运行在上述代码中,我们首先通过 AXIOMUtil.stringToOM 方法将WSDL文件内容转换为AXIOM的 OMDocument 对象。之后,我们创建了一个 WSDL2JavaAxisOperation 对象来解析WSDL文档中特定操作的详细信息。通过调用 parse 方法,我们可以获取到输入和输出消息的相关信息,并将其解析为可用的Java对象。最后,通过 AxisEndpoint 对象,我们可以得到对应操作的端点信息,为后续的服务调用打下基础。需要注意的是,解析WSDL文件只是与Web服务交互的起点。在实际应用中,还需要将解析得到的端点信息和消息格式应用到服务调用中,执行实际的服务请求和响应处理。4. WSDL文件结构解析4.1 WSDL文件结构概览4.1.1 WSDL的根元素和定义域WSDL(Web Services Description Language)文件是一种基于XML的语言,用于描述网络服务。它是Web服务通信协议的基础,用于定义如何与特定的服务进行交互。WSDL文件的根元素是 <definitions> ,它通常包含命名空间声明、消息定义、端口类型、绑定和服务描述等关键部分。<definitions name="HelloWorldService" targetNamespace="http://example.com/wsdl/" xmlns="http://schemas.xmlsoap.org/wsdl/" xmlns:ns="http://example.com/wsdl/" xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"> <!-- 其他定义 --></definitions>AI生成项目xml在上面的示例中, <definitions> 元素定义了服务的名称 name ,目标命名空间 targetNamespace ,以及必须的命名空间 xmlns 。命名空间将用于区分WSDL文件中的不同元素和类型定义。4.1.2 WSDL中的服务接口和服务实现WSDL文件描述了Web服务的两个主要方面:服务接口和其服务实现。服务接口定义了客户如何与服务进行通信,包括所支持的操作以及消息的格式。服务实现则详细说明了接口的具体部署位置,通常是通过绑定到网络地址(URL)来实现。服务接口部分通常包含如下内容:<message> :定义了操作的输入和输出消息。<portType> :将操作组合成接口,并定义了服务可以执行的操作。<binding> :描述了如何将 portType 绑定到具体的通信协议。<service> :为 portType 的集合提供了一个网络可寻址的端点。服务实现则通过 <port> 元素,结合 <binding> 和一个网络地址,指明了具体的服务地址。4.2 WSDL核心元素详解4.2.1 <portType> 的作用和属性<portType> 是WSDL文件的核心元素之一,它定义了Web服务可以执行的操作集,每个操作代表一个服务可以执行的方法。<portType name="HelloWorldPortType"> <operation name="sayHello"> <input message="ns:sayHelloRequest"/> <output message="ns:sayHelloResponse"/> </operation></portType>AI生成项目xml在上述代码中, <portType> 定义了一个名为 HelloWorldPortType 的端口类型,其中包含一个名为 sayHello 的操作。操作 sayHello 通过 <input> 和 <output> 标签定义了输入和输出消息。4.2.2 <binding> 的作用和绑定细节<binding> 元素用于将 portType 与特定的通信协议绑定,并规定了消息的编码和传输细节。例如,将 portType 绑定到SOAP协议,可以指定使用HTTP传输。<binding name="HelloWorldBinding" type="ns:HelloWorldPortType"> <soap:binding style="document" transport="http://schemas.xmlsoap.org/soap/http"/> <operation name="sayHello"> <soap:operation soapAction="urn:sayHello"/> <input> <soap:body use="literal"/> </input> <output> <soap:body use="literal"/> </output> </operation></binding>AI生成项目xml在 <binding> 中, <soap:binding> 标签指定了绑定的风格(如 document )和传输协议(如HTTP)。每个操作被 <soap:operation> 详细规定, <soap:body> 标签说明了消息体是如何被编码和封装的。4.2.3 <message> 的作用和消息结构<message> 元素描述了操作的消息结构,包括输入和输出消息。消息被定义为一系列参数,每个参数在WSDL中是一个 <part> 元素。<types> <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> <!-- 定义消息的数据类型 --> </xs:schema></types>$message name="sayHelloRequest"> <part name="name" type="xs:string"/></message>$message name="sayHelloResponse"> <part name="greeting" type="xs:string"/></message>AI生成项目xml在上述代码中,我们定义了两种消息类型: sayHelloRequest 和 sayHelloResponse 。每种消息类型由一个或多个 <part> 组成,每个 <part> 定义了一个消息片段的名称和类型。 <types> 部分包含了XML模式定义,用于说明这些 <part> 的数据类型。通过深入解析WSDL文件结构,开发者可以更好地理解Web服务的通信协议和交互方式。这不仅有助于服务的实现与集成,还能在解决服务交互问题时提供重要的参考依据。在下一章节中,我们将探讨使用Axis2框架解析WSDL文件的机制和实战演练。5. 创建服务代理类实例5.1 服务代理类的作用和实现服务代理类的概念和功能服务代理类是一种设计模式,它允许客户端通过一个间接层与远程服务进行交互,而不需要直接处理网络通信的复杂性。代理类负责创建实际的Web服务客户端,并封装了网络通信和数据处理的所有细节。在企业级应用中,服务代理类提供了以下主要功能:封装网络通信细节 :隐藏了与远程服务通信的细节,包括请求的构造、SOAP消息的封装、网络连接的建立以及响应的接收。提高代码可维护性 :由于所有通信逻辑被封装在代理类中,因此在服务接口或协议发生变化时,只需修改代理类,而不必修改客户端的业务逻辑代码。提供服务接口抽象 :代理类可以实现服务接口,使得客户端可以像调用本地方法一样调用远程服务,降低业务逻辑与远程通信的耦合。异常处理和日志记录 :服务代理类可以负责处理服务调用过程中可能出现的异常,并记录相关的交互日志,方便调试和问题追踪。使用代码创建服务代理类实例以下是如何使用Java语言和JAX-WS API创建服务代理类实例的一个示例:import javax.xml.namespace.QName;import javax.xml.ws.Service;import java.net.URL; public class ServiceProxyExample { public static void main(String[] args) { try { // WSDL文档的URL String wsdlURL = "http://example.com/service?wsdl"; // 定义命名空间和端口名称 QName serviceQName = new QName("http://example.com/", "ServiceName"); QName portQName = new QName("http://example.com/", "PortName"); // 使用JAX-WS API加载服务定义并创建服务对象 Service service = Service.create(new URL(wsdlURL), serviceQName); // 获取绑定接口 YourServiceInterface servicePort = service.getPort(portQName, YourServiceInterface.class); // 使用servicePort调用Web服务方法 ResultType response = servicePort.yourWebServiceMethod(parameterValue); // 输出响应结果 System.out.println("Service response: " + response); } catch (Exception e) { e.printStackTrace(); } }}AI生成项目java运行在上述代码中,我们首先通过指定的WSDL文档URL创建了一个服务对象 Service 。然后,我们使用服务对象的 getPort 方法来获取绑定到具体服务操作的代理对象 YourServiceInterface 。需要注意的是, YourServiceInterface 应该是由WSDL文件自动生成的Java接口,代表了远程服务端点。接下来,我们就可以像调用本地方法一样使用 servicePort 对象调用远程服务的方法了。5.2 服务代理类的配置与调用配置服务代理类的参数服务代理类的配置通常涉及设置网络连接参数、安全凭据以及其他与服务交互相关的属性。这些配置可以在创建服务代理实例时通过不同的方法进行,具体取决于使用的框架和环境。以下是一些常见的配置参数示例:// 配置代理地址System.setProperty("http.proxyHost", "proxy.example.com");System.setProperty("http.proxyPort", "8080"); // 配置用户名和密码String username = "user";String password = "pass";service Credential usernamePasswordCredential = new UsernamePasswordCredential(username, password);service.getRequestContext().put("security.username", username);service.getRequestContext().put("security.password", password);AI生成项目java运行实现服务调用和异常处理当调用远程Web服务时,通常需要处理多种异常情况,比如网络错误、服务不可用或者数据格式不匹配等问题。使用服务代理类时,可以利用Java异常处理机制来捕获和处理这些异常。以下是如何实现服务调用和异常处理的示例:try { // 假设ResultType是远程服务方法返回的类型 ResultType result = servicePort.yourWebServiceMethod(parameterValue); // 处理成功响应的逻辑 System.out.println("Success: " + result);} catch (WebServiceException e) { // 处理Web服务异常 System.err.println("WebServiceException: " + e.getMessage());} catch (ProcessingException e) { // 处理消息处理异常 System.err.println("ProcessingException: " + e.getMessage());} catch (RemoteException e) { // 处理远程调用异常 System.err.println("RemoteException: " + e.getMessage());} catch (Exception e) { // 处理其他通用异常 System.err.println("Exception: " + e.getMessage());}AI生成项目java运行在这个示例中,我们使用了 try-catch 语句来捕获和处理 WebServiceException 、 ProcessingException 和 RemoteException 等异常。这些异常通常由服务框架抛出,用于表示特定的远程服务调用问题。我们还添加了一个通用的 Exception 捕获,用于处理不属于前面特定类型的其他异常情况。通过上述步骤,我们可以创建并使用服务代理类实例,调用远程Web服务并处理可能出现的异常情况。下一章将详细讲解如何发送SOAP请求到远程服务器,并在本章的基础上进一步加深对Web服务交互过程的理解。6. 发送SOAP请求的方法SOAP(Simple Object Access Protocol)是一种基于XML的协议,用于在网络上进行分布式计算。在Web服务领域,SOAP是实现应用程序之间通信的一种常用方式。本章将深入探讨如何构造SOAP请求消息,并通过编程方式发送这些请求到远程Web服务端点,并处理响应。6.1 SOAP消息的构成和结构SOAP消息主要由三个部分组成:信封(Envelope),头部(Header)和正文(Body)。它们各自承载了不同的信息和功能,定义了整个消息的结构和语义。6.1.1 SOAP消息的格式和组成部分SOAP消息的信封部分是所有SOAP消息必须包含的元素,它定义了消息的开始和结束,以及消息的其他组成部分。一个典型的SOAP信封元素结构如下:<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <soapenv:Header> <!--SOAP头部信息--> </soapenv:Header> <soapenv:Body> <!--SOAP正文信息--> </soapenv:Body></soapenv:Envelope>AI生成项目xml头部(Header)部分是可选的,用于传递应用特定的信息,如安全凭证或事务信息。而正文(Body)部分是必须的,它包含了实际的消息内容,通常是一个或多个XML元素。6.1.2SOAP消息中的头部和正文解析在头部中,可以定义多个子元素,它们都是 <soapenv:Header> 的子节点,这些子节点定义了头部信息的细节。例如:<soapenv:Header> <ns2:Security xmlns:ns2="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd" SOAP-ENV:mustUnderstand="1"> <!--安全信息--> </ns2:Security></soapenv:Header>AI生成项目xml正文部分通常包含的是实际的调用信息,比如Web服务操作的名称和参数。例如:<soapenv:Body> <ns2:GetWeather xmlns:ns2="http://example.com/weather"> <!--请求的参数--> </ns2:GetWeather></soapenv:Body>AI生成项目xml6.2 发送SOAP请求的实践操作实际发送SOAP请求涉及到客户端和服务端的交互。客户端需要构造一个符合WSDL定义的SOAP消息,并通过HTTP协议发送到服务端。服务端处理完毕后,将响应消息以同样方式返回给客户端。6.2.1 配置和生成SOAP请求为了发送SOAP请求,可以使用各种编程语言提供的库和API。以下是一个使用Java语言配置和生成SOAP请求的例子:import javax.xml.soap.MessageFactory;import javax.xml.soap.SOAPConnection;import javax.xml.soap.SOAPConnectionFactory;import javax.xml.soap.SOAPMessage;import javax.xml.soap.SOAPPart; public class SoapClient { public static void main(String[] args) { try { MessageFactory messageFactory = MessageFactory.newInstance(); SOAPMessage message = messageFactory.createMessage(); SOAPPart soapPart = message.getSOAPPart(); String serverURI = "http://example.com/weather?wsdl"; SOAPEnvelope envelope = soapPart.getEnvelope(); SOAPBody soapBody = envelope.getBody(); String tns = "http://example.com/weather"; SOAPElement requestElement = soapBody.addChildElement("GetWeather", "ns2", tns); // 创建请求参数... SOAPConnection connection = SOAPConnectionFactory.newInstance().createConnection(); message.saveChanges(); URL endpoint = new URL(serverURI); SOAPMessage response = connection.call(message, endpoint); // 输出响应内容... } catch (Exception e) { e.printStackTrace(); } }}AI生成项目java运行在这个例子中,我们首先创建了一个SOAP消息,然后为该消息添加了一个请求体,并通过SOAP连接发送到服务器。6.2.2 发送请求并处理响应发送请求之后,需要处理从服务器返回的响应。这通常涉及解析响应消息,并从中提取所需的信息。以下是处理响应的代码逻辑:// 继续上面的例子System.out.println("Status code: " + response.getResponseCode());System.out.println("Response: " + response.getContentDescription());InputStream is = response.getContentDescription();// 使用SAX或者DOM解析响应消息...AI生成项目java运行上述代码将响应消息的内容输出到控制台,并展示了获取响应状态码的方式。实际应用中,更常用的是使用XML解析器来解析SOAP响应,如DOM或SAX解析器。在实际的工作中,使用服务代理类实例发送SOAP请求会更加方便,因为它抽象了SOAP消息的构造过程和底层通信细节,使得开发人员可以更加专注于业务逻辑的实现。通过这一章的学习,您应该已经了解了SOAP请求的基本构成,以及如何在实际项目中构造和发送SOAP请求。在下一章,我们将深入解析SOAP响应消息的结构和技术。7. 解析SOAP响应的技术SOAP响应消息是Web服务交互过程中的重要部分,通常包含服务器的响应数据以及可能发生的任何错误信息。解析这些响应消息是确保应用正确处理服务器返回信息的关键步骤。本章将深入探讨SOAP响应消息的结构分析以及使用不同技术提取数据的方法。7.1 SOAP响应消息的结构分析SOAP响应消息具有一定的结构,正确理解和分析这些结构是提取有用信息的基础。7.1.1 解析SOAP响应中的数据SOAP响应通常包含一个Envelope元素,其下有两个子元素:Header和Body。Header通常用于包含消息处理的元数据,而Body包含了实际的响应信息。<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"> <soap:Header/> <soap:Body> <ns2:getCountryResponse xmlns:ns2="http://example.com/"> <return>USA</return> </ns2:getCountryResponse> </soap:Body></soap:Envelope>AI生成项目xml在上述XML片段中, <return> 元素包含了服务器返回的实际数据。解析这部分数据,我们需要关注Body内的内容。7.1.2 处理SOAP响应中的错误信息在SOAP响应中,错误信息通常被封装在 <Fault> 元素内。开发者需要检测此元素的存在,并解析其子元素如 <faultcode> , <faultstring> 等来获取错误详情。<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"> <soap:Body> <soap:Fault> <faultcode>soap:Server</faultcode> <faultstring>Server was unable to read request</faultstring> </soap:Fault> </soap:Body></soap:Envelope>AI生成项目xml在这个例子中,服务器表明它无法读取请求,开发者可以根据这个信息进行进一步的调试。7.2 提取SOAP响应数据的技术解析SOAP响应数据常用的技术有DOM和SAX。这两种技术各有特点,适用于不同的场景。7.2.1 使用DOM解析技术提取数据DOM(Document Object Model)是解析和操作XML的一种方式,它将XML文档转换为树形结构,并允许开发者以编程方式访问和修改文档内容。DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();DocumentBuilder builder = factory.newDocumentBuilder();Document doc = builder.parse(new InputSource(new StringReader(soapResponse)));NodeList nodes = doc.getElementsByTagName("return");for (int i = 0; i < nodes.getLength(); i++) { Node node = nodes.item(i); System.out.println("Value: " + node.getTextContent());}AI生成项目java运行上述Java代码使用DOM解析SOAP响应中的数据。7.2.2 使用SAX解析技术提取数据SAX(Simple API for XML)是一种基于事件的XML解析技术,适用于处理大型文件,因为它不需要将整个文档加载到内存中。class MyHandler extends DefaultHandler { @Override public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException { if ("return".equals(qName)) { System.out.println("Value: "); } } @Override public void characters(char[] ch, int start, int length) throws SAXException { System.out.print(new String(ch, start, length)); }} XMLReader reader = XMLReaderFactory.createXMLReader();MyHandler handler = new MyHandler();reader.setContentHandler(handler);reader.parse(new InputSource(new StringReader(soapResponse)));AI生成项目java运行在上述Java代码中,我们定义了一个自定义的处理器来响应SAX事件,并输出所需的数据。SAX的解析速度快,内存使用效率高,但在处理复杂的XML文档结构时,可能需要更多的代码来跟踪当前状态和上下文。本章介绍了如何分析SOAP响应消息的结构,并详细讲解了使用DOM和SAX两种技术从SOAP响应中提取数据的方法。理解这两种技术的区别和适用场景是确保正确解析响应并有效利用数据的关键。通过结合实际的例子和代码演示,本章旨在帮助IT从业者更好地理解SOAP响应解析的核心技术,从而为后续的服务交互奠定坚实的基础。本文还有配套的精品资源,点击获取 简介:解析WSDL文件是开发基于Web服务的应用程序中的关键任务。本文将详细介绍如何使用Java以及相关库如Apache CXF和Axis2来解析WSDL文档,并实现SOAP请求与响应处理。文章首先会指导如何引入必要的库,然后深入解析WSDL文件获取服务接口、操作和消息结构。接着,通过示例代码展示如何创建服务代理,发送SOAP请求,并接收与解析SOAP响应。最后,本文也会解释如何处理主流Web服务框架生成的WSDL,并提供详细的示例代码。掌握这些知识能够帮助开发者在Java环境中构建健壮的Web服务客户端。————————————————原文链接:https://blog.csdn.net/weixin_42594427/article/details/149395353
-
前言 在当今数字化与信息化飞速发展的时代,图像的生成与处理技术正日益成为众多领域关注的焦点。从创意设计到数据可视化,从游戏开发到人工智能辅助创作,高效、精准且具有高度适应性的图像生成方案有着广泛而迫切的需求。Java 作为一种强大、稳定且广泛应用的编程语言,在图像绘制领域也发挥着不可忽视的作用。 在GIS领域,比如图例的生成就会面对以上的问题。由于在进行字符标注时无法预测文本的长度,因此我们需要能有一种自适应文本长度的生成方法,但是同时,也有可能我们需要指定一种宽度从而对字符文本进行绘制的需要。如下两图所示: 自适应宽度生成示意图指定宽度生成示意图 本实战旨在深入探讨基于 Java 的不固定长度字符集在指定宽度和自适应模型下图片绘制生成的方法与技巧。不固定长度字符集为图片绘制带来了独特的挑战与机遇。一方面,其灵活多变的字符组合方式能够创造出丰富多样、极具个性化的图像效果,为创意表达提供了广阔空间;另一方面,如何在保证图像整体协调性与美观性的前提下,合理安排不同长度字符在指定宽度内的布局,实现自适应模型下的高效绘制,需要深入研究与实践。 通过本次实战,我们期望为读者提供一套完整、实用且具有创新性的基于 Java 的图片绘制解决方案,帮助读者提升在图像生成领域的技术能力,激发他们在数字创作方面的灵感与潜力,从而在各自的应用场景中创造出更具价值与吸引力的图像作品,为推动图像技术的发展与应用贡献一份力量。 一、需求介绍 在面向地理空间的图例生成过程,我们通常会遇到以下两种情况:第一种是需要指定宽度,比如要求在宽度为200px的图片中,将指定的文字在图片中生成。第二种就是需要根据指定列,即一行展示几列,然后自适应的生成固定宽度的图片。本节将主要介绍这两个需求。这里我们需要展示的是一些不一定长的字符串集合,模拟展示以下这些地名数据,如下所示: String[] demoTexts = { " 项目管理", "软件开发", "数据分析","人工智能", "云计算", "网络安全", "用户体验", "测试验证", "运维部署", "昆明市","曲靖市","玉溪市", "保山市","昭通市","丽江市","普洱市","临沧市","楚雄彝族自治州", "红河哈尼族彝族自治州","文山壮族苗族自治州","西双版纳傣族自治州", "湘西土家族苗族自治州","深圳市","保亭黎族苗族自治县", "阿坝藏族羌族自治州","黔西南布依族苗族自治州","克孜勒苏柯尔克孜自治州", "双江拉祜族佤族布朗族傣族自治县","积石山保安族东乡族撒拉族自治县","中国石油集团东方地球物理勘探有限责任公司霸州基地管理处居委会", "天津市蓟州区京津州河科技产业园管理委员会虚拟社区","窑街煤电集团民勤县瑞霖生态农林有限责任公司生活区","沈阳市于洪区红旗土地股份合作经营有限公司生活区", "大理白族自治州","德宏傣族景颇族自治州","怒江傈僳族自治州","迪庆藏族自治州"};AI生成项目java运行 1、指定宽度生成 指定宽度生成,即我们对目标成果的图片宽度是有要求的,比如宽度指定为200px。核心需求如下: 固定总宽度模式 平均分配列宽:根据总宽度和列数计算每列可用宽度 自动换行:根据列数自动计算行数 文本截断:超长文本添加省略号 2、指定列自适应生成 自适应列宽模式 动态计算列宽:根据每列中最长的条目确定列宽,遍历所有文本,计算每个条目(矩形+间距+文本)的总宽度,确定最大宽度作为图像宽度。 计算高度:基于行数和字体高度计算总高度 自动换行:根据列数自动计算行数 保持完整显示:不截断文本 二、Java生成实现 本小节将根据上面的生成需求来具体讲解如何进行详细的生成。java生成的实现分成三个部分,第一部分是介绍两个公共方法,第二部分介绍如何按照指定宽度生成,第三部分介绍如何进行自适应生成,通过代码实例的方法进行讲解。 1、公共方法 为了方便对对绘制的文字展示得更加美观,这里我们每进行一次绘制就修改画笔的颜色。因此需要一个生成指定颜色的方法,在java中生成Color对象,并且转为十六进制的颜色表示,核心方法如下: /*** - 将color转十六进制字符串* @param color* @return*/public static String Color2String(Color color) {// 获取 RGB 颜色值,格式为 0x00RRGGBB int rgb = color.getRGB(); // 将 RGB 转换为十六进制字符串,去掉前两位的透明度部分(如果是纯不透明颜色) String hexColor = "#" + Integer.toHexString(rgb & 0xffffff); return hexColor;}AI生成项目java运行 根据不同字符串生成均匀分布的颜色方法如下: // 生成可区分颜色(HSV色环均匀分布)private static Color[] generateDistinctColors(int count) { Color[] colors = new Color[count]; float goldenRatio = 0.618033988749895f; // 黄金分割比例 float saturation = 0.8f; // 饱和度 float brightness = 0.9f; // 亮度 for (int i = 0; i < count; i++) { float hue = (i * goldenRatio) % 1.0f; colors[i] = Color.getHSBColor(hue, saturation, brightness); } return colors;}AI生成项目java运行 以上两个方法在指定宽度生成和自适应生成中均会使用到,因此在此简单列出来。 2、指定宽度生成按指定宽度生成的核心方法如下: // 固定总宽度模式public static BufferedImage createFixedColumnsImage(String[] texts, int columns, int totalWidth, Font font, int padding, int columnSpacing, int rowSpacing) { BufferedImage tempImg = new BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB); Graphics2D tempG = tempImg.createGraphics(); tempG.setFont(font); FontMetrics fm = tempG.getFontMetrics(); final int RECT_SIZE = 10; final int ENTRY_SPACING = 5; // 生成颜色序列 Color[] colors = generateDistinctColors(texts.length); // 计算列宽 int availableWidth = totalWidth - padding * 2 - (columns - 1) * columnSpacing; int columnWidth = availableWidth / columns; int textMaxWidth = columnWidth - RECT_SIZE - ENTRY_SPACING; // 处理文本 List<String> processedTexts = new ArrayList<>(); for (String text : texts) { processedTexts.add(truncateText(text, textMaxWidth, fm)); } // 计算总高度 int rows = (int) Math.ceil((double)texts.length / columns); int totalHeight = padding * 2 + rows * (fm.getHeight() + rowSpacing) - rowSpacing; // 创建图像 BufferedImage image = new BufferedImage(totalWidth, totalHeight, BufferedImage.TYPE_INT_ARGB); Graphics2D g = image.createGraphics(); setupGraphics(g, font); // 绘制背景 g.setColor(Color.WHITE); g.fillRect(0, 0, totalWidth, totalHeight); // 绘制条目 int yBase = padding + fm.getAscent(); int[] columnX = new int[columns]; for (int i = 0; i < columns; i++) { columnX[i] = padding + i * (columnWidth + columnSpacing); } for (int i = 0; i < processedTexts.size(); i++) { g.setColor(colors[i]); int col = i % columns; int row = i / columns; int y = yBase + row * (fm.getHeight() + rowSpacing); int rectY = y - fm.getAscent() + (fm.getHeight() - RECT_SIZE)/2; // 绘制矩形 g.fillRect(columnX[col], rectY, RECT_SIZE, RECT_SIZE); // 绘制文本 g.drawString(processedTexts.get(i), columnX[col] + RECT_SIZE + ENTRY_SPACING, y); } g.dispose(); tempG.dispose(); return image;}AI生成项目java运行 由于在指定宽度的生成方式中,绘制的图片宽度是固定的,而文字的字数是不固定的。因此在绘制的过程中,需要对超长的文本进行截取,超长的部分将使用省略号来进行展示。对超长文本字符进行截断处理的方法如下: private static String truncateText(String text, int maxWidth, FontMetrics fm) { if (fm.stringWidth(text) <= maxWidth) return text; int ellipsisWidth = fm.stringWidth("..."); int availableWidth = maxWidth - ellipsisWidth; int length = text.length(); while (length > 0 && fm.stringWidth(text.substring(0, length)) > availableWidth) { length--; } return length > 0 ? text.substring(0, length) + "..." : "";}AI生成项目java运行 生成指定宽度的图片调用方法如下: // 生成固定宽度图片(400px宽,2列)BufferedImage fixedImage = createFixedColumnsImage( demoTexts, 2, 400, new Font("宋体", Font.PLAIN, 12), 15, 20, 10);ImageIO.write(fixedImage, "PNG", new File("D:/fixed_columns_250420.png"));AI生成项目java运行 生成的成果图片如下: 3、指定列自适应生成 生成指定列的自适应图片生成的核心方法如下: // 自适应列宽模式 public static BufferedImage createAdaptiveColumnsImage(String[] texts, int columns, Font font, int padding, int columnSpacing, int rowSpacing) { BufferedImage tempImg = new BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB); Graphics2D tempG = tempImg.createGraphics(); tempG.setFont(font); FontMetrics fm = tempG.getFontMetrics(); final int RECT_SIZE = 10; final int ENTRY_SPACING = 5; // 图标与文字间距 // 生成颜色序列 Color[] colors = generateDistinctColors(texts.length); int index = 0; for (String text : texts) { texts[index] = Color2String(colors[index]) + " " + text; //processedTexts.add(truncateText(text, textMaxWidth, fm)); index ++; } // 计算列宽 int[] columnWidths = new int[columns]; for (int i = 0; i < texts.length; i++) { int col = i % columns; int width = RECT_SIZE + ENTRY_SPACING + fm.stringWidth(texts[i]); if (width > columnWidths[col]) { columnWidths[col] = width; } } // 计算总尺寸 int totalWidth = padding * 2; for (int w : columnWidths) { totalWidth += w + columnSpacing; } totalWidth -= columnSpacing; // 最后一列不加间距 int rows = (int) Math.ceil((double)texts.length / columns); int totalHeight = padding * 2 + rows * (fm.getHeight() + rowSpacing) - rowSpacing; // 创建图像 BufferedImage image = new BufferedImage(totalWidth, totalHeight, BufferedImage.TYPE_INT_ARGB); Graphics2D g = image.createGraphics(); setupGraphics(g, font); // 绘制背景 g.setColor(Color.WHITE); g.fillRect(0, 0, totalWidth, totalHeight); // 绘制条目 int x = padding; int yBase = padding + fm.getAscent(); int[] columnX = new int[columns]; for (int i = 0; i < columns; i++) { columnX[i] = x; x += columnWidths[i] + columnSpacing; } g.setColor(Color.RED); for (int i = 0; i < texts.length; i++) { g.setColor(colors[i]); int col = i % columns; int row = i / columns; int y = yBase + row * (fm.getHeight() + rowSpacing); int rectY = y - fm.getAscent() + (fm.getHeight() - RECT_SIZE)/2; // 绘制矩形 g.fillRect(columnX[col], rectY, RECT_SIZE, RECT_SIZE); // 绘制文本 g.drawString(texts[i], columnX[col] + RECT_SIZE + ENTRY_SPACING, y); } g.dispose(); tempG.dispose(); return image; }AI生成项目java运行 在自适应生成的过程中,最需要处理的逻辑就是动态的计算宽度等值。最终生成的结果图片如下: 三、总结 以上就是本文的主要内容,本实战旨在深入探讨基于 Java 的不固定长度字符集在指定宽度和自适应模型下图片绘制生成的方法与技巧。不固定长度字符集为图片绘制带来了独特的挑战与机遇。一方面,其灵活多变的字符组合方式能够创造出丰富多样、极具个性化的图像效果,为创意表达提供了广阔空间;另一方面,如何在保证图像整体协调性与美观性的前提下,合理安排不同长度字符在指定宽度内的布局,实现自适应模型下的高效绘制,需要深入研究与实践。 通过本次实战,我们期望为读者提供一套完整、实用且具有创新性的基于 Java 的图片绘制解决方案,帮助读者提升在图像生成领域的技术能力,激发他们在数字创作方面的灵感与潜力,从而在各自的应用场景中创造出更具价值与吸引力的图像作品,为推动图像技术的发展与应用贡献一份力量。行文仓促,定有不足之处,欢迎各位朋友在评论区批评指正,不胜感激。———————————————— 原文链接:https://blog.csdn.net/yelangkingwuzuhu/article/details/147401525
-
园区基线部署有什么要求,有没有对应材料可以发一下
-
在idea上构建含有自己依赖从父依赖继承自己依赖的项目是不会报错能正常编译的,希望能参考idea尽量减少开发者处理的错误,这种错误明显不符合常理按正常逻辑重复的依赖应该能自动剔除。
-
1.概念1.1.集合与数组的区别集合:长度不固定,动态的根据数据添加删除改变长度,并且只能存入引用类型,读取采用迭代器或其他方法数组:长度固定,不可改变,既可以存入基本类型也可以存入引用类型,读取使用索引读(for)长度 存入类型 读取集合 长度不固定,动态的根据数据添加删除改变长度 只能存入引用类型 采用迭代器或其他方法数组 长度固定,不可改变 既可以存入基本类型也可以存入引用类型 使用索引(for)1.2.集合分类分为三类:List类,Set类,Map类List集合:集合里面元素有序,并且允许可重复Set集合:集合里面元素无序,并且不可重复(保证唯一性)Map集合:集合采用键值对方式,key唯一(不允许重复)无序,value没有要求是否有序 是否可重复List 有序 可重复Set 无序 不可重复1.3.Collection和Collections的区别Collection是一个接口,给集合实现的,里面定义了一些操作集合的方法Collections是一个工具类,位于java.util包中,可以直接使用该类操作集合(增删改,排序)1.4.集合遍历的方法有六种方法:for,增强for,迭代器,列表迭代器,foeEach,Stream流for:带索引查询(区分集合是否带索引,才能使用该方法)List<String> list = Arrays.asList("A", "B", "C"); // 通过索引遍历(适合 ArrayList 等支持随机访问的集合)for (int i = 0; i < list.size(); i++) { System.out.println(list.get(i));}增强for:没有索引,直接遍历查询List<String> list = Arrays.asList("A", "B", "C"); // 直接遍历元素(底层基于迭代器实现)for (String item : list) { System.out.println(item);}迭代器:在迭代器里面只能删除元素,不能插入元素List<String> list = Arrays.asList("A", "B", "C");Iterator<String> iterator = list.iterator(); // 通过迭代器遍历(适用于所有 Collection)while (iterator.hasNext()) { String item = iterator.next(); System.out.println(item); // 可在遍历中安全删除元素:iterator.remove();}List<String> list = new ArrayList<>(Arrays.asList("A", "B", "C"));Iterator<String> iterator = list.iterator(); while (iterator.hasNext()) { String item = iterator.next(); if ("B".equals(item)) { iterator.remove(); // 允许删除当前元素 // iterator.add("D"); // 编译错误:Iterator 没有 add() 方法 }}列表迭代器:没有限制,可以进行删除查询插入元素List<String> list = Arrays.asList("A", "B", "C");ListIterator<String> listIterator = list.listIterator(); // 正向遍历(从头到尾)while (listIterator.hasNext()) { String item = listIterator.next(); System.out.println(item);} // 反向遍历(从尾到头)while (listIterator.hasPrevious()) { String item = listIterator.previous(); System.out.println(item);}List<String> list = new ArrayList<>(Arrays.asList("A", "B", "C"));ListIterator<String> listIterator = list.listIterator(); // 正向遍历while (listIterator.hasNext()) { String item = listIterator.next(); if ("B".equals(item)) { listIterator.remove(); // 删除当前元素 listIterator.add("D"); // 在当前位置插入新元素 listIterator.set("E"); // 替换当前元素(需在 next() 或 previous() 后调用) }} // 反向遍历while (listIterator.hasPrevious()) { String item = listIterator.previous(); System.out.println(item);}forEach:因为它基于迭代器实现的,因此也不能在循环中插入元素List<String> list = Arrays.asList("A", "B", "C"); // 使用 Lambda 表达式遍历list.forEach(item -> System.out.println(item)); // 或使用方法引用list.forEach(System.out::println);Stream:没有限制List<String> list = Arrays.asList("A", "B", "C"); // 转换为 Stream 并遍历list.stream().forEach(item -> System.out.println(item)); // 并行流遍历(多线程处理)list.parallelStream().forEach(item -> System.out.println(item));2.List2.1.List的实现实现List的集合有:ArrayList,LinkedList,VectorArrayList:基于动态的数组创建的,查询效率高,增删效率一般,线程不安全LinkedList:基于双向链表创建的,查询效率一般,增删效率高,线程不安全Vector:基于动态数组创建的,与ArrayList类似,不过它是线程安全的数据结构 读操作 写操作 线程安全ArrayList 数组 高 一般 不安全LinkedList 双向链表 一般 高 不安全Vector 数组 高 一般 安全2.2.可以一边遍历一边修改List的方法首先思考有几个遍历方法:六个哪些是不能修改元素的:迭代器,forEach最终得到的方法:for,增强for,列表迭代器,Stream流2.3.List快速删除元素的原理原理是基于集合底层数据结构不同,分为两类:ArrayList,LinkedListArrayList:基于数组对吧,原先数组是通过索引删除数据,那么因此ArrayList也是如此,基于索引来删除数据具体实现:如果你是删除尾部最后一个数据,直接删除即可,时间复杂度为O(1),如果不是,那么它会将索引元素删除后,将后面的元素往前面覆盖,然后计算出集合长度,时间复杂度为O(n),n为元素的个数LinkedList:基于双向链表,简单来说链表由节点组成,每个节点包含自己的数据与前一个节点的引用和后一个节点的引用,实现双向并通具体实现:如果你是删除尾部最后一个数据,直接删除即可,时间复杂度为O(1),如果不是,那么就是从头或尾进行查询删除,时间复杂度O(n)2.4.ArrayList与LinkedList的区别数据结构组成不同:Array List基于数组,LinkedList基于双向链表删除和插入效率不同:ArrayList在尾部的效率高(平均O(1)),在其他的地方效率低,由于需要进行元素覆盖,而LinkedList它基于链表引用,在尾部的效率(O(1)比ArrayList效率低(ArrayList基于数组,内存是连续的,而LinkedList基于链表,内存不连续),在其他地方删除与插入与ArrayList效率差不多(O(n))随机访问速度:由于ArrayList基于数组根据索引查询,时间复杂度O(1),而LinkedList基于链表,它需要从头或尾部访问,因此时间复杂度为O(n)适用场景不同:ArrayList更适合高频的随机访问操作或尾部插入为主,LinkedList更适合高频头尾插入/删除(队列)或需要双向遍历线程安全:都是线程不安全的2.5.线程安全实现线程(List)安全的方法有:实现Collections.synchronizedList,将线程不安全的List集合加个锁,变成安全的直接使用线程安全的List集合:比如Vector,CopyOnWirteArrayList2.6.ArrayList的扩容机制首先如果你没有指定长度,默认长度为10,当你要添加元素并且超过此时容量长度时,就会进行扩容操作实现:1.扩容:创建一个新的数组,新数组的长度为原数组的1.5倍数,然后再检查容量是否足够,不够继续扩容---2.复制:将旧的数组里面的值复制进新的数组中,再进行写操作---3.更改引用:将原先指向旧数组的引用指向新数组---4.扩容完成:可以继续扩容2.7.CopyOnWirteArrayList它实现了读写分离,写操作加了互斥锁ReentrantLock,避免出现线程安全问题,而读操作没有加锁,使用volatile关键字修饰数组,保证当前线程对数组对象重新赋值后,其他线程可以及时感知到(所有线程可见性)。线程读取数据可以直接读取,提高效率写操作:它不会向ArrayList一样直接扩容1.5倍,它是根据你的添加元素个数多少来扩容,如果你只添加一个元素,那么它会创建一个新数组,长度比旧数组长度多一,然后依旧是依次复制元素进新数组中,改变内部引用指向(需要频繁创建新的数组,以时间换空间)读操作:就是说它不会管你的数据是否修改,内部指向是旧数组,那么就读取旧数组的数据,指向是新数组就读取新数据,这样效率会高(数据弱一致性)————————————————原文链接:https://blog.csdn.net/2402_88700528/article/details/148262923
-
在Java中,==和equals()是两个常用的比较操作符和方法,但它们之间的用法和含义却有着本质的区别。本文将详细解释这两个操作符/方法之间的区别。1、==操作符==操作符 在Java中 主要用于比较两个变量的值是否相等。但是,这个“值”的含义取决于变量的类型:1、于基本数据类型(如int, char, boolean等):== 比较的是两个变量的值是否相等。2、对于引用类型(如对象、数组等):== 比较的是两个引用是否指向内存中的同一个对象(即地址是否相同)。示例:int a = 5; int b = 5; System.out.println(a == b); // 输出true,因为a和b的值相等 Integer c = new Integer(5); Integer d = new Integer(5); System.out.println(c == d); // 输出false,因为c和d指向的是不同的对象2、equals()方法equals()方法是Java Object 类的一个方法,用于比较两个对象的内容是否相等。需要注意的是,默认的 equals() 方法 实现其实就是 == 操作符对于引用类型的比较,即比较的是两个引用是否指向同一个对象。但是,很多Java类(如String, Integer等)都重写了 equals() 方法,以提供基于内容的比较。示例:String str1 = new String("hello"); String str2 = new String("hello"); System.out.println(str1.equals(str2)); // 输出true,因为str1和str2的内容相等 // Integer类也重写了equals方法 Integer e = new Integer(5); Integer f = new Integer(5); System.out.println(e.equals(f)); // 输出true,因为Integer类重写了equals方法,基于值进行比较3、总结1、==操作符:对于基本数据类型,比较的是值是否相等。对于引用类型,比较的是两个引用是否指向同一个对象(即地址是否相同)。2、equals()方法:默认实现是基于 == 操作符的,即比较两个引用是否指向同一个对象。但很多类(如String, Integer等)都重写了 equals() 方法,以提供基于内容的比较。重要提示:1、当比较两个对象的内容是否相等时,应该优先使用 equals() 方法,而不是 == 操作符。2、自定义类如果需要比较内容是否相等,也应该重写 equals() 方法。3、需要注意的是,如果重写了 equals() 方法,通常也需要重写 hashCode() 方法,以保持两者的一致性。这是因为在Java中,很多集合类(如HashSet, HashMap等)在存储和查找元素时,都会同时用到 equals() 和 hashCode() 方法。————————————————原文链接:https://blog.csdn.net/qq_41840843/article/details/139706184
-
1.内存模型1.1.JVM内存模型的介绍内存模型主要分为五个部分:虚拟机栈,本地方法栈,堆,方法区(永久代或元空间),程序计数器,当然还有一部分是直接内存。虚拟机栈:每个线程各有一个,线程独有,当执行方法(除了本地方法native修饰的方法)之前,会创建一个栈帧,栈帧里面包含局部变量表和操作数栈和动态链接和方法出口等信息,而每个栈帧就是存入栈中本地方法栈:每个线程各有一个,线程独有,当执行本地方法时类似于虚拟机栈,一样会创建栈帧,存入对应信息程序计数器:每个线程各有一个,线程独有,它的作用是记录当前线程下一次执行的二进制字节码指令地址,如果执行的是本地方法那么它会记录为定值(null)堆:所有线程共享,堆的回收由垃圾回收机制管理,堆中主要存入对象实例信息,类信息,数组信息,堆是JVM内存中最大的一个永久代:在jdk1.7及以前是方法区的实现,使用的是jvm内存,独立于堆,主要存入类信息,静态变量信息,符号引用等信息元空间:在jdk1.8及以后是方法区的实现,使用的是本地内存,主要存入类信息,静态变量信息,符号引用等信息直接内存:该内存属于操作系统,由NIO引入,操作系统和Java程序都可以进行操作,实现共享----常量池:属于class文件的一部分,主要存储字面量,符号引用----运行时常量池:属于方法区,其实就是将常量池中的符号引用替换成了直接引用,其余一样1.2.堆和栈的区别五个点:用途,生命周期,存储速度,存储空间,可见性用途:栈主要存储方法返回地址,方法参数,临时变量,每次方法执行之前会创建栈帧,而堆存储对象的实例信息,类实例信息,数组信息生命周期:栈的生命周期可见,每次方法执行完栈帧就会移除(弹出),而堆中的数据需要由垃圾回收器回收,回收时间不确定存储速度:栈的速度更快,栈保持"先进后出"的原则,操作简单快,而堆需要对对象进行内存分配和垃圾回收,并且垃圾回收器本身运行也会损耗性能,速度慢存储空间:栈的空间相对于堆的空间小,栈的空间小且固定,由操作系统管理,而堆的空间是jvm中最大的,由jvm管理可见性:栈是每个线程都有的,而堆是所有线程共享的1.3.栈的存储细节如果执行方法时,里面创建了基本类型,那么基本类型的数据会存入栈中,如果创建了引用类型,会将地址存入栈,其实例数据存入堆中1.4.堆的部分堆主要分为两部分:新生代,老年代,它的比例:1:2新生代:新生代分为两个区:伊甸园区和幸存者区,而幸存者区又平均分为S0和S1区,伊甸园区与S0与S1之间的比例:8:1:1,每次新创建的对象实例都会先存入伊甸园区,它们主要使用的垃圾回收算法是复制算法,当伊甸园区的内存使用完时,会使用可达性分析算法,标记不可存活的对象(没有被引用的对象)将存活对象复制移入S0或S1中,这个过程叫Minor GC,如果这次移入的是S0,那么下次就会将伊甸园区和S0中的对象移入S1中,循环反复,每经历一次Minor GC过程就会给对象年龄加一,直到大于等于15时,会认为该对象生命周期长,移入老年代中细节:其实新创建的对象不会直接存入伊甸园区,如果多线程情况下同时进行存入对象(线程竞争压力大)会导致性能的损失,因此会给每个线程从伊甸园区中先申请一块TLAB区域,先将对象存入该区,如果该区内存使用完,会重写申请或直接存入伊甸园区老年代:老年代就是存储生命周期长的对象(不经常回收的对象),主要使用的垃圾回收算法为标记清除算法或标记整理算法,看场景出发,其中老年代还包含一个大对象区大对象区:主要存储的就是新创建的大对象比如说大数组,会直接将该对象存入大对象区中,不在存入新生代可达性分析算法:从GC Root出发找对应引用对象,如果一个对象没有被直接引用或间接引用,那么会被标记,GC Root可以是java的核心库中的类,本地方法使用的类,还未结束的线程使用的类,使用了锁的类标记清除算法:对引用的对象进行标记,然后进行清除(不是真正的清除,而是记录其对象的起始地址和结束地址到一个地址表中,下次要添加新对象时会先从表中找,找到一个适合大小的就会进行覆盖),清除:记录地址,新对象进行覆盖,好处:速度快,缺点:内存碎片化严重(内存不连续了,本来可以存入的对象存入不了)标记整理算法:同理进行标记,然后再对可存活对象进行整理,最后清除,好处:避免了内存碎片化问题,缺点:速度慢复制算法:将内存空间分为两份,一份存对象from,一份为空to,当要回收时,复制可存活对象移入为空的内存空间to中(移入既整理),然后对存对象的空间from整体清除,然后名称from和to换过来为什么会有大对象区:因为伊甸园区的内存空间本身就不大,如果你直接创建一个大于它空间的对象,会出现问题,还有就是即使没有超过伊甸园区的空间,但是其对象依旧很大,频繁的复制移动很影响性能1.5.程序计数器的作用简单来说:线程1执行到某个地方时,线程2抢到了执行权,那么等到线程1执行时是不是需要知道上次执行到哪里了,所以程序计数器就是记录执行到哪里的,并且每次线程都需要有一个来记录1.6.方法区的内容方法区主要包含:类信息,静态变量信息,运行时常量池,即时编译器的缓存数据1.7.字符串池在jdk1.6及以前字符串池属于永久代,jdk1.7字符串池移入堆中但是还是属于永久代的,jdk1.8及以后还是存入堆中,但是不属于元空间了(1.7以前是永久代,1.8以后是元空间)细节:String s1 = "a";它的过程是:先去字符串池中找,看是否能找到该字符,找到了直接复用池中地址,没有找到会先在堆中创建一个String对象,jdk1.6它会将数据复制一份重新创建一个新的对象存入池中,jdk1.7会将其地址复用给池中String s2 = new("b");同理String s3 = "a" + "b";常量进行相加,与new("ab")基本一致String s4 = s1 + s2;变量相加,底层使用的是new StringBuilder.append("a").append("b").toString(),如果池中存在"ab",它也不会复用,而是直接创建,如果池中不存在,而不会将新创建的对象存入池中1.8.引用类型引用类型:强引用,软引用,弱引用,虚引用,(终结器引用)强引用:比如new就是,只要有强引用指向对象,那么该对象永远不会被回收软引用:如果出现内存溢出的情况,再下次GC时会对其回收弱引用:每次进行GC过程都会进行回收虚引用:每次进行GC过程都会进行回收细节:这些都是对象,等级依次递减软引用:创建一个软引用对象时你可以指定引用队列,如果不指定会导致软引用为null一个空壳,比如说出现了GC Root强引用软引用对象,导致软引用对象无法被回收,你想要其对象被回收,可以使用引用队列,简单来说就是出现了这种情况,将软引用对象存入队列中,下次GC会扫描队列进行回收,当然这是特殊情况,总结来说:软引用可以使用引用队列也可以不使用public class SoftRefDemo { public static void main(String[] args) throws InterruptedException { // 1. 创建引用队列 ReferenceQueue<Object> queue = new ReferenceQueue<>(); // 2. 创建大对象(确保能被GC回收) byte[] data = new byte[10 * 1024 * 1024]; // 10MB // 3. 创建软引用并关联队列 SoftReference<Object> softRef = new SoftReference<>(data, queue); // 4. 移除强引用(只保留软引用) data = null; System.out.println("GC前: "); System.out.println(" softRef.get() = " + softRef.get()); System.out.println(" queue.poll() = " + queue.poll()); // 5. 强制GC(模拟内存不足) System.gc(); Thread.sleep(1000); // 给GC时间 System.out.println("\nGC后: "); System.out.println(" softRef.get() = " + softRef.get()); System.out.println(" queue.poll() = " + queue.poll()); }}GC前: softRef.get() = [B@15db9742 queue.poll() = null GC后: softRef.get() = null queue.poll() = java.lang.ref.SoftReference@6d06d69c弱引用:与软引用相同WeakHashMap<Key, Value> map = new WeakHashMap<>(); Key key = new Key();map.put(key, new Value()); // 移除强引用key = null; System.gc(); // GC后Entry自动被移除System.out.println(map.size()); // 输出: 0虚引用:最好的例子就是直接内存:它就是使用了虚引用,直接内存就是从操作系统中申请了一块空间来使用,因此GC是不能对其进行回收的,如果当强引用消失只剩下虚引用,那么会将虚引用对象存入引用队列中,等队列来执行本地方法释放直接内存public class PhantomRefDemo { public static void main(String[] args) { // 1. 创建引用队列 ReferenceQueue<Object> queue = new ReferenceQueue<>(); // 2. 创建虚引用 Object obj = new Object(); PhantomReference<Object> phantomRef = new PhantomReference<>(obj, queue); // 3. 模拟直接内存分配 ByteBuffer directBuffer = ByteBuffer.allocateDirect(1024 * 1024); // 1MB System.out.println("GC前:"); System.out.println(" phantomRef.get() = " + phantomRef.get()); // null System.out.println(" queue.poll() = " + queue.poll()); // null // 4. 移除强引用(触发回收条件) obj = null; directBuffer = null; // 释放DirectByteBuffer强引用 // 5. 强制GC(实际应用中会自动触发) System.gc(); try { Thread.sleep(500); } catch (Exception e) {} System.out.println("\nGC后:"); System.out.println(" phantomRef.get() = " + phantomRef.get()); // null System.out.println(" queue.poll() = " + queue.poll()); // 返回phantomRef对象 // 6. 实际效果:DirectByteBuffer分配的1MB堆外内存已被释放 }}终结器引用:在所有父类Object中有一个终结器方法finalize()方法,如果重写该方法,那么执行GC之前会先执行该方法,当没强引用指向了,而这个对象还重写了finalize()方法,那么会将这个终结器引用对象加入队列中,下次GC时会先由队列来执行finalize()方法,但是指定执行的队列是一个优先级不高的队列,会导致资源释放缓慢public class ResourceHolder { // 重写finalize方法(不推荐!) @Override protected void finalize() throws Throwable { releaseResources(); // 释放资源 super.finalize(); }}1.9.内存泄漏与内存溢出内存泄漏:就是说没有被引用的对象没有被回收,导致可用内存空间减少比如:静态集合没有释放:一直存在线程未释放:线程应该执行完了,但是没有释放事件监听:事件源都不存在了,还在监听例子:使用对应的文件流,字节流,但是没有释放该流,就会导致内存泄漏解决:释放流内存溢出:就是说内存不足了比如:一直创建新对象持久引用:集合一直添加但是没有被清除递归例子:ThreadLocal,每个线程都有一个ThreadLocal,本质就是每个线程存在一个ThreadLocalMap对象,key(弱引用)存入的是TreadLocal的实例,value(强引用)为自己指定的Object对象,如果没有使用该TreadLocal了,也就是说没有强引用指向TreadLocalMap对象,那么其中的key就会被设置为null,那如果该线程一直不结束,导致key不能被回收,随着key为null的情况增多就会导致内存溢出解决:使用TreadLocal.recome();1.10.会出现内存溢出的结构会出现该问题的内存结构:堆,栈,元空间,直接空间————————————————原文链接:https://blog.csdn.net/2402_88700528/article/details/148516238
-
1. Canvas 基础概念什么是 Canvas?HTML5 提供了 canvas元素,这是一个空白的矩形区域,可以使用 JavaScript 在上面绘制图形、图像和文本<canvas id="myCanvas" width="500" height="300"></canvas>获取 Canvas 绘图上下文要在 Canvas 上绘图,首先需要获取绘图上下文(context)。Canvas 支持不同的绘图上下文类型:2D 上下文 (CanvasRenderingContext2D):用于绘制 2D 图形,支持路径绘制、填充、描边、文本绘制、图像处理等。WebGL (WebGLRenderingContext):用于 3D 图形渲染,基于 OpenGL ES。示例:获取 2D 上下文<canvas id="myCanvas" width="500" height="300"></canvas><script> const canvas = document.getElementById("myCanvas"); const ctx = canvas.getContext("2d"); // 获取 2D 绘图上下文</script>2. 基本绘图操作绘制矩形ctx.fillStyle = "red"; // 设置填充颜色ctx.fillRect(50, 50, 100, 100); // 绘制填充矩形ctx.strokeStyle = "blue"; // 设置描边颜色ctx.strokeRect(200, 50, 100, 100); // 绘制描边矩形效果绘制路径(线段)ctx.beginPath(); // 开始路径ctx.moveTo(50, 200); // 起点ctx.lineTo(150, 250); // 第一条线ctx.lineTo(250, 200); // 第二条线ctx.closePath(); // 关闭路径ctx.stroke(); // 绘制路径 效果绘制圆形ctx.beginPath();ctx.arc(150, 150, 50, 0, Math.PI * 2); // 绘制一个圆弧路径ctx.fill(); // 填充路径ctx.arc(150, 150, 50, 0, Math.PI * 2)绘制一个圆弧路径。150, 150:圆心的坐标(x, y)。50:圆的半径。0:起始角度(弧度制),0表示从3点钟方向开始。Math.PI * 2:结束角度,Math.PI * 2表示完整的360度,即一个完整的圆。效果3. 图像与文本绘制绘制图片Canvas 可以绘制图片,通过 drawImage 方法将图片绘制到 Canvas 上const canvas = document.getElementById('myCanvas');const ctx = canvas.getContext('2d');const img = new Image();img.src = 'http://gips1.baidu.com/it/u=1658389554,617110073&fm=3028&app=3028&f=JPEG&fmt=auto'; // 替换为实际图像 URLimg.onload = () => { // 绘制原始大小 ctx.drawImage(img, 0, 0); // 绘制缩放后的图片 ctx.drawImage(img, 50, 50, 128, 96); // (image, x, y, width, height)}; 效果绘制文本通过 fillText 和 strokeText 方法,可以在 Canvas 上绘制文本。const canvas = document.getElementById('myCanvas');const ctx = canvas.getContext('2d');// 设置字体ctx.font = "30px Arial";ctx.fillStyle = '#FF0000';ctx.fillText("Hello, Canvas!", 50, 50);ctx.strokeText("Outlined Text", 50, 100);效果4. Canvas 动画基础动画实现Canvas 动画通常通过 requestAnimationFrame 进行帧更新。在 requestAnimationFrame 周期内,通过清除先前的 Canvas 内容并绘制新内容,可以实现流畅的动画效果const canvas = document.getElementById('myCanvas');const ctx = canvas.getContext('2d');let posX = 0;const posY = 150;function animate() { // 清除画布 ctx.clearRect(0, 0, canvas.width, canvas.height); // 绘制移动的方块 ctx.fillStyle = '#FF5733'; ctx.fillRect(posX, posY, 50, 50); // 更新位置 posX += 2; // 重置位置 if (posX > canvas.width) posX = -50; // 循环调用 requestAnimationFrame(animate);}animate(); // 启动动画5. Canvas 性能优化在使用 Canvas 进行绘图和动画时,性能优化尤为重要,尤其是在处理复杂图形和高频率动画时1. 减少重绘次数尽量只更新变化部分,避免整屏重绘。使用 requestAnimationFrame 来协调动画更新,避免不必要的渲染。function draw() { // 仅在需要时重绘 if (needsRedraw) { ctx.clearRect(0, 0, canvas.width, canvas.height); // 执行绘图操作 needsRedraw = false; } requestAnimationFrame(draw);}2. 使用离屏 Canvas利用离屏 Canvas 进行预绘,然后将结果绘制到主 Canvas 上,减少主线程的计算压力,提升渲染效率// 创建离屏 Canvasconst offscreenCanvas = document.createElement("canvas");const offCtx = offscreenCanvas.getContext("2d");// 在离屏 Canvas 上绘制offCtx.fillStyle = 'red';offCtx.fillRect(0, 0, 100, 100);// 将离屏 Canvas 的内容绘制到主 Canvasconst canvas = document.getElementById('myCanvas');const ctx = canvas.getContext('2d');ctx.drawImage(offscreenCanvas, 50, 50);3. 使用多个层 Canvas将复杂的静态背景和动态元素分层绘制,可以减少需要频繁更新的绘图区域,提升整体渲染效率<div style="position: relative;"> <!-- 静态背景 Canvas --> <canvas id="bgCanvas" width="600" height="400" style="position: absolute; z-index: 0;"></canvas> <!-- 动态元素 Canvas --> <canvas id="fgCanvas" width="600" height="400" style="position: absolute; z-index: 1;"></canvas></div><script> const bgCanvas = document.getElementById('bgCanvas'); const bgCtx = bgCanvas.getContext('2d'); // 绘制静态背景 bgCtx.fillStyle = '#EEEEEE'; bgCtx.fillRect(0, 0, bgCanvas.width, bgCanvas.height); const fgCanvas = document.getElementById('fgCanvas'); const fgCtx = fgCanvas.getContext('2d'); let posX = 0; const posY = 200; function animate() { fgCtx.clearRect(0, 0, fgCanvas.width, fgCanvas.height); fgCtx.fillStyle = '#FF0000'; fgCtx.fillRect(posX, posY, 50, 50); posX += 2; if (posX > fgCanvas.width) posX = -50; requestAnimationFrame(animate); } animate();</script>6. Canvas VS SVGSVG(Scalable Vector Graphics)是一种用来描述二维矢量图形的XML格式。它是一种基于文本的图像格式,支持交互和动画,广泛用于网页设计和开发中。在选择 Canvas 或 SVG 技术进行项目开发时,需要考虑到它们各自的特性、适用场景和项目需求Canvas 是基于像素的技术,适用于生成即时图形,在处理大量对象(如游戏中的图形)时,Canvas 通常表现得更好,因为它直接操作位图。但每次绘制都是对整个 Canvas 的再绘制,无法直接对单个元素进行操作SVG 是矢量图形技术,缩放时不会失去清晰度,适合高保真度的图像展示,每个元素都是独立可操作的 DOM 元素,可以添加事件、样式和脚本,但对于复杂图形,DOM 节点数可能较大特性 Canvas SVG绘制方式 基于像素的位图绘制 基于矢量的图形绘制适用场景 高性能实时渲染、大量动态图形、游戏等 需要可缩放、交互性高的静态图形、复杂的布局与样式DOM 结构 单一 <canvas>,绘图内容不在 DOM 中 每个元素都是 DOM 节点,易于操作和样式化性能 适合大量图形和高频率更新,性能较高 动态元素较多时性能可能下降,特别是复杂的 SVG 图形可访问性 需要额外处理,默认不可访问 元素可被屏幕阅读器等辅助技术识别,具备更好的可访问性可缩放性 失真 适用于缩放动画与交互 需要手动实现动画和交互逻辑 支持 CSS 动画、SVG 动画和事件处理如何选择?动态动画(游戏、数据可视化)➡ Canvas可缩放的矢量图(图标、交互 UI)➡ SVG本文提供了 Canvas 的核心知识点、基础绘图、动画处理及优化技巧。希望对你有所帮助!————————————————原文链接:https://blog.csdn.net/XH_jing/article/details/146223643
-
一、var 声明(一)定义与基本用法var 是 JavaScript 中较早期用于声明变量的关键字。使用 var 声明变量非常简单,只需要在变量名前加上 var 关键字即可。例如:var age; age = 25; // 或者可以在声明时直接赋值 var name = "John";(二)作用域var 声明的变量具有函数作用域或全局作用域。这意味着在函数内部使用 var 声明的变量,在整个函数内部都是可访问的,但在函数外部无法访问。例如:function exampleFunction() { var localVar = "I'm a local variable"; console.log(localVar); // 输出: I'm a local variable } console.log(localVar); // 这里会报错,因为 localVar 在函数外部不可访问如果在全局作用域(即不在任何函数内部)使用 var 声明变量,该变量将成为全局对象(在浏览器环境中是 window,在 Node.js 环境中是 global)的属性。例如:var globalVar = "I'm a global variable"; console.log(window.globalVar); // 输出: I'm a global variable(三)声明提升var 声明存在一个重要特性 —— 声明提升。这意味着在函数或全局作用域内,无论 var 声明的变量出现在何处,其声明都会被提升到作用域的顶部,但是赋值操作不会被提升。例如:console.log(num); // 输出: undefined var num = 10; //上述代码等价于: var num; console.log(num); // 输出: undefined num = 10;这种声明提升可能会导致一些意想不到的结果,特别是在代码结构复杂时。例如:function hoistingExample() { console.log(x); // 输出: undefined if (false) { var x = 10; } console.log(x); // 输出: undefined } hoistingExample();在这个例子中,虽然 if 块中的代码不会执行,但由于 var 的声明提升,x 的声明仍然被提升到函数顶部,所以第一次 console.log(x) 输出 undefined。而第二次输出 undefined 是因为 if 块内的赋值操作没有执行。(四)重复声明使用 var 可以对同一个变量进行多次声明,后面的声明会被忽略(但如果有赋值操作,会覆盖之前的值)。例如:var message = "Hello"; var message; console.log(message); // 输出: Hello var count = 5; var count = 10; console.log(count); // 输出: 10二、let 声明(一)定义与基本用法let 是 ES6 引入的用于声明变量的关键字。它的基本用法与 var 类似,在变量名前加上 let 即可声明变量。例如:let age; age = 30; // 或者声明时直接赋值 let name = "Jane";(二)作用域let 声明的变量具有块级作用域。块级作用域由一对花括号 {} 定义,包括 if 语句块、for 循环块、while 循环块等。在块级作用域内使用 let 声明的变量,仅在该块级作用域内有效。例如:if (true) { let localVar = "I'm a block - level local variable"; console.log(localVar); // 输出: I'm a block - level local variable } console.log(localVar); // 这里会报错,因为 localVar 在块外部不可访问与 var 的函数作用域相比,块级作用域更加精细,能更好地控制变量的生命周期和作用范围,减少变量污染全局作用域的风险。(三)不存在声明提升与暂时性死区let 声明不存在像 var 那样的声明提升。在使用 let 声明变量之前访问该变量会导致 ReferenceError 错误,这被称为 “暂时性死区”(TDZ)。在代码执行到 let 声明语句之前,该变量就已经存在于其作用域中了,但处于一种 “不可用” 的状态。只有当执行流到达声明语句时,变量才会被初始化,从而可以正常使用。例如:console.log(age); // 报错: ReferenceError: age is not defined let age = 28;在 let age = 28; 这行代码之前,age 处于暂时性死区,任何对它的访问都会触发错误。暂时性死区的存在,实际上是 JavaScript 引擎在解析代码时的一种机制。当遇到 let 声明时,引擎会在作用域中为该变量创建一个绑定,但此时变量处于未初始化状态。只有执行到声明语句本身时,变量才会被初始化并可以正常使用。这一特性使得开发者在编写代码时,对于变量的声明和使用顺序更加清晰,避免了因变量提升而导致的一些难以调试的问题。(四)不能重复声明在同一作用域内,使用 let 重复声明同一个变量会导致 SyntaxError 错误。例如:let count = 5; let count = 10; // 报错: SyntaxError: Identifier 'count' has already been declared这种限制有助于避免变量声明冲突,使代码更加清晰和可维护。三、const 声明(一)定义与基本用法const 同样是 ES6 引入的关键字,用于声明常量。常量一旦声明,其值就不能再被修改。声明常量的方式与 var 和 let 类似,在常量名前加上 const,并且必须在声明时进行初始化赋值。例如:const PI = 3.14159; const MAX_COUNT = 100;(二)作用域const 声明的常量具有块级作用域,与 let 相同。在块级作用域内声明的常量,仅在该块级作用域内有效。例如:if (true) { const localVar = "I'm a constant in a block"; console.log(localVar); // 输出: I'm a constant in a block } console.log(localVar); // 这里会报错,因为 localVar 在块外部不可访问(三)值的不可变性const 声明的常量值不能被重新赋值。尝试对常量重新赋值会导致 TypeError 错误。例如:const PI = 3.14159; PI = 3.14; // 报错: TypeError: Assignment to constant variable.需要注意的是,对于对象和数组类型的常量,虽然不能重新赋值整个对象或数组,但可以修改其内部属性或元素。例如:const person = { name: "Alice", age: 32 }; person.name = "Bob"; // 合法,对象属性可以修改 console.log(person.name); // 输出: Bob const numbers = [1, 2, 3]; numbers.push(4); // 合法,数组元素可以修改 console.log(numbers); // 输出: [1, 2, 3, 4]如果想要确保对象或数组的内容也不可变,可以使用 Object.freeze() 方法。例如:const frozenPerson = Object.freeze({ name: "Charlie", age: 25 }); frozenPerson.name = "David"; // 虽然不会报错,但实际上属性值并未改变 console.log(frozenPerson.name); // 输出: Charlie(四)不存在声明提升与暂时性死区与 let 一样,const 声明也不存在声明提升。在使用 const 声明常量之前访问该常量会导致 ReferenceError 错误,同样存在暂时性死区。在常量声明语句之前,该常量虽然在作用域中已经有了绑定,但处于未初始化状态,无法被访问和使用。例如:console.log(MAX_COUNT); // 报错: ReferenceError: MAX_COUNT is not defined const MAX_COUNT = 200;当代码执行到 const MAX_COUNT = 200; 时,常量 MAX_COUNT 才被初始化并可以正常使用。这与 let 声明的暂时性死区原理一致,都是为了让代码在变量(常量)的声明和使用上更加规范和可预测。四、var、let 和 const 的区别(一)作用域var:具有函数作用域或全局作用域。在函数内部声明的 var 变量在整个函数内有效,在全局作用域声明的 var 变量成为全局对象的属性。let:具有块级作用域。在块级作用域(如 if 块、for 循环块等)内声明的 let 变量仅在该块内有效,能更好地控制变量的作用范围,减少变量污染。const:同样具有块级作用域,与 let 类似,在声明它的块级作用域内有效。(二)声明提升var:存在声明提升,变量声明会被提升到作用域顶部,但赋值操作不会提升。这可能导致在变量声明之前访问它时得到 undefined 值,从而引发一些不易察觉的错误。let:不存在声明提升,在声明变量之前访问会导致 ReferenceError 错误,存在暂时性死区,使得代码在变量声明之前无法访问该变量,提高了代码的可预测性。const:也不存在声明提升,同样存在暂时性死区,在声明常量之前访问会导致 ReferenceError 错误。(三)可变性var:声明的变量可以被重新赋值,也可以在同一作用域内被重复声明(后面的声明会被忽略,有赋值时会覆盖之前的值)。let:声明的变量可以被重新赋值,但在同一作用域内不能重复声明,避免了变量声明冲突。const:声明的常量不能被重新赋值(对于对象和数组类型,虽然不能重新赋值整个对象或数组,但内部属性和元素可以修改,若要完全禁止修改,可使用 Object.freeze() 方法),并且在声明时必须初始化赋值。(四)使用场景建议var:由于其存在声明提升和函数作用域的特性,可能会导致一些代码理解和维护上的困难。在现代 JavaScript 开发中,var 的使用场景逐渐减少,一般仅在需要兼容非常旧的 JavaScript 环境(不支持 ES6 及以上特性)时才考虑使用。let:适用于需要在块级作用域内声明变量,并且变量值可能会发生变化的场景。例如在 for 循环中声明循环变量,或者在 if 块内声明临时变量等。const:用于声明那些值在整个程序运行过程中不会改变的常量,如数学常量(PI)、配置项(MAX_COUNT)等。对于对象和数组类型的常量,如果希望其内部内容也不可变,可结合 Object.freeze() 使用。 综上所述,var、let 和 const 在 JavaScript 中各自具有独特的特性和适用场景。作为新手开发者,深入理解它们之间的区别,并在实际编程中正确使用,将有助于编写更加规范、健壮和易于维护的 JavaScript 代码。随着对 JavaScript 语言的不断学习和实践,能够更加熟练地运用这三种声明方式来满足不同的编程需求。————————————————原文链接:https://blog.csdn.net/2403_87566238/article/details/146290516
-
前言 在物联网开发中,通常一个服务端会和很多设备进行交互,设备普遍也会有文件升级的需求。如果下发了一个升级任务到很多个设备,这些设备在收到下载指令后同一个时间段执行升级任务,从服务器拉取升级文件,则会有将服务器带宽占满导致网络阻塞。所以我认为这里需要一个队列来分批次的执行这些任务。下面是我对下载任务实现代码的一些见解。1.工厂模式 关于工厂模式,我推荐大家可以看一下张老师的讲解,链接如下:Java设计模式之创建型:工厂模式详解(简单工厂+工厂方法+抽象工厂)_简单工厂模式,工厂方法模式,抽象工厂模式-CSDN博客2.数据库的设计 我是这样理解的,因为任务下发后不会直接发送给设备。所以需要将下载任务的数据进行存储。所以需要用到数据库将下载的任务和数据存起来,由队列不断地去执行该表中的任务。我这里用到的是MySQL数据库,表设计如下:CREATE TABLE `kwd_download_queue` ( `id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL, `device_sn` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '设备编号', `type` int NULL DEFAULT NULL COMMENT '升级类型 具体的类型根据业务而定', `status` int NULL DEFAULT NULL COMMENT '任务状态 0 等待 1 执行 2 下载失败 3 下载成功', `create_time` datetime NULL DEFAULT NULL COMMENT '任务创建时间', `create_by` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '创建人', `data_info` longblob NULL COMMENT '升级的数据信息', `execute_time` datetime NULL DEFAULT NULL COMMENT '执行时间', PRIMARY KEY (`id`) USING BTREE) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC;3.代码实现 3.1 接口设计/** * 升级处理器接口,所有类型的升级处理器都需要实现该接口 */public interface UpgradeHandler { /** * 执行升级操作 * @param deviceSn 设备编号 * @param upgradeInfo 升级信息 * @return 操作结果,true表示成功,false表示失败 */ boolean executeUpgrade(String deviceSn, UpgradeInfo upgradeInfo); /** * 处理升级回调 * @param taskId 任务ID * @param success 是否成功 * @param message 回调消息 * @return 处理结果 */ boolean handleCallback(String taskId, boolean success, String message); /** * 获取处理器支持的升级类型 * @return 升级类型 */ int getUpgradeType();} /** * 升级信息内部类,用于存储在dataInfo字段中的JSON数据 */ @Data public static class UpgradeInfo { // 保留发送内容的原始JSON结构 private JsonNode sendContent; private String taskId; // 使用Map来存储动态的标识信息字段 @JsonIgnore private Map<String, Object> additionalProperties = new HashMap<>(); // 处理未知字段 @JsonAnySetter public void setAdditionalProperty(String name, Object value) { if (!"sendContent".equals(name)) { this.additionalProperties.put(name, value); } } @JsonAnyGetter public Map<String, Object> getAdditionalProperties() { return this.additionalProperties; } // 获取特定的标识信息 public Object getProperty(String key) { return additionalProperties.get(key); } // 获取sendContent作为字符串 public String getSendContentAsString() { try { return sendContent != null ? sendContent.toString() : null; } catch (Exception e) { return null; } } } 由于业务需求,在download_queue表中的dataInfo字段中,我添加了一些判断性的标识,是区分一些升级的类型的。sendContent中的内容是无需修改直接发送给设备的具体数据;additionalProperties是动态的附加属性,由于不同升级任务可能需要一些标识所以我偷懒使用了这个方法。 但是,这里设计的不太合理了,在此我建议大家扩充数据库字段进行标识,不要写在一个字段中,这样极其不易维护!!!我是纯因为懒才写到字段中用记忆的方式进行标识的哈哈哈。 好了,言归正传,因为不同的升级类型有不同的业务逻辑,需要不同的处理,所以需要先规范接口。让具体的升级类型的类去实现该接口。然后创建抽象类整合一些共有代码。3.2 抽象类设计import com.edison.device.entity.KwdDownloadQueue;import com.edison.device.mapper.KwdDownloadQueueMapper;import com.edison.device.service.impl.KwdDownloadQueueServiceImpl;import com.edison.device.upgrade.handler.UpgradeHandler;import lombok.extern.slf4j.Slf4j;import org.springframework.beans.factory.annotation.Autowired; /** * 升级处理器的抽象基类 * 实现通用逻辑,各具体处理器继承此类并实现特定逻辑 */@Slf4jpublic abstract class AbstractUpgradeHandler implements UpgradeHandler { @Autowired protected KwdDownloadQueueMapper downloadQueueMapper; @Override public boolean handleCallback(String taskId, boolean success, String message) { log.info("处理升级回调: taskId={}, success={}, message={}", taskId, success, message); // 查询任务 KwdDownloadQueue task = downloadQueueMapper.selectById(taskId); if (task == null) { log.error("任务不存在: {}", taskId); return false; } // 检查任务类型是否匹配 if (task.getType() != getUpgradeType()) { log.error("任务类型不匹配: expected={}, actual={}", getUpgradeType(), task.getType()); return false; } // 更新任务状态 int newStatus = success ? KwdDownloadQueue.DownloadStatus.COMPLETED.getCode() : KwdDownloadQueue.DownloadStatus.FAILED.getCode(); int updated = downloadQueueMapper.updateTaskStatus(taskId, newStatus); // 任务完成后的额外处理 if (updated > 0 && success) { onUpgradeSuccess(task); } else if (updated > 0 && !success) { onUpgradeFailed(task, message); } return updated > 0; } /** * 当升级成功时的后续处理 * 子类可以覆盖此方法实现特定逻辑 * * @param task 任务实体 */ protected void onUpgradeSuccess(KwdDownloadQueue task) { log.info("升级成功: taskId={}, deviceSn={}, type={}", task.getId(), task.getDeviceSn(), task.getType()); } /** * 当升级失败时的后续处理 * 子类可以覆盖此方法实现特定逻辑 * * @param task 任务实体 * @param errorMessage 错误信息 */ protected void onUpgradeFailed(KwdDownloadQueue task, String errorMessage) { log.warn("升级失败: taskId={}, deviceSn={}, type={}, error={}", task.getId(), task.getDeviceSn(), task.getType(), errorMessage); } /** * 记录升级日志 * 用于跟踪升级过程 * @param deviceSn 设备编号 * @param message 日志消息 */ protected void logUpgradeAction(String deviceSn, String message) { log.info("设备 {} 升级操作: {}", deviceSn, message); // 可以在此实现将日志写入数据库或其他存储 }} 之后使具体的不同升级类型的类去继承该抽象类实现代码,根据自己的业务去下发升级任务。3.3 具体任务类型的实现类@Component@Slf4jpublic class FileUpgradeHandler extends AbstractUpgradeHandler{ @Override public boolean executeUpgrade(String deviceSn, KwdDownloadQueueServiceImpl.UpgradeInfo upgradeInfo) { log.info("执行文件升级操作,设备:{},升级信息:{}", deviceSn, upgradeInfo); } @Override public int getUpgradeType() { return UpgradeTypes.FILE; // 具体的升级类型的标识,后续需要由工厂获取 } } 这是接口、抽象类和具体的实现类的基础代码,那么怎么使用呢?下面就要根据这些代码去创建一个工厂类。3.4 工厂类import com.edison.device.upgrade.handler.UpgradeHandler;import lombok.extern.slf4j.Slf4j;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Component; import javax.annotation.PostConstruct;import java.util.HashMap;import java.util.List;import java.util.Map; /** * 升级处理器工厂,负责根据升级类型获取对应的处理器 */@Slf4j@Componentpublic class UpgradeHandlerFactory { @Autowired private List<UpgradeHandler> upgradeHandlers; private final Map<Integer, UpgradeHandler> handlerMap = new HashMap<>(); /** * 初始化处理器映射 */ @PostConstruct public void init() { for (UpgradeHandler handler : upgradeHandlers) { handlerMap.put(handler.getUpgradeType(), handler); log.info("注册升级处理器:type={}, handler={}", handler.getUpgradeType(), handler.getClass().getSimpleName()); } } /** * 根据升级类型获取处理器 * @param upgradeType 升级类型 * @return 对应的处理器,如果不存在则返回null */ public UpgradeHandler getHandler(int upgradeType) { UpgradeHandler handler = handlerMap.get(upgradeType); if (handler == null) { log.error("未找到类型为{}的升级处理器", upgradeType); } return handler; }} 在项目启动时,所有继承AbstractUpgradeHandler的Bean对象都会被自动注入到upgradeHandlers属性中,然后通过init方法初始化处理器映射对象handlerMap,这样后续就可以通过方法getHandler根据不同的升级类型去调用不同的处理类进行任务的下发。3.5 队列工具类(队列实体服务层代码)import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;import com.edison.common.core.utils.bean.BeanUtils;import com.edison.device.domain.vo.DownloadQueueStats;import com.edison.device.domain.vo.DownloadQueueVO;import com.edison.device.entity.po.KwdDownloadQueue;import com.edison.device.mapper.KwdDownloadQueueMapper;import com.edison.device.service.KwdDownloadQueueService;import com.edison.device.upgrade.factory.UpgradeHandlerFactory;import com.edison.device.upgrade.handler.UpgradeHandler;import com.edison.device.utils.UpgradeUtils;import com.fasterxml.jackson.annotation.JsonAnyGetter;import com.fasterxml.jackson.annotation.JsonAnySetter;import com.fasterxml.jackson.annotation.JsonIgnore;import com.fasterxml.jackson.core.JsonProcessingException;import com.fasterxml.jackson.databind.JsonNode;import com.fasterxml.jackson.databind.ObjectMapper;import lombok.Data;import lombok.extern.slf4j.Slf4j;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.beans.factory.annotation.Value;import org.springframework.stereotype.Service;import org.springframework.transaction.annotation.Transactional; import java.util.*;import java.util.concurrent.ConcurrentHashMap;import java.util.concurrent.Executor;import java.util.concurrent.Executors; /** * (KwdDownloadQueue)表服务实现类 * */@Service@Slf4jpublic class KwdDownloadQueueServiceImpl extends ServiceImpl<KwdDownloadQueueMapper, KwdDownloadQueue> implements KwdDownloadQueueService { @Autowired private KwdDownloadQueueMapper downloadQueueMapper; @Autowired private UpgradeHandlerFactory upgradeHandlerFactory; @Autowired private ObjectMapper objectMapper; @Value("${download.concurrent.limit:20}") private int concurrentLimit; // 用于跟踪正在处理的任务,确保同一任务不被重复处理 private final ConcurrentHashMap<String, Boolean> processingTasks = new ConcurrentHashMap<>(); // 线程池用于异步执行下载任务 private final Executor downloadExecutor = Executors.newFixedThreadPool(10); /** * 创建新的下载任务 * 将VO转换为实体,设置初始状态为等待,并保存到数据库 * * @param downloadQueueVO 下载队列视图对象,包含设备编号、升级类型、创建人等信息 * @return 创建的任务ID */ @Override public String createDownloadTask(DownloadQueueVO downloadQueueVO) { KwdDownloadQueue downloadQueue = new KwdDownloadQueue(); BeanUtils.copyProperties(downloadQueueVO, downloadQueue); // 设置初始状态 downloadQueue.setStatus(KwdDownloadQueue.DownloadStatus.WAITING.getCode()); downloadQueue.setCreateTime(new Date()); // 序列化升级信息 try { if (downloadQueueVO.getUpgradeInfo() != null) { downloadQueue.setDataInfo(objectMapper.writeValueAsString(downloadQueueVO.getUpgradeInfo())); } } catch (JsonProcessingException e) { log.error("序列化升级信息时出错", e); throw new RuntimeException("创建下载任务时出错", e); } save(downloadQueue); log.info("已创建ID为的下载任务: {}", downloadQueue.getId()); return downloadQueue.getId(); } /** * 批量创建下载任务 * 适用于需要同时给多个设备下发升级指令的场景 * * @param downloadQueueVOList 下载队列视图对象列表 * @return 成功创建的任务数量 */ @Override @Transactional(rollbackFor = Exception.class) public int batchCreateDownloadTasks(List<DownloadQueueVO> downloadQueueVOList) { List<KwdDownloadQueue> downloadQueueList = new ArrayList<>(); for (DownloadQueueVO vo : downloadQueueVOList) { KwdDownloadQueue downloadQueue = new KwdDownloadQueue(); BeanUtils.copyProperties(vo, downloadQueue); // 设置初始状态和ID downloadQueue.setStatus(KwdDownloadQueue.DownloadStatus.WAITING.getCode()); downloadQueue.setCreateTime(new Date()); // 序列化升级信息 try { if (vo.getUpgradeInfo() != null) { downloadQueue.setDataInfo(objectMapper.writeValueAsString(vo.getUpgradeInfo())); } } catch (JsonProcessingException e) { log.error("序列化设备的升级信息时出错: {}", vo.getDeviceSn(), e); // 继续处理其他任务 continue; } downloadQueueList.add(downloadQueue); } // 批量保存 if (!downloadQueueList.isEmpty()) { saveBatch(downloadQueueList); log.info("批量创建 [{}] 个下载任务", downloadQueueList.size()); return downloadQueueList.size(); } return 0; } /** * 开始处理等待中的任务 * 根据配置的并发数,从等待队列中选取任务开始执行 * 确保同时执行的任务数不超过并发限制 * * @param batchSize 每次处理的批次大小,控制一次最多处理多少个任务 * @return 本次开始处理的任务数量 */ @Override// @Transactional(rollbackFor = Exception.class) //根据业务需求添加 public int processWaitingTasks(int batchSize) { // 获取当前正在执行的任务数 LambdaQueryWrapper<KwdDownloadQueue> queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.eq(KwdDownloadQueue::getStatus, KwdDownloadQueue.DownloadStatus.EXECUTING.getCode()); long executingCount = count(queryWrapper); // 计算可以新启动的任务数 long availableSlots = Math.max(0, concurrentLimit - executingCount); if (availableSlots <= 0) { log.debug("没有可用于新下载任务的插槽。当前执行: {}", executingCount); return 0; } // 获取等待中的任务 long tasksToProcess = Math.min(availableSlots, batchSize); List<KwdDownloadQueue> waitingTasks = downloadQueueMapper.findWaitingTasks(tasksToProcess); int processedCount = 0; for (KwdDownloadQueue task : waitingTasks) { // 检查同一设备是否已有正在执行的任务 int deviceExecutingCount = downloadQueueMapper.countExecutingTasksByDevice(task.getDeviceSn()); if (deviceExecutingCount > 0) { log.debug("设备 {} 已具有正在执行的任务,跳过", task.getDeviceSn()); continue; } // 更新任务状态为执行中 boolean updated = updateTaskStatus(task.getId(), KwdDownloadQueue.DownloadStatus.EXECUTING.getCode()); if (!updated) { log.warn("未能更新任务的任务状态: {}", task.getId()); continue; } // 异步处理任务 final String taskId = task.getId(); // 获取升级类型示例,这里并未使用 String type = null; if (task.getDataInfo() != null && !task.getDataInfo().isEmpty()) { try { KwdDownloadQueueServiceImpl.UpgradeInfo upgradeInfo = objectMapper.readValue(task.getDataInfo(), KwdDownloadQueueServiceImpl.UpgradeInfo.class); type = UpgradeUtils.getUpgradeType(upgradeInfo); } catch (JsonProcessingException e) { log.error("解析升级信息时出错: {}", task.getId(), e); } } String finalType = type; downloadExecutor.execute(() -> { try { processingTasks.put(taskId, true); boolean success = processTask(taskId); // 如果发送指令失败,才标记为失败 if (!success) { updateTaskStatus(taskId, KwdDownloadQueue.DownloadStatus.FAILED.getCode()); } } catch (Exception e) { log.error("处理任务时出错: {}", taskId, e); updateTaskStatus(taskId, KwdDownloadQueue.DownloadStatus.FAILED.getCode()); } finally { processingTasks.remove(taskId); } }); processedCount++; } log.info("已开始处理 {} 个等待任务", processedCount); return processedCount; } /** * 处理单个任务,执行实际的下载操作 * 根据任务类型调用不同的升级处理器执行具体升级逻辑 * * @param taskId 任务ID * @return 处理结果,true表示成功,false表示失败 */ @Override public boolean processTask(String taskId) { // 1. 获取任务详情 KwdDownloadQueue task = getById(taskId); if (task == null) { log.warn("未找到任务: {}", taskId); return false; } // 2. 验证任务状态 - 只处理"执行中"状态的任务 if (task.getStatus() != KwdDownloadQueue.DownloadStatus.EXECUTING.getCode()) { log.warn("任务 {} 未处于执行状态", taskId); return false; } log.info("正在处理下载任务 {},类型 {},设备 {}", taskId, task.getType(), task.getDeviceSn()); try { // 3. 解析升级信息 (JSON数据转对象) KwdDownloadQueueServiceImpl.UpgradeInfo upgradeInfo = null; if (task.getDataInfo() != null && !task.getDataInfo().isEmpty()) { upgradeInfo = objectMapper.readValue(task.getDataInfo(), KwdDownloadQueueServiceImpl.UpgradeInfo.class); } if (upgradeInfo == null) { log.error("任务的升级信息无效: {}", taskId); return false; } // 4. 获取对应类型的升级处理器 (策略模式) UpgradeHandler handler = upgradeHandlerFactory.getHandler(task.getType()); if (handler == null) { log.error("找不到任务升级类型 {} 的处理程序: {}", task.getType(), taskId); return false; } // 5. 执行具体的升级操作 upgradeInfo.setAdditionalProperty("taskId", taskId); // 设置任务ID upgradeInfo.setTaskId(taskId); boolean result = handler.executeUpgrade(task.getDeviceSn(), upgradeInfo); log.info("任务 {} 已处理,并返回结果: {}", taskId, result); return result; } catch (Exception e) { log.error("处理任务时出错: {}", taskId, e); return false; } } /** * 更新任务状态 * 用于手动更新任务状态或任务执行完成后更新状态 * * @param taskId 任务ID * @param status 新状态值,对应DownloadQueue.DownloadStatus枚举 * @return 更新是否成功 */ @Override public boolean updateTaskStatus(String taskId, int status) { int updated = downloadQueueMapper.updateTaskStatus(taskId, status); return updated > 0; } /** * 获取设备的任务历史 * 查询指定设备的所有下载任务记录,按时间倒序排列 * * @param deviceSn 设备编号 * @param limit 限制返回的记录数量 * @return 任务历史列表 */ @Override public List<KwdDownloadQueue> getDeviceTaskHistory(String deviceSn, int limit) { return downloadQueueMapper.findDeviceTaskHistory(deviceSn, limit); } /** * 检查并处理超时任务 * 将长时间处于执行状态但未完成的任务标记为失败 * * @param timeoutMinutes 超时时间(分钟),超过这个时间仍未完成的任务会被标记为失败 * @return 处理的超时任务数量 */ @Override @Transactional(rollbackFor = Exception.class) public int checkAndHandleTimeoutTasks(int timeoutMinutes) { List<KwdDownloadQueue> timeoutTasks = downloadQueueMapper.findTimeoutTasks(timeoutMinutes); int handled = 0; for (KwdDownloadQueue task : timeoutTasks) { // 避免处理正在被处理的任务 if (processingTasks.containsKey(task.getId())) { continue; } log.warn("找到超时任务:{},标记为失败", task.getId()); boolean updated = updateTaskStatus(task.getId(), KwdDownloadQueue.DownloadStatus.FAILED.getCode()); if (updated) { if (task.getType() == 0){ // 获取升级类型 String type = null; if (task.getDataInfo() != null && !task.getDataInfo().isEmpty()) { try { KwdDownloadQueueServiceImpl.UpgradeInfo upgradeInfo = objectMapper.readValue(task.getDataInfo(), KwdDownloadQueueServiceImpl.UpgradeInfo.class); type = UpgradeUtils.getUpgradeType(upgradeInfo); } catch (JsonProcessingException e) { log.error("解析升级信息时出错: {}", task.getId(), e); } } } handled++; } } log.info("已处理[{}]个超时任务", handled); return handled; } /** * 获取下载队列统计信息 * 统计不同状态的任务数量,用于监控和展示 * * @return 队列统计信息对象 */ @Override public DownloadQueueStats getQueueStats() { DownloadQueueStats stats = new DownloadQueueStats(); LambdaQueryWrapper<KwdDownloadQueue> waitingQuery = new LambdaQueryWrapper<>(); waitingQuery.eq(KwdDownloadQueue::getStatus, KwdDownloadQueue.DownloadStatus.WAITING.getCode()); stats.setWaitingTasks(count(waitingQuery)); LambdaQueryWrapper<KwdDownloadQueue> executingQuery = new LambdaQueryWrapper<>(); executingQuery.eq(KwdDownloadQueue::getStatus, KwdDownloadQueue.DownloadStatus.EXECUTING.getCode()); stats.setExecutingTasks(count(executingQuery)); LambdaQueryWrapper<KwdDownloadQueue> failedQuery = new LambdaQueryWrapper<>(); failedQuery.eq(KwdDownloadQueue::getStatus, KwdDownloadQueue.DownloadStatus.FAILED.getCode()); stats.setFailedTasks(count(failedQuery)); LambdaQueryWrapper<KwdDownloadQueue> completedQuery = new LambdaQueryWrapper<>(); completedQuery.eq(KwdDownloadQueue::getStatus, KwdDownloadQueue.DownloadStatus.COMPLETED.getCode()); stats.setCompletedTasks(count(completedQuery)); return stats; } /** * 重试失败的任务 * 将指定的失败任务重新设置为等待状态,等待重新执行 * * @param taskId 任务ID * @return 操作是否成功 */ @Override public boolean retryFailedTask(String taskId) { return false; } /** * 取消等待中的任务 * 将等待中的任务删除或标记为已取消 * * @param taskId 任务ID * @return 操作是否成功 */ @Override public boolean cancelWaitingTask(String taskId) { return false; } /** * 获取设备当前正在执行的任务数 * 用于判断设备是否有正在进行的升级任务 * * @param deviceSn 设备编号 * @return 执行中的任务数 */ @Override public int getDeviceExecutingTaskCount(String deviceSn) { return 0; } /** * 清理历史任务 * 删除或归档指定时间之前的已完成/失败任务 * * @param daysBefore 天数,删除多少天之前的历史任务 * @return 清理的记录数 */ @Override public int cleanHistoryTasks(int daysBefore) { return 0; } /** * 升级信息类,用于存储在dataInfo字段中的JSON数据 */ @Data public static class UpgradeInfo { // 保留发送内容的原始JSON结构 private JsonNode sendContent; private String taskId; // 使用Map来存储动态的标识信息字段 @JsonIgnore private Map<String, Object> additionalProperties = new HashMap<>(); // 处理未知字段 @JsonAnySetter public void setAdditionalProperty(String name, Object value) { if (!"sendContent".equals(name)) { this.additionalProperties.put(name, value); } } @JsonAnyGetter public Map<String, Object> getAdditionalProperties() { return this.additionalProperties; } // 获取特定的标识信息 public Object getProperty(String key) { return additionalProperties.get(key); } // 获取sendContent作为字符串 public String getSendContentAsString() { try { return sendContent != null ? sendContent.toString() : null; } catch (Exception e) { return null; } } }} 这一块代码会比较杂,因为不便展示,我还删除了一些相关的业务代码,每个方法上都有注释,解释了方法的作用,因为有些接口我并没有使用的需求,所以我没有实现该接口与的完整方法,可以根据自己的业务来修改该方法。3.6 相关实体类import lombok.Data; /** * 下载队列统计数据 * 用于统计不同状态任务的数量,提供给前端展示或监控系统使用 */@Datapublic class DownloadQueueStats { /** * 等待中的任务数 */ private long waitingTasks; /** * 执行中的任务数 */ private long executingTasks; /** * 失败的任务数 */ private long failedTasks; /** * 已完成的任务数 */ private long completedTasks; /** * 获取总任务数 * @return 所有状态任务的总和 */ public long getTotalTasks() { return waitingTasks + executingTasks + failedTasks + completedTasks; } /** * 获取活跃任务数(等待中+执行中) * @return 活跃任务数量 */ public long getActiveTasks() { return waitingTasks + executingTasks; } /** * 获取完成率 * @return 完成率百分比 */ public double getCompletionRate() { long total = getTotalTasks(); return total > 0 ? (double) completedTasks / total * 100 : 0; } /** * 获取失败率 * @return 失败率百分比 */ public double getFailureRate() { long total = getTotalTasks(); return total > 0 ? (double) failedTasks / total * 100 : 0; }} import lombok.Data;import javax.validation.constraints.NotBlank;import javax.validation.constraints.NotNull;import java.util.Map; /*** 任务创建**/@Datapublic class DownloadQueueVO { /** * 设备编号 */ @NotBlank(message = "设备编号不能为空") private String deviceSn; /** * 类型:0系统升级 1文件升级 2背景图升级 3启动词升级 */ @NotNull(message = "升级类型不能为空") private Integer type; /** * 创建人 */ private String createBy; /** * 升级信息,包含url、版本号、MD5等 */ @NotNull(message = "升级信息不能为空") private Map<String, Object> upgradeInfo;} import java.util.Date;import java.io.Serializable; import com.baomidou.mybatisplus.annotation.IdType;import com.baomidou.mybatisplus.annotation.TableId;import lombok.Data;import io.swagger.annotations.ApiModel;import io.swagger.annotations.ApiModelProperty;import com.fasterxml.jackson.annotation.JsonFormat;import lombok.Getter;import org.springframework.format.annotation.DateTimeFormat; /** * @author * @since */@Data@ApiModel("下载队列实体类")public class KwdDownloadQueue implements Serializable { @ApiModelProperty(value = "${column.comment}") @TableId(type = IdType.ASSIGN_ID) private String id; /** * 设备编号 */ @ApiModelProperty(value = "设备编号") private String deviceSn; /** * 类型 0系统升级 1文件升级.... */ @ApiModelProperty(value = "类型 0系统升级 1文件升级....") private Integer type; /** * 任务状态 0 等待 1 执行 2 升级失败 3 升级成功 */ @ApiModelProperty(value = "任务状态 0 等待 1执行 2 失败 3历史") private Integer status; /** * 任务创建时间 */ @ApiModelProperty(value = "任务创建时间") @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss") @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") private Date createTime; /** * 创建人 */ @ApiModelProperty(value = "创建人") private String createBy; /** * 升级信息 */ @ApiModelProperty(value = "升级信息") private String dataInfo; /** * 执行时间 */ @ApiModelProperty(value = "执行时间") @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss") @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") private Date executeTime; /** * 任务类型枚举 */ public enum DownloadType { SYSTEM_UPGRADE(0, "系统升级"), FILE_UPGRADE(1, "文件升级"), private final int code; private final String desc; DownloadType(int code, String desc) { this.code = code; this.desc = desc; } public int getCode() { return code; } public String getDesc() { return desc; } } /** * 任务状态枚举 */ @Getter public enum DownloadStatus { WAITING(0, "等待中"), EXECUTING(1, "执行中"), FAILED(2, "失败"), COMPLETED(3, "完成"); private final int code; private final String desc; DownloadStatus(int code, String desc) { this.code = code; this.desc = desc; } }}3.7 定时任务执行器import com.edison.device.service.KwdDownloadQueueService;import lombok.extern.slf4j.Slf4j;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.beans.factory.annotation.Value;import org.springframework.scheduling.annotation.Scheduled;import org.springframework.stereotype.Component; import java.util.concurrent.atomic.AtomicBoolean; /** * 下载队列调度器,定期处理等待中的任务和检查超时任务 */@Slf4j@Componentpublic class DownloadQueueScheduler { @Autowired private KwdDownloadQueueService downloadQueueService; @Value("${download.batch.size:15}") private int batchSize; @Value("${download.timeout.minutes:30}") private int timeoutMinutes; // 防止任务重叠执行的标志 private final AtomicBoolean processingFlag = new AtomicBoolean(false); private final AtomicBoolean timeoutCheckFlag = new AtomicBoolean(false); /** * 定期处理等待中的任务 * 每30秒执行一次 */ @Scheduled(fixedDelayString = "${download.process.interval:30000}") public void processWaitingTasks() { // 如果已经有一个处理任务在执行,则跳过本次执行 if (!processingFlag.compareAndSet(false, true)) { log.debug("另一个处理任务正在执行,跳过本次调度"); return; } try { log.info("开始处理等待中的下载任务,批次大小:{}", batchSize); int processed = downloadQueueService.processWaitingTasks(batchSize); log.info("本次处理了{}个下载任务", processed); } catch (Exception e) { log.error("处理下载任务时发生异常", e); } finally { // 重置标志,允许下次执行 processingFlag.set(false); } } /** * 定期检查超时任务 * 每5分钟执行一次 */ @Scheduled(fixedDelayString = "${download.timeout.check.interval:300000}") public void checkTimeoutTasks() { // 如果已经有一个超时检查在执行,则跳过本次执行 if (!timeoutCheckFlag.compareAndSet(false, true)) { log.debug("另一个超时检查正在执行,跳过本次调度"); return; } try { log.info("开始检查超时任务,超时时间:{}分钟", timeoutMinutes); int handled = downloadQueueService.checkAndHandleTimeoutTasks(timeoutMinutes); log.info("处理了{}个超时任务", handled); } catch (Exception e) { log.error("检查超时任务时发生异常", e); } finally { // 重置标志,允许下次执行 timeoutCheckFlag.set(false); } }} 这里使用了定时任务,扫表获取待执行的任务再通过不同的任务执行器去执行任务;由于任务可能长时间没有反馈,需要有一个超时扫表任务修改任务状态来防止一些任务没有及时反馈造成队列阻塞。3.8 设备升级回调接口import com.edison.common.core.domain.Result;import com.edison.device.entity.po.KwdDownloadQueue;import com.edison.device.service.KwdDownloadQueueService;import com.edison.device.upgrade.factory.UpgradeHandlerFactory;import com.edison.device.upgrade.handler.UpgradeHandler;import io.swagger.annotations.Api;import io.swagger.annotations.ApiOperation;import lombok.Data;import lombok.extern.slf4j.Slf4j;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.web.bind.annotation.PostMapping;import org.springframework.web.bind.annotation.RequestBody;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController; @Slf4j@RestController@RequestMapping("/upgrade/callback")@Api(tags = "设备升级回调接口")public class UpgradeCallbackController { @Autowired private KwdDownloadQueueService downloadQueueService; @Autowired private UpgradeHandlerFactory upgradeHandlerFactory; /** * 接收设备升级结果回调 * @param callback 回调信息 * @return 处理结果 */ @PostMapping @ApiOperation("设备升级结果回调") public Result<Boolean> handleCallback(@RequestBody UpgradeCallback callback) { log.info("收到设备升级回调:taskId={}, deviceSn={}, success={}, message={}", callback.getTaskId(), callback.getDeviceSn(), callback.isSuccess(), callback.getMessage()); try { // 查询任务 KwdDownloadQueue task = downloadQueueService.getById(callback.getTaskId()); if (task == null) { log.error("任务不存在:{}", callback.getTaskId()); return Result.error("任务不存在"); } // 验证设备编号 if (!task.getDeviceSn().equals(callback.getDeviceSn())) { log.error("设备编号不匹配:expected={}, actual={}", task.getDeviceSn(), callback.getDeviceSn()); return Result.error("设备编号不匹配"); } // 获取对应的升级处理器 UpgradeHandler handler = upgradeHandlerFactory.getHandler(task.getType()); if (handler == null) { log.error("未找到对应的升级处理器:type={}", task.getType()); return Result.error("未找到对应的升级处理器"); } // 处理回调 boolean result = handler.handleCallback( callback.getTaskId(), callback.isSuccess(), callback.getMessage()); return result ? Result.OK("回调处理成功", true) : Result.error("回调处理失败"); } catch (Exception e) { log.error("处理升级回调时发生异常", e); return Result.error("处理回调异常:" + e.getMessage()); } } /** * 升级回调信息 */ @Data public static class UpgradeCallback { /** * 任务ID */ private String taskId; /** * 设备编号 */ private String deviceSn; /** * 是否成功 */ private boolean success; /** * 回调消息 */ private String message; }}AI写代码 这里就需要所对接的设备(安卓或者硬件)去向服务端汇报升级结果,然后去修改队列中的升级状态。如果有需要修改设备表的升级状态的需求,可以在此类中添加自己的业务逻辑。但是在此功能开发完毕进入生产环境后,我发现了一个问题,如果升级信息的数据量特别大的情况下,即使表中的记录数不多,下载队列的那张表会检索的特别特别慢!!!然后我寻思着修改数据库中的字段类型为longblob(原来是longtext类型),依然不起作用。然后我暂时没有做处理,目前我所能想到的解决方法只有分表进行处理,将data_info这个字段牵出去用两张表来维护此功能。如果各位老师有更好的办法欢迎留言!————————————————原文链接:https://blog.csdn.net/YyyGxxx/article/details/148327760
-
查找字母出现的次数这道题的思路在后面的题目过程中能用到,所以先把这题给写出来题目要求:给出一个字符串数组,要求输出结果为其中每个字符串及其出现次数。思路:我们可以把数组里的字符串按顺序放进map中,对于没被放进去过的字符串,放进去次数为1,之前被放进过去的字符串,那就在其上重新放入,并把次数重新加1.举个例子,输出的内容是:"this", "dog", "cat", "cat", "this", "dog"现在是把每个元素放进去,在没遇到一样数据之前的过程,如是上面所示,如果遇到了一样的数据, 这个操作看起来可能是把第二个cat放进去了,但是实际上是把cat重新输入了,然后把Key值输入为2了。因为map其中节点的样子如上图所示。代码部分如下import java.util.HashMap;import java.util.Map;import java.util.Set; public class Test { public static Map<String, Integer> countWords(String[] words){ Map<String, Integer> map = new HashMap<>(); for(String word : words){ if(map.get(word) == null){ map.put(word, 1); }else { int val = map.get(word); map.put(word, val+1); } } return map; } public static void main(String[] args) { String[] words = {"this", "dog", "cat", "cat", "this", "dog"}; Map<String, Integer> map = countWords(words); Set<Map.Entry<String, Integer>> entryset = map.entrySet(); for (Map.Entry<String, Integer> entry : entryset){ System.out.println("Key: " + entry + " Val: " + entry.getKey()); } }}只出现一次的数字题目链接:只出现一次的数字 - 力扣(LeetCode)题目描述:给一个 非空 整数数组 nums ,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。思路:这里的思路和上面的 查找字母出现的次数 有些像。依次把元素放到set中,如果set中没有该元素,就把该元素放进去,如果有,就把这个元素从set中删去。最后输出set中的元素以 {1,2,3,4,1,2,3} 为例,当第一次往里放,没有遇到重复的元素时,如下图按照数组的顺序,接着向下放,就会遇到重复的元素,这时候就要把set中的元素给删除了 后面的2,3也要依次从set中删除。public static int singleNumber(int[] nums){ HashSet<Integer> set = new HashSet<>(); for (int i = 0; i < nums.length; i++) { if(set.contains(nums[i])){ set.remove(nums[i]); }else{ set.add(nums[i]); } } for (int i = 0; i < nums.length; i++) { if(set.contains(nums[i])){ return nums[i]; } } return -1; } public static void main(String[] args) { int[] array = {1,2,3,4,1,2,3}; System.out.println(singleNumber(array)); }运行结果如下坏键盘打字题目链接:题目描述:旧键盘上坏了几个键,于是在敲一段文字的时候,对应的字符就不会出现。现在给出应该输入的一段文字、以及实际被输入的文字,请你列出肯定坏掉的那些键。输入在两行中分别给出应该输入的文字、以及实际输入的文字按照发现顺序,在一行中输出坏掉的键。其中英语字母只输出大写,每个坏键只输入一次。示例输入7_This_is_a_test_hs_s_a_es输出7TI题目思路:该题的思路在于如何找出坏键,这里提供一种思路,先把实际输入的数据放到set中,然后再把应该输入的文字遍历一遍,如果其中有set中没有的数据,那些没有的数据便是坏掉的键。public static void func(String str1, String str2){ //将字符串大写 str1 = str1.toUpperCase(); str2 = str2.toUpperCase(); HashSet<Character> setAct = new HashSet<>(); for (int i = 0; i < str2.length(); i++) { char ch = str2.charAt(i); setAct.add(ch); } for (int i = 0; i < str1.length(); i++) { char ch = str1.charAt(i); if(!setAct.contains(ch)){ System.out.print(ch); } } } public static void main(String[] args) { func("7_This_is_a_test", "_hs_s_a_es"); }这样的代码还是存在问题,没办法把其中重复出现的元素给消去,输出的结果是现在问题变成了如何去重,这部分不难能想到,我们可以创建一个setBroken来存放已经查找到的坏键,如果set和setBroken中都没有这个元素才打印.public class Test { public static void func(String str1, String str2){ str1 = str1.toUpperCase(Locale.ROOT); str2 = str2.toUpperCase(Locale.ROOT); HashSet<Character> setAct = new HashSet<>(); for (int i = 0; i < str2.length(); i++) { char ch = str2.charAt(i); setAct.add(ch); } //第一步是把不同的数给挑出来,然后对于重复输出的数据给去重 HashSet<Character> setBroken = new HashSet<>(); for (int i = 0; i < str1.length(); i++) { char ch = str1.charAt(i); if(!setAct.contains(ch) && !setBroken.contains(ch)){ setBroken.add(ch); System.out.print(ch); } } } public static void main(String[] args) { func("7_This_is_a_test", "_hs_s_a_es"); }}AI写代码输出结果为这次的内容就到这里,我们下篇文章再见————————————————原文链接:https://blog.csdn.net/xiaochuan_bsj/article/details/143368533
-
栈的概念(Stack)栈是常见的线性数据结构,栈的特点是以先进后出的形式,后进先出,先进后出,分为栈底和栈顶栈应用于内存的分配,表达式求值,存储临时的数据和方法的调用等。例如这把枪,第一发子弹是最后发射的,第一发子弹在栈底,而最新安装上去的子弹在栈的顶部,只有将上面的子弹打完(栈顶的数据走完),最后一发子弹才会射出栈的实现栈的实现是基于简单的数组形成的,我们可以将它想象成连续的数组,而栈的顺序是由后放到前放模拟实现栈的方法:push(放入一个元素到栈中)pop(提取栈顶的一个元素,并将在其栈中消除)peek(查看栈顶的元素)size(查看栈中的大小)empty(栈中是否为空)full(栈是否满了)代码import java.util.Arrays;public class MyStack implements IStack { private int[] elem; private int top;//数组的栈顶,以及数组栈中存放元素的数量 private static final int DEFAULT_CAPACITY = 10;//这里是初始容量 public MyStack() { elem = new int[DEFAULT_CAPACITY]; top = -1;//数组下标从0开始 } @Override public void push(int item) { if (full()) { //如果满了就扩容 elem = Arrays.copyOf(elem, 2 * elem.length); } elem[++top] = item; } @Override public int pop() throws RuntimeException { try { if (empty()) { throw new RuntimeException("栈为空"); } } catch (RuntimeException e) { e.printStackTrace(); } return elem[top--];//return返回后删除栈顶的元素 } @Override public int peek() { if (empty()) { throw new RuntimeException("栈为空"); } return elem[top];//返回栈顶元素 } @Override public int size() { return top+1;//去除数组0 } @Override public boolean empty() { return top == -1; } @Override public boolean full() { return top == elem.length;//count==当前的elem的总长度为true }}队列(Queue)队列是由先进先出的线性数据结构,采用的是先进先出,后进后出,如果要插入元素的话就是从入队尾巴方向插入,而删除作为出队要在头尾删除。队列的方法模拟实现队列(双链表实现)public class MyQueue implements IQueue{ static class Queue{ public int elem; public Queue next; public Queue prev; public Queue(int elem) { this.elem = elem; } } public Queue head; public Queue last; public int size=0; @Override public void offer(int val) { Queue queue = new Queue(val); if (this.head == null) { this.head = queue; this.last = queue; ++size; return; } this.last.next=queue; this.last.prev=this.head; this.last=last.next; ++size; } @Override public int poll() { if(this.head==null){ throw new RuntimeException("没有要丢掉的队列"); } Queue cur =this.head; if(this.head.next==null){ return -1; } this.head=this.head.next; this.head.prev=null; size--; return cur.elem; } @Override public int peek() { if(this.head!=null){ return this.head.elem; } return 0; } @Override public int size() { return size; }}循环队列(循环数组实现)数组实现队列的循环需要引入一个公式(目前的下标值+1)%当前数组的长度(index+1)%array.length,下标值从0开始少一个数,当index+1就是当前的总长度时,公式后的值一定为下标0。 private int[] array; private int front; private int rear; public MyCircularQueue(int k) { array=new int[k+1]; front=0;//初始位置 rear=0; } public boolean enQueue(int value) { //入列 if(isFull()){ //这里如果容量已经满了,需要先删除后在进行插入 return false; } array[rear]=value;//rear下标获取元素 rear=(rear+1)%array.length;//rear最终循环为0下标 return true; } public boolean deQueue() { //出列 if(isEmpty()){ //为空返回false return false; } front=(front+1)%array.length;//front只需要往后走 return true; } public int Front() { if(isEmpty()){ return -1; } return array[front]; } public int Rear() { if(isEmpty()){ return -1; } //这里三木操作符判断是否为0如果为0,将rear回退到最后一个位置,不为0则-1 int temp = (rear==0)?array.length-1:rear-1; return array[temp]; } public boolean isEmpty() { return front==rear; } public boolean isFull() { return (rear+1)%array.length==front; }}用队列实现栈因为队列是先进先出的,而我们的栈是先进后出的,两种线性结构的关系是颠倒的,一个队列是不能完成的,我们需要两个队列互相工作来完成辅助队列先获取数值,保证辅助队列是最后一个拿到值的,然后将主队列的值给到辅助队列,在交换两个队列的数值,因为队列关系先进先出,每一次最后一个值就是队列先出的数值主队列不为空,将主队列的元素都poll出放到辅助栈中,使用一个tmp来将主队列(这里主队列已经遍历完)和辅助队列交换 Queue<Integer> q1;//主队列 Queue<Integer> q2;//辅助队列 public MyStack() { q1=new LinkedList<>();//构造方法 q2=new LinkedList<>(); } public void push(int x) { q2.offer(x); while(!q1.isEmpty()){//主队列不为空,则将主队列出列给到辅助队列 q2.offer(q1.poll()); } //走到这里主队列是为空 Queue tmp=q1; q1=q2; q2=tmp; //将两个队列交换 } public int pop() { return q1.poll(); } public int top() { return q1.peek(); } public boolean empty() { return q1.isEmpty(); }}用栈来实现队列栈来实现队列,栈是先进后出的顺序,而队列是先进先出的顺序将push都放到a栈中当我们peek或者是要删除的时候,我们都将a栈的元素pop给b栈,这样b栈已经有了我们的元素但是我们还需要考虑的是丢掉元素后如果在一起添加元素到a栈呢,这里我们给一个条件,如果b的栈不为空时,我们仍然用b栈的队列如果a为空,这两个栈都是空的说明没有元素直接返回-1,如果a不为空的话且b没有新的元素b继续捕获新的a栈中所有的元素class MyQueue { Stack<Integer> A; Stack<Integer> B; public MyQueue() { A=new Stack<>(); B=new Stack<>(); } public void push(int x) { A.push(x); } public int pop() { int check=peek(); B.pop(); return check; } public int peek() { //先判断b是否是空的,如果不是空的直接返回,是空才可以往下走 if(!B.isEmpty())return B.peek(); //因为b还不是空的,所以不需要将a栈放到b中 if(A.isEmpty())return -1; while(!A.isEmpty()){ B.push(A.pop());//将所有的a放到b中 } return B.peek(); } public boolean empty() { return A.isEmpty()&&B.isEmpty(); //a和b都为空才为空 }}总结栈分为栈顶和栈底,最先进的为栈底,最后进的为栈顶。队列分为队头和队尾,最先进的为队头,最后进的为队尾。————————————————原文链接:https://blog.csdn.net/weixin_60489641/article/details/143723419
-
写在前面几个Java哥们儿瞪着满屏的报错,脸都快贴屏幕上了——项目deadline催命呢,这场景,熟吧?憋屈吧?可你扭头看看隔壁组,人家正端着咖啡杯,有说有笑地做测试呢!为啥?人家刚用了个叫飞算JavaAI的东西,把整个电商平台的后端代码,“唰”一下给整出来了!乖乖,这世道,真变了?说飞算JavaAI,你可别想岔了。它不是你写代码时蹦出来的那种“小补丁”,顶多算个“单词提示”。这玩意儿是动真格的——全球头一个专门伺候Java的,能直接给你“吐”出一整套、能跑、能用的项目代码! 背后是正经搞技术的飞算公司,牛人不少,钱也厚实。它牛在哪?简单说,就是把咱原来那套写代码的苦逼流程,给“掀桌子”了。你跟它叨咕一句“弄个订单管理系统”,它吭哧吭哧就给你整出接口、数据库、业务逻辑全套家伙事儿,直接能跑!科幻片?不,现在真有兄弟在用了。为啥说这玩意儿能救命?专治各种“工伤”!跟产品经理“鸡同鸭讲”?拜拜了您嘞! 产品老哥嘴里的“用户画像”,你以为是打标签?结果他要的是猜用户下一步买啥!来回掰扯,跟传话游戏似的,心累得慌!JavaAI咋整?你直接跟它唠嗑(说话都行),它就能整明白你要啥,连你没想到的(比如商品视频咋存咋管)都能给你拎出来。沟通成本?直接砍半!烦死人的CURD“搬砖”?丢给它! 建表?写增删改查接口?配那些乱七八糟的依赖包?这些破事儿占了大把时间,干完还没啥成就感,纯纯的“工具人”!JavaAI就猛了,点一下,Maven/Gradle项目骨架、标准代码、配置文件,全套齐活! 省下的功夫,琢磨点有意思的技术难点,不香吗?早点下班陪女朋友(如果有的话)不香吗?看见老代码就想跑?它能当“老中医”! 那些用老掉牙的Hibernate写的“祖传屎山”,看着就头大,重构?跟考古挖坟没区别,生怕动一下就塌了!JavaAI自带本地“老中医”功能,能帮你把这堆老古董“号号脉”,再看看现在有啥好用的新玩意儿,给你出个靠谱的升级方案,至少心里有底了。这玩意儿到底有啥能耐?Lethehong给你盘盘道兄弟们都说它是“六边形战士”,真不是瞎吹:嘴皮子一动,设计图就来了:你就说“搞个会员积分系统”,它立马给你列出要哪些接口、数据库表长啥样,连字段啥类型、主键咋设都给你整得门儿清。它肚子里专门琢磨过Java的“脾气”,设计出来的东西,扩展性好,不容易“牵一发动全身”。复杂业务不怕翻车?它有“防呆”招儿! 搞多张表一起操作、或者一堆人同时抢资源(高并发)?心里打鼓怕出幺蛾子吧?它能把复杂的业务逻辑掰开了、揉碎了,变成一步步能走的,还提前帮你瞅瞅哪儿可能打架。更神的是,你改了点小地方,它还能偷偷把相关的逻辑也调顺溜,有效防止“改一行代码,整个系统嗝屁”的惨案(这痛,扎心不?)。代码风格看不上眼?按你的规矩来! 嫌弃生成的代码太死板、没个性?简单!你直接跟它说你们组有啥“家规”(比如“DTO必须验数据”、“不准在代码里写死数字”),它生成的代码,立马就规规矩矩按你的“家规”来,跟你们组自己人码的一模一样。老系统不敢大动?它“小刀慢割”! 面对一堆陈年老代码,2.0版本多了个“一块一块生成”的功能,贼实用。你可以挑着某个接口或者功能,单独让它生成新代码,还能马上看到效果。往老系统里塞的时候,也不用提心吊胆怕把整个系统搞崩了。谁在用?反正不是摆设!刚入行的小白:被Spring Boot那些注解绕得七荤八素?用它生成个标准项目直接跑起来,边改边看边学,比干啃教程快多了,上手贼快!被deadline追着跑的苦命团队:真有兄弟(做医疗平台的)用了,仨小时,订单模块搞定! 搁以前,吭哧吭哧手写至少三天!省下的时间,人家转头就去搞更核心的算法优化了,效率杠杠的。总被“需求误解”气哭的产品经理:这回牛了,能直接甩给开发一个“能跑”的技术方案!再也不用背锅说“我明明说的是A,你们咋做出个C?”了,腰杆都直了!想少掉点头发的技术老大(CTO):用上它的规则引擎统一代码风格,Code Review的破事儿直接少了一大半! 团队代码看起来清清爽爽,老大也省心,少熬点夜,头发能多留几根。别小看它,可能真要“变天”别人还在吵吵AI写的代码片段靠不靠谱,飞算JavaAI已经玩得更深了:它把咱们这帮写Java的,从流水线上拧螺丝的“码农”,变成了指挥AI“施工队”干活的“包工头”(架构师)。有个用了的CTO老哥说的大实话:“以前兄弟们80%的劲儿都耗在写基础代码和擦屁股(修Bug)上了,现在?能腾出手来琢磨点真正有技术含量的、创新的东西了!”飞算这家公司,野心不小。之前搞的SoFlu软件机器人就吹过“一个人就能扛一个项目,十个人能当百人用”,在银行、医院这些地方都用上了。现在这个JavaAI是他们的“大招”,配上低代码、数据工具,搞了个给企业用的“数字化套餐” ——这架势,明摆着是要抢传统软件外包公司那万亿级别的大蛋糕啊!如何安装飞算 JavaAI步骤一:从 IDEA 插件市场安装点击导航-插件,打开应用市场,搜索相关关键字"CalEx JavaAl"或"飞算"都能搜索到,找到CalEx-JavaAl 后点击安装步骤二:注册飞算 JavaAl重启 Inteli IDEA 工具,请先注册账号并登录,登录成功后即可开始体验“智能分析”、"JavaChat”"智能问答“等功能。新项目开发”一键生成完工程文档,”“老项目二次开发”本地化智能分析深度理解+自定义开发规范。 步骤三:在日常开发中使用飞算JavaAlJava AI真的有那么神? 测试项目的优缺点 这里我正好有一个项目在线,于是我就让他给我一个文件中的优缺点 优点清晰的项目结构:文件结构符合标准的Maven项目结构,易于管理和扩展。明确的依赖管理:使用Maven管理依赖,版本明确,便于维护和更新。常用的库版本:使用的MySQL驱动和MyBatis版本都是比较稳定的版本,适合大2多数应用场景。缺点JUnit作用域设置不正确:JUnit的依赖作用域被设置为compile,而通常情况下应该设置为test,以避免将测试依赖打包到生产环境中。缺少日志库依赖:项目中没有包含日志库(如SLF4J或Log4j),这在实际应用中通常是必要的,以便于调试和监控。缺少其他实用依赖:例如数据库连接池(如HikariCP)、Spring框架(如果需要事务管理和依赖注入)等,这些在实际项目中可能会提高开发效率和应用性能。 Java AI制作Java算命这里生成代码的速度已经超越了绝大部分的AI编程工具我想利用Java写一个算命的程序,请你给我给出完整的代码 package com.test; import java.util.Scanner; public class FortuneTeller { public static void main(String[] args) { Scanner scanner = new Scanner(System.in); System.out.println("请输入您的出生年份:"); int birthYear = scanner.nextInt(); scanner.close(); String fortune = tellFortune(birthYear); System.out.println("您的命运预测如下:"); System.out.println(fortune); } private static String tellFortune(int birthYear) { switch (birthYear % 12) { case 0: return "您属鼠,今年可能会遇到很多新的开始和机会。"; case 1: return "您属牛,今年可能会有很多稳定和收获。"; case 2: return "您属虎,今年可能会有挑战和冒险,但也充满力量。"; case 3: return "您属兔,今年可能会有快乐和轻松,但也需要注意健康。"; case 4: return "您属龙,今年可能会有领导力和创造力的展现。"; case 5: return "您属蛇,今年可能会有智慧和耐心的增长。"; case 6: return "您属马,今年可能会有活力和速度的提升。"; case 7: return "您属羊,今年可能会有和谐和温暖的感受。"; case 8: return "您属猴,今年可能会有灵活性和适应性的增强。"; case 9: return "您属鸡,今年可能会有勤奋和节俭的习惯。"; case 10: return "您属狗,今年可能会有忠诚和友谊的支持。"; case 11: return "您属猪,今年可能会有幸福和满足的感觉。"; default: return "无法预测的命运。"; } }}飞算JavaAI vs 国内其他AI编程工具跟通义灵码比较最大的区别就是专注度通义灵码支持Java、Python、C++等多种语言,但飞算JavaAI就专门死磕Java这一门语言。这就像一个是全科医生,一个是心脏科专家的区别。我之前同时用过这两个,感受挺明显的:通义灵码生成代码时,经常给你一些通用的模板,但涉及到Java特有的一些优化和最佳实践,就显得有点浅飞算JavaAI生成的代码遵循Java代码编写的最佳实践,代码风格统一、规范,可读性强代码生成的完整度差别很大这个差别真的很明显。通义灵码在生成复杂业务逻辑代码时,生成的代码结构有时不够清晰,需要开发者花费更多时间去梳理和优化。我拿同一个需求测试过,通义灵码给我生成了几个代码片段,我还得自己组装。飞算JavaAI直接给我一套完整的工程代码,连数据库建表语句都有了。跟文心快码比较百度这个确实厉害,但思路不一样文心快码支持超过100种主流编程语言,覆盖了从系统编程到Web开发、移动应用开发等多个领域。功能很全面,但问题还是老毛病——太泛了。我试过用文心快码做个电商系统,它能理解我的需求,也能给代码,但给的都是一些标准的CRUD操作。想要一些高级功能,比如分布式锁、缓存策略这些,就比较吃力。飞算JavaAI专注于Java单一语言开发,对Java语言特性和编程规范有深入理解,能生成高质量、符合行业最佳实践的Java代码。在处理复杂业务逻辑时,它真的能生成结构清晰、逻辑严谨的代码。跟豆包MarsCode比较字节的这个工具我用得不多主要原因是实测下来,感觉和GitHub Copilot和通义灵码都有差距,说实话是有点失望的。可能是因为发布时间比较晚,还在持续优化中。不过豆包MarsCode有个优势是它除了编程助手,还提供了云端开发环境。但纯粹从代码生成质量来说,跟飞算JavaAI比还是有明显差距的。实际使用建议如果你是:Java专业开发者:强烈推荐飞算JavaAI,真的能大幅提升效率多语言开发者:可以考虑通义灵码或文心快码个人学习者:通义灵码免费,可以先试试企业级项目:飞算JavaAI在代码质量和完整性上更有保障说实话,用过飞算JavaAI之后,再用其他工具总感觉缺点什么。就像习惯了自动挡汽车,再开手动挡总觉得麻烦。当然,这也可能是因为我主要做Java开发的原因。不过有一点要说明,飞算JavaAI目前主要专注后端,如果你要做前端开发,可能还是得配合其他工具使用。写在最后凌晨的办公室,咖啡机还在那儿“咕噜咕噜”响。但原来那密集的键盘“交响乐”少了,多了点飞算JavaAI干活时那种低沉的“嗡嗡”声。一个开发兄弟指着屏幕,乐了:“搞定!订单退款逻辑跑通了,嘿,连测试多人同时退款的代码都给我备好了!” 他那组人已经开始收拾包,张罗着去吃宵夜了。为啥这么潇洒?因为明天产品要的新需求讨论,他们今晚就能把演示版(Demo)整出来。当AI把那些重复的、费脑子的“搬砖”活儿扛了,咱们这帮写Java的脑子,总算能腾出来,干点更带劲、更有创造性的活儿了——比如,想想宵夜点啥烤串? (或者,早点回家睡觉?)————————————————原文链接:https://blog.csdn.net/2301_76341691/article/details/148697903
-
一、Java发展史 Java最初由Sun公司的“Green”项目组开发,用于智能家电设备,最初名为Oak。因商标问题,1995年更名为“Java”(灵感源于印尼爪哇岛的咖啡)。发行版本 发行时间 发行的各版本及其特征Java 1995年 Java语言诞生Java 1.0 1996年 首个正式版本,包含基础类库和Applet支持Java 1.1 1997年 引入内部类(Inner Class)、Java Beans、JDBC(数据库连接)和反射APIJava 1.2 1998年 JDK 1.2发布,更名为Java 2,分为三个平台:J2SE(标准版)、J2EE(企业版)、J2ME(微型版)Java 1.3 2000年 引入HotSpot JVM、JNDI(Java命名与目录接口)Java 1.4 2002年 新增正则表达式、断言(Assert)、NIO(非阻塞I/O)和日志APIJava 5.0 2004年 引入泛型、注解、枚举等革命性特性,为强调版本重要性,Sun将内部版本号1.5公开命名为5.0,此后版本号逐渐简化Java 6.0 2006年 Sun将产品线更名为Java SE/EE/ME,终结“J2”前缀,并宣布开源(OpenJDK)2009年 Oracle以74亿美元收购财务困境的Sun公司,Java正式归属OracleJava 7.0 2011年 Oracle首个大版本,支持菱形语法、多异常捕获,但因收购过渡期特性较少Java 8.0 2014年 继JDK 5后最大更新,引入Lambda表达式、Stream API、新日期时间库。LTS(长期支持)版本Java 9.0 2017年 发布周期改为每半年发布一次版本,每三年推出LTS(长期支持)版本Java 10.0 2018年 废弃“1.x”格式,直接使用主版本号(如JDK 10而非JDK 1.10)Java EE移交Eclipse基金会,重命名为Jakarta EE(如包名从javax.*改为jakarta.*)Java 11.0 2018年 新增HTTP客户端API、局部变量类型推断(var)并移除部分过时功能。LTS(长期支持)版本… … Java21.0 2023年 被视为继Java 8后的新一代主流版本,生态支持(如框架适配率)快速提升。LTS(长期支持)版本二、Java技术体系平台1、JavaSEJavaSE 的全称是 Java Platform Standard Edition(Java 平台标准版)面向桌面级应用(如Windows下的应用程序),提供完整的Java核心API,是其他平台(JavaEE、JavaME)的基础JavaSE和JDK的关系JavaSE(规范):定义接口、抽象类、具体类以及JVM的行为和约束(定义语言和API应该是什么样)例:JavaSE规范要求必须有一个ArrayList类,它实现List接口,支持动态扩容JDK(实现):提供这些接口和类的具体代码实现(按照规则实现并提供开发工具和运行环境)例1:OracleJDK的ArrayList源码中,具体实现了扩容机制(如默认扩容1.5倍)例2:OpenJDK的ArrayList可能实现相同的逻辑,但代码细节可能有细微差异(如注释、内部优化)历史名称:早期称为J2SE(JDK 6之前)2、JavaEEJavaEE 的全称是 Java Platform Enterprise Edition(Java 平台企业版)在Java SE基础上扩展了大量企业级API(如Servlet、JSP、EJB),提供分布式计算、事务管理、安全性等企业级功能JavaEE接口由官方规范定义,具体实现由应用服务器(Tomcat、WildFly)或第三方库(Hibernate、ActiveMQ)提供自JDK 10起由Oracle移交Eclipse基金会管理,更名为Jakarta EE历史名称:曾用名J2EE(JDK 6之前)3、JavaMEJavaME 的全称是 Java Platform Micro Edition(Java 平台微型版)针对移动终端(手机、PDA等)的轻量级平台,精简了Java SE的API并加入移动设备支持随着 Android 和 iOS 的普及,JavaME 的使用逐渐减少历史名称:曾用名J2ME4、三者关系JavaSE 是基础:JavaEE 和 JavaME 均基于 JavaSE 的核心功能构建JavaEE 是扩展:在 JavaSE 基础上增加企业级服务规范(如 Servlet、JPA、EJB)JavaME 是精简:仅保留 JavaSE 部分功能,并添加针对微型设备的特性三、Java程序运行机制及运行过程1、Java的跨平台性2、Java虚拟机(核心机制)JVM 是一个虚拟的计算机,具有指令集并使用不同的存储区域。负责执行指令,管理数据、内存、寄存器,包含在JDK 中对于不同的平台,有不同的虚拟机Java 虚拟机机制屏蔽了底层运行平台的差别,实现了“一次编译,到处运行”四、Java语言环境搭建1、JDK(Java开发工具包)定义:JDK是用于开发Java应用程序的完整工具包,包含编译、调试、文档生成等开发工具以及运行环境组成部分:JRE:JDK中内置了JRE(包含核心类库),确保开发时可以直接运行程序开发工具:如编译器javac(将Java源代码编译为字节码)、调试器jdb、文档工具javadoc等JDK特有的工具类库:如:tools.jar,支持编译器(javac)、调试器(jdb)等工具的运行(位于JDK的lib目录下)用途:开发者必须安装JDK,才能编写、编译和调试Java程序2、JRE(Java运行时环境)定义:JRE是运行已编译Java程序所需的最小环境,无需开发功能组成部分:JVM(Java虚拟机) :负责执行字节码,实现跨平台特性JRE中的核心类库:以java.*包的形式存在,例如rt.jar、resource.jar下java.lang、java.util等(位于JRE的lib目录下,并由BootstrapClassLoader自动加载)JRE中的扩展类库:以javax.*包的形式组织,例如javax.sql等(JRE的lib/ext目录下,由ExtensionClassLoader加载)用途:普通用户只需安装JRE即可运行Java程序(如.jar或.class文件),无需开发工具3、环境变量及作用3.1、JAVA_HOME该环境变量的值是Java的安装路径,一些Java版本的软件和工具需要用到该变量例如,当Windows平台上JDK的安装目录为“C:\java\jdk8”时,设置如下所示JAVA_HOME=C:\java\jdk813.2、CLASSPATH该环境变量用于指明Java字节码文件(.class文件)的位置默认情况下,如果未设置CLASSPATH,Java启动JVM后,会在当前目录下寻找字节码文件,一旦设置了CLASSPATH,JVM会在指定目录下查找字节码文件环境变量CLASSPATH的值一般为一个以分号“;”作为分隔符的路径列表,设置如下CLASSPATH=.;%JAVA_HOME%\lib\dt.jar;%JAVA_HOME%\lib\tools.jar;1“.”表示当前目录,因为设置CLASSPATH会覆盖JVM的默认操作(查找当前目录),所以这里需要加上“.”dt.jar是 Java 开发工具包(JDK)中用于为 IDE 提供 Swing/AWT 组件的设计时元数据(如属性、事件描述),支持通过拖拽和图形化界面进行可视化开发的核心类库文件tools.jar的作用:包含编译工具(如javac)所需的类库Java5之前,若用户未显式配置CLASSPATH环境变量JVM不会在当前目录查询.class文件,所以需要配置CLASSPATH但从Java 5(2004年发布)开始,默认情况,无需显式配置CLASSPATH,JVM会自动搜索当前目录和核心类库3.3、PATH该环境变量指定一个路径列表,用于搜索可执行文件执行一个可执行文件时,如果该文件不能在当前路径下找到,则依次寻找PATH中的每一个路径,直至找到。例如:PATH=.;%JAVA_HOME%\bin;1这样可以在命令行中直接使用java和javac命令,而不需要指定完整路径,否则就会出现以下错误:不建议在PATH环境变量中添加当前目录"."的主要原因:如果当前目录"."被加入PATH,当用户进入公共可写目录/tmp时,攻击者可能在该目录下放置与系统命令同名的恶意程序例如:黑客在/tmp目录下创建名为ls的木马文件,当用户(尤其是root用户)执行ls命令时,会优先执行当前目录下的恶意程序而非系统标准的/bin/ls,导致权限泄露或数据被破坏————————————————原文链接:https://blog.csdn.net/qq_35512802/article/details/148105022
上滑加载中
推荐直播
-
HDC深度解读系列 - Serverless与MCP融合创新,构建AI应用全新智能中枢2025/08/20 周三 16:30-18:00
张昆鹏 HCDG北京核心组代表
HDC2025期间,华为云展示了Serverless与MCP融合创新的解决方案,本期访谈直播,由华为云开发者专家(HCDE)兼华为云开发者社区组织HCDG北京核心组代表张鹏先生主持,华为云PaaS服务产品部 Serverless总监Ewen为大家深度解读华为云Serverless与MCP如何融合构建AI应用全新智能中枢
回顾中 -
关于RISC-V生态发展的思考2025/09/02 周二 17:00-18:00
中国科学院计算技术研究所副所长包云岗教授
中科院包云岗老师将在本次直播中,探讨处理器生态的关键要素及其联系,分享过去几年推动RISC-V生态建设实践过程中的经验与教训。
回顾中 -
一键搞定华为云万级资源,3步轻松管理企业成本2025/09/09 周二 15:00-16:00
阿言 华为云交易产品经理
本直播重点介绍如何一键续费万级资源,3步轻松管理成本,帮助提升日常管理效率!
回顾中
热门标签