[{"content":"问题 新上线的 Bakong 查询任务未执行，也没有输出查询日志，导致 Bakong 订单无法通知。\n排查 日志分析 日志关键词：notifyBakongSyncData\n向前检查日志，发现 PostgreSQL 偶尔执行得非常慢，耗时高达 39 秒。\nJStack 排查 命令：\njstack pid | grep -C[显示上下行数] tid \u0026gt; jstack.log jstack 1 | grep -C1000 12 \u0026gt; jstack.log 得到线程栈日志：\njstack.txt\n关键日志：\n\u0026#34;scheduled-task-2\u0026#34; #23 prio=5 os_prio=0 tid=0x00007fd64de9d800 nid=0x1f runnable [0x00007fd5e3dfd000] java.lang.Thread.State: RUNNABLE at java.net.SocketInputStream.socketRead0(Native Method) at java.net.SocketInputStream.read(SocketInputStream.java:170) ... at org.postgresql.core.VisibleBufferedInputStream.readMore(VisibleBufferedInputStream.java:161) at org.postgresql.core.VisibleBufferedInputStream.ensureBytes(VisibleBufferedInputStream.java:128) at org.postgresql.core.VisibleBufferedInputStream.read(VisibleBufferedInputStream.java:73) at org.postgresql.core.PGStream.receiveChar(PGStream.java:441) at org.postgresql.core.v3.QueryExecutorImpl.processResults(QueryExecutorImpl.java:2057) at org.postgresql.core.v3.QueryExecutorImpl.execute(QueryExecutorImpl.java:323) - locked \u0026lt;0x00000000fe51b788\u0026gt; (a org.postgresql.core.v3.QueryExecutorImpl) ... at com.chaos.bakong.notifier.query.job.OrderNotifyDataSyncJob.notifyBakongSyncData(OrderNotifyDataSyncJob.java:50) 分析：\nscheduled-task-2 处于 RUNNABLE 状态，但卡在 SocketInputStream.socketRead0——这是阻塞式网络读取。 该线程正在等待 PostgreSQL 返回查询结果；数据库迟迟未响应，导致它一直占用调度线程池中的一个工作线程。 为什么会一直卡在：SocketInputStream.socketRead0\n参考 PostgreSQL JDBC 驱动的连接参数文档。\n文档说明：\nsocketTimeout（int）的默认值为 0。 它用于设置 Socket 读取操作的超时时间；如果从服务器读取数据的耗时超过该值，连接将被关闭。 此参数既可作为强制性的全局查询超时机制，也可用于检测网络问题。 超时时间以秒为单位，最大可设置为 2,147,484 秒（约 24.8 天）；值为 0 表示禁用超时，即无限等待。 查看配置：\n数据库连接没有配置 socketTimeout 参数。\n结论 结合 PostgreSQL JDBC 官方文档和 JStack 日志，可以确认：\n数据库查询偶尔会超时，存在读写失败和延迟较高的情况。 PostgreSQL 连接未设置 socketTimeout，无限等待导致任务假死。 方案 配置连接超时参数 增加 socketTimeout、loginTimeout 和 connectTimeout，解决连接、读写 PostgreSQL 超时导致的假死问题。\njdbc:postgresql://103.173.187.2:55439/tps?connectTimeout=10000\u0026amp;loginTimeout=10\u0026amp;socketTimeout=10 connectTimeout=10000\u0026amp; # TCP 连接超时：10000 毫秒 = 10 秒 loginTimeout=10\u0026amp; # 登录总超时：10 秒（单位：秒） socketTimeout=10 # 查询读取超时：10 秒（单位：秒） connectTimeout 会被传递给 Socket.connect(address, timeout)，而 Java 的 Socket.connect() 方法中 timeout 参数的单位是毫秒。参见 PGProperty.java。\n观察测试环境 PostgreSQL 数据库 经排查确认，由于广州到金边的网络状况以及测试环境本身的限制，数据库连接和查询超时属于正常预期。\n验证 测试环境增加参数后，超过 10 秒的连接会按预期超时报错；定时任务不会中断，仍可继续运行，假死问题得到解决。\n","permalink":"https://2d992714.xianglin2020-github-io.pages.dev/posts/2026/bakong-order-notification-task-troubleshooting/","summary":"通过日志与 JStack 定位 Bakong 订单通知任务因 PostgreSQL 网络读取无限等待而假死的问题，并用连接超时参数完成修复验证。","title":"测试环境 Bakong 订单通知任务不执行问题排查"},{"content":"本文记录的是微信支付 API v2 退款通知的 req_info 解密问题。API v3 的回调格式和 AES-256-GCM 解密流程不同，不能直接套用本文结论。\n问题现象 项目使用 weixin-java-pay 3.9.0 解析退款通知：\nWxPayRefundNotifyResult notifyResult; try { notifyResult = WxPayRefundNotifyResult.fromXML( reqBody, routeBO.getChannelPublicKey() ); } catch (WxPayException ex) { log.error(\u0026#34;微信退款通知处理失败\u0026#34;, ex); throw new PendingException( FundChannelCodeEnum.RESPONSE_EXTERNAL_CHANNEL_SIGN_FAILED ); } if (Objects.equals( WxpayMsgCdEnum.FAIL.getCode(), notifyResult.getReturnCode() )) { throw new PendingException( FundChannelCodeEnum.REQUEST_EXTERNAL_CHANNEL_FAILED ); } String payNo = notifyResult.getReqInfo().getOutRefundNo(); 上线后解密失败，核心异常如下：\ncom.github.binarywang.wxpay.exception.WxPayException: 解密退款通知加密信息时出错 Caused by: java.security.InvalidKeyException: Illegal key size or default parameters at javax.crypto.Cipher.checkCryptoPerm(Cipher.java:1026) at javax.crypto.Cipher.init(Cipher.java:1249) at com.github.binarywang.wxpay.bean.notify .WxPayRefundNotifyResult.fromXML(...) 同一份通知密文和商户 API 密钥在本地可以正常解密，线上却稳定失败。\n排查思路 先排除密钥和报文问题 最初怀疑密钥错误，但同一密钥可以完成其他微信支付交互，而且把脱敏后的线上报文放到本地能够成功解析。\n如果密钥内容错误，通常会在实际解密或填充校验阶段失败；这里异常发生在 Cipher.init 的密钥长度权限检查阶段，方向更接近运行环境限制。\n对比 Java 运行环境 线上同时存在两个 Java 8 版本：\nJava 8u101：解密失败，异常与线上日志一致。 Java 8u301：同一报文和密钥解密成功。 微信支付 API v2 的退款通知解密需要 AES-256。旧 JRE 使用受限 JCE 策略时，AES 最大允许密钥长度只有 128 位，因此初始化 256 位密钥会抛出 Illegal key size or default parameters。\n可以直接在目标运行环境检查：\nint maxKeyLength = Cipher.getMaxAllowedKeyLength(\u0026#34;AES\u0026#34;); System.out.println(\u0026#34;AES max key length = \u0026#34; + maxKeyLength); 如果输出 128，说明当前 JCE 策略受限；无限制策略通常会返回一个远大于 256 的值。\n解决方案 最终将服务运行环境从 Java 8u101 升级到 Java 8u301，解密恢复正常。\n升级 JDK 是优先方案，因为早期 Java 8 除了加密策略限制，还缺少大量后续安全修复。无法立即升级时，才考虑为对应旧版本安装匹配的 JCE Unlimited Strength Policy。\n从 JDK 8u161 开始，JDK 内置 limited 和 unlimited 两套策略，并通过 java.security 中的 crypto.policy 控制。升级安装可能保留旧策略文件，因此不要只根据版本号推断，应该同时检查：\ncrypto.policy=unlimited 并用 Cipher.getMaxAllowedKeyLength(\u0026quot;AES\u0026quot;) 验证实际生效结果。参考 Oracle JCA 加密强度配置。\n日志安全问题 原始排查代码曾把完整通知报文和商户密钥写入错误日志：\nlog.error( \u0026#34;微信退款通知处理失败 reqBody {} mchKey {}\u0026#34;, reqBody, merchantKey, ex ); 这会把支付数据和核心密钥暴露给日志系统，风险远高于解密失败本身。生产代码应改为只记录可用于关联的非敏感字段：\nlog.error( \u0026#34;微信退款通知解密失败, merchantId={}, notifyId={}\u0026#34;, merchantId, notifyId, ex ); 商户 API 密钥不应以任何形式写入日志。通知 Body 如确需留存，也应进行字段脱敏、访问控制和生命周期管理。\n获取旧 JDK 复现 旧版本 Oracle JDK 可以从 Java Archive 获取：\n需要注意：\n下载旧版本通常需要 Oracle 账号。 存档页面展示版本与最终下载文件应再次核对。 旧 JDK 只用于隔离的复现环境，不应重新部署到生产。 排查清单 遇到同类异常时，可以依次确认：\n记录实际 Java Vendor、版本和启动路径。 调用 Cipher.getMaxAllowedKeyLength(\u0026quot;AES\u0026quot;) 检查策略。 使用同一份脱敏报文在不同 JDK 中做对照测试。 检查 java.security、crypto.policy 和遗留 JCE Policy 文件。 优先升级到受支持的 JDK，并重新验证回调。 全面搜索日志，移除密钥和敏感报文输出。 ","permalink":"https://2d992714.xianglin2020-github-io.pages.dev/posts/2025/wechat-pay-refund-notification-decryption/","summary":"定位微信支付 API v2 退款通知在旧版 JDK 中因 JCE 密钥长度限制而解密失败的问题。","title":"微信支付退款通知解密失败排查"},{"content":"开发支付、订单或消息通知时，经常需要确认：\n系统是否真的发出了回调。 请求方法、Header 和 Content-Type 是否正确。 商户实际收到的 Body 与预期是否一致。 重试时请求内容是否发生变化。 Webhook.site 会生成一个临时 URL，并展示发送到该 URL 的 HTTP 请求，适合开发和测试阶段快速排查。\n获取临时回调地址 打开 Webhook.site 后，页面会生成一个唯一 URL：\n复制该地址，配置到测试环境的商户网关或通知地址中：\n优先使用 HTTPS 地址。只有在明确了解风险且测试环境确实不支持 HTTPS 时，才考虑临时使用 HTTP。\n触发并检查通知 执行一笔测试交易或主动触发通知后，回到 Webhook.site 查看捕获的请求：\n检查时重点关注：\nHTTP 方法是否符合约定，例如 POST。 Content-Type 与 Body 格式是否一致。 签名、时间戳、随机数等 Header 是否齐全。 订单号、状态、金额和币种是否正确。 系统重试时，幂等标识是否保持一致。 安全边界 Webhook.site 是外部第三方服务，只应接收脱敏的开发或测试数据。\n不要发送生产商户密钥、Authorization Token、Cookie 或完整用户信息。 不要把生产支付通知直接指向临时地址。 测试完成后及时移除临时 URL。 必须调试敏感数据时，应在受控环境部署内部请求收集服务。 这个工具适合回答“请求有没有发出、具体发了什么”，但不能替代正式的签名校验、幂等处理、重试监控和回调审计。\n","permalink":"https://2d992714.xianglin2020-github-io.pages.dev/posts/2025/webhook-site-testing/","summary":"使用 Webhook.site 临时接收和检查 HTTP 回调，验证请求方法、请求头及通知内容。","title":"使用 Webhook.site 调试回调通知"},{"content":"问题现象 使用 MyBatis 执行包含多个参数的 Mapper 方法时，可能遇到“找不到参数”的异常：\n异常中可选参数只有 arg0、arg1 或 param1、param2，却没有源代码中的 tradeNo。最直接且稳定的解决方式，是显式使用 @Param：\nPayOrder findOrder( @Param(\u0026#34;tradeNo\u0026#34;) String tradeNo, @Param(\u0026#34;merchantId\u0026#34;) String merchantId ); 另一条思路是在编译时保留真实参数名，这就是 javac -parameters 的作用。\n-parameters 做了什么 默认编译时，方法参数的真实名称不一定写入 .class 文件。启用 -parameters 后，编译器会把构造器和方法的形式参数名写入 MethodParameters 属性，反射 API 便可以读取它们。\n示例类：\nclass ParameterNameDemo { void test(String name) { System.out.println(name); } } 分别编译：\njavac ParameterNameDemo.java javac -parameters ParameterNameDemo.java 通过 javap 查看详细字节码：\njavap -verbose ParameterNameDemo.class 启用选项后，可以看到 MethodParameters 中保存了 name：\n也可以通过反射验证：\nMethod method = ParameterNameDemo.class .getDeclaredMethod(\u0026#34;test\u0026#34;, String.class); for (Parameter parameter : method.getParameters()) { System.out.println(parameter.getName()); System.out.println(parameter.isNamePresent()); } 在构建工具中启用 Maven 使用 maven-compiler-plugin：\n\u0026lt;plugin\u0026gt; \u0026lt;groupId\u0026gt;org.apache.maven.plugins\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;maven-compiler-plugin\u0026lt;/artifactId\u0026gt; \u0026lt;configuration\u0026gt; \u0026lt;parameters\u0026gt;true\u0026lt;/parameters\u0026gt; \u0026lt;/configuration\u0026gt; \u0026lt;/plugin\u0026gt; 也可以使用原始编译参数：\n\u0026lt;compilerArgs\u0026gt; \u0026lt;arg\u0026gt;-parameters\u0026lt;/arg\u0026gt; \u0026lt;/compilerArgs\u0026gt; Gradle tasks.withType(JavaCompile).configureEach { options.compilerArgs += [\u0026#39;-parameters\u0026#39;] } 与 MyBatis 的关系 MyBatis 可以在运行时读取参数名，但能否读取到真实名称取决于：\n编译产物是否包含 MethodParameters。 MyBatis 的 useActualParamName 配置及版本行为。 方法是否被代理、混淆或经过额外字节码处理。 因此，即使全局启用了 -parameters，对关键 Mapper 方法继续使用 @Param 仍然更明确，也能减少构建配置差异造成的问题。\n不要把调试信息中的局部变量表与 MethodParameters 混为一谈：-g 和 -parameters 保存的是不同类型的字节码元数据。\n参考：Oracle javac -parameters 说明。\n","permalink":"https://2d992714.xianglin2020-github-io.pages.dev/posts/2025/java-parameters-compiler-option/","summary":"解释 javac 的 -parameters 选项如何把真实方法参数名写入 class 文件，并给出 Maven、Gradle 和 MyBatis 中的用法。","title":"Java 编译时保留方法参数名"},{"content":"这篇文章合并两个相关场景：\nUAT 环境中的 Dubbo 消费者连接了错误的注册中心。 本地开发时，只让某个接口直连本机提供者，其余接口继续使用注册中心。 示例基于 Dubbo 2.6.x。不同大版本的配置模型有所变化，排查时应以项目实际依赖的源码和对应版本文档为准。\n注册中心配置被覆盖 背景 账单缴费服务需要调用收银台 Dubbo API。项目已有 Dubbo 依赖，因此通过 API 方式创建消费者：\ncompile(\u0026#39;com.chaos:cashier-api:6.8.1.0-SNAPSHOT\u0026#39;) 核心配置可以简化为：\n@Configuration public class DubboConsumerConfig { private final Environment environment; public DubboConsumerConfig(Environment environment) { this.environment = environment; } @Bean public ApplicationConfig applicationConfig() { return new ApplicationConfig( environment.getProperty(\u0026#34;spring.application.name\u0026#34;) ); } @Bean public RegistryConfig registryConfig() { return new RegistryConfig( environment.getRequiredProperty(\u0026#34;dubbo.registry-address\u0026#34;) ); } @Bean public CashierPaymentFacade cashierPaymentFacade() { ConsumerConfig consumer = new ConsumerConfig(); consumer.setTimeout(20_000); ReferenceConfig\u0026lt;CashierPaymentFacade\u0026gt; reference = new ReferenceConfig\u0026lt;\u0026gt;(); reference.setApplication(applicationConfig()); reference.setRegistry(registryConfig()); reference.setInterface(CashierPaymentFacade.class); reference.setVersion(\u0026#34;1.0.0\u0026#34;); reference.setGroup(\u0026#34;chaos\u0026#34;); reference.setConsumer(consumer); return reference.get(); } } 本地开发正常，但部署到 UAT 后，服务启动时报错：\nFailed to check the status of the service ... No provider available for the service ... 错误日志中的 ZooKeeper 地址与 Apollo 中配置的地址不一致。\n排查过程 首先打印 Spring Environment 中的配置：\nString address = environment.getRequiredProperty(\u0026#34;dubbo.registry-address\u0026#34;); log.info(\u0026#34;dubbo.registry-address={}\u0026#34;, address); 输出正确。随后比较创建和注入的 RegistryConfig，也确认使用的是同一个对象。这说明问题不在 Spring Bean 的创建过程，而是在 Dubbo 后续读取并覆盖属性时发生。\n调试 Dubbo 2.6.10 的 AbstractConfig#appendProperties，可以看到它会读取 JVM System Property：\nif (value == null || value.length() == 0) { String propertyName = prefix + property; value = System.getProperty(propertyName); if (!StringUtils.isBlank(value)) { logger.info( \u0026#34;Use System Property \u0026#34; + propertyName + \u0026#34; to config dubbo\u0026#34; ); } } 继续检查 Kubernetes Pod YAML，发现启动参数里确实存在：\n-Ddubbo.registry.address=zookeeper://old-registry.example:2181 根因与处理 -Ddubbo.registry.address 是 JVM System Property，其优先级高于应用中设置的注册中心地址，导致 Apollo 配置没有成为最终值。\n既然注册中心地址已经由 Apollo 明确管理，删除重复的 -Ddubbo.registry.address 即可。配置来源越多，环境差异越难排查；同一个配置项应尽量只保留一个权威来源。\n可以参考 Apache Dubbo 配置原理。\n本地按接口直连提供者 本地调试时，可能只需要让一个接口访问本机提供者，而其他接口仍访问集成环境。Dubbo 提供了接口级服务映射能力。\n参考：Dubbo 直连提供者。\n创建映射文件 默认情况下，可以在用户主目录创建 dubbo-resolve.properties。\nWindows 示例：\nC:\\Users\\Administrator\\dubbo-resolve.properties 在文件中使用“接口全限定名 = 直连 URL”的格式：\ncom.chaos.cashier.api.CashierConfManageFacade=dubbo://localhost:20485 其中 20485 是提供方的 dubbo.protocol.port。\n如果不希望使用默认位置，也可以通过 JVM 参数指定文件：\n-Ddubbo.resolve.file=C:\\dev\\dubbo-resolve.properties 配置为何能够生效 Dubbo 2.6.9 的 ReferenceConfig#init 会按以下顺序解析：\n读取以接口全限定名为 key 的 System Property。 读取 dubbo.resolve.file 指定的文件。 如果仍未指定，则查找 ${user.home}/dubbo-resolve.properties。 用接口全限定名从映射文件读取直连 URL。 核心源码如下：\nString resolve = System.getProperty(interfaceName); String resolveFile = null; if (resolve == null || resolve.length() == 0) { resolveFile = System.getProperty(\u0026#34;dubbo.resolve.file\u0026#34;); if (resolveFile == null || resolveFile.length() == 0) { File userResolveFile = new File( new File(System.getProperty(\u0026#34;user.home\u0026#34;)), \u0026#34;dubbo-resolve.properties\u0026#34; ); if (userResolveFile.exists()) { resolveFile = userResolveFile.getAbsolutePath(); } } if (resolveFile != null \u0026amp;\u0026amp; resolveFile.length() \u0026gt; 0) { Properties properties = new Properties(); try (FileInputStream input = new FileInputStream(new File(resolveFile))) { properties.load(input); } resolve = properties.getProperty(interfaceName); } } 使用文件映射的好处 多个项目可以共享一份本地直连配置。 注释一行即可恢复注册中心调用，切换速度快。 不必修改项目 XML 或 Java 配置，避免把本地调试地址误提交到 Git。 直连只适合开发和临时排障。测试结束后应及时移除映射，避免绕过注册中心的路由、治理和可用性能力。\n","permalink":"https://2d992714.xianglin2020-github-io.pages.dev/posts/2025/dubbo-config-priority-and-direct-connection/","summary":"记录 Dubbo 2.6 消费者注册中心配置被 JVM 参数覆盖的排查过程，以及本地开发时按接口直连提供者的方法。","title":"Dubbo 实践：配置优先级与消费者直连"},{"content":"本地启动 Web 服务时，如果提示端口已经被占用，可以按“端口 → PID → 进程”的顺序排查。不要一上来就强制结束进程，先确认它不是数据库、代理或其他仍在使用的服务。\n使用 netstat 定位进程 以端口 8080 为例：\nnetstat -ano | findstr \u0026#34;:8080\u0026#34; 输出的最后一列是 PID。使用冒号可以减少把 8080 误匹配到其他数字中的情况，但仍应检查本地地址和连接状态，优先关注 LISTENING。\n假设查到 PID 为 1234：\ntasklist /FI \u0026#34;PID eq 1234\u0026#34; 确认进程后再结束它：\ntaskkill /PID 1234 /F /F 表示强制终止，进程没有机会正常释放资源或保存数据。能通过应用自身的停止命令退出时，应优先正常停止。\n使用 PowerShell 命令 在较新的 Windows 版本中，也可以直接使用 PowerShell：\n$connection = Get-NetTCPConnection -LocalPort 8080 -State Listen $connection | Select-Object LocalAddress, LocalPort, OwningProcess Get-Process -Id $connection.OwningProcess 确认无误后：\nStop-Process -Id $connection.OwningProcess 仅在普通停止失败且确认可以强制终止时使用：\nStop-Process -Id $connection.OwningProcess -Force 常见问题 查询系统进程或结束其他用户的进程时，终端可能需要以管理员身份运行。 如果进程由 Windows 服务、Docker 或进程守护器管理，直接结束后可能会自动重启，应修改对应服务配置。 开发服务也可以改用其他端口，避免终止仍有用途的进程。 ","permalink":"https://2d992714.xianglin2020-github-io.pages.dev/posts/2025/windows-port-process-management/","summary":"在 Windows 中根据端口定位监听进程，确认进程身份后安全地终止或调整服务。","title":"Windows 排查和处理端口占用"},{"content":"问题背景 fundchannel-service 服务单节点负载高。\n定时任务分析 Bakong 入金定时器 BakongIncomJob\n*/2 * * * * ?\n主要功能 查询 BAKONG_INCOMING_ORDER 中 SUCCESS 状态的数据，发送消息让其它服务完成后续处理 查询 BAKONG_INCOMING_ORDER 中 PENDING 状态的数据，调用 BAKONG 接口获取最新状态 潜在问题 涉及到外部调用，较为耗时\n查询不命中索引\nselect * from BAKONG_INCOMING_ORDER where lower(STATUS) = lower(\u0026#39;PENDING\u0026#39;) and SEND_COUNT \u0026lt; 1 and QUERY_TIME \u0026lt; sysdate and query_count \u0026lt; 50 and ORDER_TYPE IN (\u0026#39;0\u0026#39;, \u0026#39;1\u0026#39;, \u0026#39;2\u0026#39;, \u0026#39;4\u0026#39;, \u0026#39;5\u0026#39;, \u0026#39;7\u0026#39;, \u0026#39;10\u0026#39;) and rownum \u0026lt; 20 Bakong 二维码定时器 BakongQrCodeJob\n0/10 * * * * ?\n主要功能 BAKONG 生成商户二维码时，可选将 qrData 和 md5 保存起来，后续根据 md5 查询 Bakong 获取 transaction_hash，再做入金操作。\n潜在问题 7月26日左右切换过地址，切换后的地址连不上 https://api-bakong.nbc.gov.kh/v1/check_transaction_by_md5 http://172.24.21.125/v1/check_transaction_by_md5\n交易会有推送（/api/v1/transactions/callback），这个查询可以不用\n一个二维码多笔交易，这个接口怎么获取？\n向外部渠道查询交易/退款状态 主要功能 查询处理中的数据，向对应渠道获取交易/退款状态\n潜在问题 定时任务过多且执行频率高，有12个任务做类似的功能\n查询没有索引，且 create_time \u0026gt; sysdate - 30 条件不算合理\n频繁创建销毁线程池，得不偿失\nCountDownLatch countDownLatch = new CountDownLatch(payJrnList.size()); int maxThreadNum = Math.min(payJrnList.size(), 10); ExecutorService executor = Executors.newFixedThreadPool(maxThreadNum); try { payJrnList.forEach(n -\u0026gt; { executor.submit(() -\u0026gt; { try { maintainingState(n); } catch (Exception e) { } finally { countDownLatch.countDown(); } }); }); countDownLatch.await(); } catch (InterruptedException e) { log.error(\u0026#34;查询订单异常\u0026#34;, e); } finally { executor.shutdown(); } 不需要查询结果的交易多次出现在任务中（主要是通过 BAKONG 向银行转账）\n分析总结 现有数据分析 有交易数据的支付机构 select * from (select j.*, row_number() over (partition by PAY_ORG order by CREATE_TIME desc ) as rn from PAY_FUND_CHANNEL.PAY_JRN j where j.TRADE_STATE = 12) where rn = 1; 各个支付机构交易成功前任务查询的次数 select PAY_ORG,max(QUERIES),min(QUERIES),avg(QUERIES),STATS_MODE(QUERIES) from PAY_FUND_CHANNEL.PAY_JRN j where j.TRADE_STATE = 12 and CREATE_TIME \u0026gt; date \u0026#39;2025-01-01\u0026#39; group by PAY_ORG 定时任务问题总结 前面两个定时任务运行正常，主要针对查询交易/退款终态的定时任务，总结两点主要问题：\n需要查询状态的交易记录没有做重试次数限制和重试时间退避，有查询了 250W 次的记录 在大表 PAY_JRN（记录1500W+） 中频繁执行没有索引的查询 此外，由于一批交易查询结果的耗时不固定，没办法较为精确的控制同一笔交易两次查询状态执行的间隔。\n建议解决方案 针对问题总结中提及的两点，能确定优化的方向就是合理控制重试次数以及减少查询次数。\n基于定时任务方案的优化 将待查询终态的记录单独存放 创建 PAY_JRN 记录时同步往新表 PAY_JRN_PROCESSING 插入记录，存放处理中的记录，以及查询次数等信息\n处理成功后，或者重试次数达到阈值后将记录删除，保持新表较少数据量\n合理重试 基于上面现有数据的分析，做如下假设：\n前 10 次保持 1 秒查询一次（要注意前一次查询完成后再发起下一次查询），后续按指数退避策略，或者斐波那契额数列作为间隔时间\n达到阈值（一周）后不再处理，由人工介入\n代码优化 有以下两点可以优化：\n正确使用线程池，线程池不能频繁创建销毁； 查询中明确返回【失败】结果，而不是非【成功】就返回【处理中】，这样能避免后续无用的重复查询 基于延迟队列方案的优化 使用延迟队列配合重试完成交易记录终态的获取，步骤如下：\n交易成功发起后，发送一个待查询终态的消息到中间件，消息包括交易ID、查询次数（下次查询时间）\n消费消息，查询交易终态，如果未到终态，将查询次数（下次查询时间）按规则递增后继续发送待查询终态的消息\n当查询到终态时更新交易状态（或查询次数达到阈值时），不再发送消息\n这样做有两点好处：\n避免频繁查询/修改数据库，减少数据库压力 可以较为精确的控制两次查询的间隔（因为是在当前查询完成后才发送消息决定下次查询时间） 有以下多种延迟队列方案可选：\nJDK 内置 DelayQueue public class DelayQueueTest { public static void main(String[] args) throws InterruptedException { DelayQueue\u0026lt;DelayTask\u0026gt; delayQueue = new DelayQueue\u0026lt;\u0026gt;(); delayQueue.add(new DelayTask(\u0026#34;delay-1\u0026#34;, 1, 1)); delayQueue.add(new DelayTask(\u0026#34;delay-2\u0026#34;, 1, 2)); while (!delayQueue.isEmpty()) { DelayTask task = delayQueue.take(); System.out.println(\u0026#34;doing task...\u0026#34;); System.out.println(task); if (task.count \u0026gt; 3) { System.out.println(task.taskId + \u0026#34; task count is over\u0026#34;); } else { DelayTask newTask = new DelayTask(task.taskId, task.count + 1, (int) Math.pow(2, task.count)); delayQueue.add(newTask); } } } static class DelayTask implements Delayed { private final String taskId; private final int count; private final long nextTime; public DelayTask(String taskId, int count, int delay) { this.taskId = taskId; this.count = count; this.nextTime = System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(delay); } @Override public long getDelay(@NotNull TimeUnit unit) { return unit.convert(nextTime - System.currentTimeMillis(), TimeUnit.MILLISECONDS); } @Override public int compareTo(@NotNull Delayed o) { return Long.compare(this.nextTime, ((DelayTask) o).nextTime); } @Override public String toString() { return \u0026#34;DelayTask{\u0026#34; + \u0026#34;taskId=\u0026#39;\u0026#34; + taskId + \u0026#39;\\\u0026#39;\u0026#39; + \u0026#34;, count=\u0026#34; + count + \u0026#34;, nextTime=\u0026#34; + nextTime + \u0026#39;}\u0026#39;; } } } 基于 Netty 的时间轮 public class HashedWheelTimerDemo { public static void main(String[] args) { TimeUnit unit = TimeUnit.SECONDS; HashedWheelTimer timer = new HashedWheelTimer(1, unit); timer.newTimeout(new Task(timer, \u0026#34;task-1\u0026#34;, 1), 1, unit); timer.newTimeout(new Task(timer, \u0026#34;task-2\u0026#34;, 1), 3, unit); } static class Task implements TimerTask { private final String taskId; private final int count; private final HashedWheelTimer timer; public Task(HashedWheelTimer timer, String taskId, int count) { this.taskId = taskId; this.count = count; this.timer = timer; } @Override public void run(Timeout timeout) { System.out.println(\u0026#34;doing task...\u0026#34; + taskId); if (count \u0026lt; 3) { timer.newTimeout(new Task(timer, taskId, count + 1), (int) Math.pow(count, 2), TimeUnit.MINUTES); } else { System.out.println(taskId + \u0026#34; task count is over\u0026#34;); } } } } RocketMQ 延迟消息 https://rocketmq.apache.org/zh/docs/4.x/producer/04message3\nRedisson 延迟队列 https://redisson.pro/docs/data-and-services/queues/#delayed-queue\npublic class RDelayedQueueTest { public static void main(String[] args) throws InterruptedException { Config config = new Config(); config.useSentinelServers() .setMasterName(\u0026#34;mymaster\u0026#34;) .setSentinelAddresses(Arrays.asList(\u0026#34;redis://redis-node-0.redis-headless.cool-tool-uat.svc.cluster.local:26379\u0026#34;, \u0026#34;redis://redis-node-1.redis-headless.cool-tool-uat.svc.cluster.local:26379\u0026#34;, \u0026#34;redis://redis-node-2.redis-headless.cool-tool-uat.svc.cluster.local:26379\u0026#34;)); RedissonClient client = Redisson.create(config); RBlockingQueue\u0026lt;Object\u0026gt; destQueue = client.getBlockingQueue(\u0026#34;dest_queue\u0026#34;); RDelayedQueue\u0026lt;Object\u0026gt; delayedQueue = client.getDelayedQueue(destQueue); delayedQueue.offer(\u0026#34;test-task-1\u0026#34;, 5, TimeUnit.SECONDS); delayedQueue.offer(\u0026#34;test-task-2\u0026#34;, 8, TimeUnit.SECONDS); delayedQueue.offer(\u0026#34;test-task-3\u0026#34;, 10, TimeUnit.SECONDS); while (true) { Object task = destQueue.take(); System.out.println(task); } // delayedQueue.destroy(); // client.shutdown(); } } ","permalink":"https://2d992714.xianglin2020-github-io.pages.dev/posts/2025/fundchannel-core-service-scheduled-task-optimization/","summary":"分析 fundchannel-core-service 定时任务的性能问题，并比较定时任务优化与延迟队列两类改造方案。","title":"渠道服务 fundchannel-core-service 定时任务优化"},{"content":"Super CSV 可以通过 Bean 字段映射和 CellProcessor 完成格式转换，适合从 Web 接口流式导出 CSV。\n导出流程 完整流程可以拆成五步：\n设置下载响应头和 UTF-8 编码。 定义列处理器、展示表头和 Bean 字段映射。 在内容最前面写入 UTF-8 BOM。 创建 CsvBeanWriter 并写入表头。 分页查询数据并持续写入响应流。 代码示例 public void exportChannelBalanceHistory( @RequestBody ChannelBalanceHistoryQueryDTO queryDTO, HttpServletResponse response ) throws IOException { String fileName = \u0026#34;ChannelBalanceHistory_\u0026#34; + DateUtil.getformatDDMMYYYY() + \u0026#34;.csv\u0026#34;; response.setHeader(\u0026#34;Access-Control-Expose-Headers\u0026#34;, \u0026#34;Content-Disposition\u0026#34;); response.setHeader( \u0026#34;Content-Disposition\u0026#34;, \u0026#34;attachment; filename=\\\u0026#34;\u0026#34; + fileName + \u0026#34;\\\u0026#34;\u0026#34; ); response.setCharacterEncoding(StandardCharsets.UTF_8.name()); response.setContentType(\u0026#34;text/csv;charset=UTF-8\u0026#34;); String language = Context.getLanguage(); CellProcessor[] processors = new CellProcessor[]{ new FmtDate(\u0026#34;dd/MM/yyyy\u0026#34;), new Optional(), new Optional(), new Optional(), new Optional(), new Optional(), new Optional(new CellProcessorAdaptor() { @Override @SuppressWarnings(\u0026#34;unchecked\u0026#34;) public \u0026lt;T\u0026gt; T execute(Object value, CsvContext context) { if (value instanceof Integer) { boolean enabled = ((Integer) value) == 1; String result = Objects.equals(language, Context.DEFAULT_LANGUAGE) ? (enabled ? \u0026#34;启动\u0026#34; : \u0026#34;关闭\u0026#34;) : (enabled ? \u0026#34;Open\u0026#34; : \u0026#34;Closed\u0026#34;); return (T) result; } return (T) value; } }) }; String[] header = Objects.equals(language, Context.DEFAULT_LANGUAGE) ? new String[]{\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;} : new String[]{\u0026#34;Balance Date\u0026#34;, \u0026#34;Operator\u0026#34;, \u0026#34;Currency\u0026#34;, \u0026#34;Balance USD\u0026#34;, \u0026#34;Warning Amount\u0026#34;, \u0026#34;Email\u0026#34;, \u0026#34;Status\u0026#34;}; String[] nameMapping = { \u0026#34;balanceDate\u0026#34;, \u0026#34;channelName\u0026#34;, \u0026#34;currency\u0026#34;, \u0026#34;balance\u0026#34;, \u0026#34;warnBalance\u0026#34;, \u0026#34;warnMails\u0026#34;, \u0026#34;doneWarn\u0026#34; }; PrintWriter writer = response.getWriter(); // Excel 打开包含中文的 UTF-8 CSV 时，可通过 BOM 正确识别编码。 writer.write(\u0026#39;\\uFEFF\u0026#39;); try (CsvBeanWriter csvWriter = new CsvBeanWriter(writer, CsvPreference.STANDARD_PREFERENCE)) { csvWriter.writeHeader(header); int pageNo = 1; final int pageSize = 1000; PageInfoDTO\u0026lt;ChannelBalanceHistoryRespDTO\u0026gt; page; do { queryDTO.setPageNo(pageNo++); queryDTO.setPageSize(pageSize); page = bossRechargeOrderHandlerFacade.queryChannelBalanceHistory(queryDTO); for (ChannelBalanceHistoryRespDTO item : page.getList()) { csvWriter.write(item, nameMapping, processors); } } while (page.getList().size() == pageSize); } } 容易踩坑的地方 BOM 必须先于 CSV 内容写入 UTF-8 BOM 的字节序列是 EF BB BF，也可以直接通过 writer.write('\\uFEFF') 写入。必须先写 BOM，再创建并使用 CsvBeanWriter，否则 BOM 可能出现在首个单元格中。\n不要混用 Writer 和 OutputStream 同一个 HttpServletResponse 不能同时调用 getWriter() 和 getOutputStream()。决定使用字符流后，BOM 和 CSV 内容都应写入同一个 Writer。\n表头与字段映射必须一一对应 header 决定用户看到的列名，nameMapping 决定从 Bean 读取哪个属性，processors 决定如何转换该值。三组数组的长度和顺序必须保持一致。\n分页导出要有稳定顺序 数据库查询应指定稳定且唯一的排序条件，避免翻页期间出现重复或遗漏。数据量非常大时，还应考虑游标/键集分页，避免深分页越来越慢。\n防止 CSV 公式注入 如果导出内容来自用户输入，以 =, +, -, @ 开头的单元格可能被表格软件当作公式执行。对不可信文本应转义或增加安全前缀。\n","permalink":"https://2d992714.xianglin2020-github-io.pages.dev/posts/2025/super-csv-export/","summary":"在 Spring Web 接口中使用 Super CSV 分页导出数据，处理字段映射、国际化表头、UTF-8 BOM 和响应流。","title":"使用 Super CSV 导出大批量 CSV 文件"},{"content":"这篇文章整理两个可以直接复用的 Java 片段：获取国际电话区号，以及为不稳定的外部调用增加指数退避重试。\n获取国家和地区的国际区号 Google libphonenumber 内置了电话号码元数据，可以从 ISO 3166-1 地区代码映射到国际区号。\nMaven 依赖可以统一由项目的版本管理控制：\n\u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;com.googlecode.libphonenumber\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;libphonenumber\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;${libphonenumber.version}\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; 保留地区与区号的对应关系 PhoneNumberUtil phoneUtil = PhoneNumberUtil.getInstance(); Map\u0026lt;String, Integer\u0026gt; regionToCallingCode = phoneUtil.getSupportedRegions() .stream() .sorted() .collect(Collectors.toMap( Function.identity(), phoneUtil::getCountryCodeForRegion, (left, right) -\u0026gt; left, LinkedHashMap::new )); 这里得到的是“地区代码 → 国际电话区号”，例如 CN → 86。它不是城市固定电话的国内区号。\n只获取不重复的区号 多个地区可能共享同一个国际区号，因此只需要数字列表时应去重：\nList\u0026lt;Integer\u0026gt; callingCodes = phoneUtil.getSupportedRegions() .stream() .map(phoneUtil::getCountryCodeForRegion) .distinct() .sorted() .collect(Collectors.toList()); 电话号码规则会变化，应用应定期升级 libphonenumber 元数据，而不是把结果永久固化。\n使用指数退避执行重试 指数退避会逐步拉长两次调用之间的等待时间，适用于短暂网络故障、服务限流或临时不可用。它不应掩盖参数错误、权限错误等不可恢复异常。\n引入 guava-retrying \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;com.github.rholder\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;guava-retrying\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;2.0.0\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; 配置 Retryer private final Retryer\u0026lt;Void\u0026gt; retryer = RetryerBuilder.\u0026lt;Void\u0026gt;newBuilder() .retryIfException() .withStopStrategy(StopStrategies.stopAfterAttempt(5)) .withWaitStrategy( WaitStrategies.exponentialWait(1000, 32, TimeUnit.SECONDS) ) .build(); 这段配置最多执行 5 次（包含首次调用），等待时间按指数增长并封顶在 32 秒。具体等待序列由库内部的尝试次数计算决定，不要把它理解为固定定时任务。\n执行业务调用 try { retryer.call(() -\u0026gt; { remoteClient.send(); return null; }); } catch (RetryException e) { // 已达到停止条件 throw new IllegalStateException(\u0026#34;调用重试后仍然失败\u0026#34;, e); } catch (ExecutionException e) { // Callable 执行过程中出现无法继续处理的异常 throw new IllegalStateException(\u0026#34;调用执行失败\u0026#34;, e.getCause()); } 生产使用建议 不要无差别重试所有异常，应尽量限定为超时、连接重置、HTTP 429/503 等可恢复错误。 对非幂等写操作增加幂等键，避免重试造成重复扣款或重复创建订单。 指数退避最好叠加随机抖动，避免大量实例同时重试形成“惊群”。 记录尝试次数、最终失败原因和总耗时，便于监控重试是否正在放大故障。 ","permalink":"https://2d992714.xianglin2020-github-io.pages.dev/posts/2025/java-country-calling-codes-and-exponential-backoff/","summary":"使用 libphonenumber 获取国家/地区国际区号，并通过 guava-retrying 实现带指数退避的有限重试。","title":"Java 实用片段：国家区号与指数退避重试"},{"content":"这篇文章合并两类常用操作：一类用于排查已有 Oracle 数据库，另一类用于初始化新的业务用户。示例中的 Schema、表名、文件路径和密码都需要按实际环境替换。\n如果数据库采用 CDB/PDB 架构，应先连接到目标 PDB，再创建本地用户和表空间。\n常用诊断查询 查询指定表的索引 下面的 SQL 会列出索引类型、唯一性、状态、表空间以及索引列顺序：\nSELECT i.owner AS schema_name, i.index_name, i.index_type, i.uniqueness, i.status, c.column_name, c.column_position, i.tablespace_name FROM dba_indexes i JOIN dba_ind_columns c ON i.owner = c.index_owner AND i.index_name = c.index_name WHERE i.table_name = \u0026#39;CHANNEL_FUND_JRN\u0026#39; AND i.owner = \u0026#39;PAY_FUND_CHANNEL\u0026#39; ORDER BY i.index_name, c.column_position; DBA_INDEXES 和 DBA_IND_COLUMNS 需要相应的数据字典权限。普通用户没有权限时，可以根据可见范围改用 ALL_INDEXES、ALL_IND_COLUMNS 或 USER_INDEXES、USER_IND_COLUMNS。\n查询共享 SQL 区中的高消耗语句 SELECT sql_id, sql_text, executions, ROUND(elapsed_time / 1000000, 2) AS \u0026#34;总耗时(秒)\u0026#34;, ROUND(cpu_time / 1000000, 2) AS \u0026#34;CPU时间(秒)\u0026#34;, ROUND((cpu_time / elapsed_time) * 100, 2) AS \u0026#34;CPU占比(%)\u0026#34;, ROUND(elapsed_time / DECODE(executions, 0, 1, executions) / 1000000, 4) AS \u0026#34;单次执行耗时(秒)\u0026#34;, ROUND(cpu_time / DECODE(executions, 0, 1, executions) / 1000000, 4) AS \u0026#34;单次CPU时间(秒)\u0026#34; FROM v$sqlarea WHERE elapsed_time \u0026gt; 0 ORDER BY cpu_time DESC; V$SQLAREA 中的数据来自共享池，不等同于完整的历史审计记录。实例重启、游标淘汰或共享池刷新后，数据会发生变化；需要长期趋势时应使用 AWR/ASH 等能力。\n查询数据库字符集 SELECT parameter, value FROM v$nls_parameters WHERE parameter = \u0026#39;NLS_CHARACTERSET\u0026#39;; 创建表空间和用户 下面以业务 Schema WEB3 为例，将表数据和索引分别放入两个表空间。\n创建数据与索引表空间 CREATE TABLESPACE WEB3_DATA DATAFILE \u0026#39;/path/to/oradata/WEB3_DATA01.dbf\u0026#39; SIZE 100M AUTOEXTEND ON NEXT 50M MAXSIZE UNLIMITED EXTENT MANAGEMENT LOCAL SEGMENT SPACE MANAGEMENT AUTO; CREATE TABLESPACE WEB3_INDEX DATAFILE \u0026#39;/path/to/oradata/WEB3_INDEX01.dbf\u0026#39; SIZE 100M AUTOEXTEND ON NEXT 50M MAXSIZE UNLIMITED EXTENT MANAGEMENT LOCAL SEGMENT SPACE MANAGEMENT AUTO; 生产环境不应直接照搬 MAXSIZE UNLIMITED，应结合磁盘容量、告警和扩容策略设置合理上限。\n创建用户并分配配额 CREATE USER WEB3 IDENTIFIED BY \u0026#34;change_me\u0026#34; DEFAULT TABLESPACE WEB3_DATA TEMPORARY TABLESPACE TEMP QUOTA UNLIMITED ON WEB3_DATA QUOTA UNLIMITED ON WEB3_INDEX; 示例密码仅为占位符。实际环境应使用密码管理系统生成和保存高强度密码，避免把真实凭据提交到代码仓库。\n授予最小必要权限 GRANT CREATE SESSION TO WEB3; GRANT CREATE TABLE TO WEB3; GRANT CREATE VIEW TO WEB3; 用户可以在自己拥有的表上创建索引，不需要额外授予 CREATE ANY INDEX。只授予业务确实需要的权限，不要为了省事直接授予 DBA。\n验证用户和表空间 CREATE TABLE WEB3.A ( id NUMBER ) TABLESPACE WEB3_DATA; CREATE INDEX WEB3.IDX_A_ID ON WEB3.A(id) TABLESPACE WEB3_INDEX; INSERT INTO WEB3.A(id) VALUES (1); COMMIT; SELECT * FROM WEB3.A; 验证完成后，可以根据需要删除测试表：\nDROP TABLE WEB3.A PURGE; ","permalink":"https://2d992714.xianglin2020-github-io.pages.dev/posts/2025/oracle-practical-queries-and-user-setup/","summary":"整理 Oracle 索引、SQL 性能和字符集查询，并记录创建表空间、用户及基础权限的完整流程。","title":"Oracle 实用手册：诊断查询与用户初始化"},{"content":"虚拟机迁移 需要将 Debian 虚拟机从一台主机迁移到另一台主机上。使用 VirtualBox 自带的【导出虚拟电脑】和【导入虚拟电脑】功能即可完成。\n启动失败 在新主机上导入 Debian 虚拟机启动时，提示启动失败。\n经查询，需要修复Debian的引导记录。\n记一次修复 Debian 12 Grub 莫名奇妙消失的经历\nGrubEFIReinstall\n修复虚拟机引导 下载 Debian Live CD。\n虚拟机启动时选择下载的 Live CD。\n进入 Live CD 后按步骤操作。\nsudo -i # 查看分区，我的 EFI 分区是 /dev/sda1，根分区是 /dev/sda2 fdisk -l # 挂载分区 mount /dev/sda2 /mnt mount /dev/sda1 /mnt/boot/efi for i in /dev /dev/pts /proc /sys /sys/firmware/efi/efivars /run; do mount -B $i /mnt/$i; done # 进入挂载的 Debian 系统 chroot /mnt # 执行修复命令 grub-install /dev/sda update-grub # 退出 chroot，重启 \u0026lt;CTRL\u0026gt;-D reboot ","permalink":"https://2d992714.xianglin2020-github-io.pages.dev/posts/2024/virtualbox-debian12-bootstrap-repair/","summary":"使用 live cd 修复 Debian12 引导。","title":"VirtualBox_Debian12_修复引导"},{"content":"Hadoop https://hadoop.apache.org/\nhttps://hadoop.org.cn/\nHadoop 适合海量数据的分布式存储和分布式计算框架。\nHadoop Common：支持其他 Hadoop 模块的通用实用程序。\nHadoop HDFS：一种分布式文件系统，可提供对应用程序数据的高吞吐量访问。\nHadoop YARN：用于作业调度和集群资源管理的框架。\nHadoop MapReduce：基于 YARN 的系统，用于并行处理大数据集。\nHadoop 伪分布式安装 https://hadoop.apache.org/docs/stable/hadoop-project-dist/hadoop-common/SingleCluster.html#Pseudo-Distributed_Operation\n下载 Hadoop 安装包和 Java 安装包。\nhttps://archive.apache.org/dist/hadoop/common/\nhttps://archive.apache.org/dist/hadoop/common/hadoop-3.2.0/hadoop-3.2.0.tar.gz\nhttps://mirror.tuna.tsinghua.edu.cn/Adoptium/8/jdk/x64/linux/OpenJDK8U-jdk_x64_linux_hotspot_8u432b06.tar.gz\n将路径添加到环境变量中。\n# .bashrc export JAVA_HOME=/root/hadoop/jdk8u432-b06 export HADOOP_HOME=/root/hadoop/hadoop-3.2.0 export PATH=$PATH:$JAVA_HOME/bin:$HADOOP_HOME/bin:$HADOOP_HOME/sbin 配置以 HADOOP_HOME 为相对路径\n按伪分布式运行修改配置文件。\n# etc/hadoop/hadoop-env.sh export JAVA_HOME=/root/hadoop/jdk8u432-b06 export HADOOP_LOG_DIR=/root/hadoop/hadoop_repo/logs/hadoop \u0026lt;!-- etc/hadoop/core-site.xml --\u0026gt; \u0026lt;configuration\u0026gt; \u0026lt;property\u0026gt; \u0026lt;name\u0026gt;fs.defaultFS\u0026lt;/name\u0026gt; \u0026lt;value\u0026gt;hdfs://debian101:9000\u0026lt;/value\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;property\u0026gt; \u0026lt;name\u0026gt;hadoop.tmp.dir\u0026lt;/name\u0026gt; \u0026lt;value\u0026gt;/root/hadoop/hadoop_repo\u0026lt;/value\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;/configuration\u0026gt; \u0026lt;!-- etc/hadoop/hdfs-site.xml --\u0026gt; \u0026lt;configuration\u0026gt; \u0026lt;property\u0026gt; \u0026lt;name\u0026gt;dfs.replication\u0026lt;/name\u0026gt; \u0026lt;value\u0026gt;1\u0026lt;/value\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;/configuration\u0026gt; \u0026lt;!-- etc/hadoop/mapred-site.xml --\u0026gt; \u0026lt;configuration\u0026gt; \u0026lt;property\u0026gt; \u0026lt;name\u0026gt;mapreduce.framework.name\u0026lt;/name\u0026gt; \u0026lt;value\u0026gt;yarn\u0026lt;/value\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;/configuration\u0026gt; \u0026lt;!-- etc/hadoop/yarn-site.xml --\u0026gt; \u0026lt;configuration\u0026gt; \u0026lt;property\u0026gt; \u0026lt;name\u0026gt;yarn.nodemanager.aux-services\u0026lt;/name\u0026gt; \u0026lt;value\u0026gt;mapreduce_shuffle\u0026lt;/value\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;property\u0026gt; \u0026lt;name\u0026gt;yarn.nodemanager.env-whitelist\u0026lt;/name\u0026gt; \u0026lt;value\u0026gt;JAVA_HOME,HADOOP_COMMON_HOME,HADOOP_HDFS_HOME,HADOOP_CONF_DIR,CLASSPATH_PREPEND_DISTCACHE,HADOOP_YARN_HOME,HADOOP_MAPRED_HOME\u0026lt;/value\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;/configuration\u0026gt; 修改启动配置。\n# sbin/start-dfs.sh HDFS_DATANODE_USER=root HDFS_DATANODE_SECURE_USER=hdfs HDFS_NAMENODE_USER=root HDFS_SECONDARYNAMENODE_USER=root # sbin/start-yarn.sh YARN_RESOURCEMANAGER_USER=root HADOOP_SECURE_DN_USER=yarn YARN_NODEMANAGER_USER=root 启动 Hadoop。\n# 格式化文件系统 bin/hdfs namenode -format # 启动 sbin/start-all.sh 验证启动效果。\nhttp://192.168.56.101:9870/dfshealth.html#tab-overview\nhttp://192.168.56.101:8088/cluster\nHDFS HDFS 命令行工具。\n# 帮助命令 bin/hdfs dfs -help # 从本地上传文件 bin/hdfs dfs -put README.txt / # 列出文件 bin/hdfs dfs -ls / # 查看文件内容 bin/hdfs dfs -cat /README.txt # 下载文件到本地 bin/hdfs dfs -get /README.txt README.txt.bak # 创建文件夹 bin/hdfs dfs -mkdir /test # 删除文件 bin/hdfs dfs -rm /README.txt Java 代码操作 HDFS。\n9000连接失败\nhttps://www.cnblogs.com/duanxz/p/5142535.html\n引入依赖\n\u0026lt;dependencies\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.apache.hadoop\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;hadoop-client\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;3.2.0\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;/dependencies\u0026gt; 编写代码\npublic class HdfsOp { public static void main(String[] args) throws IOException, URISyntaxException, InterruptedException { Configuration conf = new Configuration(); FileSystem fileSystem = FileSystem.get(new URI(\u0026#34;hdfs://debian101:9000\u0026#34;), conf, \u0026#34;root\u0026#34;); // 上传本地文件 String localPath1 = \u0026#34;C:\\\\Users\\\\xiang\\\\Documents\\\\IdeaProjects\\\\bigdata\\\\hadoop\\\\pom.xml\u0026#34;; FSDataOutputStream outputStream = fileSystem.create(new Path(\u0026#34;/pom.xml\u0026#34;)); IOUtils.copyBytes(Files.newInputStream(Paths.get(localPath1)), outputStream, conf, true); // 下载文件到本地 String localPath2 = \u0026#34;C:\\\\Users\\\\xiang\\\\Documents\\\\IdeaProjects\\\\bigdata\\\\hadoop\\\\README.txt\u0026#34;; FSDataInputStream fsis = fileSystem.open(new Path(\u0026#34;/README.txt\u0026#34;)); IOUtils.copyBytes(fsis, Files.newOutputStream(Paths.get(localPath2)), conf, true); // 删除文件 boolean ignored = fileSystem.delete(new Path(\u0026#34;/README.txt\u0026#34;), true); } } 基础概念：https://hadoop.apache.org/docs/stable/hadoop-project-dist/hadoop-hdfs/HdfsDesign.html#NameNode_and_DataNodes\nMapReduce https://hadoop.apache.org/docs/stable/hadoop-mapreduce-client/hadoop-mapreduce-client-core/MapReduceTutorial.html#Overview\nHadoop MapReduce 是一个用于编写并行处理海量数据的框架。一个 MapReduce 作业通常将输入数据集拆分成独立数据块，由并行的 map 任务进行处理。框架会对 map 任务的输出做排序，然后将其输入到 reduce 任务中。通常情况下输入和输出都存储在文件系统中，框架负责调度任务、监控任务并重新执行失败的任务。\nMapReduce 作业的输入和输出：\n(input) \u0026lt;k1, v1\u0026gt; -\u0026gt; map -\u0026gt; \u0026lt;k2, v2\u0026gt; -\u0026gt; combine -\u0026gt; \u0026lt;k2, v2\u0026gt; -\u0026gt; reduce -\u0026gt; \u0026lt;k3, v3\u0026gt; (output)\n词频统计 WordCount 程序\npublic class WordCountJob { public static void main(String[] args) throws IOException, InterruptedException, ClassNotFoundException { Configuration conf = new Configuration(); Job job = Job.getInstance(conf); job.setJarByClass(WordCountJob.class); // map 相关 job.setMapperClass(MyMapper.class); job.setMapOutputKeyClass(Text.class); job.setMapOutputValueClass(LongWritable.class); // reduce 相关 job.setReducerClass(MyReducer.class); job.setOutputKeyClass(Text.class); job.setOutputValueClass(LongWritable.class); // 指定文件路径 FileInputFormat.setInputPaths(job, new Path(args[0])); FileOutputFormat.setOutputPath(job, new Path(args[1])); job.waitForCompletion(true); } public static class MyMapper extends Mapper\u0026lt;LongWritable, Text, Text, LongWritable\u0026gt; { final LongWritable one = new LongWritable(1); @Override protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException { String[] words = value.toString().split(\u0026#34; \u0026#34;); for (String word : words) { context.write(new Text(word), one); } } } public static class MyReducer extends Reducer\u0026lt;Text, LongWritable, Text, LongWritable\u0026gt; { @Override protected void reduce(Text key, Iterable\u0026lt;LongWritable\u0026gt; values, Context context) throws IOException, InterruptedException { long sum = 0; for (final LongWritable value : values) { sum += value.get(); } context.write(key, new LongWritable(sum)); } } } 提交任务，查看执行结果\n# 提交任务 bin/hadoop jar hadoop-1.0-SNAPSHOT.jar store.xianglin.mr.WordCountJob /test/hello.txt /out # 查看结果 bin/hdfs dfs -ls /out bin/hdfs dfs -cat /out/part-r-00000 配置查看作业的日志信息\n\u0026lt;!-- etc/hadoop/yarn-site.xml --\u0026gt; \u0026lt;configuration\u0026gt; \u0026lt;property\u0026gt; \u0026lt;name\u0026gt;yarn.resourcemanager.hostname\u0026lt;/name\u0026gt; \u0026lt;value\u0026gt;debian101\u0026lt;/value\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;property\u0026gt; \u0026lt;name\u0026gt;yarn.log-aggregation-enable\u0026lt;/name\u0026gt; \u0026lt;value\u0026gt;true\u0026lt;/value\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;property\u0026gt; \u0026lt;name\u0026gt;yarn.log.server.url\u0026lt;/name\u0026gt; \u0026lt;value\u0026gt;http://debian101:19888/jobhistory/logs/\u0026lt;/value\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;/configuration\u0026gt; # 启动日志服务 bin/mapred --daemon start historyserver 然后可通过 http://192.168.56.101:8088/cluster 查看作业日志或者通过下面命令查看\nbin/yarn logs -applicationId application_1729607544722_0001 MapReduce 性能问题 HDFS 小文件问题：https://developer.aliyun.com/article/697054\n数据倾斜问题：https://www.hangge.com/blog/cache/detail_3567.html\n常见数据压缩格式 https://hadoop.apache.org/docs/stable/hadoop-mapreduce-client/hadoop-mapreduce-client-core/MapReduceTutorial.html#Data_Compression\n运行 Map Reduce 时指定压缩格式\n// 1. 在代码中指定压缩格式 Configuration conf = new Configuration(); conf.setBoolean(MRJobConfig.MAP_OUTPUT_COMPRESS, true); conf.setClass(MRJobConfig.MAP_OUTPUT_COMPRESS_CODEC, DeflateCodec.class, CompressionCodec.class); Job job = Job.getInstance(conf); // 2. 通过命令参数设置压缩格式 Configuration conf = new Configuration(); String[] remainingArgs = new GenericOptionsParser(conf, args).getRemainingArgs(); Job job = Job.getInstance(conf); bin/hadoop jar bigdata-1.0-SNAPSHOT.jar store.xianglin.hadoop.compress.DataCompress -Dmapreduce.output.fileoutputformat.compress=true -Dmapreduce.output.fileoutputformat.compress.codec=org.apache.hadoop.io.compress.DeflateCodec /words.dat /words_out/non_compress YARN https://hadoop.apache.org/docs/stable/hadoop-yarn/hadoop-yarn-site/YARN.html\nYARN 实现 Hadoop 集群的资源共享，YARN 不仅支持 MapReduce，还支持 Spark、Flink 等计算引擎。\nYARN 主要负责集群资源的管理和调度，主要负责内存和 CPU 两种资源。\nResourceManager：主节点负责集群资源的分配和管理；\nNodeManager：从节点负责当前机器资源管理\n调度程序负责在各种队列、应用程序之间分配集群资源。\nCapacity Scheduler：旨在以友好的方式将 Hadoop 应用程序作为共享的多租户集群运行，同时最大限度地提高集群的吞吐量和利用率。\nFair Scheduler：公平调度可使所有应用程序在一段事件内平均获得相同份额的资源（内存）。可以让耗时短的应用程序在合理时间内完成，也不会让耗时长的应用程序处于饥饿状态。\n使用 -Dmapreduce.job.queuename= 指定队列。\nHiDataPlus http://www.hdp.link/\n一个可持续升级的免费Hadoop发行版。\nFlume https://flume.apache.org/\nhttps://blog.csdn.net/weixin_42175752/article/details/140098093\nFlume 是一个高可用、高可靠、分布式的海量日志采集、聚合和传输的系统。\n它有一个简单、灵活的基于流的数据流结构，具备负载均衡机制和故障转移机制，有一个简单可扩展的数据模型。\nSource Source 是 Flume 的数据输入组件，负责从外部数据源收集数据，并将数据转换为 Flume 的内部事件（Event）格式。常见的 Source 类型包括：\nExec Source：从命令执行的输出中读取数据，例如从 tail -F 命令读取日志文件。 Netcat Source：通过网络套接字接收数据。 Spooling Directory Source：从指定目录中读取新文件的内容。 HTTP Source：通过 HTTP POST 请求接受数据。 Kafka Source：从 Kafka 消息队列中采集数据。 Channel Channel 是 Flume 的数据缓冲组件，负责在 Source 和 Sink 之间暂存数据，确保数据传输的可靠性和高效性。常见的 Channel 类型包括：\nMemory Channel：将数据存储在内存中，适用于低延迟和高吞吐量的场景。 File Channel：将数据存储在磁盘文件中，适用于需要高可靠性的场景。 Kafka Channel：使用 Apache Kafka 作为 Channel，适用于需要高可用性和持久化的场景。 Sink Sink 是 Flume 的数据输出组件，负责将 Channel 中的数据传输到目标存储系统。常见的 Sink 类型包括：\nHDFS Sink：将数据写入到 Hadoop 分布式文件系统（HDFS）。 HBase Sink：将数据写入到 HBase 数据库。 ElasticSearch Sink：将数据写入到 Elasticsearch。 Kafka Sink：将数据写入到 Apache Kafka。 启动 Flume https://archive.apache.org/dist/flume/1.9.0/apache-flume-1.9.0-bin.tar.gz\nhttps://flume.apache.org/releases/content/1.9.0/FlumeUserGuide.html#a-simple-example\n# example.conf: A single-node Flume configuration # Name the components on this agent a1.sources = r1 a1.sinks = k1 a1.channels = c1 # Describe/configure the source a1.sources.r1.type = netcat a1.sources.r1.bind = localhost a1.sources.r1.port = 44444 # Describe the sink a1.sinks.k1.type = logger # Use a channel which buffers events in memory a1.channels.c1.type = memory a1.channels.c1.capacity = 1000 a1.channels.c1.transactionCapacity = 100 # Bind the source and sink to the channel a1.sources.r1.channels = c1 a1.sinks.k1.channel = c1 # 启动 agent bin/flume-ng agent --conf conf --conf-file conf/example.properties --name a1 -Dflume.root.logger=INFO,console 使用 telnet 连接 netcat source 创建的 TCP 服务。\ntelnet 127.0.0.1 44444 logger sink 的输出如下：\n2024-10-23 15:50:32,990 (SinkRunner-PollingRunner-DefaultSinkProcessor) [INFO - org.apache.flume.sink.LoggerSink.process(LoggerSink.java:95)] Event: { headers:{} body: 68 65 6C 6C 6F 0D hello. } 采集文件内容上传至 HDFS 创建配置文件\n# example.conf: A single-node Flume configuration # Name the components on this agent a1.sources = r1 a1.sinks = k1 a1.channels = c1 # Describe/configure the source a1.sources.r1.type = spooldir a1.sources.r1.spoolDir = /root/flume/data/studentDir # Use file channel a1.channels.c1.type = file a1.channels.c1.checkpointDir = /root/flume/data/student/checkpoint a1.channels.c1.dataDirs = root/flume/data/student/data # Describe the sink a1.sinks.k1.type = hdfs a1.sinks.k1.hdfs.path = hdfs://debian101:9000/flume/studentDir a1.sinks.k1.hdfs.filePrefix = stu- a1.sinks.k1.hdfs.writeFormat=Text a1.sinks.k1.hdfs.fileType=DataStream a1.sinks.k1.hdfs.hdfs.rollSize=3600 a1.sinks.k1.hdfs.hdfs.rollInterval=134217728 a1.sinks.k1.hdfs.hdfs.rollCount=0 # Bind the source and sink to the channel a1.sources.r1.channels = c1 a1.sinks.k1.channel = c1 启动 agent\nbin/flume-ng agent --conf conf --conf-file conf/dirpool-to-hdfs.conf --name a1 -Dflume.root.logger=INFO,console 查看 HDFS 中的文件\nbin/hdfs dfs -ls /flume/studentDir Hive https://hive.apache.org/\nHive 是建立在 Hadoop 上的数据仓库基础架构，提供了一系列工具，可以进行数据提取、转化和加载。\nHive 是一种分布式容错数据仓库系统，可进行大规模分析，并便于使用 SQL 读写和管理驻留在分布式存储中的 PB 级数据。\n数据仓库 传统的关系型数据库主要应用在基本的事务处理，支持增删改查，例如银行交易。\n数据仓库主要做一些复杂的分析操作，侧重决策支持。相对数据库而言，数据仓库分析的数据规模要大得多，只支持查询操作。\n数据库与数据仓库的本质区别就是 OLTP 和 OLAP 的区别。\nhttps://www.ibm.com/think/topics/olap-vs-oltp\n安装 Hive 下载 hive 安装包解压。\nhttps://archive.apache.org/dist/hive/hive-3.1.2/apache-hive-3.1.2-bin.tar.gz\n修改配置文件\n# cp conf/hive-env.sh.template conf/hive-env.sh export JAVA_HOME=/root/hadoop/jdk8u432-b06 export HADOOP_HOME=/root/hadoop/hadoop-3.2.0 export HIVE_HOME=/root/hive/apache-hive-3.1.2-bin \u0026lt;!-- cp conf/hive-default.xml.template conf/hive-site.xml --\u0026gt; \u0026lt;!-- 配置 MySQL --\u0026gt; \u0026lt;property\u0026gt; \u0026lt;name\u0026gt;javax.jdo.option.ConnectionURL\u0026lt;/name\u0026gt; \u0026lt;value\u0026gt;\u0026lt;/value\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;property\u0026gt; \u0026lt;name\u0026gt;javax.jdo.option.ConnectionDriverName\u0026lt;/name\u0026gt; \u0026lt;value\u0026gt;\u0026lt;/value\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;property\u0026gt; \u0026lt;name\u0026gt;javax.jdo.option.ConnectionUserName\u0026lt;/name\u0026gt; \u0026lt;value\u0026gt;\u0026lt;/value\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;property\u0026gt; \u0026lt;name\u0026gt;javax.jdo.option.ConnectionPassword\u0026lt;/name\u0026gt; \u0026lt;value\u0026gt;\u0026lt;/value\u0026gt; \u0026lt;/property\u0026gt; 配置使用 MySQL 数据库：https://cwiki.apache.org/confluence/pages/viewpage.action?pageId=27362076#AdminManualMetastoreAdministration-RemoteMetastoreDatabase\n上传 MySQL 驱动包 到 lib 目录下。\n修改 Hadoop 配置\n\u0026lt;!-- etc/hadoop/core-site.xml --\u0026gt; \u0026lt;property\u0026gt; \u0026lt;name\u0026gt;hadoop.proxyuser.root.hosts\u0026lt;/name\u0026gt; \u0026lt;value\u0026gt;*\u0026lt;/value\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;property\u0026gt; \u0026lt;name\u0026gt;hadoop.proxyuser.root.groups\u0026lt;/name\u0026gt; \u0026lt;value\u0026gt;*\u0026lt;/value\u0026gt; \u0026lt;/property\u0026gt; 初始化 MySQL 数据库。\n# 先创建数据库 bin/schematool -dbType mysql -initSchema # 默认以 latin1 编码建表，为显示中文注释，需修改表编码 alter table COLUMNS_V2 modify column COMMENT varchar(256) character set utf8mb4; alter table TABLE_PARAMS modify column PARAM_VALUE varchar(4000) character set utf8mb4; alter table PARTITION_PARAMS modify column PARAM_VALUE varchar(4000) character set utf8mb4; alter table PARTITION_KEYS modify column PKEY_COMMENT varchar(4000) character set utf8mb4; 使用 Hive 命令行使用。\n# 启动 hive bin/hive # 或者使用 beeline bin/hiveserver2 bin/beeline -u jdbc:hive2://localhost:10000 -n root # hive 表操作 show tables; create table t1(id int, name string); insert into t1(id, name) values (1, \u0026#39;zs\u0026#39;); select * from t1; drop table t1; Java 代码操作 hive，需要先启动 bin/hiveserver2。\n引入依赖\n\u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.apache.hive\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;hive-jdbc\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;3.1.2\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; 编写代码操作 Hive\npublic class HiveJdbcDemo { public static void main(String[] args) throws SQLException { String hiveUrl = \u0026#34;jdbc:hive2://192.168.56.101:10000\u0026#34;; Connection connection = DriverManager.getConnection(hiveUrl, \u0026#34;root\u0026#34;, \u0026#34;\u0026#34;); Statement statement = connection.createStatement(); statement.execute(\u0026#34;create table t1(id int, name string)\u0026#34;); statement.execute(\u0026#34;insert into t1(id, name) values (1, \u0026#39;zs\u0026#39;)\u0026#34;); try (ResultSet resultSet = statement.executeQuery(\u0026#34;select * from t1\u0026#34;)) { while (resultSet.next()) { int id = resultSet.getInt(\u0026#34;id\u0026#34;); String name = resultSet.getString(\u0026#34;name\u0026#34;); System.out.println(id + \u0026#34; \u0026#34; + name); } } statement.execute(\u0026#34;drop table t1\u0026#34;); } } hive 的配置顺序。\n通过 set name=value 设置，针对当前会话有效。 通过 ~/.hiverc 配置，针对当前用户有效。 通过 hive-site.xml 配置，默认配置全局有效。 Hive 操作 默认数据库存放位置通过 hive.metastore.warehouse.dir 属性指定，默认值 /user/hive/warehouse。\n# 创建数据库，指定存储位置 create database mydb1; create database mydb2 location \u0026#39;/user/hive/mydb2\u0026#39;; -- 创建表 create table t2(id int); -- 查看表字段 desc t2; -- 查看建表语句 show create table t2; -- 修改表名 alter table t2 rename to t2_bak; -- 加载数据 load data local inpath \u0026#39;/root/hive/hivedata/t2.data\u0026#39; into table t2_bak; -- 添加字段 alter table t2_bak add columns (name string); -- 删除表 drop table t2; -- 建表指定列分隔符 create table t3 ( id int comment \u0026#39;ID\u0026#39;, stu_name string comment \u0026#39;name\u0026#39;, stu_birthday date comment \u0026#39;birthday\u0026#39;, online boolean comment \u0026#39;is online\u0026#39; ) row format delimited fields terminated by \u0026#39;\\t\u0026#39; lines terminated by \u0026#39;\\n\u0026#39;; load data local inpath \u0026#39;/root/hive/hivedata/t3.data\u0026#39; into table t3; Hive 数据类型\nhttps://cwiki.apache.org/confluence/display/Hive/LanguageManual+Types\nHive 表类型 https://cwiki.apache.org/confluence/display/Hive/LanguageManual+DDL#LanguageManualDDL-CreateTableCreate/Drop/TruncateTable\n内部表：Hive 中的默认表类型，表数据默认存储在 warehouse 目录中。加载数据时，实际数据会被移动到 warehouse 目录中。删除表时，表中的数据和元数据都会被删除。\n外部表：建表语句中包含 external 关键字。加载数据时，实际数据不会被移动到 warehouse 目录中，只是与外部数据建立一个链接。删除外部表时，只删除元数据，移除链接。\n-- 创建一个外部表 create external table external_table ( key string ) location \u0026#39;/data/external\u0026#39;; load data local inpath \u0026#39;/root/hive/hivedata/external_table.data\u0026#39; into table external_table; select * from external_table; drop table external_table; -- 外部表和内部表的转换 alter table managed_table set tblproperties (\u0026#39;external\u0026#39;=\u0026#39;true\u0026#39;); alter table external_table set tblproperties (\u0026#39;external\u0026#39;=\u0026#39;false\u0026#39;); 分区表：一个表可以有多个分区，分区列中的每个不同值组合都会创建一个单独的数据目录。\n-- 创建分区表 create table partition_1 ( id int, name string ) partitioned by (dt string) row format delimited fields terminated by \u0026#39;\\t\u0026#39;; -- 加载数据到分区中 load data local inpath \u0026#39;/root/hive/hivedata/partition_1.data\u0026#39; into table partition_1 partition (dt=\u0026#39;20200101\u0026#39;); -- 添加/删除分区 alter table partition_1 add partition (dt=\u0026#39;20200102\u0026#39;); alter table partition_1 drop partition (dt=\u0026#39;20200102\u0026#39;); -- 查看分区信息 show partitions partition_1; 桶表：桶表是对数据进行哈希取值，然后放到不同文件中存储。物理存储上，每个桶就是表（或分区）里的一个文件。桶表用于数据抽样或者提高查询效率（join）。\nHive 内置函数 查看函数相关信息\n-- 查看内置函数 show functions ; -- 查看函数描述信息 desc function cos; desc function extended cos; 分组取 TopN\n-- 按学科分组，取每组中分数最高的三行 select * from (select *, row_number() over (partition by sub order by score desc ) as num from student_score) s where s.num \u0026lt;= 3; row_number() over ()：正常排序\nrank() over ()：跳跃排序，有两个第一时接下来是第三\ndense_rank() over ()：连续排序，有两个第一时接下来仍是第二\n行转列\n-- zs,swing -- zs,footbal -- zs,sing -- zs,codeing -- zs,swing select * from student_favors; -- zs,\u0026#34;[\u0026#34;\u0026#34;swing\u0026#34;\u0026#34;,\u0026#34;\u0026#34;footbal\u0026#34;\u0026#34;,\u0026#34;\u0026#34;sing\u0026#34;\u0026#34;,\u0026#34;\u0026#34;codeing\u0026#34;\u0026#34;,\u0026#34;\u0026#34;swing\u0026#34;\u0026#34;]\u0026#34; select name,collect_list(favor) as favor_list from student_favors group by name ; -- zs,\u0026#34;swing,footbal,sing,codeing,swing\u0026#34; select name,concat_ws(\u0026#39;,\u0026#39;,collect_list(favor)) as favors from student_favors group by name ; -- zs,\u0026#34;swing,footbal,sing,codeing\u0026#34; select name,concat_ws(\u0026#39;,\u0026#39;,collect_set(favor)) as favors from student_favors group by name ; concat_ws()：根据指定的分隔符拼接多个字段，最终转化为一个带有分隔符的字符串。\ncollect_list() / collect_set()：返回列表或集合。\n列转行\n-- zs,\u0026#34;swing,footbal,sing\u0026#34; -- ls,\u0026#34;codeing,swing\u0026#34; select * from student_favors_2; -- zs,\u0026#34;[\u0026#34;\u0026#34;swing\u0026#34;\u0026#34;,\u0026#34;\u0026#34;footbal\u0026#34;\u0026#34;,\u0026#34;\u0026#34;sing\u0026#34;\u0026#34;]\u0026#34; -- ls,\u0026#34;[\u0026#34;\u0026#34;codeing\u0026#34;\u0026#34;,\u0026#34;\u0026#34;swing\u0026#34;\u0026#34;]\u0026#34; select name,split(favorlist,\u0026#39;,\u0026#39;) from student_favors_2; -- swing -- footbal -- sing -- codeing -- swing select explode(split(favorlist,\u0026#39;,\u0026#39;)) from student_favors_2; -- zs,swing -- zs,footbal -- zs,sing -- ls,codeing -- ls,swing select name, favor_new from student_favors_2 lateral view explode(split(favorlist,\u0026#39;,\u0026#39;)) table1 as favor_new; split()：根据规则切分字符串，返回字符数组。\nexplode()：接受数组时，将数组中每个元素转成一行。接受map时，将每个键值对转为一行，且key、value各为一列。\n排序关键字\norder by：生成一个 reduce 任务，结果全局有序\nsort by：每个 reduce 任务中数据有序\ndistribute by：根据指定的字段对数据分区输出到 reduce 中，不会排序，一般结合 sort by 一起使用。\ncluster by：效果等同于 distribute by sort by，但不能指定 asc 或 desc。\nHive 性能问题 数据倾斜的原因以及解决办法：https://www.hangge.com/blog/cache/detail_3581.html\nHive 数据存储格式 https://cwiki.apache.org/confluence/display/Hive/FileFormats\nhttps://cwiki.apache.org/confluence/display/Hive/LanguageManual+DDL#LanguageManualDDL-StorageFormats\n在 Hive 表使用 TEXTFILE 存储格式并启用压缩\n-- 创建一个不带压缩的表 create external table stu_textfile ( id int, name string, city string ) row format delimited fields terminated by \u0026#39;,\u0026#39; lines terminated by \u0026#39;\\n\u0026#39; location \u0026#39;hdfs://debian101:9000/stu_textfile\u0026#39;; select * from stu_textfile limit 2; -- 使用 deflate 压缩的表 create external table stu_textfile_deflate_compress ( id int, name string, city string ) row format delimited fields terminated by \u0026#39;,\u0026#39; lines terminated by \u0026#39;\\n\u0026#39; location \u0026#39;hdfs://debian101:9000/stu_textfile_deflate_compress\u0026#39;; -- 当前会话开启压缩选项 set hive.exec.compress.output=true; set mapreduce.output.fileoutputformat.compress=true; set mapreduce.output.fileoutputformat.compress.codec=org.apache.hadoop.io.compress.DeflateCodec; -- 导入数据 set mapreduce.job.reduces=1; insert into stu_textfile_deflate_compress select id, name, city from stu_textfile group by id, name, city; -- 查询数据 select id,count(*) from stu_textfile_deflate_compress group by id ; 在 Hive 表中使用 SEQUENCEFILE 存储格式\n-- 新开 session，避免与上面的 set 命令混淆 -- 使用 sequencefile 存储格式 create external table stu_seqfile_none_compress ( id int, name string, city string ) row format delimited fields terminated by \u0026#39;,\u0026#39; lines terminated by \u0026#39;\\n\u0026#39; stored as sequencefile location \u0026#39;hdfs://debian101:9000/stu_seqfile_none_compress\u0026#39;; -- 导入数据 set mapreduce.job.reduces=1; insert into stu_seqfile_none_compress select id, name, city from stu_textfile group by id, name, city; -- 验证是否支持拆分：支持 select id, count(*) from stu_seqfile_none_compress group by id; -- 使用 deflate 压缩 -- https://cwiki.apache.org/confluence/display/Hive/CompressedStorage create external table stu_seqfile_deflate_compress ( id int, name string, city string ) row format delimited fields terminated by \u0026#39;,\u0026#39; lines terminated by \u0026#39;\\n\u0026#39; stored as sequencefile location \u0026#39;hdfs://debian101:9000/stu_seqfile_deflate_compress\u0026#39;; -- 当前会话开启压缩选项 set hive.exec.compress.output=true; set mapreduce.output.fileoutputformat.compress=true; set mapreduce.output.fileoutputformat.compress.codec=org.apache.hadoop.io.compress.DeflateCodec; set io.seqfile.compression.type=BLOCK; -- 导入数据 set mapreduce.job.reduces=1; insert into stu_seqfile_deflate_compress select id, name, city from stu_textfile group by id, name, city; HBase HBase 数据模型 https://hbase.apache.org/book.html#datamodel\n安装 HBase https://hbase.apache.org/book.html#quickstart\nhttps://archive.apache.org/dist/hbase/2.2.7/hbase-2.2.7-bin.tar.gz\n修改配置（伪分布式）\n# conf/hbase-env.sh export JAVA_HOME=/root/hadoop/jdk8u432-b06 export HADOOP_HOME=/root/hadoop/hadoop-3.2.0 export HBASE_LOG_DIR=/root/hbase/data/log \u0026lt;!-- conf/hbase-site.xml --\u0026gt; \u0026lt;property\u0026gt; \u0026lt;name\u0026gt;hbase.unsafe.stream.capability.enforce\u0026lt;/name\u0026gt; \u0026lt;value\u0026gt;false\u0026lt;/value\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;property\u0026gt; \u0026lt;name\u0026gt;hbase.cluster.distributed\u0026lt;/name\u0026gt; \u0026lt;value\u0026gt;true\u0026lt;/value\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;property\u0026gt; \u0026lt;name\u0026gt;hbase.rootdir\u0026lt;/name\u0026gt; \u0026lt;value\u0026gt;hdfs://debian101:9000/hbase\u0026lt;/value\u0026gt; \u0026lt;/property\u0026gt; 启动 Hbase\nbin/start-hbase.sh HBase 操作 使用 bin/hbase shell 进入 HBase 命令行工具。\n基础命令：\n# 集群状态 status # 版本信息 version # 登录用户 whoami DDL 命令\n# 创建表，指定列簇 create \u0026#39;student\u0026#39;,\u0026#39;inof\u0026#39;,\u0026#39;level\u0026#39; # 查看表信息 list desc \u0026#39;student\u0026#39; # 修改表列簇信息 alter \u0026#39;student\u0026#39;,{NAME=\u0026gt;\u0026#39;level\u0026#39;,VERSIONS=\u0026gt;\u0026#39;3\u0026#39;} alter \u0026#39;student\u0026#39;,\u0026#39;about\u0026#39; alter \u0026#39;student\u0026#39;,{NAME=\u0026gt;\u0026#39;about\u0026#39;,METHOD=\u0026gt;\u0026#39;delete\u0026#39;} # 查看表状态 disable/enable \u0026#39;student\u0026#39; is_disabled/is_enabled \u0026#39;student\u0026#39; # 删除表 truncate \u0026#39;student\u0026#39; drop \u0026#39;student\u0026#39; DML 命令\n# 添加数据 put \u0026#39;student\u0026#39;,\u0026#39;jack\u0026#39;,\u0026#39;info:sex\u0026#39;,\u0026#39;man\u0026#39; put \u0026#39;student\u0026#39;,\u0026#39;jack\u0026#39;,\u0026#39;info:age\u0026#39;,\u0026#39;22\u0026#39; put \u0026#39;student\u0026#39;,\u0026#39;jack\u0026#39;,\u0026#39;level:class\u0026#39;,\u0026#39;A\u0026#39; # 查看数据 scan \u0026#39;student\u0026#39; get \u0026#39;student\u0026#39;,\u0026#39;jack\u0026#39; get \u0026#39;student\u0026#39;,\u0026#39;jack\u0026#39;,\u0026#39;info\u0026#39; get \u0026#39;student\u0026#39;,\u0026#39;jack\u0026#39;,\u0026#39;info:age\u0026#39; # 统计总数 count \u0026#39;student\u0026#39; # 删除数据 delete \u0026#39;student\u0026#39;,\u0026#39;jack\u0026#39;,\u0026#39;info:age\u0026#39; deleteall \u0026#39;student\u0026#39;,\u0026#39;jack\u0026#39; Java API 操作 HBase\nimport org.apache.hadoop.hbase.CellUtil import org.apache.hadoop.hbase.HBaseConfiguration import org.apache.hadoop.hbase.TableName import org.apache.hadoop.hbase.client.* import org.apache.hadoop.hbase.util.Bytes fun main() { // 获取 HBase 连接 val conf = HBaseConfiguration.create() conf.set(\u0026#34;hbase.rootdir\u0026#34;, \u0026#34;hdfs://debian101:9000/hbase\u0026#34;) conf.set(\u0026#34;hbase.zookeeper.quorum\u0026#34;, \u0026#34;debian101:2181\u0026#34;) val conn = ConnectionFactory.createConnection(conf) val tableName = \u0026#34;test\u0026#34; // 表操作: // 创建表 conn.admin.use { admin -\u0026gt; val familyInfo = ColumnFamilyDescriptorBuilder.newBuilder(Bytes.toBytes(\u0026#34;info\u0026#34;)) .setMaxVersions(3) .build() val familyLevel = ColumnFamilyDescriptorBuilder.newBuilder(Bytes.toBytes(\u0026#34;level\u0026#34;)) .setMaxVersions(2) .build() val descriptor = TableDescriptorBuilder.newBuilder(TableName.valueOf(tableName)) .setColumnFamilies(listOf(familyInfo, familyLevel)) .build() admin.createTable(descriptor) } dml(conn, tableName) // 删除表 conn.admin.use { admin -\u0026gt; admin.disableTable(TableName.valueOf(tableName)) admin.deleteTable(TableName.valueOf(tableName)) } conn.close() } private fun dml(conn: Connection, tableName: String) { // 数据操作: // 新增数据 conn.getTable(TableName.valueOf(tableName)).use { table -\u0026gt; val put = Put(Bytes.toBytes(\u0026#34;laowang\u0026#34;)) put.addColumn(Bytes.toBytes(\u0026#34;info\u0026#34;), Bytes.toBytes(\u0026#34;age\u0026#34;), Bytes.toBytes(\u0026#34;18\u0026#34;)) put.addColumn(Bytes.toBytes(\u0026#34;info\u0026#34;), Bytes.toBytes(\u0026#34;sex\u0026#34;), Bytes.toBytes(\u0026#34;man\u0026#34;)) put.addColumn(Bytes.toBytes(\u0026#34;level\u0026#34;), Bytes.toBytes(\u0026#34;class\u0026#34;), Bytes.toBytes(\u0026#34;A\u0026#34;)) table.put(put) } // 查询数据 conn.getTable(TableName.valueOf(tableName)).use { table -\u0026gt; val get = Get(Bytes.toBytes(\u0026#34;laowang\u0026#34;)) get.addColumn(Bytes.toBytes(\u0026#34;info\u0026#34;), Bytes.toBytes(\u0026#34;age\u0026#34;)) get.addColumn(Bytes.toBytes(\u0026#34;info\u0026#34;), Bytes.toBytes(\u0026#34;sex\u0026#34;)) get.addColumn(Bytes.toBytes(\u0026#34;level\u0026#34;), Bytes.toBytes(\u0026#34;class\u0026#34;)) val result = table.get(get) val cells = result.listCells() cells.forEach { cell -\u0026gt; val family = CellUtil.cloneFamily(cell) val column = CellUtil.cloneQualifier(cell) val value = CellUtil.cloneValue(cell) println(\u0026#34;列簇: ${String(family)}, 列: ${String(column)}, 值: ${String(value)}\u0026#34;) } val value = result.getValue(Bytes.toBytes(\u0026#34;level\u0026#34;), Bytes.toBytes(\u0026#34;class\u0026#34;)) println(\u0026#34;level:class: ${String(value)}\u0026#34;) } // 查询多版本数据 conn.getTable(TableName.valueOf(tableName)).use { table -\u0026gt; val get = Get(Bytes.toBytes(\u0026#34;laowang\u0026#34;)) // 指定读取所有版本数据，不设置时只获取最新数据 get.readAllVersions() val result = table.get(get) result.getColumnCells(Bytes.toBytes(\u0026#34;info\u0026#34;), Bytes.toBytes(\u0026#34;age\u0026#34;)).forEach { cell -\u0026gt; val value = CellUtil.cloneValue(cell) println(\u0026#34;值为: ${String(value)}, 时间戳: ${cell.timestamp}\u0026#34;) } } // 删除数据 conn.getTable(TableName.valueOf(tableName)).use { table -\u0026gt; val delete = Delete(Bytes.toBytes(\u0026#34;laowang\u0026#34;)) // 仅删除 age 列 delete.addColumn(Bytes.toBytes(\u0026#34;info\u0026#34;), Bytes.toBytes(\u0026#34;age\u0026#34;)) table.delete(delete) } } Spark Spark 是一个用于大规模数据处理的统一计算引擎。Spark 中一个重要的特性就是基于内存计算。\n安装 Spark 下载 Spark：https://archive.apache.org/dist/spark/spark-2.4.3/spark-2.4.3-bin-hadoop2.7.tgz\nhttps://archive.apache.org/dist/spark/docs/2.4.3/spark-standalone.html\n修改配置\n# cp conf/spark-env.sh.template conf/spark-env.sh export SPARK_MASTER_HOST=debian101 # cp conf/slaves.template conf/slaves debian101 启动 Spark\nsbin/start-all.sh 访问 http://debian101:8080/ 查看控制台。\n提交一个 Spark 任务：https://archive.apache.org/dist/spark/docs/2.4.3/submitting-applications.html\n./bin/spark-submit \\ --class org.apache.spark.examples.SparkPi \\ --master spark://debian101:7077 \\ --executor-memory 2G \\ --total-executor-cores 2 \\ examples/jars/spark-examples_2.11-2.4.3.jar \\ 2 Spark on YARN：https://archive.apache.org/dist/spark/docs/2.4.3/running-on-yarn.html\nSpark 查看日志信息 修改配置\n# cp conf/spark-defaults.conf.template conf/spark-defaults.conf spark.eventLog.enabled=true spark.eventLog.compress=true spark.eventLog.dir=hdfs://debian101:9000/tmp/logs/root/logs spark.history.fs.logDirectory=hdfs://debian101:9000/tmp/logs/root/logs spark.yarn.historyServer.address=http://debian101:18080 # conf/spark-env.sh export HADOOP_CONF_DIR=/root/hadoop/hadoop-3.2.0/etc/hadoop export SPARK_HISTORY_OPTS=\u0026#34;-Dspark.history.ui.port=18080 -Dspark.history.fs.logDirectory=hdfs://debian101:9000/tmp/logs/root/logs\u0026#34; 启动日志服务\nsbin/start-history-server.sh 可以从 YARN 仪表盘进入日志界面\nSpark 组件 https://archive.apache.org/dist/spark/docs/2.4.3/cluster-overview.html\nSpark 编程 https://archive.apache.org/dist/spark/docs/2.4.3/rdd-programming-guide.html\nSpark 提供的高级抽象：\nRDD：弹性分布式数据集，是可并行操作的、在集群节点上分区的元素集合。RDD 可以从任何 Hadoop 支持的文件系统中的文件和代码定义的集合中创建。\n共享变量：Spark 支持两种类型的共享变量。\n广播变量：用于在所有节点的内存中缓存一个值，节点上所有的 task 共享这一份变量；\nBroadcast\u0026lt;int[]\u0026gt; broadcastVar = sc.broadcast(new int[] {1, 2, 3}); broadcastVar.value(); 累加器：在 task 中只能执行 added 方法的变量，在 Driver 中可以读取值，例如计数器和求和。\nLongAccumulator accum = jsc.sc().longAccumulator(); sc.parallelize(Arrays.asList(1, 2, 3, 4)).foreach(x -\u0026gt; accum.add(x)); accum.value(); 创建 RDD 的两种方式：\n并行化集合：通过在现有集合上调用 parallelize 方法创建。\nList\u0026lt;Integer\u0026gt; data = Arrays.asList(1, 2, 3, 4, 5); JavaRDD\u0026lt;Integer\u0026gt; distData = sc.parallelize(data); 外部数据集：Spark 可以从任何 Hadoop 支持的数据源创建 RDD，Spark 支持文本文件、序列文件（SequenceFiles）等。\nString path = \u0026#34;hdfs://debian101:9000/test/hello.txt\u0026#34;; JavaRDD\u0026lt;String\u0026gt; distFile = sc.textFile(path); RDD 的两类操作：\ntransformations，转换：从现有数据集创建新的数据集，Spark 中的 transformations 都是惰性计算的，例如 map。 action，操作：在数据集上运行计算后向驱动程序返回值，例如 reduce。 persist，持久化：通过 persist 或 cache 方法，将 RDD 持久化在内存、磁盘，或者在多个节点上复制。 Shuffle 操作\nShuffle 是 Spark 中数据重新分组的方式，通常会涉及数据在 executor 和机器上复制数据。触发 Shuffle 的操作有两类\n重新分区：repartition coalesce groupByKey reduceByKey 合并操作：join cogroup Spark 程序性能优化 Spark性能优化指南\n高性能序列化类库\nSpark 提供了 Java 序列化和 Kyro 序列化两种机制，Kyro 序列化比 Java 序列化更快，序列化后的数据更小。\n针对程序中多次被操作的 RDD 进行持久化操作，避免对一个 RDD 反复进行计算。\nJVM 垃圾回收调优\n默认情况下，Spark 使用每个 executor 60% 的内存来缓存 RDD，剩余 40% 内存用来存放算子执行期间创建的对象。如果垃圾回收频繁发生，需要通过 spark.stoage.memoryFraction 参数调整比例。\n算子优化\n使用 reduceByKey 替代 groupByKey 使用 mapPartitions 替代 map 使用 foreachPartitions 替代 foreach Spark SQL Spark SQL 是用于处理结构化数据的 Spark 模块。\nDataset 是数据的分布式集合，DataFrame 是按列组织的数据集，概念上等同于关系型数据库中的表。在 Scala API 中，DataFrame 只是 Dataset[Row] 的类型别名，在 Java API 中，需要使用 Dataset\u0026lt;Row\u0026gt; 来表示 DataFrame。\nDataFrame 算子操作：https://archive.apache.org/dist/spark/docs/2.4.3/sql-getting-started.html#untyped-dataset-operations-aka-dataframe-operations\nDataFrame SQL 操作：https://archive.apache.org/dist/spark/docs/2.4.3/sql-getting-started.html#running-sql-queries-programmatically\nRDD 转换为 DataFrame：https://archive.apache.org/dist/spark/docs/2.4.3/sql-getting-started.html#interoperating-with-rdds\n使用反射方式推断元数据 使用编码方式指定元数据，用于无法提前知道列及其类型的情况下构建数据集 load 和 save 操作\nLoad 操作用于加载数据，创建 DataFrame，save 操作用于将 DataFrame 中的数据保存到文件中。format 用于指定数据来源格式，默认为 parquet，支持 jdbc、json、csv、orc、text 等。mode 指定数据或表已存在时的行为，有如下选项（org.apache.spark.sql.SaveMode）\nAppend：追加到已有的数据中 Overwrite：覆盖已有数据 ErrorIfExists：抛出异常，默认值 Ignore：不执行保存操作 Dataset\u0026lt;Row\u0026gt; dataset = sparkSession.read().format(\u0026#34;json\u0026#34;).load(\u0026#34;read_path\u0026#34;); dataset.write().format(\u0026#34;csv\u0026#34;).mode(SaveMode.ErrorIfExists).save(\u0026#34;save_path\u0026#34;); Data Warehouse 基础概念 数据仓库是一个面向主题的、集成的、稳定的且随时间变化的数据集合，用于支持管理人员的决策。\n事实表：保存实际业务数据的表\n维度表：维度指一个对象的属性或特征，比如时间维度、商品维度。\n数据库三范式\n1NF：数据库表的每一列都是不可再分原子数据项 2NF：数据表的每一列都和主键相关，不能只和某一部分相关 数据仓库建模方式：\nER 实体关系模型 维度建模模型 星型模型：不满足三范式，以存储空间为代价降低维度表连接数，性能较高。 雪花模型：满足三范式，有效降低数据冗余，使用时需要连接多个维度表，性能较低。 数据仓库分层 数据仓库分层原因：\n清晰的数据结构 清晰的数据关系 减少重复开发 屏蔽业务影响 数据仓库分层设计：\nODS 原始数据层：存放原始数据 DWD 明细数据层：清洗之后的数据 DWS 数据汇总层：对数据进行轻度汇总（宽表） APP/DM 数据应用层：为统计报表提供数据 每一层的表命名时以分层名称为前缀，比如 ODS 层表命名为 ods_table_name。\n数据清洗原则：\n数据唯一性校验 数据完整性校验，对缺失的数据直接丢弃或用前后数据相同列补齐 数据合法性校验，根据业务需求对错误的数据置为默认值 典型数据仓库架构 数据采集：Flume、Logstash、FileBeat、Sqoop 数据存储：Hive（HDFS）、MySQL 数据计算：Hive、Spark 数据可视化：Zeppelin、Echarts Flink Flink 是一个开源的分布式、高可用、高性能、准确的流处理框架，Flink 支持流处理和批处理。\nFlink 三大核心组件：Data Source \u0026ndash;\u0026gt; Transformations \u0026ndash;\u0026gt; Data Sink\n安装 Flink https://archive.apache.org/dist/flink/flink-1.11.1/\nhttps://nightlies.apache.org/flink/flink-docs-release-1.20/zh/docs/try-flink/local_installation/\n启动本地集群\nbin/start-cluster.sh 控制台：http://192.168.56.101:8081/#/overview\nFlink ON YARN\n# 在 YARN 中创建 Flink 集群 bin/yarn-session.sh -jm 1024m -tm 1024m -d # 提交示例任务 bin/flink run examples/batch/WordCount.jar # 停止 YARN 中的 Flink 集群 bin/yarn application -kill application_1729685357049_0016 # -m yarn-cluster 创建临时的 Flink 集群并提交任务 bin/flink run -m yarn-cluster -yjm 1024m -ytm 1024m examples/batch/WordCount.jar Flink 核心 API https://nightlies.apache.org/flink/flink-docs-release-1.20/zh/docs/concepts/overview/\nDataSource 是程序的输入数据源，基于 socket，基于 Collection，和一批 Connectors 实现读取第三方数据源。\n","permalink":"https://2d992714.xianglin2020-github-io.pages.dev/posts/2024/bigdata-basics/","summary":"\u003ch2 id=\"hadoop\"\u003eHadoop\u003c/h2\u003e\n\u003cp\u003e\u003ca href=\"https://hadoop.apache.org/\" target=\"_blank\" rel=\"noopener\"\u003ehttps://hadoop.apache.org/\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://hadoop.org.cn/\" target=\"_blank\" rel=\"noopener\"\u003ehttps://hadoop.org.cn/\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003eHadoop 适合海量数据的分布式存储和分布式计算框架。\u003c/p\u003e\n\u003cp\u003eHadoop Common：支持其他 Hadoop 模块的通用实用程序。\u003c/p\u003e\n\u003cp\u003eHadoop HDFS：一种分布式文件系统，可提供对应用程序数据的高吞吐量访问。\u003c/p\u003e","title":"大数据基础"},{"content":"部分问题 Java 各版本新特性 Java各版本语言特性\nMySQL 大数据量如何优化处理 https://javaguide.cn/high-performance/read-and-write-separation-and-library-subtable.html\n读写分离和分库分表 数据冷热分离 深度分页优化 SQL 优化 MQ 如何保证消息不丢失 Kafka 消费顺序、消息丢失和重复消费\n阅读 Spring、MyBatis 源码对工作有和帮助 Spring Bean 生命周期，三级缓存 生命周期\nSpring 的循环依赖\nSpringBoot 自动装配 SpringBoot 自动装配原理详解\nStarter原理解析\nSpring 事务，哪些场景事物不生效 Spring 事务详解\n如何设计对外接口 https://blog.csdn.net/huwei2003/article/details/123862132\n安全性 使用 appId、appKey 和 appSecret，使用 token，采用 IP 白名单 调用失败警告、记录接口请求日志 采用 HTTPS、数据加密 接口限流、高可用、高并发 幂等性 数据规范 选择合适的协议，ProtoBuf 接口版本控制，提供接口文档 状态码、数据格式规范 Redis 使用场景 分布式锁（ZooKeeper） SET lockKey uniqueValue EX 3 NX Redisson: Redisson 做了哪些工作？ RedLock 队列 使用 list 数据结构，rpush/lpush 和 lpop/rpop (blpop/brpop) 操作入队和出队 使用 zset 实现延迟队列，Redisson stream 简单限流 地理位置 Geo PostGIS MongoDB 搜索引擎：RedisSearch 布隆过滤器 定时任务的使用 XXL-JOB\nKafka 为什么快 Kafka 性能篇：为何 Kafka 这么快\nKafka吞吐量大的原因\nMySQL 死锁 https://blog.csdn.net/qq_43546721/article/details/140141830\nhttps://blog.csdn.net/AlbenXie/article/details/118613349\n项目中的 JVM 调优 Redis Sentinel | Cluster 多机 Redis\nMySQL binlog | redo log | undo log MySQL三大日志\n内存泄漏 Java 中的内存泄漏是指对象已不再被使用，但是在GCRoots有向图中仍可达，导致GC无法回收。\n常见的内存泄漏场景：\n静态集合类，静态集合类的生命周期与应用程序一致，如果集合中对象不再使用但没有被移除，就会导致内存泄漏； 未关闭的资源，如数据库连接、文件流、网络连接等未关闭，会导致资源无法释放； 使用缓存时未设置清理策略； 使用 ThreadLocal 时没有正确调用 remove 方法。 检测和解决内存泄漏：\n在缓存场景可以使用 WeakReference 或 SoftReference，使得对象在内存不足时能被回收； 使用 try-with-resources 确保资源自动关闭； 通过代码审查发现问题； 使用 VisualVM、Eclipse MAT 等工具分析内存使用情况，找出内存泄漏根源。 面试记录 公司 时间 类型 结果 卓牛科技【互联网】 0903 两轮 职位已关闭 彩讯股份【互联网】 0905 两轮 已读不回 物格智能【计算机软件】 0913 一面 已读不回 科大讯飞【教育BG】 0913 技术面 通过 SHEIN【仓储供应链】 0914 一面 已读不回 科大讯飞【教育BG】 0918 HR 面 未通过 研途科技【游戏】 0920 两轮 通过 英特仿真【计算机软件】 0920 一面 未通过 汇智通信【通信/网络设备】 0924 两轮 通过中止录用 阳普智慧医疗【计算机软件】 0924 记错时间 未参加 雪宝科技【计算机软件】 1010 HR/老板 通过 雪宝科技【计算机软件】 1012 技术面 已读不回 如祺出行【慧博云通/外包】 1012 技术面 通过 先进数通【计算机软件】 1015 技术面 未通过 广州银行【中电金信/外包】 1018 技术面 已读不回 易方达【博彦科技/外包】 1025 一面 未通过 悦信科技【互联网】 1202 三轮 未反馈 启辰科技【计算机软件】 1227 技术面 通过 启辰科技【计算机软件】 0103 老板 未反馈 佳都数据【互联网】 0107 技术面 未反馈 贤能数科【互联网金融】 0115 两轮 未反馈 行稳网络【医疗健康】 0124 技术 通过 国创中心【软通动力/外包】 0210 技术 通过 南方电网【煜邦/外包】 0211 初试 通过 南方电网【煜邦/外包】 0212 复试 通过 行稳网络【医疗健康】 0212 复试 凉凉 广州领克科技【计算机软件】 0213 技术/HR 通过 翰智软件【计算机软件】 0213 一轮 凉凉 国创中心【软通动力/外包】 0214 复试 通过 国创中心【软通动力/外包】 0217 HR 未参加 广州骏伯网络【互联网】 0217 初试 通过 总结：\n这两年基本没进步 有几个好工作也没把握住 一手好牌打得稀烂，工资骤减履历也毁了 切实沉淀两年后再做打算 ","permalink":"https://2d992714.xianglin2020-github-io.pages.dev/posts/2024/2024-interview-record/","summary":"2024社招的几次笔试面试记录。","title":"2024面试记录"},{"content":"https://kubernetes.io/zh-cn/docs/home/\n工作负载 Pod Pod 是可以在 Kubernetes 中创建和管理的、最小的可部署的计算单元。Pod 是一组（一个或多个）容器，这些容器共享存储、网络、以及怎样运行这些容器的规约。Pod 的共享上下文包括一组 Linux 名字空间、控制组（cgroup）和可能一些其他的隔离。\nKubernetes 集群中的 Pod 主要有两种用法：\n运行单个容器的 Pod。每个 Pod 一个容器，可以将 Pod 看作单个容器的包装器，并且 Kubernetes 直接管理 Pod，而不是容器。 运行多个协同工作的容器的 Pod。Pod 可以封装由紧密耦合且需要共享资源的多个并置容器组成的应用，这些位于同一位置的容器构成一个内聚单元。 Pod 的生命周期 Pod 遵循预定义的生命周期，起始于 Pending 阶段，如果至少其中有一个主要容器正常启动，则进入 Running，之后取决于 Pod 中是否有容器以失败状态结束而进入 Succeeded 或者 Failed 阶段。\nPod 在其生命周期中只会被调度一次。将 Pod 分配到特定节点的过程称为绑定，而选择使用哪个节点的过程称为调度。\nPod 的 status 字段是一个 PodStatus 对象，其中包含一个 phase 字段。\n取值 描述 Pending（悬决） Pod 已被 Kubernetes 系统接受，但有一个或多个容器尚未创建亦未运行。此阶段包括等待 Pod 被调度的时间和通过网络下载镜像的时间。 Running（运行中） Pod 已经绑定到某个节点，Pod 中所有容器都已经被创建。至少有一个容器仍在运行，或者正处于启动或重启状态。 Succeeded（成功） Pod 中的所有容器都已经成功终止，并且不会再重启。 Failed（失败） Pod 中的所有容器都已终止，并且至少有一个容器是因为失败终止。也就是说，容器以非 0 状态退出或者被系统终止，且未被设置为自动重启。 Unknown 因为某些原因无法取得 Pod 的状态，通常因为与 Pod 所在主机通信失败。 容器状态 一旦调度器将 Pod 分派给某个节点，kubelet 就通过容器运行时开始为 Pod 创建容器。容器的状态有三种：Waiting（等待）、Running（运行中）和 Terminated（已终止）。\n处于 Waiting 状态的容器仍在运行它完成启动所需要的操作：例如，从某个容器镜像仓库拉取容器镜像，或者向容器应用 Secret 数据。Reason 字段给出了容器处于等待状态的原因。\nRunning 状态表明容器正在执行状态并且没有问题发生。如果配置了 postStart 回调，那么该回调已经执行且已完成。\n处于 Terminated（已完成）状态的容器表明容器运行至正常结束或者因为某些原因失败。如果容器配置了 preStop 回调，则该回调会在容器进入 Terminated 状态之前执行。\n容器重启策略 Pod 的 spec 中包含一个 restartPolicy 字段，其可能取值包括 Always、OnFailure 和 Never。默认值是 Always。\nAlways：只要容器终止就自动重启容器。 OnFailure：只有在容器错误退出（退出状态非 0）时才重启容器。 Never：不会自动重启已终止的容器。 Kubernetes 通过 restartPolicy 管理 Pod 内容器出现的失效，其顺序如下：\n最初的崩溃：Kubernetes 尝试根据 Pod 的 restartPolicy 立即重新启动。 反复的崩溃：在最初的崩溃之后，Kubernetes 对于后续重新启动的容器采用指数级回退延迟机制（10 秒、20 秒、40 秒）重启容器，上限为 300 秒（5分钟）。这一机制可以防止快速、重复的重新启动尝试导致系统过载。 CrashLoopBackOff 状态：表明回退延迟机制正在生效。 回退重置：如果容器成功运行了一定时间（如 10 分钟），Kubernetes 会重置回退延迟机制，将新的崩溃视为第一次崩溃。 容器探针 probe 是由 kubelet 对容器执行的定期诊断。使用探针来检查容器有四种不同的方法：\nexec：在容器内执行指定命令，如果命令退出时返回码为 0 则认为诊断成功。 grpc：使用 gRPC 执行一个远程过程调用。目标应该实现 gRPC 健康检查，如果响应的状态是 SERVING 则认为诊断成功。 httpGet：对容器的 IP 地址上指定端口和路径执行 HTTP GET 请求。如果响应码在 [200, 400)，则认为诊断成功。 tcpSocket：对容器的 IP 地址上指定端口执行 TCP 检查。如果端口打开，则诊断被认为是成功的。 每次探测都将获得以下三种结果之一：\nSuccess：成功 Failure：失败 Unknown：未知 针对运行中的容器，kubelet 可以选择执行以下三种探针：\nlivenessProbe：指示容器是否正在运行。如果存活探针探测失败，则 kubelet 会杀死容器，并根据重启策略处理。如果容器不提供存活探针，则默认状态为 Success。 readinessProbe：指示容器是否准备好为请求提供服务。如果就绪态探测失败，端点控制器将从与 Pod 匹配的所有服务的端点列表中删除该 Pod 的 IP 地址。初始延迟之前的就绪的状态值默认为 Failure。如果容器不提供就绪态探针，则默认状态为 Success。 startupProbe：指示容器中的应用是否已经启动。如果提供了启动探针，则所有其他探针会被禁用，直到此探针成功为止。如果启动探测失败，则 kubelet 会杀死容器，并根据重启策略处理。如果容器不提供启动探针，则默认状态为 Success。 Init 容器 Init 容器是一种特殊容器，在 Pod 内的应用容器启动之前运行，Init 容器可以包括一些应用镜像中不存在的实用工具和安装脚本。常规的 Init 容器不支持 lifecycle、livenessProbe、readinessProbe 或 startupProbe 字段。Init 容器必须在 Pod 准备就绪之前完成运行，如果一个 Pod 指定了多个 Init 容器，这些容器会按顺序逐个运行。每个 Init 容器必须运行成功，下一个才能够运行。当所有的 Init 容器运行完成时，Kubernetes 才会为 Pod 初始化应用容器并像平常一样运行。\n实用 Init 容器的一些场景：\n等待一个 Service 完成创建 注册这个 Pod 到远程服务器 在启动应用容器之前等待一段时间 克隆 Git 仓库到卷中 将配置值放到配置文件中，运行模板工具为主应用容器动态地生成配置文件 边车容器 边车容器是与主应用容器在同一个 Pod 中运行的辅助容器。这些容器通过提供额外的服务或功能（如日志记录、监控、安全性或数据同步）来增强或扩展主应用容器的功能，而无需直接修改主应用代码。边车容器与同一 Pod 中的应用容器并行运行，不过边车容器不执行主应用逻辑，而是为主应用提供支持功能。边车容器具有独立的生命周期，他们可以独立于应用容器启动、停止和重启。边车容器与主应用容器共享相同的网络和存储命名空间。\n工作负载管理 通常不需要直接管理每个 Pod，而是使用负载资源来代替用户管理一组 Pod。这些负载资源通过配置控制器来确保正确类型的、处于运行状态的 Pod 个数是正确的，与用户指定的状态一致。用于管理工作负载的内置 API 包括：\nDeployment（也间接包括 ReplicaSet）是在集群上运行应用的最常见方式。Deployment 适合在集群上管理无状态应用工作负载，其中 Deployment 中的任何 Pod 都是可互换的，可以在需要时进行替换。 StatefulSet 管理了一个或多个运行相同应用代码，但具有不同身份标识的 Pod。StatefulSet 最常见的用途是能够建立其 Pod 与其持久化存储之间的关联。例如，可以运行一个将每个 Pod 关联到 PersistentVolume 的 StatefulSet。如果该 StatefulSet 中的一个 Pod 失败了，Kubernetes 将创建一个新的 Pod，并连接到相同的 PersistentVolume。 DaemonSet 定义了在特定节点上提供本地设施的 Pod。DaemonSet 中每个 Pod 执行类似与经典 Unix / POSIX 服务器上的系统守护进程的角色。 可以使用 Job 或 CronJob 定义一次性任务和定时任务。Job 表示一次性任务，而每个 CronJob 可以根据排期表重复执行。 Deployments Deployment 用于管理运行一个应用的一组 Pod，通常适用于不保持状态的负载。一个 Deployment 为 Pod 和 ReplicaSet 提供声明式的更新能力。你负责描述 Deployment 中的目标状态，而 Deployment 控制器（Controller）以受控速率更改实际状态，使其变为期望状态。你可以定义 Deployment 以创建新的 ReplicaSet，或删除现有 Deployment，并通过新的 Deployment 收养其资源。\nStatefulSet StatefulSet 运行一组 Pod，并为每个 Pod 保留一个稳定的标识。这可用于管理需要持久化存储或稳定、唯一网络标识的应用。使用 StatefulSet 的场景：\n稳定的、唯一的网络标识符。 稳定的、持久的存储。 有序的、优雅的部署和扩缩。 有序的、自动的滚动更新。 StatefulSet Pod 具有唯一的标识，该标识包括顺序标识、稳定的网络标识和稳定的存储。 该标识和 Pod 是绑定的，与该 Pod 调度到哪个节点上无关。\nDaemonSet DaemonSet 定义了提供节点本地设施的 Pod。这些设施可能对于集群的运行至关重要，例如网络辅助工具，或者作为 add-on 的一部分。DaemonSet 确保全部（或者某些）节点上运行一个 Pod 的副本。当有节点加入集群时，也会为它们新增一个 Pod。当有节点从集群移除时，这些 Pod 也会被回收。删除 DaemonSet 将会删除它创建的所有 Pod。DaemonSet 的一些典型用法：\n在每个节点上运行集群守护进程 在每个节点上运行日志收集收集守护进程 在每个节点上运行监控守护进程 Job / CronJob Job 表示一次性任务，运行完后就会停止。Job 会创建一个或者多个 Pod，并将继续重试 Pod 的执行，直到指定数量的 Pod 成功终止。\nCronJob 用于执行排期操作，例如备份、生成报告等。一个 CronJob 对象就像 Unix 系统上的 corntab（cron table）文件中的一行。\n# ┌───────────── 分钟 (0 - 59) # │ ┌───────────── 小时 (0 - 23) # │ │ ┌───────────── 月的某天 (1 - 31) # │ │ │ ┌───────────── 月份 (1 - 12) # │ │ │ │ ┌───────────── 周的某天 (0 - 6)（周日到周六） # │ │ │ │ │ 或者是 sun，mon，tue，web，thu，fri，sat # │ │ │ │ │ # │ │ │ │ │ # * * * * * 服务、负载均衡和联网 Kubernetes 网络模型由几个部分构成：\n集群中的每个 Pod 都会获得自己的、独一无二的集群范围 IP 地址。Pod 有自己的私有网络命名空间，Pod 内的所有容器共享这个命名空间。运行在同一个 Pod 中的不同容器进程彼此之间可以通过 localhost 进行通信。 Pod 网络（也称为集群网络）处理 Pod 之间的通信。它确保所有 Pod 可以与其他 Pod 进行通信。节点上的代理（例如系统守护进程或 kubelet）可以与该节点上的所有 Pod 进行通信。 Service API 允许你为由一个或多个后端 Pod 实现的服务提供一个稳定（长效）的 IP 地址或主机名，其中组成服务的各个 Pod 可以随时变化。 Gateway API（或 Ingress）使得集群外部的客户端能够访问 Service。 NetworkPolicy 是一个内置的 Kubernetes API，允许你控制 Pod 之间的流量或 Pod 与外部世界的流量。 服务（Service） Kubernetes 中 Service 是将运行在一个或一组 Pod 上的应用程序公开为网络服务的方法。Service 所对应的 Pod 集合通常由定义的选择算符来确定。\n如果工作负载使用 HTTP 通信，可以选择使用 Ingress 来控制 Web 流量如何到达该工作负载。Ingress 不是一种 Service，但它可用作集群的入口点。\n服务类型 Service type（类型）值及其行为有：\nClusterIP：默认值，通过集群的内部 IP 公开 Service，选择该值时 Service 只能够在集群内部访问。可以使用 Ingress 或者 Gateway API 向公共互联网公开服务。 NodePort：通过每个节点上的 IP 和静态端口（NodePort）公开 Service。为了让 Service 可以通过节点端口访问，Kubernetes 会为 Service 配置集群 IP 地址，相当于请求了 type: ClusterIP 的 Service。 LoadBalancer：使用云平台的负载均衡器向外部公开 Service。 ExternalName：将服务映射到 externalName 字段的内容（例如，映射到主机名 api.foo.bar.example）。 服务发现 对于在集群内运行的客户端，Kubernetes 支持两种主要的服务发现模式：环境变量和 DNS。\n环境变量：当 Pod 运行在某 Node 上时，kubelet 会在其中为每个活跃的 Service 添加一组环境变量。kubelet 会添加环境变量 {SVCNAME}_SERVICE_HOST 和 {SVCNAME}_SERVICE_PORT 。 DNS：DNS 服务器会监视 Kubernetes API 中新的 Service，并为每个 Service 创建一个 DNS 记录。如果在整个集群中都启用了 DNS，则所有 Pod 都应该能通过 DNS 名称自动解析 Service。 Ingress Ingress 提供从集群外部到集群内服务的 HTTP 和 HTTPS 路由，流量路由由 Ingress 资源所定义的规则来控制。Ingress 可以为 Service 提供外部可访问的 URL、对其流量作负载均衡、终止 SSL/TLS。Ingress 控制器负责完成 Ingress 的工作。可选用 NGINX Ingress 控制器。\n存储 为集群中的 Pods 提供长期和临时存储的方法。\n卷（Volume） 容器中的文件在磁盘上是临时存放的，当容器停止或崩溃时，在容器生命周期内创建或修改的所有文件都将丢失。Kubernetes 支持很多类型的卷，Pod 可以同时使用任意数目的卷类型。临时卷类型的生命周期与 Pod 相同，但持久卷可以比 Pod 的存活期长。对于给定 Pod 中任何类型的卷，在容器重启期间数据都不会丢失。\n卷的核心是一个目录，其中可能存有数据，Pod 中的容器可以访问该目录中的数据。\nKubernetes 支持下列类型的卷：\nconfigMap：提供了向 Pod 注入配置数据的方法。 emptyDir：在 Pod 被指派到某节点时此卷会被创建。Pod 中的容器都可以读写 emptyDir 卷中相同的文件。当 Pod 因为某些原因从节点上删除时，emptyDir 卷中的数据也会被永久删除。emptyDir 的一些用途： 缓存空间，例如基于磁盘的归并排序。 为耗时较长的计算任务提供检查点，以便任务能方便地从崩溃前状态恢复执行。 hostPath：能将主机节点文件系统上的文件或目录挂载到你的 Pod 中。hostPath 的一些用法： 运行一个需要访问节点级系统组件的容器（例如一个将系统日志传输到集中位置的容器，使用只读挂载 /var/log 来访问这些日志）。 让存储在主机系统上的配置文件可以被静态 Pod 以只读方式访问（静态 Pod 无法访问 ConfigMap）。 nfs：将 NFS（网络文件系统）挂载到 Pod 中，nfs 卷的内容在删除 Pod 时会被保存，卷只是被卸载。 persistentVolumeClaim：用来将持久卷（PersistentVolume）挂载到 Pod 中。持久卷申领是用户在不知道特定云环境细节的情况下”申领“持久存储的一种方法。 secret：用来给 Pod 传递敏感信息，例如密码。 ","permalink":"https://2d992714.xianglin2020-github-io.pages.dev/posts/2024/kubernetes-basics/","summary":"kubernetes 基础知识。","title":"Kubernetes基础"},{"content":"参考文档 Install Kubernetes Cluster on Debian 12 (Bookworm) servers\n环境准备 Kubernetes 也称为 K8s，是用于自动部署、扩缩和管理容器化应用程序的开源系统。\n环境 版本 链接 Kubernetes V1.31 https://kubernetes.io/zh-cn/ Docker 27.2.0 https://www.docker.com/ Debian 12.7 https://www.debian.org/ Virtualbox 7.0.20 https://www.virtualbox.org/ 安装 VirtualBox 从VirtualBox官网上下载安装即可，Linux平台安装后会提示安装 linux-head 软件包，按提示操作即可。\nhttps://www.oracle.com/cn/virtualization/technologies/vm/downloads/virtualbox-downloads.html\n安装 Debian 从代理仓库中下载 Debian 镜像，使用 VirtualBox 安装即可，建议最低分配资源如下：\n内存大小：2G 处理器：2 磁盘大小：20G 网络： 网络地址转换 NAT 仅主机（Host-Only）网络 如果安装时选择安装后灰屏无显示，修改启动项，增加 fb=false，请参照：\nhttps://forums.debian.net/viewtopic.php?p=785528\u0026sid=57f61f8a7ff595de0e3e19c86b8140cf#p785528\n镜像地址： https://mirror.tuna.tsinghua.edu.cn/debian-cd/current/amd64/iso-dvd/\n修改Debian软件源配置，使用清华源： https://mirror.tuna.tsinghua.edu.cn/help/debian/\n配置主机名称。\nhostnamectl set-hostname master 配置双网卡，仅主机（Host-Only）网络配置为静态IP：\n# The loopback network interface auto lo iface lo inet loopback # The primary network interface allow-hotplug enp0s3 iface enp0s3 inet dhcp # 上面是初始内容，enp0s3 为 NAT 网络，自动获取IP，用于访问公网 # 下面是添加内容，enp0s8 为 Host-Only 网络，配置静态 IP，用于节点间访问 allow-hotplug enp0s8 iface enp0s8 inet static address 192.168.56.101 netmask 255.255.255.0 https://unix.stackexchange.com/questions/757054/debian-virtual-machine-no-connectivity-with-two-network-adapters\n配置免密登录，在物理机上执行。\nssh-copy-id root@192.168.56.101 配置域名映射，在 vim /etc/hosts 中加入配置。\n192.168.56.101\tmaster 192.168.56.102\tnode1 192.168.56.103\tnode2 安装 Docker 参照官网文档安装即可，可以使用清华源加速安装过程。\nInstall Docker Engine on Debian： https://docs.docker.com/engine/install/debian/\nDocker CE 软件仓库： https://mirror.tuna.tsinghua.edu.cn/help/docker-ce/\n配置代理：Docker代理\n配置容器运行时 https://kubernetes.io/zh-cn/docs/setup/production-environment/container-runtimes/#docker\n使用 cri-dockerd 适配器来将 Docker Engine 与 Kubernetes 集成，从 https://github.com/Mirantis/cri-dockerd/releases 下载 cri-dockerd 安装包安装。\nwget https://github.com/Mirantis/cri-dockerd/releases/download/v0.3.15/cri-dockerd_0.3.15.3-0.debian-bookworm_amd64.deb apt install ./cri-dockerd_0.3.15.3-0.debian-bookworm_amd64.deb 同样可以安装其他容器运行时，比如：\nContainerd CRI-O 关于 Docker Containerd CRI-O Runc等概念的区别： https://smallcoderhujin.github.io/blog/2021/08/06/cri-oci/\n安装 Kubernetes 安装配置和先决条件 https://kubernetes.io/zh-cn/docs/setup/production-environment/container-runtimes/\n启用 IPv4 数据包转发 # 设置所需的 sysctl 参数，参数在重新启动后保持不变 tee /etc/sysctl.d/kubernetes.conf\u0026lt;\u0026lt;EOF net.bridge.bridge-nf-call-ip6tables = 1 net.bridge.bridge-nf-call-iptables = 1 net.ipv4.ip_forward = 1 EOF # 应用 sysctl 参数而不重新启动 sysctl --system 启用所需的内核模块 tee /etc/modules-load.d/k8s.conf \u0026lt;\u0026lt;EOF overlay br_netfilter EOF modprobe overlay modprobe br_netfilter 关闭交换分区 swapoff -a free -m 重启后不生效可参考： https://blog.csdn.net/Elko_265/article/details/126342412\n安装 kubeadm https://kubernetes.io/zh-cn/docs/setup/production-environment/tools/kubeadm/install-kubeadm/\nKubernetes 清华源和其他源最新只到 1.30，故使用官方源。如果下载缓慢，可为 apt 配置代理，一种简单的方式是使用环境变量：\nexport https_proxy=http://192.168.31.113:7897 http_proxy=http://192.168.31.113:7897 all_proxy=socks5://192.168.31.113:7897 清华源同步了最新的镜像，可以使用：\nhttps://mirror.tuna.tsinghua.edu.cn/help/kubernetes/\n复制虚拟机 为完成上述安装步骤的虚拟机生成快照，然后在此基础上复制新的虚拟机，最后登入虚拟机修改主机名称、静态IP地址。 使用 kubeadm 创建集群 https://kubernetes.io/zh-cn/docs/setup/production-environment/tools/kubeadm/create-cluster-kubeadm/\nkubeadm init \\ --apiserver-advertise-address=192.168.56.101 \\ --cri-socket unix:///run/cri-dockerd.sock \\ --service-cidr=10.1.0.0/16 \\ --pod-network-cidr=10.244.0.0/16 各参数解释：\nhttps://kubernetes.io/zh-cn/docs/reference/setup-tools/kubeadm/kubeadm-init/\n上面的命令很可能因为网络原因拉取镜像失败，可以使用 --image-repository registry.aliyuncs.com/google_containers 命令指定镜像仓库，不知道什么原因，我这里执行命令后也是安装失败。\nkubeadm init 命令输出提示中有：You can also perform this action beforehand using \u0026lsquo;kubeadm config images pull\u0026rsquo;，即我们可以手动拉取对应镜像。\n执行如下命令：\nkubeadm config images pull --image-repository=registry.aliyuncs.com/google_containers --cri-socket unix:///run/cri-dockerd.sock 也可以单个手动拉取镜像。执行 kubeadm config images list 输出的镜像列表如下：\nregistry.k8s.io/kube-apiserver:v1.31.0 registry.k8s.io/kube-controller-manager:v1.31.0 registry.k8s.io/kube-scheduler:v1.31.0 registry.k8s.io/kube-proxy:v1.31.0 registry.k8s.io/coredns/coredns:v1.11.1 registry.k8s.io/pause:3.10 registry.k8s.io/etcd:3.5.15-0 从 https://docker.aityp.com/s/registry.k8s.io 上查找对应的镜像，拉取后对镜像打标签即可，以 registry.k8s.io/kube-apiserver:v1.31.0 为例，执行如下命令：\ndocker pull swr.cn-north-4.myhuaweicloud.com/ddn-k8s/registry.k8s.io/kube-apiserver:v1.31.0 docker tag swr.cn-north-4.myhuaweicloud.com/ddn-k8s/registry.k8s.io/kube-apiserver:v1.31.0 registry.k8s.io/kube-apiserver:v1.31.0 再次执行 kubeadm init 有提示：detected that the sandbox image \u0026ldquo;registry.k8s.io/pause:3.8\u0026rdquo; of the container runtime is inconsistent with that used by kubeadm.It is recommended to use \u0026ldquo;registry.k8s.io/pause:3.10\u0026rdquo; as the CRI sandbox image.对应解决方法参见：https://github.com/kubernetes/kubeadm/issues/2851#issuecomment-1535770518 我这里用的 Docker Engine 作为运行时，没有管上述提示。\n安装完成后按提示执行即可：\nmkdir -p $HOME/.kube cp -i /etc/kubernetes/admin.conf $HOME/.kube/config chown $(id -u):$(id -g) $HOME/.kube/config # Deploying Flannel with kubectl kubectl apply -f https://github.com/flannel-io/flannel/releases/latest/download/kube-flannel.yml # 查看集群状态 kubectl get pods -n kube-flannel kubectl get nodes -o wide 重置安装状态 如果上一步初始化失败，则需要重置安装状态才能再次初始化 Kubernetes 集群，执行如下命令：\nhttps://v1-31.docs.kubernetes.io/zh-cn/docs/setup/production-environment/tools/kubeadm/create-cluster-kubeadm/#tear-down\nkubeadm reset --cri-socket unix:///run/cri-dockerd.sock rm -rf /etc/cni/net.d rm -rf $HOME/.kube/config 增加子节点 打开复制虚拟机步骤创建的虚拟机，作为节点，添加到集群中，执行如下命令：\nkubeadm join 192.168.56.101:6443 --token le0z57.zuagjq96utlicpjw \\ --discovery-token-ca-cert-hash sha256:4b534e57748be736a8aacf08c9040ea00d3544a757ca2276eadf56706ed1b42d --cri-socket unix:///run/cri-dockerd.sock 集群成功安装： ","permalink":"https://2d992714.xianglin2020-github-io.pages.dev/posts/2024/kubernetes-installation/","summary":"使用 Debian12 安装 Kubernetes 1.31 版本。","title":"Kubernetes安装"},{"content":"前提 需要一个梯子🪜\n设置代理 用于拉取镜像 Configure the daemon to use a proxy\n编辑 /etc/docker/daemon.json 文件，添加代理配置\n{ \u0026#34;proxies\u0026#34;: { \u0026#34;http-proxy\u0026#34;: \u0026#34;http://192.168.31.113:7897\u0026#34;, \u0026#34;https-proxy\u0026#34;: \u0026#34;http://192.168.31.113:7897\u0026#34;, \u0026#34;no-proxy\u0026#34;: \u0026#34;*.test.example.com,.example.org,127.0.0.0/8\u0026#34; } } 用于构建镜像 Proxy arguments\n在 build 命令中使用 \u0026ndash;build-arg 参数\ndocker image build -t web:latest --build-arg HTTP_PROXY=http://192.168.31.113:7897 --build-arg HTTPS_PROXY=http://192.168.31.113:7897 . 还可以试试直接添加环境变量\nexport https_proxy=http://192.168.31.86:7897 http_proxy=http://192.168.31.86:7897 all_proxy=socks5://192.168.31.86:7897 为容器设置代理 Automatic proxy configuration for containers\n编辑 ~/.docker/config.json 文件，添加代理配置\n{ \u0026#34;proxies\u0026#34;: { \u0026#34;default\u0026#34;: { \u0026#34;httpProxy\u0026#34;: \u0026#34;http://192.168.31.113:7897\u0026#34;, \u0026#34;httpsProxy\u0026#34;: \u0026#34;http://192.168.31.113:7897\u0026#34;, \u0026#34;noProxy\u0026#34;: \u0026#34;localhost,127.0.0.1,.example.com\u0026#34; } } } 效果 全配置了，下载 maven 项目依赖也是 Read timed out！！！\n","permalink":"https://2d992714.xianglin2020-github-io.pages.dev/posts/2024/docker-proxy/","summary":"\u003ch2 id=\"前提\"\u003e前提\u003c/h2\u003e\n\u003cp\u003e\u003cem\u003e需要一个梯子🪜\u003c/em\u003e\u003c/p\u003e\n\u003ch2 id=\"设置代理\"\u003e设置代理\u003c/h2\u003e\n\u003ch3 id=\"用于拉取镜像\"\u003e用于拉取镜像\u003c/h3\u003e\n\u003cblockquote\u003e\n\u003cp\u003e\u003ca href=\"https://docs.docker.com/engine/daemon/proxy/\" target=\"_blank\" rel=\"noopener\"\u003eConfigure the daemon to use a proxy\u003c/a\u003e\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e编辑 \u003ccode\u003e/etc/docker/daemon.json\u003c/code\u003e 文件，添加代理配置\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-json\" data-lang=\"json\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"nt\"\u003e\u0026#34;proxies\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nt\"\u003e\u0026#34;http-proxy\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;http://192.168.31.113:7897\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nt\"\u003e\u0026#34;https-proxy\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;http://192.168.31.113:7897\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nt\"\u003e\u0026#34;no-proxy\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;*.test.example.com,.example.org,127.0.0.0/8\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch3 id=\"用于构建镜像\"\u003e用于构建镜像\u003c/h3\u003e\n\u003cblockquote\u003e\n\u003cp\u003e\u003ca href=\"https://docs.docker.com/build/building/variables/#proxy-arguments\" target=\"_blank\" rel=\"noopener\"\u003eProxy arguments\u003c/a\u003e\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e在 build 命令中使用 \u0026ndash;build-arg 参数\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-shell\" data-lang=\"shell\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003edocker image build -t web:latest  --build-arg \u003cspan class=\"nv\"\u003eHTTP_PROXY\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003ehttp://192.168.31.113:7897 --build-arg \u003cspan class=\"nv\"\u003eHTTPS_PROXY\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003ehttp://192.168.31.113:7897 .\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e还可以试试直接添加环境变量\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nb\"\u003eexport\u003c/span\u003e \u003cspan class=\"nv\"\u003ehttps_proxy\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003ehttp://192.168.31.86:7897 \u003cspan class=\"nv\"\u003ehttp_proxy\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003ehttp://192.168.31.86:7897 \u003cspan class=\"nv\"\u003eall_proxy\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003esocks5://192.168.31.86:7897\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch3 id=\"为容器设置代理\"\u003e为容器设置代理\u003c/h3\u003e\n\u003cblockquote\u003e\n\u003cp\u003e\u003ca href=\"https://docs.docker.com/reference/cli/docker/#automatic-proxy-configuration-for-containers\" target=\"_blank\" rel=\"noopener\"\u003eAutomatic proxy configuration for containers\u003c/a\u003e\u003c/p\u003e","title":"Docker代理"},{"content":"C 语言快速入门 int 类型 #include \u0026lt;stdio.h\u0026gt; #include \u0026lt;limits.h\u0026gt; int main() { short int short_int = 0; int i = 0; long int long_int = 0; long long long_long = 0; printf(\u0026#34;short int: %llu\\n\u0026#34;, sizeof(short int)); printf(\u0026#34;int: %llu\\n\u0026#34;, sizeof(int)); printf(\u0026#34;long int: %llu\\n\u0026#34;, sizeof(long int)); printf(\u0026#34;long long: %llu\\n\u0026#34;, sizeof(long long)); printf(\u0026#34;int max %d, min %d\\n\u0026#34;, INT_MAX, INT_MIN); unsigned int unsigned_int = 123; unsigned long unsigned_long = 111; size_t size_of_ui = sizeof(unsigned int); printf(\u0026#34;unsigned int max %u\\n\u0026#34;, UINT_MAX); return 0; } C 语言定义了 short int、int、long int 和 long long 四种整型，以及对应的无符号整型 unsigned int。使用 sizeof 获取类型的长度，引入 \u0026lt;limits.h\u0026gt; 后使用常量 INT_MAX 等可以获取对于类型的最大值和最小值。在 MinGW 下的执行结果是：\nshort int: 2 int: 4 long int: 4 long long: 8 int max 2147483647, min -2147483648 unsigned int max 4294967295 char 类型 #include \u0026lt;stdio.h\u0026gt; int main() { char a = \u0026#39;a\u0026#39;; // 97 char char_1 = \u0026#39;1\u0026#39;; // 49 char char_0 = \u0026#39;0\u0026#39;; // 48 char i = 0; char char_1_escape_oct = \u0026#39;\\61\u0026#39;; char char_1_escape_hex = \u0026#39;\\x31\u0026#39;; // 字面量 literal // \\n newline // \\b backspace // \\r return // \\t table char newline = \u0026#39;\\n\u0026#39;; printf(\u0026#34;char a: %d\\n\u0026#34;, a); printf(\u0026#34;char 1: %d\\n\u0026#34;, char_1); printf(\u0026#34;char \u0026#39;i\u0026#39;: %d\\n\u0026#34;, i); printf(\u0026#34;char 0: %c\\n\u0026#34;, char_0); printf(\u0026#34;char 1: %c\\n\u0026#34;, char_1_escape_oct); printf(\u0026#34;char 1: %c\\n\u0026#34;, char_1_escape_hex); // C95 宽字符 wchar_t zhong = L\u0026#39;中\u0026#39;; printf(\u0026#34;中: %d\\n\u0026#34;, zhong); return 0; } C 语言使用 char 定义 ASCII 字符，使用 wchar_t 定义宽字符。\n浮点型 #include \u0026lt;stdio.h\u0026gt; int main() { float a_float = 3.14f; // 6 10^-37 printf(\u0026#34;size of float: %llu\\n\u0026#34;, sizeof(float)); double a_double = 3.14; printf(\u0026#34;size of float: %llu\\n\u0026#34;, sizeof(double)); float lat = 39.90815f; printf(\u0026#34;%f\\n\u0026#34;, 39.908156f - lat); return 0; } 变量 #include \u0026lt;stdio.h\u0026gt; int main(){ int value; int value_init = 3; value = 4; value_init = 5; printf(\u0026#34;value: %d\\n\u0026#34;, value); value_init = value; // 获取变量的地址和长度 printf(\u0026#34;size of value: %llu\\n\u0026#34;, sizeof(value)); printf (\u0026#34;address of value: %#x\\n\u0026#34;, \u0026amp;value); return 0; } 常量 #include \u0026lt;stdio.h\u0026gt; // 定义宏 #define COLOR_RED 0xFF0000 #define COLOR_GREEN 0x00FF00 #define COLOR_BLUE 0x0000FF int main() { // 只读变量 readonly variable const int kRed = 0xFF0000; const int kGreen = 0x00FF00; const int kBlue = 0x0000FF; printf(\u0026#34;kRed: %#x\\n\u0026#34;, kRed); int *p_k_red = \u0026amp;kRed; *p_k_red = 0; printf(\u0026#34;kRed: %d\\n\u0026#34;, kRed); // macro printf(\u0026#34;COLOR_RED: %d\\n\u0026#34;, COLOR_RED); // 取消宏定义 #undef COLOR_RED // 字面量 literal return 0; } 运算符 #include \u0026lt;stdio.h\u0026gt; int main() { int first = 0; int second; int third; // 赋值运算符 third = second = first; int left, right; left = 2; right = 3; // 四则运算符 int sum = left + right; int remainder = left % right; // 关系运算符 // true 1 false 0 _Bool f = 3 \u0026gt; 1; printf(\u0026#34;3 \u0026gt; 2: %d\\n\u0026#34;, 3 \u0026gt; 2); printf(\u0026#34;3 \u0026lt; 2: %d\\n\u0026#34;, 3 \u0026lt; 2); // 逻辑运算符 printf(\u0026#34;3 \u0026gt; 2 \u0026amp;\u0026amp; 3 \u0026lt; 2: %d\\n\u0026#34;, 3 \u0026gt; 2 \u0026amp;\u0026amp; 3 \u0026lt; 2); printf(\u0026#34;3 \u0026gt; 2 || 3 \u0026lt; 2: %d\\n\u0026#34;, 3 \u0026gt; 2 || 3 \u0026lt; 2); // 自增、自减 int i = 1; int j = i++; // j = 1, i = 2; int k = ++i; // k = 3, i = 3; printf(\u0026#34;i = %d, j = %d, k = %d\\n\u0026#34;, i, j, k); // 位运算符 \u0026amp; | ^ ~ #define FLAG_VISIBLE 0x1 #define FLAG_TRANSPARENT 0x2 #define FLAG_RESIZABLE 0x4 int window_flags = FLAG_RESIZABLE | FLAG_TRANSPARENT; int resizable = window_flags \u0026amp; FLAG_RESIZABLE; return 0; } 条件分支语句 #include \u0026lt;stdio.h\u0026gt; #include \u0026lt;stdbool.h\u0026gt; int main() { _Bool is_enable = true; bool is_visible = false; // if (\u0026lt;condition\u0026gt;){ // true statement // } else { // false statement // } // // if (\u0026lt;condition\u0026gt;){ // true statement // } else if (\u0026lt;condition\u0026gt;) { // // } else { // false statement // } #define MAGIC_NUMBER 10 int user_input; printf(\u0026#34;Please input a number: \\n\u0026#34;); scanf(\u0026#34;%d\u0026#34;, \u0026amp;user_input); if (user_input \u0026gt; MAGIC_NUMBER) { printf(\u0026#34;You number is bigger!\u0026#34;); } else if (user_input \u0026lt; MAGIC_NUMBER) { printf(\u0026#34;You number is smaller!\u0026#34;); } else { printf(\u0026#34;Yes! You got it!\u0026#34;); } // 三元表达式 bool is_open = is_enable \u0026amp;\u0026amp; is_visible ? true : false; // switch (\u0026lt;condition\u0026gt;) { // case 0 : { // // } // break; // case 1: { // // } // break; // default: { // // } // } return 0; } 引入 \u0026lt;stdbool.h\u0026gt; 后可以使用 bool、true 和 false。\n循环语句 #include \u0026lt;stdio.h\u0026gt; #include \u0026lt;stdbool.h\u0026gt; int main() { int incr = 0; // do-while do { if (incr++ % 2 == 0) { break; } } while (true); // while while (true) { if (incr++ % 2 == 0) { break; } } // for for (int i = 0; i \u0026lt; 10; ++i) { if (incr++ % 2 == 0) { break; } } return 0; } 猜数字游戏 #include \u0026lt;stdio.h\u0026gt; #include \u0026lt;stdlib.h\u0026gt; #include \u0026lt;stdbool.h\u0026gt; #include \u0026lt;time.h\u0026gt; int main() { srand(time(NULL)); int magic_number = rand(); while (true) { puts(\u0026#34;Please input a number:\u0026#34;); int user_input; scanf(\u0026#34;%d\u0026#34;, \u0026amp;user_input); if (user_input \u0026gt; magic_number) { puts(\u0026#34;You number is bigger!\u0026#34;); } else if (user_input \u0026lt; magic_number) { puts(\u0026#34;You number is smaller!\u0026#34;); } else { puts(\u0026#34;Yes! You got it!\u0026#34;); break; } } return 0; } 函数和程序结构 函数基础 #include \u0026lt;stdio.h\u0026gt; // 函数定义在调用之前 // x 形式参数 double f(double x) { return x * x + x + 1; } double g(double x, double y, double z) { return x * x + y * y + z * z; } int main() { puts(\u0026#34;HelloWorld\u0026#34;); // 2.0 实际参数 double result_f = f(2.0); double result_g = g(3.0, 4.0, 5.0); printf(\u0026#34;result of f: %f\\n\u0026#34;, result_f); printf(\u0026#34;result of g: %f\\n\u0026#34;, result_g); return 0; } 函数原型 // 函数原型 void EmptyParamList(void); /** * 1. 函数名 * 2. 函数返回值类型（默认为 int） * 3. 函数参数列表：参数类型和参数的顺序，形参名不重要 */ int Add(int, int); #include \u0026lt;stdio.h\u0026gt; int main() { puts(\u0026#34;\u0026#34;); EmptyParamList(); int result = Add(1,2); return 0; } void EmptyParamList(void) { } 变量类型和作用域 #include \u0026lt;stdio.h\u0026gt; // 文件作用域 int global_var = 0; void LocalStaticVar(void) { // 静态类型变量 // 1. 全局作用域，内存不会因函数和块退出而销毁 static int static_var; auto int non_static_var; printf(\u0026#34;static var: %d\\n\u0026#34;, static_var++); printf(\u0026#34;non static var: %d\\n\u0026#34;, non_static_var++); } // 函数原型作用域 double Add(double a, double b); double Sort(int size, int array[size]); // 函数作用域 int main() { // 自动变量 auto int value = 0; // 块作用域 { auto int a = 0; } LocalStaticVar(); LocalStaticVar(); return 0; } 函数变长参数 #include \u0026lt;stdio.h\u0026gt; #include \u0026lt;stdarg.h\u0026gt; void HandleVarargs(int arg_count, ...) { // 1. 定义 va_list 用于获取变长参数 va_list args; // 2. 开始遍历 va_start(args, arg_count); for (int i = 0; i \u0026lt; arg_count; ++i) { // 3. 取出对于参数：va_list, type int arg = va_arg(args, int); printf(\u0026#34;%d: %d\\n\u0026#34;, i, arg); } // 4. 结束遍历 va_end(args); } int main() { HandleVarargs(4, 1, 2, 3, 4); return 0; } 函数的递归 unsigned int Factorial(unsigned int n) { if (n == 0) { return 1; } else { return n * Factorial(n - 1); } } unsigned int Fibonacci(unsigned int n) { if (n == 1 || n == 0) { return n; } else { return Fibonacci(n - 1) + Fibonacci(n - 2); } } #include \u0026lt;stdio.h\u0026gt; int main(void) { printf(\u0026#34;3! : %d\\n\u0026#34;, Factorial(3)); printf(\u0026#34;5! : %d\\n\u0026#34;, Factorial(5)); printf(\u0026#34;10! : %d\\n\u0026#34;, Factorial(10)); printf(\u0026#34;Fibonacci 3! : %d\\n\u0026#34;, Fibonacci(3)); printf(\u0026#34;Fibonacci 5! : %d\\n\u0026#34;, Fibonacci(5)); printf(\u0026#34;Fibonacci 10! : %d\\n\u0026#34;, Fibonacci(10)); } 汉罗塔移动过程：\n#include \u0026lt;stdio.h\u0026gt; void hanoi(int n, char src, char tmp, char dst) { if (n == 1) { printf(\u0026#34;n = %d : %c -\u0026gt; %c\\n\u0026#34;, n, src, dst); } else { hanoi(n - 1, src, dst, tmp); printf(\u0026#34;n = %d : %c -\u0026gt; %c\\n\u0026#34;, n, src, dst); hanoi(n - 1, tmp, src, dst); } } int main(void) { hanoi(3, \u0026#39;A\u0026#39;, \u0026#39;B\u0026#39;, \u0026#39;C\u0026#39;); return 0; } 预处理和宏 文件包含 C语言的编译过程：\n自定义头文件 定义头文件用于导出函数原型：\n#ifndef CHAPTER5_INCLUDE_FACTORIAL_H_ #define CHAPTER5_INCLUDE_FACTORIAL_H_ unsigned int Factorial(unsigned int n); unsigned int FactorialByIteration(unsigned int n); #endif //CHAPTER5_INCLUDE_FACTORIAL_H_ 定义源文件用于编写函数实现：\n#include \u0026#34;../include/factorial.h\u0026#34; unsigned int Factorial(unsigned int n) { if (n == 0) { return 1; } else { return n * Factorial(n - 1); } } unsigned int FactorialByIteration(unsigned int n) { unsigned int result = 1; unsigned int i = n; for (; i \u0026gt; 0; i--) { result *= i; } return result; } 引用头文件使用函数：\n// 直接查找搜索路径 #include \u0026lt;stdio.h\u0026gt; // 首先查找当前源文件所在的路径 #include \u0026#34;include/factorial.h\u0026#34; int main() { printf(\u0026#34;3! = %d\\n\u0026#34;, Factorial(3)); return 0; } 宏函数 #include \u0026lt;stdio.h\u0026gt; #define MAX(a, b) (a) \u0026gt; (b) ? (a) : (b) // 定义宏时使用 \\ 换行 #define IS_HEX_CHARACTER(ch) \\ ((ch) \u0026gt;=\u0026#39;0\u0026#39; \u0026amp;\u0026amp; (ch) \u0026lt;= \u0026#39;9\u0026#39;) || \\ ((ch) \u0026gt;=\u0026#39;A\u0026#39; \u0026amp;\u0026amp; (ch) \u0026lt;= \u0026#39;F\u0026#39;) || \\ ((ch) \u0026gt;=\u0026#39;a\u0026#39; \u0026amp;\u0026amp; (ch) \u0026lt;= \u0026#39;f\u0026#39;) int Max(int a, int b) { return a \u0026gt; b ? a : b; } int main() { int max1 = MAX(1, 3); int max2 = MAX(1, MAX(3, 4)); printf(\u0026#34;max1 = %d\\n\u0026#34;, max1); printf(\u0026#34;max2 = %d\\n\u0026#34;, max2); int max3 = Max(1, Max(3, 4)); printf(\u0026#34;max3 = %d\\n\u0026#34;, max3); printf(\u0026#34;is A a hex character ? %d\u0026#34;, IS_HEX_CHARACTER(\u0026#39;a\u0026#39;)); return 0; } 条件编译 #define DEBUG void dump(char *message) { #ifdef DEBUG puts(message); #endif } int main() { dump(\u0026#34;main start\u0026#34;); printf(\u0026#34;Hello World!\u0026#34;); dump(\u0026#34;main end\u0026#34;); #if __STDC_VERSION__ \u0026gt;= 201112 puts(\u0026#34;C11!\u0026#34;); #elif __STDC_VERSION__ \u0026gt;= 199901 puts(\u0026#34;C99!\u0026#34;); #else puts(\u0026#34;maybe C90?\u0026#34;); #endif return 0; } 数组 数组基础 #include \u0026lt;stdio.h\u0026gt; #include \u0026lt;io_utils.h\u0026gt; #define ARRAY_SIZE 10 // 初始为元素默认值 int global_array[ARRAY_SIZE]; int main() { // 仅开辟空间，没有初始化元素，值不确定 int array[ARRAY_SIZE]; for (int i = 0; i \u0026lt; ARRAY_SIZE; ++i) { // array[i] = i; PRINT_INT(array[i]); } // 显示指定了数组全部元素 int array_2[ARRAY_SIZE] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}; for (int i = 0; i \u0026lt; ARRAY_SIZE; ++i) { PRINT_INT(array_2[i]); } // 显示指定了前两个元素，后三个元素为默认值 double array_double[5] = {0.1, 2.3}; for (int i = 0; i \u0026lt; 5; ++i) { PRINT_DOUBLE(array_double[i]); } // 使用 [2] = \u0026#39;o\u0026#39; 给指定位置元素赋值，其余元素为默认值 char array_char[5] = {[2] = \u0026#39;o\u0026#39;, \u0026#39;l\u0026#39;, \u0026#39;l\u0026#39;}; for (int i = 0; i \u0026lt; 5; ++i) { PRINT_CHAR(array_char[i]); } return 0; } 字符串数组 #include \u0026lt;stdio.h\u0026gt; #include \u0026lt;io_utils.h\u0026gt; int main() { // C 语言要求字符数组以 \\0 结尾 char string[] = \u0026#34;Hello World\u0026#34;; for (int i = 0; i \u0026lt; 11; ++i) { PRINT_CHAR(string[i]); } PRINTLNF(\u0026#34;%s\u0026#34;, string); char string_zh[] = \u0026#34;你好，中国\u0026#34;; wchar_t ws[] = L\u0026#34;你好，中国\u0026#34;; return 0; } 二维数组 #include \u0026lt;stdio.h\u0026gt; #include \u0026lt;io_utils.h\u0026gt; // VLA // 需要数组返回值，可以通过参数传入 void SumOfArray(int rows, int columns, int array[][columns], int result[]) { for (int i = 0; i \u0026lt; rows; ++i) { for (int j = 0; j \u0026lt; columns; ++j) { result[i] += array[i][j]; } } } int main() { // 列表初始化 int vehicle_limits[5][2] = { {0, 5}, {1, 6}, {2, 7}, {3, 8}, {4, 9} }; // int[] // vehicle_limits[0]; for (int i = 0; i \u0026lt; 5; ++i) { for (int j = 0; j \u0026lt; 2; ++j) { vehicle_limits[i][j] = i + j; } } int scores[5][4] = { {1, 2, 3, 4}, {11, 22, 33, 44}, {111, 222, 333, 444}, {1111, 2222, 3333, 4444}, {11111, 22222, 33333, 44444} }; // 需要将数组元素初始化 int result[5] = {0}; SumOfArray(5, 4, scores, result); PRINT_INT_ARRAY(result, 5); return 0; } 快速排序 #include \u0026lt;stdio.h\u0026gt; #include \u0026lt;io_utils.h\u0026gt; #include \u0026lt;stdlib.h\u0026gt; #include \u0026lt;time.h\u0026gt; // 交换数组中两个元素 void SwapElements(int array[], int first, int second) { int temp = array[first]; array[first] = array[second]; array[second] = temp; } // 数组乱序 void ShuffleArray(int array[], int length) { srand(time(NULL)); for (int i = length - 1; i \u0026gt; 0; --i) { int random_number = rand() % i; SwapElements(array, i, random_number); } } int Partition(int ARRAY[], int L, int R) { int pivot = ARRAY[R]; int partition = L; for (int i = L; i \u0026lt; R; ++i) { if (ARRAY[i] \u0026lt; pivot) { SwapElements(ARRAY, i, partition); partition++; } } SwapElements(ARRAY, R, partition); return partition; } // 快速排序 void QuickSort(int ARRAY[], int L, int R) { if (L \u0026gt;= R) { return; } int partition = Partition(ARRAY, L, R); QuickSort(ARRAY, L, partition - 1); QuickSort(ARRAY, partition + 1, R); } #define PLAYER_COUNT 50 int main() { int players[PLAYER_COUNT]; for (int i = 0; i \u0026lt; PLAYER_COUNT; ++i) { players[i] = i; } PRINT_INT_ARRAY(players, PLAYER_COUNT) ShuffleArray(players, PLAYER_COUNT); PRINT_INT_ARRAY(players, PLAYER_COUNT) // 排序 QuickSort(players, 0, PLAYER_COUNT - 1); PRINT_INT_ARRAY(players, PLAYER_COUNT) return 0; } 指针 指针基础 #include \u0026lt;stdio.h\u0026gt; #include \u0026lt;io_utils.h\u0026gt; int main() { int a; scanf(\u0026#34;%d\u0026#34;, \u0026amp;a); // 指针是一个变量，存储了一个地址 int *p = \u0026amp;a; PRINT_HEX(p); PRINT_HEX(\u0026amp;a); // 64 位处理器占8字节 PRINT_INT(sizeof(int *)); PRINT_INT(*p); PRINT_INT(a); return 0; } 只读指针变量与只读变量指针 int main() { int a; int b; // 指针变量指向 a 的地址，可以改变 a 的值，可以改变 p 指向的地址 int *p = \u0026amp;a; *p = 10; p = \u0026amp;b; // 只读指针变量，可以改变 a 的值，不可以改变 cp 指向的地址 int *const cp = \u0026amp;a; *cp = 10; // cp = \u0026amp;b; ERROR // 指针变量指向只读类型，不可以改变 a 的值，可以改变 cp2 指向的地址 int const *cp2 = \u0026amp;a; // \u0026amp;cp2 = 10; ERROR cp2 = \u0026amp;b; // 只读指针变量指向只读类型，不可以改变 a 的值，不可以改变 cpp 指向的地址 int const *const cpp = \u0026amp;a; // \u0026amp;cpp = 10; ERROR // cpp = \u0026amp;b; ERROR return 0; } 动态内存分配 #include \u0026lt;stdio.h\u0026gt; #include \u0026lt;stdlib.h\u0026gt; #include \u0026lt;io_utils.h\u0026gt; #define PLAYER_COUNT 10 void InitPointer(int **ptr, int length, int defaultValue) { // malloc 返回的内存不会清空 *ptr = malloc(sizeof(int) * length); for (int i = 0; i \u0026lt; length; ++i) { (*ptr)[i] = defaultValue; } } int main() { int *players; // InitPointer(\u0026amp;players, PLAYER_COUNT, 0); // calloc 返回的内存会被清空 players = calloc(PLAYER_COUNT, sizeof(int)); for (int i = 0; i \u0026lt; PLAYER_COUNT; ++i) { PRINT_INT(players[i]); players[i] = i; } PRINT_INT_ARRAY(players, PLAYER_COUNT) int *old = players; // realloc 追加的内存不会清空 players = realloc(players, PLAYER_COUNT * 2 * sizeof(int)); if (players) { PRINT_INT_ARRAY(players, PLAYER_COUNT * 2) } // 释放内存 free(old); free(players); return 0; } 聚合数据类型 结构体 #include \u0026lt;stdio.h\u0026gt; #include \u0026lt;io_utils.h\u0026gt; int main() { typedef struct Company { char *location; } Company; typedef struct Person { char *name; int age; char *id; Company *company; struct { int extra; char *extra_str; } extra_value; struct Person *partner; } Person; Company company = {}; Person person = {.name=\u0026#34;bennyhou\u0026#34;, 0, \u0026#34;111111111\u0026#34;, .company = \u0026amp;company, .extra_value = { .extra = 1, .extra_str = \u0026#34;\u0026#34; }}; PRINT_INT(person.age); person.age = 30; PRINT_HEX(\u0026amp;person); struct Person *person_ptr = \u0026amp;person; PRINT_INT(person_ptr-\u0026gt;age); PRINT_INT(sizeof(Person)); struct { char *name; int gae; char *id; } anonymous_person; Person person_1 = {.name=\u0026#34;andy\u0026#34;, .age=20}; return 0; } 联合体 #include \u0026lt;stdio.h\u0026gt; #include \u0026lt;io_utils.h\u0026gt; typedef union Operand { int int_operand; double double_operand; char *string_operand; } Operand; int main() { PRINT_INT(sizeof(Operand)); // 联合体字段使用时互斥 Operand operand = {.int_operand = 5, .double_operand = 2.0}; PRINT_INT(operand.int_operand); PRINT_DOUBLE(operand.double_operand); return 0; } 枚举 #include \u0026lt;stdio.h\u0026gt; #include \u0026lt;io_utils.h\u0026gt; typedef enum FileFormat { PGN, JPEG = 10, BMP = 5, UNKNOWN } FileFormat; FileFormat GuessFormat(char *file_path) { FILE *file = fopen(file_path, \u0026#34;rb\u0026#34;); FileFormat file_format = UNKNOWN; if (file) { char buffer[8] = {0}; size_t bytes_count = fread(buffer, 1, 8, file); if (bytes_count == 8) { // bmp : 42 4D if (*((short *) buffer) == 0x4D42) { file_format = BMP; } } fclose(file); } return file_format; } int main() { FileFormat file_format = UNKNOWN; PRINT_INT(file_format); return 0; } 字符串 判断字符的类型 #include \u0026lt;io_utils.h\u0026gt; #include \u0026lt;ctype.h\u0026gt; int IsDigit(char c) { return c \u0026gt;= \u0026#39;0\u0026#39; \u0026amp;\u0026amp; c \u0026lt;= \u0026#39;9\u0026#39;; } int main() { PRINT_INT(isdigit(\u0026#39;0\u0026#39;)); PRINT_INT(isspace(\u0026#39; \u0026#39;)); PRINT_INT(isalpha(\u0026#39;a\u0026#39;)); PRINT_INT(isalnum(\u0026#39;f\u0026#39;)); PRINT_INT(isalnum(\u0026#39;1\u0026#39;)); PRINT_INT(ispunct(\u0026#39;,\u0026#39;)); return 0; } ","permalink":"https://2d992714.xianglin2020-github-io.pages.dev/posts/2024/c-basics/","summary":"C 语言基础及 GTK 编程。","title":"C 语言基础"},{"content":"Java 8 https://openjdk.org/projects/jdk8/features\nLambda \u0026amp; Functional Interface @FunctionalInterface public interface Runnable { public abstract void run(); } public class LambdaExamples { @Test public void test(){ new Thread(() -\u0026gt; { try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } }).start(); } } 内部优化通过invokedynamic指令直接调用了匿名函数。\n方法引用操作符:: public class MethodReference { @Test public void test(){ var val = List.of(1,2,3,4,5).stream() .map(Object::toString) .map(Integer::new) .reduce((a,b)-\u0026gt;a+b); System.out.println(val); } } Monad Stream：函数式、流计算 Optional：流计算，减少空值检查 Try Monad：https://github.com/jasongoodwin/better-java-monads 接口的方法(static, default, private) public interface Stream\u0026lt;T\u0026gt; extends BaseStream\u0026lt;T, Stream\u0026lt;T\u0026gt;\u0026gt; { // 每个Stream Monad都有这个方法 default Stream\u0026lt;T\u0026gt; takeWhile(Predicate\u0026lt;? super T\u0026gt; predicate) { ... foo(); // 可以调用 } // 可以通过Stream.empty()构造空的流 public static\u0026lt;T\u0026gt; Stream\u0026lt;T\u0026gt; empty() { ... } // 不是源代码中的程序（Java9） private foo(){} } 本地化日期处理升级 提供了Local和Zoned两种处理日期的方式，在新的API中不再允许mutable的操作（非线程安全）；另外对时区采取更好的处理方法：如果用户的系统不考虑国际化，那么就用Local的日期，如果考虑国际化，可以构造带时区的DateTime。\n@Test public void test_date(){ // 2022年2月22日 var date = LocalDate.of(2022, 2, 22); // 10:55:59 var time = LocalTime.of(10, 55, 59); // 当前时间 var datetime = LocalDateTime.of(date, time); var zoneDT = ZonedDateTime.of(datetime, ZoneId.of(\u0026#34;Asia/Shanghai\u0026#34;)); } 内置了Base64工具 对0~255之间的字符，Base64编码是以4个可见字符去描述3个字符。会增加数据的体积，但是因为所有字符都可以读，用Base64编码描述的字符串，在URL、XML中都不会被转义。\nBase64.Decoder和Base64.Encoder类。\nJava 9 https://openjdk.org/projects/jdk9/\n模块系统 https://openjdk.org/jeps/200\n模块系统应用于平台本身和 JDK。其主要目标是使平台的实现更容易扩展到小型设备，提高安全性和可维护性，改善应用程序性能，并为开发人员提供更好的大型编程工具。\n交互式编程环境(JShell) 新的HTTP 2.0 Clinet 主要针对旧Client，提供Buidler，方便用户设置header和参数。\n另外支持HTTP2.0，HTTP2.0兼容HTTP1.1的能力，主要是从性能角度进行了调优。理解HTTP2.0主要是这几个特性：\n多个HTTP 请求/返回在一个TCP请求上多路复用（客户端要负责实现多路复用） 头部压缩算法（客户端要负责解压） Server Push：服务器可能会给客户端额外的文件（浏览器要负责识别，并缓存这些文件，客户端不一定要实现） HttpClient httpClient = HttpClient.newHttpClient(); HttpRequest httpRequest = HttpRequest .newBuilder() .headers(\u0026#34;x-key\u0026#34;, \u0026#34;123456\u0026#34;, \u0026#34;x-name\u0026#34;, \u0026#34;something\u0026#34;) .uri(new URI(\u0026#34;https://www.baidu.com\u0026#34;)) .GET() .build(); HttpResponse\u0026lt;String\u0026gt; response = httpClient.send( httpRequest, HttpResponse.BodyHandler.asString()); System.out.println(response.statusCode()); System.out.println(response.body()); 改进了Javadoc 可以生成符合HTML5标准的网页。\njavadoc -d foo -html5 MyHelloWorld.java 支持multirelease jar包 项目目录结构可以这样安排，不同的版本的程序在不同的目录下。\n|-java | └──java8 | └── hello | └── xxx.java | └──java9 | └── hello | └── xxx.java 然后可以通过javac 编译成不同版本的class文件：\njavac --release 9 java8/hello/xxx.java javac --release 8 java9/hello/xxx.java 最后用jar 可以产生不同版本的jar 包：\njar -c -f xxx.jar -C java8 . --release 9 -C java9 . 这样xxx.jar 中会同时有java8和java9的程序。\njava -cp test.jar hello // 不同java版本访问不同的类 这个功能的意义是Java升级太快了，多版本同时发布，让程序的提供者可以兼容不同用户的需求。\n新增集合的工厂方法 var set = Set.of(\u0026#34;Apple\u0026#34;, \u0026#34;Google\u0026#34;); var list = List.of(\u0026#34;...\u0026#34;, \u0026#34;abc\u0026#34;, \u0026#34;1\u0026#34;); var map = Map.of(\u0026#34;String\u0026#34;, 5, \u0026#34;Integer\u0026#34;, 6); Stream API增加了方法 takeWhile\n字面意思：take while x != 7 is hold ， 中文意思：在x != 7的时候，拿，否则停下来\nStream.of(2,1,3,7,4,6,8,0).takeWhile(x -\u0026gt; x != 7) .collect(Collectors.toList()); // [2,1,3] dropWhile\n字面意思： drop while x!=7 is hold， 中文意思：在x!=7的时候，丢弃，否则停下来\nStream.of(2,1,3,7,4,6,8,0).dropWhile(x -\u0026gt; x!=7) .collect(Collectors.toList()); //[7, 4, 6, 8, 0] iterate\nStream.iterate( 10, // 初始值 x -\u0026gt; x \u0026lt; 100, // 终结断言 x-\u0026gt;x+2 // 递推函数 ).collect(Collectors.toList()); // [10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50, 52, 54, 56, 58, 60, 62, 64, 66, 68, 70, 72, 74, 76, 78, 80, 82, 84, 86, 88, 90, 92, 94, 96, 98] ofNullable\nStream.ofNullable(null); // Stream.empty() try-with-resources的改进 Java中的一部分资源对象会继承于Closeable接口，这就可以使用try-with-resources 能力。例如：\npublic abstract class InputStream implements Closeable { ... } 如果要使用的话：\n@Test public void test() throws FileNotFoundException, IOException { var fin = new FileInputStream(\u0026#34;somefile\u0026#34;); try(fin) { // AutoClose fin.read(); } } Java 10 https://openjdk.org/projects/jdk/10/\n局部变量类型推断 var list = new LinkedList\u0026lt;Integer\u0026gt;(); // list is LinkedList\u0026lt;Integer\u0026gt; var o = Stream.of(1,2,3); // o is Stream\u0026lt;Integer\u0026gt; JDK代码仓库整理 所有JDK代码不再使用8个仓库存储：root、corba、hotspot、jaxp、jaxws、jdk、langtools、nashorn。\n统一成为1个仓库。主要是解决原子提交的问题：1个功能可能需要更新多个代码仓库。\n应用程序数据共享（Application Data Sharing) 允许多个JVM实例共享共同用到的类，这些类以共享内存的形式存在。这样对于运行了多个JVM的机器，可以节省内存空间以及类的加载速度。\n计划移除JNI头生成工具 JNI（Java Native Interface)是Java程序和Native(C, C++)程序沟通的接口。 一个Java类，如果要Native调用，通常是在Android开发当中，需要生成一个C/C++的头文件。之前可以用javah来生成，现在用javah工具生成的时候，会有一行warning。\n以后JNI的能力会被Panama项目替代，一个专门为非java语言提供接口的库。\n增加实验性的Graal编译器 可以配置参数开启：\n-XX：+ UnlockExperimentalVMOptions -XX：+ UseJVMCICompiler Graal是一款同时支持JIT和AOT的编译器。\nJIT(Just in Time)一边编译一边执行，执行完第一次之后，下一次不需要编译。 AOT(Ahead of Time)，类似C/C++那样，先编译成机器码，再执行。 注意，是机器码，越过了JVM的bytecode。 Java 11 https://openjdk.org/projects/jdk/11/\n引入NestedMembers概念 嵌套类和它的父亲作为一组NestedMembers，可以互相访问元数据。\npublic class Nested { class A{ private String name = \u0026#34;123\u0026#34;; } class B{ public void bar() throws NoSuchFieldException { System.out.println(A.class.getDeclaredField(\u0026#34;name\u0026#34;)); } } @Test public void test() throws NoSuchFieldException { var b = new B(); b.bar(); // name System.out.println(Arrays.toString(A.class.getNestMembers())); // [Nested, A, B] System.out.println(A.class.getNestHost()); // Nested System.out.println(Arrays.toString(B.class.getNestMembers())); // [Nested, A, B] System.out.println(B.class.getNestHost()); // Nested } } 增加无操作GC回收器:Epsilon Epsilon不会进行垃圾回收操作，但是仍然承担着内存分配的工作。\n优点：\n对于开发者明确知道不需要GC的程序有助于减少延迟 对于性能测试、压力测试场景，可以忽略GC带来的延迟 增加字符串处理函数 \u0026#34; \u0026#34;.isBlank(); // true \u0026#34; Foo Bar \u0026#34;.strip(); // \u0026#34;Foo Bar\u0026#34; \u0026#34; Foo Bar \u0026#34;.stripTrailing(); // \u0026#34; Foo Bar\u0026#34; \u0026#34; Foo Bar \u0026#34;.stripLeading(); // \u0026#34;Foo Bar \u0026#34; \u0026#34;Java\u0026#34;.repeat(3); // \u0026#34;JavaJavaJava\u0026#34; \u0026#34;A\\nB\\nC\u0026#34;.lines().count(); // 3 Java 12 Shenandoah 低暂停时间垃圾回收器（实验性） 该算法通过与运行中的 Java 线程同时进行疏散工作来减少 GC 暂停时间。使用 Shenandoah 算法的暂停时间与堆大小无关，这意味着无论堆大小是 200 MB 还是 200 GB，暂停时间都是一致的。\n可以配置参数开启：\n-XX:+UnlockExperimentalVMOptions -XX:+UseShenandoahGC Switch 表达式（预览） 扩展switch语句，使其可以作为语句也可以作为表达式使用，将简化日常编码，并为在switch中使用模式匹配做好准备。\nG1 收集器增强 保证G1收集器不超过预定的暂停时间 使其在空闲时自动将 Java 堆内存归还给操作系统 java 13 https://openjdk.org/projects/jdk/13/\n增强ZGC 增强 ZGC，将未使用的堆内存归还给操作系统。\n重新实现SocketAPI 用一个更简单、更现代、更易于维护和调试的实现来取代 java.net.Socket 和 java.net.ServerSocket API 所使用的底层实现。\n文本块（预览） 为 Java 语言添加文本块。文本块是一种多行字符串字面形式，可避免使用大多数转义序列，以可预测的方式自动格式化字符串，并在需要时让开发人员控制格式。\nJava 14 instanceof 的模式匹配（预览） 通过实例操作符的模式匹配增强 Java 编程语言。模式匹配允许更简洁、更安全地表达程序中的常见逻辑，即从对象中有条件地提取组件。\n优化空指针异常信息 通过精确描述哪个变量为空，提高 JVM 生成的 NullPointerException 的可用性。\n以前的NullPointerException异常信息在如下场景下很难定位到问题，例如\na.b.c.i = 99; a[i][j][k] = 99; a.i = b.j; x().y().i = 99; // message Exception in thread \u0026#34;main\u0026#34; java.lang.NullPointerException at Prog.main(Prog.java:5) 改进后的异常信息如下\nException in thread \u0026#34;main\u0026#34; java.lang.NullPointerException: Cannot assign field \u0026#34;i\u0026#34; because \u0026#34;a\u0026#34; is null at Prog.main(Prog.java:5) Records（预览） 通过 records 增强Java语言，为声明不可变类提供了一种简洁的语法。\nSwitch表达式 原来的switch语句\nswitch (day) { case MONDAY: case FRIDAY: case SUNDAY: System.out.println(6); break; case TUESDAY: System.out.println(7); break; case THURSDAY: case SATURDAY: System.out.println(8); break; case WEDNESDAY: System.out.println(9); break; } 使用 case L -\u0026gt; 形式的switch语句\nswitch (day) { case MONDAY, FRIDAY, SUNDAY -\u0026gt; System.out.println(6); case TUESDAY -\u0026gt; System.out.println(7); case THURSDAY, SATURDAY -\u0026gt; System.out.println(8); case WEDNESDAY -\u0026gt; System.out.println(9); } 扩展switch以支持表达式\nint numLetters = switch (day) { case MONDAY, FRIDAY, SUNDAY -\u0026gt; 6; case TUESDAY -\u0026gt; 7; case THURSDAY, SATURDAY -\u0026gt; 8; case WEDNESDAY -\u0026gt; 9; }; 如果switch表达式在标签后有语句块，可以通过yield返回一个值\nint j = switch (day) { case MONDAY -\u0026gt; 0; case TUESDAY -\u0026gt; 1; default -\u0026gt; { int k = day.toString().length(); int result = f(k); yield result; } }; 垃圾收集器改动 移除CMS收集器 将ZGC移植到MacOS和Windows上 弃用ParallelScavenge + SerialOld的GC组合 Java 15 https://openjdk.org/projects/jdk/15/\n密封类（预览） 封闭的类和接口限制了其他类或接口对它们的扩展或实现。\n重新实现DatagramsocketAPI 将 java.net.DatagramSocket 和 java.net.MulticastSocket API 的底层实现替换为易于维护和调试的更简单、更现代的实现。\n弃用和禁用偏向锁 默认情况下禁用偏向锁，并废弃所有相关的命令行选项。原因如下：\n新应用多使用非同步容器，应用程序不会从偏向锁中获得明显的性能提升。 偏向锁在同步子系统中引入了大量复杂的代码，同时也侵入了其他 HotSpot 组件。这种复杂性阻碍了对代码各部分的理解，也妨碍了对同步子系统进行重大设计变更。 文本块 使用文本块的示例\nString html = \u0026#34;\u0026#34;\u0026#34; \u0026lt;html\u0026gt; \u0026lt;body\u0026gt; \u0026lt;p\u0026gt;Hello, world\u0026lt;/p\u0026gt; \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; \u0026#34;\u0026#34;\u0026#34;; String query = \u0026#34;\u0026#34;\u0026#34; SELECT \u0026#34;EMP_ID\u0026#34;, \u0026#34;LAST_NAME\u0026#34; FROM \u0026#34;EMPLOYEE_TB\u0026#34; WHERE \u0026#34;CITY\u0026#34; = \u0026#39;INDIANAPOLIS\u0026#39; ORDER BY \u0026#34;EMP_ID\u0026#34;, \u0026#34;LAST_NAME\u0026#34;; \u0026#34;\u0026#34;\u0026#34;; 垃圾收集器改动 可扩展的低延迟垃圾回收器 ZGC 成为生产特性 低暂停时间垃圾回收器 Shenandoah 成为生产特性 Java 16 https://openjdk.org/projects/jdk/16/\ninstanceof 的模式匹配 原来使用instanceof判断类型\nif (obj instanceof String) { String s = (String) obj; // grr... ... } 使用instanceof模式匹配后\nif (obj instanceof String s) { // Let pattern matching do the work! ... } Records 定义一个表示Point的类\nclass Point { private final int x; private final int y; Point(int x, int y) { this.x = x; this.y = y; } int x() { return x; } int y() { return y; } public boolean equals(Object o) { if (!(o instanceof Point)) return false; Point other = (Point) o; return other.x == x \u0026amp;\u0026amp; other.y == y; } public int hashCode() { return Objects.hash(x, y); } public String toString() { return String.format(\u0026#34;Point[x=%d, y=%d]\u0026#34;, x, y); } } 使用Record后\nrecord Point(int x, int y) { } Java 17 https://openjdk.org/projects/jdk/17/\nswitch表达式模式匹配（预览） 密封类 通过 sealed 密封类或接口，通过 permits 指定可以扩展此密封类或接口的类。\npublic abstract sealed class Shape permits Circle, Rectangle, Square { ... } abstract sealed class Root { ... final class A extends Root { ... } final class B extends Root { ... } final class C extends Root { ... } } Java 18 https://openjdk.org/projects/jdk/18/\n指定 UTF-8 为默认编码 指定 UTF-8 为标准 Java API 的默认字符集。有了这一更改，依赖于默认字符集的 API 将在所有实现、操作系统、本地化和配置中表现一致。\n简单的Web服务器 提供一个命令行工具，用于启动一个仅提供静态文件的最小网络服务器。\n比如我在 Java21 目录下执行\njwebserver Java API 文档中的代码片段 为 JavaDoc 的 Standard Doclet 引入 @snippet 标签，以简化在 API 文档中包含示例源代码的工作。\n/** * The following code shows how to use {@code Optional.isPresent}: * {@snippet : * if (v.isPresent()) { * System.out.println(\u0026#34;v: \u0026#34; + v.get()); * } * } */ // 引入外部片段 /** * The following code shows how to use {@code Optional.isPresent}: * {@snippet file=\u0026#34;ShowOptional.java\u0026#34; region=\u0026#34;example\u0026#34;} */ Java 19 https://openjdk.org/projects/jdk/19/\n虚拟线程（预览） 将虚拟线程引入 Java 平台。虚拟线程是轻量级线程，可显著减少编写、维护和观察高吞吐量并发应用程序的工作量。\njava 20 https://openjdk.org/projects/jdk/20/\n无正式特性\nJava 21 https://openjdk.org/projects/jdk/21/\n字符串模板（预览） 字符串模板是对 Java 现有字符串字面量和文本块的补充，它将字面文本与表达式和模板处理器结合起来，从而产生结果。\n序列容器 新增 SequencedCollection 、SequencedSet 和 SequencedMap，引入 reversed 、getFirst、getLast等方法。\nRecord Patterns 通过 record patterns 解构 record 的值。\n// As of Java 16 record Point(int x, int y) {} static void printSum(Object obj) { if (obj instanceof Point p) { int x = p.x(); int y = p.y(); System.out.println(x+y); } } // As of Java 21 static void printSum(Object obj) { if (obj instanceof Point(int x, int y)) { System.out.println(x+y); } } switch 的模式匹配 // Prior to Java 21 static String formatter(Object obj) { String formatted = \u0026#34;unknown\u0026#34;; if (obj instanceof Integer i) { formatted = String.format(\u0026#34;int %d\u0026#34;, i); } else if (obj instanceof Long l) { formatted = String.format(\u0026#34;long %d\u0026#34;, l); } else if (obj instanceof Double d) { formatted = String.format(\u0026#34;double %f\u0026#34;, d); } else if (obj instanceof String s) { formatted = String.format(\u0026#34;String %s\u0026#34;, s); } return formatted; } // As of Java 21 static String formatterPatternSwitch(Object obj) { return switch (obj) { case Integer i -\u0026gt; String.format(\u0026#34;int %d\u0026#34;, i); case Long l -\u0026gt; String.format(\u0026#34;long %d\u0026#34;, l); case Double d -\u0026gt; String.format(\u0026#34;double %f\u0026#34;, d); case String s -\u0026gt; String.format(\u0026#34;String %s\u0026#34;, s); default -\u0026gt; obj.toString(); }; } 虚拟线程 https://openjdk.org/jeps/444\npublic static void main(String[] args) throws IOException { var s = System.currentTimeMillis(); try (var executor = Executors.newVirtualThreadPerTaskExecutor()) { IntStream.range(0, 100_000).forEach(i -\u0026gt; { executor.submit(() -\u0026gt; { Thread.sleep(Duration.ofSeconds(1)); return i; }); }); } System.out.printf(\u0026#34;time usage in virtual threads: %d ms\u0026#34;, (System.currentTimeMillis() - s)); } 其它使用虚拟线程的方法\n// 创建一个名为duke的虚拟线程，未启动 var duke = Thread.ofVirtual().name(\u0026#34;duke\u0026#34;).unstarted(() -\u0026gt; { }); // 创建并启动一个虚拟线程 var thread = Thread.startVirtualThread(() -\u0026gt; { }); ","permalink":"https://2d992714.xianglin2020-github-io.pages.dev/posts/2024/java-version-features/","summary":"Java8到Java21语言相关的部分特性。","title":"Java各版本语言特性"},{"content":"GO 语言学习 官网\nGo语言圣经（中文版）\nGo语言高级编程(Advanced Go Programming)\nUber Go 语言编码规范\nGoogle Go 编程规范\nGO 框架 Gin\nGin 是一个用 Go (Golang) 编写的 HTTP Web 框架。 它具有类似 Martini 的 API，但性能比 Martini 快 40 倍。如果你需要极好的性能，使用 Gin 吧。\nGORM\nThe fantastic ORM library for Golang aims to be developer friendly.\nProtocol Buffers\nProtocol Buffers are language-neutral, platform-neutral extensible mechanisms for serializing structured data.\ngRPC\nA high performance, open source universal RPC framework.\nGo gRPC Middleware\nGolang gRPC Middlewares: interceptor chaining, auth, logging, retries and more.\nViper\nGo configuration with fangs\nGo-MySQL-Driver\nA MySQL-Driver for Go\u0026rsquo;s database/sql package\njwt-go\nA go (or \u0026lsquo;golang\u0026rsquo; for search engine friendliness) implementation of JSON Web Tokens.\nPackage validator\nGo Struct and Field validation, including Cross Field, Cross Struct, Map, Slice and Array diving\nbase64Captcha\nA flexible and various captcha package\nGo Redis\nGolang Redis client for Redis Server and Redis Cluster\nredsync\nDistributed mutual exclusion lock using Redis for Go\nzap\nBlazing fast, structured, leveled logging in Go\nConsul\nConsul is a distributed, highly available, and data center aware solution to connect and configure applications across dynamic, distributed infrastructure.\ngrpc-consul-resolver\nEasy-pluggable full-feature GRPC resolver for services registered in Consul\nRocketMQ Client Go\nA product ready RocketMQ Client in pure go, which supports almost the full features of Apache RocketMQ, such as pub and sub messages, ACL, tracing and so on.\nZipkin Library for Go\nZipkin Go is the official Go Tracer / Tracing implementation for Zipkin, supported by the OpenZipkin community.\nApache SkyWalking Go\nThe Golang auto-instrument Agent for Apache SkyWalking, which provides the native tracing/metrics/logging abilities for Golang projects.\nJaeger\nJaeger - a Distributed Tracing System\nKong\nKong or Kong API Gateway is a cloud-native, platform-agnostic, scalable API Gateway distinguished for its high performance and extensibility via plugins.\nCobra\nCobra is a library providing a simple interface to create powerful modern CLI interfaces similar to git \u0026amp; go tools.\n","permalink":"https://2d992714.xianglin2020-github-io.pages.dev/posts/2024/go-library/","summary":"记录 GO 语言 WEB 开发中常用的框架和库。","title":"GO语言开发常用库"},{"content":"Conditional 注解 简单使用 以 @ConditionalOnProperty 注解为例，新建组件 A，当配置中存在 store.xianglin.condition 配置项时，向容器中注册 A。\n@ConditionalOnProperty(\u0026#34;store.xianglin.condition\u0026#34;) @Component public class A { } @RunWith(SpringRunner.class) @SpringBootTest public class TestA { @Resource ApplicationContext context; @Test public void testA() { var bean = context.getBean(A.class); Assert.assertNotNull(bean); } } store.xianglin.condition=a 当 store.xianglin.condition 配置有值时，测试通过。\n原理分析 @ConditionalOnProperty 注解上有 @Conditional(OnPropertyCondition.class)。\n@Retention(RetentionPolicy.RUNTIME) @Target({ ElementType.TYPE, ElementType.METHOD }) @Documented @Conditional(OnPropertyCondition.class) public @interface ConditionalOnProperty { String[] value() default {}; } @Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface Conditional { // 配置的所有Condition 都满足时向容器中注册组件 Class\u0026lt;? extends Condition\u0026gt;[] value(); } Condition 的定义是。\n@FunctionalInterface public interface Condition { // 满足条件时返回 true boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata); } 自定义 Conditional 注解 创建类 MyCondition 实现 Condition 接口，在 matches 方法中判断配置中存在相应配置项值时返回 true。\npublic class MyCondition implements Condition { @Override public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) { var value = (String[]) metadata.getAnnotationAttributes(\u0026#34;store.xianglin.sb2.condi.MyConditionAnnotation\u0026#34;).get(\u0026#34;value\u0026#34;); for (String property : value) { if (!StringUtils.isEmpty(context.getEnvironment().getProperty(property))) { return true; } } return false; } } 创建注解 MyConditionAnnotation 并使用 @Conditional 注解标注。\n@Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Documented @Conditional(MyCondition.class) public @interface MyConditionAnnotation { String[] value() default {}; } ","permalink":"https://2d992714.xianglin2020-github-io.pages.dev/posts/2023/starter-source-code-learning/","summary":"SpringBoot conditional 注解、starter 原理及动手搭建 starter。","title":"Starter原理解析 SpringBoot源码学习"},{"content":"容器启动全局流程解析 启动前准备 在 SpringApplication 构造方法中根据 classpath 下是否存在特定类来决定容器类型（SERVLET、REACTIVE、NONE），并赋值给 webApplicationType 属性。\npublic SpringApplication(ResourceLoader resourceLoader, Class\u0026lt;?\u0026gt;... primarySources) { this.webApplicationType = WebApplicationType.deduceFromClasspath(); } static WebApplicationType deduceFromClasspath() { // 存在 reactive.DispatcherHandler 且不存在 servlet.DispatcherServlet 和 org.glassfish.jersey.servlet.ServletContainer （Oracle WEB Server）时，视为 REACTIVE 环境 if (ClassUtils.isPresent(WEBFLUX_INDICATOR_CLASS, null) \u0026amp;\u0026amp; !ClassUtils.isPresent(WEBMVC_INDICATOR_CLASS, null) \u0026amp;\u0026amp; !ClassUtils.isPresent(JERSEY_INDICATOR_CLASS, null)) { return WebApplicationType.REACTIVE; } for (String className : SERVLET_INDICATOR_CLASSES) { // 如果不存在 javax.servlet.Servlet 或 ConfigurableWebApplicationContext 视为 NONE （非 WEB）环境 if (!ClassUtils.isPresent(className, null)) { return WebApplicationType.NONE; } } // 默认视为 SERVLET 环境 return WebApplicationType.SERVLET; } 在 SpringApplication 的 createApplicationContext 方法中根据 webApplicationType 属性创建对应的上下文。\nprotected ConfigurableApplicationContext createApplicationContext() { Class\u0026lt;?\u0026gt; contextClass = this.applicationContextClass; if (contextClass == null) { try { switch (this.webApplicationType) { case SERVLET: // AnnotationConfigServletWebServerApplicationContext contextClass = Class.forName(DEFAULT_SERVLET_WEB_CONTEXT_CLASS); break; case REACTIVE: // AnnotationConfigReactiveWebServerApplicationContext contextClass = Class.forName(DEFAULT_REACTIVE_WEB_CONTEXT_CLASS); break; default: // AnnotationConfigApplicationContext contextClass = Class.forName(DEFAULT_CONTEXT_CLASS); } } catch (ClassNotFoundException ex) { } return (ConfigurableApplicationContext) BeanUtils.instantiateClass(contextClass); } WEBServer 创建 在 refresh 方法中调用由 ServletWebServerApplicationContext 实现的 onRefresh 子方法，通过 createWebServer 方法完成 WEBServer 的创建。\n// ServletWebServerApplicationContext.java protected void onRefresh() { super.onRefresh(); try { createWebServer(); } catch (Throwable ex) { throw new ApplicationContextException(\u0026#34;Unable to start web server\u0026#34;, ex); } } private void createWebServer() { WebServer webServer = this.webServer; ServletContext servletContext = getServletContext(); if (webServer == null \u0026amp;\u0026amp; servletContext == null) { // 获取用于创建 webServer 的工厂类，一般情况是：TomcatServletWebServerFactory ServletWebServerFactory factory = getWebServerFactory(); // 创建 Tomcat WEBServer this.webServer = factory.getWebServer(getSelfInitializer()); } else if (servletContext != null) { try { getSelfInitializer().onStartup(servletContext); } catch (ServletException ex) { throw new ApplicationContextException(\u0026#34;Cannot initialize servlet context\u0026#34;, ex); } } initPropertySources(); } // TomcatServletWebServerFactory.java public WebServer getWebServer(ServletContextInitializer... initializers) { Tomcat tomcat = new Tomcat(); // ... return getTomcatWebServer(tomcat); } WEBServer 容器启动 在 refresh 方法中调用由 ServletWebServerApplicationContext 实现的 finishRefresh 子方法，通过 startWebServer 方法完成 WEBServer 启动。\nprotected void finishRefresh() { super.finishRefresh(); WebServer webServer = startWebServer(); if (webServer != null) { publishEvent(new ServletWebServerInitializedEvent(webServer, this)); } } private WebServer startWebServer() { WebServer webServer = this.webServer; if (webServer != null) { // 启动 webServer webServer.start(); } return webServer; } WEB 容器工厂类加载解析 WEBServer 创建时通过 getWebServerFactory 方法获取工厂类，默认是 TomcatServletWebServerFactory 对象，那这个对象是何时加载到容器中的呢？\nEnableAutoConfiguration @SpringBootApplication 注解上有 @EnableAutoConfiguration 注解，并通过 @Import 引入了 AutoConfigurationImportSelector，用于处理自动配置。\n@Import(AutoConfigurationImportSelector.class) public @interface EnableAutoConfiguration { String ENABLED_OVERRIDE_PROPERTY = \u0026#34;spring.boot.enableautoconfiguration\u0026#34;; Class\u0026lt;?\u0026gt;[] exclude() default {}; String[] excludeName() default {}; } public class AutoConfigurationImportSelector implements DeferredImportSelector { // 返回需要导入的配置类 } 需要自动配置的类定义在 spring-boot-autoconfigure/META-INF/spring.factories 中，其中就有 ServletWebServerFactoryAutoConfiguration 。\n# Auto Configure org.springframework.boot.autoconfigure.EnableAutoConfiguration=\\ org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration,\\ ServletWebServerFactoryAutoConfiguration ServletWebServerFactoryAutoConfiguration 用于处理 Servlet WEB 容器的配置类，它引入了 EmbeddedTomcat 配置类，并配置了 ServletWebServerFactoryCustomizer 对象用于配置 ServletWebServerFactory。\n@Configuration @AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE) @ConditionalOnClass(ServletRequest.class) @ConditionalOnWebApplication(type = Type.SERVLET) @EnableConfigurationProperties(ServerProperties.class) @Import({ ServletWebServerFactoryAutoConfiguration.BeanPostProcessorsRegistrar.class, ServletWebServerFactoryConfiguration.EmbeddedTomcat.class, ServletWebServerFactoryConfiguration.EmbeddedJetty.class, ServletWebServerFactoryConfiguration.EmbeddedUndertow.class }) public class ServletWebServerFactoryAutoConfiguration { @Bean public ServletWebServerFactoryCustomizer servletWebServerFactoryCustomizer(ServerProperties serverProperties) { return new ServletWebServerFactoryCustomizer(serverProperties); } @Bean @ConditionalOnClass(name = \u0026#34;org.apache.catalina.startup.Tomcat\u0026#34;) public TomcatServletWebServerFactoryCustomizer tomcatServletWebServerFactoryCustomizer( ServerProperties serverProperties) { return new TomcatServletWebServerFactoryCustomizer(serverProperties); } } 因为当前是 Servlet 环境，且引入了 org.springframework.boot:spring-boot-starter-tomcat 依赖，故此处会创建 TomcatServletWebServerFactory 对象注册到 BeanFactory 中，用于后续创建 Tomcat WEBServer。\n@Configuration class ServletWebServerFactoryConfiguration { @Configuration @ConditionalOnClass({ Servlet.class, Tomcat.class, UpgradeProtocol.class }) @ConditionalOnMissingBean(value = ServletWebServerFactory.class, search = SearchStrategy.CURRENT) public static class EmbeddedTomcat { @Bean public TomcatServletWebServerFactory tomcatServletWebServerFactory() { return new TomcatServletWebServerFactory(); } } } WEB 容器配置解析 ServerProperties 以 Tomcat 容器为例，调整如下两个配置，WEBServer 属性配置保存在 ServerProperties 中。\nserver.port=8082 server.tomcat.max-threads=201 @ConfigurationProperties(prefix = \u0026#34;server\u0026#34;, ignoreUnknownFields = true) public class ServerProperties { private Integer port; private final Tomcat tomcat = new Tomcat(); public static class Tomcat { private int maxThreads = 200; } // ... } WebServerFactoryCustomizer 在 ServletWebServerFactoryAutoConfiguration 中向容器中注册了 ServletWebServerFactoryCustomizer 和 TomcatServletWebServerFactoryCustomizer 对象用于调整 WebServer 配置。\n@FunctionalInterface public interface WebServerFactoryCustomizer\u0026lt;T extends WebServerFactory\u0026gt; { void customize(T factory); } WebServerFactoryCustomizer 实现类在 WebServerFactoryCustomizerBeanPostProcessor 的 postProcessBeforeInitialization 方法中调用。\npublic class WebServerFactoryCustomizerBeanPostProcessor implements BeanPostProcessor, BeanFactoryAware { private ListableBeanFactory beanFactory; private List\u0026lt;WebServerFactoryCustomizer\u0026lt;?\u0026gt;\u0026gt; customizers; @Override public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { if (bean instanceof WebServerFactory) { postProcessBeforeInitialization((WebServerFactory) bean); } return bean; } @SuppressWarnings(\u0026#34;unchecked\u0026#34;) private void postProcessBeforeInitialization(WebServerFactory webServerFactory) { // getCustomizers()：获取所有 WebServerFactoryCustomizer 实现类对象 LambdaSafe.callbacks(WebServerFactoryCustomizer.class, getCustomizers(), webServerFactory) .withLogger(WebServerFactoryCustomizerBeanPostProcessor.class) // 调用 customize 方法 .invoke((customizer) -\u0026gt; customizer.customize(webServerFactory)); } } ServletWebServerFactoryCustomizer 以 ServletWebServerFactoryCustomizer 为例，介绍 server.port 属性是如何设置到 WebServer 中的。\nServletWebServerFactoryAutoConfiguration 自动配置类会注册 ServletWebServerFactoryCustomizer Bean 实例，ServerProperties 对象中保存了配置项 server.port 的值。\nWebServer 容器创建时会调用 getWebServerFactory 方法获取已注册的 TomcatServletWebServerFactory Bean 实例，Bean 创建时通过 WebServerFactoryCustomizerBeanPostProcessor 后置处理器调用 WebServerFactoryCustomizer 实现，对 TomcatServletWebServerFactory 做配置。\nServletWebServerFactoryCustomizer 在 customize 方法中将 server.port 属性设置到 ConfigurableServletWebServerFactory 中，其它的 Customizer 也基本是同样的处理方式。\n最终调用 TomcatServletWebServerFactory 的 getWebServer 创建 WebServer 时，将保存在自身的 server.port属性设置到 Connector 中。\n","permalink":"https://2d992714.xianglin2020-github-io.pages.dev/posts/2023/servlet-container-source-code-learning/","summary":"Servlet 容器启动流程解析、WEB 容器工厂类加载解析、WEB 容器个性化配置解析。","title":"Servlet容器启动解析 SpringBoot源码学习"},{"content":"全局流程解析 解析入口 AbstractApplicationContext 的 refresh 方法中调用 invokeBeanFactoryPostProcessors 子方法，最终调用 ConfigurationClassPostProcessor 的 postProcessBeanDefinitionRegistry 方法。\npublic void refresh() throws BeansException, IllegalStateException { synchronized (this.startupShutdownMonitor) { invokeBeanFactoryPostProcessors(beanFactory); } } postProcessBeanDefinitionRegistry 逻辑 public class ConfigurationClassPostProcessor implements BeanDefinitionRegistryPostProcessor, PriorityOrdered, ResourceLoaderAware, BeanClassLoaderAware, EnvironmentAware { public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) { // 获取当前 registry 的唯一 ID：registryId int registryId = System.identityHashCode(registry); // 通过 registryId 判断当前 registry 是否已处理过 if (this.registriesPostProcessed.contains(registryId)) { throw new IllegalStateException( \u0026#34;postProcessBeanDefinitionRegistry already called on this post-processor against \u0026#34; + registry); } if (this.factoriesPostProcessed.contains(registryId)) { throw new IllegalStateException( \u0026#34;postProcessBeanFactory already called on this post-processor against \u0026#34; + registry); } // 将当前 registryId 添加到已处理集合中 this.registriesPostProcessed.add(registryId); // 处理配置类 processConfigBeanDefinitions(registry); } } processConfigBeanDefinitions 逻辑 public void processConfigBeanDefinitions(BeanDefinitionRegistry registry) { List\u0026lt;BeanDefinitionHolder\u0026gt; configCandidates = new ArrayList\u0026lt;\u0026gt;(); String[] candidateNames = registry.getBeanDefinitionNames(); // 遍历 BeanDefinition for (String beanName : candidateNames) { BeanDefinition beanDef = registry.getBeanDefinition(beanName); // 判断是否处理过：通过 configurationClass Bean 属性是否为 full 或者 lite if (ConfigurationClassUtils.isFullConfigurationClass(beanDef) || ConfigurationClassUtils.isLiteConfigurationClass(beanDef)) { } // 检查当前 BeanDefinition 是否为配置类，如果是增加 configurationClass 属性，并添加到 configCandidates 中 else if (ConfigurationClassUtils.checkConfigurationClassCandidate(beanDef, this.metadataReaderFactory)) { configCandidates.add(new BeanDefinitionHolder(beanDef, beanName)); } } // Return immediately if no @Configuration classes were found if (configCandidates.isEmpty()) { return; } // Sort by previously determined @Order value, if applicable configCandidates.sort((bd1, bd2) -\u0026gt; { int i1 = ConfigurationClassUtils.getOrder(bd1.getBeanDefinition()); int i2 = ConfigurationClassUtils.getOrder(bd2.getBeanDefinition()); return Integer.compare(i1, i2); }); // ... // Parse each @Configuration class ConfigurationClassParser parser = new ConfigurationClassParser( this.metadataReaderFactory, this.problemReporter, this.environment, this.resourceLoader, this.componentScanBeanNameGenerator, registry); // 遍历 configCandidates 集合进行解析处理 Set\u0026lt;BeanDefinitionHolder\u0026gt; candidates = new LinkedHashSet\u0026lt;\u0026gt;(configCandidates); Set\u0026lt;ConfigurationClass\u0026gt; alreadyParsed = new HashSet\u0026lt;\u0026gt;(configCandidates.size()); do { parser.parse(candidates); parser.validate(); // ... } while (!candidates.isEmpty()); // 注册 importRegistry // Register the ImportRegistry as a bean in order to support ImportAware @Configuration classes if (sbr != null \u0026amp;\u0026amp; !sbr.containsSingleton(IMPORT_REGISTRY_BEAN_NAME)) { sbr.registerSingleton(IMPORT_REGISTRY_BEAN_NAME, parser.getImportRegistry()); } } 执行逻辑解析 执行入口 通过 ConfigurationClassParser 的 parse 方法解析 candidates 配置类集合，直到没有新的配置类添加到此集合中。\ndo { // 最终调用 doProcessConfigurationClass 方法 parser.parse(candidates); // 验证：配置类不能为 final，@Bean 修饰的方法能被重写（未标记为 static、final 或 private） parser.validate(); // ... // 读取 BeanMethod 注册 BeanDefinition this.reader.loadBeanDefinitions(configClasses); alreadyParsed.addAll(configClasses); candidates.clear(); // 判断是否引入新的 BeanDefinition if (registry.getBeanDefinitionCount() \u0026gt; candidateNames.length) { // newCandidateNames：执行 loadBeanDefinitions 后最新的 BeanDefinition String[] newCandidateNames = registry.getBeanDefinitionNames(); // candidateNames：上一次（第一次）的 BeanDefinition Set\u0026lt;String\u0026gt; oldCandidateNames = new HashSet\u0026lt;\u0026gt;(Arrays.asList(candidateNames)); Set\u0026lt;String\u0026gt; alreadyParsedClasses = new HashSet\u0026lt;\u0026gt;(); for (ConfigurationClass configurationClass : alreadyParsed) { alreadyParsedClasses.add(configurationClass.getMetadata().getClassName()); } for (String candidateName : newCandidateNames) { if (!oldCandidateNames.contains(candidateName)) { BeanDefinition bd = registry.getBeanDefinition(candidateName); if (ConfigurationClassUtils.checkConfigurationClassCandidate(bd, this.metadataReaderFactory) \u0026amp;\u0026amp; !alreadyParsedClasses.contains(bd.getBeanClassName())) { // 如果有新注册的配置类，添加到 candidates 中，继续在下一次循环中解析 candidates.add(new BeanDefinitionHolder(bd, candidateName)); } } } candidateNames = newCandidateNames; } } while (!candidates.isEmpty()); doProcessConfigurationClass 逻辑 ConfigurationClassParser 的 parse 方法最终调用 doProcessConfigurationClass 方法完成配置类解析。\nSourceClass sourceClass = asSourceClass(configClass); do { // 如果 sourceClass 不为 null，代表父类也是需要处理的配置类 sourceClass = doProcessConfigurationClass(configClass, sourceClass); } while (sourceClass != null); protected final SourceClass doProcessConfigurationClass(ConfigurationClass configClass, SourceClass sourceClass) throws IOException { if (configClass.getMetadata().isAnnotated(Component.class.getName())) { // 递归处理内部类 processMemberClasses(configClass, sourceClass); } // 处理 @PropertySource 属性源 for (AnnotationAttributes propertySource : AnnotationConfigUtils.attributesForRepeatable( sourceClass.getMetadata(), PropertySources.class, org.springframework.context.annotation.PropertySource.class)) { } // 处理 @ComponentScan 注解：将 ComponentScan 指定的类添加到容器中，如果有配置类调用 parse 方法立即处理 Set\u0026lt;AnnotationAttributes\u0026gt; componentScans = AnnotationConfigUtils.attributesForRepeatable( sourceClass.getMetadata(), ComponentScans.class, ComponentScan.class); // 处理 @Import 注解 processImports(configClass, sourceClass, getImports(sourceClass), true); // 处理 @ImportResource 注解，解析 XML 配置 AnnotationAttributes importResource = AnnotationConfigUtils.attributesFor(sourceClass.getMetadata(), ImportResource.class); // 处理 @Bean 注解的方法 Set\u0026lt;MethodMetadata\u0026gt; beanMethods = retrieveBeanMethodMetadata(sourceClass); for (MethodMetadata methodMetadata : beanMethods) { configClass.addBeanMethod(new BeanMethod(methodMetadata, configClass)); } // 处理接口定义的默认方法，如果默认方法有 @Bean 注解，将其解析为 BeanMethod 添加到 beanMethods 集合中 processInterfaces(configClass, sourceClass); // 如果父类也是未处理的配置类，则返回父类继续下一次处理 if (sourceClass.getMetadata().hasSuperClass()) { String superclass = sourceClass.getMetadata().getSuperClassName(); if (superclass != null \u0026amp;\u0026amp; !superclass.startsWith(\u0026#34;java\u0026#34;) \u0026amp;\u0026amp; !this.knownSuperclasses.containsKey(superclass)) { this.knownSuperclasses.put(superclass, configClass); // Superclass found, return its annotation metadata and recurse return sourceClass.getSuperClass(); } } // 如果没有父类，则完成处理 return null; } 核心方法解析 定义如下类用于后续演示。\npackage store.xianglin.sb2.config.scan1; @Component public class ComponentClass1 { } package store.xianglin.sb2.config.scan2; public class ComponentClass2 { } package store.xianglin.sb2.config; @Configuration public class MyConfiguration { @Bean public ComponentClass1 componentMyConfiguration() { return new ComponentClass1(); } } 内部类处理 在 MyConfiguration 中定义内部类，并添加 @Configuration 注解。\n@Configuration public class MyConfiguration { @Bean public ComponentClass1 componentMyConfiguration() { return new ComponentClass1(); } @Configuration static class ConfigurationA { @Bean public ComponentClass1 componentConfigurationA() { return new ComponentClass1(); } } static class LiteConfiguration { @Bean public ComponentClass1 componentLiteConfiguration() { return new ComponentClass1(); } } } 解析内部类的入口方法是 org.springframework.context.annotation.ConfigurationClassParser#processMemberClasses。\nif (configClass.getMetadata().isAnnotated(Component.class.getName())) { // Recursively process any member (nested) classes first processMemberClasses(configClass, sourceClass); } private void processMemberClasses(ConfigurationClass configClass, SourceClass sourceClass) throws IOException { // 获取 MyConfiguration 的内部类遍历 Collection\u0026lt;SourceClass\u0026gt; memberClasses = sourceClass.getMemberClasses(); if (!memberClasses.isEmpty()) { List\u0026lt;SourceClass\u0026gt; candidates = new ArrayList\u0026lt;\u0026gt;(memberClasses.size()); for (SourceClass memberClass : memberClasses) { // 如果内部类是配置类，添加到 candidates 集合待处理 if (ConfigurationClassUtils.isConfigurationCandidate(memberClass.getMetadata()) \u0026amp;\u0026amp; !memberClass.getMetadata().getClassName().equals(configClass.getMetadata().getClassName())) { candidates.add(memberClass); } } OrderComparator.sort(candidates); for (SourceClass candidate : candidates) { // 处理内部配置类 processConfigurationClass(candidate.asConfigClass(configClass)); } } } LiteConfiguration 虽然没有添加 @Configuration 注解，但内部声明了带有 @Bean 注解的方法，因此会被认为是 lite configuration，也会被处理。\nPropertySource 处理 在 MyConfiguration 配置类上添加 @PropertySource 注解，指定属性文件。\n@Configuration @PropertySource({\u0026#34;classpath:demo.properties\u0026#34;}) public class MyConfiguration { } 解析 PropertySource 的入口方法是 org.springframework.context.annotation.ConfigurationClassParser#processPropertySource。\n// Process any @PropertySource annotations for (AnnotationAttributes propertySource : AnnotationConfigUtils.attributesForRepeatable( sourceClass.getMetadata(), PropertySources.class, org.springframework.context.annotation.PropertySource.class)) { if (this.environment instanceof ConfigurableEnvironment) { processPropertySource(propertySource); } } private void processPropertySource(AnnotationAttributes propertySource) throws IOException { // 从注解中获取 locations/value 和其它配置项 String[] locations = propertySource.getStringArray(\u0026#34;value\u0026#34;); for (String location : locations) { // 替换占位符，加载资源 String resolvedLocation = this.environment.resolveRequiredPlaceholders(location); Resource resource = this.resourceLoader.getResource(resolvedLocation); // 添加到 environment 中 addPropertySource(factory.createPropertySource(name, new EncodedResource(resource, encoding))); } } private void addPropertySource(PropertySource\u0026lt;?\u0026gt; propertySource) { String name = propertySource.getName(); MutablePropertySources propertySources = ((ConfigurableEnvironment) this.environment).getPropertySources(); // 存在同名属性源（name 属性指定）时，合并配置项 if (this.propertySourceNames.contains(name)) { // We\u0026#39;ve already added a version, we need to extend it } // 将加载的 propertySource 添加到 environment 中 if (this.propertySourceNames.isEmpty()) { propertySources.addLast(propertySource); } else { String firstProcessed = this.propertySourceNames.get(this.propertySourceNames.size() - 1); propertySources.addBefore(firstProcessed, propertySource); } this.propertySourceNames.add(name); } ComponentScan 处理 在 MyConfiguration 配置类上添加 @ComponentScan 注解，指定需要扫描的包路径。\n@Configuration @ComponentScan(basePackages = \u0026#34;store.xianglin.sb2.config.scan1\u0026#34;, basePackageClasses = ComponentClass2.class) public class MyConfiguration { } 解析 ComponentScan 的入口方法是 org.springframework.context.annotation.ComponentScanAnnotationParser#parse。\nSet\u0026lt;AnnotationAttributes\u0026gt; componentScans = AnnotationConfigUtils.attributesForRepeatable( sourceClass.getMetadata(), ComponentScans.class, ComponentScan.class); // 解析 componentScan 注解并扫描注解配置的包路径 for (AnnotationAttributes componentScan : componentScans) { Set\u0026lt;BeanDefinitionHolder\u0026gt; scannedBeanDefinitions = this.componentScanParser.parse(componentScan, sourceClass.getMetadata().getClassName()); for (BeanDefinitionHolder holder : scannedBeanDefinitions) { // 如果扫描得到的类中存在配置类，则立即解析配置类 if (ConfigurationClassUtils.checkConfigurationClassCandidate(bdCand, this.metadataReaderFactory)) { parse(bdCand.getBeanClassName(), holder.getBeanName()); } } } public Set\u0026lt;BeanDefinitionHolder\u0026gt; parse(AnnotationAttributes componentScan, final String declaringClass) { // 从注解中获取配置项 Set\u0026lt;String\u0026gt; basePackages = new LinkedHashSet\u0026lt;\u0026gt;(); String[] basePackagesArray = componentScan.getStringArray(\u0026#34;basePackages\u0026#34;); for (String pkg : basePackagesArray) { String[] tokenized = StringUtils.tokenizeToStringArray(this.environment.resolvePlaceholders(pkg), ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS); Collections.addAll(basePackages, tokenized); } // 从 basePackageClasses 定义的类中获取包路径 for (Class\u0026lt;?\u0026gt; clazz : componentScan.getClassArray(\u0026#34;basePackageClasses\u0026#34;)) { basePackages.add(ClassUtils.getPackageName(clazz)); } // 如果未配置扫描路径，则将配置类所在路径添加到扫描路径中 if (basePackages.isEmpty()) { basePackages.add(ClassUtils.getPackageName(declaringClass)); } // 扫描配置的包路径，返回 BeanDefinitionHolder return scanner.doScan(StringUtils.toStringArray(basePackages)); } Import 处理 @Import 注解允许导入 ImportSelector 和 ImportBeanDefinitionRegistrar 的实现。\npublic class MyImportSelector implements ImportSelector { @Override public String[] selectImports(AnnotationMetadata importingClassMetadata) { System.out.println(\u0026#34;MyImportSelector\u0026#34;); return new String[]{\u0026#34;store.xianglin.sb2.config.scan1.ComponentClass1\u0026#34;}; } } public class MyDeferredImportSelector implements DeferredImportSelector { @Override public String[] selectImports(AnnotationMetadata importingClassMetadata) { System.out.println(\u0026#34;MyDeferredImportSelector\u0026#34;); return new String[]{\u0026#34;store.xianglin.sb2.config.scan2.ComponentClass2\u0026#34;}; } } public class MyBeanImport implements ImportBeanDefinitionRegistrar { @Override public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) { var definition = new RootBeanDefinition(); definition.setBeanClass(Bird.class); registry.registerBeanDefinition(\u0026#34;bird\u0026#34;, definition); } } @Configuration @Import(value = {MyImportSelector.class, MyDeferredImportSelector.class}) public class MyConfiguration { } 解析 Import 的入口方法是 org.springframework.context.annotation.ConfigurationClassParser#processImports。\n// getImports 方法解析 @Import 注解配置的实现类 processImports(configClass, sourceClass, getImports(sourceClass), true); private void processImports(ConfigurationClass configClass, ConfigurationClassParser.SourceClass currentSourceClass, Collection\u0026lt;ConfigurationClassParser.SourceClass\u0026gt; importCandidates, boolean checkForCircularImports) { // 遍历 @Import 指定的配置类 for (ConfigurationClassParser.SourceClass candidate : importCandidates) { if (candidate.isAssignable(ImportSelector.class)) { Class\u0026lt;?\u0026gt; candidateClass = candidate.loadClass(); ImportSelector selector = BeanUtils.instantiateClass(candidateClass, ImportSelector.class); // DeferredImportSelector 实现由 deferredImportSelectorHandler 延迟处理 if (selector instanceof DeferredImportSelector) { this.deferredImportSelectorHandler.handle(configClass, (DeferredImportSelector) selector); } else { // 执行 selectImports 方法得到配置的类名称 String[] importClassNames = selector.selectImports(currentSourceClass.getMetadata()); Collection\u0026lt;ConfigurationClassParser.SourceClass\u0026gt; importSourceClasses = asSourceClasses(importClassNames); // 递归调用 processImports 方法，进入 else 分支，作为配置类解析 processImports(configClass, currentSourceClass, importSourceClasses, false); } } else if (candidate.isAssignable(ImportBeanDefinitionRegistrar.class)) { // Candidate class is an ImportBeanDefinitionRegistrar -\u0026gt; // delegate to it to register additional bean definitions Class\u0026lt;?\u0026gt; candidateClass = candidate.loadClass(); ImportBeanDefinitionRegistrar registrar = BeanUtils.instantiateClass(candidateClass, ImportBeanDefinitionRegistrar.class); // 处理 ImportBeanDefinitionRegistrar 实现类中注册的 BeanDefinition configClass.addImportBeanDefinitionRegistrar(registrar, currentSourceClass.getMetadata()); } else { // 处理普通的配置类 processConfigurationClass(candidate.asConfigClass(configClass)); } } } ImportResource 处理 使用 @ImportResource(locations = \u0026quot;classpath:config/config.xml\u0026quot;) 方式引入 XML 配置文件，解析方法如下。\nAnnotationAttributes importResource = AnnotationConfigUtils.attributesFor(sourceClass.getMetadata(), ImportResource.class); if (importResource != null) { // 获取注解中的配置项：XML 文件路径 String[] resources = importResource.getStringArray(\u0026#34;locations\u0026#34;); Class\u0026lt;? extends BeanDefinitionReader\u0026gt; readerClass = importResource.getClass(\u0026#34;reader\u0026#34;); for (String resource : resources) { // 替换占位符，加载资源 String resolvedResource = this.environment.resolveRequiredPlaceholders(resource); configClass.addImportedResource(resolvedResource, readerClass); } } // 将 resolvedResource 保存到 importedResources 中，后续在 loadBeanDefinitions 方法中解析 public void addImportedResource(String importedResource, Class\u0026lt;? extends BeanDefinitionReader\u0026gt; readerClass) { this.importedResources.put(importedResource, readerClass); } BeanMethod 处理 // 获取配置类中所有添加 @Bean 注解的方法，包装为 BeanMethod Set\u0026lt;MethodMetadata\u0026gt; beanMethods = retrieveBeanMethodMetadata(sourceClass); for (MethodMetadata methodMetadata : beanMethods) { configClass.addBeanMethod(new BeanMethod(methodMetadata, configClass)); } // 将 BeanMethod 添加到 beanMethods 中，后续在 loadBeanDefinitions 方法中解析 public void addBeanMethod(BeanMethod method) { this.beanMethods.add(method); } 默认方法处理 在接口 InterfaceConfiguration 中定义默认方法，MyConfiguration 配置类实现此接口。\npublic interface InterfaceConfiguration { @Bean default ComponentClass1 componentInterfaceConfiguration() { return new ComponentClass1(); } } @Configuration public class MyConfigurationimplements InterfaceConfiguration { } // 处理接口中的默认方法 processInterfaces(configClass, sourceClass); private void processInterfaces(ConfigurationClass configClass, SourceClass sourceClass) throws IOException { for (SourceClass ifc : sourceClass.getInterfaces()) { // 获取所有添加 @Bean 注解的方法 Set\u0026lt;MethodMetadata\u0026gt; beanMethods = retrieveBeanMethodMetadata(ifc); for (MethodMetadata methodMetadata : beanMethods) { if (!methodMetadata.isAbstract()) { // 将 BeanMethod 添加到 beanMethods 中，后续在 loadBeanDefinitions 方法中解析 configClass.addBeanMethod(new BeanMethod(methodMetadata, configClass)); } } // 递归处理父接口 processInterfaces(configClass, ifc); } } 父类处理 定义父类 SuperConfiguration，MyConfiguration 配置类继承此父类。\npublic abstract class SuperConfiguration { @Bean public ComponentClass1 componentSuperConfiguration() { return new ComponentClass1(); } } @Configuration public class MyConfiguration extends SuperConfiguration{ } if (sourceClass.getMetadata().hasSuperClass()) { String superclass = sourceClass.getMetadata().getSuperClassName(); // 父类不为 null、不是 java 开头的包、没有处理过 if (superclass != null \u0026amp;\u0026amp; !superclass.startsWith(\u0026#34;java\u0026#34;) \u0026amp;\u0026amp; !this.knownSuperclasses.containsKey(superclass)) { this.knownSuperclasses.put(superclass, configClass); // 返回父类，再次调用 doProcessConfigurationClass 方法处理 return sourceClass.getSuperClass(); } } ","permalink":"https://2d992714.xianglin2020-github-io.pages.dev/posts/2023/config-class-source-code-learning/","summary":"SpringBoot 配置类解析流程、执行逻辑解析、核心方法解析。","title":"配置类解析 SpringBoot源码学习"},{"content":"核心类解析 FailureAnalyzer：用于分析故障并提供可显示给用户的诊断信息 FailureAnalysis。\n@FunctionalInterface public interface FailureAnalyzer { FailureAnalysis analyze(Throwable failure); } FailureAnalysisReporter：用于向用户报告 FailureAnalyzer 的分析结果 FailureAnalysis。\n@FunctionalInterface public interface FailureAnalysisReporter { void report(FailureAnalysis analysis); } SpringBootExceptionReporter：用于自定义报告 SpringApplication 启动异常的回调接口。\n@FunctionalInterface public interface SpringBootExceptionReporter { boolean reportException(Throwable failure); } FailureAnalyzers：用于触发 FailureAnalyzer 和 FailureAnalysisReporter。\nfinal class FailureAnalyzers implements SpringBootExceptionReporter { @Override public boolean reportException(Throwable failure) { // 调用 FailureAnalyzer 的 analyzer 方法，得到 FailureAnalysis FailureAnalysis analysis = analyze(failure, this.analyzers); // 调用 FailureAnalysisReporter 的 report 方法，报告分析结果 return report(analysis, this.classLoader); } } 异常处理流程解析 SpringBoot 启动异常处理流程的入口在 org.springframework.boot.SpringApplication#run(java.lang.String...) 方法中。\npublic ConfigurableApplicationContext run(String... args) { try { } catch (Throwable ex) { handleRunFailure(context, ex, exceptionReporters, listeners); throw new IllegalStateException(ex); } try { listeners.running(context); } catch (Throwable ex) { handleRunFailure(context, ex, exceptionReporters, null); throw new IllegalStateException(ex); } return context; } 当启动过程中出现异常或调用 SpringApplicationRunListeners 出现异常时会调用 handleRunFailure 方法，完成异常处理流程。\nprivate void handleRunFailure(ConfigurableApplicationContext context, Throwable exception, Collection\u0026lt;SpringBootExceptionReporter\u0026gt; exceptionReporters, SpringApplicationRunListeners listeners) { try { try { // 处理 exitCode handleExitCode(context, exception); if (listeners != null) { // 发送 ApplicationFailedEvent 事件 listeners.failed(context, exception); } } finally { // 调用 FailureAnalyzers 的 reportException 方法报告异常 reportFailure(exceptionReporters, exception); if (context != null) { // 关闭应用程序上下文 context.close(); } } } catch (Exception ex) { logger.warn(\u0026#34;Unable to close ApplicationContext\u0026#34;, ex); } // 重新抛出此异常 ReflectionUtils.rethrowRuntimeException(exception); } 关闭应用程序上下文的逻辑主要有。\npublic void close() { synchronized (this.startupShutdownMonitor) { doClose(); if (this.shutdownHook != null) { try { // 移除 ShutdownHooks，因为 doClose 方法已经明确关闭了 context，不需要借助 ShutdownHooks 来完成这些操作 Runtime.getRuntime().removeShutdownHook(this.shutdownHook); } catch (IllegalStateException ex) { } } } } protected void doClose() { // 更改应用上下文状态 if (this.active.get() \u0026amp;\u0026amp; this.closed.compareAndSet(false, true)) { // 销毁单例 Bean destroyBeans(); // 将 context 中的 beanFactory 置空 closeBeanFactory(); // 留给子类实现：WEB 环境下关闭 WEB 容器 onClose(); this.active.set(false); } } ","permalink":"https://2d992714.xianglin2020-github-io.pages.dev/posts/2023/exception-reporter-source-code-learning/","summary":"SpringBoot 异常报告器类介绍、异常处理流程分析、案例分析。","title":"异常报告器解析 SpringBoot源码学习"},{"content":"属性配置介绍 https://docs.spring.io/spring-boot/docs/current/reference/html/features.html#features.external-config\nSpring 属性有三种使用方式。\n使用 @Value 注解注入到 Bean 中 使用 Environment 抽象类访问 使用 @ConfigurationProperties 与配置对象绑定 后面的属性源可以覆盖前面属性源中定义的值，Spring 属性源优先级按如下顺序。\n默认属性，使用 springApplication.setDefaultProperties(properties) 方法指定。 配置类上使用 @PropertySource 注解，Application Context 刷新后属性会添加到 Environment 中。 配置文件，例如 application.properties（properties 优先级高于 yml）。 jar 包内部的 application.properties 和 application.yml jar 包内部的 application-{profile}.properties 和 application-{profile}.yml jar 包外部的 application.properties 和 application.yml jar 包外部的 application-{profile}.properties 和 application-{profile}.yml 随机值 RandomValuePropertySource。 操作系统环境变量。 Java 系统属性，可以通过 System.getProperties() 获取。 来自 java:comp/env 的 JNDI 属性。 ServletContext 启动参数。 ServletConfig 启动参数。 通过 SPRING_APPLICATION_JSON 指定的属性。 命令行参数。 Spring Aware 常用 Aware BeanNameAware：获取该 Bean 在容器中的名称。 BeanClassLoaderAware：获取加载该 Bean Class 的类加载器。 BeanFactoryAware：获取该 Bean 的 BeanFactory。 EnvironmentAware：获得环境变量 Environment。 EmbeddedValueResolverAware：获取 Spring 容器加载的 properties 文件属性值。 ResourceLoaderAware：获取资源加载器。 ApplicationEventPublisherAware：获取应用事件发布器。 ApplicationContextAware：获取当前应用上下文。 Aware 调用原理 新建启动加载器 ResultCommandlineRunner 实现 EnvironmentAware 接口，在 setEnvironment 方法中打断点，观察方法调用路径。\n@Component public class ResultCommandlineRunner implements CommandLineRunner, EnvironmentAware { private Environment environment; @Override public void run(String... args) throws Exception { System.out.println(environment.getProperty(\u0026#34;property\u0026#34;)); } @Override public void setEnvironment(Environment environment) { // debug here this.environment = environment; } } 通过调试发现 Aware 调用的入口在 initializeBean 方法中。\nprotected Object initializeBean(final String beanName, final Object bean, @Nullable RootBeanDefinition mbd) { // ... invokeAwareMethods(beanName, bean); Object wrappedBean = bean; if (mbd == null || !mbd.isSynthetic()) { wrappedBean = applyBeanPostProcessorsBeforeInitialization(wrappedBean, beanName); } // ... return wrappedBean; } Aware 调用有两个入口，一个是 invokeAwareMethods，这里调用了 BeanNameAware、BeanClassLoaderAware 和 BeanFactoryAware。\nprivate void invokeAwareMethods(final String beanName, final Object bean) { if (bean instanceof Aware) { if (bean instanceof BeanNameAware) { ((BeanNameAware) bean).setBeanName(beanName); } if (bean instanceof BeanClassLoaderAware) { ClassLoader bcl = getBeanClassLoader(); if (bcl != null) { ((BeanClassLoaderAware) bean).setBeanClassLoader(bcl); } } if (bean instanceof BeanFactoryAware) { ((BeanFactoryAware) bean).setBeanFactory(AbstractAutowireCapableBeanFactory.this); } } } 另一个是 applyBeanPostProcessorsBeforeInitialization，通过 Bean 后置处理器 ApplicationContextAwareProcessor 的 postProcessBeforeInitialization 方法调用 invokeAwareInterfaces 方法，完成剩余的 Aware 接口调用。\n// ApplicationContextAwareProcessor.java public Object postProcessBeforeInitialization(final Object bean, String beanName) throws BeansException { // ... invokeAwareInterfaces(bean); // ... return bean; } private void invokeAwareInterfaces(Object bean) { if (bean instanceof Aware) { if (bean instanceof EnvironmentAware) { ((EnvironmentAware) bean).setEnvironment(this.applicationContext.getEnvironment()); } if (bean instanceof EmbeddedValueResolverAware) { ((EmbeddedValueResolverAware) bean).setEmbeddedValueResolver(this.embeddedValueResolver); } if (bean instanceof ResourceLoaderAware) { ((ResourceLoaderAware) bean).setResourceLoader(this.applicationContext); } if (bean instanceof ApplicationEventPublisherAware) { ((ApplicationEventPublisherAware) bean).setApplicationEventPublisher(this.applicationContext); } if (bean instanceof MessageSourceAware) { ((MessageSourceAware) bean).setMessageSource(this.applicationContext); } if (bean instanceof ApplicationContextAware) { ((ApplicationContextAware) bean).setApplicationContext(this.applicationContext); } } } 自定义实现 Aware 通过继承 Aware 接口定义 MyAware 接口，通过实现 BeanPostProcessor 接口，在 postProcessBeforeInitialization 方法中完成 MyAware 的调用。\npublic interface MyAware extends Aware { void setFlag(Flag flag); } @Component public class MyAwareProcessor implements BeanPostProcessor { private final ConfigurableApplicationContext context; public MyAwareProcessor(ConfigurableApplicationContext context) { this.context = context; } @Override public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { if (bean instanceof Aware) { if (bean instanceof MyAware) { ((MyAware) bean).setFlag(context.getBean(\u0026#34;flag\u0026#34;, Flag.class)); } } return bean; } } 使用方式同 Spring 预定义的 Aware 一样，通过实现 MyAware 的 setFlag 方法。\n@Component public class ResultCommandlineRunner implements CommandLineRunner, MyAware { @Override public void run(String... args) throws Exception { System.out.println(flag.isCanOperate()); } private Flag flag; @Override public void setFlag(Flag flag) { this.flag = flag; } } Environment 解析 获取属性 environment.getProperty(\u0026quot;property\u0026quot;) 调用流程如下，最终遍历 propertySources 读取属性。\n// AbstractEnvironment.java public String getProperty(String key) { return this.propertyResolver.getProperty(key); } // PropertySourcesPropertyResolver.java public String getProperty(String key) { return getProperty(key, String.class, true); } protected \u0026lt;T\u0026gt; T getProperty(String key, Class\u0026lt;T\u0026gt; targetValueType, boolean resolveNestedPlaceholders) { if (this.propertySources != null) { // 从 propertySources 中获取属性 for (PropertySource\u0026lt;?\u0026gt; propertySource : this.propertySources) { Object value = propertySource.getProperty(key); if (value != null) { if (resolveNestedPlaceholders \u0026amp;\u0026amp; value instanceof String) { value = resolveNestedPlaceholders((String) value); } logKeyFound(key, propertySource, value); return convertValueIfNecessary(value, targetValueType); } } } return null; } 属性加载解析 Spring 属性解析的入口在 org.springframework.boot.SpringApplication#run(java.lang.String...) 方法的 prepareEnvironment 子方法中。\npublic ConfigurableApplicationContext run(String... args) { // ... ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments); // ... } private ConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners listeners, ApplicationArguments applicationArguments) { // 根据容器类型创建对应的 Environment ConfigurableEnvironment environment = getOrCreateEnvironment(); configureEnvironment(environment, applicationArguments.getSourceArgs()); ConfigurationPropertySources.attach(environment); listeners.environmentPrepared(environment); bindToSpringApplication(environment); if (!this.isCustomEnvironment) { environment = new EnvironmentConverter(getClassLoader()).convertEnvironmentIfNecessary(environment, deduceEnvironmentClass()); } ConfigurationPropertySources.attach(environment); return environment; } 首先根据容器类型 webApplicationType 创建对应的 ConfigurableEnvironment 实例，Servlet WEB 容器创建 StandardServletEnvironment 实例。\nprivate ConfigurableEnvironment getOrCreateEnvironment() { if (this.environment != null) { return this.environment; } switch (this.webApplicationType) { case SERVLET: return new StandardServletEnvironment(); case REACTIVE: return new StandardReactiveWebEnvironment(); default: return new StandardEnvironment(); } } StandardServletEnvironment 继承自抽象类 AbstractEnvironment，AbstractEnvironment 构造方法中调用 customizePropertySources 方法，允许子类在构造期间操作 propertySources 实例。\n// AbstractEnvironment.java private final MutablePropertySources propertySources = new MutablePropertySources(); public AbstractEnvironment() { customizePropertySources(this.propertySources); } // StandardServletEnvironment.java protected void customizePropertySources(MutablePropertySources propertySources) { propertySources.addLast(new StubPropertySource(SERVLET_CONFIG_PROPERTY_SOURCE_NAME)); propertySources.addLast(new StubPropertySource(SERVLET_CONTEXT_PROPERTY_SOURCE_NAME)); if (JndiLocatorDelegate.isDefaultJndiEnvironmentAvailable()) { propertySources.addLast(new JndiPropertySource(JNDI_PROPERTY_SOURCE_NAME)); } super.customizePropertySources(propertySources); } // StandardEnvironment.java protected void customizePropertySources(MutablePropertySources propertySources) { propertySources.addLast( new PropertiesPropertySource(SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME, getSystemProperties())); propertySources.addLast( new SystemEnvironmentPropertySource(SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME, getSystemEnvironment())); } StandardServletEnvironment 中添加三种属性源：servletConfigInitParams、servletContextInitParams 和 jndiProperties。StandardEnvironment 中添加两种属性源：systemProperties（System.getProperties()） 和 systemEnvironment（System.getenv()）。即在 StandardServletEnvironment 构造时添加了五种属性源（下图没有 JNDI）。\nConfigurableEnvironment 实例创建后，调用 configureEnvironment 方法，configureEnvironment 中调用 configurePropertySources 和 configureProfiles 两个模板方法。\nprotected void configureEnvironment(ConfigurableEnvironment environment, String[] args) { configurePropertySources(environment, args); // 配置激活的 profiles configureProfiles(environment, args); } configurePropertySources 方法中添加两种属性源：defaultProperties 和 commandLineArgs。\nprotected void configurePropertySources(ConfigurableEnvironment environment, String[] args) { MutablePropertySources sources = environment.getPropertySources(); if (this.defaultProperties != null \u0026amp;\u0026amp; !this.defaultProperties.isEmpty()) { sources.addLast(new MapPropertySource(\u0026#34;defaultProperties\u0026#34;, this.defaultProperties)); } if (this.addCommandLineProperties \u0026amp;\u0026amp; args.length \u0026gt; 0) { String name = CommandLinePropertySource.COMMAND_LINE_PROPERTY_SOURCE_NAME; // ... sources.addFirst(new SimpleCommandLinePropertySource(args)); } } 接着发布 ApplicationEnvironmentPreparedEvent 事件，对此事件感兴趣的监听器有如下几种。\n其中 ConfigFileApplicationListener 用于调用所有的 EnvironmentPostProcessor，LoggingApplicationListener 用于初始化日志系统。\n// ConfigFileApplicationListener.java private void onApplicationEnvironmentPreparedEvent(ApplicationEnvironmentPreparedEvent event) { List\u0026lt;EnvironmentPostProcessor\u0026gt; postProcessors = loadPostProcessors(); // ConfigFileApplicationListener 也实现了 EnvironmentPostProcessor postProcessors.add(this); AnnotationAwareOrderComparator.sort(postProcessors); for (EnvironmentPostProcessor postProcessor : postProcessors) { postProcessor.postProcessEnvironment(event.getEnvironment(), event.getSpringApplication()); } } List\u0026lt;EnvironmentPostProcessor\u0026gt; loadPostProcessors() { return SpringFactoriesLoader.loadFactories(EnvironmentPostProcessor.class, getClass().getClassLoader()); } 实现了 EnvironmentPostProcessor 接口的类和主要逻辑有\nSystemEnvironmentPropertySourceEnvironmentPostProcessor：将 SystemEnvironmentPropertySource 包装为 OriginAwareSystemEnvironmentPropertySource 属性源，增加 OriginLookup 接口的支持。 SpringApplicationJsonEnvironmentPostProcessor：从 spring.application.json 或 SPRING_APPLICATION_JSON 解析 JSON，向 Environment 添加 spring.application.json 属性源。 CloudFoundryVcapEnvironmentPostProcessor：如果是 SpringCloud 环境，则添加 VCAP 属性源。 ConfigFileApplicationListener：向 Environment 添加 random 属性源和配置文件属性源。 ConfigurationPropertySources.attach(environment); 方法用于将 MutablePropertySources 转换为 ConfigurationPropertySources，向 Environment 添加 configurationProperties 属性源。\n使用 @PropertySource 添加到属性源是在 ConfigurationClassParser 类中解析的。\nprotected final SourceClass doProcessConfigurationClass(ConfigurationClass configClass, SourceClass sourceClass) throws IOException { // Process any @PropertySource annotations for (AnnotationAttributes propertySource : AnnotationConfigUtils.attributesForRepeatable( sourceClass.getMetadata(), PropertySources.class, org.springframework.context.annotation.PropertySource.class)) { if (this.environment instanceof ConfigurableEnvironment) { processPropertySource(propertySource); } } } Spring Profile 解析入口 当 ConfigFileApplicationListener 收到 ApplicationEnvironmentPreparedEvent 事件后，调用 addPropertySources 方法解析配置文件，即 Spring Profile 的处理入口。\nprotected void addPropertySources(ConfigurableEnvironment environment, ResourceLoader resourceLoader) { RandomValuePropertySource.addToEnvironment(environment); new Loader(environment, resourceLoader).load(); } Loader 的 load 方法如下。\n// 保存从外部配置或配置文件中解析的 profile private Deque\u0026lt;Profile\u0026gt; profiles; // 保存已完成解析的 profile private List\u0026lt;Profile\u0026gt; processedProfiles; private boolean activatedProfiles; // 保存每个 profile 中加载的配置 private Map\u0026lt;Profile, MutablePropertySources\u0026gt; loaded; public void load() { this.profiles = new LinkedList\u0026lt;\u0026gt;(); this.processedProfiles = new LinkedList\u0026lt;\u0026gt;(); this.activatedProfiles = false; this.loaded = new LinkedHashMap\u0026lt;\u0026gt;(); // 初始化 profile 信息 initializeProfiles(); while (!this.profiles.isEmpty()) { Profile profile = this.profiles.poll(); if (profile != null \u0026amp;\u0026amp; !profile.isDefaultProfile()) { addProfileToEnvironment(profile.getName()); } load(profile, this::getPositiveProfileFilter, addToLoaded(MutablePropertySources::addLast, false)); this.processedProfiles.add(profile); } resetEnvironmentProfiles(this.processedProfiles); load(null, this::getNegativeProfileFilter, addToLoaded(MutablePropertySources::addFirst, true)); addLoadedPropertySources(); } 初始化 profile 信息 private void initializeProfiles() { // 添加 null，用于处理 application.properties 或 application.yml 配置文件 this.profiles.add(null); // 从环境变量中查找 spring.profiles.active 和 spring.profiles.include 配置项，如果存在将其加入 profiles 中 Set\u0026lt;Profile\u0026gt; activatedViaProperty = getProfilesActivatedViaProperty(); this.profiles.addAll(getOtherActiveProfiles(activatedViaProperty)); // Any pre-existing active profiles set via property sources (e.g. // System properties) take precedence over those added in config files. addActiveProfiles(activatedViaProperty); if (this.profiles.size() == 1) { // only has null profile // 如果没有指定 profile，默认会添加一个名为 default 的 profile（可通过 spring.profiles.default 配置修改） for (String defaultProfileName : this.environment.getDefaultProfiles()) { Profile defaultProfile = new Profile(defaultProfileName, true); this.profiles.add(defaultProfile); } } } Profile 处理逻辑 private void load(Profile profile, DocumentFilterFactory filterFactory, DocumentConsumer consumer) { // 获取配置文件的搜索路径 // 1. 如果指定了 spring.config.location 则从指定位置查找 // 2. 如果指定了 spring.config.additional-location，则将指定位置和默认位置（classpath:/,classpath:/config/,file:./,file:./config/）合并查找 getSearchLocations().forEach((location) -\u0026gt; { boolean isFolder = location.endsWith(\u0026#34;/\u0026#34;); // 是否通过 spring.config.name 指定配置文件名字，默认为 application Set\u0026lt;String\u0026gt; names = isFolder ? getSearchNames() : NO_SEARCH_NAMES; names.forEach((name) -\u0026gt; load(location, name, profile, filterFactory, consumer)); }); } PropertySourceLoader PropertiesPropertySourceLoader 用于加载 properties 和 xml 文件，YamlPropertySourceLoader 用于加载 yml 和 yaml 文件。\nload 逻辑 在上一步获取到配置文件搜索路径和名称后，load 方法主要是判断配置文件是否存在，存在即读取配置文件。\nprivate void load(PropertySourceLoader loader, String location, Profile profile, DocumentFilter filter, DocumentConsumer consumer) { try { // 读取 location/name.extension 文件，默认 classpath:/application.properties 等。 Resource resource = this.resourceLoader.getResource(location); // 如果资源存在，则读取并解析文件。 String name = \u0026#34;applicationConfig: [\u0026#34; + location + \u0026#34;]\u0026#34;; List\u0026lt;Document\u0026gt; documents = loadDocuments(loader, name, resource); List\u0026lt;Document\u0026gt; loaded = new ArrayList\u0026lt;\u0026gt;(); for (Document document : documents) { if (filter.match(document)) { // 将此配置内激活的 profile 添加到 profiles 集合中，待后续解析 addActiveProfiles(document.getActiveProfiles()); addIncludedProfiles(document.getIncludeProfiles()); loaded.add(document); } } Collections.reverse(loaded); if (!loaded.isEmpty()) { // 将此配置文件存放于 loaded 中（Map\u0026lt;Profile, MutablePropertySources\u0026gt; loaded） loaded.forEach((document) -\u0026gt; consumer.accept(profile, document)); } } catch (Exception ex) { } } 将配置添加到 environment private void addLoadedPropertySources() { MutablePropertySources destination = this.environment.getPropertySources(); List\u0026lt;MutablePropertySources\u0026gt; loaded = new ArrayList\u0026lt;\u0026gt;(this.loaded.values()); Collections.reverse(loaded); String lastAdded = null; Set\u0026lt;String\u0026gt; added = new HashSet\u0026lt;\u0026gt;(); // 遍历从 profiles 中解析出的配置信息 for (MutablePropertySources sources : loaded) { // 一个 profile 可能对应多个配置文件，即 application-default.properties | application-default.yml for (PropertySource\u0026lt;?\u0026gt; source : sources) { if (added.add(source.getName())) { // 将属性集添加到 MutablePropertySources 中 addLoadedPropertySource(destination, lastAdded, source); lastAdded = source.getName(); } } } } ","permalink":"https://2d992714.xianglin2020-github-io.pages.dev/posts/2023/configuration-source-code-learning/","summary":"SpringBoot 属性配置介绍、Aware 介绍、Environment 解析、Spring profile 解析。","title":"属性配置解析 SpringBoot源码学习"},{"content":"计时器介绍 StopWatch 是 Spring 提供的秒表工具类，允许对多个任务进行计时，显示总运行时间和每个指定任务的运行时间，简单使用例子。\npublic class StopWatchTest { @Test public void testStopWatch() throws InterruptedException { var stopWatch = new StopWatch(\u0026#34;testWatch\u0026#34;); stopWatch.start(\u0026#34;task1\u0026#34;); TimeUnit.SECONDS.sleep(2); stopWatch.stop(); stopWatch.start(\u0026#34;task2\u0026#34;); TimeUnit.SECONDS.sleep(1); stopWatch.stop(); stopWatch.start(\u0026#34;task3\u0026#34;); TimeUnit.SECONDS.sleep(3); stopWatch.stop(); System.out.println(stopWatch.prettyPrint()); System.out.println(stopWatch); } } 启动加载器案例 实现 CommandLineRunner 和 ApplicationRunner 接口的类称为启动加载器，用于在 SpringApplication 启动完成后立即执行其 run 方法，多个启动加载器通过使用 Ordered 接口或 @Order 注释进行排序，Order 值相同时 ApplicationRunner 优先执行。\n@Component @Order(1) public class FirstApplicationRunner implements ApplicationRunner { @Override public void run(ApplicationArguments args) { System.out.println(\u0026#34;\\u001B[32m \u0026gt;\u0026gt;\u0026gt; startup first ApplicationRunner \u0026lt;\u0026lt;\u0026lt;\u0026#34;); } } @Component @Order(1) public class FirstCommandlineRunner implements CommandLineRunner { @Override public void run(String... args) { System.out.println(\u0026#34;\\u001B[32m \u0026gt;\u0026gt;\u0026gt; startup first CommandlineRunner \u0026lt;\u0026lt;\u0026lt;\u0026#34;); } } 启动加载器原理解析 启动加载器调用的入口在 org.springframework.boot.SpringApplication#run(java.lang.String...) 方法的 callRunners 方法中。\npublic ConfigurableApplicationContext run(String... args) { // 将启动参数包装为 ApplicationArguments ApplicationArguments applicationArguments = new DefaultApplicationArguments(args); // ... listeners.started(context); // SpringApplication 启动完成后执行 callRunners(context, applicationArguments); // ... listeners.running(context); } callRunners 的逻辑比较简单，从容器中获取所有实现 ApplicationRunner 和 CommandLineRunner 接口的实例，按 Order 排序后依次调用。\nprivate void callRunners(ApplicationContext context, ApplicationArguments args) { List\u0026lt;Object\u0026gt; runners = new ArrayList\u0026lt;\u0026gt;(); // 因为先加入 ApplicationRunner 后加入 CommandLineRunner，排序后 Order 值相同的 ApplicationRunner 靠前 runners.addAll(context.getBeansOfType(ApplicationRunner.class).values()); runners.addAll(context.getBeansOfType(CommandLineRunner.class).values()); AnnotationAwareOrderComparator.sort(runners); for (Object runner : new LinkedHashSet\u0026lt;\u0026gt;(runners)) { if (runner instanceof ApplicationRunner) { callRunner((ApplicationRunner) runner, args); } if (runner instanceof CommandLineRunner) { callRunner((CommandLineRunner) runner, args); } } } ApplicationRunner 和 CommandLineRunner 唯一不同是 run 方法的入参不一样，ApplicationArguments 是对 String[] args 入参的包装，默认实现 DefaultApplicationArguments 会将 String[] args 解析成 CommandLineArgs。\n","permalink":"https://2d992714.xianglin2020-github-io.pages.dev/posts/2023/runner-source-code-learning/","summary":"SpringBoot 计时器介绍、启动加载器案例演示、启动加载器原理解析。","title":"启动加载器解析 SpringBoot源码学习"},{"content":"Banner 演示 通过配置文件指定 通过配置 spring.banner.location 指定文字 Banner 文件，通过配置 spring.banner.image.location 指定图片 Banner 文件，通过配置 spring.main.banner-mode 设置 Banner 展示模式：OFF —关闭 Banner，CONSOLE —输出到 System.out，LOG—打印到日志文件 。\nspring.banner.location=favorite.txt spring.banner.image.location=favorite.jpg spring.main.banner-mode=console 可以同时配置文字 Banner 和图片 Banner。\n通过编码指定 在未使用配置文件指定和没有默认配置时生效。\n@SpringBootApplication @MapperScan(\u0026#34;store.xianglin.sb2.mapper\u0026#34;) public class SpringBoot2Application { public static void main(String[] args) { var springApplication = new SpringApplication(SpringBoot2Application.class); springApplication.setBanner(new ResourceBanner(new ClassPathResource(\u0026#34;favorite.txt\u0026#34;))); springApplication.setBannerMode(Banner.Mode.CONSOLE); springApplication.run(args); } } Banner 打印原理 Banner 打印是通过在 SpringApplication#run 方法中调用 printBanner 方法，printBanner 的逻辑为：获取 Banner，根据 Banner.Mode 打印 Banner。\nBanner printedBanner = printBanner(environment); // 根据 Banner.Mode 选择打印方式 private Banner printBanner(ConfigurableEnvironment environment) { if (this.bannerMode == Banner.Mode.OFF) { return null; } ResourceLoader resourceLoader = (this.resourceLoader != null) ? this.resourceLoader : new DefaultResourceLoader(getClassLoader()); SpringApplicationBannerPrinter bannerPrinter = new SpringApplicationBannerPrinter(resourceLoader, this.banner); if (this.bannerMode == Mode.LOG) { return bannerPrinter.print(environment, this.mainApplicationClass, logger); } return bannerPrinter.print(environment, this.mainApplicationClass, System.out); } // 打印逻辑：获取 Banner、打印 Banner public Banner print(Environment environment, Class\u0026lt;?\u0026gt; sourceClass, PrintStream out) { Banner banner = getBanner(environment); banner.printBanner(environment, sourceClass, out); return new PrintedBanner(banner, sourceClass); } Banner 内容获取 以 Banner.Mode.CONSOLE 为例，获取 Banner 的逻辑见 org.springframework.boot.SpringApplicationBannerPrinter#getBanner方法。\nprivate Banner getBanner(Environment environment) { // Banners：Banner 容器，private final List\u0026lt;Banner\u0026gt; banners = new ArrayList\u0026lt;\u0026gt;(); Banners banners = new Banners(); // 添加图片 Banner banners.addIfNotNull(getImageBanner(environment)); // 添加文字 Banner banners.addIfNotNull(getTextBanner(environment)); if (banners.hasAtLeastOneBanner()) { return banners; } // fallbackBanner 为编码指定的 Banner if (this.fallbackBanner != null) { return this.fallbackBanner; } // 默认 Banner，输出 Spring 横幅 return DEFAULT_BANNER; } getImageBanner 和 getTextBanner 的逻辑基本一致，首先判断是否通过配置文件指定 Banner location，然后判断是否存在默认配置。\nprivate Banner getImageBanner(Environment environment) { // BANNER_IMAGE_LOCATION_PROPERTY : spring.banner.image.location String location = environment.getProperty(BANNER_IMAGE_LOCATION_PROPERTY); if (StringUtils.hasLength(location)) { Resource resource = this.resourceLoader.getResource(location); return resource.exists() ? new ImageBanner(resource) : null; } // IMAGE_EXTENSION : \u0026#34;gif\u0026#34;, \u0026#34;jpg\u0026#34;, \u0026#34;png\u0026#34; for (String ext : IMAGE_EXTENSION) { Resource resource = this.resourceLoader.getResource(\u0026#34;banner.\u0026#34; + ext); if (resource.exists()) { return new ImageBanner(resource); } } return null; } Banner 内容输出 方法 org.springframework.boot.Banner#printBanner 的实现有 5 个，其中 PrintedBanner 和 Banners 不涉及打印逻辑，其余类的打印逻辑比较简单。\n","permalink":"https://2d992714.xianglin2020-github-io.pages.dev/posts/2023/banner-source-code-learning/","summary":"SpringBoot Banner解析、效果演示。","title":"Banner解析 SpringBoot源码学习"},{"content":"Bean 的配置方式 XML 方式配置 Bean 新建 Student 类，有 name、age、classList 三个属性，HelloService 类有 Student 属性。\npublic class Student { private String name; private Integer age; private List\u0026lt;String\u0026gt; classList; // getter and setter } public class HelloService { private Student student; public Student getStudent() { return student; } public void setStudent(Student student) { this.student = student; } public String hello() { return student.toString(); } } 在 resources 目录下新建 Spring 配置文件 demo.xml。\n\u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34;?\u0026gt; \u0026lt;beans xmlns=\u0026#34;http://www.springframework.org/schema/beans\u0026#34; xmlns:xsi=\u0026#34;http://www.w3.org/2001/XMLSchema-instance\u0026#34; xsi:schemaLocation=\u0026#34;http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd\u0026#34;\u0026gt; \u0026lt;!-- student 配置 --\u0026gt; \u0026lt;bean id=\u0026#34;helloService\u0026#34; class=\u0026#34;store.xianglin.sb2.ioc.xml.HelloService\u0026#34;\u0026gt; \u0026lt;property name=\u0026#34;student\u0026#34; ref=\u0026#34;student\u0026#34;/\u0026gt; \u0026lt;/bean\u0026gt; \u0026lt;/beans\u0026gt; 在 test 目录下新建测试类，测试 HelloService 类的 hello 方法。\n@SpringBootTest @RunWith(SpringRunner.class) @ContextConfiguration(locations = \u0026#34;classpath:ioc/demo.xml\u0026#34;) public class HelloServiceTest { @Resource private HelloService helloService; @Test public void testHello() { System.out.println(helloService.hello()); } } 方式一、通过属性注入。\n\u0026lt;bean id=\u0026#34;student\u0026#34; class=\u0026#34;store.xianglin.sb2.ioc.xml.Student\u0026#34;\u0026gt; \u0026lt;property name=\u0026#34;name\u0026#34; value=\u0026#34;zhangsan\u0026#34;/\u0026gt; \u0026lt;property name=\u0026#34;age\u0026#34; value=\u0026#34;13\u0026#34;/\u0026gt; \u0026lt;property name=\u0026#34;classList\u0026#34;\u0026gt; \u0026lt;list\u0026gt; \u0026lt;value\u0026gt;math\u0026lt;/value\u0026gt; \u0026lt;value\u0026gt;english\u0026lt;/value\u0026gt; \u0026lt;/list\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;/bean\u0026gt; 方式二、通过构造器注入。在 Student 类中创建两个参数的构造器，在 demo.xml 中使用 constructor-arg 标签注入。\npublic Student(String name, Integer age) { this.name = name; this.age = age; } \u0026lt;bean id=\u0026#34;student\u0026#34; class=\u0026#34;store.xianglin.sb2.ioc.xml.Student\u0026#34;\u0026gt; \u0026lt;constructor-arg index=\u0026#34;0\u0026#34; value=\u0026#34;zhangsan\u0026#34;/\u0026gt; \u0026lt;constructor-arg index=\u0026#34;1\u0026#34; value=\u0026#34;13\u0026#34;/\u0026gt; \u0026lt;property name=\u0026#34;classList\u0026#34;\u0026gt; \u0026lt;list\u0026gt; \u0026lt;value\u0026gt;math\u0026lt;/value\u0026gt; \u0026lt;value\u0026gt;english\u0026lt;/value\u0026gt; \u0026lt;/list\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;/bean\u0026gt; 新建抽象类 Animal，定义抽象方法 getName，定义子类 Cat 和 Dog。新建工厂类 AnimalFactory，定义静态方法 getAnimal、实例方法 getAnimalInstance，根据入参返回 Animal 的实例。\npublic abstract class Animal { abstract String getName(); } public class Cat extends Animal { @Override String getName() { return \u0026#34;cat\u0026#34;; } } public class Dog extends Animal{ @Override String getName() { return \u0026#34;dog\u0026#34;; } } public class AnimalFactory { public static Animal getAnimal(String type) { if (\u0026#34;dog\u0026#34;.equals(type)) { return new Dog(); } else if (\u0026#34;cat\u0026#34;.equals(type)) { return new Cat(); } return null; } public Animal getAnimalInstance(String type) { if (\u0026#34;dog\u0026#34;.equals(type)) { return new Dog(); } else if (\u0026#34;cat\u0026#34;.equals(type)) { return new Cat(); } return null; } } 方式三、静态工厂方法注入。\n\u0026lt;bean id=\u0026#34;dog\u0026#34; class=\u0026#34;store.xianglin.sb2.ioc.xml.AnimalFactory\u0026#34; factory-method=\u0026#34;getAnimal\u0026#34;\u0026gt; \u0026lt;constructor-arg value=\u0026#34;dog\u0026#34;/\u0026gt; \u0026lt;/bean\u0026gt; \u0026lt;bean id=\u0026#34;cat\u0026#34; class=\u0026#34;store.xianglin.sb2.ioc.xml.AnimalFactory\u0026#34; factory-method=\u0026#34;getAnimal\u0026#34;\u0026gt; \u0026lt;constructor-arg value=\u0026#34;cat\u0026#34;/\u0026gt; \u0026lt;/bean\u0026gt; 方式四、实例工厂方法注入。\n\u0026lt;bean id=\u0026#34;animalFactory\u0026#34; class=\u0026#34;store.xianglin.sb2.ioc.xml.AnimalFactory\u0026#34;/\u0026gt; \u0026lt;bean id=\u0026#34;dog1\u0026#34; factory-bean=\u0026#34;animalFactory\u0026#34; factory-method=\u0026#34;getAnimalInstance\u0026#34;\u0026gt; \u0026lt;constructor-arg value=\u0026#34;dog\u0026#34;/\u0026gt; \u0026lt;/bean\u0026gt; \u0026lt;bean id=\u0026#34;cat1\u0026#34; factory-bean=\u0026#34;animalFactory\u0026#34; factory-method=\u0026#34;getAnimalInstance\u0026#34;\u0026gt; \u0026lt;constructor-arg value=\u0026#34;cat\u0026#34;/\u0026gt; \u0026lt;/bean\u0026gt; 注解方式配置 Bean 方式一、使用 @Component 注解，直接在 HelloService 类上添加 @Component，将其注入到容器中。\n@Component public class HelloService { @Resource private Animal animal; public Animal getAnimal() { return animal; } public void setAnimal(Animal animal) { this.animal = animal; } public String hello() { return animal.getName(); } } 方式二、使用 @Bean 注解，在 @Configuration 注解标注的配置类中通过 @Bean 注解注入 Bean。\n@Configuration public class BeanConfiguration { @Bean(\u0026#34;dog\u0026#34;) public Animal getDog() { return new Dog(); } } 方式三、实现 FactoryBean 接口。\n@Component public class MyCat implements FactoryBean\u0026lt;Animal\u0026gt; { @Override public Animal getObject() { return new Cat(); } @Override public Class\u0026lt;?\u0026gt; getObjectType() { return Animal.class; } } 方式四、实现 BeanDefinitionRegistryPostProcessor 接口，在 postProcessBeanDefinitionRegistry 方法中添加 BeanDefinition。\n@Component public class MyBeanRegister implements BeanDefinitionRegistryPostProcessor, InstantiationAwareBeanPostProcessor { @Override public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException { // 注入 BeanDefinition var definition = new RootBeanDefinition(); definition.setBeanClass(Monkey.class); registry.registerBeanDefinition(\u0026#34;monkey\u0026#34;, definition); } @Override public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { // 为 BeanDefinition 注入属性 var monkey = beanFactory.getBeanDefinition(\u0026#34;monkey\u0026#34;); monkey.getPropertyValues().addPropertyValue(\u0026#34;property\u0026#34;, \u0026#34;value from postProcessBeanFactory\u0026#34;); } @Override public Object postProcessBeforeInstantiation(Class\u0026lt;?\u0026gt; beanClass, String beanName) throws BeansException { // 自定义创建 Bean，不会执行后续的 doCreateBean 方法以及属性注入等 if (beanClass.equals(Monkey.class)) { return new Monkey(); } return null; } } 方式五、实现 ImportBeanDefinitionRegistrar，通过 @Import 导入。\npublic class MyBeanImport implements ImportBeanDefinitionRegistrar { @Override public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) { var definition = new RootBeanDefinition(); definition.setBeanClass(Bird.class); registry.registerBeanDefinition(\u0026#34;bird\u0026#34;, definition); } } @Import(MyBeanImport.class) @Configuration public class BeanConfiguration { } refresh 解析 refresh 方法定义在 AbstractApplicationContext，用于加载或刷新配置，调用了如下 13 个方法。\npublic void refresh() throws BeansException, IllegalStateException { synchronized (this.startupShutdownMonitor) { // Prepare this context for refreshing. prepareRefresh(); // Tell the subclass to refresh the internal bean factory. ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory(); // Prepare the bean factory for use in this context. prepareBeanFactory(beanFactory); try { // Allows post-processing of the bean factory in context subclasses. postProcessBeanFactory(beanFactory); // Invoke factory processors registered as beans in the context. invokeBeanFactoryPostProcessors(beanFactory); // Register bean processors that intercept bean creation. registerBeanPostProcessors(beanFactory); // Initialize message source for this context. initMessageSource(); // Initialize event multicaster for this context. initApplicationEventMulticaster(); // Initialize other special beans in specific context subclasses. onRefresh(); // Check for listener beans and register them. registerListeners(); // Instantiate all remaining (non-lazy-init) singletons. finishBeanFactoryInitialization(beanFactory); // Last step: publish corresponding event. finishRefresh(); } catch (BeansException ex) { if (logger.isWarnEnabled()) { logger.warn(\u0026#34;Exception encountered during context initialization - \u0026#34; + \u0026#34;cancelling refresh attempt: \u0026#34; + ex); } // Destroy already created singletons to avoid dangling resources. destroyBeans(); // Reset \u0026#39;active\u0026#39; flag. cancelRefresh(ex); // Propagate exception to caller. throw ex; } finally { // Reset common introspection caches in Spring\u0026#39;s core, since we // might not ever need metadata for singleton beans anymore... resetCommonCaches(); } } } prepareRefresh 设置容器状态：启动时间、active 标记，初始化属性设置，检查必备属性是否存在。\nprotected void prepareRefresh() { // Switch to active. this.startupDate = System.currentTimeMillis(); this.closed.set(false); this.active.set(true); // Initialize any placeholder property sources in the context environment. initPropertySources(); // Validate that all properties marked as required are resolvable: // see ConfigurablePropertyResolver#setRequiredProperties getEnvironment().validateRequiredProperties(); } 必备属性可以通过在 ApplicationContextInitializer 的 initialize 方法中调用 environment.setRequiredProperties() 方法添加，缺少必备属性时抛出 MissingRequiredPropertiesException 异常，SpringApplication 初始化失败。\nobtainFreshBeanFactory 通知子类刷新内部的 beanFactory，设置 beanFactory 序列化 ID，获取 beanFactory（DefaultListableBeanFactory）。\nprotected ConfigurableListableBeanFactory obtainFreshBeanFactory() { refreshBeanFactory(); return getBeanFactory(); } protected final void refreshBeanFactory() throws IllegalStateException { if (!this.refreshed.compareAndSet(false, true)) { throw new IllegalStateException( \u0026#34;GenericApplicationContext does not support multiple refresh attempts: just call \u0026#39;refresh\u0026#39; once\u0026#34;); } // 设置序列化 ID，可用于通过此 ID 反序列化 BeanFactory 对象 this.beanFactory.setSerializationId(getId()); } prepareBeanFactory 设置 beanFactory 的属性：ClassLoader、PropertyEditorRegistrar、BeanPostProcessor，设置忽略的自动装配接口，注册默认环境相关的 Bean。\nprotected void prepareBeanFactory(ConfigurableListableBeanFactory beanFactory) { // Tell the internal bean factory to use the context\u0026#39;s class loader etc. beanFactory.setBeanClassLoader(getClassLoader()); beanFactory.setBeanExpressionResolver(new StandardBeanExpressionResolver(beanFactory.getBeanClassLoader())); beanFactory.addPropertyEditorRegistrar(new ResourceEditorRegistrar(this, getEnvironment())); // Configure the bean factory with context callbacks. beanFactory.addBeanPostProcessor(new ApplicationContextAwareProcessor(this)); beanFactory.ignoreDependencyInterface(EnvironmentAware.class); // BeanFactory interface not registered as resolvable type in a plain factory. // MessageSource registered (and found for autowiring) as a bean. beanFactory.registerResolvableDependency(BeanFactory.class, beanFactory); // Register early post-processor for detecting inner beans as ApplicationListeners. beanFactory.addBeanPostProcessor(new ApplicationListenerDetector(this)); // Register default environment beans. if (!beanFactory.containsLocalBean(ENVIRONMENT_BEAN_NAME)) { beanFactory.registerSingleton(ENVIRONMENT_BEAN_NAME, getEnvironment()); } } postProcessBeanFactory 子类重写以在 beanFactory 完成创建后做进一步设置，WEB 环境中注册 ServletContextAwareProcessor，注册 WEB 环境特有的 scope：request、session、application。\nprotected void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) { beanFactory.addBeanPostProcessor(new WebApplicationContextServletContextAwareProcessor(this)); beanFactory.ignoreDependencyInterface(ServletContextAware.class); registerWebApplicationScopes(); } invokeBeanFactoryPostProcessors 按顺序实例化并调用所有已注册的 BeanDefinitionRegistryPostProcessor 和 BeanFactoryPostProcessor，上述注解配置方式四定义的 MyBeanRegister 在这里被调用，此方法主要作用有：\n调用 BeanDefinitionRegistryPostProcessor 实现向容器内添加 Bean 的定义。 调用 BeanFactoryPostProcessor 实现向容器内 Bean 的定义添加属性。 protected void invokeBeanFactoryPostProcessors(ConfigurableListableBeanFactory beanFactory) { PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(beanFactory, getBeanFactoryPostProcessors()); } registerBeanPostProcessors 按顺序实例化并注册所有 BeanPostProcessor ，但不调用。\ninitMessageSource 初始化国际化相关属性。\ninitApplicationEventMulticaster 初始化事件广播器。\nonRefresh 在 WEB 环境中调用 createWebServer 方法创建 WEB 容器。\n// ServletWebServerApplicationContext protected void onRefresh() { super.onRefresh(); try { createWebServer(); } catch (Throwable ex) { throw new ApplicationContextException(\u0026#34;Unable to start web server\u0026#34;, ex); } } registerListeners 添加容器内事件监听器至事件广播器中，广播早期事件。\nfinishBeanFactoryInitialization 初始化所有剩余的单例 Bean。\nfinishRefresh 初始化生命周期处理器，调用生命周期处理器的 onRefresh 方法，发布 ContextRefreshedEvent 事件。\nBean 实例化解析 BeanDefinition 一个对象在 Spring 中使用 BeanDefinition 来描述，RootBeanDefinition 是其常见实现，可以通过操作 BeanDefinition 来完成 Bean 实例化和属性注入。\n自定义创建 Bean 逻辑见 org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#createBean(java.lang.String, org.springframework.beans.factory.support.RootBeanDefinition, java.lang.Object[])，在 createBean 方法中调用 resolveBeforeInstantiation 方法，随后调用 applyBeanPostProcessorsBeforeInstantiation 方法，最终调用 InstantiationAwareBeanPostProcessor 的 postProcessBeforeInstantiation 方法，该方法在实例化之前调用，如果返回 Bean 对象，则不再执行 Bean 的默认实例化逻辑（doCreateBean 方法以及属性注入等）。\nprotected Object createBean(String beanName, RootBeanDefinition mbd, @Nullable Object[] args) throws BeanCreationException { try { // Bean 实例化之前调用，如果返回 Bean 对象，不执行后续方法 Object bean = resolveBeforeInstantiation(beanName, mbdToUse); if (bean != null) { return bean; } } catch (Throwable ex) { throw new BeanCreationException(mbdToUse.getResourceDescription(), beanName, \u0026#34;BeanPostProcessor before instantiation of bean failed\u0026#34;, ex); } // 调用默认实例化方法 Object beanInstance = doCreateBean(beanName, mbdToUse, args); } protected Object resolveBeforeInstantiation(String beanName, RootBeanDefinition mbd){ Object bean = null; bean = applyBeanPostProcessorsBeforeInstantiation(targetType, beanName); if (bean != null) { bean = applyBeanPostProcessorsAfterInitialization(bean, beanName); } return bean; } protected Object applyBeanPostProcessorsBeforeInstantiation(Class\u0026lt;?\u0026gt; beanClass, String beanName) { for (BeanPostProcessor bp : getBeanPostProcessors()) { if (bp instanceof InstantiationAwareBeanPostProcessor) { // 实现 InstantiationAwareBeanPostProcessor 自定义创建 Bean InstantiationAwareBeanPostProcessor ibp = (InstantiationAwareBeanPostProcessor) bp; Object result = ibp.postProcessBeforeInstantiation(beanClass, beanName); if (result != null) { return result; } } } return null; } Bean 实例化流程 单例 Bean 实例化入口为 org.springframework.beans.factory.support.DefaultListableBeanFactory#preInstantiateSingletons 方法，主要的方法调用如图。\n","permalink":"https://2d992714.xianglin2020-github-io.pages.dev/posts/2023/bean-source-code-learning/","summary":"SpringBoot 中 IOC思想、Bean 的配置方式、refresh 方法解析、Bean 实例化解析。","title":"Bean解析 SpringBoot源码学习"},{"content":"监听器模式介绍 编码实现监听器模式 实现一个天气预报的监听器，当天气为下雨或下雪时，通知监听者。\n定义一个表示天气事件的抽象类 WeatherEvent，有下雨事件 RainEvent 和下雪事件 SnowEvent。\npublic abstract class WeatherEvent { public abstract String getWeather(); } public class RainEvent extends WeatherEvent { @Override public String getWeather() { return \u0026#34;rain\u0026#34;; } } public class SnowEvent extends WeatherEvent { @Override public String getWeather() { return \u0026#34;snow\u0026#34;; } } 定义事件监听器，分别监听下雪事件和下雨事件。\npublic interface WeatherListener { void onWeatherEvent(WeatherEvent event); } public class RainListener implements WeatherListener{ @Override public void onWeatherEvent(WeatherEvent event) { if (event instanceof RainEvent){ System.out.println(\u0026#34;hello \u0026#34; + event.getWeather()); } } } public class SnowListener implements WeatherListener{ @Override public void onWeatherEvent(WeatherEvent event) { if (event instanceof SnowEvent){ System.out.println(\u0026#34;hello \u0026#34; + event.getWeather()); } } } 定义事件广播器，用于广播事件和管理事件监听器。\npublic interface EventMulticaster { void multicastEvent(WeatherEvent event); void addListener(WeatherListener weatherListener); void removeListener(WeatherListener weatherListener); } public abstract class AbstractEventMulter implements EventMulticaster { private List\u0026lt;WeatherListener\u0026gt; listenerList; @Override public void multicastEvent(WeatherEvent event) { doStart(); listenerList.forEach(i -\u0026gt; i.onWeatherEvent(event)); doEnd(); } abstract void doStart(); abstract void doEnd(); @Override public void addListener(WeatherListener weatherListener) { if (listenerList == null) { listenerList = new ArrayList\u0026lt;\u0026gt;(); } listenerList.add(weatherListener); } @Override public void removeListener(WeatherListener weatherListener) { listenerList.remove(weatherListener); } } public class WeatherEventMulticaster extends AbstractEventMulter { @Override void doStart() { System.out.println(\u0026#34;begin broadcast weather event\u0026#34;); } @Override void doEnd() { System.out.println(\u0026#34;end broadcast weather event\u0026#34;); } } 编写测试程序（触发机制）测试天气事件及监听器。\npublic void testEventMulticast() { var eventMulticaster = new WeatherEventMulticaster(); var weatherListener = new RainListener(); eventMulticaster.addListener(weatherListener); eventMulticaster.addListener(new SnowListener()); eventMulticaster.multicastEvent(new SnowEvent()); eventMulticaster.multicastEvent(new RainEvent()); eventMulticaster.removeListener(weatherListener); eventMulticaster.multicastEvent(new SnowEvent()); eventMulticaster.multicastEvent(new RainEvent()); } 监听器模式的基本要素 监听器 广播器 事件 触发机制 SpringBoot 监听器实现 Spring 系统事件监听器由接口 ApplicationListener 定义。\n@FunctionalInterface public interface ApplicationListener\u0026lt;E extends ApplicationEvent\u0026gt; extends EventListener { // 监听到事件后的回调方法 void onApplicationEvent(E event); } Spring 系统事件广播器由接口 ApplicationEventMulticaster 定义。\n// 事件广播器用于管理 ApplicationListener 和向它们广播事件 public interface ApplicationEventMulticaster { // 管理 ApplicationListener 的方法：新增、删除 void addApplicationListener(ApplicationListener\u0026lt;?\u0026gt; listener); void removeApplicationListener(ApplicationListener\u0026lt;?\u0026gt; listener); void removeAllListeners(); // 向监听器广播事件 void multicastEvent(ApplicationEvent event); } Spring 系统事件由抽象类 ApplicationEvent 定义，主要系统事件有。\npublic abstract class ApplicationEvent extends EventObject { // 事件发生的时间 private final long timestamp; // 触发事件的对象（EventObject） protected transient Object source; } SpringBoot 系统事件发送顺序 SpringBoot 启动时系统事件和事件触发顺序由 SpringApplicationRunListener 接口定义。\n// 实现类：EventPublishingRunListener public interface SpringApplicationRunListener { // ApplicationStartingEvent：SpringApplication run 方法执行时触发 void starting(); // ApplicationEnvironmentPreparedEvent：环境准备好 ApplicationContext 还未创建时 void environmentPrepared(ConfigurableEnvironment environment); // ApplicationContextInitializedEvent： void contextPrepared(ConfigurableApplicationContext context); // ApplicationPreparedEvent：ApplicationContext 已加载未刷新 void contextLoaded(ConfigurableApplicationContext context); // ApplicationStartedEvent void started(ConfigurableApplicationContext context); // ApplicationReadyEvent void running(ConfigurableApplicationContext context); // ApplicationFailedEvent：应用发送错误时 void failed(ConfigurableApplicationContext context, Throwable exception); } 监听器注册 系统监听器和系统初始化器的注册流程基本一致，即在 SpringApplication 的构造方法中通过 SpringFactoriesLoader 加载 ApplicationListener 的实现类，具体流程参照初始化器解析-springboot源码学习/#springfactoriesloader-介绍。\n监听器事件触发机制 系统事件触发机制 以 ApplicationStartingEvent 事件为例，最初是在 SpringApplication 的 run 方法中调用 starting 方法触发。\npublic ConfigurableApplicationContext run(String... args) { // ... SpringApplicationRunListeners listeners = getRunListeners(args); listeners.starting(); // ... return context; } SpringApplicationRunListeners 类是 SpringApplicationRunListener 的集合，对每一个 RunListener 调用 starting 方法。\nclass SpringApplicationRunListeners { private final List\u0026lt;SpringApplicationRunListener\u0026gt; listeners; // ... public void starting() { for (SpringApplicationRunListener listener : this.listeners) { listener.starting(); } } } SpringApplicationRunListener 最终通过 SimpleApplicationEventMulticaster 事件广播器将 ApplicationStartingEvent 事件广播给事件监听器。\npublic class EventPublishingRunListener implements SpringApplicationRunListener, Ordered { private final SpringApplication application; private final String[] args; private final SimpleApplicationEventMulticaster initialMulticaster; public EventPublishingRunListener(SpringApplication application, String[] args) { this.application = application; this.args = args; this.initialMulticaster = new SimpleApplicationEventMulticaster(); for (ApplicationListener\u0026lt;?\u0026gt; listener : application.getListeners()) { // 将所有监听器注册到广播器中 this.initialMulticaster.addApplicationListener(listener); } } // ... @Override public void starting() { this.initialMulticaster.multicastEvent(new ApplicationStartingEvent(this.application, this.args)); } } 改造监听器模式 参照 SpringApplicationRunListener 改造上述天气预报的监听器，引入 WeatherRunListener ，定义 snow 和 rain 方法。通过 Spring 管理广播器和监听器实现自动注入。\n@Component public class WeatherRunListener { @Resource private WeatherEventMulticaster eventMulticaster; public void snow(){ eventMulticaster.multicastEvent(new SnowEvent()); } public void rain(){ eventMulticaster.multicastEvent(new RainEvent()); } } @Component public class WeatherEventMulticaster extends AbstractEventMulter { // ... } @Component public class RainListener implements WeatherListener{ // ... } // 测试 @RunWith(SpringRunner.class) @SpringBootTest public class WeatherRunListenerTest { @Resource private WeatherRunListener weatherRunListener; @Test public void testSnow() { weatherRunListener.snow(); } @Test public void testRain() { weatherRunListener.rain(); } } multicastEvent 方法解析 上面已经分析到 ApplicationStartingEvent 事件最终是通过 SimpleApplicationEventMulticaster 的 multicastEvent 方法广播给监听器，multicastEvent 方法的主要逻辑是获取监听器，触发监听器的 onApplicationEvent 事件。\n@Override public void multicastEvent(ApplicationEvent event) { // 获取事件的 class 包装，调用重载方法 multicastEvent(event, resolveDefaultEventType(event)); } @Override public void multicastEvent(final ApplicationEvent event, @Nullable ResolvableType eventType) { ResolvableType type = (eventType != null ? eventType : resolveDefaultEventType(event)); // 未指定线程池，返回 null Executor executor = getTaskExecutor(); // getApplicationListeners 获取对当前事件感兴趣的监听器 for (ApplicationListener\u0026lt;?\u0026gt; listener : getApplicationListeners(event, type)) { if (executor != null) { executor.execute(() -\u0026gt; invokeListener(listener, event)); } else { // 调用事件监听器的 onApplicationEvent 方法：listener.onApplicationEvent(event); invokeListener(listener, event); } } } 通过 getApplicationListeners 方法获取对当前事件感兴趣的监听器。首先使用事件源 source 和事件类型 sourceType 构造缓存 cacheKey，避免重复查找，如果 retrieverCache 缓存中不存在，则调用 retrieveApplicationListeners 方法获取对当前事件感兴趣的监听器。\nprotected Collection\u0026lt;ApplicationListener\u0026lt;?\u0026gt;\u0026gt; getApplicationListeners( ApplicationEvent event, ResolvableType eventType) { Object source = event.getSource(); Class\u0026lt;?\u0026gt; sourceType = (source != null ? source.getClass() : null); ListenerCacheKey cacheKey = new ListenerCacheKey(eventType, sourceType); // Quick check for existing entry on ConcurrentHashMap... ListenerRetriever retriever = this.retrieverCache.get(cacheKey); if (retriever != null) { return retriever.getApplicationListeners(); } // ... return retrieveApplicationListeners(eventType, sourceType, null) } 重载的 retrieveApplicationListeners 方法才是真正获取监听器的方法。遍历所有 SpringApplication 初始化时加载的监听器（EventPublishingRunListener 构造方法中添加到 SimpleApplicationEventMulticaster 中），依次调用 supportsEvent 方法，将返回为 true 的监听器添加到目标集合中，最后对目标监听器排序返回。\nprivate Collection\u0026lt;ApplicationListener\u0026lt;?\u0026gt;\u0026gt; retrieveApplicationListeners( ResolvableType eventType, @Nullable Class\u0026lt;?\u0026gt; sourceType, @Nullable ListenerRetriever retriever) { List\u0026lt;ApplicationListener\u0026lt;?\u0026gt;\u0026gt; allListeners = new ArrayList\u0026lt;\u0026gt;(); Set\u0026lt;ApplicationListener\u0026lt;?\u0026gt;\u0026gt; listeners; Set\u0026lt;String\u0026gt; listenerBeans; synchronized (this.retrievalMutex) { // this.defaultRetriever.applicationListeners：EventPublishingRunListener 构造方法中调用 initialMulticaster.addApplicationListener(listener) 方法注册 listeners = new LinkedHashSet\u0026lt;\u0026gt;(this.defaultRetriever.applicationListeners); listenerBeans = new LinkedHashSet\u0026lt;\u0026gt;(this.defaultRetriever.applicationListenerBeans); } for (ApplicationListener\u0026lt;?\u0026gt; listener : listeners) { if (supportsEvent(listener, eventType, sourceType)) { if (retriever != null) { retriever.applicationListeners.add(listener); } allListeners.add(listener); } } if (!listenerBeans.isEmpty()) { // ... } AnnotationAwareOrderComparator.sort(allListeners); if (retriever != null \u0026amp;\u0026amp; retriever.applicationListenerBeans.isEmpty()) { retriever.applicationListeners.clear(); retriever.applicationListeners.addAll(allListeners); } return allListeners; } supportsEvent 确定给定监听器是否支持给定事件，首先将 listener 包装为 GenericApplicationListener 的实现类，然后调用 supportsEventType 方法和 supportsSourceType 方法确定给定监听器是否支持当前事件。\nprotected boolean supportsEvent( ApplicationListener\u0026lt;?\u0026gt; listener, ResolvableType eventType, @Nullable Class\u0026lt;?\u0026gt; sourceType) { GenericApplicationListener smartListener = (listener instanceof GenericApplicationListener ? (GenericApplicationListener) listener : new GenericApplicationListenerAdapter(listener)); return (smartListener.supportsEventType(eventType) \u0026amp;\u0026amp; smartListener.supportsSourceType(sourceType)); } GenericApplicationListener 和 SmartApplicationListener 都是 ApplicationListener 的子接口，增加了 supportsEventType 方法判断监听器是否支持给定的事件类型和 supportsSourceType 方法判断监听器是否支持给定的事件源类型。\n如果监听器不是实现自 SmartApplicationListener 或 GenericApplicationListener 则使用 GenericApplicationListenerAdapter 适配器包装，通过 resolveDeclaredEventType 方法解析出监听器支持的事件类型。\n自定义监听器 监听器定义方式 方式一、实现 ApplicationListener 接口，通过泛型参数指定监听的事件。\n@Order(1) public class FirstListener implements ApplicationListener\u0026lt;ApplicationStartedEvent\u0026gt; { @Override public void onApplicationEvent(ApplicationStartedEvent event) { System.out.println(\u0026#34;hello first\u0026#34;); } } 方式二、实现 SmartApplicationListener 接口，通过 supportsEventType 方法指定监听的事件。\n@Order(4) public class FourthListener implements SmartApplicationListener { @Override public boolean supportsEventType(Class\u0026lt;? extends ApplicationEvent\u0026gt; eventType) { return ApplicationStartedEvent.class.isAssignableFrom(eventType) || ApplicationPreparedEvent.class.isAssignableFrom(eventType); } @Override public void onApplicationEvent(ApplicationEvent event) { System.out.println(\u0026#34;hello fourth\u0026#34;); } } 监听器注册方式 方式一、通过 META-INF/spring.factories 文件注册监听器。\norg.springframework.context.ApplicationListener=store.xianglin.sb2.listener.FirstListener,store.xianglin.sb2.listener.FourthListener 方式二、通过调用 SpringApplication.addListeners() 方法注册 SecondListener。\n@SpringBootApplication @MapperScan(\u0026#34;store.xianglin.sb2.mapper\u0026#34;) public class SpringBoot2Application { public static void main(String[] args) { // SpringApplication.run(SpringBoot2Application.class, args); var springApplication = new SpringApplication(SpringBoot2Application.class); springApplication.addInitializers(new SecondInitializer()); springApplication.addListeners(new SecondListener()); springApplication.run(args); } } 方式三、通过配置文件注册 ThirdListener，委托给 DelegatingApplicationListener 监听器处理。\ncontext.listener.classes=store.xianglin.sb2.listener.ThirdListener ","permalink":"https://2d992714.xianglin2020-github-io.pages.dev/posts/2023/listener-source-code-learning/","summary":"SpringBoot 监听器模式介绍、系统监听器介绍、监听事件触发机制和自定义监听器。","title":"监听器解析 SpringBoot源码学习"},{"content":"系统初始化器介绍 实现 ApplicationContextInitializer 接口的类称为系统初始化器，在容器刷新前（refresh()）执行其回调方法（initialize()），用于向 SpringBoot 容器中注册属性（通常用于 WEB 环境中）。\n自定义初始化器注入环境变量 新建三个类 FirstInitializer、SecondInitializer 和 ThirdInitializer 实现 ApplicationContextInitializer，使用 @Order 注解标记其顺序，分别为 1、2 和 3， 在 initialize 方法中为环境添加自定义属性。\n@Order(1) public class FirstInitializer implements ApplicationContextInitializer\u0026lt;ConfigurableApplicationContext\u0026gt; { @Override public void initialize(ConfigurableApplicationContext applicationContext) { var environment = applicationContext.getEnvironment(); var map = Map.\u0026lt;String, Object\u0026gt;of(\u0026#34;key1\u0026#34;, \u0026#34;value1\u0026#34;); var mapPropertySource = new MapPropertySource(\u0026#34;firstInitializer\u0026#34;, map); environment.getPropertySources().addLast(mapPropertySource); System.out.println(\u0026#34;run firstInitializer\u0026#34;); } } 注册方式一、在 resources 文件夹中新建文件 META-INF/spring.factories，注册 FirstInitializer。\norg.springframework.context.ApplicationContextInitializer=store.xianglin.sb2.Initializer.FirstInitializer 注册方式二、通过调用 SpringApplication.addInitializers() 方法注册 SecondInitializer。\n@SpringBootApplication @MapperScan(\u0026#34;store.xianglin.sb2.mapper\u0026#34;) public class SpringBoot2Application { public static void main(String[] args) { // SpringApplication.run(SpringBoot2Application.class, args); var springApplication = new SpringApplication(SpringBoot2Application.class); springApplication.addInitializers(new SecondInitializer()); springApplication.run(args); } } 注册方式三、通过配置文件注册 ThirdInitializer。\ncontext.initializer.classes=store.xianglin.sb2.Initializer.ThirdInitializer SpringFactoriesLoader 介绍 org.springframework.core.io.support.SpringFactoriesLoader 类介绍如下。\n它是框架内部使用的通用工厂加载机制 它从 classpath 下的多个 jar 包中读取 META-INF/spring.factories 文件，并加载和初始化文件中定义的类 spring.factories 文件内容必须是 Properties 格式（key-value） key 是接口或者抽象类的全限定名，value 是实现类，多个实现类用 , 分隔 loadFactories 流程 以 ApplicationContextInitializer 加载为例，loadFactories 流程见 SpringApplication 中 org.springframework.boot.SpringApplication#getSpringFactoriesInstances(java.lang.Class\u0026lt;T\u0026gt;, java.lang.Class\u0026lt;?\u0026gt;[], java.lang.Object...) 方法。\nprivate \u0026lt;T\u0026gt; Collection\u0026lt;T\u0026gt; getSpringFactoriesInstances(Class\u0026lt;T\u0026gt; type, Class\u0026lt;?\u0026gt;[] parameterTypes, Object... args) { ClassLoader classLoader = getClassLoader(); // Use names and ensure unique to protect against duplicates // 从类路径下加载所有 ApplicationContextInitializer 的实现类 Set\u0026lt;String\u0026gt; names = new LinkedHashSet\u0026lt;\u0026gt;(SpringFactoriesLoader.loadFactoryNames(type, classLoader)); // 实例化所有 ApplicationContextInitializer 对象 List\u0026lt;T\u0026gt; instances = createSpringFactoriesInstances(type, parameterTypes, classLoader, args, names); // 按 Order 接口或注解定义的顺序排序 AnnotationAwareOrderComparator.sort(instances); return instances; } 在 loadFactoryNames 方法中获取 ApplicationContextInitializer 实现类分为两步，首先加载所有 META-INF/spring.factories 文件的内容，以 factoryClassName 分组保存，然后获取 ApplicationContextInitializer 对应的实现类。\npublic static List\u0026lt;String\u0026gt; loadFactoryNames(Class\u0026lt;?\u0026gt; factoryClass, @Nullable ClassLoader classLoader) { String factoryClassName = factoryClass.getName(); return loadSpringFactories(classLoader) .getOrDefault(factoryClassName, Collections.emptyList()); } private static Map\u0026lt;String, List\u0026lt;String\u0026gt;\u0026gt; loadSpringFactories(@Nullable ClassLoader classLoader) { // 缓存已加载的结果 MultiValueMap\u0026lt;String, String\u0026gt; result = cache.get(classLoader); if (result != null) { return result; } try { Enumeration\u0026lt;URL\u0026gt; urls = (classLoader != null ? classLoader.getResources(FACTORIES_RESOURCE_LOCATION) : ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION)); result = new LinkedMultiValueMap\u0026lt;\u0026gt;(); while (urls.hasMoreElements()) { URL url = urls.nextElement(); // 遍历找到的 spring.factories 文件，读取并转成 Properties 对象 UrlResource resource = new UrlResource(url); Properties properties = PropertiesLoaderUtils.loadProperties(resource); // 遍历 Properties 中的属性，将 value 按逗号拆分，并添加到 result 中 for (Map.Entry\u0026lt;?, ?\u0026gt; entry : properties.entrySet()) { String factoryClassName = ((String) entry.getKey()).trim(); for (String factoryName : StringUtils.commaDelimitedListToStringArray((String) entry.getValue())) { result.add(factoryClassName, factoryName.trim()); } } } // 缓存加载结果 cache.put(classLoader, result); return result; } catch (IOException ex) { throw new IllegalArgumentException(\u0026#34;Unable to load factories from location [\u0026#34; + FACTORIES_RESOURCE_LOCATION + \u0026#34;]\u0026#34;, ex); } } 获取到 ApplicationContextInitializer 的实现类后，通过全限定名加载类，然后通过反射创建对象。\nprivate \u0026lt;T\u0026gt; List\u0026lt;T\u0026gt; createSpringFactoriesInstances(Class\u0026lt;T\u0026gt; type, Class\u0026lt;?\u0026gt;[] parameterTypes, ClassLoader classLoader, Object[] args, Set\u0026lt;String\u0026gt; names) { List\u0026lt;T\u0026gt; instances = new ArrayList\u0026lt;\u0026gt;(names.size()); for (String name : names) { try { // 通过类名加载类 Class\u0026lt;?\u0026gt; instanceClass = ClassUtils.forName(name, classLoader); // 确保 spring.factories 中定义的类是 ApplicationContextInitializer 的实现类 Assert.isAssignable(type, instanceClass); // 获取默认构造器，创建实例对象 Constructor\u0026lt;?\u0026gt; constructor = instanceClass.getDeclaredConstructor(parameterTypes); T instance = (T) BeanUtils.instantiateClass(constructor, args); instances.add(instance); } catch (Throwable ex) { throw new IllegalArgumentException(\u0026#34;Cannot instantiate \u0026#34; + type + \u0026#34; : \u0026#34; + name, ex); } } return instances; } 系统初始化器原理 系统初始化器有三种注册方法，通过 spring.factories 文件注册和编码调用 addInitializers() 方法注册都会将实现类对象添加到 private List\u0026lt;ApplicationContextInitializer\u0026lt;?\u0026gt;\u0026gt; initializers; 中，其调用是在 org.springframework.boot.SpringApplication#applyInitializers 方法完成。\nprotected void applyInitializers(ConfigurableApplicationContext context) { // 遍历所有注册的初始化器 for (ApplicationContextInitializer initializer : getInitializers()) { Class\u0026lt;?\u0026gt; requiredType = GenericTypeResolver.resolveTypeArgument(initializer.getClass(), ApplicationContextInitializer.class); Assert.isInstanceOf(requiredType, context, \u0026#34;Unable to call initializer.\u0026#34;); // 调用初始化器的 initialize 方法，前面实现的注入环境变量在这里执行 initializer.initialize(context); } } 从上图可以发现，通过配置文件注册的 ThirdInitializer 并不在 initializers 中，其加载调用是通过 DelegatingApplicationContextInitializer 类完成的。\npublic class DelegatingApplicationContextInitializer implements ApplicationContextInitializer\u0026lt;ConfigurableApplicationContext\u0026gt;, Ordered { // 配置文件中的属性名 private static final String PROPERTY_NAME = \u0026#34;context.initializer.classes\u0026#34;; private int order = 0; @Override public void initialize(ConfigurableApplicationContext context) { ConfigurableEnvironment environment = context.getEnvironment(); // 获取配置文件中注册的初始化器 List\u0026lt;Class\u0026lt;?\u0026gt;\u0026gt; initializerClasses = getInitializerClasses(environment); if (!initializerClasses.isEmpty()) { // 执行初始化器逻辑，由此可发现：配置文件注册的初始化器执行顺序取决于 DelegatingApplicationContextInitializer 的顺序 applyInitializerClasses(context, initializerClasses); } } } 而 DelegatingApplicationContextInitializer 是在 spring-boot-x.x.x.jar!/META-INF/spring.factories 中注册的。\n","permalink":"https://2d992714.xianglin2020-github-io.pages.dev/posts/2023/initializer-source-code-learning/","summary":"SpringBoot 系统初始化器解析。","title":"初始化器解析 SpringBoot源码学习"},{"content":"环境准备 Java 8+：https://jdk.java.net/archive/\nMySQL 5.7+：https://www.mysql.com/cn/downloads/\nMaven 3.3+：https://maven.apache.org/download.cgi\nIDEA：https://www.jetbrains.com.cn/idea/\n实例代码：https://github.com/xianglin2020/springboot2-source-study\nSpringBoot 启动介绍 一行代码完成启动。\n@SpringBootApplication @MapperScan(\u0026#34;store.xianglin.sb2.mapper\u0026#34;) public class SpringBoot2Application { public static void main(String[] args) { SpringApplication.run(SpringBoot2Application.class, args); } } 框架初始化 new SpringApplication 主要步骤见：org.springframework.boot.SpringApplication#SpringApplication(org.springframework.core.io.ResourceLoader, java.lang.Class\u0026lt;?\u0026gt;...)。\npublic SpringApplication(ResourceLoader resourceLoader, Class\u0026lt;?\u0026gt;... primarySources) { // 配置资源加载器 this.resourceLoader = resourceLoader; Assert.notNull(primarySources, \u0026#34;PrimarySources must not be null\u0026#34;); // 配置 primarySources this.primarySources = new LinkedHashSet\u0026lt;\u0026gt;(Arrays.asList(primarySources)); // 应用环境检测 this.webApplicationType = WebApplicationType.deduceFromClasspath(); // 配置系统初始化器 setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class)); // 配置应用监听器 setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class)); // 配置 main 方法所在类 this.mainApplicationClass = deduceMainApplicationClass(); } 框架启动 主要步骤见：org.springframework.boot.SpringApplication#run(java.lang.String...)。\npublic ConfigurableApplicationContext run(String... args) { // 计时器开始计时 StopWatch stopWatch = new StopWatch(); stopWatch.start(); ConfigurableApplicationContext context = null; Collection\u0026lt;SpringBootExceptionReporter\u0026gt; exceptionReporters = new ArrayList\u0026lt;\u0026gt;(); // Headless 模式赋值 configureHeadlessProperty(); // 发送 starting 事件 SpringApplicationRunListeners listeners = getRunListeners(args); listeners.starting(); try { ApplicationArguments applicationArguments = new DefaultApplicationArguments(args); // 配置环境模块，发送 environmentPrepared 事件 ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments); configureIgnoreBeanInfo(environment); // 打印 Banner Banner printedBanner = printBanner(environment); // 创建应用上下文 context = createApplicationContext(); // 初始化失败分析器 exceptionReporters = getSpringFactoriesInstances(SpringBootExceptionReporter.class, new Class[] { ConfigurableApplicationContext.class }, context); // 关联 SpringBoot 组件与应用上下文 // 1、发送 contextPrepared 事件 // 2、加载 source 到 context // 3、发送 contextLoaded 事件 prepareContext(context, environment, listeners, applicationArguments, printedBanner); // 刷新上下文 refreshContext(context); afterRefresh(context, applicationArguments); // 计时器停止 stopWatch.stop(); if (this.logStartupInfo) { new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch); } // 发送 started 事件 listeners.started(context); // 调用框架启动扩展类：实现ApplicationRunner和CommandLineRunner callRunners(context, applicationArguments); } catch (Throwable ex) { handleRunFailure(context, ex, exceptionReporters, listeners); throw new IllegalStateException(ex); } try { // 发送 running 事件 listeners.running(context); } catch (Throwable ex) { handleRunFailure(context, ex, exceptionReporters, null); throw new IllegalStateException(ex); } return context; } 上述过程中的事件定义在 org.springframework.boot.SpringApplicationRunListener 中。\n无头模式：Selecting an AWT mode\n自动化装配 收集配置文件中的配置工厂类 加载组件工厂 注册组件内定义的 bean ","permalink":"https://2d992714.xianglin2020-github-io.pages.dev/posts/2023/basics-source-code-learning/","summary":"SpringBoot 项目环境准备、工程搭建、框架启动流程。","title":"全局流程解析 SpringBoot源码学习"},{"content":"介绍 Spring Cloud Alibaba 官网：https://sca.aliyun.com/zh-cn/\nSpring Cloud Alibaba 在 SpringCloud 中的介绍：https://spring.io/projects/spring-cloud-alibaba\nSpring Cloud Alibaba 文档：https://spring-cloud-alibaba-group.github.io/github-pages/2022/zh-cn/index.html\n案例代码 sca-study\nNacos Discovery https://nacos.io/zh-cn/docs/v2/quickstart/quick-start.html\n安装 Nacos 从 https://github.com/alibaba/nacos/releases 下载最新稳定版，解压后启动即可。\nwget https://github.com/alibaba/nacos/releases/download/2.2.3/nacos-server-2.2.3.zip unzip nacos-server-2.2.3.zip cd nacos bash bin/startup.sh -m standalone 启动完成后可打开控制台 http://192.168.31.114:8848/nacos 。\n服务注册 以 sca-service 为例，在项目的 build.gradle.kts 中添加 nacos-discovery 依赖。\nimplementation(\u0026#34;com.alibaba.cloud:spring-cloud-starter-alibaba-nacos-discovery\u0026#34;) 添加 Nacos Discovery 的基本配置。\nspring: application: name: sca-service cloud: nacos: discovery: server-addr: 192.168.31.114 # -Dport=9000 server: port: ${port:9000} 在启动类上增加 @EnableDiscoveryClient 注解。\n@EnableDiscoveryClient @SpringBootApplication public class ScaServiceApplication { public static void main(String[] args) { SpringApplication.run(ScaServiceApplication.class); } } 分别以 9000 和 9001 端口启动，可在控制台查看到已注册的服务。\n负载均衡 通过 sca-web 调用 sca-service，在 sca-web 项目的 build.gradle.kts 中添加如下依赖。\nimplementation(\u0026#34;com.alibaba.cloud:spring-cloud-starter-alibaba-nacos-discovery\u0026#34;) implementation(\u0026#34;org.springframework.cloud:spring-cloud-starter-loadbalancer\u0026#34;) implementation(\u0026#34;org.springframework.cloud:spring-cloud-starter-openfeign\u0026#34;) 添加 Nacos Discovery 的基本配置。\nspring: application: name: sca-web cloud: nacos: discovery: server-addr: 192.168.31.114 server: port: 9901 在启动类上添加对应注解，启动即可。\n@EnableFeignClients @SpringBootApplication @EnableDiscoveryClient public class ScaWebApplication { public static void main(String[] args) { SpringApplication.run(ScaWebApplication.class); } } 多次调用 sca-web 接口查看返回结果，发现两个节点轮训被访问。\n权重路由 通过 Nacos 控制台调整实例权重为 3:1。\n在 sca-web 配置文件中添加配置开启权重路由。\nspring: application: name: sca-web cloud: nacos: discovery: server-addr: 192.168.31.114 loadbalancer: nacos: enabled: true 重启服务后多次调用 sca-web 接口查看返回结果，发现权重路由已生效。\nNacos Config 安装 Nacos 安装步骤同上。\n动态配置管理 以 sca-web 为例，在项目的 build.gradle.kts 中添加 nacos-config 依赖。\nimplementation(\u0026#34;com.alibaba.cloud:spring-cloud-starter-alibaba-nacos-config\u0026#34;) 添加 Nacos Config 的基本配置。\nspring: application: name: sca-web cloud: nacos: config: server-addr: 192.168.31.114 discovery: server-addr: 192.168.31.114 loadbalancer: nacos: enabled: true config: import: - nacos:sca-web-config.properties 通过 Nacos 控制台创建 Data ID 为 sca-web-config.properties 的配置。\n添加一个 Controller 验证。\n@RefreshScope @RestController public class NacosConfigController { @Value(\u0026#34;${sca.web.config.key1}\u0026#34;) private String key1; @Value(\u0026#34;${sca.web.config.key2}\u0026#34;) private String key2; @GetMapping(\u0026#34;/config\u0026#34;) public Map\u0026lt;String, String\u0026gt; config() { return Map.of(\u0026#34;key1\u0026#34;, key1, \u0026#34;key2\u0026#34;, key2); } } Nacos Config 默认支持配置的动态更新，可以通过配置 spring.cloud.nacos.config.refresh-enabled=false 关闭动态刷新。\n自定义 namespace 通过 Nacos 控制台新建命名空间 dev-env。\n将配置 sca-web-config.properties 复制一份到 dev-env 下。\n修改 Nacos Config 的配置，增加命名空间配置。\nspring: application: name: sca-web cloud: nacos: config: server-addr: 192.168.31.114 namespace: dev-env 命名空间的常用场景之一是不同环境的配置的区分隔离，例如开发测试环境和生产环境的资源（如配置、服务）隔离等。\n配置获取方式 通过将配置信息配置为bean，支持配置变自动刷新。\n@Component @ConfigurationProperties(prefix = \u0026#34;sca.web.config\u0026#34;) public class ConfigBean { private String key1; private String key2; // getter and setter } 监听配置信息。\n@Component public class ConfigListenerExample { private final Logger logger = LoggerFactory.getLogger(ConfigListenerExample.class); @Resource private NacosConfigManager nacosConfigManager; String DATA_ID = \u0026#34;sca-web-config.properties\u0026#34;; String GROUP = \u0026#34;DEFAULT_GROUP\u0026#34;; @PostConstruct public void init() throws NacosException { nacosConfigManager.getConfigService().addListener(DATA_ID, GROUP, new Listener() { @Override public Executor getExecutor() { return Executors.newSingleThreadExecutor(); } @Override public void receiveConfigInfo(String configInfo) { logger.info(\u0026#34;[dataId]:[\u0026#34; + DATA_ID + \u0026#34;],Configuration changed to:\u0026#34; + configInfo); } }); } } 对接 nacos 接口，通过接口完成配置信息查询。\nprivate final ConfigService configService; public NacosConfigController(NacosConfigManager nacosConfigManager) { this.configService = nacosConfigManager.getConfigService(); } @GetMapping(\u0026#34;/configService\u0026#34;) public ApiRes\u0026lt;String\u0026gt; configService() throws NacosException { return ApiRes.ok(configService.getConfig(DATA_ID, GROUP, 2000)); } 通过 @Value 注解进行配置信息获取，注意需要增加 @RefreshScope 注解。\n在配置中心修改配置的内容时，会发布 Spring 中的 RefreshEvent 事件。 带有 @RefreshScope 和 @ConfigurationProperties 注解的类会自动刷新。\nNacos 集群模式 Nacos 集群部署 https://nacos.io/zh-cn/docs/cluster-mode-quick-start.html\n配置集群配置文件，创建配置文件 nacos/conf/acluster.conf，每行配置成 ip:port。\n192.168.31.231:8848 192.168.31.232:8848 192.168.31.233:8848 为 Nacos 集群创建数据库，并执行 nacos/conf/mysql-schema.sql 初始化 SQL 语句。\nCREATE DATABASE `nacos` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT ENCRYPTION = \u0026#39;N\u0026#39; 修改 nacos/conf/application.properties配置文件，配置 MySQL 连接信息。\n### If use MySQL as datasource: spring.sql.init.platform=mysql ### Count of DB: db.num=1 ### Connect URL of DB: db.url.0=jdbc:mysql://192.168.31.114:3306/nacos?characterEncoding=utf8\u0026amp;connectTimeout=1000\u0026amp;socketTimeout=3000\u0026amp;autoReconnect=true\u0026amp;useUnicode=true\u0026amp;useSSL=false\u0026amp;serverTimezone=UTC db.user.0=root db.password.0=950915 使用命令 bash startup.sh 启动 Nacos 服务。\n使用 Nacos 集群 仅需要修改 server-addr 指向多个服务即可。\nspring: application: name: sca-web cloud: nacos: config: server-addr: ${nacos.server-addr} namespace: dev-env discovery: server-addr: ${nacos.server-addr} config: import: - nacos:sca-web-config.properties nacos: server-addr: 192.168.31.231,192.168.31.232,192.168.31.233 Sentinel Sentinel 是面向分布式、多语言异构化服务架构的流量治理组件，主要以流量为切入点，从流量控制、流量路由、熔断降级、系统自适应保护等多个维度来帮助用户保障微服务的稳定性。\nSentinel 控制台 从 https://github.com/alibaba/Sentinel/releases 下载最新版 sentinel-dashboard，使用命令 java -jar sentinel-dashboard-1.8.6.jar 直接启动即可。\nnohup java -jar sentinel-dashboard-1.8.6.jar \u0026gt; /dev/null 2\u0026gt;\u0026amp;1 \u0026amp; 启动完成后可打开控制台 http://192.168.31.114:8080/#/login，输入默认密码 sentinel / sentinel。\n使用 Sentinel 以 sca-web 为例，在项目的 build.gradle.kts 中添加 sentinel 依赖。\nimplementation(\u0026#34;com.alibaba.cloud:spring-cloud-starter-alibaba-sentinel\u0026#34;) 添加控制台配置信息。\nspring: application: name: sca-web cloud: sentinel: transport: dashboard: 192.168.31.114:8080 port: 8719 这里的 spring.cloud.sentinel.transport.port 端口配置会在应用对应的机器上启动一个 Http Server，该 Server 会与 Sentinel 控制台做交互。比如 Sentinel 控制台添加了1个限流规则，会把规则数据 push 给这个 Http Server 接收，Http Server 再将规则注册到 Sentinel 中。\n启动 sca-web 服务，发送几次请求，然后打开 Sentinel 控制台可看到实时监控信息。\nSentinel 流控规则 https://sentinelguard.io/zh-cn/docs/flow-control.html\nQPS 类型限流。\n为 /sentinel/hello 添加流控规则，限制 QPS 阈值为 3，然后多次请求接口，可看到触发流控后的默认失败效果。\n并发线程数类型限流。\n为 /sentinel/hello 添加流控规则，限制并发线程数阈值为 1，然后多次请求接口。\n通过 ApacheBench 3 线程发送 10 个请求，可以发现有 5 个请求失败。\nab -n 10 -c 3 http://localhost:9901/sentinel/hello 熔断策略 慢调用比例 (SLOW_REQUEST_RATIO) 异常比例 (ERROR_RATIO) 异常数 (ERROR_COUNT) 以异常数 (ERROR_COUNT)为例，设置异常数超过 6 后自动进行熔断。\n随后使用 jmeter 测试，结果如下。\nSeata https://seata.io/zh-cn/index.html\nSeata 安装 从 https://github.com/seata/seata/releases 下载最新稳定版解压。\nwget https://github.com/seata/seata/releases/download/v1.7.1/seata-server-1.7.1.zip unzip seata-server-1.7.1.zip server 端存储模式使用 file，配置和注册中心使用 nacos，修改配置文件 seata/confapplication.yml。\nseata: # 配置中心使用 nacos config: type: nacos nacos: server-addr: 127.0.0.1:8848 data-id: seata.properties group: SEATA_GROUP # 配置中心使用 nacos registry: type: nacos nacos: application: seata-server server-addr: 127.0.0.1:8848 group: DEFAULT_GROUP cluster: default # 存储模式使用 file store: mode: file 在 nacos 中创建配置 seata.properties，导入 https://github.com/seata/seata/blob/1.5.0/script/config-center/config.txt 内容。\n启动 seata，启动完成后即可打开控制台 http://192.168.31.114:7091/#/login，输入默认密码 seata / seata。\nbash bin/seata-server.sh 同时可发现 seata-server 已注册到 nacos 中。\nAT 模式演示 参考 Seata Example。\n为复用配置，创建 seata-base 项目。\n在 build.gradle.kts 中引用公共依赖。\nplugins { id(\u0026#34;multi.sca.java-library-conventions\u0026#34;) } dependencies { api(\u0026#34;com.alibaba.cloud:spring-cloud-starter-alibaba-seata\u0026#34;) api(\u0026#34;com.alibaba.cloud:spring-cloud-starter-alibaba-nacos-config\u0026#34;) api(\u0026#34;org.springframework.boot:spring-boot-starter-jdbc\u0026#34;) api(\u0026#34;com.mysql:mysql-connector-j:8.1.0\u0026#34;) } 在 application-base.yml 中配置公共属性。\nspring: cloud: nacos: discovery: server-addr: 192.168.31.114:8848 config: server-addr: 192.168.31.114:8848 group: SEATA_GROUP config: import: - nacos:seata.properties?refresh=true main: allow-bean-definition-overriding: true datasource: name: storageDataSource driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://192.168.31.114:3306/sca?useSSL=false\u0026amp;serverTimezone=UTC username: root password: 950915 seata: enabled: true application-id: ${spring.application.name} # 和 seata.properties 中 service.vgroupMapping.default_tx_group=default 对应 tx-service-group: default_tx_group config: type: nacos nacos: server-addr: 192.168.31.114:8848 registry: type: nacos nacos: server-addr: 192.168.31.114:8848 其中 seata.properties 为 Seata 安装过程中创建的配置，sca 数据库表见 https://github.com/alibaba/spring-cloud-alibaba/blob/2022.x/spring-cloud-alibaba-examples/seata-example/all.sql。\n以 account-server 项目为例，引入依赖和配置。\n在 build.gradle.kts 中引入 seata-base 。\nplugins { id(\u0026#34;multi.sca.java-common-conventions\u0026#34;) } dependencies { implementation(project(\u0026#34;:seata:seata-base\u0026#34;)) } 在 application.yml 中引入 application-base.yml 中定义的公共属性。\nserver: port: 18084 spring: profiles: include: base application: name: seata-account-service 随后启动四个项目，可在控制台查看到已注册的服务。\n使用 http://localhost:18081/seata/feign 测试，当接口异常时，account_tbl 和 storage_tbl 表中数据不变，当接口正常时，三张表数据满足一致性。\n两个问题 seata-server 和业务服务注册的命名空间和分组要一致，不然会出现 no available service found in cluster 'default' 错误。\nseata-server 和业务侧跨网络时启动 seata-server 时需要指定 IP，即 -h 192.168.31.114，不然会出现 0101 can not connect to xxx 错误。\n","permalink":"https://2d992714.xianglin2020-github-io.pages.dev/posts/2023/springcloudalibaba-basics/","summary":"学习 Spring Cloud Alibaba 分布式应用开发一站式解决方案。","title":"Spring Cloud Alibaba 学习"},{"content":"Gradle 基础 官网：https://gradle.org/\n基础教程：https://docs.gradle.org/current/userguide/part1_gradle_init.html\n多项目构建：https://docs.gradle.org/current/userguide/intro_multi_project_builds.html\nGradle 插件：https://plugins.gradle.org/\n脚手架 SCA 脚手架：https://start.aliyun.com/\nSpringBoot 脚手架：https://start.spring.io/\n案例代码 multi-sca\n创建 SCA 多模块项目 SpringBoot 脚手架中没有 SpringCloudAlibaba 相关依赖，SCA 脚手架中项目构建方式不支持 Gradle-Kotlin，且两个脚手架生成的都是单模块项目，所以需要使用 gradle init 命令生成多模块项目，然后通过脚手架查看依赖，手动创建 SCA 项目。\n生成多模块项目 这里使用 Gradle 8.3 和 Java17。\n使用gradle init命令，按提示创建项目。\n将生成的项目用 IDEA 打开，先不执行导入，依次做如下调整：\n调整项目的 SDK，Gradle 使用的 JVM。\n删除 settings.gradle.kts 中的 plugins 元素。\n修改 gradle/wrapper/gradle-wrapper.properties 中的 distributionUrl 属性，将其指向本地已下载好的 Gradle 文件，可以不用重复下载，节约时间。\n# distributionUrl=https\\://services.gradle.org/distributions/gradle-8.3-bin.zip distributionUrl=file:///Users/xianglin/Downloads/gradle-8.3-all.zip 修改 buildSrc/build.gradle.kts、buildSrc/src/main/kotlin/multi.sca.java-common-conventions.gradle.kts 中的 repositories 元素，使用阿里云公共代理仓库。\nrepositories { mavenLocal() maven(\u0026#34;https://maven.aliyun.com/repository/public\u0026#34;) maven(\u0026#34;https://maven.aliyun.com/repository/gradle-plugin\u0026#34;) mavenCentral() } 然后打开右侧的 Gradle 插件，重新加载 Gradle 项目。\n可以将 app、list、utilities 三个子模块删除，并从 settings.gradle.kts 中删除 include 元素中的值。也可以删除多余的 buildSrc/src/main/kotlin/multi.sca.java-application-conventions.gradle.kts。\n引入 SpringBoot 依赖 打开 https://start.spring.io/ ，Project 选择 Gradle-Kotlin ，Language 选择 Java，Spring Boot 选择 3.0.12，Java 版本选择 17，点击 EXPLORE 查看代码。\n将脚手架生成的内容复制到 multi.sca.java-common-conventions.gradle.kts 中。\nplugins { // Apply the java Plugin to add support for Java. java id(\u0026#34;org.springframework.boot\u0026#34;) version \u0026#34;3.0.12\u0026#34; id(\u0026#34;io.spring.dependency-management\u0026#34;) version \u0026#34;1.1.3\u0026#34; } java { sourceCompatibility = JavaVersion.VERSION_17 } repositories { // Use Maven Central for resolving dependencies. mavenLocal() maven(\u0026#34;https://maven.aliyun.com/repository/public\u0026#34;) maven(\u0026#34;https://maven.aliyun.com/repository/gradle-plugin\u0026#34;) mavenCentral() } dependencies { // 在此引入的依赖在所有使用此插件的模块生效 implementation(\u0026#34;org.springframework.boot:spring-boot-starter\u0026#34;) testImplementation(\u0026#34;org.springframework.boot:spring-boot-starter-test\u0026#34;) // Use JUnit Jupiter for testing. testImplementation(\u0026#34;org.junit.jupiter:junit-jupiter:5.9.3\u0026#34;) testRuntimeOnly(\u0026#34;org.junit.platform:junit-platform-launcher\u0026#34;) } tasks.withType\u0026lt;Test\u0026gt; { useJUnitPlatform() } 重新加载 Gradle 项目，会发现如下错误，解决方法可以查看 How to build import spring plugins。\n打开 https://plugins.gradle.org/ 查找 SpringBoot 插件对应的依赖，将其添加到 buildSrc/build.gradle.kts 的 dependencies 元素中，移除 multi.sca.java-common-conventions.gradle.kts 中插件的版本。\n// buildSrc/build.gradle.kts dependencies { implementation(\u0026#34;org.springframework.boot:spring-boot-gradle-plugin:3.0.12\u0026#34;) implementation(\u0026#34;io.spring.gradle:dependency-management-plugin:1.1.3\u0026#34;) } // multi.sca.java-common-conventions.gradle.kts plugins { id(\u0026#34;org.springframework.boot\u0026#34;) id(\u0026#34;io.spring.dependency-management\u0026#34;) } 重新加载 Gradle 项目，没有错误。\n参照 gradle init 生成的 app 子模块结构，创建 spring-boot-app 子模块，依次修改 spring-boot-app/build.gradle.kts 和 settings.gradle.kts，重新加载 Gradle 项目即可。\n// build.gradle.kts plugins { id(\u0026#34;multi.sca.java-common-conventions\u0026#34;) } dependencies { // 因为在 multi.sca.java-common-conventions 中已经引入了相关依赖 } // settings.gradle.kts rootProject.name = \u0026#34;multi-sca\u0026#34; include(\u0026#34;spring-boot-app\u0026#34;) 引入 SCA 依赖 创建三个子模块 sca-web 、sca-service、sca-api，分别作为WEB服务、业务服务和接口依赖项目。\n在 sca-api 中创建接口 ScaControllerApi，定义一个方法，sca-web 、sca-service 通过将 sca-api 添加为依赖使用此接口。\npublic interface ScaControllerApi { @GetMapping(\u0026#34;/hello/{message}\u0026#34;) ApiRes\u0026lt;String\u0026gt; hello(@PathVariable String message); } 打开 https://start.aliyun.com/ 组件与示例选择 Nacos Service Discovery，点击浏览代码，打开 https://start.spring.io/ Dependencies 选择 OpenFeign ，点击 EXPLORE 浏览代码。\n结合两个脚手架生成的内容，将其添加到项目中（仅列出新增的内容，需要将 groovy DSL 转为 Kotlin DSL）。\n// multi.sca.java-common-conventions.gradle.kts repositories { maven(\u0026#34;https://repo.spring.io/milestone\u0026#34;) } extra[\u0026#34;springCloudAlibabaVersion\u0026#34;]=\u0026#34;2022.0.0.0-RC2\u0026#34; extra[\u0026#34;springCloudVersion\u0026#34;]=\u0026#34;2022.0.0-RC2\u0026#34; dependencies { // nacos-discovery 在所有模块生效 implementation(\u0026#34;org.springframework.boot:spring-boot-starter-web\u0026#34;) implementation(\u0026#34;com.alibaba.cloud:spring-cloud-starter-alibaba-nacos-discovery\u0026#34;) } dependencyManagement{ imports{ mavenBom(\u0026#34;com.alibaba.cloud:spring-cloud-alibaba-dependencies:${property(\u0026#34;springCloudAlibabaVersion\u0026#34;)}\u0026#34;) mavenBom(\u0026#34;org.springframework.cloud:spring-cloud-dependencies:${property(\u0026#34;springCloudVersion\u0026#34;)}\u0026#34;) } } // sca-web/build.gradle.kts plugins { id(\u0026#34;multi.sca.java-common-conventions\u0026#34;) } dependencies { implementation(\u0026#34;org.springframework.cloud:spring-cloud-starter-loadbalancer\u0026#34;) implementation(\u0026#34;org.springframework.cloud:spring-cloud-starter-openfeign\u0026#34;) implementation(project(\u0026#34;:sca-api\u0026#34;)) } // sca-service/build.gradle.kts plugins { id(\u0026#34;multi.sca.java-common-conventions\u0026#34;) } dependencies { implementation(project(\u0026#34;:sca-api\u0026#34;)) } 重新加载 Gradle 项目，没有错误。\n依次实现 sca-web 和 sca-service，将服务注册到 nacos 中，使用 OpenFeign 实现服务间调用。\n// sca-service/src/main/resources/application.yml spring: application: name: sca-service cloud: nacos: discovery: server-addr: 192.168.31.114 server: port: 9900 // sca-web/src/main/resources/application.yml spring: application: name: sca-web cloud: nacos: discovery: server-addr: 192.168.31.114 server: port: 9901 // sca-service/src/main/java/sca/service/ScaController.java @RestController public class ScaController implements ScaControllerApi { @Override public ApiRes\u0026lt;String\u0026gt; hello(String message) { return ApiRes.ok(\u0026#34;sca-service: \u0026#34; + message); } } // sca-service/src/main/java/sca/service/ScaServiceApplication.java @EnableDiscoveryClient @SpringBootApplication public class ScaServiceApplication { public static void main(String[] args) { SpringApplication.run(ScaServiceApplication.class); } } // sca-web/src/main/java/sca/web/ScaService.java @FeignClient(name = \u0026#34;sca-service\u0026#34;) public interface ScaService extends ScaControllerApi { } // sca-web/src/main/java/sca/web/HelloController.java @RestController public class HelloController { private final ScaService scaService; public HelloController(ScaService scaService) { this.scaService = scaService; } @GetMapping(\u0026#34;/hello/{message}\u0026#34;) ApiRes\u0026lt;Map\u0026lt;String, String\u0026gt;\u0026gt; hello(@PathVariable String message) { var res = scaService.hello(message); return ApiRes.ok(Map.of(\u0026#34;from\u0026#34;, \u0026#34;sca-service\u0026#34;, \u0026#34;result\u0026#34;, res.getData())); } } // sca-web/src/main/java/sca/web/ScaWebApplication.java @EnableFeignClients @SpringBootApplication @EnableDiscoveryClient(autoRegister = false) public class ScaWebApplication { public static void main(String[] args) { SpringApplication.run(ScaWebApplication.class); } } 启动 sca-service 和 sca-web 两个项目，从 Nacos 的服务列表中可以看到 sca-service 服务。\n使用 HTTP 客户端测试，服务调用正常。\n构建项目 单独构建 sca-web 或者 sca-service 模块正常，可以生成可执行文件。\n如果直接使用项目顶级的构建会出现如下错误，解决方法可以查看 4.3. Packaging Executable and Plain Archives。\n在 sca-api/build.gradle.kts 中添加如下内容即可，此时构建跳过了 bootJar 任务。\ntasks.named\u0026lt;BootJar\u0026gt;(\u0026#34;bootJar\u0026#34;) { enabled = false } ","permalink":"https://2d992714.xianglin2020-github-io.pages.dev/posts/2023/gradle-kotlin-build-springcloudalibaba-multi-module/","summary":"使用 Gradle Kotlin 构建 SpringCloudAlibaba 多模块项目","title":"Gradle+Kotlin构建SpringCloudAlibaba多模块项目"},{"content":"计算机网络概述 发展简史 互联网发展历史 单个网络 ARPANET：交换机——计算机 三级结构互联网：主干网——地区网——校园网 多层次 ISP （网络服务提供商）互联网：主干 ISP——地区 ISP——公司/校园/家庭 中国互联网发展简史 1980 年开始互联网实验 1989 年第一个公共网络建立运行 1994 年接入国际互联网 层次结构 层次结构设计的基本原则 各层之间是相互独立的 每一层要有足够的灵活性 各层之间完全解耦 OSI 七层模型 开放式系统互联模型（英语：Open System Interconnection Model，缩写：OSI；简称为OSI模型）是一种概念模型，由国际标准化组织提出，一个试图使各种计算机在世界范围内互连为网络的标准框架。\n应用层：为计算机用户提供接口和服务 表示层：数据处理（编码解码、加密解密等） 会话层：管理（建立、维护、重连）通讯会话 传输层：管理端到端的通信连接 网络层：数据路由（决定数据在网络中的路径） 数据链路层：管理相邻节点之间的数据通信 物理层：数据通讯的光电物理特性 TCP/IP 四层模型 互联网协议套件（英语：Internet Protocol Suite，缩写IPS）是网络通信模型，以及整个网络传输协议家族，为网际网络的基础通信架构。它常通称为TCP/IP协议族（英语：TCP/IP Protocol Suite，或TCP/IP Protocols），简称TCP/IP。\n应用层：HTTP、FTP、SMTP、POP3 传输层：TCP、UDP 网络层：IP、ICMP 网络接口层：Ethernet、ARP、RARP 性能指标 速率 网络常用速率单位为 Mbps，bps=bit/s。\n时延 总时延=发送时延+排队时延+传播时延+处理时延。 $$ 发送时延=\\frac{数据长度(bit)}{发送速率(bit/s)} $$\n$$ 传播时延=\\frac{传输路径距离}{传播速率(bit/s)} $$\nRTT RTT（Route-Trip Time）是评估网络质量的一项重要指标。表示数据报文在端到端通信中来回一次的时间。通常使用ping命令查看 RTT。\n物理层 物理层的作用 为数据端设备提供传送数据通路。 传输比特流数据。 物理层设备 双绞线 同轴电缆 光纤 信道 信道是往一个方向传送信息的媒体，一条通信电路包含一个接收信道和一个发送信道。\n单工信道：只能一个方向通信，有线电视、无线电收音机。 半双工信道：双方都可以发送和接受信息，但不能同时发送和同时接收。 全双工信道：双方都可以同时发送和接受信息。 分用——复用 频分复用 时分复用 波分复用 码分复用 数据链路层 主要功能 封装成帧\n“帧”是数据链路层数据的基本单位，发送端在网络层的一段数据前后添加特定标记形成“帧”，接收端根据前后特定标记识别出“帧”。\n透明传输\n差错检测\n物理层只管传输比特流，无法控制是否出错，数据链路层负责“差错检测”。主要有奇偶校验码和循环冗余校验码 CRC 两种方式。\n奇偶校验码 奇偶校验位（英语：Parity bit）或校验比特（check bit）是一个表示给定位数的二进制数中1的个数是奇数还是偶数的二进制数。奇偶校验位是最简单的错误检测码。\n如果传输过程中包括校验位在内的奇数个数据位发生改变，那么奇偶校验位将出错表示传输过程有错误发生。因此，奇偶校验位是一种错误检测码，但是由于没有办法确定哪一位出错，所以它不能进行错误校正。发生错误时必须扔掉全部的数据，然后从头开始传输数据。\n循环冗余校验码 CRC 循环冗余校验（英语：Cyclic redundancy check，通称“CRC”）是一种根据网络数据包或电脑文件等数据产生简短固定位数校验码的一种散列函数，主要用来检测或校验数据传输或者保存后可能出现的错误。生成的数字在传输或者存储之前计算出来并且附加到数据后面，然后接收方进行检验确定数据是否发生变化。\n计算过程：\n选定一个用于校验的多项式G(x)，并在数据尾部添加 r 个 0； 将添加 r 个 0 后的数据，使用模 2 除法除以多项式的位串； 得到的余数填充在原数据 r 个 0 的位置得到可校验的位串。 CRC-1 多项式（x+1）也称为奇偶校验位。\nMTU 最大传输单元（英语：Maximum Transmission Unit，缩写MTU）是指数据链路层上面所能通过的最大数据包大小（以字节为单位）。最大传输单元这个参数通常与通信接口有关（网络卡、串口等）。\n在因特网协议中，一条因特网传输路径的“路径最大传输单元”被定义为从源地址到目的地址所经过“路径”上的所有IP的最大传输单元的最小值。或者从另外一个角度来看，就是无需进一步分片就能穿过这条“路径”的最大传输单元的最大值。\nMAC 地址 MAC地址（英语：Media Access Control Address），直译为媒体访问控制地址，也称为局域网地址（LAN Address），以太网地址（Ethernet Address）或物理地址（Physical Address），它是一个用来确认网络设备位置的地址。在OSI模型中，第三层网络层负责IP地址，第二层数据链接层则负责MAC地址。MAC地址用于在网络中唯一标识一个网卡，一台设备若有一或多个网卡，则每个网卡都需要并会有一个唯一的MAC地址。MAC地址共48位（6个字节），以十六进制表示。\n以太网 以太网是一种广泛使用的局域网技术，应用于数据链路层的协议，使用以太网可以完成相邻设备的数据帧传输。\n网络层 网络层 IP 协议相关 IP 协议 网际协议（英语：Internet Protocol，缩写：IP），又称互联网协议，是互联网协议包中的网络层通信协议，用于跨网络边界分组交换。它的路由功能实现了互联互通，并从本质上建立了互联网。\nIP地址（英语：IP Address，全称Internet Protocol Address），又译为网际协议地址、互联网协议地址。是网际协议中用于标识发送或接收数据报的设备的一串数字。当设备连接网络，设备将被分配一个IP地址，用作标识。通过IP地址，设备间可以互相通讯，如果没有IP地址，我们将无法知道哪个设备是发送方，无法知道哪个是接收方。IP地址有两个主要功能：标识设备或网络和网络寻址。\n常见的IP地址分为IPv4与IPv6两大类，IP地址由一串数字组成。IPv4为32位长，通常书写时以四组十进制数字组成，并以点分隔，如：172.16.254.1 ；IPv6为128位长，通常书写时以八组十六进制数字组成，以冒号分割，如：2001:db8:0: 1234:0:567:8:1。\nIPv4 报文结构：\n版本：指 IP 协议的版本，当前主流版本是 4，即 IPv4，也有 IPv6。\n首部长度：表示 IP 首部长度，占 4 位最大数值为 15，单位是“32 位字（4 个字节）”，即 IP 首部最大长度为 60 字节。\n总长度：占 16 位，最大数值为 65535，表示 IP 数据报总长度（IP 首部+IP 数据）。\nTTL：占 8 位，表明 IP 数据报文在网络中的寿命，每经过一个设备，TTL 减 1，当 TTL=0 时，网络设备丢弃此报文。 协议：占 8 位，表明 IP 数据所携带的具体数据是什么协议。\n首部校验和：占 16 位，校验 IP 首部是否出错。\n分类的 IP 地址 将 IP 地址划分为若干个固定类，每一类地址都有两个固定长度的字段组成，分别为网络号和主机号。\n其中特殊的主机号和网络号有：\n主机号全 0 表示当前网络段，不可分配为特定主机。 主机号全 1 为广播地址表示该网络上的所有主机。 A 类地址网络号字段为全0 的 IP 地址是个保留地址，意思是“本网络”。 A 类地址网络号为127(即 01111111 )保留作为本地软件环回测试(loopback test)本主机的进程之间的通信之用。 B 类地址实际上 128.0.0.0 是不指派的。 C 类网络地址 192.0.0.0 也是不指派的。 最小网络号 最大网络号 子网数量 最小主机号 最大主机号 主机数量 A 1 126 2^7-2 0.0.1 255.255.254 2^24-2 B 128.1 191.255 2^14-1 0.1 255.254 2^16-2 C 192.0.1 223.255.255 2^21-1 1 254 2^8-2 划分子网 从网络的主机号借用若干位作为子网号，在 IP 地址中增加一个“子网号字段”，使两级 IP 地址变成三级 IP 地址。划分子网只是把 IP 地址的主机号这部分进行再划分，而不改变 IP 地址原来的网络号。\n从 IP 数据报的首部无法看出源主机或目的主机所连接的网络是否进行了子网的划分，需要使用子网掩码(subnet mask) 。把三级 IP 地址的子网掩码和收到的数据报的目的 IP 地址逐位相“与”(AND)，即得出所要找的子网的网络地址。\n无分类编址CIDR 无分类域间路由选择CIDR（ClasslessInter-DomainRouting），最主要的特点是：\nCIDR消除了传统的A类、B类和C类地址以及划分子网的概念。CIDR 把 32 位的IP 地址划分为前后两个部分。前面部分是“网络前缀” (network-prefix)(或简称为 “前缀”) ，用来指明网络，后面部分则用来指明主机。 CIDR把网络前缀都相同的连续的 IP 地址组成一个“CIDR 地址块”。 IP 协议的转发流程 在路由表中，对每一条路由最重要的是以下两个信息：目的网络地址，下一跳地址。\nIP 层分组转发算法如下：\n从数据报的首部提取目的主机的 IP 地址 D，得到目的网络地址为 N； 若 N 就是与此路由器直接相连的某个网络地址，则进行直接交付（包括把目的主机地址 D 转换为具体的硬件地址，把数据报封装为 MAC 帧，再发送此帧）；否则就是间接交付，执行 3； 若路由表中有目的地址为 D 的特定主机路由，则把数据报传送给路由表中所指明的下一跳路由器；否则，执行 4； 若路由表中有到达网络 N 的路由，则把数据报传送给路由表中所指明的下一跳路由器；否则，执行 5； 若路由表中有一个默认路由，则把数据报传送给路由表中所指明的默认路由器；否则，执行 6； 报告转发分组出错。 网络层其它协议 ARP 协议和 RARP 协议 Address Resolution Protocol 地址解析协议，用于将网络层 32 位 IPv4 的地址转为数据链路层 48 位的 MAC 地址。 地址解析协议 ARP 在主机 ARP 高速缓存中存放一个从 IP 地址到硬件地址的映射表， 并且这个映射表还经常动态更新 (新增或超时删除)。\n主机 A 使用 ARP 协议找到主机 B 的硬件地址的过程如下：\nA 的 ARP 进程在本局域网上广播发送一个 ARP 请求分组。请求分组的主要内容是：我的 IP 地址是 X，硬件地址是 Y，我想知道 IP 地址为 Z 的主机的硬件地址； 在本局域网上的所有主机上运行的 ARP 进程都收到此 ARP 请求分组； 主机 B 的 IP 地址与 ARP 请求分组中要查询的 IP 地址一致，就收下这个 ARP 请求分组，并向主机 A 发送 ARP 响应分组，同时在这个 ARP 响应分组中写入自己的硬件地址。B 也会把 A 的 IP 地址和硬件地址映射写入自己的 ARP 高速缓存中； 主机 A 收到主机 B 的 ARP 响应分组后，就在其 ARP 高速缓存中写入主机 B 的 IP 地址到硬件地址的映射。 逆地址解析协议 RARP，它的作用是使只知道自己硬件地址的主机能够通过 RARP 协议找出其 IP 地址。现在的 DHCP 协议己经包含了 RARP 协议的功能。\nICMP 协议 网际控制报文协议ICMP (Internet Control Message Protocol)，ICMP允许主机或路由器报告差错情况和提供有关异常情况的报告。ICMP 是 IP 层协议，ICMP 报文作为IP 层数据报的数据，加上数据报的首部，组成IP 数据报发送出去。ICMP 报文格式如图：\nICMP 报文的种类有两种，即ICMP差错报告报文和ICMP询问报文。\nICMP 报文种类 类型的值 ICMP 报文的类型 代码 差错报告报文 3（终点不可达） 网络不可达 0 主机可不达 1 5（改变路由） 对网络重定向 0 对主机重定向 1 11 传输超时 12 IP 头错误 0 缺少其它必要参数 1 询问报文 0 或 8 回送（Echo）请求或应答 13 或 14 时间戳（Timestamp）请求或回答 ICMP的一个重要应用就是分组网间探测PING(Packet InterNet Groper)，用来测试两台主机之间的连通性。PING 使用了ICMP 回送请求与回送回答报文。PING 是应用层直接使用网络层 ICMP 的一个例子 。它没有通过运输层的 TCP 或 UDP。\n另一个非常有用的应用是traceroute / tracert，它用来跟踪一个分组从源点到终点的路径。 traceroute 从源主机向目的主机发送一连串的 IP 数据报，数据报中封装的是无法交付的 UDP 用户数据报。数据报的 TTL 从 1 依次增大，数据报每经过一个路由器 TTL 减 1，每当 TTL 为 0 时，路由器丢弃数据报并向源主机发送一个 ICMP 传输超时差错报告报文； 最后一个数据报因为封装的是无法交付的 UDP 用户数据报，因此目的主机要向源主机发送 ICMP 终点不可达差错报告报文。\n当运输层从 IP 层收到UDP 数据报时，就根据首部中的目的端口，把UDP 数据报通过相应的端口，上交到对应的应用进程。如果接收方 UDP 发现收到的报文中的目的端口号不正确 (即不存在对应于该端口号的应用进程)，就丢弃该报文，并由网际控制报文协议 ICMP 发送“端口不可达”差错报文给发送方。\nIP 的路由算法 路由的概述 互联网采用的路由选择协议主要是自适应的（即动态的）、分布式路由选择协议。可以把整个互联网划分为许多较小的自治系统（AS），自治系统 AS 是在单一技术管理下的一组路由器，而这些路由器使用一种自治系统内部的路由选择协议和共同的度量。一个 AS 对其他 AS 表现出来的是一个单一的和一致的路由选择策略。\n内部网关路由协议 RIP 距离矢量（DV）算法：\n每一个节点使用两个向量 $D_i$ 和 $S_i$ $D_i$ 描述当前节点到别的节点的距离 $S_i$ 描述当前节点到别的节点的下一节点 每一个节点与相邻的节点交换向量 $D_i$ 和 $S_i$ 的信息 每一个节点根据交换的信息更新自己的节点信息 RIP （Routing Information Protocol）协议，RIP 协议把网络的跳数（hop）作为 DV 算法的距离，RIP 协议每隔 30s 交换一次路由信息，认为跳数大于 15 的路由不可达。\n对地址为 X 的相邻路由器发来的 RIP 报文，先修改此报文中的所有项目：把“下一跳”字段中的地址都修改为 X，并把所有的“距离”字段的值都加 1。每一个项目都有三个关键数据：到目的网络 N，距离是 d，下一跳路由是 X。 对修改后的 RIP 报文中的每一个项目，进行以下步骤： 若原来的路由表中没有目的网络 N，则把该项目添加到路由表中，否则 若下一跳路由器地址是 X，则把收到的项目替换原路由表中的项目，否则 若收到的项目中的距离 d 小于路由表中的距离，则进行更新，否则什么也不做 若 3 分钟还没有收到相邻路由器的更新路由表，则吧此相邻路由器标记为不可达路由器，即把距离置为 16。 举例：己知路由器R6的路由表，现在收到相邻路由器R4发来的路由更新信息。试更新路由器R6的路由表。\nR6 的路由表\n目的网络 距离 下一跳路由器 Net2 3 R4 Net3 4 R5 R4 发来的路由更新信息\n目的网络 距离 下一跳路由器 Net1 3 R1 Net2 4 R2 Net3 1 直接交付 修改 R4 中所有项目得到的路由信息\n目的网络 距离 下一跳路由器 Net1 4 R4 Net2 5 R4 Net3 2 R4 R6 更新后的路由表信息\n目的网络 距离 下一跳路由器 Net1 4 R4 Net2 5 R4 Net3 2 R4 内部网关路由协议 OSPF OSPF（Open Shortest Path First）的五种分组类型：\n问候分组，用来发现和维持邻站的可达性 数据库描述分组，向邻站给出自己的链路状态数据库中的所有链路状态项目的摘要信息 链路状态请求分组，向对方请求发送所有链路状态项目的详细信息 链路状态更新分组，用洪泛法对全网更新链路状态 链路状态确认分组，对链路更新分组的确认 OSPF 的过程：\n路由器接入网络 路由器向邻站发送问候分组 与邻站交流链路状态数据库 广播和更新未知路由 外部网关路由协议 BGP（Border Gateway Protocol）边际网关协议，是运行在 AS 之间的一种协议。边界网关协议 BGP 只能是力求寻找一条能够到达目的网络且比较好的路由(不能兜圈子)，而并非要寻找一条最佳路由。\n传输层 网络相关知识总结\nUDP 协议 UDP（User Datagram Protocol）用户数据报协议。\nUDP 的特点 UDP 是无连接的，即发送数据之前不需要建立连接，因此减少了开销和发送数据之前的时延。 UDP 使用尽最大努力交付，即不保证可靠交付，因此主机不需要维持复杂的连接状态表。 UDP 是面向报文的。发送方的 UDP 对应用程序交下来的报文，在添加首部后就向下交付IP 层。 UDP 没有拥塞控制，因此网络出现的拥塞不会使源主机的发送速率降低。 UDP 的首部开销小。 UDP 支持一对一、一对多、多对一和多对多的交互通信。 UDP 的首部格式 源端口，需要对方回信时选用，不需要时可全 0。 目的端口，交付报文时必须使用。 长度，UDP 用户数据报的长度，最小值是 8（仅有首部）。 校验和，检测 UDP 用户数据报在传输过程中是否有错，有错就丢弃。 伪首部，临时添加在 UDP 用户数据报前面，得到一个临时的 UDP 用户数据报，伪首部既不向下传送也不向上递交，而仅仅是为了计算检验和。 TCP 协议 TCP（Transmission Control Protocol）传输控制协议。\nTCP 的特点 TCP 是面向连接的运输层协议。 每一条 TCP 连接只能有两个端点，每一条 TCP 连接只能是点对点的。 TCP 提供可靠交付的服务。通过 TCP 连接传送的数据，无差错、不丢失、不重复， 并且按序到达。 TCP 提供全双工通信。TCP 允许通信双方的应用进程在任何时候都能发送数据。 面向字节流。TCP 中的“流” (stream)指的是流入到进程或从进程流出的字节序列。 TCP 的首部格式 可靠传输的基本原理 停止等待协议\n连续 ARQ 协议\nTCP 可靠传输的实现 以字节为单位的滑动窗口\n超时重传时间的选择，TCP采用了一种自适应算法，它记录一个报文段发出的时间，以及收到相应的确认的时间。这两个时间之差就是报文段的往返时间RTT。TCP保留了RTT的一个加权平均往返时间RTTs。\n选择确认 SACK，若收到的报文段无差错，只是未按序号，中间还缺少一些序号的数据，选择确认可以让发送方只传送缺少的数据而不重传已经正确到达接收方的数据。\nTCP 的流量控制 流量控制(flow control)就是让发送方的发送速率不要太快，要让接收方来得及接收，利用滑动窗口实现流量控制。设A向B发送数据，在连接建立时，B告诉了A:“我的接收窗口rwnd=400”。因此，发送方的发送窗口不能超过接收方给出的接收窗口的数值。\n当rwnd =0时不允许发送方再发送数据，持续到主机B 重新发出一个新的窗口值为止。假设B向A发送了rwnd=400的报文段，然而这个报文段在传送过程中丢失了，A一直等待收到B发送的非零窗口的通知，而B也一直等待A发送的数据。\n为了解决这个问题，TCP 为每一个连接设有一个持续计时器(persistence timer)。只要 TCP 连接的一方收到对方的零窗口通知，就启动持续计时器。若持续计时器设置的时间到期，就发送一个零窗口探测报文段 (仅携带 1 字节的数据)，而对方就在确认这个探测报文段时给出了现在的窗口值。\nTCP 的拥塞控制 所谓拥塞控制就是防止过多的数据注入到网络中，这样可以使网络中的路由器或链路不致过载。拥塞控制所要做的都有一个前提，就是网络能够承受现有的网络负荷。拥塞控制是一个全局性的过程，涉及到所有的主机、所有的路由器，以及与降低网络传输性能有关的所有因素。\nTCP 进行拥塞控制的算法有四种，即慢开始(slow-start)、拥塞避免(congestion avoidance)、快重传(fast retransmit)和快恢复(fast recovery) 。\nTCP 连接的建立 TCP 连接的释放 应用层 应用层协议 域名系统 DNS DNS（Domain Name System）域名系统。域名系统DNS能够把互联网上的主机名字转换为IP地址。每一个域名都由标号(label)序列组成，而各标号之间用点隔开。\n顶级域名共分为三大类：\n国家顶级域名nTLD，如：cn表示中国，us表示美国，uk表示英国等。 通用顶级域名gTLD，com (公司企业)，net (网络服务机构)，org (非营利性组织)，int (国际组织)，edu ( 美国专用的教育机构)，gov (美国的政府部门)，mil 表示(美国的军事部门)。 基础结构域名，即 arpa，用于反向域名解析，因此又称为反向域名。 我国把二级域名划分为“类别域名” 和“行政区域名” 两大类：\n类别域名：ac (科研机构)，com (工、商、金融等企业)，edu (中国的教育机构)，gov (中国的政府机构)，mil (中国的国防机构)，net (提供互联网络服务的机构 )，org(非营利性的组织）。 行政区域名：共34 个，适用于我国的各省、自治区、直辖市。 动态主机配置协议 DHCP DHCP（Dynamic Host Configuration Protocol）动态主机配置协议，提供了即插即用连网机制，允许一台计算机加入新的网络和获取IP地址而不用手工参与，DHCP 使用客户服务器方式。\n需要IP地址的主机在启动时就向DHCP服务器广播发送发现报文(DHCPDISCOVER)(将目的IP地址置为全1，即255.255.255.255)，这时该主机就成为DHCP客户。发送广播报文是因为现在还不知道DHCP服务器在什么地方，因此要发现(DISCOVER)DHCP服务器的IP地址。这台主机目前还没有自己的IP地址，因此它将IP数据报的源IP地址设为全0。这样，在本地网络上的所有主机都能够收到这个广播报文，但只有DHCP服务器才对此广播报文进行回答。DHCP 服务器先在其数据库中查找该计算机的配置信息。若找到，则返回找到的信息。若找不到，则从服务器的IP地址池(address pool)中取一个地址分配给该计算机。DHCP服务器的回答报文叫做提供报文(DHCPOFFER)，表示“提供” 了IP地址等配置信息。\n超文本传送协议 HTTP HTTP（HyperText Transfer Protocol）超文本传输协议。http://\u0026lt;主机\u0026gt;\u0026lt;端口\u0026gt;/\u0026lt;路径\u0026gt;，HTTP 的默认端口号是80，通常可省略。\nHTTP的报文结构：\nHTTP 响应报文状态码：\n1xx表示通知信息，如请求收到了或正在进行处理。 2xx表示成功，如接受或知道了。 3xx表示重定向，如要完成请求还必须采取进 一步的行动。 4xx表示客户的差错，如请求中有错误的语法或不能完成。 5xx表示服务器的差错，如服务器失效无法完成请求。 ","permalink":"https://2d992714.xianglin2020-github-io.pages.dev/posts/2023/computer-network/","summary":"计算机网络概述、网络层、传输层、应用层和实践。","title":"计算机网络"},{"content":"基础篇 操作系统的演进 无操作系统 人工操作、用户独占。资源利用率很低。\n批处理系统 无需等待人工操作、批量输入任务。多道程序设计（指计算机内存中同时存放多个程序，使得批处理系统可以一次处理多个任务），资源利用率提升。\n分时系统 人——机交互、多用户共享。及时调试程序，资源利用率提升。\n操作系统的功能 进程管理 存储管理 作业管理 文件管理 设备管理 操作系统概览 What\u0026amp;Why 操作系统是管理计算机硬件和软件资源的计算机程序。\n管理配置内存、决定资源供需顺序、控制输入输出设备等。\n操作系统提供让用户和系统交互的操作界面。\n操作系统的基本功能 操作系统统一管理计算机资源：处理器资源、存储器资源、I/O设备资源、文件资源。\n操作系统实现了对计算机资源的抽象，用户无需面向硬件接口编程。\n操作系统提供了用户与计算机之间的接口：图形窗口形式、命令形式、系统调用形式。\n操作系统相关概念 并发性\n共享性：操作系统中的资源可供多个并发的程序共同使用，这种共同使用的形式称之为资源共享。资源共享分为互斥共享形式和同时访问形式。\n虚拟性：虚拟性表现为把一个物理实体转变为若干个逻辑实体，物理实体是真实存在的，逻辑实体是虚拟的。虚拟的技术主要有时分复用技术和空分复用技术。\n异步性：在多道程序环境下，运行多个进程并发执行。进程在使用资源时可能需要等待或放弃。\n进程管理 为什么需要进程 进程是系统进行资源分配和调度的基本单位。\n进程作为程序独立运行的载体保障程序正常执行。\n进程的存在使得操作系统资源的利用率大幅提升。\n进程的实体 主存中的进程形态是进程控制块（PCB），包含：标识符、状态、优先级、程序计数器、内存指针、上下文数据、I/O 状态信息等。\n线程是操作系统进行运行调度的最小单位，线程包含在进程之中，是进程中实际运行工作的单位。\n五状态模型 创建、就绪、阻塞、执行、终止。\n创建：创建进程时拥有 PCB 但其它资源尚未就绪的状态，操作系统提供 fork 函数接口创建进程。\n就绪：当进程被分配到除 CPU 时间片以外所有必要资源后处于就绪状态，只要再获得 CPU 的使用权，就可以立即运行。\n执行：进程获得 CPU 后程序正在执行称为执行状态，在单处理机中，在某个时刻只能有一个进程是处于执行状态。\n阻塞：进程因某种原因（其它设备未就绪）无法继续执行从而放弃 CPU 的状态称为阻塞状态。\n终止：进程结束由系统清理或归还 PCB 的状态。\n进程同步 生产者消费者问题（有限缓冲区问题）\n哲学家就餐问题\n上述两个问题产生的原因是彼此间没有通信（即进程间没有同步）。\n进程间同步原则：\n空闲让进：资源无占用，允许使用。 忙则等待：资源有占用，请求进程等待。 有限等待：保证有限等待时间能够使用资源，避免其它等待的进程僵死。 让权等待：等待时，进程需要让出 CPU，也就是进程由执行状态变为阻塞状态，这也是保证 CPU 可以高效使用的前提。 进程间同步的方法：\n消息队列 共享存储 信号量 Unix域套接字 线程间同步的方法：\n互斥量 读写锁 自旋锁 条件变量 Linux 进程 进程的类型：\n前台进程：具有终端，可以和用户交互的进程。 后台进程：没有占用终端的进程，基本上不和用户交互，优先级比前台进程低（使用\u0026lt;command\u0026gt; \u0026amp;启动后台进程）。 守护进程：是特殊的后台进程，很多都是随系统一起启动，直到系统关闭（systemd、crond、sshd、mysqld）。 进程的标记：\n进程 ID 是进程的唯一标记，表现为一个非负整数，最大值由操作系统限定。\nID 为 0 的进程为 idle 进程，是系统创建的第一个进程。ID 为 1 的进程为 init 进程，是 0 号进程的子进程，完成系统初始化。init 进程是所有用户进程的祖先进程。\n进程的状态（man ps）：\n作业管理 进程调度 进程调度器是操作系统的一部分，决定了何时运行什么进程。它通常能够暂停一个运行中的进程，将它放回到运行队列当中，并运行一个新进程，我们把这样的调度器叫做抢占调度器。否则，它就是协同调度器。\n进程调度算法 先来先服务调度算法。 短进程优先调度算法：优先选择就绪队列中估计运行时间最短的进程，不利于长作业进程。 高优先权调度算法：进程附带权重，调度程序优先选择权重高的进程，紧迫的任务可以优先处理。 时间片轮转调度算法：按先来先服务的原则排列就绪进程，每次从队列头部取出待执行进程，分配一个时间片执行。是相对公平的调度算法，但不能保证及时响应用户。 死锁 两个或两个以上的进程在执行过程中，由于竞争资源或者由于彼此通讯而造成的一种阻塞现象，若无外力作用，它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁。\n死锁产生的原因：\n竞争资源 进程调度顺序不当 死锁的四个必要条件：\n互斥条件：资源只能同时分配给一个进程，无法多个进程共享。 请求保持条件：进程至少持有一个资源，又提出新的资源请求。新资源被占用，请求被阻塞，被阻塞的进程不释放自己持有的资源。 不可剥夺条件：进程获得的资源在未完成使用前不能被剥夺，获得的资源只能由进程自身释放。 环路等待条件：发生死锁时，必然存在“进程—资源”环形链。一系列进程互相持有其他进程所需要的资源。 死锁的处理：\n鸵鸟策略（即不理睬策略） 预防策略： 预先静态分配法。破坏了“ 不可剥夺条件”，预先分配所需资源，保证不等待资源。 资源有序分配法。破坏了“ 环路条件”，把资源分类按顺序排列，保证不形成环路。 避免策略：Dijkstra 提出的银行家算法。 检测与解除： 资源剥夺法。从一些进程那里强行剥夺足够数量的资源分配给死锁进程。 撇销进程法。根据某种策略逐个地撤销死锁进程，直到解除死锁为止。 存储管理 存储器管理的对象是主存存储器简称主存或内存。存储器管理的主要功能包括主存空间的分配和回收、提高主存的利用率、扩充主存、对主存信息实现有效保护。\n内存分配的过程 内存分配的方式：\n单一连续分配，是最简单的内存分配方式，只能在单用户、单进程的操作系统中使用。 固定分区分配，支持多道程序的最简单存储分配方式，内存空间被划分为若干个固定大小的区域，每个区域只提供给一个程序使用，互不干扰。 动态分区分配，按照进程实际需要，动态分配内存空间。 动态分区分配算法：\n首次适应算法：每当用户作业申请一个空间时，系统总是从主存的低地址开始选择一个能装入作业的空白区。当用户释放空间时，该算法更易实现相邻的空白区合并。 循环首次适应算法。与首次适应算法的不同之处是，每次分配都是从刚分配的空白区开始寻找一个能满足用户要求的空白区。 最佳适应算法 。 假设系统中有 n 个空白区(自由区)，每当用户申请一个空间时，将从这n个空白区中找到一个最接近用户需求的分区。这种算法能保留较大的空白区，缺点是空闲区不可能刚好等于用户要求的区，所以必然要将一个分区一分为二，但是随着系统不断地释放空问，可能会使产生的小分区小到无法再继续分配，将这样的无用小分区称为外碎片。 最差适应算法。系统总是将用户作业装入最大的空白分区 。这种算法将一个最大的分区一分为二，所以剩下的空白区通常也大，不容易产生外碎片。 段页式存储管理 段页式系统的基本原理是先将整个主存划分成大小相等的存储块 (页框)，将用户程序按程序的逻辑关系分为若干个段，并为每个段赋予一个段名，再将每个段划分成若干页，以页框为单位离散分配。在段页式系统中，其地址结构由段号、段内页号和页内地址三部分组成。\n虚拟内存 作业只部分装入主存便可开始启动运行，其余部分暂时留在磁盘上，在需要时再装入主存，这样可以有效地利用主存空间。从用户角度看，该系统所具有的主存容量将比实际主存容量大得多，人们把这样的存储器称为虚拟存储器。虚拟存储器是为了扩大主存容量而采用的一种设计方法，其容量是由计算机的地址结构决定的。\n程序局部性原理，程序的局限性表现在时间局限性和空间局限性两个方面：\n时间局限性是指如果程序中的某条指令 一旦执行，则不久的将来该指令可能再次被执行；如果某个存储单元被访问，则不久以后该存储单元可能再次被访问。产生时间局限性的典型原因是在程序中存在着大量的循环操作。 空间局限性是指一旦程序访问了某个存储单元，则在不久的将来，其附近的存储单元也最有可能被访问。即程序在一段时间内所访问的地址可能集中在一定的范围内，其典型原因为程序是顺序执行的。 Linux 存储管理 Buddy 内存管理算法：\n内存分配原则：向上取整为 2 的幂次大小。\n此算法基于计算机处理二进制的优势具有极高的效率，主要解决了内存外碎片的问题（将内存外碎片转为内存内碎片）。\nLinux 交换空间：\n交换空间可以是磁盘的一个分区，也可以是一个文件。用户可以在安装时或安装后的任何时候创建交换空间。\n交换空间有两种用途：\n将虚拟内存扩大到超过已安装的物理内存（RAM）的容量； 用于 suspend-to-disk 支持。 文件管理 文件的逻辑结构 文件的逻辑结构可分为两大类：一是有结构的记录式文件，它是由一个以上的记录构成的文件，故又称为记录式文件；二是无结构的流式文件，它是由一串顺序字节流构成的文件。\n有结构的记录式文件：\n文件内容由定长记录和可变长记录组成。 定长记录存储文件格式、文件描述等结构化数据项。 可变长记录存储文件具体内容。 无结构文件：\n文件体为字节流，不划分记录。 可以把流式文件看作是记录式文件的一个特例。 文件的物理结构 连续结构：也称顺序结构，它将逻辑上连续的文件信息(如记录)依次存放在连续编号的物理块上。只要知道文件的起始物理块号和文件的长度，就可以很方便地进行文件的存取。 索引结构：在采用索引结构时，将逻辑上连续的文件信息(如记录)存放在不连续的物理块中，系统为每个文件建立一张索引表。索引表记录了文件信息所在的逻辑块号对应的物理块号，并将索引表的起始地址放在与文件对应的文件目录项中。 文件存储空间的管理 外存空闲空间管理的数据结构通常称为磁盘分配表(Disk Allocation Table)。常用的空闲空间的管理方法有空闲区表、位示图、空闲块链和成组链接法 4 种。\n空闲区表。将外存空间上的一个连续的末分配区域称为“空闲区”。操作系统为磁盘外存上的所有空闲区建立一张空闲表，每个表项对应一个空闲区，空闲表中包含序号、空闲区的第一块号、空闲块的块数和状态等信息，它适用于连续文件结构。 位示图。这种方法是在外存上建立一张位示图 (Bitmap)，记录文件存储器的使用情况。每一位对应文件存储器上的一个物理块，取值0 和1 分别表示空闲和占用。 空闲块链。每个空闲物理块中有指向下一个空闲物理块的指针，所有空闲物理块构成一个链表，链表的头指针放在文件存储器的特定位置上(如管理块中)，不需要磁盘分配表，节省空间。每次申请空闲物理块只需根据链表的头指针取出第一个空闲物理块，根据第一个空闲物理块的指针可找到第二个空闲物理块，依此类推。 成组链接法。UNIX系统采用该方法。例如，在实现时系统将空闲块分成若干组，每 100 个空闲块为一组，每组的第一个空闲块登记了下一组空闲块的物理盘块号和空闲块总数。 假如某个组的第一个空闲块号等于0，意味着该组是最后一组，无下一组空闲块。 目录结构 多级日录结构像一棵倒置的有根树，所以也称为树型目录结构。从树根向下，每一个结点是一个目录，叶结点是文件。MS-DOS 和UNIX等操作系统均采用多级目录结构。在采用多级目录结构的文件系统中，用户要访问一个文件，必领指出文件所在的路径名， 路径名是从根目录开始到该文件的通路上所有各级日录名拼起来得到的。\nLinux 文件的基本操作 Linux 目录结构：\nLinux 文件类型（ls 命令展示的内容）：\n-：普通文件 d：目录文件 l：符号链接 d / c：设备文件 s：套接字 p：FIFO（具名管道） Linux系统核心可以支持十多种文件系统类型。\n设备管理 广义的 I/O 设备 对 CPU 而言，对 CPU 进行数据输入的称为输入设备，对 CPU 进行数据输出的称为输出设备。\n按照设备的功能分类：存储设备、输入设备、输出设备。 按数据组织分类：块设备(BlockDevice)、字符设备(Character Device)。 按资源分配角度分类：分为独占设备、共享设备和虚拟设备。 按数据传输率分类。分为低速设备、中速设各和高速设备。 Spooling 技术 Spooling 是Simultaneous Peripheral Operations On Line (外围设备联机操作)的简称。所谓 Spooling 技术，实际上是用一类物理设备模拟另一类物理设备的技术，是使独占使用的设备变成多台虚拟设备的一种技术，也是一种速度匹配技术。\nSpooling系统的工作过程是操作系统初启后激活Spooling 预输入程序使它处于捕获输入请求的状态， 一旦有输入请求消息，Spooling输入程序立即得到执行，把装在输入设备上的作业输入到硬盘的输入井中并填写好作业表，以便在作业执行中要求输入信息时可以随时找到它们的存放位置。当作业需要输出数据时，可以先将数据送到输出井，当输出设备空闲时，由 Spooling 输出程序把硬盘上输出井的数据送到慢速的输出设备上。\n","permalink":"https://2d992714.xianglin2020-github-io.pages.dev/posts/2023/operating-system-basics/","summary":"操作系统基础知识","title":"操作系统"},{"content":"概述 发展简史 计算机发展的四个阶段 电子管计算机 晶体管计算机 集成电路计算机 超大规模集成电路计算机 （未来）生物计算机、量子计算机 微型计算机的发展历史 单核CPU -\u0026gt; 多核CPU\n摩尔定律：集成电路的性能，每18~24个月就会提升一倍。\n计算机的分类 超级计算机 标记超级计算机运算速度的单位是TFlop/s：1TFlop/s=每秒一万亿次浮点运算。\n大型计算机 具有高性能，可处理大量数据与复杂的运算。\n迷你计算机（服务器） 也称为小型机、普通服务器，具有不错的算力，可以完成较复杂的运算。\n工作站 高端的通用微型计算机，提供比个人计算机更强大的性能。\n微型计算机 又称为个人计算机，包括：台式机、笔记本电脑、一体机。\n计算机的体系与机构 冯诺依曼体系 是一种将程序指令存储器和数据存储器合并在一起的电脑设计概念结构。包含：存储器、控制器、运算器、输入设备和输出设备。\n计算机的层次与编程语言 程序翻译与程序解释 编译器是用于将高级编程语言转换为低级编程语言的翻译器。它在一个会话中转换整个程序并报告转换后检测到的错误。\n解释器是一种计算机程序，能够把解释型语言解释执行。解释器就像一位“中间人”。解释器边解释边执行，因此依赖于解释器的程序运行速度比较缓慢。\n计算机的层次 应用层：满足计算机针对某种用途而专门设计。\n高级语言层：常见的高级语言有：C、Python、Golang、Java等。\n汇编语言层：编程语言是汇编语言，通过汇编器翻译成可直接执行的机器语言。\n操作系统层：向上提供了简易的操作界面，向下对接指令系统，管理硬件资源，是软硬件之间的适配层。\n传统机器层：编程语言是CPU指令集（机器指令），和硬件直接相关。\n微程序机器层：编程语言是微指令集。\n硬件逻辑层：门、触发器等逻辑电路组成。\n计算机的计算单位 容量单位 在物理层面，高低电平记录信息，理论上只认识0/1两种状态，称为bit（比特位）。\n字节：1Byte=8bits\nbit Byte KB MB GB TB PB EB 名称 比特位 字节 千字节 兆字节 吉字节 太字节 拍字节 艾字节 比例 - 8bits 1024B 1024KB 1024MB 1024GB 1024TB 1024PB 常见设备 门电路 - 寄存器 高速缓存 内存/硬盘 硬盘 云硬盘 数据仓库 速度单位 网络速度\n网络速度常用单位是Mbps：\n100M/s=100Mbps=100Mbit/s\n100Mbit/s=(100/8)MB/s=12.5MB/s\nCPU速度\nCPU的速度一般体现为CPU的时钟频率，时钟频率的单位一般是赫兹（Hz），主流CPU的时钟频率都在2GHz以上。\n赫兹是频率的国际单位，表示每一秒周期性事件发生的次数。\n计算机的字符与编码集 字符编码集的历史 ASCII码，美国信息交换标准代码。\n使用7个bits就可以完全表示ASCII码，包含95个可打印字符，33个不可打印字符（包括控制字符）。\nExtended ASCII码，是将ASCII码由7位扩充为8位而成。EASCII码比ASCII码扩充出来的符号包括表格符号、计算符号、希腊字母和特殊的拉丁符号。\n中文编码集 GB2312，《信息交换用汉字编码字符集·基本集》，GB/T 2312标准共收录6763个汉字，其中一级汉字3755个，二级汉字3008个；同时收录了包括拉丁字母、希腊字母、日文平假名及片假名字母、俄语西里尔字母在内的682个字符。\nGBK，《汉字内码扩展规范》， GBK共收录21886个汉字和图形符号，其中汉字（包括部首和构件）21003个，图形符号883个。GBK向下完全兼容GB2312-80编码，向上支持国际ISO标准。\nUnicode，全称Unicode标准，又称为统一码，万国码。Unicode最普遍的编码格式是UTF-8和UTF-16。UTF-8以字节为单位对Unicode进行编码。\n组成 计算机的总线 总线是指计算机设备和设备之间传输信息的公共数据通道。总线是连接计算机硬件系统内多种设备的通信线路，它的一个重要特征是由总线上的所有设备共享，因此可以将计算机系统内的多种设备连接到总线上。\n总线是指计算机组件间规范化的交换数据的方式，即以一种通用的方式为各个组件提供数据传送和控制逻辑。\n总线的分类 数据总线（Data Bus）在CPU和RAM之间来回传送需要处理或是需要存储的数据。DB的宽度决定了CPU和RAM每次交换数据的位数。\n地址总线（Address Bus）用来指定在RAM中存储的数据的地址，地址总线的宽度决定了CPU的最大寻址能力。如访问1MB存储器中的任一单元，需要给出2^20个地址，即需要20位地址（2^20=1M）。\n控制总线（Control Bus）用来传送控制信息、时序信号和状态信息等。\n常见总线 PCI总线，是目前微型机上广泛采用的内总线，采用并行传输方式。\nSATA（Serial ATA），主要用于主板和大量存储设备之间的数据传输。\nUSB，通用串行总线，USB由4条信号线组成，其中两条用以传送数据，另外两条传送+5V容量为500mA的电源。\n计算机的输入输出设备 常见的输入输出设备 字符输入设备：键盘（薄膜键盘、机械键盘、电容键盘）。\n图形输入设备：鼠标、数位板（输入板和压感笔）、扫描仪。\n图像输出设备：显示器（CRT显示器、液晶显示器）、打印机、投影仪。\n输入输出接口的通用设计 数据线是I/O设备与主机之间进行数据交换的传送线。\n状态线是I/O设备向主机报告状态的信号线，查询设备是否已经被占用。\n命令线是CPU向设备发送命令的信号线，发送读/写、启用/停止信号。\n设备选择线是主机选择I/O设备进行操作的信号线，对连在总线上的设备进行选择。\nCPU与IO设备的通信 程序中断：当外围I/O设备就绪时，向CPU发送中断信号，CPU有专门的电路响应中断信号。\nDMA（直接存储器访问）：DMA直接连接主存和I/O设备，当主存和I/O设备交换信息时，不需要中断CPU。\n计算机存储器概念 存储器分类 按存储介质分类：半导体存储器（：内存、U盘、固态硬盘）、磁存储器（：磁带、磁盘）。\n按存取方式分类：随机存储器（RAM）、串行存储器、只读存储器（ROM）。\n存储器层次结构 寄存器（Register）\n高速缓存（Cache）\n主存（Primary Storage）\n外存（Secondary Storage）\n主存储器与辅助存储器 内存 RAM（随机存取存储器：Random Access Memory）通过电容存储数据，必须隔一段时间刷新一次，如果掉电，那么一段时间后将丢失所有数据。\n磁盘 表面是可磁化的硬磁特性材料，移动磁头径向运动读取磁道信息。\n磁盘调度算法 磁盘调度分为移臂调度和旋转调度两类，并且是先进行移臂调度，然后进行旋转调度。\n先来先服务：按顺序访问进程的磁道读写需求。\n最短寻道时间优先：优先访问离磁头最近的磁道。\n扫描算法（电梯调度算法）：每次只往一个方向移动，到达一个方向的尽头再反方向移动。\n单向扫描调度算法：规定磁头只做单向运动。\n计算机的高速缓存 工作原理 字：指存放在一个存储单元的二进制代码组合。\n字块：存储在连续的存储单元中而被看作是一个单元的一组字。\n假设：一个字有32位、一个字块共B个字、主存共有M个字块，那么：B*M=主存总字数；B*M*32=主存总容量（bits）。\n例子：假设主存用户空间容量为4G，字块大小为4M、字长为32位，则对于字地址中的块地址m和块内地址b的位数，至少应该是多少？\n字块数：4GB/4M=4*1024M/4M=1024=2^10\n地址数：4M/32b=4*1024K/32b=4*1024*1024B/32b=4*2^10*2^10*8b/32b=2^20\n缓存命中率 h：命中次数$N_c$/总次数N = 命中次数$N_c$ / （命中次数$N_c$ + 未命中次数$N_m$） $$ h=\\frac{N_c}{N_c+N_m} $$ 访问主存时间：$t_m$、访问缓存时间：$t_c$\n访问Cache-主存系统平均时间：$t_a=ht_c+(1-h)t_m$\n访问效率 e $$ e=\\frac{t_c}{t_a}=\\frac{t_c}{ht_c+(1-h)t_m} $$ 例子：假设CPU在执行某段程序时，共访问了Cache命中2000次，访问主存50此，已知Cache的存取时间为50ns，主存的存取时间为200ns，求Cache-主存系统的命中率、访问效率和平均访问时间？ $$ 命中率：h=\\frac{N_c}{N_c+N_m}=\\frac{2000}{2000+50}=\\frac{40}{41} $$\n$$ 平均访问时间：t_a=ht_c+(1-h)t_m=\\frac{40}{41}*50+(1-\\frac{40}{41})*200=\\frac{2200}{41} $$\n$$ 访问效率：e=\\frac{t_c}{t_a}=\\frac{50}{\\frac{2200}{41}}=\\frac{41}{44} $$\n替换策略 随机算法。\n先进先出算法（FIFO）：把高速缓存看作队列，优先替换先进入队列的字块。\n最不经常使用算法（LFU）：优先淘汰最不经常使用的字块，需要额外的空间记录字块使用的频率。\n最近最少使用算法（LRU）：优先淘汰一段时间内没有使用的字块，一般用双向链表实现。\n计算机的指令系统 机器指令的形式 机器指令主要由两个部分组成：操作码、地址码。\n操作码指明指令要完成的操作，操作码的位数反映了机器的操作种类。\n地址码直接给出操作数或操作数的地址，分为三地址码指令、二地址码指令和一地址码指令。\n零地址指令是在机器指令中无地址码，一般有空操作、停机操作、中断返回操作等。\n机器指令的操作类型 数据传输：寄存器之间、寄存器与存储单元、存储单元之间传送。\n算术逻辑操作：操作数之间的加减乘除运算、操作数的与或非等逻辑位运算。\n移位操作：数据左移、数据右移操作。\n控制指令：等待指令、停止指令、空操作指令、中断指令等。\n机器指令的寻址方式 指令寻址：顺序寻址、跳跃寻址。\n数据寻址：立即寻址、直接寻址、间接寻址。\n计算机的控制器 控制器是协调和控制计算机运行的，主要有以下部分：\n程序计数器，用来存储下一条指令的地址。\n时序发生器，用于发送时序脉冲，CPU依据不同的时序脉冲有节奏的进行工作。\n指令译码器，翻译操作码对应的操作以及控制传输地址码对应的数据。\n各种寄存器：指令寄存器、主存地址寄存器、主存数据寄存器、通用寄存器。\n总线\n计算机的运算器 运算器是用来进行数据运算加工的，主要有以下部分：\n数据缓冲器，分为输入缓冲和输出缓冲。\nALU，算术逻辑单元，完成常见的位运算、算术运算。\n通用寄存器，用于暂时存放或传送数据或指令，可保存ALU的运算中间结果。\n状态字寄存器，存放运算状态（条件码、进位、溢出、结果正负），存放运算控制信息（调试跟踪标记位、允许中断位）。\n总线\n计算机指令的执行过程 指令执行过程 取指令 \u0026ndash;\u0026gt; 分析指令 \u0026ndash;\u0026gt; 执行指令\nCPU的流水线设计 串行执行m条指令：$T_1=3t*m$\n流水线执行m条指令：$T_2=t*(m+2)$ $$ H=\\frac{T_2}{T1}=\\frac{t*(m+2)}{3t*m}=\\frac{1}{3}+\\frac{1}{3*m} $$\n假设指令的各个阶段耗时为 $t_1$……$t_k$，流水线执行时长的计算有如下公式：\n$$ 理论公式：(t_1 + t_2 + ... + t_k)+(n - 1)*\\Delta t \\\\ \\\\ 实践公式：(k + n - 1)* \\Delta t $$\n计算 进制运算的基础 进制的定义 进位制是一种记数方式，亦称为进位计数法或位值计数法。是用有限种数字符号来表示无限的数值，使用的数字符号的数目称为这种进位制的基数或底数。\n正整数N，基础为r，可以表示为： $$ N = d_{n-1}d_{n-2}…d_1d_0=d_{n-1}r^{n-1}+d_{n-2}r^{n-2}+…+d_1r+d0 $$ 十进制 N = 1024：$N=1*10^3+2*10^1+4$。\n二进制N = 10000000000：$N=1*2^{10}$。\n二进制运算的基础 整数二进制转十进制，按权展开法： $$ N=(01100101)=1*2^6+1*2^5+1*2^2+1=101 $$ $$ N=(11101101)=1*2^7+1*2^6+1*2^5+1*2^3+1*2^2+1=237 $$\n整数十进制（101）转二进制，重复相除法：\n重复除以2 得商 取余数 101/2 50 1 50/2 25 0 25/2 12 1 12/2 6 0 6/2 3 0 3/2 1 1 1/2 0 1 将“取余数”列从下往上排列即可得到十进制101的二进制表示。\n小数二进制转十进制，按权展开法：\n$$ N=(0.11001)=1*2^{-1}+1*2^{-2}+1*2^{-5}=\\frac{25}{32}=0.78125 $$ $$ N=(0.01011)=1*2^{-2}+1*2^{-4}+1*2^{-5}=\\frac{11}{32}=0.34375 $$\n小数十进制（0.78125）转二进制，重复相乘法：\n重复乘以2 得积 取1 25/32 50/32=1+18/32 1 18/32 36/32=1+4/32 1 4/32 8/32=0+8/32 0 8/32 16/32=0+16/32 0 16/32 1=1+0 1 将“取1”列从上往下排列即可得到十进制0.78125的二进制表示。\n有符号数和无符号数 使用0表示正数，使用1表示负数。\n原码表示法 原码是指一个二进制数左边加上符号位后得到的码，在原码表示法中，最高位是符号位，0表示正号，1表示负号，其余的n-1位表示数值的绝对值。数值0的原码表示有两种形式：[+0]原=0 0000000，[-0]原= 1 0000000。\n反码表示法 在反码表示法中，最高位是符号位，0表示正号，1表示负号，正数的反码与原码相同，负数的反码则是其绝对值按位求反。数值0的反码表示有两种形式：[+0]反=0 0000000，[-0]反=1 1111111。\n补码表示法 在补码表示法中，最高位是符号位，0表示正号，1表示负号，正数的补码与其原码和反码相同，负数的补码则等于其反码的末位加1.在补码表示中，0有唯一的编码：[+0]补=0 0000000，[-0]补=0 0000000。\n定点数与浮点数 定点数的表示方法 小数点固定在某个位置的数称之为定点数。小数点固定在符号位之后表示纯小数，小数点固定在数值位末尾表示纯整数。\n浮点数的表示方法 浮点数的表示格式\nS：尾数（尾数规定使用纯小数）、r：基数、j：阶码\n浮点数的表示范围\n假设阶码数值取m位，尾数数值取n位。阶码能够表示的范围：$[-(2^m-1),2^m-1]$，尾数能够表示的范围：$[-(1-2^{-n}),-2^{-n}]$ 和$[2^{-n}, 1-2^{-n}]$。\n使用4字节，32位来表示浮点数称为单精度浮点数，使用8字节，64位来表示浮点数称为双精度浮点数。\n浮点数的规格化\n例子：设浮点数字长位16位，阶码为5位，尾数为11位，将十进制数13/128表示为二进制浮点数。\n定点数的加减法运算 补码运算：\n数值位与符号位一同运算，并将符号位产生的进位自然丢掉。\n例子：A=-110010，B=001101，求A+B\nA[补]=1,001110\nB[补]=0,001101\n(A+B)[补]=1,011011，换算成二进制为：A+B=-100101\n例子：A=-0.1010010，B=0.0110100，求A+B\nA[补]=1,1.0101110\nB[补]=0,0.0110100\n(A+B)[补]=1,1.1100010，换成二进制为：A+B=-0.0011110\n减法通常转化为加法进行运算，将减数与被减数的补码进行加法运算，即可得出差。-B[补]等于B[补]连同符号位按位取反，末位加一。\n浮点数运算 浮点数的加减法运算 对阶，保持两个浮点数阶码一致，使得尾数可以进行运算。\n尾数求和，使用补码进行运算，减法转换为加法进行运算。\n尾数规格化，若运算结果所得的尾数不是规格化的数，则需要进行规格化处理。当尾数溢出时，需要调整阶码。\n舍入，“0舍1入”，右移时，阶码加一。\n溢出判断，通过阶码的双符号位判断是否溢出。\n浮点数的乘数法运算 浮点数相乘，其积的阶码等于两乘数的阶码相加，积的尾数等于两乘数的尾数相乘。浮点数相除，其商的阶码等于被除数的阶码减去除数的阶码，商的尾数等于被除数的尾数除以除数的尾数。乘除运算的结果都需要进行规格化处理并判断阶码是否溢出。\n","permalink":"https://2d992714.xianglin2020-github-io.pages.dev/posts/2023/principles-of-computer-composition/","summary":"计算机组成原理，包括概述、组成、计算和实践","title":"计算机组成原理"},{"content":"Kotlin基础 Kotlin官网：https://kotlinlang.org/\nKotlin官方文档中文版：https://book.kotlincn.net/\n内置类型 变量类型 类型 Kotlin Java 字节 Byte byte/Byte 整型 Short \u0026amp; Int \u0026amp; Long short/Short \u0026amp; int/Integre \u0026amp; long/Long 浮点型 Float \u0026amp; Double float/Float \u0026amp; double/Double 字符 Char char/Character 字符串 String String 声明变量 val b:String = \u0026#34;Hello Kotlin\u0026#34; // 自动类型推导 var a = 2 val l = 123L val：只读变量 var：可读写变量 数值类型转换 int i = 10; long l = i; val i:Int = 10 val l:Long = i.toLong() Kotlin 中的数字没有隐式拓宽转换，如需将数值转换为不同的类型，请使用显式转换。所有数字类型都支持转换为其他类型：\ntoByte(): Byte toShort(): Short toInt(): Int toLong(): Long toFloat(): Float toDouble(): Double 无符号类型 除了整数类型，对于无符号整数，Kotlin 还提供了以下类型：\nUByte: 无符号 8 比特整数，范围是 0 到 255 UShort: 无符号 16 比特整数，范围是 0 到 65535 UInt: 无符号 32 比特整数，范围是 0 到 2^32 - 1 ULong: 无符号 64 比特整数，范围是 0 到 2^64 - 1 字符串 字符串比较\na == b：比较内容，等价于Java的equals() a === b：比较对象是否是同一个对象 字符串模板\nval j = \u0026#34;I❤️China\u0026#34; println(\u0026#34;Value of String \u0026#39;j\u0026#39; is: $j\u0026#34;) // no need brackets println(\u0026#34;Length of String \u0026#39;j\u0026#39; is: ${j.length}\u0026#34;) // need brackets 数组 Kotlin Java 整型 IntArray int[] 包装类整型 Array\u0026lt;Int\u0026gt; Integer[] 字符 CharArray char[] 包装类字符 Array\u0026lt;Char\u0026gt; Character[] 字符串 Array\u0026lt;String\u0026gt; String[] 区间 val intRange = 1..10 // [1, 10] val charRange = \u0026#39;a\u0026#39;..\u0026#39;z\u0026#39; val longRange = 1L..100L val intRangeExclusive = 1 until 10 step 2 // [1, 10) val intRangeReverse = 10 downTo 1 // [10, 9, ... , 1] 集合框架 增加了“不可变”集合框架的接口 通过类型别名实现复用JavaAPI实现集合：public actual typealias ArrayList\u0026lt;E\u0026gt; = java.util.ArrayList\u0026lt;E\u0026gt; 提供了丰富易用的方法，例如：forEach/map/flatMap 提供了运算符支持，简化集合框架的访问 Kotlin Java 列表 List\u0026lt;T\u0026gt; / MutableList\u0026lt;T\u0026gt; List\u0026lt;T\u0026gt; 映射 Map\u0026lt;K,V\u0026gt; / MutableMap\u0026lt;K,V\u0026gt; Map\u0026lt;K,V\u0026gt; 集合 Set\u0026lt;T\u0026gt; / MutableSet\u0026lt;T\u0026gt; Set\u0026lt;T\u0026gt; List\u0026lt;Integer\u0026gt; intList = new ArrayList\u0026lt;\u0026gt;(Arrays.asList(1, 2, 3, 4)); HashMap\u0026lt;String, Integer\u0026gt; hashMap = new HashMap\u0026lt;\u0026gt;(); hashMap.put(\u0026#34;Hello\u0026#34;, 10); val intList: List\u0026lt;Int\u0026gt; = listOf(1, 2, 3, 4) val intList2: MutableList\u0026lt;Int\u0026gt; = mutableListOf(1, 2, 3, 4) val map: Map\u0026lt;String, Any\u0026gt; = mapOf(\u0026#34;name\u0026#34; to \u0026#34;benny\u0026#34;, \u0026#34;age\u0026#34; to 20) val map2: Map\u0026lt;String, Any\u0026gt; = mutableMapOf(\u0026#34;name\u0026#34; to \u0026#34;benny\u0026#34;, \u0026#34;age\u0026#34; to 20) 函数 函数有自己的类型，是Kotlin中的”一等公民“ 可以赋值、传递，并在合适的条件下调用 方法可以认为是函数的一种特殊类型，从形式上看，有receiver的函数即为方法。 基本概念 函数的定义\nfun main(args: Array\u0026lt;String\u0026gt;):Unit{ } 函数的类型\nfun foo(){}\t() -\u0026gt; Unit fun foo(p0: Int): String {}\t(Int) -\u0026gt; String class Foo{ fun bar(p0: String, p1: Long):Any{}\tFoo.(String, Long) -\u0026gt; Any } 函数的引用\nfun foo(){}\t::foo\tval f: () -\u0026gt; Unit = ::foo fun foo(p0: Int): String{} ::foo\tval g:(Int) -\u0026gt; String = ::foo class Foo{ fun bar(p0: String, p1: Long):Any{}\tFoo::bar\tval h:(Foo, String, Long) -\u0026gt; Any = Foo:bar } 变长参数\nfun multiParameters(vararg ints: Int) { println(ints.contentToString()) } 多返回值\nfun multiReturnValues(): Triple\u0026lt;Int, Long, Double\u0026gt; { return Triple(1, 3L, 4.0) } val (a, b, c) = multiReturnValues() 默认参数和具名参数\nfun defaultParameter(x: Int = 0, y: String, z: Long = 1L){ } defaultParameter(y = \u0026#34;Hello\u0026#34;) 匿名函数 // 匿名函数 fun() { } // 匿名函数的传递 val func = fun() { } // 匿名函数的类型 val func: () -\u0026gt; Unit = fun() { } Lambda表达式 val lambda = { } val lambda: () -\u0026gt; Unit = { } 高阶函数 参数类型包含函数类型或返回值类型为函数类型的函数称为高阶函数。\n// 参数类型包含函数类型 fun cost(block: ()-\u0026gt;Unit){ val start = System.currentTimeMillis() block() println(\u0026#34;${System.currentTimeMillis() - start}ms\u0026#34;) } // 返回值类型为函数类型 fun fibonacci(): () -\u0026gt; Long { var first = 0L var second = 1L return { val next = first + second val current = first first = second second = next current } } 内联函数 inline fun cost(block: ()-\u0026gt;Unit){ val start = System.currentTimeMillis() block() println(\u0026#34;${System.currentTimeMillis() - start}ms\u0026#34;) } cost { println(\u0026#34;inline\u0026#34;) } // 内联后等价于： val start = System.currentTimeMillis() println(\u0026#34;inline\u0026#34;) println(\u0026#34;${System.currentTimeMillis() - start}ms\u0026#34;) 高阶函数的内联：\n函数本身被内联到调用处 内联函数的函数参数被内联到调用处 类和接口 类的定义\nclass SimpleClass{ } // 没有内容可以省略 {} class SimpleClass class SimpleClass{ // 成员必须初始化 var x: Int = 0 fun y(){ } } class SimpleClass{ var x: Int // 类的构造方法（辅助构造器） constructor(x: Int){ this.x = x } } class SimpleClass // 主构造器 constructor(x: Int){ var x: Int = x } // ==\u0026gt; class SimpleClass(x: Int){ var x: Int = x } // ==\u0026gt; class SimpleClass(var x: Int){ } 类的实例化\nval simpleClass = SimpleClass(10) println(simpleClass.x) 接口的定义\ninterface SimpleInf { fun method() } 接口实现\nclass SimpleClass(var x: Int) : SimpleInf { override fun method() { } } 抽象类的定义\nabstract class AbsClass{ abstract fun absMethod() // 默认不可覆写，使用 open 关键字声明方法可以被子类覆写 open fun overridable(){} fun nonOverridable(){} } 类的继承\nclass SimpleClass(var x: Int):AbsClass(), SimpleInf{} 属性的定义\nclass Person(age: Int, name: String) { // property var age: Int = age get() { return field } set(value) { field = value } } val ageRef = Person::age val person = Person(18, \u0026#34;Name\u0026#34;) val nameRef = person::name ageRef.set(person, 20) nameRef.set(\u0026#34;NewName\u0026#34;) 内部类 class Outer { inner class Inner class StaticInner } val inner = Outer().Inner() val statucInner = Outer.StaticInner() 匿名内部类 // 可以继承父类或实现多个接口 object: Runnable, Cloneable { override fun run(){ } } 数据类 data class Book(val id: Long, val name: String, val auther: Person) val (id, name, auther) = book 枚举类 enum class State(val id: Int) { Idle, Busy } State.Idle.name State.Idle.ordinal 密封类 密封类是一种特殊的抽象类，密封类的子类定义在与自身相同的文件中。\nsealed class PlayerState object Idle: PlayerState() class Playing(val song: Song): PlayerState() {} 内联类 内联类是对某一个类型的包装。\n// 包装 Int 必须是 val inline class BoxInt(val value: Int) 扩展 Kotlin 能够对一个类或接口扩展新功能而无需继承该类或者使用像装饰者这样的设计模式。 这通过叫做扩展的特殊声明完成。\n扩展方法 扩展方法的类型\nfun String.times(count: Int): String{} String::times (String, Int) -\u0026gt; String \u0026#34;*\u0026#34;::times (Int) -\u0026gt; String 扩展属性 由于扩展没有实际的将成员插入类中，因此对扩展属性来说backing field是无效的。这就是为什么扩展属性不能有初始化器。他们的行为只能由显式提供的 getter/setter 定义。\nval \u0026lt;T\u0026gt; List\u0026lt;T\u0026gt;.lastIndex: Int get() = size - 1 空类型安全 var nonNull: String = \u0026#34;Hello\u0026#34; val len = nonNull.length var nullable: String? = \u0026#34;World\u0026#34; val len1 = nullable?.length?: 0 // elvis 运算符 val len2 = nullable!!.length 空类型的继承关系\nvar x: String = \u0026#34;Hello\u0026#34; var y: String? = \u0026#34;World\u0026#34; y = x 类型转换 // 继承关系的转换 interface Kotliner class Person(val name: String, val age: Int) : Kotliner val kotliner: Kotliner = Person(\u0026#34;benny\u0026#34;, 20) if (kotliner is Person) { println(kotliner.name) } var value: String? = null value = \u0026#34;\u0026#34; if(value != null) { println(value.length) } 表达式 运算符重载 https://book.kotlincn.net/text/operator-overloading.html\nclass MathComplex(val r: Double, val v: Double) { override fun toString(): String { return \u0026#34;$r + ${v}i\u0026#34; } operator fun plus(c: MathComplex): MathComplex { return MathComplex(this.r + c.r, this.v + c.v) } operator fun minus(c: MathComplex): MathComplex { return MathComplex(this.r - c.r, this.v - c.v) } operator fun times(c: MathComplex): MathComplex { return MathComplex(this.r * c.r - this.v * c.v, this.r * c.v + this.v * c.r) } operator fun div(c: MathComplex): MathComplex { // 未实现 return MathComplex(0.0, 0.0) } } fun main(){ val c1 = MathComplex(1.0,1.0) val c2 = MathComplex(2.0,2.0) println(c1 + c2) } 中缀表达式 infix fun String.rotate(count: Int): String { val index = count % length return this.substring(index) + this.substring(0, index) } fun main(){ println(\u0026#34;HelloWorld\u0026#34; rotate 5) } 委托 接口委托 对象X代替当前类A实现接口B的方法\n// 定义接口 interface IApi { fun a() fun b() } // 接口的默认实现类 class ApiImpl1 : IApi { override fun a() { println(\u0026#34;ApiImpl1.a()\u0026#34;) } override fun b() { println(\u0026#34;ApiImpl1.b()\u0026#34;) } } // 使用静态代理，增强 a() class WrapperApi(private val api: IApi) : IApi { override fun a() { // enhance println(\u0026#34;enhance\u0026#34;) api.a() } override fun b() { api.b() } } // 使用委托 class DelegateApi(private val api: IApi) : IApi by api { override fun a() { println(\u0026#34;enhance\u0026#34;) api.a() } } 属性委托 对象X代替属性a实现getter/setter方法\nclass Person1(val name: String) { // lazy val firstName: String by lazy { name.split(\u0026#34; \u0026#34;)[0] } // observable var other: String by Delegates.observable(\u0026#34;\u0026#34;) { property, oldValue, newValue -\u0026gt; println(\u0026#34;$oldValue -\u0026gt; $newValue\u0026#34;) } } 泛型 fun \u0026lt;T\u0026gt; maxOf(t1: T, t2: T): T {} sealed class List\u0026lt;T\u0026gt; 泛型约束 fun \u0026lt;T : Comparable\u0026lt;T\u0026gt;\u0026gt; maxOf(t1: T, t2: T): T { return if (t1 \u0026gt; t2) t1 else t2 } fun \u0026lt;T\u0026gt; callMax(a: T, b: T) where T : Comparable\u0026lt;T\u0026gt;, T : () -\u0026gt; Unit { if (a \u0026gt; b) a() else b() } fun \u0026lt;T, R\u0026gt; callMax(a: T, b: T): R where T : Comparable\u0026lt;T\u0026gt;, T : () -\u0026gt; R, R : Number { return if (a \u0026gt; b) a() else b() } 泛型型变 泛型实参的继承关系对泛型类型的继承关系的影响：\n协变：继承关系一致 逆变：继承关系相反 不变：没有继承关系 sealed class List\u0026lt;T\u0026gt; { object Nil: List\u0026lt;Nothing\u0026gt;() } // 协变 sealed class List\u0026lt;out T\u0026gt; { object Nil: List\u0026lt;Nothing\u0026gt;() } // 逆变 interface Comparable\u0026lt;in T\u0026gt;{} 反射 反射是可以在运行时自省程序结构的一组语言和库功能。\nval c = MyClass::class 注解 注解概念 注解是将元数据附加到代码的方法。注解的附加属性可以通过用元注解标注注解类来指定：\n@Target指定可以用该注解标注的元素的可能的类型（类、函数、属性与表达式）； @Retention 指定该注解是否存储在编译后的 class 文件中，以及它在运行时能否通过反射可见 （默认都是 true）； @Repeatable允许在单个元素上多次使用相同的该注解； @MustBeDocumented指定该注解是公有 API 的一部分，并且应该包含在生成的 API 文档中显示的类或方法的签名中。 @Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION, AnnotationTarget.TYPE_PARAMETER, AnnotationTarget.VALUE_PARAMETER, AnnotationTarget.EXPRESSION) @Retention(AnnotationRetention.SOURCE) @MustBeDocumented annotation class Fancy 注解参数支持：基本类型、KClass、枚举、其它注解，以及其数组。\n内置注解 kotlin.annotation.*用于标注注解的注解 kotlin.*标准库的一些通用用途的注解 kotlin.jvm.*用于与Java虚拟机交互的注解 协程 协程的概念 协程是可以由程序自行控制挂起、恢复的程序。\n协程可以用来实现多任务的协作执行。\n协程可以用来解决异步任务控制流的灵活转移。\n协程可以让异步代码同步化、降低程序的设计复杂度。\n协程的分类 按调用栈分类\n有栈协程：每个协程会分配单独的调用栈，类似线程的调用栈 无栈协程：不会分配单独的调用栈，挂起点状态通过闭包或对象保存 按调用关系分类\n对称协程：调度权可以转移给任意协程，协程之间是对等关系 非对称协程：调度权只能转移给调用自己的协程，协程存在父子关系 协程的常见实现 Python Generator：无栈的非对称协程。\ndef numbers(): i = 0 while True: yield i i += 1 time.sleep(1) gen = numbers() print(f\u0026#34;[0]{next(gen)}\u0026#34;) print(f\u0026#34;[1]{next(gen)}\u0026#34;) Lua Coroutine：有栈的非对称协程。\n创建协程：coroutine.create(\u0026lt;function\u0026gt;) 查询状态：coroutine.status(\u0026lt;Coroutine-Object\u0026gt;) 挂起协程：coroutine.yield(\u0026lt;Values-to-Yield\u0026gt;) 恢复协程：coroutine.resume(\u0026lt;Coroutine-Object\u0026gt;) function producer() for i = 0,3 do print(\u0026#34;send\u0026#34;..i) coroutine.yield(i) end print(\u0026#34;End Producer\u0026#34;) end function consumer(value) repeat print(\u0026#34;receive\u0026#34;..value) value = coroutine.yield() until(not value) print(\u0026#34;End Consumer\u0026#34;) end producerCoroutine = coroutine.create(producer) consumerCoroutine = coroutine.create(consumer) repeat status, product = coroutine.resume(producerCoroutine) coroutine.resume(consumerCoroutine, product) until(not status) print(\u0026#34;End Main\u0026#34;) Go rotine\nchannel := make(chan int) var readChannel \u0026lt;-chan int = channel var writeChannel chan\u0026lt;- int = channel go func(){ for i :=0; i\u0026lt;3; i++{ fmt.Println(\u0026#34;wirte\u0026#34;,i) wirteChannel \u0026lt;- i } close(writeChannel) }() go func(){ fmt.Println(\u0026#34;wait for read\u0026#34;) for i:=range readChannel{ fmt.Println(\u0026#34;read\u0026#34;,i) } fmt.Println(\u0026#34;read end\u0026#34;) }() async / await：无栈非对称的协程实现。\n// Promise\u0026lt;AxiosResponse\u0026lt;T\u0026gt;\u0026gt; function getUser(name){ return axios.get(`https://api.github.com/users/${name}`); } async function main(){ const user = await getUser(\u0026#34;JetBrains\u0026#34;); console.log(user); } 协程的基本要素 挂起函数：以suspend修饰的函数，挂起函数的调用处称为挂起点。\n挂起函数只能在其它挂起函数或协程中调用。\n挂起函数调用时包含了协程“挂起”的语义，挂起函数返回时则包含了协程“恢复”的语义。\npublic interface Continuation\u0026lt;in T\u0026gt; { public val context: CoroutineContext public fun resumeWith(result: Result\u0026lt;T\u0026gt;) } public inline fun \u0026lt;T\u0026gt; Continuation\u0026lt;T\u0026gt;.resume(value: T): Unit = resumeWith(Result.success(value)) public inline fun \u0026lt;T\u0026gt; Continuation\u0026lt;T\u0026gt;.resumeWithException(exception: Throwable): Unit = resumeWith(Result.failure(exception)) 挂起函数的类型\nsuspend fun foo(){}\tsuspend () -\u0026gt; Unit fun foo(continuation: Continuation\u0026lt;Unit\u0026gt;): Any{} suspend fun bar(a: Int):String{} suspend (Int) -\u0026gt; String fun bar(a:Int continuation: Continuation\u0026lt;String\u0026gt;):Any {} 在没有真正挂起时，Any承载返回值，真正挂起时，Any承载挂起标志COROUTINE_SUSPENDED。\n使用suspendCoroutine获取到挂起函数的Continuation。\npublic suspend inline fun \u0026lt;T\u0026gt; suspendCoroutine(crossinline block: (Continuation\u0026lt;T\u0026gt;) -\u0026gt; Unit): T {} 协程执行过程中需要携带数据，使用协程上下文CoroutineContext表示。\n拦截器ContinuationInterceptor是一类协程上下文元素，可以对协程上下文所在的Continuation进行拦截。\npublic interface ContinuationInterceptor : CoroutineContext.Element { public fun \u0026lt;T\u0026gt; interceptContinuation(continuation: Continuation\u0026lt;T\u0026gt;): Continuation\u0026lt;T\u0026gt; } 协程的创建 public fun \u0026lt;T\u0026gt; (suspend () -\u0026gt; T).createCoroutine( completion: Continuation\u0026lt;T\u0026gt; ): Continuation\u0026lt;Unit\u0026gt; public fun \u0026lt;R, T\u0026gt; (suspend R.() -\u0026gt; T).createCoroutine( receiver: R, completion: Continuation\u0026lt;T\u0026gt; ): Continuation\u0026lt;Unit\u0026gt; public fun \u0026lt;T\u0026gt; (suspend () -\u0026gt; T).startCoroutine( completion: Continuation\u0026lt;T\u0026gt; ) public fun \u0026lt;R, T\u0026gt; (suspend R.() -\u0026gt; T).startCoroutine( receiver: R, completion: Continuation\u0026lt;T\u0026gt; ) suspend函数本身执行需要一个Continuation实例在恢复时调用，即参数：completion。 返回值Continuation\u0026lt;Unit\u0026gt;则是创建出来的协程的载体，receiver、suspend函数会被传递给该实例作为协程的实际执行体。 协程的启动模式 DEAFULT：立即开始调度协程体，调度前若取消则直接取消\nATOMIC：立即开始调度，且在第一个挂起点前不能被取消\nLAZY：只有在需要（start/join/await）时开始调度\nUNDISPATCHED：立即在当前线程执行协程体，指导遇到第一个挂起点，后面取决于调度器\n协程的调度器 调度器 JVM JavaScript Native Default 线程池 主线程循环 主线程循环 Main UI线程 主线程循环 主线程循环 Unconfined 直接执行 直接执行 直接执行 IO 线程池 \u0026ndash; \u0026ndash; 协程框架 Channel的概念 非阻塞的通信基础设施，类似于BlockingQueue+挂起函数。 分类 描述 RENDEZVOUS send调用后挂起直到receive到达 UNLIMITED 无限容量，send调用后直接返回 CONFLATED 保留最新，receive只能获得最近一次send的值 BUFFERED 默认容量，可通过kotlinx.coroutines.channels.defaultBuffer参数设置大小，默认为64 suspend fun main() { val channel = Channel\u0026lt;Int\u0026gt;() val producer = GlobalScope.launch { for (i in 0..5) { log(\u0026#34;send $i\u0026#34;) channel.send(i) log(\u0026#34;sent $i\u0026#34;) } channel.close() } val consumer = GlobalScope.launch { while (!channel.isClosedForReceive) { log(\u0026#34;receiving\u0026#34;) val i = channel.receiveCatching().getOrNull() log(\u0026#34;received $i\u0026#34;) } } producer.join() consumer.join() val receiver = GlobalScope.produce(capacity = Channel.UNLIMITED) { for (i in 0..5) { send(i) } } for (i in receiver) { println(i) } val sender = GlobalScope.actor\u0026lt;Int\u0026gt;(capacity = Channel.BUFFERED){ println(receive()) } for (i in 0..4) { sender.send(i) } } Select的概念 Select是一个IO多路复用的概念，协程的Select用于挂起函数的多路复用。 select总是按顺序检查事件，selectUnbiased会随机检查事件，对事件处理更公平。 Flow的概念 Flow是一个按顺序产生值的异步数据流。\nval intFlow = flow { emit(1) delay(100) emit(2) emit(3) } intFlow.collect(::println) listOf(1,2,3).asFlow() flowOf(1,2,3) val intChannel = Channel\u0026lt;Int\u0026gt;() intChannel.consumeAsFlow() 使用调度器\nintFlow .flowOn(Dispatchers.IO) .collect(::println) 异常处理\nflow { emit(1) throw ArithmeticException(\u0026#34;Div 0\u0026#34;) }.catch { it: Throwable -\u0026gt; // 捕获之前的异常（不包括取消异常） }.onCompletion { it: Throwable? -\u0026gt; // 前序flow完成调用时，无异常时t为null } Flow的取消\nFlow的运行依赖于协程，Flow的取消取决于collect所在协程的取消。 collect作为挂起函数可以响应所在协程的取消状态。 Back Pressure\nintFlow.buffer(capacity = 10).collect {} intFlow.conflate().collect {} intFlow.collectLatest { } ","permalink":"https://2d992714.xianglin2020-github-io.pages.dev/posts/2023/kotlin-basics/","summary":"Kotlin的基础知识学习和与Java的异同点比较。","title":"Kotlin基础"},{"content":"Java性能监控与调优 基于JDK命令行工具的监控 参考文档：https://docs.oracle.com/javase/8/docs/technotes/tools/unix/toc.html\nJVM参数类型 标准参数\n-version -help -cp -classpath -server -client X参数\n-Xint：解释执行 -Xcomp：第一次使用就编译成本地代码 -Xmixed：混合模式，JVM决定是否编译成本地代码 XX参数\nBoolean类型：-XX:[+ -]\u0026lt;name\u0026gt;表示启用或者禁用name属性，比如：-XX:+UseG1GC。 其它类型：-XX:\u0026lt;name\u0026gt;=\u0026lt;value\u0026gt;表示name属性的值是value，比如：-XX:MaxGCPauseMillis=500。 查看JVM运行时参数 -XX:+PrintFlagsInitial：查看虚拟机启动时使用的JVM参数。\n-XX:+PrintFlagsFinal：打印所有系统参数和值。\n-XX:+PrintVMOptions：打印传递给虚拟机的显示参数。\n-XX:+PrintCommandLineFlags：打印传递给虚拟机的所有参数。\njinfo：查看或设置JVM参数。\nhttps://docs.oracle.com/javase/8/docs/technotes/tools/unix/jinfo.html\njstat查看JVM统计信息 https://docs.oracle.com/javase/8/docs/technotes/tools/unix/jstat.html\noptions参数：\n-class：类加载信息。\n-compiler -printcompilation：Java HotSpot虚拟机JIT编译信息。\n-gc -gccapacity -gccause -gcmetacapacity -gcnew -gcnewcapacity -gcold -gcoldcapacity -gcutil：GC信息。\njmap和MAT定位内存溢出 通过不断创建对象和不断生成类来构造堆内存和元空间内存溢出情况：\n@RestController public class MemoryController { private final List\u0026lt;User\u0026gt; userList = new ArrayList\u0026lt;\u0026gt;(); /** * -Xmx32m -Xms32m */ @GetMapping(\u0026#34;/heap\u0026#34;) public String heap() { int i = 0; while (true) { userList.add(new User(i++, UUID.randomUUID().toString())); } } private final List\u0026lt;Class\u0026lt;?\u0026gt;\u0026gt; classList = new ArrayList\u0026lt;\u0026gt;(); /** * -XX:MetaspaceSize=32m -XX:MaxMetaspaceSize=32m */ @GetMapping(\u0026#34;/nonHeap\u0026#34;) public String nonHeap() { while (true) { classList.addAll(Metaspace.createClasses()); } } } 发生内存溢出时自动导出：\n-XX:+HeapDumpOnOutOfMemoryError\n-XX:HeapDumpPath=./\n使用jmap命令手动导出：\njmap -dump:format=b,file=heap.hprof 15927\nMemory Analyzer 下载地址：https://eclipse.dev/mat/downloads.php\n使用Memory Analyzer打开内存映像文件，默认展示可能存在内存泄漏的情况。\n前三个按钮分别表示：概要图、对象直方图、堆内存分配树。\n对象直方图可按类名称过滤，在User上右键查看对象的GC Roots，可以发现所有对象都由MemoryController.userList持有。\njstack和死循环与死锁 线程状态：https://docs.oracle.com/javase/8/docs/technotes/guides/troubleshoot/tooldescr034.html\nThread State Description NEW The thread has not yet started. RUNNABLE The thread is executing in the JVM. BLOCKED The thread is blocked waiting for a monitor lock. WAITING The thread is waiting indefinitely for another thread to perform a particular action. TIMED_WAITING The thread is waiting for another thread to perform an action for up to a specified waiting time. TERMINATED The thread has exited. 通过代码构造死循环和死锁情况：\n@RestController public class CPUController { /** * 死循环 */ @RequestMapping(\u0026#34;/loop\u0026#34;) public String loop() { return doLoop(1); } private String doLoop(int i) { while (i \u0026gt; 0) { ++i; --i; } return \u0026#34;completed\u0026#34;; } private final Object lock1 = new Object(); private final Object lock2 = new Object(); /** * 死锁 */ @RequestMapping(\u0026#34;/deadlock\u0026#34;) public String deadlock() { new Thread(() -\u0026gt; { synchronized (lock1) { try { Thread.sleep(1000); } catch (Exception ignored) { } synchronized (lock2) { System.out.println(\u0026#34;Thread1 over\u0026#34;); } } }).start(); new Thread(() -\u0026gt; { synchronized (lock2) { try { Thread.sleep(1000); } catch (Exception ignored) { } synchronized (lock1) { System.out.println(\u0026#34;Thread2 over\u0026#34;); } } }).start(); return \u0026#34;deadlock\u0026#34;; } } 使用top命令查看CPU负载情况，发现load average值达到6（https://unix.stackexchange.com/questions/9465/understanding-top-and-load-average），java进程占用CPU较高，使用top -p 5839 -H查看线程情况。\n使用jstack 5839 \u0026gt; 5839.txt导出Java线程信息，将十进制线程号5889转换为十六进制1701，在5839.txt中定位到对应的线程信息，即可发现问题代码。\njstack导出的Java线程信息会展示死锁相关信息。\n基于JVisualVM的可视化监控 VisualVM文档：https://visualvm.github.io/documentation.html\nJMX文档：https://www.oracle.com/technical-resources/articles/javase/jmx.html\njstatd文档：https://docs.oracle.com/javase/6/docs/technotes/tools/share/jstatd.html\n基于BTrace的监控调试 Btrace文档：https://github.com/btraceio/btrace\n下载安装BTrace 从https://github.com/btraceio/btrace/releases下载BTrace，可以选择将其加入环境变量。\n编写BTrace脚本 编写一个简单的脚本打印方法参数：\n@RestController @RequestMapping(\u0026#34;/ch4\u0026#34;) public class Ch4Controller { @GetMapping(\u0026#34;/arg1\u0026#34;) public String arg1(@RequestParam(\u0026#34;name\u0026#34;) String name) { return \u0026#34;hello \u0026#34; + name; } } // BTrace脚本 import org.openjdk.btrace.core.BTraceUtils; import org.openjdk.btrace.core.annotations.*; import org.openjdk.btrace.core.types.AnyType; @BTrace public class PrintArgSimple { @OnMethod( clazz = \u0026#34;store.xianglin.monitoring.chapter4.Ch4Controller\u0026#34;, method = \u0026#34;arg1\u0026#34;, location = @Location(Kind.ENTRY) ) public static void anyRead(@ProbeClassName String pcn, @ProbeMethodName String pmn, AnyType[] args) { BTraceUtils.printArray(args); BTraceUtils.println(pcn + \u0026#34;,\u0026#34; + pmn); BTraceUtils.println(); } } 运行BTrace脚本 使用\u0026lt;btrace\u0026gt;/bin/btrace \u0026lt;PID\u0026gt; \u0026lt;trace_script\u0026gt;命令格式来运行BTrace脚本：\n借助BTrace Workbench插件执行BTrace脚本 在VisualVM中安装BTrace Workbench插件。\n在目标应用上右键选择“Trace application”打开BTrace Workbench，在编辑框中编写BTrace脚本后点击“start”即可运行BTrace脚本。\nTomcat性能监控与调优 Tomcat远程Debug JDWP协议：https://docs.oracle.com/javase/8/docs/technotes/guides/troubleshoot/introclientissues005.html\n修改bin/startup.sh文件在启动命令上添加jpda参数。\n修改bin/catalina.sh文件中JPDA_ADDRESS配置。\n在IDEA中配置远程JVM调试，即可连接到Tomcat。\nTomcat-manager监控 文档地址：\u0026lt;tomcat\u0026gt;/docs/manager-howto.html\n修改conf/tomcat-users.xml文件中角色和用户的配置。\n\u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34;?\u0026gt; \u0026lt;tomcat-users xmlns=\u0026#34;http://tomcat.apache.org/xml\u0026#34; xmlns:xsi=\u0026#34;http://www.w3.org/2001/XMLSchema-instance\u0026#34; xsi:schemaLocation=\u0026#34;http://tomcat.apache.org/xml tomcat-users.xsd\u0026#34; version=\u0026#34;1.0\u0026#34;\u0026gt; \u0026lt;role rolename=\u0026#34;tomcat\u0026#34;/\u0026gt; \u0026lt;role rolename=\u0026#34;manager-status\u0026#34;/\u0026gt; \u0026lt;role rolename=\u0026#34;manager-gui\u0026#34;/\u0026gt; \u0026lt;user username=\u0026#34;tomcat\u0026#34; password=\u0026#34;tomcat\u0026#34; roles=\u0026#34;tomcat,manager-status,manager-gui\u0026#34;/\u0026gt; \u0026lt;/tomcat-users\u0026gt; 新建文件conf/Catalina/localhost/manager.xml增加如下配置：\n\u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34;?\u0026gt; \u0026lt;Context privileged=\u0026#34;true\u0026#34;\u0026gt; \u0026lt;Valve className=\u0026#34;org.apache.catalina.valves.RemoteAddrValve\u0026#34; allow=\u0026#34;127\\.0\\.0\\.1\u0026#34;/\u0026gt; \u0026lt;/Context\u0026gt; 重启Tomcat后打开http://127.0.0.1:8080/manager/html输入用户名和密码，即可进入管理界面。\npsi-probe监控 文档地址：https://github.com/psi-probe/psi-probe\n从releases页面下载对应的war文件，将其复制到Tomcat的webapps目录下即可，psi-probe使用的用户名密码和Tomcat-manager一致，需要提前配置。\nTomcat常用配置 Attribute Description docs maxConnections 服务器同一时间接收和处理的最大连接数。 docs/config/http.html acceptCount 当达到maxConnections时，系统为新请求提供的队列的最大长度。 docs/config/http.html maxThreads Connector创建请求处理线程的最大数量，决定了可以处理的最大并发请求数量。 docs/config/http.html minSpareThreads 始终保持运行的最小线程数，包括活动线程和空闲线程。 docs/config/http.html enableLookups 设置true时调用request.getRemoteHost()会执行DNS查找，以返回客户端实际主机名。 docs/config/http.html autoDeploy Tomcat是否在运行时定期检查新的或更新的web应用程序。 docs/config/host.html reloadable 设置true时会监视WEB-INF/classes和WEB-INF/lib中的类更新，并自动重新加载Web应用程序。 docs/config/context.html Nginx性能监控与调优 Nginx安装 Nginx文档：https://nginx.org/en/\n可以选择下载二进制文件安装或者通过包管理器安装。\nNginx 基础知识\nngx_http_stub_status监控连接信息 文档地址：https://nginx.org/en/docs/http/ngx_http_stub_status_module.html\n首先确认http_stub_status_module模块是否安装，使用nginx -V命令在输出的configure arguments中找到http_stub_status_module即表明模块已安装。\n在/etc/nginx/conf.d/default.conf中配置如下信息：\nlocation = /nginx_status { stub_status; access_log off; allow 127.0.0.1; deny all; } 访问nginx_status即可展示连接信息\nngxtob监控请求信息 文档地址：https://github.com/lebinh/ngxtop\n使用pip install ngxtop命令安装。\nnginx-rrd图像化监控 文档地址：htts://www.nginx.com/resources/wiki/modules/rrd_graph/\nnginx优化 配置线程数和并发数\n# 工作进程 worker_processes auto; events { # 每个进程打开的最大连接数，包括nginx与客户端和nginx与upstream的连接 worker_connections 1024; # 让nginx worker进程尽可能多地接受请求 multi_accept on; use epoll; } 配置与后端服务保持长连接，文档地址：http://nginx.org/en/docs/http/ngx_http_upstream_module.html#keepalive\nupstream http_backend { server 127.0.0.1:8080; keepalive 16; } server { location /http/ { proxy_pass http://http_backend; proxy_http_version 1.1; proxy_set_header Connection \u0026#34;\u0026#34;; } } 配置压缩：http://nginx.org/en/docs/http/ngx_http_gzip_module.html\nJVM层GC调优 垃圾收集器 GC调优指南：https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/toc.html\n如何选择垃圾收集器：https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/collectors.html\nG1最佳实践：https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/g1_gc_tuning.html#recommendations\n可视化GC日志分析工具 通用GC日志分析器：http://gceasy.io/\nGCViewer：https://github.com/chewiebug/GCViewer\n","permalink":"https://2d992714.xianglin2020-github-io.pages.dev/posts/2023/java-performance-monitoring-and-tuning/","summary":"生产环境可能存在的问题和常用的性能监控工具。","title":"Java性能监控与调优"},{"content":"JVM规范 JVM概述 JVM：Java Virtual Machine\n程序虚拟机：被设计用来在平台无关的环境中执行计算机程序。\nJVM是通过软件来模拟Java字节码的指令集，是Java程序的运行环境。\n参见：虚拟机、Java虚拟机\nJVM的主要功能 通过ClassLoader寻找和装载class文件 解释字节码成为指令并执行，提供class文件的运行环境 进行运行期间的内存分配和垃圾回收 提供与硬件交互的平台 JVM规范 Java SE Specifications\nJVM规范的作用 Java的虚拟机规范为不同的硬件平台提供了一种编译Java技术代码的规范 该规范使Java软件独立于平台，因为编译是针对Java虚拟机的，即保障了Java的平台无关性 JVM规范定义的主要内容 字节码指令集（相当于中央处理器CPU） Class文件格式 数据类型和值（范围、实现要求） 运行时数据区 栈帧 特殊方法 \u0026lt;init\u0026gt;：实例初始化方法，通过JVM的invokespecial指令来调用； \u0026lt;clinit\u0026gt;：类或接口的初始化方法，不包含参数，返回void。 虚拟机提供基础类库（应用程序自身无法实现） 反射 加载和创建类或接口，如ClassLoader 连接和初始化类和接口的类 安全，如security 多线程 弱引用 异常处理 虚拟机的启动、加载、链接和初始化 Class字节码解析 Class文件是JVM的输入，Java虚拟机规范中定义了Class文件的结构，Class文件是JVM实现平台无关、技术无关的基础\nClass文件是一组以8字节为单位的字节流，各个数据项目按顺序紧凑排列 对于占用空间大于8字节的数据项，按照高位在前的方式分割成多个8字节进行存储 Class文件格式里面只有两种类型：无符号数、表 无符号数：基本数据类型，以u1、u2、u4、u8来表示几个字节的无符号数 表：由多个无符号数和其它表构成的复合数据类型，通常以“_info”结尾 javap工具生成的非正式的“虚拟机汇编语言”。\nClassFile结构\nASM开发入门 ASM概述 ASM是一个Java字节码操纵框架，能用来动态生成类或者增强既有类的功能 ASM可以直接产生二进制class文件，也可以在类被加载入虚拟机之前动态改变类行为，ASM从类文件中读入信息后，能够改变类行为，分析类信息，甚至能根据要求生成新类 目前许多框架如cglib、Hibernate、Spring都直接或间接地使用ASM操作字节码 ASM编程模型 Core API：提供了基于事件形式的编程模型。该模型不需要一次性将整个类的结构读取到内存中，需要内存较少，但是这种编程方式难度较大。 Tree API：提供了基于树形的编程模型。该模型需要一次性将一个类的完整结构全部读取到内存中，需要更多的内存，但是这种编程方式较简单。 ASM Core API 中操纵字节码的功能基于ClassVisitor接口。这个接口中每个方法对应了class文件中的每一项。 ASM提供了三个基于ClassVisitor接口的类来实现class文件的生成和转换 ClassReader：解析一个类的class字节码 ClassAdapter：是ClassVisitor的实现类，实现要变化的功能 ClassWriter：是ClassVisitor的实现类，用来输出变化后的字节码 类加载、连接和初始化 类的生命周期 加载：查找并加载类文件的二进制数据 连接：将已经读入内存的类的二进制数据合并到JVM运行时环境中 验证：确保被加载类的正确性 准备：为类的静态变量分配内存并初始化为零值 解析：把常量池中的符号引用转换成直接引用 初始化：为类的静态变量赋初始值 类加载 通过类的全限定名来获取该类的二进制字节流 把二进制字节流转化为方法区的运行时数据结构 在堆上创建一个java.lang.Class对象，用来封装类在方法区内的数据结构，并向外提供了访问方法区内数据结构的接口 加载类的方式 从本地文件系统、jar等归档文件中加载 动态加载：将Java源文件动态编译成class文件 从网络、专有数据库加载 类加载器 JVM自带的加载器\n启动类加载器（BootstrapClassLoader）：用于加载启动基础模块类，比如：java.base、java.management、java.xml等 平台类加载器（PlatformClassLoader）：用于加载一些平台相关的模块，比如：java.scripting、java.compiler*、java.corba*等 应用程序类加载器（AppClassLoader）：用于加载应用级别的模块，比如：jdk.compiler、jdk.jartool、jdk.jshell等，还加载classpath路径中的所有类库 用户自定义加载器，是java.lang.ClassLoader的子类\nJDK8的类加载器\n启动类加载器：负责将\u0026lt;JAVA_HOME\u0026gt;/lib，或者-Xbootclasspath参数指定的路径中的，且是虚拟机识别的类库加载到内存中 扩展类加载器：负责加载\u0026lt;JRE_HOME\u0026gt;/lib/ext或者java.ext.dirs系统变量所指定路径中的所有类库 应用程序加载器：负责加载classpath路径中的所有类库 JVM规范允许类加载器在预料到某个类将要被使用的时候就预先加载它\n类加载器的关系 双亲委派模型 除了启用类加载器外，其余的类加载器都应该有自己的父级加载器，通过组合形成父子关系，工作过程如下\n一个类加载器收到类加载请求后，首先搜索它的内建加载器定义的所有“具名模块”\n如果找到了合适的模块定义，就会使用该加载器类加载\n如果class没有在这些加载器定义的具名模块中找到，那么将会委托给父级加载器，直到启动类加载器\n如果父级加载器反馈它不能完成加载请求，比如在它的搜索路径下找不到这个类，那子加载器才自己加载\n在类路径下找到的类将成为这些加载器的无名模块\n双亲委派模型对于保证Java程序的稳定运行很重要\n实现双亲委派的代码在java.lang.ClassLoader的loadClass()方法，如果自定义类加载器，推荐覆盖findClass()方法。\n类连接 类连接主要验证的内容 类文件结构检查：按照JVM规范规定的类文件结构进行检查 元数据验证：对字节码描述的信息进行语义分析，保证其符合Java语言规范的要求 字节码验证：通过对数据流和控制流进行分析，确保程序语义是合法和符合逻辑的，这里主要对方法体进行校验 符号引用验证：对类自身以外的信息，也就是常量池中的各种符号引用，进行匹配校验 类连接中的解析 把常量池中的符号引用转换为直接引用的过程，主要针对：类、接口、字段、类方法、接口方法、方法类型、方法句柄、调用点限定符\n符号引用：以一组无歧义的符号来描述所引用的目标，与虚拟机的实现无关。 直接引用：直接指向目标的指针，相对偏移量或是能间接定位到目标的句柄，是和虚拟机实现相关的。 类初始化 类的初始化就是为类的静态变量赋初始值，或者说是执行类构造器\u0026lt;clinit\u0026gt;方法的过程\n如果类还没有加载和连接，就先执行加载和连接 如果类存在父类，且父类没有初始化，就先初始化父类 如果类中存在初始化语句，就依次执行这些初始化语句 针对接口： 初始化一个类时，并不会先初始化它实现的接口（如果接口定义了默认方法，则会初始化接口） 初始化一个接口时，并不会初始化它的父接口 只有当程序首次使用接口中的变量或访问接口方法时，才会导致接口初始化 调用ClassLoader的loadClass方法，并不会初始化这个类 类初始化时机 JVM必须在每个类或接口“首次主动使用”时才初始化它们。\n主动使用类的时机包括：\n创建类实例 访问类或接口的静态变量 调用类的静态方法 通过反射访问类 虚拟机启动时运行的主类 类的卸载 当代表一个类的Class对象不再被引用，那么Class对象的声明周期就结束了，对应的在方法区中的数据也会被卸载。\nJVM自带类加载器加载的类是不会被卸载的，由用户自定义类加载器加载的类可以被卸载。\n内存分配 JVM的简化架构 程序寄存器（Program Counter Register）\n每个线程拥有一个PC寄存器，是线程私有的，记录正在执行的虚拟机字节码指令的地址 在创建线程的时候，创建相应的PC寄存器 执行本地方法时，PC寄存器的值为undefined 是一块较小的内存空间，是唯一一个在JVM规范中没有规定OutofMemory的区域 虚拟机栈\n栈由一系列帧（Frame）组成，是线程私有的 帧用来保存一个方法的局部变量、操作数栈、常量池指针、动态链接、方法返回值等 每一次方法调用时创建一个帧并压栈，退出方法时修改栈顶指针就可以把栈帧中的内容销毁 局部变量表存放了编译期可知的各种基本数据类型和引用类型，每个slot存放32位的数据，long、double占两个slot Java堆\n用来存放应用系统创建的对象和数组，所有线程共享Java堆 GC主要管理堆空间，对分代GC来说，堆也是分代的 方法区\n通常用来保存装载的类的结构信息，是线程共享的 JVM规范把方法区描述为堆的一个逻辑部分，但它有一个别名Non-heap，应该是为了与Java堆区分开 运行时常量池\n是Class文件中每个类或接口的常量池表在运行期间的表现形式，通常包括：类的版本、字段、方法、接口等信息 在方法区中分配，通常在加载类和接口到JVM后，就创建相应的运行时常量池 本地方法栈是JVM中用来支持native方法执行的栈\nJava堆内存 Java堆的结构 新生代用来存放新分配的对象，新生代中经过多次垃圾回收没有被回收掉的对象，会被复制到老年代 老年代存储一些大对象 永久代被元空间取代，元空间直接使用本地内存 对象的内存布局 以HotSpot为例：对象头、实例数据和对齐填充 对象头 Mark Word：存储自身的运行数据，如hashCode、GC年龄、锁状态标志等 类型指针：对象指向它的类元数据的指针 实例数据，真正存放对象实例数据的地方 对齐填充，仅仅是占位符，因为HotSpot要求对象起始地址都是8字节的整数倍，如果不是就需要对齐 对象的访问定位 JVM规范规定reference类型是一个指向对象的引用，对象的访问方式取决于JVM实现，目前主流的有使用句柄或使用指针两种方式。\n使用句柄：Java堆中会划分出一块内存来作为句柄池，reference中存储句柄的地址，句柄中存储对象实例数据和类元数据的地址\n使用指针：reference直接存放对象实例数据的地址，实例数据中保存类元数据的地址\nJava内存分配的参数 Xlog Configuration\nTrace跟踪参数 打印GC的简要信息：-Xlog:gc 打印GC的详细信息：-Xlog:gc* 指定GC log的位置，以文件输出：-Xlog:gc:filename 每一次GC后，都打印堆信息：-Xlog:gc+heap=debug G1日志格式 [0.090s][info][gc,start ] GC(0) Pause Young (Concurrent Start) (G1 Humongous Allocation) [0.096s][info][gc,heap,exit] garbage-first heap total 10240K, used 9315K [0x00000000ff600000, 0x0000000100000000) Java堆参数 Xms：初始堆大小，默认物理内存的1/64\nSets the minimum and the initial size (in bytes) of the heap. This value must be a multiple of 1024 and greater than 1 MB.\nXmx：最大堆大小，默认物理内存的1/4\nXmn：新生代大小，默认整个堆的3/8\n-XX:+HeapDumpOnOutOfMemoryError：OOM时导出堆到文件\n-XX:+HeapDumpPath：HeapDump的存放位置\n-XX:NewRatio：老年代与新生代的比值\nSets the ratio between young and old generation sizes. By default, this option is set to 2.\n-XX:SurvivorRatio：Eden区与Survivor区的比值\nSets the ratio between eden space size and survivor space size. By default, this option is set to 8.\nJava栈参数 -Xss：通常只有几百K，决定函数调用的深度 字节码执行引擎 字节码执行引擎概述 JVM的字节码执行引擎，功能是输入字节码文件，然后对字节码进行解析并处理，最后输出执行的结果 实现方式有通过解析器直接解释执行字节码，通过及时编译器产生本地代码编译执行，也可能两者皆有 栈帧 栈帧是用于支持JVM进行方法调用和方法执行的数据结构 栈帧随着方法调用而创建，随着方法结束而销毁 栈帧里存储了方法的局部变量、操作数栈、动态链接、方法返回地址等信息 局部变量表 用来存放方法参数和方法内部定义的局部变量的存储空间\n以变量槽slot为单位，目前一个slot存放32位以内的数据类型 对于实例方法，第0位slot存放的是this，然后从1到n依次分配给参数列表 根据方法体内部定义的变量顺序和作用域来分配slot slot是复用的，以节省栈帧空间 操作数栈 用来存放方法运行期间，各个指令操作的数据\n操作数栈中元素的数据类型必须和字节码指令的顺序严格匹配 虚拟机在实现栈帧的时候可能会做一些优化，让两个栈帧出现部分重叠区域，以存放公用的数据 动态链接 每个栈帧持有一个指向运行时常量池中该栈帧所属方法的引用，以支持方法调用过程的动态链接\n静态解析：类加载时符号引用转化为直接引用 动态链接：运行期间转换为直接引用 方法返回地址 方法执行后返回的地址\n方法调用 确定具体调用哪一个方法，并不涉及方法内部的执行过程\n部分方法直接在类加载的解析阶段，就确定了直接引用关系 对于实例方法（虚方法），因为重载和多态需要在运行期动态指派 分派 静态分派：所有依赖静态类型来定位方法执行版本的分配方式，比如重载方法 动态分配：根据运行期的实际类型来定位方法执行版本的分配方式，比如覆盖方法 垃圾回收 垃圾回收基础 内存中已经不再被使用到的内存空间就是垃圾。\n引用计数法：给对象添加一个引用计数器，有引用时计数器加一，引用失效时计数器减一。优点是实现简单、效率高，缺点是不能解决对象之间的循环引用。\n根搜索算法：从根节点向下搜索对象节点，搜索走过的路径称为引用链，如一个对象与根之间没有连通，则该对象不可用。\n能作为GC Roots的对象包括：虚拟机栈中引用的对象、方法区中类静态属性引用的对象、方法区中常量引用的对象、本地方法栈中JNI引用的对象。\n判断垃圾的步骤 根搜索算法判定对象不可用 判断是否有必要执行对象的finalize()方法 上述两步后仍未存在引用，就判定对象为垃圾 判断类无用 JVM中该类的所有实例都已经被回收\n加载该类的ClassLoader已经被回收\n没有任何地方引用该类的Class对象\n无法在任何地方通过反射访问这个类\n引用分类 强引用：类似A a = new A()，不会被回收\n软引用：有用但不必须的对象，是垃圾回收第二梯队的对象，在系统将要发生内存溢出异常前，会将这部分对象进行回收，用SoftReference来实现\n弱引用：非必须对象，比软引用还弱，垃圾回收时会回收掉，用WeakReference来实现\n虚引用：垃圾回收时会回收掉，无法通过虚引用来获取一个对象实例，用PhantomReference来实现\nGC类型 MinorGC/YoungGC：发生在新生代的收集动作 MajorGC/OldGC：发生在老年代的GC，目前只有CMS收集器有单独收集老年代的行为 MixedGC：收集整个新生代以及部分老年代，目前只有G1收集器会有这种行为 FullGC：收集整个Java堆和方法区的GC StopTheWorld STW是Java中一种全局暂停的现象，多半是由于GC引起。所有Java代码停止运行，native代码可以执行，但不能和JVM交互。其危害是长时间服务停止，没有响应。对于HA系统，可能引起主备切换，严重危害生产环境。\n垃圾收集类型 串行收集：GC单线程内存回收，会暂停所有的用户线程，如：Serial\n并行收集：多个GC线程并发工作，此时用户线程是暂停的，如：Parallel\n并发收集：用户线程和GC线程同时执行（不一定是并行，可能是交替执行），不需要停顿用户线程，如：CMS\n跨代引用 跨代引用：一个代中对象引用另一个代中的对象。\n跨代引用假说：跨代引用相对于同代引用来说只是极少数，存在相互引用关系的两个对象，应该是同时生存或同时消亡的。\n记忆集和写屏障 记忆集（Remembered Set）：一种用于记录从非收集区域指向收集区域的指针集合的抽象数据结构。\n卡表（Card Table）：是记忆集的一种具体实现，定义了记忆集的记录精度和与堆内存的映射关系，卡表的每个元素都对应着其标识的内存区域中一块特定大小的内存块，这个内存块称为卡页（Card Page）。\n写屏障可以看成是JVM对“引用类型字段赋值”这个动作的AOP，通过写屏障来实现当对象状态改变后，维护卡表状态。\n垃圾收集算法 标记清除法 标记清除算法（Mark-Sweep）分为标记和清除两个阶段，先标记出要回收的对象，然后统一回收这些对象。\n优点是简单。缺点是效率不高，标记清除后会产生大量不连续的内存碎片，从而导致在分配大对象时触发GC。\n复制算法 复制算法（Coping）：把内存分为两块完全相同的区域，每次使用其中一块。当一块分配完后，就把还存活的对象拷贝到另一块内存区域，然后把这块内存清除掉。\n优点是实现简单，运行高效，不用考虑内存碎片问题。缺点是只能使用一半内存。\n标记整理法 标记整理算法（Mark-Compact）：标记过程与标记清除算法一样，整理过程是让所有存活对象都向一端移动，然后直接清除边界以外的内存。\n垃圾收集器 串行收集器 Serial收集器/Serial Old收集器，是一个单线程的收集器，在垃圾收集时会Stop-the-World。\n优点是简单，对于单CPU，由于没有多线程的交互开销，可能更高效，是Client模式下默认的新生代收集器。\n使用-XX:+UseSerialGC来开启，使用Serial+Serial Old的收集器组合。\n并行收集器 ParNew收集器，使用多线程进行垃圾回收，在垃圾收集时会Stop-the-World。\n在并发能力好的CPU环境里，它停顿的时间要比串行收集器短。\n是Server模式下首选的新生代收集器，且能和CMS收集器配合使用。\n-XX:ParallelGCThreads：指定GC线程数\nSets the number of the stop-the-world (STW) worker threads. The default value depends on the number of CPUs available to the JVM and the garbage collector selected.\n新生代Parallel Scavenge收集器/Parallel Old收集器，是一款应用于新生代使用复制算法的并行收集器。但更关注吞吐量，能最高效的利用CPU，适合运行后台应用。使用-XX:+UseParallelGC来开启。\n-XX:MaxGCPauseMillis：设置GC的最大停顿时间\nSets a target for the maximum GC pause time (in milliseconds).\nCMS收集器 CMS（Concurrent Mark and Sweep）并发标记清除收集器分为：\n初始标记：指标及GC Roots能直接关联到的对象； 并发标记：进行GC Roots Tracing的过程； 重新标记：修正并发标记期间，因程序运行导致标记发生变化的那一部分； 并发清除：并发回收垃圾对象。 优点是低停顿，并发执行。缺点是并发执行对CPU资源压力大，无法处理在垃圾回收过程中产生的垃圾，采用的标记清除算法会导致大量碎片。\nG1收集器 G1（Garbage First）收集器是一款面向服务端应用的收集器，具有如下特点：\nG1把内存划分为多个独立的区域（Region）； G1仍然采用分代思想，保留了新生代和老年代。但它们不再是物理隔离的，而是一部分Region的集合，且不需要Region是连续的； G1能充分利用多CPU、多核环境硬件优势，尽量缩短STW； G1整体上采用标记整理算法，局部是通过复制算法，不会产生内存碎片； G1的停顿是可预测，能明确指定在一个时间段内消耗在垃圾收集的时间比例； G1跟踪各个Region里面垃圾堆的价值大小，在后台维护一个优先列表，每次根据允许的时间来回收价值最大的区域，从而保证在有限时间内的高效收集。 G1运行分为四个阶段：\n初始标记：只标记GC Roots能直接关联到的对象 并发标记：经行GC Roots Tracing的过程 最终标记：修正并发标记期间，因程序运行导致标记发生变化的那一部分对象 筛选回收：根据停顿时间来进行价值最大化的回收 ZGC收集器 JDK11加入的具有实验性质的低延迟收集器。\nZGC的设计目标是支持TB级别的内存容量，暂停时间低（\u0026lt;10ms），对整个程序吞吐量的影响小于15%。\nZGC里面的新技术：着色指针和读屏障。\nGC性能指标 吞吐量：应用代码执行时间/运行总时间；\nGC负荷：GC时间/运行总时间；\n暂停时间：发生STW的总时间；\nGC频率：GC在一个时间段发生的次数；\n反应速度：从对象成为垃圾到被回收的时间。\n高效并发 Java内存模型 JCP定义了一种Java内存模型，以前在JVM规范中，后来独立出来成为JSP-133（Java内存模型和线程规范修订）\n内存模型：在特定的操作协议下，对特定的内存或高速缓存进行读写访问过程的抽象。Java内存模型主要关注JVM中把变量值存储到内存和从内存中取出变量值这样的底层细节。\n所有变量（共享的）都存储在主内存中，每个线程都有自己的工作内存。工作内存中保存该线程使用到的变量的副本。\n线程对变量的所有操作（读写）都应该在工作内存中完成。\n不同线程不能相互访问工作内存，交互数据要通过主内存完成。\n内存间交互操作 Java内存模型规定了一些操作来实现内存间交互，JVM会保证其原子性：\nlock：锁定，把变量标识为线程独占，作用于主内存变量； unlock：解锁，把锁定的变量释放，别的线程才能使用，作用于主内存变量； read：读取，把变量值从主内存读取到工作内存中； load：载入，把read读取到的值放入工作内存的变量副本中； use：使用，把工作内存中一个变量的值传递给执行引擎； assign：赋值，把从执行引擎接收到的值赋给工作内存里面的变量； store：存储，把工作内存中一个变量的值传递到主内存中； write：写入，把store的数据存放到主内存的变量中。 线程可见性 可见性是指当一个线程修改了变量，其它线程可以获取被修改后的值。保证可见性的方法：volatile、synchronized、final。\nvolatile是JVM提供的最轻量级的同步机制，用volatile修饰的变量，对所有的线程可见，即对volatile变量所做的写操作能立即反映到其它线程中。\n用volatile修饰的变量，在多线程环境下仍然不是安全的。\nvolatile修饰的变量禁止指令重排优化。\n指令重排 指JVM为了优化，在条件允许的情况下，对指令进行一定的重新排列。直接运行当前能够立即执行的后续指令，避开获取下一条指令所需数据造成的等待。\n指令重排规则：\n程序顺序原则：一个线程内要保证语义的串行性；\nvolatile规则：volatile变量的写，先发生于读；\n锁规则：解锁必然发生在随后的加锁前；\n传递性：A先于B，B先于C，那么A先于C执行；\n线程的start()方法优先于它的每一个动作；\n线程的所有操作优先于线程的终结；\n线程的中断（interrupt()）先于被中断线程的代码；\n对象的构造函数执行结束先于finalize()方法。\n线程安全的处理方法 不可变对象是线程安全的； 互斥同步：synchronized、ReentrantLock； 非阻塞同步：基于冲突检查的乐观锁策略。 锁优化 自旋：如果线程可以很快获得锁，那么可以不在OS层挂起线程，而是让线程做几个忙循环。\n自适应自旋：自旋的时间不再固定，而是由前一次在同一个锁上的自旋时间和锁的拥有者状态来决定。\n如果锁被占用时间很短，线程能自旋成功，那么能节省线程挂起和线程切换时间，从而提升系统性能。\n锁消除：在编译代码的时候，检测到根本不存在共享数据竞争的情况自然无需同步加锁。\n逃逸分析，使用-XX:+DoEscapeAnalysis开启逃逸分析。\n如果一个方法中定义的一个对象，可能被外部方法引用，称为方法逃逸。 如果对象可能被其它外部线程访问，称为线程逃逸。 锁粗化：通常要求同步块要小，但一系列连续操作对同一个对象反复的加锁和解锁，会导致不必要的性能损耗。这种情况建议把锁同步的范围扩大到整个操作序列。\n轻量级锁：没有多线程竞争的情况下，使用类似乐观锁的方式减少使用OS实现互斥所产生的性能损耗。如果轻量级锁失败，表示存在竞争则升级为重量级锁。\n偏向锁：在无竞争情况下，锁偏向于当前已经占有锁的线程。获得偏向锁的线程，在将来进入同步块也不需要做同步。\n性能监控和故障处理工具 命令行工具 jps（JVM Process Status Tool）：主要用来输出JVM中运行的进程状态信息；\njinfo：打印给定进程或核心文件或远程调试服务器的配置信息；\njstack：主要用来查看某个Java进程内的线程堆栈信息；\njmap：用来查看堆内存使用情况；\njstat：JVM统计监测工具，用于查看JVM各个区内存和GC情况；\njstatd：虚拟机的jstat守护进程，主要用于监控JVM的创建与终止，并提供一个接口，以允许远程监视工具附加到本地系统上的JVM；\njcmd：JVM诊断命令工具，将诊断命令请求发送到正在运行的Java虚拟机。比如可以用来导出堆，查看Java进程，导出线程信息，执行GC等。\n图像化工具 jconsole：一个用于监视Java虚拟机的符合JMX的图像工具。它可以监视本地和远程JVM，还可以监视和管理应用程序。\njmc（JDK Mission Control）：Java任务控制客户端包括用于监视和管理Java应用程序的工具，优点是不会引入与这些类型工具相关联的性能开销。下载地址：https://www.oracle.com/java/technologies/jdk-mission-control.html。\n使用命令行工具记录JFR：jcmd 15592 JFR.start delay=10s duration=1m filename=\u0026quot;C:\\Users\\xiang\\Desktop\\log.jfr\u0026quot;\nVisualVM：提供有关在Java虚拟机中运行的基于Java技术的应用程序详细信息：内存和CPU分析、堆转存分析、内存泄漏、访问MBean和垃圾收集。下载地址：https://visualvm.github.io/。\n两种远程连接方式 JMX连接可以查看：系统信息、CPU使用情况，线程情况、手动执行GC等比较偏于系统级层面的信息；\nJMC 远程连接参数示例：\n# 有多个网卡时，通过 java.rmi.server.hostname 指定，不然连不上 jdk-21.0.5+11/bin/java -Dcom.sun.management.jmxremote=true -Dcom.sun.management.jmxremote.port=7091 -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Djava.rmi.server.hostname=192.168.56.103 -jar jvm-goods-0.0.1-SNAPSHOT.jar https://docs.oracle.com/html/E28023_02/mc_comm.htm#A1005288\nhttps://docs.oracle.com/en/java/java-components/jdk-mission-control/9/user-guide/faq.html#GUID-9B310BA1-A7FB-45AB-879D-F762E1AB34EF\njstatd连接可以提供：JVM内存分布详细信息、垃圾回收分布图、线程详细信息，以及某个对象使用内存的大小。\nJVM调优基础 JVM调优涉及的内容 内存方面\nJVM需要的内存总大小； 各块内存分配的大小（新生代、存活区、老年代等）； 选择合适的垃圾回收算法（垃圾收集器），控制GC停顿的次数和时间； 通过JVM监控信息查看热点代码，辅助分析解决内存泄漏问题（优化代码）。 线程方面\n通过检查线程死锁情况，辅助优化代码； Dump线程详细信息，查看线程内部运行情况，查找竞争线程，辅助代码优化； 检查系统哪些方法占用了大量CPU时间，辅助代码优化。 JVM调优步骤 监控JVM的状态，主要是内存、线程、代码和I/O部分； 分析监控信息，判断是否需要优化； 调整内容：垃圾收集器和内存分配、修改优化代码； 不断的重复“监控—分析—调整”三个步骤，直至达到调优的平衡点。 JVM调优目标 GC的时间足够小（单次GC时间和总时间）； GC的次数足够少； 将转移到老年代的对象数量降低到最小； 减少Full GC的执行时间； 发生Full GC的间隔足够长。 常见调优策略 减少创建对象的数量； 减少使用全局变量和大对象； 调整新生代、老年代大小到最合适； 选择合适的垃圾收集器，并设置合理的参数。 ","permalink":"https://2d992714.xianglin2020-github-io.pages.dev/posts/2023/jvm-basics/","summary":"JVM基础知识体系学习。","title":"JVM基础"},{"content":"初识Kafka 基本介绍 官网：https://kafka.apache.org/\nApache Kafka is an open-source distributed event streaming platform。 Kafka是基于zookeeper的分布式消息系统。 Kafka具有高吞吐量、高性能、实时性及高可靠等特点。 基本概念 Topic：一个虚拟概念，由1到多个Partition组成。 Partition：实际消息存储单位。 Producer：消息生产者。 Consumer：消息消费者。 Kafka安装 安装文件：\njdk-8u181-linux-x64.tar.gz\napache-zookeeper-3.5.7-bin.tar.gz\nkafka_2.11-2.4.0.tgz\n安装JDK 在~/.bashrc文件中增加如下内容，然后执行source ~/.bashrc命令。\nexport JAVA_HOME=\u0026lt;path\u0026gt;/jdk1.8.0_181 export PATH=$PATH:$JAVA_HOME/bin 安装zookeeper zookeeper默认配置文件是zoo.cfg，进入conf目录，将zoo_sample.cfg复制一份命名为zoo.cfg。\n进入bin目录，执行./zkServer.sh start命令启动zookeeper。\n安装Kafka 修改config/server.properties文件如下内容。\nlisteners=PLAINTEXT://192.168.31.114:9092 dvertised.listeners=PLAINTEXT://192.168.31.114:9092 zookeeper.connect=localhost:2181 Kafka基础操作 启动Kafka\nbin/kafka-server-start.sh config/server.properties \u0026amp; 停止Kafka\nbin/kafka-server-stop.sh 创建Topic\nbin/kafka-topics.sh --create --zookeeper localhost:2181 --replication-factor 1 --partitions 1 --topic jiangzh-topic 查看已创建的Topic信息\nbin/kafka-topics.sh --list --zookeeper localhost:2181 发送消息\nbin/kafka-console-producer.sh --broker-list 192.168.31.114:9092 --topic jiangzh-topic 接收消息\nbin/kafka-console-consumer.sh --bootstrap-server 192.168.31.114:9092 --topic jiangzh-topic --from-beginning Kafka客户端操作 示例代码：Kafka-study\nKafka客户端API类型 Admin Client API：允许管理和检测Topic、borker以及其它Kafka对象。 Producer API：发布消息到1个或多个topic。 Consumer API：订阅1个或多个topic，并处理产生的消息。 Streams API：高效地将输入流转换到输出流。 Connector API：从一些源系统或应用程序中拉取数据到Kafka。 Admin API 创建AdminClient\nProperties properties = new Properties(); properties.setProperty(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG, \u0026#34;192.168.31.114:9092\u0026#34;); try (AdminClient adminClient = AdminClient.create(properties)) { } 创建Topic\nNewTopic newTopic = new NewTopic(TOPIC_NAME, 1, (short) 1); CreateTopicsResult topics = adminClient.createTopics(Collections.singletonList(newTopic)); 查询Topic列表\nListTopicsOptions options = new ListTopicsOptions(); // 是否查看 internal topic options.listInternal(true); ListTopicsResult listTopicsResult = adminClient.listTopics(options); 删除Topic\nDeleteTopicsResult deleteTopicsResult = adminClient.deleteTopics(Collections.singletonList(TOPIC_NAME)); 查询Topic详细信息\nDescribeTopicsResult describeTopicsResult = adminClient.describeTopics(Collections.singletonList(TOPIC_NAME)); {jz-topic= (name=jz-topic, internal=false, partitions= (partition=0, leader=192.168.31.114:9092 (id: 0 rack: null), replicas=192.168.31.114:9092 (id: 0 rack: null), isr=192.168.31.114:9092 (id: 0 rack: null) ), authorizedOperations=[]) } 查看Config\nConfigResource configResource = new ConfigResource(ConfigResource.Type.TOPIC, TOPIC_NAME); DescribeConfigsResult describeConfigsResult = adminClient.describeConfigs(Collections.singletonList(configResource)); 修改Config\nConfigResource configResource = new ConfigResource(ConfigResource.Type.TOPIC, TOPIC_NAME); AlterConfigOp alterConfigOp = new AlterConfigOp(new ConfigEntry(\u0026#34;preallocate\u0026#34;, \u0026#34;true\u0026#34;), AlterConfigOp.OpType.SET); AlterConfigsResult alterConfigsResult1 = adminClient.incrementalAlterConfigs(Collections.singletonMap(configResource, Collections.singletonList(alterConfigOp))); 增加partition数量\nCreatePartitionsResult partitionsResult = adminClient.createPartitions(Collections.singletonMap(TOPIC_NAME, NewPartitions.increaseTo(2))); Producer Producer API 创建Producer\nProperties properties = new Properties(); properties.setProperty(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, \u0026#34;192.168.31.114:9092\u0026#34;); properties.setProperty(ProducerConfig.ACKS_CONFIG, \u0026#34;all\u0026#34;); properties.setProperty(ProducerConfig.LINGER_MS_CONFIG, \u0026#34;1\u0026#34;); properties.setProperty(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, \u0026#34;org.apache.kafka.common.serialization.StringSerializer\u0026#34;); properties.setProperty(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, \u0026#34;org.apache.kafka.common.serialization.StringSerializer\u0026#34;); try (Producer\u0026lt;String, String\u0026gt; producer = new KafkaProducer\u0026lt;\u0026gt;(properties)) { } 异步发送\nfor (int i = 0; i \u0026lt; 10; i++) { ProducerRecord\u0026lt;String, String\u0026gt; record = new ProducerRecord\u0026lt;\u0026gt;(TOPIC_NAME, \u0026#34;key-\u0026#34; + i, \u0026#34;value-\u0026#34; + i); producer.send(record); } 异步阻塞发送\nfor (int i = 0; i \u0026lt; 10; i++) { ProducerRecord\u0026lt;String, String\u0026gt; record = new ProducerRecord\u0026lt;\u0026gt;(TOPIC_NAME, \u0026#34;key-\u0026#34; + i, \u0026#34;value-\u0026#34; + i); Future\u0026lt;RecordMetadata\u0026gt; metadataFuture = producer.send(record); RecordMetadata metadata = metadataFuture.get(); System.out.println(metadata); } 带回调函数的异步发送\nfor (int i = 0; i \u0026lt; 10; i++) { ProducerRecord\u0026lt;String, String\u0026gt; record = new ProducerRecord\u0026lt;\u0026gt;(TOPIC_NAME, \u0026#34;key-\u0026#34; + i, \u0026#34;value-\u0026#34; + i); producer.send(record, (metadata, exception) -\u0026gt; { System.out.println(metadata); if (exception != null) { exception.printStackTrace(); } }); } 自定义partitioner\npublic class SamplePartitioner implements Partitioner { @Override public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster) { String keyStr = (String) key; String prefix = \u0026#34;key-\u0026#34;; if (keyStr.startsWith(prefix)) { Integer partitionCountForTopic = cluster.partitionCountForTopic(topic); int keyIndex = Integer.parseInt(keyStr.substring(prefix.length())); return keyIndex % partitionCountForTopic; } return 0; } ... } 修改配置partitioner.class传入自定义partitioner\nproperties.setProperty(ProducerConfig.PARTITIONER_CLASS_CONFIG, \u0026#34;com.example.kafka.producer.SamplePartitioner\u0026#34;); Producer源码分析 创建Producer：org.apache.kafka.clients.producer.KafkaProducer#KafkaProducer。\nKafkaProducer(Map\u0026lt;String, Object\u0026gt; configs, Serializer\u0026lt;K\u0026gt; keySerializer, Serializer\u0026lt;V\u0026gt; valueSerializer, ProducerMetadata metadata, KafkaClient kafkaClient, ProducerInterceptors interceptors, Time time) { ProducerConfig config = new ProducerConfig(ProducerConfig.addSerializerToConfig(configs, keySerializer, valueSerializer)); try { ... // 1、初始化metrics（性能监控） this.metrics = new Metrics(metricConfig, reporters, time); // 2、初始化partitioner（分区策略） this.partitioner = config.getConfiguredInstance(ProducerConfig.PARTITIONER_CLASS_CONFIG, Partitioner.class); ... // 3、初始化serializer（序列化） if (keySerializer == null) { this.keySerializer = config.getConfiguredInstance(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG,Serializer.class); this.keySerializer.configure(config.originals(), true); } else { config.ignore(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG); this.keySerializer = keySerializer; } if (valueSerializer == null) { this.valueSerializer = config.getConfiguredInstance(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG,Serializer.class); this.valueSerializer.configure(config.originals(), false); } else { config.ignore(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG); this.valueSerializer = valueSerializer; } ... // 4、初始化accumulator（计数器） this.accumulator = new RecordAccumulator(...); ... // 5、初始化sender、ioThread（守护线程） this.sender = newSender(logContext, kafkaClient, this.metadata); String ioThreadName = NETWORK_THREAD_PREFIX + \u0026#34; | \u0026#34; + clientId; this.ioThread = new KafkaThread(ioThreadName, this.sender, true); this.ioThread.start(); } catch (Throwable t) { ... } } 发送消息：org.apache.kafka.clients.producer.KafkaProducer#doSend。\nprivate Future\u0026lt;RecordMetadata\u0026gt; doSend(ProducerRecord\u0026lt;K, V\u0026gt; record, Callback callback) { TopicPartition tp = null; try { ... // 1、计算partition（分区） int partition = partition(record, serializedKey, serializedValue, cluster); tp = new TopicPartition(record.topic(), partition); ... // 2、往accumulator中追加记录 RecordAccumulator.RecordAppendResult result = accumulator.append(tp, timestamp, serializedKey, serializedValue, headers, interceptCallback, remainingWaitMs, true); ... // 3、唤醒sender（发送消息） if (result.batchIsFull || result.newBatchCreated) { log.trace(\u0026#34;Waking up the sender since topic {} partition {} is either full or getting a new batch\u0026#34;, record.topic(), partition); this.sender.wakeup(); } return result.future; } catch (ApiException e) { ... } } 从源码中得到如下信息：\nproducer是线程安全的。 producer是批量发送的。 消息传递保障 最多一次（0~1次） 至少一次（1~多次） 正好一次（有且仅有1次） Consumer Consumer API 创建comsumer\nProperties properties = new Properties(); properties.setProperty(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, \u0026#34;192.168.31.114:9092\u0026#34;); properties.setProperty(ConsumerConfig.GROUP_ID_CONFIG, \u0026#34;test\u0026#34;); properties.setProperty(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, \u0026#34;org.apache.kafka.common.serialization.StringDeserializer\u0026#34;); properties.setProperty(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, \u0026#34;org.apache.kafka.common.serialization.StringDeserializer\u0026#34;); // enable.auto.commit 设置为 true，允许自动提交 properties.setProperty(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, \u0026#34;true\u0026#34;); properties.setProperty(ConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG, \u0026#34;1000\u0026#34;); try (Consumer\u0026lt;String, String\u0026gt; consumer = new KafkaConsumer\u0026lt;\u0026gt;(properties)) { } 使用poll方法拉取数据，自动提交comsumer offset\n// properties.setProperty(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, \u0026#34;true\u0026#34;); // properties.setProperty(ConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG, \u0026#34;1000\u0026#34;); clientConsumer.subscribe(Collections.singletonList(TOPIC_NAME)); while (true) { ConsumerRecords\u0026lt;String, String\u0026gt; consumerRecords = clientConsumer.poll(Duration.ofMillis(10000)); consumerRecords.forEach(System.out::println); } 使用poll方法拉取数据，手动提交consumer offset\n// properties.setProperty(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, \u0026#34;false\u0026#34;); clientConsumer.subscribe(Collections.singletonList(TOPIC_NAME)); while (true) { ConsumerRecords\u0026lt;String, String\u0026gt; consumerRecords = clientConsumer.poll(Duration.ofMillis(10000)); consumerRecords.forEach(System.out::println); clientConsumer.commitAsync(); } 每个partition的消息分别处理，分别提交consumer offset\nconsumerClient.subscribe(Collections.singletonList(TOPIC_NAME)); while (true) { ConsumerRecords\u0026lt;String, String\u0026gt; consumerRecords = consumerClient.poll(Duration.ofMillis(10000)); // 按 partition 处理 for (TopicPartition topicPartition : consumerRecords.partitions()) { List\u0026lt;ConsumerRecord\u0026lt;String, String\u0026gt;\u0026gt; records = consumerRecords.records(topicPartition); if (!CollectionUtils.isEmpty(records)) { records.forEach(System.out::println); // 按 partition 提交 consumer offset long offset = records.get(records.size() - 1).offset(); consumerClient.commitSync(Collections.singletonMap(topicPartition, new OffsetAndMetadata(offset + 1))); } } } 指定订阅partition\n// 订阅指定partition TopicPartition topicPartition = new TopicPartition(TOPIC_NAME, 0); consumerClient.assign(Collections.singletonList(topicPartition)); ConsumerRecords\u0026lt;String, String\u0026gt; consumerRecords = consumerClient.poll(Duration.ofMillis(10000)); List\u0026lt;ConsumerRecord\u0026lt;String, String\u0026gt;\u0026gt; records = consumerRecords.records(topicPartition); records.forEach(System.out::println); // 按 partition 提交 consumer offset long offset = records.get(records.size() - 1).offset(); consumerClient.commitSync(Collections.singletonMap(topicPartition, new OffsetAndMetadata(offset + 1))); 使用多个consumer完成多线程处理\nint consumerSize = 2; ExecutorService executorService = Executors.newFixedThreadPool(consumerSize); for (int i = 0; i \u0026lt; consumerSize; i++) { int partition = i; executorService.execute(() -\u0026gt; consumer(consumerClient -\u0026gt; { TopicPartition topicPartition = new TopicPartition(TOPIC_NAME, partition); consumerClient.assign(Collections.singletonList(topicPartition)); ConsumerRecords\u0026lt;String, String\u0026gt; consumerRecords = consumerClient.poll(Duration.ofMillis(10000)); List\u0026lt;ConsumerRecord\u0026lt;String, String\u0026gt;\u0026gt; records = consumerRecords.records(topicPartition); records.forEach(r -\u0026gt; { System.out.println(Thread.currentThread().getName()); System.out.println(r); }); long offset = records.get(records.size() - 1).offset(); consumerClient.commitSync(Collections.singletonMap(topicPartition, new OffsetAndMetadata(offset + 1))); })); } 使用多个eventHandler完成多线程\nconsumer(consumerClient -\u0026gt; { ExecutorService executorService = Executors.newCachedThreadPool(); consumerClient.subscribe(Collections.singletonList(TOPIC_NAME)); ConsumerRecords\u0026lt;String, String\u0026gt; consumerRecords = consumerClient.poll(Duration.ofMillis(10000)); consumerRecords.forEach(r -\u0026gt; executorService.execute(() -\u0026gt; { System.out.println(Thread.currentThread().getName()); System.out.println(r); })); consumerClient.commitAsync(); }); 指定consumer offset\nconsumer(consumerClient -\u0026gt; { // 订阅指定partition TopicPartition topicPartition = new TopicPartition(TOPIC_NAME, 0); consumerClient.seek(topicPartition, offset); }); Consumer基本概念 Consumer Group\n单个Partition的消息只能由ConsumerGroup中某个Consumer消费； Consumer从Partition中消费消息是顺序的，默认从头开始消费； 单个ConsumerGroup会消费所有Partition中的消息。 Kafka Stream Kafka Stream基本概念 Kafka Stream是处理分析存储在Kafka数据的客户端程序库；\nKafka Stream通过state store可以实现高效状态操作；\n支持原语Processor和高层抽象DSL。\nKafka Stream关键词\n流及流处理器 流处理拓扑 源处理器及Sink处理器 Word Count Stream 使用Stream API构建一个Word Count程序\n定义两个topic，向streams-plaintext-input发送消息，订阅streams-plaintext-output的消息\nprivate static final String STREAM_INPUT_TOPIC = \u0026#34;streams-plaintext-input\u0026#34;; private static final String STREAM_OUT_TOPIC = \u0026#34;streams-plaintext-output\u0026#34;; private static void createTopic() { AdminSample.adminClient(adminClient -\u0026gt; { NewTopic outputTopic = new NewTopic(STREAM_OUT_TOPIC, 1, (short) 1); NewTopic inputTopic = new NewTopic(STREAM_INPUT_TOPIC, 1, (short) 1); adminClient.createTopics(Arrays.asList(inputTopic, outputTopic)); }); } 定义流计算过程\nprivate static Topology wordCountStream() { StreamsBuilder builder = new StreamsBuilder(); KStream\u0026lt;String, String\u0026gt; source = builder.stream(STREAM_INPUT_TOPIC); KTable\u0026lt;String, Long\u0026gt; count = source.flatMapValues(value -\u0026gt; Arrays.asList(value.toLowerCase(Locale.getDefault()).split(\u0026#34; \u0026#34;))) .groupBy((key, value) -\u0026gt; value) .count(); count.toStream().to(STREAM_OUT_TOPIC, Produced.with(Serdes.String(), Serdes.Long())); return builder.build(); } 创建KafkaStreams实例\ncreateTopic(); CountDownLatch latch = new CountDownLatch(1); Properties properties = new Properties(); properties.setProperty(StreamsConfig.BOOTSTRAP_SERVERS_CONFIG, \u0026#34;192.168.31.114:9092\u0026#34;); properties.setProperty(StreamsConfig.APPLICATION_ID_CONFIG, \u0026#34;wordCountApp\u0026#34;); properties.put(StreamsConfig.DEFAULT_KEY_SERDE_CLASS_CONFIG, Serdes.StringSerde.class); properties.put(StreamsConfig.DEFAULT_VALUE_SERDE_CLASS_CONFIG, Serdes.StringSerde.class); properties.put(StreamsConfig.CACHE_MAX_BYTES_BUFFERING_CONFIG, 0); properties.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, \u0026#34;earliest\u0026#34;); try (KafkaStreams streams = new KafkaStreams(wordCountStream(), properties)) { streams.start(); latch.await(); } Connect API Kafka Connect是Kafka流式计算的一部分； Kafka Connect主要用来与其它中间件建立流式通道； Kafka Connect支持流式和批处理集成。 Kafka集群 Kafka集群部署 Kafka天然支持集群；\nKafka集群依赖于Zookeeper进行协调；\nKafka主要通过brokerId区分不同节点。\n通过修改server.properties配置创建3个节点的伪集群\n# broker1 broker.id=0 listeners=PLAINTEXT://192.168.31.114:9092 dvertised.listeners=PLAINTEXT://192.168.31.114:9092 log.dirs=/tmp/kafka-logs0 # broker2 broker.id=1 listeners=PLAINTEXT://192.168.31.114:9093 dvertised.listeners=PLAINTEXT://192.168.31.114:9093 log.dirs=/tmp/kafka-logs1 # broker3 broker.id=2 listeners=PLAINTEXT://192.168.31.114:9094 dvertised.listeners=PLAINTEXT://192.168.31.114:9094 log.dirs=/tmp/kafka-logs2 Kafka副本集 Kafka副本集是指将日志复制多份；\nKafka可以为每个Topic设置副本集；\nKafka可以通过配置设置默认副本集数量。\n创建partition为3，replicationFactor为2的Topic\nNewTopic newTopic = new NewTopic(TOPIC_NAME, 3, (short) 2); adminClient.createTopics(Collections.singletonList(newTopic)); Kafka核心概念 Broker：一般指Kafka的部署节点；\nLeader：用于处理消息的接收和消费等请求；\nFollower：主要用于备份消息数据。\nKafka节点故障 Kafka与Zookeeper心跳未保持视为节点故障； follower消息落后leader太多也视为节点故障； Kafka会对故障节点进行移除。 Kafka节点故障处理 Kafka基本不会因为节点故障而丢失数据； Kafka的语义担保acks也很大程度上避免数据丢失； Kafka会对消息进行集群内平衡，减少消息在某些节点热度过高。 Leader选取 Kafka没有采用多数投票来选举Leader； Kafka会动态维护一组Leader数据的副本(ISR)； Kafka会在ISR中选择一个速度比较快的设为Leader。 Kafka监控 yahoo/CMAK EFAK Kafka概念 Kafka常见应用场景 日志收集或流式系统； 消息系统； 用户活动跟踪或运营指标监控； Kafka吞吐量大的原因 日志顺序读写和快速检索； Partition机制； 批量发送接收及数据压缩机制； 通过sendfile实现零拷贝原则； Kafka日志 日志结构 Kafka的日志是以Partition为单位进行保存的； 日志目录格式为Topic名称+数字； 日志文件格式是一个“日志条目”序列； 每条日志消息由4字节整形和N字节消息组成； message length : 4 bytes (value : 1+4+n) \u0026ldquo;magic\u0026rdquo; value : 1 byte CRC : 4 bytes payload : n bytes 日志存储与分段 每个Partition的日志会分为N个大小相等的segment中； 每个segment中消息数量不一定相等； 每个Partition只支持顺序读写； Partition会将消息添加到最后一个segmaent上； 当segment达到一定阈值后会flush到磁盘； segment文件分为两个部分：index和data； 日志读取操作 首先需要在存储数据中找出segment文件； 通过全局offset计算出segment中的offset； 通过index中的offset寻找具体数据内容； Kafka零拷贝 Kafka删除Topic的流程 ","permalink":"https://2d992714.xianglin2020-github-io.pages.dev/posts/2023/kafka-basics/","summary":"分布式事件流处理平台Kafka学习。","title":"Kafka基础"},{"content":"数据初始化 实验环境 Intel(R) Core(TM) i5-8250U CPU @ 1.60GHz\n5.15.0-73-generic #80~20.04.1-Ubuntu\nmysql Ver 8.0.33-0ubuntu0.20.04.2 for Linux on x86_64 ((Ubuntu))\nopenjdk version \u0026quot;19.0.2\u0026quot; 2023-01-17\n初始化 post 表结构见 post.sql，初始化程序见 gen.GenExecutor。\n初始磁盘使用空间 18G。\n数据初始化完成后磁盘使用空间 76G。\n写入 post 表记录总数 100000000 条。\n测试查询 从 1000000 开始取 2 条记录，耗时 1.66s。\n查询 user_id 为 7009336 的记录，耗时 2'43s。\nselect * from post where user_id = 7009336; 建立索引 尝试在 user_id 上创建索引，耗时 12'55s。\ncreate index `idx_user_id` on post(`user_id`); 再次查询 user_id 为 7009336 的记录，耗时降低到毫秒级。\n查看磁盘空间已使用 78G，索引大致占 2G。\n垂直拆分 使用 sql 复制数据 post 表中的 info 为 text 类型的字段，将其拆分为 post_info 表，将 post 表的数据复制到 post_info 中，语句见 post_info.sql。\n使用 iostat 工具查看磁盘读写速度。\n# 监控 nvme0n1，间隔 1s，打印 1000 次 iostat nvme0n1 1 1000 多线程复制 直接使用上述语句因为是单个事务操作，耗时过长，通过 Java 程序将其改为多线程复制，每次复制 1000000 条数据，磁盘的读写速度将近提升一倍，代码见 migration.CopyData。\n","permalink":"https://2d992714.xianglin2020-github-io.pages.dev/posts/2023/shardingsphere-hands-on/","summary":"\u003ch1 id=\"数据初始化\"\u003e数据初始化\u003c/h1\u003e\n\u003ch2 id=\"实验环境\"\u003e实验环境\u003c/h2\u003e\n\u003cp\u003e\u003ccode\u003eIntel(R) Core(TM) i5-8250U CPU @ 1.60GHz\u003c/code\u003e\u003c/p\u003e\n\u003cp\u003e\u003ccode\u003e5.15.0-73-generic #80~20.04.1-Ubuntu\u003c/code\u003e\u003c/p\u003e\n\u003cp\u003e\u003ccode\u003emysql  Ver 8.0.33-0ubuntu0.20.04.2 for Linux on x86_64 ((Ubuntu))\u003c/code\u003e\u003c/p\u003e\n\u003cp\u003e\u003ccode\u003eopenjdk version \u0026quot;19.0.2\u0026quot; 2023-01-17\u003c/code\u003e\u003c/p\u003e\n\u003ch2 id=\"初始化\"\u003e初始化\u003c/h2\u003e\n\u003cp\u003e\u003ccode\u003epost\u003c/code\u003e 表结构见 \u003ccode\u003epost.sql\u003c/code\u003e，初始化程序见 \u003ccode\u003egen.GenExecutor\u003c/code\u003e。\u003c/p\u003e\n\u003cp\u003e初始磁盘使用空间 \u003ccode\u003e18G\u003c/code\u003e。\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"https://cdn.jsdelivr.net/gh/xianglin2020/gallery@master//202307021650748.png\" alt=\"image-20230612202601520\"  /\u003e\n\u003c/p\u003e\n\u003cp\u003e数据初始化完成后磁盘使用空间 \u003ccode\u003e76G\u003c/code\u003e。\u003c/p\u003e","title":"ShardingSphere亿级分库分表"},{"content":"分布式搜索引擎概念 倒排序索引 Lucene Solr ElasticSearch Lucene Solr ElasticSearch ES 核心术语 Elasticsearch 是一个开源的分布式 RESTful 搜索和分析引擎，可用来集中存储数据，以便对形形色色、规模不一的数据进行搜索、索引和分析。\n索引index\n类型type\n文档document\n字段fields\n映射mapping\n近实时NRT\n节点node\nshard replica\nElasticsearch 安装 从二进制安装 参考文档：https://www.elastic.co/guide/en/elasticsearch/reference/8.7/targz.html\n下载 wget https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-8.7.0-linux-x86_64.tar.gz wget https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-8.7.0-linux-x86_64.tar.gz.sha512 shasum -a 512 -c elasticsearch-8.7.0-linux-x86_64.tar.gz.sha512 tar -xzf elasticsearch-8.7.0-linux-x86_64.tar.gz cd elasticsearch-8.7.0/ 调整配置 调整 config/elasticsearch.yml：\npath.data: /home/xianglin/Documents/elasticsearch-8.7.0/data path.logs: /home/xianglin/Documents/elasticsearch-8.7.0/data network.host: 0.0.0.0 新建 jvm 配置文件 config/jvm.options.d/heap_size.options ，配置堆大小：\n-Xms256m -Xmx256m 启动 ./bin/elasticsearch 验证 curl --cacert /home/xianglin/Documents/elasticsearch-8.7.0/config/certs/http_ca.crt -u elastic https://localhost:9200 Enter host password for user \u0026#39;elastic\u0026#39; 或者使用 postman：\n部分问题 bootstrap check failure [1] of [1]: max virtual memory areas vm.max_map_count [65530] is too low, increase to at least [262144]\n参考官方解答：https://www.elastic.co/guide/en/elasticsearch/reference/8.7/vm-max-map-count.html\nsudo sysctl -w vm.max_map_count=262144 从 Docker 安装 参考文档：https://www.elastic.co/guide/en/elasticsearch/reference/8.7/docker.html\n创建 Docker 网络 docker network create elastic 启动单节点集群 docker run -e ES_JAVA_OPTS=\u0026#34;-Xms256m -Xmx256m\u0026#34; -e \u0026#34;network.host=0.0.0.0\u0026#34; --name es01 --net elastic -p 9200:9200 -it docker.elastic.co/elasticsearch/elasticsearch:8.7.1 使用 docker-compose 安装 参照 https://github.com/deviantony/docker-elk 项目\n会部署 ElasticSearch、Logstash 和 Kibana 三个容器，如果暂不需要 Logstash，可以使用 docker compose up -d --scale logstash=0 命令或者直接在 docker-compose.yml 中注释掉 Logstash 相关内容。\n如果需要安装 IK 分词器，需要修改 Elasticsearch 的 Dockerfile 后，执行 docker compose build 重新构建镜像\n# elasticsearch/Dockerfile ARG ELASTIC_VERSION # https://www.docker.elastic.co/ FROM docker.elastic.co/elasticsearch/elasticsearch:${ELASTIC_VERSION:-8.17.0} # Add your elasticsearch plugins setup here # Example: RUN elasticsearch-plugin install analysis-icu RUN elasticsearch-plugin install --batch https://get.infini.cloud/elasticsearch/analysis-ik/${ELASTIC_VERSION:-8.17.0} 跳过授权步骤：https://www.elastic.co/guide/en/elasticsearch/plugins/8.17/_other_command_line_parameters.html#_batch_mode\n连接到 Elastic Cloud 打开 Elastic 项目控制台，获取连接信息和生成用于访问的 API 秘钥\n方便学习时测试，可以赋予 API KEY 所有权限\n{ \u0026#34;superuser_role\u0026#34;: { \u0026#34;cluster\u0026#34;: [ \u0026#34;all\u0026#34; ], \u0026#34;indices\u0026#34;: [ { \u0026#34;names\u0026#34;: [ \u0026#34;*\u0026#34; ], \u0026#34;privileges\u0026#34;: [ \u0026#34;all\u0026#34; ], \u0026#34;allow_restricted_indices\u0026#34;: false } ], \u0026#34;applications\u0026#34;: [], \u0026#34;run_as\u0026#34;: [], \u0026#34;metadata\u0026#34;: {}, \u0026#34;transient_metadata\u0026#34;: { \u0026#34;enabled\u0026#34;: true } } } 通过 Rest API 操作时携带认证信息\nGET /my-index/_doc/WLcaxJMBOBfKeuyFiYkr HTTP/1.1 Host: my-observability-project-f222f4.es.us-east-1.aws.elastic.cloud Authorization: ApiKey VzdjeXhKT**********************************b0tUQQ== 安装 Kibana 参考文档：https://www.elastic.co/guide/en/kibana/current/docker.html\ndocker run --name kib-01 --net elastic -p 5601:5601 docker.elastic.co/kibana/kibana:8.7.1 浏览器访问：http://192.168.31.67:5601/app/dev_tools#/console\n使用 enrollment token 连接 Elasticsearch\n输入 Elasticsearch 的用户名和密码即可。\n配置中文界面 参考文档：https://www.elastic.co/guide/en/kibana/current/settings.html\ni18n.locale\nSet this value to change the Kibana interface language. Valid locales are: en, zh-CN, ja-JP. Default: en\n修改 kibana.yml 配置 i18n.locale: \u0026quot;zh-CN\u0026quot; 即可。\nElasticsearch使用 使用 Kibana Console 完成基础操作 增删改查 # 往索引 twitter 中添加一条 id 为 1 的记录 POST twitter/_doc/1 { \u0026#34;user\u0026#34;: \u0026#34;GB\u0026#34;, \u0026#34;uid\u0026#34;: 1, \u0026#34;city\u0026#34;: \u0026#34;Beijing\u0026#34;, \u0026#34;province\u0026#34;: \u0026#34;Beijing\u0026#34;, \u0026#34;country\u0026#34;: \u0026#34;China\u0026#34; } DELETE twitter # 按 id 更新文档 PUT twitter/_doc/1 { \u0026#34;user\u0026#34;: \u0026#34;GB\u0026#34;, \u0026#34;uid\u0026#34;: 1, \u0026#34;city\u0026#34;: \u0026#34;北京\u0026#34;, \u0026#34;province\u0026#34;: \u0026#34;北京\u0026#34;, \u0026#34;country\u0026#34;: \u0026#34;中国\u0026#34;, \u0026#34;location\u0026#34;: { \u0026#34;lat\u0026#34;: \u0026#34;29.08\u0026#34;, \u0026#34;lon\u0026#34;: \u0026#34;111.35\u0026#34; } } # 按查询条件批量更新文档 POST twitter/_update_by_query { \u0026#34;script\u0026#34;: { \u0026#34;source\u0026#34;: \u0026#34;ctx._source.city = params.city;ctx._source.province = params.province;\u0026#34;, \u0026#34;lang\u0026#34;: \u0026#34;painless\u0026#34;, \u0026#34;params\u0026#34;: { \u0026#34;city\u0026#34;: \u0026#34;上海\u0026#34;, \u0026#34;province\u0026#34;: \u0026#34;上海\u0026#34; } }, \u0026#34;query\u0026#34;: { \u0026#34;match\u0026#34;: { \u0026#34;user\u0026#34;: \u0026#34;GB\u0026#34; } } } GET twitter/_doc/1 批量插入 POST _bulk {\u0026#34;index\u0026#34;:{\u0026#34;_index\u0026#34;:\u0026#34;twitter\u0026#34;}} {\u0026#34;user\u0026#34;:\u0026#34;双榆树-张三\u0026#34;,\u0026#34;message\u0026#34;:\u0026#34;今儿天气不错啊，出去转转去\u0026#34;,\u0026#34;uid\u0026#34;:2,\u0026#34;age\u0026#34;:20,\u0026#34;city\u0026#34;:\u0026#34;北京\u0026#34;,\u0026#34;province\u0026#34;:\u0026#34;北京\u0026#34;,\u0026#34;country\u0026#34;:\u0026#34;中国\u0026#34;,\u0026#34;address\u0026#34;:\u0026#34;中国北京市海淀区\u0026#34;,\u0026#34;location\u0026#34;:{\u0026#34;lat\u0026#34;:\u0026#34;39.970718\u0026#34;,\u0026#34;lon\u0026#34;:\u0026#34;116.326747\u0026#34;}} {\u0026#34;index\u0026#34;:{\u0026#34;_index\u0026#34;:\u0026#34;twitter\u0026#34;}} {\u0026#34;user\u0026#34;:\u0026#34;东城区-张三\u0026#34;,\u0026#34;message\u0026#34;:\u0026#34;出发，下一站云南！\u0026#34;,\u0026#34;uid\u0026#34;:3,\u0026#34;age\u0026#34;:30,\u0026#34;city\u0026#34;:\u0026#34;北京\u0026#34;,\u0026#34;province\u0026#34;:\u0026#34;北京\u0026#34;,\u0026#34;country\u0026#34;:\u0026#34;中国\u0026#34;,\u0026#34;address\u0026#34;:\u0026#34;中国北京市东城区台基厂三条3号\u0026#34;,\u0026#34;location\u0026#34;:{\u0026#34;lat\u0026#34;:\u0026#34;39.904313\u0026#34;,\u0026#34;lon\u0026#34;:\u0026#34;116.412754\u0026#34;}} {\u0026#34;index\u0026#34;:{\u0026#34;_index\u0026#34;:\u0026#34;twitter\u0026#34;}} {\u0026#34;user\u0026#34;:\u0026#34;东城区-李四\u0026#34;,\u0026#34;message\u0026#34;:\u0026#34;happy birthday!\u0026#34;,\u0026#34;uid\u0026#34;:4,\u0026#34;age\u0026#34;:30,\u0026#34;city\u0026#34;:\u0026#34;北京\u0026#34;,\u0026#34;province\u0026#34;:\u0026#34;北京\u0026#34;,\u0026#34;country\u0026#34;:\u0026#34;中国\u0026#34;,\u0026#34;address\u0026#34;:\u0026#34;中国北京市东城区\u0026#34;,\u0026#34;location\u0026#34;:{\u0026#34;lat\u0026#34;:\u0026#34;39.893801\u0026#34;,\u0026#34;lon\u0026#34;:\u0026#34;116.408986\u0026#34;}} {\u0026#34;index\u0026#34;:{\u0026#34;_index\u0026#34;:\u0026#34;twitter\u0026#34;}} {\u0026#34;user\u0026#34;:\u0026#34;朝阳区-老贾\u0026#34;,\u0026#34;message\u0026#34;:\u0026#34;123,gogogo\u0026#34;,\u0026#34;uid\u0026#34;:5,\u0026#34;age\u0026#34;:35,\u0026#34;city\u0026#34;:\u0026#34;北京\u0026#34;,\u0026#34;province\u0026#34;:\u0026#34;北京\u0026#34;,\u0026#34;country\u0026#34;:\u0026#34;中国\u0026#34;,\u0026#34;address\u0026#34;:\u0026#34;中国北京市朝阳区建国门\u0026#34;,\u0026#34;location\u0026#34;:{\u0026#34;lat\u0026#34;:\u0026#34;39.718256\u0026#34;,\u0026#34;lon\u0026#34;:\u0026#34;116.367910\u0026#34;}} {\u0026#34;index\u0026#34;:{\u0026#34;_index\u0026#34;:\u0026#34;twitter\u0026#34;}} {\u0026#34;user\u0026#34;:\u0026#34;朝阳区-老王\u0026#34;,\u0026#34;message\u0026#34;:\u0026#34;Happy Birthday My Friend!\u0026#34;,\u0026#34;uid\u0026#34;:6,\u0026#34;age\u0026#34;:50,\u0026#34;city\u0026#34;:\u0026#34;北京\u0026#34;,\u0026#34;province\u0026#34;:\u0026#34;北京\u0026#34;,\u0026#34;country\u0026#34;:\u0026#34;中国\u0026#34;,\u0026#34;address\u0026#34;:\u0026#34;中国北京市朝阳区国贸\u0026#34;,\u0026#34;location\u0026#34;:{\u0026#34;lat\u0026#34;:\u0026#34;39.918256\u0026#34;,\u0026#34;lon\u0026#34;:\u0026#34;116.467910\u0026#34;}} {\u0026#34;index\u0026#34;:{\u0026#34;_index\u0026#34;:\u0026#34;twitter\u0026#34;}} {\u0026#34;user\u0026#34;:\u0026#34;虹桥-老吴\u0026#34;,\u0026#34;message\u0026#34;:\u0026#34;好友来了都今天我生日，好友来了，什么 birthday happy 就成!\u0026#34;,\u0026#34;uid\u0026#34;:7,\u0026#34;age\u0026#34;:90,\u0026#34;city\u0026#34;:\u0026#34;上海\u0026#34;,\u0026#34;province\u0026#34;:\u0026#34;上海\u0026#34;,\u0026#34;country\u0026#34;:\u0026#34;中国\u0026#34;,\u0026#34;address\u0026#34;:\u0026#34;中国上海市闵行区\u0026#34;,\u0026#34;location\u0026#34;:{\u0026#34;lat\u0026#34;:\u0026#34;31.175927\u0026#34;,\u0026#34;lon\u0026#34;:\u0026#34;121.383328\u0026#34;}} Mapping 操作 GET twitter/_mapping { \u0026#34;twitter\u0026#34;: { \u0026#34;mappings\u0026#34;: { \u0026#34;properties\u0026#34;: { \u0026#34;address\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;text\u0026#34;, \u0026#34;fields\u0026#34;: { \u0026#34;keyword\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;keyword\u0026#34;, \u0026#34;ignore_above\u0026#34;: 256 } } } } } } } PUT twitter { \u0026#34;settings\u0026#34;: { \u0026#34;number_of_shards\u0026#34;: 1 } } // 指定 mapping PUT twitter/_mapping { \u0026#34;properties\u0026#34;: { \u0026#34;address\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;text\u0026#34;, \u0026#34;fields\u0026#34;: { \u0026#34;keyword\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;keyword\u0026#34;, \u0026#34;ignore_above\u0026#34;: 256 } } }, \u0026#34;city\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;keyword\u0026#34; }, \u0026#34;country\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;keyword\u0026#34; }, \u0026#34;location\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;geo_point\u0026#34; }, \u0026#34;province\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;keyword\u0026#34; }, \u0026#34;uid\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;long\u0026#34; }, \u0026#34;user\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;text\u0026#34;, \u0026#34;fields\u0026#34;: { \u0026#34;keyword\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;keyword\u0026#34;, \u0026#34;ignore_above\u0026#34;: 256 } } } } } 基本查询 GET twitter/_search { \u0026#34;query\u0026#34;: { \u0026#34;match\u0026#34;: { \u0026#34;city\u0026#34;: \u0026#34;北京\u0026#34; } } } GET twitter/_search { \u0026#34;query\u0026#34;: { \u0026#34;bool\u0026#34;: { \u0026#34;must\u0026#34;: [ { \u0026#34;match\u0026#34;: { \u0026#34;city\u0026#34;: \u0026#34;北京\u0026#34; } }, { \u0026#34;match\u0026#34;: { \u0026#34;age\u0026#34;: \u0026#34;30\u0026#34; } } ] } } } GET twitter/_search { \u0026#34;query\u0026#34;: { \u0026#34;bool\u0026#34;: { \u0026#34;must_not\u0026#34;: [ { \u0026#34;match\u0026#34;: { \u0026#34;city\u0026#34;: \u0026#34;北京\u0026#34; } } ] } } } GET twitter/_search { \u0026#34;query\u0026#34;: { \u0026#34;bool\u0026#34;: { \u0026#34;should\u0026#34;: [ { \u0026#34;match\u0026#34;: { \u0026#34;city\u0026#34;: \u0026#34;北京\u0026#34; } }, { \u0026#34;match\u0026#34;: { \u0026#34;city\u0026#34;: \u0026#34;上海\u0026#34; } } ] } } } GET twitter/_count { \u0026#34;query\u0026#34;: { \u0026#34;bool\u0026#34;: { \u0026#34;should\u0026#34;: [ { \u0026#34;match\u0026#34;: { \u0026#34;city\u0026#34;: \u0026#34;北京\u0026#34; } }, { \u0026#34;match\u0026#34;: { \u0026#34;city\u0026#34;: \u0026#34;上海\u0026#34; } } ] } } } GET twitter/_search { \u0026#34;query\u0026#34;: { \u0026#34;bool\u0026#34;: { \u0026#34;must\u0026#34;: [ { \u0026#34;match\u0026#34;: { \u0026#34;address\u0026#34;: \u0026#34;北京\u0026#34; } } ] } }, \u0026#34;post_filter\u0026#34;: { \u0026#34;geo_distance\u0026#34;: { \u0026#34;distance\u0026#34;: \u0026#34;5km\u0026#34;, \u0026#34;location\u0026#34;: { \u0026#34;lat\u0026#34;: 39.92, \u0026#34;lon\u0026#34;: 116.45 } } }, \u0026#34;sort\u0026#34;: [ { \u0026#34;_geo_distance\u0026#34;: { \u0026#34;location\u0026#34;: \u0026#34;39.92,116.45\u0026#34;, \u0026#34;order\u0026#34;: \u0026#34;desc\u0026#34;, \u0026#34;unit\u0026#34;: \u0026#34;km\u0026#34; } } ] } GET twitter/_search { \u0026#34;query\u0026#34;: { \u0026#34;range\u0026#34;: { \u0026#34;age\u0026#34;: { \u0026#34;gte\u0026#34;: 20, \u0026#34;lte\u0026#34;: 40 } } }, \u0026#34;sort\u0026#34;: [ { \u0026#34;age\u0026#34;: { \u0026#34;order\u0026#34;: \u0026#34;desc\u0026#34; } } ] } GET twitter/_search { \u0026#34;query\u0026#34;: { \u0026#34;match\u0026#34;: { \u0026#34;message\u0026#34;: \u0026#34;happy birthday\u0026#34; } } } # 注意大小写的自动处理 GET twitter/_search { \u0026#34;query\u0026#34;: { \u0026#34;match_phrase\u0026#34;: { \u0026#34;message\u0026#34;: \u0026#34;happy birthday\u0026#34; } } } GET twitter/_search { \u0026#34;query\u0026#34;: { \u0026#34;match_phrase\u0026#34;: { \u0026#34;message\u0026#34;: \u0026#34;happy birthday\u0026#34; } }, \u0026#34;highlight\u0026#34;: { \u0026#34;fields\u0026#34;: { \u0026#34;message\u0026#34;: {} } } } 聚合操作 GET twitter/_search { \u0026#34;size\u0026#34;: 0, \u0026#34;aggs\u0026#34;: { \u0026#34;age\u0026#34;: { \u0026#34;range\u0026#34;: { \u0026#34;field\u0026#34;: \u0026#34;age\u0026#34;, \u0026#34;ranges\u0026#34;: [ { \u0026#34;from\u0026#34;: 20, \u0026#34;to\u0026#34;: 30 }, { \u0026#34;from\u0026#34;: 30, \u0026#34;to\u0026#34;: 40 }, { \u0026#34;from\u0026#34;: 40, \u0026#34;to\u0026#34;: 50 } ] } } } } { \u0026#34;age\u0026#34;: { \u0026#34;buckets\u0026#34;: [ { \u0026#34;key\u0026#34;: \u0026#34;20.0-30.0\u0026#34;, \u0026#34;from\u0026#34;: 20, \u0026#34;to\u0026#34;: 30, \u0026#34;doc_count\u0026#34;: 1 }, { \u0026#34;key\u0026#34;: \u0026#34;30.0-40.0\u0026#34;, \u0026#34;from\u0026#34;: 30, \u0026#34;to\u0026#34;: 40, \u0026#34;doc_count\u0026#34;: 3 }, { \u0026#34;key\u0026#34;: \u0026#34;40.0-50.0\u0026#34;, \u0026#34;from\u0026#34;: 40, \u0026#34;to\u0026#34;: 50, \u0026#34;doc_count\u0026#34;: 0 } ] } } GET twitter/_search { \u0026#34;query\u0026#34;: { \u0026#34;match\u0026#34;: { \u0026#34;message\u0026#34;: \u0026#34;happy birthday\u0026#34; } }, \u0026#34;size\u0026#34;: 0, \u0026#34;aggs\u0026#34;: { \u0026#34;city\u0026#34;: { \u0026#34;terms\u0026#34;: { \u0026#34;field\u0026#34;: \u0026#34;city\u0026#34;, \u0026#34;size\u0026#34;: 10 } } } } { \u0026#34;city\u0026#34;: { \u0026#34;doc_count_error_upper_bound\u0026#34;: 0, \u0026#34;sum_other_doc_count\u0026#34;: 0, \u0026#34;buckets\u0026#34;: [ { \u0026#34;key\u0026#34;: \u0026#34;北京\u0026#34;, \u0026#34;doc_count\u0026#34;: 2 }, { \u0026#34;key\u0026#34;: \u0026#34;上海\u0026#34;, \u0026#34;doc_count\u0026#34;: 1 } ] } } 分词操作 # 标准分词器，按空格分词 GET _analyze { \u0026#34;text\u0026#34;: [ \u0026#34;Happy Birthday\u0026#34; ], \u0026#34;analyzer\u0026#34;: \u0026#34;standard\u0026#34; } { \u0026#34;tokens\u0026#34;: [ { \u0026#34;token\u0026#34;: \u0026#34;happy\u0026#34;, \u0026#34;start_offset\u0026#34;: 0, \u0026#34;end_offset\u0026#34;: 5, \u0026#34;type\u0026#34;: \u0026#34;\u0026lt;ALPHANUM\u0026gt;\u0026#34;, \u0026#34;position\u0026#34;: 0 }, { \u0026#34;token\u0026#34;: \u0026#34;birthday\u0026#34;, \u0026#34;start_offset\u0026#34;: 6, \u0026#34;end_offset\u0026#34;: 14, \u0026#34;type\u0026#34;: \u0026#34;\u0026lt;ALPHANUM\u0026gt;\u0026#34;, \u0026#34;position\u0026#34;: 1 } ] } # 分词后转化为小写 GET _analyze { \u0026#34;text\u0026#34;: [ \u0026#34;Happy Birthday\u0026#34; ], \u0026#34;tokenizer\u0026#34;: \u0026#34;standard\u0026#34;, \u0026#34;filter\u0026#34;: [ \u0026#34;lowercase\u0026#34; ] } { \u0026#34;tokens\u0026#34;: [ { \u0026#34;token\u0026#34;: \u0026#34;happy\u0026#34;, \u0026#34;start_offset\u0026#34;: 0, \u0026#34;end_offset\u0026#34;: 5, \u0026#34;type\u0026#34;: \u0026#34;\u0026lt;ALPHANUM\u0026gt;\u0026#34;, \u0026#34;position\u0026#34;: 0 }, { \u0026#34;token\u0026#34;: \u0026#34;birthday\u0026#34;, \u0026#34;start_offset\u0026#34;: 6, \u0026#34;end_offset\u0026#34;: 14, \u0026#34;type\u0026#34;: \u0026#34;\u0026lt;ALPHANUM\u0026gt;\u0026#34;, \u0026#34;position\u0026#34;: 1 } ] } 分词与内置分词器 standard、simple、whitespace、stop、keyword\nPOST http://192.168.31.67:9200/_analyze { \u0026#34;analyzer\u0026#34;: \u0026#34;standard\u0026#34;, \u0026#34;text\u0026#34;: \u0026#34;这是text 文本\u0026#34; } { \u0026#34;tokens\u0026#34;: [ { \u0026#34;token\u0026#34;: \u0026#34;这\u0026#34;, \u0026#34;start_offset\u0026#34;: 0, \u0026#34;end_offset\u0026#34;: 1, \u0026#34;type\u0026#34;: \u0026#34;\u0026lt;IDEOGRAPHIC\u0026gt;\u0026#34;, \u0026#34;position\u0026#34;: 0 }, { \u0026#34;token\u0026#34;: \u0026#34;是\u0026#34;, \u0026#34;start_offset\u0026#34;: 1, \u0026#34;end_offset\u0026#34;: 2, \u0026#34;type\u0026#34;: \u0026#34;\u0026lt;IDEOGRAPHIC\u0026gt;\u0026#34;, \u0026#34;position\u0026#34;: 1 }, { \u0026#34;token\u0026#34;: \u0026#34;text\u0026#34;, \u0026#34;start_offset\u0026#34;: 2, \u0026#34;end_offset\u0026#34;: 6, \u0026#34;type\u0026#34;: \u0026#34;\u0026lt;ALPHANUM\u0026gt;\u0026#34;, \u0026#34;position\u0026#34;: 2 }, { \u0026#34;token\u0026#34;: \u0026#34;文\u0026#34;, \u0026#34;start_offset\u0026#34;: 7, \u0026#34;end_offset\u0026#34;: 8, \u0026#34;type\u0026#34;: \u0026#34;\u0026lt;IDEOGRAPHIC\u0026gt;\u0026#34;, \u0026#34;position\u0026#34;: 3 }, { \u0026#34;token\u0026#34;: \u0026#34;本\u0026#34;, \u0026#34;start_offset\u0026#34;: 8, \u0026#34;end_offset\u0026#34;: 9, \u0026#34;type\u0026#34;: \u0026#34;\u0026lt;IDEOGRAPHIC\u0026gt;\u0026#34;, \u0026#34;position\u0026#34;: 4 } ] } ","permalink":"https://2d992714.xianglin2020-github-io.pages.dev/posts/2023/elasticsearch-basics/","summary":"分布式搜索引擎与ElasticSearch的基本使用。","title":"ElasticSearch 基础"},{"content":"源码下载编译 Spring 源码下载 在 GitHub 上打开 spring-framework 项目，从源码分支中找到 v5.2.0.RELEASE 标签，点击 Code 按钮下载源码压缩包。\n导入 IDEA 参照官网步骤将源码导入 IDEA 中。\n预编译 spring-oxm 模块，打开 spring 源码目录，在命名行中执行 ./gradlew :spring-oxm:compileTestJava 命令完成预编译。\n在 IDEA 中导入项目。\n排除 spring-aspects 模块\nSpring IOC 容器 容器初始化主要做的事情 解析配置\n定位与注册组件\n源码解析核心接口和类 Bean 和 BeanDefinition Bean 的本质就是 Java 对象，只是这个对象的生命周期由容器管理。根据配置，生成用来描述 Bean 的 BeanDefinition，常用属性：\n作用范围：scope（@Scope） 懒加载 lazy-init（@Lazy） 首选 primary（@Primary）：设置为 true 的 bean 会是优先的实现类 factory-bean 和 factory-method（@Configuration 和 @Bean） BeanFactory 组件扫描：Spring 自动发现应用容器中需要创建的 Bean。 自动装配：自动满足 Bean 之间的依赖。 ApplicationContext 基于 XML 配置的经典容器\nFileSystemXmlApplicationContext：从文件系统加载配置。\nClassPathXmlApplicationContext：从 classpath 加载配置。\nXmlWebApplicationContext：用于 WEB 应用程序的容器。\n基于注解的容器\nAnnotationConfigServletWebServerApplicationContext\nAnnotationConfigReactiveWebServerApplicationContext\nAnnotationConfigApplicationContext：基于注解的应用程序容器。\nResource ResourceLoader BeanDefinitionReader BeanDefinitionRegistry 容器初始化 后置处理器 PostProcessor 本身也是一种需要注册到容器里的 Bean，其里面的方法会在特定时机被容器调用。\n实现不改变容器或者 Bean 核心逻辑的情况下对 Bean 进行扩展：对 Bean 进行包装，影响其行为、修改 Bean 的内容。\n容器级别的后置处理器：BeanFactoryPostProcessor、BeanDefinitionRegistryPostProcessor；\nBean 级别的后置处理器：BeanPostProcessor。\nAware 一个标记接口，指示 Bean 实现接口后能通过对应的回调方法获取所需的容器对象。\n事件监听器 事件：ApplicationEvent 抽象类\n事件监听器：ApplicationListener\n事件发布器：Publisher 以及 Multicaster\nrefresh org.springframework.context.support.AbstractApplicationContext#refresh\nprepareRefresh：刷新前的准备工作，设置状态、设置启动时间 obtainFreshBeanFactory：获取子类刷新后的 BeanFactory 实例 prepareBeanFactory：为容器注册必要的系统级别的 bean postProcessBeanFactory：允许容器的子类去注册 PostProcessor invokeBeanFactoryPostProcessors：实例化并调用所有已注册的的 BeanFactoryPostProcessor registerBeanPostProcessor：注册Bean级别的后置处理器 initMessageSource：初始化国际化配置 initApplicationEventMulticaster：初始化事件发布者 onRefresh：预留给子类用于初始化其它特殊的 bean registerListeners：向事件发布者注册事件监听器 finishBeanFactoryInitialization：完成 BeanFactory 初始化，初始化所有非延迟加载的单例 finishRefresh：触发初始化完成的回调方法，发送容器刷新完毕事件 resetCommonCaches：重置Spring中的共用的缓存 Bean 实例创建 AbstractBeanFactory#doGetBean：获取 Bean 实例 DefaultSingletonBeanRegistry#getSingleton：获取单例实例（三级缓存） AbstractAutowireCapableBeanFactory#createBean：创建 Bean 实例 doCreateBean：创建 Bean 实例 applyMergedBeanDefinitionPostProcessors：处理 @Autowired 和 @Value 注解 populateBean：为 Bean 实例注入属性值 AutowiredAnnotationBeanPostProcessor#postProcessProperties：依赖注入逻辑 DefaultListableBeanFactory#doResolveDependency：依赖解析 DependencyDescriptor (InjectionPoint) ：创建依赖实例 doGetBean getSingleton：尝试从缓存获取 Bean 判断循环依赖 getParentBeanFactory：递归去父容器获取 Bean 实例 getMergedLocalBeanDefinition：从当前容器获取 BeanDefinition 实例 getDependsOn：递归实例化显示依赖的 Bean depends-on 根据不同的 Scope 采用不同的策略创建 Bean 实例 对 Bean 进行类型检查 createBean resolveBeanClass：Bean 类型解析 prepareMethodOverrides：处理方法覆盖 resolveBeforeInstantiation：应用 Bean 实例化前的后置处理器，如果生成对象则直接返回 doCreateBean：创建Bean的入口 doCreateBean createBeanInstance：创建 Bean 实例（工厂方法、含参构造器、无参构造器） applyMergedBeanDefinitionPostProcessors：记录下被 @Autowired 或者 @Value 标记上的方法和成员变量 addSingletonFactory：如果允许提前暴露，向三级缓存添加记录 populateBean：填充 Bean 属性 initializeBean：初始化 Bean registerDisposableBeanIfNecessary：注册 Bean 的销毁逻辑 Spring 单例循环依赖 Spring AOP AOP 概念 切面 Aspect：将横切关注点逻辑进行模块化封装的实体对象。 通知 Advice：好比是 Class 里面的方法，还定义了织入逻辑的时机。 连接点 Joinpoint：允许使用 Advice 的地方（Spring 只支持方法级别的 Joinpoint）。 切入点 Pointcut：定义一些列规则对 Joinpoint 进行筛选。 目标对象 Target：符合 Pointcut 条件，要被织入横切逻辑的对象。 织入：将 Aspect 模块化的横切关注点集成到 OOP 里。 织入器：完成织入过程的执行者，如 ajc。 Advice 的种类 BeforeAdvice AfterAdvice：好比 try……catch……finally 里面的 finally AfterReturning AfterThrowingAdvice AroundAdvice Aspect 执行顺序 单个 Aspect 的执行顺序\n多个 Aspect 的执行顺序\n引入型 Advice Introduction 为目标类引入新接口，不需要目标类做任何实现。新建一个接口以及实现类。\npublic interface ILittleUniverse { void burningUp(); } public class LittleUniverseImpl implements ILittleUniverse { @Override public void burningUp() { System.out.println(\u0026#34;burningUp\u0026#34;); } } 在切面类中配置 introduction。\n@Aspect @Component public class ServeAspect { @DeclareParents(value = \u0026#34;store.xianglin.controller..*\u0026#34;, defaultImpl = LittleUniverseImpl.class) public ILittleUniverse littleUniverse; } 使用 introduction。\nObject bean = applicationContext.getBean(WelcomeController.class); ILittleUniverse littleUniverse = (ILittleUniverse) bean; littleUniverse. burningUp(); Spring MVC 核心流程 建立请求和 Controller 方法的映射集合的流程； 根据请求查找对应的 Controller 方法的流程； 请求参数绑定到方法形参，执行方法处理请求，渲染视图的流程。 ","permalink":"https://2d992714.xianglin2020-github-io.pages.dev/posts/2023/spring-source-basics/","summary":"Spring v5.2.0.RELEASE 源码学习。","title":"Spring 源码学习"},{"content":"Vue3 基础 Vue3：https://cn.vuejs.org/。\n创建 Vue3 工程 使用 vue-cli 创建 使用如下命令\nvue create hello-vue3-cli 选择 [Vue 3] 即可\n使用 vite 创建 官网：https://cn.vitejs.dev/。\nvite 优势：\n开发环境中，无需打包操作，可快速冷启动。 轻量快速的热重载（HMR）。 真正的按需编译，不再等待整个应用编译完成， 使用如下命令（https://cn.vuejs.org/guide/quick-start.html#creating-a-vue-application）\nnpm init vue@latest 按命令行提示填入项目名称和选择可选功能即可。\n也可以使用如下命令（https://cn.vitejs.dev/guide/#scaffolding-your-first-vite-project）\nnpm create vite@latest vue-vite -- --template vue 常用 Composition API setup setup 是 Vue3 中一个新配置项，值为一个函数。\n组件中所用到的数据方法等均要配置在 setup 中。\nsetup 函数的两种返回值：\n返回对象，则对象中的属性方法在模板中可以直接使用。\n\u0026lt;template\u0026gt; \u0026lt;h1\u0026gt;一个人的信息\u0026lt;/h1\u0026gt; \u0026lt;h2\u0026gt;姓名：{{ name }}\u0026lt;/h2\u0026gt; \u0026lt;h2\u0026gt;年龄：{{ age }}\u0026lt;/h2\u0026gt; \u0026lt;button @click=\u0026#34;sayHello\u0026#34;\u0026gt;说话\u0026lt;/button\u0026gt; \u0026lt;/template\u0026gt; \u0026lt;script\u0026gt; export default { name: \u0026#39;App\u0026#39;, components: {}, setup() { // 数据 let name = \u0026#39;张三\u0026#39; let age = 18 // 方法 function sayHello() { alert(`姓名：${name}，年龄：${age}`) } return { name, age, sayHello, } } } \u0026lt;/script\u0026gt; 返回一个渲染函数，则可以自定义展示内容。\nimport {h} from \u0026#39;vue\u0026#39; return () =\u0026gt; { return h(\u0026#39;h1\u0026#39;, \u0026#39;渲染函数\u0026#39;) } setup 的执行时机，在 beforeCreate 之前执行，此时 this 的值是 undifined。\nsetup 的参数\nprops：值为对象，包含组件外部传递，且组件内使用 props 配置项声明接收的属性。 context：上下文对象： attrs：值对对象，包含组件外部传递，但没有在 props 配置中声明接收的属性。 slots：收到的插槽内容。 emit：触发自定义事件的函数，相当于 this.$emit。 ref 函数 作用：定义一个响应式数据。\n语法：const xxx = ref(initValue)\n创建一个包含响应式数据的引用对象。 JavaScript 中操作数据：xxx.value。 模版中读取数据不需要 .value，直接使用 \u0026lt;div\u0026gt;{{ xxx }}\u0026lt;/div\u0026gt; 备注：\n接收的参数可以是基本类型，也可以是对象类型。 基本类型的数据实现响应式依然借助 Object.defineProperty() 的 get 与 set 完成。 对象类型的数据实现响应式借助 Vue3 中的 reactive 函数。 reactive 函数 作用：定义一个对象类型的响应式数据。\n语法：const obj = reactive(srcObj) 接收一个对象或数组，返回一个代理对象（Proxy 对象）。\nreactive 定义的响应式数据是“深层次的”。\n内部基于 ES6 的 Proxy 实现，通过代理对象操作源对象的内部数据。\nVue3 响应式原理 通过 Proxy 拦截对象中任意属性的变化，包括属性值的读写、属性的添加删除等。\n通过 Reflect 对源对象的属性进行操作。\nlet person = { name: \u0026#39;张三\u0026#39;, age: 18 } const p = new Proxy(person, { get(target, propName) { console.log(`读取 p 的 ${propName} 属性`) return Reflect.get(target, propName) }, set(target, propName, newValue) { console.log(`修改或增加 p 的 ${propName} 属性`) return Reflect.set(target, propName, newValue) }, deleteProperty(target, propName) { console.log(`删除 p 的 ${propName} 属性`) return Reflect.deleteProperty(target, propName) } }) 计算属性与监视 computed 方法 \u0026lt;template\u0026gt; \u0026lt;h1\u0026gt;一个人的信息\u0026lt;/h1\u0026gt; 姓：\u0026lt;input type=\u0026#34;text\u0026#34; v-model=\u0026#34;person.firstName\u0026#34;\u0026gt; 名：\u0026lt;input type=\u0026#34;text\u0026#34; v-model=\u0026#34;person.lastName\u0026#34;\u0026gt; \u0026lt;br\u0026gt; \u0026lt;input type=\u0026#34;text\u0026#34; v-model=\u0026#34;person.fullName\u0026#34;\u0026gt; \u0026lt;/template\u0026gt; \u0026lt;script\u0026gt; import {reactive, computed} from \u0026#34;vue\u0026#34;; export default { name: \u0026#39;Demo\u0026#39;, setup() { let person = reactive({ firstName: \u0026#39;张\u0026#39;, lastName: \u0026#39;三\u0026#39; }) // 计算属性（简写） // person.fullName = computed(() =\u0026gt; { // return person.firstName + \u0026#39;-\u0026#39; + person.lastName // }) person.fullName = computed({ get() { return person.firstName + \u0026#39;-\u0026#39; + person.lastName }, set(value) { let arr = value.split(\u0026#39;-\u0026#39;); person.firstName = arr[0] person.lastName = arr[1] } }) return { person, } } } \u0026lt;/script\u0026gt; watch 方法 import {reactive, ref, watch} from \u0026#34;vue\u0026#34;; export default { name: \u0026#39;Demo\u0026#39;, setup() { let sum = ref(0) let msg = ref(\u0026#39;你好\u0026#39;) // 监视 ref 定义的一个响应式数据 watch(sum, (newVal, oldVal) =\u0026gt; { console.log(\u0026#39;sum 改变\u0026#39;, newVal, oldVal) }) // 监视 ref 定义的多个响应式数据 watch([sum, msg], (newVal, oldVal) =\u0026gt; { console.log(\u0026#39;sum/msg 改变\u0026#39;, newVal, oldVal) }) let person = reactive({ name: \u0026#39;张三\u0026#39;, age: 18, job: { j1: { salary: 20 } } }) // 监视 reactive 定义的响应式数据（深层监视），此处 newVal 和 oldVal 是相同的，因为它们是同一个对象 watch(person, (newVal, oldVal) =\u0026gt; { console.log(\u0026#39;person 改变\u0026#39;, newVal, oldVal) }) // 监视 reactive 定时响应式数据中的某些属性 watch(() =\u0026gt; person.job, (newVal, oldVal) =\u0026gt; { console.log(\u0026#39;person job 改变\u0026#39;, newVal, oldVal) }, { deep: true }) return { sum, msg, person } } } watchEffect 函数 watchEffect 会立即执行一遍回调函数，如果这时函数产生了副作用，Vue 会自动追踪副作用的依赖关系，自动分析出响应源。\n// 当 value 或 salary 变化时，再次执行回调 watchEffect(() =\u0026gt; { const x1 = sum.value const x2 = person.job.j1.salary console.log(\u0026#39;watchEffect\u0026#39;, x1, x2) }) 生命周期 配置式生命周期钩子\nbeforeDestroy \u0026ndash;\u0026gt; beforeUnmount、destroyed \u0026ndash;\u0026gt; Unmounted。\n组合式生命周期钩子\nimport { ref, onBeforeMount, onMounted, onBeforeUpdate, onUpdated, onBeforeUnmount, onUnmounted } from \u0026#34;vue\u0026#34;; export default { name: \u0026#39;Demo\u0026#39;, setup() { let sum = ref(0) // Composition API 生命周期 onBeforeMount(() =\u0026gt; { console.log(\u0026#39;onBeforeMount\u0026#39;) }) onMounted(() =\u0026gt; { console.log(\u0026#39;onMounted\u0026#39;) }) onBeforeUpdate(() =\u0026gt; { console.log(\u0026#39;onBeforeUpdate\u0026#39;) }) onUpdated(() =\u0026gt; { console.log(\u0026#39;onUpdated\u0026#39;) }) onBeforeUnmount(() =\u0026gt; { console.log(\u0026#39;onBeforeUnmount\u0026#39;) }) onUnmounted(() =\u0026gt; { console.log(\u0026#39;onUnmounted\u0026#39;) }) return { sum } }, // beforeCreate() { // console.log(\u0026#39;beforeCreate\u0026#39;) // }, // created() { // console.log(\u0026#39;created\u0026#39;) // }, // beforeMount() { // console.log(\u0026#39;beforeMount\u0026#39;) // }, // mounted() { // console.log(\u0026#39;mounted\u0026#39;) // }, // beforeUpdate() { // console.log(\u0026#39;beforeUpdate\u0026#39;) // }, // updated() { // console.log(\u0026#39;updated\u0026#39;) // }, // beforeUnmount() { // console.log(\u0026#39;beforeUnmount\u0026#39;) // }, // unmounted() { // console.log(\u0026#39;unmounted\u0026#39;) // } } 自定义 hook 函数 本质是一个函数，对 setup 函数中使用的 Composition API 进行封装。类似于 Vue2 中的 mixin，作用是复用代码，让 setup 中的逻辑更清晰。\n在 /src/hooks/usePoint.js 中定义一个 hook 函数\nimport {onBeforeUnmount, onMounted, reactive} from \u0026#34;vue\u0026#34;; export default function () { let point = reactive({ x: 0, y: 0 }) const savePoint = (e) =\u0026gt; { point.x = e.pageX point.y = e.pageY }; onMounted(() =\u0026gt; { window.addEventListener(\u0026#39;click\u0026#39;, savePoint) }) onBeforeUnmount(() =\u0026gt; { window.removeEventListener(\u0026#39;click\u0026#39;, savePoint) }) return point } 在组件中使用 hook\nimport {ref} from \u0026#34;vue\u0026#34;; import usePoint from \u0026#34;@/hooks/usePoint\u0026#34;; export default { name: \u0026#39;Demo\u0026#39;, setup() { let sum = ref(0) let point = usePoint(); return { sum, point } }, } toRef 函数 作用：创建一个 ref 对象（ObjectRefImpl），其 value 值执行另一个对象中的属性。\n语法：let salary = toRef(person.job.j1, 'salary')或者 let x = toRefs(person)。\n用于将响应式对象中的某些属性单独提供给外部使用。\n\u0026lt;template\u0026gt; \u0026lt;h4\u0026gt;{{ person }}\u0026lt;/h4\u0026gt; \u0026lt;h2\u0026gt;姓名：{{ name }}\u0026lt;/h2\u0026gt; \u0026lt;h2\u0026gt;年龄：{{ age }}\u0026lt;/h2\u0026gt; \u0026lt;h2\u0026gt;薪资：{{ job.j1.salary }}\u0026lt;/h2\u0026gt; \u0026lt;button @click=\u0026#34;name += \u0026#39;-\u0026#39;\u0026#34;\u0026gt;修改姓名\u0026lt;/button\u0026gt; \u0026lt;button @click=\u0026#34;age++\u0026#34;\u0026gt;增长年龄\u0026lt;/button\u0026gt; \u0026lt;button @click=\u0026#34;salary++\u0026#34;\u0026gt;涨薪\u0026lt;/button\u0026gt; \u0026lt;/template\u0026gt; \u0026lt;script\u0026gt; import {reactive, toRef, toRefs} from \u0026#34;vue\u0026#34;; export default { name: \u0026#39;Demo\u0026#39;, setup() { let person = reactive({ name: \u0026#39;张三\u0026#39;, age: 18, job: { j1: { salary: 20 } } }) // ObjectRefImpl let salary = toRef(person.job.j1, \u0026#39;salary\u0026#39;) let x = toRefs(person) return { person, salary, ...x } } } \u0026lt;/script\u0026gt; 其它 Composition API shallowReactive()：reactive() 的浅层作用形式，只有根级别的属性是响应式的。\nshallowRef()：ref() 的浅层作用形式，不会被深层递归地转为响应式只有对 .value 的访问是响应式的。\nreadonly()：接收一个对象或者 ref，返回一个原值的只读代理。\nshallowReadonly()：与 readonly() 相比，只有根层级的属性变为只读。\ntoRaw()：根据一个 Vue 创建的代理返回其原始对象，可以用于临时读取而不引起代理访问/跟踪开销，或是写入而不触发更改的特殊方法。\nmarkRaw()：将一个对象标记为不可被转为代理。\ncustomRef 作用：创建一个自定义的 ref，显式声明对其依赖追踪和更新触发的控制方式。\n文档地址：https://cn.vuejs.org/api/reactivity-advanced.html#customref。\n// 创建一个防抖 ref function myRef(value, delay) { let timer return customRef((track, trigger) =\u0026gt; { return { get() { track() return value }, set(newVal) { clearTimeout(timer) timer = setTimeout(() =\u0026gt; { value = newVal trigger() }, delay) } } }) } let keyword = myRef(\u0026#39;hello\u0026#39;, 500) 响应式数据判断 isRef()：检查某个值是否为 ref。\nisReactive()：检查一个对象是否是由 reactive() 或 shallowReactive() 创建的代理。\nisReadonly()：检查传入的值是否为只读对象，通过 readonly() 和 shallowReadonly() 创建的代理都是只读的。\nisProxy()：检查一个对象是否是由 reactive()、readonly()、shallowReactive() 或 shallowReadonly() 创建的代理。\nprovide 和 inject 作用：实现祖和后代组件间通信。\n父组件使用 provide 提供数据，后代组件中使用 inject 注入数据。\n// 父组件 let car = reactive({ name: \u0026#39;奔驰\u0026#39;, price: \u0026#39;40W\u0026#39; }) provide(\u0026#39;car\u0026#39;, car) // 后代组件 let car = inject(\u0026#39;car\u0026#39;) 组件 Fragment 在 Vue2 中组件必须有一个根标签，在 Vue3 中，组件可以没有根标签，内部会将多个标签包含在 Fragment 虚拟元素中。\nTeleport 可以将一个组件内部的一部分模版“传送”到该组件的 DOM 结构外层的位置去。\nTeleport 接收一个 to 选项来指定传送的目标。\n\u0026lt;button @click=\u0026#34;open = true\u0026#34;\u0026gt;Open Modal\u0026lt;/button\u0026gt; \u0026lt;Teleport to=\u0026#34;body\u0026#34;\u0026gt; \u0026lt;div v-if=\u0026#34;open\u0026#34; class=\u0026#34;modal\u0026#34;\u0026gt; \u0026lt;p\u0026gt;Hello from the modal!\u0026lt;/p\u0026gt; \u0026lt;button @click=\u0026#34;open = false\u0026#34;\u0026gt;Close\u0026lt;/button\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/Teleport\u0026gt; Suspense 作用：等待一部组件渲染时提供一些默认内容，让应用有更好的体验。\n异步引入组件\nimport {defineAsyncComponent} from \u0026#39;vue\u0026#39; const Child = defineAsyncComponent(() =\u0026gt; import(\u0026#39;./components/Child.vue\u0026#39;)) 使用 Suspense 包裹组件，并配置好 default 和 fallback 插槽的内容\n\u0026lt;Suspense\u0026gt; \u0026lt;template v-slot:default\u0026gt; \u0026lt;Child/\u0026gt; \u0026lt;/template\u0026gt; \u0026lt;template v-slot:fallback\u0026gt; \u0026lt;h3\u0026gt;加载中……\u0026lt;/h3\u0026gt; \u0026lt;/template\u0026gt; \u0026lt;/Suspense\u0026gt; ","permalink":"https://2d992714.xianglin2020-github-io.pages.dev/posts/2022/vue3-basic/","summary":"Vue3 基础知识与语法。","title":"Vue3 基础"},{"content":"Vuex 与 Vue2 匹配的 Vuex3：https://v3.vuex.vuejs.org/zh/。\n与 Vue3 匹配的 Vuex4：https://vuex.vuejs.org/zh/index.html。\n概念 在 Vue 中实现集中式状态（数据）管理的一个 Vue 插件，对 Vue 应用中多组件的共享状态进行集中式的管理（读/写），也是一种适用于任意组件间通信的方式。\nVuex 开发环境 安装 Vuex\nnpm i vuex@3 编写 /src/store/index.js 文件，创建 Vuex 中核心的 store\nimport Vuex from \u0026#39;vuex\u0026#39; import Vue from \u0026#34;vue\u0026#34;; // 应用 Vuex 插件 Vue.use(Vuex) const actions = {} const mutations = {} const state = {} // 导出 store export default new Vuex.Store({ actions, mutations, state }) 在 main.js 中创建 Vue 实例时传入 store 配置项\nimport store from \u0026#34;@/store\u0026#34;; new Vue({ el: \u0026#39;#app\u0026#39;, store, render: h =\u0026gt; h(App) }) 基本使用 使用 Vuex 完成求和功能。\n// Count.vue 组件 \u0026lt;template\u0026gt; \u0026lt;div\u0026gt; \u0026lt;h2\u0026gt;当前求和为{{ $store.state.sum }}\u0026lt;/h2\u0026gt; \u0026lt;button @click=\u0026#34;increment\u0026#34;\u0026gt;+\u0026lt;/button\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/template\u0026gt; \u0026lt;script\u0026gt; export default { name: \u0026#34;Count\u0026#34;, data() { return { n: 1 } }, methods: { increment() { this.$store.commit(\u0026#39;JIA\u0026#39;, this.n) }, incrementWait() { this.$store.dispatch(\u0026#39;jiaWait\u0026#39;, this.n) }, } } \u0026lt;/script\u0026gt; // store/index.js import Vuex from \u0026#39;vuex\u0026#39; import Vue from \u0026#34;vue\u0026#34;; Vue.use(Vuex) const actions = { jiaWait(context, value) { setTimeout(() =\u0026gt; { context.commit(\u0026#39;JIA\u0026#39;, value) }, 500) }, } const mutations = { JIA(state, value) { state.sum += value } } const state = { sum: 0 } // 导出 store export default new Vuex.Store({ actions, mutations, state }) 在组件中读取 Vuex 中的数据：$store.state.sum。\n在组件中修改 Vuex 中的数据：$store.dispatch(actionName, value) 或 $store.commit(mutationName, value)。\ngetters 的使用 当需要对 state 中数据加工后再使用时，在 Vuex 的 store 中定义 getter （可以认为是 store 的计算属性）。\nconst getters = { bigSum(state) { return state.sum * 10 } } export default new Vuex.Store({ ..., getters }) 在组件中访问 getters 属性 ：$store.getters.bigSum。\n辅助函数 使用 mapState 辅助函数生成计算属性，减少冗余代码。mapGetters 辅助函数将 store 中的 getter 映射为计算属性。\nimport {mapState, mapGetters} from \u0026#34;vuex\u0026#34;; computed: { // he() { // return this.$store.state.sum // }, ...mapState({he: \u0026#39;sum\u0026#39;}), // bigSum() { // return this.$store.getters.bigSum // }, ...mapGetters([\u0026#39;bigSum\u0026#39;]) } 使用 mapActions 辅助函数将组件的 methods 映射为 store.dispatch 调用，使用 mapMutations 辅助函数将组件中的 methods 映射为 store.commit 调用。\nmethods: { // increment() { // this.$store.commit(\u0026#39;JIA\u0026#39;, this.n) // }, ...mapMutations({\u0026#39;increment\u0026#39;: \u0026#39;JIA\u0026#39;}), // decrement() { // this.$store.commit(\u0026#39;JIAN\u0026#39;, this.n) // }, ...mapMutations([\u0026#39;JIAN\u0026#39;]), // incrementOdd() { // this.$store.dispatch(\u0026#39;jiaOdd\u0026#39;, this.n) // }, ...mapActions({\u0026#39;incrementOdd\u0026#39;: \u0026#39;jiaOdd\u0026#39;}), // incrementWait() { // this.$store.dispatch(\u0026#39;jiaWait\u0026#39;, this.n) // }, ...mapActions([\u0026#39;jiaWait\u0026#39;]) } 模块化和命名空间 Vuex 支持将 store 拆分成多个模块（module），每个模块拥有自己的 state 、mutation、action、getter，通过添加 namespaced:true 使模块成为带有命名空间的模块。\n修改 store.js 配置，以开启命名空间\nimport Vuex from \u0026#39;vuex\u0026#39; import Vue from \u0026#34;vue\u0026#34;; Vue.use(Vuex) const countOptions = { namespaced: true, actions: { jiaOdd(context, value) { if (context.state.sum % 2) { context.commit(\u0026#39;JIA\u0026#39;, value) } }, jiaWait(context, value) { setTimeout(() =\u0026gt; { context.commit(\u0026#39;JIA\u0026#39;, value) }, 500) }, }, mutations: { JIA(state, value) { state.sum += value }, JIAN(state, value) { state.sum -= value } }, state: { sum: 0, }, getters: { bigSum(state) { return state.sum * 10 } } } const personOptions = { namespaced: true, actions: {}, mutations: { ADD_PERSON(state, value) { state.personList.push(value) } }, state: { personList: [ {id: \u0026#39;001\u0026#39;, name: \u0026#39;张三\u0026#39;} ] }, getters: {} } // 导出 store export default new Vuex.Store({ modules: { countAbout: countOptions, personAbout: personOptions } }) 通过辅助函数使用时传入 namespace 参数\ncomputed: { ...mapState(\u0026#39;countAbout\u0026#39;, {he: \u0026#39;sum\u0026#39;}), ...mapState(\u0026#39;personAbout\u0026#39;, [\u0026#39;personList\u0026#39;]), ...mapGetters(\u0026#39;countAbout\u0026#39;, [\u0026#39;bigSum\u0026#39;]) }, methods: { ...mapMutations(\u0026#39;countAbout\u0026#39;, {\u0026#39;increment\u0026#39;: \u0026#39;JIA\u0026#39;}), ...mapMutations(\u0026#39;countAbout\u0026#39;, [\u0026#39;JIAN\u0026#39;]), ...mapActions(\u0026#39;countAbout\u0026#39;, {\u0026#39;incrementOdd\u0026#39;: \u0026#39;jiaOdd\u0026#39;}), ...mapActions(\u0026#39;countAbout\u0026#39;, [\u0026#39;jiaWait\u0026#39;]) } 不通过辅助函数时读取 state 数据\ncomputed: { sum() { return this.$store.state.countAbout.sum } } 不通过辅助函数访问 getters、actions、mutations\ncomputed: { bigSum(){ return this.$store.getters[\u0026#34;countAbout/bigSum\u0026#34;] } } this.$store.dispatch(\u0026#39;countAbout/jiaOdd\u0026#39;,value) this.$store.commit(\u0026#39;personAbout/ADD_PERSON\u0026#39;, value) Vue Router 与 Vue2 匹配的 Vue Router3 ：https://v3.router.vuejs.org/zh/。\n与 Vue3 匹配的 Vue Router4：https://router.vuejs.org/zh/。\n基础理解 一个路由（route）就是一组映射关系（key-value），多个路由需要路由器（router）进行管理，前端路由中：key 是路径，value 是组件。\n基本使用 安装 vue-router\nnpm i vue-router@3 在 /src/route/index.js 中创建路由器，编写 router 配置项\nimport VueRouter from \u0026#34;vue-router\u0026#34;; import About from \u0026#34;@/components/About.vue\u0026#34;; import Home from \u0026#34;@/components/Home.vue\u0026#34;; // 创建一个路由器 export default new VueRouter({ routes: [ { path: \u0026#39;/about\u0026#39;, component: About }, { path: \u0026#39;/home\u0026#39;, component: Home } ] }) 应用 VueRouter 插件，创建 Vue 实例时传入 router 配置项\nimport Vue from \u0026#39;vue\u0026#39; import App from \u0026#34;@/App.vue\u0026#34;; import VueRouter from \u0026#34;vue-router\u0026#34;; import router from \u0026#34;@/router\u0026#34;; Vue.use(VueRouter) new Vue({ el: \u0026#39;#app\u0026#39;, router, render: h =\u0026gt; h(App) }) 使用 \u0026lt;router-link\u0026gt; 实现路由切换，使用 \u0026lt;vue-view/\u0026gt; 指定展示位置\n\u0026lt;div class=\u0026#34;row\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;col-xs-2 col-xs-offset-2\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;list-group\u0026#34;\u0026gt; \u0026lt;!-- Vue 中使用 router-link 实现路由切换 --\u0026gt; \u0026lt;router-link to=\u0026#34;/about\u0026#34; active-class=\u0026#34;active\u0026#34; class=\u0026#34;list-group-item\u0026#34;\u0026gt;About\u0026lt;/router-link\u0026gt; \u0026lt;router-link to=\u0026#34;/home\u0026#34; active-class=\u0026#34;active\u0026#34; class=\u0026#34;list-group-item\u0026#34;\u0026gt;Home\u0026lt;/router-link\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;div class=\u0026#34;col-xs-6\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;panel\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;panel-body\u0026#34;\u0026gt; \u0026lt;!-- 指定组件的呈现位置 --\u0026gt; \u0026lt;router-view/\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; 注意点 路由组件通常存放在 pages 文件夹，一般组件通常存放在 components 文件夹。 路由切换时，组件默认被销毁，需要时重新挂载。 每个组件都有自己的 $route 属性，保存着自身的路由信息。 整个应用只有一个 router，通过 $router 属性获取。 多级路由 路由配置中使用 children 配置项\nimport VueRouter from \u0026#34;vue-router\u0026#34;; import Home from \u0026#34;@/pages/Home.vue\u0026#34;; import Message from \u0026#34;@/pages/Message.vue\u0026#34;; import MessageDetail from \u0026#34;@/pages/MessageDetail.vue\u0026#34;; // 创建一个路由器 export default new VueRouter({ routes: [ { path: \u0026#39;/home\u0026#39;, component: Home, children: [ { path: \u0026#39;message\u0026#39;, component: Message, children: [ { path: \u0026#39;detail\u0026#39;, component: MessageDetail } ] } ] } ] }) \u0026lt;router-link\u0026gt; 中使用全路径名称\n\u0026lt;router-link to=\u0026#34;/home/news\u0026#34;\u0026gt;News\u0026lt;/router-link\u0026gt; 路由传参 query 参数 字符串写法\n\u0026lt;router-link :to=\u0026#34;`/home/message/detail?id=${message.id}\u0026amp;title=${message.title}`\u0026#34;\u0026gt; {{ message.title }} \u0026lt;/router-link\u0026gt; 对象写法\n\u0026lt;router-link :to=\u0026#34;{ path: \u0026#39;/home/message/detail\u0026#39;, query: { id: message.id, title: message.title } }\u0026#34;\u0026gt; {{ message.title }} \u0026lt;/router-link\u0026gt; 接收参数\n\u0026lt;li\u0026gt;消息编号：{{ $route.query.id }}\u0026lt;/li\u0026gt; \u0026lt;li\u0026gt;消息标题：{{ $route.query.title}}\u0026lt;/li\u0026gt; params 参数 配置路由时，申明接收 params 参数\nimport VueRouter from \u0026#34;vue-router\u0026#34;; import Home from \u0026#34;@/pages/Home.vue\u0026#34;; import Message from \u0026#34;@/pages/Message.vue\u0026#34;; import MessageDetail from \u0026#34;@/pages/MessageDetail.vue\u0026#34;; // 创建一个路由器 export default new VueRouter({ routes: [ { path: \u0026#39;/home\u0026#39;, component: Home, children: [ { path: \u0026#39;message\u0026#39;, component: Message, children: [ { name: \u0026#39;xq\u0026#39;, path: \u0026#39;detail\u0026#39;, component: MessageDetail } ] } ] } ] }) 传递参数\n\u0026lt;router-link :to=\u0026#34;`/home/message/detail/${message.id}/${message.title}`\u0026#34;\u0026gt; {{ message.title }} \u0026lt;/router-link\u0026gt; \u0026lt;router-link :to=\u0026#34;{ name: \u0026#39;xq\u0026#39;, params: { id: message.id, title: message.title } }\u0026#34;\u0026gt; {{ message.title }} \u0026lt;/router-link\u0026gt; 接收参数\n\u0026lt;li\u0026gt;消息编号：{{ $route.params.id }}\u0026lt;/li\u0026gt; \u0026lt;li\u0026gt;消息标题：{{ $route.params.title}}\u0026lt;/li\u0026gt; 命名路由 命名路由用于简化路由配置，使用 name 配置项指定路由名称\nimport VueRouter from \u0026#34;vue-router\u0026#34;; import Home from \u0026#34;@/pages/Home.vue\u0026#34;; import Message from \u0026#34;@/pages/Message.vue\u0026#34;; import MessageDetail from \u0026#34;@/pages/MessageDetail.vue\u0026#34;; // 创建一个路由器 export default new VueRouter({ routes: [ { path: \u0026#39;/home\u0026#39;, component: Home, children: [ { path: \u0026#39;message\u0026#39;, component: Message, children: [ { name: \u0026#39;xq\u0026#39;, path: \u0026#39;detail\u0026#39;, component: MessageDetail } ] } ] } ] }) 在 \u0026lt;router-link\u0026gt; 中使用 name 配置项代替 path\n\u0026lt;router-link :to=\u0026#34;{ name: \u0026#39;xq\u0026#39;, query: { id: message.id, title: message.title } }\u0026#34;\u0026gt; {{ message.title }} \u0026lt;/router-link\u0026gt; 路由的 props 配置项 将组件和路由解耦，让组件更方便接收参数。\n{ name: \u0026#39;xq\u0026#39;, path: \u0026#39;detail/:id/:title\u0026#39;, component: MessageDetail, // props: true, // props: { // id: \u0026#39;id\u0026#39;, // title: \u0026#39;title\u0026#39; // }, props: function (route) { return { id: route.params.id, title: route.params.title } } } 布尔模式 如果 props 被设置为 true ，把路由收到的 params 参数以 props 形式传递给组件。\n对象模式 如果 props 是对象，该对象被按原样设置为组件属性。\n函数模式 函数返回值作为 props 传递给组件。\n编程式路由导航 使用 router 实例方法代替 \u0026lt;router-link\u0026gt; 创建 a 标签来定义导航链接。\n声明式 编程式 \u0026lt;router-link :to=\u0026quot;...\u0026quot;\u0026gt; router.push(...) \u0026lt;router-link :to=\u0026quot;...\u0026quot; replace\u0026gt; router.replace(...) pushShow(message) { this.$router.push({ name: \u0026#39;xq\u0026#39;, params: { id: message.id, title: message.title } }) }, replaceShow(message) { this.$router.replace({ name: \u0026#39;xq\u0026#39;, params: { id: message.id, title: message.title } }) } 解决 push 和 replace 多次跳转到统一位置报错（NavigationDuplicated）\nimport VueRouter from \u0026#34;vue-router\u0026#34;; const originalPush = VueRouter.prototype.push; const originalReplace = VueRouter.prototype.replace; VueRouter.prototype.push = function push(location) { return originalPush.call(this, location).catch(err =\u0026gt; { if (err.name !== \u0026#39;NavigationDuplicated\u0026#39;) throw err }); } VueRouter.prototype.replace = function replace(location) { return originalReplace.call(this, location).catch(err =\u0026gt; { if (err.name !== \u0026#39;NavigationDuplicated\u0026#39;) throw err }); } 缓存路由组件 让不展示的路由组件保持挂载，不被销毁。\n\u0026lt;keep-alive include=\u0026#34;News\u0026#34;\u0026gt; \u0026lt;router-view/\u0026gt; \u0026lt;/keep-alive\u0026gt; // 缓存多个路由组件 \u0026lt;keep-alive :include=\u0026#34;[\u0026#39;News\u0026#39;, \u0026#39;Message\u0026#39;]\u0026#34;\u0026gt; \u0026lt;router-view/\u0026gt; \u0026lt;/keep-alive\u0026gt; Vue Router 生命周期钩子 用于捕获路由组件的激活状态\nactivated：组件被激活时触发。 deactivated：组件失活是触发。 路由守卫 全局守卫 // 注册一个全局前置守卫 router.beforeEach((to, from, next) =\u0026gt; { console.log(\u0026#39;前置守卫\u0026#39;, to, from) if (to.meta.isAuth) { if (localStorage.getItem(\u0026#39;school\u0026#39;) === \u0026#39;school\u0026#39;) { next(); } } else { next(); } }) // 注册全局后置钩子 router.afterEach((to, from) =\u0026gt; { console.log(\u0026#39;后置守卫\u0026#39;, to, from) document.title = to.meta.title }) 当路由切换时，全局前置守卫按照创建顺序调用。\n独享守卫 const router = new VueRouter({ routes: [ { name: \u0026#39;xinwen\u0026#39;, path: \u0026#39;news\u0026#39;, component: News, meta: { isAuth: true, title: \u0026#39;新闻\u0026#39; }, beforeEnter(to, from, next) { console.log(\u0026#39;独享前置守卫\u0026#39;, to, from) if (localStorage.getItem(\u0026#39;school\u0026#39;) === \u0026#39;school\u0026#39;) { next(); } } } ] }) export default router 在路由配置上直接定义 beforeEnter 守卫。\n组件内守卫 // 通过路由规则，进入该组件时调用 beforeRouteEnter(to, from, next) { console.log(\u0026#39;beforeRouteEnter\u0026#39;) next() }, // 通过路由规则，离开该组件时调用 beforeRouteLeave(to, from, next) { console.log(\u0026#39;beforeRouteLeave\u0026#39;); next() } 完整流程 导航被触发。 在失活的组件里调用 beforeRouteLeave 守卫。 调用全局的 beforeEach 守卫。 在重用的组件里调用 beforeRouteUpdate 守卫 。 在路由配置里调用 beforeEnter。 解析异步路由组件。 在被激活的组件里调用 beforeRouteEnter。 调用全局的 beforeResolve 守卫。 导航被确认。 调用全局的 afterEach 钩子。 触发 DOM 更新。 调用 beforeRouteEnter 守卫中传给 next 的回调函数，创建好的组件实例会作为回调函数的参数传入。 ","permalink":"https://2d992714.xianglin2020-github-io.pages.dev/posts/2022/vuex-and-vuerouter/","summary":"Vuex 和 Vue Router 的使用。","title":"Vuex 和 Vue Router"},{"content":"组件是实现应用中局部功能代码和资源的集合，主要作用是复用代码，简化项目编码，提高运行效率。\n非单文件组件 一个文件中包含多个组件。\n\u0026lt;div id=\u0026#34;root\u0026#34;\u0026gt; {{msg}} \u0026lt;hr\u0026gt; \u0026lt;!-- 使用组件 --\u0026gt; \u0026lt;school\u0026gt;\u0026lt;/school\u0026gt; \u0026lt;hr\u0026gt; \u0026lt;student\u0026gt;\u0026lt;/student\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;script\u0026gt; Vue.config.productionTip = false // 定义组件 const school = Vue.extend({ template: ` \u0026lt;div\u0026gt; \u0026lt;h1\u0026gt;学校名称：{{ schoolName }}\u0026lt;/h1\u0026gt; \u0026lt;h1\u0026gt;学校地址：{{ address }}\u0026lt;/h1\u0026gt; \u0026lt;button @click=\u0026#34;showName\u0026#34;\u0026gt;显示学校名称\u0026lt;/button\u0026gt; \u0026lt;/div\u0026gt; `, data() { return { schoolName: \u0026#39;学校\u0026#39;, address: \u0026#39;地址\u0026#39; } }, methods: { showName() { alert(this.schoolName) } } }) const student = Vue.extend({ template: ` \u0026lt;div\u0026gt; \u0026lt;h2\u0026gt;学生姓名：{{ studentName }}\u0026lt;/h2\u0026gt; \u0026lt;h2\u0026gt;学生年龄：{{ age }}\u0026lt;/h2\u0026gt; \u0026lt;/div\u0026gt; `, data() { return { studentName: \u0026#39;姓名\u0026#39;, age: 18 } } }) // 注册组件 new Vue({ el: \u0026#39;#root\u0026#39;, components: { school, student }, data: { msg: \u0026#39;Hello Vue!\u0026#39; } }) \u0026lt;/script\u0026gt; 使用组件的步骤： 定义组件：使用 Vue.extend(options) 创建组件，使用 template 配置组件结构。 注册组件：使用 Vue.component(componentName, component) 全局注册组件，使用 components 选项局部注册组件。 使用组件：通过编写组件标签的方式使用组件。 VueComponent 组件本质是一个 Vue.extend 生成的名为 VueComponent 的构造函数。 Vue 在解析模版时会创建组件的实例对象，即调用 new VueComponent(options)。 每次调用 Vue.extend 都会产生一个全新的 VueComponent。 VueComponent.prototype.__proto__ === Vue.prototype ，让组件实例对象可以访问到 Vue 原型上的属性和方法。 单文件组件 一个文件中只包含一个组件。\n\u0026lt;template\u0026gt; \u0026lt;!--结构--\u0026gt; \u0026lt;/template\u0026gt; \u0026lt;script\u0026gt; // 交互 \u0026lt;/script\u0026gt; \u0026lt;style\u0026gt; /*样式*/ \u0026lt;/style\u0026gt; ref 属性 \u0026lt;template\u0026gt; \u0026lt;div\u0026gt; \u0026lt;h1 ref=\u0026#34;h1\u0026#34;\u0026gt;{{ msg }}\u0026lt;/h1\u0026gt; \u0026lt;button @click=\u0026#34;showDOM\u0026#34; ref=\u0026#34;btn\u0026#34;\u0026gt;展示 DOM 元素\u0026lt;/button\u0026gt; \u0026lt;school ref=\u0026#34;sch\u0026#34;\u0026gt;\u0026lt;/school\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/template\u0026gt; \u0026lt;script\u0026gt; import School from \u0026#34;@/components/School\u0026#34;; export default { name: \u0026#34;App\u0026#34;, components: {School}, data() { return { msg: \u0026#39;Vue\u0026#39; } }, methods: { showDOM() { console.log(this.$refs.h1) console.log(this.$refs.btn) console.log(this.$refs.sch) } } } \u0026lt;/script\u0026gt; ref 用来给元素或子组件注册引用信息，使用在 HTML 元素上，引用指向 DOM 元素，使用在子组件上，引用指向组件实例。\n注册方式：\n\u0026lt;h1 ref=\u0026#34;h1\u0026#34;\u0026gt;{{ msg }}\u0026lt;/h1\u0026gt; \u0026lt;school ref=\u0026#34;sch\u0026#34;\u0026gt;\u0026lt;/school\u0026gt; 使用方式：this.$refs[refName]。\n配置项 props \u0026lt;template\u0026gt; \u0026lt;div\u0026gt; \u0026lt;h1\u0026gt;{{ msg }}\u0026lt;/h1\u0026gt; \u0026lt;h2\u0026gt;姓名：{{ name }}\u0026lt;/h2\u0026gt; \u0026lt;h2\u0026gt;年龄：{{ myAge }}\u0026lt;/h2\u0026gt; \u0026lt;h2\u0026gt;性别：{{ sex }}\u0026lt;/h2\u0026gt; \u0026lt;button @click=\u0026#34;updateAge\u0026#34;\u0026gt;修改年龄\u0026lt;/button\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/template\u0026gt; \u0026lt;script\u0026gt; export default { name: \u0026#34;School\u0026#34;, data() { return { msg: \u0026#39;学生信息\u0026#39;, myAge: this.age } }, methods: { updateAge() { this.myAge++ } }, // props: [\u0026#39;name\u0026#39;, \u0026#39;age\u0026#39;, \u0026#39;sex\u0026#39;] // props: { // name: String, // age: Number, // sex: String // } props: { name: { type: String, required: true }, age: { type: Number, required: false, default: 18 }, sex: { type: String, required: true, // 自定义验证函数 validator: (value) =\u0026gt; { return \u0026#39;男\u0026#39; === value || \u0026#39;女\u0026#39; === value } } } } \u0026lt;/script\u0026gt; 功能：让组件接收父组件传递的数据。 传递数据：\n\u0026lt;demo name=\u0026#34;xxx\u0026#34;/\u0026gt; 接收数据：\n简单声明接收\nprops:[\u0026#39;name\u0026#39;] 限制类型\nprops:{ name:String } 类型检测、自定义验证和默认值\nprops:{ name:{ type: String, required: false, default: \u0026#39;老王\u0026#39; } } props 是只读的，对 props 修改会触发警告，可以通过复制 props 内容到 data 中修改。\nmixin（混入） \u0026lt;script\u0026gt; import {mixin} from \u0026#34;@/mixin\u0026#34;; export default { name: \u0026#34;School\u0026#34;, data() { return { name: \u0026#39;学生\u0026#39;, } }, mixins: [mixin] } \u0026lt;/script\u0026gt; import {mixin2} from \u0026#34;@/mixin\u0026#34;; Vue.mixin(mixin2) 可以把多个组件公用的配置提取成一个混入对象。\n定义混合对象：\nexport const mixin = { methods: { showName() { alert(this.name) } }, data() { return { x: 1, y: 2 } }, } export const mixin2 = { mounted() { console.log(\u0026#39;mixin mounted\u0026#39;) } } 使用混入：\n全局配置：Vue.mixin() 局部配置：mixins:[] 插件 用于增强 Vue，本质是包含 install 方法的一个对象。install 方法第一个参数是 Vue，后续参数是插件使用者传递的数据。\n定义插件：\n{ install(Vue, options) { Vue.filter() Vue.prototype.hello = () =\u0026gt; { } } } 使用插件：\nVue.use(plugins) scoped 属性 当 style 标签有 scoped 属性时，它的 CSS 只作用于当前组件中的元素。\n\u0026lt;style scoped\u0026gt; .demo { background-color: pink; } \u0026lt;/style\u0026gt; 添加 scoped Vue 编译时会添加一个唯一的 data 属性（data-v-22321ebb）为组件内 CSS 指定作用域，.demo 会被编译为 .demo[data-v-22321ebb] { background-color: pink; }。\nscoped 原理：\n给 HTML 的 DOM 节点加一个不重复的 data 属性标识唯一性。 给每句 css 选择器的末尾加一个当前组件的 data 属性选择器来私有化样式。 如果组件内包含其它组件，只会给组件最外层标签加上当前组件的 data 属性。 scoped 穿透\n如果需要父组件的样式覆盖子组件的样式有两种方式：\n使用两个 style，一个用于私有样式，一个用于共有样式。\n\u0026lt;style scoped\u0026gt; .basic { background-color: blue; } \u0026lt;/style\u0026gt; \u0026lt;style\u0026gt; .title { font-size: 40px; } \u0026lt;/style\u0026gt; 深度作用选择器（\u0026gt;\u0026gt;\u0026gt; 、 /deep/ 或 ::v-deep）\n\u0026lt;style scoped\u0026gt; .basic { background-color: blue; } \u0026gt;\u0026gt;\u0026gt; .title { font-size: 40px; } \u0026lt;/style\u0026gt; 上述代码会编译成：\n\u0026lt;style type=\u0026#34;text/css\u0026#34;\u0026gt; .basic[data-v-7ba5bd90] { background-color: blue; } [data-v-7ba5bd90] .title { font-size: 40px; } \u0026lt;/style\u0026gt; 自定义事件 自定义事件是一种组件间通信方式，适用于子组件向父组件传递数据。\n绑定自定义事件的方式：\n在父组件中通过 v-on: 绑定事件\n\u0026lt;Student @showStudentName=\u0026#34;showStudentName\u0026#34;/\u0026gt; 在父组件中通过 $on 函数绑定事件\nthis.$refs.student.$on(\u0026#39;showStudentName\u0026#39;, this.showStudentName) // 使用箭头函数保证 this 指向当前实例对象 this.$refs.student.$once(\u0026#39;method\u0026#39;, (value) =\u0026gt; {this.name = value}) 如果希望自定义事件只能触发一次，可以使用 once 修饰符，或者使用 $once 方法。\n触发自定义事件的方式：\nthis.$emit(\u0026#39;showStudentName\u0026#39;, this.name) 解绑自定义事件的方式：\nthis.$off(\u0026#39;showStudentName\u0026#39;) this.$off([\u0026#39;m1\u0026#39;, \u0026#39;m2\u0026#39;]) 组件上可以绑定原生 DOM 事件，但需要使用 native 修饰符。\n全局事件总线 适用于任意组件间通信，使用前需安装全局事件总线：\nnew Vue({ beforeCreate() { Vue.prototype.$bus = this } }) 使用方式：\n组件 A 希望接收数据，则在组件 A 中给 $bus 绑定自定义事件，事件回调写在组件 A 中。\nmethods(){ handle(data){} }, mounted(){ this.$bus.$on(\u0026#39;xxx\u0026#39;, this.handle) } 其它组件调用组件 A 绑定的自定义事件提供数据。\nthis.$bus.$emit(\u0026#39;xxx\u0026#39;, data) 最好在组件的 beforeDestroy 生命周期钩子中，使用 $off 函数解绑当前组件绑定的自定义事件。\n消息订阅与发布（pubsub-js） PubSubJS 是一个用 JavaScript 编写的基于主题的发布订阅库。\n安装 PubSubJS：\nnpm i pubsub-js 导入模块：\nimport PubSub from \u0026#39;pubsub-js\u0026#39; 使用 PubSubJS 发布与订阅：\n// 发布消息 PubSub.publish(\u0026#39;removeTodo\u0026#39;, this.todo.id) // 订阅消息 this.pubId = PubSub.subscribe(\u0026#39;removeTodo\u0026#39;, this.removeTodo) // 取消订阅 PubSub.unsubscribe(this.pubId) $nextTick this.myTodo.edit = true this.$nextTick(function () { this.$refs.input.focus() }) 语法：this.$nextTick(function(){})。\n作用：在下一次 DOM 更新结束后执行回调操作，用于数据改变后，要基于更新后的 DOM 进行操作时。\nslot 插槽 一种组件间通信方式，让父组件向子组件指定位置插入 HTML 结构，分为：默认插槽、具名插槽、作用域插槽。\n默认插槽 // 子组件中 \u0026lt;template\u0026gt; \u0026lt;div class=\u0026#34;category\u0026#34;\u0026gt; \u0026lt;h2\u0026gt;{{ title }}\u0026lt;/h2\u0026gt; \u0026lt;slot\u0026gt;插槽默认值\u0026lt;/slot\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/template\u0026gt; // 父组件中 \u0026lt;Category title=\u0026#34;美食分类\u0026#34;\u0026gt; \u0026lt;img alt=\u0026#34;美食\u0026#34; src=\u0026#34;\u0026#34;\u0026gt; \u0026lt;/Category\u0026gt; 具名插槽 // 子组件中 \u0026lt;template\u0026gt; \u0026lt;div class=\u0026#34;category\u0026#34;\u0026gt; \u0026lt;h2\u0026gt;{{ title }}\u0026lt;/h2\u0026gt; \u0026lt;slot name=\u0026#34;slot1\u0026#34;\u0026gt;插槽 1 默认值\u0026lt;/slot\u0026gt; \u0026lt;slot name=\u0026#34;slot2\u0026#34;\u0026gt;插槽 2 默认值\u0026lt;/slot\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/template\u0026gt; // 父组件中 \u0026lt;Category title=\u0026#34;美食分类\u0026#34;\u0026gt; \u0026lt;img slot=\u0026#34;slot1\u0026#34; alt=\u0026#34;美食\u0026#34; src=\u0026#34;\u0026#34;\u0026gt; \u0026lt;template v-slot:slot2\u0026gt; \u0026lt;ol\u0026gt; \u0026lt;li v-for=\u0026#34;(f, index) in foods\u0026#34; :key=\u0026#34;index\u0026#34;\u0026gt;{{ f }}\u0026lt;/li\u0026gt; \u0026lt;/ol\u0026gt; \u0026lt;/template\u0026gt; \u0026lt;/Category\u0026gt; v-slot 只能添加在 template 上，插槽的默认名称是 default，v-slot:default 的缩写是 #default。\n作用域插槽 数据在组件自身，但根据数据生成的结构需要父组件决定时使用作用域插槽。\n// 子组件 \u0026lt;template\u0026gt; \u0026lt;div class=\u0026#34;category\u0026#34;\u0026gt; \u0026lt;h2\u0026gt;{{ title }}\u0026lt;/h2\u0026gt; \u0026lt;slot :games=\u0026#34;games\u0026#34;\u0026gt;插槽默认值\u0026lt;/slot\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/template\u0026gt; // 父组件 \u0026lt;Category title=\u0026#34;游戏分类\u0026#34;\u0026gt; \u0026lt;template v-slot:default=\u0026#34;{games}\u0026#34;\u0026gt; \u0026lt;ol\u0026gt; \u0026lt;li v-for=\u0026#34;(g, index) in games\u0026#34; :key=\u0026#34;index\u0026#34;\u0026gt;{{ g }}\u0026lt;/li\u0026gt; \u0026lt;/ol\u0026gt; \u0026lt;/template\u0026gt; \u0026lt;/Category\u0026gt; Vue 脚手架 官网地址：https://cli.vuejs.org/zh/\nVue 脚手架是 Vue 官方提供的标准化开发工具。\n创建脚手架项目 安装 Vue CLI npm install -g @vue/cli 创建项目 vue create hello-vue Vue 项目结构 ├── babel.config.js babel 的配置文件 ├── jsconfig.json ├── node_modules ├── package-lock.json 包版本控制文件 ├── package.json 应用包配置文件 ├── public |\t|—— favicon.ico 页签图标 |\t|—— index.html 主页面 ├── src |\t|—— assets 存放静态资源 |\t|\t|—— logo.png |\t|—— components 存放组件 |\t|—— App.vue |\t|—— main.js 入口文件 └── vue.config.js Render 函数 new Vue({ // render: h =\u0026gt; h(App), render(createElement) { return createElement(App) } }).$mount(\u0026#39;#app\u0026#39;) vue.js 是完整版的 Vue，包含核心功能和模版解析器。 vue.runtime.xxx.js 是运行版 Vue，只包含核心功能。 由于运行版 Vue 没有模版解析器，所以在 new Vue({}) 中不能使用 template 配置项，需要使用 render 函数接收到的 createElement 函数去指定内容。 修改默认配置 使用 vue inspect \u0026gt; output.js 可以查看 Vue 脚手架的默认配置。\nvue.config.js 是一个可选的配置文件，如果项目的根目录中存在这个文件，那么它会被 @vue/cli-service 自动加载。\n配置代理 简单配置 在 vue.config.js 中添加如下配置：\nmodule.exports = { devServer: { proxy: \u0026#39;http://localhost:9509\u0026#39; } } 请求了前端不存在的资源时，会将请求转发给代理服务器。\n完整配置 在 vue.config.js 中添加如下配置：\nmodule.exports = { devServer: { proxy: { \u0026#39;/api1\u0026#39;: { target: \u0026#39;http://localhost:9509\u0026#39;, pathRewrite: {\u0026#39;^api1\u0026#39;: \u0026#39;\u0026#39;}, ws: true, changeOrigin: true }, \u0026#39;/api2\u0026#39;: { target: \u0026#39;http://localhost:9000\u0026#39;, pathRewrite: {\u0026#39;^api2\u0026#39;: \u0026#39;\u0026#39;}, } } } } 可以配置多个代理。\ntarget：代理目标的 basePath。\nws：是否支持 websocket。\nchangeOrigin：修改请求头中 host 字段与目标服务器一致。\n","permalink":"https://2d992714.xianglin2020-github-io.pages.dev/posts/2022/vue-scaffold/","summary":"Vue 组件语法与脚手架使用。","title":"Vue2 组件和脚手架"},{"content":"Gradle 基础 官网：Gradle\n安装 Gradle 需要 JDK8 及以上版本。\n从 releases 页面下载 Gradle 二进制包，解压后添加环境变量即可。使用 gradle -v 验证安装。\n创建 Java 应用 使用 init 任务创建项目 创建空文件夹 gradleJava 保存项目。\n在 gradleJava 文件夹中执行 gradle init 命令，根据提示完成创建。\n由 Gradle 创建的 Java 应用结构如下：\ngradle：gradle wrapper 目录；\ngradlew、gradlew.bat：gradle wrapper 启动脚本；\nsettings.gradle：配置文件，定义了应用名称和子项目；\napp/build.gradle：app 项目的构建脚本；\napp/src/main(test)/java：Java 源（测试）代码路径。\n项目文件介绍 settings.gradle 内如如下：\nrootProject.name = \u0026#39;gradleJava\u0026#39; include(\u0026#39;app\u0026#39;) rootProject.name：指定应用名称。 include()：指定该应用包含的子项目。 子项目 app 的配置文件是：app/build.gradle，内容如下：\nplugins { id \u0026#39;application\u0026#39; } repositories { // 使用 Maven 中央仓库 mavenCentral() } dependencies { // 指定依赖 testImplementation \u0026#39;org.junit.jupiter:junit-jupiter:5.8.2\u0026#39; implementation \u0026#39;com.google.guava:guava:31.0.1-jre\u0026#39; } application { mainClass = \u0026#39;store.xianglin.gradle.App\u0026#39; } tasks.named(\u0026#39;test\u0026#39;) { useJUnitPlatform() } Groovy 与 Java 比较 默认导入 groovy 默认导入如下包和类：\njava.io.* java.lang.* java.net.* java.util.* java.math.BigInteger java.math.BigDecimal groovy.lang.* groovy.util.* 运行时参数类型匹配 在 groovy 中，重载方法根据运行时参数的实际类型选择，Java 中重载方式基于编译时静态声明的参数类型选择。 比如如下重载方法在 groovy 和 Java 中结果不一致：\nint method(String arg) { 1 } int method(Object arg) { 2 } Object o = \u0026#34;object\u0026#34; // result: 1 int result = method(o) public class Main { int method(String arg) { return 1; } int method(Object arg) { return 2; } public static void main(String[] args) { Object o = \u0026#34;object\u0026#34;; // result: 2 int result = new java.MainJava().method(o); } } 创建数组 Java 中初始化数组有如下两种方式：\nclass Array { // 简短语法 int[] shortArr = {1, 2, 3}; // 完整语法 int[] longArr = new int[]{1, 2, 3}; } Groovy 中 {...} 代码块称为闭包，所以不能使用 Java 的简短语法创建 Groovy 数组。Groovy 中初始化数组有以下两种方式：\nint[] shortArr = [1, 2, 3] int[] longArr = new int[]{1, 2, 3} 包可见性 Java 中省略修饰符时为 default 包可见。Groovy 中属性省略修饰符时，代表一个私有字段，并自动关联 setter 和 getter 方法。 使用 @PackageScope 声明一个包可见性的字段。\n// package g class Scope { String name @PackageScope int age } //package g.s import g.Scope new Scope().getName() ARM 代码块 Java 中 Automatic Resource Management (try-with-resources) 如下：\npublic class Main { public static void main(String[] args) { try (var reader = Files.newBufferedReader(Paths.get(\u0026#34;file\u0026#34;), StandardCharsets.UTF_8)) { String line; while ((line = reader.readLine()) != null) { System.out.println(line); } } catch (IOException e) { e.printStackTrace(); } } } Groovy 通过各种使用闭包参数的方法支持 ARM，如下：\nnew File(\u0026#34;file\u0026#34;).eachLine { print it } new File(\u0026#34;file\u0026#34;).withReader(\u0026#39;UTF-8\u0026#39;) { reader -\u0026gt; { reader.eachLine { print it } } } Lambda 表达式和方法引用操作符 Java8 和 Groovy 都支持 Lambda 和 method reference operator :: 如下：\npublic class Main { public static void main(String[] args) { Runnable run = () -\u0026gt; System.out.println(\u0026#34;lambda\u0026#34;); List.of(1, 2, 3).forEach(System.out::println); } } Runnable runnable = () -\u0026gt; println \u0026#34;lambda\u0026#34; List.of(1, 2, 3).forEach(System.out::println) List.of(1, 2, 3).forEach({ print it }) GStrings Groovy 支持单引号字符串和双引号字符串，双引号字符串的类型时 GString，如下：\nString s = \u0026#34;double\u0026#34; String s1 = \u0026#39;singly-quoted string\u0026#39; def s2 = \u0026#34;${s}-quoted string\u0026#34; // class java.lang.String println s1.getClass() // class org.codehaus.groovy.runtime.GStringImpl println s2.getClass() String 和 Character 字面量 单引号字面量用 String 表示，双引号字面根据是否有插值 ${} 使用 String 或 GString 表示。 单个字符仅仅在使用 char 声明时才表示字符，如下：\ndef s = \u0026#39;s\u0026#39; // class java.lang.String println s.getClass() char c = \u0026#39;c\u0026#39; // class java.lang.Character println c.getClass() Groovy 中强制类型转换如下：\nassert ((char) \u0026#39;c\u0026#39;).class == Character assert (\u0026#39;c\u0026#39; as char).class == Character assert \u0026#39;cx\u0026#39;.asType(Character) == \u0026#39;c\u0026#39; == 行为 Java 中使用 == 比较基本类型和对象引用是否相等。 Groovy 中使用 == 比较非基本类型时，如果对象实现了 Comparable 接口则使用 a.compareTo(b) == 0，否则使用 a.equals(b)。 比较对象引用是否相等使用 is 方法或者运算符 ===，如下：\ndef s1 = \u0026#34;123\u0026#34; def s2 = \u0026#34;12\u0026#34; + \u0026#34;3\u0026#34; // true println s1 == s2 // false println s1 === s2 println s1.is(s2) 基本类型和包装类型 Java 在处理方法入参和返回值时会自动装箱和拆箱，如下：\npublic class Main { public static void main(String[] args) { var i1 = 1; var i2 = 2; var result = add(i1, i2); } static int add(Integer i1, Integer i2) { return i1 + i2; } } 在 Groovy 中，基本类型可以直接使用其包装类型的方法，例如 true.toString()，如下方法在 groovy 和 Java 中结果不一致：\nint i = 1 m(i) // java 中类型提升会优先于装箱操作 void m(long l) { println \u0026#34;in m(long)\u0026#34; } // groovy 所有基本类型都是使用对应的包装类 void m(Integer i) { println \u0026#34;in m(Integer)\u0026#34; } 额外的关键字 Groovy 有如下额外的关键字：as、def、in、trait 和 it（仅在闭包中）。 Groovy 同样支持局部变量类型推断 var。\n","permalink":"https://2d992714.xianglin2020-github-io.pages.dev/posts/2022/gradle-basics/","summary":"gradle 的基础用法，以及 groovy 与 Java 语法的区别。","title":"Gradle基础"},{"content":"Spring Data MongoDB 按照 Spring 官网教程学习 Spring Data MongoDB。\n核心概念 Spring Data repository 抽象中的核心接口是 org.springframework.data.repository.Repository，作为一个标记接口，没有定义任何方法：\n// Central repository marker interface @Indexed public interface Repository\u0026lt;T, ID\u0026gt; { } 接口 org.springframework.data.repository.CrudRepository 定义了复杂的 CRUD 操作方法：\n// Interface for generic CRUD operations on a repository for a specific type. @NoRepositoryBean public interface CrudRepository\u0026lt;T, ID\u0026gt; extends Repository\u0026lt;T, ID\u0026gt; { // 保存实体 \u0026lt;S extends T\u0026gt; S save(S entity); \u0026lt;S extends T\u0026gt; Iterable\u0026lt;S\u0026gt; saveAll(Iterable\u0026lt;S\u0026gt; entities); // 通过 ID 属性查询 Optional\u0026lt;T\u0026gt; findById(ID id); Iterable\u0026lt;T\u0026gt; findAllById(Iterable\u0026lt;ID\u0026gt; ids); boolean existsById(ID id); // 返回所有记录 Iterable\u0026lt;T\u0026gt; findAll(); // 查询所有记录数 long count(); // 删除指定记录 void deleteById(ID id); void delete(T entity); void deleteAllById(Iterable\u0026lt;? extends ID\u0026gt; ids); void deleteAll(Iterable\u0026lt;? extends T\u0026gt; entities); // 删除所有记录 void deleteAll(); } 针对 MongoDB 提供了 org.springframework.data.mongodb.repository.MongoRepository 接口：\n// Mongo specific Repository interface. @NoRepositoryBean public interface MongoRepository\u0026lt;T, ID\u0026gt; extends ListCrudRepository\u0026lt;T, ID\u0026gt;, ListPagingAndSortingRepository\u0026lt;T, ID\u0026gt;, QueryByExampleExecutor\u0026lt;T\u0026gt; { \u0026lt;S extends T\u0026gt; S insert(S entity); \u0026lt;S extends T\u0026gt; List\u0026lt;S\u0026gt; insert(Iterable\u0026lt;S\u0026gt; entities); @Override \u0026lt;S extends T\u0026gt; List\u0026lt;S\u0026gt; findAll(Example\u0026lt;S\u0026gt; example); @Override \u0026lt;S extends T\u0026gt; List\u0026lt;S\u0026gt; findAll(Example\u0026lt;S\u0026gt; example, Sort sort); } org.springframework.data.repository.PagingAndSortingRepository 接口提供方法用于分页查询：\n@NoRepositoryBean public interface PagingAndSortingRepository\u0026lt;T, ID\u0026gt; extends Repository\u0026lt;T, ID\u0026gt; { Iterable\u0026lt;T\u0026gt; findAll(Sort sort); Page\u0026lt;T\u0026gt; findAll(Pageable pageable); } 使用方法 通过以下步骤使用 Spring Data MongoDB：\n以查询 Person 为例\n创建一个接口继承自 Repository 或者它的子接口，并指定泛型参数 T 和 ID。\npublic interface PersonRepository extends Repository\u0026lt;Person, ObjectId\u0026gt; { } 在接口中声明查询方法。\npublic interface PersonRepository extends Repository\u0026lt;Person, ObjectId\u0026gt; { List\u0026lt;Person\u0026gt; findByLastname(String lastname); } 通过注解 @EnableMongoRepositories 让 Spring 管理查询接口生成接口代理实现类。\n通过依赖注入获取接口实例，使用查询方法。\n@SpringBootTest class PersonRepositoryTest { @Resource private PersonRepository personRepository; @Test void findByLastname() { var personList = personRepository.findByLastname(\u0026#34;Matthews\u0026#34;); assertFalse(personList.isEmpty()); } } 定义 Repository 接口 定义 Repository 接口可以通过继承 Repository 及其子接口，也可以在自定义接口上增加 org.springframework.data.repository.RepositoryDefinition 注解：\n// Annotating an interface with RepositoryDefinition will cause the same behaviour as extending Repository. public @interface RepositoryDefinition { Class\u0026lt;?\u0026gt; domainClass(); Class\u0026lt;?\u0026gt; idClass(); } 例如：\n@RepositoryDefinition(domainClass = Person.class, idClass = ObjectId.class) public interface AnnotationPersonRepository { List\u0026lt;Person\u0026gt; findByLastname(String lastname); } 微调 Repository 定义 如果只想暴露部分方法，比如自定义的 Repository 中不提供删除方法，可以从 CrudRepository 复制其它方法到接口中。\n/** * 自定义 Repository 只提供保存和查询方法 * * @author linxiang */ @NoRepositoryBean public interface CustomBaseRepository\u0026lt;T, ID\u0026gt; extends Repository\u0026lt;T, ID\u0026gt; { \u0026lt;S extends T\u0026gt; S save(S entity); Optional\u0026lt;T\u0026gt; findById(ID id); } 增加 org.springframework.data.repository.NoRepositoryBean 注解避免 Spring 为接口创建代理对象。\n使用多个 Spring Data 模块 可以在项目中同时使用多个 Spring Data 模块，比如 Spring Data Jpa 和 Spring Data MongoDB，有几种方式可以区分它们：\nRepository 定义时继承特定接口，比如 MongoRepository 和 JpaRepository；\n通过实体类上的注解区分，比如 @Entity 用于 Jpa ，@Document 用于 MongoDB；\n通过 @Enable${store}Repositories 注解区分\n@EnableJpaRepositories(basePackages = \u0026#34;com.acme.repositories.jpa\u0026#34;) @EnableMongoRepositories(basePackages = \u0026#34;com.acme.repositories.mongo\u0026#34;) class Configuration {} MongoTemplate 介绍 org.springframework.data.mongodb.core.MongoTemplate 是 Spring MongoDB 的核心类，该模板提供了增删改查方法以及 MongoDB Document 和 Java POJO 转换的方法。MongoTemplate 是线程安全的。\nMongoDB Document 和 Java pojo 转换是委托 org.springframework.data.mongodb.core.convert.MongoConverter 接口实现的，Spring 的默认实现类是 org.springframework.data.mongodb.core.convert.MappingMongoConverter。\nhttps://docs.spring.io/spring-data/mongodb/docs/current/reference/html/#mongo-template\n","permalink":"https://2d992714.xianglin2020-github-io.pages.dev/posts/2022/spring-data-mongodb/","summary":"学习 Spring Data MongoDB 的基础用法。","title":"Spring Data MongoDB"},{"content":"Vue2：https://v2.cn.vuejs.org/\nVue3：https://cn.vuejs.org/\nVue 是一套用于构建用户界面的渐进式框架。\nVue 特点 采用组件化模式，提高代码复用率，且让代码更好维护； 声明式编码，无需直接操作 DOM，提高开发效率； 使用虚拟 DOM 和 Diff 算法，尽量复用 DOM。 Vue 实例 通过 Vue 函数创建 Vue 实例\n\u0026lt;!DOCTYPE html\u0026gt; \u0026lt;html lang=\u0026#34;en\u0026#34;\u0026gt; \u0026lt;head\u0026gt; \u0026lt;title\u0026gt;vue 实例\u0026lt;/title\u0026gt; \u0026lt;script src=\u0026#34;../js/vue.js\u0026#34;\u0026gt;\u0026lt;/script\u0026gt; \u0026lt;/head\u0026gt; \u0026lt;body\u0026gt; \u0026lt;div id=\u0026#34;root\u0026#34;\u0026gt; \u0026lt;h2\u0026gt;{{msg}}\u0026lt;/h2\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;script\u0026gt; new Vue({ el: \u0026#39;#root\u0026#39;, data: { msg: \u0026#39;Vue 实例\u0026#39; } }) \u0026lt;/script\u0026gt; \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; el：用于指定当前 Vue 实例为哪个容器服务，通常为 css 选择器字符串。\nVue 实例和容器是一一对应的。\nel 和 data 的两种写法 \u0026lt;div id=\u0026#34;root\u0026#34;\u0026gt; \u0026lt;h1\u0026gt;Hello {{name}}\u0026lt;/h1\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;script\u0026gt; new Vue({ el: \u0026#39;#root\u0026#39;, data: { name: \u0026#39;Vue\u0026#39; } }) \u0026lt;/script\u0026gt; \u0026lt;div id=\u0026#34;root1\u0026#34;\u0026gt; \u0026lt;h1\u0026gt;Hello {{name}}\u0026lt;/h1\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;script\u0026gt; new Vue({ data() { return { name: \u0026#39;Vue\u0026#39; } } }).$mount(\u0026#39;#root1\u0026#39;) \u0026lt;/script\u0026gt; el 的两种写法：\nnew Vue 是指定配置属性 先创建 Vue 实例，通过 $mount('#root1') 指定 el 值 data 可以使用对象式或函数式写法；\n由 Vue 管理的函数，不能使用箭头函数，箭头函数中的 this 不是 Vue 实例。\n模板语法 \u0026lt;div id=\u0026#34;root\u0026#34;\u0026gt; \u0026lt;h1\u0026gt;插值语法\u0026lt;/h1\u0026gt; \u0026lt;h3\u0026gt;Hello,{{name}}\u0026lt;/h3\u0026gt; \u0026lt;hr\u0026gt; \u0026lt;h1\u0026gt;指令语法\u0026lt;/h1\u0026gt; \u0026lt;a href=\u0026#34;https://www.bing.com\u0026#34;\u0026gt;Bing\u0026lt;/a\u0026gt; \u0026lt;br\u0026gt; \u0026lt;a v-bind:href=\u0026#34;url\u0026#34;\u0026gt;Bing\u0026lt;/a\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;script\u0026gt; Vue.config.productionTip = false new Vue({ el: \u0026#39;#root\u0026#39;, data: { name: \u0026#39;Vue\u0026#39;, url: \u0026#39;https://www.bing.com\u0026#39; } }) \u0026lt;/script\u0026gt; 插值语法 用于解析标签体内容，写法 {{xx}}，xx 是 js 表达式，且可以读取到 data 中的所有属性。\n指令语法 用于解析标签，包括：标签属性、标签体内容、绑定事件等，v-bind:href=\u0026quot;url\u0026quot; 简写为 :href=\u0026quot;url\u0026quot;\n数据绑定 \u0026lt;div id=\u0026#34;root\u0026#34;\u0026gt; \u0026lt;label\u0026gt; 单向数据绑定： \u0026lt;input type=\u0026#34;text\u0026#34; v-bind:value=\u0026#34;name\u0026#34;\u0026gt; \u0026lt;input type=\u0026#34;text\u0026#34; :value=\u0026#34;name\u0026#34;\u0026gt; \u0026lt;/label\u0026gt; \u0026lt;br\u0026gt; \u0026lt;label\u0026gt; 双向数据绑定： \u0026lt;input type=\u0026#34;text\u0026#34; v-model:value=\u0026#34;name\u0026#34;\u0026gt; \u0026lt;input type=\u0026#34;text\u0026#34; v-model=\u0026#34;name\u0026#34;\u0026gt; \u0026lt;/label\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;script\u0026gt; Vue.config.productionTip = false new Vue({ el: \u0026#39;#root\u0026#39;, data: { name: \u0026#39;Vue\u0026#39; } }) \u0026lt;/script\u0026gt; Vue 中的数据绑定方式：\n单向绑定 v-bind：数据从 data 流向页面 双向绑定 v-model：它负责监听用户的输入事件以更新数据 双向绑定一般都应用在表单类元素上（如 input 、select）； v-model:value 可以简写为 v-model，因为 v-model 默认收集 value 值。 MVVM 模型 MVVM 模型：https://zh.wikipedia.org/wiki/MVVM\nMVVM 模型代表 M、V 和 VM\nM：模型 Model：data 中的数据\nV：视图 View：模板代码\nVM：视图模型 ViewModel：Vue 实例\n数据代理 Object.defineProperty() https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty\n语法：Object.defineProperty(obj, prop, descriptor)\n参数：\nobj：要定义属性的对象。\nprop：要定义或修改的属性的名称。\ndescriptor：要定义或修改的属性描述符。\ndescriptor 的配置项：\nvalue：该属性对应的值。可以是任何有效的 JavaScript 值（数值，对象，函数等）。 默认为 undefined configurable：配置为 true 时属性能从对象上删除，默认为 false； enumerable：配置为 true 时属性出现在对象的枚举属性中，默认为 false； writable：配置为 true 时，配置的 value 才能被赋值运算符改变，默认为 false； 数据代理 通过一个对象代理对另一个对象中属性的读写操作。\nVue 中的数据代理 通过 vm 对象代理 data 对象中数据的读写操作，好处是可以方便的操作 data 中的数据。\n基本原理：通过 Object.defineProperty() 把 data 对象中所有属性添加到 vm 中，为每一个添加到 vm 的属性都指定 getter/setter ，在 getter/setter 中读写 data 中对应的属性。\nVue 中事件处理 监听事件 \u0026lt;div id=\u0026#34;root\u0026#34;\u0026gt; \u0026lt;h1\u0026gt;Hello, {{msg}}\u0026lt;/h1\u0026gt; \u0026lt;!-- \u0026lt;button v-on:click=\u0026#34;showInfo\u0026#34;\u0026gt;点击提示信息\u0026lt;/button\u0026gt;--\u0026gt; \u0026lt;button @click=\u0026#34;showInfo1\u0026#34;\u0026gt;点击提示信息1\u0026lt;/button\u0026gt; \u0026lt;button @click=\u0026#34;showInfo2(66,$event)\u0026#34;\u0026gt;点击提示信息2\u0026lt;/button\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;script\u0026gt; new Vue({ el: \u0026#39;#root\u0026#39;, data: { msg: \u0026#39;Vue\u0026#39; }, methods: { showInfo1(event) { // console.log(event.target) // console.log(this) alert(\u0026#39;点击\u0026#39;) }, showInfo2(number, e) { console.log(number, e) } } }) \u0026lt;/script\u0026gt; 使用 v-on:xxx 或 @xxx 绑定事件，事件回调配置在 methods 中。可以用特殊变量 $event 访问原始的 DOM 事件对象。\n事件修饰符 \u0026lt;div id=\u0026#34;root\u0026#34;\u0026gt; \u0026lt;!-- 阻止单击事件继续传播 --\u0026gt; \u0026lt;button v-on:click.stop=\u0026#34;doThis\u0026#34;\u0026gt;点击\u0026lt;/button\u0026gt; \u0026lt;!-- 提交事件不再重载页面 --\u0026gt; \u0026lt;form v-on:submit.prevent=\u0026#34;onSubmit\u0026#34;\u0026gt; \u0026lt;input type=\u0026#34;submit\u0026#34; value=\u0026#34;提交\u0026#34;\u0026gt; \u0026lt;/form\u0026gt; \u0026lt;!-- 点击事件将只会触发一次 --\u0026gt; \u0026lt;a v-on:click.once=\u0026#34;doThis\u0026#34;\u0026gt;\u0026lt;/a\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;script\u0026gt; new Vue({ el: \u0026#39;#root\u0026#39;, methods: { doThis() { console.log(\u0026#39;doThis\u0026#39;) }, onSubmit() { console.log(\u0026#39;onSubmit\u0026#39;) } } }) \u0026lt;/script\u0026gt; prevent：同 event.preventDefault() ，阻止默认事件。\nstop：同 event.stopPropagation() ，阻止事件冒泡。\nonce：事件只会触发一次。\n事件修饰符可以连写：@click.stop.prevent\n键盘事件 \u0026lt;div id=\u0026#34;root\u0026#34;\u0026gt; \u0026lt;input type=\u0026#34;text\u0026#34; placeholder=\u0026#34;按下回车提示输入\u0026#34; @keyup=\u0026#34;showInfo\u0026#34;\u0026gt; \u0026lt;input type=\u0026#34;text\u0026#34; placeholder=\u0026#34;制表符\u0026#34; @keydown.tab=\u0026#34;showKeyCode\u0026#34;\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;script\u0026gt; Vue.config.productionTip = false new Vue({ el: \u0026#39;#root\u0026#39;, methods: { showInfo(e) { if (e.keyCode === 13) { console.log(e.target.value) } }, showKeyCode(e) { console.log(e.key, e.keyCode) } } }) \u0026lt;/script\u0026gt; Vue 中常用的按键别名：\n回车 =\u0026gt; enter =\u0026gt; 13\n删除 =\u0026gt; delete （同时捕获“删除8”和“退格46”）\n退出 =\u0026gt; esc =\u0026gt; 27\n空格 =\u0026gt; space =\u0026gt; 32\n制表符 =\u0026gt; tab =\u0026gt; 9\n上、下、左、右 =\u0026gt; up down left right\nVue 未提供按键别名的按键，可以使用按键原始的 key 值绑定，但要转换为 kebab-case（短横线命名）。\n系统修饰键：ctrl、alt、shift 和 meta 使用 keydown 正常触发事件，使用 keyup 需要再按下其它键，随后释放才触发事件。\n计算属性与监视 计算属性 \u0026lt;div id=\u0026#34;root\u0026#34;\u0026gt; \u0026lt;label\u0026gt; 姓： \u0026lt;input type=\u0026#34;text\u0026#34; v-model=\u0026#34;firstName\u0026#34;\u0026gt; \u0026lt;/label\u0026gt; \u0026lt;br\u0026gt;\u0026lt;br\u0026gt; \u0026lt;label\u0026gt; 名： \u0026lt;input type=\u0026#34;text\u0026#34; v-model=\u0026#34;lastName\u0026#34;\u0026gt; \u0026lt;/label\u0026gt; \u0026lt;br\u0026gt;\u0026lt;br\u0026gt; 全名：\u0026lt;span\u0026gt;{{fullName}}\u0026lt;/span\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;script\u0026gt; Vue.config.productionTip = false new Vue({ el: \u0026#39;#root\u0026#39;, data: { firstName: \u0026#39;张\u0026#39;, lastName: \u0026#39;三\u0026#39; }, computed: { fullName: { get() { return this.firstName + \u0026#39;-\u0026#39; + this.lastName }, set(val) { let arr = val.split(\u0026#39;-\u0026#39;) this.firstName = arr[0] this.lastName = arr[1] } } } }) \u0026lt;/script\u0026gt; 通过已有的属性计算出新属性称为计算属性，底层借助了 Object.defineproperty 方法提供 getter 和 setter。\n计算属性会缓存结果，只有在其依赖的属性变化时，计算属性才会重新计算。\n监视属性 \u0026lt;div id=\u0026#34;root\u0026#34;\u0026gt; \u0026lt;h2\u0026gt;今天天气很{{info}}\u0026lt;/h2\u0026gt; \u0026lt;button @click=\u0026#34;changeWeather\u0026#34;\u0026gt;切换天气\u0026lt;/button\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;script\u0026gt; Vue.config.productionTip = false const vm = new Vue({ el: \u0026#39;#root\u0026#39;, data: { isHot: true }, computed: { info() { return this.isHot ? \u0026#39;炎热\u0026#39; : \u0026#39;凉爽\u0026#39; } }, watch: { isHot: { immediate: true, handler(newValue, oldValue) { console.log(\u0026#39;isHot 被修改了\u0026#39;, newValue, oldValue) } } }, methods: { changeWeather() { this.isHot = !this.isHot } } }) // 监视属性API vm.$watch(\u0026#39;isHot\u0026#39;, { immediate: true, handler(newValue, oldValue) { console.log(\u0026#39;isHot 被修改了\u0026#39;, newValue, oldValue) } }) \u0026lt;/script\u0026gt; 当被监视的属性变化时，回调函数自动调用，或执行异步或耗时较长的操作。监视属性使用 watch 配置或使用 vm.$watch 配置监视。\nimmediate：立即触发回调函数\ndeep：深度监视，监视对象内部值的变化\n监视对象的回调函数中 newValue 和 oldValue 是一致的，可以配合计算属性，序列化生成新对象避免此问题。\n\u0026lt;div id=\u0026#34;root\u0026#34;\u0026gt; \u0026lt;h2\u0026gt;numbers 的 a 是 {{numbers.a}}\u0026lt;/h2\u0026gt; \u0026lt;button @click=\u0026#34;numbers.a++\u0026#34;\u0026gt;点击 a + 1\u0026lt;/button\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;script\u0026gt; Vue.config.productionTip = false const vm = new Vue({ el: \u0026#39;#root\u0026#39;, data: { numbers: { a: 1, b: 2 } }, computed: { newNumbers() { return JSON.parse(JSON.stringify(this.numbers)) } }, watch: { numbers: { handler(newValue, oldValue) { console.log(\u0026#39;numbers 被改变了\u0026#39;, newValue, oldValue) }, deep: true }, newNumbers(newVal, oldVal) { console.log(\u0026#39;newNumbers 被改变了\u0026#39;, newVal, oldVal) } }, }) \u0026lt;/script\u0026gt; 绑定样式 绑定 class 样式 \u0026lt;style\u0026gt; .basic { } .normal { } .happy { } .sad { } .s1 { } .s2 { } .s3 { } \u0026lt;/style\u0026gt; \u0026lt;div id=\u0026#34;root\u0026#34;\u0026gt; \u0026lt;!-- 绑定 class 样式——字符串写法 --\u0026gt; \u0026lt;div class=\u0026#34;basic\u0026#34; :class=\u0026#34;mood\u0026#34; id=\u0026#34;demo\u0026#34; @click=\u0026#34;changeMood\u0026#34;\u0026gt;{{name}}\u0026lt;/div\u0026gt; \u0026lt;!-- 绑定 class 样式——数组写法 --\u0026gt; \u0026lt;div class=\u0026#34;basic\u0026#34; :class=\u0026#34;classArr\u0026#34;\u0026gt;{{name}}\u0026lt;/div\u0026gt; \u0026lt;!-- 绑定 class 样式——对象写法 --\u0026gt; \u0026lt;div class=\u0026#34;basic\u0026#34; :class=\u0026#34;classObj\u0026#34;\u0026gt;{{name}}\u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;script type=\u0026#34;text/javascript\u0026#34;\u0026gt; Vue.config.productionTip = false new Vue({ el: \u0026#39;#root\u0026#39;, data: { name: \u0026#39;Vue\u0026#39;, mood: \u0026#39;normal\u0026#39;, classArr: [\u0026#39;s1\u0026#39;, \u0026#39;s2\u0026#39;, \u0026#39;s3\u0026#39;], classObj: { s1: true, s2: true, s3: true, } }, methods: { changeMood() { this.mood = \u0026#39;happy\u0026#39; } } }) \u0026lt;/script\u0026gt; 绑定 style 样式 \u0026lt;div id=\u0026#34;root\u0026#34;\u0026gt; \u0026lt;!-- 绑定 style 样式——对象写法 --\u0026gt; \u0026lt;div class=\u0026#34;basic\u0026#34; :style=\u0026#34;styleObj\u0026#34;\u0026gt;{{name}}\u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;script\u0026gt; Vue.config.productionTip = false const vm = new Vue({ el: \u0026#39;#root\u0026#39;, data: { styleObj: { fontSize: \u0026#39;40px\u0026#39; } } }) \u0026lt;/script\u0026gt; 条件渲染 \u0026lt;div id=\u0026#34;root\u0026#34;\u0026gt; \u0026lt;h1 v-show=\u0026#34;show\u0026#34;\u0026gt;Hello,{{name}}\u0026lt;/h1\u0026gt; \u0026lt;div v-if=\u0026#34;type === \u0026#39;A\u0026#39;\u0026#34;\u0026gt; A \u0026lt;/div\u0026gt; \u0026lt;div v-else-if=\u0026#34;type === \u0026#39;B\u0026#39;\u0026#34;\u0026gt; B \u0026lt;/div\u0026gt; \u0026lt;div v-else-if=\u0026#34;type === \u0026#39;C\u0026#39;\u0026#34;\u0026gt; C \u0026lt;/div\u0026gt; \u0026lt;div v-else\u0026gt; Not A/B/C \u0026lt;/div\u0026gt; \u0026lt;template v-if=\u0026#34;show\u0026#34;\u0026gt; \u0026lt;h1\u0026gt;Title\u0026lt;/h1\u0026gt; \u0026lt;p\u0026gt;Paragraph 1\u0026lt;/p\u0026gt; \u0026lt;p\u0026gt;Paragraph 2\u0026lt;/p\u0026gt; \u0026lt;/template\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;script\u0026gt; Vue.config.productionTip = false new Vue({ el: \u0026#39;#root\u0026#39;, data: { name: \u0026#39;Vue\u0026#39;, show: true, type: \u0026#39;D\u0026#39; } }) \u0026lt;/script\u0026gt; v-if：可以与 v-else-if 和 v-else 配合使用，可以与 template 配合使用渲染分组； v-show：元素被渲染，通过 display 样式切换，适用于非常频繁的切换。 列表渲染 \u0026lt;div id=\u0026#34;root\u0026#34;\u0026gt; \u0026lt;!-- 遍历数组 --\u0026gt; \u0026lt;h2\u0026gt;人员列表\u0026lt;/h2\u0026gt; \u0026lt;ul\u0026gt; \u0026lt;li v-for=\u0026#34;(p, index) in persons\u0026#34; :key=\u0026#34;p.id\u0026#34;\u0026gt; {{index}}-{{p.name}}-{{p.age}} \u0026lt;/li\u0026gt; \u0026lt;/ul\u0026gt; \u0026lt;!-- 遍历对象 --\u0026gt; \u0026lt;h2\u0026gt;汽车信息\u0026lt;/h2\u0026gt; \u0026lt;ul\u0026gt; \u0026lt;li v-for=\u0026#34;(v, k) of car\u0026#34; :key=\u0026#34;k\u0026#34;\u0026gt; {{k}}: {{v}} \u0026lt;/li\u0026gt; \u0026lt;/ul\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;script\u0026gt; Vue.config.productionTip = false new Vue({ el: \u0026#39;#root\u0026#39;, data: { persons: [ {id: \u0026#39;001\u0026#39;, name: \u0026#39;张三\u0026#39;, age: 18}, {id: \u0026#39;002\u0026#39;, name: \u0026#39;李四\u0026#39;, age: 19}, {id: \u0026#39;003\u0026#39;, name: \u0026#39;王五\u0026#39;, age: 20}, ], car: { name: \u0026#39;A8\u0026#39;, price: \u0026#39;70W\u0026#39;, color: \u0026#39;黑色\u0026#39; } } }) \u0026lt;/script\u0026gt; v-for 可以用于遍历数组、对象、字符串以及次数。\nkey 的原理 key 是虚拟 DOM 对象的标识，当数据发生变化时，Vue 会根据新数据生成新的虚拟 DOM，随后 Vue 进行新旧虚拟 DOM 的差异对比。 虚拟 DOM 对比规则：\n旧虚拟 DOM 中存在与新虚拟 DOM key 相同的元素，如果虚拟 DOM 中内容没变，直接复用之前的真实 DOM。如果虚拟 DOM中内容发生变化，生成新的真实 DOM。 旧虚拟 DOM 中不存在与新虚拟 DOM key 相同的元素，生成新的真实 DOM，渲染到页面中。 使用 index 作为 key 时对数据进行破环顺序的操作，会产生没有必要的真实 DOM 更新，效率较低。如果结构中还包含输入类的元素，会产生错误的 DOM 更新。\nVue 监视数据的原理 Vue 会监视 data 中所有层次的数据。\n通过 setter 监视对象中的数据，且要在 new Vue 时传入监视的数据。对象中后追加的属性，Vue 默认不做响应式处理，可以使用如下 API 为添加的属性做响应式：\nVue.set(target, propertyName/index, value) vm.$set(target, propertyName/index, value) 通过包裹数组更新方法监视数组中的数据，即调用数组原生方法更新数据，然后重新解析模板。这些方法包括：push、pop、shift、unshift、splice、sort 和 reverse。\nVue.set() 和 vm.$set() 不能给 vm 或 vm 的根数据对象(_data) 添加属性。\n过滤器 \u0026lt;div id=\u0026#34;root\u0026#34;\u0026gt; \u0026lt;h1\u0026gt;当前时间\u0026lt;/h1\u0026gt; \u0026lt;h2\u0026gt;时间戳：{{time}}\u0026lt;/h2\u0026gt; \u0026lt;h2\u0026gt;格式化：{{fmtTime}}\u0026lt;/h2\u0026gt; \u0026lt;h2\u0026gt;格式化：{{getFmtTime()}}\u0026lt;/h2\u0026gt; \u0026lt;h2\u0026gt;格式化：{{time | timeFormatter(\u0026#39;YYYY年MM月DD日\u0026#39;)}}\u0026lt;/h2\u0026gt; \u0026lt;h2\u0026gt;首字母大写：{{\u0026#39;hello\u0026#39; | capitalize}}\u0026lt;/h2\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;script\u0026gt; Vue.config.productionTip = false Vue.filter(\u0026#39;capitalize\u0026#39;, function (val) { if (!val) { return \u0026#39;\u0026#39; } val = val.toString() return val.charAt(0).toUpperCase() + val.slice(1) }) new Vue({ el: \u0026#39;#root\u0026#39;, data: { time: 1669816105394 }, computed: { fmtTime() { return dayjs(this.time).format(\u0026#39;YYYY-MM-DD HH:mm:ss\u0026#39;) } }, methods: { getFmtTime() { return dayjs(this.time).format(\u0026#39;YYYY-MM-DD HH:mm:ss\u0026#39;) } }, filters: { timeFormatter(value, pattern = \u0026#39;YYYY-MM-DD HH:mm:ss\u0026#39;) { return dayjs(value).format(pattern) } } }) \u0026lt;/script\u0026gt; 过滤器可用于一些简单的操作，比如文本格式化。过滤器可以用在 {{}} 插值和 v-bind 表达式中，过滤器跟在管道符号 | 后面。\n使用过滤器的两种方式：\n使用 filters 配置项在组件中定义本地过滤器。 在创建 Vue 实例前使用 Vue.filter() 定义全局过滤器。 过滤器默认接受管道符号前表达式的值作为第一个参数。\n自定义指令 \u0026lt;div id=\u0026#34;root\u0026#34;\u0026gt; \u0026lt;h2\u0026gt;当前的 n：\u0026lt;span v-text=\u0026#34;n\u0026#34;\u0026gt;\u0026lt;/span\u0026gt;\u0026lt;/h2\u0026gt; \u0026lt;h2\u0026gt;放大的 n：\u0026lt;span v-big-number=\u0026#34;n\u0026#34;\u0026gt;\u0026lt;/span\u0026gt;\u0026lt;/h2\u0026gt; \u0026lt;button @click=\u0026#34;n++\u0026#34;\u0026gt;点击+1\u0026lt;/button\u0026gt; \u0026lt;br\u0026gt; \u0026lt;br\u0026gt; \u0026lt;input type=\u0026#34;text\u0026#34; v-fbind=\u0026#34;n\u0026#34;\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;script\u0026gt; Vue.config.productionTip = false // bind 和 update 触发相同行为 Vue.directive(\u0026#39;big-number\u0026#39;, function (element, binding) { element.innerText = binding.value * 10 }) new Vue({ el: \u0026#39;#root\u0026#39;, data: { n: 0 }, directives: { fbind: { bind(element, binding) { element.value = binding.value }, inserted(element) { element.focus() }, update(element, binding) { element.value = binding.value } } } }) \u0026lt;/script\u0026gt; 指令的两种注册方式 使用 Vue.directive() 注册全局指令。 使用 directives 配置项注册局部指令。 钩子函数 bind：只调用一次，指令第一次绑定到元素时调用，做初始化设置。 inserted：被绑定元素插入父节点时调用。 update：所在组件的 VNode 更新时调用。 钩子函数的参数：\nel：指令所绑定的真实 DOM。 binding：对象，包含一下属性： name：指令名，不包括 v- 前缀。 value：指令绑定的值。 …… Vue 生命周期 常用的生命周期钩子：\nmounted：发送请求、启动定时器、绑定自定义事件、订阅消息等初始化操作。 beforeDestroy：清除定时器、解绑自定义事件、取消订阅消息等收尾工作。 关于销毁 Vue 实例：\n销毁后自定义事件会失效、但原生 DOM 事件依然有效。 一般不会在 beforeDestory 中操作数据，即使操作了数据，也不会触发更新流程。 ","permalink":"https://2d992714.xianglin2020-github-io.pages.dev/posts/2022/vue-basics/","summary":"Vue 基础知识与语法","title":"Vue2 基础"},{"content":"MongoDB 基本操作 MongoDB 基本操作细节请参考官方文档：MongoDB CRUD Operations。\nCRUD 的基本命令如下：\nCreate：\ndb.collection.insertOne() db.collection.insertMany() Read：\ndb.collection.find() Update：\ndb.collection.updateOne() db.collection.updateMany() db.collection.replaceOne() Delete：\ndb.collection.deleteOne() db.collection.deleteMany() 插入文档 使用 db.collection.insertOne() 插入单条记录到集合中，如果插入的集合没有指定 _id 字段，MongoDB 会自动生成一个 ObjectId，如果集合不存在，那么插入时会创建对应的集合。\ndb.inventory.insertOne( { item: \u0026#34;canvas\u0026#34;, qty: 100, tags: [\u0026#34;cotton\u0026#34;], size: { h: 28, w: 35.5, uom: \u0026#34;cm\u0026#34; } }) 插入成功后会返回记录对应的 _id。\n插入多条记录和插入单条记录一样，只需要使用 db.collection.insertMany()：\ndb.inventory.insertMany([ { item: \u0026#34;journal\u0026#34;, qty: 25, tags: [\u0026#34;blank\u0026#34;, \u0026#34;red\u0026#34;], size: { h: 14, w: 21, uom: \u0026#34;cm\u0026#34; } }, { item: \u0026#34;mat\u0026#34;, qty: 85, tags: [\u0026#34;gray\u0026#34;], size: { h: 27.9, w: 35.5, uom: \u0026#34;cm\u0026#34; } }, { item: \u0026#34;mousepad\u0026#34;, qty: 25, tags: [\u0026#34;gel\u0026#34;, \u0026#34;blue\u0026#34;], size: { h: 19, w: 22.85, uom: \u0026#34;cm\u0026#34; } } ]) 插入行为 如果集合不存在，插入操作会新建集合；\n集合中的每条文档记录需要一个唯一的 _id 字段作为主键，如果插入的文档没有提供 _id 字段，MongoDB 会自动生成一个 ObjectId 类型的 _id 作为主键；\nMongoDB 中的所有写操作在单个文档级别上都是原子的。\n查询文档 查询选择器：query-selectors。\n基本查询操作 使用如下测试数据：\ndb.inventory.insertMany([ { item: \u0026#34;journal\u0026#34;, qty: 25, size: { h: 14, w: 21, uom: \u0026#34;cm\u0026#34; }, status: \u0026#34;A\u0026#34; }, { item: \u0026#34;notebook\u0026#34;, qty: 50, size: { h: 8.5, w: 11, uom: \u0026#34;in\u0026#34; }, status: \u0026#34;A\u0026#34; }, { item: \u0026#34;paper\u0026#34;, qty: 100, size: { h: 8.5, w: 11, uom: \u0026#34;in\u0026#34; }, status: \u0026#34;D\u0026#34; }, { item: \u0026#34;planner\u0026#34;, qty: 75, size: { h: 22.85, w: 30, uom: \u0026#34;cm\u0026#34; }, status: \u0026#34;D\u0026#34; }, { item: \u0026#34;postcard\u0026#34;, qty: 45, size: { h: 10, w: 15.25, uom: \u0026#34;cm\u0026#34; }, status: \u0026#34;A\u0026#34; } ]); 使用 db.collection.find({}) 查询文档，查询所有记录：\ndb.inventory.find( {} ) 指定相等条件查询：status 等于 D\ndb.inventory.find( { status: \u0026#34;D\u0026#34; } ) 指定 AND 条件查询：status 等于 A 且 qty 小于 30\ndb.inventory.find({status: \u0026#34;A\u0026#34;, qty:{$lt: 30}}) 指定 OR 查询条件：status 等于 A 或 qty 小于 30\ndb.inventory.find({$or: [{status: \u0026#34;A\u0026#34;}, {qty: {$lt: 30}}]}) 同时指定 AND 和 OR 查询条件：status 等于 A 并且 qty 小于 30 或者 item 以 p 开头：\ndb.inventory.find( { status: \u0026#34;A\u0026#34;, $or: [ { qty: { $lt: 30 } }, { item: /^p/ } ] } ) 查询嵌套文档 使用如下测试数据：\ndb.inventory.insertMany( [ { item: \u0026#34;journal\u0026#34;, qty: 25, size: { h: 14, w: 21, uom: \u0026#34;cm\u0026#34; }, status: \u0026#34;A\u0026#34; }, { item: \u0026#34;notebook\u0026#34;, qty: 50, size: { h: 8.5, w: 11, uom: \u0026#34;in\u0026#34; }, status: \u0026#34;A\u0026#34; }, { item: \u0026#34;paper\u0026#34;, qty: 100, size: { h: 8.5, w: 11, uom: \u0026#34;in\u0026#34; }, status: \u0026#34;D\u0026#34; }, { item: \u0026#34;planner\u0026#34;, qty: 75, size: { h: 22.85, w: 30, uom: \u0026#34;cm\u0026#34; }, status: \u0026#34;D\u0026#34; }, { item: \u0026#34;postcard\u0026#34;, qty: 45, size: { h: 10, w: 15.25, uom: \u0026#34;cm\u0026#34; }, status: \u0026#34;A\u0026#34; } ]); 匹配嵌入文档（要求文档内的键值及顺序完全匹配）：size 等于 { h: 14, w: 21, uom: \u0026quot;cm\u0026quot; }\ndb.inventory.find({size:{ h: 14, w: 21, uom: \u0026#34;cm\u0026#34; }}) 查询嵌套字段：查询 size 中 uom 值为 in 的文档\ndb.inventory.find({\u0026#39;size.uom\u0026#39;:\u0026#39;in\u0026#39;}) 查询简单元素 Array 使用如下测试数据：\ndb.inventory.insertMany([ { item: \u0026#34;journal\u0026#34;, qty: 25, tags: [\u0026#34;blank\u0026#34;, \u0026#34;red\u0026#34;], dim_cm: [ 14, 21 ] }, { item: \u0026#34;notebook\u0026#34;, qty: 50, tags: [\u0026#34;red\u0026#34;, \u0026#34;blank\u0026#34;], dim_cm: [ 14, 21 ] }, { item: \u0026#34;paper\u0026#34;, qty: 100, tags: [\u0026#34;red\u0026#34;, \u0026#34;blank\u0026#34;, \u0026#34;plain\u0026#34;], dim_cm: [ 14, 21 ] }, { item: \u0026#34;planner\u0026#34;, qty: 75, tags: [\u0026#34;blank\u0026#34;, \u0026#34;red\u0026#34;], dim_cm: [ 22.85, 30 ] }, { item: \u0026#34;postcard\u0026#34;, qty: 45, tags: [\u0026#34;blue\u0026#34;], dim_cm: [ 10, 15.25 ] } ]); 匹配数组（要求数组值及顺序完全匹配）：tags 等于 [\u0026quot;blank\u0026quot;, \u0026quot;red\u0026quot;]\ndb.inventory.find({tags:[\u0026#39;blank\u0026#39;,\u0026#39;red\u0026#39;]}) 匹配数组中部分元素（不要求值及顺序完全匹配）：tags 包含 blank 和 red\ndb.inventory.find({tags:{$all:[\u0026#39;blank\u0026#39;,\u0026#39;red\u0026#39;]}}) 匹配数组中某个元素：tags 包含 red\ndb.inventory.find({tags:\u0026#39;red\u0026#39;}) dim_cm 中有大于 25 的元素：\ndb.inventory.find({dim_cm:{$gt:25}}) dim_cm 中包含大于 15 或小于 20 的元素：\ndb.inventory.find({dim_cm:{$gt:15,$lt:20}}) dim_cm 中包含大于 15 且小于 20 的元素：\ndb.inventory.find({dim_cm:{$elemMatch:{$gt:15,$lt:20}}}) 按数组索引位置查询元素：dim_cm 中第二个元素大于 20\ndb.inventory.find({\u0026#39;dim_cm.1\u0026#39;:{$gt:20}}) 按数组长度查询数组：tags 数组有三个元素\ndb.inventory.find({tags:{$size:3}}) 查询嵌套文档数组 使用如下测试数据：\ndb.inventory.insertMany( [ { item: \u0026#34;journal\u0026#34;, instock: [ { warehouse: \u0026#34;A\u0026#34;, qty: 5 }, { warehouse: \u0026#34;C\u0026#34;, qty: 15 } ] }, { item: \u0026#34;notebook\u0026#34;, instock: [ { warehouse: \u0026#34;C\u0026#34;, qty: 5 } ] }, { item: \u0026#34;paper\u0026#34;, instock: [ { warehouse: \u0026#34;A\u0026#34;, qty: 60 }, { warehouse: \u0026#34;B\u0026#34;, qty: 15 } ] }, { item: \u0026#34;planner\u0026#34;, instock: [ { warehouse: \u0026#34;A\u0026#34;, qty: 40 }, { warehouse: \u0026#34;B\u0026#34;, qty: 5 } ] }, { item: \u0026#34;postcard\u0026#34;, instock: [ { warehouse: \u0026#34;B\u0026#34;, qty: 15 }, { warehouse: \u0026#34;C\u0026#34;, qty: 35 } ] } ]); 以文档形式匹配数组中某个元素：\ndb.inventory.find({instock:{warehouse:\u0026#39;A\u0026#39;,qty:5}}) 在文档数组中某个字段上指定查询条件：\ndb.inventory.find({\u0026#39;instock.qty\u0026#39;:{$lt:20}}) 在数组中指定索引的文档某个字段上指定查询条件：\ndb.inventory.find({\u0026#39;instock.0.qty\u0026#39;:{$lte:20}}) 查询 null 或不存在字段 使用如下测试数据：\ndb.inventory.insertMany([ { _id: 1, item: null }, { _id: 2 } ]) 查询字段存在且等于 null 或者字段不存在：\ndb.inventory.find({item:null}) 类型检查 bson-types，查询查询字段存在且等于 null ：\ndb.inventory.find({item:{$type:10}}) 存在检查：\ndb.inventory.find( { item : { $exists: false } } ) 更新文档 使用如下测试数据：\ndb.inventory.insertMany( [ { item: \u0026#34;canvas\u0026#34;, qty: 100, size: { h: 28, w: 35.5, uom: \u0026#34;cm\u0026#34; }, status: \u0026#34;A\u0026#34; }, { item: \u0026#34;journal\u0026#34;, qty: 25, size: { h: 14, w: 21, uom: \u0026#34;cm\u0026#34; }, status: \u0026#34;A\u0026#34; }, { item: \u0026#34;mat\u0026#34;, qty: 85, size: { h: 27.9, w: 35.5, uom: \u0026#34;cm\u0026#34; }, status: \u0026#34;A\u0026#34; }, { item: \u0026#34;mousepad\u0026#34;, qty: 25, size: { h: 19, w: 22.85, uom: \u0026#34;cm\u0026#34; }, status: \u0026#34;P\u0026#34; }, { item: \u0026#34;notebook\u0026#34;, qty: 50, size: { h: 8.5, w: 11, uom: \u0026#34;in\u0026#34; }, status: \u0026#34;P\u0026#34; }, { item: \u0026#34;paper\u0026#34;, qty: 100, size: { h: 8.5, w: 11, uom: \u0026#34;in\u0026#34; }, status: \u0026#34;D\u0026#34; }, { item: \u0026#34;planner\u0026#34;, qty: 75, size: { h: 22.85, w: 30, uom: \u0026#34;cm\u0026#34; }, status: \u0026#34;D\u0026#34; }, { item: \u0026#34;postcard\u0026#34;, qty: 45, size: { h: 10, w: 15.25, uom: \u0026#34;cm\u0026#34; }, status: \u0026#34;A\u0026#34; }, { item: \u0026#34;sketchbook\u0026#34;, qty: 80, size: { h: 14, w: 21, uom: \u0026#34;cm\u0026#34; }, status: \u0026#34;A\u0026#34; }, { item: \u0026#34;sketch pad\u0026#34;, qty: 95, size: { h: 22.85, w: 30.5, uom: \u0026#34;cm\u0026#34; }, status: \u0026#34;A\u0026#34; } ] ); Update Operators\n","permalink":"https://2d992714.xianglin2020-github-io.pages.dev/posts/2022/mongodb-crud-basics/","summary":"MongoDB CRUD 基础操作。","title":"MongoDB-CRUD操作"},{"content":"MongoDB 基础知识 为「MongoDB高手课 - 极客时间视频课程」第一部分「MongoDB 再入门」的学习记录，相关内容地址如下：\n视频教程：MongoDB教程全集\n课件资料：geektime-mongodb-course\nMongoDB 介绍 OLTP 和 OLAP OLTP与OLAP的关系：OLTP（On-Line Transaction Processing）翻译为联机事务处理， OLAP（On-Line Analytical Processing）翻译为联机分析处理，从字面上来看 OLTP 是做事务处理，OLAP 是做分析处理。从对数据库操作来看，OLTP 主要是对数据的增删改，OLAP 是对数据的查询。\n关于 MongoDB MongoDB 是一个以 JSON 为数据模型的文档型数据库（JSON Document），属于 OLTP 数据库。\n主要用途：\n应用数据库，类似于 MySQL、Oracle 海量数据处理 数据平台 主要特点：\n建模过程为可选步骤 JSON 数据模型比较适合开发者 横向扩展能力可以支持很大数据量和并发 MongoDB 和关系型数据库对比 MongoDB RDBMS 数据模型 文档模型 关系模型 数据库类型 OLTP OLTP DML MQL/SQL SQL 高可用实现方式 复制集 集群模式 横向扩展能力 通过原生分片完善支持 数据分区或者应用程序分区处理 索引支持 B 树、全文索引、地理位置索引、多键索引、TTL 索引 B+树 数据容量 没有理论上线 千万、亿级 MongoDB 特色及优势 MongoDB 的优势：\nJSON 结构和对象模型接近，开发代码量低； JSON 的动态模型意味着更容易响应新的业务需求； 复制集提供 99.999% 的高可用能力； 分片架构支持海量数据和无缝扩容。 面向开发者的易用且高效数据库 简单直观：以自然的方式建模，以直观的方式来与数据库交互；\n结构灵活：弹性模式从容响应需求的频繁变化；\nMongoDB 的灵活体现在三个方面：\n多型性：同一个集合中可以包含不同字段（类型）的文档对象；\n动态性：线上修改数据模式，修改时应用和数据库均无须下线；\n数据治理：支持使用 JSON Schema 来规范数据模式。在保证数据模式灵活动态的前提下，提供数据治理能力。\n快速开发：以更少的代码完成功能，专注于业务逻辑。\n原生的高可用和横向扩展能力 MongoDB 通过复制集提供了高可用能力。\nMongoDB 通过分片提供横向扩展能力。特点有：对应用全透明、提供多种数据分布策略、轻松支持 TB ～ PB 的数量级。\n安装 MongoDB 下载 MongoDB 社区版下载地址：https://www.mongodb.com/try/download/community\n官方安装教程：https://www.mongodb.com/docs/manual/administration/install-community/\n以在 CentOS7 虚拟机上安装使用 MongoDB 社区版二进制包为例：\n获取可执行文件地址 在 MongoDB 下载界面中选择「On-permises」—「MongoDB Community Server」，选择合适的版本及平台后，点击「Copy Link」复制下载连接：\nhttps://fastdl.mongodb.org/linux/mongodb-linux-x86_64-rhel70-5.0.8.tgz\n下载 MongoDB 可执行文件 创建 MongoDB 下载目录和数据文件存放目录：\nmkdir -p /data/db 下载并解压 MongoDB 可执行文件：\ncurl -O https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-rhel70-5.0.8.tgz tar -xf mongodb-linux-x86_64-rhel70-5.0.8.tgz 将 MongoDB 可执行文件加入 PATH 中：\nexport PATH=$PATH:/data/mongodb-linux-x86_64-rhel70-5.0.8/bin 启动 MongoDB 启动 MongoDB ：\nmongod --dbpath /data/db --port 27017 --logpath /data/db/mongod.log --fork -bind_ip 0.0.0.0 连接到 MongoDB：\nmongo localhost:27017 开放 27017 端口，供外部访问：\nfirewall-cmd --add-port=27017/tcp --permanent firewall-cmd --reload 使用 Atlas 免费帐号 Atlas 是 MongoDB 提供的数据托管平台，并且给开发者提供了一个免费测试帐号。\n注册帐号 在 https://www.mongodb.com/cloud/atlas/register 中输入基本信息，然后验证邮箱即可：\n创建 Atlas 添加允许连接的 IP 列表，可以使用 0.0.0.0/0 表示允许任意 IP 连接。\n连接到 Cluster 在 Altas 控制台中点击「Connect」按钮，在「Setup connection security」中选择通过哪种方式连接。这里选择使用 Java 客户端连接，故选择「Connect your application」。\n选择对应的 Java 驱动版本，复制生成的连接字符串到应用程序中。\n连接到 Cluster 和 连接本地 MongoDB了一样，示例代理如下：\npackage store.xianglin.mongo; import com.mongodb.client.MongoClients; import org.bson.Document; /** * 连接 Mongo Atlas * * @author xianglin */ public class QuickStartClusterExample { public static void main(String[] args) { final var connectionString = \u0026#34;mongodb+srv://xianglin:\u0026lt;password\u0026gt;@cluster0.boqb4.mongodb.net/?retryWrites=true\u0026amp;w=majority\u0026#34;; try (var mongoClient = MongoClients.create(connectionString)) { final var database = mongoClient.getDatabase(\u0026#34;mock\u0026#34;); final var collection = database.getCollection(\u0026#34;order\u0026#34;); collection.insertOne(new Document(\u0026#34;key1\u0026#34;, \u0026#34;value1\u0026#34;).append(\u0026#34;key2\u0026#34;, \u0026#34;value2\u0026#34;)); var iter = collection.find(); iter.forEach(System.out::println); } } } 随后到 Atlas 控制台中查看刚刚插入的记录：\n导入样本数据 导入课程提供的样本数据：\nmongorestore 命令在 Database Tools 中，需要单独安装，或者按照官方教程安装。\ncurl -O -k https://github.com/geektime-geekbang/geektime-mongodb-course/blob/master/aggregation/dump.tar.gz tar -xf dump.tar.gz mongorestore -h localhost:27017 下载和使用 MongoDB Compass 下载地址：https://www.mongodb.com/try/download/compass\n我这里使用 Manjaro 的包管理工具安装：\nyay mongodb-compass 然后连接到刚刚启动的 MongoDB：\nMongoDB 基本操作 MongoDB 基本操作细节请参考官方文档：MongoDB CRUD Operations。\nCRUD 的基本命令如下：\nCreate：\ndb.collection.insertOne() db.collection.insertMany() Read：\ndb.collection.find() Update：\ndb.collection.updateOne() db.collection.updateMany() db.collection.replaceOne() Delete：\ndb.collection.deleteOne() db.collection.deleteMany() 使用 MongoDB 开发程序 Java API 的使用细节请参考官方教程：MongoDB Java Driver。\n创建简单应用 编写一个简单的 Java 应用查询一条样本数据，数据如下：\n创建 Maven 项目 在 IDEA 中使用 Maven 创建一个简单 Java 应用：\n引入 MongoDB Java 驱动的依赖：\n\u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.mongodb\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;mongodb-driver-sync\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;4.6.0\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; 查询导入的样本数据 package store.xianglin.mongo; import com.mongodb.client.MongoClients; import com.mongodb.client.model.Filters; import java.util.Objects; /** * connect mongo use Java * * @author xianglin */ public class QuickStart { public static void main(String[] args) { final var connectionString = \u0026#34;mongodb://192.168.56.101:27017/mock\u0026#34;; try (final var mongoClient = MongoClients.create(connectionString)) { final var dataBaseMock = mongoClient.getDatabase(\u0026#34;mock\u0026#34;); final var collectionOrders = dataBaseMock.getCollection(\u0026#34;orders\u0026#34;); var documentOrder = collectionOrders.find(Filters.eq(\u0026#34;street\u0026#34;, \u0026#34;493 Hilll Curve\u0026#34;)).first(); System.out.println(Objects.requireNonNull(documentOrder).toJson()); } } } 使用 POJOs 查询 Document 的实现是一个 LinkedHashMap，实际上使用时，更倾向于使用 POJO。先创建一个 Order 的实体类（省略 getter 和 setter 方法）：\npublic class Order { private String street; private String city; private String state; private String country; private String zip; private String phone; private String name; private int userId; private LocalDateTime orderDate; private String status; private BigDecimal shippingFee; private List\u0026lt;OrderLine\u0026gt; orderLines; private BigDecimal total; public static class OrderLine { private String product; private String sku; private int qty; private BigDecimal price; private BigDecimal cost; } } 需要注意的有两点：\nMongoDB 中 Date 类型的 orderDate 在 Java 中可以使用 java.timeLocalDateTime 也可以使用 java.util.Date； MongoDB 中的 Decimal128 对应 Java 中的 java.math.BigDecimal。 使用 Order 查询的方法如下：\npackage store.xianglin.mongo; import com.mongodb.MongoClientSettings; import com.mongodb.client.MongoClients; import com.mongodb.client.model.Filters; import org.bson.codecs.configuration.CodecRegistries; import org.bson.codecs.pojo.PojoCodecProvider; /** * connect mongo use java with pojos * * @author xianglin */ public class QuickStartPojoExample { public static void main(String[] args) { final var pojoCodecProvider = PojoCodecProvider.builder().automatic(true).build(); final var codecRegistries = CodecRegistries.fromRegistries(MongoClientSettings.getDefaultCodecRegistry(), CodecRegistries.fromProviders(pojoCodecProvider)); final var connectionString = \u0026#34;mongodb://192.168.56.101:27017/mock\u0026#34;; try (final var mongoClient = MongoClients.create(connectionString)) { final var dataBaseMock = mongoClient.getDatabase(\u0026#34;mock\u0026#34;).withCodecRegistry(codecRegistries); final var collectionOrders = dataBaseMock.getCollection(\u0026#34;orders\u0026#34;, Order.class); var order = collectionOrders.find(Filters.eq(\u0026#34;street\u0026#34;, \u0026#34;493 Hilll Curve\u0026#34;)).first(); System.out.println(order); } } } ","permalink":"https://2d992714.xianglin2020-github-io.pages.dev/posts/2022/mongodb-basics/","summary":"MongoDB 入门知识，包括 MongoDB 介绍、安装、基本操作、复制集等内容。","title":"MongoDB 基础知识"},{"content":"Ubuntu 安装 MySQL Ubuntu 安装 推荐从 USTC 下载镜像。\n创建 VirtualBox 虚拟机 点击「新建」，名称输入 Ubuntu，类型和版本会自动选择 Ubuntu(64-bit)：\n使用 Ubuntu 桌面版时，建议调整「系统」栏中的「处理器数量」和「显示」栏中的「显存大小」：\n双击启动，选择已下载的 Ubuntu 安装镜像，进入安装界面：\n安装 Ubuntu 在左边下拉选择「中文（简体）」：\n推荐选择「最小安装」：\n没有特殊情况不需要自己分区，选择「清除整个磁盘并安装 Ubuntu」即可：\n安装完成后，即可进入虚拟机：\nUbuntu 基础配置 更换国内源 可以参考Ubuntu 源使用帮助，首先打开「软件和更新」，在「下载自」选择「其它站点」然后在「中国」条目下选择合适的源：\n点击「重新载入」刷新本地缓存即可：\n安装搜狗输入法 下载Ubuntu搜狗输入法：https://pinyin.sogou.com/linux/?r=pinyin\nsudo dpkg -i sogoupinyin_2.3.2.07_amd64-831.deb # 提示缺少依赖时，使用如下命令 sudo apt install -f ","permalink":"https://2d992714.xianglin2020-github-io.pages.dev/posts/2022/virtualbox-install-ubuntu/","summary":"使用 VirtualBox 安装 Ubuntu 桌面版，并更换软件源。","title":"Virtual Box 安装配置 Ubuntu"},{"content":"Virtual Box 安装配置 CentOS7 下载安装 下载 CentOS7 镜像 通过清华大学开源软件镜像站，下载 CentOS7 的安装镜像，如图所示：\n安装 CentOS7 在 VirtualBox 中创建虚拟机的过程比较简单，安装提示即可，如下图所示：\n进入安装界面后，首先需要选择安装过程中使用的语言：\n在「安装信息摘要」中，「本地化」和「软件」两栏的内容保持不变：\n在「安装位置」中选中虚拟磁盘：\n在「网络和主机名」中打开网络：\n设置完成后点击「开始安装」：\n安装过程中需要设置「ROOT密码」，并按需创建用户：\n如果是”弱密码“，需要点击两次「完成」按钮：\n等待 CentOS7 安装完成即可：\n基础配置 配置 Host-Only 网络并指定静态 IP 虚拟机内 CentOS 的默认终端很不好用，一般都是通过 SSH 从宿主机连接使用，这时可以开启 Host-Only 网络并指定一个静态 IP。\n为 CentOS7 增加 Host-Only 网络配置 在关机状态下，打开虚拟机设置，在「网络」栏中新增网卡：\n如果在「界面名称」中只有「未指定」一个选项，并且下方有“发现无效设置”时，需要先添加网络配置：\n在「管理」—「主机网络管理器」中新增一条 Host-Only 网络配置：\n完成后，再到虚拟机「设置」—「网络」中选择即可：\n设置完成后，进入虚拟机，使用 ip addr 命令查看当前网络信息，可以发现已存在 Host-Only 网络：\n为新增的 Host-Only 网络配置静态 IP CentOS7 的网络配置文件位于 /etc/sysconfig/network-scripts，并与设备名称同名。即需要编辑 /etc/sysconfig/network-scripts/ifcfg-enp0s8 文件：\nTYPE=Ethernet PROXY_METHOD=none BROWSER_ONLY=no # 设置为静态 IP BOOTPROTO=static DEFROUTE=yes IPV4_FAILURE_FATAL=no IPV6INIT=yes IPV6_AUTOCONF=yes IPV6_DEFROUTE=yes IPV6_FAILURE_FATAL=no IPV6_ADDR_GEN_MODE=stable-privacy NAME=enp0s8 UUID=76b90704-1ecd-4c8e-a411-9c776cb7834d DEVICE=enp0s8 # 设置开启启动 ONBOOT=yes # 添加静态 IP 项 IPADDR=192.168.56.106 NETMASK=255.255.255.0 GATEWAY=192.168.56.1 同时可以在 /etc/sysconfig/network 中添加 DNS 配置：\n# Created by anaconda DNS1=192.168.56.1 DNS2=8.8.8.8 最后重启网络配置即可：\nservice network restart 再次使用 ip addr 命令查看网络信息，发现配置已经生效：\n为 CentOS7 安装增强功能 CentOS Server 无法从「设备」—「安装增强功能」菜单为其安装增强功能，可以按照以下步骤手动安装。\n安装依赖包：\n[root@localhost ~]# yum install -y gcc gcc-c++ make kernel kernel-headers kernel-devel bzip2 tar [root@localhost ~]# reboot 从菜单中挂载 VBoxGuestAdditions.iso ：\n使用如下命令挂载光驱：\n# 创建挂载点 [root@localhost ~]# mkdir /media/cdrom # 挂载光驱 [root@localhost ~]# mount /dev/sr0 /media/cdrom mount: /dev/sr0 写保护，将以只读方式挂载 [root@localhost ~]# ll /media/cdrom/ 总用量 46826 -r--r--r--. 1 root root 763 2月 20 2020 AUTORUN.INF -r-xr-xr-x. 1 root root 6384 10月 15 22:42 autorun.sh dr-xr-xr-x. 2 root root 792 10月 15 22:48 cert dr-xr-xr-x. 2 root root 1824 10月 15 22:48 NT3x dr-xr-xr-x. 2 root root 2652 10月 15 22:48 OS2 -r-xr-xr-x. 1 root root 4821 10月 15 22:42 runasroot.sh -r--r--r--. 1 root root 547 10月 15 22:48 TRANS.TBL -r--r--r--. 1 root root 3830063 10月 15 22:41 VBoxDarwinAdditions.pkg -r-xr-xr-x. 1 root root 3949 10月 15 22:41 VBoxDarwinAdditionsUninstall.tool -r-xr-xr-x. 1 root root 7413172 10月 15 22:42 VBoxLinuxAdditions.run -r--r--r--. 1 root root 9401856 10月 15 23:41 VBoxSolarisAdditions.pkg -r-xr-xr-x. 1 root root 16950792 10月 15 22:45 VBoxWindowsAdditions-amd64.exe -r-xr-xr-x. 1 root root 270616 10月 15 22:42 VBoxWindowsAdditions.exe -r-xr-xr-x. 1 root root 10057608 10月 15 22:43 VBoxWindowsAdditions-x86.exe 执行安装命令，安装完成后重启即可：\n[root@localhost cdrom]# sh ./VBoxLinuxAdditions.run Verifying archive integrity... All good. Uncompressing VirtualBox 6.1.16 Guest Additions for Linux........ VirtualBox Guest Additions installer Removing installed version 6.1.16 of VirtualBox Guest Additions... Copying additional installer modules ... Installing additional modules ... VirtualBox Guest Additions: Starting. VirtualBox Guest Additions: Building the VirtualBox Guest Additions kernel modules. This may take a while. VirtualBox Guest Additions: To build modules for other installed kernels, run VirtualBox Guest Additions: /sbin/rcvboxadd quicksetup \u0026lt;version\u0026gt; VirtualBox Guest Additions: or VirtualBox Guest Additions: /sbin/rcvboxadd quicksetup all VirtualBox Guest Additions: Building the modules for kernel 3.10.0-1160.6.1.el7.x86_64. VirtualBox Guest Additions: Running kernel modules will not be replaced until the system is restarted [root@localhost ~]# reboot 使用 「共享文件夹」验证增强功能安装成功，在「共享文件夹」中新增一条配置，将宿主机的下载目录挂载到虚拟机的 Downloads 下，然后进入虚拟机查看到对应路径存在，代表安装成功：\n","permalink":"https://2d992714.xianglin2020-github-io.pages.dev/posts/2022/virtualbox-install-centos/","summary":"使用 VirtualBox 安装 CentOS 服务器版，并安装增强功能。","title":"Virtual Box 安装配置 CentOS"},{"content":"Manjaro 安装配置记录 安装步骤 以在 Virtual Box 虚拟机中安装为例，介绍如何安装 Manjaro。\n下载 Manjaro 镜像 下载地址，目前 Manjaro 官方提供三种桌面：xface、kde 和 gnome，可以选择自己喜欢的版本下载。\n创建虚拟机 在 Virtual Box 管理界面点击「新建」，填写虚拟机的基本信息，其中「版本」选择「Arch Linux（64-bit）」：\n创建完成后，选中创建的虚拟机，右键点击「设置」进入设置页面，在「系统」栏调整「处理器数量」，在「显示」栏调整「显存大小」：\n设置完成后，双击启动虚拟机，首次启动时，会提示选择虚拟光盘（即 Manjaro 的镜像文件），按提示选择已下载好的 Manjaro 镜像文件（忘记截图了，以 Ubuntu 的镜像为例），即可进入安装界面：\n安装 Manjaro 可以在 Manjaro 的引导界面调整一些参数，比如时区、语言、键盘布局等，这里选择「Boot with open source drivers」进入 Manjaro 的 LiveCD 界面：\n点击「Luanch installer」进入安装界面：\n根据「Manjaro Linux 安装程序」的引导步骤完成对应到设置即可顺利安装：\n在「分区」这里，如果没有特殊需求，可以不做调整，如果想要「手动分区」或者设置「交换分区」，推荐阅读 Arch Linux 的安装教程：建立硬盘分区。\n在「用户」栏推荐选中「为管理员帐号使用同样的密码」：\n在「摘要」栏会展示即将安装的概要信息，确认后点击「安装」即可：\n安装结束后即可重启虚拟机，进入新装的 Manjaro 系统中：\n基础配置 修改源配置 参见：Manjaro Linux 源使用帮助。\n选择国内镜像站提升体验，使用如下命令生成可用中国镜像站列表：\nsudo pacman-mirrors -i -c China -m rank 勾选最接近的镜像站，然后按两次 OK ，最后使用如下命令刷新缓存：\nsudo pacman -Syy\n添加 Arch Linux CN 源 参见：Arch Linux CN 源使用帮助。\nArch Linux 中文社区仓库是由 Arch Linux 中文社区驱动的非官方用户仓库。包含中文用户常用软件、工具、字体/美化包等。\n在 /etc/pacman.conf 文件末尾添加两行：\n[archlinuxcn] Server = https://mirrors.ustc.edu.cn/archlinuxcn/$arch 然后请安装 archlinuxcn-keyring 包以导入 GPG key：\nsudo pacman -Sy archlinuxcn-keyring yay 是一个用 Go 语言编码的 Arch Linux 帮助工具和包管理器，允许自动从 PKGBUILD 安装包，使用如下命令安装：\nsudo pacman -S --needed git base-devel yay 使用英文目录 默认情况下，Manjaro 会根据语言环境创建中文目录，比如：文档、图片等，可以通过以下两步调整为英文目录：\n参见： XDG user directories (简体中文)\n在终端中输入如下命令\nLC_ALL=C xdg-user-dirs-update --force 重新登录系统，选择“保留旧的名称”\n安装中文输入法 参见：IBus_GNOME\nGnome 默认使用 IBus，所以只需要安装输入法引擎，这里选择 ibus-rime ，一个强大的智能中文输入法，支持拼音、注音或者没有音调的拼音、双拼、粤拼、中州韵、仓颉和五笔 86：\nsudo pacman -S ibus-rime 随后打开设置界面，通过“键盘”中的“输入源”添加：\n安装 Microsoft Edge 使用 yay 安装所需的版本：\nyay microsoft-edge 使用 OneDrive 参见：OneDrive Client for Linux\n安装 OneDrive 使用如下命令安装 OneDrive：\npacman -Ss onedrive-abraunegg sudo pacman -S onedrive-abraunegg 配置 OneDrive 同步文件 使用如下命令启动文件同步：\nonedrive --synchronize 开机自启动 使用如下命令将 onedrive 添加为系统服务，设置为开机自启动：\nsystemctl --user enable onedrive systemctl --user start onedrive 科学上网 这里使用 clash 作为客户端，具体参见：Clash。\n安装 使用如下命令安装 clash 客户端：\nsudo pacman -S clash 配置 下载订阅配置到 ~/.config/clash/config.yaml，配置如下：\nport: 7890 socks-port: 7891 allow-lan: false mode: Rule log-level: info external-controller: 127.0.0.1:9090 随后打开 http://clash.razord.top/#/settings 进入配置页面：\n开机自启动 在 /usr/share/applications/ 创建 clash_start.desktop 文件，内容如下：\n[Desktop Entry] Exec=/usr/bin/clash Name=Clash Type=Application StartupNotify=false Terminal=false Hidden=false 然后在「优化」—「开机启动程序」中选择 clash，实际上是在 ~/.config/autostart 中创建了相同的文件：\n开发工具安装及配置 Typora 安装 Typora 是一款 Markdown 编辑器和阅读器，可以通过 pacman 直接安装：\nsudo pacman -S typora 自动上传图片 Typora 支持自动上传图片至图床，比如 GitHub，参见 Upload Images，我这里选择使用 Upgit 作为上传工具。首先从 GitHub 上下载 upgit 可执行文件，然后在可执行文件同级目录下增加配置文件config.toml，内容如下：\n# ============================================================================= # UPGIT Config # ============================================================================= # default uploader id default_uploader = \u0026#34;github\u0026#34; # The file name formatting template was uploaded rename = \u0026#34;{year}{month}/{hour}{minute}{second}{ext}\u0026#34; # ----------------------------------------------------------------------------- # Custom extra output formats # ----------------------------------------------------------------------------- # {url} direct URL of the file [output_formats] \u0026#34;bbcode\u0026#34; = \u0026#34;[img]{url}[/img]\u0026#34; \u0026#34;html\u0026#34; = \u0026#39;\u0026lt;img src=\u0026#34;{url}\u0026#34; /\u0026gt;\u0026#39; \u0026#34;markdown-simple\u0026#34; = \u0026#34;![]({url})\u0026#34; # If your network access to Github is abnormal or sluggish, you can try the following CDN acceleration. [replacements] \u0026#34;raw.githubusercontent.com\u0026#34; = \u0026#34;cdn.jsdelivr.net/gh\u0026#34; \u0026#34;/master\u0026#34; = \u0026#34;@master\u0026#34; # Github uploader [uploaders.github] # Branch to save files, for example master or main branch = \u0026#34;master\u0026#34; # \u0026#34;pat\u0026#34; enter the Github token that has the \u0026#34;repo\u0026#34; permission # Get token from https://github.com/settings/tokens pat = \u0026#34;ghp_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX\u0026#34; # The name of your public Github repository repo = \u0026#34;gallery\u0026#34; # your Github username username = \u0026#34;xianglin2020\u0026#34; 主要是填写 repo 、 username 和 pat 三项，其中 replacements 用于 CDN 加速。\n最后在 Typora 中配置使用 upgit ，「上传服务」选择「Custom Command」，命令填写 upgit 执行路径，点击「验证图片上传选项」，如下图所示：\n生成 Lisence 参见：Typora。\nOpen JDK 参见：Java (简体中文)。\n使用 pacman 查找和安装 Open JDK，如下所示：\n# 查找 JDK pacman -Ss openjdk | grep jdk # 安装 JDK 17 sudo pacman -S jdk-openjdk # 安装 JDK 11 sudo pacman -S jdk11-openjdk 使用 archlinux-java 命令管理已安装的 JDK：\n查看已安装的 JDK：\narchlinux-java status 修改默认 JDK：\nsudo archlinux-java set java-11-openjdk Intellij IDEA 使用 pacman 查找和安装 Intellij IDEA，如下所示：\npacman -Ss intellij IDEA 中文输入法候选框不跟随光标的解决办法参见：JetBrainsRuntime-for-Linux-x64。\nMaven IDEA 自带 Maven，但是为了在终端使用，可以单独安装。使用如下命令安装最新版 Maven：\nsudo pacman -S maven 使用如下命令查看 Maven 安装路径：\nls -l `which mvn` 复制一份 settings.xml 到 ~/.m2 目录下，并修改使用阿里云的 Maven 仓库：\ncp /opt/maven/conf/settings.xml ~/.m2/ \u0026lt;mirror\u0026gt; \u0026lt;id\u0026gt;aliyunmaven\u0026lt;/id\u0026gt; \u0026lt;mirrorOf\u0026gt;*\u0026lt;/mirrorOf\u0026gt; \u0026lt;name\u0026gt;阿里云公共仓库\u0026lt;/name\u0026gt; \u0026lt;url\u0026gt;https://maven.aliyun.com/repository/public\u0026lt;/url\u0026gt; \u0026lt;/mirror\u0026gt; Sbt sbt 是一个类似于 maven 的构建工具，使用如下命令安装：\nsudo pacman -S sbt Sublime Text 安装 使用 yay 安装 Sublime Text，如下所示：\nyay sublime 激活码 一个可用的激活码（2022-04-30）:\n—– BEGIN LICENSE —–\nMifeng User\nSingle User License\nEA7E-1184812\nC0DAA9CD 6BE825B5 FF935692 1750523A\nEDF59D3F A3BD6C96 F8D33866 3F1CCCEA\n1C25BE4D 25B1C4CC 5110C20E 5246CC42\nD232C83B C99CCC42 0E32890C B6CBF018\nB1D4C178 2F9DDB16 ABAA74E5 95304BEF\n9D0CCFA9 8AF8F8E2 1E0A955E 4771A576\n50737C65 325B6C32 817DCB83 A7394DFA\n27B7E747 736A1198 B3865734 0B434AA5\n—— END LICENSE ——\n一些插件 点击 Tools — Install Package Control 安装包管理工具；\n点击 Preferences — Package Control 选择 Install Package；\n输入 ChineseLocalzations 安装中文语言包：\n输入 PrettyJSON 安装 JSON 格式化工具，使用 Ctrl + Shift + P 打开命令面板，输入并选择 Format JSON 即可格式化 JSON：\nNode.js 安装 使用 pacman 安装 Node.js ，如下所示：\nsudo pacman -S nodejs npm 使用 cnpm 替代 npm 加速下载：\nnpm install -g cnpm --registry=https://registry.npmmirror.com 解决 EACCES 问题 参考官方指导：Resolving EACCES permissions errors when installing packages globally\n安装 nvm\nsudo pacman -S nvm echo \u0026#39;source /usr/share/nvm/init-nvm.sh\u0026#39; \u0026gt;\u0026gt; ~/.zshrc 使用 nvm 安装 node\nnvm install node 重新使用 npm install\n使用 Hexo 参见官方文档：安装 Hexo\nnpm install -g hexo-cli npx hexo server 使用 Docker 安装 Docker 使用如下命令安装和启动 docker：\n# 安装 Docker sudo pacman -S docker # 启动 Docker sudo systemctl start docker 加入 docker 组 使用如下命令将当前用户加入 docker 组：\nsudo usermod -aG docker $USER reboot 使用镜像加速服务 参见：Docker 镜像加速器：\nsudo mkdir -p /etc/docker sudo tee /etc/docker/daemon.json \u0026lt;\u0026lt;-\u0026#39;EOF\u0026#39; { \u0026#34;registry-mirrors\u0026#34;: [\u0026#34;https://rgotb8v4.mirror.aliyuncs.com\u0026#34;] } EOF sudo systemctl daemon-reload sudo systemctl restart docker MongoDB Compass yay mongodb-compass Postman sudo pacman -S postman-bin Wireshark sudo pacman -S wireshark-qt sudo usermod -aG wireshark $USER 其他常用软件 vlc sudo pacman -S vlc Google Chrome yay -S google-chrome 截图工具 参见：Flameshot。\nsudo pacman -S flameshot VirtualBox 参见：VirtualBox。\nsudo pacman -S virtualbox linux-headers virtualbox-ext-oracle # 手动加载内核模块（或者重启） sudo modprobe vboxdrv vboxnetadp vboxnetflt VSCode sudo pacman -S vscodium 有道词典 yay youdao-dict uluancher 参见：Application launcher for Linux，在 Wayland 下需要手动添加快捷键，详见：Hotkey In Wayland。\nyay ulauncher WPS yay wps-office-cn yay wps-office-mui-zh-cn yay ttf-wps-fonts yay ttf-ms-fonts Aria2 sudo pacman -S aria2 连接 iPhone 从 iPhone 中复制照片到 Manjaro 如果想将 iPhone 中的照片传输到 Manjaro 中，可以通过 ifuse 将 iPhone 作为磁盘挂在到 Manjaro 上。\n首先需要安装 ifuse ，如图：\npacman -Ss ifuse 为 iPhone 创建挂载点：\nmkdir -p ~/Documents/iPhone 使用如下命令将 iPhone 挂在到 Manjaro，（执行如下命令时，iPhone 会提示你“是否信任此设备”，然后输入密码，所以第一次挂载时需要多次执行如下命令）：\nifuse ~/Documents/iPhone 从 Manjaro 中复制文件到 iPhone iPhone 的「文件」APP 中能连接服务器，因此可以在 Manjaro 中启动文件共享服务，iPhone 作为客户端连接即可。这里选择使用 samba，一种Linux、UNIX系统上可用于共享文件和打印机等资源的协议，这种协议是基于 Client \\ Server 型的协议，Client 端可以通过 SMB 访问到 Server（服务器）上的共享资源。\n首先需要安装 samba：\nsudo pacman -S samba manjaro-settings-samba 然后编辑 samba 的配置文件：\nsudo vim /etc/samba/smb.conf 只需要编辑 [homes] 这部分即可，如下所示：\n[homes] comment = Home Directories browseable = yes # no -\u0026gt; yes read only = yes create mask = 0700 directory mask = 0700 valid users = %S path = /run/media/xianglin # path to share 使用如下命令为 samba 添加用户：\nsudo smbpasswd -a xianglin 完成后启动 smb 服务，即可在 iPhone 上连接到共享目录中：\nsudo systemctl restart smb ","permalink":"https://2d992714.xianglin2020-github-io.pages.dev/posts/2022/manjaro-install-configuration/","summary":"把自己的台式机从 Hackintosh 转到 Manjaro，记录安装步骤。","title":"Manjaro 安装配置记录"},{"content":"Getting started 已经成功将项目从 Play 改为 SpringBoot 不再更新\n官方网站：\nplay\nsbt\n创建 Play2 项目 你可能需要一个梯子🪜\n使用 IDEA 创建 Play2 项目 参见：Getting started with Play 2.x。\n启动 Scala 插件：\n通过 IDEA 的模板创建项目：\n创建完成后 Play2 项目结构如下：\n使用 sbt 从命令行创建 参见：Creating a New Application。\n使用如下命令创建 Java Play 项目：\nsbt new playframework/play-java-seed.g8 创建完成后的结构如下：\n从 IDEA 中启动项目或者在控制台中输入sbt run 启动项目后访问 http:localhost:9000：\n项目结构介绍 参见：Anatomy of a Play application。\napp → Application sources └ assets → Compiled asset sources └ stylesheets → Typically LESS CSS sources └ javascripts → Typically CoffeeScript sources └ controllers → Application controllers └ models → Application business layer └ views → Templates build.sbt → Application build script conf → Configurations files and other non-compiled resources (on classpath) └ application.conf → Main configuration file └ routes → Routes definition └ logback.xml → logback definition dist → Arbitrary files to be included in your projects distribution public → Public assets └ stylesheets → CSS files └ javascripts → Javascript files └ images → Image files project → sbt configuration files └ build.properties → Marker for sbt project └ plugins.sbt → sbt plugins including the declaration for Play itself lib → Unmanaged libraries dependencies logs → Logs folder └ application.log → Default log file target → Generated stuff └ resolution-cache → Info about dependencies └ scala-2.13 └ api → Generated API docs └ classes → Compiled class files └ routes → Sources generated from routes └ twirl → Sources generated from templates └ universal → Application packaging └ web → Compiled web assets test → source folder for unit or functional tests app/ app 目录包含所有可执行文件：Java 和 Scala 的源代码，模板文件和 assets；\n默认创建三个包对应 MVC 的三层架构：\napp/controllers\napp/models\napp/views\napp 下可以随意创建包，包路径也是随意的，比如：app/store/xianglin/play2/controllers 或者 app/store/xiangln/play2/services。\npublic/ public 存储资源文件，三个子目录分别用于存储 images、CSS 和 JavaScript。\nconf/ conf 目录存放 Play2 的配置文件，主要有三个配置文件：\napplication.conf：保存应用大部分的配置项，参见：Configuration file syntax and features；\nroutes：保存 Play2 的路由规则，即如何将 uri 和 Action 对应；\nlogback.xml：保存 Logback 日志框架的配置。\nlib/ lib 是可选的，用于存放需要手动管理的依赖，只需要添加需要的 JAR 文件，Play2 会自动把它加入到应用的 classpath，参见：Unmanaged dependencies。\nbuild.sbt 类似于 Maven 项目中的 pom.xml ，定义项目构建的细节，如：项目名称、版本、依赖等。\nproject/ 保存如下两个文件，用于定义 sbt 的构建规则：\nplugins.sbt：定义项目使用的 sbt 插件；\nbuild.properties：一般用于定义项目使用的 sbt 版本。\ntarget/ 保存项目构建产生的文件。\n开发 Hello World 实例程序 参见： Implementing Hello World。\n使用 Play 创建 Hello World 请求很简单，主要有如下几步：\n创建 Hello World 页面； 创建一个 Action 方法； 定义请求映射规则； 测试请求。 创建 Hello World 页面 在 app/views 目录下创建 hello.scala.html ，添加如下内容：\n@(name: String) @main(\u0026#34;Hello\u0026#34;) { \u0026lt;section id=\u0026#34;top\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;wrapper\u0026#34;\u0026gt; \u0026lt;h1\u0026gt;Hello @name\u0026lt;/h1\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/section\u0026gt; } 创建 Action 方法 在 app/controllers/HomeController 中添加方法处理请求：\npublic Result hello(String name) { return ok(views.html.hello.render(name)); } 定义映射规则 在 conf/routes 中添加请求 /hello 和 Action 的映射规则：\nGET /hello controllers.HomeController.hello(name:String) 访问请求 在浏览器输入 http://localhost:9000/hello?name=MyName 访问 Hello 页面，如图：\nPlay 应用的处理流程 参见：Play Application Overview。\nPlay 处理 http://localhost:9000/ 请求的主要步骤如下：\n浏览器使用 GET 请求访问根路径 / ； Play 内部的 HTTP Server 收到请求； Play 使用 routes 文件解析请求，将请求映射到对应的 Action 方法； Action 方法渲染 index 页面； HTTP Server 返回 HTML 格式的响应内容。 如下图：\nPlay 示例项目 Play Tutorials 有很多 Play 的实例项目，供学习使用。\nMain concepts for Java 参见：Main concepts for Java。\n使用 Play Configuration 使用依赖注入的方式在项目中使用 Play 的配置对象 Config：\npackage controllers; import com.typesafe.config.Config; import play.mvc.Controller; import javax.inject.Inject; public class ConfigController extends Controller { private final Config config; @Inject public ConfigController(Config config) { this.config = config; } public Result config() { return ok(Json.toJson(config)); } } 处理 HTTP 请求 Play 基础概念：Action、Controller 和 Result Action Play 接收的大部分方法都交由 Action 处理，Action 用于处理请求并返回结果，如下所示：\npublic play.mvc.Result index(play.mvc.Http.Request request) { return play.mvc.Results.ok(\u0026#34;Got request \u0026#34; + request + \u0026#34;!\u0026#34;); } Controller Play 中使用的 Controller 继承自 play.mvc.Controllr ，用于定义一组 Action ，如下所示：\npackage controllers; import play.*; import play.mvc.*; public class Application extends Controller { public Result index() { return ok(\u0026#34;It works!\u0026#34;); } public Result hello() { return ok(\u0026#34;Hello world!\u0026#34;); } } Result HTTP Response 包括：响应行、响应头和返回数据，Play 中 play.mvc.Result 定义了 HTTP 响应结构，play.mvc.Results 定义了许多静态方法，方便返回各种 Result，如下所示：\n// 200 OK Result ok = ok(\u0026#34;Hello world!\u0026#34;); // 404 Not Found Result notFound = notFound(); Result pageNotFound = notFound(\u0026#34;\u0026lt;h1\u0026gt;Page not found\u0026lt;/h1\u0026gt;\u0026#34;).as(\u0026#34;text/html\u0026#34;); // 400 Bad Request Result badRequest = badRequest(views.html.form.render(formWithErrors)); // 500 Internal Server Error Result oops = internalServerError(\u0026#34;Oops\u0026#34;); // play.mvc.Http.Status Result anyStatus = status(488, \u0026#34;Strange response type\u0026#34;); // 3XX Redirection Result redirect = redirect(\u0026#34;/user/home\u0026#34;); Result temporaryRedirect = temporaryRedirect(\u0026#34;/user/home\u0026#34;); HTTP 映射 router 的作用是将每个请求映射到一个 Action 方法的调用，HTTP 请求包括两个部分：\n请求路径（例如：/clients/1234、/photos/list），包括查询字符串； HTTP 请求方法（GET、POST 等）。 Play2 的映射定义在 conf/routes 文件中，Play2 会编译它，因此可以直接在浏览器中看到错误。映射定义为如下格式：\nProtocol URLPATH ControllerMapping 最简单的映射如下：\nGET / controllers.HomeController.index() GET /hello controllers.HomeController.hello(name:String) 存在动态参数的映射如下：\n# match /bookshop/book/123/ GET /bookshop/book/:id/ controllers.Application.getBook(id:String) # match # /bookshop/book/images/book1.jpg # /bookshop/book/images/books/book2.jpg # /bookshop/book/images/books/thumbnails/small/image1.jpg GET /bookshop/book/images/* controllers.Application.fetchImage(name:String) # $variablename\u0026lt;regular expression\u0026gt; # match /bookshop/book/10/page/12/ # not match /bookshop/book/10/page/a/ GET /bookshop/book/:id/page/$page\u0026lt;[0-9]+\u0026gt;/ controllers.Application.fetchBookpage(bookid:String, page:Integer) 存在固定参数的映射如下：\nGET /bookshop//authors/ controllers.Application.authors(limit: Integer = 10) 存在可选参数的映射如下：\n# variable? = default-value GET /bookshop//showcomment/ controllers.Application.showComment(userid ?= null) 参见官方文档中有更详细的介绍。\n处理响应 更改默认的 Content-Type Play2 中 HTTP 响应的 ContentType 会根据 Body 中内容的 Java 类型自动推导，例如：\n// text/plain Result textResult = ok(\u0026#34;Hello World!\u0026#34;); // application/json Result jsonResult = ok(Json.toJson(object)); 可以通过如下方法修改 Content-Type ：\npublic play.mvc.Result as(String contentType) { } 例如：\nResult htmlResult = ok(\u0026#34;\u0026lt;h1\u0026gt;Hello World!\u0026lt;/h1\u0026gt;\u0026#34;).as(\u0026#34;text/html\u0026#34;); Result htmlResult = ok(\u0026#34;\u0026lt;h1\u0026gt;Hello World!\u0026lt;/h1\u0026gt;\u0026#34;).as(play.mvc.Http.MimeTypes.HTML); 更改 HTTP 响应头 通过如下方法修改响应头：\npublic play.mvc.Result withHeader(String name, String value) { } // The headers are processed in pairs, so nameValues(0) is the first header\u0026#39;s name, and nameValues(1) is the first header\u0026#39;s value, nameValues(2) is second header\u0026#39;s name, and so on. public play.mvc.Result withHeaders(String... nameValues) { } 例如：\npublic Result index() { return ok(\u0026#34;\u0026lt;h1\u0026gt;Hello World!\u0026lt;/h1\u0026gt;\u0026#34;) .as(MimeTypes.HTML) .withHeader(CACHE_CONTROL, \u0026#34;max-age=3600\u0026#34;) .withHeader(ETAG, \u0026#34;some-etag-calculated-value\u0026#34;); } 使用 Cookies 通过如下方法设置 Cookies 或删除 Cookies ：\n// set cookies public play.mvc.Result withCookies(play.mvc.Http.Cookie... newCookies) { } // discarding cookies public play.mvc.Result discardingCookie(String name) { } 例如：\n// add a Cookie public Result index() { return ok(\u0026#34;\u0026lt;h1\u0026gt;Hello World!\u0026lt;/h1\u0026gt;\u0026#34;) .as(MimeTypes.HTML) .withCookies( Cookie.builder(\u0026#34;theme\u0026#34;, \u0026#34;blue\u0026#34;) .withMaxAge(Duration.ofSeconds(3600)) .withPath(\u0026#34;/some/path\u0026#34;) .withDomain(\u0026#34;.example.com\u0026#34;) .withSecure(false) .withHttpOnly(true) .withSameSite(Cookie.SameSite.STRICT) .build()); } // discard a Cookie public Result index() { return ok(\u0026#34;\u0026lt;h1\u0026gt;Hello World!\u0026lt;/h1\u0026gt;\u0026#34;).as(MimeTypes.HTML).discardingCookie(\u0026#34;theme\u0026#34;); } Session And Flash 保存在 Session 中的数据在整个会话中有效；保存在 Flash 中的数据仅会在下次请求中生效。Play 中 Session 和 Flash 都是通过 Cookies 实现的，所以有几条注意事项：\n数据的大小不能超过 4K； 只能存储字符串内容； Cookies 中的内容在浏览器中可见，可能会导致敏感信息泄漏。 Session 配置 play.http.session.cookieName：Cookie 名称，默认为：PLAY_SESSION；\nplay.http.session.maxAge：Session 的超时时间，默认没有超时时间，仅在浏览器关闭时无效。\n操作 Session 使用如下方式设置、读取和删除 Session：\npublic class SessionAndFlashController extends Controller { public Result addSession(Http.Request request) { // 添加 Session return redirect(\u0026#34;/session/read\u0026#34;).addingToSession(request, \u0026#34;connected\u0026#34;, \u0026#34;user@gmail.com\u0026#34;); } public Result readSession(Http.Request request) { // 读取 Session return request .session() .get(\u0026#34;connected\u0026#34;) .map(user -\u0026gt; ok(\u0026#34;Hello \u0026#34; + user)) .orElseGet(() -\u0026gt; unauthorized(\u0026#34;Oops, you are not connected\u0026#34;)); } public Result clearSession() { // 清除 Session return redirect(\u0026#34;/session/read\u0026#34;).withNewSession(); } } 操作 Flash Flash 类似 Session，但仅在下一次请求有效。使用如下方式设置、读取 Flash：\npublic Result addFlash() { return redirect(\u0026#34;/flash/read\u0026#34;).flashing(\u0026#34;success\u0026#34;, \u0026#34;The item has been created\u0026#34;); } public Result readFlash(Http.Request request) { return ok(request.flash().get(\u0026#34;success\u0026#34;).orElse(\u0026#34;Welcome!\u0026#34;)); } 请求体解析器 什么是请求体解析器 HTTP 请求的格式是：请求行、请求头和请求体。通常来说请求头很小，能够安全的缓存在内存中，Play 使用 play.mvc.Http.RequestHeader 表示请求头。请求体可能很大，因此不能直接缓存在内存中，一般称之为流。事实上很多请求体比较小而且能映射为某种模型缓存在内存中，Play 使用 play.mvc.BodyParser 来完成请求体从流到模型的映射。Play 是一个异步框架，因此不能使用传统的 java.io.InputStream 来读取请求体，Play 使用 Akka Stream 完成这一操作。\n使用自带的请求体解析器 没有明确指定时 Play 根据 Content-Type 来选择合适的请求体解析器，如下面表格所示：\nContent-Type Type Method text/plain String asText() application/json com.fasterxml.jackson.databind.JsonNode asJson() application/xml、text/xml、application/XXX+xml org.w3c.Document asXml() application/x-www-form-urlencoded Map\u0026lt;String, String[]\u0026gt; asFormUrlEncoded() multipart/form-data play.mvc.Http.MultipartFormData、play.mvc.Http.MultipartFormData.FilePart asMultipartFormData() other play.mvc.Http.RawBuffer asRaw() 使用方法如下：\npublic class BodyParserController extends Controller { public Result json(Http.Request request) { JsonNode jsonNode = request.body().asJson(); return ok(\u0026#34;Got name: \u0026#34; + jsonNode.get(\u0026#34;name\u0026#34;).asText()); } } 明确指定请求体解析器 使用 @play.mvc.BodyParser.Of 注解明确指定当前请求的请求体解析器，Play 自带的解析器作为 play.mvc.BodyParser 的内部类提供，如下所示：\n@play.mvc.BodyParser.Of(BodyParser.Json.class) public Result json(Http.Request request) { JsonNode jsonNode = request.body().asJson(); return ok(\u0026#34;Got name: \u0026#34; + jsonNode.get(\u0026#34;name\u0026#34;).asText()); } 请求体长度限制 play.http.parser.maxMemoryBuffer：内存缓存限制，默认100KB；\nplay.http.parser.maxDiskBuffer：磁盘缓存限制，默认10MB。\n组合 Action 使用组合 Action 需要两步：\n1、实现一个 Action 用于完成通用功能，比如打印日志等：\npublic class VerboseAction extends Action.Simple { private final Logger logger = LoggerFactory.getLogger(VerboseAction.class); @Override public CompletionStage\u0026lt;Result\u0026gt; call(Http.Request req) { logger.info(\u0026#34;Calling action for {}\u0026#34;, req); return delegate.call(req); } } 2、在需要打印日志的 Action 方法上增加 @With() 注解：\npublic class VerboseController extends Controller { @With(VerboseAction.class) public Result verboseIndex() { return ok(\u0026#34;It works!\u0026#34;); } } 测试 verboseIndex 请求，控制台中打印出对应的日志：\n2022-05-15 15:58:55 INFO controllers.actions.VerboseAction Calling action for GET /composition/index @With() 注解能应用在 Controller 中，此时其中所有的 Action 都会生效。\n自定义注解 上面 @With(VerboseAction.class) 也可以用一个自定义的注解实现，比如 @VerboseAnnotation ，这个注解自身需要添加 @With 注解，定义自定义注解需要三步：\n1、定义一个注解，表示需要的功能：\n@Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @With(VerboseAnnotationAction.class) public @interface VerboseAnnotation { boolean value() default true; } 2、在 Action 中实现通用功能，比如打印日志：\nVerboseAnnotationAction 通过 configuration 获取 @VerboseAnnotation 中定义的值：\npublic class VerboseAnnotationAction extends Action\u0026lt;VerboseAnnotation\u0026gt; { private final static Logger logger = LoggerFactory.getLogger(VerboseAction.class); @Override public CompletionStage\u0026lt;Result\u0026gt; call(Http.Request req) { if (configuration.value()) { logger.info(\u0026#34;Calling action for {}\u0026#34;, req); } return delegate.call(req); } } 3、在 Action 方法上增加自定义的注解：\n@VerboseAnnotation public Result verboseHome() { return ok(\u0026#34;It works too!\u0026#34;); } 事实上 @play.mvc.Security.Authenticated 和 @play.cache.Cached 也是通过此方式实现的。\n打印组合 Action 的作用顺序 在 logback.xml 配置文件中添加如下配置，就能在日志中看到完整的 Action 组合链：\n\u0026lt;logger name=\u0026#34;play.mvc.Action\u0026#34; level=\u0026#34;DEBUG\u0026#34; /\u0026gt; 2022-05-15 16:29:45 DEBUG play.mvc.Action ### Start of action order 2022-05-15 16:29:45 DEBUG play.mvc.Action 1. controllers.actions.VerboseAnnotationAction defined on public play.mvc.Result controllers.VerboseController.verboseHome() 2022-05-15 16:29:45 DEBUG play.mvc.Action 2. play.http.DefaultActionCreator$1() 2022-05-15 16:29:45 DEBUG play.mvc.Action 3. play.core.j.JavaAction$$anon$1() 2022-05-15 16:29:45 DEBUG play.mvc.Action ### End of action order 拦截 HTTP 请求 Play 提供了两种方式拦截 Action 的调用，第一种是 ActionCreator ，通过 createAction 方法返回一个 Action，这个 Action 会作为 Action 组合链\n中第一个 Action 或者最后一个 Action，取决于配置：play.http.actionComposition.executeActionCreatorActionFirst。\n第二种方式是实现自定义的 HttpRequestHandler。\nAction creators 默认需要在根包下定义一个名为 ActionCreator 且实现 play.http.ActionCreator 的类，如下所示：\npublic class ActionCreator implements play.http.ActionCreator { @Override public Action createAction(Http.Request request, Method actionMethod) { return new Action.Simple() { @Override public CompletionStage\u0026lt;Result\u0026gt; call(Http.Request req) { return delegate.call(req); } }; } } 或者使用如下配置：\nplay.http.actionCreator = \u0026#34;com.example.MyActionCreator\u0026#34; 异步 HTTP 编程 处理异步结果 创建和使用 CompletionStage\u0026lt;Result\u0026gt; Java8 提供了 CompletionStage 表示 promise，CompletionStage\u0026lt;Result\u0026gt; 最终会得到一个 Result。通过 CompletableFuture.supplyAsync() 方法可以得到一个 CompletionStage：\n// creates new task CompletionStage\u0026lt;Integer\u0026gt; promiseOfInt = CompletableFuture.supplyAsync(() -\u0026gt; intensiveComputation()); supplyAsync 会创建一个新的任务提交到 fork/join 框架中执行。\n使用 HttpExecutionContext 在 Action 中使用 Java CompletionStage 时，必须显式提供 HTTP 执行上下文作为执行期，以确保类加载器保持在作用域内，代码如下所示：\npublic class AsyncController extends Controller { private final HttpExecutionContext httpExecutionContext; @Inject public AsyncController(HttpExecutionContext httpExecutionContext) { this.httpExecutionContext = httpExecutionContext; } public CompletionStage\u0026lt;Result\u0026gt; index() { CompletionStage\u0026lt;Integer\u0026gt; promiseOfInt = CompletableFuture.supplyAsync(this::intensiveComputation, httpExecutionContext.current()); return promiseOfInt.thenApply(integer -\u0026gt; ok(String.valueOf(integer))); } private Integer intensiveComputation() { int value = Integer.MIN_VALUE; for (int i = 0; i \u0026lt; Integer.MAX_VALUE; i++) { value++; } return value; } } 以确保类加载器保持在作用域内。 使用 CustomExecutionContext and HttpExecution 使用 CompletionStage 或者 HttpExecutionContext 时仍然使用的是 Play 默认的执行上下文，如果需要执行一些阻塞 API 比如 JDBC，最好的方式是实现自己的 CustomExecutionContext，将阻塞任务从 Play 的线程池中移出去，通过继承 play.libs.concurrent.CustomExecutionContext 实现自定义的 ExecutionContex：\npublic class MyCustomExecutionContext extends CustomExecutionContext { @Inject public MyCustomExecutionContext(ActorSystem actorSystem) { super(actorSystem, \u0026#34;blocking-io-dispatcher\u0026#34;); } } blocking-io-dispatcher 在 application.conf 中定义：\nblocking-io-dispatcher { type = Dispatcher executor = \u0026#34;thread-pool-executor\u0026#34; thread-pool-executor { fixed-pool-size = 32 } throughput = 1 } 使用方法如下：\npublic class AsyncWithCustomExecutionContentController extends Controller { private final MyCustomExecutionContext myCustomExecutionContext; @Inject public AsyncWithCustomExecutionContentController(MyCustomExecutionContext myCustomExecutionContext) { this.myCustomExecutionContext = myCustomExecutionContext; } public CompletionStage\u0026lt;Result\u0026gt; index() { var executor = HttpExecution.fromThread(myCustomExecutionContext); return CompletableFuture.supplyAsync(() -\u0026gt; ok(\u0026#34;It\u0026#39;s work!\u0026#34;), executor); } } 使用 HttpExecution.fromThread 方法从 MyCustomExecutionContext 中获取已存在的线程池。\n处理超时 请求经常需要限定等待时间，避免出错时浏览器无限等待。通过 play.api.libs.concurrent.Futures.timeout 包装 CompletionStage 实现：\npublic class TimeoutController extends Controller { private final Futures futures; private final MyCustomExecutionContext myCustomExecutionContext; @Inject public TimeoutController(Futures futures, MyCustomExecutionContext myCustomExecutionContext) { this.futures = futures; this.myCustomExecutionContext = myCustomExecutionContext; } public CompletionStage\u0026lt;Result\u0026gt; index() { return futures.timeout(delayedResult(), Duration.ofSeconds(1)); } public CompletionStage\u0026lt;Result\u0026gt; delayedResult() { ExecutionContextExecutor executor = HttpExecution.fromThread(myCustomExecutionContext); long start = System.currentTimeMillis(); return futures.delayed(() -\u0026gt; CompletableFuture.supplyAsync(() -\u0026gt; { long end = System.currentTimeMillis(); long seconds = end - start; return \u0026#34;rendered after \u0026#34; + seconds + \u0026#34; seconds\u0026#34;; }, executor), Duration.of(3, SECONDS)).thenApply(Results::ok); } } 需要注意的是，超时 timeout 和取消 cancel 不同，超时后 delayedResult 仍会执行，尽管结果不会被返回。\n流式 HTTP 响应 Content-Length 响应头 HTTP1.1 支持长连接，一个 TCP 连接可以服务多次 HTTP 请求，此时服务端必须使用 Content-Length 响应头。默认情况下，使用 Play 不需要明确指定 HTTP 响应头，因为返回的内容是明确的，Play 可以自行计算并添加对应的响应头。HTTP 响应由响应头和响应体组成，使用 play.http.HttpEntity 表示响应体，Play 中一个 HTTP 响应的表示如下：\npublic Result httpResponse() { return new Result( new ResponseHeader(Http.Status.OK, Collections.emptyMap()), new HttpEntity.Strict(ByteString.fromString(\u0026#34;It\u0026#39;s work!\u0026#34;), Optional.of(Http.MimeTypes.TEXT)) ); } 发送大量数据 使用 Play 返回一个文件给客户端，构建一个 Source[ByteString, _] 作为响应内容，然后使用 play.http.HttpEntity 返回，如下所示：\npublic Result index() { var path = Paths.get(\u0026#34;/home/xianglin/Downloads/20220504100611.json\u0026#34;); var source = FileIO.fromPath(path); var contentLength = Optional.\u0026lt;Long\u0026gt;empty(); return new Result( new ResponseHeader(Http.Status.OK, Collections.emptyMap()), new HttpEntity.Streamed(source, contentLength, Optional.of(Http.MimeTypes.TEXT)) ); } 上面的响应没有明确指定 contentLength，Play 就会将文件加载到内存然后计算长度。对于大文件，需要明确指定 contentLength 避免这种情况发送，如下：\npublic Result index() throws IOException { var path = Paths.get(\u0026#34;/home/xianglin/Downloads/20220504100611.json\u0026#34;); var source = FileIO.fromPath(path); // compute the response size var contentLength = Optional.of(Files.size(path)); return new Result( new ResponseHeader(Http.Status.OK, Collections.emptyMap()), new HttpEntity.Streamed(source, contentLength, Optional.of(Http.MimeTypes.TEXT)) ); } 处理文件 Play 可以使用 play.mvc.Results#ok(java.io.File) 方法直接返回文件，此方法会添加 Content-Type 和 Content-Disposition 响应头。默认情况下是 Content-Disposition: inline; filename=，如果不想浏览器下载文件而是直接打开文件，可以使用 play.mvc.Results#ok(java.io.File, boolean)：\npublic Result file() { return ok(new File(\u0026#34;/home/xianglin/Downloads/20220504100611.json\u0026#34;), Optional.of(\u0026#34;示例文件.json\u0026#34;)).as(Http.MimeTypes.BINARY); } public Result fileToDisplay() { return ok(new File(\u0026#34;/home/xianglin/Downloads/20220504100611.json\u0026#34;), false); } 分片响应 分块传输编码允许 HTTP 服务器发送给客户端的数据分成多个部分。Play 中可以使用 play.mvc.Results#ok(java.io.InputStream) 和 ok().chunked() 方法返回分块数据：\npublic Result fileStream() throws FileNotFoundException { return ok(new FileInputStream(\u0026#34;/home/xianglin/Downloads/20220504100611.json\u0026#34;)); } public Result trunked() { Source\u0026lt;ByteString, ?\u0026gt; trunked = Source.\u0026lt;ByteString\u0026gt;actorRef(256, OverflowStrategy.dropNew()) .mapMaterializedValue( sourceActor -\u0026gt; { sourceActor.tell(ByteString.fromString(\u0026#34;\u0026#34;), null); sourceActor.tell(ByteString.fromString(\u0026#34;\u0026#34;), null); sourceActor.tell(ByteString.fromString(\u0026#34;\u0026#34;), null); sourceActor.tell(ByteString.fromString(\u0026#34;\u0026#34;), null); return NotUsed.getInstance(); } ); return ok().chunked(trunked); } Play modules Play with Mongo PlayMorphia 是 Morphia 针对 Play 的拓展，方便使用 Morphia 进行 Play MongoDB 开发。\n安装方法 添加 Morphia 的依赖：\nlibraryDependencies ++= Seq( guice, \u0026#34;dev.morphia.morphia\u0026#34; % \u0026#34;core\u0026#34; % \u0026#34;1.6.1\u0026#34; ) 从 PlayMorphia 项目中下载 play_morphia_jar 放在项目的 lib 目录下；\n在 conf/application.conf 中配置 Mongo 的连接信息，最终效果如下图所示（如果使用 IDEA,可以右键点击 lib 选择“添加为库”）：\n使用方法 简单按照 controllers、models 和 repositories 的结构创建三个包，以操作 User 为例。\nUser Model：\npackage models; import dev.morphia.annotations.Entity; import dev.morphia.annotations.Id; import org.bson.types.ObjectId; /** * @author xianglin */ @Entity(\u0026#34;DB.users\u0026#34;) public class User { @Id private ObjectId id; private String firstname; private String lastname; private String email; // getter and setter } UserRepository：\npackage repositories; import dev.morphia.Key; import it.unifi.cerm.playmorphia.PlayMorphia; import models.User; import org.bson.types.ObjectId; import javax.inject.Inject; /** * @author xianglin */ public class UserRepository { private final PlayMorphia playMorphia; @Inject public UserRepository(PlayMorphia playMorphia) { this.playMorphia = playMorphia; } public User findById(String id) { return playMorphia .datastore() .createQuery(User.class) .field(\u0026#34;id\u0026#34;) .equal(new ObjectId(id)) .first(); } public Key\u0026lt;User\u0026gt; save(User user) { return playMorphia .datastore() .save(user); } } MongoController：\npackage controllers; import models.User; import play.libs.Json; import play.mvc.Controller; import play.mvc.Result; import repositories.UserRepository; import javax.inject.Inject; /** * 测试 MongoDB 操作 * * @author xianglin */ public class MongoController extends Controller { private final UserRepository userRepository; @Inject public MongoController(UserRepository userRepository) { this.userRepository = userRepository; } public Result getUser(String id) { return ok(Json.toJson(userRepository.findById(id))); } public Result saveUser() { User user = new User(); user.setFirstname(\u0026#34;Xiang\u0026#34;); user.setLastname(\u0026#34;Lin\u0026#34;); user.setEmail(\u0026#34;mail@mail.com\u0026#34;); return ok(Json.toJson(userRepository.save(user))); } } routes：\nGET /mongo/user/get controllers.MongoController.getUser(id: String) POST /mongo/user/save controllers.MongoController.saveUser() 最后使用 postman 测试接口，结果如下：\n使用新版 Morphia Morphia 2.0 以上版本需要 Java11，已经更高版本的 mongo-drive，请使用 play-morphia 中修改的 play-morphia.jar。\n","permalink":"https://2d992714.xianglin2020-github-io.pages.dev/posts/2022/play2-use-java/","summary":"Play is a high-productivity Java and Scala web application framework that integrates the components and APIs you need for modern web application development.","title":"Play2 in Java"},{"content":"使用 Docker 配置自动化构建 安装 GitLab 官方文档中文：https://docs.gitlab.cn/jh/install/docker.html\n官方文档英文：https://docs.gitlab.com/ee/install/docker.html\nGitLab 镜像地址：https://hub.docker.com/r/gitlab/gitlab-ce\n创建目录存储持久数据 mkdir -p ~/.docker/data/gitlab 安装运行镜像 docker run --detach \\ --publish 443:443 --publish 80:80 --publish 22:22 \\ --name gitlab \\ --restart always \\ --volume ~/.docker/data/gitlab/config:/etc/gitlab \\ --volume ~/.docker/data/gitlab/logs:/var/log/gitlab \\ --volume ~/.docker/data/gitlab/data:/var/opt/gitlab \\ --shm-size 256m \\ gitlab/gitlab-ee:latest 安装需要一段时间，可以使用如下命令查看安装日志：\ndocker logs -f gitlab 当出现如下内容时安装完成，从 /etc/gitlab/initial_root_password 查找 root 账户的密码。\ndocker exec -it gitlab grep \u0026#39;Password:\u0026#39; /etc/gitlab/initial_root_password 安装 Jenkins 创建目录存储持久数据 mkdir -p ~/.docker/data/jenkins 安装运行镜像 docker run -d -p 8080:8080 -p 50000:50000 \\ -v ~/.docker/data/jenkins:/var/jenkins_home \\ --restart always \\ jenkins/jenkins:lts-jdk11 安装完成后，访问 http://localhost:8080 访问 Jenkins 首页。\n初始化 Jenkins 解锁 Jenkins，使用如下命令从日志中查找密码：\ndocker logs containerId 安装推荐插件\n创建用户\n完成安装\n准备项目 准备两个项目用于配置自动构建：一个 SpringBoot 项目，直接使用内置 tomcat 容器启动；另一个 Vue 项目，使用 nginx 提供服务。\n创建 SpringBoot 项目 使用 IDEA 的 Spring Initializr 创建 Spring Boot 项目：\n根据项目需要选择所需的依赖项：\n添加一个用于测试的请求 hello，打印当前日期：\npackage store.xianglin.cicd.background.controller; import org.springframework.web.bind.annotation.CrossOrigin; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; /** * @author xianglin */ @RestController @CrossOrigin(originPatterns = \u0026#34;*\u0026#34;) public class HelloController { @GetMapping(\u0026#34;/hello\u0026#34;) public String hello(@RequestParam(value = \u0026#34;name\u0026#34;) String name) { String now = LocalDateTime.now().format(DateTimeFormatter.ofPattern(\u0026#34;yyyy-MM-dd HH:mm:ss\u0026#34;)); return \u0026#34;hello \u0026#34; + name + \u0026#34;， now is \u0026#34; + now; } } 在浏览器中测试结果如下：\n创建 Vue 项目 使用 IDEA 创建 Vue 项目可能需要先安装 Vue.js 插件：\n使用 IDEA 创建 Vue 项目：\n在 Vue 中使用 axios 向 SpringBoot 发送请求并展示。首先添加 axios 的依赖：\nnpm install axios 然后修改 src/components/HelloWorld.vue：\n\u0026lt;template\u0026gt; \u0026lt;div class=\u0026#34;hello\u0026#34;\u0026gt; \u0026lt;h1\u0026gt;{{ msg }}\u0026lt;/h1\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/template\u0026gt; \u0026lt;script\u0026gt; export default { name: \u0026#39;HelloWorld\u0026#39;, data() { return { msg: \u0026#39;Vue\u0026#39; } }, mounted() { setInterval(() =\u0026gt; { const axios = require(\u0026#39;axios\u0026#39;); const a = this axios.get(\u0026#39;http://localhost:9509/hello?name=Vue\u0026#39;) .then(function (response) { a.msg = response.data }).catch(function (error) { console.log(error); }) }, 1000) } } \u0026lt;/script\u0026gt; 从 IDEA 中启动项目，在浏览器中测试结果：\n将项目提交到 GitLab 上 首先需要在 GitLab 上创建项目对应的仓库，选择 “create a project” —— “创建空白项目”：\n填写项目基本信息后，点击“新建项目”：\n使用如下命令将本地仓库推送到 GitLab 上（以 Spring Boot 项目为例）：\n# 设置帐号信息 git config --global user.email \u0026#34;you@example.com\u0026#34; git config --global user.name \u0026#34;Your Name\u0026#34; # 提交修改 git add . git commit -m \u0026#39;init\u0026#39; # 推送远程仓库 git remote add origin http://localhost/xianglin/background.git git push -u origin --all 刷新 GitLab 仓库，即可看到提交信息：\n在 IDEA 中也可以通过以下步骤提交并推送（以 Vue 项目为例）：\n配置 Jenkins 自动构建及部署 全局工具配置 依次点击 “系统管理” —— “插件管理”、 “全局工具配置”：\n在插件管理中，选择“可选插件”，选择对应插件勾选后点击 “Install without restart” 安装插件：\n安装 NodeJS：\n安装 JDK，这里选择使用 Jenkins 已有的 JDK 11：\n安装 Maven：\n修改 Maven 配置，使用阿里云的 Maven 镜像（根据创建 jenkins 使用的 -v ~/.docker/data/jenkins:/var/jenkins_home 可以将 settings.xml 文件复制到对应目录）：\n配置 Spring Boot 自动构建 选择“新建任务”：\n输入任务名称，选择”构建一个自由风格的软件项目“，点击“确定”：\n在“源码管理”中选择“Git”，输入GitLab仓库地址，并添加访问的用户名和密码，其中 IP 地址可以通过命令 docker inspect --format '{{ .NetworkSettings.IPAddress }}' \u0026lt;container-ID\u0026gt; 获取：\n在“构建”中选择“调用顶层 Maven 目标”，在“构建后操作”中选择“归档成品”：\n保存后点击“立即构建”，查看构建结果：\n配置 Vue 自动构建 “源码管理”和上述一样，在“构建触发器”中选择“Provide Node \u0026amp; npm bin/folder to PATH”：\n在“构建”中选择“执行shell”，在“构建后操作”中选择“归档成品”：\ncnpm install cnpm run build # 打包，方便下载和上传到服务器部署 tar -cf dist.tar dist 保存后点击“立即构建”，查看构建结果：\n自动部署到服务器 自动构建完成后，可以手动将构建结果下载到本地执行，也可以通过 Jenkins 自动部署到指定服务器上，借助“Publish over SSH”插件完成。安装插件后在“系统管理”——“系统配置”中配置“Publish over SSH”，如图：\n配置完成后可以通过“Test Configuration”验证配置是否正确可用：\n部署 Spring Boot 应用 直接通过 jar 命令启动 Spring Boot 应用，计划将 jar 文件放在 /root/deploy/background，编写启动脚本 deploy.sh：\n#!/bin/bash # deploy spring boot app # stop server if [ -f \u0026#34;/var/run/springboot.pid\u0026#34; ]; then kill -15 $(cat /var/run/springboot.pid) fi # start server nohup /root/jdk-11.0.15+10/bin/java -jar /root/deploy/background/backgroud-0.0.1-SNAPSHOT.jar \u0026gt; /dev/null 2\u0026gt;\u0026amp;1 \u0026amp; 在”构建后操作“——”Send build artifacts over SSH“中配置如图所示：\n最总效果如下：\n部署 Vue 应用 Vue 上传到服务器到配置与上述相同，静态文件使用 Nginx 作为服务器，将 Jenkins 构建到 dist.tar 解压到对应目录即可，命令如下：\n#!/bin/bash # deploy vue app # extract file tar -xf /root/deploy/frontend/dist.tar -C /usr/share/nginx/html 修改 Nginx 到配置 /etc/nginx/conf.d/default.conf ，如下：\nlocation / { root /usr/share/nginx/html/dist; index index.html index.htm; } 启动 Nginx 即可。\n","permalink":"https://2d992714.xianglin2020-github-io.pages.dev/posts/2022/gitlab-jenkins-cicd-in-docker/","summary":"使用 Docker 安装配置 GitLab，搭建代码托管平台，安装配置 Jenkins，实现自动化构建。","title":"使用 Docker 安装GitLab、Jenkins 配置自动化构建"},{"content":"易诚互动 一面 项目相关 目前项目某个功能的数据流向、代码如何做到可扩展； MyBatis 的工作原理； 如何实现文件上传、下载显示实时进度。 二面 多线程 线程池的使用和基础参数； 什么是线程上下文。 Redis Redis 的特点、为什么快； Redis 的使用场景； Redis 的缓存雪崩和应对方法； Redis 的持久化方式。 MySQL 如何优化 SQL 语句； 索引失效的情况； 如何降低 MySQL 的压力。 MQ 如何处理消息的重复发送和消息丢失。 JVM JVM 调优经验； 对象如何进入老年代。 分布式 项目中用了何种分布式框架、什么场景； 对 Dubbo 的掌握和理解。 结果 13K * 13\n易方达基金（外包） 笔试 选择题 涉及计算机网络、数据结构、Java 基础、项目管理等。\n简述题 在浏览器中键入域名到显示页面的过程； 简述 ClassLoader 的作用和工作原理； Java 线程中有哪些同步和交互的机制； 忘了。 算法题 （在线编辑器，允许切屏使用本地 IDE）\n给定一个未经排序的整数数组，请使用 Java 语言编码实现找到最长且连续的的递增序列的长度。输入: [1,3,5,4,7] 输出: 3；输入: [2,2,2,2,2],输出: 1； 字符串压缩，形如 aaaa -\u0026gt; a4, AAbbbdf -\u0026gt; A2b3df； 系统设计题 如何设计实现一个支持弹性伸缩和 7*24H 服务的系统。 编码题 （纯文本编辑器，没有语法提示）\n使用 ForkJoin 框架实现计算 1~10000 的和。 一面 项目相关 介绍下目前正在做的项目； 平时项目开发的具体流程； 怎么排查生产问题； 为新项目搭建环境，如何做技术选型？你会选择 Redis 还是 MongoDB？ Java 相关 Java 中抽象类和接口的比较； Java 字符串和字符数组、字节数组的转换 toCharArray、getBytes。 字符集 有哪些字符集； ISO-8859-1 的了解。 设计模式 有哪些常用的设计模式，在哪里使用或看见过； 介绍观察者模式。 数据库 对 MySQL 的数据同步的了解（binlog、canal）。 MQ 消息队列中消费者是否需要对消息进行确认； 对未确认的消息，队列怎么处理？消费者还能继续查看消费到它吗？ 未对消息确认前业务节点异常宕机，该消息怎么处理？ Spring、MyBatis 谈谈自己对 Spring 的了解和理解； Spring 中怎么对 MyBatis 做事务管理； Spring 中定义的事务传播行为，默认传播行为是什么？ JVM 栈内存会溢出吗？ 怎么排查 OOM 问题。 分布式相关 有哪些 RPC 协议（RPC是上层协议，底层可以基于 TCP 协议，也可以基于 HTTP 协议） 多个系统如何查看日志（分布式监控）； 阿里云的鹰眼如何实现的？日志中 [-JYLSH-null-] 是如何实现的？ 你如何实现鹰眼的链路追踪功能？ 如何处理分布式事务。 二面 闲聊 目前项目情况； 项目中遇到的困难的问题，如何排查解决的； 最近在学习什么东西； 你觉得自己的技术水平怎么样？ 你期望的工资是多少？ 当时说我不到三年的工作经验，价格可能要高了，所以安排我多做了一轮笔试题和面试题。\n笔试题（加餐） 选择题 涉及计算机网络、数据结构、操作系统、Java 基础、项目管理等。\n简述题 Java 的并发包（JUC）中，除了提供高性能的线程安全集合类，还提供了很多并发场景需要的原子类，请列举你熟悉的并发包类和使用场景； Java 线程中有哪些同步和交互的机制； 在分布式架构下，如何在不影响客户请求的情况下做到弹性扩展和 7*24 小时服务可用，同时如何识别同一个客户在不同服务器的请求，请简述你的方案； 列出 Java 发生内存溢出的几种不同场景和发生原因。 编码题 （纯文本编辑器，没有语法提示）\n在一个大小为 N 的整形数组中，寻找第 K 大的数。要求不能对数组全部排序； 字符串压缩，形如 aaaa -\u0026gt; a4, AAbbbdf -\u0026gt; A2b3df； 问题描述：编写一个程序，模拟一个100米短跑的场景。每个运动员的跑步过程就是一个线程。 要求如下： 1） 发令枪鸣枪后选手们开始赛跑； 2） 在所有运动员达到终点后系统开始统计选手们的成绩并把成绩发送到成绩统计系统； 3） 对所有运动员的成绩进行排序输出。 面试（加餐） 技术 JVM 内存划分及内存溢出情况； 不断创建线程会发生什么； 如何避免 SQL 注入； 如何获取用户的物理地址； restful 接口。 项目 目前团队的组成和分工； 举一个最近做的功能，如何对其分解设计和编码实现； 举一个排查问题的经历； 谈谈自己的编码规范，列举几个； 怎么做对外接口设计的，有哪些重要的点。 结果 17 * 14\nByteGreen 性格测试 好像都可以随便选，也没看到后续有看这个，直接就叫去面试了。\n面试 项目相关 xxl-job 在项目中是如何工作的； 目前有在学习哪方面的内容。 Java 方法重载和方法重写的区别； Java 方法是如何调用的，如何正确选择多个重载的方法进行调用； protected 关键字的作用； Java 优先级队列：proviteQueue； 静态代码块、静态变量和构造函数的执行顺序是什么，为什么是这个执行顺序。 JVM 判断垃圾对象的方法；\n常见垃圾收集器的算法和实现。\nRedis Redis 的缓存穿透和解决方法； Redis 的缓存雪崩和解决方法。 MQ 如何做到消息不丢失？如何判断消息已成功发送。 Spring 谈谈对 Spring 中 IOC 和 APO 的理解，其对应实现原理； 计算机基础、网络基础 有没有接触过物联网操作系统； 浏览器输入地址到页面展现内容的过程，请结合计算机硬件和软件相关知识进行描述； 网际层 IP 协议如何保证收到的包是完整的。 复试 我说太远了，能不能调整到周末去参加复试。HR 协商后说周末没有时间，就不用参加复试了。\n结果 19 * 13\n太平洋网络科技 一面 太难了，面试官一半的时间都在说：“这个不会没关系，我们换一个”，大致涉及如下部分：\n数据结构和算法 红黑树、二叉树； 二叉树怎么用于查找； 有向图的最短路径和算法。 缓存 Redis 和 MemCache 的区别； MemCache 如何实现集群。 数据库 MongoDB 的了解； MySQL 如何分库分表，数据迁移是怎么做的。 SpringCloud SpringCloud 怎么做负载均衡的，有哪些策略。 MQ 消息如何做到分布式一致性； 如何确认消息发送成功。 ElasticSearch 有没有用过 Elasticsearch。 结果 没有结果就是最好的结果。\n广州邮政 比较简单，就感觉像是招应届毕业生一样。\n一面 Java 基础 创建 String 对象的几种方式； ArrayList 和 LinkedList 的特点和使用场景； List 和 Set 的区别； Map 的实现类和使用场景。 MySQL 如何做分库分表，如何迁移数据； 如何做 SQL 优化。 Linux 线上排查问题时常用哪些指令； 随便说说你知道的 Linux 指令。 结果 14K * 13\n华为外包 算法题 字符串 aaabbccccd 压缩后为 3abb4cd，请编写解压函数，根据输入的字符串（只包含大小写字母和数字），判断是否为合法压缩的字符串，若合法则输出解压缩后的字符串，否则输出 !error。\n4dff -\u0026gt; ddddf 2dff -\u0026gt; !error 4d@A -\u0026gt;!error 给定 N 个正整数，为每个正整数图上一种颜色。要求同种颜色的所有数都可以被这种蓝色中最小的那个数整数。算出最少需要多少种颜色才能给这 N 个数进行上色。\n输入描述：\n第一行输入一个正胜数\n第二行有 N 个 int 型数\n3 2 4 6 1 4 2 3 4 9 2 牌面由颜色和数字组成，颜色为红、黄、蓝、绿中的一种，数字为0-9中的一个。开始时从手牌中选取一张卡牌打出，接下来如果玩家中有他上一次打出手牌的颜色或着数字相同的手牌，则可以继续打出，直至手牌打光或者没有符合条件的手牌，请找出最大可连续出牌的次数。\n1 4 3 4 5 r y b b r 3 （4y 4b 3b） 1 2 3 4 r y b l 1 结果 确定去 ByteGreen 所以没有去参加面试。\n","permalink":"https://2d992714.xianglin2020-github-io.pages.dev/posts/2022/2022-interview-record/","summary":"社招的几次笔试面试记录。","title":"2022面试记录"},{"content":"MySQL 中类型转换的几种方式 表达式中的隐式转换 # 在数学运算中隐式转换 select \u0026#39;1\u0026#39;, \u0026#39;1\u0026#39; + 0; # double 使用 CAST() 函数 CAST函数签名为：CAST(ANY:any AS datatype:cast_datatype):TypeOf(2)，作用是将任何类型的值转换为具有指定类型的值。目标类型可以是：BINARY、CHAR、DATE、DATETIME、TIME、DECIMAL、SIGNED、UNSIGNED。\n# 使用 CAST 函数 select cast(\u0026#39;5\u0026#39; AS BINARY); # varbinary select cast(\u0026#39;5\u0026#39; AS CHAR); # varchar(1) select cast(\u0026#39;2022-02-12\u0026#39; AS DATE), cast(20220212 AS DATE); # 2022-02-12 date(yyyy-MM-dd) select cast(\u0026#39;2022-02-12\u0026#39; AS DATETIME); # 2022-02-12 00:00:00 datetime(yyyy-MM-dd HH:mm:ss) select cast(\u0026#39;111500\u0026#39; AS TIME), cast(\u0026#39;11:15:00\u0026#39; AS TIME); # 11:15:00 time(HH:mm:ss) select cast(\u0026#39;5.45\u0026#39; AS DECIMAL), cast(\u0026#39;5.45\u0026#39; AS DECIMAL(10, 3)); # decimal(10, 0) select cast(\u0026#39;5\u0026#39; AS SIGNED), cast(\u0026#39;5\u0026#39; AS UNSIGNED); # bigint 需要注意的是：\nDECIMAL(M, D) 默认 M 为 10、D 为 0； DATE、DATETIME、TIME 类型的表达式不符合格式要求时返回 NULL。 使用 CONVERT() 函数 CONVERT 函数签名有两种：\nCONVERT(str:varchar, datatype:cast_datatype):TypeOf(2)，作用是将字符串转换为具有指定类型的值，此时和 CAST() 函数的目标类型和使用方式基本一致； CONVERT(str:varchar USING ref:reference/CHARSET):varchar，用于字符串在不同的字符集间转换。 # 使用 CONVERT 函数 select convert(20220212, DATE); select convert(_latin1\u0026#39;Müller\u0026#39; USING utf8); ","permalink":"https://2d992714.xianglin2020-github-io.pages.dev/posts/2022/mysql-type-convert/","summary":"MySQL 中类型转换的几种方式","title":"MySQL 中类型转换的几种方式"},{"content":"Cron 表达式学习 基本结构 Cron 表达式是一种使用简单字符串来表示时间规则的方式，其基本结构如下：\nSeconds Minutes Hours DayofMonth Month DayofWeak [Year]\n其中 Year 是可选的，不写代表包含所有年份。\n举个例子，每月1 号 0 点：0 0 0 1 * ?。\n各个域的范围 Seconds、Minutes：0 ~ 59\nHours：0 ~ 23\nDayofMonth：1 ~ L\nMonth：1 ~ 12 或者 JAN FEB MAR APR MAY JUN JUL AUG SEP OCT NOV DEC\nDayofWeak：1 ~ 7 或者 SUN MON TUE WED THU FRI SAT\nYear：1970 ~ 2099\n部分特殊字符 *：表示匹配任意值，如 Month 使用*表示每个月都会触发；\n?：只能用在 DayofMonth 和 DayofWeak 两个域，因为这两个域相互影响，比如每月 1 号，是这个周的第几天是不确定的，只能用 ?；\n-：表示范围，比如在 Minutes 中使用 10-20，表示从 10 分钟到 20 分钟每分钟触发一次；\n/：表示从起始时间开始，每隔固定间隔时间触发一次，比如在 Minutes 中使用 5/20 表示5、25、45分别触发一次；\n,：表示列出枚举值，比如在 Minutes 中使用 5,20，表示在第 5 分钟、第 20 分钟分别触发一次；\nL、W、#：L 表示最后，使用在DayofMonth 和 DayofWeak 两个域，如果在 DayofWeak 中使用 5L 表示最后一个周四触发；xW 表示最接近 x 的工作日，只能用在 DayofMonth 中，LW 表示每个月最后一个工作日；# 表示每个月第几个星期几，比如 2#1 表示第二个星期日。\n部分示例 每天凌晨一点触发一次：0 0 1 * * ? *\n每 30 分钟触发一次：0 0/30 * * * ?\n每天凌晨 2 ~ 6 点每 20 分钟触发一次：0 0/20 2-6 * * ?\n工作日 8 点触发一次：0 0 8 ? * MON-FRI\n每月最后一天 23 点执行一次：0 0 23 L * ?\n在线 CRON 表达式工具\n","permalink":"https://2d992714.xianglin2020-github-io.pages.dev/posts/2021/cron-basics/","summary":"Cron 表达式是一种使用简单字符串来表示时间规则的方式。","title":"Cron表达式学习"},{"content":"使用 VirtualBox 安装 Windows11 链接 Window11 下载链接：itellyou VirtualBox 下载链接：virtualbox How to install Microsoft Windows 11 on VirtualBox! 安装步骤 推荐设置 系统\n显示\n存储\n安装设置 在安装界面，使用Shift+F10调出控制台，在控制台中输入regedit调出注册表编辑器；\n在左侧依次找到如下路径：HKEY_LOCAL_MACHINE\\SYSTEM\\Setup；\n在Setup中新增配置项LabConfig，新建三个**DWORD（32-位）值**，值都设置为 1：\nBypassTPMCheck BypassRAMCheck BypassSecureBootCheck 结果如下：\n完成后关闭注册表和控制台，即可正常安装。\n","permalink":"https://2d992714.xianglin2020-github-io.pages.dev/posts/2021/virtualbox_install_win11/","summary":"使用 VirtualBox 安装 Windows11。","title":"VirtualBox_install_Win11"},{"content":"CAS 集成、认证流程和模拟登录 cas-demo\nCAS 集成 CAS Server 简单配置启动 cas-overlay-template 项目地址\nCAS 文档地址\nCAS 服务器启动步骤：\n从 cas-overlay-template 项目中获取 5.3 分支的文件，如图；\ngit clone -b 5.3 git@github.com:apereo/cas-overlay-template.git CAS 默认使用 https，需要先生成证书；\n可以使用项目中已提供的脚本生成，默认存储在 /etc/cas/thekeystore：\n./build.sh gencert 也可以手动生成，如图：\n# 生成证书 keytool -genkey -alias cas -keyalg RSA -keystore thekeystore -storepass changeit -deststoretype pkcs12 # 导出证书 keytool -export -file cas.crt -alias cas -keystore thekeystore -storepass changeit # 导入证书到JVM的证书库 keytool -import -keystore \u0026#34;/usr/local/opt/openjdk@8/libexec/openjdk.jdk/Contents/Home/jre/lib/security/cacerts\u0026#34; -file /Users/linxiang/Documents/cas/cas.crt -alias cas -storepass changeit 将 etc 文件夹及内容复制到根目录下；\ncp -r etc/cas/config /etc/cas/config 启动项目前可以修改 cas.properties 中的配置，比如静态认证时默认的用户名密码，应用端口等。\nlogging.config=file:/etc/cas/config/log4j2.xml # 静态认证用户 cas.authn.accept.users=admin::admin,xianglin::xianglin server.ssl.key-store=file:/etc/cas/thekeystore server.ssl.key-store-password=changeit server.ssl.key-password=changeit 其中 server.ssl.key-store 配置了证书的存储位置。可以将第 2 步生成的证书复制到 /etc/cas/thekeystore，也可以修改 key-store 的值指向证书位置。\n使用 WAR 方式启动项目，访问认证页面 https://cas.server.com:8443/cas/login，如图；\n./build.sh run 客户端集成 CAS CAS 客户端示例\nSpringBoot 配置\nCAS 客户端启动步骤：\n修改 web.xml 配置，增加 CAS 提供的过滤器，关键参数有：\ncasServerUrlPrefix：指向 CAS 服务器地址，如：https://cas.server.com:8443/cas，\nserverName：CAS 认证后重定向的地址，即客户端地址，如：https://localhost:9443/sample。\n完整 web.xml 示例如下：\n\u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34;?\u0026gt; \u0026lt;web-app version=\u0026#34;2.4\u0026#34; xmlns=\u0026#34;http://java.sun.com/xml/ns/j2ee\u0026#34; xmlns:xsi=\u0026#34;http://www.w3.org/2001/XMLSchema-instance\u0026#34; xsi:schemaLocation=\u0026#34;http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd\u0026#34;\u0026gt; \u0026lt;filter\u0026gt; \u0026lt;filter-name\u0026gt;CAS Single Sign Out Filter\u0026lt;/filter-name\u0026gt; \u0026lt;filter-class\u0026gt;org.jasig.cas.client.session.SingleSignOutFilter\u0026lt;/filter-class\u0026gt; \u0026lt;init-param\u0026gt; \u0026lt;param-name\u0026gt;casServerUrlPrefix\u0026lt;/param-name\u0026gt; \u0026lt;param-value\u0026gt;https://cas.server.com:8443/cas\u0026lt;/param-value\u0026gt; \u0026lt;/init-param\u0026gt; \u0026lt;/filter\u0026gt; \u0026lt;listener\u0026gt; \u0026lt;listener-class\u0026gt;org.jasig.cas.client.session.SingleSignOutHttpSessionListener\u0026lt;/listener-class\u0026gt; \u0026lt;/listener\u0026gt; \u0026lt;filter\u0026gt; \u0026lt;filter-name\u0026gt;CAS Authentication Filter\u0026lt;/filter-name\u0026gt; \u0026lt;filter-class\u0026gt;org.jasig.cas.client.authentication.AuthenticationFilter\u0026lt;/filter-class\u0026gt; \u0026lt;init-param\u0026gt; \u0026lt;param-name\u0026gt;casServerLoginUrl\u0026lt;/param-name\u0026gt; \u0026lt;param-value\u0026gt;https://cas.server.com:8443/cas/login\u0026lt;/param-value\u0026gt; \u0026lt;/init-param\u0026gt; \u0026lt;init-param\u0026gt; \u0026lt;!--这是客户端的部署地址，认证时会带着这个地址，认证成功后会跳转到这个地址--\u0026gt; \u0026lt;param-name\u0026gt;serverName\u0026lt;/param-name\u0026gt; \u0026lt;param-value\u0026gt;https://localhost:9443/sample\u0026lt;/param-value\u0026gt; \u0026lt;/init-param\u0026gt; \u0026lt;/filter\u0026gt; \u0026lt;filter\u0026gt; \u0026lt;filter-name\u0026gt;CAS Validation Filter\u0026lt;/filter-name\u0026gt; \u0026lt;filter-class\u0026gt;org.jasig.cas.client.validation.Cas30ProxyReceivingTicketValidationFilter\u0026lt;/filter-class\u0026gt; \u0026lt;init-param\u0026gt; \u0026lt;param-name\u0026gt;casServerUrlPrefix\u0026lt;/param-name\u0026gt; \u0026lt;param-value\u0026gt;https://cas.server.com:8443/cas\u0026lt;/param-value\u0026gt; \u0026lt;/init-param\u0026gt; \u0026lt;init-param\u0026gt; \u0026lt;param-name\u0026gt;serverName\u0026lt;/param-name\u0026gt; \u0026lt;param-value\u0026gt;https://localhost:9443/sample\u0026lt;/param-value\u0026gt; \u0026lt;/init-param\u0026gt; \u0026lt;init-param\u0026gt; \u0026lt;param-name\u0026gt;redirectAfterValidation\u0026lt;/param-name\u0026gt; \u0026lt;param-value\u0026gt;true\u0026lt;/param-value\u0026gt; \u0026lt;/init-param\u0026gt; \u0026lt;init-param\u0026gt; \u0026lt;param-name\u0026gt;useSession\u0026lt;/param-name\u0026gt; \u0026lt;param-value\u0026gt;true\u0026lt;/param-value\u0026gt; \u0026lt;/init-param\u0026gt; \u0026lt;init-param\u0026gt; \u0026lt;param-name\u0026gt;authn_method\u0026lt;/param-name\u0026gt; \u0026lt;param-value\u0026gt;mfa-duo\u0026lt;/param-value\u0026gt; \u0026lt;/init-param\u0026gt; \u0026lt;/filter\u0026gt; \u0026lt;!-- 该过滤器负责实现HttpServletRequest请求的包裹，比如允许开发者通过HttpServletRequest的getRemoteUser()方法获得SSO登录用户的登录名，可选配置--\u0026gt; \u0026lt;filter\u0026gt; \u0026lt;filter-name\u0026gt;CAS HttpServletRequest Wrapper Filter\u0026lt;/filter-name\u0026gt; \u0026lt;filter-class\u0026gt;org.jasig.cas.client.util.HttpServletRequestWrapperFilter\u0026lt;/filter-class\u0026gt; \u0026lt;/filter\u0026gt; \u0026lt;filter-mapping\u0026gt; \u0026lt;filter-name\u0026gt;CAS Single Sign Out Filter\u0026lt;/filter-name\u0026gt; \u0026lt;url-pattern\u0026gt;/*\u0026lt;/url-pattern\u0026gt; \u0026lt;/filter-mapping\u0026gt; \u0026lt;filter-mapping\u0026gt; \u0026lt;filter-name\u0026gt;CAS Validation Filter\u0026lt;/filter-name\u0026gt; \u0026lt;url-pattern\u0026gt;/*\u0026lt;/url-pattern\u0026gt; \u0026lt;/filter-mapping\u0026gt; \u0026lt;filter-mapping\u0026gt; \u0026lt;filter-name\u0026gt;CAS Authentication Filter\u0026lt;/filter-name\u0026gt; \u0026lt;url-pattern\u0026gt;/*\u0026lt;/url-pattern\u0026gt; \u0026lt;/filter-mapping\u0026gt; \u0026lt;filter-mapping\u0026gt; \u0026lt;filter-name\u0026gt;CAS HttpServletRequest Wrapper Filter\u0026lt;/filter-name\u0026gt; \u0026lt;url-pattern\u0026gt;/*\u0026lt;/url-pattern\u0026gt; \u0026lt;/filter-mapping\u0026gt; \u0026lt;welcome-file-list\u0026gt; \u0026lt;welcome-file\u0026gt; index.jsp \u0026lt;/welcome-file\u0026gt; \u0026lt;/welcome-file-list\u0026gt; \u0026lt;/web-app\u0026gt; 修改 Tomcat 配置，见 server.xml：\n\u0026lt;!-- \u0026lt;Connector port=\u0026#34;8080\u0026#34; protocol=\u0026#34;HTTP/1.1\u0026#34; connectionTimeout=\u0026#34;20000\u0026#34; redirectPort=\u0026#34;8443\u0026#34; /\u0026gt; --\u0026gt; \u0026lt;Connector port=\u0026#34;9443\u0026#34; protocol=\u0026#34;org.apache.coyote.http11.Http11Protocol\u0026#34; maxThreads=\u0026#34;150\u0026#34; SSLEnabled=\u0026#34;true\u0026#34; scheme=\u0026#34;https\u0026#34; secure=\u0026#34;true\u0026#34; clientAuth=\u0026#34;false\u0026#34; sslProtocol=\u0026#34;TLS\u0026#34; keystoreFile=\u0026#34;/Users/linxiang/Documents/cas/thekeystore\u0026#34; keystorePass=\u0026#34;changeit\u0026#34;/\u0026gt; 启动客户端，如图。\nSpring Boot 应用简单集成CAS 认证：\n在 pom.xml 中添加 CAS 客户端的依赖；\n\u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.jasig.cas.client\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;cas-client-support-springboot\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;3.6.2\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; 在 application.yaml 添加 CAS 相关配置；\nserver: port: 7443 ssl: key-store: classpath:thekeystore key-store-password: changeit key-store-type: pkcs12 key-alias: cas cas: server-url-prefix: https://cas.server.com:8443/cas server-login-url: https://cas.server.com:8443/cas/login client-host-url: https://client1.server.com:7443/ 使用注解 @EnableCasClient来启用 CAS；\n@SpringBootApplication @EnableCasClient public class SpringbootCasDemoApplication { public static void main(String[] args) { SpringApplication.run(SpringbootCasDemoApplication.class, args); } } 启动客户端，如图。\n认证流程 CAS 名词 通过 Fiddler 抓包学习 CAS 的认证流程前，先熟悉几个名词 （见 CAS Protocol）：\nTGC：（Ticket-granting Cookie），存放 SSO 用户身份凭据的 Cookie，类似 JSESSIONID。 TGT：（Ticket Granting Ticket），在 SSO 中标识一位用户，类似 Session。 ST：（Service Ticket），作为一个参数跟在 url 后面， 代表 CAS 为某一用户访问 CAS 客户端签发的凭据。 The TGT (Ticket Granting Ticket), stored in the TGC cookie, represents a SSO session for a user. The ST (Service Ticket), transmitted as a GET parameter in urls, stands for the access granted by the CAS server to the CASified application for a specific user. 首次认证流程 按上述步骤启动 CAS 相关应用后，打开浏览器，访问 https://localhost:9443/sample/，输入口令完成登录，在 Fiddler 中过滤无关请求，需要学习的请求如图。\n序号 930，第一次访问 https://localhost:9443/sample/，会经过 CAS 提供的过滤器 org.jasig.cas.client.authentication.AuthenticationFilter，过滤器判断到当前用户没有登录，将请求重定向到认证中心，如图；\n重定向的地址是在过滤器参数中配置的 casServerLoginUrl，service 参数值则是过滤器参数中配置的 serverName，即认证后要跳转的地址。\n序号 932，CAS 服务器接收到登录请求，返回登录页面；\n序号 956，用户输入口令后提交登录，CAS 服务器验证用户名密码是否有效（这里使用静态认证），如图；\n使用 POST 请求的方式提交登录表单，用户认证成功后，请求的响应码是 302，指示浏览器将请求重定向到最开始访问的地址。重定向的地址通过 querySpring 携带了一个 ticket 参数。同时会设置一个名为 TGC 的 Cookie，路径为 /cas/。下次访问 https://cas.server.com:8443 时会携带这个 Cookie，服务器可以根据这个 TGC，查找与之对应的 TGT，从而判断用户是否认证过，是否需要展示登录页面。TGT 与 TGC 的关系就像 SESSION 与 Cookie 中 JSESSIONID 的关系；\n序号 957，浏览器向 https://localhost:9443/sample/ 发起请求，请求携带 CAS 服务器签发的 ST，localhost:9443 在过滤器中获取到 ticket 值，通过 HTTP 请求的方式调用 CAS 服务验证该 ticket 是否有效，过滤器全称 org.jasig.cas.client.validation.Cas30ProxyReceivingTicketValidationFilter ，验证方法在 org.jasig.cas.client.validation.AbstractUrlBasedTicketValidator#validate， 如图。\n此时，CAS 服务器的日志如下图。\nlocalhost:9443 收到 CAS 服务器返回，知道用户合法，即可正常响应请求，展示页面，如图。\n序号 958，ST 验证通过后再次重定向是在 Cas30ProxyReceivingTicketValidationFilter 过滤器中使用了 redirectAfterValidation 参数（默认为 true），将其配置成 false 即可在验证用户后返回正常页面，如图。\n再次认证流程 第一次访问 localhost:9443 的步骤已梳理完成，保持用户在 SSO 的会话，尝试访问 localhost:7443，观察其流程。请求重定向到 CAS 服务器时，携带了值为 TGC 的 Cookie。CAS 服务器通过此 TGC 查找到对应的 TGT，判断到用户已经登录，便不再展示登录页面，即少了 CAS 服务器登录这个步骤，剩余步骤和首次登录一致，如图。\n官方文档中的时序图如下，见 CAS - CAS Protocol。\n模拟登录 示例代码见 Simulation.java\n使用 HttpClient 模拟登录 分析首次登录流程可知，模拟登录时只需要处理好 CAS 认证、重定向到客户端两个步骤，就可以获取包含 JSESSIONID 的 Cookie，使用此 Cookie 便可以完成其它后续请求的模拟，模拟登录的简单示例代码如下：\npublic class Simulation { private static final String SERVER_LOGIN_URL = \u0026#34;https://cas.server.com:8443/cas/login\u0026#34;; private static final String CLIENT_HOST_URL = \u0026#34;https://client1.server.com:7443/\u0026#34;; private static final CloseableHttpClient client; static { client = HttpClients.createDefault(); } /** * 模拟登录，返回包含 JSESSIONID 的 Cookie * * @param userName userName * @param password password * @return CookieValue * @throws IOException IOException */ public String simulationLogin(String userName, String password) throws IOException { CloseableHttpResponse response; String url = SERVER_LOGIN_URL + \u0026#34;?service=\u0026#34; + CLIENT_HOST_URL; // 1、获取 CAS 登录页面 HttpGet httpGet = new HttpGet(url); response = client.execute(httpGet); StatusLine statusLine = response.getStatusLine(); int statusCode = statusLine.getStatusCode(); if (HttpStatus.SC_OK != statusCode) { System.out.println(\u0026#34;获取 CAS 登录页面返回错误！\u0026#34; + statusLine); return null; } String casLoginPageHtml = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8); IOUtils.closeQuietly(response); // 使用 JSoup 解析表单 Document document = Jsoup.parse(casLoginPageHtml); Element fm1Element = document.getElementById(\u0026#34;fm1\u0026#34;); Elements inputElements = fm1Element.getElementsByTag(\u0026#34;input\u0026#34;); // 提交参数 List\u0026lt;NameValuePair\u0026gt; casLoginBody = new ArrayList\u0026lt;\u0026gt;(8); for (Element inputElement : inputElements) { String type = inputElement.attr(\u0026#34;type\u0026#34;); if (\u0026#34;submit\u0026#34;.equals(type)) { continue; } String name = inputElement.attr(\u0026#34;name\u0026#34;); String value = inputElement.attr(\u0026#34;value\u0026#34;); if (\u0026#34;username\u0026#34;.equals(name)) { value = userName; } if (\u0026#34;password\u0026#34;.equals(name)) { value = password; } casLoginBody.add(new BasicNameValuePair(name, value)); } // 2、提交登录表单 HttpEntity loginEntity = new UrlEncodedFormEntity(casLoginBody, StandardCharsets.UTF_8); HttpPost httpPost = new HttpPost(url); httpPost.setEntity(loginEntity); response = client.execute(httpPost); statusLine = response.getStatusLine(); if (HttpStatus.SC_MOVED_TEMPORARILY != statusLine.getStatusCode()) { System.out.println(\u0026#34;登录 CAS 返回错误！\u0026#34; + statusLine); return null; } // 处理响应头 Header cookie = response.getFirstHeader(\u0026#34;Set-Cookie\u0026#34;); System.out.println(\u0026#34;cookie: \u0026#34; + cookie.getName() + \u0026#34; =\u0026gt; \u0026#34; + cookie.getValue()); Header location = response.getFirstHeader(\u0026#34;Location\u0026#34;); String locationUrl = location.getValue(); System.out.println(\u0026#34;locationUrl : \u0026#34; + locationUrl); IOUtils.closeQuietly(response); // 3、重定向到客户端 httpGet = new HttpGet(locationUrl); response = client.execute(httpGet); statusLine = response.getStatusLine(); if (HttpStatus.SC_MOVED_TEMPORARILY != statusLine.getStatusCode()) { System.out.println(\u0026#34;登录客户端返回错误！\u0026#34; + statusLine); return null; } // 处理响应头，返回 Cookie cookie = response.getFirstHeader(\u0026#34;Set-Cookie\u0026#34;); System.out.println(\u0026#34;cookie: \u0026#34; + cookie.getName() + \u0026#34; =\u0026gt; \u0026#34; + cookie.getValue()); location = response.getFirstHeader(\u0026#34;Location\u0026#34;); locationUrl = location.getValue(); System.out.println(\u0026#34;locationUrl : \u0026#34; + locationUrl); IOUtils.closeQuietly(response); return cookie.getValue(); } } 常见问题 忽略 SSL 证书 CAS 默认不支持 HTTP 请求，使用 HttpClient 模拟登录时可能会遇到如下错误：\nException in thread \u0026#34;main\u0026#34; javax.net.ssl.SSLHandshakeException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target at java.base/sun.security.ssl.Alert.createSSLException(Alert.java:131) ......... Caused by: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target at java.base/sun.security.validator.PKIXValidator.doBuild(PKIXValidator.java:439) 有两种方式解决这个问题：\n使用如下语句将证书导入 JVM 的证书库：\nkeytool -import -keystore \u0026#34;JAVA_HOME/jre/lib/security/cacerts\u0026#34; -file \u0026lt;path-to-cert\u0026gt; -alias cas -storepass changeit 设置 HttpClient 忽略 SSL 证书，如示例代码如下：\nstatic { // client = HttpClients.createDefault(); SSLContext sslContext; try { sslContext = SSLContexts.custom().loadTrustMaterial(null, new TrustStrategy() { @Override public boolean isTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException { // 返回 true 即忽略认证 return true; } }).build(); } catch (Exception e) { throw new RuntimeException(e); } client = HttpClients .custom() .setSslcontext(sslContext) .build(); } 忽略主机名称验证 HttpClient 在 SSL 握手时会验证对方主机名称，防止链接被重定向到其他地址上去，若服务器证书上的 HostName 和实际访问的 URL 不匹配时，会抛出如下异常：\nException in thread \u0026#34;main\u0026#34; javax.net.ssl.SSLPeerUnverifiedException: Host name \u0026#39;client1.server.com\u0026#39; does not match the certificate subject provided by the peer (CN=cas.server.com, OU=Unknown, O=Unknown, L=Unknown, ST=Unknown, C=Unknown) 这里是因为 CAS 登录后会重定向到客户端 client1.server.com，可以通过如下配置忽略这个问题，参考 Host name does not match the certificate subject provided by the peer - Stack Overflow：\nstatic { // client = HttpClients.createDefault(); client = HttpClients.custom() .setSSLHostnameVerifier(NoopHostnameVerifier.INSTANCE) .build(); } NoopHostnameVerifier 在 verify 方法中返回 true 忽略主机名验证。\npublic boolean verify(String s, SSLSession sslSession) { return true; } 关闭 HttpClient 自动重定向 simulationLogin 方法期望返回 client.server.com 响应头中的 Cookie 信息，实际执行时控制台打印如下。\ncookie: Set-Cookie =\u0026gt; TGC=eyJhbGciOiJIUzUxMiJ9.ZXlKNmFYQWlPaUpFUlVZaUxDSmhiR2NpT2lKa2FYSWlMQ0psYm1NaU9pSkJNVEk0UTBKRExVaFRNalUySW4wLi5lZGRGZVhZT3A1ekVscy10aTFlRFhnLm1wemwtdnFxMEo0UDM2cGp3SFZnMTN4cVc3UWxlUUtfSzdhNkFOUHI4TFkwRFZDUnYzby13VUE2V2FCMG1zamQ3OGVNcmh4MVAxTDdpdDZBMWhveUZ5dDJMZkxTTEpNbjRNa0dhT3lHTWxERjV5SXZhd0xvQWlxNVZsclR4S0dPY3J5S3FXMF9wdl9aa09TT3RoMmVuT2NLSFg4VkMzMkhiaENyRHFGcWdYNlBtQ2RZdUNRMWl6aUVYVTM5X2tMdy45RVdvbVgzaVBQbEhzd2lCQlB4Q1Zn.YKSYFjFhm1TXHOr1Bzrb3z9-oSDsGECpJK7kOMeTz4oAeOF8Nd2WYii6oV1kb9Bzat4w7NhPxUxjvLmPjoe8Jg; Path=/cas/; Secure; HttpOnly locationUrl : https://client1.server.com:7443/?ticket=ST-16-eYyvOXKLcocogqqtv84HVwxhuuMLindeMacBook-Pro 登录客户端返回错误！HTTP/1.1 200 cookie =\u0026gt; null 发现“3、重定向到客户端”这一步并没有像在“认证流程”中分析的那样返回 302 状态，而是直接返回 200，这是因为 HttpClient 默认自动处理重定向，可以通过 Fiddler 抓包观察到这一点。自动重定向后的请求返回响应头中并没有 Cookie 信息，想要获取 Cookie 可以配置 HttpClient 不自动重定向，代码如下。\nstatic { // client = HttpClients.createDefault(); final RequestConfig config = RequestConfig .custom() .setRedirectsEnabled(false) .build(); client = HttpClients.custom() .setDefaultRequestConfig(config) .setSSLHostnameVerifier(NoopHostnameVerifier.INSTANCE) .build(); } 再次执行，即可获取到 Cookie 信息，如图。\nHttpClient 自动重定向默认不支持 POST 请求，这也是有“3、重定向到客户端”这一步的原因，HttpClient 使用 RedirectStrategy 来完成重定向的处理，默认实现类 DefaultRedirectStrategy 中只允许 GET、HEAD方法的重定向，如果需要支持 POST 可以使用 LaxRedirectStrategy，代码如下。\nclient = HttpClients.custom() .setRedirectStrategy(LaxRedirectStrategy.INSTANCE) .build(); 使用 Fillder 观察 HttpClient 请求 正常情况下 Fillder 是无法抓取 HttpClient 请求的，可以通过设置代理的方式，代码如下。\nHttpHost httpHost = new HttpHost(\u0026#34;127.0.0.1\u0026#34;, 8866); final RequestConfig config = RequestConfig.custom() .setRedirectsEnabled(false) .setProxy(httpHost) .build(); client = HttpClients.custom() .setDefaultRequestConfig(config) .setSSLHostnameVerifier(NoopHostnameVerifier.INSTANCE) .build(); 获取 Cookie 信息 HttpClient 自动重定向时，可能无法在响应头中获取到 Cookie，而且自己解析请求头获取 Cookie 也比较繁琐。可以借助 CookieStore 来管理 Cookie，代码如下。\nfinal CookieStore cookieStore = new BasicCookieStore(); client = HttpClients.custom() .setDefaultCookieStore(cookieStore) .build(); // 遍历 Cookie List\u0026lt;Cookie\u0026gt; cookieList = cookieStore.getCookies(); for (Cookie cookie1 : cookieList) { System.out.println(cookie1.getName() + \u0026#34; =\u0026gt; \u0026#34; + cookie1.getValue() + \u0026#34; =\u0026gt; \u0026#34; + cookie1.getPath()); } 打印结果如下，第一个是 client1.server.com 的 Cookie，第二个是 cas.server.com 的 Cookie。\nJSESSIONID =\u0026gt; A07EF954C7591C5F1F724EFAA8141E86 =\u0026gt; / TGC =\u0026gt; eyJhbGciOiJIUzUxMiJ9.ZXlKNmFYQWlPaUpFUlVZaUxDSmhiR2NpT2lKa2FYSWlMQ0psYm1NaU9pSkJNVEk0UTBKRExVaFRNalUySW4wLi4ycXMwX2lJZzYya1o1bDN5alcyTFpnLnVjOWdlR2VlbjJpM3lmdTZQd25jYzZqLUxpOThGNk5zTzE4d2NFaHBvdmI3REMtaG16TmVEbXhhNGhpVDJNU2FqWUdsNU5CSm16d3FfVkgzVFZCZG9rcG5HUFVBZkkyUE1LZ2pkOWthMTZYOVlfdnE1S3ZwdWlSdmE3QTJSSTlWM0pBS3IzS2p6aUVIUUpLQUQzLTM0Rmd3R3VQVUViak5LM3psZHA1UnVfRlktS0lMUmZPaFNYd2tqaF9wbGRiVS42Uy1CWTFTaUozNkN5b2tEV2VsbzVR.CsMVzXEYTWg5v7Oyh_DvosrY4mhFaqWTUqyn5Vzg2Spx3EMwhul12u9sovG8HS5lh73T5bKshw8UUtL82elLnQ =\u0026gt; /cas/ 借助 Postman 简化编码 遇到不会写或者代码繁杂的请求，可以借助 Postman 的示例代码快速完成开发。比如我不会使用 HttpClient 上传文件，可以先使用 Postman 完成文件上传接口的验证，如图：\n在右侧工具栏找到 Code 标签，在 Code snippet 中选择 “Java - OkHttp”，即可复用代码，如图。\n将其复制到自己的代码中，简化开发，Cookie 可以使用上面提到的方法获取，详细代码如下。\npublic void upload(String cookie, String filePath) throws IOException { OkHttpClient client = new OkHttpClient().newBuilder().hostnameVerifier(NoopHostnameVerifier.INSTANCE) .build(); RequestBody body = new MultipartBody.Builder().setType(MultipartBody.FORM) .addFormDataPart(\u0026#34;file\u0026#34;, filePath.substring(filePath.lastIndexOf(\u0026#34;/\u0026#34;)), RequestBody.create(MediaType.parse(\u0026#34;application/octet-stream\u0026#34;), new File(filePath))) .build(); Request request = new Request.Builder() .url(CLIENT_HOST_URL + \u0026#34;upload\u0026#34;) .method(\u0026#34;POST\u0026#34;, body) .addHeader(\u0026#34;Cookie\u0026#34;, cookie) .build(); Response response = client.newCall(request).execute(); ResponseBody responseBody = response.body(); if (responseBody != null) { String string = responseBody.string(); System.out.println(string); } IOUtils.closeQuietly(response); IOUtils.closeQuietly(responseBody); } 配合 simulationLogin() 方法可以完整的实现模拟 CAS 登录并上传文件的需求，结果如图：\npublic class App { public static void main(String[] args) throws IOException { String filePath = \u0026#34;/Users/linxiang/Documents/cas/cas-demo/server.xml\u0026#34;; Simulation simulation = new Simulation(); String cookie = simulation.simulationLogin(\u0026#34;xianglin\u0026#34;, \u0026#34;xianglin\u0026#34;); System.out.println(\u0026#34;cookie =\u0026gt; \u0026#34; + cookie); simulation.upload(cookie, filePath); } } ","permalink":"https://2d992714.xianglin2020-github-io.pages.dev/posts/2021/cas-integration/","summary":"CAS 集成、认证流程和模拟登录分享。","title":"CAS 集成、认证流程和模拟登录"},{"content":"使用jar命令将 class 文件打包成 jar 文件 jar cvf test.jar .\\store\\xianglin\\mvn\\test\\ 将生成的 jar 文件安装至本地仓库 mvn install:install-file -Dfile=C:\\Users\\xianglin\\Documents\\mvn\\target\\classes\\test-1.0.jar -DgroupId=store.xianglin -DartifactId=mvn -Dversion=1.0 -Dpackaging=jar 将生成的 jar 文件上传至 Maven 私服 mvn deploy:deploy-file -DgroupId=store.xinaglin -DartifactId=test -Dversion=1.0 -Dfile=test-1.0.jar -Durl=http://127.0.0.1/nexus/content/repositories/mvn/ -DrepositoryId=mvn ","permalink":"https://2d992714.xianglin2020-github-io.pages.dev/posts/2021/java-interface-install/","summary":"接口打包、安装到本地、上传至仓库。","title":"接口打包、安装到本地、上传至仓库"},{"content":"Docker 安装 Oracle-11g 参考链接 DockerHub：jaspeen/oracle-11g (docker.com) Oracle Database：适用于 Linux x86-64 的 Oracle Database 11g 第 2 版 | Oracle 中国 Docker CE 清华源：docker-ce | 镜像站使用帮助 | 清华大学开源软件镜像站 | Tsinghua Open Source Mirror 安装步骤 Docker 安装步骤 删除旧版 Docker\nsudo apt-get remove docker docker-engine docker.io 安装依赖\nsudo apt-get install apt-transport-https ca-certificates curl gnupg2 software-properties-common 导入 Docker GPG 公钥\ncurl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add - 添加软件仓库\nsudo add-apt-repository \\ \u0026#34;deb [arch=amd64] https://mirrors.tuna.tsinghua.edu.cn/docker-ce/linux/ubuntu \\ $(lsb_release -cs) \\ stable\u0026#34; 安装 Docker\nsudo apt-get update sudo apt-get install docker-ce 替换国内源，编辑/etc/docker/daemon.json\n{ \u0026#34;registry-mirrors\u0026#34;: [ \u0026#34;http://registry.docker-cn.com\u0026#34;, \u0026#34;http://docker.mirrors.ustc.edu.cn\u0026#34;, \u0026#34;http://hub-mirror.c.163.com\u0026#34; ], \u0026#34;insecure-registries\u0026#34;: [ \u0026#34;registry.docker-cn.com\u0026#34;, \u0026#34;docker.mirrors.ustc.edu.cn\u0026#34; ], \u0026#34;debug\u0026#34;: true, \u0026#34;experimental\u0026#34;: true } 将当前用户加入 docker 组\nsudo gpasswd -a username docker newgrp docker Oracle-11g 安装步骤 下载 linux.x64_11gR2_database_1of2.zip、linux.x64_11gR2_database_2of2.zip两个文件，将其解压。\n拉取 Oracle-11g 的 docker 镜像\ndocker pull jaspeen/oracle-11g 安装启动 Oracle Database\ndocker run --privileged --name oracle11g -p 1521:1521 -v /home/xianglin/oracle:/install jaspeen/oracle-11g 安装过程较长，查看输出，有 100% complete 时，表示安装成功。\n解除账户限制 连接到容器\ndocker exec -it oracle11g /bin/bash 切换到 oracle 用户，然后链接到 sql 控制台\n[root@9b255eb5dd63 /]# su - oracle Last login: Tue Apr 6 02:14:23 UTC 2021 on pts/0 [oracle@9b255eb5dd63 ~]$ sqlplus / as sysdba SQL*Plus: Release 11.2.0.1.0 Production on Tue Apr 6 02:38:35 2021 Copyright (c) 1982, 2009, Oracle. All rights reserved. Connected to: Oracle Database 11g Enterprise Edition Release 11.2.0.1.0 - 64bit Production With the Partitioning, OLAP, Data Mining and Real Application Testing options SQL\u0026gt; 解锁账户\nSQL\u0026gt; alter user scott account unlock; User altered. SQL\u0026gt; conn scott/tiger ERROR: ORA-28001: the password has expired Changing password for scott New password: Retype new password: Password changed Connected. SQL\u0026gt; 使用可视化工具登录\n","permalink":"https://2d992714.xianglin2020-github-io.pages.dev/posts/2021/docker-install-oracle11g/","summary":"\u003ch1 id=\"docker-安装-oracle-11g\"\u003eDocker 安装 Oracle-11g\u003c/h1\u003e\n\u003ch2 id=\"参考链接\"\u003e参考链接\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003eDockerHub：\u003ca href=\"https://hub.docker.com/r/jaspeen/oracle-11g\" target=\"_blank\" rel=\"noopener\"\u003ejaspeen/oracle-11g (docker.com)\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003eOracle Database：\u003ca href=\"https://www.oracle.com/cn/database/enterprise-edition/downloads/oracle-db11g-linux.html\" target=\"_blank\" rel=\"noopener\"\u003e适用于 Linux x86-64 的 Oracle Database 11g 第 2 版 | Oracle 中国\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003eDocker CE 清华源：\u003ca href=\"https://mirrors.tuna.tsinghua.edu.cn/help/docker-ce/\" target=\"_blank\" rel=\"noopener\"\u003edocker-ce | 镜像站使用帮助 | 清华大学开源软件镜像站 | Tsinghua Open Source Mirror\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"安装步骤\"\u003e安装步骤\u003c/h2\u003e\n\u003ch3 id=\"docker-安装步骤\"\u003eDocker 安装步骤\u003c/h3\u003e\n\u003col\u003e\n\u003cli\u003e\n\u003cp\u003e删除旧版 Docker\u003c/p\u003e","title":"Docker 安装 Oracle-11g"},{"content":"单机问题 机器故障 容量瓶颈 QPS瓶颈 Redis 安装 使用 Debian 12 从源码中安装 Redis，操作的主目录在 /root/redis。\nhttps://redis.io/docs/latest/operate/oss_and_stack/install/install-redis/install-redis-from-source/\n# 安装编译环境 apt install build-essential tcl # 下载源码包安装 wget https://download.redis.io/redis-stable.tar.gz tar -xzvf redis-stable.tar.gz cd redis-stable make # 启动 redis ./src/redis-server Redis 主从复制 提供了数据副本、扩展了读性能。\n主从结构搭建 有两种方式可以配置 Redis 的复制功能，分别是使用slaveof host port命令或修改 Slave 的配置文件。\n创建目录存放配置、日志和数据文件。\n# 位于 /root/redis mkdir conf mkdir data mkdir log 创建主节点配置文件 conf/redis.conf。\n# 放行 IP bind 0.0.0.0 # 后台启动 daemonize yes # 日志存储 logfile \u0026#34;/root/redis/log/redis.log\u0026#34; # rdb 数据文件 dbfilename dump.rdb # 开启 aof 模式 appendonly yes appendfilename \u0026#34;appendonly.aof\u0026#34; # 数据文件目录 dir /root/redis/data # 密码 requirepass 123456 # 从节点访问主节点的密码 masterauth 123456 # 从节点只读 replica-read-only yes 从节点配置文件增加一行：\n# 隶属的主节点 slaveof 192.168.56.101 6379 启动 redis。\n# 位于 /root/redis redis-stable/src/redis-server conf/redis.conf 登录 redis 查看配置主从信息。\nredis-stable/src/redis-cli -a 123456 # 登录 redis 后 info replication 主节点信息\n从节点信息\n取消复制 在 slave 节点上执行 slaveof no one。\nRedis Sentinel Redis Sentinel 的作用 Redis 的 Sentinel 系统用于管理多个 Redis 服务器（instance），Redis Sentinel 主要有三个作用：\n监控（Monitoring）：Sentinel 会不断的检查 Redis 主服务器和从服务器是否运行正常。 提醒（Notification）：当被监控的某个 Redis 服务器出现问题时，Sentinel 可以通过 API 向管理员或其它应用程序发送通知。 自动故障迁移（Atomic failover）：当一个主服务器不能正常工作时，Sentinel 会开始启动一次自动故障迁移。它会将失效主服务器的其中一个从服务器升级为新的主服务器，并让其它从服务器改为同步新主服务器的数据。当客户端试图连接失效主服务器时，集群会向客户端返回新主服务器的地址。 配置提供（Configuration provider）：客户端连接 sentinel，以查询当前负责特定服务的 Redis 主节点地址，如果发生故障转移，sentinel 会报告新的地址。 Redis Sentinel 是一个分布式系统，可以在一个架构中运行多个 Sentinel 进程，这些进程使用流言协议（Gossip Protocol）来接收关于主服务器是否下线的信息，并使用投票协议（Agreement Protocol）来决定是否执行自动故障迁移，以及选择哪个从服务器作为新的主服务器。\nSentinel 搭建 先配置开启主从节点。\n创建配置文件 conf/sentinel.conf\nbind 0.0.0.0 port 26379 daemonize yes logfile \u0026#34;/root/redis/log/sentinel.log\u0026#34; pidfile \u0026#34;/var/run/sentinel.pid\u0026#34; sentinel monitor mymaster 192.168.56.101 6379 2 sentinel auth-pass mymaster 123456 sentinel down-after-milliseconds mymaster 10000 启动 sentinel\nredis-stable/src/redis-server conf/redis.conf redis-stable/src/redis-sentinel conf/sentinel.conf 查看日志\n客户端通过 Sentinel 获取主节点地址 连接到一个或多个 sentinel 节点；\n通过 sentinel get-master-addr-by-name \u0026lt;name\u0026gt; 命令查询主节点信息；\n客户端解析 sentinel 返回的主节点信息；\n客户端使用获取到的主节点信息连接到主节点，开始进行数据操作；\n如果主节点故障，sentinel 会选举新的主节点，客户端需要重新查询 sentinel 获取新的主节点地址并重新连接；\n客户端可以订阅 sentinel 的事件通知 +switch-master，以便在主节点变更时及时获取通知并重新连接；\n+convert-to-slave：切换从节点（元主节点将为从节点）\n+sdown：主观下线\nSentinel 故障转移 检测主节点故障\nsentinel 节点会定期向主节点、从节点和其他 sentinel 节点发送心跳检测（PING 命令）；\n如果主节点在配置的超时时间内（down-after-milliseconds）未响应，sentinel 会将其标记为主观下线（SDOWN）；\n当多个 sentinel 节点都认为主节点不可用时，sentinel 会将其标记为客观下线（ODOWN）；\nsentinel 集群会通过 Raft 算法选举 Sentinel Leader 来负责故障转移；\nSentinel Leader 从当前从节点中选择一个作为新的主节点，选举规则包括：\n优先级：从节点的 slave-priority 配置值； 复制偏移量：选择复制数据最完整的从节点； 运行 ID：选择 run_id 最小的从节点。 Sentinel Leader 会向选中的从节点发送 slaveof no one 命令，将其提升为新的主节点；\nSentinel Leader 会向其他从节点发送 slaveof 命令，让它们开始复制新的主节点；\nSentinel 会通过发布订阅向客户端发送 +switch-master 事件，通知主节点已变更；\n故障转移完成后 sentinel 会更新其内部状态，同时将故障转移结果持久化到配置文件中，以便再重启后保持一致性。\n如果旧的主节点恢复，Sentinel 会将其降级为从节点，并让其复制新的主节点。\nsentinel 工作原理 Sentinel 内部有3个定时任务：\n每1秒每个 sentinel 对其它 sentinel 和 redis 节点执行 PING 操作（监控）； 每2秒每个 sentinel 通过 master 节点的 channel 交换信息（publish/subscribe）； 每10秒每个 sentinel 会对 master 和 slave 执行 INFO 命令。 Redis 集群架构 Redis Cluster 主要解决并发量和数据量问题。\n集群搭建 使用 3 个节点启动 6 个 redis 服务，节点 ip 分别为 192.168.56.10[1,2,3]，redis 服务端口分别为 637[1-6]。\n创建目录存放配置、数据和日志文件。\nmkdir -p redis/cluster/conf redis/cluster/data redis/cluster/log 创建配置文件 redis/cluster/conf/redis-6371.conf\nbind 0.0.0.0 port 6371 daemonize yes logfile \u0026#34;/root/redis/cluster/log/redis-6371.log\u0026#34; dbfilename dump-6371.rdb appendonly yes appendfilename \u0026#34;appendonly-6371.aof\u0026#34; dir /root/redis/cluster/data requirepass 123456 masterauth 123456 # 集群配置 cluster-enabled yes cluster-config-file nodes-6371.conf cluster-node-timeout 15000 cluster-announce-ip 192.168.56.101 cluster-announce-port 6371 cluster-announce-bus-port 16371 不同服务使用的配置文件不同，体现在端口和命名后缀上，将与 6371 和 56.101 有关的值依次递增，在 3 个节点上创建 6 份配置文件。\n依次启动 6 个 redis 服务\nredis-stable/src/redis-server cluster/conf/redis-6371.conf 随便选取一个节点执行 cluster 创建操作\nredis-stable/src/redis-cli -a 123456 --cluster create 192.168.56.101:6371 192.168.56.101:6372 192.168.56.102:6373 192.168.56.102:6374 192.168.56.103:6375 192.168.56.103:6376 --cluster-replicas 1 确认配置无误后输入 yes 即可\n集群管理 集群状态 检查集群状态\nredis-stable/src/redis-cli -a 123456 --cluster check 192.168.56.101:6371 查看集群信息\n# 连接到 redis cluster redis-stable/src/redis-cli -c -a 123456 -h 192.168.56.101 -p 6371 # 集群信息 cluster info # 节点信息 cluster nodes 性能测试 集群性能测试\nredis-stable/src/redis-benchmark -a 123456 -h 192.168.56.101 -p 6371 --cluster -t set,get -r 1000000 -n 1000000 -c 1000 添加节点 添加主节点\n# 添加主节点 # add-node new-node exists-master-node redis-stable/src/redis-cli -a 123456 --cluster add-node 192.168.56.103:6377 192.168.56.103:6375 --cluster-master-id db9b855f6db790d5a99c7062bd1a98107e1949ce # 重新分片 redis-stable/src/redis-cli -a 123456 --cluster reshard 192.168.56.101:6371 --cluster-from 76d2931297beabb29c1263e45dba6a6b2635d194 --cluster-to b6ff5ce0bc981184e21f5b2e7fcd32056999cd35 --cluster-slots 2000 --cluster-yes 添加从节点\n# add-node new-node replication-from-node redis-stable/src/redis-cli -a 123456 --cluster add-node 192.168.56.103:6378 192.168.56.103:6377 --cluster-slave --cluster-master-id b6ff5ce0bc981184e21f5b2e7fcd32056999cd35 删除节点 删除从节点\n# 从集群中移除从节点 redis-stable/src/redis-cli -a 123456 --cluster del-node 192.168.56.103:6378 b2ee915237e7e1163090ce73000592d6b3445d90 # 停止 redis 服务 redis-stable/src/redis-cli -a 123456 -h 192.168.56.103 -p 6378 shutdown 删除主节点\n先执行重新分片命令，将主节点上的 slot 移到其它主节点上。\n# 交换式命令 redis-stable/src/redis-cli -a 123456 --cluster reshard 192.168.56.101:6371 删除节点\n# 从集群中移除节点 redis-stable/src/redis-cli -a 123456 --cluster del-node 192.168.56.103:6377 b6ff5ce0bc981184e21f5b2e7fcd32056999cd35 # 停止 redis 服务 redis-stable/src/redis-cli -a 123456 -h 192.168.56.103 -p 6377 shutdown Redis 数据迁移 RedisShake\n","permalink":"https://2d992714.xianglin2020-github-io.pages.dev/posts/2021/multi-redis/","summary":"Redis 主从复制、哨兵、集群配置。","title":"多机 Redis"},{"content":" 数据库模块划分 关键语法 GROUP BY 满足 SELECT 子句中的列名必须为分组列或列函数 列函数对于 GROUP BY 子句定义的每一个组各返回一个结果 HAVING 通常与 GROUP BY 子句一起使用 WHERE 过滤行，HAVING 过滤组 关键字的顺序：WHERE \u0026ndash; GROUP BY \u0026ndash; HAVING 索引相关内容 为什么要使用索引 全表扫描适用范围窄，借助字典的思想，使用索引可以加快查询速度。\n什么样的字段可以作为索引 主键、唯一键等能使数据区分开的字段。\n索引的数据结构 二叉查找树 B - Tree B+ - Tree Hash 索引 MySQL 索引使用的数据结构主要有 BTree 索引和哈希索引。对于哈希索引，底层数据结构就是哈希表，因此在绝大多数需求为单条记录查询的时候，可以选择哈希索引，查询性能最快。其余场景，建议选择 BTree 索引，MySQL 的 BTree 索引使用的是 B+Tree。\n密集索引和稀疏索引的区别 密集索引文件中的每一个搜索码值都对应一个索引值。 稀疏索引文件只为索引码的某些值建立索引项。 InnoDB 存储引擎的必须仅有一个密集索引，其选取规则如下：\n若一个主键被定义，该主键则作为密集索引； 若没有主键被定义，该表的第一个唯一非空索引作为密集索引； 若不满足以上条件，InnoDB 内部会生成一个隐藏主键。 非主键索引存储相关键位和其对应的主键值，包含两次查找。\n如图：表 test.innodb 使用 InnoDB 存储引擎，其数据和索引存储在文件 innodb.ibd 中；表 test.myisam 使用 MyISAM 存储引擎，其数据文件保存在 myisam.MYD 中，索引文件保存在 myisam.MYI 中。\n如何定位并优化慢查询 SQL 根据慢日志定位慢查询 SQL； 使用 explain 等工具分析 SQL； 修改 SQL 或者尽量让 SQL 走索引。 慢查询相关配置\n使用 show variables like '%query%'; 查询当前数据库对慢查询的配置，主要有以下几个参数：\nVariable_name Value 描述 show_query_log OFF / ON 是否记录慢SQL slow_query_log_file /var/lib/mysql/*-slow.log 慢日志文件路径 long_query_time 10.0000 执行时间多长判定为慢 SQL 可以修改 MySQL 的配置文件 /etc/mysql/my.cnf，修改这些参数，使其永久有效。\n也可以通过设置全局变量的方式使其在 MySQL 服务重启前一直生效：\nset global long_query_time = 1; set global slow_query_log = on; 可以使用 show status like '%slow_queries%' 来查询当前会话的慢 SQL 数：\n在慢查询日志 /var/lib/mysql/manjaro-slow.log 中查看慢 SQL 。\n使用 explain 分析 SQL 执行计划：\nMySQL索引原理及慢查询优化\nexplain select name from person_info_large order by name desc; 主要需要关注的字段有：\nid：执行编号\ntype：访问类型\nextra：查询优化器对查询计划的补充信息\n联合索引的最作匹配原则 由多列组成的索引称为联合索引。\n有如下表结构：\nCREATE TABLE `person_info_large` ( `id` INT NOT NULL AUTO_INCREMENT, `account` VARCHAR ( 10 ) DEFAULT NULL, `name` VARCHAR ( 20 ) DEFAULT NULL, `area` VARCHAR ( 20 ) DEFAULT NULL, `title` VARCHAR ( 20 ) DEFAULT NULL, `motto` VARCHAR ( 50 ) DEFAULT NULL, PRIMARY KEY ( `id` ), UNIQUE KEY `account` ( `account` ), KEY `index_area_title` ( `area`, `title` ) ) ENGINE = INNODB AUTO_INCREMENT = 196606 DEFAULT CHARSET = utf8; 索引 index_area_title 是对 area 和 title 两个字段建立的联合索引。考虑以下查询语句：\nEXPLAIN SELECT * FROM person_info_large WHERE area = \u0026#39;nTtqWvRzVvuuYXBk5AFC\u0026#39; AND title = \u0026#39;lLrm20htGVVqRqJDWBU0\u0026#39;; EXPLAIN SELECT * FROM person_info_large WHERE area = \u0026#39;nTtqWvRzVvuuYXBk5AFC\u0026#39;; EXPLAIN SELECT * FROM person_info_large WHERE title = \u0026#39;lLrm20htGVVqRqJDWBU0\u0026#39;; 前两个查询的分析结果为：\n最后一个查询的分析结果为：\n最左前缀匹配原则是指：\nMySQL 会一直向右匹配直到遇到范围查询（\u0026gt; 、\u0026lt;、 between、 like）就停止匹配。比如 a = 3 and b = 4 and c \u0026gt; 5 and d = 6，如果建立 (a, b, c, d)顺序的索引，则 d 是用不到索引的，如果建立 (a, b, d, c) 的索引则都可以用到，且 a b d 的顺序可以任意调整。 = 和 in 可以乱序，比如 a = 1 and b = 2 and c = 3 建立(a, b, c) 索引可以任意顺序，MySQL 的查询优化器会优化成索引可以识别的形式。 最左匹配原则的成因：如图，创建联合索引时，数据库依据联合索引最左的字段来构建 B+Tree，即会按索引字段顺序对数据排序。比如 a 的值是有序的： 1,1,2,2,3,3 ，B 的值是无序的。当 a 值相等时，b 的值是有序的。所以最左匹配原则遇上范围查询就会停止，因为在一个范围中，剩下的字段不是有序的，剩下的字段都无法使用索引。\n索引是建立得越多越好吗 数据量小的表不需要建立索引，建立索引会带来额外的开销。 数据变化时需要维护索引，因此更多的索引意味着更多的维护成本（参照同理）。 更多的索引意味着需要更多的空间。 锁相关内容 MySQL MyISAM 和 InnoDB 存储引擎 查看存储引擎相关的命令：\n查看 MySQL 提供的所有存储引擎\nshow engines; 查看默认的存储引擎\nshow variables like \u0026#39;%storage_engine%\u0026#39;; 查看表相关内容\nshow table status like \u0026#39;shop_info_small\u0026#39;; MyISAM 和 InnoDB 的区别\nMySQL 5.5 之前使用 MyISAM 作为默认的数据库存储引擎，MyISAM 提供了全文索引、压缩和空间函数等特性，但是不支持事务和行级锁。MySQL 5.5 版本时引入 InnoDB 作为其默认的存储引擎。主要的区别有：\n是否支持行级锁：MyISAM 只有表级锁，而 InnoDB 默认为行级锁，支持表表级锁。 对事务和安全恢复的支持：MyISAM 强调性能，故不提供事务支持。InnoDB 支持事务，外部键等功能，具有事务、回滚和崩溃修复能力。 是否支持外键：MyISAM 不支持外键，InnoDB 支持。 MyISAM 与 InnoDB 关于锁方面的区别 MyISAM 默认是表级锁，不支持行级锁。\nInnoDB 默认是行级锁，也支持行级锁。\n表级锁: MySQL 中锁定粒度最大的一种锁,对当前操作的整张表加锁,实现简单,资源消耗也比较少,加锁快,不会出现死锁。其锁定粒度最大,触发锁冲突的概率最高,并发度最低, MyISAM 和 InnoDB 引擎都支持表级锁。\n行级锁: MySQL 中锁定粒度最小的一种锁,只针对当前操作的行进行加锁。 行级锁能大大减少数据库操作的冲突。其加锁粒度最小,并发度高,但加锁的开销也最大,加锁慢,会出现死锁。\n手动锁定表：\nselect * from table_name for update; lock tables table_name read | write; unlock tables; MyISAM 适合的场景\n频繁执行全表 count 语句。 对数据进行增删改的频率不高，查询非常频繁。 不需要事务。 InnoDB 使合的场景\n数据增删改查都相当频繁。 可靠性要求比较高，需要使用事务。 数据库锁的分类\n按锁的粒度划分，可分为表级锁、行级锁、页级锁 按锁级别划分，可分为共享锁、排它锁 按加锁方式划分，可分为自动锁、显示锁 按操作划分，可分为 DML 锁，DDL 锁 按使用方式划分，可分为乐观锁、悲观锁 数据事务的四大特性 原子性（Atomicity）：事务是最小的执行单位,不允许分割。事务的原子性确保动作要么全部 完成,要么完全不起作用;\n一致性（Consistency）：执行事务前后,数据保持一致，多个事务对同一个数据读取的结果是 相同的;\n隔离性（Isolation）：并发访问数据库时，一个用户的事务不被其他事务所干扰,各并发事务 之间数据库是独立的;\n持久性（Durability）：一个事务被提交之后。它对数据库中数据的改变是持久的,即使数据 库发生故障也不应该对其有任何影响。\n事务并发访问引起的问题 更新丢失（Lost to modify，MySQL 所有事务隔离级别均已避免此类问题） 脏读（Dirty read）：一个事务读到另一个事务未提交的事务。 不可重复读（Unrepeatable-read）：在一个事务下多次读取同一记录得到的数据不一致。 幻读（Phantom read）：事务 A 读取了多行，事务 B 在事务 A 未完成前对 A 读取范围内的数据做增删操作，导致事务 A 多次读取的记录数不一致的情况。 REPEATABLE READ 如何避免幻读 当前读和快照读\n当前读：加了锁的 SQL 语句，读取的是记录的最新版本，且其它事务无法修改记录。\nselect ... lock in share mode; select ... from update; update delete insert 快照读：不加锁的非阻塞读，基于 MVCC 实现。\nnext-key 锁（行锁 + Gap 锁）\n行锁\nGap 锁：如果 where 条件全部命中，则不会加 Gap 锁。\nGap 锁会用在非唯一索引和无索引的当前读中。\n","permalink":"https://2d992714.xianglin2020-github-io.pages.dev/posts/2020/mysql-basics-1/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"https://cdn.jsdelivr.net/gh/xianglin2020/gallery/202101/%E5%85%B3%E7%B3%BB%E5%9E%8B%E6%95%B0%E6%8D%AE%E5%BA%93.png\" alt=\"关系型数据库\"  /\u003e\n\u003c/p\u003e\n\u003ch2 id=\"数据库模块划分\"\u003e数据库模块划分\u003c/h2\u003e\n\u003ch2 id=\"关键语法\"\u003e关键语法\u003c/h2\u003e\n\u003ch3 id=\"group-by\"\u003eGROUP BY\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e满足 SELECT 子句中的列名必须为分组列或列函数\u003c/li\u003e\n\u003cli\u003e列函数对于 GROUP BY 子句定义的每一个组各返回一个结果\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"having\"\u003eHAVING\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e通常与 GROUP BY 子句一起使用\u003c/li\u003e\n\u003cli\u003eWHERE 过滤行，HAVING 过滤组\u003c/li\u003e\n\u003cli\u003e关键字的顺序：WHERE \u0026ndash; GROUP BY \u0026ndash; HAVING\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"索引相关内容\"\u003e索引相关内容\u003c/h2\u003e\n\u003ch3 id=\"为什么要使用索引\"\u003e为什么要使用索引\u003c/h3\u003e\n\u003cp\u003e全表扫描适用范围窄，借助字典的思想，使用索引可以加快查询速度。\u003c/p\u003e","title":"MySQL 数据库知识"},{"content":" 网络基础知识 国际标准化组织 ISO 提出的开放系统互联基本参考模型（Open Systems Interconnection Reference Model），简称为 OSI ，是一个概念模型。TCP/IP 是一个四层的体系结构，它包含应用层、 运输层、网际层和网络接口层。在阐述计算机网络原理时，通常综合 OSI 和 TCP/IP 的优点，采用一种只有五层协议的体系结构。\n应用层（application layer） 应用层的作用是通过应用进程间的交互来完成特定网络应用。应用层协议定义的是应用进程（主机中正在运行的程序）间通信和交互的规则。如域名系统 DNS、支持万维网的 HTTP 协议和支持电子邮件的 SMTP 协议。应用层交互的数据单元称为报文（message）。\n运输层（transport layer） 运输层的任务就是负责向两台主机中进程间的通信提供通用的数据传输服务。应用进程利用该服务传送应用层报文。运输层具有复用和分用两个功能。复用是指多个应用层进程可以同时使用下面运输层的服务，分用是指运输层把收到的信息分别交付给上面应用层中的相应进程。运输层主要使用 TCP 和 UDP 两种协议。\n传输控制协议 TCP（Transmission Control Protocol）：提供面向连接的、可靠的数据传输服务，其数据传输的单位是报文段。 用户数据报协议 UDP（User Datagram Protocol）：提供无连接的、尽最大努力交付（不保证数据传输可靠性）的数据传输服务，其数据传输的单位是用户数据报。 网络层（network layer） 网络层主要有两个任务：\n为分组交换组网上的不同主机提供通信服务，网络层把运输层产生的报文段或用户数据报封装成分组或包进行传送。 选择合适的路由，使源主机运输层所传下来的分组，能够通过网络中的路由器找到目的主机。 数据链路层（data link layer） 两台主机之间的数据传输，总是在一段一段的数据链路上传送的，这就需要专门的链路层协议。在两个相邻节点传送数据时，数据链路层将网络层交下来的 IP 数据报组装成帧，在两个相邻节点间的链路上传递帧。每一帧包括数据和必要的控制信息（如同步信息、地址信息、差错控制等）。\n物理层（physical layer） 在物理层上传输的单位是比特。物理层需要考虑多大的电压表示 “1” 或 “0” ，以及接收方如何识别发送方所发送的比特。物理层还要确定连接电缆的插头应该有多少根引脚以及各引脚应如何连接。\nTCP/IP 协议族 TCP/IP 的体系结构只有四层。\n可以分层画出具体的协议来表示 TCP/IP 协议族，它的特点是上下两头大而中间小：应用层和网络接口层都有多种协议，而中间的 IP 层很小，上层的各种协议都向下汇聚到一个 IP 协议中。这表明：TCP/IP 协议可以为各式各样的应用提供服务，同时 TCP/IP 协议也允许 IP 协议在各式各样的网络构成的互联网上运行。\nTCP 基础知识 TCP 的主要特点 TCP 是面向连接的运输层协议。应用程序在使用 TCP 协议之前，必须建立 TCP 连接。在传送完数据之后，必须释放已经建立的 TCP 连接。 每一条 TCP 连接只能有两个端点，每一条 TCP 连接只能是点对点的。 TCP 连接提供可靠交付的服务。通过 TCP 连接传送的数据，无差错、不丢失、不重复，并且按序到达。 TCP 提供全双工通信。TCP 允许通信双方的应用进程在任何时候都能发送数据。 面向字节流。TCP 中的 流（stream）指的是流入到进程或从进程流出的字节序列。 TCP 报文段的首部格式 TCP 报文段的前 20 个字节是固定的，后面有 4n 字节是根据需要而增加的选项。因此 TCP 首部的最小长度是 20 字节。\n源端口和目的端口：各占 2 个字节，分别写入源端口号和目的端口号。 序号：占 4 个字节。序号范围是 [0, 2^32 -1] ，且使用 mod 2^32 运算。TCP 是面向字节流的，在一个 TCP 连接中传送的字节流中的每一个字节都按顺序编号。整个要传送的字节流的起始序号必须在建立连接时设置。首部中的序号字段值则指的是本报文段所发送的数据的第一个字节序号。 确认号：占 4 个字节，是期望收到对方下一个报文段的第一个数据字节的编号。例如，B 正确收到了 A 发送的一个报文段，其序号字段值是 501，而数据长度是 200 字节，这表明 B 正确收到了 A 发送的到序号 700 为止的数据。因此，B 期望收到 A 的下一个数据序号是 701，于是 B 在发给 A 的确认报文段中把确认号设置为 701 。 数据偏移：占 4 位，指出 TCP 报文段的首部长度。“数据偏移”的单位是 32 位字（4 字节），4 位二进制最大表示十进制 15，因此“数据偏移”字段的最大值是 60 字节，这同时是 TCP 首部的最大长度。 保留：占 6 位，目前应置为 0 。 六个标志位： 紧急 URG ：当 URG = 1 时，表明紧急字段有效。发送应用进程告诉系统 TCP 有紧急数据要传送，于是 TCP 就把紧急数据插入本报文段数据的最前面，而后面的数据仍是普通数据。这时需要与首部中紧急指针字段配合使用。 确认 ACK ： 仅当 ACK = 1 时确认号字段才有效。当 ACK = 0 时，确认号无效。TCP 规定，在建立连接后所有传送的报文段都必须把 ACK 置 1 。 推送 PSH ：发送方 TCP 把 PSH 置 1，并理立即创建一个报文段发送出去。接收方 TCP 收到 PSH = 1 的报文段，就尽快地交付接收应用程序。 复位 RST ：当 RST = 1 时，表明 TCP 连接中出现严重差错，必须释放连接，然后再重新建立运输连接。 同步 SYN ：在连接建立时用来同步序号。当 SYN = 1 而 ACK = 0 时，表明这是一个连接请求报文段。对方若同意建立连接，则在相应的报文段中使用 SYN = 1 和 ACK = 1 。因此，SYN 置为 1 就表示这是一个请求或接收建立连接报文。 终止 FIN ：用来释放一个连接。当 FIN = 1 时，表明此报文段的发送方的数据已发送完毕，并要求释放运输连接。 窗口：占 2 个字节。窗口值是 [0, 2^16 -1] 之间的整数。窗口指发送本报文段的一端的接收窗口。窗口值作为接收方让发送方设置其发送窗口的依据。 校验和：占 2 个字节。校验和字段校验的范围包括首部和数据这两部分。同样需要伪首部。 紧急指针：占 2 个字节。仅在 URG = 1 时才有意义。即使窗口为零时也可以发送紧急数据。 选项：长度可变，最长可达 40 字节。 TCP 可靠传输的工作原理 TCP 所发送的报文段是交给 IP 层传送的，但是 IP 层只能提供最大努力服务，也就是说，TCP 下面的网络所提供的是不可靠的传输。TCP 必须采用适当的措施才能使得两个运输层之间的通信变得可靠。\n停止等待协议 “停止等待协议”就是每发送完一个分组就停止发送，等待对方的确认。在收到确认后再发送下一个分组。\n无差错情况\nA 发送分组 M1，发送完后暂停发送，等待 B 的确认。B 收到了 M1 就向 A 发送确认。A 在收到对 M1 的确认后，在发送下一个分组 M2 。同样，在收到 B 对 M2 的确认后，再发送 M3 。\n出现差错\n分组在传输过程中出现差错，B 在接收 M1 时检测到差错，就丢弃 M1，其它什么也不做（也不通知 A 收到有差错的分组）。也可能是 M1 在传输过程中丢失了，这时 B 什么都不知道。可靠传输协议中超时重传的设计是：A 只要超过一段时间仍然没有收到确认，就会认为刚才发送的分组丢失了，因而重传前面发送的分组。\n确认丢失和确认迟到\nA 发送分组后等待超时就会重传分组，B 在接受到重复分组时：丢弃重复的分组，对分组进行确认。A 在收到重复的确认时：收下后就丢弃。\n连续 ARQ 协议 发送方维持一个发送窗口，它的意义是：位于发送窗口的 5 个分组都可以连续发送出去，而不需要等待对方的确认。可以提高信道利用率。发送方每收到一个确认，就把发送窗口向前滑动一个分组的位置。\n接收方一般都是采用累积确认的方式。不必对接收到的分组逐个发送确认，而是在收到几个分组后，对按顺序到达的最后一个分组发送确认，表示到这个分组为止的所有分组都已经正确接收了。\nTCP 可靠传输的实现 以字节为单位的滑动窗口\n超时重传时间的选择\n选择确认 SACK\nTCP 滑动窗口协议 TCP 的滑动窗口是以字节为单位的。\n发送方的滑动窗口 构建发送窗口\n现假定 A 收到了 B 发来的确认报文段，其窗口值是 20 字节，确认号是 31 。根据这两个数据，A 可以构造出自己的发送窗口。\n发送窗口表示：在没有收到 B 的确认的情况下，A 可以连续把窗口内的数据都发送出去。凡是已经发送过的数据，在未收到确认之前都必须暂时保留，以便在超时重传时使用。\n发送窗口后沿的后面部分表示已经发送且已收到确认。这些数据不需要再保留。而发送窗口前沿的前面部分表示不允许发送，因为接收方都没有为这部分数据保留临时存放的缓冲区。\n部分发送但未收到确认\n假定 A 发送了序号为 31 ~ 41 的数据。这时，发送窗口位置未改变，但发送窗口内靠后面有 11 个字节表示已发送但未收到确认。发送窗口内靠前面的 9 个字节是允许发送但尚未发送的。\n收到确认后发送窗口向前滑动\n发送方 A 在收到 B 的确认号 33 后，就可以把发送窗口向前滑动 3 个序号，此时可发送的序号范围是 42 ~ 53 。\n接收方的滑动窗口 接收窗口的状态有三种：已经接收且已回复、允许接收但未收到、不允许接收。\n接收方接收窗口的初始状态 B 的接收窗口大小是 20 ，在接收窗口外面，到 30 号为止的数据是已经发送过确认且上交给主机的。接收窗口内的序号 31 ~ 50 是允许接收的。\nB 只能对按顺序收到的数据中的最高序号给予确认，因此 B 即使收到了序号为 32、33 的数据，因为序号为 31 的数据未收到，此时 B 对 A 的确认报文中的确认号仍然是 31 。\nB 接收到序号连续的数据\n当 B 接收到序号为 31 的数据，并把序号为 31 ~ 33 的数据交付主机，然后 B 删除这些数据。接着把接收窗口向前移动 3 个序号，同时给 A 发送确认，其中窗口值仍是 20 ，但确认号是 34 。\n窗口与缓存的关系 TCP 的运输连接管理 TCP 是面向链接的协议。运输连接是用来传送 TCP 报文的。TCP 运输连接的建立和释放是每一次面向连接的通信中必不可少的过程。因此，运输连接就有三个阶段，即：建立连接、数据传输和释放连接。在 TCP 连接建立过程中要解决以下三个问题：\n要使每一方能够明知对方的存在。 要允许双方协商一些参数。 能够对运输实体资源进行分配。 TCP 连接的建立采用客户端服务器方式。主动发起连接建立的应用进程叫做客户端，而被动等待连接建立的应用进程叫做服务器。\nTCP 的三次握手 TCP 建立连接的过程叫做握手，握手需要在客户端和服务器之间交换三个 TCP 报文段。\n最初两端的 TCP 进程都处于 CLOSED 状态。一开始，B 的 TCP 服务器进程先创建传输控制块 TCB，准备接受客户进程的连接请求。然后服务器就处于 LISTEN 状态，等待客户的连接请求。如有，即作出相应。A 的 TCP 客户进程也是首先创建传输控制模块 TCB。\n在打算建立 TCP 连接时，A 向 B 发出连接请求报文段，这时首部中的同步位 SYN = 1，同时选择一个初始序号 seq = x 。TCP规定，SYN 报文段不能携带数据，但要消耗一个序号。这时，TCP 客户端进程进入 SYN-SENT 状态。\nB 在接受到连接请求报文段后，如果同意建立连接，则向 A 发送确认。在确认报文段中应把 SYN 位和 ACK 位都置 1，确认号是 ack = x + 1，同时也为自己选择一个初始序号 seq = y。这个报文段也不能携带数据，但同样需要消耗掉一个序号。这时 TCP 服务器进程进入 SYN-RCVD 状态。\nTCP 客户端进程收到 B 的确认后，还要向 B 给出确认。确认报文段的 ACK 置 1 。确认号 ack = y + 1，而自己的序号 seq = x + 1 。TCP 的标准规定， ACK 报文段可以携带数据。但如果不携带数据则不消耗序号，下一个数据报文段的序号仍是 seq = x + 1 。这时，TCP 连接已经建立，A 进入 ESTABLISHED 状态。\n当 B 收到 A 的确认后，也进入 ESTABLISHED 状态。\n为什么客户端 TCP 最后还要发送一次确认 主要是为了防止已失效的连接请求报文段突然又传到了服务器 TCP，因而产生错误。即 A 发出的第一个连接请求报文段并没有丢失，而是在某些网络节点长时间滞留了，以至延误到连接释放以后的某个时间才来到 B。本来这是一个早就失效的报文段。但 B 收到此失效的连接请求报文段后，就误认为是 A 又发出一次新的连接请求。于是就向 A 发出确认报文段，同意建立连接。假定不采用报文握手，那么只要 B 发出确认，新的连接就建立了。但是实际上 A 并没有发出建立连接的请求，因此不会理睬 B 的确认报文，也不会向 B 发送数据。导致 B 一直等待，浪费资源。\nTCP 的四次挥手 数据传输结束后，通信双方都可以释放连接。现在 A 和 B 都处于 ESTABLISHED 状态。\nA 的应用进程先向其 TCP 进程发出连接释放报文段，再停止发送数据，主动关闭 TCP 连接。\nA 把连接释放报文段首部的终止控制位 FIN 置 1 ， 其序号 seq = u 。这时 A 进入 FIN-WAIT1 状态，等待 B 的确认。TCP 规定，FIN 报文段即使不携带数据，也要消耗一个序号。\nB 收到连接释放报文段后即发出确认，确认号 ack = u + 1，而确认报文段自己的序号时 seq = v 。然后 B 就进入 CLOSE-WAIT 状态。TCP 服务器进程这时应通知高层应用进程，因而从 A 到 B 这个方向的连接就释放了，这时 TCP 连接处于半关闭状态，即使 A 已经没有数据要发送来了，但 B 若发送数据，A 仍要接收，即从 B 到 A 这个方向的连接并未关闭。\nA 收到来自 B 的确认后，就进入 FIN-WAIT2 状态.等待 B 发出连接释放报文段。\n若 B 已经没有要向 A 发送的数据，其应用进程就通知 TCP 释放连接。这时 B 发出的连接释放报文段把 FIN 置为 1 。假定 B 的序号为 seq = w，B 还必须重复上次已发送过的确认号 ack = u + 1 。这时 B 就进入 LAST-ACK 状态，等待 A 的确认。\nA 在收到 B 的连接释放报文段后，必须对此发出确认。在确认报文段中把 ACK 置 1，确认号 ack = w + 1，而自己的序号是 seq = u + 1 。然后进入到 TIME-WAIT 状态，现在 TCP 连接还没有释放掉，必须经过时间等待计时器设置的时间 2MSL后，A 才进入到CLOSED 状态。因此，从 A 进入到 TIME-WAIT 状态后，要经过 2MSL 时间才能进入到 CLOSED 状态，才能开始建立下一个新的连接。当 A 撤销相应的 TCB 后，就结束了这次的 TCP 连接。\nB 在收到 A 的确认后进入 CLOSED 状态。\n时间 MSL 叫做最长报文段寿命，TCP 建议设为 2 分钟。Linux 服务器默认设置为 60 秒。\n# xianglin @ manjaro in ~ [15:24:57] $ cat /proc/sys/net/ipv4/tcp_fin_timeout 60 为什么在 TIME-WAIT 状态要等待 2MSL 的时间 为了保证 A 发送的最后一个确认释放连接的 ACK 报文能到达 B。这个报文可能丢失，因而处于 LAST-ACK 状态的 B 收不到对已发送的 FIN + ACK 报文段的确认会重传这个 FIN + ACK 报文段，而 A 就能在 2MSL 时间内收到这个重传的 FIN + ACK 报文段。A 重新确认，重新启动 2MSL 计时器。 A 在发送完最后一个 ACK 报文段后，再经过时间 2MSL，就可以使本连接持续时间内所产生的报文段都从网络中消失。这样就避免了下一次新的连接中不会出现这种旧的连接请求报文段。 查看 Linux 服务器下 TCP 连接状态 ➜ ~ netstat -n | awk \u0026#39;/^tcp/{++S[$NF]}END{for(a in S) print a,S[a]}\u0026#39; ESTABLISHED 17 FIN_WAIT1 1 TIME_WAIT 2 UDP 基础知识 UDP 的主要特点 UDP 是无连接的，即发送数据之前不需要建立连接，因此减少了开销和发送数据之前的时延。 UDP 使用尽最大努力交付，即不保证可靠交付，因此主机不需要维持复杂的连接状态。 UDP 是面向报文的。UDP 对应用层交下来的报文，既不合并，也不拆分。在添加首部后就向下交付给 IP 层。 UDP 没有拥塞控制，网络出现拥塞不会使源主机的发送速率降低。适用于 IP 电话、实时视频会议、视频直播等场景。 UDP 支持一对一、一对多、多对多的交互通信。 UDP 的首部开销小，只有 8 个字节。 UDP 的首部格式 用户数据报 UDP 有两个字段：数据字段和首部字段。首部字段占用 8 个字节，由四个字段组成。\n源端口：源端口号，在需要对方回信时选用，不需要时可用全 0 。 目的端口：目的端口号，在终点交付报文时必须使用。 长度：UDP 用户数据报的长度，其值最小是 8 （仅有首部）。 校验和：检测 UDP 用户数据报在传输中是否有错，有错就丢弃。 伪首部用于 UDP 计算校验和，既不向下传送也不向上递交\nHTTP 协议 HTTP 协议定义了浏览器怎样向万维网服务器请求万维网文档，以及服务器怎样把文档传送给浏览器。HTTP 是面向事务的应用层协议，它是万维网上能够可靠地交换文件的重要基础。HTTP 使用了面向连接的 TCP 作为运输层协议，保证了数据的可靠传输。HTTP 不必考虑数据在传输过程中被丢弃后又怎样重传的问题。但是，HTTP 协议本身是无连接的，HTTP 协议是无状态的。\nHTTP/1.1 协议的持续连接有两种工作方式。即非流水线方式和流水线方式。非流水线方式是指客户端在收到前一个相应后才能发出下一请求。流水线方式是指客户端在收到 HTTP 的相应报文之前就能够接着发送新的请求报文。\nHTTP 报文格式 常见状态码 类别 原因短语 1XX Information（信息性状态码） 接收的请求正在处理 2XX Success（成功状态码） 请求正常处理完毕 3XX Redirection（重定向状态码） 需要进行附加操作以完成请求 4XX Client Error（客户端错误状态码） 服务器无法处理请求 5XX Server Error（服务器错误状态码） 服务器无法处理请求 2XX 成功 200 OK：表示从客户端发来的请求在服务器被正常处理了。 204 No Content：表示服务器接收的请求已被成功处理，但在返回的响应报文中不含实体的主体部分。 206 Partial Content：表示客户端进行了范围请求，而服务器成功执行了这部分的 GET 请求。响应报文中包含由 Content-Range 指定范围的实体内容。 3XX 301 moved Permanently：永久性重定向，表示资源已被分配新的 URI。 302 Found：临时性重定向。 304 Not Modified：服务器资源未改变，可以直接使用客户端未过期的缓存。 4XX 400 Bad Request：表示请求报文中存在语法错误。 401 Unauthorized：表示发送的请求需要通过有 HTTP 认证的认证信息。 403 Forbidden：表示对请求资源的访问被服务器拒绝了。 404 Not Found：表示服务器上无法找到请求的资源。 5XX 500 Internal Server Error：表示服务端在执行请求时发生了错误。 503 Service Unavailable：表明服务器暂时处于超负载或正在进行停机维护，现在无法处理请求。 GET 请求与 POST 请求的区别 从 HTTP 报文层面：GET 请求将信息附加在 URL 上，POST 请求一般是将请求参数放在报文体中。 数据库层面：GET 请求符合幂等性和安全性。 GET 请求可以被缓存、被存储，POST 请求不行。 GET 和 POST 到底有什么区别？ - 大宽宽的回答 - 知乎 https://www.zhihu.com/question/28586791/answer/767316172\nCookis 和 Session 的区别 HTTP 是无状态的协议。\nCookies：\n是由服务器发给客户端的特殊信息，以文本的形式存放在客户端。 客户端再次请求的时候，会把 Cookies 回发。 服务器接收到请求后会解析 Cookies 相关的内容。 Session：\n服务端的机制，在服务器上保存会话信息。 解析客户端请求并操作 sessionId，按需保存状态信息。 Session 的实现方式\n使用 Cookies 来实现：JSESSIONID。 使用 URL 回写来实现。 主要区别\nCookies 数据存放在客户端的浏览器上，Session 数据放在服务器上 Session 相对于 Cookies 更安全 应当使用 Cookies 来减轻服务器的开销 HTTP 和 HTTPS 协议的区别 SSL（Security Sockets Layer）\n为网络通信提供安全及数据完整性的一种安全协议。 是操作系统对外的 API，SSL3.0 之后更名为 TLS。 采用身份验证和数据加密保证网络通信的安全和数据完整性。 HTTPS 数据传输流程\n浏览器将支持的加密算法信息发送给服务器； 服务器选择一套浏览器支持的加密算法，以证书的形式回发给浏览器； 浏览器验证证书合法性，并结合证书公钥加密信息并发送给服务器； 服务器使用私钥解密信息，验证哈希，加密响应消息回发给浏览器； 浏览器解密响应消息，并对消息进行验真，之后进行加密交互数据。 HTTPS 和 HTTP 协议的区别\nHTTPS 需要到 CA 申请证书。 HTTPS 密文传输，HTTP 明文传输。 连接方式不同，HTTPS 默认使用 443 端口，HTTP 使用 80 端口。 HTTPS = HTTP + 加密 + 认证 + 完整性保护。 HTTPS 存在的问题：浏览器默认填充 http://，请求需要进行跳转，有被劫持的风险\n浏览器地址栏键入 URL，按下回车之后经历的流程 DNS 解析 TCP 连接 发送 HTTP 请求 服务器处理请求并返回 HTTP 报文 浏览器解析并渲染页面 关闭连接 Socket Socket 是对 TCP/IP 协议的抽象，是操作系统对外开放的接口。\nSocket 通信过程 Java 使用 Socket 通信 使用 Socket 编写网络应用程序，分别用 TCP 和 UDP 的方式实现。\n使用 TCP 实现\n/** * 使用 Sockets 实现 Tcp服务端 */ public class TcpServer { public static void main(String[] args) { try { // 创建一个Sockets服务，并绑定在本机的9509端口上 ServerSocket serverSocket = new ServerSocket(9509); // 循环监听客户端链接 while (true) { // 有客户端链接才返回 Socket socket = serverSocket.accept(); // 新建一个服务线程处理任务 new TcpService(socket).start(); } } catch (IOException e) { e.printStackTrace(); } } } class TcpService extends Thread { private final Socket socket; TcpService(Socket socket) { this.socket = socket; } @Override public void run() { try { // 获取socket对应的输入流和输出流 InputStream inputStream = this.socket.getInputStream(); OutputStream outputStream = this.socket.getOutputStream(); // 读取客户端发送的数据 byte[] bytes = new byte[1024]; int read = inputStream.read(bytes); String content = new String(bytes, 0, read); System.out.println(\u0026#34;收到客户端信息：\u0026#34; + content); // 返回字符串长度 outputStream.write(String.valueOf(content.length()).getBytes(StandardCharsets.UTF_8)); // 关闭连接资源 outputStream.close(); inputStream.close(); this.socket.close(); } catch (IOException e) { e.printStackTrace(); } } } /** * 使用Sockets 完成Tcp客户端 */ public class TcpClient { public static void main(String[] args) { try { // 创建Socket对象，指定ip地址和端口 Socket socket = new Socket(\u0026#34;127.0.0.1\u0026#34;, 9509); // 使用输出流向服务端发送数据 OutputStream outputStream = socket.getOutputStream(); outputStream.write(\u0026#34;Hello World!\u0026#34;.getBytes(StandardCharsets.UTF_8)); // 使用输入流接收服务端返回的数据 InputStream inputStream = socket.getInputStream(); byte[] bytes = new byte[1024]; int read = inputStream.read(bytes); System.out.println(\u0026#34;收到服务端回应：\u0026#34; + new String(bytes, 0, read)); // 关闭连接资源 outputStream.close(); inputStream.close(); socket.close(); } catch (IOException e) { e.printStackTrace(); } } } 使用 UDP 实现\n/** * 使用Socket实现UDP服务端 */ public class UdpServer { public static void main(String[] args) { try { // 为本机创建一个DatagramSocket对象，并绑定到9509端口 DatagramSocket datagramSocket = new DatagramSocket(9509); // 创建DatagramPacket对应接收客户端发送的数据 byte[] bytes = new byte[1024]; DatagramPacket datagramPacket = new DatagramPacket(bytes, bytes.length); // 将客户端发送的数据保存到DatagramPacket对象中 datagramSocket.receive(datagramPacket); // 打印内容 byte[] data = datagramPacket.getData(); String content = new String(data, 0, datagramPacket.getLength()); System.out.println(\u0026#34;接收客户端请求：\u0026#34; + content); // 创建DatagramPacket对象，封装需要发送给客户端的内容 byte[] sendBytes = String.valueOf(content.length()).getBytes(StandardCharsets.UTF_8); // 获取客户端的地址和端口 DatagramPacket datagramPacket1 = new DatagramPacket(sendBytes, sendBytes.length, datagramPacket.getAddress(), datagramPacket.getPort()); // 将数据发送给客户端 datagramSocket.send(datagramPacket1); datagramSocket.close(); } catch (IOException e) { e.printStackTrace(); } } } /** * 使用Socket实现UDP客户端 */ public class UdpClient { public static void main(String[] args) { try { // 客户端发送给服务端的内容 byte[] bytes = \u0026#34;Hello World!\u0026#34;.getBytes(StandardCharsets.UTF_8); // 获取本机对应的InetAddress对象 InetAddress localHost = InetAddress.getLocalHost(); // 创建DatagramSocket对象 DatagramSocket datagramSocket = new DatagramSocket(); // 将需要发送的内容封装成DatagramPacket对象，并指定服务端的ip和端口 DatagramPacket datagramPacket = new DatagramPacket(bytes, bytes.length, localHost, 9509); // 发送数据给服务端 datagramSocket.send(datagramPacket); // 接收服务端的返回 byte[] receive = new byte[1024]; // 创建DatagramPacket对象存储服务端返回的数据 DatagramPacket datagramPacket1 = new DatagramPacket(receive, receive.length); // 接收数据，并存储到DatagramPacket中 datagramSocket.receive(datagramPacket1); // 打印服务端返回的内容 System.out.println(\u0026#34;服务端返回：\u0026#34; + new String(datagramPacket1.getData(), 0, datagramPacket1.getLength())); datagramSocket.close(); } catch (IOException e) { e.printStackTrace(); } } } ","permalink":"https://2d992714.xianglin2020-github-io.pages.dev/posts/2020/network-knowledge-basics/","summary":"国际标准化组织 ISO 提出的开放系统互联基本参考模型（Open Systems Interconnection Reference Model），简称为 OSI ，是一个概念模型。","title":"网络相关知识总结"},{"content":"Zookeeper 理解 Zookeeper 为什么需要 Zookeeper 用起来像单机但是又比单机更可靠 类似于 Leader 在团队里面的协调作用 在多节点中尽量压缩数据同步时间 Zookeeper 是什么 Zookeeper 是开源的高性能的分布式应用协调系统，一个高性能的分布式数据一致性解决方案 它的设计目标是将那些复杂的容易出错的分布式一致性服务封装起来，构成一个高效可靠的原语集，并以一系列简单易用的接口提供给用户。 Zookeeper 的特点 顺序一致性：从同一客户端发起的事务请求，会严格的按照顺序被应用到 Zookeeper 服务器中 原子性：所有事务请求的处理结果在整个集群中所有机器上的应用情况是一致的 单一视图：无论连接哪一个 Zookeeper 节点，看到的服务端数据模型都是一致性的 可靠性：一旦一次更改请求被应用，更改的结果就会被持久化，直到被下一次更改覆盖 及时性 Zookeeper 与 CAP 的关系 CP：一致性+分区容错性 任何时候都能得到一致的数据结果，同时系统对网络具有容错性 它不能保证每次服务请求的可用性 Zookeeper 的作用 分布式服务注册与订阅 统一配置文件 生成分布式唯一 ID：通过 Zookeeper 的顺序节点生成全局唯一ID Master 节点选举 分布式锁：通过创建唯一节点获得分布式锁，当获取锁的一方执行完代码或挂掉后就释放锁 数据发布/订阅：通过 Watcher 机制可以很方便的实现数据的发布与订阅。当你将数据发布到 Zookeeper 被监听的节点上，其它机器可以通过监听 Zookeeper 上节点的变化来实现配置的 Zookeeper 的重要概念 数据模型 DataModel Zookeeper 数据模型采用层次化的多叉树结构，每个节点上可以存储数据（数字、字符串、二进制序列） 每个节点还可以用于多个子节点，最上层是根节点，用/表示 每个数据节点在 Zookeeper 中被称为 znode，它是 Zookeeper 中数据的最小单元 每个 znode 都有一个唯一的路径标识 数据节点 Znode znode 的四种类型 持久节点（PRESISTENT）：一旦创建就一直存在即是 Zookeeper 集群宕机，直到被删除 临时节点（EPHEMERAL）：临时节点与客户端会话（Session）绑定，会话消失则节点消失。临时节点只能做叶子节点，不能创建子节点 持久顺序节点（PRESISTENT_SEQUENTIAL）：子节点的名称具有顺序 临时顺序节点（EPHEMERAL_SEQUENTIAL）：子节点的名称具有顺序 Znode 的数据结构 每个 znode 由两部分组成\nstat：状态信息 data：节点存放的数据的具体内容 [zk: 127.0.0.1:2181(CONNECTED) 6] get /zk_node content [zk: 127.0.0.1:2181(CONNECTED) 7] stat /zookeeper # create ZXID 数据节点被创建时的事务 ID cZxid = 0x0 # create time 节点的创建时间 ctime = Thu Jan 01 08:00:00 CST 1970 # modified ZXID 节点最后一次更新时的事务 ID mZxid = 0x0 # modified time 节点最后一次更新时间 mtime = Thu Jan 01 08:00:00 CST 1970 # 该节点的子节点列表最后一次修改时的事务 ID，只有子节点列表变更才会更新 pZxid，子节点内容变更不会更新 pZxid = 0x0 # 子节点版本号，当前节点的子节点每次变化时值增加 1 cversion = -2 # 数据节点内容版本号，节点创建时为 0，每更新一次节点内容（无论数据有无变化）该版本号值增加 1 dataVersion = 0 # 节点的 ACL 版本号，表示该节点 ACL 信息变更次数 aclVersion = 0 # 创建该临时节点的会话 sessionId，如果当前节点为持久节点，则 ephemeralOwner = 0 ephemeralOwner = 0x0 # 数据节点内容长度 dataLength = 0 # 当前节点的子节点个数 numChildren = 2 权限控制 ACL Zookeeper 采用 ACL（AccessControlLists）策略进行权限控制，类似于 UNIX文件系统的权限控制\n对于 znode 操作的权限，Zookeeper 提供了一下 5 种：\nCREATE：能够创建子节点 READ：能回去节点数据和列出其子节点 WRITE：能设置/更新节点数据 DELETE：能删除子节点 ADMIN：能设置节点 ACL 的权限 对于身份认证，提供了一下几种方式：\nworld：默认方式，任何用户都可以无条件访问 auth：代表任何已认证的用户 digest：用户名:密码认证方式：username:password IP：对指定 IP 进行限制 Watcher 事件监听器 Watcher 事件监听器，是 Zookeeper 中一个很重要的特征。 Zookeeper 允许用户在指定的节点上注册一些 Watcher，并且在一些特定事件触发时，Zookeeper 服务端会将事件通知到感兴趣的客户端上去，该特性是 Zookeeper 实现分布式协调服务的重要特性。 使用场景：统一资源配置 Watcher 事件类型 NodeCreated NodeDeleted NodeDataChanged NodeChildrenChanged 会话 Session Session 可以看做是 Zookeeper 服务器与客户端之间的一个 TCP 长连接，客户端能够通过心跳检测与服务器保持有效的会话，也能够向 Zookeeper 服务器发送请求并接受响应，同时还能够通过该连接接受来自服务器的 Watcher 事件通知。 Session 有一个属性叫做 sessionTimeout，代表会话的超时时间。客户端断开连接后，只要在 sessionTimeout 规定的时间内能够重新连接上集群中任意一台服务器，那么之前创建的会话仍然有效。 在为客户端创建会话之前，服务端首先会为每个客户端都分配一个 sessionID，sessionID 是 Zookeeper 会话的一个重要标识，是全局唯一的。 Zookeeper 集群 每个 Server 代表一个运行 Zookeeper 服务的服务器。组成 Zookeeper 服务的服务器会在内存中维护当前的服务器状态，并且每台服务器之间都互相保持着通信。 集群间通过 ZAB 协议（Zookeeper Atomic BroadCast）来保持数据的一致性。 Zookeeper 集群角色 Zookeeper 没有选择传统的 Master/Slave 概念，而是引入了 Leader、Follower和 Observer 三种角色。\nZookeeper 集群中所有机器通过一个 Leader 选举过程来选定一台称为 Leader 的机器。 Leader 可以为客户端提供读写服务。除 Leader 外，Follower 和 Observer 都只能提供读服务。 Follower 和 Observer 的唯一区别在于 Observer 集群不参与 Leader 的选举过程，也不参与写操作的“过半写成功”策略。 当 Leader 服务器出现异常时，就会进入 Leader 选举过程，这个过程会选举产生新的 Leader 服务器。\nZookeeper 集群中服务器状态 LOOKING：寻找 Leader LEADING：Leader 状态，对应节点为 Leader FOLLOWING：Follower 状态，对应节点为 Follower OBSERVING：Observer 状态，对应节点为 Observer，该节点不参与 Leader 选举 Zookeeper 集群最好是基数台 Zookeeper 集群在宕机几个 Zookeeper 服务器后，如果剩下的 Zookeeper 服务器个数大于宕机个数的话整个 Zookeeper 才依然可用。 ZAB 协议和 Paxos 算法 ZAB 协议 ZAB（Zookeeper Atomic BroadCast 原子广播）协议是为分布式协调服务 Zookeeper 专门设计的一种支持崩溃恢复的原子广播协议。 在 Zookeeper 中，主要依赖 ZAB 协议来实现分布式数据一致性。基于该协议，Zookeeper 实现了一种主备模式的系统架构来保持集群中各个副本之间的数据一致性。 ZAB 协议的两种基本模式 崩溃恢复：当整个服务框架在启动过程中，或是当 Leader 服务器出现网络中断、崩溃退出与重启等异常情况时，ZAB 协议就会进入恢复模式并选举产生新的 Leader 服务器。当选举产生了新的 Leader 服务器，同时集群中已经有过半的机器与该 Leader 服务器完成了状态同步之后，ZAB 协议就会退出恢复模式。其中，状态同步是指数据同步，用来保证集群中存在过半的机器能够和 Leader 服务器的数据状态保持一致。 消息广播：当集群中已经有过半的 Follower 服务器完成了和 Leader 服务器的状态同步，那么整个服务框架就可以进入消息广播模式了。当一台同样遵守 ZAB 协议的服务器启动后加入到集群中时，如果此时集群中已经存在一个 Leader 服务器在负责进行消息广播，那么新加入的服务器就会自觉地进入数据恢复模式：找到 Leader 所在的服务器，并与其进行数据同步，然后一起参与到消息广播流程中去。 安装、配置 Zookeeper CentOS 下安装 Zookeeper 使用 wget 命令下载 Zookeeper\nwget https://mirrors.tuna.tsinghua.edu.cn/apache/zookeeper/zookeeper-3.6.2/apache-zookeeper-3.6.2-bin.tar.gz 解压使用\ntar -zxvf apache-zookeeper-3.6.2-bin.tar.gz 启动停止\n➜ bin ./zkServer.sh start /usr/bin/java ZooKeeper JMX enabled by default Using config: /root/zookeeper/apache-zookeeper-3.6.2-bin/bin/../conf/zoo.cfg Starting zookeeper ... STARTED ➜ bin ./zkServer.sh stop /usr/bin/java ZooKeeper JMX enabled by default Using config: /root/zookeeper/apache-zookeeper-3.6.2-bin/bin/../conf/zoo.cfg Stopping zookeeper ... STOPPED Zookeeper 的使用 Zookeeper 的常用命令 启动 Zookeeper\n➜ apache-zookeeper-3.6.2-bin bin/zkServer.sh start /usr/bin/java ZooKeeper JMX enabled by default Using config: /root/zookeeper/apache-zookeeper-3.6.2-bin/bin/../conf/zoo.cfg Starting zookeeper ... STARTED 连接到 Zookeeper\n➜ apache-zookeeper-3.6.2-bin bin/zkCli.sh -server 127.0.0.1:2181 /usr/bin/java Connecting to 127.0.0.1:2181 # .... WatchedEvent state:SyncConnected type:None path:null [zk: 127.0.0.1:2181(CONNECTED) 0] 查看节点ls\n[zk: 127.0.0.1:2181(CONNECTED) 1] ls / [zookeeper] [zk: 127.0.0.1:2181(CONNECTED) 2] ls /zookeeper [config, quota] 查看状态stat\n[zk: 127.0.0.1:2181(CONNECTED) 3] stat / cZxid = 0x0 ctime = Thu Jan 01 08:00:00 CST 1970 # 创建时间 mZxid = 0x0 mtime = Thu Jan 01 08:00:00 CST 1970 # 修改时间 pZxid = 0x0 cversion = -1 dataVersion = 0 aclVersion = 0 ephemeralOwner = 0x0 # 节点类型 dataLength = 0 numChildren = 1 # 子节点数量 创建、删除节点\n# 创建节点 [zk: 127.0.0.1:2181(CONNECTED) 11] create /xianglin 123 Created /xianglin # 创建顺序节点 [zk: 127.0.0.1:2181(CONNECTED) 0] create -s /xianglin/s Created /xianglin/s0000000000 [zk: 127.0.0.1:2181(CONNECTED) 1] create -s /xianglin/s Created /xianglin/s0000000001 # 创建临时节点 [zk: 127.0.0.1:2181(CONNECTED) 2] create -e /xianglin/tmp 123 Created /xianglin/tmp # 断开连接后重新连接 [zk: 127.0.0.1:2181(CONNECTED) 2] stat /xianglin/tmp Node does not exist: /xianglin/tmp # 修改节点 [zk: 127.0.0.1:2181(CONNECTED) 14] set /xianglin 456 # 乐观锁的使用 [zk: 127.0.0.1:2181(CONNECTED) 6] set -v 1 /xianglin 123 version No is not valid : /xianglin # 删除节点 [zk: 127.0.0.1:2181(CONNECTED) 22] ls /xianglin [s0000000001] [zk: 127.0.0.1:2181(CONNECTED) 23] delete /xianglin/s0000000001 [zk: 127.0.0.1:2181(CONNECTED) 24] delete /xianglin [zk: 127.0.0.1:2181(CONNECTED) 25] 查看节点数据和状态get\n[zk: 127.0.0.1:2181(CONNECTED) 12] get /xianglin 123 Java 使用 Zookeeper 使用原生的 JavaAPI 引入 Zookeeper 依赖\n\u0026lt;groupId\u0026gt;org.apache.zookeeper\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;zookeeper\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;3.6.2\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; 简单的使用\n原生 Java API 的缺点 不支持连接超时后的自动重连 Watcher 注册一次后会失效 不支持递归创建节点 使用 Apache Curator TODO ","permalink":"https://2d992714.xianglin2020-github-io.pages.dev/posts/2020/zookeeper-basics/","summary":"\u003ch1 id=\"zookeeper\"\u003eZookeeper\u003c/h1\u003e\n\u003ch2 id=\"理解-zookeeper\"\u003e理解 Zookeeper\u003c/h2\u003e\n\u003ch3 id=\"为什么需要-zookeeper\"\u003e为什么需要 Zookeeper\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e用起来像单机但是又比单机更可靠\u003c/li\u003e\n\u003cli\u003e类似于 Leader 在团队里面的协调作用\u003c/li\u003e\n\u003cli\u003e在多节点中尽量压缩数据同步时间\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"zookeeper-是什么\"\u003eZookeeper 是什么\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003eZookeeper 是开源的高性能的分布式应用协调系统，一个高性能的分布式数据一致性解决方案\u003c/li\u003e\n\u003cli\u003e它的设计目标是将那些复杂的容易出错的分布式一致性服务封装起来，构成一个高效可靠的原语集，并以一系列简单易用的接口提供给用户。\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"zookeeper-的特点\"\u003eZookeeper 的特点\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e顺序一致性：从同一客户端发起的事务请求，会严格的按照顺序被应用到 Zookeeper 服务器中\u003c/li\u003e\n\u003cli\u003e原子性：所有事务请求的处理结果在整个集群中所有机器上的应用情况是一致的\u003c/li\u003e\n\u003cli\u003e单一视图：无论连接哪一个 Zookeeper 节点，看到的服务端数据模型都是一致性的\u003c/li\u003e\n\u003cli\u003e可靠性：一旦一次更改请求被应用，更改的结果就会被持久化，直到被下一次更改覆盖\u003c/li\u003e\n\u003cli\u003e及时性\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"zookeeper-与-cap-的关系\"\u003eZookeeper 与 CAP 的关系\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003eCP：一致性+分区容错性\u003c/li\u003e\n\u003cli\u003e任何时候都能得到一致的数据结果，同时系统对网络具有容错性\u003c/li\u003e\n\u003cli\u003e它不能保证每次服务请求的可用性\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"zookeeper-的作用\"\u003eZookeeper 的作用\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e分布式服务注册与订阅\u003c/li\u003e\n\u003cli\u003e统一配置文件\u003c/li\u003e\n\u003cli\u003e生成分布式唯一 ID：通过 Zookeeper 的顺序节点生成全局唯一ID\u003c/li\u003e\n\u003cli\u003eMaster 节点选举\u003c/li\u003e\n\u003cli\u003e分布式锁：通过创建唯一节点获得分布式锁，当获取锁的一方执行完代码或挂掉后就释放锁\u003c/li\u003e\n\u003cli\u003e数据发布/订阅：通过 Watcher 机制可以很方便的实现数据的发布与订阅。当你将数据发布到 Zookeeper 被监听的节点上，其它机器可以通过监听 Zookeeper 上节点的变化来实现配置的\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"zookeeper-的重要概念\"\u003eZookeeper 的重要概念\u003c/h2\u003e\n\u003ch3 id=\"数据模型-datamodel\"\u003e数据模型 DataModel\u003c/h3\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"https://cdn.jsdelivr.net/gh/xianglin2020/gallery@master/202012/172917.png\" alt=\"image-20201213141229663\"  /\u003e\n\u003c/p\u003e","title":"Zookeeper 基础"},{"content":"微服务 微服务的基础概念 单体应用的不足 部署效率低下：依赖众多、应用体积较大、启动时间较长 团队协作开发成本高 系统可用性差 服务化 把传统的单机应用中的本地方法调用，改造成通过 RPC、HTTP 产生的远程方法调用 把模块从单体应用中拆分出来，独立成一个服务部署 微服务概念 一种架构风格 开发单个应用作为一系列小型服务的套件，其中每个服务都运行在自己的进程中，并且通过轻量级的机制实现彼此间的通信，这通常是 HTTP 资源 API 这些服务是围绕着业务功能构建的，并且可以通过完全自动化的部署机制进行独立部署 这些服务的集中化管理做到了最小化（例如 docker 相关技术），每一种服务都可以通过不同的编程语言进行编写，并且可以使用不同的数据存储技术 微服务的特点 组件以服务形式来提供 微服务是产品而不是项目 轻量级通信、独立进程 分散治理、去中心化治理 容错性设计 会带来团队组织架构的调整 微服务的优缺点 服务简单、便于学习和上手、相对易于维护\n独立部署、灵活扩展\n技术栈丰富\n运维成本过高\n接口可能不匹配\n代码可能重复\n架构复杂度提高\n微服的两大门派 Spring Cloud：众多子项目 dubbo：高性能、轻量级的开源 JavaRPC 框架，它提供了三大核心能力：面向接口的远程方法调用、智能容错和负载均衡、服务自动注册和发现 通信协议：RPC、HTTP RPC 的整体效率较高 服务提供方与调用方接口依赖方式太强 服务对平台敏感，难以简单复用 微服务重要模块 服务描述 注册中心 服务框架 负载均衡 熔断和降级 网关 Spring Cloud SpringCloud 是一个成熟的微服务框架，定位为开发人员提供工具，以快速构建分布式系统 Spring Cloud Netflix Netflix Eureka 服务中心，云端服务发现，一个基于 REST 的服务，用于定位服务。 Netflix Hystrix 熔断器，容错管理工具，旨在通过熔断机制控制服务和第三方库的节点，从而对延迟和故障提供更强大的容错能力。 Netflix Zuul Zuul 是提供动态路由、监控、弹性、安全等边缘服务的框架。 基础项目结构搭建 基础功能 实现一个课程列表和课程价格的接口服务\n创建两个项目：course-list和course-price\n实现两个接口：\n返回课程列表：/courses\n返回单个课程价格：/price\n表设计如下 CREATE TABLE `course` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT, `course_id` int(20) DEFAULT NULL COMMENT \u0026#39;课程 ID\u0026#39;, `course_name` varchar(50) DEFAULT NULL COMMENT \u0026#39;课程名\u0026#39;, `valid` int(1) DEFAULT NULL COMMENT \u0026#39;是否上架\u0026#39;, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; INSERT INTO `course` (`id`, `course_id`, `course_name`, `valid`) VALUES (1,362,\u0026#39;Java 并发编程精讲\u0026#39;,1), (2,409,\u0026#39;玩转 Java 并发工具\u0026#39;,1), (3,345,\u0026#39;Nginx 教程\u0026#39;,0); CREATE TABLE `course_price` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT, `course_id` int(50) DEFAULT NULL COMMENT \u0026#39;课程 ID\u0026#39;, `price` int(20) DEFAULT NULL COMMENT \u0026#39;课程价格\u0026#39;, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; INSERT INTO `course_price` (`id`, `course_id`, `price`) VALUES (1,362,348), (2,409,399); 项目搭建 搭建一个空的 springboot 项目spring-cloud-exercise-course，作为整个项目的 parent，删掉无用的src目录\n新建一个空的 maven 项目course-service作为course-list和course-price的parent，删掉无用的src目录。\n引入一些公共依赖，比如 jdbc、mybatis等。\n\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;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;mysql\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;mysql-connector-java\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.mybatis.spring.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;mybatis-spring-boot-starter\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;2.1.3\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.projectlombok\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;lombok\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.18.12\u0026lt;/version\u0026gt; \u0026lt;scope\u0026gt;provided\u0026lt;/scope\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;/dependencies\u0026gt; 新建course-list和course-price两个 module，开发/courses和/price接口。因为需要的依赖已经在course-service中引入，这两个 module 无需引入依赖。\n开发/courses接口\n开发/price接口\n简单测试这两个接口\n准备工作已经完成，基于 springboot 使用 MyBatis 的查询项目很简单。\n版本搭配 SpringBoot 与 SpringCloud 的版本选择，可以参见https://start.spring.io/actuator/info的返回\nSpringBoot 和 MyBatis 的版本选择，可以参见http://mybatis.org/spring-boot-starter/mybatis-spring-boot-autoconfigure/\n服务注册与发现 Eureka 服务中心又称为注册中心，管理各种服务功能包括服务的注册、发现、熔断、负载、降级等。 Eureka 介绍 Spring Cloud 封装了 Netflix 公司开发的 Eureka 模块来实现服务注册和发现。 Eureka 采用了 C-S 的设计架构，Eureka Server 作为服务注册的服务器，它是服务注册中心。 系统中的其它微服务，使用 Eureka 的客户端连接到 Eureka Server，并维持心跳连接。 系统维护人员可以通过 Eureka Server 来监控系统中各个微服务是否正常运行。 Spring Cloud 的其它模块可以通过 Eureka Server 来发现系统中的其它微服务，并执行性格逻辑。 如图所示： Eureka Server：提供服务注册于发现 Service Provider：服务提供方，将自身服务注册到 Eureka，使得消费者能到找到 Service Consumer：服务消费方，从 Eureka 获取注册服务列表，从而消费服务 使用 Eureka Server 基于上面的案例，course-price模块需要使用course-list模块提供的服务，可以启动一个 Eureka Server，将两个模块作为 Eureka Client 注册到服务中心，course-list作为服务提供者，course-price作为服务消费者。 配置 EurekaServer 新建一个模块eureka-server 作为注册中心\n引入 Eureka Server 的依赖\n首先在整个项目的顶层 POM文件中加入对 Spring Cloud 版本的定义\n\u0026lt;dependencyManagement\u0026gt; \u0026lt;dependencies\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.cloud\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-cloud-dependencies\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;Hoxton.SR9\u0026lt;/version\u0026gt; \u0026lt;type\u0026gt;pom\u0026lt;/type\u0026gt; \u0026lt;scope\u0026gt;import\u0026lt;/scope\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;/dependencies\u0026gt; \u0026lt;/dependencyManagement\u0026gt; 然后在eureka-server模块引入Eureka Server 的依赖\n\u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.cloud\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-cloud-starter-netflix-eureka-server\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; 此时整个项目的情况是\n配置 Eureka Server\n新建配置文件application.yml，对 Eureka Server 进行配置\nspring: application: name: eureka-server server: port: 8000 eureka: instance: hostname: localhost client: fetch-registry: off register-with-eureka: false service-url: defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/ eureka.client.fetch-registry：是否需要从注册中心获取其它服务的地址，因为当前模块是注册中心，设置为 false eureka.client.register-with-eureka：是否将自己作为服务注册到服务中心，设置为false，取消默认的注册行为 eureka.client.service-url.defaultZone：设置与 Eureka Server 交互地址，查询服务和注册服务都依赖这个地址，默认是http://localhost:8761/eureka/。 为eureka-server模块编写启动类\n@SpringBootApplication @EnableEurekaServer public class EurekaServerApplication { public static void main(String[] args) { SpringApplication.run(EurekaServerApplication.class, args); } } 启动服务后，访问配置的地址\n此时没有任务服务注册到 Eureka Server，如果将上面的register-with-eureka设置为true。Eureka Server 会将自己注册到服务中心。\n将course-list和course-price两个模块注册到服务中心 为 Eureka Client 添加依赖\n\u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.cloud\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-cloud-starter-netflix-eureka-client\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; 在配置文件中增加 Eureka Server 的配置\neureka.client.service-url.defaultZone=http://localhost:8000/eureka/ 启动服务后，访问http://localhost:8000，发现course-list和course-price已经正确注册到服务中心。\n多节点注册中心 Eureka 可以通过互相注册的方式实现集群部署，只需要将 Eureka Server 的service-url.defaultZone配置为其它节点的地址即可。\n保证多个节点的应用名一致，register-with-eureka和fetch-registry配置为true。如下所示：\neureka: client: register-with-eureka: true fetch-registry: true spring: application: name: eureka-server --- spring: profiles: eureka-server1 server: port: 7000 eureka: instance: hostname: xianglin1 client: service-url: defaultZone: http://xianglin2:8000/eureka/,http://xianglin3:9000/eureka/ --- spring: profiles: eureka-server2 server: port: 8000 eureka: instance: hostname: xianglin2 client: service-url: defaultZone: http://xianglin1:7000/eureka/,http://xianglin3:9000/eureka/ --- spring: profiles: eureka-server3 server: port: 9000 eureka: instance: hostname: xianglin3 client: service-url: defaultZone: http://xianglin1:7000/eureka/,http://xianglin2:9000/eureka/ 在本机上启动三个 Eureka Server 的实例，相互同步注册信息。\n为三个 hostname 添加解析\n将 spring boot 应用打包后使用：--spring.profiles.active=参数分别启动三个应用\nmvn clean package -Dmaven.test.skip=true java -jar eureka-server-0.0.1-SNAPSHOT.jar --spring.profiles.active=eureka-server 打开浏览器查看 Eureka Server 的启动情况\n再次启动course-list和course-price两个模块，可以发现，三个注册中心都同步了应用的注册信息\n服务间调用 Feign Feign 是一个声明式的webservice 调用客户端。 只需要创建接口和使用 Feign 提供的注解。 Spring Cloud 为 Feign 提供了基于 Spring MVC 注解的支持，并且使用HttpMessageConverters作为默认的序列化工具。 Spring Cloud 整合了Ribbon 和 Eureka ，使得 Feign 具有负载均衡的作用。 负载均衡 负载均衡策略 RandomRule：随机策略 RoundRobinRule：轮询策略 WeightedResponseTimeRule：加权，根据平均响应时间动态加权 配置负载均衡策略：course-list.ribbon.NFLoadBalancerRuleClassName=com.netflix.loadbalancer.RoundRobinRule 使用 Feign 调用服务 course-price使用 Feign 调用course-list提供的/courses服务 在course-price模块中添加 Feign 的依赖\n\u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.cloud\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-cloud-starter-openfeign\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; 在course-price的启动类上添加@EnableFeignClients注解\n@SpringBootApplication @EnableFeignClients public class CoursePriceApplication { public static void main(String[] args) { SpringApplication.run(CoursePriceApplication.class, args); } } 使用 Feign Client 调用course-list的服务\n@FeignClient(value = \u0026#34;course-list\u0026#34;) public interface CourseListClient { /** * 获取所有的课程信息 * * @return 所有的课程信息 */ @GetMapping(value = \u0026#34;/courses\u0026#34;) List\u0026lt;Course\u0026gt; courseList(); } 在 course-price新增一个 REST 请求，用于验证服务间调用是否正常\n@RestController public class CoursePriceController { @Resource private CourseListClient courseListClient; @GetMapping(value = \u0026#34;/feign/courses\u0026#34;) public List\u0026lt;Course\u0026gt; courseList() { return courseListClient.courseList(); } } 启动Eureka Server 、course-list和course-price三个模块，请求http://localhost:8082/feign/courses接口\n负载均衡测试 在course-list模块新增一个方法，用于输出应用启动的端口，如下：\n@Value(\u0026#34;${server.port}\u0026#34;) private String port; @GetMapping(value = \u0026#34;/rule\u0026#34;) public String rule() { return \u0026#34;course-list instance port : \u0026#34; + port; } 在course-price中使用 Feign 进行调用，如下：\n@FeignClient(value = \u0026#34;course-list\u0026#34;) public interface CourseListClient { /** * 测试负载均衡 * * @return 端口信息 */ @GetMapping(value = \u0026#34;/courses\u0026#34;) String rule(); } @GetMapping(value = \u0026#34;/feign/port\u0026#34;) public String port() { return courseListClient.rule(); } 在调用方course-price中为服务配置负载均衡策略\n# 配置负载均衡 course-list.ribbon.NFLoadBalancerRuleClassName=com.netflix.loadbalancer.RandomRule 启动多个course-list实例，调用/feign/port接口，观察输出\n![image-20201212162254699](/Users/xianglin/Library/Application Support/typora-user-images/image-20201212162254699.png)\ncourse-list instance port : 8091 course-list instance port : 8071 Feign 调用携带 Session 服务间的 Feign 调用是基于 HTTP 请求的，也就是说，服务间调用是无状态的，类似于 Session 信息无法在服务间传递，此时可以使用FeignRequestInterceptor拦截器对请求增加指定的请求头。\n/** * Feign 请求的拦截器，为请求增加请求头 * * @author xianglin */ @EnableFeignClients @Configuration public class FeignRequestInterceptor implements RequestInterceptor { @Override public void apply(RequestTemplate requestTemplate) { // 获取到请求 RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes(); if (requestAttributes == null) { return; } HttpServletRequest request = ((ServletRequestAttributes) requestAttributes).getRequest(); // 将网关的请求头附加到 Feign 调用的请求中 Enumeration\u0026lt;String\u0026gt; headerNames = request.getHeaderNames(); if (headerNames != null) { while (headerNames.hasMoreElements()) { String name = headerNames.nextElement(); Enumeration\u0026lt;String\u0026gt; values = request.getHeaders(name); while (values.hasMoreElements()) { String value = values.nextElement(); requestTemplate.header(name, value); } } } } } 熔断器 Hystrix 雪崩效应 微服务架构中基本是多个服务层相互调用，基础服务的故障可能会导致级联故障，进而造成整个系统不可用的情况，这种情况被称为服务雪崩效应。服务雪崩效应是一种因“服务提供者”不可用导致“服务消费者”不可用，并将不可用逐渐放大的过程。\nHystrix 特性 断路器机制 当Hystrix Command 请求后端服务失败超过一定比例时，断路器会自动切换到开路（OPEN）状态，这时候所有请求会直接失败而不是发送到后端服务。断路器保持在开路状态一段时间后，自动切换到半开状态（HALF-OPEN），这时会判断下一次请求的返回状况。如果请求成功，断路器切回关闭（CLOSE）状态， 否则重新切换到开路状态。\nFallBack fallback 相当于降级操作。对于查询操作，我们可以实现一个 fallback 方法，当请求后端服务异常时，可以使用 fallback 的返回值。fallback 的返回值一般是设置的默认值或者缓存。\n资源隔离 熔断器使用 在服务调用方配置熔断器 在course-price模块中配置使用熔断器\n# 熔断器使用 feign.hystrix.enabled=true 为CourseListClient创建回调类\n/** * CourseList 的 回调类 调用 * * @author xianglin */ public class CourseListClientHystrix implements CourseListClient { @Override @GetMapping(value = \u0026#34;/courses\u0026#34;) public List\u0026lt;Course\u0026gt; courseList() { return Collections.emptyList(); } } 在@FeignClient注解上指定回调类\n@FeignClient(value = \u0026#34;course-list\u0026#34;, fallback = CourseListClientHystrix.class) 启动项目，验证熔断器\n先验证服务是否正常\n将course-list服务停止后再次访问\n后再次启动course-list服务后访问，发现服务恢复正常。\n服务网关 Zuul Spring Cloud Zuul路由是微服务架构的不可或缺的一部分，提供动态路由，监控，弹性，安全等的边缘服务。Zuul是Netflix出品的一个基于JVM路由和服务端的负载均衡器。\nZuul 简单使用 新建一个网关项目course-zuul\n添加依赖\n\u0026lt;dependencies\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.cloud\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-cloud-starter-netflix-eureka-client\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.cloud\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-cloud-starter-netflix-zuul\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;/dependencies\u0026gt; 配置网关相关内容\nserver: port: 8080 spring: application: name: course-zuul eureka: client: service-url: defaultZone: http://localhost:8000/eureka/ zuul: # 定义网关前缀 prefix: /course # 为服务指定前缀 routes: course-list: path: /list/* service-id: course-list course-price: path: /price/* service-id: course-price 创建启动类\n/** * Zuul 网关启动类 * * @author xianglin */ @SpringBootApplication @EnableZuulProxy public class ZuulApplication { public static void main(String[] args) { SpringApplication.run(ZuulApplication.class, args); } } 通过网关访问/courses接口\nZuul 的核心应用 Filter是Zuul的核心，用来实现对外服务的控制。Filter的生命周期有4个，分别是“PRE”、“ROUTING”、“POST”、“ERROR”，整个生命周期可以用下图来表示。\nZuul 中大部分功能都是基于过滤器实现的，这些过滤器对应于请求的典型生命周期：\nPRE：在请求被路由之前调用，可以用于身份校验，在集群中选择请求的微服务等。 ROUTING：将请求路由到微服务，用于构建发送给微服务的请求，使用 Apache HTTPClient 或者Netflix Ribbon请求微服务。 POST：在请求从微服务返回后执行，可以用于设置统一的响应头，收集统计信息等。 ERROR：在其他阶段发生错误时执行该过滤器。 自定义过滤器 Zuul 中默认实现的过滤器 类型 顺序 过滤器 功能 pre -3 ServletDetectionFilter 标记处理Servlet的类型 pre -2 Servlet30WrapperFilter 包装HttpServletRequest请求 pre -1 FormBodyWrapperFilter 包装请求体 route 1 DebugFilter 标记调试标志 route 5 PreDecorationFilter 处理请求上下文供后续使用 route 10 RibbonRoutingFilter serviceId请求转发 route 100 SimpleHostRoutingFilter url请求转发 route 500 SendForwardFilter forward请求转发 post 0 SendErrorFilter 处理有错误的请求响应 post 1000 SendResponseFilter 处理正常的请求响应 可以在配置文件中配置需要禁用的过滤器\nzuul: FormBodyWrapperFilter: pre: disable: true 自定义 Filter 自定义 Filter 需要继承com.netflix.zuul.ZuulFilter，并实现四个方法\n/** * 自定义过滤器 * @author xianglin */ @Component public class CustomerZuulFilter extends ZuulFilter { @Override public String filterType() { // 定义过滤器的类型 return FilterConstants.PRE_TYPE; } @Override public int filterOrder() { // 定义同类过滤器的执行顺序，数字越小，优先级越高 return 0; } @Override public boolean shouldFilter() { // 判断是否需要执行此过滤器 return false; } @Override public Object run() throws ZuulException { // 过滤器要执行的具体操作 return null; } } Zuul 的其它应用 路由熔断 当后端服务出现异常时，不希望直接将异常抛出给最外层，而是希望服务可以自动进行降级。此时可以实现org.springframework.cloud.netflix.zuul.filters.route.FallbackProvider。其中getRoute方法告诉 Zuul 它负责哪个 route定义的熔断，fallbackResponse用于处理请求并返回。\npublic interface FallbackProvider { String getRoute(); ClientHttpResponse fallbackResponse(String route, Throwable cause); } 为course-price定制熔断内容\n/** * 为 course-price 服务定制熔断内容 * * @author xianglin */ @Component public class CoursePriceFallbackProvider implements FallbackProvider { @Override public String getRoute() { return \u0026#34;course-price\u0026#34;; } @Override public ClientHttpResponse fallbackResponse(String route, Throwable cause) { String message = \u0026#34;\u0026#34;; if (cause != null \u0026amp;\u0026amp; cause.getCause() != null) { message = cause.getCause().getMessage(); } String finalMessage = message; return new ClientHttpResponse() { @Override public HttpStatus getStatusCode() throws IOException { return HttpStatus.OK; } @Override public int getRawStatusCode() throws IOException { return HttpStatus.OK.value(); } @Override public String getStatusText() throws IOException { return HttpStatus.OK.name(); } @Override public void close() { } @Override public InputStream getBody() throws IOException { return new ByteArrayInputStream((\u0026#34;服务异常，请稍后！\u0026#34; + finalMessage).getBytes()); } @Override public HttpHeaders getHeaders() { return new HttpHeaders() {{ setContentType(MediaType.APPLICATION_JSON); }}; } }; } } 当course-price服务停止后，再次调用则会返回“服务异常”的提示\nZuul 目前只支持服务级别的熔断，不支持具体到某个URL进行熔断。\n","permalink":"https://2d992714.xianglin2020-github-io.pages.dev/posts/2020/springcloud-micro-service/","summary":"\u003ch1 id=\"微服务\"\u003e微服务\u003c/h1\u003e\n\u003ch2 id=\"微服务的基础概念\"\u003e微服务的基础概念\u003c/h2\u003e\n\u003ch3 id=\"单体应用的不足\"\u003e单体应用的不足\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e部署效率低下：依赖众多、应用体积较大、启动时间较长\u003c/li\u003e\n\u003cli\u003e团队协作开发成本高\u003c/li\u003e\n\u003cli\u003e系统可用性差\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"服务化\"\u003e服务化\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e把传统的单机应用中的本地方法调用，改造成通过 RPC、HTTP 产生的远程方法调用\u003c/li\u003e\n\u003cli\u003e把模块从单体应用中拆分出来，独立成一个服务部署\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"微服务概念\"\u003e微服务概念\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e一种架构风格\u003c/li\u003e\n\u003cli\u003e开发单个应用作为一系列小型服务的套件，其中每个服务都运行在自己的进程中，并且通过轻量级的机制实现彼此间的通信，这通常是 HTTP 资源 API\u003c/li\u003e\n\u003cli\u003e这些服务是围绕着业务功能构建的，并且可以通过完全自动化的部署机制进行独立部署\u003c/li\u003e\n\u003cli\u003e这些服务的集中化管理做到了最小化（例如 docker 相关技术），每一种服务都可以通过不同的编程语言进行编写，并且可以使用不同的数据存储技术\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"微服务的特点\"\u003e微服务的特点\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e组件以服务形式来提供\u003c/li\u003e\n\u003cli\u003e微服务是产品而不是项目\u003c/li\u003e\n\u003cli\u003e轻量级通信、独立进程\u003c/li\u003e\n\u003cli\u003e分散治理、去中心化治理\u003c/li\u003e\n\u003cli\u003e容错性设计\u003c/li\u003e\n\u003cli\u003e会带来团队组织架构的调整\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"微服务的优缺点\"\u003e微服务的优缺点\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e服务简单、便于学习和上手、相对易于维护\u003c/p\u003e","title":"SpringCloud 和微服务"},{"content":"RabbitMQ 消息队列 MQ（Message Queue）：MQ 的模型大致如下\n消息队列的特性 业务无关性 FIFO 容灾 性能 使用消息队列的原因 系统解耦 异步调用 流量削峰 RabbitMQ 介绍 RabbitMQ 基础思想 核心思想：接收并转发消息，可以类比于邮局 producer：生产者、消息发送者 queue：消息存储队列 consumer：消费者 RabbitMQ 的特点 开源、跨语言 Erlang 语言编写、内建集群 应用广泛 社区活跃、API 丰富 RabbitMQ 核心概念 RabbitMQ 是基于 AMPQ 协议的开源实现，其模型如下： Server：服务\nconnection：与 Server 建立的连接\nchannel：信道，几乎所有的操作都是在信道上完成的，客户端可以建立多个信道\n项目与 RabbitMQ 之间使用 TCP 连接，引入信道的概念，所有需要发布消息的线程都包装成一条信道在 TCP 中传输，理论上一条 TCP 连接支持无限多个信道。\nmessage：消息，由 properties 和 body 组成\nvirtual host：虚拟主机，顶层隔离。统一虚拟主机下，不能有重复的交换机和 queue\nexchange：交换机，接收生产者的消息，根据指定的路由键去把消息转发到所绑定的队列上\nbinding：绑定交换机和队列\n用于消息队列和交换器之间的关联。一个绑定就是基于路由键将交换器和消息队列连接起来的路由规则，所以可以将交换器理解成一个由绑定构成的路由表。\nrouting key：路由键，路由规则\nqueue：队列，消费者只需要监听队列来消费消息\n消息队列，用来保存消息直到发送给消费者。它是消息的容器，也是消息的终点。一个消息可以投入一个或多个队列。消息一直在队列里面，等待消费者连接这个队列将其取走。\nAMQP 协议 Advanced Message Queuing Protocol\nRabbitMQ 的安装启动 官方教程见：https://www.rabbitmq.com/install-rpm.html\nCentOS7 下使用 YUM 安装 Erlang 新建erlang 的源配置\n➜ ~ vim /etc/yum.repos.d/rabbitmq_erlang.repo [rabbitmq-erlang] name=rabbitmq-erlang baseurl=https://dl.bintray.com/rabbitmq-erlang/rpm/erlang/22/el/7 gpgcheck=1 gpgkey=https://dl.bintray.com/rabbitmq/Keys/rabbitmq-release-signing-key.asc repo_gpgcheck=0 enabled=1 更新源信息\n➜ ~ yum clear all ➜ ~ yum makecache 安装 erlang\n➜ ~ yum install erlang # 确认源为 rabbitmq-erlang ================================================================================ Package 架构 版本 源 大小 ================================================================================ 正在安装: erlang x86_64 22.3.4.12-1.el7 rabbitmq-erlang 19 M CentOS7 下安装 RabbitMQ 导入秘钥\n➜ ~ rpm --import https://www.rabbitmq.com/rabbitmq-release-signing-key.asc 新建 rabbitMq 源信息\n➜ ~ vim /etc/yum.repos.d/rabbitmq.repo [bintray-rabbitmq-server] name=bintray-rabbitmq-rpm baseurl=https://dl.bintray.com/rabbitmq/rpm/rabbitmq-server/v3.8.x/el/7/ gpgcheck=0 repo_gpgcheck=0 enabled=1 更新源信息\n➜ ~ yum clear all ➜ ~ yum makecache 安装 rabbitmq\n➜ ~ yum install rabbitmq-server.noarch RabbitMQ 的运行与管理 启动 RabbitMQ\n# 使用 systemd 管理服务 ➜ ~ systemctl start rabbitmq-server # 使用 rabbitmq 可执行文件启动 -detached 参数指定为后台启动 ➜ ~ rabbitmq-server -detached 停止 RabbitMQ\n# 停止应用 ➜ ~ rabbitmqctl --node rabbit@localhost stop_app Stopping rabbit application on node rabbit@localhost ... # 停止特定节点 ➜ ~ rabbitmqctl -n rabbit@localhost stop Stopping and halting node rabbit@localhost ... 查询服务器状态\n➜ ~ rabbitmqctl status Status of node rabbit@localhost ... Runtime OS PID: 29695 OS: Linux 查看队列、绑定、交换器\n➜ ~ rabbitmqctl list_queues Timeout: 60.0 seconds ... Listing queues for vhost / ... name\tmessages task_queue\t0 queue1\t0 queue2\t0 ➜ ~ rabbitmqctl list_exchanges Listing exchanges for vhost / ... name\ttype amq.direct\tdirect amq.match\theaders ➜ ~ rabbitmqctl list_bindings Listing bindings for vhost /... source_name\tsource_kind\tdestination_name\tdestination_kind\trouting_key\targuments exchange\ttask_queue\tqueue\ttask_queue\t[] 启用管理页面 启动插件\n➜ ~ rabbitmq-plugins enable rabbitmq_management 新建管理员用户\n# amdin 为用户名 password 为密码 ➜ ~ rabbitmqctl add_user admin password ➜ ~ rabbitmqctl set_user_tags admin administrator 开放15672端口\n➜ ~ firewall-cmd --add-port=15672/tcp --permanent ➜ ~ firewall-cmd --reload 尝试登陆后台管理页面\nJava 使用 RabbitMQ 简单使用，无交换器概念 引入 Maven 依赖\n\u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;com.rabbitmq\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;amqp-client\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;5.8.0\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.slf4j\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;slf4j-nop\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.7.29\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; RabbitMQ 消息发送者\npublic class Send { private final static String QUEUE_NAME = \u0026#34;hello\u0026#34;; public static void main(String[] args) throws IOException, TimeoutException { // 创建连接工厂 ConnectionFactory connectionFactory = new ConnectionFactory(); // 设置 RabbitMQ 地址 connectionFactory.setHost(\u0026#34;192.168.56.106\u0026#34;); connectionFactory.setUsername(\u0026#34;admin\u0026#34;); connectionFactory.setPassword(\u0026#34;password\u0026#34;); // 建立连接 Connection connection = connectionFactory.newConnection(); // 获得信道 Channel channel = connection.createChannel(); // 声明队列 channel.queueDeclare(QUEUE_NAME, false, false, false, null); // 发布消息 String message = \u0026#34;Hello World!\u0026#34;; channel.basicPublish(\u0026#34;\u0026#34;, QUEUE_NAME, null, message.getBytes()); System.out.println(\u0026#34;发送了消息：\u0026#34; + message); // 关闭连接 channel.close(); connection.close(); } } 消费者\npublic class Receiver { private final static String QUEUE_NAME = \u0026#34;hello\u0026#34;; public static void main(String[] args) throws IOException, TimeoutException { // 创建连接工厂 ConnectionFactory connectionFactory = new ConnectionFactory(); // 设置 RabbitMQ 地址 connectionFactory.setHost(\u0026#34;192.168.56.106\u0026#34;); connectionFactory.setUsername(\u0026#34;admin\u0026#34;); connectionFactory.setPassword(\u0026#34;password\u0026#34;); // 建立连接 Connection connection = connectionFactory.newConnection(); // 获得信道 Channel channel = connection.createChannel(); // 声明队列 channel.queueDeclare(QUEUE_NAME, false, false, false, null); channel.basicConsume(QUEUE_NAME, true, new DefaultConsumer(channel) { @Override public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException { String message = new String(body); System.out.println(\u0026#34;收到消息：\u0026#34; + message); } }); } } RabbitMQ 交换器工作模式 fanout：广播，只需要将队列绑定到交换机上即可，不需要设置路由键 direct：根据RoutingKey 匹配消息路由到指定的队列 topic：生产者指定 RoutingKey， 消息根据消费端指定的队列通过模糊匹配的方式进行相应转发 headers：根据发送消息内容中的 headers 属性来匹配 fanout 模式 每个发送到fanout类型交换器的消息都会被分发到所有绑定的队列上去，它不会处理路由键。 消息生产者\npublic static void main(String[] args) { // 创建连接工厂 ConnectionFactory connectionFactory = new ConnectionFactory(); connectionFactory.setHost(\u0026#34;192.168.56.106\u0026#34;); connectionFactory.setUsername(\u0026#34;admin\u0026#34;); connectionFactory.setPassword(\u0026#34;password\u0026#34;); // 创建连接、获取信道 try (Connection connection = connectionFactory.newConnection(); Channel channel = connection.createChannel()) { // 声明 fanout 类型的交换器 String exchange = \u0026#34;fanout_exchange\u0026#34;; channel.exchangeDeclare(exchange, BuiltinExchangeType.FANOUT, false); // 发布消息 for (int i = 0; i \u0026lt; 10; i++) { String message = \u0026#34;fanout_exchange--\u0026#34; + i; // fanout 类型的交换器不处理 RoutingKey channel.basicPublish(exchange, \u0026#34;\u0026#34;, null, message.getBytes()); System.out.println(\u0026#34;发送了消息 ： \u0026#34; + message); } } catch (Exception ignored) { } } 消息消费者\npublic static void main(String[] args) throws IOException, TimeoutException { // 创建连接工厂 ConnectionFactory connectionFactory = new ConnectionFactory(); connectionFactory.setHost(\u0026#34;192.168.56.106\u0026#34;); connectionFactory.setUsername(\u0026#34;admin\u0026#34;); connectionFactory.setPassword(\u0026#34;password\u0026#34;); Connection connection = connectionFactory.newConnection(); Channel channel = connection.createChannel(); // 声明交换器 String exchange = \u0026#34;fanout_exchange\u0026#34;; channel.exchangeDeclare(exchange, BuiltinExchangeType.FANOUT, false); // 生成一个非持久、独占、自动删除的临时队列 String queue = channel.queueDeclare().getQueue(); // 绑定队列 channel.queueBind(queue, exchange, \u0026#34;\u0026#34;); // 接收消息 channel.basicConsume(queue, false, new DefaultConsumer(channel) { @Override public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException { String routingKey = envelope.getRoutingKey(); System.out.println(\u0026#34;routingKey = \u0026#34; + routingKey); String message = new String(body); System.out.println(\u0026#34;消费了消息：\u0026#34; + message); // 确认消息 channel.basicAck(envelope.getDeliveryTag(), false); } }); } 启动多个消费者实例，后运行消息生产者\ndirect 模式 消息中的路由键（routing key）和 Binding 中的 binding key 一致，交换器就将消息发送到对应的队列中。如果一个队列绑定到交换器要求路由键为error，则只转发 routing key 标记为error的消息，不会转发标记为info或者error.error的消息。它是完全匹配、单播的模式。\n消息生产者\n// 声明 direct 类型的交换器 String exchange = \u0026#34;direct_exchange\u0026#34;; channel.exchangeDeclare(exchange, BuiltinExchangeType.DIRECT, false); // 发布消息 String message = \u0026#34;direct_exchange simple\u0026#34;; // 假定发送日志信息，级别为：debug info error channel.basicPublish(exchange, \u0026#34;debug\u0026#34;, null, message.getBytes()); System.out.println(\u0026#34;发送了 debug 消息 ： \u0026#34; + message); channel.basicPublish(exchange, \u0026#34;info\u0026#34;, null, message.getBytes()); System.out.println(\u0026#34;发送了 info 消息 ： \u0026#34; + message); channel.basicPublish(exchange, \u0026#34;error\u0026#34;, null, message.getBytes()); System.out.println(\u0026#34;发送了 error 消息 ： \u0026#34; + message); 消息消费者\nErrorReceiver.java // 绑定队列、只处理 error 标记的日志，记录到文件 channel.queueBind(queue, exchange, \u0026#34;error\u0026#34;); AllReceiver.java // 关注所有标记的日志，打印至控制台 channel.queueBind(queue, exchange, \u0026#34;debug\u0026#34;); channel.queueBind(queue, exchange, \u0026#34;info\u0026#34;); channel.queueBind(queue, exchange, \u0026#34;error\u0026#34;); 先后启动消息消费者和消息生产者\ntopic 模式 topic交换器类似于direct交换器，它通过模式匹配分配消息的路由键属性，将路由键与某个模式进行匹配，此时队列需要绑定到一个模式上。它将路由键和绑定键的字符串以.切分为单词，同时识别两个通配符：\n*可以匹配一个单词 #可以匹配任意个单词 消息发送者\npublic static void main(String[] args) throws IOException, TimeoutException { ConnectionFactory connectionFactory = new ConnectionFactory(); connectionFactory.setHost(\u0026#34;192.168.56.106\u0026#34;); connectionFactory.setUsername(\u0026#34;admin\u0026#34;); connectionFactory.setPassword(\u0026#34;password\u0026#34;); Connection connection = connectionFactory.newConnection(); Channel channel = connection.createChannel(); // 声明交换机 String exchange = \u0026#34;topic_exchange\u0026#34;; channel.exchangeDeclare(exchange, BuiltinExchangeType.TOPIC); String message = \u0026#34;Animal World!\u0026#34;; String[] routingKeys = new String[9]; routingKeys[0] = \u0026#34;quick.orange.rabbit\u0026#34;; routingKeys[1] = \u0026#34;lazy.orange.elephant\u0026#34;; routingKeys[2] = \u0026#34;quick.orange.fox\u0026#34;; routingKeys[3] = \u0026#34;lazy.brown.fox\u0026#34;; routingKeys[4] = \u0026#34;lazy.pink.rabbit\u0026#34;; routingKeys[5] = \u0026#34;quick.brown.fox\u0026#34;; routingKeys[6] = \u0026#34;orange\u0026#34;; routingKeys[7] = \u0026#34;quick.orange.male.rabbit\u0026#34;; routingKeys[8] = \u0026#34;lazy.orange.male.rabbit\u0026#34;; for (String routingKey : routingKeys) { channel.basicPublish(exchange, routingKey, null, message.getBytes()); System.out.println(\u0026#34;发送了：\u0026#34; + message + \u0026#34;routingKey: \u0026#34; + routingKey); } channel.close(); connection.close(); } 消息消费者\nComsumer1.java String exchange = \u0026#34;topic_exchange\u0026#34;; channel.queueBind(queueName, exchange, \u0026#34;*.orange.*\u0026#34;); Comsumer2.java channel.queueBind(queueName, exchange, \u0026#34;*.*.rabbit\u0026#34;); channel.queueBind(queueName, exchange, \u0026#34;lazy.#\u0026#34;); 先后启动消费者和生产者\nSpringBoot 中使用 RabbitMQ 生产者应用 引入起步依赖\n\u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-starter-amqp\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; 在application.properties中配置连接信息\nserver.port=8080 spring.application.name=producer spring.rabbitmq.addresses=192.168.56.106:5672 spring.rabbitmq.username=admin spring.rabbitmq.password=password spring.rabbitmq.virtual-host=/ spring.rabbitmq.connection-timeout=15000 编写TopicRabbitConfig配置类\n@Configuration public class TopicRabbitConfig { @Bean public Queue queue1() { return new Queue(\u0026#34;queue1\u0026#34;); } @Bean public Queue queue2() { return new Queue(\u0026#34;queue2\u0026#34;); } @Bean public TopicExchange exchange() { return new TopicExchange(\u0026#34;bootExchange\u0026#34;); } @Bean public Binding bindingExchangeMessage1(Queue queue1, TopicExchange exchange) { return BindingBuilder.bind(queue1).to(exchange).with(\u0026#34;dog.red\u0026#34;); } @Bean public Binding bindingExchangeMessage2(Queue queue2, TopicExchange exchange) { return BindingBuilder.bind(queue2).to(exchange).with(\u0026#34;dog.#\u0026#34;); } } 发送消息\n@Component public class MessageSender { @Resource private AmqpTemplate rabbitMqTemplate; public void send1() { String message = \u0026#34;this is message 1, routing key is dog.red.\u0026#34;; System.out.println(\u0026#34;发送消息：\u0026#34; + message); this.rabbitMqTemplate.convertAndSend(\u0026#34;bootExchange\u0026#34;, \u0026#34;dog.red\u0026#34;, message); } public void send2() { String message = \u0026#34;this is message 2, routing key is dog.blank.\u0026#34;; System.out.println(\u0026#34;发送消息：\u0026#34; + message); this.rabbitMqTemplate.convertAndSend(\u0026#34;bootExchange\u0026#34;, \u0026#34;dog.blank\u0026#34;, message); } } 消费者应用 引入起步依赖\n配置连接信息\n编写消费者\n@Component @RabbitListener(queues = \u0026#34;queue1\u0026#34;) public class Receiver1 { @RabbitHandler public void process(String message) { System.out.println(\u0026#34;Receiver1 :\u0026#34; + message); } } @Component @RabbitListener(queues = \u0026#34;queue2\u0026#34;) public class Receiver2 { @RabbitHandler public void process(String message) { System.out.println(\u0026#34;Receiver2 :\u0026#34; + message); } } 使用@RabbitListener(queues = \u0026quot;queue2\u0026quot;)指定需要监听的队列\n运行项目 使用单元测试的方式，发送消息\n@SpringBootTest class ProducerApplicationTests { @Resource private MessageSender messageSender; @Test public void send1() { messageSender.send1(); } @Test public void send2() { messageSender.send2(); } } 启动消费者应用\nRabbitMQ 集群部署 学习RabbitMQ集群原理与部署\n","permalink":"https://2d992714.xianglin2020-github-io.pages.dev/posts/2020/rabbitmq-basics/","summary":"\u003ch1 id=\"rabbitmq\"\u003eRabbitMQ\u003c/h1\u003e\n\u003ch2 id=\"消息队列\"\u003e消息队列\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003eMQ（Message Queue）：MQ 的模型大致如下\u003c/p\u003e\n\u003cimg src=\"https://cdn.jsdelivr.net/gh/xianglin2020/gallery@master/202012/095535.png\" alt=\"image-20201206095534595\" style=\"zoom:50%;\" /\u003e\n\u003cimg src=\"https://cdn.jsdelivr.net/gh/xianglin2020/gallery@master/202012/133513.png\" alt=\"image-20201205133512771\" style=\"zoom:25%;\" /\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"消息队列的特性\"\u003e消息队列的特性\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e业务无关性\u003c/li\u003e\n\u003cli\u003eFIFO\u003c/li\u003e\n\u003cli\u003e容灾\u003c/li\u003e\n\u003cli\u003e性能\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"使用消息队列的原因\"\u003e使用消息队列的原因\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e系统解耦\u003c/li\u003e\n\u003cli\u003e异步调用\u003c/li\u003e\n\u003cli\u003e流量削峰\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"rabbitmq-介绍\"\u003eRabbitMQ 介绍\u003c/h2\u003e\n\u003ch3 id=\"rabbitmq-基础思想\"\u003eRabbitMQ 基础思想\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e核心思想：接收并转发消息，可以类比于邮局\u003c/li\u003e\n\u003cli\u003eproducer：生产者、消息发送者\u003c/li\u003e\n\u003cli\u003equeue：消息存储队列\u003c/li\u003e\n\u003cli\u003econsumer：消费者\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"rabbitmq-的特点\"\u003eRabbitMQ 的特点\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e开源、跨语言\u003c/li\u003e\n\u003cli\u003eErlang 语言编写、内建集群\u003c/li\u003e\n\u003cli\u003e应用广泛\u003c/li\u003e\n\u003cli\u003e社区活跃、API 丰富\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"rabbitmq-核心概念\"\u003eRabbitMQ 核心概念\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003eRabbitMQ 是基于 AMPQ 协议的开源实现，其模型如下：\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"https://cdn.jsdelivr.net/gh/xianglin2020/gallery@master/202012/135354.png\" alt=\"image-20201205135354320\"  /\u003e\n\u003c/p\u003e","title":"RabbitMQ 基础"},{"content":"Nginx Nginx 是一款轻量级的 Web 服务器、反向代理服务器及电子邮件代理服务器，其特点是占用内存小，并发能力强。\nNginx 使用场景 HTTP 的反向代理服务器 正向代理\n反向代理\nNginx 的反向代理\n正向代理：是一个位于客户端和原始服务器之间的服务器，为了从原始服务器取得内容，客户端向代理发送请求并指定原始服务器，然后代理向原始服务器转发请求并将获得的内容返回给客户端。\n反向代理：代理服务器接收网络上的请求，它将请求转发给内部网络上的服务器，并将从服务器上得到的结果返回给原始请求的客户端。\n动态静态资源分离 静态资源无需经过 Tomcat，Tomcat 只负责处理动态请求 Nginx 本身是一个静态资源服务器 Nginx 的优点 高并发、高性能 可扩展性好：模块化设计 可靠性高 热部署：不停止服务的情况下升级 Nginx 开源、可商用 Nginx 安装使用 Nginx 在 CentOS7 下的安装 安装 yum-utils\nyum install yum-utils\n为 Nginx 增加源\n[root@localhost ~]# vi /etc/yum.repos.d/nginx.repo [nginx-stable] name=nginx stable repo baseurl=http://nginx.org/packages/centos/7/$basearch/ gpgcheck=1 enabled=1 gpgkey=https://nginx.org/keys/nginx_signing.key module_hotfixes=true [nginx-mainline] name=nginx mainline repo baseurl=http://nginx.org/packages/mainline/centos/7/$basearch/ gpgcheck=1 enabled=0 gpgkey=https://nginx.org/keys/nginx_signing.key module_hotfixes=true 查看 Nginx 源\n[root@localhost ~]# yum list | grep nginx nginx.x86_64 1:1.18.0-2.el7.ngx @nginx-stable nginx-debug.x86_64 1:1.8.0-1.el7.ngx nginx-stable nginx-debuginfo.x86_64 1:1.18.0-2.el7.ngx nginx-stable nginx-module-geoip.x86_64 1:1.18.0-2.el7.ngx nginx-stable nginx-module-geoip-debuginfo.x86_64 1:1.18.0-2.el7.ngx nginx-stable nginx-module-image-filter.x86_64 1:1.18.0-2.el7.ngx nginx-stable nginx-module-image-filter-debuginfo.x86_64 1:1.18.0-2.el7.ngx nginx-stable nginx-module-njs.x86_64 1:1.18.0+0.5.0-1.el7.ngx nginx-stable nginx-module-njs-debuginfo.x86_64 1:1.18.0+0.5.0-1.el7.ngx nginx-stable nginx-module-perl.x86_64 1:1.18.0-2.el7.ngx nginx-stable nginx-module-perl-debuginfo.x86_64 1:1.18.0-2.el7.ngx nginx-stable nginx-module-xslt.x86_64 1:1.18.0-2.el7.ngx nginx-stable nginx-module-xslt-debuginfo.x86_64 1:1.18.0-2.el7.ngx nginx-stable nginx-nr-agent.noarch 2.0.0-12.el7.ngx nginx-stable pcp-pmda-nginx.x86_64 4.3.2-12.el7 base 安装指定版本\n[root@localhost ~]# yum install nginx 1:1.18.0-2.el7.ngx 验证 Nginx 是否安装成功\n[root@localhost ~]# nginx -v nginx version: nginx/1.18.0 可能需要配置防火墙\n# 查看防火墙状态 [root@localhost conf.d]# firewall-cmd --state running # 查看启用的端口 [root@localhost conf.d]# firewall-cmd --list-port 80/tcp # 开放 80 端口 --permanent 永久生效 [root@localhost conf.d]# firewall-cmd --add-port=80/tcp --permanent 或者同时打开 80 和443端口\nfirewall-cmd --permanent --zone=public --add-service=http firewall-cmd --permanent --zone=public --add-service=https firewall-cmd --reload Nginx 常用命令 [root@localhost ~]# nginx -h nginx version: nginx/1.18.0 Usage: nginx [-?hvVtTq] [-s signal] [-c filename] [-p prefix] [-g directives] Options: -?,-h : this help -v : show version and exit -V : show version and configure options then exit -t : test configuration and exit -T : test configuration, dump it and exit -q : suppress non-error messages during configuration testing -s signal : send signal to a master process: stop, quit, reopen, reload -p prefix : set prefix path (default: /etc/nginx/) -c filename : set configuration file (default: /etc/nginx/nginx.conf) -g directives : set global directives out of configuration file 命令 含义 ?/h 显示帮助信息 v/V 显示版本信息 c 使用指定的配置文件 g 指定配置指令 p 指定运行目录 t/T 测试配置文件编写是否正确 s 向 master 进程发送命令 stop 立即停止 quit 优雅停止 reopen 重新开始记录日志文件 reload 重启 Nginx 配置文件 Nginx 相关的配置文件在/etc/nginx/目录中\nNginx 的主配置文件是/etc/nginx/nginx.conf\nNginx 日志文件access.log和error.log位于/var/log/nginx/目录下\n基础语法：\n使用;结尾 使用{}组织多条指令 使用include引入其它配置文件 使用#注释 $表示变量 Nginx 默认配置文件及注释nginx.conf\nuser nginx; # 工作进程数 worker_processes 1; # Nginx 的错误日志 error_log /var/log/nginx/error.log warn; # Nginx 的pid，pid 是文本文件，只有一行，记录了该进程的 ID pid /var/run/nginx.pid; events { # 每个进程最大连接数 worker_connections 1024; } http { include /etc/nginx/mime.types; default_type application/octet-stream; # 自定义日志格式 log_format main \u0026#39;$remote_addr - $remote_user [$time_local] \u0026#34;$request\u0026#34; \u0026#39; \u0026#39;$status $body_bytes_sent \u0026#34;$http_referer\u0026#34; \u0026#39; \u0026#39;\u0026#34;$http_user_agent\u0026#34; \u0026#34;$http_x_forwarded_for\u0026#34;\u0026#39;; # 访问日志 access_log /var/log/nginx/access.log main; # 高效传输文件模式 sendfile on; tcp_nopush on; # 请求超时时间 keepalive_timeout 65; #gzip on; include /etc/nginx/conf.d/*.conf; } default.conf\n# 每一个 server 对应着一个虚拟主机或域名 server { # 监听端口 listen 80; # server_name localhost; #charset koi8-r; #access_log /var/log/nginx/host.access.log main; # 一个 server 可以配置多个 location ，针对不同的访问路径做配置 # 访问首页 location / { root /usr/share/nginx/html; index index.html index.htm; } #error_page 404 /404.html; # redirect server error pages to the static page /50x.html # # 当出现 5XX 状态码时，重定向到新页面 error_page 500 502 503 504 /50x.html; location = /50x.html { root /usr/share/nginx/html; } # proxy the PHP scripts to Apache listening on 127.0.0.1:80 # #location ~ \\.php$ { # proxy_pass http://127.0.0.1; #} # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000 # #location ~ \\.php$ { # root html; # fastcgi_pass 127.0.0.1:9000; # fastcgi_index index.php; # fastcgi_param SCRIPT_FILENAME /scripts$fastcgi_script_name; # include fastcgi_params; #} # deny access to .htaccess files, if Apache\u0026#39;s document root # concurs with nginx\u0026#39;s one # #location ~ /\\.ht { # deny all; #} } Nginx 的内置全局变量\n变量名 描述 $host 请求信息中的 Host，如果请求中没有 Host 行，则等于设置的服务器名 $request_method 客户端的请求类型，如 GET、POST $remote_addr 客户端的 IP 地址 $remote_port 客户端的端口 $args 请求中的参数 $content-length 请求头的 Content-length 字段 $http_user_agent 客户端 agent 信息 $http_cookies Cookies 信息 $server_protocol 请求使用的协议 $server_address 服务器地址 $server_name 服务器名称 $server_port 服务器的端口号 Nginx 开启压缩 ngx_http_gzip_module 为 Nginx 提供了 gzip 的压缩支持，可以减少传输资源，节省宽带，但会增加 CPU 消耗。\n主要参数有：\n参数名 参数值 描述 gzip on / off 是否开启 gzip 压缩 gzip_comp_level 1 ~ 9 压缩级别 gzip_min_length 小于此设定值的文件不会被压缩，长度来源于 Content-Length 字段 gzip_types text/html 进行压缩的 MIME 类型 Nginx 配置反向代理 ngx_http_upstream_module 为 Nginx 提供列反向代理功能。此模块用于将多个服务器定义为服务器组，可以通过 proxy_pass 传递指令来引用服务器组。\n一个简单的配置如下：\nworker_processes 1; events { worker_connections 1024; } http { include mime.types; default_type application/octet-stream; sendfile on; keepalive_timeout 65; upstream springboot{ server 127.0.0.1:8080; server 127.0.0.1:8081; server 127.0.0.1:8082; } server { listen 80; server_name localhost; location / { proxy_pass http://springboot; } error_page 500 502 503 504 /50x.html; location = /50x.html { root /usr/share/nginx/html; } } } Nginx 负载均衡策略 upstream 默认依照轮询的方式进行负载均衡，每一个请求按时间顺序逐一分配到不同的后端服务器。假使后端服务器故障，能主动剔除。除此之外，upstream 还有如下的分配策略：\nweight（权重）\nweight 值和分配概率成正比，用于后端服务器性能不均的情况下。\nupstream springboot{ server 127.0.0.1:8080 weight=1; server 127.0.0.1:8081 weight=2; server 127.0.0.1:8082 weight=3; } ip_hash（访问 IP）\n每一个请求按访问 IP 的 hash 运算结果分配到指定的后端服务器，能够解决 session 的问题。\nupstream springboot{ ip_hash; server 127.0.0.1:8080; server 127.0.0.1:8081; server 127.0.0.1:8082; } fair\n根据页面大小、加载时间长短智能的进行负载均衡。\nleast_conn（最少连接）\n下一个请求将被分派到活动连接数量最少的服务器。\n","permalink":"https://2d992714.xianglin2020-github-io.pages.dev/posts/2020/nginx-basics/","summary":"\u003ch1 id=\"nginx\"\u003eNginx\u003c/h1\u003e\n\u003cp\u003eNginx 是一款轻量级的 Web 服务器、反向代理服务器及电子邮件代理服务器，其特点是占用内存小，并发能力强。\u003c/p\u003e\n\u003ch2 id=\"nginx-使用场景\"\u003eNginx 使用场景\u003c/h2\u003e\n\u003ch3 id=\"http-的反向代理服务器\"\u003eHTTP 的反向代理服务器\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e正向代理\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"https://cdn.jsdelivr.net/gh/xianglin2020/gallery@master/202012/210919.png\" alt=\"image-20201203210919188\"  /\u003e\n\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e反向代理\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"https://cdn.jsdelivr.net/gh/xianglin2020/gallery@master/202012/211044.png\" alt=\"image-20201203211044613\"  /\u003e\n\u003c/p\u003e","title":"Nginx 基础知识"},{"content":"Docker Docker 基础概念 Docker 是一个用来装程序及其环境的容器，属于Linux 容器的一种封装，提供简单易用的容器使用接口。它是目前最流行的 Linux 容器解决方案。\nDocker 的用途 提供统一的环境 提供快速拓展、弹性伸缩的云服务 防止其他用户的进程把服务器资源占用过多 Docker 的特点 标准化 运输方式：把程序和环境从一个机器运到另一个机器 存储方法：程序和环境的存储 API 接口 灵活：即使是最复杂的应用也可以集装箱化 轻量级：容器利用并共享主机内核 便携式：可以在本地构造，部署到云，并可以在任何地方运行 Docker 组成 镜像 镜像存储：联合文件系统，UnionFS\n容器 镜像类似于 Java 中的类，而容器类似于实例 容器层可以修改，镜像是不可以修改的 同一个镜像可以生成多个独立的容器，容器之间互不干扰 仓库 hub.docker.com 私有、共有仓库 Client、Deamon client 提供给用户一个终端，用户输入 Docker 提供的命令来管理本地或远程的服务器 deamon：服务端守护进程，接收 client 发送的命令并执行相应的操作 Docker 的网络模式 Bridge Host None Docker 安装 Docker 在 CentOS7 的安装 查看 CentOS版本\n[root@localhost ~]# cat /etc/redhat-release CentOS Linux release 7.9.2009 (Core) 设置阿里云的 yum 源\n[root@localhost ~]# wget -O /etc/yum.repos.d/CentOS-Base.repo http://mirrors.aliyun.com/repo/Centos-7.repo --2020-12-02 22:36:00-- http://mirrors.aliyun.com/repo/Centos-7.repo 正在解析主机 mirrors.aliyun.com (mirrors.aliyun.com)... 157.148.73.246, 157.148.73.241, 157.148.73.242, ... 正在连接 mirrors.aliyun.com (mirrors.aliyun.com)|157.148.73.246|:80... 已连接。 已发出 HTTP 请求，正在等待回应... 200 OK 长度：2523 (2.5K) [application/octet-stream] 正在保存至: “/etc/yum.repos.d/CentOS-Base.repo” 100%[======================================\u0026gt;] 2,523 --.-K/s 用时 0s 2020-12-02 22:36:00 (499 MB/s) - 已保存 “/etc/yum.repos.d/CentOS-Base.repo” [2523/2523]) 更新 yum\n[root@localhost ~]# yum clean all 已加载插件：fastestmirror 正在清理软件源： base docker-ce-stable extras updates Cleaning up list of fastest mirrors [root@localhost ~]# yum makecache 已加载插件：fastestmirror Determining fastest mirrors * base: mirrors.aliyun.com * extras: mirrors.aliyun.com * updates: mirrors.aliyun.com base | 3.6 kB 00:00 docker-ce-stable | 3.5 kB 00:00 extras | 2.9 kB 00:00 updates | 2.9 kB 00:00 (1/14): base/7/x86_64/group_gz | 153 kB 00:00 (2/14): base/7/x86_64/filelists_db | 7.2 MB 00:00 # ..... (14/14): updates/7/x86_64/other_db | 225 kB 00:00 元数据缓存已建立 [root@localhost ~]# yum check-update [root@localhost ~]# yum update 安装所需的软件包\n[root@localhost ~]# yum install -y yum-utils device-mapper-persistent-data lvm2 为 Docker 指定稳定的存储库\n[root@localhost ~]# yum-config-manager --add-repo http://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo 已加载插件：fastestmirror adding repo from: http://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo grabbing file http://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo to /etc/yum.repos.d/docker-ce.repo repo saved to /etc/yum.repos.d/docker-ce.repo 查看 Docker 版本\n[root@localhost ~]# yum list docker-ce --showduplicates | sort -r 安装（可指定版本）Docker\n[root@localhost ~]# yum install docker-ce docker-ce-cli containerd.io 启动 Docker\n[root@localhost ~]# systemctl start docker # 设置开机自启 [root@localhost ~]# systemctl enable docker 验证是否安装成功\n[root@localhost ~]# docker version [root@localhost ~]# docker info 运行第一个 Docker 容器 下载镜像\ndocker pull [OPTIONS] NAME[:TAG|@DIGEST]\n[root@localhost ~]# docker pull hello-world Using default tag: latest latest: Pulling from library/hello-world 0e03bdcc26d7: Pull complete Digest: sha256:e7c70bb24b462baa86c102610182e3efcb12a04854e8c582838d92970a09f323 Status: Downloaded newer image for hello-world:latest docker.io/library/hello-world:latest 查看镜像\ndocker images [OPTIONS] [REPOSITORY[:TAG]]\n[root@localhost ~]# docker images REPOSITORY TAG IMAGE ID CREATED SIZE hello-world latest bf756fb1ae65 11 months ago 13.3kB 运行镜像\ndocker run [OPTIONS] IMAGE [COMMAND] [ARG...]\n[root@localhost ~]# docker run hello-world Hello from Docker! This message shows that your installation appears to be working correctly. To generate this message, Docker took the following steps: 1. The Docker client contacted the Docker daemon. 2. The Docker daemon pulled the \u0026#34;hello-world\u0026#34; image from the Docker Hub. (amd64) 3. The Docker daemon created a new container from that image which runs the executable that produces the output you are currently reading. 4. The Docker daemon streamed that output to the Docker client, which sent it to your terminal. To try something more ambitious, you can run an Ubuntu container with: $ docker run -it ubuntu bash Share images, automate workflows, and more with a free Docker ID: https://hub.docker.com/ For more examples and ideas, visit: https://docs.docker.com/get-started/ 运行 Nginx 容器 拉取 Nginx 镜像\n[root@localhost ~]# docker pull hub.c.163.com/library/nginx:latest latest: Pulling from library/nginx 5de4b4d551f8: Pull complete d4b36a5e9443: Pull complete 0af1f0713557: Pull complete Digest: sha256:f84932f738583e0169f94af9b2d5201be2dbacc1578de73b09a6dfaaa07801d6 Status: Downloaded newer image for hub.c.163.com/library/nginx:latest hub.c.163.com/library/nginx:latest 运行 Nginx 容器\n[root@localhost ~]# docker run -d hub.c.163.com/library/nginx:latest 0708d1119dfedcd40371935d901fc460682d6eb8cc0177b510b47eaa8ffeb506 登入容器\n[root@localhost ~]# docker exec -it 0708 bash root@0708d1119dfe:/# root@0708d1119dfe:/# which nginx /usr/sbin/nginx 停止容器\ndocker stop [OPTIONS] CONTAINER [CONTAINER...]\n[root@localhost ~]# docker stop 0708 0708 使用端口映射\n[root@localhost ~]# docker run -d -p 8080:80 hub.c.163.com/library/nginx:latest 8dc5cef54fcf00dc4f5e428d1de14c8efe6c867b9e2f4870078d99bf3c0e0cef -p, --publish list Publish a container's port(s) to the host -P, --publish-all\tPublish all exposed ports to random ports\n查看进行端口映射的容器信息\n[root@localhost ~]# docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 8dc5cef54fcf hub.c.163.com/library/nginx:latest \u0026#34;nginx -g \u0026#39;daemon of…\u0026#34; About a minute ago Up About a minute 0.0.0.0:8080-\u0026gt;80/tcp dreamy_ritchie [root@localhost ~]# netstat -na| grep 8080 tcp6 0 0 :::8080 :::* LISTEN 在浏览器访问 Nginx\nDockerfile 简单的 Dockerfile 编写 Dockerfile\nFROM alpine:latest MAINTAINER xianglin CMD echo \u0026#39;hello my dockerfile\u0026#39; 构建镜像\n[root@localhost ~]# docker build -t hello-docker . Sending build context to Docker daemon 15.36kB Step 1/3 : FROM alpine:latest ---\u0026gt; d6e46aa2470d Step 2/3 : MAINTAINER xianglin ---\u0026gt; Using cache ---\u0026gt; 5b2571b3eae2 Step 3/3 : CMD echo \u0026#39;hello my dockerfile\u0026#39; ---\u0026gt; Using cache ---\u0026gt; 465286d0513b Successfully built 465286d0513b Successfully tagged hello-docker:latest 查看镜像、运行容器\n[root@localhost ~]# docker images REPOSITORY TAG IMAGE ID CREATED SIZE hello-docker latest 465286d0513b 41 seconds ago 5.57MB alpine latest d6e46aa2470d 5 weeks ago 5.57MB hello-world latest bf756fb1ae65 11 months ago 13.3kB hub.c.163.com/library/nginx latest 46102226f2fd 3 years ago 109MB # hello-docker 基于 alpine 构建 [root@localhost ~]# docker run hello-docker hello my dockerfile 常用软件工具的 docker 命令 https://hub.docker.com/\n在 Docker 实例中安装软件包\n# 登入容器 docker exec -it 7ec9e6cf98a9 bash # 替换源 sed -i s@/deb.debian.org/@/mirrors.163.com/@g /etc/apt/sources.list # 更新源、安装软件 apt update apt install vim MySQL\ndocker run --name mysql-dev -e MYSQL_ROOT_PASSWORD=12345678 -e MYSQL_DATABASE=dev -e MYSQL_USER=dev -e MYSQL_PASSWORD=password -p 3306:3306 -d mysql:latest 禅道\nmkdir -p /data/zbox \u0026amp;\u0026amp; \\ docker run -d -p 80:80 -p 3306:3306 \\ -e ADMINER_USER=\u0026#34;root\u0026#34; -e ADMINER_PASSWD=\u0026#34;password\u0026#34; \\ -e BIND_ADDRESS=\u0026#34;false\u0026#34; \\ -v /data/zbox/:/opt/zbox/ \\ --name zentao-server \\ idoop/zentao:latest Jenkins\nmkdir -p /data/jenkins \u0026amp;\u0026amp; \\ docker run -d -p 8080:8080 -p 50000:50000 \\ -v /data/jenkins:/var/jenkins_home \\ --restart always \\ jenkins/jenkins:lts-jdk11 GitLab\n中文教程\ndocker run --detach \\ --hostname gitlab.example.com \\ --publish 443:443 --publish 80:80 --publish 22:22 \\ --name gitlab \\ --restart always \\ --volume /data/gitlab/config:/etc/gitlab \\ --volume /data/gitlab/logs:/var/log/gitlab \\ --volume /data/gitlab/data:/var/opt/gitlab \\ --shm-size 256m \\ gitlab/gitlab-ee:latest MongoDB\n参见：Docker版MongoDB的安装\nmkdir -p .docker/data/mongodb \u0026amp;\u0026amp; \\ docker run -d -p 27017:27017 -v ~/.docker/data/mongodb:/data/db -e MONGO_INITDB_ROOT_PASSWORD=mongomongo -e MONGO_INITDB_ROOT_USERNAME=mongo --name mongo mongo # 进入容器中的 MongoDB 命令行 docker exec -it mongo mongo admin # 创建管理员账户 db.createUser({ user: \u0026#39;mongo\u0026#39;, pwd: \u0026#39;mongomongo\u0026#39;, roles: [ { role: \u0026#34;root\u0026#34;, db: \u0026#34;admin\u0026#34; } ] }); Redis\ndocker run --name some-redis -d -p 6379:6379 redis ","permalink":"https://2d992714.xianglin2020-github-io.pages.dev/posts/2020/docker-basics/","summary":"Docker 是一个用来装程序及其环境的容器，属于Linux 容器的一种封装，提供简单易用的容器使用接口。它是目前最流行的 Linux 容器解决方案。","title":"Docker基础"},{"content":"分布式基础 分布式含义 权威定义 利用物理架构形成多个自治的处理元素，不共享主内存，但是通过发送信息合作。\n分布式作用 分布式的好处 增大系统容器 加强系统可用性 因为模块化，所有系统模块重用度更高 因为软件服务模块被拆分，开发和发布速度可以并行而变得快 系统扩展性更高 团队协作、技术升级更容易 分布式与单体结构对比 单体架构 分布式架构 新人的学习成本 业务逻辑成本高 架构逻辑成本高 部署、运维 容易 发布频繁、发布顺序复杂、运维难 隔离性 一损俱损，殃及鱼池 故障影响范围小 架构设计 难度低 难度巨大 系统性能 响应快、吞吐量小 响应慢、吞吐量大 测试成本 低 很高 技术多样性 技术单一且封闭 技术多样且开放 系统扩展性 扩展性差 扩展性很好 系统管理成本 成本低 成本高 CAP 定理 CAP 的重要性 C（Consistency，一致性）：读操作是否总能读到前一个写操作的结果 A（Availability，可用性）：非故障节点应该在合理的时间内做出合理的响应 P（Partition tolerance，分区容错性）：当出现网络分区现象后，系统能够继续运行 集群、分布式、微服务的区别 集群和分布式 分布式：一个业务拆分为多个子业务，部署在不同的服务器上 集群：同一个业务，部署在多个服务器上 集群和微服务 集群：分散压力 微服务：分散能力 微服务和分布式 微服务是架构设计方式 分布式是系统部署方法 ","permalink":"https://2d992714.xianglin2020-github-io.pages.dev/posts/2020/distributed-basics/","summary":"\u003ch1 id=\"分布式基础\"\u003e分布式基础\u003c/h1\u003e\n\u003ch2 id=\"分布式含义\"\u003e分布式含义\u003c/h2\u003e\n\u003ch3 id=\"权威定义\"\u003e权威定义\u003c/h3\u003e\n\u003cp\u003e利用物理架构形成多个自治的处理元素，不共享主内存，但是通过发送信息合作。\u003c/p\u003e\n\u003ch2 id=\"分布式作用\"\u003e分布式作用\u003c/h2\u003e\n\u003ch3 id=\"分布式的好处\"\u003e分布式的好处\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e增大系统容器\u003c/li\u003e\n\u003cli\u003e加强系统可用性\u003c/li\u003e\n\u003cli\u003e因为模块化，所有系统模块重用度更高\u003c/li\u003e\n\u003cli\u003e因为软件服务模块被拆分，开发和发布速度可以并行而变得快\u003c/li\u003e\n\u003cli\u003e系统扩展性更高\u003c/li\u003e\n\u003cli\u003e团队协作、技术升级更容易\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"分布式与单体结构对比\"\u003e分布式与单体结构对比\u003c/h2\u003e\n\u003ctable\u003e\n\t\u003cthead\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003cth\u003e\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003e单体架构\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003e分布式架构\u003c/th\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/thead\u003e\n\t\u003ctbody\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e新人的学习成本\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e业务逻辑成本高\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e架构逻辑成本高\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e部署、运维\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e容易\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e发布频繁、发布顺序复杂、运维难\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e隔离性\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e一损俱损，殃及鱼池\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e故障影响范围小\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e架构设计\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e难度低\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e难度巨大\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e系统性能\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e响应快、吞吐量小\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e响应慢、吞吐量大\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e测试成本\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e低\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e很高\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e技术多样性\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e技术单一且封闭\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e技术多样且开放\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e系统扩展性\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e扩展性差\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e扩展性很好\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e系统管理成本\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e成本低\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e成本高\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch2 id=\"cap-定理\"\u003eCAP 定理\u003c/h2\u003e\n\u003ch3 id=\"cap-的重要性\"\u003eCAP 的重要性\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003eC（Consistency，一致性）：读操作是否总能读到前一个写操作的结果\u003c/li\u003e\n\u003cli\u003eA（Availability，可用性）：非故障节点应该在合理的时间内做出合理的响应\u003c/li\u003e\n\u003cli\u003eP（Partition tolerance，分区容错性）：当出现网络分区现象后，系统能够继续运行\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"https://cdn.jsdelivr.net/gh/xianglin2020/gallery@master/202012/222343.png\" alt=\"image-20201201222342807\"  /\u003e\n\u003c/p\u003e","title":"分布式基础"},{"content":"ThreadLocal ThreadLocal 典型应用场景 场景介绍：资源持有、线程安全、线程一致、并发计算\n线程独享对象 每个 Thread 内有自己的实例副本，各个线程间不共享。 线程保存全局变量（资源持有） 避免参数在各个方法之间传递 ThreadLocal 主要 API ThreadLocal\u0026lt;T\u0026gt;() 构造函数\ninitialValue 返回当前线程对应的“初始值”，是一个延迟加载的方法，只有在调用get的时候，才会触发\n一个线程只需要调用一次此方法，可以调用remove方法删除后在调用initialValue\nset / get 为这个线程设置一个新值\n获取保存的值，如果为空，则调用initialValue方法初始化\nremove 回收值\nThreadLocal 基本原理 ThreadLocal 在 Java 中使用哈希表实现。\nThreadLocal 使用的注意点 内存泄漏 某个对象不再被使用，但占用的内存无法被回收。 主动调用remove方法避免内存泄漏 ","permalink":"https://2d992714.xianglin2020-github-io.pages.dev/posts/2020/threadlocal-basics/","summary":"\u003ch2 id=\"threadlocal\"\u003eThreadLocal\u003c/h2\u003e\n\u003ch3 id=\"threadlocal-典型应用场景\"\u003eThreadLocal 典型应用场景\u003c/h3\u003e\n\u003cp\u003e场景介绍：资源持有、线程安全、线程一致、并发计算\u003c/p\u003e\n\u003ch4 id=\"线程独享对象\"\u003e线程独享对象\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e每个 Thread 内有自己的实例副本，各个线程间不共享。\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"线程保存全局变量资源持有\"\u003e线程保存全局变量（资源持有）\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e避免参数在各个方法之间传递\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"threadlocal-主要-api\"\u003eThreadLocal 主要 API\u003c/h3\u003e\n\u003ch4 id=\"threadlocalt\"\u003e\u003ccode\u003eThreadLocal\u0026lt;T\u0026gt;()\u003c/code\u003e\u003c/h4\u003e\n\u003cp\u003e构造函数\u003c/p\u003e","title":"ThreadLocal 基础"},{"content":"线程池学习 线程池概念 使用线程池的优势 线程池是一种基于池化管理线程的工具，经常出现在多线程服务器中。既避免处理任务时创建销毁线程的开销，也避免了线程数据膨胀导致过分调度的问题。使用线程池的好处有：\n降低资源消耗：通过池化技术重复利用已创建的线程，降低线程创建和销毁造成的损耗。 提高响应速度：任务到达时，无需等待线程创建即可执行。 提高线程的可管理性：线程是稀缺资源，无限制的创建既会销毁系统资源，也会导致调度失衡，减低系统稳定性。使用线程池可以进行统一的分配、调优和监控。 提供更多更强大的功能：基于线程池提供的钩子函数，可以扩展线程池的行为。 线程池的设计与实现 继承关系及类作用介绍 Executor Executor 接口提供了一种思想：将任务提交和任务执行分离。用户无需关注线程的创建、调度和任务执行过程，只需要将表示任务的 Runnable 对象提交到执行器 Executor 中，由 Executor 完成线程的调配和任务的执行部分。\nExecutorService ExecutorService 接口增加了一些能力：\n扩展执行任务的能力：补充可以为一个或一批异步任务生成 Future 的方法。 提供了管理线程池的方法：停止线程池、查看任务数量等。 AbstractExecutorService AbstractExecutorService 是上层抽象类，将任务执行的流程串联起来，保证下层的实现只需要关注一个执行任务的方法即可。\nThreadPoolExecutor ThreadPoolExecutor 一方面维护自身的生命周期，另一方面管理线程和任务。\n线程池在内部构建了一个生产者消费者模型，将线程和任务两者解耦，并不直接关联，从而良好的缓冲任务，复用线程。线程池的运行主要分为两部分：任务管理和线程管理。\n任务管理充当生产者角色，当任务提交后，线程会判断该任务的后续流转：（1）直接申请线程执行该任务；（2）缓冲到队列中等待线程执行；（3）拒绝该任务。\n线程管理部分是消费者，它们被维护在线程池内，根据任务请求进行线程的分配，当线程执行完任务后则继续获取新的任务去执行，最终当线程获取不到任务时就会被回收。\n线程池生命周期管理 线程池内部使用一个变量来维护两个值：运行状态（runState）和线程数量（workCount）：\nprivate final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0)); 高 3 位保存 runState，低 29 位保存 workCount，用一个变量存储两个值，可以避免使用锁维护两者的一致。\n线程池的运行状态有 5 种，分别是：\n状态 含义 RUNNING 能接收新任务，也能处理排队任务 SHUTWODN 关闭状态，不接受新任务，但处理排队任务 STOP 不接受新任务，不处理排队任务，并中断正在进行的任务 TIDYING 所有任务都已经终止，workerCount 为零，并调用terminated钩子方法 TERMINATED terminated()方法执行完成后进入该状态 其生命周期转换关系如下：\n任务执行机制 所有任务的调度都是由 execute 方法完成的，这部分完成的工作是：检查现在线程池的运行状态、运行线程数、运行策略，决定接下来执行的流程，是直接申请线程执行，或是缓冲到队列中执行，亦或是直接拒绝该任务。其执行流程如下：\n首先检测线程池运行状态，如果不是 RUNNING ，则直接拒绝，线程池要保证在 RUNNING 状态下执行任务； 如果 workerCount \u0026lt; corePoolSize，则创建并启动一个线程来执行新提交的任务； 如果 workerCount \u0026gt;= corePoolSize，且线程池内的阻塞队列未满，则将该任务添加到阻塞队列中； 如果 workCount \u0026gt;= corePoolSize \u0026amp;\u0026amp; workerCount \u0026lt; maximumPoolSize，且线程池内的阻塞队列已满，则创建一个新线程来执行新提交的任务； 如果 workerCount \u0026gt;= maximumPoolSize，并且线程池内的阻塞队列已满，则根据拒绝策略来处理该任务，默认的处理方式是直抛出异常。 其执行流程如图所示：\n任务拒绝模块是线程池的保护部分，线程池有一个最大的容量，当线程池的任务缓存队列已满，并且线程池中的线程数达到 maximumPoolSize 时，就需要拒绝掉该任务，采取任务拒绝策略，保护线程池。\n拒绝策略是一个接口，设计如下：\npublic interface RejectedExecutionHandler { void rejectedExecution(Runnable r, ThreadPoolExecutor executor); } 用户可以通过实现这个接口定制拒绝策略，也可以选择 JDK 提供的四种拒绝策略：\nAbortPolicy：抛出RejectedExecutionException\npublic void rejectedExecution(Runnable r, ThreadPoolExecutor e) { throw new RejectedExecutionException(\u0026#34;Task \u0026#34; + r.toString() + \u0026#34; rejected from \u0026#34; + e.toString()); } DiscardPolicy：什么也不做，直接忽略\npublic void rejectedExecution(Runnable r, ThreadPoolExecutor e) { } DiscardOldestPolicy：丢弃执行队列中最老的任务，尝试为当前提交的任务腾出位置\npublic void rejectedExecution(Runnable r, ThreadPoolExecutor e) { if (!e.isShutdown()) { e.getQueue().poll(); e.execute(r); } } CallerRunsPolicy：直接由提交任务者执行这个任务\npublic void rejectedExecution(Runnable r, ThreadPoolExecutor e) { if (!e.isShutdown()) { r.run(); } } 线程池的默认拒绝策略是：\nprivate static final RejectedExecutionHandler defaultHandler = new AbortPolicy(); Worker 线程管理 线程池为了掌握线程的状态并维护线程的生命周期，设计了线程池内部工作线程 Worker，部分代码如下：\nprivate final class Worker extends AbstractQueuedSynchronizer implements Runnable{ // worker 持有的线程 final Thread thread; // 初始化的任务 Runnable firstTask; public void run() { runWorker(this); } } Worker 线程实现了Runnable 接口，并持有一个线程 thread，一个初始化任务 firstTask。thread 是在调用构造方式时通过 ThreadFactory 来创建的线程，可以用来执行任务；firstTask 用来保存传入的第一个任务。\n线程池需要管理线程的生命周期，需要在线程长时间不允许的时候进行回收。线程池使用 HashSet 持有线程的引用，这样可以通过添加引用、移除引用的操作来控制线程的生命周期。\nWorker 通过继承 AQS，使用 AQS 来实现独占锁这个功能。\nlock 方法一旦获取了独占锁，表示当前线程正在执行任务中。 如果正在执行任务，则不应该中断线程，如果该线程现在不是独占锁状态，也就是空闲状态，说明它没有在处理任务，这时可以对该线程进行中断。 线程池在执行 shutdown 方法或 tryTerminate 方法时会调用 interruptIdleWorkers 方法来中断空闲线程，interruptIdleWorkers 方法会使用 tryLock 方法来判断线程池中的线程是否是空闲状态，如果线程是空闲状态则可以安全回收。 增加线程是通过 addWorker 方法完成的。addWorker 方法有两个参数：firstTask、core 。firstTask 参数用于指定新增的线程执行的第一个任务，该参数可以为空；core 参数为 true 时表示新增线程时会判断当前活动的线程数是否少于 corePoolSize ，false 表示新增线程前需要判断当前活动线程数是否少于 maximumPoolSize ，其增加流程如下：\nWorker 类中的 run() 方法调用了 runWorker() 方法来执行任务，其执行过程如下：\n循环通过 getTask() 方法获取任务； 如果线程池正在停止，那么保证当前线程是中断状态，否则保证当前线程不是中断状态； 执行任务。 获取不到任务时，执行 processWorkerExit() 方法主动销毁线程。 线程池中线程的销毁依赖 JVM 自动的回收，线程池做的工作就是根据当前线程池状维护一定数量的线程引用，防止这部分线程被 JVM 回收，当线程池决定哪些线程需要回收时，只需要将其引用消除即可。Worker 被创建出来后，就会不断循环获取任务执行，核心线程可以无限等待获取任务，非核心线程要限时获取任务。当 Worker 无法获取到任务时，循环会结束，Worker 会主动消除自身在线程池内的引用。\nfinal void runWorker(Worker w) { try { while (task != null || (task = getTask()) != null) { // 执行任务 } } finally { // 获取不到任务时，主动回收自己 processWorkerExit(w, completedAbruptly); } } private void processWorkerExit(Worker w, boolean completedAbruptly) { final ReentrantLock mainLock = this.mainLock; mainLock.lock(); try { completedTaskCount += w.completedTasks; // 将线程引用移除线程池 workers.remove(w); } finally { mainLock.unlock(); } } 创建和停止线程池 线程池的构造器的参数 参数名 类型 含义 corePoolSize int 核心线程数 maximumPoolSize int 最大线程数 keepAliveTime long 保持存活时间 workQueue BlockingQueue 任务存储队列 threadFactory ThreadFactory 使用 threadFactory 创建新的线程 handler RejectedExecutionHandler 拒绝策略 corePoolSize：指的是核心线程数。\nmaxPoolSize：指的是最大线程数。\n如果线程数小于corePoolSize，创建一个新的线程来运行新任务； 如果线程数等于或大于corePoolSize但小于maximumPoolSize，则将任务添加到任务队列中； 如果队列已满，且线程数小于maximumPoolSize，则创建新线程； 如果队列已满，且线程数达到maximumPoolSize，则调用handler执行拒绝策略。 keepAliveTime：如果线程池当前的线程数多于corePoolSize，那么多余的线程闲置超过指定时间会被终止。\nthreadFactory：创建线程的工厂。\n默认线程池创建的线程的属性为：\n属于同一个ThreadGroup 线程池和线程的名称按序号递增 不是 daemon 线程 线程的优先级为NORM_PRIORITY private final String namePrefix = \u0026#34;pool-\u0026#34; + poolNumber.getAndIncrement() + \u0026#34;-thread-\u0026#34;; public Thread newThread(Runnable r) { Thread t = new Thread(group, r, namePrefix + threadNumber.getAndIncrement(), 0); if (t.isDaemon()) t.setDaemon(false); if (t.getPriority() != Thread.NORM_PRIORITY) t.setPriority(Thread.NORM_PRIORITY); return t; } workQueue：任务队列\n直接交接：SynchronousQueue 无界队列：LinkedBlockingQueue 有界队列：ArrayBlockingQueue Executors创建线程池 方法名 功能 缺点 newFixedThreadPool 创建固定大小的线程池 容易造成大量内存占用，导致 OOM newSingleThreadExecutor 创建只有一个线程的线程池 当请求堆积的时候，占用大量内存 newCachedThreadPool 创建一个不设线程上限的线程池，任何任务都将立即执行 创建数量非常多的线程，导致 OOM newFixedThreadPool\npublic static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory){ return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue\u0026lt;Runnable\u0026gt;(), threadFactory); } newSingleThreadExecutor\npublic static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) { return new FinalizableDelegatedExecutorService (new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue\u0026lt;Runnable\u0026gt;(), threadFactory)); } newCachedThreadPool\npublic static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) { return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue\u0026lt;Runnable\u0026gt;(), threadFactory); } 线程池的线程数量如何设定 CPU 密集型（加密、计算 hash）：最佳线程数为CPU 核心数的 1-2 倍左右； 耗时 IO 型（读写数据库、文件、网络）：最大线程数一般会大于 CPU 核心数很多倍。 线程数 = CPU 核心数 * (1 + 平均等待时间 / 平均工作时间) 停止线程池的方法 shutdown\n将线程池的状态置为SHUTDOWN 调用此方法后，不允许继续提交任务，即调用指定的拒绝策略拒绝任务 所有在调用此方法前提交的任务都会被执行 所有任务被执行完毕，ExecutorService才会真正关闭 shutdownNow\n将线程池的状态置为STOP 使用中断操作尝试停止运行中的任务 返回未尚未执行的任务 isShutdown：线程池是否关闭\nisTerminated：判断线程池关闭后所有的任务是否都执行完了\nawaitTermination：阻塞，直到出现以下情况\nshutdown调用后所有任务执行完成 超时返回 当前线程中断 线程池钩子beforeExecute 实现一个可以暂停的线程池\npublic class PauseableThreadPool extends ThreadPoolExecutor { public PauseableThreadPool(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue\u0026lt;Runnable\u0026gt; workQueue) { super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue); } private boolean isPaused; private final Lock lock = new ReentrantLock(); private final Condition condition = lock.newCondition(); @Override protected void beforeExecute(Thread t, Runnable r) { lock.lock(); try { while (isPaused) { condition.await(); } } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } } private void pause() { lock.lock(); try { isPaused = true; } finally { lock.unlock(); } } private void resume() { lock.lock(); try { isPaused = false; condition.signalAll(); } finally { lock.unlock(); } } } ","permalink":"https://2d992714.xianglin2020-github-io.pages.dev/posts/2020/java-thread-pool/","summary":"线程池是一种基于池化管理线程的工具，经常出现在多线程服务器中。既避免处理任务时创建销毁线程的开销，也避免了线程数据膨胀导致过分调度的问题。","title":"Java 线程池学习"},{"content":"Spring 事务管理 事务的基础概念 什么是事务 事务是指逻辑上的一组操作，要么一起成功、要么一起失败。 事务的特性 原子性：原子性是指事务是一个不可分割的工作单位，事务中的操作要么都发生，要么都不发生。 一致性：一致性指事务前后数据的完整性必须保持一致。 隔离性：隔离性是指多个用户并发访问数据库时，一个用户的事务不能被其他用户的事务所干扰，多个并发事务之间数据要相互隔离。 持久性：持久性是指一个事务一旦被提交，它对数据库中数据的改变就是永久性的，即使数据库发生故障也不应该对其有影响。 事务隔离级别 READ_UNCOMMITED：允许读取还未提交的数据，可能导致脏读、不可重复读、幻读。 READ_COMMITTED：允许读取并发事务已提交的数据，可防止脏读，但不可重复读、幻读仍可能发生。 REPEATABLE_READ： 在统一事务中读取的数据保持一致，即使其他事务对其修改并提交。 SERIALIZABLE：将并发事务串行化，解决一切问题。 Spring 接口介绍 三个抽象接口 事务管理器：PlatformTransactionManager 事务定义信息（隔离级别、传播行为、超时、只读）：TransactionDefinition 事务运行状态：TransactionStatus FlatformTransactionManager TranscationDefinition 事务的传播行为 事务传播行为 描述 PROPAGATION_REQUIRED 支持当前事务，如果不存在就新建一个 PROPAGATION_SUPPORTS 支持当前事务，如果不存在就不使用事务 PROPAGATION_MANDATORY 支持当前事务，如果不存在则抛出异常 PROPAGATION_REQUIRES_NEW 创建一个新事务，如果有事务存在则挂起 PROPAGATION_NOT_SUPPORTED 以非事务方式运行，如果有事务存在则挂起 PROPAGATION_NEVER 以非事务方式运行，如果存在事务则抛出异常 PROPAGATION_NESTED 如果当前事务存在，则嵌套事务执行 TransactionStatus Spring 事务管理操作 准备转账案例 表设计 CREATE TABLE `account` ( `id` int(11) NOT NULL, `name` varchar(50) NOT NULL, `money` decimal(10, 2) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE = InnoDB DEFAULT CHARSET = utf8; INSERT INTO imooc_demo.account (id, name, money) VALUES (1, \u0026#39;a\u0026#39;, 1000.00); INSERT INTO imooc_demo.account (id, name, money) VALUES (2, \u0026#39;b\u0026#39;, 1000.00); INSERT INTO imooc_demo.account (id, name, money) VALUES (3, \u0026#39;c\u0026#39;, 1000.00); 实现 DAO 层 public interface AccountDao { void accountAdd(String name, BigDecimal decimal); void accountSub(String name, BigDecimal decimal); } public class AccountDaoImpl implements AccountDao { private JdbcTemplate jdbcTemplate; public void setJdbcTemplate(JdbcTemplate jdbcTemplate) { this.jdbcTemplate = jdbcTemplate; } @Override public void accountAdd(String name, BigDecimal decimal) { String sql = \u0026#34;update account set money = money + ? where name = ?\u0026#34;; this.jdbcTemplate.update(sql, decimal, name); } @Override public void accountSub(String name, BigDecimal decimal) { String sql = \u0026#34;update account set money = money - ? where name = ?\u0026#34;; this.jdbcTemplate.update(sql, decimal, name); } } 实现 Service 层 public interface AccountService { void transfer(String from, String to, BigDecimal money); } public class AccountServiceImpl implements AccountService { private AccountDao accountDao; public void setAccountDao(AccountDao accountDao) { this.accountDao = accountDao; } @Override public void transfer(String from, String to, BigDecimal money) { accountDao.accountSub(from, money); accountDao.accountAdd(to, money); } } 使用 XML 方式声明对象 \u0026lt;bean id=\u0026#34;dataSource\u0026#34; class=\u0026#34;org.springframework.jdbc.datasource.DriverManagerDataSource\u0026#34;\u0026gt; \u0026lt;property name=\u0026#34;driverClassName\u0026#34; value=\u0026#34;com.mysql.cj.jdbc.Driver\u0026#34;/\u0026gt; \u0026lt;property name=\u0026#34;url\u0026#34; value=\u0026#34;jdbc:mysql://hostip:port/dbname\u0026#34;/\u0026gt; \u0026lt;property name=\u0026#34;username\u0026#34; value=\u0026#34;name\u0026#34;/\u0026gt; \u0026lt;property name=\u0026#34;password\u0026#34; value=\u0026#34;password\u0026#34;/\u0026gt; \u0026lt;/bean\u0026gt; \u0026lt;bean id=\u0026#34;jdbcTemplate\u0026#34; class=\u0026#34;org.springframework.jdbc.core.JdbcTemplate\u0026#34;\u0026gt; \u0026lt;property name=\u0026#34;dataSource\u0026#34; ref=\u0026#34;dataSource\u0026#34;/\u0026gt; \u0026lt;/bean\u0026gt; \u0026lt;bean id=\u0026#34;accountDao\u0026#34; class=\u0026#34;dao.AccountDaoImpl\u0026#34;\u0026gt; \u0026lt;property name=\u0026#34;jdbcTemplate\u0026#34; ref=\u0026#34;jdbcTemplate\u0026#34;/\u0026gt; \u0026lt;/bean\u0026gt; \u0026lt;bean id=\u0026#34;accountService\u0026#34; class=\u0026#34;service.AccountServiceImpl\u0026#34;\u0026gt; \u0026lt;property name=\u0026#34;accountDao\u0026#34; ref=\u0026#34;accountDao\u0026#34;/\u0026gt; \u0026lt;/bean\u0026gt; 测试转账效果 @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(\u0026#34;classpath:applicationContext.xml\u0026#34;) public class AccountServiceImplTest { @Resource private AccountService accountService; @Test public void transfer() { accountService.transfer(\u0026#34;a\u0026#34;, \u0026#34;b\u0026#34;, BigDecimal.valueOf(200)); } } 编程式的事务管理 配置事务管理器\n\u0026lt;bean id=\u0026#34;transactionManager\u0026#34; class=\u0026#34;org.springframework.jdbc.datasource.DataSourceTransactionManager\u0026#34;\u0026gt; \u0026lt;property name=\u0026#34;dataSource\u0026#34; ref=\u0026#34;dataSource\u0026#34;/\u0026gt; \u0026lt;/bean\u0026gt; \u0026lt;!-- 用于简化事务管理的代码 --\u0026gt; \u0026lt;bean id=\u0026#34;transactionTemplate\u0026#34; class=\u0026#34;org.springframework.transaction.support.TransactionTemplate\u0026#34;\u0026gt; \u0026lt;property name=\u0026#34;transactionManager\u0026#34; ref=\u0026#34;transactionManager\u0026#34;/\u0026gt; \u0026lt;/bean\u0026gt; 在业务层中注入事务管理模板 public class AccountServiceImpl implements AccountService { private TransactionTemplate transactionTemplate; private AccountDao accountDao; // ... public void setTransactionTemplate(TransactionTemplate transactionTemplate) { this.transactionTemplate = transactionTemplate; } // ... } \u0026lt;bean id=\u0026#34;accountService\u0026#34; class=\u0026#34;person.xiangln.transaction.service.AccountServiceImpl\u0026#34;\u0026gt; \u0026lt;property name=\u0026#34;accountDao\u0026#34; ref=\u0026#34;accountDao\u0026#34;/\u0026gt; \u0026lt;property name=\u0026#34;transactionTemplate\u0026#34; ref=\u0026#34;transactionTemplate\u0026#34;/\u0026gt; \u0026lt;/bean\u0026gt; 使用编程式事务 public void transfer(String from, String to, BigDecimal money) { transactionTemplate.executeWithoutResult(transactionStatus -\u0026gt; { accountDao.accountSub(from, money); int a = 1 / 0; accountDao.accountAdd(to, money); }); } 声明式事务管理 基于 Aspectj 的 XML 配置 \u0026lt;bean id=\u0026#34;transactionManager\u0026#34; class=\u0026#34;org.springframework.jdbc.datasource.DataSourceTransactionManager\u0026#34;\u0026gt; \u0026lt;property name=\u0026#34;dataSource\u0026#34; ref=\u0026#34;dataSource\u0026#34;/\u0026gt; \u0026lt;/bean\u0026gt; \u0026lt;tx:advice id=\u0026#34;txAdvice\u0026#34;\u0026gt; \u0026lt;tx:attributes\u0026gt; \u0026lt;tx:method name=\u0026#34;transfer\u0026#34;/\u0026gt; \u0026lt;/tx:attributes\u0026gt; \u0026lt;/tx:advice\u0026gt; \u0026lt;aop:config \u0026gt; \u0026lt;aop:pointcut id=\u0026#34;pointcut1\u0026#34; expression=\u0026#34;execution(* person.xiangln.transaction.service.AccountService+.*(..))\u0026#34;/\u0026gt; \u0026lt;aop:advisor advice-ref=\u0026#34;txAdvice\u0026#34; pointcut-ref=\u0026#34;pointcut1\u0026#34;/\u0026gt; \u0026lt;/aop:config\u0026gt; 基于注解的事务管理 \u0026lt;!-- 基于注解的事务配置 --\u0026gt; \u0026lt;bean id=\u0026#34;transactionManager\u0026#34; class=\u0026#34;org.springframework.jdbc.datasource.DataSourceTransactionManager\u0026#34;\u0026gt; \u0026lt;property name=\u0026#34;dataSource\u0026#34; ref=\u0026#34;dataSource\u0026#34;/\u0026gt; \u0026lt;/bean\u0026gt; \u0026lt;!--开启注解事务--\u0026gt; \u0026lt;tx:annotation-driven/\u0026gt; @Override @Transactional public void transfer(String from, String to, BigDecimal money) { accountDao.accountSub(from, money); int a = 1 / 0; accountDao.accountAdd(to, money); } ","permalink":"https://2d992714.xianglin2020-github-io.pages.dev/posts/2020/spring-transaction/","summary":"\u003ch1 id=\"spring-事务管理\"\u003eSpring 事务管理\u003c/h1\u003e\n\u003ch2 id=\"事务的基础概念\"\u003e事务的基础概念\u003c/h2\u003e\n\u003ch3 id=\"什么是事务\"\u003e什么是事务\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e事务是指逻辑上的一组操作，要么一起成功、要么一起失败。\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"事务的特性\"\u003e事务的特性\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e原子性：原子性是指事务是一个不可分割的工作单位，事务中的操作要么都发生，要么都不发生。\u003c/li\u003e\n\u003cli\u003e一致性：一致性指事务前后数据的完整性必须保持一致。\u003c/li\u003e\n\u003cli\u003e隔离性：隔离性是指多个用户并发访问数据库时，一个用户的事务不能被其他用户的事务所干扰，多个并发事务之间数据要相互隔离。\u003c/li\u003e\n\u003cli\u003e持久性：持久性是指一个事务一旦被提交，它对数据库中数据的改变就是永久性的，即使数据库发生故障也不应该对其有影响。\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"事务隔离级别\"\u003e事务隔离级别\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ccode\u003eREAD_UNCOMMITED\u003c/code\u003e：允许读取还未提交的数据，可能导致脏读、不可重复读、幻读。\u003c/li\u003e\n\u003c/ul\u003e\n\u003col\u003e\n\u003cli\u003e\u003ccode\u003eREAD_COMMITTED\u003c/code\u003e：允许读取并发事务已提交的数据，可防止脏读，但不可重复读、幻读仍可能发生。\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003eREPEATABLE_READ\u003c/code\u003e： 在统一事务中读取的数据保持一致，即使其他事务对其修改并提交。\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003eSERIALIZABLE\u003c/code\u003e：将并发事务串行化，解决一切问题。\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"spring-接口介绍\"\u003eSpring 接口介绍\u003c/h2\u003e\n\u003ch3 id=\"三个抽象接口\"\u003e三个抽象接口\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e事务管理器：\u003ccode\u003ePlatformTransactionManager\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e事务定义信息（隔离级别、传播行为、超时、只读）：\u003ccode\u003eTransactionDefinition\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e事务运行状态：\u003ccode\u003eTransactionStatus\u003c/code\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"flatformtransactionmanager\"\u003e\u003ccode\u003eFlatformTransactionManager\u003c/code\u003e\u003c/h3\u003e\n\u003cimg src=\"https://cdn.jsdelivr.net/gh/xianglin2020/gallery@master/202011/112045.png\" alt=\"PlatformTransactionManager\" style=\"zoom:50%;\" /\u003e\n\u003ch3 id=\"transcationdefinition\"\u003e\u003ccode\u003eTranscationDefinition\u003c/code\u003e\u003c/h3\u003e\n\u003cimg src=\"https://cdn.jsdelivr.net/gh/xianglin2020/gallery@master/202011/111550.png\" alt=\"transactiondefinition\" style=\"zoom:50%;\" /\u003e\n\u003ch4 id=\"事务的传播行为\"\u003e事务的传播行为\u003c/h4\u003e\n\u003ctable\u003e\n\t\u003cthead\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003cth\u003e事务传播行为\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003e描述\u003c/th\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/thead\u003e\n\t\u003ctbody\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003ccode\u003ePROPAGATION_REQUIRED\u003c/code\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e支持当前事务，如果不存在就新建一个\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003ccode\u003ePROPAGATION_SUPPORTS\u003c/code\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e支持当前事务，如果不存在就不使用事务\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003ccode\u003ePROPAGATION_MANDATORY\u003c/code\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e支持当前事务，如果不存在则抛出异常\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003ccode\u003ePROPAGATION_REQUIRES_NEW\u003c/code\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e创建一个新事务，如果有事务存在则挂起\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003ccode\u003ePROPAGATION_NOT_SUPPORTED\u003c/code\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e以非事务方式运行，如果有事务存在则挂起\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003ccode\u003ePROPAGATION_NEVER\u003c/code\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e以非事务方式运行，如果存在事务则抛出异常\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003ccode\u003ePROPAGATION_NESTED\u003c/code\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e如果当前事务存在，则嵌套事务执行\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch3 id=\"transactionstatus\"\u003e\u003ccode\u003eTransactionStatus\u003c/code\u003e\u003c/h3\u003e\n\u003cimg src=\"https://cdn.jsdelivr.net/gh/xianglin2020/gallery@master/202011/122018.png\" alt=\"TransactionStatus\" style=\"zoom:33%;\" /\u003e\n\u003ch2 id=\"spring-事务管理操作\"\u003eSpring 事务管理操作\u003c/h2\u003e\n\u003ch3 id=\"准备转账案例\"\u003e准备转账案例\u003c/h3\u003e\n\u003ch4 id=\"表设计\"\u003e表设计\u003c/h4\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-sql\" data-lang=\"sql\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eCREATE\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"k\"\u003eTABLE\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"o\"\u003e`\u003c/span\u003e\u003cspan class=\"n\"\u003eaccount\u003c/span\u003e\u003cspan class=\"o\"\u003e`\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"o\"\u003e`\u003c/span\u003e\u003cspan class=\"n\"\u003eid\u003c/span\u003e\u003cspan class=\"o\"\u003e`\u003c/span\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"nb\"\u003eint\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"mi\"\u003e11\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\u003cspan class=\"w\"\u003e     \u003c/span\u003e\u003cspan class=\"k\"\u003eNOT\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"k\"\u003eNULL\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"o\"\u003e`\u003c/span\u003e\u003cspan class=\"n\"\u003ename\u003c/span\u003e\u003cspan class=\"o\"\u003e`\u003c/span\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"nb\"\u003evarchar\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"mi\"\u003e50\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"k\"\u003eNOT\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"k\"\u003eNULL\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"o\"\u003e`\u003c/span\u003e\u003cspan class=\"n\"\u003emoney\u003c/span\u003e\u003cspan class=\"o\"\u003e`\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"nb\"\u003edecimal\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"mi\"\u003e10\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"mi\"\u003e2\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"k\"\u003eDEFAULT\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"k\"\u003eNULL\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"k\"\u003ePRIMARY\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"k\"\u003eKEY\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"o\"\u003e`\u003c/span\u003e\u003cspan class=\"n\"\u003eid\u003c/span\u003e\u003cspan class=\"o\"\u003e`\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003eENGINE\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003eInnoDB\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"k\"\u003eDEFAULT\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003eCHARSET\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003eutf8\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eINSERT\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"k\"\u003eINTO\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003eimooc_demo\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eaccount\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eid\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003ename\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003emoney\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"k\"\u003eVALUES\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"mi\"\u003e1\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;a\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"mi\"\u003e1000\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"mi\"\u003e00\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eINSERT\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"k\"\u003eINTO\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003eimooc_demo\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eaccount\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eid\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003ename\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003emoney\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"k\"\u003eVALUES\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"mi\"\u003e2\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;b\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"mi\"\u003e1000\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"mi\"\u003e00\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eINSERT\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"k\"\u003eINTO\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003eimooc_demo\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eaccount\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eid\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003ename\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003emoney\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"k\"\u003eVALUES\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"mi\"\u003e3\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;c\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"mi\"\u003e1000\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"mi\"\u003e00\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\u003cspan class=\"w\"\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch4 id=\"实现-dao-层\"\u003e实现 DAO 层\u003c/h4\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-java\" data-lang=\"java\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"kd\"\u003einterface\u003c/span\u003e \u003cspan class=\"nc\"\u003eAccountDao\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"kt\"\u003evoid\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"nf\"\u003eaccountAdd\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eString\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003ename\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003eBigDecimal\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003edecimal\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"kt\"\u003evoid\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"nf\"\u003eaccountSub\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eString\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003ename\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003eBigDecimal\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003edecimal\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"kd\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eAccountDaoImpl\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"kd\"\u003eimplements\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003eAccountDao\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"kd\"\u003eprivate\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003eJdbcTemplate\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003ejdbcTemplate\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"kt\"\u003evoid\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"nf\"\u003esetJdbcTemplate\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eJdbcTemplate\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003ejdbcTemplate\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e        \u003c/span\u003e\u003cspan class=\"k\"\u003ethis\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003ejdbcTemplate\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003ejdbcTemplate\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"nd\"\u003e@Override\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"kt\"\u003evoid\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"nf\"\u003eaccountAdd\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eString\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003ename\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003eBigDecimal\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003edecimal\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e        \u003c/span\u003e\u003cspan class=\"n\"\u003eString\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003esql\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;update account set money = money + ? where name = ?\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e        \u003c/span\u003e\u003cspan class=\"k\"\u003ethis\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003ejdbcTemplate\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003eupdate\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003esql\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003edecimal\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003ename\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"nd\"\u003e@Override\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"kt\"\u003evoid\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"nf\"\u003eaccountSub\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eString\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003ename\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003eBigDecimal\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003edecimal\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e        \u003c/span\u003e\u003cspan class=\"n\"\u003eString\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003esql\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;update account set money = money - ? where name = ?\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e        \u003c/span\u003e\u003cspan class=\"k\"\u003ethis\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003ejdbcTemplate\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003eupdate\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003esql\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003edecimal\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003ename\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch4 id=\"实现-service-层\"\u003e实现 Service 层\u003c/h4\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-java\" data-lang=\"java\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"kd\"\u003einterface\u003c/span\u003e \u003cspan class=\"nc\"\u003eAccountService\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"kt\"\u003evoid\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"nf\"\u003etransfer\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eString\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003efrom\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003eString\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003eto\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003eBigDecimal\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003emoney\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"kd\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eAccountServiceImpl\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"kd\"\u003eimplements\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003eAccountService\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"kd\"\u003eprivate\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003eAccountDao\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003eaccountDao\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"kt\"\u003evoid\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"nf\"\u003esetAccountDao\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eAccountDao\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003eaccountDao\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e        \u003c/span\u003e\u003cspan class=\"k\"\u003ethis\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003eaccountDao\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003eaccountDao\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"nd\"\u003e@Override\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"kt\"\u003evoid\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"nf\"\u003etransfer\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eString\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003efrom\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003eString\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003eto\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003eBigDecimal\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003emoney\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e        \u003c/span\u003e\u003cspan class=\"n\"\u003eaccountDao\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003eaccountSub\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003efrom\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003emoney\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e        \u003c/span\u003e\u003cspan class=\"n\"\u003eaccountDao\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003eaccountAdd\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eto\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003emoney\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch4 id=\"使用-xml-方式声明对象\"\u003e使用 XML 方式声明对象\u003c/h4\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-xml\" data-lang=\"xml\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nt\"\u003e\u0026lt;bean\u003c/span\u003e \u003cspan class=\"na\"\u003eid=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;dataSource\u0026#34;\u003c/span\u003e \u003cspan class=\"na\"\u003eclass=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;org.springframework.jdbc.datasource.DriverManagerDataSource\u0026#34;\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"nt\"\u003e\u0026lt;property\u003c/span\u003e \u003cspan class=\"na\"\u003ename=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;driverClassName\u0026#34;\u003c/span\u003e \u003cspan class=\"na\"\u003evalue=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;com.mysql.cj.jdbc.Driver\u0026#34;\u003c/span\u003e\u003cspan class=\"nt\"\u003e/\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"nt\"\u003e\u0026lt;property\u003c/span\u003e \u003cspan class=\"na\"\u003ename=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;url\u0026#34;\u003c/span\u003e \u003cspan class=\"na\"\u003evalue=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;jdbc:mysql://hostip:port/dbname\u0026#34;\u003c/span\u003e\u003cspan class=\"nt\"\u003e/\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"nt\"\u003e\u0026lt;property\u003c/span\u003e \u003cspan class=\"na\"\u003ename=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;username\u0026#34;\u003c/span\u003e \u003cspan class=\"na\"\u003evalue=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;name\u0026#34;\u003c/span\u003e\u003cspan class=\"nt\"\u003e/\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"nt\"\u003e\u0026lt;property\u003c/span\u003e \u003cspan class=\"na\"\u003ename=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;password\u0026#34;\u003c/span\u003e \u003cspan class=\"na\"\u003evalue=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;password\u0026#34;\u003c/span\u003e\u003cspan class=\"nt\"\u003e/\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nt\"\u003e\u0026lt;/bean\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nt\"\u003e\u0026lt;bean\u003c/span\u003e \u003cspan class=\"na\"\u003eid=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;jdbcTemplate\u0026#34;\u003c/span\u003e \u003cspan class=\"na\"\u003eclass=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;org.springframework.jdbc.core.JdbcTemplate\u0026#34;\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"nt\"\u003e\u0026lt;property\u003c/span\u003e \u003cspan class=\"na\"\u003ename=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;dataSource\u0026#34;\u003c/span\u003e \u003cspan class=\"na\"\u003eref=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;dataSource\u0026#34;\u003c/span\u003e\u003cspan class=\"nt\"\u003e/\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nt\"\u003e\u0026lt;/bean\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nt\"\u003e\u0026lt;bean\u003c/span\u003e \u003cspan class=\"na\"\u003eid=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;accountDao\u0026#34;\u003c/span\u003e \u003cspan class=\"na\"\u003eclass=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;dao.AccountDaoImpl\u0026#34;\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"nt\"\u003e\u0026lt;property\u003c/span\u003e \u003cspan class=\"na\"\u003ename=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;jdbcTemplate\u0026#34;\u003c/span\u003e \u003cspan class=\"na\"\u003eref=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;jdbcTemplate\u0026#34;\u003c/span\u003e\u003cspan class=\"nt\"\u003e/\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nt\"\u003e\u0026lt;/bean\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nt\"\u003e\u0026lt;bean\u003c/span\u003e \u003cspan class=\"na\"\u003eid=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;accountService\u0026#34;\u003c/span\u003e \u003cspan class=\"na\"\u003eclass=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;service.AccountServiceImpl\u0026#34;\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"nt\"\u003e\u0026lt;property\u003c/span\u003e \u003cspan class=\"na\"\u003ename=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;accountDao\u0026#34;\u003c/span\u003e \u003cspan class=\"na\"\u003eref=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;accountDao\u0026#34;\u003c/span\u003e\u003cspan class=\"nt\"\u003e/\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nt\"\u003e\u0026lt;/bean\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e   \n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch4 id=\"测试转账效果\"\u003e测试转账效果\u003c/h4\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-java\" data-lang=\"java\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nd\"\u003e@RunWith\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eSpringJUnit4ClassRunner\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003eclass\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nd\"\u003e@ContextConfiguration\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;classpath:applicationContext.xml\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"kd\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eAccountServiceImplTest\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"nd\"\u003e@Resource\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"kd\"\u003eprivate\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003eAccountService\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003eaccountService\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"nd\"\u003e@Test\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"kt\"\u003evoid\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"nf\"\u003etransfer\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e        \u003c/span\u003e\u003cspan class=\"n\"\u003eaccountService\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003etransfer\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;a\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;b\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003eBigDecimal\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003evalueOf\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003e200\u003c/span\u003e\u003cspan class=\"p\"\u003e));\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch3 id=\"编程式的事务管理\"\u003e编程式的事务管理\u003c/h3\u003e\n\u003cp\u003e配置事务管理器\u003c/p\u003e","title":"Spring 事务管理"},{"content":"MyBatis 插件原理及 PageHelper 分页插件介绍 从 JDBC 开始 JDBC 介绍 JDBC（Java Database Connectivity）API 是 Java 语言与广泛数据库产品交互的基础，JDBC 技术体现了 Java \u0026quot;write Once,Run AnyWhere\u0026quot;的思想，即Java 为数据库访问提供一套纯 Java API，同时提供一个驱动管理器（DriverManager）。数据库供应商提供自己的数据库驱动，并注册到 Java 提供的DriverManager 中。\n普通的 Java 开发人员只需要基于 JDBC API 开发应用程序即可，不需要关注驱动的实现和驱动与数据库交互的逻辑。只有在程序运行时，才需要加载和使用相应的驱动。这也是基于 Maven 的项目在声明依赖的时候，可以将其依赖范围声明为runtime的原因。\nJDBC 使用步骤 使用 JBDC 完成对数据库操作的基本步骤如下\n注册驱动数据库类； 通过DriverManager#getConnection()建立到指定数据库的连接； 通过Connection对象创建Statement； 通过Statement中相关方法执行 SQL，按业务处理其返回值； 释放资源，关闭数据库连接等。 // 获取数据库连接 connection = DriverManager.getConnection( properties.getProperty(\u0026#34;jdbc.url\u0026#34;), properties.getProperty(\u0026#34;jdbc.username\u0026#34;), properties.getProperty(\u0026#34;jdbc.password\u0026#34;)); // 获取执行 SQL 的 Statement 对象 String listAllSql = \u0026#34;select * from products\u0026#34;; preparedStatement = connection.prepareStatement(listAllSql); // 执行查询 resultSet = preparedStatement.executeQuery(); // 按业务要求处理查询结果 // 释放资源 resultSet.close(); preparedStatement.close(); connection.close(); 基于 JDBC 的思考 使用 JDBC API 开发的不足之处比较明显，仅针对上面的查询场景而言，有以下几点\n创建数据库连接、关闭连接释放资源等语句繁琐且冗余； SQL 语句和业务代码糅合在一起，不便于维护； 需要自己解析查询结果。 Commons DbUtils和Spring JDBC都对 JDBC 做了封装，隐藏了创建数据库连接及释放资源等繁琐操作的处理过程，也提供了映射查询结果的方法，可以方便的将查询结果映射为 Map 或对应的 JavaBean。\n// 使用 DbUtils 的 QueryRunner 完成查询及结果映射 QueryRunner queryRunner = new QueryRunner(dataSource); List\u0026lt;Map\u0026gt; mapList = queryRunner.query(sql,new BeanListHandler\u0026lt;\u0026gt;(Map.class)); // 使用 Spring JdbcTemplate 完成查询并将结果映射为 JavaBean List\u0026lt;Employee\u0026gt; list = jdbcTemplate.query(sql, new BeanPropertyRowMapper\u0026lt;\u0026gt;(Employee.class)); 使用 MyBatis MyBatis 是一款优秀的支持自定义SQL查询、存储过程和高级映射的持久层框架，消除了几乎所有的JDBC代码和参数的手动设置以及结果集的处理。MyBatis 可以使用XML 或注解进行配置和映射，MyBatis 通过将参数映射到配置的 SQL 形成最终执行的 SQL 语句，最后将执行SQL 的结果映射成 Java 对象返回。 MyBatis 相关网站 [mybatis](mybatis – MyBatis 3 | 简介) [mybatis-spring](mybatis-spring – MyBatis-Spring | 简介) [mybatis-spring-boot](mybatis-spring-boot – About) MyBatis 配置文件mybatis-config.xml MyBatis 的配置文件包含了会深深影响 MyBatis 行为的设置和属性信息。\nMyBatis 的配置包含如下项：\nconfiguration properties（属性） settings（设置） typeAliases（类型别名） typeHandlers（类型处理器） objectFactory（对象工厂） plugins（插件） environments（环境配置） environment（环境变量） transactionManager（事务管理器） dataSource（数据源） databaseIdProvider（数据库厂商标识） mappers（映射器） 每个配置项的具体内容可以参照MyBatis官网或者MyBatis jar包中提供的mybatis-3-config.dtd或mybatis-config.xsd。需要注意的是，并不要求每个配置项必需存在，但是需要按照上述顺序进行配置。\n\u0026lt;!ELEMENT configuration (properties?, settings?, typeAliases?, typeHandlers?, objectFactory?, objectWrapperFactory?, reflectorFactory?, plugins?, environments?, databaseIdProvider?, mappers?)\u0026gt; 一个简单的配置示例如下：\n\u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34; ?\u0026gt; \u0026lt;!DOCTYPE configuration PUBLIC \u0026#34;-//mybatis.org//DTD Config 3.0//EN\u0026#34; \u0026#34;http://mybatis.org/dtd/mybatis-3-config.dtd\u0026#34;\u0026gt; \u0026lt;configuration\u0026gt; \u0026lt;properties resource=\u0026#34;jdbc-mysql.properties\u0026#34;/\u0026gt; \u0026lt;settings\u0026gt; \u0026lt;setting name=\u0026#34;mapUnderscoreToCamelCase\u0026#34; value=\u0026#34;true\u0026#34;/\u0026gt; \u0026lt;/settings\u0026gt; \u0026lt;environments default=\u0026#34;dev\u0026#34;\u0026gt; \u0026lt;environment id=\u0026#34;dev\u0026#34;\u0026gt; \u0026lt;transactionManager type=\u0026#34;JDBC\u0026#34;/\u0026gt; \u0026lt;dataSource type=\u0026#34;UNPOOLED\u0026#34;\u0026gt; \u0026lt;property name=\u0026#34;driver\u0026#34; value=\u0026#34;${jdbc.driver}\u0026#34;/\u0026gt; \u0026lt;property name=\u0026#34;url\u0026#34; value=\u0026#34;${jdbc.url}\u0026#34;/\u0026gt; \u0026lt;property name=\u0026#34;username\u0026#34; value=\u0026#34;${jdbc.username}\u0026#34;/\u0026gt; \u0026lt;property name=\u0026#34;password\u0026#34; value=\u0026#34;${jdbc.password}\u0026#34;/\u0026gt; \u0026lt;/dataSource\u0026gt; \u0026lt;/environment\u0026gt; \u0026lt;/environments\u0026gt; \u0026lt;mappers\u0026gt; \u0026lt;mapper resource=\u0026#34;mapper/ProductMapper.xml\u0026#34;/\u0026gt; \u0026lt;/mappers\u0026gt; \u0026lt;/configuration\u0026gt; MyBatis 映射文件Mapper.xml MyBatis 的强大在于它的语句映射，在完成基本配置后，MyBatis 开发只需要将重点放在Mapper映射中。MyBatis的结果映射、动态SQL、缓存等配置都在mapper.xml文件中。\nMapper.xml的部分配置项如下：\ncache – 该命名空间的缓存配置。 cache-ref – 引用其它命名空间的缓存配置。 resultMap – 自定义结果集映射方式。 sql – 定义可重用的 sql 块。 insert、update、delete、select – 映射SQL语句。 同样的，每个标签的使用细节可以参照MyBatis官网或者MyBatis jar包中提供的mybatis-3-mapper.dtd或mybatis-mapper.xsd。\n\u0026lt;!ELEMENT mapper (cache-ref | cache | resultMap* | parameterMap* | sql* | insert* | update* | delete* | select* )+\u0026gt; 一个简单的配置如下：\n\u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34; ?\u0026gt; \u0026lt;!DOCTYPE mapper PUBLIC \u0026#34;-//mybatis.org//DTD Mapper 3.0//EN\u0026#34; \u0026#34;http://mybatis.org/dtd/mybatis-3-mapper.dtd\u0026#34;\u0026gt; \u0026lt;mapper namespace=\u0026#34;product\u0026#34;\u0026gt; \u0026lt;select id=\u0026#34;getProduct\u0026#34; resultType=\u0026#34;site.xianglin.share.entity.Product\u0026#34;\u0026gt; select prod_id, vend_id, prod_name, prod_price, prod_desc from products limit 1 \u0026lt;/select\u0026gt; \u0026lt;select id=\u0026#34;pageProduct\u0026#34; resultType=\u0026#34;Map\u0026#34;\u0026gt; select * from products limit #{pageSize} offset #{offset} \u0026lt;/select\u0026gt; \u0026lt;select id=\u0026#34;pageProductRowBounds\u0026#34; resultType=\u0026#34;Map\u0026#34;\u0026gt; select * from products \u0026lt;/select\u0026gt; \u0026lt;/mapper\u0026gt; MyBatis 示例 基于以上示例配置，完善其他内容，并编写一个简单的测试类，体会 MyBatis 给 JDBC 开发带来的便捷。 MyBatis 的基础使用一般分为以下几步：\n在项目中引入MyBatis的依赖 通常创建基于XML的配置文件mybatis-config.xml 通常创建基于XML的映射文件ProductMapper.xml 通过配置文件创建SqlSessionFactory 从SqlSessionFactory中获取SqlSession，完成数据库操作 对分析、处理结果数据 其中4、5两步的示例代码如下：\n// 使用MyBatis提供的Resources工具类获取配置文件，并由SqlSessionFactoryBuilder解析创建SqlSessionFactory Reader resource = Resources.getResourceAsReader(\u0026#34;mybatis-config.xml\u0026#34;); SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resource); // 从SqlSessionFactory中获取SqlSession完成数据库操作，使用try-with-resources自动关闭资源 try (SqlSession sqlSession = sqlSessionFactory.openSession()) { Product product = sqlSession.selectOne(\u0026#34;product.getProduct\u0026#34;); Assert.assertNotNull(product); } MyBatis 执行过程 测试方法执行成功后，不妨借助 IDE 的调试功能，简单跟踪 MyBatis 初始化、SQL 执行以及结果映射的过程。\n从SqlSessionFactoryBuilder的build方法入手，build方法大致有三种类型的重载：\nSqlSessionFactory build(InputStream inputStream, String environment, Properties properties); SqlSessionFactory build(Reader reader, String environment, Properties properties)； SqlSessionFactory build(Configuration config)； 且前两种方法都会调用build(Configuration config)。MyBatis 运行时的配置信息都保存在Configuration实例中，分析 MyBatis 基于配置文件初始化的步骤也就是分析配置信息从配置文件读取、解析并保存在到Configuration的过程。主要代码如下：\nXMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties); parser.parse() parse方法会调用XMLConfigBuilder中的parseConfiguration方法将 MyBatis 配置文件解析为Configuration实例。MyBatis 会读取配置文件中的各个配置项，做对应的处理，并将其赋值给Configuration中对应的属性。\n有了Configuration实例后，就会调用 build(Configuration config)方法，生成SqlSessionFactoryBuilder实例。\npublic SqlSessionFactory build(Configuration config) { return new DefaultSqlSessionFactory(config); } 有了 SqlSessionFactory实例后，我们可以从中获得 SqlSession 的实例。SqlSession 提供了在数据库执行 SQL 命令所需的所有方法。通过 SqlSession 实例可以直接执行已映射的 SQL 语句，也可以获取接口对应的代理对象，并通过代理对象执行 SQL 语句。其过程比较简单，如下：\nfinal Environment environment = configuration.getEnvironment(); final TransactionFactory transactionFactory = environment.getTransactionFactory(); tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit); final Executor executor = configuration.newExecutor(tx, execType); return new DefaultSqlSession(configuration, executor, autoCommit); 获取SqlSession实例后就可以调用其中执行 SQL 语句的方法，以sqlSession.selectOne()方法为例，跟踪分析其执行过程。SqlSession的默认实现是DefaultSqlSession，按照selectOne的调用链路，最终会调用同类中selectList方法，方法签名如下：\n\u0026lt;E\u0026gt; List\u0026lt;E\u0026gt; selectList(String statement, Object parameter, RowBounds rowBounds) 其中statement即为 Mapper 文件中配置的 SQL 语句的 id 属性，parameter为预编译 SQL 需要的参数，rowBounds是 MyBatis 提供的保存分页信息的对象。\n在selectList方法中获取 MyBatis 初始化时解析并保存在Configuration实例中的MappedStatement对象，一个MappedStatement即对应一个映射SQL语句的标签。然后调用executor的query方法。\nMappedStatement ms = configuration.getMappedStatement(statement); return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER); 在query方法中获取到BoundSql和CacheKey后继续调用抽象方法doQuery，该方法被SimpleExecutor重写。BoundSql负责保存SQL，而CacheKey与 MyBatis 的缓存机制相关。\nBoundSql boundSql = ms.getBoundSql(parameterObject); CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql); return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); 忽略缓存相关内容，在doQuery方法中获取StatementHandler对象，同时StatementHandler会创建并持有ParameterHandler用于设置参数，和ResultSetHandler用于处理结果。\nConfiguration configuration = ms.getConfiguration(); StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql); stmt = prepareStatement(handler, ms.getStatementLog()); return handler.query(stmt, resultHandler); prepareStatement方法完成获取Connection对象、获取Statement和为预编译 SQL 占位符赋值等操作，至此一个基于 JDBC 的Statement对象创建完成。\nStatement stmt; Connection connection = getConnection(statementLog); stmt = handler.prepare(connection, transaction.getTimeout()); // 为SQL占位符赋值 handler.parameterize(stmt); Statement实例执行的代码很简单\nPreparedStatement ps = (PreparedStatement) statement; ps.execute(); return resultSetHandler.handleResultSets(ps); 如果执行的是查询类 SQL，会调用ResultSetHandler的handleResultSets方法完成结果映射，假如使用了MyBatis提供的RowBounds参数，内存分页的过程也在这里完成。内存分页的实现很简单，调用skipRows将结果集的游标偏移 offset 位，同时遍历结果的时候只取 limit 条记录。\nprivate void skipRows(ResultSet rs, RowBounds rowBounds) throws SQLException { // TYPE_FORWARD_ONLY 结果集的游标只能向下滚动 if (rs.getType() != ResultSet.TYPE_FORWARD_ONLY) { if (rowBounds.getOffset() != RowBounds.NO_ROW_OFFSET) { rs.absolute(rowBounds.getOffset()); } } else { for (int i = 0; i \u0026lt; rowBounds.getOffset(); i++) { if (!rs.next()) { break; } } } } private boolean shouldProcessMoreRows(ResultContext\u0026lt;?\u0026gt; context, RowBounds rowBounds) { return !context.isStopped() \u0026amp;\u0026amp; context.getResultCount() \u0026lt; rowBounds.getLimit(); } MyBatis 完成一个简单查询的大致过程如上所诉，我将其分为：初始化 MyBatis 创建SqlSessionFactory实例、获取SqlSession实例、得到preparedStatement实例并执行SQL操作、处理映射查询结果等步骤，示意图大致如下所示。以上代码跟踪暂未涉及其中细节，对 MyBatis 执行流程有了大致认识后，可以尝试具体分析 MyBatis 的插件实现原理。\nMyBatis 插件 Plugin MyBatis 允许在映射语句执行过程中的某一点进行拦截调用，允许使用插件来拦截的方法调用包括：\nExecutor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed) ParameterHandler (getParameterObject, setParameters) ResultSetHandler (handleResultSets, handleOutputParameters) StatementHandler (prepare, parameterize, batch, update, query) MyBatis在org.apache.ibatis.plugin包下提供了插件相关的接口、类和注解，如下所示\n名称 描述 Interceptor 拦截器接口，通过实现此接口，对拦截对象和方法进行处理 InterceptorChain 保存所有Interceptor的实例 Invocation 保存被代理对象、被代理方法及运行参数 Plugin 实现InvocationHandler，是动态代理的默认实现类 PluginException 插件相关异常 @Intercepts 指定拦截目标的注解 @Signature 指定方法签名的注解，与@Intercepts配合使用 MyBatis 插件示例 实现org.apache.ibatis.plugin.Interceptor接口，并使用@Intercepts和@Signature注解指定插件需要拦截的方法。\n@Intercepts({ @Signature( type = Executor.class, method = \u0026#34;query\u0026#34;, args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}) }) public class ExamplePlugin implements Interceptor { private Properties properties; @Override public Object intercept(Invocation invocation) throws Throwable { String simpleName = invocation.getTarget().getClass().getSimpleName(); String name = invocation.getMethod().getName(); Object[] args = invocation.getArgs(); System.out.println(\u0026#34;ExamplePlugin 插件拦截了\u0026#34; + simpleName + \u0026#34;.\u0026#34; + name + \u0026#34;方法，方法入参为：\u0026#34; + Arrays.toString(args)); Object result = invocation.proceed(); System.out.println(\u0026#34;同插件一起配置的属性有：\u0026#34; + properties.toString()); return result; } @Override public void setProperties(Properties properties) { this.properties = properties; } } 在 MyBatis 配置文件中使用plugins标签注册插件\n\u0026lt;plugins\u0026gt; \u0026lt;plugin interceptor=\u0026#34;site.xianglin.share.plugin.ExamplePlugin\u0026#34;\u0026gt; \u0026lt;property name=\u0026#34;pluginProperty\u0026#34; value=\u0026#34;100\u0026#34;/\u0026gt; \u0026lt;/plugin\u0026gt; \u0026lt;/plugins\u0026gt; 执行sqlSession.selectOne(\u0026quot;product.getProduct\u0026quot;);方法观察输出\nExamplePlugin 插件拦截了SimpleExecutor.query方法，方法入参为：[org.apache.ibatis.mapping.MappedStatement@64485a47, {limit=1}, org.apache.ibatis.session.RowBounds@25bbf683, null] 同插件一起配置的属性有：{pluginProperty=100} MyBatis 插件工作原理 加载插件配置信息 上面提到，XMLConfigBuilder中的parseConfiguration方法将 MyBatis 配置文件解析为Configuration实例，插件的信息也在此处解析加载，如下所示（略去mybatis-config.xml中未配置的部分）。\nprivate void parseConfiguration(XNode root) { try { propertiesElement(root.evalNode(\u0026#34;properties\u0026#34;)); Properties settings = settingsAsProperties(root.evalNode(\u0026#34;settings\u0026#34;)); // 插件信息在此加载 pluginElement(root.evalNode(\u0026#34;plugins\u0026#34;)); settingsElement(settings); environmentsElement(root.evalNode(\u0026#34;environments\u0026#34;)); mapperElement(root.evalNode(\u0026#34;mappers\u0026#34;)); } catch (Exception e) { throw new BuilderException(\u0026#34;Error parsing SQL Mapper Configuration. Cause: \u0026#34; + e, e); } } pluginElement方法的代码及主要完成的工作如下：\nprivate void pluginElement(XNode parent) throws Exception { if (parent != null) { for (XNode child : parent.getChildren()) { // 根据配置类信息创建 Interceptor 对象 String interceptor = child.getStringAttribute(\u0026#34;interceptor\u0026#34;); Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).getDeclaredConstructor().newInstance(); // 调用 setProperties 方法设置配置的属性 Properties properties = child.getChildrenAsProperties(); interceptorInstance.setProperties(properties); // 将插件实例添加到 configuration 的拦截器链中 configuration.addInterceptor(interceptorInstance); } } } 拦截器链InterceptorChain维护了一个Interceptor列表保存所有的拦截器实例，定义了pluginAll方法用于为被拦截对象创建代理对象。\nprivate final List\u0026lt;Interceptor\u0026gt; interceptors = new ArrayList\u0026lt;\u0026gt;(); 生成代理对象 Mybatis 插件主要是基于动态代理实现的，MyBatis 的四大对象都是由 Configuration 负责创建的，查看对应的方法：\n// Executor 代理对象 public Executor newExecutor(Transaction transaction, ExecutorType executorType) { executorType = executorType == null ? ExecutorType.SIMPLE : executorType; Executor executor = new SimpleExecutor(this, transaction); // ... executor = (Executor) interceptorChain.pluginAll(executor); return executor; } // StatementHandler 代理对象 public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) { StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql); statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler); return statementHandler; } // ParameterHandler 代理对象 public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) { ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement, parameterObject, boundSql); parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler); return parameterHandler; } // ResultSetHandler代理对象 public ResultSetHandler newResultSetHandler(Executor executor, MappedStatement mappedStatement, RowBounds rowBounds, ParameterHandler parameterHandler, ResultHandler resultHandler, BoundSql boundSql) { ResultSetHandler resultSetHandler = new DefaultResultSetHandler(executor, mappedStatement, parameterHandler, resultHandler, boundSql, rowBounds); resultSetHandler = (ResultSetHandler) interceptorChain.pluginAll(resultSetHandler); return resultSetHandler; } 这些代理对象都是通过interceptorChain.pluginAll方法生成的，pluginAll方法定义如下。\npublic Object pluginAll(Object target) { for (Interceptor interceptor : interceptors) { target = interceptor.plugin(target); } return target; } 可以知道，当配置多个拦截器时，MyBatis 会遍历所有拦截器，按顺序执行拦截器的 plugin 方法，被拦截的对象就会被层层代理。\nMyBatis 为plugin方法提供了默认实现，那么Plugin.wrap就是典型的动态代理实现。\ndefault Object plugin(Object target) { return Plugin.wrap(target, this); } public static Object wrap(Object target, Interceptor interceptor) { Map\u0026lt;Class\u0026lt;?\u0026gt;, Set\u0026lt;Method\u0026gt;\u0026gt; signatureMap = getSignatureMap(interceptor); Class\u0026lt;?\u0026gt; type = target.getClass(); Class\u0026lt;?\u0026gt;[] interfaces = getAllInterfaces(type, signatureMap); if (interfaces.length \u0026gt; 0) { return Proxy.newProxyInstance( type.getClassLoader(), interfaces, new Plugin(target, interceptor, signatureMap)); } return target; } 拦截器执行逻辑 由于Executor、ParameterHandler、ResultSetHandler和StatementHandler均被代理，所以在执行方法时，首先执行Plugin中的invoke方法，invoke方法的实现如下：\npublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable { try { Set\u0026lt;Method\u0026gt; methods = signatureMap.get(method.getDeclaringClass()); if (methods != null \u0026amp;\u0026amp; methods.contains(method)) { return interceptor.intercept(new Invocation(target, method, args)); } return method.invoke(target, args); } catch (Exception e) { throw ExceptionUtil.unwrapThrowable(e); } } 判断当前方法是否需要被拦截，如果需要，则执行interceptor对象的intercept方法，不需要则直接执行原方法。\n实现一个分页插件 在MyBatis 执行过程中分析到 MyBatis 默认的分页实现是内存分页，即查询所有记录，在内存中取分页数据。可以通过拦截StatementHandler的prepare方法，修改MyBatis 最终执行的SQL 来实现物理分页。具体实现如下所示：\n/** * 简单的分页实现： * 1. 如果有RowBounds参数，使用其作为分页参数 * 2. 如果接口方法类似 list(int pageSize, int pageNum)，使用其作为分页参数 * 3. 使用ThreadLocal存储分页参数 * * @author xianglin */ @Intercepts({ @Signature( type = StatementHandler.class, method = \u0026#34;prepare\u0026#34;, args = { Connection.class, Integer.class } ) }) @SuppressWarnings(\u0026#34;unchecked\u0026#34;) public class SimplePageHelper implements Interceptor { private String pageSizeKey = \u0026#34;pageSize\u0026#34;; private String pageNumKey = \u0026#34;pageNum\u0026#34;; private Page page; @Override public Object intercept(Invocation invocation) throws Throwable { StatementHandler statementHandler = (StatementHandler) invocation.getTarget(); MetaObject metaStatementHandler = SystemMetaObject.forObject(statementHandler); while (metaStatementHandler.hasGetter(\u0026#34;h\u0026#34;)) { Object object = metaStatementHandler.getValue(\u0026#34;h\u0026#34;); metaStatementHandler = SystemMetaObject.forObject(object); } // 分离最后一个代理对象的目标类 while (metaStatementHandler.hasGetter(\u0026#34;target\u0026#34;)) { Object object = metaStatementHandler.getValue(\u0026#34;target\u0026#34;); metaStatementHandler = SystemMetaObject.forObject(object); } // 判断是否需要分页 if (needPage(metaStatementHandler)) { // 拼接分页条件 BoundSql boundSql = (BoundSql) metaStatementHandler.getValue(\u0026#34;delegate.boundSql\u0026#34;); String sql = boundSql.getSql(); String newSql = \u0026#34;select * from (\u0026#34; + sql + \u0026#34;) t limit \u0026#34; + this.page.limit + \u0026#34; offset \u0026#34; + this.page.offset; metaStatementHandler.setValue(\u0026#34;delegate.boundSql.sql\u0026#34;, newSql); } return invocation.proceed(); } private boolean needPage(MetaObject metaObject) { // 判断 RowBounds RowBounds rowBounds = (RowBounds) metaObject.getValue(\u0026#34;delegate.rowBounds\u0026#34;); if (rowBounds != null \u0026amp;\u0026amp; rowBounds != RowBounds.DEFAULT) { // 取消默认的内存分页 metaObject.setValue(\u0026#34;delegate.rowBounds\u0026#34;, RowBounds.DEFAULT); this.page = new Page(); this.page.limit = rowBounds.getLimit(); this.page.offset = rowBounds.getOffset(); return true; } // 判断方法参数 Object parameterObject = metaObject.getValue(\u0026#34;delegate.parameterHandler.parameterObject\u0026#34;); SqlCommandType sqlCommandType = (SqlCommandType) metaObject.getValue(\u0026#34;delegate.parameterHandler.mappedStatement.sqlCommandType\u0026#34;); if (parameterObject instanceof Map \u0026amp;\u0026amp; sqlCommandType == SqlCommandType.SELECT) { Map\u0026lt;String, Object\u0026gt; paraMap = (Map\u0026lt;String, Object\u0026gt;) parameterObject; Integer pageSize = (Integer) paraMap.get(this.pageSizeKey); Integer pageNum = (Integer) paraMap.get(this.pageNumKey); if (pageSize != null \u0026amp;\u0026amp; pageNum != null) { this.page = new Page(); this.page.limit = pageSize; this.page.offset = (pageNum - 1) * pageSize; return true; } } // 从ThreadLocal获取分页信息 Page page = getPage(); if (page != null) { this.page = new Page(); this.page.limit = page.pageSize; this.page.offset = (page.pageNum - 1) * page.pageSize; return true; } return false; } @Override public void setProperties(Properties properties) { this.pageSizeKey = properties.getProperty(\u0026#34;pageSizeKey\u0026#34;, \u0026#34;pageSize\u0026#34;); this.pageNumKey = properties.getProperty(\u0026#34;pageNumKey\u0026#34;, \u0026#34;pageNum\u0026#34;); } public static void startPage(int pageSize, int pageNum) { Page page = new Page(); page.pageSize = pageSize; page.pageNum = pageNum; PAGE_LOCAL.set(page); } private static Page getPage() { Page page = PAGE_LOCAL.get(); PAGE_LOCAL.remove(); return page; } private final static ThreadLocal\u0026lt;Page\u0026gt; PAGE_LOCAL = new ThreadLocal\u0026lt;\u0026gt;(); static class Page { private int pageSize; private int pageNum; private int limit; private int offset; } } MyBatis 分页插件 PageHelper PageHelper 介绍 引入分页插件\n\u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;com.github.pagehelper\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;pagehelper\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;最新版本\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; 配置拦截器插件com.github.pagehelper.PageInterceptor\n\u0026lt;plugins\u0026gt; \u0026lt;plugin interceptor=\u0026#34;com.github.pagehelper.PageInterceptor\u0026#34;\u0026gt; \u0026lt;property name=\u0026#34;param1\u0026#34; value=\u0026#34;value1\u0026#34;/\u0026gt; \u0026lt;/plugin\u0026gt; \u0026lt;/plugins\u0026gt; 分页插件参数介绍：\npageSizeZero：默认值为 false，当该参数设置为 true 时，如果 pageSize=0 或者 RowBounds.limit = 0 就会查询出全部的结果（相当于没有执行分页查询，但是返回结果仍然是 Page 类型）。\nreasonable：分页合理化参数，默认值为false。当该参数设置为 true 时，pageNum\u0026lt;=0 时会查询第一页， pageNum\u0026gt;pages（超过总数时），会查询最后一页。默认false 时，直接根据参数进行查询。\nsupportMethodsArguments：支持通过 Mapper 接口参数来传递分页参数，默认值false，分页插件会从查询方法的参数值中取值，查找到合适的值时就会自动分页。\n分页插件常见使用方式 RowBounds方式的调用 List\u0026lt;Object\u0026gt; selectList = sqlSession.selectList(\u0026#34;product.pageProductRowBounds\u0026#34;, null, rowBounds); // 接口也可以增加RowBounds参数 PageRowBounds pageRowBounds = new PageRowBounds(0, 10); List\u0026lt;Product\u0026gt; productList = mapper.pageProduct(pageRowBounds); Assert.assertNotNull(pageRowBounds.getTotal()); 由于默认情况下的 RowBounds 无法获取查询总数，分页插件提供了一个继承自 RowBounds 的 PageRowBounds，这个对象中增加了 total 属性，执行分页查询后，可以从该属性得到查询总数。\nPageHelper.startPage 静态方法调用 PageHelper.startPage(2, 5); List\u0026lt;Product\u0026gt; productList = productMapper.listAllProduct(); // 分页时，实际返回的结果list类型是Page\u0026lt;E\u0026gt;，如果想取出分页信息，需要强制转换为Page\u0026lt;E\u0026gt;， // 或者使用PageInfo对结果进行包装 Page\u0026lt;Product\u0026gt; page = (Page\u0026lt;Product\u0026gt;) productList; PageInfo\u0026lt;Product\u0026gt; pageInfo = new PageInfo\u0026lt;\u0026gt;(productList); PageHelper 方法使用了静态的 ThreadLocal 参数，分页参数和线程是绑定的。\n使用参数方式 List\u0026lt;Product\u0026gt; pageProduct(int pageSize, int pageNum); List\u0026lt;Product\u0026gt; productList = productMapper.pageProduct(5, 2); 使用参数方式，需要配置 supportMethodsArguments 参数为 true\n在 Spring 中使用 PageHelper 见项目演示\n在 SpringBoot 中使用 PageHelper 见项目演示\n","permalink":"https://2d992714.xianglin2020-github-io.pages.dev/posts/2020/mybatis-interceptor/","summary":"\u003ch1 id=\"mybatis-插件原理及-pagehelper-分页插件介绍\"\u003eMyBatis 插件原理及 PageHelper 分页插件介绍\u003c/h1\u003e\n\u003ch2 id=\"从-jdbc-开始\"\u003e从 JDBC 开始\u003c/h2\u003e\n\u003ch3 id=\"jdbc-介绍\"\u003eJDBC 介绍\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003eJDBC（Java Database Connectivity）API 是 Java 语言与广泛数据库产品交互的基础，JDBC 技术体现了 Java \u003ccode\u003e\u0026quot;write Once,Run AnyWhere\u0026quot;\u003c/code\u003e的思想，即Java 为数据库访问提供一套纯 Java API，同时提供一个驱动管理器（DriverManager）。数据库供应商提供自己的数据库驱动，并注册到 Java 提供的DriverManager 中。\u003c/p\u003e","title":"MyBatis 拦截器"},{"content":"Ubuntu 安装 MySQL Ubuntu Server 和 MySQL Ubuntu Server MySQL Server 使用 APT 安装 MySQL 使用命令安装 MySQL Server\nsudo apt install mysql-server\n查看 MySQL 运行情况\nsudo service mysql status\n或者\nsudo /etc/init.d/mysql status\nsudo /etc/init.d/mysql status ● mysql.service - MySQL Community Server Loaded: loaded (/lib/systemd/system/mysql.service; enabled; vendor preset: enabled) Active: active (running) since Thu 2020-10-08 22:41:04 CST; 2 days ago Main PID: 55128 (mysqld) Status: \u0026#34;Server is operational\u0026#34; Tasks: 40 (limit: 2319) Memory: 332.3M CGroup: /system.slice/mysql.service └─55128 /usr/sbin/mysqld Oct 08 22:41:03 iZwz92js0u6m08arfj6jp8Z systemd[1]: Starting MySQL Community…... Oct 08 22:41:04 iZwz92js0u6m08arfj6jp8Z systemd[1]: Started MySQL Community …er. Hint: Some lines were ellipsized, use -l to show in full. 启动、停止、重启MySQL 的命令类似\nsudo [/etc/init.d/mysql | service mysql] [force-reload|restart|status|reload|start|stop] 在 Ubuntu 中安装 MySQL 并没有提示设置 root 密码，但可以在 /etc/mysql 路径下找到相关配置文件\ncd /etc/mysql sudo cat debian.cnf 会得到如下信息：\n可以使用debain.cnf中的 user 和 password 登录 MySQL\n新建一个用于远程连接的用户，并分配权限\n新建用户\ncreate user 'xianglin'@'119.130.215.117' identified by '950915';\n分配权限\ngrant all privileges on *.* to 'xianglin'@'119.130.215.117';\nflush privileges;\n找到 /etc/mysql/mysql.conf.d/mysqld.cnf 并注释 bind-address=127.0.01，后重启 MySQL 服务\n在阿里云的实例列表中找到如下菜单\n添加访问规则，允许 MySQL 默认端口 3306 访问\n使用 MySQL 客户端工具 datagrid 连接 MySQL\n","permalink":"https://2d992714.xianglin2020-github-io.pages.dev/posts/2020/ubuntu-install-mysql/","summary":"\u003ch1 id=\"ubuntu-安装-mysql\"\u003eUbuntu 安装 MySQL\u003c/h1\u003e\n\u003ch2 id=\"ubuntu-server-和-mysql\"\u003eUbuntu Server 和 MySQL\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://ubuntu.com/download/alternative-downloads\" target=\"_blank\" rel=\"noopener\"\u003eUbuntu Server\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://downloads.mysql.com/archives/community/\" target=\"_blank\" rel=\"noopener\"\u003eMySQL Server\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"使用-apt-安装-mysql\"\u003e使用 APT 安装 MySQL\u003c/h3\u003e\n\u003col\u003e\n\u003cli\u003e\n\u003cp\u003e使用命令安装 MySQL Server\u003c/p\u003e\n\u003cp\u003e\u003ccode\u003esudo apt install mysql-server\u003c/code\u003e\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e查看 MySQL 运行情况\u003c/p\u003e\n\u003cp\u003e\u003ccode\u003esudo service mysql status\u003c/code\u003e\u003c/p\u003e\n\u003cp\u003e或者\u003c/p\u003e\n\u003cp\u003e\u003ccode\u003esudo /etc/init.d/mysql status\u003c/code\u003e\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-shell\" data-lang=\"shell\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003esudo /etc/init.d/mysql status\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e● mysql.service - MySQL Community Server\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e     Loaded: loaded \u003cspan class=\"o\"\u003e(\u003c/span\u003e/lib/systemd/system/mysql.service\u003cspan class=\"p\"\u003e;\u003c/span\u003e enabled\u003cspan class=\"p\"\u003e;\u003c/span\u003e vendor preset: enabled\u003cspan class=\"o\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e     Active: active \u003cspan class=\"o\"\u003e(\u003c/span\u003erunning\u003cspan class=\"o\"\u003e)\u003c/span\u003e since Thu 2020-10-08 22:41:04 CST\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"m\"\u003e2\u003c/span\u003e days ago\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e   Main PID: \u003cspan class=\"m\"\u003e55128\u003c/span\u003e \u003cspan class=\"o\"\u003e(\u003c/span\u003emysqld\u003cspan class=\"o\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e     Status: \u003cspan class=\"s2\"\u003e\u0026#34;Server is operational\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e      Tasks: \u003cspan class=\"m\"\u003e40\u003c/span\u003e \u003cspan class=\"o\"\u003e(\u003c/span\u003elimit: 2319\u003cspan class=\"o\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e     Memory: 332.3M\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e     CGroup: /system.slice/mysql.service\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e             └─55128 /usr/sbin/mysqld\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eOct \u003cspan class=\"m\"\u003e08\u003c/span\u003e 22:41:03 iZwz92js0u6m08arfj6jp8Z systemd\u003cspan class=\"o\"\u003e[\u003c/span\u003e1\u003cspan class=\"o\"\u003e]\u003c/span\u003e: Starting MySQL Community…...\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eOct \u003cspan class=\"m\"\u003e08\u003c/span\u003e 22:41:04 iZwz92js0u6m08arfj6jp8Z systemd\u003cspan class=\"o\"\u003e[\u003c/span\u003e1\u003cspan class=\"o\"\u003e]\u003c/span\u003e: Started MySQL Community …er.\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eHint: Some lines were ellipsized, use -l to show in full.\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e启动、停止、重启MySQL 的命令类似\u003c/p\u003e","title":"Ubuntu 安装 MySQL"},{"content":"Spring MVC Spring MVC 环境搭建 引入 spring-mvc 相关依赖\n\u0026lt;dependencies\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;javax.servlet\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;servlet-api\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;2.5\u0026lt;/version\u0026gt; \u0026lt;scope\u0026gt;provided\u0026lt;/scope\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-webmvc\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;/dependencies\u0026gt; 在web.xml中配置DispatcherServlet\n\u0026lt;servlet\u0026gt; \u0026lt;servlet-name\u0026gt;springmvc\u0026lt;/servlet-name\u0026gt; \u0026lt;servlet-class\u0026gt;org.springframework.web.servlet.DispatcherServlet\u0026lt;/servlet-class\u0026gt; \u0026lt;init-param\u0026gt; \u0026lt;param-name\u0026gt;contextConfigLocation\u0026lt;/param-name\u0026gt; \u0026lt;param-value\u0026gt;classpath:applicationContext.xml\u0026lt;/param-value\u0026gt; \u0026lt;/init-param\u0026gt; \u0026lt;load-on-startup\u0026gt;0\u0026lt;/load-on-startup\u0026gt; \u0026lt;/servlet\u0026gt; \u0026lt;servlet-mapping\u0026gt; \u0026lt;servlet-name\u0026gt;springmvc\u0026lt;/servlet-name\u0026gt; \u0026lt;url-pattern\u0026gt;/\u0026lt;/url-pattern\u0026gt; \u0026lt;/servlet-mapping\u0026gt; 创建并配置applicationContext.xml\n\u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34;?\u0026gt; \u0026lt;beans xmlns=\u0026#34;http://www.springframework.org/schema/beans\u0026#34; xmlns:xsi=\u0026#34;http://www.w3.org/2001/XMLSchema-instance\u0026#34; xmlns:context=\u0026#34;http://www.springframework.org/schema/context\u0026#34; xmlns:mvc=\u0026#34;http://www.springframework.org/schema/mvc\u0026#34; xsi:schemaLocation=\u0026#34;http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/mvc https://www.springframework.org/schema/mvc/spring-mvc.xsd\u0026#34;\u0026gt; \u0026lt;context:component-scan base-package=\u0026#34;person.xianglin.springmvc\u0026#34;/\u0026gt; \u0026lt;mvc:annotation-driven/\u0026gt; \u0026lt;mvc:default-servlet-handler/\u0026gt; \u0026lt;/beans\u0026gt; 开发业务代码\nSpringMVC 基础及数据绑定 URL 绑定 @RequestMapping\n接收请求参数 方法参数接收请求数据\n@RequestParam\nJavaBean 接收请求参数\n日期类型转换\n@DateTimeFormat\n实现org.springframework.core.convert.converter.Converter自定义转换\npublic class DateConverter implements Converter\u0026lt;String, Date\u0026gt; { @Override public Date convert(String s) { DateFormat dateFormat = new SimpleDateFormat(\u0026#34;yyyy-MM-dd\u0026#34;); Date parse = null; try { parse = dateFormat.parse(s); return parse; } catch (ParseException e) { return null; } } } \u0026lt;context:component-scan base-package=\u0026#34;person.xianglin.springmvc\u0026#34;/\u0026gt; \u0026lt;mvc:default-servlet-handler/\u0026gt; \u0026lt;mvc:annotation-driven conversion-service=\u0026#34;conversionService\u0026#34;/\u0026gt; \u0026lt;bean id=\u0026#34;conversionService\u0026#34; class=\u0026#34;org.springframework.format.support.FormattingConversionServiceFactoryBean\u0026#34;\u0026gt; \u0026lt;property name=\u0026#34;converters\u0026#34;\u0026gt; \u0026lt;set\u0026gt; \u0026lt;bean class=\u0026#34;person.xianglin.springmvc.convert.DateConverter\u0026#34;/\u0026gt; \u0026lt;/set\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;/bean\u0026gt; Spring MVC 中文乱码 Get 请求：server.xml中增加URIEncoding属性（Tomcat8.0 之前）\n\u0026lt;Connector port=\u0026#34;8080\u0026#34; protocol=\u0026#34;HTTP/1.1\u0026#34; connectionTimeout=\u0026#34;20000\u0026#34; redirectPort=\u0026#34;8443\u0026#34; URIEncoding=\u0026#34;UTF-8\u0026#34;/\u0026gt; Post 请求：web.xml中配置CharacterEncodingFilter\n\u0026lt;filter\u0026gt; \u0026lt;filter-name\u0026gt;characterFilter\u0026lt;/filter-name\u0026gt; \u0026lt;filter-class\u0026gt;org.springframework.web.filter.CharacterEncodingFilter\u0026lt;/filter-class\u0026gt; \u0026lt;init-param\u0026gt; \u0026lt;param-name\u0026gt;encoding\u0026lt;/param-name\u0026gt; \u0026lt;param-value\u0026gt;UTF-8\u0026lt;/param-value\u0026gt; \u0026lt;/init-param\u0026gt; \u0026lt;/filter\u0026gt; \u0026lt;filter-mapping\u0026gt; \u0026lt;filter-name\u0026gt;characterFilter\u0026lt;/filter-name\u0026gt; \u0026lt;url-pattern\u0026gt;/*\u0026lt;/url-pattern\u0026gt; \u0026lt;/filter-mapping\u0026gt; Response 响应：Spring 配置StringHttpMessageConverter\n\u0026lt;mvc:annotation-driven\u0026gt; \u0026lt;mvc:message-converters\u0026gt; \u0026lt;bean class=\u0026#34;org.springframework.http.converter.StringHttpMessageConverter\u0026#34;\u0026gt; \u0026lt;property name=\u0026#34;supportedMediaTypes\u0026#34;\u0026gt; \u0026lt;list\u0026gt; \u0026lt;value\u0026gt;application/json;charset=utf-8\u0026lt;/value\u0026gt; \u0026lt;value\u0026gt;text/html;charset=utf-8\u0026lt;/value\u0026gt; \u0026lt;/list\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;/bean\u0026gt; \u0026lt;/mvc:message-converters\u0026gt; \u0026lt;/mvc:annotation-driven\u0026gt; 响应输出结果 @ResponseBody\nModelAndView\n属性保存在当前请求 request 对象中\nmodelAndView 使用请求转发完成页面跳转\n使用redirect:index.html完成重定向\nSpring 与模板引擎整合\n引入 freemaker 依赖\n\u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-context-support\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.freemarker\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;freemarker\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;2.3.30\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; 配置FreeMarkerViewResolver\n\u0026lt;bean id=\u0026#34;viewResolver\u0026#34; class=\u0026#34;org.springframework.web.servlet.view.freemarker.FreeMarkerViewResolver\u0026#34;\u0026gt; \u0026lt;property name=\u0026#34;contentType\u0026#34; value=\u0026#34;text/html;charset=UTF-8\u0026#34;/\u0026gt; \u0026lt;property name=\u0026#34;suffix\u0026#34; value=\u0026#34;.ftl\u0026#34;/\u0026gt; \u0026lt;/bean\u0026gt; 配置FreeMarkerConfig\n\u0026lt;!-- NoSuchBeanDefinitionException: No qualifying bean of type \u0026#39;org.springframework.web.servlet.view.freemarker.FreeMarkerConfig\u0026#39; available --\u0026gt; \u0026lt;bean id=\u0026#34;freeMarkerConfig\u0026#34; class=\u0026#34;org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer\u0026#34;\u0026gt; \u0026lt;property name=\u0026#34;templateLoaderPath\u0026#34; value=\u0026#34;/WEB-INF/ftl/\u0026#34;/\u0026gt; \u0026lt;property name=\u0026#34;freemarkerSettings\u0026#34;\u0026gt; \u0026lt;props\u0026gt; \u0026lt;prop key=\u0026#34;defaultEncoding\u0026#34;\u0026gt;UTF-8\u0026lt;/prop\u0026gt; \u0026lt;/props\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;/bean\u0026gt; Restful风格的应用 REST：表现层状态转换，资源在网络中以某种形式进行状态转换 RESTFul 开发规范： 使用 URL 作为用户交互入口 明确的语义规范，使用不同的 HTTP 方法完成对应的操作 只返回数据，不包含展示 SpringMVC RESTFul 使用 @RestController 创建 RestFul 风格控制器对象\n@PathVariable 路径变量注解\n非简单请求JavaBean无法获取参数问题\n在web.xml中配置FormContentFilter过滤器\n\u0026lt;filter\u0026gt; \u0026lt;filter-name\u0026gt;formContextFilter\u0026lt;/filter-name\u0026gt; \u0026lt;filter-class\u0026gt;org.springframework.web.filter.FormContentFilter\u0026lt;/filter-class\u0026gt; \u0026lt;/filter\u0026gt; \u0026lt;filter-mapping\u0026gt; \u0026lt;filter-name\u0026gt;formContextFilter\u0026lt;/filter-name\u0026gt; \u0026lt;url-pattern\u0026gt;/*\u0026lt;/url-pattern\u0026gt; \u0026lt;/filter-mapping\u0026gt; jackson 中对日期格式的注解@JsonFormat\n@JsonFormat(pattern = \u0026#34;yyyy-MM-dd HH:mm:ss SSS\u0026#34;,timezone = \u0026#34;Asia/Shanghai\u0026#34;) private Date date; 跨域问题 同源策略阻止从一个域加载脚本去获取另一个域上的资源 只要协议、域名、端口有一个不同，都视为不同的域 Access-Control-Allow-Origin HTML 中允许跨域的标签 \u0026lt;img\u0026gt; 图片 \u0026lt;srcipt\u0026gt; JS 脚本 link CSS 样式 SpringMVC 解决跨域访问问题 @CrosOrigin Controller 跨域注解\n字段 描述 值 origins 被允许跨区请求的源列表 allowedHeaders 被允许在实际请求中的请求头 Access-Control-Allow-Headers exposedHeaders Access-Control-Expose-Headers methods 支持的HTTP请求方法的列表 allowCredentials Access-Control-Allow-Credentials maxAge 预检响应缓存持续时间 Access-Control-Max-Age \u0026lt;mvc:cros\u0026gt; 全局跨域设置\n\u0026lt;mvc:cors\u0026gt; \u0026lt;mvc:mapping path=\u0026#34;/restful/**\u0026#34; allowed-origins=\u0026#34;http://127.0.0.1,http://example.com\u0026#34; max-age=\u0026#34;3600\u0026#34;/\u0026gt; \u0026lt;/mvc:cors\u0026gt; Spring 拦截器 拦截器（Interceptor）用于对请求进行前置、后置过滤 拦截器基于 Spring AOP 实现 Spring 拦截器开发流程 引入servlet-api\n\u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;javax.servlet\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;javax.servlet-api\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;3.1.0\u0026lt;/version\u0026gt; \u0026lt;scope\u0026gt;provided\u0026lt;/scope\u0026gt; \u0026lt;/dependency\u0026gt; 继承org.springframework.web.servlet.HandlerInterceptor实现拦截器逻辑\n配置拦截器\n\u0026lt;mvc:interceptors\u0026gt; \u0026lt;mvc:interceptor\u0026gt; \u0026lt;mvc:mapping path=\u0026#34;/**\u0026#34;/\u0026gt; \u0026lt;bean class=\u0026#34;person.xianglin.springrestful.interceptor.MyInterceptor\u0026#34;/\u0026gt; \u0026lt;/mvc:interceptor\u0026gt; \u0026lt;/mvc:interceptors\u0026gt; HandlerInterceptor 接口 方法 描述 preHandle 前置执行处理：返回 true 允许执行后续操作，返回 false 则立即返回 postHandle 目标资源已被 SpringMVC框架处理 afterCompletion 响应文本已经产生 拦截器处理 URL 资源过滤\n\u0026lt;mvc:exclude-mapping path=\u0026#34;/**.ico\u0026#34;/\u0026gt; \u0026lt;mvc:exclude-mapping path=\u0026#34;/resources/**\u0026#34;/\u0026gt; 多个拦截器执行顺序\nSpring MVC 处理流程 SSM 整合过程 Spring与 SpringMVC环境配置 在pom.xml中引入相应依赖\n\u0026lt;dependencies\u0026gt; \u0026lt;!-- SpringMVC 依赖 --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-webmvc\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!-- Freemarker 依赖 --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.freemarker\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;freemarker\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;2.3.30\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-context-support\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!-- Jackson 依赖 --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;com.fasterxml.jackson.core\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;jackson-core\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;2.11.1\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;com.fasterxml.jackson.core\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;jackson-annotations\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;2.11.1\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;com.fasterxml.jackson.core\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;jackson-databind\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;2.11.1\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;/dependencies\u0026gt; 在web.xml中声明ContextLoaderListener和DispatcherServlet\nContextLoaderListener用于初始化 IOC 容器，DispatcherServlet用于初始化 SpringMVC 相关内容\n\u0026lt;!-- 将 Spring 的配置信息以 context-param 的方式注册，并让ContextLoaderListener 进行读取--\u0026gt; \u0026lt;context-param\u0026gt; \u0026lt;param-name\u0026gt;contextConfigLocation\u0026lt;/param-name\u0026gt; \u0026lt;param-value\u0026gt;classpath:applicationContext.xml\u0026lt;/param-value\u0026gt; \u0026lt;/context-param\u0026gt; \u0026lt;listener\u0026gt; \u0026lt;listener-class\u0026gt;org.springframework.web.context.ContextLoaderListener\u0026lt;/listener-class\u0026gt; \u0026lt;/listener\u0026gt; \u0026lt;servlet\u0026gt; \u0026lt;servlet-name\u0026gt;springmvc\u0026lt;/servlet-name\u0026gt; \u0026lt;servlet-class\u0026gt;org.springframework.web.servlet.DispatcherServlet\u0026lt;/servlet-class\u0026gt; \u0026lt;!-- 指定 spring-mvc 配置文件路径 --\u0026gt; \u0026lt;init-param\u0026gt; \u0026lt;param-name\u0026gt;contextConfigLocation\u0026lt;/param-name\u0026gt; \u0026lt;param-value\u0026gt;classpath:spring-mvc.xml\u0026lt;/param-value\u0026gt; \u0026lt;/init-param\u0026gt; \u0026lt;!-- 让DispatcherServlet在应用启动时初始化 --\u0026gt; \u0026lt;load-on-startup\u0026gt;1\u0026lt;/load-on-startup\u0026gt; \u0026lt;/servlet\u0026gt; \u0026lt;servlet-mapping\u0026gt; \u0026lt;servlet-name\u0026gt;springmvc\u0026lt;/servlet-name\u0026gt; \u0026lt;url-pattern\u0026gt;/\u0026lt;/url-pattern\u0026gt; \u0026lt;/servlet-mapping\u0026gt; 处理请求响应中的中文乱码问题\n在web.xml中声明CharacterEncodingFilter用于处理请求中的乱码问题\n\u0026lt;filter\u0026gt; \u0026lt;filter-name\u0026gt;characterFilter\u0026lt;/filter-name\u0026gt; \u0026lt;filter-class\u0026gt;org.springframework.web.filter.CharacterEncodingFilter\u0026lt;/filter-class\u0026gt; \u0026lt;init-param\u0026gt; \u0026lt;param-name\u0026gt;encoding\u0026lt;/param-name\u0026gt; \u0026lt;param-value\u0026gt;UTF-8\u0026lt;/param-value\u0026gt; \u0026lt;/init-param\u0026gt; \u0026lt;/filter\u0026gt; \u0026lt;filter-mapping\u0026gt; \u0026lt;filter-name\u0026gt;characterFilter\u0026lt;/filter-name\u0026gt; \u0026lt;url-pattern\u0026gt;/*\u0026lt;/url-pattern\u0026gt; \u0026lt;/filter-mapping\u0026gt; 在spring-mvc.xml中使用\u0026lt;mvc:message-converters\u0026gt;标签处理响应中的乱码问题\n\u0026lt;mvc:message-converters\u0026gt; \u0026lt;bean class=\u0026#34;org.springframework.http.converter.StringHttpMessageConverter\u0026#34;\u0026gt; \u0026lt;property name=\u0026#34;supportedMediaTypes\u0026#34;\u0026gt; \u0026lt;list\u0026gt; \u0026lt;value\u0026gt;text/html;charset=utf-8\u0026lt;/value\u0026gt; \u0026lt;value\u0026gt;application/json;charset=utf-8\u0026lt;/value\u0026gt; \u0026lt;/list\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;/bean\u0026gt; \u0026lt;/mvc:message-converters\u0026gt; 创建spring-mvc.xml配置 spring mvc相关配置\n\u0026lt;!-- 指定扫描 controller 包 --\u0026gt; \u0026lt;context:component-scan base-package=\u0026#34;person.xianglin.reader.controller\u0026#34;/\u0026gt; \u0026lt;!-- 启用 mvc 注解开发 --\u0026gt; \u0026lt;mvc:annotation-driven\u0026gt; \u0026lt;mvc:message-converters\u0026gt; \u0026lt;bean class=\u0026#34;org.springframework.http.converter.StringHttpMessageConverter\u0026#34;\u0026gt; \u0026lt;property name=\u0026#34;supportedMediaTypes\u0026#34;\u0026gt; \u0026lt;list\u0026gt; \u0026lt;value\u0026gt;text/html;charset=utf-8\u0026lt;/value\u0026gt; \u0026lt;value\u0026gt;application/json;charset=utf-8\u0026lt;/value\u0026gt; \u0026lt;/list\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;/bean\u0026gt; \u0026lt;/mvc:message-converters\u0026gt; \u0026lt;/mvc:annotation-driven\u0026gt; \u0026lt;!-- 处理静态资源：将静态资源转发至 Tomcat 的默认Servlet 中 --\u0026gt; \u0026lt;mvc:default-servlet-handler/\u0026gt; 在spring-mvc.xml中整合模板引擎（Freemarker），同时在创建指定目录WEB-INF/ftl\n\u0026lt;bean id=\u0026#34;freeMarkerConfig\u0026#34; class=\u0026#34;org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer\u0026#34;\u0026gt; \u0026lt;property name=\u0026#34;templateLoaderPath\u0026#34; value=\u0026#34;/WEB-INF/ftl/\u0026#34;/\u0026gt; \u0026lt;property name=\u0026#34;freemarkerSettings\u0026#34;\u0026gt; \u0026lt;props\u0026gt; \u0026lt;prop key=\u0026#34;defaultEncoding\u0026#34;\u0026gt;UTF-8\u0026lt;/prop\u0026gt; \u0026lt;/props\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;/bean\u0026gt; \u0026lt;bean id=\u0026#34;viewResolver\u0026#34; class=\u0026#34;org.springframework.web.servlet.view.freemarker.FreeMarkerViewResolver\u0026#34;\u0026gt; \u0026lt;property name=\u0026#34;contentType\u0026#34; value=\u0026#34;text/html;charset=utf-8\u0026#34;/\u0026gt; \u0026lt;property name=\u0026#34;suffix\u0026#34; value=\u0026#34;.ftl\u0026#34;/\u0026gt; \u0026lt;/bean\u0026gt; 如果使用 JSP ，可配置InternalResourceViewResolver\n\u0026lt;bean id=\u0026#34;internalResourceViewResolver\u0026#34; class=\u0026#34;org.springframework.web.servlet.view.InternalResourceViewResolver\u0026#34;\u0026gt; \u0026lt;property name=\u0026#34;prefix\u0026#34; value=\u0026#34;/WEB-INF/jsp/\u0026#34;/\u0026gt; \u0026lt;property name=\u0026#34;suffix\u0026#34; value=\u0026#34;.jsp\u0026#34;/\u0026gt; \u0026lt;/bean\u0026gt; 编写测试用的TestController\n@Controller @RequestMapping(value = \u0026#34;/test\u0026#34;) public class TestController { @GetMapping(value = \u0026#34;/t1\u0026#34;) public ModelAndView test1() { ModelAndView modelAndView = new ModelAndView(\u0026#34;test\u0026#34;); modelAndView.addObject(\u0026#34;test\u0026#34;, \u0026#34;/test/t1\u0026#34;); return modelAndView; } @GetMapping(value = \u0026#34;/t2\u0026#34;) @ResponseBody public Map\u0026lt;String, Object\u0026gt; test2() { Map\u0026lt;String, Object\u0026gt; map = new HashMap\u0026lt;\u0026gt;(3); map.put(\u0026#34;success\u0026#34;, true); return map; } } Spring 与 MyBatis 整合 在pom.xml中引入依赖\n\u0026lt;dependencies\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-jdbc\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.mybatis\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;mybatis\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;3.5.5\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.mybatis\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;mybatis-spring\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;2.0.5\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;mysql\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;mysql-connector-java\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;8.0.20\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;com.alibaba\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;druid\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.1.23\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;/dependencies\u0026gt; 在applicationContext.xml中配置数据源与连接池\n\u0026lt;bean id=\u0026#34;dataSource\u0026#34; class=\u0026#34;com.alibaba.druid.pool.DruidDataSource\u0026#34;\u0026gt; \u0026lt;property name=\u0026#34;driverClassName\u0026#34; value=\u0026#34;com.mysql.cj.jdbc.Driver\u0026#34;/\u0026gt; \u0026lt;property name=\u0026#34;url\u0026#34; value=\u0026#34;jdbc:mysql://122.51.48.52:3306/imooc_reader?useSSL=false\u0026amp;amp;useUnicode=true\u0026#34;/\u0026gt; \u0026lt;property name=\u0026#34;username\u0026#34; value=\u0026#34;\u0026#34;/\u0026gt; \u0026lt;property name=\u0026#34;password\u0026#34; value=\u0026#34;\u0026#34;/\u0026gt; \u0026lt;property name=\u0026#34;initialSize\u0026#34; value=\u0026#34;5\u0026#34;/\u0026gt; \u0026lt;property name=\u0026#34;maxActive\u0026#34; value=\u0026#34;20\u0026#34;/\u0026gt; \u0026lt;/bean\u0026gt; 在applicationContext.xml中配置SqlSessionFactoryBean和MapperScannerConfigurer，用于将 MyBatis 的 SQLSession 和 Mapper 交于 Spring 管理\n\u0026lt;bean id=\u0026#34;sqlSessionFactory\u0026#34; class=\u0026#34;org.mybatis.spring.SqlSessionFactoryBean\u0026#34;\u0026gt; \u0026lt;property name=\u0026#34;dataSource\u0026#34; ref=\u0026#34;dataSource\u0026#34;/\u0026gt; \u0026lt;property name=\u0026#34;mapperLocations\u0026#34; value=\u0026#34;classpath:mappers/*.xml\u0026#34;/\u0026gt; \u0026lt;property name=\u0026#34;configLocation\u0026#34; value=\u0026#34;classpath:mybatis-config.xml\u0026#34;/\u0026gt; \u0026lt;/bean\u0026gt; \u0026lt;bean class=\u0026#34;org.mybatis.spring.mapper.MapperScannerConfigurer\u0026#34;\u0026gt; \u0026lt;property name=\u0026#34;basePackage\u0026#34; value=\u0026#34;person.xianglin.reader.mapper\u0026#34;/\u0026gt; \u0026lt;/bean\u0026gt; 配置其他组件 配置logback日志输出\n引入logback依赖\n\u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;ch.qos.logback\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;logback-classic\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.2.3\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; 创建logback.xml配置文件\n\u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34; ?\u0026gt; \u0026lt;configuration\u0026gt; \u0026lt;appender name=\u0026#34;console\u0026#34; class=\u0026#34;ch.qos.logback.core.ConsoleAppender\u0026#34;\u0026gt; \u0026lt;encoder class=\u0026#34;ch.qos.logback.classic.encoder.PatternLayoutEncoder\u0026#34;\u0026gt; \u0026lt;pattern\u0026gt;%d{HH:mm:ss} %-5level [%thread] %logger{30} -%msg%n\u0026lt;/pattern\u0026gt; \u0026lt;charset\u0026gt;UTF-8\u0026lt;/charset\u0026gt; \u0026lt;/encoder\u0026gt; \u0026lt;/appender\u0026gt; \u0026lt;root level=\u0026#34;debug\u0026#34;\u0026gt; \u0026lt;appender-ref ref=\u0026#34;console\u0026#34;/\u0026gt; \u0026lt;/root\u0026gt; \u0026lt;/configuration\u0026gt; 声明式事务配置\n在applicationContext.xml配置\n\u0026lt;bean id=\u0026#34;transactionManager\u0026#34; class=\u0026#34;org.springframework.jdbc.datasource.DataSourceTransactionManager\u0026#34;\u0026gt; \u0026lt;property name=\u0026#34;dataSource\u0026#34; ref=\u0026#34;dataSource\u0026#34;/\u0026gt; \u0026lt;/bean\u0026gt; \u0026lt;tx:annotation-driven/\u0026gt; 整合 JUnit 单元测试\n引入依赖\n\u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-test\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;junit\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;junit\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;4.13\u0026lt;/version\u0026gt; \u0026lt;scope\u0026gt;test\u0026lt;/scope\u0026gt; \u0026lt;/dependency\u0026gt; 编写测试类\n@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(locations = {\u0026#34;classpath:applicationContext.xml\u0026#34;}) public class TestServiceTest { @Resource private TestService testService; @Test public void insert() { testService.insert(); } } 配置 MyBatis-Plus 在pom.xml中引入依赖\n\u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;com.baomidou\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;mybatis-plus\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;3.4.0\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; 调整applicationContext.xml中的SqlSessionFactory\n\u0026lt;!-- \u0026lt;bean id=\u0026#34;sqlSessionFactory\u0026#34; class=\u0026#34;org.mybatis.spring.SqlSessionFactoryBean\u0026#34;\u0026gt;--\u0026gt; \u0026lt;bean id=\u0026#34;sqlSessionFactory\u0026#34; class=\u0026#34;com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean\u0026#34;\u0026gt; \u0026lt;property name=\u0026#34;dataSource\u0026#34; ref=\u0026#34;dataSource\u0026#34;/\u0026gt; \u0026lt;property name=\u0026#34;mapperLocations\u0026#34; value=\u0026#34;classpath:mappers/*.xml\u0026#34;/\u0026gt; \u0026lt;property name=\u0026#34;configLocation\u0026#34; value=\u0026#34;classpath:mybatis-config.xml\u0026#34;/\u0026gt; \u0026lt;/bean\u0026gt; 在mybatis-config.xml中增加分页插件\n\u0026lt;plugins\u0026gt; \u0026lt;plugin interceptor=\u0026#34;com.baomidou.mybatisplus.extension.plugins.PaginationInterceptor\u0026#34;/\u0026gt; \u0026lt;/plugins\u0026gt; MyBatis-Plus 使用步骤 创建实体类\n@Data @TableName(\u0026#34;test\u0026#34;) public class Test { @TableId(type = IdType.AUTO) @TableField(\u0026#34;id\u0026#34;) private Integer id; @TableField(\u0026#34;context\u0026#34;) private String context; } @TableName @TableId @TableField 创建 Mapper 接口和MapperXML\n继承自com.baomidou.mybatisplus.core.mapper.BaseMapper\npublic interface TestMapper extends BaseMapper\u0026lt;Test\u0026gt; { void insertSample(); } 注入 Mapper对象，通过内置 API 完成 CURD 操作\n@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(locations = {\u0026#34;classpath:applicationContext.xml\u0026#34;}) public class TestMapperTest { @Resource private TestMapper testMapper; @org.junit.Test public void testInsert() { Test test = new Test(); test.setContext(\u0026#34;MyBatis-Plus 测试\u0026#34;); testMapper.insert(test); } @org.junit.Test public void testUpdate() { Test test = testMapper.selectById(8); test.setContext(\u0026#34;测试 1111\u0026#34;); testMapper.updateById(test); } @org.junit.Test public void testSelectList() { QueryWrapper\u0026lt;Test\u0026gt; queryWrapper = new QueryWrapper\u0026lt;\u0026gt;(); queryWrapper.ge(\u0026#34;id\u0026#34;, 8); List\u0026lt;Test\u0026gt; selectList = testMapper.selectList(queryWrapper); Assert.assertEquals(selectList.size(), 1); } } insert、updateById、deleteById、selectById、selectList、selectPage Kaptcha 验证码 引入依赖\n\u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;com.github.penggle\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;kaptcha\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;2.3.2\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; 配置com.google.code.kaptcha.Producer实现类\n\u0026lt;bean id=\u0026#34;defaultKaptcha\u0026#34; class=\u0026#34;com.google.code.kaptcha.impl.DefaultKaptcha\u0026#34;\u0026gt; \u0026lt;property name=\u0026#34;config\u0026#34;\u0026gt; \u0026lt;bean class=\u0026#34;com.google.code.kaptcha.util.Config\u0026#34;\u0026gt; \u0026lt;constructor-arg name=\u0026#34;properties\u0026#34;\u0026gt; \u0026lt;props\u0026gt; \u0026lt;prop key=\u0026#34;kaptcha.border\u0026#34;\u0026gt;no\u0026lt;/prop\u0026gt; \u0026lt;prop key=\u0026#34;kaptcha.image.width\u0026#34;\u0026gt;120\u0026lt;/prop\u0026gt; \u0026lt;prop key=\u0026#34;kaptcha.textproducer.font.color\u0026#34;\u0026gt;blue\u0026lt;/prop\u0026gt; \u0026lt;prop key=\u0026#34;kaptcha.textproducer.font.size\u0026#34;\u0026gt;40\u0026lt;/prop\u0026gt; \u0026lt;prop key=\u0026#34;kaptcha.textproducer.char.length\u0026#34;\u0026gt;4\u0026lt;/prop\u0026gt; \u0026lt;/props\u0026gt; \u0026lt;/constructor-arg\u0026gt; \u0026lt;/bean\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;/bean\u0026gt; 在 controller 中使用\n@Controller public class KaptchaController { @Resource private Producer defaultKaptcha; @GetMapping(value = \u0026#34;/verify_code\u0026#34;) public void createVerifyCode(HttpServletRequest request, HttpServletResponse response) throws IOException { response.setDateHeader(\u0026#34;Expires\u0026#34;, 0); response.setHeader(\u0026#34;Cache-Control\u0026#34;, \u0026#34;no-store,no-cache,must-revalidate\u0026#34;); response.setHeader(\u0026#34;Cache-Control\u0026#34;, \u0026#34;post-check=0;pre-check=0\u0026#34;); response.setHeader(\u0026#34;Pragma\u0026#34;, \u0026#34;no-cache\u0026#34;); response.setContentType(\u0026#34;image/png\u0026#34;); String verifyCode = defaultKaptcha.createText(); request.getSession().setAttribute(\u0026#34;kaptchaVerifyCode\u0026#34;, verifyCode); BufferedImage kaptchaImage = defaultKaptcha.createImage(verifyCode); ServletOutputStream outputStream = response.getOutputStream(); ImageIO.write(kaptchaImage, \u0026#34;png\u0026#34;, outputStream); outputStream.flush(); outputStream.close(); } } ","permalink":"https://2d992714.xianglin2020-github-io.pages.dev/posts/2020/springmvc-basics/","summary":"\u003ch1 id=\"spring-mvc\"\u003eSpring MVC\u003c/h1\u003e\n\u003ch2 id=\"spring-mvc-环境搭建\"\u003eSpring MVC 环境搭建\u003c/h2\u003e\n\u003col\u003e\n\u003cli\u003e\n\u003cp\u003e引入 spring-mvc 相关依赖\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-xml\" data-lang=\"xml\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nt\"\u003e\u0026lt;dependencies\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"nt\"\u003e\u0026lt;dependency\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nt\"\u003e\u0026lt;groupId\u0026gt;\u003c/span\u003ejavax.servlet\u003cspan class=\"nt\"\u003e\u0026lt;/groupId\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nt\"\u003e\u0026lt;artifactId\u0026gt;\u003c/span\u003eservlet-api\u003cspan class=\"nt\"\u003e\u0026lt;/artifactId\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nt\"\u003e\u0026lt;version\u0026gt;\u003c/span\u003e2.5\u003cspan class=\"nt\"\u003e\u0026lt;/version\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nt\"\u003e\u0026lt;scope\u0026gt;\u003c/span\u003eprovided\u003cspan class=\"nt\"\u003e\u0026lt;/scope\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"nt\"\u003e\u0026lt;/dependency\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"nt\"\u003e\u0026lt;dependency\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nt\"\u003e\u0026lt;groupId\u0026gt;\u003c/span\u003eorg.springframework\u003cspan class=\"nt\"\u003e\u0026lt;/groupId\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nt\"\u003e\u0026lt;artifactId\u0026gt;\u003c/span\u003espring-webmvc\u003cspan class=\"nt\"\u003e\u0026lt;/artifactId\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"nt\"\u003e\u0026lt;/dependency\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nt\"\u003e\u0026lt;/dependencies\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e在\u003ccode\u003eweb.xml\u003c/code\u003e中配置\u003ccode\u003eDispatcherServlet\u003c/code\u003e\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-xml\" data-lang=\"xml\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nt\"\u003e\u0026lt;servlet\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"nt\"\u003e\u0026lt;servlet-name\u0026gt;\u003c/span\u003espringmvc\u003cspan class=\"nt\"\u003e\u0026lt;/servlet-name\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"nt\"\u003e\u0026lt;servlet-class\u0026gt;\u003c/span\u003eorg.springframework.web.servlet.DispatcherServlet\u003cspan class=\"nt\"\u003e\u0026lt;/servlet-class\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"nt\"\u003e\u0026lt;init-param\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nt\"\u003e\u0026lt;param-name\u0026gt;\u003c/span\u003econtextConfigLocation\u003cspan class=\"nt\"\u003e\u0026lt;/param-name\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nt\"\u003e\u0026lt;param-value\u0026gt;\u003c/span\u003eclasspath:applicationContext.xml\u003cspan class=\"nt\"\u003e\u0026lt;/param-value\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"nt\"\u003e\u0026lt;/init-param\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"nt\"\u003e\u0026lt;load-on-startup\u0026gt;\u003c/span\u003e0\u003cspan class=\"nt\"\u003e\u0026lt;/load-on-startup\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nt\"\u003e\u0026lt;/servlet\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nt\"\u003e\u0026lt;servlet-mapping\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"nt\"\u003e\u0026lt;servlet-name\u0026gt;\u003c/span\u003espringmvc\u003cspan class=\"nt\"\u003e\u0026lt;/servlet-name\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"nt\"\u003e\u0026lt;url-pattern\u0026gt;\u003c/span\u003e/\u003cspan class=\"nt\"\u003e\u0026lt;/url-pattern\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nt\"\u003e\u0026lt;/servlet-mapping\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e创建并配置\u003ccode\u003eapplicationContext.xml\u003c/code\u003e\u003c/p\u003e","title":"SpringMVC"},{"content":"VirtualBox VirtualBox 安装 VirtualBox 安装比较简单，参考其官方说明即可：Download VirtualBox。\nVBoxManage 基础命令 VBoxManage list 列出已安装的虚拟机\n➜ ~ VBoxManage list vms \u0026#34;fedora\u0026#34; {b9daec39-7e85-485f-969b-629fa52bd549} \u0026#34;centos\u0026#34; {49074fd5-386b-42ca-9e25-af29f5e42f1f} 列出正在运行的虚拟机\n➜ ~ VBoxManage list runningvms \u0026#34;centos\u0026#34; {49074fd5-386b-42ca-9e25-af29f5e42f1f} VBoxManage startvm 命令格式\n➜ ~ VBoxManage startvm Usage: VBoxManage startvm \u0026lt;uuid|vmname\u0026gt;... [--type gui|headless|separate] [-E|--putenv \u0026lt;NAME\u0026gt;[=\u0026lt;VALUE\u0026gt;]] 以窗口模式启动虚拟机\n➜ ~ VBoxManage startvm fedora --type gui Waiting for VM \u0026#34;fedora\u0026#34; to power on... VM \u0026#34;fedora\u0026#34; has been successfully started. 以无窗口模式启动虚拟机\nStarts a VM without a window for remote display only.\n➜ ~ VBoxManage startvm centos --type headless VBoxManage controlvm 命令格式\n➜ ~ VBoxManage controlvm Usage: VBoxManage controlvm \u0026lt;uuid|vmname\u0026gt; pause|resume|reset|poweroff|savestate| acpipowerbutton|acpisleepbutton| 暂停虚拟机\n➜ ~ VBoxManage controlvm fedora pause 恢复已暂停的虚拟机\n➜ ~ VBoxManage controlvm fedora resume 关闭虚拟机：相当于拉闸断电\n➜ ~ VBoxManage controlvm fedora poweroff 0%...10%...20%...30%...40%...50%...60%...70%...80%...90%...100% 关闭虚拟机：正常关闭\n➜ ~ VBoxManage controlvm fedora acpipowerbutton 休眠虚拟机\n➜ ~ VBoxManage controlvm fedora savestate 0%...10%...20%...30%...40%...50%...60%...70%...80%...90%...100% VBoxManage guestproperty 获取或者设置正在运行的虚拟机的属性\n命令格式\n➜ ~ VBoxManage guestproperty Usage: VBoxManage guestproperty get \u0026lt;uuid|vmname\u0026gt; \u0026lt;property\u0026gt; [--verbose] VBoxManage guestproperty set \u0026lt;uuid|vmname\u0026gt; \u0026lt;property\u0026gt; [\u0026lt;value\u0026gt; [--flags \u0026lt;flags\u0026gt;]] VBoxManage guestproperty delete|unset \u0026lt;uuid|vmname\u0026gt; \u0026lt;property\u0026gt; VBoxManage guestproperty enumerate \u0026lt;uuid|vmname\u0026gt; [--patterns \u0026lt;patterns\u0026gt;] VBoxManage guestproperty wait \u0026lt;uuid|vmname\u0026gt; \u0026lt;patterns\u0026gt; [--timeout \u0026lt;msec\u0026gt;] [--fail-on-timeout] 获取当前运行虚拟机的 IP 地址\n➜ ~ VBoxManage guestproperty enumerate centos | grep \u0026#34;Net.*V4.*IP\u0026#34; Name: /VirtualBox/GuestInfo/Net/0/V4/IP, value: 10.0.2.15, timestamp: 1607138767607688000, flags: Name: /VirtualBox/GuestInfo/Net/1/V4/IP, value: 192.168.56.106, timestamp: 1607138767607908000, flags: ➜ ~ VBoxManage guestproperty get centos \u0026#39;/VirtualBox/GuestInfo/Net/1/V4/IP\u0026#39; Value: 192.168.56.106 ","permalink":"https://2d992714.xianglin2020-github-io.pages.dev/posts/2020/virtualbox-basics/","summary":"\u003ch1 id=\"virtualbox\"\u003eVirtualBox\u003c/h1\u003e\n\u003ch2 id=\"virtualbox-安装\"\u003eVirtualBox 安装\u003c/h2\u003e\n\u003cp\u003eVirtualBox 安装比较简单，参考其官方说明即可：\u003ca href=\"https://www.virtualbox.org/wiki/Downloads\" target=\"_blank\" rel=\"noopener\"\u003eDownload VirtualBox\u003c/a\u003e。\u003c/p\u003e\n\u003ch2 id=\"vboxmanage-基础命令\"\u003eVBoxManage 基础命令\u003c/h2\u003e\n\u003ch3 id=\"vboxmanage-list\"\u003e\u003ccode\u003eVBoxManage list\u003c/code\u003e\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e列出已安装的虚拟机\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-shell\" data-lang=\"shell\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e➜  ~ VBoxManage list vms\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"s2\"\u003e\u0026#34;fedora\u0026#34;\u003c/span\u003e \u003cspan class=\"o\"\u003e{\u003c/span\u003eb9daec39-7e85-485f-969b-629fa52bd549\u003cspan class=\"o\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"s2\"\u003e\u0026#34;centos\u0026#34;\u003c/span\u003e \u003cspan class=\"o\"\u003e{\u003c/span\u003e49074fd5-386b-42ca-9e25-af29f5e42f1f\u003cspan class=\"o\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e列出正在运行的虚拟机\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-shell\" data-lang=\"shell\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e➜  ~ VBoxManage list runningvms\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"s2\"\u003e\u0026#34;centos\u0026#34;\u003c/span\u003e \u003cspan class=\"o\"\u003e{\u003c/span\u003e49074fd5-386b-42ca-9e25-af29f5e42f1f\u003cspan class=\"o\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"vboxmanage-startvm\"\u003e\u003ccode\u003eVBoxManage startvm\u003c/code\u003e\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e命令格式\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-shell\" data-lang=\"shell\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e➜  ~ VBoxManage startvm\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eUsage:\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eVBoxManage startvm          \u0026lt;uuid\u003cspan class=\"p\"\u003e|\u003c/span\u003evmname\u0026gt;...\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                            \u003cspan class=\"o\"\u003e[\u003c/span\u003e--type gui\u003cspan class=\"p\"\u003e|\u003c/span\u003eheadless\u003cspan class=\"p\"\u003e|\u003c/span\u003eseparate\u003cspan class=\"o\"\u003e]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                            \u003cspan class=\"o\"\u003e[\u003c/span\u003e-E\u003cspan class=\"p\"\u003e|\u003c/span\u003e--putenv \u0026lt;NAME\u0026gt;\u003cspan class=\"o\"\u003e[=\u003c/span\u003e\u0026lt;VALUE\u0026gt;\u003cspan class=\"o\"\u003e]]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e以窗口模式启动虚拟机\u003c/p\u003e","title":"VirtualBox 的安装和使用"},{"content":"Spring Spring IOC IOC：控制反转 IOC 控制反转，全称 Inverse of Control，是一种设计理念。 由代理人来创建与管理对象，消费者通过代理人获取对象。 IoC 的目的是降低对象之间的直接耦合。 DI：依赖注入 IOC 是设计理念，是现代程序设计遵循的标准，是宏观目标。 DI（Dependency Injection）是具体技术实现。 DI 在 Java 中利用反射技术实现对象注入。 Bean 配置方式 基于 XML 配置 JavaBean：applicationContext.xml 基于注解配置 JavaBean 基于 Java 代码配置 JavaBean 路径表达式：\n实例化 Bean 的三种方式 基于对象构造方法实例化：使用constructor-arg标签指定构造器参数\n\u0026lt;bean id=\u0026#34;sourApple\u0026#34; class=\u0026#34;org.example.entity.Apple\u0026#34;\u0026gt; \u0026lt;constructor-arg name=\u0026#34;title\u0026#34; value=\u0026#34;红富士\u0026#34;/\u0026gt; \u0026lt;constructor-arg name=\u0026#34;color\u0026#34; value=\u0026#34;红色\u0026#34;/\u0026gt; \u0026lt;constructor-arg name=\u0026#34;origin\u0026#34; value=\u0026#34;欧洲\u0026#34;/\u0026gt; \u0026lt;constructor-arg name=\u0026#34;price\u0026#34; value=\u0026#34;19.8\u0026#34;/\u0026gt; \u0026lt;/bean\u0026gt; \u0026lt;bean id=\u0026#34;softApple\u0026#34; class=\u0026#34;org.example.entity.Apple\u0026#34;\u0026gt; \u0026lt;constructor-arg index=\u0026#34;0\u0026#34; value=\u0026#34;金帅\u0026#34;/\u0026gt; \u0026lt;constructor-arg index=\u0026#34;1\u0026#34; value=\u0026#34;中国\u0026#34;/\u0026gt; \u0026lt;constructor-arg index=\u0026#34;2\u0026#34; value=\u0026#34;黄色\u0026#34;/\u0026gt; \u0026lt;constructor-arg index=\u0026#34;3\u0026#34; value=\u0026#34;10.90\u0026#34;/\u0026gt; \u0026lt;/bean\u0026gt; 基于静态工厂实例化对象\npublic class AppleStaticFactory { public static Apple createSweetApple() { return new Apple(\u0026#34;红富士\u0026#34;, \u0026#34;红色\u0026#34;, \u0026#34;欧洲\u0026#34;); } } \u0026lt;bean id=\u0026#34;apple4\u0026#34; class=\u0026#34;org.example.factory.AppleStaticFactory\u0026#34; factory-method=\u0026#34;createSweetApple\u0026#34;/\u0026gt; 基于工厂实例方法实例化对象\npublic class AppleFactoryInstance { public Apple createSweetApple() { return new Apple(\u0026#34;红富士\u0026#34;, \u0026#34;红色\u0026#34;, \u0026#34;欧洲\u0026#34;); } } \u0026lt;bean id=\u0026#34;factoryInstance\u0026#34; class=\u0026#34;org.example.factory.AppleFactoryInstance\u0026#34;/\u0026gt; \u0026lt;bean id=\u0026#34;apple5\u0026#34; factory-bean=\u0026#34;factoryInstance\u0026#34; factory-method=\u0026#34;createSweetApple\u0026#34;/\u0026gt; 从 IoC 容器中获取 Bean Object getBean(String var1) throws BeansException; \u0026lt;T\u0026gt; T getBean(String var1, Class\u0026lt;T\u0026gt; var2) throws BeansException; Object getBean(String var1, Object... var2) throws BeansException; \u0026lt;T\u0026gt; T getBean(Class\u0026lt;T\u0026gt; var1) throws BeansException; \u0026lt;T\u0026gt; T getBean(Class\u0026lt;T\u0026gt; var1, Object... var2) throws BeansException; 对象依赖注入 基于 setter 方法注入对象\n注入静态数值\n\u0026lt;property name=\u0026quot;\u0026quot; value=\u0026quot;\u0026quot;/\u0026gt;\n注入对象\n\u0026lt;peoperty name=\u0026quot;\u0026quot; ref=\u0026quot;\u0026quot;/\u0026gt;\n基于构造器方法注入对象\n\u0026lt;constructor-arg name=\u0026#34;name\u0026#34; value=\u0026#34;lili\u0026#34;/\u0026gt; \u0026lt;constructor-arg name=\u0026#34;apple\u0026#34; ref=\u0026#34;softApple\u0026#34;/\u0026gt; 使用注解方式注入属性\n配置 Bean 扫描包\n\u0026lt;context:component-scan base-package=\u0026#34;\u0026#34;/\u0026gt; 使用组件注解标识类\n@Repository @Service @Controller @Component 配置@Resource或@Autowired注入属性\n@Resource @Autowired 使用 JavaConfig 注入依赖\n使用@Configuration注解标识此为配置类\n使用@Bean注解创建对象交由容器管理\n注入 list、set、map、properties\n\u0026lt;bean id=\u0026#34;company\u0026#34; class=\u0026#34;org.example.entity.Company\u0026#34;\u0026gt; \u0026lt;property name=\u0026#34;rooms\u0026#34;\u0026gt; \u0026lt;!-- ArrayList --\u0026gt; \u0026lt;list\u0026gt; \u0026lt;value\u0026gt;room1\u0026lt;/value\u0026gt; \u0026lt;value\u0026gt;room2\u0026lt;/value\u0026gt; \u0026lt;value\u0026gt;room3\u0026lt;/value\u0026gt; \u0026lt;/list\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;property name=\u0026#34;jobs\u0026#34;\u0026gt; \u0026lt;!-- LinkedHashSet --\u0026gt; \u0026lt;set\u0026gt; \u0026lt;value\u0026gt;job1\u0026lt;/value\u0026gt; \u0026lt;value\u0026gt;job1\u0026lt;/value\u0026gt; \u0026lt;value\u0026gt;job2\u0026lt;/value\u0026gt; \u0026lt;/set\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;property name=\u0026#34;computers\u0026#34;\u0026gt; \u0026lt;!-- LinkedHashMap --\u0026gt; \u0026lt;map\u0026gt; \u0026lt;entry key=\u0026#34;lenovo\u0026#34; value-ref=\u0026#34;computer\u0026#34;/\u0026gt; \u0026lt;entry key=\u0026#34;sumsun\u0026#34;\u0026gt; \u0026lt;bean class=\u0026#34;org.example.entity.Computer\u0026#34;\u0026gt; \u0026lt;property name=\u0026#34;band\u0026#34; value=\u0026#34;sumsun\u0026#34;/\u0026gt; \u0026lt;/bean\u0026gt; \u0026lt;/entry\u0026gt; \u0026lt;/map\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;property name=\u0026#34;info\u0026#34;\u0026gt; \u0026lt;props\u0026gt; \u0026lt;prop key=\u0026#34;address\u0026#34;\u0026gt;北京市\u0026lt;/prop\u0026gt; \u0026lt;prop key=\u0026#34;website\u0026#34;\u0026gt;www.baidu.com\u0026lt;/prop\u0026gt; \u0026lt;/props\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;/bean\u0026gt; Bean 作用域 Bean 的 scope 属性默认是 singleton 单例模式，会在容器初始化时被创建 prototype 表示多例，容器初始化时不会创建，在使用时才创建 lazy-init 为 true 时表示将对象的创建延迟到使用时创建，主要用于延迟初始化单例对象 Bean 声明周期 使用 XML 定义的init-method和destroy-method方法\n实现InitializingBean和 DisposableBean中的回调方法\npublic class Order implements InitializingBean, DisposableBean { private Float price; private Integer quantity; private Float total; public Order() { System.out.println(\u0026#34;创建 Order:\u0026#34; + this); } public void initMethod() { System.out.println(\u0026#34;执行 initMethod\u0026#34;); this.total = this.price * this.quantity; } public void destroyMethod() { System.out.println(\u0026#34;执行 destroyMethod\u0026#34;); } @Override public void afterPropertiesSet() throws Exception { System.out.println(\u0026#34;执行 afterPropertiesSet\u0026#34;); } @Override public void destroy() throws Exception { System.out.println(\u0026#34;执行 destroy\u0026#34;); } } \u0026lt;bean id=\u0026#34;order1\u0026#34; class=\u0026#34;org.example.entity.Order\u0026#34; init-method=\u0026#34;initMethod\u0026#34; destroy-method=\u0026#34;destroyMethod\u0026#34;\u0026gt; \u0026lt;property name=\u0026#34;price\u0026#34; value=\u0026#34;19.3\u0026#34;/\u0026gt; \u0026lt;property name=\u0026#34;quantity\u0026#34; value=\u0026#34;1000\u0026#34;/\u0026gt; \u0026lt;/bean\u0026gt; 基于注解配置 IoC 容器 Spring 从 2.5 开始支持注解来配置依赖注入\n四种组件类型注解 两类自动装配注解 @Autowried注解在属性和 setter 方法的不同\n@Autowried注解在存在多个类型相同实例时指定注入实例的方法\n在需要被注入实例上增加@Primary\n在@Autowired注解的字段或方法上增加@Qualifier注解，指定需要注入的对象\n@Qualifier优先级大于@Primary\n元数据注解 基于Java 的配置 Spring 与 JUnit 整合 @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes = Config.class) public class UserServiceTest { @Resource private UserService userService; @Test public void test1() { userService.test(); } } Spring AOP AOP 关键概念 AspectJ一种基于Java 平台的面向切面编程的语言 Spring 使用aspectjweaver实现类与方法匹配 SpringAOP 使用代理模式实现对象运行时扩展 AOP 配置过程 引入aspectj依赖\n\u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.aspectj\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;aspectjweaver\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.9.5\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; 编写切面类和切面方法\n配置Aspect Bean\n\u0026lt;bean id=\u0026#34;methodChecker\u0026#34; class=\u0026#34;org.example.aspect.MethodChecker\u0026#34;/\u0026gt; 定义 pointcut\n定义切面类、配置 advice\n\u0026lt;aop:config\u0026gt; \u0026lt;aop:pointcut id=\u0026#34;pointcut\u0026#34; expression=\u0026#34;execution(* *(..))\u0026#34;/\u0026gt; \u0026lt;aop:aspect ref=\u0026#34;methodChecker\u0026#34;\u0026gt; \u0026lt;aop:around method=\u0026#34;check\u0026#34; pointcut-ref=\u0026#34;pointcut\u0026#34;/\u0026gt; \u0026lt;/aop:aspect\u0026gt; \u0026lt;/aop:config\u0026gt; JoinPoint核心方法 Object getTarget(); Object[] getArgs(); Signature getSignature(); PointCut切点表达式 execution(modifiers-pattern? ret-type-pattern declaring-type-pattern?name-pattern(param-pattern)throws-pattern?)\nexecution(public * org.example..*.*(..))\n* 通配符 .. 包通配符 (..) 参数通配符 aop-pointcuts-example\n五种通知类型 使用注解配置 AOP @Around、@Before、@After、@AfterReturning、@AfterThrowing\nSpring AOP 的实现原理 Spring 基于代理模式实现功能扩展 目标类实现接口，通过 JDK动态代理实现功能扩展 com.sun.proxy.$Proxy20 目标类没有实现接口，通过 CGLib组件实现功能扩展 EmployeeService$$EnhancerBySpringCGLIB$$ed94134b Spring JDBC Spring JDBC Spring JDBC 是Spring 框架对用于处理关系型数据库的模块 Spring JDBC对 JDBC API进行封装 JDBCTemplate是 SpringJDBC 的核心类，提供数据的 CURD 操作 Spring JDBC使用步骤 引入spring-jdbc依赖\n\u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-context\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-jdbc\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;mysql\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;mysql-connector-java\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;8.0.20\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; 在applicationContext.xml中配置数据源\n\u0026lt;!-- 数据源 --\u0026gt; \u0026lt;bean id=\u0026#34;dataSource\u0026#34; class=\u0026#34;org.springframework.jdbc.datasource.DriverManagerDataSource\u0026#34;\u0026gt; \u0026lt;property name=\u0026#34;driverClassName\u0026#34; value=\u0026#34;com.mysql.cj.jdbc.Driver\u0026#34;/\u0026gt; \u0026lt;property name=\u0026#34;url\u0026#34; value=\u0026#34;jdbc:mysql://122.51.48.52:3306/imooc_demo?useSSL=false\u0026amp;amp;useUnicode=true\u0026#34;/\u0026gt; \u0026lt;property name=\u0026#34;username\u0026#34; value=\u0026#34;\u0026#34;/\u0026gt; \u0026lt;property name=\u0026#34;password\u0026#34; value=\u0026#34;\u0026#34;/\u0026gt; \u0026lt;/bean\u0026gt; \u0026lt;bean id=\u0026#34;jdbcTemplate\u0026#34; class=\u0026#34;org.springframework.jdbc.core.JdbcTemplate\u0026#34;\u0026gt; \u0026lt;property name=\u0026#34;dataSource\u0026#34; ref=\u0026#34;dataSource\u0026#34;/\u0026gt; \u0026lt;/bean\u0026gt; \u0026lt;bean id=\u0026#34;employeeDao\u0026#34; class=\u0026#34;org.example.dao.EmployeeDao\u0026#34;\u0026gt; \u0026lt;property name=\u0026#34;jdbcTemplate\u0026#34; ref=\u0026#34;jdbcTemplate\u0026#34;/\u0026gt; \u0026lt;/bean\u0026gt; 在 dao 对象中注入JdbcTemplate依赖\npublic class EmployeeDao { private JdbcTemplate jdbcTemplate; public JdbcTemplate getJdbcTemplate() { return jdbcTemplate; } public void setJdbcTemplate(JdbcTemplate jdbcTemplate) { this.jdbcTemplate = jdbcTemplate; } } Spring JDBC 事务管理 编程式事务 TransactionManger\n\u0026lt;!-- 事务管理器 --\u0026gt; \u0026lt;bean id=\u0026#34;transactionManager\u0026#34; class=\u0026#34;org.springframework.jdbc.datasource.DataSourceTransactionManager\u0026#34;\u0026gt; \u0026lt;property name=\u0026#34;dataSource\u0026#34; ref=\u0026#34;dataSource\u0026#34;/\u0026gt; \u0026lt;/bean\u0026gt; // 事务标准配置 TransactionDefinition definition = new DefaultTransactionDefinition(); // 开始事务，返回事务状态 TransactionStatus transaction = transactionManager.getTransaction(definition); // 提交事务 transactionManager.commit(transaction); // 回滚事务 transactionManager.rollback(transaction); 声明式事务\n\u0026lt;tx:advice id=\u0026#34;txAdvice\u0026#34; transaction-manager=\u0026#34;transactionManager\u0026#34;\u0026gt; \u0026lt;tx:attributes\u0026gt; \u0026lt;tx:method name=\u0026#34;batchImport\u0026#34;/\u0026gt; \u0026lt;tx:method name=\u0026#34;find*\u0026#34; propagation=\u0026#34;NOT_SUPPORTED\u0026#34; read-only=\u0026#34;true\u0026#34;/\u0026gt; \u0026lt;tx:method name=\u0026#34;*\u0026#34; propagation=\u0026#34;NOT_SUPPORTED\u0026#34; read-only=\u0026#34;true\u0026#34;/\u0026gt; \u0026lt;/tx:attributes\u0026gt; \u0026lt;/tx:advice\u0026gt; \u0026lt;aop:config\u0026gt; \u0026lt;aop:pointcut id=\u0026#34;pointcut\u0026#34; expression=\u0026#34;execution(* org.example..*Service.*(..))\u0026#34;/\u0026gt; \u0026lt;aop:advisor advice-ref=\u0026#34;txAdvice\u0026#34; pointcut-ref=\u0026#34;pointcut\u0026#34;/\u0026gt; \u0026lt;/aop:config\u0026gt; 事务传播行为 事务传播行为是指多个拥有事务控制的方法在嵌套调用时的事物控制方式\n\u0026lt;tx:method name=\u0026quot;batchImport\u0026quot; propagation=\u0026quot;REQUIRED\u0026quot;/\u0026gt;\n@Transactional(propagation = Propagation.REQUIRED)\n七种事务传播行为：org.springframework.transaction.annotation.Propagation\n","permalink":"https://2d992714.xianglin2020-github-io.pages.dev/posts/2020/spring-basics/","summary":"\u003ch1 id=\"spring\"\u003eSpring\u003c/h1\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"https://cdn.jsdelivr.net/gh/xianglin2020/gallery@master/202010/153710.png\" alt=\"image-20201005094800988\"  /\u003e\n\u003c/p\u003e\n\u003ch2 id=\"spring-ioc\"\u003eSpring IOC\u003c/h2\u003e\n\u003ch3 id=\"ioc控制反转\"\u003eIOC：控制反转\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003eIOC 控制反转，全称 Inverse of Control，是一种设计理念。\u003c/li\u003e\n\u003cli\u003e由代理人来创建与管理对象，消费者通过代理人获取对象。\u003c/li\u003e\n\u003cli\u003eIoC 的目的是降低对象之间的直接耦合。\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"di依赖注入\"\u003eDI：依赖注入\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003eIOC 是设计理念，是现代程序设计遵循的标准，是宏观目标。\u003c/li\u003e\n\u003cli\u003eDI（Dependency Injection）是具体技术实现。\u003c/li\u003e\n\u003cli\u003eDI 在 Java 中利用反射技术实现对象注入。\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"bean-配置方式\"\u003eBean 配置方式\u003c/h3\u003e\n\u003col\u003e\n\u003cli\u003e基于 XML 配置 JavaBean：\u003ccode\u003eapplicationContext.xml\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e基于注解配置 JavaBean\u003c/li\u003e\n\u003cli\u003e基于 Java 代码配置 JavaBean\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e路径表达式：\u003c/p\u003e","title":"Spring 基础"},{"content":" Redis 介绍 Redis 简介 Redis 是 key-value 型 NoSQL 数据库 Redis 将数据存储在内存中，同时也能持久化到磁盘中 Redis 常用于缓存，利用内存的高效提高程序的处理速度 Redis 特点 速度快 广泛的语言支持 支持数据磁盘持久化 多种数据结构 支持主从复制 支持分片 分布式与高可用 Redis 常用配置项 配置 示例 说明 daemonize daemonize yes 是否启用后台运行 port port 6379 设置端口号 databases databases 16 设置 redis 数据库个数 requirepass requirepass abc123 设置 redis 连接密码 Redis通用命令 select set get keys dbsize exists del expire ttl Redis 数据结构 string：字符串，值的长度不能超过 512 MB，是二进制安全的。 set key value [EX seconds] [PX milliseconds] [NX|XX] EX：将键的过期时间设置为seconds秒，set key value ex seconds等同于setex key seconds value PX：将键的过期时间设置为milliseconds毫秒，与psetex key milliseconds value 等价 NX|XX：只有在键不存在|存在时，才对键执行设置操作，set key value NX等同于setnx key value mset key value [key value ...] mget key [key ...] incr key | incrby k increment decr key | decrby key decrement exists key [key ...] del key [key ...] expire key seconds ttl key hash：键值类型 list：列表，Redis lists基于Linked Lists实现。 set：无序排列 zset：有序排列 Redis Redis 为啥这么快 Redis 支持 100000+QPS。\n完全基于内存，绝大部分请求是纯粹的内存操作，执行效率高 数据结构简单，对数据操作也简单 采用单线程，单线程能处理高并发请求 使用 I/O 多路复用，Redis 使用的 I/O 多路复用函数：epoll、kqueue、evport 传统的阻塞 I / O 模型\nRedis 数据类型 String：最基本的数据类型，二进制安全。 Hash：键值类型，String 元素组成的字典，适用于存储对象。 List：列表，按照 String 元素插入顺序排序。 Set：String 元素组成的无序集合，基于哈希表实现，不允许重复。 Sorted Set：通过分数来为集合中的成员进行从小到大的排序。 用于计数的 HyperLogLogs，用于支持存储地址位置信息的 Geo 从海量 key 里查询出某一个固定前缀的 key KEYS pattern：查找所有符合给定模式 pattern 的 key。\nKEYS 指令一次性返回所有匹配的 key。 键的数量过大会使服务卡顿。 SCAN cursor [MATCH pattern] [COUNT count] [Type type]\n基于游标的迭代器，需要基于上一次的游标延续之前的迭代过程。 以 0 作为游标开始一次新的迭代，直到命令返回游标 0 完成一次遍历。 不保证每次执行都返回某个给定数量的元素，支持模糊查询。 一次返回的数量不可控，只能是大概率符合 count 参数。 通过 Redis 实现分布式锁 分布式锁需要解决的问题：\n互斥性 安全性 死锁 容错 SETNX key value ：如果 key 不存在，则创建并赋值。\n时间复杂度：O(1) 。 返回值：设置成功，返回 1 ；设置失败，返回 0 。 SETNX 是原子操作。 使用 EXPIRE key seconds 设置过期时间\nset key value [EX seconds] [PX milliseconds] [NX|XX]\nEX：将键的过期时间设置为seconds秒，set key value ex seconds等同于setex key seconds value PX：将键的过期时间设置为milliseconds毫秒，与psetex key milliseconds value 等价 NX|XX：只有在键不存在、存在时，才对键执行设置操作，set key value NX等同于setnx key value 使用 Redis 实现异步队列 方式一：\n使用 Redis 的 List 数据类型作为队列，使用 RPUSH 生产消息和 LPOP 消费消息。\n缺点：不能阻塞等待消息。\n方式二：\n使用 BLPOP key [key...] timeout ：阻塞直到队列有消息或者超时。\n缺点：消息不能被多个消费者使用。\npub / sub ：主题订阅模式：\n发送者 pub 发送消息，订阅者 sub 接收消息；订阅者可以订阅任意数量的频道。\n缺点：消息的发布是无状态的，无法保证可达。\nRedis 持久化 RDB（快照）持久化，保存某个时间点的全量数据的快照。\nSAVE：阻塞 Redis 的服务器进程，直到 RDB 文件被创建完毕。 BGSAVE：fork 出一个子进程来创建 RDB 文件，不阻塞服务器进程。 lastsave：上次成功保存快照的时间戳。 缺点：\n内存数据的全量同步，数据量大会由于 I/O 而严重影响性能。 可能因为 Redis 服务异常丢失从当前至最近一次快照期间的数据。 自动触发 RDB 持久化的方式：\n根据 redis.conf 配置的触发时机定时触发（使用 BGSAVE）。 主从复制时，主节点自动触发。 执行 Debug Reload。 执行 shutdown 且没有开启 AOF 持久化。 AOF（Append-Only-File）持久化：保存写状态\n记录下除了查询以外的所有变更数据库状态的指令。 以 append 的形式追加保存到 AOF 文件中。 使用 appendonly yes 配置开启 AOF 持久化。 appendfsync everysec 日志重写解决 AOF 文件不断增大的问题：\n在执行 BGREWRITEAOF 命令时,Redis 服务器会维护一个 AOF 重写缓冲区,该缓冲区会在子进程创建 新AOF文件期间,记录服务器执行的所有写命令。当子进程完成创建新AOF文件的工作之后,服务器会将 重写缓冲区中的所有内容追加到新AOF文件的末尾,使得新旧两个AOF文件所保存的数据库状态一致。最 后,服务器用新的AOF文件替换旧的AOF文件,以此来完成AOF文件重写操作。\n调用 fork，新建一个子进程； 子进程把新的 AOF 写到一个临时文件里，不依赖原来的 AOF 文件； 主基础持续将新的变动同时写到内存和原来的 AOF 文件里； 主进程获取子进程重写 AOF 的完成信号，往新的 AOF 同步增量变动； 使用新的 AOF 文件替换掉旧的 AOF 文件。 混合持久化\nRedis 4.0 开始支持 RDB 和 AOF 的混合持久化（默认关闭,可以通过配置项 aof-use-rdb-preamble 开启）。\nAOF 重写的时候就直接把 RDB 的内容写到 AOF 文件开头。这样做的好处是可以结合 RDB 和 AOF 的优点, 快速加载同时避免丢失过多的数据。当然缺点也是有的, AOF 里面的 RDB 部分是压缩格式不再是 AOF 格式,可读性较差。\n为什么使用 Pipline Redis 是一种基于客户端-服务端模型及请求/响应协议的 TCP 服务，Pipeline 批量执行指令，节省 IO 时间。\nRedis 的同步机制 全同步过程\nSlave 发送 sync 命令到 Master Master 启动一个后台进程，将 Redis 中的数据快照保存到文件中 Master 将保存数据快照期间接收到的写命令缓存起来 Master 完成写文件操作后，将该文件发送给 Slave Slave 使用新的 RDB 文件替换掉旧的 RDB 文件，并根据其恢复到内存中 Master 将这期间收集的增量写命令发送给 Slave 端 增量同步过程\nMaster 接受到用户的操作指令，判断是否需要传播到 Slave 将该操作记录追加到 AOF 文件 将操作传播到其他 Slave：1. 对齐主从库；2. 往响应缓存写入指令 将缓存中的数据发送给 Slave Redis Sentinel\n解决主从同步 Master 宕机后的主从切换问题\n监控：检查主从服务器是否运行正常 提醒：通过 API 向管理员或其它应用程序发送故障通知 自动故障迁移：主从切换 Redis集群原理 一致性哈希算法：对 2^32 取模，将哈希值空间组成虚拟的圆环。 将数据 key 使用相同的 hash 函数计算出哈希值。 引入虚拟节点解决数据倾斜的问题。 Redis 集群的数据分片 Redis 集群有 16384 个哈希槽，每个 key 通过 CRC16 校验后对 16384 取模来决定放置在哪个槽，集群中的每个节点负责一部分哈希槽。\n","permalink":"https://2d992714.xianglin2020-github-io.pages.dev/posts/2020/redis-basics/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"https://cdn.jsdelivr.net/gh/xianglin2020/gallery/202101/Redis%E7%BC%93%E5%AD%98.png\" alt=\"Redis 缓存\"  /\u003e\n\u003c/p\u003e\n\u003ch2 id=\"redis-介绍\"\u003eRedis 介绍\u003c/h2\u003e\n\u003ch3 id=\"redis-简介\"\u003eRedis 简介\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003eRedis 是 \u003ccode\u003ekey-value\u003c/code\u003e 型 NoSQL 数据库\u003c/li\u003e\n\u003cli\u003eRedis 将数据存储在内存中，同时也能持久化到磁盘中\u003c/li\u003e\n\u003cli\u003eRedis 常用于缓存，利用内存的高效提高程序的处理速度\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"redis-特点\"\u003eRedis 特点\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e速度快\u003c/li\u003e\n\u003cli\u003e广泛的语言支持\u003c/li\u003e\n\u003cli\u003e支持数据磁盘持久化\u003c/li\u003e\n\u003cli\u003e多种数据结构\u003c/li\u003e\n\u003cli\u003e支持主从复制\u003c/li\u003e\n\u003cli\u003e支持分片\u003c/li\u003e\n\u003cli\u003e分布式与高可用\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"redis-常用配置项\"\u003eRedis 常用配置项\u003c/h3\u003e\n\u003ctable\u003e\n\t\u003cthead\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003cth\u003e配置\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003e示例\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003e说明\u003c/th\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/thead\u003e\n\t\u003ctbody\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003ccode\u003edaemonize\u003c/code\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e\u003ccode\u003edaemonize yes\u003c/code\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e是否启用后台运行\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003ccode\u003eport\u003c/code\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e\u003ccode\u003eport 6379\u003c/code\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e设置端口号\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003ccode\u003edatabases\u003c/code\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e\u003ccode\u003edatabases 16\u003c/code\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e设置 redis 数据库个数\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003ccode\u003erequirepass\u003c/code\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e\u003ccode\u003erequirepass abc123\u003c/code\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e设置 redis 连接密码\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch3 id=\"redis通用命令\"\u003eRedis通用命令\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ccode\u003eselect\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003eset\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003eget\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003ekeys\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003edbsize\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003eexists\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003edel\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003eexpire\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003ettl\u003c/code\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"redis-数据结构\"\u003eRedis 数据结构\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ccode\u003estring\u003c/code\u003e：字符串，值的长度不能超过 512 MB，是二进制安全的。\n\u003col\u003e\n\u003cli\u003e\u003ccode\u003eset key value [EX seconds] [PX milliseconds] [NX|XX]\u003c/code\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ccode\u003eEX\u003c/code\u003e：将键的过期时间设置为\u003ccode\u003eseconds\u003c/code\u003e秒，\u003ccode\u003eset key value ex seconds\u003c/code\u003e等同于\u003ccode\u003esetex key seconds value\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003ePX\u003c/code\u003e：将键的过期时间设置为\u003ccode\u003emilliseconds\u003c/code\u003e毫秒，与\u003ccode\u003epsetex key milliseconds value\u003c/code\u003e 等价\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003eNX|XX\u003c/code\u003e：只有在键不存在|存在时，才对键执行设置操作，\u003ccode\u003eset key value NX\u003c/code\u003e等同于\u003ccode\u003esetnx key value\u003c/code\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003emset key value [key value ...] mget key [key ...]\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003eincr key | incrby k increment\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003edecr key | decrby key decrement\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003eexists key [key ...]  del key [key ...]\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003eexpire key seconds  ttl key \u003c/code\u003e\u003c/li\u003e\n\u003c/ol\u003e\n\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003ehash\u003c/code\u003e：键值类型\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003elist\u003c/code\u003e：列表，Redis lists基于Linked Lists实现。\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003eset\u003c/code\u003e：无序排列\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003ezset\u003c/code\u003e：有序排列\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"redis\"\u003eRedis\u003c/h2\u003e\n\u003ch3 id=\"redis-为啥这么快\"\u003eRedis 为啥这么快\u003c/h3\u003e\n\u003cp\u003eRedis 支持 100000+QPS。\u003c/p\u003e","title":"Redis 基础"},{"content":"MyBatis 基础 MyBatis 中文文档 MyBatis-Spring 中文文档 MyBatis-Spring-Boot 文档 PageHelper 地址 MyBatis 介绍 MyBatis 是优秀的持久层框架 MyBatis 使用 XML 将 SQL 与程序代码解耦，便于维护 MyBatis 学习简单，执行高效，是 JDBC 的延伸 MyBatis 开发流程 引入 MyBatis 依赖 创建核心配置文件 创建实体（Entity）类 创建 Mapper 映射文件 初始化 SessionFactory 利用 SQLSession对象操作数据 SqlSessionFactory SqlSessionFactory 是 MyBatis 的核心对象 SQLSessionFactory 用于初始化MyBatis，创建 SQLSession 对象 应保证 SQLSessionFactory 全局唯一 SqlSession SQLSession 是MyBatis 操作数据库的核心对象 SQLSession 使用 JDBC 与数据库交互 MyBatis 查询 MyBatis 日志框架 Mybatis 通过使用内置的日志工厂org.apache.ibatis.logging.LogFactory提供日志功能。内置日志工厂将会把日志工作委托给下面的实现之一：\norg.apache.ibatis.logging.slf4j.Slf4jImpl org.apache.ibatis.logging.commons.JakartaCommonsLoggingImpl org.apache.ibatis.logging.log4j.Log4jImpl org.apache.ibatis.logging.log4j2.Log4j2Impl org.apache.ibatis.logging.jdk14.Jdk14LoggingImpl org.apache.ibatis.logging.nologging.NoLoggingImpl MyBatis 内置日志工厂会基于运行时检测信息选择日志委托实现。它会（按上面罗列的顺序）使用第一个查找到的实现。当没有找到这些实现时，将会禁用日志功能。\nMyBatis 查找日志的顺序定义在org.apache.ibatis.logging.LogFactory中\n// 使用静态块加载日志框架 static { tryImplementation(LogFactory::useSlf4jLogging); tryImplementation(LogFactory::useCommonsLogging); tryImplementation(LogFactory::useLog4J2Logging); tryImplementation(LogFactory::useLog4JLogging); tryImplementation(LogFactory::useJdkLogging); tryImplementation(LogFactory::useNoLogging); } public static synchronized void useSlf4jLogging() { setImplementation(org.apache.ibatis.logging.slf4j.Slf4jImpl.class); } NoLoggingImpl中的所有方法都未实现，即为禁用日志功能。\npublic class NoLoggingImpl implements Log { public NoLoggingImpl(String clazz) { // Do Nothing } @Override public void error(String s, Throwable e) { // Do Nothing } } 可以在MyBatis 配置文件mybatis-config.xml 中使用setting标签来指定 MyBatis 的日志实现\n\u0026lt;configuration\u0026gt; \u0026lt;settings\u0026gt; ... \u0026lt;setting name=\u0026#34;logImpl\u0026#34; value=\u0026#34;LOG4J\u0026#34;/\u0026gt; ... \u0026lt;/settings\u0026gt; \u0026lt;/configuration\u0026gt; logImpl 的可选值有：SLF4J、LOG4J、LOG4J2、JDK_LOGGING、COMMONS_LOGGING、STDOUT_LOGGING、NO_LOGGING，或者是实现了org.apache.ibatis.logging.Log接口且构造方法以字符串为参数的类完全限定名。\nprivate static void setImplementation(Class\u0026lt;? extends Log\u0026gt; implClass) { try { Constructor\u0026lt;? extends Log\u0026gt; candidate = implClass.getConstructor(String.class); Log log = candidate.newInstance(LogFactory.class.getName()); if (log.isDebugEnabled()) { log.debug(\u0026#34;Logging initialized using \u0026#39;\u0026#34; + implClass + \u0026#34;\u0026#39; adapter.\u0026#34;); } logConstructor = candidate; } catch (Throwable t) { throw new LogException(\u0026#34;Error setting Log implementation. Cause: \u0026#34; + t, t); } } MyBatis 动态 SQL if if 标签通常用于WHERE语句中，通过参数值来决定是否使用某个查询条件，也常用于UPDATE语句中判断是否更新某一个字段，还可以在INSERT语句中用来判断是否插入某个字段的值。\n在WHERE条件中使用if 实现一个用户高级查询功能，根据输入条件去检索用户信息。当只输入用户名时，需要根据用户名进行模糊查询；只输入邮箱时，根据邮箱进行完全匹配；当同时输入用户名和邮箱时，用两个条件进行查询。\n\u0026lt;select id=\u0026#34;selectByUser\u0026#34; resultType=\u0026#34;person.xianglin.simple.model.SysUser\u0026#34;\u0026gt; select id, user_name userName, user_password userPassword, user_email userEmail, user_info userInfo, head_img headImg, create_time createTime from sys_user where 1 = 1 \u0026lt;if test=\u0026#34;userName != null and userName != \u0026#39;\u0026#39;\u0026#34;\u0026gt; and user_name like concat(\u0026#39;%\u0026#39;, #{userName}, \u0026#39;%\u0026#39;) \u0026lt;/if\u0026gt; \u0026lt;if test=\u0026#34;userEmail != null and userEmail != \u0026#39;\u0026#39;\u0026#34;\u0026gt; and user_email = #{userEmail} \u0026lt;/if\u0026gt; \u0026lt;/if\u0026gt; \u0026lt;/select\u0026gt; if标签有一个必填属性test，test属性值是一个符合OGNL要求的判断表达式，表达式的结果可以是true或false，除此之外所有的非 0 值都是true，只有0为false。\n判断条件property != null或property == null：适用于任何类型的字段，用于判断属性值是否为空。 判断条件property != ''或property == ''：适用于String类型的字段，用于判断是否为空字符串。 and和or：当有多个判断条件时，使用and或or进行连接，嵌套的判断条件可以使用小括号分组，and相当于 Java 中的与\u0026amp;\u0026amp;，or相当于 Java 中的或||。 在UPDATE更新列中使用if 如果只更新有变化的字段，更新时不能将原来有值但没有变化的字段更新为空或null。\n\u0026lt;update id=\u0026#34;updateByIdSelective\u0026#34;\u0026gt; update sys_user set \u0026lt;if test=\u0026#34;userName != null and userName != \u0026#39;\u0026#39;\u0026#34;\u0026gt; user_name = #{userName}, \u0026lt;/if\u0026gt; \u0026lt;if test=\u0026#34;userPassword != null and userPassword != \u0026#39;\u0026#39;\u0026#34;\u0026gt; user_password = #{userPassword}, \u0026lt;/if\u0026gt; \u0026lt;if test=\u0026#34;userEmail != null and userEmail != \u0026#39;\u0026#39;\u0026#34;\u0026gt; user_email = #{userEmail}, \u0026lt;/if\u0026gt; \u0026lt;if test=\u0026#34;userInfo != null and userInfo != \u0026#39;\u0026#39;\u0026#34;\u0026gt; user_info = #{userInfo}, \u0026lt;/if\u0026gt; \u0026lt;if test=\u0026#34;headImg != null and headImg != \u0026#39;\u0026#39;\u0026#34;\u0026gt; head_img = #{headImg}, \u0026lt;/if\u0026gt; \u0026lt;if test=\u0026#34;createTime != null and createTime != \u0026#39;\u0026#39;\u0026#34;\u0026gt; create_time = #{createTime,JDBC_TYPE=TIMESTAMP}, \u0026lt;/if\u0026gt; id = #{id} where id = #{id} \u0026lt;/update\u0026gt; 在INSERT动态插入列中使用if 在插入操作时。如果某一列的参数值不为空，就使用传入的值，如果传入参数为空，就使用数据库中的默认值。使用if可以实现这种动态插入列的功能。\n\u0026lt;insert id=\u0026#34;insert2\u0026#34; useGeneratedKeys=\u0026#34;true\u0026#34; keyProperty=\u0026#34;id\u0026#34;\u0026gt; insert into sys_user(user_name,user_password \u0026lt;if test=\u0026#34;userEmail != null and userEmail != \u0026#39;\u0026#39;\u0026#34;\u0026gt; ,user_email \u0026lt;/if\u0026gt;,user_info,head_img,create_time) values(#{userName},#{userPassword} \u0026lt;if test=\u0026#34;userEmail != null and userEmail != \u0026#39;\u0026#39;\u0026#34;\u0026gt; ,#{userEmail} \u0026lt;/if\u0026gt; ,#{userInfo},#{headImg,jdbcType=BLOB},#{createTime,jdbcType=TIMESTAMP}) \u0026lt;/insert\u0026gt; 在INSERT中使用时需要注意，若在列部分增加if条件，则在 value部分也要增加相同的if条件，必须保证列和插入的值相对应。\nchoose、when、otherwise if标签提供了基本的条件判断，但是无法实现if...else的逻辑。choose when otherwise标签可以实现这样的逻辑。choose元素中包含when和otherwise两个标签，一个choose中至少一个when，有0 个或 1 个otherwise。\n进行如下查询：当参数 ID 有值时，优先使用 ID 查询，当 ID 没有值时判断用户名是否有值，如果有值就用用户名查询，如果用户名也没有值，就是 SQL 查询无结果。\n\u0026lt;select id=\u0026#34;selectUserByIdOrUserName\u0026#34; resultType=\u0026#34;SysUser\u0026#34;\u0026gt; select * from sys_user where 1 = 1 \u0026lt;choose\u0026gt; \u0026lt;when test=\u0026#34;id != null\u0026#34;\u0026gt; and id = #{id} \u0026lt;/when\u0026gt; \u0026lt;when test=\u0026#34;userName != null and userName != \u0026#39;\u0026#39;\u0026#34;\u0026gt; and user_name = #{userName} \u0026lt;/when\u0026gt; \u0026lt;otherwise\u0026gt; and 1 = 2 \u0026lt;/otherwise\u0026gt; \u0026lt;/choose\u0026gt; \u0026lt;/select\u0026gt; trim、where、set where用法 where 标签的作用：如果该标签包含的元素中有返回值，就插入一个where；如果where后面的字符串是以AND和OR开头的，就将它们剔除。\n修改上述 [在WHERE中使用if] 的 SQL 语句：\n\u0026lt;select id=\u0026#34;selectByUser\u0026#34; resultType=\u0026#34;SysUser\u0026#34;\u0026gt; select * from sys_user \u0026lt;where\u0026gt; \u0026lt;if test=\u0026#34;userName != null and userName != \u0026#39;\u0026#39;\u0026#34;\u0026gt; and user_name like concat(\u0026#39;%\u0026#39;, #{userName}, \u0026#39;%\u0026#39;) \u0026lt;/if\u0026gt; \u0026lt;if test=\u0026#34;userEmail != \u0026#39;\u0026#39; and userEmail != null\u0026#34;\u0026gt; and user_email = #{userEmail} \u0026lt;/if\u0026gt; \u0026lt;/where\u0026gt; \u0026lt;/select\u0026gt; set用法 set标签的作用：如果该标签包含的元素中有返回值，就插入一个set；如果set后面的字符串是以逗号结尾的，就剔除这个逗号。\n修改上述 [在UPDATE中使用if] 的 SQL 语句：\n\u0026lt;update id=\u0026#34;updateByIdSelective\u0026#34;\u0026gt; update sys_user \u0026lt;set\u0026gt; \u0026lt;if test=\u0026#34;userName != null and userName != \u0026#39;\u0026#39;\u0026#34;\u0026gt; user_name = #{userName}, \u0026lt;/if\u0026gt; \u0026lt;if test=\u0026#34;userPassword != null and userPassword != \u0026#39;\u0026#39;\u0026#34;\u0026gt; user_password = #{userPassword}, \u0026lt;/if\u0026gt; \u0026lt;if test=\u0026#34;userEmail != null and userEmail != \u0026#39;\u0026#39;\u0026#34;\u0026gt; user_email = #{userEmail}, \u0026lt;/if\u0026gt; \u0026lt;if test=\u0026#34;userInfo != null and userInfo != \u0026#39;\u0026#39;\u0026#34;\u0026gt; user_info = #{userInfo}, \u0026lt;/if\u0026gt; \u0026lt;if test=\u0026#34;headImg != null and headImg != \u0026#39;\u0026#39;\u0026#34;\u0026gt; head_img = #{headImg}, \u0026lt;/if\u0026gt; \u0026lt;if test=\u0026#34;createTime != null and createTime != \u0026#39;\u0026#39;\u0026#34;\u0026gt; create_time = #{createTime,JDBC_TYPE=TIMESTAMP}, \u0026lt;/if\u0026gt; id = #{id} \u0026lt;/set\u0026gt; where id = #{id} \u0026lt;/update\u0026gt; set标签只解决了逗号问题，如果set元素没有内容，SQL 语句格式仍然有误，所以类似id =#{id}这样必然存在的赋值仍有保留的必要。\ntrim用法 where 和 set 标签的功能都可以用 trim 标签来实现，并且在底层就是通过 TrimSqlNode 实现的。\nwhere 标签对应的 trim 实现如下：\n\u0026lt;trim prefix=\u0026#34;WHERE\u0026#34; prefixOverrides=\u0026#34;AND | OR \u0026#34;\u0026gt; ... \u0026lt;/trim\u0026gt; set标签对应的trim实现如下：\n\u0026lt;trim prefix=\u0026#34;SET\u0026#34; suffixOverrides=\u0026#34;,\u0026#34;\u0026gt; ... \u0026lt;/trim\u0026gt; trim标签有如下属性：\nprefix：当trim元素包含内容时，会给内容增加prefix指定的前缀。 prefixOverrides：当trim元素包含内容时，会把内容中匹配的前缀字符串去掉。 suffix：当trim元素包含内容时，会给内容增加suffix指定的后缀。 suffixOverrides：当trim元素包含内容时，会把内容中匹配的后缀字符串去掉。 foreach SQL 语句中会使用IN关键字，例如id in (1, 2, 3)，可以使用${ids}方式直接传参，但这种写法不能防止 SQL 注入，想要使用#{}的方式，就需要配合foreach标签使用。\nforeach遍历对象可以分为两大类：Iterable类型和Map类型。\nforeach实现in集合 \u0026lt;select id=\u0026#34;selectByIdList\u0026#34; resultType=\u0026#34;SysUser\u0026#34;\u0026gt; select * from sys_user where id in \u0026lt;foreach collection=\u0026#34;list\u0026#34; open=\u0026#34;(\u0026#34; close=\u0026#34;)\u0026#34; separator=\u0026#34;,\u0026#34; item=\u0026#34;id\u0026#34; index=\u0026#34;i\u0026#34;\u0026gt; #{id} \u0026lt;/foreach\u0026gt; \u0026lt;/select\u0026gt; foreach包含以下属性：\ncollection：必填，值为要迭代循环的属性名。 item：变量名，值为从迭代对象中取出的每一个值。 index：索引的属性名，在集合数组情况下为当前索引值，当迭代循环的对象时 Map类型时，这个值为Map的key。 open：整个循环内容开头的字符串。 close：整个循环内容结尾的字符串。 separator：每次循环的分隔符。 关于collection属性值的设置。分析DefaultSqlSession的源码：\nprivate Object wrapCollection(final Object object) { if (object instanceof Collection) { StrictMap\u0026lt;Object\u0026gt; map = new StrictMap\u0026lt;Object\u0026gt;(); map.put(\u0026#34;collection\u0026#34;, object); if (object instanceof List) { map.put(\u0026#34;list\u0026#34;, object); } return map; } else if (object != null \u0026amp;\u0026amp; object.getClass().isArray()) { StrictMap\u0026lt;Object\u0026gt; map = new StrictMap\u0026lt;Object\u0026gt;(); map.put(\u0026#34;array\u0026#34;, object); return map; } return object; } foreach实现批量插入 \u0026lt;insert id=\u0026#34;insertList\u0026#34; useGeneratedKeys=\u0026#34;true\u0026#34; keyColumn=\u0026#34;id\u0026#34; keyProperty=\u0026#34;id\u0026#34;\u0026gt; insert into sys_user(user_name,user_password,user_email,create_time) values \u0026lt;foreach collection=\u0026#34;list\u0026#34; item=\u0026#34;user\u0026#34; separator=\u0026#34;,\u0026#34;\u0026gt; (#{user.userName},#{user.userPassword},#{user.userEmail},#{user.createTime,jdbcType=TIMESTAMP}) \u0026lt;/foreach\u0026gt; \u0026lt;/insert\u0026gt; foreach实现动态UPDATE \u0026lt;update id=\u0026#34;updateByMap\u0026#34;\u0026gt; update sys_user \u0026lt;set\u0026gt; \u0026lt;foreach collection=\u0026#34;_parameter\u0026#34; separator=\u0026#34;,\u0026#34; item=\u0026#34;value\u0026#34; index=\u0026#34;key\u0026#34;\u0026gt; ${key} = #{value} \u0026lt;/foreach\u0026gt; \u0026lt;/set\u0026gt; where id = #{id} \u0026lt;/update\u0026gt; bind bind标签可以使用OGNL表达式创建一个变量并将其绑定到上下文中。如上面所述的selectByUser 方法中的like查询条件\n\u0026lt;if test=\u0026#34;userName != null and userName != \u0026#39;\u0026#39;\u0026#34;\u0026gt; and user_name like concat(\u0026#39;%\u0026#39;, #{userName}, \u0026#39;%\u0026#39;) \u0026lt;/if\u0026gt; 改为bind方式\n\u0026lt;if test=\u0026#34;userName != null and userName != \u0026#39;\u0026#39;\u0026#34;\u0026gt; \u0026lt;bind name=\u0026#34;userNameLike\u0026#34; value=\u0026#34;\u0026#39;%\u0026#39; + userName + \u0026#39;%\u0026#39;\u0026#34;/\u0026gt; and user_name like #{userNameLike} \u0026lt;/if\u0026gt; name：绑定到上下文的变量名 value：OGNL表达式 MyBatis缓存技术 一般提到 MyBatis 缓存的时候，都是指二级缓存。一级缓存默认启用，且不能控制。\n一级缓存 MyBatis 的一级缓存存在于 SqlSession 的生命周期中，在同一个SqlSession 中查询时，MyBatis 会把执行的方法和参数通过算法生成缓存的键值，将键值和查询结果存入一个 Map 对象中。如果同一个 SqlSession 中执行的方法和参数完全一致，那么通过算法会生成相同的键值，当 Map 缓存对象中已经存在该键值时，则会返回缓存中的对象。任何的 INSERT、 UPDATE、DELETE 操作都会清空一级缓存。也可以在select标签中添加flushCache=\u0026quot;true\u0026quot;元素，让 MyBatis 在查询数据前清空当前的一级缓存。\n一级缓存实现\n一级缓存对象：org.apache.ibatis.cache.impl.PerpetualCache\npublic class PerpetualCache implements Cache { // 一级缓存使用 Map 实现 private Map\u0026lt;Object, Object\u0026gt; cache = new HashMap\u0026lt;Object, Object\u0026gt;(); } 执行查询时一级缓存实现\npublic \u0026lt;E\u0026gt; List\u0026lt;E\u0026gt; query(...){ // ... // 尝试从一级缓存中获取结果 list = resultHandler == null ? (List\u0026lt;E\u0026gt;) localCache.getObject(key) : null; if (list != null) { handleLocallyCachedOutputParameters(ms, key, parameter, boundSql); } else { // 没有存在一级缓存中时，从数据库查询 list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql); } // ... } private \u0026lt;E\u0026gt; List\u0026lt;E\u0026gt; queryFromDatabase(...) { List\u0026lt;E\u0026gt; list; localCache.putObject(key, EXECUTION_PLACEHOLDER); try { // 执行数据库查询 list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql); } finally { localCache.removeObject(key); } // 将查询结果存于本地缓存 localCache.putObject(key, list); if (ms.getStatementType() == StatementType.CALLABLE) { localOutputParameterCache.putObject(key, parameter); } return list; } flushCache\n配置flushCache后MappedStatement对象中flushCacheRequired属性值由false变为true 对应代码段在org.apache.ibatis.executor.BaseExecutor#query中\nif (queryStack == 0 \u0026amp;\u0026amp; ms.isFlushCacheRequired()) { clearLocalCache(); } // 清除一级缓存 // 最后调用的方法时 Map 的 clear() public void clearLocalCache() { if (!closed) { localCache.clear(); localOutputParameterCache.clear(); } } 二级缓存 MyBatis 的二级缓存非常强大，它不同于一级缓存只存在于 SqlSession 的生命周期中， 而是可以理解为存在于 SqlSessionFactory 的生命周期中。当存在多个 SqlSessionFactory 时，它们的缓存都是绑定在各自对象上的，缓存数据在一般情况下是不相通的。只有在使用如 Redis 这样的缓存数据库时，才可以共享缓存。\n配置二级缓存 MyBatis 全局配置 setting中有一个参数cacheEnabled，是二级缓存的全局开关，默认值为true。\n\u0026lt;settings\u0026gt; \u0026lt;setting name=\u0026#34;cacheEnabled\u0026#34; value=\u0026#34;true\u0026#34;/\u0026gt; \u0026lt;/settings\u0026gt; MyBatis 的二级缓存和命名空间绑定的，即二级缓存需要配置在Mapper.xml映射文件或者配置在Mapper.java接口中。\nMapper.xml中配置二级缓存 在保证二级缓存的全局配置开启的情况下，给某个命名空间开启二级缓存只需要在Mapper.xml中添加\u0026lt;cache/\u0026gt;元素即可。\n＜?xml version=＂1.0＂ encoding=＂UTF-8＂ ?＞ ＜!DOCTYPE mapper PUBLIC ＂-//mybatis.org//DTD Mapper 3.0//EN＂ ＂http：//mybatis.org/dtd/mybatis-3-mapper.dtd＂ ＞ ＜mapper namespace=＂tk.mybatis.simple.mapper.RoleMapper＂＞ ＜cache/＞ ＜!--其他配置--＞ ＜/mapper＞ 默认的二级缓存会有如下效果：\n映射语句中的所有SELECT语句将会被缓存。\n映射语句中所有的INSERT、UPDATE、DELETE语句会刷新缓存。\n缓存会使用Least Recently Used（最近最少使用的）算法来收回。\n根据时间表属性缓存\n缓存存储集合或对象的 1024 个引用。\n缓存会被视为read/write的，意味着对象检索不是共享的，而且可以安全地被调用者修改，而不干扰其它调用者或线程所做的潜在修改。\n\u0026lt;cache eviction=\u0026#34;FIFO\u0026#34; flushInterval=\u0026#34;60000\u0026#34; size=\u0026#34;512\u0026#34; readOnly=\u0026#34;true\u0026#34;/\u0026gt; 对可配置属性的解释如下：\neviction：（回收策略） LRU（最近最少使用的）：移除最长时间不被使用的对象，默认值。 FIFO（先进先出）：按对象进入缓存的顺序来移除。 SOFT（软引用）：移除基于垃圾回收器状态和软引用规则的对象。 WEAK（弱引用）：更积极地移除基于垃圾收集器状态和弱引用规则的对象。 flushInterval（刷新间隔）。设置为任意的正整数，代表一个合理的毫秒形式的时间段。默认情况下不设置，即没有刷新间隔，缓存仅在调用语句时刷新。 size（引用数目）。默认值是 1024. readOnly（只读）。可以设置为true或false。只读的缓存会给所有调用者返回缓存对象的相同实例，因此这些对象不能被修改。可读写的缓存会通过序列化返回缓存对象的拷贝。 Mapper接口中配置二级缓存 当只使用注解方式配置二级缓存时，需要增加如下配置。\n@CacheNamespace( eviction = FifoCache.class, flushInterval = 60000, size = 512, readWrite = true ) public interface RoleMapper { // other code } 当同时使用注解方式和 XML 映射文件时，如果同时配置了上述的二级缓存，会抛出如下异常\nCaches collection already contains value for tk.mybatis.simple.mapper.RoleMapper\n这时候应该使用参照缓存，参照缓存配置如下：\n@CacheNamespaceRef(RoleMapper.class) public interface RoleMapper { // other code } Mapper 接口可以通过注解引用 XML 映射文件或者其他接口的缓存，在 XML 中也可以配置参照缓存，如下配置：\n\u0026lt;cache-ref namespace=\u0026#34;tk.mybatis.simple.mapper.RoleMapper\u0026#34;/\u0026gt; 使用二级缓存 当配置缓存为可读写时，MyBatis 使用SerializedCache序列化缓存来实现可读写缓存类，并通过序列化和反序列化来保证通过缓存获取数据时，得到的是一个新的实例。因此，要求被序列化的对象实现Serializable接口。如果配置为只读缓存，MyBatis 就会使用 Map 来存储缓存值，这种情况下，从缓存中获取的对象就是同一个实例。\n@Test public void testL2Cache() { SqlSession sqlSession = getSqlSession(); SysRole role1; try { RoleMapper mapper = sqlSession.getMapper(RoleMapper.class); role1 = mapper.selectById(1L); role1.setRoleName(\u0026#34;NEW NAME\u0026#34;); SysRole role2 = mapper.selectById(1L); Assert.assertEquals(\u0026#34;NEW NAME\u0026#34;, role2.getRoleName()); Assert.assertEquals(role1, role2); } finally { sqlSession.close(); } System.out.println(\u0026#34;开启新的 SQLSession\u0026#34;); sqlSession = getSqlSession(); try { RoleMapper mapper = sqlSession.getMapper(RoleMapper.class); SysRole role2 = mapper.selectById(1L); Assert.assertEquals(role2.getRoleName(), \u0026#34;NEW NAME\u0026#34;); Assert.assertNotEquals(role2, role1); SysRole role3 = mapper.selectById(1L); Assert.assertNotEquals(role2, role3); } finally { sqlSession.close(); } } 测试执行的日志为：\nDEBUG [main] - Cache Hit Ratio [person.xianglin.simple.mapper.RoleMapper]: 0.0 DEBUG [main] - ==\u0026gt; Preparing: select id,role_name roleName,enabled, create_by createBy,create_time createTime from sys_role where id = ? DEBUG [main] - ==\u0026gt; Parameters: 1(Long) TRACE [main] - \u0026lt;== Columns: id, roleName, enabled, createBy, createTime TRACE [main] - \u0026lt;== Row: 1, 管理员, 1, 1, 2016-04-01 17:02:14.0 DEBUG [main] - \u0026lt;== Total: 1 DEBUG [main] - Cache Hit Ratio [person.xianglin.simple.mapper.RoleMapper]: 0.0 开启新的 SQLSession DEBUG [main] - Cache Hit Ratio [person.xianglin.simple.mapper.RoleMapper]: 0.3333333333333333 DEBUG [main] - Cache Hit Ratio [person.xianglin.simple.mapper.RoleMapper]: 0.5 集成EhCache缓存 MyBatis 项目开发者最早提供了 EhCache 的 MyBatis 二级缓存实现， 该项目名为 ehcache-cache，地址是 https://github.com/mybatis/ehcache-cache。\n添加项目依赖\n在pom.xml文件中加入如下依赖\n\u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.mybatis.caches\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;mybatis-ehcache\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.2.1\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; 配置 EhCache\n在 resources 目录下新增ehcache.xml文件\n\u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34; ?\u0026gt; \u0026lt;ehcache xmlns:xsi=\u0026#34;http://www.w3.org/2001/XMLSchema-instance\u0026#34; xsi:noNamespaceSchemaLocation=\u0026#34;ehcache.xsd\u0026#34; updateCheck=\u0026#34;false\u0026#34; monitoring=\u0026#34;autodetect\u0026#34; dynamicConfig=\u0026#34;true\u0026#34;\u0026gt; \u0026lt;diskStore path=\u0026#34;C:\\Users\\xianglin\\project\\idea-workspace\\mybatis\\cache\u0026#34;/\u0026gt; \u0026lt;defaultCache maxElementsInMemory=\u0026#34;3000\u0026#34; eternal=\u0026#34;false\u0026#34; copyOnRead=\u0026#34;true\u0026#34; copyOnWrite=\u0026#34;true\u0026#34; timeToIdleSeconds=\u0026#34;3600\u0026#34; timeToLiveSeconds=\u0026#34;3600\u0026#34; overflowToDisk=\u0026#34;true\u0026#34; diskPersistent=\u0026#34;true\u0026#34;/\u0026gt; \u0026lt;/ehcache\u0026gt; copyOnRead 的含义是，判断从缓存中读取数据时是返回对象的引用还是复制一个对象返回。默认情况下是 false，即返回数据的引用，这种情况下返回的都是相同的对象，和 MyBatis 默认缓存中的只读对象是相同的。如果设置为 true，那就是可读写缓存，每次读取缓存时都会复制一个新的实例。\ncopyOnWrite 的含义是，判断写入缓存时是直接缓存对象的引用还是复制一个对象然后缓存，默认也是 false。如果想使用可读写缓存，就需要将这两个属性配置为 true，如果使用只读缓存，可以不配置这两个属性，使用默认值 false 即可。\n修改缓存配置\n\u0026lt;cache type=\u0026#34;org.mybatis.caches.ehcache.EhcacheCache\u0026#34;/\u0026gt; 集成 Redis缓存 MyBatis 项目开发者提供了 Redis 的 MyBatis 二级缓存实现，该项目名为 redis-cache，目前 只有 beta 版本，项目地址是 https://github.com/mybatis/redis-cache。\n添加项目依赖\n在pom.xml文件中加入如下依赖\n\u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.mybatis.caches\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;mybatis-redis\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.0.0-beta2\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; 配置Redis\n在 resource 目录下新增redis.properties文件\nredis.host=http://122.51.48.52 redis.port=6379 redis.connectionTimeout=5000 redis.soTimeout=5000 redis.password=950915 redis.database=0 redis.clientName= 修改缓存配置\n\u0026lt;cache type=\u0026#34;org.mybatis.caches.redis.RedisCache\u0026#34;/\u0026gt; 脏数据的产生和避免 MyBatis 的二级缓存是和命名空间绑定的，所以通常情况下每一个 Mapper 映射文件都拥有自己的二级缓存，不同Mapper 的二级缓存互不影响。在常见的数据库操作中，多表联合查询非常常见，由于关系型数据库的设计，使得很多时候需要关联多个表才能获得想要的数据。在关联多表查询时肯定会将该查询放到某个命名空间下的映射文件中，这样一个多表的查询就会缓存在该命名空间的二级缓存中。涉及这些表的增、删、改操作通常不在一个映射文件中，它们的命名空间不同，因此当有数据变化时，多表查询的缓存未必会被清空，这种情况下就会产生脏数据。\n当某几个表可以作为一个业务整体时，通常是让几个会关联的 ER 表同时使用同一个二级缓存，这样就能解决脏数据问题。\n","permalink":"https://2d992714.xianglin2020-github-io.pages.dev/posts/2020/mybatis-basics/","summary":"\u003ch1 id=\"mybatis-基础\"\u003eMyBatis 基础\u003c/h1\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://mybatis.org/mybatis-3/zh/index.html\" target=\"_blank\" rel=\"noopener\"\u003eMyBatis 中文文档\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"http://mybatis.org/spring/zh/\" target=\"_blank\" rel=\"noopener\"\u003eMyBatis-Spring 中文文档\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"http://mybatis.org/spring-boot-starter/mybatis-spring-boot-autoconfigure/\" target=\"_blank\" rel=\"noopener\"\u003eMyBatis-Spring-Boot 文档\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://github.com/pagehelper/Mybatis-PageHelper\" target=\"_blank\" rel=\"noopener\"\u003ePageHelper 地址\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"mybatis-介绍\"\u003eMyBatis 介绍\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003eMyBatis 是优秀的持久层框架\u003c/li\u003e\n\u003cli\u003eMyBatis 使用 XML 将 SQL 与程序代码解耦，便于维护\u003c/li\u003e\n\u003cli\u003eMyBatis 学习简单，执行高效，是 JDBC 的延伸\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"mybatis-开发流程\"\u003eMyBatis 开发流程\u003c/h2\u003e\n\u003col\u003e\n\u003cli\u003e引入 MyBatis 依赖\u003c/li\u003e\n\u003cli\u003e创建核心配置文件\u003c/li\u003e\n\u003cli\u003e创建实体（Entity）类\u003c/li\u003e\n\u003cli\u003e创建 Mapper 映射文件\u003c/li\u003e\n\u003cli\u003e初始化 SessionFactory\u003c/li\u003e\n\u003cli\u003e利用 SQLSession对象操作数据\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"sqlsessionfactory\"\u003eSqlSessionFactory\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003eSqlSessionFactory 是 MyBatis 的核心对象\u003c/li\u003e\n\u003cli\u003eSQLSessionFactory 用于初始化MyBatis，创建 SQLSession 对象\u003c/li\u003e\n\u003cli\u003e应保证 SQLSessionFactory 全局唯一\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"sqlsession\"\u003eSqlSession\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003eSQLSession 是MyBatis 操作数据库的核心对象\u003c/li\u003e\n\u003cli\u003eSQLSession 使用 JDBC 与数据库交互\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"mybatis-查询\"\u003eMyBatis 查询\u003c/h2\u003e\n\u003ch2 id=\"mybatis-日志框架\"\u003eMyBatis 日志框架\u003c/h2\u003e\n\u003cp\u003eMybatis 通过使用内置的日志工厂\u003ccode\u003eorg.apache.ibatis.logging.LogFactory\u003c/code\u003e提供日志功能。内置日志工厂将会把日志工作委托给下面的实现之一：\u003c/p\u003e","title":"MyBatis 基础"},{"content":"Lambda Java8 开始支持Lambda 表达式，让程序编写更简洁、优雅 利用 Lambda 表达式可以更简洁的实现匿名内部类和函数声明与调用 Lambda 使得 Stream 流式编程为操作集合提供便利 Lambda 表达式语法结构 (参数列表) -\u0026gt; 实现语句 public static void main(String[] args) { // 标准使用方式 MathOperation operation = (double a, double b) -\u0026gt; { System.out.println(\u0026#34;加法运算\u0026#34;); return a + b; }; System.out.println(operation.opt(1, 3)); // Lambda 允许忽略参数类型 MathOperation operation1 = (a, b) -\u0026gt; { return a - b; }; System.out.println(operation1.opt(4, 1)); // 单行代码可以省略方法体和 return 语句 MathOperation operation2 = (a, b) -\u0026gt; a * b; System.out.println(operation2.opt(2, 5)); } 函数式编程 基于函数式接口并使用 Lambda 表达式的编程方式 函数式编程的理念是将代码作为可重用数据代入到程序运行中 函数式接口 有且仅有一个抽象方法的接口 Java8 提供了一系列新的函数式接口，位于java.util.function包中 Predicate用于判断传入的数据是否满足要求，使用test()方法进行逻辑判断 java.util.function.Predicate :用于条件判断，返回 Boolean 值 java.util.function.Consumer：对应只有一个输入参数，而不需要返回的情况 java.util.function.Function：对应一个输入参数，且返回数据的情况 Stream Stream流式处理是建立在 Lambda 表达式基础上的多数据处理技术 Stream 对集合数据处理高度抽象，极大简化代码量 Stream 可以对集合数据进行迭代、去重、筛选、排序、聚合等操作 位于java.util.stream.Stream 创建流的五种方式 基于数组创建\nString[] arr = {\u0026#34;lili\u0026#34;, \u0026#34;mark\u0026#34;, \u0026#34;jackson\u0026#34;}; Stream\u0026lt;String\u0026gt; stringStream = Stream.of(arr); stringStream.forEach(System.out::print); 基于集合创建\nList\u0026lt;String\u0026gt; list = new ArrayList\u0026lt;\u0026gt;(); list.add(\u0026#34;123\u0026#34;); list.add(\u0026#34;345\u0026#34;); Stream\u0026lt;String\u0026gt; stream = list.stream(); stream.forEach(System.out::println); 使用 generate 方法创建\nStream\u0026lt;Integer\u0026gt; generate = Stream.generate(() -\u0026gt; new Random().nextInt(100000)).limit(100); generate.forEach(System.out::print); System.out.println(); 使用迭代器创建\nStream\u0026lt;Integer\u0026gt; limit = Stream.iterate(1, n -\u0026gt; n + 1).limit(100); limit.forEach(System.out::print); 使用字符序列创建\nString string = \u0026#34;abcdefg\u0026#34;; IntStream chars = string.chars(); chars.forEach(System.out::println); ","permalink":"https://2d992714.xianglin2020-github-io.pages.dev/posts/2020/lambda-stream-functional/","summary":"\u003ch1 id=\"lambda\"\u003eLambda\u003c/h1\u003e\n\u003cul\u003e\n\u003cli\u003eJava8 开始支持Lambda 表达式，让程序编写更简洁、优雅\u003c/li\u003e\n\u003cli\u003e利用 Lambda 表达式可以更简洁的实现匿名内部类和函数声明与调用\u003c/li\u003e\n\u003cli\u003eLambda 使得 Stream 流式编程为操作集合提供便利\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"lambda-表达式语法结构\"\u003eLambda 表达式语法结构\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ccode\u003e(参数列表) -\u0026gt; 实现语句\u003c/code\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-java\" data-lang=\"java\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"kd\"\u003estatic\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"kt\"\u003evoid\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"nf\"\u003emain\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eString\u003c/span\u003e\u003cspan class=\"o\"\u003e[]\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003eargs\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e        \u003c/span\u003e\u003cspan class=\"c1\"\u003e// 标准使用方式\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e        \u003c/span\u003e\u003cspan class=\"n\"\u003eMathOperation\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003eoperation\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003edouble\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003ea\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"kt\"\u003edouble\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003eb\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"o\"\u003e-\u0026gt;\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e            \u003c/span\u003e\u003cspan class=\"n\"\u003eSystem\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003eout\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003eprintln\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;加法运算\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e            \u003c/span\u003e\u003cspan class=\"k\"\u003ereturn\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003ea\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"o\"\u003e+\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003eb\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e        \u003c/span\u003e\u003cspan class=\"p\"\u003e};\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e        \u003c/span\u003e\u003cspan class=\"n\"\u003eSystem\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003eout\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003eprintln\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eoperation\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003eopt\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003e1\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003e3\u003c/span\u003e\u003cspan class=\"p\"\u003e));\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e        \u003c/span\u003e\u003cspan class=\"c1\"\u003e// Lambda 允许忽略参数类型\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e        \u003c/span\u003e\u003cspan class=\"n\"\u003eMathOperation\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003eoperation1\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ea\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003eb\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"o\"\u003e-\u0026gt;\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e            \u003c/span\u003e\u003cspan class=\"k\"\u003ereturn\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003ea\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"o\"\u003e-\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003eb\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e        \u003c/span\u003e\u003cspan class=\"p\"\u003e};\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e        \u003c/span\u003e\u003cspan class=\"n\"\u003eSystem\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003eout\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003eprintln\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eoperation1\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003eopt\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003e4\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003e1\u003c/span\u003e\u003cspan class=\"p\"\u003e));\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e        \u003c/span\u003e\u003cspan class=\"c1\"\u003e// 单行代码可以省略方法体和 return 语句\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e        \u003c/span\u003e\u003cspan class=\"n\"\u003eMathOperation\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003eoperation2\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ea\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003eb\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"o\"\u003e-\u0026gt;\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003ea\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"o\"\u003e*\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003eb\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e        \u003c/span\u003e\u003cspan class=\"n\"\u003eSystem\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003eout\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003eprintln\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eoperation2\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003eopt\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003e2\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003e5\u003c/span\u003e\u003cspan class=\"p\"\u003e));\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch1 id=\"函数式编程\"\u003e函数式编程\u003c/h1\u003e\n\u003cul\u003e\n\u003cli\u003e基于函数式接口并使用 Lambda 表达式的编程方式\u003c/li\u003e\n\u003cli\u003e函数式编程的理念是将代码作为可重用数据代入到程序运行中\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"函数式接口\"\u003e函数式接口\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e有且仅有一个抽象方法的接口\u003c/li\u003e\n\u003cli\u003eJava8 提供了一系列新的函数式接口，位于\u003ccode\u003ejava.util.function\u003c/code\u003e包中\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003ePredicate\u003c/code\u003e用于判断传入的数据是否满足要求，使用\u003ccode\u003etest()\u003c/code\u003e方法进行逻辑判断\u003c/li\u003e\n\u003c/ul\u003e\n\u003col\u003e\n\u003cli\u003e\u003ccode\u003ejava.util.function.Predicate\u003c/code\u003e :用于条件判断，返回 Boolean 值\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003ejava.util.function.Consumer\u003c/code\u003e：对应只有一个输入参数，而不需要返回的情况\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003ejava.util.function.Function\u003c/code\u003e：对应一个输入参数，且返回数据的情况\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch1 id=\"stream\"\u003eStream\u003c/h1\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ccode\u003eStream\u003c/code\u003e流式处理是建立在 Lambda 表达式基础上的多数据处理技术\u003c/li\u003e\n\u003cli\u003eStream 对集合数据处理高度抽象，极大简化代码量\u003c/li\u003e\n\u003cli\u003eStream 可以对集合数据进行迭代、去重、筛选、排序、聚合等操作\u003c/li\u003e\n\u003cli\u003e位于\u003ccode\u003ejava.util.stream.Stream\u003c/code\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"创建流的五种方式\"\u003e创建流的五种方式\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e基于数组创建\u003c/p\u003e","title":"Lambda、Stream与函数式编程"},{"content":"Java反射 反射概念 反射是在运行时动态访问类与对象的技术 反射是JDK1.2版本后的高级技术，隶属于java.lang.reflect包中 大多数Java 框架都基于反射实现参数配置，动态注入等特性 反射的核心类 Class Constructor Method Field Class Class类是JVM 中表示“类和接口”的类 Class 对象具体包含了某个特定类的结构信息 通过Class对象可以获取对应类的方法和成员信息 public static void main(String[] args) { try { Class\u0026lt;?\u0026gt; employeeClass = Class.forName(\u0026#34;reflect.entity.Employee\u0026#34;); System.out.println(\u0026#34;Employee已被加载！\u0026#34;); Employee employee = (Employee) employeeClass.newInstance(); System.out.println(employee); // 类不存在时抛出ClassNotFoundException 访问私有成员是抛出IllegalAccessException 类无法实例化时抛出InstantiationException } catch (ClassNotFoundException | IllegalAccessException | InstantiationException e) { e.printStackTrace(); } Constructor\nConstructor类是对 Java类中构造方法的抽象 包含了类某个具体构造方法的声明 调用 Constructor 带参构造方法创建对象 public static void main(String[] args) { try { Class\u0026lt;?\u0026gt; employeeClass = Class.forName(\u0026#34;reflect.entity.Employee\u0026#34;); Constructor\u0026lt;?\u0026gt; constructor = employeeClass.getConstructor(Integer.class, String.class, Float.class, String.class); Employee employee = (Employee) constructor.newInstance(1, \u0026#34;姓名\u0026#34;, 300.4F, \u0026#34;部门\u0026#34;); System.out.println(employee); } catch (ClassNotFoundException e) { e.printStackTrace(); } catch (NoSuchMethodException e) { // 找不到特定方法时引发 e.printStackTrace(); } catch (IllegalAccessException e) { // 无访问权限 e.printStackTrace(); } catch (InstantiationException e) { // 无法实例化指定的类对象时引发 e.printStackTrace(); } catch (InvocationTargetException e) { // 当被调用方法内部抛出异常而没有被捕获时 e.printStackTrace(); } } Method public static void main(String[] args) { try { Class\u0026lt;?\u0026gt; aClass = Class.forName(\u0026#34;reflect.entity.Employee\u0026#34;); Constructor\u0026lt;?\u0026gt; constructor = aClass.getConstructor(Integer.class, String.class, Float.class, String.class); Employee employee = (Employee) constructor.newInstance(1, \u0026#34;姓名\u0026#34;, 123F, \u0026#34;部门\u0026#34;); Method method = aClass.getMethod(\u0026#34;updateSalary\u0026#34;, Float.class); employee = (Employee) method.invoke(employee, 100F); System.out.println(employee); } catch (ClassNotFoundException | InvocationTargetException | IllegalAccessException | InstantiationException | NoSuchMethodException e) { e.printStackTrace(); } } Field Field对应类中的成员变量的声明 可以通过Field类对成员变量赋值和取值 public static void main(String[] args) { Class\u0026lt;Employee\u0026gt; aClass = Employee.class; String ename = null; try { Constructor\u0026lt;Employee\u0026gt; constructor = aClass.getConstructor(Integer.class, String.class, Float.class, String.class); Employee employee = constructor.newInstance(1, \u0026#34;姓名\u0026#34;, 3000F, \u0026#34;部门\u0026#34;); Field field = aClass.getField(\u0026#34;ename\u0026#34;); field.set(employee, \u0026#34;李磊\u0026#34;); ename = (String) field.get(employee); } catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException | InstantiationException e) { e.printStackTrace(); } catch (NoSuchFieldException e) { // 表示类没有指定名称的字段 e.printStackTrace(); } System.out.println(\u0026#34;ename: \u0026#34; + ename); } ","permalink":"https://2d992714.xianglin2020-github-io.pages.dev/posts/2020/java-reflect/","summary":"反射是在运行时动态访问类与对象的技术。","title":"Java 反射"},{"content":"Maven Maven 的核心特性 Maven 是项目管理工具，对软件项目提供构建和依赖管理 Maven 是 Apache 下的 Java 开源项目 Maven 为 Java 项目提供了统一的管理方式，已成为业界标准 项目设置遵循统一的规则，保证不同开发环境的兼容性 强大的依赖管理，项目依赖组件自动下载，自动更新 Maven 官网 Maven 项目的标准结构 使用 mvn命令创建 Maven 项目\nmvn -B archetype:generate -DgroupId=person.xianglin -DartifactId=myApp -DarchetypeArtifacttId=maven-archetype-quickstart -DarchetyVersion=1.4\n查看 Maven 项目结构\ntree\n➜ ImoocJava tree myApp myApp ├── pom.xml └── src ├── main │ └── java │ └── person │ └── xianglin │ └── App.java └── test └── java └── person └── xianglin └── AppTest.java Maven 的依赖管理 Maven 使用 Dependency自动下载，管理第三方 Jar 包 在pom.xml文件中配置项目的依赖 Maven 自动将依赖从远程仓库下载至本地仓库，并引入项目中 Maven 的打包方式 Maven 常用命令 命令 用途 mvn archetype:generate 创建 Maven 工程结构 mvn compile 编译源代码 mvn test 执行测试用例 mvn clean 清除产生的项目 mvn package 项目打包 mvn install 安装至本地仓库 archetype:generate 键入命令mvn archetype:generate\n输入项目信息\ncompile 键入编译命令mvn compile\n查看编译后的结构\ntest 运行当前项目中所有的测试用例：mvn test\npackage 将当前项目打包\nmvn package\nclean 清除产生的项目\nmvn clean\ninstall 安装项目至本地仓库\nmvn install\n查看本地仓库文件\nMaven 实战 Maven 安装 Maven 安装方式 至官网下载二进制压缩包解压至本地目录即可完成安装\nwget https://downloads.apache.org/maven/maven-3/3.6.3/binaries/apache-maven-3.6.3-bin.tar.gz tar -xzvf apache-maven-3.6.3-bin.tar.gz 使用 homebrew 完成安装\nbrew install maven 使用mvn -v命令验证安装\nbin ./mvn -v Apache Maven 3.6.3 (cecedd343002696d0abb50b32b541b8a6ba2883f) Maven home: /Users/xianglin/Documents/JavaToolPackage/apache-maven-3.6.3 Java version: 14.0.2, vendor: AdoptOpenJDK, runtime: /Library/Java/JavaVirtualMachines/adoptopenjdk-14.jdk/Contents/Home Default locale: zh_CN_#Hans, platform encoding: UTF-8 OS name: \u0026#34;mac os x\u0026#34;, version: \u0026#34;10.15.6\u0026#34;, arch: \u0026#34;x86_64\u0026#34;, family: \u0026#34;mac\u0026#34; 运行mvn help:system\n安装目录分析 使用tree命令查看 maven 的目录结构\ntree -L 1 apache-maven-3.6.3 apache-maven-3.6.3 ├── LICENSE ├── NOTICE ├── README.txt ├── bin ├── boot ├── conf └── lib bin\nbin目录包含了mvn命令运行的脚本，在命令行输入任何命令时，都是调用这些脚本。mvnDebug比mvn多了一条MAVEN_DEBUG_OPTS，作用是在运行 Maven 时开启 debug，以便调试Maven 本身。m2.conf是classworlds的配置文件。\nboot\n该目录只包含一个文件，即plexus-classworlds-2.6.0.jar。plexus-classworlds 是一个类加载器框架，Maven 使用该框架加载自己的类库。\nconf\n该目录包含一个非常重要的配置文件settings.xml，修改该文件，能在全局范围内定制Maven 的行为。\nlib\n该目录包含了所有 Maven 运行时所需要的Java 类库，包含 Maven 依赖的第三方依赖。\nMaven 使用入门 编写pom.xml文件 Maven 项目的核心是pom.xml,POM(Project Object Model)项目对象模型定义了项目的基本信息，描述项目如何构建，声明项目依赖，等等。\n现为 Hello-World 项目编写最简单的pom.xml文件\n创建名为hello-world的文件夹\nmkdir hello-world cd hello-world 编写pom.xml文件\n\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;modelVersion\u0026gt;4.0.0\u0026lt;/modelVersion\u0026gt; \u0026lt;groupId\u0026gt;person.xianglin\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;hello-world\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.0-SNAPSHOT\u0026lt;/version\u0026gt; \u0026lt;name\u0026gt;Maven Hello World Project\u0026lt;/name\u0026gt; \u0026lt;build\u0026gt; \u0026lt;plugins\u0026gt; \u0026lt;plugin\u0026gt; \u0026lt;groupId\u0026gt;org.apache.maven.plugins\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;maven-compiler-plugin\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;3.1\u0026lt;/version\u0026gt; \u0026lt;configuration\u0026gt; \u0026lt;source\u0026gt;1.8\u0026lt;/source\u0026gt; \u0026lt;target\u0026gt;1.8\u0026lt;/target\u0026gt; \u0026lt;/configuration\u0026gt; \u0026lt;/plugin\u0026gt; \u0026lt;/plugins\u0026gt; \u0026lt;/build\u0026gt; \u0026lt;/project\u0026gt; 第一行指定了 XML 文件的版本和编码方式 project元素是pom.xml的根元素 modelVersion指定了当前 POM 模型的版本 groupId、artifactId、version这三个元素定义一个项目的基本坐标。groupId定义项目属于哪个组，这个组一般与项目所在的公司和组织相关；artifactId定义当前Maven 项目在组中的唯一 ID；version指定当前项目的版本。 name元素声明一个友好的项目名称 编写主代码\n默认情况下，Maven 假定项目主代码位于src/main/java路径下，我们遵循 Maven 约定，创建该目录，然后在该目录下创建person/xianglin/helloworld/HelloWorld.java文件。\nmkdir -p src/main/java cd src/main/java mkdir -p person/xianglin/helloworld cd person/xianglin/helloworld vim HelloWorld.java package person.xianglin.helloworld; public class HelloWorld{ private String sayHello(){ return \u0026#34;Hello Maven\u0026#34;; } public static void main(String[] args){ System.out.println(new HelloWorld().sayHello()); } } 使用 Maven 进行编译\n在项目的根路径下运行mvn clean compile命令\nmvn clean compile Building Maven Hello World Project 1.0-SNAPSHOT [INFO] --- maven-clean-plugin:2.5:clean (default-clean) @ hello-world --- [INFO] Deleting /Users/xianglin/Documents/Maven/hello-world/target [INFO] --- maven-resources-plugin:2.6:resources (default-resources) @ hello-world --- [INFO] skip non existing resourceDirectory /Users/xianglin/Documents/Maven/hello-world/src/main/resources [INFO] [INFO] --- maven-compiler-plugin:3.1:compile (default-compile) @ hello-world --- [INFO] Changes detected - recompiling the module! [WARNING] File encoding has not been set, using platform encoding UTF-8, i.e. build is platform dependent! [INFO] Compiling 1 source file to /Users/xianglin/Documents/Maven/hello-world/target/classes [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------ [INFO] Total time: 1.832 s [INFO] Finished at: 2020-09-13T23:01:12+08:00 [INFO] ------------------------------------------------------------------------ Maven 首先执行了clean:clean任务，删除target 目录，然后执行resources:resources任务，最后执行compile:compile任务，将项目代码编译至target/classes目录下。\n编写测试代码\nMaven 项目默认的测试代码位于src/test/java目录下。为项目添加junit依赖，并编写测试代码\npackage person.xianglin.helloworld; import static org.junit.Assert.assertEquals; import org.junit.Test; public class HelloWorldTest{ @Test public void testSayHello(){ HelloWorld helloworld = new HelloWorld(); String result helloworld.sayHello(); assertEquals(\u0026#34;Hello Maven\u0026#34;, result); } } 调用 Maven执行测试\n使用mvn clean test命令\nBuilding Maven Hello World Project 1.0-SNAPSHOT maven-clean-plugin:2.5:clean (default-clean) @ hello-world maven-resources-plugin:2.6:resources (default-resources) @ hello-world maven-compiler-plugin:3.1:compile (default-compile) @ hello-world maven-resources-plugin:2.6:testResources (default-testResources) @ hello-world maven-compiler-plugin:3.1:testCompile (default-testCompile) @ hello-world maven-surefire-plugin:2.12.4:test (default-test) @ hello-world ------------------------------------------------------- T E S T S ------------------------------------------------------- Running person.xianglin.helloworld.HelloWorldTest Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.123 sec Results : Tests run: 1, Failures: 0, Errors: 0, Skipped: 0 [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------ [INFO] Total time: 2.921 s [INFO] Finished at: 2020-09-13T23:17:42+08:00 [INFO] ------------------------------------------------------------------------ 可见，Maven 先后执行了clean:clean resources:resources compile:compile resources:testResources compile:testCompile test:test六个任务。最后显示输出测试报告，显示一共运行了多少、失败、错误、跳过了多少测试用例。\n打包和运行\npom.xml中没有指定打包类型时，使用mvn clean package默认打包类型为 jar。\n[INFO] [INFO] --- maven-jar-plugin:2.4:jar (default-jar) @ hello-world --- [INFO] Building jar: /Users/xianglin/Documents/Maven/hello-world/target/hello-world-1.0-SNAPSHOT.jar [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------ [INFO] Total time: 3.320 s [INFO] Finished at: 2020-09-13T23:24:32+08:00 [INFO] ------------------------------------------------------------------------ Maven 会在打包之前执行编译、测试等操作，jar:jar任务负责打包，打包的文件按照artifactId-version.jar的格式命名。可以使用finalName来自定义该文件的命令。\n借助maven-shade-plugin生成可执行 jar\n在pom.xml中配置如下插件\n\u0026lt;plugin\u0026gt; \u0026lt;groupId\u0026gt;org.apache.maven.plugins\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;maven-shade-plugin\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.4\u0026lt;/version\u0026gt; \u0026lt;executions\u0026gt; \u0026lt;execution\u0026gt; \u0026lt;phase\u0026gt;package\u0026lt;/phase\u0026gt; \u0026lt;goals\u0026gt; \u0026lt;goal\u0026gt;shade\u0026lt;/goal\u0026gt; \u0026lt;/goals\u0026gt; \u0026lt;configuration\u0026gt; \u0026lt;transformers\u0026gt; \u0026lt;transformer implementation=\u0026#34;org.apache.maven.plugins.shade.resource.ManifestResourceTransformer\u0026#34;\u0026gt; \u0026lt;mainClass\u0026gt;person.xianglin.helloworld.HelloWorld\u0026lt;/mainClass\u0026gt; \u0026lt;/transformer\u0026gt; \u0026lt;/transformers\u0026gt; \u0026lt;/configuration\u0026gt; \u0026lt;/execution\u0026gt; \u0026lt;/executions\u0026gt; \u0026lt;/plugin\u0026gt; java -jar hello-world-1.0-SNAPSHOT.jar Hello Maven 使用Archetype生成项目骨架\nmvn archetype:generate 坐标和依赖 Maven 坐标 Maven 坐标为各种构件引入了秩序，任何一个构件都必须明确定义自己的坐标，而一组 Maven 坐标是通过一些元素定义的，它们是：groupId、artifactId、version、packaging、classifier。\n\u0026lt;groupId\u0026gt;org.sonatype.nexus\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;nexus-indexer\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;2.0.0\u0026lt;/version\u0026gt; \u0026lt;packaging\u0026gt;jar\u0026lt;/packaging\u0026gt; groupId：定义当前项目的实际项目。由于Maven中模块的概念，一个项目往往会被划分为很多模块。 artifactId：定义实际项目中的一个Maven模块，推荐做法是使用实际项目的名称作为artifactId的前缀。 version：定义Maven项目当前所处的版本。 package：定义Maven项目的打包方式，打包方式会影响到构建的生命周期。当不定义packaging时，Maven会使用默认值jar。 classifier：用来帮助定义构建输出的一些辅助构建。 依赖的配置 一个依赖声明可以包含如下一些元素\n\u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;\u0026lt;/version\u0026gt; \u0026lt;type\u0026gt;\u0026lt;/type\u0026gt; \u0026lt;scope\u0026gt;\u0026lt;/scope\u0026gt; \u0026lt;optional\u0026gt;\u0026lt;/optional\u0026gt; \u0026lt;exclusions\u0026gt; \u0026lt;exclusion\u0026gt; \u0026lt;/exclusion\u0026gt; \u0026lt;/exclusions\u0026gt; \u0026lt;/dependency\u0026gt; type：依赖的类型，对应于项目坐标定义中的packaging。大部分情况下，该元素不必声明，默认值为jar。 scope：依赖的范围。 option：标记依赖是否可选。 exclusions：用来排除传递性依赖。 依赖范围 依赖范围就是用来控制依赖与三种 classpath（编译 classpath、测试 classpath、运行 classpath）的关系，Maven 中有以下几种依赖范围：\ncompile：编译依赖范围，默认值。使用此依赖范围的 Maven 依赖，对于编译、测试、运行三种 classpath 都有效。\ntest：测试依赖范围，只对测试 classpath 有效，在编译主代码和运行项目时将无法使用此类依赖，典型的例子就是 JUnit。\nprovided：已提供依赖范围。使用此依赖范围的 Maven 依赖，对于编译和测试 classpath 有效，但在运行时无效。典型的例子就是servlet-api，编译和测试时需要此依赖，在运行项目时，容器已提供，就不想要重复地引入。\nruntime：运行时依赖范围。使用此依赖范围的 Maven 依赖，对于测试和运行 classpath 有效，但在编译主代码时无效。典型的例子是 JDBC 驱动实现，项目主代码编译时只需要 JDK 提供的 JDBC 接口，只有在执行时测试或运行项目时才需要具体的 JDBC 驱动。\nsystem：系统依赖范围。和provided依赖范围一致。使用 system 范围的依赖必须通过 systemPath 元素显示地指定依赖文件的路径。systemPath 可以引用环境变量。\n\u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;javax.sql\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;jdbc-stdext\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;2.0\u0026lt;/version\u0026gt; \u0026lt;scope\u0026gt;system\u0026lt;/scope\u0026gt; \u0026lt;systemPath\u0026gt;${java.home}/lib/rt.jar\u0026lt;/systemPath\u0026gt; \u0026lt;/dependency\u0026gt; import：导入依赖范围。该依赖范围不会对三种 classpath 产生实际的影响。\n传递性依赖 假设 A 依赖于 B，B 依赖于 C，A 对于 B 是第一直接依赖，B 对于 C 是第二直接依赖，A 对于 C 是传递性依赖。第一直接依赖的范围和第二直接依赖的范围决定了传递性依赖的范围。\n如表所示，最左边一行表示第一直接依赖，最上面一行表示第二直接依赖，中间单元格表示传递性依赖范围\ncompile test provided runtime compile compile -- -- runtime test test -- -- test provided provided -- provided provided runtime runtime -- -- runtime 依赖调解 路径最近者优先\n比如项目A 中有如下依赖：A-\u0026gt;B-\u0026gt;C-\u0026gt;X(1.0)、A-\u0026gt;D-\u0026gt;X(2.0)，X是 A 的传递性依赖，X(1.0)的路径长度为 3，而 X(2.0)都路径长度为 2，依据路径最近者优先原则，X(2.0)会被项目 A 解析使用。\n第一声明者优先\n在依赖路径长度相等的前提下，在 POM 中依赖声明的顺序决定了谁会被解析使用，顺序最靠前的那个依赖优先。\n覆盖优先\n在同一 POM 文件中后面声明的依赖会覆盖前面的依赖\n可选依赖 假设项目 A 依赖于项目B，项目 B依赖于项目 X或 Y，B 对于 X 和 Y 的依赖都是可选依赖，即A-\u0026gt;B、B-\u0026gt;X(optional)、 B-\u0026gt;Y(optional)。由于 X 和 Y是可选依赖，对 A 不会产生任何影响。如图所示\n排除依赖 假设项目 A 依赖于项目 B，项目 B依赖于项目 C，但由于某些原因不想引入传递性依赖C，而是自己显示的声明对项目 C 特定版本的依赖。使用exclusions元素声明排除依赖，使用 exclusion 时只需要groupId和artifictId。\n\u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;ch.qos.logback\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;logback-classic\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.2.3\u0026lt;/version\u0026gt; \u0026lt;exclusions\u0026gt; \u0026lt;exclusion\u0026gt; \u0026lt;groupId\u0026gt;org.slf4j\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;slf4j-api\u0026lt;/artifactId\u0026gt; \u0026lt;/exclusion\u0026gt; \u0026lt;/exclusions\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.slf4j\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;slf4j-api\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.7.30\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; ","permalink":"https://2d992714.xianglin2020-github-io.pages.dev/posts/2020/maven-basics/","summary":"\u003ch1 id=\"maven\"\u003eMaven\u003c/h1\u003e\n\u003ch2 id=\"maven-的核心特性\"\u003eMaven 的核心特性\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003eMaven 是项目管理工具，对软件项目提供构建和依赖管理\u003c/li\u003e\n\u003cli\u003eMaven 是 Apache 下的 Java 开源项目\u003c/li\u003e\n\u003cli\u003eMaven 为 Java 项目提供了统一的管理方式，已成为业界标准\u003c/li\u003e\n\u003cli\u003e项目设置遵循统一的规则，保证不同开发环境的兼容性\u003c/li\u003e\n\u003cli\u003e强大的依赖管理，项目依赖组件自动下载，自动更新\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://maven.apache.org/\" target=\"_blank\" rel=\"noopener\"\u003eMaven 官网\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"maven-项目的标准结构\"\u003eMaven 项目的标准结构\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e使用 \u003ccode\u003emvn\u003c/code\u003e命令创建 Maven 项目\u003c/p\u003e","title":"Maven 基础"},{"content":"MySQL事务 MySQL事务简介 数据库的事务是指一组SQL语句组成的数据库逻辑处理单元，在这组SQL操作中，要么全部执行成功，要么全部执行失败。简单又经典的例子就是转账，事务A中进行转账，那么转出的账号要扣钱，转入的账号要加钱。为了保证数据一致性，这两个操作都必需要同时执行成功。 MySQL 事务都是指在 InnoDB 引擎下，MyISAM 引擎是不支持事务的。 事务用来管理insert、update、delete语句。 MySQL事务特性 ACID简介 MySQL中事务的四大特性包括：原子性（Atomicity）、一致性（Consistent）、隔离性（Isolation）、持久性（Durable）,简称ACID。\n原子性是指事务的原子操作，对数据的修改要么全部执行成功，要么全部失败。实现事务的原子性，是基于日志的Redo/Undo机制。 一致性是指执行事务前后的状态要一致，可以理解为数据一致性。 隔离性侧重指事务之间相互隔离，不受影响，隔离性与事务设置的隔离级别有密切的关系。 持久性是指一个事务提交后，这个事务的状态会被持久化到数据库中，也就是事务提交，对数据的新增、更新将会持久化到数据库中。 原子性、隔离性、持久性都是为了保障一致性而存在的，一致性也是最终目的。\n事务的隔离级别 隔离级别 描述 READ UNCOMMITTED 可以读到其它事务未提交的数据 READ COMMITTED 事务只能读到其他事务已经提交过的数据 REPEATABLE READ 事务不会读到其它事务对数据的更改 SERIALIZABLE 将事务的执行变为顺序执行 MySQL 使用事务\n开启事务：begin / start transaction / set autocommit = 0\n提交事务：commit\n回滚事务：rollback\n在执行begin/start transaction命令时，它们并不是一个事务的起点，在执行完它们后的第一个 sql 语句，才表示事务真正的启动 。\n查看当前数据库的隔离级别\nselect @@transaction_isolation\nshow variables like 'transaction_isolation'\n查看当前运行的事务\nselect * from information_schema.innodb_trx;\n设置隔离级别\nset [session | global] transaction isolation level {read uncommitted | read committed | repeatable read | serializable}\n当设置完隔离级别后对于之前打开的会话，是无效的，要重新打开一个窗口设置隔离级别才生效。\n事务隔离级别演示\n创建一个User表并插入相关测试数据\ncreate table user ( id int(11) not null primary key auto_increment, name varchar(20), age tinyint default 0 ) engine = InnoDB default charset = utf8; 读未提交\n设置默认隔离级别为READ UNCOMMITTED\nset global transaction isolation level read uncommitted;\n打开两个查询窗口，同时开启事务，一个窗口更新数据，另一个窗口在更新数据前后分别查询一次数据\n# console1 select * from user; # console2 update user set name = \u0026#39;非科班的科班\u0026#39; where id = 1; # console1 select * from user;\n![image-20200907215622686](https://cdn.jsdelivr.net/gh/xianglin2020/gallery@master/202009/215641.png) 第二个session产生的未提交的事务状态会直接影响到第一个session，也就是脏读。 2. 读已提交 设置默认的事务隔离级别为`READ COMMITTED` `set global transaction isolation level read committed;` ![image-20200907220111041](https://cdn.jsdelivr.net/gh/xianglin2020/gallery@master/202009/220111.png) 重新打开两个窗口同时开启事务A和事务B。在事务B中使用update语句更新行，在事务A中进行查询。在事务B修改数据未提交时，事务A查询到的数据没有改变，当事务B执行提交后，事务A查询到的数据就是改变后的数据 ![image-20200907220353267](https://cdn.jsdelivr.net/gh/xianglin2020/gallery@master/202009/220353.png) 事务B的查询结果会受到事务A的影响，这就是不可重复读。 3. 可重复读 可重复读是对比不可重复读而言的，不可重复读是指同一事务不同时刻读取到的数据值可能不一致。而可重复读是指，事务不会读到其它事务对已有数据的修改，即使其它事务已提交。也就是说，事务开始时读取到的已有数据是什么，在事务提交前的任意时刻，这些数值都是一样的。*\u0026lt;s\u0026gt;但是，对于其它事务新插入的数据是可以读到的，也就引发了幻读问题。\u0026lt;/s\u0026gt;（MySQL的`repeatable read`解决了幻读问题）* ![image-20200907220740412](https://cdn.jsdelivr.net/gh/xianglin2020/gallery@master/202009/220740.png) 事务 A 在事务 B 删除记录前查询了 ID为 1 的记录，在事务 B中删除记录，并且提交事务，在事务 A 中依旧能查询到，即事务 A 不会读到其它事务对数据的修改。 4. 串行化 串行化是4种事务隔离级别中隔离效果最好的，解决了脏读、可重复读、幻读的问题，但是效果最差，它将事务的执行变为顺序执行，与其他三个隔离级别相比，它就相当于单线程，后一个事务的执行必须等待前一个事务结束。 Redo/Undo机制 redo log redo log叫做重做日志，用来实现事务的持久性。该日志文件由两个部分组成：重做日志缓冲（redo log buffer）、重做日志文件（redo log），前者在内存中，后者在磁盘中。当事务提交后会把所有修改信息保存到该日志中。\nMySQL 为了提升性能不会把每次的修改都实时同步到磁盘，而是先存到缓冲池中，然后使用后台线程去做缓冲池和磁盘间的同步。引入 redo log 来记录已成功提交事务的修改信息，并且把 redo log 同步到磁盘，用于恢复数据，保障数据库的一致性。\nundo log undo log叫做回滚日志，用于记录数据被修改前的信息。为了回滚之前的状态，需要将之前的操作都记录下来。用于保障事务的原子性。\nMySQL锁机制 MySQL中的锁可以分为：共享锁（Shared Locks）、排它锁（Exclusive Locks）、行锁（Record Locks）、表锁、间隙锁。\n共享锁是针对同一份数据，多个读操作可以同时进行，即读加锁，不能写并且可以并行读。 排它锁针对写操作，假设当前写操作没有完成，它会阻断其它的写锁和读锁，即写加锁，其它读写都阻塞。 行锁和表锁是从锁的粒度上划分的，行锁锁定当前数据行，锁的粒度小，加锁慢，发生锁冲突的概率小，并发度高。 表锁锁的粒度大，加锁快，发生冲突的概率大，并发度低。 间隙锁 数据库只有增、删、改才会加上排它锁，只是查询时并不会加锁。只能通过在 select 语句后显示的加lock in share mode或者for update 来加共享锁和排它锁。\nREAD UNCOMMITTED：没有加任何锁，所以它没有隔离效果，性能最好。 SERIALIZABLE：读的时候加共享锁，也就是其他事务可以并发读，但是不能写。写的时候加排它锁，其他事务不能并发写也不能并发读。 REPEATABLE READ：在事务开始的时候创建一致性视图，也叫做快照，之后的查询都公用这一个一致性视图，后续事务对数据的更改是对当前事务不可见的。 READ COMMITTED：每次执行语句的时候都重新生成一次快照。 快照遵循的规则 当前事务内的更新都是可见的； 版本未提交的，都是不可见的； 版本已提交，但是在创建快照之后提交的，是不可见的； 版本已提交，但是在创建快照之前提交的，是可见的。 MVCC READ COMMITTED、REPEATABLE READ 底层实现采用 MVCC（多版本并发控制）实现。\nInnoDB 的 MVCC，是通过在每行记录后面保存两个隐藏的列来实现的。这两个列，一个保存了行创建时间，一个保存了行过期时间，存储的是系统版本号。\n并发写问题 假如事务1和事务2都要执行update操作，事务1先update数据行的时候，先回获取行锁，锁定数据，当事务2要进行update操作的时候，也会取获取该数据行的行锁，但是已经被事务1占有，事务2只能wait。若是事务1长时间没有释放锁，事务2就会出现超时异常。\n加锁的过程要分有索引和无索引两种情况：\n有索引的情况，那么 MySQL 直接就在索引数中找到了这行数据，然后加上行锁； 若是没有索引的条件下，就获取所有行，都加上行锁，然后 MySQL 会再次过滤符合条件的的行并释放锁，只有符合条件的行才会继续持有锁。 ","permalink":"https://2d992714.xianglin2020-github-io.pages.dev/posts/2020/mysql-transaction/","summary":"\u003ch1 id=\"mysql事务\"\u003eMySQL事务\u003c/h1\u003e\n\u003ch2 id=\"mysql事务简介\"\u003eMySQL事务简介\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e数据库的事务是指一组SQL语句组成的数据库逻辑处理单元，在这组SQL操作中，要么全部执行成功，要么全部执行失败。简单又经典的例子就是转账，事务A中进行转账，那么转出的账号要扣钱，转入的账号要加钱。为了保证数据一致性，这两个操作都必需要同时执行成功。\u003c/li\u003e\n\u003cli\u003eMySQL 事务都是指在 InnoDB 引擎下，MyISAM 引擎是不支持事务的。\u003c/li\u003e\n\u003cli\u003e事务用来管理insert、update、delete语句。\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"mysql事务特性\"\u003eMySQL事务特性\u003c/h2\u003e\n\u003ch3 id=\"acid简介\"\u003eACID简介\u003c/h3\u003e\n\u003cp\u003eMySQL中事务的四大特性包括：\u003cstrong\u003e原子性（Atomicity）\u003c/strong\u003e、\u003cstrong\u003e一致性（Consistent）\u003c/strong\u003e、\u003cstrong\u003e隔离性（Isolation）\u003c/strong\u003e、\u003cstrong\u003e持久性（Durable）\u003c/strong\u003e,简称\u003ccode\u003eACID\u003c/code\u003e。\u003c/p\u003e","title":"MySQL 事务"},{"content":"JDBC 基础 IDEA IDEA 的下载地址：https://www.jetbrains.com/idea/ IDEA 使用视频教程：IDEA 神器使用技巧 IDEA 使用教程：IntelliJ-IDEA-Tutorial JDBC JDBC 简介 Java DataBase Connectivity\nJDBC 能让 Java 操作数据库\nJDBC 的优点\n统一的 API，提供一致的开发过程 抑郁学习，代码结构稳定 功能强大，执行效率高，能用于处理海量的数据 JDBC 使用步骤 加载并注册 JDBC 驱动 创建数据库连接(Connection) 创建 Statement 对象 执行查询操作(ResultSet) 关闭连接对象 public static void main(String[] args) throws Exception { // 1. 加载并注册 JDBC 驱动 // Class.forName(\u0026#34;com.mysql.cj.jdbc.Driver\u0026#34;); // 2. 获取数据库连接 Connection connection = DriverManager.getConnection( \u0026#34;jdbc:mysql://122.51.48.52:3306/imooc_demo\u0026#34;, \u0026#34;xianglin\u0026#34;, \u0026#34;950915\u0026#34; ); // 3. 创建 Statement 对象，执行查询操作 Statement statement = connection.createStatement(); ResultSet resultSet = statement.executeQuery(\u0026#34;select * from employee\u0026#34;); // 4. 遍历查询结果集 while (resultSet.next()) { int eno = resultSet.getInt(1); String ename = resultSet.getString(\u0026#34;ename\u0026#34;); float salary = resultSet.getFloat(\u0026#34;salary\u0026#34;); String dname = resultSet.getString(\u0026#34;dname\u0026#34;); System.out.println(dname + \u0026#34;--\u0026#34; + eno + \u0026#34;--\u0026#34; + ename + \u0026#34;--\u0026#34; + salary); } // 5. 关闭连接，释放资源 resultSet.close(); statement.close(); connection.close(); } JDBC 细节 DriverManager\n用于注册、管理 JDBC 驱动程序 DriverManager.getConnection()返回数据库连接 返回值Connection对象，对应数据库的物理网络连接 Connection\n用于 JDBC 与数据库的网络通信对象 java.sql.Connection是一个接口，具体由驱动厂商实现 所有数据库操作都建立在 Connection对象基础上 MySQL 连接字符串\njdbc:mysql://[hostip][:port]/databaseName?param\n参数列表采用 url 编码格式\n参数名 建议值 说明 useSSL 是否启用 ssl useUnicode true 启用 Unicode 编码传输数据 characterEncoding UTF-8 使用 UTF-8 编码传输数据 serverTimeZone Asia/Shanghai 使用东 8 时区 allowPublicKeyRetrieval true 运行从客户端获取公钥加密传输 PreparedStatement SQL 注入攻击是未对原始 SQL 中的铭感字符做特殊处理 PreparedStatement是Statement的子接口 PreparedStatement对SQL 进行参数化，防止 SQL 注入 数据库连接池 阿里巴巴 Druid连接池 Druid 是阿里巴巴开源的数据库连接池组件。\nDruid 能对数据库连接进行有效的管理与重用，最大化程序执行效率。\n连接池负责创建管理连接，应用程序只管使用和归还。\nDruid 使用方式\n获取 jar 文件\n\u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;com.alibaba\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;druid\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.1.23\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; 创建配置文件：db.properties\ndriverClass=com.mysql.cj.jdbc.Driver url=jdbc:mysql://122.51.48.52:3306/imooc_demo?useSSL=false username=xianglin password=950915 initialSize=10 maxActive=20 使用数据库连接池\n// 1. 加载属性文件 InputStream inputStream = DruidSample.class.getResourceAsStream(\u0026#34;/druid.properties\u0026#34;); Properties properties = new Properties(); properties.load(inputStream); // 2. 获取 DateSource 数据源对象 DataSource dataSource = DruidDataSourceFactory.createDataSource(properties); // 3. 创建数据库连接 Connection connection = dataSource.getConnection(); C3P0 C3P0 官网\nC3P0 使用方式\n获取 jar 文件\n\u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;com.mchange\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;c3p0\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;0.9.5.5\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; c3p0-config.xml\n\u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34; ?\u0026gt; \u0026lt;c3p0-config\u0026gt; \u0026lt;default-config\u0026gt; \u0026lt;property name=\u0026#34;driverClass\u0026#34;\u0026gt;com.mysql.cj.jdbc.Driver\u0026lt;/property\u0026gt; \u0026lt;property name=\u0026#34;jdbcUrl\u0026#34;\u0026gt;jdbc:mysql://122.51.48.52:3306/imooc_demo?useSSL=false\u0026lt;/property\u0026gt; \u0026lt;property name=\u0026#34;user\u0026#34;\u0026gt;xianglin\u0026lt;/property\u0026gt; \u0026lt;property name=\u0026#34;password\u0026#34;\u0026gt;950915\u0026lt;/property\u0026gt; \u0026lt;property name=\u0026#34;initialPoolSize\u0026#34;\u0026gt;10\u0026lt;/property\u0026gt; \u0026lt;property name=\u0026#34;maxPoolSize\u0026#34;\u0026gt;10\u0026lt;/property\u0026gt; \u0026lt;/default-config\u0026gt; \u0026lt;/c3p0-config\u0026gt; 使用数据库连接池\n// 1. 加载配置文件 // 2. 创建 DataSource 数据源 DataSource dataSource = new ComboPooledDataSource(); // 3. 获取数据库连接 Connection connection = dataSource.getConnection(); Apache Commons DBUtils Apache Commons DBUtils是 Apache 提供的开源JDBC 工具类库\n它对 JDBC简单封装，学习成本低\n使用 DBUtils 能简化编码工作量\ncommons-dbutil 官网\nDBUtils 的依赖坐标\n\u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;commons-dbutils\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;commons-dbutils\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.7\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; Statement 和 PreparedStatement Statement用于执行静态SQL 并返回结果 PreparedStatement用于执行预编译的 SQL 语句。 PreparedStatement由于预编译使得效率较高 PreparedStatement可以有效的防止 SQL 注入风险 ","permalink":"https://2d992714.xianglin2020-github-io.pages.dev/posts/2020/jdbc-basics/","summary":"jdbc 基础知识学习。","title":"jdbc 基础"},{"content":"MySQL SQL 与 NoSQL 常见的关系型数据库： MySQL Oracle DB2 SQL Server 常见的非关系型数据库 Redis MongoDB MySQL 安装与用户管理 MySQL 配置文件 /etc/mysql/mysql.conf.d/mysqld.cnf\nMySQL 数据库、表操作 SQL DML：数据操作语言 DCL：数据控制语言 DDL：数据定义语言 SQL 语句的注意事项 SQL 语句不区分大小写 SQL 语句必须以分号结尾 SQL 语句的注释 # 这是一段注释文字 /* 这是另一段注释文字 */ 数据库操作 # 数据库操作 # 创建数据库 create database demo; # 查询存在的数据库 show databases; # 删除数据库 drop database demo; 数据表操作 use demo; # 创建数据表--student create table student ( id int unsigned primary key, name varchar(20) not null, sex char(1) not null, birthday date not null, tel char(11) not null, remark varchar(200) ); insert into student values (1, \u0026#39;李强\u0026#39;, \u0026#39;男\u0026#39;, \u0026#39;1999-01-01\u0026#39;, \u0026#39;17328222989\u0026#39;, \u0026#39;备注\u0026#39;); # 查看表 show tables; # 查看表结构 desc student; # 查看建表语句 show create table student; # 删除表 drop table student; 修改数据表结构 # 修改表结构 # 增加表字段 alter table student add address VARCHAR(200) not null, add home_tel CHAR(11) not null; # 修改表字段属性 alter table student modify home_tel VARCHAR(20) not null; # 修改字段名 alter table student change address home_address VARCHAR(200) NOT NULL; # 删除字段 alter table student drop home_address, drop home_tel; desc student; MySQL 的数据类型和约束 数据类型 数值型\n类型 大小（字节） 范围（有符号） 范围（无符号） 说明 TINYINT 1 [-128, 127] [0, 255] 小整型值 SMALLINT 2 [-32768, 32767] [0, 65535] MEDIUMINT 3 INT 4 [-2147483648, 2147483647] [0, 4294967295] BIGINT 8 FLOAT 4 单精度浮点数 DOUBLE 8 双精度浮点数 DECIMAL DECIMAL(M,D) 对DECIMAL(M,D) ，如果M\u0026gt;D，为M+2否则为D+2 字符串类型\n类型 大小（字符） 说明 CHAR 1 - 255 定长字符串 VARCHAR 1 - 65535 变长字符串 TEXT 1 - 65535 长文本数据 BLOB 1 - 65535 二进制形式的长文本数据 char(n) 和 varchar(n) 中括号中 n 代表字符的个数，并不代表字节个数，比如 CHAR(30) 就可以存储 30 个字符。\n日期/时间类型\n类型 大小（字节） 范围 格式 说明 DATE 3 1000-01-01 / 9999-12-31 YYYY-MM-DD 日期值 TIME 3 -838:59:59 / 838:59:59 HH:MM:SS 时间值或持续时间 YEAR 1 1901 / 2155 YYYY 年份值 DATETIME 8 1000-01-01 00:00:00 / 9999-12-31 23:59:59 YYYY-MM-DD HH:MM:SS 日期时间 TIMESTAMP 4 YYYYMMDD HHMMSS 时间戳 关系型数据库三范式 列不可以再分：数据表的每一列都是不可分割的基本数据项，同一列中不能有多个值，也不能存在重复的属性 属性完全依赖于主键：数据表中的每条记录必须是唯一的。为了区分，通常要为表加上一个列存储唯一标识 属性不依赖于其他非主属性：每列都与主键有直接关系，不存在传递依赖 字段约束 约束名称 关键字 描述 主键约束 PRIMARY KEY 字段值唯一，且不能为 NULL 非空约束 NOT NULL 字段值不能为 NULL 唯一约束 UNIQUE 字段值唯一，可以为 NULL 外键约束 FOREIGN KEY 索引 数据量很大，而且经常被查询的数据可以设置索引\n索引只添加在经常被用作检索条件的字段上\n创建表时添加索引\n# 使用索引 create table t_message ( id int unsigned primary key, content varchar(200) not null, type enum (\u0026#34;公告\u0026#34;,\u0026#34;通报\u0026#34;,\u0026#34;个人通知\u0026#34;) not null, create_time timestamp not null, index idx_type (type) ); 操作索引\n# 添加删除索引 create index idx_type on t_message (type); alter table t_message add index idx_time (create_time); # 查看索引 show index from t_message; # 删除索引 drop index idx_type on t_message; 数据库的基本查询 数据的简单查询（别名、字段计算） # 基本查询 select * from t_emp; select empno, ename from imooc_demo.t_emp; # 使用列别名 select empno, sal * 12 as income from imooc_demo.t_emp; 数据的高级查询（排序、分页、去重） 分页\n# 分页查询 select empno, ename from imooc_demo.t_emp limit 10,5; select * from imooc_demo.t_emp limit 10; 排序：ASC、DESC\n# 排序 select empno, ename, sal, deptno from imooc_demo.t_emp order by sal desc; select empno, ename, sal, hiredate from imooc_demo.t_emp order by sal desc, hiredate; select empno,ename,sal from imooc_demo.t_emp order by sal desc limit 0,5; 去重\n# 去重 select job from imooc_demo.t_emp; select distinct job from imooc_demo.t_emp; # 去重失效 select distinct job, ename from imooc_demo.t_emp; 数据的有条件查询（表达式） # 条件查询 select empno, ename, sal from imooc_demo.t_emp where deptno = 10 and sal \u0026gt;= 2000; # 数字运算 select empno, ename, sal, hiredate from imooc_demo.t_emp where deptno = 10 and (sal + ifnull(comm, 0)) * 12 \u0026gt;= 15000 and datediff(now(), hiredate) / 365 \u0026gt;= 20; IS NULL IS NOT NULL BETWEEN AND LIKE REGEXP 数据库高级查询 聚合函数 函数 说明 avg()、sum() 返回某列的平均值、和 count() 返回某列的行数 max()、min() 返回某列的最大值、最小值 select avg(sal + ifnull(comm, 0)) as avg from imooc_demo.t_emp; select sum(sal) from imooc_demo.t_emp where deptno in (10, 20); select ename from t_emp where char_length(ename) = (select max(char_length(ename)) from imooc_demo.t_emp); select max(hiredate) from imooc_demo.t_emp; select count(*), count(comm) from imooc_demo.t_emp; 分组 select deptno, round(avg(sal)) from imooc_demo.t_emp group by deptno; select deptno, avg(sal), sum(sal), max(sal), min(sal) from imooc_demo.t_emp group by deptno with rollup; # group_concat select deptno, group_concat(ename), count(*) from imooc_demo.t_emp where sal \u0026gt;= 2000 group by deptno; 表连接查询 表连接分为两种：内连接和外连接\n内连接用于查询多张关系表满足连接条件的记录\n内连接的多种语法形式\nselect ename from imooc_demo.t_emp where deptno = (select deptno from imooc_demo.t_emp where ename = \u0026#39;SCOTT\u0026#39;); select e2.ename from imooc_demo.t_emp e1 join imooc_demo.t_emp e2 on e1.deptno = e2.deptno where e1.ename = \u0026#39;SCOTT\u0026#39; and e2.ename != e1.ename; 外连接的各种语法形式\n# 查询每名员工的编号、姓名、部门、月薪、工资等级、工龄、上司编号、上司姓名、上司部门 select e1.empno, e1.ename, d1.dname, (e1.sal + ifnull(e1.comm, 0)) as sal, s.grade, floor(datediff(now(), e1.hiredate) / 365) as gl, e2.empno ssbh, e2.ename ssxm, d2.dname ssbm from t_emp e1 join t_emp e2 on e1.mgr = e2.empno left join t_dept d1 on e1.deptno = d1.deptno left join t_salgrade s on (e1.sal + ifnull(e1.comm, 0)) between s.losal and s.hisal left join t_dept d2 on e2.deptno = d2.deptno; select e1.empno, e1.ename, (select d1.dname from t_dept d1 where d1.deptno = e1.deptno) dname, (e1.sal + ifnull(e1.comm, 0)) as sal, (select s.grade from t_salgrade s where (e1.sal + ifnull(e1.comm, 0)) between s.losal and s.hisal) grade, floor(datediff(now(), e1.hiredate) / 365) as gl, e2.empno ssbh, e2.ename ssxm, (select d2.dname from t_dept d2 where d2.deptno = e2.deptno) ssbm from t_emp e1 left join t_emp e2 on e1.mgr = e2.empno; 子查询 子查询是一种查询中嵌套查询的语句 WHERE 子查询 FROM 子查询，只会执行一次 SELECT 子查询每输出一条记录都要执行一次，查询效率很低 单行子查询 多行子查询，只能出现在 WHERE 和 FROM 子查询中 MySQL对数据的基本操作 数据添加 INSERT insert into tableName(col1,clo2,...) values(value1,value2,...),(value1,value2,...); insert into t_dept(deptno, dname, loc) values (60, \u0026#39;技术部\u0026#39;, \u0026#39;北京\u0026#39;), (70, \u0026#39;保安部\u0026#39;, \u0026#39;北京\u0026#39;); insert into t_emp (empno, ename, job, mgr, hiredate, sal, comm, deptno) VALUES (8001, \u0026#39;李娜\u0026#39;, \u0026#39;SALESMAN\u0026#39;, 8000, \u0026#39;1988-12-20\u0026#39;, 200, null, (select deptno from t_dept where dname = \u0026#39;技术部\u0026#39;)); MySQL 的 INSERT 语句方言\ninsert [into] tableName set col1 = value1,col2 = value2; insert into t_dept set deptno = 8000, dname = \u0026#39;开发部\u0026#39;, loc = \u0026#39;上海\u0026#39;; IGNORE 关键字会让 INSERT 只插入数据库不存在的记录\ninsert ignore into t_dept (deptno, dname, loc) values (40, \u0026#39;部门名称\u0026#39;, \u0026#39;部门地址\u0026#39;); 数据修改 update [ignore] tableName set col1 = value1, col2 = value2,... [where condition] [order by ...] [limit ...]; UPDATE 语句的表连接\n表链接的 UPDATE 语句可以修改多张表的记录\nupdate table1 [left | right]join table2 on condition set col1 = value1, col2 = value2 ...; update table1, table2 set col1 = value1, col2 = value2 where condition; # 把 ALLEN 调往 REASERCH 部门，职务调整为ANALYST update t_emp e join t_dept d set e.deptno = d.deptno, e.job = \u0026#39;ANALYST\u0026#39;, d.loc = \u0026#39;北京\u0026#39; where e.ename = \u0026#39;ALLEN\u0026#39; AND d.dname = \u0026#39;RESEARCH\u0026#39;; # 把底薪低于公司平均底薪的员工，底薪增加150 元 update t_emp join (select avg(sal) as avg from t_emp) as s on sal \u0026lt; s.avg set sal = sal + 150; update t_emp e left join t_dept d on e.deptno = d.deptno set e.deptno = 20 where e.deptno is null or (d.dname = \u0026#39;SALES\u0026#39; and e.sal \u0026lt; 2000); 数据删除 DELETE [IGNORE] FROM tableName [where condition] [order by ...] [limit ...]; delete from t_emp where deptno = 20 order by sal + ifnull(comm, 0) desc limit 1; DELETE 语句可以引入表连接\nDELETE table1 ,... FROM table1 JOIN table2 ON codition [WHERE ...] [ORDER BY ...] [LIMIT ...]; # 删除 SALES 部门下所有员工和 SALES 部门记录 DELETE e,d FROM t_emp e join t_dept d on e.deptno = d.deptno where d.dname = \u0026#39;SALES\u0026#39;; # 删除每个部门中底薪低于部门平均底薪的员工 delete e from t_emp e join (select deptno, avg(sal) as avg from t_emp group by deptno) t on e.sal \u0026lt; t.avg; # 删除员工 KING 和直接下属的记录 delete e from t_emp e join (select empno from t_emp where ename = \u0026#39;KING\u0026#39;) t on e.mgr = t.empno or e.empno = t.empno; DELETE 语句既可以是内连接也可以是外连接\n快速删除数据表中的全量记录\nTRUNCATE TABLE tableName\nMySQL 函数 数字函数 # 数字函数 select abs(-100); select round(4.6288, 2); # 4.63 select floor(9.9); # 9 select ceil(3.2); # 4 select power(2, 3); # 8 select log(7, 3); select ln(10); select sqrt(9); select pi(); 日期函数 获取当前日期时间：now()、curDate()、curTime()\n日期格式化函数：date_format()\n日期偏移计算：date_add(date,interval 偏移量 时间单位)\nselect ADDDATE(now(), 1); select date_add(now(), interval 1 day); 字符函数 函数 说明 left() 返回串左边的字符 right() 返回串右边的字符 ltrim() 去掉串左边的空格 rtrim() 去掉串右边的空格 trim() 去掉串左、右两边的字符 upper() 将串转为大写 lower() 将串转为小写 length()、char_length()、character_length() 返回串的长度 locate() 找出一个串的子串 substr()、substring() 返回字串的字符 concat() 链接字符串 replace() 替换字符 insert() 插入、替换字符串 select left(\u0026#39;left\u0026#39;, 0); select right(\u0026#39;right\u0026#39;, 4); select ltrim(\u0026#39; 123 \u0026#39;); select rtrim(\u0026#39; 123 \u0026#39;); select trim(\u0026#39; 1 2 3 \u0026#39;); select upper(\u0026#39;Abc\u0026#39;); select lower(\u0026#39;Abc\u0026#39;); select length(\u0026#39;123\u0026#39;); -- 3 select length(\u0026#39;abc\u0026#39;); -- 3 select length(\u0026#39;中文 \u0026#39;); -- 7 select length(\u0026#39;中A\u0026#39;); -- 4 select char_length(\u0026#39;123 \u0026#39;); -- 4 select char_length(\u0026#39;abc\u0026#39;); -- 3 select char_length(\u0026#39;中文\u0026#39;); -- 2 select char_length(\u0026#39;中A\u0026#39;); -- 2 select character_length(\u0026#39;123\u0026#39;); -- 3 select character_length(\u0026#39;abc\u0026#39;); -- 3 select character_length(\u0026#39;中文\u0026#39;); -- 2 select character_length(\u0026#39;中A\u0026#39;); -- 2 select locate(\u0026#39;ab1c\u0026#39;, \u0026#39;中3abcde\u0026#39;); select substr(\u0026#39;123\u0026#39;, -3); select substring(\u0026#39;123\u0026#39;, -1); select substring_index(\u0026#39;www.ee.com\u0026#39;, \u0026#39;.\u0026#39;, -1); select insert(\u0026#39;你好\u0026#39;, 1, 1, \u0026#39;先生\u0026#39;); select replace(\u0026#39;先生你好\u0026#39;, \u0026#39;先生\u0026#39;, \u0026#39;女士\u0026#39;); 条件函数 IF()\nselect if(\u0026#39;a\u0026#39; \u0026gt; \u0026#39;b\u0026#39;, \u0026#39;a\u0026#39;, \u0026#39;b\u0026#39;); select IF(day(now()) = 1, \u0026#39;第一天\u0026#39;, \u0026#39;不是第一天\u0026#39;) IFNULL()\nselect ifnull(null, \u0026#39;a\u0026#39;); CASE()\nCASE WHEN condition1 THEN value1 WHEN condition2 THEN value2 ELSE value END select case day(now()) when 1 then \u0026#39;第一天\u0026#39; else \u0026#39;不是第一天\u0026#39; end ; MySQL 的事务机制 MySQL 事务 START TRANSACTION; comment; [ROLLBACK]|[COMMIT]; start transaction; delete from t_emp; delete from t_dept; select * from t_dept; commit; 事务的 ACID 原子性：一个事务中的所有操作要么全部成功，要么全部失败。事务执行后，不允许存在中间状态； 一致性：不管在任何给定时间，并发事务有多少，事务必须保证运行结果的一致性； 隔离性：事务不受其它并发事务的影响； 持久性：事务一旦提交，结果便是永久性的。即便发生宕机，依然可以依靠事务日志完成数据的持久化。 事务的四个隔离级别 隔离级别 描述 READ UNCOMMITTED 可以读取其它事务未提交的数据 READ COMMITTED 读取其它事务提交的数据 repeatable read 重复读，就是在开始读取数据（事务开启）时，不再允许修改操作 Serializable 事务串行化顺序执行，可以避免脏读、不可重复读与幻读 SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; MySQL 数据导入导出 数据导出，导出的是业务数据\n数据备份，备份数据文件、日志文件、索引文件等等\nmysqldump 用来把业务数据导出成 SQL 文件\nmysqldump -uroot -p [no-data] database \u0026gt; path mysqldump -uxianglin -p mybatis \u0026gt; mybatis.sql source 命令用于导入 SQL 文件，包括创建数据表、写入记录等。\nSQL 练习 表结构如下：\nCREATE TABLE `dept` ( `deptno` int(10) UNSIGNED NOT NULL AUTO_INCREMENT, `dname` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, `loc` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, PRIMARY KEY (`deptno`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 71 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic; CREATE TABLE `emp` ( `empno` int(10) UNSIGNED NOT NULL AUTO_INCREMENT, `ename` varchar(15) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, `job` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, `mgr` int(10) UNSIGNED NULL DEFAULT NULL, `hiredate` date NULL DEFAULT NULL, `sal` decimal(10, 2) NULL DEFAULT NULL, `comm` decimal(7, 2) NULL DEFAULT NULL, `deptno` int(10) UNSIGNED NULL DEFAULT NULL, PRIMARY KEY (`empno`) USING BTREE, INDEX `deptno`(`deptno`) USING BTREE, CONSTRAINT `emp_ibfk_1` FOREIGN KEY (`deptno`) REFERENCES `dept` (`deptno`) ON DELETE RESTRICT ON UPDATE RESTRICT ) ENGINE = InnoDB AUTO_INCREMENT = 8019 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic; 按部门编号升序、工资倒序排列员工信息\nselect * from emp order by deptno asc, sal desc; 列出 deptno=30的部门名称及员工\nselect e.*,d.dname from emp e, dept d where e.deptno = d.deptno and d.deptno = 30; 列出每个部门最高、最低、平均工资\nselect deptno, max(sal) max, min(sal) min, avg(sal) avg from emp group by deptno; 列出市场部 SALES 及研发部RESEARCH的员工\nselect e.*,d.dname from emp e, dept d where e.deptno = d.deptno and d.dname in (\u0026#39;SALES\u0026#39;,\u0026#39;RESEARCH\u0026#39;) ORDER BY d.deptno; 列出人数超过三人的部门\nselect * from dept d where d.deptno in (select e.deptno from emp e group by e.deptno having count(*) \u0026gt; 3); select d.dname, count(*) from dept d , emp e where d.deptno = e.deptno group by d.dname having count(*) \u0026gt; 3; 计算 MILLER 年薪比 SMITH 高多少\nselect s.salary - m.salary from (select e.sal * 12 + ifnull(e.comm,0) salary, e.ename from emp e where e.ename = \u0026#39;SMITH\u0026#39;) m, (select e.sal * 12 + ifnull(e.comm,0) salary, e.ename from emp e where e.ename = \u0026#39;MILLER\u0026#39;) s 列出直接向KING 汇报的员工\nselect * from emp e where e.mgr = (select empno from emp where ename = \u0026#39;KING\u0026#39;); select e.* from emp e,emp e1 where e.mgr = e1.empno and e1.ename = \u0026#39;KING\u0026#39;; select e.* from emp e1 left join emp e on e1.empno = e.mgr where e1.ename = \u0026#39;KING\u0026#39;; 列出公司所有员工的工龄，并按倒序排列\nselect e.ename ,ceil(datediff(now(),hiredate)/365) age from emp e order by age 计算管理者与基层员工平均薪资的差距\nselect a.avg - b.avg from (select avg(sal) avg from emp where job = \u0026#39;PRESIDENT\u0026#39; or job = \u0026#39;MANAGER\u0026#39;) a, (select avg(sal) avg from emp where job not IN (\u0026#39;PRESIDENT\u0026#39; ,\u0026#39;MANAGER\u0026#39;)) b ","permalink":"https://2d992714.xianglin2020-github-io.pages.dev/posts/2020/mysql-basics/","summary":"\u003ch1 id=\"mysql\"\u003eMySQL\u003c/h1\u003e\n\u003ch2 id=\"sql-与-nosql\"\u003eSQL 与 NoSQL\u003c/h2\u003e\n\u003ch3 id=\"常见的关系型数据库\"\u003e常见的关系型数据库：\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003eMySQL\u003c/li\u003e\n\u003cli\u003eOracle\u003c/li\u003e\n\u003cli\u003eDB2\u003c/li\u003e\n\u003cli\u003eSQL Server\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"常见的非关系型数据库\"\u003e常见的非关系型数据库\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003eRedis\u003c/li\u003e\n\u003cli\u003eMongoDB\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"mysql-安装与用户管理\"\u003eMySQL 安装与用户管理\u003c/h3\u003e\n\u003ch3 id=\"mysql-配置文件\"\u003eMySQL 配置文件\u003c/h3\u003e\n\u003cp\u003e\u003ccode\u003e/etc/mysql/mysql.conf.d/mysqld.cnf\u003c/code\u003e\u003c/p\u003e\n\u003ch2 id=\"mysql-数据库表操作\"\u003eMySQL 数据库、表操作\u003c/h2\u003e\n\u003ch3 id=\"sql\"\u003eSQL\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003eDML：数据操作语言\u003c/li\u003e\n\u003cli\u003eDCL：数据控制语言\u003c/li\u003e\n\u003cli\u003eDDL：数据定义语言\u003c/li\u003e\n\u003cli\u003eSQL 语句的注意事项\n\u003col\u003e\n\u003cli\u003eSQL 语句不区分大小写\u003c/li\u003e\n\u003cli\u003eSQL 语句必须以分号结尾\u003c/li\u003e\n\u003c/ol\u003e\n\u003c/li\u003e\n\u003cli\u003eSQL 语句的注释\n\u003col\u003e\n\u003cli\u003e\u003ccode\u003e# 这是一段注释文字\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003e/* 这是另一段注释文字 */\u003c/code\u003e\u003c/li\u003e\n\u003c/ol\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"数据库操作\"\u003e数据库操作\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-sql\" data-lang=\"sql\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"o\"\u003e#\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"err\"\u003e数据库操作\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"o\"\u003e#\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"err\"\u003e创建数据库\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003ecreate\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"k\"\u003edatabase\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003edemo\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"o\"\u003e#\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"err\"\u003e查询存在的数据库\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eshow\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003edatabases\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"o\"\u003e#\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"err\"\u003e删除数据库\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003edrop\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"k\"\u003edatabase\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003edemo\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch3 id=\"数据表操作\"\u003e数据表操作\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-sql\" data-lang=\"sql\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003euse\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003edemo\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"o\"\u003e#\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"err\"\u003e创建数据表\u003c/span\u003e\u003cspan class=\"c1\"\u003e--student\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003ecreate\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"k\"\u003etable\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003estudent\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"n\"\u003eid\u003c/span\u003e\u003cspan class=\"w\"\u003e       \u003c/span\u003e\u003cspan class=\"nb\"\u003eint\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003eunsigned\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"k\"\u003eprimary\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"k\"\u003ekey\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"n\"\u003ename\u003c/span\u003e\u003cspan class=\"w\"\u003e     \u003c/span\u003e\u003cspan class=\"nb\"\u003evarchar\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"mi\"\u003e20\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"k\"\u003enot\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"k\"\u003enull\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"n\"\u003esex\u003c/span\u003e\u003cspan class=\"w\"\u003e      \u003c/span\u003e\u003cspan class=\"nb\"\u003echar\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"mi\"\u003e1\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\u003cspan class=\"w\"\u003e     \u003c/span\u003e\u003cspan class=\"k\"\u003enot\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"k\"\u003enull\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"n\"\u003ebirthday\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"nb\"\u003edate\u003c/span\u003e\u003cspan class=\"w\"\u003e        \u003c/span\u003e\u003cspan class=\"k\"\u003enot\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"k\"\u003enull\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"n\"\u003etel\u003c/span\u003e\u003cspan class=\"w\"\u003e      \u003c/span\u003e\u003cspan class=\"nb\"\u003echar\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"mi\"\u003e11\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"k\"\u003enot\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"k\"\u003enull\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"n\"\u003eremark\u003c/span\u003e\u003cspan class=\"w\"\u003e   \u003c/span\u003e\u003cspan class=\"nb\"\u003evarchar\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"mi\"\u003e200\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003einsert\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"k\"\u003einto\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003estudent\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003evalues\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"mi\"\u003e1\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;李强\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;男\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;1999-01-01\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;17328222989\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;备注\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"o\"\u003e#\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"err\"\u003e查看表\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eshow\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003etables\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"o\"\u003e#\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"err\"\u003e查看表结构\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003edesc\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003estudent\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"o\"\u003e#\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"err\"\u003e查看建表语句\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eshow\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"k\"\u003ecreate\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"k\"\u003etable\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003estudent\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"o\"\u003e#\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"err\"\u003e删除表\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003edrop\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"k\"\u003etable\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003estudent\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch3 id=\"修改数据表结构\"\u003e修改数据表结构\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-sql\" data-lang=\"sql\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"o\"\u003e#\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"err\"\u003e修改表结构\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"o\"\u003e#\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"err\"\u003e增加表字段\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003ealter\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"k\"\u003etable\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003estudent\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"k\"\u003eadd\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003eaddress\u003c/span\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"nb\"\u003eVARCHAR\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"mi\"\u003e200\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"k\"\u003enot\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"k\"\u003enull\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"k\"\u003eadd\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003ehome_tel\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"nb\"\u003eCHAR\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"mi\"\u003e11\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\u003cspan class=\"w\"\u003e     \u003c/span\u003e\u003cspan class=\"k\"\u003enot\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"k\"\u003enull\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"o\"\u003e#\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"err\"\u003e修改表字段属性\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003ealter\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"k\"\u003etable\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003estudent\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"k\"\u003emodify\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003ehome_tel\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"nb\"\u003eVARCHAR\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"mi\"\u003e20\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"k\"\u003enot\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"k\"\u003enull\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"o\"\u003e#\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"err\"\u003e修改字段名\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003ealter\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"k\"\u003etable\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003estudent\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"n\"\u003echange\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003eaddress\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003ehome_address\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"nb\"\u003eVARCHAR\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"mi\"\u003e200\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"k\"\u003eNOT\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"k\"\u003eNULL\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"o\"\u003e#\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"err\"\u003e删除字段\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003ealter\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"k\"\u003etable\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003estudent\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"k\"\u003edrop\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003ehome_address\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"k\"\u003edrop\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003ehome_tel\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003edesc\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003estudent\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch2 id=\"mysql-的数据类型和约束\"\u003eMySQL 的数据类型和约束\u003c/h2\u003e\n\u003ch3 id=\"数据类型\"\u003e数据类型\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e数值型\u003c/p\u003e","title":"MySQL 与数据库"},{"content":"JSON入门 JSON语法规则 JavaScript Object Notation JSON是轻量级的数据交换格式 独立于语言、自解释、易于理解书写 JavaScript与JSON JSON.parse()将json格式的字符串转为JSON对象 JSON.stringify()将JSON对象转为json字符串 Java与JSON Java 的JSON工具包：FastJson、Jackson、Gson、Json-lib fastjson是阿里巴巴的开源JSON解析库，它可以解析JSON格式的字符串，支持将Java Bean序列化为JSON字符串，也可以从JSON字符串反序列化到JavaBean。 JSON.toJSONString(Object object) JSON.parseObject(String text, Class\u0026lt;T\u0026gt; clazz) JSON.parseArray(String text, Class\u0026lt;T\u0026gt; clazz) jQuery\u0026amp;Ajax jQuery使用 jQuery是一个轻量级的js库，使用简便 jQuery的核心是选择器，用于获取页面的元素 jQuery官网，采用独立JS文件发布 jQuery选择器 jQuery选择器用于选中需要操作的页面元素：$(expression)\njQuery基础选择器\njQuery层叠选择器\njQuery属性选择器\n位置选择器\n表单选择器\njQuery操作元素 操作元素的属性：$().attr()、$().removeAttr(name) 操作元素的样式：css()、addClass()、removeClass() 操作元素的内容：val()、text()、html() jQuery处理事件 on('eventName',function(event){})\njQuery常用事件\nAjax原理 Ajax:Asynchronous JavaScript and XML（异步的JavaScript和XML） XMLHttpRequest使用步骤和属性及方法 创建 XMLHttpRequest\n发送请求\n处理服务类响应\nlet xhr; // 创建请求 if (window.XMLHttpRequest) { xhr = new XMLHttpRequest(); } else { xhr = new ActiveXObject(\u0026#34;Microsoft.XMLHTTP\u0026#34;); } // 发送请求 xhr.open(\u0026#39;GET\u0026#39;, \u0026#39;http://localhost:8080/ajax/ajax\u0026#39;, true); xhr.send(); // 处理响应 xhr.onreadystatechange = function () { // 响应已被接收且服务器处理成功 if (xhr.readyState === 4 \u0026amp;\u0026amp; xhr.status === 200) { document.getElementById(\u0026#34;divContent\u0026#34;).innerHTML = xhr.responseText; } } XMLHttpRequest状态\njQuery 对 Ajax 的支持 Ajax 常用设置项\n使用方法：\n$.ajax({ url: \u0026#39;/ajax/news\u0026#39;, type: \u0026#39;get\u0026#39;, dataType: \u0026#39;json\u0026#39;, success: function (json) { console.log(json); $.each(json, (i, news) =\u0026gt; { console.log(news); console.log(i); let content = $(\u0026#39;#divContent\u0026#39;); content.html(content.html() + `\u0026lt;h2\u0026gt;${i + 1}.${news.title}\u0026lt;/h2\u0026gt;\u0026lt;br\u0026gt;\u0026lt;p\u0026gt;${news.date}:${news.content}\u0026lt;/p\u0026gt;`); }); } }) Java正则表达式 正则表达式语法 正则表达式是检查、匹配字符串的表达式\n字符串的查找、替换是正则表达式的主要用途\n字符范围匹配，正则表达式测试工具\n元字符\n多次重复匹配\n定位匹配\n贪婪模式和非贪婪模式\n表达式分组\n使用正则表达式 Java 使用正则表达式\npublic class RegexSample { // 1. 创建正则表达式 private static final Pattern PATTERN = Pattern.compile(\u0026#34;\u0026lt;li\u0026gt;([\\\\u4e00-\\\\u9fa5]{2,8})([a-zA-Z]{1,10})\u0026lt;/li\u0026gt;\u0026#34;); public static void main(String[] args) throws IOException { // 2. 获取内容 String source = \u0026#34;/Users/xianglin/Documents/IdeaProjects/JavaWeb/json-ajax-reg-filter-listener/web/sample.html\u0026#34;; BufferedReader reader = new BufferedReader(new FileReader(source, StandardCharsets.UTF_8)); StringBuilder content = new StringBuilder(); String lineText; while ((lineText = reader.readLine()) != null) { content.append(lineText); } // 3. 匹配正则表达式 Matcher matcher = PATTERN.matcher(content); // 4. 分析处理结果 while (matcher.find()) { // 匹配完整字符串 System.out.println(matcher.group(0)); String chineseName = matcher.group(1); String englishName = matcher.group(2); System.out.println(chineseName + \u0026#34;-\u0026#34; + englishName); } reader.close(); } } 过滤器 过滤器的作用和使用场景 过滤器是 J2EE Servlet 模块下的标准组件 Filter 的作用是对 URL进行统一的拦截处理 Filter通常用于应用程序层面的全局处理 任何过滤器都要实现接口：javax.servlet.Filter 在Filter 实现类中的doFilter方法编写过滤器的功能代码 在 web.xml 中对过滤器进行配置，说明拦截 URL 的范围 在web.xml中配置\u0026lt;init-param\u0026gt;为 Filter 提供参数 过滤器开发和原理 过滤器的生命周期 初始化：Filter 创建 Filter.init() 提供服务：Filter.doFilter() 销毁：Filter.destroy() 过滤链 每个过滤器应该只有单独的职能\n过滤器的执行顺序是以\u0026lt;filter-mapping\u0026gt;配置为准\n注解形式的过滤链是按照过滤器的类名的字母升序的顺序依次执行的而不是filterName名称\n监听器 监听器的作用 监听器 Listener 是 J2EE Servlet 中的标准模块 Listener的作用是对Web 应用对象的行为进行监控 三种监听对象 ServletContext:javax.servlet.ServletContextListener HttpSession:javax.servlet.http.HttpSessionListener ServletRequest:javax.servlet.ServletRequestListener 属性监听接口 javax.servlet.ServletContextAttributeListener javax.servlet.http.HttpSessionAttributeListener javax.servlet.ServletRequestAttributeListener 监听器的使用 实现 XXXListener 接口，不同接口对应不同的监听对象 实现监听器中的独有方法 在web.xml对监听器进行配置 重定向与请求转发 请求转发是服务器跳转，只会产生一次请求 请求转发语句是：request.getRequestDispatcher().forward() 重定向是浏览器端的跳转，会产生两次请求 Status Code 302 Location: /sb 重定向语句是：response.sendRedirect() Session原理 Freemarker 模板引擎的实现过程 数据+模板=结果 将数据与展示解耦 JSP、Freemarker ","permalink":"https://2d992714.xianglin2020-github-io.pages.dev/posts/2020/javaweb-advanced/","summary":"\u003ch1 id=\"json入门\"\u003eJSON入门\u003c/h1\u003e\n\u003ch2 id=\"json语法规则\"\u003eJSON语法规则\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003eJavaScript Object Notation\u003c/li\u003e\n\u003cli\u003eJSON是轻量级的数据交换格式\u003c/li\u003e\n\u003cli\u003e独立于语言、自解释、易于理解书写\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"javascript与json\"\u003eJavaScript与JSON\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ccode\u003eJSON.parse()\u003c/code\u003e将json格式的字符串转为JSON对象\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003eJSON.stringify()\u003c/code\u003e将JSON对象转为json字符串\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"java与json\"\u003eJava与JSON\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003eJava 的JSON工具包：FastJson、Jackson、Gson、Json-lib\u003c/li\u003e\n\u003cli\u003efastjson是阿里巴巴的开源JSON解析库，它可以解析JSON格式的字符串，支持将Java Bean序列化为JSON字符串，也可以从JSON字符串反序列化到JavaBean。\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003eJSON.toJSONString(Object object)\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003eJSON.parseObject(String text, Class\u0026lt;T\u0026gt; clazz)\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003eJSON.parseArray(String text, Class\u0026lt;T\u0026gt; clazz)\u003c/code\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch1 id=\"jqueryajax\"\u003ejQuery\u0026amp;Ajax\u003c/h1\u003e\n\u003ch2 id=\"jquery使用\"\u003ejQuery使用\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003ejQuery是一个轻量级的js库，使用简便\u003c/li\u003e\n\u003cli\u003ejQuery的核心是选择器，用于获取页面的元素\u003c/li\u003e\n\u003cli\u003ejQuery\u003ca href=\"https://jquery.com/\" target=\"_blank\" rel=\"noopener\"\u003e官网\u003c/a\u003e，采用独立JS文件发布\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"jquery选择器\"\u003ejQuery选择器\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003ejQuery选择器用于选中需要操作的页面元素：\u003ccode\u003e$(expression)\u003c/code\u003e\u003c/p\u003e","title":"Java Web 进阶"},{"content":"XML入门 XML的作用 XML（EXtensible Makup Language）是可扩展标记语言 编写XML就是编写标签，XML没有预定义标签 XML重在保存和传输数据 良好的可读性 XML用途 用于Java程序的配置文件（web.xml） 用于保存、传输程序产生的数据（EJB调用、WebService的soap协议） XML的语法规则 XML第一行必须是XML声明；XML声明说明XML文档的基本信息，包括版本号与字符集。\n\u0026lt;?xml version=\u0026quot;1.0\u0026quot; encoding=\u0026quot;UTF-8\u0026quot;?\u0026gt;\nXML只有一个根节点\nXML书写规则 合法的标签名\n适当的注释与缩进可以让XML文档易于阅读理解\n合理使用属性\n处理特殊字符：\n实体引用：\n实体引用 对应符号 说明 \u0026amp;lt; \u0026lt; 小于 \u0026amp;gt; \u0026gt; 大于 \u0026amp;amp; \u0026amp; 和号 \u0026amp;apos; '' 单引号 \u0026amp;quot; \u0026quot;\u0026quot; 双引号 使用CDATA标签：CDATA指的是不应该由XML解析器进行解析的文本数据\n\u0026lt;![CDATA[1+1\u0026lt;3]]\u0026gt;\n有序的子元素\nJava解析XML DOM文档对象模型：定义了访问和操作XML文档的标准方法 Dom4j是一个易用的，开源的库，用于解析XML。 Dom4j将XML视为一个Document对象 XML标签被Dom4j定义为Element对象 XML语义约束 XML文档结构正确，但可能不是有效的 XML语义约束的两种方式：DTD、Schema DTD DTD（Document Type Definition），是一种简单易用的语义约束方式。 DTD文件的扩展名为.dtd 在XML中使用\u0026lt;!DOCTYPE\u0026gt;标签来引用DTD 利用DTD中的\u0026lt;!ELEMENT\u0026gt;标签，定义XML中运行出现的节点及数量 XML Schema XML Schema提供了数据类型，格式限定，数据范围等特性 XML Schema是W3C标准 XPath路径表达式 XPath路径表达式是XML文档中查找数据的语言\n最常用的基本表达式\n表达式 描述 nodename 选取此节点的所有子节点 / 从根节点选取 // 从匹配的当前节点选择文档中的节点，而不考虑它们的位置 . 选取当前节点 .. 选取当前节点的父节点 @ 选取属性 XPath谓语表达式\nJaxen：Jaxen的是一个Java编写的开源的XPath库。\n适应多种不同的对象模型，包括DOM，XOM，dom4j和JDOM。\nServlet入门 J2EE J2EE（Java 2 Platform Enterprise Edition）是指Java 2 企业版 J2EE是一组技术规范与指南，具体实现由软件厂商实现 开发Web应用程序是J2EE最核心功能 J2EE是由13个功能模块组成的 Servlet、JSP、JDBC、XML、EJB、RMI、JNDI、JMS、JTA、JavaMail、JAF、CORBA、JTS Tomcat Tomcat是J2EE Web标准实现 Servlet 开发技巧 Servlet（Server Applet）服务器小程序，主要用于生成动态Web内容\n标准Java WEB工程目录\nServlet开发步骤 创建类，继承自javax.servlet.http.HttpServlet 在service方法中处理事件 在web.xml中配置servlet访问路径和servlet实现类的映射 Servlet执行原理 Servlet生命周期 装载\u0026ndash;扫描web.xml和@Servlet注解 执行构造函数创建实例 执行初始化方法init()进行初始化 通过service()方法提供服务 Servlet销毁时执行destroy()方法 使用注解简化Servlet配置 在Servlet3.x之后引用了注解特性 Servlet核心注解：@Servlet 启动时加载Servlet 在web.xml中使用\u0026lt;load-on-startup\u0026gt; 启动时加载在工作中常用于系统预处理 Servlet核心对象 JSP入门 JSP用途 Servlet的缺点 静态的HTML和和Java代码柔和在一起，难以维护 Servlet利用out.pringln()输出HTML代码，效率低下 JSP全称是：Java Server Page，本质就是Servlert JSP执行原理 JSP的执行过程 浏览器请求 Tomcat找到对应的JSP（index.jsp），将其转译为Servlet ( index_jsp.java) Servlet编译成Java字节码 (index_jsp.class) JSP基本语法规则 JSP代码块 用于在JSP中嵌入Java代码\nJSP代码块的语法：\u0026lt;% code %\u0026gt;\nJSP声明构造块 JSP声明构造块用于定义声明方法和变量\nJSP声明语法块语法：\u0026lt;%! code %\u0026gt;\nJSP输出指令 JSP输出指令用于在JSP页面中显示Java代码执行的结果\nJSP输出指令语法：\u0026lt;%= code %\u0026gt;\nJSP处理指令 JSP处理指令用于提供JSP执行过程中的辅助信息\n指令 描述 \u0026lt;%@page%\u0026gt; 定义当前JSP页面的全局设置 \u0026lt;%@include%\u0026gt; 将其他JSP页面与当前JSP页面合并 \u0026lt;%@taglib%\u0026gt; 引入JSP标签库 JSP中的注释 JSP注释：\u0026lt;%-- statment --%\u0026gt; Java注释：// /* */ HTML注释：\u0026lt;!-- --\u0026gt; JavaWeb核心特性 Cookies Session ServletContext Tomcat乱码问题 Tomcat默认使用ISO-8859-1编码规则 Tomcat的server.xml中配置URLEncoding=\u0026quot;UTF-8\u0026quot; JSP九大内置对象 JSTL和EL表达式 EL表达式 Expression Language，用于简化JSP的输出\nEL表达式的基本语法：${expression}\n作用域对象，忽略作用域对象时，EL表达式默认会按作用域范围从小到大依次尝试获取\nEL表达式输出：${[scope.]attr[.subAttr]}\nEL表达式支持运算\nJSTL常用标签 JSP Standard Tag Library，JSP标准标签库\nJSTL由Sun定义规范，由Apache Tomcat进行实现\nJSTL下载地址：https://tomcat.apache.org/download-taglibs.cgi\nJSTL按功能划分为五类标签库\n核心标签库Core是JSTL标签库中最重要的标签库，提供了JSTL的基础功能\n\u0026lt;%@ taglib prefix=\u0026quot;c\u0026quot; uri=\u0026quot;http://java.sun.com/jsp/jstl/core\u0026quot; %\u0026gt;：taglibs-standard-impl-1.2.5.jar!\\META-INF\\c.tld\n\u0026lt;c:if\u0026gt;单分支判断 \u0026lt;c:when\u0026gt; \u0026lt;c:choose\u0026gt; \u0026lt;c:otherwise\u0026gt;多分支判断 \u0026lt;c:forEach\u0026gt;遍历集合中的每一个对象 格式化标签库\n\u0026lt;%@ taglib prefix=\u0026quot;fmt\u0026quot; uri=\u0026quot;http://java.sun.com/jsp/jstl/fmt\u0026quot; %\u0026gt;\n\u0026lt;fmt:formatDate\u0026gt; \u0026lt;fmt:formatNumber\u0026gt; ","permalink":"https://2d992714.xianglin2020-github-io.pages.dev/posts/2020/javaweb-basics/","summary":"\u003ch1 id=\"xml入门\"\u003eXML入门\u003c/h1\u003e\n\u003ch2 id=\"xml的作用\"\u003eXML的作用\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003eXML（EXtensible Makup Language）是可扩展标记语言\u003c/li\u003e\n\u003cli\u003e编写XML就是编写标签，XML没有预定义标签\u003c/li\u003e\n\u003cli\u003eXML重在保存和传输数据\u003c/li\u003e\n\u003cli\u003e良好的可读性\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"xml用途\"\u003eXML用途\u003c/h3\u003e\n\u003col\u003e\n\u003cli\u003e用于Java程序的配置文件（\u003ccode\u003eweb.xml\u003c/code\u003e）\u003c/li\u003e\n\u003cli\u003e用于保存、传输程序产生的数据（EJB调用、WebService的soap协议）\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"xml的语法规则\"\u003eXML的语法规则\u003c/h2\u003e\n\u003col\u003e\n\u003cli\u003e\n\u003cp\u003eXML第一行必须是XML声明；XML声明说明XML文档的基本信息，包括版本号与字符集。\u003c/p\u003e","title":"Java Web 基础"},{"content":"Java 面向对象 对象 在 Java 中，万物皆对象，只要是现实存在的，都可以是对象。\n面向对象 类 类是模型，确定对象将会有的属性（特征）和方法（行为）\n对象是类的实例体现。\n定义一个类Cat描述猫\npublic class Cat { // 属性：昵称、年龄、体重、品种 /** * String 类型的默认值是 null * 昵称 */ String name; /** * int 类型的默认值是 0 * 年龄 */ int month; /** * double 类型的默认值是 0.0 * 体重 */ double weight; /** * 品种 */ String species; // 方法：跑动、吃东西 /** * 跑动 */ public void run() { System.out.println(\u0026#34;小猫快跑！\u0026#34;); } public void run(String name) { System.out.println(name + \u0026#34;快跑！\u0026#34;); } /** * 吃东西 */ public void eat() { System.out.println(\u0026#34;小猫吃鱼！\u0026#34;); } } 对象实例化 测试Cat对象的方法和属性\npublic class CatTest { public static void main(String[] args) { // 对象实例化 Cat one = new Cat(); // 测试方法 one.eat(); one.run(); // 为属性赋值 one.name = \u0026#34;花花\u0026#34;; one.month = 2; one.weight = 1000; one.species = \u0026#34;英国短毛猫\u0026#34;; // 输出属性 System.out.println(\u0026#34;年龄：\u0026#34; + one.month); System.out.println(\u0026#34;昵称：\u0026#34; + one.name); System.out.println(\u0026#34;体重：\u0026#34; + one.weight); System.out.println(\u0026#34;品种：\u0026#34; + one.species); one.run(one.name); } } 对象实例化步骤 声明对象 Cat cat 实例化对象 new Cat() 构造方法 构造方法也称为构造器\n构造方法与类名相同且没有返回值 构造方法只能在对象实例化的时候调用\n当没有指定构造方法时，编译器会自动添加无参的构造方法。 This 关键字 this 是 Java中常用的关键字，代表当前对象本身，可以理解为：指向当前对象的一个引用。\nthis 在Java中可用于调用成员属性、成员方法、构造方法，也可以当作参数进行方法传参以及方法返回值。\n可以在构造方法中，通过this调用本类中的另一个构造方法，但是，调用动作必须置于方法中的第一行。 可以通过this调用本类中带参或者无参构造方法，调用带参构造方法时，需要按顺序传入设定的参数。 面向对象设计原则 单一职责原则 单一职责原则（SRP：Single responsibility principle）又称单一功能原则。它规定一个类应该只有一个发生变化的原因。所谓职责是指类变化的原因，如果一个类有多余一个的动机被改变，name 这个类就具有多余一个职责。\n单一职责原则表明，一个类承担的职责越多，它被复用的可能性及越小，而且一个类承担的职责过多，就相当于将这些职责耦合在一起，当其中一个职责变化时，可能会影响到其它职责的运作，因此要将这些职责进行分离，将不同职责封装在不同的类中，即将不同的变化原因封装在不同的类中，如果多个职责总是同时发生改变则可将它们封装在同一类中。\nJava 封装 将类的某些信息在类内部，不允许外部程序访问 通过该类提供的方法，访问隐藏的属性 封装类（对象）的属性，隐藏实现的细节，方便修改和维护 提供改变的接口 Java 封装的实现 修改属性的可见性，设为private 创建getter、setter方法，设为public用于属性的读写 在方法方法中对属性进行操作 使用包进行类管理 package语句必须放在 Java 源文件中的第一行，一个 Java 源文件只能有一条package语句。\nimport语句只能加载包路径下直接访问的类\n使用package定义包\npackage person.xianglin.pkg; 使用import引入类\n使用import引入包下所有类 java import person.xianglin.pkg; 使用import引入包下特定类\nimport person.xianglin.pkg.Cat; Static关键字 static是 Java 中常用的关键字，代表“全局”或者“静态”的意思。通常用于修饰成员变量和方法，页可以用于静态代码块。 静态成员是属于整个类的，由类进行维护，仅在类初次加载时会被初始化，在类销毁时回收。 通过该类实例化的所有对象都共享类中静态资源，任一对象中信息的修改都将影响所有对象。 Java 继承 面向对象的编程思想来源为生活\n继承 描述一种类与类之间的关系 使用已存在的类定义作为基础建立新类 新类的定义可以增加新的数据或新的功能，也可以使用父类的功能，但不能选择性地继承父类 满足A is a B的关系就可以形成继承关系 Java 使用extends实现继承关系 子类可以获取父类非私有的成员 方法重写 在子类中定义，方法名、参数列表与父类中方法一致 方法重载： 在同一个类中 方法名相同，参数列表不同（参数顺序、个数、类型） 方法返回值、访问修饰符任意 方法重写： 在有继承关系的子类中 方法名相同，参数列表相同（参数顺序、个数、类型） 与方法的参数名无关 方法返回值小于等于父类方法返回值 方法的访问修饰符大于等于父类的访问修饰符 访问修饰符 共有的：public，允许在任意位置访问 私有的：private，只能在本类中访问 保护的：protected，能在当前类、相同包中的类及子类中访问 默认：允许在相同包中被访问 访问修饰符 本类 同包 子类 其他 public √ √ √ √ protected √ √ √ default √ √ private √ 继承的初始化顺序 public class BaseClass { static { System.out.println(\u0026#34;父类的静态代码块\u0026#34;); } { System.out.println(\u0026#34;父类的构造代码块\u0026#34;); } public BaseClass() { System.out.println(\u0026#34;父类的构造方法\u0026#34;); } } public class SubClass extends BaseClass { static { System.out.println(\u0026#34;子类的静态代码块\u0026#34;); } { System.out.println(\u0026#34;子类的构造代码块\u0026#34;); } public SubClass() { System.out.println(\u0026#34;子类的构造方法\u0026#34;); } } final关键字 修饰类：表示类不能被继承 修饰方法：表示方法不能被重写 修饰变量：对于基本数据类表示变量的值不能改变，对于引用数据类型表示引用指向的地址不能改变 修饰局部变量：局部变量在使用前赋值即可 修饰成员变量：定义时初始化、构造块初始化、构造方法里初始化 修饰静态变量：定义时初始化，静态代码块里初始化 注解简介 JDK1.5引入注解 用来对元素进行说明、注释 Java 多态 多态实现形式 编译时多态：方法重载 运行时多态：方法重写 实现多态的条件 满足继承关系 父类引用指向子类应用 转型 向上转型：隐式转型、自动转型，父类引用指向子类对象 向下转型：强制转型：子类引用指向父类对象 抽象类 为子类提供一个公共的引用类型 封装子类中的重复内容 父类需要限定子类应该包含的方法，但无法准确知道方法的实现。 抽象方法必须包含在抽象类中，抽象类中可以没有抽象方法 抽象方法使用abstract修饰，不允许有方法体 接口 interface Java 只支持单继承 使用interface定义接口 使用implements实现接口 接口的访问修饰符：public或者 default 接口方法的修饰语默认是：public abstract 接口中可以定义常量，常量的修饰语是：public static final JDK1.8 后接口方法可以用default修饰，提供默认实现；可以创建静态方法 当类实现接口时，需要实现接口所有方法，或者将类修饰为抽象类 内部类 Java 中可以将一个类定义在另一个类的内部或方法的内部，这样的类称为内部类；包含内部类的类称为外部类。 内部类能将信息封装在类的内部 成员内部类 内部类中最常见的就是成员内部类，也称为普通内部类\n获取内部类\n方式一：new outerClass().new innerClass(); 方式二：使用外部类对象获取内部类：outer.new innerClass() 方式三：获取内部类对象实例 public InnerClass getInner() { return new InnerClass(); } 内部类可以直接访问外部类的成员属性和方法，如果重名，优先访问内部类属性\n可以使用 OutClass.this.field 访问外部类的信息\n成员内部类无法定义静态变量、静态方法。外部类访问内部类属性需要通过内部类实例对象访问\n静态内部类 静态内部类创建可以不依赖外部类 获取静态内部类实例：Outer.Inner myHeart = new Outer.Inner(); 静态内部类实例化时，不依赖外部类 可以通过 Outer.Inner.method 访问内部类的静态资源 方法内部类 定义在方法中的类，也称为局部内部类 方法内部类的成员只能在方法内部使用 方法前面不能加访问修饰符和 static 类中不能包含静态成员 方法内部类可以定义为抽象类，类中可以定义抽象方法 匿名内部类 将类的定义与对象实例创建一起完成 匿名内部类没有类型名称、实例对象名称 编译后的文件命名：外部类$数字.class 无法使用访问修饰符 无法编写构造方法 面试题 面向对象的三大特征 接口和抽象类 静态变量与实例变量 使用static关键字定义静态变量 静态变量属于类，不需要依附于实例 静态变量在类加载时初始化与 JVM 的方法区，实例变量在对象创建时初始化与堆内存中 代码块的执行顺序 静态块优先 父类优先 构造块由于构造函数 public class OrderDemo { public static void main(String[] args) { new SubClass(); } } class Parent { Parent() { System.out.println(\u0026#34;父类构造器\u0026#34;); } { System.out.println(\u0026#34;父类构造块\u0026#34;); } static { System.out.println(\u0026#34;父类静态块\u0026#34;); } } class SubClass extends Parent { SubClass() { System.out.println(\u0026#34;子类构造器\u0026#34;); } { System.out.println(\u0026#34;子类构造块\u0026#34;); } static { System.out.println(\u0026#34;子类静态块2\u0026#34;); } static { System.out.println(\u0026#34;子类静态块1\u0026#34;); } } /* 父类静态块 子类静态块2 子类静态块1 父类构造块 父类构造器 子类构造块 子类构造器 */ Java 的异常体系 Exception 和 Error 的区别 字符串与常量池 字符串不可变与字符串引用不可变是两个概念 可以通过反射改变value[]的值，不过没有实际意义 JDK9 开始，存储字符串的数组由private final char[] value 变为private final byte[] value import java.lang.reflect.Field; public class StringDemo { public static void main(String[] args) throws Exception { String str = \u0026#34;abcdefg\u0026#34;; System.out.println(str); Field valueField = String.class.getDeclaredField(\u0026#34;value\u0026#34;); valueField.setAccessible(true); // java9 byte[] obj = (byte[]) valueField.get(str); obj[0] = (byte) (obj[0] - 32); System.out.println(str); } } // abcdefg // Abcdefg String、StringBuilder、StringBuffer String 不可变，使用final byte[] value存储字符 StringBuilder、StringBuffer继承自AbstractStringBuilder，字符串存储于父类的byte[] value StringBuffer主要方法均使用synchronized保证线程安全 AbstractStringBuilder并没有实现equals方法 Java IO 从装饰者模式理解 Java IO 文件拷贝 import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; public class FileCopyDemo { public static void main(String[] args) { copyFile(\u0026#34;/Users/xianglin/Documents/IdeaProjects/share-mybatis-plugin.zip\u0026#34;, \u0026#34;/Users/xianglin/Documents/IdeaProjects/share-mybatis-plugin\u0026#34;); } public static void copyFile(String srcFilePath, String descDirPath) { File srcFile = new File(srcFilePath); File descDir = new File(descDirPath); if (!srcFile.exists() || !srcFile.isFile()) { return; } if (descDir.isFile()) { return; } if (!descDir.exists()) { if (descDir.mkdirs()) { return; } } int index = srcFilePath.lastIndexOf(\u0026#34;/\u0026#34;); String fileName = srcFilePath.substring(index); try (FileInputStream inputStream = new FileInputStream(srcFile); FileOutputStream outputStream = new FileOutputStream(new File(descDir, fileName))) { byte[] bytes = new byte[1024]; int length; while ((length = inputStream.read(bytes)) != -1) { outputStream.write(bytes, 0, length); } } catch (Exception exception) { exception.printStackTrace(); } } } ","permalink":"https://2d992714.xianglin2020-github-io.pages.dev/posts/2020/java-oop-basics/","summary":"\u003ch1 id=\"java-面向对象\"\u003eJava 面向对象\u003c/h1\u003e\n\u003ch2 id=\"对象\"\u003e对象\u003c/h2\u003e\n\u003cp\u003e在 Java 中，万物皆对象，只要是现实存在的，都可以是对象。\u003c/p\u003e\n\u003ch2 id=\"面向对象\"\u003e面向对象\u003c/h2\u003e\n\u003ch2 id=\"类\"\u003e类\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e类是模型，确定对象将会有的属性（特征）和方法（行为）\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e对象是类的实例体现。\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e定义一个类\u003ccode\u003eCat\u003c/code\u003e描述猫\u003c/p\u003e","title":"java 面向对象基础"},{"content":"Java 流程控制 三大流程控制语句：顺序、选择、循环\n选择结构 if 结构 判断条件是布尔类型，且是一个范围 Scanner scanner = new Scanner(System.in); int score = scanner.nextInt(); if (score \u0026gt;= 90) { System.out.println(\u0026#34;优\u0026#34;); } if (score \u0026gt;= 80 \u0026amp;\u0026amp; score \u0026lt; 90) { System.out.println(\u0026#34;良\u0026#34;); } if (score \u0026gt;= 60 \u0026amp;\u0026amp; score \u0026lt; 80) { System.out.println(\u0026#34;中\u0026#34;); } if (score \u0026lt; 60) { System.out.println(\u0026#34;不及格\u0026#34;); } 多重 if if (score \u0026gt;= 90) { System.out.println(\u0026#34;优\u0026#34;); } else if (score \u0026gt;= 80) { System.out.println(\u0026#34;良\u0026#34;); } else if (score \u0026gt;= 60) { System.out.println(\u0026#34;中\u0026#34;); } else { System.out.println(\u0026#34;不及格\u0026#34;); } 嵌套 if 将整个 if 块嵌套到另一个 if 块中 public static void main(String[] args) { // 定义两个整型变量并初始化 int x, y; Scanner scanner = new Scanner(System.in); x = scanner.nextInt(); y = scanner.nextInt(); // 判断 x 和 y 是否相等 if (x != y) { if (x \u0026gt; y) { System.out.println(\u0026#34;x 大于 y!\u0026#34;); } else { System.out.println(\u0026#34;x 小于 y！\u0026#34;); } } else { System.out.println(\u0026#34;x 和 y 相等！\u0026#34;); } } switch结构 判断条件是一个常量\nswitch结构\nswitch(condition){ case 常量表达式 1: 语句 1;break; case 常量表达式 2: 语句 2;break; default: 语句 3; } JDK7.0以后表达式结果可以是 byte、short、int、char 以及 String\n循环结构 while while(condition){ statement; } do-while do-while循环最少执行一次 循环条件后的分号;不能丢 for public static void main(String[] args) { int sum = 0; for (int i = 1; i \u0026lt;= 5; i++) { sum += i; } System.out.println(\u0026#34;1-5 的累加和是：\u0026#34; + sum); } 三种循环结构的应用场景 从for循环的结构看，三个表达式会依次被执行到，执行的顺序也是固定的，所以for循环适用于循环次数固定的场景 while 循环，只有一个判断条件，结果为布尔值，如果为true就执行循环，为false就不执行。所以while循环适用于不知道循环次数，只知道循环达到某个条件可以执行时使用。 在循环内，一般需要对循环变量进行改变，否则会发生死循环。 do-while循环，与while循环类似，只是判断条件放到了循环最后，不管是否满足条件，循环都会被执行一次。因此，do-while循环适用于不知道循环具体执行次数，只知道满足某个条件继续执行或结束执行，并且循环肯定执行一次时使用。 ","permalink":"https://2d992714.xianglin2020-github-io.pages.dev/posts/2020/java-control-flow/","summary":"\u003ch1 id=\"java-流程控制\"\u003eJava 流程控制\u003c/h1\u003e\n\u003cp\u003e三大流程控制语句：顺序、选择、循环\u003c/p\u003e\n\u003ch2 id=\"选择结构\"\u003e选择结构\u003c/h2\u003e\n\u003ch3 id=\"if-结构\"\u003e\u003ccode\u003eif\u003c/code\u003e 结构\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e判断条件是布尔类型，且是一个范围\u003c/li\u003e\n\u003c/ul\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-java\" data-lang=\"java\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003eScanner\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003escanner\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"k\"\u003enew\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003eScanner\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eSystem\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003ein\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kt\"\u003eint\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003escore\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003escanner\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003enextInt\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eif\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003escore\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026gt;=\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003e90\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"n\"\u003eSystem\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003eout\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003eprintln\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;优\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eif\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003escore\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026gt;=\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003e80\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026amp;\u0026amp;\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003escore\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003e90\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"n\"\u003eSystem\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003eout\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003eprintln\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;良\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eif\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003escore\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026gt;=\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003e60\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026amp;\u0026amp;\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003escore\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003e80\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"n\"\u003eSystem\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003eout\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003eprintln\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;中\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eif\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003escore\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003e60\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"n\"\u003eSystem\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003eout\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003eprintln\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;不及格\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch4 id=\"多重-if\"\u003e多重 \u003ccode\u003eif\u003c/code\u003e\u003c/h4\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-java\" data-lang=\"java\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eif\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003escore\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026gt;=\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003e90\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"n\"\u003eSystem\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003eout\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003eprintln\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;优\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"k\"\u003eelse\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"k\"\u003eif\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003escore\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026gt;=\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003e80\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"n\"\u003eSystem\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003eout\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003eprintln\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;良\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"k\"\u003eelse\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"k\"\u003eif\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003escore\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026gt;=\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003e60\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"n\"\u003eSystem\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003eout\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003eprintln\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;中\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"k\"\u003eelse\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"n\"\u003eSystem\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003eout\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003eprintln\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;不及格\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch4 id=\"嵌套-if\"\u003e嵌套 \u003ccode\u003eif\u003c/code\u003e\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e将整个 if 块嵌套到另一个 if 块中\u003c/li\u003e\n\u003c/ul\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-java\" data-lang=\"java\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"kd\"\u003estatic\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"kt\"\u003evoid\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"nf\"\u003emain\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eString\u003c/span\u003e\u003cspan class=\"o\"\u003e[]\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003eargs\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"c1\"\u003e// 定义两个整型变量并初始化\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"kt\"\u003eint\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003ex\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003ey\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"n\"\u003eScanner\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003escanner\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"k\"\u003enew\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003eScanner\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eSystem\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003ein\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"n\"\u003ex\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003escanner\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003enextInt\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"n\"\u003ey\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003escanner\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003enextInt\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"c1\"\u003e// 判断 x 和 y 是否相等\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"k\"\u003eif\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ex\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"o\"\u003e!=\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003ey\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e        \u003c/span\u003e\u003cspan class=\"k\"\u003eif\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ex\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026gt;\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003ey\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e            \u003c/span\u003e\u003cspan class=\"n\"\u003eSystem\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003eout\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003eprintln\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;x 大于 y!\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e        \u003c/span\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"k\"\u003eelse\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e            \u003c/span\u003e\u003cspan class=\"n\"\u003eSystem\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003eout\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003eprintln\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;x 小于 y！\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e        \u003c/span\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"k\"\u003eelse\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e        \u003c/span\u003e\u003cspan class=\"n\"\u003eSystem\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003eout\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003eprintln\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;x 和 y 相等！\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch3 id=\"switch结构\"\u003e\u003ccode\u003eswitch\u003c/code\u003e结构\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e判断条件是一个常量\u003c/p\u003e","title":"java 控制流程"},{"content":"Java 运算符 表达式 表达式由运算符和操作数组成 运算符 算术运算符 算术运算符主要用于进行基本的算术运算，包括加减乘除\n算术运算符 名称 举例 + 加法 5+10=15 - 减法 10-5=5 * 乘法 3*6=18 / 除法 10/2=5 % 取余数 3%2=1 ++ 自增1 int n;n++; \u0026ndash; 自减1 int n=4;\u0026ndash;n; 自增自减运算符\n表达式 运算顺序 结果 num2=++num1; num1=num1+1;num2=num1; num1=2;num2=2; num2=num1++; num2=num1;num1=num1+1; num1=2;num2=1; num2=\u0026ndash;num1; num1=num1-1;num2=num1; num1=0;num2=0; num2=num1\u0026ndash;; num2=num1;num1=num1-1; num2=1;num1=0; 赋值运算符 格式：变量=表达式;\n赋值运算符是从有往左运算！\n赋值运算符的左边不能为常量\n复合赋值运算符\n运算符 表达式 计算 结果（假设 x=15） += x+=5 x=x+5 20 -= x-=5 x=x-5 10 *= x*=5 x=x*5 75 /= x/=5 x=x/5 3 %= x%=5 x=x%5 0 关系运算符 用于判断两个数据的大小\n比较的结果是一个布尔值\n运算符 名称 \u0026gt; 大于 \u0026lt; 小于 \u0026gt;= 大于等于 \u0026lt;= 小于等于 == 等于 != 不等于 关于运算符主要用于条件结构或循环结构中\n逻辑运算符 用于连接一个或多个条件，判断这些条件是否成立\n逻辑运算符的结果是布尔值\n名称 运算符 表达式 与 \u0026amp;\u0026amp;或\u0026amp; opt1\u0026amp;\u0026amp;opt2 或 ||或| opt1||opt2 非 ！ !opt \u0026amp;\u0026amp;和\u0026amp;都可以表示逻辑与，但他们是有区别的，共同点是他们两边的条件都成立的时候最终结果才是true；不同点是\u0026amp;\u0026amp; 只要是第一个条件不成立为false，就不会再去判断第二个条件，最终结果直接为false，而\u0026amp;判断的是所有的条件；\n||和|都表示逻辑或，共同点是只要两个判断条件其中有一个成立最终的结果就是true，区别是||只要满足第一个条件，后面的条件就不再判断，而 |要对所有的条件进行判断。\n条件运算符 简单 if 语句的格式：\nif(condition){ code } if-else语句的形式\nif(true){ code }else{ anothor code } Java中的条件运算符是三目运算符。condition?expression1:expression2\n位运算符 运算符的优先级 运算符 描述 () 圆括号 !、++、-- 逻辑非、自增、自减 *、/、% 乘、除、取余 +、- 加、减 \u0026lt;、\u0026lt;=、\u0026gt;、\u0026gt;= 小于、小于等于、大于、大于等于 ==、!= 等于、不等于 \u0026amp;\u0026amp; 逻辑与 ` =、+=、-=、*=、%=、/= 赋值运算符、符合赋值运算符 ","permalink":"https://2d992714.xianglin2020-github-io.pages.dev/posts/2020/java-operator/","summary":"\u003ch1 id=\"java-运算符\"\u003eJava 运算符\u003c/h1\u003e\n\u003ch2 id=\"表达式\"\u003e表达式\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e表达式由运算符和操作数组成\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"运算符\"\u003e运算符\u003c/h2\u003e\n\u003ch3 id=\"算术运算符\"\u003e算术运算符\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e算术运算符主要用于进行基本的算术运算，包括加减乘除\u003c/p\u003e\n\u003ctable\u003e\n\t\u003cthead\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003cth style=\"text-align: center\"\u003e算术运算符\u003c/th\u003e\n\t\t\t\t\t\u003cth style=\"text-align: center\"\u003e名称\u003c/th\u003e\n\t\t\t\t\t\u003cth style=\"text-align: left\"\u003e举例\u003c/th\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/thead\u003e\n\t\u003ctbody\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd style=\"text-align: center\"\u003e+\u003c/td\u003e\n\t\t\t\t\t\u003ctd style=\"text-align: center\"\u003e加法\u003c/td\u003e\n\t\t\t\t\t\u003ctd style=\"text-align: left\"\u003e5+10=15\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd style=\"text-align: center\"\u003e-\u003c/td\u003e\n\t\t\t\t\t\u003ctd style=\"text-align: center\"\u003e减法\u003c/td\u003e\n\t\t\t\t\t\u003ctd style=\"text-align: left\"\u003e10-5=5\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd style=\"text-align: center\"\u003e*\u003c/td\u003e\n\t\t\t\t\t\u003ctd style=\"text-align: center\"\u003e乘法\u003c/td\u003e\n\t\t\t\t\t\u003ctd style=\"text-align: left\"\u003e3*6=18\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd style=\"text-align: center\"\u003e/\u003c/td\u003e\n\t\t\t\t\t\u003ctd style=\"text-align: center\"\u003e除法\u003c/td\u003e\n\t\t\t\t\t\u003ctd style=\"text-align: left\"\u003e10/2=5\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd style=\"text-align: center\"\u003e%\u003c/td\u003e\n\t\t\t\t\t\u003ctd style=\"text-align: center\"\u003e取余数\u003c/td\u003e\n\t\t\t\t\t\u003ctd style=\"text-align: left\"\u003e3%2=1\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd style=\"text-align: center\"\u003e++\u003c/td\u003e\n\t\t\t\t\t\u003ctd style=\"text-align: center\"\u003e自增1\u003c/td\u003e\n\t\t\t\t\t\u003ctd style=\"text-align: left\"\u003eint n;n++;\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd style=\"text-align: center\"\u003e\u0026ndash;\u003c/td\u003e\n\t\t\t\t\t\u003ctd style=\"text-align: center\"\u003e自减1\u003c/td\u003e\n\t\t\t\t\t\u003ctd style=\"text-align: left\"\u003eint n=4;\u0026ndash;n;\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/tbody\u003e\n\u003c/table\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e自增自减运算符\u003c/p\u003e","title":"java 运算符"},{"content":"Java 常量与变量 标识符 标识符可以由字母、数字、下划线_和美元符$组成，且不能以数字开头 标识符完全区分大小写，即void是关键字，Void是合法的标识符 标识符不能是 Java 关键字和保留字 标识符命名最好能见名知意 关键字 goto、const在 Java 中被定义为保留字，暂未使用\n变量 变量是程序中数据的零时存放场所 变量的三元素：变量类型、变量名和变量值 变量的命名规则 满足标识符命名规则 符合驼峰法命名规范 尽量简短、见名知意 类的命名规则 满足 Pascal 命名规范 数据类型 数据类型介绍 整型的应用场景\n整型主要用来存储整数，如：1，2，3等等。 按照存储数据的范围不同分为四种，分别是：byte、short、int、long，数据表示范围依次增大。 byte类型主要对二进制数据进行存储，在文件输入输出流部分将会使用，用于二进制数据的传输。 short类型在有的需求中会用到，比如：对于登录账号的状态表示，一般用0表示正常，1表示冻结，2表示已删除。这里的数值比较固定，值也比较小，可以使用short类型表示。 浮点型的应用场景\n浮点类型表示小数，包括float和double两种类型，具体使用哪种数据类型，也是可以按照表示范围进行选择。但double类型使用相对较多。 浮点型因为其表示方式的原因，会产生精度丢失的问题，所以会结合BigDecimal类进行使用 基本数据类型 数据类型 说明 字节数 范围 byte 字节型 1 -128~127 short 短整型 2 -32768~32767 int 整型 4 -2^31~2^31-1 long 长整型 8 -2^63~2^63 float 单精度浮点型 4 -3.40E+38~3.40E+38 double 双精度浮点型 8 -1.79E+308~1.79E+308 char 字符型 2 0~65535 boolean 布尔型 1 true/false 进制表示 八进制：以 0 开头，包括 0-7的数字 十六进制：以 0x或 0X 开头，包括数字 0-9，以及字母a-f 或 A-F 字面值 整形字面值 Java 中有三种表示整数的方式：十进制、八进制、十六进制 浮点型字面值 浮点值字面值默认表示double 类型，也可以在值后面加 d 或者 D 如需表示float 类型，需要在字面值后面加 f 或 F 字符型字面值 字符型字面值用单引号内的单个字符表示 布尔字面值 布尔值只能定义为 true false 字符串字面值 字符串不属于基本数据类型 双引号引起来的 0 个或多个字符 变量声明 格式：变量类型 变量名; int n;声明整形变量 n long count;声明长整型变量 count 赋值 使用=运算符进行赋值 将=右边的值赋值给左边的变量 int n;n=3;定义 int 型变量 n，将 3 赋值给变量 n 变量定义 可以在变量声明时给变量赋值：int n = 3; 类型转换 类型转换分为自动类型转换和强制类型转换：char ch = (char)65536; 自动类型转换也叫做隐式类型转换 常量 final int n = 5; ASCII 码 ASCII（American Standard Code for Information Interchange，美国标准信息交换代码） 基于拉丁字母的一套电脑编码系统 主要用于显示现代英语和其他西欧语言 使用 7 位或 8 位二进制组合来表示128 种或 256 种可能的字符 ASCII 码对照表\nBin Dec Hex 缩写/字符 解释 00000000 0 00 NUL(null) 空字符 00000001 1 01 SOH(start of headling) 标题开始 00000010 2 02 STX (start of text) 正文开始 00000011 3 03 ETX (end of text) 正文结束 00000100 4 04 EOT (end of transmission) 传输结束 00000101 5 05 ENQ (enquiry) 请求 00000110 6 06 ACK (acknowledge) 收到通知 00000111 7 07 BEL (bell) 响铃 00001000 8 08 BS (backspace) 退格 00001001 9 09 HT (horizontal tab) 水平制表符 00001010 10 0A LF (NL line feed, new line) 换行键 00001011 11 0B VT (vertical tab) 垂直制表符 00001100 12 0C FF (NP form feed, new page) 换页键 00001101 13 0D CR (carriage return) 回车键 00001110 14 0E SO (shift out) 不用切换 00001111 15 0F SI (shift in) 启用切换 00010000 16 10 DLE (data link escape) 数据链路转义 00010001 17 11 DC1 (device control 1) 设备控制1 00010010 18 12 DC2 (device control 2) 设备控制2 00010011 19 13 DC3 (device control 3) 设备控制3 00010100 20 14 DC4 (device control 4) 设备控制4 00010101 21 15 NAK (negative acknowledge) 拒绝接收 00010110 22 16 SYN (synchronous idle) 同步空闲 00010111 23 17 ETB (end of trans. block) 传输块结束 00011000 24 18 CAN (cancel) 取消 00011001 25 19 EM (end of medium) 介质中断 00011010 26 1A SUB (substitute) 替补 00011011 27 1B ESC (escape) 溢出 00011100 28 1C FS (file separator) 文件分割符 00011101 29 1D GS (group separator) 分组符 00011110 30 1E RS (record separator) 记录分离符 00011111 31 1F US (unit separator) 单元分隔符 00100000 32 20 (space) 空格 00100001 33 21 ! 00100010 34 22 \u0026quot; 00100011 35 23 # 00100100 36 24 $ 00100101 37 25 % 00100110 38 26 \u0026amp; 00100111 39 27 ' 00101000 40 28 ( 00101001 41 29 ) 00101010 42 2A * 00101011 43 2B + 00101100 44 2C , 00101101 45 2D - 00101110 46 2E . 00101111 47 2F / 00110000 48 30 0 00110001 49 31 1 00110010 50 32 2 00110011 51 33 3 00110100 52 34 4 00110101 53 35 5 00110110 54 36 6 00110111 55 37 7 00111000 56 38 8 00111001 57 39 9 00111010 58 3A : 00111011 59 3B ; 00111100 60 3C \u0026lt; 00111101 61 3D = 00111110 62 3E \u0026gt; 00111111 63 3F ? 01000000 64 40 @ 01000001 65 41 A 01000010 66 42 B 01000011 67 43 C 01000100 68 44 D 01000101 69 45 E 01000110 70 46 F 01000111 71 47 G 01001000 72 48 H 01001001 73 49 I 01001010 74 4A J 01001011 75 4B K 01001100 76 4C L 01001101 77 4D M 01001110 78 4E N 01001111 79 4F O 01010000 80 50 P 01010001 81 51 Q 01010010 82 52 R 01010011 83 53 S 01010100 84 54 T 01010101 85 55 U 01010110 86 56 V 01010111 87 57 W 01011000 88 58 X 01011001 89 59 Y 01011010 90 5A Z 01011011 91 5B [ 01011100 92 5C \\ 01011101 93 5D ] 01011110 94 5E ^ 01011111 95 5F _ 01100000 96 60 ` 01100001 97 61 a 01100010 98 62 b 01100011 99 63 c 01100100 100 64 d 01100101 101 65 e 01100110 102 66 f 01100111 103 67 g 01101000 104 68 h 01101001 105 69 i 01101010 106 6A j 01101011 107 6B k 01101100 108 6C l 01101101 109 6D m 01101110 110 6E n 01101111 111 6F o 01110000 112 70 p 01110001 113 71 q 01110010 114 72 r 01110011 115 73 s 01110100 116 74 t 01110101 117 75 u 01110110 118 76 v 01110111 119 77 w 01111000 120 78 x 01111001 121 79 y 01111010 122 7A z 01111011 123 7B { 01111100 124 7C | 01111101 125 7D } 01111110 126 7E ~ 01111111 127 7F DEL (delete) 删除 Unicode 编码 Unicode 编码的目标是支持全世界的字符集 在值前面加\\u：char u = '\\u005d' 转移字符 转移字符 描述 ASCII 值 \\a 响铃 007 \\b 退格 008 \\f 换页 012 \\n 换行 010 \\r 回车 013 \\t 水平制表 009 \\v 垂直制表 011 \\\\ 表示一个反斜杠字符\\ 092 \\' 代表一个单引号字符' 039 \\\u0026quot; 代表一个双引号字符\u0026quot; 034 \\? 代表一个问号? 063 \\0 空字符 000 \\ddd 一到三位八进制数表示的任意字符 \\xhh 十六进制所表示的任意字符 面试题 数字类型 IEEE754 Float 精度问题 public class FloatDemo { public static void main(String[] args) { float f1 = 423432423F; float f2 = f1 + 1; System.out.println(\u0026#34;f1 == f2 :\u0026#34; + (f1 == f2)); } } // f1 == f2 :true 生成随机数 import java.util.Random; public class RandomDemo { public static void main(String[] args) { // 生成 300 - 100 的随机数 int max = 100; int min = 30; // [30,100) int random = new Random().nextInt(max + 1 - min) + min; System.out.println(random); int random2 = (int) (Math.random() * 70 + 30); System.out.println(random2); } } 求 1-1000 的质数 public class PrimeNumber { public static void main(String[] args) { // 打印出 1 - 1000 之间的质数 int round = 1000; for (int i = 2; i \u0026lt;= round; i++) { boolean prime = true; for (int j = 2; j \u0026lt; i; j++) { if (i % j == 0) { prime = false; break; } } if (prime) { System.out.println(i + \u0026#34; 是质数！\u0026#34;); } } } } ","permalink":"https://2d992714.xianglin2020-github-io.pages.dev/posts/2020/java-variables/","summary":"\u003ch1 id=\"java-常量与变量\"\u003eJava 常量与变量\u003c/h1\u003e\n\u003ch2 id=\"标识符\"\u003e标识符\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e标识符可以由字母、数字、下划线\u003ccode\u003e_\u003c/code\u003e和美元符\u003ccode\u003e$\u003c/code\u003e组成，且不能以数字开头\u003c/li\u003e\n\u003cli\u003e标识符完全区分大小写，即\u003ccode\u003evoid\u003c/code\u003e是关键字，\u003ccode\u003eVoid\u003c/code\u003e是合法的标识符\u003c/li\u003e\n\u003cli\u003e标识符不能是 Java 关键字和保留字\u003c/li\u003e\n\u003cli\u003e标识符命名最好能见名知意\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"关键字\"\u003e关键字\u003c/h2\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"https://cdn.jsdelivr.net/gh/xianglin2020/gallery@master/202009/213028.png\" alt=\"img\"  /\u003e\n\u003c/p\u003e","title":"java 变量与常量"},{"content":"Java 初识 Java 简介 Java 是一门面向对象的程序设计语言。 1995 年由 sun 公司发布。 2010 年 sun 公司被 Oracle 收购，Java 现在属于 Oracle 公司。 JVM JVM（Java Virtual Machine），Java 虚拟机的简称。 JVM 是 Java 语言平台无关性的关键。 Java 程序执行的流程 JDK JDK（Java Development Kit），Java 语言的软件开发包。 两个主要的组件： javac，将源程序编译成字节码 java，运行编译后的Java 程序 JRE JRE（Java Runtime Environment） 包括 Java 虚拟机（JVM）、Java 核心类库和支持文件 如果只需要运行 Java 程序，下载并安装 JRE即可 如果要开发 Java 软件，需要下载 JDK 在 JDK 中包含 JRE JDK、JRE 和 JVM 三者的关系 JRE = JVM + JavaSE标准类库 JDK = JRE + 开发工具集 Java 平台 Java SE：Java 标准版，用于开发桌面程序 Java EE：Java 企业版，用于开发 Web 程序（电商网站、门户网站） Java ME：Java 微型版，用于移动设备，现占用率已不高 JDK 安装 Oracle官网下载各个版本的 JDK\nMAC 下使用 HomeBrew 安装各个版本的 JDK\n验证 JDK 安装：java -version\n使用 Eclipse 开发 Java 程序 Eclipse 下载地址\n使用 HomeBrew 安装 Eclipse\n使用 Eclipse 创建 Java 项目\n创建一个名为FirstJavaProject的项目\n创建一个名为person.xianglin的包\n创建第一个名为HelloWorld的 Java 类\n完成编程语言第一壮举：输出Hello World!\n","permalink":"https://2d992714.xianglin2020-github-io.pages.dev/posts/2020/java-beginning/","summary":"\u003ch1 id=\"java-初识\"\u003eJava 初识\u003c/h1\u003e\n\u003ch2 id=\"java-简介\"\u003eJava 简介\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003eJava 是一门面向对象的程序设计语言。\u003c/li\u003e\n\u003cli\u003e1995 年由 sun 公司发布。\u003c/li\u003e\n\u003cli\u003e2010 年 sun 公司被 Oracle 收购，Java 现在属于 Oracle 公司。\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"jvm\"\u003eJVM\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003eJVM（Java Virtual Machine），Java 虚拟机的简称。\u003c/li\u003e\n\u003cli\u003eJVM 是 Java 语言平台无关性的关键。\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"java-程序执行的流程\"\u003eJava 程序执行的流程\u003c/h2\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"https://cdn.jsdelivr.net/gh/xianglin2020/gallery@master/202009/224832.png\" alt=\"image-20200511220044340\"  /\u003e\n\u003c/p\u003e","title":"java 初识"},{"content":"Linux 介绍 GNU 项目 GNU 是GNU is Not Unix 的递归缩写。\nLinus Torvalds 1991 年，Linus Torvalds完成了 Linux 内核的第一个版本。Linux 可以说是 Linus 和 Unix 的合并，也可以说是 Linux is Not Unix 的递归缩写。\n操作系统的核心称为“内核”，但内核并不等于操作系统。内核提供系统服务，比如文件管理、虚拟内存、设备 I/O等。 单独的 Linux 内核是没办法工作的，必须要有 GNU 项目的众多应用程序来添砖加瓦。 Linux 内核的官网是https://www.kernel.org。Linux 的官方称谓是 GNU/Linux。 Linux 发行版 Linux 发展至今，已经是一个相当复杂和丰富的操作系统了，其大部分源代码还是 GNU 项目的。为了简化安装过程，以及提供一些基本的软件，产生了不少的 Linux 发行版，不同 Linux 发行版的主要区别有：\n安装方法不一样； 安装应用程序的方法不一样，及软件包的管理方式不一样。 不同的 Linux预装的应用程序不一样。 众多的 Linux 发行版 Debian 一组 继承自 Debian 的 Ubuntu 发行版 Linux 终端和文件操作 命令提示符 对tomcat@VM-0-2-ubuntu:~$的解释 tomcat是当前登录用户的名字，Linux 是一个完全的多用户多进程的操作系统。 @就是英语中 at 的意思，@前面是用户名，后面是所在的域。 VM-0-2-ubuntu是当前主机的名称，因为这是腾讯云的虚拟主机，所以默认名称是这样。 :就是分隔符，没有特殊的含义。 ~是当前所在目录的名称，会随着用户进入不同的目录而改变，~表示当前用户的家目录。 $/#指示当前用户权限的字符。$表示普通用户，有权限的限制；#表示超级用户，即root,拥有所有的权限。 命令和命令参数 简单的命令 date显示当前日期及时间\nls 是list的缩写，显示当前目录下的文件\n命令参数 命令参数就是写在命令后的一些补充选项，命令和参数之间用空格隔开。\n短参数：最常见的参数形式就是一个短横线后接一个字母。例如：command -p，如果要一次性加好几个参数，可以使用空格隔开，例如：command -p -a -T -c，多个短参数可以合并在一起，例如 ：command -paTc。\n长参数：长参数是以两个短横开始的，例如：command --parameter，如果有多个长参数，只能使用空格隔开，例如：command --parameter1 -parameter2，可以组合使用短参数和长参数，例如：command --parameter -acT。\n有时候，同一个意义的参数有短参数和长参数两种形式，效果是一样的，例如ls -a，ls --all。\n参数的值：短参数的赋值：command -p 10，长参数的赋值：command --parameter=10。\n如何查找命令及历史记录 Tab 可以补全命令及路径\nCtrl+R可以查找使用过的命令\nhistory命令用于列出之前使用过的所有命令\n一些实用的快捷键\n命令 解释 Ctrl + L 清屏，同clear命令 Ctrl + D 给终端传递 EOF Shift + PgUp 向上滚屏 Shift + PgDn 向下滚屏 Ctrl + A 光标跳到一行命令的开头，Home键有相同的效果 Ctrl + E 光标跳到一行命令的结尾，End键有相同的效果 文件组织，pwd和which命令 Linux 的两种类型文件\n普通的文件：是我们已经熟知的文件类型，这样的文件包括：文本类型的文件、声音文件、程序等。 特殊的文件：这类特殊的文件表示一些东西。例如，光盘驱动就是特殊的文件。甚至根目录也是文件。 根目录：Linux 中只有一个根目录，就是/。根目录就是Linux 的最顶层目录。\n根目录的直属子目录\nbin：英语 binary 的缩写，表示二进制文件，包含了会被所有用户使用的可执行程序\nboot：英语 boot 表示启动，包含与 Linux 启动密切相关的文件\ndev：英语 device 的缩写，表示设备，包含外设。它里面的子目录，每一个对应一个外设\netc：包含系统的配置文件\nhome：用户的私人目录，Linux 中每个用户在home目录下都有自己的私人目录\nlib：英语 library 的缩写，表示库，包含程序所调用的库文件\nmedia：当一个可移动的外设插入电脑时，Linux 就可以让我们通过 media 的子目录来访问这些外设的内容\nmnt：表示挂载，一般用于临时挂载一些装置\nopt：英语 optionnal application software package 的缩写，表示可选的应用程序，用于安装多数第三方软件和插件\nroot：超级用户 root 的家目录/主目录\nsbin：系统二进制文件，包含系统级的重要可执行程序\nsrv：service 的缩写，包含一些网络服务启动之后所需要存取的数据\ntmp：temporary 的缩写，表示临时的，普通用户和程序存放临时文件的地方\nusr：Unix Software Resource 的缩写，表示 Unix 操作系统软件资源，安装了大部分用户要调用的程序\nvar：variable 的缩写，表示动态的、可变的。通常包含程序的数据，比如一些日志文件\npwd命令，显示当前目录的路径。Print Working Director 的缩写，表示打印当前工作目录\nwhich命令，获取命令的可执行文件的位置\n目录相关的命令：ls、cd、du ls是list的缩写，用于列出文件和目录\n-a：显示所有的文件和目录，包括隐藏的，-A不列出.和..两个文件\n-l：列出一个显示文件和目录的详细信息列表\n-h：以 Ko、Mo、Go 的形式显示文件大小\n仅用ls -l时，列出的文件详细信息中，文件的大小是以字节为单位的，可以加一个参数-h，h是 human-readable 的缩写，表示适合人阅读的。\n-t：按文件最近一次修改时间排序\ncd命令：切换目录\ncd是 change directory 的缩写，表示切换目录。\ncd ..：切换到当前目录的上级目录 cd -：切换到前一个目录 cd ~或cd返回到家目录 du命令：显示目录包含的文件大小\ndu命令会深入遍历每个目录的子目录，把所有文件的大小都做一个统计。du是 disk usage 的缩写。\n-h，同ls -lh，以 Ko、Mo、Go 的形式显示文件大小\n-a,显示文件和目录大小\n-s，只显示当前目录的总大小\n文件操作，创建和浏览文件：cat、less、head、tail、touch、mkdir cat命令：一次性显示文件的所有内容\ncat命令的描述是：concatenate files and print on the standard output，意思是：把文件链接起来，一并打在在标准输出。\n-n，在显示的文件内容上加上行号\nless命令：分页显示文件内容\nless命令用法和cat命令是一样的，也是在命令后直接加文件的路径。less命令的好处是它会先读入文件开始的若干行，然后就停在那里，而这若干行的行数取决于终端屏幕的大小。\n空格键：文件内容读取下一个终端屏幕的行数，相当于前进一个屏幕。 回车键：文件内容读取下一行，也就是前进一行。 d 键：前进半页。 b 键：后退一页。 y 键：后退一行。 u 键：后退半页。 q 键：停止读取文件，终止less命令。 /：进入搜索模式，只要在斜杠后面输入要搜索的文字，按下回车键，就会把素有符合的结果标识出来。要在搜索所得结果中跳转，可以按 n 键（跳到下一个结果），N 键（跳到上一个结果）。 head命令：显示文件开头\n默认情况下，head会显示文件的头 10 行。\n-n，指定显示的行数\ntail命令：显示文件结尾\n默认情况下，tail命令会显示文件的尾10 行\n-n，指定显示的行数 -f，实时追踪文件的更新，默认地，tail -f每隔 1 秒检车一下文件是否有更新。可以使用-s参数指定间隔检查的秒数。\ntouch命令：创建一个空白文件\ntouch命令设计初衷是修改文件的时间戳，就是可以修改文件的创建时间或修改时间，让电脑以为文件就是在那个时候被修改或创建的。如果touch后面跟着的文件名是不存在的，那么它就会新建一个。\nmkdir命令：创建一个目录\nmkdir其实是mk和dir的缩合，mk是make的缩写，表示创建，dir是directory的缩写，表示目录。\n-p，递归地创建目录结构\n文件的复制，移动，删除和链接：cp、mv、rm、ln cp命令，复制文件和目录\ncp命令是copy的缩写，用于文件和目录的拷贝。\n-r、-R，拷贝目录\nmv命令，移动文件或目录（重命名文件或目录）\nrm命令，删除文件和目录\nrm是remove的缩写，表示移除，rm命令可以删除一个文件、多个文件、目录。\n-i，向用户确认是否删除，对于每一个要删除的文件，终端都会询问我们是否确定删除，i是inform的缩写。\n-f，不会询问是否删除，强制删除，f是force的缩写。\n-r，递归地删除，使用-r参数，可以删除目录，并且递归地删除其包含的子目录和文件。\nln命令，创建链接\nln是link的缩写，用于在文件之间创建链接。Linux 有两种链接类型：\nPhysical link：物理链接或硬链接 Symbolic link：符号链接或软链接 创建硬链接，一般情况下，只能创建指向文件的硬链接\n-s，创建软连接\n用户和权限、用户管理 TODO ","permalink":"https://2d992714.xianglin2020-github-io.pages.dev/posts/2020/ubuntu-linux-basics/","summary":"Linux 发展至今，已经是一个相当复杂和丰富的操作系统了，其大部分源代码还是 GNU 项目的。","title":"ubuntu-linux"},{"content":"都说程序员应该写写博客、文章，记录自己的成长。一方面总结自己、一方面给后者提供参考。之前做过这样的事，但因为某些原因没有坚持下去，现在想着再继续写博客。\n这个博客记录自己工作学习中觉得有意义的技术。2020 年了，都说十年磨一剑，不知道我这个博客能不能更新到 2030 年。\n希望自己像这个头像一样！\n","permalink":"https://2d992714.xianglin2020-github-io.pages.dev/posts/2020/start-new-year/","summary":"\u003cp\u003e都说程序员应该写写博客、文章，记录自己的成长。一方面总结自己、一方面给后者提供参考。之前做过这样的事，但因为某些原因没有坚持下去，现在想着再继续写博客。\u003c/p\u003e","title":"start-new-year"},{"content":"个人信息 向林 / 男 / 1996 本科 / 重庆交通大学计算机科学与技术专业 工作年限：6 年 求职意向：Java 中高级后端开发工程师 Email：xianglin2020@outlook.com 个人博客：https://blog.xianglin.uk Github：https://github.com/xianglin2020 工作经历 广州青芒软件开发有限责任公司 参与CoolCash移动支付类产品开发和维护。\n省略2份工作\n广东省超算绿色科技股份有限公司 开发岗 ByteGreen 绿色科技云平台项目的核心开发和主要负责人。\n税友软件集团股份有限公司 开发岗 主要参与广东省电子税务局项目的维护和开发。\n教育经历 重庆交通大学\n计算机科学与技术\t2015-2019\n项目和作品 开源贡献 xxl-job-jpa\n将 XXL-JOB 所使用的持久层框架由 MyBatis 改为 Spring Data JPA 以支持更多的关系型数据库。\nplay-morphia\n升级 Morphia 至 2.x 以支持 Java 11 及以上版本。\nsmart-doc\n工具生成的 URL 结尾存在多余 / ，在 Spring Framework 6.0 后接口 404 问题。\n技术文章 JVM 基础 MyBatis 基础 Spring Boot 源码学习 演讲和讲义 Java 线程池介绍 CAS 集成、认证流程和模拟登录 MyBatis 插件原理及 PageHelper 介绍 技能清单 以下是我熟悉和了解的技能\n计算机基础：掌握计算机网络、操作系统、数据结构和算法等基础知识\nJava 语言\n掌握 Java 语言基础、I/O流、容器、元编程、Monad 掌握 Java 锁、同步器、并发数据结构、多线程 熟悉 Java 新特性、JVM、GC 数据库：掌握 MySQL 数据库、掌握 Redis 缓存、熟悉 MongoDB、了解 TimescaleDB\n开发框架：掌握 Spring Boot、Spring Data（JPA、MongoDB、Redis）、MyBatis 等\n分布式\n熟悉 Spring Cloud / Spring Cloud Alibaba 核心组件的使用 了解 Dubbo、OpenTelemetry、Kong 等 了解分布式存储理论 Paxos、CAP，分布式事务方案 2PC、TCC 等 其它技术\n了解 Elasticsearch、RocketMQ、Arthas、ApacheCommonsUtil 熟练使用 Docker、Nginx、Jenkins、XXL-JOB Linux 环境：熟悉 Debian 系Linux，能基于 Linux Desktop 开发\n其它语言：\n系统学习过 Go 语言及生态内框架，包括：gRPC、Gin、GORM、zap 等 熟练使用 HTML5、JavaScript，有 Vue、uni-app 开发经验 学习过 Kotlin、Ktor 参考技能关键字 Java Spring MySQL Linux MyBatis 致谢 感谢您花时间阅读我的简历，期待能有机会和您共事。\n","permalink":"https://2d992714.xianglin2020-github-io.pages.dev/about/","summary":"\u003ch2 id=\"个人信息\"\u003e个人信息\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e向林 / 男 / 1996\u003c/li\u003e\n\u003cli\u003e本科 / 重庆交通大学计算机科学与技术专业\u003c/li\u003e\n\u003cli\u003e工作年限：6 年\u003c/li\u003e\n\u003cli\u003e求职意向：Java 中高级后端开发工程师\u003c/li\u003e\n\u003cli\u003eEmail：xianglin2020@outlook.com\u003c/li\u003e\n\u003cli\u003e个人博客：https://blog.xianglin.uk\u003c/li\u003e\n\u003cli\u003eGithub：https://github.com/xianglin2020\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr\u003e\n\u003ch2 id=\"工作经历\"\u003e工作经历\u003c/h2\u003e\n\u003ch3 id=\"广州青芒软件开发有限责任公司\"\u003e广州青芒软件开发有限责任公司\u003c/h3\u003e\n\u003cp\u003e参与\u003ca href=\"https://www.coolcashcam.com/index.html\" target=\"_blank\" rel=\"noopener\"\u003eCoolCash\u003c/a\u003e移动支付类产品开发和维护。\u003c/p\u003e","title":"🙋🏻‍♂️关于我"}]