diff --git a/.babelrc b/.babelrc new file mode 100644 index 00000000..55754d07 --- /dev/null +++ b/.babelrc @@ -0,0 +1,3 @@ +{ + "compact": false +} diff --git a/.editorconfig b/.editorconfig index ee762040..d72a75ea 100644 --- a/.editorconfig +++ b/.editorconfig @@ -19,7 +19,7 @@ insert_final_newline = true [*.{bat, cmd}] end_of_line = crlf -[*.{java, gradle, groovy, kt, sh, xml}] +[*.{java, gradle, groovy, kt, sh}] indent_size = 4 [*.md] diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 36b705cb..04010943 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -15,7 +15,7 @@ jobs: strategy: matrix: - node-version: [14.x] + node-version: [16.x] steps: # 使用的动作。格式:userName/repoName。作用:检出仓库,获取源码。 官方actions库:https://github.com/actions diff --git a/README.md b/README.md index 11f3a7db..7d3dbdd3 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@

- logo + logo

diff --git a/codes/java-distributed/java-distributed-id/pom.xml b/codes/java-distributed/java-distributed-id/pom.xml new file mode 100644 index 00000000..5f72fc0d --- /dev/null +++ b/codes/java-distributed/java-distributed-id/pom.xml @@ -0,0 +1,52 @@ + + + 4.0.0 + + + io.github.dunwu.distributed + java-distributed + 1.0.0 + + + io.github.dunwu.javatech + java-distributed-id + 1.0.0 + jar + ${project.artifactId} + + + UTF-8 + 1.8 + ${java.version} + ${java.version} + + + + + org.apache.zookeeper + zookeeper + + + org.apache.curator + curator-recipes + + + redis.clients + jedis + + + cn.hutool + hutool-all + + + org.projectlombok + lombok + + + ch.qos.logback + logback-classic + true + + + diff --git a/codes/java-distributed/java-distributed-id/src/main/java/io/github/dunwu/distributed/id/ZookeeperDistributedId.java b/codes/java-distributed/java-distributed-id/src/main/java/io/github/dunwu/distributed/id/ZookeeperDistributedId.java new file mode 100644 index 00000000..06d7092c --- /dev/null +++ b/codes/java-distributed/java-distributed-id/src/main/java/io/github/dunwu/distributed/id/ZookeeperDistributedId.java @@ -0,0 +1,55 @@ +package io.github.dunwu.distributed.id; + +import cn.hutool.core.collection.CollectionUtil; +import lombok.extern.slf4j.Slf4j; +import org.apache.curator.RetryPolicy; +import org.apache.curator.framework.CuratorFramework; +import org.apache.curator.framework.CuratorFrameworkFactory; +import org.apache.curator.retry.ExponentialBackoffRetry; +import org.apache.zookeeper.CreateMode; + +import java.util.List; + +/** + * ZK 分布式 ID + * + * @author Zhang Peng + * @date 2024-12-20 + */ +@Slf4j +public class ZookeeperDistributedId { + + public static void main(String[] args) throws Exception { + + // 获取客户端 + RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 3); + CuratorFramework client = CuratorFrameworkFactory.newClient("127.0.0.1:2181", retryPolicy); + + // 开启会话 + client.start(); + + String id1 = client.create() + .creatingParentsIfNeeded() + .withMode(CreateMode.PERSISTENT_SEQUENTIAL) + .forPath("/zkid/id_"); + log.info("id: {}", id1); + + String id2 = client.create() + .creatingParentsIfNeeded() + .withMode(CreateMode.PERSISTENT_SEQUENTIAL) + .forPath("/zkid/id_"); + log.info("id: {}", id2); + + List children = client.getChildren().forPath("/zkid"); + if (CollectionUtil.isNotEmpty(children)) { + for (String child : children) { + client.delete().forPath("/zkid/" + child); + } + } + client.delete().forPath("/zkid"); + + // 关闭客户端 + client.close(); + } + +} diff --git a/codes/java-distributed/java-distributed-id/src/main/java/io/github/dunwu/distributed/id/ZookeeperDistributedId2.java b/codes/java-distributed/java-distributed-id/src/main/java/io/github/dunwu/distributed/id/ZookeeperDistributedId2.java new file mode 100644 index 00000000..69d5d543 --- /dev/null +++ b/codes/java-distributed/java-distributed-id/src/main/java/io/github/dunwu/distributed/id/ZookeeperDistributedId2.java @@ -0,0 +1,46 @@ +package io.github.dunwu.distributed.id; + +import lombok.extern.slf4j.Slf4j; +import org.apache.curator.RetryPolicy; +import org.apache.curator.framework.CuratorFramework; +import org.apache.curator.framework.CuratorFrameworkFactory; +import org.apache.curator.framework.recipes.atomic.AtomicValue; +import org.apache.curator.framework.recipes.atomic.DistributedAtomicLong; +import org.apache.curator.retry.ExponentialBackoffRetry; + +/** + * ZK 分布式 ID + *

+ * 基于原子计数器生成 ID + * + * @author Zhang Peng + * @date 2024-12-20 + */ +@Slf4j +public class ZookeeperDistributedId2 { + + public static void main(String[] args) throws Exception { + + // 获取客户端 + RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 3); + CuratorFramework client = CuratorFrameworkFactory.newClient("127.0.0.1:2181", retryPolicy); + DistributedAtomicLong atomicLong = new DistributedAtomicLong(client, "/zkid", retryPolicy); + + // 开启会话 + client.start(); + + // 基于原子计数器生成 ID + AtomicValue id1 = atomicLong.increment(); + log.info("id: {}", id1.postValue()); + + AtomicValue id2 = atomicLong.increment(); + log.info("id: {}", id2.postValue()); + + // 清理节点 + client.delete().forPath("/zkid"); + + // 关闭客户端 + client.close(); + } + +} diff --git a/codes/java-distributed/java-distributed-id/src/main/resources/scripts/fixed_window_rate_limit.lua b/codes/java-distributed/java-distributed-id/src/main/resources/scripts/fixed_window_rate_limit.lua new file mode 100644 index 00000000..e0c9ad00 --- /dev/null +++ b/codes/java-distributed/java-distributed-id/src/main/resources/scripts/fixed_window_rate_limit.lua @@ -0,0 +1,21 @@ +-- 缓存 Key +local key = KEYS[1] +-- 访问请求数 +local permits = tonumber(ARGV[1]) +-- 过期时间 +local seconds = tonumber(ARGV[2]) +-- 限流阈值 +local limit = tonumber(ARGV[3]) + +-- 获取统计值 +local count = tonumber(redis.call('GET', key) or "0") + +if count + permits > limit then + -- 请求拒绝 + return -1 +else + -- 请求通过 + redis.call('INCRBY', key, permits) + redis.call('EXPIRE', key, seconds) + return count + permits +end \ No newline at end of file diff --git a/codes/java-distributed/java-distributed-id/src/main/resources/scripts/token_bucket_rate_limit.lua b/codes/java-distributed/java-distributed-id/src/main/resources/scripts/token_bucket_rate_limit.lua new file mode 100644 index 00000000..541d70c9 --- /dev/null +++ b/codes/java-distributed/java-distributed-id/src/main/resources/scripts/token_bucket_rate_limit.lua @@ -0,0 +1,39 @@ +local tokenKey = KEYS[1] +local timeKey = KEYS[2] + +-- 申请令牌数 +local permits = tonumber(ARGV[1]) +-- QPS +local qps = tonumber(ARGV[2]) +-- 桶的容量 +local capacity = tonumber(ARGV[3]) +-- 当前时间(单位:毫秒) +local nowMillis = tonumber(ARGV[4]) +-- 填满令牌桶所需要的时间 +local fillTime = capacity / qps +local ttl = math.min(capacity, math.floor(fillTime * 2)) + +local currentTokenNum = tonumber(redis.call("GET", tokenKey)) +if currentTokenNum == nil then + currentTokenNum = capacity +end + +local endTimeMillis = tonumber(redis.call("GET", timeKey)) +if endTimeMillis == nil then + endTimeMillis = 0 +end + +local gap = nowMillis - endTimeMillis +local newTokenNum = math.max(0, gap * qps / 1000) +local currentTokenNum = math.min(capacity, currentTokenNum + newTokenNum) + +if currentTokenNum < permits then + -- 请求拒绝 + return -1 +else + -- 请求通过 + local finalTokenNum = currentTokenNum - permits + redis.call("SETEX", tokenKey, ttl, finalTokenNum) + redis.call("SETEX", timeKey, ttl, nowMillis) + return finalTokenNum +end diff --git a/codes/java-distributed/java-load-balance/pom.xml b/codes/java-distributed/java-load-balance/pom.xml index 5f14ae1a..b4e73f0f 100644 --- a/codes/java-distributed/java-load-balance/pom.xml +++ b/codes/java-distributed/java-load-balance/pom.xml @@ -3,11 +3,10 @@ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 - io.github.dunwu.javatech + io.github.dunwu.distributed java-load-balance 1.0.0 jar - ${project.artifactId} UTF-8 diff --git a/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/javatech/BaseLoadBalance.java b/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/BaseLoadBalance.java similarity index 96% rename from codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/javatech/BaseLoadBalance.java rename to codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/BaseLoadBalance.java index e4f03ab2..ebb317a8 100644 --- a/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/javatech/BaseLoadBalance.java +++ b/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/BaseLoadBalance.java @@ -1,4 +1,4 @@ -package io.github.dunwu.javatech; +package io.github.dunwu.distributed; import cn.hutool.core.collection.CollectionUtil; diff --git a/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/javatech/ConsistentHashLoadBalance.java b/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/ConsistentHashLoadBalance.java similarity index 99% rename from codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/javatech/ConsistentHashLoadBalance.java rename to codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/ConsistentHashLoadBalance.java index 42394a12..c67558a2 100644 --- a/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/javatech/ConsistentHashLoadBalance.java +++ b/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/ConsistentHashLoadBalance.java @@ -1,4 +1,4 @@ -package io.github.dunwu.javatech; +package io.github.dunwu.distributed; import java.nio.charset.StandardCharsets; import java.security.MessageDigest; diff --git a/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/javatech/IpHashLoadBalance.java b/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/IpHashLoadBalance.java similarity index 94% rename from codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/javatech/IpHashLoadBalance.java rename to codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/IpHashLoadBalance.java index 00be0c93..3d71cbb7 100644 --- a/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/javatech/IpHashLoadBalance.java +++ b/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/IpHashLoadBalance.java @@ -1,4 +1,4 @@ -package io.github.dunwu.javatech; +package io.github.dunwu.distributed; import cn.hutool.core.util.HashUtil; import cn.hutool.core.util.StrUtil; diff --git a/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/javatech/LeastActiveLoadBalance.java b/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/LeastActiveLoadBalance.java similarity index 98% rename from codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/javatech/LeastActiveLoadBalance.java rename to codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/LeastActiveLoadBalance.java index 25dc6f13..23a7f03c 100644 --- a/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/javatech/LeastActiveLoadBalance.java +++ b/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/LeastActiveLoadBalance.java @@ -1,4 +1,4 @@ -package io.github.dunwu.javatech; +package io.github.dunwu.distributed; import java.util.List; import java.util.Random; diff --git a/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/javatech/LoadBalance.java b/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/LoadBalance.java similarity index 86% rename from codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/javatech/LoadBalance.java rename to codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/LoadBalance.java index 046734af..f2d0c561 100644 --- a/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/javatech/LoadBalance.java +++ b/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/LoadBalance.java @@ -1,4 +1,4 @@ -package io.github.dunwu.javatech; +package io.github.dunwu.distributed; import java.util.List; diff --git a/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/javatech/LoadBalanceDemo.java b/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/LoadBalanceDemo.java similarity index 97% rename from codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/javatech/LoadBalanceDemo.java rename to codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/LoadBalanceDemo.java index 1077f48e..57dc61f1 100644 --- a/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/javatech/LoadBalanceDemo.java +++ b/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/LoadBalanceDemo.java @@ -1,6 +1,10 @@ -package io.github.dunwu.javatech; +package io.github.dunwu.distributed; -import java.util.*; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.TreeMap; /** * 负载均衡算法测试例 diff --git a/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/javatech/Node.java b/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/Node.java similarity index 97% rename from codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/javatech/Node.java rename to codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/Node.java index ad0c8ed4..2fc9c712 100644 --- a/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/javatech/Node.java +++ b/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/Node.java @@ -1,4 +1,4 @@ -package io.github.dunwu.javatech; +package io.github.dunwu.distributed; import java.util.Objects; diff --git a/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/javatech/RandomLoadBalance.java b/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/RandomLoadBalance.java similarity index 94% rename from codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/javatech/RandomLoadBalance.java rename to codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/RandomLoadBalance.java index 8f4b7d8d..5b775dd2 100644 --- a/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/javatech/RandomLoadBalance.java +++ b/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/RandomLoadBalance.java @@ -1,4 +1,4 @@ -package io.github.dunwu.javatech; +package io.github.dunwu.distributed; import java.util.List; import java.util.Random; diff --git a/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/javatech/RoundRobinLoadBalance.java b/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/RoundRobinLoadBalance.java similarity index 94% rename from codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/javatech/RoundRobinLoadBalance.java rename to codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/RoundRobinLoadBalance.java index d14b251d..c0858152 100644 --- a/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/javatech/RoundRobinLoadBalance.java +++ b/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/RoundRobinLoadBalance.java @@ -1,4 +1,4 @@ -package io.github.dunwu.javatech; +package io.github.dunwu.distributed; import java.util.List; import java.util.concurrent.atomic.AtomicInteger; diff --git a/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/javatech/StatisticsUtil.java b/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/StatisticsUtil.java similarity index 95% rename from codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/javatech/StatisticsUtil.java rename to codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/StatisticsUtil.java index 50eaa59a..cbb66d13 100644 --- a/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/javatech/StatisticsUtil.java +++ b/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/StatisticsUtil.java @@ -1,4 +1,4 @@ -package io.github.dunwu.javatech; +package io.github.dunwu.distributed; public class StatisticsUtil { diff --git a/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/javatech/WeightRandomLoadBalance.java b/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/WeightRandomLoadBalance.java similarity index 96% rename from codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/javatech/WeightRandomLoadBalance.java rename to codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/WeightRandomLoadBalance.java index a3f1c853..c7135667 100644 --- a/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/javatech/WeightRandomLoadBalance.java +++ b/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/WeightRandomLoadBalance.java @@ -1,4 +1,4 @@ -package io.github.dunwu.javatech; +package io.github.dunwu.distributed; import java.util.List; import java.util.Random; diff --git a/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/javatech/WeightRoundRobinLoadBalance.java b/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/WeightRoundRobinLoadBalance.java similarity index 99% rename from codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/javatech/WeightRoundRobinLoadBalance.java rename to codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/WeightRoundRobinLoadBalance.java index 3180958d..3f71a573 100644 --- a/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/javatech/WeightRoundRobinLoadBalance.java +++ b/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/WeightRoundRobinLoadBalance.java @@ -1,4 +1,4 @@ -package io.github.dunwu.javatech; +package io.github.dunwu.distributed; import java.util.List; import java.util.concurrent.ConcurrentHashMap; diff --git a/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/javatech/package-info.java b/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/package-info.java similarity index 76% rename from codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/javatech/package-info.java rename to codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/package-info.java index 8633bb5a..4d8b7a26 100644 --- a/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/javatech/package-info.java +++ b/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/package-info.java @@ -4,4 +4,4 @@ * @author Zhang Peng * @since 2020-01-22 */ -package io.github.dunwu.javatech; +package io.github.dunwu.distributed; diff --git a/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/javatech/support/CRCHashStrategy.java b/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/support/CRCHashStrategy.java similarity index 98% rename from codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/javatech/support/CRCHashStrategy.java rename to codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/support/CRCHashStrategy.java index 8622bb51..5c732a5b 100644 --- a/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/javatech/support/CRCHashStrategy.java +++ b/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/support/CRCHashStrategy.java @@ -1,4 +1,4 @@ -package io.github.dunwu.javatech.support; +package io.github.dunwu.distributed.support; import java.nio.charset.StandardCharsets; diff --git a/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/javatech/support/FnvHashStrategy.java b/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/support/FnvHashStrategy.java similarity index 92% rename from codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/javatech/support/FnvHashStrategy.java rename to codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/support/FnvHashStrategy.java index 807f3c33..fc7cea13 100644 --- a/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/javatech/support/FnvHashStrategy.java +++ b/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/support/FnvHashStrategy.java @@ -1,4 +1,4 @@ -package io.github.dunwu.javatech.support; +package io.github.dunwu.distributed.support; public class FnvHashStrategy implements HashStrategy { diff --git a/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/javatech/support/HashStrategy.java b/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/support/HashStrategy.java similarity index 59% rename from codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/javatech/support/HashStrategy.java rename to codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/support/HashStrategy.java index 2ad6deef..f574c86e 100644 --- a/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/javatech/support/HashStrategy.java +++ b/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/support/HashStrategy.java @@ -1,4 +1,4 @@ -package io.github.dunwu.javatech.support; +package io.github.dunwu.distributed.support; public interface HashStrategy { diff --git a/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/javatech/support/JdkHashCodeStrategy.java b/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/support/JdkHashCodeStrategy.java similarity index 77% rename from codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/javatech/support/JdkHashCodeStrategy.java rename to codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/support/JdkHashCodeStrategy.java index f4a8389c..6541e2a4 100644 --- a/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/javatech/support/JdkHashCodeStrategy.java +++ b/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/support/JdkHashCodeStrategy.java @@ -1,4 +1,4 @@ -package io.github.dunwu.javatech.support; +package io.github.dunwu.distributed.support; public class JdkHashCodeStrategy implements HashStrategy { diff --git a/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/javatech/support/KetamaHashStrategy.java b/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/support/KetamaHashStrategy.java similarity index 96% rename from codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/javatech/support/KetamaHashStrategy.java rename to codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/support/KetamaHashStrategy.java index 8e98ef1f..ef479aaf 100644 --- a/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/javatech/support/KetamaHashStrategy.java +++ b/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/support/KetamaHashStrategy.java @@ -1,4 +1,4 @@ -package io.github.dunwu.javatech.support; +package io.github.dunwu.distributed.support; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; diff --git a/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/javatech/support/MurmurHashStrategy.java b/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/support/MurmurHashStrategy.java similarity index 96% rename from codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/javatech/support/MurmurHashStrategy.java rename to codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/support/MurmurHashStrategy.java index a100f3d3..82ee1620 100644 --- a/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/javatech/support/MurmurHashStrategy.java +++ b/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/support/MurmurHashStrategy.java @@ -1,4 +1,4 @@ -package io.github.dunwu.javatech.support; +package io.github.dunwu.distributed.support; import java.nio.ByteBuffer; import java.nio.ByteOrder; diff --git a/codes/java-distributed/java-rate-limit/pom.xml b/codes/java-distributed/java-rate-limit/pom.xml new file mode 100644 index 00000000..312d7f21 --- /dev/null +++ b/codes/java-distributed/java-rate-limit/pom.xml @@ -0,0 +1,41 @@ + + + 4.0.0 + + io.github.dunwu.distributed + java-rate-limit + 1.0.0 + jar + + + UTF-8 + 1.8 + ${java.version} + ${java.version} + + + + + redis.clients + jedis + 5.1.0 + + + cn.hutool + hutool-all + 5.8.25 + + + org.projectlombok + lombok + 1.18.30 + + + ch.qos.logback + logback-classic + 1.2.3 + true + + + diff --git a/codes/java-distributed/java-rate-limit/src/main/java/io/github/dunwu/distributed/ratelimit/FixedWindowRateLimiter.java b/codes/java-distributed/java-rate-limit/src/main/java/io/github/dunwu/distributed/ratelimit/FixedWindowRateLimiter.java new file mode 100644 index 00000000..0af8d142 --- /dev/null +++ b/codes/java-distributed/java-rate-limit/src/main/java/io/github/dunwu/distributed/ratelimit/FixedWindowRateLimiter.java @@ -0,0 +1,59 @@ +package io.github.dunwu.distributed.ratelimit; + +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; + +/** + * 固定时间窗口限流算法 + * + * @author Zhang Peng + * @date 2024-01-18 + */ +public class FixedWindowRateLimiter implements RateLimiter { + + /** + * 允许的最大请求数 + */ + private final long maxPermits; + + /** + * 窗口期时长 + */ + private final long periodMillis; + + /** + * 窗口期截止时间 + */ + private long lastPeriodMillis; + + /** + * 请求计数 + */ + private AtomicLong count = new AtomicLong(0); + + public FixedWindowRateLimiter(long qps) { + this(qps, 1000, TimeUnit.MILLISECONDS); + } + + public FixedWindowRateLimiter(long maxPermits, long period, TimeUnit timeUnit) { + this.maxPermits = maxPermits; + this.periodMillis = timeUnit.toMillis(period); + this.lastPeriodMillis = System.currentTimeMillis() + this.periodMillis; + } + + @Override + public synchronized boolean tryAcquire(int permits) { + long now = System.currentTimeMillis(); + if (lastPeriodMillis <= now) { + this.lastPeriodMillis = now + this.periodMillis; + count = new AtomicLong(0); + } + if (count.get() + permits <= maxPermits) { + count.addAndGet(permits); + return true; + } else { + return false; + } + } + +} \ No newline at end of file diff --git a/codes/java-distributed/java-rate-limit/src/main/java/io/github/dunwu/distributed/ratelimit/LeakyBucketRateLimiter.java b/codes/java-distributed/java-rate-limit/src/main/java/io/github/dunwu/distributed/ratelimit/LeakyBucketRateLimiter.java new file mode 100644 index 00000000..0d99a227 --- /dev/null +++ b/codes/java-distributed/java-rate-limit/src/main/java/io/github/dunwu/distributed/ratelimit/LeakyBucketRateLimiter.java @@ -0,0 +1,64 @@ +package io.github.dunwu.distributed.ratelimit; + +import java.util.concurrent.atomic.AtomicLong; + +/** + * 漏桶限流算法 + * + * @author Zhang Peng + * @date 2024-01-18 + */ +public class LeakyBucketRateLimiter implements RateLimiter { + + /** + * QPS + */ + private final int qps; + + /** + * 桶的容量 + */ + private final long capacity; + + /** + * 计算的起始时间 + */ + private long beginTimeMillis; + + /** + * 桶中当前的水量 + */ + private final AtomicLong waterNum = new AtomicLong(0); + + public LeakyBucketRateLimiter(int qps, int capacity) { + this.qps = qps; + this.capacity = capacity; + } + + @Override + public synchronized boolean tryAcquire(int permits) { + + // 如果桶中没有水,直接放行 + if (waterNum.get() == 0) { + beginTimeMillis = System.currentTimeMillis(); + waterNum.addAndGet(permits); + return true; + } + + // 计算水量 + long leakedWaterNum = ((System.currentTimeMillis() - beginTimeMillis) / 1000) * qps; + long currentWaterNum = waterNum.get() - leakedWaterNum; + waterNum.set(Math.max(0, currentWaterNum)); + + // 重置时间 + beginTimeMillis = System.currentTimeMillis(); + + if (waterNum.get() + permits < capacity) { + waterNum.addAndGet(permits); + return true; + } else { + return false; + } + } + +} \ No newline at end of file diff --git a/codes/java-distributed/java-rate-limit/src/main/java/io/github/dunwu/distributed/ratelimit/RateLimiter.java b/codes/java-distributed/java-rate-limit/src/main/java/io/github/dunwu/distributed/ratelimit/RateLimiter.java new file mode 100644 index 00000000..4fbc9646 --- /dev/null +++ b/codes/java-distributed/java-rate-limit/src/main/java/io/github/dunwu/distributed/ratelimit/RateLimiter.java @@ -0,0 +1,13 @@ +package io.github.dunwu.distributed.ratelimit; + +/** + * 限流器 + * + * @author Zhang Peng + * @date 2024-01-18 + */ +public interface RateLimiter { + + boolean tryAcquire(int permits); + +} diff --git a/codes/java-distributed/java-rate-limit/src/main/java/io/github/dunwu/distributed/ratelimit/RateLimiterDemo.java b/codes/java-distributed/java-rate-limit/src/main/java/io/github/dunwu/distributed/ratelimit/RateLimiterDemo.java new file mode 100644 index 00000000..e4a50641 --- /dev/null +++ b/codes/java-distributed/java-rate-limit/src/main/java/io/github/dunwu/distributed/ratelimit/RateLimiterDemo.java @@ -0,0 +1,95 @@ +package io.github.dunwu.distributed.ratelimit; + +import cn.hutool.core.thread.ThreadUtil; +import cn.hutool.core.util.RandomUtil; +import lombok.extern.slf4j.Slf4j; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * 限流器示例 + * + * @author Zhang Peng + * @date 2024-01-18 + */ +@Slf4j +public class RateLimiterDemo { + + public static void main(String[] args) { + + // ============================================================================ + + int qps = 20; + + System.out.println("======================= 固定时间窗口限流算法 ======================="); + FixedWindowRateLimiter fixedWindowRateLimiter = new FixedWindowRateLimiter(qps); + testRateLimit(fixedWindowRateLimiter, qps); + + System.out.println("======================= 滑动时间窗口限流算法 ======================="); + SlidingWindowRateLimiter slidingWindowRateLimiter = new SlidingWindowRateLimiter(qps, 10); + testRateLimit(slidingWindowRateLimiter, qps); + + System.out.println("======================= 漏桶限流算法 ======================="); + LeakyBucketRateLimiter leakyBucketRateLimiter = new LeakyBucketRateLimiter(qps, 100); + testRateLimit(leakyBucketRateLimiter, qps); + + System.out.println("======================= 令牌桶限流算法 ======================="); + TokenBucketRateLimiter tokenBucketRateLimiter = new TokenBucketRateLimiter(qps, 100); + testRateLimit(tokenBucketRateLimiter, qps); + } + + private static void testRateLimit(RateLimiter rateLimiter, int qps) { + + AtomicInteger okNum = new AtomicInteger(0); + AtomicInteger limitNum = new AtomicInteger(0); + ExecutorService executorService = ThreadUtil.newFixedExecutor(10, "限流测试", true); + long beginTime = System.currentTimeMillis(); + + int threadNum = 4; + final CountDownLatch latch = new CountDownLatch(threadNum); + for (int i = 0; i < threadNum; i++) { + executorService.submit(() -> { + try { + batchRequest(rateLimiter, okNum, limitNum, 1000); + } catch (Exception e) { + log.error("发生异常!", e); + } finally { + latch.countDown(); + } + }); + } + + try { + latch.await(10, TimeUnit.SECONDS); + long endTime = System.currentTimeMillis(); + long gap = endTime - beginTime; + log.info("限流 QPS: {} -> 实际结果:耗时 {} ms,{} 次请求成功,{} 次请求被限流,实际 QPS: {}", + qps, gap, okNum.get(), limitNum.get(), okNum.get() * 1000 / gap); + if (okNum.get() == qps) { + log.info("限流符合预期"); + } + } catch (Exception e) { + log.error("发生异常!", e); + } finally { + executorService.shutdown(); + } + } + + private static void batchRequest(RateLimiter rateLimiter, AtomicInteger okNum, AtomicInteger limitNum, int num) + throws InterruptedException { + for (int j = 0; j < num; j++) { + if (rateLimiter.tryAcquire(1)) { + log.info("请求成功"); + okNum.getAndIncrement(); + } else { + log.info("请求限流"); + limitNum.getAndIncrement(); + } + TimeUnit.MILLISECONDS.sleep(RandomUtil.randomInt(0, 10)); + } + } + +} diff --git a/codes/java-distributed/java-rate-limit/src/main/java/io/github/dunwu/distributed/ratelimit/RedisFixedWindowRateLimiter.java b/codes/java-distributed/java-rate-limit/src/main/java/io/github/dunwu/distributed/ratelimit/RedisFixedWindowRateLimiter.java new file mode 100644 index 00000000..ec5d77d9 --- /dev/null +++ b/codes/java-distributed/java-rate-limit/src/main/java/io/github/dunwu/distributed/ratelimit/RedisFixedWindowRateLimiter.java @@ -0,0 +1,100 @@ +package io.github.dunwu.distributed.ratelimit; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.io.resource.ResourceUtil; +import cn.hutool.core.util.RandomUtil; +import cn.hutool.core.util.StrUtil; +import redis.clients.jedis.Jedis; +import redis.clients.jedis.exceptions.JedisConnectionException; + +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.TimeUnit; + +/** + * 基于 Redis + Lua 实现的固定时间窗口限流算法 + * + * @author Zhang Peng + * @date 2024-01-23 + */ +public class RedisFixedWindowRateLimiter implements RateLimiter { + + private static final String REDIS_HOST = "localhost"; + + private static final int REDIS_PORT = 6379; + + private static final Jedis JEDIS; + + public static final String SCRIPT; + + static { + // Jedis 有多种构造方法,这里选用最简单的一种情况 + JEDIS = new Jedis(REDIS_HOST, REDIS_PORT); + + // 触发 ping 命令 + try { + JEDIS.ping(); + System.out.println("jedis 连接成功"); + } catch (JedisConnectionException e) { + e.printStackTrace(); + } + + SCRIPT = FileUtil.readString(ResourceUtil.getResource("scripts/fixed_window_rate_limit.lua"), + StandardCharsets.UTF_8); + } + + private final long maxPermits; + private final long periodSeconds; + private final String key; + + public RedisFixedWindowRateLimiter(long qps, String key) { + this(qps * 60, 60, TimeUnit.SECONDS, key); + } + + public RedisFixedWindowRateLimiter(long maxPermits, long period, TimeUnit timeUnit, String key) { + this.maxPermits = maxPermits; + this.periodSeconds = timeUnit.toSeconds(period); + this.key = key; + } + + @Override + public boolean tryAcquire(int permits) { + List keys = Collections.singletonList(key); + List args = CollectionUtil.newLinkedList(String.valueOf(permits), String.valueOf(periodSeconds), + String.valueOf(maxPermits)); + Object eval = JEDIS.eval(SCRIPT, keys, args); + long value = (long) eval; + return value != -1; + } + + public static void main(String[] args) throws InterruptedException { + + int qps = 20; + RateLimiter jedisFixedWindowRateLimiter = new RedisFixedWindowRateLimiter(qps, "rate:limit:20240122210000"); + + // 模拟在一分钟内,不断收到请求,限流是否有效 + int seconds = 60; + long okNum = 0L; + long total = 0L; + long beginTime = System.currentTimeMillis(); + int num = RandomUtil.randomInt(qps, 100); + for (int second = 0; second < seconds; second++) { + for (int i = 0; i < num; i++) { + total++; + if (jedisFixedWindowRateLimiter.tryAcquire(1)) { + okNum++; + System.out.println("请求成功"); + } else { + System.out.println("请求限流"); + } + } + TimeUnit.SECONDS.sleep(1); + } + long endTime = System.currentTimeMillis(); + long time = (endTime - beginTime) / 1000; + System.out.println(StrUtil.format("请求通过数:{},总请求数:{},实际 QPS:{}", okNum, total, okNum / time)); + } + +} diff --git a/codes/java-distributed/java-rate-limit/src/main/java/io/github/dunwu/distributed/ratelimit/RedisTokenBucketRateLimiter.java b/codes/java-distributed/java-rate-limit/src/main/java/io/github/dunwu/distributed/ratelimit/RedisTokenBucketRateLimiter.java new file mode 100644 index 00000000..9dd219df --- /dev/null +++ b/codes/java-distributed/java-rate-limit/src/main/java/io/github/dunwu/distributed/ratelimit/RedisTokenBucketRateLimiter.java @@ -0,0 +1,104 @@ +package io.github.dunwu.distributed.ratelimit; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.io.resource.ResourceUtil; +import cn.hutool.core.util.RandomUtil; +import cn.hutool.core.util.StrUtil; +import redis.clients.jedis.Jedis; +import redis.clients.jedis.exceptions.JedisConnectionException; + +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.concurrent.TimeUnit; + +/** + * 基于 Redis + Lua 实现的令牌桶限流算法 + * + * @author Zhang Peng + * @date 2024-01-23 + */ +public class RedisTokenBucketRateLimiter implements RateLimiter { + + private static final String REDIS_HOST = "localhost"; + + private static final int REDIS_PORT = 6379; + + private static final Jedis JEDIS; + + public static final String SCRIPT; + + static { + // Jedis 有多种构造方法,这里选用最简单的一种情况 + JEDIS = new Jedis(REDIS_HOST, REDIS_PORT); + + // 触发 ping 命令 + try { + JEDIS.ping(); + System.out.println("jedis 连接成功"); + } catch (JedisConnectionException e) { + e.printStackTrace(); + } + + SCRIPT = FileUtil.readString(ResourceUtil.getResource("scripts/token_bucket_rate_limit.lua"), + StandardCharsets.UTF_8); + } + + private final long qps; + private final long capacity; + private final String tokenKey; + private final String timeKey; + + public RedisTokenBucketRateLimiter(long qps, long capacity, String tokenKey, String timeKey) { + this.qps = qps; + this.capacity = capacity; + this.tokenKey = tokenKey; + this.timeKey = timeKey; + } + + @Override + public boolean tryAcquire(int permits) { + long now = System.currentTimeMillis(); + List keys = CollectionUtil.newLinkedList(tokenKey, timeKey); + List args = CollectionUtil.newLinkedList(String.valueOf(permits), String.valueOf(qps), + String.valueOf(capacity), String.valueOf(now)); + Object eval = JEDIS.eval(SCRIPT, keys, args); + long value = (long) eval; + return value != -1; + } + + public static void main(String[] args) throws InterruptedException { + + int qps = 20; + int bucket = 100; + RedisTokenBucketRateLimiter redisTokenBucketRateLimiter = + new RedisTokenBucketRateLimiter(qps, bucket, "token:rate:limit", "token:rate:limit:time"); + + // 先将令牌桶预热令牌申请完,后续才能真实反映限流 QPS + redisTokenBucketRateLimiter.tryAcquire(bucket); + TimeUnit.SECONDS.sleep(1); + + // 模拟在一分钟内,不断收到请求,限流是否有效 + int seconds = 60; + long okNum = 0L; + long total = 0L; + long beginTime = System.currentTimeMillis(); + for (int second = 0; second < seconds; second++) { + int num = RandomUtil.randomInt(qps, 100); + for (int i = 0; i < num; i++) { + total++; + if (redisTokenBucketRateLimiter.tryAcquire(1)) { + okNum++; + System.out.println("请求成功"); + } else { + System.out.println("请求限流"); + } + } + TimeUnit.SECONDS.sleep(1); + } + long endTime = System.currentTimeMillis(); + long time = (endTime - beginTime) / 1000; + System.out.println(StrUtil.format("请求通过数:{},总请求数:{},实际 QPS:{}", okNum, total, okNum / time)); + } + +} diff --git a/codes/java-distributed/java-rate-limit/src/main/java/io/github/dunwu/distributed/ratelimit/SlidingWindowRateLimiter.java b/codes/java-distributed/java-rate-limit/src/main/java/io/github/dunwu/distributed/ratelimit/SlidingWindowRateLimiter.java new file mode 100644 index 00000000..a93613a2 --- /dev/null +++ b/codes/java-distributed/java-rate-limit/src/main/java/io/github/dunwu/distributed/ratelimit/SlidingWindowRateLimiter.java @@ -0,0 +1,87 @@ +package io.github.dunwu.distributed.ratelimit; + +import java.util.LinkedList; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; + +/** + * 滑动时间窗口限流算法 + * + * @author Zhang Peng + * @date 2024-01-18 + */ +public class SlidingWindowRateLimiter implements RateLimiter { + + /** + * 允许的最大请求数 + */ + private final long maxPermits; + + /** + * 窗口期时长 + */ + private final long periodMillis; + + /** + * 分片窗口期时长 + */ + private final long shardPeriodMillis; + + /** + * 窗口期截止时间 + */ + private long lastPeriodMillis; + + /** + * 分片窗口数 + */ + private final int shardNum; + + /** + * 请求总计数 + */ + private final AtomicLong totalCount = new AtomicLong(0); + + /** + * 分片窗口计数列表 + */ + private final List countList = new LinkedList<>(); + + public SlidingWindowRateLimiter(long qps, int shardNum) { + this(qps, 1000, TimeUnit.MILLISECONDS, shardNum); + } + + public SlidingWindowRateLimiter(long maxPermits, long period, TimeUnit timeUnit, int shardNum) { + this.maxPermits = maxPermits; + this.periodMillis = timeUnit.toMillis(period); + this.lastPeriodMillis = System.currentTimeMillis(); + this.shardPeriodMillis = timeUnit.toMillis(period) / shardNum; + this.shardNum = shardNum; + for (int i = 0; i < shardNum; i++) { + countList.add(new AtomicLong(0)); + } + } + + @Override + public synchronized boolean tryAcquire(int permits) { + long now = System.currentTimeMillis(); + if (now > lastPeriodMillis) { + for (int shardId = 0; shardId < shardNum; shardId++) { + long shardCount = countList.get(shardId).get(); + totalCount.addAndGet(-shardCount); + countList.set(shardId, new AtomicLong(0)); + lastPeriodMillis += shardPeriodMillis; + } + } + int shardId = (int) (now % periodMillis / shardPeriodMillis); + if (totalCount.get() + permits <= maxPermits) { + countList.get(shardId).addAndGet(permits); + totalCount.addAndGet(permits); + return true; + } else { + return false; + } + } + +} \ No newline at end of file diff --git a/codes/java-distributed/java-rate-limit/src/main/java/io/github/dunwu/distributed/ratelimit/TokenBucketRateLimiter.java b/codes/java-distributed/java-rate-limit/src/main/java/io/github/dunwu/distributed/ratelimit/TokenBucketRateLimiter.java new file mode 100644 index 00000000..e03e4c7d --- /dev/null +++ b/codes/java-distributed/java-rate-limit/src/main/java/io/github/dunwu/distributed/ratelimit/TokenBucketRateLimiter.java @@ -0,0 +1,59 @@ +package io.github.dunwu.distributed.ratelimit; + +import java.util.concurrent.atomic.AtomicLong; + +/** + * 令牌桶限流算法 + * + * @author Zhang Peng + * @date 2024-01-18 + */ +public class TokenBucketRateLimiter implements RateLimiter { + + /** + * QPS + */ + private final long qps; + + /** + * 桶的容量 + */ + private final long capacity; + + /** + * 上一次令牌发放时间 + */ + private long endTimeMillis; + + /** + * 桶中当前的令牌数量 + */ + private final AtomicLong tokenNum = new AtomicLong(0); + + public TokenBucketRateLimiter(long qps, long capacity) { + this.qps = qps; + this.capacity = capacity; + this.endTimeMillis = System.currentTimeMillis(); + } + + @Override + public synchronized boolean tryAcquire(int permits) { + + long now = System.currentTimeMillis(); + long gap = now - endTimeMillis; + + // 计算令牌数 + long newTokenNum = (gap * qps / 1000); + long currentTokenNum = tokenNum.get() + newTokenNum; + tokenNum.set(Math.min(capacity, currentTokenNum)); + + if (tokenNum.get() < permits) { + return false; + } else { + tokenNum.addAndGet(-permits); + endTimeMillis = now; + return true; + } + } + +} \ No newline at end of file diff --git a/codes/java-distributed/java-rate-limit/src/main/resources/scripts/fixed_window_rate_limit.lua b/codes/java-distributed/java-rate-limit/src/main/resources/scripts/fixed_window_rate_limit.lua new file mode 100644 index 00000000..e0c9ad00 --- /dev/null +++ b/codes/java-distributed/java-rate-limit/src/main/resources/scripts/fixed_window_rate_limit.lua @@ -0,0 +1,21 @@ +-- 缓存 Key +local key = KEYS[1] +-- 访问请求数 +local permits = tonumber(ARGV[1]) +-- 过期时间 +local seconds = tonumber(ARGV[2]) +-- 限流阈值 +local limit = tonumber(ARGV[3]) + +-- 获取统计值 +local count = tonumber(redis.call('GET', key) or "0") + +if count + permits > limit then + -- 请求拒绝 + return -1 +else + -- 请求通过 + redis.call('INCRBY', key, permits) + redis.call('EXPIRE', key, seconds) + return count + permits +end \ No newline at end of file diff --git a/codes/java-distributed/java-rate-limit/src/main/resources/scripts/token_bucket_rate_limit.lua b/codes/java-distributed/java-rate-limit/src/main/resources/scripts/token_bucket_rate_limit.lua new file mode 100644 index 00000000..541d70c9 --- /dev/null +++ b/codes/java-distributed/java-rate-limit/src/main/resources/scripts/token_bucket_rate_limit.lua @@ -0,0 +1,39 @@ +local tokenKey = KEYS[1] +local timeKey = KEYS[2] + +-- 申请令牌数 +local permits = tonumber(ARGV[1]) +-- QPS +local qps = tonumber(ARGV[2]) +-- 桶的容量 +local capacity = tonumber(ARGV[3]) +-- 当前时间(单位:毫秒) +local nowMillis = tonumber(ARGV[4]) +-- 填满令牌桶所需要的时间 +local fillTime = capacity / qps +local ttl = math.min(capacity, math.floor(fillTime * 2)) + +local currentTokenNum = tonumber(redis.call("GET", tokenKey)) +if currentTokenNum == nil then + currentTokenNum = capacity +end + +local endTimeMillis = tonumber(redis.call("GET", timeKey)) +if endTimeMillis == nil then + endTimeMillis = 0 +end + +local gap = nowMillis - endTimeMillis +local newTokenNum = math.max(0, gap * qps / 1000) +local currentTokenNum = math.min(capacity, currentTokenNum + newTokenNum) + +if currentTokenNum < permits then + -- 请求拒绝 + return -1 +else + -- 请求通过 + local finalTokenNum = currentTokenNum - permits + redis.call("SETEX", tokenKey, ttl, finalTokenNum) + redis.call("SETEX", timeKey, ttl, nowMillis) + return finalTokenNum +end diff --git a/codes/java-distributed/java-task/pom.xml b/codes/java-distributed/java-task/pom.xml new file mode 100644 index 00000000..8d7cc462 --- /dev/null +++ b/codes/java-distributed/java-task/pom.xml @@ -0,0 +1,40 @@ + + + 4.0.0 + + + io.github.dunwu.distributed + java-distributed + 1.0.0 + + + io.github.dunwu.distributed + java-task + 1.0.0 + jar + + + UTF-8 + 1.8 + ${java.version} + ${java.version} + + + + + cn.hutool + hutool-all + + + org.projectlombok + lombok + + + ch.qos.logback + logback-classic + 1.2.3 + true + + + diff --git a/codes/java-distributed/java-task/src/main/java/io/github/dunwu/local/task/DelayQueueExample.java b/codes/java-distributed/java-task/src/main/java/io/github/dunwu/local/task/DelayQueueExample.java new file mode 100644 index 00000000..d510eed1 --- /dev/null +++ b/codes/java-distributed/java-task/src/main/java/io/github/dunwu/local/task/DelayQueueExample.java @@ -0,0 +1,52 @@ +package io.github.dunwu.local.task; + +import cn.hutool.core.date.DateUtil; +import lombok.extern.slf4j.Slf4j; + +import java.util.Date; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.DelayQueue; +import java.util.concurrent.Delayed; +import java.util.concurrent.TimeUnit; + +@Slf4j +public class DelayQueueExample { + + public static void main(String[] args) throws InterruptedException { + BlockingQueue delayQueue = new DelayQueue<>(); + long now = System.currentTimeMillis(); + delayQueue.put(new SampleTask(now + 1000)); + delayQueue.put(new SampleTask(now + 2000)); + delayQueue.put(new SampleTask(now + 3000)); + for (int i = 0; i < 3; i++) { + log.info("task 执行时间:{}", DateUtil.format(new Date(delayQueue.take().getTime()), "yyyy-MM-dd HH:mm:ss")); + } + } + + static class SampleTask implements Delayed { + + long time; + + public SampleTask(long time) { + this.time = time; + } + + public long getTime() { + return time; + } + + @Override + public int compareTo(Delayed o) { + return Long.compare(this.getDelay(TimeUnit.MILLISECONDS), o.getDelay(TimeUnit.MILLISECONDS)); + } + + @Override + public long getDelay(TimeUnit unit) { + return unit.convert(time - System.currentTimeMillis(), TimeUnit.MILLISECONDS); + } + + } + +} + + diff --git a/codes/java-distributed/java-task/src/main/java/io/github/dunwu/local/task/ScheduledExecutorServiceExample.java b/codes/java-distributed/java-task/src/main/java/io/github/dunwu/local/task/ScheduledExecutorServiceExample.java new file mode 100644 index 00000000..78e8f5bd --- /dev/null +++ b/codes/java-distributed/java-task/src/main/java/io/github/dunwu/local/task/ScheduledExecutorServiceExample.java @@ -0,0 +1,37 @@ +package io.github.dunwu.local.task; + +import cn.hutool.core.date.DateUtil; +import lombok.extern.slf4j.Slf4j; + +import java.util.Date; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +@Slf4j +public class ScheduledExecutorServiceExample { + + public static void main(String[] args) { + // 创建一个 ScheduledExecutorService 对象,它将使用一个线程池来执行任务 + ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor(); + + // 创建一个 Runnable 对象,这个任务将在 2 秒后执行,并且每 1 秒重复执行一次 + Runnable task = () -> { + log.info("task 执行时间:{}", DateUtil.format(new Date(), "yyyy-MM-dd HH:mm:ss")); + }; + + // 安排任务在 2 秒后执行,并且每 1 秒重复执行一次 + executor.scheduleAtFixedRate(task, 2, 1, TimeUnit.SECONDS); + + // 主线程等待 10 秒后结束 + try { + Thread.sleep(10000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + + // 关闭 executor,这将停止所有正在执行的任务,并拒绝新任务的提交 + executor.shutdown(); + } + +} diff --git a/codes/java-distributed/java-task/src/main/java/io/github/dunwu/local/task/TimerExample.java b/codes/java-distributed/java-task/src/main/java/io/github/dunwu/local/task/TimerExample.java new file mode 100644 index 00000000..ce1d7756 --- /dev/null +++ b/codes/java-distributed/java-task/src/main/java/io/github/dunwu/local/task/TimerExample.java @@ -0,0 +1,39 @@ +package io.github.dunwu.local.task; + +import cn.hutool.core.date.DateUtil; +import lombok.extern.slf4j.Slf4j; + +import java.util.Date; +import java.util.Timer; +import java.util.TimerTask; + +@Slf4j +public class TimerExample { + + public static void main(String[] args) { + // 创建一个 Timer 对象 + Timer timer = new Timer(); + + // 创建一个 TimerTask 对象,这个任务将在 2 秒后执行,并且每 1 秒重复执行一次 + TimerTask task = new TimerTask() { + @Override + public void run() { + log.info("task 执行时间:{}", DateUtil.format(new Date(), "yyyy-MM-dd HH:mm:ss")); + } + }; + + // 安排任务在 2 秒后执行,并且每 1 秒重复执行一次 + timer.schedule(task, 2000, 1000); + + // 主线程等待 10 秒后结束 + try { + Thread.sleep(10000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + + // 取消定时器和所有已安排的任务 + timer.cancel(); + } + +} diff --git a/codes/java-distributed/pom.xml b/codes/java-distributed/pom.xml index 35fd5860..aa88d15d 100644 --- a/codes/java-distributed/pom.xml +++ b/codes/java-distributed/pom.xml @@ -3,15 +3,52 @@ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 - io.github.dunwu.javatech + io.github.dunwu.distributed java-distributed 1.0.0 pom - JAVA-DISTRIBUTED - JAVA-DISTRIBUTED 示例源码 java-load-balance + java-rate-limit + java-distributed-id + java-task + + + + org.apache.zookeeper + zookeeper + 3.9.2 + + + org.apache.curator + curator-recipes + 4.3.0 + + + redis.clients + jedis + 5.1.0 + + + cn.hutool + hutool-all + 5.8.34 + + + org.projectlombok + lombok + 1.18.30 + + + ch.qos.logback + logback-classic + 1.4.12 + true + + + + diff --git a/codes/javatech/javatech-server/src/main/java/io/github/dunwu/javatech/service/HelloService.java b/codes/javatech/javatech-server/src/main/java/io/github/dunwu/javatech/service/HelloService.java index b7c9d1de..75d9b577 100644 --- a/codes/javatech/javatech-server/src/main/java/io/github/dunwu/javatech/service/HelloService.java +++ b/codes/javatech/javatech-server/src/main/java/io/github/dunwu/javatech/service/HelloService.java @@ -3,7 +3,7 @@ import org.springframework.stereotype.Service; /** - * @author 11123558 + * @author Zhang Peng * @date 2022-07-28 */ @Service diff --git a/codes/javatech/javatech-server/src/test/java/io/github/dunwu/javatech/HelloControllerTests.java b/codes/javatech/javatech-server/src/test/java/io/github/dunwu/javatech/HelloControllerTests.java index 3e16d05e..946e65de 100644 --- a/codes/javatech/javatech-server/src/test/java/io/github/dunwu/javatech/HelloControllerTests.java +++ b/codes/javatech/javatech-server/src/test/java/io/github/dunwu/javatech/HelloControllerTests.java @@ -10,7 +10,7 @@ /** * 单元测试 - * @author 11123558 + * @author Zhang Peng * @date 2022-07-28 */ @RunWith(SpringJUnit4ClassRunner.class) diff --git a/docs/.vuepress/config.js b/docs/.vuepress/config.js index 4c6f199d..6bba8f05 100644 --- a/docs/.vuepress/config.js +++ b/docs/.vuepress/config.js @@ -14,48 +14,55 @@ module.exports = { ['link', { rel: 'icon', href: '/img/favicon.ico' }], //favicons,资源放在public文件夹 ['meta', { name: 'keywords', content: 'vuepress,theme,blog,vdoing' }], ['meta', { name: 'theme-color', content: '#11a8cd' }], // 移动浏览器主题颜色 + + ['meta', { name: 'wwads-cn-verify', content: 'mxqWx62nfQQ9ocT4e5DzISHzOWyF4s' }], // 广告相关,你可以去掉 + ['script', { src: 'https://cdn.wwads.cn/js/makemoney.js', type: 'text/javascript' }] // 广告相关,你可以去掉 ], markdown: { // lineNumbers: true, extractHeaders: ['h2', 'h3', 'h4', 'h5', 'h6'], // 提取标题到侧边栏的级别,默认['h2', 'h3'] externalLinks: { target: '_blank', - rel: 'noopener noreferrer', - }, + rel: 'noopener noreferrer' + } }, // 主题配置 themeConfig: { nav: [ + { text: '首页', link: '/' }, + { text: 'JavaEE', link: '/01.Java/02.JavaEE/' }, { - text: 'JavaEE', + text: 'Java软件', + link: '/01.Java/11.软件/', items: [ - { text: 'JavaWeb', link: '/02.JavaEE/01.JavaWeb/' }, - { text: '服务器', link: '/02.JavaEE/02.服务器/' }, - ], + { text: 'Java 构建', link: '/01.Java/11.软件/01.构建/' }, + { text: 'Java IDE', link: '/01.Java/11.软件/02.IDE/' }, + { text: 'Java 监控诊断', link: '/01.Java/11.软件/03.监控诊断/' } + ] }, { - text: 'Java软件', - link: '/11.软件/', + text: 'Java工具', + link: '/01.Java/12.工具/', items: [ - { text: 'Java构建', link: '/11.软件/01.构建/' }, - { text: 'Java IDE', link: '/11.软件/02.IDE/' }, - { text: 'Java监控诊断', link: '/11.软件/03.监控诊断/' }, - ], + { text: 'Java IO 工具', link: '/01.Java/12.工具/01.IO/' }, + { text: 'JavaBean 工具', link: '/01.Java/12.工具/02.JavaBean/' }, + { text: 'Java 模板引擎', link: '/01.Java/12.工具/03.模板引擎/' }, + { text: 'Java 测试工具', link: '/01.Java/12.工具/04.测试/' } + ] }, - { text: 'Java工具', link: '/12.工具/' }, - { text: 'Java框架', link: '/13.框架/' }, - { text: 'Java中间件', link: '/14.中间件/' }, + { text: 'Java框架', link: '/01.Java/13.框架/' }, + { text: 'Java中间件', link: '/01.Java/14.中间件/' }, { text: '✨ Java系列', ariaLabel: 'Java', items: [ { text: 'Java 教程 📚', link: 'https://dunwu.github.io/java-tutorial/', target: '_blank' }, - { text: 'JavaCore 教程 📚', link: 'https://dunwu.github.io/javacore/', target: '_blank' }, - ], - }, + { text: 'JavaCore 教程 📚', link: 'https://dunwu.github.io/javacore/', target: '_blank' } + ] + } ], sidebarDepth: 2, // 侧边栏显示深度,默认1,最大2(显示到h3标题) - logo: 'https://raw.githubusercontent.com/dunwu/images/dev/common/dunwu-logo.png', // 导航栏logo + logo: 'https://raw.githubusercontent.com/dunwu/images/master/common/dunwu-logo.png', // 导航栏logo repo: 'dunwu/java-tutorial', // 导航栏右侧生成Github链接 searchMaxSuggestions: 10, // 搜索结果显示最大数 lastUpdated: '上次更新', // 更新的时间,及前缀文字 string | boolean (取值为git提交时间) @@ -65,12 +72,13 @@ module.exports = { editLinkText: '📝 帮助改善此页面!', // 以下配置是Vdoing主题改动的和新增的配置 - sidebar: { mode: 'structuring', collapsable: false }, // 侧边栏 'structuring' | { mode: 'structuring', collapsable: Boolean} | 'auto' | 自定义 温馨提示:目录页数据依赖于结构化的侧边栏数据,如果你不设置为'structuring',将无法使用目录页 + sidebar: { mode: 'structuring', collapsable: true }, // 侧边栏 'structuring' | { mode: 'structuring', collapsable: + // Boolean} | 'auto' | 自定义 温馨提示:目录页数据依赖于结构化的侧边栏数据,如果你不设置为'structuring',将无法使用目录页 - // sidebarOpen: false, // 初始状态是否打开侧边栏,默认true + sidebarOpen: true, // 初始状态是否打开侧边栏,默认true updateBar: { // 最近更新栏 - showToArticle: true, // 显示到文章页底部,默认true + showToArticle: true // 显示到文章页底部,默认true // moreArticle: '/archives' // “更多文章”跳转的页面,默认'/archives' }, // titleBadge: false, // 文章标题前的图标是否显示,默认true @@ -95,7 +103,7 @@ module.exports = { author: { // 文章默认的作者信息,可在md文件中单独配置此信息 String | {name: String, href: String} name: 'dunwu', // 必需 - href: 'https://github.com/dunwu', // 可选的 + href: 'https://github.com/dunwu' // 可选的 }, social: { // 社交图标,显示于博主信息栏和页脚栏 @@ -104,21 +112,21 @@ module.exports = { { iconClass: 'icon-youjian', title: '发邮件', - link: 'mailto:forbreak@163.com', + link: 'mailto:forbreak@163.com' }, { iconClass: 'icon-github', title: 'GitHub', - link: 'https://github.com/dunwu', - }, - ], + link: 'https://github.com/dunwu' + } + ] }, footer: { // 页脚信息 createYear: 2019, // 博客创建年份 - copyrightInfo: '钝悟(dunwu) | CC-BY-SA-4.0', // 博客版权信息,支持a标签 + copyrightInfo: '钝悟(dunwu) | CC-BY-SA-4.0' // 博客版权信息,支持a标签 }, - htmlModules, + htmlModules }, // 插件 @@ -128,8 +136,8 @@ module.exports = { { // 鼠标点击爱心特效 color: '#11a8cd', // 爱心颜色,默认随机色 - excludeClassName: 'theme-vdoing-content', // 要排除元素的class, 默认空'' - }, + excludeClassName: 'theme-vdoing-content' // 要排除元素的class, 默认空'' + } ], ['fulltext-search'], // 全文搜索 @@ -159,8 +167,8 @@ module.exports = { copySelector: ['div[class*="language-"] pre', 'div[class*="aside-code"] aside'], // String or Array copyMessage: '复制成功', // default is 'Copy successfully and then paste it for use.' duration: 1000, // prompt message display time. - showInMobile: false, // whether to display on the mobile side, default: false. - }, + showInMobile: false // whether to display on the mobile side, default: false. + } ], [ 'demo-block', @@ -172,18 +180,18 @@ module.exports = { // vue: 'https://cdn.jsdelivr.net/npm/vue/dist/vue.min.js', // 在线示例中的vue依赖 jsfiddle: false, // 是否显示 jsfiddle 链接 codepen: true, // 是否显示 codepen 链接 - horizontal: false, // 是否展示为横向样式 - }, - }, + horizontal: false // 是否展示为横向样式 + } + } ], [ 'vuepress-plugin-zooming', // 放大图片 { selector: '.theme-vdoing-content img:not(.no-zoom)', options: { - bgColor: 'rgba(0,0,0,0.6)', - }, - }, + bgColor: 'rgba(0,0,0,0.6)' + } + } ], [ '@vuepress/last-updated', // "上次更新"时间格式 @@ -191,11 +199,11 @@ module.exports = { transformer: (timestamp, lang) => { const dayjs = require('dayjs') // https://day.js.org/ return dayjs(timestamp).format('YYYY/MM/DD, HH:mm:ss') - }, - }, - ], + } + } + ] ], // 监听文件变化并重新构建 - extraWatchFiles: ['.vuepress/config.js', '.vuepress/config/htmlModules.js'], + extraWatchFiles: ['.vuepress/config.js', '.vuepress/config/htmlModules.js'] } diff --git a/docs/.vuepress/config/baiduCode.js b/docs/.vuepress/config/baiduCode.js index 9dc5fc1e..b0c50903 100644 --- a/docs/.vuepress/config/baiduCode.js +++ b/docs/.vuepress/config/baiduCode.js @@ -1 +1 @@ -module.exports = ''; +module.exports = '' diff --git a/docs/.vuepress/config/htmlModules.js b/docs/.vuepress/config/htmlModules.js index 6ba3782b..fc0a47eb 100644 --- a/docs/.vuepress/config/htmlModules.js +++ b/docs/.vuepress/config/htmlModules.js @@ -20,20 +20,37 @@ module.exports = { // 万维广告 - pageB: ` -

- - `, + // pageT: ` + //
+ // + // `, windowRB: ` -
为了本站的长期运营,请将我们的网站加入广告拦截器的白名单,感谢您的支持!如何添加白名单?万维广告
" - const wwadsEl = document.getElementsByClassName('wwads-cn') - const wwadsContentEl = document.querySelector('.wwads-content') + const h = "
为了本站的长期运营,请将我们的网站加入广告拦截器的白名单,感谢您的支持!如何添加白名单?广告
"; + const wwadsEl = document.getElementsByClassName("wwads-cn"); + const wwadsContentEl = document.querySelector('.wwads-content'); if (wwadsEl[0] && !wwadsContentEl) { - wwadsEl[0].innerHTML = h + wwadsEl[0].innerHTML = h; } -} +}; //check document ready function docReady(t) { - 'complete' === document.readyState || 'interactive' === document.readyState - ? setTimeout(t, 1) - : document.addEventListener('DOMContentLoaded', t) -} - -// 集成 Gitalk 评论插件 -function integrateGitalk(router) { - const linkGitalk = document.createElement('link') - linkGitalk.href = 'https://cdn.jsdelivr.net/npm/gitalk@1/dist/gitalk.css' - linkGitalk.rel = 'stylesheet' - document.body.appendChild(linkGitalk) - const scriptGitalk = document.createElement('script') - scriptGitalk.src = 'https://cdn.jsdelivr.net/npm/gitalk@1/dist/gitalk.min.js' - document.body.appendChild(scriptGitalk) - - router.afterEach((to) => { - if (scriptGitalk.onload) { - loadGitalk(to) - } else { - scriptGitalk.onload = () => { - loadGitalk(to) - } - } - }) - - function loadGitalk(to) { - let commentsContainer = document.getElementById('gitalk-container') - if (!commentsContainer) { - commentsContainer = document.createElement('div') - commentsContainer.id = 'gitalk-container' - commentsContainer.classList.add('content') - } - const $page = document.querySelector('.page') - if ($page) { - $page.appendChild(commentsContainer) - if (typeof Gitalk !== 'undefined' && Gitalk instanceof Function) { - renderGitalk(to.fullPath) - } - } - } - function renderGitalk(fullPath) { - console.info(fullPath) - const gitalk = new Gitalk({ - clientID: '1ba9606d18aec7c070d4', - clientSecret: '57c7e5e3611840efe117ccbad4d87fb60cb364cc', // come from github development - repo: 'java-tutorial', - owner: 'dunwu', - admin: ['dunwu'], - id: 'comment', - distractionFreeMode: false, - language: 'zh-CN', - }) - gitalk.render('gitalk-container') - } + "complete" === document.readyState || + "interactive" === document.readyState + ? setTimeout(t, 1) + : document.addEventListener("DOMContentLoaded", t); } diff --git a/docs/.vuepress/plugins/love-me/index.js b/docs/.vuepress/plugins/love-me/index.js index 67f5ea9c..2851beb0 100644 --- a/docs/.vuepress/plugins/love-me/index.js +++ b/docs/.vuepress/plugins/love-me/index.js @@ -7,6 +7,6 @@ const LoveMyPlugin = (options = {}) => ({ const EXCLUDECLASS = options.excludeClassName || '' return { COLOR, EXCLUDECLASS } }, - enhanceAppFiles: [path.resolve(__dirname, 'love-me.js')], + enhanceAppFiles: [path.resolve(__dirname, 'love-me.js')] }) module.exports = LoveMyPlugin diff --git a/docs/.vuepress/plugins/love-me/love-me.js b/docs/.vuepress/plugins/love-me/love-me.js index f93855e6..5c0369ac 100644 --- a/docs/.vuepress/plugins/love-me/love-me.js +++ b/docs/.vuepress/plugins/love-me/love-me.js @@ -1,62 +1,89 @@ export default () => { - if (typeof window !== "undefined") { - (function(e, t, a) { - function r() { - for (var e = 0; e < s.length; e++) s[e].alpha <= 0 ? (t.body.removeChild(s[e].el), s.splice(e, 1)) : (s[e].y--, s[e].scale += .004, s[e].alpha -= .013, s[e].el.style.cssText = "left:" + s[e].x + "px;top:" + s[e].y + "px;opacity:" + s[e].alpha + ";transform:scale(" + s[e].scale + "," + s[e].scale + ") rotate(45deg);background:" + s[e].color + ";z-index:99999"); - requestAnimationFrame(r) - } - function n() { - var t = "function" == typeof e.onclick && e.onclick; - - e.onclick = function(e) { - // 过滤指定元素 - let mark = true; - EXCLUDECLASS && e.path && e.path.forEach((item) =>{ - if(item.nodeType === 1) { - typeof item.className === 'string' && item.className.indexOf(EXCLUDECLASS) > -1 ? mark = false : '' - } - }) - - if(mark) { - t && t(), - o(e) + if (typeof window !== 'undefined') { + ;(function (e, t, a) { + function r() { + for (var e = 0; e < s.length; e++) + s[e].alpha <= 0 + ? (t.body.removeChild(s[e].el), s.splice(e, 1)) + : (s[e].y--, + (s[e].scale += 0.004), + (s[e].alpha -= 0.013), + (s[e].el.style.cssText = + 'left:' + + s[e].x + + 'px;top:' + + s[e].y + + 'px;opacity:' + + s[e].alpha + + ';transform:scale(' + + s[e].scale + + ',' + + s[e].scale + + ') rotate(45deg);background:' + + s[e].color + + ';z-index:99999')) + requestAnimationFrame(r) + } + function n() { + var t = 'function' == typeof e.onclick && e.onclick + + e.onclick = function (e) { + // 过滤指定元素 + let mark = true + EXCLUDECLASS && + e.path && + e.path.forEach((item) => { + if (item.nodeType === 1) { + typeof item.className === 'string' && item.className.indexOf(EXCLUDECLASS) > -1 ? (mark = false) : '' } - } - } - function o(e) { - var a = t.createElement("div"); - a.className = "heart", - s.push({ - el: a, - x: e.clientX - 5, - y: e.clientY - 5, - scale: 1, - alpha: 1, - color: COLOR - }), - t.body.appendChild(a) + }) + + if (mark) { + t && t(), o(e) + } } - function i(e) { - var a = t.createElement("style"); - a.type = "text/css"; - try { - a.appendChild(t.createTextNode(e)) - } catch(t) { - a.styleSheet.cssText = e - } - t.getElementsByTagName("head")[0].appendChild(a) + } + function o(e) { + var a = t.createElement('div') + ;(a.className = 'heart'), + s.push({ + el: a, + x: e.clientX - 5, + y: e.clientY - 5, + scale: 1, + alpha: 1, + color: COLOR + }), + t.body.appendChild(a) + } + function i(e) { + var a = t.createElement('style') + a.type = 'text/css' + try { + a.appendChild(t.createTextNode(e)) + } catch (t) { + a.styleSheet.cssText = e } - // function c() { - // return "rgb(" + ~~ (255 * Math.random()) + "," + ~~ (255 * Math.random()) + "," + ~~ (255 * Math.random()) + ")" - // } - var s = []; - e.requestAnimationFrame = e.requestAnimationFrame || e.webkitRequestAnimationFrame || e.mozRequestAnimationFrame || e.oRequestAnimationFrame || e.msRequestAnimationFrame || - function(e) { - setTimeout(e, 1e3 / 60) - }, - i(".heart{width: 10px;height: 10px;position: fixed;background: #f00;transform: rotate(45deg);-webkit-transform: rotate(45deg);-moz-transform: rotate(45deg);}.heart:after,.heart:before{content: '';width: inherit;height: inherit;background: inherit;border-radius: 50%;-webkit-border-radius: 50%;-moz-border-radius: 50%;position: fixed;}.heart:after{top: -5px;}.heart:before{left: -5px;}"), + t.getElementsByTagName('head')[0].appendChild(a) + } + // function c() { + // return "rgb(" + ~~ (255 * Math.random()) + "," + ~~ (255 * Math.random()) + "," + ~~ (255 * Math.random()) + ")" + // } + var s = [] + ;(e.requestAnimationFrame = + e.requestAnimationFrame || + e.webkitRequestAnimationFrame || + e.mozRequestAnimationFrame || + e.oRequestAnimationFrame || + e.msRequestAnimationFrame || + function (e) { + setTimeout(e, 1e3 / 60) + }), + i( + ".heart{width: 10px;height: 10px;position: fixed;background: #f00;transform: rotate(45deg);-webkit-transform: rotate(45deg);-moz-transform: rotate(45deg);}.heart:after,.heart:before{content: '';width: inherit;height: inherit;background: inherit;border-radius: 50%;-webkit-border-radius: 50%;-moz-border-radius: 50%;position: fixed;}.heart:after{top: -5px;}.heart:before{left: -5px;}" + ), n(), r() })(window, document) } -} \ No newline at end of file +} diff --git a/docs/.vuepress/public/favicon.ico b/docs/.vuepress/public/favicon.ico new file mode 100644 index 00000000..51e9bfa0 Binary files /dev/null and b/docs/.vuepress/public/favicon.ico differ diff --git a/docs/.vuepress/public/img/EB-logo.png b/docs/.vuepress/public/img/EB-logo.png deleted file mode 100644 index 8e1d5674..00000000 Binary files a/docs/.vuepress/public/img/EB-logo.png and /dev/null differ diff --git a/docs/.vuepress/public/img/bg.jpeg b/docs/.vuepress/public/img/bg.jpeg deleted file mode 100644 index 85e53e7d..00000000 Binary files a/docs/.vuepress/public/img/bg.jpeg and /dev/null differ diff --git a/docs/.vuepress/public/img/bg.jpg b/docs/.vuepress/public/img/bg.jpg deleted file mode 100644 index f093e799..00000000 Binary files a/docs/.vuepress/public/img/bg.jpg and /dev/null differ diff --git "a/docs/.vuepress/public/img/gif/\347\214\2531.gif" "b/docs/.vuepress/public/img/gif/\347\214\2531.gif" deleted file mode 100644 index e84d8a28..00000000 Binary files "a/docs/.vuepress/public/img/gif/\347\214\2531.gif" and /dev/null differ diff --git "a/docs/.vuepress/public/img/gif/\347\214\2532.gif" "b/docs/.vuepress/public/img/gif/\347\214\2532.gif" deleted file mode 100644 index 28114599..00000000 Binary files "a/docs/.vuepress/public/img/gif/\347\214\2532.gif" and /dev/null differ diff --git "a/docs/.vuepress/public/img/gif/\347\214\2533.gif" "b/docs/.vuepress/public/img/gif/\347\214\2533.gif" deleted file mode 100644 index 894372d9..00000000 Binary files "a/docs/.vuepress/public/img/gif/\347\214\2533.gif" and /dev/null differ diff --git "a/docs/.vuepress/public/img/gif/\347\214\2534.gif" "b/docs/.vuepress/public/img/gif/\347\214\2534.gif" deleted file mode 100644 index 7b991309..00000000 Binary files "a/docs/.vuepress/public/img/gif/\347\214\2534.gif" and /dev/null differ diff --git a/docs/.vuepress/public/img/panda-waving.png b/docs/.vuepress/public/img/panda-waving.png deleted file mode 100644 index 20246c60..00000000 Binary files a/docs/.vuepress/public/img/panda-waving.png and /dev/null differ diff --git "a/docs/.vuepress/public/img/png/\346\200\235\347\273\264\345\257\274\345\233\276.png" "b/docs/.vuepress/public/img/png/\346\200\235\347\273\264\345\257\274\345\233\276.png" deleted file mode 100644 index 819ef70b..00000000 Binary files "a/docs/.vuepress/public/img/png/\346\200\235\347\273\264\345\257\274\345\233\276.png" and /dev/null differ diff --git "a/docs/.vuepress/public/img/png/\346\225\260\346\215\256\345\272\223.png" "b/docs/.vuepress/public/img/png/\346\225\260\346\215\256\345\272\223.png" deleted file mode 100644 index 4d13c3f2..00000000 Binary files "a/docs/.vuepress/public/img/png/\346\225\260\346\215\256\345\272\223.png" and /dev/null differ diff --git "a/docs/.vuepress/public/img/png/\346\225\260\346\215\256\347\273\223\346\236\204.png" "b/docs/.vuepress/public/img/png/\346\225\260\346\215\256\347\273\223\346\236\204.png" deleted file mode 100644 index 1bafe966..00000000 Binary files "a/docs/.vuepress/public/img/png/\346\225\260\346\215\256\347\273\223\346\236\204.png" and /dev/null differ diff --git "a/docs/.vuepress/public/img/png/\346\234\215\345\212\241\345\231\250.png" "b/docs/.vuepress/public/img/png/\346\234\215\345\212\241\345\231\250.png" deleted file mode 100644 index 4127eb56..00000000 Binary files "a/docs/.vuepress/public/img/png/\346\234\215\345\212\241\345\231\250.png" and /dev/null differ diff --git "a/docs/.vuepress/public/img/png/\346\234\272\345\231\250\345\255\246\344\271\240.png" "b/docs/.vuepress/public/img/png/\346\234\272\345\231\250\345\255\246\344\271\240.png" deleted file mode 100644 index 081c2b88..00000000 Binary files "a/docs/.vuepress/public/img/png/\346\234\272\345\231\250\345\255\246\344\271\240.png" and /dev/null differ diff --git "a/docs/.vuepress/public/img/png/\347\256\227\346\263\225.png" "b/docs/.vuepress/public/img/png/\347\256\227\346\263\225.png" deleted file mode 100644 index 92fef3f6..00000000 Binary files "a/docs/.vuepress/public/img/png/\347\256\227\346\263\225.png" and /dev/null differ diff --git "a/docs/.vuepress/public/img/png/\347\263\273\347\273\237.png" "b/docs/.vuepress/public/img/png/\347\263\273\347\273\237.png" deleted file mode 100644 index 87f0de0a..00000000 Binary files "a/docs/.vuepress/public/img/png/\347\263\273\347\273\237.png" and /dev/null differ diff --git "a/docs/.vuepress/public/img/png/\347\263\273\347\273\237\345\210\206\346\236\220.png" "b/docs/.vuepress/public/img/png/\347\263\273\347\273\237\345\210\206\346\236\220.png" deleted file mode 100644 index 208e2597..00000000 Binary files "a/docs/.vuepress/public/img/png/\347\263\273\347\273\237\345\210\206\346\236\220.png" and /dev/null differ diff --git "a/docs/.vuepress/public/img/png/\347\274\226\347\250\213.png" "b/docs/.vuepress/public/img/png/\347\274\226\347\250\213.png" deleted file mode 100644 index a8bca769..00000000 Binary files "a/docs/.vuepress/public/img/png/\347\274\226\347\250\213.png" and /dev/null differ diff --git "a/docs/.vuepress/public/img/png/\347\275\221\347\273\234\346\212\200\346\234\257.png" "b/docs/.vuepress/public/img/png/\347\275\221\347\273\234\346\212\200\346\234\257.png" deleted file mode 100644 index f81edb71..00000000 Binary files "a/docs/.vuepress/public/img/png/\347\275\221\347\273\234\346\212\200\346\234\257.png" and /dev/null differ diff --git "a/docs/.vuepress/public/img/png/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234.png" "b/docs/.vuepress/public/img/png/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234.png" deleted file mode 100644 index 63a1819f..00000000 Binary files "a/docs/.vuepress/public/img/png/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234.png" and /dev/null differ diff --git "a/docs/.vuepress/public/img/png/\350\275\257\344\273\266\345\274\200\345\217\221.png" "b/docs/.vuepress/public/img/png/\350\275\257\344\273\266\345\274\200\345\217\221.png" deleted file mode 100644 index 72c5e852..00000000 Binary files "a/docs/.vuepress/public/img/png/\350\275\257\344\273\266\345\274\200\345\217\221.png" and /dev/null differ diff --git "a/docs/.vuepress/public/img/png/\351\235\242\345\220\221\345\257\271\350\261\241.png" "b/docs/.vuepress/public/img/png/\351\235\242\345\220\221\345\257\271\350\261\241.png" deleted file mode 100644 index cd13fa3c..00000000 Binary files "a/docs/.vuepress/public/img/png/\351\235\242\345\220\221\345\257\271\350\261\241.png" and /dev/null differ diff --git "a/docs/.vuepress/public/img/png/\351\241\271\347\233\256\347\256\241\347\220\206.png" "b/docs/.vuepress/public/img/png/\351\241\271\347\233\256\347\256\241\347\220\206.png" deleted file mode 100644 index 0467abd5..00000000 Binary files "a/docs/.vuepress/public/img/png/\351\241\271\347\233\256\347\256\241\347\220\206.png" and /dev/null differ diff --git a/docs/.vuepress/public/img/python.png b/docs/.vuepress/public/img/python.png deleted file mode 100644 index c3ddebeb..00000000 Binary files a/docs/.vuepress/public/img/python.png and /dev/null differ diff --git a/docs/.vuepress/public/img/ui.png b/docs/.vuepress/public/img/ui.png deleted file mode 100644 index 617c56d7..00000000 Binary files a/docs/.vuepress/public/img/ui.png and /dev/null differ diff --git a/docs/.vuepress/public/img/web.png b/docs/.vuepress/public/img/web.png deleted file mode 100644 index 0a6e27c4..00000000 Binary files a/docs/.vuepress/public/img/web.png and /dev/null differ diff --git a/docs/.vuepress/public/markmap/01.html b/docs/.vuepress/public/markmap/01.html index c55f2d03..c4e0bdbc 100644 --- a/docs/.vuepress/public/markmap/01.html +++ b/docs/.vuepress/public/markmap/01.html @@ -1,25 +1,113 @@ - - - - -Markmap - - - - - - - + + + + + Markmap + + + + + + + + diff --git "a/docs/02.JavaEE/01.JavaWeb/01.JavaWeb\344\271\213Servlet\346\214\207\345\215\227.md" "b/docs/01.Java/02.JavaEE/01.JavaWeb/01.JavaWeb\344\271\213Servlet\346\214\207\345\215\227.md" similarity index 99% rename from "docs/02.JavaEE/01.JavaWeb/01.JavaWeb\344\271\213Servlet\346\214\207\345\215\227.md" rename to "docs/01.Java/02.JavaEE/01.JavaWeb/01.JavaWeb\344\271\213Servlet\346\214\207\345\215\227.md" index c49ed4f2..2133814e 100644 --- "a/docs/02.JavaEE/01.JavaWeb/01.JavaWeb\344\271\213Servlet\346\214\207\345\215\227.md" +++ "b/docs/01.Java/02.JavaEE/01.JavaWeb/01.JavaWeb\344\271\213Servlet\346\214\207\345\215\227.md" @@ -1,16 +1,16 @@ --- title: JavaWeb 之 Servlet 指南 -categories: - - 编程 +date: 2020-08-24 19:41:46 +order: 01 +categories: - Java + - JavaEE - JavaWeb -tags: +tags: - Java - JavaWeb - Servlet -abbrlink: 2ce8dedd -date: 2020-08-24 19:41:46 -permalink: /pages/be093b/ +permalink: /pages/e98894/ --- # JavaWeb 之 Servlet 指南 @@ -310,4 +310,4 @@ Apache Tomcat/5.5.29 ## 参考资料 - [深入拆解 Tomcat & Jetty](https://time.geekbang.org/column/intro/100027701) -- [Java Web 整合开发王者归来](https://book.douban.com/subject/4189495/) +- [Java Web 整合开发王者归来](https://book.douban.com/subject/4189495/) \ No newline at end of file diff --git "a/docs/02.JavaEE/01.JavaWeb/02.JavaWeb\344\271\213Jsp\346\214\207\345\215\227.md" "b/docs/01.Java/02.JavaEE/01.JavaWeb/02.JavaWeb\344\271\213Jsp\346\214\207\345\215\227.md" similarity index 95% rename from "docs/02.JavaEE/01.JavaWeb/02.JavaWeb\344\271\213Jsp\346\214\207\345\215\227.md" rename to "docs/01.Java/02.JavaEE/01.JavaWeb/02.JavaWeb\344\271\213Jsp\346\214\207\345\215\227.md" index f0c28076..32b8ab7c 100644 --- "a/docs/02.JavaEE/01.JavaWeb/02.JavaWeb\344\271\213Jsp\346\214\207\345\215\227.md" +++ "b/docs/01.Java/02.JavaEE/01.JavaWeb/02.JavaWeb\344\271\213Jsp\346\214\207\345\215\227.md" @@ -1,16 +1,16 @@ --- title: JavaWeb 之 Jsp 指南 -categories: - - 编程 +date: 2020-02-07 23:04:47 +order: 02 +categories: - Java + - JavaEE - JavaWeb -tags: +tags: - Java - JavaWeb - JSP -abbrlink: 339b3115 -date: 2020-02-07 23:04:47 -permalink: /pages/6bbb16/ +permalink: /pages/8cc787/ --- # JavaWeb 之 Jsp 指南 @@ -21,7 +21,7 @@ permalink: /pages/6bbb16/ `JSP`全称`Java Server Pages`,是一种动态网页开发技术。 -它使用 JSP 标签在 HTML 网页中插入 Java 代码。标签通常以`<%`开头以`%>`结束。 +它使用 JSP 标签在 HTML 网页中插入 Java 代码。标签通常以 `<%` 开头以 `%>` 结束。 JSP 是一种 Java servlet,主要用于实现 Java web 应用程序的用户界面部分。网页开发者们通过结合 HTML 代码、XHTML 代码、XML 元素以及嵌入 JSP 操作和命令来编写 JSP。 @@ -334,14 +334,14 @@ pageEncoding="UTF-8"%> 不同情况下使用注释的语法规则: -| **语法** | 描述 | -| ---------------- | ----------------------------------------------------- | -| `<%-- 注释 --%>` | JSP 注释,注释内容不会被发送至浏览器甚至不会被编译 | +| **语法** | 描述 | +| ---------------- |-------------------------------| +| `<%-- 注释 --%>` | JSP 注释,注释内容不会被发送至浏览器甚至不会被编译 | | `` | HTML 注释,通过浏览器查看网页源代码时可以看见注释内容 | -| `<%` | 代表静态 <%常量 | -| `%>` | 代表静态 %> 常量 | -| `'` | 在属性中使用的单引号 | -| `"` | 在属性中使用的双引号 | +| `<%` | 代表静态 `<%` 常量 | +| `%>` | 代表静态 `%>` 常量 | +| `'` | 在属性中使用的单引号 | +| `"` | 在属性中使用的双引号 | ### 控制语句 @@ -471,23 +471,23 @@ JSP 支持所有 Java 逻辑和算术运算符。 下表罗列出了 JSP 常见运算符,优先级从高到底: -| **类别** | **操作符** | **结合性** | -| --------- | -------------------------------- | ---------- | ------ | ------ | -| 后缀 | `() [] .` (点运算符) | 左到右 | -| 一元 | `++ - - ! ~` | 右到左 | -| 可乘性 | `* / %` | 左到右 | -| 可加性 | `+ -` | 左到右 | -| 移位 | `>> >>> <<` | 左到右 | -| 关系 | `> >= < <=` | 左到右 | -| 相等/不等 | `== !=` | 左到右 | -| 位与 | `&` | 左到右 | -| 位异或 | `^` | 左到右 | -| 位或 | ` | ` | 左到右 | -| 逻辑与 | `&&` | 左到右 | -| 逻辑或 | ` | | ` | 左到右 | -| 条件判断 | `?:` | 右到左 | -| 赋值 | `= += -= \*= /= %= >>= <<= &= ^= | =` | 右到左 | -| 逗号 | `,` | 左到右 | +| **类别** | **操作符** | **结合性** | +| --------- | ------------------------------------- | ---------- | +| 后缀 | `() [] .` (点运算符) | 左到右 | +| 一元 | `++ - - ! ~` | 右到左 | +| 可乘性 | `* / %` | 左到右 | +| 可加性 | `+ -` | 左到右 | +| 移位 | `>> >>> <<` | 左到右 | +| 关系 | `> >= < <=` | 左到右 | +| 相等/不等 | `== !=` | 左到右 | +| 位与 | `&` | 左到右 | +| 位异或 | `^` | 左到右 | +| 位或 | `|` | 左到右 | +| 逻辑与 | `&&` | 左到右 | +| 逻辑或 | `| |` | 左到右 | +| 条件判断 | `?:` | 右到左 | +| 赋值 | `= += -= \*= /= %= >>= <<= &= ^= | =` | 右到左 | +| 逗号 | `,` | 左到右 | ### JSP 字面量 @@ -1354,7 +1354,7 @@ Apache Tomcat 安装 JSTL 库步骤如下: ``` -使用任何库,你必须在每个 JSP 文件中的头部包含 **** 标签。 +使用任何库,你必须在每个 JSP 文件中的头部包含 **``** 标签。 ### 核心标签 @@ -1364,22 +1364,22 @@ Apache Tomcat 安装 JSTL 库步骤如下: <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> ``` -| 标签 | 描述 | -| :---------------------------------------------------------------------- | :-------------------------------------------------------------------------- | -| [``](http://www.runoob.com/jsp/jstl-core-out-tag.html) | 用于在 JSP 中显示数据,就像<%= ... > | -| [``](http://www.runoob.com/jsp/jstl-core-set-tag.html) | 用于保存数据 | -| [``](http://www.runoob.com/jsp/jstl-core-remove-tag.html) | 用于删除数据 | -| [``](http://www.runoob.com/jsp/jstl-core-catch-tag.html) | 用来处理产生错误的异常状况,并且将错误信息储存起来 | -| [``](http://www.runoob.com/jsp/jstl-core-if-tag.html) | 与我们在一般程序中用的 if 一样 | -| [``](http://www.runoob.com/jsp/jstl-core-choose-tag.html) | 本身只当做的父标签 | -| [``](http://www.runoob.com/jsp/jstl-core-choose-tag.html) | 的子标签,用来判断条件是否成立 | -| [``](http://www.runoob.com/jsp/jstl-core-choose-tag.html) | 的子标签,接在标签后,当标签判断为 false 时被执行 | -| [``](http://www.runoob.com/jsp/jstl-core-import-tag.html) | 检索一个绝对或相对 URL,然后将其内容暴露给页面 | -| [``](http://www.runoob.com/jsp/jstl-core-foreach-tag.html) | 基础迭代标签,接受多种集合类型 | -| [``](http://www.runoob.com/jsp/jstl-core-foreach-tag.html) | 根据指定的分隔符来分隔内容并迭代输出 | -| [``](http://www.runoob.com/jsp/jstl-core-param-tag.html) | 用来给包含或重定向的页面传递参数 | -| [``](http://www.runoob.com/jsp/jstl-core-redirect-tag.html) | 重定向至一个新的 URL. | -| [``](http://www.runoob.com/jsp/jstl-core-url-tag.html) | 使用可选的查询参数来创造一个 URL | +| 标签 | 描述 | +| :---------------------------------------------------------------------- |:------------------------------------------------------------------| +| [``](http://www.runoob.com/jsp/jstl-core-out-tag.html) | 用于在 JSP 中显示数据,就像<%= ... > | +| [``](http://www.runoob.com/jsp/jstl-core-set-tag.html) | 用于保存数据 | +| [``](http://www.runoob.com/jsp/jstl-core-remove-tag.html) | 用于删除数据 | +| [``](http://www.runoob.com/jsp/jstl-core-catch-tag.html) | 用来处理产生错误的异常状况,并且将错误信息储存起来 | +| [``](http://www.runoob.com/jsp/jstl-core-if-tag.html) | 与我们在一般程序中用的 if 一样 | +| [``](http://www.runoob.com/jsp/jstl-core-choose-tag.html) | 本身只当做 `` 和 `` 的父标签 | +| [``](http://www.runoob.com/jsp/jstl-core-choose-tag.html) | `` 的子标签,用来判断条件是否成立 | +| [``](http://www.runoob.com/jsp/jstl-core-choose-tag.html) | `` 的子标签,接在 `` 标签后,当 `` 标签判断为 false 时被执行 | +| [``](http://www.runoob.com/jsp/jstl-core-import-tag.html) | 检索一个绝对或相对 URL,然后将其内容暴露给页面 | +| [``](http://www.runoob.com/jsp/jstl-core-foreach-tag.html) | 基础迭代标签,接受多种集合类型 | +| [``](http://www.runoob.com/jsp/jstl-core-foreach-tag.html) | 根据指定的分隔符来分隔内容并迭代输出 | +| [``](http://www.runoob.com/jsp/jstl-core-param-tag.html) | 用来给包含或重定向的页面传递参数 | +| [``](http://www.runoob.com/jsp/jstl-core-redirect-tag.html) | 重定向至一个新的 URL. | +| [``](http://www.runoob.com/jsp/jstl-core-url-tag.html) | 使用可选的查询参数来创造一个 URL | ### 格式化标签 @@ -1438,18 +1438,18 @@ JSTL XML 标签库提供了创建和操作 XML 文档的标签。引用 XML 标 下载地址: -| 标签 | 描述 | -| :----------------------------------------------------------------------- | :---------------------------------------------------------- | -| [``](http://www.runoob.com/jsp/jstl-xml-out-tag.html) | 与<%= ... >,类似,不过只用于 XPath 表达式 | -| [``](http://www.runoob.com/jsp/jstl-xml-parse-tag.html) | 解析 XML 数据 | -| [``](http://www.runoob.com/jsp/jstl-xml-set-tag.html) | 设置 XPath 表达式 | -| [``](http://www.runoob.com/jsp/jstl-xml-if-tag.html) | 判断 XPath 表达式,若为真,则执行本体中的内容,否则跳过本体 | -| [``](http://www.runoob.com/jsp/jstl-xml-foreach-tag.html) | 迭代 XML 文档中的节点 | -| [``](http://www.runoob.com/jsp/jstl-xml-choose-tag.html) | 的父标签 | -| [``](http://www.runoob.com/jsp/jstl-xml-choose-tag.html) | 的子标签,用来进行条件判断 | -| [``](http://www.runoob.com/jsp/jstl-xml-choose-tag.html) | 的子标签,当判断为 false 时被执行 | -| [``](http://www.runoob.com/jsp/jstl-xml-transform-tag.html) | 将 XSL 转换应用在 XML 文档中 | -| [``](http://www.runoob.com/jsp/jstl-xml-param-tag.html) | 与共同使用,用于设置 XSL 样式表 | +| 标签 | 描述 | +| :----------------------------------------------------------------------- |:----------------------------------------------| +| [``](http://www.runoob.com/jsp/jstl-xml-out-tag.html) | 与 `<%= ... >`,类似,不过只用于 XPath 表达式 | +| [``](http://www.runoob.com/jsp/jstl-xml-parse-tag.html) | 解析 XML 数据 | +| [``](http://www.runoob.com/jsp/jstl-xml-set-tag.html) | 设置 XPath 表达式 | +| [``](http://www.runoob.com/jsp/jstl-xml-if-tag.html) | 判断 XPath 表达式,若为真,则执行本体中的内容,否则跳过本体 | +| [``](http://www.runoob.com/jsp/jstl-xml-foreach-tag.html) | 迭代 XML 文档中的节点 | +| [``](http://www.runoob.com/jsp/jstl-xml-choose-tag.html) | `` 和 `` 的父标签 | +| [``](http://www.runoob.com/jsp/jstl-xml-choose-tag.html) | `` 的子标签,用来进行条件判断 | +| [``](http://www.runoob.com/jsp/jstl-xml-choose-tag.html) | `` 的子标签,当 `` 判断为 false 时被执行 | +| [``](http://www.runoob.com/jsp/jstl-xml-transform-tag.html) | 将 XSL 转换应用在 XML 文档中 | +| [``](http://www.runoob.com/jsp/jstl-xml-param-tag.html) | 与 `` 共同使用,用于设置 XSL 样式表 | ### JSTL 函数 @@ -1650,7 +1650,7 @@ public class HelloTag extends SimpleTagSupport { } ``` -属性的名称是"message",所以 setter 方法是的 setMessage()。现在让我们在 TLD 文件中使用的元素添加此属性: +属性的名称是"message",所以 setter 方法是的 setMessage()。现在让我们在 TLD 文件中使用的 `` 元素添加此属性: ``` @@ -1728,4 +1728,4 @@ This is custom tag java.util.Date ..... -``` +``` \ No newline at end of file diff --git "a/docs/02.JavaEE/01.JavaWeb/03.JavaWeb\344\271\213Filter\345\222\214Listener.md" "b/docs/01.Java/02.JavaEE/01.JavaWeb/03.JavaWeb\344\271\213Filter\345\222\214Listener.md" similarity index 98% rename from "docs/02.JavaEE/01.JavaWeb/03.JavaWeb\344\271\213Filter\345\222\214Listener.md" rename to "docs/01.Java/02.JavaEE/01.JavaWeb/03.JavaWeb\344\271\213Filter\345\222\214Listener.md" index f13e99be..aea60247 100644 --- "a/docs/02.JavaEE/01.JavaWeb/03.JavaWeb\344\271\213Filter\345\222\214Listener.md" +++ "b/docs/01.Java/02.JavaEE/01.JavaWeb/03.JavaWeb\344\271\213Filter\345\222\214Listener.md" @@ -1,17 +1,17 @@ --- title: JavaWeb 之 Filter 和 Listener -categories: - - 编程 +date: 2020-08-24 19:41:46 +order: 03 +categories: - Java + - JavaEE - JavaWeb -tags: +tags: - Java - JavaWeb - Filter - Listener -abbrlink: 9eb5517a -date: 2020-08-24 19:41:46 -permalink: /pages/5ecb29/ +permalink: /pages/82df5f/ --- # JavaWeb 之 Filter 和 Listener @@ -24,7 +24,7 @@ permalink: /pages/5ecb29/ Filter 提供了过滤链(Filter Chain)的概念,一个过滤链包括多个 Filter。客户端请求 request 在抵达 Servlet 之前会经过过滤链的所有 Filter,服务器响应 response 从 Servlet 抵达客户端浏览器之前也会经过过滤链的所有 FIlter。 -![img](https://raw.githubusercontent.com/dunwu/images/dev/snap/1559054413341.png) +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/1559054413341.png) ### 过滤器方法 @@ -215,4 +215,4 @@ Filter 和 Listener 的本质区别: ## 参考资料 - [深入拆解 Tomcat & Jetty](https://time.geekbang.org/column/intro/100027701) -- [Java Web 整合开发王者归来](https://book.douban.com/subject/4189495/) +- [Java Web 整合开发王者归来](https://book.douban.com/subject/4189495/) \ No newline at end of file diff --git "a/docs/02.JavaEE/01.JavaWeb/04.JavaWeb\344\271\213Cookie\345\222\214Session.md" "b/docs/01.Java/02.JavaEE/01.JavaWeb/04.JavaWeb\344\271\213Cookie\345\222\214Session.md" similarity index 99% rename from "docs/02.JavaEE/01.JavaWeb/04.JavaWeb\344\271\213Cookie\345\222\214Session.md" rename to "docs/01.Java/02.JavaEE/01.JavaWeb/04.JavaWeb\344\271\213Cookie\345\222\214Session.md" index 9358744c..3729f4a6 100644 --- "a/docs/02.JavaEE/01.JavaWeb/04.JavaWeb\344\271\213Cookie\345\222\214Session.md" +++ "b/docs/01.Java/02.JavaEE/01.JavaWeb/04.JavaWeb\344\271\213Cookie\345\222\214Session.md" @@ -1,17 +1,17 @@ --- title: JavaWeb 之 Cookie 和 Session -categories: - - 编程 +date: 2020-08-24 19:41:46 +order: 04 +categories: - Java + - JavaEE - JavaWeb -tags: +tags: - Java - JavaWeb - Cookie - Session -abbrlink: bb93a37b -date: 2020-08-24 19:41:46 -permalink: /pages/e61883/ +permalink: /pages/c46bff/ --- # JavaWeb 之 Cookie 和 Session @@ -587,4 +587,4 @@ Cookie 需要浏览器支持才能使用。 ### 跨域名 - Cookie 支持跨域名。 -- Session 不支持跨域名。 +- Session 不支持跨域名。 \ No newline at end of file diff --git "a/docs/02.JavaEE/01.JavaWeb/99.JavaWeb\351\235\242\347\273\217.md" "b/docs/01.Java/02.JavaEE/01.JavaWeb/99.JavaWeb\351\235\242\347\273\217.md" similarity index 99% rename from "docs/02.JavaEE/01.JavaWeb/99.JavaWeb\351\235\242\347\273\217.md" rename to "docs/01.Java/02.JavaEE/01.JavaWeb/99.JavaWeb\351\235\242\347\273\217.md" index e5a71bb9..8e803b40 100644 --- "a/docs/02.JavaEE/01.JavaWeb/99.JavaWeb\351\235\242\347\273\217.md" +++ "b/docs/01.Java/02.JavaEE/01.JavaWeb/99.JavaWeb\351\235\242\347\273\217.md" @@ -1,16 +1,16 @@ --- title: JavaWeb 面经 -categories: - - 编程 +date: 2020-02-07 23:04:47 +order: 99 +categories: - Java + - JavaEE - JavaWeb -tags: +tags: - Java - JavaWeb - Servlet -abbrlink: f216aa98 -date: 2020-02-07 23:04:47 -permalink: /pages/1933b3/ +permalink: /pages/e175ce/ --- # JavaWeb 面经 @@ -266,4 +266,4 @@ Servlet 生命周期如下: ## 参考资料 - https://blog.csdn.net/YM_IlY/article/details/81266959 -- https://www.jianshu.com/p/f073dde56262 +- https://www.jianshu.com/p/f073dde56262 \ No newline at end of file diff --git a/docs/02.JavaEE/01.JavaWeb/README.md b/docs/01.Java/02.JavaEE/01.JavaWeb/README.md similarity index 86% rename from docs/02.JavaEE/01.JavaWeb/README.md rename to docs/01.Java/02.JavaEE/01.JavaWeb/README.md index 5f07df05..56a9b42a 100644 --- a/docs/02.JavaEE/01.JavaWeb/README.md +++ b/docs/01.Java/02.JavaEE/01.JavaWeb/README.md @@ -1,15 +1,15 @@ --- title: JavaWeb -categories: - - 编程 +date: 2020-02-07 23:04:47 +categories: - Java + - JavaEE - JavaWeb -tags: +tags: - JavaWeb +permalink: /pages/50f49f/ hidden: true -abbrlink: 99720b1c -date: 2020-02-07 23:04:47 -permalink: /pages/c1cee9/ +index: false --- # ☕ JavaWeb @@ -31,4 +31,4 @@ permalink: /pages/c1cee9/ - [深入拆解 Tomcat & Jetty](https://time.geekbang.org/column/intro/100027701) - [Servlet 教程](https://www.runoob.com/servlet/servlet-tutorial.html) - [博客园孤傲苍狼 JavaWeb 学习总结](https://www.cnblogs.com/xdp-gacl/tag/JavaWeb%E5%AD%A6%E4%B9%A0%E6%80%BB%E7%BB%93/) - - [JSP 教程](https://www.runoob.com/jsp/jsp-tutorial.html) + - [JSP 教程](https://www.runoob.com/jsp/jsp-tutorial.html) \ No newline at end of file diff --git "a/docs/02.JavaEE/02.\346\234\215\345\212\241\345\231\250/01.Tomcat/01.Tomcat\345\277\253\351\200\237\345\205\245\351\227\250.md" "b/docs/01.Java/02.JavaEE/02.\346\234\215\345\212\241\345\231\250/01.Tomcat/01.Tomcat\345\277\253\351\200\237\345\205\245\351\227\250.md" similarity index 95% rename from "docs/02.JavaEE/02.\346\234\215\345\212\241\345\231\250/01.Tomcat/01.Tomcat\345\277\253\351\200\237\345\205\245\351\227\250.md" rename to "docs/01.Java/02.JavaEE/02.\346\234\215\345\212\241\345\231\250/01.Tomcat/01.Tomcat\345\277\253\351\200\237\345\205\245\351\227\250.md" index 77264e08..05822b5e 100644 --- "a/docs/02.JavaEE/02.\346\234\215\345\212\241\345\231\250/01.Tomcat/01.Tomcat\345\277\253\351\200\237\345\205\245\351\227\250.md" +++ "b/docs/01.Java/02.JavaEE/02.\346\234\215\345\212\241\345\231\250/01.Tomcat/01.Tomcat\345\277\253\351\200\237\345\205\245\351\227\250.md" @@ -1,17 +1,18 @@ --- title: Tomcat 快速入门 -categories: - - 编程 +date: 2022-02-17 22:34:30 +order: 01 +categories: - Java + - JavaEE - 服务器 -tags: + - Tomcat +tags: - Java - JavaWeb - 服务器 - Tomcat -abbrlink: ea4ca089 -date: 2022-02-17 22:34:30 -permalink: /pages/c50d2b/ +permalink: /pages/4a4c02/ --- # Tomcat 快速入门 @@ -111,7 +112,7 @@ tar -zxf apache-tomcat-8.5.24.tar.gz 启动后,访问 `http://localhost:8080` ,可以看到 Tomcat 安装成功的测试页面。 -![img](https://raw.githubusercontent.com/dunwu/images/dev/cs/java/javaweb/tools/tomcat/tomcat.png) +![img](https://raw.githubusercontent.com/dunwu/images/master/cs/java/javaweb/tools/tomcat/tomcat.png) ### 2.2. 配置 @@ -364,7 +365,7 @@ public class SimpleTomcatServer { - 设置启动应用的端口、JVM 参数、启动浏览器等。 - 成功后,可以访问 `http://localhost:8080/`(当然,你也可以在 url 中设置上下文名称)。 -![img](https://raw.githubusercontent.com/dunwu/images/dev/cs/java/javaweb/tools/tomcat/tomcat-intellij-run-config.png) +![img](https://raw.githubusercontent.com/dunwu/images/master/cs/java/javaweb/tools/tomcat/tomcat-intellij-run-config.png) > **说明** > @@ -374,7 +375,7 @@ public class SimpleTomcatServer { ## 3. Tomcat 架构 -![img](https://raw.githubusercontent.com/dunwu/images/dev/snap/20201113193431.png) +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20201113193431.png) Tomcat 要实现 2 个核心功能: @@ -402,7 +403,7 @@ Tomcat 支持的应用层协议有: Tomcat 支持多种 I/O 模型和应用层协议。为了实现这点,一个容器可能对接多个连接器。但是,单独的连接器或容器都不能对外提供服务,需要把它们组装起来才能工作,组装后这个整体叫作 Service 组件。Tomcat 内可能有多个 Service,通过在 Tomcat 中配置多个 Service,可以实现通过不同的端口号来访问同一台机器上部署的不同应用。 -![img](https://raw.githubusercontent.com/dunwu/images/dev/snap/20201111093124.png) +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20201111093124.png) **一个 Tomcat 实例有一个或多个 Service;一个 Service 有多个 Connector 和 Container**。Connector 和 Container 之间通过标准的 ServletRequest 和 ServletResponse 通信。 @@ -418,13 +419,13 @@ Tomcat 支持多种 I/O 模型和应用层协议。为了实现这点,一个 Tomcat 设计了 3 个组件来实现这 3 个功能,分别是 **`EndPoint`**、**`Processor`** 和 **`Adapter`**。 -![img](https://raw.githubusercontent.com/dunwu/images/dev/snap/20201111101440.png) +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20201111101440.png) 组件间通过抽象接口交互。这样做还有一个好处是**封装变化。**这是面向对象设计的精髓,将系统中经常变化的部分和稳定的部分隔离,有助于增加复用性,并降低系统耦合度。网络通信的 I/O 模型是变化的,可能是非阻塞 I/O、异步 I/O 或者 APR。应用层协议也是变化的,可能是 HTTP、HTTPS、AJP。浏览器端发送的请求信息也是变化的。但是整体的处理逻辑是不变的,EndPoint 负责提供字节流给 Processor,Processor 负责提供 Tomcat Request 对象给 Adapter,Adapter 负责提供 ServletRequest 对象给容器。 如果要支持新的 I/O 方案、新的应用层协议,只需要实现相关的具体子类,上层通用的处理逻辑是不变的。由于 I/O 模型和应用层协议可以自由组合,比如 NIO + HTTP 或者 NIO2 + AJP。Tomcat 的设计者将网络通信和应用层协议解析放在一起考虑,设计了一个叫 ProtocolHandler 的接口来封装这两种变化点。各种协议和通信模型的组合有相应的具体实现类。比如:Http11NioProtocol 和 AjpNioProtocol。 -![img](https://raw.githubusercontent.com/dunwu/images/dev/snap/20201027091819.png) +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20201027091819.png) #### 3.2.1. ProtocolHandler 组件 @@ -444,7 +445,7 @@ EndPoint 是一个接口,对应的抽象实现类是 AbstractEndpoint,而 Ab Processor 是一个接口,定义了请求的处理等方法。它的抽象实现类 AbstractProcessor 对一些协议共有的属性进行封装,没有对方法进行实现。具体的实现有 AJPProcessor、HTTP11Processor 等,这些具体实现类实现了特定协议的解析方法和请求处理方式。 -![img](https://raw.githubusercontent.com/dunwu/images/dev/snap/20201113185929.png) +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20201113185929.png) 从图中我们看到,EndPoint 接收到 Socket 连接后,生成一个 SocketProcessor 任务提交到线程池去处理,SocketProcessor 的 Run 方法会调用 Processor 组件去解析应用层协议,Processor 通过解析生成 Request 对象后,会调用 Adapter 的 Service 方法。 @@ -471,7 +472,7 @@ Tomcat 是怎么确定请求是由哪个 Wrapper 容器里的 Servlet 来处理 举例来说,假如有一个网购系统,有面向网站管理人员的后台管理系统,还有面向终端客户的在线购物系统。这两个系统跑在同一个 Tomcat 上,为了隔离它们的访问域名,配置了两个虚拟域名:`manage.shopping.com`和`user.shopping.com`,网站管理人员通过`manage.shopping.com`域名访问 Tomcat 去管理用户和商品,而用户管理和商品管理是两个单独的 Web 应用。终端客户通过`user.shopping.com`域名去搜索商品和下订单,搜索功能和订单管理也是两个独立的 Web 应用。如下所示,演示了 url 应声 Servlet 的处理流程。 -![img](https://raw.githubusercontent.com/dunwu/images/dev/snap/20201113192022.jpg) +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20201113192022.jpg) 假如有用户访问一个 URL,比如图中的`http://user.shopping.com:8080/order/buy`,Tomcat 如何将这个 URL 定位到一个 Servlet 呢? @@ -490,7 +491,7 @@ Pipeline-Valve 是责任链模式,责任链模式是指在一个请求处理 先来了解一下 Valve 和 Pipeline 接口的设计: -![img](https://raw.githubusercontent.com/dunwu/images/dev/cs/java/javaweb/tools/tomcat/Pipeline与Valve.png) +![img](https://raw.githubusercontent.com/dunwu/images/master/cs/java/javaweb/tools/tomcat/Pipeline与Valve.png) - 每一个容器都有一个 Pipeline 对象,只要触发这个 Pipeline 的第一个 Valve,这个容器里 Pipeline 中的 Valve 就都会被调用到。但是,不同容器的 Pipeline 是怎么链式触发的呢,比如 Engine 中 Pipeline 需要调用下层容器 Host 中的 Pipeline。 - 这是因为 Pipeline 中还有个 getBasic 方法。这个 BasicValve 处于 Valve 链表的末端,它是 Pipeline 中必不可少的一个 Valve,负责调用下层容器的 Pipeline 里的第一个 Valve。 @@ -499,7 +500,7 @@ Pipeline-Valve 是责任链模式,责任链模式是指在一个请求处理 - 各层容器对应的 basic valve 分别是 `StandardEngineValve`、`StandardHostValve`、 `StandardContextValve`、`StandardWrapperValve`。 - 由于 Valve 是一个处理点,因此 invoke 方法就是来处理请求的。注意到 Valve 中有 getNext 和 setNext 方法,因此我们大概可以猜到有一个链表将 Valve 链起来了。 -![img](https://raw.githubusercontent.com/dunwu/images/dev/cs/java/javaweb/tools/tomcat/请求处理过程.png) +![img](https://raw.githubusercontent.com/dunwu/images/master/cs/java/javaweb/tools/tomcat/请求处理过程.png) 整个调用过程由连接器中的 Adapter 触发的,它会调用 Engine 的第一个 Valve: @@ -511,7 +512,7 @@ connector.getService().getContainer().getPipeline().getFirst().invoke(request, r ### 4.1. Tomcat 的启动过程 -![img](https://raw.githubusercontent.com/dunwu/images/dev/snap/20201118145455.png) +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20201118145455.png) 1. Tomcat 是一个 Java 程序,它的运行从执行 `startup.sh` 脚本开始。`startup.sh` 会启动一个 JVM 来运行 Tomcat 的启动类 `Bootstrap`。 2. `Bootstrap` 会初始化 Tomcat 的类加载器并实例化 `Catalina`。 @@ -731,12 +732,12 @@ ContextConfig 解析 web.xml 顺序: ### 4.3. LifeCycle -![img](https://raw.githubusercontent.com/dunwu/images/dev/snap/20201118105012.png) +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20201118105012.png) #### 4.3.1. 请求处理过程
- +
1. 根据 server.xml 配置的指定的 connector 以及端口监听 http、或者 ajp 请求 @@ -747,25 +748,25 @@ ContextConfig 解析 web.xml 顺序: ### 4.4. Connector 流程
- +
#### 4.4.1. 阻塞 IO
- +
#### 4.4.2. 非阻塞 IO
- +
#### 4.4.3. IO 多路复用
- +
阻塞与非阻塞的区别在于进行读操作和写操作的系统调用时,如果此时内核态没有数据可读或者没有缓冲空间可写时,是否阻塞。 @@ -775,7 +776,7 @@ IO 多路复用的好处在于可同时监听多个 socket 的可读和可写事 #### 4.4.4. Tomcat 各类 Connector 对比
- +
- JIO:用 java.io 编写的 TCP 模块,阻塞 IO @@ -796,7 +797,7 @@ Apache Portable Runtime 是一个高度可移植的库,它是 Apache HTTP Serv **NIO 处理相关类**
- +
Poller 线程从 EventQueue 获取 PollerEvent,并执行 PollerEvent 的 run 方法,调用 Selector 的 select 方法,如果有可读的 Socket 则创建 Http11NioProcessor,放入到线程池中执行; @@ -829,7 +830,7 @@ Note: ### 4.6. 异步 Servlet
- +
传统流程: @@ -839,7 +840,7 @@ Note: - 最后,根据处理的结果提交响应,Servlet 线程结束
- +
异步处理流程: @@ -882,4 +883,4 @@ onComplete 执行后,就不可再操作 request 和 response - [Creating a Web App with Bootstrap and Tomcat Embedded](http://www.oracle.com/webfolder/technetwork/tutorials/obe/java/basic_app_embedded_tomcat/basic_app-tomcat-embedded.html) - [Tomcat 组成与工作原理](https://juejin.im/post/58eb5fdda0bb9f00692a78fc) - [Tomcat 工作原理](https://www.ibm.com/developerworks/cn/java/j-lo-tomcat1/index.html) - - [Tomcat 设计模式分析](https://www.ibm.com/developerworks/cn/java/j-lo-tomcat2/index.html?ca=drs-) + - [Tomcat 设计模式分析](https://www.ibm.com/developerworks/cn/java/j-lo-tomcat2/index.html?ca=drs-) \ No newline at end of file diff --git "a/docs/02.JavaEE/02.\346\234\215\345\212\241\345\231\250/01.Tomcat/02.Tomcat\350\277\236\346\216\245\345\231\250.md" "b/docs/01.Java/02.JavaEE/02.\346\234\215\345\212\241\345\231\250/01.Tomcat/02.Tomcat\350\277\236\346\216\245\345\231\250.md" similarity index 98% rename from "docs/02.JavaEE/02.\346\234\215\345\212\241\345\231\250/01.Tomcat/02.Tomcat\350\277\236\346\216\245\345\231\250.md" rename to "docs/01.Java/02.JavaEE/02.\346\234\215\345\212\241\345\231\250/01.Tomcat/02.Tomcat\350\277\236\346\216\245\345\231\250.md" index d76dc06d..fbf9fe8e 100644 --- "a/docs/02.JavaEE/02.\346\234\215\345\212\241\345\231\250/01.Tomcat/02.Tomcat\350\277\236\346\216\245\345\231\250.md" +++ "b/docs/01.Java/02.JavaEE/02.\346\234\215\345\212\241\345\231\250/01.Tomcat/02.Tomcat\350\277\236\346\216\245\345\231\250.md" @@ -1,17 +1,18 @@ --- title: Tomcat连接器 -categories: - - 编程 +date: 2022-02-17 22:34:30 +order: 02 +categories: - Java + - JavaEE - 服务器 -tags: + - Tomcat +tags: - Java - JavaWeb - 服务器 - Tomcat -abbrlink: 23822cab -date: 2022-02-17 22:34:30 -permalink: /pages/3c954b/ +permalink: /pages/13f070/ --- # Tomcat 连接器 @@ -20,7 +21,7 @@ permalink: /pages/3c954b/ Tomcat 的 NioEndPoint 组件利用 Java NIO 实现了 I/O 多路复用模型。 -![img](https://raw.githubusercontent.com/dunwu/images/dev/snap/20201127094302.jpg) +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20201127094302.jpg) NioEndPoint 子组件功能简介: @@ -123,7 +124,7 @@ private final SynchronizedQueue events = new SynchronizedQueue<>(); Nio2Endpoint 工作流程跟 NioEndpoint 较为相似。 -![img](https://raw.githubusercontent.com/dunwu/images/dev/snap/20201127143839.jpg) +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20201127143839.jpg) Nio2Endpoint 子组件功能说明: @@ -218,7 +219,7 @@ Tomcat 本身是 Java 编写的,为了调用 C 语言编写的 APR,需要通 ### 3.1. AprEndpoint 工作流程 -![img](https://raw.githubusercontent.com/dunwu/images/dev/snap/20201127145740.jpg) +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20201127145740.jpg) #### 3.1.1. Acceptor @@ -282,7 +283,7 @@ java my.class 这个命令行中的`java`其实是**一个可执行程序,这个程序会创建 JVM 来加载和运行你的 Java 类**。操作系统会创建一个进程来执行这个`java`可执行程序,而每个进程都有自己的虚拟地址空间,JVM 用到的内存(包括堆、栈和方法区)就是从进程的虚拟地址空间上分配的。请你注意的是,JVM 内存只是进程空间的一部分,除此之外进程空间内还有代码段、数据段、内存映射区、内核空间等。从 JVM 的角度看,JVM 内存之外的部分叫作本地内存,C 程序代码在运行过程中用到的内存就是本地内存中分配的。下面我们通过一张图来理解一下。 -![img](https://raw.githubusercontent.com/dunwu/images/dev/snap/20201127150729.jpg) +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20201127150729.jpg) Tomcat 的 Endpoint 组件在接收网络数据时需要预先分配好一块 Buffer,所谓的 Buffer 就是字节数组`byte[]`,Java 通过 JNI 调用把这块 Buffer 的地址传给 C 代码,C 代码通过操作系统 API 读取 Socket 并把数据填充到这块 Buffer。Java NIO API 提供了两种 Buffer 来接收数据:HeapByteBuffer 和 DirectByteBuffer,下面的代码演示了如何创建两种 Buffer。 @@ -323,7 +324,7 @@ Tomcat 中的 AprEndpoint 就是通过 DirectByteBuffer 来接收数据的,而 从下面的图你会发现这个过程有 6 次内存拷贝,并且 read 和 write 等系统调用将导致进程从用户态到内核态的切换,会耗费大量的 CPU 和内存资源。 -![img](https://raw.githubusercontent.com/dunwu/images/dev/snap/20201127151041.jpg) +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20201127151041.jpg) 而 Tomcat 的 AprEndpoint 通过操作系统层面的 sendfile 特性解决了这个问题,sendfile 系统调用方式非常简洁。 @@ -337,7 +338,7 @@ sendfile(socket, file, len); 第二步:数据并没有从内核缓冲区复制到 Socket 关联的缓冲区,只有记录数据位置和长度的描述符被添加到 Socket 缓冲区中;接着把数据直接从内核缓冲区传递给网卡。这个过程你可以看下面的图。 -![img](https://raw.githubusercontent.com/dunwu/images/dev/snap/20201127151155.jpg) +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20201127151155.jpg) ## 4. Executor 组件 @@ -508,7 +509,7 @@ Tomcat 用 ProtocolHandler 组件屏蔽应用层协议的差异,其中 Protoco WebSocket 是通过 HTTP 协议来进行握手的,因此当 WebSocket 的握手请求到来时,HttpProtocolHandler 首先接收到这个请求,在处理这个 HTTP 请求时,Tomcat 通过一个特殊的 Filter 判断该当前 HTTP 请求是否是一个 WebSocket Upgrade 请求(即包含`Upgrade: websocket`的 HTTP 头信息),如果是,则在 HTTP 响应里添加 WebSocket 相关的响应头信息,并进行协议升级。具体来说就是用 UpgradeProtocolHandler 替换当前的 HttpProtocolHandler,相应的,把当前 Socket 的 Processor 替换成 UpgradeProcessor,同时 Tomcat 会创建 WebSocket Session 实例和 Endpoint 实例,并跟当前的 WebSocket 连接一一对应起来。这个 WebSocket 连接不会立即关闭,并且在请求处理中,不再使用原有的 HttpProcessor,而是用专门的 UpgradeProcessor,UpgradeProcessor 最终会调用相应的 Endpoint 实例来处理请求。 -![img](https://raw.githubusercontent.com/dunwu/images/dev/snap/20201127153521.jpg) +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20201127153521.jpg) 你可以看到,Tomcat 对 WebSocket 请求的处理没有经过 Servlet 容器,而是通过 UpgradeProcessor 组件直接把请求发到 ServerEndpoint 实例,并且 Tomcat 的 WebSocket 实现不需要关注具体 I/O 模型的细节,从而实现了与具体 I/O 方式的解耦。 @@ -519,4 +520,4 @@ WebSocket 是通过 HTTP 协议来进行握手的,因此当 WebSocket 的握 - [Tomcat Wiki](http://wiki.apache.org/tomcat/FrontPage) - [Tomee 官方网站](http://tomee.apache.org/) - **教程** - - [深入拆解 Tomcat & Jetty](https://time.geekbang.org/column/intro/100027701) + - [深入拆解 Tomcat & Jetty](https://time.geekbang.org/column/intro/100027701) \ No newline at end of file diff --git "a/docs/02.JavaEE/02.\346\234\215\345\212\241\345\231\250/01.Tomcat/03.Tomcat\345\256\271\345\231\250.md" "b/docs/01.Java/02.JavaEE/02.\346\234\215\345\212\241\345\231\250/01.Tomcat/03.Tomcat\345\256\271\345\231\250.md" similarity index 99% rename from "docs/02.JavaEE/02.\346\234\215\345\212\241\345\231\250/01.Tomcat/03.Tomcat\345\256\271\345\231\250.md" rename to "docs/01.Java/02.JavaEE/02.\346\234\215\345\212\241\345\231\250/01.Tomcat/03.Tomcat\345\256\271\345\231\250.md" index d72a1c6b..0f71d9ce 100644 --- "a/docs/02.JavaEE/02.\346\234\215\345\212\241\345\231\250/01.Tomcat/03.Tomcat\345\256\271\345\231\250.md" +++ "b/docs/01.Java/02.JavaEE/02.\346\234\215\345\212\241\345\231\250/01.Tomcat/03.Tomcat\345\256\271\345\231\250.md" @@ -1,17 +1,18 @@ --- title: Tomcat容器 -categories: - - 编程 +date: 2022-02-17 22:34:30 +order: 03 +categories: - Java + - JavaEE - 服务器 -tags: + - Tomcat +tags: - Java - JavaWeb - 服务器 - Tomcat -abbrlink: fc56014a -date: 2022-02-17 22:34:30 -permalink: /pages/2fea08/ +permalink: /pages/d5076a/ --- # Tomcat 容器 @@ -335,7 +336,7 @@ Tomcat 作为 Web 容器,需要解决以下问题: 2. 两个 Web 应用都依赖同一个第三方的 JAR 包,比如 Spring,那 Spring 的 JAR 包被加载到内存后,Tomcat 要保证这两个 Web 应用能够共享,也就是说 Spring 的 JAR 包只被加载一次,否则随着依赖的第三方 JAR 包增多,JVM 的内存会膨胀。 3. 需要隔离 Tomcat 本身的类和 Web 应用的类。 -![img](https://raw.githubusercontent.com/dunwu/images/dev/snap/20201130141536.png) +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20201130141536.png) #### WebAppClassLoader @@ -724,4 +725,4 @@ public boolean processSocket(SocketWrapperBase socketWrapper, - [Tomcat Wiki](http://wiki.apache.org/tomcat/FrontPage) - [Tomee 官方网站](http://tomee.apache.org/) - **教程** - - [深入拆解 Tomcat & Jetty](https://time.geekbang.org/column/intro/100027701) + - [深入拆解 Tomcat & Jetty](https://time.geekbang.org/column/intro/100027701) \ No newline at end of file diff --git "a/docs/02.JavaEE/02.\346\234\215\345\212\241\345\231\250/01.Tomcat/04.Tomcat\344\274\230\345\214\226.md" "b/docs/01.Java/02.JavaEE/02.\346\234\215\345\212\241\345\231\250/01.Tomcat/04.Tomcat\344\274\230\345\214\226.md" similarity index 98% rename from "docs/02.JavaEE/02.\346\234\215\345\212\241\345\231\250/01.Tomcat/04.Tomcat\344\274\230\345\214\226.md" rename to "docs/01.Java/02.JavaEE/02.\346\234\215\345\212\241\345\231\250/01.Tomcat/04.Tomcat\344\274\230\345\214\226.md" index 67cefb7e..3beb96e8 100644 --- "a/docs/02.JavaEE/02.\346\234\215\345\212\241\345\231\250/01.Tomcat/04.Tomcat\344\274\230\345\214\226.md" +++ "b/docs/01.Java/02.JavaEE/02.\346\234\215\345\212\241\345\231\250/01.Tomcat/04.Tomcat\344\274\230\345\214\226.md" @@ -1,17 +1,18 @@ --- title: Tomcat优化 -categories: - - 编程 +date: 2022-02-17 22:34:30 +order: 04 +categories: - Java + - JavaEE - 服务器 -tags: + - Tomcat +tags: - Java - JavaWeb - 服务器 - Tomcat -abbrlink: c37025e9 -date: 2022-02-17 22:34:30 -permalink: /pages/6c22f4/ +permalink: /pages/f9e1e6/ --- # Tomcat 优化 @@ -152,4 +153,4 @@ Tomcat 启动的时候,默认情况下 Web 应用都是一个一个启动的 - [Tomcat Wiki](http://wiki.apache.org/tomcat/FrontPage) - [Tomee 官方网站](http://tomee.apache.org/) - **教程** - - [深入拆解 Tomcat & Jetty](https://time.geekbang.org/column/intro/100027701) + - [深入拆解 Tomcat & Jetty](https://time.geekbang.org/column/intro/100027701) \ No newline at end of file diff --git "a/docs/02.JavaEE/02.\346\234\215\345\212\241\345\231\250/01.Tomcat/05.Tomcat\345\222\214Jetty.md" "b/docs/01.Java/02.JavaEE/02.\346\234\215\345\212\241\345\231\250/01.Tomcat/05.Tomcat\345\222\214Jetty.md" similarity index 95% rename from "docs/02.JavaEE/02.\346\234\215\345\212\241\345\231\250/01.Tomcat/05.Tomcat\345\222\214Jetty.md" rename to "docs/01.Java/02.JavaEE/02.\346\234\215\345\212\241\345\231\250/01.Tomcat/05.Tomcat\345\222\214Jetty.md" index 3bb92e84..a841530f 100644 --- "a/docs/02.JavaEE/02.\346\234\215\345\212\241\345\231\250/01.Tomcat/05.Tomcat\345\222\214Jetty.md" +++ "b/docs/01.Java/02.JavaEE/02.\346\234\215\345\212\241\345\231\250/01.Tomcat/05.Tomcat\345\222\214Jetty.md" @@ -1,18 +1,19 @@ --- title: Tomcat 和 Jetty -categories: - - 编程 +date: 2022-02-17 22:34:30 +order: 05 +categories: - Java + - JavaEE - 服务器 -tags: + - Tomcat +tags: - Java - JavaWeb - 服务器 - Tomcat - Jetty -abbrlink: a6fd024b -date: 2022-02-17 22:34:30 -permalink: /pages/f1bba6/ +permalink: /pages/f37326/ --- ## Tomcat 和 Jetty @@ -31,4 +32,4 @@ Web 容器 Tomcat 或 Jetty,作为重要的系统中间件,连接着浏览 其他应用服务器比如 JBoss 和 WebLogic,它们不仅仅有 Servlet 容器的功能,也包含 EJB 容器,是完整的 Java EE 应用服务器。从这个角度看,Tomcat 和 Jetty 算是一个轻量级的应用服务器。 -在微服务架构日渐流行的今天,开发人员更喜欢稳定的、轻量级的应用服务器,并且应用程序用内嵌的方式来运行 Servlet 容器也逐渐流行起来。之所以选择轻量级,是因为在微服务架构下,我们把一个大而全的单体应用,拆分成一个个功能单一的微服务,在这个过程中,服务的数量必然要增加,但为了减少资源的消耗,并且降低部署的成本,我们希望运行服务的 Web 容器也是轻量级的,Web 容器本身应该消耗较少的内存和 CPU 资源,并且由应用本身来启动一个嵌入式的 Web 容器,而不是通过 Web 容器来部署和启动应用,这样可以降低应用部署的复杂度。 +在微服务架构日渐流行的今天,开发人员更喜欢稳定的、轻量级的应用服务器,并且应用程序用内嵌的方式来运行 Servlet 容器也逐渐流行起来。之所以选择轻量级,是因为在微服务架构下,我们把一个大而全的单体应用,拆分成一个个功能单一的微服务,在这个过程中,服务的数量必然要增加,但为了减少资源的消耗,并且降低部署的成本,我们希望运行服务的 Web 容器也是轻量级的,Web 容器本身应该消耗较少的内存和 CPU 资源,并且由应用本身来启动一个嵌入式的 Web 容器,而不是通过 Web 容器来部署和启动应用,这样可以降低应用部署的复杂度。 \ No newline at end of file diff --git "a/docs/02.JavaEE/02.\346\234\215\345\212\241\345\231\250/01.Tomcat/README.md" "b/docs/01.Java/02.JavaEE/02.\346\234\215\345\212\241\345\231\250/01.Tomcat/README.md" similarity index 72% rename from "docs/02.JavaEE/02.\346\234\215\345\212\241\345\231\250/01.Tomcat/README.md" rename to "docs/01.Java/02.JavaEE/02.\346\234\215\345\212\241\345\231\250/01.Tomcat/README.md" index a7fc606a..6204b0c8 100644 --- "a/docs/02.JavaEE/02.\346\234\215\345\212\241\345\231\250/01.Tomcat/README.md" +++ "b/docs/01.Java/02.JavaEE/02.\346\234\215\345\212\241\345\231\250/01.Tomcat/README.md" @@ -1,18 +1,19 @@ --- title: Tomcat 教程 -categories: - - 编程 +date: 2022-02-18 08:53:11 +categories: - Java + - JavaEE - 服务器 -tags: + - Tomcat +tags: - Java - JavaWeb - 服务器 - Tomcat +permalink: /pages/33e817/ hidden: true -abbrlink: 3804ebfc -date: 2022-02-18 08:53:11 -permalink: /pages/86288e/ +index: false --- # Tomcat 教程 @@ -32,4 +33,4 @@ permalink: /pages/86288e/ ## 🚪 传送 -◾ 🏠 [JAVA-TUTORIAL 首页](https://github.com/dunwu/java-tutorial) ◾ 🎯 [我的博客](https://github.com/dunwu/blog) ◾ +◾ 💧 [钝悟的 IT 知识图谱](https://dunwu.github.io/waterdrop/) ◾ \ No newline at end of file diff --git "a/docs/02.JavaEE/02.\346\234\215\345\212\241\345\231\250/02.Jetty.md" "b/docs/01.Java/02.JavaEE/02.\346\234\215\345\212\241\345\231\250/02.Jetty.md" similarity index 99% rename from "docs/02.JavaEE/02.\346\234\215\345\212\241\345\231\250/02.Jetty.md" rename to "docs/01.Java/02.JavaEE/02.\346\234\215\345\212\241\345\231\250/02.Jetty.md" index 369b7409..db02b55c 100644 --- "a/docs/02.JavaEE/02.\346\234\215\345\212\241\345\231\250/02.Jetty.md" +++ "b/docs/01.Java/02.JavaEE/02.\346\234\215\345\212\241\345\231\250/02.Jetty.md" @@ -1,17 +1,17 @@ --- title: Jetty 快速入门 -categories: - - 编程 +date: 2022-02-17 22:34:30 +order: 02 +categories: - Java + - JavaEE - 服务器 -tags: +tags: - Java - JavaWeb - 服务器 - Jetty -abbrlink: 9709eee9 -date: 2022-02-17 22:34:30 -permalink: /pages/9ecdc1/ +permalink: /pages/ec364e/ --- # Jetty 快速入门 @@ -254,7 +254,7 @@ mvn jetty:run ### Jetty 架构简介 -![img](https://raw.githubusercontent.com/dunwu/images/dev/snap/20201127154145.jpg) +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20201127154145.jpg) Jetty Server 就是由多个 Connector(连接器)、多个 Handler(处理器),以及一个线程池组成。 @@ -379,7 +379,7 @@ getEndPoint().fillInterested(_readCallback); 到此你应该了解了 Connector 的工作原理,下面我画张图再来回顾一下 Connector 的工作流程。 -![img](https://raw.githubusercontent.com/dunwu/images/dev/snap/20201118175805.jpg) +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20201118175805.jpg) 1. Acceptor 监听连接请求,当有连接请求到达时就接受连接,一个连接对应一个 Channel,Acceptor 将 Channel 交给 ManagedSelector 来处理。 @@ -427,7 +427,7 @@ public interface Handler extends LifeCycle, Destroyable Handler 只是一个接口,完成具体功能的还是它的子类。那么 Handler 有哪些子类呢?它们的继承关系又是怎样的?这些子类是如何实现 Servlet 容器功能的呢? -![img](https://raw.githubusercontent.com/dunwu/images/dev/snap/20201118181025.png) +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20201118181025.png) 在 AbstractHandler 之下有 AbstractHandlerContainer,为什么需要这个类呢?这其实是个过渡,为了实现链式调用,一个 Handler 内部必然要有其他 Handler 的引用,所以这个类的名字里才有 Container,意思就是这样的 Handler 里包含了其他 Handler 的引用。 @@ -674,4 +674,4 @@ SelectorProducer 是 ManagedSelector 的内部类,SelectorProducer 实现了 E - [Jetty 官方网址](http://www.eclipse.org/jetty/index.html) - [Jetty Github](https://github.com/eclipse/jetty.project) -- [Jetty wiki](http://wiki.eclipse.org/Jetty/Reference/jetty-env.xml) +- [Jetty wiki](http://wiki.eclipse.org/Jetty/Reference/jetty-env.xml) \ No newline at end of file diff --git "a/docs/02.JavaEE/02.\346\234\215\345\212\241\345\231\250/README.md" "b/docs/01.Java/02.JavaEE/02.\346\234\215\345\212\241\345\231\250/README.md" similarity index 80% rename from "docs/02.JavaEE/02.\346\234\215\345\212\241\345\231\250/README.md" rename to "docs/01.Java/02.JavaEE/02.\346\234\215\345\212\241\345\231\250/README.md" index 595e3d43..9e9a4473 100644 --- "a/docs/02.JavaEE/02.\346\234\215\345\212\241\345\231\250/README.md" +++ "b/docs/01.Java/02.JavaEE/02.\346\234\215\345\212\241\345\231\250/README.md" @@ -1,17 +1,17 @@ --- title: Java 服务器 -categories: - - 编程 +date: 2022-02-17 22:34:30 +categories: - Java + - JavaEE - 服务器 -tags: +tags: - Java - JavaWeb - 服务器 +permalink: /pages/e3f3f3/ hidden: true -abbrlink: c34dbf07 -date: 2022-02-17 22:34:30 -permalink: /pages/9d17ce/ +index: false --- # Java 服务器 @@ -36,4 +36,4 @@ permalink: /pages/9d17ce/ ## 🚪 传送 -◾ 🏠 [JAVA-TUTORIAL 首页](https://github.com/dunwu/java-tutorial) ◾ 🎯 [我的博客](https://github.com/dunwu/blog) ◾ +◾ 💧 [钝悟的 IT 知识图谱](https://dunwu.github.io/waterdrop/) ◾ \ No newline at end of file diff --git a/docs/02.JavaEE/README.md b/docs/01.Java/02.JavaEE/README.md similarity index 93% rename from docs/02.JavaEE/README.md rename to docs/01.Java/02.JavaEE/README.md index d70da973..846c3677 100644 --- a/docs/02.JavaEE/README.md +++ b/docs/01.Java/02.JavaEE/README.md @@ -1,16 +1,15 @@ --- title: JavaEE -categories: - - 编程 +date: 2022-02-18 08:53:11 +categories: - Java - JavaEE -tags: +tags: - Java - JavaEE +permalink: /pages/80a822/ hidden: true -abbrlink: 37aa865c -date: 2022-02-18 08:53:11 -permalink: /pages/ca58e7/ +index: false --- # JavaEE @@ -59,4 +58,4 @@ permalink: /pages/ca58e7/ ## 🚪 传送 -◾ 💧 [钝悟的 IT 知识图谱](https://dunwu.github.io/waterdrop/) ◾ 🎯 [钝悟的博客](https://dunwu.github.io/blog/) ◾ +◾ 💧 [钝悟的 IT 知识图谱](https://dunwu.github.io/waterdrop/) ◾ \ No newline at end of file diff --git "a/docs/11.\350\275\257\344\273\266/01.\346\236\204\345\273\272/01.Maven/01.Maven\345\277\253\351\200\237\345\205\245\351\227\250.md" "b/docs/01.Java/11.\350\275\257\344\273\266/01.\346\236\204\345\273\272/01.Maven/01.Maven\345\277\253\351\200\237\345\205\245\351\227\250.md" similarity index 96% rename from "docs/11.\350\275\257\344\273\266/01.\346\236\204\345\273\272/01.Maven/01.Maven\345\277\253\351\200\237\345\205\245\351\227\250.md" rename to "docs/01.Java/11.\350\275\257\344\273\266/01.\346\236\204\345\273\272/01.Maven/01.Maven\345\277\253\351\200\237\345\205\245\351\227\250.md" index 265088c7..b2efbdda 100644 --- "a/docs/11.\350\275\257\344\273\266/01.\346\236\204\345\273\272/01.Maven/01.Maven\345\277\253\351\200\237\345\205\245\351\227\250.md" +++ "b/docs/01.Java/11.\350\275\257\344\273\266/01.\346\236\204\345\273\272/01.Maven/01.Maven\345\277\253\351\200\237\345\205\245\351\227\250.md" @@ -1,17 +1,17 @@ --- title: Maven 快速入门 -categories: - - 编程 +date: 2020-02-07 23:04:47 +order: 01 +categories: - Java - 软件 - 构建 -tags: + - Maven +tags: - Java - 构建 - Maven -abbrlink: ad26e4b1 -date: 2020-02-07 23:04:47 -permalink: /pages/6b8149/ +permalink: /pages/e5b79f/ --- # Maven 快速入门 @@ -135,9 +135,9 @@ export PATH=$MAVEN_HOME/bin:$PATH 右键 "计算机",选择 "属性",之后点击 "高级系统设置",点击"环境变量",来设置环境变量,有以下系统变量需要配置: -![img](https://raw.githubusercontent.com/dunwu/images/dev/snap/20200108143017.png) +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200108143017.png) -![img](https://raw.githubusercontent.com/dunwu/images/dev/snap/20200108143038.png) +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200108143038.png) ### 检测安装成功 @@ -265,15 +265,15 @@ java -cp target/my-app-1.0-SNAPSHOT.jar com.mycompany.app.App 依次点击 File -> New -> Project 打开创建工程对话框,选择 Maven 工程。 -![img](https://raw.githubusercontent.com/dunwu/images/dev/snap/1555414103572.png) +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/1555414103572.png) (2)输入项目信息 -![img](https://raw.githubusercontent.com/dunwu/images/dev/snap/1555415549748.png) +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/1555415549748.png) (3)点击 Intellij 侧边栏中的 Maven 工具界面,有几个可以直接使用的 maven 命令,可以帮助你进行构建。 -![img](https://raw.githubusercontent.com/dunwu/images/dev/snap/1555415806237.png) +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/1555415806237.png) ### 在 Eclipse 中创建 Maven 工程 @@ -285,7 +285,7 @@ java -cp target/my-app-1.0-SNAPSHOT.jar com.mycompany.app.App 点击 Help -> Eclipse Marketplace,搜索 maven 关键字,选择安装红框对应的 Maven 插件。 -![img](https://raw.githubusercontent.com/dunwu/images/dev/snap/20181127195117.png) +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20181127195117.png) (2)Maven 环境配置 @@ -293,7 +293,7 @@ java -cp target/my-app-1.0-SNAPSHOT.jar com.mycompany.app.App 如下图所示,配置 settings.xml 文件的位置 -![img](https://raw.githubusercontent.com/dunwu/images/dev/snap/20181127195128.png) +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20181127195128.png) (3)创建 Maven 工程 @@ -301,7 +301,7 @@ File -> New -> Maven Project -> Next,在接下来的窗口中会看到一大 接下来设置项目的参数,如下: -![img](https://raw.githubusercontent.com/dunwu/images/dev/snap/20181127195151.png) +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20181127195151.png) **groupId**是项目组织唯一的标识符,实际对应 JAVA 的包的结构,是 main 目录里 java 的目录结构。 @@ -315,11 +315,11 @@ Eclipse 中构建方式: 在 Elipse 项目上右击 -> Run As 就能看到很多 Maven 操作。这些操作和 maven 命令是等效的。例如 Maven clean,等同于 mvn clean 命令。 -![img](https://raw.githubusercontent.com/dunwu/images/dev/snap/20181127195208.png) +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20181127195208.png) 你也可以点击 Maven build,输入组合命令,并保存下来。如下图: -![img](https://raw.githubusercontent.com/dunwu/images/dev/snap/20181127195219.png) +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20181127195219.png) Maven 命令构建方式: @@ -327,7 +327,7 @@ Maven 命令构建方式: 进入工程所在目录,输入 maven 命令就可以了。 -![img](https://raw.githubusercontent.com/dunwu/images/dev/snap/20181127195243.png) +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20181127195243.png) ## 使用说明 @@ -554,4 +554,4 @@ mvn clean install -Dmaven.test.skip=true -B -U - [Maven in 5 Minutes](https://maven.apache.org/guides/getting-started/maven-in-five-minutes.html) - [Maven Getting Started Guide](https://maven.apache.org/guides/getting-started/index.html) - [maven 常见问题问答](http://www.oschina.net/question/158170_29368) -- [常用 Maven 插件介绍](https://www.cnblogs.com/crazy-fox/archive/2012/02/09/2343722.html) +- [常用 Maven 插件介绍](https://www.cnblogs.com/crazy-fox/archive/2012/02/09/2343722.html) \ No newline at end of file diff --git "a/docs/11.\350\275\257\344\273\266/01.\346\236\204\345\273\272/01.Maven/02.Maven\346\225\231\347\250\213\344\271\213pom.xml\350\257\246\350\247\243.md" "b/docs/01.Java/11.\350\275\257\344\273\266/01.\346\236\204\345\273\272/01.Maven/02.Maven\346\225\231\347\250\213\344\271\213pom.xml\350\257\246\350\247\243.md" similarity index 99% rename from "docs/11.\350\275\257\344\273\266/01.\346\236\204\345\273\272/01.Maven/02.Maven\346\225\231\347\250\213\344\271\213pom.xml\350\257\246\350\247\243.md" rename to "docs/01.Java/11.\350\275\257\344\273\266/01.\346\236\204\345\273\272/01.Maven/02.Maven\346\225\231\347\250\213\344\271\213pom.xml\350\257\246\350\247\243.md" index 60c8a837..d19300f4 100644 --- "a/docs/11.\350\275\257\344\273\266/01.\346\236\204\345\273\272/01.Maven/02.Maven\346\225\231\347\250\213\344\271\213pom.xml\350\257\246\350\247\243.md" +++ "b/docs/01.Java/11.\350\275\257\344\273\266/01.\346\236\204\345\273\272/01.Maven/02.Maven\346\225\231\347\250\213\344\271\213pom.xml\350\257\246\350\247\243.md" @@ -1,17 +1,17 @@ --- title: Maven 教程之 pom.xml 详解 -categories: - - 编程 +date: 2019-05-14 14:57:33 +order: 02 +categories: - Java - 软件 - 构建 -tags: + - Maven +tags: - Java - 构建 - Maven -abbrlink: 15944f49 -date: 2019-05-14 14:57:33 -permalink: /pages/f594fa/ +permalink: /pages/d893c2/ --- # Maven 教程之 pom.xml 详解 @@ -799,4 +799,4 @@ POM 执行的预设条件。 ## 参考资料 -- [maven 官方文档之 pom](https://maven.apache.org/pom.html) +- [maven 官方文档之 pom](https://maven.apache.org/pom.html) \ No newline at end of file diff --git "a/docs/11.\350\275\257\344\273\266/01.\346\236\204\345\273\272/01.Maven/03.Maven\346\225\231\347\250\213\344\271\213settings.xml\350\257\246\350\247\243.md" "b/docs/01.Java/11.\350\275\257\344\273\266/01.\346\236\204\345\273\272/01.Maven/03.Maven\346\225\231\347\250\213\344\271\213settings.xml\350\257\246\350\247\243.md" similarity index 99% rename from "docs/11.\350\275\257\344\273\266/01.\346\236\204\345\273\272/01.Maven/03.Maven\346\225\231\347\250\213\344\271\213settings.xml\350\257\246\350\247\243.md" rename to "docs/01.Java/11.\350\275\257\344\273\266/01.\346\236\204\345\273\272/01.Maven/03.Maven\346\225\231\347\250\213\344\271\213settings.xml\350\257\246\350\247\243.md" index 09f242d8..9fce2931 100644 --- "a/docs/11.\350\275\257\344\273\266/01.\346\236\204\345\273\272/01.Maven/03.Maven\346\225\231\347\250\213\344\271\213settings.xml\350\257\246\350\247\243.md" +++ "b/docs/01.Java/11.\350\275\257\344\273\266/01.\346\236\204\345\273\272/01.Maven/03.Maven\346\225\231\347\250\213\344\271\213settings.xml\350\257\246\350\247\243.md" @@ -1,17 +1,17 @@ --- title: Maven 教程之 settings.xml 详解 -categories: - - 编程 +date: 2019-05-14 14:57:33 +order: 03 +categories: - Java - 软件 - 构建 -tags: + - Maven +tags: - Java - 构建 - Maven -abbrlink: e9105404 -date: 2019-05-14 14:57:33 -permalink: /pages/d05c38/ +permalink: /pages/1d58f1/ --- # Maven 教程之 settings.xml 详解 @@ -406,4 +406,4 @@ maven 插件是一种特殊类型的构件。由于这个原因,插件仓库 ## 参考资料 -- [maven 官方文档之 settings](https://maven.apache.org/settings.html) +- [maven 官方文档之 settings](https://maven.apache.org/settings.html) \ No newline at end of file diff --git "a/docs/11.\350\275\257\344\273\266/01.\346\236\204\345\273\272/01.Maven/04.Maven\345\256\236\346\210\230\351\227\256\351\242\230\345\222\214\346\234\200\344\275\263\345\256\236\350\267\265.md" "b/docs/01.Java/11.\350\275\257\344\273\266/01.\346\236\204\345\273\272/01.Maven/04.Maven\345\256\236\346\210\230\351\227\256\351\242\230\345\222\214\346\234\200\344\275\263\345\256\236\350\267\265.md" similarity index 94% rename from "docs/11.\350\275\257\344\273\266/01.\346\236\204\345\273\272/01.Maven/04.Maven\345\256\236\346\210\230\351\227\256\351\242\230\345\222\214\346\234\200\344\275\263\345\256\236\350\267\265.md" rename to "docs/01.Java/11.\350\275\257\344\273\266/01.\346\236\204\345\273\272/01.Maven/04.Maven\345\256\236\346\210\230\351\227\256\351\242\230\345\222\214\346\234\200\344\275\263\345\256\236\350\267\265.md" index ee3b4efd..af20c6e4 100644 --- "a/docs/11.\350\275\257\344\273\266/01.\346\236\204\345\273\272/01.Maven/04.Maven\345\256\236\346\210\230\351\227\256\351\242\230\345\222\214\346\234\200\344\275\263\345\256\236\350\267\265.md" +++ "b/docs/01.Java/11.\350\275\257\344\273\266/01.\346\236\204\345\273\272/01.Maven/04.Maven\345\256\236\346\210\230\351\227\256\351\242\230\345\222\214\346\234\200\344\275\263\345\256\236\350\267\265.md" @@ -1,17 +1,17 @@ --- title: Maven 实战问题和最佳实践 -categories: - - 编程 +date: 2018-11-28 09:29:22 +order: 04 +categories: - Java - 软件 - 构建 -tags: + - Maven +tags: - Java - 构建 - Maven -abbrlink: 684ab4a9 -date: 2018-11-28 09:29:22 -permalink: /pages/7908f2/ +permalink: /pages/198618/ --- # Maven 实战问题和最佳实践 @@ -48,21 +48,21 @@ maven 的 JDK 源与指定的 JDK 编译版本不符。 Project SDK 是否正确 -![img](https://raw.githubusercontent.com/dunwu/images/dev/snap/20181127203324.png) +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20181127203324.png) SDK 路径是否正确 -![img](https://raw.githubusercontent.com/dunwu/images/dev/snap/20181127203427.png) +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20181127203427.png) - **查看 Settings > Maven 的配置** JDK for importer 是否正确 -![img](https://raw.githubusercontent.com/dunwu/images/dev/snap/20181127203408.png) +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20181127203408.png) Runner 是否正确 -![img](https://raw.githubusercontent.com/dunwu/images/dev/snap/20181127203439.png) +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20181127203439.png) ### 重复引入依赖 @@ -292,4 +292,4 @@ spring-boot-dependencies 的 pom.xml 形式: -``` +``` \ No newline at end of file diff --git "a/docs/11.\350\275\257\344\273\266/01.\346\236\204\345\273\272/01.Maven/05.Maven\346\225\231\347\250\213\344\271\213\345\217\221\345\270\203jar\345\210\260\347\247\201\346\234\215\346\210\226\344\270\255\345\244\256\344\273\223\345\272\223.md" "b/docs/01.Java/11.\350\275\257\344\273\266/01.\346\236\204\345\273\272/01.Maven/05.Maven\346\225\231\347\250\213\344\271\213\345\217\221\345\270\203jar\345\210\260\347\247\201\346\234\215\346\210\226\344\270\255\345\244\256\344\273\223\345\272\223.md" similarity index 97% rename from "docs/11.\350\275\257\344\273\266/01.\346\236\204\345\273\272/01.Maven/05.Maven\346\225\231\347\250\213\344\271\213\345\217\221\345\270\203jar\345\210\260\347\247\201\346\234\215\346\210\226\344\270\255\345\244\256\344\273\223\345\272\223.md" rename to "docs/01.Java/11.\350\275\257\344\273\266/01.\346\236\204\345\273\272/01.Maven/05.Maven\346\225\231\347\250\213\344\271\213\345\217\221\345\270\203jar\345\210\260\347\247\201\346\234\215\346\210\226\344\270\255\345\244\256\344\273\223\345\272\223.md" index 32416c41..2f29b940 100644 --- "a/docs/11.\350\275\257\344\273\266/01.\346\236\204\345\273\272/01.Maven/05.Maven\346\225\231\347\250\213\344\271\213\345\217\221\345\270\203jar\345\210\260\347\247\201\346\234\215\346\210\226\344\270\255\345\244\256\344\273\223\345\272\223.md" +++ "b/docs/01.Java/11.\350\275\257\344\273\266/01.\346\236\204\345\273\272/01.Maven/05.Maven\346\225\231\347\250\213\344\271\213\345\217\221\345\270\203jar\345\210\260\347\247\201\346\234\215\346\210\226\344\270\255\345\244\256\344\273\223\345\272\223.md" @@ -1,17 +1,17 @@ --- title: Maven 教程之发布 jar 到私服或中央仓库 -categories: - - 编程 +date: 2019-05-14 14:57:33 +order: 05 +categories: - Java - 软件 - 构建 -tags: + - Maven +tags: - Java - 构建 - Maven -abbrlink: b88f29ef -date: 2019-05-14 14:57:33 -permalink: /pages/2ddf04/ +permalink: /pages/7bdaf9/ --- # Maven 教程之发布 jar 到私服或中央仓库 @@ -32,7 +32,7 @@ permalink: /pages/2ddf04/ 注册账号成功后,根据你 Java 包的功能分别写上`Summary`、`Description`、`Group Id`、`SCM url`以及`Project URL`等必要信息,可以参见我之前创建的 Issue:[OSSRH-36187](https://issues.sonatype.org/browse/OSSRH-36187)。 -![img](https://raw.githubusercontent.com/dunwu/images/dev/snap/20181106143734.png) +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20181106143734.png) 创建完之后需要等待 Sonatype 的工作人员审核处理,审核时间还是很快的,我的审核差不多等待了两小时。当 Issue 的 Status 变为`RESOLVED`后,就可以进行下一步操作了。 @@ -308,7 +308,7 @@ gpg: unchanged: 1 进入[官方下载地址](https://www.sonatype.com/download-oss-sonatype),选择合适版本下载。 -![img](https://raw.githubusercontent.com/dunwu/images/dev/snap/20181127203029.png) +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20181127203029.png) 本人希望将 Nexus 部署在 Linux 机器,所以选用的是 Unix 版本。 @@ -340,13 +340,13 @@ Usage: ./nexus {start|stop|run|run-redirect|status|restart|force-reload} 启动成功后,在浏览器中访问 `http://:8081`,欢迎页面如下图所示: -![img](https://raw.githubusercontent.com/dunwu/images/dev/snap/20181127203131.png) +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20181127203131.png) 点击右上角 Sign in 登录,默认用户名/密码为:admin/admin123。 有必要提一下的是,在 Nexus 的 Repositories 管理页面,展示了可用的 maven 仓库,如下图所示: -![img](https://raw.githubusercontent.com/dunwu/images/dev/snap/20181127203156.png) +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20181127203156.png) > 说明: > @@ -476,4 +476,4 @@ $ mvn clean deploy -Dmaven.skip.test=true -P zp - http://www.ruanyifeng.com/blog/2013/07/gpg.html - https://www.cnblogs.com/hoobey/p/6102382.html - https://blog.csdn.net/wzygis/article/details/49276779 -- https://blog.csdn.net/clj198606061111/article/details/52200928 +- https://blog.csdn.net/clj198606061111/article/details/52200928 \ No newline at end of file diff --git "a/docs/11.\350\275\257\344\273\266/01.\346\236\204\345\273\272/01.Maven/06.Maven\346\217\222\344\273\266\344\271\213\344\273\243\347\240\201\346\243\200\346\237\245.md" "b/docs/01.Java/11.\350\275\257\344\273\266/01.\346\236\204\345\273\272/01.Maven/06.Maven\346\217\222\344\273\266\344\271\213\344\273\243\347\240\201\346\243\200\346\237\245.md" similarity index 98% rename from "docs/11.\350\275\257\344\273\266/01.\346\236\204\345\273\272/01.Maven/06.Maven\346\217\222\344\273\266\344\271\213\344\273\243\347\240\201\346\243\200\346\237\245.md" rename to "docs/01.Java/11.\350\275\257\344\273\266/01.\346\236\204\345\273\272/01.Maven/06.Maven\346\217\222\344\273\266\344\271\213\344\273\243\347\240\201\346\243\200\346\237\245.md" index c1484499..561aa81a 100644 --- "a/docs/11.\350\275\257\344\273\266/01.\346\236\204\345\273\272/01.Maven/06.Maven\346\217\222\344\273\266\344\271\213\344\273\243\347\240\201\346\243\200\346\237\245.md" +++ "b/docs/01.Java/11.\350\275\257\344\273\266/01.\346\236\204\345\273\272/01.Maven/06.Maven\346\217\222\344\273\266\344\271\213\344\273\243\347\240\201\346\243\200\346\237\245.md" @@ -1,17 +1,17 @@ --- title: Maven 插件之代码检查 -categories: - - 编程 +date: 2019-12-16 17:09:26 +order: 06 +categories: - Java - 软件 - 构建 -tags: + - Maven +tags: - Java - 构建 - Maven -abbrlink: 7b76d24a -date: 2019-12-16 17:09:26 -permalink: /pages/149013/ +permalink: /pages/370f1d/ --- # Maven 插件之代码检查 @@ -89,7 +89,7 @@ permalink: /pages/149013/ scope: 可以检查的方法的范围,例如:public只能检查public修饰的方法,private可以检查所有的方法 allowMissingParamTags: 是否忽略对参数注释的检查 allowMissingThrowsTags: 是否忽略对throws注释的检查 - allowMissingReturnTag: 是否忽略对return注释的检查 --> + allowMissingReturntags: 是否忽略对return注释的检查 --> @@ -427,4 +427,4 @@ permalink: /pages/149013/ - https://www.jianshu.com/p/557b975ae40d - 阿里巴巴编程规范 - https://github.com/alibaba/p3c - - https://github.com/alibaba/p3c/blob/master/p3c-pmd/pom.xml + - https://github.com/alibaba/p3c/blob/master/p3c-pmd/pom.xml \ No newline at end of file diff --git "a/docs/11.\350\275\257\344\273\266/01.\346\236\204\345\273\272/01.Maven/README.md" "b/docs/01.Java/11.\350\275\257\344\273\266/01.\346\236\204\345\273\272/01.Maven/README.md" similarity index 90% rename from "docs/11.\350\275\257\344\273\266/01.\346\236\204\345\273\272/01.Maven/README.md" rename to "docs/01.Java/11.\350\275\257\344\273\266/01.\346\236\204\345\273\272/01.Maven/README.md" index 3e7f7d5f..179cb604 100644 --- "a/docs/11.\350\275\257\344\273\266/01.\346\236\204\345\273\272/01.Maven/README.md" +++ "b/docs/01.Java/11.\350\275\257\344\273\266/01.\346\236\204\345\273\272/01.Maven/README.md" @@ -1,18 +1,18 @@ --- title: Maven 教程 -categories: - - 编程 +date: 2020-08-04 15:20:54 +categories: - Java - 软件 - 构建 -tags: + - Maven +tags: - Java - 构建 - Maven +permalink: /pages/85f27a/ hidden: true -abbrlink: a96d715 -date: 2020-08-04 15:20:54 -permalink: /pages/14735b/ +index: false --- # Maven 教程 @@ -46,4 +46,4 @@ permalink: /pages/14735b/ ## 🚪 传送 -◾ 🏠 [JAVA-TUTORIAL 首页](https://github.com/dunwu/java-tutorial) ◾ 🎯 [我的博客](https://github.com/dunwu/blog) ◾ +◾ 💧 [钝悟的 IT 知识图谱](https://dunwu.github.io/waterdrop/) ◾ \ No newline at end of file diff --git "a/docs/11.\350\275\257\344\273\266/01.\346\236\204\345\273\272/02.Ant.md" "b/docs/01.Java/11.\350\275\257\344\273\266/01.\346\236\204\345\273\272/02.Ant.md" similarity index 99% rename from "docs/11.\350\275\257\344\273\266/01.\346\236\204\345\273\272/02.Ant.md" rename to "docs/01.Java/11.\350\275\257\344\273\266/01.\346\236\204\345\273\272/02.Ant.md" index 19493eb0..d0b8d139 100644 --- "a/docs/11.\350\275\257\344\273\266/01.\346\236\204\345\273\272/02.Ant.md" +++ "b/docs/01.Java/11.\350\275\257\344\273\266/01.\346\236\204\345\273\272/02.Ant.md" @@ -1,17 +1,16 @@ --- title: Ant 简易教程 -categories: - - 编程 +date: 2017-12-06 09:46:28 +order: 02 +categories: - Java - 软件 - 构建 -tags: +tags: - Java - 构建 - Ant -abbrlink: 389d1b12 -date: 2017-12-06 09:46:28 -permalink: /pages/e2af3a/ +permalink: /pages/0bafae/ --- # Ant 简易教程 @@ -378,4 +377,4 @@ _——Ant1.8.0 新增特性。_ ## 参考资料 - [ant 官方手册](http://ant.apache.org/manual/index.html) -- [http://www.blogjava.net/amigoxie/archive/2007/11/09/159413.html](http://www.blogjava.net/amigoxie/archive/2007/11/09/159413.html) +- [http://www.blogjava.net/amigoxie/archive/2007/11/09/159413.html](http://www.blogjava.net/amigoxie/archive/2007/11/09/159413.html) \ No newline at end of file diff --git "a/docs/11.\350\275\257\344\273\266/01.\346\236\204\345\273\272/README.md" "b/docs/01.Java/11.\350\275\257\344\273\266/01.\346\236\204\345\273\272/README.md" similarity index 87% rename from "docs/11.\350\275\257\344\273\266/01.\346\236\204\345\273\272/README.md" rename to "docs/01.Java/11.\350\275\257\344\273\266/01.\346\236\204\345\273\272/README.md" index 388ac2d8..08c9c99a 100644 --- "a/docs/11.\350\275\257\344\273\266/01.\346\236\204\345\273\272/README.md" +++ "b/docs/01.Java/11.\350\275\257\344\273\266/01.\346\236\204\345\273\272/README.md" @@ -1,17 +1,16 @@ --- title: Java 构建 -categories: - - 编程 +date: 2020-08-04 15:20:54 +categories: - Java - 软件 - 构建 -tags: +tags: - Java - 构建 -abbrlink: 7ebcc307 -date: 2020-08-04 15:20:54 +permalink: /pages/d1859b/ hidden: true -permalink: /pages/95388e/ +index: false --- # Java 构建 @@ -48,4 +47,4 @@ permalink: /pages/95388e/ ## 🚪 传送 -◾ 🏠 [JAVA-TUTORIAL 首页](https://github.com/dunwu/java-tutorial) ◾ 🎯 [我的博客](https://github.com/dunwu/blog) ◾ +◾ 💧 [钝悟的 IT 知识图谱](https://dunwu.github.io/waterdrop/) ◾ \ No newline at end of file diff --git "a/docs/11.\350\275\257\344\273\266/02.IDE/01.Intellij.md" "b/docs/01.Java/11.\350\275\257\344\273\266/02.IDE/01.Intellij.md" similarity index 94% rename from "docs/11.\350\275\257\344\273\266/02.IDE/01.Intellij.md" rename to "docs/01.Java/11.\350\275\257\344\273\266/02.IDE/01.Intellij.md" index 66aa6402..85b4f254 100644 --- "a/docs/11.\350\275\257\344\273\266/02.IDE/01.Intellij.md" +++ "b/docs/01.Java/11.\350\275\257\344\273\266/02.IDE/01.Intellij.md" @@ -1,16 +1,16 @@ --- title: Intellij IDEA 快速入门 -categories: - - 编程 +date: 2019-11-29 18:10:14 +order: 01 +categories: - Java - 软件 - IDE -tags: +tags: - Java - IDE -abbrlink: a688dbc5 -date: 2019-11-29 18:10:14 -permalink: /pages/ea83ee/ + - Intellij +permalink: /pages/ac5c6a/ --- # Intellij IDEA 快速入门 @@ -200,10 +200,10 @@ IntelliJ IDEA 作为一个以快捷键为中心的 IDE,为大多数操作建 #### VCS/Local History | 快捷键 | 介绍 | -| --------------- | -------------------------------------------------- | ---------------------------------------- | +| --------------- | -------------------------------------------------- | | Ctrl + K | 版本控制提交项目,需要此项目有加入到版本控制才可用 | | Ctrl + T | 版本控制更新项目,需要此项目有加入到版本控制才可用 | -| `Alt + | ` | 显示版本控制常用操作菜单弹出层`(必备)` | +| `Alt + |` | 显示版本控制常用操作菜单弹出层`(必备)` | | Alt + Shift + C | 查看最近操作项目的变化情况列表 | | Alt + Shift + N | 选择/添加 task`(必备)` | @@ -256,29 +256,23 @@ IntelliJ IDEA 作为一个以快捷键为中心的 IDE,为大多数操作建 [下载地址](https://github.com/altercation/solarized) -## 破解 +## FAQ -Intellij 是一个收费的 IDE,坦白说有点小贵,买不起。 +(1)运行时报错 -所以,很惭愧,只好用下破解方法了。网上有很多使用注册码的网文,但是注册码不稳定,随时可能被封。还是自行搭建一个注册服务器比较稳定。我使用了 [ilanyu](http://blog.lanyus.com/) 博文 [IntelliJ IDEA License Server 本地搭建教程](http://blog.lanyus.com/archives/174.html) 的方法,亲测十分有效。 +> Error running XXX. Command line is too long. Shorten the command line via JAR manifest or via a classpath file and rerun -我的备用地址:[百度云盘](https://yun.baidu.com/disk/home?#list/vmode=list&path=%2F%E8%BD%AF%E4%BB%B6%2F%E5%BC%80%E5%8F%91%E8%BD%AF%E4%BB%B6%2FIDE) +解决方案: -下载并解压文中的压缩包到本地,选择适合操作系统的版本运行。 +找到 `.idea/libraies/workspace.xml` 中的 `` -如果是在 Linux 上运行,推荐创建一个脚本,代码如下: +添加一行配置: -```bash -# 使用 nohup 创建守护进程,运行 IntelliJIDEALicenseServer_linux_amd64 -# 如果运行在其他 Linux 发行版本,替换执行的脚本即可 -nohup sh IntelliJIDEALicenseServer_linux_amd64 2>&1 +```xml + ``` -这样做是因为:大部分人使用 linux 是使用仿真器连接虚拟机,如果断开连接,进程也会被 kill,每次启动这个注册服务器很麻烦不是吗?而启动了守护进程,则不会出现这种情况,只有你主动 kill 进程才能将其干掉。 - -Windows 版本是 exe 程序,将其设为开机自动启动即可,别告诉我你不知道怎么设置开机自动启动。 - ## 参考资料 - [IntelliJ-IDEA-Tutorial](https://github.com/judasn/IntelliJ-IDEA-Tutorial) -- [极客学院 - Intellij IDEA 使用教程](http://wiki.jikexueyuan.com/project/intellij-idea-tutorial/) +- [极客学院 - Intellij IDEA 使用教程](http://wiki.jikexueyuan.com/project/intellij-idea-tutorial/) \ No newline at end of file diff --git "a/docs/11.\350\275\257\344\273\266/02.IDE/02.Eclipse.md" "b/docs/01.Java/11.\350\275\257\344\273\266/02.IDE/02.Eclipse.md" similarity index 99% rename from "docs/11.\350\275\257\344\273\266/02.IDE/02.Eclipse.md" rename to "docs/01.Java/11.\350\275\257\344\273\266/02.IDE/02.Eclipse.md" index e75751cc..d8f3730d 100644 --- "a/docs/11.\350\275\257\344\273\266/02.IDE/02.Eclipse.md" +++ "b/docs/01.Java/11.\350\275\257\344\273\266/02.IDE/02.Eclipse.md" @@ -1,16 +1,15 @@ --- title: Eclipse 快速入门 -categories: - - 编程 +date: 2018-07-01 11:27:47 +order: 02 +categories: - Java - 软件 - IDE -tags: +tags: - Java - IDE -abbrlink: bd5534bf -date: 2018-07-01 11:27:47 -permalink: /pages/a897a9/ +permalink: /pages/2257c7/ --- # Eclipse 快速入门 @@ -245,4 +244,4 @@ http://tomcat.apache.org/download-70.cgi这里有Tomcat的安装包和源码包 | Ctrl+T | 快速显示当前类的继承结构 | | Ctrl+W | 关闭当前 Editer | | Ctrl+L | 文本编辑器 转至行 | -| F2 | 显示工具提示描述 | +| F2 | 显示工具提示描述 | \ No newline at end of file diff --git "a/docs/11.\350\275\257\344\273\266/02.IDE/03.VsCode.md" "b/docs/01.Java/11.\350\275\257\344\273\266/02.IDE/03.VsCode.md" similarity index 88% rename from "docs/11.\350\275\257\344\273\266/02.IDE/03.VsCode.md" rename to "docs/01.Java/11.\350\275\257\344\273\266/02.IDE/03.VsCode.md" index 4cf7ea00..909e80c5 100644 --- "a/docs/11.\350\275\257\344\273\266/02.IDE/03.VsCode.md" +++ "b/docs/01.Java/11.\350\275\257\344\273\266/02.IDE/03.VsCode.md" @@ -1,16 +1,15 @@ --- title: Vscode 快速入门 -categories: - - 编程 +date: 2019-05-14 14:57:33 +order: 03 +categories: - Java - 软件 - IDE -tags: +tags: - Java - IDE -abbrlink: 849a3ae4 -date: 2019-05-14 14:57:33 -permalink: /pages/a537c7/ +permalink: /pages/0f7153/ --- # Vscode 快速入门 @@ -59,4 +58,4 @@ permalink: /pages/a537c7/ - https://github.com/Microsoft/vscode-docs - https://github.com/Microsoft/vscode-tips-and-tricks - 更多资源 - - https://github.com/viatsko/awesome-vscode + - https://github.com/viatsko/awesome-vscode \ No newline at end of file diff --git "a/docs/11.\350\275\257\344\273\266/02.IDE/README.md" "b/docs/01.Java/11.\350\275\257\344\273\266/02.IDE/README.md" similarity index 87% rename from "docs/11.\350\275\257\344\273\266/02.IDE/README.md" rename to "docs/01.Java/11.\350\275\257\344\273\266/02.IDE/README.md" index afd8b09b..e487ace7 100644 --- "a/docs/11.\350\275\257\344\273\266/02.IDE/README.md" +++ "b/docs/01.Java/11.\350\275\257\344\273\266/02.IDE/README.md" @@ -1,17 +1,16 @@ --- title: Java IDE -categories: - - 编程 +date: 2022-02-18 08:53:11 +categories: - Java - 软件 - IDE -tags: +tags: - Java - IDE -abbrlink: c44dd053 -date: 2022-02-18 08:53:11 +permalink: /pages/8695a7/ hidden: true -permalink: /pages/2df5ea/ +index: false --- # Java IDE @@ -24,4 +23,4 @@ permalink: /pages/2df5ea/ - [Intellij IDEA 快速入门](01.Intellij.md) - [Eclipse 快速入门](02.Eclipse.md) -- [Vscode 快速入门](03.VsCode.md) +- [Vscode 快速入门](03.VsCode.md) \ No newline at end of file diff --git "a/docs/11.\350\275\257\344\273\266/03.\347\233\221\346\216\247\350\257\212\346\226\255/01.\347\233\221\346\216\247\345\267\245\345\205\267\345\257\271\346\257\224.md" "b/docs/01.Java/11.\350\275\257\344\273\266/03.\347\233\221\346\216\247\350\257\212\346\226\255/01.\347\233\221\346\216\247\345\267\245\345\205\267\345\257\271\346\257\224.md" similarity index 57% rename from "docs/11.\350\275\257\344\273\266/03.\347\233\221\346\216\247\350\257\212\346\226\255/01.\347\233\221\346\216\247\345\267\245\345\205\267\345\257\271\346\257\224.md" rename to "docs/01.Java/11.\350\275\257\344\273\266/03.\347\233\221\346\216\247\350\257\212\346\226\255/01.\347\233\221\346\216\247\345\267\245\345\205\267\345\257\271\346\257\224.md" index 2c1d9e94..7ccdb465 100644 --- "a/docs/11.\350\275\257\344\273\266/03.\347\233\221\346\216\247\350\257\212\346\226\255/01.\347\233\221\346\216\247\345\267\245\345\205\267\345\257\271\346\257\224.md" +++ "b/docs/01.Java/11.\350\275\257\344\273\266/03.\347\233\221\346\216\247\350\257\212\346\226\255/01.\347\233\221\346\216\247\345\267\245\345\205\267\345\257\271\346\257\224.md" @@ -1,33 +1,32 @@ --- title: 监控工具对比 -categories: - - 编程 +date: 2020-02-11 17:48:32 +order: 01 +categories: - Java - 软件 - 监控诊断 -tags: +tags: - Java - 监控 -abbrlink: 7331d4e5 -date: 2020-02-11 17:48:32 -permalink: /pages/c7c5ec/ +permalink: /pages/16563a/ --- # 监控工具对比 ## 监控工具发展史 -![img](https://raw.githubusercontent.com/dunwu/images/dev/snap/20200211165813.png) +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200211165813.png) ## 监控工具比对 ### 特性对比 -![img](https://raw.githubusercontent.com/dunwu/images/dev/snap/20200211171551.png) +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200211171551.png) ### 生态对比 -![img](https://raw.githubusercontent.com/dunwu/images/dev/snap/20200211172631.png) +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200211172631.png) ## 技术选型 @@ -39,4 +38,4 @@ permalink: /pages/c7c5ec/ ## 参考资料 -[CAT、Zipkin 和 SkyWalking 该如何选型?](https://time.geekbang.org/dailylesson/detail/100028416) +[CAT、Zipkin 和 SkyWalking 该如何选型?](https://time.geekbang.org/dailylesson/detail/100028416) \ No newline at end of file diff --git "a/docs/11.\350\275\257\344\273\266/03.\347\233\221\346\216\247\350\257\212\346\226\255/02.CAT.md" "b/docs/01.Java/11.\350\275\257\344\273\266/03.\347\233\221\346\216\247\350\257\212\346\226\255/02.CAT.md" similarity index 94% rename from "docs/11.\350\275\257\344\273\266/03.\347\233\221\346\216\247\350\257\212\346\226\255/02.CAT.md" rename to "docs/01.Java/11.\350\275\257\344\273\266/03.\347\233\221\346\216\247\350\257\212\346\226\255/02.CAT.md" index 639d3a9f..ee099d36 100644 --- "a/docs/11.\350\275\257\344\273\266/03.\347\233\221\346\216\247\350\257\212\346\226\255/02.CAT.md" +++ "b/docs/01.Java/11.\350\275\257\344\273\266/03.\347\233\221\346\216\247\350\257\212\346\226\255/02.CAT.md" @@ -1,17 +1,16 @@ --- title: CAT 快速入门 -categories: - - 编程 +date: 2020-02-11 17:48:32 +order: 02 +categories: - Java - 软件 - 监控诊断 -tags: +tags: - Java - 监控 - CAT -abbrlink: 9e5e43a8 -date: 2020-02-11 17:48:32 -permalink: /pages/83e684/ +permalink: /pages/821ca3/ --- # CAT 快速入门 @@ -38,7 +37,7 @@ CAT 监控系统将每次 URL、Service 的请求内部执行情况都封装为 - **Heartbeat** 表示程序内定期产生的统计信息, 如 CPU 利用率, 内存利用率, 连接池状态, 系统负载等 - **Metric** 用于记录业务指标、指标可能包含对一个指标记录次数、记录平均值、记录总和,业务指标最低统计粒度为 1 分钟 -![img](https://raw.githubusercontent.com/dunwu/images/dev/snap/20200211174235.png) +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200211174235.png) ## CAT 部署 @@ -73,7 +72,7 @@ CAT 主要分为三个模块: 在实际开发和部署中,cat-consumer 和 cat-home 是部署在一个 jvm 内部,每个 CAT 服务端都可以作为 consumer 也可以作为 home,这样既能减少整个 CAT 层级结构,也可以增加整个系统稳定性。 -![img](https://raw.githubusercontent.com/dunwu/images/dev/snap/20200211174001.png) +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200211174001.png) 上图是 CAT 目前多机房的整体结构图: @@ -84,4 +83,4 @@ CAT 主要分为三个模块: ## 参考资料 -- [CAT Github](https://github.com/dianping/cat) +- [CAT Github](https://github.com/dianping/cat) \ No newline at end of file diff --git "a/docs/11.\350\275\257\344\273\266/03.\347\233\221\346\216\247\350\257\212\346\226\255/03.Zipkin.md" "b/docs/01.Java/11.\350\275\257\344\273\266/03.\347\233\221\346\216\247\350\257\212\346\226\255/03.Zipkin.md" similarity index 97% rename from "docs/11.\350\275\257\344\273\266/03.\347\233\221\346\216\247\350\257\212\346\226\255/03.Zipkin.md" rename to "docs/01.Java/11.\350\275\257\344\273\266/03.\347\233\221\346\216\247\350\257\212\346\226\255/03.Zipkin.md" index a36e4d17..e665495d 100644 --- "a/docs/11.\350\275\257\344\273\266/03.\347\233\221\346\216\247\350\257\212\346\226\255/03.Zipkin.md" +++ "b/docs/01.Java/11.\350\275\257\344\273\266/03.\347\233\221\346\216\247\350\257\212\346\226\255/03.Zipkin.md" @@ -1,17 +1,16 @@ --- title: Zipkin 快速入门 -categories: - - 编程 +date: 2020-03-23 22:56:45 +order: 03 +categories: - Java - 软件 - 监控诊断 -tags: +tags: - Java - 监控 - Zipkin -abbrlink: bed182d4 -date: 2020-03-23 22:56:45 -permalink: /pages/82c168/ +permalink: /pages/0a8826/ --- # Zipkin 快速入门 @@ -30,7 +29,7 @@ Zipkin 基于 Google Dapper 的论文设计而来,由 Twitter 公司开发贡 Zipkin UI 还提供了一个依赖关系图,该关系图显示了每个应用程序中跟踪了多少个请求。这对于识别聚合行为(包括错误路径或对不赞成使用的服务的调用)很有帮助。 -![Zipkin UI](https://raw.githubusercontent.com/dunwu/images/dev/snap/20200211161706.png) +![Zipkin UI](https://raw.githubusercontent.com/dunwu/images/master/snap/20200211161706.png) ### 多平台 @@ -48,7 +47,7 @@ Zipkin 服务器捆绑了用于采集和存储数据的扩展。 数据以 json 形式存储,可以参考:[Zipkin 官方的 Swagger API](https://zipkin.io/zipkin-api/#/default/post_spans) -![Zipkin Swagger API](https://raw.githubusercontent.com/dunwu/images/dev/snap/20200211162055.png) +![Zipkin Swagger API](https://raw.githubusercontent.com/dunwu/images/master/snap/20200211162055.png) ## 二、Zipkin 安装 @@ -94,7 +93,7 @@ ZipKin 可以分为两部分, 架构如下: -![Zipkin 架构](https://raw.githubusercontent.com/dunwu/images/dev/snap/20200211155836.png) +![Zipkin 架构](https://raw.githubusercontent.com/dunwu/images/master/snap/20200211155836.png) ### Zipkin Server @@ -170,4 +169,4 @@ Instrumented client 和 server 是分别使用了 ZipKin Client 的服务,Zipk - [Zipkin 官网](https://zipkin.io/) - [Zipkin Github](https://github.com/openzipkin/zipkin) -- [brave](https://github.com/openzipkin/brave) +- [brave](https://github.com/openzipkin/brave) \ No newline at end of file diff --git "a/docs/11.\350\275\257\344\273\266/03.\347\233\221\346\216\247\350\257\212\346\226\255/04.Skywalking.md" "b/docs/01.Java/11.\350\275\257\344\273\266/03.\347\233\221\346\216\247\350\257\212\346\226\255/04.Skywalking.md" similarity index 91% rename from "docs/11.\350\275\257\344\273\266/03.\347\233\221\346\216\247\350\257\212\346\226\255/04.Skywalking.md" rename to "docs/01.Java/11.\350\275\257\344\273\266/03.\347\233\221\346\216\247\350\257\212\346\226\255/04.Skywalking.md" index e0c84c3e..e3573437 100644 --- "a/docs/11.\350\275\257\344\273\266/03.\347\233\221\346\216\247\350\257\212\346\226\255/04.Skywalking.md" +++ "b/docs/01.Java/11.\350\275\257\344\273\266/03.\347\233\221\346\216\247\350\257\212\346\226\255/04.Skywalking.md" @@ -1,17 +1,16 @@ --- title: SkyWalking 快速入门 -categories: - - 编程 +date: 2020-02-07 23:04:47 +order: 04 +categories: - Java - 软件 - 监控诊断 -tags: +tags: - Java - 监控 - SkyWalking -abbrlink: a6f15946 -date: 2020-02-07 23:04:47 -permalink: /pages/d82d3c/ +permalink: /pages/df7dec/ --- # SkyWalking 快速入门 @@ -24,7 +23,7 @@ SkyWalking 是观察性分析平台和应用性能管理系统。 提供分布式追踪、服务网格遥测分析、度量聚合和可视化一体化解决方案。 -![img](https://raw.githubusercontent.com/dunwu/images/dev/snap/20200211152235.png) +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200211152235.png) ### SkyWalking 特性 @@ -45,7 +44,7 @@ SkyWalking 是观察性分析平台和应用性能管理系统。 从逻辑上讲,SkyWalking 分为四个部分:探针(Probes),平台后端,存储和 UI。 -![SkyWalking 架构](https://raw.githubusercontent.com/dunwu/images/dev/snap/20200211153516.png) +![SkyWalking 架构](https://raw.githubusercontent.com/dunwu/images/master/snap/20200211153516.png) - **探针(Probes)** - 探针是指集成到目标系统中的代理或 SDK 库。它们负责收集数据(包括跟踪数据和统计数据)并将其按照 SkyWalking 的要求重新格式化为。 - **平台后端** - 平台后端是一个提供后端服务的集群。它用于聚合、分析和驱动从探针到 UI 的流程。它还为传入格式(如 Zipkin 的格式),存储实现程序和集群管理提供可插入功能。 您甚至可以使用 Observability Analysis Language 自定义聚合和分析。 @@ -56,7 +55,7 @@ SkyWalking 是观察性分析平台和应用性能管理系统。 进入 [Apache SkyWalking 官方下载页面](http://skywalking.apache.org/downloads/),选择安装版本,下载解压到本地。 -![SkyWalking 组件](https://raw.githubusercontent.com/dunwu/images/dev/snap/20200211154612.png) +![SkyWalking 组件](https://raw.githubusercontent.com/dunwu/images/master/snap/20200211154612.png) 安装分为三个部分: @@ -66,4 +65,4 @@ SkyWalking 是观察性分析平台和应用性能管理系统。 ## 参考资料 -- [SkyWalking Github](https://github.com/apache/skywalking) +- [SkyWalking Github](https://github.com/apache/skywalking) \ No newline at end of file diff --git "a/docs/11.\350\275\257\344\273\266/03.\347\233\221\346\216\247\350\257\212\346\226\255/05.Arthas.md" "b/docs/01.Java/11.\350\275\257\344\273\266/03.\347\233\221\346\216\247\350\257\212\346\226\255/05.Arthas.md" similarity index 99% rename from "docs/11.\350\275\257\344\273\266/03.\347\233\221\346\216\247\350\257\212\346\226\255/05.Arthas.md" rename to "docs/01.Java/11.\350\275\257\344\273\266/03.\347\233\221\346\216\247\350\257\212\346\226\255/05.Arthas.md" index a8ea212c..7e5726c9 100644 --- "a/docs/11.\350\275\257\344\273\266/03.\347\233\221\346\216\247\350\257\212\346\226\255/05.Arthas.md" +++ "b/docs/01.Java/11.\350\275\257\344\273\266/03.\347\233\221\346\216\247\350\257\212\346\226\255/05.Arthas.md" @@ -1,17 +1,16 @@ --- title: Arthas 快速入门 -categories: - - 编程 +date: 2020-02-07 23:04:47 +order: 05 +categories: - Java - 软件 - 监控诊断 -tags: +tags: - Java - 诊断 - Arthas -abbrlink: e60b19c0 -date: 2020-02-07 23:04:47 -permalink: /pages/b6a542/ +permalink: /pages/c689d1/ --- # Arthas 快速入门 @@ -425,4 +424,4 @@ Arthas 支持使用管道对上述命令的结果进行进一步的处理,如` - [Arthas Github](https://github.com/alibaba/arthas) - [Arthas 用户文档](https://alibaba.github.io/arthas/index.html) -- [arthas 源码分析](https://www.jianshu.com/p/4e34d0ab47d1) +- [arthas 源码分析](https://www.jianshu.com/p/4e34d0ab47d1) \ No newline at end of file diff --git "a/docs/11.\350\275\257\344\273\266/03.\347\233\221\346\216\247\350\257\212\346\226\255/README.md" "b/docs/01.Java/11.\350\275\257\344\273\266/03.\347\233\221\346\216\247\350\257\212\346\226\255/README.md" similarity index 80% rename from "docs/11.\350\275\257\344\273\266/03.\347\233\221\346\216\247\350\257\212\346\226\255/README.md" rename to "docs/01.Java/11.\350\275\257\344\273\266/03.\347\233\221\346\216\247\350\257\212\346\226\255/README.md" index 693f6ddb..a2caaea0 100644 --- "a/docs/11.\350\275\257\344\273\266/03.\347\233\221\346\216\247\350\257\212\346\226\255/README.md" +++ "b/docs/01.Java/11.\350\275\257\344\273\266/03.\347\233\221\346\216\247\350\257\212\346\226\255/README.md" @@ -1,17 +1,17 @@ --- title: Java 监控诊断 -categories: - - 编程 +date: 2020-02-11 17:48:32 +categories: - Java - 软件 - 监控诊断 -tags: +tags: - Java - 监控 - 诊断 -abbrlink: 7af21817 -date: 2020-02-11 17:48:32 -permalink: /pages/2853ec/ +permalink: /pages/3d16d3/ +hidden: true +index: false --- # Java 监控诊断 @@ -30,4 +30,4 @@ permalink: /pages/2853ec/ - [Zipkin Github](https://github.com/openzipkin/zipkin) - [SkyWalking Github](https://github.com/apache/skywalking) - [PinPoint Github](https://github.com/naver/pinpoint) -- [Arthas Github](https://github.com/alibaba/arthas) +- [Arthas Github](https://github.com/alibaba/arthas) \ No newline at end of file diff --git "a/docs/11.\350\275\257\344\273\266/README.md" "b/docs/01.Java/11.\350\275\257\344\273\266/README.md" similarity index 93% rename from "docs/11.\350\275\257\344\273\266/README.md" rename to "docs/01.Java/11.\350\275\257\344\273\266/README.md" index 2e52122f..d44fbe38 100644 --- "a/docs/11.\350\275\257\344\273\266/README.md" +++ "b/docs/01.Java/11.\350\275\257\344\273\266/README.md" @@ -1,16 +1,14 @@ --- title: Java 软件 -categories: - - 编程 +date: 2022-02-18 08:53:11 +categories: - Java - 软件 - - IDE -tags: +tags: - Java -abbrlink: bd90f980 -date: 2022-02-18 08:53:11 +permalink: /pages/2cb045/ hidden: true -permalink: /pages/dab057/ +index: false --- # Java 软件 @@ -69,4 +67,4 @@ permalink: /pages/dab057/ ## 🚪 传送 -◾ 🏠 [JAVA-TUTORIAL 首页](https://github.com/dunwu/java-tutorial) ◾ 🎯 [我的博客](https://github.com/dunwu/blog) ◾ +◾ 💧 [钝悟的 IT 知识图谱](https://dunwu.github.io/waterdrop/) ◾ \ No newline at end of file diff --git "a/docs/12.\345\267\245\345\205\267/01.IO/01.JSON\345\272\217\345\210\227\345\214\226.md" "b/docs/01.Java/12.\345\267\245\345\205\267/01.IO/01.JSON\345\272\217\345\210\227\345\214\226.md" similarity index 99% rename from "docs/12.\345\267\245\345\205\267/01.IO/01.JSON\345\272\217\345\210\227\345\214\226.md" rename to "docs/01.Java/12.\345\267\245\345\205\267/01.IO/01.JSON\345\272\217\345\210\227\345\214\226.md" index 4cf5ca34..2e3cfea8 100644 --- "a/docs/12.\345\267\245\345\205\267/01.IO/01.JSON\345\272\217\345\210\227\345\214\226.md" +++ "b/docs/01.Java/12.\345\267\245\345\205\267/01.IO/01.JSON\345\272\217\345\210\227\345\214\226.md" @@ -1,18 +1,17 @@ --- title: Java 和 JSON 序列化 -categories: - - 编程 +date: 2022-02-17 22:34:30 +order: 01 +categories: - Java - 工具 - IO -tags: +tags: - Java - IO - 序列化 - JSON -abbrlink: 37e168b9 -date: 2022-02-17 22:34:30 -permalink: /pages/a14952/ +permalink: /pages/4622a6/ --- # Java 和 JSON 序列化 @@ -483,4 +482,4 @@ private class SomeObject { - - [json 的 RFC 文档](http://tools.ietf.org/html/rfc4627) - [JSON 最佳实践](https://kimmking.github.io/2017/06/06/json-best-practice/) - - [【简明教程】JSON](https://www.jianshu.com/p/8b428e1d1564) + - [【简明教程】JSON](https://www.jianshu.com/p/8b428e1d1564) \ No newline at end of file diff --git "a/docs/12.\345\267\245\345\205\267/01.IO/02.\344\272\214\350\277\233\345\210\266\345\272\217\345\210\227\345\214\226.md" "b/docs/01.Java/12.\345\267\245\345\205\267/01.IO/02.\344\272\214\350\277\233\345\210\266\345\272\217\345\210\227\345\214\226.md" similarity index 82% rename from "docs/12.\345\267\245\345\205\267/01.IO/02.\344\272\214\350\277\233\345\210\266\345\272\217\345\210\227\345\214\226.md" rename to "docs/01.Java/12.\345\267\245\345\205\267/01.IO/02.\344\272\214\350\277\233\345\210\266\345\272\217\345\210\227\345\214\226.md" index 99670f4a..631a5fcb 100644 --- "a/docs/12.\345\267\245\345\205\267/01.IO/02.\344\272\214\350\277\233\345\210\266\345\272\217\345\210\227\345\214\226.md" +++ "b/docs/01.Java/12.\345\267\245\345\205\267/01.IO/02.\344\272\214\350\277\233\345\210\266\345\272\217\345\210\227\345\214\226.md" @@ -1,18 +1,17 @@ --- title: Java 二进制序列化 -categories: - - 编程 +date: 2022-02-17 22:34:30 +order: 02 +categories: - Java - 工具 - IO -tags: +tags: - Java - IO - 序列化 - 二进制 -abbrlink: 7684267f -date: 2022-02-17 22:34:30 -permalink: /pages/95f25b/ +permalink: /pages/08d872/ --- # Java 二进制序列化 @@ -23,7 +22,7 @@ permalink: /pages/95f25b/ 原因很简单,就是 Java 默认的序列化机制(`ObjectInputStream` 和 `ObjectOutputStream`)具有很多缺点。 -> 不了解 Java 默认的序列化机制,可以参考:[Java 序列化](https://dunwu.github.io/blog/pages/2b2f0f/) +> 不了解 Java 默认的序列化机制,可以参考:[Java 序列化](https://dunwu.github.io/waterdrop/pages/2b2f0f/) Java 自身的序列化方式具有以下缺点: @@ -38,15 +37,23 @@ Java 自身的序列化方式具有以下缺点: #### Protobuf -[Protobuf](https://developers.google.com/protocol-buffers/) 是 Google 开发的结构序列化库。 +[Protobuf](https://developers.google.com/protocol-buffers/) 是 Google 公司内部的混合语言数据标准,是一种轻便、高效的结构化数据存储 +格式,可以用于结构化数据序列化,支持 Java、Python、C++、Go 等语言。Protobuf +使用的时候需要定义 IDL(Interface description language),然后使用不同语言的 IDL +编译器,生成序列化工具类。 -它具有以下特性: +优点: -- 结构化数据存储格式(xml,json 等) -- 高性能编解码技术 -- 语言和平台无关,扩展性好 +- 序列化后体积相比 JSON、Hessian 小很多 +- 序列化反序列化速度很快,不需要通过反射获取类型 +- 语言和平台无关(基于 IDL),IDL 能清晰地描述语义,所以足以帮助并保证应用程序之间的类型不会丢失,无需类似 XML 解析器 +- 消息格式升级和兼容性不错,可以做到后向兼容 - 支持 Java, C++, Python 三种语言 +缺点: + +- Protobuf 对于具有反射和动态能力的语言来说,用起来很费劲。 + #### Thrift > [Thrift](https://github.com/apache/thrift) 是 apache 开源项目,是一个点对点的 RPC 实现。 @@ -59,15 +66,23 @@ Java 自身的序列化方式具有以下缺点: #### Hessian -> [Hessian](http://hessian.caucho.com/) 是一种二进制传输协议。 -> -> RPC 框架 Dubbo 就支持 Thrift 和 Hession。 +[Hessian](http://hessian.caucho.com/) 是动态类型、二进制、紧凑的,并且可跨语言移植的一种序列化框架。Hessian 协 +议要比 JDK、JSON 更加紧凑,性能上要比 JDK、JSON 序列化高效很多,而且生成的字节 +数也更小。 + +RPC 框架 Dubbo 就支持 Thrift 和 Hession。 它具有以下特性: - 支持多种语言。如:Java、Python、C++、C#、PHP、Ruby 等。 - 相对其他二进制序列化库较慢。 +Hessian 本身也有问题,官方版本对 Java 里面一些常见对象的类型不支持: + +- Linked 系列,LinkedHashMap、LinkedHashSet 等,但是可以通过扩展 CollectionDeserializer 类修复; +- Locale 类,可以通过扩展 ContextSerializerFactory 类修复; +- Byte/Short 反序列化的时候变成 Integer。 + #### Kryo > [Kryo](https://github.com/EsotericSoftware/kryo) 是用于 Java 的快速高效的二进制对象图序列化框架。Kryo 还可以执行自动的深拷贝和浅拷贝。 这是从对象到对象的直接复制,而不是从对象到字节的复制。 @@ -342,6 +357,27 @@ long end = System.currentTimeMillis(); System.out.printf("Kryo 序列化/反序列化耗时:%s", (end - begin)); ``` +Hessian 应用 + +```java +Student student = new Student(); +student.setNo(101); +student.setName("HESSIAN"); +//把student对象转化为byte数组 +ByteArrayOutputStream bos = new ByteArrayOutputStream(); +Hessian2Output output = new Hessian2Output(bos); +output.writeObject(student); +output.flushBuffer(); +byte[] data = bos.toByteArray(); +bos.close(); +//把刚才序列化出来的byte数组转化为student对象 +ByteArrayInputStream bis = new ByteArrayInputStream(data); +Hessian2Input input = new Hessian2Input(bis); +Student deStudent = (Student) input.readObject(); +input.close(); +System.out.println(deStudent); +``` + ## 参考资料 - **官方** @@ -352,4 +388,4 @@ System.out.printf("Kryo 序列化/反序列化耗时:%s", (end - begin)); - [Hessian 官网](http://hessian.caucho.com/) - [FST Github](https://github.com/RuedigerMoeller/fast-serialization) - **文章** - - [java 序列化框架对比](https://www.jianshu.com/p/937883b6b2e5) + - [java 序列化框架对比](https://www.jianshu.com/p/937883b6b2e5) \ No newline at end of file diff --git "a/docs/12.\345\267\245\345\205\267/01.IO/README.md" "b/docs/01.Java/12.\345\267\245\345\205\267/01.IO/README.md" similarity index 90% rename from "docs/12.\345\267\245\345\205\267/01.IO/README.md" rename to "docs/01.Java/12.\345\267\245\345\205\267/01.IO/README.md" index 443ade0d..1cd5ba95 100644 --- "a/docs/12.\345\267\245\345\205\267/01.IO/README.md" +++ "b/docs/01.Java/12.\345\267\245\345\205\267/01.IO/README.md" @@ -1,23 +1,22 @@ --- title: Java 序列化工具 -categories: - - 编程 +date: 2022-02-17 22:34:30 +categories: - Java - 工具 - IO -tags: +tags: - Java - IO - 序列化 -abbrlink: 53c50fed -date: 2022-02-17 22:34:30 +permalink: /pages/08b504/ hidden: true -permalink: /pages/0358e9/ +index: false --- # Java 序列化工具 -Java 官方的序列化存在许多问题,因此,很多人更愿意使用优秀的第三方序列化工具来替代 Java 自身的序列化机制。 如果想详细了解 Java 自身序列化方式,可以参考:[Java 序列化](https://dunwu.github.io/blog/pages/2b2f0f/) +Java 官方的序列化存在许多问题,因此,很多人更愿意使用优秀的第三方序列化工具来替代 Java 自身的序列化机制。 如果想详细了解 Java 自身序列化方式,可以参考:[Java 序列化](https://dunwu.github.io/waterdrop/pages/2b2f0f/) 序列化库技术选型: @@ -37,4 +36,4 @@ Java 官方的序列化存在许多问题,因此,很多人更愿意使用优 - [Hessian 官网](http://hessian.caucho.com/doc/hessian-overview.xtp) - [Fastjson Github](https://github.com/alibaba/fastjson) - [Jackson Github](https://github.com/FasterXML/jackson) -- [Gson Github](https://github.com/google/gson) +- [Gson Github](https://github.com/google/gson) \ No newline at end of file diff --git "a/docs/12.\345\267\245\345\205\267/02.JavaBean/01.Lombok.md" "b/docs/01.Java/12.\345\267\245\345\205\267/02.JavaBean/01.Lombok.md" similarity index 99% rename from "docs/12.\345\267\245\345\205\267/02.JavaBean/01.Lombok.md" rename to "docs/01.Java/12.\345\267\245\345\205\267/02.JavaBean/01.Lombok.md" index 8c48c43a..39b1df3a 100644 --- "a/docs/12.\345\267\245\345\205\267/02.JavaBean/01.Lombok.md" +++ "b/docs/01.Java/12.\345\267\245\345\205\267/02.JavaBean/01.Lombok.md" @@ -1,17 +1,16 @@ --- title: Lombok 快速入门 -categories: - - 编程 +date: 2022-02-17 22:34:30 +order: 01 +categories: - Java - 工具 - JavaBean -tags: +tags: - Java - JavaBean - Lombok -abbrlink: 6f8c7136 -date: 2022-02-17 22:34:30 -permalink: /pages/0d31cd/ +permalink: /pages/eb1d46/ --- # Lombok 快速入门 @@ -534,4 +533,4 @@ public void testEqualsAndHashCodeDemo() { - [Lombok 官网](https://projectlombok.org/) - [Lombok Github](https://github.com/rzwitserloot/lombok) -- [IntelliJ IDEA - Lombok Plugin](http://plugins.jetbrains.com/plugin/6317-lombok-plugin) +- [IntelliJ IDEA - Lombok Plugin](http://plugins.jetbrains.com/plugin/6317-lombok-plugin) \ No newline at end of file diff --git "a/docs/12.\345\267\245\345\205\267/02.JavaBean/02.Dozer.md" "b/docs/01.Java/12.\345\267\245\345\205\267/02.JavaBean/02.Dozer.md" similarity index 99% rename from "docs/12.\345\267\245\345\205\267/02.JavaBean/02.Dozer.md" rename to "docs/01.Java/12.\345\267\245\345\205\267/02.JavaBean/02.Dozer.md" index 4870defd..e9a7c230 100644 --- "a/docs/12.\345\267\245\345\205\267/02.JavaBean/02.Dozer.md" +++ "b/docs/01.Java/12.\345\267\245\345\205\267/02.JavaBean/02.Dozer.md" @@ -1,17 +1,16 @@ --- title: Dozer 快速入门 -categories: - - 编程 +date: 2022-02-17 22:34:30 +order: 02 +categories: - Java - 工具 - JavaBean -tags: +tags: - Java - JavaBean - Dozer -abbrlink: 3632a3b1 -date: 2022-02-17 22:34:30 -permalink: /pages/596174/ +permalink: /pages/45e21b/ --- # Dozer 快速入门 @@ -787,4 +786,4 @@ superAttribute 和 superAttr 的映射规则会被子类所继承,所以不必 ## 参考 -[Dozer 官方文档](http://dozer.sourceforge.net/documentation/gettingstarted.html) | [Dozer 源码地址](https://github.com/DozerMapper/dozer) +[Dozer 官方文档](http://dozer.sourceforge.net/documentation/gettingstarted.html) | [Dozer 源码地址](https://github.com/DozerMapper/dozer) \ No newline at end of file diff --git "a/docs/12.\345\267\245\345\205\267/03.\346\250\241\346\235\277\345\274\225\346\223\216/01.Freemark.md" "b/docs/01.Java/12.\345\267\245\345\205\267/03.\346\250\241\346\235\277\345\274\225\346\223\216/01.Freemark.md" similarity index 98% rename from "docs/12.\345\267\245\345\205\267/03.\346\250\241\346\235\277\345\274\225\346\223\216/01.Freemark.md" rename to "docs/01.Java/12.\345\267\245\345\205\267/03.\346\250\241\346\235\277\345\274\225\346\223\216/01.Freemark.md" index a36c7028..8de94dbc 100644 --- "a/docs/12.\345\267\245\345\205\267/03.\346\250\241\346\235\277\345\274\225\346\223\216/01.Freemark.md" +++ "b/docs/01.Java/12.\345\267\245\345\205\267/03.\346\250\241\346\235\277\345\274\225\346\223\216/01.Freemark.md" @@ -1,17 +1,16 @@ --- title: Freemark 快速入门 -categories: - - 编程 +date: 2022-02-17 22:34:30 +order: 01 +categories: - Java - 工具 - 模板引擎 -tags: +tags: - Java - 模板引擎 - Freemark -abbrlink: 468afcee -date: 2022-02-17 22:34:30 -permalink: /pages/34ec25/ +permalink: /pages/a60ccf/ --- # Freemark 快速入门 @@ -35,7 +34,7 @@ Freemark 模板一句话概括就是:**_`模板 + 数据模型 = 输出`_** - **FTL 标签**:FTL 标签和 HTML 标签很相似,但是它们却是给 FreeMarker 的指示, 而且不会打印在输出内容中。 - **注释**:注释和 HTML 的注释也很相似,但它们是由 `<#--` 和 `-->`来分隔的。注释会被 FreeMarker 直接忽略, 更不会在输出内容中显示。 -![img](https://raw.githubusercontent.com/dunwu/images/dev/snap/ftl-template.png) +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/ftl-template.png) > 🔔 注意: > @@ -173,4 +172,4 @@ FTL 支持的所有转义字符: - [Freemark Github](https://github.com/apache/freemarker) - [Freemark 中文教程](http://freemarker.foofun.cn/) -- [在线 Freemark 工具](https://try.freemarker.apache.org/) +- [在线 Freemark 工具](https://try.freemarker.apache.org/) \ No newline at end of file diff --git "a/docs/12.\345\267\245\345\205\267/03.\346\250\241\346\235\277\345\274\225\346\223\216/02.Thymeleaf.md" "b/docs/01.Java/12.\345\267\245\345\205\267/03.\346\250\241\346\235\277\345\274\225\346\223\216/02.Thymeleaf.md" similarity index 99% rename from "docs/12.\345\267\245\345\205\267/03.\346\250\241\346\235\277\345\274\225\346\223\216/02.Thymeleaf.md" rename to "docs/01.Java/12.\345\267\245\345\205\267/03.\346\250\241\346\235\277\345\274\225\346\223\216/02.Thymeleaf.md" index 7ea9607a..df2e8fc4 100644 --- "a/docs/12.\345\267\245\345\205\267/03.\346\250\241\346\235\277\345\274\225\346\223\216/02.Thymeleaf.md" +++ "b/docs/01.Java/12.\345\267\245\345\205\267/03.\346\250\241\346\235\277\345\274\225\346\223\216/02.Thymeleaf.md" @@ -1,17 +1,16 @@ --- title: Thymeleaf 快速入门 -categories: - - 编程 +date: 2022-02-17 22:34:30 +order: 02 +categories: - Java - 工具 - 模板引擎 -tags: +tags: - Java - 模板引擎 - Thymeleaf -abbrlink: 33db93c3 -date: 2022-02-17 22:34:30 -permalink: /pages/2263fb/ +permalink: /pages/e7d2ad/ --- # Thymeleaf 快速入门 @@ -481,4 +480,4 @@ TODO - [Thymeleaf 官网](https://www.thymeleaf.org/) - [Thymeleaf Github](https://github.com/thymeleaf/thymeleaf/) -- [Thymeleaf 教程](https://fanlychie.github.io/post/thymeleaf.html) +- [Thymeleaf 教程](https://fanlychie.github.io/post/thymeleaf.html) \ No newline at end of file diff --git "a/docs/12.\345\267\245\345\205\267/03.\346\250\241\346\235\277\345\274\225\346\223\216/03.Velocity.md" "b/docs/01.Java/12.\345\267\245\345\205\267/03.\346\250\241\346\235\277\345\274\225\346\223\216/03.Velocity.md" similarity index 98% rename from "docs/12.\345\267\245\345\205\267/03.\346\250\241\346\235\277\345\274\225\346\223\216/03.Velocity.md" rename to "docs/01.Java/12.\345\267\245\345\205\267/03.\346\250\241\346\235\277\345\274\225\346\223\216/03.Velocity.md" index e83961e4..ccb6514f 100644 --- "a/docs/12.\345\267\245\345\205\267/03.\346\250\241\346\235\277\345\274\225\346\223\216/03.Velocity.md" +++ "b/docs/01.Java/12.\345\267\245\345\205\267/03.\346\250\241\346\235\277\345\274\225\346\223\216/03.Velocity.md" @@ -1,17 +1,16 @@ --- title: Velocity 快速入门 -categories: - - 编程 +date: 2022-02-17 22:34:30 +order: 03 +categories: - Java - 工具 - 模板引擎 -tags: +tags: - Java - 模板引擎 - Velocity -abbrlink: bed8d6a9 -date: 2022-02-17 22:34:30 -permalink: /pages/7ecb81/ +permalink: /pages/3ba0ff/ --- # Velocity 快速入门 @@ -329,4 +328,4 @@ Send me - [Velocity Github](https://github.com/apache/velocity-engine/) - [Velocity 官网](https://velocity.apache.org/) - [Velocity 中文文档](https://wizardforcel.gitbooks.io/velocity-doc/content/) -- [velocity-spring-boot-project](https://github.com/alibaba/velocity-spring-boot-project) +- [velocity-spring-boot-project](https://github.com/alibaba/velocity-spring-boot-project) \ No newline at end of file diff --git "a/docs/12.\345\267\245\345\205\267/03.\346\250\241\346\235\277\345\274\225\346\223\216/README.md" "b/docs/01.Java/12.\345\267\245\345\205\267/03.\346\250\241\346\235\277\345\274\225\346\223\216/README.md" similarity index 95% rename from "docs/12.\345\267\245\345\205\267/03.\346\250\241\346\235\277\345\274\225\346\223\216/README.md" rename to "docs/01.Java/12.\345\267\245\345\205\267/03.\346\250\241\346\235\277\345\274\225\346\223\216/README.md" index 605d4815..ffd80ba3 100644 --- "a/docs/12.\345\267\245\345\205\267/03.\346\250\241\346\235\277\345\274\225\346\223\216/README.md" +++ "b/docs/01.Java/12.\345\267\245\345\205\267/03.\346\250\241\346\235\277\345\274\225\346\223\216/README.md" @@ -1,17 +1,16 @@ --- title: Java 模板引擎 -categories: - - 编程 +date: 2022-02-17 22:34:30 +categories: - Java - 工具 - 模板引擎 -tags: +tags: - Java - 模板引擎 -abbrlink: d29d234e -date: 2022-02-17 22:34:30 +permalink: /pages/9d37fa/ hidden: true -permalink: /pages/9a5880/ +index: false --- # Java 模板引擎 @@ -56,4 +55,4 @@ permalink: /pages/9a5880/ - [Velocity Github](https://github.com/apache/velocity-engine/) - [Velocity 官网](https://velocity.apache.org/) - [Velocity 中文文档](https://wizardforcel.gitbooks.io/velocity-doc/content/) - - [velocity-spring-boot-project](https://github.com/alibaba/velocity-spring-boot-project) + - [velocity-spring-boot-project](https://github.com/alibaba/velocity-spring-boot-project) \ No newline at end of file diff --git "a/docs/12.\345\267\245\345\205\267/04.\346\265\213\350\257\225/01.Junit.md" "b/docs/01.Java/12.\345\267\245\345\205\267/04.\346\265\213\350\257\225/01.Junit.md" similarity index 92% rename from "docs/12.\345\267\245\345\205\267/04.\346\265\213\350\257\225/01.Junit.md" rename to "docs/01.Java/12.\345\267\245\345\205\267/04.\346\265\213\350\257\225/01.Junit.md" index defa29af..b1d02cb9 100644 --- "a/docs/12.\345\267\245\345\205\267/04.\346\265\213\350\257\225/01.Junit.md" +++ "b/docs/01.Java/12.\345\267\245\345\205\267/04.\346\265\213\350\257\225/01.Junit.md" @@ -1,24 +1,35 @@ --- title: JUnit5 快速入门 -categories: - - 编程 +date: 2022-02-17 22:34:30 +order: 01 +categories: - Java - 工具 - 测试 -tags: +tags: - Java - 测试 - JUnit -abbrlink: e85f4bbe -date: 2022-02-17 22:34:30 -permalink: /pages/06533c/ +permalink: /pages/b39f47/ --- # JUnit5 快速入门 -> version: junit5 +## JUnit5 简介 + +与以前的 JUnit 版本不同,JUnit 5 由来自三个不同子项目的几个不同模块组成。 + +JUnit 5 = JUnit Platform + JUnit Jupiter + JUnit Vintage + +JUnit Platform 是在 JVM 上启动测试框架的基础。它还定义了用于开发在平台上运行的测试框架的 TestEngine API。此外,该平台还提供了一个控制台启动器,用于从命令行启动平台,并提供 JUnit 平台套件引擎,用于使用平台上的一个或多个测试引擎运行自定义测试套件。 -## 安装 +JUnit Jupiter 是编程模型和扩展模型的组合,用于在 JUnit 5 中编写测试和扩展。Jupiter 子项目提供了一个 测试引擎(`TestEngine` )用于在平台上运行基于 Jupiter 的测试。 + +JUnit Vintage 提供了一个测试引擎(`TestEngine` ),用于在平台上运行基于 JUnit 3 和 JUnit 4 的测试。它要求 JUnit 4.12 或更高版本。 + +JUnit 5 在运行时需要 Java 8(或更高版本)。 + +## JUnit5 安装 在 pom 中添加依赖 @@ -51,9 +62,9 @@ permalink: /pages/06533c/ 组件间依赖关系: -![img](https://raw.githubusercontent.com/dunwu/images/dev/cs/java/javalib/test/junit/junit5-components.png) +![img](https://raw.githubusercontent.com/dunwu/images/master/cs/java/javalib/test/junit/junit5-components.png) -## JUnit 注解 +## JUnit5 注解 | Annotation | Description | | -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | @@ -73,7 +84,11 @@ permalink: /pages/06533c/ | `@Disabled` | Used to _disable_ a test class or test method; analogous to JUnit 4’s `@Ignore`. Such annotations are not _inherited_. | | `@ExtendWith` | Used to register custom [extensions](https://junit.org/junit5/docs/current/user-guide/#extensions). Such annotations are _inherited_. | -## 编写单元测试 +## JUnit5 示例 + +> 我将一部分官方示例放在了我的个人项目中,可以直接下载测试。 +> +> 示例源码路径:https://github.com/dunwu/java-tutorial/tree/master/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/test/junit5 ### 基本的单元测试类和方法 @@ -571,10 +586,9 @@ void palindromes(String candidate) { } ``` -## 引用和引申 +## 参考资料 -- [Github](https://github.com/junit-team/junit5) -- [官方用户手册](https://junit.org/junit5/docs/current/user-guide/) -- [Javadoc](https://junit.org/junit5/docs/current/api/) -- [版本声明](https://junit.org/junit5/docs/current/release-notes/) -- [官方示例](https://github.com/junit-team/junit5-samples) +- [Junit5 Github](https://github.com/junit-team/junit5) +- [Junit5 官方用户手册](https://junit.org/junit5/docs/current/user-guide/) +- [Junit5 Javadoc](https://junit.org/junit5/docs/current/api/) +- [Junit5 官方示例](https://github.com/junit-team/junit5-samples) \ No newline at end of file diff --git "a/docs/12.\345\267\245\345\205\267/04.\346\265\213\350\257\225/02.Mockito.md" "b/docs/01.Java/12.\345\267\245\345\205\267/04.\346\265\213\350\257\225/02.Mockito.md" similarity index 99% rename from "docs/12.\345\267\245\345\205\267/04.\346\265\213\350\257\225/02.Mockito.md" rename to "docs/01.Java/12.\345\267\245\345\205\267/04.\346\265\213\350\257\225/02.Mockito.md" index 6eae9846..e33ffaa4 100644 --- "a/docs/12.\345\267\245\345\205\267/04.\346\265\213\350\257\225/02.Mockito.md" +++ "b/docs/01.Java/12.\345\267\245\345\205\267/04.\346\265\213\350\257\225/02.Mockito.md" @@ -1,17 +1,16 @@ --- title: Mockito 快速入门 -categories: - - 编程 +date: 2022-02-17 22:34:30 +order: 02 +categories: - Java - 工具 - 测试 -tags: +tags: - Java - 测试 - Mockito -abbrlink: 6d4202df -date: 2022-02-17 22:34:30 -permalink: /pages/ab18ad/ +permalink: /pages/f2c6f5/ --- # Mockito 快速入门 @@ -575,4 +574,4 @@ class FooWraper { - [官网](https://site.mockito.org/) - [Github](https://github.com/mockito/mockito) -- [使用强大的 Mockito 测试框架来测试你的代码](https://github.com/xitu/gold-miner/blob/master/TODO/Unit-tests-with-Mockito.md) +- [使用强大的 Mockito 测试框架来测试你的代码](https://github.com/xitu/gold-miner/blob/master/TODO/Unit-tests-with-Mockito.md) \ No newline at end of file diff --git "a/docs/12.\345\267\245\345\205\267/04.\346\265\213\350\257\225/03.Jmeter.md" "b/docs/01.Java/12.\345\267\245\345\205\267/04.\346\265\213\350\257\225/03.Jmeter.md" similarity index 89% rename from "docs/12.\345\267\245\345\205\267/04.\346\265\213\350\257\225/03.Jmeter.md" rename to "docs/01.Java/12.\345\267\245\345\205\267/04.\346\265\213\350\257\225/03.Jmeter.md" index e1649bff..7378e09f 100644 --- "a/docs/12.\345\267\245\345\205\267/04.\346\265\213\350\257\225/03.Jmeter.md" +++ "b/docs/01.Java/12.\345\267\245\345\205\267/04.\346\265\213\350\257\225/03.Jmeter.md" @@ -1,17 +1,16 @@ --- title: JMeter 快速入门 -categories: - - 编程 +date: 2022-02-17 22:34:30 +order: 03 +categories: - Java - 工具 - 测试 -tags: +tags: - Java - 测试 - JMeter -abbrlink: 808e762c -date: 2022-02-17 22:34:30 -permalink: /pages/d99171/ +permalink: /pages/0e5ab1/ --- # JMeter 快速入门 @@ -45,7 +44,7 @@ Jmeter 的工作原理是仿真用户向服务器发送请求,并收集服务 Jmeter 的工作流如下图所示: -![img](https://raw.githubusercontent.com/dunwu/images/dev/cs/java/javaweb/technology/test/jmeter-workflow.png) +![img](https://raw.githubusercontent.com/dunwu/images/master/cs/java/javaweb/technology/test/jmeter-workflow.png) ### 主要元素 @@ -65,7 +64,7 @@ Jmeter 的主要元素如下: - **`预处理器元素(Pre-Processor Elements)`** - 预处理器元素在采样器发出请求之前执行,如果预处理器附加到采样器元素,那么它将在该采样器元素运行之前执行。预处理器元素用于在运行之前准备环境及参数。 - **`后处理器元素(Post-Processor Elements)`** - 后处理器元素是在发送采样器请求之后执行的元素,常用于处理响应数据。 -![img](https://raw.githubusercontent.com/dunwu/images/dev/cs/java/javaweb/technology/test/jmeter-elements.png) +![img](https://raw.githubusercontent.com/dunwu/images/master/cs/java/javaweb/technology/test/jmeter-elements.png) > 📌 提示: > @@ -99,7 +98,7 @@ Jmeter 的主要元素如下: Unix 类系统运行 `jmeter` ;Windows 系统运行 `jmeter.bat` -![image-20191024104517721](https://raw.githubusercontent.com/dunwu/images/dev/snap/jmeter/image-20191024104517721.png) +![image-20191024104517721](https://raw.githubusercontent.com/dunwu/images/master/snap/jmeter/image-20191024104517721.png) ## 使用 @@ -117,7 +116,7 @@ Unix 类系统运行 `jmeter` ;Windows 系统运行 `jmeter.bat` - 设置线程数和循环次数 -![image-20191024105545736](https://raw.githubusercontent.com/dunwu/images/dev/snap/jmeter/image-20191024105545736.png) +![image-20191024105545736](https://raw.githubusercontent.com/dunwu/images/master/snap/jmeter/image-20191024105545736.png) #### 配置原件 @@ -125,7 +124,7 @@ Unix 类系统运行 `jmeter` ;Windows 系统运行 `jmeter.bat` - 填写协议、服务器名称或 IP、端口号 -![image-20191024110016264](https://raw.githubusercontent.com/dunwu/images/dev/snap/jmeter/image-20191024110016264.png) +![image-20191024110016264](https://raw.githubusercontent.com/dunwu/images/master/snap/jmeter/image-20191024110016264.png) #### 构造 HTTP 请求 @@ -135,35 +134,35 @@ Unix 类系统运行 `jmeter` ;Windows 系统运行 `jmeter.bat` - 填写方法、路径 - 填写参数、消息体数据、文件上传 -![image-20191024110953063](https://raw.githubusercontent.com/dunwu/images/dev/snap/jmeter/image-20191024110953063.png) +![image-20191024110953063](https://raw.githubusercontent.com/dunwu/images/master/snap/jmeter/image-20191024110953063.png) #### 添加 HTTP 请求头 - 在“线程组”上右键 【添加】=>【配置元件】=>【HTTP 信息头管理器】 - 由于我的测试例中传输的数据为 json 形式,所以设置键值对 `Content-Type`:`application/json` -![image-20191024111825226](https://raw.githubusercontent.com/dunwu/images/dev/snap/jmeter/image-20191024111825226.png) +![image-20191024111825226](https://raw.githubusercontent.com/dunwu/images/master/snap/jmeter/image-20191024111825226.png) #### 添加断言 - 在“线程组”上右键 【添加】=>【断言】=>【 响应断言 】 - 在我的案例中,以 HTTP 应答状态码为 200 来判断请求是否成功 -![image-20191024112335130](https://raw.githubusercontent.com/dunwu/images/dev/snap/jmeter/image-20191024112335130.png) +![image-20191024112335130](https://raw.githubusercontent.com/dunwu/images/master/snap/jmeter/image-20191024112335130.png) #### 添加察看结果树 - 在“线程组”上右键 【添加】=>【监听器】=>【察看结果树】 - 直接点击运行,就可以查看测试结果 -![image-20191024113849270](https://raw.githubusercontent.com/dunwu/images/dev/snap/jmeter/image-20191024113849270.png) +![image-20191024113849270](https://raw.githubusercontent.com/dunwu/images/master/snap/jmeter/image-20191024113849270.png) #### 添加汇总报告 - 在“线程组”上右键 【添加】=>【监听器】=>【汇总报告】 - 直接点击运行,就可以查看测试结果 -![image-20191024114016424](https://raw.githubusercontent.com/dunwu/images/dev/snap/jmeter/image-20191024114016424.png) +![image-20191024114016424](https://raw.githubusercontent.com/dunwu/images/master/snap/jmeter/image-20191024114016424.png) #### 保存测试计划 @@ -181,7 +180,7 @@ jmeter -n -t [jmx file] -l [results file] -e -o [Path to web report folder] 执行测试计划后,在 `-e -o` 参数后指定的 web 报告目录下,可以找到测试报告内容。在浏览器中打开 `index.html` 文件,可以看到如下报告: -![image-20191024120233058](https://raw.githubusercontent.com/dunwu/images/dev/snap/jmeter/image-20191024120233058.png) +![image-20191024120233058](https://raw.githubusercontent.com/dunwu/images/master/snap/jmeter/image-20191024120233058.png) ## 问题 @@ -193,7 +192,7 @@ jmeter -n -t [jmx file] -l [results file] -e -o [Path to web report folder] 配置如下所示: -![image-20191127175820747](https://raw.githubusercontent.com/dunwu/images/dev/snap/image-20191127175820747.png) +![image-20191127175820747](https://raw.githubusercontent.com/dunwu/images/master/snap/image-20191127175820747.png) 重要配置说明(其他配置根据实际情况填): @@ -217,4 +216,4 @@ jmeter -n -t [jmx file] -l [results file] -e -o [Path to web report folder] - [Jmeter Github](https://github.com/apache/jmeter) - [Jmeter 性能测试入门](https://www.cnblogs.com/TankXiao/p/4045439.html) - [易百教程 - Jmeter 教程](https://www.yiibai.com/jmeter) -- [Jmeter 读取本地 txt/csv 文件作为请求参数,实现接口自动化](https://www.jianshu.com/p/3b2d3b643415) +- [Jmeter 读取本地 txt/csv 文件作为请求参数,实现接口自动化](https://www.jianshu.com/p/3b2d3b643415) \ No newline at end of file diff --git "a/docs/12.\345\267\245\345\205\267/04.\346\265\213\350\257\225/04.JMH.md" "b/docs/01.Java/12.\345\267\245\345\205\267/04.\346\265\213\350\257\225/04.JMH.md" similarity index 99% rename from "docs/12.\345\267\245\345\205\267/04.\346\265\213\350\257\225/04.JMH.md" rename to "docs/01.Java/12.\345\267\245\345\205\267/04.\346\265\213\350\257\225/04.JMH.md" index f28a9ae6..e5707faf 100644 --- "a/docs/12.\345\267\245\345\205\267/04.\346\265\213\350\257\225/04.JMH.md" +++ "b/docs/01.Java/12.\345\267\245\345\205\267/04.\346\265\213\350\257\225/04.JMH.md" @@ -1,17 +1,16 @@ --- title: JMH 快速入门 -categories: - - 编程 +date: 2022-02-17 22:34:30 +order: 04 +categories: - Java - 工具 - 测试 -tags: +tags: - Java - 测试 - JUnit -abbrlink: 293b6b64 -date: 2022-02-17 22:34:30 -permalink: /pages/747d3e/ +permalink: /pages/9c6402/ --- # JMH 快速入门 @@ -353,4 +352,4 @@ State 用于声明某个类是一个“状态”,然后接受一个 Scope 参 - [jmh 官方示例](http://hg.openjdk.java.net/code-tools/jmh/file/tip/jmh-samples/src/main/java/org/openjdk/jmh/samples/) - [Java 微基准测试框架 JMH](https://www.xncoding.com/2018/01/07/java/jmh.html) -- [JAVA 拾遗 — JMH 与 8 个测试陷阱](https://www.cnkirito.moe/java-jmh/) +- [JAVA 拾遗 — JMH 与 8 个测试陷阱](https://www.cnkirito.moe/java-jmh/) \ No newline at end of file diff --git "a/docs/01.Java/12.\345\267\245\345\205\267/04.\346\265\213\350\257\225/README.md" "b/docs/01.Java/12.\345\267\245\345\205\267/04.\346\265\213\350\257\225/README.md" new file mode 100644 index 00000000..ab247b86 --- /dev/null +++ "b/docs/01.Java/12.\345\267\245\345\205\267/04.\346\265\213\350\257\225/README.md" @@ -0,0 +1,23 @@ +--- +title: Java 测试 +date: 2022-02-17 22:34:30 +categories: + - Java + - 工具 + - 测试 +tags: + - Java + - 测试 +permalink: /pages/2cecc3/ +hidden: true +index: false +--- + +# Java 测试 + +## 内容 + +- [Junit](01.Junit.md) +- [Mockito](02.Mockito.md) +- [Jmeter](03.Jmeter.md) +- [JMH](04.JMH.md) \ No newline at end of file diff --git "a/docs/12.\345\267\245\345\205\267/99.\345\205\266\344\273\226/01.Java\346\227\245\345\277\227.md" "b/docs/01.Java/12.\345\267\245\345\205\267/99.\345\205\266\344\273\226/01.Java\346\227\245\345\277\227.md" similarity index 97% rename from "docs/12.\345\267\245\345\205\267/99.\345\205\266\344\273\226/01.Java\346\227\245\345\277\227.md" rename to "docs/01.Java/12.\345\267\245\345\205\267/99.\345\205\266\344\273\226/01.Java\346\227\245\345\277\227.md" index c37498e5..efdc23d6 100644 --- "a/docs/12.\345\267\245\345\205\267/99.\345\205\266\344\273\226/01.Java\346\227\245\345\277\227.md" +++ "b/docs/01.Java/12.\345\267\245\345\205\267/99.\345\205\266\344\273\226/01.Java\346\227\245\345\277\227.md" @@ -1,15 +1,15 @@ --- title: javalib-log -categories: - - 编程 +date: 2022-02-17 22:34:30 +order: 01 +categories: - Java - 工具 -tags: + - 其他 +tags: - Java - 日志 -abbrlink: da18907e -date: 2022-02-17 22:34:30 -permalink: /pages/337701/ +permalink: /pages/fcc1c4/ --- # 细说 Java 主流日志工具库 @@ -64,7 +64,7 @@ logback 当前分成三个模块:`logback-core`、`logback-classic` 和 `logba Log4j2 架构: -![img](https://raw.githubusercontent.com/dunwu/images/dev/cs/java/javalib/log/log4j2-architecture.jpg) +![img](https://raw.githubusercontent.com/dunwu/images/master/cs/java/javalib/log/log4j2-architecture.jpg) ### Log4j vs Logback vs Log4j2 @@ -108,7 +108,7 @@ common-logging 的功能是提供日志功能的 API 接口,本身并不提供 [官网地址](http://www.slf4j.org/) -![img](https://raw.githubusercontent.com/dunwu/images/dev/cs/java/javalib/log/slf4j-to-other-log.png) +![img](https://raw.githubusercontent.com/dunwu/images/master/cs/java/javalib/log/slf4j-to-other-log.png) ### common-logging vs slf4j @@ -202,7 +202,7 @@ _slf4j-jdk14-1.7.21.jar_ 会自动将 _slf4j-api-1.7.21.jar_ 也添加到你的 假如你正在开发应用程序所调用的组件当中已经使用了 common-logging,这时你需要 jcl-over-slf4j.jar 把日志信息输出重定向到 slf4j-api,slf4j-api 再去调用 slf4j 实际依赖的日志组件。这个过程称为桥接。下图是官方的 slf4j 桥接策略图: -![img](https://raw.githubusercontent.com/dunwu/images/dev/cs/java/javalib/log/slf4j-bind-strategy.png) +![img](https://raw.githubusercontent.com/dunwu/images/master/cs/java/javalib/log/slf4j-bind-strategy.png) 从图中应该可以看出,无论你的老项目中使用的是 common-logging 或是直接使用 log4j、java.util.logging,都可以使用对应的桥接 jar 包来解决兼容问题。 @@ -418,7 +418,7 @@ log4j2 基本配置形式如下: - 要点 - 它有 ``、``、`` 三个子元素。 -![img](https://raw.githubusercontent.com/dunwu/images/dev/cs/java/javalib/log/logback-configuration.png) +![img](https://raw.githubusercontent.com/dunwu/images/master/cs/java/javalib/log/logback-configuration.png) ### `` @@ -456,7 +456,7 @@ log4j2 基本配置形式如下: - 属性 - class:设置具体的实例化类。 -![img](https://raw.githubusercontent.com/dunwu/images/dev/cs/java/javalib/log/logback-appender.png) +![img](https://raw.githubusercontent.com/dunwu/images/master/cs/java/javalib/log/logback-appender.png) ### `` @@ -742,4 +742,4 @@ log4j 的配置文件一般有 xml 格式或 properties 格式。这里为了和 - [logback 官方文档](http://logback.qos.ch/) - [log4j 官方文档](http://logging.apache.org/log4j/1.2/) - [commons-logging 官方文档](http://commons.apache.org/proper/commons-logging/) -- +- \ No newline at end of file diff --git "a/docs/12.\345\267\245\345\205\267/99.\345\205\266\344\273\226/02.Java\345\267\245\345\205\267\345\214\205.md" "b/docs/01.Java/12.\345\267\245\345\205\267/99.\345\205\266\344\273\226/02.Java\345\267\245\345\205\267\345\214\205.md" similarity index 73% rename from "docs/12.\345\267\245\345\205\267/99.\345\205\266\344\273\226/02.Java\345\267\245\345\205\267\345\214\205.md" rename to "docs/01.Java/12.\345\267\245\345\205\267/99.\345\205\266\344\273\226/02.Java\345\267\245\345\205\267\345\214\205.md" index fb12c3eb..192f3c42 100644 --- "a/docs/12.\345\267\245\345\205\267/99.\345\205\266\344\273\226/02.Java\345\267\245\345\205\267\345\214\205.md" +++ "b/docs/01.Java/12.\345\267\245\345\205\267/99.\345\205\266\344\273\226/02.Java\345\267\245\345\205\267\345\214\205.md" @@ -1,15 +1,15 @@ --- title: javalib-util -categories: - - 编程 +date: 2022-02-17 22:34:30 +order: 02 +categories: - Java - 工具 -tags: + - 其他 +tags: - Java - 工具包 -abbrlink: 8280ff5f -date: 2022-02-17 22:34:30 -permalink: /pages/14e432/ +permalink: /pages/27ad42/ --- # 细说 Java 主流工具包 @@ -18,4 +18,4 @@ permalink: /pages/14e432/ - [commons-lang](https://github.com/apache/commons-lang) - [commons-collections](https://github.com/apache/commons-collections) - [common-io](https://github.com/apache/commons-io) -- [guava](https://github.com/google/guava) +- [guava](https://github.com/google/guava) \ No newline at end of file diff --git "a/docs/12.\345\267\245\345\205\267/99.\345\205\266\344\273\226/03.Reflections.md" "b/docs/01.Java/12.\345\267\245\345\205\267/99.\345\205\266\344\273\226/03.Reflections.md" similarity index 97% rename from "docs/12.\345\267\245\345\205\267/99.\345\205\266\344\273\226/03.Reflections.md" rename to "docs/01.Java/12.\345\267\245\345\205\267/99.\345\205\266\344\273\226/03.Reflections.md" index de422ac1..6200ebea 100644 --- "a/docs/12.\345\267\245\345\205\267/99.\345\205\266\344\273\226/03.Reflections.md" +++ "b/docs/01.Java/12.\345\267\245\345\205\267/99.\345\205\266\344\273\226/03.Reflections.md" @@ -1,16 +1,16 @@ --- title: Reflections 快速入门 -categories: - - 编程 +date: 2022-02-17 22:34:30 +order: 03 +categories: - Java - 工具 -tags: + - 其他 +tags: - Java - 反射 - Reflections -abbrlink: 3e51d2b2 -date: 2022-02-17 22:34:30 -permalink: /pages/ea4914/ +permalink: /pages/ce6195/ --- # Reflections 快速入门 @@ -105,4 +105,4 @@ Set listMethodsFromCollectionToBoolean = withParametersAssignableTo(Collection.class), withReturnType(boolean.class)); Set fields = getAllFields(SomeClass.class, withAnnotation(annotation), withTypeAssignableTo(type)); -``` +``` \ No newline at end of file diff --git "a/docs/12.\345\267\245\345\205\267/99.\345\205\266\344\273\226/04.JavaMail.md" "b/docs/01.Java/12.\345\267\245\345\205\267/99.\345\205\266\344\273\226/04.JavaMail.md" similarity index 99% rename from "docs/12.\345\267\245\345\205\267/99.\345\205\266\344\273\226/04.JavaMail.md" rename to "docs/01.Java/12.\345\267\245\345\205\267/99.\345\205\266\344\273\226/04.JavaMail.md" index 1eae960e..dc623bbf 100644 --- "a/docs/12.\345\267\245\345\205\267/99.\345\205\266\344\273\226/04.JavaMail.md" +++ "b/docs/01.Java/12.\345\267\245\345\205\267/99.\345\205\266\344\273\226/04.JavaMail.md" @@ -1,15 +1,15 @@ --- title: JavaMail 快速入门 -categories: - - 编程 +date: 2022-02-17 22:34:30 +order: 04 +categories: - Java - 工具 -tags: + - 其他 +tags: - Java - 邮件 -abbrlink: 83e73e -date: 2022-02-17 22:34:30 -permalink: /pages/da3f07/ +permalink: /pages/cd38ec/ --- # JavaMail 快速入门 @@ -453,4 +453,4 @@ public static void main(String[] args) throws Exception { ts.close(); System.out.println("message forwarded successfully...."); } -``` +``` \ No newline at end of file diff --git "a/docs/12.\345\267\245\345\205\267/99.\345\205\266\344\273\226/05.Jsoup.md" "b/docs/01.Java/12.\345\267\245\345\205\267/99.\345\205\266\344\273\226/05.Jsoup.md" similarity index 99% rename from "docs/12.\345\267\245\345\205\267/99.\345\205\266\344\273\226/05.Jsoup.md" rename to "docs/01.Java/12.\345\267\245\345\205\267/99.\345\205\266\344\273\226/05.Jsoup.md" index 7c44697f..04f0cbd5 100644 --- "a/docs/12.\345\267\245\345\205\267/99.\345\205\266\344\273\226/05.Jsoup.md" +++ "b/docs/01.Java/12.\345\267\245\345\205\267/99.\345\205\266\344\273\226/05.Jsoup.md" @@ -1,16 +1,16 @@ --- title: Jsoup 快速入门 -categories: - - 编程 +date: 2022-02-17 22:34:30 +order: 05 +categories: - Java - 工具 -tags: + - 其他 +tags: - Java - Html - Jsoup -abbrlink: 2dece711 -date: 2022-02-17 22:34:30 -permalink: /pages/c516cc/ +permalink: /pages/5dd78d/ --- # Jsoup 快速入门 @@ -447,4 +447,4 @@ jsoup 提供了一系列的 `Whitelist` 基本配置,能够满足大多数要 - [jsoup github 托管代码](https://github.com/jhy/jsoup) - [jsoup Cookbook](https://jsoup.org/cookbook/) - [jsoup Cookbook(中文版)](http://www.open-open.com/jsoup/) -- [不错的 jsoup 学习笔记](https://github.com/code4craft/jsoup-learning) +- [不错的 jsoup 学习笔记](https://github.com/code4craft/jsoup-learning) \ No newline at end of file diff --git "a/docs/12.\345\267\245\345\205\267/99.\345\205\266\344\273\226/06.Thumbnailator.md" "b/docs/01.Java/12.\345\267\245\345\205\267/99.\345\205\266\344\273\226/06.Thumbnailator.md" similarity index 98% rename from "docs/12.\345\267\245\345\205\267/99.\345\205\266\344\273\226/06.Thumbnailator.md" rename to "docs/01.Java/12.\345\267\245\345\205\267/99.\345\205\266\344\273\226/06.Thumbnailator.md" index f6841876..71151154 100644 --- "a/docs/12.\345\267\245\345\205\267/99.\345\205\266\344\273\226/06.Thumbnailator.md" +++ "b/docs/01.Java/12.\345\267\245\345\205\267/99.\345\205\266\344\273\226/06.Thumbnailator.md" @@ -1,16 +1,16 @@ --- title: Thumbnailator 快速入门 -categories: - - 编程 +date: 2022-02-17 22:34:30 +order: 06 +categories: - Java - 工具 -tags: + - 其他 +tags: - Java - 图形处理 - Thumbnailator -abbrlink: 950b94a1 -date: 2022-02-17 22:34:30 -permalink: /pages/aa9f61/ +permalink: /pages/adacc5/ --- # Thumbnailator 快速入门 @@ -239,4 +239,4 @@ Thumbnails.of("oldFile.png", "oldFile2.png") ## 参考 -[Thumbnailator 官方示例文档](https://github.com/coobird/thumbnailator/wiki/Examples) +[Thumbnailator 官方示例文档](https://github.com/coobird/thumbnailator/wiki/Examples) \ No newline at end of file diff --git "a/docs/12.\345\267\245\345\205\267/99.\345\205\266\344\273\226/07.Zxing.md" "b/docs/01.Java/12.\345\267\245\345\205\267/99.\345\205\266\344\273\226/07.Zxing.md" similarity index 96% rename from "docs/12.\345\267\245\345\205\267/99.\345\205\266\344\273\226/07.Zxing.md" rename to "docs/01.Java/12.\345\267\245\345\205\267/99.\345\205\266\344\273\226/07.Zxing.md" index 0e6b2fd0..d5f691bb 100644 --- "a/docs/12.\345\267\245\345\205\267/99.\345\205\266\344\273\226/07.Zxing.md" +++ "b/docs/01.Java/12.\345\267\245\345\205\267/99.\345\205\266\344\273\226/07.Zxing.md" @@ -1,16 +1,16 @@ --- title: ZXing 快速入门 -categories: - - 编程 +date: 2022-02-17 22:34:30 +order: 07 +categories: - Java - 工具 -tags: + - 其他 +tags: - Java - 条形码 - ZXing -abbrlink: b465c57d -date: 2022-02-17 22:34:30 -permalink: /pages/cc8ce5/ +permalink: /pages/b563af/ --- # ZXing 快速入门 @@ -94,4 +94,4 @@ public String decode(String filepath) throws IOException, NotFoundException { ## 参考 -[ZXing github 仓库](https://github.com/zxing/zxing) +[ZXing github 仓库](https://github.com/zxing/zxing) \ No newline at end of file diff --git "a/docs/12.\345\267\245\345\205\267/README.md" "b/docs/01.Java/12.\345\267\245\345\205\267/README.md" similarity index 87% rename from "docs/12.\345\267\245\345\205\267/README.md" rename to "docs/01.Java/12.\345\267\245\345\205\267/README.md" index cd9949f6..dbd1c924 100644 --- "a/docs/12.\345\267\245\345\205\267/README.md" +++ "b/docs/01.Java/12.\345\267\245\345\205\267/README.md" @@ -1,16 +1,15 @@ --- title: Java 工具 -categories: - - 编程 +date: 2022-02-18 08:53:11 +categories: - Java - 工具 -tags: +tags: - Java - 工具 -abbrlink: 13035347 -date: 2022-02-18 08:53:11 +permalink: /pages/1123e1/ hidden: true -permalink: /pages/4b6820/ +index: false --- # Java 工具 @@ -54,4 +53,4 @@ permalink: /pages/4b6820/ ## 🚪 传送 -◾ 🏠 [JAVA-TUTORIAL 首页](https://github.com/dunwu/java-tutorial) ◾ 🎯 [我的博客](https://github.com/dunwu/blog) ◾ +◾ 💧 [钝悟的 IT 知识图谱](https://dunwu.github.io/waterdrop/) ◾ \ No newline at end of file diff --git "a/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/00.Spring\347\273\274\345\220\210/01.Spring\346\246\202\350\277\260.md" "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/00.Spring\347\273\274\345\220\210/01.Spring\346\246\202\350\277\260.md" new file mode 100644 index 00000000..3a202241 --- /dev/null +++ "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/00.Spring\347\273\274\345\220\210/01.Spring\346\246\202\350\277\260.md" @@ -0,0 +1,188 @@ +--- +title: Spring Framework 综述 +date: 2019-11-22 10:46:02 +order: 01 +categories: + - Java + - 框架 + - Spring + - Spring综合 +tags: + - Java + - 框架 + - Spring +permalink: /pages/9d3091/ +--- + +# Spring Framework 综述 + +## Spring Framework 简介 + +Spring Framework 是最受欢迎的企业级 Java 应用程序开发框架。用于构建企业级应用的轻量级、一站式解决方案。 + +当谈论到大小和透明度时, Spring 是轻量级的。 Spring 框架的基础版本是在 2 MB 左右的。 + +Spring 框架的核心特性可以用于开发任何 Java 应用程序,但是在 Java EE 平台上构建 web 应用程序是需要扩展的。 Spring 框架的目标是使 J2EE 开发变得更容易使用,通过启用基于 POJO 编程模型来促进良好的编程实践。 + +Spring Framework 设计理念如下: + +- 力争让选择无处不在 +- 体现海纳百川的精神 +- 保持后向兼容性 +- 专注 API 设计 +- 追求严苛的代码质量 + +## 为什么使用 Spring + +下面列出的是使用 Spring 框架主要的好处: + +- Spring 可以使开发人员使用 POJOs 开发企业级的应用程序。只使用 POJOs 的好处是你不需要一个 EJB 容器产品,比如一个应用程序服务器,但是你可以选择使用一个健壮的 servlet 容器,比如 Tomcat 或者一些商业产品。 +- Spring 在一个单元模式中是有组织的。即使包和类的数量非常大,你只需要选择你需要的部分,而忽略剩余的那部分。 +- Spring 不会让你白费力气做重复工作,它真正的利用了一些现有的技术,像几个 ORM 框架、日志框架、JEE、Quartz 和 JDK 计时器,其他视图技术。 +- 测试一个用 Spring 编写的应用程序很容易,因为 environment-dependent 代码被放进了这个框架中。此外,通过使用 JavaBean-style POJOs,它在使用依赖注入注入测试数据时变得更容易。 +- Spring 的 web 框架是一个设计良好的 web MVC 框架,它为 web 框架,比如 Structs 或者其他工程上的或者很少受欢迎的 web 框架,提供了一个很好的供替代的选择。 +- 为将特定技术的异常(例如,由 JDBC、Hibernate,或者 JDO 抛出的异常)翻译成一致的, Spring 提供了一个方便的 API,而这些都是未经检验的异常。 +- 轻量级的 IOC 容器往往是轻量级的,例如,特别是当与 EJB 容器相比的时候。这有利于在内存和 CPU 资源有限的计算机上开发和部署应用程序。 +- Spring 提供了一个一致的事务管理界面,该界面可以缩小成一个本地事务(例如,使用一个单一的数据库)和扩展成一个全局事务(例如,使用 JTA)。 + +## 核心思想 + +Spring 最核心的两个技术思想是:IoC 和 Aop + +### IoC + +`IoC` 即 `Inversion of Control` ,意为控制反转。 + +Spring 最认同的技术是控制反转的**依赖注入(DI)**模式。控制反转(IoC)是一个通用的概念,它可以用许多不同的方式去表达,依赖注入仅仅是控制反转的一个具体的例子。 + +当编写一个复杂的 Java 应用程序时,应用程序类应该尽可能的独立于其他的 Java 类来增加这些类可重用可能性,当进行单元测试时,可以使它们独立于其他类进行测试。依赖注入(或者有时被称为配线)有助于将这些类粘合在一起,并且在同一时间让它们保持独立。 + +到底什么是依赖注入?让我们将这两个词分开来看一看。这里将依赖关系部分转化为两个类之间的关联。例如,类 A 依赖于类 B。现在,让我们看一看第二部分,注入。所有这一切都意味着类 B 将通过 IoC 被注入到类 A 中。 + +依赖注入可以以向构造函数传递参数的方式发生,或者通过使用 setter 方法 post-construction。由于依赖注入是 Spring 框架的核心部分,所以我将在一个单独的章节中利用很好的例子去解释这一概念。 + +### Aop + +Spring 框架的一个关键组件是**面向方面的程序设计(AOP)**框架。一个程序中跨越多个点的功能被称为**横切关注点**,这些横切关注点在概念上独立于应用程序的业务逻辑。有各种各样常见的很好的关于方面的例子,比如日志记录、声明性事务、安全性,和缓存等等。 + +在 OOP 中模块化的关键单元是类,而在 AOP 中模块化的关键单元是方面。AOP 帮助你将横切关注点从它们所影响的对象中分离出来,然而依赖注入帮助你将你的应用程序对象从彼此中分离出来。 + +Spring 框架的 AOP 模块提供了面向方面的程序设计实现,允许你定义拦截器方法和切入点,可以实现将应该被分开的代码干净的分开功能。我将在一个独立的章节中讨论更多关于 Spring AOP 的概念。 + +## Spring 体系结构 + +Spring 当前框架有**20**个 jar 包,大致可以分为**6**大模块: + +- 1. 为什么使用 Spring +- 2. 核心思想 + - 2.1. IoC + - 2.2. Aop +- 3. Spring 体系结构 + - 3.1. Core Container + - 3.1.1. BeanFactory + - 3.1.2. ApplicationContext + - 3.2. AOP and Instrumentation + - 3.3. Messaging + - 3.4. Data Access / Integaration + - 3.5. Web + - 3.6. Test +- 4. 术语 + +Spring 框架提供了非常丰富的功能,因此整个架构也很庞大。 +在我们实际的应用开发中,并不一定要使用所有的功能,而是可以根据需要选择合适的 Spring 模块。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/cs/java/spring/spring-framework.png) + +### Core Container + +IoC 容器是 Spring 框架的核心。spring 容器使用依赖注入管理构成应用的组件,它会创建相互协作的组件之间的关联。毫无疑问,这些对象更简单干净,更容易理解,也更容易重用和测试。 +Spring 自带了几种容器的实现,可归纳为两种类型: + +#### BeanFactory + +由 org.springframework.beans.factory.BeanFactory 接口定义。 +它是最简单的容器,提供基本的 DI 支持。 + +#### ApplicationContext + +由 org.springframework.context.ApplicationContext 接口定义。 +它是基于 BeanFactory 之上构建,并提供面向应用的服务,例如从属性文件解析文本信息的能力,以及发布应用事件给感兴趣的事件监听者的能力。 +**_注:Bean 工厂对于大多数应用来说往往太低级了,所以应用上下文使用更广泛。推荐在开发中使用应用上下文容器。_** + +Spring 自带了多种应用上下文,最可能遇到的有以下几种: +`ClassPathXmlApplicationContext`:从类路径下的 XML 配置文件中加载上下文定义,把应用上下文定义文件当做类资源。 +`FileSystemXmlApplicationContext`:读取文件系统下的 XML 配置文件并加载上下文定义。 +`XmlWebApplicationContext`:读取 Web 应用下的 XML 配置文件并装载上下文定义。 + +**_范例_** + +```java +ApplicationContext context = new FileSystemXmlApplicationContext("D:\Temp\build.xml"); +ApplicationContext context2 = new ClassPathXmlApplicationContext("build.xml"); +``` + +可以看到,加载 `FileSystemXmlApplicationContext` 和 `ClassPathXmlApplicationContext` 十分相似。 +差异在于:前者在指定文件系统路径下查找 build.xml 文件;而后在所有类路径(包含 JAR 文件)下查找 build.xml 文件。 +通过引用应用上下文,可以很方便的调用 getBean() 方法从 Spring 容器中获取 Bean。 + +**相关 jar 包** + +- `spring-core`, `spring-beans`, 提供框架的基础部分,包括 IoC 和依赖注入特性。 + +- `spring-context`, 在`spring-core`, `spring-beans`基础上构建。它提供一种框架式的访问对象的方法。它也支持类似 Java EE 特性,例如:EJB,JMX 和基本 remoting。ApplicationContext 接口是它的聚焦点。 +- `springcontext-support`, 集成第三方库到 Spring application context。 +- `spring-expression`,提供一种强有力的表达语言在运行时来查询和操纵一个对象图。 + +### AOP and Instrumentation + +**相关 jar 包** + +- `spring-aop`,提供了对面向切面编程的丰富支持。 +- `spring-aspects`,提供了对 AspectJ 的集成。 +- `spring-instrument`,提供了对类 instrumentation 的支持和类加载器。 +- `spring-instrument-tomcat`,包含了 Spring 对 Tomcat 的 instrumentation 代理。 + +### Messaging + +**相关 jar 包** + +- `spring-messaging`,包含 spring 的消息处理功能,如 Message,MessageChannel,MessageHandler。 + +### Data Access / Integaration + +Data Access/Integration 层包含了 JDBC / ORM / OXM / JMS 和 Transaction 模块。 + +**相关 jar 包** + +- `spring-jdbc`,提供了一个 JDBC 抽象层。 + +- `spring-tx`,支持编程和声明式事务管理类。 +- `spring-orm`,提供了流行的对象关系型映射 API 集,如 JPA,JDO,Hibernate。 +- `spring-oxm`,提供了一个抽象层以支持对象/XML 映射的实现,如 JAXB,Castor,XMLBeans,JiBX 和 XStream. +- `spring-jms`,包含了生产和消费消息的功能。 + +### Web + +**相关 jar 包** + +- `spring-web`,提供了基本的面向 web 的功能,如多文件上传、使用 Servlet 监听器的 Ioc 容器的初始化。一个面向 web 的应用层上下文。 + +- `spring-webmvc`,包括 MVC 和 REST web 服务实现。 +- `spring-webmvc-portlet`,提供在 Protlet 环境的 MVC 实现和`spring-webmvc`功能的镜像。 + +### Test + +**相关 jar 包** + +- `spring-test`,以 Junit 和 TestNG 来支持 spring 组件的单元测试和集成测试。 + +## 术语 + +- **应用程序**:是能完成我们所需要功能的成品,比如购物网站、OA 系统。 +- **框架**:是能完成一定功能的半成品,比如我们可以使用框架进行购物网站开发;框架做一部分功能,我们自己做一部分功能,这样应用程序就创建出来了。而且框架规定了你在开发应用程序时的整体架构,提供了一些基础功能,还规定了类和对象的如何创建、如何协作等,从而简化我们开发,让我们专注于业务逻辑开发。 +- **非侵入式设计**:从框架角度可以这样理解,无需继承框架提供的类,这种设计就可以看作是非侵入式设计,如果继承了这些框架类,就是侵入设计,如果以后想更换框架之前写过的代码几乎无法重用,如果非侵入式设计则之前写过的代码仍然可以继续使用。 +- **轻量级及重量级**:轻量级是相对于重量级而言的,轻量级一般就是非入侵性的、所依赖的东西非常少、资源占用非常少、部署简单等等,其实就是比较容易使用,而重量级正好相反。 +- **POJO**:POJO(Plain Old Java Objects)简单的 Java 对象,它可以包含业务逻辑或持久化逻辑,但不担当任何特殊角色且不继承或不实现任何其它 Java 框架的类或接口。 +- **容器**:在日常生活中容器就是一种盛放东西的器具,从程序设计角度看就是装对象的的对象,因为存在放入、拿出等操作,所以容器还要管理对象的生命周期。 +- **控制反转:**即 Inversion of Control,缩写为 IoC,控制反转还有一个名字叫做依赖注入(Dependency Injection),就是由容器控制程序之间的关系,而非传统实现中,由程序代码直接操控。 +- **JavaBean**:一般指容器管理对象,在 Spring 中指 Spring IoC 容器管理对象。 \ No newline at end of file diff --git "a/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/00.Spring\347\273\274\345\220\210/21.SpringBoot\347\237\245\350\257\206\345\233\276\350\260\261.md" "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/00.Spring\347\273\274\345\220\210/21.SpringBoot\347\237\245\350\257\206\345\233\276\350\260\261.md" new file mode 100644 index 00000000..3a5733b4 --- /dev/null +++ "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/00.Spring\347\273\274\345\220\210/21.SpringBoot\347\237\245\350\257\206\345\233\276\350\260\261.md" @@ -0,0 +1,951 @@ +--- +title: SpringBoot 知识图谱 +date: 2020-08-12 07:01:26 +order: 21 +categories: + - Java + - 框架 + - Spring + - Spring综合 +tags: + - Java + - 框架 + - Spring + - SpringBoot +permalink: /pages/430f53/ +--- + +# SpringBoot 知识图谱 + +> 1. 预警:本文非常长,建议先 mark 后看,也许是最后一次写这么长的文章 +> 2. 说明:前面有 4 个小节关于 Spring 的基础知识,分别是:IOC 容器、JavaConfig、事件监听、SpringFactoriesLoader 详解,它们占据了本文的大部分内容,虽然它们之间可能没有太多的联系,但这些知识对于理解 Spring Boot 的核心原理至关重要,如果你对 Spring 框架烂熟于心,完全可以跳过这 4 个小节。正是因为这个系列的文章是由这些看似不相关的知识点组成,因此取名知识清单。 + +在过去两三年的 Spring 生态圈,最让人兴奋的莫过于 Spring Boot 框架。或许从命名上就能看出这个框架的设计初衷:快速的启动 Spring 应用。因而 Spring Boot 应用本质上就是一个基于 Spring 框架的应用,它是 Spring 对“约定优先于配置”理念的最佳实践产物,它能够帮助开发者更快速高效地构建基于 Spring 生态圈的应用。 + +那 Spring Boot 有何魔法?**自动配置**、**起步依赖**、**Actuator**、**命令行界面(CLI)** 是 Spring Boot 最重要的 4 大核心特性,其中 CLI 是 Spring Boot 的可选特性,虽然它功能强大,但也引入了一套不太常规的开发模型,因而这个系列的文章仅关注其它 3 种特性。如文章标题,本文是这个系列的第一部分,将为你打开 Spring Boot 的大门,重点为你剖析其启动流程以及自动配置实现原理。要掌握这部分核心内容,理解一些 Spring 框架的基础知识,将会让你事半功倍。 + +## 一、抛砖引玉:探索 Spring IoC 容器 + +如果有看过`SpringApplication.run()`方法的源码,Spring Boot 冗长无比的启动流程一定会让你抓狂,透过现象看本质,SpringApplication 只是将一个典型的 Spring 应用的启动流程进行了扩展,因此,透彻理解 Spring 容器是打开 Spring Boot 大门的一把钥匙。 + +### 1.1、Spring IoC 容器 + +可以把 Spring IoC 容器比作一间餐馆,当你来到餐馆,通常会直接招呼服务员:点菜!至于菜的原料是什么?如何用原料把菜做出来?可能你根本就不关心。IoC 容器也是一样,你只需要告诉它需要某个 bean,它就把对应的实例(instance)扔给你,至于这个 bean 是否依赖其他组件,怎样完成它的初始化,根本就不需要你关心。 + +作为餐馆,想要做出菜肴,得知道菜的原料和菜谱,同样地,IoC 容器想要管理各个业务对象以及它们之间的依赖关系,需要通过某种途径来记录和管理这些信息。`BeanDefinition`对象就承担了这个责任:容器中的每一个 bean 都会有一个对应的 BeanDefinition 实例,该实例负责保存 bean 对象的所有必要信息,包括 bean 对象的 class 类型、是否是抽象类、构造方法和参数、其它属性等等。当客户端向容器请求相应对象时,容器就会通过这些信息为客户端返回一个完整可用的 bean 实例。 + +原材料已经准备好(把 BeanDefinition 看着原料),开始做菜吧,等等,你还需要一份菜谱,`BeanDefinitionRegistry`和`BeanFactory`就是这份菜谱,BeanDefinitionRegistry 抽象出 bean 的注册逻辑,而 BeanFactory 则抽象出了 bean 的管理逻辑,而各个 BeanFactory 的实现类就具体承担了 bean 的注册以及管理工作。它们之间的关系就如下图: + +![img](https://user-gold-cdn.xitu.io/2018/9/9/165bd49d06649b0b?imageView2/0/w/1280/h/960/format/webp/ignore-error/1) _BeanFactory、BeanDefinitionRegistry 关系图(来自:Spring 揭秘)_ + +`DefaultListableBeanFactory`作为一个比较通用的 BeanFactory 实现,它同时也实现了 BeanDefinitionRegistry 接口,因此它就承担了 Bean 的注册管理工作。从图中也可以看出,BeanFactory 接口中主要包含 getBean、containBean、getType、getAliases 等管理 bean 的方法,而 BeanDefinitionRegistry 接口则包含 registerBeanDefinition、removeBeanDefinition、getBeanDefinition 等注册管理 BeanDefinition 的方法。 + +下面通过一段简单的代码来模拟 BeanFactory 底层是如何工作的: + +``` +// 默认容器实现 +DefaultListableBeanFactory beanRegistry = new DefaultListableBeanFactory(); +// 根据业务对象构造相应的BeanDefinition +AbstractBeanDefinition definition = new RootBeanDefinition(Business.class,true); +// 将bean定义注册到容器中 +beanRegistry.registerBeanDefinition("beanName",definition); +// 如果有多个bean,还可以指定各个bean之间的依赖关系 +// ........ + +// 然后可以从容器中获取这个bean的实例 +// 注意:这里的beanRegistry其实实现了BeanFactory接口,所以可以强转, +// 单纯的BeanDefinitionRegistry是无法强制转换到BeanFactory类型的 +BeanFactory container = (BeanFactory)beanRegistry; +Business business = (Business)container.getBean("beanName"); +``` + +这段代码仅为了说明 BeanFactory 底层的大致工作流程,实际情况会更加复杂,比如 bean 之间的依赖关系可能定义在外部配置文件(XML/Properties)中、也可能是注解方式。Spring IoC 容器的整个工作流程大致可以分为两个阶段: + +①、容器启动阶段 + +容器启动时,会通过某种途径加载`Configuration MetaData`。除了代码方式比较直接外,在大部分情况下,容器需要依赖某些工具类,比如:`BeanDefinitionReader`,BeanDefinitionReader 会对加载的`Configuration MetaData`进行解析和分析,并将分析后的信息组装为相应的 BeanDefinition,最后把这些保存了 bean 定义的 BeanDefinition,注册到相应的 BeanDefinitionRegistry,这样容器的启动工作就完成了。这个阶段主要完成一些准备性工作,更侧重于 bean 对象管理信息的收集,当然一些验证性或者辅助性的工作也在这一阶段完成。 + +来看一个简单的例子吧,过往,所有的 bean 都定义在 XML 配置文件中,下面的代码将模拟 BeanFactory 如何从配置文件中加载 bean 的定义以及依赖关系: + +``` +// 通常为BeanDefinitionRegistry的实现类,这里以DeFaultListabeBeanFactory为例 +BeanDefinitionRegistry beanRegistry = new DefaultListableBeanFactory(); +// XmlBeanDefinitionReader实现了BeanDefinitionReader接口,用于解析XML文件 +XmlBeanDefinitionReader beanDefinitionReader = new XmlBeanDefinitionReaderImpl(beanRegistry); +// 加载配置文件 +beanDefinitionReader.loadBeanDefinitions("classpath:spring-bean.xml"); + +// 从容器中获取bean实例 +BeanFactory container = (BeanFactory)beanRegistry; +Business business = (Business)container.getBean("beanName"); +``` + +②、Bean 的实例化阶段 + +经过第一阶段,所有 bean 定义都通过 BeanDefinition 的方式注册到 BeanDefinitionRegistry 中,当某个请求通过容器的 getBean 方法请求某个对象,或者因为依赖关系容器需要隐式的调用 getBean 时,就会触发第二阶段的活动:容器会首先检查所请求的对象之前是否已经实例化完成。如果没有,则会根据注册的 BeanDefinition 所提供的信息实例化被请求对象,并为其注入依赖。当该对象装配完毕后,容器会立即将其返回给请求方法使用。 + +BeanFactory 只是 Spring IoC 容器的一种实现,如果没有特殊指定,它采用采用延迟初始化策略:只有当访问容器中的某个对象时,才对该对象进行初始化和依赖注入操作。而在实际场景下,我们更多的使用另外一种类型的容器:`ApplicationContext`,它构建在 BeanFactory 之上,属于更高级的容器,除了具有 BeanFactory 的所有能力之外,还提供对事件监听机制以及国际化的支持等。它管理的 bean,在容器启动时全部完成初始化和依赖注入操作。 + +### 1.2、Spring 容器扩展机制 + +IoC 容器负责管理容器中所有 bean 的生命周期,而在 bean 生命周期的不同阶段,Spring 提供了不同的扩展点来改变 bean 的命运。在容器的启动阶段,`BeanFactoryPostProcessor`允许我们在容器实例化相应对象之前,对注册到容器的 BeanDefinition 所保存的信息做一些额外的操作,比如修改 bean 定义的某些属性或者增加其他信息等。 + +如果要自定义扩展类,通常需要实现`org.springframework.beans.factory.config.BeanFactoryPostProcessor`接口,与此同时,因为容器中可能有多个 BeanFactoryPostProcessor,可能还需要实现`org.springframework.core.Ordered`接口,以保证 BeanFactoryPostProcessor 按照顺序执行。Spring 提供了为数不多的 BeanFactoryPostProcessor 实现,我们以`PropertyPlaceholderConfigurer`来说明其大致的工作流程。 + +在 Spring 项目的 XML 配置文件中,经常可以看到许多配置项的值使用占位符,而将占位符所代表的值单独配置到独立的 properties 文件,这样可以将散落在不同 XML 文件中的配置集中管理,而且也方便运维根据不同的环境进行配置不同的值。这个非常实用的功能就是由 PropertyPlaceholderConfigurer 负责实现的。 + +根据前文,当 BeanFactory 在第一阶段加载完所有配置信息时,BeanFactory 中保存的对象的属性还是以占位符方式存在的,比如`${jdbc.mysql.url}`。当 PropertyPlaceholderConfigurer 作为 BeanFactoryPostProcessor 被应用时,它会使用 properties 配置文件中的值来替换相应的 BeanDefinition 中占位符所表示的属性值。当需要实例化 bean 时,bean 定义中的属性值就已经被替换成我们配置的值。当然其实现比上面描述的要复杂一些,这里仅说明其大致工作原理,更详细的实现可以参考其源码。 + +与之相似的,还有`BeanPostProcessor`,其存在于对象实例化阶段。跟 BeanFactoryPostProcessor 类似,它会处理容器内所有符合条件并且已经实例化后的对象。简单的对比,BeanFactoryPostProcessor 处理 bean 的定义,而 BeanPostProcessor 则处理 bean 完成实例化后的对象。BeanPostProcessor 定义了两个接口: + +``` +public interface BeanPostProcessor { + // 前置处理 + Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException; + // 后置处理 + Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException; +} +``` + +为了理解这两个方法执行的时机,简单的了解下 bean 的整个生命周期: + +`postProcessBeforeInitialization()`方法与`postProcessAfterInitialization()`分别对应图中前置处理和后置处理两个步骤将执行的方法。这两个方法中都传入了 bean 对象实例的引用,为扩展容器的对象实例化过程提供了很大便利,在这儿几乎可以对传入的实例执行任何操作。注解、AOP 等功能的实现均大量使用了`BeanPostProcessor`,比如有一个自定义注解,你完全可以实现 BeanPostProcessor 的接口,在其中判断 bean 对象的脑袋上是否有该注解,如果有,你可以对这个 bean 实例执行任何操作,想想是不是非常的简单? + +再来看一个更常见的例子,在 Spring 中经常能够看到各种各样的 Aware 接口,其作用就是在对象实例化完成以后将 Aware 接口定义中规定的依赖注入到当前实例中。比如最常见的`ApplicationContextAware`接口,实现了这个接口的类都可以获取到一个 ApplicationContext 对象。当容器中每个对象的实例化过程走到 BeanPostProcessor 前置处理这一步时,容器会检测到之前注册到容器的 ApplicationContextAwareProcessor,然后就会调用其 postProcessBeforeInitialization()方法,检查并设置 Aware 相关依赖。看看代码吧,是不是很简单: + +``` +// 代码来自:org.springframework.context.support.ApplicationContextAwareProcessor +// 其postProcessBeforeInitialization方法调用了invokeAwareInterfaces方法 +private void invokeAwareInterfaces(Object bean) { + if (bean instanceof EnvironmentAware) { + ((EnvironmentAware) bean).setEnvironment(this.applicationContext.getEnvironment()); + } + if (bean instanceof ApplicationContextAware) { + ((ApplicationContextAware) bean).setApplicationContext(this.applicationContext); + } + // ...... +} +``` + +最后总结一下,本小节内容和你一起回顾了 Spring 容器的部分核心内容,限于篇幅不能写更多,但理解这部分内容,足以让您轻松理解 Spring Boot 的启动原理,如果在后续的学习过程中遇到一些晦涩难懂的知识,再回过头来看看 Spring 的核心知识,也许有意想不到的效果。也许 Spring Boot 的中文资料很少,但 Spring 的中文资料和书籍有太多太多,总有东西能给你启发。 + +## 二、夯实基础:JavaConfig 与常见 Annotation + +### 2.1、JavaConfig + +我们知道`bean`是 Spring IOC 中非常核心的概念,Spring 容器负责 bean 的生命周期的管理。在最初,Spring 使用 XML 配置文件的方式来描述 bean 的定义以及相互间的依赖关系,但随着 Spring 的发展,越来越多的人对这种方式表示不满,因为 Spring 项目的所有业务类均以 bean 的形式配置在 XML 文件中,造成了大量的 XML 文件,使项目变得复杂且难以管理。 + +后来,基于纯 Java Annotation 依赖注入框架`Guice`出世,其性能明显优于采用 XML 方式的 Spring,甚至有部分人认为,`Guice`可以完全取代 Spring(`Guice`仅是一个轻量级 IOC 框架,取代 Spring 还差的挺远)。正是这样的危机感,促使 Spring 及社区推出并持续完善了`JavaConfig`子项目,它基于 Java 代码和 Annotation 注解来描述 bean 之间的依赖绑定关系。比如,下面是使用 XML 配置方式来描述 bean 的定义: + +``` + +``` + +而基于 JavaConfig 的配置形式是这样的: + +``` +@Configuration +public class MoonBookConfiguration { + + // 任何标志了@Bean的方法,其返回值将作为一个bean注册到Spring的IOC容器中 + // 方法名默认成为该bean定义的id + @Bean + public BookService bookService() { + return new BookServiceImpl(); + } +} +``` + +如果两个 bean 之间有依赖关系的话,在 XML 配置中应该是这样: + +``` + + + + + + + + + +``` + +而在 JavaConfig 中则是这样: + +``` +@Configuration +public class MoonBookConfiguration { + + // 如果一个bean依赖另一个bean,则直接调用对应JavaConfig类中依赖bean的创建方法即可 + // 这里直接调用dependencyService() + @Bean + public BookService bookService() { + return new BookServiceImpl(dependencyService()); + } + + @Bean + public OtherService otherService() { + return new OtherServiceImpl(dependencyService()); + } + + @Bean + public DependencyService dependencyService() { + return new DependencyServiceImpl(); + } +} +``` + +你可能注意到这个示例中,有两个 bean 都依赖于 dependencyService,也就是说当初始化 bookService 时会调用`dependencyService()`,在初始化 otherService 时也会调用`dependencyService()`,那么问题来了?这时候 IOC 容器中是有一个 dependencyService 实例还是两个?这个问题留着大家思考吧,这里不再赘述。 + +### 2.2、@ComponentScan + +`@ComponentScan`注解对应 XML 配置形式中的``元素,表示启用组件扫描,Spring 会自动扫描所有通过注解配置的 bean,然后将其注册到 IOC 容器中。我们可以通过`basePackages`等属性来指定`@ComponentScan`自动扫描的范围,如果不指定,默认从声明`@ComponentScan`所在类的`package`进行扫描。正因为如此,SpringBoot 的启动类都默认在`src/main/java`下。 + +### 2.3、@Import + +`@Import`注解用于导入配置类,举个简单的例子: + +``` +@Configuration +public class MoonBookConfiguration { + @Bean + public BookService bookService() { + return new BookServiceImpl(); + } +} +``` + +现在有另外一个配置类,比如:`MoonUserConfiguration`,这个配置类中有一个 bean 依赖于`MoonBookConfiguration`中的 bookService,如何将这两个 bean 组合在一起?借助`@Import`即可: + +``` +@Configuration +// 可以同时导入多个配置类,比如:@Import({A.class,B.class}) +@Import(MoonBookConfiguration.class) +public class MoonUserConfiguration { + @Bean + public UserService userService(BookService bookService) { + return new BookServiceImpl(bookService); + } +} +``` + +需要注意的是,在 4.2 之前,`@Import`注解只支持导入配置类,但是在 4.2 之后,它支持导入普通类,并将这个类作为一个 bean 的定义注册到 IOC 容器中。 + +### 2.4、@Conditional + +`@Conditional`注解表示在满足某种条件后才初始化一个 bean 或者启用某些配置。它一般用在由`@Component`、`@Service`、`@Configuration`等注解标识的类上面,或者由`@Bean`标记的方法上。如果一个`@Configuration`类标记了`@Conditional`,则该类中所有标识了`@Bean`的方法和`@Import`注解导入的相关类将遵从这些条件。 + +在 Spring 里可以很方便的编写你自己的条件类,所要做的就是实现`Condition`接口,并覆盖它的`matches()`方法。举个例子,下面的简单条件类表示只有在`Classpath`里存在`JdbcTemplate`类时才生效: + +``` +public class JdbcTemplateCondition implements Condition { + + @Override + public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata annotatedTypeMetadata) { + try { + conditionContext.getClassLoader().loadClass("org.springframework.jdbc.core.JdbcTemplate"); + return true; + } catch (ClassNotFoundException e) { + e.printStackTrace(); + } + return false; + } +} +``` + +当你用 Java 来声明 bean 的时候,可以使用这个自定义条件类: + +``` +@Conditional(JdbcTemplateCondition.class) +@Service +public MyService service() { + ...... +} +``` + +这个例子中只有当`JdbcTemplateCondition`类的条件成立时才会创建 MyService 这个 bean。也就是说 MyService 这 bean 的创建条件是`classpath`里面包含`JdbcTemplate`,否则这个 bean 的声明就会被忽略掉。 + +`Spring Boot`定义了很多有趣的条件,并把他们运用到了配置类上,这些配置类构成了`Spring Boot`的自动配置的基础。`Spring Boot`运用条件化配置的方法是:定义多个特殊的条件化注解,并将它们用到配置类上。下面列出了`Spring Boot`提供的部分条件化注解: + +| 条件化注解 | 配置生效条件 | +| ------------------------------- | ------------------------------------------------------- | +| @ConditionalOnBean | 配置了某个特定 bean | +| @ConditionalOnMissingBean | 没有配置特定的 bean | +| @ConditionalOnClass | Classpath 里有指定的类 | +| @ConditionalOnMissingClass | Classpath 里没有指定的类 | +| @ConditionalOnExpression | 给定的 Spring Expression Language 表达式计算结果为 true | +| @ConditionalOnJava | Java 的版本匹配特定指或者一个范围值 | +| @ConditionalOnProperty | 指定的配置属性要有一个明确的值 | +| @ConditionalOnResource | Classpath 里有指定的资源 | +| @ConditionalOnWebApplication | 这是一个 Web 应用程序 | +| @ConditionalOnNotWebApplication | 这不是一个 Web 应用程序 | + +### 2.5、@ConfigurationProperties 与@EnableConfigurationProperties + +当某些属性的值需要配置的时候,我们一般会在`application.properties`文件中新建配置项,然后在 bean 中使用`@Value`注解来获取配置的值,比如下面配置数据源的代码。 + +``` +// jdbc config +jdbc.mysql.url=jdbc:mysql://localhost:3306/sampledb +jdbc.mysql.username=root +jdbc.mysql.password=123456 +...... + +// 配置数据源 +@Configuration +public class HikariDataSourceConfiguration { + + @Value("jdbc.mysql.url") + public String url; + @Value("jdbc.mysql.username") + public String user; + @Value("jdbc.mysql.password") + public String password; + + @Bean + public HikariDataSource dataSource() { + HikariConfig hikariConfig = new HikariConfig(); + hikariConfig.setJdbcUrl(url); + hikariConfig.setUsername(user); + hikariConfig.setPassword(password); + // 省略部分代码 + return new HikariDataSource(hikariConfig); + } +} +``` + +使用`@Value`注解注入的属性通常都比较简单,如果同一个配置在多个地方使用,也存在不方便维护的问题(考虑下,如果有几十个地方在使用某个配置,而现在你想改下名字,你改怎么做?)。对于更为复杂的配置,Spring Boot 提供了更优雅的实现方式,那就是`@ConfigurationProperties`注解。我们可以通过下面的方式来改写上面的代码: + +``` +@Component +// 还可以通过@PropertySource("classpath:jdbc.properties")来指定配置文件 +@ConfigurationProperties("jdbc.mysql") +// 前缀=jdbc.mysql,会在配置文件中寻找jdbc.mysql.*的配置项 +pulic class JdbcConfig { + public String url; + public String username; + public String password; +} + +@Configuration +public class HikariDataSourceConfiguration { + + @AutoWired + public JdbcConfig config; + + @Bean + public HikariDataSource dataSource() { + HikariConfig hikariConfig = new HikariConfig(); + hikariConfig.setJdbcUrl(config.url); + hikariConfig.setUsername(config.username); + hikariConfig.setPassword(config.password); + // 省略部分代码 + return new HikariDataSource(hikariConfig); + } +} +``` + +`@ConfigurationProperties`对于更为复杂的配置,处理起来也是得心应手,比如有如下配置文件: + +``` +#App +app.menus[0].title=Home +app.menus[0].name=Home +app.menus[0].path=/ +app.menus[1].title=Login +app.menus[1].name=Login +app.menus[1].path=/login + +app.compiler.timeout=5 +app.compiler.output-folder=/temp/ + +app.error=/error/ +``` + +可以定义如下配置类来接收这些属性 + +``` +@Component +@ConfigurationProperties("app") +public class AppProperties { + + public String error; + public List menus = new ArrayList<>(); + public Compiler compiler = new Compiler(); + + public static class Menu { + public String name; + public String path; + public String title; + } + + public static class Compiler { + public String timeout; + public String outputFolder; + } +} +``` + +`@EnableConfigurationProperties`注解表示对`@ConfigurationProperties`的内嵌支持,默认会将对应 Properties Class 作为 bean 注入的 IOC 容器中,即在相应的 Properties 类上不用加`@Component`注解。 + +## 三、削铁如泥:SpringFactoriesLoader 详解 + +JVM 提供了 3 种类加载器:`BootstrapClassLoader`、`ExtClassLoader`、`AppClassLoader`分别加载 Java 核心类库、扩展类库以及应用的类路径(`CLASSPATH`)下的类库。JVM 通过双亲委派模型进行类的加载,我们也可以通过继承`java.lang.classloader`实现自己的类加载器。 + +何为双亲委派模型?当一个类加载器收到类加载任务时,会先交给自己的父加载器去完成,因此最终加载任务都会传递到最顶层的 BootstrapClassLoader,只有当父加载器无法完成加载任务时,才会尝试自己来加载。 + +采用双亲委派模型的一个好处是保证使用不同类加载器最终得到的都是同一个对象,这样就可以保证 Java 核心库的类型安全,比如,加载位于 rt.jar 包中的`java.lang.Object`类,不管是哪个加载器加载这个类,最终都是委托给顶层的 BootstrapClassLoader 来加载的,这样就可以保证任何的类加载器最终得到的都是同样一个 Object 对象。查看 ClassLoader 的源码,对双亲委派模型会有更直观的认识: + +``` +protected Class loadClass(String name, boolean resolve) { + synchronized (getClassLoadingLock(name)) { + // 首先,检查该类是否已经被加载,如果从JVM缓存中找到该类,则直接返回 + Class c = findLoadedClass(name); + if (c == null) { + try { + // 遵循双亲委派的模型,首先会通过递归从父加载器开始找, + // 直到父类加载器是BootstrapClassLoader为止 + if (parent != null) { + c = parent.loadClass(name, false); + } else { + c = findBootstrapClassOrNull(name); + } + } catch (ClassNotFoundException e) {} + if (c == null) { + // 如果还找不到,尝试通过findClass方法去寻找 + // findClass是留给开发者自己实现的,也就是说 + // 自定义类加载器时,重写此方法即可 + c = findClass(name); + } + } + if (resolve) { + resolveClass(c); + } + return c; + } +} +``` + +但双亲委派模型并不能解决所有的类加载器问题,比如,Java 提供了很多服务提供者接口(`Service Provider Interface`,SPI),允许第三方为这些接口提供实现。常见的 SPI 有 JDBC、JNDI、JAXP 等,这些 SPI 的接口由核心类库提供,却由第三方实现,这样就存在一个问题:SPI 的接口是 Java 核心库的一部分,是由 BootstrapClassLoader 加载的;SPI 实现的 Java 类一般是由 AppClassLoader 来加载的。BootstrapClassLoader 是无法找到 SPI 的实现类的,因为它只加载 Java 的核心库。它也不能代理给 AppClassLoader,因为它是最顶层的类加载器。也就是说,双亲委派模型并不能解决这个问题。 + +线程上下文类加载器(`ContextClassLoader`)正好解决了这个问题。从名称上看,可能会误解为它是一种新的类加载器,实际上,它仅仅是 Thread 类的一个变量而已,可以通过`setContextClassLoader(ClassLoader cl)`和`getContextClassLoader()`来设置和获取该对象。如果不做任何的设置,Java 应用的线程的上下文类加载器默认就是 AppClassLoader。在核心类库使用 SPI 接口时,传递的类加载器使用线程上下文类加载器,就可以成功的加载到 SPI 实现的类。线程上下文类加载器在很多 SPI 的实现中都会用到。但在 JDBC 中,你可能会看到一种更直接的实现方式,比如,JDBC 驱动管理`java.sql.Driver`中的`loadInitialDrivers()`方法中,你可以直接看到 JDK 是如何加载驱动的: + +``` +for (String aDriver : driversList) { + try { + // 直接使用AppClassLoader + Class.forName(aDriver, true, ClassLoader.getSystemClassLoader()); + } catch (Exception ex) { + println("DriverManager.Initialize: load failed: " + ex); + } +} +``` + +其实讲解线程上下文类加载器,最主要是让大家在看到`Thread.currentThread().getClassLoader()`和`Thread.currentThread().getContextClassLoader()`时不会一脸懵逼,这两者除了在许多底层框架中取得的 ClassLoader 可能会有所不同外,其他大多数业务场景下都是一样的,大家只要知道它是为了解决什么问题而存在的即可。 + +类加载器除了加载 class 外,还有一个非常重要功能,就是加载资源,它可以从 jar 包中读取任何资源文件,比如,`ClassLoader.getResources(String name)`方法就是用于读取 jar 包中的资源文件,其代码如下: + +``` +public Enumeration getResources(String name) throws IOException { + Enumeration[] tmp = (Enumeration[]) new Enumeration[2]; + if (parent != null) { + tmp[0] = parent.getResources(name); + } else { + tmp[0] = getBootstrapResources(name); + } + tmp[1] = findResources(name); + return new CompoundEnumeration<>(tmp); +} +``` + +是不是觉得有点眼熟,不错,它的逻辑其实跟类加载的逻辑是一样的,首先判断父类加载器是否为空,不为空则委托父类加载器执行资源查找任务,直到 BootstrapClassLoader,最后才轮到自己查找。而不同的类加载器负责扫描不同路径下的 jar 包,就如同加载 class 一样,最后会扫描所有的 jar 包,找到符合条件的资源文件。 + +类加载器的`findResources(name)`方法会遍历其负责加载的所有 jar 包,找到 jar 包中名称为 name 的资源文件,这里的资源可以是任何文件,甚至是.class 文件,比如下面的示例,用于查找 Array.class 文件: + +``` +// 寻找Array.class文件 +public static void main(String[] args) throws Exception{ + // Array.class的完整路径 + String name = "java/sql/Array.class"; + Enumeration urls = Thread.currentThread().getContextClassLoader().getResources(name); + while (urls.hasMoreElements()) { + URL url = urls.nextElement(); + System.out.println(url.toString()); + } +} +``` + +运行后可以得到如下结果: + +``` +$JAVA_HOME/jre/lib/rt.jar!/java/sql/Array.class +``` + +根据资源文件的 URL,可以构造相应的文件来读取资源内容。 + +看到这里,你可能会感到挺奇怪的,你不是要详解`SpringFactoriesLoader`吗?上来讲了一堆 ClassLoader 是几个意思?看下它的源码你就知道了: + +``` +public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories"; +// spring.factories文件的格式为:key=value1,value2,value3 +// 从所有的jar包中找到META-INF/spring.factories文件 +// 然后从文件中解析出key=factoryClass类名称的所有value值 +public static List loadFactoryNames(Class factoryClass, ClassLoader classLoader) { + String factoryClassName = factoryClass.getName(); + // 取得资源文件的URL + Enumeration urls = (classLoader != null ? classLoader.getResources(FACTORIES_RESOURCE_LOCATION) : ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION)); + List result = new ArrayList(); + // 遍历所有的URL + while (urls.hasMoreElements()) { + URL url = urls.nextElement(); + // 根据资源文件URL解析properties文件 + Properties properties = PropertiesLoaderUtils.loadProperties(new UrlResource(url)); + String factoryClassNames = properties.getProperty(factoryClassName); + // 组装数据,并返回 + result.addAll(Arrays.asList(StringUtils.commaDelimitedListToStringArray(factoryClassNames))); + } + return result; +} +``` + +有了前面关于 ClassLoader 的知识,再来理解这段代码,是不是感觉豁然开朗:从`CLASSPATH`下的每个 Jar 包中搜寻所有`META-INF/spring.factories`配置文件,然后将解析 properties 文件,找到指定名称的配置后返回。需要注意的是,其实这里不仅仅是会去 ClassPath 路径下查找,会扫描所有路径下的 Jar 包,只不过这个文件只会在 Classpath 下的 jar 包中。来简单看下`spring.factories`文件的内容吧: + +``` +// 来自 org.springframework.boot.autoconfigure下的META-INF/spring.factories +// EnableAutoConfiguration后文会讲到,它用于开启Spring Boot自动配置功能 +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,\ +org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,\ +org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration\ +``` + +执行`loadFactoryNames(EnableAutoConfiguration.class, classLoader)`后,得到对应的一组`@Configuration`类, +我们就可以通过反射实例化这些类然后注入到 IOC 容器中,最后容器里就有了一系列标注了`@Configuration`的 JavaConfig 形式的配置类。 + +这就是`SpringFactoriesLoader`,它本质上属于 Spring 框架私有的一种扩展方案,类似于 SPI,Spring Boot 在 Spring 基础上的很多核心功能都是基于此,希望大家可以理解。 + +## 四、另一件武器:Spring 容器的事件监听机制 + +过去,事件监听机制多用于图形界面编程,比如:**点击**按钮、在文本框**输入**内容等操作被称为事件,而当事件触发时,应用程序作出一定的响应则表示应用监听了这个事件,而在服务器端,事件的监听机制更多的用于异步通知以及监控和异常处理。Java 提供了实现事件监听机制的两个基础类:自定义事件类型扩展自`java.util.EventObject`、事件的监听器扩展自`java.util.EventListener`。来看一个简单的实例:简单的监控一个方法的耗时。 + +首先定义事件类型,通常的做法是扩展 EventObject,随着事件的发生,相应的状态通常都封装在此类中: + +``` +public class MethodMonitorEvent extends EventObject { + // 时间戳,用于记录方法开始执行的时间 + public long timestamp; + + public MethodMonitorEvent(Object source) { + super(source); + } +} +``` + +事件发布之后,相应的监听器即可对该类型的事件进行处理,我们可以在方法开始执行之前发布一个 begin 事件,在方法执行结束之后发布一个 end 事件,相应地,事件监听器需要提供方法对这两种情况下接收到的事件进行处理: + +``` +// 1、定义事件监听接口 +public interface MethodMonitorEventListener extends EventListener { + // 处理方法执行之前发布的事件 + public void onMethodBegin(MethodMonitorEvent event); + // 处理方法结束时发布的事件 + public void onMethodEnd(MethodMonitorEvent event); +} +// 2、事件监听接口的实现:如何处理 +public class AbstractMethodMonitorEventListener implements MethodMonitorEventListener { + + @Override + public void onMethodBegin(MethodMonitorEvent event) { + // 记录方法开始执行时的时间 + event.timestamp = System.currentTimeMillis(); + } + + @Override + public void onMethodEnd(MethodMonitorEvent event) { + // 计算方法耗时 + long duration = System.currentTimeMillis() - event.timestamp; + System.out.println("耗时:" + duration); + } +} +``` + +事件监听器接口针对不同的事件发布实际提供相应的处理方法定义,最重要的是,其方法只接收 MethodMonitorEvent 参数,说明这个监听器类只负责监听器对应的事件并进行处理。有了事件和监听器,剩下的就是发布事件,然后让相应的监听器监听并处理。通常情况,我们会有一个事件发布者,它本身作为事件源,在合适的时机,将相应的事件发布给对应的事件监听器: + +``` +public class MethodMonitorEventPublisher { + + private List listeners = new ArrayList(); + + public void methodMonitor() { + MethodMonitorEvent eventObject = new MethodMonitorEvent(this); + publishEvent("begin",eventObject); + // 模拟方法执行:休眠5秒钟 + TimeUnit.SECONDS.sleep(5); + publishEvent("end",eventObject); + + } + + private void publishEvent(String status,MethodMonitorEvent event) { + // 避免在事件处理期间,监听器被移除,这里为了安全做一个复制操作 + List copyListeners = ➥ new ArrayList(listeners); + for (MethodMonitorEventListener listener : copyListeners) { + if ("begin".equals(status)) { + listener.onMethodBegin(event); + } else { + listener.onMethodEnd(event); + } + } + } + + public static void main(String[] args) { + MethodMonitorEventPublisher publisher = new MethodMonitorEventPublisher(); + publisher.addEventListener(new AbstractMethodMonitorEventListener()); + publisher.methodMonitor(); + } + // 省略实现 + public void addEventListener(MethodMonitorEventListener listener) {} + public void removeEventListener(MethodMonitorEventListener listener) {} + public void removeAllListeners() {} +``` + +对于事件发布者(事件源)通常需要关注两点: + +1. 在合适的时机发布事件。此例中的 methodMonitor()方法是事件发布的源头,其在方法执行之前和结束之后两个时间点发布 MethodMonitorEvent 事件,每个时间点发布的事件都会传给相应的监听器进行处理。在具体实现时需要注意的是,事件发布是顺序执行,为了不影响处理性能,事件监听器的处理逻辑应尽量简单。 +2. 事件监听器的管理。publisher 类中提供了事件监听器的注册与移除方法,这样客户端可以根据实际情况决定是否需要注册新的监听器或者移除某个监听器。如果这里没有提供 remove 方法,那么注册的监听器示例将一直被 MethodMonitorEventPublisher 引用,即使已经废弃不用了,也依然在发布者的监听器列表中,这会导致隐性的内存泄漏。 + +#### Spring 容器内的事件监听机制 + +Spring 的 ApplicationContext 容器内部中的所有事件类型均继承自`org.springframework.context.ApplicationEvent`,容器中的所有监听器都实现`org.springframework.context.ApplicationListener`接口,并且以 bean 的形式注册在容器中。一旦在容器内发布 ApplicationEvent 及其子类型的事件,注册到容器的 ApplicationListener 就会对这些事件进行处理。 + +你应该已经猜到是怎么回事了。 + +ApplicationEvent 继承自 EventObject,Spring 提供了一些默认的实现,比如:`ContextClosedEvent`表示容器在即将关闭时发布的事件类型,`ContextRefreshedEvent`表示容器在初始化或者刷新的时候发布的事件类型...... + +容器内部使用 ApplicationListener 作为事件监听器接口定义,它继承自 EventListener。ApplicationContext 容器在启动时,会自动识别并加载 EventListener 类型的 bean,一旦容器内有事件发布,将通知这些注册到容器的 EventListener。 + +ApplicationContext 接口继承了 ApplicationEventPublisher 接口,该接口提供了`void publishEvent(ApplicationEvent event)`方法定义,不难看出,ApplicationContext 容器担当的就是事件发布者的角色。如果有兴趣可以查看`AbstractApplicationContext.publishEvent(ApplicationEvent event)`方法的源码:ApplicationContext 将事件的发布以及监听器的管理工作委托给`ApplicationEventMulticaster`接口的实现类。在容器启动时,会检查容器内是否存在名为 applicationEventMulticaster 的 ApplicationEventMulticaster 对象实例。如果有就使用其提供的实现,没有就默认初始化一个 SimpleApplicationEventMulticaster 作为实现。 + +最后,如果我们业务需要在容器内部发布事件,只需要为其注入 ApplicationEventPublisher 依赖即可:实现 ApplicationEventPublisherAware 接口或者 ApplicationContextAware 接口(Aware 接口相关内容请回顾上文)。 + +## 五、出神入化:揭秘自动配置原理 + +典型的 Spring Boot 应用的启动类一般均位于`src/main/java`根路径下,比如`MoonApplication`类: + +``` +@SpringBootApplication +public class MoonApplication { + + public static void main(String[] args) { + SpringApplication.run(MoonApplication.class, args); + } +} +``` + +其中`@SpringBootApplication`开启组件扫描和自动配置,而`SpringApplication.run`则负责启动引导应用程序。`@SpringBootApplication`是一个复合`Annotation`,它将三个有用的注解组合在一起: + +``` +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +@SpringBootConfiguration +@EnableAutoConfiguration +@ComponentScan(excludeFilters = { + @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class), + @Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) }) +public @interface SpringBootApplication { + // ...... +} +``` + +`@SpringBootConfiguration`就是`@Configuration`,它是 Spring 框架的注解,标明该类是一个`JavaConfig`配置类。而`@ComponentScan`启用组件扫描,前文已经详细讲解过,这里着重关注`@EnableAutoConfiguration`。 + +`@EnableAutoConfiguration`注解表示开启 Spring Boot 自动配置功能,Spring Boot 会根据应用的依赖、自定义的 bean、classpath 下有没有某个类 等等因素来猜测你需要的 bean,然后注册到 IOC 容器中。那`@EnableAutoConfiguration`是如何推算出你的需求?首先看下它的定义: + +``` +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +@AutoConfigurationPackage +@Import(EnableAutoConfigurationImportSelector.class) +public @interface EnableAutoConfiguration { + // ...... +} +``` + +你的关注点应该在`@Import(EnableAutoConfigurationImportSelector.class)`上了,前文说过,`@Import`注解用于导入类,并将这个类作为一个 bean 的定义注册到容器中,这里它将把`EnableAutoConfigurationImportSelector`作为 bean 注入到容器中,而这个类会将所有符合条件的@Configuration 配置都加载到容器中,看看它的代码: + +``` +public String[] selectImports(AnnotationMetadata annotationMetadata) { + // 省略了大部分代码,保留一句核心代码 + // 注意:SpringBoot最近版本中,这句代码被封装在一个单独的方法中 + // SpringFactoriesLoader相关知识请参考前文 + List factories = new ArrayList(new LinkedHashSet( + SpringFactoriesLoader.loadFactoryNames(EnableAutoConfiguration.class, this.beanClassLoader))); +} +``` + +这个类会扫描所有的 jar 包,将所有符合条件的@Configuration 配置类注入的容器中,何为符合条件,看看`META-INF/spring.factories`的文件内容: + +``` +// 来自 org.springframework.boot.autoconfigure下的META-INF/spring.factories +// 配置的key = EnableAutoConfiguration,与代码中一致 +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration,\ +org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,\ +org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration\ +..... +``` + +以`DataSourceAutoConfiguration`为例,看看 Spring Boot 是如何自动配置的: + +``` +@Configuration +@ConditionalOnClass({ DataSource.class, EmbeddedDatabaseType.class }) +@EnableConfigurationProperties(DataSourceProperties.class) +@Import({ Registrar.class, DataSourcePoolMetadataProvidersConfiguration.class }) +public class DataSourceAutoConfiguration { +} +``` + +分别说一说: + +- `@ConditionalOnClass({ DataSource.class, EmbeddedDatabaseType.class })`:当 Classpath 中存在 DataSource 或者 EmbeddedDatabaseType 类时才启用这个配置,否则这个配置将被忽略。 +- `@EnableConfigurationProperties(DataSourceProperties.class)`:将 DataSource 的默认配置类注入到 IOC 容器中,DataSourceproperties 定义为: + +``` +// 提供对datasource配置信息的支持,所有的配置前缀为:spring.datasource +@ConfigurationProperties(prefix = "spring.datasource") +public class DataSourceProperties { + private ClassLoader classLoader; + private Environment environment; + private String name = "testdb"; + ...... +} +``` + +- `@Import({ Registrar.class, DataSourcePoolMetadataProvidersConfiguration.class })`:导入其他额外的配置,就以`DataSourcePoolMetadataProvidersConfiguration`为例吧。 + +``` +@Configuration +public class DataSourcePoolMetadataProvidersConfiguration { + + @Configuration + @ConditionalOnClass(org.apache.tomcat.jdbc.pool.DataSource.class) + static class TomcatDataSourcePoolMetadataProviderConfiguration { + @Bean + public DataSourcePoolMetadataProvider tomcatPoolDataSourceMetadataProvider() { + ..... + } + } + ...... +} +``` + +DataSourcePoolMetadataProvidersConfiguration 是数据库连接池提供者的一个配置类,即 Classpath 中存在`org.apache.tomcat.jdbc.pool.DataSource.class`,则使用 tomcat-jdbc 连接池,如果 Classpath 中存在`HikariDataSource.class`则使用 Hikari 连接池。 + +这里仅描述了 DataSourceAutoConfiguration 的冰山一角,但足以说明 Spring Boot 如何利用条件话配置来实现自动配置的。回顾一下,`@EnableAutoConfiguration`中导入了 EnableAutoConfigurationImportSelector 类,而这个类的`selectImports()`通过 SpringFactoriesLoader 得到了大量的配置类,而每一个配置类则根据条件化配置来做出决策,以实现自动配置。 + +整个流程很清晰,但漏了一个大问题:`EnableAutoConfigurationImportSelector.selectImports()`是何时执行的?其实这个方法会在容器启动过程中执行:`AbstractApplicationContext.refresh()`,更多的细节在下一小节中说明。 + +## 六、启动引导:Spring Boot 应用启动的秘密 + +### 6.1 SpringApplication 初始化 + +SpringBoot 整个启动流程分为两个步骤:初始化一个 SpringApplication 对象、执行该对象的 run 方法。看下 SpringApplication 的初始化流程,SpringApplication 的构造方法中调用 initialize(Object[] sources)方法,其代码如下: + +``` +private void initialize(Object[] sources) { + if (sources != null && sources.length > 0) { + this.sources.addAll(Arrays.asList(sources)); + } + // 判断是否是Web项目 + this.webEnvironment = deduceWebEnvironment(); + setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class)); + setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class)); + // 找到入口类 + this.mainApplicationClass = deduceMainApplicationClass(); +} +``` + +初始化流程中最重要的就是通过 SpringFactoriesLoader 找到`spring.factories`文件中配置的`ApplicationContextInitializer`和`ApplicationListener`两个接口的实现类名称,以便后期构造相应的实例。`ApplicationContextInitializer`的主要目的是在`ConfigurableApplicationContext`做 refresh 之前,对 ConfigurableApplicationContext 实例做进一步的设置或处理。ConfigurableApplicationContext 继承自 ApplicationContext,其主要提供了对 ApplicationContext 进行设置的能力。 + +实现一个 ApplicationContextInitializer 非常简单,因为它只有一个方法,但大多数情况下我们没有必要自定义一个 ApplicationContextInitializer,即便是 Spring Boot 框架,它默认也只是注册了两个实现,毕竟 Spring 的容器已经非常成熟和稳定,你没有必要来改变它。 + +而`ApplicationListener`的目的就没什么好说的了,它是 Spring 框架对 Java 事件监听机制的一种框架实现,具体内容在前文 Spring 事件监听机制这个小节有详细讲解。这里主要说说,如果你想为 Spring Boot 应用添加监听器,该如何实现? + +Spring Boot 提供两种方式来添加自定义监听器: + +- 通过`SpringApplication.addListeners(ApplicationListener... listeners)`或者`SpringApplication.setListeners(Collection> listeners)`两个方法来添加一个或者多个自定义监听器 +- 既然 SpringApplication 的初始化流程中已经从`spring.factories`中获取到`ApplicationListener`的实现类,那么我们直接在自己的 jar 包的`META-INF/spring.factories`文件中新增配置即可: + +``` +org.springframework.context.ApplicationListener=\ +cn.moondev.listeners.xxxxListener\ +``` + +关于 SpringApplication 的初始化,我们就说这么多。 + +### 6.2 Spring Boot 启动流程 + +Spring Boot 应用的整个启动流程都封装在 SpringApplication.run 方法中,其整个流程真的是太长太长了,但本质上就是在 Spring 容器启动的基础上做了大量的扩展,按照这个思路来看看源码: + +``` +public ConfigurableApplicationContext run(String... args) { + StopWatch stopWatch = new StopWatch(); + stopWatch.start(); + ConfigurableApplicationContext context = null; + FailureAnalyzers analyzers = null; + configureHeadlessProperty(); + // ① + SpringApplicationRunListeners listeners = getRunListeners(args); + listeners.starting(); + try { + // ② + ApplicationArguments applicationArguments = new DefaultApplicationArguments(args); + ConfigurableEnvironment environment = prepareEnvironment(listeners,applicationArguments); + // ③ + Banner printedBanner = printBanner(environment); + // ④ + context = createApplicationContext(); + // ⑤ + analyzers = new FailureAnalyzers(context); + // ⑥ + prepareContext(context, environment, listeners, applicationArguments,printedBanner); + // ⑦ + refreshContext(context); + // ⑧ + afterRefresh(context, applicationArguments); + // ⑨ + listeners.finished(context, null); + stopWatch.stop(); + return context; + } + catch (Throwable ex) { + handleRunFailure(context, listeners, analyzers, ex); + throw new IllegalStateException(ex); + } + } +``` + +① 通过 SpringFactoriesLoader 查找并加载所有的`SpringApplicationRunListeners`,通过调用 starting()方法通知所有的 SpringApplicationRunListeners:应用开始启动了。SpringApplicationRunListeners 其本质上就是一个事件发布者,它在 SpringBoot 应用启动的不同时间点发布不同应用事件类型(ApplicationEvent),如果有哪些事件监听者(ApplicationListener)对这些事件感兴趣,则可以接收并且处理。还记得初始化流程中,SpringApplication 加载了一系列 ApplicationListener 吗?这个启动流程中没有发现有发布事件的代码,其实都已经在 SpringApplicationRunListeners 这儿实现了。 + +简单的分析一下其实现流程,首先看下 SpringApplicationRunListener 的源码: + +``` +public interface SpringApplicationRunListener { + + // 运行run方法时立即调用此方法,可以用户非常早期的初始化工作 + void starting(); + + // Environment准备好后,并且ApplicationContext创建之前调用 + void environmentPrepared(ConfigurableEnvironment environment); + + // ApplicationContext创建好后立即调用 + void contextPrepared(ConfigurableApplicationContext context); + + // ApplicationContext加载完成,在refresh之前调用 + void contextLoaded(ConfigurableApplicationContext context); + + // 当run方法结束之前调用 + void finished(ConfigurableApplicationContext context, Throwable exception); + +} +``` + +SpringApplicationRunListener 只有一个实现类:`EventPublishingRunListener`。① 处的代码只会获取到一个 EventPublishingRunListener 的实例,我们来看看 starting()方法的内容: + +``` +public void starting() { + // 发布一个ApplicationStartedEvent + this.initialMulticaster.multicastEvent(new ApplicationStartedEvent(this.application, this.args)); +} +``` + +顺着这个逻辑,你可以在 ② 处的`prepareEnvironment()`方法的源码中找到`listeners.environmentPrepared(environment);`即 SpringApplicationRunListener 接口的第二个方法,那不出你所料,`environmentPrepared()`又发布了另外一个事件`ApplicationEnvironmentPreparedEvent`。接下来会发生什么,就不用我多说了吧。 + +② 创建并配置当前应用将要使用的`Environment`,Environment 用于描述应用程序当前的运行环境,其抽象了两个方面的内容:配置文件(profile)和属性(properties),开发经验丰富的同学对这两个东西一定不会陌生:不同的环境(eg:生产环境、预发布环境)可以使用不同的配置文件,而属性则可以从配置文件、环境变量、命令行参数等来源获取。因此,当 Environment 准备好后,在整个应用的任何时候,都可以从 Environment 中获取资源。 + +总结起来,② 处的两句代码,主要完成以下几件事: + +- 判断 Environment 是否存在,不存在就创建(如果是 web 项目就创建`StandardServletEnvironment`,否则创建`StandardEnvironment`) +- 配置 Environment:配置 profile 以及 properties +- 调用 SpringApplicationRunListener 的`environmentPrepared()`方法,通知事件监听者:应用的 Environment 已经准备好 + +③、SpringBoot 应用在启动时会输出这样的东西: + +``` + . ____ _ __ _ _ + /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \ +( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \ + \\/ ___)| |_)| | | | | || (_| | ) ) ) ) + ' |____| .__|_| |_|_| |_\__, | / / / / + =========|_|==============|___/=/_/_/_/ + :: Spring Boot :: (v1.5.6.RELEASE) +``` + +如果想把这个东西改成自己的涂鸦,你可以研究以下 Banner 的实现,这个任务就留给你们吧。 + +④、根据是否是 web 项目,来创建不同的 ApplicationContext 容器。 + +⑤、创建一系列`FailureAnalyzer`,创建流程依然是通过 SpringFactoriesLoader 获取到所有实现 FailureAnalyzer 接口的 class,然后在创建对应的实例。FailureAnalyzer 用于分析故障并提供相关诊断信息。 + +⑥、初始化 ApplicationContext,主要完成以下工作: + +- 将准备好的 Environment 设置给 ApplicationContext +- 遍历调用所有的 ApplicationContextInitializer 的`initialize()`方法来对已经创建好的 ApplicationContext 进行进一步的处理 +- 调用 SpringApplicationRunListener 的`contextPrepared()`方法,通知所有的监听者:ApplicationContext 已经准备完毕 +- 将所有的 bean 加载到容器中 +- 调用 SpringApplicationRunListener 的`contextLoaded()`方法,通知所有的监听者:ApplicationContext 已经装载完毕 + +⑦、调用 ApplicationContext 的`refresh()`方法,完成 IoC 容器可用的最后一道工序。从名字上理解为刷新容器,那何为刷新?就是插手容器的启动,联系一下第一小节的内容。那如何刷新呢?且看下面代码: + +``` +// 摘自refresh()方法中一句代码 +invokeBeanFactoryPostProcessors(beanFactory); +``` + +看看这个方法的实现: + +``` +protected void invokeBeanFactoryPostProcessors(ConfigurableListableBeanFactory beanFactory) { + PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(beanFactory, getBeanFactoryPostProcessors()); + ...... +} +``` + +获取到所有的`BeanFactoryPostProcessor`来对容器做一些额外的操作。BeanFactoryPostProcessor 允许我们在容器实例化相应对象之前,对注册到容器的 BeanDefinition 所保存的信息做一些额外的操作。这里的 getBeanFactoryPostProcessors()方法可以获取到 3 个 Processor: + +``` +ConfigurationWarningsApplicationContextInitializer$ConfigurationWarningsPostProcessor +SharedMetadataReaderFactoryContextInitializer$CachingMetadataReaderFactoryPostProcessor +ConfigFileApplicationListener$PropertySourceOrderingPostProcessor +``` + +不是有那么多 BeanFactoryPostProcessor 的实现类,为什么这儿只有这 3 个?因为在初始化流程获取到的各种 ApplicationContextInitializer 和 ApplicationListener 中,只有上文 3 个做了类似于如下操作: + +``` +public void initialize(ConfigurableApplicationContext context) { + context.addBeanFactoryPostProcessor(new ConfigurationWarningsPostProcessor(getChecks())); +} +``` + +然后你就可以进入到`PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors()`方法了,这个方法除了会遍历上面的 3 个 BeanFactoryPostProcessor 处理外,还会获取类型为`BeanDefinitionRegistryPostProcessor`的 bean:`org.springframework.context.annotation.internalConfigurationAnnotationProcessor`,对应的 Class 为`ConfigurationClassPostProcessor`。`ConfigurationClassPostProcessor`用于解析处理各种注解,包括:@Configuration、@ComponentScan、@Import、@PropertySource、@ImportResource、@Bean。当处理`@import`注解的时候,就会调用自动配置这一小节中的`EnableAutoConfigurationImportSelector.selectImports()`来完成自动配置功能。其他的这里不再多讲,如果你有兴趣,可以查阅参考资料 6。 + +⑧、查找当前 context 中是否注册有 CommandLineRunner 和 ApplicationRunner,如果有则遍历执行它们。 + +⑨、执行所有 SpringApplicationRunListener 的 finished()方法。 + +这就是 Spring Boot 的整个启动流程,其核心就是在 Spring 容器初始化并启动的基础上加入各种扩展点,这些扩展点包括:ApplicationContextInitializer、ApplicationListener 以及各种 BeanFactoryPostProcessor 等等。你对整个流程的细节不必太过关注,甚至没弄明白也没有关系,你只要理解这些扩展点是在何时如何工作的,能让它们为你所用即可。 + +整个启动流程确实非常复杂,可以查询参考资料中的部分章节和内容,对照着源码,多看看,我想最终你都能弄清楚的。言而总之,Spring 才是核心,理解清楚 Spring 容器的启动流程,那 Spring Boot 启动流程就不在话下了。 + +## 参考资料 + +[1][王福强 著;springboot 揭秘:快速构建微服务体系; 机械工业出版社, 2016](https://link.jianshu.com/?t=http%3A%2F%2Funion-click.jd.com%2Fjdc%3Fd%3D4jESQ9) +[2][王福强 著;spring 揭秘; 人民邮件出版社, 2009](https://link.jianshu.com/?t=http%3A%2F%2Funion-click.jd.com%2Fjdc%3Fd%3DyzfgeF) +[3][craig walls 著;丁雪丰 译;spring boot 实战;中国工信出版集团 人民邮电出版社,2016](https://link.jianshu.com/?t=http%3A%2F%2Funion-click.jd.com%2Fjdc%3Fd%3DAQ6oHO) +[4][深入探讨 java 类加载器](https://link.jianshu.com/?t=https%3A%2F%2Fwww.ibm.com%2Fdeveloperworks%2Fcn%2Fjava%2Fj-lo-classloader%2F) : [www.ibm.com/developerwo…](https://link.jianshu.com/?t=https%3A%2F%2Fwww.ibm.com%2Fdeveloperworks%2Fcn%2Fjava%2Fj-lo-classloader%2F) +[5][spring boot 实战:自动配置原理分析](https://link.jianshu.com/?t=http%3A%2F%2Fblog.csdn.net%2Fliaokailin%2Farticle%2Fdetails%2F49559951) : [blog.csdn.net/liaokailin/…](https://link.jianshu.com/?t=http%3A%2F%2Fblog.csdn.net%2Fliaokailin%2Farticle%2Fdetails%2F49559951) +[6][spring boot实战:spring boot bean加载源码分析](https://link.jianshu.com/?t=http%3A%2F%2Fblog.csdn.net%2Fliaokailin%2Farticle%2Fdetails%2F49107209): [blog.csdn.net/liaokailin/…](https://link.jianshu.com/?t=http%3A%2F%2Fblog.csdn.net%2Fliaokailin%2Farticle%2Fdetails%2F49107209) \ No newline at end of file diff --git "a/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/00.Spring\347\273\274\345\220\210/22.SpringBoot\345\237\272\346\234\254\345\216\237\347\220\206.md" "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/00.Spring\347\273\274\345\220\210/22.SpringBoot\345\237\272\346\234\254\345\216\237\347\220\206.md" new file mode 100644 index 00000000..a7ee34ae --- /dev/null +++ "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/00.Spring\347\273\274\345\220\210/22.SpringBoot\345\237\272\346\234\254\345\216\237\347\220\206.md" @@ -0,0 +1,286 @@ +--- +title: SpringBoot 基本原理 +date: 2020-08-12 07:01:26 +order: 22 +categories: + - Java + - 框架 + - Spring + - Spring综合 +tags: + - Java + - 框架 + - Spring + - SpringBoot +permalink: /pages/dbf521/ +--- + +# SpringBoot 基本原理 + +SpringBoot 为我们做的自动配置,确实方便快捷,但一直搞不明白它的内部启动原理,这次就来一步步解开 SpringBoot 的神秘面纱,让它不再神秘。 + +![img](https:////upload-images.jianshu.io/upload_images/6430208-ebcb376f96103703.png?imageMogr2/auto-orient/strip|imageView2/2/w/1200/format/webp) + +--- + +```java +@SpringBootApplication +public class Application { + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } +} +``` + +从上面代码可以看出,**Annotation 定义(@SpringBootApplication)和类定义(SpringApplication.run)**最为耀眼,所以要揭开 SpringBoot 的神秘面纱,我们要从这两位开始就可以了。 + +## SpringBootApplication 背后的秘密 + +```kotlin +@Target(ElementType.TYPE) // 注解的适用范围,其中TYPE用于描述类、接口(包括包注解类型)或enum声明 +@Retention(RetentionPolicy.RUNTIME) // 注解的生命周期,保留到class文件中(三个生命周期) +@Documented // 表明这个注解应该被javadoc记录 +@Inherited // 子类可以继承该注解 +@SpringBootConfiguration // 继承了Configuration,表示当前是注解类 +@EnableAutoConfiguration // 开启springboot的注解功能,springboot的四大神器之一,其借助@import的帮助 +@ComponentScan(excludeFilters = { // 扫描路径设置(具体使用待确认) + @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class), + @Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) }) +public @interface SpringBootApplication { +... +} +``` + +虽然定义使用了多个 Annotation 进行了原信息标注,但实际上重要的只有三个 Annotation: + +**@Configuration**(@SpringBootConfiguration 点开查看发现里面还是应用了@Configuration) +**@EnableAutoConfiguration +@ComponentScan** +所以,如果我们使用如下的 SpringBoot 启动类,整个 SpringBoot 应用依然可以与之前的启动类功能对等: + +```java +@Configuration +@EnableAutoConfiguration +@ComponentScan +public class Application { + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } +} +``` + +每次写这 3 个比较累,所以写一个@SpringBootApplication 方便点。接下来分别介绍这 3 个 Annotation。 + +## @Configuration + +这里的@Configuration 对我们来说不陌生,**它就是 JavaConfig 形式的 Spring Ioc 容器的配置类使用的那个@Configuration**,SpringBoot 社区推荐使用基于 JavaConfig 的配置形式,所以,这里的启动类标注了@Configuration 之后,本身其实也是一个 IoC 容器的配置类。 +举几个简单例子回顾下,XML 跟 config 配置方式的区别: + +表达形式层面 +基于 XML 配置的方式是这样: + +```xml + + + + +``` + +而基于 JavaConfig 的配置方式是这样: + +```java +@Configuration +public class MockConfiguration{ + //bean定义 +} +``` + +**任何一个标注了@Configuration 的 Java 类定义都是一个 JavaConfig 配置类。** + +注册 bean 定义层面 +基于 XML 的配置形式是这样: + +```csharp + + ... + +``` + +而基于 JavaConfig 的配置形式是这样的: + +```java +@Configuration +public class MockConfiguration{ + @Bean + public MockService mockService(){ + return new MockServiceImpl(); + } +} +``` + +**任何一个标注了@Bean 的方法,其返回值将作为一个 bean 定义注册到 Spring 的 IoC 容器,方法名将默认成该 bean 定义的 id。** + +表达依赖注入关系层面 +为了表达 bean 与 bean 之间的依赖关系,在 XML 形式中一般是这样: + +```jsx + + + + + +``` + +而基于 JavaConfig 的配置形式是这样的: + +```java +@Configuration +public class MockConfiguration{ + @Bean + public MockService mockService(){ + return new MockServiceImpl(dependencyService()); + } + + @Bean + public DependencyService dependencyService(){ + return new DependencyServiceImpl(); + } +} +``` + +**如果一个 bean 的定义依赖其他 bean,则直接调用对应的 JavaConfig 类中依赖 bean 的创建方法就可以了。** + +## @ComponentScan + +**@ComponentScan 这个注解在 Spring 中很重要,它对应 XML 配置中的元素,@ComponentScan 的功能其实就是自动扫描并加载符合条件的组件(比如@Component 和@Repository 等)或者 bean 定义,最终将这些 bean 定义加载到 IoC 容器中。** + +我们可以通过 basePackages 等属性来细粒度的定制@ComponentScan 自动扫描的范围,如果不指定,则默认 Spring 框架实现会从声明@ComponentScan 所在类的 package 进行扫描。 + +> 注:所以 SpringBoot 的启动类最好是放在 root package 下,因为默认不指定 basePackages。 + +## @EnableAutoConfiguration + +个人感觉**@EnableAutoConfiguration 这个 Annotation 最为重要**,所以放在最后来解读,大家是否还记得 Spring 框架提供的各种名字为@Enable 开头的 Annotation 定义?比如@EnableScheduling、@EnableCaching、@EnableMBeanExport 等,@EnableAutoConfiguration 的理念和做事方式其实一脉相承,简单概括一下就是,借助@Import 的支持,收集和注册特定场景相关的 bean 定义。 + +**@EnableScheduling**是通过@Import 将 Spring 调度框架相关的 bean 定义都加载到 IoC 容器。 +**@EnableMBeanExport**是通过@Import 将 JMX 相关的 bean 定义加载到 IoC 容器。 +而**@EnableAutoConfiguration**也是借助@Import 的帮助,将所有符合自动配置条件的 bean 定义加载到 IoC 容器,仅此而已! + +@EnableAutoConfiguration 作为一个复合 Annotation,其自身定义关键信息如下: + +```kotlin +@SuppressWarnings("deprecation") +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +@AutoConfigurationPackage +@Import(EnableAutoConfigurationImportSelector.class) +public @interface EnableAutoConfiguration { + ... +} +``` + +两个比较重要的注解: + +**@AutoConfigurationPackage:自动配置包** + +**@Import: 导入自动配置的组件** + +#### AutoConfigurationPackage 注解: + +```java +static class Registrar implements ImportBeanDefinitionRegistrar, DeterminableImports { + + @Override + public void registerBeanDefinitions(AnnotationMetadata metadata, + BeanDefinitionRegistry registry) { + register(registry, new PackageImport(metadata).getPackageName()); + } +``` + +它其实是注册了一个 Bean 的定义。 + +new PackageImport(metadata).getPackageName(),它其实返回了当前主程序类的 同级以及子级 的包组件。 + +![img](https:////upload-images.jianshu.io/upload_images/6430208-439283a70a24c7a0.jpg?imageMogr2/auto-orient/strip|imageView2/2/w/281/format/webp) + +以上图为例,DemoApplication 是和 demo 包同级,但是 demo2 这个类是 DemoApplication 的父级,和 example 包同级 + +也就是说,DemoApplication 启动加载的 Bean 中,并不会加载 demo2,这也就是为什么,我们要把 DemoApplication 放在项目的最高级中。 + +#### Import(AutoConfigurationImportSelector.class)注解: + +![img](https:////upload-images.jianshu.io/upload_images/6430208-1c448a69c41dc35c.png?imageMogr2/auto-orient/strip|imageView2/2/w/877/format/webp) + +可以从图中看出 AutoConfigurationImportSelector 继承了 DeferredImportSelector 继承了 ImportSelector + +ImportSelector 有一个方法为:selectImports。 + +```dart +@Override + public String[] selectImports(AnnotationMetadata annotationMetadata) { + if (!isEnabled(annotationMetadata)) { + return NO_IMPORTS; + } + AutoConfigurationMetadata autoConfigurationMetadata = AutoConfigurationMetadataLoader + .loadMetadata(this.beanClassLoader); + AnnotationAttributes attributes = getAttributes(annotationMetadata); + List configurations = getCandidateConfigurations(annotationMetadata, + attributes); + configurations = removeDuplicates(configurations); + Set exclusions = getExclusions(annotationMetadata, attributes); + checkExcludedClasses(configurations, exclusions); + configurations.removeAll(exclusions); + configurations = filter(configurations, autoConfigurationMetadata); + fireAutoConfigurationImportEvents(configurations, exclusions); + return StringUtils.toStringArray(configurations); + } +``` + +可以看到第九行,它其实是去加载 public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";外部文件。这个外部文件,有很多自动配置的类。如下: + +![img](https:////upload-images.jianshu.io/upload_images/6430208-250f3320c15e5c99.jpg?imageMogr2/auto-orient/strip|imageView2/2/w/1200/format/webp) + +image + +其中,最关键的要属**@Import(EnableAutoConfigurationImportSelector.class)**,借助**EnableAutoConfigurationImportSelector**,**@EnableAutoConfiguration**可以帮助 SpringBoot 应用将所有符合条件的**@Configuration**配置都加载到当前 SpringBoot 创建并使用的 IoC 容器。就像一只“八爪鱼”一样。 + +![img](https:////upload-images.jianshu.io/upload_images/6430208-6f3a835755ee7710.png?imageMogr2/auto-orient/strip|imageView2/2/w/640/format/webp) + +### 自动配置幕后英雄:SpringFactoriesLoader 详解 + +借助于 Spring 框架原有的一个工具类:SpringFactoriesLoader 的支持,@EnableAutoConfiguration 可以智能的自动配置功效才得以大功告成! + +SpringFactoriesLoader 属于 Spring 框架私有的一种扩展方案,其主要功能就是从指定的配置文件 META-INF/spring.factories 加载配置。 + +```php +public abstract class SpringFactoriesLoader { + //... + public static List loadFactories(Class factoryClass, ClassLoader classLoader) { + ... + } + + + public static List loadFactoryNames(Class factoryClass, ClassLoader classLoader) { + .... + } +} +``` + +配合**@EnableAutoConfiguration**使用的话,它更多是提供一种配置查找的功能支持,即根据@EnableAutoConfiguration 的完整类名 org.springframework.boot.autoconfigure.EnableAutoConfiguration 作为查找的 Key,获取对应的一组**@Configuration**类 + +![img](https:////upload-images.jianshu.io/upload_images/6430208-fcdfcb56828a015a?imageMogr2/auto-orient/strip|imageView2/2/w/1200/format/webp) + +上图就是从 SpringBoot 的 autoconfigure 依赖包中的 META-INF/spring.factories 配置文件中摘录的一段内容,可以很好地说明问题。 + +所以,@EnableAutoConfiguration 自动配置的魔法骑士就变成了:**从 classpath 中搜寻所有的 META-INF/spring.factories 配置文件,并将其中 org.springframework.boot.autoconfigure.EnableutoConfiguration 对应的配置项通过反射(Java Refletion)实例化为对应的标注了@Configuration 的 JavaConfig 形式的 IoC 容器配置类,然后汇总为一个并加载到 IoC 容器。** + +![img](https:////upload-images.jianshu.io/upload_images/6430208-10850d62d44c95ce.png?imageMogr2/auto-orient/strip|imageView2/2/w/822/format/webp) + +## 参考资料 + +- [一文搞懂 springboot 启动原理](https://www.jianshu.com/p/943650ab7dfd) \ No newline at end of file diff --git "a/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/00.Spring\347\273\274\345\220\210/99.Spring\351\235\242\350\257\225.md" "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/00.Spring\347\273\274\345\220\210/99.Spring\351\235\242\350\257\225.md" new file mode 100644 index 00000000..461f16fb --- /dev/null +++ "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/00.Spring\347\273\274\345\220\210/99.Spring\351\235\242\350\257\225.md" @@ -0,0 +1,612 @@ +--- +title: Spring 面试 +date: 2018-08-02 17:33:32 +order: 99 +categories: + - Java + - 框架 + - Spring + - Spring综合 +tags: + - Java + - 框架 + - Spring + - 面试 +permalink: /pages/db33b0/ +--- + +# Spring 面试 + +## 综合篇 + +### 不同版本的 Spring Framework 有哪些主要功能? + +| Version | Feature | +| ---------- | ------------------------------------------------------------------- | +| Spring 2.5 | 发布于 2007 年。这是第一个支持注解的版本。 | +| Spring 3.0 | 发布于 2009 年。它完全利用了 Java5 中的改进,并为 JEE6 提供了支持。 | +| Spring 4.0 | 发布于 2013 年。这是第一个完全支持 JAVA8 的版本。 | + +### 什么是 Spring Framework? + +- Spring 是一个开源应用框架,旨在降低应用程序开发的复杂度。 +- 它是轻量级、松散耦合的。 +- 它具有分层体系结构,允许用户选择组件,同时还为 J2EE 应用程序开发提供了一个有凝聚力的框架。 +- 它可以集成其他框架,如 Structs、Hibernate、EJB 等,所以又称为框架的框架。 + +### 列举 Spring Framework 的优点。 + +- 由于 Spring Frameworks 的分层架构,用户可以自由选择自己需要的组件。 +- Spring Framework 支持 POJO(Plain Old Java Object) 编程,从而具备持续集成和可测试性。 +- 由于依赖注入和控制反转,JDBC 得以简化。 +- 它是开源免费的。 + +### Spring Framework 有哪些不同的功能? + +- **轻量级** - Spring 在代码量和透明度方面都很轻便。 +- **IOC** - 控制反转 +- **AOP** - 面向切面编程可以将应用业务逻辑和系统服务分离,以实现高内聚。 +- **容器** - Spring 负责创建和管理对象(Bean)的生命周期和配置。 +- **MVC** - 对 web 应用提供了高度可配置性,其他框架的集成也十分方便。 +- **事务管理** - 提供了用于事务管理的通用抽象层。Spring 的事务支持也可用于容器较少的环境。 +- **JDBC 异常** - Spring 的 JDBC 抽象层提供了一个异常层次结构,简化了错误处理策略。 + +### Spring Framework 中有多少个模块,它们分别是什么? + +![img](https://raw.githubusercontent.com/dunwu/images/master/cs/java/spring/spring-framework.png) + +- **Spring 核心容器** – 该层基本上是 Spring Framework 的核心。它包含以下模块: + - Spring Core + - Spring Bean + - SpEL (Spring Expression Language) + - Spring Context +- **数据访问/集成** – 该层提供与数据库交互的支持。它包含以下模块: + - JDBC (Java DataBase Connectivity) + - ORM (Object Relational Mapping) + - OXM (Object XML Mappers) + - JMS (Java Messaging Service) + - Transaction +- **Web** – 该层提供了创建 Web 应用程序的支持。它包含以下模块: + - Web + - Web – Servlet + - Web – Socket + - Web – Portlet +- **AOP** – 该层支持面向切面编程 +- **Instrumentation** – 该层为类检测和类加载器实现提供支持。 +- **Test** – 该层为使用 JUnit 和 TestNG 进行测试提供支持。 +- **几个杂项模块:** + - Messaging – 该模块为 STOMP 提供支持。它还支持注解编程模型,该模型用于从 WebSocket 客户端路由和处理 STOMP 消息。 + - Aspects – 该模块为与 AspectJ 的集成提供支持。 + +### 什么是 Spring 配置文件? + +Spring 配置文件是 XML 文件。该文件主要包含类信息。它描述了这些类是如何配置以及相互引入的。但是,XML 配置文件冗长且更加干净。如果没有正确规划和编写,那么在大项目中管理变得非常困难。 + +### Spring 应用程序有哪些不同组件? + +Spring 应用一般有以下组件: + +- **接口** - 定义功能。 +- **Bean 类** - 它包含属性,setter 和 getter 方法,函数等。 +- **Spring 面向切面编程(AOP)** - 提供面向切面编程的功能。 +- **Bean 配置文件** - 包含类的信息以及如何配置它们。 +- **用户程序** - 它使用接口。 + +### 使用 Spring 有哪些方式? + +使用 Spring 有以下方式: + +- 作为一个成熟的 Spring Web 应用程序。 +- 作为第三方 Web 框架,使用 Spring Frameworks 中间层。 +- 用于远程使用。 +- 作为企业级 Java Bean,它可以包装现有的 POJO(Plain Old Java Objects)。 + +## 核心篇 + +### IoC + +#### 什么是 IoC?什么是依赖注入?什么是 Spring IoC? + +**IoC** 即**控制反转**(Inversion of Control,缩写为 IoC)。IoC 又称为**依赖倒置原则**(设计模式六大原则之一),它的要点在于:**程序要依赖于抽象接口,不要依赖于具体实现**。它的作用就是**用于降低代码间的耦合度**。 + +IoC 的实现方式有两种: + +- **依赖注入**(Dependency Injection,简称 DI):不通过 `new()` 的方式在类内部创建依赖类对象,而是将依赖的类对象在外部创建好之后,通过构造器、函数参数等方式传递(或注入)给类使用。 +- **依赖查找**(Dependency Lookup):容器中的受控对象通过容器的 API 来查找自己所依赖的资源和协作对象。 + +Spring IoC 是 IoC 的一种实现。DI 是 Spring IoC 的主要实现原则。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/20221005163639.png) + +#### 依赖注入有哪些实现方式? + +依赖注入有如下方式: + +| 依赖注入方式 | 配置元数据举例 | +| --------------- | -------------------------------------------------- | +| Setter 方法注入 | `` | +| 构造器注入 | `` | +| 字段注入 | `@Autowired User user;` | +| 方法注入 | `@Autowired public void user(User user) { ... }` | +| 接口回调注入 | `class MyBean implements BeanFactoryAware { ... }` | + +#### 构造器注入 VS. setter 注入 + +| 构造器注入 | setter 注入 | +| -------------------------- | -------------------------- | +| 没有部分注入 | 有部分注入 | +| 不会覆盖 setter 属性 | 会覆盖 setter 属性 | +| 任意修改都会创建一个新实例 | 任意修改不会创建一个新实例 | +| 适用于设置很多属性 | 适用于设置少量属性 | + +官方推荐使用构造器注入。 + +#### BeanFactory VS. ApplicationContext + +在 Spring 中,有两种 IoC 容器:`BeanFactory` 和 `ApplicationContext`。 + +- `BeanFactory`:**`BeanFactory` 是 Spring 基础 IoC 容器**。`BeanFactory` 提供了 Spring 容器的配置框架和基本功能。 +- `ApplicationContext`:**`ApplicationContext` 是具备应用特性的 `BeanFactory` 的子接口**。它还扩展了其他一些接口,以支持更丰富的功能,如:国际化、访问资源、事件机制、更方便的支持 AOP、在 web 应用中指定应用层上下文等。 + +实际开发中,更推荐使用 `ApplicationContext` 作为 IoC 容器,因为它的功能远多于 `BeanFactory`。 + +#### BeanFactory VS. FactoryBean + +**`BeanFactory` 是 Spring 基础 IoC 容器**。 + +`FactoryBean` 是创建 Bean 的一种方式,帮助实现复杂的初始化逻辑。 + +#### Spring IoC 启动时做了哪些准备 + +IoC 配置元信息读取和解析 + +IoC 容器生命周期管理 + +Spring 事件发布 + +国际化 + +等等 + +#### Spring IoC 的实现机制是什么 + +Spring 中的 IoC 的实现原理就是工厂模式加反射机制。 + +示例: + +```java +interface Fruit { + public abstract void eat(); +} +class Apple implements Fruit { + public void eat(){ + System.out.println("Apple"); + } +} +class Orange implements Fruit { + public void eat(){ + System.out.println("Orange"); + } +} +class Factory { + public static Fruit getInstance(String ClassName) { + Fruit f=null; + try { + f=(Fruit)Class.forName(ClassName).newInstance(); + } catch (Exception e) { + e.printStackTrace(); + } + return f; + } +} +class Client { + public static void main(String[] a) { + Fruit f=Factory.getInstance("io.github.dunwu.spring.Apple"); + if(f!=null){ + f.eat(); + } + } +} +``` + +### Bean + +#### 什么是 Spring Bean + +在 Spring 中,构成应用程序主体由 Spring IoC 容器管理的对象称为 Bean。**Bean 是由 Spring IoC 容器实例化、装配和管理的对象**。 Bean 以及它们之间的依赖关系反映在容器使用的配置元数据中。 + +Spring IoC 容器本身,并不能识别配置的元数据。为此,要将这些配置信息转为 Spring 能识别的格式——`BeanDefinition` 对象。 + +**`BeanDefinition` 是 Spring 中定义 Bean 的配置元信息接口**,它包含: + +- Bean 类名 +- Bean 行为配置元素,如:作用域、自动绑定的模式、生命周期回调等 +- 其他 Bean 引用,也可称为合作者(Collaborators)或依赖(Dependencies) +- 配置设置,如 Bean 属性(Properties) + +#### 如何注册 Spring Bean + +通过 `BeanDefinition` 和外部单例对象来注册。 + +#### spring 提供了哪些配置方式? + +- 基于 xml 配置 + +bean 所需的依赖项和服务在 XML 格式的配置文件中指定。这些配置文件通常包含许多 bean 定义和特定于应用程序的配置选项。它们通常以 bean 标签开头。例如: + +```xml + + + +``` + +- 基于注解配置 + +您可以通过在相关的类,方法或字段声明上使用注解,将 bean 配置为组件类本身,而不是使用 XML 来描述 bean 装配。默认情况下,Spring 容器中未打开注解装配。因此,您需要在使用它之前在 Spring 配置文件中启用它。例如: + +```xml + + + + +``` + +- 基于 Java API 配置 + +Spring 的 Java 配置是通过使用 @Bean 和 @Configuration 来实现。 + +1. @Bean 注解扮演与 `` 元素相同的角色。 +2. @Configuration 类允许通过简单地调用同一个类中的其他 @Bean 方法来定义 bean 间依赖关系。 + +例如: + +```java +@Configuration +public class StudentConfig { + @Bean + public StudentBean myStudent() { + return new StudentBean(); + } +} +``` + +#### spring 支持集中 bean scope? + +Spring bean 支持 5 种 scope: + +- **Singleton** - 每个 Spring IoC 容器仅有一个单实例。 +- **Prototype** - 每次请求都会产生一个新的实例。 +- **Request** - 每一次 HTTP 请求都会产生一个新的实例,并且该 bean 仅在当前 HTTP 请求内有效。 +- **Session** - 每一次 HTTP 请求都会产生一个新的 bean,同时该 bean 仅在当前 HTTP session 内有效。 +- **Global-session** - 类似于标准的 HTTP Session 作用域,不过它仅仅在基于 portlet 的 web 应用中才有意义。Portlet 规范定义了全局 Session 的概念,它被所有构成某个 portlet web 应用的各种不同的 portlet 所共享。在 global session 作用域中定义的 bean 被限定于全局 portlet Session 的生命周期范围内。如果你在 web 中使用 global session 作用域来标识 bean,那么 web 会自动当成 session 类型来使用。 + +仅当用户使用支持 Web 的 ApplicationContext 时,最后三个才可用。 + +#### Spring Bean 的生命周期 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/20211201102734.png) + +spring bean 容器的生命周期如下: + +1. Spring 对 Bean 进行实例化(相当于 new XXX()) + +2. Spring 将值和引用注入到 Bean 对应的属性中 + +3. 如果 Bean 实现了 `BeanNameAware` 接口,Spring 将 Bean 的 ID 传递给 `setBeanName` 方法 + - 作用是通过 Bean 的引用来获得 Bean ID,一般业务中是很少有用到 Bean 的 ID 的 +4. 如果 Bean 实现了 `BeanFactoryAware` 接口,Spring 将调用 `setBeanDactory` 方法,并把 `BeanFactory` 容器实例作为参数传入。 + - 作用是获取 Spring 容器,如 Bean 通过 Spring 容器发布事件等 +5. 如果 Bean 实现了 `ApplicationContextAware` 接口,Spring 容器将调用 `setApplicationContext` 方法,把应用上下文作为参数传入 + - 作用与 `BeanFactory` 类似都是为了获取 Spring 容器,不同的是 Spring 容器在调用 `setApplicationContext` 方法时会把它自己作为 `setApplicationContext` 的参数传入,而 Spring 容器在调用 `setBeanFactory` 前需要使用者自己指定(注入)`setBeanFactory` 里的参数 `BeanFactory` +6. 如果 Bean 实现了 `BeanPostProcess` 接口,Spring 将调用 `postProcessBeforeInitialization` 方法 + - 作用是在 Bean 实例创建成功后对其进行增强处理,如对 Bean 进行修改,增加某个功能 +7. 如果 Bean 实现了 `InitializingBean` 接口,Spring 将调用 `afterPropertiesSet` 方法,作用与在配置文件中对 Bean 使用 `init-method` 声明初始化的作用一样,都是在 Bean 的全部属性设置成功后执行的初始化方法。 +8. 如果 Bean 实现了 `BeanPostProcess` 接口,Spring 将调用 `postProcessAfterInitialization` 方法 + - `postProcessBeforeInitialization` 是在 Bean 初始化前执行的,而 `postProcessAfterInitialization` 是在 Bean 初始化后执行的 +9. 经过以上的工作后,Bean 将一直驻留在应用上下文中给应用使用,直到应用上下文被销毁 +10. 如果 Bean 实现了 `DispostbleBean` 接口,Spring 将调用它的 `destory` 方法,作用与在配置文件中对 Bean 使用 `destory-method` 属性的作用一样,都是在 Bean 实例销毁前执行的方法。 + +#### 什么是 spring 的内部 bean? + +只有将 bean 用作另一个 bean 的属性时,才能将 bean 声明为内部 bean。为了定义 bean,Spring 的基于 XML 的配置元数据在 `` 或 `` 中提供了 `` 元素的使用。内部 bean 总是匿名的,它们总是作为原型。 + +例如,假设我们有一个 Student 类,其中引用了 Person 类。这里我们将只创建一个 Person 类实例并在 Student 中使用它。 + +Student.java + +```java +public class Student { + private Person person; + //Setters and Getters +} +public class Person { + private String name; + private String address; + //Setters and Getters +} +``` + +bean.xml + +```xml + + + + + + + + + +``` + +#### 什么是 spring 装配 + +当 bean 在 Spring 容器中组合在一起时,它被称为装配或 bean 装配。 Spring 容器需要知道需要什么 bean 以及容器应该如何使用依赖注入来将 bean 绑定在一起,同时装配 bean。 + +#### 自动装配有哪些方式? + +Spring 容器能够自动装配 bean。也就是说,可以通过检查 BeanFactory 的内容让 Spring 自动解析 bean 的协作者。 + +自动装配的不同模式: + +- **no** - 这是默认设置,表示没有自动装配。应使用显式 bean 引用进行装配。 +- **byName** - 它根据 bean 的名称注入对象依赖项。它匹配并装配其属性与 XML 文件中由相同名称定义的 bean。 +- **byType** - 它根据类型注入对象依赖项。如果属性的类型与 XML 文件中的一个 bean 名称匹配,则匹配并装配属性。 +- **构造器** - 它通过调用类的构造器来注入依赖项。它有大量的参数。 +- **autodetect** - 首先容器尝试通过构造器使用 autowire 装配,如果不能,则尝试通过 byType 自动装配。 + +#### 自动装配有什么局限? + +- 覆盖的可能性 - 您始终可以使用 `` 和 `` 设置指定依赖项,这将覆盖自动装配。 +- 基本元数据类型 - 简单属性(如原数据类型,字符串和类)无法自动装配。 +- 令人困惑的性质 - 总是喜欢使用明确的装配,因为自动装配不太精确。 + +### AOP + +#### 什么是 AOP? + +AOP(Aspect-Oriented Programming), 即 **面向切面编程**, 它与 OOP( Object-Oriented Programming, 面向对象编程) 相辅相成, 提供了与 OOP 不同的抽象软件结构的视角. +在 OOP 中, 我们以类(class)作为我们的基本单元, 而 AOP 中的基本单元是 **Aspect(切面)** + +#### AOP 中的 Aspect、Advice、Pointcut、JointPoint 和 Advice 参数分别是什么? + +![img](https://raw.githubusercontent.com/dunwu/images/master/cs/java/spring/core/spring-aop.png) + +- **Aspect** - Aspect 是一个实现交叉问题的类,例如事务管理。方面可以是配置的普通类,然后在 Spring Bean 配置文件中配置,或者我们可以使用 Spring AspectJ 支持使用 @Aspect 注解将类声明为 Aspect。 +- **Advice** - Advice 是针对特定 JoinPoint 采取的操作。在编程方面,它们是在应用程序中达到具有匹配切入点的特定 JoinPoint 时执行的方法。您可以将 Advice 视为 Spring 拦截器(Interceptor)或 Servlet 过滤器(filter)。 +- **Advice Arguments** - 我们可以在 advice 方法中传递参数。我们可以在切入点中使用 args() 表达式来应用于与参数模式匹配的任何方法。如果我们使用它,那么我们需要在确定参数类型的 advice 方法中使用相同的名称。 +- **Pointcut** - Pointcut 是与 JoinPoint 匹配的正则表达式,用于确定是否需要执行 Advice。 Pointcut 使用与 JoinPoint 匹配的不同类型的表达式。Spring 框架使用 AspectJ Pointcut 表达式语言来确定将应用通知方法的 JoinPoint。 +- **JoinPoint** - JoinPoint 是应用程序中的特定点,例如方法执行,异常处理,更改对象变量值等。在 Spring AOP 中,JoinPoint 始终是方法的执行器。 + +#### 什么是通知(Advice)? + +特定 JoinPoint 处的 Aspect 所采取的动作称为 Advice。Spring AOP 使用一个 Advice 作为拦截器,在 JoinPoint “周围”维护一系列的拦截器。 + +#### 有哪些类型的通知(Advice)? + +- **Before** - 这些类型的 Advice 在 joinpoint 方法之前执行,并使用 @Before 注解标记进行配置。 +- **After Returning** - 这些类型的 Advice 在连接点方法正常执行后执行,并使用@AfterReturning 注解标记进行配置。 +- **After Throwing** - 这些类型的 Advice 仅在 joinpoint 方法通过抛出异常退出并使用 @AfterThrowing 注解标记配置时执行。 +- **After (finally)** - 这些类型的 Advice 在连接点方法之后执行,无论方法退出是正常还是异常返回,并使用 @After 注解标记进行配置。 +- **Around** - 这些类型的 Advice 在连接点之前和之后执行,并使用 @Around 注解标记进行配置。 + +#### 指出在 spring aop 中 concern 和 cross-cutting concern 的不同之处。 + +concern 是我们想要在应用程序的特定模块中定义的行为。它可以定义为我们想要实现的功能。 + +cross-cutting concern 是一个适用于整个应用的行为,这会影响整个应用程序。例如,日志记录,安全性和数据传输是应用程序几乎每个模块都需要关注的问题,因此它们是跨领域的问题。 + +#### AOP 有哪些实现方式? + +实现 AOP 的技术,主要分为两大类: + +- 静态代理 - 指使用 AOP 框架提供的命令进行编译,从而在编译阶段就可生成 AOP 代理类,因此也称为编译时增强; + - 编译时编织(特殊编译器实现) + - 类加载时编织(特殊的类加载器实现)。 +- 动态代理 - 在运行时在内存中“临时”生成 AOP 动态代理类,因此也被称为运行时增强。 + - JDK 动态代理 + - CGLIB + +#### Spring AOP and AspectJ AOP 有什么区别? + +Spring AOP 基于动态代理方式实现;AspectJ 基于静态代理方式实现。 +Spring AOP 仅支持方法级别的 PointCut;提供了完全的 AOP 支持,它还支持属性级别的 PointCut。 + +#### 如何理解 Spring 中的代理? + +将 Advice 应用于目标对象后创建的对象称为代理。在客户端对象的情况下,目标对象和代理对象是相同的。 + +``` +Advice + Target Object = Proxy +``` + +#### 什么是编织(Weaving)? + +为了创建一个 advice 对象而链接一个 aspect 和其它应用类型或对象,称为编织(Weaving)。在 Spring AOP 中,编织在运行时执行。请参考下图: + +![img](https://upload-images.jianshu.io/upload_images/3101171-cfaa92f0e4115b4a.png) + +## 注解 + +### 你用过哪些重要的 Spring 注解? + +- **@Controller** - 用于 Spring MVC 项目中的控制器类。 +- **@Service** - 用于服务类。 +- **@RequestMapping** - 用于在控制器处理程序方法中配置 URI 映射。 +- **@ResponseBody** - 用于发送 Object 作为响应,通常用于发送 XML 或 JSON 数据作为响应。 +- **@PathVariable** - 用于将动态值从 URI 映射到处理程序方法参数。 +- **@Autowired** - 用于在 spring bean 中自动装配依赖项。 +- **@Qualifier** - 使用 @Autowired 注解,以避免在存在多个 bean 类型实例时出现混淆。 +- **@Scope** - 用于配置 spring bean 的范围。 +- **@Configuration**,**@ComponentScan** 和 **@Bean** - 用于基于 java 的配置。 +- **@Aspect**,**@Before**,**@After**,**@Around**,**@Pointcut** - 用于切面编程(AOP)。 + +### 如何在 spring 中启动注解装配? + +默认情况下,Spring 容器中未打开注解装配。因此,要使用基于注解装配,我们必须通过配置`` 元素在 Spring 配置文件中启用它。 + +### @Component, @Controller, @Repository, @Service 有何区别? + +- @Component:这将 java 类标记为 bean。它是任何 Spring 管理组件的通用构造型。spring 的组件扫描机制现在可以将其拾取并将其拉入应用程序环境中。 +- @Controller:这将一个类标记为 Spring Web MVC 控制器。标有它的 Bean 会自动导入到 IoC 容器中。 +- @Service:此注解是组件注解的特化。它不会对 @Component 注解提供任何其他行为。您可以在服务层类中使用 @Service 而不是 @Component,因为它以更好的方式指定了意图。 +- @Repository:这个注解是具有类似用途和功能的 @Component 注解的特化。它为 DAO 提供了额外的好处。它将 DAO 导入 IoC 容器,并使未经检查的异常有资格转换为 Spring DataAccessException。 + +### @Required 注解有什么用? + +@Required 应用于 bean 属性 setter 方法。此注解仅指示必须在配置时使用 bean 定义中的显式属性值或使用自动装配填充受影响的 bean 属性。如果尚未填充受影响的 bean 属性,则容器将抛出 BeanInitializationException。 + +示例: + +```java +public class Employee { + private String name; + @Required + public void setName(String name){ + this.name=name; + } + public string getName(){ + return name; + } +} +``` + +### @Autowired 注解有什么用? + +@Autowired 可以更准确地控制应该在何处以及如何进行自动装配。此注解用于在 setter 方法,构造器,具有任意名称或多个参数的属性或方法上自动装配 bean。默认情况下,它是类型驱动的注入。 + +```java +public class Employee { + private String name; + @Autowired + public void setName(String name) { + this.name=name; + } + public string getName(){ + return name; + } +} +``` + +### @Qualifier 注解有什么用? + +当您创建多个相同类型的 bean 并希望仅使用属性装配其中一个 bean 时,您可以使用@Qualifier 注解和 @Autowired 通过指定应该装配哪个确切的 bean 来消除歧义。 + +例如,这里我们分别有两个类,Employee 和 EmpAccount。在 EmpAccount 中,使用@Qualifier 指定了必须装配 id 为 emp1 的 bean。 + +Employee.java + +```java +public class Employee { + private String name; + @Autowired + public void setName(String name) { + this.name=name; + } + public string getName() { + return name; + } +} +``` + +EmpAccount.java + +```java +public class EmpAccount { + private Employee emp; + + @Autowired + @Qualifier(emp1) + public void showName() { + System.out.println(“Employee name : ”+emp.getName); + } +} +``` + +### @RequestMapping 注解有什么用? + +@RequestMapping 注解用于将特定 HTTP 请求方法映射到将处理相应请求的控制器中的特定类/方法。此注解可应用于两个级别: + +- 类级别:映射请求的 URL +- 方法级别:映射 URL 以及 HTTP 请求方法 + +## 数据篇 + +### spring DAO 有什么用? + +Spring DAO 使得 JDBC,Hibernate 或 JDO 这样的数据访问技术更容易以一种统一的方式工作。这使得用户容易在持久性技术之间切换。它还允许您在编写代码时,无需考虑捕获每种技术不同的异常。 + +### 列举 Spring DAO 抛出的异常。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/cs/java/spring/data-access/spring-data-access-exception.png) + +### spring JDBC API 中存在哪些类? + +- JdbcTemplate +- SimpleJdbcTemplate +- NamedParameterJdbcTemplate +- SimpleJdbcInsert +- SimpleJdbcCall + +### 使用 Spring 访问 Hibernate 的方法有哪些? + +我们可以通过两种方式使用 Spring 访问 Hibernate: + +1. 使用 Hibernate 模板和回调进行控制反转 +2. 扩展 HibernateDAOSupport 并应用 AOP 拦截器节点 + +### 列举 spring 支持的事务管理类型 + +Spring 支持两种类型的事务管理: + +1. 程序化事务管理:在此过程中,在编程的帮助下管理事务。它为您提供极大的灵活性,但维护起来非常困难。 +2. 声明式事务管理:在此,事务管理与业务代码分离。仅使用注解或基于 XML 的配置来管理事务。 + +### spring 支持哪些 ORM 框架 + +- Hibernate +- iBatis +- JPA +- JDO +- OJB + +## MVC + +### Spring MVC 框架有什么用? + +Spring Web MVC 框架提供 **模型-视图-控制器** 架构和随时可用的组件,用于开发灵活且松散耦合的 Web 应用程序。 MVC 模式有助于分离应用程序的不同方面,如输入逻辑,业务逻辑和 UI 逻辑,同时在所有这些元素之间提供松散耦合。 + +### 描述一下 DispatcherServlet 的工作流程 + +DispatcherServlet 的工作流程可以用一幅图来说明: + +![img](https://raw.githubusercontent.com/dunwu/images/master/cs/java/spring/web/spring-dispatcher-servlet.png) + +1. 向服务器发送 HTTP 请求,请求被前端控制器 `DispatcherServlet` 捕获。 +2. `DispatcherServlet` 根据 **`-servlet.xml`** 中的配置对请求的 URL 进行解析,得到请求资源标识符(URI)。然后根据该 URI,调用 `HandlerMapping` 获得该 Handler 配置的所有相关的对象(包括 Handler 对象以及 Handler 对象对应的拦截器),最后以`HandlerExecutionChain` 对象的形式返回。 +3. `DispatcherServlet` 根据获得的`Handler`,选择一个合适的 `HandlerAdapter`。(附注:如果成功获得`HandlerAdapter`后,此时将开始执行拦截器的 preHandler(...)方法)。 +4. 提取`Request`中的模型数据,填充`Handler`入参,开始执行`Handler`(`Controller`)。 在填充`Handler`的入参过程中,根据你的配置,Spring 将帮你做一些额外的工作: + - HttpMessageConveter: 将请求消息(如 Json、xml 等数据)转换成一个对象,将对象转换为指定的响应信息。 + - 数据转换:对请求消息进行数据转换。如`String`转换成`Integer`、`Double`等。 + - 数据根式化:对请求消息进行数据格式化。 如将字符串转换成格式化数字或格式化日期等。 + - 数据验证: 验证数据的有效性(长度、格式等),验证结果存储到`BindingResult`或`Error`中。 +5. Handler(Controller)执行完成后,向 `DispatcherServlet` 返回一个 `ModelAndView` 对象; +6. 根据返回的`ModelAndView`,选择一个适合的 `ViewResolver`(必须是已经注册到 Spring 容器中的`ViewResolver`)返回给`DispatcherServlet`。 +7. `ViewResolver` 结合`Model`和`View`,来渲染视图。 +8. 视图负责将渲染结果返回给客户端。 + +### 介绍一下 WebApplicationContext + +WebApplicationContext 是 ApplicationContext 的扩展。它具有 Web 应用程序所需的一些额外功能。它与普通的 ApplicationContext 在解析主题和决定与哪个 servlet 关联的能力方面有所不同。 + +(完) + +--- + +:point_right: 想学习更多 Spring 内容可以访问我的 Spring 教程:**[spring-notes](https://github.com/dunwu/spring-notes)** + +## 资料 + +- [Top 50 Spring Interview Questions You Must Prepare In 2018](https://www.edureka.co/blog/interview-questions/spring-interview-questions/) +- [Spring Interview Questions and Answers](https://www.journaldev.com/2696/spring-interview-questions-and-answers) \ No newline at end of file diff --git "a/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/00.Spring\347\273\274\345\220\210/README.md" "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/00.Spring\347\273\274\345\220\210/README.md" new file mode 100644 index 00000000..cb55bf58 --- /dev/null +++ "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/00.Spring\347\273\274\345\220\210/README.md" @@ -0,0 +1,43 @@ +--- +title: Spring 综述 +date: 2020-02-26 23:48:06 +categories: + - Java + - 框架 + - Spring + - Spring综合 +tags: + - Java + - 框架 + - Spring + - SpringBoot +permalink: /pages/9e0b67/ +hidden: true +index: false +--- + +# Spring 综述 + +## 📖 内容 + +- [Spring 概述](01.Spring概述.md) +- [SpringBoot 知识图谱](21.SpringBoot知识图谱.md) +- [SpringBoot 基本原理](22.SpringBoot基本原理.md) +- [Spring 面试](99.Spring面试.md) + +## 📚 资料 + +- **官方** + - [Spring 官网](https://spring.io/) + - [Spring Framework 官方文档](https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/index.html) + - [Spring Github](https://github.com/spring-projects/spring-framework) +- **书籍** + - [《Spring In Action》](https://item.jd.com/12622829.html) +- **教程** + - [《小马哥讲 Spring 核心编程思想》](https://time.geekbang.org/course/intro/265) + - [geekbang-lessons](https://github.com/geektime-geekbang/geekbang-lessons) + - [跟我学 Spring3](http://jinnianshilongnian.iteye.com/blog/1482071) + +## 🚪 传送 + +◾ 💧 [钝悟的 IT 知识图谱](https://dunwu.github.io/waterdrop/) ◾ \ No newline at end of file diff --git "a/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/01.SpringBean.md" "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/01.SpringBean.md" new file mode 100644 index 00000000..1df26d7b --- /dev/null +++ "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/01.SpringBean.md" @@ -0,0 +1,214 @@ +--- +title: Spring Bean +date: 2021-12-10 19:15:42 +order: 01 +categories: + - Java + - 框架 + - Spring + - Spring核心 +tags: + - Java + - 框架 + - Spring + - Bean + - BeanDefinition +permalink: /pages/68097d/ +--- + +# Spring Bean + +在 Spring 中,构成应用程序主体由 Spring IoC 容器管理的对象称为 Bean。**Bean 是由 Spring IoC 容器实例化、装配和管理的对象**。 Bean 以及它们之间的依赖关系反映在容器使用的配置元数据中。 + +## Spring Bean 定义 + +### BeanDefinition + +Spring IoC 容器本身,并不能识别配置的元数据。为此,要将这些配置信息转为 Spring 能识别的格式——`BeanDefinition` 对象。 + +**`BeanDefinition` 是 Spring 中定义 Bean 的配置元信息接口**,它包含: + +- Bean 类名 +- Bean 行为配置元素,如:作用域、自动绑定的模式、生命周期回调等 +- 其他 Bean 引用,也可称为合作者(Collaborators)或依赖(Dependencies) +- 配置设置,如 Bean 属性(Properties) + +#### BeanDefinition 元信息 + +`BeanDefinition` 元信息如下: + +| 属性(Property) | 说明 | +| ----------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------ | +| [Class](https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#beans-factory-class) | 全类名,必须是具体类,不能用抽象类或接口 | +| [Name](https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#beans-beanname) | Bean 的名称或者 ID | +| [Scope](https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#beans-factory-scopes) | Bean 的作用域(如:`singleton`、`prototype` 等) | +| [Constructor arguments](https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#beans-factory-collaborators) | Bean 构造器参数(用于依赖注入) | +| [Properties](https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#beans-factory-collaborators) | Bean 属性设置(用于依赖注入) | +| [Autowiring mode](https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#beans-factory-autowire) | Bean 自动绑定模式(如:通过名称 byName) | +| [Lazy initialization mode](https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#beans-factory-lazy-init) | Bean 延迟初始化模式(延迟和非延迟) | +| [Initialization method](https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#beans-factory-lifecycle-initializingbean) | Bean 初始化回调方法名称 | +| [Destruction method](https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#beans-factory-lifecycle-disposablebean) | Bean 销毁回调方法名称 | + +#### BeanDefinition 构建 + +BeanDefinition 构建方式: + +- 通过 `BeanDefinitionBuilder` + +- 通过 `AbstractBeanDefinition` 以及派生类 + +> 💻 Spring Bean 定义示例源码:[BeanDefinitionTests](https://github.com/dunwu/spring-tutorial/blob/master/codes/core/spring-core-ioc/src/test/java/io/github/dunwu/spring/core/bean/BeanDefinitionTests.java) + +### Spring Bean 命名 + +#### Spring Bean 命名规则 + +每个 Bean 拥有一个或多个标识符(identifiers),这些标识符在 Bean 所在的容器必须是唯一的。通常,一个 Bean 仅有一个标识符,如果需要额外的,可考虑使用别名(Alias)来扩充。 + +在基于 XML 的配置元信息中,开发人员**可以使用 `id` 属性、`name` 属性或来指定 Bean 标识符**。通常,Bean 的标识符由字母组成,允许出现特殊字符。如果要想引入 Bean 的别名的话,可在 `name` 属性使用半角逗号(“,”)或分号(“;”) 来间隔。 + +Spring 中,**为 Bean 指定 `id` 和 `name` 属性不是必须的**。如果不指定,Spring 会自动为 Bean 分配一个唯一的名称。尽管 Bean 的命名没有限制,不过**官方建议采用驼峰命名法来命名 Bean**。 + +#### Spring Bean 命名生成器 + +Spring 提供了两种 Spring Bean 命名生成器: + +- `DefaultBeanNameGenerator`:默认通用 `BeanNameGenerator` 实现。 +- `AnnotationBeanNameGenerator`:基于注解扫描的 `BeanNameGenerator` 实现。 + +```java +public interface BeanNameGenerator { + String generateBeanName(BeanDefinition definition, BeanDefinitionRegistry registry); +} +``` + +#### Spring Bean 别名 + +Spring 支持通过 `` 属性为 Bean 设置别名。 + +Bean 别名(Alias)的作用: + +- 复用现有的 `BeanDefinition` +- 更具有场景化的命名方法,比如: + - `` + - `` + +```xml + + + + +``` + +## Spring Bean 生命周期 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/20211201102734.png) + +1. Spring 对 Bean 进行实例化(相当于 new XXX()) + +2. Spring 将值和引用注入到 Bean 对应的属性中 + +3. 如果 Bean 实现了 `BeanNameAware` 接口,Spring 将 Bean 的 ID 传递给 `setBeanName` 方法 + - 作用是通过 Bean 的引用来获得 Bean ID,一般业务中是很少有用到 Bean 的 ID 的 +4. 如果 Bean 实现了 `BeanFactoryAware` 接口,Spring 将调用 `setBeanDactory` 方法,并把 `BeanFactory` 容器实例作为参数传入。 + - 作用是获取 Spring 容器,如 Bean 通过 Spring 容器发布事件等 +5. 如果 Bean 实现了 `ApplicationContextAware` 接口,Spring 容器将调用 `setApplicationContext` 方法,把应用上下文作为参数传入 + - 作用与 `BeanFactory` 类似都是为了获取 Spring 容器,不同的是 Spring 容器在调用 `setApplicationContext` 方法时会把它自己作为 `setApplicationContext` 的参数传入,而 Spring 容器在调用 `setBeanFactory` 前需要使用者自己指定(注入)`setBeanFactory` 里的参数 `BeanFactory` +6. 如果 Bean 实现了 `BeanPostProcess` 接口,Spring 将调用 `postProcessBeforeInitialization` 方法 + - 作用是在 Bean 实例创建成功后对其进行增强处理,如对 Bean 进行修改,增加某个功能 +7. 如果 Bean 实现了 `InitializingBean` 接口,Spring 将调用 `afterPropertiesSet` 方法,作用与在配置文件中对 Bean 使用 `init-method` 声明初始化的作用一样,都是在 Bean 的全部属性设置成功后执行的初始化方法。 +8. 如果 Bean 实现了 `BeanPostProcess` 接口,Spring 将调用 `postProcessAfterInitialization` 方法 + - `postProcessBeforeInitialization` 是在 Bean 初始化前执行的,而 `postProcessAfterInitialization` 是在 Bean 初始化后执行的 +9. 经过以上的工作后,Bean 将一直驻留在应用上下文中给应用使用,直到应用上下文被销毁 +10. 如果 Bean 实现了 `DispostbleBean` 接口,Spring 将调用它的 `destory` 方法,作用与在配置文件中对 Bean 使用 `destory-method` 属性的作用一样,都是在 Bean 实例销毁前执行的方法。 + +## Spring Bean 注册 + +注册 Spring Bean 实际上是将 `BeanDefinition` 注册到 IoC 容器中。 + +### XML 配置元信息 + +Spring 的传统配置方式。在 `` 标签中配置元数据内容。 + +缺点是当 JavaBean 过多时,产生的配置文件足以让你眼花缭乱。 + +### 注解配置元信息 + +使用 `@Bean`、`@Component`、`@Import` 注解注册 Spring Bean。 + +### Java API 配置元信息 + +- 命名方式:`BeanDefinitionRegistry#registerBeanDefinition(String,BeanDefinition)` +- 非命名方式:`BeanDefinitionReaderUtils#registerWithGeneratedName(AbstractBeanDefinition,BeanDefinitionRegistry)` +- 配置类方式:`AnnotatedBeanDefinitionReader#register(Class...)` + +> 💻 Spring Bean 注册示例源码:[BeanRegistryTests](https://github.com/dunwu/spring-tutorial/blob/master/codes/core/spring-core-ioc/src/test/java/io/github/dunwu/spring/core/bean/BeanRegistryTests.java) + +## Spring Bean 实例化 + +Spring Bean 实例化方式: + +- 常规方式 + - 通过构造器(配置元信息:XML、Java 注解和 Java API) + - 通过静态方法(配置元信息:XML、Java 注解和 Java API) + - 通过 Bean 工厂方法(配置元信息:XML、Java 注解和 Java API) + - 通过 `FactoryBean`(配置元信息:XML、Java 注解和 Java API) +- 特殊方式 + - 通过 `ServiceLoaderFactoryBean`(配置元信息:XML、Java 注解和 Java API ) + - 通过 `AutowireCapableBeanFactory#createBean(java.lang.Class, int, boolean)` + - 通过 `BeanDefinitionRegistry#registerBeanDefinition(String,BeanDefinition)` + +> 💻 Spring Bean 实例化示例源码:[BeanInstantiationTests](https://github.com/dunwu/spring-tutorial/blob/master/codes/core/spring-core-ioc/src/test/java/io/github/dunwu/spring/core/bean/BeanInstantiationTests.java)、[BeanInstantiationSpecialTests](https://github.com/dunwu/spring-tutorial/blob/master/codes/core/spring-core-ioc/src/test/java/io/github/dunwu/spring/core/bean/BeanInstantiationSpecialTests.java) + +## Spring Bean 初始化和销毁 + +Spring Bean 初始化和销毁的方式有以下几种: + +1. 使用 `@PostConstruct` 和 `@PreDestroy` 注解分别指定相应的初始化方法和销毁方法。 +2. 实现 `InitializingBean` 接口的 `afterPropertiesSet()` 方法来编写初始化方法;实现 `DisposableBean` 接口的 `destroy()` 方法来编写销毁方法。 + + - `InitializingBean` 接口包含一个 `afterPropertiesSet` 方法,可以通过实现该接口,然后在这个方法中编写初始化逻辑。 + - `DisposableBean`接口包含一个 `destory` 方法,可以通过实现该接口,然后在这个方法中编写销毁逻辑。 + +3. 自定义初始化方法 + - XML 配置:`` + - Java 注解:`@Bean(initMethod = "init", destroyMethod = "destroy")` + - Java API:`AbstractBeanDefinition#setInitMethodName(String)` 和 `AbstractBeanDefinition#setDestroyMethodName(String)` 分别定义初始化和销毁方法 + +注意:如果同时存在,执行顺序会按照序列执行。 + +Bean 的延迟初始化 + +- xml 方式:`` +- 注解方式:`@Lazy` + +Spring 提供了一个 `BeanPostProcessor` 接口,提供了两个方法 `postProcessBeforeInitialization` 和 `postProcessAfterInitialization`。其中`postProcessBeforeInitialization` 在组件的初始化方法调用之前执行,`postProcessAfterInitialization` 在组件的初始化方法调用之后执行。它们都包含两个入参: + +- `bean`:当前组件对象; +- `beanName`:当前组件在容器中的名称。 + +> 💻 Spring Bean 初始化和销毁示例源码:[BeanInitDestroyTests](https://github.com/dunwu/spring-tutorial/blob/master/codes/core/spring-core-ioc/src/test/java/io/github/dunwu/spring/core/bean/BeanInitDestroyTests.java) + +## Spring Bean 垃圾回收 + +Spring Bean 垃圾回收步骤: + +1. 关闭 Spring 容器(应用上下文) +2. 执行 GC +3. Spring Bean 覆盖的 `finalize()` 方法被回调 + +## Spring Bean 作用范围 + +| Scope | Description | +| :---------------------------------------------------------------------------------------------------------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [singleton](https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#beans-factory-scopes-singleton) | (Default) Scopes a single bean definition to a single object instance for each Spring IoC container. | +| [prototype](https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#beans-factory-scopes-prototype) | Scopes a single bean definition to any number of object instances. | +| [request](https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#beans-factory-scopes-request) | Scopes a single bean definition to the lifecycle of a single HTTP request. That is, each HTTP request has its own instance of a bean created off the back of a single bean definition. Only valid in the context of a web-aware Spring `ApplicationContext`. | +| [session](https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#beans-factory-scopes-session) | Scopes a single bean definition to the lifecycle of an HTTP `Session`. Only valid in the context of a web-aware Spring `ApplicationContext`. | +| [application](https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#beans-factory-scopes-application) | Scopes a single bean definition to the lifecycle of a `ServletContext`. Only valid in the context of a web-aware Spring `ApplicationContext`. | +| [websocket](https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#websocket-stomp-websocket-scope) | Scopes a single bean definition to the lifecycle of a `WebSocket`. Only valid in the context of a web-aware Spring `ApplicationContext`. | + +## 参考资料 + +- [Spring 官方文档之 Core Technologies](https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/core.html#beans) +- [《小马哥讲 Spring 核心编程思想》](https://time.geekbang.org/course/intro/265) \ No newline at end of file diff --git "a/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/02.SpringIoC.md" "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/02.SpringIoC.md" new file mode 100644 index 00000000..505172fb --- /dev/null +++ "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/02.SpringIoC.md" @@ -0,0 +1,921 @@ +--- +title: Spring IoC +date: 2020-08-30 16:06:10 +order: 02 +categories: + - Java + - 框架 + - Spring + - Spring核心 +tags: + - Java + - 框架 + - Spring + - IOC +permalink: /pages/915530/ +--- + +# Spring IoC + +## IoC 简介 + +### IoC 是什么 + +**IoC** 即**控制反转**(Inversion of Control,缩写为 IoC)。IoC 又称为**依赖倒置原则**(设计模式六大原则之一),它的要点在于:**程序要依赖于抽象接口,不要依赖于具体实现**。它的作用就是**用于降低代码间的耦合度**。 + +IoC 的实现方式有两种: + +- **依赖注入**(Dependency Injection,简称 DI):不通过 `new()` 的方式在类内部创建依赖类对象,而是将依赖的类对象在外部创建好之后,通过构造函数、函数参数等方式传递(或注入)给类使用。 +- **依赖查找**(Dependency Lookup):容器中的受控对象通过容器的 API 来查找自己所依赖的资源和协作对象。 + +理解 Ioc 的关键是要明确两个要点: + +- **谁控制谁,控制什么**:传统 Java SE 程序设计,我们直接在对象内部通过 new 进行创建对象,是程序主动去创建依赖对象;而 IoC 是有专门一个容器来创建这些对象,即由 Ioc 容器来控制对象的创建;谁控制谁?当然是 IoC 容器控制了对象;控制什么?那就是主要控制了外部资源获取(不只是对象包括比如文件等)。 +- **为何是反转,哪些方面反转了**:有反转就有正转,传统应用程序是由我们自己在对象中主动控制去直接获取依赖对象,也就是正转;而反转则是由容器来帮忙创建及注入依赖对象;为何是反转?因为由容器帮我们查找及注入依赖对象,对象只是被动的接受依赖对象,所以是反转;哪些方面反转了?依赖对象的获取被反转了。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/20221006120112.png) + +### IoC 能做什么 + +IoC 不是一种技术,而是编程思想,一个重要的面向对象编程的法则,它能指导我们如何设计出松耦合、更优良的程序。传统应用程序都是由我们在类内部主动创建依赖对象,从而导致类与类之间高耦合,难于测试;有了 IoC 容器后,把创建和查找依赖对象的控制权交给了容器,由容器进行注入组合对象,所以对象与对象之间是松散耦合,这样也方便测试,利于功能复用,更重要的是使得程序的整个体系结构变得非常灵活。 + +其实 IoC 对编程带来的最大改变不是从代码上,而是从思想上,发生了“主从换位”的变化。应用程序原本是老大,要获取什么资源都是主动出击,但是在 IoC/DI 思想中,应用程序就变成被动的了,被动的等待 IoC 容器来创建并注入它所需要的资源了。 + +IoC 很好的体现了面向对象设计法则之一—— **好莱坞法则:“别找我们,我们找你”**;即由 IoC 容器帮对象找相应的依赖对象并注入,而不是由对象主动去找。 + +### IoC 和 DI + +其实它们是同一个概念的不同角度描述,由于控制反转概念比较含糊(可能只是理解为容器控制对象这一个层面,很难让人想到谁来维护对象关系),所以 2004 年大师级人物 Martin Fowler 又给出了一个新的名字:“依赖注入”,相对 IoC 而言,“依赖注入”明确描述了“被注入对象依赖 IoC 容器配置依赖对象”。 + +> 注:如果想要更加深入的了解 IoC 和 DI,请参考大师级人物 Martin Fowler 的一篇经典文章 [Inversion of Control Containers and the Dependency Injection pattern](http://www.martinfowler.com/articles/injection.html) 。 + +### IoC 容器 + +IoC 容器就是具有依赖注入功能的容器。IoC 容器负责实例化、定位、配置应用程序中的对象及建立这些对象间的依赖。应用程序无需直接在代码中 new 相关的对象,应用程序由 IoC 容器进行组装。在 Spring 中 BeanFactory 是 IoC 容器的实际代表者。 + +Spring IoC 容器如何知道哪些是它管理的对象呢?这就需要配置文件,Spring IoC 容器通过读取配置文件中的配置元数据,通过元数据对应用中的各个对象进行实例化及装配。一般使用基于 xml 配置文件进行配置元数据,而且 Spring 与配置文件完全解耦的,可以使用其他任何可能的方式进行配置元数据,比如注解、基于 java 文件的、基于属性文件的配置都可以。 + +### Bean + +> **JavaBean** 是一种 JAVA 语言写成的可重用组件。为写成 JavaBean,类必须是具体的和公共的,并且具有无参数的构造器。JavaBean 对外部通过提供 getter / setter 方法来访问其成员。 + +由 IoC 容器管理的那些组成你应用程序的对象我们就叫它 Bean。Bean 就是由 Spring 容器初始化、装配及管理的对象,除此之外,bean 就与应用程序中的其他对象没有什么区别了。那 IoC 怎样确定如何实例化 Bean、管理 Bean 之间的依赖关系以及管理 Bean 呢?这就需要配置元数据,在 Spring 中由 BeanDefinition 代表,后边会详细介绍,配置元数据指定如何实例化 Bean、如何组装 Bean 等。 + +### Spring IoC + +Spring IoC 容器中的对象仅通过构造函数参数、工厂方法的参数或在对象实例被构造或从工厂方法返回后设置的属性来定义它们的依赖关系(即与它们一起工作的其他对象)。然后容器在创建 bean 时注入这些依赖项。这个过程基本上是 bean 本身通过使用类的直接构造或诸如服务定位器模式之类的机制来控制其依赖关系的实例化或位置的逆过程(因此称为控制反转)。 + +`org.springframework.beans` 和 `org.springframework.context` 是 IoC 容器的基础。 + +## IoC 容器 + +在 Spring 中,有两种 IoC 容器:`BeanFactory` 和 `ApplicationContext`。 + +- `BeanFactory`:**`BeanFactory` 是 Spring 基础 IoC 容器**。`BeanFactory` 提供了 Spring 容器的配置框架和基本功能。 +- `ApplicationContext`:**`ApplicationContext` 是具备应用特性的 `BeanFactory` 的子接口**。它还扩展了其他一些接口,以支持更丰富的功能,如:国际化、访问资源、事件机制、更方便的支持 AOP、在 web 应用中指定应用层上下文等。 + +实际开发中,更推荐使用 `ApplicationContext` 作为 IoC 容器,因为它的功能远多于 `BeanFactory`。 + +`org.springframework.context.ApplicationContext` 接口代表 Spring IoC 容器,负责实例化、配置和组装 bean。容器通过读取配置元数据来获取关于要实例化、配置和组装哪些对象的指令。配置元数据以 XML、Java 注释或 Java 代码表示。它允许您表达组成应用程序的对象以及这些对象之间丰富的相互依赖关系。 + +Spring 提供了 `ApplicationContext` 接口的几个实现,例如: + +- **[ClassPathXmlApplicationContext](https://docs.spring.io/spring-framework/docs/5.3.23/javadoc-api/org/springframework/context/support/ClassPathXmlApplicationContext.html)**:`ApplicationContext` 的实现,从 classpath 获取配置信息。 + +```java +BeanFactory beanFactory = new ClassPathXmlApplicationContext("classpath.xml"); +``` + +- **[FileSystemXmlApplicationContext](https://docs.spring.io/spring-framework/docs/5.3.23/javadoc-api/org/springframework/context/support/FileSystemXmlApplicationContext.html)**:`ApplicationContext` 的实现,从文件系统获取配置信息。 + +```java +BeanFactory beanFactory = new FileSystemXmlApplicationContext("fileSystemConfig.xml"); +``` + +在大多数应用场景中,不需要显式通过用户代码来实例化 Spring IoC 容器的一个或多个实例。 + +下图显示了 Spring IoC 容器的工作步骤 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200723102456.png) + +使用 IoC 容器可分为三步骤: + +1. **配置元数据**:需要配置一些元数据来告诉 Spring,你希望容器如何工作,具体来说,就是如何去初始化、配置、管理 JavaBean 对象。 +2. **实例化容器**:由 IoC 容器解析配置的元数据。IoC 容器的 Bean Reader 读取并解析配置文件,根据定义生成 BeanDefinition 配置元数据对象,IoC 容器根据 `BeanDefinition` 进行实例化、配置及组装 Bean。 +3. **使用容器**:由客户端实例化容器,获取需要的 Bean。 + +### 配置元数据 + +**元数据(Metadata)**又称中介数据、中继数据,为描述数据的数据(data about data),主要是描述数据属性(property)的信息。 + +配置元数据的方式: + +- **基于 xml 配置**:Spring 的传统配置方式。通常是在顶级元素 `` 中通过 ``元素配置元数据。这种方式的缺点是:如果 JavaBean 过多,则产生的配置文件足以让你眼花缭乱。 +- **[基于注解配置](https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#beans-annotation-config)**:Spring 2.5 引入了对基于注解的配置元数据的支持。可以大大简化你的配置。 +- **[基于 Java 配置](https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#beans-java)**:从 Spring 3.0 开始,Spring 支持使用 Java 代码来配置元数据。通常是在 `@Configuration` 修饰的类中通过 `@Bean` 指定实例化 Bean 的方法。更多详情,可以参阅 [`@Configuration`](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/context/annotation/Configuration.html)、[`@Bean`](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/context/annotation/Bean.html)、[`@Import`](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/context/annotation/Import.html) 和 [`@DependsOn`](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/context/annotation/DependsOn.html) 注释。 + +这些 bean 定义对应于构成应用程序的实际对象。例如:定义服务层对象、数据访问对象 (DAO)、表示对象(如 Struts Action 实例)、基础设施对象(如 Hibernate SessionFactories、JMS 队列等)。通常,不会在容器中配置细粒度的域对象,因为创建和加载域对象通常是 DAO 和业务逻辑的责任。但是,可以使用 Spring 与 AspectJ 的集成来配置在 IoC 容器控制之外创建的对象。 + +以下示例显示了基于 XML 的配置元数据的基本结构: + +```xml + + + + + + + + + + + + + + + + +``` + +### 实例化容器 + +可以通过为 `ApplicationContext` 的构造函数指定外部资源路径,来加载配置元数据。 + +```java +ApplicationContext context = new ClassPathXmlApplicationContext("services.xml", "daos.xml"); +``` + +以下示例显示了服务层对象 (services.xml) 配置文件: + +```xml + + + + + + + + + + + + + + +``` + +以下示例显示了数据访问对象 (daos.xml) 配置文件: + +```xml + + + + + + + + + + + + + + +``` + +上面的示例中,服务层由 `PetStoreServiceImpl` 类和类型为 `JpaAccountDao` 和 `JpaItemDao` 的两个数据访问对象(基于 JPA 对象关系映射标准)组成。 `property name` 元素指的是 JavaBean 属性的名称,`ref` 元素指的是另一个 bean 定义的名称。 `id` 和 `ref` 元素之间的这种联系表达了协作对象之间的依赖关系。 + +**Spring 支持通过多个 xml 文件来定义 Bean,每个单独的 XML 配置文件都代表架构中的一个逻辑层或模块。可以使用 `ApplicationContext` 构造函数从所有这些 XML 片段加载 bean 定义。或者,使用 `` 元素从另一个或多个文件加载 bean 定义**。如下所示: + +```xml + + + + + + + + +``` + +在上面的示例中,外部 bean 定义从三个文件加载:`services.xml`、`messageSource.xml` 和 `themeSource.xml`。`services.xml` 文件必须和当前 xml 文件位于同一目录或类路径位置;而 `messageSource.xml` 和 `themeSource.xml` 必须位于当前文件所在目录的子目录 `resources` 下。`/resources` 的 `/` 会被忽略。但是,鉴于这些路径是相对的,最好不要使用 `/`。根据 Spring Schema,被导入文件的内容,包括顶级 `` 元素,必须是有效的 XML bean 定义。 + +> 注意: +> +> 可以,但不推荐使用相对 `“../”` 路径来引用父目录中的文件。这样做会创建对当前应用程序之外的文件的依赖。特别是,不建议将此引用用于 `classpath`:URL(例如, `classpath:../services.xml`),其中运行时解析过程会选择“最近的”类路径根,然后查看其父目录。类路径配置更改可能会导致选择不同的、不正确的目录。 +> +> 可以使用完全限定的资源位置而不是相对路径:例如,`file:C:/config/services.xml` 或 `classpath:/config/services.xml`。建议为此类绝对路径保留一定的间接性  —  例如,通过 `“${...}”` 占位符来引用运行时指定 的 JVM 参数。 + +命名空间本身提供了导入指令功能。 Spring 提供的一系列 XML 命名空间中提供了除了普通 bean 定义之外的更多配置特性  —  例如,`context` 和 `util` 命名空间。 + +### 使用容器 + +`ApplicationContext` 能够维护不同 bean 及其依赖项的注册表。通过**使用方法 `T getBean(String name, Class T requiredType)`,可以检索并获取 bean 的实例**。 + +`ApplicationContext` 允许读取 bean 定义并访问它们,如以下示例所示: + +```java +// create and configure beans +ApplicationContext context = new ClassPathXmlApplicationContext("services.xml", "daos.xml"); + +// retrieve configured instance +PetStoreService service = context.getBean("petStore", PetStoreService.class); + +// use configured instance +List userList = service.getUsernameList(); +``` + +最灵活的变体是 `GenericApplicationContext` 结合阅读器委托  —  例如,结合 XML 文件的 `XmlBeanDefinitionReader`,如下例所示: + +```java +GenericApplicationContext context = new GenericApplicationContext(); +new XmlBeanDefinitionReader(context).loadBeanDefinitions("services.xml", "daos.xml"); +context.refresh(); +``` + +可以在同一个 `ApplicationContext` 上混合和匹配此类读取器委托,从不同的配置源读取 bean 定义。 + +然后,可以使用 `getBean` 检索 bean 的实例。 `ApplicationContext` 接口还有一些其他方法用于检索 bean,但理想情况下,应用程序代码不应该使用它们。实际上,应用程序代码根本不应该调用 `getBean()` 方法,因此根本不依赖 Spring API。例如,Spring 与 Web 框架的集成为各种 Web 框架组件(例如控制器和 JSF 管理的 bean)提供了依赖注入,让您可以通过元数据(例如自动装配注释)声明对特定 bean 的依赖。 + +## IoC 依赖来源 + +自定义 Bean + +容器内建 Bean 对象 + +容器内建依赖 + +## IoC 配置元数据 + +IoC 容器的配置有三种方式: + +- 基于 xml 配置 +- 基于 properties 配置 +- 基于注解配置 +- 基于 Java 配置 + +作为 Spring 传统的配置方式,xml 配置方式一般为大家所熟知。 + +如果厌倦了 xml 配置,Spring 也提供了注解配置方式或 Java 配置方式来简化配置。 + +**本文,将对 Java 配置 IoC 容器做详细的介绍。** + +### Xml 配置 + +```xml + + + + + + + + + + +``` + +标签说明: + +- `` 是 Spring 配置文件的根节点。 +- `` 用来定义一个 JavaBean。`id` 属性是它的标识,在文件中必须唯一;`class` 属性是它关联的类。 +- `` 用来定义 Bean 的别名。 +- `` 用来导入其他配置文件的 Bean 定义。这是为了加载多个配置文件,当然也可以把这些配置文件构造为一个数组(new String[] {“config1.xml”, config2.xml})传给 `ApplicationContext` 实现类进行加载多个配置文件,那一个更适合由用户决定;这两种方式都是通过调用 Bean Definition Reader 读取 Bean 定义,内部实现没有任何区别。`` 标签可以放在 `` 下的任何位置,没有顺序关系。 + +#### 实例化容器 + +实例化容器的过程: +定位资源(XML 配置文件) +读取配置信息(Resource) +转化为 Spring 可识别的数据形式(BeanDefinition) + +```java +ApplicationContext context = + new ClassPathXmlApplicationContext(new String[] {"services.xml", "daos.xml"}); +``` + +组合 xml 配置文件 +配置的 Bean 功能各不相同,都放在一个 xml 文件中,不便管理。 +Java 设计模式讲究职责单一原则。配置其实也是如此,功能不同的 JavaBean 应该被组织在不同的 xml 文件中。然后使用 import 标签把它们统一导入。 + +```xml + + +``` + +#### 使用容器 + +使用容器的方式就是通过`getBean`获取 IoC 容器中的 JavaBean。 +Spring 也有其他方法去获得 JavaBean,但是 Spring 并不推荐其他方式。 + +```java +// create and configure beans +ApplicationContext context = +new ClassPathXmlApplicationContext(new String[] {"services.xml", "daos.xml"}); +// retrieve configured instance +PetStoreService service = context.getBean("petStore", PetStoreService.class); +// use configured instance +List userList = service.getUsernameList(); +``` + +### 注解配置 + +Spring2.5 引入了注解。 +于是,一个问题产生了:**使用注解方式注入 JavaBean 是不是一定完爆 xml 方式?** +未必。正所谓,仁者见仁智者见智。任何事物都有其优缺点,看你如何取舍。来看看注解的优缺点: +**优点**:大大减少了配置,并且可以使配置更加精细——类,方法,字段都可以用注解去标记。 +**缺点**:使用注解,不可避免产生了侵入式编程,也产生了一些问题。 + +- 你需要将注解加入你的源码并编译它; + +- 注解往往比较分散,不易管控。 + +> 注:spring 中,先进行注解注入,然后才是 xml 注入,因此如果注入的目标相同,后者会覆盖前者。 + +#### 启动注解 + +Spring 默认是不启用注解的。如果想使用注解,需要先在 xml 中启动注解。 +启动方式:在 xml 中加入一个标签,很简单吧。 + +```xml + +``` + +> 注:`` 只会检索定义它的上下文。什么意思呢?就是说,如果你 +> 为 DispatcherServlet 指定了一个`WebApplicationContext`,那么它只在 controller 中查找`@Autowired`注解,而不会检查其它的路径。 + +#### `@Required` + +`@Required` 注解只能用于修饰 bean 属性的 setter 方法。受影响的 bean 属性必须在配置时被填充在 xml 配置文件中,否则容器将抛出`BeanInitializationException`。 + +```java +public class AnnotationRequired { + private String name; + private String sex; + + public String getName() { + return name; + } + + /** + * @Required 注解用于bean属性的setter方法并且它指示,受影响的bean属性必须在配置时被填充在xml配置文件中, + * 否则容器将抛出BeanInitializationException。 + */ + @Required + public void setName(String name) { + this.name = name; + } + + public String getSex() { + return sex; + } + + public void setSex(String sex) { + this.sex = sex; + } +} +``` + +#### `@Autowired` + +`@Autowired`注解可用于修饰属性、setter 方法、构造方法。 + +@Autowired 注入过程 + +- 元信息解析 +- 依赖查找 +- 依赖注入(字段、方法) + +> 注:`@Autowired`注解也可用于修饰构造方法,但如果类中只有默认构造方法,则没有必要。如果有多个构造器,至少应该修饰一个,来告诉容器哪一个必须使用。 + +可以使用 JSR330 的注解`@Inject`来替代`@Autowired`。 + +**_范例_** + +```java +public class AnnotationAutowired { + private static final Logger log = LoggerFactory.getLogger(AnnotationRequired.class); + + @Autowired + private Apple fieldA; + + private Banana fieldB; + + private Orange fieldC; + + public Apple getFieldA() { + return fieldA; + } + + public void setFieldA(Apple fieldA) { + this.fieldA = fieldA; + } + + public Banana getFieldB() { + return fieldB; + } + + @Autowired + public void setFieldB(Banana fieldB) { + this.fieldB = fieldB; + } + + public Orange getFieldC() { + return fieldC; + } + + public void setFieldC(Orange fieldC) { + this.fieldC = fieldC; + } + + public AnnotationAutowired() {} + + @Autowired + public AnnotationAutowired(Orange fieldC) { + this.fieldC = fieldC; + } + + public static void main(String[] args) throws Exception { + AbstractApplicationContext ctx = + new ClassPathXmlApplicationContext("spring/spring-annotation.xml"); + + AnnotationAutowired annotationAutowired = + (AnnotationAutowired) ctx.getBean("annotationAutowired"); + log.debug("fieldA: {}, fieldB:{}, fieldC:{}", annotationAutowired.getFieldA().getName(), + annotationAutowired.getFieldB().getName(), + annotationAutowired.getFieldC().getName()); + ctx.close(); + } +} +``` + +xml 中的配置 + +```xml + + + + + +``` + +#### `@Qualifier` + +在`@Autowired`注解中,提到了如果发现有多个候选的 bean 都符合修饰类型,Spring 就会抓瞎了。 + +那么,如何解决这个问题。 + +可以通过`@Qualifier`指定 bean 名称来锁定真正需要的那个 bean。 + +**_范例_** + +```java +public class AnnotationQualifier { + private static final Logger log = LoggerFactory.getLogger(AnnotationQualifier.class); + + @Autowired + @Qualifier("dog") /** 去除这行,会报异常 */ + Animal dog; + + Animal cat; + + public Animal getDog() { + return dog; + } + + public void setDog(Animal dog) { + this.dog = dog; + } + + public Animal getCat() { + return cat; + } + + @Autowired + public void setCat(@Qualifier("cat") Animal cat) { + this.cat = cat; + } + + public static void main(String[] args) throws Exception { + AbstractApplicationContext ctx = + new ClassPathXmlApplicationContext("spring/spring-annotation.xml"); + + AnnotationQualifier annotationQualifier = + (AnnotationQualifier) ctx.getBean("annotationQualifier"); + + log.debug("Dog name: {}", annotationQualifier.getDog().getName()); + log.debug("Cat name: {}", annotationQualifier.getCat().getName()); + ctx.close(); + } +} + +abstract class Animal { + public String getName() { + return null; + } +} + +class Dog extends Animal { + public String getName() { + return "狗"; + } +} + +class Cat extends Animal { + public String getName() { + return "猫"; + } +} +``` + +xml 中的配置 + +```xml + + + + +``` + +#### `@Resource` + +Spring 支持 JSP250 规定的注解`@Resource`。这个注解根据指定的名称来注入 bean。 + +如果没有为`@Resource`指定名称,它会像`@Autowired`一样按照类型去寻找匹配。 + +在 Spring 中,由`CommonAnnotationBeanPostProcessor`来处理`@Resource`注解。 + +**_范例_** + +```java +public class AnnotationResource { + private static final Logger log = LoggerFactory.getLogger(AnnotationResource.class); + + @Resource(name = "flower") + Plant flower; + + @Resource(name = "tree") + Plant tree; + + public Plant getFlower() { + return flower; + } + + public void setFlower(Plant flower) { + this.flower = flower; + } + + public Plant getTree() { + return tree; + } + + public void setTree(Plant tree) { + this.tree = tree; + } + + public static void main(String[] args) throws Exception { + AbstractApplicationContext ctx = + new ClassPathXmlApplicationContext("spring/spring-annotation.xml"); + + AnnotationResource annotationResource = + (AnnotationResource) ctx.getBean("annotationResource"); + log.debug("type: {}, name: {}", annotationResource.getFlower().getClass(), annotationResource.getFlower().getName()); + log.debug("type: {}, name: {}", annotationResource.getTree().getClass(), annotationResource.getTree().getName()); + ctx.close(); + } +} +``` + +xml 的配置 + +```xml + + + + +``` + +#### `@PostConstruct` 和 `@PreDestroy` + +`@PostConstruct` 和 `@PreDestroy` 是 JSR 250 规定的用于生命周期的注解。 + +从其名号就可以看出,一个是在构造之后调用的方法,一个是销毁之前调用的方法。 + +```java +public class AnnotationPostConstructAndPreDestroy { + private static final Logger log = LoggerFactory.getLogger(AnnotationPostConstructAndPreDestroy.class); + + @PostConstruct + public void init() { + log.debug("call @PostConstruct method"); + } + + @PreDestroy + public void destroy() { + log.debug("call @PreDestroy method"); + } +} +``` + +#### `@Inject` + +从 Spring3.0 开始,Spring 支持 JSR 330 标准注解(依赖注入)。 + +注:如果要使用 JSR 330 注解,需要使用外部 jar 包。 + +若你使用 maven 管理 jar 包,只需要添加依赖到 pom.xml 即可: + +```xml + + javax.inject + javax.inject + 1 + +``` + +`@Inject` 和 `@Autowired` 一样,可以修饰属性、setter 方法、构造方法。 + +**_范例_** + +```java +public class AnnotationInject { + private static final Logger log = LoggerFactory.getLogger(AnnotationInject.class); + @Inject + Apple fieldA; + + Banana fieldB; + + Orange fieldC; + + public Apple getFieldA() { + return fieldA; + } + + public void setFieldA(Apple fieldA) { + this.fieldA = fieldA; + } + + public Banana getFieldB() { + return fieldB; + } + + @Inject + public void setFieldB(Banana fieldB) { + this.fieldB = fieldB; + } + + public Orange getFieldC() { + return fieldC; + } + + public AnnotationInject() {} + + @Inject + public AnnotationInject(Orange fieldC) { + this.fieldC = fieldC; + } + + public static void main(String[] args) throws Exception { + AbstractApplicationContext ctx = + new ClassPathXmlApplicationContext("spring/spring-annotation.xml"); + AnnotationInject annotationInject = (AnnotationInject) ctx.getBean("annotationInject"); + + log.debug("type: {}, name: {}", annotationInject.getFieldA().getClass(), + annotationInject.getFieldA().getName()); + + log.debug("type: {}, name: {}", annotationInject.getFieldB().getClass(), + annotationInject.getFieldB().getName()); + + log.debug("type: {}, name: {}", annotationInject.getFieldC().getClass(), + annotationInject.getFieldC().getName()); + + ctx.close(); + } +} +``` + +### Java 配置 + +基于 Java 配置 Spring IoC 容器,实际上是**Spring 允许用户定义一个类,在这个类中去管理 IoC 容器的配置**。 + +为了让 Spring 识别这个定义类为一个 Spring 配置类,需要用到两个注解:`@Configuration`和`@Bean`。 + +如果你熟悉 Spring 的 xml 配置方式,你可以将`@Configuration`等价于``标签;将`@Bean`等价于``标签。 + +#### `@Bean` + +@Bean 的修饰目标只能是方法或注解。 + +@Bean 只能定义在 `@Configuration` 或 `@Component` 注解修饰的类中。 + +#### 声明一个 bean + +此外,@Configuration 类允许在同一个类中通过@Bean 定义内部 bean 依赖。 + +声明一个 bean,只需要在 bean 属性的 set 方法上标注@Bean 即可。 + +```java +@Configuration +public class AnnotationConfiguration { + private static final Logger log = LoggerFactory.getLogger(JavaComponentScan.class); + + @Bean + public Job getPolice() { + return new Police(); + } + + public static void main(String[] args) { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(AnnotationConfiguration.class); + ctx.scan("org.zp.notes.spring.beans"); + ctx.refresh(); + Job job = (Job) ctx.getBean("police"); + log.debug("job: {}, work: {}", job.getClass(), job.work()); + } +} + +public interface Job { + String work(); +} + +@Component("police") +public class Police implements Job { + @Override + public String work() { + return "抓罪犯"; + } +} +``` + +这等价于配置 + +```xml + + + +``` + +@Bean 注解用来表明一个方法实例化、配置合初始化一个被 Spring IoC 容器管理的新对象。 + +如果你熟悉 Spring 的 xml 配置,你可以将@Bean 视为等价于``标签。 + +@Bean 注解可以用于任何的 Spring `@Component` bean,然而,通常被用于`@Configuration` bean。 + +#### `@Configuration` + +`@Configuration` 是一个类级别的注解,用来标记被修饰类的对象是一个`BeanDefinition`。 + +`@Configuration` 声明 bean 是通过被 `@Bean` 修饰的公共方法。此外,`@Configuration` 允许在同一个类中通过 `@Bean` 定义内部 bean 依赖。 + +```java +@Configuration +public class AppConfig { + @Bean + public MyService myService() { + return new MyServiceImpl(); + } +} +``` + +这等价于配置 + +```xml + + + +``` + +用 `AnnotationConfigApplicationContext` 实例化 IoC 容器。 + +## 依赖解决过程 + +容器执行 bean 依赖解析如下: + +- `ApplicationContext` 使用配置元数据创建和初始化 Bean。配置元数据可以由 XML、Java 代码或注解指定。 +- 对于每个 bean,其依赖关系以属性、构造函数参数或静态工厂方法的参数的形式表示。这些依赖项在实际创建 bean 时提供给 bean。 +- 每个属性或构造函数参数都是要设置的值的实际定义,或者是对容器中另一个 bean 的引用。 +- 作为值的每个属性或构造函数参数都从其指定格式转换为该属性或构造函数参数的实际类型。默认情况下,Spring 可以将以字符串格式提供的值转换为所有内置类型,例如 int、long、String、boolean 等。 + +Spring 容器在创建容器时验证每个 bean 的配置。但是,在实际创建 bean 之前,不会设置 bean 属性本身。在创建容器时会创建 singleton 型的实例并设置为默认的 Bean。否则,只有在请求时才会创建 bean。 + +需注意:构造器注入,可能会导致无法解决循环依赖问题。 + +例如:A 类通过构造器注入需要 B 类的实例,B 类通过构造器注入需要 A 类的实例。Spring IoC 容器会在运行时检测到此循环引用,并抛出 `BeanCurrentlyInCreationException`。 + +一种解决方案是使用 setter 方法注入替代构造器注入。 + +另一种解决方案是:bean A 和 bean B 之间的循环依赖关系,强制其中一个 bean 在完全初始化之前注入另一个 bean(典型的先有鸡还是先有蛋的场景)。 + +Spring 会在容器加载时检测配置问题,例如引用不存在的 bean 或循环依赖。在实际创建 bean 时,Spring 会尽可能晚地设置属性并解析依赖关系。这意味着,如果在创建该对象或其依赖项之一时出现问题,则正确加载的 Spring 容器稍后可以在您请求对象时生成异常  —  例如,bean 由于丢失或无效而引发异常。某些配置问题的这种潜在的延迟可见性是默认情况下 ApplicationContext 实现预实例化单例 bean 的原因。以在实际需要之前创建这些 bean 的一些前期时间和内存为代价,您会在创建 ApplicationContext 时发现配置问题,而不是稍后。您仍然可以覆盖此默认行为,以便单例 bean 延迟初始化,而不是急切地预先实例化。 + +## 最佳实践 + +### singleton 的 Bean 如何注入 prototype 的 Bean + +Spring 创建的 Bean 默认是单例的,但当 Bean 遇到继承的时候,可能会忽略这一点。 + +假设有一个 SayService 抽象类,其中维护了一个类型是 ArrayList 的字段 data,用于保存方法处理的中间数据。每次调用 say 方法都会往 data 加入新数据,可以认为 SayService 是有状态,如果 SayService 是单例的话必然会 OOM。 + +```java +/** + * SayService 是有状态,如果 SayService 是单例的话必然会 OOM + */ +@Slf4j +public abstract class SayService { + + List data = new ArrayList<>(); + + public void say() { + data.add(IntStream.rangeClosed(1, 1000000) + .mapToObj(__ -> "a") + .collect(Collectors.joining("")) + UUID.randomUUID().toString()); + log.info("I'm {} size:{}", this, data.size()); + } + +} +``` + +但实际开发的时候,开发同学没有过多思考就把 SayHello 和 SayBye 类加上了 @Service 注解,让它们成为了 Bean,也没有考虑到父类是有状态的。 + +```java +@Service +@Slf4j +public class SayBye extends SayService { + + @Override + public void say() { + super.say(); + log.info("bye"); + } + +} + +@Service +@Slf4j +public class SayHello extends SayService { + + @Override + public void say() { + super.say(); + log.info("hello"); + } + +} +``` + +在为类标记上 @Service 注解把类型交由容器管理前,首先评估一下类是否有状态,然后为 Bean 设置合适的 Scope。 + +调用代码: + +```java +@Slf4j +@RestController +@RequestMapping("beansingletonandorder") +public class BeanSingletonAndOrderController { + + @Autowired + List sayServiceList; + @Autowired + private ApplicationContext applicationContext; + + @GetMapping("test") + public void test() { + log.info("===================="); + sayServiceList.forEach(SayService::say); + } + +} +``` + +可能有人认为,为 SayHello 和 SayBye 两个类都标记了 @Scope 注解,设置了 PROTOTYPE 的生命周期就可以解决上面的问题。 + +```java +@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE) +``` + +但实际上还是有问题。因为@RestController 注解 =@Controller 注解 +@ResponseBody 注解,又因为 @Controller 标记了 @Component 元注解,所以 @RestController 注解其实也是一个 Spring Bean。 + +Bean 默认是单例的,所以单例的 Controller 注入的 Service 也是一次性创建的,即使 Service 本身标识了 prototype 的范围也没用。 + +修复方式是,让 Service 以代理方式注入。这样虽然 Controller 本身是单例的,但每次都能从代理获取 Service。这样一来,prototype 范围的配置才能真正生效。 + +```java +@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE, proxyMode = ScopedProx) +``` + +## 参考资料 + +- [Spring 官方文档之 Core Technologies](https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/core.html#beans) +- [《小马哥讲 Spring 核心编程思想》](https://time.geekbang.org/course/intro/265) \ No newline at end of file diff --git "a/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/03.Spring\344\276\235\350\265\226\346\237\245\346\211\276.md" "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/03.Spring\344\276\235\350\265\226\346\237\245\346\211\276.md" new file mode 100644 index 00000000..0d1fa338 --- /dev/null +++ "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/03.Spring\344\276\235\350\265\226\346\237\245\346\211\276.md" @@ -0,0 +1,149 @@ +--- +title: Spring 依赖查找 +date: 2020-08-30 16:06:10 +order: 03 +categories: + - Java + - 框架 + - Spring + - Spring核心 +tags: + - Java + - 框架 + - Spring + - IOC + - 依赖查找 +permalink: /pages/9a6f6b/ +--- + +# Spring 依赖查找 + +**依赖查找是主动或手动的依赖查找方式,通常需要依赖容器或标准 API 实现**。 + +IoC 依赖查找大致可以分为以下几类: + +- 根据 Bean 名称查找 +- 根据 Bean 类型查找 +- 根据 Bean 名称 + 类型查找 +- 根据 Java 注解查找 + +此外,根据查找的 Bean 对象是单一或集合对象,是否需要延迟查找等特定常见,有相应不同的 API。 + +## 单一类型依赖查找 + +单一类型依赖查找接口- `BeanFactory` + +- 根据 Bean 名称查找 + - `getBean(String)` + - Spring 2.5 覆盖默认参数:`getBean(String,Object...)` +- 根据 Bean 类型查找 + - Bean 实时查找 + - Spring 3.0 `getBean(Class)` + - Spring 4.1 覆盖默认参数:`getBean(Class,Object...)` + - Spring 5.1 Bean 延迟查找 + - `getBeanProvider(Class)` + - `getBeanProvider(ResolvableType)` +- 根据 Bean 名称 + 类型查找:`getBean(String,Class)` + +## 集合类型依赖查找 + +集合类型依赖查找接口- `ListableBeanFactory` + +- 根据 Bean 类型查找 + - 获取同类型 Bean 名称列表 + - `getBeanNamesForType(Class)` + - Spring 4.2 `getBeanNamesForType(ResolvableType)` + - 获取同类型 Bean 实例列表 + - `getBeansOfType(Class)` 以及重载方法 +- 通过注解类型查找 + + - Spring 3.0 获取标注类型 Bean 名称列表 + + - `getBeanNamesForAnnotation(Class)` + + - Spring 3.0 获取标注类型 Bean 实例列表 + + - `getBeansWithAnnotation(Class)` + + - Spring 3.0 获取指定名称+ 标注类型 Bean 实例 + + - `findAnnotationOnBean(String,Class)` + +## 层次性依赖查找 + +层次性依赖查找接口- `HierarchicalBeanFactory` + +- 双亲 `BeanFactory`:`getParentBeanFactory()` +- 层次性查找 + - 根据 Bean 名称查找 + - 基于 `containsLocalBean` 方法实现 + - 根据 Bean 类型查找实例列表 + - 单一类型:`BeanFactoryUtils#beanOfType` + - 集合类型:`BeanFactoryUtils#beansOfTypeIncludingAncestors` + - 根据 Java 注解查找名称列表 + - `BeanFactoryUtils#beanNamesForTypeIncludingAncestors` + +## 延迟依赖查找 + +Bean 延迟依赖查找接口 + +- `org.springframework.beans.factory.ObjectFactory` +- `org.springframework.beans.factory.ObjectProvider`(Spring 5 对 Java 8 特性扩展) +- 函数式接口 + - `getIfAvailable(Supplier)` + - `ifAvailable(Consumer)` +- Stream 扩展- stream() + +## 安全依赖查找 + +| 依赖查找类型 | 代表实现 | 是否安全 | +| ------------ | ------------------------------------ | -------- | +| 单一类型查找 | `BeanFactory#getBean` | 否 | +| | `ObjectFactory#getObject` | 否 | +| | `ObjectProvider#getIfAvailable` | 是 | +| | | | +| 集合类型查找 | `ListableBeanFactory#getBeansOfType` | 是 | +| | `ObjectProvider#stream` | 是 | + +注意:层次性依赖查找的安全性取决于其扩展的单一或集合类型的 `BeanFactory` 接口 + +## 内建可查找的依赖 + +`AbstractApplicationContext` 内建可查找的依赖 + +| Bean | 名称 Bean | 实例使用场景 | +| --------------------------- | -------------------------------- | ----------------------- | +| environment | Environment 对象 | 外部化配置以及 Profiles | +| systemProperties | java.util.Properties 对象 | Java 系统属性 | +| systemEnvironment | java.util.Map 对象 | 操作系统环境变量 | +| messageSource | MessageSource 对象 | 国际化文案 | +| lifecycleProcessor | LifecycleProcessor 对象 | Lifecycle Bean 处理器 | +| applicationEventMulticaster | ApplicationEventMulticaster 对象 | Spring 事件广播器 | + +注解驱动 Spring 应用上下文内建可查找的依赖(部分) + +| Bean 名称 | Bean 实例 | 使用场景 | +| ------------------------------------------------------------------------------- | ------------------------------------------- | ----------------------------------------------------- | +| org.springframework.context.annotation.internalConfigurationAnnotationProcessor | ConfigurationClassPostProcessor 对象 | 处理 Spring 配置类 | +| org.springframework.context.annotation.internalAutowiredAnnotationProcessor | AutowiredAnnotationBeanPostProcessor 对象 | 处理@Autowired 以及@Value 注解 | +| org.springframework.context.annotation.internalCommonAnnotationProcessor | CommonAnnotationBeanPostProcessor 对象 | (条件激活)处理 JSR-250 注解,如@PostConstruct 等 | +| org.springframework.context.event.internalEventListenerProcessor | EventListenerMethodProcessor 对象 | 处理标注@EventListener 的 Spring 事件监听方法 | +| org.springframework.context.event.internalEventListenerFactory | DefaultEventListenerFactory 对象 | @EventListener 事件监听方法适配为 ApplicationListener | +| org.springframework.context.annotation.internalPersistenceAnnotationProcessor | PersistenceAnnotationBeanPostProcessor 对象 | (条件激活)处理 JPA 注解场景 | + +## 依赖查找中的经典异常 + +`BeansException` 子类型 + +| 异常类型 | 触发条件(举例) | 场景举例 | +| --------------------------------- | ------------------------------------------ | -------------------------------------------- | +| `NoSuchBeanDefinitionException` | 当查找 Bean 不存在于 IoC 容器时 | `BeanFactory#getBeanObjectFactory#getObject` | +| `NoUniqueBeanDefinitionException` | 类型依赖查找时,IoC 容器存在多个 Bean 实例 | `BeanFactory#getBean(Class)` | +| `BeanInstantiationException` | 当 Bean 所对应的类型非具体类时 | `BeanFactory#getBean` | +| `BeanCreationException` | 当 Bean 初始化过程中 | Bean 初始化方法执行异常时 | +| `BeanDefinitionStoreException` | 当 `BeanDefinition` 配置元信息非法时 | XML 配置资源无法打开时 | + +## 参考资料 + +- [Spring 官方文档之 Core Technologies](https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/core.html#beans) +- [《小马哥讲 Spring 核心编程思想》](https://time.geekbang.org/course/intro/265) \ No newline at end of file diff --git "a/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/04.Spring\344\276\235\350\265\226\346\263\250\345\205\245.md" "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/04.Spring\344\276\235\350\265\226\346\263\250\345\205\245.md" new file mode 100644 index 00000000..c44e438f --- /dev/null +++ "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/04.Spring\344\276\235\350\265\226\346\263\250\345\205\245.md" @@ -0,0 +1,378 @@ +--- +title: Spring 依赖注入 +date: 2020-08-30 16:06:10 +order: 04 +categories: + - Java + - 框架 + - Spring + - Spring核心 +tags: + - Java + - 框架 + - Spring + - IOC + - 依赖注入 +permalink: /pages/f61a1c/ +--- + +# Spring 依赖注入 + +DI,是 Dependency Injection 的缩写,即依赖注入。依赖注入是 IoC 的最常见形式。依赖注入是手动或自动绑定的方式,无需依赖特定的容器或 API。 + +依赖注入 (Dependency Injection,简称 DI) 是一个过程,其中对象仅通过构造函数参数、工厂方法的参数或对象实例在构造或从工厂方法返回。然后容器在创建 bean 时注入这些依赖项。这个过程基本上是 bean 本身的逆过程(因此得名,控制反转),它通过使用类的直接构造或服务定位器模式自行控制其依赖项的实例化或位置。 + +使用 DI,代码更干净,当对象具有依赖关系时,解耦更有效。对象不查找其依赖项,也不知道依赖项的位置或类别。结果,您的类变得更容易测试,特别是当依赖关系在接口或抽象基类上时,它们允许在单元测试中使用存根或模拟实现。 + +**容器全权负责组件的装配,它会把符合依赖关系的对象通过 JavaBean 属性或者构造函数传递给需要的对象**。 + +DI 是组件之间依赖关系由容器在运行期决定,形象的说,即由容器动态的将某个依赖关系注入到组件之中。依赖注入的目的并非为软件系统带来更多功能,而是为了提升组件重用的频率,并为系统搭建一个灵活、可扩展的平台。通过依赖注入机制,我们只需要通过简单的配置,而无需任何代码就可指定目标需要的资源,完成自身的业务逻辑,而不需要关心具体的资源来自何处,由谁实现。 + +理解 DI 的关键是:“谁依赖谁,为什么需要依赖,谁注入谁,注入了什么”,那我们来深入分析一下: + +- **谁依赖于谁:**当然是应用程序依赖于 IoC 容器; +- **为什么需要依赖:**应用程序需要 IoC 容器来提供对象需要的外部资源; +- **谁注入谁:**很明显是 IoC 容器注入应用程序某个对象,应用程序依赖的对象; +- **注入了什么**:就是注入某个对象所需要的外部资源(包括对象、资源、常量数据)。 + +## IoC 依赖注入 API + +- 根据 Bean 名称注入 +- 根据 Bean 类型注入 +- 注入容器内建 Bean 对象 +- 注入非 Bean 对象 +- 注入类型 + - 实时注入 + - 延迟注入 + +## 依赖注入模式 + +依赖注入模式可以分为手动注入模式和自动注入模式。 + +### 手动注入模式 + +手动注入模式:配置或者编程的方式,提前安排注入规则 + +- XML 资源配置元信息 +- Java 注解配置元信息 +- API 配置元信息 + +### 自动注入模式 + +自动注入模式即自动装配。自动装配(Autowiring)是指 Spring 容器可以自动装配 Bean 之间的关系。Spring 可以通过检查 `ApplicationContext` 的内容,自动解析合作者(其他 Bean)。 + +- 自动装配可以显著减少属性或构造函数参数的配置。 +- 随着对象的发展,自动装配可以更新配置。 + +> 注:由于自动装配存在一些限制和不足,官方不推荐使用。 + +#### 自动装配策略 + +当使用基于 XML 的配置元数据时,可以使用 `` 元素的 `autowire` 属性为 Bean 指定自动装配模式。自动装配模式有以下类型: + +| 模式 | 说明 | +| ------------- | ---------------------------------------------------------------------- | +| `no` | 默认值,未激活 Autowiring,需要手动指定依赖注入对象。 | +| `byName` | 根据被注入属性的名称作为 Bean 名称进行依赖查找,并将对象设置到该属性。 | +| `byType` | 根据被注入属性的类型作为依赖类型进行查找,并将对象设置到该属性。 | +| `constructor` | 特殊 byType 类型,用于构造器参数。 | + +`org.springframework.beans.factory.config.AutowireCapableBeanFactory` 是 `BeanFactory` 的子接口,它是 Spring 中用于实现自动装配的容器。 + +#### @Autowired 注入过程 + +- 元信息解析 +- 依赖查找 +- 依赖注入(字段、方法) + +#### 自动装配的限制和不足 + +自动装配有以下限制和不足: + +- 属性和构造函数参数设置中的显式依赖项会覆盖自动装配。您不能自动装配简单属性,例如基础数据类型、字符串和类(以及此类简单属性的数组)。 +- 自动装配不如显式装配精准。Spring 会尽量避免猜测可能存在歧义的结果。 +- Spring 容器生成文档的工具可能无法解析自动装配信息。 +- 如果同一类型存在多个 Bean 时,自动装配时会存在歧义。容器内的多个 Bean 定义可能与要自动装配的 Setter 方法或构造函数参数指定的类型匹配。对于数组、集合或 Map 实例,这不一定是问题。但是,对于期望单值的依赖项,如果没有唯一的 Bean 定义可用,则会引发异常。 + +> 自动装配的限制和不足,详情可以参考官方文档:[Limitations and Disadvantages of Autowiring 小节](https://docs.spring.io/spring/docs/5.2.2.RELEASE/spring-frameworkreference/core.html#beans-autowired-exceptions) + +## 依赖注入方式 + +依赖注入有如下方式: + +| 依赖注入方式 | 配置元数据举例 | +| --------------- | -------------------------------------------------- | +| Setter 方法注入 | `` | +| 构造器注入 | `` | +| 字段注入 | `@Autowired User user;` | +| 方法注入 | `@Autowired public void user(User user) { ... }` | +| 接口回调注入 | `class MyBean implements BeanFactoryAware { ... }` | + +### 构造器注入 + +- 手动模式 + - xml 配置元信息 + - 注解配置元信息 + - Java 配置元信息 +- 自动模式 + - constructor + +构造器注入是通过容器调用具有多个参数的构造函数来完成的,每个参数代表一个依赖项。调用带有特定参数的静态工厂方法来构造 bean 几乎是等价的,并且本次讨论对构造函数和静态工厂方法的参数进行了类似的处理。 + +下面是一个构造器注入示例: + +```java +public class SimpleMovieLister { + + // the SimpleMovieLister has a dependency on a MovieFinder + private final MovieFinder movieFinder; + + // a constructor so that the Spring container can inject a MovieFinder + public SimpleMovieLister(MovieFinder movieFinder) { + this.movieFinder = movieFinder; + } + + // business logic that actually uses the injected MovieFinder is omitted... +} +``` + +构造函数参数解析匹配通过使用参数的类型进行。如果 bean 定义的构造函数参数中不存在潜在的歧义,则在 bean 定义中定义构造函数参数的顺序是在实例化 bean 时将这些参数提供给适当构造函数的顺序。 + +``` +package x.y; + +public class ThingOne { + + public ThingOne(ThingTwo thingTwo, ThingThree thingThree) { + // ... + } +} +``` + +假设 ThingTwo 和 ThingThree 类没有继承关系,则不存在潜在的歧义。因此,以下配置工作正常,您无需在 `` 元素中显式指定构造函数参数索引或类型。 + +```xml + + + + + + + + + + +``` + +当引用另一个 bean 时,类型是已知的,并且可以发生匹配(就像前面的示例一样)。当使用简单类型时,例如 `true` ,Spring 无法确定 value 的类型,因此无法在没有帮助的情况下按类型匹配。考虑以下类: + +```java +package examples; + +public class ExampleBean { + + // Number of years to calculate the Ultimate Answer + private final int years; + + // The Answer to Life, the Universe, and Everything + private final String ultimateAnswer; + + public ExampleBean(int years, String ultimateAnswer) { + this.years = years; + this.ultimateAnswer = ultimateAnswer; + } +} +``` + +构造函数参数类型匹配 + +在上述场景中,如果您使用 type 属性显式指定构造函数参数的类型,则容器可以使用简单类型的类型匹配,如以下示例所示: + +```xml + + + + +``` + +构造函数参数索引匹配 + +可以使用 `index` 属性显式指定构造函数参数的索引,如以下示例所示 + +```xml + + + + +``` + +构造函数参数名称匹配 + +```xml + + + + +``` + +可以使用 `@ConstructorProperties` 显式命名构造函数参数。 + +```java +package examples; + +public class ExampleBean { + + // Fields omitted + + @ConstructorProperties({"years", "ultimateAnswer"}) + public ExampleBean(int years, String ultimateAnswer) { + this.years = years; + this.ultimateAnswer = ultimateAnswer; + } +} +``` + +### Setter 方法注入 + +- 手动模式 + - xml 配置元信息 + - 注解配置元信息 + - Java 配置元信息 +- 自动模式 + - byName + - byType + +Setter 方法注入是通过容器在调用无参数构造函数或无参数静态工厂方法来实例化 bean 后调用 bean 上的 setter 方法来完成的。 + +以下示例显示了一个只能通过使用纯 setter 注入进行依赖注入的类。 + +```java +public class SimpleMovieLister { + + // the SimpleMovieLister has a dependency on the MovieFinder + private MovieFinder movieFinder; + + // a setter method so that the Spring container can inject a MovieFinder + public void setMovieFinder(MovieFinder movieFinder) { + this.movieFinder = movieFinder; + } + + // business logic that actually uses the injected MovieFinder is omitted... +} +``` + +在 Spring 中,可以混合使用构造器注入和 setter 方法注入。建议将构造器注入用于强制依赖项;并将 setter 方法注入或配置方法用于可选依赖项。需要注意的是,在 setter 方法上使用 `@Required` 注解可用于使属性成为必需的依赖项;然而,更建议使用构造器注入来完成这项工作。 + +### 字段注入 + +手动模式(Java 注解配置元信息) + +- `@Autowired` +- `@Resource` +- `@Inject`(可选) + +### 方法注入 + +手动模式(Java 注解配置元信息) + +- `@Autowired` +- `@Resource` +- `@Inject`(可选) +- `@Bean` + +### 接口回调注入 + +Aware 系列接口回调 + +| 內建接口 | 说明 | +| -------------------------------- | ---------------------------------------------------------- | +| `BeanFactoryAware` | 获取 IoC 容器- `BeanFactory` | +| `ApplicationContextAware` | 获取 Spring 应用上下文- `ApplicationContext` 对象 | +| `EnvironmentAware` | 获取 `Environment` 对象 | +| `ResourceLoaderAware` | 获取资源加载器对象- `ResourceLoader` | +| `BeanClassLoaderAware` | 获取加载当前 Bean Class 的 `ClassLoader` | +| `BeanNameAware` | 获取当前 Bean 的名称 | +| `MessageSourceAware` | 获取 `MessageSource` 对象,用于 Spring 国际化 | +| `ApplicationEventPublisherAware` | 获取 `ApplicationEventPublishAware` 对象,用于 Spring 事件 | +| `EmbeddedValueResolverAware` | 获取 `StringValueResolver` 对象,用于占位符处理 | + +### 依赖注入选型 + +- 低依赖:构造器注入 +- 多依赖:Setter 方法注入 +- 便利性:字段注入 +- 声明类:方法注入 + +## 限定注入和延迟注入 + +### 限定注入 + +- 使用 `@Qualifier` 注解限定 + - 通过 Bean 名称限定 + - 通过分组限定 +- 通过 `@Qualifier` 注解扩展限定 + - 自定义注解:如 Spring Cloud 的 `@LoadBalanced` + +### 延迟注入 + +- 使用 `ObjectFactory` +- 使用 `ObjectProvider`(推荐) + +## 依赖注入数据类型 + +### 基础类型 + +- 基础数据类型:`boolean`、`byte`、`char`、`short`、`int`、`float`、`long`、`double` +- 标量类型:`Number`、`Character`、`Boolean`、`Enum`、`Locale`、`Charset`、`Currency`、`Properties`、`UUID` +- 常规类型:`Object`、`String`、`TimeZone`、`Calendar`、`Optional` 等 +- Spring 类型:`Resource`、`InputSource`、`Formatter` 等。 + +### 集合类型 + +数组类型:基础数据类型、标量类型、常规类型、String 类型的数组 + +集合类型: + +- `Collection`:`List`、`Set` +- `Map`:`Properties` + +## 依赖处理过程 + +入口:`DefaultListableBeanFactory#resolveDependency` + +依赖描述符:`DependencyDescriptor` + +自定义绑定候选对象处理器:`AutowireCandidateResolver` + +`@Autowired`、`@Value`、`@javax.inject.Inject` 处理器:`AutowiredAnnotationBeanPostProcessor` + +通用注解处理器:`CommonAnnotationBeanPostProcessor` + +- 注入注解 + - `javax.xml.ws.WebServiceRef` + - `javax.ejb.EJB` + - `javax.annotation.Resources` +- 生命周期注解 + - `javax.annotation.PostConstruct` + - `javax.annotation.PreDestroy` + +自定义依赖注入注解: + +- 生命周期处理 + - `InstantiationAwareBeanPostProcessor` + - `MergedBeanDefinitionPostProcessor` +- 元数据 + - `InjectionMetadata` + - `InjectionMetadata.InjectedElement` + +## 依赖查找 VS. 依赖注入 + +| 类型 | 依赖处理 | 实现复杂度 | 代码侵入性 | API 依赖性 | 可读性 | +| -------- | -------- | ---------- | ------------ | -------------- | ------ | +| 依赖查找 | 主动 | 相对繁琐 | 侵入业务逻辑 | 依赖容器 API | 良好 | +| 依赖注入 | 被动 | 相对便利 | 低侵入性 | 不依赖容器 API | 一般 | + +## 参考资料 + +- [Spring 官方文档之 Core Technologies](https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/core.html#beans) +- [《小马哥讲 Spring 核心编程思想》](https://time.geekbang.org/course/intro/265) \ No newline at end of file diff --git "a/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/05.SpringIoC\344\276\235\350\265\226\346\235\245\346\272\220.md" "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/05.SpringIoC\344\276\235\350\265\226\346\235\245\346\272\220.md" new file mode 100644 index 00000000..b633b853 --- /dev/null +++ "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/05.SpringIoC\344\276\235\350\265\226\346\235\245\346\272\220.md" @@ -0,0 +1,124 @@ +--- +title: Spring IoC 依赖来源 +date: 2022-12-20 20:33:51 +order: 05 +categories: + - Java + - 框架 + - Spring + - Spring核心 +tags: + - Java + - 框架 + - Spring + - IOC + - 依赖注入 +permalink: /pages/a5f257/ +--- + +# Spring IoC 依赖来源 + +## 依赖查找的来源 + +查找来源 + +| 来源 | 配置元数据 | +| --------------------- | ---------------------------------------- | +| Spring BeanDefinition | `` | +| | `@Bean public User user() {...}` | +| | `BeanDefinitionBuilder` | +| 单例对象 | API 实现 | + +Spring 內建 BeanDefintion + +| Bean 名称 | Bean 实例 | 使用场景 | +| ------------------------------------------------------------------------------- | ----------------------------------------- | --------------------------------------------------- | +| org.springframework.context.annotation.internalConfigurationAnnotationProcessor | ConfigurationClassPostProcessor 对象 | 处理 Spring 配置类 | +| org.springframework.context.annotation.internalAutowiredAnnotationProcessor | AutowiredAnnotationBeanPostProcessor 对象 | 处理 @Autowired 以及 @Value 注解 | +| org.springframework.context.annotation.internalCommonAnnotationProcessor | CommonAnnotationBeanPostProcessor 对象 | (条件激活)处理 JSR-250 注解,如 @PostConstruct 等 | +| org.springframework.context.event.internalEventListenerProcessor | EventListenerMethodProcessor 对象 | 处理标注 @EventListener 的 Spring 事件监听方法 | + +Spring 內建单例对象 + +| Bean 名称 | Bean 实例 | 使用场景 | +| --------------------------- | -------------------------------- | ----------------------- | +| environment | Environment 对象 | 外部化配置以及 Profiles | +| systemProperties | java.util.Properties 对象 | Java 系统属性 | +| systemEnvironment | java.util.Map 对象 | 操作系统环境变量 | +| messageSource | MessageSource 对象 | 国际化文案 | +| lifecycleProcessor | LifecycleProcessor 对象 | Lifecycle Bean 处理器 | +| applicationEventMulticaster | ApplicationEventMulticaster 对象 | Spring 事件广播器 | + +## 依赖注入的来源 + +| 来源 | 配置元数据 | +| ---------------------- | ---------------------------------------- | +| Spring BeanDefinition | `` | +| | `@Bean public User user() {...}` | +| | `BeanDefinitionBuilder` | +| 单例对象 | API 实现 | +| 非 Spring 容器管理对象 | | + +## Spring 容器管理和游离对象 + +| 来源 | Spring Bean 对象 | 生命周期管理 | 配置元信息 | 使用场景 | +| --------------------- | ---------------- | ------------ | ---------- | ------------------ | +| Spring BeanDefinition | 是 | 是 | 有 | 依赖查找、依赖注入 | +| 单体对象 | 是 | 否 | 无 | 依赖查找、依赖注入 | +| Resolvable Dependency | 否 | 否 | 无 | 依赖注入 | + +## Spring BeanDefinition 作为依赖来源 + +- 元数据:`BeanDefinition` +- 注册:`BeanDefinitionRegistry#registerBeanDefinition` +- 类型:延迟和非延迟 +- 顺序:Bean 生命周期顺序按照注册顺序 + +## 单例对象作为依赖来源 + +- 要素 + - 来源:外部普通 Java 对象(不一定是 POJO) + - 注册:`SingletonBeanRegistry#registerSingleton` +- 限制 + - 无生命周期管理 + - 无法实现延迟初始化 Bean + +## 非 Spring 对象容器管理对象作为依赖来源 + +- 要素 + - 注册:`ConfigurableListableBeanFactory#registerResolvableDependency` +- 限制 + - 无生命周期管理 + - 无法实现延迟初始化 Bean + - 无法通过依赖查找 + +## 外部化配置作为依赖来源 + +- 要素 + - 类型:非常规 Spring 对象依赖来源 +- 限制 + - 无生命周期管理 + - 无法实现延迟初始化 Bean + - 无法通过依赖查找 + +## 问题 + +注入和查找的依赖来源是否相同? + +否,依赖查找的来源仅限于 Spring `BeanDefinition` 以及单例对象,而依赖注入的来源还包括 Resolvable Dependency 以及 `@Value` 所标注的外部化配置 + +单例对象能在 IoC 容器启动后注册吗? + +可以的,单例对象的注册与 `BeanDefinition` 不同,`BeanDefinition` 会被 `ConfigurableListableBeanFactory#freezeConfiguration()` 方法影响,从而冻结注册,单例对象则没有这个限制。 + +Spring 依赖注入的来源有哪些? + +- Spring `BeanDefinition` +- 单例对象 +- Resolvable Dependency +- `@Value` 外部化配置 + +## 参考资料 + +- [Spring 官方文档之 Core Technologies](https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/core.html#beans) +- [《小马哥讲 Spring 核心编程思想》](https://time.geekbang.org/course/intro/265) \ No newline at end of file diff --git "a/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/06.SpringBean\344\275\234\347\224\250\345\237\237.md" "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/06.SpringBean\344\275\234\347\224\250\345\237\237.md" new file mode 100644 index 00000000..c07dacac --- /dev/null +++ "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/06.SpringBean\344\275\234\347\224\250\345\237\237.md" @@ -0,0 +1,102 @@ +--- +title: Spring Bean 作用域 +date: 2022-12-21 11:42:00 +order: 06 +categories: + - Java + - 框架 + - Spring + - Spring核心 +tags: + - Java + - 框架 + - Spring + - Bean +permalink: /pages/8289f5/ +--- + +# Spring Bean 作用域 + +## Spring Bean 作用域 + +| 来源 | 说明 | +| ----------- | ---------------------------------------------------------- | +| singleton | 默认 Spring Bean 作用域,一个 BeanFactory 有且仅有一个实例 | +| prototype | 原型作用域,每次依赖查找和依赖注入生成新 Bean 对象 | +| request | 将 Spring Bean 存储在 ServletRequest 上下文中 | +| session | 将 Spring Bean 存储在 HttpSession 中 | +| application | 将 Spring Bean 存储在 ServletContext 中 | + +## "singleton" Bean 作用域 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/20221221170833.png) + +## "prototype" Bean 作用域 + +Spring 容器没有办法管理 prototype Bean 的完整生命周期,也没有办法记录实例的存在。销毁回调方法将不会执行,可以利用 `BeanPostProcessor` 进行清扫工作。 + +## "request" Bean 作用域 + +- 配置 + - XML - `` + - Java 注解 - `@RequestScope` 或 `@Scope(WebApplicationContext.SCOPE_REQUEST)` +- 实现 + - API - RequestScope + +## "session" Bean 作用域 + +- 配置 + - XML - `` + - Java 注解 - `@SessionScope` 或 `@Scope(WebApplicationContext.SCOPE_SESSION)` +- 实现 + - API - SessionScope + +## "application" Bean 作用域 + +- 配置 + - XML - `` + - Java 注解 - `@ApplicationScope` 或 `@Scope(WebApplicationContext.SCOPE_APPLICATION)` +- 实现 + - API - ServletContextScope + +## 自定义 Bean 作用域 + +- 实现 Scope + + - `org.springframework.beans.factory.config.Scope` + +- 注册 Scope + + - API - `org.springframework.beans.factory.config.ConfigurableBeanFactory#registerScope` + +- 配置 + + ```xml + + + + + + + + + ``` + +## 问题 + +Spring 內建的 Bean 作用域有几种? + +singleton、prototype、request、session、application 以及 websocket + +singleton Bean 是否在一个应用是唯一的? + +否。singleton bean 仅在当前 Spring IoC 容器(BeanFactory)中是单例对象。 + +application Bean 是否可以被其他方案替代? + +可以的,实际上,“application” Bean 与“singleton” Bean 没有本质区别 + +## 参考资料 + +- [Spring 官方文档之 Core Technologies](https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/core.html#beans) +- [《小马哥讲 Spring 核心编程思想》](https://time.geekbang.org/course/intro/265) \ No newline at end of file diff --git "a/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/07.SpringBean\347\224\237\345\221\275\345\221\250\346\234\237.md" "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/07.SpringBean\347\224\237\345\221\275\345\221\250\346\234\237.md" new file mode 100644 index 00000000..4a72cd8c --- /dev/null +++ "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/07.SpringBean\347\224\237\345\221\275\345\221\250\346\234\237.md" @@ -0,0 +1,186 @@ +--- +title: Spring Bean 生命周期 +date: 2022-12-21 19:26:01 +order: 07 +categories: + - Java + - 框架 + - Spring + - Spring核心 +tags: + - Java + - 框架 + - Spring + - Bean +permalink: /pages/4ab176/ +--- + +# Spring Bean 生命周期 + +## Spring Bean 元信息配置阶段 + +BeanDefinition 配置 + +- 面向资源 + - XML 配置 + - Properties 资源配置 +- 面向注解 +- 面向 API + +## Spring Bean 元信息解析阶段 + +- 面向资源 BeanDefinition 解析 + - BeanDefinitionReader + - XML 解析器 - BeanDefinitionParser +- 面向注解 BeanDefinition 解析 + - AnnotatedBeanDefinitionReader + +## Spring Bean 注册阶段 + +BeanDefinition 注册接口:BeanDefinitionRegistry + +## Spring BeanDefinition 合并阶段 + +BeanDefinition 合并 + +父子 BeanDefinition 合并 + +- 当前 BeanFactory 查找 +- 层次性 BeanFactory 查找 + +## Spring Bean Class 加载阶段 + +- ClassLoader 类加载 +- Java Security 安全控制 +- ConfigurableBeanFactory 临时 ClassLoader + +## Spring Bean 实例化前阶段 + +实例化方式 + +- 传统实例化方式:实例化策略(InstantiationStrategy) +- 构造器依赖注入 + +## Spring Bean 实例化阶段 + +非主流生命周期 - Bean 实例化前阶段 + +InstantiationAwareBeanPostProcessor#postProcessBeforeInstantiation + +## Spring Bean 实例化后阶段 + +Bean 属性赋值(Populate)判断 + +InstantiationAwareBeanPostProcessor#postProcessAfterInstantiation + +## Spring Bean 属性赋值前阶段 + +- Bean 属性值元信息 + - PropertyValues +- Bean 属性赋值前回调 + - Spring 1.2 - 5.0:InstantiationAwareBeanPostProcessor#postProcessPropertyValues + - Spring 5.1:InstantiationAwareBeanPostProcessor#postProcessProperties + +## Spring Bean Aware 接口回调阶段 + +Spring Aware 接口: + +- BeanNameAware +- BeanClassLoaderAware +- BeanFactoryAware +- EnvironmentAware +- EmbeddedValueResolverAware +- ResourceLoaderAware +- ApplicationEventPublisherAware +- MessageSourceAware +- ApplicationContextAware + +## Spring Bean 初始化前阶段 + +已完成: + +- Bean 实例化 + +- Bean 属性赋值 + +- Bean Aware 接口回调 + +方法回调: + +- BeanPostProcessor#postProcessBeforeInitialization + +## Spring Bean 初始化阶段 + +Bean 初始化(Initialization) + +- @PostConstruct 标注方法 +- 实现 InitializingBean 接口的 afterPropertiesSet() 方法 +- 自定义初始化方法 + +## Spring Bean 初始化后阶段 + +方法回调:BeanPostProcessor#postProcessAfterInitialization + +## Spring Bean 初始化完成阶段 + +方法回调:Spring 4.1 +:SmartInitializingSingleton#afterSingletonsInstantiated + +## Spring Bean 销毁前阶段 + +方法回调:DestructionAwareBeanPostProcessor#postProcessBeforeDestruction + +## Spring Bean 销毁阶段 + +Bean 销毁(Destroy) + +- @PreDestroy 标注方法 +- 实现 DisposableBean 接口的 destroy() 方法 +- 自定义销毁方法 + +## Spring Bean 垃圾收集 + +Bean 垃圾回收(GC) + +- 关闭 Spring 容器(应用上下文) +- 执行 GC +- Spring Bean 覆盖的 finalize() 方法被回调 + +## 问题 + +**BeanPostProcessor 的使用场景有哪些**? + +BeanPostProcessor 提供 Spring Bean 初始化前和初始化后的生命周期回调,分别对应 postProcessBeforeInitialization 以及 postProcessAfterInitialization 方法,允许对关心的 Bean 进行扩展,甚至是替换。 + +加分项:其中,ApplicationContext 相关的 Aware 回调也是基于 BeanPostProcessor 实现,即 ApplicationContextAwareProcessor。 + +**BeanFactoryPostProcessor 与 BeanPostProcessor 的区别**? + +BeanFactoryPostProcessor 是 Spring BeanFactory(实际为 ConfigurableListableBeanFactory) 的后置处理器,用于扩展 BeanFactory,或通过 BeanFactory 进行依赖查找和依赖注入。 + +BeanFactoryPostProcessor 必须有 Spring ApplicationContext 执行,BeanFactory 无法与其直接交互。 + +而 BeanPostProcessor 则直接与 BeanFactory 关联,属于 N 对 1 的关系。 + +**BeanFactory 是怎样处理 Bean 生命周期**? + +BeanFactory 的默认实现为 `DefaultListableBeanFactory`,其中 Bean生命周期与方法映射如下: + +- BeanDefinition 注册阶段 - registerBeanDefinition +- BeanDefinition 合并阶段 - getMergedBeanDefinition +- Bean 实例化前阶段 - resolveBeforeInstantiation +- Bean 实例化阶段 - createBeanInstance +- Bean 初始化后阶段 - populateBean +- Bean 属性赋值前阶段 - populateBean +- Bean 属性赋值阶段 - populateBean +- Bean Aware 接口回调阶段 - initializeBean +- Bean 初始化前阶段 - initializeBean +- Bean 初始化阶段 - initializeBean +- Bean 初始化后阶段 - initializeBean +- Bean 初始化完成阶段 - preInstantiateSingletons +- Bean 销毁前阶段 - destroyBean +- Bean 销毁阶段 - destroyBean + +## 参考资料 + +- [Spring 官方文档之 Core Technologies](https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/core.html#beans) +- [《小马哥讲 Spring 核心编程思想》](https://time.geekbang.org/course/intro/265) \ No newline at end of file diff --git "a/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/08.Spring\351\205\215\347\275\256\345\205\203\346\225\260\346\215\256.md" "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/08.Spring\351\205\215\347\275\256\345\205\203\346\225\260\346\215\256.md" new file mode 100644 index 00000000..54150343 --- /dev/null +++ "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/08.Spring\351\205\215\347\275\256\345\205\203\346\225\260\346\215\256.md" @@ -0,0 +1,299 @@ +--- +title: Spring 配置元数据 +date: 2022-12-21 19:49:48 +order: 08 +categories: + - Java + - 框架 + - Spring + - Spring核心 +tags: + - Java + - 框架 + - Spring + - Bean +permalink: /pages/55f315/ +--- + +# Spring 配置元数据 + +## Spring 配置元信息 + +- Spring Bean 配置元信息 - BeanDefinition +- Spring Bean 属性元信息 - PropertyValues +- Spring 容器配置元信息 +- Spring 外部化配置元信息 - PropertySource +- Spring Profile 元信息 - @Profile + +## Spring Bean 配置元信息 + +Bean 配置元信息 - BeanDefinition + +- GenericBeanDefinition:通用型 BeanDefinition +- RootBeanDefinition:无 Parent 的 BeanDefinition 或者合并后 BeanDefinition +- AnnotatedBeanDefinition:注解标注的 BeanDefinition + +## Spring Bean 属性元信息 + +- Bean 属性元信息 - PropertyValues + - 可修改实现 - MutablePropertyValues + - 元素成员 - PropertyValue +- Bean 属性上下文存储 - AttributeAccessor +- Bean 元信息元素 - BeanMetadataElement + +## Spring 容器配置元信息 + +Spring XML 配置元信息 - beans 元素相关 + +| beans 元素属性 | 默认值 | 使用场景 | +| --------------------------- | ------------ | ----------------------------------------------------------------------- | +| profile | null(留空) | Spring Profiles 配置值 | +| default-lazy-init | default | 当 outter beans “default-lazy-init” 属性存在时,继承该值,否则为“false” | +| default-merge | default | 当 outter beans “default-merge” 属性存在时,继承该值,否则为“false” | +| default-autowire | default | 当 outter beans “default-autowire” 属性存在时,继承该值,否则为“no” | +| default-autowire-candidates | null(留空) | 默认 Spring Beans 名称 pattern | +| default-init-method | null(留空) | 默认 Spring Beans 自定义初始化方法 | +| default-destroy-method | null(留空) | 默认 Spring Beans 自定义销毁方法 | + +Spring XML 配置元信息 - 应用上下文相关 + +| XML 元素 | 使用场景 | +| ---------------------------------- | ------------------------------------ | +| `` | 激活 Spring 注解驱动 | +| `` | Spring @Component 以及自定义注解扫描 | +| `` | 激活 Spring LoadTimeWeaver | +| `` | 暴露 Spring Beans 作为 JMX Beans | +| `` | 将当前平台作为 MBeanServer | +| `` | 加载外部化配置资源作为 Spring 属性配 | +| `` | 利用外部化配置资源覆盖 Spring 属 | + +## 基于 XML 文件装载 Spring Bean 配置元信息 + +底层实现 - XmlBeanDefinitionReader + +| XML 元素 | 使用场景 | +| ------------------ | --------------------------------------------- | +| `` | 单 XML 资源下的多个 Spring Beans 配置 | +| `` | 单个 Spring Bean 定义(BeanDefinition)配置 | +| `` | 为 Spring Bean 定义(BeanDefinition)映射别名 | +| `` | 加载外部 Spring XML 配置资源 | + +## 基于 Properties 文件装载 Spring Bean 配置元信息 + +底层实现 - PropertiesBeanDefinitionReader + +| Properties 属性名 | 使用场景 | +| ----------------- | ------------------------------- | +| `class` | Bean 类全称限定名 | +| `abstract` | 是否为抽象的 BeanDefinition | +| `parent` | 指定 parent BeanDefinition 名称 | +| `lazy-init` | 是否为延迟初始化 | +| `ref` | 引用其他 Bean 的名称 | +| `scope` | 设置 Bean 的 scope 属性 | +| ${n} | n 表示第 n+1 个构造器参数 | + +## 基于 Java 注解装载 Spring Bean 配置元信息 + +Spring 模式注解 + +| Spring 注解 | 场景说明 | 起始版本 | +| ---------------- | ------------------ | -------- | +| `@Repository` | 数据仓储模式注解 | 2.0 | +| `@Component` | 通用组件模式注解 | 2.5 | +| `@Service` | 服务模式注解 | 2.5 | +| `@Controller` | Web 控制器模式注解 | 2.5 | +| `@Configuration` | 配置类模式注解 | 3.0 | + +Spring Bean 定义注解 + +| Spring 注解 | 场景说明 | 起始版本 | +| ------------ | ------------------------------------------ | ----------- | --- | +| `@Bean` | 替换 XML 元素 `` | 3.0 | +| `@DependsOn` | 替代 XML 属性 `` | 3.0 | +| `@Lazy` | 替代 XML 属性 `` | 3.0 | +| `@Primary` | 替换 XML 元素 `` | 3.0 | +| `@Role` | 替换 XML 元素 `` | 3.1 | +| `@Lookup` | 替代 XML 属性 `` | 4.1 | + +Spring Bean 依赖注入注解 + +| Spring 注解 | 场景说明 | 起始版本 | +| ------------ | ----------------------------------- | -------- | +| `@Autowired` | Bean 依赖注入,支持多种依赖查找方式 | 2.5 | +| `@Qualifier` | 细粒度的 @Autowired 依赖查找 | 2.5 | + +| Java 注解 | 场景说明 | 起始版本 | +| --------- | ----------------- | -------- | +| @Resource | 类似于 @Autowired | 2.5 | +| @Inject | 类似于 @Autowired | 2.5 | + +Spring Bean 条件装配注解 + +| Spring 注解 | 场景说明 | 起始版本 | +| ------------ | -------------- | -------- | +| @Profile | 配置化条件装配 | 3.1 | +| @Conditional | 编程条件装配 | 4.0 | + +Spring Bean 生命周期回调注解 + +| Spring 注解 | 场景说明 | 起始版本 | +| -------------- | ------------------------------------------------------------- | -------- | +| @PostConstruct | 替换 XML 元素 或 InitializingBean | 2.5 | +| @PreDestroy | 替换 XML 元素 或 DisposableBean | 2.5 | + +Spring BeanDefinition 解析与注册 + +| Spring 注解 | 场景说明 | 起始版本 | +| --------------- | ------------------------------ | -------- | +| XML 资源 | XmlBeanDefinitionReader | 1.0 | +| Properties 资源 | PropertiesBeanDefinitionReader | 1.0 | +| Java 注解 | AnnotatedBeanDefinitionReader | 3.0 | + +## Spring Bean 配置元信息底层实现 + +### Spring XML 资源 BeanDefinition 解析与注册 + +核心 API - XmlBeanDefinitionReader + +- 资源 - Resource +- 底层 - BeanDefinitionDocumentReader + - XML 解析 - Java DOM Level 3 API + - BeanDefinition 解析 - BeanDefinitionParserDelegate + - BeanDefinition 注册 - BeanDefinitionRegistry + +### Spring Properties 资源 BeanDefinition 解析与注册 + +核心 API - PropertiesBeanDefinitionReader + +- 资源 + - 字节流 - Resource + - 字符流 - EncodedResouce +- 底层 + - 存储 - java.util.Properties + - BeanDefinition 解析 - API 内部实现 + - BeanDefinition 注册 - BeanDefinitionRegistry + +### Spring Java 注册 BeanDefinition 解析与注册 + +核心 API - AnnotatedBeanDefinitionReader + +- 资源 + - 类对象 - java.lang.Class +- 底层 + - 条件评估 - ConditionEvaluator + - Bean 范围解析 - ScopeMetadataResolver + - BeanDefinition 解析 - 内部 API 实现 + - BeanDefinition 处理 - AnnotationConfigUtils.processCommonDefinitionAnnotations + - BeanDefinition 注册 - BeanDefinitionRegistry + +## 基于 XML 文件装载 Spring IoC 容器配置元信息 + +Spring IoC 容器相关 XML 配置 + +| 命名空间 | 所属模块 | Schema 资源 URL | +| -------- | -------------- | ----------------------------------------------------------------- | +| beans | spring-beans | https://www.springframework.org/schema/beans/spring-beans.xsd | +| context | spring-context | https://www.springframework.org/schema/context/spring-context.xsd | +| aop | spring-aop | https://www.springframework.org/schema/aop/spring-aop.xsd | +| tx | spring-tx | https://www.springframework.org/schema/tx/spring-tx.xsd | +| util | spring-beans | beans https://www.springframework.org/schema/util/spring-util.xsd | +| tool | spring-beans | https://www.springframework.org/schema/tool/spring-tool.xsd | + +## 基于 Java 注解装载 Spring IoC 容器配置元信息 + +Spring IoC 容器装配注解 + +| Spring 注解 | 场景说明 | 起始版本 | +| --------------- | ------------------------------------------- | -------- | +| @ImportResource | 替换 XML 元素 `` | 3.0 | +| @Import | 导入 Configuration Class | 3.0 | +| @ComponentScan | 扫描指定 package 下标注 Spring 模式注解的类 | 3.1 | + +Spring IoC 配属属性注解 + +| Spring 注解 | 场景说明 | 起始版本 | +| ---------------- | -------------------------------- | -------- | +| @PropertySource | 配置属性抽象 PropertySource 注解 | 3.1 | +| @PropertySources | @PropertySource 集合注解 | 4.0 | + +## 基于 Extensible XML authoring 扩展 SpringXML 元素 + +Spring XML 扩展 + +- 编写 XML Schema 文件:定义 XML 结构 +- 自定义 NamespaceHandler 实现:命名空间绑定 +- 自定义 BeanDefinitionParser 实现:XML 元素与 BeanDefinition 解析 +- 注册 XML 扩展:命名空间与 XML Schema 映射 + +## Extensible XML authoring 扩展原理 + +### 触发时机 + +- AbstractApplicationContext#obtainFreshBeanFactory + - AbstractRefreshableApplicationContext#refreshBeanFactory + - AbstractXmlApplicationContext#loadBeanDefinitions + - ... + - XmlBeanDefinitionReader#doLoadBeanDefinitions + - ... + - BeanDefinitionParserDelegate#parseCustomElement + +### 核心流程 + +BeanDefinitionParserDelegate#parseCustomElement(org.w3c.dom.Element, BeanDefinition) + +- 获取 namespace +- 通过 namespace 解析 NamespaceHandler +- 构造 ParserContext +- 解析元素,获取 BeanDefinintion + +## 基于 Properties 文件装载外部化配置 + +注解驱动 + +- @org.springframework.context.annotation.PropertySource +- @org.springframework.context.annotation.PropertySources + +API 编程 + +- org.springframework.core.env.PropertySource +- org.springframework.core.env.PropertySources + +## 基于 YAML 文件装载外部化配置 + +API 编程 + +- org.springframework.beans.factory.config.YamlProcessor + - org.springframework.beans.factory.config.YamlMapFactoryBean + - org.springframework.beans.factory.config.YamlPropertiesFactoryBean + +## 问题 + +**Spring 內建 XML Schema 常见有哪些**? + +| 命名空间 | 所属模块 | Schema 资源 URL | +| -------- | -------------- | ----------------------------------------------------------------- | +| beans | spring-beans | https://www.springframework.org/schema/beans/spring-beans.xsd | +| context | spring-context | https://www.springframework.org/schema/context/spring-context.xsd | +| aop | spring-aop | https://www.springframework.org/schema/aop/spring-aop.xsd | +| tx | spring-tx | https://www.springframework.org/schema/tx/spring-tx.xsd | +| util | spring-beans | beans https://www.springframework.org/schema/util/spring-util.xsd | +| tool | spring-beans | https://www.springframework.org/schema/tool/spring-tool.xsd | + +**Spring 配置元信息具体有哪些**? + +- Bean 配置元信息:通过媒介(如 XML、Proeprties 等),解析 BeanDefinition +- IoC 容器配置元信息:通过媒介(如 XML、Proeprties 等),控制 IoC 容器行为,比如注解驱动、AOP 等 +- 外部化配置:通过资源抽象(如 Proeprties、YAML 等),控制 PropertySource +- Spring Profile:通过外部化配置,提供条件分支流程 + +**Extensible XML authoring 的缺点**? + +- 高复杂度:开发人员需要熟悉 XML Schema,spring.handlers,spring.schemas 以及 Spring API +- 嵌套元素支持较弱:通常需要使用方法递归或者其嵌套解析的方式处理嵌套(子)元素 +- XML 处理性能较差:Spring XML 基于 DOM Level 3 API 实现,该 API 便于理解,然而性能较差 +- XML 框架移植性差:很难适配高性能和便利性的 XML 框架,如 JAXB + +## 参考资料 + +- [Spring 官方文档之 Core Technologies](https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/core.html#beans) +- [《小马哥讲 Spring 核心编程思想》](https://time.geekbang.org/course/intro/265) \ No newline at end of file diff --git "a/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/09.Spring\345\272\224\347\224\250\344\270\212\344\270\213\346\226\207\347\224\237\345\221\275\345\221\250\346\234\237.md" "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/09.Spring\345\272\224\347\224\250\344\270\212\344\270\213\346\226\207\347\224\237\345\221\275\345\221\250\346\234\237.md" new file mode 100644 index 00000000..fea4b398 --- /dev/null +++ "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/09.Spring\345\272\224\347\224\250\344\270\212\344\270\213\346\226\207\347\224\237\345\221\275\345\221\250\346\234\237.md" @@ -0,0 +1,171 @@ +--- +title: Spring 应用上下文生命周期 +date: 2022-12-23 09:58:09 +order: 09 +categories: + - Java + - 框架 + - Spring + - Spring核心 +tags: + - Java + - 框架 + - Spring +permalink: /pages/ad472e/ +--- + +# Spring 应用上下文生命周期 + +## Spring 应用上下文启动准备阶段 + +AbstractApplicationContext#prepareRefresh() 方法 + +- 启动时间 - startupDate +- 状态标识 - closed(false)、active(true) +- 初始化 PropertySources - initPropertySources() +- 检验 Environment 中必须属性 +- 初始化事件监听器集合 +- 初始化早期 Spring 事件集合 + +## BeanFactory 创建阶段 + +AbstractApplicationContext#obtainFreshBeanFactory() 方法 + +- 刷新 Spring 应用上下文底层 BeanFactory - refreshBeanFactory() + - 销毁或关闭 BeanFactory,如果已存在的话 + - 创建 BeanFactory - createBeanFactory() + - 设置 BeanFactory Id + - 设置“是否允许 BeanDefinition 重复定义” - customizeBeanFactory(DefaultListableBeanFactory) + - 设置“是否允许循环引用(依赖)” - customizeBeanFactory(DefaultListableBeanFactory) + - 加载 BeanDefinition - loadBeanDefinitions(DefaultListableBeanFactory) 方法 + - 关联新建 BeanFactory 到 Spring 应用上下文 +- 返回 Spring 应用上下文底层 BeanFactory - getBeanFactory() + +## BeanFactory 准备阶段 + +AbstractApplicationContext#prepareBeanFactory(ConfigurableListableBeanFactory) 方法 + +- 关联 ClassLoader +- 设置 Bean 表达式处理器 +- 添加 PropertyEditorRegistrar 实现 - ResourceEditorRegistrar +- 添加 Aware 回调接口 BeanPostProcessor 实现 - ApplicationContextAwareProcessor +- 忽略 Aware 回调接口作为依赖注入接口 +- 注册 ResolvableDependency 对象 - BeanFactory、ResourceLoader、ApplicationEventPublisher 以及 ApplicationContext +- 注册 ApplicationListenerDetector 对象 +- 注册 LoadTimeWeaverAwareProcessor 对象 +- 注册单例对象 - Environment、Java System Properties 以及 OS 环境变量 + +## BeanFactory 后置处理阶段 + +- AbstractApplicationContext#postProcessBeanFactory(ConfigurableListableBeanFactory) 方法 + - 由子类覆盖该方法 +- AbstractApplicationContext#invokeBeanFactoryPostProcessors(ConfigurableListableBeanFactory 方法 + - 调用 BeanFactoryPostProcessor 或 BeanDefinitionRegistry 后置处理方法 + - 注册 LoadTimeWeaverAwareProcessor 对象 + +## BeanFactory 注册 BeanPostProcessor 阶段 + +AbstractApplicationContext#registerBeanPostProcessors(ConfigurableListableBeanFactory) 方法 + +- 注册 PriorityOrdered 类型的 BeanPostProcessor Beans +- 注册 Ordered 类型的 BeanPostProcessor Beans +- 注册普通 BeanPostProcessor Beans +- 注册 MergedBeanDefinitionPostProcessor Beans +- 注册 ApplicationListenerDetector 对象 + +## 初始化內建 Bean:MessageSource + +AbstractApplicationContext#initMessageSource() 方法 + +## 初始化內建 Bean:Spring 事件广播器 + +AbstractApplicationContext#initApplicationEventMulticaster() 方法 + +## Spring 应用上下文刷新阶段 + +AbstractApplicationContext#onRefresh() 方法 + +子类覆盖该方法 + +- org.springframework.web.context.support.AbstractRefreshableWebApplicationContext#onRefresh() +- org.springframework.web.context.support.GenericWebApplicationContext#onRefresh() +- org.springframework.boot.web.reactive.context.ReactiveWebServerApplicationContext#onRefresh() +- org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext#onRefresh() +- org.springframework.web.context.support.StaticWebApplicationContext#onRefresh() + +## Spring 事件监听器注册阶段 + +AbstractApplicationContext#registerListeners() 方法 + +- 添加当前应用上下文所关联的 ApplicationListener 对象(集合) +- 添加 BeanFactory 所注册 ApplicationListener Beans +- 广播早期 Spring 事件 + +## BeanFactory 初始化完成阶段 + +AbstractApplicationContext#finishBeanFactoryInitialization(ConfigurableListableBeanFactory) 方法 + +- BeanFactory 关联 ConversionService Bean,如果存在 +- 添加 StringValueResolver 对象 +- 依赖查找 LoadTimeWeaverAware Bean +- BeanFactory 临时 ClassLoader 置为 null +- BeanFactory 冻结配置 +- BeanFactory 初始化非延迟单例 Beans + +## Spring 应用上下刷新完成阶段 + +AbstractApplicationContext#finishRefresh() 方法 + +- 清除 ResourceLoader 缓存 - clearResourceCaches() @since 5.0 +- 初始化 LifecycleProcessor 对象 - initLifecycleProcessor() +- 调用 LifecycleProcessor#onRefresh() 方法 +- 发布 Spring 应用上下文已刷新事件 - ContextRefreshedEvent +- 向 MBeanServer 托管 Live Beans + +## Spring 应用上下文启动阶段 + +AbstractApplicationContext#start() 方法 + +- 启动 LifecycleProcessor + - 依赖查找 Lifecycle Beans + - 启动 Lifecycle Beans +- 发布 Spring 应用上下文已启动事件 - ContextStartedEvent + +## Spring 应用上下文停止阶段 + +AbstractApplicationContext#stop() 方法 + +- 停止 LifecycleProcessor + - 依赖查找 Lifecycle Beans + - 停止 Lifecycle Beans +- 发布 Spring 应用上下文已停止事件 - ContextStoppedEvent + +## Spring 应用上下文关闭阶段 + +AbstractApplicationContext#close() 方法 + +- 状态标识:active(false)、closed(true) +- Live Beans JMX 撤销托管 + - LiveBeansView.unregisterApplicationContext(ConfigurableApplicationContext) +- 发布 Spring 应用上下文已关闭事件 - ContextClosedEvent +- 关闭 LifecycleProcessor + - 依赖查找 Lifecycle Beans + - 停止 Lifecycle Beans +- 销毁 Spring Beans +- 关闭 BeanFactory +- 回调 onClose() +- 注册 Shutdown Hook 线程(如果曾注册) + +## 问题 + +**Spring 应用上下文生命周期有哪些阶段**? + +- 刷新阶段 - ConfigurableApplicationContext#refresh() +- 启动阶段 - ConfigurableApplicationContext#start() +- 停止阶段 - ConfigurableApplicationContext#stop() +- 关闭阶段 - ConfigurableApplicationContext#close() + +## 参考资料 + +- [Spring 官方文档之 Core Technologies](https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/core.html#beans) +- [《小马哥讲 Spring 核心编程思想》](https://time.geekbang.org/course/intro/265) \ No newline at end of file diff --git "a/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/10.SpringAop.md" "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/10.SpringAop.md" new file mode 100644 index 00000000..1a89468a --- /dev/null +++ "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/10.SpringAop.md" @@ -0,0 +1,399 @@ +--- +title: Spring AOP +date: 2020-02-26 23:47:47 +order: 10 +categories: + - Java + - 框架 + - Spring + - Spring核心 +tags: + - Java + - 框架 + - Spring + - AOP +permalink: /pages/53aedb/ +--- + +# Spring AOP + +## AOP 概念 + +### 什么是 AOP + +AOP(Aspect-Oriented Programming,即 **面向切面编程**)与 OOP( Object-Oriented Programming,面向对象编程) 相辅相成,提供了与 OOP 不同的抽象软件结构的视角。 + +在 OOP 中,我们以类(class)作为我们的基本单元,而 AOP 中的基本单元是 **Aspect(切面)** + +### 术语 + +#### Aspect(切面) + +`aspect` 由 `pointcount` 和 `advice` 组成, 它既包含了横切逻辑的定义, 也包括了连接点的定义. Spring AOP 就是负责实施切面的框架, 它将切面所定义的横切逻辑织入到切面所指定的连接点中. +AOP 的工作重心在于如何将增强织入目标对象的连接点上, 这里包含两个工作: + +1. 如何通过 pointcut 和 advice 定位到特定的 joinpoint 上 +2. 如何在 advice 中编写切面代码. + +**可以简单地认为, 使用 @Aspect 注解的类就是切面.** + +#### advice(增强) + +由 aspect 添加到特定的 join point(即满足 point cut 规则的 join point) 的一段代码. +许多 AOP 框架, 包括 Spring AOP, 会将 advice 模拟为一个拦截器(interceptor), 并且在 join point 上维护多个 advice, 进行层层拦截. +例如 HTTP 鉴权的实现, 我们可以为每个使用 RequestMapping 标注的方法织入 advice, 当 HTTP 请求到来时, 首先进入到 advice 代码中, 在这里我们可以分析这个 HTTP 请求是否有相应的权限, 如果有, 则执行 Controller, 如果没有, 则抛出异常. 这里的 advice 就扮演着鉴权拦截器的角色了. + +#### 连接点(join point) + +> a point during the execution of a program, such as the execution of a method or the handling of an exception. In Spring AOP, a join point always represents a method execution. + +程序运行中的一些时间点, 例如一个方法的执行, 或者是一个异常的处理. +`在 Spring AOP 中, join point 总是方法的执行点, 即只有方法连接点.` + +#### 切点(point cut) + +匹配 join point 的谓词(a predicate that matches join points). +Advice 是和特定的 point cut 关联的, 并且在 point cut 相匹配的 join point 中执行. +`在 Spring 中, 所有的方法都可以认为是 joinpoint, 但是我们并不希望在所有的方法上都添加 Advice, 而 pointcut 的作用就是提供一组规则(使用 AspectJ pointcut expression language 来描述) 来匹配joinpoint, 给满足规则的 joinpoint 添加 Advice.` + +#### 关于 join point 和 point cut 的区别 + +在 Spring AOP 中, 所有的方法执行都是 join point. 而 point cut 是一个描述信息, 它修饰的是 join point, 通过 point cut, 我们就可以确定哪些 join point 可以被织入 Advice. 因此 join point 和 point cut 本质上就是两个不同纬度上的东西. +`advice 是在 join point 上执行的, 而 point cut 规定了哪些 join point 可以执行哪些 advice` + +#### introduction + +为一个类型添加额外的方法或字段. Spring AOP 允许我们为 `目标对象` 引入新的接口(和对应的实现). 例如我们可以使用 introduction 来为一个 bean 实现 IsModified 接口, 并以此来简化 caching 的实现. + +#### 目标对象(Target) + +织入 advice 的目标对象. 目标对象也被称为 `advised object`. +`因为 Spring AOP 使用运行时代理的方式来实现 aspect, 因此 adviced object 总是一个代理对象(proxied object)` +`注意, adviced object 指的不是原来的类, 而是织入 advice 后所产生的代理类.` + +#### AOP proxy + +一个类被 AOP 织入 advice, 就会产生一个结果类, 它是融合了原类和增强逻辑的代理类. +在 Spring AOP 中, 一个 AOP 代理是一个 JDK 动态代理对象或 CGLIB 代理对象. + +#### 织入(Weaving) + +将 aspect 和其他对象连接起来, 并创建 adviced object 的过程. +根据不同的实现技术, AOP 织入有三种方式: + +- 编译器织入, 这要求有特殊的 Java 编译器. +- 类装载期织入, 这需要有特殊的类装载器. +- 动态代理织入, 在运行期为目标类添加增强(Advice)生成子类的方式. + Spring 采用动态代理织入, 而 AspectJ 采用编译器织入和类装载期织入. + +### advice 的类型 + +- before advice, 在 join point 前被执行的 advice. 虽然 before advice 是在 join point 前被执行, 但是它并不能够阻止 join point 的执行, 除非发生了异常(即我们在 before advice 代码中, 不能人为地决定是否继续执行 join point 中的代码) +- after return advice, 在一个 join point 正常返回后执行的 advice +- after throwing advice, 当一个 join point 抛出异常后执行的 advice +- after(final) advice, 无论一个 join point 是正常退出还是发生了异常, 都会被执行的 advice. +- around advice, 在 join point 前和 joint point 退出后都执行的 advice. 这个是最常用的 advice. + +### 关于 AOP Proxy + +Spring AOP 默认使用标准的 JDK 动态代理(dynamic proxy)技术来实现 AOP 代理, 通过它, 我们可以为任意的接口实现代理. +`如果需要为一个类实现代理, 那么可以使用 CGLIB 代理.` 当一个业务逻辑对象没有实现接口时, 那么 Spring AOP 就默认使用 CGLIB 来作为 AOP 代理了. 即如果我们需要为一个方法织入 advice, 但是这个方法不是一个接口所提供的方法, 则此时 Spring AOP 会使用 CGLIB 来实现动态代理. 鉴于此, Spring AOP 建议基于接口编程, 对接口进行 AOP 而不是类. + +### 彻底理解 aspect, join point, point cut, advice + +看完了上面的理论部分知识, 我相信还是会有不少朋友感觉到 AOP 的概念还是很模糊, 对 AOP 中的各种概念理解的还不是很透彻. 其实这很正常, 因为 AOP 中的概念是在是太多了, 我当时也是花了老大劲才梳理清楚的. +下面我以一个简单的例子来比喻一下 AOP 中 aspect, jointpoint, pointcut 与 advice 之间的关系. + +让我们来假设一下, 从前有一个叫爪哇的小县城, 在一个月黑风高的晚上, 这个县城中发生了命案. 作案的凶手十分狡猾, 现场没有留下什么有价值的线索. 不过万幸的是, 刚从隔壁回来的老王恰好在这时候无意中发现了凶手行凶的过程, 但是由于天色已晚, 加上凶手蒙着面, 老王并没有看清凶手的面目, 只知道凶手是个男性, 身高约七尺五寸. 爪哇县的县令根据老王的描述, 对守门的士兵下命令说: 凡是发现有身高七尺五寸的男性, 都要抓过来审问. 士兵当然不敢违背县令的命令, 只好把进出城的所有符合条件的人都抓了起来. + +来让我们看一下上面的一个小故事和 AOP 到底有什么对应关系. +首先我们知道, 在 Spring AOP 中 join point 指代的是所有方法的执行点, 而 point cut 是一个描述信息, 它修饰的是 join point, 通过 point cut, 我们就可以确定哪些 join point 可以被织入 Advice. 对应到我们在上面举的例子, 我们可以做一个简单的类比, join point 就相当于 **爪哇的小县城里的百姓**, point cut 就相当于 **老王所做的指控, 即凶手是个男性, 身高约七尺五寸**, 而 advice 则是施加在符合老王所描述的嫌疑人的动作: **抓过来审问**. +为什么可以这样类比呢? + +- join point --> 爪哇的小县城里的百姓: 因为根据定义, join point 是所有可能被织入 advice 的候选的点, 在 Spring AOP 中, 则可以认为所有方法执行点都是 join point. 而在我们上面的例子中, 命案发生在小县城中, 按理说在此县城中的所有人都有可能是嫌疑人. +- point cut --> 男性, 身高约七尺五寸: 我们知道, 所有的方法(joint point) 都可以织入 advice, 但是我们并不希望在所有方法上都织入 advice, 而 pointcut 的作用就是提供一组规则来匹配 joinpoint, 给满足规则的 joinpoint 添加 advice. 同理, 对于县令来说, 他再昏庸, 也知道不能把县城中的所有百姓都抓起来审问, 而是根据`凶手是个男性, 身高约七尺五寸`, 把符合条件的人抓起来. 在这里`凶手是个男性, 身高约七尺五寸` 就是一个修饰谓语, 它限定了凶手的范围, 满足此修饰规则的百姓都是嫌疑人, 都需要抓起来审问. +- advice --> 抓过来审问, advice 是一个动作, 即一段 Java 代码, 这段 Java 代码是作用于 point cut 所限定的那些 join point 上的. 同理, 对比到我们的例子中, `抓过来审问` 这个动作就是对作用于那些满足 `男性, 身高约七尺五寸` 的`爪哇的小县城里的百姓`. +- aspect: aspect 是 point cut 与 advice 的组合, 因此在这里我们就可以类比: **"根据老王的线索, 凡是发现有身高七尺五寸的男性, 都要抓过来审问"** 这一整个动作可以被认为是一个 aspect. + +或则我们也可以从语法的角度来简单类比一下. 我们在学英语时, 经常会接触什么 `定语`, `被动句` 之类的概念, 那么可以做一个不严谨的类比, 即 `joinpoint` 可以认为是一个 `宾语`, 而 `pointcut` 则可以类比为修饰 `joinpoint` 的定语, 那么整个 `aspect` 就可以描述为: `满足 pointcut 规则的 joinpoint 会被添加相应的 advice 操作.` + +## @AspectJ 支持 + +**`@AspectJ`** 是一种使用 Java 注解来实现 AOP 的编码风格。 + +@AspectJ 风格的 AOP 是 AspectJ Project 在 AspectJ 5 中引入的, 并且 Spring 也支持 @AspectJ 的 AOP 风格. + +### 使能 @AspectJ 支持 + +@AspectJ 可以以 XML 的方式或以注解的方式来使能, 并且不论以哪种方式使能@ASpectJ, 我们都必须保证 aspectjweaver.jar 在 classpath 中. + +#### 使用 Java Configuration 方式使能@AspectJ + +```java +@Configuration +@EnableAspectJAutoProxy +public class AppConfig { +} +``` + +#### 使用 XML 方式使能@AspectJ + +``` + +``` + +### 定义 aspect(切面) + +当使用注解 **@Aspect** 标注一个 Bean 后, 那么 Spring 框架会自动收集这些 Bean, 并添加到 Spring AOP 中, 例如: + +```java +@Component +@Aspect +public class MyTest { +} +``` + +`注意, 仅仅使用@Aspect 注解, 并不能将一个 Java 对象转换为 Bean, 因此我们还需要使用类似 @Component 之类的注解.` +`注意, 如果一个 类被@Aspect 标注, 则这个类就不能是其他 aspect 的 **advised object** 了, 因为使用 @Aspect 后, 这个类就会被排除在 auto-proxying 机制之外.` + +### 声明 pointcut + +一个 pointcut 的声明由两部分组成: + +- 一个方法签名, 包括方法名和相关参数 +- 一个 pointcut 表达式, 用来指定哪些方法执行是我们感兴趣的(即因此可以织入 advice). + +在@AspectJ 风格的 AOP 中, 我们使用一个方法来描述 pointcut, 即: + +```java +@Pointcut("execution(* com.xys.service.UserService.*(..))") // 切点表达式 +private void dataAccessOperation() {} // 切点前面 +``` + +`这个方法必须无返回值.` +`这个方法本身就是 pointcut signature, pointcut 表达式使用@Pointcut 注解指定.` +上面我们简单地定义了一个 pointcut, 这个 pointcut 所描述的是: 匹配所有在包 **com.xys.service.UserService** 下的所有方法的执行. + +#### 切点标志符(designator) + +AspectJ5 的切点表达式由标志符(designator)和操作参数组成. 如 "execution(\* greetTo(..))" 的切点表达式, \*\*execution** 就是 标志符, 而圆括号里的 \*\*\***greetTo(..) 就是操作参数 + +##### execution + +匹配 join point 的执行, 例如 "execution(\* hello(..))" 表示匹配所有目标类中的 hello() 方法. 这个是最基本的 pointcut 标志符. + +##### within + +匹配特定包下的所有 join point, 例如 `within(com.xys.*)` 表示 com.xys 包中的所有连接点, 即包中的所有类的所有方法. 而`within(com.xys.service.*Service)` 表示在 com.xys.service 包中所有以 Service 结尾的类的所有的连接点. + +##### this 与 target + +this 的作用是匹配一个 bean, 这个 bean(Spring AOP proxy) 是一个给定类型的实例(instance of). 而 target 匹配的是一个目标对象(target object, 即需要织入 advice 的原始的类), 此对象是一个给定类型的实例(instance of). + +##### bean + +匹配 bean 名字为指定值的 bean 下的所有方法, 例如: + +``` +bean(*Service) // 匹配名字后缀为 Service 的 bean 下的所有方法 +bean(myService) // 匹配名字为 myService 的 bean 下的所有方法 +``` + +##### args + +匹配参数满足要求的的方法. +例如: + +```java +@Pointcut("within(com.xys.demo2.*)") +public void pointcut2() { +} + +@Before(value = "pointcut2() && args(name)") +public void doSomething(String name) { + logger.info("---page: {}---", name); +} +``` + +```java +@Service +public class NormalService { + private Logger logger = LoggerFactory.getLogger(getClass()); + + public void someMethod() { + logger.info("---NormalService: someMethod invoked---"); + } + + public String test(String name) { + logger.info("---NormalService: test invoked---"); + return "服务一切正常"; + } +} +``` + +当 NormalService.test 执行时, 则 advice `doSomething` 就会执行, test 方法的参数 name 就会传递到 `doSomething` 中. + +常用例子: + +```java +// 匹配只有一个参数 name 的方法 +@Before(value = "aspectMethod() && args(name)") +public void doSomething(String name) { +} + +// 匹配第一个参数为 name 的方法 +@Before(value = "aspectMethod() && args(name, ..)") +public void doSomething(String name) { +} + +// 匹配第二个参数为 name 的方法 +Before(value = "aspectMethod() && args(*, name, ..)") +public void doSomething(String name) { +} +``` + +##### @annotation + +匹配由指定注解所标注的方法, 例如: + +```java +@Pointcut("@annotation(com.xys.demo1.AuthChecker)") +public void pointcut() { +} +``` + +则匹配由注解 `AuthChecker` 所标注的方法. + +#### 常见的切点表达式 + +##### 匹配方法签名 + +``` +// 匹配指定包中的所有的方法 +execution(* com.xys.service.*(..)) + +// 匹配当前包中的指定类的所有方法 +execution(* UserService.*(..)) + +// 匹配指定包中的所有 public 方法 +execution(public * com.xys.service.*(..)) + +// 匹配指定包中的所有 public 方法, 并且返回值是 int 类型的方法 +execution(public int com.xys.service.*(..)) + +// 匹配指定包中的所有 public 方法, 并且第一个参数是 String, 返回值是 int 类型的方法 +execution(public int com.xys.service.*(String name, ..)) +``` + +##### 匹配类型签名 + +``` +// 匹配指定包中的所有的方法, 但不包括子包 +within(com.xys.service.*) + +// 匹配指定包中的所有的方法, 包括子包 +within(com.xys.service..*) + +// 匹配当前包中的指定类中的方法 +within(UserService) + + +// 匹配一个接口的所有实现类中的实现的方法 +within(UserDao+) +``` + +##### 匹配 Bean 名字 + +``` +// 匹配以指定名字结尾的 Bean 中的所有方法 +bean(*Service) +``` + +##### 切点表达式组合 + +``` +// 匹配以 Service 或 ServiceImpl 结尾的 bean +bean(*Service || *ServiceImpl) + +// 匹配名字以 Service 结尾, 并且在包 com.xys.service 中的 bean +bean(*Service) && within(com.xys.service.*) +``` + +### 声明 advice + +advice 是和一个 pointcut 表达式关联在一起的, 并且会在匹配的 join point 的方法执行的前/后/周围 运行. `pointcut 表达式可以是简单的一个 pointcut 名字的引用, 或者是完整的 pointcut 表达式`. +下面我们以几个简单的 advice 为例子, 来看一下一个 advice 是如何声明的. + +#### Before advice + +```java +/** + * @author xiongyongshun + * @version 1.0 + * @created 16/9/9 13:13 + */ +@Component +@Aspect +public class BeforeAspectTest { + // 定义一个 Pointcut, 使用 切点表达式函数 来描述对哪些 Join point 使用 advise. + @Pointcut("execution(* com.xys.service.UserService.*(..))") + public void dataAccessOperation() { + } +} +``` + +```java +@Component +@Aspect +public class AdviseDefine { + // 定义 advise + @Before("com.xys.aspect.PointcutDefine.dataAccessOperation()") + public void doBeforeAccessCheck(JoinPoint joinPoint) { + System.out.println("*****Before advise, method: " + joinPoint.getSignature().toShortString() + " *****"); + } +} +``` + +这里, **@Before** 引用了一个 pointcut, 即 "com.xys.aspect.PointcutDefine.dataAccessOperation()" 是一个 pointcut 的名字. +如果我们在 advice 在内置 pointcut, 则可以: + +```java +@Component +@Aspect +public class AdviseDefine { + // 将 pointcut 和 advice 同时定义 + @Before("within(com.xys.service..*)") + public void doAccessCheck(JoinPoint joinPoint) { + System.out.println("*****doAccessCheck, Before advise, method: " + joinPoint.getSignature().toShortString() + " *****"); + } +} +``` + +#### around advice + +around advice 比较特别, 它可以在一个方法的之前之前和之后添加不同的操作, 并且甚至可以决定何时, 如何, 是否调用匹配到的方法. + +```java +@Component +@Aspect +public class AdviseDefine { + // 定义 advise + @Around("com.xys.aspect.PointcutDefine.dataAccessOperation()") + public Object doAroundAccessCheck(ProceedingJoinPoint pjp) throws Throwable { + StopWatch stopWatch = new StopWatch(); + stopWatch.start(); + // 开始 + Object retVal = pjp.proceed(); + stopWatch.stop(); + // 结束 + System.out.println("invoke method: " + pjp.getSignature().getName() + ", elapsed time: " + stopWatch.getTotalTimeMillis()); + return retVal; + } +} +``` + +around advice 和前面的 before advice 差不多, 只是我们把注解 **@Before** 改为了 **@Around** 了. + +## 参考资料 + +- [《 Spring 实战(第 4 版)》](https://item.jd.com/11899370.html) \ No newline at end of file diff --git "a/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/20.Spring\350\265\204\346\272\220\347\256\241\347\220\206.md" "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/20.Spring\350\265\204\346\272\220\347\256\241\347\220\206.md" new file mode 100644 index 00000000..b285f663 --- /dev/null +++ "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/20.Spring\350\265\204\346\272\220\347\256\241\347\220\206.md" @@ -0,0 +1,261 @@ +--- +title: Spring 资源管理 +date: 2019-09-04 19:46:41 +order: 20 +categories: + - Java + - 框架 + - Spring + - Spring核心 +tags: + - Java + - 框架 + - Spring + - Resource +permalink: /pages/a1549f/ +--- + +# Spring 资源管理 + +> Version 6.0.3 + +## Resource 接口 + +相对标准 URL 访问机制,Spring 的 `org.springframework.core.io.Resource` 接口抽象了对底层资源的访问接口,提供了一套更好的访问方式。 + +```java +public interface Resource extends InputStreamSource { + + boolean exists(); + + boolean isReadable(); + + boolean isOpen(); + + boolean isFile(); + + URL getURL() throws IOException; + + URI getURI() throws IOException; + + File getFile() throws IOException; + + ReadableByteChannel readableChannel() throws IOException; + + long contentLength() throws IOException; + + long lastModified() throws IOException; + + Resource createRelative(String relativePath) throws IOException; + + String getFilename(); + + String getDescription(); +} +``` + +正如 `Resource` 接口的定义所示,它扩展了 `InputStreamSource` 接口。`Resource` 最核心的方法如下: + +- `getInputStream()` - 定位并且打开当前资源,返回当前资源的 `InputStream`。每次调用都会返回一个新的 `InputStream`。调用者需要负责关闭流。 +- `exists()` - 判断当前资源是否真的存在。 +- `isOpen()` - 判断当前资源是否是一个已打开的 `InputStream`。如果为 true,则 `InputStream` 不能被多次读取,必须只读取一次然后关闭以避免资源泄漏。对所有常用资源实现返回 false,`InputStreamResource` 除外。 +- `getDescription()` - 返回当前资源的描述,当处理资源出错时,资源的描述会用于错误信息的输出。一般来说,资源的描述是一个完全限定的文件名称,或者是当前资源的真实 URL。 + +常见 Spring 资源接口: + +| 类型 | 接口 | +| ---------- | ----------------------------------------------------- | +| 输入流 | `org.springframework.core.io.InputStreamSource` | +| 只读资源 | `org.springframework.core.io.Resource` | +| 可写资源 | `org.springframework.core.io.WritableResource` | +| 编码资源 | `org.springframework.core.io.support.EncodedResource` | +| 上下文资源 | `org.springframework.core.io.ContextResource` | + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/20221223155859.png) + +## 内置的 Resource 实现 + +Spring 包括几个内置的 Resource 实现: + +| 资源来源 | 前缀 | 说明 | +| ---------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [`UrlResource`](https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#resources-implementations-urlresource) | `file:`、`https:`、`ftp:` 等 | `UrlResource` 封装了一个 `java.net.URL` 对象,**用于访问可通过 URL 访问的任何对象**,例如文件、HTTPS 目标、FTP 目标等。所有 URL 都可以通过标准化的字符串形式表示,因此可以使用适当的标准化前缀来指示一种 URL 类型与另一种 URL 类型的区别。 这包括:`file`:用于访问文件系统路径;`https`:用于通过 HTTPS 协议访问资源;`ftp`:用于通过 FTP 访问资源等等。 | +| [`ClassPathResource`](https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#resources-implementations-classpathresource) | `classpath:` | `ClassPathResource` **从类路径上加载资源**。它使用线程上下文加载器、给定的类加载器或指定的 class 类型中的任意一个来加载资源。 | +| [`FileSystemResource`](https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#resources-implementations-filesystemresource) | `file:` | `FileSystemResource` **是 `java.io.File` 的资源实现**。它还支持 `java.nio.file.Path` ,应用 Spring 的标准对字符串路径进行转换。`FileSystemResource` 支持解析为文件和 URL。 | +| [`PathResource`](https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#resources-implementations-pathresource) | 无 | `PathResource` 是 `java.nio.file.Path` 的资源实现。 | +| [`ServletContextResource`](https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#resources-implementations-servletcontextresource) | 无 | `ServletContextResource` **是 `ServletContext` 的资源实现**。它表示相应 Web 应用程序根目录中的相对路径。 | +| [`InputStreamResource`](https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#resources-implementations-inputstreamresource) | 无 | `InputStreamResource` **是指定 `InputStream` 的资源实现**。注意:如果该 `InputStream` 已被打开,则不可以多次读取该流。 | +| [`ByteArrayResource`](https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#resources-implementations-bytearrayresource) | 无 | `ByteArrayResource` 是指定的二进制数组的资源实现。它会为给定的字节数组创建一个 `ByteArrayInputStream`。 | + +## ResourceLoader 接口 + +`ResourceLoader` 接口用于加载 `Resource` 对象。其定义如下: + +```java +public interface ResourceLoader { + + Resource getResource(String location); + + ClassLoader getClassLoader(); +} +``` + +Spring 中主要的 ResourceLoader 实现: + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/20221223164745.png) + +Spring 中,所有的 `ApplicationContext` 都实现了 `ResourceLoader` 接口。因此,所有 `ApplicationContext` 都可以通过 `getResource()` 方法获取 `Resource` 实例。 + +【示例】 + +```java +// 如果没有指定资源前缀,Spring 会尝试返回合适的资源 +Resource template = ctx.getResource("some/resource/path/myTemplate.txt"); +// 如果指定 classpath: 前缀,Spring 会强制使用 ClassPathResource +Resource template = ctx.getResource("classpath:some/resource/path/myTemplate.txt"); +// 如果指定 file:、http 等 URL 前缀,Spring 会强制使用 UrlResource +Resource template = ctx.getResource("file:///some/resource/path/myTemplate.txt"); +Resource template = ctx.getResource("http://myhost.com/resource/path/myTemplate.txt"); +``` + +下表列举了 Spring 根据各种位置路径加载资源的策略: + +| 前缀 | 样例 | 说明 | +| ------------ | -------------------------------- | :----------------------------------- | +| `classpath:` | `classpath:com/myapp/config.xml` | 从类路径加载 | +| `file:` | `file:///data/config.xml` | 以 URL 形式从文件系统加载 | +| `http:` | `http://myserver/logo.png` | 以 URL 形式加载 | +| 无 | `/data/config.xml` | 由底层的 ApplicationContext 实现决定 | + +## ResourcePatternResolver 接口 + +`ResourcePatternResolver` 接口是 `ResourceLoader` 接口的扩展,它的作用是定义策略,根据位置模式解析 `Resource` 对象。 + +```java +public interface ResourcePatternResolver extends ResourceLoader { + + String CLASSPATH_ALL_URL_PREFIX = "classpath*:"; + + Resource[] getResources(String locationPattern) throws IOException; +} +``` + +`PathMatchingResourcePatternResolver` 是一个独立的实现,可以在 `ApplicationContext` 之外使用,也可以被 `ResourceArrayPropertyEditor` 用于填充 `Resource[]` bean 属性。`PathMatchingResourcePatternResolver` 能够将指定的资源位置路径解析为一个或多个匹配的 `Resource` 对象。 + +> 注意:任何标准 `ApplicationContext` 中的默认 `ResourceLoader` 实际上是 `PathMatchingResourcePatternResolver` 的一个实例,它实现了 `ResourcePatternResolver` 接口。 + +## ResourceLoaderAware 接口 + +`ResourceLoaderAware` 接口是一个特殊的回调接口,用来标记提供 `ResourceLoader` 引用的对象。`ResourceLoaderAware` 接口定义如下: + +```java +public interface ResourceLoaderAware { + void setResourceLoader(ResourceLoader resourceLoader); +} +``` + +当一个类实现 `ResourceLoaderAware` 并部署到应用程序上下文中(作为 Spring 管理的 bean)时,它会被应用程序上下文识别为 `ResourceLoaderAware`,然后,应用程序上下文会调用 `setResourceLoader(ResourceLoader)`,将自身作为参数提供(请记住,Spring 中的所有应用程序上下文都实现 `ResourceLoader` 接口)。 + +由于 `ApplicationContext` 是一个 `ResourceLoader`,该 bean 还可以实现 `ApplicationContextAware` 接口并直接使用提供的应用程序上下文来加载资源。 但是,一般来说,如果您只需要这些,最好使用专门的 `ResourceLoader` 接口。 该代码将仅耦合到资源加载接口(可以被视为实用程序接口),而不耦合到整个 Spring `ApplicationContext` 接口。 + +在应用程序中,还可以使用 `ResourceLoader` 的自动装配作为实现 `ResourceLoaderAware` 接口的替代方法。传统的构造函数和 `byType` 自动装配模式能够分别为构造函数参数或 setter 方法参数提供 `ResourceLoader`。 为了获得更大的灵活性(包括自动装配字段和多参数方法的能力),请考虑使用基于注解的自动装配功能。 在这种情况下,`ResourceLoader` 会自动连接到需要 `ResourceLoader` 类型的字段、构造函数参数或方法参数中,只要相关字段、构造函数或方法带有 `@Autowired` 注解即可。 + +## 资源依赖 + +如果 bean 本身要通过某种动态过程来确定和提供资源路径,那么 bean 可以使用 `ResourceLoader` 或 `ResourcePatternResolver` 接口来加载资源。 例如,考虑加载某种模板,其中所需的特定资源取决于用户的角色。 如果资源是静态的,完全消除 `ResourceLoader` 接口(或 `ResourcePatternResolver` 接口)的使用,让 bean 公开它需要的 `Resource` 属性,并期望将它们注入其中是有意义的。 + +使注入这些属性变得简单的原因是所有应用程序上下文都注册并使用一个特殊的 JavaBeans `PropertyEditor`,它可以将 `String` 路径转换为 `Resource` 对象。 例如,下面的 MyBean 类有一个 `Resource` 类型的模板属性。 + +【示例】 + +```xml + + + +``` + +请注意,配置中引用的模板资源路径没有前缀,因为应用程序上下文本身将用作 `ResourceLoader`,资源本身将根据需要通过 `ClassPathResource`,`FileSystemResource` 或 ServletContextResource 加载,具体取决于上下文的确切类型。 + +如果需要强制使用特定的资源类型,则可以使用前缀。 以下两个示例显示如何强制使用 `ClassPathResource` 和 `UrlResource`(后者用于访问文件系统文件)。 + +```xml + + +``` + +可以通过 `@Value` 注解加载资源文件 `myTemplate.txt`,示例如下: + +```java +@Component +public class MyBean { + + private final Resource template; + + public MyBean(@Value("${template.path}") Resource template) { + this.template = template; + } + + // ... +} +``` + +Spring 的 `PropertyEditor` 会根据资源文件的路径字符串,加载 `Resource` 对象,并将其注入到 MyBean 的构造方法。 + +如果想要加载多个资源文件,可以使用 `classpath*:` 前缀,例如:`classpath*:/config/templates/*.txt`。 + +```java +@Component +public class MyBean { + + private final Resource[] templates; + + public MyBean(@Value("${templates.path}") Resource[] templates) { + this.templates = templates; + } + + // ... +} +``` + +## 应用上下文和资源路径 + +### 构造应用上下文 + +应用上下文构造函数(针对特定的应用上下文类型)通常将字符串或字符串数组作为资源的位置路径,例如构成上下文定义的 XML 文件。 + +【示例】 + +```java +ApplicationContext ctx = new ClassPathXmlApplicationContext("conf/appContext.xml"); +ApplicationContext ctx = new FileSystemXmlApplicationContext("conf/appContext.xml"); +ApplicationContext ctx = new FileSystemXmlApplicationContext("classpath:conf/appContext.xml"); +ApplicationContext ctx = new ClassPathXmlApplicationContext( + new String[] {"services.xml", "daos.xml"}, MessengerService.class); +``` + +### 使用通配符构造应用上下文 + +ApplicationContext 构造器的中的资源路径可以是单一的路径(即一对一地映射到目标资源);也可以是通配符形式——可包含 classpath\*:也可以是前缀或 ant 风格的正则表达式(使用 spring 的 PathMatcher 来匹配)。 + +示例: + +```java +ApplicationContext ctx = new ClassPathXmlApplicationContext("classpath*:conf/appContext.xml"); +``` + +使用 `classpath*` 表示类路径下所有匹配文件名称的资源都会被获取(本质上就是调用了 ClassLoader.getResources(…) 方法),接着将获取到的资源组装成最终的应用上下文。 + +在位置路径的其余部分,`classpath*:` 前缀可以与 PathMatcher 结合使用,如:`classpath*:META-INF/*-beans.xml`。 + +## 问题 + +Spring 配置资源中有哪些常见类型? + +- XML 资源 +- Properties 资源 +- YAML 资源 + +## 参考资料 + +- [Spring 官方文档之 Core Technologies](https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/core.html#beans) +- [《小马哥讲 Spring 核心编程思想》](https://time.geekbang.org/course/intro/265) \ No newline at end of file diff --git "a/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/21.Spring\346\240\241\351\252\214.md" "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/21.Spring\346\240\241\351\252\214.md" new file mode 100644 index 00000000..d99a6fcc --- /dev/null +++ "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/21.Spring\346\240\241\351\252\214.md" @@ -0,0 +1,637 @@ +--- +title: Spring 校验 +date: 2022-12-22 17:42:28 +order: 21 +categories: + - Java + - 框架 + - Spring + - Spring核心 +tags: + - Java + - 框架 + - Spring +permalink: /pages/fe6aad/ +--- + +# Spring 校验 + +Java API 规范(`JSR303`)定义了`Bean`校验的标准`validation-api`,但没有提供实现。`hibernate validation`是对这个规范的实现,并增加了校验注解如`@Email`、`@Length`等。`Spring Validation`是对`hibernate validation`的二次封装,用于支持`spring mvc`参数自动校验。 + +## 快速入门 + +### 引入依赖 + +如果 spring-boot 版本小于 2.3.x,spring-boot-starter-web 会自动传入 hibernate-validator 依赖。如果 spring-boot 版本大于 2.3.x,则需要手动引入依赖: + +```xml + + org.hibernate.validator + hibernate-validator-parent + 6.2.5.Final + +``` + +对于 web 服务来说,为防止非法参数对业务造成影响,在 Controller 层一定要做参数校验的!大部分情况下,请求参数分为如下两种形式: + +- POST、PUT 请求,使用 requestBody 传递参数; +- GET 请求,使用 requestParam/PathVariable 传递参数。 + +实际上,不管是 requestBody 参数校验还是方法级别的校验,最终都是调用 Hibernate Validator 执行校验,Spring Validation 只是做了一层封装。 + +### 校验示例 + +(1)在实体上标记校验注解 + +```kotlin +@Data +@NoArgsConstructor +@AllArgsConstructor +public class User implements Serializable { + + @NotNull + private Long id; + + @NotBlank + @Size(min = 2, max = 10) + private String name; + + @Min(value = 1) + @Max(value = 100) + private Integer age; + +} +``` + +(2)在方法参数上声明校验注解 + +```less +@Slf4j +@Validated +@RestController +@RequestMapping("validate1") +public class ValidatorController { + + /** + * {@link RequestBody} 参数校验 + */ + @PostMapping(value = "save") + public DataResult save(@Valid @RequestBody User entity) { + log.info("保存一条记录:{}", JSONUtil.toJsonStr(entity)); + return DataResult.ok(true); + } + + /** + * {@link RequestParam} 参数校验 + */ + @GetMapping(value = "queryByName") + public DataResult queryByName( + @RequestParam("username") + @NotBlank + @Size(min = 2, max = 10) + String name + ) { + User user = new User(1L, name, 18); + return DataResult.ok(user); + } + + /** + * {@link PathVariable} 参数校验 + */ + @GetMapping(value = "detail/{id}") + public DataResult detail(@PathVariable("id") @Min(1L) Long id) { + User user = new User(id, "李四", 18); + return DataResult.ok(user); + } + +} +``` + +(3)如果请求参数不满足校验规则,则会抛出 `ConstraintViolationException` 或 `MethodArgumentNotValidException` 异常。 + +### 统一异常处理 + +在实际项目开发中,通常会用统一异常处理来返回一个更友好的提示。 + +```java +@Slf4j +@ControllerAdvice +public class GlobalExceptionHandler { + + /** + * 处理所有不可知的异常 + */ + @ResponseBody + @ResponseStatus(HttpStatus.OK) + @ExceptionHandler(Throwable.class) + public Result handleException(Throwable e) { + log.error("未知异常", e); + return new Result(ResultStatus.HTTP_SERVER_ERROR.getCode(), e.getMessage()); + } + + /** + * 统一处理请求参数校验异常(普通传参) + * + * @param e ConstraintViolationException + * @return {@link DataResult} + */ + @ResponseBody + @ResponseStatus(HttpStatus.BAD_REQUEST) + @ExceptionHandler({ ConstraintViolationException.class }) + public Result handleConstraintViolationException(final ConstraintViolationException e) { + log.error("ConstraintViolationException", e); + List errors = new ArrayList<>(); + for (ConstraintViolation violation : e.getConstraintViolations()) { + Path path = violation.getPropertyPath(); + List pathArr = StrUtil.split(path.toString(), ','); + errors.add(pathArr.get(0) + " " + violation.getMessage()); + } + return new Result(ResultStatus.REQUEST_ERROR.getCode(), CollectionUtil.join(errors, ",")); + } + + /** + * 处理参数校验异常 + * + * @param e MethodArgumentNotValidException + * @return {@link DataResult} + */ + @ResponseBody + @ResponseStatus(HttpStatus.BAD_REQUEST) + @ExceptionHandler({ MethodArgumentNotValidException.class }) + private Result handleMethodArgumentNotValidException(final MethodArgumentNotValidException e) { + log.error("MethodArgumentNotValidException", e); + List errors = new ArrayList<>(); + for (ObjectError error : e.getBindingResult().getAllErrors()) { + errors.add(((FieldError) error).getField() + " " + error.getDefaultMessage()); + } + return new Result(ResultStatus.REQUEST_ERROR.getCode(), CollectionUtil.join(errors, ",")); + } + +} +``` + +## 进阶使用 + +### 分组校验 + +在实际项目中,可能多个方法需要使用同一个 DTO 类来接收参数,而不同方法的校验规则很可能是不一样的。这个时候,简单地在 DTO 类的字段上加约束注解无法解决这个问题。因此,spring-validation 支持了分组校验的功能,专门用来解决这类问题。 + +(1)定义分组 + +```java +@Target({ ElementType.FIELD, ElementType.PARAMETER }) +@Retention(RetentionPolicy.RUNTIME) +public @interface AddCheck { } + +@Target({ ElementType.FIELD, ElementType.PARAMETER }) +@Retention(RetentionPolicy.RUNTIME) +public @interface EditCheck { } +``` + +(2)在实体上标记校验注解 + +```less +@Data +public class User2 { + + @NotNull(groups = EditCheck.class) + private Long id; + + @NotNull(groups = { AddCheck.class, EditCheck.class }) + @Size(min = 2, max = 10, groups = { AddCheck.class, EditCheck.class }) + private String name; + + @IsMobile(message = "不是有效手机号", groups = { AddCheck.class, EditCheck.class }) + private String mobile; + +} +``` + +(3)在方法上根据不同场景进行校验分组 + +```less +@Slf4j +@Validated +@RestController +@RequestMapping("validate2") +public class ValidatorController2 { + + /** + * {@link RequestBody} 参数校验 + */ + @PostMapping(value = "add") + public DataResult add(@Validated(AddCheck.class) @RequestBody User2 entity) { + log.info("添加一条记录:{}", JSONUtil.toJsonStr(entity)); + return DataResult.ok(true); + } + + /** + * {@link RequestBody} 参数校验 + */ + @PostMapping(value = "edit") + public DataResult edit(@Validated(EditCheck.class) @RequestBody User2 entity) { + log.info("编辑一条记录:{}", JSONUtil.toJsonStr(entity)); + return DataResult.ok(true); + } + +} +``` + +### 嵌套校验 + +前面的示例中,DTO 类里面的字段都是基本数据类型和 String 类型。但是实际场景中,有可能某个字段也是一个对象,这种情况先,可以使用嵌套校验。 +post +比如,上面保存 User 信息的时候同时还带有 Job 信息。需要注意的是,此时 DTO 类的对应字段必须标记@Valid 注解。 + +```less +@Data +public class UserDTO { + + @Min(value = 10000000000000000L, groups = Update.class) + private Long userId; + + @NotNull(groups = {Save.class, Update.class}) + @Length(min = 2, max = 10, groups = {Save.class, Update.class}) + private String userName; + + @NotNull(groups = {Save.class, Update.class}) + @Length(min = 6, max = 20, groups = {Save.class, Update.class}) + private String account; + + @NotNull(groups = {Save.class, Update.class}) + @Length(min = 6, max = 20, groups = {Save.class, Update.class}) + private String password; + + @NotNull(groups = {Save.class, Update.class}) + @Valid + private Job job; + + @Data + public static class Job { + + @Min(value = 1, groups = Update.class) + private Long jobId; + + @NotNull(groups = {Save.class, Update.class}) + @Length(min = 2, max = 10, groups = {Save.class, Update.class}) + private String jobName; + + @NotNull(groups = {Save.class, Update.class}) + @Length(min = 2, max = 10, groups = {Save.class, Update.class}) + private String position; + } + + /** + * 保存的时候校验分组 + */ + public interface Save { + } + + /** + * 更新的时候校验分组 + */ + public interface Update { + } +} +复制代码 +``` + +嵌套校验可以结合分组校验一起使用。还有就是嵌套集合校验会对集合里面的每一项都进行校验,例如`List`字段会对这个 list 里面的每一个 Job 对象都进行校验 + +### 自定义校验注解 + +(1)自定义校验注解 `@IsMobile` + +```less +@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE }) +@Retention(RUNTIME) +@Constraint(validatedBy = MobileValidator.class) +public @interface IsMobile { + + String message(); + + Class[] groups() default {}; + + Class[] payload() default {}; + +} +``` + +(2)实现 `ConstraintValidator` 接口,编写 `@IsMobile` 校验注解的解析器 + +```java +import cn.hutool.core.util.StrUtil; +import io.github.dunwu.spring.core.validation.annotation.IsMobile; +import io.github.dunwu.tool.util.ValidatorUtil; + +import javax.validation.ConstraintValidator; +import javax.validation.ConstraintValidatorContext; + +public class MobileValidator implements ConstraintValidator { + + @Override + public void initialize(IsMobile isMobile) { } + + @Override + public boolean isValid(String s, ConstraintValidatorContext constraintValidatorContext) { + if (StrUtil.isBlank(s)) { + return false; + } else { + return ValidatorUtil.isMobile(s); + } + } + +} +``` + +### 自定义校验 + +可以通过实现 `org.springframework.validation.Validator` 接口来自定义校验。 + +有以下要点 + +- 实现 `supports` 方法 +- 实现 `validate` 方法 + - 通过 `Errors` 对象收集错误 + - `ObjectError`:对象(Bean)错误: + - `FieldError`:对象(Bean)属性(Property)错误 + - 通过 `ObjectError` 和 `FieldError` 关联 `MessageSource` 实现获取最终的错误文案 + +```less +package io.github.dunwu.spring.core.validation; + +import io.github.dunwu.spring.core.validation.annotation.Valid; +import io.github.dunwu.spring.core.validation.config.CustomValidatorConfig; +import io.github.dunwu.spring.core.validation.entity.Person; +import org.springframework.stereotype.Component; +import org.springframework.validation.Errors; +import org.springframework.validation.ValidationUtils; +import org.springframework.validation.Validator; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +@Component +public class CustomValidator implements Validator { + + private final CustomValidatorConfig validatorConfig; + + public CustomValidator(CustomValidatorConfig validatorConfig) { + this.validatorConfig = validatorConfig; + } + + /** + * 本校验器只针对 Person 对象进行校验 + */ + @Override + public boolean supports(Class clazz) { + return Person.class.equals(clazz); + } + + @Override + public void validate(Object target, Errors errors) { + ValidationUtils.rejectIfEmpty(errors, "name", "name.empty"); + + List fields = getFields(target.getClass()); + for (Field field : fields) { + Annotation[] annotations = field.getAnnotations(); + for (Annotation annotation : annotations) { + if (annotation.annotationType().getAnnotation(Valid.class) != null) { + try { + ValidatorRule validatorRule = validatorConfig.findRule(annotation); + if (validatorRule != null) { + validatorRule.valid(annotation, target, field, errors); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + } + } + } + + private List getFields(Class clazz) { + // 声明Field数组 + List fields = new ArrayList<>(); + // 如果class类型不为空 + while (clazz != null) { + // 添加属性到属性数组 + Collections.addAll(fields, clazz.getDeclaredFields()); + clazz = clazz.getSuperclass(); + } + return fields; + } + +} +``` + +### 快速失败(Fail Fast) + +Spring Validation 默认会校验完所有字段,然后才抛出异常。可以通过一些简单的配置,开启 Fali Fast 模式,一旦校验失败就立即返回。 + +```scss +@Bean +public Validator validator() { + ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class) + .configure() + // 快速失败模式 + .failFast(true) + .buildValidatorFactory(); + return validatorFactory.getValidator(); +} +``` + +## Spring 校验原理 + +### Spring 校验使用场景 + +- Spring 常规校验(Validator) +- Spring 数据绑定(DataBinder) +- Spring Web 参数绑定(WebDataBinder) +- Spring WebMVC/WebFlux 处理方法参数校验 + +### Validator 接口设计 + +- 接口职责 + - Spring 内部校验器接口,通过编程的方式校验目标对象 +- 核心方法 + - `supports(Class)`:校验目标类能否校验 + - `validate(Object,Errors)`:校验目标对象,并将校验失败的内容输出至 Errors 对象 +- 配套组件 + - 错误收集器:`org.springframework.validation.Errors` + - Validator 工具类:`org.springframework.validation.ValidationUtils` + +### Errors 接口设计 + +- 接口职责 + - 数据绑定和校验错误收集接口,与 Java Bean 和其属性有强关联性 +- 核心方法 + - `reject` 方法(重载):收集错误文案 + - `rejectValue` 方法(重载):收集对象字段中的错误文案 +- 配套组件 + - Java Bean 错误描述:`org.springframework.validation.ObjectError` + - Java Bean 属性错误描述:`org.springframework.validation.FieldError` + +### Errors 文案来源 + +Errors 文案生成步骤 + +- 选择 Errors 实现(如:`org.springframework.validation.BeanPropertyBindingResult`) +- 调用 reject 或 rejectValue 方法 +- 获取 Errors 对象中 ObjectError 或 FieldError +- 将 ObjectError 或 FieldError 中的 code 和 args,关联 MessageSource 实现(如:`ResourceBundleMessageSource`) + +### spring web 校验原理 + +#### RequestBody 参数校验实现原理 + +在 spring-mvc 中,`RequestResponseBodyMethodProcessor` 是用于解析 `@RequestBody` 标注的参数以及处理`@ResponseBody` 标注方法的返回值的。其中,执行参数校验的逻辑肯定就在解析参数的方法 `resolveArgument()` 中: + +```java +@Override +public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, + NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception { + + parameter = parameter.nestedIfOptional(); + Object arg = readWithMessageConverters(webRequest, parameter, parameter.getNestedGenericParameterType()); + String name = Conventions.getVariableNameForParameter(parameter); + + if (binderFactory != null) { + WebDataBinder binder = binderFactory.createBinder(webRequest, arg, name); + if (arg != null) { + // 尝试进行参数校验 + validateIfApplicable(binder, parameter); + if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) { + // 如果存在校验错误,则抛出 MethodArgumentNotValidException + throw new MethodArgumentNotValidException(parameter, binder.getBindingResult()); + } + } + if (mavContainer != null) { + mavContainer.addAttribute(BindingResult.MODEL_KEY_PREFIX + name, binder.getBindingResult()); + } + } + + return adaptArgumentIfNecessary(arg, parameter); +} +``` + +可以看到,resolveArgument()调用了 validateIfApplicable()进行参数校验。 + +```java +protected void validateIfApplicable(WebDataBinder binder, MethodParameter parameter) { + // 获取参数注解,如 @RequestBody、@Valid、@Validated + Annotation[] annotations = parameter.getParameterAnnotations(); + for (Annotation ann : annotations) { + // 先尝试获取 @Validated 注解 + Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class); + // 如果标注了 @Validated,直接开始校验。 + // 如果没有,那么判断参数前是否有 Valid 开头的注解。 + if (validatedAnn != null || ann.annotationType().getSimpleName().startsWith("Valid")) { + Object hints = (validatedAnn != null ? validatedAnn.value() : AnnotationUtils.getValue(ann)); + Object[] validationHints = (hints instanceof Object[] ? (Object[]) hints : new Object[] {hints}); + // 执行校验 + binder.validate(validationHints); + break; + } + } +} +``` + +以上代码,就解释了 Spring 为什么能同时支持 `@Validated`、`@Valid` 两个注解。 + +接下来,看一下 WebDataBinder.validate() 的实现: + +```typescript +@Override +public void validate(Object target, Errors errors, Object... validationHints) { + if (this.targetValidator != null) { + processConstraintViolations( + // 此处调用 Hibernate Validator 执行真正的校验 + this.targetValidator.validate(target, asValidationGroups(validationHints)), errors); + } +} +``` + +通过上面代码,可以看出 Spring 校验实际上是基于 Hibernate Validator 的封装。 + +#### 方法级别的参数校验实现原理 + +Spring 支持根据方法去进行拦截、校验,原理就在于应用了 AOP 技术。具体来说,是通过 `MethodValidationPostProcessor` 动态注册 AOP 切面,然后使用 `MethodValidationInterceptor` 对切点方法织入增强。 + +```java +public class MethodValidationPostProcessor extends AbstractBeanFactoryAwareAdvisingPostProcessorimplements InitializingBean { + @Override + public void afterPropertiesSet() { + // 为所有 @Validated 标注的 Bean 创建切面 + Pointcut pointcut = new AnnotationMatchingPointcut(this.validatedAnnotationType, true); + // 创建 Advisor 进行增强 + this.advisor = new DefaultPointcutAdvisor(pointcut, createMethodValidationAdvice(this.validator)); + } + + // 创建 Advice,本质就是一个方法拦截器 + protected Advice createMethodValidationAdvice(@Nullable Validator validator) { + return (validator != null ? new MethodValidationInterceptor(validator) : new MethodValidationInterceptor()); + } +} +``` + +接着看一下 `MethodValidationInterceptor`: + +```scss +public class MethodValidationInterceptor implements MethodInterceptor { + @Override + public Object invoke(MethodInvocation invocation) throws Throwable { + // 无需增强的方法,直接跳过 + if (isFactoryBeanMetadataMethod(invocation.getMethod())) { + return invocation.proceed(); + } + // 获取分组信息 + Class[] groups = determineValidationGroups(invocation); + ExecutableValidator execVal = this.validator.forExecutables(); + Method methodToValidate = invocation.getMethod(); + Set> result; + try { + // 方法入参校验,最终还是委托给 Hibernate Validator 来校验 + result = execVal.validateParameters( + invocation.getThis(), methodToValidate, invocation.getArguments(), groups); + } + catch (IllegalArgumentException ex) { + ... + } + // 有异常直接抛出 + if (!result.isEmpty()) { + throw new ConstraintViolationException(result); + } + // 真正的方法调用 + Object returnValue = invocation.proceed(); + // 对返回值做校验,最终还是委托给Hibernate Validator来校验 + result = execVal.validateReturnValue(invocation.getThis(), methodToValidate, returnValue, groups); + // 有异常直接抛出 + if (!result.isEmpty()) { + throw new ConstraintViolationException(result); + } + return returnValue; + } +} +``` + +实际上,不管是 requestBody 参数校验还是方法级别的校验,最终都是调用 Hibernate Validator 执行校验,Spring Validation 只是做了一层封装。 + +## 问题 + +**Spring 有哪些校验核心组件**? + +- 检验器:`org.springframework.validation.Validator` +- 错误收集器:`org.springframework.validation.Errors` +- Java Bean 错误描述:`org.springframework.validation.ObjectError` +- Java Bean 属性错误描述:`org.springframework.validation.FieldError` +- Bean Validation 适配:`org.springframework.validation.beanvalidation.LocalValidatorFactoryBean` + +## 参考资料 + +- [Spring 官方文档之 Core Technologies](https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/core.html#beans) +- [《小马哥讲 Spring 核心编程思想》](https://time.geekbang.org/course/intro/265) +- https://juejin.cn/post/6856541106626363399 \ No newline at end of file diff --git "a/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/22.Spring\346\225\260\346\215\256\347\273\221\345\256\232.md" "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/22.Spring\346\225\260\346\215\256\347\273\221\345\256\232.md" new file mode 100644 index 00000000..3c5fe260 --- /dev/null +++ "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/22.Spring\346\225\260\346\215\256\347\273\221\345\256\232.md" @@ -0,0 +1,181 @@ +--- +title: Spring 数据绑定 +date: 2022-12-22 19:26:57 +order: 22 +categories: + - Java + - 框架 + - Spring + - Spring核心 +tags: + - Java + - 框架 + - Spring + - 数据绑定 +permalink: /pages/267b4c/ +--- + +# Spring 数据绑定 + +**Spring 数据绑定(Data Binding)的作用是将用户的输入动态绑定到 JavaBean**。换句话说,Spring 数据绑定机制是将属性值设置到目标对象中。 + +在 Spring 中,数据绑定功能主要由 `DataBinder` 类实现。此外,`BeanWrapper` 也具有类似的功能,但 `DataBinder` 额外支持字段验证、字段格式化和绑定结果分析。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/20230111150930.png) + +## 快速入门 + +定义一个用于测试的 JavaBean + +```java +public class TestBean { + + private int num; + + public int getNum() { + return num; + } + + public void setNum(int num) { + this.num = num; + } + + @Override + public String toString() { + return "TestBean{" + "num=" + num + '}'; + } + +} +``` + +数据绑定示例 + +```java +public class DataBindingDemo { + + public static void main(String[] args) { + + MutablePropertyValues mpv = new MutablePropertyValues(); + mpv.add("num", "10"); + + TestBean testBean = new TestBean(); + DataBinder db = new DataBinder(testBean); + + db.bind(mpv); + System.out.println(testBean); + } + +} +``` + +## Spring 数据绑定使用场景 + +- Spring `BeanDefinition` 到 Bean 实例创建 +- Spring 数据绑定(`DataBinder`) +- Spring Web 参数绑定(`WebDataBinder`) + +## DataBinder + +在 Spring 中,`DataBinder` 类是数据绑定功能的基类。`WebDataBinder` 是 `DataBinder` 的子类,主要用于 Spring Web 数据绑定,此外,还有一些 `WebDataBinder` 的扩展子类,其类族如下图所示: + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/20230111152225.png) + +DataBinder 核心属性: + +| 属性 | 说明 | +| ---------------------- | ------------------------------ | +| `target` | 关联目标 Bean | +| `objectName` | 目标 Bean 名称 | +| `bindingResult` | 属性绑定结果 | +| `typeConverter` | 类型转换器 | +| `conversionService` | 类型转换服务 | +| `messageCodesResolver` | 校验错误文案 Code 处理器 | +| `validators` | 关联的 Bean Validator 实例集合 | + +`DataBinder` 类的核心方法是 `bind(PropertyValues)`:将 PropertyValues Key-Value 内容映射到关联 Bean(target)中的属性上 + +- 假设 PropertyValues 中包含 name=dunwu 的键值对时, 同时 Bean 对象 User 中存在 name 属性, 当 bind 方法执行时, User 对象中的 name 属性值将被绑定为 dunwu + +## Spring 数据绑定元数据 + +DataBinder 元数据 - PropertyValues + +| 特征 | 说明 | +| ------------ | -------------------------------------------------------------------- | +| 数据来源 | BeanDefinition,主要来源 XML 资源配置 BeanDefinition | +| 数据结构 | 由一个或多个 PropertyValue 组成 | +| 成员结构 | PropertyValue 包含属性名称,以及属性值(包括原始值、类型转换后的值) | +| 常见实现 | MutablePropertyValues | +| Web 扩展实现 | ServletConfigPropertyValues、ServletRequestParameterPropertyValues | +| 相关生命周期 | InstantiationAwareBeanPostProcessor#postProcessProperties | + +## Spring 数据绑定控制参数 + +DataBinder 绑定特殊场景分析 + +- 当 PropertyValues 中包含名称 x 的 PropertyValue,目标对象 B 不存在 x 属性,当 bind 方法执 + 行时,会发生什么? +- 当 PropertyValues 中包含名称 x 的 PropertyValue,目标对象 B 中存在 x 属性,当 bind 方法执 + 行时,如何避免 B 属性 x 不被绑定? +- 当 PropertyValues 中包含名称 x.y 的 PropertyValue,目标对象 B 中存在 x 属性(嵌套 y 属性) + ,当 bind 方法执行时,会发生什么? + +### DataBinder 绑定控制参数 + +| 参数名称 | 说明 | +| ------------------- | ---------------------------------- | +| ignoreUnknownFields | 是否忽略未知字段,默认值:true | +| ignoreInvalidFields | 是否忽略非法字段,默认值:false | +| autoGrowNestedPaths | 是否自动增加嵌套路径,默认值:true | +| allowedFields | 绑定字段白名单 | +| disallowedFields | 绑定字段黑名单 | +| requiredFields | 必须绑定字段 | + +## BeanWrapper 的使用场景 + +- Spring 底层 JavaBeans 基础设施的中心化接口 +- 通常不会直接使用,间接用于 BeanFactory 和 DataBinder +- 提供标准 JavaBeans 分析和操作,能够单独或批量存储 Java Bean 的属性(properties) +- 支持嵌套属性路径(nested path) +- 实现类 org.springframework.beans.BeanWrapperImpl + +## Spring 底层 Java Beans 替换实现 + +JavaBeans 核心实现 - `java.beans.BeanInfo` + +- 属性(Property) + - `java.beans.PropertyEditor` +- 方法(Method) +- 事件(Event) +- 表达式(Expression) + +Spring 替代实现 - `org.springframework.beans.BeanWrapper` + +- 属性(Property) + - `java.beans.PropertyEditor` +- 嵌套属性路径(nested path) + +## DataBinder 数据校验 + +DataBinder 与 BeanWrapper + +- bind 方法生成 BeanPropertyBindingResult +- BeanPropertyBindingResult 关联 BeanWrapper + +## 问题 + +标准 JavaBeans 是如何操作属性的? + +| API | 说明 | +| ----------------------------- | ------------------------ | +| java.beans.Introspector | Java Beans 内省 API | +| java.beans.BeanInfo | Java Bean 元信息 API | +| java.beans.BeanDescriptor | Java Bean 信息描述符 | +| java.beans.PropertyDescriptor | Java Bean 属性描述符 | +| java.beans.MethodDescriptor | Java Bean 方法描述符 | +| java.beans.EventSetDescriptor | Java Bean 事件集合描述符 | + +## 参考资料 + +- [Spring 官方文档之 Core Technologies](https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/core.html#beans) +- [《小马哥讲 Spring 核心编程思想》](https://time.geekbang.org/course/intro/265) \ No newline at end of file diff --git "a/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/23.Spring\347\261\273\345\236\213\350\275\254\346\215\242.md" "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/23.Spring\347\261\273\345\236\213\350\275\254\346\215\242.md" new file mode 100644 index 00000000..2da331a6 --- /dev/null +++ "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/23.Spring\347\261\273\345\236\213\350\275\254\346\215\242.md" @@ -0,0 +1,214 @@ +--- +title: Spring 类型转换 +date: 2022-12-22 19:43:59 +order: 23 +categories: + - Java + - 框架 + - Spring + - Spring核心 +tags: + - Java + - 框架 + - Spring +permalink: /pages/6662dc/ +--- + +# Spring 类型转换 + +## Spring 类型转换的实现 + +- 基于 JavaBeans 接口的类型转换实现 + - 基于 java.beans.PropertyEditor 接口扩展 +- Spring 3.0+ 通用类型转换实现 + +## 使用场景 + +| 场景 | 基于 JavaBeans 接口的类型转换实现 | Spring 3.0+ 通用类型转换实现 | +| ------------------ | --------------------------------- | ---------------------------- | +| 数据绑定 | YES | YES | +| BeanWrapper | YES | YES | +| Bean 属性类型转换 | YES | YES | +| 外部化属性类型转换 | NO | YES | + +## 基于 JavaBeans 接口的类型转换 + +核心职责 + +- 将 String 类型的内容转化为目标类型的对象 + +扩展原理 + +- Spring 框架将文本内容传递到 PropertyEditor 实现的 setAsText(String) 方法 +- PropertyEditor#setAsText(String) 方法实现将 String 类型转化为目标类型的对象 +- 将目标类型的对象传入 PropertyEditor#setValue(Object) 方法 +- PropertyEditor#setValue(Object) 方法实现需要临时存储传入对象 +- Spring 框架将通过 PropertyEditor#getValue() 获取类型转换后的对象 + +## Spring 內建 PropertyEditor 扩展 + +內建扩展(org.springframework.beans.propertyeditors 包下) + +| 转换场景 | 实现类 | +| ------------------- | ----------------------------------------------------------------- | +| String -> Byte 数组 | org.springframework.beans.propertyeditors.ByteArrayPropertyEditor | +| String -> Char | org.springframework.beans.propertyeditors.CharacterEditor | +| String -> Char 数组 | org.springframework.beans.propertyeditors.CharArrayPropertyEditor | +| String -> Charset | org.springframework.beans.propertyeditors.CharsetEditor | +| String -> Class | org.springframework.beans.propertyeditors.ClassEditor | +| String -> Currency | org.springframework.beans.propertyeditors.CurrencyEditor | +| | | + +## 自定义 PropertyEditor 扩展 + +扩展模式 + +- 扩展 `java.beans.PropertyEditorSupport` 类 + +实现 `org.springframework.beans.PropertyEditorRegistrar` + +- 实现 `registerCustomEditors(org.springframework.beans.PropertyEditorRegistry)` 方法 +- 将 `PropertyEditorRegistrar` 实现注册为 Spring Bean + +向 `org.springframework.beans.PropertyEditorRegistry` 注册自定义 PropertyEditor 实现 + +- 通用类型实现 `registerCustomEditor(Class, PropertyEditor)` +- Java Bean 属性类型实现:`registerCustomEditor(Class, String, PropertyEditor)` + +## Spring PropertyEditor 的设计缺陷 + +违反职责单一原则 + +- `java.beans.PropertyEditor` 接口职责太多,除了类型转换,还包括 Java Beans 事件和 Java GUI 交 + 互 + +`java.beans.PropertyEditor` 实现类型局限 + +- 来源类型只能为 `java.lang.String` 类型 + +`java.beans.PropertyEditor` 实现缺少类型安全 + +- 除了实现类命名可以表达语义,实现类无法感知目标转换类型 + +## Spring 3 通用类型转换接口 + +类型转换接口 - org.springframework.core.convert.converter.Converter + +- 泛型参数 S:来源类型,参数 T:目标类型 +- 核心方法:T convert(S) + +通用类型转换接口 - org.springframework.core.convert.converter.GenericConverter + +- 核心方法:convert(Object,TypeDescriptor,TypeDescriptor) +- 配对类型:org.springframework.core.convert.converter.GenericConverter.ConvertiblePair +- 类型描述:org.springframework.core.convert.TypeDescriptor + +## Spring 內建类型转换器 + +內建扩展 + +| 转换场景 | 实现类所在包名(package) | +| -------------------- | -------------------------------------------- | +| 日期/时间相关 | org.springframework.format.datetime | +| Java 8 日期/时间相关 | org.springframework.format.datetime.standard | +| 通用实现 | org.springframework.core.convert.support | + +## Converter 接口的局限性 + +局限一:缺少 Source Type 和 Target Type 前置判断 + +- 应对:增加 org.springframework.core.convert.converter.ConditionalConverter 实现 + +局限二:仅能转换单一的 Source Type 和 Target Type + +- 应对:使用 org.springframework.core.convert.converter.GenericConverter 代替 + +## GenericConverter 接口 + +`org.springframework.core.convert.converter.GenericConverter` + +| 核心要素 | 说明 | +| -------- | ----------------------------------------------------------------------------- | +| 使用场景 | 用于“复合”类型转换场景,比如 Collection、Map、数组等 | +| 转换范围 | `Set getConvertibleTypes()` | +| 配对类型 | `org.springframework.core.convert.converter.GenericConverter.ConvertiblePair` | +| 转换方法 | `convert(Object,TypeDescriptor,TypeDescriptor)` | +| 类型描述 | `org.springframework.core.convert.TypeDescriptor` | + +## 优化 GenericConverter 接口 + +GenericConverter 局限性 + +- 缺少 Source Type 和 Target Type 前置判断 +- 单一类型转换实现复杂 + +GenericConverter 优化接口 - `ConditionalGenericConverter` + +- 复合类型转换:`org.springframework.core.convert.converter.GenericConverter` +- 类型条件判断:`org.springframework.core.convert.converter.ConditionalConverter` + +## 扩展 Spring 类型转换器 + +实现转换器接口 + +- `org.springframework.core.convert.converter.Converter` +- `org.springframework.core.convert.converter.ConverterFactory` +- `org.springframework.core.convert.converter.GenericConverter` + +注册转换器实现 + +- 通过 `ConversionServiceFactoryBean` Spring Bean +- 通过 `org.springframework.core.convert.ConversionService API` + +## 统一类型转换服务 + +`org.springframework.core.convert.ConversionService` + +| 实现类型 | 说明 | +| ------------------------------------ | ----------------------------------------------------------------------------------------- | +| `GenericConversionService` | 通用 ConversionService 模板实现,不内置转化器实现 | +| `DefaultConversionService` | 基础 ConversionService 实现,内置常用转化器实现 | +| `FormattingConversionService` | 通用 Formatter + GenericConversionService 实现,不内置转化器和 Formatter 实现 | +| `DefaultFormattingConversionService` | DefaultConversionService + 格式化 实现(如:JSR-354 Money & Currency, JSR-310 Date-Time) | + +## ConversionService 作为依赖 + +类型转换器底层接口 - `org.springframework.beans.TypeConverter` + +- 起始版本:Spring 2.0 +- 核心方法 - convertIfNecessary 重载方法 +- 抽象实现 - `org.springframework.beans.TypeConverterSupport` +- 简单实现 - `org.springframework.beans.SimpleTypeConverter` + +类型转换器底层抽象实现 - `org.springframework.beans.TypeConverterSupport` + +- 实现接口 - `org.springframework.beans.TypeConverter` +- 扩展实现 - `org.springframework.beans.PropertyEditorRegistrySupport` +- 委派实现 - `org.springframework.beans.TypeConverterDelegate` + +类型转换器底层委派实现 - `org.springframework.beans.TypeConverterDelegate` + +- 构造来源 - `org.springframework.beans.AbstractNestablePropertyAccessor` 实现 + - `org.springframework.beans.BeanWrapperImpl` +- 依赖 - `java.beans.PropertyEditor` 实现 + - 默认內建实现 - `PropertyEditorRegistrySupport#registerDefaultEditors` +- 可选依赖 - `org.springframework.core.convert.ConversionService` 实现 + +## 问题 + +**Spring 类型转换实现有哪些**? + +- 基于 JavaBeans PropertyEditor 接口实现 +- Spring 3.0+ 通用类型转换实现 + +**Spring 类型转换器接口有哪些**? + +- 类型转换接口 - `org.springframework.core.convert.converter.Converter` +- 通用类型转换接口 - `org.springframework.core.convert.converter.GenericConverter` +- 类型条件接口 - `org.springframework.core.convert.converter.ConditionalConverter` +- 综合类型转换接口 - `org.springframework.core.convert.converter.ConditionalGenericConverter` + +## 参考资料 + +- [Spring 官方文档之 Core Technologies](https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/core.html#beans) +- [《小马哥讲 Spring 核心编程思想》](https://time.geekbang.org/course/intro/265) \ No newline at end of file diff --git "a/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/24.SpringEL.md" "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/24.SpringEL.md" new file mode 100644 index 00000000..1e939038 --- /dev/null +++ "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/24.SpringEL.md" @@ -0,0 +1,22 @@ +--- +title: Spring EL 表达式 +date: 2023-01-12 20:26:46 +order: 24 +categories: + - Java + - 框架 + - Spring + - Spring核心 +tags: + - Java + - 框架 + - Spring +permalink: /pages/1f743f/ +--- + +# Spring EL 表达式 + +## 参考资料 + +- [Spring 官方文档之 Core Technologies](https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/core.html#beans) +- [《小马哥讲 Spring 核心编程思想》](https://time.geekbang.org/course/intro/265) \ No newline at end of file diff --git "a/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/25.Spring\344\272\213\344\273\266.md" "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/25.Spring\344\272\213\344\273\266.md" new file mode 100644 index 00000000..8dc880d0 --- /dev/null +++ "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/25.Spring\344\272\213\344\273\266.md" @@ -0,0 +1,232 @@ +--- +title: Spring 事件 +date: 2022-12-22 20:31:02 +order: 25 +categories: + - Java + - 框架 + - Spring + - Spring核心 +tags: + - Java + - 框架 + - Spring +permalink: /pages/cca414/ +--- + +# Spring 事件 + +## Java 事件/监听器编程模型 + +设计模式 - 观察者模式扩展 + +- 可观者对象(消息发送者) - java.util.Observable +- 观察者 - java.util.Observer + +标准化接口 + +- 事件对象 - java.util.EventObject +- 事件监听器 - java.util.EventListener + +## 面向接口的事件/监听器设计模式 + +事件/监听器场景举例 + +| Java 技术规范 | 事件接口 | 监听器接口 | +| --------------- | ------------------------------------- | ---------------------------------------- | +| JavaBeans | java.beans.PropertyChangeEvent | java.beans.PropertyChangeListener | +| Java AWT | java.awt.event.MouseEvent | java.awt.event.MouseListener | +| Java Swing | javax.swing.event.MenuEvent | javax.swing.event.MenuListener | +| Java Preference | java.util.prefs.PreferenceChangeEvent | java.util.prefs.PreferenceChangeListener | + +## 面向注解的事件/监听器设计模式 + +事件/监听器注解场景举例 + +| Java 技术规范 | 事件注解 | 监听器注解 | +| ------------- | ------------------------------ | ------------------------------------- | +| Servlet 3.0+ | | @javax.servlet.annotation.WebListener | +| JPA 1.0+ | @javax.persistence.PostPersist | | +| Java Common | @PostConstruct | | +| EJB 3.0+ | @javax.ejb.PrePassivate | | +| JSF 2.0+ | @javax.faces.event.ListenerFor | | + +## Spring 标准事件 - ApplicationEvent + +Java 标准事件 `java.util.EventObject` 扩展 + +- 扩展特性:事件发生事件戳 +- Spring 应用上下文 ApplicationEvent 扩展 - `ApplicationContextEvent` +- Spring 应用上下文(ApplicationContext)作为事件源 + +具体实现: + +- `org.springframework.context.event.ContextClosedEvent` +- `org.springframework.context.event.ContextRefreshedEvent` +- `org.springframework.context.event.ContextStartedEvent` +- `org.springframework.context.event.ContextStoppedEvent` + +## 基于接口的 Spring 事件监听器 + +Java 标准事件监听器 `java.util.EventListener` 扩展 + +- 扩展接口 - `org.springframework.context.ApplicationListener` +- 设计特点:单一类型事件处理 +- 处理方法:`onApplicationEvent(ApplicationEvent)` +- 事件类型:`org.springframework.context.ApplicationEvent` + +## 基于注解的 Spring 事件监听器 + +Spring 注解 - `@org.springframework.context.event.EventListener` + +| 特性 | 说明 | +| -------------------- | -------------------------------------------- | +| 设计特点 | 支持多 `ApplicationEvent` 类型,无需接口约束 | +| 注解目标 | 方法 | +| 是否支持异步执行 | 支持 | +| 是否支持泛型类型事件 | 支持 | +| 是指支持顺序控制 | 支持,配合 `@Order` 注解控制 | + +## 注册 Spring ApplicationListener + +- 方法一:ApplicationListener 作为 Spring Bean 注册 +- 方法二:通过 ConfigurableApplicationContext API 注册 + +## Spring 事件发布器 + +- 方法一:通过 ApplicationEventPublisher 发布 Spring 事件 + - 获取 ApplicationEventPublisher + - 依赖注入 +- 方法二:通过 ApplicationEventMulticaster 发布 Spring 事件 + - 获取 ApplicationEventMulticaster + - 依赖注入 + - 依赖查找 + +## Spring 层次性上下文事件传播 + +- 发生说明 +- 当 Spring 应用出现多层次 Spring 应用上下文(ApplicationContext)时,如 Spring WebMVC、Spring Boot 或 Spring Cloud 场景下,由子 ApplicationContext 发起 Spring 事件可能会传递到其 Parent ApplicationContext(直到 Root)的过程 +- 如何避免 +- 定位 Spring 事件源(ApplicationContext)进行过滤处理 + +## Spring 内建事件 + +ApplicationContextEvent 派生事件 + +- ContextRefreshedEvent :Spring 应用上下文就绪事件 +- ContextStartedEvent :Spring 应用上下文启动事件 +- ContextStoppedEvent :Spring 应用上下文停止事件 +- ContextClosedEvent :Spring 应用上下文关闭事件 + +## Spring 4.2 Payload 事件 + +Spring Payload 事件 - org.springframework.context.PayloadApplicationEvent + +- 使用场景:简化 Spring 事件发送,关注事件源主体 +- 发送方法:ApplicationEventPublisher#publishEvent(java.lang.Object) + +## 自定义 Spring 事件 + +- 扩展 org.springframework.context.ApplicationEvent +- 实现 org.springframework.context.ApplicationListener +- 注册 org.springframework.context.ApplicationListener + +## 依赖注入 ApplicationEventPublisher + +- 通过 ApplicationEventPublisherAware 回调接口 +- 通过 @Autowired ApplicationEventPublisher + +## 依赖查找 ApplicationEventMulticaster + +查找条件 + +- Bean 名称:"applicationEventMulticaster" +- Bean 类型:org.springframework.context.event.ApplicationEventMulticaster + +## ApplicationEventPublisher 底层实现 + +- 接口:org.springframework.context.event.ApplicationEventMulticaster +- 抽象类:org.springframework.context.event.AbstractApplicationEventMulticaster +- 实现类:org.springframework.context.event.SimpleApplicationEventMulticaster + +## 同步和异步 Spring 事件广播 + +基于实现类 - `org.springframework.context.event.SimpleApplicationEventMulticaster` + +- 模式切换:`setTaskExecutor(java.util.concurrent.Executor)` 方法 + - 默认模式:同步 + - 异步模式:如 `java.util.concurrent.ThreadPoolExecutor` +- 设计缺陷:非基于接口契约编程 + +基于注解 - `@org.springframework.context.event.EventListener` + +- 模式切换 + - 默认模式:同步 + - 异步模式:标注 `@org.springframework.scheduling.annotation.Async` +- 实现限制:无法直接实现同步/异步动态切换 + +## Spring 4.1 事件异常处理 + +Spring 3.0 错误处理接口 - org.springframework.util.ErrorHandler + +使用场景 + +- Spring 事件(Events) + - SimpleApplicationEventMulticaster Spring 4.1 开始支持 +- Spring 本地调度(Scheduling) + - org.springframework.scheduling.concurrent.ConcurrentTaskScheduler + - org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler + +## Spring 事件/监听器实现原理 + +核心类 - `org.springframework.context.event.SimpleApplicationEventMulticaster` + +- 设计模式:观察者模式扩展 + - 被观察者 - org.springframework.context.ApplicationListener + - API 添加 + - 依赖查找 + - 通知对象 - org.springframework.context.ApplicationEvent +- 执行模式:同步/异步 +- 异常处理:org.springframework.util.ErrorHandler +- 泛型处理:org.springframework.core.ResolvableType + +## 问题 + +**Spring Boot 事件** + +| 事件类型 | 发生时机 | +| ----------------------------------- | --------------------------------------- | +| ApplicationStartingEvent | 当 Spring Boot 应用已启动时 | +| ApplicationStartedEvent | 当 Spring Boot 应用已启动时 | +| ApplicationEnvironmentPreparedEvent | 当 Spring Boot Environment 实例已准备时 | +| ApplicationPreparedEvent | 当 Spring Boot 应用预备时 | +| ApplicationReadyEvent | 当 Spring Boot 应用完全可用时 | +| ApplicationFailedEvent | 当 Spring Boot 应用启动失败时 | + +**Spring Cloud 事件** + +| 事件类型 | 发生时机 | +| -------------------------- | ------------------------------------- | +| EnvironmentChangeEvent | 当 Environment 示例配置属性发生变化时 | +| HeartbeatEvent | 当 DiscoveryClient 客户端发送心跳时 | +| InstancePreRegisteredEvent | 当服务实例注册前 | +| InstanceRegisteredEvent | 当服务实例注册后 | +| RefreshEvent | 当 RefreshEndpoint 被调用时 | +| RefreshScopeRefreshedEvent | 当 Refresh Scope Bean 刷新后 | + +**Spring 事件核心接口/组件**? + +- Spring 事件 - org.springframework.context.ApplicationEvent +- Spring 事件监听器 - org.springframework.context.ApplicationListener +- Spring 事件发布器 - org.springframework.context.ApplicationEventPublisher +- Spring 事件广播器 - org.springframework.context.event.ApplicationEventMulticaster + +**Spring 同步和异步事件处理的使用场景**? + +- Spring 同步事件 - 绝大多数 Spring 使用场景,如 ContextRefreshedEvent +- Spring 异步事件 - 主要 @EventListener 与 @Async 配合,实现异步处理,不阻塞主线程,比如长时间的数据计算任务等。不要轻易调整 SimpleApplicationEventMulticaster 中关联的 taskExecutor 对象,除非使用者非常了解 Spring 事件机制,否则容易出现异常行为。 + +## 参考资料 + +- [Spring 官方文档之 Core Technologies](https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/core.html#beans) +- [《小马哥讲 Spring 核心编程思想》](https://time.geekbang.org/course/intro/265) \ No newline at end of file diff --git "a/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/26.Spring\345\233\275\351\231\205\345\214\226.md" "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/26.Spring\345\233\275\351\231\205\345\214\226.md" new file mode 100644 index 00000000..e07843fd --- /dev/null +++ "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/26.Spring\345\233\275\351\231\205\345\214\226.md" @@ -0,0 +1,121 @@ +--- +title: Spring 国际化 +date: 2022-12-22 11:44:54 +order: 26 +categories: + - Java + - 框架 + - Spring + - Spring核心 +tags: + - Java + - 框架 + - Spring +permalink: /pages/b5b8ad/ +--- + +# Spring 国际化 + +## Spring 国际化使用场景 + +- 普通国际化文案 +- Bean Validation 校验国际化文案 +- Web 站点页面渲染 +- Web MVC 错误消息提示 + +## Spring 国际化接口 + +- 核心接口:`org.springframework.context.MessageSource` +- 主要概念 + - 文案模板编码(code) + - 文案模板参数(args) + - 区域(Locale) + +## 层次性 MessageSource + +- Spring 层次性接口回顾 + - `org.springframework.beans.factory.HierarchicalBeanFactory` + - `org.springframework.context.ApplicationContext` + - `org.springframework.beans.factory.config.BeanDefinition` +- Spring 层次性国际化接口 + - `org.springframework.context.HierarchicalMessageSource` + +## Java 国际化标准实现 + +核心接口: + +- 抽象实现 - `java.util.ResourceBundle` +- Properties 资源实现 - `java.util.PropertyResourceBundle` +- 例举实现 - `java.util.ListResourceBundle` + +`ResourceBundle` 核心特性 + +- Key-Value 设计 +- 层次性设计 +- 缓存设计 +- 字符编码控制 - `java.util.ResourceBundle.Control`(@since 1.6) +- Control SPI 扩展 - `java.util.spi.ResourceBundleControlProvider`(@since 1.8) + +## Java 文本格式化 + +- 核心接口 + - java.text.MessageFormat +- 基本用法 + - 设置消息格式模式- new MessageFormat(...) + - 格式化 - format(new Object[]{...}) +- 消息格式模式 + - 格式元素:{ArgumentIndex (,FormatType,(FormatStyle))} + - FormatType:消息格式类型,可选项,每种类型在 number、date、time 和 choice 类型选其一 + - FormatStyle:消息格式风格,可选项,包括:short、medium、long、full、integer、currency、 + percent +- 高级特性 + - 重置消息格式模式 + - 重置 java.util.Locale + - 重置 java.text.Format + +## MessageSource 开箱即用实现 + +- 基于 ResourceBundle + MessageFormat 组合 MessageSource 实现 +- org.springframework.context.support.ResourceBundleMessageSource +- 可重载 Properties + MessageFormat 组合 MessageSource 实现 +- org.springframework.context.support.ReloadableResourceBundleMessageSource + +## MessageSource 內建依赖 + +- MessageSource 內建 Bean 可能来源 +- 预注册 Bean 名称为:“messageSource”,类型为:MessageSource Bean +- 默认內建实现 - DelegatingMessageSource +- 层次性查找 MessageSource 对象 + +## 问题 + +**Spring Boot 为什么要新建 MessageSource Bean**? + +- AbstractApplicationContext 的实现决定了 MessageSource 內建实现 +- Spring Boot 通过外部化配置简化 MessageSource Bean 构建 +- Spring Boot 基于 Bean Validation 校验非常普遍 + +**Spring 国际化接口有哪些**? + +- 核心接口 - MessageSource +- 层次性接口 - `org.springframework.context.HierarchicalMessageSource` + +**Spring 有哪些 MessageSource 內建实现**? + +- `org.springframework.context.support.ResourceBundleMessageSource` +- `org.springframework.context.support.ReloadableResourceBundleMessageSource` +- `org.springframework.context.support.StaticMessageSource` +- `org.springframework.context.support.DelegatingMessageSource` + +**如何实现配置自动更新 MessageSource**? + +主要技术 + +- Java NIO 2:`java.nio.file.WatchService` +- Java Concurrency : `java.util.concurrent.ExecutorService` +- Spring:`org.springframework.context.support.AbstractMessageSource` + +## 参考资料 + +- [Spring 官方文档之 Core Technologies](https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/core.html#beans) +- [《小马哥讲 Spring 核心编程思想》](https://time.geekbang.org/course/intro/265) \ No newline at end of file diff --git "a/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/27.Spring\346\263\233\345\236\213\345\244\204\347\220\206.md" "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/27.Spring\346\263\233\345\236\213\345\244\204\347\220\206.md" new file mode 100644 index 00000000..ffc2db65 --- /dev/null +++ "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/27.Spring\346\263\233\345\236\213\345\244\204\347\220\206.md" @@ -0,0 +1,141 @@ +--- +title: Spring 泛型处理 +date: 2022-12-22 20:11:52 +order: 27 +categories: + - Java + - 框架 + - Spring + - Spring核心 +tags: + - Java + - 框架 + - Spring +permalink: /pages/175cbd/ +--- + +# Spring 泛型处理 + +## Java 泛型基础 + +泛型类型 + +- 泛型类型是在类型上参数化的泛型类或接口 + +泛型使用场景 + +- 编译时强类型检查 +- 避免类型强转 +- 实现通用算法 + +泛型类型擦写 + +- 泛型被引入到 Java 语言中,以便在编译时提供更严格的类型检查并支持泛型编程。类型擦除确保不会 + 为参数化类型创建新类;因此,泛型不会产生运行时开销。为了实现泛型,编译器将类型擦除应用于: + - 将泛型类型中的所有类型参数替换为其边界,如果类型参数是无边界的,则将其替换为 + “Object”。因此,生成的字节码只包含普通类、接口和方法 + - 必要时插入类型转换以保持类型安全 + - 生成桥方法以保留扩展泛型类型中的多态性 + +## Java 5 类型接口 + +Java 5 类型接口 - `java.lang.reflect.Type` + +| 派生类或接口 | 说明 | +| ------------------------------------- | --------------------------------------- | +| `java.lang.Class` | Java 类 API,如 `java.lang.String` | +| `java.lang.reflect.GenericArrayType` | 泛型数组类型 | +| `java.lang.reflect.ParameterizedType` | 泛型参数类型 | +| `java.lang.reflect.TypeVariable` | 泛型类型变量,如 `Collection` 中的 E | +| `java.lang.reflect.WildcardType` | 泛型通配类型 | + +Java 泛型反射 API + +| 类型 | API | +| -------------------------------- | ---------------------------------------- | +| 泛型信息(Generics Info) | `java.lang.Class#getGenericInfo()` | +| 泛型参数(Parameters) | `java.lang.reflect.ParameterizedType` | +| 泛型父类(Super Classes) | `java.lang.Class#getGenericSuperclass()` | +| 泛型接口(Interfaces) | `java.lang.Class#getGenericInterfaces()` | +| 泛型声明(Generics Declaration) | `java.lang.reflect.GenericDeclaration` | + +## Spring 泛型类型辅助类 + +核心 API - `org.springframework.core.GenericTypeResolver` + +- 版本支持:[2.5.2 , ) +- 处理类型相关(Type)相关方法 + - `resolveReturnType` + - `resolveType` +- 处理泛型参数类型(`ParameterizedType`)相关方法 + - `resolveReturnTypeArgument` + - `resolveTypeArgument` + - `resolveTypeArguments` +- 处理泛型类型变量(`TypeVariable`)相关方法 + - `getTypeVariableMap` + +## Spring 泛型集合类型辅助类 + +核心 API - `org.springframework.core.GenericCollectionTypeResolver` + +- 版本支持:[2.0 , 4.3] +- 替换实现:`org.springframework.core.ResolvableType` +- 处理 Collection 相关 + - `getCollection*Type` +- 处理 Map 相关 + - `getMapKey*Type` + - `getMapValue*Type` + +## Spring 方法参数封装 - MethodParameter + +核心 API - `org.springframework.core.MethodParameter` + +- 起始版本:[2.0 , ) +- 元信息 + - 关联的方法 - Method + - 关联的构造器 - Constructor + - 构造器或方法参数索引 - parameterIndex + - 构造器或方法参数类型 - parameterType + - 构造器或方法参数泛型类型 - genericParameterType + - 构造器或方法参数参数名称 - parameterName + - 所在的类 - containingClass + +## Spring 4.0 泛型优化实现 - ResolvableType + +核心 API - `org.springframework.core.ResolvableType` + +- 起始版本:[4.0 , ) +- 扮演角色:`GenericTypeResolver` 和 `GenericCollectionTypeResolver` 替代者 +- 工厂方法:`for*` 方法 +- 转换方法:`as*` 方法 +- 处理方法:`resolve*` 方法 + +## ResolvableType 的局限性 + +- 局限一:ResolvableType 无法处理泛型擦写 +- 局限二:ResolvableType 无法处理非具体化的 ParameterizedType + +## 问题 + +**Java 泛型擦写发生在编译时还是运行时**? + +运行时 + +**请介绍 Java 5 Type 类型的派生类或接口** + +- `java.lang.Class` +- `java.lang.reflect.GenericArrayType` +- `java.lang.reflect.ParameterizedType` +- `java.lang.reflect.TypeVariable` +- `java.lang.reflect.WildcardType` + +**请说明 ResolvableType 的设计优势**? + +- 简化 Java 5 Type API 开发,屏蔽复杂 API 的运用,如 ParameterizedType +- 不变性设计(Immutability) +- Fluent API 设计(Builder 模式),链式(流式)编程 + +## 参考资料 + +- [Spring 官方文档之 Core Technologies](https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/core.html#beans) +- [《小马哥讲 Spring 核心编程思想》](https://time.geekbang.org/course/intro/265) \ No newline at end of file diff --git "a/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/28.Spring\346\263\250\350\247\243.md" "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/28.Spring\346\263\250\350\247\243.md" new file mode 100644 index 00000000..e8ce595c --- /dev/null +++ "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/28.Spring\346\263\250\350\247\243.md" @@ -0,0 +1,147 @@ +--- +title: Spring 注解 +date: 2022-12-23 09:08:15 +order: 28 +categories: + - Java + - 框架 + - Spring + - Spring核心 +tags: + - Java + - 框架 + - Spring +permalink: /pages/b6556f/ +--- + +# Spring 注解 + +## Spring 注解驱动编程发展历程 + +- 注解驱动启蒙时代:Spring Framework 1.x +- 注解驱动过渡时代:Spring Framework 2.x +- 注解驱动黄金时代:Spring Framework 3.x +- 注解驱动完善时代:Spring Framework 4.x +- 注解驱动当下时代:Spring Framework 5.x + +## Spring 核心注解场景分类 + +Spring 模式注解 + +| Spring 注解 | 场景说明 | 起始版本 | +| -------------- | ------------------ | -------- | +| @Repository | 数据仓储模式注解 | 2.0 | +| @Component | 通用组件模式注解 | 2.5 | +| @Service | 服务模式注解 | 2.5 | +| @Controller | Web 控制器模式注解 | 2.5 | +| @Configuration | 配置类模式注解 | 3.0 | + +装配注解 + +| Spring 注解 | 场景说明 | 起始版本 | +| --------------- | ------------------------------------------- | -------- | +| @ImportResource | 替换 XML 元素 `` | 2.5 | +| @Import | 导入 Configuration 类 | 2.5 | +| @ComponentScan | 扫描指定 package 下标注 Spring 模式注解的类 | 3.1 | + +依赖注入注解 + +| Spring 注解 | 场景说明 | 起始版本 | +| ----------- | ----------------------------------- | -------- | +| @Autowired | Bean 依赖注入,支持多种依赖查找方式 | 2.5 | +| @Qualifier | 细粒度的 @Autowired 依赖查找 | 2.5 | + +## Spring 注解编程模型 + +- 元注解(Meta-Annotations) +- Spring 模式注解(Stereotype Annotations) +- Spring 组合注解(Composed Annotations) +- Spring 注解属性别名和覆盖(Attribute Aliases and Overrides) + +## Spring 元注解(Meta-Annotations) + +- java.lang.annotation.Documented +- java.lang.annotation.Inherited +- java.lang.annotation.Repeatable + +## Spring 模式注解(Stereotype Annotations) + +理解 @Component “派⽣性”:元标注 @Component 的注解在 XML 元素 或注解 @ComponentScan 扫描中“派生”了 @Component 的特性,并且从 Spring Framework 4.0 开始支持多层次“派⽣性”。 + +举例说明: + +- @Repository +- @Service +- @Controller +- @Configuration +- @SpringBootConfiguration(Spring Boot) + +@Component “派⽣性”原理 + +- 核心组件 - org.springframework.context.annotation.ClassPathBeanDefinitionScanner +- org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider +- 资源处理 - org.springframework.core.io.support.ResourcePatternResolver +- 资源-类元信息 +- org.springframework.core.type.classreading.MetadataReaderFactory +- 类元信息 - org.springframework.core.type.ClassMetadata +- ASM 实现 - org.springframework.core.type.classreading.ClassMetadataReadingVisitor +- 反射实现 - org.springframework.core.type.StandardAnnotationMetadata +- 注解元信息 - org.springframework.core.type.AnnotationMetadata +- ASM 实现 - org.springframework.core.type.classreading.AnnotationMetadataReadingVisitor +- 反射实现 - org.springframework.core.type.StandardAnnotationMetadata + +## Spring 组合注解(Composed Annotations) + +Spring 组合注解(Composed Annotations)中的元注允许是 Spring 模式注解(Stereotype Annotation)与其他 Spring 功能性注解的任意组合。 + +## Spring 注解属性别名(Attribute Aliases) + +## Spring 注解属性覆盖(Attribute Overrides) + +## Spring @Enable 模块驱动 + +@Enable 模块驱动 + +@Enable 模块驱动是以 @Enable 为前缀的注解驱动编程模型。所谓“模块”是指具备相同领域的功能组件集合,组合所形成⼀个独⽴的单元。⽐如 Web MVC 模块、AspectJ 代理模块、Caching(缓存)模块、JMX(Java 管理扩展)模块、Async(异步处理)模块等。 + +举例说明 + +- @EnableWebMvc +- @EnableTransactionManagement +- @EnableCaching +- @EnableMBeanExport +- @EnableAsync + +@Enable 模块驱动编程模式 + +- 驱动注解:@EnableXXX +- 导入注解:@Import 具体实现 +- 具体实现 +- 基于 Configuration Class +- 基于 ImportSelector 接口实现 +- 基于 ImportBeanDefinitionRegistrar 接口实现 + +## Spring 条件注解 + +基于配置条件注解 - @org.springframework.context.annotation.Profile + +- 关联对象 - org.springframework.core.env.Environment 中的 Profiles +- 实现变化:从 Spring 4.0 开始,@Profile 基于 @Conditional 实现 + +基于编程条件注解 - @org.springframework.context.annotation.Conditional + +- 关联对象 - org.springframework.context.annotation.Condition 具体实现 + +@Conditional 实现原理 + +- 上下文对象 - org.springframework.context.annotation.ConditionContext +- 条件判断 - org.springframework.context.annotation.ConditionEvaluator +- 配置阶段 - org.springframework.context.annotation.ConfigurationCondition.ConfigurationPhase +- 判断入口 + - org.springframework.context.annotation.ConfigurationClassPostProcessor + - org.springframework.context.annotation.ConfigurationClassParser + +## 参考资料 + +- [Spring 官方文档之 Core Technologies](https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/core.html#beans) +- [《小马哥讲 Spring 核心编程思想》](https://time.geekbang.org/course/intro/265) \ No newline at end of file diff --git "a/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/29.SpringEnvironment\346\212\275\350\261\241.md" "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/29.SpringEnvironment\346\212\275\350\261\241.md" new file mode 100644 index 00000000..e95e298a --- /dev/null +++ "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/29.SpringEnvironment\346\212\275\350\261\241.md" @@ -0,0 +1,167 @@ +--- +title: Spring Environment 抽象 +date: 2022-12-23 09:27:44 +order: 29 +categories: + - Java + - 框架 + - Spring + - Spring核心 +tags: + - Java + - 框架 + - Spring +permalink: /pages/03d838/ +--- + +# Spring Environment 抽象 + +## 理解 Spring Environment 抽象 + +统一的 Spring 配置属性管理 + +Spring Framework 3.1 开始引入 Environment 抽象,它统一 Spring 配置属性的存储,包括占位符处理和类型转换,不仅完整地替换 PropertyPlaceholderConfigurer,而且还支持更丰富的配置属性源(PropertySource) + +条件化 Spring Bean 装配管理 + +通过 Environment Profiles 信息,帮助 Spring 容器提供条件化地装配 Bean + +## Spring Environment 接口使用场景 + +- ⽤于属性占位符处理 +- 用于转换 Spring 配置属性类型 +- 用于存储 Spring 配置属性源(PropertySource) +- 用于 Profiles 状态的维护 + +## Environment 占位符处理 + +Spring 3.1 前占位符处理 + +- 组件:org.springframework.beans.factory.config.PropertyPlaceholderConfigurer +- 接口:org.springframework.util.StringValueResolver + +Spring 3.1 + 占位符处理 + +- 组件:org.springframework.context.support.PropertySourcesPlaceholderConfigurer +- 接口:org.springframework.beans.factory.config.EmbeddedValueResolver + +## 理解条件配置 Spring Profiles + +Spring 3.1 条件配置 + +- API:org.springframework.core.env.ConfigurableEnvironment +- 修改:addActiveProfile(String)、setActiveProfiles(String...) 和 setDefaultProfiles(String...) +- 获取:getActiveProfiles() 和 getDefaultProfiles() +- 匹配:#acceptsProfiles(String...) 和 acceptsProfiles(Profiles) +- 注解:@org.springframework.context.annotation.Profile + +## Spring 4 重构 @Profile + +基于 Spring 4 org.springframework.context.annotation.Condition 接口实现 + +org.springframework.context.annotation.ProfileCondition + +## 依赖注入 Environment + +直接依赖注入 + +- 通过 EnvironmentAware 接口回调 +- 通过 @Autowired 注入 Environment + +间接依赖注入 + +- 通过 ApplicationContextAware 接口回调 +- 通过 @Autowired 注入 ApplicationContext + +## 依赖查找 Environment + +直接依赖查找 + +- 通过 org.springframework.context.ConfigurableApplicationContext#ENVIRONMENT_BEAN_NAME + +间接依赖查找 + +- 通过 org.springframework.context.ConfigurableApplicationContext#getEnvironment + +## 依赖注入 @Value + +通过注入 @Value + +实现 - org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor + +## Spring 类型转换在 Environment 中的运用 + +Environment 底层实现 + +- 底层实现 - org.springframework.core.env.PropertySourcesPropertyResolver +- 核心方法 - convertValueIfNecessary(Object,Class) +- 底层服务 - org.springframework.core.convert.ConversionService +- 默认实现 - org.springframework.core.convert.support.DefaultConversionService + +## Spring 类型转换在 @Value 中的运用 + +@Value 底层实现 + +- 底层实现 - org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor + - org.springframework.beans.factory.support.DefaultListableBeanFactory#doResolveDependency +- 底层服务 - org.springframework.beans.TypeConverter + - 默认实现 - org.springframework.beans.TypeConverterDelegate + - java.beans.PropertyEditor + - org.springframework.core.convert.ConversionService + +## Spring 配置属性源 PropertySource + +- API + - 单配置属性源 - org.springframework.core.env.PropertySource + - 多配置属性源 - org.springframework.core.env.PropertySources +- 注解 + - 单配置属性源 - @org.springframework.context.annotation.PropertySource + - 多配置属性源 - @org.springframework.context.annotation.PropertySources +- 关联 + - 存储对象 - org.springframework.core.env.MutablePropertySources + - 关联方法 - org.springframework.core.env.ConfigurableEnvironment#getPropertySources() + +## Spring 內建的配置属性源 + +內建 PropertySource + +| PropertySource 类型 | 说明 | +| -------------------------------------------------------------------- | ------------------------- | +| org.springframework.core.env.CommandLinePropertySource | 命令行配置属性源 | +| org.springframework.jndi.JndiPropertySource | JDNI 配置属性源 | +| org.springframework.core.env.PropertiesPropertySource | Properties 配置属性源 | +| org.springframework.web.context.support.ServletConfigPropertySource | Servlet 配置属性源 | +| org.springframework.web.context.support.ServletContextPropertySource | ServletContext 配置属性源 | +| org.springframework.core.env.SystemEnvironmentPropertySource | 环境变量配置属性源 | + +## 基于注解扩展 Spring 配置属性源 + +@org.springframework.context.annotation.PropertySource 实现原理 + +- 入口 - org.springframework.context.annotation.ConfigurationClassParser#doProcessConfigurationClass + - org.springframework.context.annotation.ConfigurationClassParser#processPropertySource +- 4.3 新增语义 + - 配置属性字符编码 - encoding + - org.springframework.core.io.support.PropertySourceFactory +- 适配对象 - org.springframework.core.env.CompositePropertySource + +## 基于 API 扩展 Spring 配置属性源 + +- Spring 应用上下文启动前装配 PropertySource +- Spring 应用上下文启动后装配 PropertySource + +## 问题 + +简单介绍 Spring Environment 接口? + +- 核心接口 - org.springframework.core.env.Environment +- 父接口 - org.springframework.core.env.PropertyResolver +- 可配置接口 - org.springframework.core.env.ConfigurableEnvironment +- 职责: + - 管理 Spring 配置属性源 + - 管理 Profiles + +## 参考资料 + +- [Spring 官方文档之 Core Technologies](https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/core.html#beans) +- [《小马哥讲 Spring 核心编程思想》](https://time.geekbang.org/course/intro/265) \ No newline at end of file diff --git "a/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/31.SpringBoot\344\271\213\345\277\253\351\200\237\345\205\245\351\227\250.md" "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/31.SpringBoot\344\271\213\345\277\253\351\200\237\345\205\245\351\227\250.md" new file mode 100644 index 00000000..da73823d --- /dev/null +++ "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/31.SpringBoot\344\271\213\345\277\253\351\200\237\345\205\245\351\227\250.md" @@ -0,0 +1,387 @@ +--- +title: SpringBoot 之快速入门 +date: 2021-12-10 18:22:26 +order: 31 +categories: + - Java + - 框架 + - Spring + - Spring核心 +tags: + - Java + - 框架 + - Spring + - SpringBoot +permalink: /pages/950e4d/ +--- + +# SpringBoot 之快速入门 + +## Spring Boot 简介 + +Spring Boot 可以让使用者非常方便的创建 Spring 应用。 + +Spring Boot 的目标是: + +- 为所有 Spring 开发者提供更快且可广泛访问的入门体验。 +- 开箱即用 +- 提供一系列通用的非功能特性(例如嵌入式服务、安全、指标、健康检查和外部化配置) +- 完全不需要代码生成,也不需要 XML 配置。 + +## Spring Boot 系统要求 + +Spring Boot 的构建工具要求: + +| Build Tool | Version | +| :--------- | :-------------------- | +| Maven | 3.5+ | +| Gradle | 6.8.x, 6.9.x, and 7.x | + +Spring Boot 支持的 Servlet 容器: + +| Name | Servlet Version | +| :----------- | :-------------- | +| Tomcat 9.0 | 4.0 | +| Jetty 9.4 | 3.1 | +| Jetty 10.0 | 4.0 | +| Undertow 2.0 | 4.0 | + +## 部署第一个 Spring Boot 项目 + +> 本节介绍如何开发一个小的“Hello World!” web 应用示例,来展示 Spring Boot 的一些关键功能。我们使用 Maven 来构建这个项目,因为大多数 IDE 都支持它。 + +### 环境检查 + +Spring Boot 项目依赖于 Java 环境和 Mave,开始项目之前需要先检查一下环境。 + +本地是否已安装 Java: + +```shell +$ java -version +java version "1.8.0_102" +Java(TM) SE Runtime Environment (build 1.8.0_102-b14) +Java HotSpot(TM) 64-Bit Server VM (build 25.102-b14, mixed mode) +``` + +本地是否已安装 Maven: + +```java +$ mvn -v +Apache Maven 3.5.4 (1edded0938998edf8bf061f1ceb3cfdeccf443fe; 2018-06-17T14:33:14-04:00) +Maven home: /usr/local/Cellar/maven/3.3.9/libexec +Java version: 1.8.0_102, vendor: Oracle Corporation +``` + +### 创建 pom + +我们需要从创建 Maven pom.xml 文件开始。 pom.xml 是 Maven 用于构建项目的配置文件。 + +```xml + + + 4.0.0 + + com.example + myproject + 0.0.1-SNAPSHOT + + + org.springframework.boot + spring-boot-starter-parent + 2.6.1 + + + + + +``` + +使用者可以通过运行 mvn package 来测试它 + +### 添加依赖 + +Spring Boot 提供了许多启动器(Starters)以应对不同的使用场景。使用者可将 jars 添加到类路径中。我们的示例程序在 POM 的 parent 使用 spring-boot-starter-parent。 spring-boot-starter-parent 是一个特殊的启动器,提供有用的 Maven 默认值。它还提供了一个依赖项的版本管理,可以让使用者使用时不必显示指定版本。 + +其他启动器(Starters)提供了各种针对不同使用场景的功能。比如,我们需要开发一个 Web 应用程序,就可以添加了一个 spring-boot-starter-web 依赖项。在此之前,我们可以通过运行以下命令来查看我们当前拥有的 maven 依赖: + +```shell +$ mvn dependency:tree + +[INFO] com.example:myproject:jar:0.0.1-SNAPSHOT +``` + +mvn dependency:tree 命令打印项目依赖项的层级结构。可以看到 spring-boot-starter-parent 本身没有提供任何依赖。要添加必要的依赖,需要编辑 pom.xml 并在 `` 部分添加 spring-boot-starter-web 依赖项: + +```xml + + + org.springframework.boot + spring-boot-starter-web + + +``` + +### 编写代码 + +要运行应用程序,我们需要创建一个启动类。默认情况下,Maven 从 `src/main/java` 编译源代码,因此您需要创建该目录结构,然后添加一个名为 `src/main/java/MyApplication.java` 的文件以包含以下代码: + +```java +@RestController +@EnableAutoConfiguration +public class MyApplication { + + @RequestMapping("/") + String home() { + return "Hello World!"; + } + + public static void main(String[] args) { + SpringApplication.run(MyApplication.class, args); + } + +} +``` + +说明: + +@RestController 注解告诉 Spring,这个类是用来处理 Rest 请求的。 + +`@RequestMapping` 注解提供了“路由”信息。它告诉 Spring 任何带有 `/` 路径的 HTTP 请求都应该映射到 `home` 方法。 `@RestController` 注解告诉 Spring 将结果字符串直接呈现给调用者。 + +`@EnableAutoConfiguration` 注解告诉 Spring Boot 根据你添加的 jar 依赖去自动装配 Spring。 + +> 自动配置旨在与“Starters”配合使用,但这两个概念并没有直接联系。您可以自由选择 starters 之外的 jar 依赖项。 Spring Boot 仍然尽力自动配置您的应用程序。 + +Spring Boot 的 main 方法通过调用 run 委托给 Spring Boot 的 `SpringApplication` 类。 `SpringApplication` 引导我们的应用程序,启动 Spring,进而启动自动配置的 Tomcat Web 服务器。我们需要将 `MyApplication.class` 作为参数传递给 run 方法,以告诉 `SpringApplication` 哪个是入口类。还传递 args 数组以公开任何命令行参数。 + +### 运行示例 + +此时,您的应用程序应该可以工作了。由于您使用了 spring-boot-starter-parent POM,因此您有一个有用的运行目标,可用于启动应用程序。从项目根目录键入 mvn spring-boot:run 以启动应用程序。您应该会看到类似于以下内容的输出: + +```shell +$ mvn spring-boot:run + + . ____ _ __ _ _ + /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \ +( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \ + \\/ ___)| |_)| | | | | || (_| | ) ) ) ) + ' |____| .__|_| |_|_| |_\__, | / / / / + =========|_|==============|___/=/_/_/_/ + :: Spring Boot :: (v2.6.1) +....... . . . +....... . . . (log output here) +....... . . . +........ Started MyApplication in 2.222 seconds (JVM running for 6.514) +``` + +如果您打开 Web 浏览器访问 localhost:8080,您应该会看到以下输出: + +``` +Hello World! +``` + +要正常退出应用程序,请按 `ctrl-c`。 + +### 创建可执行 jar + +要创建一个可执行的 jar,我们需要将 spring-boot-maven-plugin 添加到我们的 pom.xml 中。为此,请在依赖项部分下方插入以下行: + +```xml + + + + org.springframework.boot + spring-boot-maven-plugin + + + +``` + +保存 pom.xml 并从命令行运行 mvn package,如下所示: + +```shell +$ mvn package + +[INFO] Scanning for projects... +[INFO] +[INFO] ------------------------------------------------------------------------ +[INFO] Building myproject 0.0.1-SNAPSHOT +[INFO] ------------------------------------------------------------------------ +[INFO] .... .. +[INFO] --- maven-jar-plugin:2.4:jar (default-jar) @ myproject --- +[INFO] Building jar: /Users/developer/example/spring-boot-example/target/myproject-0.0.1-SNAPSHOT.jar +[INFO] +[INFO] --- spring-boot-maven-plugin:2.6.1:repackage (default) @ myproject --- +[INFO] ------------------------------------------------------------------------ +[INFO] BUILD SUCCESS +[INFO] ------------------------------------------------------------------------ +``` + +如果您查看 target 目录,应该会看到 `myproject-0.0.1-SNAPSHOT.jar`。该文件的大小应约为 10 MB。如果想看里面,可以使用 jar tvf,如下: + +```shell +$ jar tvf target/myproject-0.0.1-SNAPSHOT.jar +``` + +您还应该在目标目录中看到一个更小的名为 `myproject-0.0.1-SNAPSHOT.jar.original` 的文件。这是 Maven 在 Spring Boot 重新打包之前创建的原始 jar 文件。 + +要运行该应用程序,请使用 java -jar 命令,如下所示: + +``` +$ java -jar target/myproject-0.0.1-SNAPSHOT.jar + + . ____ _ __ _ _ + /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \ +( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \ + \\/ ___)| |_)| | | | | || (_| | ) ) ) ) + ' |____| .__|_| |_|_| |_\__, | / / / / + =========|_|==============|___/=/_/_/_/ + :: Spring Boot :: (v2.6.1) +....... . . . +....... . . . (log output here) +....... . . . +........ Started MyApplication in 2.536 seconds (JVM running for 2.864) +``` + +和以前一样,要退出应用程序,请按 `ctrl-c`。 + +## 通过 SPRING INITIALIZR 创建 Spring Boot 项目 + +### 创建项目 + +通过 `SPRING INITIALIZR` 工具产生基础项目 + +1. 访问:`http://start.spring.io/` +2. 选择构建工具`Maven Project`、Spring Boot 版本 `1.5.10` 以及一些工程基本信息,可参考下图所示: + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/start.spring.io.png) + +3. 点击`Generate Project`下载项目压缩包 +4. 解压压缩包,包中已是一个完整的项目。 + +如果你使用 Intellij 作为 IDE,那么你可以直接使用 SPRING INITIALIZR,参考下图操作: + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/intellij-spring-initializr.gif) + +### 项目说明 + +**重要文件** + +- `src/main/java` 路径下的 `Chapter1Application` 类 :程序入口 +- `src/main/resources` 路径下的 `application.properties` :项目配置文件 +- `src/test/java` 路径下的 `Chapter01ApplicationTests` :程序测试入口 + +**pom.xml** + +pom 中指定 parent 为以下内容,表示此项目继承了 `spring-boot-starter-parent` 的 maven 配置(主要是指定了常用依赖、插件的版本)。 + +```xml + + org.springframework.boot + spring-boot-starter-parent + 1.5.10.RELEASE + + +``` + +此外,pom 中默认引入两个依赖包,和一个插件。 + +```xml + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + +``` + +- `spring-boot-starter-web`:核心模块,包括自动配置支持、日志和 YAML。 +- `spring-boot-starter-test`:测试模块,包括 JUnit、Hamcrest、Mockito。 +- `spring-boot-maven-plugin`:spring boot 插件, 提供了一系列 spring boot 相关的 maven 操作。 + - `spring-boot:build-info`,生成 Actuator 使用的构建信息文件 build-info.properties + - `spring-boot:repackage`,默认 goal。在 mvn package 之后,再次打包可执行的 jar/war,同时保留 mvn package 生成的 jar/war 为.origin + - `spring-boot:run`,运行 Spring Boot 应用 + - `spring-boot:start`,在 mvn integration-test 阶段,进行 Spring Boot 应用生命周期的管理 + - `spring-boot:stop`,在 mvn integration-test 阶段,进行 Spring Boot 应用生命周期的管理 + +### 编写 REST 服务 + +- 创建 `package` ,名为 `io.github.zp.springboot.chapter1.web`(根据项目情况修改) +- 创建 `HelloController` 类,内容如下: + +```java +@RestController +public class HelloController { + + @RequestMapping("/hello") + public String index() { + return "Hello World"; + } + +} +``` + +- 启动主程序 `XXXApplication`,打开浏览器访问`http://localhost:8080/hello` ,可以看到页面输出`Hello World` + +### 编写单元测试用例 + +在 `XXXApplicationTests` 类中编写一个简单的单元测试来模拟 HTTP 请求,具体如下: + +```java +@RunWith(SpringJUnit4ClassRunner.class) +@SpringApplicationConfiguration(classes = MockServletContext.class) +@WebAppConfiguration +public class SpringBootHelloWorldApplicationTest { + + private MockMvc mvc; + + @Before + public void setUp() { + mvc = MockMvcBuilders.standaloneSetup(new HelloController()).build(); + } + + @Test + public void getHello() throws Exception { + mvc.perform(MockMvcRequestBuilders.get("/hello").accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(content().string(equalTo("Hello World"))); + } + +} +``` + +使用`MockServletContext`来构建一个空的`WebApplicationContext`,这样我们创建的`HelloController`就可以在`@Before`函数中创建并传递到`MockMvcBuilders.standaloneSetup()`函数中。 + +- 注意引入下面内容,让`status`、`content`、`equalTo`函数可用 + +```java +import static org.hamcrest.Matchers.equalTo; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +``` + +至此已完成目标,通过 Maven 构建了一个空白 Spring Boot 项目,再通过引入 web 模块实现了一个简单的请求处理。 + +### 示例源码 + +> 示例源码:[spring-boot-web-helloworld](https://github.com/dunwu/spring-boot-tutorial/tree/master/codes/web/spring-boot-web-helloworld) + +## 参考资料 + +- [Spring Boot 官方文档之 Getting Started](https://docs.spring.io/spring-boot/docs/current/reference/html/getting-started.html#getting-started) \ No newline at end of file diff --git "a/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/32.SpringBoot\344\271\213\345\261\236\346\200\247\345\212\240\350\275\275.md" "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/32.SpringBoot\344\271\213\345\261\236\346\200\247\345\212\240\350\275\275.md" new file mode 100644 index 00000000..8a6ab562 --- /dev/null +++ "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/32.SpringBoot\344\271\213\345\261\236\346\200\247\345\212\240\350\275\275.md" @@ -0,0 +1,387 @@ +--- +title: SpringBoot 之属性加载详解 +date: 2019-01-10 11:55:54 +order: 32 +categories: + - Java + - 框架 + - Spring + - Spring核心 +tags: + - Java + - 框架 + - Spring + - SpringBoot +permalink: /pages/0fb992/ +--- + +# SpringBoot 之属性加载详解 + +## 加载 property 顺序 + +Spring Boot 加载 property 顺序如下: + +1. [Devtools 全局配置](https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#using-boot-devtools-globalsettings) (当 devtools 被激活 `~/.spring-boot-devtools.properties`). +2. [测试环境中的 `@TestPropertySource` 注解配置](https://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/test/context/TestPropertySource.html) +3. 测试环境中的属性 `properties`:[`@SpringBootTest`](https://docs.spring.io/spring-boot/docs/current/api/org/springframework/boot/test/context/SpringBootTest.html) 和 [测试注解](https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#boot-features-testing-spring-boot-applications-testing-autoconfigured-tests). +4. 命令行参数 +5. `SPRING_APPLICATION_JSON` 属性 +6. `ServletConfig` 初始化参数 +7. `ServletContext` 初始化参数 +8. JNDI attributes from 通过 `java:comp/env` 配置的 JNDI 属性 +9. Java 系统属性 (`System.getProperties()`) +10. 操作系统环境比那里 +11. `RandomValuePropertySource` 加载 `random.*` 形式的属性 +12. jar 包外的 `application-{profile}.properties` 或 `application-{profile}.yml` 配置 +13. jar 包内的 `application-{profile}.properties` 或 `application-{profile}.yml` 配置 +14. jar 包外的 `application.properties` 或 `application.yml` 配置 +15. jar 包内的 `application.properties` 或 `application.yml` 配置 +16. `@PropertySource` 绑定的配置 +17. 默认属性 (通过 `SpringApplication.setDefaultProperties` 指定) + +## 随机属性 + +`RandomValuePropertySource` 类用于配置随机值。 + +示例: + +```properties +my.secret=${random.value} +my.number=${random.int} +my.bignumber=${random.long} +my.uuid=${random.uuid} +my.number.less.than.ten=${random.int(10)} +my.number.in.range=${random.int[1024,65536]} +``` + +## 命令行属性 + +默认情况下, `SpringApplication` 会获取 `--` 参数(例如 `--server.port=9000` ),并将这个 `property` 添加到 Spring 的 `Environment` 中。 + +如果不想加载命令行属性,可以通过 `SpringApplication.setAddCommandLineProperties(false)` 禁用。 + +## Application 属性文件 + +`SpringApplication` 会自动加载以下路径下的 `application.properties` 配置文件,将其中的属性读到 Spring 的 `Environment` 中。 + +1. 当前目录的 `/config` 子目录 +2. 当前目录 +3. classpath 路径下的 `/config` package +4. classpath 根路径 + +> 注: +> +> 以上列表的配置文件会根据顺序,后序的配置会覆盖前序的配置。 +> +> 你可以选择 [YAML(yml)](https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#boot-features-external-config-yaml) 配置文件替换 properties 配置文件。 + +如果不喜欢 `application.properties` 作为配置文件名,可以使用 `spring.config.name` 环境变量替换: + +``` +$ java -jar myproject.jar --spring.config.name=myproject +``` + +可以使用 `spring.config.location` 环境变量指定配置文件路径: + +```properties +$ java -jar myproject.jar --spring.config.location=classpath:/default.properties,classpath:/override.properties +``` + +## Profile 特定属性 + +如果定义 `application-{profile}.properties` 形式的配置文件,将被视为 `profile` 环境下的特定配置。 + +可以通过 `spring.profiles.active` 参数来激活 profile,如果没有激活的 profile,默认会加载 `application-default.properties` 中的配置。 + +## 属性中的占位符 + +`application.properties` 中的值会被 `Environment` 过滤,所以,可以引用之前定义的属性。 + +``` +app.name=MyApp +app.description=${app.name} is a Spring Boot application +``` + +> 注:你可以使用此技术来创建 Spring Boot 属性变量。请参考: [Section 77.4, “Use ‘Short’ Command Line Arguments](https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#howto-use-short-command-line-arguments) + +## YAML 属性 + +Spring 框架有两个类支持加载 YAML 文件。 + +- `YamlPropertiesFactoryBean` 将 YAML 文件的配置加载为 `Properties` 。 +- `YamlMapFactoryBean` 将 YAML 文件的配置加载为 `Map` 。 + +示例 1 + +```yaml +environments: + dev: + url: http://dev.example.com + name: Developer Setup + prod: + url: http://another.example.com + name: My Cool App +``` + +等价于: + +```properties +environments.dev.url=http://dev.example.com +environments.dev.name=Developer Setup +environments.prod.url=http://another.example.com +environments.prod.name=My Cool App +``` + +YAML 支持列表形式,等价于 property 中的 `[index]` : + +```yaml +my: +servers: + - dev.example.com + - another.example.com +``` + +等价于 + +```properties +my.servers[0]=dev.example.com +my.servers[1]=another.example.com +``` + +### 访问属性 + +`YamlPropertySourceLoader` 类会将 YAML 配置转化为 Spring `Environment` 类中的 `PropertySource` 。然后,你可以如同 properties 文件中的属性一样,使用 `@Value` 注解来访问 YAML 中配置的属性。 + +### 多 profile 配置 + +```yaml +server: + address: 192.168.1.100 +--- +spring: + profiles: development +server: + address: 127.0.0.1 +--- +spring: + profiles: production & eu-central +server: + address: 192.168.1.120 +``` + +### YAML 的缺点 + +注:YAML 注解中的属性不能通过 `@PropertySource` 注解来访问。所以,如果你的项目中使用了一些自定义属性文件,建议不要用 YAML。 + +## 属性前缀 + +```java +package com.example; + +import java.net.InetAddress; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +@ConfigurationProperties(prefix="acme") +public class AcmeProperties { + + private boolean enabled; + + private InetAddress remoteAddress; + + private final Security security = new Security(); + + public boolean isEnabled() { ... } + + public void setEnabled(boolean enabled) { ... } + + public InetAddress getRemoteAddress() { ... } + + public void setRemoteAddress(InetAddress remoteAddress) { ... } + + public Security getSecurity() { ... } + + public static class Security { + + private String username; + + private String password; + + private List roles = new ArrayList<>(Collections.singleton("USER")); + + public String getUsername() { ... } + + public void setUsername(String username) { ... } + + public String getPassword() { ... } + + public void setPassword(String password) { ... } + + public List getRoles() { ... } + + public void setRoles(List roles) { ... } + + } +} +``` + +相当于支持配置以下属性: + +- `acme.enabled` +- `acme.remote-address` +- `acme.security.username` +- `acme.security.password` +- `acme.security.roles` + +然后,你需要使用 `@EnableConfigurationProperties` 注解将属性类注入配置类中。 + +```java +@Configuration +@EnableConfigurationProperties(AcmeProperties.class) +public class MyConfiguration { +} +``` + +## 属性松散绑定规则 + +Spring Boot 属性名绑定比较松散。 + +以下属性 key 都是等价的: + +| Property | Note | +| ----------------------------------- | -------- | +| `acme.my-project.person.first-name` | `-` 分隔 | +| `acme.myProject.person.firstName` | 驼峰命名 | +| `acme.my_project.person.first_name` | `_` 分隔 | +| `ACME_MYPROJECT_PERSON_FIRSTNAME` | 大写字母 | + +## 属性转换 + +如果需要类型转换,你可以提供一个 `ConversionService` bean (一个名叫 `conversionService` 的 bean) 或自定义属性配置 (一个 `CustomEditorConfigurer` bean) 或自定义的 `Converters` (被 `@ConfigurationPropertiesBinding` 注解修饰的 bena)。 + +### 时间单位转换 + +Spring 使用 `java.time.Duration` 类代表时间大小,以下场景适用: + +- 除非指定 `@DurationUnit` ,否则一个 long 代表的时间为毫秒。 +- ISO-8601 标准格式( [`java.time.Duration`](https://docs.oracle.com/javase/8/docs/api//java/time/Duration.html#parse-java.lang.CharSequence-) 的实现就是参照此标准) +- 你也可以使用以下支持的单位: + - `ns` - 纳秒 + - `us` - 微秒 + - `ms` - 毫秒 + - `s` - 秒 + - `m` - 分 + - `h` - 时 + - `d` - 天 + +示例: + +```java +@ConfigurationProperties("app.system") +public class AppSystemProperties { + + @DurationUnit(ChronoUnit.SECONDS) + private Duration sessionTimeout = Duration.ofSeconds(30); + + private Duration readTimeout = Duration.ofMillis(1000); + + public Duration getSessionTimeout() { + return this.sessionTimeout; + } + + public void setSessionTimeout(Duration sessionTimeout) { + this.sessionTimeout = sessionTimeout; + } + + public Duration getReadTimeout() { + return this.readTimeout; + } + + public void setReadTimeout(Duration readTimeout) { + this.readTimeout = readTimeout; + } + +} +``` + +### 数据大小转换 + +Spring 使用 `DataSize` 类代表数据大小,以下场景适用: + +- long 值(默认视为 byte) +- 你也可以使用以下支持的单位: + - `B` + - `KB` + - `MB` + - `GB` + - `TB` + +示例: + +```java +@ConfigurationProperties("app.io") +public class AppIoProperties { + + @DataSizeUnit(DataUnit.MEGABYTES) + private DataSize bufferSize = DataSize.ofMegabytes(2); + + private DataSize sizeThreshold = DataSize.ofBytes(512); + + public DataSize getBufferSize() { + return this.bufferSize; + } + + public void setBufferSize(DataSize bufferSize) { + this.bufferSize = bufferSize; + } + + public DataSize getSizeThreshold() { + return this.sizeThreshold; + } + + public void setSizeThreshold(DataSize sizeThreshold) { + this.sizeThreshold = sizeThreshold; + } + +} +``` + +## 校验属性 + +```java +@ConfigurationProperties(prefix="acme") +@Validated +public class AcmeProperties { + + @NotNull + private InetAddress remoteAddress; + + @Valid + private final Security security = new Security(); + + // ... getters and setters + + public static class Security { + + @NotEmpty + public String username; + + // ... getters and setters + + } + +} +``` + +你也可以自定义一个名为 `configurationPropertiesValidator` 的校验器 Bean。获取这个 `@Bean` 的方法必须声明为 `static`。 + +## 示例源码 + +> 示例源码:[spring-boot-property](https://github.com/dunwu/spring-boot-tutorial/tree/master/codes/spring-boot-property) + +## 参考资料 + +- [Spring Boot 官方文档之 boot-features-external-config](https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#boot-features-external-config) \ No newline at end of file diff --git "a/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/33.SpringBoot\344\271\213Profile.md" "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/33.SpringBoot\344\271\213Profile.md" new file mode 100644 index 00000000..667df738 --- /dev/null +++ "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/33.SpringBoot\344\271\213Profile.md" @@ -0,0 +1,200 @@ +--- +title: SpringBoot 之 Profile +date: 2019-11-18 14:55:01 +order: 33 +categories: + - Java + - 框架 + - Spring + - Spring核心 +tags: + - Java + - 框架 + - Spring + - SpringBoot +permalink: /pages/cb598e/ +--- + +# SpringBoot 之 Profile + +> 一个应用为了在不同的环境下工作,常常会有不同的配置,代码逻辑处理。Spring Boot 对此提供了简便的支持。 +> +> 关键词: `@Profile`、`spring.profiles.active` + +## 区分环境的配置 + +### properties 配置 + +假设,一个应用的工作环境有:dev、test、prod + +那么,我们可以添加 4 个配置文件: + +- `applcation.properties` - 公共配置 +- `application-dev.properties` - 开发环境配置 +- `application-test.properties` - 测试环境配置 +- `application-prod.properties` - 生产环境配置 + +在 `applcation.properties` 文件中可以通过以下配置来激活 profile: + +```properties +spring.profiles.active = test +``` + +### yml 配置 + +与 properties 文件类似,我们也可以添加 4 个配置文件: + +- `applcation.yml` - 公共配置 +- `application-dev.yml` - 开发环境配置 +- `application-test.yml` - 测试环境配置 +- `application-prod.yml` - 生产环境配置 + +在 `applcation.yml` 文件中可以通过以下配置来激活 profile: + +```yml +spring: + profiles: + active: prod +``` + +此外,yml 文件也可以在一个文件中完成所有 profile 的配置: + +```yml +# 激活 prod +spring: + profiles: + active: prod +# 也可以同时激活多个 profile +# spring.profiles.active: prod,proddb,prodlog +--- +# dev 配置 +spring: + profiles: dev + +# 略去配置 + +--- +spring: + profiles: test + +# 略去配置 + +--- +spring.profiles: prod +spring.profiles.include: + - proddb + - prodlog + +--- +spring: + profiles: proddb + +# 略去配置 + +--- +spring: + profiles: prodlog +# 略去配置 +``` + +注意:不同 profile 之间通过 `---` 分割 + +## 区分环境的代码 + +使用 `@Profile` 注解可以指定类或方法在特定的 Profile 环境生效。 + +### 修饰类 + +```java +@Configuration +@Profile("production") +public class JndiDataConfig { + + @Bean(destroyMethod="") + public DataSource dataSource() throws Exception { + Context ctx = new InitialContext(); + return (DataSource) ctx.lookup("java:comp/env/jdbc/datasource"); + } +} +``` + +### 修饰注解 + +```java +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Profile("production") +public @interface Production { +} +``` + +### 修饰方法 + +```java +@Configuration +public class AppConfig { + + @Bean("dataSource") + @Profile("development") + public DataSource standaloneDataSource() { + return new EmbeddedDatabaseBuilder() + .setType(EmbeddedDatabaseType.HSQL) + .addScript("classpath:com/bank/config/sql/schema.sql") + .addScript("classpath:com/bank/config/sql/test-data.sql") + .build(); + } + + @Bean("dataSource") + @Profile("production") + public DataSource jndiDataSource() throws Exception { + Context ctx = new InitialContext(); + return (DataSource) ctx.lookup("java:comp/env/jdbc/datasource"); + } +} +``` + +## 激活 profile + +### 插件激活 profile + +``` +spring-boot:run -Drun.profiles=prod +``` + +### main 方法激活 profile + +``` +--spring.profiles.active=prod +``` + +### jar 激活 profile + +``` +java -jar -Dspring.profiles.active=prod *.jar +``` + +### 在 Java 代码中激活 profile + +直接指定环境变量来激活 profile: + +```java +System.setProperty("spring.profiles.active", "test"); +``` + +在 Spring 容器中激活 profile: + +```java +AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); +ctx.getEnvironment().setActiveProfiles("development"); +ctx.register(SomeConfig.class, StandaloneDataConfig.class, JndiDataConfig.class); +ctx.refresh(); +``` + +## 示例源码 + +> 示例源码:[spring-boot-profile](https://github.com/dunwu/spring-boot-tutorial/tree/master/codes/spring-boot-profile) + +## 参考资料 + +- [Spring 官方文档之 Bean Definition Profiles](https://docs.spring.io/spring/docs/current/spring-framework-reference/core.html#beans-definition-profiles) +- [Spring Boot 官方文档之 boot-features-profiles](https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#boot-features-profiles) \ No newline at end of file diff --git "a/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/README.md" "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/README.md" new file mode 100644 index 00000000..544eb1c1 --- /dev/null +++ "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/README.md" @@ -0,0 +1,62 @@ +--- +title: Spring 核心 +date: 2020-02-26 23:47:47 +categories: + - Java + - 框架 + - Spring + - Spring核心 +tags: + - Java + - 框架 + - Spring + - SpringBoot +permalink: /pages/5e7c20/ +hidden: true +index: false +--- + +# Spring 核心 + +> 章节主要针对:Spring & Spring Boot 框架的核心技术。如;Spring Bean、IoC、依赖查找、依赖注入、AOP、数据绑定、资源管理等。 + +## 📖 内容 + +- [Spring Bean](01.SpringBean.md) +- [Spring IoC](02.SpringIoC.md) +- [Spring 依赖查找](03.Spring依赖查找.md) +- [Spring 依赖注入](04.Spring依赖注入.md) +- [Spring IoC 依赖来源](05.SpringIoC依赖来源.md) +- [Spring Bean 作用域](06.SpringBean作用域.md) +- [Spring Bean 生命周期](07.SpringBean生命周期.md) +- [Spring 配置元数据](08.Spring配置元数据.md) +- [Spring AOP](10.SpringAop.md) +- [Spring 资源管理](20.Spring资源管理.md) +- [Spring 校验](21.Spring校验.md) +- [Spring 数据绑定](22.Spring数据绑定.md) +- [Spring 类型转换](23.Spring类型转换.md) +- [Spring EL 表达式](24.SpringEL.md) +- [Spring 事件](25.Spring事件.md) +- [Spring 国际化](26.Spring国际化.md) +- [Spring 泛型处理](27.Spring泛型处理.md) +- [Spring 注解](28.Spring注解.md) +- [SpringBoot 教程之快速入门](31.SpringBoot之快速入门.md) +- [SpringBoot 之属性加载](32.SpringBoot之属性加载.md) +- [SpringBoot 之 Profile](33.SpringBoot之Profile.md) + +## 📚 资料 + +- **官方** + - [Spring 官网](https://spring.io/) + - [Spring Framework 官方文档](https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/index.html) + - [Spring Github](https://github.com/spring-projects/spring-framework) +- **书籍** + - [《Spring In Action》](https://item.jd.com/12622829.html) +- **教程** + - [《小马哥讲 Spring 核心编程思想》](https://time.geekbang.org/course/intro/265) + - [geekbang-lessons](https://github.com/geektime-geekbang/geekbang-lessons) + - [跟我学 Spring3](http://jinnianshilongnian.iteye.com/blog/1482071) + +## 🚪 传送 + +◾ 💧 [钝悟的 IT 知识图谱](https://dunwu.github.io/waterdrop/) ◾ 🎯 [我的博客](https://github.com/dunwu/blog) ◾ \ No newline at end of file diff --git "a/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/02.Spring\346\225\260\346\215\256/01.Spring\344\271\213\346\225\260\346\215\256\346\272\220.md" "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/02.Spring\346\225\260\346\215\256/01.Spring\344\271\213\346\225\260\346\215\256\346\272\220.md" new file mode 100644 index 00000000..5d01d703 --- /dev/null +++ "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/02.Spring\346\225\260\346\215\256/01.Spring\344\271\213\346\225\260\346\215\256\346\272\220.md" @@ -0,0 +1,555 @@ +--- +title: Spring 之数据源 +date: 2017-10-20 09:27:55 +order: 01 +categories: + - Java + - 框架 + - Spring + - Spring数据 +tags: + - Java + - 框架 + - Spring + - SpringBoot + - 数据库 + - DataSource +permalink: /pages/1b774c/ +--- + +# Spring 之数据源 + +> 本文基于 Spring Boot 2.7.3 版本。 + +## Spring Boot 数据源基本配置 + +Spring Boot 提供了一系列 `spring.datasource.*` 配置来控制 `DataSource` 的配置。用户可以在 `application.properties` 或 `application.yml` 文件中指定数据源配置。这些配置项维护在 [`DataSourceProperties`](https://github.com/spring-projects/spring-boot/tree/v2.7.4/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceProperties.java) 。 + +下面是一个最基本的 mysql 数据源配置示例(都是必填项): + +```properties +# 数据库访问地址 +spring.datasource.url = jdbc:mysql://localhost:3306/spring_tutorial?serverTimezone=UTC&useUnicode=true&characterEncoding=utf8 +# 数据库驱动类,必须保证驱动类是可加载的 +spring.datasource.driver-class-name = com.mysql.cj.jdbc.Driver +# 数据库账号 +spring.datasource.username = root +# 数据库账号密码 +spring.datasource.password = root +``` + +需要根据实际情况,替换 `url`、`username`、`password`。 + +## Spring Boot 连接嵌入式数据源 + +使用内存嵌入式数据库开发应用程序通常很方便。显然,内存数据库不提供持久存储。使用者需要在应用程序启动时填充数据库,并准备在应用程序结束时丢弃数据。 + +Spring Boot 可以自动配置嵌入式数据库 [H2](https://www.h2database.com/)、[HSQL](https://hsqldb.org/) 和 [Derby](https://db.apache.org/derby/)。使用者无需提供任何连接 URL,只需要包含对要使用的嵌入式数据库的构建依赖项。如果类路径上有多个嵌入式数据库,需要设置 `spring.datasource.embedded-database-connection` 配置属性来控制使用哪一个。将该属性设置为 none 会禁用嵌入式数据库的自动配置。 + +> 注意:如果在测试中使用此功能,无论使用多少应用程序上下文,整个测试套件都会重用同一个数据库。如果要确保每个上下文都有一个单独的嵌入式数据库,则应将 `spring.datasource.generate-unique-name` 设置为 true。 + +下面,通过一个实例展示如何连接 H2 嵌入式数据库。 + +(1)在 pom.xml 中引入所需要的依赖: + +```xml + + org.springframework.boot + spring-boot-starter-data-jdbc + + + com.h2database + h2 + +``` + +(2)数据源配置 + +```properties +spring.datasource.jdbc-url = jdbc:h2:mem:test +spring.datasource.driver-class-name = org.h2.Driver +spring.datasource.username = sa +spring.datasource.password = +``` + +## Spring Boot 连接池化数据源 + +> 完整示例:[spring-boot-data-jdbc](https://github.com/dunwu/spring-tutorial/tree/master/codes/data/spring-boot-data-jdbc) + +在生产环境中,出于性能考虑,一般会通过数据库连接池连接数据源。 + +除了 [`DataSourceProperties`](https://github.com/spring-projects/spring-boot/tree/v2.7.4/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceProperties.java) 中的数据源通用配置以外,Spring Boot 还支持通过使用类似`spring.datasource.hikari.*`、`spring.datasource.tomcat.*`、`spring.datasource.dbcp2.*` 和 `spring.datasource.oracleucp.*` 的前缀来配置指定的数据库连接池属性。 + +下面,就是一份 hikari 的连接池配置示例: + +```properties +# 连接池名称 +spring.datasource.hikari.pool-name = SpringTutorialHikariPool +# 最大连接数,小于等于 0 会被重置为默认值 10;大于零小于 1 会被重置为 minimum-idle 的值 +spring.datasource.hikari.maximum-pool-size = 10 +# 最小空闲连接,默认值10,小于 0 或大于 maximum-pool-size,都会重置为 maximum-pool-size +spring.datasource.hikari.minimum-idle = 10 +# 连接超时时间(单位:毫秒),小于 250 毫秒,会被重置为默认值 30 秒 +spring.datasource.hikari.connection-timeout = 60000 +# 空闲连接超时时间,默认值 600000(10分钟),大于等于 max-lifetime 且 max-lifetime>0,会被重置为0;不等于 0 且小于 10 秒,会被重置为 10 秒 +# 只有空闲连接数大于最大连接数且空闲时间超过该值,才会被释放 +spring.datasource.hikari.idle-timeout = 600000 +# 连接最大存活时间,不等于 0 且小于 30 秒,会被重置为默认值 30 分钟。该值应该比数据库所设置的超时时间短 +spring.datasource.hikari.max-lifetime = 540000 +``` + +Spring Boot 会按以下顺序检测连接池是否可用,如果可用就选择对应的池化 `DataSource`: + +HikariCP -> Tomcat pooling DataSource -> DBCP2 -> Oracle UCP + +用户也可以通过 `spring.datasource.type` 来指定数据源类型。 + +此外,也可以使用 `DataSourceBuilder` 手动配置其他连接池。如果自定义 DataSource bean,则不会发生自动配置。 `DataSourceBuilder` 支持以下连接池: + +- HikariCP +- Tomcat pooling `Datasource` +- Commons DBCP2 +- Oracle UCP & `OracleDataSource` +- Spring Framework’s `SimpleDriverDataSource` +- H2 `JdbcDataSource` +- PostgreSQL `PGSimpleDataSource` +- C3P0 + +### 引入 Spring Boot 依赖 + +你可以通过 Spring Boot 官方的初始化器([Spring Initializr](https://start.spring.io/))选择需要的组件来创建一个 Spring Boot 工程。或者,直接在 pom.xml 中引入所需要的依赖: + +```xml + + org.springframework.boot + spring-boot-starter-data-jdbc + + + mysql + mysql-connector-java + +``` + +### 测试单数据源连接 + +```java +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.CommandLineRunner; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.jdbc.core.JdbcTemplate; + +import java.sql.Connection; +import javax.sql.DataSource; + +@Slf4j +@SpringBootApplication +public class SpringBootDataJdbcApplication implements CommandLineRunner { + + private final JdbcTemplate jdbcTemplate; + + public SpringBootDataJdbcApplication(JdbcTemplate jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; + } + + public static void main(String[] args) { + SpringApplication.run(SpringBootDataJdbcApplication.class, args); + } + + @Override + public void run(String... args) throws Exception { + DataSource dataSource = jdbcTemplate.getDataSource(); + + Connection connection; + if (dataSource != null) { + connection = dataSource.getConnection(); + } else { + log.error("连接数据源失败!"); + return; + } + + if (connection != null) { + log.info("数据源 Url: {}", connection.getMetaData().getURL()); + } else { + log.error("连接数据源失败!"); + } + } + +} +``` + +运行 `main` 方法后,控制台会输出以下内容,表示数据源连接成功: + +``` +20:50:18.449 [main] [INFO ] i.g.d.s.d.SpringBootDataJdbcApplication.run - 数据源 Url: jdbc:mysql://localhost:3306/spring_tutorial?serverTimezone=UTC&useUnicode=true&characterEncoding=utf8 +``` + +## Spring Boot 连接多数据源 + +> 完整示例:[spring-boot-data-jdbc-multi-datasource](https://github.com/dunwu/spring-tutorial/tree/master/codes/data/spring-boot-data-jdbc-multi-datasource) + +Spring Boot 连接多数据源所需要的依赖并无不同,主要差异在于数据源的配置。Spring Boot 默认的数据源配置类为 `org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration`。使用者只要指定一些必要的 spring.datasource 配置,`DataSourceAutoConfiguration` 类就会自动完成剩下的数据源实例化工作。 + +### 多数据源配置 + +下面的示例中,自定义了一个数据源配置类,通过读取不同的 spring.datasource.xxx 来完成对于不同数据源的实例化工作。对于 JDBC 来说,最重要的,就是实例化 `DataSource` 和 `JdbcTemplate`。 + +```java +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.jdbc.DataSourceBuilder; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; +import org.springframework.jdbc.core.JdbcTemplate; + +@Configuration +public class DataSourceConfig { + + @Primary + @Bean("mysqlDataSource") + @ConfigurationProperties(prefix = "spring.datasource.mysql") + public DataSource mysqlDataSource() { + return DataSourceBuilder.create().build(); + } + + @Primary + @Bean("mysqlJdbcTemplate") + public JdbcTemplate mysqlJdbcTemplate(@Qualifier("mysqlDataSource") DataSource dataSource) { + return new JdbcTemplate(dataSource); + } + + @Bean("h2DataSource") + @ConfigurationProperties(prefix = "spring.datasource.h2") + public DataSource h2DataSource() { + return DataSourceBuilder.create().build(); + } + + @Bean(name = "h2JdbcTemplate") + public JdbcTemplate h2JdbcTemplate(@Qualifier("h2DataSource") DataSource dataSource) { + return new JdbcTemplate(dataSource); + } + +} +``` + +`application.properties` 或 `application.yml` 配置文件中也必须以 `@ConfigurationProperties` 所指定的配置前缀进行配置: + +```properties +# 数据源一:Mysql +spring.datasource.mysql.jdbc-url = jdbc:mysql://localhost:3306/spring_tutorial?serverTimezone=UTC&useUnicode=true&characterEncoding=utf8&useSSL=false +spring.datasource.mysql.driver-class-name = com.mysql.cj.jdbc.Driver +spring.datasource.mysql.username = root +spring.datasource.mysql.password = root +# 数据源一:H2 +spring.datasource.h2.jdbc-url = jdbc:h2:mem:test +spring.datasource.h2.driver-class-name = org.h2.Driver +spring.datasource.h2.username = sa +spring.datasource.h2.password = +``` + +### 测试多数据源连接 + +```java + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.CommandLineRunner; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.jdbc.core.JdbcTemplate; + +import java.sql.Connection; +import java.sql.SQLException; +import javax.sql.DataSource; + +@SpringBootApplication +public class SpringBootDataJdbcMultiDataSourceApplication implements CommandLineRunner { + + private static final Logger log = LoggerFactory.getLogger(SpringBootDataJdbcMultiDataSourceApplication.class); + + private final UserDao mysqlUserDao; + + private final UserDao h2UserDao; + + public SpringBootDataJdbcMultiDataSourceApplication(@Qualifier("mysqlUserDao") UserDao mysqlUserDao, + @Qualifier("h2UserDao") UserDao h2UserDao) { + this.mysqlUserDao = mysqlUserDao; + this.h2UserDao = h2UserDao; + } + + public static void main(String[] args) { + SpringApplication.run(SpringBootDataJdbcMultiDataSourceApplication.class, args); + } + + @Override + public void run(String... args) throws Exception { + + if (mysqlUserDao != null && mysqlUserDao.getJdbcTemplate() != null) { + printDataSourceInfo(mysqlUserDao.getJdbcTemplate()); + log.info("Connect to mysql datasource success."); + } else { + log.error("Connect to mysql datasource failed!"); + return; + } + + if (h2UserDao != null) { + printDataSourceInfo(h2UserDao.getJdbcTemplate()); + log.info("Connect to h2 datasource success."); + } else { + log.error("Connect to h2 datasource failed!"); + return; + } + + // 主数据源执行 JDBC SQL + mysqlUserDao.recreateTable(); + + // 次数据源执行 JDBC SQL + h2UserDao.recreateTable(); + } + + private void printDataSourceInfo(JdbcTemplate jdbcTemplate) throws SQLException { + + DataSource dataSource = jdbcTemplate.getDataSource(); + + Connection connection; + if (dataSource != null) { + connection = dataSource.getConnection(); + } else { + log.error("Get dataSource failed!"); + return; + } + + if (connection != null) { + log.info("DataSource Url: {}", connection.getMetaData().getURL()); + } else { + log.error("Connect to datasource failed!"); + } + } + +} +``` + +运行 `main` 方法后,控制台会输出以下内容,表示数据源连接成功: + +``` +21:16:44.654 [main] [INFO ] i.g.d.s.d.SpringBootDataJdbcMultiDataSourceApplication.printDataSourceInfo - DataSource Url: jdbc:mysql://localhost:3306/spring_tutorial?serverTimezone=UTC&useUnicode=true&characterEncoding=utf8&useSSL=false +21:16:44.654 [main] [INFO ] i.g.d.s.d.SpringBootDataJdbcMultiDataSourceApplication.run - Connect to mysql datasource success. + +21:16:44.726 [main] [INFO ] i.g.d.s.d.SpringBootDataJdbcMultiDataSourceApplication.printDataSourceInfo - DataSource Url: jdbc:h2:mem:test +21:16:44.726 [main] [INFO ] i.g.d.s.d.SpringBootDataJdbcMultiDataSourceApplication.run - Connect to h2 datasource success. +``` + +## Spring 之数据源 + +如果你的项目是传统的 Spring 项目,当然也可以轻松建立数据源连接,只是需要自行设置的配置更多一些。 + +### 引入 Spring 依赖 + +在 pom.xml 中引入所需要的依赖: + +```xml + + + com.alibaba + druid + + + mysql + mysql-connector-java + + + + org.springframework + spring-context-support + + + org.springframework + spring-jdbc + + + org.springframework + spring-tx + + + +``` + +### Spring 配置数据源 + +Spring 配置数据源有多种方式,下面一一列举: + +#### 使用 JNDI 数据源 + +如果 Spring 应用部署在支持 JNDI 的 WEB 服务器上(如 WebSphere、JBoss、Tomcat 等),就可以使用 JNDI 获取数据源。 + +```xml + + + + + + + + + + + +``` + +#### 使用数据库连接池 + +Spring 本身并没有提供数据库连接池的实现,需要自行选择合适的数据库连接池。下面是一个使用 [Druid](https://github.com/alibaba/druid) 作为数据库连接池的示例: + +```xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +``` + +#### 基于 JDBC 驱动的数据源 + +```xml + + + + + + +``` + +## SpringBoot 数据源配置 + +> Spring Boot 数据库配置官方文档:https://docs.spring.io/spring-boot/docs/current/reference/html/data.html#data.sql + +通过前面的实战,我们已经知道了 Spring、Spring Boot 是如何连接数据源,并通过 JDBC 方式访问数据库。 + +SpringBoot 数据源的配置方式是在 `application.properties` 或 `application.yml` 文件中指定 `spring.datasource.*` 的配置。 + +(1)数据源基本配置方式是指定 url、用户名、密码 + +```properties +spring.datasource.url=jdbc:mysql://localhost/test +spring.datasource.username=dbuser +spring.datasource.password=dbpass +``` + +(2)配置 JNDI + +如果想要通过 JNDI 方式连接数据源,可以采用如下方式: + +```properties +spring.datasource.jndi-name=java:jboss/datasources/customers +``` + +## DataSourceAutoConfiguration 类 + +显而易见,Spring Boot 的配置更加简化,那么, Spring Boot 做了哪些工作,使得接入更加便捷呢?奥秘就在于 `spring-boot-autoconfigure` jar 包,其中定义了大量的 Spring Boot 自动配置类。其中,与数据库访问相关的比较核心的配置类有: + +- `DataSourceAutoConfiguration`:数据源自动配置类 +- `JdbcTemplateAutoConfiguration`:`JdbcTemplate` 自动配置类 +- `DataSourceTransactionManagerAutoConfiguration`:数据源事务管理自动配置类 +- `JndiDataSourceAutoConfiguration`:JNDI 数据源自动配置类 +- `EmbeddedDataSourceConfiguration`:嵌入式数据库数据源自动配置类 +- 等等 + +这些自动配置类会根据各种条件控制核心类的实例化。 + +`DataSourceAutoConfiguration` 是数据源自动配置类,它负责实例化 `DataSource`。 + +`DataSourceAutoConfiguration` 的源码如下(省略部分代码): + +```java +@AutoConfiguration(before = SqlInitializationAutoConfiguration.class) +@ConditionalOnClass({ DataSource.class, EmbeddedDatabaseType.class }) +@ConditionalOnMissingBean(type = "io.r2dbc.spi.ConnectionFactory") +@EnableConfigurationProperties(DataSourceProperties.class) +@Import(DataSourcePoolMetadataProvidersConfiguration.class) +public class DataSourceAutoConfiguration { + + @Configuration(proxyBeanMethods = false) + @Conditional(EmbeddedDatabaseCondition.class) + @ConditionalOnMissingBean({ DataSource.class, XADataSource.class }) + @Import(EmbeddedDataSourceConfiguration.class) + protected static class EmbeddedDatabaseConfiguration { + } + + @Configuration(proxyBeanMethods = false) + @Conditional(PooledDataSourceCondition.class) + @ConditionalOnMissingBean({ DataSource.class, XADataSource.class }) + @Import({ DataSourceConfiguration.Hikari.class, DataSourceConfiguration.Tomcat.class, + DataSourceConfiguration.Dbcp2.class, DataSourceConfiguration.OracleUcp.class, + DataSourceConfiguration.Generic.class, DataSourceJmxConfiguration.class }) + protected static class PooledDataSourceConfiguration { + } + + static class PooledDataSourceCondition extends AnyNestedCondition { + // 略 + } + + static class PooledDataSourceAvailableCondition extends SpringBootCondition { + // 略 + } + + static class EmbeddedDatabaseCondition extends SpringBootCondition { + // 略 + } +} +``` + +`DataSourceAutoConfiguration` 类的源码解读: + +- `DataSourceProperties` 是 `DataSourceAutoConfiguration` 的配置选项类,允许使用者通过设置选项控制 `DataSource` 初始化行为。 +- `DataSourceAutoConfiguration` 通过 `@Import` 注解引入 `DataSourcePoolMetadataProvidersConfiguration` 类。 +- `DataSourceAutoConfiguration` 中定义了两个内部类:嵌入式数据源配置类 `EmbeddedDatabaseConfiguration` 和 池化数据源配置类 `PooledDataSourceConfiguration`,分别标记了不同的实例化条件。 + - 当满足 `EmbeddedDatabaseConfiguration` 的示例化条件时,将引入 `EmbeddedDataSourceConfiguration` 类初始化数据源,这个类实际上是加载嵌入式数据源驱动的 ClassLoader 去进行初始化。 + - 当满足 `PooledDataSourceConfiguration` 的示例化条件时,将引入 `DataSourceConfiguration.Hikari.class`、`DataSourceConfiguration.Tomcat.class`、`DataSourceConfiguration.Dbcp2.class`、`DataSourceConfiguration.OracleUcp.class`、`DataSourceConfiguration.Generic.class`、`DataSourceJmxConfiguration.class` 这些配置类,分别对应不同的数据库连接池方式。具体选用哪种数据库连接池,可以通过 `spring.datasource.type` 配置指定。其中,Hikari 是 Spring Boot 默认的数据库连接池,spring-boot-starter-data-jdbc 中内置了 Hikari 连接池驱动包。如果想要替换其他数据库连接池,前提是必须先手动引入对应的连接池驱动包。 + +## 参考资料 + +- [Spring 官网](https://spring.io/) +- [Spring Framework 官方文档](https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/index.html) +- [Spring Boot 官方文档](https://docs.spring.io/spring-boot/docs/current/reference/html/data.html) \ No newline at end of file diff --git "a/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/02.Spring\346\225\260\346\215\256/02.Spring\344\271\213JDBC.md" "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/02.Spring\346\225\260\346\215\256/02.Spring\344\271\213JDBC.md" new file mode 100644 index 00000000..e1990392 --- /dev/null +++ "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/02.Spring\346\225\260\346\215\256/02.Spring\344\271\213JDBC.md" @@ -0,0 +1,768 @@ +--- +title: Spring 之 JDBC +date: 2019-02-18 14:33:55 +order: 02 +categories: + - Java + - 框架 + - Spring + - Spring数据 +tags: + - Java + - 框架 + - Spring + - SpringBoot + - JDBC + - JdbcTemplate +permalink: /pages/cf19fd/ +--- + +# Spring 之 JDBC + +JDBC 是 Java 语言中用来规范客户端程序如何访问数据库的应用程序接口,提供了增、删、改、查数据库的方法。 + +## JDBC 入门示例 + +JDBC 的工作步骤大致如下: + +1. 创建实体类。 +2. 声明数据库读写接口的 DAO 接口。定义 DAO 的好处在于对于数据层上层的业务,调用 DAO 时仅关注对外暴露的读写方法,而不考虑底层的具体持久化方式。这样,便于替换持久化方式。 +3. 创建一个 DAO 接口的实现类,使用 Spring 的 JDBC 模板去实现接口。 +4. 最后,定义一个 DAO 接口的实现类的 JavaBean,并将数据源注入进去。 + +假设,我们要通过 Spring + JDBC 访问一张 Mysql 数据表 `user`,`user` 表的数据结构如下: + +```sql +-- 创建用户表 +CREATE TABLE `user` ( + `id` BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 'ID', + `name` VARCHAR(255) NOT NULL DEFAULT '' COMMENT '用户名', + `age` INT(3) NOT NULL DEFAULT 0 COMMENT '年龄', + `address` VARCHAR(255) NOT NULL DEFAULT '' COMMENT '地址', + `email` VARCHAR(255) NOT NULL DEFAULT '' COMMENT '邮件', + PRIMARY KEY (`id`), + UNIQUE (`name`) +) COMMENT = '用户表'; + +INSERT INTO `user` (`name`, `age`, `address`, `email`) +VALUES ('张三', 18, '北京', 'xxx@163.com'); +INSERT INTO `user` (`name`, `age`, `address`, `email`) +VALUES ('李四', 19, '上海', 'xxx@163.com'); +``` + +### 定义实体 + +```java +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.ToString; + +import java.util.Objects; + +@Data +@ToString +@NoArgsConstructor +@AllArgsConstructor +public class User { + private Long id; + private String name; + private Integer age; + private String address; + private String email; +} +``` + +### 定义 DAO 接口 + +```java +import org.springframework.jdbc.core.JdbcTemplate; + +import java.util.List; + +/** + * user 表 Dao 接口 + * + * @author Zhang Peng + * @since 2019-11-18 + */ +public interface UserDao { + + // DML + // ------------------------------------------------------------------- + void insert(User user); + + void batchInsert(List users); + + void deleteByName(String name); + + void deleteAll(); + + void update(User user); + + Integer count(); + + List list(); + + User queryByName(String name); + + JdbcTemplate getJdbcTemplate(); + + // DDL + // ------------------------------------------------------------------- + void truncate(); + + void recreateTable(); + +} +``` + +### 定义 DAO 实现类 + +通过 `JdbcTemplate` 执行对应数据源符合语法的 SQL,即可完成各种数据库访问。 + +```java +package io.github.dunwu.springboot.core.data.jdbc; + +import org.springframework.dao.EmptyResultDataAccessException; +import org.springframework.jdbc.core.BeanPropertyRowMapper; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; + +import java.util.ArrayList; +import java.util.List; + +/** + * user 表 Dao 接口实现类 + * + * @author Zhang Peng + * @since 2019-11-18 + */ +@Repository +public class UserDaoImpl implements UserDao { + + private JdbcTemplate jdbcTemplate; + + public UserDaoImpl(JdbcTemplate jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; + } + + @Override + public void insert(User user) { + jdbcTemplate.update("INSERT INTO user(name, age, address, email) VALUES(?, ?, ?, ?)", + user.getName(), user.getAge(), user.getAddress(), user.getEmail()); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void batchInsert(List users) { + String sql = "INSERT INTO user(name, age, address, email) VALUES(?, ?, ?, ?)"; + + List params = new ArrayList<>(); + + users.forEach(user -> { + params.add(new Object[] { user.getName(), user.getAge(), user.getAddress(), user.getEmail() }); + }); + jdbcTemplate.batchUpdate(sql, params); + } + + @Override + public void deleteByName(String name) { + jdbcTemplate.update("DELETE FROM user WHERE name = ?", name); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteAll() { + jdbcTemplate.execute("DELETE FROM user"); + } + + @Override + public void update(User user) { + jdbcTemplate.update("UPDATE user SET name=?, age=?, address=?, email=? WHERE id=?", + user.getName(), user.getAge(), user.getAddress(), user.getEmail(), user.getId()); + } + + @Override + public Integer count() { + try { + return jdbcTemplate.queryForObject("SELECT COUNT(*) FROM user", Integer.class); + } catch (EmptyResultDataAccessException e) { + return null; + } + } + + @Override + public List list() { + return jdbcTemplate.query("SELECT * FROM user", new BeanPropertyRowMapper<>(User.class)); + } + + @Override + public User queryByName(String name) { + try { + return jdbcTemplate.queryForObject("SELECT * FROM user WHERE name = ?", + new BeanPropertyRowMapper<>(User.class), name); + } catch (EmptyResultDataAccessException e) { + return null; + } + } + + @Override + public JdbcTemplate getJdbcTemplate() { + return jdbcTemplate; + } + + @Override + public void truncate() { + jdbcTemplate.execute("TRUNCATE TABLE user"); + } + + @Override + public void recreateTable() { + jdbcTemplate.execute("DROP TABLE IF EXISTS user"); + + String sqlStatement = + "CREATE TABLE IF NOT EXISTS user (\n" + + " id BIGINT(10) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 'Id',\n" + + " name VARCHAR(255) NOT NULL DEFAULT '' COMMENT '用户名',\n" + + " age INT(3) NOT NULL DEFAULT 0 COMMENT '年龄',\n" + + " address VARCHAR(255) NOT NULL DEFAULT '' COMMENT '地址',\n" + + " email VARCHAR(255) NOT NULL DEFAULT '' COMMENT '邮件',\n" + + " PRIMARY KEY (id)\n" + + ") COMMENT = '用户表';"; + jdbcTemplate.execute(sqlStatement); + } + +} +``` + +### 测试类 + +```java +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.annotation.Rollback; + +import java.util.ArrayList; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +@Slf4j +@Rollback +@SpringBootTest(classes = { SpringBootDataJdbcApplication.class }) +public class DataJdbcMysqlDataSourceTest { + + @Autowired + private UserDao userDAO; + + @BeforeEach + public void before() { + userDAO.truncate(); + } + + @Test + public void insert() { + userDAO.insert(new User("张三", 18, "北京", "user1@163.com")); + User linda = userDAO.queryByName("张三"); + assertThat(linda).isNotNull(); + } + + @Test + public void batchInsert() { + List users = new ArrayList<>(); + users.add(new User("张三", 18, "北京", "user1@163.com")); + users.add(new User("李四", 19, "上海", "user1@163.com")); + users.add(new User("王五", 18, "南京", "user1@163.com")); + users.add(new User("赵六", 20, "武汉", "user1@163.com")); + + userDAO.batchInsert(users); + int count = userDAO.count(); + assertThat(count).isEqualTo(4); + + List list = userDAO.list(); + assertThat(list).isNotEmpty().hasSize(4); + list.forEach(user -> { + log.info(user.toString()); + }); + } + + @Test + public void delete() { + List users = new ArrayList<>(); + users.add(new User("张三", 18, "北京", "user1@163.com")); + users.add(new User("李四", 19, "上海", "user1@163.com")); + users.add(new User("王五", 18, "南京", "user1@163.com")); + users.add(new User("赵六", 20, "武汉", "user1@163.com")); + userDAO.batchInsert(users); + + userDAO.deleteByName("张三"); + User user = userDAO.queryByName("张三"); + assertThat(user).isNull(); + + userDAO.deleteAll(); + List list = userDAO.list(); + assertThat(list).isEmpty(); + } + + @Test + public void update() { + userDAO.insert(new User("张三", 18, "北京", "user1@163.com")); + User oldUser = userDAO.queryByName("张三"); + oldUser.setName("张三丰"); + userDAO.update(oldUser); + User newUser = userDAO.queryByName("张三丰"); + assertThat(newUser).isNotNull(); + } + +} +``` + +## Spring Boot JDBC + +> 完整示例:[spring-boot-data-jdbc](https://github.com/dunwu/spring-tutorial/tree/master/codes/data/jdbc/spring-boot-data-jdbc) + +### 引入 Spring Boot 依赖 + +你可以通过 Spring Boot 官方的初始化器([Spring Initializr](https://start.spring.io/))选择需要的组件来创建一个 Spring Boot 工程。或者,直接在 pom.xml 中引入所需要的依赖: + +```xml + + org.springframework.boot + spring-boot-starter-parent + 2.7.7 + + + + + org.springframework.boot + spring-boot-starter-data-jdbc + + + mysql + mysql-connector-java + + +``` + +### 配置数据源 + +引入依赖后,需要在 `application.properties` 或 `application.yml` 文件中指定数据源配置。 + +下面是一个最基本的数据源配置示例: + +```properties +spring.datasource.url = jdbc:mysql://localhost:3306/spring_tutorial?serverTimezone=UTC&useUnicode=true&characterEncoding=utf8 +spring.datasource.driver-class-name = com.mysql.cj.jdbc.Driver +spring.datasource.username = root +spring.datasource.password = root +``` + +需要根据实际情况,替换 `url`、`username`、`password`。 + +### 测试 + +```java +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.CommandLineRunner; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.jdbc.core.JdbcTemplate; + +import java.sql.Connection; +import javax.sql.DataSource; + +@Slf4j +@SpringBootApplication +public class SpringBootDataJdbcApplication implements CommandLineRunner { + + private final JdbcTemplate jdbcTemplate; + + public SpringBootDataJdbcApplication(JdbcTemplate jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; + } + + public static void main(String[] args) { + SpringApplication.run(SpringBootDataJdbcApplication.class, args); + } + + @Override + public void run(String... args) throws Exception { + DataSource dataSource = jdbcTemplate.getDataSource(); + + Connection connection; + if (dataSource != null) { + connection = dataSource.getConnection(); + } else { + log.error("连接数据源失败!"); + return; + } + + if (connection != null) { + log.info("数据源 Url: {}", connection.getMetaData().getURL()); + } else { + log.error("连接数据源失败!"); + } + } + +} +``` + +运行 `main` 方法后,控制台会输出以下内容,表示数据源连接成功: + +``` +20:50:18.449 [main] [INFO ] i.g.d.s.d.SpringBootDataJdbcApplication.run - 数据源 Url: jdbc:mysql://localhost:3306/spring_tutorial?serverTimezone=UTC&useUnicode=true&characterEncoding=utf8 +``` + +## Spring JDBC + +> 完整示例:[spring-data-jdbc](https://github.com/dunwu/spring-tutorial/tree/master/codes/data/jdbc/spring-data-jdbc) + +`spring-boot-starter-data-jdbc` 引入了 `spring-jdbc` ,其 JDBC 特性就是基于 `spring-jdbc`。 + +### 引入 Spring 依赖 + +在 pom.xml 中引入所需要的依赖: + +```xml + + + com.alibaba + druid + + + mysql + mysql-connector-java + + + + org.springframework + spring-context-support + + + org.springframework + spring-jdbc + + + org.springframework + spring-tx + + + +``` + +### 基于 JDBC 驱动的数据源配置 + +下面是一个 mysql 的 JDBC 数据源配置实例: + +```xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +``` + +### 测试 + +```java + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.context.support.ClassPathXmlApplicationContext; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +import java.io.IOException; +import java.sql.SQLException; + +@SuppressWarnings("all") +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(locations = { "classpath:data/spring-mysql.xml" }) +public class MysqlJdbcTest { + + @Autowired + private ApplicationContext ctx; + + @Before + public void before() { + ctx = JdbcDemo.getMysqlApplicationContext(); + } + + @Test + public void testExecJdbcOper() throws SQLException, IOException { + UserDao userDao = (UserDaoImpl) ctx.getBean("userDao"); + JdbcDemo.execJdbcOper(userDao); + } + + @After + public void after() { + ((ClassPathXmlApplicationContext) ctx).close(); + } + +} +``` + +## JdbcTemplate API + +Spring 将数据访问的样板式代码提取到模板类中。Spring 提供了 3 个 JDBC 模板类: + +- `JdbcTemplate`:最基本的 Spring JDBC 模板,这个模板支持最简单的 JDBC 数据库访问功能以及简单的索引参数查询。 +- `SimpleJdbcTemplate`:改模板类利用 Java 5 的一些特性,如自动装箱、泛型以及可变参数列表来简化 JDBC 模板的使用。 +- `NamedParameterJdbcTemplate`:使用该模板类执行查询时,可以将查询值以命名参数的形式绑定到 SQL 中,而不是使用简单的索引参数。 + +`spring-jdbc` 最核心的 API 无疑就是 `JdbcTemplate`,可以说所有的 JDBC 数据访问,几乎都是围绕着这个类去工作的。Spring 对数据库的操作在 Jdbc 层面做了深层次的封装,利用依赖注入,把数据源配置装配到 `JdbcTemplate` 中,再由 `JdbcTemplate` 负责具体的数据访问。 + +`JdbcTemplate` 主要提供以下几类方法: + +- `execute` 方法:可以用于执行任何 SQL 语句,一般用于执行 DDL 语句; +- `update` 方法及 `batchUpdate` 方法:`update` 方法用于执行新增、修改、删除等语句;`batchUpdate` 方法用于执行批处理相关语句; +- `query` 方法及 `queryForXXX` 方法:用于执行查询相关语句; +- `call` 方法:用于执行存储过程、函数相关语句。 + +为了方便演示,以下增删改查操作都围绕一个名为 user 的表(该表的主键 id 是自增序列)进行,该表的数据实体如下: + +```java +public class User { + private Integer id; + private String name; + private Integer age; + + // 省略 getter/setter +} +``` + +数据实体只要是一个纯粹的 Java Bean 即可,无需任何注解修饰。 + +### execute 方法 + +使用 execute 执行 DDL 语句,创建一个名为 test 的数据库,并在此数据库下新建一个名为 user 的表。 + +```java +public void recreateTable() { + jdbcTemplate.execute("DROP DATABASE IF EXISTS test"); + jdbcTemplate.execute("CREATE DATABASE test"); + jdbcTemplate.execute("USE test"); + jdbcTemplate.execute("DROP TABLE if EXISTS user"); + jdbcTemplate.execute("DROP TABLE if EXISTS user"); + // @formatter:off + StringBuilder sb = new StringBuilder(); + sb.append("CREATE TABLE user (id int (10) unsigned NOT NULL AUTO_INCREMENT,\n") + .append("name varchar (64) NOT NULL DEFAULT '',\n") + .append("age tinyint (3) NOT NULL DEFAULT 0,\n") + .append("PRIMARY KEY (ID));\n"); + // @formatter:on + jdbcTemplate.execute(sb.toString()); +} +``` + +### update 方法 + +新增数据 + +```java +public void insert(String name, Integer age) { + jdbcTemplate.update("INSERT INTO user(name, age) VALUES(?, ?)", name, age); +} +``` + +删除数据 + +```java +public void delete(String name) { + jdbcTemplate.update("DELETE FROM user WHERE name = ?", name); +} +``` + +修改数据 + +```java +public void update(User user) { + jdbcTemplate.update("UPDATE USER SET name=?, age=? WHERE id=?", user.getName(), user.getAge(), user.getId()); +} +``` + +批处理 + +```java +public void batchInsert(List users) { + String sql = "INSERT INTO user(name, age) VALUES(?, ?)"; + + List params = new ArrayList<>(); + + users.forEach(item -> { + params.add(new Object[] {item.getName(), item.getAge()}); + }); + jdbcTemplate.batchUpdate(sql, params); +} +``` + +### query 方法 + +查单个对象 + +```java +public User queryByName(String name) { + try { + return jdbcTemplate + .queryForObject("SELECT * FROM user WHERE name = ?", new BeanPropertyRowMapper<>(User.class), name); + } catch (EmptyResultDataAccessException e) { + return null; + } +} +``` + +查多个对象 + +```java +public List list() { + return jdbcTemplate.query("select * from USER", new BeanPropertyRowMapper(User.class)); +} +``` + +获取某个记录某列或者 count、avg、sum 等函数返回唯一值 + +```java +public Integer count() { + try { + return jdbcTemplate.queryForObject("SELECT COUNT(*) FROM user", Integer.class); + } catch (EmptyResultDataAccessException e) { + return null; + } +} +``` + +## SpringBoot JDBC 配置 + +### JdbcTemplateAutoConfiguration 类 + +`JdbcTemplateAutoConfiguration` 是 `JdbcTemplate` 自动配置类,它负责实例化 `JdbcTemplate`。 + +```java +@Configuration(proxyBeanMethods = false) +@ConditionalOnClass({ DataSource.class, JdbcTemplate.class }) +@ConditionalOnSingleCandidate(DataSource.class) +@AutoConfigureAfter(DataSourceAutoConfiguration.class) +@EnableConfigurationProperties(JdbcProperties.class) +@Import({ JdbcTemplateConfiguration.class, NamedParameterJdbcTemplateConfiguration.class }) +public class JdbcTemplateAutoConfiguration { + +} +``` + +`JdbcTemplateAutoConfiguration` 类的源码解读: + +- `@AutoConfigureAfter(DataSourceAutoConfiguration.class)` 表明 `JdbcTemplateAutoConfiguration` 必须在 `DataSourceAutoConfiguration` 执行完之后才开始工作,这意味着:`JdbcTemplate` 的初始化必须在 `DataSource` 初始化之后。 +- `JdbcProperties` 是 `JdbcTemplateAutoConfiguration` 的配置选项类,允许使用者通过设置选项控制 `JdbcTemplate` 初始化行为。 +- `@Import({ JdbcTemplateConfiguration.class, NamedParameterJdbcTemplateConfiguration.class })` 表明引入 `JdbcTemplateConfiguration`、`NamedParameterJdbcTemplateConfiguration` 两个配置类,具体的实例化 `JdbcTemplate` 的工作也是放在这两个配置中完成。 + +### JdbcTemplateConfiguration 类 + +`JdbcTemplateConfiguration` 源码如下: + +```java +@Configuration(proxyBeanMethods = false) +@ConditionalOnMissingBean(JdbcOperations.class) +class JdbcTemplateConfiguration { + + @Bean + @Primary + JdbcTemplate jdbcTemplate(DataSource dataSource, JdbcProperties properties) { + JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource); + JdbcProperties.Template template = properties.getTemplate(); + jdbcTemplate.setFetchSize(template.getFetchSize()); + jdbcTemplate.setMaxRows(template.getMaxRows()); + if (template.getQueryTimeout() != null) { + jdbcTemplate.setQueryTimeout((int) template.getQueryTimeout().getSeconds()); + } + return jdbcTemplate; + } + +} +``` + +`JdbcTemplateConfiguration` 源码解读:`JdbcTemplateConfiguration` 中根据 `DataSource` 和 `JdbcProperties` 实例化了一个 `JdbcTemplate`。 + +### NamedParameterJdbcTemplateConfiguration 类 + +`NamedParameterJdbcTemplateConfiguration` 源码如下: + +```java +@Configuration(proxyBeanMethods = false) +@ConditionalOnSingleCandidate(JdbcTemplate.class) +@ConditionalOnMissingBean(NamedParameterJdbcOperations.class) +class NamedParameterJdbcTemplateConfiguration { + + @Bean + @Primary + NamedParameterJdbcTemplate namedParameterJdbcTemplate(JdbcTemplate jdbcTemplate) { + return new NamedParameterJdbcTemplate(jdbcTemplate); + } + +} +``` + +`NamedParameterJdbcTemplateConfiguration` 源码解读:`NamedParameterJdbcTemplateConfiguration` 中根据 `JdbcTemplate` 实例化了一个 `NamedParameterJdbcTemplate`。 + +## spring-data-jdbc + +Spring Data 项目包含了对 JDBC 的存储库支持,并将自动为 `CrudRepository` 上的方法生成 SQL。对于更高级的查询,提供了 `@Query` 注解。 + +当 classpath 上存在必要的依赖项时,Spring Boot 将自动配置 Spring Data 的 JDBC 存储库。它们可以通过 `spring-boot-starter-data-jdbc` 的单一依赖项添加到项目中。如有必要,可以通过将 `@EnableJdbcRepositories` 批注或 `JdbcConfiguration` 子类添加到应用程序来控制 Spring Data JDBC 的配置。 + +> 更多 Spring Data JDBC 细节,可以参考 [Spring Data JDBC 官方文档](http://spring.io/projects/spring-data-jdbc)。 + +## 参考资料 + +- [Spring 官网](https://spring.io/) +- [Spring Framework 官方文档](https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/index.html) +- [Spring Boot 官方文档](https://docs.spring.io/spring-boot/docs/current/reference/html/data.html) \ No newline at end of file diff --git "a/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/02.Spring\346\225\260\346\215\256/03.Spring\344\271\213\344\272\213\345\212\241.md" "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/02.Spring\346\225\260\346\215\256/03.Spring\344\271\213\344\272\213\345\212\241.md" new file mode 100644 index 00000000..5a9b29fe --- /dev/null +++ "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/02.Spring\346\225\260\346\215\256/03.Spring\344\271\213\344\272\213\345\212\241.md" @@ -0,0 +1,1272 @@ +--- +title: Spring 之事务 +date: 2022-09-22 07:46:49 +order: 03 +categories: + - Java + - 框架 + - Spring + - Spring数据 +tags: + - Java + - 框架 + - Spring + - SpringBoot + - 事务 +permalink: /pages/128c54/ +--- + +# Spring 之事务 + +Spring 针对 Java Transaction API (JTA)、JDBC、Hibernate 和 Java Persistence API(JPA) 等事务 API,实现了一致的编程模型,而 Spring 的声明式事务功能更是提供了极其方便的事务配置方式,配合 Spring Boot 的自动配置,大多数 Spring Boot 项目只需要在方法上标记 `@Transactional` 注解,即可一键开启方法的事务性配置。 + +## 理解事务 + +在软件开发领域,全有或全无的操作被称为**事务(transaction)**。事务允许你将几个操作组合成一个要么全部发生要么全部不发生的工作单元。传统上 Java EE 开发对事务管理有两种选择:**全局事务**或**本地事务**,两者都有很大的局限性。 + +### 事务的特性 + +事务应该具有 4 个属性:原子性、一致性、隔离性、持久性。这四个属性通常称为 **ACID**。 + +- **原子性(Atomic)**:一个事务是一个不可分割的工作单位,事务中包括的诸操作要么都做,要么都不做。 +- **一致性(Consistent)**:事务必须是使数据库从一个一致性状态变到另一个一致性状态。一致性与原子性是密切相关的。 +- **隔离性(Isolated)**:一个事务的执行不能被其他事务干扰。即一个事务内部的操作及使用的数据对并发的其他事务是隔离的,并发执行的各个事务之间不能互相干扰。 +- **持久性(Durable)**:持久性也称永久性(permanence),指一个事务一旦提交,它对数据库中数据的改变就应该是永久性的。接下来的其他操作或故障不应该对其有任何影响。 + +### 全局事务 + +全局事务允许您使用多个事务资源,通常是关系数据库和消息队列。应用服务器通过 JTA 管理全局事务,这是一个繁琐的 API(部分原因在于其异常模型)。此外,JTA UserTransaction 通常需要来自 JNDI,这意味着您还需要使用 JNDI 才能使用 JTA。全局事务的使用限制了应用程序代码的任何潜在重用,因为 JTA 通常仅在应用程序服务器环境中可用。 + +以前,使用全局事务的首选方式是通过 EJB CMT(容器管理事务)。 CMT 是一种声明式事务管理(不同于程序化事务管理)。 EJB CMT 消除了对与事务相关的 JNDI 查找的需要,尽管使用 EJB 本身就需要使用 JNDI。它消除了大部分(但不是全部)编写 Java 代码来控制事务的需要。其明显的缺点是 CMT 与 JTA 和应用程序服务器环境相关联。此外,它仅在选择在 EJB 中实现业务逻辑(或至少在事务性 EJB 外观之后)时才可用。一般来说,EJB 的负面影响是如此之大,以至于这不是一个有吸引力的提议,尤其是在面对声明式事务管理的引人注目的替代方案时。 + +### 本地事务 + +本地事务是指定资源的,例如与 JDBC 连接关联的事务。本地事务可能更容易使用,但有一个明显的缺点:它们不能跨多个事务资源工作。例如,使用 JDBC 连接管理事务的代码不能在全局 JTA 事务中运行。因为应用服务器不参与事务管理,它不能确保跨多个资源的正确性(值得注意的是,大多数应用程序使用单个事务资源。)。另一个缺点是本地事务对编程模型具有侵入性。 + +### Spring 对事务的支持 + +Spring 通过回调机制将实际的事务实现从事务性的代码中抽象出来。Spring 解决了全局和本地事务的缺点。它允许开发人员在任何环境中使用一致的编程模型。您只需编写一次代码,它就可以从不同环境中的不同事务管理策略中受益。Spring 提供了对编码式和声明式事务管理的支持,大多数情况下都推荐使用声明式事务管理。 + +- 编码式事务允许用户在代码中精确定义事务的边界 +- 声明式事务(基于 AOP)有助于用户将操作与事务规则进行解耦 + +通过程序化事务管理,开发人员可以使用 Spring 事务抽象,它可以在任何底层事务基础上运行。使用首选的声明性模型,开发人员通常编写很少或根本不编写与事务管理相关的代码,因此不依赖 Spring 事务 API 或任何其他事务 API。 + +### Spring 事务的优点 + +Spring 框架为事务管理提供了一致的抽象,具有以下好处: + +- 跨不同事务 API 的一致编程模型,例如 Java Transaction API (JTA)、JDBC、Hibernate 和 Java Persistence API (JPA)。 +- 支持声明式事务管理。 +- 用于编程事务管理的 API 比复杂事务 API(如 JTA)更简单。 +- 与 Spring 的数据访问抽象完美集成。 + +## 核心 API + +### TransactionManager + +Spring 事务抽象的关键是事务策略的概念。事务策略由 `TransactionManager` 定义,特别是用于命令式事务管理的 `org.springframework.transaction.PlatformTransactionManager` 接口和用于响应式事务管理的 `org.springframework.transaction.ReactiveTransactionManager` 接口。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/20220922073737.png) + +#### PlatformTransactionManager + +以下清单显示了 `PlatformTransactionManager` API 的定义: + +```java +public interface PlatformTransactionManager extends TransactionManager { + + TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException; + + void commit(TransactionStatus status) throws TransactionException; + + void rollback(TransactionStatus status) throws TransactionException; +} +``` + +`PlatformTransactionManager` 是一个 SPI 接口,所以使用者可以以编程方式使用它。因为 `PlatformTransactionManager` 是一个接口,所以可以根据需要轻松地 MOCK 或存根。它不依赖于查找策略,例如 JNDI。 `PlatformTransactionManager` 实现的定义与 Spring IoC 容器中的任何其他对象(或 bean)一样。仅此一项优势就使 Spring 事务成为有价值的抽象,即使您使用 JTA 也是如此。与直接使用 JTA 相比,您可以更轻松地测试事务代码。 + +同样,为了与 Spring 的理念保持一致,任何 `PlatformTransactionManager` 接口的方法可以抛出的 `TransactionException` 都是未经检查的(也就是说,它扩展了 `java.lang.RuntimeException` 类)。事务架构故障几乎总是致命的。极少数情况下,应用程序可以从事务失败中恢复,开发人员可以选择捕获和处理 `TransactionException`。重点是开发人员并非被迫这样做。 + +`getTransaction(..)` 方法根据 `TransactionDefinition` 参数返回一个 `TransactionStatus` 对象。如果当前调用堆栈中存在匹配的事务,则返回的 `TransactionStatus` 可能表示新事务或可以表示现有事务。后一种情况的含义是,与 Java EE 事务上下文一样,`TransactionStatus` 与执行线程相关联。 + +从以上可以看出,具体的事务管理机制对 Spring 来说是透明的,它并不关心那些,那些是对应各个平台需要关心的,所以 Spring 事务管理的一个优点就是为不同的事务 API 提供一致的编程模型,如 JTA、JDBC、Hibernate、JPA。下面分别介绍各个平台框架实现事务管理的机制。 + +#### JDBC 事务 + +如果应用程序中直接使用 JDBC 来进行持久化,`DataSourceTransactionManager` 会为你处理事务边界。为了使用 `DataSourceTransactionManager`,你需要使用如下的 XML 将其装配到应用程序的上下文定义中: + +```xml + + + +``` + +实际上,`DataSourceTransactionManager` 是通过调用 `java.sql.Connection` 来管理事务,而后者是通过 `DataSource` 获取到的。通过调用连接的 `commit()` 方法来提交事务,同样,事务失败则通过调用 `rollback()` 方法进行回滚。 + +#### Hibernate 事务 + +如果应用程序的持久化是通过 Hibernate 实现的,那么你需要使用 `HibernateTransactionManager`。对于 Hibernate3,需要在 Spring 上下文定义中添加如下的 `bean` 声明: + +```xml + + + +``` + +`sessionFactory` 属性需要装配一个 Hibernate 的 session 工厂,`HibernateTransactionManager` 的实现细节是它将事务管理的职责委托给 `org.hibernate.Transaction` 对象,而后者是从 Hibernate Session 中获取到的。当事务成功完成时,`HibernateTransactionManager` 将会调用 `Transaction` 对象的 `commit()` 方法,反之,将会调用 `rollback()` 方法。 + +#### Java 持久化 API 事务(JPA) + +Hibernate 多年来一直是事实上的 Java 持久化标准,但是现在 Java 持久化 API 作为真正的 Java 持久化标准进入大家的视野。如果你计划使用 JPA 的话,那你需要使用 Spring 的 `JpaTransactionManager` 来处理事务。你需要在 Spring 中这样配置 `JpaTransactionManager`: + +```xml + + + +``` + +`JpaTransactionManager` 只需要装配一个 JPA 实体管理工厂(`javax.persistence.EntityManagerFactory` 接口的任意实现)。`JpaTransactionManager` 将与由工厂所产生的 JPA EntityManager 合作来构建事务。 + +#### Java 原生 API 事务(JTA) + +如果你没有使用以上所述的事务管理,或者是跨越了多个事务管理源(比如两个或者是多个不同的数据源),你就需要使用`JtaTransactionManager`: + +```xml + + + +``` + +`JtaTransactionManager` 将事务管理的责任委托给 `javax.transaction.UserTransaction` 和 `javax.transaction.TransactionManager` 对象,其中事务成功完成通过 `UserTransaction.commit()` 方法提交,事务失败通过 `UserTransaction.rollback()` 方法回滚。 + +#### ReactiveTransactionManager + +Spring 还为使用响应式类型或 Kotlin 协程的响应式应用程序提供了事务管理抽象。以下清单显示了 `org.springframework.transaction.ReactiveTransactionManager` 定义的事务策略: + +```java +public interface ReactiveTransactionManager extends TransactionManager { + + Mono getReactiveTransaction(TransactionDefinition definition) throws TransactionException; + + Mono commit(ReactiveTransaction status) throws TransactionException; + + Mono rollback(ReactiveTransaction status) throws TransactionException; +} +``` + +响应式事务管理器主要是一个 SPI,所以使用者可以以编程方式使用它。因为 `ReactiveTransactionManager` 是一个接口,所以可以根据需要轻松地 MOCK 或存根。 + +### TransactionDefinition + +`PlatformTransactionManager` 通过 `getTransaction(TransactionDefinition definition)` 方法来得到事务,这个方法里面的参数是 `TransactionDefinition` 类,这个类就定义了一些基本的事务属性。事务属性可以理解成事务的一些基本配置,描述了事务策略如何应用到方法上。 + +`TransactionDefinition` 接口内容如下: + +```java +public interface TransactionDefinition { + int getPropagationBehavior(); // 返回事务的传播行为 + int getIsolationLevel(); // 返回事务的隔离级别,事务管理器根据它来控制另外一个事务可以看到本事务内的哪些数据 + int getTimeout(); // 返回事务必须在多少秒内完成 + boolean isReadOnly(); // 事务是否只读,事务管理器能够根据这个返回值进行优化,确保事务是只读的 +} +``` + +我们可以发现 `TransactionDefinition` 正好用来定义事务属性,下面详细介绍一下各个事务属性。 + +#### 传播行为 + +事务的传播行为(propagation behavior)是指:当事务方法被另一个事务方法调用时,必须指定事务应该如何传播。例如:方法可能继续在现有事务中运行,也可能开启一个新事务,并在自己的事务中运行。Spring 定义了七种传播行为: + +| 传播行为 | 含义 | +| --------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `PROPAGATION_REQUIRED` | 表示当前方法必须运行在事务中。如果当前事务存在,方法将会在该事务中运行。否则,会启动一个新的事务 | +| `PROPAGATION_SUPPORTS` | 表示当前方法不需要事务上下文,但是如果存在当前事务的话,那么该方法会在这个事务中运行 | +| `PROPAGATION_MANDATORY` | 表示该方法必须在事务中运行,如果当前事务不存在,则会抛出一个异常 | +| `PROPAGATION_REQUIRED_NEW` | 表示当前方法必须运行在它自己的事务中。一个新的事务将被启动。如果存在当前事务,在该方法执行期间,当前事务会被挂起。如果使用 JTATransactionManager 的话,则需要访问 TransactionManager | +| `PROPAGATION_NOT_SUPPORTED` | 表示该方法不应该运行在事务中。如果存在当前事务,在该方法运行期间,当前事务将被挂起。如果使用 JTATransactionManager 的话,则需要访问 TransactionManager | +| `PROPAGATION_NEVER` | 表示当前方法不应该运行在事务上下文中。如果当前正有一个事务在运行,则会抛出异常 | +| `PROPAGATION_NESTED` | 表示如果当前已经存在一个事务,那么该方法将会在嵌套事务中运行。嵌套的事务可以独立于当前事务进行单独地提交或回滚。如果当前事务不存在,那么其行为与 PROPAGATION_REQUIRED 一样。注意各厂商对这种传播行为的支持是有所差异的。可以参考资源管理器的文档来确认它们是否支持嵌套事务 | + +_注:以下具体讲解传播行为的内容参考自 Spring 事务机制详解_ + +1. PROPAGATION_REQUIRED 如果存在一个事务,则支持当前事务。如果没有事务则开启一个新的事务。 + +```java +// 事务属性 PROPAGATION_REQUIRED +methodA { + …… + methodB(); + …… +} +``` + +```java +// 事务属性 PROPAGATION_REQUIRED +methodB { + …… +} +``` + +使用 spring 声明式事务,spring 使用 AOP 来支持声明式事务,会根据事务属性,自动在方法调用之前决定是否开启一个事务,并在方法执行之后决定事务提交或回滚事务。 + +单独调用 methodB 方法: + +```java +main { + metodB(); +} +``` + +相当于 + +```java +Main { + Connection con=null; + try{ + con = getConnection(); + con.setAutoCommit(false); + + //方法调用 + methodB(); + + //提交事务 + con.commit(); + } Catch(RuntimeException ex) { + //回滚事务 + con.rollback(); + } finally { + //释放资源 + closeCon(); + } +} +``` + +Spring 保证在 methodB 方法中所有的调用都获得到一个相同的连接。在调用 methodB 时,没有一个存在的事务,所以获得一个新的连接,开启了一个新的事务。 +单独调用 MethodA 时,在 MethodA 内又会调用 MethodB. + +执行效果相当于: + +```java +main{ + Connection con = null; + try{ + con = getConnection(); + methodA(); + con.commit(); + } catch(RuntimeException ex) { + con.rollback(); + } finally { + closeCon(); + } +} +``` + +调用 MethodA 时,环境中没有事务,所以开启一个新的事务.当在 MethodA 中调用 MethodB 时,环境中已经有了一个事务,所以 methodB 就加入当前事务。 + +2. `PROPAGATION_SUPPORTS` 如果存在一个事务,支持当前事务。如果没有事务,则非事务的执行。但是对于事务同步的事务管理器,`PROPAGATION_SUPPORTS` 与不使用事务有少许不同。 + +```java +//事务属性 PROPAGATION_REQUIRED +methodA(){ + methodB(); +} + +//事务属性 PROPAGATION_SUPPORTS +methodB(){ + …… +} +``` + +单纯的调用 methodB 时,methodB 方法是非事务的执行的。当调用 methdA 时,methodB 则加入了 methodA 的事务中,事务地执行。 + +3. `PROPAGATION_MANDATORY` 如果已经存在一个事务,支持当前事务。如果没有一个活动的事务,则抛出异常。 + +``` +//事务属性 PROPAGATION_REQUIRED +methodA(){ + methodB(); +} + +//事务属性 PROPAGATION_MANDATORY + methodB(){ + …… +} +``` + +当单独调用 methodB 时,因为当前没有一个活动的事务,则会抛出异常 throw new IllegalTransactionStateException(“Transaction propagation ‘mandatory’ but no existing transaction found”);当调用 methodA 时,methodB 则加入到 methodA 的事务中,事务地执行。 + +4. `PROPAGATION_REQUIRES_NEW` 总是开启一个新的事务。如果一个事务已经存在,则将这个存在的事务挂起。 + +``` +//事务属性 PROPAGATION_REQUIRED +methodA(){ + doSomeThingA(); + methodB(); + doSomeThingB(); +} + +//事务属性 PROPAGATION_REQUIRES_NEW +methodB(){ + …… +} +``` + +调用 A 方法: + +``` +main(){ + methodA(); +} +``` + +相当于 + +``` +main(){ + TransactionManager tm = null; + try{ + //获得一个JTA事务管理器 + tm = getTransactionManager(); + tm.begin();//开启一个新的事务 + Transaction ts1 = tm.getTransaction(); + doSomeThing(); + tm.suspend();//挂起当前事务 + try{ + tm.begin();//重新开启第二个事务 + Transaction ts2 = tm.getTransaction(); + methodB(); + ts2.commit();//提交第二个事务 + } Catch(RunTimeException ex) { + ts2.rollback();//回滚第二个事务 + } finally { + //释放资源 + } + //methodB执行完后,恢复第一个事务 + tm.resume(ts1); + doSomeThingB(); + ts1.commit();//提交第一个事务 + } catch(RunTimeException ex) { + ts1.rollback();//回滚第一个事务 + } finally { + //释放资源 + } +} +``` + +在这里,我把 ts1 称为外层事务,ts2 称为内层事务。从上面的代码可以看出,ts2 与 ts1 是两个独立的事务,互不相干。Ts2 是否成功并不依赖于 ts1。如果 methodA 方法在调用 methodB 方法后的 doSomeThingB 方法失败了,而 methodB 方法所做的结果依然被提交。而除了 methodB 之外的其它代码导致的结果却被回滚了。使用 PROPAGATION_REQUIRES_NEW,需要使用 JtaTransactionManager 作为事务管理器。 + +5. `PROPAGATION_NOT_SUPPORTED` 总是非事务地执行,并挂起任何存在的事务。使用 PROPAGATION_NOT_SUPPORTED,也需要使用 JtaTransactionManager 作为事务管理器。(代码示例同上,可同理推出) +6. PROPAGATION_NEVER 总是非事务地执行,如果存在一个活动事务,则抛出异常。 +7. PROPAGATION_NESTED 如果一个活动的事务存在,则运行在一个嵌套的事务中. 如果没有活动事务, 则按 TransactionDefinition.PROPAGATION_REQUIRED 属性执行。这是一个嵌套事务,使用 JDBC 3.0 驱动时,仅仅支持 DataSourceTransactionManager 作为事务管理器。需要 JDBC 驱动的 java.sql.Savepoint 类。有一些 JTA 的事务管理器实现可能也提供了同样的功能。使用 PROPAGATION_NESTED,还需要把 PlatformTransactionManager 的 nestedTransactionAllowed 属性设为 true;而 nestedTransactionAllowed 属性值默认为 false。 + +``` +//事务属性 PROPAGATION_REQUIRED +methodA(){ + doSomeThingA(); + methodB(); + doSomeThingB(); +} + +//事务属性 PROPAGATION_NESTED +methodB(){ + …… +} +``` + +如果单独调用 methodB 方法,则按 REQUIRED 属性执行。如果调用 methodA 方法,相当于下面的效果: + +``` +main(){ + Connection con = null; + Savepoint savepoint = null; + try{ + con = getConnection(); + con.setAutoCommit(false); + doSomeThingA(); + savepoint = con2.setSavepoint(); + try{ + methodB(); + } catch(RuntimeException ex) { + con.rollback(savepoint); + } finally { + //释放资源 + } + doSomeThingB(); + con.commit(); + } catch(RuntimeException ex) { + con.rollback(); + } finally { + //释放资源 + } +} +``` + +当 methodB 方法调用之前,调用 setSavepoint 方法,保存当前的状态到 savepoint。如果 methodB 方法调用失败,则恢复到之前保存的状态。但是需要注意的是,这时的事务并没有进行提交,如果后续的代码(doSomeThingB()方法)调用失败,则回滚包括 methodB 方法的所有操作。 + +嵌套事务一个非常重要的概念就是内层事务依赖于外层事务。外层事务失败时,会回滚内层事务所做的动作。而内层事务操作失败并不会引起外层事务的回滚。 + +PROPAGATION_NESTED 与 PROPAGATION_REQUIRES_NEW 的区别:它们非常类似,都像一个嵌套事务,如果不存在一个活动的事务,都会开启一个新的事务。使用 PROPAGATION_REQUIRES_NEW 时,内层事务与外层事务就像两个独立的事务一样,一旦内层事务进行了提交后,外层事务不能对其进行回滚。两个事务互不影响。两个事务不是一个真正的嵌套事务。同时它需要 JTA 事务管理器的支持。 + +使用 PROPAGATION_NESTED 时,外层事务的回滚可以引起内层事务的回滚。而内层事务的异常并不会导致外层事务的回滚,它是一个真正的嵌套事务。DataSourceTransactionManager 使用 savepoint 支持 PROPAGATION_NESTED 时,需要 JDBC 3.0 以上驱动及 1.4 以上的 JDK 版本支持。其它的 JTA TrasactionManager 实现可能有不同的支持方式。 + +PROPAGATION_REQUIRES_NEW 启动一个新的, 不依赖于环境的 “内部” 事务. 这个事务将被完全 commited 或 rolled back 而不依赖于外部事务, 它拥有自己的隔离范围, 自己的锁, 等等. 当内部事务开始执行时, 外部事务将被挂起, 内务事务结束时, 外部事务将继续执行。 + +另一方面, PROPAGATION_NESTED 开始一个 “嵌套的” 事务, 它是已经存在事务的一个真正的子事务. 潜套事务开始执行时, 它将取得一个 savepoint. 如果这个嵌套事务失败, 我们将回滚到此 savepoint. 潜套事务是外部事务的一部分, 只有外部事务结束后它才会被提交。 + +由此可见, PROPAGATION_REQUIRES_NEW 和 PROPAGATION_NESTED 的最大区别在于, PROPAGATION_REQUIRES_NEW 完全是一个新的事务, 而 PROPAGATION_NESTED 则是外部事务的子事务, 如果外部事务 commit, 嵌套事务也会被 commit, 这个规则同样适用于 roll back. + +PROPAGATION_REQUIRED 应该是我们首先的事务传播行为。它能够满足我们大多数的事务需求。 + +#### 隔离级别 + +事务的第二个维度就是隔离级别(isolation level)。隔离级别定义了一个事务可能受其他并发事务影响的程度。 + +1. 并发事务引起的问题 + +在典型的应用程序中,多个事务并发运行,经常会操作相同的数据来完成各自的任务。并发虽然是必须的,但可能会导致一下的问题。 + +- 脏读(Dirty reads)——脏读发生在一个事务读取了另一个事务改写但尚未提交的数据时。如果改写在稍后被回滚了,那么第一个事务获取的数据就是无效的。 +- 不可重复读(Nonrepeatable read)——不可重复读发生在一个事务执行相同的查询两次或两次以上,但是每次都得到不同的数据时。这通常是因为另一个并发事务在两次查询期间进行了更新。 +- 幻读(Phantom read)——幻读与不可重复读类似。它发生在一个事务(T1)读取了几行数据,接着另一个并发事务(T2)插入了一些数据时。在随后的查询中,第一个事务(T1)就会发现多了一些原本不存在的记录。 + +**不可重复读与幻读的区别** + +不可重复读的重点是修改: +同样的条件, 你读取过的数据, 再次读取出来发现值不一样了 +例如:在事务 1 中,Mary 读取了自己的工资为 1000,操作并没有完成 + +``` + con1 = getConnection(); + select salary from employee empId ="Mary"; +``` + +在事务 2 中,这时财务人员修改了 Mary 的工资为 2000,并提交了事务. + +``` + con2 = getConnection(); + update employee set salary = 2000; + con2.commit(); +``` + +在事务 1 中,Mary 再次读取自己的工资时,工资变为了 2000 + +``` + //con1 + select salary from employee empId ="Mary"; +``` + +在一个事务中前后两次读取的结果并不一致,导致了不可重复读。 + +幻读的重点在于新增或者删除: +同样的条件, 第 1 次和第 2 次读出来的记录数不一样 +例如:目前工资为 1000 的员工有 10 人。事务 1,读取所有工资为 1000 的员工。 + +``` + con1 = getConnection(); + Select * from employee where salary =1000; +``` + +共读取 10 条记录 + +这时另一个事务向 employee 表插入了一条员工记录,工资也为 1000 + +``` + con2 = getConnection(); + Insert into employee(empId,salary) values("Lili",1000); + con2.commit(); +``` + +事务 1 再次读取所有工资为 1000 的员工 + +``` + //con1 + select * from employee where salary =1000; +``` + +共读取到了 11 条记录,这就产生了幻像读。 + +从总的结果来看, 似乎不可重复读和幻读都表现为两次读取的结果不一致。但如果你从控制的角度来看, 两者的区别就比较大。 +对于前者, 只需要锁住满足条件的记录。 +对于后者, 要锁住满足条件及其相近的记录。 + +2. 隔离级别 + +| 隔离级别 | 含义 | +| -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | +| ISOLATION_DEFAULT | 使用后端数据库默认的隔离级别 | +| ISOLATION_READ_UNCOMMITTED | 最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读 | +| ISOLATION_READ_COMMITTED | 允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生 | +| ISOLATION_REPEATABLE_READ | 对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生 | +| ISOLATION_SERIALIZABLE | 最高的隔离级别,完全服从 ACID 的隔离级别,确保阻止脏读、不可重复读以及幻读,也是最慢的事务隔离级别,因为它通常是通过完全锁定事务相关的数据库表来实现的 | + +#### 只读 + +事务的第三个特性是它是否为只读事务。如果事务只对后端的数据库进行该操作,数据库可以利用事务的只读特性来进行一些特定的优化。通过将事务设置为只读,你就可以给数据库一个机会,让它应用它认为合适的优化措施。 + +#### 事务超时 + +为了使应用程序很好地运行,事务不能运行太长的时间。因为事务可能涉及对后端数据库的锁定,所以长时间的事务会不必要的占用数据库资源。事务超时就是事务的一个定时器,在特定时间内事务如果没有执行完毕,那么就会自动回滚,而不是一直等待其结束。 + +#### 回滚规则 + +事务五边形的最后一个方面是一组规则,这些规则定义了哪些异常会导致事务回滚而哪些不会。默认情况下,事务只有遇到运行期异常时才会回滚,而在遇到检查型异常时不会回滚(这一行为与 EJB 的回滚行为是一致的) +但是你可以声明事务在遇到特定的检查型异常时像遇到运行期异常那样回滚。同样,你还可以声明事务遇到特定的异常不回滚,即使这些异常是运行期异常。 + +### TransactionStatus + +`TransactionStatus` 接口为事务代码提供了一种简单的方式来控制事务执行和查询事务状态。这些概念应该很熟悉,因为它们对所有事务 API 都是通用的。以下清单显示了 `TransactionStatus` 接口: + +```java +public interface TransactionStatus extends TransactionExecution, SavepointManager, Flushable { + + @Override + boolean isNewTransaction(); + + boolean hasSavepoint(); + + @Override + void setRollbackOnly(); + + @Override + boolean isRollbackOnly(); + + void flush(); + + @Override + boolean isCompleted(); +} +``` + +可以发现这个接口描述的是一些处理事务提供简单的控制事务执行和查询事务状态的方法,在回滚或提交的时候需要应用对应的事务状态。 + +### TransactionTemplate + +Spring 提供了对编程式事务和声明式事务的支持。编程式事务允许用户在代码中精确定义事务的边界,而声明式事务(基于 AOP)有助于用户将操作与事务规则进行解耦。TransactionTemplate 就是用于支持编程式事务的核心 API。 + +采用 TransactionTemplate 和采用其他 Spring 模板,如 JdbcTempalte 和 HibernateTemplate 是一样的方法。它使用回调方法,把应用程序从处理取得和释放资源中解脱出来。如同其他模板,TransactionTemplate 是线程安全的。代码片段: + +```java + TransactionTemplate tt = new TransactionTemplate(); // 新建一个TransactionTemplate + Object result = tt.execute( + new TransactionCallback(){ + public Object doTransaction(TransactionStatus status){ + updateOperation(); + return resultOfUpdateOperation(); + } + }); // 执行execute方法进行事务管理 +``` + +使用 TransactionCallback()可以返回一个值。如果使用 TransactionCallbackWithoutResult 则没有返回值。 + +## 声明式事务管理 + +> 大多数 Spring 用户选择声明式事务管理。此选项对应用程序代码的影响最小,因此最符合非侵入式轻量级容器的理想。 + +Spring 框架的声明式事务管理是通过 Spring AOP 实现的。然而,由于事务方面代码随 Spring 发行版一起提供并且可以以样板方式使用,因此通常不必理解 AOP 概念即可有效地使用此代码。 + +Spring 框架的声明式事务管理类似于 EJB CMT,因为您可以指定事务行为(或缺少它)到单个方法级别。如有必要,您可以在事务上下文中进行 `setRollbackOnly()` 调用。两种类型的事务管理之间的区别是: + +- 与绑定到 JTA 的 EJB CMT 不同,Spring 框架的声明式事务管理适用于任何环境。通过调整配置文件,它可以使用 JDBC、JPA 或 Hibernate 处理 JTA 事务或本地事务。 +- 您可以将 Spring 声明式事务管理应用于任何类,而不仅仅是诸如 EJB 之类的特殊类。 +- Spring 提供声明性回滚规则,这是一个没有 EJB 等效功能的特性。提供了对回滚规则的编程和声明性支持。 +- Spring 允许您使用 AOP 自定义事务行为。例如,您可以在事务回滚的情况下插入自定义行为。您还可以添加任意 advice 以及事务性 advice。使用 EJB CMT,您无法影响容器的事务管理,除非使用 `setRollbackOnly()`。 +- Spring 不像高端应用服务器那样支持跨远程调用传播事务上下文。如果您需要此功能,我们建议您使用 EJB。但是,在使用这种特性之前要仔细考虑,因为通常情况下,不希望事务跨越远程调用。 + +回滚规则的概念很重要。它们让您指定哪些异常(和 throwable)应该导致自动回滚。您可以在配置中以声明方式指定它,而不是在 Java 代码中。因此,尽管您仍然可以在 TransactionStatus 对象上调用 setRollbackOnly() 来回滚当前事务,但通常您可以指定 MyApplicationException 必须始终导致回滚的规则。此选项的显着优势是业务对象不依赖于事务基础架构。例如,它们通常不需要导入 Spring 事务 API 或其他 Spring API。 + +尽管 EJB 容器默认行为会在系统异常(通常是运行时异常)上自动回滚事务,但 EJB CMT 不会在应用程序异常(即除 java.rmi.RemoteException 之外的检查异常)上自动回滚事务。虽然声明式事务管理的 Spring 默认行为遵循 EJB 约定(回滚仅在未经检查的异常上自动),但自定义此行为通常很有用。 + +### Spring 声明式事务管理的实现 + +关于 Spring 框架的声明式事务支持,最重要的概念是这种支持是通过 AOP 代理启用的,并且事务 advice 是由元数据驱动的(目前是基于 XML 或基于注释的)。 AOP 与事务元数据的结合产生了一个 AOP 代理,它使用 `TransactionInterceptor` 和适当的 `TransactionManager` 实现来驱动围绕方法调用的事务。 + +Spring 的 `TransactionInterceptor` 为命令式和响应式编程模型提供事务管理。拦截器通过检查方法返回类型来检测所需的事务管理风格。返回响应式类型的方法,例如 Publisher 或 Kotlin Flow(或它们的子类型)有资格进行响应式事务管理。包括 void 在内的所有其他返回类型都使用代码路径进行命令式事务管理。 + +事务管理风格会影响需要哪个事务管理器。命令式事务需要 `PlatformTransactionManager`,而响应式事务使用 `ReactiveTransactionManager` 实现。 + +> `@Transactional` 通常与 `PlatformTransactionManager` 管理的线程绑定事务一起使用,将事务公开给当前执行线程中的所有数据访问操作。注意:这不会传播到方法中新启动的线程。 +> +> 由 `ReactiveTransactionManager` 管理的反应式事务使用 Reactor 上下文而不是线程本地属性。因此,所有参与的数据访问操作都需要在同一个反应式管道中的同一个 Reactor 上下文中执行。 + +下图显示了在事务代理上调用方法的概念视图: + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/20220927093737.png) + +### 声明式事务示例 + +考虑以下接口及其伴随的实现。此示例使用 Foo 和 Bar 类作为占位符,以便您可以专注于事务使用,而无需关注特定的域模型。就本示例而言,DefaultFooService 类在每个已实现方法的主体中抛出 `UnsupportedOperationException` 实例这一事实很好。该行为使您可以看到正在创建的事务,然后回滚以响应 `UnsupportedOperationException` 实例。 + +以下清单显示了 FooService 接口: + +```java +// the service interface that we want to make transactional + +package x.y.service; + +public interface FooService { + + Foo getFoo(String fooName); + + Foo getFoo(String fooName, String barName); + + void insertFoo(Foo foo); + + void updateFoo(Foo foo); + +} +``` + +以下示例显示了上述接口的实现: + +```java +package x.y.service; + +public class DefaultFooService implements FooService { + + @Override + public Foo getFoo(String fooName) { + // ... + } + + @Override + public Foo getFoo(String fooName, String barName) { + // ... + } + + @Override + public void insertFoo(Foo foo) { + // ... + } + + @Override + public void updateFoo(Foo foo) { + // ... + } +} +``` + +假设 FooService 接口的前两个方法 getFoo(String) 和 getFoo(String, String) 必须在具有只读语义的事务上下文中运行,并且其他方法 insertFoo(Foo) 和 updateFoo(Foo ),必须在具有读写语义的事务上下文中运行。以下配置将在接下来的几段中详细说明: + +```xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +``` + +检查前面的配置。它假定您要使服务对象 fooService bean 具有事务性。要应用的事务语义封装在 `` 定义中。`` 定义读作“所有以 get 开头的方法都将在只读事务的上下文中运行,所有其他方法都将以默认事务语义运行”。`` 标签的 `transaction-manager` 属性设置为将驱动事务的 TransactionManager bean 的名称(在本例中为 txManager bean)。 + +> 如果要连接的 TransactionManager 的 bean 名称具有名称 transactionManager,则可以省略事务 advice (tx:advice/) 中的 transaction-manager 属性。如果要连接的 TransactionManager bean 有任何其他名称,则必须显式使用 transaction-manager 属性,如前面的示例所示。 + +`` 定义确保由 `txAdvice` bean 定义的事务性建议在程序中的适当位置运行。首先,您定义一个切入点,该切入点与 `FooService` 接口 (fooServiceOperation) 中定义的任何操作的执行相匹配。然后,您使用一个 adviser 将切入点与 `txAdvice` 相关联。结果表明,在执行 fooServiceOperation 时,会运行 `txAdvice` 定义的建议。 + +一个常见的要求是使整个服务层具有事务性。最好的方法是更改切入点表达式以匹配服务层中的任何操作。以下示例显示了如何执行此操作: + +```xml + + + + +``` + +前面显示的配置用于围绕从 fooService bean 定义创建的对象创建事务代理。代理配置了事务 advice,以便在代理上调用适当的方法时,根据与该方法关联的事务配置,启动、暂停、标记为只读等事务。考虑以下测试驱动前面显示的配置的程序: + +```java +public final class Boot { + + public static void main(final String[] args) throws Exception { + ApplicationContext ctx = new ClassPathXmlApplicationContext("context.xml"); + FooService fooService = ctx.getBean(FooService.class); + fooService.insertFoo(new Foo()); + } +} +``` + +### 回滚一个声明性事务 + +Spring 框架中,触发事务回滚的推荐方式是在事务上下文的代码中抛出异常。Spring 事务框架会捕获任何未处理的异常,并确定是否将事务标记为回滚。 + +在其默认配置中,Spring 事务框架只会将存在运行时且未经检查异常的事务标记为回滚。也就是说,当抛出的异常是 `RuntimeException` 的实例或子类时。 (默认情况下,错误实例也会导致回滚)。从事务方法抛出的检查异常不会导致默认配置中的回滚。 + +您可以通过指定回滚规则,明确指定哪些异常类型将导致事务回滚。 + +> 回滚规则约定在抛出指定异常时是否应回滚事务,并且规则基于模式。模式可以是完全限定的类名或异常类型的完全限定类名的子字符串(必须是 `Throwable` 的子类),目前不支持通配符。例如,`javax.servlet.ServletException` 或 `ServletException` 的值将匹配 `javax.servlet.ServletException` 及其子类。 +> +> 回滚规则可以通过 `rollback-for` 和 `no-rollback-for` 属性在 XML 中配置,这允许将模式指定为字符串。使用 `@Transactional` 时,可以通过 `rollbackFor` / `noRollbackFor` 和`rollbackForClassName` / `noRollbackForClassName` 属性配置回滚规则,它们允许将模式分别指定为类引用或字符串。当异常类型被指定为类引用时,其完全限定名称将用作模式。因此,`@Transactional(rollbackFor = example.CustomException.class)` 等价于 `@Transactional(rollbackForClassName = 'example.CustomException')`。 + +以下 XML 片段演示了如何通过 `rollback-for` 属性提供异常模式来为已检查的、特定的 `Exception` 类型配置回滚: + +```xml + + + + + + +``` + +如果您不希望在抛出异常时回滚事务,您还可以指定“不回滚”规则。下面的例子告诉 Spring 事务框架,即使在面对未处理的 InstrumentNotFoundException 时也要提交伴随事务。 + +```xml + + + + + + +``` + +当 Spring Framework 事务框架捕获到异常,并检查配置的回滚规则以确定是否将事务标记为回滚时,由最重要的匹配规则决定。因此,在以下配置的情况下,除 `InstrumentNotFoundException` 之外的任何异常都会导致伴随事务的回滚。 + +```xml + + + + + +``` + +您还可以以编程方式指示所需的回滚。虽然很简单,但这个过程非常具有侵入性,并且将您的代码与 Spring Framework 的事务基础设施紧密耦合。以下示例显示如何以编程方式指示所需的回滚。 + +```java +public void resolvePosition() { + try { + // some business logic... + } catch (NoProductInStockException ex) { + // trigger rollback programmatically + TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(); + } +} +``` + +如果可能的话,强烈建议您使用声明性方法进行回滚。如果您绝对需要,可以使用程序化回滚,但它的使用与实现干净的基于 POJO 的架构背道而驰。 + +### 为不同的 Bean 配置不同的事务语义 + +考虑您有许多服务层对象的场景,并且您希望对每个对象应用完全不同的事务配置。您可以通过定义具有不同 `` 元素和不同 `advice-ref` 属性值的切点来实现这一点。 + +作为一个比较点,首先假设您的所有服务层类都定义在根 x.y.service 包中。 要使作为该包(或子包)中定义的类的实例并且名称以 Service 结尾的所有 bean 都具有默认的事务配置,您可以编写以下内容: + +```xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +``` + +以下示例显示了如何使用完全不同的事务设置配置两个不同的 bean + +```xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +``` + +### `` 配置 + +`` 的默认配置为: + +- 传播设置是 `REQUIRED` + +- 隔离级别为 `DEFAULT` + +- 事务是 read-write + +- 事务超时默认为底层事务系统的默认超时,如果不支持超时,则为无。 + +- 任何 `RuntimeException` 都会触发回滚,而任何已检查的 `Exception` 都不会 + +`` 配置属性 + +| 属性 | 是否必要 | 默认值 | 描述 | +| :---------------- | :------- | :--------- | :--------------------------------------------------------------------------- | +| `name` | Yes | | 与事务属性关联的方法名称。支持通配符,如:`get*`、`handle*`、`on*Event` | +| `propagation` | No | `REQUIRED` | 事务传播行为 | +| `isolation` | No | `DEFAULT` | 事务隔离级别。仅适用于 `REQUIRED` 或 `REQUIRES_NEW` 的传播设置。 | +| `timeout` | No | -1 | 事务超时时间(单位:秒)。仅适用于 `REQUIRED` 或 `REQUIRES_NEW` 的传播设置。 | +| `read-only` | No | false | read-write 或 read-only 事务。 | +| `rollback-for` | No | | 触发回滚的 `Exception` 实例列表(通过逗号分隔)。 | +| `no-rollback-for` | No | | 不触发回滚的 `Exception` 实例列表(通过逗号分隔)。 | + +### 使用 `@Transactional` 注解 + +除了基于 XML 的声明式事务配置方法之外,您还可以使用基于注解的方法。 + +下面是一个使用 `@Transactional` 注解的示例: + +```java +@Transactional +public class DefaultFooService implements FooService { + + @Override + public Foo getFoo(String fooName) { + // ... + } + + @Override + public Foo getFoo(String fooName, String barName) { + // ... + } + + @Override + public void insertFoo(Foo foo) { + // ... + } + + @Override + public void updateFoo(Foo foo) { + // ... + } +} +``` + +如上所述在类级别使用,`@Transactional` 注解表明声明类(及其子类)的所有方法都使用默认事务配置。 或者,可以单独为每个方法指定注解。请注意,类级别的注解不适用于类层次结构中的祖先类; 在这种情况下,继承的方法需要在本地重新声明才能参与子类级别的注解。 + +当上面的 POJO 类在 Spring 上下文中定义为 bean 时,您可以通过 `@Configuration` 类中的 `@EnableTransactionManagement` 注解使 bean 实例具有事务性。 + +在 XML 配置中, `` 标签提供了类似的便利: + +```xml + + + + + + + + + + + + + + + + + + + +``` + +#### `@Transactional` 配置 + +| Property | Type | Description | +| :------------------------------------------------------------------------------------------------------------------------------------- | :---------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------- | +| [value](https://docs.spring.io/spring-framework/docs/current/reference/html/data-access.html#tx-multiple-tx-mgrs-with-attransactional) | `String` | Optional qualifier that specifies the transaction manager to be used. | +| `transactionManager` | `String` | Alias for `value`. | +| `label` | Array of `String` labels to add an expressive description to the transaction. | Labels may be evaluated by transaction managers to associate implementation-specific behavior with the actual transaction. | +| [propagation](https://docs.spring.io/spring-framework/docs/current/reference/html/data-access.html#tx-propagation) | `enum`: `Propagation` | Optional propagation setting. | +| `isolation` | `enum`: `Isolation` | Optional isolation level. Applies only to propagation values of `REQUIRED` or `REQUIRES_NEW`. | +| `timeout` | `int` (in seconds of granularity) | Optional transaction timeout. Applies only to propagation values of `REQUIRED` or `REQUIRES_NEW`. | +| `timeoutString` | `String` (in seconds of granularity) | Alternative for specifying the `timeout` in seconds as a `String` value — for example, as a placeholder. | +| `readOnly` | `boolean` | Read-write versus read-only transaction. Only applicable to values of `REQUIRED` or `REQUIRES_NEW`. | +| `rollbackFor` | Array of `Class` objects, which must be derived from `Throwable.` | Optional array of exception types that must cause rollback. | +| `rollbackForClassName` | Array of exception name patterns. | Optional array of exception name patterns that must cause rollback. | +| `noRollbackFor` | Array of `Class` objects, which must be derived from `Throwable.` | Optional array of exception types that must not cause rollback. | +| `noRollbackForClassName` | Array of exception name patterns. | Optional array of exception name patterns that must not cause rollback. | + +#### 多事务管理器场景下使用 `@Transactional` + +某些情况下,应用程序中可能需要接入多个数据源,相应的,也需要多个独立的事务管理器。使用者可以使用 `@Transactional` 注释的 value 或 `transactionManager` 属性来选择性地指定要使用的 `TransactionManager` 的标识。这可以是 bean 名称或事务管理器 bean 的限定符值。 + +```java +public class TransactionalService { + + @Transactional("order") + public void setSomething(String name) { ... } + + @Transactional("account") + public void doSomething() { ... } + + @Transactional("reactive-account") + public Mono doSomethingReactive() { ... } +} +``` + +下面展示如何定义 `TransactionManager`: + +```xml + + + + ... + + + + + ... + + + + + ... + + +``` + +在这种情况下,`TransactionalService` 上的各个方法在单独的事务管理器下运行,由 order、account 和 reactive-account 限定符区分。 如果没有找到明确指定的 `TransactionManager` bean,则仍使用默认的 `` 目标 bean 名称。 + +#### 自定义组合注解 + +如果您发现在许多不同的方法上重复使用 `@Transactional` 相同的属性,可以使用 Spring 的元注解自定义组合注解。 + +```java +@Target({ElementType.METHOD, ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Transactional(transactionManager = "order", label = "causal-consistency") +public @interface OrderTx { +} + +@Target({ElementType.METHOD, ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Transactional(transactionManager = "account", label = "retryable") +public @interface AccountTx { +} +``` + +使用示例: + +```java +public class TransactionalService { + + @OrderTx + public void setSomething(String name) { + // ... + } + + @AccountTx + public void doSomething() { + // ... + } +} +``` + +在上面的示例中,我们使用语法来定义事务管理器限定符和事务标签,但我们也可以包括传播行为、回滚规则、超时和其他特性。 + +#### 事务传播 + +在 Spring 管理的事务中,请注意物理事务和逻辑事务之间的差异,以及传播设置如何应用于这种差异。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/20220928114544.png) + +`PROPAGATION_REQUIRED` 强制执行物理事务,如果尚不存在事务,则在当前范围的本地执行或参与更大范围定义的现有“外部”事务。 这是同一线程内的常见调用堆栈安排中的一个很好的默认设置(例如,委托给多个存储库方法的服务外观,其中所有底层资源都必须参与服务级事务)。 + +当传播设置为 PROPAGATION_REQUIRED 时,将为应用该设置的每个方法创建一个逻辑事务范围。每个这样的逻辑事务范围可以单独确定仅回滚状态,外部事务范围在逻辑上独立于内部事务范围。在标准 PROPAGATION_REQUIRED 行为的情况下,所有这些范围都映射到同一个物理事务。因此,在内部事务范围内设置的仅回滚标记确实会影响外部事务实际提交的机会。 + +但是,在内部事务范围设置了仅回滚标记的情况下,外部事务尚未决定回滚本身,因此回滚(由内部事务范围静默触发)是意外的。此时会引发相应的 `UnexpectedRollbackException`。这是预期的行为,因此事务的调用者永远不会被误导以为执行了提交,而实际上并没有执行。因此,如果内部事务(外部调用者不知道)默默地将事务标记为仅回滚,外部调用者仍会调用提交。外部调用者需要接收 `UnexpectedRollbackException` 以清楚地指示执行了回滚。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/20220928115243.png) + +PROPAGATION_REQUIRES_NEW 与 PROPAGATION_REQUIRED 相比,始终为每个受影响的事务范围使用独立的物理事务,从不参与外部范围的现有事务。 在这种安排下,底层资源事务是不同的,因此可以独立提交或回滚,外部事务不受内部事务回滚状态的影响,内部事务的锁在完成后立即释放。 这样一个独立的内部事务也可以声明自己的隔离级别、超时和只读设置,而不是继承外部事务的特性。 + +## JDBC 异常抽象 + +Spring 会将数据操作的异常转换为 `DataAccessException`。 + +Spring 是怎么认识那些错误码的 + +通过 SQLErrorCodeSQLExceptionTranslator 解析错误码 + +ErrorCode 定义(sql-error-codes.xml 文件) + +## Spring 事务最佳实践 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200805171418.png) + +### Spring 事务未生效 + +使用 `@Transactional` 注解开启声明式事务时, 最容易忽略的问题是,很可能事务并没有生效。 + +`@Transactional` 生效原则: + +#### @Transactional 方法必须是 public + +原则一:除非特殊配置(比如使用 AspectJ 静态织入实现 AOP),否则**只有定义在 `public` 方法上的 `@Transactional` 才能生效**。原因是,Spring 默认通过动态代理的方式实现 AOP,对目标方法进行增强,private 方法无法代理到,Spring 自然也无法动态增强事务处理逻辑。 + +【示例】错误使用 `@Transactional` 案例一 + +```java + @Transactional + void createUserPrivate(UserEntity entity) { + userRepository.save(entity); + if (entity.getName().contains("test")) { throw new RuntimeException("invalid username!"); } + } + + //私有方法 + public int createUserWrong1(String name) { + try { + this.createUserPrivate(new UserEntity(name)); + } catch (Exception ex) { + log.error("create user failed because {}", ex.getMessage()); + } + return userRepository.findByName(name).size(); + } +``` + +当传入名为 test 的用户实体,会抛出异常,但 `@Transactional` 未生效,不会触发回滚。 + +#### 必须通过 Spring 注入的 Bean 进行调用 + +原则二:**必须通过代理过的类从外部调用目标方法才能生效**。 + +【示例】错误使用 `@Transactional` 案例二 + +```java + //自调用 + public int createUserWrong2(String name) { + try { + this.createUserPublic(new UserEntity(name)); + } catch (Exception ex) { + log.error("create user failed because {}", ex.getMessage()); + } + return userRepository.findByName(name).size(); + } + + //可以传播出异常 + @Transactional + public void createUserPublic(UserEntity entity) { + userRepository.save(entity); + if (entity.getName().contains("test")) { throw new RuntimeException("invalid username!"); } + } +``` + +当传入名为 test 的用户实体,会抛出异常,但 `@Transactional` 未生效,不会触发回滚。 + +说明:Spring 通过 AOP 技术对方法进行字节码增强,要调用增强过的方法必然是调用代理后的对象。 + +### 事务虽然生效但未回滚 + +通过 AOP 实现事务处理可以理解为,使用 `try…catch…` 来包裹标记了 `@Transactional` 注解的方法,当方法出现了异常并且满足**一定条件**的时候,在 `catch` 里面我们可以设置事务回滚,没有异常则直接提交事务。 + +“一定条件”,主要包括两点: + +第一,只有异常传播出了标记了 @Transactional 注解的方法,事务才能回滚。在 Spring 的 TransactionAspectSupport 里有个 invokeWithinTransaction 方法,里面就是处理事务的逻辑。 + +第二,默认情况下,**出现 RuntimeException(非受检异常)或 Error 的时候,Spring 才会回滚事务**。 + +```java +@Service +@Slf4j +public class UserService { + + @Autowired + private UserRepository userRepository; + + //异常无法传播出方法,导致事务无法回滚 + @Transactional + public void createUserWrong1(String name) { + try { + userRepository.save(new UserEntity(name)); + throw new RuntimeException("error"); + } catch (Exception ex) { + log.error("create user failed", ex); + } + } + + //即使出了受检异常也无法让事务回滚 + @Transactional + public void createUserWrong2(String name) throws IOException { + userRepository.save(new UserEntity(name)); + otherTask(); + } + + //因为文件不存在,一定会抛出一个IOException + private void otherTask() throws IOException { + Files.readAllLines(Paths.get("file-that-not-exist")); + } + +} +``` + +在 createUserWrong1 方法中会抛出一个 RuntimeException,但由于方法内 catch 了所有异常,异常无法从方法传播出去,事务自然无法回滚。 + +在 createUserWrong2 方法中,注册用户的同时会有一次 otherTask 文件读取操作,如果文件读取失败,我们希望用户注册的数据库操作回滚。虽然这里没有捕获异常,但因为 otherTask 方法抛出的是受检异常,createUserWrong2 传播出去的也是受检异常,事务同样不会回滚。 + +【解决方案一】如果你希望自己捕获异常进行处理的话,也没关系,**可以手动设置 `TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();` 让当前事务处于回滚状态**: + +```java +@Transactional +public void createUserRight1(String name) { + try { + userRepository.save(new UserEntity(name)); + throw new RuntimeException("error"); + } catch (Exception ex) { + log.error("create user failed", ex); + TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(); + } +} +``` + +【解决方案二】在注解中声明 `@Transactional(rollbackFor = Exception.class)`,期望遇到所有的 Exception 都回滚事务(来突破默认不回滚受检异常的限制): + +```java +@Transactional(rollbackFor = Exception.class) +public void createUserRight2(String name) throws IOException { + userRepository.save(new UserEntity(name)); + otherTask(); +} +``` + +### 细化事务传播方式 + +如果方法涉及多次数据库操作,并希望将它们作为独立的事务进行提交或回滚,那么 +我们需要考虑进一步细化配置事务传播方式,也就是 `@Transactional` 注解的 `Propagation` 属性。 + +```java +/** + * {@link Propagation#REQUIRES_NEW} 表示执行到这个方法时需要开启新的事务,并挂起当前事务 + */ +@Transactional(propagation = Propagation.REQUIRES_NEW) +public void createSubUserWithExceptionRight(UserEntity entity) { + log.info("createSubUserWithExceptionRight start"); + userRepository.save(entity); + throw new RuntimeException("invalid status"); +} +``` + +## 参考资料 + +- [Spring 官网](https://spring.io/) +- [Spring 官方文档](https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/index.html) +- [Spring Boot 官方文档](https://docs.spring.io/spring-boot/docs/current/reference/html/data.html) +- [《Java 业务开发常见错误 100 例》](https://time.geekbang.org/column/intro/100047701) \ No newline at end of file diff --git "a/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/02.Spring\346\225\260\346\215\256/04.Spring\344\271\213JPA.md" "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/02.Spring\346\225\260\346\215\256/04.Spring\344\271\213JPA.md" new file mode 100644 index 00000000..8d362789 --- /dev/null +++ "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/02.Spring\346\225\260\346\215\256/04.Spring\344\271\213JPA.md" @@ -0,0 +1,609 @@ +--- +title: Spring 之 JPA +date: 2019-02-18 14:33:55 +order: 04 +categories: + - Java + - 框架 + - Spring + - Spring数据 +tags: + - Java + - 框架 + - Spring + - SpringBoot + - JPA +permalink: /pages/a03d7b/ +--- + +# Spring 之 JPA + +JPA 为对象关系映射提供了一种基于 POJO 的持久化模型。 + +- 简化数据持久化代码的开发 +- 为 Java 社区屏蔽不同持久化 API 的差异 + +## 快速入门 + +(1)在 pom.xml 中引入依赖 + +```xml + + org.springframework.boot + spring-boot-starter-data-jpa + +``` + +(2)设置启动注解 + +```java +// 【可选】指定扫描的 Entity 目录,如果不指定,会扫描全部目录 +@EntityScan("io.github.dunwu.springboot.data.jpa") +// 【可选】指定扫描的 Repository 目录,如果不指定,会扫描全部目录 +@EnableJpaRepositories(basePackages = {"io.github.dunwu.springboot.data.jpa"}) +// 【可选】开启 JPA auditing 能力,可以自动赋值一些字段,比如创建时间、最后一次修改时间等等 +@EnableJpaAuditing +@SpringBootApplication +public class Application { + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } +} +``` + +(3)配置 + +```properties +# 数据库连接 +spring.datasource.url = jdbc:mysql://localhost:3306/spring_tutorial?serverTimezone=UTC&useUnicode=true&characterEncoding=utf8 +spring.datasource.driver-class-name = com.mysql.cj.jdbc.Driver +spring.datasource.username = root +spring.datasource.password = root +# 是否打印 JPA SQL 日志 +spring.jpa.show-sql = true +# Hibernate的DDL策略 +spring.jpa.hibernate.ddl-auto = create-drop +``` + +(4)定义实体 + +```java +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.ToString; + +import java.util.Objects; +import javax.persistence.*; + +@Entity +@Data +@ToString +@NoArgsConstructor +@AllArgsConstructor +public class User { + + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + private Long id; + + @Column(unique = true) + private String name; + + private Integer age; + + private String address; + + private String email; + + public User(String name, Integer age, String address, String email) { + this.name = name; + this.age = age; + this.address = address; + this.email = email; + } + + @Override + public int hashCode() { + return Objects.hash(id, name); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + + if (!(o instanceof User)) { + return false; + } + + User user = (User) o; + + if (id != null && id.equals(user.id)) { + return true; + } + + return name.equals(user.name); + } + +} +``` + +(5)定义 Repository + +```java + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.data.rest.core.annotation.RepositoryRestResource; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.bind.annotation.PathVariable; + +import java.util.List; + +@RepositoryRestResource(collectionResourceRel = "user", path = "user") +public interface UserRepository extends JpaRepository { + + User findUserById(@PathVariable("id") Long id); + + /** + * 根据用户名查找用户 + *

+ * 示例:http://localhost:8080/user/search/findByName?name=lisi + * + * @param name 用户名 + * @return {@link User} + */ + User findUserByName(@Param("name") String name); + + /** + * 根据邮箱查找用户 + *

+ * 示例:http://localhost:8080/user/search/findByEmail?email=xxx@163.com + * + * @param email 邮箱 + * @return {@link User} + */ + @Query("from User u where u.email=:email") + List findByEmail(@Param("email") String email); + + /** + * 根据用户名删除用户 + * + * @param name 用户名 + */ + @Transactional(rollbackFor = Exception.class) + void deleteByName(@Param("name") String name); + +} +``` + +(6)测试 + +```java +@Slf4j +@SpringBootTest(classes = { DataJpaApplication.class }) +public class DataJpaTests { + + @Autowired + private UserRepository repository; + + @BeforeEach + public void before() { + repository.deleteAll(); + } + + @Test + public void insert() { + User user = new User("张三", 18, "北京", "user1@163.com"); + repository.save(user); + Optional optional = repository.findById(user.getId()); + assertThat(optional).isNotNull(); + assertThat(optional.isPresent()).isTrue(); + } + + @Test + public void batchInsert() { + List users = new ArrayList<>(); + users.add(new User("张三", 18, "北京", "user1@163.com")); + users.add(new User("李四", 19, "上海", "user1@163.com")); + users.add(new User("王五", 18, "南京", "user1@163.com")); + users.add(new User("赵六", 20, "武汉", "user1@163.com")); + repository.saveAll(users); + + long count = repository.count(); + assertThat(count).isEqualTo(4); + + List list = repository.findAll(); + assertThat(list).isNotEmpty().hasSize(4); + list.forEach(this::accept); + } + + private void accept(User user) { log.info(user.toString()); } + + @Test + public void delete() { + List users = new ArrayList<>(); + users.add(new User("张三", 18, "北京", "user1@163.com")); + users.add(new User("李四", 19, "上海", "user1@163.com")); + users.add(new User("王五", 18, "南京", "user1@163.com")); + users.add(new User("赵六", 20, "武汉", "user1@163.com")); + repository.saveAll(users); + + repository.deleteByName("张三"); + assertThat(repository.findUserByName("张三")).isNull(); + + repository.deleteAll(); + List list = repository.findAll(); + assertThat(list).isEmpty(); + } + + @Test + public void findAllInPage() { + List users = new ArrayList<>(); + users.add(new User("张三", 18, "北京", "user1@163.com")); + users.add(new User("李四", 19, "上海", "user1@163.com")); + users.add(new User("王五", 18, "南京", "user1@163.com")); + users.add(new User("赵六", 20, "武汉", "user1@163.com")); + repository.saveAll(users); + + PageRequest pageRequest = PageRequest.of(1, 2); + Page page = repository.findAll(pageRequest); + assertThat(page).isNotNull(); + assertThat(page.isEmpty()).isFalse(); + assertThat(page.getTotalElements()).isEqualTo(4); + assertThat(page.getTotalPages()).isEqualTo(2); + + List list = page.get().collect(Collectors.toList()); + System.out.println("user list: "); + list.forEach(System.out::println); + } + + @Test + public void update() { + User oldUser = new User("张三", 18, "北京", "user1@163.com"); + oldUser.setName("张三丰"); + repository.save(oldUser); + + User newUser = repository.findUserByName("张三丰"); + assertThat(newUser).isNotNull(); + } + +} +``` + +## 常用 JPA 注解 + +### 实体 + +#### `@Entity` + +#### `@MappedSuperclass` + +当多个实体有共同的属性字段,比如说 id,则可以把它提炼出一个父类,并且加上 `@MappedSuperclass`,则实体基类就可以继承了。 + +#### `@Table` + +当实体名和表名不一致时,可以通过 `@Table(name="CUSTOMERS")` 的形式来明确指定一个表名。 + +### 主键 + +#### `@Id` + +@Id 注解用于声明一个实体类的属性映射为数据库的主键。 + +#### `@GeneratedValue` + +`@GeneratedValue` 用于标注主键的生成策略,通过 `strategy` 属性指定。 + +默认情况下,JPA 自动选择一个最适合底层数据库的主键生成策略:SqlServer 对应 identity,MySQL 对应 auto increment。 + +在 `javax.persistence.GenerationType` 中定义了以下几种可供选择的策略: + +```java +public enum GenerationType { + TABLE, + SEQUENCE, + IDENTITY, + AUTO +} +``` + +- `IDENTITY`:采用数据库 ID 自增长的方式来自增主键字段,Oracle 不支持这种方式; +- `AUTO`: JPA 自动选择合适的策略,是默认选项; +- `SEQUENCE`:通过序列产生主键,通过 `@SequenceGenerator` 注解指定序列名,MySql 不支持这种方式 +- `TABLE`:通过表产生主键,框架借由表模拟序列产生主键,使用该策略可以使应用更易于数据库移植。 + +也就是如果你没有指定 strategy 属性,默认策略是 AUTO,JPA 会根据你使用的数据库来自动选择策略,比如说我使用的是 mysql 则,自动的主键策略就是 IDENTITY (auto increment)。 + +### 映射 + +#### `@Column` + +当你的 entity 属性名和数据库中的字段名不一致,可以使用 `@Column` 明确指定,它也可以设置一些属性 + +```java +@Column(length = 10, nullable = false, unique = true) +``` + +```java +@Column(columnDefinition = "INT(3)") +private int age; +``` + +`@Column` 支持的参数: + +- `unique` 属性表示该字段是否为唯一标识,默认为 false。如果表中有一个字段需要唯一标识,则既可以使用该标记,也可以使用 `@Table` 标记中的 `@UniqueConstraint`。 +- `nullable` 属性表示该字段是否可以为 `null` 值,默认为 true。 +- `insertable` 属性表示在使用 `INSERT` 插入数据时,是否需要插入该字段的值。 +- `updatable` 属性表示在使用 `UPDATE` 更新数据时,是否需要更新该字段的值。`insertable` 和 `updatable` 属性一般多用于只读的属性,例如主键和外键等。这些字段的值通常是自动生成的。 +- `columnDefinition` 属性表示创建表时,该字段创建的 SQL 语句,一般用于通过 Entity 生成表定义时使用。 +- `table` 属性表示当映射多个表时,指定表的表中的字段。默认值为主表的表名。 +- `length` 属性表示字段的长度,当字段的类型为 `varchar` 时,该属性才有效,默认为 255 个字符。 +- `precision` 属性和 scale 属性表示精度,当字段类型为 `double` 时,`precision` 表示数值的总长度,`scale` 表示小数点所占的位数。 + +`@JoinTable` + +`@JoinColumn` + +### 关系 + +表关系映射(双向映射) + +- `@OneToOne`:一对一关系 +- `@OneToMany`:一对多 +- `@ManyToMany`(不推荐使用,而是采用用中间对象,把多对多拆成两个对多一关系) + +字段映射(单向映射): + +- `@Embedded`、`@Embeddable` 嵌入式关系(单向映射) +- `@ElementCollection` 集合一对多关系(单向映射) + +#### `@OneToOne` + +`@OneToOne` 表示一对一关系 + +#### `@OneToMany` + +`@OneToMany` 表示一对多关系 + +`@ManyToOne` + +`@ManyToMany` + +`OrderBy` + +## 查询 + +查询方式有: + +- 方法名字方式查询 + +- `@Query` 注解方式查询 +- 动态 SQL 方式查询 + +- Example 方式查询 + +`JpaRepository` 提供了如下表所述的内置查询 + +- `List findAll();` - 返回所有实体 +- `List findAllById(Iterable var1);` - 返回指定 id 的所有实体 +- `T getOne(ID var1);` - 根据 id 返回对应的实体,如果未找到,则返回空。 +- `List findAll(Sort var1);` - 返回所有实体,按照指定顺序返回。 +- `Page findAll(Pageable var1);` - 返回实体列表,实体的 offset 和 limit 通过 pageable 来指定 + +### 方法名字方式查询方式 + +Spring Data 通过查询的方法名和参数名来自动构造一个 JPA QQL 查询。 + +```java +public interface UserRepository extends JpaRepository { + public User findByName(String name); +} +``` + +方法名和参数名要遵守一定的规则,Spring Data JPA 才能自动转换为 JPQL: + +- 方法名通常包含多个实体属性用于查询,属性之间可以使用 `AND` 和 `OR` 连接,也支持 `Between`、`LessThan`、`GreaterThan`、`Like`; + +- 方法名可以以 `findBy`、`getBy`、`queryBy` 开头; + +- 查询结果可以排序,方法名包含 OrderBy+属性+ASC(DESC); + +- 可以通过 `Top`、`First` 来限定查询的结果集; + +- 一些特殊的参数可以出现在参数列表里,比如 `Pageeable`、`Sort` + +示例: + +```java +// 根据名字查询,且按照名字升序 +List findByLastnameOrderByFirstnameAsc(String name); + +// 根据名字查询,且使用翻页查询 +Page findByLastname(String lastname, Pageable pageable); + +// 查询满足条件的前10个用户 +List findFirst10ByLastname(String lastname, Sort sort); + +// 使用And联合查询 +List findByFirstnameAndLastname(String firstname, String lastname); + +// 使用Or查询 +List findDistinctPeopleByLastnameOrFirstname(String lastname, String firstname); + +// 使用like查询,name 必须包含like中的%或者? +public User findByNameLike(String name); +``` + +| Keyword | Sample | JPQL snippet | +| ------------------- | --------------------------------------------------------- | ------------------------------------------------------------------ | +| `And` | `findByLastnameAndFirstname` | `… where x.lastname = ?1 and x.firstname = ?2` | +| `Or` | `findByLastnameOrFirstname` | `… where x.lastname = ?1 or x.firstname = ?2` | +| `Is,Equals` | `findByFirstname,findByFirstnameIs,findByFirstnameEquals` | `… where x.firstname = 1?` | +| `Between` | `findByStartDateBetween` | `… where x.startDate between 1? and ?2` | +| `LessThan` | `findByAgeLessThan` | `… where x.age < ?1` | +| `LessThanEqual` | `findByAgeLessThanEqual` | `… where x.age <= ?1` | +| `GreaterThan` | `findByAgeGreaterThan` | `… where x.age > ?1` | +| `GreaterThanEqual` | `findByAgeGreaterThanEqual` | `… where x.age >= ?1` | +| `After` | `findByStartDateAfter` | `… where x.startDate > ?1` | +| `Before` | `findByStartDateBefore` | `… where x.startDate < ?1` | +| `IsNull` | `findByAgeIsNull` | `… where x.age is null` | +| `IsNotNull,NotNull` | `findByAge(Is)NotNull` | `… where x.age not null` | +| `Like` | `findByFirstnameLike` | `… where x.firstname like ?1` | +| `NotLike` | `findByFirstnameNotLike` | `… where x.firstname not like ?1` | +| `StartingWith` | `findByFirstnameStartingWith` | `… where x.firstname like ?1` (parameter bound with appended `%`) | +| `EndingWith` | `findByFirstnameEndingWith` | `… where x.firstname like ?1` (parameter bound with prepended `%`) | +| `Containing` | `findByFirstnameContaining` | `… where x.firstname like ?1` (parameter bound wrapped in `%`) | +| `OrderBy` | `findByAgeOrderByLastnameDesc` | `… where x.age = ?1 order by x.lastname desc` | +| `Not` | `findByLastnameNot` | `… where x.lastname <> ?1` | +| `In` | `findByAgeIn(Collection ages)` | `… where x.age in ?1` | +| `NotIn` | `findByAgeNotIn(Collection age)` | `… where x.age not in ?1` | +| `True` | `findByActiveTrue()` | `… where x.active = true` | +| `False` | `findByActiveFalse()` | `… where x.active = false` | +| `IgnoreCase` | `findByFirstnameIgnoreCase` | `… where UPPER(x.firstame) = UPPER(?1)` | + +### @Query 注解方式查询 + +注解 `@Query` 允许在方法上使用 JPQL。 + +其中操作针对的是对象名和对象属性名,而非数据库中的表名和字段名。 + +```java +@Query("select u form User u where u.name=?1 and u.depantment.id=?2"); +public User findUser(String name, Integer departmentId); +``` + +```java +@Query("form User u where u.name=?1 and u.depantment.id=?2"); +public User findUser(String name, Integer departmentId); +``` + +如果使用 SQL 而不是 JPSQL,可以使用 `nativeQuery` 属性,设置为 true。 + +```java +@Query(value="select * from user where name=?1 and department_id=?2", nativeQuery=true) +public User nativeQuery(String name, Integer departmentId); +``` + +无论 JPQL,还是 SQL,都支持"命名参数": + +```java +@Query(value="select * from user where name=:name and department_id=:departmentId", nativeQuery=true) +public User nativeQuery2(String name, Integer departmentId); +``` + +如果 SQL 活着 JPQL 查询结果集并非 Entity,可以用 `Object[]` 数组代替,比如分组统计每个部分的用户数 + +```java +@Query(value="select department_id,count(*) from user group by department_id", nativeQuery=true) +public List queryUserCount() +``` + +这条查询将返回数组,对象类型依赖于查询结果,被示例中,返回的是 `String` 和 `BigInteger` 类型 + +查询时可以使用 `Pageable` 和 `Sort` 来完成翻页和排序。 + +```java +@Query("select u from User u where department.id=?1") +public Page QueryUsers(Integer departmentId, Pageable page); +``` + +`@Query` 还允许 SQL 更新、删除语句,此时必须搭配 `@Modifying` 使用,比如: + +```java +@Modifying +@Query("update User u set u.name= ?1 where u.id= ?2") +int updateName(String name, Integer id); +``` + +### 动态 SQL 方式查询 + +可参考:[SpringDataJpa 中的复杂查询和动态查询,多表查询](https://juejin.cn/post/6844904160807092237) + +### Example 方式查询 + +允许根据实体创建一个 Example 对象,Spring Data 通过 Example 对象来构造 JPQL。但是使用不灵活条件是 AND,不能使用 or,时间的大于小于,between 等。 + +继承 `JpaRepository` + +```java + List findAll(Example var1); + List findAll(Example var1, Sort var2); +``` + +```java +public List getByExample(String name) { + Department dept = new Department(); + dept.setId(1); + + User user = new User(); + user.setName(name); + user.setDepartment(dept); + Example example = Example.of(user); + List list = userDao.findAll(example); + return list +} +``` + +以上代码首先创建了 User 对象,设置 查询条件,名称为参数 name,部门 id 为 1,通过 `Example.of` 构造了此查询。 + +大部分查询并非完全匹配查询,ExampleMatcher 提供了更多的条件指定.比如以 xxx 开头的所有用户,则可以使用以下代码构造 + +```java +ExampleMatcher matcher = ExampleMatcher.matching().withMatcher("xxx", + GenericPropertyMatchers.startsWith().ignoreCase()); +Example example = Example.of(user, matcher); +``` + +### 排序 Sort + +Sort 对象用来指定排序,最简单的 Sort 对象构造可以传入一个属性名列表(不是数据库列名,是属性名)。默认采用升序排序。 + +```java +Sort sort = new Sort("id"); +//Sort sort = new Sort(Direction.DESC, "id"); +return userDao.findAll(sort); +``` + +Hibernate 根据 Sort 构造了排序条件,Sort("id") 表示按照 id 采用默认 升序进行排序 + +其他 Sort 的构造方法还包括以下主要的一些: + +- `public Sort(String... properties)`,按照指定的属性列表升序排序。 +- `public Sort(Sort.Direction direction, String... properties)`,按照指定属性列表排序,排序由 direction 指定,direction 是一个枚举类型,有 `Direction.ASC` 和 `Direction.DESC`。 +- `public Sort(Sort.Order... orders)`,可以通过 Order 静态方法来创建 + - `public static Sort.Order asc(String property)` + - `public static Sort.Order desc(String property)` + +### 分页 Page 和 Pageable + +Pageable 接口用于构造翻页查询,PageRequest 是其实现类,可以通过提供的工厂方法创建 PageRequest: + +注意我这边使用的是 sring boot 2.0.2 ,jpa 版本是 2.0.8,新版本与之前版本的操作方法有所不同。 + +- `public static PageRequest of(int page, int size)` + +- `public static PageRequest of(int page, int size, Sort sort)` - 也可以在 PageRequest 中加入排序 + +- `public static PageRequest of(int page, int size, Direction direction, String... properties)`,或者自定义排序规则 + +page 是从 0 开始,表示查询页,size 指每页的期望行数。 + +Spring Data 翻页查询总是返回 Page 对象,Page 对象提供了以下常用的方法 + +- `int getTotalPages();`,总的页数 +- `long getTotalElements();` - 返回总数 +- `List getContent();` - 返回此次查询的结果集 + +## 核心 API + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/20230123160810.png) + +## 参考资料 + +- [Spring 官网](https://spring.io/) +- [Spring Framework 官方文档](https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/index.html) +- [Spring Boot 官方文档](https://docs.spring.io/spring-boot/docs/current/reference/html/data.html) \ No newline at end of file diff --git "a/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/02.Spring\346\225\260\346\215\256/10.Spring\351\233\206\346\210\220Mybatis.md" "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/02.Spring\346\225\260\346\215\256/10.Spring\351\233\206\346\210\220Mybatis.md" new file mode 100644 index 00000000..d20f39b6 --- /dev/null +++ "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/02.Spring\346\225\260\346\215\256/10.Spring\351\233\206\346\210\220Mybatis.md" @@ -0,0 +1,252 @@ +--- +title: Spring 集成 Mybatis +date: 2019-05-09 17:09:25 +order: 10 +categories: + - Java + - 框架 + - Spring + - Spring数据 +tags: + - Java + - 框架 + - Spring + - SpringBoot + - MyBatis + - PageHelper + - Mapper +permalink: /pages/88219e/ +--- + +# Spring 集成 Mybatis + +[Mybatis 官网](http://www.mybatis.org/mybatis-3/) 是一款持久层框架,它支持定制化 SQL、存储过程以及高级映射。MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集。MyBatis 可以使用简单的 XML 或注解来配置和映射原生类型、接口和 Java 的 POJO(Plain Old Java Objects,普通老式 Java 对象)为数据库中的记录。 + +## 快速入门 + +要使用 MyBatis, 只需将 [mybatis-x.x.x.jar](https://github.com/mybatis/mybatis-3/releases) 文件置于类路径(classpath)中即可。 + +如果使用 Maven 来构建项目,则需将下面的依赖代码置于 pom.xml 文件中: + +```xml + + org.mybatis + mybatis + x.x.x + +``` + +### 从 XML 中构建 SqlSessionFactory + +每个基于 MyBatis 的应用都是以一个 SqlSessionFactory 的实例为核心的。SqlSessionFactory 的实例可以通过 SqlSessionFactoryBuilder 获得。而 SqlSessionFactoryBuilder 则可以从 XML 配置文件或一个预先配置的 Configuration 实例来构建出 SqlSessionFactory 实例。 + +从 XML 文件中构建 SqlSessionFactory 的实例非常简单,建议使用类路径下的资源文件进行配置。 但也可以使用任意的输入流(InputStream)实例,比如用文件路径字符串或 file:// URL 构造的输入流。MyBatis 包含一个名叫 Resources 的工具类,它包含一些实用方法,使得从类路径或其它位置加载资源文件更加容易。 + +```java +String resource = "org/mybatis/example/mybatis-config.xml"; +InputStream inputStream = Resources.getResourceAsStream(resource); +SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); +``` + +XML 配置文件中包含了对 MyBatis 系统的核心设置,包括获取数据库连接实例的数据源(DataSource)以及决定事务作用域和控制方式的事务管理器(TransactionManager)。后面会再探讨 XML 配置文件的详细内容,这里先给出一个简单的示例: + +```xml + + + + + + + + + + + + + + + + + + +``` + +当然,还有很多可以在 XML 文件中配置的选项,上面的示例仅罗列了最关键的部分。 注意 XML 头部的声明,它用来验证 XML 文档的正确性。environment 元素体中包含了事务管理和连接池的配置。mappers 元素则包含了一组映射器(mapper),这些映射器的 XML 映射文件包含了 SQL 代码和映射定义信息。 + +### 不使用 XML 构建 SqlSessionFactory + +如果你更愿意直接从 Java 代码而不是 XML 文件中创建配置,或者想要创建你自己的配置构建器,MyBatis 也提供了完整的配置类,提供了所有与 XML 文件等价的配置项。 + +```java +DataSource dataSource = BlogDataSourceFactory.getBlogDataSource(); +TransactionFactory transactionFactory = new JdbcTransactionFactory(); +Environment environment = new Environment("development", transactionFactory, dataSource); +Configuration configuration = new Configuration(environment); +configuration.addMapper(BlogMapper.class); +SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(configuration); +``` + +注意该例中,configuration 添加了一个映射器类(mapper class)。映射器类是 Java 类,它们包含 SQL 映射注解从而避免依赖 XML 映射文件。不过,由于 Java 注解的一些限制以及某些 MyBatis 映射的复杂性,要使用大多数高级映射(比如:嵌套联合映射),仍然需要使用 XML 映射文件进行映射。有鉴于此,如果存在一个同名 XML 映射文件,MyBatis 会自动查找并加载它(在这个例子中,基于类路径和 BlogMapper.class 的类名,会加载 BlogMapper.xml)。具体细节稍后讨论。 + +### 从 SqlSessionFactory 中获取 SqlSession + +既然有了 SqlSessionFactory,顾名思义,我们可以从中获得 SqlSession 的实例。SqlSession 提供了在数据库执行 SQL 命令所需的所有方法。你可以通过 SqlSession 实例来直接执行已映射的 SQL 语句。例如: + +```java +try (SqlSession session = sqlSessionFactory.openSession()) { + Blog blog = (Blog) session.selectOne("org.mybatis.example.BlogMapper.selectBlog", 101); +} +``` + +诚然,这种方式能够正常工作,对使用旧版本 MyBatis 的用户来说也比较熟悉。但现在有了一种更简洁的方式——使用和指定语句的参数和返回值相匹配的接口(比如 BlogMapper.class),现在你的代码不仅更清晰,更加类型安全,还不用担心可能出错的字符串字面值以及强制类型转换。 + +例如: + +```java +try (SqlSession session = sqlSessionFactory.openSession()) { + BlogMapper mapper = session.getMapper(BlogMapper.class); + Blog blog = mapper.selectBlog(101); +} +``` + +现在我们来探究一下这段代码究竟做了些什么。 + +### 探究已映射的 SQL 语句 + +现在你可能很想知道 SqlSession 和 Mapper 到底具体执行了些什么操作,但 SQL 语句映射是个相当广泛的话题,可能会占去文档的大部分篇幅。 但为了让你能够了解个大概,这里先给出几个例子。 + +在上面提到的例子中,一个语句既可以通过 XML 定义,也可以通过注解定义。我们先看看 XML 定义语句的方式,事实上 MyBatis 提供的所有特性都可以利用基于 XML 的映射语言来实现,这使得 MyBatis 在过去的数年间得以流行。如果你用过旧版本的 MyBatis,你应该对这个概念比较熟悉。 但相比于之前的版本,新版本改进了许多 XML 的配置,后面我们会提到这些改进。这里给出一个基于 XML 映射语句的示例,它应该可以满足上个示例中 SqlSession 的调用。 + +```xml + + + + + +``` + +为了这个简单的例子,我们似乎写了不少配置,但其实并不多。在一个 XML 映射文件中,可以定义无数个映射语句,这样一来,XML 头部和文档类型声明部分就显得微不足道了。文档的其它部分很直白,容易理解。 它在命名空间 “org.mybatis.example.BlogMapper” 中定义了一个名为 “selectBlog” 的映射语句,这样你就可以用全限定名 “org.mybatis.example.BlogMapper.selectBlog” 来调用映射语句了,就像上面例子中那样: + +```java +Blog blog = (Blog) session.selectOne("org.mybatis.example.BlogMapper.selectBlog", 101); +``` + +你可能会注意到,这种方式和用全限定名调用 Java 对象的方法类似。这样,该命名就可以直接映射到在命名空间中同名的映射器类,并将已映射的 select 语句匹配到对应名称、参数和返回类型的方法。因此你就可以像上面那样,不费吹灰之力地在对应的映射器接口调用方法,就像下面这样: + +```java +BlogMapper mapper = session.getMapper(BlogMapper.class); +Blog blog = mapper.selectBlog(101); +``` + +第二种方法有很多优势,首先它不依赖于字符串字面值,会更安全一点;其次,如果你的 IDE 有代码补全功能,那么代码补全可以帮你快速选择到映射好的 SQL 语句。 + +**提示** **对命名空间的一点补充** + +在之前版本的 MyBatis 中,**命名空间(Namespaces)**的作用并不大,是可选的。 但现在,随着命名空间越发重要,你必须指定命名空间。 + +命名空间的作用有两个,一个是利用更长的全限定名来将不同的语句隔离开来,同时也实现了你上面见到的接口绑定。就算你觉得暂时用不到接口绑定,你也应该遵循这里的规定,以防哪天你改变了主意。 长远来看,只要将命名空间置于合适的 Java 包命名空间之中,你的代码会变得更加整洁,也有利于你更方便地使用 MyBatis。 + +**命名解析:**为了减少输入量,MyBatis 对所有具有名称的配置元素(包括语句,结果映射,缓存等)使用了如下的命名解析规则。 + +- 全限定名(比如 “com.mypackage.MyMapper.selectAllThings)将被直接用于查找及使用。 +- 短名称(比如 “selectAllThings”)如果全局唯一也可以作为一个单独的引用。 如果不唯一,有两个或两个以上的相同名称(比如 “com.foo.selectAllThings” 和 “com.bar.selectAllThings”),那么使用时就会产生“短名称不唯一”的错误,这种情况下就必须使用全限定名。 + +对于像 BlogMapper 这样的映射器类来说,还有另一种方法来完成语句映射。 它们映射的语句可以不用 XML 来配置,而可以使用 Java 注解来配置。比如,上面的 XML 示例可以被替换成如下的配置: + +```java +package org.mybatis.example; +public interface BlogMapper { + @Select("SELECT * FROM blog WHERE id = #{id}") + Blog selectBlog(int id); +} +``` + +使用注解来映射简单语句会使代码显得更加简洁,但对于稍微复杂一点的语句,Java 注解不仅力不从心,还会让本就复杂的 SQL 语句更加混乱不堪。 因此,如果你需要做一些很复杂的操作,最好用 XML 来映射语句。 + +选择何种方式来配置映射,以及是否应该要统一映射语句定义的形式,完全取决于你和你的团队。 换句话说,永远不要拘泥于一种方式,你可以很轻松地在基于注解和 XML 的语句映射方式间自由移植和切换。 + +### 作用域(Scope)和生命周期 + +理解我们之前讨论过的不同作用域和生命周期类别是至关重要的,因为错误的使用会导致非常严重的并发问题。 + +**提示** **对象生命周期和依赖注入框架** + +依赖注入框架可以创建线程安全的、基于事务的 SqlSession 和映射器,并将它们直接注入到你的 bean 中,因此可以直接忽略它们的生命周期。 如果对如何通过依赖注入框架使用 MyBatis 感兴趣,可以研究一下 MyBatis-Spring 或 MyBatis-Guice 两个子项目。 + +#### SqlSessionFactoryBuilder + +这个类可以被实例化、使用和丢弃,一旦创建了 SqlSessionFactory,就不再需要它了。 因此 SqlSessionFactoryBuilder 实例的最佳作用域是方法作用域(也就是局部方法变量)。 你可以重用 SqlSessionFactoryBuilder 来创建多个 SqlSessionFactory 实例,但最好还是不要一直保留着它,以保证所有的 XML 解析资源可以被释放给更重要的事情。 + +#### SqlSessionFactory + +SqlSessionFactory 一旦被创建就应该在应用的运行期间一直存在,没有任何理由丢弃它或重新创建另一个实例。 使用 SqlSessionFactory 的最佳实践是在应用运行期间不要重复创建多次,多次重建 SqlSessionFactory 被视为一种代码“坏习惯”。因此 SqlSessionFactory 的最佳作用域是应用作用域。 有很多方法可以做到,最简单的就是使用单例模式或者静态单例模式。 + +#### SqlSession + +每个线程都应该有它自己的 SqlSession 实例。SqlSession 的实例不是线程安全的,因此是不能被共享的,所以它的最佳的作用域是请求或方法作用域。 绝对不能将 SqlSession 实例的引用放在一个类的静态域,甚至一个类的实例变量也不行。 也绝不能将 SqlSession 实例的引用放在任何类型的托管作用域中,比如 Servlet 框架中的 HttpSession。 如果你现在正在使用一种 Web 框架,考虑将 SqlSession 放在一个和 HTTP 请求相似的作用域中。 换句话说,每次收到 HTTP 请求,就可以打开一个 SqlSession,返回一个响应后,就关闭它。 这个关闭操作很重要,为了确保每次都能执行关闭操作,你应该把这个关闭操作放到 finally 块中。 下面的示例就是一个确保 SqlSession 关闭的标准模式: + +```java +try (SqlSession session = sqlSessionFactory.openSession()) { + // 你的应用逻辑代码 +} +``` + +在所有代码中都遵循这种使用模式,可以保证所有数据库资源都能被正确地关闭。 + +#### 映射器实例 + +映射器是一些绑定映射语句的接口。映射器接口的实例是从 SqlSession 中获得的。虽然从技术层面上来讲,任何映射器实例的最大作用域与请求它们的 SqlSession 相同。但方法作用域才是映射器实例的最合适的作用域。 也就是说,映射器实例应该在调用它们的方法中被获取,使用完毕之后即可丢弃。 映射器实例并不需要被显式地关闭。尽管在整个请求作用域保留映射器实例不会有什么问题,但是你很快会发现,在这个作用域上管理太多像 SqlSession 的资源会让你忙不过来。 因此,最好将映射器放在方法作用域内。就像下面的例子一样: + +```java +try (SqlSession session = sqlSessionFactory.openSession()) { + BlogMapper mapper = session.getMapper(BlogMapper.class); + // 你的应用逻辑代码 +} +``` + +## Mybatis 扩展工具 + +### Mybatis Plus + +[MyBatis-Plus](https://github.com/baomidou/mybatis-plus)(简称 MP)是一个 [MyBatis](https://www.mybatis.org/mybatis-3/) 的增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生。 + +【集成示例】[spring-boot-data-mybatis-plus](https://github.com/dunwu/spring-tutorial/tree/develop/codes/data/orm/spring-boot-data-mybatis-plus) + +### Mapper + +[Mapper](https://github.com/abel533/Mapper) 是一个 Mybatis CRUD 扩展插件。 + +Mapper 的基本原理是将实体类映射为数据库中的表和字段信息,因此实体类需要通过注解配置基本的元数据,配置好实体后, 只需要创建一个继承基础接口的 Mapper 接口就可以开始使用了。 + +【集成示例】[spring-boot-data-mybatis-mapper](https://github.com/dunwu/spring-tutorial/tree/develop/codes/data/orm/spring-boot-data-mybatis-mapper) + +### PageHelper + +[PageHelper](https://github.com/pagehelper/Mybatis-PageHelper) 是一个 Mybatis 通用分页插件。 + +【集成示例】[spring-boot-data-mybatis-mapper](https://github.com/dunwu/spring-tutorial/tree/develop/codes/data/orm/spring-boot-data-mybatis-mapper) + +## 参考资料 + +- **官方** + - [Mybatis Github](https://github.com/mybatis/mybatis-3) + - [Mybatis 官网](http://www.mybatis.org/mybatis-3/) + - [MyBatis 官方代码生成(mybatis-generator)](https://github.com/mybatis/generator) + - [MyBatis 官方集成 Spring(mybatis-spring)](https://github.com/mybatis/spring) + - [Mybatis 官方集成 Spring Boot(mybatis-spring-boot)](https://github.com/mybatis/spring-boot-starter) +- **扩展插件** + - [MyBatis-Plus](https://github.com/baomidou/mybatis-plus) - CRUD 扩展插件、代码生成器、分页器等多功能 + - [Mapper](https://github.com/abel533/Mapper) - Mybatis CRUD 扩展插件 + - [PageHelper](https://github.com/pagehelper/Mybatis-PageHelper) - Mybatis 通用分页插件 +- **文章** + - [深入理解 mybatis 原理](https://blog.csdn.net/luanlouis/article/details/40422941) + - [mybatis 源码中文注释](https://github.com/tuguangquan/mybatis) + - [MyBatis Generator 详解](https://blog.csdn.net/isea533/article/details/42102297) + - [Mybatis 常见面试题](https://juejin.im/post/5aa646cdf265da237e095da1) + - [Mybatis 中强大的 resultMap](https://juejin.im/post/5cee8b61e51d455d88219ea4) \ No newline at end of file diff --git "a/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/02.Spring\346\225\260\346\215\256/20.SpringData\347\273\274\345\220\210.md" "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/02.Spring\346\225\260\346\215\256/20.SpringData\347\273\274\345\220\210.md" new file mode 100644 index 00000000..d4120edf --- /dev/null +++ "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/02.Spring\346\225\260\346\215\256/20.SpringData\347\273\274\345\220\210.md" @@ -0,0 +1,301 @@ +--- +title: Spring Data 综合 +date: 2023-02-08 09:10:35 +order: 20 +categories: + - Java + - 框架 + - Spring + - Spring数据 +tags: + - Java + - 框架 + - Spring + - SpringBoot +permalink: /pages/191cdb/ +--- + +# Spring Data 综合 + +Spring Data Repository 抽象的目标是显著减少各种访问持久化存储的样板式代码。 + +## 核心概念 + +Repository 是 Spring Data 的核心接口。此接口主要用作标记接口,以捕获要使用的类型并帮助您发现扩展此接口的接口。`CrudRepository` 和 `ListCrudRepository` 接口为被管理的实体类提供复杂的 CRUD 功能。`ListCrudRepository` 提供等效方法,但它们返回 `List`,而 `CrudRepository` 方法返回 `Iterable`。 + +`CrudRepository` 接口定义: + +```java +public interface CrudRepository extends Repository { + + S save(S entity); + + Optional findById(ID primaryKey); + + Iterable findAll(); + + long count(); + + void delete(T entity); + + boolean existsById(ID primaryKey); + + // … more functionality omitted. +} +``` + +> Spring Data 项目也提供了一些特定持久化技术的抽象接口,如:JpaRepository 或 MongoRepository。这些接口扩展了 CrudRepository 并暴露了一些持久化技术的底层功能。 + +除了 `CrudRepository` 之外,还有一个 `PagingAndSortingRepository` 接口,它添加了额外的方法来简化对实体的分页访问: + +```java +public interface PagingAndSortingRepository { + + Iterable findAll(Sort sort); + + Page findAll(Pageable pageable); +} +``` + +【示例】要按页面大小 20 访问 User 的第二页,可以执行如下操作 + +```java +PagingAndSortingRepository repository = // … get access to a bean +Page users = repository.findAll(PageRequest.of(1, 20)); +``` + +除了查询方法之外,计数和删除时的查询也是可用的。 + +【示例】根据姓氏计数 + +```java +interface UserRepository extends CrudRepository { + long countByLastname(String lastname); +} +``` + +【示例】根据姓氏删除 + +```java +interface UserRepository extends CrudRepository { + + long deleteByLastname(String lastname); + + List removeByLastname(String lastname); +} +``` + +## 查询方法 + +使用 Spring Data 对数据库进行查询有以下四步: + +1. 声明一个扩展 `Repository` 或其子接口的接口,并指定泛型类型(实体类和 ID 类型),如以下示例所示: + + ```java + interface PersonRepository extends Repository { … } + ``` + +2. 在接口中声明查询方法 + + ```java + interface PersonRepository extends Repository { + List findByLastname(String lastname); + } + ``` + +3. 使用 [JavaConfig](https://docs.spring.io/spring-data/jdbc/docs/current/reference/html/#repositories.create-instances.java-config) 或 [XML 配置](https://docs.spring.io/spring-data/jdbc/docs/current/reference/html/#repositories.create-instances)为这些接口创建代理实例 + + ```java + @EnableJpaRepositories + class Config { … } + ``` + +4. 注入 `Repository` 实例并使用 + + ```java + class SomeClient { + + private final PersonRepository repository; + + SomeClient(PersonRepository repository) { + this.repository = repository; + } + + void doSomething() { + List persons = repository.findByLastname("Matthews"); + } + } + ``` + +## 定义 Repository + +首先需要定义一个 Repository 接口,该接口必须扩展 Repository 并且指定泛型类型(实体类和 ID 类型)。如果想为该实体暴露 CRUD 方法,可以扩展 CrudRepository 接口。 + +### 微调 Repository 定义 + +Spring Data 提供了很多种 Repository 以应对不同的需求场景。 + +`CrudRepository` 提供了 CRUD 功能。 + +`ListCrudRepository` 和 `CrudRepository` 类似,但对于那些返回多个实体的方法,它返回一个 `List` 而不是 `Iterable`,这样使用可能更方便。 + +如果使用响应式框架,可以使用 `ReactiveCrudRepository` 或 `RxJava3CrudRepository`。 + +`CoroutineCrudRepository` 支持 Kotlin 的协程特性。 + +`PagingAndSortingRepository` 提供了分页、排序功能。 + +如果不想扩展 Spring Data 接口,还可以使用 `@RepositoryDefinition` 注释您的 `Repository` 接口。 扩展一个 CRUD Repository 接口,需要暴露一组完整的方法来操作实体。如果希望对暴露的方法有选择性,可以将要暴露的方法从 CRUD Repository 复制到自定义的 Repository 中。 这样做时,可以更改方法的返回类型。 如果可能,Spring Data 将遵循返回类型。 例如,对于返回多个实体的方法,可以选择 `Iterable`、`List`、`Collection` 或 `VAVR` 列表。 + +自定义基础 `Repository` 接口,必须用 `@NoRepositoryBean` 标记。 这可以防止 Spring Data 尝试直接创建它的实例并失败,因为它无法确定该 Repository 的实体,因为它仍然包含一个通用类型变量。 + +以下示例显示了如何有选择地暴露 CRUD 方法(在本例中为 findById 和 save): + +```java +@NoRepositoryBean +interface MyBaseRepository extends Repository { + + Optional findById(ID id); + + S save(S entity); +} + +interface UserRepository extends MyBaseRepository { + User findByEmailAddress(EmailAddress emailAddress); +} +``` + +### 使用多个 Spring 数据模块 + +有时,程序中需要使用多个 Spring Data 模块。在这种情况下,必须区分持久化技术。当检测到类路径上有多个 Repository 工厂时,Spring Data 进入严格的配置模式。 + +如果定义的 Repository 扩展了特定模块中的 Repository,则它是特定 Spring Data 模块的有效候选者。 + +如果实体类使用了特定模块的类型注解,则它是特定 Spring Data 模块的有效候选者。 Spring Data 模块接受第三方注解(例如 JPA 的 `@Entity`)或提供自己的注解(例如用于 Spring Data MongoDB 和 Spring Data Elasticsearch 的 `@Document`)。 + +以下示例显示了一个使用模块特定接口(在本例中为 JPA)的 Repository: + +```java +interface MyRepository extends JpaRepository { } + +@NoRepositoryBean +interface MyBaseRepository extends JpaRepository { … } + +interface UserRepository extends MyBaseRepository { … } +``` + +MyRepository 和 UserRepository 扩展了 JpaRepository。它们是 Spring Data JPA 模块的有效候选者。 + +以下示例显示了一个使用通用接口的 Repository + +```java +interface AmbiguousRepository extends Repository { … } + +@NoRepositoryBean +interface MyBaseRepository extends CrudRepository { … } + +interface AmbiguousUserRepository extends MyBaseRepository { … } +``` + +AmbiguousRepository 和 AmbiguousUserRepository 仅扩展了 Repository 和 CrudRepository。 虽然这在使用唯一的 Spring Data 模块时很好,但是存在多个模块时,无法区分这些 Repository 应该绑定到哪个特定的 Spring Data。 + +以下示例显示了一个使用带注解的实体类的 Repository + +```java +interface PersonRepository extends Repository { … } + +@Entity +class Person { … } + +interface UserRepository extends Repository { … } + +@Document +class User { … } +``` + +PersonRepository 引用 Person,它使用 JPA @Entity 注解进行标记,因此这个 Repository 显然属于 Spring Data JPA。 UserRepository 引用 User,它使用 Spring Data MongoDB 的 @Document 注解进行标记。 + +以下错误示例显示了一个使用带有混合注解的实体类的 Repository + +```java +interface JpaPersonRepository extends Repository { … } + +interface MongoDBPersonRepository extends Repository { … } + +@Entity +@Document +class Person { … } +``` + +此示例中的实体类同时使用了 JPA 和 Spring Data MongoDB 的注解。示例中定义了两个 Repository:JpaPersonRepository 和 MongoDBPersonRepository。 一个用于 JPA,另一个用于 MongoDB。 Spring Data 不再能够区分 Repository,这会导致未定义的行为。 + +区分 Repository 的最后一种方法是确定 Repository 扫描 package 的范围。 + +```java +@EnableJpaRepositories(basePackages = "com.acme.repositories.jpa") +@EnableMongoRepositories(basePackages = "com.acme.repositories.mongo") +class Configuration { … } +``` + +## 定义查询方法 + +Repository 代理有两种方法可以从方法名称派生特定于存储的查询: + +- 通过直接从方法名称派生查询。 +- 通过使用手动定义的查询。 + +可用选项取决于实际存储。但是,必须有一个策略来决定创建什么实际查询。 + +### 查询策略 + +以下策略可用于Repository 基础结构来解析查询。 对于 Java 配置,您可以使用 EnableJpaRepositories 注释的 queryLookupStrategy 属性。 特定数据存储可能不支持某些策略。 + +- `CREATE` 尝试从查询方法名称构造特定存储的查询。 +- `USE_DECLARED_QUERY` 尝试查找已声明的查询,如果找不到则抛出异常。 +- `CREATE_IF_NOT_FOUND` (默认)结合了 `CREATE` 和 `USE_DECLARED_QUERY`。 + +### 查询创建 + +Spring Data 中有一套内置的查询构建器机制,可以自动映射符合命名和参数规则的方法。 + +```java +interface PersonRepository extends Repository { + + List findByEmailAddressAndLastname(EmailAddress emailAddress, String lastname); + + // Enables the distinct flag for the query + List findDistinctPeopleByLastnameOrFirstname(String lastname, String firstname); + List findPeopleDistinctByLastnameOrFirstname(String lastname, String firstname); + + // Enabling ignoring case for an individual property + List findByLastnameIgnoreCase(String lastname); + // Enabling ignoring case for all suitable properties + List findByLastnameAndFirstnameAllIgnoreCase(String lastname, String firstname); + + // Enabling static ORDER BY for a query + List findByLastnameOrderByFirstnameAsc(String lastname); + List findByLastnameOrderByFirstnameDesc(String lastname); +} +``` + +解析查询方法名称分为主语和谓语。第一部分 (find…By, exists…By) 定义查询的主语,第二部分构成谓词。 主语可以包含更多的表达。 `find`(或其他引入关键字)和 `By` 之间的任何文本都被认为是描述性的,除非使用其中一个结果限制关键字,例如 `Distinct` 在要创建的查询上设置不同的标志或 `Top`/`First` 限制查询结果。 + +> 参考: +> +> [Spring Data 支持的查询主语关键词](https://docs.spring.io/spring-data/jdbc/docs/current/reference/html/#appendix.query.method.subject) +> +> [Spring Data 支持的查询谓语关键词](https://docs.spring.io/spring-data/jdbc/docs/current/reference/html/#appendix.query.method.predicate) + +## 创建 Repository 实例 + +## 自定义 Repository 实现 + +## Spring Data 扩展 + +## 参考资料 + +- [Redis 官网](https://redis.io/) +- [Redis Github](https://github.com/redis/redis) +- [spring-data-redis Github](https://github.com/spring-projects/spring-data-redis) +- [Spring Data Redis 官方文档](https://docs.spring.io/spring-data/redis/docs/current/reference/html/) +- [Spring Data 官方示例](https://github.com/spring-projects/spring-data-examples/) \ No newline at end of file diff --git "a/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/02.Spring\346\225\260\346\215\256/21.Spring\350\256\277\351\227\256Redis.md" "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/02.Spring\346\225\260\346\215\256/21.Spring\350\256\277\351\227\256Redis.md" new file mode 100644 index 00000000..c8bf04e3 --- /dev/null +++ "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/02.Spring\346\225\260\346\215\256/21.Spring\350\256\277\351\227\256Redis.md" @@ -0,0 +1,248 @@ +--- +title: Spring 访问 Redis +date: 2023-01-31 20:54:42 +order: 21 +categories: + - Java + - 框架 + - Spring + - Spring数据 +tags: + - Java + - 框架 + - Spring + - SpringBoot + - Redis +permalink: /pages/65e4a2/ +--- + +# Spring 访问 Redis + +## 简介 + +[Redis](https://redis.io/) 是一个被数百万开发人员用作数据库、缓存、流引擎和消息代理的开源内存数据库。 + +在 Spring 中,[spring-data-redis](https://github.com/spring-projects/spring-data-redis) 项目对访问 [Redis](https://redis.io/) 进行了 API 封装,提供了便捷的访问方式。 [spring-data-redis](https://github.com/spring-projects/spring-data-redis) + +[spring-boot](https://github.com/spring-projects/spring-boot) 项目中的子模块 [spring-boot-starter-data-redis](https://github.com/spring-projects/spring-boot/tree/main/spring-boot-project/spring-boot-starters/spring-boot-starter-data-redis) 基于 [spring-data-redis](https://github.com/spring-projects/spring-data-redis) 项目,做了二次封装,大大简化了 Redis 的相关配置。 + +## Spring Boot 快速入门 + +### 引入依赖 + +在 pom.xml 中引入依赖: + +```xml + + org.springframework.boot + spring-boot-starter-data-redis + +``` + +### 数据源配置 + +```properties +spring.redis.database = 0 +spring.redis.host = localhost +spring.redis.port = 6379 +spring.redis.password = +``` + +### 定义实体 + +```java +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.ToString; + +import java.io.Serializable; + +@Data +@ToString +@NoArgsConstructor +@AllArgsConstructor +public class User implements Serializable { + + private static final long serialVersionUID = 4142994984277644695L; + + private Long id; + private String name; + private Integer age; + private String address; + private String email; + +} +``` + +### 定义 CRUD 接口 + +```java +import java.util.Map; + +public interface UserService { + + void batchSetUsers(Map users); + + long count(); + + User getUser(Long id); + + void setUser(User user); + +} +``` + +### 创建 CRUD 接口实现 + +```java + +import cn.hutool.core.bean.BeanUtil; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Service; + +import java.util.Map; + +@Service +public class UserServiceImpl implements UserService { + + public static final String DEFAULT_KEY = "spring:tutorial:user"; + + private final RedisTemplate redisTemplate; + + public UserServiceImpl(RedisTemplate redisTemplate) { + this.redisTemplate = redisTemplate; + } + + @Override + public void batchSetUsers(Map users) { + redisTemplate.opsForHash().putAll(DEFAULT_KEY, users); + } + + @Override + public long count() { + return redisTemplate.opsForHash().size(DEFAULT_KEY); + } + + @Override + public User getUser(Long id) { + Object obj = redisTemplate.opsForHash().get(DEFAULT_KEY, id.toString()); + return BeanUtil.toBean(obj, User.class); + } + + @Override + public void setUser(User user) { + redisTemplate.opsForHash().put(DEFAULT_KEY, user.getId().toString(), user); + } + +} +``` + +### 创建 Application + +创建 Application,实例化一个 `RedisTemplate` 对象。 + +```java +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.PropertyAccessor; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Primary; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +@Slf4j +@SpringBootApplication +public class RedisQuickstartApplication { + + @Autowired + private ObjectMapper objectMapper; + + @Bean + @Primary + public RedisTemplate redisTemplate(RedisConnectionFactory factory) { + + // 指定要序列化的域,field,get和set,以及修饰符范围,ANY是都有包括private和public + objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); + // // 指定序列化输入的类型,类必须是非final修饰的,final修饰的类,比如String,Integer等会跑出异常 + // objectMapper.activateDefaultTyping(new DefaultBaseTypeLimitingValidator(), + // ObjectMapper.DefaultTyping.NON_FINAL); + + // 使用Jackson2JsonRedisSerializer来序列化和反序列化redis的value值(默认使用JDK的序列化方式) + Jackson2JsonRedisSerializer serializer = new Jackson2JsonRedisSerializer<>(Object.class); + serializer.setObjectMapper(objectMapper); + + RedisTemplate template = new RedisTemplate<>(); + // 配置连接工厂 + template.setConnectionFactory(factory); + // 值采用json序列化 + template.setValueSerializer(serializer); + // 使用StringRedisSerializer来序列化和反序列化redis的key值 + template.setKeySerializer(new StringRedisSerializer()); + // 设置hash key 和value序列化模式 + template.setHashKeySerializer(new StringRedisSerializer()); + template.setHashValueSerializer(serializer); + template.afterPropertiesSet(); + + return template; + } + + public static void main(String[] args) { + SpringApplication.run(RedisQuickstartApplication.class, args); + } + +} +``` + +### 测试 + +```java +@Slf4j +@SpringBootTest(classes = { RedisQuickstartApplication.class }) +public class RedisQuickstartTests { + + @Autowired + private UserService userService; + + @Test + public void test() { + final long SIZE = 1000L; + Map map = new HashMap<>(); + for (long i = 0; i < SIZE; i++) { + User user = new User(i, RandomUtil.randomChineseName(), + RandomUtil.randomInt(1, 100), + RandomUtil.randomEnum(Location.class).name(), + RandomUtil.randomEmail()); + map.put(String.valueOf(i), user); + } + userService.batchSetUsers(map); + long count = userService.count(); + Assertions.assertThat(count).isEqualTo(SIZE); + + for (int i = 0; i < 100; i++) { + long id = RandomUtil.randomLong(0, 1000); + User user = userService.getUser(id); + log.info("user-{}: {}", id, user.toString()); + } + } + +} +``` + +## 示例源码 + +更多 Spring 访问 Redis 示例请参考:[Redis 示例源码](https://github.com/dunwu/spring-tutorial/tree/master/codes/data/nosql/redis) + +## 参考资料 + +- [Redis 官网](https://redis.io/) +- [Redis Github](https://github.com/redis/redis) +- [spring-data-redis Github](https://github.com/spring-projects/spring-data-redis) +- [Spring Data Redis 官方文档](https://docs.spring.io/spring-data/redis/docs/current/reference/html/) +- [Spring Data 官方示例](https://github.com/spring-projects/spring-data-examples/) \ No newline at end of file diff --git "a/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/02.Spring\346\225\260\346\215\256/22.Spring\350\256\277\351\227\256MongoDB.md" "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/02.Spring\346\225\260\346\215\256/22.Spring\350\256\277\351\227\256MongoDB.md" new file mode 100644 index 00000000..03ddf78c --- /dev/null +++ "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/02.Spring\346\225\260\346\215\256/22.Spring\350\256\277\351\227\256MongoDB.md" @@ -0,0 +1,186 @@ +--- +title: Spring 访问 MongoDB +date: 2018-12-15 17:29:36 +order: 22 +categories: + - Java + - 框架 + - Spring + - Spring数据 +tags: + - Java + - 框架 + - Spring + - SpringBoot + - MongoDB +permalink: /pages/db2a41/ +--- + +# Spring 访问 MongoDB + +## 简介 + +[MongoDB](https://www.mongodb.org/) 是一个基于分布式文件存储的数据库。由 C++ 语言编写。旨在为 WEB 应用提供可扩展的高性能数据存储解决方案。MongoDB 将数据存储为一个文档,数据结构由键值对组成。MongoDB 文档类似于 JSON 对象。字段值可以包含其他文档,数组及文档数组。 + +在 Spring 中,[spring-data-mongodb](https://github.com/spring-projects/spring-data-mongodb) 项目对访问 [MongoDB](https://www.mongodb.org/) 进行了 API 封装,提供了便捷的访问方式。 Spring Data MongoDB 的核心是一个以 POJO 为中心的模型,用于与 MongoDB `DBCollection` 交互并轻松编写 `Repository` 样式的数据访问层。 + +[spring-boot](https://github.com/spring-projects/spring-boot) 项目中的子模块 [spring-boot-starter-data-mongodb](https://github.com/spring-projects/spring-boot/tree/main/spring-boot-project/spring-boot-starters/spring-boot-starter-data-mongodb) 基于 [spring-data-mongodb](https://github.com/spring-projects/spring-data-mongodb) 项目,做了二次封装,大大简化了 MongoDB 的相关配置。 + +## Spring Boot 快速入门 + +### 引入依赖 + +在 pom.xml 中引入依赖: + +```xml + + org.springframework.boot + spring-boot-starter-data-mongodb + +``` + +### 数据源配置 + +```properties +spring.data.mongodb.host = localhost +spring.data.mongodb.port = 27017 +spring.data.mongodb.database = test +spring.data.mongodb.username = root +spring.data.mongodb.password = root +``` + +### 定义实体 + +定义一个具有三个属性的 `Customer` 类:`id`、`firstName` 和 `lastName` + +```java +import org.springframework.data.annotation.Id; + +public class Customer { + + @Id + public String id; + + public String firstName; + + public String lastName; + + public Customer(String firstName, String lastName) { + this.firstName = firstName; + this.lastName = lastName; + } + + @Override + public String toString() { + return String.format( + "Customer[id=%s, firstName='%s', lastName='%s']", + id, firstName, lastName); + } + +} +``` + +[spring-data-mongodb](https://github.com/spring-projects/spring-data-mongodb) 会将 `Customer` 类映射到一个名为 `customer` 的集合中。如果要更改集合的名称,可以在类上使用 `@Document` 注解。 + +### 创建 Repository + +[spring-data-mongodb](https://github.com/spring-projects/spring-data-mongodb) 继承了 [Spring Data Commons](https://github.com/spring-projects/spring-data-commons) 项目的能力,所以可以使用其通用 API——`Repository`。 + +先定义一个 `CustomerRepository` 类,继承 `MongoRepository` 接口,并指定其泛型参数:`Customer` 和 `String`。MongoRepository 接口支持多种操作,包括 CRUD 和分页查询。在下面的例子中,定义了两个查询方法: + +```java +import java.util.List; + +import org.springframework.data.mongodb.repository.MongoRepository; + +public interface CustomerRepository extends MongoRepository { + + Customer findByFirstName(String firstName); + List findByLastName(String lastName); + +} +``` + +### 创建 Application + +创建一个 Spring Boot 的启动类 Application,并在启动的 main 方法中使用 `CustomerRepository` 实例访问 MongoDB。 + +```java +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.CommandLineRunner; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class DataMongodbApplication implements CommandLineRunner { + + @Autowired + private CustomerRepository repository; + + public static void main(String[] args) { + SpringApplication.run(DataMongodbApplication.class, args); + } + + @Override + public void run(String... args) { + + repository.deleteAll(); + + // save a couple of customers + repository.save(new Customer("Alice", "Smith")); + repository.save(new Customer("Bob", "Smith")); + + // fetch all customers + System.out.println("Customers found with findAll():"); + System.out.println("-------------------------------"); + for (Customer customer : repository.findAll()) { + System.out.println(customer); + } + System.out.println(); + + // fetch an individual customer + System.out.println("Customer found with findByFirstName('Alice'):"); + System.out.println("--------------------------------"); + System.out.println(repository.findByFirstName("Alice")); + + System.out.println("Customers found with findByLastName('Smith'):"); + System.out.println("--------------------------------"); + for (Customer customer : repository.findByLastName("Smith")) { + System.out.println(customer); + } + } + +} +``` + +运行 `DataMongodbApplication` 的 main 方法后,输出类似如下类容: + +``` +Customers found with findAll(): +------------------------------- +Customer(id=63d6157b265e7c5e48077f63, firstName=Alice, lastName=Smith) +Customer(id=63d6157b265e7c5e48077f64, firstName=Bob, lastName=Smith) + +Customer found with findByFirstName('Alice'): +-------------------------------- +Customer(id=63d6157b265e7c5e48077f63, firstName=Alice, lastName=Smith) +Customers found with findByLastName('Smith'): +-------------------------------- +Customer(id=63d6157b265e7c5e48077f63, firstName=Alice, lastName=Smith) +Customer(id=63d6157b265e7c5e48077f64, firstName=Bob, lastName=Smith) +``` + +## 示例源码 + +更多 Spring 访问 MongoDB 示例请参考:[MongoDB 示例源码](https://github.com/dunwu/spring-tutorial/tree/master/codes/data/nosql/mongodb) + +## 参考资料 + +- [MongoDB 官网](https://www.mongodb.com/) +- [MongoDB Github](https://github.com/mongodb/mongo) +- [MongoDB 官方免费教程](https://university.mongodb.com/) +- [spring-data-mongodb Github](https://github.com/spring-projects/spring-data-mongodb) +- [Spring Data MongoDB 官方文档](https://docs.spring.io/spring-data/mongodb/docs/current/reference/html/) +- [Spring Data 官方示例](https://github.com/spring-projects/spring-data-examples/) +- [Accessing Data with MongoDB](https://spring.io/guides/gs/accessing-data-mongodb/) +- [Accessing MongoDB Data with REST](https://spring.io/guides/gs/accessing-mongodb-data-rest/) \ No newline at end of file diff --git "a/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/02.Spring\346\225\260\346\215\256/23.Spring\350\256\277\351\227\256Elasticsearch.md" "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/02.Spring\346\225\260\346\215\256/23.Spring\350\256\277\351\227\256Elasticsearch.md" new file mode 100644 index 00000000..695ae7eb --- /dev/null +++ "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/02.Spring\346\225\260\346\215\256/23.Spring\350\256\277\351\227\256Elasticsearch.md" @@ -0,0 +1,133 @@ +--- +title: Spring 访问 Elasticsearch +date: 2018-12-25 14:06:36 +order: 23 +categories: + - Java + - 框架 + - Spring + - Spring数据 +tags: + - Java + - 框架 + - Spring + - SpringBoot + - Elasticsearch +permalink: /pages/fac14c/ +--- + +# Spring 访问 Elasticsearch + +## 简介 + +[Elasticsearch](https://www.elastic.co/products/elasticsearch) 是一个开源的、分布式的搜索和分析引擎。 + +### 通过 REST 客户端连接 Elasticsearch + +如果在 classpath 路径下存在 `org.elasticsearch.client:elasticsearch-rest-client` jar 包,Spring Boot 会自动配置并注册一个 `RestClient` Bean,它的默认访问路径为:`localhost:9200`。 + +你可以使用如下方式进行定制: + +```properties +spring.elasticsearch.rest.uris=http://search.example.com:9200 +spring.elasticsearch.rest.username=user +spring.elasticsearch.rest.password=secret +``` + +您还可以注册实现任意数量的 `RestClientBuilderCustomizer` bean,以进行更高级的定制。要完全控制注册,请定义 `RestClient` bean。 + +如果 classpath 路径有 `org.elasticsearch.client:elasticsearch-rest-high-level-client` jar 包,Spring Boot 将自动配置一个 `RestHighLevelClient`,它包装任何现有的 `RestClient` bean,重用其 HTTP 配置。 + +### 通过 Jest 连接 Elasticsearch + +如果 classpath 上有 Jest,你可以注入一个自动配置的 `JestClient`,默认情况下是 `localhost:9200`。您可以进一步调整客户端的配置方式,如以下示例所示: + +```properties +spring.elasticsearch.jest.uris=http://search.example.com:9200 +spring.elasticsearch.jest.read-timeout=10000 +spring.elasticsearch.jest.username=user +spring.elasticsearch.jest.password=secret +``` + +您还可以注册实现任意数量的 `HttpClientConfigBuilderCustomizer` bean,以进行更高级的定制。以下示例调整为其他 HTTP 设置: + +```java +static class HttpSettingsCustomizer implements HttpClientConfigBuilderCustomizer { + + @Override + public void customize(HttpClientConfig.Builder builder) { + builder.maxTotalConnection(100).defaultMaxTotalConnectionPerRoute(5); + } + +} +``` + +要完全控制注册,请定义 `JestClient` bean。 + +### 通过 Spring Data 访问 Elasticsearch + +要连接到 Elasticsearch,您必须提供一个或多个集群节点的地址。可以通过将 `spring.data.elasticsearch.cluster-nodes` 属性设置为以逗号分隔的 `host:port` 列表来指定地址。使用此配置,可以像任何其他 Spring bean 一样注入 `ElasticsearchTemplate` 或 `TransportClient`,如以下示例所示: + +```java +spring.data.elasticsearch.cluster-nodes=localhost:9300 +@Component +public class MyBean { + + private final ElasticsearchTemplate template; + + public MyBean(ElasticsearchTemplate template) { + this.template = template; + } + + // ... + +} +``` + +如果你添加了自定义的 `ElasticsearchTemplate` 或 `TransportClient` `@Bean` ,就会替换默认的配置。 + +### Elasticsearch Repositories + +Spring Data 包含对 Elasticsearch 的 repository 支持。基本原则是根据方法名称自动为您构建查询。 + +事实上,Spring Data JPA 和 Spring Data Elasticsearch 共享相同的通用基础架构。 + +## 源码 + +完整示例:[源码](https://github.com/dunwu/spring-boot-tutorial/tree/master/codes/spring-boot-data-elasticsearch) + +使用方法: + +```bash +mvn clean package +cd target +java -jar spring-boot-data-elasticsearch.jar +``` + +## 版本 + +Spring 和 Elasticsearch 匹配版本: + +| Spring Data Elasticsearch | Elasticsearch | Spring Framework | Spring Boot | +| :------------------------------------------------------------------------------------------------------: | :-----------: | :--------------: | :---------: | +| 5.0.x | 8.5.3 | 6.0.x | 3.0.x | +| 4.4.x | 7.17.3 | 5.3.x | 2.7.x | +| 4.3.x | 7.15.2 | 5.3.x | 2.6.x | +| 4.2.x[[1](https://docs.spring.io/spring-data/elasticsearch/docs/current/reference/html/#_footnotedef_1)] | 7.12.0 | 5.3.x | 2.5.x | +| 4.1.x[[1](https://docs.spring.io/spring-data/elasticsearch/docs/current/reference/html/#_footnotedef_1)] | 7.9.3 | 5.3.2 | 2.4.x | +| 4.0.x[[1](https://docs.spring.io/spring-data/elasticsearch/docs/current/reference/html/#_footnotedef_1)] | 7.6.2 | 5.2.12 | 2.3.x | +| 3.2.x[[1](https://docs.spring.io/spring-data/elasticsearch/docs/current/reference/html/#_footnotedef_1)] | 6.8.12 | 5.2.12 | 2.2.x | +| 3.1.x[[1](https://docs.spring.io/spring-data/elasticsearch/docs/current/reference/html/#_footnotedef_1)] | 6.2.2 | 5.1.19 | 2.1.x | +| 3.0.x[[1](https://docs.spring.io/spring-data/elasticsearch/docs/current/reference/html/#_footnotedef_1)] | 5.5.0 | 5.0.13 | 2.0.x | +| 2.1.x[[1](https://docs.spring.io/spring-data/elasticsearch/docs/current/reference/html/#_footnotedef_1)] | 2.4.0 | 4.3.25 | 1.5.x | + +## 参考资料 + +- **官方** + - [Elasticsearch 官网](https://www.elastic.co/cn/products/elasticsearch) + - [Elasticsearch Github](https://github.com/elastic/elasticsearch) + - [Elasticsearch 官方文档](https://www.elastic.co/guide/en/elasticsearch/reference/current/index.html) + - [Elasticsearch: The Definitive Guide](https://www.elastic.co/guide/en/elasticsearch/guide/master/index.html) - ElasticSearch 官方学习资料 +- [Spring Boot 官方文档之 boot-features-elasticsearch](https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#boot-features-elasticsearch) +- [Spring Data Elasticsearch Github](https://github.com/spring-projects/spring-data-elasticsearch) +- [Spring Data Elasticsearch 官方文档](https://docs.spring.io/spring-data/elasticsearch/docs/current/reference/html/) \ No newline at end of file diff --git "a/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/02.Spring\346\225\260\346\215\256/README.md" "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/02.Spring\346\225\260\346\215\256/README.md" new file mode 100644 index 00000000..a079aa02 --- /dev/null +++ "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/02.Spring\346\225\260\346\215\256/README.md" @@ -0,0 +1,75 @@ +--- +title: Spring 数据篇 +date: 2022-09-18 11:05:36 +categories: + - Java + - 框架 + - Spring + - Spring数据 +tags: + - Java + - 框架 + - Spring + - SpringBoot + - 数据库 +permalink: /pages/b912d1/ +hidden: true +index: false +--- + +# Spring 数据篇 + +## 📖 内容 + +- [Spring 之数据源](01.Spring之数据源.md) +- [Spring 之 JDBC](02.Spring之JDBC.md) +- [Spring 之事务](03.Spring之事务.md) +- [Spring 之 JPA](04.Spring之JPA.md) +- [Spring 集成 Mybatis](10.Spring集成Mybatis.md) +- [Spring 访问 Redis](21.Spring访问Redis.md) +- [Spring 访问 MongoDB](22.Spring访问MongoDB.md) +- [Spring 访问 Elasticsearch](23.Spring访问Elasticsearch.md) + +## 💻 示例 + +- **JDBC** + - [spring-data-jdbc-basics](https://github.com/dunwu/spring-tutorial/tree/master/codes/data/jdbc/basics) - Spring Boot 以 JDBC 方式访问关系型数据库,通过 `JdbcTemplate` 执行基本的 CRUD 操作。 + - [spring-data-jdbc-druid](https://github.com/dunwu/spring-tutorial/tree/master/codes/data/jdbc/druid) - SpringBoot 使用 [Druid](https://github.com/alibaba/druid) 作为数据库连接池。 + - [spring-data-jdbc-multi-datasource](https://github.com/dunwu/spring-tutorial/tree/master/codes/data/jdbc/multi-datasource) - SpringBoot 连接多数据源示例。 + - [spring-data-jdbc-xml](https://github.com/dunwu/spring-tutorial/tree/master/codes/data/jdbc/xml) - Spring 以 JDBC 方式访问关系型数据库,通过 `JdbcTemplate` 执行基本的 CRUD 操作。 +- **ORM** + - [spring-data-orm-jpa](https://github.com/dunwu/spring-tutorial/tree/master/codes/data/orm/jpa) - SpringBoot 使用 JPA 作为 ORM 框架访问数据库示例。 + - [spring-data-orm-mybatis](https://github.com/dunwu/spring-tutorial/tree/master/codes/data/orm/mybatis) - Spring 使用 [MyBatis](https://github.com/mybatis/mybatis-3) 作为 ORM 框架访问数据库示例。 + - [spring-data-orm-mybatis-mapper](https://github.com/dunwu/spring-tutorial/tree/master/codes/data/orm/mybatis-mapper) - SpringBoot 使用 [MyBatis](https://github.com/mybatis/mybatis-3) + [Mapper](https://github.com/abel533/Mapper) + [PageHelper](https://github.com/pagehelper/Mybatis-PageHelper) 作为 ORM 框架访问数据库示例。 + - [spring-data-orm-mybatis-multi-datasource](https://github.com/dunwu/spring-tutorial/tree/master/codes/data/orm/mybatis-multi-datasource) - SpringBoot 连接多数据源,并使用 [MyBatis Plus](https://github.com/baomidou/mybatis-plus) 作为 ORM 框架访问数据库示例。 + - [spring-data-orm-mybatis-plus](https://github.com/dunwu/spring-tutorial/tree/master/codes/data/orm/mybatis-plus) - SpringBoot 使用 [MyBatis Plus](https://github.com/baomidou/mybatis-plus) 作为 ORM 框架访问数据库示例。 +- **Nosql** + - [spring-data-nosql-basics](https://github.com/dunwu/spring-tutorial/tree/master/codes/data/nosql/basics) - Spring 访问各种 NoSQL 的示例。 + - [spring-data-nosql-mongodb](https://github.com/dunwu/spring-tutorial/tree/master/codes/data/nosql/mongodb) - SpringBoot 访问 [MongoDB](https://www.mongodb.com/) 的示例。 + - [spring-data-nosql-redis](https://github.com/dunwu/spring-tutorial/tree/master/codes/data/nosql/redis) - SpringBoot 访问 [Redis](https://redis.io/) 单节点、集群的示例。 + - [spring-data-nosql-elasticsearch](https://github.com/dunwu/spring-tutorial/tree/master/codes/data/nosql/elasticsearch) - SpringBoot 访问 [Elasticsearch](https://www.elastic.co/guide/index.html) 的示例。 + - [spring-data-nosql-hdfs](https://github.com/dunwu/spring-tutorial/tree/master/codes/data/nosql/hdfs) - SpringBoot 访问 HDFS 的示例。 +- **Cache** + - [spring-data-cache-basics](https://github.com/dunwu/spring-tutorial/tree/master/codes/data/cache/basics) - SpringBoot 默认缓存框架的示例。 + - [spring-data-cache-j2cache](https://github.com/dunwu/spring-tutorial/tree/master/codes/data/cache/j2cache) - SpringBoot 使用 [j2cache](https://gitee.com/ld/J2Cache) 作为缓存框架的示例。 + - [spring-data-cache-jetcache](https://github.com/dunwu/spring-tutorial/tree/master/codes/data/cache/jetcache) - SpringBoot 使用 [jetcache](https://github.com/alibaba/jetcache) 作为缓存框架的示例。 +- **中间件** + - [spring-data-middleware-flyway](https://github.com/dunwu/spring-tutorial/tree/master/codes/data/middleware/flyway) - Spring 使用版本管理中间件 Flyway 示例。 + - [spring-data-middleware-sharding](https://github.com/dunwu/spring-tutorial/tree/master/codes/data/middleware/sharding) - Spring 使用分库分表中间件示例。 + +## 📚 资料 + +- **官方** + - [Spring 官网](https://spring.io/) + - [Spring Framework 官方文档](https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/index.html) + - [Spring Github](https://github.com/spring-projects/spring-framework) +- **书籍** + - [《Spring In Action》](https://item.jd.com/12622829.html) +- **教程** + - [《小马哥讲 Spring 核心编程思想》](https://time.geekbang.org/course/intro/265) + - [geekbang-lessons](https://github.com/geektime-geekbang/geekbang-lessons) + - [跟我学 Spring3](http://jinnianshilongnian.iteye.com/blog/1482071) + +## 🚪 传送 + +◾ 💧 [钝悟的 IT 知识图谱](https://dunwu.github.io/waterdrop/) ◾ 🎯 [我的博客](https://github.com/dunwu/blog) ◾ \ No newline at end of file diff --git "a/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/03.SpringWeb/01.SpringWebMvc.md" "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/03.SpringWeb/01.SpringWebMvc.md" new file mode 100644 index 00000000..3e8d4b44 --- /dev/null +++ "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/03.SpringWeb/01.SpringWebMvc.md" @@ -0,0 +1,37 @@ +--- +title: spring-mvc +date: 2017-11-08 16:53:27 +order: 01 +categories: + - Java + - 框架 + - Spring + - SpringWeb +tags: + - Java + - 框架 + - Spring + - Web +permalink: /pages/65351b/ +--- + +# SpringMVC 简介 + +## SpringMVC 工作流程描述 + +Spring MVC 的工作流程可以用一幅图来说明: + +![img](https://raw.githubusercontent.com/dunwu/images/master/cs/java/spring/web/spring-dispatcher-servlet.png) + +1. 向服务器发送 HTTP 请求,请求被前端控制器 `DispatcherServlet` 捕获。 +2. `DispatcherServlet` 根据 **`-servlet.xml`** 中的配置对请求的 URL 进行解析,得到请求资源标识符(URI)。然后根据该 URI,调用 `HandlerMapping` 获得该 Handler 配置的所有相关的对象(包括 Handler 对象以及 Handler 对象对应的拦截器),最后以`HandlerExecutionChain` 对象的形式返回。 +3. `DispatcherServlet` 根据获得的`Handler`,选择一个合适的 `HandlerAdapter`。(附注:如果成功获得`HandlerAdapter`后,此时将开始执行拦截器的 preHandler(...)方法)。 +4. 提取`Request`中的模型数据,填充`Handler`入参,开始执行`Handler`(`Controller`)。 在填充`Handler`的入参过程中,根据你的配置,Spring 将帮你做一些额外的工作: + - HttpMessageConveter: 将请求消息(如 Json、xml 等数据)转换成一个对象,将对象转换为指定的响应信息。 + - 数据转换:对请求消息进行数据转换。如`String`转换成`Integer`、`Double`等。 + - 数据根式化:对请求消息进行数据格式化。 如将字符串转换成格式化数字或格式化日期等。 + - 数据验证: 验证数据的有效性(长度、格式等),验证结果存储到`BindingResult`或`Error`中。 +5. Handler(Controller)执行完成后,向 `DispatcherServlet` 返回一个 `ModelAndView` 对象; +6. 根据返回的`ModelAndView`,选择一个适合的 `ViewResolver`(必须是已经注册到 Spring 容器中的`ViewResolver`)返回给`DispatcherServlet`。 +7. `ViewResolver` 结合`Model`和`View`,来渲染视图。 +8. 视图负责将渲染结果返回给客户端。 \ No newline at end of file diff --git "a/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/03.SpringWeb/21.SpringBoot\344\271\213\345\272\224\347\224\250EasyUI.md" "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/03.SpringWeb/21.SpringBoot\344\271\213\345\272\224\347\224\250EasyUI.md" new file mode 100644 index 00000000..ef62068a --- /dev/null +++ "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/03.SpringWeb/21.SpringBoot\344\271\213\345\272\224\347\224\250EasyUI.md" @@ -0,0 +1,509 @@ +--- +title: SpringBoot 之应用 EasyUI +date: 2019-01-08 17:19:34 +order: 21 +categories: + - Java + - 框架 + - Spring + - SpringWeb +tags: + - Java + - 框架 + - Spring + - SpringBoot + - Web +permalink: /pages/ad0516/ +--- + +# SpringBoot 之应用 EasyUI + +> EasyUI 是一个简单的用户界面组件的集合。由于 EasyUI 已经封装好大部分 UI 基本功能,能帮用户减少大量的 js 和 css 代码。所以,EasyUI 非常适合用于开发简单的系统或原型系统。 +> +> 本文示例使用技术点: +> +> - Spring Boot:主要使用了 spring-boot-starter-web、spring-boot-starter-data-jpa +> - EasyUI:按需加载,并没有引入所有的 EasyUI 特性 +> - 数据库:为了测试方便,使用 H2 + +![img](http://www.jeasyui.cn/images/easyui.png) + +## 简介 + +### 什么是 EasyUI? + +- easyui 是基于 jQuery、Angular.、Vue 和 React 的用户界面组件的集合。 +- easyui 提供了构建现代交互式 javascript 应用程序的基本功能。 +- 使用 easyui,您不需要编写许多 javascript 代码,通常通过编写一些 HTML 标记来定义用户界面。 +- 完整的 HTML5 网页框架。 +- 使用 easyui 开发你的产品时可以大量节省你的时间和规模。 +- easyui 使用非常简单但功能非常强大。 + +## Spring Boot 整合 EasyUI + +### 配置 + +application.properties 修改: + +```properties +spring.mvc.view.prefix = /views/ +spring.mvc.view.suffix = .html +``` + +### 引入 easyui + +EasyUI 下载地址:http://www.jeasyui.cn/download.html + +在 `src/main/resources/static` 目录下引入 easyui。 + +然后在 html 中引用: + +```html + + + + + + + + + + + + + + + +``` + +引入 easyui 后,需要使用哪种组件,可以查看相关文档或 API,十分简单,此处不一一赘述。 + +## 实战 + +### 引入 maven 依赖 + +```xml + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springframework.boot + spring-boot-starter-tomcat + provided + + + org.springframework.boot + spring-boot-starter-test + test + + + + com.h2database + h2 + + + org.springframework.boot + spring-boot-devtools + + + commons-collections + commons-collections + 3.2.2 + + +``` + +### 使用 JPA + +为了使用 JPA 技术访问数据,我们需要定义 Entity 和 Repository + +定义一个 Entity: + +```java +@Entity +public class User { + + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + private Long id; + private String firstName; + private String lastName; + private String phone; + private String email; + + protected User() {} + + public User(String firstName, String lastName, String phone, String email) { + this.firstName = firstName; + this.lastName = lastName; + this.phone = phone; + this.email = email; + } + + // 略 getter/setter +} +``` + +定义一个 Repository: + +``` +public interface UserRepository extends CrudRepository { + + List findByLastName(String lastName); +} +``` + +### 使用 Web + +首页 Controller,将 web 请求定向到指定页面(下面的例子定向到 index.html) + +```java +@Controller +public class IndexController { + + @RequestMapping(value = {"", "/", "index"}) + public String index() { + return "index"; + } + +} +``` + +此外,需要定义一个 Controller,提供后台的 API 接口 + +```java +@Controller +public class UserController { + + @Autowired + private UserRepository customerRepository; + + @RequestMapping(value = "/user", method = RequestMethod.GET) + public String user() { + return "user"; + } + + @ResponseBody + @RequestMapping(value = "/user/list") + public ResponseDTO list() { + Iterable all = customerRepository.findAll(); + List list = IteratorUtils.toList(all.iterator()); + return new ResponseDTO<>(true, list.size(), list); + } + + @ResponseBody + @RequestMapping(value = "/user/add") + public ResponseDTO add(User user) { + User result = customerRepository.save(user); + List list = new ArrayList<>(); + list.add(result); + return new ResponseDTO<>(true, 1, list); + } + + @ResponseBody + @RequestMapping(value = "/user/save") + public ResponseDTO save(@RequestParam("id") Long id, User user) { + user.setId(id); + customerRepository.save(user); + List list = new ArrayList<>(); + list.add(user); + return new ResponseDTO<>(true, 1, list); + } + + @ResponseBody + @RequestMapping(value = "/user/delete") + public ResponseDTO delete(@RequestParam("id") Long id) { + customerRepository.deleteById(id); + return new ResponseDTO<>(true, null, null); + } + +} +``` + +### 使用 EasyUI + +接下来,我们要使用前面定义的后台接口,仅需要在 EasyUI API 中指定 `url` 即可。 + +请留意下面示例中的 url 字段,和实际接口是一一对应的。 + +```html + + + + Complex Layout - jQuery EasyUI Demo + + + + + + + + + + +
+

基本的 CRUD 应用

+

数据来源于后台系统

+ + + + + + + + + + + +
IDFirst NameLast NamePhoneEmail
+
+ 添加 + 修改 + 删除 +
+ +
+
+

User Information

+
+ +
+
+ +
+
+ +
+
+ +
+
+
+
+ Save + Cancel +
+
+ + + + +``` + +## 完整示例 + +请参考 [源码](https://github.com/dunwu/spring-boot-tutorial/tree/master/codes/spring-boot-web-ui/spring-boot-web-ui-easyui) + +运行方式: + +``` +mvn clean package -DskipTests=true +java -jar target/ +``` + +在浏览器中访问:http://localhost:8080/ + +## 引用和引申 + +- [EasyUI 官网](http://www.jeasyui.com/) +- [EasyUI 中文网](http://www.jeasyui.cn/) \ No newline at end of file diff --git "a/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/03.SpringWeb/README.md" "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/03.SpringWeb/README.md" new file mode 100644 index 00000000..0d69c4ff --- /dev/null +++ "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/03.SpringWeb/README.md" @@ -0,0 +1,44 @@ +--- +title: Spring Web +date: 2020-02-26 23:48:06 +categories: + - Java + - 框架 + - Spring + - SpringWeb +tags: + - Java + - 框架 + - Spring + - SpringBoot + - Web +permalink: /pages/e2586a/ +hidden: true +index: false +--- + +# Spring Web + +> 章节主要针对:Spring 在 web 领域的应用。如:Spring MVC、WebSocket 等。 + +## 📖 内容 + +- [Spring WebMvc](01.SpringWebMvc.md) +- [SpringBoot 之应用 EasyUI](21.SpringBoot之应用EasyUI.md) + +## 📚 资料 + +- **官方** + - [Spring 官网](https://spring.io/) + - [Spring Framework 官方文档](https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/index.html) + - [Spring Github](https://github.com/spring-projects/spring-framework) +- **书籍** + - [《Spring In Action》](https://item.jd.com/12622829.html) +- **教程** + - [《小马哥讲 Spring 核心编程思想》](https://time.geekbang.org/course/intro/265) + - [geekbang-lessons](https://github.com/geektime-geekbang/geekbang-lessons) + - [跟我学 Spring3](http://jinnianshilongnian.iteye.com/blog/1482071) + +## 🚪 传送 + +◾ 💧 [钝悟的 IT 知识图谱](https://dunwu.github.io/waterdrop/) ◾ 🎯 [我的博客](https://github.com/dunwu/blog) ◾ \ No newline at end of file diff --git "a/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/04.SpringIO/01.SpringBoot\344\271\213\345\274\202\346\255\245\350\257\267\346\261\202.md" "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/04.SpringIO/01.SpringBoot\344\271\213\345\274\202\346\255\245\350\257\267\346\261\202.md" new file mode 100644 index 00000000..605bc8ad --- /dev/null +++ "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/04.SpringIO/01.SpringBoot\344\271\213\345\274\202\346\255\245\350\257\267\346\261\202.md" @@ -0,0 +1,139 @@ +--- +title: spring-boot-async +date: 2019-11-18 14:55:01 +order: 01 +categories: + - Java + - 框架 + - Spring + - SpringIO +tags: + - Java + - 框架 + - Spring + - SpringBoot + - 异步 +permalink: /pages/92add2/ +--- + +# SpringBoot 教程之处理异步请求 + +## `@EnableAsync` 注解 + +要使用 `@Async`,首先需要使用 `@EnableAsync` 注解开启 Spring Boot 中的异步特性。 + +```java +@Configuration +@EnableAsync +public class AppConfig { +} +``` + +更详细的配置说明,可以参考:[`AsyncConfigurer`](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/scheduling/annotation/AsyncConfigurer.html) + +## `@Async` 注解 + +### 支持的用法 + +(1)**无入参无返回值方法** + +您可以用 `@Async` 注解修饰方法,这表明这个方法是异步方式调用。换句话说,程序在调用此方法时会立即返回,而方法的实际执行发生在已提交给 Spring `TaskExecutor` 的任务中。在最简单的情况下,您可以将注解应用于返回 void 的方法,如以下示例所示: + +```java +@Async +void doSomething() { + // this will be executed asynchronously +} +``` + +(2)**有入参无返回值方法** + +与使用 `@Scheduled` 注释注释的方法不同,这些方法可以指定参数,因为它们在运行时由调用者以“正常”方式调用,而不是由容器管理的调度任务调用。例如,以下代码是 `@Async` 注解的合法应用: + +```java +@Async +void doSomething(String s) { + // this will be executed asynchronously +} +``` + +(3)**有入参有返回值方法** + +甚至可以异步调用返回值的方法。但是,这些方法需要具有 `Future` 类型的返回值。这仍然提供了异步执行的好处,以便调用者可以在调用 `Future` 上的 `get()` 之前执行其他任务。以下示例显示如何在返回值的方法上使用`@Async`: + +```java +@Async +Future returnSomething(int i) { + // this will be executed asynchronously +} +``` + +### 不支持的用法 + +`@Async` 不能与生命周期回调一起使用,例如 `@PostConstruct`。 + +要异步初始化 Spring bean,必须使用单独的初始化 Spring bean,然后在目标上调用 `@Async` 带注释的方法,如以下示例所示: + +```java +public class SampleBeanImpl implements SampleBean { + + @Async + void doSomething() { + // ... + } + +} + +public class SampleBeanInitializer { + + private final SampleBean bean; + + public SampleBeanInitializer(SampleBean bean) { + this.bean = bean; + } + + @PostConstruct + public void initialize() { + bean.doSomething(); + } + +} +``` + +## 明确指定执行器 + +默认情况下,在方法上指定 `@Async` 时,使用的执行器是在启用异步支持时配置的执行器,即如果使用 XML 或 `AsyncConfigurer` 实现(如果有),则为 `annotation-driven` 元素。但是,如果需要指示在执行给定方法时应使用默认值以外的执行器,则可以使用 `@Async` 注解的 value 属性。以下示例显示了如何执行此操作: + +```java +@Async("otherExecutor") +void doSomething(String s) { + // this will be executed asynchronously by "otherExecutor" +} +``` + +在这种情况下,“otherExecutor”可以是 Spring 容器中任何 Executor bean 的名称,也可以是与任何 Executor 关联的限定符的名称(例如,使用 `` 元素或 Spring 的 `@Qualifier` 注释指定) )。 + +## 管理 `@Async` 的异常 + +当 `@Async` 方法的返回值类型为 `Future` 型时,很容易管理在方法执行期间抛出的异常,因为在调用 `get` 结果时会抛出此异常。但是,对于返回值类型为 void 型的方法,异常不会被捕获且无法传输。您可以提供 `AsyncUncaughtExceptionHandler` 来处理此类异常。以下示例显示了如何执行此操作: + +```java +public class MyAsyncUncaughtExceptionHandler implements AsyncUncaughtExceptionHandler { + + @Override + public void handleUncaughtException(Throwable ex, Method method, Object... params) { + // handle exception + } +} +``` + +默认情况下,仅记录异常。您可以使用 `AsyncConfigurer` 或 `` XML 元素定义自定义 `AsyncUncaughtExceptionHandler`。 + +## 示例源码 + +> 示例源码:[spring-boot-async](https://github.com/dunwu/spring-boot-tutorial/tree/master/codes/spring-boot-async) + +## 参考资料 + +- [Spring Boot 官方文档之 boot-features-external-config](https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#boot-features-external-config) +- [Spring Boot 官方文档之 scheduling-annotation-support](https://docs.spring.io/spring/docs/current/spring-framework-reference/integration.html#scheduling-annotation-support) \ No newline at end of file diff --git "a/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/04.SpringIO/02.SpringBoot\344\271\213Json.md" "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/04.SpringIO/02.SpringBoot\344\271\213Json.md" new file mode 100644 index 00000000..0e5da987 --- /dev/null +++ "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/04.SpringIO/02.SpringBoot\344\271\213Json.md" @@ -0,0 +1,272 @@ +--- +title: SpringBoot 之集成 Json +date: 2018-12-30 22:24:16 +order: 02 +categories: + - Java + - 框架 + - Spring + - SpringIO +tags: + - Java + - 框架 + - Spring + - SpringBoot + - JSON +permalink: /pages/676725/ +--- + +# SpringBoot 之集成 Json + +## 简介 + +### Spring Boot 支持的 Json 库 + +Spring Boot 支持三种 Json 库: + +- Gson +- Jackson +- JSON-B + +**Jackson 是 Spring Boot 官方推荐的默认库。** + +Spring Boot 提供了 Jackson 的自动配置,Jackson 是 `spring-boot-starter-json` 的一部分。当 Jackson 在类路径上时,会自动配置 ObjectMapper bean。 + +Spring Boot 提供了 Gson 的自动配置。当 Gson 在 classpath 上时,会自动配置 Gson bean。提供了几个 `spring.gson.*` 配置属性来自定义配置。为了获得更多控制,可以使用一个或多个 `GsonBuilderCustomizer` bean。 + +Spring Boot 提供了 JSON-B 的自动配置。当 JSON-B API 在 classpath 上时,将自动配置 Jsonb bean。首选的 JSON-B 实现是 Apache Johnzon,它提供了依赖关系管理。 + +### Spring Web 中的序列化、反序列化 + +以下注解都是 `spring-web` 中提供的支持。 + +#### `@ResponseBody` + +`@Responsebody` 注解用于将 Controller 的方法返回的对象,通过适当的 `HttpMessageConverter` 转换为指定格式后,写入到 HTTP Response 对象的 body 数据区。一般在异步获取数据时使用。通常是在使用 `@RequestMapping` 后,返回值通常解析为跳转路径,加上 @Responsebody 后返回结果不会被解析为跳转路径,而是直接写入 HTTP 响应正文中。 + +示例: + +```java +@ResponseBody +@RequestMapping(name = "/getInfo", method = RequestMethod.GET) +public InfoDTO getInfo() { + return new InfoDTO(); +} +``` + +#### `@RequestBody` + +@RequestBody 注解用于读取 HTTP Request 请求的 body 部分数据,使用系统默认配置的 `HttpMessageConverter` 进行解析,然后把相应的数据绑定到要返回的对象上;再把 `HttpMessageConverter` 返回的对象数据绑定到 controller 中方法的参数上。 + +request 的 body 部分的数据编码格式由 header 部分的 `Content-Type` 指定。 + +示例: + +```java +@RequestMapping(name = "/postInfo", method = RequestMethod.POST) +public void postInfo(@RequestBody InfoDTO infoDTO) { + // ... +} +``` + +#### `@RestController` + +Spring 4 以前: + +如果需要返回到指定页面,则需要用 `@Controller` 配合视图解析器 `InternalResourceViewResolver` 。 + +如果需要返回 JSON,XML 或自定义 mediaType 内容到页面,则需要在对应的方法上加上 `@ResponseBody` 注解。 + +Spring 4 以后,新增了 `@RestController` 注解: + +它相当于 `@Controller` + `@RequestBody` 。 + +如果使用 `@RestController` 注解 Controller,则 Controller 中的方法无法返回 jsp 页面,或者 html,配置的视图解析器 `InternalResourceViewResolver` 将不起作用,直接返回内容。 + +## 指定类的 Json 序列化、反序列化 + +如果使用 Jackson 序列化和反序列化 JSON 数据,您可能需要编写自己的 `JsonSerializer` 和 `JsonDeserializer` 类。自定义序列化程序通常通过模块向 Jackson 注册,但 Spring Boot 提供了另一种 `@JsonComponent` 注释,可以更容易地直接注册 Spring Beans。 + +您可以直接在 `JsonSerializer` 或 `JsonDeserializer` 实现上使用 `@JsonComponent` 注释。您还可以在包含序列化程序/反序列化程序作为内部类的类上使用它,如以下示例所示: + +```java +import java.io.*; +import com.fasterxml.jackson.core.*; +import com.fasterxml.jackson.databind.*; +import org.springframework.boot.jackson.*; + +@JsonComponent +public class Example { + + public static class Serializer extends JsonSerializer { + // ... + } + + public static class Deserializer extends JsonDeserializer { + // ... + } + +} +``` + +`ApplicationContext` 中的所有 `@JsonComponent` bean 都会自动注册到 Jackson。因为 `@JsonComponent` 是使用 `@Component` 进行元注释的,所以通常的组件扫描规则适用。 + +Spring Boot 还提供了 [`JsonObjectSerializer`](https://github.com/spring-projects/spring-boot/tree/v2.1.1.RELEASE/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/jackson/JsonObjectSerializer.java) 和 [`JsonObjectDeserializer`](https://github.com/spring-projects/spring-boot/tree/v2.1.1.RELEASE/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/jackson/JsonObjectDeserializer.java) 基类,它们在序列化对象时提供了标准 Jackson 版本的有用替代方法。有关详细信息,请参阅 Javadoc 中的 [`JsonObjectSerializer`](https://docs.spring.io/spring-boot/docs/current/api/org/springframework/boot/jackson/JsonObjectSerializer.html) 和 [`JsonObjectDeserializer`](https://docs.spring.io/spring-boot/docs/current/api/org/springframework/boot/jackson/JsonObjectDeserializer.html)。 + +## @JsonTest + +使用 `@JsonTest` 可以很方便的在 Spring Boot 中测试序列化、反序列化。 + +使用 `@JsonTest` 相当于使用以下自动配置: + +``` +org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration org.springframework.boot.autoconfigure.gson.GsonAutoConfiguration org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration org.springframework.boot.autoconfigure.jsonb.JsonbAutoConfiguration org.springframework.boot.test.autoconfigure.json.JsonTestersAutoConfiguration +``` + +`@JsonTest` 使用示例: + +想试试完整示例,可以参考:[源码](https://github.com/dunwu/spring-boot-tutorial/tree/master/codes/spring-boot-web-fastjson) + +```java +@JsonTest +@RunWith(SpringRunner.class) +public class SimpleJsonTest { + + private final Logger log = LoggerFactory.getLogger(this.getClass()); + + @Autowired + private JacksonTester json; + + @Test + public void testSerialize() throws Exception { + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + InfoDTO infoDTO = new InfoDTO("JSON测试应用", "1.0.0", sdf.parse("2019-01-01 12:00:00")); + JsonContent jsonContent = json.write(infoDTO); + log.info("json content: {}", jsonContent.getJson()); + // 或者使用基于JSON path的校验 + assertThat(jsonContent).hasJsonPathStringValue("@.appName"); + assertThat(jsonContent).extractingJsonPathStringValue("@.appName").isEqualTo("JSON测试应用"); + assertThat(jsonContent).hasJsonPathStringValue("@.version"); + assertThat(jsonContent).extractingJsonPathStringValue("@.version").isEqualTo("1.0.0"); + assertThat(jsonContent).hasJsonPathStringValue("@.date"); + assertThat(jsonContent).extractingJsonPathStringValue("@.date").isEqualTo("2019-01-01 12:00:00"); + } + + @Test + public void testDeserialize() throws Exception { + String content = "{\"appName\":\"JSON测试应用\",\"version\":\"1.0.0\",\"date\":\"2019-01-01\"}"; + InfoDTO actual = json.parseObject(content); + assertThat(actual.getAppName()).isEqualTo("JSON测试应用"); + assertThat(actual.getVersion()).isEqualTo("1.0.0"); + } +} +``` + +## Spring Boot 中的 json 配置 + +### Jackson 配置 + +当 Spring Boot 的 json 库为 jackson 时,可以使用以下配置属性(对应 [`JacksonProperties`](https://github.com/spring-projects/spring-boot/tree/v2.1.1.RELEASE/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jackson/JacksonProperties.java) 类): + +```properties +spring.jackson.date-format= # Date format string or a fully-qualified date format class name. For instance, `yyyy-MM-dd HH:mm:ss`. +spring.jackson.default-property-inclusion= # Controls the inclusion of properties during serialization. Configured with one of the values in Jackson's JsonInclude.Include enumeration. +spring.jackson.deserialization.*= # Jackson on/off features that affect the way Java objects are deserialized. +spring.jackson.generator.*= # Jackson on/off features for generators. +spring.jackson.joda-date-time-format= # Joda date time format string. If not configured, "date-format" is used as a fallback if it is configured with a format string. +spring.jackson.locale= # Locale used for formatting. +spring.jackson.mapper.*= # Jackson general purpose on/off features. +spring.jackson.parser.*= # Jackson on/off features for parsers. +spring.jackson.property-naming-strategy= # One of the constants on Jackson's PropertyNamingStrategy. Can also be a fully-qualified class name of a PropertyNamingStrategy subclass. +spring.jackson.serialization.*= # Jackson on/off features that affect the way Java objects are serialized. +spring.jackson.time-zone= # Time zone used when formatting dates. For instance, "America/Los_Angeles" or "GMT+10". +spring.jackson.visibility.*= # Jackson visibility thresholds that can be used to limit which methods (and fields) are auto-detected. +``` + +### GSON 配置 + +当 Spring Boot 的 json 库为 gson 时,可以使用以下配置属性(对应 [`GsonProperties`](https://github.com/spring-projects/spring-boot/tree/v2.1.1.RELEASE/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/gson/GsonProperties.java) 类): + +```properties +spring.gson.date-format= # Format to use when serializing Date objects. +spring.gson.disable-html-escaping= # Whether to disable the escaping of HTML characters such as '<', '>', etc. +spring.gson.disable-inner-class-serialization= # Whether to exclude inner classes during serialization. +spring.gson.enable-complex-map-key-serialization= # Whether to enable serialization of complex map keys (i.e. non-primitives). +spring.gson.exclude-fields-without-expose-annotation= # Whether to exclude all fields from consideration for serialization or deserialization that do not have the "Expose" annotation. +spring.gson.field-naming-policy= # Naming policy that should be applied to an object's field during serialization and deserialization. +spring.gson.generate-non-executable-json= # Whether to generate non executable JSON by prefixing the output with some special text. +spring.gson.lenient= # Whether to be lenient about parsing JSON that doesn't conform to RFC 4627. +spring.gson.long-serialization-policy= # Serialization policy for Long and long types. +spring.gson.pretty-printing= # Whether to output serialized JSON that fits in a page for pretty printing. +spring.gson.serialize-nulls= # Whether to serialize null fields. +``` + +## Spring Boot 中使用 Fastjson + +国内很多的 Java 程序员更喜欢使用阿里的 fastjson 作为 json lib。那么,如何在 Spring Boot 中将其替换默认的 jackson 库呢? + +你需要做如下处理: + +(1)引入 fastjson jar 包: + +```xml + + com.alibaba + fastjson + 1.2.54 + +``` + +(2)实现 WebMvcConfigurer 接口,自定义 `configureMessageConverters` 接口。如下所示: + +```java +@Configuration +public class WebMvcConfig implements WebMvcConfigurer { + + private final Logger log = LoggerFactory.getLogger(this.getClass()); + + /** + * 自定义消息转换器 + * @param converters + */ + @Override + public void configureMessageConverters(List> converters) { + // 清除默认 Json 转换器 + converters.removeIf(converter -> converter instanceof MappingJackson2HttpMessageConverter); + + // 配置 FastJson + FastJsonConfig config = new FastJsonConfig(); + config.setSerializerFeatures(SerializerFeature.QuoteFieldNames, SerializerFeature.WriteEnumUsingToString, + SerializerFeature.WriteMapNullValue, SerializerFeature.WriteDateUseDateFormat, + SerializerFeature.DisableCircularReferenceDetect); + + // 添加 FastJsonHttpMessageConverter + FastJsonHttpMessageConverter fastJsonHttpMessageConverter = new FastJsonHttpMessageConverter(); + fastJsonHttpMessageConverter.setFastJsonConfig(config); + List fastMediaTypes = new ArrayList<>(); + fastMediaTypes.add(MediaType.APPLICATION_JSON_UTF8); + fastJsonHttpMessageConverter.setSupportedMediaTypes(fastMediaTypes); + converters.add(fastJsonHttpMessageConverter); + + // 添加 StringHttpMessageConverter,解决中文乱码问题 + StringHttpMessageConverter stringHttpMessageConverter = new StringHttpMessageConverter(Charset.forName("UTF-8")); + converters.add(stringHttpMessageConverter); + } + + // ... +} +``` + +## 示例源码 + +完整示例:[源码](https://github.com/dunwu/spring-boot-tutorial/tree/master/codes/spring-boot-web-fastjson) + +## 引申和引用 + +**引申** + +- [Spring Boot 教程](https://github.com/dunwu/spring-boot-tutorial) + +**引用** + +- [Spring Boot 官方文档之 boot-features-json](https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#boot-features-json) \ No newline at end of file diff --git "a/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/04.SpringIO/03.SpringBoot\344\271\213\351\202\256\344\273\266.md" "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/04.SpringIO/03.SpringBoot\344\271\213\351\202\256\344\273\266.md" new file mode 100644 index 00000000..f2c2259c --- /dev/null +++ "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/04.SpringIO/03.SpringBoot\344\271\213\351\202\256\344\273\266.md" @@ -0,0 +1,278 @@ +--- +title: SpringBoot 之发送邮件 +date: 2019-11-20 15:20:44 +order: 03 +categories: + - Java + - 框架 + - Spring + - SpringIO +tags: + - Java + - 框架 + - Spring + - SpringBoot + - 邮件 +permalink: /pages/2586f1/ +--- + +# SpringBoot 之发送邮件 + +## 简介 + +Spring Boot 收发邮件最简便方式是通过 `spring-boot-starter-mail`。 + +```xml + + org.springframework.boot + spring-boot-starter-mail + +``` + +spring-boot-starter-mail 本质上是使用 JavaMail(javax.mail)。如果想对 JavaMail 有进一步了解,可以参考: [JavaMail 使用指南](https://dunwu.github.io/java-tutorial/#/javalib/javamail) + +## API + +Spring Framework 提供了一个使用 `JavaMailSender` 接口发送电子邮件的简单抽象,这是发送邮件的核心 API。 + +`JavaMailSender` 接口提供的 API 如下: + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20190110111102.png) + +## 配置 + +Spring Boot 为 `JavaMailSender` 提供了自动配置以及启动器模块。 + +如果 `spring.mail.host` 和相关库(由 spring-boot-starter-mail 定义)可用,则 Spring Boot 会创建默认 `JavaMailSender`(如果不存在)。可以通过 `spring.mail` 命名空间中的配置项进一步自定义发件人。 +特别是,某些默认超时值是无限的,您可能希望更改它以避免线程被无响应的邮件服务器阻塞,如以下示例所示: + +```properties +spring.mail.properties.mail.smtp.connectiontimeout=5000 +spring.mail.properties.mail.smtp.timeout=3000 +spring.mail.properties.mail.smtp.writetimeout=5000 +``` + +也可以使用 JNDI 中的现有会话配置 `JavaMailSender`: + +``` +spring.mail.jndi-name=mail/Session +``` + +以下为 Spring Boot 关于 Mail 的配置: + +有关更多详细信息,请参阅 [`MailProperties`](https://github.com/spring-projects/spring-boot/tree/v2.1.1.RELEASE/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/mail/MailProperties.java)。 + +```properties +# Email (MailProperties) +spring.mail.default-encoding=UTF-8 # Default MimeMessage encoding. +spring.mail.host= # SMTP server host. For instance, `smtp.example.com`. +spring.mail.jndi-name= # Session JNDI name. When set, takes precedence over other Session settings. +spring.mail.password= # Login password of the SMTP server. +spring.mail.port= # SMTP server port. +spring.mail.properties.*= # Additional JavaMail Session properties. +spring.mail.protocol=smtp # Protocol used by the SMTP server. +spring.mail.test-connection=false # Whether to test that the mail server is available on startup. +spring.mail.username= # Login user of the SMTP server. +``` + +## 实战 + +### 引入依赖 + +```xml + + + org.springframework.boot + spring-boot-starter-mail + + + org.springframework.boot + spring-boot-starter-test + test + + + + org.projectlombok + lombok + + + com.github.dozermapper + dozer-spring-boot-starter + 6.4.0 + + +``` + +### 配置邮件属性 + +在 `src/main/resources` 目录下添加 `application-163.properties` 配置文件,内容如下: + +```properties +spring.mail.host = smtp.163.com +spring.mail.username = xxxxxx +spring.mail.password = xxxxxx +spring.mail.properties.mail.smtp.auth = true +spring.mail.properties.mail.smtp.starttls.enable = true +spring.mail.properties.mail.smtp.starttls.required = true +spring.mail.default-encoding = UTF-8 + +mail.domain = 163.com +mail.from = ${spring.mail.username}@${mail.domain} +``` + +注:需替换有效的 `spring.mail.username`、`spring.mail.password`。 + +`application-163.properties` 配置文件表示使用 163 邮箱时的配置,为了使之生效,需要通过 `spring.profiles.active = 163` 来激活它。 + +在 `src/main/resources` 目录下添加 `application.properties` 配置文件,内容如下: + +```properties +spring.profiles.active = 163 +``` + +### Java 代码 + +首先,需要读取部分配置属性,方法如下: + +```java +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; +import org.springframework.validation.annotation.Validated; + +@Validated +@Component +@ConfigurationProperties(prefix = "mail") +public class MailProperties { + private String domain; + private String from; + + public String getDomain() { + return domain; + } + + public void setDomain(String domain) { + this.domain = domain; + } + + public String getFrom() { + return from; + } + + public void setFrom(String from) { + this.from = from; + } +} +``` + +接着,定义一个邮件参数实体类(使用 lombok 简化了 getter、setter): + +```java +import lombok.Data; +import java.util.Date; + +@Data +public class MailDTO { + private String from; + private String replyTo; + private String[] to; + private String[] cc; + private String[] bcc; + private Date sentDate; + private String subject; + private String text; + private String[] filenames; +} +``` + +接着,实现发送邮件的功能接口: + +```java +import com.github.dozermapper.core.Mapper; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.mail.SimpleMailMessage; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.mail.javamail.MimeMessageHelper; +import org.springframework.stereotype.Service; + +import javax.mail.MessagingException; +import javax.mail.internet.MimeBodyPart; +import javax.mail.internet.MimeMessage; +import javax.mail.internet.MimeMultipart; +import java.io.IOException; + +@Service +public class MailService { + + private final Logger log = LoggerFactory.getLogger(this.getClass()); + + @Autowired + private MailProperties mailProperties; + + @Autowired + private JavaMailSender javaMailSender; + + @Autowired + private Mapper mapper; + + public void sendSimpleMailMessage(MailDTO mailDTO) { + SimpleMailMessage simpleMailMessage = mapper.map(mailDTO, SimpleMailMessage.class); + if (StringUtils.isEmpty(mailDTO.getFrom())) { + mailDTO.setFrom(mailProperties.getFrom()); + } + javaMailSender.send(simpleMailMessage); + } + + public void sendMimeMessage(MailDTO mailDTO) { + + MimeMessage mimeMessage = javaMailSender.createMimeMessage(); + MimeMessageHelper messageHelper; + try { + messageHelper = new MimeMessageHelper(mimeMessage, true); + + if (StringUtils.isEmpty(mailDTO.getFrom())) { + messageHelper.setFrom(mailProperties.getFrom()); + } + messageHelper.setTo(mailDTO.getTo()); + messageHelper.setSubject(mailDTO.getSubject()); + + mimeMessage = messageHelper.getMimeMessage(); + MimeBodyPart mimeBodyPart = new MimeBodyPart(); + mimeBodyPart.setContent(mailDTO.getText(), "text/html;charset=UTF-8"); + + // 描述数据关系 + MimeMultipart mm = new MimeMultipart(); + mm.setSubType("related"); + mm.addBodyPart(mimeBodyPart); + + // 添加邮件附件 + for (String filename : mailDTO.getFilenames()) { + MimeBodyPart attachPart = new MimeBodyPart(); + try { + attachPart.attachFile(filename); + } catch (IOException e) { + e.printStackTrace(); + } + mm.addBodyPart(attachPart); + } + mimeMessage.setContent(mm); + mimeMessage.saveChanges(); + + } catch (MessagingException e) { + e.printStackTrace(); + } + + javaMailSender.send(mimeMessage); + } +} +``` + +## 示例源码 + +> 示例源码:[spring-boot-mail](https://github.com/dunwu/spring-boot-tutorial/tree/master/codes/spring-boot-mail) + +## 参考资料 + +- [Spring Boot 官方文档之 Sending Email](https://docs.spring.io/spring-boot/docs/2.1.1.RELEASE/reference/htmlsingle/#boot-features-email) \ No newline at end of file diff --git "a/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/04.SpringIO/README.md" "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/04.SpringIO/README.md" new file mode 100644 index 00000000..32a77f97 --- /dev/null +++ "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/04.SpringIO/README.md" @@ -0,0 +1,43 @@ +--- +title: Spring IO +date: 2022-09-18 11:34:00 +categories: + - Java + - 框架 + - Spring + - SpringIO +tags: + - Java + - 框架 + - Spring + - SpringBoot + - IO +permalink: /pages/56581b/ +hidden: true +index: false +--- + +# Spring IO + +## 📖 内容 + +- [SpringBoot 之异步请求](01.SpringBoot之异步请求.md) +- [SpringBoot 之 Json](02.SpringBoot之Json.md) +- [SpringBoot 之邮件](03.SpringBoot之邮件.md) + +## 📚 资料 + +- **官方** + - [Spring 官网](https://spring.io/) + - [Spring Framework 官方文档](https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/index.html) + - [Spring Github](https://github.com/spring-projects/spring-framework) +- **书籍** + - [《Spring In Action》](https://item.jd.com/12622829.html) +- **教程** + - [《小马哥讲 Spring 核心编程思想》](https://time.geekbang.org/course/intro/265) + - [geekbang-lessons](https://github.com/geektime-geekbang/geekbang-lessons) + - [跟我学 Spring3](http://jinnianshilongnian.iteye.com/blog/1482071) + +## 🚪 传送 + +◾ 💧 [钝悟的 IT 知识图谱](https://dunwu.github.io/waterdrop/) ◾ 🎯 [我的博客](https://github.com/dunwu/blog) ◾ \ No newline at end of file diff --git "a/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/05.Spring\351\233\206\346\210\220/01.Spring\351\233\206\346\210\220\347\274\223\345\255\230.md" "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/05.Spring\351\233\206\346\210\220/01.Spring\351\233\206\346\210\220\347\274\223\345\255\230.md" new file mode 100644 index 00000000..58c15632 --- /dev/null +++ "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/05.Spring\351\233\206\346\210\220/01.Spring\351\233\206\346\210\220\347\274\223\345\255\230.md" @@ -0,0 +1,230 @@ +--- +title: Spring集成缓存 +date: 2017-11-08 16:53:27 +order: 01 +categories: + - Java + - 框架 + - Spring + - Spring集成 +tags: + - Java + - 框架 + - Spring + - 集成 + - 缓存 +permalink: /pages/a311cb/ +--- + +# Spring 集成缓存中间件 + +> Spring 中提供了缓存功能的抽象,允许你在底层灵活的替换缓存实现,而对上层暴露相同的缓存接口。 + +## 缓存接口 + +Spring 的缓存 API 以注解方式提供。 + +### 开启注解 + +Spring 为缓存功能提供了注解功能,但是你必须启动注解。 +你有两个选择: +(1) 在 xml 中声明 +像上一节 spring-ehcache.xml 中的做法一样,使用`` + +```xml + +``` + +(2) 使用标记注解 +你也可以通过对一个类进行注解修饰的方式在这个类中使用缓存注解。 +范例如下: + +```java +@Configuration +@EnableCaching +public class AppConfig { +} +``` + +### 缓存注解使用 + +Spring 对缓存的支持类似于对事务的支持。 +首先使用注解标记方法,相当于定义了切点,然后使用 Aop 技术在这个方法的调用前、调用后获取方法的入参和返回值,进而实现了缓存的逻辑。 +下面三个注解都是方法级别: + +#### @Cacheable + +表明所修饰的方法是可以缓存的:当第一次调用这个方法时,它的结果会被缓存下来,在缓存的有效时间内,以后访问这个方法都直接返回缓存结果,不再执行方法中的代码段。 +这个注解可以用`condition`属性来设置条件,如果不满足条件,就不使用缓存能力,直接执行方法。 +可以使用`key`属性来指定 key 的生成规则。 + +#### @CachePut + +与`@Cacheable`不同,`@CachePut`不仅会缓存方法的结果,还会执行方法的代码段。 +它支持的属性和用法都与`@Cacheable`一致。 + +#### @CacheEvict + +与`@Cacheable`功能相反,`@CacheEvict`表明所修饰的方法是用来删除失效或无用的缓存数据。 +下面是`@Cacheable`、`@CacheEvict`和`@CachePut`基本使用方法的一个集中展示: + +```java +@Service +public class UserService { + // @Cacheable可以设置多个缓存,形式如:@Cacheable({"books", "isbns"}) + @Cacheable(value={"users"}, key="#user.id") + public User findUser(User user) { + return findUserInDB(user.getId()); + } + + @Cacheable(value = "users", condition = "#user.getId() <= 2") + public User findUserInLimit(User user) { + return findUserInDB(user.getId()); + } + + @CachePut(value = "users", key = "#user.getId()") + public void updateUser(User user) { + updateUserInDB(user); + } + + @CacheEvict(value = "users") + public void removeUser(User user) { + removeUserInDB(user.getId()); + } + + @CacheEvict(value = "users", allEntries = true) + public void clear() { + removeAllInDB(); + } +} +``` + +#### @Caching + +如果需要使用同一个缓存注解(`@Cacheable`、`@CacheEvict`或`@CachePut`)多次修饰一个方法,就需要用到`@Caching`。 + +```java +@Caching(evict = { @CacheEvict("primary"), @CacheEvict(cacheNames="secondary", key="#p0") }) +public Book importBooks(String deposit, Date date) +``` + +#### @CacheConfig + +与前面的缓存注解不同,这是一个类级别的注解。 +如果类的所有操作都是缓存操作,你可以使用`@CacheConfig`来指定类,省去一些配置。 + +```java +@CacheConfig("books") +public class BookRepositoryImpl implements BookRepository { + @Cacheable + public Book findBook(ISBN isbn) {...} +} +``` + +## 缓存存储 + +Spring 允许通过配置方式接入多种不同的缓存存储。用户可以根据实际需要选择。 + +不同的缓存存储,具有不同的性能和特性,如果想了解具体原理,可以参考:[全面理解缓存原理](https://dunwu.github.io/javatech/#/technology/cache/cache-theory?id=%e5%85%a8%e9%9d%a2%e7%90%86%e8%a7%a3%e7%bc%93%e5%ad%98%e5%8e%9f%e7%90%86)。这里不再赘述。 + +### 使用 ConcurrentHashMap 作为缓存 + +参考配置: + +```xml + + + + 使用 ConcurrentHashMap 作为 Spring 缓存 + + + + + + + + + + + + + + + + +``` + +### 使用 Ehcache 作为缓存 + +参考配置: + +```xml + + + + 使用 EhCache 作为 Spring 缓存 + + + + + + + + + + + + + + + +``` + +ehcache.xml 中的配置内容完全符合 Ehcache 的官方配置标准。 + +### 使用 Caffeine 作为缓存 + +参考配置: + +```xml + + + + 使用 Caffeine 作为 Spring 缓存 + + + + + + + + + +``` + +## 示例代码 + +我的示例代码地址:[spring-tutorial-integration-cache](https://github.com/dunwu/spring-tutorial/tree/master/spring-tutorial/spring-tutorial-integration/spring-tutorial-integration-cache) + +## 参考资料 + +- [Spring 官方文档之缓存抽象](https://docs.spring.io/spring/docs/current/spring-framework-reference/integration.html#cache) +- [注释驱动的 Spring cache 缓存介绍](http://www.ibm.com/developerworks/cn/opensource/os-cn-spring-cache/) \ No newline at end of file diff --git "a/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/05.Spring\351\233\206\346\210\220/02.Spring\351\233\206\346\210\220\350\260\203\345\272\246\345\231\250.md" "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/05.Spring\351\233\206\346\210\220/02.Spring\351\233\206\346\210\220\350\260\203\345\272\246\345\231\250.md" new file mode 100644 index 00000000..2f460e46 --- /dev/null +++ "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/05.Spring\351\233\206\346\210\220/02.Spring\351\233\206\346\210\220\350\260\203\345\272\246\345\231\250.md" @@ -0,0 +1,350 @@ +--- +title: Spring 集成调度器 +date: 2017-11-08 16:53:27 +order: 02 +categories: + - Java + - 框架 + - Spring + - Spring集成 +tags: + - Java + - 框架 + - Spring + - 集成 + - 调度器 +permalink: /pages/a187f0/ +--- + +# Spring 集成调度器 + +## 概述 + +如果想在 Spring 中使用任务调度功能,除了集成调度框架 Quartz 这种方式,也可以使用 Spring 自己的调度任务框架。 +使用 Spring 的调度框架,优点是:支持注解`@Scheduler`,可以省去大量的配置。 + +## 实时触发调度任务 + +### TaskScheduler 接口 + +Spring3 引入了`TaskScheduler`接口,这个接口定义了调度任务的抽象方法。 +TaskScheduler 接口的声明: + +```java +public interface TaskScheduler { + + ScheduledFuture schedule(Runnable task, Trigger trigger); + + ScheduledFuture schedule(Runnable task, Date startTime); + + ScheduledFuture scheduleAtFixedRate(Runnable task, Date startTime, long period); + + ScheduledFuture scheduleAtFixedRate(Runnable task, long period); + + ScheduledFuture scheduleWithFixedDelay(Runnable task, Date startTime, long delay); + + ScheduledFuture scheduleWithFixedDelay(Runnable task, long delay); + +} +``` + +从以上方法可以看出 TaskScheduler 有两类重要参数: + +- 一个是要调度的方法,即一个实现了 Runnable 接口的线程类的 run()方法; +- 另一个就是触发条件。 + +**TaskScheduler 接口的实现类** +它有三个实现类:`DefaultManagedTaskScheduler`、`ThreadPoolTaskScheduler`、`TimerManagerTaskScheduler`。 +**DefaultManagedTaskScheduler**:基于 JNDI 的调度器。 +**TimerManagerTaskScheduler**:托管`commonj.timers.TimerManager`实例的调度器。 +**ThreadPoolTaskScheduler**:提供线程池管理的调度器,它也实现了`TaskExecutor`接口,从而使的单一的实例可以尽可能快地异步执行。 + +#### Trigger 接口 + +Trigger 接口抽象了触发条件的方法。 +Trigger 接口的声明: + +``` +public interface Trigger { + Date nextExecutionTime(TriggerContext triggerContext); +} +``` + +**Trigger 接口的实现类** +**CronTrigger**:实现了 cron 规则的触发器类(和 Quartz 的 cron 规则相同)。 +**PeriodicTrigger**:实现了一个周期性规则的触发器类(例如:定义触发起始时间、间隔时间等)。 + +#### 完整范例 + +实现一个调度任务的功能有以下几个关键点: +**(1) 定义调度器** +在 spring-bean.xml 中进行配置 +使用`task:scheduler`标签定义一个大小为 10 的线程池调度器,spring 会实例化一个`ThreadPoolTaskScheduler`。 + +```xml + + + + + +``` + +**_注:不要忘记引入 xsd:_** + +```xml +http://www.springframework.org/schema/task +http://www.springframework.org/schema/task/spring-task-3.1.xsd +``` + +**(2) 定义调度任务** +定义实现`Runnable`接口的线程类。 + +``` +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class DemoTask implements Runnable { + final Logger logger = LoggerFactory.getLogger(this.getClass()); + + @Override + public void run() { + logger.info("call DemoTask.run"); + } +} +``` + +**(3) 装配调度器,并执行调度任务** +在一个`Controller`类中用`@Autowired`注解装配`TaskScheduler`。 +然后调动 TaskScheduler 对象的 schedule 方法启动调度器,就可以执行调度任务了。 + +```java +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.scheduling.TaskScheduler; +import org.springframework.scheduling.support.CronTrigger; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; + +@Controller +@RequestMapping("/scheduler") +public class SchedulerController { + @Autowired + TaskScheduler scheduler; + + @RequestMapping(value = "/start", method = RequestMethod.POST) + public void start() { + scheduler.schedule(new DemoTask(), new CronTrigger("0/5 * * * * *")); + } +} +``` + +访问/scheduler/start 接口,启动调度器,可以看到如下日志内容: + +``` +13:53:15.010 myScheduler-1 o.zp.notes.spring.scheduler.DemoTask.run - call DemoTask.run +13:53:20.003 myScheduler-1 o.zp.notes.spring.scheduler.DemoTask.run - call DemoTask.run +13:53:25.004 myScheduler-2 o.zp.notes.spring.scheduler.DemoTask.run - call DemoTask.run +13:53:30.005 myScheduler-1 o.zp.notes.spring.scheduler.DemoTask.run - call DemoTask.run +``` + +### @Scheduler 的使用方法 + +Spring 的调度器一个很大的亮点在于`@Scheduler`注解,这可以省去很多繁琐的配置。 + +#### 启动注解 + +使用@Scheduler 注解先要使用``启动注解开关。 +**_例:_** + +```xml + + + + + + + +``` + +#### @Scheduler 定义触发条件 + +例:使用`fixedDelay`指定触发条件为每 5000 毫秒执行一次。注意:必须在上一次调度成功后的 5000 秒才能执行。 + +```java +@Scheduled(fixedDelay=5000) +public void doSomething() { + // something that should execute periodically +} +``` + +例:使用`fixedRate`指定触发条件为每 5000 毫秒执行一次。注意:无论上一次调度是否成功,5000 秒后必然执行。 + +```java +@Scheduled(fixedRate=5000) +public void doSomething() { + // something that should execute periodically +} +``` + +例:使用`initialDelay`指定方法在初始化 1000 毫秒后才开始调度。 + +```java +@Scheduled(initialDelay=1000, fixedRate=5000) +public void doSomething() { + // something that should execute periodically +} +``` + +例:使用`cron`表达式指定触发条件为每 5000 毫秒执行一次。cron 规则和 Quartz 中的 cron 规则一致。 + +```java +@Scheduled(cron="*/5 * * * * MON-FRI") +public void doSomething() { + // something that should execute on weekdays only +} +``` + +#### 完整范例 + +**(1) 启动注解开关,并定义调度器和执行器** + +```xml + + + + + + + + +``` + +**(2) 使用@Scheduler 注解来修饰一个要调度的方法** +下面的例子展示了@Scheduler 注解定义触发条件的不同方式。 + +```java +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +import java.text.SimpleDateFormat; +import java.util.Date; + +/** + * @description 使用@Scheduler注解调度任务范例 + * @author Vicotr Zhang + * @date 2016年8月31日 + */ +@Component +public class ScheduledMgr { + private final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + + final Logger logger = LoggerFactory.getLogger(this.getClass()); + + /** + * 构造函数中打印初始化时间 + */ + public ScheduledMgr() { + logger.info("Current time: {}", dateFormat.format(new Date())); + } + + /** + * fixedDelay属性定义调度间隔时间。调度需要等待上一次调度执行完成。 + */ + @Scheduled(fixedDelay = 5000) + public void testFixedDelay() throws Exception { + Thread.sleep(6000); + logger.info("Current time: {}", dateFormat.format(new Date())); + } + + /** + * fixedRate属性定义调度间隔时间。调度不等待上一次调度执行完成。 + */ + @Scheduled(fixedRate = 5000) + public void testFixedRate() throws Exception { + Thread.sleep(6000); + logger.info("Current time: {}", dateFormat.format(new Date())); + } + + /** + * initialDelay属性定义初始化后的启动延迟时间 + */ + @Scheduled(initialDelay = 1000, fixedRate = 5000) + public void testInitialDelay() throws Exception { + Thread.sleep(6000); + logger.info("Current time: {}", dateFormat.format(new Date())); + } + + /** + * cron属性支持使用cron表达式定义触发条件 + */ + @Scheduled(cron = "0/5 * * * * ?") + public void testCron() throws Exception { + Thread.sleep(6000); + logger.info("Current time: {}", dateFormat.format(new Date())); + } +} +``` + +我刻意设置触发方式的间隔都是 5s,且方法中均有 Thread.sleep(6000);语句。从而确保方法在下一次调度触发时间点前无法完成执行,来看一看各种方式的表现吧。 +启动 spring 项目后,spring 会扫描`@Component`注解,然后初始化 ScheduledMgr。 +接着,spring 会扫描`@Scheduler`注解,初始化调度器。调度器在触发条件匹配的情况下开始工作,输出日志。 +截取部分打印日志来进行分析。 + +``` +10:58:46.479 localhost-startStop-1 o.z.n.s.scheduler.ScheduledTasks. - Current time: 2016-08-31 10:58:46 +10:58:52.523 myScheduler-1 o.z.n.s.scheduler.ScheduledTasks.testFixedRate - Current time: 2016-08-31 10:58:52 +10:58:52.523 myScheduler-3 o.z.n.s.scheduler.ScheduledTasks.testFixedDelay - Current time: 2016-08-31 10:58:52 +10:58:53.524 myScheduler-2 o.z.n.s.scheduler.ScheduledTasks.testInitialDelay - Current time: 2016-08-31 10:58:53 +10:58:55.993 myScheduler-4 o.z.n.s.scheduler.ScheduledTasks.testCron - Current time: 2016-08-31 10:58:55 +10:58:58.507 myScheduler-1 o.z.n.s.scheduler.ScheduledTasks.testFixedRate - Current time: 2016-08-31 10:58:58 +10:58:59.525 myScheduler-5 o.z.n.s.scheduler.ScheduledTasks.testInitialDelay - Current time: 2016-08-31 10:58:59 +10:59:03.536 myScheduler-3 o.z.n.s.scheduler.ScheduledTasks.testFixedDelay - Current time: 2016-08-31 10:59:03 +10:59:04.527 myScheduler-1 o.z.n.s.scheduler.ScheduledTasks.testFixedRate - Current time: 2016-08-31 10:59:04 +10:59:05.527 myScheduler-4 o.z.n.s.scheduler.ScheduledTasks.testInitialDelay - Current time: 2016-08-31 10:59:05 +10:59:06.032 myScheduler-2 o.z.n.s.scheduler.ScheduledTasks.testCron - Current time: 2016-08-31 10:59:06 +10:59:10.534 myScheduler-9 o.z.n.s.scheduler.ScheduledTasks.testFixedRate - Current time: 2016-08-31 10:59:10 +10:59:11.527 myScheduler-10 o.z.n.s.scheduler.ScheduledTasks.testInitialDelay - Current time: 2016-08-31 10:59:11 +10:59:14.524 myScheduler-4 o.z.n.s.scheduler.ScheduledTasks.testFixedDelay - Current time: 2016-08-31 10:59:14 +10:59:15.987 myScheduler-6 o.z.n.s.scheduler.ScheduledTasks.testCron - Current time: 2016-08-31 10:59:15 +``` + +构造方法打印一次,时间点在 10:58:46。 +testFixedRate 打印四次,每次间隔 6 秒。说明,fixedRate 不等待上一次调度执行完成,在间隔时间达到时立即执行。 +testFixedDelay 打印三次,每次间隔大于 6 秒,且时间不固定。说明,fixedDelay 等待上一次调度执行成功后,开始计算间隔时间,再执行。 +testInitialDelay 第一次调度时间和构造方法调度时间相隔 7 秒。说明,initialDelay 在初始化后等待指定的延迟时间才开始调度。 +testCron 打印三次,时间间隔并非 5 秒或 6 秒,显然,cron 等待上一次调度执行成功后,开始计算间隔时间,再执行。 +此外,可以从日志中看出,打印日志的线程最多只有 10 个,说明 2.1 中的调度器线程池配置生效。 + +## 参考 + +[Spring Framework 官方文档](http://docs.spring.io/spring/docs/current/spring-framework-reference/htmlsingle/) \ No newline at end of file diff --git "a/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/05.Spring\351\233\206\346\210\220/03.Spring\351\233\206\346\210\220Dubbo.md" "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/05.Spring\351\233\206\346\210\220/03.Spring\351\233\206\346\210\220Dubbo.md" new file mode 100644 index 00000000..a36f1168 --- /dev/null +++ "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/05.Spring\351\233\206\346\210\220/03.Spring\351\233\206\346\210\220Dubbo.md" @@ -0,0 +1,260 @@ +--- +title: Spring集成Dubbo +date: 2017-10-27 17:30:41 +order: 03 +categories: + - Java + - 框架 + - Spring + - Spring集成 +tags: + - Java + - 框架 + - Spring + - 集成 + - Dubbo +permalink: /pages/274fd7/ +--- + +# Spring 集成 Dubbo + +## ZooKeeper + +ZooKeeper 可以作为 Dubbo 的注册中心。 + +Dubbo 未对 Zookeeper 服务器端做任何侵入修改,只需安装原生的 Zookeeper 服务器即可,所有注册中心逻辑适配都在调用 Zookeeper 客户端时完成。 + +**安装** + +在 [ZooKeeper 发布中心](http://zookeeper.apache.org/releases.html) 选择需要的版本,下载后解压到本地。 + +**配置** + +``` +vi conf/zoo.cfg + +``` + +如果不需要集群,`zoo.cfg` 的内容如下 [2](https://dubbo.gitbooks.io/dubbo-admin-book/content/install/zookeeper.html#fn_2): + +``` +tickTime=2000 +initLimit=10 +syncLimit=5 +dataDir=/home/dubbo/zookeeper-3.3.3/data +clientPort=2181 +``` + +如果需要集群,`zoo.cfg` 的内容如下 [3](https://dubbo.gitbooks.io/dubbo-admin-book/content/install/zookeeper.html#fn_3): + +``` +tickTime=2000 +initLimit=10 +syncLimit=5 +dataDir=/home/dubbo/zookeeper-3.3.3/data +clientPort=2181 +server.1=10.20.153.10:2555:3555 +server.2=10.20.153.11:2555:3555 + +``` + +并在 data 目录 [4](https://dubbo.gitbooks.io/dubbo-admin-book/content/install/zookeeper.html#fn_4) 下放置 myid 文件: + +``` +mkdir data +vi myid + +``` + +myid 指明自己的 id,对应上面 `zoo.cfg` 中 `server.` 后的数字,第一台的内容为 1,第二台的内容为 2,内容如下: + +``` +1 + +``` + +**启动** + +Linux 下执行 `bin/zkServer.sh` ;Windows `bin/zkServer.cmd` 启动 ZooKeeper 。 + +**命令行** + +``` +telnet 127.0.0.1 2181 +dump +``` + +或者: + +``` +echo dump | nc 127.0.0.1 2181 +``` + +用法: + +``` +dubbo.registry.address=zookeeper://10.20.153.10:2181?backup=10.20.153.11:2181 + +``` + +或者: + +``` + + +``` + +> 1. Zookeeper 是 Apache Hadoop 的子项目,强度相对较好,建议生产环境使用该注册中心 +> 2. 其中 data 目录需改成你真实输出目录 +> 3. 其中 data 目录和 server 地址需改成你真实部署机器的信息 +> 4. 上面 `zoo.cfg` 中的 `dataDir` +> 5. [http://zookeeper.apache.org/doc/r3.3.3/zookeeperAdmin.html](http://zookeeper.apache.org/doc/r3.3.3/zookeeperAdmin.html) + +## Dubbo + +Dubbo 采用全 Spring 配置方式,透明化接入应用,对应用没有任何 API 侵入,只需用 Spring 加载 Dubbo 的配置即可,Dubbo 基于 Spring 的 Schema 扩展进行加载。 + +如果不想使用 Spring 配置,可以通过 [API 的方式](https://dubbo.gitbooks.io/configuration/api.md) 进行调用。 + +## 服务提供者 + +完整安装步骤,请参见:[示例提供者安装](https://dubbo.gitbooks.io/dubbo-admin-book/install/provider-demo.html) + +### 定义服务接口 + +DemoService.java [1](https://dubbo.gitbooks.io/dubbo-user-book/quick-start.html#fn_1): + +```java +package com.alibaba.dubbo.demo; + +public interface DemoService { + String sayHello(String name); +} +``` + +### 在服务提供方实现接口 + +DemoServiceImpl.java [2](https://dubbo.gitbooks.io/dubbo-user-book/quick-start.html#fn_2): + +```java +package com.alibaba.dubbo.demo.provider; + +import com.alibaba.dubbo.demo.DemoService; + +public class DemoServiceImpl implements DemoService { + public String sayHello(String name) { + return "Hello " + name; + } +} +``` + +### 用 Spring 配置声明暴露服务 + +provider.xml: + +```xml + + + + + + + + + + + + + + + + + + +``` + +如果注册中心使用 ZooKeeper,可以将 dubbo:registry 改为 zookeeper://127.0.0.1:2181 + +### 加载 Spring 配置 + +Provider.java: + +```java +import org.springframework.context.support.ClassPathXmlApplicationContext; + +public class Provider { + public static void main(String[] args) throws Exception { + ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext(new String[] {"http://10.20.160.198/wiki/display/dubbo/provider.xml"}); + context.start(); + System.in.read(); // 按任意键退出 + } +} +``` + +## 服务消费者 + +完整安装步骤,请参见:[示例消费者安装](https://dubbo.gitbooks.io/dubbo-admin-book/install/consumer-demo.html) + +### 通过 Spring 配置引用远程服务 + +consumer.xml: + +```xml + + + + + + + + + + + + +``` + +如果注册中心使用 ZooKeeper,可以将 dubbo:registry 改为 zookeeper://127.0.0.1:2181 + +### 加载 Spring 配置,并调用远程服务 + +Consumer.java [3](https://dubbo.gitbooks.io/dubbo-user-book/quick-start.html#fn_3): + +``` +import org.springframework.context.support.ClassPathXmlApplicationContext; +import com.alibaba.dubbo.demo.DemoService; + +public class Consumer { + public static void main(String[] args) throws Exception { + ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext(new String[] {"http://10.20.160.198/wiki/display/dubbo/consumer.xml"}); + context.start(); + DemoService demoService = (DemoService)context.getBean("demoService"); // 获取远程服务代理 + String hello = demoService.sayHello("world"); // 执行远程方法 + System.out.println( hello ); // 显示调用结果 + } +} +``` + +> 1. 该接口需单独打包,在服务提供方和消费方共享 +> 2. 对服务消费方隐藏实现 +> 3. 也可以使用 IoC 注入 + +## FAQ + +建议使用 `dubbo-2.3.3` 以上版本的 zookeeper 注册中心客户端。 + +## 资料 + +**Dubbo** + +[Github](https://github.com/alibaba/dubbo) | [用户手册](https://dubbo.gitbooks.io/dubbo-user-book/content/) | [开发手册](https://dubbo.gitbooks.io/dubbo-dev-book/content/) | [管理员手册](https://dubbo.gitbooks.io/dubbo-admin-book/content/) + +**ZooKeeper** + +[官网](http://zookeeper.apache.org/) | [官方文档](http://zookeeper.apache.org/doc/trunk/) \ No newline at end of file diff --git "a/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/05.Spring\351\233\206\346\210\220/README.md" "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/05.Spring\351\233\206\346\210\220/README.md" new file mode 100644 index 00000000..068cd0a8 --- /dev/null +++ "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/05.Spring\351\233\206\346\210\220/README.md" @@ -0,0 +1,45 @@ +--- +title: Spring 集成 +date: 2020-02-26 23:47:47 +categories: + - Java + - 框架 + - Spring + - Spring集成 +tags: + - Java + - 框架 + - Spring + - SpringBoot + - 集成 +permalink: /pages/d6025b/ +hidden: true +index: false +--- + +# Spring 集成 + +> 章节主要针对:Spring 与第三方框架、库集成。如:Cache、Scheduling、JMS、JMX 等。 + +## 📖 内容 + +- [Spring 集成缓存中间件](01.Spring集成缓存.md) +- [Spring 集成定时任务中间件](02.Spring集成调度器.md) +- [Spring 集成 Dubbo](03.Spring集成Dubbo.md) + +## 📚 资料 + +- **官方** + - [Spring 官网](https://spring.io/) + - [Spring Framework 官方文档](https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/index.html) + - [Spring Github](https://github.com/spring-projects/spring-framework) +- **书籍** + - [《Spring In Action》](https://item.jd.com/12622829.html) +- **教程** + - [《小马哥讲 Spring 核心编程思想》](https://time.geekbang.org/course/intro/265) + - [geekbang-lessons](https://github.com/geektime-geekbang/geekbang-lessons) + - [跟我学 Spring3](http://jinnianshilongnian.iteye.com/blog/1482071) + +## 🚪 传送 + +◾ 💧 [钝悟的 IT 知识图谱](https://dunwu.github.io/waterdrop/) ◾ 🎯 [我的博客](https://github.com/dunwu/blog) ◾ \ No newline at end of file diff --git "a/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/10.Spring\345\256\211\345\205\250/01.SpringBoot\344\271\213\345\256\211\345\205\250\345\277\253\351\200\237\345\205\245\351\227\250.md" "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/10.Spring\345\256\211\345\205\250/01.SpringBoot\344\271\213\345\256\211\345\205\250\345\277\253\351\200\237\345\205\245\351\227\250.md" new file mode 100644 index 00000000..90cd3fd6 --- /dev/null +++ "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/10.Spring\345\256\211\345\205\250/01.SpringBoot\344\271\213\345\256\211\345\205\250\345\277\253\351\200\237\345\205\245\351\227\250.md" @@ -0,0 +1,46 @@ +--- +title: SpringBoot 之安全快速入门 +date: 2021-05-13 18:21:56 +order: 01 +categories: + - Java + - 框架 + - Spring + - Spring安全 +tags: + - Java + - 框架 + - Spring + - SpringBoot + - 安全 +permalink: /pages/568352/ +--- + +# SpringBoot 之安全快速入门 + +## QuickStart + +(1)添加依赖 + +```xml + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-security + +``` + +(2)添加配置 + +```properties +spring.security.user.name = root +spring.security.user.password = root +spring.security.user.roles = USER +``` + +(3)启动应用后,访问任意路径,都会出现以下页面,提示你先执行登录操作。输入配置的用户名、密码(root/root)即可访问应用页面。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/image-20191118150326556.png) \ No newline at end of file diff --git "a/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/99.Spring\345\205\266\344\273\226/01.Spring4\345\215\207\347\272\247.md" "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/99.Spring\345\205\266\344\273\226/01.Spring4\345\215\207\347\272\247.md" new file mode 100644 index 00000000..486e5600 --- /dev/null +++ "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/99.Spring\345\205\266\344\273\226/01.Spring4\345\215\207\347\272\247.md" @@ -0,0 +1,345 @@ +--- +title: Spring 4 升级踩雷指南 +date: 2017-12-15 15:10:32 +order: 01 +categories: + - Java + - 框架 + - Spring + - Spring其他 +tags: + - Java + - 框架 + - Spring +permalink: /pages/752c6a/ +--- + +# Spring 4 升级踩雷指南 + +## 前言 + +最近,一直在为公司老项目做核心库升级工作。本来只是想升级一下 JDK8 ,却因为兼容性问题而不得不升级一些其他的库,而其他库本身依赖的一些库可能也要同步升级。这是一系列连锁问题,你很难一一识别,往往只有在编译时、运行时才能发现问题。 + +总之,这是个费劲的活啊。 + +本文小结一下升级 Spring4 的连锁问题。 + +## 为什么升级 spring4 + +升级 Spring4 的原因是:Spring 4 以前的版本不兼容 JDK8。当你的项目同时使用 Spring3 和 JDK8,如果代码中有使用 JDK8 字节码或 Lambada 表达式,那么会出问题。 + +也许你会问,为什么不使用最新的 Spring 5 呢?因为作为企业软件,一般更倾向使用稳定的版本(bug 少),而不是最新的版本,尤其是一些核心库。 + +更多细节可以参考: + +https://spring.io/blog/2013/05/21/spring-framework-4-0-m1-3-2-3-available/ + +## spring 4 重要新特性 + +Spring 4 相比 Spring 3,引入许多新特性,这里列举几条较为重要的: + +1. 支持 `JDK8` (这个是最主要的)。 +2. `Groovy Bean Definition DSL` 风格配置。 +3. 支持 WebSocket、SockJS、STOMP 消息 +4. 移除 Deprecated 包和方法 +5. 一些功能加强,如:核心容器、Web、Test 等等,不一一列举。 + +更多 Spring 4 新特性可以参考: + +https://docs.spring.io/spring/docs/4.3.14.BUILD-SNAPSHOT/spring-framework-reference/htmlsingle/#spring-whats-new + +http://jinnianshilongnian.iteye.com/blog/1995111 + +## 升级 spring 4 步骤 + +了解了前面内容,我们知道了升级 Spring 4 带来的好处。现在开始真刀真枪的升级了。 + +不要以为升级一下 Spring 4,仅仅是改一下版本号,那么简单,细节处多着呢。 + +下面,结合我在公司项目升级 Spring4 时遇到的一系列坑,希望能帮助各位少走弯路。 + +> **注** +> +> 下文内容基于假设你的项目是用 maven 管理这一前提。如果不满足这一前提,那么这篇文章对你没什么太大帮助。 + +### 修改 spring 版本 + +第一步,当然是修改 pom.xml 中的 spring 版本。 + +`3.x.x.RELEASE` > `4.x.x.RELEASE` + +实例:升级 spring-core + +其它 spring 库的升级也如此: + +```xml + + 4.3.13.RELEASE + + + org.springframework + spring-core + ${spring.version} + +``` + +### 修改 spring xml 文件的 xsd + +用过 spring 的都知道,spring 通常依赖于大量的 xml 配置。 + +spring 的 xml 解析器在解析 xml 时,需要读取 xml schema,schema 定义了 xml 的命名空间。它的好处在于可以避免命名冲突,有点像 Java 中的 package。 + +实例:一个 spring xml 的 schema + +```xml + + +``` + +> **说明** +> +> - `xmlns="http://www.springframework.org/schema/beans"` 声明 xml 文件默认的命名空间,表示未使用其他命名空间的所有标签的默认命名空间。 +> +> - `xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"` 声明 XML Schema 实例,声明后就可以使用 schemaLocation 属性了。 +> +> - `xmlns:mvc="http://www.springframework.org/schema/mvc"` +> 声明前缀为 mvc 的命名空间,后面的 URL 用于标示命名空间的地址不会被解析器用于查找信息。其惟一的作用是赋予命名空间一个惟一的名称。当命名空间被定义在元素的开始标签中时,所有带有相同前缀的子元素都会与同一个命名空间相关联。 其它的类似 `xmlns:context` 、`xmlns:jdbc` 等等同样如此。 +> +> - ``` +> xsi:schemaLocation="http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-3.1.xsd +> ..." +> ``` +> +> 这个从命名可以看出个大概,指定 schema 位置这个属性必须结合命名空间使用。这个属性有两个值,第一个值表示需要使用的命名空间。第二个值表示供命名空间使用的 xml schema 的位置。 + +上面示例中的 xsd 版本是 `3.1.xsd` ,表示 spring 的 xml 解析器会将其视为 3.1 版本的 xml 文件来处理。 + +现在,我们使用了 Spring 4,`3.1.xsd` 版本显然就不正确了,我们可以根据自己引入的 Spring 4 的子版本号将其改为 `4.x.xsd` 。 + +但是,还有一种更好的做法:把这个指定 xsd 版本的关键字干掉,类似这样:`http://www.springframework.org/schema/tx/spring-tx.xsd` 。 + +**这么做的原因如下:** + +- Spring 默认在启动时要加载 xsd 文件来验证 xml 文件。 +- 如果没有提供 `schemaLocation`,那么 spring 的 xml 解析器会从 namespace 的 uri 里加载 xsd 文件。 +- `schemaLocation` 提供了一个 xml namespace 到对应的 xsd 文件的一个映射。 +- 如果不指定 spring xsd 的版本号,spring 取的就是当前本地 jar 里的 xsd 文件,减少了各种风险(比如 xsd 与实际 spring jar 版本不一致)。 + +更多详细内容可以参考这篇文章:[为什么在 Spring 的配置里,最好不要配置 xsd 文件的版本号](http://blog.csdn.net/hengyunabc/article/details/22295749) + +### 修改 spring xml 文件 + +spring 4 对 xml 做了一些改动。这里说一个最常用的改动: + +#### ref local + +spring 不再支持 `ref` 元素的 `local` 属性,如果你的项目中使用了,需要改为 `bean`。 + +shi + +spring 4 以前: + +```xml + + + + + +``` + +spring 4 以后: + +```xml + + + + + +``` + +如果不改启动会报错: + +``` +Caused by: org.xml.sax.SAXParseException: cvc-complex-type.3.2.2: Attribute 'local' is not allowed to appear in element 'ref'. +``` + +当然,可能还有一些其他配置改动,这个只能说兵来将挡水来土掩,遇到了再去查官方文档吧。 + +### 加入 spring support + +spring 3 中很多的扩展内容不需要引入 support 。但是 spring 4 中分离的更彻底了,如果不分离,会有很多`ClassNotFound` 。 + +```xml + + org.springframework + spring-context-support + 4.2.3.RELEASE + +``` + +### 更换 spring-mvc jackson + +spring mvc 中如果返回结果为 json 需要依赖 jackson 的 jar 包,但是他升级到了 2, 以前是 `codehaus.jackson`,现在换成了 `fasterxml.jackson` + +```xml + + com.fasterxml.jackson.core + jackson-core + 2.7.0 + + + com.fasterxml.jackson.core + jackson-databind + 2.7.0 + +``` + +同时修改 spring mvc 的配置文件: + +```xml + + + + + + + + + + + + + + text/plain;charset=UTF-8 + + + +``` + +### 解决 ibatis 兼容问题 + +**问题** + +如果你的项目中使用了 ibatis (mybatis 的前身)这个 orm 框架,当 spring3 升级 spring4 后,会出现兼容性问题,编译都不能通过。 + +这是因为 Spring4 官方已经不再支持 ibatis。 + +**解决方案** + +添加兼容性 jar 包 + +```xml + + org.mybatis + mybatis-2-spring + 1.0.1 + +``` + +更多内容可参考:https://stackoverflow.com/questions/32353286/no-support-for-ibatis-in-spring4-2-0 + +### 升级 Dubbo + +我们的项目中使用了 soa 框架 Dubbo 。由于 Dubbo 是老版本的,具体来说是(2013 年的 2.4.10),而老版本中使用的 spirng 版本为 2.x,有兼容性问题。 + +Dubbo 项目从今年开始恢复维护了,首先把一些落后的库升级到较新版本,比如 jdk8,spring4 等,并修复了一些 bug。所以,我们可以通过升级一下 Dubbo 版本来解决问题。 + +```xml + + com.alibaba + dubbo + 2.5.8 + + + org.springframework + spring-context + + + org.springframework + spring-web + + + org.javassist + javassist + + + +``` + +### 升级 Jedis + +升级 Dubbo 为当前最新的 2.5.8 版本后,运行时报错: + +- **JedisPoolConfig 配置错误** + +``` +Caused by: java.lang.ClassNotFoundException: org.apache.commons.pool2.impl.GenericObjectPoolConfig +``` + +由于项目中使用了 redis,版本为 2.0.0 ,这个问题是由于 jedis 需要升级: + +```xml + + redis.clients + jedis + 2.9.0 + +``` + +jedis 2.4.1 以上版本的 `JedisPoolConfig` 已经没有了`maxActive` 和 `maxWait` 属性。 + +修改方法如下: + +**maxActive** > **maxTotal** + +**maxWait** > **maxWaitMillis** + +```xml + + + + + + +``` + +JedisPool 配置错误 + +``` +InvalidURIException: Cannot open Redis connection due invalid URI +``` + +原来的配置如下: + +```xml + + + + + +``` + +查看源码可以发现,初始化 JedisPool 时未指定结构方法参数的类型,导致 host 字符串值被视为 URI 类型,当然类型不匹配。 + +解决方法是修改上面的 host 配置,为:`` + +--- + +至此,spring 4 升级结束。后面如果遇到其他升级问题再补充。 + +## 资料 + +- https://spring.io/blog/2013/05/21/spring-framework-4-0-m1-3-2-3-available/ +- https://docs.spring.io/spring/docs/4.3.14.BUILD-SNAPSHOT/spring-framework-reference/htmlsingle/#spring-whats-new +- [Spring 3.x 升级到 Spring 4.x 注意事项和步骤,错误解决方法](http://www.sojson.com/blog/145.html) +- http://jinnianshilongnian.iteye.com/blog/1995111 +- [为什么在 Spring 的配置里,最好不要配置 xsd 文件的版本号](http://blog.csdn.net/hengyunabc/article/details/22295749) +- https://stackoverflow.com/questions/32353286/no-support-for-ibatis-in-spring4-2-0 diff --git "a/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/99.Spring\345\205\266\344\273\226/21.SpringBoot\344\271\213banner.md" "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/99.Spring\345\205\266\344\273\226/21.SpringBoot\344\271\213banner.md" new file mode 100644 index 00000000..09a1e61c --- /dev/null +++ "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/99.Spring\345\205\266\344\273\226/21.SpringBoot\344\271\213banner.md" @@ -0,0 +1,131 @@ +--- +title: SpringBoot 之 banner 定制 +date: 2018-12-21 23:22:44 +order: 21 +categories: + - Java + - 框架 + - Spring + - Spring其他 +tags: + - Java + - 框架 + - Spring + - SpringBoot +permalink: /pages/bac2ce/ +--- + +# SpringBoot 之 banner 定制 + +## 简介 + +Spring Boot 启动时默认会显示以下 LOGO: + +``` + . ____ _ __ _ _ + /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \ +( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \ + \\/ ___)| |_)| | | | | || (_| | ) ) ) ) + ' |____| .__|_| |_|_| |_\__, | / / / / + =========|_|==============|___/=/_/_/_/ + :: Spring Boot :: (v2.1.1.RELEASE) +``` + +实际上,Spring Boot 支持自定义 logo 的功能。 + +让我们来看看如何实现的。 + +只要你在 `resources` 目录下放置名为 `banner.txt`、`banner.gif` 、`banner.jpg` 或 `banner.png` 的文件,Spring Boot 会自动加载,将其作为启动时打印的 logo。 + +- 对于文本文件,Spring Boot 会将其直接输出。 +- 对于图像文件( `banner.gif` 、`banner.jpg` 或 `banner.png` ),Spring Boot 会将图像转为 ASCII 字符,然后输出。 + +## 变量 + +banner.txt 文件中还可以使用变量来设置字体、颜色、版本号。 + +| 变量 | 描述 | +| :------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `${application.version}` | `MANIFEST.MF` 中定义的版本。如:`1.0` | +| `${application.formatted-version}` | `MANIFEST.MF` 中定义的版本,并添加一个 `v` 前缀。如:`v1.0` | +| `${spring-boot.version}` | Spring Boot 版本。如:`2.1.1.RELEASE`. | +| `${spring-boot.formatted-version}` | Spring Boot 版本,并添加一个 `v` 前缀。如:`v2.1.1.RELEASE` | +| `${Ansi.NAME}` (or `${AnsiColor.NAME}`, `${AnsiBackground.NAME}`, `${AnsiStyle.NAME}`) | ANSI 颜色、字体。更多细节,参考:[`AnsiPropertySource`](https://github.com/spring-projects/spring-boot/tree/v2.1.1.RELEASE/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ansi/AnsiPropertySource.java)。 | +| `${application.title}` | `MANIFEST.MF` 中定义的应用名。 | + +示例: + +在 Spring Boot 项目中的 `resources` 目录下添加一个名为 banner.txt 的文件,内容如下: + +``` +${AnsiColor.BRIGHT_YELLOW}${AnsiStyle.BOLD} + ________ ___ ___ ________ ___ __ ___ ___ +|\ ___ \|\ \|\ \|\ ___ \|\ \ |\ \|\ \|\ \ +\ \ \_|\ \ \ \\\ \ \ \\ \ \ \ \ \ \ \ \ \\\ \ + \ \ \ \\ \ \ \\\ \ \ \\ \ \ \ \ __\ \ \ \ \\\ \ + \ \ \_\\ \ \ \\\ \ \ \\ \ \ \ \|\__\_\ \ \ \\\ \ + \ \_______\ \_______\ \__\\ \__\ \____________\ \_______\ + \|_______|\|_______|\|__| \|__|\|____________|\|_______| +${AnsiBackground.WHITE}${AnsiColor.RED}${AnsiStyle.UNDERLINE} +:: Spring Boot :: (v${spring-boot.version}) +:: Spring Boot Tutorial :: (v1.0.0) +``` + +> 注:`${}` 设置字体颜色的变量之间不能换行或空格分隔,否则会导致除最后一个变量外,都不生效。 + +启动应用后,控制台将打印如下 logo: + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20181221231330.png) +推荐两个生成字符画的网站,可以将生成的字符串放入这个`banner.txt` 文件: + +- +- + +## 配置 + +`application.properties` 中与 Banner 相关的配置: + +```properties +# banner 模式。有三种模式:console/log/off +# console 打印到控制台(通过 System.out) +# log - 打印到日志中 +# off - 关闭打印 +spring.main.banner-mode = off +# banner 文件编码 +spring.banner.charset = UTF-8 +# banner 文本文件路径 +spring.banner.location = classpath:banner.txt +# banner 图像文件路径(可以选择 png,jpg,gif 文件) +spring.banner.image.location = classpath:banner.gif +used). +# 图像 banner 的宽度(字符数) +spring.banner.image.width = 76 +# 图像 banner 的高度(字符数) +spring.banner.image.height = +# 图像 banner 的左边界(字符数) +spring.banner.image.margin = 2 +# 是否将图像转为黑色控制台主题 +spring.banner.image.invert = false +``` + +当然,你也可以在 YAML 文件中配置,例如: + +```yml +spring: + main: + banner-mode: off +``` + +## 编程 + +默认,Spring Boot 会注册一个 `SpringBootBanner` 的单例 Bean,用来负责打印 Banner。 + +如果想完全个人定制 Banner,可以这么做:先实现 `org.springframework.boot.Banner#printBanner` 接口来自己定制 Banner。在将这个 Banner 通过 `SpringApplication.setBanner(…)` 方法注入 Spring Boot。 + +## 示例源码 + +> 示例源码:[spring-boot-banner](https://github.com/dunwu/spring-boot-tutorial/tree/master/codes/spring-boot-banner) + +## 参考资料 + +- [Spring Boot 官方文档之 Customizing the Banner](https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#boot-features-banner) \ No newline at end of file diff --git "a/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/99.Spring\345\205\266\344\273\226/22.SpringBoot\344\271\213Actuator.md" "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/99.Spring\345\205\266\344\273\226/22.SpringBoot\344\271\213Actuator.md" new file mode 100644 index 00000000..f49e516c --- /dev/null +++ "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/99.Spring\345\205\266\344\273\226/22.SpringBoot\344\271\213Actuator.md" @@ -0,0 +1,371 @@ +--- +title: SpringBoot Actuator 快速入门 +date: 2022-06-14 20:51:22 +order: 22 +categories: + - Java + - 框架 + - Spring + - Spring其他 +tags: + - Java + - 框架 + - Spring + - SpringBoot +permalink: /pages/c013cc/ +--- + +# SpringBoot Actuator 快速入门 + +[`spring-boot-actuator`](https://github.com/spring-projects/spring-boot/tree/v2.7.0/spring-boot-project/spring-boot-actuator) 模块提供了 Spring Boot 的所有生产就绪功能。启用这些功能的推荐方法是添加 `spring-boot-starter-actuator` 依赖。 + +如果是 Maven 项目,添加以下依赖: + +```xml + + + org.springframework.boot + spring-boot-starter-actuator + + +``` + +如果是 Gradle 项目,添加以下声明: + +```groovy +dependencies { + implementation 'org.springframework.boot:spring-boot-starter-actuator' +} +``` + +## 端点(Endpoint) + +Actuator Endpoint 使 Spring Boot 用户可以监控应用,并和应用进行交互。Spring Boot 内置了许多 端点,并允许用户自定义端点。例如,`health` 端点提供基本的应用健康信息。 + +用户可以启用或禁用每个单独的端点并通过 HTTP 或 JMX 暴露它们(使它们可远程访问)。当端点被启用和公开时,它被认为是可用的。内置端点仅在可用时才会自动配置。大多数应用程序选择通过 HTTP 公开。例如,默认情况下,`health` 端点映射到 `/actuator/health`。 + +### 启用端点 + +默认情况下,除了 `shutdown` 之外的所有端点都已启用。要配置端点的启用,请使用 `management.endpoint..enabled` 属性。以下示例启用 `shutdown` 端点: + +```properties +management.endpoint.shutdown.enabled=true +``` + +如果您希望端点是明确指定才启用,请将 `management.endpoints.enabled-by-default` 属性设置为 false 并根据需要明确指定启用的端点,以下为示例: + +```properties +management.endpoints.enabled-by-default=false +management.endpoint.info.enabled=true +``` + +### 暴露端点 + +由于端点可能包含敏感信息,您应该仔细考虑何时暴露它们。下表显示了内置端点的默认曝光: + +| ID | JMX | Web | +| :----------------- | :-- | :-- | +| `auditevents` | Yes | No | +| `beans` | Yes | No | +| `caches` | Yes | No | +| `conditions` | Yes | No | +| `configprops` | Yes | No | +| `env` | Yes | No | +| `flyway` | Yes | No | +| `health` | Yes | Yes | +| `heapdump` | N/A | No | +| `httptrace` | Yes | No | +| `info` | Yes | No | +| `integrationgraph` | Yes | No | +| `jolokia` | N/A | No | +| `logfile` | N/A | No | +| `loggers` | Yes | No | +| `liquibase` | Yes | No | +| `metrics` | Yes | No | +| `mappings` | Yes | No | +| `prometheus` | N/A | No | +| `quartz` | Yes | No | +| `scheduledtasks` | Yes | No | +| `sessions` | Yes | No | +| `shutdown` | Yes | No | +| `startup` | Yes | No | +| `threaddump` | Yes | No | + +要更改暴露的端点,请使用以下特定于技术的包含和排除属性: + +| Property | Default | +| :------------------------------------------ | :------- | +| `management.endpoints.jmx.exposure.exclude` | | +| `management.endpoints.jmx.exposure.include` | `*` | +| `management.endpoints.web.exposure.exclude` | | +| `management.endpoints.web.exposure.include` | `health` | + +`include` 属性列出了暴露的端点的 ID。 `exclude` 属性列出了不应暴露的端点的 ID。 `exclude` 属性优先于 `include` 属性。您可以使用端点 ID 列表配置包含和排除属性。 + +例如,仅暴露 `health` 和 info 端点,其他端点都不通过 JMX 暴露,可以按如下配置: + +```properties +management.endpoints.jmx.exposure.include=health,info +``` + +注意:`*` 可用于选择所有端点。 + +### 安全 + +出于安全考虑,只有 `/health` 端点会通过 HTTP 方式暴露。用户可以通过 `management.endpoints.web.exposure.include` 决定哪些端点可以通过 HTTP 方式暴露。 + +如果 Spring Security 在类路径上并且不存在其他 `WebSecurityConfigurerAdapter` 或 `SecurityFilterChain` bean,则除 `/health` 之外的所有 actuator 都由 Spring Boot 自动启用安全控制。如果用户自定义了 `WebSecurityConfigurerAdapter` 或 `SecurityFilterChain` bean,Spring Boot 不再启用安全控制,由用户自行控制访问规则。 + +如果您希望为 HTTP 端点定义安全控制(例如,只允许具有特定角色的用户访问它们),Spring Boot 提供了一些方便的 `RequestMatcher` 对象,您可以将它们与 Spring Security 结合使用。 + +下面是一个典型的 Spring Security 配置示例: + +```java +@Configuration(proxyBeanMethods = false) +public class MySecurityConfiguration { + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + http.requestMatcher(EndpointRequest.toAnyEndpoint()) + .authorizeRequests((requests) -> requests.anyRequest().hasRole("ENDPOINT_ADMIN")); + http.httpBasic(); + return http.build(); + } + +} +``` + +前面的示例使用 EndpointRequest.toAnyEndpoint() 将请求匹配到任何端点,然后确保所有端点都具有 ENDPOINT_ADMIN 角色。 EndpointRequest 上还提供了其他几种匹配器方法。 + +如果希望无需身份验证即可访问所有执行器端点。可以通过更改 management.endpoints.web.exposure.include 属性来做到这一点,如下所示: + +```properties +management.endpoints.web.exposure.include=* +``` + +此外,如果存在 Spring Security,您将需要添加自定义安全配置,以允许未经身份验证的访问端点,如以下示例所示: + +```java +@Configuration(proxyBeanMethods = false) +public class MySecurityConfiguration { + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + http.requestMatcher(EndpointRequest.toAnyEndpoint()) + .authorizeRequests((requests) -> requests.anyRequest().permitAll()); + return http.build(); + } + +} +``` + +由于 Spring Boot 依赖于 Spring Security 的默认设置,因此 CSRF 保护默认开启。这意味着在使用默认安全配置时,需要 POST(关闭和记录器端点)、PUT 或 DELETE 的执行器端点会收到 403(禁止)错误。 + +> 建议仅在创建非浏览器客户端使用的服务时完全禁用 CSRF 保护。 + +### 配置端点 + +端点会自动缓存对不带任何参数的读操作的响应数据。要配置端点缓存响应的时间量,请使用其 `cache.time-to-live` 属性。以下示例将 bean 端点缓存的生存时间设置为 10 秒: + +```properties +management.endpoint.beans.cache.time-to-live=10s +``` + +### Actuator Web 端点的超媒体 + +Spring Boot Actuator 中内置了一个“发现页面”端点,其中包含了所有端点的链接。默认情况下,“发现页面”在 `/actuator` 上可用。 + +要禁用“发现页面”,请将以下属性添加到您的应用程序属性中: + +```properties +management.endpoints.web.discovery.enabled=false +``` + +配置自定义管理上下文路径后,“发现页面”会自动从 `/actuator` 移动到应用管理上下文的根目录。例如,如果管理上下文路径是 `/management`,则发现页面可从 `/management` 获得。当管理上下文路径设置为 / 时,发现页面被禁用以防止与其他映射发生冲突的可能性。 + +### 跨域支持 + +CORS 是一种 W3C 规范,可让用户以灵活的方式指定授权哪种跨域请求。如果使用 Spring MVC 或 Spring WebFlux,则可以配置 Actuator 的 Web 端点以支持此类场景。 + +CORS 支持默认是禁用的,只有在设置 `management.endpoints.web.cors.allowed-origins` 属性后才会启用。以下配置允许来自 example.com 域的 GET 和 POST 调用: + +```properties +management.endpoints.web.cors.allowed-origins=https://example.com +management.endpoints.web.cors.allowed-methods=GET,POST +``` + +### 自定义端点 + +如果添加带有 `@Endpoint` 注释的 `@Bean`,则任何带有 `@ReadOperation`、`@WriteOperation` 或 `@DeleteOperation` 注释的方法都会自动通过 JMX 公开,并且在 Web 应用程序中,也可以通过 HTTP 公开。可以使用 Jersey、Spring MVC 或 Spring WebFlux 通过 HTTP 公开端点。如果 Jersey 和 Spring MVC 都可用,则使用 Spring MVC。 + +以下示例公开了一个返回自定义对象的读取操作: + +```java +@ReadOperation +public CustomData getData() { + return new CustomData("test", 5); +} +``` + +您还可以使用 `@JmxEndpoint` 或 `@WebEndpoint` 编写特定技术的端点。这些端点仅限于各自的技术。例如,`@WebEndpoint` 仅通过 HTTP 而不是通过 JMX 公开。 + +您可以使用 `@EndpointWebExtension` 和 `@EndpointJmxExtension` 编写特定技术的扩展。这些注释让您可以提供特定技术的操作来扩充现有端点。 + +最后,如果您需要访问 Web 框架的功能,您可以实现 servlet 或 Spring `@Controller` 和 `@RestController` 端点,但代价是它们无法通过 JMX 或使用不同的 Web 框架获得。 + +## 通过 HTTP 进行监控和管理 + +### 自定义管理端点路径 + +如果是 Web 应用,Spring Boot Actuator 会自动将所有启用的端点通过 HTTP 方式暴露。默认约定是使用前缀为 `/actuator` 的端点的 id 作为 URL 路径。例如,健康被暴露为 `/actuator/health`。 + +有时,自定义管理端点的前缀很有用。例如,您的应用程序可能已经将 `/actuator` 用于其他目的。您可以使用 `management.endpoints.web.base-path` 属性更改管理端点的前缀,如以下示例所示: + +```properties +management.endpoints.web.base-path=/manage +``` + +该示例将端点从 `/actuator/{id}` 更改为 `/manage/{id}`(例如,`/manage/info`)。 + +### 自定义管理服务器端口 + +```properties +management.server.port=8081 +``` + +### 配置 SSL + +当配置为使用自定义端口时,还可以使用各种 `management.server.ssl.*` 属性为管理服务器配置自己的 SSL。例如,这样做可以让管理服务器在主应用程序使用 HTTPS 时通过 HTTP 可用,如以下属性设置所示: + +```properties +server.port=8443 +server.ssl.enabled=true +server.ssl.key-store=classpath:store.jks +server.ssl.key-password=secret +management.server.port=8080 +management.server.ssl.enabled=false +``` + +或者,主服务器和管理服务器都可以使用 SSL,但使用不同的密钥存储,如下所示: + +```properties +server.port=8443 +server.ssl.enabled=true +server.ssl.key-store=classpath:main.jks +server.ssl.key-password=secret +management.server.port=8080 +management.server.ssl.enabled=true +management.server.ssl.key-store=classpath:management.jks +management.server.ssl.key-password=secret +``` + +### 自定义管理服务器地址 + +```properties +management.server.port=8081 +management.server.address=127.0.0.1 +``` + +### 禁用 HTTP 端点 + +如果您不想通过 HTTP 方式暴露端点,可以将管理端口设置为 -1,如以下示例所示: + +```properties +management.server.port=-1 +``` + +也可以通过使用 management.endpoints.web.exposure.exclude 属性来实现这一点,如以下示例所示: + +```properties +management.endpoints.web.exposure.exclude=* +``` + +## 通过 JMX 进行监控和管理 + +Java 管理扩展 (JMX) 提供了一种标准机制来监视和管理应用程序。默认情况下,此功能未启用。您可以通过将 `spring.jmx.enabled` 配置属性设置为 true 来打开它。 Spring Boot 将最合适的 `MBeanServer` 暴露为 ID 为 `mbeanServer` 的 bean。使用 Spring JMX 注释(`@ManagedResource`、`@ManagedAttribute` 或 `@ManagedOperation`)注释的任何 bean 都会暴露给它。 + +如果您的平台提供标准 `MBeanServer`,则 Spring Boot 会使用该标准并在必要时默认使用 VM `MBeanServer`。如果一切都失败了,则创建一个新的 `MBeanServer`。 + +有关更多详细信息,请参阅 [`JmxAutoConfiguration`](https://github.com/spring-projects/spring-boot/tree/v2.7.0/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jmx/JmxAutoConfiguration.java) 类。 + +默认情况下,Spring Boot 还将管理端点公开为 `org.springframework.boot` 域下的 JMX MBean。要完全控制 JMX 域中的端点注册,请考虑注册您自己的 `EndpointObjectNameFactory` 实现。 + +### 定制化 MBean Names + +MBean 的名称通常由端点的 id 生成。例如,健康端点公开为 `org.springframework.boot:type=Endpoint,name=Health`。 + +如果您的应用程序包含多个 Spring `ApplicationContext`,您可能会发现名称冲突。要解决此问题,您可以将 `spring.jmx.unique-names` 属性设置为 true,以便 MBean 名称始终是唯一的。 + +如果需要定制,跨域按如下配置: + +```properties +spring.jmx.unique-names=true +management.endpoints.jmx.domain=com.example.myapp +``` + +### 禁用 JMX 端点 + +想禁用 JMX 端点,可以按如下配置: + +``` +management.endpoints.jmx.exposure.exclude=* +``` + +### 将 Jolokia 用于基于 HTTP 的 JMX + +Jolokia 是一个 JMX-HTTP 的桥接工具,它提供了另一种访问 JMX bean 的方法。要使用 Jolokia,需要先添加依赖: + +```xml + + org.jolokia + jolokia-core + 🍃 **`spring-tutorial`** 是一个 Spring & Spring Boot 教程。 +> +> - 🔁 项目同步维护:[Github](https://github.com/dunwu/spring-tutorial/) | [Gitee](https://gitee.com/turnon/spring-tutorial/) +> - 📖 电子书阅读:[Github Pages](https://dunwu.github.io/spring-tutorial/) | [Gitee Pages](http://turnon.gitee.io/spring-tutorial/) + +## 📖 内容 + +### 综合 + +- [Spring 概述](00.Spring综合/01.Spring概述.md) +- [SpringBoot 知识图谱](00.Spring综合/21.SpringBoot知识图谱.md) +- [SpringBoot 基本原理](00.Spring综合/22.SpringBoot基本原理.md) +- [Spring 面试](00.Spring综合/99.Spring面试.md) + +### 核心 + +- [Spring Bean](01.Spring核心/01.SpringBean.md) +- [Spring IoC](01.Spring核心/02.SpringIoC.md) +- [Spring 依赖查找](01.Spring核心/03.Spring依赖查找.md) +- [Spring 依赖注入](01.Spring核心/04.Spring依赖注入.md) +- [Spring IoC 依赖来源](01.Spring核心/05.SpringIoC依赖来源.md) +- [Spring Bean 作用域](01.Spring核心/06.SpringBean作用域.md) +- [Spring Bean 生命周期](01.Spring核心/07.SpringBean生命周期.md) +- [Spring 配置元数据](01.Spring核心/08.Spring配置元数据.md) +- [Spring AOP](01.Spring核心/10.SpringAop.md) +- [Spring 资源管理](01.Spring核心/20.Spring资源管理.md) +- [Spring 校验](01.Spring核心/21.Spring校验.md) +- [Spring 数据绑定](01.Spring核心/22.Spring数据绑定.md) +- [Spring 类型转换](01.Spring核心/23.Spring类型转换.md) +- [Spring EL 表达式](01.Spring核心/24.SpringEL.md) +- [Spring 事件](01.Spring核心/25.Spring事件.md) +- [Spring 国际化](01.Spring核心/26.Spring国际化.md) +- [Spring 泛型处理](01.Spring核心/27.Spring泛型处理.md) +- [Spring 注解](01.Spring核心/28.Spring注解.md) +- [Spring Environment 抽象](01.Spring核心/29.SpringEnvironment抽象.md) +- [SpringBoot 教程之快速入门](01.Spring核心/31.SpringBoot之快速入门.md) +- [SpringBoot 之属性加载](01.Spring核心/32.SpringBoot之属性加载.md) +- [SpringBoot 之 Profile](01.Spring核心/33.SpringBoot之Profile.md) + +### 数据 + +- [Spring 之数据源](02.Spring数据/01.Spring之数据源.md) +- [Spring 之 JDBC](02.Spring数据/02.Spring之JDBC.md) +- [Spring 之事务](02.Spring数据/03.Spring之事务.md) +- [Spring 之 JPA](02.Spring数据/04.Spring之JPA.md) +- [Spring 集成 Mybatis](02.Spring数据/10.Spring集成Mybatis.md) +- [Spring 访问 Redis](02.Spring数据/21.Spring访问Redis.md) +- [Spring 访问 MongoDB](02.Spring数据/22.Spring访问MongoDB.md) +- [Spring 访问 Elasticsearch](02.Spring数据/23.Spring访问Elasticsearch.md) + +### Web + +- [Spring WebMvc](03.SpringWeb/01.SpringWebMvc.md) +- [SpringBoot 之应用 EasyUI](03.SpringWeb/21.SpringBoot之应用EasyUI.md) + +### IO + +- [SpringBoot 之异步请求](04.SpringIO/01.SpringBoot之异步请求.md) +- [SpringBoot 之 Json](04.SpringIO/02.SpringBoot之Json.md) +- [SpringBoot 之邮件](04.SpringIO/03.SpringBoot之邮件.md) + +### 集成 + +- [Spring 集成缓存中间件](05.Spring集成/01.Spring集成缓存.md) +- [Spring 集成定时任务中间件](05.Spring集成/02.Spring集成调度器.md) +- [Spring 集成 Dubbo](05.Spring集成/03.Spring集成Dubbo.md) + +### 其他 + +- [Spring4 升级](99.Spring其他/01.Spring4升级.md) +- [SpringBoot 之 banner](99.Spring其他/21.SpringBoot之banner.md) +- [SpringBoot 之 Actuator](99.Spring其他/22.SpringBoot之Actuator.md) + +## 💻 示例 + +### 核心篇示例 + +- [spring-core-actuator](https://github.com/dunwu/spring-tutorial/tree/master/codes/core/actuator) - Spring 应用监控示例。 +- [spring-core-aop](https://github.com/dunwu/spring-tutorial/tree/master/codes/core/aop) - Spring AOP 编程示例。 +- [spring-core-async](https://github.com/dunwu/spring-tutorial/tree/master/codes/core/async) - Spring 使用异步接口示例。 +- [spring-core-banner](https://github.com/dunwu/spring-tutorial/tree/master/codes/core/banner) - Spring 定制启动时的输出 Logo。 +- [spring-core-bean](https://github.com/dunwu/spring-tutorial/tree/master/codes/core/bean) - Spring 管理 JavaBean 生命周期示例。 +- [spring-core-conversion](https://github.com/dunwu/spring-tutorial/tree/master/codes/core/conversion) - Spring 数据转换示例。 +- [spring-core-data-binding](https://github.com/dunwu/spring-tutorial/tree/master/codes/core/data-binding) - Spring 数据绑定示例。 +- [spring-core-ioc](https://github.com/dunwu/spring-tutorial/tree/master/codes/core/ioc) - Spring IOC 示例。 +- [spring-core-profile](https://github.com/dunwu/spring-tutorial/tree/master/codes/core/profile) - 在 Spring 中根据 profile 在不同的环境下执行不同的行为。 +- [spring-core-property](https://github.com/dunwu/spring-tutorial/tree/master/codes/core/property) - 全方位的演示 Spring 加载属性的方式:记载 `properties` 和 `yaml` 两种文件;通过 `@Value`、`@ConfigurationProperties`、`Environment` 读取属性。 +- [spring-core-resource](https://github.com/dunwu/spring-tutorial/tree/master/codes/core/resource) - Spring 资源加载示例。 +- [spring-core-validation](https://github.com/dunwu/spring-tutorial/tree/master/codes/core/validation) - Spring 数据校验示例。 + +### 数据篇示例 + +- **JDBC** + - [spring-data-jdbc-basics](https://github.com/dunwu/spring-tutorial/tree/master/codes/data/jdbc/basics) - Spring Boot 以 JDBC 方式访问关系型数据库,通过 `JdbcTemplate` 执行基本的 CRUD 操作。 + - [spring-data-jdbc-druid](https://github.com/dunwu/spring-tutorial/tree/master/codes/data/jdbc/druid) - SpringBoot 使用 [Druid](https://github.com/alibaba/druid) 作为数据库连接池。 + - [spring-data-jdbc-multi-datasource](https://github.com/dunwu/spring-tutorial/tree/master/codes/data/jdbc/multi-datasource) - SpringBoot 连接多数据源示例。 + - [spring-data-jdbc-xml](https://github.com/dunwu/spring-tutorial/tree/master/codes/data/jdbc/xml) - Spring 以 JDBC 方式访问关系型数据库,通过 `JdbcTemplate` 执行基本的 CRUD 操作。 +- **ORM** + - [spring-data-orm-jpa](https://github.com/dunwu/spring-tutorial/tree/master/codes/data/orm/jpa) - SpringBoot 使用 JPA 作为 ORM 框架访问数据库示例。 + - [spring-data-orm-mybatis](https://github.com/dunwu/spring-tutorial/tree/master/codes/data/orm/mybatis) - Spring 使用 [MyBatis](https://github.com/mybatis/mybatis-3) 作为 ORM 框架访问数据库示例。 + - [spring-data-orm-mybatis-mapper](https://github.com/dunwu/spring-tutorial/tree/master/codes/data/orm/mybatis-mapper) - SpringBoot 使用 [MyBatis](https://github.com/mybatis/mybatis-3) + [Mapper](https://github.com/abel533/Mapper) + [PageHelper](https://github.com/pagehelper/Mybatis-PageHelper) 作为 ORM 框架访问数据库示例。 + - [spring-data-orm-mybatis-multi-datasource](https://github.com/dunwu/spring-tutorial/tree/master/codes/data/orm/mybatis-multi-datasource) - SpringBoot 连接多数据源,并使用 [MyBatis Plus](https://github.com/baomidou/mybatis-plus) 作为 ORM 框架访问数据库示例。 + - [spring-data-orm-mybatis-plus](https://github.com/dunwu/spring-tutorial/tree/master/codes/data/orm/mybatis-plus) - SpringBoot 使用 [MyBatis Plus](https://github.com/baomidou/mybatis-plus) 作为 ORM 框架访问数据库示例。 +- **Nosql** + - [spring-data-nosql-basics](https://github.com/dunwu/spring-tutorial/tree/master/codes/data/nosql/basics) - Spring 访问各种 NoSQL 的示例。 + - [spring-data-nosql-mongodb](https://github.com/dunwu/spring-tutorial/tree/master/codes/data/nosql/mongodb) - SpringBoot 访问 [MongoDB](https://www.mongodb.com/) 的示例。 + - [spring-data-nosql-redis](https://github.com/dunwu/spring-tutorial/tree/master/codes/data/nosql/redis) - SpringBoot 访问 [Redis](https://redis.io/) 单节点、集群的示例。 + - [spring-data-nosql-elasticsearch](https://github.com/dunwu/spring-tutorial/tree/master/codes/data/nosql/elasticsearch) - SpringBoot 访问 [Elasticsearch](https://www.elastic.co/guide/index.html) 的示例。 + - [spring-data-nosql-hdfs](https://github.com/dunwu/spring-tutorial/tree/master/codes/data/nosql/hdfs) - SpringBoot 访问 HDFS 的示例。 +- **Cache** + - [spring-data-cache-basics](https://github.com/dunwu/spring-tutorial/tree/master/codes/data/cache/basics) - SpringBoot 默认缓存框架的示例。 + - [spring-data-cache-j2cache](https://github.com/dunwu/spring-tutorial/tree/master/codes/data/cache/j2cache) - SpringBoot 使用 [j2cache](https://gitee.com/ld/J2Cache) 作为缓存框架的示例。 + - [spring-data-cache-jetcache](https://github.com/dunwu/spring-tutorial/tree/master/codes/data/cache/jetcache) - SpringBoot 使用 [jetcache](https://github.com/alibaba/jetcache) 作为缓存框架的示例。 +- **中间件** + - [spring-data-middleware-flyway](https://github.com/dunwu/spring-tutorial/tree/master/codes/data/middleware/flyway) - Spring 使用版本管理中间件 Flyway 示例。 + - [spring-data-middleware-sharding](https://github.com/dunwu/spring-tutorial/tree/master/codes/data/middleware/sharding) - Spring 使用分库分表中间件示例。 + +## 📚 资料 + +- **官方** + - [Spring 官网](https://spring.io/) + - [Spring Github](https://github.com/spring-projects/spring-framework) + - [Spring Framework 官方文档](https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/index.html) + - [Spring Boot 官方文档](https://docs.spring.io/spring-boot/docs/current/reference/html/data.html) +- **书籍** + - [《 Spring 实战(第 5 版)》](https://book.douban.com/subject/34949443/) +- **教程** + - [《小马哥讲 Spring 核心编程思想》](https://time.geekbang.org/course/intro/265) + - [geekbang-lessons](https://github.com/geektime-geekbang/geekbang-lessons) + - [跟我学 Spring3](http://jinnianshilongnian.iteye.com/blog/1482071) + +## 🚪 传送 + +◾ 💧 [钝悟的 IT 知识图谱](https://dunwu.github.io/waterdrop/) ◾ \ No newline at end of file diff --git "a/docs/13.\346\241\206\346\236\266/11.ORM/01.Mybatis\345\277\253\351\200\237\345\205\245\351\227\250.md" "b/docs/01.Java/13.\346\241\206\346\236\266/11.ORM/01.Mybatis\345\277\253\351\200\237\345\205\245\351\227\250.md" similarity index 97% rename from "docs/13.\346\241\206\346\236\266/11.ORM/01.Mybatis\345\277\253\351\200\237\345\205\245\351\227\250.md" rename to "docs/01.Java/13.\346\241\206\346\236\266/11.ORM/01.Mybatis\345\277\253\351\200\237\345\205\245\351\227\250.md" index 704af1b1..c6bbb278 100644 --- "a/docs/13.\346\241\206\346\236\266/11.ORM/01.Mybatis\345\277\253\351\200\237\345\205\245\351\227\250.md" +++ "b/docs/01.Java/13.\346\241\206\346\236\266/11.ORM/01.Mybatis\345\277\253\351\200\237\345\205\245\351\227\250.md" @@ -1,29 +1,28 @@ --- title: Mybatis快速入门 -categories: - - 编程 +date: 2022-02-17 22:34:30 +order: 01 +categories: - Java - 框架 - ORM -tags: +tags: - Java - 框架 - ORM - Mybatis -abbrlink: a7445586 -date: 2022-02-17 22:34:30 -permalink: /pages/538358/ +permalink: /pages/d4e6ee/ --- # MyBatis 快速入门 > MyBatis 的前身就是 iBatis ,是一个作用在数据持久层的对象关系映射(Object Relational Mapping,简称 ORM)框架。 -![img](https://raw.githubusercontent.com/dunwu/images/dev/snap/20200716162305.png) +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200716162305.png) ## Mybatis 简介 -![img](https://raw.githubusercontent.com/dunwu/images/dev/snap/20210510164925.png) +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20210510164925.png) ### 什么是 MyBatis @@ -254,7 +253,7 @@ public class MybatisDemo { ## Mybatis xml 配置 -> 配置的详细内容请参考:「 [Mybatis 官方文档之配置](http://www.mybatis.org/mybatis-3/zh/configuration.html) 」 。 +> 配置的详细内容请参考:“ [Mybatis 官方文档之配置](http://www.mybatis.org/mybatis-3/zh/configuration.html) ” 。 MyBatis 的配置文件包含了会深深影响 MyBatis 行为的设置和属性信息。主要配置项有以下: @@ -273,7 +272,7 @@ MyBatis 的配置文件包含了会深深影响 MyBatis 行为的设置和属性 ## Mybatis xml 映射器 -> SQL XML 映射文件详细内容请参考:「 [Mybatis 官方文档之 XML 映射文件](http://www.mybatis.org/mybatis-3/zh/sqlmap-xml.html) 」。 +> SQL XML 映射文件详细内容请参考:“ [Mybatis 官方文档之 XML 映射文件](http://www.mybatis.org/mybatis-3/zh/sqlmap-xml.html) ”。 XML 映射文件只有几个顶级元素: diff --git "a/docs/13.\346\241\206\346\236\266/11.ORM/02.Mybatis\345\216\237\347\220\206.md" "b/docs/01.Java/13.\346\241\206\346\236\266/11.ORM/02.Mybatis\345\216\237\347\220\206.md" similarity index 97% rename from "docs/13.\346\241\206\346\236\266/11.ORM/02.Mybatis\345\216\237\347\220\206.md" rename to "docs/01.Java/13.\346\241\206\346\236\266/11.ORM/02.Mybatis\345\216\237\347\220\206.md" index d36c074d..bd8b3760 100644 --- "a/docs/13.\346\241\206\346\236\266/11.ORM/02.Mybatis\345\216\237\347\220\206.md" +++ "b/docs/01.Java/13.\346\241\206\346\236\266/11.ORM/02.Mybatis\345\216\237\347\220\206.md" @@ -1,19 +1,19 @@ --- title: Mybatis原理 -categories: - - 编程 +date: 2022-02-17 22:34:30 +order: 02 +categories: - Java - 框架 - ORM -tags: +tags: - Java - 框架 - ORM - Mybatis -abbrlink: b18bb060 -date: 2022-02-17 22:34:30 -permalink: /pages/3f3dba/ +permalink: /pages/d55184/ --- + # Mybatis 原理 > Mybatis 的前身就是 iBatis ,是一款优秀的持久层框架,它支持自定义 SQL、存储过程以及高级映射。本文以一个 Mybatis 完整示例为切入点,结合 Mybatis 底层源码分析,图文并茂的讲解 Mybatis 的核心工作机制。 @@ -229,7 +229,7 @@ public class MybatisDemo { ## Mybatis 生命周期 -![img](https://raw.githubusercontent.com/dunwu/images/dev/snap/20210510113446.png) +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20210510113446.png) ### SqlSessionFactoryBuilder @@ -239,7 +239,7 @@ public class MybatisDemo { `Configuration` 类包含了对一个 `SqlSessionFactory` 实例你可能关心的所有内容。 -![img](https://raw.githubusercontent.com/dunwu/images/dev/snap/20210508173040.png) +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20210508173040.png) `SqlSessionFactoryBuilder` 应用了建造者设计模式,它有五个 `build` 方法,允许你通过不同的资源创建 `SqlSessionFactory` 实例。 @@ -261,7 +261,7 @@ SqlSessionFactory build(Configuration config) **`SqlSessionFactory` 负责创建 `SqlSession` 实例。** -![img](https://raw.githubusercontent.com/dunwu/images/dev/snap/20210510105641.png) +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20210510105641.png) `SqlSessionFactory` 应用了工厂设计模式,它提供了一组方法,用于创建 SqlSession 实例。 @@ -300,11 +300,11 @@ Configuration getConfiguration(); **Mybatis 的主要 Java 接口就是 `SqlSession`。它包含了所有执行语句,获取映射器和管理事务等方法。** -> 详细内容可以参考:「 [Mybatis 官方文档之 SqlSessions](http://www.Mybatis.org/Mybatis-3/zh/java-api.html#sqlSessions) 」 。 +> 详细内容可以参考:“ [Mybatis 官方文档之 SqlSessions](http://www.Mybatis.org/Mybatis-3/zh/java-api.html#sqlSessions) ” 。 SqlSession 类的方法可以按照下图进行大致分类: -![img](https://raw.githubusercontent.com/dunwu/images/dev/snap/20210510110638.png) +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20210510110638.png) #### SqlSession 生命周期 @@ -338,7 +338,7 @@ Mybatis 会根据相应的接口声明的方法信息,通过动态代理机制 下面的示例展示了一些方法签名以及它们是如何映射到 `SqlSession` 上的。 -![img](https://raw.githubusercontent.com/dunwu/images/dev/snap/20210512111723.png) +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20210512111723.png) > **注意** > @@ -383,7 +383,7 @@ Mybatis 支持诸如 `@Insert`、`@Update`、`@Delete`、`@Select`、`@Result` 这些组件的架构层次如下: -![img](https://raw.githubusercontent.com/dunwu/images/dev/snap/20210512114852.png) +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20210512114852.png) ### 配置层 @@ -437,13 +437,13 @@ Mybatis 和数据库的交互有两种方式: - 如果开启了二级缓存,`SqlSession` 会先使用 `CachingExecutor` 对象来处理查询请求。`CachingExecutor` 会在二级缓存中查看是否有匹配的数据,如果匹配,则直接返回缓存结果;如果缓存中没有,再交给真正的 `Executor` 对象来完成查询,之后 `CachingExecutor` 会将真正 `Executor` 返回的查询结果放置到缓存中,然后在返回给用户。 - 二级缓存的生命周期是应用级别的。 -![img](https://raw.githubusercontent.com/dunwu/images/dev/snap/20210512185709.png) +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20210512185709.png) ## SqlSession 内部工作机制 从前文,我们已经了解了,Mybatis 封装了对数据库的访问,把对数据库的会话和事务控制放到了 SqlSession 对象中。那么具体是如何工作的呢?接下来,我们通过源码解读来进行分析。 -![img](https://raw.githubusercontent.com/dunwu/images/dev/snap/20210512173437.png) +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20210512173437.png) `SqlSession` 对于 insert、update、delete、select 的内部处理机制基本上大同小异。所以,接下来,我会以一次完整的 select 查询流程为例讲解 `SqlSession` 内部的工作机制。相信读者如果理解了 select 的处理流程,对于其他 CRUD 操作也能做到一通百通。 @@ -459,7 +459,7 @@ Mybatis 和数据库的交互有两种方式: Executor 即执行器,它负责生成动态 SQL 以及管理缓存。 -![img](https://raw.githubusercontent.com/dunwu/images/dev/snap/20210512150000.png) +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20210512150000.png) - `Executor` 即执行器接口。 - `BaseExecutor` 是 `Executor` 的抽象类,它采用了模板方法设计模式,内置了一些共性方法,而将定制化方法留给子类去实现。 @@ -474,7 +474,7 @@ Executor 即执行器,它负责生成动态 SQL 以及管理缓存。 `StatementHandler` 的家族成员: -![img](https://raw.githubusercontent.com/dunwu/images/dev/snap/20210512160243.png) +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20210512160243.png) - `StatementHandler` 是接口; - `BaseStatementHandler` 是实现 `StatementHandler` 的抽象类,内置一些共性方法; @@ -572,7 +572,7 @@ Mybatis 所有的配置信息都维持在 `Configuration` 对象之中。中维 `MappedStatement` 维护了一个 Mapper 方法的元数据信息,其数据组织可以参考下面的 debug 截图: -![img](https://raw.githubusercontent.com/dunwu/images/dev/snap/20210511150650.png) +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20210511150650.png) > 小结: > diff --git "a/docs/13.\346\241\206\346\236\266/11.ORM/README.md" "b/docs/01.Java/13.\346\241\206\346\236\266/11.ORM/README.md" similarity index 85% rename from "docs/13.\346\241\206\346\236\266/11.ORM/README.md" rename to "docs/01.Java/13.\346\241\206\346\236\266/11.ORM/README.md" index fe20c394..1cb04ab8 100644 --- "a/docs/13.\346\241\206\346\236\266/11.ORM/README.md" +++ "b/docs/01.Java/13.\346\241\206\346\236\266/11.ORM/README.md" @@ -1,18 +1,17 @@ --- title: Java ORM 框架 -categories: - - 编程 +date: 2022-02-17 22:34:30 +categories: - Java - 框架 - ORM -tags: +tags: - Java - 框架 - ORM -abbrlink: 2f780626 -date: 2022-02-17 22:34:30 +permalink: /pages/fe879a/ hidden: true -permalink: /pages/e873e1/ +index: false --- # Java ORM 框架 @@ -21,7 +20,7 @@ permalink: /pages/e873e1/ > Mybatis 的前身就是 iBatis ,是一款优秀的持久层框架,它支持自定义 SQL、存储过程以及高级映射。本文以一个 Mybatis 完整示例为切入点,结合 Mybatis 底层源码分析,图文并茂的讲解 Mybatis 的核心工作机制。 -![img](https://raw.githubusercontent.com/dunwu/images/dev/snap/20210522101005.png) +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20210522101005.png) ### [Mybatis 快速入门](01.Mybatis快速入门.md) @@ -48,4 +47,4 @@ permalink: /pages/e873e1/ ## 🚪 传送 -◾ 🏠 [JAVA-TUTORIAL 首页](https://github.com/dunwu/java-tutorial) ◾ 🎯 [我的博客](https://github.com/dunwu/blog) ◾ +◾ 💧 [钝悟的 IT 知识图谱](https://dunwu.github.io/waterdrop/) ◾ \ No newline at end of file diff --git "a/docs/13.\346\241\206\346\236\266/12.\345\256\211\345\205\250/01.Shiro.md" "b/docs/01.Java/13.\346\241\206\346\236\266/12.\345\256\211\345\205\250/01.Shiro.md" similarity index 97% rename from "docs/13.\346\241\206\346\236\266/12.\345\256\211\345\205\250/01.Shiro.md" rename to "docs/01.Java/13.\346\241\206\346\236\266/12.\345\256\211\345\205\250/01.Shiro.md" index e2b4f31e..70d8f3dd 100644 --- "a/docs/13.\346\241\206\346\236\266/12.\345\256\211\345\205\250/01.Shiro.md" +++ "b/docs/01.Java/13.\346\241\206\346\236\266/12.\345\256\211\345\205\250/01.Shiro.md" @@ -1,18 +1,17 @@ --- title: Shiro 快速入门 -categories: - - 编程 +date: 2022-02-17 22:34:30 +order: 01 +categories: - Java - 框架 - 安全 -tags: +tags: - Java - 框架 - 安全 - Shiro -abbrlink: 1c025fc -date: 2022-02-17 22:34:30 -permalink: /pages/cd25bf/ +permalink: /pages/3295c4/ --- # Shiro 快速入门 @@ -24,7 +23,7 @@ permalink: /pages/cd25bf/ ### Shiro 特性

- +

核心功能: @@ -48,7 +47,7 @@ permalink: /pages/cd25bf/ ### Shiro 架构概述

- +

- **Subject** - **主题**。它代表当前用户,`Subject` 可以是一个人,但也可以是第三方服务、守护进程帐户、时钟守护任务或者其它——当前和软件交互的任何事件。`Subject` 是 Shiro 的入口。 @@ -63,7 +62,7 @@ permalink: /pages/cd25bf/ `SecurityManager` 是 Shiro 框架核心中的核心,它相当于 Shiro 的总指挥,负责调度所有行为,包括:认证、授权、获取安全数据(调用 `Realm`)、会话管理等。 -![img](https://raw.githubusercontent.com/dunwu/images/dev/cs/java/javaweb/standalone/security/shiro/ShiroArchitecture.png) +![img](https://raw.githubusercontent.com/dunwu/images/master/cs/java/javaweb/standalone/security/shiro/ShiroArchitecture.png) `SecurityManager` 聚合了以下组件: @@ -128,7 +127,7 @@ currentUser.logout(); ### 认证流程 -![img](https://raw.githubusercontent.com/dunwu/images/dev/snap/20200317092427.png) +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200317092427.png) 1. 应用程序代码调用 `Subject.login` 方法,传入构造的 `AuthenticationToken` 实例,该实例代表最终用户的 `Principals` 和 `Credentials`。 @@ -282,7 +281,7 @@ public void updateAccount(Account userAccount) { ### 授权流程 -![img](https://raw.githubusercontent.com/dunwu/images/dev/snap/20200317092618.png) +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200317092618.png) 1. 应用程序或框架代码调用任何 `Subject` 的 `hasRole*`,`checkRole*`,`isPermitted*` 或 `checkPermission*` 方法,并传入所需的权限或角色。 @@ -439,4 +438,4 @@ SecurityUtils.getSubject().login(token); - [Shiro 官方文档](http://shiro.apache.org/reference.html) - [跟我学 Shiro](http://jinnianshilongnian.iteye.com/category/305053) -- [The New RBAC: Resource-Based Access Control](https://stormpath.com/blog/new-rbac-resource-based-access-control) +- [The New RBAC: Resource-Based Access Control](https://stormpath.com/blog/new-rbac-resource-based-access-control) \ No newline at end of file diff --git "a/docs/13.\346\241\206\346\236\266/12.\345\256\211\345\205\250/02.SpringSecurity.md" "b/docs/01.Java/13.\346\241\206\346\236\266/12.\345\256\211\345\205\250/02.SpringSecurity.md" similarity index 98% rename from "docs/13.\346\241\206\346\236\266/12.\345\256\211\345\205\250/02.SpringSecurity.md" rename to "docs/01.Java/13.\346\241\206\346\236\266/12.\345\256\211\345\205\250/02.SpringSecurity.md" index 4c136eb7..3434c2dd 100644 --- "a/docs/13.\346\241\206\346\236\266/12.\345\256\211\345\205\250/02.SpringSecurity.md" +++ "b/docs/01.Java/13.\346\241\206\346\236\266/12.\345\256\211\345\205\250/02.SpringSecurity.md" @@ -1,19 +1,19 @@ --- title: Spring Security 快速入门 -categories: - - 编程 +date: 2022-02-17 22:34:30 +order: 02 +categories: - Java - 框架 - 安全 -tags: +tags: - Java - 框架 - 安全 - SpringSecurity -abbrlink: a750524a -date: 2022-02-17 22:34:30 -permalink: /pages/a6cc5f/ +permalink: /pages/050cdd/ --- + # Spring Security 快速入门 ## 快速开始 @@ -70,7 +70,7 @@ try { Spring Security 框架中的认证数据模型如下: -![img](https://raw.githubusercontent.com/dunwu/images/dev/snap/20200331115710.png) +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200331115710.png) - `Authentication` - 认证信息实体。 - `principal` - 用户标识。如:用户名、账户名等。通常是 `UserDetails` 的实例(后面详细讲解)。 @@ -234,4 +234,4 @@ Spring Security 的 servlet 支持通过与 `PasswordEncoder` 集成来安全地 ## 参考资料 - [Spring Security Architecture](https://spring.io/guides/topicals/spring-security-architecture) -- [Securing a Web Application](https://spring.io/guides/gs/securing-web/) +- [Securing a Web Application](https://spring.io/guides/gs/securing-web/) \ No newline at end of file diff --git "a/docs/13.\346\241\206\346\236\266/13.IO/01.Netty.md" "b/docs/01.Java/13.\346\241\206\346\236\266/13.IO/01.Netty.md" similarity index 98% rename from "docs/13.\346\241\206\346\236\266/13.IO/01.Netty.md" rename to "docs/01.Java/13.\346\241\206\346\236\266/13.IO/01.Netty.md" index 36e0ded8..8bf182aa 100644 --- "a/docs/13.\346\241\206\346\236\266/13.IO/01.Netty.md" +++ "b/docs/01.Java/13.\346\241\206\346\236\266/13.IO/01.Netty.md" @@ -1,17 +1,16 @@ --- title: Netty 快速入门 -categories: - - 编程 +date: 2022-02-17 22:34:30 +order: 01 +categories: - Java - 框架 - IO -tags: +tags: - Java - IO - Netty -abbrlink: ddaa8ce6 -date: 2022-02-17 22:34:30 -permalink: /pages/520c52/ +permalink: /pages/10bd70/ --- # Netty 快速入门 @@ -144,4 +143,4 @@ public class NettyOioServer { - **文章** - [Netty 入门教程——认识 Netty](https://www.jianshu.com/p/b9f3f6a16911) - [彻底理解 Netty,这一篇文章就够了](https://juejin.im/post/5bdaf8ea6fb9a0227b02275a) - - [Java 200+ 面试题补充 ② Netty 模块](https://juejin.im/post/5c81b08f5188257a323f4cef) + - [Java 200+ 面试题补充 ② Netty 模块](https://juejin.im/post/5c81b08f5188257a323f4cef) \ No newline at end of file diff --git "a/docs/01.Java/13.\346\241\206\346\236\266/README.md" "b/docs/01.Java/13.\346\241\206\346\236\266/README.md" new file mode 100644 index 00000000..7843b3d3 --- /dev/null +++ "b/docs/01.Java/13.\346\241\206\346\236\266/README.md" @@ -0,0 +1,121 @@ +--- +title: Java 框架 +date: 2022-02-18 08:53:11 +categories: + - Java + - 框架 +tags: + - Java + - 框架 +permalink: /pages/e373d7/ +hidden: true +index: false +--- + +# Java 框架 + +## 📖 内容 + +### Spring + +#### 综合 + +- [Spring 概述](01.Spring/00.Spring综合/01.Spring概述.md) +- [SpringBoot 知识图谱](01.Spring/00.Spring综合/21.SpringBoot知识图谱.md) +- [SpringBoot 基本原理](01.Spring/00.Spring综合/22.SpringBoot基本原理.md) +- [Spring 面试](01.Spring/00.Spring综合/99.Spring面试.md) + +#### 核心 + +- [Spring Bean](01.Spring/01.Spring核心/01.SpringBean.md) +- [Spring IoC](01.Spring/01.Spring核心/02.SpringIoC.md) +- [Spring 依赖查找](01.Spring/01.Spring核心/03.Spring依赖查找.md) +- [Spring 依赖注入](01.Spring/01.Spring核心/04.Spring依赖注入.md) +- [Spring IoC 依赖来源](01.Spring/01.Spring核心/05.SpringIoC依赖来源.md) +- [Spring Bean 作用域](01.Spring/01.Spring核心/06.SpringBean作用域.md) +- [Spring Bean 生命周期](01.Spring/01.Spring核心/07.SpringBean生命周期.md) +- [Spring 配置元数据](01.Spring/01.Spring核心/08.Spring配置元数据.md) +- [Spring AOP](01.Spring/01.Spring核心/10.SpringAop.md) +- [Spring 资源管理](01.Spring/01.Spring核心/20.Spring资源管理.md) +- [Spring 校验](01.Spring/01.Spring核心/21.Spring校验.md) +- [Spring 数据绑定](01.Spring/01.Spring核心/22.Spring数据绑定.md) +- [Spring 类型转换](01.Spring/01.Spring核心/23.Spring类型转换.md) +- [Spring EL 表达式](01.Spring/01.Spring核心/24.SpringEL.md) +- [Spring 事件](01.Spring/01.Spring核心/25.Spring事件.md) +- [Spring 国际化](01.Spring/01.Spring核心/26.Spring国际化.md) +- [Spring 泛型处理](01.Spring/01.Spring核心/27.Spring泛型处理.md) +- [Spring 注解](01.Spring/01.Spring核心/28.Spring注解.md) +- [Spring Environment 抽象](01.Spring/01.Spring核心/29.SpringEnvironment抽象.md) +- [SpringBoot 教程之快速入门](01.Spring/01.Spring核心/31.SpringBoot之快速入门.md) +- [SpringBoot 之属性加载](01.Spring/01.Spring核心/32.SpringBoot之属性加载.md) +- [SpringBoot 之 Profile](01.Spring/01.Spring核心/33.SpringBoot之Profile.md) + +#### 数据 + +- [Spring 之数据源](01.Spring/02.Spring数据/01.Spring之数据源.md) +- [Spring 之 JDBC](01.Spring/02.Spring数据/02.Spring之JDBC.md) +- [Spring 之事务](01.Spring/02.Spring数据/03.Spring之事务.md) +- [Spring 之 JPA](01.Spring/02.Spring数据/04.Spring之JPA.md) +- [Spring 集成 Mybatis](01.Spring/02.Spring数据/10.Spring集成Mybatis.md) +- [Spring 访问 Redis](01.Spring/02.Spring数据/21.Spring访问Redis.md) +- [Spring 访问 MongoDB](01.Spring/02.Spring数据/22.Spring访问MongoDB.md) +- [Spring 访问 Elasticsearch](01.Spring/02.Spring数据/23.Spring访问Elasticsearch.md) + +#### Web + +- [Spring WebMvc](01.Spring/03.SpringWeb/01.SpringWebMvc.md) +- [SpringBoot 之应用 EasyUI](01.Spring/03.SpringWeb/21.SpringBoot之应用EasyUI.md) + +#### IO + +- [SpringBoot 之异步请求](01.Spring/04.SpringIO/01.SpringBoot之异步请求.md) +- [SpringBoot 之 Json](01.Spring/04.SpringIO/02.SpringBoot之Json.md) +- [SpringBoot 之邮件](01.Spring/04.SpringIO/03.SpringBoot之邮件.md) + +#### 集成 + +- [Spring 集成缓存中间件](01.Spring/05.Spring集成/01.Spring集成缓存.md) +- [Spring 集成定时任务中间件](01.Spring/05.Spring集成/02.Spring集成调度器.md) +- [Spring 集成 Dubbo](01.Spring/05.Spring集成/03.Spring集成Dubbo.md) + +#### 其他 + +- [Spring4 升级](01.Spring/99.Spring其他/01.Spring4升级.md) +- [SpringBoot 之 banner](01.Spring/99.Spring其他/21.SpringBoot之banner.md) +- [SpringBoot 之 Actuator](01.Spring/99.Spring其他/22.SpringBoot之Actuator.md) + +### ORM + +- [Mybatis 快速入门](11.ORM/01.Mybatis快速入门.md) +- [Mybatis 原理](11.ORM/02.Mybatis原理.md) + +### 安全 + +> Java 领域比较流行的安全框架就是 shiro 和 spring-security。 +> +> shiro 更为简单、轻便,容易理解,能满足大多数基本安全场景下的需要。 +> +> spring-security 功能更丰富,也比 shiro 更复杂。值得一提的是由于 spring-security 是 spring 团队开发,所以集成 spring 和 spring-boot 框架更容易。 + +- [Shiro](12.安全/01.Shiro.md) +- [SpringSecurity](12.安全/02.SpringSecurity.md) + +### IO + +- [Netty](13.IO/01.Netty.md) + +## 📚 资料 + +- **Mybatis** + - [Mybatis Github](https://github.com/mybatis/mybatis-3) + - [Mybatis 官网](http://www.mybatis.org/mybatis-3/) + - [MyBatis 官方代码生成(mybatis-generator)](https://github.com/mybatis/generator) + - [MyBatis 官方集成 Spring(mybatis-spring)](https://github.com/mybatis/spring) + - [Mybatis 官方集成 Spring Boot(mybatis-spring-boot)](https://github.com/mybatis/spring-boot-starter) + - [MyBatis-Plus](https://github.com/baomidou/mybatis-plus) - CRUD 扩展插件、代码生成器、分页器等多功能 + - [Mapper](https://github.com/abel533/Mapper) - CRUD 扩展插件 + - [Mybatis-PageHelper](https://github.com/pagehelper/Mybatis-PageHelper) - Mybatis 通用分页插件 + +## 🚪 传送 + +◾ 💧 [钝悟的 IT 知识图谱](https://dunwu.github.io/waterdrop/) ◾ \ No newline at end of file diff --git "a/docs/14.\344\270\255\351\227\264\344\273\266/02.\347\274\223\345\255\230/02.Java\347\274\223\345\255\230\344\270\255\351\227\264\344\273\266.md" "b/docs/01.Java/14.\344\270\255\351\227\264\344\273\266/02.\347\274\223\345\255\230/02.Java\347\274\223\345\255\230\344\270\255\351\227\264\344\273\266.md" similarity index 98% rename from "docs/14.\344\270\255\351\227\264\344\273\266/02.\347\274\223\345\255\230/02.Java\347\274\223\345\255\230\344\270\255\351\227\264\344\273\266.md" rename to "docs/01.Java/14.\344\270\255\351\227\264\344\273\266/02.\347\274\223\345\255\230/02.Java\347\274\223\345\255\230\344\270\255\351\227\264\344\273\266.md" index 84a962ca..9adf2b83 100644 --- "a/docs/14.\344\270\255\351\227\264\344\273\266/02.\347\274\223\345\255\230/02.Java\347\274\223\345\255\230\344\270\255\351\227\264\344\273\266.md" +++ "b/docs/01.Java/14.\344\270\255\351\227\264\344\273\266/02.\347\274\223\345\255\230/02.Java\347\274\223\345\255\230\344\270\255\351\227\264\344\273\266.md" @@ -1,17 +1,16 @@ --- title: Java 缓存中间件 -categories: - - 编程 +date: 2022-02-17 22:34:30 +order: 02 +categories: - Java - 中间件 - 缓存 -tags: +tags: - Java - 中间件 - 缓存 -abbrlink: e87a26e9 -date: 2022-02-17 22:34:30 -permalink: /pages/970fa6/ +permalink: /pages/85460d/ --- # Java 缓存中间件 @@ -24,7 +23,7 @@ permalink: /pages/970fa6/ 因此,在很多缓存框架、缓存库中,其 API 都参考了 JSR 107 规范。 -![img](https://raw.githubusercontent.com/dunwu/images/dev/snap/20200709174139.png) +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200709174139.png) Java Caching 定义了 5 个核心接口 @@ -338,4 +337,4 @@ public interface UserService { - [Spring Boot Cache 特性官方文档](https://docs.spring.io/spring-boot/docs/2.3.1.RELEASE/reference/html/spring-boot-features.html#boot-features-caching) - [J2Cache Gitee](https://gitee.com/ld/J2Cache) - [jetcache Github](https://github.com/alibaba/jetcache) -- [jetcache wiki](https://github.com/alibaba/jetcache/wiki/Home_CN) +- [jetcache wiki](https://github.com/alibaba/jetcache/wiki/Home_CN) \ No newline at end of file diff --git "a/docs/14.\344\270\255\351\227\264\344\273\266/02.\347\274\223\345\255\230/04.Ehcache.md" "b/docs/01.Java/14.\344\270\255\351\227\264\344\273\266/02.\347\274\223\345\255\230/04.Ehcache.md" similarity index 98% rename from "docs/14.\344\270\255\351\227\264\344\273\266/02.\347\274\223\345\255\230/04.Ehcache.md" rename to "docs/01.Java/14.\344\270\255\351\227\264\344\273\266/02.\347\274\223\345\255\230/04.Ehcache.md" index 5ffa73cd..dd76153f 100644 --- "a/docs/14.\344\270\255\351\227\264\344\273\266/02.\347\274\223\345\255\230/04.Ehcache.md" +++ "b/docs/01.Java/14.\344\270\255\351\227\264\344\273\266/02.\347\274\223\345\255\230/04.Ehcache.md" @@ -1,25 +1,24 @@ --- title: Ehcache 快速入门 -categories: - - 编程 +date: 2022-02-17 22:34:30 +order: 04 +categories: - Java - 中间件 - 缓存 -tags: +tags: - Java - 中间件 - 缓存 - Ehcache -abbrlink: 2720adf1 -date: 2022-02-17 22:34:30 -permalink: /pages/c4647d/ +permalink: /pages/5f7893/ --- # Ehcache 快速入门 > EhCache 是一个纯 Java 的进程内缓存框架,具有快速、精干等特点,是 Hibernate 中默认的 CacheProvider。 -![img](https://raw.githubusercontent.com/dunwu/images/dev/cs/java/javaweb/technology/cache/ehcache-architecture.png) +![img](https://raw.githubusercontent.com/dunwu/images/master/cs/java/javaweb/technology/cache/ehcache-architecture.png) ## 一、简介 @@ -531,4 +530,4 @@ public class BookRepositoryImpl implements BookRepository { - [Ehcache 优缺点以及分布式详解](https://yq.aliyun.com/articles/72885?utm_campaign=wenzhang&utm_medium=article&utm_source=QQ-qun&2017331&utm_content=m_15513) - [Ehcache 详细解读](http://raychase.iteye.com/blog/1545906) - [注释驱动的 Spring cache 缓存介绍](http://www.ibm.com/developerworks/cn/opensource/os-cn-spring-cache/) - - [Spring 官方文档第 36 章缓存抽象](http://docs.spring.io/spring/docs/current/spring-framework-reference/htmlsingle/) + - [Spring 官方文档第 36 章缓存抽象](http://docs.spring.io/spring/docs/current/spring-framework-reference/htmlsingle/) \ No newline at end of file diff --git "a/docs/14.\344\270\255\351\227\264\344\273\266/02.\347\274\223\345\255\230/05.Java\350\277\233\347\250\213\345\206\205\347\274\223\345\255\230.md" "b/docs/01.Java/14.\344\270\255\351\227\264\344\273\266/02.\347\274\223\345\255\230/05.Java\350\277\233\347\250\213\345\206\205\347\274\223\345\255\230.md" similarity index 98% rename from "docs/14.\344\270\255\351\227\264\344\273\266/02.\347\274\223\345\255\230/05.Java\350\277\233\347\250\213\345\206\205\347\274\223\345\255\230.md" rename to "docs/01.Java/14.\344\270\255\351\227\264\344\273\266/02.\347\274\223\345\255\230/05.Java\350\277\233\347\250\213\345\206\205\347\274\223\345\255\230.md" index 24ec0c3e..53562aa9 100644 --- "a/docs/14.\344\270\255\351\227\264\344\273\266/02.\347\274\223\345\255\230/05.Java\350\277\233\347\250\213\345\206\205\347\274\223\345\255\230.md" +++ "b/docs/01.Java/14.\344\270\255\351\227\264\344\273\266/02.\347\274\223\345\255\230/05.Java\350\277\233\347\250\213\345\206\205\347\274\223\345\255\230.md" @@ -1,17 +1,16 @@ --- title: Java 进程内缓存 -categories: - - 编程 +date: 2022-02-17 22:34:30 +order: 05 +categories: - Java - 中间件 - 缓存 -tags: +tags: - Java - 中间件 - 缓存 -abbrlink: bd2603d3 -date: 2022-02-17 22:34:30 -permalink: /pages/9632fd/ +permalink: /pages/59f078/ --- # Java 进程内缓存 @@ -152,7 +151,7 @@ Caffeine 实现了 W-TinyLFU(**LFU** + **LRU** 算法的变种),其**命中率 ## 五、Ehcache -> 参考:[Ehcache](docs/04.中间件/02.缓存/ehcache.md) +> 参考:[Ehcache](04.Ehcache.md) ## 六、进程内缓存对比 @@ -182,4 +181,4 @@ Caffeine 实现了 W-TinyLFU(**LFU** + **LRU** 算法的变种),其**命中率 - [Caffeine 缓存](https://www.jianshu.com/p/9a80c662dac4) - [Google Guava 官方教程(中文版)](https://wizardforcel.gitbooks.io/guava-tutorial/content/1.html) - [Google Guava Cache 全解析](https://www.jianshu.com/p/38bd5f1cf2f2) -- [注释驱动的 Spring cache 缓存介绍](https://developer.ibm.com/zh/articles/os-cn-spring-cache/) +- [注释驱动的 Spring cache 缓存介绍](https://developer.ibm.com/zh/articles/os-cn-spring-cache/) \ No newline at end of file diff --git "a/docs/14.\344\270\255\351\227\264\344\273\266/02.\347\274\223\345\255\230/06.Http\347\274\223\345\255\230.md" "b/docs/01.Java/14.\344\270\255\351\227\264\344\273\266/02.\347\274\223\345\255\230/06.Http\347\274\223\345\255\230.md" similarity index 98% rename from "docs/14.\344\270\255\351\227\264\344\273\266/02.\347\274\223\345\255\230/06.Http\347\274\223\345\255\230.md" rename to "docs/01.Java/14.\344\270\255\351\227\264\344\273\266/02.\347\274\223\345\255\230/06.Http\347\274\223\345\255\230.md" index ee4c337c..b9c26d34 100644 --- "a/docs/14.\344\270\255\351\227\264\344\273\266/02.\347\274\223\345\255\230/06.Http\347\274\223\345\255\230.md" +++ "b/docs/01.Java/14.\344\270\255\351\227\264\344\273\266/02.\347\274\223\345\255\230/06.Http\347\274\223\345\255\230.md" @@ -1,16 +1,15 @@ --- -title: http-cache -categories: - - 编程 +title: Http 缓存 +date: 2022-02-17 22:34:30 +order: 06 +categories: - Java - 中间件 - 缓存 -tags: +tags: - 缓存 - Http -abbrlink: 74d07b78 -date: 2022-02-17 22:34:30 -permalink: /pages/c09100/ +permalink: /pages/30abaa/ --- # Http 缓存 @@ -75,4 +74,4 @@ if-Unmodified-Since: 从字面上看, 就是说: 从某个时间点算起, 是 - [图解 HTTP 缓存](https://juejin.im/post/5eb7f811f265da7bbc7cc5bd) - [HTTP----HTTP 缓存机制](https://juejin.im/post/5a1d4e546fb9a0450f21af23) -- [缓存详解](https://juejin.im/post/5a6c87c46fb9a01ca560b4d7) +- [缓存详解](https://juejin.im/post/5a6c87c46fb9a01ca560b4d7) \ No newline at end of file diff --git "a/docs/14.\344\270\255\351\227\264\344\273\266/02.\347\274\223\345\255\230/README.md" "b/docs/01.Java/14.\344\270\255\351\227\264\344\273\266/02.\347\274\223\345\255\230/README.md" similarity index 79% rename from "docs/14.\344\270\255\351\227\264\344\273\266/02.\347\274\223\345\255\230/README.md" rename to "docs/01.Java/14.\344\270\255\351\227\264\344\273\266/02.\347\274\223\345\255\230/README.md" index 758a8e1d..b5487f64 100644 --- "a/docs/14.\344\270\255\351\227\264\344\273\266/02.\347\274\223\345\255\230/README.md" +++ "b/docs/01.Java/14.\344\270\255\351\227\264\344\273\266/02.\347\274\223\345\255\230/README.md" @@ -1,18 +1,17 @@ --- title: Java 缓存 -categories: - - 编程 +date: 2022-02-17 22:34:30 +categories: - Java - 中间件 - 缓存 -tags: +tags: - Java - 中间件 - 缓存 -abbrlink: ad7f7d3 -date: 2022-02-17 22:34:30 +permalink: /pages/c4efe9/ hidden: true -permalink: /pages/71104f/ +index: false --- # Java 缓存 @@ -21,13 +20,11 @@ permalink: /pages/71104f/ > > 如果想深入学习缓存,建议先了解一下 [缓存基本原理](https://dunwu.github.io/design/distributed/分布式缓存.html),有助于理解缓存的特性、原理,使用缓存常见的问题及解决方案。 -![img](https://raw.githubusercontent.com/dunwu/images/dev/snap/20200710163555.png) +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200710163555.png) ## 📖 内容 -- [缓存面试题](01.缓存面试题.md) - [Java 缓存框架](02.Java缓存中间件.md) -- [Memcached 快速入门](03.Memcached.md) - [Ehcache 快速入门](04.Ehcache.md) - [Java 缓存库](05.Java进程内缓存.md) - [Http 缓存](06.Http缓存.md) @@ -48,4 +45,4 @@ permalink: /pages/71104f/ ## 🚪 传送 -◾ 🏠 [JAVA-TUTORIAL 首页](https://github.com/dunwu/java-tutorial) ◾ 🎯 [我的博客](https://github.com/dunwu/blog) ◾ +◾ 💧 [钝悟的 IT 知识图谱](https://dunwu.github.io/waterdrop/) ◾ \ No newline at end of file diff --git "a/docs/14.\344\270\255\351\227\264\344\273\266/03.\346\265\201\351\207\217\346\216\247\345\210\266/01.Hystrix.md" "b/docs/01.Java/14.\344\270\255\351\227\264\344\273\266/03.\346\265\201\351\207\217\346\216\247\345\210\266/01.Hystrix.md" similarity index 86% rename from "docs/14.\344\270\255\351\227\264\344\273\266/03.\346\265\201\351\207\217\346\216\247\345\210\266/01.Hystrix.md" rename to "docs/01.Java/14.\344\270\255\351\227\264\344\273\266/03.\346\265\201\351\207\217\346\216\247\345\210\266/01.Hystrix.md" index 6d7d8e2a..5e347d9a 100644 --- "a/docs/14.\344\270\255\351\227\264\344\273\266/03.\346\265\201\351\207\217\346\216\247\345\210\266/01.Hystrix.md" +++ "b/docs/01.Java/14.\344\270\255\351\227\264\344\273\266/03.\346\265\201\351\207\217\346\216\247\345\210\266/01.Hystrix.md" @@ -1,31 +1,30 @@ --- title: Hystrix 快速入门 -categories: - - 编程 +date: 2022-02-17 22:34:30 +order: 01 +categories: - Java - 中间件 - 流量控制 -tags: +tags: - Java - 中间件 - 流量控制 - Hystrix -abbrlink: eea3b9ff -date: 2022-02-17 22:34:30 -permalink: /pages/f2ebed/ +permalink: /pages/364124/ --- # Hystrix 快速入门 -## 一、Hystrix 简介 +## Hystrix 简介 ### Hystrix 是什么 -Hystrix 是 Netflix 开源的一款容错框架,包含常用的容错方法:线程池隔离、信号量隔离、熔断、降级。 +Hystrix 是由 Netflix 开源,用于处理分布式系统的延迟和容错的一个开源组件。在分布式系统里,许多依赖不可避免的会调用失败,比如超时、异常等。Hystrix 采用**断路器模式**来实现服务间的彼此隔离,从而避免级联故障,以提高分布式系统整体的弹性。 -Hystrix 官方宣布**不再发布新版本**。 +“断路器”本身是一种开关装置,当某个服务单元发生故障之后,通过断路器的故障监控(类似熔断保险丝),**向调用方返回一个符合预期的、可处理的备选响应(FallBack),而不是长时间的等待或者抛出调用方无法处理的异常**,这样就保证了服务调用方的线程不会被长时间、不必要地占用,从而避免了故障在分布式系统中的蔓延,乃至雪崩。 -但是 Hystrix 的客户端熔断保护,断路器设计理念,有非常高的学习价值。 +Hystrix 官方已宣布**不再发布新版本**。但是,Hystrix 的断路器设计理念,有非常高的学习价值。 ### 为什么需要 Hystrix @@ -45,15 +44,15 @@ Hystrix 官方宣布**不再发布新版本**。 当一切正常时,整体系统如下所示: -![img](https://raw.githubusercontent.com/dunwu/images/dev/snap/20200717141615.png) +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200717141615.png) 在高并发场景,这些依赖的稳定性与否对系统的影响非常大,但是依赖有很多不可控问题:如网络连接、资源繁忙、服务宕机等。例如:下图中有一个 QPS 为 50 的依赖 I 出现不可用,但是其他依赖服务是可用的。 -![img](https://raw.githubusercontent.com/dunwu/images/dev/snap/20200717141749.png) +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200717141749.png) 但是,在高并发场景下,当依赖 I 阻塞时,大多数服务器的线程池就出现阻塞(BLOCK)。当这种级联故障愈演愈烈,就可能造成整个线上服务不可用的雪崩效应,如下图: -![img](https://raw.githubusercontent.com/dunwu/images/dev/snap/20200717141859.png) +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200717141859.png) Hystrix 就是为了解决这类问题而应运而生。 @@ -71,28 +70,22 @@ Hystrix 具有以下功能: 如果使用 Hystrix 对每个基础依赖服务进行过载保护,则整个系统架构将会类似下图所示,每个依赖项彼此隔离,受到延迟时发生饱和的资源的被限制访问,并包含 fallback 逻辑(用于降级处理),该逻辑决定了在依赖项中发生任何类型的故障时做出对应的处理。 -![img](https://raw.githubusercontent.com/dunwu/images/dev/snap/20200717142842.png) - -## Hystrix 核心概念 +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200717142842.png) -## 二、Hystrix 工作流程 +## Hystrix 原理 如下图所示,Hystrix 的工作流程大致可以分为 9 个步骤。 -![img](https://raw.githubusercontent.com/dunwu/images/dev/snap/20200717143247.png) - -### (一)包装命令 - -x 支持资源隔离。 +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200717143247.png) -资源隔离,就是说,你如果要把对某一个依赖服务的所有调用请求,全部隔离在同一份资源池内,不会去用其它资源了,这就叫资源隔离。哪怕对这个依赖服务,比如说商品服务,现在同时发起的调用量已经到了 1000,但是分配给商品服务线程池内就 10 个线程,最多就只会用这 10 个线程去执行。不会因为对商品服务调用的延迟,将 Tomcat 内部所有的线程资源全部耗尽。 +### (一)构建一个 HystrixCommand 或 HystrixObservableCommand 对象 -Hystrix 进行资源隔离,其实是提供了一个抽象,叫做命令模式。这也是 Hystrix 最最基本的资源隔离技术。 +Hystrix 进行资源隔离,其实是提供了一个抽象,叫做命令模式。这也是 Hystrix 最基本的资源隔离技术。 -在使用 Hystrix 的过程中,会对**依赖服务**的调用请求封装成**命令对象**,Hystrix 对 **命令对象**抽象了两个抽象类:`HystrixCommand` 和`HystrixObservableCommand` 。 +在使用 Hystrix 的过程中,会对依赖服务的调用请求封装成命令对象,Hystrix 对 命令对象抽象了两个抽象类:`HystrixCommand` 和 `HystrixObservableCommand` 。 -- `HystrixCommand` 表示的**命令对象**会返回一个唯一返回值。 -- `HystrixObservableCommand` 表示的**命令对象** 会返回多个返回值。 +- `HystrixCommand` 表示的命令对象会返回一个唯一返回值。 +- `HystrixObservableCommand` 表示的命令对象 会返回多个返回值。 ```java HystrixCommand command = new HystrixCommand(arg1, arg2); @@ -103,14 +96,14 @@ HystrixObservableCommand command = new HystrixObservableCommand(arg1, arg2); Hystrix 中共有 4 种方式执行命令,如下所示: -| 执行方式 | 说明 | 可用对象 | -| :------------- | :----------------------------------------------------------------------------------------------------------------------------- | :------------------------- | -| `execute()` | 阻塞式同步执行,返回依赖服务的单一返回结果(或者抛出异常) | `HystrixCommand` | -| `queue()` | 基于 Future 的异步方式执行,返回依赖服务的单一返回结果(或者抛出异常) | `HystrixCommand` | -| `observe()` | 基于 Rxjava 的 Observable 方式,返回通过 Observable 表示的依赖服务返回结果,代调用代码先执行(Hot Obserable) | `HystrixObservableCommand` | -| `toObvsevable` | 基于 Rxjava 的 Observable 方式,返回通过 Observable 表示的依赖服务返回结果,执行代码等到真正订阅的时候才会执行(cold observable) | `HystrixObservableCommand` | +| 执行方式 | 说明 | 可用对象 | +| :------------------------------------------------------------------------------------------------------------------------------ | :------------------------------------------------------------------------------------------------------------------------------ | :------------------------- | +| [`execute()`]() | 阻塞式同步执行,返回依赖服务的单一返回结果(或者抛出异常) | `HystrixCommand` | +| [`queue()`]() | 异步执行,通过 `Future` 返回依赖服务的单一返回结果(或者抛出异常) | `HystrixCommand` | +| [`observe()`]() | 基于 Rxjava 的 Observable 方式,返回通过 Observable 表示的依赖服务返回结果。代调用代码先执行(Hot Obserable) | `HystrixObservableCommand` | +| [`toObservable()`]() | 基于 Rxjava 的 Observable 方式,返回通过 Observable 表示的依赖服务返回结果。执行代码等到真正订阅的时候才会执行(cold observable) | `HystrixObservableCommand` | -这四种命令中,`exeucte()`、`queue()`、`observe()`的表示也是通过`toObservable()`实现的,其转换关系如下图所示: +这四种命令中,`exeucte()`、`queue()`、`observe()` 的表示其实是通过 `toObservable()` 实现的,其转换关系如下图所示: ![img](https:////upload-images.jianshu.io/upload_images/14126519-60964d9fa41614c1.png?imageMogr2/auto-orient/strip|imageView2/2/w/563/format/webp) @@ -137,16 +130,22 @@ Observable ocValue = command.toObservable(); //cold observable,延后订阅 ### (三)是否缓存 -如果当前命令对象配置了允许从`结果缓存`中取返回结果,并且在`结果缓存`中已经缓存了请求结果,则缓存的请求结果会立刻通过 `Observable` 的格式返回。 +如果当前命令对象启用了请求缓存,并且请求的响应存在于缓存中,则缓存的响应会立刻以 `Observable` 的形式返回。 ### (四)是否开启断路器 -如果第三步没有缓存没有命中,则判断一下当前断路器的断路状态是否打开。如果断路器状态为`打开`状态,则 `Hystrix` 将不会执行此 Command 命令,直接执行**步骤 8** 调用 Fallback; +如果第三步没有缓存没有命中,则判断一下当前断路器的断路状态是否打开。如果断路器状态为打开状态,则 Hystrix 将不会执行此 Command 命令,直接执行步骤 8 调用 Fallback; -如果断路器状态是`关闭`,则执行 **步骤 5** 检查是否有足够的资源运行 Command 命令 +如果断路器状态是关闭,则执行步骤 5 检查是否有足够的资源运行 Command 命令 ### (五)信号量、线程池是否拒绝 +当您执行该命令时,Hystrix 会检查断路器以查看电路是否打开。 + +如果电路开路(或“跳闸”),则 Hystrix 将不会执行该命令,而是将流程路由到 (8) 获取回退。 + +如果电路闭合,则流程前进至 (5) 以检查是否有可用容量来运行命令。 + 如果当前要执行的 Command 命令 先关连的线程池 和队列(或者信号量)资源已经满了,Hystrix 将不会运行 Command 命令,直接执行 **步骤 8**的 Fallback 降级处理;如果未满,表示有剩余的资源执行 Command 命令,则执行**步骤 6** ### (六)construct() 或 run() @@ -188,7 +187,7 @@ Hystrix 会统计 Command 命令执行执行过程中的**成功数**、**失败 - `watch()` —订阅 `Observable` 并开始执行命令的流程; 返回一个 `Observable`,当订阅该 `Observable` 时,它会重新通知。 - `toObservable()` —返回不变的 `Observable`; 必须订阅它才能真正开始执行命令的流程。 -## 三、断路器工作原理 +## 断路器工作原理 ![img](https:////upload-images.jianshu.io/upload_images/14126519-dce007513bf90794.png?imageMogr2/auto-orient/strip|imageView2/2/w/640/format/webp) @@ -239,7 +238,7 @@ Hystrix 对系统指标的统计是基于时间窗模式的: ![img](https:////upload-images.jianshu.io/upload_images/14126519-11710915e1a5dcda.png?imageMogr2/auto-orient/strip|imageView2/2/w/640/format/webp) -## 四、资源隔离技术 +## 资源隔离技术 ### 线程池隔离 @@ -380,9 +379,7 @@ semaphore > 弊:本质上基于信号量的隔离是同步行为,所以无法做到超时熔断,所以服务方自身要控制住执行时间,避免超时。 > 应用场景:**业务服务上,有并发上限限制时,可以考虑此方式** > `Alibaba Sentinel`开源框架,就是基于信号量的熔断和断路器框架。 -## 五、Hystrix 应用 - -### Spring Cloud + Hystrix +## Hystrix 应用 - **Hystrix 配置无法动态调节生效**。Hystrix 框架本身是使用的[Archaius](https://links.jianshu.com/go?to=https%3A%2F%2Fgithub.com%2FNetflix%2Farchaius)框架完成的配置加载和刷新,但是集成自 Spring Cloud 下,无法有效地根据实时监控结果,动态调整熔断和系统参数 - **线程池和 Command 之间的配置比较复杂**,在 Spring Cloud 在做 feigin-hystrix 集成的时候,还有些 BUG,对 command 的默认配置没有处理好,导致所有 command 占用公共的 command 线程池,没有细粒度控制,还需要做框架适配调整 @@ -467,7 +464,7 @@ public interface SetterFactory { | [`queueSizeRejectionThreshold`](https://github.com/Netflix/Hystrix/wiki/Configuration#queueSizeRejectionThreshold) | 队列大小阈值,超过则拒绝 | 5 | | [`allowMaximumSizeToDivergeFromCoreSize`](https://github.com/Netflix/Hystrix/wiki/Configuration#allowMaximumSizeToDivergeFromCoreSize) | 此属性允许 maximumSize 的配置生效。该值可以等于或大于 coreSize。设置 coreSize 数据库连接池负责分配、管理和释放数据库连接,它允许应用程序重复使用一个现有的数据库连接,而不是再重新建立一个;释放空闲时间超过最大空闲时间的数据库连接来避免因为没有释放数据库连接而引起的数据库连接遗漏。这项技术能明显提高对数据库操作的性能。——摘自百度百科 + +## 什么是数据库连接池 + +数据库连接是一种关键的有限的昂贵的资源,这一点在多用户的网页应用程序中体现得尤为突出。 一个数据库连接对象均对应一个物理数据库连接,每次操作都打开一个物理连接,使用完都关闭连接,这样造成系统的 性能低下。 数据库连接池的解决方案是在应用程序启动时建立足够的数据库连接,并讲这些连接组成一个连接池(简单说:在一个“池”里放了好多半成品的数据库联接对象),由应用程序动态地对池中的连接进行申请、使用和释放。对于多于连接池中连接数的并发请求,应该在请求队列中排队等待。并且应用程序可以根据池中连接的使用率,动态增加或减少池中的连接数。 连接池技术尽可能多地重用了消耗内存地资源,大大节省了内存,提高了服务器地服务效率,能够支持更多的客户服务。通过使用连接池,将大大提高程序运行效率,同时,我们可以通过其自身的管理机制来监视数据库连接的数量、使用情况等。 + +## 为什么需要数据库连接池 + +### 不使用数据库连接池 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/20220921231353.png) + +不使用数据库连接池的**步骤**: + +1. TCP 建立连接的三次握手 +2. MySQL 认证的三次握手 +3. 真正的 SQL 执行 +4. MySQL 的关闭 +5. TCP 的四次握手关闭 + +不使用数据库连接池的特性: + +- **优点**:实现简单 +- **缺点**: + - 网络 IO 较多 + - 数据库的负载较高 + - 响应时间较长及 QPS 较低 + - 应用频繁的创建连接和关闭连接,导致临时对象较多,GC 频繁 + - 在关闭连接后,会出现大量 TIME_WAIT 的 TCP 状态(在 2 个 MSL 之后关闭) + +### 使用数据库连接池 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/20220921231500.png) + +使用数据库连接池的步骤:只有第一次访问的时候,需要建立连接。 但是之后的访问,均会**复用**之前创建的连接,直接执行 SQL 语句。 + +使用数据库连接池的**优点**: + +- 减少了网络开销 +- 系统的性能会有一个实质的提升 +- 没有了 TIME_WAIT 状态 + +## 数据库连接池如何工作 + +数据库连接池工作的核心在于以下几点: + +1. **创建连接池**:与线程池等池化对象类似,数据库连接池会在进程启动之初,根据配置初始化,并在池中创建了几个连接对象,以便使用时能从连接池中获取。连接池中的连接不能随意创建和关闭,以避免创建、关闭所带来的系统开销。 +2. **使用、管理连接池中**:连接池管理策略是连接池机制的核心,连接池内连接的分配和释放对系统的性能有很大的影响。合理的策略可以保证数据库连接的有效复用,避免频繁的建立、释放连接所带来的系统资源开销。通常,数据库连接池的管理策略如下: + + 1. 当请求数据库连接时,首先查看连接池中是否有空闲连接。 + 2. 如果存在空闲连接,则将连接分配给客户使用。 + 3. 如果没有空闲连接,则查看当前所开的连接数是否已经达到最大连接数。若未达到,就重新创建一个连接,并分配给请求的客户;如果达到,就按设定的最大等待时间进行等待,若超出最大等待时间,则抛出异常给客户。 + 4. 当客户释放数据库连接时,先判断该连接的引用次数是否超过了规定值。如果超过,就从连接池中删除该连接;否则保留为其他客户服务。 + +3. **关闭连接池**:当应用程序退出时,关闭连接池中所有的连接,释放连接池相关的资源,该过程正好与创建相反。 + +## 数据库连接池的核心参数 + +使用数据库连接池,需要为其配置一些参数,以控制其工作。 + +通常,数据库连接池都会包含以下核心参数: + +- **最小连接数**:是连接池一直保持的数据库连接,所以如果应用程序对数据库连接的使用量不大,将会有大量的数据库连接资源被浪费. +- **最大连接数**:是连接池能申请的最大连接数,如果数据库连接请求超过次数,后面的数据库连接请求将被加入到等待队列中,这会影响以后的数据库操作 +- 最大空闲时间 +- 获取连接超时时间 +- 超时重试连接次数 + +## 数据库连接池的问题 + +**并发问题**:为了保证连接管理服务具有最大的通用性,必须考虑多线程环境,即并发问题。 + +**事务处理**:我们知道,事务具有原子性,此时要求对数据库的操作符合“ALL-OR-NOTHING”原则,即对于一组 SQL 语句要么全做,要么全不做。我们知道当 2 个线程共用一个连接 Connection 对象,而且各自都有自己的事务要处理时候,对于连接池是一个很头疼的问题,因为即使 Connection 类提供了相应的事务支持,可是我们仍然不能确定那个数据库操作是对应那个事务的,这是由于我们有2个线程都在进行事务操作而引起的。为此我们可以使用每一个事务独占一个连接来实现,虽然这种方法有点浪费连接池资源但是可以大大降低事务管理的复杂性。 + +**连接池的分配与释放**:连接池的分配与释放,对系统的性能有很大的影响。合理的分配与释放,可以提高连接的复用度,从而降低建立新连接的开销,同时还可以加快用户的访问速度。 对于连接的管理可使用一个 List。即把已经创建的连接都放入 List 中去统一管理。每当用户请求一个连接时,系统检查这个 List 中有没有可以分配的连接。如果有就把那个最合适的连接分配给他;如果没有就抛出一个异常给用户。 + +**连接池的配置与维护**:连接池中到底应该放置多少连接,才能使系统的性能最佳?系统可采取设置最小连接数(minConnection)和最大连接数(maxConnection)等参数来控制连接池中的连接。比方说,最小连接数是系统启动时连接池所创建的连接数。如果创建过多,则系统启动就慢,但创建后系统的响应速度会很快;如果创建过少,则系统启动的很快,响应起来却慢。这样,可以在开发时,设置较小的最小连接数,开发起来会快,而在系统实际使用时设置较大的,因为这样对访问客户来说速度会快些。最大连接数是连接池中允许连接的最大数目,具体设置多少,要看系统的访问量,可通过软件需求上得到。 如何确保连接池中的最小连接数呢?有动态和静态两种策略。动态即每隔一定时间就对连接池进行检测,如果发现连接数量小于最小连接数,则补充相应数量的新连接,以保证连接池的正常运转。静态是发现空闲连接不够时再去检查。 + +## 数据库连接池技术选型 + +常见的数据库连接池: + +- [HikariCP](https://github.com/brettwooldridge/HikariCP):HiKariCP 号称是跑的最快的连接池,并且是 SpringBoot 框架的默认连接池。 +- [Druid](https://github.com/apache/druid):Druid 是阿里巴巴开源的数据库连接池。Druid 内置强大的监控功能,监控特性不影响性能。功能强大,能防 SQL 注入,内置 Loging 能诊断 Hack 应用行为。 +- [DBCP](https://commons.apache.org/proper/commons-dbcp/): 由 Apache 开发的一个 Java 数据库连接池。`commons-dbcp2` 基于 `commons-pool2` 来实现底层的对象池机制。单线程,性能较差,适用于小型系统。官方自 2021 年后没有再更新。 +- [C3P0](https://github.com/swaldman/c3p0):开源的 JDBC 连接池,实现了数据源和 JNDI 绑定,支持 JDBC3 规范和 JDBC2 的标准扩展。单线程,性能较差,适用于小型系统。官方自 2019 年后再没有更新。 +- Tomcat-jdbc:Tomcat 在 7.0 以前使用 DBCP 做为连接池组件,从 7.0 后新增了 Tomcat jdbc pool 模块,基于 Tomcat JULI,使用 Tomcat 日志框架,完全兼容 dbcp,通过异步方式获取连接,支持高并发应用环境,超级简单核心文件只有 8 个,支持 JMX,支持 XA Connection。 + +来自 Druid 的竞品对比(https://github.com/alibaba/druid/wiki/Druid%E8%BF%9E%E6%8E%A5%E6%B1%A0%E4%BB%8B%E7%BB%8D): + +| 功能类别 | 功能 | Druid | HikariCP | DBCP | Tomcat-jdbc | C3P0 | +| ------------------- | --------------- | ------------ | ----------- | ---- | --------------- | ---- | +| 性能 | PSCache | 是 | 否 | 是 | 是 | 是 | +| LRU | 是 | 否 | 是 | 是 | 是 | | +| SLB 负载均衡支持 | 是 | 否 | 否 | 否 | 否 | | +| 稳定性 | ExceptionSorter | 是 | 否 | 否 | 否 | 否 | +| 扩展 | 扩展 | Filter | | | JdbcIntercepter | | +| 监控 | 监控方式 | jmx/log/http | jmx/metrics | jmx | jmx | jmx | +| 支持 SQL 级监控 | 是 | 否 | 否 | 否 | 否 | | +| Spring/Web 关联监控 | 是 | 否 | 否 | 否 | 否 | | +| | 诊断支持 | LogFilter | 否 | 否 | 否 | 否 | +| 连接泄露诊断 | logAbandoned | 否 | 否 | 否 | 否 | | +| 安全 | SQL 防注入 | 是 | 无 | 无 | 无 | 无 | +| 支持配置加密 | 是 | 否 | 否 | 否 | 否 | | + +从数据库连接池最重要的性能角度来看:HikariCP 应该性能最好;Druid 也不错,并且有更多、更久的生产实践,更为可靠;而其他常见的数据库连接池性能远远不如。 + +从功能角度来看:Druid 功能最全面,除基本的数据库连接池能力以外,还支持 sql 级监控、扩展、SQL 防注入以及监控等功能。 + +综合来看:HikariCP 是 Spring Boot 首选数据库连接池,对于 Spring Boot 项目来说,无疑适配性最好。而非 Spring Boot 项目,可以优先考虑 Druid,在国内有大规模应用,中文社区支持良好。 + +## HikariCP + +HiKariCP 号称是跑的最快的连接池,并且是 SpringBoot 框架的默认连接池。 + +HiKariCP 为了提升性能,做了很多细节上的优化,例如: + +- 使用 FastList 替代 ArrayList,通过初始化的默认值,减少了越界检查的操作 +- 优化并精简了字节码,通过使用 Javassist,减少了动态代理的性能损耗,比如使用 invokestatic 指令代替 invokevirtual 指令 +- 实现了无锁的 ConcurrentBag,减少了并发场景下的锁竞争 + +HikariCP 关键配置: + +- `maximum-pool-size`:池中最大连接数(包括空闲和正在使用的连接)。默认值是 10,这个一般预估应用的最大连接数,后期根据监测得到一个最大值的一个平均值。要知道,最大连接并不是越多越好,一个 connection 会占用系统的带宽和存储。但是 当连接池没有空闲连接并且已经到达最大值,新来的连接池请求(HikariPool#getConnection)会被阻塞直到`connectionTimeout`(毫秒),超时后便抛出 SQLException。 +- `minimum-idle`:池中最小空闲连接数量。默认值 10,小于池中最大连接数,一般根据系统大部分情况下的数据库连接情况取一个平均值。Hikari 会尽可能、尽快地将空闲连接数维持在这个数量上。如果为了获得最佳性能和对峰值需求的响应能力,我们也不妨让他和最大连接数保持一致,使得 HikariCP 成为一个固定大小的数据库连接池。 +- `connection-timeout`:连接超时时间。默认值为 30s,可以接收的最小超时时间为 250ms。但是连接池请求也可以自定义超时时间(com.zaxxer.hikari.pool.HikariPool#getConnection(long))。 +- `idle-timeout`:空闲连接存活最大时间,默认 600000(十分钟) +- `max-lifetime`:连接池中连接的最大生命周期。当连接一致处于闲置状态时,超过 8 小时数据库会主动断开连接。为了防止大量的同一时间处于空闲连接因为数据库方的闲置超时策略断开连接(可以理解为连接雪崩),一般将这个值设置的比数据库的“闲置超时时间”小几秒,以便这些连接断开后,HikariCP 能迅速的创建新一轮的连接。 +- `pool-name`:连接池的名字。一般会出现在日志和 JMX 控制台中。默认值:auto-genenrated。建议取一个合适的名字,便于监控。 +- `auto-commit`:是否自动提交池中返回的连接。默认值为 true。一般是有必要自动提交上一个连接中的事物的。如果为 false,那么就需要应用层手动提交事物。 + +参考配置: + +```properties +# 连接池名称 +spring.datasource.hikari.pool-name = SpringTutorialHikariPool +# 最大连接数,小于等于 0 会被重置为默认值 10;大于零小于 1 会被重置为 minimum-idle 的值 +spring.datasource.hikari.maximum-pool-size = 10 +# 最小空闲连接,默认值10,小于 0 或大于 maximum-pool-size,都会重置为 maximum-pool-size +spring.datasource.hikari.minimum-idle = 10 +# 连接超时时间(单位:毫秒),小于 250 毫秒,会被重置为默认值 30 秒 +spring.datasource.hikari.connection-timeout = 60000 +# 空闲连接超时时间,默认值 600000(10分钟),大于等于 max-lifetime 且 max-lifetime>0,会被重置为0;不等于 0 且小于 10 秒,会被重置为 10 秒 +# 只有空闲连接数大于最大连接数且空闲时间超过该值,才会被释放 +spring.datasource.hikari.idle-timeout = 600000 +# 连接最大存活时间,不等于 0 且小于 30 秒,会被重置为默认值 30 分钟。该值应该比数据库所设置的超时时间短 +spring.datasource.hikari.max-lifetime = 1800000 +``` + +## Druid + +Druid 是阿里巴巴开源的数据库连接池。Druid 连接池为监控而生,内置强大的监控功能,监控特性不影响性能。功能强大,能防 SQL 注入,内置 Loging 能诊断 Hack 应用行为。 + +Druid 关键配置: + +```properties +# 数据库访问配置 +# 主数据源,默认的 +spring.datasource.type=com.alibaba.druid.pool.DruidDataSource +spring.datasource.driver-class-name=com.mysql.jdbc.Driver +spring.datasource.url=jdbc:mysql://localhost:3306/druid +spring.datasource.username=root +spring.datasource.password=root + +# 下面为连接池的补充设置,应用到上面所有数据源中 +# 初始化大小,最小,最大 +spring.datasource.initialSize=5 +spring.datasource.minIdle=5 +spring.datasource.maxActive=20 +# 配置获取连接等待超时的时间 +spring.datasource.maxWait=60000 +# 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 +spring.datasource.timeBetweenEvictionRunsMillis=60000 +# 配置一个连接在池中最小生存的时间,单位是毫秒 +spring.datasource.minEvictableIdleTimeMillis=300000 +spring.datasource.validationQuery=SELECT 1 FROM DUAL +spring.datasource.testWhileIdle=true +spring.datasource.testOnBorrow=false +spring.datasource.testOnReturn=false +# 打开PSCache,并且指定每个连接上PSCache的大小 +spring.datasource.poolPreparedStatements=true +spring.datasource.maxPoolPreparedStatementPerConnectionSize=20 +# 配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙 +spring.datasource.filters=stat,wall,log4j +# 通过connectProperties属性来打开mergeSql功能;慢SQL记录 +spring.datasource.connectionProperties=druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000 +# 合并多个DruidDataSource的监控数据 +#spring.datasource.useGlobalDataSourceStat=true +``` + +## 参考资料 + +- [数据库连接池学习笔记(一):原理介绍+常用连接池介绍](https://blog.csdn.net/crankz/article/details/82874158) +- [高性能数据库连接池的内幕](https://mp.weixin.qq.com/s?__biz=MzI3MzEzMDI1OQ==&mid=2651814835&idx=1&sn=cb775d3926ce39d12fa420a292c1f83d&scene=0#wechat_redirect) +- [HikariCP](https://github.com/brettwooldridge/HikariCP) +- [druid](https://github.com/apache/druid) \ No newline at end of file diff --git "a/docs/14.\344\270\255\351\227\264\344\273\266/README.md" "b/docs/01.Java/14.\344\270\255\351\227\264\344\273\266/README.md" similarity index 59% rename from "docs/14.\344\270\255\351\227\264\344\273\266/README.md" rename to "docs/01.Java/14.\344\270\255\351\227\264\344\273\266/README.md" index 29563022..fd3311e0 100644 --- "a/docs/14.\344\270\255\351\227\264\344\273\266/README.md" +++ "b/docs/01.Java/14.\344\270\255\351\227\264\344\273\266/README.md" @@ -1,44 +1,29 @@ --- title: Java 中间件 -categories: - - 编程 +date: 2022-04-09 15:11:37 +categories: - Java - 中间件 -tags: +tags: - 编程 - Java - 中间件 -abbrlink: 8b30e720 -date: 2022-04-09 15:11:37 -permalink: /pages/3cbcff/ +permalink: /pages/fe6d83/ +hidden: true +index: false --- # Java 中间件 ## 📖 内容 -#### 消息队列 - -> 消息队列(Message Queue,简称 MQ)技术是分布式应用间交换信息的一种技术。 -> -> 消息队列主要解决应用耦合,异步消息,流量削锋等问题,实现高性能,高可用,可伸缩和最终一致性架构。是大型分布式系统不可缺少的中间件。 -> -> 如果想深入学习各种消息队列产品,建议先了解一下 [消息队列基本原理](https://dunwu.github.io/blog/pages/1fd240/) ,有助于理解消息队列特性的实现和设计思路。 - -- [消息队列面试](01.MQ/01.消息队列面试.md) -- [消息队列基本原理](01.MQ/02.消息队列基本原理.md) -- [RocketMQ](01.MQ/03.RocketMQ.md) -- [ActiveMQ](01.MQ/04.ActiveMQ.md) - #### 缓存 > 缓存可以说是优化系统性能的第一手段,在各种技术中都会有缓存的应用。 > > 如果想深入学习缓存,建议先了解一下 [缓存基本原理](https://dunwu.github.io/design/distributed/分布式缓存.html),有助于理解缓存的特性、原理,使用缓存常见的问题及解决方案。 -- [缓存面试题](02.缓存/01.缓存面试题.md) - [Java 缓存框架](02.缓存/02.Java缓存中间件.md) -- [Memcached 快速入门](02.缓存/03.Memcached.md) - [Ehcache 快速入门](02.缓存/04.Ehcache.md) - [Java 进程内缓存](02.缓存/05.Java进程内缓存.md) - [Http 缓存](02.缓存/06.Http缓存.md) @@ -61,4 +46,4 @@ permalink: /pages/3cbcff/ ## 🚪 传送 -◾ 🏠 [JAVA-TUTORIAL 首页](https://github.com/dunwu/java-tutorial) ◾ 🎯 [我的博客](https://github.com/dunwu/blog) ◾ +◾ 💧 [钝悟的 IT 知识图谱](https://dunwu.github.io/waterdrop/) ◾ \ No newline at end of file diff --git a/docs/01.Java/README.md b/docs/01.Java/README.md new file mode 100644 index 00000000..168f4e05 --- /dev/null +++ b/docs/01.Java/README.md @@ -0,0 +1,291 @@ +--- +title: Java 教程 +date: 2022-04-27 18:23:47 +categories: + - Java +tags: + - Java +permalink: /pages/0d2474/ +hidden: true +index: false +--- + +

+ + logo + +

+ +

+ + + star + + + + fork + + + + build + + + + code style + + +

+ +

JavaTutorial

+ +> ☕ **java-tutorial** 是一个 Java 教程,汇集一个老司机在 Java 领域的十年积累。 +> +> - 🔁 项目同步维护:[Github](https://github.com/dunwu/java-tutorial/) | [Gitee](https://gitee.com/turnon/java-tutorial/) +> - 📖 电子书阅读:[Github Pages](https://dunwu.github.io/java-tutorial/) | [Gitee Pages](https://turnon.gitee.io/java-tutorial/) +> +> 说明: +> +> - 下面的内容清单中,凡是有 📚 标记的技术,都已整理成详细的教程。 +> - 部分技术因为可以应用于不同领域,所以可能会同时出现在不同的类别下。 + +## 📖 内容 + +### JavaEE + +#### JavaWeb + +- [JavaWeb 面经](02.JavaEE/01.JavaWeb/99.JavaWeb面经.md) +- [JavaWeb 之 Servlet 指南](02.JavaEE/01.JavaWeb/01.JavaWeb之Servlet指南.md) +- [JavaWeb 之 Jsp 指南](02.JavaEE/01.JavaWeb/02.JavaWeb之Jsp指南.md) +- [JavaWeb 之 Filter 和 Listener](02.JavaEE/01.JavaWeb/03.JavaWeb之Filter和Listener.md) +- [JavaWeb 之 Cookie 和 Session](02.JavaEE/01.JavaWeb/04.JavaWeb之Cookie和Session.md) + +#### Java 服务器 + +> Tomcat 和 Jetty 都是 Java 比较流行的轻量级服务器。 +> +> Nginx 是目前最流行的反向代理服务器,也常用于负载均衡。 + +- [Tomcat 快速入门](02.JavaEE/02.服务器/01.Tomcat/01.Tomcat快速入门.md) +- [Tomcat 连接器](02.JavaEE/02.服务器/01.Tomcat/02.Tomcat连接器.md) +- [Tomcat 容器](02.JavaEE/02.服务器/01.Tomcat/03.Tomcat容器.md) +- [Tomcat 优化](02.JavaEE/02.服务器/01.Tomcat/04.Tomcat优化.md) +- [Tomcat 和 Jetty](02.JavaEE/02.服务器/01.Tomcat/05.Tomcat和Jetty.md) +- [Jetty](02.JavaEE/02.服务器/02.Jetty.md) + +### Java 软件 + +#### Java 构建 + +> Java 项目需要通过 [**构建工具**](11.软件/01.构建) 来管理项目依赖,完成编译、打包、发布、生成 JavaDoc 等任务。 +> +> - 目前最主流的构建工具是 Maven,它的功能非常强大。 +> - Gradle 号称是要替代 Maven 等构件工具,它的版本管理确实简洁,但是需要学习 Groovy,学习成本比 Maven 高。 +> - Ant 功能比 Maven 和 Gradle 要弱,现代 Java 项目基本不用了,但也有一些传统的 Java 项目还在使用。 + +- [Maven](11.软件/01.构建/01.Maven) 📚 + - [Maven 快速入门](11.软件/01.构建/01.Maven/01.Maven快速入门.md) + - [Maven 教程之 pom.xml 详解](11.软件/01.构建/01.Maven/02.Maven教程之pom.xml详解.md) + - [Maven 教程之 settings.xml 详解](11.软件/01.构建/01.Maven/03.Maven教程之settings.xml详解.md) + - [Maven 实战问题和最佳实践](11.软件/01.构建/01.Maven/04.Maven实战问题和最佳实践.md) + - [Maven 教程之发布 jar 到私服或中央仓库](11.软件/01.构建/01.Maven/05.Maven教程之发布jar到私服或中央仓库.md) + - [Maven 插件之代码检查](11.软件/01.构建/01.Maven/06.Maven插件之代码检查.md) +- [Ant 简易教程](11.软件/01.构建/02.Ant.md) + +#### Java IDE + +> 自从有了 [**IDE**](11.软件/02.IDE),写代码从此就告别了刀耕火种的蛮荒时代。 +> +> - [Eclipse](11.软件/02.IDE/02.Eclipse.md) 是久负盛名的开源 Java IDE,我的学生时代一直使用它写 Java。 +> - 曾经抗拒从转 [Intellij Idea](11.软件/02.IDE/01.Intellij.md) ,但后来发现真香,不得不说,确实是目前最优秀的 Java IDE。 +> - 你可以在 [vscode](11.软件/02.IDE/03.VsCode.md) 中写各种语言,只要安装相应插件即可。如果你的项目中使用了很多种编程语言,又懒得在多个 IDE 之间切换,那么就用 vscode 来一网打尽吧。 + +- [Intellij Idea](11.软件/02.IDE/01.Intellij.md) +- [Eclipse](11.软件/02.IDE/02.Eclipse.md) +- [vscode](11.软件/02.IDE/03.VsCode.md) + +#### Java 监控诊断 + +> [监控/诊断](11.软件/03.监控诊断) 工具主要用于 Java 应用的运维。通过采集、分析、存储、可视化应用的有效数据,帮助开发者、使用者快速定位问题,找到性能瓶颈。 + +- [监控工具对比](11.软件/03.监控诊断/01.监控工具对比.md) +- [CAT](11.软件/03.监控诊断/02.CAT.md) +- [Zipkin](11.软件/03.监控诊断/03.Zipkin.md) +- [SkyWalking](11.软件/03.监控诊断/04.Skywalking.md) +- [Arthas](11.软件/03.监控诊断/05.Arthas.md) + +### Java 工具 + +#### Java IO + +- [JSON 序列化](12.工具/01.IO/01.JSON序列化.md) - [fastjson](https://github.com/alibaba/fastjson)、[Jackson](https://github.com/FasterXML/jackson)、[Gson](https://github.com/google/gson) +- [二进制序列化](12.工具/01.IO/02.二进制序列化.md) - [Protobuf](https://developers.google.com/protocol-buffers)、[Thrift](https://thrift.apache.org/)、[Hessian](http://hessian.caucho.com/)、[Kryo](https://github.com/EsotericSoftware/kryo)、[FST](https://github.com/RuedigerMoeller/fast-serialization) + +#### JavaBean 工具 + +- [Lombok](12.工具/02.JavaBean/01.Lombok.md) +- [Dozer](12.工具/02.JavaBean/02.Dozer.md) + +#### Java 模板引擎 + +- [Freemark](12.工具/03.模板引擎/01.Freemark.md) +- [Velocity](12.工具/03.模板引擎/02.Thymeleaf.md) +- [Thymeleaf](12.工具/03.模板引擎/03.Velocity.md) + +#### Java 测试工具 + +- [Junit](12.工具/04.测试/01.Junit.md) +- [Mockito](12.工具/04.测试/02.Mockito.md) +- [Jmeter](12.工具/04.测试/03.Jmeter.md) +- [JMH](12.工具/04.测试/04.JMH.md) + +#### 其他 + +- [Java 日志](12.工具/99.其他/01.Java日志.md) +- [Java 工具包](12.工具/99.其他/02.Java工具包.md) +- [Reflections](12.工具/99.其他/03.Reflections.md) +- [JavaMail](12.工具/99.其他/04.JavaMail.md) +- [Jsoup](12.工具/99.其他/05.Jsoup.md) +- [Thumbnailator](12.工具/99.其他/06.Thumbnailator.md) +- [Zxing](12.工具/99.其他/07.Zxing.md) + +### Java 框架 + +#### Spring + +##### 综合 + +- [Spring 概述](13.框架/01.Spring/00.Spring综合/01.Spring概述.md) +- [SpringBoot 知识图谱](13.框架/01.Spring/00.Spring综合/21.SpringBoot知识图谱.md) +- [SpringBoot 基本原理](13.框架/01.Spring/00.Spring综合/22.SpringBoot基本原理.md) +- [Spring 面试](13.框架/01.Spring/00.Spring综合/99.Spring面试.md) + +##### 核心 + +- [Spring Bean](13.框架/01.Spring/01.Spring核心/01.SpringBean.md) +- [Spring IoC](13.框架/01.Spring/01.Spring核心/02.SpringIoC.md) +- [Spring 依赖查找](13.框架/01.Spring/01.Spring核心/03.Spring依赖查找.md) +- [Spring 依赖注入](13.框架/01.Spring/01.Spring核心/04.Spring依赖注入.md) +- [Spring IoC 依赖来源](13.框架/01.Spring/01.Spring核心/05.SpringIoC依赖来源.md) +- [Spring Bean 作用域](13.框架/01.Spring/01.Spring核心/06.SpringBean作用域.md) +- [Spring Bean 生命周期](13.框架/01.Spring/01.Spring核心/07.SpringBean生命周期.md) +- [Spring 配置元数据](13.框架/01.Spring/01.Spring核心/08.Spring配置元数据.md) +- [Spring AOP](13.框架/01.Spring/01.Spring核心/10.SpringAop.md) +- [Spring 资源管理](13.框架/01.Spring/01.Spring核心/20.Spring资源管理.md) +- [Spring 校验](13.框架/01.Spring/01.Spring核心/21.Spring校验.md) +- [Spring 数据绑定](13.框架/01.Spring/01.Spring核心/22.Spring数据绑定.md) +- [Spring 类型转换](13.框架/01.Spring/01.Spring核心/23.Spring类型转换.md) +- [Spring EL 表达式](13.框架/01.Spring/01.Spring核心/24.SpringEL.md) +- [Spring 事件](13.框架/01.Spring/01.Spring核心/25.Spring事件.md) +- [Spring 国际化](13.框架/01.Spring/01.Spring核心/26.Spring国际化.md) +- [Spring 泛型处理](13.框架/01.Spring/01.Spring核心/27.Spring泛型处理.md) +- [Spring 注解](13.框架/01.Spring/01.Spring核心/28.Spring注解.md) +- [Spring Environment 抽象](13.框架/01.Spring/01.Spring核心/29.SpringEnvironment抽象.md) +- [SpringBoot 教程之快速入门](13.框架/01.Spring/01.Spring核心/31.SpringBoot之快速入门.md) +- [SpringBoot 之属性加载](13.框架/01.Spring/01.Spring核心/32.SpringBoot之属性加载.md) +- [SpringBoot 之 Profile](13.框架/01.Spring/01.Spring核心/33.SpringBoot之Profile.md) + +##### 数据 + +- [Spring 之数据源](13.框架/01.Spring/02.Spring数据/01.Spring之数据源.md) +- [Spring 之 JDBC](13.框架/01.Spring/02.Spring数据/02.Spring之JDBC.md) +- [Spring 之事务](13.框架/01.Spring/02.Spring数据/03.Spring之事务.md) +- [Spring 之 JPA](13.框架/01.Spring/02.Spring数据/04.Spring之JPA.md) +- [Spring 集成 Mybatis](13.框架/01.Spring/02.Spring数据/10.Spring集成Mybatis.md) +- [Spring 访问 Redis](13.框架/01.Spring/02.Spring数据/21.Spring访问Redis.md) +- [Spring 访问 MongoDB](13.框架/01.Spring/02.Spring数据/22.Spring访问MongoDB.md) +- [Spring 访问 Elasticsearch](13.框架/01.Spring/02.Spring数据/23.Spring访问Elasticsearch.md) + +##### Web + +- [Spring WebMvc](13.框架/01.Spring/03.SpringWeb/01.SpringWebMvc.md) +- [SpringBoot 之应用 EasyUI](13.框架/01.Spring/03.SpringWeb/21.SpringBoot之应用EasyUI.md) + +##### IO + +- [SpringBoot 之异步请求](13.框架/01.Spring/04.SpringIO/01.SpringBoot之异步请求.md) +- [SpringBoot 之 Json](13.框架/01.Spring/04.SpringIO/02.SpringBoot之Json.md) +- [SpringBoot 之邮件](13.框架/01.Spring/04.SpringIO/03.SpringBoot之邮件.md) + +##### 集成 + +- [Spring 集成缓存中间件](13.框架/01.Spring/05.Spring集成/01.Spring集成缓存.md) +- [Spring 集成定时任务中间件](13.框架/01.Spring/05.Spring集成/02.Spring集成调度器.md) +- [Spring 集成 Dubbo](13.框架/01.Spring/05.Spring集成/03.Spring集成Dubbo.md) + +##### 其他 + +- [Spring4 升级](13.框架/01.Spring/99.Spring其他/01.Spring4升级.md) +- [SpringBoot 之 banner](13.框架/01.Spring/99.Spring其他/21.SpringBoot之banner.md) +- [SpringBoot 之 Actuator](13.框架/01.Spring/99.Spring其他/22.SpringBoot之Actuator.md) + +#### ORM + +- [Mybatis 快速入门](13.框架/11.ORM/01.Mybatis快速入门.md) +- [Mybatis 原理](13.框架/11.ORM/02.Mybatis原理.md) + +#### 安全 + +> Java 领域比较流行的安全框架就是 shiro 和 spring-security。 +> +> shiro 更为简单、轻便,容易理解,能满足大多数基本安全场景下的需要。 +> +> spring-security 功能更丰富,也比 shiro 更复杂。值得一提的是由于 spring-security 是 spring 团队开发,所以集成 spring 和 spring-boot 框架更容易。 + +- [Shiro](13.框架/12.安全/01.Shiro.md) +- [SpringSecurity](13.框架/12.安全/02.SpringSecurity.md) + +#### IO + +- [Shiro](13.框架/13.IO/01.Netty.md) + +### Java 中间件 + +#### 缓存 + +> 缓存可以说是优化系统性能的第一手段,在各种技术中都会有缓存的应用。 +> +> 如果想深入学习缓存,建议先了解一下 [缓存基本原理](https://dunwu.github.io/design/distributed/分布式缓存.html),有助于理解缓存的特性、原理,使用缓存常见的问题及解决方案。 + +- [Java 缓存中间件](14.中间件/02.缓存/02.Java缓存中间件.md) +- [Ehcache 快速入门](14.中间件/02.缓存/04.Ehcache.md) +- [Java 进程内缓存](14.中间件/02.缓存/05.Java进程内缓存.md) +- [Http 缓存](14.中间件/02.缓存/06.Http缓存.md) + +#### 流量控制 + +- [Hystrix](14.中间件/03.流量控制/01.Hystrix.md) + +### [大数据](https://dunwu.github.io/bigdata-tutorial) + +> 大数据技术点以归档在:[bigdata-tutorial](https://dunwu.github.io/bigdata-tutorial) + +- [Hdfs](https://dunwu.github.io/bigdata-tutorial/hdfs) 📚 +- [Hbase](https://dunwu.github.io/bigdata-tutorial/hbase) 📚 +- [Hive](https://dunwu.github.io/bigdata-tutorial/hive) 📚 +- [MapReduce](https://dunwu.github.io/bigdata-tutorial/mapreduce) +- [Yarn](https://dunwu.github.io/bigdata-tutorial/yarn) +- [ZooKeeper](https://dunwu.github.io/bigdata-tutorial/zookeeper) 📚 +- [Kafka](https://dunwu.github.io/bigdata-tutorial/kafka) 📚 +- Spark +- Storm +- [Flink](https://dunwu.github.io/bigdata-tutorial/tree/master/docs/flink) + +## 📚 资料 + +- Java 经典书籍 + - [《Effective Java 中文版》](https://item.jd.com/12507084.html) - 本书介绍了在 Java 编程中 78 条极具实用价值的经验规则,这些经验规则涵盖了大多数开发人员每天所面临的问题的解决方案。同推荐《重构 : 改善既有代码的设计》、《代码整洁之道》、《代码大全》,有一定的内容重叠。 + - [《Java 并发编程实战》](https://item.jd.com/10922250.html) - 本书深入浅出地介绍了 Java 线程和并发,是一本完美的 Java 并发参考手册。 + - [《深入理解 Java 虚拟机》](https://item.jd.com/11252778.html) - 不去了解 JVM 的工程师,和咸鱼有什么区 + - [《Maven 实战》](https://item.jd.com/10476794.html) - 国内最权威的 Maven 专家的力作,唯一一本哦! +- 其他领域书籍 + - [《Redis 设计与实现》](https://item.jd.com/11486101.html) - 系统而全面地描述了 Redis 内部运行机制。图示丰富,描述清晰,并给出大量参考信息,是 NoSQL 数据库开发人员案头必备。 + - [《鸟哥的 Linux 私房菜 (基础学习篇)》](https://item.jd.com/12443890.html) - 本书是最具知名度的 Linux 入门书《鸟哥的 Linux 私房菜基础学习篇》的最新版,全面而详细地介绍了 Linux 操作系统。内容非常全面,建议挑选和自己实际工作相关度较高的,其他部分有需要再阅读。 + - [《Head First 设计模式》](https://item.jd.com/10100236.html) - 《Head First 设计模式》(中文版)共有 14 章,每章都介绍了几个设计模式,完整地涵盖了四人组版本全部 23 个设计模式。 + - [《HTTP 权威指南》](https://item.jd.com/11056556.html) - 本书尝试着将 HTTP 中一些互相关联且常被误解的规则梳理清楚,并编写了一系列基于各种主题的章节,对 HTTP 各方面的特性进行了介绍。纵观全书,对 HTTP“为什么”这样做进行了详细的解释,而不仅仅停留在它是“怎么做”的。 + - [《TCP/IP 详解 系列》](https://item.jd.com/11966296.html) - 完整而详细的 TCP/IP 协议指南。针对任何希望理解 TCP/IP 协议是如何实现的读者设计。 + - [《剑指 Offer:名企面试官精讲典型编程题》](https://item.jd.com/12163054.html) - 剖析了 80 个典型的编程面试题,系统整理基础知识、代码质量、解题思路、优化效率和综合能力这 5 个面试要点。 + +## 🚪 传送 + +◾ 💧 [钝悟的 IT 知识图谱](https://dunwu.github.io/waterdrop/) ◾ diff --git "a/docs/13.\346\241\206\346\236\266/14.\345\276\256\346\234\215\345\212\241/01.Dubbo.md" "b/docs/13.\346\241\206\346\236\266/14.\345\276\256\346\234\215\345\212\241/01.Dubbo.md" deleted file mode 100644 index ec3281ff..00000000 --- "a/docs/13.\346\241\206\346\236\266/14.\345\276\256\346\234\215\345\212\241/01.Dubbo.md" +++ /dev/null @@ -1,891 +0,0 @@ ---- -title: Dubbo 快速入门 -categories: - - 编程 - - Java - - 框架 - - 微服务 -tags: - - Java - - 框架 - - 微服务 - - Dubbo -abbrlink: 37e41c9c -date: 2022-02-17 22:34:30 -permalink: /pages/e79b77/ ---- - -# Dubbo 快速入门 - -> [Apache Dubbo](https://dubbo.apache.org/zh-cn/) 是一款高性能、轻量级的开源 Java RPC 框架,它提供了三大核心能力:面向接口的远程方法调用,智能容错和负载均衡,以及服务自动注册和发现。 - -## 一、Dubbo 简介 - -[Apache Dubbo](https://dubbo.apache.org/zh-cn/) 是一款高性能、轻量级的开源 Java RPC 框架。 - -Dubbo 提供了三大核心能力: - -- 面向接口的远程方法调用 -- 智能容错和负载均衡 -- 服务自动注册和发现 - -### RPC 原理简介 - -#### 什么是 RPC - -RPC(Remote Procedure Call),即远程过程调用,它是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议。比如两个不同的服务 A、B 部署在两台不同的机器上,那么服务 A 如果想要调用服务 B 中的某个方法该怎么办呢?使用 HTTP 请求 当然可以,但是可能会比较慢而且一些优化做的并不好。 RPC 的出现就是为了解决这个问题。 - -#### RPC 工作流程 - -![img](https://raw.githubusercontent.com/dunwu/images/dev/snap/20200305121252.jpg) - -1. 服务消费方(client)调用以本地调用方式调用服务; -2. client stub 接收到调用后负责将方法、参数等组装成能够进行网络传输的消息体; -3. client stub 找到服务地址,并将消息发送到服务端; -4. server stub 收到消息后进行解码; -5. server stub 根据解码结果调用本地的服务; -6. 本地服务执行并将结果返回给 server stub; -7. server stub 将返回结果打包成消息并发送至消费方; -8. client stub 接收到消息,并进行解码; -9. 服务消费方得到最终结果。 - -### 为什么需要 Dubbo - -**如果你要开发分布式程序,你也可以直接基于 HTTP 接口进行通信,但是为什么要用 Dubbo 呢?** - -我觉得主要可以从 Dubbo 提供的下面四点特性来说为什么要用 Dubbo: - -1. **负载均衡**——同一个服务部署在不同的机器时该调用那一台机器上的服务。 -2. **服务调用链路**——随着系统的发展,服务越来越多,服务间依赖关系变得错踪复杂,甚至分不清哪个应用要在哪个应用之前启动,架构师都不能完整的描述应用的架构关系。Dubbo 可以为我们解决服务之间互相是如何调用的。 -3. **服务访问压力以及时长统计、资源调度和治理**——基于访问压力实时管理集群容量,提高集群利用率。 -4. **服务治理**——某个服务挂掉之后调用备用服务。 - -另外,Dubbo 除了能够应用在分布式系统中,也可以应用在现在比较火的微服务系统中。不过,由于 Spring Cloud 在微服务中应用更加广泛,所以,我觉得一般我们提 Dubbo 的话,大部分是分布式系统的情况。 - -## 二、QuickStart - -(1)添加 maven 依赖 - -```xml - - com.alibaba - dubbo - ${dubbo.version} - -``` - -(2)定义 Provider - -```java -package com.alibaba.dubbo.demo; - -public interface DemoService { - String sayHello(String name); -} -``` - -(3)实现 Provider - -```java -package com.alibaba.dubbo.demo.provider; -import com.alibaba.dubbo.demo.DemoService; - -public class DemoServiceImpl implements DemoService { - public String sayHello(String name) { - return "Hello " + name; - } -} -``` - -(4)配置 Provider - -```xml - - - - - - - - -``` - -(5)启动 Provider - -```java -import org.springframework.context.support.ClassPathXmlApplicationContext; - -public class Provider { - public static void main(String[] args) throws Exception { - ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext( - new String[] {"META-INF/spring/dubbo-demo-provider.xml"}); - context.start(); - // press any key to exit - System.in.read(); - } -} -``` - -(6)配置 Consumer - -```xml - - - - - - -``` - -(7)启动 Consumer - -```java -import com.alibaba.dubbo.demo.DemoService; -import org.springframework.context.support.ClassPathXmlApplicationContext; - -public class Consumer { - public static void main(String[] args) throws Exception { - ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext( - new String[]{"META-INF/spring/dubbo-demo-consumer.xml"}); - context.start(); - // obtain proxy object for remote invocation - DemoService demoService = (DemoService) context.getBean("demoService"); - // execute remote invocation - String hello = demoService.sayHello("world"); - // show the result - System.out.println(hello); - } -} -``` - -## 三、Dubbo 配置 - -Dubbo 所有配置最终都将转换为 URL 表示,并由服务提供方生成,经注册中心传递给消费方,各属性对应 URL 的参数,参见配置项一览表中的 "对应 URL 参数" 列。 - -只有 group,interface,version 是服务的匹配条件,三者决定是不是同一个服务,其它配置项均为调优和治理参数。 - -URL 格式:`protocol://username:password@host:port/path?key=value&key=value` - -### 配置方式 - -Dubbo 支持多种配置方式: - -- xml 配置 -- properties 配置 -- API 配置 -- 注解配置 - -如果同时存在多种配置方式,遵循以下覆盖策略: - -- JVM 启动 -D 参数优先,这样可以使用户在部署和启动时进行参数重写,比如在启动时需改变协议的端口。 -- XML 次之,如果在 XML 中有配置,则 dubbo.properties 中的相应配置项无效。 -- Properties 最后,相当于缺省值,只有 XML 没有配置时,dubbo.properties 的相应配置项才会生效,通常用于共享公共配置,比如应用名。 - -
- -
- -#### xml 配置 - -示例: - -```xml - - - - - - - - - - - - - - - - -``` - -Dubbo 会把以上配置项解析成下面的 URL 格式: - -``` -dubbo://host-ip:20880/com.alibaba.dubbo.demo.DemoService -``` - -然后基于[扩展点自适应机制](http://dubbo.incubator.apache.org/zh-cn/docs/dev/SPI.html),通过 URL 的 `dubbo://` 协议头识别,就会调用 DubboProtocol 的 export() 方法,打开服务端口 20880,就可以把服务 demoService 暴露到 20880 端口了。 - -#### properties 配置 - -示例: - -```properties -dubbo.application.name=foo -dubbo.application.owner=bar -dubbo.registry.address=10.20.153.10:9090 -``` - -### 配置项 - -所有配置项分为三大类: - -- 服务发现:表示该配置项用于服务的注册与发现,目的是让消费方找到提供方。 -- 服务治理:表示该配置项用于治理服务间的关系,或为开发测试提供便利条件。 -- 性能调优:表示该配置项用于调优性能,不同的选项对性能会产生影响。 - -配置项清单: - -| 标签 | 用途 | 解释 | -| ------------------- | ------------ | ------------------------------------------------------------------------------------------------ | -| `dubbo:service` | 服务配置 | 用于暴露一个服务,定义服务的元信息,一个服务可以用多个协议暴露,一个服务也可以注册到多个注册中心 | -| `dubbo:reference` | 引用配置 | 用于创建一个远程服务代理,一个引用可以指向多个注册中心 | -| `dubbo:protocol` | 协议配置 | 用于配置提供服务的协议信息,协议由提供方指定,消费方被动接受 | -| `dubbo:application` | 应用配置 | 用于配置当前应用信息,不管该应用是提供者还是消费者 | -| `dubbo:module` | 模块配置 | 用于配置当前模块信息,可选 | -| `dubbo:registry` | 注册中心配置 | 用于配置连接注册中心相关信息 | -| `dubbo:monitor` | 监控中心配置 | 用于配置连接监控中心相关信息,可选 | -| `dubbo:provider` | 提供方配置 | 当 ProtocolConfig 和 ServiceConfig 某属性没有配置时,采用此缺省值,可选 | -| `dubbo:consumer` | `消费方配置` | `当 ReferenceConfig 某属性没有配置时,采用此缺省值,可选` | -| `dubbo:method` | 方法配置 | 用于 ServiceConfig 和 ReferenceConfig 指定方法级的配置信息 | -| `dubbo:argument` | 参数配置 | 用于指定方法参数配置 | - -> 详细配置说明请参考:[官方配置](http://dubbo.apache.org/books/dubbo-user-book/references/xml/introduction.html) - -#### 配置之间的关系 - -
- -
- -#### 配置覆盖关系 - -以 timeout 为例,显示了配置的查找顺序,其它 retries, loadbalance, actives 等类似: - -- **方法级优先,接口级次之,全局配置再次之**。 -- **如果级别一样,则消费方优先,提供方次之**。 - -其中,服务提供方配置,通过 URL 经由注册中心传递给消费方。 - -
- -
-### 动态配置中心 - -配置中心(v2.7.0)在 Dubbo 中承担两个职责: - -1. 外部化配置。启动配置的集中式存储 (简单理解为 dubbo.properties 的外部化存储)。 -2. 服务治理。服务治理规则的存储与通知。 - -启用动态配置: - -```xml - -``` - -或者 - -```properties -dubbo.config-center.address=zookeeper://127.0.0.1:2181 -``` - -或者 - -```java -ConfigCenterConfig configCenter = new ConfigCenterConfig(); -configCenter.setAddress("zookeeper://127.0.0.1:2181"); -``` - -## 四、Dubbo 架构 - -### Dubbo 核心组件 - -
- -
- -节点角色: - -| 节点 | 角色说明 | -| --------- | -------------------------------------- | -| Provider | 暴露服务的服务提供方 | -| Consumer | 调用远程服务的服务消费方 | -| Registry | 服务注册与发现的注册中心 | -| Monitor | 统计服务的调用次数和调用时间的监控中心 | -| Container | 服务运行容器 | - -调用关系: - -1. 服务容器负责启动,加载,运行服务提供者。 -2. 服务提供者在启动时,向注册中心注册自己提供的服务。 -3. 服务消费者在启动时,向注册中心订阅自己所需的服务。 -4. 注册中心返回服务提供者地址列表给消费者,如果有变更,注册中心将基于长连接推送变更数据给消费者。 -5. 服务消费者,从提供者地址列表中,基于软负载均衡算法,选一台提供者进行调用,如果调用失败,再选另一台调用。 -6. 服务消费者和提供者,在内存中累计调用次数和调用时间,定时每分钟发送一次统计数据到监控中心。 - -**重要知识点总结:** - -- **注册中心负责服务地址的注册与查找,相当于目录服务,服务提供者和消费者只在启动时与注册中心交互,注册中心不转发请求,压力较小** -- **监控中心负责统计各服务调用次数,调用时间等,统计先在内存汇总后每分钟一次发送到监控中心服务器,并以报表展示** -- **注册中心,服务提供者,服务消费者三者之间均为长连接,监控中心除外** -- **注册中心通过长连接感知服务提供者的存在,服务提供者宕机,注册中心将立即推送事件通知消费者** -- **注册中心和监控中心全部宕机,不影响已运行的提供者和消费者,消费者在本地缓存了提供者列表** -- **注册中心和监控中心都是可选的,服务消费者可以直连服务提供者** -- **服务提供者无状态,任意一台宕掉后,不影响使用** -- **服务提供者全部宕掉后,服务消费者应用将无法使用,并无限次重连等待服务提供者恢复** - -> 问:注册中心挂了可以继续通信吗? -> -> 答:可以,因为刚开始初始化的时候,消费者会将提供者的地址等信息**拉取到本地缓存**,所以注册中心挂了可以继续通信。 - -### Dubbo 架构层次 - -
- -
- -图例说明: - -- 图中左边淡蓝背景的为服务消费方使用的接口,右边淡绿色背景的为服务提供方使用的接口,位于中轴线上的为双方都用到的接口。 -- 图中从下至上分为十层,各层均为单向依赖,右边的黑色箭头代表层之间的依赖关系,每一层都可以剥离上层被复用,其中,Service 和 Config 层为 API,其它各层均为 SPI。 -- 图中绿色小块的为扩展接口,蓝色小块为实现类,图中只显示用于关联各层的实现类。 -- 图中蓝色虚线为初始化过程,即启动时组装链,红色实线为方法调用过程,即运行时调时链,紫色三角箭头为继承,可以把子类看作父类的同一个节点,线上的文字为调用的方法。 - -#### 各层说明 - -- **config 配置层**:对外配置接口,以 ServiceConfig, ReferenceConfig 为中心,可以直接初始化配置类,也可以通过 spring 解析配置生成配置类 -- **proxy 服务代理层**:服务接口透明代理,生成服务的客户端 Stub 和服务器端 Skeleton, 以 ServiceProxy 为中心,扩展接口为 ProxyFactory -- **registry 注册中心层**:封装服务地址的注册与发现,以服务 URL 为中心,扩展接口为 RegistryFactory, Registry, RegistryService -- **cluster 路由层**:封装多个提供者的路由及负载均衡,并桥接注册中心,以 Invoker 为中心,扩展接口为 Cluster, Directory, Router, LoadBalance -- **monitor 监控层**:RPC 调用次数和调用时间监控,以 Statistics 为中心,扩展接口为 MonitorFactory, Monitor, MonitorService -- **protocol 远程调用层**:封装 RPC 调用,以 Invocation, Result 为中心,扩展接口为 Protocol, Invoker, Exporter -- **exchange 信息交换层**:封装请求响应模式,同步转异步,以 Request, Response 为中心,扩展接口为 Exchanger, ExchangeChannel, ExchangeClient, ExchangeServer -- **transport 网络传输层**:抽象 mina 和 netty 为统一接口,以 Message 为中心,扩展接口为 Channel, Transporter, Client, Server, Codec -- **serialize 数据序列化层**:可复用的一些工具,扩展接口为 Serialization, ObjectInput, ObjectOutput, ThreadPool -- **serialize 数据序列化层**:可复用的一些工具,扩展接口为 Serialization, ObjectInput, ObjectOutput, ThreadPool - -#### 各层关系说明 - -- 在 RPC 中,Protocol 是核心层,也就是只要有 Protocol + Invoker + Exporter 就可以完成非透明的 RPC 调用,然后在 Invoker 的主过程上 Filter 拦截点。 -- 图中的 Consumer 和 Provider 是抽象概念,只是想让看图者更直观的了解哪些类分属于客户端与服务器端,不用 Client 和 Server 的原因是 Dubbo 在很多场景下都使用 Provider, Consumer, Registry, Monitor 划分逻辑拓普节点,保持统一概念。 -- 而 Cluster 是外围概念,所以 Cluster 的目的是将多个 Invoker 伪装成一个 Invoker,这样其它人只要关注 Protocol 层 Invoker 即可,加上 Cluster 或者去掉 Cluster 对其它层都不会造成影响,因为只有一个提供者时,是不需要 Cluster 的。 -- Proxy 层封装了所有接口的透明化代理,而在其它层都以 Invoker 为中心,只有到了暴露给用户使用时,才用 Proxy 将 Invoker 转成接口,或将接口实现转成 Invoker,也就是去掉 Proxy 层 RPC 是可以 Run 的,只是不那么透明,不那么看起来像调本地服务一样调远程服务。 -- 而 Remoting 实现是 Dubbo 协议的实现,如果你选择 RMI 协议,整个 Remoting 都不会用上,Remoting 内部再划为 Transport 传输层和 Exchange 信息交换层,Transport 层只负责单向消息传输,是对 Mina, Netty, Grizzly 的抽象,它也可以扩展 UDP 传输,而 Exchange 层是在传输层之上封装了 Request-Response 语义。 -- Registry 和 Monitor 实际上不算一层,而是一个独立的节点,只是为了全局概览,用层的方式画在一起。 - -## 五、服务发现 - -服务提供者注册服务的过程: - -Dubbo 配置项 `dubbo://registry` 声明了注册中心的地址,Dubbo 会把以上配置项解析成类似下面的 URL 格式: - -``` -registry://multicast://224.5.6.7:1234/com.alibaba.dubbo.registry.RegistryService?export=URL.encode("dubbo://host-ip:20880/com.alibaba.dubbo.demo.DemoService") -``` - -然后基于扩展点自适应机制,通过 URL 的“registry://”协议头识别,就会调用 RegistryProtocol 的 export() 方法,将 export 参数中的提供者 URL,注册到注册中心。 - -服务消费者发现服务的过程: - -Dubbo 配置项 `dubbo://registry` 声明了注册中心的地址,跟服务注册的原理类似,Dubbo 也会把以上配置项解析成下面的 URL 格式: - -``` -registry://multicast://224.5.6.7:1234/com.alibaba.dubbo.registry.RegistryService?refer=URL.encode("consummer://host-ip/com.alibaba.dubbo.demo.DemoService") -``` - -然后基于扩展点自适应机制,通过 URL 的“registry://”协议头识别,就会调用 RegistryProtocol 的 refer() 方法,基于 refer 参数中的条件,查询服务 demoService 的地址。 - -### 启动时检查 - -Dubbo 缺省会在启动时检查依赖的服务是否可用,不可用时会抛出异常,阻止 Spring 初始化完成,以便上线时,能及早发现问题,默认 `check="true"`。 - -可以通过 xml、properties、-D 参数三种方式设置。启动时检查 - -## 六、Dubbo 协议 - -Dubbo 支持多种通信协议,不同的协议针对不同的序列化方式。 - -### dubbo 协议 - -[dubbo](http://dubbo.apache.org/zh-cn/docs/user/references/protocol/dubbo.html) 协议是 Dubbo 的默认通信协议,采用单一长连接和 NIO 异步通信,基于 hessian 作为序列化协议。 - -[dubbo](http://dubbo.apache.org/zh-cn/docs/user/references/protocol/dubbo.html) 协议适合于小数据量大并发的服务调用,以及服务消费者机器数远大于服务提供者机器数的情况。反之,Dubbo 缺省协议不适合传送大数据量的服务,比如传文件,传视频等,除非请求量很低。 - -为了要支持高并发场景,一般是服务提供者就几台机器,但是服务消费者有上百台,可能每天调用量达到上亿次!此时用长连接是最合适的,就是跟每个服务消费者维持一个长连接就可以,可能总共就 100 个连接。然后后面直接基于长连接 NIO 异步通信,可以支撑高并发请求。 - -### rmi 协议 - -[rmi](http://dubbo.apache.org/zh-cn/docs/user/references/protocol/rmi.html) - 采用 JDK 标准的 `java.rmi.*` 实现,采用阻塞式短连接和 JDK 标准序列化方式。 - -注意:如果正在使用 RMI 提供服务给外部访问,同时应用里依赖了老的 `common-collections` 包的情况下,存在反序列化安全风险。 - -### hessian 协议 - -[hessian](http://dubbo.apache.org/zh-cn/docs/user/references/protocol/hessian.html) 协议用于集成 Hessian 的服务,Hessian 底层采用 Http 通讯,采用 Servlet 暴露服务,Dubbo 缺省内嵌 Jetty 作为服务器实现。 - -Dubbo 的 Hessian 协议可以和原生 Hessian 服务互操作,即: - -- 提供者用 Dubbo 的 Hessian 协议暴露服务,消费者直接用标准 Hessian 接口调用 -- 或者提供方用标准 Hessian 暴露服务,消费方用 Dubbo 的 Hessian 协议调用。 - -### thrift 协议 - -当前 dubbo 支持的 [thrift](http://dubbo.apache.org/zh-cn/docs/user/references/protocol/thrift.html) 协议是对 thrift 原生协议的扩展,在原生协议的基础上添加了一些额外的头信息,比如 service name,magic number 等。 - -使用 dubbo thrift 协议同样需要使用 thrift 的 idl compiler 编译生成相应的 java 代码,后续版本中会在这方面做一些增强。 - -### http 协议 - -[http](http://dubbo.apache.org/zh-cn/docs/user/references/protocol/http.html) 协议基于 HTTP 表单的远程调用协议,采用 Spring 的 HttpInvoker 实现。 - -使用 JSON 序列化方式。 - -### webservice 协议 - -基于 WebService 的远程调用协议,基于 [Apache CXF](http://cxf.apache.org/) 的 `frontend-simple` 和 `transports-http` 实现。 - -使用 SOAP 序列化方式。 - -可以和原生 WebService 服务互操作,即: - -- 提供者用 Dubbo 的 WebService 协议暴露服务,消费者直接用标准 WebService 接口调用, -- 或者提供方用标准 WebService 暴露服务,消费方用 Dubbo 的 WebService 协议调用。 - -### rest 协议 - -基于标准的 Java REST API——JAX-RS 2.0(Java API for RESTful Web Services 的简写)实现的 REST 调用支持 - -### memcached 协议 - -基于 memcached 实现的 RPC 协议。 - -### redis 协议 - -基于 redis 实现的 RPC 协议。 - -> 在现实世界中,序列化有多种方式。 -> -> JDK 自身提供的序列化方式,效率不高,但是 Java 程序使用最多。 -> -> 如果想要较好的可读性,可以使用 JSON (常见库有:[jackson](https://github.com/FasterXML/jackson)、[gson](https://github.com/google/gson)、[fastjson](https://github.com/alibaba/fastjson))或 SOAP (即 xml 形式) -> -> 如果想要更好的性能,可以使用 [thrift](https://github.com/apache/thrift)、[protobuf](https://github.com/protocolbuffers/protobuf)、[hessian](http://hessian.caucho.com/doc/hessian-overview.xtp) -> -> 想深入了解可以参考:[序列化](https://github.com/dunwu/java-tutorial/blob/master/docs/lib/serialized) - -## 七、集群容错 - -在集群调用失败时,Dubbo 提供了多种容错方案,缺省为 failover 重试。 - -
- -
- -- **Failover** - **失败自动切换**,当出现失败,重试其它服务器。通常用于读操作,但重试会带来更长延迟。可通过 retries="2" 来设置重试次数(不含第一次)。 -- **Failfast** - **快速失败**,只发起一次调用,失败立即报错。通常用于非幂等性的写操作,比如新增记录。 -- **Failsafe** - **失败安全**,出现异常时,直接忽略。通常用于写入审计日志等操作。 -- **Failback** - **失败自动恢复**,后台记录失败请求,定时重发。通常用于消息通知操作。 -- **Forking** - **并行调用多个服务器**,只要一个成功即返回。通常用于实时性要求较高的读操作,但需要浪费更多服务资源。可通过 forks="2" 来设置最大并行数。 -- **Broadcast** - **广播调用所有提供者**,逐个调用,任意一台报错则报错。通常用于通知所有提供者更新缓存或日志等本地资源信息。 - -集群容错配置示例: - -```xml - - -``` - -## 八、负载均衡 - -Dubbo 提供了多种负载均衡(LoadBalance)策略,缺省为 `Random` 随机调用。 - -Dubbo 的负载均衡配置可以细粒度到服务、方法级别,且 `dubbo:service` 和 `dubbo:reference` 均可配置。 - -```xml - - - - - - - - - - - - -``` - -#### Random - -- **随机**,按权重设置随机概率。 -- 在一个截面上碰撞的概率高,但调用量越大分布越均匀,而且按概率使用权重后也比较均匀,有利于动态调整提供者权重。 - -#### RoundRobin - -- **轮询**,按公约后的权重设置轮询比率。 -- 存在慢的提供者累积请求的问题,比如:第二台机器很慢,但没挂,当请求调到第二台时就卡在那,久而久之,所有请求都卡在调到第二台上。 - -#### LeastActive - -- **最少活跃调用数**,相同活跃数的随机,活跃数指调用前后计数差。 -- 使慢的提供者收到更少请求,因为越慢的提供者的调用前后计数差会越大。 - -#### ConsistentHash - -- **一致性 Hash**,相同参数的请求总是发到同一提供者。 -- 当某一台提供者挂时,原本发往该提供者的请求,基于虚拟节点,平摊到其它提供者,不会引起剧烈变动。 -- 算法参见:[http://en.wikipedia.org/wiki/Consistent_hashing](http://en.wikipedia.org/wiki/Consistent_hashing) -- 缺省只对第一个参数 Hash,如果要修改,请配置 `` -- 缺省用 160 份虚拟节点,如果要修改,请配置 `` - -## 九、Dubbo 服务治理 - -### 服务治理简介 - -- 当服务越来越多时,服务 URL 配置管理变得非常困难,F5 硬件负载均衡器的单点压力也越来越大。 -- 当进一步发展,服务间依赖关系变得错踪复杂,甚至分不清哪个应用要在哪个应用之前启动,架构师都不能完整的描述应用的架构关系。 -- 接着,服务的调用量越来越大,服务的容量问题就暴露出来,这个服务需要多少机器支撑?什么时候该加机器? - -以上问题可以归纳为服务治理问题,这也是 Dubbo 的核心功能。 - -#### 调用链路 - -一个微服务架构,往往由大量分布式服务组成。那么这些服务之间互相是如何调用的?调用链路是啥?说实话,几乎到后面没人搞的清楚了,因为服务实在太多了,可能几百个甚至几千个服务。 - -那就需要基于 dubbo 做的分布式系统中,对各个服务之间的调用自动记录下来,然后自动将**各个服务之间的依赖关系和调用链路生成出来**,做成一张图,显示出来,大家才可以看到对吧。 - -#### 服务访问压力以及时长统计 - -需要自动统计**各个接口和服务之间的调用次数以及访问延时**,而且要分成两个级别。 - -- 一个级别是接口粒度,就是每个服务的每个接口每天被调用多少次,TP50/TP90/TP99,三个档次的请求延时分别是多少; -- 第二个级别是从源头入口开始,一个完整的请求链路经过几十个服务之后,完成一次请求,每天全链路走多少次,全链路请求延时的 TP50/TP90/TP99,分别是多少。 - -#### 其他 - -- 服务分层(避免循环依赖) -- 调用链路失败监控和报警 -- 服务鉴权 -- 每个服务的可用性的监控(接口调用成功率?几个 9?99.99%,99.9%,99%) - -所谓失败重试,就是 consumer 调用 provider 要是失败了,比如抛异常了,此时应该是可以重试的,或者调用超时了也可以重试。配置如下: - -``` - -``` - -举个栗子。 - -某个服务的接口,要耗费 5s,你这边不能干等着,你这边配置了 timeout 之后,我等待 2s,还没返回,我直接就撤了,不能干等你。 - -可以结合你们公司具体的场景来说说你是怎么设置这些参数的: - -- `timeout`:一般设置为 `200ms`,我们认为不能超过 `200ms` 还没返回。 -- `retries`:设置 retries,一般是在读请求的时候,比如你要查询个数据,你可以设置个 retries,如果第一次没读到,报错,重试指定的次数,尝试再次读取。 - -### 路由规则 - -路由规则决定一次 dubbo 服务调用的目标服务器,分为条件路由规则和脚本路由规则,并且支持可扩展。 - -向注册中心写入路由规则的操作通常由监控中心或治理中心的页面完成。 - -```java -RegistryFactory registryFactory = ExtensionLoader.getExtensionLoader(RegistryFactory.class).getAdaptiveExtension(); -Registry registry = registryFactory.getRegistry(URL.valueOf("zookeeper://10.20.153.10:2181")); -registry.register(URL.valueOf("condition://0.0.0.0/com.foo.BarService?category=routers&dynamic=false&rule=" + URL.encode("host = 10.20.153.10 => host = 10.20.153.11") + ")); -``` - -- **condition://** - 表示路由规则的类型,支持条件路由规则和脚本路由规则,可扩展,必填。 -- **0.0.0.0** - 表示对所有 IP 地址生效,如果只想对某个 IP 的生效,请填入具体 IP,必填。 -- **com.foo.BarService** - 表示只对指定服务生效,必填。 -- **category=routers** - 表示该数据为动态配置类型,必填。 -- **dynamic=false** - 表示该数据为持久数据,当注册方退出时,数据依然保存在注册中心,必填。 -- **enabled=true** - 覆盖规则是否生效,可不填,缺省生效。 -- **force=false** - 当路由结果为空时,是否强制执行,如果不强制执行,路由结果为空的路由规则将自动失效,可不填,缺省为 flase。 -- **runtime=false** - 是否在每次调用时执行路由规则,否则只在提供者地址列表变更时预先执行并缓存结果,调用时直接从缓存中获取路由结果。如果用了参数路由,必须设为 true,需要注意设置会影响调用的性能,可不填,缺省为 flase。 -- **priority=1** - 路由规则的优先级,用于排序,优先级越大越靠前执行,可不填,缺省为 0。 -- **rule=URL.encode("host = 10.20.153.10 => host = 10.20.153.11")** - 表示路由规则的内容,必填。 - -### 服务降级 - -可以通过服务降级功能临时屏蔽某个出错的非关键服务,并定义降级后的返回策略。 - -向注册中心写入动态配置覆盖规则: - -```java -RegistryFactory registryFactory = ExtensionLoader.getExtensionLoader(RegistryFactory.class).getAdaptiveExtension(); -Registry registry = registryFactory.getRegistry(URL.valueOf("zookeeper://10.20.153.10:2181")); -registry.register(URL.valueOf("override://0.0.0.0/com.foo.BarService?category=configurators&dynamic=false&application=foo&mock=force:return+null")); -``` - -其中: - -**`mock=force:return+null`** 表示消费方对该服务的方法调用都直接返回 null 值,不发起远程调用。用来屏蔽不重要服务不可用时对调用方的影响。 -还可以改为 **`mock=fail:return+null`** 表示消费方对该服务的方法调用在失败后,再返回 null 值,不抛异常。用来容忍不重要服务不稳定时对调用方的影响。 - -比如说服务 A 调用服务 B,结果服务 B 挂掉了,服务 A 重试几次调用服务 B,还是不行,那么直接降级,走一个备用的逻辑,给用户返回响应。 - -举个例子,我们有接口 `HelloService`。`HelloServiceImpl` 有该接口的具体实现。 - -```java -public interface HelloService { - void sayHello(); -} - -public class HelloServiceImpl implements HelloService { - public void sayHello() { - System.out.println("hello world......"); - } -} -``` - -Dubbo 配置: - -```xml - - - - - - - - - - - - - - - - - - - - - - - - -``` - -我们调用接口失败的时候,可以通过 `mock` 统一返回 null。 - -mock 的值也可以修改为 true,然后再跟接口同一个路径下实现一个 Mock 类,命名规则是 “接口名称+`Mock`” 后缀。然后在 Mock 类里实现自己的降级逻辑。 - -```java -public class HelloServiceMock implements HelloService { - public void sayHello() { - // 降级逻辑 - } -} -``` - -### 访问控制 - -#### 直连 - -在开发及测试环境下,经常需要绕过注册中心,只测试指定服务提供者,这时候可能需要点对点直连,点对点直联方式,将以服务接口为单位,忽略注册中心的提供者列表,A 接口配置点对点,不影响 B 接口从注册中心获取列表。 - -
- -
- -配置方式: - -(1)通过 XML 配置 - -如果是线上需求需要点对点,可在 中配置 url 指向提供者,将绕过注册中心,多个地址用分号隔开,配置如下: - -```xml - -``` - -(2)通过 -D 参数指定 - -在 JVM 启动参数中加入-D 参数映射服务地址: - -``` -java -Dcom.alibaba.xxx.XxxService=dubbo://localhost:20890 -``` - -(3)通过文件映射 -如果服务比较多,也可以用文件映射,用 -Ddubbo.resolve.file 指定映射文件路径,此配置优先级高于 中的配置: - -``` -java -Ddubbo.resolve.file=xxx.properties -``` - -然后在映射文件 xxx.properties 中加入配置,其中 key 为服务名,value 为服务提供者 URL: - -```properties -com.alibaba.xxx.XxxService=dubbo://localhost:20890 -``` - -#### 只订阅 - -为方便开发测试,经常会在线下共用一个所有服务可用的注册中心,这时,如果一个正在开发中的服务提供者注册,可能会影响消费者不能正常运行。 - -可以让服务提供者开发方,只订阅服务(开发的服务可能依赖其它服务),而不注册正在开发的服务,通过直连测试正在开发的服务。 - -禁用注册配置: - -```xml - -``` - -或者 - -```xml - -``` - -#### 只注册 - -如果有两个镜像环境,两个注册中心,有一个服务只在其中一个注册中心有部署,另一个注册中心还没来得及部署,而两个注册中心的其它应用都需要依赖此服务。这个时候,可以让服务提供者方只注册服务到另一注册中心,而不从另一注册中心订阅服务。 - -禁用订阅配置 - -```xml - - -``` - -或者 - -```xml - - -``` - -#### 静态服务 - -有时候希望人工管理服务提供者的上线和下线,此时需将注册中心标识为非动态管理模式。 - -``` - -``` - -或者 - -``` - -``` - -服务提供者初次注册时为禁用状态,需人工启用。断线时,将不会被自动删除,需人工禁用。 - -### 动态配置 - -向注册中心写入动态配置覆盖规则。该功能通常由监控中心或治理中心的页面完成。 - -```java -RegistryFactory registryFactory = ExtensionLoader.getExtensionLoader(RegistryFactory.class).getAdaptiveExtension(); -Registry registry = registryFactory.getRegistry(URL.valueOf("zookeeper://10.20.153.10:2181")); -registry.register(URL.valueOf("override://0.0.0.0/com.foo.BarService?category=configurators&dynamic=false&application=foo&timeout=1000")); -``` - -其中: - -- **override://** - 表示数据采用覆盖方式,支持 override 和 absent,可扩展,必填。 -- **0.0.0.0** - 表示对所有 IP 地址生效,如果只想覆盖某个 IP 的数据,请填入具体 IP,必填。 -- **com.foo.BarService** - 表示只对指定服务生效,必填。 -- **category=configurators** - 表示该数据为动态配置类型,必填。 -- **dynamic=false** - 表示该数据为持久数据,当注册方退出时,数据依然保存在注册中心,必填。 -- **enabled=true** - 覆盖规则是否生效,可不填,缺省生效。 -- **application=foo** - 表示只对指定应用生效,可不填,表示对所有应用生效。 -- **timeout=1000** - 表示将满足以上条件的 timeout 参数的值覆盖为 1000。如果想覆盖其它参数,直接加在 override 的 URL 参数上。 - -示例: - -- 禁用提供者:(通常用于临时踢除某台提供者机器,相似的,禁止消费者访问请使用路由规则) - -``` -override://10.20.153.10/com.foo.BarService?category=configurators&dynamic=false&disbaled=true -``` - -- 调整权重:(通常用于容量评估,缺省权重为 100) - -``` -override://10.20.153.10/com.foo.BarService?category=configurators&dynamic=false&weight=200 -``` - -- 调整负载均衡策略:(缺省负载均衡策略为 random) - -``` -override://10.20.153.10/com.foo.BarService?category=configurators&dynamic=false&loadbalance=leastactive -``` - -- 服务降级:(通常用于临时屏蔽某个出错的非关键服务) - -``` -override://0.0.0.0/com.foo.BarService?category=configurators&dynamic=false&application=foo&mock=force:return+null -``` - -## 十、多版本 - -当一个接口实现,出现不兼容升级时,可以用版本号过渡,版本号不同的服务相互间不引用。 - -可以按照以下的步骤进行版本迁移: - -1. 在低压力时间段,先升级一半提供者为新版本 -2. 再将所有消费者升级为新版本 -3. 然后将剩下的一半提供者升级为新版本 - -老版本服务提供者配置: - -```xml - -``` - -新版本服务提供者配置: - -```xml - -``` - -老版本服务消费者配置: - -```xml - -``` - -新版本服务消费者配置: - -```xml - -``` - -如果不需要区分版本,可以按照以下的方式配置 [[1\]](http://dubbo.apache.org/zh-cn/docs/user/demos/multi-versions.html#fn1): - -```xml - -``` - -## 十一、Dubbo SPI - -SPI 全称为 Service Provider Interface,是一种服务发现机制。SPI 的本质是**将接口实现类的全限定名配置在文件中,并由服务加载器读取配置文件,加载实现类**。这样可以在运行时,动态为接口替换实现类。正因此特性,我们可以很容易的通过 SPI 机制为我们的程序提供拓展功能。SPI 机制在第三方框架中也有所应用,比如 Dubbo 就是通过 SPI 机制加载所有的组件。不过,Dubbo 并未使用 Java 原生的 SPI 机制,而是对其进行了增强,使其能够更好的满足需求。在 Dubbo 中,SPI 是一个非常重要的模块。基于 SPI,我们可以很容易的对 Dubbo 进行拓展。 - -Dubbo SPI 的相关逻辑被封装在了 `ExtensionLoader` 类中,通过 `ExtensionLoader`,我们可以加载指定的实现类。Dubbo SPI 所需的配置文件需放置在 `META-INF/dubbo` 路径下。 - -## 参考资料 - -- **官方** - - [Dubbo Github](https://github.com/apache/dubbo) - - [Dubbo 官方文档](https://dubbo.apache.org/zh-cn/) - - [管理员手册](https://dubbo.gitbooks.io/dubbo-admin-book/content/) -- **文章** - - [如何基于 Dubbo 进行服务治理、服务降级、失败重试以及超时重试?](https://github.com/doocs/advanced-java/blob/master/docs/distributed-system/dubbo-service-management.md) diff --git "a/docs/13.\346\241\206\346\236\266/README.md" "b/docs/13.\346\241\206\346\236\266/README.md" deleted file mode 100644 index 6be5b453..00000000 --- "a/docs/13.\346\241\206\346\236\266/README.md" +++ /dev/null @@ -1,66 +0,0 @@ ---- -title: Java 框架 -categories: - - 编程 - - Java - - 框架 -tags: - - Java - - 框架 -abbrlink: b3f7d0e8 -date: 2022-02-18 08:53:11 -hidden: true -permalink: /pages/fd4995/ ---- - -# Java 框架 - -## 📖 内容 - -### Spring - -📚 [spring-tutorial](https://dunwu.github.io/spring-tutorial/) 是一个 Spring 实战教程。 - -### Spring Boot - -📚 [Spring Boot 教程](https://dunwu.github.io/spring-boot-tutorial/) 是一个 Spring Boot 实战教程。 - -### ORM - -- [Mybatis 快速入门](11.ORM/01.Mybatis快速入门.md) -- [Mybatis 原理](11.ORM/02.Mybatis原理.md) - -### 安全 - -> Java 领域比较流行的安全框架就是 shiro 和 spring-security。 -> -> shiro 更为简单、轻便,容易理解,能满足大多数基本安全场景下的需要。 -> -> spring-security 功能更丰富,也比 shiro 更复杂。值得一提的是由于 spring-security 是 spring 团队开发,所以集成 spring 和 spring-boot 框架更容易。 - -- [Shiro](12.安全/01.Shiro.md) -- [SpringSecurity](12.安全/02.SpringSecurity.md) - -### IO - -- [Netty](13.IO/01.Netty.md) - -### 微服务 - -- [Dubbo](14.微服务/01.Dubbo.md) - -## 📚 资料 - -- **Mybatis** - - [Mybatis Github](https://github.com/mybatis/mybatis-3) - - [Mybatis 官网](http://www.mybatis.org/mybatis-3/) - - [MyBatis 官方代码生成(mybatis-generator)](https://github.com/mybatis/generator) - - [MyBatis 官方集成 Spring(mybatis-spring)](https://github.com/mybatis/spring) - - [Mybatis 官方集成 Spring Boot(mybatis-spring-boot)](https://github.com/mybatis/spring-boot-starter) - - [MyBatis-Plus](https://github.com/baomidou/mybatis-plus) - CRUD 扩展插件、代码生成器、分页器等多功能 - - [Mapper](https://github.com/abel533/Mapper) - CRUD 扩展插件 - - [Mybatis-PageHelper](https://github.com/pagehelper/Mybatis-PageHelper) - Mybatis 通用分页插件 - -## 🚪 传送 - -◾ 🏠 [JAVA-TUTORIAL 首页](https://github.com/dunwu/java-tutorial) ◾ 🎯 [我的博客](https://github.com/dunwu/blog) ◾ diff --git "a/docs/14.\344\270\255\351\227\264\344\273\266/01.MQ/01.\346\266\210\346\201\257\351\230\237\345\210\227\351\235\242\350\257\225.md" "b/docs/14.\344\270\255\351\227\264\344\273\266/01.MQ/01.\346\266\210\346\201\257\351\230\237\345\210\227\351\235\242\350\257\225.md" deleted file mode 100644 index b8f46bd7..00000000 --- "a/docs/14.\344\270\255\351\227\264\344\273\266/01.MQ/01.\346\266\210\346\201\257\351\230\237\345\210\227\351\235\242\350\257\225.md" +++ /dev/null @@ -1,378 +0,0 @@ ---- -title: 消息队列面试 -categories: - - 编程 - - Java - - 中间件 - - MQ -tags: - - Java - - 中间件 - - MQ - - 面试 -abbrlink: 82d4fac9 -date: 2022-02-17 22:34:30 -permalink: /pages/5a6cf3/ ---- - -# 消息队列面试夺命连环问 - -## 为什么使用消息队列? - -### 解耦 - -看这么个场景。A 系统发送数据到 BCD 三个系统,通过接口调用发送。如果 E 系统也要这个数据呢?那如果 C 系统现在不需要了呢?A 系统负责人几乎崩溃...... - -![img](https://github.com/doocs/advanced-java/blob/master/images/mq-1.png) - -在这个场景中,A 系统跟其它各种乱七八糟的系统严重耦合,A 系统产生一条比较关键的数据,很多系统都需要 A 系统将这个数据发送过来。A 系统要时时刻刻考虑 BCDE 四个系统如果挂了该咋办?要不要重发,要不要把消息存起来?头发都白了啊! - -如果使用 MQ,A 系统产生一条数据,发送到 MQ 里面去,哪个系统需要数据自己去 MQ 里面消费。如果新系统需要数据,直接从 MQ 里消费即可;如果某个系统不需要这条数据了,就取消对 MQ 消息的消费即可。这样下来,A 系统压根儿不需要去考虑要给谁发送数据,不需要维护这个代码,也不需要考虑人家是否调用成功、失败超时等情况。 - -![img](https://github.com/doocs/advanced-java/blob/master/images/mq-2.png) - -**总结**:通过一个 MQ,Pub/Sub 发布订阅消息这么一个模型,A 系统就跟其它系统彻底解耦了。 - -**面试技巧**:你需要去考虑一下你负责的系统中是否有类似的场景,就是一个系统或者一个模块,调用了多个系统或者模块,互相之间的调用很复杂,维护起来很麻烦。但是其实这个调用是不需要直接同步调用接口的,如果用 MQ 给它异步化解耦,也是可以的,你就需要去考虑在你的项目里,是不是可以运用这个 MQ 去进行系统的解耦。在简历中体现出来这块东西,用 MQ 作解耦。 - -### 异步 - -再来看一个场景,A 系统接收一个请求,需要在自己本地写库,还需要在 BCD 三个系统写库,自己本地写库要 3ms,BCD 三个系统分别写库要 300ms、450ms、200ms。最终请求总延时是 3 + 300 + 450 + 200 = 953ms,接近 1s,用户感觉搞个什么东西,慢死了慢死了。用户通过浏览器发起请求,等待个 1s,这几乎是不可接受的。 - -![img](https://github.com/doocs/advanced-java/blob/master/images/mq-3.png) - -一般互联网类的企业,对于用户直接的操作,一般要求是每个请求都必须在 200 ms 以内完成,对用户几乎是无感知的。 - -如果**使用 MQ**,那么 A 系统连续发送 3 条消息到 MQ 队列中,假如耗时 5ms,A 系统从接受一个请求到返回响应给用户,总时长是 3 + 5 = 8ms,对于用户而言,其实感觉上就是点个按钮,8ms 以后就直接返回了,爽!网站做得真好,真快! - -![img](https://github.com/doocs/advanced-java/blob/master/images/mq-4.png) - -### 削峰 - -每天 0:00 到 12:00,A 系统风平浪静,每秒并发请求数量就 50 个。结果每次一到 12:00 \~ 13:00 ,每秒并发请求数量突然会暴增到 5k+ 条。但是系统是直接基于 MySQL 的,大量的请求涌入 MySQL,每秒钟对 MySQL 执行约 5k 条 SQL。 - -一般的 MySQL,扛到每秒 2k 个请求就差不多了,如果每秒请求到 5k 的话,可能就直接把 MySQL 给打死了,导致系统崩溃,用户也就没法再使用系统了。 - -但是高峰期一过,到了下午的时候,就成了低峰期,可能也就 1w 的用户同时在网站上操作,每秒中的请求数量可能也就 50 个请求,对整个系统几乎没有任何的压力。 - -![img](https://github.com/doocs/advanced-java/blob/master/images/mq-5.png) - -如果使用 MQ,每秒 5k 个请求写入 MQ,A 系统每秒钟最多处理 2k 个请求,因为 MySQL 每秒钟最多处理 2k 个。A 系统从 MQ 中慢慢拉取请求,每秒钟就拉取 2k 个请求,不要超过自己每秒能处理的最大请求数量就 ok,这样下来,哪怕是高峰期的时候,A 系统也绝对不会挂掉。而 MQ 每秒钟 5k 个请求进来,就 2k 个请求出去,结果就导致在中午高峰期(1 个小时),可能有几十万甚至几百万的请求积压在 MQ 中。 - -![img](https://github.com/doocs/advanced-java/blob/master/images/mq-6.png) - -这个短暂的高峰期积压是 ok 的,因为高峰期过了之后,每秒钟就 50 个请求进 MQ,但是 A 系统依然会按照每秒 2k 个请求的速度在处理。所以说,只要高峰期一过,A 系统就会快速将积压的消息给解决掉。 - -## 消息队列有什么优缺点 - -优点上面已经说了,就是**在特殊场景下有其对应的好处**,**解耦**、**异步**、**削峰**。 - -缺点有以下几个: - -- 系统可用性降低 - 系统引入的外部依赖越多,越容易挂掉。本来你就是 A 系统调用 BCD 三个系统的接口就好了,人 ABCD 四个系统好好的,没啥问题,你偏加个 MQ 进来,万一 MQ 挂了咋整,MQ 一挂,整套系统崩溃的,你不就完了?如何保证消息队列的高可用,可以[点击这里查看](https://github.com/doocs/advanced-java/blob/master/docs/high-concurrency/how-to-ensure-high-availability-of-message-queues.md)。 -- 系统复杂度提高 - 硬生生加个 MQ 进来,你怎么[保证消息没有重复消费](https://github.com/doocs/advanced-java/blob/master/docs/high-concurrency/how-to-ensure-that-messages-are-not-repeatedly-consumed.md)?怎么[处理消息丢失的情况](https://github.com/doocs/advanced-java/blob/master/docs/high-concurrency/how-to-ensure-the-reliable-transmission-of-messages.md)?怎么保证消息传递的顺序性?头大头大,问题一大堆,痛苦不已。 -- 一致性问题 - A 系统处理完了直接返回成功了,人都以为你这个请求就成功了;但是问题是,要是 BCD 三个系统那里,BD 两个系统写库成功了,结果 C 系统写库失败了,咋整?你这数据就不一致了。 - -所以消息队列实际是一种非常复杂的架构,你引入它有很多好处,但是也得针对它带来的坏处做各种额外的技术方案和架构来规避掉,做好之后,你会发现,妈呀,系统复杂度提升了一个数量级,也许是复杂了 10 倍。但是关键时刻,用,还是得用的。 - -## Kafka、ActiveMQ、RabbitMQ、RocketMQ 有什么优缺点? - -| 特性 | ActiveMQ | RabbitMQ | RocketMQ | Kafka | -| ------------------------ | ------------------------------------- | -------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | -| 单机吞吐量 | 万级,比 RocketMQ、Kafka 低一个数量级 | 同 ActiveMQ | 10 万级,支撑高吞吐 | 10 万级,高吞吐,一般配合大数据类的系统来进行实时数据计算、日志采集等场景 | -| topic 数量对吞吐量的影响 | | | topic 可以达到几百/几千的级别,吞吐量会有较小幅度的下降,这是 RocketMQ 的一大优势,在同等机器下,可以支撑大量的 topic | topic 从几十到几百个时候,吞吐量会大幅度下降,在同等机器下,Kafka 尽量保证 topic 数量不要过多,如果要支撑大规模的 topic,需要增加更多的机器资源 | -| 时效性 | ms 级 | 微秒级,这是 RabbitMQ 的一大特点,延迟最低 | ms 级 | 延迟在 ms 级以内 | -| 可用性 | 高,基于主从架构实现高可用 | 同 ActiveMQ | 非常高,分布式架构 | 非常高,分布式,一个数据多个副本,少数机器宕机,不会丢失数据,不会导致不可用 | -| 消息可靠性 | 有较低的概率丢失数据 | 基本不丢 | 经过参数优化配置,可以做到 0 丢失 | 同 RocketMQ | -| 功能支持 | MQ 领域的功能极其完备 | 基于 erlang 开发,并发能力很强,性能极好,延时很低 | MQ 功能较为完善,还是分布式的,扩展性好 | 功能较为简单,主要支持简单的 MQ 功能,在大数据领域的实时计算以及日志采集被大规模使用 | - -综上,各种对比之后,有如下建议: - -- 一般的业务系统要引入 MQ,最早大家都用 ActiveMQ,但是现在确实大家用的不多了,没经过大规模吞吐量场景的验证,社区也不是很活跃,所以大家还是算了吧,我个人不推荐用这个了; -- 后来大家开始用 RabbitMQ,但是确实 erlang 语言阻止了大量的 Java 工程师去深入研究和掌控它,对公司而言,几乎处于不可控的状态,但是确实人家是开源的,比较稳定的支持,活跃度也高; -- 不过现在确实越来越多的公司会去用 RocketMQ,确实很不错,毕竟是阿里出品,但社区可能有突然黄掉的风险(目前 RocketMQ 已捐给 [Apache](https://github.com/apache/rocketmq),但 GitHub 上的活跃度其实不算高)对自己公司技术实力有绝对自信的,推荐用 RocketMQ,否则回去老老实实用 RabbitMQ 吧,人家有活跃的开源社区,绝对不会黄。 -- 所以**中小型公司**,技术实力较为一般,技术挑战不是特别高,用 RabbitMQ 是不错的选择;**大型公司**,基础架构研发实力较强,用 RocketMQ 是很好的选择。 -- 如果是**大数据领域**的实时计算、日志采集等场景,用 Kafka 是业内标准的,绝对没问题,社区活跃度很高,绝对不会黄,何况几乎是全世界这个领域的事实性规范。 - -## 如何保证消息队列的高可用? - -### RabbitMQ 的高可用性 - -RabbitMQ 是比较有代表性的,因为是**基于主从**(非分布式)做高可用性的,我们就以 RabbitMQ 为例子讲解第一种 MQ 的高可用性怎么实现。 - -RabbitMQ 有三种模式:单机模式、普通集群模式、镜像集群模式。 - -#### 单机模式 - -单机模式,就是 Demo 级别的,一般就是你本地启动了玩玩儿的 😄,没人生产用单机模式。 - -#### 普通集群模式(无高可用性) - -普通集群模式,意思就是在多台机器上启动多个 RabbitMQ 实例,每个机器启动一个。你**创建的 queue,只会放在一个 RabbitMQ 实例上**,但是每个实例都同步 queue 的元数据(元数据可以认为是 queue 的一些配置信息,通过元数据,可以找到 queue 所在实例)。你消费的时候,实际上如果连接到了另外一个实例,那么那个实例会从 queue 所在实例上拉取数据过来。 - -![img](https://github.com/doocs/advanced-java/blob/master/images/mq-7.png) - -这种方式确实很麻烦,也不怎么好,**没做到所谓的分布式**,就是个普通集群。因为这导致你要么消费者每次随机连接一个实例然后拉取数据,要么固定连接那个 queue 所在实例消费数据,前者有**数据拉取的开销**,后者导致**单实例性能瓶颈**。 - -而且如果那个放 queue 的实例宕机了,会导致接下来其他实例就无法从那个实例拉取,如果你**开启了消息持久化**,让 RabbitMQ 落地存储消息的话,**消息不一定会丢**,得等这个实例恢复了,然后才可以继续从这个 queue 拉取数据。 - -所以这个事儿就比较尴尬了,这就**没有什么所谓的高可用性**,**这方案主要是提高吞吐量的**,就是说让集群中多个节点来服务某个 queue 的读写操作。 - -#### 镜像集群模式(高可用性) - -这种模式,才是所谓的 RabbitMQ 的高可用模式。跟普通集群模式不一样的是,在镜像集群模式下,你创建的 queue,无论元数据还是 queue 里的消息都会**存在于多个实例上**,就是说,每个 RabbitMQ 节点都有这个 queue 的一个**完整镜像**,包含 queue 的全部数据的意思。然后每次你写消息到 queue 的时候,都会自动把**消息同步**到多个实例的 queue 上。 - -![img](https://github.com/doocs/advanced-java/blob/master/images/mq-8.png) - -那么**如何开启这个镜像集群模式**呢?其实很简单,RabbitMQ 有很好的管理控制台,就是在后台新增一个策略,这个策略是**镜像集群模式的策略**,指定的时候是可以要求数据同步到所有节点的,也可以要求同步到指定数量的节点,再次创建 queue 的时候,应用这个策略,就会自动将数据同步到其他的节点上去了。 - -这样的话,好处在于,你任何一个机器宕机了,没事儿,其它机器(节点)还包含了这个 queue 的完整数据,别的 consumer 都可以到其它节点上去消费数据。坏处在于,第一,这个性能开销也太大了吧,消息需要同步到所有机器上,导致网络带宽压力和消耗很重!第二,这么玩儿,不是分布式的,就**没有扩展性可言**了,如果某个 queue 负载很重,你加机器,新增的机器也包含了这个 queue 的所有数据,并**没有办法线性扩展**你的 queue。你想,如果这个 queue 的数据量很大,大到这个机器上的容量无法容纳了,此时该怎么办呢? - -### Kafka 的高可用性 - -Kafka 一个最基本的架构认识:由多个 broker 组成,每个 broker 是一个节点;你创建一个 topic,这个 topic 可以划分为多个 partition,每个 partition 可以存在于不同的 broker 上,每个 partition 就放一部分数据。 - -这就是**天然的分布式消息队列**,就是说一个 topic 的数据,是**分散放在多个机器上的,每个机器就放一部分数据**。 - -实际上 RabbmitMQ 之类的,并不是分布式消息队列,它就是传统的消息队列,只不过提供了一些集群、HA(High Availability, 高可用性) 的机制而已,因为无论怎么玩儿,RabbitMQ 一个 queue 的数据都是放在一个节点里的,镜像集群下,也是每个节点都放这个 queue 的完整数据。 - -Kafka 0.8 以前,是没有 HA 机制的,就是任何一个 broker 宕机了,那个 broker 上的 partition 就废了,没法写也没法读,没有什么高可用性可言。 - -比如说,我们假设创建了一个 topic,指定其 partition 数量是 3 个,分别在三台机器上。但是,如果第二台机器宕机了,会导致这个 topic 的 1/3 的数据就丢了,因此这个是做不到高可用的。 - -![img](https://github.com/doocs/advanced-java/blob/master/images/kafka-before.png) - -Kafka 0.8 以后,提供了 HA 机制,就是 replica(复制品) 副本机制。每个 partition 的数据都会同步到其它机器上,形成自己的多个 replica 副本。所有 replica 会选举一个 leader 出来,那么生产和消费都跟这个 leader 打交道,然后其他 replica 就是 follower。写的时候,leader 会负责把数据同步到所有 follower 上去,读的时候就直接读 leader 上的数据即可。只能读写 leader?很简单,**要是你可以随意读写每个 follower,那么就要 care 数据一致性的问题**,系统复杂度太高,很容易出问题。Kafka 会均匀地将一个 partition 的所有 replica 分布在不同的机器上,这样才可以提高容错性。 - -![img](https://github.com/doocs/advanced-java/blob/master/images/kafka-after.png) - -这么搞,就有所谓的**高可用性**了,因为如果某个 broker 宕机了,没事儿,那个 broker 上面的 partition 在其他机器上都有副本的。如果这个宕机的 broker 上面有某个 partition 的 leader,那么此时会从 follower 中**重新选举**一个新的 leader 出来,大家继续读写那个新的 leader 即可。这就有所谓的高可用性了。 - -**写数据**的时候,生产者就写 leader,然后 leader 将数据落地写本地磁盘,接着其他 follower 自己主动从 leader 来 pull 数据。一旦所有 follower 同步好数据了,就会发送 ack 给 leader,leader 收到所有 follower 的 ack 之后,就会返回写成功的消息给生产者。(当然,这只是其中一种模式,还可以适当调整这个行为) - -**消费**的时候,只会从 leader 去读,但是只有当一个消息已经被所有 follower 都同步成功返回 ack 的时候,这个消息才会被消费者读到。 - -看到这里,相信你大致明白了 Kafka 是如何保证高可用机制的了,对吧?不至于一无所知,现场还能给面试官画画图。要是遇上面试官确实是 Kafka 高手,深挖了问,那你只能说不好意思,太深入的你没研究过。 - -## 如何保证消息不被重复消费?(如何保证消息消费的幂等性) - -首先,比如 RabbitMQ、RocketMQ、Kafka,都有可能会出现消息重复消费的问题,正常。因为这问题通常不是 MQ 自己保证的,是由我们开发来保证的。挑一个 Kafka 来举个例子,说说怎么重复消费吧。 - -Kafka 实际上有个 offset 的概念,就是每个消息写进去,都有一个 offset,代表消息的序号,然后 consumer 消费了数据之后,**每隔一段时间**(定时定期),会把自己消费过的消息的 offset 提交一下,表示“我已经消费过了,下次我要是重启啥的,你就让我继续从上次消费到的 offset 来继续消费吧”。 - -但是凡事总有意外,比如我们之前生产经常遇到的,就是你有时候重启系统,看你怎么重启了,如果碰到点着急的,直接 kill 进程了,再重启。这会导致 consumer 有些消息处理了,但是没来得及提交 offset,尴尬了。重启之后,少数消息会再次消费一次。 - -举个栗子。 - -有这么个场景。数据 1/2/3 依次进入 kafka,kafka 会给这三条数据每条分配一个 offset,代表这条数据的序号,我们就假设分配的 offset 依次是 152/153/154。消费者从 kafka 去消费的时候,也是按照这个顺序去消费。假如当消费者消费了 `offset=153` 的这条数据,刚准备去提交 offset 到 zookeeper,此时消费者进程被重启了。那么此时消费过的数据 1/2 的 offset 并没有提交,kafka 也就不知道你已经消费了 `offset=153` 这条数据。那么重启之后,消费者会找 kafka 说,嘿,哥儿们,你给我接着把上次我消费到的那个地方后面的数据继续给我传递过来。由于之前的 offset 没有提交成功,那么数据 1/2 会再次传过来,如果此时消费者没有去重的话,那么就会导致重复消费。 - -![img](https://github.com/doocs/advanced-java/blob/master/images/mq-10.png) - -如果消费者干的事儿是拿一条数据就往数据库里写一条,会导致说,你可能就把数据 1/2 在数据库里插入了 2 次,那么数据就错啦。 - -其实重复消费不可怕,可怕的是你没考虑到重复消费之后,**怎么保证幂等性**。 - -举个例子吧。假设你有个系统,消费一条消息就往数据库里插入一条数据,要是你一个消息重复两次,你不就插入了两条,这数据不就错了?但是你要是消费到第二次的时候,自己判断一下是否已经消费过了,若是就直接扔了,这样不就保留了一条数据,从而保证了数据的正确性。 - -一条数据重复出现两次,数据库里就只有一条数据,这就保证了系统的幂等性。 - -幂等性,通俗点说,就一个数据,或者一个请求,给你重复来多次,你得确保对应的数据是不会改变的,**不能出错**。 - -所以第二个问题来了,怎么保证消息队列消费的幂等性? - -其实还是得结合业务来思考,我这里给几个思路: - -- 比如你拿个数据要写库,你先根据主键查一下,如果这数据都有了,你就别插入了,update 一下好吧。 -- 比如你是写 Redis,那没问题了,反正每次都是 set,天然幂等性。 -- 比如你不是上面两个场景,那做的稍微复杂一点,你需要让生产者发送每条数据的时候,里面加一个全局唯一的 id,类似订单 id 之类的东西,然后你这里消费到了之后,先根据这个 id 去比如 Redis 里查一下,之前消费过吗?如果没有消费过,你就处理,然后这个 id 写 Redis。如果消费过了,那你就别处理了,保证别重复处理相同的消息即可。 -- 比如基于数据库的唯一键来保证重复数据不会重复插入多条。因为有唯一键约束了,重复数据插入只会报错,不会导致数据库中出现脏数据。 - -![img](https://github.com/doocs/advanced-java/blob/master/images/mq-11.png) - -当然,如何保证 MQ 的消费是幂等性的,需要结合具体的业务来看。 - -## 如何保证消息的可靠性传输?(如何处理消息丢失的问题) - -数据的丢失问题,可能出现在生产者、MQ、消费者中,咱们从 RabbitMQ 和 Kafka 分别来分析一下吧。 - -### RabbitMQ - -![img](https://github.com/doocs/advanced-java/blob/master/images/rabbitmq-message-lose.png) - -#### 生产者弄丢了数据 - -生产者将数据发送到 RabbitMQ 的时候,可能数据就在半路给搞丢了,因为网络问题啥的,都有可能。 - -此时可以选择用 RabbitMQ 提供的事务功能,就是生产者**发送数据之前**开启 RabbitMQ 事务`channel.txSelect`,然后发送消息,如果消息没有成功被 RabbitMQ 接收到,那么生产者会收到异常报错,此时就可以回滚事务`channel.txRollback`,然后重试发送消息;如果收到了消息,那么可以提交事务`channel.txCommit`。 - -```java -// 开启事务 -channel.txSelect -try { - // 这里发送消息 -} catch (Exception e) { - channel.txRollback - - // 这里再次重发这条消息 -} - -// 提交事务 -channel.txCommit -``` - -但是问题是,RabbitMQ 事务机制(同步)一搞,基本上**吞吐量会下来,因为太耗性能**。 - -所以一般来说,如果你要确保说写 RabbitMQ 的消息别丢,可以开启 `confirm` 模式,在生产者那里设置开启 `confirm`模式之后,你每次写的消息都会分配一个唯一的 id,然后如果写入了 RabbitMQ 中,RabbitMQ 会给你回传一个 `ack` 消息,告诉你说这个消息 ok 了。如果 RabbitMQ 没能处理这个消息,会回调你的一个 `nack` 接口,告诉你这个消息接收失败,你可以重试。而且你可以结合这个机制自己在内存里维护每个消息 id 的状态,如果超过一定时间还没接收到这个消息的回调,那么你可以重发。 - -事务机制和 `confirm` 机制最大的不同在于,**事务机制是同步的**,你提交一个事务之后会**阻塞**在那儿,但是 `confirm` 机制是**异步**的,你发送个消息之后就可以发送下一个消息,然后那个消息 RabbitMQ 接收了之后会异步回调你的一个接口通知你这个消息接收到了。 - -所以一般在生产者这块**避免数据丢失**,都是用 `confirm` 机制的。 - -#### RabbitMQ 弄丢了数据 - -就是 RabbitMQ 自己弄丢了数据,这个你必须**开启 RabbitMQ 的持久化**,就是消息写入之后会持久化到磁盘,哪怕是 RabbitMQ 自己挂了,**恢复之后会自动读取之前存储的数据**,一般数据不会丢。除非极其罕见的是,RabbitMQ 还没持久化,自己就挂了,**可能导致少量数据丢失**,但是这个概率较小。 - -设置持久化有**两个步骤**: - -- 创建 queue 的时候将其设置为持久化 - 这样就可以保证 RabbitMQ 持久化 queue 的元数据,但是它是不会持久化 queue 里的数据的。 -- 第二个是发送消息的时候将消息的 `deliveryMode` 设置为 2 - 就是将消息设置为持久化的,此时 RabbitMQ 就会将消息持久化到磁盘上去。 - -必须要同时设置这两个持久化才行,RabbitMQ 哪怕是挂了,再次重启,也会从磁盘上重启恢复 queue,恢复这个 queue 里的数据。 - -注意,哪怕是你给 RabbitMQ 开启了持久化机制,也有一种可能,就是这个消息写到了 RabbitMQ 中,但是还没来得及持久化到磁盘上,结果不巧,此时 RabbitMQ 挂了,就会导致内存里的一点点数据丢失。 - -所以,持久化可以跟生产者那边的 `confirm` 机制配合起来,只有消息被持久化到磁盘之后,才会通知生产者 `ack` 了,所以哪怕是在持久化到磁盘之前,RabbitMQ 挂了,数据丢了,生产者收不到 `ack`,你也是可以自己重发的。 - -#### 消费端弄丢了数据 - -RabbitMQ 如果丢失了数据,主要是因为你消费的时候,**刚消费到,还没处理,结果进程挂了**,比如重启了,那么就尴尬了,RabbitMQ 认为你都消费了,这数据就丢了。 - -这个时候得用 RabbitMQ 提供的 `ack` 机制,简单来说,就是你必须关闭 RabbitMQ 的自动 `ack`,可以通过一个 api 来调用就行,然后每次你自己代码里确保处理完的时候,再在程序里 `ack` 一把。这样的话,如果你还没处理完,不就没有 `ack` 了?那 RabbitMQ 就认为你还没处理完,这个时候 RabbitMQ 会把这个消费分配给别的 consumer 去处理,消息是不会丢的。 - -![img](https://github.com/doocs/advanced-java/blob/master/images/rabbitmq-message-lose-solution.png) - -### Kafka - -#### 消费端弄丢了数据 - -唯一可能导致消费者弄丢数据的情况,就是说,你消费到了这个消息,然后消费者那边**自动提交了 offset**,让 Kafka 以为你已经消费好了这个消息,但其实你才刚准备处理这个消息,你还没处理,你自己就挂了,此时这条消息就丢咯。 - -这不是跟 RabbitMQ 差不多吗,大家都知道 Kafka 会自动提交 offset,那么只要**关闭自动提交** offset,在处理完之后自己手动提交 offset,就可以保证数据不会丢。但是此时确实还是**可能会有重复消费**,比如你刚处理完,还没提交 offset,结果自己挂了,此时肯定会重复消费一次,自己保证幂等性就好了。 - -生产环境碰到的一个问题,就是说我们的 Kafka 消费者消费到了数据之后是写到一个内存的 queue 里先缓冲一下,结果有的时候,你刚把消息写入内存 queue,然后消费者会自动提交 offset。然后此时我们重启了系统,就会导致内存 queue 里还没来得及处理的数据就丢失了。 - -#### Kafka 弄丢了数据 - -这块比较常见的一个场景,就是 Kafka 某个 broker 宕机,然后重新选举 partition 的 leader。大家想想,要是此时其他的 follower 刚好还有些数据没有同步,结果此时 leader 挂了,然后选举某个 follower 成 leader 之后,不就少了一些数据?这就丢了一些数据啊。 - -生产环境也遇到过,我们也是,之前 Kafka 的 leader 机器宕机了,将 follower 切换为 leader 之后,就会发现说这个数据就丢了。 - -所以此时一般是要求起码设置如下 4 个参数: - -- 给 topic 设置 `replication.factor` 参数:这个值必须大于 1,要求每个 partition 必须有至少 2 个副本。 -- 在 Kafka 服务端设置 `min.insync.replicas` 参数:这个值必须大于 1,这个是要求一个 leader 至少感知到有至少一个 follower 还跟自己保持联系,没掉队,这样才能确保 leader 挂了还有一个 follower 吧。 -- 在 producer 端设置 `acks=all`:这个是要求每条数据,必须是**写入所有 replica 之后,才能认为是写成功了**。 -- 在 producer 端设置 `retries=MAX`(很大很大很大的一个值,无限次重试的意思):这个是**要求一旦写入失败,就无限重试**,卡在这里了。 - -我们生产环境就是按照上述要求配置的,这样配置之后,至少在 Kafka broker 端就可以保证在 leader 所在 broker 发生故障,进行 leader 切换时,数据不会丢失。 - -#### 生产者会不会弄丢数据? - -如果按照上述的思路设置了 `acks=all`,一定不会丢,要求是,你的 leader 接收到消息,所有的 follower 都同步到了消息之后,才认为本次写成功了。如果没满足这个条件,生产者会自动不断的重试,重试无限次。 - -## 如何保证消息的顺序性? - -我举个例子,我们以前做过一个 mysql `binlog` 同步的系统,压力还是非常大的,日同步数据要达到上亿,就是说数据从一个 mysql 库原封不动地同步到另一个 mysql 库里面去(mysql -> mysql)。常见的一点在于说比如大数据 team,就需要同步一个 mysql 库过来,对公司的业务系统的数据做各种复杂的操作。 - -你在 mysql 里增删改一条数据,对应出来了增删改 3 条 `binlog` 日志,接着这三条 `binlog` 发送到 MQ 里面,再消费出来依次执行,起码得保证人家是按照顺序来的吧?不然本来是:增加、修改、删除;你楞是换了顺序给执行成删除、修改、增加,不全错了么。 - -本来这个数据同步过来,应该最后这个数据被删除了;结果你搞错了这个顺序,最后这个数据保留下来了,数据同步就出错了。 - -先看看顺序会错乱的俩场景: - -- **RabbitMQ**:一个 queue,多个 consumer。比如,生产者向 RabbitMQ 里发送了三条数据,顺序依次是 data1/data2/data3,压入的是 RabbitMQ 的一个内存队列。有三个消费者分别从 MQ 中消费这三条数据中的一条,结果消费者 2 先执行完操作,把 data2 存入数据库,然后是 data1/data3。这不明显乱了。 - -![img](https://github.com/doocs/advanced-java/blob/master/images/rabbitmq-order-01.png) - -- **Kafka**:比如说我们建了一个 topic,有三个 partition。生产者在写的时候,其实可以指定一个 key,比如说我们指定了某个订单 id 作为 key,那么这个订单相关的数据,一定会被分发到同一个 partition 中去,而且这个 partition 中的数据一定是有顺序的。 - 消费者从 partition 中取出来数据的时候,也一定是有顺序的。到这里,顺序还是 ok 的,没有错乱。接着,我们在消费者里可能会搞**多个线程来并发处理消息**。因为如果消费者是单线程消费处理,而处理比较耗时的话,比如处理一条消息耗时几十 ms,那么 1 秒钟只能处理几十条消息,这吞吐量太低了。而多个线程并发跑的话,顺序可能就乱掉了。 - -![img](https://github.com/doocs/advanced-java/blob/master/images/kafka-order-01.png) - -### 解决方案 - -#### RabbitMQ - -![img](https://github.com/doocs/advanced-java/blob/master/images/rabbitmq-order-02.png) - -#### Kafka - -- 一个 topic,一个 partition,一个 consumer,内部单线程消费,单线程吞吐量太低,一般不会用这个。 -- 写 N 个内存 queue,具有相同 key 的数据都到同一个内存 queue;然后对于 N 个线程,每个线程分别消费一个内存 queue 即可,这样就能保证顺序性。 - -![img](https://github.com/doocs/advanced-java/blob/master/images/kafka-order-02.png) - -## 如何解决消息队列的延时以及过期失效问题?消息队列满了以后该怎么处理?有几百万消息持续积压几小时,说说怎么解决? - -你看这问法,其实本质针对的场景,都是说,可能你的消费端出了问题,不消费了;或者消费的速度极其慢。接着就坑爹了,可能你的消息队列集群的磁盘都快写满了,都没人消费,这个时候怎么办?或者是这整个就积压了几个小时,你这个时候怎么办?或者是你积压的时间太长了,导致比如 RabbitMQ 设置了消息过期时间后就没了怎么办? - -所以就这事儿,其实线上挺常见的,一般不出,一出就是大 case。一般常见于,举个例子,消费端每次消费之后要写 mysql,结果 mysql 挂了,消费端 hang 那儿了,不动了;或者是消费端出了个什么岔子,导致消费速度极其慢。 - -### 面试题剖析 - -关于这个事儿,我们一个一个来梳理吧,先假设一个场景,我们现在消费端出故障了,然后大量消息在 mq 里积压,现在出事故了,慌了。 - -### 大量消息在 mq 里积压了几个小时了还没解决 - -几千万条数据在 MQ 里积压了七八个小时,从下午 4 点多,积压到了晚上 11 点多。这个是我们真实遇到过的一个场景,确实是线上故障了,这个时候要不然就是修复 consumer 的问题,让它恢复消费速度,然后傻傻的等待几个小时消费完毕。这个肯定不能在面试的时候说吧。 - -一个消费者一秒是 1000 条,一秒 3 个消费者是 3000 条,一分钟就是 18 万条。所以如果你积压了几百万到上千万的数据,即使消费者恢复了,也需要大概 1 小时的时间才能恢复过来。 - -一般这个时候,只能临时紧急扩容了,具体操作步骤和思路如下: - -- 先修复 consumer 的问题,确保其恢复消费速度,然后将现有 consumer 都停掉。 -- 新建一个 topic,partition 是原来的 10 倍,临时建立好原先 10 倍的 queue 数量。 -- 然后写一个临时的分发数据的 consumer 程序,这个程序部署上去消费积压的数据,**消费之后不做耗时的处理**,直接均匀轮询写入临时建立好的 10 倍数量的 queue。 -- 接着临时征用 10 倍的机器来部署 consumer,每一批 consumer 消费一个临时 queue 的数据。这种做法相当于是临时将 queue 资源和 consumer 资源扩大 10 倍,以正常的 10 倍速度来消费数据。 -- 等快速消费完积压数据之后,**得恢复原先部署的架构**,**重新**用原先的 consumer 机器来消费消息。 - -### mq 中的消息过期失效了 - -假设你用的是 RabbitMQ,RabbtiMQ 是可以设置过期时间的,也就是 TTL。如果消息在 queue 中积压超过一定的时间就会被 RabbitMQ 给清理掉,这个数据就没了。那这就是第二个坑了。这就不是说数据会大量积压在 mq 里,而是**大量的数据会直接搞丢**。 - -这个情况下,就不是说要增加 consumer 消费积压的消息,因为实际上没啥积压,而是丢了大量的消息。我们可以采取一个方案,就是**批量重导**,这个我们之前线上也有类似的场景干过。就是大量积压的时候,我们当时就直接丢弃数据了,然后等过了高峰期以后,比如大家一起喝咖啡熬夜到晚上 12 点以后,用户都睡觉了。这个时候我们就开始写程序,将丢失的那批数据,写个临时程序,一点一点的查出来,然后重新灌入 mq 里面去,把白天丢的数据给他补回来。也只能是这样了。 - -假设 1 万个订单积压在 mq 里面,没有处理,其中 1000 个订单都丢了,你只能手动写程序把那 1000 个订单给查出来,手动发到 mq 里去再补一次。 - -### mq 都快写满了 - -如果消息积压在 mq 里,你很长时间都没有处理掉,此时导致 mq 都快写满了,咋办?这个还有别的办法吗?没有,谁让你第一个方案执行的太慢了,你临时写程序,接入数据来消费,**消费一个丢弃一个,都不要了**,快速消费掉所有的消息。然后走第二个方案,到了晚上再补数据吧。 - -## 如果让你写一个消息队列,该如何进行架构设计啊?说一下你的思路。 - -### 面试官心理分析 - -其实聊到这个问题,一般面试官要考察两块: - -- 你有没有对某一个消息队列做过较为深入的原理的了解,或者从整体了解把握住一个消息队列的架构原理。 -- 看看你的设计能力,给你一个常见的系统,就是消息队列系统,看看你能不能从全局把握一下整体架构设计,给出一些关键点出来。 - -说实话,问类似问题的时候,大部分人基本都会蒙,因为平时从来没有思考过类似的问题,大多数人就是平时埋头用,从来不去思考背后的一些东西。类似的问题,比如,如果让你来设计一个 Spring 框架你会怎么做?如果让你来设计一个 Dubbo 框架你会怎么做?如果让你来设计一个 MyBatis 框架你会怎么做? - -### 面试题剖析 - -其实回答这类问题,说白了,不求你看过那技术的源码,起码你要大概知道那个技术的基本原理、核心组成部分、基本架构构成,然后参照一些开源的技术把一个系统设计出来的思路说一下就好。 - -比如说这个消息队列系统,我们从以下几个角度来考虑一下: - -- 首先这个 mq 得支持可伸缩性吧,就是需要的时候快速扩容,就可以增加吞吐量和容量,那怎么搞?设计个分布式的系统呗,参照一下 kafka 的设计理念,broker -> topic -> partition,每个 partition 放一个机器,就存一部分数据。如果现在资源不够了,简单啊,给 topic 增加 partition,然后做数据迁移,增加机器,不就可以存放更多数据,提供更高的吞吐量了? -- 其次你得考虑一下这个 mq 的数据要不要落地磁盘吧?那肯定要了,落磁盘才能保证别进程挂了数据就丢了。那落磁盘的时候怎么落啊?顺序写,这样就没有磁盘随机读写的寻址开销,磁盘顺序读写的性能是很高的,这就是 kafka 的思路。 -- 其次你考虑一下你的 mq 的可用性啊?这个事儿,具体参考之前可用性那个环节讲解的 kafka 的高可用保障机制。多副本 -> leader & follower -> broker 挂了重新选举 leader 即可对外服务。 -- 能不能支持数据 0 丢失啊?可以的,参考我们之前说的那个 kafka 数据零丢失方案。 - -mq 肯定是很复杂的,面试官问你这个问题,其实是个开放题,他就是看看你有没有从架构角度整体构思和设计的思维以及能力。确实这个问题可以刷掉一大批人,因为大部分人平时不思考这些东西。 diff --git "a/docs/14.\344\270\255\351\227\264\344\273\266/01.MQ/02.\346\266\210\346\201\257\351\230\237\345\210\227\345\237\272\346\234\254\345\216\237\347\220\206.md" "b/docs/14.\344\270\255\351\227\264\344\273\266/01.MQ/02.\346\266\210\346\201\257\351\230\237\345\210\227\345\237\272\346\234\254\345\216\237\347\220\206.md" deleted file mode 100644 index fa5d6f9c..00000000 --- "a/docs/14.\344\270\255\351\227\264\344\273\266/01.MQ/02.\346\266\210\346\201\257\351\230\237\345\210\227\345\237\272\346\234\254\345\216\237\347\220\206.md" +++ /dev/null @@ -1,571 +0,0 @@ ---- -title: 消息队列基本原理 -categories: - - 编程 - - Java - - 中间件 - - MQ -tags: - - Java - - 中间件 - - MQ -abbrlink: 2d0902c8 -date: 2019-07-05 15:11:00 -permalink: /pages/055069/ ---- - -# 消息队列基本原理 - -> 消息队列(Message Queue,简称 MQ)技术是**应用间交换信息**的一种技术。 -> -> 消息队列主要解决异步处理、应用间耦合,流量削锋等问题,实现高性能,高可用,可伸缩和最终一致性架构。是大型分布式系统不可缺少的中间件。 -> -> 目前主流的 MQ 有:Kafka、RabbitMQ、RocketMQ、ActiveMQ,而部分数据库如 Redis、MySQL 以及 phxsql 也可实现消息队列的功能。 -> -> 注意:_为了简便,下文中除了文章标题,一律使用 MQ 简称_。 - -## MQ 的简介 - -### 什么是 MQ - -消息队列(Message Queue,简称 MQ)技术是应用间交换信息的一种技术。 - -消息队列主要解决应用耦合,异步消息,流量削锋等问题,实现高性能,高可用,可伸缩和最终一致性架构。是大型分布式系统不可缺少的中间件。 - -MQ 是消费-生产者模型的一个典型的代表,一端往消息队列中不断写入消息,而另一端则可以读取队列中的消息。消息发布者只管把消息发布到 MQ 中而不用管谁来取,消息使用者只管从 MQ 中取消息而不管是谁发布的。这样发布者和使用者都不用知道对方的存在。 - -MQ 的数据可驻留在内存或磁盘上,直到它们被应用程序读取。通过 MQ,应用程序可独立地执行,它们不需要知道彼此的位置,不需要等待接收程序接收此消息。在分布式计算环境中,为了集成分布式应用,开发者需要对异构网络环境下的分布式应用提供有效的通信手段。为了管理需要共享的信息,对应用提供公共的信息交换机制是重要的。 - -目前主流的 MQ 有:Kafka、RabbitMQ、RocketMQ、ActiveMQ。 - -### MQ 通信模型 - -MQ 通信模型大致有以下类型: - -- **点对点** - 点对点方式是最为传统和常见的通讯方式,它支持一对一、一对多、多对多、多对一等多种配置方式,支持树状、网状等多种拓扑结构。 -- **多点广播** - MQ 适用于不同类型的应用。其中重要的,也是正在发展中的是"多点广播"应用,即能够将消息发送到多个目标站点 (Destination List)。可以使用一条 MQ 指令将单一消息发送到多个目标站点,并确保为每一站点可靠地提供信息。MQ 不仅提供了多点广播的功能,而且还拥有智能消息分发功能,在将一条消息发送到同一系统上的多个用户时,MQ 将消息的一个复制版本和该系统上接收者的名单发送到目标 MQ 系统。目标 MQ 系统在本地复制这些消息,并将它们发送到名单上的队列,从而尽可能减少网络的传输量。 -- **发布/订阅 (Publish/Subscribe)** - 发布/订阅模式使消息的分发可以突破目的队列地理位置的限制,使消息按照特定的主题甚至内容进行分发,用户或应用程序可以根据主题或内容接收到所需要的消息。发布/订阅模式使得发送者和接收者之间的耦合关系变得更为松散,发送者不必关心接收者的目的地址,而接收者也不必关心消息的发送地址,而只是根据消息的主题进行消息的收发。 -- **集群 (Cluster)** - 为了简化点对点通讯模式中的系统配置,MQ 提供 Cluster(集群) 的解决方案。集群类似于一个域 (Domain),集群内部的队列管理器之间通讯时,不需要两两之间建立消息通道,而是采用集群 (Cluster) 通道与其它成员通讯,从而大大简化了系统配置。此外,集群中的队列管理器之间能够自动进行负载均衡,当某一队列管理器出现故障时,其它队列管理器可以接管它的工作,从而大大提高系统的高可靠性。 - -## MQ 的应用 - -### 异步处理 - -> MQ 可以将系统间的处理流程异步化,减少等待响应的时间,从而提高整体并发吞吐量。 -> -> 一般,MQ 异步处理应用于非核心流程,例如:短信/邮件通知、数据推送、上报数据到监控中心/日志中心等。 - -假设这样一个场景,用户向系统 A 发起请求,系统 A 处理计算只需要 10 ms,然后通知系统 BCD 写库,系统 BCD 写库耗时分别为:100ms、200ms、300ms。最终总耗时为: 10+100ms+200ms+300ms=610ms。此外,加上请求和响应的网络传输时间,从用户角度看,可能要等待将近 1s 才能得到结果。 - -![img](https://raw.githubusercontent.com/dunwu/images/dev/cs/design/theory/mq/mq_3.png) - -如果使用 MQ,系统 A 接到请求后,耗时 10ms 处理计算,然后向系统 BCD 连续发送消息,假设耗时 5ms。那么 这一过程的总耗时为 3ms + 5ms = 8ms,这相比于 610 ms,大大缩短了响应时间。至于系统 BCD 的写库操作,只要自行消费 MQ 后处理即可,用户无需关注。 - -![img](https://raw.githubusercontent.com/dunwu/images/dev/cs/design/theory/mq/mq_4.png) - -### 系统解耦 - -> 通过 MQ,可以消除系统间的强耦合。它的好处在于: -> -> - 消息的消费者系统可以随意增加,无需修改生产者系统的代码。 -> - 生产者系统、消费者系统彼此不会影响对方的流程。 -> - 如果生产者系统宕机,消费者系统收不到消息,就不会有下一步的动作。 -> - 如果消费者系统宕机,生产者系统让然可以正常发送消息,不影响流程。 - -不同系统如果要建立通信,传统的做法是:调用接口。 - -如果需要和新的系统建立通信或删除已建立的通信,都需要修改代码,这种方案显然耦合度很高。 - -![img](https://raw.githubusercontent.com/dunwu/images/dev/cs/design/theory/mq/mq_1.png) - -如果使用 MQ,系统间的通信只需要通过发布/订阅(Pub/Sub)模型即可,彼此没有直接联系,也就不需要相互感知,从而达到 **解耦**。 - -![img](https://raw.githubusercontent.com/dunwu/images/dev/cs/design/theory/mq/mq_2.png) - -### 流量削峰 - -> 当 **上下游系统** 处理能力存在差距的时候,利用 MQ 做一个 “**漏斗**” 模型,进行 **流控**。把 MQ 当成可靠的 **消息暂存地**,进行一定程度的 **消息堆积**;在下游有能力处理的时候,再发送消息。 -> -> MQ 的流量削峰常用于高并发场景(例如:秒杀、团抢等业务场景),它是缓解瞬时暴增流量的核心手段之一。 -> -> 如果没有 MQ,两个系统之间通过 **协商**、**滑动窗口**、**限流**/**降级**/**熔断** 等复杂的方案也能实现 **流控**。但 **系统复杂性** 指数级增长,势必在上游或者下游做存储,并且要处理 **定时**、**拥塞** 等一系列问题。而且每当有 **处理能力有差距** 的时候,都需要 **单独** 开发一套逻辑来维护这套逻辑。 - -假设某个系统读写数据库的稳定性能为每秒处理 1000 条数据。平常情况下,远远达不到这么大的处理量。假设,因为因为做活动,系统的瞬时请求量剧增,达到每秒 10000 个并发请求,数据库根本承受不了,可能直接就把数据库给整崩溃了,这样系统服务就不可用了。 - -![img](https://raw.githubusercontent.com/dunwu/images/dev/cs/design/theory/mq/mq_5.png) - -如果使用 MQ,每秒写入 10000 条请求,但是系统 A 每秒只从 MQ 中消费 1000 条请求,然后写入数据库。这样,就不会超过数据库的承受能力,而是把请求积压在 MQ 中。只要高峰期一过,系统 A 就会很快把积压的消息给处理掉。 - -![img](https://raw.githubusercontent.com/dunwu/images/dev/cs/design/theory/mq/mq_6.png) - -### 传输缓冲 - -(1)MQ 常被用于做海量数据的传输缓冲。 - -例如,Kafka 常被用于做为各种日志数据、采集数据的数据中转。然后,Kafka 将数据转发给 Logstash、Elasticsearch 中,然后基于 Elasticsearch 来做日志中心,提供检索、聚合、分析日志的能力。开发者可以通过 Kibana 集成 Elasticsearch 数据进行可视化展示,或自行进行定制化开发。 - -![img](https://raw.githubusercontent.com/dunwu/images/dev/snap/20200930164342.png) - -(2)MQ 也可以被用于流式处理。 - -例如,Kafka 几乎已经是流计算的数据采集端的标准组件。而流计算通过实时数据处理能力,提供了更为快捷的聚合计算能力,被大量应用于链路监控、实时监控、实时数仓、实时大屏、风控、推荐等应用领域。 - -### 最终一致性 - -**最终一致性** 不是 **消息队列** 的必备特性,但确实可以依靠 **消息队列** 来做 **最终一致性** 的事情。 - -- **先写消息再操作**,确保操作完成后再修改消息状态。**定时任务补偿机制** 实现消息 **可靠发送接收**、业务操作的可靠执行,要注意 **消息重复** 与 **幂等设计**。 -- 所有不保证 `100%` **不丢消息** 的消息队列,理论上无法实现 **最终一致性**。 - -> 像 `Kafka` 一类的设计,在设计层面上就有 **丢消息** 的可能(比如 **定时刷盘**,如果掉电就会丢消息)。哪怕只丢千分之一的消息,业务也必须用其他的手段来保证结果正确。 - -### 系统间通信 - -消息队列一般都内置了 **高效的通信机制**,因此也可以用于单纯的 **消息通讯**,比如实现 **点对点消息队列** 或者 **聊天室** 等。 - -**生产者/消费者** 模式,只需要关心消息是否 **送达队列**,至于谁希望订阅和需要消费,是 **下游** 的事情,无疑极大地减少了开发和联调的工作量。 - -## MQ 的问题 - -任何技术都会有利有弊,MQ 给整体系统架构带来很多好处,但也会付出一定的代价。 - -MQ 主要引入了以下问题: - -- **系统可用性降低**:引入了 MQ 后,通信需要基于 MQ 完成,如果 MQ 宕机,则服务不可用。因此,MQ 要保证是高可用的,详情参考:[MQ 的高可用](#MQ-的高可用) -- **系统复杂度提高**:使用 MQ,需要关注一些新的问题: - - 如何保证消息没有 **重复消费**? - - 如何处理 **消息丢失** 的问题? - - 如何保证传递 **消息的顺序性**? - - 如何处理大量 **消息积压** 的问题? -- **一致性问题**:假设系统 A 处理完直接返回成功的结果给用户,用户认为请求成功。但如果此时,系统 BCD 中只要有任意一个写库失败,那么数据就不一致了。这种情况如何处理? - -下面,我们针对以上问题来一一分析。 - -### 重复消费 - -**如何保证消息不被重复消费** 和 **如何保证消息消费的幂等性** 是同一个问题。 - -必须先明确产生重复消费的原因,才能对症下药。 - -#### 重复消费问题原因 - -重复消费问题通常不是 MQ 来处理,而是由开发来处理的。 - -以 Kafka 举例,Kafka 每个 Partition 都是一个有序的、不可变的记录序列,不断追加到结构化的提交日志中。Partition 中为每条记录分配一个连续的 id 号,称为偏移量(Offset),用于唯一标识 Partition 内的记录。 - -Kafka 的客户端和 Broker 都会保存 Offset。客户端消费消息后,每隔一段时间,就把已消费的 Offset 提交给 Kafka Broker,表示已消费。 - -![img](https://raw.githubusercontent.com/dunwu/images/dev/snap/20210427194009.png) - -在这个过程中,如果客户端应用消费消息后,因为宕机、重启等情况而没有提交已消费的 Offset 。当系统恢复后,会继续消费消息,由于 Offset 未提交,就会出现重复消费的问题。 - -#### 重复消费解决方案 - -应对重复消费问题,需要在业务层面,通过 **幂等性设计** 来解决。 - -MQ 重复消费不可怕,可怕的是没有应对机制,可以借鉴的思路有: - -- 如果是写关系型数据库,可以先根据主键查询,判断数据是否已存在,存在则更新,不存在则插入; -- 如果是写 Redis,由于 set 操作天然具有幂等性,所以什么都不用做; -- 如果是根据消息做较复杂的逻辑处理,可以在消息中加入全局唯一 ID,例如:订单 ID 等。在客户端存储中(Mysql、Redis 等)保存已消费消息的 ID。一旦接受到新消息,先判断消息中的 ID 是否在已消费消息 ID 表中存在,存在则不再处理,不存在则处理。 - -在实际开发中,可以参考上面的例子,结合现实场景,设计合理的幂等性方案。 - -### 消息丢失 - -**如何处理消息丢失的问题** 和 **如何保证消息不被重复消费** 是同一个问题。关注点有: - -- MQ Server 丢失数据 -- 消费方丢失数据 -- 生产方丢失数据 - -#### 消费方丢失数据 - -唯一可能导致消费方丢失数据的情况是:消费方设置了**自动提交 Offset**。一旦设置了自动提交 Offset,接受到消息后就会自动提交 Offset 给 Kafka ,Kafka 就认为消息已被消费。如果此时,消费方尚未来得及处理消息就挂了,那么消息就丢了。 - -解决方法就是:消费方关闭自动提交 Offset,处理完消息后**手动提交 Offset**。但这种情况下可能会出现重复消费的情形,需要自行保证幂等性。 - -#### Kafka Server 丢失数据 - -当 Kafka 某个 Broker 宕机,需要重新选举 Partition 的 Leader。若此时其他的 Follower 尚未同步 Leader 的数据,那么新选某个 Follower 为 Leader 后,就丢失了部分数据。 - -为此,一般要求至少设置 4 个参数: - -- 给 Topic 设置 `replication.factor` 参数 - 这个值必须大于 1,要求每个 Partition 必须有至少 2 个副本。 -- 在 Kafka 服务端设置 `min.insync.replicas` 参数 - 这个值必须大于 1,这是要求一个 Leader 需要和至少一个 Follower 保持通信,这样才能确保 Leader 挂了还有替补。 -- 在 Producer 端设置 `acks=all` - 这意味着:要求每条数据,必须是**写入所有 replica 之后,才能认为写入成功了**。 -- 在 Producer 端设置 `retries=MAX`(很大很大很大的一个值,无限次重试的意思) - 这意味着**要求一旦写入失败,就无限重试**,卡在这里了。 - -#### 生产方丢失数据 - -如果按照上述的思路设置了 `acks=all`,生产方一定不会丢数据。 - -要求是,你的 Leader 接收到消息,所有的 Follower 都同步到了消息之后,才认为本生产消息成功了。如果未满足这个条件,生产者会自动不断的重试,重试无限次。 - -### 消息的顺序性 - -要保证 MQ 的顺序性,势必要付出一定的代价,所以实施方案前,要先明确业务场景是不是有必要保证消息的顺序性。只有那些明确对消息处理顺序有要求的业务场景才值得去保证消息顺序性。 - -方案一 - -一个 Topic,一个 Partition,一个 Consumer,内部单线程消费,单线程吞吐量太低,一般不会用这个。 - -方案二 - -- 写入数据到 Partition 时指定一个全局唯一的 ID,例如订单 ID。发送方保证相同 ID 的消息有序的发送到同一个 Partition。 -- 基于上一点,消费方从 Kafka Partition 中消费消息时,此刻一定是顺序的。但如果消费方式以并发方式消费消息,顺序就可能会被打乱。为此,还有做到以下几点: - - 消费方维护 N 个缓存队列,具有相同 ID 的数据都写入同一个队列中; - - 创建 N 个线程,每个线程只负责从指定的一个队列中取数据。 - -![img](https://raw.githubusercontent.com/dunwu/images/dev/snap/20210427194215.png) - -### 消息积压 - -假设一个 MQ 消费者可以一秒处理 1000 条消息,三个 MQ 消费者可以一秒处理 3000 条消息,那么一分钟的处理量是 18 万条。如果 MQ 中积压了几百万到上千万的数据,即使消费者恢复了,也需要大概很长的时间才能恢复过来。 - -对于产线环境来说,漫长的等待是不可接受的,所以面临这种窘境时,只能临时紧急扩容以应对了,具体操作步骤和思路如下: - -- 先修复 Consumer 的问题,确保其恢复消费速度,然后将现有 Consumer 都停掉。 -- 新建一个 Topic,Partition 是原来的 10 倍,临时建立好原先 10 倍的 Queue 数量。 -- 然后写一个临时的分发数据的 Consumer 程序,这个程序部署上去消费积压的数据,**消费之后不做耗时的处理**,直接均匀轮询写入临时建立好的 10 倍数量的 Queue。 -- 接着临时征用 10 倍的机器来部署 Consumer ,每一批 Consumer 消费一个临时 Queue 的数据。这种做法相当于是临时将 Queue 资源和 Consumer 资源扩大 10 倍,以正常的 10 倍速度来消费数据。 -- 等快速消费完积压数据之后,**得恢复原先部署的架构**,**重新**用原先的 consumer 机器来消费消息。 - -## MQ 的高可用 - -不同 MQ 实现高可用的原理各不相同。因为 Kafka 比较具有代表性,所以这里以 Kafka 为例。 - -### Kafka 的高可用 - -#### Kafka 的核心概念 - -了解 Kafka,必须先了解 Kafka 的核心概念: - -- **Broker** - Kafka 集群包含一个或多个节点,这种节点被称为 Broker。 - -- **Topic** - 每条发布到 Kafka 集群的消息都有一个类别,这个类别被称为 Topic。(不同 Topic 的消息是物理隔离的;同一个 Topic 的消息保存在一个或多个 Broker 上,但用户只需指定消息的 Topic 即可生产或消费数据而不必关心数据存于何处)。对于每一个 Topic, Kafka 集群都会维持一个分区日志。 - -- **Partition** - 了提高 Kafka 的吞吐率,每个 Topic 包含一个或多个 Partition,每个 Partition 在物理上对应一个文件夹,该文件夹下存储这个 Partition 的所有消息和索引文件。 - - - Kafka 日志的分区(Partition)分布在 Kafka 集群的节点上。每个节点在处理数据和请求时,共享这些分区。每一个分区都会在已配置的节点上进行备份,确保容错性。 - -![img](https://raw.githubusercontent.com/dunwu/images/dev/cs/java/javaweb/distributed/mq/kafka/kafka-cluster-roles.png) - -#### Kafka 的副本机制 - -Kafka 是如何实现高可用的呢? - -Kafka 在 0.8 以前的版本中,如果一个 Broker 宕机了,其上面的 Partition 都不能用了,这自然不是高可用的。 - -为了实现高可用,Kafka 引入了复制功能。 - -简单来说,就是副本机制( Replicate )。 - -**每个 Partition 都有一个 Leader,零个或多个 Follower**。Leader 和 Follower 都是 Broker,每个 Broker 都会成为某些分区的 Leader 和某些分区的 Follower,因此集群的负载是平衡的。 - -- **Leader 处理一切对 Partition (分区)的读写请求**; -- **而 Follower 只需被动的同步 Leader 上的数据**。 - -**同一个 Topic 的不同 Partition 会分布在多个 Broker 上,而且一个 Partition 还会在其他的 Broker 上面进行备份**,Producer 在发布消息到某个 Partition 时,先找到该 Partition 的 Leader,然后向这个 Leader 推送消息;每个 Follower 都从 Leader 拉取消息,拉取消息成功之后,向 Leader 发送一个 ACK 确认。 - -![img](https://raw.githubusercontent.com/dunwu/images/dev/cs/java/javaweb/distributed/mq/kafka/kafka-replication.png) - -> FAQ -> -> 问:为什么让 Leader 处理一切对对 Partition (分区)的读写请求? -> -> 答:因为如果允许所有 Broker 都可以处理读写请求,就可能产生数据一致性问题。 - -#### Kafka 选举 Leader - -由上文可知,Partition 在多个 Broker 上存在副本。 - -如果某个 Follower 宕机,啥事儿没有,正常工作。 - -如果 Leader 宕机了,会从 Follower 中**重新选举**一个新的 Leader。 - -## 主流 MQ - -### ActiveMQ - -`ActiveMQ` 是由 `Apache` 出品,`ActiveMQ` 是一个完全支持`JMS1.1` 和 `J2EE 1.4` 规范的 `JMS Provider` 实现。它非常快速,支持 **多种语言的客户端** 和 **协议**,而且可以非常容易的嵌入到企业的应用环境中,并有许多高级功能。 - -![img](https://user-gold-cdn.xitu.io/2018/7/8/16479c8ea7cdc2c0?imageView2/0/w/1280/h/960/format/webp/ignore-error/1) - -#### (a) 主要特性 - -1. **服从 JMS 规范**:`JMS` 规范提供了良好的标准和保证,包括:**同步** 或 **异步** 的消息分发,一次和仅一次的消息分发,**消息接收** 和 **订阅** 等等。遵从 `JMS` 规范的好处在于,不论使用什么 `JMS` 实现提供者,这些基础特性都是可用的; -2. **连接灵活性**:`ActiveMQ` 提供了广泛的 **连接协议**,支持的协议有:`HTTP/S`,`IP` **多播**,`SSL`,`TCP`,`UDP` 等等。对众多协议的支持让 `ActiveMQ` 拥有了很好的灵活性; -3. **支持的协议种类多**:`OpenWire`、`STOMP`、`REST`、`XMPP`、`AMQP`; -4. **持久化插件和安全插件**:`ActiveMQ` 提供了 **多种持久化** 选择。而且,`ActiveMQ` 的安全性也可以完全依据用户需求进行 **自定义鉴权** 和 **授权**; -5. **支持的客户端语言种类多**:除了 `Java` 之外,还有:`C/C++`,`.NET`,`Perl`,`PHP`,`Python`,`Ruby`; -6. **代理集群**:多个 `ActiveMQ` **代理** 可以组成一个 **集群** 来提供服务; -7. **异常简单的管理**:`ActiveMQ` 是以开发者思维被设计的。所以,它并不需要专门的管理员,因为它提供了简单又使用的管理特性。有很多中方法可以 **监控** `ActiveMQ` 不同层面的数据,包括使用在 `JConsole` 或者在 `ActiveMQ` 的 `Web Console` 中使用 `JMX`。通过处理 `JMX` 的告警消息,通过使用 **命令行脚本**,甚至可以通过监控各种类型的 **日志**。 - -#### (b) 部署环境 - -`ActiveMQ` 可以运行在 `Java` 语言所支持的平台之上。使用 `ActiveMQ` 需要: - -- `Java JDK` -- `ActiveMQ` 安装包 - -#### (c) 优点 - -1. **跨平台** (`JAVA` 编写与平台无关,`ActiveMQ` 几乎可以运行在任何的 `JVM` 上); -2. 可以用 `JDBC`:可以将 **数据持久化** 到数据库。虽然使用 `JDBC` 会降低 `ActiveMQ` 的性能,但是数据库一直都是开发人员最熟悉的存储介质; -3. 支持 `JMS` 规范:支持 `JMS` 规范提供的 **统一接口**; -4. 支持 **自动重连** 和 **错误重试机制**; -5. 有安全机制:支持基于 `shiro`,`jaas` 等多种 **安全配置机制**,可以对 `Queue/Topic` 进行 **认证和授权**; -6. 监控完善:拥有完善的 **监控**,包括 `Web Console`,`JMX`,`Shell` 命令行,`Jolokia` 的 `RESTful API`; -7. 界面友善:提供的 `Web Console` 可以满足大部分情况,还有很多 **第三方的组件** 可以使用,比如 `hawtio`; - -#### (d) 缺点 - -1. 社区活跃度不及 `RabbitMQ` 高; -2. 根据其他用户反馈,会出莫名其妙的问题,会 **丢失消息**; -3. 目前重心放到 `activemq 6.0` 产品 `Apollo`,对 `5.x` 的维护较少; -4. 不适合用于 **上千个队列** 的应用场景; - -### RabbitMQ - -`RabbitMQ` 于 `2007` 年发布,是一个在 `AMQP` (**高级消息队列协议**)基础上完成的,可复用的企业消息系统,是当前最主流的消息中间件之一。 - -![img](https://user-gold-cdn.xitu.io/2018/7/8/16479c8ece3b5d7a?imageView2/0/w/1280/h/960/format/webp/ignore-error/1) - -#### (a) 主要特性 - -1. **可靠性**:提供了多种技术可以让你在 **性能** 和 **可靠性** 之间进行 **权衡**。这些技术包括 **持久性机制**、**投递确认**、**发布者证实** 和 **高可用性机制**; -2. **灵活的路由**:消息在到达队列前是通过 **交换机** 进行 **路由** 的。`RabbitMQ` 为典型的路由逻辑提供了 **多种内置交换机** 类型。如果你有更复杂的路由需求,可以将这些交换机组合起来使用,你甚至可以实现自己的交换机类型,并且当做 `RabbitMQ` 的 **插件** 来使用; -3. **消息集群**:在相同局域网中的多个 `RabbitMQ` 服务器可以 **聚合** 在一起,作为一个独立的逻辑代理来使用; -4. **队列高可用**:队列可以在集群中的机器上 **进行镜像**,以确保在硬件问题下还保证 **消息安全**; -5. **支持多种协议**:支持 **多种消息队列协议**; -6. **支持多种语言**:用 `Erlang` 语言编写,支持只要是你能想到的 **所有编程语言**; -7. **管理界面**: `RabbitMQ` 有一个易用的 **用户界面**,使得用户可以 **监控** 和 **管理** 消息 `Broker` 的许多方面; -8. **跟踪机制**:如果 **消息异常**,`RabbitMQ` 提供消息跟踪机制,使用者可以找出发生了什么; -9. **插件机制**:提供了许多 **插件**,来从多方面进行扩展,也可以编写自己的插件。 - -#### (b) 部署环境 - -`RabbitMQ` 可以运行在 `Erlang` 语言所支持的平台之上,包括 `Solaris`,`BSD`,`Linux`,`MacOSX`,`TRU64`,`Windows` 等。使用 `RabbitMQ` 需要: - -- `ErLang` 语言包 -- `RabbitMQ` 安装包 - -#### (c) 优点 - -1. 由于 `Erlang` 语言的特性,消息队列性能较好,支持 **高并发**; -2. 健壮、稳定、易用、**跨平台**、支持 **多种语言**、文档齐全; -3. 有消息 **确认机制** 和 **持久化机制**,可靠性高; -4. 高度可定制的 **路由**; -5. **管理界面** 较丰富,在互联网公司也有较大规模的应用,社区活跃度高。 - -#### (d) 缺点 - -1. 尽管结合 `Erlang` 语言本身的并发优势,性能较好,但是不利于做 **二次开发和维护**; -2. 实现了 **代理架构**,意味着消息在发送到客户端之前可以在 **中央节点** 上排队。此特性使得 `RabbitMQ` 易于使用和部署,但是使得其 **运行速度较慢**,因为中央节点 **增加了延迟**,**消息封装后** 也比较大; -3. 需要学习 **比较复杂** 的 **接口和协议**,学习和维护成本较高。 - -### RocketMQ - -`RocketMQ` 出自 **阿里** 的开源产品,用 `Java` 语言实现,在设计时参考了 `Kafka`,并做出了自己的一些改进,**消息可靠性上** 比 `Kafka` 更好。`RocketMQ` 在阿里内部  被广泛应用在 **订单**,**交易**,**充值**,**流计算**,**消息推送**,**日志流式处理**,`binglog` **分发** 等场景。 - -#### (a) 主要特性 - -![img](data:image/svg+xml;utf8,) - -1. 基于 **队列模型**:具有 **高性能**、**高可靠**、**高实时**、**分布式** 等特点; -2. `Producer`、`Consumer`、**队列** 都支持 **分布式**; -3. `Producer` 向一些队列轮流发送消息,**队列集合** 称为 `Topic`。`Consumer` 如果做 **广播消费**,则一个 `Consumer` 实例消费这个 `Topic` 对应的 **所有队列**;如果做 **集群消费**,则 **多个** `Consumer` 实例 **平均消费** 这个 `Topic` 对应的队列集合; -4. 能够保证 **严格的消息顺序**; -5. 提供丰富的 **消息拉取模式**; -6. 高效的订阅者 **水平扩展**能力; -7. **实时** 的 **消息订阅机制**; -8. 亿级 **消息堆积** 能力; -9. 较少的外部依赖。 - -#### (b) 部署环境 - -`RocketMQ` 可以运行在 `Java` 语言所支持的平台之上。使用 `RocketMQ` 需要: - -- `Java JDK` -- 安装 `git`、`Maven` -- `RocketMQ` 安装包 - -#### (c) 优点 - -1. **单机** 支持 `1` 万以上 **持久化队列**; -2. `RocketMQ` 的所有消息都是 **持久化的**,先写入系统 `PAGECACHE`,然后 **刷盘**,可以保证 **内存** 与 **磁盘** 都有一份数据,而 **访问** 时,直接 **从内存读取**。 -3. 模型简单,接口易用(`JMS` 的接口很多场合并不太实用); -4. **性能非常好**,可以允许 **大量堆积消息** 在 `Broker` 中; -5. 支持 **多种消费模式**,包括 **集群消费**、**广播消费**等; -6. 各个环节 **分布式扩展设计**,支持 **主从** 和 **高可用**; -7. 开发度较活跃,版本更新很快。 - -#### (d) 缺点 - -1. 支持的 **客户端语言** 不多,目前是 `Java` 及 `C++`,其中 `C++` 还不成熟; -2. `RocketMQ` 社区关注度及成熟度也不及前两者; -3. 没有 `Web` 管理界面,提供了一个 `CLI` (命令行界面) 管理工具带来 **查询**、**管理** 和 **诊断各种问题**; -4. 没有在 `MQ` 核心里实现 `JMS` 等接口; - -### Kafka - -`Apache Kafka` 是一个 **分布式消息发布订阅** 系统。它最初由 `LinkedIn` 公司基于独特的设计实现为一个 **分布式的日志提交系统** (`a distributed commit log`),之后成为 `Apache` 项目的一部分。`Kafka` **性能高效**、**可扩展良好** 并且 **可持久化**。它的 **分区特性**,**可复制** 和 **可容错** 都是其不错的特性。 - -![img](data:image/svg+xml;utf8,) - -#### (a) 主要特性 - -1. **快速持久化**:可以在 `O(1)` 的系统开销下进行 **消息持久化**; -2. **高吞吐**:在一台普通的服务器上既可以达到 `10W/s` 的 **吞吐速率**; -3. **完全的分布式系统**:`Broker`、`Producer` 和 `Consumer` 都原生自动支持 **分布式**,自动实现 **负载均衡**; -4. 支持 **同步** 和 **异步** 复制两种 **高可用机制**; -5. 支持 **数据批量发送** 和 **拉取**; -6. **零拷贝技术(zero-copy)**:减少 `IO` 操作步骤,提高 **系统吞吐量**; -7. **数据迁移**、**扩容** 对用户透明; -8. **无需停机** 即可扩展机器; -9. **其他特性**:丰富的 **消息拉取模型**、高效 **订阅者水平扩展**、实时的 **消息订阅**、亿级的 **消息堆积能力**、定期删除机制; - -#### (b) 部署环境 - -使用 `Kafka` 需要: - -- `Java JDK` -- `Kafka` 安装包 - -#### (c) 优点 - -1. **客户端语言丰富**:支持 `Java`、`.Net`、`PHP`、`Ruby`、`Python`、`Go` 等多种语言; -2. **高性能**:单机写入 `TPS` 约在 `100` 万条/秒,消息大小 `10` 个字节; -3. 提供 **完全分布式架构**,并有 `replica` 机制,拥有较高的 **可用性** 和 **可靠性**,理论上支持 **消息无限堆积**; -4. 支持批量操作; -5. **消费者** 采用 `Pull` 方式获取消息。**消息有序**,**通过控制** 能够保证所有消息被消费且仅被消费 **一次**; -6. 有优秀的第三方 `Kafka Web` 管理界面 `Kafka-Manager`; -7. 在 **日志领域** 比较成熟,被多家公司和多个开源项目使用。 - -#### (d) 缺点 - -1. `Kafka` 单机超过 `64` 个 **队列/分区** 时,`Load` 时会发生明显的飙高现象。**队列** 越多,**负载** 越高,发送消息 **响应时间变长**; -2. 使用 **短轮询方式**,**实时性** 取决于 **轮询间隔时间**; -3. 消费失败 **不支持重试**; -4. 支持 **消息顺序**,但是 **一台代理宕机** 后,就会产生 **消息乱序**; -5. 社区更新较慢。 - -### MQ 的技术选型 - -MQ 的技术选型一般要考虑以下几点: - -- **是否开源**:这决定了能否商用,所以最为重要。 -- **社区活跃度越高越好**:高社区活跃度,一般保证了低 Bug 率,因为大部分 Bug,已经有人遇到并解决了。 -- **技术生态适配性**:客户端对各种编程语言的支持。比如:如果使用 MQ 的都是 Java 应用,那么 ActiveMQ、RabbitMQ、RocketMQ、Kafka 都可以。如果需要支持其他语言,那么 RMQ 比较合适,因为它支持的编程语言比较丰富。如果 MQ 是应用于大数据或流式计算,那么 Kafka 几乎是标配。如果是应用于在线业务系统,那么 Kafka 就不合适了,可以考虑 RabbitMQ、 RocketMQ 很合适。 -- **高可用性**:应用于线上的准入标准。 -- **性能**:具备足够好的性能,能满足绝大多数场景的性能要求。 - -| 特性 | ActiveMQ | RabbitMQ | RocketMQ | Kafka | -| ------------------------ | ------------------------------------- | -------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | -| 单机吞吐量 | 万级,比 RocketMQ、Kafka 低一个数量级 | 同 ActiveMQ | 10 万级,支撑高吞吐 | 10 万级,高吞吐,一般配合大数据类的系统来进行流式计算、日志采集等场景 | -| topic 数量对吞吐量的影响 | | | topic 可以达到几百/几千的级别,吞吐量会有较小幅度的下降,这是 RocketMQ 的一大优势,在同等机器下,可以支撑大量的 topic | topic 从几十到几百个时候,吞吐量会大幅度下降,在同等机器下,Kafka 尽量保证 topic 数量不要过多,如果要支撑大规模的 topic,需要增加更多的机器资源 | -| 时效性 | ms 级 | 微秒级,这是 RabbitMQ 的一大特点,延迟最低 | ms 级 | 延迟在 ms 级以内 | -| 可用性 | 高,基于主从架构实现高可用 | 同 ActiveMQ | 非常高,分布式架构 | 非常高,分布式,一个数据多个副本,少数机器宕机,不会丢失数据,不会导致不可用 | -| 消息可靠性 | 有较低的概率丢失数据 | 基本不丢 | 经过参数优化配置,可以做到 0 丢失 | 同 RocketMQ | -| 功能支持 | MQ 领域的功能极其完备 | 基于 erlang 开发,并发能力很强,性能极好,延时很低 | MQ 功能较为完善,还是分布式的,扩展性好 | 功能较为简单,主要支持简单的 MQ 功能,在大数据领域的实时计算以及日志采集被大规模使用 | - -综上,各种对比之后,有如下建议: - -- 业务系统场景,建议使用 RocketMQ、RabbitMQ。如果所有应用都是 Java,优选 RocketMQ,因为 RocketMQ 本身就是 Java 开发的,所以最适配。如果业务中有多种编程语言的应用,建议选择 RabbitMQ。 -- 大数据和流式计算领域,或是作为日志缓冲,强烈建议选择 Kafka,业界标准,久经考验。 - -## JMS - -提到 MQ,就顺便提一下 JMS 。 - -**JMS(JAVA Message Service,java 消息服务)API 是一个消息服务的标准/规范,允许应用程序组件基于 JavaEE 平台创建、发送、接收和读取消息**。它使分布式通信耦合度更低,消息服务更加可靠以及异步性。 - -在 EJB 架构中,有消息 bean 可以无缝的与 JMS 消息服务集成。在 J2EE 架构模式中,有消息服务者模式,用于实现消息与应用直接的解耦。 - -### 消息模型 - -在 JMS 标准中,有两种消息模型: - -- P2P(Point to Point) -- Pub/Sub(Publish/Subscribe) - -#### P2P 模式 - -
-P2P 模式包含三个角色:MQ(Queue),发送者(Sender),接收者(Receiver)。每个消息都被发送到一个特定的队列,接收者从队列中获取消息。队列保留着消息,直到他们被消费或超时。 - -P2P 的特点 - -- 每个消息只有一个消费者(Consumer)(即一旦被消费,消息就不再在 MQ 中) -- 发送者和接收者之间在时间上没有依赖性,也就是说当发送者发送了消息之后,不管接收者有没有正在运行,它不会影响到消息被发送到队列 -- 接收者在成功接收消息之后需向队列应答成功 - -如果希望发送的每个消息都会被成功处理的话,那么需要 P2P 模式。 - -#### Pub/sub 模式 - -
-包含三个角色主题(Topic),发布者(Publisher),订阅者(Subscriber) 。多个发布者将消息发送到 Topic,系统将这些消息传递给多个订阅者。 - -Pub/Sub 的特点 - -- 每个消息可以有多个消费者 -- 发布者和订阅者之间有时间上的依赖性。针对某个主题(Topic)的订阅者,它必须创建一个订阅者之后,才能消费发布者的消息。 -- 为了消费消息,订阅者必须保持运行的状态。 - -为了缓和这样严格的时间相关性,JMS 允许订阅者创建一个可持久化的订阅。这样,即使订阅者没有被激活(运行),它也能接收到发布者的消息。 - -如果希望发送的消息可以不被做任何处理、或者只被一个消息者处理、或者可以被多个消费者处理的话,那么可以采用 Pub/Sub 模型。 - -### 消息消费 - -在 JMS 中,消息的产生和消费都是异步的。对于消费来说,JMS 的消息者可以通过两种方式来消费消息。 - -- **同步** - 订阅者或接收者通过 `receive` 方法来接收消息,`receive` 方法在接收到消息之前(或超时之前)将一直阻塞; -- **异步** - 订阅者或接收者可以注册为一个消息监听器。当消息到达之后,系统自动调用监听器的 `onMessage` 方法。 - -`JNDI` - Java 命名和目录接口,是一种标准的 Java 命名系统接口。可以在网络上查找和访问服务。通过指定一个资源名称,该名称对应于数据库或命名服务中的一个记录,同时返回资源连接建立所必须的信息。 - -JNDI 在 JMS 中起到查找和访问发送目标或消息来源的作用。 - -### JMS 编程模型 - -#### ConnectionFactory - -创建 Connection 对象的工厂,针对两种不同的 jms 消息模型,分别有 QueueConnectionFactory 和 TopicConnectionFactory 两种。可以通过 JNDI 来查找 ConnectionFactory 对象。 - -#### Destination - -Destination 的意思是消息生产者的消息发送目标或者说消息消费者的消息来源。对于消息生产者来说,它的 Destination 是某个队列(Queue)或某个主题(Topic);对于消息消费者来说,它的 Destination 也是某个队列或主题(即消息来源)。 - -所以,Destination 实际上就是两种类型的对象:Queue、Topic。可以通过 JNDI 来查找 Destination。 - -#### Connection - -Connection 表示在客户端和 JMS 系统之间建立的链接(对 TCP/IP socket 的包装)。Connection 可以产生一个或多个 Session。跟 ConnectionFactory 一样,Connection 也有两种类型:QueueConnection 和 TopicConnection。 - -#### Session - -Session 是操作消息的接口。可以通过 session 创建生产者、消费者、消息等。Session 提供了事务的功能。当需要使用 session 发送/接收多个消息时,可以将这些发送/接收动作放到一个事务中。同样,也分 QueueSession 和 TopicSession。 - -#### 消息的生产者 - -消息生产者由 Session 创建,并用于将消息发送到 Destination。同样,消息生产者分两种类型:QueueSender 和 TopicPublisher。可以调用消息生产者的方法(send 或 publish 方法)发送消息。 - -#### 消息消费者 - -消息消费者由 Session 创建,用于接收被发送到 Destination 的消息。两种类型:QueueReceiver 和 TopicSubscriber。可分别通过 session 的 createReceiver(Queue)或 createSubscriber(Topic)来创建。当然,也可以 session 的 creatDurableSubscriber 方法来创建持久化的订阅者。 - -#### MessageListener - -消息监听器。如果注册了消息监听器,一旦消息到达,将自动调用监听器的 onMessage 方法。EJB 中的 MDB(Message-Driven Bean)就是一种 MessageListener。 - -## 参考资料 - -- [大型网站架构系列:分布式 MQ(一)](https://www.cnblogs.com/itfly8/p/5155983.html) -- [大型网站架构系列:MQ(二)](https://www.cnblogs.com/itfly8/p/5156155.html) -- [分布式开放 MQ(RocketMQ)的原理与实践](https://www.jianshu.com/p/453c6e7ff81c) -- [阿里 RocketMQ 优势对比](https://juejin.im/entry/5a0abfb5f265da43062a4a91) -- [advanced-java 之 MQ](https://github.com/doocs/advanced-java/blob/master/docs/high-concurrency/mq-interview.md) -- [浅谈消息队列及常见的消息中间件](https://juejin.im/post/6844903635046924296) diff --git "a/docs/14.\344\270\255\351\227\264\344\273\266/01.MQ/03.RocketMQ.md" "b/docs/14.\344\270\255\351\227\264\344\273\266/01.MQ/03.RocketMQ.md" deleted file mode 100644 index c1cf2288..00000000 --- "a/docs/14.\344\270\255\351\227\264\344\273\266/01.MQ/03.RocketMQ.md" +++ /dev/null @@ -1,501 +0,0 @@ ---- -title: RocketMQ 快速入门 -categories: - - 编程 - - Java - - 中间件 - - MQ -tags: - - Java - - 中间件 - - MQ - - RocketMQ -abbrlink: 3e7adddd -date: 2022-02-17 22:34:30 -permalink: /pages/f56a96/ ---- - -# RocketMQ 快速入门 - -## 简介 - -RocketMQ 是一款开源的分布式消息队列,基于高可用分布式集群技术,提供低延时的、高可靠的消息发布与订阅服务。 - -RocketMQ 被阿里巴巴捐赠给 Apache,成为 Apache 的孵化项目。 - -![img](https://raw.githubusercontent.com/dunwu/images/dev/cs/java/javaweb/distributed/mq/rocketmq/rmq-model.png) - -RocketMQ 有以下核心概念: - -- **Producer** - 将业务应用程序系统生成的消息发送给代理。RocketMQ 提供多种发送范例:同步,异步和单向。 - - **Producer Group** - 具有相同角色的 Producer 组合在一起。如果原始 Producer 在事务之后崩溃,则代理可以联系同一 Producer 组的不同 Producer 实例以提交或回滚事务。**_警告:考虑到提供的 Producer 在发送消息方面足够强大,每个 Producer 组只允许一个实例,以避免不必要的生成器实例初始化。_** -- **Consumer** - Consumer 从 Broker 那里获取消息并将其提供给应用程序。从用户应用的角度来看,提供了两种类型的 Consumer: - - **PullConsumer** - PullConsumer 积极地从 Broker 那里获取消息。一旦提取了批量消息,用户应用程序就会启动消费过程。 - - **PushConsumer** - PushConsumer 封装消息提取,消费进度并维护其他内部工作,为最终用户留下回调接口,这个借口会在消息到达时被执行。 - - **Consumer Group** - 完全相同角色的 Consumer 被组合在一起并命名为 Consumer Group。Consumer Group 是一个很好的概念,在消息消费方面实现负载平衡和容错目标非常容易。**_警告:Consumer Group 中的 Consumer 实例必须具有完全相同的主题订阅。_** -- **Broker** - Broker 是 RocketMQ 的主要组成部分。它接收从 Producer 发送的消息,存储它们并准备处理来自 Consumer 的消费请求。它还存储与消息相关的元数据,包括 Consumer Group,消耗进度偏移和主题/队列信息。 -- Name Server - 充当路由信息提供者。Producer/Consumer 客户查找主题以查找相应的 Broker 列表。 -- **Topic** - 是 Producer 传递消息和 Consumer 提取消息的类别。 -- **Message** - 是要传递的信息。消息必须有一个主题,可以将其解释为您要发送给的邮件地址。消息还可以具有可选 Tag 和额外的键值对。例如,您可以为消息设置业务密钥,并在代理服务器上查找消息以诊断开发期间的问题。 - - **Message Queue** - 主题被划分为一个或多个子主题“消息队列”。 - - **Tag** - 即子主题,为用户提供了额外的灵活性。对于 Tag,来自同一业务模块的具有不同目的的消息可以具有相同的主题和不同的 Tag。 - -## 安装 - -### 环境要求 - -- 推荐 64 位操作系统:Linux/Unix/Mac -- 64bit JDK 1.8+ -- Maven 3.2.x -- Git - -### 下载解压 - -进入官方下载地址: - -建议选择 binary 版本。 - -解压到本地: - -```bash -> unzip rocketmq-all-4.2.0-source-release.zip -> cd rocketmq-all-4.2.0/ -``` - -### 启动 Name Server - -```bash -> nohup sh bin/mqnamesrv & -> tail -f ~/logs/rocketmqlogs/namesrv.log -The Name Server boot success... -``` - -### 启动 Broker - -```bash -> nohup sh bin/mqbroker -n localhost:9876 -c conf/broker.conf & -> tail -f ~/logs/rocketmqlogs/broker.log -The broker[%s, 172.30.30.233:10911] boot success... -``` - -### 收发消息 - -执行收发消息操作之前,不许告诉客户端命名服务器的位置。在 RocketMQ 中有多种方法来实现这个目的。这里,我们使用最简单的方法——设置环境变量 `NAMESRV_ADDR` : - -```bash -> export NAMESRV_ADDR=localhost:9876 -> sh bin/tools.sh org.apache.rocketmq.example.quickstart.Producer -SendResult [sendStatus=SEND_OK, msgId= ... - -> sh bin/tools.sh org.apache.rocketmq.example.quickstart.Consumer -ConsumeMessageThread_%d Receive New Messages: [MessageExt... -``` - -### 关闭服务器 - -```bash -> sh bin/mqshutdown broker -The mqbroker(36695) is running... -Send shutdown request to mqbroker(36695) OK - -> sh bin/mqshutdown namesrv -The mqnamesrv(36664) is running... -Send shutdown request to mqnamesrv(36664) OK -``` - -## API - -首先在项目中引入 maven 依赖: - -```xml - - org.apache.rocketmq - rocketmq-client - 4.2.0 - -``` - -### Producer - -Producer 在 RocketMQ 中负责发送消息。 - -RocketMQ 有三种消息发送方式: - -- 可靠的同步发送 -- 可靠的异步发送 -- 单项发送 - -#### 可靠的同步发送 - -可靠的同步传输用于广泛的场景,如重要的通知消息,短信通知,短信营销系统等。 - -```java -public class SyncProducer { - public static void main(String[] args) throws Exception { - //Instantiate with a producer group name. - DefaultMQProducer producer = new - DefaultMQProducer("please_rename_unique_group_name"); - //Launch the instance. - producer.start(); - for (int i = 0; i < 100; i++) { - //Create a message instance, specifying topic, tag and message body. - Message msg = new Message("TopicTest" /* Topic */, - "TagA" /* Tag */, - ("Hello RocketMQ " + - i).getBytes(RemotingHelper.DEFAULT_CHARSET) /* Message body */ - ); - //Call send message to deliver message to one of brokers. - SendResult sendResult = producer.send(msg); - System.out.printf("%s%n", sendResult); - } - //Shut down once the producer instance is not longer in use. - producer.shutdown(); - } -} -``` - -#### 可靠的异步发送 - -异步传输通常用于响应时间敏感的业务场景。 - -```java -public class AsyncProducer { - public static void main(String[] args) throws Exception { - //Instantiate with a producer group name. - DefaultMQProducer producer = new DefaultMQProducer("ExampleProducerGroup"); - //Launch the instance. - producer.start(); - producer.setRetryTimesWhenSendAsyncFailed(0); - for (int i = 0; i < 100; i++) { - final int index = i; - //Create a message instance, specifying topic, tag and message body. - Message msg = new Message("TopicTest", - "TagA", - "OrderID188", - "Hello world".getBytes(RemotingHelper.DEFAULT_CHARSET)); - producer.send(msg, new SendCallback() { - @Override - public void onSuccess(SendResult sendResult) { - System.out.printf("%-10d OK %s %n", index, - sendResult.getMsgId()); - } - @Override - public void onException(Throwable e) { - System.out.printf("%-10d Exception %s %n", index, e); - e.printStackTrace(); - } - }); - } - //Shut down once the producer instance is not longer in use. - producer.shutdown(); - } -} -``` - -#### 单向传输 - -单向传输用于需要中等可靠性的情况,例如日志收集。 - -```java -public class OnewayProducer { - public static void main(String[] args) throws Exception{ - //Instantiate with a producer group name. - DefaultMQProducer producer = new DefaultMQProducer("ExampleProducerGroup"); - //Launch the instance. - producer.start(); - for (int i = 0; i < 100; i++) { - //Create a message instance, specifying topic, tag and message body. - Message msg = new Message("TopicTest" /* Topic */, - "TagA" /* Tag */, - ("Hello RocketMQ " + - i).getBytes(RemotingHelper.DEFAULT_CHARSET) /* Message body */ - ); - //Call send message to deliver message to one of brokers. - producer.sendOneway(msg); - - } - //Shut down once the producer instance is not longer in use. - producer.shutdown(); - } -} -``` - -### Consumer - -Consumer 在 RocketMQ 中负责接收消息。 - -```java -public class OrderedConsumer { - public static void main(String[] args) throws Exception { - DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("example_group_name"); - consumer.setNamesrvAddr(RocketConfig.HOST); - - consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET); - - consumer.subscribe("TopicTest", "TagA || TagC || TagD"); - - consumer.registerMessageListener(new MessageListenerOrderly() { - - AtomicLong consumeTimes = new AtomicLong(0); - - @Override - public ConsumeOrderlyStatus consumeMessage(List msgs, - ConsumeOrderlyContext context) { - context.setAutoCommit(false); - System.out.printf(Thread.currentThread().getName() + " Receive New Messages: " + msgs + "%n"); - this.consumeTimes.incrementAndGet(); - if ((this.consumeTimes.get() % 2) == 0) { - return ConsumeOrderlyStatus.SUCCESS; - } else if ((this.consumeTimes.get() % 3) == 0) { - return ConsumeOrderlyStatus.ROLLBACK; - } else if ((this.consumeTimes.get() % 4) == 0) { - return ConsumeOrderlyStatus.COMMIT; - } else if ((this.consumeTimes.get() % 5) == 0) { - context.setSuspendCurrentQueueTimeMillis(3000); - return ConsumeOrderlyStatus.SUSPEND_CURRENT_QUEUE_A_MOMENT; - } - return ConsumeOrderlyStatus.SUCCESS; - - } - }); - - consumer.start(); - - System.out.printf("Consumer Started.%n"); - } -} -``` - -### FAQ - -#### connect to `<172.17.0.1:10909>` failed - -启动后,Producer 客户端连接 RocketMQ 时报错: - -```java -org.apache.rocketmq.remoting.exception.RemotingConnectException: connect to <172.17.0.1:10909> failed - at org.apache.rocketmq.remoting.netty.NettyRemotingClient.invokeSync(NettyRemotingClient.java:357) - at org.apache.rocketmq.client.impl.MQClientAPIImpl.sendMessageSync(MQClientAPIImpl.java:343) - at org.apache.rocketmq.client.impl.MQClientAPIImpl.sendMessage(MQClientAPIImpl.java:327) - at org.apache.rocketmq.client.impl.MQClientAPIImpl.sendMessage(MQClientAPIImpl.java:290) - at org.apache.rocketmq.client.impl.producer.DefaultMQProducerImpl.sendKernelImpl(DefaultMQProducerImpl.java:688) - at org.apache.rocketmq.client.impl.producer.DefaultMQProducerImpl.sendSelectImpl(DefaultMQProducerImpl.java:901) - at org.apache.rocketmq.client.impl.producer.DefaultMQProducerImpl.send(DefaultMQProducerImpl.java:878) - at org.apache.rocketmq.client.impl.producer.DefaultMQProducerImpl.send(DefaultMQProducerImpl.java:873) - at org.apache.rocketmq.client.producer.DefaultMQProducer.send(DefaultMQProducer.java:369) - at com.emrubik.uc.mdm.sync.utils.MdmInit.sendMessage(MdmInit.java:62) - at com.emrubik.uc.mdm.sync.utils.MdmInit.main(MdmInit.java:2149) -``` - -原因:RocketMQ 部署在虚拟机上,内网 ip 为 10.10.30.63,该虚拟机一个 docker0 网卡,ip 为 172.17.0.1。RocketMQ broker 启动时默认使用了 docker0 网卡,Producer 客户端无法连接 172.17.0.1,造成以上问题。 - -解决方案 - -(1)干掉 docker0 网卡或修改网卡名称 - -(2)停掉 broker,修改 broker 配置文件,重启 broker。 - -修改 conf/broker.conf,增加两行来指定启动 broker 的 IP: - -``` -namesrvAddr = 10.10.30.63:9876 -brokerIP1 = 10.10.30.63 -``` - -启动时需要指定配置文件 - -```bash -nohup sh bin/mqbroker -n localhost:9876 -c conf/broker.conf & -``` - -## 架构 - -![img](https://raw.githubusercontent.com/dunwu/images/dev/cs/java/javaweb/distributed/mq/rocketmq/rmq-basic-arc.png) - -RocketMQ 由四部分组成:NameServer、Broker、Producer、Consumer。其中任意一个组成都可以水平扩展为集群模式,以避免单点故障问题。 - -### Producer - -Producers 支持分布式集群方式部署。Producer 通过 MQ 的负载均衡模块选择相应的 Broker 集群队列进行消息投递,投递的过程支持快速失败并且低延迟。 - -### Consumer - -Consumer 支持分布式集群方式部署。支持以 push 推,pull 拉两种模式对消息进行消费。同时也支持集群方式和广播方式的消费,它提供实时消息订阅机制,可以满足大多数用户的需求。 - -### NameServer - -NameServer 是一个 Topic 路由注册中心,其角色类似 Dubbo 中的 zookeeper,支持 Broker 的动态注册与发现。主要包括两个功能: - -- **Broker 管理**,NameServer 接受 Broker 集群的注册信息并且保存下来作为路由信息的基本数据。然后提供心跳检测机制,检查 Broker 是否还存活; -- **路由信息管理**,每个 NameServer 将保存关于 Broker 集群的整个路由信息和用于客户端查询的队列信息。然后 Producer 和 Conumser 通过 NameServer 就可以知道整个 Broker 集群的路由信息,从而进行消息的投递和消费。 - -NameServer 通常也是集群的方式部署,各实例间相互不进行信息通讯。Broker 是向每一台 NameServer 注册自己的路由信息,所以每一个 NameServer 实例上面都保存一份完整的路由信息。当某个 NameServer 因某种原因下线了,Broker 仍然可以向其它 NameServer 同步其路由信息,Producer、Consumer 仍然可以动态感知 Broker 的路由的信息。 - -NameServer 是一个功能齐全的服务器,主要包括两个功能: - -1. Broker 管理 - NameServer 接受来自 Broker 集群的注册,并提供心跳机制来检查 Broker 节点是否存活。 -2. 路由管理 - 每个 NameServer 将保存有关 Broker 集群的完整路由信息和客户端查询的查询队列。 - -RocketMQ 客户端(Producer/Consumer)将从 NameServer 查询队列路由信息。 - -将 NameServer 地址列表提供给客户端有四种方法: - -1. 编程方式 - 类似:`producer.setNamesrvAddr("ip:port")` -2. Java 选项 - 使用 `rocketmq.namesrv.addr` 参数 -3. 环境变量 - 设置环境变量 `NAMESRV_ADDR` -4. HTTP 端点 - -> 更详细信息可以参考官方文档:[here](http://rocketmq.apache.org/rocketmq/four-methods-to-feed-name-server-address-list/) - -### Broker - -Broker 主要负责消息的存储、投递和查询以及服务高可用保证,为了实现这些功能,Broker 包含了以下几个重要子模块。 - -Broker 有几个重要的子模块: - -- **Remoting Module**:整个 Broker 的实体,负责处理来自 clients 端的请求。 -- **Client Manager**:负责管理客户端(Producer/Consumer)和维护 Consumer 的 Topic 订阅信息。 -- **Store Service**:提供方便简单的 API 接口处理消息存储到物理硬盘和查询功能。 -- **HA Service**:高可用服务,提供 Master Broker 和 Slave Broker 之间的数据同步功能。 -- **Index Service**:根据特定的 Message key 对投递到 Broker 的消息进行索引服务,以提供消息的快速查询。 - -![img](https://raw.githubusercontent.com/dunwu/images/dev/cs/java/javaweb/distributed/mq/rocketmq/rmq-basic-component.png) - -## 原理 - -分布式消息系统作为实现分布式系统可扩展、可伸缩性的关键组件,需要具有高吞吐量、高可用等特点。而谈到消息系统的设计,就回避不了两个问题: - -1. 消息的顺序问题 -2. 消息的重复问题 - -### 顺序消息 - -#### 第一种模型 - -假如生产者产生了 2 条消息:M1、M2,要保证这两条消息的顺序,应该怎样做?你脑中想到的可能是这样: - -
- -
- -假定 M1 发送到 S1,M2 发送到 S2,如果要保证 M1 先于 M2 被消费,那么需要 M1 到达消费端被消费后,通知 S2,然后 S2 再将 M2 发送到消费端。 - -这个模型存在的问题是,如果 M1 和 M2 分别发送到两台 Server 上,就不能保证 M1 先达到 MQ 集群,也不能保证 M1 被先消费。换个角度看,如果 M2 先于 M1 达到 MQ 集群,甚至 M2 被消费后,M1 才达到消费端,这时消息也就乱序了,说明以上模型是不能保证消息的顺序的。 - -
- -
- -#### 第二种模型 - -如何才能在 MQ 集群保证消息的顺序?一种简单的方式就是将 M1、M2 发送到同一个 Server 上: - -这样可以保证 M1 先于 M2 到达 MQServer(生产者等待 M1 发送成功后再发送 M2),根据先达到先被消费的原则,M1 会先于 M2 被消费,这样就保证了消息的顺序。 - -这个模型也仅仅是理论上可以保证消息的顺序,在实际场景中可能会遇到下面的问题: - -
- -
- -只要将消息从一台服务器发往另一台服务器,就会存在网络延迟问题。如上图所示,如果发送 M1 耗时大于发送 M2 的耗时,那么 M2 就仍将被先消费,仍然不能保证消息的顺序。即使 M1 和 M2 同时到达消费端,由于不清楚消费端 1 和消费端 2 的负载情况,仍然有可能出现 M2 先于 M1 被消费的情况。 - -如何解决这个问题?将 M1 和 M2 发往同一个消费者,且发送 M1 后,需要消费端响应成功后才能发送 M2。 - -这可能产生另外的问题:如果 M1 被发送到消费端后,消费端 1 没有响应,那是继续发送 M2 呢,还是重新发送 M1?一般为了保证消息一定被消费,肯定会选择重发 M1 到另外一个消费端 2,就如下图所示。 - -
- -
- -这样的模型就严格保证消息的顺序,细心的你仍然会发现问题,消费端 1 没有响应 Server 时有两种情况,一种是 M1 确实没有到达(数据在网络传送中丢失),另外一种消费端已经消费 M1 且已经发送响应消息,只是 MQ Server 端没有收到。如果是第二种情况,重发 M1,就会造成 M1 被重复消费。也就引入了我们要说的第二个问题,消息重复问题,这个后文会详细讲解。 - -回过头来看消息顺序问题,严格的顺序消息非常容易理解,也可以通过文中所描述的方式来简单处理。总结起来,要实现严格的顺序消息,简单且可行的办法就是: - -**保证生产者 - MQServer - 消费者是一对一对一的关系。** - -这样的设计虽然简单易行,但也会存在一些很严重的问题,比如: - -1. 并行度就会成为消息系统的瓶颈(吞吐量不够) -2. 更多的异常处理,比如:只要消费端出现问题,就会导致整个处理流程阻塞,我们不得不花费更多的精力来解决阻塞的问题。 - -RocketMQ 的解决方案:通过合理的设计或者将问题分解来规避。如果硬要把时间花在解决问题本身,实际上不仅效率低下,而且也是一种浪费。从这个角度来看消息的顺序问题,我们可以得出两个结论: - -1. 不关注乱序的应用实际大量存在 -2. 队列无序并不意味着消息无序 - -最后我们从源码角度分析 RocketMQ 怎么实现发送顺序消息。 - -RocketMQ 通过轮询所有队列的方式来确定消息被发送到哪一个队列(负载均衡策略)。比如下面的示例中,订单号相同的消息会被先后发送到同一个队列中: - -```java -// RocketMQ 通过 MessageQueueSelector 中实现的算法来确定消息发送到哪一个队列上 -// RocketMQ 默认提供了两种 MessageQueueSelector 实现:随机/Hash -// 当然你可以根据业务实现自己的 MessageQueueSelector 来决定消息按照何种策略发送到消息队列中 -SendResult sendResult = producer.send(msg, new MessageQueueSelector() { - @Override - public MessageQueue select(List mqs, Message msg, Object arg) { - Integer id = (Integer) arg; - int index = id % mqs.size(); - return mqs.get(index); - } -}, orderId); -``` - -在获取到路由信息以后,会根据 MessageQueueSelector 实现的算法来选择一个队列,同一个 OrderId 获取到的肯定是同一个队列。 - -```java -private SendResult send() { - // 获取topic路由信息 - TopicPublishInfo topicPublishInfo = this.tryToFindTopicPublishInfo(msg.getTopic()); - if (topicPublishInfo != null && topicPublishInfo.ok()) { - MessageQueue mq = null; - // 根据我们的算法,选择一个发送队列 - // 这里的arg = orderId - mq = selector.select(topicPublishInfo.getMessageQueueList(), msg, arg); - if (mq != null) { - return this.sendKernelImpl(msg, mq, communicationMode, sendCallback, timeout); - } - } -} -``` - -### 消息重复 - -造成消息重复的根本原因是:网络不可达。只要通过网络交换数据,就无法避免这个问题。所以解决这个问题的办法就是绕过这个问题。那么问题就变成了:如果消费端收到两条一样的消息,应该怎样处理? - -1. 消费端处理消息的业务逻辑保持幂等性。 -2. 保证每条消息都有唯一编号且保证消息处理成功与去重表的日志同时出现。 - -第 1 条很好理解,只要保持幂等性,不管来多少条重复消息,最后处理的结果都一样。 - -第 2 条原理就是利用一张日志表来记录已经处理成功的消息的 ID,如果新到的消息 ID 已经在日志表中,那么就不再处理这条消息。 - -第 1 条解决方案,很明显应该在消费端实现,不属于消息系统要实现的功能。 - -第 2 条可以消息系统实现,也可以业务端实现。正常情况下出现重复消息的概率其实很小,如果由消息系统来实现的话,肯定会对消息系统的吞吐量和高可用有影响,所以最好还是由业务端自己处理消息重复的问题,这也是 RocketMQ 不解决消息重复的问题的原因。 - -**RocketMQ 不保证消息不重复,如果你的业务需要保证严格的不重复消息,需要你自己在业务端去重。** - -### 事务消息 - -RocketMQ 除了支持普通消息,顺序消息,另外还支持事务消息。 - -假设这样的场景: - -![img](https://upload-images.jianshu.io/upload_images/3101171-253d8bd65736694f.png) - -图中执行本地事务(Bob 账户扣款)和发送异步消息应该保证同时成功或者同时失败,也就是扣款成功了,发送消息一定要成功,如果扣款失败了,就不能再发送消息。那问题是:我们是先扣款还是先发送消息呢? - -![img](http://upload-images.jianshu.io/upload_images/3101171-088dc074c4ecd192) - -RocketMQ 分布式事务步骤: - -发送 Prepared 消息 2222222222222222222,并拿到接受消息的地址。 -执行本地事务 -通过第 1 步骤拿到的地址去访问消息,并修改消息状态。 - -## 参考资料 - -- [RocketMQ 官方文档](http://rocketmq.apache.org/docs/quick-start/) -- [分布式开放消息系统(RocketMQ)的原理与实践](https://www.jianshu.com/p/453c6e7ff81c) diff --git "a/docs/14.\344\270\255\351\227\264\344\273\266/01.MQ/04.ActiveMQ.md" "b/docs/14.\344\270\255\351\227\264\344\273\266/01.MQ/04.ActiveMQ.md" deleted file mode 100644 index 04890480..00000000 --- "a/docs/14.\344\270\255\351\227\264\344\273\266/01.MQ/04.ActiveMQ.md" +++ /dev/null @@ -1,312 +0,0 @@ ---- -title: ActiveMQ 快速入门 -categories: - - 编程 - - Java - - 中间件 - - MQ -tags: - - Java - - 中间件 - - MQ - - ActiveMQ -abbrlink: 30b5d114 -date: 2022-02-17 22:34:30 -permalink: /pages/3f7c49/ ---- - -# ActiveMQ 快速入门 - -## JMS 基本概念 - -`JMS` 即 **Java 消息服务(Java Message Service)API**,是一个 Java 平台中关于面向消息中间件的 API。它用于在两个应用程序之间,或分布式系统中发送消息,进行异步通信。Java 消息服务是一个与具体平台无关的 API,绝大多数 MOM 提供商都对 JMS 提供支持。 - -### 消息模型 - -JMS 有两种消息模型: - -- Point-to-Point(P2P) -- Publish/Subscribe(Pub/Sub) - -#### P2P 的特点 - -![img](https://raw.githubusercontent.com/dunwu/images/dev/cs/java/javalib/jms/jms-pointToPoint.gif) - -在点对点的消息系统中,消息分发给一个单独的使用者。点对点消息往往与队列 `javax.jms.Queue` 相关联。 - -每个消息只有一个消费者(Consumer)(即一旦被消费,消息就不再在消息队列中)。 - -发送者和接收者之间在时间上没有依赖性,也就是说当发送者发送了消息之后,不管接收者有没有正在运行,它不会影响到消息被发送到队列。 - -接收者在成功接收消息之后需向队列应答成功。 - -如果你希望发送的每个消息都应该被成功处理的话,那么你需要 P2P 模式。 - -#### Pub/Sub 的特点 - -![img](https://raw.githubusercontent.com/dunwu/images/dev/cs/java/javalib/jms/jms-publishSubscribe.gif) - -发布/订阅消息系统支持一个事件驱动模型,消息生产者和消费者都参与消息的传递。生产者发布事件,而使用者订阅感兴趣的事件,并使用事件。该类型消息一般与特定的主题 `javax.jms.Topic` 关联。 - -每个消息可以有多个消费者。 - -发布者和订阅者之间有时间上的依赖性。针对某个主题(Topic)的订阅者,它必须创建一个订阅者之后,才能消费发布者的消息,而且为了消费消息,订阅者必须保持运行的状态。 - -为了缓和这样严格的时间相关性,JMS 允许订阅者创建一个可持久化的订阅。这样,即使订阅者没有被激活(运行),它也能接收到发布者的消息。 - -如果你希望发送的消息可以不被做任何处理、或者被一个消息者处理、或者可以被多个消费者处理的话,那么可以采用 Pub/Sub 模型。 - -### JMS 编程模型 - -![img](https://raw.githubusercontent.com/dunwu/images/dev/cs/java/javalib/jms/jms-publishSubscribe.gif) - -#### ConnectionFactory - -创建 `Connection` 对象的工厂,针对两种不同的 jms 消息模型,分别有 `QueueConnectionFactory` 和`TopicConnectionFactory` 两种。可以通过 JNDI 来查找 `ConnectionFactory` 对象。 - -#### Connection - -`Connection` 表示在客户端和 JMS 系统之间建立的链接(对 TCP/IP socket 的包装)。`Connection` 可以产生一个或多个`Session`。跟 `ConnectionFactory` 一样,`Connection` 也有两种类型:`QueueConnection` 和 `TopicConnection`。 - -#### Destination - -`Destination` 是一个包装了消息目标标识符的被管对象。消息目标是指消息发布和接收的地点,或者是队列 `Queue` ,或者是主题 `Topic` 。JMS 管理员创建这些对象,然后用户通过 JNDI 发现它们。和连接工厂一样,管理员可以创建两种类型的目标,点对点模型的 `Queue`,以及发布者/订阅者模型的 `Topic`。 - -#### Session - -`Session` 表示一个单线程的上下文,用于发送和接收消息。由于会话是单线程的,所以消息是连续的,就是说消息是按照发送的顺序一个一个接收的。会话的好处是它支持事务。如果用户选择了事务支持,会话上下文将保存一组消息,直到事务被提交才发送这些消息。在提交事务之前,用户可以使用回滚操作取消这些消息。一个会话允许用户创建消息,生产者来发送消息,消费者来接收消息。同样,`Session` 也分 `QueueSession` 和 `TopicSession`。 - -#### MessageConsumer - -`MessageConsumer` 由 `Session` 创建,并用于将消息发送到 `Destination`。消费者可以同步地(阻塞模式),或(非阻塞)接收 `Queue` 和 `Topic` 类型的消息。同样,消息生产者分两种类型:`QueueSender` 和`TopicPublisher`。 - -#### MessageProducer - -`MessageProducer` 由 `Session` 创建,用于接收被发送到 `Destination` 的消息。`MessageProducer` 有两种类型:`QueueReceiver` 和 `TopicSubscriber`。可分别通过 `session` 的 `createReceiver(Queue)` 或 `createSubscriber(Topic)` 来创建。当然,也可以 `session` 的 `creatDurableSubscriber` 方法来创建持久化的订阅者。 - -#### Message - -是在消费者和生产者之间传送的对象,也就是说从一个应用程序传送到另一个应用程序。一个消息有三个主要部分: - -- 消息头(必须):包含用于识别和为消息寻找路由的操作设置。 -- 一组消息属性(可选):包含额外的属性,支持其他提供者和用户的兼容。可以创建定制的字段和过滤器(消息选择器)。 -- 一个消息体(可选):允许用户创建五种类型的消息(文本消息,映射消息,字节消息,流消息和对象消息)。 - -消息接口非常灵活,并提供了许多方式来定制消息的内容。 - -| Common | Point-to-Point | Publish-Subscribe | -| ----------------- | --------------------------- | ---------------------- | -| ConnectionFactory | QueueConnectionFactory | TopicConnectionFactory | -| Connection | QueueConnection | TopicConnection | -| Destination | Queue | Topic | -| Session | QueueSession | TopicSession | -| MessageProducer | QueueSender | TopicPublisher | -| MessageSender | QueueReceiver, QueueBrowser | TopicSubscriber | - -## 安装 - -**安装条件** - -JDK1.7 及以上版本 - -本地配置了 **JAVA_HOME** 环境变量。 - -**下载** - -支持 Windows/Unix/Linux/Cygwin - -[ActiveMQ 官方下载地址](http://activemq.apache.org/download.html) - -**Windows 下运行** - -(1)解压压缩包到本地; - -(2)打开控制台,进入安装目录的 `bin` 目录下; - -``` -cd [activemq_install_dir] -``` - -(3)执行 `activemq start` 来启动 ActiveMQ - -``` -bin\activemq start -``` - -**测试安装是否成功** - -(1)ActiveMQ 默认监听端口为 61616 - -``` -netstat -an|find “61616” -``` - -(2)访问 - -(3)输入用户名、密码 - -``` -Login: admin -Passwort: admin -``` - -(4)点击导航栏的 Queues 菜单 - -(5)添加一个队列(queue) - -## 项目中的应用 - -**引入依赖** - -```xml - - org.apache.activemq - activemq-all - 5.14.1 - -``` - -**Sender.java** - -```java -public class Sender { - private static final int SEND_NUMBER = 4; - - public static void main(String[] args) { - // ConnectionFactory :连接工厂,JMS 用它创建连接 - ConnectionFactory connectionFactory; - // Connection :JMS 客户端到JMS Provider 的连接 - Connection connection = null; - // Session: 一个发送或接收消息的线程 - Session session; - // Destination :消息的目的地;消息发送给谁. - Destination destination; - // MessageProducer:消息发送者 - MessageProducer producer; - // TextMessage message; - // 构造ConnectionFactory实例对象,此处采用ActiveMq的实现jar - connectionFactory = new ActiveMQConnectionFactory( - ActiveMQConnection.DEFAULT_USER, - ActiveMQConnection.DEFAULT_PASSWORD, - "tcp://localhost:61616"); - try { - // 构造从工厂得到连接对象 - connection = connectionFactory.createConnection(); - // 启动 - connection.start(); - // 获取操作连接 - session = connection.createSession(Boolean.TRUE, - Session.AUTO_ACKNOWLEDGE); - // 获取session注意参数值xingbo.xu-queue是一个服务器的queue,须在在ActiveMq的console配置 - destination = session.createQueue("FirstQueue"); - // 得到消息生成者【发送者】 - producer = session.createProducer(destination); - // 设置不持久化,此处学习,实际根据项目决定 - producer.setDeliveryMode(DeliveryMode.NON_PERSISTENT); - // 构造消息,此处写死,项目就是参数,或者方法获取 - sendMessage(session, producer); - session.commit(); - } catch (Exception e) { - e.printStackTrace(); - } finally { - try { - if (null != connection) - connection.close(); - } catch (Throwable ignore) { - } - } - } - - public static void sendMessage(Session session, MessageProducer producer) - throws Exception { - for (int i = 1; i <= SEND_NUMBER; i++) { - TextMessage message = session - .createTextMessage("ActiveMq 发送的消息" + i); - // 发送消息到目的地方 - System.out.println("发送消息:" + "ActiveMq 发送的消息" + i); - producer.send(message); - } - } -} -``` - -**Receiver.java** - -```java -public class Receiver { - public static void main(String[] args) { - // ConnectionFactory :连接工厂,JMS 用它创建连接 - ConnectionFactory connectionFactory; - // Connection :JMS 客户端到JMS Provider 的连接 - Connection connection = null; - // Session: 一个发送或接收消息的线程 - Session session; - // Destination :消息的目的地;消息发送给谁. - Destination destination; - // 消费者,消息接收者 - MessageConsumer consumer; - connectionFactory = new ActiveMQConnectionFactory( - ActiveMQConnection.DEFAULT_USER, - ActiveMQConnection.DEFAULT_PASSWORD, - "tcp://localhost:61616"); - try { - // 构造从工厂得到连接对象 - connection = connectionFactory.createConnection(); - // 启动 - connection.start(); - // 获取操作连接 - session = connection.createSession(Boolean.FALSE, - Session.AUTO_ACKNOWLEDGE); - // 获取session注意参数值xingbo.xu-queue是一个服务器的queue,须在在ActiveMq的console配置 - destination = session.createQueue("FirstQueue"); - consumer = session.createConsumer(destination); - while (true) { - //设置接收者接收消息的时间,为了便于测试,这里谁定为100s - TextMessage message = (TextMessage) consumer.receive(100000); - if (null != message) { - System.out.println("收到消息" + message.getText()); - } else { - break; - } - } - } catch (Exception e) { - e.printStackTrace(); - } finally { - try { - if (null != connection) - connection.close(); - } catch (Throwable ignore) { - } - } - } -} -``` - -**运行** - -先运行 Receiver.java 进行消息监听,再运行 Send.java 发送消息。 - -**输出** - -Send 的输出内容 - -``` -发送消息:Activemq 发送消息0 -发送消息:Activemq 发送消息1 -发送消息:Activemq 发送消息2 -发送消息:Activemq 发送消息3 -``` - -Receiver 的输出内容 - -``` -收到消息ActiveMQ 发送消息0 -收到消息ActiveMQ 发送消息1 -收到消息ActiveMQ 发送消息2 -收到消息ActiveMQ 发送消息3 -``` - -## 资源 - -- [ActiveMQ 官网](http://activemq.apache.org/) -- [oracle 官方的 jms 介绍](https://docs.oracle.com/cd/E19575-01/819-3669/6n5sg7cgq/index.html) diff --git "a/docs/14.\344\270\255\351\227\264\344\273\266/01.MQ/README.md" "b/docs/14.\344\270\255\351\227\264\344\273\266/01.MQ/README.md" deleted file mode 100644 index be1eee16..00000000 --- "a/docs/14.\344\270\255\351\227\264\344\273\266/01.MQ/README.md" +++ /dev/null @@ -1,65 +0,0 @@ ---- -title: Java 和消息队列 -categories: - - 编程 - - Java - - 中间件 - - MQ -tags: - - Java - - 中间件 - - MQ -abbrlink: 71639a39 -date: 2022-02-17 22:34:30 -hidden: true -permalink: /pages/fc0b29/ ---- - -# Java 和消息队列 - -> 消息队列(Message Queue,简称 MQ)技术是分布式应用间交换信息的一种技术。 -> -> 消息队列主要解决应用耦合,异步消息,流量削锋等问题,实现高性能,高可用,可伸缩和最终一致性架构。是大型分布式系统不可缺少的中间件。 -> -> 如果想深入学习各种消息队列产品,建议先了解一下 [消息队列基本原理](https://dunwu.github.io/blog/pages/1fd240/) ,有助于理解消息队列特性的实现和设计思路。 - -## 内容 - -- [消息队列面试](01.消息队列面试.md) -- [消息队列基本原理](02.消息队列基本原理.md) -- [Kafka 教程](https://dunwu.github.io/bigdata-tutorial/kafka) 📚 -- [RocketMQ](03.RocketMQ.md) -- [ActiveMQ](04.ActiveMQ.md) - -## 技术对比 - -| 特性 | ActiveMQ | RabbitMQ | RocketMQ | Kafka | -| ------------------------ | ------------------------------------- | -------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | -| 单机吞吐量 | 万级,比 RocketMQ、Kafka 低一个数量级 | 同 ActiveMQ | 10 万级,支撑高吞吐 | 10 万级,高吞吐,一般配合大数据类的系统来进行实时数据计算、日志采集等场景 | -| topic 数量对吞吐量的影响 | | | topic 可以达到几百/几千的级别,吞吐量会有较小幅度的下降,这是 RocketMQ 的一大优势,在同等机器下,可以支撑大量的 topic | topic 从几十到几百个时候,吞吐量会大幅度下降,在同等机器下,Kafka 尽量保证 topic 数量不要过多,如果要支撑大规模的 topic,需要增加更多的机器资源 | -| 时效性 | ms 级 | 微秒级,这是 RabbitMQ 的一大特点,延迟最低 | ms 级 | 延迟在 ms 级以内 | -| 可用性 | 高,基于主从架构实现高可用 | 同 ActiveMQ | 非常高,分布式架构 | 非常高,分布式,一个数据多个副本,少数机器宕机,不会丢失数据,不会导致不可用 | -| 消息可靠性 | 有较低的概率丢失数据 | 基本不丢 | 经过参数优化配置,可以做到 0 丢失 | 同 RocketMQ | -| 功能支持 | MQ 领域的功能极其完备 | 基于 erlang 开发,并发能力很强,性能极好,延时很低 | MQ 功能较为完善,还是分布式的,扩展性好 | 功能较为简单,主要支持简单的 MQ 功能,在大数据领域的实时计算以及日志采集被大规模使用 | - -综上,各种对比之后,有如下建议: - -- 一般的业务系统要引入 MQ,最早大家都用 ActiveMQ,但是现在确实大家用的不多了,没经过大规模吞吐量场景的验证,社区也不是很活跃,所以大家还是算了吧,我个人不推荐用这个了; -- 后来大家开始用 RabbitMQ,但是确实 erlang 语言阻止了大量的 Java 工程师去深入研究和掌控它,对公司而言,几乎处于不可控的状态,但是确实人家是开源的,比较稳定的支持,活跃度也高; -- 不过现在确实越来越多的公司会去用 RocketMQ,确实很不错,毕竟是阿里出品,但社区可能有突然黄掉的风险(目前 RocketMQ 已捐给 [Apache](https://github.com/apache/rocketmq),但 GitHub 上的活跃度其实不算高)对自己公司技术实力有绝对自信的,推荐用 RocketMQ,否则回去老老实实用 RabbitMQ 吧,人家有活跃的开源社区,绝对不会黄。 -- 所以**中小型公司**,技术实力较为一般,技术挑战不是特别高,用 RabbitMQ 是不错的选择;**大型公司**,基础架构研发实力较强,用 RocketMQ 是很好的选择。 -- 如果是**大数据领域**的实时计算、日志采集等场景,用 Kafka 是业内标准的,绝对没问题,社区活跃度很高,绝对不会黄,何况几乎是全世界这个领域的事实性规范。 - -## 📚 资料 - -- **Kafka** - - [Kafka Github](https://github.com/apache/kafka) - - [Kafka 官网](http://kafka.apache.org/) - - [Kafka 官方文档](https://kafka.apache.org/documentation/) - - [Kafka 中文文档](https://github.com/apachecn/kafka-doc-zh) -- **ActiveMQ** - - [ActiveMQ 官网](http://activemq.apache.org/) - -## 🚪 传送 - -◾ 🏠 [JAVA-TUTORIAL 首页](https://github.com/dunwu/java-tutorial) ◾ 🎯 [我的博客](https://github.com/dunwu/blog) ◾ diff --git "a/docs/14.\344\270\255\351\227\264\344\273\266/02.\347\274\223\345\255\230/01.\347\274\223\345\255\230\351\235\242\350\257\225\351\242\230.md" "b/docs/14.\344\270\255\351\227\264\344\273\266/02.\347\274\223\345\255\230/01.\347\274\223\345\255\230\351\235\242\350\257\225\351\242\230.md" deleted file mode 100644 index 6a76cf6a..00000000 --- "a/docs/14.\344\270\255\351\227\264\344\273\266/02.\347\274\223\345\255\230/01.\347\274\223\345\255\230\351\235\242\350\257\225\351\242\230.md" +++ /dev/null @@ -1,588 +0,0 @@ ---- -title: 缓存夺命连环问 -categories: - - 编程 - - Java - - 中间件 - - 缓存 -tags: - - Java - - 中间件 - - 缓存 - - 面试 -abbrlink: ba1174c4 -date: 2022-02-17 22:34:30 -permalink: /pages/eb30d6/ ---- - -# 缓存夺命连环问 - -## 为什么要用缓存? - -用缓存,主要有两个用途:**高性能**、**高并发**。 - -### 高性能 - -假设这么个场景,你有个操作,一个请求过来,吭哧吭哧你各种乱七八糟操作 mysql,半天查出来一个结果,耗时 600ms。但是这个结果可能接下来几个小时都不会变了,或者变了也可以不用立即反馈给用户。那么此时咋办? - -缓存啊,折腾 600ms 查出来的结果,扔缓存里,一个 key 对应一个 value,下次再有人查,别走 mysql 折腾 600ms 了,直接从缓存里,通过一个 key 查出来一个 value,2ms 搞定。性能提升 300 倍。 - -就是说对于一些需要复杂操作耗时查出来的结果,且确定后面不怎么变化,但是有很多读请求,那么直接将查询出来的结果放在缓存中,后面直接读缓存就好。 - -### 高并发 - -mysql 这么重的数据库,压根儿设计不是让你玩儿高并发的,虽然也可以玩儿,但是天然支持不好。mysql 单机支撑到 `2000QPS` 也开始容易报警了。 - -所以要是你有个系统,高峰期一秒钟过来的请求有 1 万,那一个 mysql 单机绝对会死掉。你这个时候就只能上缓存,把很多数据放缓存,别放 mysql。缓存功能简单,说白了就是 `key-value` 式操作,单机支撑的并发量轻松一秒几万十几万,支撑高并发 so easy。单机承载并发量是 mysql 单机的几十倍。 - -> 缓存是走内存的,内存天然就支撑高并发。 - -## 用了缓存之后会有什么不良后果? - -常见的缓存问题有以下几个: - -- [缓存与数据库双写不一致](https://github.com/doocs/advanced-java/blob/master/docs/high-concurrency/redis-consistence.md) -- [缓存雪崩、缓存穿透](https://github.com/doocs/advanced-java/blob/master/docs/high-concurrency/redis-caching-avalanche-and-caching-penetration.md) -- [缓存并发竞争](https://github.com/doocs/advanced-java/blob/master/docs/high-concurrency/redis-cas.md) - -后面再详细说明。 - -## redis 和 memcached 有啥区别? - -redis 支持复杂的数据结构 - -redis 相比 memcached 来说,拥有[更多的数据结构](https://github.com/doocs/advanced-java/blob/master/docs/high-concurrency/redis-data-types.md),能支持更丰富的数据操作。如果需要缓存能够支持更复杂的结构和操作, redis 会是不错的选择。 - -redis 原生支持集群模式 - -在 redis3.x 版本中,便能支持 cluster 模式,而 memcached 没有原生的集群模式,需要依靠客户端来实现往集群中分片写入数据。 - -性能对比 - -由于 redis 只使用**单核**,而 memcached 可以使用**多核**,所以平均每一个核上 redis 在存储小数据时比 memcached 性能更高。而在 100k 以上的数据中,memcached 性能要高于 redis。虽然 redis 最近也在存储大数据的性能上进行优化,但是比起 memcached,还是稍有逊色。 - -## 为啥 redis 单线程模型也能效率这么高? - -### redis 的线程模型 - -redis 内部使用文件事件处理器 `file event handler`,这个文件事件处理器是单线程的,所以 redis 才叫做单线程的模型。它采用 IO 多路复用机制同时监听多个 socket,将产生事件的 socket 压入内存队列中,事件分派器根据 socket 上的事件类型来选择对应的事件处理器进行处理。 - -文件事件处理器的结构包含 4 个部分: - -- 多个 socket -- IO 多路复用程序 -- 文件事件分派器 -- 事件处理器(连接应答处理器、命令请求处理器、命令回复处理器) - -多个 socket 可能会并发产生不同的操作,每个操作对应不同的文件事件,但是 IO 多路复用程序会监听多个 socket,会将产生事件的 socket 放入队列中排队,事件分派器每次从队列中取出一个 socket,根据 socket 的事件类型交给对应的事件处理器进行处理。 - -来看客户端与 redis 的一次通信过程: - -![img](https://github.com/doocs/advanced-java/blob/master/images/redis-single-thread-model.png) - -要明白,通信是通过 socket 来完成的,不懂的同学可以先去看一看 socket 网络编程。 - -首先,redis 服务端进程初始化的时候,会将 server socket 的 `AE_READABLE` 事件与连接应答处理器关联。 - -客户端 socket01 向 redis 进程的 server socket 请求建立连接,此时 server socket 会产生一个 `AE_READABLE` 事件,IO 多路复用程序监听到 server socket 产生的事件后,将该 socket 压入队列中。文件事件分派器从队列中获取 socket,交给**连接应答处理器**。连接应答处理器会创建一个能与客户端通信的 socket01,并将该 socket01 的 `AE_READABLE` 事件与命令请求处理器关联。 - -假设此时客户端发送了一个 `set key value` 请求,此时 redis 中的 socket01 会产生 `AE_READABLE` 事件,IO 多路复用程序将 socket01 压入队列,此时事件分派器从队列中获取到 socket01 产生的 `AE_READABLE` 事件,由于前面 socket01 的 `AE_READABLE` 事件已经与命令请求处理器关联,因此事件分派器将事件交给命令请求处理器来处理。命令请求处理器读取 socket01 的 `key value` 并在自己内存中完成 `key value` 的设置。操作完成后,它会将 socket01 的 `AE_WRITABLE` 事件与命令回复处理器关联。 - -如果此时客户端准备好接收返回结果了,那么 redis 中的 socket01 会产生一个 `AE_WRITABLE` 事件,同样压入队列中,事件分派器找到相关联的命令回复处理器,由命令回复处理器对 socket01 输入本次操作的一个结果,比如 `ok`,之后解除 socket01 的 `AE_WRITABLE` 事件与命令回复处理器的关联。 - -这样便完成了一次通信。 - -### 为啥 redis 单线程模型也能效率这么高? - -- 纯内存操作。 -- 核心是基于非阻塞的 IO 多路复用机制。 -- C 语言实现,一般来说,C 语言实现的程序“距离”操作系统更近,执行速度相对会更快。 -- 单线程反而避免了多线程的频繁上下文切换问题,预防了多线程可能产生的竞争问题。 - -## Redis 有哪些数据类型 - -redis 主要有以下几种数据类型: - -- string -- hash -- list -- set -- sorted set - -### string - -这是最简单的类型,就是普通的 set 和 get,做简单的 KV 缓存。 - -``` -set college szu -``` - -### hash - -这个是类似 map 的一种结构,这个一般就是可以将结构化的数据,比如一个对象(前提是**这个对象没嵌套其他的对象**)给缓存在 redis 里,然后每次读写缓存的时候,可以就操作 hash 里的**某个字段**。 - -```bash -hset person name bingo -hset person age 20 -hset person id 1 -hget person name -person = { - "name": "bingo", - "age": 20, - "id": 1 -} -``` - -### list - -list 是有序列表,这个可以玩儿出很多花样。 - -比如可以通过 list 存储一些列表型的数据结构,类似粉丝列表、文章的评论列表之类的东西。 - -比如可以通过 lrange 命令,读取某个闭区间内的元素,可以基于 list 实现分页查询,这个是很棒的一个功能,基于 redis 实现简单的高性能分页,可以做类似微博那种下拉不断分页的东西,性能高,就一页一页走。 - -``` -# 0开始位置,-1结束位置,结束位置为-1时,表示列表的最后一个位置,即查看所有。 -lrange mylist 0 -1 -``` - -比如可以搞个简单的消息队列,从 list 头怼进去,从 list 尾巴那里弄出来。 - -``` -lpush mylist 1 -lpush mylist 2 -lpush mylist 3 4 5 - -# 1 -rpop mylist -``` - -### set - -set 是无序集合,自动去重。 - -直接基于 set 将系统里需要去重的数据扔进去,自动就给去重了,如果你需要对一些数据进行快速的全局去重,你当然也可以基于 jvm 内存里的 HashSet 进行去重,但是如果你的某个系统部署在多台机器上呢?得基于 redis 进行全局的 set 去重。 - -可以基于 set 玩儿交集、并集、差集的操作,比如交集吧,可以把两个人的粉丝列表整一个交集,看看俩人的共同好友是谁?对吧。 - -把两个大 V 的粉丝都放在两个 set 中,对两个 set 做交集。 - -``` -#-------操作一个set------- -# 添加元素 -sadd mySet 1 - -# 查看全部元素 -smembers mySet - -# 判断是否包含某个值 -sismember mySet 3 - -# 删除某个/些元素 -srem mySet 1 -srem mySet 2 4 - -# 查看元素个数 -scard mySet - -# 随机删除一个元素 -spop mySet - -#-------操作多个set------- -# 将一个set的元素移动到另外一个set -smove yourSet mySet 2 - -# 求两set的交集 -sinter yourSet mySet - -# 求两set的并集 -sunion yourSet mySet - -# 求在yourSet中而不在mySet中的元素 -sdiff yourSet mySet -``` - -### sorted set - -sorted set 是排序的 set,去重但可以排序,写进去的时候给一个分数,自动根据分数排序。 - -``` -zadd board 85 zhangsan -zadd board 72 lisi -zadd board 96 wangwu -zadd board 63 zhaoliu - -# 获取排名前三的用户(默认是升序,所以需要 rev 改为降序) -zrevrange board 0 3 - -# 获取某用户的排名 -zrank board zhaoliu -``` - -## 如何保证 redis 的高并发和高可用?redis 的主从复制原理能介绍一下么?redis 的哨兵原理能介绍一下么? - -如果你用 redis 缓存技术的话,肯定要考虑如何用 redis 来加多台机器,保证 redis 是高并发的,还有就是如何让 redis 保证自己不是挂掉以后就直接死掉了,即 redis 高可用。 - -由于此节内容较多,因此,会分为两个小节进行讲解。 - -- [redis 主从架构](https://github.com/doocs/advanced-java/blob/master/docs/high-concurrency/redis-master-slave.md) -- [redis 基于哨兵实现高可用](https://github.com/doocs/advanced-java/blob/master/docs/high-concurrency/redis-sentinel.md) - -redis 实现**高并发**主要依靠**主从架构**,一主多从,一般来说,很多项目其实就足够了,单主用来写入数据,单机几万 QPS,多从用来查询数据,多个从实例可以提供每秒 10w 的 QPS。 - -如果想要在实现高并发的同时,容纳大量的数据,那么就需要 redis 集群,使用 redis 集群之后,可以提供每秒几十万的读写并发。 - -redis 高可用,如果是做主从架构部署,那么加上哨兵就可以了,就可以实现,任何一个实例宕机,可以进行主备切换。 - -## redis 的过期策略都有哪些?内存淘汰机制都有哪些?手写一下 LRU 代码实现? - -### redis 过期策略 - -redis 过期策略是:**定期删除+惰性删除**。 - -所谓**定期删除**,指的是 redis 默认是每隔 100ms 就随机抽取一些设置了过期时间的 key,检查其是否过期,如果过期就删除。 - -假设 redis 里放了 10w 个 key,都设置了过期时间,你每隔几百毫秒,就检查 10w 个 key,那 redis 基本上就死了,cpu 负载会很高的,消耗在你的检查过期 key 上了。注意,这里可不是每隔 100ms 就遍历所有的设置过期时间的 key,那样就是一场性能上的**灾难**。实际上 redis 是每隔 100ms **随机抽取**一些 key 来检查和删除的。 - -但是问题是,定期删除可能会导致很多过期 key 到了时间并没有被删除掉,那咋整呢?所以就是惰性删除了。这就是说,在你获取某个 key 的时候,redis 会检查一下 ,这个 key 如果设置了过期时间那么是否过期了?如果过期了此时就会删除,不会给你返回任何东西。 - -> 获取 key 的时候,如果此时 key 已经过期,就删除,不会返回任何东西。 - -但是实际上这还是有问题的,如果定期删除漏掉了很多过期 key,然后你也没及时去查,也就没走惰性删除,此时会怎么样?如果大量过期 key 堆积在内存里,导致 redis 内存块耗尽了,咋整? - -答案是:**走内存淘汰机制**。 - -### 内存淘汰机制 - -redis 内存淘汰机制有以下几个: - -- noeviction: 当内存不足以容纳新写入数据时,新写入操作会报错,这个一般没人用吧,实在是太恶心了。 -- **allkeys-lru**:当内存不足以容纳新写入数据时,在**键空间**中,移除最近最少使用的 key(这个是**最常用**的)。 -- allkeys-random:当内存不足以容纳新写入数据时,在**键空间**中,随机移除某个 key,这个一般没人用吧,为啥要随机,肯定是把最近最少使用的 key 给干掉啊。 -- volatile-lru:当内存不足以容纳新写入数据时,在**设置了过期时间的键空间**中,移除最近最少使用的 key(这个一般不太合适)。 -- volatile-random:当内存不足以容纳新写入数据时,在**设置了过期时间的键空间**中,**随机移除**某个 key。 -- volatile-ttl:当内存不足以容纳新写入数据时,在**设置了过期时间的键空间**中,有**更早过期时间**的 key 优先移除。 - -### 手写一个 LRU 算法 - -你可以现场手写最原始的 LRU 算法,那个代码量太大了,似乎不太现实。 - -不求自己纯手工从底层开始打造出自己的 LRU,但是起码要知道如何利用已有的 JDK 数据结构实现一个 Java 版的 LRU。 - -```java -class LRUCache extends LinkedHashMap { - private final int CACHE_SIZE; - - /** - * 传递进来最多能缓存多少数据 - * - * @param cacheSize 缓存大小 - */ - public LRUCache(int cacheSize) { - // true 表示让 linkedHashMap 按照访问顺序来进行排序,最近访问的放在头部,最老访问的放在尾部。 - super((int) Math.ceil(cacheSize / 0.75) + 1, 0.75f, true); - CACHE_SIZE = cacheSize; - } - - @Override - protected boolean removeEldestEntry(Map.Entry eldest) { - // 当 map中的数据量大于指定的缓存个数的时候,就自动删除最老的数据。 - return size() > CACHE_SIZE; - } -} -``` - -## redis 集群模式的工作原理能说一下么?在集群模式下,redis 的 key 是如何寻址的?分布式寻址都有哪些算法?了解一致性 hash 算法吗? - -在前几年,redis 如果要搞几个节点,每个节点存储一部分的数据,得**借助一些中间件**来实现,比如说有 `codis`,或者 `twemproxy`,都有。有一些 redis 中间件,你读写 redis 中间件,redis 中间件负责将你的数据分布式存储在多台机器上的 redis 实例中。 - -这两年,redis 不断在发展,redis 也不断有新的版本,现在的 redis 集群模式,可以做到在多台机器上,部署多个 redis 实例,每个实例存储一部分的数据,同时每个 redis 主实例可以挂 redis 从实例,自动确保说,如果 redis 主实例挂了,会自动切换到 redis 从实例上来。 - -现在 redis 的新版本,大家都是用 redis cluster 的,也就是 redis 原生支持的 redis 集群模式,那么面试官肯定会就 redis cluster 对你来个几连炮。要是你没用过 redis cluster,正常,以前很多人用 codis 之类的客户端来支持集群,但是起码你得研究一下 redis cluster 吧。 - -如果你的数据量很少,主要是承载高并发高性能的场景,比如你的缓存一般就几个 G,单机就足够了,可以使用 replication,一个 master 多个 slaves,要几个 slave 跟你要求的读吞吐量有关,然后自己搭建一个 sentinel 集群去保证 redis 主从架构的高可用性。 - -redis cluster,主要是针对**海量数据+高并发+高可用**的场景。redis cluster 支撑 N 个 redis master node,每个 master node 都可以挂载多个 slave node。这样整个 redis 就可以横向扩容了。如果你要支撑更大数据量的缓存,那就横向扩容更多的 master 节点,每个 master 节点就能存放更多的数据了。 - -### 面试题剖析 - -### redis cluster 介绍 - -- 自动将数据进行分片,每个 master 上放一部分数据 -- 提供内置的高可用支持,部分 master 不可用时,还是可以继续工作的 - -在 redis cluster 架构下,每个 redis 要放开两个端口号,比如一个是 6379,另外一个就是 加 1w 的端口号,比如 16379。 - -16379 端口号是用来进行节点间通信的,也就是 cluster bus 的东西,cluster bus 的通信,用来进行故障检测、配置更新、故障转移授权。cluster bus 用了另外一种二进制的协议,`gossip` 协议,用于节点间进行高效的数据交换,占用更少的网络带宽和处理时间。 - -### 节点间的内部通信机制 - -#### 基本通信原理 - -集群元数据的维护有两种方式:集中式、Gossip 协议。redis cluster 节点间采用 gossip 协议进行通信。 - -**集中式**是将集群元数据(节点信息、故障等等)几种存储在某个节点上。集中式元数据集中存储的一个典型代表,就是大数据领域的 `storm`。它是分布式的大数据实时计算引擎,是集中式的元数据存储的结构,底层基于 zookeeper(分布式协调的中间件)对所有元数据进行存储维护。 - -![img](https://github.com/doocs/advanced-java/blob/master/images/zookeeper-centralized-storage.png) - -redis 维护集群元数据采用另一个方式, `gossip` 协议,所有节点都持有一份元数据,不同的节点如果出现了元数据的变更,就不断将元数据发送给其它的节点,让其它节点也进行元数据的变更。 - -![img](https://github.com/doocs/advanced-java/blob/master/images/redis-gossip.png) - -**集中式**的**好处**在于,元数据的读取和更新,时效性非常好,一旦元数据出现了变更,就立即更新到集中式的存储中,其它节点读取的时候就可以感知到;**不好**在于,所有的元数据的更新压力全部集中在一个地方,可能会导致元数据的存储有压力。 - -gossip 好处在于,元数据的更新比较分散,不是集中在一个地方,更新请求会陆陆续续打到所有节点上去更新,降低了压力;不好在于,元数据的更新有延时,可能导致集群中的一些操作会有一些滞后。 - -- 10000 端口:每个节点都有一个专门用于节点间通信的端口,就是自己提供服务的端口号+10000,比如 7001,那么用于节点间通信的就是 17001 端口。每个节点每隔一段时间都会往另外几个节点发送 `ping` 消息,同时其它几个节点接收到 `ping` 之后返回 `pong`。 -- 交换的信息:信息包括故障信息,节点的增加和删除,hash slot 信息等等。 - -#### gossip 协议 - -gossip 协议包含多种消息,包含 `ping`,`pong`,`meet`,`fail` 等等。 - -- meet:某个节点发送 meet 给新加入的节点,让新节点加入集群中,然后新节点就会开始与其它节点进行通信。 - -``` -redis-trib.rb add-node -``` - -其实内部就是发送了一个 gossip meet 消息给新加入的节点,通知那个节点去加入我们的集群。 - -- ping:每个节点都会频繁给其它节点发送 ping,其中包含自己的状态还有自己维护的集群元数据,互相通过 ping 交换元数据。 -- pong:返回 ping 和 meeet,包含自己的状态和其它信息,也用于信息广播和更新。 -- fail:某个节点判断另一个节点 fail 之后,就发送 fail 给其它节点,通知其它节点说,某个节点宕机啦。 - -#### ping 消息深入 - -ping 时要携带一些元数据,如果很频繁,可能会加重网络负担。 - -每个节点每秒会执行 10 次 ping,每次会选择 5 个最久没有通信的其它节点。当然如果发现某个节点通信延时达到了 `cluster_node_timeout / 2`,那么立即发送 ping,避免数据交换延时过长,落后的时间太长了。比如说,两个节点之间都 10 分钟没有交换数据了,那么整个集群处于严重的元数据不一致的情况,就会有问题。所以 `cluster_node_timeout` 可以调节,如果调得比较大,那么会降低 ping 的频率。 - -每次 ping,会带上自己节点的信息,还有就是带上 1/10 其它节点的信息,发送出去,进行交换。至少包含 `3` 个其它节点的信息,最多包含 `总节点数减 2` 个其它节点的信息。 - -### 分布式寻址算法 - -- hash 算法(大量缓存重建) -- 一致性 hash 算法(自动缓存迁移)+ 虚拟节点(自动负载均衡) -- redis cluster 的 hash slot 算法 - -#### hash 算法 - -来了一个 key,首先计算 hash 值,然后对节点数取模。然后打在不同的 master 节点上。一旦某一个 master 节点宕机,所有请求过来,都会基于最新的剩余 master 节点数去取模,尝试去取数据。这会导致**大部分的请求过来,全部无法拿到有效的缓存**,导致大量的流量涌入数据库。 - -![img](https://github.com/doocs/advanced-java/blob/master/images/hash.png) - -#### 一致性 hash 算法 - -一致性 hash 算法将整个 hash 值空间组织成一个虚拟的圆环,整个空间按顺时针方向组织,下一步将各个 master 节点(使用服务器的 ip 或主机名)进行 hash。这样就能确定每个节点在其哈希环上的位置。 - -来了一个 key,首先计算 hash 值,并确定此数据在环上的位置,从此位置沿环**顺时针“行走”**,遇到的第一个 master 节点就是 key 所在位置。 - -在一致性哈希算法中,如果一个节点挂了,受影响的数据仅仅是此节点到环空间前一个节点(沿着逆时针方向行走遇到的第一个节点)之间的数据,其它不受影响。增加一个节点也同理。 - -燃鹅,一致性哈希算法在节点太少时,容易因为节点分布不均匀而造成**缓存热点**的问题。为了解决这种热点问题,一致性 hash 算法引入了虚拟节点机制,即对每一个节点计算多个 hash,每个计算结果位置都放置一个虚拟节点。这样就实现了数据的均匀分布,负载均衡。 - -![img](https://github.com/doocs/advanced-java/blob/master/images/consistent-hashing-algorithm.png) - -#### redis cluster 的 hash slot 算法 - -redis cluster 有固定的 `16384` 个 hash slot,对每个 `key` 计算 `CRC16` 值,然后对 `16384` 取模,可以获取 key 对应的 hash slot。 - -redis cluster 中每个 master 都会持有部分 slot,比如有 3 个 master,那么可能每个 master 持有 5000 多个 hash slot。hash slot 让 node 的增加和移除很简单,增加一个 master,就将其他 master 的 hash slot 移动部分过去,减少一个 master,就将它的 hash slot 移动到其他 master 上去。移动 hash slot 的成本是非常低的。客户端的 api,可以对指定的数据,让他们走同一个 hash slot,通过 `hash tag` 来实现。 - -任何一台机器宕机,另外两个节点,不影响的。因为 key 找的是 hash slot,不是机器。 - -![img](https://github.com/doocs/advanced-java/blob/master/images/hash-slot.png) - -### redis cluster 的高可用与主备切换原理 - -redis cluster 的高可用的原理,几乎跟哨兵是类似的。 - -#### 判断节点宕机 - -如果一个节点认为另外一个节点宕机,那么就是 `pfail`,**主观宕机**。如果多个节点都认为另外一个节点宕机了,那么就是 `fail`,**客观宕机**,跟哨兵的原理几乎一样,sdown,odown。 - -在 `cluster-node-timeout` 内,某个节点一直没有返回 `pong`,那么就被认为 `pfail`。 - -如果一个节点认为某个节点 `pfail` 了,那么会在 `gossip ping` 消息中,`ping` 给其他节点,如果**超过半数**的节点都认为 `pfail` 了,那么就会变成 `fail`。 - -#### 从节点过滤 - -对宕机的 master node,从其所有的 slave node 中,选择一个切换成 master node。 - -检查每个 slave node 与 master node 断开连接的时间,如果超过了 `cluster-node-timeout * cluster-slave-validity-factor`,那么就**没有资格**切换成 `master`。 - -#### 从节点选举 - -每个从节点,都根据自己对 master 复制数据的 offset,来设置一个选举时间,offset 越大(复制数据越多)的从节点,选举时间越靠前,优先进行选举。 - -所有的 master node 开始 slave 选举投票,给要进行选举的 slave 进行投票,如果大部分 master node`(N/2 + 1)`都投票给了某个从节点,那么选举通过,那个从节点可以切换成 master。 - -从节点执行主备切换,从节点切换为主节点。 - -#### 与哨兵比较 - -整个流程跟哨兵相比,非常类似,所以说,redis cluster 功能强大,直接集成了 replication 和 sentinel 的功能。 - -## 如何保证缓存与数据库的双写一致性? - -一般来说,如果允许缓存可以稍微的跟数据库偶尔有不一致的情况,也就是说如果你的系统**不是严格要求** “缓存+数据库” 必须保持一致性的话,最好不要做这个方案,即:**读请求和写请求串行化**,串到一个**内存队列**里去。 - -串行化可以保证一定不会出现不一致的情况,但是它也会导致系统的吞吐量大幅度降低,用比正常情况下多几倍的机器去支撑线上的一个请求。 - -### Cache Aside Pattern - -最经典的缓存+数据库读写的模式,就是 Cache Aside Pattern。 - -- 读的时候,先读缓存,缓存没有的话,就读数据库,然后取出数据后放入缓存,同时返回响应。 -- 更新的时候,**先更新数据库,然后再删除缓存**。 - -**为什么是删除缓存,而不是更新缓存?** - -原因很简单,很多时候,在复杂点的缓存场景,缓存不单单是数据库中直接取出来的值。 - -比如可能更新了某个表的一个字段,然后其对应的缓存,是需要查询另外两个表的数据并进行运算,才能计算出缓存最新的值的。 - -另外更新缓存的代价有时候是很高的。是不是说,每次修改数据库的时候,都一定要将其对应的缓存更新一份?也许有的场景是这样,但是对于**比较复杂的缓存数据计算的场景**,就不是这样了。如果你频繁修改一个缓存涉及的多个表,缓存也频繁更新。但是问题在于,**这个缓存到底会不会被频繁访问到?** - -举个栗子,一个缓存涉及的表的字段,在 1 分钟内就修改了 20 次,或者是 100 次,那么缓存更新 20 次、100 次;但是这个缓存在 1 分钟内只被读取了 1 次,有**大量的冷数据**。实际上,如果你只是删除缓存的话,那么在 1 分钟内,这个缓存不过就重新计算一次而已,开销大幅度降低。**用到缓存才去算缓存。** - -其实删除缓存,而不是更新缓存,就是一个 lazy 计算的思想,不要每次都重新做复杂的计算,不管它会不会用到,而是让它到需要被使用的时候再重新计算。像 mybatis,hibernate,都有懒加载思想。查询一个部门,部门带了一个员工的 list,没有必要说每次查询部门,都里面的 1000 个员工的数据也同时查出来啊。80% 的情况,查这个部门,就只是要访问这个部门的信息就可以了。先查部门,同时要访问里面的员工,那么这个时候只有在你要访问里面的员工的时候,才会去数据库里面查询 1000 个员工。 - -### 最初级的缓存不一致问题及解决方案 - -问题:先更新数据库,再删除缓存。如果删除缓存失败了,那么会导致数据库中是新数据,缓存中是旧数据,数据就出现了不一致。 - -![img](https://github.com/doocs/advanced-java/blob/master/images/redis-junior-inconsistent.png) - -解决思路:先删除缓存,再更新数据库。如果数据库更新失败了,那么数据库中是旧数据,缓存中是空的,那么数据不会不一致。因为读的时候缓存没有,所以去读了数据库中的旧数据,然后更新到缓存中。 - -### 比较复杂的数据不一致问题分析 - -数据发生了变更,先删除了缓存,然后要去修改数据库,此时还没修改。一个请求过来,去读缓存,发现缓存空了,去查询数据库,**查到了修改前的旧数据**,放到了缓存中。随后数据变更的程序完成了数据库的修改。完了,数据库和缓存中的数据不一样了... - -**为什么上亿流量高并发场景下,缓存会出现这个问题?** - -只有在对一个数据在并发的进行读写的时候,才可能会出现这种问题。其实如果说你的并发量很低的话,特别是读并发很低,每天访问量就 1 万次,那么很少的情况下,会出现刚才描述的那种不一致的场景。但是问题是,如果每天的是上亿的流量,每秒并发读是几万,每秒只要有数据更新的请求,就**可能会出现上述的数据库+缓存不一致的情况**。 - -**解决方案如下:** - -更新数据的时候,根据**数据的唯一标识**,将操作路由之后,发送到一个 jvm 内部队列中。读取数据的时候,如果发现数据不在缓存中,那么将重新读取数据+更新缓存的操作,根据唯一标识路由之后,也发送同一个 jvm 内部队列中。 - -一个队列对应一个工作线程,每个工作线程**串行**拿到对应的操作,然后一条一条的执行。这样的话,一个数据变更的操作,先删除缓存,然后再去更新数据库,但是还没完成更新。此时如果一个读请求过来,没有读到缓存,那么可以先将缓存更新的请求发送到队列中,此时会在队列中积压,然后同步等待缓存更新完成。 - -这里有一个**优化点**,一个队列中,其实**多个更新缓存请求串在一起是没意义的**,因此可以做过滤,如果发现队列中已经有一个更新缓存的请求了,那么就不用再放个更新请求操作进去了,直接等待前面的更新操作请求完成即可。 - -待那个队列对应的工作线程完成了上一个操作的数据库的修改之后,才会去执行下一个操作,也就是缓存更新的操作,此时会从数据库中读取最新的值,然后写入缓存中。 - -如果请求还在等待时间范围内,不断轮询发现可以取到值了,那么就直接返回;如果请求等待的时间超过一定时长,那么这一次直接从数据库中读取当前的旧值。 - -高并发的场景下,该解决方案要注意的问题: - -- 读请求长时阻塞 - -由于读请求进行了非常轻度的异步化,所以一定要注意读超时的问题,每个读请求必须在超时时间范围内返回。 - -该解决方案,最大的风险点在于说,**可能数据更新很频繁**,导致队列中积压了大量更新操作在里面,然后**读请求会发生大量的超时**,最后导致大量的请求直接走数据库。务必通过一些模拟真实的测试,看看更新数据的频率是怎样的。 - -另外一点,因为一个队列中,可能会积压针对多个数据项的更新操作,因此需要根据自己的业务情况进行测试,可能需要**部署多个服务**,每个服务分摊一些数据的更新操作。如果一个内存队列里居然会挤压 100 个商品的库存修改操作,每隔库存修改操作要耗费 10ms 去完成,那么最后一个商品的读请求,可能等待 10 \* 100 = 1000ms = 1s 后,才能得到数据,这个时候就导致**读请求的长时阻塞**。 - -一定要做根据实际业务系统的运行情况,去进行一些压力测试,和模拟线上环境,去看看最繁忙的时候,内存队列可能会挤压多少更新操作,可能会导致最后一个更新操作对应的读请求,会 hang 多少时间,如果读请求在 200ms 返回,如果你计算过后,哪怕是最繁忙的时候,积压 10 个更新操作,最多等待 200ms,那还可以的。 - -**如果一个内存队列中可能积压的更新操作特别多**,那么你就要**加机器**,让每个机器上部署的服务实例处理更少的数据,那么每个内存队列中积压的更新操作就会越少。 - -其实根据之前的项目经验,一般来说,数据的写频率是很低的,因此实际上正常来说,在队列中积压的更新操作应该是很少的。像这种针对读高并发、读缓存架构的项目,一般来说写请求是非常少的,每秒的 QPS 能到几百就不错了。 - -我们来**实际粗略测算一下**。 - -如果一秒有 500 的写操作,如果分成 5 个时间片,每 200ms 就 100 个写操作,放到 20 个内存队列中,每个内存队列,可能就积压 5 个写操作。每个写操作性能测试后,一般是在 20ms 左右就完成,那么针对每个内存队列的数据的读请求,也就最多 hang 一会儿,200ms 以内肯定能返回了。 - -经过刚才简单的测算,我们知道,单机支撑的写 QPS 在几百是没问题的,如果写 QPS 扩大了 10 倍,那么就扩容机器,扩容 10 倍的机器,每个机器 20 个队列。 - -- 读请求并发量过高 - -这里还必须做好压力测试,确保恰巧碰上上述情况的时候,还有一个风险,就是突然间大量读请求会在几十毫秒的延时 hang 在服务上,看服务能不能扛的住,需要多少机器才能扛住最大的极限情况的峰值。 - -但是因为并不是所有的数据都在同一时间更新,缓存也不会同一时间失效,所以每次可能也就是少数数据的缓存失效了,然后那些数据对应的读请求过来,并发量应该也不会特别大。 - -- 多服务实例部署的请求路由 - -可能这个服务部署了多个实例,那么必须**保证**说,执行数据更新操作,以及执行缓存更新操作的请求,都通过 Nginx 服务器**路由到相同的服务实例上**。 - -比如说,对同一个商品的读写请求,全部路由到同一台机器上。可以自己去做服务间的按照某个请求参数的 hash 路由,也可以用 Nginx 的 hash 路由功能等等。 - -- 热点商品的路由问题,导致请求的倾斜 - -万一某个商品的读写请求特别高,全部打到相同的机器的相同的队列里面去了,可能会造成某台机器的压力过大。就是说,因为只有在商品数据更新的时候才会清空缓存,然后才会导致读写并发,所以其实要根据业务系统去看,如果更新频率不是太高的话,这个问题的影响并不是特别大,但是的确可能某些机器的负载会高一些。 - -## 了解什么是 redis 的雪崩、穿透和击穿?redis 崩溃之后会怎么样?系统该如何应对这种情况?如何处理 redis 的穿透? - -### 缓存雪崩 - -对于系统 A,假设每天高峰期每秒 5000 个请求,本来缓存在高峰期可以扛住每秒 4000 个请求,但是缓存机器意外发生了全盘宕机。缓存挂了,此时 1 秒 5000 个请求全部落数据库,数据库必然扛不住,它会报一下警,然后就挂了。此时,如果没有采用什么特别的方案来处理这个故障,DBA 很着急,重启数据库,但是数据库立马又被新的流量给打死了。 - -这就是缓存雪崩。 - -![img](https://github.com/doocs/advanced-java/blob/master/images/redis-caching-avalanche.png) - -大约在 3 年前,国内比较知名的一个互联网公司,曾因为缓存事故,导致雪崩,后台系统全部崩溃,事故从当天下午持续到晚上凌晨 3\~4 点,公司损失了几千万。 - -缓存雪崩的事前事中事后的解决方案如下。 - -- 事前:redis 高可用,主从+哨兵,redis cluster,避免全盘崩溃。 -- 事中:本地 ehcache 缓存 + hystrix 限流&降级,避免 MySQL 被打死。 -- 事后:redis 持久化,一旦重启,自动从磁盘上加载数据,快速恢复缓存数据。 - -![img](https://github.com/doocs/advanced-java/blob/master/images/redis-caching-avalanche-solution.png) - -用户发送一个请求,系统 A 收到请求后,先查本地 ehcache 缓存,如果没查到再查 redis。如果 ehcache 和 redis 都没有,再查数据库,将数据库中的结果,写入 ehcache 和 redis 中。 - -限流组件,可以设置每秒的请求,有多少能通过组件,剩余的未通过的请求,怎么办?**走降级**!可以返回一些默认的值,或者友情提示,或者空白的值。 - -好处: - -- 数据库绝对不会死,限流组件确保了每秒只有多少个请求能通过。 -- 只要数据库不死,就是说,对用户来说,2/5 的请求都是可以被处理的。 -- 只要有 2/5 的请求可以被处理,就意味着你的系统没死,对用户来说,可能就是点击几次刷不出来页面,但是多点几次,就可以刷出来一次。 - -### 缓存穿透 - -对于系统 A,假设一秒 5000 个请求,结果其中 4000 个请求是黑客发出的恶意攻击。 - -黑客发出的那 4000 个攻击,缓存中查不到,每次你去数据库里查,也查不到。 - -举个栗子。数据库 id 是从 1 开始的,结果黑客发过来的请求 id 全部都是负数。这样的话,缓存中不会有,请求每次都“视缓存于无物”,直接查询数据库。这种恶意攻击场景的缓存穿透就会直接把数据库给打死。 - -![img](https://github.com/doocs/advanced-java/blob/master/images/redis-caching-penetration.png) - -解决方式很简单,每次系统 A 从数据库中只要没查到,就写一个空值到缓存里去,比如 `set -999 UNKNOWN`。然后设置一个过期时间,这样的话,下次有相同的 key 来访问的时候,在缓存失效之前,都可以直接从缓存中取数据。 - -### 缓存击穿 - -缓存击穿,就是说某个 key 非常热点,访问非常频繁,处于集中式高并发访问的情况,当这个 key 在失效的瞬间,大量的请求就击穿了缓存,直接请求数据库,就像是在一道屏障上凿开了一个洞。 - -解决方式也很简单,可以将热点数据设置为永远不过期;或者基于 redis or zookeeper 实现互斥锁,等待第一个请求构建完缓存之后,再释放锁,进而其它请求才能通过该 key 访问数据。 - -## redis 的并发竞争问题是什么?如何解决这个问题?了解 redis 事务的 CAS 方案吗? - -某个时刻,多个系统实例都去更新某个 key。可以基于 zookeeper 实现分布式锁。每个系统通过 zookeeper 获取分布式锁,确保同一时间,只能有一个系统实例在操作某个 key,别人都不允许读和写。 - -![img](https://github.com/doocs/advanced-java/blob/master/images/zookeeper-distributed-lock.png) - -你要写入缓存的数据,都是从 mysql 里查出来的,都得写入 mysql 中,写入 mysql 中的时候必须保存一个时间戳,从 mysql 查出来的时候,时间戳也查出来。 - -每次要**写之前,先判断**一下当前这个 value 的时间戳是否比缓存里的 value 的时间戳要新。如果是的话,那么可以写,否则,就不能用旧的数据覆盖新的数据。 - -## 生产环境中的 redis 是怎么部署的? - -redis cluster,10 台机器,5 台机器部署了 redis 主实例,另外 5 台机器部署了 redis 的从实例,每个主实例挂了一个从实例,5 个节点对外提供读写服务,每个节点的读写高峰 qps 可能可以达到每秒 5 万,5 台机器最多是 25 万读写请求/s。 - -机器是什么配置?32G 内存+ 8 核 CPU + 1T 磁盘,但是分配给 redis 进程的是 10g 内存,一般线上生产环境,redis 的内存尽量不要超过 10g,超过 10g 可能会有问题。 - -5 台机器对外提供读写,一共有 50g 内存。 - -因为每个主实例都挂了一个从实例,所以是高可用的,任何一个主实例宕机,都会自动故障迁移,redis 从实例会自动变成主实例继续提供读写服务。 - -你往内存里写的是什么数据?每条数据的大小是多少?商品数据,每条数据是 10kb。100 条数据是 1mb,10 万条数据是 1g。常驻内存的是 200 万条商品数据,占用内存是 20g,仅仅不到总内存的 50%。目前高峰期每秒就是 3500 左右的请求量。 - -其实大型的公司,会有基础架构的 team 负责缓存集群的运维。 diff --git "a/docs/14.\344\270\255\351\227\264\344\273\266/02.\347\274\223\345\255\230/03.Memcached.md" "b/docs/14.\344\270\255\351\227\264\344\273\266/02.\347\274\223\345\255\230/03.Memcached.md" deleted file mode 100644 index c8dffe9b..00000000 --- "a/docs/14.\344\270\255\351\227\264\344\273\266/02.\347\274\223\345\255\230/03.Memcached.md" +++ /dev/null @@ -1,93 +0,0 @@ ---- -title: Memcached 快速入门 -categories: - - 编程 - - Java - - 中间件 - - 缓存 -tags: - - Java - - 中间件 - - 缓存 - - 面试 -abbrlink: 2e7f2a78 -date: 2022-02-17 22:34:30 -permalink: /pages/25a710/ ---- - -# Memcached 快速入门 - -## 一、Memcached 简介 - -Memcached 是一个自由开源的,高性能,分布式内存对象缓存系统。 - -Memcached 是一种基于内存的 key-value 存储,用来存储小块的任意数据(字符串、对象)。这些数据可以是数据库调用、API 调用或者是页面渲染的结果。 - -Memcached 简洁而强大。它的简洁设计便于快速开发,减轻开发难度,解决了大数据量缓存的很多问题。它的 API 兼容大部分流行的开发语言。本质上,它是一个简洁的 key-value 存储系统。 - -### Memcached 特性 - -memcached 作为高速运行的分布式缓存服务器,具有以下的特点。 - -- 协议简单 -- 基于 libevent 的事件处理 -- 内置内存存储方式 -- memcached 不互相通信的分布式 - -## 二、Memcached 命令 - -可以通过 telnet 命令并指定主机 ip 和端口来连接 Memcached 服务。 - -``` -telnet 127.0.0.1 11211 - -Trying 127.0.0.1... -Connected to 127.0.0.1. -Escape character is '^]'. -set foo 0 0 3 保存命令 -bar 数据 -STORED 结果 -get foo 取得命令 -VALUE foo 0 3 数据 -bar 数据 -END 结束行 -quit 退出 -``` - -## 三、Java 连接 Memcached - -使用 Java 程序连接 Memcached,需要在你的 classpath 中添加 Memcached jar 包。 - -本站 jar 包下载地址:[spymemcached-2.10.3.jar](https://www.runoob.com/try/download/spymemcached-2.10.3.jar)。 - -Google Code jar 包下载地址:[spymemcached-2.10.3.jar](http://code.google.com/p/spymemcached/downloads/list)(需要科学上网)。 - -以下程序假定 Memcached 服务的主机为 127.0.0.1,端口为 11211。 - -```java -import net.spy.memcached.MemcachedClient; -import java.net.*; - - -public class MemcachedJava { - public static void main(String[] args) { - try{ - // 本地连接 Memcached 服务 - MemcachedClient mcc = new MemcachedClient(new InetSocketAddress("127.0.0.1", 11211)); - System.out.println("Connection to server sucessful."); - - // 关闭连接 - mcc.shutdown(); - - }catch(Exception ex){ - System.out.println( ex.getMessage() ); - } - } -} -``` - -## 参考资料 - -- [Memcached 官网](https://memcached.org/) -- [Memcached Github](https://github.com/memcached/memcached/) -- [Memcached 教程](https://www.runoob.com/memcached/memcached-tutorial.html) diff --git a/docs/README.md b/docs/README.md index 56b1b599..f525fbba 100644 --- a/docs/README.md +++ b/docs/README.md @@ -48,11 +48,11 @@ footer: CC-BY-SA-4.0 Licensed | Copyright © 2018-Now Dunwu #### JavaWeb -- [JavaWeb 面经](02.JavaEE/01.JavaWeb/99.JavaWeb面经.md) -- [JavaWeb 之 Servlet 指南](02.JavaEE/01.JavaWeb/01.JavaWeb之Servlet指南.md) -- [JavaWeb 之 Jsp 指南](02.JavaEE/01.JavaWeb/02.JavaWeb之Jsp指南.md) -- [JavaWeb 之 Filter 和 Listener](02.JavaEE/01.JavaWeb/03.JavaWeb之Filter和Listener.md) -- [JavaWeb 之 Cookie 和 Session](02.JavaEE/01.JavaWeb/04.JavaWeb之Cookie和Session.md) +- [JavaWeb 面经](01.Java/02.JavaEE/01.JavaWeb/99.JavaWeb面经.md) +- [JavaWeb 之 Servlet 指南](01.Java/02.JavaEE/01.JavaWeb/01.JavaWeb之Servlet指南.md) +- [JavaWeb 之 Jsp 指南](01.Java/02.JavaEE/01.JavaWeb/02.JavaWeb之Jsp指南.md) +- [JavaWeb 之 Filter 和 Listener](01.Java/02.JavaEE/01.JavaWeb/03.JavaWeb之Filter和Listener.md) +- [JavaWeb 之 Cookie 和 Session](01.Java/02.JavaEE/01.JavaWeb/04.JavaWeb之Cookie和Session.md) #### Java 服务器 @@ -60,95 +60,95 @@ footer: CC-BY-SA-4.0 Licensed | Copyright © 2018-Now Dunwu > > Nginx 是目前最流行的反向代理服务器,也常用于负载均衡。 -- [Tomcat 快速入门](02.JavaEE/02.服务器/01.Tomcat/01.Tomcat快速入门.md) -- [Tomcat 连接器](02.JavaEE/02.服务器/01.Tomcat/02.Tomcat连接器.md) -- [Tomcat 容器](02.JavaEE/02.服务器/01.Tomcat/03.Tomcat容器.md) -- [Tomcat 优化](02.JavaEE/02.服务器/01.Tomcat/04.Tomcat优化.md) -- [Tomcat 和 Jetty](02.JavaEE/02.服务器/01.Tomcat/05.Tomcat和Jetty.md) -- [Jetty](02.JavaEE/02.服务器/02.Jetty.md) +- [Tomcat 快速入门](01.Java/02.JavaEE/02.服务器/01.Tomcat/01.Tomcat快速入门.md) +- [Tomcat 连接器](01.Java/02.JavaEE/02.服务器/01.Tomcat/02.Tomcat连接器.md) +- [Tomcat 容器](01.Java/02.JavaEE/02.服务器/01.Tomcat/03.Tomcat容器.md) +- [Tomcat 优化](01.Java/02.JavaEE/02.服务器/01.Tomcat/04.Tomcat优化.md) +- [Tomcat 和 Jetty](01.Java/02.JavaEE/02.服务器/01.Tomcat/05.Tomcat和Jetty.md) +- [Jetty](01.Java/02.JavaEE/02.服务器/02.Jetty.md) ### Java 软件 #### Java 构建 -> Java 项目需要通过 [**构建工具**](11.软件/01.构建) 来管理项目依赖,完成编译、打包、发布、生成 JavaDoc 等任务。 +> Java 项目需要通过 [**构建工具**](01.Java/11.软件/01.构建) 来管理项目依赖,完成编译、打包、发布、生成 JavaDoc 等任务。 > > - 目前最主流的构建工具是 Maven,它的功能非常强大。 > - Gradle 号称是要替代 Maven 等构件工具,它的版本管理确实简洁,但是需要学习 Groovy,学习成本比 Maven 高。 > - Ant 功能比 Maven 和 Gradle 要弱,现代 Java 项目基本不用了,但也有一些传统的 Java 项目还在使用。 -- [Maven](11.软件/01.构建/01.Maven) 📚 - - [Maven 快速入门](11.软件/01.构建/01.Maven/01.Maven快速入门.md) - - [Maven 教程之 pom.xml 详解](11.软件/01.构建/01.Maven/02.Maven教程之pom.xml详解.md) - - [Maven 教程之 settings.xml 详解](11.软件/01.构建/01.Maven/03.Maven教程之settings.xml详解.md) - - [Maven 实战问题和最佳实践](11.软件/01.构建/01.Maven/04.Maven实战问题和最佳实践.md) - - [Maven 教程之发布 jar 到私服或中央仓库](11.软件/01.构建/01.Maven/05.Maven教程之发布jar到私服或中央仓库.md) - - [Maven 插件之代码检查](11.软件/01.构建/01.Maven/06.Maven插件之代码检查.md) -- [Ant 简易教程](11.软件/01.构建/02.Ant.md) +- [Maven](01.Java/11.软件/01.构建/01.Maven) 📚 + - [Maven 快速入门](01.Java/11.软件/01.构建/01.Maven/01.Maven快速入门.md) + - [Maven 教程之 pom.xml 详解](01.Java/11.软件/01.构建/01.Maven/02.Maven教程之pom.xml详解.md) + - [Maven 教程之 settings.xml 详解](01.Java/11.软件/01.构建/01.Maven/03.Maven教程之settings.xml详解.md) + - [Maven 实战问题和最佳实践](01.Java/11.软件/01.构建/01.Maven/04.Maven实战问题和最佳实践.md) + - [Maven 教程之发布 jar 到私服或中央仓库](01.Java/11.软件/01.构建/01.Maven/05.Maven教程之发布jar到私服或中央仓库.md) + - [Maven 插件之代码检查](01.Java/11.软件/01.构建/01.Maven/06.Maven插件之代码检查.md) +- [Ant 简易教程](01.Java/11.软件/01.构建/02.Ant.md) #### Java IDE -> 自从有了 [**IDE**](11.软件/02.IDE),写代码从此就告别了刀耕火种的蛮荒时代。 +> 自从有了 [**IDE**](01.Java/11.软件/02.IDE),写代码从此就告别了刀耕火种的蛮荒时代。 > -> - [Eclipse](11.软件/02.IDE/02.Eclipse.md) 是久负盛名的开源 Java IDE,我的学生时代一直使用它写 Java。 -> - 曾经抗拒从转 [Intellij Idea](11.软件/02.IDE/01.Intellij.md) ,但后来发现真香,不得不说,确实是目前最优秀的 Java IDE。 -> - 你可以在 [vscode](11.软件/02.IDE/03.VsCode.md) 中写各种语言,只要安装相应插件即可。如果你的项目中使用了很多种编程语言,又懒得在多个 IDE 之间切换,那么就用 vscode 来一网打尽吧。 +> - [Eclipse](01.Java/11.软件/02.IDE/02.Eclipse.md) 是久负盛名的开源 Java IDE,我的学生时代一直使用它写 Java。 +> - 曾经抗拒从转 [Intellij Idea](01.Java/11.软件/02.IDE/01.Intellij.md) ,但后来发现真香,不得不说,确实是目前最优秀的 Java IDE。 +> - 你可以在 [vscode](01.Java/11.软件/02.IDE/03.VsCode.md) 中写各种语言,只要安装相应插件即可。如果你的项目中使用了很多种编程语言,又懒得在多个 IDE 之间切换,那么就用 vscode 来一网打尽吧。 -- [Intellij Idea](11.软件/02.IDE/01.Intellij.md) -- [Eclipse](11.软件/02.IDE/02.Eclipse.md) -- [vscode](11.软件/02.IDE/03.VsCode.md) +- [Intellij Idea](01.Java/11.软件/02.IDE/01.Intellij.md) +- [Eclipse](01.Java/11.软件/02.IDE/02.Eclipse.md) +- [vscode](01.Java/11.软件/02.IDE/03.VsCode.md) #### Java 监控诊断 -> [监控/诊断](11.软件/03.监控诊断) 工具主要用于 Java 应用的运维。通过采集、分析、存储、可视化应用的有效数据,帮助开发者、使用者快速定位问题,找到性能瓶颈。 +> [监控/诊断](01.Java/11.软件/03.监控诊断) 工具主要用于 Java 应用的运维。通过采集、分析、存储、可视化应用的有效数据,帮助开发者、使用者快速定位问题,找到性能瓶颈。 -- [监控工具对比](11.软件/03.监控诊断/01.监控工具对比.md) -- [CAT](11.软件/03.监控诊断/02.CAT.md) -- [Zipkin](11.软件/03.监控诊断/03.Zipkin.md) -- [SkyWalking](11.软件/03.监控诊断/04.Skywalking.md) -- [Arthas](11.软件/03.监控诊断/05.Arthas.md) +- [监控工具对比](01.Java/11.软件/03.监控诊断/01.监控工具对比.md) +- [CAT](01.Java/11.软件/03.监控诊断/02.CAT.md) +- [Zipkin](01.Java/11.软件/03.监控诊断/03.Zipkin.md) +- [SkyWalking](01.Java/11.软件/03.监控诊断/04.Skywalking.md) +- [Arthas](01.Java/11.软件/03.监控诊断/05.Arthas.md) ### Java 工具 #### Java IO -- [JSON 序列化](12.工具/01.IO/01.JSON序列化.md) - [fastjson](https://github.com/alibaba/fastjson)、[Jackson](https://github.com/FasterXML/jackson)、[Gson](https://github.com/google/gson) -- [二进制序列化](12.工具/01.IO/02.二进制序列化.md) - [Protobuf](https://developers.google.com/protocol-buffers)、[Thrift](https://thrift.apache.org/)、[Hessian](http://hessian.caucho.com/)、[Kryo](https://github.com/EsotericSoftware/kryo)、[FST](https://github.com/RuedigerMoeller/fast-serialization) +- [JSON 序列化](01.Java/12.工具/01.IO/01.JSON序列化.md) - [fastjson](https://github.com/alibaba/fastjson)、[Jackson](https://github.com/FasterXML/jackson)、[Gson](https://github.com/google/gson) +- [二进制序列化](01.Java/12.工具/01.IO/02.二进制序列化.md) - [Protobuf](https://developers.google.com/protocol-buffers)、[Thrift](https://thrift.apache.org/)、[Hessian](http://hessian.caucho.com/)、[Kryo](https://github.com/EsotericSoftware/kryo)、[FST](https://github.com/RuedigerMoeller/fast-serialization) #### JavaBean 工具 -- [Lombok](12.工具/02.JavaBean/01.Lombok.md) -- [Dozer](12.工具/02.JavaBean/02.Dozer.md) +- [Lombok](01.Java/12.工具/02.JavaBean/01.Lombok.md) +- [Dozer](01.Java/12.工具/02.JavaBean/02.Dozer.md) #### Java 模板引擎 -- [Freemark](12.工具/03.模板引擎/01.Freemark.md) -- [Velocity](12.工具/03.模板引擎/02.Thymeleaf.md) -- [Thymeleaf](12.工具/03.模板引擎/03.Velocity.md) +- [Freemark](01.Java/12.工具/03.模板引擎/01.Freemark.md) +- [Velocity](01.Java/12.工具/03.模板引擎/02.Thymeleaf.md) +- [Thymeleaf](01.Java/12.工具/03.模板引擎/03.Velocity.md) #### Java 测试工具 -- [Junit](12.工具/04.测试/01.Junit.md) -- [Mockito](12.工具/04.测试/02.Mockito.md) -- [Jmeter](12.工具/04.测试/03.Jmeter.md) -- [JMH](12.工具/04.测试/04.JMH.md) +- [Junit](01.Java/12.工具/04.测试/01.Junit.md) +- [Mockito](01.Java/12.工具/04.测试/02.Mockito.md) +- [Jmeter](01.Java/12.工具/04.测试/03.Jmeter.md) +- [JMH](01.Java/12.工具/04.测试/04.JMH.md) #### 其他 -- [Java 日志](12.工具/99.其他/01.Java日志.md) -- [Java 工具包](12.工具/99.其他/02.Java工具包.md) -- [Reflections](12.工具/99.其他/03.Reflections.md) -- [JavaMail](12.工具/99.其他/04.JavaMail.md) -- [Jsoup](12.工具/99.其他/05.Jsoup.md) -- [Thumbnailator](12.工具/99.其他/06.Thumbnailator.md) -- [Zxing](12.工具/99.其他/07.Zxing.md) +- [Java 日志](01.Java/12.工具/99.其他/01.Java日志.md) +- [Java 工具包](01.Java/12.工具/99.其他/02.Java工具包.md) +- [Reflections](01.Java/12.工具/99.其他/03.Reflections.md) +- [JavaMail](01.Java/12.工具/99.其他/04.JavaMail.md) +- [Jsoup](01.Java/12.工具/99.其他/05.Jsoup.md) +- [Thumbnailator](01.Java/12.工具/99.其他/06.Thumbnailator.md) +- [Zxing](01.Java/12.工具/99.其他/07.Zxing.md) ### Java 框架 #### ORM -- [Mybatis 快速入门](13.框架/11.ORM/01.Mybatis快速入门.md) -- [Mybatis 原理](13.框架/11.ORM/02.Mybatis原理.md) +- [Mybatis 快速入门](01.Java/13.框架/11.ORM/01.Mybatis快速入门.md) +- [Mybatis 原理](01.Java/13.框架/11.ORM/02.Mybatis原理.md) #### Spring @@ -166,48 +166,29 @@ footer: CC-BY-SA-4.0 Licensed | Copyright © 2018-Now Dunwu > > spring-security 功能更丰富,也比 shiro 更复杂。值得一提的是由于 spring-security 是 spring 团队开发,所以集成 spring 和 spring-boot 框架更容易。 -- [Shiro](13.框架/12.安全/01.Shiro.md) -- [SpringSecurity](13.框架/12.安全/02.SpringSecurity.md) +- [Shiro](01.Java/13.框架/12.安全/01.Shiro.md) +- [SpringSecurity](01.Java/13.框架/12.安全/02.SpringSecurity.md) #### IO -- [Shiro](13.框架/13.IO/01.Netty.md) - -#### 微服务 - -- [Dubbo](13.框架/14.微服务/01.Dubbo.md) +- [Shiro](01.Java/13.框架/13.IO/01.Netty.md) ### Java 中间件 -#### MQ - -> 消息队列(Message Queue,简称 MQ)技术是分布式应用间交换信息的一种技术。 -> -> 消息队列主要解决应用耦合,异步消息,流量削锋等问题,实现高性能,高可用,可伸缩和最终一致性架构。是大型分布式系统不可缺少的中间件。 -> -> 如果想深入学习各种消息队列产品,建议先了解一下 [消息队列基本原理](https://dunwu.github.io/blog/pages/1fd240/) ,有助于理解消息队列特性的实现和设计思路。 - -- [消息队列面试](14.中间件/01.MQ/01.消息队列面试.md) -- [消息队列基本原理](14.中间件/01.MQ/02.消息队列基本原理.md) -- [RocketMQ](14.中间件/01.MQ/03.RocketMQ.md) -- [ActiveMQ](14.中间件/01.MQ/04.ActiveMQ.md) - #### 缓存 > 缓存可以说是优化系统性能的第一手段,在各种技术中都会有缓存的应用。 > > 如果想深入学习缓存,建议先了解一下 [缓存基本原理](https://dunwu.github.io/design/distributed/分布式缓存.html),有助于理解缓存的特性、原理,使用缓存常见的问题及解决方案。 -- [缓存面试题](14.中间件/02.缓存/01.缓存面试题.md) -- [Java 缓存中间件](14.中间件/02.缓存/02.Java缓存中间件.md) -- [Memcached 快速入门](14.中间件/02.缓存/03.Memcached.md) -- [Ehcache 快速入门](14.中间件/02.缓存/04.Ehcache.md) -- [Java 进程内缓存](14.中间件/02.缓存/05.Java进程内缓存.md) -- [Http 缓存](14.中间件/02.缓存/06.Http缓存.md) +- [Java 缓存中间件](01.Java/14.中间件/02.缓存/02.Java缓存中间件.md) +- [Ehcache 快速入门](01.Java/14.中间件/02.缓存/04.Ehcache.md) +- [Java 进程内缓存](01.Java/14.中间件/02.缓存/05.Java进程内缓存.md) +- [Http 缓存](01.Java/14.中间件/02.缓存/06.Http缓存.md) #### 流量控制 -- [Hystrix](14.中间件/03.流量控制/01.Hystrix.md) +- [Hystrix](01.Java/14.中间件/03.流量控制/01.Hystrix.md) ### [大数据](https://dunwu.github.io/bigdata-tutorial) @@ -252,19 +233,4 @@ footer: CC-BY-SA-4.0 Licensed | Copyright © 2018-Now Dunwu - [数据库教程](https://dunwu.github.io/db-tutorial/) 📚 - [数据结构和算法教程](https://dunwu.github.io/algorithm-tutorial/) 📚 - [Linux 教程](https://dunwu.github.io/linux-tutorial/) 📚 -- [Nginx 教程](https://github.com/dunwu/nginx-tutorial/) 📚 - - +- [Nginx 教程](https://github.com/dunwu/nginx-tutorial/) 📚 \ No newline at end of file diff --git a/package.json b/package.json index d5c0d8b9..93a7223a 100644 --- a/package.json +++ b/package.json @@ -1,37 +1,41 @@ { "name": "java-tutorial", "version": "1.0.0", + "private": true, "scripts": { "clean": "rimraf docs/.temp", - "start": "vuepress dev docs", - "build": "vuepress build docs", + "start": "node --max_old_space_size=4096 ./node_modules/vuepress/cli.js dev docs", + "build": "node --max_old_space_size=4096 ./node_modules/vuepress/cli.js build docs", "deploy": "bash scripts/deploy.sh", "updateTheme": "yarn remove vuepress-theme-vdoing && rm -rf node_modules && yarn && yarn add vuepress-theme-vdoing -D", "editFm": "node utils/editFrontmatter.js", - "lint": "markdownlint -r markdownlint-rule-emphasis-style -c ./.markdownlint.json **/*.md -i node_modules", - "lint:fix": "markdownlint -f -r markdownlint-rule-emphasis-style -c ./.markdownlint.json **/*.md -i node_modules", + "lint": "markdownlint -r markdownlint-rule-emphasis-style -c docs/.markdownlint.json **/*.md -i node_modules", + "lint:fix": "markdownlint -f -r markdownlint-rule-emphasis-style -c docs/.markdownlint.json **/*.md -i node_modules", "show-help": "vuepress --help", "view-info": "vuepress view-info ./ --temp docs/.temp" }, - "license": "MIT", "devDependencies": { - "dayjs": "^1.9.7", - "inquirer": "^7.1.0", + "dayjs": "^1.11.7", + "inquirer": "^9.1.4", "json2yaml": "^1.1.0", - "vuepress": "1.9.2", + "markdownlint-cli": "^0.33.0", + "markdownlint-rule-emphasis-style": "^1.0.1", + "rimraf": "^4.1.2", + "vue-toasted": "^1.1.25", + "vuepress": "1.9.9", "vuepress-plugin-baidu-tongji": "^1.0.1", + "vuepress-plugin-comment": "^0.7.3", "vuepress-plugin-demo-block": "^0.7.2", + "vuepress-plugin-flowchart": "^1.4.2", "vuepress-plugin-fulltext-search": "^2.2.1", "vuepress-plugin-one-click-copy": "^1.0.2", "vuepress-plugin-thirdparty-search": "^1.0.2", "vuepress-plugin-zooming": "^1.1.7", - "vuepress-plugin-flowchart": "^1.4.2", - "vuepress-theme-vdoing": "^1.10.3", + "vuepress-theme-vdoing": "^1.12.9", "yamljs": "^0.3.0", "markdownlint-cli": "^0.25.0", "markdownlint-rule-emphasis-style": "^1.0.1", "rimraf": "^3.0.1", "vue-toasted": "^1.1.25" - }, - "dependencies": {} + } } diff --git a/prettier.config.js b/prettier.config.js index eb6bb1f5..c9848e74 100644 --- a/prettier.config.js +++ b/prettier.config.js @@ -3,5 +3,8 @@ * @see https://prettier.io/docs/en/configuration.html */ module.exports = { - tabWidth: 2, semi: false, singleQuote: true + tabWidth: 2, + semi: false, + singleQuote: true, + trailingComma: 'none' } diff --git a/scripts/deploy.sh b/scripts/deploy.sh index 6fe5b282..b6ee6ee0 100644 --- a/scripts/deploy.sh +++ b/scripts/deploy.sh @@ -2,7 +2,7 @@ # ------------------------------------------------------------------------------ # gh-pages 部署脚本 -# @author Zhang Peng +# @author Zhang Peng # @since 2020/2/10 # ------------------------------------------------------------------------------ @@ -24,23 +24,24 @@ cd ${ROOT_DIR}/docs/.temp # 如果是发布到自定义域名 # echo 'www.example.com' > CNAME -if [[ ${GITHUB_TOKEN} && ${GITEE_TOKEN} ]]; then +#if [[ ${GITHUB_TOKEN} && ${GITEE_TOKEN} ]]; then +if [[ ${GITHUB_TOKEN} ]]; then msg='自动部署' GITHUB_URL=https://dunwu:${GITHUB_TOKEN}@github.com/dunwu/java-tutorial.git - GITEE_URL=https://turnon:${GITEE_TOKEN}@gitee.com/turnon/java-tutorial.git +# GITEE_URL=https://turnon:${GITEE_TOKEN}@gitee.com/turnon/java-tutorial.git git config --global user.name "dunwu" git config --global user.email "forbreak@163.com" else msg='手动部署' GITHUB_URL=git@github.com:dunwu/java-tutorial.git - GITEE_URL=git@gitee.com:turnon/java-tutorial.git +# GITEE_URL=git@gitee.com:turnon/java-tutorial.git fi git init git add -A git commit -m "${msg}" # 推送到github gh-pages分支 git push -f "${GITHUB_URL}" master:gh-pages -git push -f "${GITEE_URL}" master:gh-pages +#git push -f "${GITEE_URL}" master:gh-pages cd - rm -rf ${ROOT_DIR}/docs/.temp diff --git a/utils/config.yml b/utils/config.yml index 6fac6a22..d387646b 100644 --- a/utils/config.yml +++ b/utils/config.yml @@ -1,14 +1,15 @@ -#批量添加和修改、删除front matter配置文件 +# 批量添加和修改、删除front matter配置文件 -# 需要批量处理的路径,docs文件夹内的文件夹 (数组。映射路径:docs/arr[0]/arr[1] ... ) +# 需要批量处理的路径,docs文件夹内的文件夹 (数组,映射路径:path[0]/path[1]/path[2] ... ) path: - docs # 第一个成员必须是docs # 要删除的字段 (数组) -delete: - # - test +delete: # - tags - # 要添加、修改front matter的数据 (front matter中没有的数据则添加,已有的数据则覆盖) + # 要添加、修改front matter的数据 (front matter中没有的数据则添加,已有的数据则覆盖) data: - article: false \ No newline at end of file + # author: + # name: xugaoyi + # link: https://github.com/xugaoyi diff --git a/utils/editFrontmatter.js b/utils/editFrontmatter.js index 8c223f4e..0998bf3d 100644 --- a/utils/editFrontmatter.js +++ b/utils/editFrontmatter.js @@ -1,41 +1,43 @@ /** * 批量添加和修改front matter ,需要配置 ./config.yml 文件。 */ -const fs = require('fs'); // 文件模块 -const path = require('path'); // 路径模块 -const matter = require('gray-matter'); // front matter解析器 https://github.com/jonschlinkert/gray-matter +const fs = require('fs') // 文件模块 +const path = require('path') // 路径模块 +const matter = require('gray-matter') // front matter解析器 https://github.com/jonschlinkert/gray-matter const jsonToYaml = require('json2yaml') const yamlToJs = require('yamljs') const inquirer = require('inquirer') // 命令行操作 const chalk = require('chalk') // 命令行打印美化 -const readFileList = require('./modules/readFileList'); -const { type, repairDate} = require('./modules/fn'); +const readFileList = require('./modules/readFileList') +const { type, repairDate } = require('./modules/fn') const log = console.log const configPath = path.join(__dirname, 'config.yml') // 配置文件的路径 -main(); +main() /** * 主体函数 */ async function main() { + const promptList = [ + { + type: 'confirm', + message: chalk.yellow('批量操作frontmatter有修改数据的风险,确定要继续吗?'), + name: 'edit' + } + ] + let edit = true - const promptList = [{ - type: "confirm", - message: chalk.yellow('批量操作frontmatter有修改数据的风险,确定要继续吗?'), - name: "edit", - }]; - let edit = true; - - await inquirer.prompt(promptList).then(answers => { + await inquirer.prompt(promptList).then((answers) => { edit = answers.edit }) - if(!edit) { // 退出操作 + if (!edit) { + // 退出操作 return } - + const config = yamlToJs.load(configPath) // 解析配置文件的数据转为js对象 if (type(config.path) !== 'array') { @@ -48,27 +50,26 @@ async function main() { return } - const filePath = path.join(__dirname, '..', ...config.path); // 要批量修改的文件路径 - const files = readFileList(filePath); // 读取所有md文件数据 + const filePath = path.join(__dirname, '..', ...config.path) // 要批量修改的文件路径 + const files = readFileList(filePath) // 读取所有md文件数据 - files.forEach(file => { - let dataStr = fs.readFileSync(file.filePath, 'utf8');// 读取每个md文件的内容 + files.forEach((file) => { + let dataStr = fs.readFileSync(file.filePath, 'utf8') // 读取每个md文件的内容 const fileMatterObj = matter(dataStr) // 解析md文件的front Matter。 fileMatterObj => {content:'剔除frontmatter后的文件内容字符串', data:{}, ...} - let matterData = fileMatterObj.data; // 得到md文件的front Matter - + let matterData = fileMatterObj.data // 得到md文件的front Matter + let mark = false // 删除操作 if (config.delete) { - if( type(config.delete) !== 'array' ) { + if (type(config.delete) !== 'array') { log(chalk.yellow('未能完成删除操作,delete字段的值应该是一个数组!')) } else { - config.delete.forEach(item => { + config.delete.forEach((item) => { if (matterData[item]) { delete matterData[item] mark = true } }) - } } @@ -77,16 +78,21 @@ async function main() { Object.assign(matterData, config.data) // 将配置数据合并到front Matter对象 mark = true } - + // 有操作时才继续 if (mark) { - if(matterData.date && type(matterData.date) === 'date') { + if (matterData.date && type(matterData.date) === 'date') { matterData.date = repairDate(matterData.date) // 修复时间格式 } - const newData = jsonToYaml.stringify(matterData).replace(/\n\s{2}/g,"\n").replace(/"/g,"") + '---\r\n' + fileMatterObj.content; - fs.writeFileSync(file.filePath, newData); // 写入 + const newData = + jsonToYaml + .stringify(matterData) + .replace(/\n\s{2}/g, '\n') + .replace(/"/g, '') + + '---\r\n' + + fileMatterObj.content + fs.writeFileSync(file.filePath, newData) // 写入 log(chalk.green(`update frontmatter:${file.filePath} `)) } - }) } diff --git a/utils/modules/fn.js b/utils/modules/fn.js index 48cbbd17..a4654f18 100644 --- a/utils/modules/fn.js +++ b/utils/modules/fn.js @@ -1,21 +1,25 @@ // 类型判断 -exports.type = function (o){ +exports.type = function (o) { var s = Object.prototype.toString.call(o) return s.match(/\[object (.*?)\]/)[1].toLowerCase() } - // 修复date时区格式的问题 - exports.repairDate = function (date) { - date = new Date(date); - return `${date.getUTCFullYear()}-${zero(date.getUTCMonth()+1)}-${zero(date.getUTCDate())} ${zero(date.getUTCHours())}:${zero(date.getUTCMinutes())}:${zero(date.getUTCSeconds())}`; +// 修复date时区格式的问题 +exports.repairDate = function (date) { + date = new Date(date) + return `${date.getUTCFullYear()}-${zero(date.getUTCMonth() + 1)}-${zero(date.getUTCDate())} ${zero( + date.getUTCHours() + )}:${zero(date.getUTCMinutes())}:${zero(date.getUTCSeconds())}` } // 日期的格式 exports.dateFormat = function (date) { - return `${date.getFullYear()}-${zero(date.getMonth()+1)}-${zero(date.getDate())} ${zero(date.getHours())}:${zero(date.getMinutes())}:${zero(date.getSeconds())}` + return `${date.getFullYear()}-${zero(date.getMonth() + 1)}-${zero(date.getDate())} ${zero(date.getHours())}:${zero( + date.getMinutes() + )}:${zero(date.getSeconds())}` } // 小于10补0 -function zero(d){ - return d.toString().padStart(2,'0') +function zero(d) { + return d.toString().padStart(2, '0') } \ No newline at end of file diff --git a/utils/modules/readFileList.js b/utils/modules/readFileList.js index 8eb97c62..74a4eb6b 100644 --- a/utils/modules/readFileList.js +++ b/utils/modules/readFileList.js @@ -1,43 +1,49 @@ /** * 读取所有md文件数据 */ -const fs = require('fs'); // 文件模块 -const path = require('path'); // 路径模块 -const docsRoot = path.join(__dirname, '..', '..', 'docs'); // docs文件路径 +const fs = require('fs') // 文件模块 +const path = require('path') // 路径模块 +const docsRoot = path.join(__dirname, '..', '..', 'docs') // docs文件路径 function readFileList(dir = docsRoot, filesList = []) { - const files = fs.readdirSync(dir); - files.forEach( (item, index) => { - let filePath = path.join(dir, item); - const stat = fs.statSync(filePath); - if (stat.isDirectory() && item !== '.vuepress') { - readFileList(path.join(dir, item), filesList); //递归读取文件 - } else { - if(path.basename(dir) !== 'docs'){ // 过滤docs目录级下的文件 + const files = fs.readdirSync(dir) + files.forEach((item, index) => { + let filePath = path.join(dir, item) + const stat = fs.statSync(filePath) + if (stat.isDirectory() && item !== '.vuepress') { + readFileList(path.join(dir, item), filesList) //递归读取文件 + } else { + if (path.basename(dir) !== 'docs') { + // 过滤docs目录级下的文件 - const fileNameArr = path.basename(filePath).split('.') - let name = null, type = null; - if (fileNameArr.length === 2) { // 没有序号的文件 - name = fileNameArr[0] - type = fileNameArr[1] - } else if (fileNameArr.length === 3) { // 有序号的文件 - name = fileNameArr[1] - type = fileNameArr[2] - } else { // 超过两个‘.’的 - log(chalk.yellow(`warning: 该文件 "${filePath}" 没有按照约定命名,将忽略生成相应数据。`)) - return - } - if(type === 'md'){ // 过滤非md文件 - filesList.push({ - name, - filePath - }); - } + const filename = path.basename(filePath) + const fileNameArr = filename.split('.') + const firstDotIndex = filename.indexOf('.') + const lastDotIndex = filename.lastIndexOf('.') + let name = null, + type = null + if (fileNameArr.length === 2) { + // 没有序号的文件 + name = fileNameArr[0] + type = fileNameArr[1] + } else if (fileNameArr.length >= 3) { + // 有序号的文件(或文件名中间有'.') + name = filename.substring(firstDotIndex + 1, lastDotIndex) + type = filename.substring(lastDotIndex + 1) } - } - }); - return filesList; + + if (type === 'md') { + // 过滤非md文件 + filesList.push({ + name, + filePath + }) + } + } + } + }) + return filesList } -module.exports = readFileList; \ No newline at end of file +module.exports = readFileList