-
布隆过滤器(Bloom Filter)因其空间效率高、查询速度快的特点,被广泛应用于需要快速判断元素是否存在的场景,尤其适合允许一定误判率但不能漏判的情况。以下是其典型应用场景及具体案例:1. 缓存穿透防护问题恶意请求或高频查询不存在的数据(如 id=-1、未爬取的 URL),导致缓存和数据库均未命中,直接穿透到后端存储,引发性能雪崩。解决方案在缓存前加一层布隆过滤器,存储所有可能存在的键(如数据库主键、URL)。查询流程:先查布隆过滤器 → 若不存在,直接返回空结果(避免穿透)。若可能存在,再查缓存或数据库。案例电商系统:防止用户频繁查询不存在的商品 ID。爬虫系统:避免重复请求不存在的网页。2. URL 去重问题爬虫需要抓取海量网页,但需避免重复抓取同一 URL,否则浪费资源且可能陷入循环。解决方案用布隆过滤器存储已抓取的 URL,每次抓取前先检查是否存在。优势:相比哈希表或数据库,布隆过滤器节省大量内存(例如存储 1 亿 URL 仅需约 120MB 内存,误判率 1%)。案例Google 爬虫:早期使用布隆过滤器去重,后升级为更高效的变种(如 Counting Bloom Filter)。新闻聚合应用:避免重复抓取相同新闻源的链接。3. 垃圾邮件/恶意内容过滤问题需要快速判断邮件发送者、IP 或内容是否在黑名单中,但黑名单可能包含数百万条记录。解决方案用布隆过滤器存储黑名单(如垃圾邮件发送者的邮箱、恶意 IP),实时过滤请求。误判处理:若布隆过滤器返回“可能存在”,再通过精确查询(如数据库)确认。案例SpamAssassin:开源垃圾邮件过滤工具使用布隆过滤器加速黑名单检查。CDN 防护:拦截已知恶意请求的 IP。4. 区块链与加密货币问题区块链节点需要快速验证交易或地址是否有效,但全量存储所有地址不现实(如比特币地址约 2^160 个)。解决方案用布隆过滤器存储已使用的地址或交易哈希,节点间同步时过滤无效数据。比特币轻客户端:通过布隆过滤器向全节点请求与自己钱包相关的交易,减少数据传输量。案例Ethereum:使用布隆过滤器优化日志查询。Zcash:隐私交易中过滤无关数据。5. 数据库与大数据查询优化问题在大数据集(如用户行为日志、点击流)中查询某元素是否存在时,直接扫描全表效率极低。解决方案用布隆过滤器预过滤不可能存在的数据,减少磁盘 I/O 或网络传输。Hive/Spark:支持将布隆过滤器下推到存储层(如 HDFS),加速查询。案例Facebook:用布隆过滤器优化 HBase 的 Scan 操作,避免全表扫描。Elasticsearch:通过布隆过滤器加速 exists 查询。6. 推荐系统与用户行为分析问题需要快速判断用户是否已对某商品点赞、收藏或浏览过,但用户行为数据量巨大。解决方案用布隆过滤器存储用户行为记录(如 user_id + item_id 的哈希),实时去重或推荐。冷启动优化:对新用户快速生成近似行为画像。案例Netflix:用布隆过滤器记录用户观看历史,优化推荐算法。TikTok:实时过滤用户已刷过的视频。7. 分布式系统与一致性哈希问题分布式缓存(如 Redis Cluster)中,节点需要快速判断键是否属于本机分片,避免跨节点查询。解决方案用布隆过滤器存储本节点负责的键范围,减少哈希计算或网络开销。变种应用:结合一致性哈希环,优化数据迁移时的键分配。案例Twitter:用布隆过滤器加速 Timeline 服务的缓存查询。Akamai:CDN 节点用布隆过滤器路由请求到正确的边缘服务器。8. 网络安全与入侵检测问题需要实时检测网络流量中是否包含恶意签名(如病毒特征、攻击模式),但签名库可能包含数百万条规则。解决方案用布隆过滤器存储恶意签名,快速过滤正常流量,减少深度检测(DPI)的负载。误判处理:对布隆过滤器标记的可疑流量进行进一步分析。案例Snort:开源入侵检测系统使用布隆过滤器加速规则匹配。Cloudflare:用布隆过滤器拦截已知的 DDoS 攻击流量。9. 生物信息学与基因测序问题基因测序数据量极大(如人类基因组约 30 亿碱基对),需要快速查找特定序列是否存在。解决方案用布隆过滤器存储已知基因片段,加速测序数据的比对和分析。应用场景:疾病关联分析、进化树构建。案例BWA/Bowtie:主流基因比对工具使用布隆过滤器优化索引查询。COVID-19 测序:快速筛选病毒变异位点。10. 游戏开发与反作弊问题需要实时检测玩家行为是否异常(如外挂、刷分),但行为模式库可能包含大量规则。解决方案用布隆过滤器存储作弊行为特征(如特定按键序列、网络包模式),实时拦截可疑操作。动态更新:通过后台服务定期更新布隆过滤器的作弊规则。案例PUBG Mobile:用布隆过滤器检测外挂脚本。Steam:反作弊系统(VAC)使用布隆过滤器优化行为分析。总结:布隆过滤器的适用性场景是否适用原因需要快速判断存在性✔️查询时间复杂度 O(k),与数据量无关。允许一定误判率✔️误判率可通过调整参数控制(如 1% 或更低)。数据量极大(亿级以上)✔️空间效率远高于哈希表或数据库,内存占用低。不支持删除或动态更新❌(需变种)传统布隆过滤器不支持删除,可用 Counting Bloom Filter 或 Cuckoo Filter。需要零误判❌误判率不可为 0,需结合精确查询(如数据库)使用。推荐实践:单机场景:优先使用 Guava 的 BloomFilter 或 RedisBloom。分布式场景:结合 Redis、HBase 或自定义分片实现。高并发场景:注意布隆过滤器的线程安全性(Guava 实现是线程安全的)。通过合理应用布隆过滤器,可以显著提升系统性能,尤其在处理海量数据的场景下。
-
Java 中的布隆过滤器(Bloom Filter)是一种空间效率极高的概率型数据结构,用于快速判断一个元素是否可能存在于集合中(可能存在误判,但绝不会漏判)。它特别适合处理大规模数据且允许一定误判率的场景,如缓存穿透防护、URL去重、垃圾邮件过滤等。一、布隆过滤器核心原理1. 数据结构位数组(Bit Array):初始时所有位为 0,长度通常为 m。哈希函数集合:k 个独立的哈希函数,每个函数将元素映射到位数组的某个位置。2. 操作流程添加元素:用 k 个哈希函数计算元素的 k 个位置。将这些位置的位全部设为 1。查询元素:用同样的 k 个哈希函数计算 k 个位置。如果所有位置均为 1,则元素可能存在;否则一定不存在。3. 误判率误判(False Positive):元素不在集合中,但布隆过滤器返回“可能存在”。误判率公式:[p \approx \left(1 - e^{-\frac{kn}{m}}\right)^k]n:已添加的元素数量。m:位数组长度。k:哈希函数数量。优化目标:通过调整 m 和 k,使误判率 p 最小化(通常 k ≈ m/n * ln2 时最优)。二、Java 实现方式1. Guava 的 BloomFilter(推荐)Google Guava 库提供了开箱即用的布隆过滤器实现,支持自动计算最优参数。依赖引入<dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>32.1.3-jre</version> <!-- 使用最新版本 --> </dependency> 代码示例import com.google.common.hash.BloomFilter; import com.google.common.hash.Funnels; import java.nio.charset.StandardCharsets; public class BloomFilterDemo { public static void main(String[] args) { // 1. 创建布隆过滤器:预期插入100万个元素,误判率控制在1%以内 BloomFilter<String> bloomFilter = BloomFilter.create( Funnels.stringFunnel(StandardCharsets.UTF_8), 1_000_000, // 预期元素数量 0.01 // 误判率 ); // 2. 添加元素 bloomFilter.put("apple"); bloomFilter.put("banana"); // 3. 查询元素 System.out.println(bloomFilter.mightContain("apple")); // true System.out.println(bloomFilter.mightContain("orange")); // false(可能误判为true) // 4. 序列化(可选) // byte[] bytes = bloomFilter.toByteArray(); // BloomFilter<String> restored = BloomFilter.create( // Funnels.stringFunnel(StandardCharsets.UTF_8), bytes); } } 关键参数说明Funnels.stringFunnel():指定元素类型(如 String、Long 等)。expectedInsertions:预期插入的元素数量(需预估准确,否则影响误判率)。fpp(False Positive Probability):误判率(值越小,位数组越大)。2. Redis 的布隆过滤器(分布式场景)如果需要在分布式系统中使用布隆过滤器,可以通过 Redis 模块 RedisBloom 实现。依赖引入<dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> <version>4.4.3</version> </dependency> <dependency> <groupId>io.rebloom</groupId> <artifactId>rebloom</artifactId> <version>2.2.0</version> </dependency> 代码示例import redis.clients.jedis.Jedis; import redis.clients.jedis.params.SetParams; public class RedisBloomFilterDemo { public static void main(String[] args) { try (Jedis jedis = new Jedis("localhost", 6379)) { // 1. 创建布隆过滤器(key="my_bloom", 预期元素数=1000, 误判率=0.01) jedis.bfReserve("my_bloom", 0.01, 1000); // 2. 添加元素 jedis.bfAdd("my_bloom", "apple"); jedis.bfAdd("my_bloom", "banana"); // 3. 查询元素 System.out.println(jedis.bfExists("my_bloom", "apple")); // true System.out.println(jedis.bfExists("my_bloom", "orange")); // false } } } 三、布隆过滤器的应用场景缓存穿透防护问题:恶意请求查询不存在的数据(如 id=-1),导致缓存和数据库均未命中。解决方案:在缓存前加一层布隆过滤器,若元素不存在则直接返回空结果。URL去重爬虫系统中快速判断 URL 是否已抓取。垃圾邮件过滤判断邮件发送者是否在黑名单中(允许少量误判)。区块链地址校验快速检查地址是否有效(避免全量存储)。四、布隆过滤器的局限性不支持删除操作传统布隆过滤器无法删除元素(需使用变种如 Counting Bloom Filter)。误判率不可为 0需根据业务容忍度调整参数。空间效率与误判率的权衡误判率越低,位数组越大(例如 1% 误判率约需 9.6 位/元素)。五、替代方案对比方案查询时间空间效率支持删除误判率布隆过滤器O(k)高❌可控布谷鸟过滤器O(k)更高✔️更低哈希表O(1)低✔️0Redis 集合O(1)中✔️0选择建议:需要极致空间效率且允许误判 → 布隆过滤器。需要支持删除 → 布谷鸟过滤器或 Redis 集合。需要零误判 → 哈希表或 数据库。总结单机场景:优先使用 Guava 的 BloomFilter,简单高效。分布式场景:选择 RedisBloom 或自研基于 Redis 的布隆过滤器。关键参数:根据预期元素数量和误判率调整 m 和 k(Guava 已自动优化)。通过合理使用布隆过滤器,可以显著提升系统性能,尤其在处理海量数据的场景下。
-
对象的构造及初始化5.1 如何初始化对象通过前面知识点的学习我们知道,在Java方法内部定义一个局部变量时,必须要初始化,否则会编译失败。 public static void main(String[] args) {int a;System.out.println(a);}// Error:(26, 28) java: 可能尚未初始化变量aAI生成项目java运行 要让上述代码通过编译,非常简单,只需在正式使用变量之前给它设置初始值即可。 public static void main(String[] args) { Date d = new Date(); d.printDate(); d.setDate(2021,6,9); d.printDate();}// 代码可以正常通过编译AI生成项目java运行 如果是对象,就需要调用之前写的 setDate 方法将具体的日期设置到对象中。 通过上述例子我们发现两个问题: 问题1:每次对象创建好后调用 setDate 方法设置具体日期显得比较麻烦,那么对象该如何初始化?问题2:局部变量必须初始化才能使用,而字段声明之后没有给值依然可以使用,这是因为字段具有默认初始值。为了解决问题1,Java引入了 构造方法,使得对象在创建时就能完成初始化操作。 5.2 构造方法构造方法(也称为构造器)是一种特殊的成员方法,其主要作用是初始化对象。 5.2.1 构造方法的概念public class Date { public int year; public int month; public int day; // 构造方法: // 名字与类名相同,没有返回值类型,设置为void也不行 // 一般情况下使用public修饰 // 在创建对象时由编译器自动调用,并且在对象的生命周期内只调用一次 public Date(int year, int month, int day){ this.year = year; this.month = month; this.day = day; System.out.println("Date(int,int,int)方法被调用了"); } public void printDate(){ System.out.println(year + "-" + month + "-" + day); } public static void main(String[] args) { // 此处创建了一个Date类型的对象,并没有显式调用构造方法 Date d = new Date(2021,6,9); // 输出Date(int,int,int)方法被调用了 d.printDate(); // 2021-6-9 }}AI生成项目java运行 构造方法的特点是: 名字必须与类名相同,且没有返回值类型(连void都不行)。在创建对象时由编译器自动调用,并且在对象的生命周期内只调用一次。注意:构造方法的作用是对对象中的成员进行初始化,并不负责给对象开辟内存空间。5.2.2 构造方法的特性构造方法具有如下特性: 名字必须与类名完全相同没有返回值类型,即使设置为void也不行创建对象时由编译器自动调用,且在对象生命周期内只调用一次(就像人的出生,每个人只能出生一次)支持重载:同一个类中可以定义多个构造方法,只要参数列表不同即可示例代码1:带参构造方法 public class Date { public int year; public int month; public int day; // 构造方法:名字与类名相同,没有返回值类型,使用public修饰 // 在创建对象时由编译器自动调用,并且在对象的生命周期内只调用一次 public Date(int year, int month, int day) { this.year = year; this.month = month; this.day = day; System.out.println("Date(int, int, int)方法被调用了"); } public void printDate() { System.out.println(year + "-" + month + "-" + day); } public static void main(String[] args) { // 此处创建了一个Date类型的对象,并没有显式调用构造方法 Date d = new Date(2021, 6, 9); // 输出:Date(int, int, int)方法被调用了 d.printDate(); // 输出:2021-6-9 }}AI生成项目java运行 示例代码2:无参构造方法 public class Date { public int year; public int month; public int day; // 无参构造方法:给成员变量设置默认初始值 public Date() { this.year = 1900; this.month = 1; this.day = 1; }}AI生成项目java运行 上述两个构造方法名字相同但参数列表不同,构成了方法的重载。 如果用户没有显式定义构造方法,编译器会生成一个默认的无参构造方法。注意:一旦用户显式定义了构造方法,编译器就不会再生成默认构造方法了 示例代码3:仅定义带参构造方法时默认构造方法不会生成 // 带有三个参数的构造方法public Date(int year, int month, int day) { this.year = year; this.month = month; this.day = day;} public void printDate(){ System.out.println(year + "-" + month + "-" + day);} public static void main(String[] args) { Date d = new Date(); // 编译期报错,因为没有无参构造方法 d.printDate();}AI生成项目java运行 示例代码4:只有无参构造方法的情况 public class Date { public int year; public int month; public int day; public void printDate(){ System.out.println(year + "-" + month + "-" + day); } public static void main(String[] args) { Date d = new Date(); d.printDate(); }}AI生成项目java运行 示例代码5:只有带参构造方法的情况 public class Date { public int year; public int month; public int day; public Date(int year, int month, int day) { this.year = year; this.month = month; this.day = day; }}AI生成项目java运行 构造方法中可以通过 this(…) 调用其他构造方法来简化代码。注意:this(…) 必须是构造方法中的第一条语句,否则编译器会报错。示例代码6:正确使用this(…)实现构造器链 public class Date { public int year; public int month; public int day; // 无参构造方法 -- 内部调用带参构造方法实现初始化 // 注意:this(1900, 1, 1);必须是构造方法中的第一条语句 public Date(){ // System.out.println(year); // 若取消注释则编译会失败 this(1900, 1, 1); // 以下赋值代码被省略,因为已在带参构造方法中完成初始化 // this.year = 1900; // this.month = 1; // this.day = 1; } // 带有三个参数的构造方法 public Date(int year, int month, int day) { this.year = year; this.month = month; this.day = day; }}AI生成项目java运行 注意:构造方法中的 this(…) 调用不能形成循环,否则会导致编译错误。 public Date(){this(1900,1,1);} public Date(int year, int month, int day) {this();}/*无参构造器调用三个参数的构造器,而三个参数构造器有调用无参的构造器,形成构造器的递归调用编译报错:Error:(19, 12) java: 递归构造器调用*/AI生成项目java运行 在大多数情况下,我们使用 public 来修饰构造方法,但在特殊场景下(如实现单例模式)可能会使用 private 修饰构造方法。5.3 默认初始化在上文中提到的第二个问题:为什么局部变量在使用前必须初始化,而成员变量可以不初始化? 要搞清楚这个过程,就需要知道 new 关键字背后所发生的一些事情: Date d = new Date(2021,6,9);AI生成项目java运行1在程序员看来只是一句简单的语句,但 JVM 层面需要做好多事情。下面简单介绍下: 检测对象对应的类是否被加载,如果没有则加载为对象分配内存空间并先默认初始化处理并执行类中的 init 方法初始化分配好的空间 (说明:多个线程同时申请资源,JVM 要保证分配给对象的空间内干净。)即:对象空间被申请好之后,对象中包含的成员已经设置好了初始值,比如:Java中的 成员变量 会由 JVM 自动赋予默认值(如int类型为0,boolean类型为false等),而局部变量则需要显式初始化才能使用。 Java为不同的数据类型提供了默认值,具体如下所示: 数据类型 默认值byte 0char ‘\u0000’short 0int 0long 0Lboolean falsefloat 0.0fdouble 0.0dreference null这些默认值确保了即使对象的成员变量没有显式初始化,也不会发生错误,成员变量会有一个初始的稳定状态。 设置对象头信息(关于对象内存模型后面会介绍) 调用构造方法,给对象中各个成员赋值 5.4 就地初始化就地初始化是指在成员变量声明时直接为它们赋初值。这种方法在代码的简洁性上具有优势,可以避免每次创建对象时重复设置成员变量的值。 代码示例: public class Date { public int year = 1900; // 就地初始化 public int month = 1; // 就地初始化 public int day = 1; // 就地初始化 public Date() { // 构造方法在此处不需要再次初始化year、month、day,它们已经有默认值 } public void printDate() { System.out.println(year + "-" + month + "-" + day); } public static void main(String[] args) { Date d = new Date(); d.printDate(); // 输出:1900-1-1 }}AI生成项目java运行 在这个例子中,year、month 和 day 的值在声明时就被初始化为1900、1和1,确保了每次创建对象时这些值已经存在。 六、封装6.1 封装的概念封装是面向对象编程的三大特性之一,其核心思想是将数据和操作数据的方法结合在一起,并对外隐藏实现细节。例如,电脑作为一个复杂的设备,用户只需通过开关、键盘和鼠标等接口进行交互,而不必关心内部CPU、显卡等工作原理。 简单来说就是套壳屏蔽细节。 6.2 访问限定符访问修饰符说明 public:可以理解为一个人的外部接口,能被外部访问。protected:主要是给继承使用,子类继承后就能访问到。default(不写修饰符时即为默认访问修饰符):对于同一包内的类可见,不同包则不可见。private:只能在当前类中访问。(这部分要介绍完继承后才能完全理解) 【说明】protected 主要是给继承使用;default 只能给同一个包内使用;按照自己的理解去记忆。 示例代码: public class Computer { private String cpu; private String brand; private String memory; private String screen; public Computer(String brand, String cpu, String memory, String screen) { this.brand = brand; this.cpu = cpu; this.memory = memory; this.screen = screen; } public void boot() { System.out.println("开机"); } public void shutDown() { System.out.println("关机"); }}AI生成项目java运行 public class TestComputer { public static void main(String[] args) { Computer c = new Computer("华为", "i9", "16G", "4K"); c.boot(); c.shutDown(); }}AI生成项目java运行 注意:一般情况下,成员变量通常设置为 private,成员方法设置为 public。 6.3 封装扩展之包6.3.1 包的概念包(Package)用于对类进行分组管理,有助于解决类名冲突并提高代码的组织性。例如,将相似功能的类归为同一包。 为了更好的管理类,把多个类收集在一起成为一组,称为软件包 6.3.3导入包在Java中,如果我们需要使用不在默认 java.lang 包中的类或接口,就需要使用 import 关键字来导入。例如,我们想使用 java.util.Date 这个类,就可以这样做: import java.util.Date; public class TestImport { public static void main(String[] args) { Date d = new Date(); System.out.println(d); }}AI生成项目java运行 这样就可以正常创建 Date 对象并使用。 6.3.3全类名当不同包中存在同名的类时(如 java.util.Date 与 java.sql.Date 都叫 Date),可能会引发冲突。这时,可以使用 全类名(Fully Qualified Name)来指定使用哪个类: public class TestFullName { public static void main(String[] args) { // 使用全类名来区分两个Date类 java.util.Date d1 = new java.util.Date(); java.sql.Date d2 = new java.sql.Date(System.currentTimeMillis()); System.out.println(d1); System.out.println(d2); }}AI生成项目java运行 通过在创建对象时加上包名,就可以区分来自 java.util 和 java.sql 的 Date 类。 6.3.4 静态导入从 Java 5 开始,支持使用 静态导入(import static)的方式将某个类中的 静态成员(常量或方法) 导入到当前类中,从而在调用时可以省略类名。 示例代码: import static java.lang.Math.PI;import static java.lang.Math.random; public class TestStaticImport { public static void main(String[] args) { System.out.println(PI); // 直接使用PI常量 System.out.println(random()); // 直接使用random()方法 }}AI生成项目java运行 如果不使用静态导入,则需要写成: System.out.println(Math.PI);System.out.println(Math.random());AI生成项目java运行 6.3.5 IDE工具中的包结构当我们使用 IntelliJ IDEA 或 Eclipse 等 IDE 工具时,会在项目结构中直观地看到包名与文件夹一一对应。 包名一般使用 小写 的域名反写形式(如 com.example.project),在 IDE 中会对应层级文件夹结构。在同一个包下,可以放置多个类文件,便于组织与管理。在实际开发中,合理划分包结构能让项目更易于维护和理解。 6.3.6 包的访问权限控制举例Computer类位于com.bit.demo1包中,TestComputer位于com.bit.demo2包中: package com.bit.demo1; public class Computer { private String cpu; // cpu private String memory; // 内存 public String screen; // 屏幕 String brand; // 品牌 public Computer(String brand, String cpu, String memory, String screen) { this.brand = brand; this.cpu = cpu; this.memory = memory; this.screen = screen; } public void PowerOff(){ System.out.println("关机~~~"); } public void SurfInternet(){ System.out.println("上网~~~"); }}AI生成项目java运行 ////////////////////////////////////package com.bit.demo2; import com.bit.demo1.Computer; public class TestComputer { public static void main(String[] args) { Computer p = new Computer("HW", "i7", "8G", "13*14"); System.out.println(p.screen); // 公有属性,可以被其他包访问 // System.out.println(p.cpu); // 私有属性,不能被其他包访问 // System.out.println(p.brand); // brand是default,不允许被其他包中的类访问 }}AI生成项目java运行 注意:如果去掉前面的Computer类中的public修饰符,代码也会编译失败。 6.3.7 常见的包java.lang:系统常用基础类(String,Object),此包从JDK1.1后自动导入。java.lang.reflect:Java反射机制包;java.net:进行网络编程开发包;java.sql:进行数据库开发的包;javax.util:Java提供的工具程序包(集合类等)非常重要;javalio.io:编程程序包。注意事项: import 和 C++ 的 #include 差别很大. C++ 必须 #include 来引入其他文件内容, 但是 Java 不需要.import 只是为了写代码的时候更方便. 区别如下: Java 的 import:仅在编译时为你提供类或接口的简写路径,使你可以直接使用类名而不用写出完整的包名。实际上,编译器会在编译过程中通过类路径(classpath)去查找相应的类文件,而不会把代码插入到当前文件中。 C/C++ 的 #include:预处理器会在编译之前将头文件的内容直接拷贝进源代码文件中,这种方式实际上是将文件的内容“粘贴”到包含它的文件里。 因此,Java 的 import 只是一种简化引用的机制,并不涉及代码的复制。 七、总结与展望在本篇文章中,我们围绕对象的构造与初始化、封装、包的管理以及访问权限等内容展开了详细讲解,帮助大家深入理解Java面向对象编程的核心概念。 7.1 总结对象的构造与初始化 构造方法:通过构造方法,我们可以在创建对象时立即为其成员赋值,保证对象在使用前处于有效状态。构造器重载与this调用:支持多个构造方法以及构造器链,让对象初始化更加灵活和简洁。默认初始化与就地初始化:成员变量会自动获得默认值,同时也可以在声明时直接赋值,避免重复代码。封装 核心思想:将数据与操作数据的方法绑定在一起,通过访问限定符(public、private、protected、default)隐藏内部实现细节。访问控制:合理使用访问修饰符保护数据安全,提供公开接口供外部使用,增强了程序的健壮性和安全性。包的管理与导入 包的概念:通过将相关类归为一组,实现代码的模块化管理和命名空间隔离,避免类名冲突。import语句:在编译时提供类名简写路径,与C/C++的#include机制不同,import不进行代码复制,仅起到标识和简化引用的作用。7.2 总结深入static成员在后续的内容中,我们将更详细地探讨static关键字的使用,包括静态变量、静态方法及其在类中的意义,帮助大家更好地理解类级别的共享特性。 代码块与初始化块将介绍类中的初始化块和静态代码块,讨论它们在对象创建过程中的执行顺序和作用,为进一步掌握对象生命周期奠定基础。 内部类与匿名类内部类是一种特殊的类定义方式,它能够更紧密地绑定外部类的成员,将在未来篇章中深入剖析其用法和设计思想。 面向对象的设计原则随着对类和对象理解的深入,我们也将探讨更多面向对象设计的原则和模式,帮助大家构建更健壮、可维护的Java应用程序———————————————— 原文链接:https://blog.csdn.net/2301_79849925/article/details/146015106
-
前言:在 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
上滑加载中
推荐直播
-
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步轻松管理成本,帮助提升日常管理效率!
回顾中
热门标签