[{"content":"== 运算符 当应用于基本数据类型（如 int, double, char 等）时，它比较的是值是否相等。 当用于引用类型（比如对象、数组）时，== 比较的是两个引用是否指向堆内存中的同一个对象，即它们的内存地址是否相同。\nequals() 方法 equals() 是从 Object 类继承而来的方法，默认实现实际上也是比较对象的引用是否相同，和 == 的行为一致。 然而，许多类（尤其是像 String, Integer 这样的包装类）重写了 equals() 方法，使其比较的是对象的实际内容是否相等，而不是引用。\nString类的equals方法：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 public boolean equals(Object anObject) { // 判断两个对象是否为同一个引用 if (this == anObject) { return true; } // 检查参数是否为字符串实例 if (anObject instanceof String) { String anotherString = (String)anObject; int n = value.length; // 比较两个字符串的长度 if (n == anotherString.value.length) { char v1[] = value; char v2[] = anotherString.value; int i = 0; // 循环比较两个字符串的字符内容 while (n-- != 0) { if (v1[i] != v2[i]) // 如果发现字符不同，则字符串不相等 return false; i++; } return true; } } return false; } Integer类的equals方法：\n1 2 3 4 5 6 7 8 public boolean equals(Object obj) { if (obj instanceof Integer) { // 与传入的实际值进行比较 // .intValue()将Integer对象包装的整数值提取出来，返回一个基本类型的int值 return value == ((Integer)obj).intValue(); } return false; } 示例代码：\n下面通过具体的Java代码示例来阐述这两个操作符的区别：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public class EqualsDemo { public static void main(String[] args) { // 基本类型比较 int num1 = 10; int num2 = 10; System.out.println(num1 == num2); // 输出 true，因为数值相等 // 引用类型的 == 比较 String str1 = new String(\u0026#34;Hello\u0026#34;); String str2 = new String(\u0026#34;Hello\u0026#34;); System.out.println(str1 == str2); // 输出 false，虽然内容相同，但不是同一个对象引用 // 使用 equals 比较 System.out.println(str1.equals(str2)); // 输出 true，因为 String 类重写了 equals 方法，比较的是内容 } } 实操：bean equals方法重写进行比较\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 public class EqualsDemo { public static void main(String[] args) { // 自定义类的示例 Person person1 = new Person(\u0026#34;Alice\u0026#34;, 30); Person person2 = new Person(\u0026#34;Alice\u0026#34;, 30); System.out.println(person1 == person2); // 输出 false，因为是两个不同对象 System.out.println(person1.equals(person2)); // 默认输出 false，除非重写 equals 方法 } static class Person { String name; int age; Person(String name, int age) { this.name = name; this.age = age; } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj == null || getClass() != obj.getClass()) { return false; } Person person = (Person) obj; return age == person.age \u0026amp;\u0026amp; name.equals(person.name); } } } 总结 == 主要来判断基本类型变量的值是否相等，或判断两个引用是否指向同一个对象。\nequals() 默认行为为比较引用与 == 相同，当使用String、Integer等重写了equals方法包装类时会比较对象的实际属性。\n","date":"2024-08-01T00:00:00Z","image":"/p/%E5%92%8C-equals-%E7%9A%84%E5%8C%BA%E5%88%AB/cover.webp","permalink":"/p/%E5%92%8C-equals-%E7%9A%84%E5%8C%BA%E5%88%AB/","title":"== 和 equals 的区别"},{"content":"pom.xml 文件是 Maven 项目的核心配置文件，全称为 Project Object Model (POM)，它以 XML 格式定义了项目的构建、依赖、插件、属性、构建生命周期等关键信息。以下是 pom.xml 文件的详细配置说明：\n1. 基本结构 一个典型的 pom.xml 文件结构如下：\n1 2 3 4 5 6 \u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34;?\u0026gt; \u0026lt;project xmlns=\u0026#34;http://maven.apache.org/POM/4.0.0\u0026#34; xmlns:xsi=\u0026#34;http://www.w3.org/2001/XMLSchema-instance\u0026#34; xsi:schemaLocation=\u0026#34;http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd\u0026#34;\u0026gt; \u0026lt;!-- 元素配置 --\u0026gt; \u0026lt;/project\u0026gt; \u0026lt;?xml ... ?\u0026gt;：XML 声明，指定了文件的版本和编码。 \u0026lt;project\u0026gt; 根元素，包含了所有 POM 相关配置。 xmlns 和 xsi 属性定义了 XML 命名空间和对应的 XML 架构位置，确保 Maven 能正确解析 POM 文件。 2. 核心配置元素 2.1 modelVersion 1 \u0026lt;modelVersion\u0026gt;4.0.0\u0026lt;/modelVersion\u0026gt; 指定 POM 的模型版本，通常固定为 4.0.0，表示遵循 Maven 2 或 3 的 POM 规范。\n2.2 groupId 1 \u0026lt;groupId\u0026gt;com.example.myproject\u0026lt;/groupId\u0026gt; 标识项目的唯一性，通常采用反向域名形式，对应项目的组织或公司域名。例如，公司域名为 example.com，项目的 groupId 可设置为 com.example。\n2.3 artifactId 1 \u0026lt;artifactId\u0026gt;my-project\u0026lt;/artifactId\u0026gt; 定义项目的唯一标识符，通常与项目文件夹名称或主 JAR/WAR 文件名相同。在同一 groupId 下，不同项目应有不同的 artifactId。\n2.4 version 1 \u0026lt;version\u0026gt;1.0.0-SNAPSHOT\u0026lt;/version\u0026gt; 设定项目的版本号，遵循语义化版本控制（Semantic Versioning, SemVer）。SNAPSHOT 表示开发版本，发布版则去掉 -SNAPSHOT。\n2.5 packaging 1 \u0026lt;packaging\u0026gt;jar\u0026lt;/packaging\u0026gt; 指定项目打包类型，默认为 jar（Java 库）。其他常见类型包括 war（Web 应用）、pom（聚合项目）等。\n3. 依赖管理 3.1 dependencies 1 2 3 4 5 6 7 \u0026lt;dependencies\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-starter-web\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.0.0\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;/dependencies\u0026gt; 定义项目依赖的其他库或模块。每个 dependency 元素包含 groupId、artifactId 和 version，用于唯一确定依赖项。\n3.2 properties 1 2 3 4 \u0026lt;properties\u0026gt; \u0026lt;java.version\u0026gt;1.8\u0026lt;/java.version\u0026gt; \u0026lt;spring-boot.version\u0026gt;2.7.0\u0026lt;/spring-boot.version\u0026gt; \u0026lt;/properties\u0026gt; 定义项目中可复用的属性值，通常用于存放版本号、路径、变量等，方便在整个 POM 文件中引用。\n3.3 dependencyManagement 1 2 3 4 5 \u0026lt;dependencyManagement\u0026gt; \u0026lt;dependencies\u0026gt; \u0026lt;!-- 依赖版本统一管理 --\u0026gt; \u0026lt;/dependencies\u0026gt; \u0026lt;/dependencyManagement\u0026gt; 用于集中管理项目及其子模块中依赖的版本。在此处声明的依赖版本会被子模块继承，但不会直接引入依赖。\n3.4 exclusions 1 2 3 4 5 6 7 8 9 10 11 \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;...\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;...\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;...\u0026lt;/version\u0026gt; \u0026lt;exclusions\u0026gt; \u0026lt;exclusion\u0026gt; \u0026lt;groupId\u0026gt;...\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;...\u0026lt;/artifactId\u0026gt; \u0026lt;/exclusion\u0026gt; \u0026lt;/exclusions\u0026gt; \u0026lt;/dependency\u0026gt; 在某个依赖中排除特定的传递依赖。\n4. 构建配置 在 Maven 的 pom.xml 文件中，\u0026lt;build\u0026gt; 元素用于定义项目的构建过程和相关配置。以下是 \u0026lt;build\u0026gt; 元素及其子元素的详细说明：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 \u0026lt;build\u0026gt; \u0026lt;pluginManagement\u0026gt; \u0026lt;plugins\u0026gt; \u0026lt;!-- ... --\u0026gt; \u0026lt;/plugins\u0026gt; \u0026lt;/pluginManagement\u0026gt; \u0026lt;plugins\u0026gt; \u0026lt;plugin\u0026gt; \u0026lt;groupId\u0026gt;...\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;...\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;...\u0026lt;/version\u0026gt; \u0026lt;executions\u0026gt; \u0026lt;!-- 插件执行配置 --\u0026gt; \u0026lt;/executions\u0026gt; \u0026lt;configuration\u0026gt; \u0026lt;!-- 插件具体配置 --\u0026gt; \u0026lt;/configuration\u0026gt; \u0026lt;/plugin\u0026gt; \u0026lt;/plugins\u0026gt; \u0026lt;resources\u0026gt; \u0026lt;!-- ... --\u0026gt; \u0026lt;/resources\u0026gt; \u0026lt;testResources\u0026gt; \u0026lt;!-- ... --\u0026gt; \u0026lt;/testResources\u0026gt; \u0026lt;directory\u0026gt;...\u0026lt;/directory\u0026gt; \u0026lt;outputDirectory\u0026gt;...\u0026lt;/outputDirectory\u0026gt; \u0026lt;testOutputDirectory\u0026gt;...\u0026lt;/testOutputDirectory\u0026gt; \u0026lt;sourceDirectory\u0026gt;...\u0026lt;/sourceDirectory\u0026gt; \u0026lt;scriptSourceDirectory\u0026gt;...\u0026lt;/scriptSourceDirectory\u0026gt; \u0026lt;testSourceDirectory\u0026gt;...\u0026lt;/testSourceDirectory\u0026gt; \u0026lt;resourcesDirectory\u0026gt;...\u0026lt;/resourcesDirectory\u0026gt; \u0026lt;testResourcesDirectory\u0026gt;...\u0026lt;/testResourcesDirectory\u0026gt; \u0026lt;filters\u0026gt; \u0026lt;!-- ... --\u0026gt; \u0026lt;/filters\u0026gt; \u0026lt;extensions\u0026gt; \u0026lt;!-- ... --\u0026gt; \u0026lt;/extensions\u0026gt; \u0026lt;finalName\u0026gt;${artifactId}-${version}\u0026lt;/finalName\u0026gt; \u0026lt;defaultGoal\u0026gt;...\u0026lt;/defaultGoal\u0026gt; \u0026lt;sourceEncoding\u0026gt;...\u0026lt;/sourceEncoding\u0026gt; \u0026lt;targetEncoding\u0026gt;...\u0026lt;/targetEncoding\u0026gt; \u0026lt;encoding\u0026gt;...\u0026lt;/encoding\u0026gt; \u0026lt;finalName\u0026gt;...\u0026lt;/finalName\u0026gt; \u0026lt;filtering\u0026gt;...\u0026lt;/filtering\u0026gt; \u0026lt;!-- ... --\u0026gt; \u0026lt;/build\u0026gt; 4.1 Plugin Management \u0026lt;pluginManagement\u0026gt;：集中管理项目及其子模块中插件的版本和配置。在此处声明的插件配置会被子模块继承，但不会直接执行。子模块仍需在 \u0026lt;plugins\u0026gt; 中声明要使用的插件。 4.2 Plugins \u0026lt;plugins\u0026gt;：配置项目使用的 Maven 插件及其执行细节。插件用于扩展 Maven 的构建功能，如编译、打包、测试、代码检查、部署等。 4.3 Directory Settings \u0026lt;directory\u0026gt;：指定项目构建生成文件的根目录。 \u0026lt;outputDirectory\u0026gt;：指定项目主构建结果（如 JAR、WAR 文件）的输出目录。 \u0026lt;testOutputDirectory\u0026gt;：指定项目测试构建结果（如测试报告、测试覆盖率报告）的输出目录。 \u0026lt;sourceDirectory\u0026gt;：指定项目主源代码目录。 \u0026lt;scriptSourceDirectory\u0026gt;：指定项目脚本源代码目录（如 Groovy、Ruby 等）。 \u0026lt;testSourceDirectory\u0026gt;：指定项目测试源代码目录。 \u0026lt;resourcesDirectory\u0026gt;：指定项目主资源文件目录。 \u0026lt;testResourcesDirectory\u0026gt;：指定项目测试资源文件目录。 4.4 Filters \u0026lt;filters\u0026gt;：定义过滤器列表，这些过滤器在资源文件复制过程中用于替换资源文件中的占位符（如 ${property}）。 4.5 Build Extensions \u0026lt;extensions\u0026gt;：配置 Maven 扩展插件，这些插件可以扩展 Maven 的核心功能，如支持新的打包类型、构建生命周期等。 4.6 Other Settings \u0026lt;finalName\u0026gt;：指定项目构建产物（如 JAR、WAR 文件）的最终文件名，默认值是 `${artifactId}-${version}。 \u0026lt;defaultGoal\u0026gt;：指定项目默认的构建目标（Maven 命令中的 mvn [defaultGoal]）。 \u0026lt;sourceEncoding\u0026gt;、\u0026lt;targetEncoding\u0026gt;、\u0026lt;encoding\u0026gt;：指定源代码、构建产物、资源文件的字符编码。 \u0026lt;filtering\u0026gt;：在 \u0026lt;resources\u0026gt; 或 \u0026lt;testResources\u0026gt; 中使用，指定是否对资源文件进行过滤（即替换占位符）。 4.7 Resources \u0026lt;resources\u0026gt;：定义项目主资源文件，如 properties 文件、配置文件、静态资源等。Maven 在构建过程中会将这些资源复制到输出目录中。 \u0026lt;testResources\u0026gt;：定义项目测试资源文件，如测试用到的配置文件、数据文件等。Maven 在构建过程中会将这些资源复制到测试输出目录中。 \u0026lt;build\u0026gt; 元素包含了项目构建过程的方方面面，包括资源文件管理、插件配置、构建目录设置、过滤器定义、扩展插件配置等。合理配置 \u0026lt;build\u0026gt; 元素，可以定制项目的构建过程，满足特定的构建需求。\n5. 其他重要元素 5.1 profiles 1 2 3 4 5 6 7 8 9 10 \u0026lt;profiles\u0026gt; \u0026lt;profile\u0026gt; \u0026lt;id\u0026gt;dev\u0026lt;/id\u0026gt; \u0026lt;activation\u0026gt; \u0026lt;activeByDefault\u0026gt;true\u0026lt;/activeByDefault\u0026gt; \u0026lt;/activation\u0026gt; \u0026lt;!-- 环境特定配置 --\u0026gt; \u0026lt;/profile\u0026gt; \u0026lt;!-- 更多环境配置 --\u0026gt; \u0026lt;/profiles\u0026gt; 定义不同构建环境（如开发、测试、生产）的配置，通过激活特定 profile 来切换环境。\n5.2 repositories \u0026amp; pluginRepositories 1 2 3 4 5 6 7 8 9 10 11 12 13 \u0026lt;repositories\u0026gt; \u0026lt;repository\u0026gt; \u0026lt;id\u0026gt;...\u0026lt;/id\u0026gt; \u0026lt;url\u0026gt;...\u0026lt;/url\u0026gt; \u0026lt;/repository\u0026gt; \u0026lt;/repositories\u0026gt; \u0026lt;pluginRepositories\u0026gt; \u0026lt;pluginRepository\u0026gt; \u0026lt;id\u0026gt;...\u0026lt;/id\u0026gt; \u0026lt;url\u0026gt;...\u0026lt;/url\u0026gt; \u0026lt;/pluginRepository\u0026gt; \u0026lt;/pluginRepositories\u0026gt; 配置项目所需的远程仓库地址，用于下载依赖和插件。\n总结 pom.xml 文件是 Maven 项目的核心配置文件，涵盖了项目基本信息、依赖管理、构建过程、环境配置等多个方面。通过合理配置 pom.xml，可以有效地管理项目依赖、定制构建过程、适应不同环境需求，确保项目的构建、测试、部署等工作高效、规范地进行。\n","date":"2024-04-15T00:00:00Z","image":"/p/pom.xml%E9%85%8D%E7%BD%AE%E5%AD%A6%E4%B9%A0/cover.webp","permalink":"/p/pom.xml%E9%85%8D%E7%BD%AE%E5%AD%A6%E4%B9%A0/","title":"pom.xml配置学习"},{"content":"什么是Apache PDFBox Apache PDFBox是一个开源的Java库，用于处理和操作PDF文档。它提供了一组功能强大的API，可以用于创建、修改、提取内容和元数据、加密、签名以及验证PDF文档。\n以下是Apache PDFBox的一些主要特点和功能：\n创建和编辑PDF文档：Apache PDFBox允许你使用Java代码创建新的PDF文档，并且可以添加文本、图像、表格、链接和其他元素。它还提供了修改现有PDF文档的功能，如添加、删除和更新页面、注释、书签等。\n提取和操作内容：你可以使用Apache PDFBox提取PDF文档中的文本、图像和其他内容。它支持文本搜索、文本替换、图像提取、页面提取等操作，使你能够对PDF文档中的内容进行灵活的处理。\n加密和签名：Apache PDFBox支持对PDF文档进行加密和数字签名，以确保文档的安全性和完整性。你可以使用不同的加密算法和密钥长度来保护PDF文档，并且可以使用数字证书对文档进行签名。\n最重要的是不需要商业许可\n下载字体 阿里巴巴普惠体是一套全球永久免费正版商用的字体家族。阿里巴巴普惠体3.0为一套符合新国家标准GB18030-2022的简体中文字符集，包含GB18030-2022强制规范三个实现级别：实现级别1＋实现级别2标准规格的7字重、实现级别3标准规格的Regular 单一字重。7字重共194,460个全形汉字。包含拉丁字母、希腊字母、西里尔字母、标点符号。\n下载字体：Alibaba PuHuiTi 3.0 - 55 Regular 阿里巴巴普惠体 (alibabagroup.com) 放到resources目录 代码实现 pom 引入 1 2 3 4 5 \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.apache.pdfbox\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;pdfbox\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;2.0.27\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; 代码示例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 import cn.hutool.core.date.DateUtil; import cn.hutool.core.util.NumberUtil; import com.umetrip.hotel.order.constants.LogSchemaOrder; import com.umetrip.hotel.order.constants.OrderErrorCode; import com.umetrip.hotel.order.service.dto.Result; import com.umetrip.hotel.order.service.iface.HotelReceiptService; import com.umetrip.hotel.order.service.iface.dto.ProducePdfRequest; import lombok.extern.slf4j.Slf4j; import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDDocumentInformation; import org.apache.pdfbox.pdmodel.PDPage; import org.apache.pdfbox.pdmodel.PDPageContentStream; import org.apache.pdfbox.pdmodel.common.PDRectangle; import org.apache.pdfbox.pdmodel.font.PDType0Font; import org.apache.pdfbox.pdmodel.graphics.image.JPEGFactory; import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject; import org.springframework.stereotype.Service; import java.awt.Color; import java.io.IOException; import java.io.InputStream; import java.util.Arrays; import java.util.List; public class HotelReceiptServiceImpl implements HotelReceiptService { private PDPageContentStream contentStream; @Override public Result\u0026lt;String\u0026gt; producePdf(ProducePdfRequest producePdfRequest) { try (PDDocument document = new PDDocument()) { PDPage page = new PDPage(PDRectangle.A4); document.addPage(page); // 创建 PDDocumentInformation 对象 PDDocumentInformation pdd = document.getDocumentInformation(); // 设置文档的作者 pdd.setAuthor(\u0026#34;XX平台\u0026#34;); // 设置文档的标题 pdd.setTitle(\u0026#34;XX平台酒店预订账单（水单）\u0026#34;); contentStream = new PDPageContentStream(document, page); InputStream inFont = this.getClass().getClassLoader().getResourceAsStream(\u0026#34;font/AlibabaPuHuiTi-3-55-Regular.ttf\u0026#34;); PDType0Font font = PDType0Font.load(document, inFont); // 设置标题的字体大小 float fontSize = 15; contentStream.setFont(font, fontSize); // 设置标题文本 String title = \u0026#34;XX平台酒店预订账单（水单）\u0026#34;; float titleWidth = font.getStringWidth(title) / 1000 * fontSize; float titleHeight = font.getFontDescriptor().getFontBoundingBox().getHeight() / 1000 * fontSize; float startX = (page.getMediaBox().getWidth() - titleWidth) / 2; // 从页面顶部向下50点开始 float startY = page.getMediaBox().getHeight() - titleHeight - 50; showTextByLeft(title, \u0026#34;\u0026#34;, startX, startY); contentStream.setFont(font, 12); List\u0026lt;String\u0026gt; titleList = Arrays.asList(\u0026#34;酒店名称:\u0026#34;, \u0026#34;订单号:\u0026#34;, \u0026#34;下单时间:\u0026#34;, \u0026#34;入住人:\u0026#34;, \u0026#34;联系手机:\u0026#34;, \u0026#34;房型:\u0026#34;, \u0026#34;入住:\u0026#34;, \u0026#34;离店:\u0026#34;, \u0026#34;\u0026#34;); String checkin = producePdfRequest.getCheckin() + \u0026#34;(\u0026#34; + DateUtil.dayOfWeekEnum(DateUtil.parse(producePdfRequest.getCheckin())).toChinese() + \u0026#34;)\u0026#34;; String checkout = producePdfRequest.getCheckout() + \u0026#34;(\u0026#34; + DateUtil.dayOfWeekEnum(DateUtil.parse(producePdfRequest.getCheckout())).toChinese() + \u0026#34;)\u0026#34;; List\u0026lt;String\u0026gt; valueList = Arrays.asList( producePdfRequest.getHotelName(), producePdfRequest.getOrderNo(), producePdfRequest.getOrderTime(), producePdfRequest.getOccupant(), producePdfRequest.getPhone(), producePdfRequest.getRoomType(), checkin, checkout, \u0026#34;\u0026#34; ); pdfTable(page, 150, 8, 2, titleList, valueList); titleList = Arrays.asList(\u0026#34;项目\u0026#34;, \u0026#34;房费\u0026#34;, \u0026#34;其他费用\u0026#34;, \u0026#34;退款\u0026#34;, \u0026#34;实付\u0026#34;, \u0026#34;\u0026#34;); String actualPayment = NumberUtil.round(NumberUtil.sub(producePdfRequest.getPayAmount(), producePdfRequest.getRefundAmount()), 2).toString(); valueList = Arrays.asList( \u0026#34;金额\u0026#34;, \u0026#34;RMB \u0026#34; + NumberUtil.round(producePdfRequest.getPayAmount(), 2), \u0026#34;RMB 0.00\u0026#34;, \u0026#34;RMB \u0026#34; + NumberUtil.round(producePdfRequest.getRefundAmount(), 2), \u0026#34;RMB \u0026#34; + actualPayment, \u0026#34;\u0026#34; ); pdfTable(page, 350, 5, 2, titleList, valueList); showTextByLeft(String.format(\u0026#34;用户本单通过XX平台平台预订，发票由%s提供。\u0026#34;, producePdfRequest.getReceiptIssuer()), \u0026#34;\u0026#34;, 100, page.getMediaBox().getHeight() - 490); showTextByLeft(String.format(\u0026#34;客服电话%s\u0026#34;, producePdfRequest.getReceiptTel()), \u0026#34;\u0026#34;, 100, page.getMediaBox().getHeight() - 510); InputStream logoFile = this.getClass().getClassLoader().getResourceAsStream(\u0026#34;invoice/XX平台酒店.jpeg\u0026#34;); PDImageXObject logo = JPEGFactory.createFromStream(document, logoFile); // 缩放因子 float logoScale = 0.4f; contentStream.drawImage(logo, 100, page.getMediaBox().getHeight() - 560, logo.getWidth() * logoScale, logo.getHeight() * logoScale); if (producePdfRequest.getIsStamp() == 1) { InputStream imageFile = this.getClass().getClassLoader().getResourceAsStream(\u0026#34;invoice/电子印章.jpeg\u0026#34;); PDImageXObject image = JPEGFactory.createFromStream(document, imageFile); float scale = 0.3f; contentStream.drawImage(image, page.getMediaBox().getWidth() - 270, page.getMediaBox().getHeight() - 670, image.getWidth() * scale, image.getHeight() * scale); } contentStream.close(); String file = String.format(\u0026#34;/opt/applog/MskyLog/umehotel-order-service/XX平台水单%s.pdf\u0026#34;, producePdfRequest.getOrderNo()); document.save(file); return Result.success(file); } catch (IOException e) { log.error(LogSchemaOrder.ERROR_KEY_REQUEST_EXCEPTION, \u0026#34;producePdf\u0026#34;, producePdfRequest, e); return Result.fail(OrderErrorCode.SYSTEM_ERROR); } } /** * pdf表格生成 * * @param page pdf页 * @param yStart 上边距 * @param rows 行数 * @param cols 列数 * @param titleList 固定标题 * @param valueList 动态值 * @throws IOException io */ private void pdfTable(PDPage page, float yStart, int rows, int cols, List\u0026lt;String\u0026gt; titleList, List\u0026lt;String\u0026gt; valueList) throws IOException { // 开始绘制表格 float margin = 100; // top margin yStart = page.getMediaBox().getHeight() - yStart; // 表格宽度 float tableWidth = page.getMediaBox().getWidth() - 2 * 100; // 单元格高度 float rowHeight = 20f; // 表格高度 float tableHeight = rowHeight * rows; // 单元格宽度 float colWidth = tableWidth / (float) cols; // 第一列单元格宽度 float firstColumnWidth = tableWidth * 20 / 100; // 染色 List\u0026lt;String\u0026gt; stainList = Arrays.asList(\u0026#34;酒店名称:\u0026#34;, \u0026#34;订单号:\u0026#34;, \u0026#34;下单时间:\u0026#34;, \u0026#34;入住人:\u0026#34;, \u0026#34;联系手机:\u0026#34;, \u0026#34;房型:\u0026#34;, \u0026#34;入住:\u0026#34;, \u0026#34;离店:\u0026#34;, \u0026#34;项目\u0026#34;, \u0026#34;金额\u0026#34;); // 绘制行 for (int i = 0; i \u0026lt;= rows; i++) { contentStream.moveTo(margin, yStart - i * rowHeight); contentStream.lineTo(margin + tableWidth, yStart - i * rowHeight); contentStream.setNonStrokingColor(Color.black); if (stainList.contains(titleList.get(i))) { contentStream.setNonStrokingColor(Color.gray); } showTextByLeft(titleList.get(i), \u0026#34;\u0026#34;, margin + 5, yStart - i * rowHeight - 15); contentStream.setNonStrokingColor(Color.black); if (stainList.contains(valueList.get(i))) { contentStream.setNonStrokingColor(Color.gray); } showTextByLeft(valueList.get(i), \u0026#34;\u0026#34;, margin + firstColumnWidth + 5, yStart - i * rowHeight - 15); } // 绘制列 for (int i = 0; i \u0026lt;= cols; i++) { float colWidthSpe = colWidth; if (i == 1) { colWidthSpe = firstColumnWidth; } contentStream.moveTo(margin + i * colWidthSpe, yStart); contentStream.lineTo(margin + i * colWidthSpe, yStart - tableHeight); } contentStream.stroke(); } /** * 文本添加 * * @param txt 添加文本 * @param def 默认文本 * @param x x轴 * @param y y轴 * @throws IOException IOException */ private void showTextByLeft(String txt, String def, float x, float y) throws IOException { // Begin the Content stream contentStream.beginText(); // contentStream.setWordSpacing(0.01F); if (null == txt) { txt = def; } // Setting the position for the line contentStream.newLineAtOffset(x, y); // Adding text in the form of string contentStream.showText(txt); // Ending the content stream contentStream.endText(); } } 实现效果 ","date":"2023-07-03T00:00:00Z","image":"/p/%E4%BD%BF%E7%94%A8pdfbox%E7%94%9F%E6%88%90%E9%85%92%E5%BA%97%E6%B0%B4%E5%8D%95/cover.webp","permalink":"/p/%E4%BD%BF%E7%94%A8pdfbox%E7%94%9F%E6%88%90%E9%85%92%E5%BA%97%E6%B0%B4%E5%8D%95/","title":"使用PDFBox生成酒店水单"},{"content":"什么是WSDL WSDL 是用于描述 Web Service 的一种 XML 表示法。 WSDL 定义告诉客户如何编写 Web Service 请求，并且描述了由 Web Service 提供程序提供的接口。\nSoapUI简介 SoapUI是一个开源测试工具，通过soap/http来检查、调用、实现Web Service的功能/负载/符合性测试。该工具既可作为一个单独的测试软件使用，也可利用插件集成到Eclipse，maven2.X，Netbeans 和intellij中使用。\nSoapUI是一个自由和开放源码的跨平台功能测试解决方案。通过一个易于使用的图形界面和企业级功能，SoapUI让您轻松， 快速创建和执行自动化功能、回归、合规和负载测试。在一个测试环境，SoapUI提供完整的测试覆盖，并支持所有的标准协议和技术。\n对接流程 下载wsdl文件 启动一个wsdl服务 导出wsdl文件\n把wsdl文件放入resources目录\npom引入 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 \u0026lt;plugin\u0026gt; \u0026lt;groupId\u0026gt;org.codehaus.mojo\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;jaxws-maven-plugin\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;2.6\u0026lt;/version\u0026gt; \u0026lt;executions\u0026gt; \u0026lt;execution\u0026gt; \u0026lt;goals\u0026gt; \u0026lt;goal\u0026gt;wsimport\u0026lt;/goal\u0026gt; \u0026lt;/goals\u0026gt; \u0026lt;configuration\u0026gt; \u0026lt;!--\u0026lt;wsdlUrls\u0026gt;--\u0026gt; \u0026lt;!--\u0026lt;wsdlUrl\u0026gt;${basedir}/src/main/resources/goldenTax/zkiService_1.wsdl\u0026lt;/wsdlUrl\u0026gt;--\u0026gt; \u0026lt;!--\u0026lt;/wsdlUrls\u0026gt;--\u0026gt; \u0026lt;wsdlDirectory\u0026gt;${project.basedir}/src/main/resources/goldenTax\u0026lt;/wsdlDirectory\u0026gt; \u0026lt;wsdlFiles\u0026gt; \u0026lt;wsdlFile\u0026gt;zkiService_1.wsdl\u0026lt;/wsdlFile\u0026gt; \u0026lt;/wsdlFiles\u0026gt; \u0026lt;wsdlLocation\u0026gt;http://example.com/eisp-lslx/ws/zkiService?wsdl\u0026lt;/wsdlLocation\u0026gt; \u0026lt;!--\u0026lt;wsdlLocation\u0026gt;${project.build.directory}/goldenTax/zkiService_1.wsdl\u0026lt;/wsdlLocation\u0026gt;--\u0026gt; \u0026lt;packageName\u0026gt;com.umetrip.hotel.proxy.service.wsdl.goldenTax\u0026lt;/packageName\u0026gt; \u0026lt;/configuration\u0026gt; \u0026lt;/execution\u0026gt; \u0026lt;/executions\u0026gt; \u0026lt;/plugin\u0026gt; wsdlFiles 指定用于生成Java代码的WSDL文件。 wsdlLocation 指定运行时应用程序中的WSDL文件位置，一般来说都是引用本地文件。由于我用不到pom里面的配置，我就直接写了一个wsdl文件的地址。\n代码调整 ZKInvoiceService 和 AsyncInvoiceServiceService 为wsdl文件生成的类\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 private ZKInvoiceService getAsyncInvoiceServiceService() { // 获取代理配置 String proxyName = ApolloUtil.getNamespaceConfig(ApolloConstant.GOLDEN_TAX, \u0026#34;proxyName\u0026#34;, \u0026#34;hotel:jinshui\u0026#34;); // 获取代理地址 String baseUrl = UmeProxyUtil.getProxy(proxyName); if (StringUtils.isEmpty(baseUrl)) { baseUrl = \u0026#34;http://example.com\u0026#34;; } // 拼接金税接口请求地址 String apiUrlPath = ApolloUtil.getNamespaceConfig(ApolloConstant.GOLDEN_TAX, \u0026#34;apiUrlPath\u0026#34;, \u0026#34;/eisp-lslx/ws/zkiService\u0026#34;); String url = baseUrl + apiUrlPath; // 获取打包文件夹内的wsdl文件流 URL wsdlLocation = this.getClass().getResource(\u0026#34;/goldenTax/zkiService_1.wsdl\u0026#34;); // 修改在pom中设置的wsdlLocation AsyncInvoiceServiceService asyncInvoiceServiceService = new AsyncInvoiceServiceService(wsdlLocation); ZKInvoiceService zkInvoiceService = asyncInvoiceServiceService.getAsyncInvoiceServicePort(); BindingProvider bindingProvider = (BindingProvider) zkInvoiceService; // 修改请求的接口地址（soap:address location） bindingProvider.getRequestContext().put(BindingProvider.ENDPOINT_ADDRESS_PROPERTY, url); return zkInvoiceService; } 备注 由于打包和运行不在同一个环境内，生成的Java代码一直找不到pom文件 wsdlLocation 配置的wsdl文件，所以在代码内部重新引入运行时需要用到的wsdl文件。 如果没有正向代理不需要修改请求的接口地址（soap:address location）。 ","date":"2023-05-15T00:00:00Z","image":"/p/%E5%AF%B9%E6%8E%A5%E9%87%91%E7%A8%8Ewsdl%E8%AE%B0%E5%BD%95/cover.webp","permalink":"/p/%E5%AF%B9%E6%8E%A5%E9%87%91%E7%A8%8Ewsdl%E8%AE%B0%E5%BD%95/","title":"对接金税wsdl记录"},{"content":"背景 公司把服务从阿里云迁移自有机房中，需要对多个php服务进行容器化，在阿里云可对负载均衡的服务进行单台配置定时任务，容器由于是统一的镜像，无法对单台配置定时任务。\n当时有两种解决方案：1. 对每个定时任务进行改造加锁；2. 合并php服务的所有代码单独开一台容器，后期维护两套代码。\n迁移时间的紧迫感和后续维护的困难，这两种方案都让人难以接受，遂通过搜索引擎搜寻解决方案，发现了git子模块的功能。\nGit子模块介绍 有种情况我们经常会遇到：某个工作中的项目需要包含并使用另一个项目。 也许是第三方库，或者你独立开发的，用于多个父项目的库。 现在问题来了：你想要把它们当做两个独立的项目，同时又想在一个项目中使用另一个。\n我们举一个例子。 假设你正在开发一个网站然后创建了 Atom 订阅。 你决定使用一个库，而不是写自己的 Atom 生成代码。 你可能不得不通过 CPAN 安装或 Ruby gem 来包含共享库中的代码，或者将源代码直接拷贝到自己的项目中。 如果将这个库包含进来，那么无论用何种方式都很难定制它，部署则更加困难，因为你必须确保每一个客户端都包含该库。 如果将代码复制到自己的项目中，那么你做的任何自定义修改都会使合并上游的改动变得困难。\nGit 通过子模块来解决这个问题。 子模块允许你将一个 Git 仓库作为另一个 Git 仓库的子目录。 它能让你将另一个仓库克隆到自己的项目中，同时还保持提交的独立。\n实践 创建子模块服务 由于不同环境开启的定时任务不同，不同环境使用的分支无法进行合并\n创建生产master分支 1 2 3 4 5 6 7 8 9 10 11 12 git clone http://gitlab.com/php-cron.git cd php-cron/ # 项目过大 git config --global http.postBuffer 524288000 git submodule add http://gitlab.com/test1.git git submodule add http://gitlab.com/test2.git # 配置对应各git子目录的分支 git config -f .gitmodules submodule.test1.branch master;git config -f .gitmodules submodule.test2.branch master; git add -A git commit -m \u0026#34;init\u0026#34; git push 创建测试dev分支 1 2 3 4 5 6 7 8 git checkout -b dev # 配置对应各git子目录的分支 git config -f .gitmodules submodule.test1.branch dev;git config -f .gitmodules submodule.test2.branch dev; git add -A git commit -m \u0026#34;dev\u0026#34; git push origin dev # 查看效果 git submodule update --remote 克隆子模块项目 1 git clone --recurse-submodules http://gitlab.com/php-cron.git 更新代码：git submodule update --remote\n流水线发布命令 git子模块与正常的git拉取代码有差异，需要修改Jenkins拉取代码命令。\n1 git clone http://gitlab.com/php-cron.git -b dev;cd php-cron;git submodule update --init;git submodule update --remote git submodule init 用来初始化本地配置文件\ngit submodule update 则从该项目中抓取所有数据并检出父项目中列出的合适的提交\ngit submodule update --remote Git 将会进入子模块然后抓取并更新。\n结论 使用git子模块后，后续只需维护原服务代码即可，发布php-cron流水线时会自动拉取各子项目的最新代码。\n参考链接 Git - 子模块 (git-scm.com)\n","date":"2022-12-15T00:00:00Z","image":"/p/git%E5%AD%90%E6%A8%A1%E5%9D%97/cover.webp","permalink":"/p/git%E5%AD%90%E6%A8%A1%E5%9D%97/","title":"Git子模块"},{"content":"使用场景 长期趋势分析：通过对监控样本数据的持续收集和统计，对监控指标进行长期趋势分析。例如，通过对磁盘空间增长率的判断，我们可以提前预测在未来什么时间节点上需要对资源进行扩容。 对照分析：两个版本的系统运行资源使用情况的差异如何？在不同容量情况下系统的并发和负载变化如何？通过监控能够方便的对系统进行跟踪和比较。 告警：当系统出现或者即将出现故障时，监控系统需要迅速反应并通知管理员，从而能够对问题进行快速的处理或者提前预防问题的发生，避免出现对业务的影响。 故障分析与定位：当问题发生后，需要对问题进行调查和处理。通过对不同监控监控以及历史数据的分析，能够找到并解决根源问题。 数据可视化：通过可视化仪表盘能够直接获取系统的运行状态、资源使用情况、以及服务运行状态等直观的信息。 如何使用 引入软件包 composer require promphp/prometheus_client_php\n封装公共类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 use Prometheus\\CollectorRegistry; use Prometheus\\Storage\\Redis; use Prometheus\\RenderTextFormat; class Prometheus { public static $registry; public static $renderer; public static $counter; public function __construct() { // 连接redis $adapter = new Redis(); self::$registry = new CollectorRegistry($adapter); self::$renderer = new RenderTextFormat($adapter); } // 打点方法 public function registerCounter($prometheus) { if (!empty($prometheus[\u0026#39;action\u0026#39;])) { $namespace = $prometheus[\u0026#39;namespace\u0026#39;]; $name = $prometheus[\u0026#39;name\u0026#39;]; // it increases对应promethus counter数据类型 self::$counter = self::$registry-\u0026gt;registerCounter($namespace, $name, \u0026#39;it increases\u0026#39;, [\u0026#39;type\u0026#39;]); if (is_array($prometheus[\u0026#39;action\u0026#39;])) { foreach ($prometheus[\u0026#39;action\u0026#39;] as $v) { // 将统计结果增加1 self::$counter-\u0026gt;incBy(1, [$v]); } } else { // 将统计结果增加1 self::$counter-\u0026gt;incBy(1, [$prometheus[\u0026#39;action\u0026#39;]]); } } } // 输出结果给prometheus服务 public function RenderTextFormat(){ $result = self::$renderer-\u0026gt;render(self::$registry-\u0026gt;getMetricFamilySamples()); header(\u0026#39;Content-type: \u0026#39; . RenderTextFormat::MIME_TYPE); return $result; } } 示例 下面代码表示给登录成功次数进行打点记录\n1 2 3 4 5 6 7 $prometheus = new Prometheus(); $pthParam = [ \u0026#39;action\u0026#39; =\u0026gt; \u0026#39;loginSuccess\u0026#39;, // 要打点的数据 \u0026#39;namespace\u0026#39; =\u0026gt; \u0026#39;service\u0026#39;, // 命名空间 \u0026#39;name\u0026#39; =\u0026gt; \u0026#39;loginApi\u0026#39;, // 名称 ]; $prometheus-\u0026gt;registerCounter($pthParam); 扩展阅读 Prometheus简介 · Prometheus中文技术文档 第1章 天降奇兵 - prometheus-book (gitbook.io) prometheus四种数据类型_puppycuty的博客-CSDN博客_prometheus 类型 ","date":"2022-12-13T00:00:00Z","image":"/p/php%E4%BD%BF%E7%94%A8prometheus/cover.webp","permalink":"/p/php%E4%BD%BF%E7%94%A8prometheus/","title":"php使用prometheus"},{"content":"什么是javax.validation JSR303 是一套JavaBean参数校验的标准，它定义了很多常用的校验注解，我们可以直接将这些注解加在我们JavaBean的属性上面(面向注解编程的时代)，就可以省略很多繁琐的串行校验代码。\n引入依赖包 可根据需要，自行调整依赖包版本。\n1 2 3 4 5 6 7 8 9 10 11 12 \u0026lt;!--jsr 303--\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;javax.validation\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;validation-api\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.1.0.Final\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!-- hibernate validator--\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.hibernate\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;hibernate-validator\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;5.2.0.Final\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; 接管validation抛出的异常 @ControllerAdvice 是Spring3.2提供的新注解，它是一个Controller增强器 ，可对controller中被 @RequestMapping、@getmapping、@postmapping、@deletemapping注解的方法加一些逻辑处理。\n最常用的就是异常处理 @ControllerAdvice需要配合@ExceptionHandler使用。 当将异常抛到controller时，可以对异常进行统一处理，规定返回的json格式或是跳转到一个错误页面。\nvalidation的异常类是MethodArgumentNotValidException.class，下面的代码对validation异常进行捕获并统一返回格式。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 import lombok.extern.slf4j.Slf4j; import org.springframework.validation.BindingResult; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseBody; /** * @author ydong */ @ControllerAdvice @Slf4j public class ValidatorException { @ExceptionHandler(MethodArgumentNotValidException.class) @ResponseBody public S2cRespWrap exceptionHandler(MethodArgumentNotValidException methodArgumentNotValidException) { RespBean result = new RespBean(); BindingResult bindingResult = methodArgumentNotValidException.getBindingResult(); // 创建封装校验出错信息map // HashMap\u0026lt;String, String\u0026gt; errorMap = new HashMap\u0026lt;\u0026gt;(); // 遍历所有校验出错字段 // bindingResult.getFieldErrors().forEach(field -\u0026gt; { // errorMap.put(field.getField(), field.getDefaultMessage()); // }); String errorMsg = \u0026#34;捕获异常失败\u0026#34;; if (bindingResult.getAllErrors().size() \u0026gt; 0) { errorMsg = bindingResult.getAllErrors().get(0).getDefaultMessage(); } result.setErrorMsg(errorMsg); result.setErrorCode(-2240001); return UmeCommonRuler.getResponse(result); } } 简单验证示例 更多用法：搜索引擎搜索“validator”\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import org.hibernate.validator.constraints.NotBlank; import org.hibernate.validator.constraints.Range; import javax.validation.constraints.Min; import javax.validation.constraints.NotNull; @NotNull(message = \u0026#34;page:页数必须\u0026#34;) @Min(value = 1, message = \u0026#34;page需大于0\u0026#34;) private Integer page; @NotNull(message = \u0026#34;size必须\u0026#34;) @Range(min = 1, max = 50, message = \u0026#34;size要求在1-50内\u0026#34;) private Integer size; @NotBlank(message = \u0026#34;酒店ID必须\u0026#34;) private String hotelId; 自定义注解验证 待补充\n思考 可使用@ControllerAdvice接管特殊异常进行飞书推送，提高异常响应率\n参考链接 https://blog.csdn.net/xnn_fjj/article/details/100603270 https://blog.csdn.net/Cjava_math/article/details/120001150 https://blog.csdn.net/qq_41175358/article/details/121254789\n","date":"2022-10-25T00:00:00Z","image":"/p/java%E8%AF%B7%E6%B1%82%E5%8F%82%E6%95%B0%E6%B3%A8%E8%A7%A3%E9%AA%8C%E8%AF%81%E5%B9%B6%E7%BB%9F%E4%B8%80%E5%A4%84%E7%90%86/cover.webp","permalink":"/p/java%E8%AF%B7%E6%B1%82%E5%8F%82%E6%95%B0%E6%B3%A8%E8%A7%A3%E9%AA%8C%E8%AF%81%E5%B9%B6%E7%BB%9F%E4%B8%80%E5%A4%84%E7%90%86/","title":"Java请求参数注解验证并统一处理"},{"content":"了解Consul Consul 是一种服务网格解决方案，提供具有服务发现、配置和分段功能的全功能控制平面。这些功能中的每一个都可以根据需要单独使用，也可以一起使用以构建完整的服务网格。\n如何注册 文档：https://www.consul.io/api-docs/agent/service\n引入软件包 Composer PHP的软件包管理系统，它提供用于管理PHP软件和依赖库关系的标准格式。\n搜索软件包 composer软件包地址：https://packagist.org/\n根据PHP版本引入扩展包 我的php版本是7.2，所以我选择了v4.2.1版本。 composer require sensiolabs/consul-php-sdk:v4.2.1\n注册Consul php注册代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 public function actionConsulRegister() { // 只注册一台到consul $redisKey = \u0026#34;SERVICE:CONSULREGISTER\u0026#34;; $predis = \\Yii::$app-\u0026gt;predis; $result = $predis-\u0026gt;client-\u0026gt;get($redisKey); if ($result) { echo \u0026#34;no\u0026#34;; exit; } $result = $predis-\u0026gt;client-\u0026gt;set($redisKey, 1); $expireTime = 60; $predis-\u0026gt;client-\u0026gt;expire($redisKey, $expireTime); putenv(\u0026#34;CONSUL_HTTP_ADDR=CONSUL_URL\u0026#34;); $kv = new \\SensioLabs\\Consul\\Services\\Agent(); $port = \u0026#34;80\u0026#34;; $ip = getHostByName(getHostName()); $array = [ \u0026#39;id\u0026#39; =\u0026gt; \u0026#34;{$ip}:{$port}\u0026#34;, \u0026#39;name\u0026#39; =\u0026gt; \u0026#39;prometheus\u0026#39;, \u0026#39;tags\u0026#39; =\u0026gt; [\u0026#39;prometheus_v1\u0026#39;], \u0026#39;address\u0026#39; =\u0026gt; $ip, \u0026#39;port\u0026#39; =\u0026gt; (int)$port, \u0026#39;enabletagoverride\u0026#39; =\u0026gt; false, \u0026#39;check\u0026#39; =\u0026gt; array( // \u0026#39;deregistercriticalserviceafter\u0026#39; =\u0026gt; \u0026#39;90m\u0026#39;, \u0026#39;http\u0026#39; =\u0026gt; \u0026#34;http://{$ip}:{$port}/metrics\u0026#34;, \u0026#39;interval\u0026#39; =\u0026gt; \u0026#34;15s\u0026#34;, ) ]; $kv-\u0026gt;registerService($array); echo \u0026#34;success\u0026#34;; exit; } 代码释义 1 2 3 4 5 6 7 8 9 10 11 // 只注册一台到consul $redisKey = \u0026#34;HTL:HOTELDETAIL:CONSULREGISTER\u0026#34;; $predis = \\Yii::$app-\u0026gt;predis; $result = $predis-\u0026gt;client-\u0026gt;get($redisKey); if ($result) { echo \u0026#34;no\u0026#34;; exit; } $result = $predis-\u0026gt;client-\u0026gt;set($redisKey, 1); $expireTime = 60; $predis-\u0026gt;client-\u0026gt;expire($redisKey, $expireTime); 为什么要有redis锁 因php不能像java一样自请求，是通过容器增加启动钩子访问容器内的php服务注册consul，由于php的promethues数据是存在redis中，如果注册两台会造成prometheus获取的打点数据double。\nconsul注册参数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 // consul地址 putenv(\u0026#34;CONSUL_HTTP_ADDR=CONSUL_URL\u0026#34;); $kv = new \\SensioLabs\\Consul\\Services\\Agent(); $port = \u0026#34;80\u0026#34;; // 获取本机ip $ip = getHostByName(getHostName()); $array = [ // 服务id \u0026#39;id\u0026#39; =\u0026gt; \u0026#34;{$ip}:{$port}\u0026#34;, // 服务名称 \u0026#39;name\u0026#39; =\u0026gt; \u0026#39;prometheus\u0026#39;, // 服务tag，数组，自定义，可以根据这个tag来区分同一个服务名的服务 \u0026#39;tags\u0026#39; =\u0026gt; [\u0026#39;prometheus_v1\u0026#39;], // 服务注册到consul的IP，服务发现，发现的就是这个IP \u0026#39;address\u0026#39; =\u0026gt; $ip, // 服务IP对应端口号 \u0026#39;port\u0026#39; =\u0026gt; (int)$port, \u0026#39;enabletagoverride\u0026#39; =\u0026gt; false, \u0026#39;check\u0026#39; =\u0026gt; array( // 如果服务启动失败，多少时间内注销consul上的该服务。单位：minute // \u0026#39;deregistercriticalserviceafter\u0026#39; =\u0026gt; \u0026#39;90m\u0026#39;, // 指定健康检查的URL，调用后只要返回20X，consul都认为是健康的 \u0026#39;http\u0026#39; =\u0026gt; \u0026#34;http://{$ip}:{$port}/metrics\u0026#34;, // 健康检查间隔时间 \u0026#39;interval\u0026#39; =\u0026gt; \u0026#34;15s\u0026#34;, ) ]; $kv-\u0026gt;registerService($array); deregisterCriticalServiceAfter： 由于consul是安装在虚拟机上无法访问容器服务，所以不设置该参数，避免自动注销注册。\n注册结果 ","date":"2022-08-30T00:00:00Z","image":"/p/php%E6%B3%A8%E5%86%8Cconsul/cover.webp","permalink":"/p/php%E6%B3%A8%E5%86%8Cconsul/","title":"php注册consul"},{"content":"MySQL读写分离的原理 主库将变更写入binlog日志，然后从库连接到主库之后，从库有一个IO线程，将主库的binlog日志拷贝到自己本地，写入一个relay中继日志中。接着从库中有一个SQL线程会从中继日志读取binlog，然后执行binlog日志中的内容，也就是在自己本地再次执行一遍SQL，这样就可以保证自己跟主库的数据是一样的。\n实践环境 系统：macOS 工具：Docker CE\n获取MySQL镜像\n1 docker pull mysql:8.0.23 创建容器网络，用于固定mysql容器ip\n1 docker network create --subnet=172.18.0.0/24 docker-network 创建配置文件 master.cnf\n1 2 3 4 5 6 7 [mysqld] #设置此参数表示启用binlog功能，并制定二进制日志的存储目录 log-bin=/var/lib/mysql/mysql-bin #数据库实例标识 server-id=101 #binlog 日志格式 binlog-format=MIXED slave.cnf\n1 2 3 4 5 6 7 [mysqld] #设置此参数表示启用binlog功能，并制定二进制日志的存储目录 log-bin=/var/lib/mysql/mysql-bin #数据库实例标识 server-id=102 #binlog 日志格式 binlog-format=MIXED 启动两个MySQL容器 Master\n1 2 3 4 5 6 docker run \\ -p 3306:3306 \\ -v /mysql/master.cnf:/etc/mysql/conf.d/replication.cnf \\ -v /mysql/master/data/:/var/lib/mysql \\ --net docker-network --ip 172.18.0.101 \\ --name mysql-master -e MYSQL_ROOT_PASSWORD=123456 -d mysql:8.0.23 Slave\n1 2 3 4 5 6 docker run \\ -p 3307:3306 \\ -v /mysql/slave.cnf:/etc/mysql/conf.d/replication.cnf \\ -v /mysql/slave/data/:/var/lib/mysql \\ --net docker-network --ip 172.18.0.102 \\ --name mysql-slave -e MYSQL_ROOT_PASSWORD=123456 -d mysql:8.0.23 配置主从复制 进入mysql-master容器\n1 docker exec -it mysql-master bash 登陆MySQL查看master状态\n1 show master status; 进入mysql-slave容器\n1 docker exec -it mysql-slave bash 执行mysql命令\n1 2 3 4 5 change master to master_host=\u0026#39;172.18.0.101\u0026#39;, master_user=\u0026#39;root\u0026#39;, master_password=\u0026#39;123456\u0026#39;, master_log_file=\u0026#39;mysql-bin.000003\u0026#39;, master_log_pos=156; 启动slave\n1 start slave; 查看slave是否成功\n1 show slave status \\G; 这两个状态都为Yes才代表成功\n1 2 Slave_IO_Running: Yes Slave_SQL_Running: Yes 通过截图所示错误（不安全的连接），在mysql-slave中执行以下命令\n1 mysql --ssl-mode=DISABLED -h 172.18.0.101 -uroot -p123456 --get-server-public-key 参考链接：https://www.cnblogs.com/summertime-wu/p/11637520.html\n重新登入mysql-slave数据库查看slave状态\n在mysql-master创建一个库\n1 create database test; 在mysql-slave mysql中执行show databases;\n成功！\n","date":"2020-09-05T00:00:00Z","image":"/p/mysql%E8%AF%BB%E5%86%99%E5%88%86%E7%A6%BB%E5%AE%9E%E8%B7%B5/cover.webp","permalink":"/p/mysql%E8%AF%BB%E5%86%99%E5%88%86%E7%A6%BB%E5%AE%9E%E8%B7%B5/","title":"MySQL读写分离实践"},{"content":"进制 四种进制 10 进制 decimal 简写：dec\n16 进制 hexadecimal 简写：hex\n8 进制 octonary 简写：oct\n2 进制 binary 简写：bin\n10 进制 十进制数由不以0开头的数字序列表示\n1 2 3 4 ... 万位 千位 百位 十位 个位 （满10进1） 9 9 9 9 9 echo 123; //从右到左 3个位，分别时以 1顶1，以1顶10，以1顶100 echo 1*3 + 10*2 + 100*1; 16 进制（满16进1） 十六进制数是以0x开头，接着是数字（0 ～ 9）或字母（A ～ F）的序列。字母可以是大写或小写，通常是大写\n1 2 echo 0x123; //从右到左 3 个位，分别是以 1顶1，以 1顶16， 以 1顶256（16的n次方） echo 0x1c; //12*1 + 1*16 = 28 8 进制（满8进1） 八进制数是有一个前导0和一个从0～7的数字序列组成\n1 2 3 echo 0123; //从右到左 3 个位，分别是以 1顶1，以 1顶8，以 1顶64（8的n次方） echo 027; //1*7 + 2*8 = 23 echo 028; //2 当出现不合法值，后面的值会忽略 2 进制（满2进1） 二进制数以0b开始，接着由一个数字（0 和 1）序列组成\n1 2 3 //从右到左，分别以 1顶1，以 1顶2， 顶4， 顶8， 顶16 ..... （2的n次方） echo 0b0011; //1*1 + 1*2 = 3 echo 0b01100; // 0*1 + 0*2 + 1*4 + 1*8 = 12 进制转换 10 进制转换其他进制，除以该进制，直到除尽或除不尽即可\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 133 转 2 进制 ______ 2 | 133 余1 ______ 2 | 66 余0 ______ 2 | 33 余1 ______ 2 | 16 余0 ______ 2 | 8 余0 ______ 2 | 4 余0 ______ 2 | 2 余0 ______ | 1 0b10000101 // 1*1 + 0*2 + 1*4 + 0*8 + 0*16 + 0*32 + 0*64 + 1*128 = 133 133 转 8 0205 133 转 16 0x85 位运算 一个字节有8个位，我们将这8个位来进行运算，就叫位运算\n位运算 是针对字节上的位来进行运算\n位运算符： \u0026amp; | ^ ~ \u0026raquo; \u0026laquo;\n位上有 1/0，如果将 1/0 当真假来看，1是真 0是假\n\u0026amp; 逻辑与 1 2 3 4 5 5 0000 0101 12 0000 1100 \u0026amp;------------- 0000 0100 echo 5 \u0026amp; 12; //4 | 逻辑或 1 2 3 4 5 5 0000 0101 12 0000 1100 |------------- 0000 1101 echo 5 | 12; //13 ^ 逻辑异或 逻辑异或,两者不一样则为真 一样为假\n1 2 3 4 5 5 0000 0101 12 0000 1100 ^------------- 0000 1001 echo 5 ^ 12; //9 ~ 逻辑反 逻辑反，只能求一个字符，就是 1-\u0026gt;0 ， 0-\u0026gt;1\n1 2 3 4 5 0000 0101 ~------------- 1111 1010 echo ~5; //-6 PHP 的 int 型则是有符号的\n取决于8个位的 最左面的位(最高位) 是否为1\n如果为 1 则是 负数 -1~-128\n如果为 0 则是 整数 0~127\n\u0026lt;\u0026lt; \u0026gt;\u0026gt; 位运算\n\u0026lt;\u0026lt; 按位左移 空位补0\n\u0026gt;\u0026gt; 按位右移 空位补0\n1 2 3 4 5 6 7 8 9 5 0000 0101 \u0026lt;\u0026lt;------------ 0000 1010 echo 5\u0026lt;\u0026lt;1; //10 5 0000 0101 \u0026gt;\u0026gt;------------ 0000 0010 echo 5\u0026gt;\u0026gt;1; //2 2 进制，左移一位增大2倍\n将一个数快速的增大8倍\n1 echo 2\u0026lt;\u0026lt;3; //16 按位移动比乘法要快，计算机最后是存储2进制的，省了转换\n","date":"2018-05-18T00:00:00Z","image":"/p/%E8%BF%9B%E5%88%B6%E5%92%8C%E4%BD%8D%E8%BF%90%E7%AE%97/cover.webp","permalink":"/p/%E8%BF%9B%E5%88%B6%E5%92%8C%E4%BD%8D%E8%BF%90%E7%AE%97/","title":"进制和位运算"}]