diff --git a/.DS_Store b/.DS_Store deleted file mode 100644 index 762001e..0000000 Binary files a/.DS_Store and /dev/null differ diff --git a/.gitignore b/.gitignore index c3e9e42..3540bb2 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,10 @@ Spring-Boot/learnsb.iml Spring/learnspring.iml Spring-AOP/learnaop.iml Spring-Netty/Spring-Netty.iml +Spring-Netty/learnnetty.iml +Spring-Security/SpringSecurityDemo.iml +Spring-Security/springsecurity.iml +rocketmqdemo/rocketmqdemo.iml # target JdkLearn/target @@ -14,6 +18,8 @@ Spring/target Spring-AOP/target Spring-Boot/target Spring-Netty/target +Spring-Security/target +rocketmqdemo/target # .DO_Store .DS_Store diff --git a/JdkLearn/pom.xml b/JdkLearn/pom.xml index d196fc9..9fb74db 100644 --- a/JdkLearn/pom.xml +++ b/JdkLearn/pom.xml @@ -20,6 +20,18 @@ + + + com.github.ben-manes.caffeine + caffeine + + + + + io.netty + netty-all + + org.springframework.boot diff --git a/JdkLearn/src/main/java/com/learnjava/collection/LRUCache.java b/JdkLearn/src/main/java/com/learnjava/collection/LRUCache.java new file mode 100644 index 0000000..aca54da --- /dev/null +++ b/JdkLearn/src/main/java/com/learnjava/collection/LRUCache.java @@ -0,0 +1,71 @@ +package com.learnjava.collection; + +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.Spliterator; +import java.util.function.Consumer; + +/** + * @Description + * @Author luohaiyang + * @Date 2022/4/6 + */ +public class LRUCache implements Iterable { + + private int MAX = 3; + private LinkedHashMap cache = new LinkedHashMap<>(); + + public void cache(K key, V value) { + if (cache.containsKey(key)) { + cache.remove(key); + } else if (cache.size() >= MAX) { + Iterator iterator = cache.keySet().iterator(); + K first = iterator.next(); + cache.remove(first); + } + cache.put(key, value); + } + + public V getValue(K k) { + return cache.get(k); + } + + @Override + public void forEach(Consumer action) { + Iterable.super.forEach(action); + } + + @Override + public Spliterator spliterator() { + return Iterable.super.spliterator(); + } + + @Override + public Iterator iterator() { + Iterator iterator = cache.keySet().iterator(); + return new Iterator() { + + @Override + public boolean hasNext() { + return iterator.hasNext(); + } + + @Override + public K next() { + return iterator.next(); + } + }; + } + + public static void main(String[] args) { + LRUCache cache = new LRUCache<>(); + cache.cache("1", "1A"); + cache.cache("2", "2A"); + cache.cache("3", "3A"); + cache.cache("1", "1A"); + + for (String next : cache) { + System.out.println(cache.getValue(next)); + } + } +} diff --git a/JdkLearn/src/main/java/com/learnjava/concurent/ReentrantLockDemo.java b/JdkLearn/src/main/java/com/learnjava/concurent/ReentrantLockDemo.java new file mode 100644 index 0000000..0d04fb8 --- /dev/null +++ b/JdkLearn/src/main/java/com/learnjava/concurent/ReentrantLockDemo.java @@ -0,0 +1,9 @@ +package com.learnjava.concurent; + +/** + * @Description + * @Author luohaiyang + * @Date 2022/4/7 + */ +public class ReentrantLockDemo { +} diff --git a/JdkLearn/src/main/java/com/learnjava/concurent/SnowIdDemo.java b/JdkLearn/src/main/java/com/learnjava/concurent/SnowIdDemo.java new file mode 100644 index 0000000..aa00a30 --- /dev/null +++ b/JdkLearn/src/main/java/com/learnjava/concurent/SnowIdDemo.java @@ -0,0 +1,12 @@ +package com.learnjava.concurent; + +/** + * @author lhy + * @date 2021/7/27 + */ +public class SnowIdDemo { + public static void main(String[] args) { + // 通过雪花秀算法获取分布式id + System.out.println(SnowIdUtils.uniqueLongHex()); + } +} diff --git a/JdkLearn/src/main/java/com/learnjava/concurent/SnowIdUtils.java b/JdkLearn/src/main/java/com/learnjava/concurent/SnowIdUtils.java new file mode 100644 index 0000000..384d980 --- /dev/null +++ b/JdkLearn/src/main/java/com/learnjava/concurent/SnowIdUtils.java @@ -0,0 +1,114 @@ +package com.learnjava.concurent; + +/** + * @author lhy + * @date 2021/7/27 + */ +public class SnowIdUtils { + /** + * 私有的 静态内部类 + */ + private static class SnowFlake { + + /** + * 内部类对象(单例模式) + */ + private static final SnowIdUtils.SnowFlake SNOW_FLAKE = new SnowIdUtils.SnowFlake(); + /** + * 起始的时间戳 + */ + private final long START_TIMESTAMP = 1609464335121L; + /** + * 序列号占用位数 + */ + private final long SEQUENCE_BIT = 12; + /** + * 机器标识占用位数 + */ + private final long MACHINE_BIT = 10; + /** + * 时间戳位移位数 + */ + private final long TIMESTAMP_LEFT = SEQUENCE_BIT + MACHINE_BIT; + /** + * 最大序列号 (4095) + */ + private final long MAX_SEQUENCE = ~(-1L << SEQUENCE_BIT); + /** + * 最大机器编号 (1023) + */ + private final long MAX_MACHINE_ID = ~(-1L << MACHINE_BIT); + /** + * 生成id机器标识部分 + */ + private long machineIdPart; + /** + * 序列号 + */ + private long sequence = 0L; + /** + * 上一次时间戳 + */ + private long lastStamp = -1L; + + /** + * 构造函数初始化机器编码 + */ + private SnowFlake() { +// String ip = instance.getDockerIp().replace(".", ""); + // 模拟获取机器节点ip + String ip = "127.0.0.1"; + long localIp = Long.parseLong(ip.replace(".", "")); + machineIdPart = (localIp & MAX_MACHINE_ID) << SEQUENCE_BIT; + } + /** + * 获取雪花ID + */ + public synchronized long nextId() { + long currentStamp = timeGen(); + while (currentStamp < lastStamp) { + throw new RuntimeException(String.format("时钟已经回拨. Refusing to generate id for %d milliseconds", lastStamp - currentStamp)); + } + if (currentStamp == lastStamp) { + sequence = (sequence + 1) & MAX_SEQUENCE; + if (sequence == 0) { + currentStamp = getNextMill(); + } + } else { + sequence = 0L; + } + lastStamp = currentStamp; + return (currentStamp - START_TIMESTAMP) << TIMESTAMP_LEFT | machineIdPart | sequence; + } + /** + * 阻塞到下一个毫秒,直到获得新的时间戳 + */ + private long getNextMill() { + long mill = timeGen(); + // + while (mill <= lastStamp) { + mill = timeGen(); + } + return mill; + } + /** + * 返回以毫秒为单位的当前时间 + */ + protected long timeGen() { + return System.currentTimeMillis(); + } + } + + /** + * 获取long类型雪花ID + */ + public static long uniqueLong() { + return SnowIdUtils.SnowFlake.SNOW_FLAKE.nextId(); + } + /** + * 获取String类型雪花ID + */ + public static String uniqueLongHex() { + return String.format("%016X", uniqueLong()); + } +} diff --git a/JdkLearn/src/main/java/com/learnjava/concurent/SynchronizeDemo.java b/JdkLearn/src/main/java/com/learnjava/concurent/SynchronizeDemo.java new file mode 100644 index 0000000..4a89467 --- /dev/null +++ b/JdkLearn/src/main/java/com/learnjava/concurent/SynchronizeDemo.java @@ -0,0 +1,9 @@ +package com.learnjava.concurent; + +/** + * @Description + * @Author luohaiyang + * @Date 2022/4/7 + */ +public class SynchronizeDemo { +} diff --git a/JdkLearn/src/main/java/com/learnjava/concurent/ThreadPoolExecutorDemo.java b/JdkLearn/src/main/java/com/learnjava/concurent/ThreadPoolExecutorDemo.java new file mode 100644 index 0000000..5d5b5fc --- /dev/null +++ b/JdkLearn/src/main/java/com/learnjava/concurent/ThreadPoolExecutorDemo.java @@ -0,0 +1,48 @@ +package com.learnjava.concurent; + +import java.util.concurrent.atomic.AtomicInteger; + +/** + * @Description + * @Author luohaiyang + * @Date 2022/4/24 + */ +public class ThreadPoolExecutorDemo { + + public static void main(String[] args) { + testThreadPoolExecutorBinaryCalc(); + } + + + /** + * 验证ThreadPoolExecutor中的二进制位运算操作 + */ + private static void testThreadPoolExecutorBinaryCalc() { +// System.out.println(ctl.get()); +// System.out.println(Integer.toBinaryString(ctlOf(RUNNING, 0))); +// System.out.println(Integer.toBinaryString(RUNNING)); + // 修改线程状态-STOP + System.out.println(Integer.toBinaryString(~runStateOf(ctlOf(STOP, 10)))); + // 修改线程状态-TERMINATED +// System.out.println(runStateOf(3)); +// System.out.println(Integer.toBinaryString(~CAPACITY)); + } + + private static final int COUNT_BITS = Integer.SIZE - 3; + + private static final int CAPACITY = (1 << COUNT_BITS) - 1; + + private static final int RUNNING = -1 << COUNT_BITS; + private static final int SHUTDOWN = 0 << COUNT_BITS; + private static final int STOP = 1 << COUNT_BITS; + private static final int TIDYING = 2 << COUNT_BITS; + private static final int TERMINATED = 3 << COUNT_BITS; + + private static AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0)); + + private static int runStateOf(int c) { return c & ~CAPACITY; } + + private static int workerCountOf(int c) { return c & CAPACITY; } + + private static int ctlOf(int rs, int wc) { return rs | wc; } +} diff --git a/JdkLearn/src/main/java/com/learnjava/io/bio/BIO.java b/JdkLearn/src/main/java/com/learnjava/io/bio/BIO.java new file mode 100644 index 0000000..4226be5 --- /dev/null +++ b/JdkLearn/src/main/java/com/learnjava/io/bio/BIO.java @@ -0,0 +1,47 @@ +package com.learnjava.io.bio; + +import java.io.InputStream; +import java.net.ServerSocket; +import java.net.Socket; + +/** + * @author lhy + * + * 在windows服务器下,可以使用telnet来合serversocket建立连接 + */ +public class BIO { + public static void main(String[] args) throws Exception { + ServerSocket serverSocket = new ServerSocket(666); + System.out.println("Server started..."); + while (true) { + System.out.println("socket accepting..."); + Socket socket = serverSocket.accept(); + new Thread(new Runnable() { + @Override + public void run() { + try { + byte[] bytes = new byte[1024]; + InputStream inputStream = socket.getInputStream(); + while (true) { + System.out.println("reading..."); + int read = inputStream.read(bytes); + if (read != -1) { + System.out.println(new String(bytes, 0, read)); + } else { + break; + } + } + } catch (Exception e) { + e.printStackTrace(); + } finally { + try { + socket.close(); + } catch (Exception e) { + e.printStackTrace(); + } + } + } + }).start(); + } + } +} diff --git a/JdkLearn/src/main/java/com/learnjava/nettysource/Client.java b/JdkLearn/src/main/java/com/learnjava/io/netty/bio/Client.java similarity index 97% rename from JdkLearn/src/main/java/com/learnjava/nettysource/Client.java rename to JdkLearn/src/main/java/com/learnjava/io/netty/bio/Client.java index 5514bee..f84b58e 100644 --- a/JdkLearn/src/main/java/com/learnjava/nettysource/Client.java +++ b/JdkLearn/src/main/java/com/learnjava/io/netty/bio/Client.java @@ -1,4 +1,4 @@ -package com.learnjava.nettysource; +package com.learnjava.io.netty.bio; import java.io.IOException; import java.net.Socket; diff --git a/JdkLearn/src/main/java/com/learnjava/nettysource/ClientHandler.java b/JdkLearn/src/main/java/com/learnjava/io/netty/bio/ClientHandler.java similarity index 96% rename from JdkLearn/src/main/java/com/learnjava/nettysource/ClientHandler.java rename to JdkLearn/src/main/java/com/learnjava/io/netty/bio/ClientHandler.java index d0ddba9..d2d6b88 100644 --- a/JdkLearn/src/main/java/com/learnjava/nettysource/ClientHandler.java +++ b/JdkLearn/src/main/java/com/learnjava/io/netty/bio/ClientHandler.java @@ -1,4 +1,4 @@ -package com.learnjava.nettysource; +package com.learnjava.io.netty.bio; import java.io.IOException; import java.io.InputStream; diff --git a/JdkLearn/src/main/java/com/learnjava/nettysource/Server.java b/JdkLearn/src/main/java/com/learnjava/io/netty/bio/Server.java similarity index 94% rename from JdkLearn/src/main/java/com/learnjava/nettysource/Server.java rename to JdkLearn/src/main/java/com/learnjava/io/netty/bio/Server.java index 834f41b..af55558 100644 --- a/JdkLearn/src/main/java/com/learnjava/nettysource/Server.java +++ b/JdkLearn/src/main/java/com/learnjava/io/netty/bio/Server.java @@ -1,4 +1,4 @@ -package com.learnjava.nettysource; +package com.learnjava.io.netty.bio; import java.io.IOException; import java.net.ServerSocket; @@ -30,7 +30,7 @@ public void run() { } public void doStart() { - while (true) { + for (;;) { try { Socket client = serverSocket.accept(); new ClientHandler(client).start(); diff --git a/JdkLearn/src/main/java/com/learnjava/nettysource/ServerBoot.java b/JdkLearn/src/main/java/com/learnjava/io/netty/bio/ServerBoot.java similarity index 76% rename from JdkLearn/src/main/java/com/learnjava/nettysource/ServerBoot.java rename to JdkLearn/src/main/java/com/learnjava/io/netty/bio/ServerBoot.java index 9348e09..bbb2762 100644 --- a/JdkLearn/src/main/java/com/learnjava/nettysource/ServerBoot.java +++ b/JdkLearn/src/main/java/com/learnjava/io/netty/bio/ServerBoot.java @@ -1,4 +1,4 @@ -package com.learnjava.nettysource; +package com.learnjava.io.netty.bio; /** @@ -9,6 +9,6 @@ public class ServerBoot { public static void main(String[] args) { Server server = new Server(PORT); - + server.start(); } } diff --git a/JdkLearn/src/main/java/com/learnjava/io/netty/demo01/Server.java b/JdkLearn/src/main/java/com/learnjava/io/netty/demo01/Server.java new file mode 100644 index 0000000..7c1d994 --- /dev/null +++ b/JdkLearn/src/main/java/com/learnjava/io/netty/demo01/Server.java @@ -0,0 +1,53 @@ +package com.learnjava.io.netty.demo01; + +import io.netty.bootstrap.ServerBootstrap; +import io.netty.channel.*; +import io.netty.channel.ChannelOption; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.SocketChannel; +import io.netty.channel.socket.nio.NioServerSocketChannel; +import io.netty.util.AttributeKey; + +/** + * @author lhy + * @date 2021/5/21 + */ +public class Server { + + public static void main(String[] args) throws Exception { + + /** + * EventLoopGroup: + * NioEventLoopGruop + * NioEventLoop + */ + + EventLoopGroup bossGroup = new NioEventLoopGroup(1); + EventLoopGroup workerGroup = new NioEventLoopGroup(); + + try { + ServerBootstrap bootstrap = new ServerBootstrap(); + bootstrap.group(bossGroup, workerGroup) + .channel(NioServerSocketChannel.class) + .childOption(ChannelOption.TCP_NODELAY, true) + .childAttr(AttributeKey.newInstance("childAttr"), "childAttrValue") + .handler(new ServerHandler()) + .childHandler(new ChannelInitializer() { + @Override + public void initChannel(SocketChannel ch) { +// ch.pipeline().addLast(new AuthHandler()); + //.. + + } + }); + + ChannelFuture future = bootstrap.bind(8888).sync(); + + future.channel().closeFuture().sync(); + } finally { + bossGroup.shutdownGracefully(); + workerGroup.shutdownGracefully(); + } + } +} diff --git a/JdkLearn/src/main/java/com/learnjava/io/netty/demo01/ServerHandler.java b/JdkLearn/src/main/java/com/learnjava/io/netty/demo01/ServerHandler.java new file mode 100644 index 0000000..7123116 --- /dev/null +++ b/JdkLearn/src/main/java/com/learnjava/io/netty/demo01/ServerHandler.java @@ -0,0 +1,74 @@ +package com.learnjava.io.netty.demo01; + +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; + +/** + * @author lhy + * @date 2021/5/22 + */ +public class ServerHandler extends ChannelInboundHandlerAdapter { + public ServerHandler() { + super(); + } + + @Override + public void channelRegistered(ChannelHandlerContext ctx) throws Exception { + super.channelRegistered(ctx); + } + + @Override + public void channelUnregistered(ChannelHandlerContext ctx) throws Exception { + super.channelUnregistered(ctx); + } + + @Override + public void channelActive(ChannelHandlerContext ctx) throws Exception { + super.channelActive(ctx); + } + + @Override + public void channelInactive(ChannelHandlerContext ctx) throws Exception { + super.channelInactive(ctx); + } + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + super.channelRead(ctx, msg); + } + + @Override + public void channelReadComplete(ChannelHandlerContext ctx) throws Exception { + super.channelReadComplete(ctx); + } + + @Override + public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { + super.userEventTriggered(ctx, evt); + } + + @Override + public void channelWritabilityChanged(ChannelHandlerContext ctx) throws Exception { + super.channelWritabilityChanged(ctx); + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { + super.exceptionCaught(ctx, cause); + } + + @Override + public boolean isSharable() { + return super.isSharable(); + } + + @Override + public void handlerAdded(ChannelHandlerContext ctx) throws Exception { + super.handlerAdded(ctx); + } + + @Override + public void handlerRemoved(ChannelHandlerContext ctx) throws Exception { + super.handlerRemoved(ctx); + } +} diff --git a/JdkLearn/src/main/java/com/learnjava/io/nio/MappedByteBufferTest.java b/JdkLearn/src/main/java/com/learnjava/io/nio/MappedByteBufferTest.java index 4097936..75cf208 100644 --- a/JdkLearn/src/main/java/com/learnjava/io/nio/MappedByteBufferTest.java +++ b/JdkLearn/src/main/java/com/learnjava/io/nio/MappedByteBufferTest.java @@ -1,5 +1,8 @@ package com.learnjava.io.nio; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; import java.io.RandomAccessFile; import java.nio.MappedByteBuffer; import java.nio.channels.FileChannel; @@ -17,13 +20,55 @@ public static void main(String[] args) throws Exception { MappedByteBuffer mappedByteBuffer = channel.map(FileChannel.MapMode.READ_WRITE, 0, 5); - mappedByteBuffer.put(0, (byte)'H'); - mappedByteBuffer.put(3, (byte)'9'); + mappedByteBuffer.put(0, (byte) 'H'); + mappedByteBuffer.put(3, (byte) '9'); // IndexOutOfBoundsException - mappedByteBuffer.put(5, (byte)'Y'); + mappedByteBuffer.put(5, (byte) 'Y'); randomAccessFile.close(); System.out.println("change success"); } + + /** + * + * @param from + * @param to + * @throws IOException + */ + public static void mmap4zeroCopy(String from, String to) throws IOException { + FileChannel source = null; + FileChannel destination = null; + try { + source = new RandomAccessFile(from, "r").getChannel(); + destination = new RandomAccessFile(to, "rw").getChannel(); + MappedByteBuffer inMappedBuf = + source.map(FileChannel.MapMode.READ_ONLY, 0, source.size()); + destination.write(inMappedBuf); + } finally { + if (source != null) { + source.close(); + } + if (destination != null) { + destination.close(); + } + } + } + + public static void sendfile4zeroCopy(String from, String to) throws IOException{ + FileChannel source = null; + FileChannel destination = null; + try { + source = new FileInputStream(from).getChannel(); + destination = new FileOutputStream(to).getChannel(); + source.transferTo(0, source.size(), destination); + } finally { + if (source != null) { + source.close(); + } + if (destination != null) { + destination.close(); + } + } + } } diff --git a/JdkLearn/src/main/java/com/learnjava/io/nio/demo01/NioClinet.java b/JdkLearn/src/main/java/com/learnjava/io/nio/demo01/NioClinet.java new file mode 100644 index 0000000..aab903b --- /dev/null +++ b/JdkLearn/src/main/java/com/learnjava/io/nio/demo01/NioClinet.java @@ -0,0 +1,40 @@ +package com.learnjava.io.nio.demo01; + +import java.net.InetSocketAddress; +import java.nio.ByteBuffer; +import java.nio.channels.SocketChannel; +import java.util.Date; +import java.util.Scanner; + +/** + * @author lhy + * @date 2021/5/22 + */ +public class NioClinet { + + public static void main(String[] args) throws Exception { + + // 获取通道 + SocketChannel channel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 9999)); + // 切换至非阻塞模式 + channel.configureBlocking(false); + // 分配缓冲区大小 + ByteBuffer buffer = ByteBuffer.allocate(1024); + + Scanner scan = new Scanner(System.in); + + while (scan.hasNext()) { + String next = scan.next(); + // 向缓冲区里写入数据 + buffer.put( (new Date().toString() + "\n" + next).getBytes()); + buffer.flip(); + + // 向通道里写入带有数据的缓冲区对象, 表示向服务器发送数据 + channel.write( buffer); + buffer.clear(); + } + + // 关闭通道 + channel.close(); + } +} diff --git a/JdkLearn/src/main/java/com/learnjava/io/nio/demo01/NioServer.java b/JdkLearn/src/main/java/com/learnjava/io/nio/demo01/NioServer.java new file mode 100644 index 0000000..7ea41ec --- /dev/null +++ b/JdkLearn/src/main/java/com/learnjava/io/nio/demo01/NioServer.java @@ -0,0 +1,57 @@ +package com.learnjava.io.nio.demo01; + +import java.nio.ByteBuffer; +import java.nio.channels.SelectionKey; +import java.nio.channels.Selector; +import java.nio.channels.ServerSocketChannel; +import java.nio.channels.SocketChannel; +import java.util.Iterator; + +/** + * @author lhy + * @date 2021/5/22 + */ +public class NioServer { + public static void main(String[] args) throws Exception { + // 获取服务端通道 + ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); + // 切换为非阻塞模式 + serverSocketChannel.configureBlocking(false); + // 绑定链接 + Selector selector = Selector.open(); + // 将通道注册在selector上,并绑定为读事件 + serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); + // 选择器轮训,阻塞 + while (selector.select() > 0) { + Iterator it = selector.selectedKeys().iterator(); + + // 判断是否有事件进来 + while (it.hasNext()) { + // 获取就绪的事件 + SelectionKey selectionKey = it.next(); + + // 读事件 + if (selectionKey.isAcceptable()) { + // 就绪的客户端连接事件 + SocketChannel acceptChannel = serverSocketChannel.accept(); + acceptChannel.configureBlocking(false); + acceptChannel.register(selector, SelectionKey.OP_READ); + } else if (selectionKey.isReadable()) { + // 读就绪事件 + SocketChannel readAcceptChannel = serverSocketChannel.accept(); + ByteBuffer allocateBuffer = ByteBuffer.allocate(1024); + + int len = 0; + while ((len = readAcceptChannel.read(allocateBuffer)) > 0) { + allocateBuffer.flip(); + System.out.println(new String(allocateBuffer.array(), 0, len)); + allocateBuffer.clear(); + } + } + } + + // 取消选择键selectionKey + it.remove(); + } + } +} diff --git a/JdkLearn/src/main/java/com/learnjava/lambda/LambdaComparatorDemo.java b/JdkLearn/src/main/java/com/learnjava/lambda/LambdaComparatorDemo.java new file mode 100644 index 0000000..1db2858 --- /dev/null +++ b/JdkLearn/src/main/java/com/learnjava/lambda/LambdaComparatorDemo.java @@ -0,0 +1,165 @@ +package com.learnjava.lambda; + +import lombok.AllArgsConstructor; +import lombok.Data; +import sun.java2d.pipe.SpanShapeRenderer; + +import java.text.SimpleDateFormat; +import java.util.Arrays; +import java.util.Comparator; +import java.util.Date; +import java.util.List; +import java.util.function.Predicate; +import java.util.stream.Stream; + +public class LambdaComparatorDemo { + + public static String[] arrays = {"Milan", "london", "San Francisco", "Tokyo", "New Delhi"}; + + public static List employees; + + static { + Employee e1 = new Employee(1,23,"M","Rick","Beethovan", "2021-04-01"); + Employee e2 = new Employee(2,13,"F","Martina","Hengis", "2021-04-02"); + Employee e3 = new Employee(3,43,"M","Ricky","Martin","2021-04-09" ); + Employee e4 = new Employee(4,26,"M","Jon","Lowman", "2021-04-10"); + Employee e5 = new Employee(5,19,"F","Cristine","Maria", "2021-04-01"); + Employee e6 = new Employee(6,15,"M","David","Feezor", "2021-04-06"); + Employee e7 = new Employee(7,68,"F","Melissa","Roy", "2021-04-06"); + Employee e8 = new Employee(8,79,"M","Alex","Gussin", "2021-04-08"); + Employee e9 = new Employee(9,15,"F","Neetu","Singh", "2021-04-09"); + Employee e10 = new Employee(10,45,"M","Naveen","Jain", "2021-04-10"); + employees = Arrays.asList(e1, e2, e3, e4, e5, e6, e7, e8, e9, e10); + } + + public static void main(String[] args) { +// test01(); +// test02(); + test03(); + } + + /** + * List字符串排序 + */ + public static void test01() { + List cities = Arrays.asList(arrays); + + System.out.println(cities); + + // CASE_INSENSITIVE_ORDER 是一个排序器Comparator接口,意思是不区分大小写进行排序 + cities.sort(String.CASE_INSENSITIVE_ORDER); + System.out.println(cities); + + // 自然排序 + cities.sort(Comparator.naturalOrder()); + System.out.println(cities); + + // 可以将排序器放在Stream管道流中 + Stream.of(arrays) + .sorted(Comparator.naturalOrder()) + .forEach(System.out::println); + } + + /** + * 对整数数组进行排序 + */ + public static void test02() { + List numbers = Arrays.asList(6, 2, 4, 3, 1, 9); + System.out.println(numbers); + + // 自然排序(升序) + numbers.sort(Comparator.naturalOrder()); + System.out.println(numbers); + + // 降序 + numbers.sort(Comparator.reverseOrder()); + System.out.println(numbers); + } + + /** + * 对对象进行排序 + */ + public static void test03() { + // 根据employee的年龄进行自然排序 + employees.sort(Comparator.comparing(Employee::getAge)); + employees.forEach(System.out::println); + + System.out.println(); + + // 根据employee的年龄进行降序排序 + employees.sort(Comparator.comparing(Employee::getAge).reversed()); + employees.forEach(System.out::println); + + System.out.println(); + + // 先对性别排序,然后对年龄进行排序 + employees.sort( + Comparator.comparing(Employee::getGender) + .thenComparing(Employee::getAge) + // 性别,年龄都进行倒序排序 + .reversed() + ); + employees.forEach(System.out::println); + + // 自定义排序器 + employees.sort((em1, em2) -> { + if (em1.getAge().equals(em2.getAge())) { + return 0; + } + return em1.getAge() - em2.getAge() > 0 ? -1 : 1; + }); + employees.forEach(System.out::println); + } + + + + + + + + + + + + + + + + + + + + + + + @Data + @AllArgsConstructor + public static class Employee { + + private Integer id; + // 年龄 + private Integer age; + // 性别 + private String gender; + private String firstName; + private String lastName; + private String date; + + @Override + public String toString() { + return "Employee{" + + "id=" + id + + ", age=" + age + + ", gender='" + gender + '\'' + + ", firstName='" + firstName + '\'' + + ", lastName='" + lastName + '\'' + + ", date='" + date + '\'' + + '}'; + } + + // 年龄大于70的谓语 + static Predicate ageGreaterThan70 = e -> e.getAge() > 70; + + static Predicate ageLessThan18 = e -> e.getAge() < 18; + } +} diff --git a/JdkLearn/src/main/java/com/learnjava/lambda/LambdaDemo01.java b/JdkLearn/src/main/java/com/learnjava/lambda/LambdaDemo01.java new file mode 100644 index 0000000..ae7e1d4 --- /dev/null +++ b/JdkLearn/src/main/java/com/learnjava/lambda/LambdaDemo01.java @@ -0,0 +1,37 @@ +package com.learnjava.lambda; + +/** + * @author bruis + * lambda表达式 + */ +public class LambdaDemo01 { + + /** + * 打印内部类 + */ + interface Printer { + void print(String content); +// void print(String content, String operator); + } + + public static void printSomething(String content, Printer printer) { + printer.print(content); + } + + public static void main(String[] args) { +// Printer printer = (String content) -> { +// System.out.println(content); +// }; + +// 去掉参数类型,只有一个参数时可以去掉括号 +// Printer printer = (content) -> { +// System.out.println(content); +// }; + +// 只有一个参数提 +// Printer printer = val -> System.out.println(val); + + Printer printer = System.out::println; + printSomething("hello lambda", printer); + } +} diff --git a/JdkLearn/src/main/java/com/learnjava/lambda/LambdaDemoForList.java b/JdkLearn/src/main/java/com/learnjava/lambda/LambdaDemoForList.java new file mode 100644 index 0000000..e1aa8e6 --- /dev/null +++ b/JdkLearn/src/main/java/com/learnjava/lambda/LambdaDemoForList.java @@ -0,0 +1,88 @@ +package com.learnjava.lambda; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * @author bruis + * Lambda的List demo + */ +public class LambdaDemoForList { + + public static String[] arrays = {"Monkey", "Lion", "Giraffe", "Lemur"}; + + public static String[] arrays2 = {"Monkey", "Lion", "Giraffe", "Lemur", "Lion"}; + + public static void main(String[] args) { +// test01(); +// test02(); + test03(); + } + + /** + * 去重Distinct + 排序Sort + */ + public static void test03() { + // 去重 + List uniqueAnimals = Stream.of(arrays2) + .distinct() + .collect(Collectors.toList()); + System.out.println(uniqueAnimals); + // 排序 + List sortedAnimals = Stream.of(arrays) + // 对字母是按照自然顺序进行排序的 + .sorted() + .collect(Collectors.toList()); + System.out.println(sortedAnimals); + } + + /** + * Limit + Skip 数据截取 + */ + public static void test02() { + + /** + * 截取前两位字符串 + */ + List limitN = Stream.of(arrays) + .limit(2) + .collect(Collectors.toList()); + + /** + * 过滤掉前两位元素 + */ + List skipN = Stream.of(arrays) + .skip(2) + .collect(Collectors.toList()); + + System.out.println(limitN); + System.out.println(skipN); + } + + /** + * 过滤 + map处理 + */ + public static void test01() { + List nameStrList = Arrays.asList("abc", "efg", "hig", "hii", "klm"); + + List result = nameStrList + .stream() + .filter(s -> s.startsWith("h")) + .map(String::toUpperCase) + // 调用自定义的方法 + .map(MyStringUtils::myToUpperCase) + .collect(Collectors.toList()); + + for (String name : result) { + System.out.println(name); + } + } + + private static class MyStringUtils { + public static String myToUpperCase(String str) { + return str.toUpperCase(); + } + } +} diff --git a/JdkLearn/src/main/java/com/learnjava/lambda/LambdaMapDemo.java b/JdkLearn/src/main/java/com/learnjava/lambda/LambdaMapDemo.java new file mode 100644 index 0000000..cc03752 --- /dev/null +++ b/JdkLearn/src/main/java/com/learnjava/lambda/LambdaMapDemo.java @@ -0,0 +1,139 @@ +package com.learnjava.lambda; + +import java.text.SimpleDateFormat; +import java.util.*; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * @author bruis + * 操作Map + */ +public class LambdaMapDemo { + + public static SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd"); + + public static void main(String[] args) { +// test01(); +// test02(); + test03(); +// test04(); + } + + /** + * 需求,将employee集合中的元素根据date进行排序 + */ + public static void test01() { + List sortedByDate = LambdaComparatorDemo.employees + .stream() + .sorted() + .sorted((a, b) -> { + int result; + try { + result = getDate(b.getDate()).compareTo(getDate(a.getDate())); + } catch (Exception e) { + result = 0; + } + return result; + }) + .collect(Collectors.toList()); + System.out.println(sortedByDate); + } + + /** + * HashMap的merge方法,如果key相同,则通过merge来对key相同的元素进行处理 + */ + public static void test02() { + String key = "money"; + Map map = new HashMap(){{put(key, 100);}}; + + // 第三个参数时BiFunction,聚合函数(可以这么理解) + map.merge(key,100,(oldValue, newValue) -> oldValue + newValue); +// map.merge(key, 100,Integer::sum); + System.out.println(map); + } + + /** + * 对map进行排序 + */ + public static void test03() { + Map codes = new HashMap<>(); + codes.put("2021-03", 1); + codes.put("2021-02", 49); + codes.put("2021-05", 33); +// codes.put("2021-04-01", 1); +// codes.put("2021-04-15", 49); +// codes.put("2021-04-10", 33); +// codes.put("2021-04-05", 86); +// codes.put("2021-04-20", 92); + + // 先将Map转化为List,通过collect处理后再转为Map + Map sortedMap = codes.entrySet() + .stream() +// .sorted((c1, c2) -> c2.getValue().compareTo(c1.getValue())) +// .sorted(Map.Entry.comparingByValue()) + .sorted((c1, c2) -> c2.getKey().compareTo(c1.getKey())) + .collect( + Collectors.toMap( + Map.Entry::getKey, + Map.Entry::getValue, + (oldVal, newVal) -> oldVal, + LinkedHashMap::new + ) + ); + sortedMap.entrySet().forEach(System.out::println); + + } + + /** + * 将list转为map,并用其中一个value作为key + */ + public static void test04() { + LinkedHashMap collect = LambdaComparatorDemo.employees + .stream() + .collect( + Collectors.toMap( + LambdaComparatorDemo.Employee::getDate, + // 这样是返回本身对象的一个表达式,还可以用Function.identity() + // Function.indentity() 就是 t -> t +// employee -> employee, + Function.identity(), + (oldVal, newVal) -> { + // 重复的key就将年纪相加,然后FirstName通过--->加起来 + oldVal.setAge(oldVal.getAge() + newVal.getAge()); + oldVal.setFirstName(oldVal + .getFirstName() + .concat("--->") + .concat(newVal.getFirstName())); + return oldVal; + }, + LinkedHashMap::new + ) + ); + // 这样打印出的map元素不好观察 +// System.out.println(collect); +// collect.entrySet().forEach(System.out::println); + + LinkedHashMap sortedCollect = collect + .entrySet() + .stream() + .sorted((a, b) -> b.getKey().compareTo(a.getKey())) + .collect( + Collectors.toMap( + Map.Entry::getKey, + Map.Entry::getValue, + // 上面已经对重复key做处理了,这里就直接范围oldVal就行 + (oldVal, newVal) -> oldVal, + LinkedHashMap::new + ) + ); + + // 根据日期排序 + sortedCollect.entrySet() + .forEach(System.out::println); + } + + public static Date getDate(String date) throws Exception { + return format.parse(date); + } +} diff --git a/JdkLearn/src/main/java/com/learnjava/lambda/LambdaMapMerge.java b/JdkLearn/src/main/java/com/learnjava/lambda/LambdaMapMerge.java new file mode 100644 index 0000000..d78e73f --- /dev/null +++ b/JdkLearn/src/main/java/com/learnjava/lambda/LambdaMapMerge.java @@ -0,0 +1,58 @@ +package com.learnjava.lambda; + +import java.util.HashMap; +import java.util.Map; + +/** + * 关于Map的合并操作 + * + * @author lhy + * @date 2021/7/20 + */ +public class LambdaMapMerge { + + public static void main(String[] args) { +// mapMerge(); +// mapMerge2(); + } + + /** + * value为int类型的map merge操作,将两个map,相同key merge在一起 + * + * key:string + * value:int + */ + public static void mapMerge() { + Map map1= new HashMap<>(); + map1.put("one",1); + map1.put("two",2); + map1.put("three",3); + Map map2= new HashMap<>(); + map2.put("one",1); + map2.put("two",2); + + map1.forEach((key, value) -> map2.merge(key, value, Integer::sum)); + System.out.println(map2); + } + + /** + * value为int类型的map merge操作,将两个map,相同key merge在一起 + * + * key:string + * value:String + */ + public static void mapMerge2() { + Map map1= new HashMap<>(); + map1.put("one","1"); + map1.put("two","2"); + map1.put("three","3"); + Map map2= new HashMap<>(); + map2.put("one","1"); + map2.put("two","2"); + + map1.forEach((key, value) -> map2.merge(key, value,(total, num) -> String.valueOf(Integer.parseInt(total) + Integer.parseInt(num)))); + + System.out.println(map2); + + } +} diff --git a/JdkLearn/src/main/java/com/learnjava/lambda/LambdaReduceDemo.java b/JdkLearn/src/main/java/com/learnjava/lambda/LambdaReduceDemo.java new file mode 100644 index 0000000..b656747 --- /dev/null +++ b/JdkLearn/src/main/java/com/learnjava/lambda/LambdaReduceDemo.java @@ -0,0 +1,104 @@ +package com.learnjava.lambda; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Stream; + +/** + * @author bruis + * 累加器 + */ +public class LambdaReduceDemo { + + public static void main(String[] args) { +// test01(); +// test02(); + test03(); + } + + /** + * 对整形进行操作 + */ + public static void test01() { + List numbers = Arrays.asList(1, 2, 3, 4, 5); + Integer reduce = numbers + .stream() + .reduce(0, LambdaReduceDemo::mySum); +// .reduce(0, Integer::sum); +// .reduce(0, (total, element) -> total + element); + System.out.println(reduce); + } + + /** + * 对字符串进行操作 + */ + public static void test02() { + List letters = Arrays.asList("a", "b", "c", "d", "e"); + String reduce = letters + .stream() + .reduce("", String::concat); +// .reduce("", (totol, element) -> totol.concat(element)); + System.out.println(reduce); + } + + /** + * 操作Employee集合元素,将所有员工的年龄通过reduce累加起来 + * + * U identity, BiFunction accumulator, BinaryOperator combiner + * reduce的三个参数: + * 1)初始值; + * 2)累加器(可自己实现逻辑) + * 3) 合并器(parallelStream模式时的合并) + * + */ + public static void test03() { + // 将Employee集合元素转化为Integer集合元素(流) + // map的操作就是将a类型元素转化为b类型元素 + Stream integerStream = LambdaComparatorDemo + .employees + .stream() + .map(LambdaComparatorDemo.Employee::getAge); + + // 求所有员工的年龄 + Integer totalAge = integerStream.reduce(0, Integer::sum); + System.out.println(totalAge); + + // 数据量大的话,可以用并行计算 + // 先讲员工集合转化为“并行流” + Stream parallelStream = LambdaComparatorDemo + .employees + .parallelStream() + .map(LambdaComparatorDemo.Employee::getAge); + + // 相比于普通的单个流,parallelStream多了个合并器,将多个CPU计算的结果再合并到同一个流 + Integer reduce = parallelStream.reduce(0, Integer::sum, Integer::sum); + System.out.println(reduce); + + // 可以不用map将employee转化为Integer对象,可以直接reduce操作,最后通过Integer::sum这个合并器来将结果合并为Integer类型 + Integer total = LambdaComparatorDemo + .employees + .stream() + .reduce(0, (subTotal, emp) -> subTotal + emp.getAge(), Integer::sum); + System.out.println(total); + + Integer total2 = LambdaComparatorDemo + .employees + .stream() + .reduce(0, LambdaReduceDemo::mySum2, Integer::sum); + System.out.println(total2); + } + + /** + * 可作为BiFunction,传入到reduce作为入参 + * @param a + * @param b + * @return + */ + public static Integer mySum(int a, int b) { + return a + b; + } + + public static Integer mySum2(int a, LambdaComparatorDemo.Employee employee) { + return a + employee.getAge(); + } +} diff --git a/JdkLearn/src/main/java/com/learnjava/lambda/StreamMatchDemo.java b/JdkLearn/src/main/java/com/learnjava/lambda/StreamMatchDemo.java new file mode 100644 index 0000000..3962b48 --- /dev/null +++ b/JdkLearn/src/main/java/com/learnjava/lambda/StreamMatchDemo.java @@ -0,0 +1,49 @@ +package com.learnjava.lambda; + +import java.util.Optional; + +/** + * @author bruis + */ +public class StreamMatchDemo { + + public static void main(String[] args) { +// test01(); + test02(); + } + + /** + * 判断是否有年龄大于70的员工 + */ + public static void test01() { + boolean isExistAgeThan70 = LambdaComparatorDemo.employees + .stream() + // 使用了Employee的谓语语句(这种写法方便复用) + .anyMatch(LambdaComparatorDemo.Employee.ageGreaterThan70); +// .anyMatch(e -> e.getAge() > 70); + System.out.println(isExistAgeThan70); + + boolean isExistAgeLessThan18 = LambdaComparatorDemo.employees + .stream() + .noneMatch(LambdaComparatorDemo.Employee.ageLessThan18); + + System.out.println(isExistAgeLessThan18); + } + + /** + * 元素查找与Optional + */ + public static void test02() { + + Optional employeeOptional = LambdaComparatorDemo.employees + .stream() + .filter(e -> e.getAge() > 400) + .findFirst(); + + // Optional#get 会报空 +// System.out.println(employeeOptional.get()); + LambdaComparatorDemo.Employee employee = employeeOptional.orElse(null); + System.out.println(employee == null); + } + +} diff --git a/JdkLearn/src/main/java/com/learnjava/lambda/StreamParallelDemo.java b/JdkLearn/src/main/java/com/learnjava/lambda/StreamParallelDemo.java new file mode 100644 index 0000000..93c120c --- /dev/null +++ b/JdkLearn/src/main/java/com/learnjava/lambda/StreamParallelDemo.java @@ -0,0 +1,10 @@ +package com.learnjava.lambda; + +/** + * + * 串行、并行 todo + * + * @author bruis + */ +public class StreamParallelDemo { +} diff --git a/JdkLearn/src/main/java/com/learnjava/optimization/BeanCopierDemo.java b/JdkLearn/src/main/java/com/learnjava/optimization/BeanCopierDemo.java new file mode 100644 index 0000000..5068206 --- /dev/null +++ b/JdkLearn/src/main/java/com/learnjava/optimization/BeanCopierDemo.java @@ -0,0 +1,170 @@ +package com.learnjava.optimization; + +import org.springframework.cglib.beans.BeanCopier; + +/** + * + * 经过测试,BeanCopier性能是BeanUtils10倍左右。 + * + * BeanCopier拷贝速度快,性能瓶颈出现在创建BeanCopier实例的过程中。 所以,把创建过的BeanCopier实例放到缓存中,下次可以直接获取,提升性能: + * + * + * @author lhy + * @date 2021/7/21 + */ +public class BeanCopierDemo { + + private static final BeanCopier BEAN_COPIER = BeanCopier.create(Person.class, PersonVo.class, false); + + public static void main(String[] args) { + Person person = new Person("zs", "high School", 16, 177, 126); + PersonVo vo = new PersonVo(); + + BEAN_COPIER.copy(person, vo, null); + + System.out.println(vo); + } + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + public static class PersonVo { + private String name; + private String grade; + private Integer age; + private Integer height; + private Integer weight; + + @Override + public String toString() { + return "PersonVo{" + + "name='" + name + '\'' + + ", grade='" + grade + '\'' + + ", age=" + age + + ", height=" + height + + ", weight=" + weight + + '}'; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getGrade() { + return grade; + } + + public void setGrade(String grade) { + this.grade = grade; + } + + public Integer getAge() { + return age; + } + + public void setAge(Integer age) { + this.age = age; + } + + public Integer getHeight() { + return height; + } + + public void setHeight(Integer height) { + this.height = height; + } + + public Integer getWeight() { + return weight; + } + + public void setWeight(Integer weight) { + this.weight = weight; + } + } + + public static class Person { + private String name; + private String grade; + private Integer age; + private Integer height; + private Integer weight; + + public Person(String name, String grade, Integer age, Integer height, Integer weight) { + this.name = name; + this.grade = grade; + this.age = age; + this.height = height; + this.weight = weight; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getGrade() { + return grade; + } + + public void setGrade(String grade) { + this.grade = grade; + } + + public Integer getAge() { + return age; + } + + public void setAge(Integer age) { + this.age = age; + } + + public Integer getHeight() { + return height; + } + + public void setHeight(Integer height) { + this.height = height; + } + + public Integer getWeight() { + return weight; + } + + public void setWeight(Integer weight) { + this.weight = weight; + } + } +} diff --git a/JdkLearn/src/main/java/com/learnjava/optimization/CaffeineDemo.java b/JdkLearn/src/main/java/com/learnjava/optimization/CaffeineDemo.java new file mode 100644 index 0000000..88de1a0 --- /dev/null +++ b/JdkLearn/src/main/java/com/learnjava/optimization/CaffeineDemo.java @@ -0,0 +1,131 @@ +package com.learnjava.optimization; + +import com.github.benmanes.caffeine.cache.Cache; +import com.github.benmanes.caffeine.cache.Caffeine; +import com.github.benmanes.caffeine.cache.RemovalListener; + +import java.math.BigDecimal; +import java.util.concurrent.TimeUnit; + +/** + * Caffeine 代码Demo(SpringBoot自带的缓存类) + * + * + * @author lhy + * @date 2021/7/22 + */ +public class CaffeineDemo { + + private static Cache productVoCache; + + public static RemovalListener listener = (k, v, cause) -> { + // 业务逻辑 + + + // 触发异常 + switch (cause) { + // 过期 + case EXPIRED: + break; + // 手动删除 + case EXPLICIT: + break; + // 被替换 + case REPLACED: + break; + // 垃圾回收 + case COLLECTED: + break; + // 超过数量限制 + case SIZE: + break; + default: + break; + } + }; + + public static void main(String[] args) { + // 初始化 + // afterPropertiesSet(); + } + + /** + * 模拟Spring的类初始化的时候对缓存进行初始化 + */ + public static void afterPropertiesSet() { + productVoCache = Caffeine.newBuilder() + .softValues() + .refreshAfterWrite(7200, TimeUnit.SECONDS) + .removalListener(listener) + // .build(k -> loadSync(k)) + // 非static类中,可以使用这种方式.build(this::loadSync); + .build(CaffeineDemo::loadSync); + } + + /** + * 获取对应缓存内容 + * @param key + * @return + */ + public static ProductVo getProductVo(String key) { + return productVoCache.get(key, CaffeineDemo::loadSync); + } + + /** + * 对对应商品进行缓存 + * @param key + */ + public static void putProductVo(String key) { + productVoCache.put(key, loadSync(key)); + } + + private static ProductVo loadSync(String key) { + // 业务逻辑 + return new ProductVo(); + } + + + + + + + + + + + + + + + + + + + + + + public static class ProductVo { + + private String productName; + + private BigDecimal price; + + public ProductVo() {} + + public String getProductName() { + return productName; + } + + public void setProductName(String productName) { + this.productName = productName; + } + + public BigDecimal getPrice() { + return price; + } + + public void setPrice(BigDecimal price) { + this.price = price; + } + } +} diff --git a/JdkLearn/src/main/java/com/learnjava/optimization/OptimizeDemo.java b/JdkLearn/src/main/java/com/learnjava/optimization/OptimizeDemo.java new file mode 100644 index 0000000..fe81a19 --- /dev/null +++ b/JdkLearn/src/main/java/com/learnjava/optimization/OptimizeDemo.java @@ -0,0 +1,70 @@ +package com.learnjava.optimization; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * + * 代码优化技巧总结 + * + * @author lhy + * @date 2021/7/19 + */ +public class OptimizeDemo { + public static void main(String[] args) { + Map map = new HashMap<>(); + mergeData(map); + + Map concurrentMap = new ConcurrentHashMap<>(); + concurrentMergeData(concurrentMap); + } + + /** + * 对于通过map来聚合数据(非Lambda方式) + * @param map + */ + public static void mergeData(Map map) { + String key = "mapKey"; + int value = 1; + // 普通方式 + if (map.containsKey(key)) { + map.put(key, map.get(key) + value); + } else { + map.put(key,value); + } + + // 简洁方式 + Integer mapValue = map.get(key); + if (null != mapValue) { + mapValue += value; + } else { + mapValue = value; + } + map.put(key, mapValue); + } + + /** + * 针对mergeData里map的put操作,在并发情况下会存在put的时候,以及有其他线程已经put成功了,导致线程不安全, + * 所以需要使用并发集合列的putIfAbsent方法 + * @param map + */ + public static void concurrentMergeData(Map map) { + String key = "mapKey"; + int value = 1; + Integer mapValue = map.get(key); + if (null != mapValue) { + mapValue += value; + } else { + mapValue = value; + } + map.putIfAbsent(key, mapValue); + + // computeIfAbsent方法对map中的key只进行重新计算,如果不存在这个key,则添加到map中 + map.computeIfAbsent(key, (k) -> { + // 其他计算 + int a = 1, b = 2; + return a + b; + }); + } +} diff --git a/JdkLearn/src/main/java/com/learnjava/optimization/OptimizeUtilDemo.java b/JdkLearn/src/main/java/com/learnjava/optimization/OptimizeUtilDemo.java new file mode 100644 index 0000000..6f131f6 --- /dev/null +++ b/JdkLearn/src/main/java/com/learnjava/optimization/OptimizeUtilDemo.java @@ -0,0 +1,21 @@ +package com.learnjava.optimization; + +/** + * 优化工具类例子 + * + * @author lhy + * @date 2021/7/20 + */ +public class OptimizeUtilDemo { + + /** + * 超快深拷贝工具类BeanCopier + */ + private BeanCopierDemo beanCopierDemo; + + /** + * Caffeine Cache + */ + private CaffeineDemo caffeineDemo; + +} diff --git a/JdkLearn/src/main/java/com/learnjava/proxy/dynamicproxy/DemoInvokerHandler.java b/JdkLearn/src/main/java/com/learnjava/proxy/dynamicproxy/DemoInvokerHandler.java new file mode 100644 index 0000000..c04119e --- /dev/null +++ b/JdkLearn/src/main/java/com/learnjava/proxy/dynamicproxy/DemoInvokerHandler.java @@ -0,0 +1,53 @@ +package com.learnjava.proxy.dynamicproxy; + +import com.learnjava.proxy.staticproxy.RealSubject; +import com.learnjava.proxy.staticproxy.Subject; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; + +/** + * @author bruis + * + * jdk动态代理 + * + */ +public class DemoInvokerHandler implements InvocationHandler { + + // 真正的业务对象 + private Object realSubject; + + public DemoInvokerHandler(Object realSubject) { + this.realSubject = realSubject; + } + + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + System.out.println("代理预处理操作"); + + // 调用真正的代理对象逻辑 + Object result = method.invoke(realSubject, args); + + System.out.println("代理后处理操作"); + return result; + } + + /** + * 创建代理对象并返回 + * @return + */ + public Object getProxy() { + return Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(), + realSubject.getClass().getInterfaces(), this); + } + + public static void main(String[] args) { + RealSubject realSubject = new RealSubject(); + DemoInvokerHandler invokerHandler = new DemoInvokerHandler(realSubject); + Subject proxy = (Subject) invokerHandler.getProxy(); + + // 拿到业务对象,执行业务逻辑,此时业务逻辑已经被代理对象代理了 + proxy.operation(); + } +} diff --git a/JdkLearn/src/main/java/com/learnjava/proxy/staticproxy/JdkStaticProxy.java b/JdkLearn/src/main/java/com/learnjava/proxy/staticproxy/JdkStaticProxy.java new file mode 100644 index 0000000..885687d --- /dev/null +++ b/JdkLearn/src/main/java/com/learnjava/proxy/staticproxy/JdkStaticProxy.java @@ -0,0 +1,16 @@ +package com.learnjava.proxy.staticproxy; + +/** + * @author bruis + * + * JDK静态代理 + * + * 特点: + * 1. 静态代理模式需要在编译模式为每个业务类创建代理类Proxy,当需要代理的类很多时,就会出现大量的Proxy类 + * + */ +public class JdkStaticProxy { + + + +} diff --git a/JdkLearn/src/main/java/com/learnjava/proxy/staticproxy/RealSubject.java b/JdkLearn/src/main/java/com/learnjava/proxy/staticproxy/RealSubject.java new file mode 100644 index 0000000..600ccc1 --- /dev/null +++ b/JdkLearn/src/main/java/com/learnjava/proxy/staticproxy/RealSubject.java @@ -0,0 +1,10 @@ +package com.learnjava.proxy.staticproxy; + +public class RealSubject implements Subject { + + @Override + public void operation() { + System.out.println("这是真正的业务类"); + } + +} diff --git a/JdkLearn/src/main/java/com/learnjava/proxy/staticproxy/Subject.java b/JdkLearn/src/main/java/com/learnjava/proxy/staticproxy/Subject.java new file mode 100644 index 0000000..d48cf48 --- /dev/null +++ b/JdkLearn/src/main/java/com/learnjava/proxy/staticproxy/Subject.java @@ -0,0 +1,5 @@ +package com.learnjava.proxy.staticproxy; + +public interface Subject { + public void operation(); +} diff --git a/JdkLearn/src/main/java/com/learnjava/proxy/staticproxy/SubjectProxy.java b/JdkLearn/src/main/java/com/learnjava/proxy/staticproxy/SubjectProxy.java new file mode 100644 index 0000000..f151e73 --- /dev/null +++ b/JdkLearn/src/main/java/com/learnjava/proxy/staticproxy/SubjectProxy.java @@ -0,0 +1,22 @@ +package com.learnjava.proxy.staticproxy; + +public class SubjectProxy implements Subject { + + private RealSubject realSubject; + + public SubjectProxy() { + realSubject = new RealSubject(); + } + + @Override + public void operation() { + System.out.println("代理预处理逻辑"); + realSubject.operation(); + System.out.println("代理后处理逻辑"); + } + + public static void main(String[] args) { + SubjectProxy proxy = new SubjectProxy(); + proxy.operation(); + } +} diff --git a/JdkLearn/src/main/java/com/learnjava/reference/PhantomRefTest.java b/JdkLearn/src/main/java/com/learnjava/reference/PhantomRefTest.java new file mode 100644 index 0000000..426ef67 --- /dev/null +++ b/JdkLearn/src/main/java/com/learnjava/reference/PhantomRefTest.java @@ -0,0 +1,35 @@ +package com.learnjava.reference; + +import java.lang.ref.PhantomReference; +import java.lang.ref.Reference; +import java.lang.ref.ReferenceQueue; + +public class PhantomRefTest { + + public static void main(String[] args) throws InterruptedException { + ReferenceQueue referenceQueue = new ReferenceQueue(); + + // 10mb + byte[] buffer = new byte[1024 * 1024 * 10]; + + PhantomReference phantomReference = new PhantomReference(buffer, referenceQueue); + + // 字节数组对象,失去了强引用 + buffer = null; + + Reference ref0 = referenceQueue.poll(); + + System.out.println("gc 执行之前, refQueue中是否有数据?" + (ref0 != null ? "有" : "没有")); + System.out.println("gc 执行之前, ref引用的对象:" + phantomReference.get()); + + System.gc(); + // 确保gc程序执行 + Thread.sleep(1000); + + System.out.println("gc 执行之后, ref引用的对象:" + phantomReference.get()); + + Reference ref = referenceQueue.poll(); + System.out.println("gc 执行之后, refQueue中是否有数据?" + (ref != null ? "有" : "没有")); + System.out.println("referenceQueue 中获取的 ref与 weakReference中的是否一致?" + (ref == phantomReference)); + } +} diff --git a/JdkLearn/src/main/java/com/learnjava/reference/SoftReferenceTest.java b/JdkLearn/src/main/java/com/learnjava/reference/SoftReferenceTest.java new file mode 100644 index 0000000..38e706e --- /dev/null +++ b/JdkLearn/src/main/java/com/learnjava/reference/SoftReferenceTest.java @@ -0,0 +1,35 @@ +package com.learnjava.reference; + +import java.lang.ref.Reference; +import java.lang.ref.SoftReference; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +/** + * 软引用 + */ +public class SoftReferenceTest { + public static void main(String[] args) throws InterruptedException { + List refList = new ArrayList(); + + for (int i = 0; i < 1000; i++) { + // 10mb + byte[] buffer = new byte[1024 * 1024 * 10]; + + SoftReference softReference = new SoftReference(buffer); + + refList.add(softReference); + } + + System.gc(); + Thread.sleep(1000); + + Iterator it = refList.iterator(); + + while (it.hasNext()) { + Reference ref = it.next(); + System.out.println("当前ref引用的对象:" + ref.get()); + } + } +} diff --git a/JdkLearn/src/main/java/com/learnjava/reference/WeakReferenceTest.java b/JdkLearn/src/main/java/com/learnjava/reference/WeakReferenceTest.java new file mode 100644 index 0000000..2e2ec2d --- /dev/null +++ b/JdkLearn/src/main/java/com/learnjava/reference/WeakReferenceTest.java @@ -0,0 +1,66 @@ +package com.learnjava.reference; + +import java.lang.ref.Reference; +import java.lang.ref.ReferenceQueue; +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +/** + * soft软引用 gc堆内存不够时,可能会回收 + * weak弱引用 gc时,内存一定会回收不可用对象 + * phantom虚引用 + */ +public class WeakReferenceTest { + public static void main(String[] args) throws Exception { +// test01(); + test02(); + } + + private static void test01() throws InterruptedException { + List refList = new ArrayList(); + + for (int i = 0; i < 1000; i++) { + // 10mb + byte[] buffer = new byte[1024 * 1024 * 10]; + + WeakReference weakReference = new WeakReference(buffer); + + refList.add(weakReference); + } + + // 将buffer直接全部回收了 + System.gc(); + Thread.sleep(1000); + Iterator iterator = refList.iterator(); + while (iterator.hasNext()) { + Reference ref = iterator.next(); + System.out.println("当前ref引用的对象:" + ref.get()); + } + } + + private static void test02() throws InterruptedException { + ReferenceQueue refQueue = new ReferenceQueue(); + + // 10 mb + byte[] buffer = new byte[1024 * 1024 * 10]; + WeakReference weakReference = new WeakReference(buffer, refQueue); + // 失去强引用关联 + buffer = null; + + Reference ref0 = refQueue.poll(); + System.out.println("gc 执行之前, refQueue中是否有数据?" + (ref0 != null ? "有" : "没有")); + System.out.println("gc 执行之前, ref引用的对象:" + weakReference.get()); + + System.gc(); + // 确保gc程序执行 + Thread.sleep(1000); + + System.out.println("gc 执行之后, ref引用的对象:" + weakReference.get()); + + Reference ref = refQueue.poll(); + System.out.println("gc 执行之后, refQueue中是否有数据?" + (ref != null ? "有" : "没有")); + System.out.println("referenceQueue 中获取的 ref与 weakReference中的是否一致?" + (ref == weakReference)); + } +} diff --git a/JdkLearn/src/test/java/com/learnjava/string/StringTest.java b/JdkLearn/src/test/java/com/learnjava/string/StringTest.java new file mode 100644 index 0000000..72e66c0 --- /dev/null +++ b/JdkLearn/src/test/java/com/learnjava/string/StringTest.java @@ -0,0 +1,49 @@ +package com.learnjava.string; + +import org.junit.Test; + +/** + * @author LuoHaiYang + */ +public class StringTest { + + @Test + public void testSplitWithSplit() { + String splitStr1 = "what,is,,,,split"; + String[] strs1 = splitStr1.split(","); + for (String s : strs1) { + System.out.println(s); + } + System.out.println(strs1.length); + } + + @Test + public void testSplitWithOutSplit() { + String splitStr1 = "what,is,,,,"; + String[] strs1 = splitStr1.split(","); + for (String s : strs1) { + System.out.println(s); + } + System.out.println(strs1.length); + } + + @Test + public void testSplitLimit() { + String splitStr1 = "what,is,,,,"; + String[] strs1 = splitStr1.split(",", -1); + for (String s : strs1) { + System.out.println(s); + } + System.out.println(strs1.length); + } + + @Test + public void testSplitLimitGreaterThanZero() { + String splitStr1 = "what,is,,,,"; + String[] strs1 = splitStr1.split(",", 3); + for (String s : strs1) { + System.out.println(s); + } + System.out.println(strs1.length); + } +} diff --git a/README.md b/README.md index 577b9cb..fae4153 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,4 @@ +![JavaSourceCodeLearningImage](https://github.com/coderbruis/JavaSourceCodeLearning/blob/master/note/images/JavaSourceCodeLearningImage.png)

@@ -16,65 +17,153 @@ Spring-Security-OAuth2 - Netty + Netty + + + Netty

-源码深入学习:Spring源码、SpringBoot源码、SpringAOP源码、SpringSecurity源码、OAuth2源码、JDK源码、Netty源码 +Java流行框架源码分析,学习以及总结。项目持续更新中,不建议直接Fork,欢迎star、watch。 + +对于框架底层源码的学习,需要反复、认真思考,并做到温故而知新,这样才能将底层原理吸收得更加牢固。 + + +框架包括: + + +✅ Spring源码 + +✅ SpringBoot源码 + +✅ SpringAOP源码 + +✅ SpringSecurity源码 + +✅ SpringSecurity OAuth2源码 + +✅ JDK源码 + +✅ Dubbo源码 + +✅ Netty源码 + +✅ RocketMQ源码 + +MyBatis源码 + +SpringCloud源码 + +> 为什么要分析、学习源码? -Tip: -如果读者电脑无法浏览到github图片,则需要设置hosts配置文件, 解决办法:[解决GitHub网页githubusercontent地址无法访问问题 -](https://zhuanlan.zhihu.com/p/107691233) +学习一个框架的源码,不仅在实际使用时如果出现问题,可以快速定位出问题,找到问题原因并解决,同时还可以学习到框架的架构思想以与设计模式。当然,学习框架底层源码还可以提升我们自身的水平,在大厂面试时能够在众多面试者中脱颖而出。因此学习框架虽然枯燥乏味,但罗马并非一日建成的,所以平时之余就要多学习框架底层源码, +这样在用到的时候就能游刃有余。 + +> Tip:如果读者电脑无法浏览到github图片,则需要设置hosts配置文件, 解决办法:[解决GitHub网页githubusercontent地址无法访问问题](https://zhuanlan.zhihu.com/p/107691233) # 目录 - 项目导入 - 将整个JavaSourceLearning导入IDEA中,然后选中项目pom.xml文件右键,open as maven project然后等待maven下载相应jar包即可。 + 将整个JavaSourceCodeLearning导入IDEA中,然后选中项目pom.xml文件右键,open as maven project然后等待maven下载相应jar包即可。 - JDK源码学习 - JDK版本:1.8.0_77 - - [深入学习String源码与底层(一)](https://blog.csdn.net/CoderBruis/article/details/94884673) - - [深入学习String源码与底层(二)](https://blog.csdn.net/CoderBruis/article/details/95620935) - - [深入解读CompletableFuture源码与原理](https://blog.csdn.net/CoderBruis/article/details/103181520) + - [深入学习String源码与底层(一)](https://github.com/coderbruis/JavaSourceLearning/blob/master/note/JDK/%E6%B7%B1%E5%85%A5%E5%AD%A6%E4%B9%A0String%E6%BA%90%E7%A0%81%E4%B8%8E%E5%BA%95%E5%B1%82%EF%BC%88%E4%B8%80%EF%BC%89.md) + - [深入学习String源码与底层(二)](https://github.com/coderbruis/JavaSourceLearning/blob/master/note/JDK/%E6%B7%B1%E5%85%A5%E5%AD%A6%E4%B9%A0String%E6%BA%90%E7%A0%81%E4%B8%8E%E5%BA%95%E5%B1%82%EF%BC%88%E4%BA%8C%EF%BC%89.md) + - [深入解读CompletableFuture源码与原理](https://github.com/coderbruis/JavaSourceLearning/blob/master/note/JDK/%E6%B7%B1%E5%85%A5%E8%A7%A3%E8%AF%BBCompletableFuture%E6%BA%90%E7%A0%81%E4%B8%8E%E5%8E%9F%E7%90%86.md) + - [一篇文章快速深入学习ThreadLocal](https://github.com/coderbruis/JavaSourceLearning/blob/master/note/JDK/%E4%B8%80%E7%AF%87%E6%96%87%E7%AB%A0%E5%BF%AB%E9%80%9F%E6%B7%B1%E5%85%A5%E5%AD%A6%E4%B9%A0ThreadLocal.md) + - [深入学习Java volatile关键字](https://github.com/coderbruis/JavaSourceLearning/blob/master/note/JDK/%E6%B7%B1%E5%85%A5%E5%AD%A6%E4%B9%A0Java%20volatile%E5%85%B3%E9%94%AE%E5%AD%97.md) + - [深入学习Thread底层原理](https://github.com/coderbruis/JavaSourceCodeLearning/blob/master/note/JDK/%E6%B7%B1%E5%85%A5%E5%AD%A6%E4%B9%A0Thread%E5%BA%95%E5%B1%82%E6%BA%90%E7%A0%81.md) + - [深入学习JDK1.7、8 HashMap扩容原理]() + - [开源项目里那些看不懂的位运算分析](https://github.com/coderbruis/JavaSourceCodeLearning/blob/master/note/JDK/%E5%BC%80%E6%BA%90%E9%A1%B9%E7%9B%AE%E9%87%8C%E9%82%A3%E4%BA%9B%E7%9C%8B%E4%B8%8D%E6%87%82%E7%9A%84%E4%BD%8D%E8%BF%90%E7%AE%97%E5%88%86%E6%9E%90.md) - Spring源码学习 - Spring版本:5.2.1.RELEASE - - 超大超全Spring源码知识脑图 - - - [在IDEA中构建Spring源码](https://blog.csdn.net/CoderBruis/article/details/85840438) - - [深入SpringIOC容器一](https://blog.csdn.net/CoderBruis/article/details/85940756) - - [深入SpringIOC容器二](https://blog.csdn.net/CoderBruis/article/details/86505582) + - [深入Spring源码系列(一)——在IDEA中构建Spring源码](https://github.com/coderbruis/JavaSourceLearning/blob/master/note/Spring/%E6%B7%B1%E5%85%A5Spring%E6%BA%90%E7%A0%81%E7%B3%BB%E5%88%97%EF%BC%88%E4%B8%80%EF%BC%89%E2%80%94%E2%80%94%E5%9C%A8IDEA%E4%B8%AD%E6%9E%84%E5%BB%BASpring%E6%BA%90%E7%A0%81.md) + - [深入Spring源码系列(二)——深入Spring容器,通过源码阅读和时序图来彻底弄懂Spring容器(上)](https://github.com/coderbruis/JavaSourceLearning/blob/master/note/Spring/%E6%B7%B1%E5%85%A5Spring%E6%BA%90%E7%A0%81%E7%B3%BB%E5%88%97%EF%BC%88%E4%BA%8C%EF%BC%89%E2%80%94%E2%80%94%E6%B7%B1%E5%85%A5Spring%E5%AE%B9%E5%99%A8%EF%BC%8C%E9%80%9A%E8%BF%87%E6%BA%90%E7%A0%81%E9%98%85%E8%AF%BB%E5%92%8C%E6%97%B6%E5%BA%8F%E5%9B%BE%E6%9D%A5%E5%BD%BB%E5%BA%95%E5%BC%84%E6%87%82Spring%E5%AE%B9%E5%99%A8%EF%BC%88%E4%B8%8A%EF%BC%89.md) + - [深入Spring源码系列(二)——深入Spring容器,通过源码阅读和时序图来彻底弄懂Spring容器(下)](https://github.com/coderbruis/JavaSourceLearning/blob/master/note/Spring/%E6%B7%B1%E5%85%A5Spring%E6%BA%90%E7%A0%81%E7%B3%BB%E5%88%97%EF%BC%88%E4%BA%8C%EF%BC%89%E2%80%94%E2%80%94%E6%B7%B1%E5%85%A5Spring%E5%AE%B9%E5%99%A8%EF%BC%8C%E9%80%9A%E8%BF%87%E6%BA%90%E7%A0%81%E9%98%85%E8%AF%BB%E5%92%8C%E6%97%B6%E5%BA%8F%E5%9B%BE%E6%9D%A5%E5%BD%BB%E5%BA%95%E5%BC%84%E6%87%82Spring%E5%AE%B9%E5%99%A8%EF%BC%88%E4%B8%8B%EF%BC%89.md) + - [深入Spring源码系列(补充篇)——程序调用Spring源码](https://github.com/coderbruis/JavaSourceLearning/blob/master/note/Spring/%E6%B7%B1%E5%85%A5Spring%E6%BA%90%E7%A0%81%E7%B3%BB%E5%88%97%EF%BC%88%E8%A1%A5%E5%85%85%E7%AF%87%EF%BC%89%E2%80%94%E2%80%94%E7%A8%8B%E5%BA%8F%E8%B0%83%E7%94%A8Spring%E6%BA%90%E7%A0%81.md) + - [从Spring源码中学习——策略模式](https://github.com/coderbruis/JavaSourceLearning/blob/master/note/Spring/%E4%BB%8ESpring%E6%BA%90%E7%A0%81%E4%B8%AD%E5%AD%A6%E4%B9%A0%E2%80%94%E2%80%94%E7%AD%96%E7%95%A5%E6%A8%A1%E5%BC%8F.md) - SpringAOP源码学习 - Spring版本:5.2.1.RELEASE - - [深入学习SpringAOP源码(一)——注册AnnotationAwareAspectJAutoProxyCreator](https://blog.csdn.net/CoderBruis/article/details/100031756) - - [深入学习SpringAOP源码(二)—— 深入AnnotationAwareAspectJAutoProxyCreator](https://blog.csdn.net/CoderBruis/article/details/100042081) - - [深入学习SpringAOP源码(三)——揭开JDK动态代理和CGLIB代理的神秘面纱](https://blog.csdn.net/CoderBruis/article/details/100083575) - + - [深入学习SpringAOP源码(一)——注册AnnotationAwareAspectJAutoProxyCreator](https://github.com/coderbruis/JavaSourceLearning/blob/master/note/SpringAOP/%E6%B7%B1%E5%85%A5%E5%AD%A6%E4%B9%A0SpringAOP%E6%BA%90%E7%A0%81%EF%BC%88%E4%B8%80%EF%BC%89%E2%80%94%E2%80%94%E6%B3%A8%E5%86%8CAnnotationAwareAspectJAutoProxyCreator.md) + - [深入学习SpringAOP源码(二)—— 深入AnnotationAwareAspectJAutoProxyCreator](https://github.com/coderbruis/JavaSourceLearning/blob/master/note/SpringAOP/%E6%B7%B1%E5%85%A5%E5%AD%A6%E4%B9%A0SpringAOP%E6%BA%90%E7%A0%81%EF%BC%88%E4%BA%8C%EF%BC%89%E2%80%94%E2%80%94%20%E6%B7%B1%E5%85%A5AnnotationAwareAspectJAutoProxyCreator.md) + - [深入学习SpringAOP源码(三)——揭开JDK动态代理和CGLIB代理的神秘面纱](https://github.com/coderbruis/JavaSourceLearning/blob/master/note/SpringAOP/%E6%B7%B1%E5%85%A5%E5%AD%A6%E4%B9%A0SpringAOP%E6%BA%90%E7%A0%81%EF%BC%88%E4%B8%89%EF%BC%89%E2%80%94%E2%80%94%E6%8F%AD%E5%BC%80JDK%E5%8A%A8%E6%80%81%E4%BB%A3%E7%90%86%E5%92%8CCGLIB%E4%BB%A3%E7%90%86%E7%9A%84%E7%A5%9E%E7%A7%98%E9%9D%A2%E7%BA%B1.md) + - SpringBoot源码学习 - SpringBoot版本:2.2.1.RELEASE - - 超大超全Spring源码知识脑图 + - [深入浅出SpringBoot源码——SpringFactoriesLoader](https://github.com/coderbruis/JavaSourceLearning/blob/master/note/SpringBoot/%E6%B7%B1%E5%85%A5SpringBoot%E6%BA%90%E7%A0%81%E5%AD%A6%E4%B9%A0%E4%B9%8B%E2%80%94%E2%80%94SpringFactoriesLoader.md) + - [深入浅出SpringBoot源码——监听器与事件机制](https://github.com/coderbruis/JavaSourceLearning/blob/master/note/SpringBoot/%E6%B7%B1%E5%85%A5SpringBoot%E6%BA%90%E7%A0%81%E5%AD%A6%E4%B9%A0%E4%B9%8B%E2%80%94%E2%80%94%E7%9B%91%E5%90%AC%E5%99%A8%E4%B8%8E%E4%BA%8B%E4%BB%B6%E6%9C%BA%E5%88%B6.md) + - [深入浅出SpringBoot源码——系统初始化器](https://github.com/coderbruis/JavaSourceCodeLearning/blob/master/note/SpringBoot/%E6%B7%B1%E5%85%A5SpringBoot%E6%BA%90%E7%A0%81%E5%AD%A6%E4%B9%A0%E4%B9%8B%E2%80%94%E2%80%94%E7%B3%BB%E7%BB%9F%E5%88%9D%E5%A7%8B%E5%8C%96%E5%99%A8.md) + - [深入浅出SpringBoot源码——启动加载器](https://github.com/coderbruis/JavaSourceCodeLearning/blob/master/note/SpringBoot/%E6%B7%B1%E5%85%A5SpringBoot%E6%BA%90%E7%A0%81%E5%AD%A6%E4%B9%A0%E4%B9%8B%E2%80%94%E2%80%94%E5%90%AF%E5%8A%A8%E5%8A%A0%E8%BD%BD%E5%99%A8.md) - - [深入SpringBoot源码学习之——SpringFactoriesLoader](https://blog.csdn.net/CoderBruis/article/details/106559304) - - [深入SpringBoot源码学习之——系统初始化器](https://blog.csdn.net/CoderBruis/article/details/106610007) - - [深入SpringBoot源码学习之——SpringBoot的事件和监听器](https://blog.csdn.net/CoderBruis/article/details/106896656) +- SpringSecurity&OAuth2源码学习 + - SpringSecurity版本:5.1.0.RELEASE + - [深入浅出SpringSecurity和OAuth2(一)—— 初识SpringSecurity](https://github.com/coderbruis/JavaSourceLearning/blob/master/note/SpringSecurity/%E4%BB%8E%E9%9B%B6%E5%BC%80%E5%A7%8B%E7%B3%BB%E7%BB%9F%E5%AD%A6%E4%B9%A0SpringSecurity%E5%92%8COAuth2%EF%BC%88%E4%B8%80%EF%BC%89%E2%80%94%E2%80%94%20%E5%88%9D%E8%AF%86SpringSecurity.md) + - [深入浅出SpringSecurity和OAuth2(二)—— 安全过滤器FilterChainProxy](https://github.com/coderbruis/JavaSourceLearning/blob/master/note/SpringSecurity/%E4%BB%8E%E9%9B%B6%E5%BC%80%E5%A7%8B%E7%B3%BB%E7%BB%9F%E5%AD%A6%E4%B9%A0SpringSecurity%E5%92%8COAuth2%EF%BC%88%E4%BA%8C%EF%BC%89%E2%80%94%E2%80%94%20%E5%AE%89%E5%85%A8%E8%BF%87%E6%BB%A4%E5%99%A8FilterChainProxy.md) + - [深入浅出SpringSecurity和OAuth2(三)—— WebSecurity建造核心逻辑](https://github.com/coderbruis/JavaSourceCodeLearning/blob/master/note/SpringSecurity/%E4%BB%8E%E9%9B%B6%E5%BC%80%E5%A7%8B%E7%B3%BB%E7%BB%9F%E5%AD%A6%E4%B9%A0SpringSecurity%E5%92%8COAuth2%EF%BC%88%E4%B8%89%EF%BC%89%E2%80%94%E2%80%94%20WebSecurity%E5%BB%BA%E9%80%A0%E6%A0%B8%E5%BF%83%E9%80%BB%E8%BE%91.md) + - [深入浅出SpringSecurity和OAuth2(四)—— FilterChainProxy过滤器链中的几个重要的过滤器](https://github.com/coderbruis/JavaSourceCodeLearning/blob/master/note/SpringSecurity/%E4%BB%8E%E9%9B%B6%E5%BC%80%E5%A7%8B%E7%B3%BB%E7%BB%9F%E5%AD%A6%E4%B9%A0SpringSecurity%E5%92%8COAuth2%EF%BC%88%E5%9B%9B%EF%BC%89%E2%80%94%E2%80%94%20FilterChainProxy%E8%BF%87%E6%BB%A4%E5%99%A8%E9%93%BE%E4%B8%AD%E7%9A%84%E5%87%A0%E4%B8%AA%E9%87%8D%E8%A6%81%E7%9A%84%E8%BF%87%E6%BB%A4%E5%99%A8.md) - -- Netty源码学习 +- Dubbo底层源码解析 + - Dubbo底层源码版本:2.7.8 + - [Dubbo底层源码学习—— 源码搭建](https://github.com/coderbruis/JavaSourceCodeLearning/blob/master/note/Dubbo/Dubbo%E6%BA%90%E7%A0%81%E6%90%AD%E5%BB%BA.md) + - [Dubbo底层源码学习(一)—— Dubbo的URL](https://github.com/coderbruis/JavaSourceCodeLearning/blob/master/note/Dubbo/Dubbo%E5%BA%95%E5%B1%82%E6%BA%90%E7%A0%81%E5%AD%A6%E4%B9%A0%EF%BC%88%E4%B8%80%EF%BC%89%E2%80%94%E2%80%94%20Dubbo%E7%9A%84URL.md) + - [Dubbo底层源码学习(二)—— Dubbo的SPI机制(上)](https://github.com/coderbruis/JavaSourceCodeLearning/blob/master/note/Dubbo/Dubbo%E5%BA%95%E5%B1%82%E6%BA%90%E7%A0%81%E5%AD%A6%E4%B9%A0%EF%BC%88%E4%BA%8C%EF%BC%89%E2%80%94%E2%80%94%20Dubbo%E7%9A%84SPI%E6%9C%BA%E5%88%B6%EF%BC%88%E4%B8%8A%EF%BC%89.md) + - [Dubbo底层源码学习(二)—— Dubbo的SPI机制(中)](https://github.com/coderbruis/JavaSourceCodeLearning/blob/master/note/Dubbo/Dubbo底层源码学习%EF%BC%88二%EF%BC%89——%20Dubbo的SPI机制%EF%BC%88中%EF%BC%89.md +) + - [Dubbo底层源码学习(二)—— Dubbo的SPI机制(下)](https://github.com/coderbruis/JavaSourceCodeLearning/blob/master/note/Dubbo/Dubbo%E5%BA%95%E5%B1%82%E6%BA%90%E7%A0%81%E5%AD%A6%E4%B9%A0%EF%BC%88%E4%BA%8C%EF%BC%89%E2%80%94%E2%80%94%20Dubbo%E7%9A%84SPI%E6%9C%BA%E5%88%B6%EF%BC%88%E4%B8%8B%EF%BC%89.md) + - Dubbo底层源码学习(三)—— Dubbo的注册中心 + - Dubbo底层源码学习(四)—— Dubbo的注册中心缓存机制 + - Dubbo底层源码学习(五)—— Dubbo的注册中心重试机制 + - [Dubbo底层源码学习(六)—— Dubbo的服务暴露](https://github.com/coderbruis/JavaSourceCodeLearning/blob/master/note/Dubbo/Dubbo%E5%BA%95%E5%B1%82%E6%BA%90%E7%A0%81%E5%AD%A6%E4%B9%A0%E2%80%94%E2%80%94%E6%9C%8D%E5%8A%A1%E6%9A%B4%E9%9C%B2.md) + - Dubbo底层源码学习(七)—— Dubbo的服务消费 + +- Netty底层源码解析 + - Netty版本:4.1.43.Final - [二进制运算以及源码、反码以及补码学习](https://github.com/coderbruis/JavaSourceLearning/blob/master/note/Netty/%E4%BA%8C%E8%BF%9B%E5%88%B6.md) - [Netty源码包结构](https://github.com/coderbruis/JavaSourceLearning/blob/master/note/Netty/Netty%E6%BA%90%E7%A0%81%E5%8C%85%E7%BB%93%E6%9E%84.md) - - [Netty中的EventLoopGroup](https://github.com/coderbruis/JavaSourceLearning/blob/master/note/Netty/Netty%E4%B8%AD%E7%9A%84EventLoopGroup%E6%98%AF%E4%BB%80%E4%B9%88.md) + - [Netty底层源码解析-EventLoopGroup](https://github.com/coderbruis/JavaSourceLearning/blob/master/note/Netty/Netty%E4%B8%AD%E7%9A%84EventLoopGroup%E6%98%AF%E4%BB%80%E4%B9%88.md) + - [Netty底层源码解析-初始Netty及其架构](https://github.com/coderbruis/JavaSourceCodeLearning/blob/master/note/Netty/Netty%E5%BA%95%E5%B1%82%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90-%E5%88%9D%E5%A7%8BNetty%E5%8F%8A%E5%85%B6%E6%9E%B6%E6%9E%84.md) + - [Netty底层源码解析-Netty服务端启动分析](https://github.com/coderbruis/JavaSourceCodeLearning/blob/master/note/Netty/Netty%E5%BA%95%E5%B1%82%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90-Netty%E6%9C%8D%E5%8A%A1%E7%AB%AF%E5%90%AF%E5%8A%A8%E5%88%86%E6%9E%90.md) + - [Netty底层源码解析-NioEventLoop原理分析](https://github.com/coderbruis/JavaSourceCodeLearning/blob/master/note/Netty/Netty%E5%BA%95%E5%B1%82%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90-NioEventLoop%E5%8E%9F%E7%90%86%E5%88%86%E6%9E%90.md) + - [Netty底层源码解析-ChannelPipeline分析(上)](https://github.com/coderbruis/JavaSourceCodeLearning/blob/master/note/Netty/Netty%E5%BA%95%E5%B1%82%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90-ChannelPipeline%E5%88%86%E6%9E%90%EF%BC%88%E4%B8%8A%EF%BC%89.md) + - [Netty底层源码解析-ChannelPipeline分析(下)](https://github.com/coderbruis/JavaSourceCodeLearning/blob/master/note/Netty/Netty%E5%BA%95%E5%B1%82%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90-ChannelPipeline%E5%88%86%E6%9E%90%EF%BC%88%E4%B8%8B%EF%BC%89.md) + - [Netty底层源码解析-NioServerSocketChannel接受数据原理分析](https://github.com/coderbruis/JavaSourceCodeLearning/blob/master/note/Netty/Netty%E5%BA%95%E5%B1%82%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90-NioServerSocketChannel%E6%8E%A5%E5%8F%97%E6%95%B0%E6%8D%AE%E5%8E%9F%E7%90%86%E5%88%86%E6%9E%90.md) + - Netty底层源码解析-NioSocketChannel接受、发送数据原理分析 + - Netty底层源码解析-FastThreadLocal原理分析 + - Netty底层源码解析-内存分配原理分析 + - Netty底层源码解析-RocketMQ底层使用到的Netty + - [Netty底层的优化总结]() + - [实战+原理效果更佳!强烈推荐闪电侠大佬实战课:《Netty 入门与实战:仿写微信 IM 即时通讯系统》](https://juejin.cn/book/6844733738119593991) -- SpringSecurity&OAuth2源码学习 - - SpringSecurity版本:5.1.0.RELEASE - - [从零开始系统学习SpringSecurity和OAuth2(一)—— 初识SpringSecurity](https://blog.csdn.net/CoderBruis/article/details/107297547) - - [从零开始系统学习SpringSecurity和OAuth2(二)—— 安全过滤器FilterChainProxy](https://blog.csdn.net/CoderBruis/article/details/107604400) +Netty实战课相关点位于:Spring-Netty,com/bruis/learnnetty/im包下,有需要的读者可前往查看。 + + +- RocketMQ底层源码解析 + - RocketMQ版本:4.9.0 + - RocketMQ底层源码解析-RocketMQ环境搭建 + - RocketMQ底层源码解析-本地调试RocketMQ源码 + - RocketMQ底层源码解析-NameServer分析 + + 持续更新中... + +todo + +2021年年底完成了人生的两件大事,所以一直没时间持续输出源码分析,2022年开始需要继续努力,继续完成这个源码分析项目! + +- 完成Netty剩余源码分析文章 +- 完成RocketMQ剩余源码分析文章 +- 完成Dubbo剩余源码分析文章 +- C语言基础学习(为Redis底层源码学习做准备) +- Redis底层源码分析 +- JUC底层源码分析 # 支持 - 码字不易,各位帅哥美女们star支持一下.... + 原创不易,各位帅哥美女star支持下... diff --git a/Spring-Boot/src/main/java/com/bruis/learnsb/condi/ConditionTest.java b/Spring-Boot/src/main/java/com/bruis/learnsb/condi/ConditionTest.java new file mode 100644 index 0000000..b8ed0a9 --- /dev/null +++ b/Spring-Boot/src/main/java/com/bruis/learnsb/condi/ConditionTest.java @@ -0,0 +1,13 @@ +package com.bruis.learnsb.condi; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.stereotype.Component; + +/** + * @author LuoHaiYang + */ +@Component +// 当存在com.bruis.condition这个配置时才注入ConditionTest这个bean +@ConditionalOnProperty("com.bruis.condition") +public class ConditionTest { +} diff --git a/Spring-Boot/src/main/java/com/bruis/learnsb/startup/FirstApplicationRunner.java b/Spring-Boot/src/main/java/com/bruis/learnsb/startup/FirstApplicationRunner.java index 82b1f87..c01464a 100644 --- a/Spring-Boot/src/main/java/com/bruis/learnsb/startup/FirstApplicationRunner.java +++ b/Spring-Boot/src/main/java/com/bruis/learnsb/startup/FirstApplicationRunner.java @@ -2,11 +2,13 @@ import org.springframework.boot.ApplicationArguments; import org.springframework.boot.ApplicationRunner; +import org.springframework.core.annotation.Order; import org.springframework.stereotype.Component; /** * @author LuoHaiYang */ +@Order(1) @Component public class FirstApplicationRunner implements ApplicationRunner { diff --git a/Spring-Boot/src/main/resources/application.properties b/Spring-Boot/src/main/resources/application.properties index 1b8afa2..664dc88 100644 --- a/Spring-Boot/src/main/resources/application.properties +++ b/Spring-Boot/src/main/resources/application.properties @@ -2,4 +2,5 @@ server.port=8802 context.initializer.classes=com.bruis.learnsb.initializer.SecondInitializer context.listener.classes=com.bruis.learnsb.listener.ThirdListener,com.bruis.learnsb.listener.FourthListener -bruis.github.url=properties-https://github.com/coderbruis/JavaSourceLearning \ No newline at end of file +bruis.github.url=properties-https://github.com/coderbruis/JavaSourceLearning +com.bruis.condition=ConditionTest \ No newline at end of file diff --git a/Spring-Boot/src/test/java/com/bruis/learnsb/ApplicationTests.java b/Spring-Boot/src/test/java/com/bruis/learnsb/ApplicationTests.java index 090ed72..87287b8 100644 --- a/Spring-Boot/src/test/java/com/bruis/learnsb/ApplicationTests.java +++ b/Spring-Boot/src/test/java/com/bruis/learnsb/ApplicationTests.java @@ -1,16 +1,35 @@ package com.bruis.learnsb; +import com.bruis.learnsb.condi.ConditionTest; import org.junit.Test; import org.junit.runner.RunWith; +import org.springframework.beans.BeansException; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; import org.springframework.test.context.junit4.SpringRunner; @RunWith(SpringRunner.class) @SpringBootTest -public class ApplicationTests { +public class ApplicationTests implements ApplicationContextAware { + + private ApplicationContext applicationContext; + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + this.applicationContext = applicationContext; + } @Test public void contextLoads() { } + /** + * 测试当前SpringIOC容器中是否存在ConditionTest这个bean + */ + @Test + public void testConditionTest() { + System.out.println(applicationContext.getBean(ConditionTest.class)); + } + } diff --git a/Spring-Netty/pom.xml b/Spring-Netty/pom.xml index 14ee73a..82d7684 100644 --- a/Spring-Netty/pom.xml +++ b/Spring-Netty/pom.xml @@ -16,7 +16,6 @@ 1.8 - 4.1.6.Final @@ -35,7 +34,12 @@ io.netty netty-all - ${netty-all.version} + + + + com.alibaba + fastjson + 1.2.76 diff --git a/Spring-Netty/src/main/java/com/bruis/learnnetty/bio/Client.java b/Spring-Netty/src/main/java/com/bruis/learnnetty/bio/Client.java index 8e30274..456f285 100644 --- a/Spring-Netty/src/main/java/com/bruis/learnnetty/bio/Client.java +++ b/Spring-Netty/src/main/java/com/bruis/learnnetty/bio/Client.java @@ -20,7 +20,9 @@ public class Client { private static final String HOST = "127.0.0.1"; private static final int PORT = 8000; + // 休眠时间 private static final int SLEEP_TIME = 5000; + // 最大数据大小 public static final int MAX_DATA_LEN = 1024; public static void main(String[] args) throws IOException { @@ -31,10 +33,13 @@ public static void main(String[] args) throws IOException { while (true) { try { Scanner scan = new Scanner(System.in); + // 读取控制台输入信息 String clientMessage = scan.nextLine(); System.out.println("客户端发送数据: " + clientMessage); + // 将数据写入流中 socket.getOutputStream().write(clientMessage.getBytes()); + // 将数据从JVM中读取出来,存放在输入流中 InputStream inputStream = socket.getInputStream(); byte[] data = new byte[MAX_DATA_LEN]; diff --git a/Spring-Netty/src/main/java/com/bruis/learnnetty/im/client/NettyClient.java b/Spring-Netty/src/main/java/com/bruis/learnnetty/im/client/NettyClient.java new file mode 100644 index 0000000..4a701b4 --- /dev/null +++ b/Spring-Netty/src/main/java/com/bruis/learnnetty/im/client/NettyClient.java @@ -0,0 +1,106 @@ +package com.bruis.learnnetty.im.client; + +import com.bruis.learnnetty.im.client.handler.*; +import com.bruis.learnnetty.im.codec.PacketDecoder; +import com.bruis.learnnetty.im.codec.PacketEncoder; +import com.bruis.learnnetty.im.codec.Spliter; +import com.bruis.learnnetty.im.console.ConsoleCommandManager; +import com.bruis.learnnetty.im.console.LoginConsoleCommand; +import com.bruis.learnnetty.im.model.LoginRequestPacket; +import com.bruis.learnnetty.im.model.MessageRequestPacket; +import com.bruis.learnnetty.im.util.SessionUtil; +import io.netty.bootstrap.Bootstrap; +import io.netty.channel.Channel; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelOption; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.SocketChannel; +import io.netty.channel.socket.nio.NioSocketChannel; + +import java.util.Date; +import java.util.Scanner; +import java.util.concurrent.TimeUnit; + +/** + * @Description 客户端 + * @Author luohaiyang + * @Date 2022/3/22 + */ +public class NettyClient { + private static final int MAX_RETRY = 5; + private static final String HOST = "127.0.0.1"; + private static final int PORT = 8000; + + + public static void main(String[] args) { + NioEventLoopGroup workerGroup = new NioEventLoopGroup(); + + Bootstrap bootstrap = new Bootstrap(); + bootstrap + .group(workerGroup) + .channel(NioSocketChannel.class) + .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000) + .option(ChannelOption.SO_KEEPALIVE, true) + .option(ChannelOption.TCP_NODELAY, true) + .handler(new ChannelInitializer() { + @Override + public void initChannel(SocketChannel ch) { + // 拆包粘包处理 + ch.pipeline().addLast(new Spliter()); + // 编码 + ch.pipeline().addLast(new PacketDecoder()); + // 登录响应 + ch.pipeline().addLast(new LoginResponseHandler()); + // 消息返回 + ch.pipeline().addLast(new MessageResponseHandler()); + ch.pipeline().addLast(new CreateGroupResponseHandler()); + ch.pipeline().addLast(new JoinGroupResponseHandler()); + ch.pipeline().addLast(new QuitGroupResponseHandler()); + ch.pipeline().addLast(new ListGroupMembersResponseHandler()); + ch.pipeline().addLast(new GroupMessageResponseHandler()); + ch.pipeline().addLast(new LogoutResponseHandler()); + // 解码 + ch.pipeline().addLast(new PacketEncoder()); + } + }); + + connect(bootstrap, HOST, PORT, MAX_RETRY); + } + + private static void connect(Bootstrap bootstrap, String host, int port, int retry) { + bootstrap.connect(host, port).addListener(future -> { + if (future.isSuccess()) { + System.out.println(new Date() + ": 连接成功,启动控制台线程……"); + Channel channel = ((ChannelFuture) future).channel(); + startConsoleThread(channel); + } else if (retry == 0) { + System.err.println("重试次数已用完,放弃连接!"); + } else { + // 第几次重连 + int order = (MAX_RETRY - retry) + 1; + // 本次重连的间隔 + int delay = 1 << order; + System.err.println(new Date() + ": 连接失败,第" + order + "次重连……"); + bootstrap.config().group().schedule(() -> connect(bootstrap, host, port, retry - 1), delay, TimeUnit + .SECONDS); + } + }); + } + + private static void startConsoleThread(Channel channel) { + ConsoleCommandManager consoleCommandManager = new ConsoleCommandManager(); + LoginConsoleCommand loginConsoleCommand = new LoginConsoleCommand(); + Scanner scanner = new Scanner(System.in); + + new Thread(() -> { + while (!Thread.interrupted()) { + if (!SessionUtil.hasLogin(channel)) { + loginConsoleCommand.exec(scanner, channel); + } else { + consoleCommandManager.exec(scanner, channel); + } + } + }).start(); + } +} diff --git a/Spring-Netty/src/main/java/com/bruis/learnnetty/im/client/handler/CreateGroupResponseHandler.java b/Spring-Netty/src/main/java/com/bruis/learnnetty/im/client/handler/CreateGroupResponseHandler.java new file mode 100644 index 0000000..04125de --- /dev/null +++ b/Spring-Netty/src/main/java/com/bruis/learnnetty/im/client/handler/CreateGroupResponseHandler.java @@ -0,0 +1,19 @@ +package com.bruis.learnnetty.im.client.handler; + +import com.bruis.learnnetty.im.model.CreateGroupResponsePacket; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.SimpleChannelInboundHandler; + +/** + * @Description + * @Author luohaiyang + * @Date 2022/3/23 + */ +public class CreateGroupResponseHandler extends SimpleChannelInboundHandler { + + @Override + protected void channelRead0(ChannelHandlerContext ctx, CreateGroupResponsePacket msg) throws Exception { + System.out.print("群创建成功,id 为[" + msg.getGroupId() + "], "); + System.out.println("群里面有:" + msg.getUserNameList()); + } +} diff --git a/Spring-Netty/src/main/java/com/bruis/learnnetty/im/client/handler/GroupMessageResponseHandler.java b/Spring-Netty/src/main/java/com/bruis/learnnetty/im/client/handler/GroupMessageResponseHandler.java new file mode 100644 index 0000000..3dc9921 --- /dev/null +++ b/Spring-Netty/src/main/java/com/bruis/learnnetty/im/client/handler/GroupMessageResponseHandler.java @@ -0,0 +1,20 @@ +package com.bruis.learnnetty.im.client.handler; + +import com.bruis.learnnetty.im.model.GroupMessageResponsePacket; +import com.bruis.learnnetty.im.session.Session; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.SimpleChannelInboundHandler; + +/** + * @Description + * @Author luohaiyang + * @Date 2022/3/24 + */ +public class GroupMessageResponseHandler extends SimpleChannelInboundHandler { + @Override + protected void channelRead0(ChannelHandlerContext ctx, GroupMessageResponsePacket responsePacket) { + String fromGroupId = responsePacket.getFromGroupId(); + Session fromUser = responsePacket.getFromUser(); + System.out.println("收到群[" + fromGroupId + "]中[" + fromUser + "]发来的消息:" + responsePacket.getMessage()); + } +} \ No newline at end of file diff --git a/Spring-Netty/src/main/java/com/bruis/learnnetty/im/client/handler/JoinGroupResponseHandler.java b/Spring-Netty/src/main/java/com/bruis/learnnetty/im/client/handler/JoinGroupResponseHandler.java new file mode 100644 index 0000000..cc7efea --- /dev/null +++ b/Spring-Netty/src/main/java/com/bruis/learnnetty/im/client/handler/JoinGroupResponseHandler.java @@ -0,0 +1,21 @@ +package com.bruis.learnnetty.im.client.handler; + +import com.bruis.learnnetty.im.model.JoinGroupResponsePacket; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.SimpleChannelInboundHandler; + +/** + * @Description + * @Author luohaiyang + * @Date 2022/3/24 + */ +public class JoinGroupResponseHandler extends SimpleChannelInboundHandler { + @Override + protected void channelRead0(ChannelHandlerContext ctx, JoinGroupResponsePacket responsePacket) throws Exception { + if (responsePacket.isSuccess()) { + System.out.println("加入群[" + responsePacket.getGroupId() + "]成功!"); + } else { + System.err.println("加入群[" + responsePacket.getGroupId() + "]失败,原因为:" + responsePacket.getReason()); + } + } +} diff --git a/Spring-Netty/src/main/java/com/bruis/learnnetty/im/client/handler/ListGroupMembersResponseHandler.java b/Spring-Netty/src/main/java/com/bruis/learnnetty/im/client/handler/ListGroupMembersResponseHandler.java new file mode 100644 index 0000000..0117eac --- /dev/null +++ b/Spring-Netty/src/main/java/com/bruis/learnnetty/im/client/handler/ListGroupMembersResponseHandler.java @@ -0,0 +1,18 @@ +package com.bruis.learnnetty.im.client.handler; + +import com.bruis.learnnetty.im.model.ListGroupMembersResponsePacket; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.SimpleChannelInboundHandler; + +/** + * @Description + * @Author luohaiyang + * @Date 2022/3/24 + */ +public class ListGroupMembersResponseHandler extends SimpleChannelInboundHandler { + + @Override + protected void channelRead0(ChannelHandlerContext ctx, ListGroupMembersResponsePacket responsePacket) { + System.out.println("群[" + responsePacket.getGroupId() + "]中的人包括:" + responsePacket.getSessionList()); + } +} \ No newline at end of file diff --git a/Spring-Netty/src/main/java/com/bruis/learnnetty/im/client/handler/LoginResponseHandler.java b/Spring-Netty/src/main/java/com/bruis/learnnetty/im/client/handler/LoginResponseHandler.java new file mode 100644 index 0000000..282afcc --- /dev/null +++ b/Spring-Netty/src/main/java/com/bruis/learnnetty/im/client/handler/LoginResponseHandler.java @@ -0,0 +1,33 @@ +package com.bruis.learnnetty.im.client.handler; + +import com.bruis.learnnetty.im.model.LoginResponsePacket; +import com.bruis.learnnetty.im.session.Session; +import com.bruis.learnnetty.im.util.SessionUtil; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.SimpleChannelInboundHandler; + +/** + * @Description 登录响应的reponse + * @Author luohaiyang + * @Date 2022/3/23 + */ +public class LoginResponseHandler extends SimpleChannelInboundHandler { + + @Override + protected void channelRead0(ChannelHandlerContext ctx, LoginResponsePacket loginResponsePacket) throws Exception { + String userId = loginResponsePacket.getUserId(); + String userName = loginResponsePacket.getUserName(); + + if (loginResponsePacket.isSuccess()) { + System.out.println("[" + userName + "]登录成功,userId 为: " + loginResponsePacket.getUserId()); + SessionUtil.bindSession(new Session(userId, userName), ctx.channel()); + } else { + System.out.println("[" + userName + "]登录失败,原因:" + loginResponsePacket.getReason()); + } + } + + @Override + public void channelInactive(ChannelHandlerContext ctx) throws Exception { + System.out.println("客户端连接被关闭"); + } +} diff --git a/Spring-Netty/src/main/java/com/bruis/learnnetty/im/client/handler/LogoutResponseHandler.java b/Spring-Netty/src/main/java/com/bruis/learnnetty/im/client/handler/LogoutResponseHandler.java new file mode 100644 index 0000000..78fd173 --- /dev/null +++ b/Spring-Netty/src/main/java/com/bruis/learnnetty/im/client/handler/LogoutResponseHandler.java @@ -0,0 +1,19 @@ +package com.bruis.learnnetty.im.client.handler; + +import com.bruis.learnnetty.im.model.LogoutResponsePacket; +import com.bruis.learnnetty.im.util.SessionUtil; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.SimpleChannelInboundHandler; + +/** + * @Description + * @Author luohaiyang + * @Date 2022/3/23 + */ +public class LogoutResponseHandler extends SimpleChannelInboundHandler { + + @Override + protected void channelRead0(ChannelHandlerContext ctx, LogoutResponsePacket logoutResponsePacket) { + SessionUtil.unBindSession(ctx.channel()); + } +} \ No newline at end of file diff --git a/Spring-Netty/src/main/java/com/bruis/learnnetty/im/client/handler/MessageResponseHandler.java b/Spring-Netty/src/main/java/com/bruis/learnnetty/im/client/handler/MessageResponseHandler.java new file mode 100644 index 0000000..ea6fac1 --- /dev/null +++ b/Spring-Netty/src/main/java/com/bruis/learnnetty/im/client/handler/MessageResponseHandler.java @@ -0,0 +1,19 @@ +package com.bruis.learnnetty.im.client.handler; + +import com.bruis.learnnetty.im.model.MessageResponsePacket; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.SimpleChannelInboundHandler; + +/** + * @Description + * @Author luohaiyang + * @Date 2022/3/23 + */ +public class MessageResponseHandler extends SimpleChannelInboundHandler { + @Override + protected void channelRead0(ChannelHandlerContext channelHandlerContext, MessageResponsePacket messageResponsePacket) throws Exception { + String fromUserId = messageResponsePacket.getFromUserId(); + String fromUserName = messageResponsePacket.getFromUserName(); + System.out.println(fromUserId + ":" + fromUserName + " -> " + messageResponsePacket.getMessage()); + } +} diff --git a/Spring-Netty/src/main/java/com/bruis/learnnetty/im/client/handler/QuitGroupResponseHandler.java b/Spring-Netty/src/main/java/com/bruis/learnnetty/im/client/handler/QuitGroupResponseHandler.java new file mode 100644 index 0000000..be82bd5 --- /dev/null +++ b/Spring-Netty/src/main/java/com/bruis/learnnetty/im/client/handler/QuitGroupResponseHandler.java @@ -0,0 +1,22 @@ +package com.bruis.learnnetty.im.client.handler; + +import com.bruis.learnnetty.im.model.QuitGroupResponsePacket; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.SimpleChannelInboundHandler; + +/** + * @Description + * @Author luohaiyang + * @Date 2022/3/24 + */ +public class QuitGroupResponseHandler extends SimpleChannelInboundHandler { + + @Override + protected void channelRead0(ChannelHandlerContext ctx, QuitGroupResponsePacket responsePacket) throws Exception { + if (responsePacket.isSuccess()) { + System.out.println("退出群聊[" + responsePacket.getGroupId() + "]成功!"); + } else { + System.out.println("退出群聊[" + responsePacket.getGroupId() + "]失败!"); + } + } +} diff --git a/Spring-Netty/src/main/java/com/bruis/learnnetty/im/codec/PacketCodecHandler.java b/Spring-Netty/src/main/java/com/bruis/learnnetty/im/codec/PacketCodecHandler.java new file mode 100644 index 0000000..35aa573 --- /dev/null +++ b/Spring-Netty/src/main/java/com/bruis/learnnetty/im/codec/PacketCodecHandler.java @@ -0,0 +1,35 @@ +package com.bruis.learnnetty.im.codec; + +import com.bruis.learnnetty.im.model.Packet; +import com.bruis.learnnetty.im.model.PacketCodeC; +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.MessageToMessageCodec; + +import java.util.List; + +/** + * @Description + * @Author luohaiyang + * @Date 2022/3/25 + */ +@ChannelHandler.Sharable +public class PacketCodecHandler extends MessageToMessageCodec { + + public static final PacketCodecHandler INSTANCE = new PacketCodecHandler(); + + private PacketCodecHandler() {} + + @Override + protected void encode(ChannelHandlerContext ctx, Packet msg, List out) throws Exception { + ByteBuf byteBuf = ctx.channel().alloc().ioBuffer(); + PacketCodeC.INSTANCE.encode(byteBuf, msg); + out.add(byteBuf); + } + + @Override + protected void decode(ChannelHandlerContext ctx, ByteBuf msg, List out) throws Exception { + out.add(PacketCodeC.INSTANCE.decode(msg)); + } +} diff --git a/Spring-Netty/src/main/java/com/bruis/learnnetty/im/codec/PacketDecoder.java b/Spring-Netty/src/main/java/com/bruis/learnnetty/im/codec/PacketDecoder.java new file mode 100644 index 0000000..751d007 --- /dev/null +++ b/Spring-Netty/src/main/java/com/bruis/learnnetty/im/codec/PacketDecoder.java @@ -0,0 +1,20 @@ +package com.bruis.learnnetty.im.codec; + +import com.bruis.learnnetty.im.model.PacketCodeC; +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.ByteToMessageDecoder; + +import java.util.List; + +/** + * @Description + * @Author luohaiyang + * @Date 2022/3/23 + */ +public class PacketDecoder extends ByteToMessageDecoder { + @Override + protected void decode(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf, List out) throws Exception { + out.add(PacketCodeC.INSTANCE.decode(byteBuf)); + } +} diff --git a/Spring-Netty/src/main/java/com/bruis/learnnetty/im/codec/PacketEncoder.java b/Spring-Netty/src/main/java/com/bruis/learnnetty/im/codec/PacketEncoder.java new file mode 100644 index 0000000..d3d4fa7 --- /dev/null +++ b/Spring-Netty/src/main/java/com/bruis/learnnetty/im/codec/PacketEncoder.java @@ -0,0 +1,19 @@ +package com.bruis.learnnetty.im.codec; + +import com.bruis.learnnetty.im.model.Packet; +import com.bruis.learnnetty.im.model.PacketCodeC; +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.MessageToByteEncoder; + +/** + * @Description + * @Author luohaiyang + * @Date 2022/3/23 + */ +public class PacketEncoder extends MessageToByteEncoder { + @Override + protected void encode(ChannelHandlerContext channelHandlerContext, Packet packet, ByteBuf byteBuf) throws Exception { + PacketCodeC.INSTANCE.encode(byteBuf, packet); + } +} diff --git a/Spring-Netty/src/main/java/com/bruis/learnnetty/im/codec/Spliter.java b/Spring-Netty/src/main/java/com/bruis/learnnetty/im/codec/Spliter.java new file mode 100644 index 0000000..c09b096 --- /dev/null +++ b/Spring-Netty/src/main/java/com/bruis/learnnetty/im/codec/Spliter.java @@ -0,0 +1,30 @@ +package com.bruis.learnnetty.im.codec; + +import com.bruis.learnnetty.im.model.PacketCodeC; +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.LengthFieldBasedFrameDecoder; + +/** + * @Description 拆包、粘包处理 + * @Author luohaiyang + * @Date 2022/3/23 + */ +public class Spliter extends LengthFieldBasedFrameDecoder { + private static final int LENGTH_FIELD_OFFSET = 7; + private static final int LENGTH_FIELD_LENGTH = 4; + + public Spliter() { + super(Integer.MAX_VALUE, LENGTH_FIELD_OFFSET, LENGTH_FIELD_LENGTH); + } + + @Override + protected Object decode(ChannelHandlerContext ctx, ByteBuf in) throws Exception { + // 校验协议 + if (in.getInt(in.readerIndex()) != PacketCodeC.MAGIC_NUMBER) { + ctx.channel().close(); + return null; + } + return super.decode(ctx, in); + } +} \ No newline at end of file diff --git a/Spring-Netty/src/main/java/com/bruis/learnnetty/im/console/ConsoleCommand.java b/Spring-Netty/src/main/java/com/bruis/learnnetty/im/console/ConsoleCommand.java new file mode 100644 index 0000000..dd41e27 --- /dev/null +++ b/Spring-Netty/src/main/java/com/bruis/learnnetty/im/console/ConsoleCommand.java @@ -0,0 +1,14 @@ +package com.bruis.learnnetty.im.console; + +import io.netty.channel.Channel; + +import java.util.Scanner; + +/** + * @Description 指令接口 + * @Author luohaiyang + * @Date 2022/3/23 + */ +public interface ConsoleCommand { + void exec(Scanner scanner, Channel channel); +} diff --git a/Spring-Netty/src/main/java/com/bruis/learnnetty/im/console/ConsoleCommandManager.java b/Spring-Netty/src/main/java/com/bruis/learnnetty/im/console/ConsoleCommandManager.java new file mode 100644 index 0000000..8bf69f6 --- /dev/null +++ b/Spring-Netty/src/main/java/com/bruis/learnnetty/im/console/ConsoleCommandManager.java @@ -0,0 +1,43 @@ +package com.bruis.learnnetty.im.console; + +import com.bruis.learnnetty.im.util.SessionUtil; +import io.netty.channel.Channel; + +import java.util.HashMap; +import java.util.Map; +import java.util.Scanner; + +/** + * @Description + * @Author luohaiyang + * @Date 2022/3/23 + */ +public class ConsoleCommandManager implements ConsoleCommand { + + private Map consoleCommandMap; + + public ConsoleCommandManager() { + consoleCommandMap = new HashMap<>(); + consoleCommandMap.put("sendToUser", new SendToUserConsoleCommand()); + consoleCommandMap.put("logout", new LogoutConsoleCommand()); + consoleCommandMap.put("createGroup", new CreateGroupConsoleCommand()); + consoleCommandMap.put("joinGroup", new JoinGroupConsoleCommand()); + consoleCommandMap.put("quitGroup", new QuitGroupConsoleCommand()); + consoleCommandMap.put("listGroup", new ListGroupMembersConsoleCommand()); + consoleCommandMap.put("sendToGroup", new SendToGroupConsoleCommand()); + } + + @Override + public void exec(Scanner scanner, Channel channel) { + String command = scanner.next(); + if (!SessionUtil.hasLogin(channel)) { + return; + } + ConsoleCommand consoleCommand = consoleCommandMap.get(command); + if (null != consoleCommand) { + consoleCommand.exec(scanner, channel); + } else { + System.err.println("无法识别[" + command + "]指令,请重新输入"); + } + } +} diff --git a/Spring-Netty/src/main/java/com/bruis/learnnetty/im/console/CreateGroupConsoleCommand.java b/Spring-Netty/src/main/java/com/bruis/learnnetty/im/console/CreateGroupConsoleCommand.java new file mode 100644 index 0000000..db68b98 --- /dev/null +++ b/Spring-Netty/src/main/java/com/bruis/learnnetty/im/console/CreateGroupConsoleCommand.java @@ -0,0 +1,28 @@ +package com.bruis.learnnetty.im.console; + +import com.bruis.learnnetty.im.model.CreateGroupRequestPacket; +import io.netty.channel.Channel; + +import java.util.Arrays; +import java.util.Scanner; + +/** + * @Description + * @Author luohaiyang + * @Date 2022/3/23 + */ +public class CreateGroupConsoleCommand implements ConsoleCommand { + + private static final String USER_ID_SPLITER = ","; + + @Override + public void exec(Scanner scanner, Channel channel) { + CreateGroupRequestPacket createGroupRequestPacket = new CreateGroupRequestPacket(); + + System.out.print("【拉人群聊】输入 userId 列表,userId 之间英文逗号隔开:"); + String userIds = scanner.next(); + createGroupRequestPacket.setUserIdList(Arrays.asList(userIds.split(USER_ID_SPLITER))); + channel.writeAndFlush(createGroupRequestPacket); + } + +} \ No newline at end of file diff --git a/Spring-Netty/src/main/java/com/bruis/learnnetty/im/console/JoinGroupConsoleCommand.java b/Spring-Netty/src/main/java/com/bruis/learnnetty/im/console/JoinGroupConsoleCommand.java new file mode 100644 index 0000000..c3ff3f4 --- /dev/null +++ b/Spring-Netty/src/main/java/com/bruis/learnnetty/im/console/JoinGroupConsoleCommand.java @@ -0,0 +1,23 @@ +package com.bruis.learnnetty.im.console; + +import com.bruis.learnnetty.im.model.JoinGroupRequestPacket; +import io.netty.channel.Channel; + +import java.util.Scanner; + +/** + * @Description + * @Author luohaiyang + * @Date 2022/3/24 + */ +public class JoinGroupConsoleCommand implements ConsoleCommand{ + + @Override + public void exec(Scanner scanner, Channel channel) { + JoinGroupRequestPacket requestPacket = new JoinGroupRequestPacket(); + System.out.println("输入groupId, 加入群聊:"); + String groupId = scanner.next(); + requestPacket.setGroupId(groupId); + channel.writeAndFlush(requestPacket); + } +} diff --git a/Spring-Netty/src/main/java/com/bruis/learnnetty/im/console/ListGroupMembersConsoleCommand.java b/Spring-Netty/src/main/java/com/bruis/learnnetty/im/console/ListGroupMembersConsoleCommand.java new file mode 100644 index 0000000..87e79c4 --- /dev/null +++ b/Spring-Netty/src/main/java/com/bruis/learnnetty/im/console/ListGroupMembersConsoleCommand.java @@ -0,0 +1,25 @@ +package com.bruis.learnnetty.im.console; + +import com.bruis.learnnetty.im.model.ListGroupMembersRequestPacket; +import io.netty.channel.Channel; + +import java.util.Scanner; + +/** + * @Description + * @Author luohaiyang + * @Date 2022/3/24 + */ +public class ListGroupMembersConsoleCommand implements ConsoleCommand { + + @Override + public void exec(Scanner scanner, Channel channel) { + ListGroupMembersRequestPacket listGroupMembersRequestPacket = new ListGroupMembersRequestPacket(); + + System.out.print("输入 groupId,获取群成员列表:"); + String groupId = scanner.next(); + + listGroupMembersRequestPacket.setGroupId(groupId); + channel.writeAndFlush(listGroupMembersRequestPacket); + } +} \ No newline at end of file diff --git a/Spring-Netty/src/main/java/com/bruis/learnnetty/im/console/LoginConsoleCommand.java b/Spring-Netty/src/main/java/com/bruis/learnnetty/im/console/LoginConsoleCommand.java new file mode 100644 index 0000000..3e632ca --- /dev/null +++ b/Spring-Netty/src/main/java/com/bruis/learnnetty/im/console/LoginConsoleCommand.java @@ -0,0 +1,38 @@ +package com.bruis.learnnetty.im.console; + +import com.bruis.learnnetty.im.model.LoginRequestPacket; +import io.netty.channel.Channel; + +import java.util.Scanner; + +/** + * @Description + * @Author luohaiyang + * @Date 2022/3/23 + */ +public class LoginConsoleCommand implements ConsoleCommand { + + @Override + public void exec(Scanner scanner, Channel channel) { + LoginRequestPacket loginRequestPacket = new LoginRequestPacket(); + + System.out.print("输入用户名登录: "); + String userIdStr; // 在退出登录logout之后 这里会读取到最后一个回车符 用户名就是空字符串会导致无法退出登录 + while ((userIdStr = scanner.nextLine()).isEmpty()) { + System.out.println("用户名异常, 请重新输入"); + } + loginRequestPacket.setUserName(userIdStr); + loginRequestPacket.setPassword("pwd"); + + // 发送登录数据包 + channel.writeAndFlush(loginRequestPacket); + waitForLoginResponse(); + } + + private static void waitForLoginResponse() { + try { + Thread.sleep(1000); + } catch (InterruptedException ignored) { + } + } +} diff --git a/Spring-Netty/src/main/java/com/bruis/learnnetty/im/console/LogoutConsoleCommand.java b/Spring-Netty/src/main/java/com/bruis/learnnetty/im/console/LogoutConsoleCommand.java new file mode 100644 index 0000000..2d13370 --- /dev/null +++ b/Spring-Netty/src/main/java/com/bruis/learnnetty/im/console/LogoutConsoleCommand.java @@ -0,0 +1,19 @@ +package com.bruis.learnnetty.im.console; + +import com.bruis.learnnetty.im.model.LogoutRequestPacket; +import io.netty.channel.Channel; + +import java.util.Scanner; + +/** + * @Description + * @Author luohaiyang + * @Date 2022/3/23 + */ +public class LogoutConsoleCommand implements ConsoleCommand { + @Override + public void exec(Scanner scanner, Channel channel) { + LogoutRequestPacket logoutRequestPacket = new LogoutRequestPacket(); + channel.writeAndFlush(logoutRequestPacket); + } +} diff --git a/Spring-Netty/src/main/java/com/bruis/learnnetty/im/console/QuitGroupConsoleCommand.java b/Spring-Netty/src/main/java/com/bruis/learnnetty/im/console/QuitGroupConsoleCommand.java new file mode 100644 index 0000000..4b4b284 --- /dev/null +++ b/Spring-Netty/src/main/java/com/bruis/learnnetty/im/console/QuitGroupConsoleCommand.java @@ -0,0 +1,25 @@ +package com.bruis.learnnetty.im.console; + +import com.bruis.learnnetty.im.model.QuitGroupRequestPacket; +import io.netty.channel.Channel; + +import java.util.Scanner; + +/** + * @Description + * @Author luohaiyang + * @Date 2022/3/24 + */ +public class QuitGroupConsoleCommand implements ConsoleCommand { + + @Override + public void exec(Scanner scanner, Channel channel) { + QuitGroupRequestPacket quitGroupRequestPacket = new QuitGroupRequestPacket(); + + System.out.print("输入 groupId,退出群聊:"); + String groupId = scanner.next(); + + quitGroupRequestPacket.setGroupId(groupId); + channel.writeAndFlush(quitGroupRequestPacket); + } +} diff --git a/Spring-Netty/src/main/java/com/bruis/learnnetty/im/console/SendToGroupConsoleCommand.java b/Spring-Netty/src/main/java/com/bruis/learnnetty/im/console/SendToGroupConsoleCommand.java new file mode 100644 index 0000000..90f8f90 --- /dev/null +++ b/Spring-Netty/src/main/java/com/bruis/learnnetty/im/console/SendToGroupConsoleCommand.java @@ -0,0 +1,24 @@ +package com.bruis.learnnetty.im.console; + +import com.bruis.learnnetty.im.model.GroupMessageRequestPacket; +import io.netty.channel.Channel; + +import java.util.Scanner; + +/** + * @Description + * @Author luohaiyang + * @Date 2022/3/24 + */ +public class SendToGroupConsoleCommand implements ConsoleCommand { + + @Override + public void exec(Scanner scanner, Channel channel) { + System.out.print("发送消息给某个某个群组:"); + + String toGroupId = scanner.next(); + String message = scanner.next(); + channel.writeAndFlush(new GroupMessageRequestPacket(toGroupId, message)); + + } +} diff --git a/Spring-Netty/src/main/java/com/bruis/learnnetty/im/console/SendToUserConsoleCommand.java b/Spring-Netty/src/main/java/com/bruis/learnnetty/im/console/SendToUserConsoleCommand.java new file mode 100644 index 0000000..9ead2b5 --- /dev/null +++ b/Spring-Netty/src/main/java/com/bruis/learnnetty/im/console/SendToUserConsoleCommand.java @@ -0,0 +1,23 @@ +package com.bruis.learnnetty.im.console; + +import com.bruis.learnnetty.im.model.MessageRequestPacket; +import io.netty.channel.Channel; + +import java.util.Scanner; + +/** + * @Description + * @Author luohaiyang + * @Date 2022/3/23 + */ +public class SendToUserConsoleCommand implements ConsoleCommand { + + @Override + public void exec(Scanner scanner, Channel channel) { + System.out.print("发送消息给某个某个用户:"); + + String toUserId = scanner.next(); + String message = scanner.next(); + channel.writeAndFlush(new MessageRequestPacket(toUserId, message)); + } +} diff --git a/Spring-Netty/src/main/java/com/bruis/learnnetty/im/model/Attributes.java b/Spring-Netty/src/main/java/com/bruis/learnnetty/im/model/Attributes.java new file mode 100644 index 0000000..fe72853 --- /dev/null +++ b/Spring-Netty/src/main/java/com/bruis/learnnetty/im/model/Attributes.java @@ -0,0 +1,13 @@ +package com.bruis.learnnetty.im.model; + +import com.bruis.learnnetty.im.session.Session; +import io.netty.util.AttributeKey; + +/** + * @Description Netty 属性集 + * @Author haiyangluo + * @Date 2022/3/22 + */ +public interface Attributes { + AttributeKey SESSION = AttributeKey.newInstance("session"); +} diff --git a/Spring-Netty/src/main/java/com/bruis/learnnetty/im/model/Command.java b/Spring-Netty/src/main/java/com/bruis/learnnetty/im/model/Command.java new file mode 100644 index 0000000..f84f29f --- /dev/null +++ b/Spring-Netty/src/main/java/com/bruis/learnnetty/im/model/Command.java @@ -0,0 +1,44 @@ +package com.bruis.learnnetty.im.model; + +/** + * @Description + * @Author haiyangluo + * @Date 2022/3/22 + */ +public interface Command { + Byte LOGIN_REQUEST = 1; + + Byte LOGIN_RESPONSE = 2; + + Byte MESSAGE_REQUEST = 3; + + Byte MESSAGE_RESPONSE = 4; + + Byte LOGOUT_REQUEST = 5; + + Byte LOGOUT_RESPONSE = 6; + + Byte CREATE_GROUP_REQUEST = 7; + + Byte CREATE_GROUP_RESPONSE = 8; + + Byte LIST_GROUP_MEMBERS_REQUEST = 9; + + Byte LIST_GROUP_MEMBERS_RESPONSE = 10; + + Byte JOIN_GROUP_REQUEST = 11; + + Byte JOIN_GROUP_RESPONSE = 12; + + Byte QUIT_GROUP_REQUEST = 13; + + Byte QUIT_GROUP_RESPONSE = 14; + + Byte GROUP_MESSAGE_REQUEST = 15; + + Byte GROUP_MESSAGE_RESPONSE = 16; + + Byte HEARTBEAT_REQUEST = 17; + + Byte HEARTBEAT_RESPONSE = 18; +} diff --git a/Spring-Netty/src/main/java/com/bruis/learnnetty/im/model/CreateGroupRequestPacket.java b/Spring-Netty/src/main/java/com/bruis/learnnetty/im/model/CreateGroupRequestPacket.java new file mode 100644 index 0000000..dc9ffc5 --- /dev/null +++ b/Spring-Netty/src/main/java/com/bruis/learnnetty/im/model/CreateGroupRequestPacket.java @@ -0,0 +1,28 @@ +package com.bruis.learnnetty.im.model; + +import java.util.List; + +import static com.bruis.learnnetty.im.model.Command.CREATE_GROUP_REQUEST; + +/** + * @Description + * @Author luohaiyang + * @Date 2022/3/23 + */ +public class CreateGroupRequestPacket extends Packet { + + private List userIdList; + + @Override + public Byte getCommand() { + return CREATE_GROUP_REQUEST; + } + + public List getUserIdList() { + return userIdList; + } + + public void setUserIdList(List userIdList) { + this.userIdList = userIdList; + } +} diff --git a/Spring-Netty/src/main/java/com/bruis/learnnetty/im/model/CreateGroupResponsePacket.java b/Spring-Netty/src/main/java/com/bruis/learnnetty/im/model/CreateGroupResponsePacket.java new file mode 100644 index 0000000..6209205 --- /dev/null +++ b/Spring-Netty/src/main/java/com/bruis/learnnetty/im/model/CreateGroupResponsePacket.java @@ -0,0 +1,48 @@ +package com.bruis.learnnetty.im.model; + +import java.util.List; + +import static com.bruis.learnnetty.im.model.Command.CREATE_GROUP_RESPONSE; + +/** + * @Description + * @Author luohaiyang + * @Date 2022/3/23 + */ +public class CreateGroupResponsePacket extends Packet { + private boolean success; + + private String groupId; + + private List userNameList; + + @Override + public Byte getCommand() { + + return CREATE_GROUP_RESPONSE; + } + + public boolean isSuccess() { + return success; + } + + public void setSuccess(boolean success) { + this.success = success; + } + + public String getGroupId() { + return groupId; + } + + public void setGroupId(String groupId) { + this.groupId = groupId; + } + + public List getUserNameList() { + return userNameList; + } + + public void setUserNameList(List userNameList) { + this.userNameList = userNameList; + } +} \ No newline at end of file diff --git a/Spring-Netty/src/main/java/com/bruis/learnnetty/im/model/GroupMessageRequestPacket.java b/Spring-Netty/src/main/java/com/bruis/learnnetty/im/model/GroupMessageRequestPacket.java new file mode 100644 index 0000000..3a6f812 --- /dev/null +++ b/Spring-Netty/src/main/java/com/bruis/learnnetty/im/model/GroupMessageRequestPacket.java @@ -0,0 +1,39 @@ +package com.bruis.learnnetty.im.model; + +import static com.bruis.learnnetty.im.model.Command.GROUP_MESSAGE_REQUEST; + +/** + * @Description + * @Author luohaiyang + * @Date 2022/3/24 + */ +public class GroupMessageRequestPacket extends Packet { + private String toGroupId; + private String message; + + public GroupMessageRequestPacket(String toGroupId, String message) { + this.toGroupId = toGroupId; + this.message = message; + } + + @Override + public Byte getCommand() { + return GROUP_MESSAGE_REQUEST; + } + + public String getToGroupId() { + return toGroupId; + } + + public void setToGroupId(String toGroupId) { + this.toGroupId = toGroupId; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } +} diff --git a/Spring-Netty/src/main/java/com/bruis/learnnetty/im/model/GroupMessageResponsePacket.java b/Spring-Netty/src/main/java/com/bruis/learnnetty/im/model/GroupMessageResponsePacket.java new file mode 100644 index 0000000..986333b --- /dev/null +++ b/Spring-Netty/src/main/java/com/bruis/learnnetty/im/model/GroupMessageResponsePacket.java @@ -0,0 +1,49 @@ +package com.bruis.learnnetty.im.model; + +import com.bruis.learnnetty.im.session.Session; + +import static com.bruis.learnnetty.im.model.Command.GROUP_MESSAGE_RESPONSE; + +/** + * @Description + * @Author luohaiyang + * @Date 2022/3/24 + */ +public class GroupMessageResponsePacket extends Packet { + + private String fromGroupId; + + private Session fromUser; + + private String message; + + @Override + public Byte getCommand() { + + return GROUP_MESSAGE_RESPONSE; + } + + public String getFromGroupId() { + return fromGroupId; + } + + public void setFromGroupId(String fromGroupId) { + this.fromGroupId = fromGroupId; + } + + public Session getFromUser() { + return fromUser; + } + + public void setFromUser(Session fromUser) { + this.fromUser = fromUser; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } +} \ No newline at end of file diff --git a/Spring-Netty/src/main/java/com/bruis/learnnetty/im/model/JoinGroupRequestPacket.java b/Spring-Netty/src/main/java/com/bruis/learnnetty/im/model/JoinGroupRequestPacket.java new file mode 100644 index 0000000..26fb73d --- /dev/null +++ b/Spring-Netty/src/main/java/com/bruis/learnnetty/im/model/JoinGroupRequestPacket.java @@ -0,0 +1,26 @@ +package com.bruis.learnnetty.im.model; + +import static com.bruis.learnnetty.im.model.Command.JOIN_GROUP_REQUEST; + +/** + * @Description + * @Author luohaiyang + * @Date 2022/3/24 + */ +public class JoinGroupRequestPacket extends Packet { + + private String groupId; + + @Override + public Byte getCommand() { + return JOIN_GROUP_REQUEST; + } + + public String getGroupId() { + return groupId; + } + + public void setGroupId(String groupId) { + this.groupId = groupId; + } +} diff --git a/Spring-Netty/src/main/java/com/bruis/learnnetty/im/model/JoinGroupResponsePacket.java b/Spring-Netty/src/main/java/com/bruis/learnnetty/im/model/JoinGroupResponsePacket.java new file mode 100644 index 0000000..dce9a1d --- /dev/null +++ b/Spring-Netty/src/main/java/com/bruis/learnnetty/im/model/JoinGroupResponsePacket.java @@ -0,0 +1,46 @@ +package com.bruis.learnnetty.im.model; + +import static com.bruis.learnnetty.im.model.Command.JOIN_GROUP_RESPONSE; + +/** + * @Description + * @Author luohaiyang + * @Date 2022/3/24 + */ +public class JoinGroupResponsePacket extends Packet { + + private String groupId; + + private boolean success; + + private String reason; + + @Override + public Byte getCommand() { + return JOIN_GROUP_RESPONSE; + } + + public String getGroupId() { + return groupId; + } + + public void setGroupId(String groupId) { + this.groupId = groupId; + } + + public boolean isSuccess() { + return success; + } + + public void setSuccess(boolean success) { + this.success = success; + } + + public String getReason() { + return reason; + } + + public void setReason(String reason) { + this.reason = reason; + } +} diff --git a/Spring-Netty/src/main/java/com/bruis/learnnetty/im/model/ListGroupMembersRequestPacket.java b/Spring-Netty/src/main/java/com/bruis/learnnetty/im/model/ListGroupMembersRequestPacket.java new file mode 100644 index 0000000..886b19c --- /dev/null +++ b/Spring-Netty/src/main/java/com/bruis/learnnetty/im/model/ListGroupMembersRequestPacket.java @@ -0,0 +1,27 @@ +package com.bruis.learnnetty.im.model; + +import static com.bruis.learnnetty.im.model.Command.LIST_GROUP_MEMBERS_REQUEST; + +/** + * @Description + * @Author luohaiyang + * @Date 2022/3/24 + */ +public class ListGroupMembersRequestPacket extends Packet { + + private String groupId; + + @Override + public Byte getCommand() { + + return LIST_GROUP_MEMBERS_REQUEST; + } + + public String getGroupId() { + return groupId; + } + + public void setGroupId(String groupId) { + this.groupId = groupId; + } +} \ No newline at end of file diff --git a/Spring-Netty/src/main/java/com/bruis/learnnetty/im/model/ListGroupMembersResponsePacket.java b/Spring-Netty/src/main/java/com/bruis/learnnetty/im/model/ListGroupMembersResponsePacket.java new file mode 100644 index 0000000..dbc174e --- /dev/null +++ b/Spring-Netty/src/main/java/com/bruis/learnnetty/im/model/ListGroupMembersResponsePacket.java @@ -0,0 +1,41 @@ +package com.bruis.learnnetty.im.model; + +import com.bruis.learnnetty.im.session.Session; + +import java.util.List; + +import static com.bruis.learnnetty.im.model.Command.LIST_GROUP_MEMBERS_RESPONSE; + +/** + * @Description + * @Author luohaiyang + * @Date 2022/3/24 + */ +public class ListGroupMembersResponsePacket extends Packet { + + private String groupId; + + private List sessionList; + + @Override + public Byte getCommand() { + + return LIST_GROUP_MEMBERS_RESPONSE; + } + + public String getGroupId() { + return groupId; + } + + public void setGroupId(String groupId) { + this.groupId = groupId; + } + + public List getSessionList() { + return sessionList; + } + + public void setSessionList(List sessionList) { + this.sessionList = sessionList; + } +} \ No newline at end of file diff --git a/Spring-Netty/src/main/java/com/bruis/learnnetty/im/model/LoginRequestPacket.java b/Spring-Netty/src/main/java/com/bruis/learnnetty/im/model/LoginRequestPacket.java new file mode 100644 index 0000000..d1122bd --- /dev/null +++ b/Spring-Netty/src/main/java/com/bruis/learnnetty/im/model/LoginRequestPacket.java @@ -0,0 +1,46 @@ +package com.bruis.learnnetty.im.model; + +import static com.bruis.learnnetty.im.model.Command.LOGIN_REQUEST; + +/** + * @Description + * @Author luohaiyang + * @Date 2022/3/22 + */ +public class LoginRequestPacket extends Packet { + + private String userId; + + private String userName; + + private String password; + + @Override + public Byte getCommand() { + return LOGIN_REQUEST; + } + + public String getUserName() { + return userName; + } + + public void setUserName(String userName) { + this.userName = userName; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public String getUserId() { + return userId; + } + + public void setUserId(String userId) { + this.userId = userId; + } +} diff --git a/Spring-Netty/src/main/java/com/bruis/learnnetty/im/model/LoginResponsePacket.java b/Spring-Netty/src/main/java/com/bruis/learnnetty/im/model/LoginResponsePacket.java new file mode 100644 index 0000000..32599fd --- /dev/null +++ b/Spring-Netty/src/main/java/com/bruis/learnnetty/im/model/LoginResponsePacket.java @@ -0,0 +1,57 @@ +package com.bruis.learnnetty.im.model; + +import static com.bruis.learnnetty.im.model.Command.LOGIN_RESPONSE; + +/** + * @Description + * @Author luohaiyang + * @Date 2022/3/22 + */ +public class LoginResponsePacket extends Packet { + + private String userId; + + private String userName; + + private boolean success; + + private String reason; + + + @Override + public Byte getCommand() { + return LOGIN_RESPONSE; + } + + public boolean isSuccess() { + return success; + } + + public void setSuccess(boolean success) { + this.success = success; + } + + public String getReason() { + return reason; + } + + public void setReason(String reason) { + this.reason = reason; + } + + public String getUserId() { + return userId; + } + + public void setUserId(String userId) { + this.userId = userId; + } + + public String getUserName() { + return userName; + } + + public void setUserName(String userName) { + this.userName = userName; + } +} diff --git a/Spring-Netty/src/main/java/com/bruis/learnnetty/im/model/LogoutRequestPacket.java b/Spring-Netty/src/main/java/com/bruis/learnnetty/im/model/LogoutRequestPacket.java new file mode 100644 index 0000000..c66dc68 --- /dev/null +++ b/Spring-Netty/src/main/java/com/bruis/learnnetty/im/model/LogoutRequestPacket.java @@ -0,0 +1,16 @@ +package com.bruis.learnnetty.im.model; + +import static com.bruis.learnnetty.im.model.Command.LOGOUT_REQUEST; + +/** + * @Description + * @Author luohaiyang + * @Date 2022/3/23 + */ +public class LogoutRequestPacket extends Packet { + @Override + public Byte getCommand() { + + return LOGOUT_REQUEST; + } +} diff --git a/Spring-Netty/src/main/java/com/bruis/learnnetty/im/model/LogoutResponsePacket.java b/Spring-Netty/src/main/java/com/bruis/learnnetty/im/model/LogoutResponsePacket.java new file mode 100644 index 0000000..73d2a71 --- /dev/null +++ b/Spring-Netty/src/main/java/com/bruis/learnnetty/im/model/LogoutResponsePacket.java @@ -0,0 +1,37 @@ +package com.bruis.learnnetty.im.model; + +import static com.bruis.learnnetty.im.model.Command.LOGOUT_RESPONSE; + +/** + * @Description + * @Author luohaiyang + * @Date 2022/3/24 + */ +public class LogoutResponsePacket extends Packet { + + private boolean success; + + private String reason; + + + @Override + public Byte getCommand() { + return LOGOUT_RESPONSE; + } + + public boolean isSuccess() { + return success; + } + + public void setSuccess(boolean success) { + this.success = success; + } + + public String getReason() { + return reason; + } + + public void setReason(String reason) { + this.reason = reason; + } +} \ No newline at end of file diff --git a/Spring-Netty/src/main/java/com/bruis/learnnetty/im/model/MessageRequestPacket.java b/Spring-Netty/src/main/java/com/bruis/learnnetty/im/model/MessageRequestPacket.java new file mode 100644 index 0000000..4dae50c --- /dev/null +++ b/Spring-Netty/src/main/java/com/bruis/learnnetty/im/model/MessageRequestPacket.java @@ -0,0 +1,43 @@ +package com.bruis.learnnetty.im.model; + +import static com.bruis.learnnetty.im.model.Command.MESSAGE_REQUEST; + +/** + * @Description + * @Author luohaiyang + * @Date 2022/3/22 + */ +public class MessageRequestPacket extends Packet { + + private String toUserId; + + private String message; + + public MessageRequestPacket(){} + + public MessageRequestPacket(String toUserId, String message) { + this.toUserId = toUserId; + this.message = message; + } + + @Override + public Byte getCommand() { + return MESSAGE_REQUEST; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + public String getToUserId() { + return toUserId; + } + + public void setToUserId(String toUserId) { + this.toUserId = toUserId; + } +} diff --git a/Spring-Netty/src/main/java/com/bruis/learnnetty/im/model/MessageResponsePacket.java b/Spring-Netty/src/main/java/com/bruis/learnnetty/im/model/MessageResponsePacket.java new file mode 100644 index 0000000..372a33b --- /dev/null +++ b/Spring-Netty/src/main/java/com/bruis/learnnetty/im/model/MessageResponsePacket.java @@ -0,0 +1,47 @@ +package com.bruis.learnnetty.im.model; + +import static com.bruis.learnnetty.im.model.Command.MESSAGE_RESPONSE; + +/** + * @Description + * @Author luohaiyang + * @Date 2022/3/22 + */ +public class MessageResponsePacket extends Packet { + + private String fromUserId; + + private String fromUserName; + + private String message; + + @Override + public Byte getCommand() { + + return MESSAGE_RESPONSE; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + public String getFromUserId() { + return fromUserId; + } + + public void setFromUserId(String fromUserId) { + this.fromUserId = fromUserId; + } + + public String getFromUserName() { + return fromUserName; + } + + public void setFromUserName(String fromUserName) { + this.fromUserName = fromUserName; + } +} \ No newline at end of file diff --git a/Spring-Netty/src/main/java/com/bruis/learnnetty/im/model/Packet.java b/Spring-Netty/src/main/java/com/bruis/learnnetty/im/model/Packet.java new file mode 100644 index 0000000..0100f52 --- /dev/null +++ b/Spring-Netty/src/main/java/com/bruis/learnnetty/im/model/Packet.java @@ -0,0 +1,27 @@ +package com.bruis.learnnetty.im.model; + +import com.alibaba.fastjson.annotation.JSONField; + +/** + * @Description + * @Author luohaiyang + * @Date 2022/3/22 + */ +public abstract class Packet { + /** + * 协议版本 + */ + @JSONField(deserialize = false , serialize = false) + private Byte version = 1; + + @JSONField(serialize = false) + public abstract Byte getCommand(); + + public Byte getVersion() { + return version; + } + + public void setVersion(Byte version) { + this.version = version; + } +} diff --git a/Spring-Netty/src/main/java/com/bruis/learnnetty/im/model/PacketCodeC.java b/Spring-Netty/src/main/java/com/bruis/learnnetty/im/model/PacketCodeC.java new file mode 100644 index 0000000..7a81483 --- /dev/null +++ b/Spring-Netty/src/main/java/com/bruis/learnnetty/im/model/PacketCodeC.java @@ -0,0 +1,102 @@ +package com.bruis.learnnetty.im.model; + +import com.bruis.learnnetty.im.serialize.Serializer; +import com.bruis.learnnetty.im.serialize.impl.JSONSerializer; +import io.netty.buffer.ByteBuf; + +import java.util.HashMap; +import java.util.Map; + +import static com.bruis.learnnetty.im.model.Command.*; + +/** + * @Description + * @Author luohaiyang + * @Date 2022/3/22 + */ +public class PacketCodeC { + public static final int MAGIC_NUMBER = 0x12345678; + public static final PacketCodeC INSTANCE = new PacketCodeC(); + + private final Map> packetTypeMap; + private final Map serializerMap; + + + private PacketCodeC() { + packetTypeMap = new HashMap<>(); + packetTypeMap.put(LOGIN_REQUEST, LoginRequestPacket.class); + packetTypeMap.put(LOGIN_RESPONSE, LoginResponsePacket.class); + packetTypeMap.put(MESSAGE_REQUEST, MessageRequestPacket.class); + packetTypeMap.put(MESSAGE_RESPONSE, MessageResponsePacket.class); + packetTypeMap.put(LOGOUT_REQUEST, LogoutRequestPacket.class); + packetTypeMap.put(LOGOUT_RESPONSE, LogoutResponsePacket.class); + packetTypeMap.put(CREATE_GROUP_REQUEST, CreateGroupRequestPacket.class); + packetTypeMap.put(CREATE_GROUP_RESPONSE, CreateGroupResponsePacket.class); + packetTypeMap.put(JOIN_GROUP_REQUEST, JoinGroupRequestPacket.class); + packetTypeMap.put(JOIN_GROUP_RESPONSE, JoinGroupResponsePacket.class); + packetTypeMap.put(QUIT_GROUP_REQUEST, QuitGroupRequestPacket.class); + packetTypeMap.put(QUIT_GROUP_RESPONSE, QuitGroupResponsePacket.class); + packetTypeMap.put(LIST_GROUP_MEMBERS_REQUEST, ListGroupMembersRequestPacket.class); + packetTypeMap.put(LIST_GROUP_MEMBERS_RESPONSE, ListGroupMembersResponsePacket.class); + packetTypeMap.put(GROUP_MESSAGE_REQUEST, GroupMessageRequestPacket.class); + packetTypeMap.put(GROUP_MESSAGE_RESPONSE, GroupMessageResponsePacket.class); + + serializerMap = new HashMap<>(); + Serializer serializer = new JSONSerializer(); + serializerMap.put(serializer.getSerializerAlogrithm(), serializer); + } + + + public void encode(ByteBuf byteBuf, Packet packet) { + // 1. 序列化 java 对象 + byte[] bytes = Serializer.DEFAULT.serialize(packet); + + // 2. 实际编码过程 + byteBuf.writeInt(MAGIC_NUMBER); + byteBuf.writeByte(packet.getVersion()); + byteBuf.writeByte(Serializer.DEFAULT.getSerializerAlogrithm()); + byteBuf.writeByte(packet.getCommand()); + byteBuf.writeInt(bytes.length); + byteBuf.writeBytes(bytes); + } + + + public Packet decode(ByteBuf byteBuf) { + // 跳过 magic number + byteBuf.skipBytes(4); + + // 跳过版本号 + byteBuf.skipBytes(1); + + // 序列化算法 + byte serializeAlgorithm = byteBuf.readByte(); + + // 指令 + byte command = byteBuf.readByte(); + + // 数据包长度 + int length = byteBuf.readInt(); + + byte[] bytes = new byte[length]; + byteBuf.readBytes(bytes); + + Class requestType = getRequestType(command); + Serializer serializer = getSerializer(serializeAlgorithm); + + if (requestType != null && serializer != null) { + return serializer.deserialize(requestType, bytes); + } + + return null; + } + + private Serializer getSerializer(byte serializeAlgorithm) { + + return serializerMap.get(serializeAlgorithm); + } + + private Class getRequestType(byte command) { + + return packetTypeMap.get(command); + } +} diff --git a/Spring-Netty/src/main/java/com/bruis/learnnetty/im/model/QuitGroupRequestPacket.java b/Spring-Netty/src/main/java/com/bruis/learnnetty/im/model/QuitGroupRequestPacket.java new file mode 100644 index 0000000..ca5342f --- /dev/null +++ b/Spring-Netty/src/main/java/com/bruis/learnnetty/im/model/QuitGroupRequestPacket.java @@ -0,0 +1,26 @@ +package com.bruis.learnnetty.im.model; + +import static com.bruis.learnnetty.im.model.Command.QUIT_GROUP_REQUEST; + +/** + * @Description + * @Author luohaiyang + * @Date 2022/3/24 + */ +public class QuitGroupRequestPacket extends Packet { + + private String groupId; + + @Override + public Byte getCommand() { + return QUIT_GROUP_REQUEST; + } + + public String getGroupId() { + return groupId; + } + + public void setGroupId(String groupId) { + this.groupId = groupId; + } +} diff --git a/Spring-Netty/src/main/java/com/bruis/learnnetty/im/model/QuitGroupResponsePacket.java b/Spring-Netty/src/main/java/com/bruis/learnnetty/im/model/QuitGroupResponsePacket.java new file mode 100644 index 0000000..99529c7 --- /dev/null +++ b/Spring-Netty/src/main/java/com/bruis/learnnetty/im/model/QuitGroupResponsePacket.java @@ -0,0 +1,47 @@ +package com.bruis.learnnetty.im.model; + +import static com.bruis.learnnetty.im.model.Command.QUIT_GROUP_REQUEST; +import static com.bruis.learnnetty.im.model.Command.QUIT_GROUP_RESPONSE; + +/** + * @Description + * @Author luohaiyang + * @Date 2022/3/24 + */ +public class QuitGroupResponsePacket extends Packet { + + private String groupId; + + private boolean success; + + private String reason; + + @Override + public Byte getCommand() { + return QUIT_GROUP_RESPONSE; + } + + public String getGroupId() { + return groupId; + } + + public void setGroupId(String groupId) { + this.groupId = groupId; + } + + public boolean isSuccess() { + return success; + } + + public void setSuccess(boolean success) { + this.success = success; + } + + public String getReason() { + return reason; + } + + public void setReason(String reason) { + this.reason = reason; + } +} diff --git a/Spring-Netty/src/main/java/com/bruis/learnnetty/im/serialize/Serializer.java b/Spring-Netty/src/main/java/com/bruis/learnnetty/im/serialize/Serializer.java new file mode 100644 index 0000000..1bce9c9 --- /dev/null +++ b/Spring-Netty/src/main/java/com/bruis/learnnetty/im/serialize/Serializer.java @@ -0,0 +1,29 @@ +package com.bruis.learnnetty.im.serialize; + + +import com.bruis.learnnetty.im.serialize.impl.JSONSerializer; + +/** + * @Description + * @Author haiyangluo + * @Date 2022/3/22 + */ +public interface Serializer { + Serializer DEFAULT = new JSONSerializer(); + + /** + * 序列化算法 + * @return + */ + byte getSerializerAlogrithm(); + + /** + * java 对象转换成二进制 + */ + byte[] serialize(Object object); + + /** + * 二进制转换成 java 对象 + */ + T deserialize(Class clazz, byte[] bytes); +} diff --git a/Spring-Netty/src/main/java/com/bruis/learnnetty/im/serialize/SerializerAlogrithm.java b/Spring-Netty/src/main/java/com/bruis/learnnetty/im/serialize/SerializerAlogrithm.java new file mode 100644 index 0000000..ef50887 --- /dev/null +++ b/Spring-Netty/src/main/java/com/bruis/learnnetty/im/serialize/SerializerAlogrithm.java @@ -0,0 +1,13 @@ +package com.bruis.learnnetty.im.serialize; + +/** + * @Description + * @Author haiyangluo + * @Date 2022/3/22 + */ +public interface SerializerAlogrithm { + /** + * json 序列化 + */ + byte JSON = 1; +} diff --git a/Spring-Netty/src/main/java/com/bruis/learnnetty/im/serialize/impl/JSONSerializer.java b/Spring-Netty/src/main/java/com/bruis/learnnetty/im/serialize/impl/JSONSerializer.java new file mode 100644 index 0000000..9a2881f --- /dev/null +++ b/Spring-Netty/src/main/java/com/bruis/learnnetty/im/serialize/impl/JSONSerializer.java @@ -0,0 +1,29 @@ +package com.bruis.learnnetty.im.serialize.impl; + +import com.alibaba.fastjson.JSON; +import com.bruis.learnnetty.im.serialize.Serializer; +import com.bruis.learnnetty.im.serialize.SerializerAlogrithm; + +/** + * @Description + * @Author luohaiyang + * @Date 2022/3/22 + */ +public class JSONSerializer implements Serializer { + @Override + public byte getSerializerAlogrithm() { + return SerializerAlogrithm.JSON; + } + + @Override + public byte[] serialize(Object object) { + + return JSON.toJSONBytes(object); + } + + @Override + public T deserialize(Class clazz, byte[] bytes) { + + return JSON.parseObject(bytes, clazz); + } +} diff --git a/Spring-Netty/src/main/java/com/bruis/learnnetty/im/server/NettyServer.java b/Spring-Netty/src/main/java/com/bruis/learnnetty/im/server/NettyServer.java new file mode 100644 index 0000000..08d3f14 --- /dev/null +++ b/Spring-Netty/src/main/java/com/bruis/learnnetty/im/server/NettyServer.java @@ -0,0 +1,58 @@ +package com.bruis.learnnetty.im.server; + +import com.bruis.learnnetty.im.codec.PacketCodecHandler; +import com.bruis.learnnetty.im.codec.PacketDecoder; +import com.bruis.learnnetty.im.codec.PacketEncoder; +import com.bruis.learnnetty.im.codec.Spliter; +import com.bruis.learnnetty.im.server.handler.*; +import io.netty.bootstrap.ServerBootstrap; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelOption; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.nio.NioServerSocketChannel; +import io.netty.channel.socket.nio.NioSocketChannel; + +import java.util.Date; + +/** + * @Description + * @Author luohaiyang + * @Date 2022/3/22 + */ +public class NettyServer { + + private static final int PORT = 8000; + + public static void main(String[] args) { + NioEventLoopGroup bossGroup = new NioEventLoopGroup(); + NioEventLoopGroup workerGroup = new NioEventLoopGroup(); + + ServerBootstrap serverBootstrap = new ServerBootstrap(); + serverBootstrap.group(bossGroup, workerGroup) + .channel(NioServerSocketChannel.class) + .option(ChannelOption.SO_BACKLOG, 1024) + .childOption(ChannelOption.SO_KEEPALIVE, true) + .childOption(ChannelOption.TCP_NODELAY, true) + .childHandler(new ChannelInitializer() { + @Override + protected void initChannel(NioSocketChannel ch) throws Exception { + ch.pipeline().addLast(new Spliter()); + ch.pipeline().addLast(PacketCodecHandler.INSTANCE); + ch.pipeline().addLast(LoginRequestHandler.INSTANCE); + ch.pipeline().addLast(AuthHandler.INSTANCE); + ch.pipeline().addLast(IMHandler.INSTANCE); + } + }); + bind(serverBootstrap, PORT); + } + + private static void bind(final ServerBootstrap serverBootstrap, final int port) { + serverBootstrap.bind(port).addListener(future -> { + if (future.isSuccess()) { + System.out.println(new Date() + ": 端口[" + port + "]绑定成功!"); + } else { + System.err.println("端口[" + port + "]绑定失败!"); + } + }); + } +} diff --git a/Spring-Netty/src/main/java/com/bruis/learnnetty/im/server/handler/AuthHandler.java b/Spring-Netty/src/main/java/com/bruis/learnnetty/im/server/handler/AuthHandler.java new file mode 100644 index 0000000..ad5c784 --- /dev/null +++ b/Spring-Netty/src/main/java/com/bruis/learnnetty/im/server/handler/AuthHandler.java @@ -0,0 +1,29 @@ +package com.bruis.learnnetty.im.server.handler; + +import com.bruis.learnnetty.im.util.SessionUtil; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; + +/** + * @Description + * @Author luohaiyang + * @Date 2022/3/23 + */ +@ChannelHandler.Sharable +public class AuthHandler extends ChannelInboundHandlerAdapter { + + public static final AuthHandler INSTANCE = new AuthHandler(); + + protected AuthHandler() {} + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + if (!SessionUtil.hasLogin(ctx.channel())) { + ctx.channel().close(); + } else { + ctx.pipeline().remove(this); + super.channelRead(ctx, msg); + } + } +} diff --git a/Spring-Netty/src/main/java/com/bruis/learnnetty/im/server/handler/CreateGroupRequestHandler.java b/Spring-Netty/src/main/java/com/bruis/learnnetty/im/server/handler/CreateGroupRequestHandler.java new file mode 100644 index 0000000..64c57cf --- /dev/null +++ b/Spring-Netty/src/main/java/com/bruis/learnnetty/im/server/handler/CreateGroupRequestHandler.java @@ -0,0 +1,58 @@ +package com.bruis.learnnetty.im.server.handler; + +import com.bruis.learnnetty.im.model.CreateGroupRequestPacket; +import com.bruis.learnnetty.im.model.CreateGroupResponsePacket; +import com.bruis.learnnetty.im.util.IDUtil; +import com.bruis.learnnetty.im.util.SessionUtil; +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.SimpleChannelInboundHandler; +import io.netty.channel.group.ChannelGroup; +import io.netty.channel.group.DefaultChannelGroup; +import org.omg.PortableServer.ID_UNIQUENESS_POLICY_ID; + +import java.util.ArrayList; +import java.util.List; + +/** + * @Description + * @Author luohaiyang + * @Date 2022/3/24 + */ +@ChannelHandler.Sharable +public class CreateGroupRequestHandler extends SimpleChannelInboundHandler { + + public static final CreateGroupRequestHandler INSTANCE = new CreateGroupRequestHandler(); + + protected CreateGroupRequestHandler() {} + + @Override + protected void channelRead0(ChannelHandlerContext ctx, CreateGroupRequestPacket msg) throws Exception { + List userIdList = msg.getUserIdList(); + List userNameList = new ArrayList<>(); + + ChannelGroup channelGroup = new DefaultChannelGroup(ctx.executor()); + + for (String userId : userIdList) { + Channel channel = SessionUtil.getChannel(userId); + if (null != channel) { + channelGroup.add(channel); + userNameList.add(SessionUtil.getSession(channel).getUserName()); + } + } + + String groupId = IDUtil.randomUserId(); + CreateGroupResponsePacket createGroupResponsePacket = new CreateGroupResponsePacket(); + createGroupResponsePacket.setSuccess(true); + createGroupResponsePacket.setGroupId(groupId); + createGroupResponsePacket.setUserNameList(userNameList); + + channelGroup.writeAndFlush(createGroupResponsePacket); + + System.out.print("群创建成功,id 为[" + createGroupResponsePacket.getGroupId() + "], "); + System.out.println("群里面有:" + createGroupResponsePacket.getUserNameList()); + + SessionUtil.bindChannelGroup(groupId, channelGroup); + } +} diff --git a/Spring-Netty/src/main/java/com/bruis/learnnetty/im/server/handler/GroupMessageRequestHandler.java b/Spring-Netty/src/main/java/com/bruis/learnnetty/im/server/handler/GroupMessageRequestHandler.java new file mode 100644 index 0000000..5507f45 --- /dev/null +++ b/Spring-Netty/src/main/java/com/bruis/learnnetty/im/server/handler/GroupMessageRequestHandler.java @@ -0,0 +1,34 @@ +package com.bruis.learnnetty.im.server.handler; + +import com.bruis.learnnetty.im.model.GroupMessageRequestPacket; +import com.bruis.learnnetty.im.model.GroupMessageResponsePacket; +import com.bruis.learnnetty.im.util.SessionUtil; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.SimpleChannelInboundHandler; +import io.netty.channel.group.ChannelGroup; + +/** + * @Description + * @Author luohaiyang + * @Date 2022/3/24 + */ +@ChannelHandler.Sharable +public class GroupMessageRequestHandler extends SimpleChannelInboundHandler { + + public static final GroupMessageRequestHandler INSTANCE = new GroupMessageRequestHandler(); + + public GroupMessageRequestHandler() {} + + @Override + protected void channelRead0(ChannelHandlerContext ctx, GroupMessageRequestPacket msg) throws Exception { + String toGroupId = msg.getToGroupId(); + GroupMessageResponsePacket responsePacket = new GroupMessageResponsePacket(); + responsePacket.setFromGroupId(toGroupId); + responsePacket.setMessage(msg.getMessage()); + responsePacket.setFromUser(SessionUtil.getSession(ctx.channel())); + + ChannelGroup channelGroup = SessionUtil.getChannelGroup(toGroupId); + channelGroup.writeAndFlush(responsePacket); + } +} diff --git a/Spring-Netty/src/main/java/com/bruis/learnnetty/im/server/handler/IMHandler.java b/Spring-Netty/src/main/java/com/bruis/learnnetty/im/server/handler/IMHandler.java new file mode 100644 index 0000000..8420f57 --- /dev/null +++ b/Spring-Netty/src/main/java/com/bruis/learnnetty/im/server/handler/IMHandler.java @@ -0,0 +1,42 @@ +package com.bruis.learnnetty.im.server.handler; + +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.SimpleChannelInboundHandler; +import com.bruis.learnnetty.im.model.Packet; + +import java.util.HashMap; +import java.util.Map; + +import static com.bruis.learnnetty.im.model.Command.*; + + +/** + * @Description + * @Author luohaiyang + * @Date 2022/3/25 + */ +@ChannelHandler.Sharable +public class IMHandler extends SimpleChannelInboundHandler { + + public static final IMHandler INSTANCE = new IMHandler(); + + private Map> handlerMap; + + private IMHandler() { + handlerMap = new HashMap<>(); + + handlerMap.put(MESSAGE_REQUEST, MessageRequestHandler.INSTANCE); + handlerMap.put(CREATE_GROUP_REQUEST, CreateGroupRequestHandler.INSTANCE); + handlerMap.put(JOIN_GROUP_REQUEST, JoinGroupRequestHandler.INSTANCE); + handlerMap.put(QUIT_GROUP_REQUEST, QuitGroupRequestHandler.INSTANCE); + handlerMap.put(LIST_GROUP_MEMBERS_REQUEST, ListGroupMembersRequestHandler.INSTANCE); + handlerMap.put(GROUP_MESSAGE_REQUEST, GroupMessageRequestHandler.INSTANCE); + handlerMap.put(LOGOUT_REQUEST, LogoutRequestHandler.INSTANCE); + } + + @Override + protected void channelRead0(ChannelHandlerContext ctx, Packet packet) throws Exception { + handlerMap.get(packet.getCommand()).channelRead(ctx, packet); + } +} diff --git a/Spring-Netty/src/main/java/com/bruis/learnnetty/im/server/handler/JoinGroupRequestHandler.java b/Spring-Netty/src/main/java/com/bruis/learnnetty/im/server/handler/JoinGroupRequestHandler.java new file mode 100644 index 0000000..1188ec6 --- /dev/null +++ b/Spring-Netty/src/main/java/com/bruis/learnnetty/im/server/handler/JoinGroupRequestHandler.java @@ -0,0 +1,39 @@ +package com.bruis.learnnetty.im.server.handler; + +import com.bruis.learnnetty.im.model.JoinGroupRequestPacket; +import com.bruis.learnnetty.im.model.JoinGroupResponsePacket; +import com.bruis.learnnetty.im.util.SessionUtil; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.SimpleChannelInboundHandler; +import io.netty.channel.group.ChannelGroup; + +/** + * @Description + * @Author luohaiyang + * @Date 2022/3/24 + */ +@ChannelHandler.Sharable +public class JoinGroupRequestHandler extends SimpleChannelInboundHandler { + + public static final JoinGroupRequestHandler INSTANCE = new JoinGroupRequestHandler(); + + protected JoinGroupRequestHandler() {} + + @Override + protected void channelRead0(ChannelHandlerContext ctx, JoinGroupRequestPacket msg) throws Exception { + // 目标群聊id + String groupId = msg.getGroupId(); + ChannelGroup channelGroup = SessionUtil.getChannelGroup(groupId); + JoinGroupResponsePacket responsePacket = new JoinGroupResponsePacket(); + responsePacket.setSuccess(true); + responsePacket.setGroupId(groupId); + if (null == channelGroup) { + responsePacket.setSuccess(false); + responsePacket.setReason("没有该群聊,请重试..."); + } else { + channelGroup.add(ctx.channel()); + } + ctx.channel().writeAndFlush(responsePacket); + } +} diff --git a/Spring-Netty/src/main/java/com/bruis/learnnetty/im/server/handler/ListGroupMembersRequestHandler.java b/Spring-Netty/src/main/java/com/bruis/learnnetty/im/server/handler/ListGroupMembersRequestHandler.java new file mode 100644 index 0000000..8be361d --- /dev/null +++ b/Spring-Netty/src/main/java/com/bruis/learnnetty/im/server/handler/ListGroupMembersRequestHandler.java @@ -0,0 +1,48 @@ +package com.bruis.learnnetty.im.server.handler; + +import com.bruis.learnnetty.im.model.ListGroupMembersRequestPacket; +import com.bruis.learnnetty.im.model.ListGroupMembersResponsePacket; +import com.bruis.learnnetty.im.session.Session; +import com.bruis.learnnetty.im.util.SessionUtil; +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.SimpleChannelInboundHandler; +import io.netty.channel.group.ChannelGroup; + +import java.util.ArrayList; +import java.util.List; + +/** + * @Description + * @Author luohaiyang + * @Date 2022/3/24 + */ +@ChannelHandler.Sharable +public class ListGroupMembersRequestHandler extends SimpleChannelInboundHandler { + + public static final ListGroupMembersRequestHandler INSTANCE = new ListGroupMembersRequestHandler(); + + protected ListGroupMembersRequestHandler() {} + + @Override + protected void channelRead0(ChannelHandlerContext ctx, ListGroupMembersRequestPacket requestPacket) { + // 1. 获取群的 ChannelGroup + String groupId = requestPacket.getGroupId(); + ChannelGroup channelGroup = SessionUtil.getChannelGroup(groupId); + + // 2. 遍历群成员的 channel,对应的 session,构造群成员的信息 + List sessionList = new ArrayList<>(); + for (Channel channel : channelGroup) { + Session session = SessionUtil.getSession(channel); + sessionList.add(session); + } + + // 3. 构建获取成员列表响应写回到客户端 + ListGroupMembersResponsePacket responsePacket = new ListGroupMembersResponsePacket(); + + responsePacket.setGroupId(groupId); + responsePacket.setSessionList(sessionList); + ctx.channel().writeAndFlush(responsePacket); + } +} \ No newline at end of file diff --git a/Spring-Netty/src/main/java/com/bruis/learnnetty/im/server/handler/LoginRequestHandler.java b/Spring-Netty/src/main/java/com/bruis/learnnetty/im/server/handler/LoginRequestHandler.java new file mode 100644 index 0000000..0286a96 --- /dev/null +++ b/Spring-Netty/src/main/java/com/bruis/learnnetty/im/server/handler/LoginRequestHandler.java @@ -0,0 +1,62 @@ +package com.bruis.learnnetty.im.server.handler; + +import com.bruis.learnnetty.im.model.LoginRequestPacket; +import com.bruis.learnnetty.im.model.LoginResponsePacket; +import com.bruis.learnnetty.im.session.Session; +import com.bruis.learnnetty.im.util.IDUtil; +import com.bruis.learnnetty.im.util.SessionUtil; +import io.netty.channel.*; + +import java.util.Arrays; +import java.util.Date; + +/** + * @Description 接收客户端登录请求 + * @Author luohaiyang + * @Date 2022/3/23 + */ +@ChannelHandler.Sharable +public class LoginRequestHandler extends SimpleChannelInboundHandler { + + public static final LoginRequestHandler INSTANCE = new LoginRequestHandler(); + + protected LoginRequestHandler() {} + + @Override + protected void channelRead0(ChannelHandlerContext ctx, LoginRequestPacket loginRequestPacket) { + // 登录校验响应 + LoginResponsePacket loginResponsePacket = new LoginResponsePacket(); + loginResponsePacket.setVersion(loginRequestPacket.getVersion()); + loginResponsePacket.setUserName(loginRequestPacket.getUserName()); + + if (valid(loginRequestPacket)) { + loginResponsePacket.setSuccess(true); + String userId = IDUtil.randomUserId(); + loginResponsePacket.setUserId(userId); + System.out.println("[" + loginRequestPacket.getUserName() + "]登录成功"); + SessionUtil.bindSession(new Session(userId, loginRequestPacket.getUserName()), ctx.channel()); + } else { + loginResponsePacket.setReason("账号密码校验失败"); + loginResponsePacket.setSuccess(false); + System.out.println(new Date() + ": 登录失败!"); + } + + // 登录响应 + ctx.writeAndFlush(loginResponsePacket).addListener((ChannelFutureListener) future -> { + // 关闭channel成功 + Throwable cause = future.cause(); + if (null != cause) { + System.out.println(Arrays.toString(cause.getStackTrace())); + } + }); + } + + private boolean valid(LoginRequestPacket loginRequestPacket) { + return true; + } + + @Override + public void channelInactive(ChannelHandlerContext ctx) { + SessionUtil.unBindSession(ctx.channel()); + } +} diff --git a/Spring-Netty/src/main/java/com/bruis/learnnetty/im/server/handler/LogoutRequestHandler.java b/Spring-Netty/src/main/java/com/bruis/learnnetty/im/server/handler/LogoutRequestHandler.java new file mode 100644 index 0000000..4436802 --- /dev/null +++ b/Spring-Netty/src/main/java/com/bruis/learnnetty/im/server/handler/LogoutRequestHandler.java @@ -0,0 +1,29 @@ +package com.bruis.learnnetty.im.server.handler; + +import com.bruis.learnnetty.im.model.LogoutRequestPacket; +import com.bruis.learnnetty.im.model.LogoutResponsePacket; +import com.bruis.learnnetty.im.util.SessionUtil; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.SimpleChannelInboundHandler; + +/** + * @Description + * @Author luohaiyang + * @Date 2022/3/24 + */ +@ChannelHandler.Sharable +public class LogoutRequestHandler extends SimpleChannelInboundHandler { + + public static final LogoutRequestHandler INSTANCE = new LogoutRequestHandler(); + + protected LogoutRequestHandler () {} + + @Override + protected void channelRead0(ChannelHandlerContext ctx, LogoutRequestPacket msg) { + SessionUtil.unBindSession(ctx.channel()); + LogoutResponsePacket logoutResponsePacket = new LogoutResponsePacket(); + logoutResponsePacket.setSuccess(true); + ctx.channel().writeAndFlush(logoutResponsePacket); + } +} \ No newline at end of file diff --git a/Spring-Netty/src/main/java/com/bruis/learnnetty/im/server/handler/MessageRequestHandler.java b/Spring-Netty/src/main/java/com/bruis/learnnetty/im/server/handler/MessageRequestHandler.java new file mode 100644 index 0000000..b9b83a0 --- /dev/null +++ b/Spring-Netty/src/main/java/com/bruis/learnnetty/im/server/handler/MessageRequestHandler.java @@ -0,0 +1,45 @@ +package com.bruis.learnnetty.im.server.handler; + +import com.bruis.learnnetty.im.model.MessageRequestPacket; +import com.bruis.learnnetty.im.model.MessageResponsePacket; +import com.bruis.learnnetty.im.session.Session; +import com.bruis.learnnetty.im.util.SessionUtil; +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.SimpleChannelInboundHandler; + +/** + * @Description + * @Author luohaiyang + * @Date 2022/3/23 + */ +@ChannelHandler.Sharable +public class MessageRequestHandler extends SimpleChannelInboundHandler { + + public static final MessageRequestHandler INSTANCE = new MessageRequestHandler(); + + protected MessageRequestHandler() {} + + @Override + protected void channelRead0(ChannelHandlerContext channelHandlerContext, MessageRequestPacket messageRequestPacket) throws Exception { + // 1.拿到消息发送方的会话信息 + Session session = SessionUtil.getSession(channelHandlerContext.channel()); + + // 2.通过消息发送方的会话信息构造要发送的消息 + MessageResponsePacket messageResponsePacket = new MessageResponsePacket(); + messageResponsePacket.setFromUserId(session.getUserId()); + messageResponsePacket.setFromUserName(session.getUserName()); + messageResponsePacket.setMessage(messageRequestPacket.getMessage()); + + // 3.拿到消息接收方的 channel + Channel toUserChannel = SessionUtil.getChannel(messageRequestPacket.getToUserId()); + + // 4.将消息发送给消息接收方 + if (toUserChannel != null && SessionUtil.hasLogin(toUserChannel)) { + toUserChannel.writeAndFlush(messageResponsePacket); + } else { + System.err.println("[" + messageRequestPacket.getToUserId() + "] 不在线,发送失败!"); + } + } +} diff --git a/Spring-Netty/src/main/java/com/bruis/learnnetty/im/server/handler/QuitGroupRequestHandler.java b/Spring-Netty/src/main/java/com/bruis/learnnetty/im/server/handler/QuitGroupRequestHandler.java new file mode 100644 index 0000000..86455bb --- /dev/null +++ b/Spring-Netty/src/main/java/com/bruis/learnnetty/im/server/handler/QuitGroupRequestHandler.java @@ -0,0 +1,37 @@ +package com.bruis.learnnetty.im.server.handler; + +import com.bruis.learnnetty.im.model.QuitGroupRequestPacket; +import com.bruis.learnnetty.im.model.QuitGroupResponsePacket; +import com.bruis.learnnetty.im.util.SessionUtil; +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.SimpleChannelInboundHandler; +import io.netty.channel.group.ChannelGroup; + +/** + * @Description + * @Author luohaiyang + * @Date 2022/3/24 + */ +@ChannelHandler.Sharable +public class QuitGroupRequestHandler extends SimpleChannelInboundHandler { + + public static final QuitGroupRequestHandler INSTANCE = new QuitGroupRequestHandler(); + + protected QuitGroupRequestHandler() {} + + @Override + protected void channelRead0(ChannelHandlerContext ctx, QuitGroupRequestPacket msg) throws Exception { + String groupId = msg.getGroupId(); + Channel channel = ctx.channel(); + ChannelGroup channelGroup = SessionUtil.getChannelGroup(groupId); + channelGroup.remove(channel); + + QuitGroupResponsePacket responsePacket = new QuitGroupResponsePacket(); + responsePacket.setSuccess(true); + responsePacket.setGroupId(groupId); + + channel.writeAndFlush(responsePacket); + } +} diff --git a/Spring-Netty/src/main/java/com/bruis/learnnetty/im/session/Session.java b/Spring-Netty/src/main/java/com/bruis/learnnetty/im/session/Session.java new file mode 100644 index 0000000..7a7be2d --- /dev/null +++ b/Spring-Netty/src/main/java/com/bruis/learnnetty/im/session/Session.java @@ -0,0 +1,39 @@ +package com.bruis.learnnetty.im.session; + +/** + * @Description + * @Author luohaiyang + * @Date 2022/3/23 + */ +public class Session { + + private String userId; + + private String userName; + + public Session(String userId, String userName) { + this.userId = userId; + this.userName = userName; + } + + public String getUserId() { + return userId; + } + + public void setUserId(String userId) { + this.userId = userId; + } + + public String getUserName() { + return userName; + } + + public void setUserName(String userName) { + this.userName = userName; + } + + @Override + public String toString() { + return userId + "->" + userName; + } +} \ No newline at end of file diff --git a/Spring-Netty/src/main/java/com/bruis/learnnetty/im/util/IDUtil.java b/Spring-Netty/src/main/java/com/bruis/learnnetty/im/util/IDUtil.java new file mode 100644 index 0000000..3b5403f --- /dev/null +++ b/Spring-Netty/src/main/java/com/bruis/learnnetty/im/util/IDUtil.java @@ -0,0 +1,16 @@ +package com.bruis.learnnetty.im.util; + +import java.util.UUID; + +/** + * @Description + * @Author luohaiyang + * @Date 2022/3/24 + */ +public class IDUtil { + + public static String randomUserId() { + return UUID.randomUUID().toString().split("-")[0]; + } + +} diff --git a/Spring-Netty/src/main/java/com/bruis/learnnetty/im/util/SessionUtil.java b/Spring-Netty/src/main/java/com/bruis/learnnetty/im/util/SessionUtil.java new file mode 100644 index 0000000..f4b41c3 --- /dev/null +++ b/Spring-Netty/src/main/java/com/bruis/learnnetty/im/util/SessionUtil.java @@ -0,0 +1,58 @@ +package com.bruis.learnnetty.im.util; + +import com.bruis.learnnetty.im.model.Attributes; +import com.bruis.learnnetty.im.session.Session; +import io.netty.channel.Channel; +import io.netty.channel.group.ChannelGroup; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * @Description + * @Author luohaiyang + * @Date 2022/3/23 + */ +public class SessionUtil { + + private static final Map userIdChannelMap = new ConcurrentHashMap<>(); + + private static final Map groupIdChannelGroupMap = new ConcurrentHashMap<>(); + + public static void bindSession(Session session, Channel channel) { + userIdChannelMap.put(session.getUserId(), channel); + channel.attr(Attributes.SESSION).set(session); + } + + public static void unBindSession(Channel channel) { + if (hasLogin(channel)) { + Session session = getSession(channel); + userIdChannelMap.remove(session.getUserId()); + channel.attr(Attributes.SESSION).set(null); + System.out.println(session + " 退出登录"); + } + } + + public static boolean hasLogin(Channel channel) { + + return channel.hasAttr(Attributes.SESSION); + } + + public static Session getSession(Channel channel) { + + return channel.attr(Attributes.SESSION).get(); + } + + public static Channel getChannel(String userId) { + + return userIdChannelMap.get(userId); + } + + public static void bindChannelGroup(String groupId, ChannelGroup channelGroup) { + groupIdChannelGroupMap.put(groupId, channelGroup); + } + + public static ChannelGroup getChannelGroup(String groupId) { + return groupIdChannelGroupMap.get(groupId); + } +} diff --git a/Spring-Netty/src/main/java/com/bruis/learnnetty/netty/channelpipeline/BusinessException.java b/Spring-Netty/src/main/java/com/bruis/learnnetty/netty/channelpipeline/BusinessException.java new file mode 100644 index 0000000..e9e5caf --- /dev/null +++ b/Spring-Netty/src/main/java/com/bruis/learnnetty/netty/channelpipeline/BusinessException.java @@ -0,0 +1,8 @@ +package com.bruis.learnnetty.netty.channelpipeline; + +public class BusinessException extends Exception { + + public BusinessException(String message) { + super(message); + } +} diff --git a/Spring-Netty/src/main/java/com/bruis/learnnetty/netty/channelpipeline/InBoundHandlerA.java b/Spring-Netty/src/main/java/com/bruis/learnnetty/netty/channelpipeline/InBoundHandlerA.java new file mode 100644 index 0000000..31004a3 --- /dev/null +++ b/Spring-Netty/src/main/java/com/bruis/learnnetty/netty/channelpipeline/InBoundHandlerA.java @@ -0,0 +1,16 @@ +package com.bruis.learnnetty.netty.channelpipeline; + +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; + +/** + * @author + */ +public class InBoundHandlerA extends ChannelInboundHandlerAdapter { + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + System.out.println("InBoundHandlerA: " + msg); + ctx.fireChannelRead(msg); + } +} diff --git a/Spring-Netty/src/main/java/com/bruis/learnnetty/netty/channelpipeline/InBoundHandlerB.java b/Spring-Netty/src/main/java/com/bruis/learnnetty/netty/channelpipeline/InBoundHandlerB.java new file mode 100644 index 0000000..270d84f --- /dev/null +++ b/Spring-Netty/src/main/java/com/bruis/learnnetty/netty/channelpipeline/InBoundHandlerB.java @@ -0,0 +1,20 @@ +package com.bruis.learnnetty.netty.channelpipeline; + +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; + +/** + * @author + */ +public class InBoundHandlerB extends ChannelInboundHandlerAdapter { + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + System.out.println("InBoundHandlerB: " + msg); + ctx.fireChannelRead(msg); + } + + @Override + public void channelActive(ChannelHandlerContext ctx) { + ctx.channel().pipeline().fireChannelRead("hello world"); + } +} diff --git a/Spring-Netty/src/main/java/com/bruis/learnnetty/netty/channelpipeline/InBoundHandlerC.java b/Spring-Netty/src/main/java/com/bruis/learnnetty/netty/channelpipeline/InBoundHandlerC.java new file mode 100644 index 0000000..e4dcd7e --- /dev/null +++ b/Spring-Netty/src/main/java/com/bruis/learnnetty/netty/channelpipeline/InBoundHandlerC.java @@ -0,0 +1,16 @@ +package com.bruis.learnnetty.netty.channelpipeline; + +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.util.ReferenceCountUtil; + +/** + * @author + */ +public class InBoundHandlerC extends ChannelInboundHandlerAdapter { + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + System.out.println("InBoundHandlerC: " + msg); + ctx.fireChannelRead(msg); + } +} diff --git a/Spring-Netty/src/main/java/com/bruis/learnnetty/netty/channelpipeline/OutBoundHandlerA.java b/Spring-Netty/src/main/java/com/bruis/learnnetty/netty/channelpipeline/OutBoundHandlerA.java new file mode 100644 index 0000000..4711fd5 --- /dev/null +++ b/Spring-Netty/src/main/java/com/bruis/learnnetty/netty/channelpipeline/OutBoundHandlerA.java @@ -0,0 +1,17 @@ +package com.bruis.learnnetty.netty.channelpipeline; + +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelOutboundHandlerAdapter; +import io.netty.channel.ChannelPromise; + +/** + * @author + */ +public class OutBoundHandlerA extends ChannelOutboundHandlerAdapter { + + @Override + public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { + System.out.println("OutBoundHandlerA: " + msg); + ctx.write(msg, promise); + } +} diff --git a/Spring-Netty/src/main/java/com/bruis/learnnetty/netty/channelpipeline/OutBoundHandlerB.java b/Spring-Netty/src/main/java/com/bruis/learnnetty/netty/channelpipeline/OutBoundHandlerB.java new file mode 100644 index 0000000..05a74fd --- /dev/null +++ b/Spring-Netty/src/main/java/com/bruis/learnnetty/netty/channelpipeline/OutBoundHandlerB.java @@ -0,0 +1,27 @@ +package com.bruis.learnnetty.netty.channelpipeline; + +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelOutboundHandlerAdapter; +import io.netty.channel.ChannelPromise; + +import java.util.concurrent.TimeUnit; + +/** + * @author + */ +public class OutBoundHandlerB extends ChannelOutboundHandlerAdapter { + @Override + public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { + System.out.println("OutBoundHandlerB: " + msg); + ctx.write(msg, promise); + } + + + @Override + public void handlerAdded(final ChannelHandlerContext ctx) { + ctx.executor().schedule(() -> { + ctx.channel().write("ctx.channel().write -> hello world"); + ctx.write("hello world"); + }, 3, TimeUnit.SECONDS); + } +} diff --git a/Spring-Netty/src/main/java/com/bruis/learnnetty/netty/channelpipeline/OutBoundHandlerC.java b/Spring-Netty/src/main/java/com/bruis/learnnetty/netty/channelpipeline/OutBoundHandlerC.java new file mode 100644 index 0000000..6b49e11 --- /dev/null +++ b/Spring-Netty/src/main/java/com/bruis/learnnetty/netty/channelpipeline/OutBoundHandlerC.java @@ -0,0 +1,18 @@ +package com.bruis.learnnetty.netty.channelpipeline; + +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelOutboundHandlerAdapter; +import io.netty.channel.ChannelPromise; + +/** + * @author + */ +public class OutBoundHandlerC extends ChannelOutboundHandlerAdapter { + + @Override + public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { + System.out.println("OutBoundHandlerC: " + msg); + ctx.write(msg, promise); +// throw new BusinessException("from OutBoundHandlerC"); + } +} diff --git a/Spring-Netty/src/main/java/com/bruis/learnnetty/netty/channelpipeline/Server.java b/Spring-Netty/src/main/java/com/bruis/learnnetty/netty/channelpipeline/Server.java new file mode 100644 index 0000000..dbcd6f7 --- /dev/null +++ b/Spring-Netty/src/main/java/com/bruis/learnnetty/netty/channelpipeline/Server.java @@ -0,0 +1,48 @@ +package com.bruis.learnnetty.netty.channelpipeline; + +import io.netty.bootstrap.ServerBootstrap; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelOption; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.SocketChannel; +import io.netty.channel.socket.nio.NioServerSocketChannel; +import io.netty.util.AttributeKey; + +/** + * @author + */ +public final class Server { + + public static void main(String[] args) throws Exception { + EventLoopGroup bossGroup = new NioEventLoopGroup(1); + EventLoopGroup workerGroup = new NioEventLoopGroup(); + + try { + ServerBootstrap b = new ServerBootstrap(); + b.group(bossGroup, workerGroup) + .channel(NioServerSocketChannel.class) + .childOption(ChannelOption.TCP_NODELAY, true) + .childAttr(AttributeKey.newInstance("childAttr"), "childAttrValue") + .childHandler(new ChannelInitializer() { + @Override + public void initChannel(SocketChannel ch) { + ch.pipeline().addLast(new OutBoundHandlerA()); + ch.pipeline().addLast(new OutBoundHandlerB()); + ch.pipeline().addLast(new OutBoundHandlerC()); +// ch.pipeline().addLast(new InBoundHandlerA()); +// ch.pipeline().addLast(new InBoundHandlerB()); +// ch.pipeline().addLast(new InBoundHandlerC()); + } + }); + + ChannelFuture f = b.bind(8888).sync(); + + f.channel().closeFuture().sync(); + } finally { + bossGroup.shutdownGracefully(); + workerGroup.shutdownGracefully(); + } + } +} \ No newline at end of file diff --git a/Spring-Netty/src/main/java/com/bruis/learnnetty/netty/connections/longconnections/ClientHandler.java b/Spring-Netty/src/main/java/com/bruis/learnnetty/netty/connections/longconnections/ClientHandler.java new file mode 100644 index 0000000..7d69f53 --- /dev/null +++ b/Spring-Netty/src/main/java/com/bruis/learnnetty/netty/connections/longconnections/ClientHandler.java @@ -0,0 +1,17 @@ +package com.bruis.learnnetty.netty.connections.longconnections; + +import com.alibaba.fastjson.JSONObject; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; + +/** + * @author lhy + * @date 2022/2/11 + */ +public class ClientHandler extends ChannelInboundHandlerAdapter { + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + Response response = JSONObject.parseObject(msg.toString(), Response.class); + RequestFuture.received(response); + } +} diff --git a/Spring-Netty/src/main/java/com/bruis/learnnetty/netty/connections/longconnections/NettyClient.java b/Spring-Netty/src/main/java/com/bruis/learnnetty/netty/connections/longconnections/NettyClient.java new file mode 100644 index 0000000..cd3a840 --- /dev/null +++ b/Spring-Netty/src/main/java/com/bruis/learnnetty/netty/connections/longconnections/NettyClient.java @@ -0,0 +1,91 @@ +package com.bruis.learnnetty.netty.connections.longconnections; + +import com.alibaba.fastjson.JSONObject; +import com.bruis.learnnetty.netty.connections.longconnections.ClientHandler; +import io.netty.bootstrap.Bootstrap; +import io.netty.buffer.PooledByteBufAllocator; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelOption; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.nio.NioSocketChannel; +import io.netty.handler.codec.LengthFieldBasedFrameDecoder; +import io.netty.handler.codec.LengthFieldPrepender; +import io.netty.handler.codec.string.StringDecoder; +import io.netty.handler.codec.string.StringEncoder; +import org.springframework.util.StringUtils; + +import java.nio.charset.Charset; + +/** + * @author lhy + * @date 2022/2/16 + */ +public class NettyClient { + public static EventLoopGroup group = null; + public static Bootstrap bootstrap = null; + public static ChannelFuture future = null; + static { + bootstrap = new Bootstrap(); + group = new NioEventLoopGroup(); + bootstrap.channel(NioSocketChannel.class); + bootstrap.group(group); + bootstrap.option(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT); + final ClientHandler clientHandler = new ClientHandler(); + bootstrap.handler(new ChannelInitializer() { + @Override + protected void initChannel(NioSocketChannel ch) throws Exception { + ch.pipeline().addLast(new LengthFieldBasedFrameDecoder(Integer.MAX_VALUE, + 0, 4, 0, 4)); + ch.pipeline().addLast(new StringDecoder()); + ch.pipeline().addLast(clientHandler); + ch.pipeline().addLast(new LengthFieldPrepender(4, false)); + ch.pipeline().addLast(new StringEncoder(Charset.forName("utf-8"))); + } + }); + try { + future = bootstrap.connect("127.0.0.1", 8080).sync(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + + /** + * 说明:对于这个长连接的例子中,使用了静态化,即单链接、长连接,如果是多链接的话不可使用静态化,需使用线程池。 + * @param msg + * @return + */ + public Object sendRequest(Object msg) { + try { + RequestFuture request = new RequestFuture(); + request.setRequest(msg); + String requestStr = JSONObject.toJSONString(request); + future.channel().writeAndFlush(requestStr); + myselfPrint("我阻塞了", null); + return request.get(); + } catch (Exception e) { + e.printStackTrace(); + throw e; + } + } + public static void main(String[] args) { + NettyClient nettyClient = new NettyClient(); + for (int i = 0; i < 10; i++) { + Object result = nettyClient.sendRequest("hello"); + myselfPrint("拿到结果了", result); + } + } + + public static void myselfPrint(String description, Object value) { + StringBuilder builder = new StringBuilder(); + builder.append(Thread.currentThread().getName()); + if (!StringUtils.isEmpty(description)) { + builder.append("-").append(description); + } + if (!StringUtils.isEmpty(value)) { + builder.append("-").append(value); + } + System.out.println(builder.toString()); + } +} diff --git a/Spring-Netty/src/main/java/com/bruis/learnnetty/netty/connections/longconnections/NettyServer.java b/Spring-Netty/src/main/java/com/bruis/learnnetty/netty/connections/longconnections/NettyServer.java new file mode 100644 index 0000000..d668c6b --- /dev/null +++ b/Spring-Netty/src/main/java/com/bruis/learnnetty/netty/connections/longconnections/NettyServer.java @@ -0,0 +1,52 @@ +package com.bruis.learnnetty.netty.connections.longconnections; + +import com.bruis.learnnetty.netty.connections.shortconnections.ServerHandler; +import io.netty.bootstrap.ServerBootstrap; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelOption; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.SocketChannel; +import io.netty.channel.socket.nio.NioServerSocketChannel; +import io.netty.handler.codec.LengthFieldBasedFrameDecoder; +import io.netty.handler.codec.LengthFieldPrepender; +import io.netty.handler.codec.string.StringDecoder; +import io.netty.handler.codec.string.StringEncoder; + +/** + * 基于短连接的Netty服务端 + * + * @author lhy + * @date 2022/2/11 + */ +public class NettyServer { + public static void main(String[] args) { + EventLoopGroup bossGroup = new NioEventLoopGroup(1); + EventLoopGroup workerGroup = new NioEventLoopGroup(); + try { + ServerBootstrap serverBootstrap = new ServerBootstrap(); + serverBootstrap.group(bossGroup, workerGroup); + serverBootstrap.channel(NioServerSocketChannel.class); + + serverBootstrap.option(ChannelOption.SO_BACKLOG, 128) + .childHandler(new ChannelInitializer() { + @Override + protected void initChannel(SocketChannel ch) throws Exception { + ch.pipeline().addLast(new LengthFieldBasedFrameDecoder(Integer.MAX_VALUE, 0, 4, 0, 4)) + .addLast(new StringDecoder()) + .addLast(new ServerHandler()) + .addLast(new LengthFieldPrepender(4, false)) + .addLast(new StringEncoder()); + } + }); + ChannelFuture future = serverBootstrap.bind(8080).sync(); + future.channel().closeFuture().sync(); + } catch (Exception e) { + e.printStackTrace(); + } finally { + bossGroup.shutdownGracefully(); + workerGroup.shutdownGracefully(); + } + } +} diff --git a/Spring-Netty/src/main/java/com/bruis/learnnetty/netty/connections/longconnections/RequestFuture.java b/Spring-Netty/src/main/java/com/bruis/learnnetty/netty/connections/longconnections/RequestFuture.java new file mode 100644 index 0000000..6c5167a --- /dev/null +++ b/Spring-Netty/src/main/java/com/bruis/learnnetty/netty/connections/longconnections/RequestFuture.java @@ -0,0 +1,130 @@ +package com.bruis.learnnetty.netty.connections.longconnections; + +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.locks.Condition; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +/** + * 模拟客户端请求类,用于构建请求对象 + * + * @author lhy + * @date 2022/2/10 + */ +public class RequestFuture { + public static Map futures = new ConcurrentHashMap<>(); + private final Lock lock = new ReentrantLock(); + private final Condition condition = lock.newCondition(); + private long id; + /** + * 请求参数 + */ + private Object request; + /** + * 响应结果 + */ + private Object result; + /** + * 超时时间 + */ + private long timeout = 5000; + public static final AtomicLong aid = new AtomicLong(); + + public RequestFuture() { + id = aid.incrementAndGet(); + addFuture(this); + } + + /** + * 把请求放入本地缓存中 + * @param future + */ + public static void addFuture(RequestFuture future) { + futures.put(future.getId(), future); + } + + /** + * 同步获取响应结果 + * @return + */ + public Object get() { + lock.lock(); + try { + while (this.result == null) { + try { + // 主线程默认等待5s,然后查看下结果 + condition.await(timeout, TimeUnit.MILLISECONDS); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + } finally { + lock.unlock(); + } + return this.result; + } + + /** + * 表明服务端发送过来的结果已经接收到了,可以signal了 + * @param result + */ + public static void received(Response result) { + RequestFuture future = futures.remove(result.getId()); + if (null != future) { + future.setResult(result.getResult()); + } + /** + * 通知主线程 + */ + Objects.requireNonNull(future, "RequestFuture").getLock().lock(); + try { + future.getCondition().signalAll(); + } finally { + Objects.requireNonNull(future, "RequestFuture").getLock().unlock(); + } + } + + public long getId() { + return id; + } + + public void setId(long id) { + this.id = id; + } + + public Object getRequest() { + return request; + } + + public void setRequest(Object request) { + this.request = request; + } + + public Object getResult() { + return result; + } + + public void setResult(Object result) { + this.result = result; + } + + public long getTimeout() { + return timeout; + } + + public void setTimeout(long timeout) { + this.timeout = timeout; + } + + public Lock getLock() { + return lock; + } + + public Condition getCondition() { + return condition; + } +} diff --git a/Spring-Netty/src/main/java/com/bruis/learnnetty/netty/connections/longconnections/Response.java b/Spring-Netty/src/main/java/com/bruis/learnnetty/netty/connections/longconnections/Response.java new file mode 100644 index 0000000..34ee0d0 --- /dev/null +++ b/Spring-Netty/src/main/java/com/bruis/learnnetty/netty/connections/longconnections/Response.java @@ -0,0 +1,28 @@ +package com.bruis.learnnetty.netty.connections.longconnections; + +/** + * 响应结果类 + * + * @author lhy + * @date 2022/2/10 + */ +public class Response { + private long id; + private Object result; + + public long getId() { + return id; + } + + public void setId(long id) { + this.id = id; + } + + public Object getResult() { + return result; + } + + public void setResult(Object result) { + this.result = result; + } +} diff --git a/Spring-Netty/src/main/java/com/bruis/learnnetty/netty/connections/shortconnections/ClientHandler.java b/Spring-Netty/src/main/java/com/bruis/learnnetty/netty/connections/shortconnections/ClientHandler.java new file mode 100644 index 0000000..6918c68 --- /dev/null +++ b/Spring-Netty/src/main/java/com/bruis/learnnetty/netty/connections/shortconnections/ClientHandler.java @@ -0,0 +1,28 @@ +package com.bruis.learnnetty.netty.connections.shortconnections; + +import com.alibaba.fastjson.JSONObject; +import com.bruis.learnnetty.thread.synchronize.Response; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.util.concurrent.Promise; + +/** + * @author lhy + * @date 2022/2/11 + */ +public class ClientHandler extends ChannelInboundHandlerAdapter { + private Promise promise; + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + Response response = JSONObject.parseObject(msg.toString(), Response.class); + promise.setSuccess(response); + } + + public Promise getPromise() { + return promise; + } + + public void setPromise(Promise promise) { + this.promise = promise; + } +} diff --git a/Spring-Netty/src/main/java/com/bruis/learnnetty/netty/connections/shortconnections/NettyClient.java b/Spring-Netty/src/main/java/com/bruis/learnnetty/netty/connections/shortconnections/NettyClient.java new file mode 100644 index 0000000..e00bc64 --- /dev/null +++ b/Spring-Netty/src/main/java/com/bruis/learnnetty/netty/connections/shortconnections/NettyClient.java @@ -0,0 +1,66 @@ +package com.bruis.learnnetty.netty.connections.shortconnections; + +import com.alibaba.fastjson.JSONObject; +import com.bruis.learnnetty.thread.synchronize.RequestFuture; +import com.bruis.learnnetty.thread.synchronize.Response; +import io.netty.bootstrap.Bootstrap; +import io.netty.buffer.PooledByteBufAllocator; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelOption; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.nio.NioSocketChannel; +import io.netty.handler.codec.LengthFieldBasedFrameDecoder; +import io.netty.handler.codec.LengthFieldPrepender; +import io.netty.handler.codec.string.StringDecoder; +import io.netty.handler.codec.string.StringEncoder; +import io.netty.util.concurrent.DefaultPromise; +import io.netty.util.concurrent.Promise; + +import java.nio.charset.StandardCharsets; + +/** + * @author lhy + * @date 2022/2/11 + */ +public class NettyClient { + public static EventLoopGroup group = null; + public static Bootstrap bootstrap = null; + static { + bootstrap = new Bootstrap(); + group = new NioEventLoopGroup(); + bootstrap.channel(NioSocketChannel.class); + bootstrap.group(group); + bootstrap.option(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT); + } + public static void main(String[] args) { + try { + Promise promise = new DefaultPromise<>(group.next()); + final ClientHandler clientHandler = new ClientHandler(); + clientHandler.setPromise(promise); + bootstrap.handler(new ChannelInitializer() { + @Override + protected void initChannel(NioSocketChannel ch) throws Exception { + ch.pipeline().addLast(new LengthFieldBasedFrameDecoder(Integer.MAX_VALUE, 0, 4, 0, 4)) + .addLast(new StringDecoder()) + .addLast(clientHandler) + .addLast(new LengthFieldPrepender(4, false)) + .addLast(new StringEncoder(StandardCharsets.UTF_8)); + } + }); + ChannelFuture future = bootstrap.connect("127.0.0.1", 8080).sync(); + RequestFuture request = new RequestFuture(); + request.setId(1); + request.setRequest("hello world!"); + String requestString = JSONObject.toJSONString(request); + // 向服务端发送请求 + future.channel().writeAndFlush(requestString); + // 同步阻塞等待服务端响应请求 + Response response = promise.get(); + System.out.println(JSONObject.toJSONString(response)); + } catch (Exception e) { + e.printStackTrace(); + } + } +} diff --git a/Spring-Netty/src/main/java/com/bruis/learnnetty/netty/connections/shortconnections/NettyServer.java b/Spring-Netty/src/main/java/com/bruis/learnnetty/netty/connections/shortconnections/NettyServer.java new file mode 100644 index 0000000..4453bd6 --- /dev/null +++ b/Spring-Netty/src/main/java/com/bruis/learnnetty/netty/connections/shortconnections/NettyServer.java @@ -0,0 +1,51 @@ +package com.bruis.learnnetty.netty.connections.shortconnections; + +import io.netty.bootstrap.ServerBootstrap; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelOption; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.SocketChannel; +import io.netty.channel.socket.nio.NioServerSocketChannel; +import io.netty.handler.codec.LengthFieldBasedFrameDecoder; +import io.netty.handler.codec.LengthFieldPrepender; +import io.netty.handler.codec.string.StringDecoder; +import io.netty.handler.codec.string.StringEncoder; + +/** + * 基于短连接的Netty服务端 + * + * @author lhy + * @date 2022/2/11 + */ +public class NettyServer { + public static void main(String[] args) { + EventLoopGroup bossGroup = new NioEventLoopGroup(1); + EventLoopGroup workerGroup = new NioEventLoopGroup(); + try { + ServerBootstrap serverBootstrap = new ServerBootstrap(); + serverBootstrap.group(bossGroup, workerGroup); + serverBootstrap.channel(NioServerSocketChannel.class); + + serverBootstrap.option(ChannelOption.SO_BACKLOG, 128) + .childHandler(new ChannelInitializer() { + @Override + protected void initChannel(SocketChannel ch) throws Exception { + ch.pipeline().addLast(new LengthFieldBasedFrameDecoder(Integer.MAX_VALUE, 0, 4, 0, 4)) + .addLast(new StringDecoder()) + .addLast(new ServerHandler()) + .addLast(new LengthFieldPrepender(4, false)) + .addLast(new StringEncoder()); + } + }); + ChannelFuture future = serverBootstrap.bind(8080).sync(); + future.channel().closeFuture().sync(); + } catch (Exception e) { + e.printStackTrace(); + } finally { + bossGroup.shutdownGracefully(); + workerGroup.shutdownGracefully(); + } + } +} diff --git a/Spring-Netty/src/main/java/com/bruis/learnnetty/netty/connections/shortconnections/ServerHandler.java b/Spring-Netty/src/main/java/com/bruis/learnnetty/netty/connections/shortconnections/ServerHandler.java new file mode 100644 index 0000000..ec172ec --- /dev/null +++ b/Spring-Netty/src/main/java/com/bruis/learnnetty/netty/connections/shortconnections/ServerHandler.java @@ -0,0 +1,32 @@ +package com.bruis.learnnetty.netty.connections.shortconnections; + +import com.alibaba.fastjson.JSONObject; +import com.bruis.learnnetty.thread.synchronize.RequestFuture; +import com.bruis.learnnetty.thread.synchronize.Response; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; + +/** + * @author lhy + * @date 2022/2/11 + */ +public class ServerHandler extends ChannelInboundHandlerAdapter { + /** + * 接受客户端发送过来的请求 + * @param ctx + * @param msg + * @throws Exception + */ + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + RequestFuture request = JSONObject.parseObject(msg.toString(), RequestFuture.class); + long id = request.getId(); + Response response = new Response(); + response.setId(id); + response.setResult("我是服务端(" + id + ")"); + /** + * 给客户端发送响应结果 + */ + ctx.channel().writeAndFlush(JSONObject.toJSONString(response)); + } +} diff --git a/Spring-Netty/src/main/java/com/bruis/learnnetty/netty/fastthreadlocal/FastThreadLocalTest.java b/Spring-Netty/src/main/java/com/bruis/learnnetty/netty/fastthreadlocal/FastThreadLocalTest.java new file mode 100644 index 0000000..ec00911 --- /dev/null +++ b/Spring-Netty/src/main/java/com/bruis/learnnetty/netty/fastthreadlocal/FastThreadLocalTest.java @@ -0,0 +1,62 @@ +package com.bruis.learnnetty.netty.fastthreadlocal; + +import io.netty.util.concurrent.FastThreadLocal; + +/** + * FastThreadLocal测试类 + * + * @author lhy + * @date 2021/7/13 + */ +public class FastThreadLocalTest { + + /** + * FastThreadLocal对象1 + */ + private static FastThreadLocal threadLocal0 = new FastThreadLocal() { + @Override + protected Object initialValue() throws Exception { + Object o = new Object(); + System.out.println("threadLocal0 initialValue: " + o); + return o; + } + + @Override + protected void onRemoval(Object value) throws Exception { + System.out.println("onRemoval"); + } + }; + + private static FastThreadLocal threadLocal1 = new FastThreadLocal() { + @Override + protected Object initialValue() throws Exception { + Object o = new Object(); + System.out.println("threadLocal1 initialValue: " + o); + return o; + } + }; + + public static void main(String[] args) { + new Thread(() -> { + Object object0 = threadLocal0.get(); + System.out.println(Thread.currentThread().getName() + "---> " + object0); + + threadLocal0.set(new Object()); + }) .start(); + + new Thread(() -> { + Object object0 = threadLocal0.get(); + System.out.println(Thread.currentThread().getName() + "---> " + object0); + + System.out.println(Thread.currentThread().getName() + "---> " + (threadLocal0.get() == object0)); +// while (true) { +// System.out.println(Thread.currentThread().getName() + "---> " + (threadLocal0.get() == object0)); +// try { +// Thread.sleep(1000); +// } catch (Exception e) { +// e.printStackTrace(); +// } +// } + }).start(); + } +} diff --git a/Spring-Netty/src/main/java/com/bruis/learnnetty/netty/fastthreadlocal/ThreadLocalTest.java b/Spring-Netty/src/main/java/com/bruis/learnnetty/netty/fastthreadlocal/ThreadLocalTest.java new file mode 100644 index 0000000..11b1b33 --- /dev/null +++ b/Spring-Netty/src/main/java/com/bruis/learnnetty/netty/fastthreadlocal/ThreadLocalTest.java @@ -0,0 +1,38 @@ +package com.bruis.learnnetty.netty.fastthreadlocal; + +/** + * @author lhy + * @date 2021/7/14 + */ +public class ThreadLocalTest { + + private static ThreadLocal threadLocal0 = new ThreadLocal<>(); + + private static ThreadLocal threadLocal1 = new ThreadLocal<>(); + + public static void main(String[] args) { + // 线程外 + System.out.println("main线程1: " + threadLocal0.get()); + Object o = new Object(); + threadLocal0.set(o); + + new Thread(() -> { + Object threadObject = threadLocal0.get(); + System.out.println("线程内: " + threadObject); + if (threadObject == null) { + Object newObject = new Object(); + System.out.println("新new一个对象:" + newObject); + threadLocal0.set(newObject); + } + try { + Thread.sleep(1000); + System.out.println("休眠了一秒"); + } catch (Exception e) { + e.printStackTrace(); + } + System.out.println("线程内,从ThreadLocal获取:" + threadLocal0.get()); + }).start(); + + System.out.println("main线程2: " + threadLocal0.get()); + } +} diff --git a/Spring-Netty/src/main/java/com/bruis/learnnetty/netty/heartbeat/ClientTest.java b/Spring-Netty/src/main/java/com/bruis/learnnetty/netty/heartbeat/ClientTest.java new file mode 100644 index 0000000..cd1d1d8 --- /dev/null +++ b/Spring-Netty/src/main/java/com/bruis/learnnetty/netty/heartbeat/ClientTest.java @@ -0,0 +1,100 @@ +package com.bruis.learnnetty.netty.heartbeat; + +import io.netty.bootstrap.Bootstrap; +import io.netty.channel.*; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.SocketChannel; +import io.netty.channel.socket.nio.NioSocketChannel; +import io.netty.util.concurrent.DefaultEventExecutorGroup; + +import java.net.InetSocketAddress; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * @author lhy + * @date 2021/8/19 + */ +public class ClientTest { + + public static final EventLoopGroup myEventLoopGroup = new NioEventLoopGroup(1, new ThreadFactory() { + + private AtomicInteger threadIndex = new AtomicInteger(0); + + @Override + public Thread newThread(Runnable r) { + return new Thread(r, String.format("MyNettyClientSelector_%d", this.threadIndex.incrementAndGet())); + } + }); + + public static final DefaultEventExecutorGroup nettyHandlerExecutorGroup = new DefaultEventExecutorGroup(1, + new ThreadFactory() { + private AtomicInteger threadIndex = new AtomicInteger(0); + @Override + public Thread newThread(Runnable r) { + return new Thread(r, "nettyHandlerThread_" + this.threadIndex.incrementAndGet()); + } + }); + + public static final ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor(new ThreadFactory() { + @Override + public Thread newThread(Runnable r) { + Thread thread = new Thread(r, "scheduledThread_"); + thread.setDaemon(false); + return thread; + } + }); + + public static void main(String[] args) { + + Bootstrap bootstrap = new Bootstrap() + .group(myEventLoopGroup) + .channel(NioSocketChannel.class) + .option(ChannelOption.TCP_NODELAY, true) + .option(ChannelOption.SO_KEEPALIVE, false) + .option(ChannelOption.SO_SNDBUF, 65535) + .option(ChannelOption.SO_RCVBUF, 65535) + .handler(new ChannelInitializer() { + @Override + protected void initChannel(SocketChannel ch) throws Exception { + ChannelPipeline pipeline = ch.pipeline(); + pipeline.addLast(nettyHandlerExecutorGroup, + new NettyEncoder(), + new NettyDecoder(), + new ConnectResponseHandler()); + } + }); + + InetSocketAddress inetSocketAddress = new InetSocketAddress("127.0.0.1", 9090); + + final ChannelFuture channelFuture = bootstrap.connect(inetSocketAddress); + + if (channelFuture.awaitUninterruptibly(2, TimeUnit.MINUTES)) { +// heartBeat(channelFuture.channel()); + scheduledExecutorService.scheduleAtFixedRate(new Runnable() { + @Override + public void run() { + heartBeat(channelFuture.channel()); + } + }, 1000, 30 * 1000, TimeUnit.MILLISECONDS); + } + } + + public static void heartBeat(Channel channel) { + String request = "客户端发起了心跳请求"; + RemotingCommand command= new RemotingCommand(); + command.setBody(request.getBytes()); + command.setCode(1); + channel.writeAndFlush(command); + } + + public static class ConnectResponseHandler extends SimpleChannelInboundHandler { + @Override + protected void channelRead0(ChannelHandlerContext ctx, RemotingCommand msg) throws Exception { + System.out.println("服务端返回消息了:" + new String(msg.getBody())); + } + } +} diff --git a/Spring-Netty/src/main/java/com/bruis/learnnetty/netty/heartbeat/NettyDecoder.java b/Spring-Netty/src/main/java/com/bruis/learnnetty/netty/heartbeat/NettyDecoder.java new file mode 100644 index 0000000..81be993 --- /dev/null +++ b/Spring-Netty/src/main/java/com/bruis/learnnetty/netty/heartbeat/NettyDecoder.java @@ -0,0 +1,40 @@ +package com.bruis.learnnetty.netty.heartbeat; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.LengthFieldBasedFrameDecoder; + +import java.nio.ByteBuffer; + +/** + * @author lhy + * @date 2021/8/19 + */ +public class NettyDecoder extends LengthFieldBasedFrameDecoder { + + public NettyDecoder() { + super(16777216, 0, 4, 0, 4); + } + + @Override + protected Object decode(ChannelHandlerContext ctx, ByteBuf in) throws Exception { + ByteBuf frame = null; + + try { + frame = (ByteBuf) super.decode(ctx, in); + if (null == frame) { + return null; + } + ByteBuffer byteBuffer = frame.nioBuffer(); + return RemotingCommand.decode(byteBuffer); + } catch (Exception e) { + e.printStackTrace(); + } finally { + if (null != frame) { + frame.release(); + } + } + + return null; + } +} diff --git a/Spring-Netty/src/main/java/com/bruis/learnnetty/netty/heartbeat/NettyEncoder.java b/Spring-Netty/src/main/java/com/bruis/learnnetty/netty/heartbeat/NettyEncoder.java new file mode 100644 index 0000000..1a7fd50 --- /dev/null +++ b/Spring-Netty/src/main/java/com/bruis/learnnetty/netty/heartbeat/NettyEncoder.java @@ -0,0 +1,39 @@ +package com.bruis.learnnetty.netty.heartbeat; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelFutureListener; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.MessageToByteEncoder; + +import java.nio.ByteBuffer; + +/** + * @author lhy + * @date 2021/8/19 + */ +@ChannelHandler.Sharable +public class NettyEncoder extends MessageToByteEncoder { + + @Override + protected void encode(ChannelHandlerContext ctx, RemotingCommand remotingCommand, ByteBuf out) throws Exception { + try { + ByteBuffer header = remotingCommand.encodeHeader(); + out.writeBytes(header); + byte[] body = remotingCommand.getBody(); + if (null != body) { + out.writeBytes(body); + } +// out.writeBytes(remotingCommand.getBody()); + } catch (Exception e) { + e.printStackTrace(); + ctx.channel().close().addListener(new ChannelFutureListener() { + @Override + public void operationComplete(ChannelFuture future) throws Exception { + // 关闭channel成功 + } + }); + } + } +} diff --git a/Spring-Netty/src/main/java/com/bruis/learnnetty/netty/heartbeat/RemotingCommand.java b/Spring-Netty/src/main/java/com/bruis/learnnetty/netty/heartbeat/RemotingCommand.java new file mode 100644 index 0000000..6bf9aa9 --- /dev/null +++ b/Spring-Netty/src/main/java/com/bruis/learnnetty/netty/heartbeat/RemotingCommand.java @@ -0,0 +1,87 @@ +package com.bruis.learnnetty.netty.heartbeat; + +import java.nio.ByteBuffer; + +/** + * @author lhy + * @date 2021/8/19 + */ +public class RemotingCommand { + + private Integer code; // 请求码 + + private byte[] body; // 请求内容 + + public static RemotingCommand decode(final ByteBuffer byteBuffer) { + int limit = byteBuffer.limit(); + + int oriHeaderLen = byteBuffer.getInt(); + int headerLength = getHeaderLength(oriHeaderLen); + + byte[] headerData = new byte[headerLength]; + byteBuffer.get(headerData); + + int bodyLength = limit - 4 - headerLength; + + byte[] body = new byte[bodyLength]; + byteBuffer.get(body); + RemotingCommand remotingCommand = new RemotingCommand(); + remotingCommand.setBody(body); + return remotingCommand; + } + + public ByteBuffer encodeHeader() { + return encodeHeader(this.body.length); + } + + public ByteBuffer encodeHeader(final int bodyLength) { + int length = 4; + + byte[] headerData; + headerData = this.headerEncode(); + length += headerData.length; // 头 + length += bodyLength; // 请求/响应体 + + ByteBuffer result = ByteBuffer.allocate(4 + length - bodyLength); // 分配header + result.putInt(length); + result.put(markProtocolType(headerData.length, SerializeType.JSON)); + result.put(headerData); // 添加头 + result.flip(); + + return result; + } + + public static byte[] markProtocolType(int source, SerializeType type) { + byte[] result = new byte[4]; + + result[0] = type.getCode(); + result[1] = (byte) ((source >> 16) & 0xFF); + result[2] = (byte) ((source >> 8) & 0xFF); + result[3] = (byte) (source & 0xFF); + return result; + } + + private byte[] headerEncode() { + return RemotingSerializable.encode(this); + } + + public static int getHeaderLength(int length) { + return length & 0xFFFFFF; + } + + public Integer getCode() { + return code; + } + + public void setCode(Integer code) { + this.code = code; + } + + public byte[] getBody() { + return body; + } + + public void setBody(byte[] body) { + this.body = body; + } +} diff --git a/Spring-Netty/src/main/java/com/bruis/learnnetty/netty/heartbeat/RemotingSerializable.java b/Spring-Netty/src/main/java/com/bruis/learnnetty/netty/heartbeat/RemotingSerializable.java new file mode 100644 index 0000000..2dca4f3 --- /dev/null +++ b/Spring-Netty/src/main/java/com/bruis/learnnetty/netty/heartbeat/RemotingSerializable.java @@ -0,0 +1,50 @@ +package com.bruis.learnnetty.netty.heartbeat; + +import com.alibaba.fastjson.JSON; + +import java.nio.charset.Charset; + +/** + * @author lhy + * @date 2021/8/19 + */ +public abstract class RemotingSerializable { + private final static Charset CHARSET_UTF8 = Charset.forName("UTF-8"); + + public static byte[] encode(final Object obj) { + final String json = toJson(obj, false); + if (json != null) { + return json.getBytes(CHARSET_UTF8); + } + return null; + } + + public static String toJson(final Object obj, boolean prettyFormat) { + return JSON.toJSONString(obj, prettyFormat); + } + + public static T decode(final byte[] data, Class classOfT) { + final String json = new String(data, CHARSET_UTF8); + return fromJson(json, classOfT); + } + + public static T fromJson(String json, Class classOfT) { + return JSON.parseObject(json, classOfT); + } + + public byte[] encode() { + final String json = this.toJson(); + if (json != null) { + return json.getBytes(CHARSET_UTF8); + } + return null; + } + + public String toJson() { + return toJson(false); + } + + public String toJson(final boolean prettyFormat) { + return toJson(this, prettyFormat); + } +} diff --git a/Spring-Netty/src/main/java/com/bruis/learnnetty/netty/heartbeat/SerializeType.java b/Spring-Netty/src/main/java/com/bruis/learnnetty/netty/heartbeat/SerializeType.java new file mode 100644 index 0000000..6187252 --- /dev/null +++ b/Spring-Netty/src/main/java/com/bruis/learnnetty/netty/heartbeat/SerializeType.java @@ -0,0 +1,28 @@ +package com.bruis.learnnetty.netty.heartbeat; + +/** + * @author lhy + * @date 2021/8/20 + */ +public enum SerializeType { + JSON((byte) 0); + + private byte code; + + SerializeType(byte code) { + this.code = code; + } + + public static SerializeType valueOf(byte code) { + for (SerializeType serializeType : SerializeType.values()) { + if (serializeType.getCode() == code) { + return serializeType; + } + } + return null; + } + + public byte getCode() { + return code; + } +} diff --git a/Spring-Netty/src/main/java/com/bruis/learnnetty/netty/heartbeat/ServerTest.java b/Spring-Netty/src/main/java/com/bruis/learnnetty/netty/heartbeat/ServerTest.java new file mode 100644 index 0000000..4753a2e --- /dev/null +++ b/Spring-Netty/src/main/java/com/bruis/learnnetty/netty/heartbeat/ServerTest.java @@ -0,0 +1,55 @@ +package com.bruis.learnnetty.netty.heartbeat; + +import io.netty.bootstrap.ServerBootstrap; +import io.netty.channel.*; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.nio.NioServerSocketChannel; +import io.netty.channel.socket.nio.NioSocketChannel; + +/** + * @author lhy + * @date 2021/8/19 + */ +public class ServerTest { + public static void main(String[] args) throws InterruptedException { + EventLoopGroup bossGroup = new NioEventLoopGroup(1); + EventLoopGroup workerGroup=new NioEventLoopGroup(); + ServerBootstrap serverBootstrap=new ServerBootstrap(); + serverBootstrap.group(bossGroup,workerGroup) + .channel(NioServerSocketChannel.class) + //设置线程队列中等待连接的个数 + .option(ChannelOption.SO_BACKLOG,128) + .option(ChannelOption.SO_REUSEADDR, true) + .childOption(ChannelOption.TCP_NODELAY, true) + .childOption(ChannelOption.SO_SNDBUF, 65535) + .childOption(ChannelOption.SO_RCVBUF, 65535) + .childOption(ChannelOption.SO_KEEPALIVE,true) + .childHandler(new ChannelInitializer() { + @Override + protected void initChannel(NioSocketChannel nioSocketChannel) throws Exception { + nioSocketChannel.pipeline() + .addLast(new NettyEncoder(), + new NettyDecoder(), + new ConnectServerHandler()); + + } + }); + + ChannelFuture future = serverBootstrap.bind(9090).sync(); + future.channel().closeFuture().sync(); + bossGroup.shutdownGracefully(); + workerGroup.shutdownGracefully(); + } + + public static class ConnectServerHandler extends SimpleChannelInboundHandler { + + @Override + protected void channelRead0(ChannelHandlerContext ctx, RemotingCommand msg) throws Exception { + System.out.println("Server服务端:" + new String(msg.getBody())); + RemotingCommand response = new RemotingCommand(); + response.setBody("接受到请求了".getBytes()); + response.setCode(0); + ctx.writeAndFlush(response); + } + } +} diff --git a/Spring-Netty/src/main/java/com/bruis/learnnetty/netty/demo01/NettyClient.java b/Spring-Netty/src/main/java/com/bruis/learnnetty/netty/quickstart/NettyClient.java similarity index 97% rename from Spring-Netty/src/main/java/com/bruis/learnnetty/netty/demo01/NettyClient.java rename to Spring-Netty/src/main/java/com/bruis/learnnetty/netty/quickstart/NettyClient.java index f429e99..db6994b 100644 --- a/Spring-Netty/src/main/java/com/bruis/learnnetty/netty/demo01/NettyClient.java +++ b/Spring-Netty/src/main/java/com/bruis/learnnetty/netty/quickstart/NettyClient.java @@ -1,4 +1,4 @@ -package com.bruis.learnnetty.netty.demo01; +package com.bruis.learnnetty.netty.quickstart; import io.netty.bootstrap.Bootstrap; import io.netty.channel.ChannelFuture; diff --git a/Spring-Netty/src/main/java/com/bruis/learnnetty/netty/demo01/NettyClientHandler.java b/Spring-Netty/src/main/java/com/bruis/learnnetty/netty/quickstart/NettyClientHandler.java similarity index 98% rename from Spring-Netty/src/main/java/com/bruis/learnnetty/netty/demo01/NettyClientHandler.java rename to Spring-Netty/src/main/java/com/bruis/learnnetty/netty/quickstart/NettyClientHandler.java index f777499..976fdda 100644 --- a/Spring-Netty/src/main/java/com/bruis/learnnetty/netty/demo01/NettyClientHandler.java +++ b/Spring-Netty/src/main/java/com/bruis/learnnetty/netty/quickstart/NettyClientHandler.java @@ -1,4 +1,4 @@ -package com.bruis.learnnetty.netty.demo01; +package com.bruis.learnnetty.netty.quickstart; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; diff --git a/Spring-Netty/src/main/java/com/bruis/learnnetty/netty/demo01/NettyServerHandler.java b/Spring-Netty/src/main/java/com/bruis/learnnetty/netty/quickstart/NettyServerHandler.java similarity index 96% rename from Spring-Netty/src/main/java/com/bruis/learnnetty/netty/demo01/NettyServerHandler.java rename to Spring-Netty/src/main/java/com/bruis/learnnetty/netty/quickstart/NettyServerHandler.java index 9d131fa..b7a9d04 100644 --- a/Spring-Netty/src/main/java/com/bruis/learnnetty/netty/demo01/NettyServerHandler.java +++ b/Spring-Netty/src/main/java/com/bruis/learnnetty/netty/quickstart/NettyServerHandler.java @@ -1,11 +1,8 @@ -package com.bruis.learnnetty.netty.demo01; +package com.bruis.learnnetty.netty.quickstart; -import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; -import io.netty.channel.Channel; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandlerAdapter; -import io.netty.channel.ChannelPipeline; import io.netty.util.CharsetUtil; import java.util.concurrent.TimeUnit; diff --git a/Spring-Netty/src/main/java/com/bruis/learnnetty/netty/demo01/Server.java b/Spring-Netty/src/main/java/com/bruis/learnnetty/netty/quickstart/Server.java similarity index 98% rename from Spring-Netty/src/main/java/com/bruis/learnnetty/netty/demo01/Server.java rename to Spring-Netty/src/main/java/com/bruis/learnnetty/netty/quickstart/Server.java index a3f99d2..3adeabc 100644 --- a/Spring-Netty/src/main/java/com/bruis/learnnetty/netty/demo01/Server.java +++ b/Spring-Netty/src/main/java/com/bruis/learnnetty/netty/quickstart/Server.java @@ -1,4 +1,4 @@ -package com.bruis.learnnetty.netty.demo01; +package com.bruis.learnnetty.netty.quickstart; import io.netty.bootstrap.ServerBootstrap; import io.netty.channel.ChannelFuture; diff --git a/Spring-Netty/src/main/java/com/bruis/learnnetty/nio/NIOClient.java b/Spring-Netty/src/main/java/com/bruis/learnnetty/nio/NIOClient.java new file mode 100644 index 0000000..92178e8 --- /dev/null +++ b/Spring-Netty/src/main/java/com/bruis/learnnetty/nio/NIOClient.java @@ -0,0 +1,41 @@ +package com.bruis.learnnetty.nio; + +import java.net.InetSocketAddress; +import java.nio.ByteBuffer; +import java.nio.channels.SocketChannel; + +/** + * @author LuoHaiYang + */ +public class NIOClient { + + public static void main(String[] args) throws Exception { + + // 获取一个SocketChannel + SocketChannel socketChannel = SocketChannel.open(); + + // 设置非阻塞 + socketChannel.configureBlocking(false); + + // 提供服务端的ip和端口 + InetSocketAddress inetSocketAddress = new InetSocketAddress("127.0.0.1", 6666); + + // 连接服务器 + if (!socketChannel.connect(inetSocketAddress)) { + while (!socketChannel.finishConnect()) { + System.out.println("因为连接需要时间, 客户端不会阻塞,可以做其他工作了..."); + } + } + + // 如果连接成功,就发送数据 + String str = "hello, bruis~"; + + // 把字节数组包装成buffer + ByteBuffer buffer = ByteBuffer.wrap(str.getBytes()); + + // 发送数据,将buffer数据写入channel + socketChannel.write(buffer); + + System.in.read(); + } +} diff --git a/Spring-Netty/src/main/java/com/bruis/learnnetty/nio/NIOServer.java b/Spring-Netty/src/main/java/com/bruis/learnnetty/nio/NIOServer.java new file mode 100644 index 0000000..15ec3f2 --- /dev/null +++ b/Spring-Netty/src/main/java/com/bruis/learnnetty/nio/NIOServer.java @@ -0,0 +1,94 @@ +package com.bruis.learnnetty.nio; + +import java.net.InetSocketAddress; +import java.nio.ByteBuffer; +import java.nio.channels.SelectionKey; +import java.nio.channels.Selector; +import java.nio.channels.ServerSocketChannel; +import java.nio.channels.SocketChannel; +import java.util.Iterator; +import java.util.Set; + +/** + * @author LuoHaiYang + */ +public class NIOServer { + + public static void main(String[] args) throws Exception { + + // 创建ServerSocketChannel -> ServerSocket + ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); + + // 得到一个Selector对象 + Selector selector = Selector.open(); + + // 绑定一个端口:6666,在服务端监听 + serverSocketChannel.socket().bind(new InetSocketAddress(6666)); + + // 设置为非阻塞 + serverSocketChannel.configureBlocking(false); + + // 把ServerSocketChannel注册到selector, 事件为:OP_ACCEPT + SelectionKey registerSelectionKey = serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); + System.out.println("Register to selector, selectionKey is OP_ACCEPT, return selectionKey = " + registerSelectionKey + ", 注册后selectionKey 数量 = " + selector.keys().size()); + + // 循环等待客户端连接 + while (true) { + // 这里我们等待1秒, 如果没有事件发生,返回 + if (selector.select(1000) == 0) { + // 没有事件发生 + System.out.println("服务器等待了1秒,无连接"); + continue; + } + + // 如果返回结果 > 0, 就获取到相关的selectionKey集合 + // 1. 如果返回 > 0,表示已经获取到关注的事件 + // 2. selector.selectedKeys() 返回关注事件的集合 + // 通过selectionKeys反向获取通道 + Set selectionKeys = selector.selectedKeys(); + + // 通过迭代器遍历Set + Iterator keyIterator = selectionKeys.iterator(); + + while (keyIterator.hasNext()) { + // 获取Selectionkey + SelectionKey key = keyIterator.next(); + // 根据key对应的通道发生的事件做响应处理 + if (key.isAcceptable()) { + // 如果key是OP_ACCEPT,有新的客户端连接 + // 通过ServerSocketChannel生成一个socketChannel + SocketChannel socketChannel = serverSocketChannel.accept(); + + System.out.println("Register to selector, selectionKey is OP_ACCEPT, return selectionKey = " + registerSelectionKey + ", 注册后selectionKey 数量 = " + selector.keys().size()); + + // 将SocketChannel设置为非阻塞 + socketChannel.configureBlocking(false); + + // 将socketChannel注册到selector,关注事件为OP_READ,同时给socketChannel关联一个buffer + socketChannel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024)); + + } + + System.out.println("isReadable = " + key.isReadable() + ", isWritable = " + key.isWritable() + ", isValid = " + key.isValid()); + + if (key.isReadable()) { + // 如果key是OP_READ + // 通过key反向获取到对应channel + SocketChannel channel = (SocketChannel) key.channel(); + + // 获取该channel关联的buffer + ByteBuffer buffer = (ByteBuffer)key.attachment(); + + // 从buffer中读取数据到channel中 + channel.read(buffer); + + System.out.println("from 客户端 " + new String(buffer.array())); + } + + // 手动从集合中移动当前的selectionKey,防止重复操作 + keyIterator.remove(); + } + + } + } +} diff --git a/Spring-Netty/src/main/java/com/bruis/learnnetty/rpc/client/ClientHandler.java b/Spring-Netty/src/main/java/com/bruis/learnnetty/rpc/client/ClientHandler.java new file mode 100644 index 0000000..2ceb2a8 --- /dev/null +++ b/Spring-Netty/src/main/java/com/bruis/learnnetty/rpc/client/ClientHandler.java @@ -0,0 +1,19 @@ +package com.bruis.learnnetty.rpc.client; + +import com.alibaba.fastjson.JSONObject; +import com.bruis.learnnetty.rpc.utils.RequestFuture; +import com.bruis.learnnetty.rpc.utils.Response; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; + +/** + * @author lhy + * @date 2022/2/11 + */ +public class ClientHandler extends ChannelInboundHandlerAdapter { + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + Response response = JSONObject.parseObject(msg.toString(), Response.class); + RequestFuture.received(response); + } +} diff --git a/Spring-Netty/src/main/java/com/bruis/learnnetty/rpc/client/NettyClient.java b/Spring-Netty/src/main/java/com/bruis/learnnetty/rpc/client/NettyClient.java new file mode 100644 index 0000000..e80d8c1 --- /dev/null +++ b/Spring-Netty/src/main/java/com/bruis/learnnetty/rpc/client/NettyClient.java @@ -0,0 +1,92 @@ +package com.bruis.learnnetty.rpc.client; + +import com.alibaba.fastjson.JSONObject; +import com.bruis.learnnetty.rpc.utils.RequestFuture; +import io.netty.bootstrap.Bootstrap; +import io.netty.buffer.PooledByteBufAllocator; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelOption; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.nio.NioSocketChannel; +import io.netty.handler.codec.LengthFieldBasedFrameDecoder; +import io.netty.handler.codec.LengthFieldPrepender; +import io.netty.handler.codec.string.StringDecoder; +import io.netty.handler.codec.string.StringEncoder; +import org.springframework.util.StringUtils; + +import java.nio.charset.Charset; + +/** + * @author lhy + * @date 2022/2/16 + */ +public class NettyClient { + public static EventLoopGroup group = null; + public static Bootstrap bootstrap = null; + public static ChannelFuture future = null; + static { + bootstrap = new Bootstrap(); + group = new NioEventLoopGroup(); + bootstrap.channel(NioSocketChannel.class); + bootstrap.group(group); + bootstrap.option(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT); + final ClientHandler clientHandler = new ClientHandler(); + bootstrap.handler(new ChannelInitializer() { + @Override + protected void initChannel(NioSocketChannel ch) throws Exception { + ch.pipeline().addLast(new LengthFieldBasedFrameDecoder(Integer.MAX_VALUE, + 0, 4, 0, 4)); + ch.pipeline().addLast(new StringDecoder()); + ch.pipeline().addLast(clientHandler); + ch.pipeline().addLast(new LengthFieldPrepender(4, false)); + ch.pipeline().addLast(new StringEncoder(Charset.forName("utf-8"))); + } + }); + try { + future = bootstrap.connect("127.0.0.1", 8080).sync(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + + /** + * 说明:对于这个长连接的例子中,使用了静态化,即单链接、长连接,如果是多链接的话不可使用静态化,需使用线程池。 + * @param msg + * @return + */ + public Object sendRequest(Object msg, String path) { + try { + RequestFuture request = new RequestFuture(); + request.setRequest(msg); + request.setPath(path); + String requestStr = JSONObject.toJSONString(request); + future.channel().writeAndFlush(requestStr); + myselfPrint("我阻塞了", null); + return request.get(); + } catch (Exception e) { + e.printStackTrace(); + throw e; + } + } + public static void main(String[] args) { + NettyClient nettyClient = new NettyClient(); + for (int i = 0; i < 10; i++) { + Object result = nettyClient.sendRequest("hello-" + i, "getUserNameById"); + myselfPrint("拿到结果了", result); + } + } + + public static void myselfPrint(String description, Object value) { + StringBuilder builder = new StringBuilder(); + builder.append(Thread.currentThread().getName()); + if (!StringUtils.isEmpty(description)) { + builder.append("-").append(description); + } + if (!StringUtils.isEmpty(value)) { + builder.append("-").append(value); + } + System.out.println(builder.toString()); + } +} diff --git a/Spring-Netty/src/main/java/com/bruis/learnnetty/rpc/controller/UserController.java b/Spring-Netty/src/main/java/com/bruis/learnnetty/rpc/controller/UserController.java new file mode 100644 index 0000000..2df7483 --- /dev/null +++ b/Spring-Netty/src/main/java/com/bruis/learnnetty/rpc/controller/UserController.java @@ -0,0 +1,17 @@ +package com.bruis.learnnetty.rpc.controller; + +import com.bruis.learnnetty.rpc.utils.Remote; +import org.springframework.stereotype.Controller; + +/** + * @author lhy + * @date 2022/2/17 + */ +@Controller +public class UserController { + @Remote(value = "getUserNameById") + public Object getUserNameById(String userId) { + System.out.println(Thread.currentThread().getName() + "-> 接受到请求:" + userId); + return "做了业务处理了,结果是用户编号userId为" + userId + "的用户姓名为张三"; + } +} diff --git a/Spring-Netty/src/main/java/com/bruis/learnnetty/rpc/server/ApplicationMain.java b/Spring-Netty/src/main/java/com/bruis/learnnetty/rpc/server/ApplicationMain.java new file mode 100644 index 0000000..b8fbb85 --- /dev/null +++ b/Spring-Netty/src/main/java/com/bruis/learnnetty/rpc/server/ApplicationMain.java @@ -0,0 +1,38 @@ +package com.bruis.learnnetty.rpc.server; + +import org.springframework.context.annotation.AnnotationConfigApplicationContext; + +/** + * @author lhy + * @date 2022/2/17 + */ +public class ApplicationMain { + + private static volatile boolean running = true; + + public static void main(String[] args) { + try { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext("com.bruis.learnnetty.rpc"); + Runtime.getRuntime().addShutdownHook(new Thread(() -> { + try { + context.stop(); + } catch (Exception e) {} + + synchronized (ApplicationMain.class) { + running = false; + ApplicationMain.class.notify(); + } + })); + context.start(); + } catch (Exception e) { + e.printStackTrace(); + System.exit(1); + } + System.out.println("服务器已启动"); + synchronized (ApplicationMain.class) { + try { + ApplicationMain.class.wait(); + } catch (Exception e) {} + } + } +} diff --git a/Spring-Netty/src/main/java/com/bruis/learnnetty/rpc/server/InitLoadRemoteMethod.java b/Spring-Netty/src/main/java/com/bruis/learnnetty/rpc/server/InitLoadRemoteMethod.java new file mode 100644 index 0000000..6942514 --- /dev/null +++ b/Spring-Netty/src/main/java/com/bruis/learnnetty/rpc/server/InitLoadRemoteMethod.java @@ -0,0 +1,55 @@ +package com.bruis.learnnetty.rpc.server; + +import com.bruis.learnnetty.rpc.utils.Mediator; +import com.bruis.learnnetty.rpc.utils.Remote; +import org.springframework.context.ApplicationListener; +import org.springframework.context.event.ContextRefreshedEvent; +import org.springframework.core.Ordered; +import org.springframework.stereotype.Component; +import org.springframework.stereotype.Controller; + +import java.lang.reflect.Method; +import java.util.Map; + +/** + * @author lhy + * @date 2022/2/17 + */ +@Component +public class InitLoadRemoteMethod implements ApplicationListener, Ordered { + + @Override + public void onApplicationEvent(ContextRefreshedEvent context) { + // 获取Spring容器中带有@Controller的注解类 + Map controllerBeans = context.getApplicationContext() + .getBeansWithAnnotation(Controller.class); + for (String beanName : controllerBeans.keySet()) { + Object beanObj = controllerBeans.get(beanName); + // 获取这个bean的方法集合 + Method[] methods = beanObj.getClass().getMethods(); + for (Method method : methods) { + // 判断这个方法是否带有@Remote注解 + if (method.isAnnotationPresent(Remote.class)) { + Remote remote = method.getAnnotation(Remote.class); + // 注解的值 + String remoteValue = remote.value(); + // 缓存这个类 + Mediator.MethodBean methodBean = new Mediator.MethodBean(); + methodBean.setBean(beanObj); + methodBean.setMethod(method); + // @Remote的value值作为key,MethodBean作为value + Mediator.methodBeans.put(remoteValue, methodBean); + } + } + } + } + + /** + * 值越小优先级越高 + * @return + */ + @Override + public int getOrder() { + return -1; + } +} diff --git a/Spring-Netty/src/main/java/com/bruis/learnnetty/rpc/server/NettyApplicationListener.java b/Spring-Netty/src/main/java/com/bruis/learnnetty/rpc/server/NettyApplicationListener.java new file mode 100644 index 0000000..dd63c71 --- /dev/null +++ b/Spring-Netty/src/main/java/com/bruis/learnnetty/rpc/server/NettyApplicationListener.java @@ -0,0 +1,23 @@ +package com.bruis.learnnetty.rpc.server; + +import org.springframework.context.ApplicationListener; +import org.springframework.context.event.ContextRefreshedEvent; +import org.springframework.stereotype.Component; + +/** + * @author lhy + * @date 2022/2/17 + */ +@Component +public class NettyApplicationListener implements ApplicationListener { + @Override + public void onApplicationEvent(ContextRefreshedEvent event) { + // 开启额外线程启动Netty服务 + new Thread() { + @Override + public void run() { + NettyServer.start(); + } + }.start(); + } +} diff --git a/Spring-Netty/src/main/java/com/bruis/learnnetty/rpc/server/NettyServer.java b/Spring-Netty/src/main/java/com/bruis/learnnetty/rpc/server/NettyServer.java new file mode 100644 index 0000000..cc8bfee --- /dev/null +++ b/Spring-Netty/src/main/java/com/bruis/learnnetty/rpc/server/NettyServer.java @@ -0,0 +1,51 @@ +package com.bruis.learnnetty.rpc.server; + +import io.netty.bootstrap.ServerBootstrap; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelOption; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.SocketChannel; +import io.netty.channel.socket.nio.NioServerSocketChannel; +import io.netty.handler.codec.LengthFieldBasedFrameDecoder; +import io.netty.handler.codec.LengthFieldPrepender; +import io.netty.handler.codec.string.StringDecoder; +import io.netty.handler.codec.string.StringEncoder; + +/** + * 基于短连接的Netty服务端 + * + * @author lhy + * @date 2022/2/11 + */ +public class NettyServer { + public static void start() { + EventLoopGroup bossGroup = new NioEventLoopGroup(1); + EventLoopGroup workerGroup = new NioEventLoopGroup(); + try { + ServerBootstrap serverBootstrap = new ServerBootstrap(); + serverBootstrap.group(bossGroup, workerGroup); + serverBootstrap.channel(NioServerSocketChannel.class); + + serverBootstrap.option(ChannelOption.SO_BACKLOG, 128) + .childHandler(new ChannelInitializer() { + @Override + protected void initChannel(SocketChannel ch) throws Exception { + ch.pipeline().addLast(new LengthFieldBasedFrameDecoder(Integer.MAX_VALUE, 0, 4, 0, 4)) + .addLast(new StringDecoder()) + .addLast(new ServerHandler()) + .addLast(new LengthFieldPrepender(4, false)) + .addLast(new StringEncoder()); + } + }); + ChannelFuture future = serverBootstrap.bind(8080).sync(); + future.channel().closeFuture().sync(); + } catch (Exception e) { + e.printStackTrace(); + } finally { + bossGroup.shutdownGracefully(); + workerGroup.shutdownGracefully(); + } + } +} diff --git a/Spring-Netty/src/main/java/com/bruis/learnnetty/rpc/server/ServerHandler.java b/Spring-Netty/src/main/java/com/bruis/learnnetty/rpc/server/ServerHandler.java new file mode 100644 index 0000000..7daa7ed --- /dev/null +++ b/Spring-Netty/src/main/java/com/bruis/learnnetty/rpc/server/ServerHandler.java @@ -0,0 +1,29 @@ +package com.bruis.learnnetty.rpc.server; + +import com.alibaba.fastjson.JSONObject; +import com.bruis.learnnetty.rpc.utils.Mediator; +import com.bruis.learnnetty.rpc.utils.RequestFuture; +import com.bruis.learnnetty.rpc.utils.Response; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; + +/** + * @author lhy + * @date 2022/2/11 + */ +@ChannelHandler.Sharable +public class ServerHandler extends ChannelInboundHandlerAdapter { + /** + * 接受客户端发送过来的请求 + * @param ctx + * @param msg + * @throws Exception + */ + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + RequestFuture request = JSONObject.parseObject(msg.toString(), RequestFuture.class); + Response response = Mediator.process(request); + ctx.channel().writeAndFlush(JSONObject.toJSONString(response)); + } +} diff --git a/Spring-Netty/src/main/java/com/bruis/learnnetty/rpc/utils/Mediator.java b/Spring-Netty/src/main/java/com/bruis/learnnetty/rpc/utils/Mediator.java new file mode 100644 index 0000000..99ccf31 --- /dev/null +++ b/Spring-Netty/src/main/java/com/bruis/learnnetty/rpc/utils/Mediator.java @@ -0,0 +1,80 @@ +package com.bruis.learnnetty.rpc.utils; + +import com.alibaba.fastjson.JSONArray; +import com.alibaba.fastjson.JSONObject; + +import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * + * 存储RPC中的映射以及方法Bean + * + * @author lhy + * @date 2022/2/17 + */ +public class Mediator { + + public static Map methodBeans; + + static { + methodBeans = new HashMap<>(); + } + + public static Response process(RequestFuture future) { + Response response = new Response(); + try { + String path = future.getPath(); + MethodBean methodBean = methodBeans.get(path); + if (null != methodBean) { + Object bean = methodBean.getBean(); + Method method = methodBean.getMethod(); + Object request = future.getRequest(); + Class[] parameterTypes = method.getParameterTypes(); + // 此处只支持一个参数,所以写死固定0为索引 + Class parameterType = parameterTypes[0]; + Object param = null; + // 如果参数是List类型 + if (parameterType.isAssignableFrom(List.class)) { + param = JSONArray.parseArray(JSONArray.toJSONString(request), parameterType); + } else if (parameterType.getName().equalsIgnoreCase(String.class.getName())) { + param = request; + } else { + param = JSONObject.parseObject(JSONObject.toJSONString(request), parameterType); + } + // 反射调用方法 + Object result = method.invoke(bean, param); + response.setResult(result); + } + } catch (Exception e) { + e.printStackTrace(); + } + response.setId(future.getId()); + return response; + } + + public static class MethodBean { + + private Object bean; + + private Method method; + + public Object getBean() { + return bean; + } + + public void setBean(Object bean) { + this.bean = bean; + } + + public Method getMethod() { + return method; + } + + public void setMethod(Method method) { + this.method = method; + } + } +} diff --git a/Spring-Netty/src/main/java/com/bruis/learnnetty/rpc/utils/Remote.java b/Spring-Netty/src/main/java/com/bruis/learnnetty/rpc/utils/Remote.java new file mode 100644 index 0000000..c173567 --- /dev/null +++ b/Spring-Netty/src/main/java/com/bruis/learnnetty/rpc/utils/Remote.java @@ -0,0 +1,16 @@ +package com.bruis.learnnetty.rpc.utils; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * @author lhy + * @date 2022/2/17 + */ +@Target({ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface Remote { + String value(); +} diff --git a/Spring-Netty/src/main/java/com/bruis/learnnetty/rpc/utils/RequestFuture.java b/Spring-Netty/src/main/java/com/bruis/learnnetty/rpc/utils/RequestFuture.java new file mode 100644 index 0000000..340f30a --- /dev/null +++ b/Spring-Netty/src/main/java/com/bruis/learnnetty/rpc/utils/RequestFuture.java @@ -0,0 +1,143 @@ +package com.bruis.learnnetty.rpc.utils; + +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.locks.Condition; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +/** + * 模拟客户端请求类,用于构建请求对象 + * + * @author lhy + * @date 2022/2/10 + */ +public class RequestFuture { + public static Map futures = new ConcurrentHashMap<>(); + private final Lock lock = new ReentrantLock(); + private final Condition condition = lock.newCondition(); + private long id; + /** + * 请求参数 + */ + private Object request; + /** + * 响应结果 + */ + private Object result; + /** + * 超时时间 + */ + private long timeout = 5000; + /** + * 请求路径 + */ + private String path; + + public static final AtomicLong aid = new AtomicLong(); + + public RequestFuture() { + id = aid.incrementAndGet(); + addFuture(this); + } + + /** + * 把请求放入本地缓存中 + * @param future + */ + public static void addFuture(RequestFuture future) { + futures.put(future.getId(), future); + } + + /** + * 同步获取响应结果 + * @return + */ + public Object get() { + lock.lock(); + try { + while (this.result == null) { + try { + // 主线程默认等待5s,然后查看下结果 + condition.await(timeout, TimeUnit.MILLISECONDS); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + } finally { + lock.unlock(); + } + return this.result; + } + + /** + * 异步线程将结果返回主线程 + * @param result + */ + public static void received(Response result) { + RequestFuture future = futures.remove(result.getId()); + if (null != future) { + future.setResult(result.getResult()); + } + /** + * 通知主线程 + */ + Objects.requireNonNull(future, "RequestFuture").getLock().lock(); + try { + future.getCondition().signalAll(); + } finally { + Objects.requireNonNull(future, "RequestFuture").getLock().unlock(); + } + } + + public long getId() { + return id; + } + + public void setId(long id) { + this.id = id; + } + + public Object getRequest() { + return request; + } + + public void setRequest(Object request) { + this.request = request; + } + + public Object getResult() { + return result; + } + + public void setResult(Object result) { + this.result = result; + } + + public long getTimeout() { + return timeout; + } + + public void setTimeout(long timeout) { + this.timeout = timeout; + } + + public Lock getLock() { + return lock; + } + + public Condition getCondition() { + return condition; + } + + public String getPath() { + return path; + } + + public void setPath(String path) { + this.path = path; + } +} diff --git a/Spring-Netty/src/main/java/com/bruis/learnnetty/rpc/utils/Response.java b/Spring-Netty/src/main/java/com/bruis/learnnetty/rpc/utils/Response.java new file mode 100644 index 0000000..ac5478f --- /dev/null +++ b/Spring-Netty/src/main/java/com/bruis/learnnetty/rpc/utils/Response.java @@ -0,0 +1,28 @@ +package com.bruis.learnnetty.rpc.utils; + +/** + * 响应结果类 + * + * @author lhy + * @date 2022/2/10 + */ +public class Response { + private long id; + private Object result; + + public long getId() { + return id; + } + + public void setId(long id) { + this.id = id; + } + + public Object getResult() { + return result; + } + + public void setResult(Object result) { + this.result = result; + } +} diff --git a/Spring-Netty/src/main/java/com/bruis/learnnetty/thread/reentranlock/FutureMain.java b/Spring-Netty/src/main/java/com/bruis/learnnetty/thread/reentranlock/FutureMain.java new file mode 100644 index 0000000..d0de69c --- /dev/null +++ b/Spring-Netty/src/main/java/com/bruis/learnnetty/thread/reentranlock/FutureMain.java @@ -0,0 +1,64 @@ +package com.bruis.learnnetty.thread.reentranlock; + +import java.util.ArrayList; +import java.util.List; + +/** + * 模拟Netty通讯过程 + * 主线程,获取子线程的结果 + * + * @author lhy + * @date 2022/2/10 + */ +public class FutureMain { + private static List reqs = new ArrayList<>(); + public static void main(String[] args) { + mockClient(); + mockServer(); + } + + /** + * 模拟服务端 接受结果 + */ + private static void mockServer() { + for (RequestFuture req : reqs) { + /** + * 主线程获取结果 + */ + Object result = req.get(); + System.out.println("服务端接受到响应结果:" + result.toString()); + } + } + /** + * 模拟客户端 发送请求 + */ + private static void mockClient() { + for (int i = 0; i < 100; i++) { + long id = i; + RequestFuture req = new RequestFuture(); + req.setId(id); + req.setRequest("hello world"); + /** + * 把请求缓存起来 + */ + RequestFuture.addFuture(req); + /** + * 将请求放入到请求列表中 + */ + reqs.add(req); + sendMsg(req); + SubThread subThread = new SubThread(req); + /** + * 开启子线程 + */ + subThread.start(); + } + } + /** + * 模拟请求处理 + * @param req + */ + private static void sendMsg(RequestFuture req) { + System.out.println("客户端发送数据,请求id为===============" + req.getId()); + } +} diff --git a/Spring-Netty/src/main/java/com/bruis/learnnetty/thread/reentranlock/RequestFuture.java b/Spring-Netty/src/main/java/com/bruis/learnnetty/thread/reentranlock/RequestFuture.java new file mode 100644 index 0000000..1fd7dec --- /dev/null +++ b/Spring-Netty/src/main/java/com/bruis/learnnetty/thread/reentranlock/RequestFuture.java @@ -0,0 +1,123 @@ +package com.bruis.learnnetty.thread.reentranlock; + +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.Condition; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +/** + * 模拟客户端请求类,用于构建请求对象 + * + * @author lhy + * @date 2022/2/10 + */ +public class RequestFuture { + public static Map futures = new ConcurrentHashMap<>(); + private final Lock lock = new ReentrantLock(); + private final Condition condition = lock.newCondition(); + private long id; + /** + * 请求参数 + */ + private Object request; + /** + * 响应结果 + */ + private Object result; + /** + * 超时时间 + */ + private long timeout = 5000; + + /** + * 把请求放入本地缓存中 + * @param future + */ + public static void addFuture(RequestFuture future) { + futures.put(future.getId(), future); + } + + /** + * 同步获取响应结果 + * @return + */ + public Object get() { + lock.lock(); + try { + while (this.result == null) { + try { + // 主线程默认等待5s,然后查看下结果 + condition.await(timeout, TimeUnit.MILLISECONDS); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + } finally { + lock.unlock(); + } + return this.result; + } + + /** + * 异步线程将结果返回主线程 + * @param result + */ + public static void received(Response result) { + RequestFuture future = futures.remove(result.getId()); + if (null != future) { + future.setResult(result.getResult()); + } + /** + * 通知主线程 + */ + Objects.requireNonNull(future, "RequestFuture").getLock().lock(); + try { + future.getCondition().signalAll(); + } finally { + Objects.requireNonNull(future, "RequestFuture").getLock().unlock(); + } + } + + public long getId() { + return id; + } + + public void setId(long id) { + this.id = id; + } + + public Object getRequest() { + return request; + } + + public void setRequest(Object request) { + this.request = request; + } + + public Object getResult() { + return result; + } + + public void setResult(Object result) { + this.result = result; + } + + public long getTimeout() { + return timeout; + } + + public void setTimeout(long timeout) { + this.timeout = timeout; + } + + public Lock getLock() { + return lock; + } + + public Condition getCondition() { + return condition; + } +} diff --git a/Spring-Netty/src/main/java/com/bruis/learnnetty/thread/reentranlock/Response.java b/Spring-Netty/src/main/java/com/bruis/learnnetty/thread/reentranlock/Response.java new file mode 100644 index 0000000..ae1852c --- /dev/null +++ b/Spring-Netty/src/main/java/com/bruis/learnnetty/thread/reentranlock/Response.java @@ -0,0 +1,28 @@ +package com.bruis.learnnetty.thread.reentranlock; + +/** + * 响应结果类 + * + * @author lhy + * @date 2022/2/10 + */ +public class Response { + private long id; + private Object result; + + public long getId() { + return id; + } + + public void setId(long id) { + this.id = id; + } + + public Object getResult() { + return result; + } + + public void setResult(Object result) { + this.result = result; + } +} diff --git a/Spring-Netty/src/main/java/com/bruis/learnnetty/thread/reentranlock/SubThread.java b/Spring-Netty/src/main/java/com/bruis/learnnetty/thread/reentranlock/SubThread.java new file mode 100644 index 0000000..9101e54 --- /dev/null +++ b/Spring-Netty/src/main/java/com/bruis/learnnetty/thread/reentranlock/SubThread.java @@ -0,0 +1,30 @@ +package com.bruis.learnnetty.thread.reentranlock; + +/** + * 子线程,用于模拟服务端处理 + * + * @author lhy + * @date 2022/2/10 + */ +public class SubThread extends Thread { + + private RequestFuture request; + + public SubThread(RequestFuture request) { + this.request = request; + } + + @Override + public void run() { + Response response = new Response(); + response.setId(request.getId()); + response.setResult("服务端响应了结果,线程id: " + Thread.currentThread().getId() + ", 请求id:" + response.getId()); + // 子线程睡眠1s + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + RequestFuture.received(response); + } +} diff --git a/Spring-Netty/src/main/java/com/bruis/learnnetty/thread/synchronize/FutureMain.java b/Spring-Netty/src/main/java/com/bruis/learnnetty/thread/synchronize/FutureMain.java new file mode 100644 index 0000000..bd4ee93 --- /dev/null +++ b/Spring-Netty/src/main/java/com/bruis/learnnetty/thread/synchronize/FutureMain.java @@ -0,0 +1,64 @@ +package com.bruis.learnnetty.thread.synchronize; + +import java.util.ArrayList; +import java.util.List; + +/** + * 模拟Netty通讯过程 + * 主线程,获取子线程的结果 + * + * @author lhy + * @date 2022/2/10 + */ +public class FutureMain { + private static List reqs = new ArrayList<>(); + public static void main(String[] args) { + mockClient(); + mockServer(); + } + + /** + * 模拟服务端 接受结果 + */ + private static void mockServer() { + for (RequestFuture req : reqs) { + /** + * 主线程获取结果 + */ + Object result = req.get(); + System.out.println("服务端接受到响应结果:" + result.toString()); + } + } + /** + * 模拟客户端 发送请求 + */ + private static void mockClient() { + for (int i = 0; i < 100; i++) { + long id = i; + RequestFuture req = new RequestFuture(); + req.setId(id); + req.setRequest("hello world"); + /** + * 把请求缓存起来 + */ + RequestFuture.addFuture(req); + /** + * 将请求放入到请求列表中 + */ + reqs.add(req); + sendMsg(req); + SubThread subThread = new SubThread(req); + /** + * 开启子线程 + */ + subThread.start(); + } + } + /** + * 模拟请求处理 + * @param req + */ + private static void sendMsg(RequestFuture req) { + System.out.println("客户端发送数据,请求id为===============" + req.getId()); + } +} diff --git a/Spring-Netty/src/main/java/com/bruis/learnnetty/thread/synchronize/RequestFuture.java b/Spring-Netty/src/main/java/com/bruis/learnnetty/thread/synchronize/RequestFuture.java new file mode 100644 index 0000000..f1dca80 --- /dev/null +++ b/Spring-Netty/src/main/java/com/bruis/learnnetty/thread/synchronize/RequestFuture.java @@ -0,0 +1,103 @@ +package com.bruis.learnnetty.thread.synchronize; + +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; + +/** + * 模拟客户端请求类,用于构建请求对象 + * + * @author lhy + * @date 2022/2/10 + */ +public class RequestFuture { + public static Map futures = new ConcurrentHashMap<>(); + private long id; + /** + * 请求参数 + */ + private Object request; + /** + * 响应结果 + */ + private Object result; + /** + * 超时时间 + */ + private long timeout = 5000; + + /** + * 把请求放入本地缓存中 + * @param future + */ + public static void addFuture(RequestFuture future) { + futures.put(future.getId(), future); + } + + /** + * 同步获取响应结果 + * @return + */ + public Object get() { + synchronized (this) { + while (this.result == null) { + try { + // 主线程默认等待5s,然后查看下结果 + this.wait(timeout); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + } + return this.result; + } + + /** + * 异步线程将结果返回主线程 + * @param result + */ + public static void received(Response result) { + RequestFuture future = futures.remove(result.getId()); + if (null != future) { + future.setResult(result.getResult()); + } + /** + * 通知主线程 + */ + synchronized (Objects.requireNonNull(future, "RequestFuture")) { + future.notify(); + } + } + + public long getId() { + return id; + } + + public void setId(long id) { + this.id = id; + } + + public Object getRequest() { + return request; + } + + public void setRequest(Object request) { + this.request = request; + } + + public Object getResult() { + return result; + } + + public void setResult(Object result) { + this.result = result; + } + + public long getTimeout() { + return timeout; + } + + public void setTimeout(long timeout) { + this.timeout = timeout; + } +} diff --git a/Spring-Netty/src/main/java/com/bruis/learnnetty/thread/synchronize/Response.java b/Spring-Netty/src/main/java/com/bruis/learnnetty/thread/synchronize/Response.java new file mode 100644 index 0000000..c5dfc05 --- /dev/null +++ b/Spring-Netty/src/main/java/com/bruis/learnnetty/thread/synchronize/Response.java @@ -0,0 +1,28 @@ +package com.bruis.learnnetty.thread.synchronize; + +/** + * 响应结果类 + * + * @author lhy + * @date 2022/2/10 + */ +public class Response { + private long id; + private Object result; + + public long getId() { + return id; + } + + public void setId(long id) { + this.id = id; + } + + public Object getResult() { + return result; + } + + public void setResult(Object result) { + this.result = result; + } +} diff --git a/Spring-Netty/src/main/java/com/bruis/learnnetty/thread/synchronize/SubThread.java b/Spring-Netty/src/main/java/com/bruis/learnnetty/thread/synchronize/SubThread.java new file mode 100644 index 0000000..16e2a57 --- /dev/null +++ b/Spring-Netty/src/main/java/com/bruis/learnnetty/thread/synchronize/SubThread.java @@ -0,0 +1,31 @@ +package com.bruis.learnnetty.thread.synchronize; + +/** + * 子线程,用于模拟服务端处理 + * + * @author lhy + * @date 2022/2/10 + */ +public class SubThread extends Thread { + + private RequestFuture request; + + public SubThread(RequestFuture request) { + this.request = request; + } + + @Override + public void run() { + Response response = new Response(); + response.setId(request.getId()); + response.setResult("服务端响应了结果,线程id: " + Thread.currentThread().getId() + ", 请求id:" + response.getId()); + // 子线程睡眠1s + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + System.out.println(this + " -> 当前线程准备调用received: " + Thread.currentThread().getName()); + RequestFuture.received(response); + } +} diff --git a/Spring-Security/SpringSecurityDemo.iml b/Spring-Security/SpringSecurityDemo.iml new file mode 100644 index 0000000..3a7ce81 --- /dev/null +++ b/Spring-Security/SpringSecurityDemo.iml @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Spring-Security/pom.xml b/Spring-Security/pom.xml new file mode 100644 index 0000000..4bf388a --- /dev/null +++ b/Spring-Security/pom.xml @@ -0,0 +1,84 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 2.1.14.RELEASE + + + com.bruis + springsecurity + 1.0.0-SNAPSHOT + springsecurity + Learn SpringSecurity + + + 1.8 + + + + + + org.springframework.boot + spring-boot-starter + + + + + org.springframework.boot + spring-boot-starter-web + + + + org.projectlombok + lombok + true + + + + + org.springframework.boot + spring-boot-starter-security + + + + + io.springfox + springfox-swagger2 + 2.9.2 + + + + io.springfox + springfox-swagger-ui + 2.9.2 + + + + + io.jsonwebtoken + jjwt + 0.9.1 + + + + + com.alibaba + fastjson + 1.2.58 + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + diff --git a/Spring-Security/src/main/java/com/bruis/springsecurity/SpringsecurityApplication.java b/Spring-Security/src/main/java/com/bruis/springsecurity/SpringsecurityApplication.java new file mode 100644 index 0000000..044a27e --- /dev/null +++ b/Spring-Security/src/main/java/com/bruis/springsecurity/SpringsecurityApplication.java @@ -0,0 +1,13 @@ +package com.bruis.springsecurity; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class SpringsecurityApplication { + + public static void main(String[] args) { + SpringApplication.run(SpringsecurityApplication.class, args); + } + +} diff --git a/Spring-Security/src/main/java/com/bruis/springsecurity/config/CorsConfig.java b/Spring-Security/src/main/java/com/bruis/springsecurity/config/CorsConfig.java new file mode 100644 index 0000000..b5bfb3f --- /dev/null +++ b/Spring-Security/src/main/java/com/bruis/springsecurity/config/CorsConfig.java @@ -0,0 +1,27 @@ +package com.bruis.springsecurity.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.CorsRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +/** + * @author LuoHaiYang + */ +@Configuration +public class CorsConfig implements WebMvcConfigurer { + @Override + public void addCorsMappings(CorsRegistry registry) { + // 允许跨域访问的路径 + registry.addMapping("/**") + // 允许跨域访问的源 + .allowedOrigins("*") + // 允许请求方法 + .allowedMethods("POST", "GET", "PUT", "OPTIONS", "DELETE") + // 预检间隔时间 + .maxAge(1680000) + // 允许头部 + .allowedHeaders("*") + // 允许发送cookie + .allowCredentials(true); + } +} diff --git a/Spring-Security/src/main/java/com/bruis/springsecurity/config/SwaggerConfig.java b/Spring-Security/src/main/java/com/bruis/springsecurity/config/SwaggerConfig.java new file mode 100644 index 0000000..14477e6 --- /dev/null +++ b/Spring-Security/src/main/java/com/bruis/springsecurity/config/SwaggerConfig.java @@ -0,0 +1,64 @@ +package com.bruis.springsecurity.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import springfox.documentation.builders.ApiInfoBuilder; +import springfox.documentation.builders.ParameterBuilder; +import springfox.documentation.builders.PathSelectors; +import springfox.documentation.builders.RequestHandlerSelectors; +import springfox.documentation.schema.ModelRef; +import springfox.documentation.service.ApiInfo; +import springfox.documentation.service.Parameter; +import springfox.documentation.spi.DocumentationType; +import springfox.documentation.spring.web.plugins.Docket; +import springfox.documentation.swagger2.annotations.EnableSwagger2; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author LuoHaiYang + */ +@Configuration +@EnableSwagger2 +public class SwaggerConfig { + + public static final String AUTHORIZATION = "Authorization"; + public static final String MODELREF = "string"; + public static final String PARAMETERTYPE = "header"; + public static final String TITLE = "SpringSecurity学习"; + public static final String DESCRIPTION = "Learn more about SpringSecurity!"; + public static final String VERSION = "1.0"; + + @Bean + public Docket createRestApi() { + // 添加请求参数, 把token作为请求头参数 + ParameterBuilder builder = new ParameterBuilder(); + List parameters = new ArrayList<>(); + + builder.name(AUTHORIZATION) + .description("请求令牌") + .modelRef(new ModelRef(MODELREF)) + .parameterType(PARAMETERTYPE) + .required(false) + .build(); + + parameters.add(builder.build()); + + return new Docket(DocumentationType.SWAGGER_2) + .apiInfo(apiInfo()) + .select() + .apis(RequestHandlerSelectors.any()) + .paths(PathSelectors.any()) + .build() + .globalOperationParameters(parameters); + } + + private ApiInfo apiInfo() { + return new ApiInfoBuilder() + .title(TITLE) + .description(DESCRIPTION) + .version(VERSION) + .build(); + } +} diff --git a/Spring-Security/src/main/java/com/bruis/springsecurity/config/WebSecurityConfig.java b/Spring-Security/src/main/java/com/bruis/springsecurity/config/WebSecurityConfig.java new file mode 100644 index 0000000..22a00ee --- /dev/null +++ b/Spring-Security/src/main/java/com/bruis/springsecurity/config/WebSecurityConfig.java @@ -0,0 +1,83 @@ +package com.bruis.springsecurity.config; + +import com.bruis.springsecurity.utils.security.JwtAuthenticationFilter; +import com.bruis.springsecurity.utils.security.JwtAuthenticationProvider; +import com.bruis.springsecurity.utils.security.JwtLoginFilter; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.security.web.authentication.logout.HttpStatusReturningLogoutSuccessHandler; + +/** + * @author LuoHaiYang + */ +@Configuration +@EnableWebSecurity +@EnableGlobalMethodSecurity(prePostEnabled = true) +public class WebSecurityConfig extends WebSecurityConfigurerAdapter { + + @Autowired + private UserDetailsService userDetailsService; + + /** + * http请求拦截认证 + * @param http + * @throws Exception + */ + @Override + protected void configure(HttpSecurity http) throws Exception { + // 禁用 csrf, 由于使用的是JWT,我们这里不需要csrf + http.cors().and().csrf().disable() + .authorizeRequests() + // 跨域预检请求 + .antMatchers(HttpMethod.OPTIONS, "/**").permitAll() + // 登录URL + .antMatchers("/login").permitAll() + // swagger + .antMatchers("/swagger-ui.html").permitAll() + .antMatchers("/swagger-resources").permitAll() + .antMatchers("/v2/api-docs").permitAll() + .antMatchers("/webjars/springfox-swagger-ui/**").permitAll() + // 其他所有请求需要身份认证 + .anyRequest().authenticated(); + // 退出登录处理器 + http.logout().logoutSuccessHandler(new HttpStatusReturningLogoutSuccessHandler()); + // 开启登录认证流程过滤器,如果使用LoginController的login接口, 需要注释掉此过滤器,根据使用习惯二选一即可 + // http.addFilterBefore(new JwtLoginFilter(authenticationManager()), UsernamePasswordAuthenticationFilter.class); + // 访问控制时登录状态检查过滤器 + // http.addFilterBefore(new JwtAuthenticationFilter(authenticationManager()), UsernamePasswordAuthenticationFilter.class); + + } + + @Override + protected void configure(AuthenticationManagerBuilder auth) throws Exception { + // 只用系统默认的身份认证组件,并配上passwordEncoder + // auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder()); + + // 使用自定义登录身份认证组件 + auth.authenticationProvider(new JwtAuthenticationProvider(userDetailsService)); + } + + @Bean + @Override + public AuthenticationManager authenticationManager() throws Exception { + return super.authenticationManager(); + } + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + +} diff --git a/Spring-Security/src/main/java/com/bruis/springsecurity/controller/LoginController.java b/Spring-Security/src/main/java/com/bruis/springsecurity/controller/LoginController.java new file mode 100644 index 0000000..fddaa44 --- /dev/null +++ b/Spring-Security/src/main/java/com/bruis/springsecurity/controller/LoginController.java @@ -0,0 +1,34 @@ +package com.bruis.springsecurity.controller; + +import com.bruis.springsecurity.model.HttpResult; +import com.bruis.springsecurity.model.LoginRequestParam; +import com.bruis.springsecurity.utils.security.JwtAuthenticationToken; +import com.bruis.springsecurity.utils.security.SecurityUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +import javax.servlet.http.HttpServletRequest; +import java.io.IOException; + +/** + * @author LuoHaiYang + */ +@RestController +public class LoginController { + + @Autowired + AuthenticationManager authenticationManager; + + @PostMapping("/login") + public HttpResult login(@RequestBody LoginRequestParam loginRequestParam, HttpServletRequest request) throws IOException { + String username = loginRequestParam.getUsername(); + String password = loginRequestParam.getPassword(); + + JwtAuthenticationToken token = SecurityUtils.login(request, username, password, authenticationManager); + + return HttpResult.ok(token); + } +} diff --git a/Spring-Security/src/main/java/com/bruis/springsecurity/controller/UserController.java b/Spring-Security/src/main/java/com/bruis/springsecurity/controller/UserController.java new file mode 100644 index 0000000..f83c150 --- /dev/null +++ b/Spring-Security/src/main/java/com/bruis/springsecurity/controller/UserController.java @@ -0,0 +1,35 @@ +package com.bruis.springsecurity.controller; + +import com.bruis.springsecurity.model.HttpResult; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * @author LuoHaiYang + */ +@RestController +@RequestMapping("user") +public class UserController { + + + @PreAuthorize("hasAuthority('sys:user:view')") + @GetMapping(value="/findAll") + public HttpResult findAll() { + return HttpResult.ok("the findAll service is called success."); + } + + @PreAuthorize("hasAuthority('sys:user:edit')") + @GetMapping(value="/edit") + public HttpResult edit() { + return HttpResult.ok("the edit service is called success."); + } + + @PreAuthorize("hasAuthority('sys:user:delete')") + @GetMapping(value="/delete") + public HttpResult delete() { + return HttpResult.ok("the delete service is called success."); + } + +} \ No newline at end of file diff --git a/Spring-Security/src/main/java/com/bruis/springsecurity/dto/User.java b/Spring-Security/src/main/java/com/bruis/springsecurity/dto/User.java new file mode 100644 index 0000000..7b2eae6 --- /dev/null +++ b/Spring-Security/src/main/java/com/bruis/springsecurity/dto/User.java @@ -0,0 +1,36 @@ +package com.bruis.springsecurity.dto; + +/** + * @author LuoHaiYang + */ +public class User { + private Long id; + + private String username; + + private String password; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } +} diff --git a/Spring-Security/src/main/java/com/bruis/springsecurity/model/HttpResult.java b/Spring-Security/src/main/java/com/bruis/springsecurity/model/HttpResult.java new file mode 100644 index 0000000..9a3d4d3 --- /dev/null +++ b/Spring-Security/src/main/java/com/bruis/springsecurity/model/HttpResult.java @@ -0,0 +1,65 @@ +package com.bruis.springsecurity.model; + +/** + * @author LuoHaiYang + */ +public class HttpResult { + private int code = 200; + private String msg; + private Object data; + + public static HttpResult error() { + return error(HttpStatus.SC_INTERNAL_SERVER_ERROR, "未知异常,请联系管理员"); + } + + public static HttpResult error(String msg) { + return error(HttpStatus.SC_INTERNAL_SERVER_ERROR, msg); + } + + public static HttpResult error(int code, String msg) { + HttpResult r = new HttpResult(); + r.setCode(code); + r.setMsg(msg); + return r; + } + + public static HttpResult ok(String msg) { + HttpResult r = new HttpResult(); + r.setMsg(msg); + return r; + } + + public static HttpResult ok(Object data) { + HttpResult r = new HttpResult(); + r.setData(data); + return r; + } + + public static HttpResult ok() { + return new HttpResult(); + } + + public int getCode() { + return code; + } + + public void setCode(int code) { + this.code = code; + } + + public String getMsg() { + return msg; + } + + public void setMsg(String msg) { + this.msg = msg; + } + + public Object getData() { + return data; + } + + public void setData(Object data) { + this.data = data; + } +} diff --git a/Spring-Security/src/main/java/com/bruis/springsecurity/model/HttpStatus.java b/Spring-Security/src/main/java/com/bruis/springsecurity/model/HttpStatus.java new file mode 100644 index 0000000..915c861 --- /dev/null +++ b/Spring-Security/src/main/java/com/bruis/springsecurity/model/HttpStatus.java @@ -0,0 +1,141 @@ +package com.bruis.springsecurity.model; + +/** + * @author LuoHaiYang + */ +public interface HttpStatus { + // --- 1xx Informational --- + + /** {@code 100 Continue} (HTTP/1.1 - RFC 2616) */ + public static final int SC_CONTINUE = 100; + /** {@code 101 Switching Protocols} (HTTP/1.1 - RFC 2616)*/ + public static final int SC_SWITCHING_PROTOCOLS = 101; + /** {@code 102 Processing} (WebDAV - RFC 2518) */ + public static final int SC_PROCESSING = 102; + + // --- 2xx Success --- + + /** {@code 200 OK} (HTTP/1.0 - RFC 1945) */ + public static final int SC_OK = 200; + /** {@code 201 Created} (HTTP/1.0 - RFC 1945) */ + public static final int SC_CREATED = 201; + /** {@code 202 Accepted} (HTTP/1.0 - RFC 1945) */ + public static final int SC_ACCEPTED = 202; + /** {@code 203 Non Authoritative Information} (HTTP/1.1 - RFC 2616) */ + public static final int SC_NON_AUTHORITATIVE_INFORMATION = 203; + /** {@code 204 No Content} (HTTP/1.0 - RFC 1945) */ + public static final int SC_NO_CONTENT = 204; + /** {@code 205 Reset Content} (HTTP/1.1 - RFC 2616) */ + public static final int SC_RESET_CONTENT = 205; + /** {@code 206 Partial Content} (HTTP/1.1 - RFC 2616) */ + public static final int SC_PARTIAL_CONTENT = 206; + /** + * {@code 207 Multi-Status} (WebDAV - RFC 2518) + * or + * {@code 207 Partial Update OK} (HTTP/1.1 - draft-ietf-http-v11-spec-rev-01?) + */ + public static final int SC_MULTI_STATUS = 207; + + // --- 3xx Redirection --- + + /** {@code 300 Mutliple Choices} (HTTP/1.1 - RFC 2616) */ + public static final int SC_MULTIPLE_CHOICES = 300; + /** {@code 301 Moved Permanently} (HTTP/1.0 - RFC 1945) */ + public static final int SC_MOVED_PERMANENTLY = 301; + /** {@code 302 Moved Temporarily} (Sometimes {@code Found}) (HTTP/1.0 - RFC 1945) */ + public static final int SC_MOVED_TEMPORARILY = 302; + /** {@code 303 See Other} (HTTP/1.1 - RFC 2616) */ + public static final int SC_SEE_OTHER = 303; + /** {@code 304 Not Modified} (HTTP/1.0 - RFC 1945) */ + public static final int SC_NOT_MODIFIED = 304; + /** {@code 305 Use Proxy} (HTTP/1.1 - RFC 2616) */ + public static final int SC_USE_PROXY = 305; + /** {@code 307 Temporary Redirect} (HTTP/1.1 - RFC 2616) */ + public static final int SC_TEMPORARY_REDIRECT = 307; + + // --- 4xx Client Error --- + + /** {@code 400 Bad Request} (HTTP/1.1 - RFC 2616) */ + public static final int SC_BAD_REQUEST = 400; + /** {@code 401 Unauthorized} (HTTP/1.0 - RFC 1945) */ + public static final int SC_UNAUTHORIZED = 401; + /** {@code 402 Payment Required} (HTTP/1.1 - RFC 2616) */ + public static final int SC_PAYMENT_REQUIRED = 402; + /** {@code 403 Forbidden} (HTTP/1.0 - RFC 1945) */ + public static final int SC_FORBIDDEN = 403; + /** {@code 404 Not Found} (HTTP/1.0 - RFC 1945) */ + public static final int SC_NOT_FOUND = 404; + /** {@code 405 Method Not Allowed} (HTTP/1.1 - RFC 2616) */ + public static final int SC_METHOD_NOT_ALLOWED = 405; + /** {@code 406 Not Acceptable} (HTTP/1.1 - RFC 2616) */ + public static final int SC_NOT_ACCEPTABLE = 406; + /** {@code 407 Proxy Authentication Required} (HTTP/1.1 - RFC 2616)*/ + public static final int SC_PROXY_AUTHENTICATION_REQUIRED = 407; + /** {@code 408 Request Timeout} (HTTP/1.1 - RFC 2616) */ + public static final int SC_REQUEST_TIMEOUT = 408; + /** {@code 409 Conflict} (HTTP/1.1 - RFC 2616) */ + public static final int SC_CONFLICT = 409; + /** {@code 410 Gone} (HTTP/1.1 - RFC 2616) */ + public static final int SC_GONE = 410; + /** {@code 411 Length Required} (HTTP/1.1 - RFC 2616) */ + public static final int SC_LENGTH_REQUIRED = 411; + /** {@code 412 Precondition Failed} (HTTP/1.1 - RFC 2616) */ + public static final int SC_PRECONDITION_FAILED = 412; + /** {@code 413 Request Entity Too Large} (HTTP/1.1 - RFC 2616) */ + public static final int SC_REQUEST_TOO_LONG = 413; + /** {@code 414 Request-URI Too Long} (HTTP/1.1 - RFC 2616) */ + public static final int SC_REQUEST_URI_TOO_LONG = 414; + /** {@code 415 Unsupported Media Type} (HTTP/1.1 - RFC 2616) */ + public static final int SC_UNSUPPORTED_MEDIA_TYPE = 415; + /** {@code 416 Requested Range Not Satisfiable} (HTTP/1.1 - RFC 2616) */ + public static final int SC_REQUESTED_RANGE_NOT_SATISFIABLE = 416; + /** {@code 417 Expectation Failed} (HTTP/1.1 - RFC 2616) */ + public static final int SC_EXPECTATION_FAILED = 417; + + /** + * Static constant for a 418 error. + * {@code 418 Unprocessable Entity} (WebDAV drafts?) + * or {@code 418 Reauthentication Required} (HTTP/1.1 drafts?) + */ + // not used + // public static final int SC_UNPROCESSABLE_ENTITY = 418; + + /** + * Static constant for a 419 error. + * {@code 419 Insufficient Space on Resource} + * (WebDAV - draft-ietf-webdav-protocol-05?) + * or {@code 419 Proxy Reauthentication Required} + * (HTTP/1.1 drafts?) + */ + public static final int SC_INSUFFICIENT_SPACE_ON_RESOURCE = 419; + /** + * Static constant for a 420 error. + * {@code 420 Method Failure} + * (WebDAV - draft-ietf-webdav-protocol-05?) + */ + public static final int SC_METHOD_FAILURE = 420; + /** {@code 422 Unprocessable Entity} (WebDAV - RFC 2518) */ + public static final int SC_UNPROCESSABLE_ENTITY = 422; + /** {@code 423 Locked} (WebDAV - RFC 2518) */ + public static final int SC_LOCKED = 423; + /** {@code 424 Failed Dependency} (WebDAV - RFC 2518) */ + public static final int SC_FAILED_DEPENDENCY = 424; + + // --- 5xx Server Error --- + + /** {@code 500 Server Error} (HTTP/1.0 - RFC 1945) */ + public static final int SC_INTERNAL_SERVER_ERROR = 500; + /** {@code 501 Not Implemented} (HTTP/1.0 - RFC 1945) */ + public static final int SC_NOT_IMPLEMENTED = 501; + /** {@code 502 Bad Gateway} (HTTP/1.0 - RFC 1945) */ + public static final int SC_BAD_GATEWAY = 502; + /** {@code 503 Service Unavailable} (HTTP/1.0 - RFC 1945) */ + public static final int SC_SERVICE_UNAVAILABLE = 503; + /** {@code 504 Gateway Timeout} (HTTP/1.1 - RFC 2616) */ + public static final int SC_GATEWAY_TIMEOUT = 504; + /** {@code 505 HTTP Version Not Supported} (HTTP/1.1 - RFC 2616) */ + public static final int SC_HTTP_VERSION_NOT_SUPPORTED = 505; + + /** {@code 507 Insufficient Storage} (WebDAV - RFC 2518) */ + public static final int SC_INSUFFICIENT_STORAGE = 507; +} diff --git a/Spring-Security/src/main/java/com/bruis/springsecurity/model/LoginRequestParam.java b/Spring-Security/src/main/java/com/bruis/springsecurity/model/LoginRequestParam.java new file mode 100644 index 0000000..91f9f92 --- /dev/null +++ b/Spring-Security/src/main/java/com/bruis/springsecurity/model/LoginRequestParam.java @@ -0,0 +1,26 @@ +package com.bruis.springsecurity.model; + +/** + * @author LuoHaiYang + */ +public class LoginRequestParam { + + private String username; + private String password; + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } +} diff --git a/Spring-Security/src/main/java/com/bruis/springsecurity/service/SysUserServiceImpl.java b/Spring-Security/src/main/java/com/bruis/springsecurity/service/SysUserServiceImpl.java new file mode 100644 index 0000000..0d2ddb9 --- /dev/null +++ b/Spring-Security/src/main/java/com/bruis/springsecurity/service/SysUserServiceImpl.java @@ -0,0 +1,36 @@ +package com.bruis.springsecurity.service; + +import com.bruis.springsecurity.dto.User; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.stereotype.Service; + +import java.util.HashSet; +import java.util.Set; + +/** + * @author LuoHaiYang + */ +@Service +public class SysUserServiceImpl implements UserService{ + + @Override + public User findByUsername(String username) { + User user = new User(); + user.setId(1L); + user.setUsername(username); + String password = new BCryptPasswordEncoder().encode("123"); + user.setPassword(password); + return user; + } + + @Override + public Set findPermissions(String username) { + Set permissions = new HashSet<>(); + permissions.add("sys:user:view"); + permissions.add("sys:user:add"); + permissions.add("sys:user:edit"); + permissions.add("sys:user:delete"); + return permissions; + } + +} diff --git a/Spring-Security/src/main/java/com/bruis/springsecurity/service/UserService.java b/Spring-Security/src/main/java/com/bruis/springsecurity/service/UserService.java new file mode 100644 index 0000000..cd1acb9 --- /dev/null +++ b/Spring-Security/src/main/java/com/bruis/springsecurity/service/UserService.java @@ -0,0 +1,15 @@ +package com.bruis.springsecurity.service; + +import com.bruis.springsecurity.dto.User; + +import java.util.Set; + +/** + * @author LuoHaiYang + */ +public interface UserService { + + User findByUsername(String username); + + Set findPermissions(String username); +} diff --git a/Spring-Security/src/main/java/com/bruis/springsecurity/utils/HttpUtils.java b/Spring-Security/src/main/java/com/bruis/springsecurity/utils/HttpUtils.java new file mode 100644 index 0000000..0371428 --- /dev/null +++ b/Spring-Security/src/main/java/com/bruis/springsecurity/utils/HttpUtils.java @@ -0,0 +1,38 @@ +package com.bruis.springsecurity.utils; + +import com.alibaba.fastjson.JSONObject; +import com.bruis.springsecurity.model.HttpResult; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * @author LuoHaiYang + */ +public class HttpUtils { + /** + * 获取HttpServletRequest对象 + * @return + */ + public static HttpServletRequest getHttpServletRequest() { + return ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); + } + + /** + * 输出信息到浏览器 + * @param response + * @param data + * @throws IOException + */ + public static void write(HttpServletResponse response, Object data) throws IOException { + response.setContentType("application/json; charset=utf-8"); + HttpResult result = HttpResult.ok(data); + String json = JSONObject.toJSONString(result); + response.getWriter().print(json); + response.getWriter().flush(); + response.getWriter().close(); + } +} diff --git a/Spring-Security/src/main/java/com/bruis/springsecurity/utils/JwtTokenUtils.java b/Spring-Security/src/main/java/com/bruis/springsecurity/utils/JwtTokenUtils.java new file mode 100644 index 0000000..303d376 --- /dev/null +++ b/Spring-Security/src/main/java/com/bruis/springsecurity/utils/JwtTokenUtils.java @@ -0,0 +1,209 @@ +package com.bruis.springsecurity.utils; + +import com.bruis.springsecurity.utils.security.GrantedAuthorityImpl; +import com.bruis.springsecurity.utils.security.JwtAuthenticationToken; +import com.bruis.springsecurity.utils.security.SecurityUtils; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.util.StringUtils; + +import javax.servlet.http.HttpServletRequest; +import java.io.Serializable; +import java.util.*; + +/** + * @author LuoHaiYang + */ +public class JwtTokenUtils implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * 用户名称 + */ + private static final String USERNAME = Claims.SUBJECT; + /** + * 创建时间 + */ + private static final String CREATED = "created"; + /** + * 权限列表 + */ + private static final String AUTHORITIES = "authorities"; + /** + * 密钥 + */ + private static final String SECRET = "abcdefgh"; + /** + * 有效期12小时 + */ + private static final long EXPIRE_TIME = 12 * 60 * 60 * 1000; + + /** + * 生成令牌 + * + * @return 令牌 + */ + public static String generateToken(Authentication authentication) { + Map claims = new HashMap<>(3); + claims.put(USERNAME, SecurityUtils.getUsername(authentication)); + claims.put(CREATED, new Date()); + claims.put(AUTHORITIES, authentication.getAuthorities()); + return generateToken(claims); + } + + /** + * 从数据声明生成令牌 + * + * @param claims 数据声明 + * @return 令牌 + */ + private static String generateToken(Map claims) { + Date expirationDate = new Date(System.currentTimeMillis() + EXPIRE_TIME); + return Jwts.builder().setClaims(claims).setExpiration(expirationDate).signWith(SignatureAlgorithm.HS512, SECRET).compact(); + } + + /** + * 从令牌中获取用户名 + * + * @param token 令牌 + * @return 用户名 + */ + public static String getUsernameFromToken(String token) { + String username; + try { + Claims claims = getClaimsFromToken(token); + username = claims.getSubject(); + } catch (Exception e) { + username = null; + } + return username; + } + + /** + * 根据请求令牌获取登录认证信息 + */ + public static Authentication getAuthenticationeFromToken(HttpServletRequest request) { + Authentication authentication = null; + // 获取请求携带的令牌 + String token = JwtTokenUtils.getToken(request); + if(token != null) { + // 请求令牌不能为空 + if(SecurityUtils.getAuthentication() == null) { + // 上下文中Authentication为空 + Claims claims = getClaimsFromToken(token); + if(claims == null) { + return null; + } + String username = claims.getSubject(); + if(username == null) { + return null; + } + if(isTokenExpired(token)) { + return null; + } + Object authors = claims.get(AUTHORITIES); + List authorities = new ArrayList(); + if (authors != null && authors instanceof List) { + for (Object object : (List) authors) { + authorities.add(new GrantedAuthorityImpl((String) ((Map) object).get("authority"))); + } + } + authentication = new JwtAuthenticationToken(username, null, authorities, token); + } else { + if(validateToken(token, SecurityUtils.getUsername())) { + // 如果上下文中Authentication非空,且请求令牌合法,直接返回当前登录认证信息 + authentication = SecurityUtils.getAuthentication(); + } + } + } + return authentication; + } + + /** + * 从令牌中获取数据声明 + * + * @param token 令牌 + * @return 数据声明 + */ + private static Claims getClaimsFromToken(String token) { + Claims claims; + try { + claims = Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token).getBody(); + } catch (Exception e) { + claims = null; + } + return claims; + } + + /** + * 验证令牌 + * @param token + * @param username + * @return + */ + public static Boolean validateToken(String token, String username) { + String userName = getUsernameFromToken(token); + if (username == null) { + System.err.println("非法的Token"); + return false; + } + return (userName.equals(username) && !isTokenExpired(token)); + } + + /** + * 刷新令牌 + * @param token + * @return + */ + public static String refreshToken(String token) { + String refreshedToken; + try { + Claims claims = getClaimsFromToken(token); + claims.put(CREATED, new Date()); + refreshedToken = generateToken(claims); + } catch (Exception e) { + refreshedToken = null; + } + return refreshedToken; + } + + /** + * 判断令牌是否过期 + * + * @param token 令牌 + * @return 是否过期 + */ + public static Boolean isTokenExpired(String token) { + try { + Claims claims = getClaimsFromToken(token); + Date expiration = claims.getExpiration(); + return expiration.before(new Date()); + } catch (Exception e) { + return false; + } + } + + /** + * 获取请求token + * @param request + * @return + */ + public static String getToken(HttpServletRequest request) { + String token = request.getHeader("Authorization"); + String tokenHead = "Bearer "; + if(token == null) { + token = request.getHeader("token"); + } else if(token.contains(tokenHead)){ + token = token.substring(tokenHead.length()); + } + if(StringUtils.isEmpty(token)) { + token = null; + } + return token; + } + +} diff --git a/Spring-Security/src/main/java/com/bruis/springsecurity/utils/security/GrantedAuthorityImpl.java b/Spring-Security/src/main/java/com/bruis/springsecurity/utils/security/GrantedAuthorityImpl.java new file mode 100644 index 0000000..7f51ff0 --- /dev/null +++ b/Spring-Security/src/main/java/com/bruis/springsecurity/utils/security/GrantedAuthorityImpl.java @@ -0,0 +1,26 @@ +package com.bruis.springsecurity.utils.security; + +import org.springframework.security.core.GrantedAuthority; + +/** + * @author LuoHaiYang + */ +public class GrantedAuthorityImpl implements GrantedAuthority { + + private static final long serialVersionUID = 1L; + + private String authority; + + public GrantedAuthorityImpl(String authority) { + this.authority = authority; + } + + public void setAuthority(String authority) { + this.authority = authority; + } + + @Override + public String getAuthority() { + return this.authority; + } +} \ No newline at end of file diff --git a/Spring-Security/src/main/java/com/bruis/springsecurity/utils/security/JwtAuthenticationFilter.java b/Spring-Security/src/main/java/com/bruis/springsecurity/utils/security/JwtAuthenticationFilter.java new file mode 100644 index 0000000..6c5515e --- /dev/null +++ b/Spring-Security/src/main/java/com/bruis/springsecurity/utils/security/JwtAuthenticationFilter.java @@ -0,0 +1,34 @@ +package com.bruis.springsecurity.utils.security; + +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * @author LuoHaiYang + * + * 登录检查过滤器, 原逻辑是在BasicAuthenticationFilter类中. + * 即判断请求头中是否包含:Authorization, 是否包含basic。 + * + * 这个类非常重要。 + * + */ +public class JwtAuthenticationFilter extends BasicAuthenticationFilter { + + public JwtAuthenticationFilter(AuthenticationManager authenticationManager) { + super(authenticationManager); + } + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { + // 获取token,并检查登录状态 + // super.doFilterInternal(request, response, chain); + SecurityUtils.checkAuthentication(request); + chain.doFilter(request, response); + } +} diff --git a/Spring-Security/src/main/java/com/bruis/springsecurity/utils/security/JwtAuthenticationProvider.java b/Spring-Security/src/main/java/com/bruis/springsecurity/utils/security/JwtAuthenticationProvider.java new file mode 100644 index 0000000..5abe6c7 --- /dev/null +++ b/Spring-Security/src/main/java/com/bruis/springsecurity/utils/security/JwtAuthenticationProvider.java @@ -0,0 +1,43 @@ +package com.bruis.springsecurity.utils.security; + +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.authentication.dao.DaoAuthenticationProvider; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; + +/** + * @author LuoHaiYang + * + * DaoAuthenticationProvider 系统默认的身份认证组件 + * + */ +public class JwtAuthenticationProvider extends DaoAuthenticationProvider { + + public JwtAuthenticationProvider(UserDetailsService userDetailsService) { + // 父类的方法 + setUserDetailsService(userDetailsService); + setPasswordEncoder(new BCryptPasswordEncoder()); + } + + /** + * 接口AuthenticationProvider的authenticate方法 + * @param authentication + * @return + * @throws AuthenticationException + */ + @Override + public Authentication authenticate(Authentication authentication) throws AuthenticationException { + // 这里可以覆写整个登录认证逻辑。 + // 这里调用的是抽象类AbstractUserDetailsAuthenticationProvider的authenticate方法。 + return super.authenticate(authentication); + } + + @Override + protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException { + // 可以在此处覆写密码验证逻辑 + super.additionalAuthenticationChecks(userDetails, authentication); + } +} diff --git a/Spring-Security/src/main/java/com/bruis/springsecurity/utils/security/JwtAuthenticationToken.java b/Spring-Security/src/main/java/com/bruis/springsecurity/utils/security/JwtAuthenticationToken.java new file mode 100644 index 0000000..61c1973 --- /dev/null +++ b/Spring-Security/src/main/java/com/bruis/springsecurity/utils/security/JwtAuthenticationToken.java @@ -0,0 +1,44 @@ +package com.bruis.springsecurity.utils.security; + +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.GrantedAuthority; + +import java.util.Collection; + +/** + * + * 自定义令牌 + * + * @author LuoHaiYang + */ +public class JwtAuthenticationToken extends UsernamePasswordAuthenticationToken { + private static final long serialVersionUID = 1L; + + private String token; + + public JwtAuthenticationToken(Object principal, Object credentials){ + super(principal, credentials); + } + + public JwtAuthenticationToken(Object principal, Object credentials, String token){ + super(principal, credentials); + this.token = token; + } + + public JwtAuthenticationToken(Object principal, Object credentials, Collection authorities, String token) { + super(principal, credentials, authorities); + this.token = token; + } + + public String getToken() { + return token; + } + + public void setToken(String token) { + this.token = token; + } + + public static long getSerialversionuid() { + return serialVersionUID; + } +} diff --git a/Spring-Security/src/main/java/com/bruis/springsecurity/utils/security/JwtLoginFilter.java b/Spring-Security/src/main/java/com/bruis/springsecurity/utils/security/JwtLoginFilter.java new file mode 100644 index 0000000..5230299 --- /dev/null +++ b/Spring-Security/src/main/java/com/bruis/springsecurity/utils/security/JwtLoginFilter.java @@ -0,0 +1,149 @@ +package com.bruis.springsecurity.utils.security; + +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import com.bruis.springsecurity.utils.HttpUtils; +import com.bruis.springsecurity.utils.JwtTokenUtils; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.event.InteractiveAuthenticationSuccessEvent; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.Charset; + +/** + * 启动登录认证流程过滤器 + * @author LuoHaiYang + */ +public class JwtLoginFilter extends UsernamePasswordAuthenticationFilter { + + public static final String UFT8 = "UTF-8"; + + public JwtLoginFilter(AuthenticationManager authenticationManager) { + setAuthenticationManager(authenticationManager); + } + + /** + * + * super.doFilter() 主要调用的是AbstractAuthenticationProcessingFilter方法中的doFilter() + * + * @param req + * @param res + * @param chain + * @throws IOException + * @throws ServletException + */ + @Override + public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { + // POST请求时,对 /login 登录请求进行拦截,由此方法触发执行的登录认证流程,可以在此覆写整个登录认证流程 + super.doFilter(req, res, chain); + } + + @Override + public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { + // 可以在此覆写尝试进行登录认证的逻辑,登录成功之后等操作不再此方法内 + // 如果使用此过滤器来触发登录认证流程,注意登录请求数据格式的问题 + // 此过滤器的用户名密码默认从request.getParameter()获取,但是这种 + // 读取方式不能读取到如 application/json 等 post 请求数据,需要把 + // 用户名密码的读取逻辑修改为到流中读取request.getInputStream() + + String body = getBody(request); + JSONObject jsonObject = JSON.parseObject(body); + // 从json类型请求中获取username + String username = jsonObject.getString("username"); + String password = jsonObject.getString("password"); + + if (username == null) { + username = ""; + } + + if (password == null) { + password = ""; + } + + JwtAuthenticationToken token = new JwtAuthenticationToken(username, password); + + // 调用UsernamePasswordAuthenticationFilter#setDetails方法。 + setDetails(request,token); + + // return super.attemptAuthentication(request, response); + return this.getAuthenticationManager().authenticate(token); + } + + /** + * 认证成功之后 + * @param request + * @param response + * @param chain + * @param authResult + * @throws IOException + * @throws ServletException + */ + @Override + protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException { + // super.successfulAuthentication(request, response, chain, authResult); + // 存储登录认证信息到上下文中 + SecurityContextHolder.getContext().setAuthentication(authResult); + // 记住我, 调用父类的getRememberMeServices + getRememberMeServices().loginSuccess(request, response, authResult); + // 触发事件监听器 + if (this.eventPublisher != null) { + eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass())); + } + // 生成并返回token给客户端,后续访问携带此token + JwtAuthenticationToken token = new JwtAuthenticationToken(null, null, JwtTokenUtils.generateToken(authResult)); + HttpUtils.write(response, token); + } + + /** + * 获取请求Body + * @param request + * @return + */ + public String getBody(HttpServletRequest request) { + StringBuilder stringBuilder = new StringBuilder(); + InputStream inputStream = null; + BufferedReader reader = null; + + try { + inputStream = request.getInputStream(); + // 将输入流中报装为BufferedReader + reader = new BufferedReader(new InputStreamReader(inputStream, Charset.forName(UFT8))); + String line = ""; + while ((line = reader.readLine()) != null) { + stringBuilder.append(line); + } + } catch (IOException e) { + e.printStackTrace(); + } finally { + if (inputStream != null) { + try { + inputStream.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + if (reader != null) { + try { + reader.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + + return stringBuilder.toString(); + } +} diff --git a/Spring-Security/src/main/java/com/bruis/springsecurity/utils/security/JwtUserDetails.java b/Spring-Security/src/main/java/com/bruis/springsecurity/utils/security/JwtUserDetails.java new file mode 100644 index 0000000..dcfd63d --- /dev/null +++ b/Spring-Security/src/main/java/com/bruis/springsecurity/utils/security/JwtUserDetails.java @@ -0,0 +1,23 @@ +package com.bruis.springsecurity.utils.security; + +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.User; + +import java.util.Collection; + +/** + * @author LuoHaiYang + */ +public class JwtUserDetails extends User { + + private static final long serialVersionUID = 1L; + + public JwtUserDetails(String username, String password, Collection authorities) { + this(username, password, true, true, true, true, authorities); + } + + public JwtUserDetails(String username, String password, boolean enabled, boolean accountNonExpired, + boolean credentialsNonExpired, boolean accountNonLocked, Collection authorities) { + super(username, password, enabled, accountNonExpired, credentialsNonExpired, accountNonLocked, authorities); + } +} \ No newline at end of file diff --git a/Spring-Security/src/main/java/com/bruis/springsecurity/utils/security/SecurityUtils.java b/Spring-Security/src/main/java/com/bruis/springsecurity/utils/security/SecurityUtils.java new file mode 100644 index 0000000..d982afd --- /dev/null +++ b/Spring-Security/src/main/java/com/bruis/springsecurity/utils/security/SecurityUtils.java @@ -0,0 +1,82 @@ +package com.bruis.springsecurity.utils.security; + +import com.bruis.springsecurity.utils.JwtTokenUtils; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; + +import javax.servlet.http.HttpServletRequest; + +/** + * @author LuoHaiYang + */ +public class SecurityUtils { + + public static JwtAuthenticationToken login(HttpServletRequest request, String username, + String password, AuthenticationManager authenticationManager) { + // 构造token + JwtAuthenticationToken token = new JwtAuthenticationToken(username, password); + // 通过authenticationManager + Authentication authentication = authenticationManager.authenticate(token); + // 认证成功后将认证信息存储到SpringSecurity上下文中 + SecurityContextHolder.getContext().setAuthentication(authentication); + // 将认证信息封装成令牌返回给客户端 + token.setToken(JwtTokenUtils.generateToken(authentication)); + return token; + } + + /** + * 获取当前登录信息 + * @return + */ + public static Authentication getAuthentication() { + if(SecurityContextHolder.getContext() == null) { + return null; + } + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + return authentication; + } + + /** + * 获取令牌进行认证 + * @param request + */ + public static void checkAuthentication(HttpServletRequest request) { + // 获取令牌并根据令牌获取登录认证信息 + Authentication authentication = JwtTokenUtils.getAuthenticationeFromToken(request); + // 设置登录认证信息到上下文 + SecurityContextHolder.getContext().setAuthentication(authentication); + } + + /** + * 获取当前用户名 + * @return + */ + public static String getUsername() { + String username = null; + Authentication authentication = getAuthentication(); + if(authentication != null) { + Object principal = authentication.getPrincipal(); + if(principal != null && principal instanceof UserDetails) { + username = ((UserDetails) principal).getUsername(); + } + } + return username; + } + + /** + * 获取用户名 + * @return + */ + public static String getUsername(Authentication authentication) { + String username = null; + if(authentication != null) { + Object principal = authentication.getPrincipal(); + if(principal != null && principal instanceof UserDetails) { + username = ((UserDetails) principal).getUsername(); + } + } + return username; + } +} diff --git a/Spring-Security/src/main/java/com/bruis/springsecurity/utils/security/UserDetailsServiceImpl.java b/Spring-Security/src/main/java/com/bruis/springsecurity/utils/security/UserDetailsServiceImpl.java new file mode 100644 index 0000000..93e0763 --- /dev/null +++ b/Spring-Security/src/main/java/com/bruis/springsecurity/utils/security/UserDetailsServiceImpl.java @@ -0,0 +1,35 @@ +package com.bruis.springsecurity.utils.security; + +import com.bruis.springsecurity.dto.User; +import com.bruis.springsecurity.service.UserService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * @author LuoHaiYang + */ +@Service +public class UserDetailsServiceImpl implements UserDetailsService { + @Autowired + private UserService userService; + + @Override + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + User user = userService.findByUsername(username); + if (user == null) { + throw new UsernameNotFoundException("该用户不存在"); + } + // 用户权限列表,根据用户拥有的权限标识与如 @PreAuthorize("hasAuthority('sys:menu:view')") 标注的接口对比,决定是否可以调用接口 + Set permissions = userService.findPermissions(username); + List grantedAuthorities = permissions.stream().map(GrantedAuthorityImpl::new).collect(Collectors.toList()); + return new JwtUserDetails(username, user.getPassword(), grantedAuthorities); + } +} diff --git a/Spring-Security/src/main/resources/application.properties b/Spring-Security/src/main/resources/application.properties new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/Spring-Security/src/main/resources/application.properties @@ -0,0 +1 @@ + diff --git a/Spring-Security/src/test/java/com/bruis/springsecurity/SpringsecurityApplicationTests.java b/Spring-Security/src/test/java/com/bruis/springsecurity/SpringsecurityApplicationTests.java new file mode 100644 index 0000000..18c6068 --- /dev/null +++ b/Spring-Security/src/test/java/com/bruis/springsecurity/SpringsecurityApplicationTests.java @@ -0,0 +1,4 @@ +package com.bruis.springsecurity; + +class SpringsecurityApplicationTests { +} diff --git "a/note/Dubbo/Dubbo\345\272\225\345\261\202\346\272\220\347\240\201\345\255\246\344\271\240\342\200\224\342\200\224\346\234\215\345\212\241\345\274\225\347\224\250.md" "b/note/Dubbo/Dubbo\345\272\225\345\261\202\346\272\220\347\240\201\345\255\246\344\271\240\342\200\224\342\200\224\346\234\215\345\212\241\345\274\225\347\224\250.md" new file mode 100644 index 0000000..089722d --- /dev/null +++ "b/note/Dubbo/Dubbo\345\272\225\345\261\202\346\272\220\347\240\201\345\255\246\344\271\240\342\200\224\342\200\224\346\234\215\345\212\241\345\274\225\347\224\250.md" @@ -0,0 +1,23 @@ +## 前言 + +## 正文 + +就DubboProtocolTest类下的testDubboProtocol()方法,服务引用流程: + +1. ProtocolFilterWrapper#refer() +2. ProtocolListenerWrapper#refer() +3. AbstractProtocol#refer() +4. DubboProtocol#protocolBindingRefer() +5. DubboProtocol#getClients() +6. DubboProtocol#getSharedClient() +7. DubboProtocol#buildReferenceCountExchangeClientList() +8. DubboProtocol#buildReferenceCountExchangeClient() +9. DubboProtocol#initClient() +10. Exchangers#connect() +11. HeaderExchanger#connect() +12. Transporters#connect() +13. Transporter$Adaptive,代理出来的类,在服务暴露中有代码 +14. NettyTransporter#connect() +15. NettyClient#init() +16. AbstractClient#init() +17. NettyClient#doOpen() \ No newline at end of file diff --git "a/note/Dubbo/Dubbo\345\272\225\345\261\202\346\272\220\347\240\201\345\255\246\344\271\240\342\200\224\342\200\224\346\234\215\345\212\241\346\232\264\351\234\262.md" "b/note/Dubbo/Dubbo\345\272\225\345\261\202\346\272\220\347\240\201\345\255\246\344\271\240\342\200\224\342\200\224\346\234\215\345\212\241\346\232\264\351\234\262.md" new file mode 100644 index 0000000..eede346 --- /dev/null +++ "b/note/Dubbo/Dubbo\345\272\225\345\261\202\346\272\220\347\240\201\345\255\246\344\271\240\342\200\224\342\200\224\346\234\215\345\212\241\346\232\264\351\234\262.md" @@ -0,0 +1,144 @@ +```Java +package org.apache.dubbo.rpc; +import org.apache.dubbo.common.extension.ExtensionLoader; +public class Protocol$Adaptive implements org.apache.dubbo.rpc.Protocol { + public void destroy() { + throw new UnsupportedOperationException("The method public abstract void org.apache.dubbo.rpc.Protocol.destroy() of interface org.apache.dubbo.rpc.Protocol is not adaptive method!"); + } + public int getDefaultPort() { + throw new UnsupportedOperationException("The method public abstract int org.apache.dubbo.rpc.Protocol.getDefaultPort() of interface org.apache.dubbo.rpc.Protocol is not adaptive method!"); + } + public org.apache.dubbo.rpc.Invoker refer(java.lang.Class arg0, org.apache.dubbo.common.URL arg1) throws org.apache.dubbo.rpc.RpcException { + if (arg1 == null) throw new IllegalArgumentException("url == null"); + org.apache.dubbo.common.URL url = arg1; + String extName = ( url.getProtocol() == null ? "dubbo" : url.getProtocol() ); + if(extName == null) throw new IllegalStateException("Failed to get extension (org.apache.dubbo.rpc.Protocol) name from url (" + url.toString() + ") use keys([protocol])"); + // 这里extension为ProtocolFilterWrapper类 + org.apache.dubbo.rpc.Protocol extension = (org.apache.dubbo.rpc.Protocol)ExtensionLoader.getExtensionLoader(org.apache.dubbo.rpc.Protocol.class).getExtension(extName); + return extension.refer(arg0, arg1); + } + public org.apache.dubbo.rpc.Exporter export(org.apache.dubbo.rpc.Invoker arg0) throws org.apache.dubbo.rpc.RpcException { + if (arg0 == null) throw new IllegalArgumentException("org.apache.dubbo.rpc.Invoker argument == null"); + if (arg0.getUrl() == null) throw new IllegalArgumentException("org.apache.dubbo.rpc.Invoker argument getUrl() == null"); + org.apache.dubbo.common.URL url = arg0.getUrl(); + String extName = ( url.getProtocol() == null ? "dubbo" : url.getProtocol() ); + if(extName == null) throw new IllegalStateException("Failed to get extension (org.apache.dubbo.rpc.Protocol) name from url (" + url.toString() + ") use keys([protocol])"); + // 此处传入的extName为dubbo,extension获得的扩展点实现类为ProtocolFilterWrapper + org.apache.dubbo.rpc.Protocol extension = (org.apache.dubbo.rpc.Protocol)ExtensionLoader.getExtensionLoader(org.apache.dubbo.rpc.Protocol.class).getExtension(extName); + return extension.export(arg0); + } + public java.util.List getServers() { + throw new UnsupportedOperationException("The method public default java.util.List org.apache.dubbo.rpc.Protocol.getServers() of interface org.apache.dubbo.rpc.Protocol is not adaptive method!"); + } +} +``` + +```Java +package org.apache.dubbo.rpc; +import org.apache.dubbo.common.extension.ExtensionLoader; +public class ProxyFactory$Adaptive implements org.apache.dubbo.rpc.ProxyFactory { + public java.lang.Object getProxy(org.apache.dubbo.rpc.Invoker arg0) throws org.apache.dubbo.rpc.RpcException { + if (arg0 == null) throw new IllegalArgumentException("org.apache.dubbo.rpc.Invoker argument == null"); + if (arg0.getUrl() == null) throw new IllegalArgumentException("org.apache.dubbo.rpc.Invoker argument getUrl() == null"); + org.apache.dubbo.common.URL url = arg0.getUrl(); + // 判断如果取不到proxy,则使用默认值javassist + String extName = url.getParameter("proxy", "javassist"); + if(extName == null) throw new IllegalStateException("Failed to get extension (org.apache.dubbo.rpc.ProxyFactory) name from url (" + url.toString() + ") use keys([proxy])"); + // 这里extension是StubProxyFactoryWrapper对象 + org.apache.dubbo.rpc.ProxyFactory extension = (org.apache.dubbo.rpc.ProxyFactory)ExtensionLoader.getExtensionLoader(org.apache.dubbo.rpc.ProxyFactory.class).getExtension(extName); + return extension.getProxy(arg0); + } + public java.lang.Object getProxy(org.apache.dubbo.rpc.Invoker arg0, Boolean arg1) throws org.apache.dubbo.rpc.RpcException { + if (arg0 == null) throw new IllegalArgumentException("org.apache.dubbo.rpc.Invoker argument == null"); + if (arg0.getUrl() == null) throw new IllegalArgumentException("org.apache.dubbo.rpc.Invoker argument getUrl() == null"); + org.apache.dubbo.common.URL url = arg0.getUrl(); + String extName = url.getParameter("proxy", "javassist"); + if(extName == null) throw new IllegalStateException("Failed to get extension (org.apache.dubbo.rpc.ProxyFactory) name from url (" + url.toString() + ") use keys([proxy])"); + org.apache.dubbo.rpc.ProxyFactory extension = (org.apache.dubbo.rpc.ProxyFactory)ExtensionLoader.getExtensionLoader(org.apache.dubbo.rpc.ProxyFactory.class).getExtension(extName); + return extension.getProxy(arg0, arg1); + } + public org.apache.dubbo.rpc.Invoker getInvoker(java.lang.Object arg0, java.lang.Class arg1, org.apache.dubbo.common.URL arg2) throws org.apache.dubbo.rpc.RpcException { + if (arg2 == null) throw new IllegalArgumentException("url == null"); + org.apache.dubbo.common.URL url = arg2; + String extName = url.getParameter("proxy", "javassist"); + if(extName == null) throw new IllegalStateException("Failed to get extension (org.apache.dubbo.rpc.ProxyFactory) name from url (" + url.toString() + ") use keys([proxy])"); + // 这里获取到的ProxyFactory的扩展点是StubProxyFactoryWrapper,StubProxyFactoryWrapper是ProxyFactory的实现类,由于ProxyFactory注解@SPI("javassist"),所以extName取javassist值 + org.apache.dubbo.rpc.ProxyFactory extension = (org.apache.dubbo.rpc.ProxyFactory)ExtensionLoader.getExtensionLoader(org.apache.dubbo.rpc.ProxyFactory.class).getExtension(extName); + // 所以去调用了StubProxyFactoryWrapper#getInvoker()方法,在该方法中调用的是JavassistProxyFactory的getInvoker方法,最终实际 + // 返回的是一个JavassistProxyFactory的一个匿名内部类:AbstractProxyInvoker + return extension.getInvoker(arg0, arg1, arg2); + } +} +``` + +```Java +package org.apache.dubbo.remoting; +import org.apache.dubbo.common.extension.ExtensionLoader; +public class Transporter$Adaptive implements org.apache.dubbo.remoting.Transporter { + public org.apache.dubbo.remoting.Client connect(org.apache.dubbo.common.URL arg0, org.apache.dubbo.remoting.ChannelHandler arg1) throws org.apache.dubbo.remoting.RemotingException { + if (arg0 == null) throw new IllegalArgumentException("url == null"); + org.apache.dubbo.common.URL url = arg0; + String extName = url.getParameter("client", url.getParameter("transporter", "netty")); + if(extName == null) throw new IllegalStateException("Failed to get extension (org.apache.dubbo.remoting.Transporter) name from url (" + url.toString() + ") use keys([client, transporter])"); + org.apache.dubbo.remoting.Transporter extension = (org.apache.dubbo.remoting.Transporter)ExtensionLoader.getExtensionLoader(org.apache.dubbo.remoting.Transporter.class).getExtension(extName); + return extension.connect(arg0, arg1); + } + public org.apache.dubbo.remoting.RemotingServer bind(org.apache.dubbo.common.URL arg0, org.apache.dubbo.remoting.ChannelHandler arg1) throws org.apache.dubbo.remoting.RemotingException { + if (arg0 == null) throw new IllegalArgumentException("url == null"); + org.apache.dubbo.common.URL url = arg0; + String extName = url.getParameter("server", url.getParameter("transporter", "netty")); + if(extName == null) throw new IllegalStateException("Failed to get extension (org.apache.dubbo.remoting.Transporter) name from url (" + url.toString() + ") use keys([server, transporter])"); + // 这里获取的扩展点实现类是netty4包下的NettyTransporter + org.apache.dubbo.remoting.Transporter extension = (org.apache.dubbo.remoting.Transporter)ExtensionLoader.getExtensionLoader(org.apache.dubbo.remoting.Transporter.class).getExtension(extName); + return extension.bind(arg0, arg1); + } +} +``` + +对于JavassistProxyFactory对象中Invoker类型的匿名内部类,类结构如下图所示 +![export01](https://github.com/coderbruis/JavaSourceCodeLearning/blob/master/note/images/Dubbo/export01.png) + +JavassistProxyFactory代码如下: +```Java +public class JavassistProxyFactory extends AbstractProxyFactory { + + @Override + @SuppressWarnings("unchecked") + public T getProxy(Invoker invoker, Class[] interfaces) { + return (T) Proxy.getProxy(interfaces).newInstance(new InvokerInvocationHandler(invoker)); + } + + @Override + public Invoker getInvoker(T proxy, Class type, URL url) { + // TODO Wrapper cannot handle this scenario correctly: the classname contains '$' + final Wrapper wrapper = Wrapper.getWrapper(proxy.getClass().getName().indexOf('$') < 0 ? proxy.getClass() : type); + return new AbstractProxyInvoker(proxy, type, url) { + @Override + protected Object doInvoke(T proxy, String methodName, + Class[] parameterTypes, + Object[] arguments) throws Throwable { + return wrapper.invokeMethod(proxy, methodName, parameterTypes, arguments); + } + }; + } + +} +``` + + +- testDubboProtocol()调用流程 + +1. 首先protocol和proxy变量分别在DubboProtocolTest.java类加载过程时就已经分别加载好了,这里的Protocol和ProxyFactory对象都生成的是代理对象,分别为Protocol$Adaptive和ProxyFactory$Adaptive对象,动态代理处的代码在上面展示了; +2. 首先调用proxy#getInvoker(),调用的实际是ProxyFactory$Adaptive代理对象的getInvoker()方法,在上图可以看到具体逻辑,其方法逻辑中需要去获取ProxyFactory对象的扩展点实现类,经过调试ProxyFactory对象的扩展点实现类为StubProxyFactoryWrapper,最终会调用到JavassistProxyFactory的getInvoker()方法,它会最终生成一个AbstractProxyInvoker()匿名类对象,所以调试的时候可以发现invoker的引用是JavassistProxyFactory$1; +3. 获取到invoker之后,就传入到Protocol的扩展点实现类中去调用export()方法,由上图的Protocol$Adaptive可以知道,在export()方法中会去生成Protocol的扩展点实现类,这里的实现类是ProtocolFilterWrapper(这里为什么是这个实现类呢?去调研一下)。经过代理类的传递包装,最终来到了ProtocolFilterWrapper#export()方法; +4. 接着就是调用ProtocolFilterWrapper#buildInvocation()方法,构建调用链。就是先去获取Filter的扩展点实现类集合对象,然后倒叙遍历该集合对象然后将invoker对象和最后一个Filter对象封装为另一个Invoker,然后再继续传递到和上一个Filter对象继续封装成Invoker,以此往复封装。最终的结果就是将传入的invoker对象封装到了最后一个Filter之后,请求进来之后需要调用所有的Filter过后才会调用invoker对象,形成了一个Invoker链; + +效果如下: +![export02](https://github.com/coderbruis/JavaSourceCodeLearning/blob/master/note/images/Dubbo/export02.png) + +还需要注意的是,在ProtocolFilterWrapper#buildInvokerChain()方法中,生成的是匿名对象,即将3中传递过来的JavassistProxyFactory$1又包装起来放在了ProtocolFilterWrapper对象中,所以你会发现此时的invoker对象是ProtocolFilterWrapper$1; + +5. 在ProtocolFilterWrapper#export()中,会调用ProtocolListenerWrapper#export()对象,在ProtocolListenerWrapper#export()中会返回一个ListenerExporterWrapper的匿名内部类对象,生成该匿名内部类之前会调用DubboProtocol#export()方法。 +6. 千辛万苦,终于来到了DubboProtocol#export()的openServer()方法了,在openServer()方法中,会调用createServer(url)方法去创建Server;在DubboProtocol中会调用Exchangers#bind()去和Netty服务端进行绑定? +7. 随后调用Exchangers#getExchanger()方法去获取Exchanger的扩展点实现类,他的扩展点实现类是HeaderExchanger,即调用的是HeaderExchagner#bind()方法。在其方法中会构造一个HeaderExchangeServer对象,但是在传入入参时需要调用Transporters#bind()方法,将返回的对象作为入参。 +8. 随后调用Transporters#getTransporter()方法,去获取Transporter的自适应扩展点实现类,这里实际获取的是一个代理类,是Transporter$Adaptive,类实现体在上面。接着就是调用的Transporter$Adaptive的bind()方法。最终去调用的netty4包下的NettyTransporter#bind() \ No newline at end of file diff --git "a/note/Dubbo/Dubbo\345\272\225\345\261\202\346\272\220\347\240\201\345\255\246\344\271\240\357\274\210\344\270\200\357\274\211\342\200\224\342\200\224 Dubbo\347\232\204URL.md" "b/note/Dubbo/Dubbo\345\272\225\345\261\202\346\272\220\347\240\201\345\255\246\344\271\240\357\274\210\344\270\200\357\274\211\342\200\224\342\200\224 Dubbo\347\232\204URL.md" new file mode 100644 index 0000000..99cea39 --- /dev/null +++ "b/note/Dubbo/Dubbo\345\272\225\345\261\202\346\272\220\347\240\201\345\255\246\344\271\240\357\274\210\344\270\200\357\274\211\342\200\224\342\200\224 Dubbo\347\232\204URL.md" @@ -0,0 +1,156 @@ +## 前言 + +在Dubbo中,URL扮演者非常重要的作用,可以说,理解了URl,就理解了一般的Dubbo了。在Dubbo源码中,可随处看到URL的使用踪迹。 +URL 是整个 Dubbo 中非常基础,也是非常核心的一个组件,阅读源码的过程中你会发现很多方法都是以 URL 作为参数的,在方法内部解析传入的 URL 得到有用的参数,所以有人将 URL 称为Dubbo 的配置总线。 + +## 正文 + +### 1. 什么是URL?URL有哪些组成部分?Dubbo中如何使用URL的? + +URL全称(Uniform Resource Locator,统一资源定位符),它是互联网的统一资源定位标志,也就是指网络地址。 + +一个标准的URL是这样的: + +``` +protocol://username:password@host:port/path?key=value&key=value +``` +- protocol:URL 的协议。我们常见的就是 HTTP 协议和 HTTPS 协议,当然,还有其他协议,如 FTP 协议、SMTP 协议等。 +- username/password:用户名/密码。 HTTP Basic Authentication 中多会使用在 URL 的协议之后直接携带用户名和密码的方式。 +- host/port:主机/端口。在实践中一般会使用域名,而不是使用具体的 host 和 port。 +- path:请求的路径。 +- parameters:参数键值对。一般在 GET 请求中会将参数放到 URL 中,POST 请求会将参数放到请求体中。 + +URL 是整个 Dubbo 中非常基础,也是非常核心的一个组件,阅读源码的过程中你会发现很多方法都是以 URL 作为参数的,在方法内部解析传入的 URL 得到有用的参数,所以有人将 URL 称为Dubbo 的配置总线。 + +下面来看下Dubbo中的URL组成部分: + +Dubbo 中任意的一个实现都可以抽象为一个 URL,Dubbo 使用 URL 来统一描述了所有对象和配置信息,并贯穿在整个 Dubbo 框架之中。这里我们来看 Dubbo 中一个典型 URL 的示例,如下: + +``` +dubbo://172.17.32.91:20880/org.apache.dubbo.demo.DemoService?anyhost=true&application=dubbo-demo-api-provider&dubbo=2.0.2&interface=org.apache.dubbo.demo.DemoService&methods=sayHello,sayHelloAsync&pid=32508&release=&side=provider×tamp=1593253404714dubbo://172.17.32.91:20880/org.apache.dubbo.demo.DemoService?anyhost=true&application=dubbo-demo-api-provider&dubbo=2.0.2&interface=org.apache.dubbo.demo.DemoService&methods=sayHello,sayHelloAsync&pid=32508&release=&side=provider×tamp=1593253404714 +``` + +### 2. Dubbo中的URL + +先看下Dubbo中org.apache.dubbo.common包下的URL类源码: + +```Java +public /*final**/ +class URL implements Serializable { + + private static final long serialVersionUID = -1985165475234910535L; + + private final String protocol; + + private final String username; + + private final String password; + + // by default, host to registry + private final String host; + + // by default, port to registry + private final int port; + + private final String path; + + private final Map parameters; + + private final Map> methodParameters; + + ...省略 +} +``` + +可以看到URL中定义了协议、用户名、密码、host、端口等信息,和上述讲解的一致。 + +看下在Dubbo中典型的URL。 + +``` +dubbo://192.168.1.6:20880/moe.cnkirito.sample.HelloService?timeout=3000 +描述一个 dubbo 协议的服务 + +zookeeper://127.0.0.1:2181/org.apache.dubbo.registry.RegistryService?application=demo-consumer&dubbo=2.0.2&interface=org.apache.dubbo.registry.RegistryService&pid=1214&qos.port=33333×tamp=1545721981946 +描述一个 zookeeper 注册中心 + +consumer://30.5.120.217/org.apache.dubbo.demo.DemoService?application=demo-consumer&category=consumers&check=false&dubbo=2.0.2&interface=org.apache.dubbo.demo.DemoService&methods=sayHello&pid=1209&qos.port=33333&side=consumer×tamp=1545721827784 +描述一个消费者 + +``` + +可以说,任意的一个领域中的一个实现都可以认为是一类 URL,dubbo 使用 URL 来统一描述了元数据,配置信息,贯穿在整个框架之中。 + +### 3. Dubbo中URL是如何生成的呢? + +一个URL对象是如何在Dubbo中生成的呢?它的协议、参数等都是如何生成的?这可以概括为URL的生命周期是怎样的。 + +这里可以概括为三点: + +1. 解析服务 +2. 暴露服务 +3. 引用服务 + +> 解析服务 + +![dubbo01](https://github.com/coderbruis/JavaSourceCodeLearning/blob/master/note/images/Dubbo/dubbo01.png) + +在Dubbo的jar包中的META-INF/spring.handlers配置,配置了DubboNamespaceHandler处理类,在遇到dubbo命名空间时,会进行解析。 + +可以看到DubboNamespaceHandler处理类中,init方法注册了很多DubboBeanDefinitionParser对象,该对象的parse方法就是对所有的Dubbo标签都进行解析, +将新建的bean注册到spring中,并解析xml中的属性,并添加到该bean中。 + +(DubboNamespaceHandler的parse主要步骤分为:1)初始化RootBeanDefinition;2)获取BeanId;3)将获取到的Bean注册到Spring中;4)将xml中配置的信息放到beandefinition的ProvertyValues中;) + +接着,在 ServiceConfig.export() 或 ReferenceConfig.get() 初始化时,将 Bean 对象转换 URL 格式,所有 Bean 属性转成 URL 的参数。 + +然后将 URL 传给协议扩展点,基于扩展点自适应机制,根据 URL 的协议头,进行不同协议的服务暴露或引用。 + + +> 暴露服务 + +1. 只暴露服务端口: + +在没有注册中心,直接暴露提供者的情况下,ServiceConfig 解析出的 URL 的格式为:dubbo://service-host/com.foo.FooService?version=1.0.0。 + +基于扩展点自适应机制,通过 URL 的 dubbo:// 协议头识别,直接调用 DubboProtocol的 export() 方法,打开服务端口。 + +2. 向注册中心暴露服务: + +在有注册中心,需要注册提供者地址的情况下,ServiceConfig 解析出的 URL 的格式为: registry://registry-host/org.apache.dubbo.registry.RegistryService?export=URL.encode("dubbo://service-host/com.foo.FooService?version=1.0.0"), + +基于扩展点自适应机制,通过 URL 的 registry:// 协议头识别,就会调用 RegistryProtocol 的 export() 方法,将 export 参数中的提供者 URL,先注册到注册中心。 + +再重新传给 Protocol 扩展点进行暴露: dubbo://service-host/com.foo.FooService?version=1.0.0,然后基于扩展点自适应机制,通过提供者 URL 的 dubbo:// 协议头识别,就会调用 DubboProtocol 的 export() 方法,打开服务端口。 + +3. ServiceConfig的doExportUrlsFor1Protocol方法 + +在该方法中,创建出URL,然后将URL传递给协议扩展点,基于扩展点自适应机制进行服务暴露。通俗点说就是创建出URL后,通过DubboProtocol来暴露服务。 + +ServiceConfig这个类非常复杂,后面专门拿一篇来讲解。 + +> 引用服务 + +1. 直连引用服务: + +在没有注册中心,直连提供者的情况下,ReferenceConfig 解析出的 URL 的格式为:dubbo://service-host/com.foo.FooService?version=1.0.0。 + +基于扩展点自适应机制,通过 URL 的 dubbo:// 协议头识别,直接调用 DubboProtocol 的 refer() 方法,返回提供者引用。 + +2. 从注册中心发现引用服务: + +在有注册中心,通过注册中心发现提供者地址的情况下,ReferenceConfig 解析出的 URL 的格式为:registry://registry-host/org.apache.dubbo.registry.RegistryService?refer=URL.encode("consumer://consumer-host/com.foo.FooService?version=1.0.0")。 + +基于扩展点自适应机制,通过 URL 的 registry:// 协议头识别,就会调用 RegistryProtocol 的 refer() 方法,基于 refer 参数中的条件,查询提供者 URL,如: dubbo://service-host/com.foo.FooService?version=1.0.0。 + +基于扩展点自适应机制,通过提供者 URL 的 dubbo:// 协议头识别,就会调用 DubboProtocol 的 refer() 方法,得到提供者引用。 + +然后 RegistryProtocol 将多个提供者引用,通过 Cluster 扩展点,伪装成单个提供者引用返回。 + +### 4. Dubbo中使用的URL + +1. Dubbo在SPI中的应用 + +2. Dubbo中的服务暴露 + +3. Dubbo中的服务引用 + diff --git "a/note/Dubbo/Dubbo\345\272\225\345\261\202\346\272\220\347\240\201\345\255\246\344\271\240\357\274\210\344\270\211\357\274\211\342\200\224\342\200\224 Dubbo\347\232\204\346\263\250\345\206\214\344\270\255\345\277\203.md" "b/note/Dubbo/Dubbo\345\272\225\345\261\202\346\272\220\347\240\201\345\255\246\344\271\240\357\274\210\344\270\211\357\274\211\342\200\224\342\200\224 Dubbo\347\232\204\346\263\250\345\206\214\344\270\255\345\277\203.md" new file mode 100644 index 0000000..664cf6b --- /dev/null +++ "b/note/Dubbo/Dubbo\345\272\225\345\261\202\346\272\220\347\240\201\345\255\246\344\271\240\357\274\210\344\270\211\357\274\211\342\200\224\342\200\224 Dubbo\347\232\204\346\263\250\345\206\214\344\270\255\345\277\203.md" @@ -0,0 +1,17 @@ +## 前言 + +本篇重点内容: + +- 注册中心的工作流程 +- 注册中心的数据结构 +- 订阅发布的实现 +- 缓存机制 +- 重试机制 +- 设计模式 + +本篇文章会先讲解注册中心的工作流程,然后讲解不同注册中心的数据结构和类型;接着讲解注册中心支持的通用特性,如:缓存机制、重试机制等。 + +最后在围绕着设计模式讲解注册中心底层实现原理。 + +## 正文 + diff --git "a/note/Dubbo/Dubbo\345\272\225\345\261\202\346\272\220\347\240\201\345\255\246\344\271\240\357\274\210\344\272\214\357\274\211\342\200\224\342\200\224 Dubbo\347\232\204SPI\346\234\272\345\210\266\357\274\210\344\270\212\357\274\211.md" "b/note/Dubbo/Dubbo\345\272\225\345\261\202\346\272\220\347\240\201\345\255\246\344\271\240\357\274\210\344\272\214\357\274\211\342\200\224\342\200\224 Dubbo\347\232\204SPI\346\234\272\345\210\266\357\274\210\344\270\212\357\274\211.md" new file mode 100644 index 0000000..b0d01fa --- /dev/null +++ "b/note/Dubbo/Dubbo\345\272\225\345\261\202\346\272\220\347\240\201\345\255\246\344\271\240\357\274\210\344\272\214\357\274\211\342\200\224\342\200\224 Dubbo\347\232\204SPI\346\234\272\345\210\266\357\274\210\344\270\212\357\274\211.md" @@ -0,0 +1,225 @@ +## 前言 + +Dubbo的SPI机制是什么呢?首先,SPI全称为(Service Provider Interface),主要是被框架开发人员使用的一种技术。 + +Dubbo的SPI机制提供的是一种将服务Provider接口定义在META-INF中,当Dubbo服务启动时, +通过SPI加载机制加载文件中的接口,从而使用那些被加载的接口。那么这么做的目的是什么呢?这么做的目的就是为了更好地达到 OCP 原则(即“对扩展开放,对修改封闭”的原则)。这种"对扩展开放,对修改封闭"的原则能在维持内核功能稳定的基础上,更好的对系统功能进行扩展。换句话说,基于Dubbo SPI加载机制,让整个框架的接口和具体实现完全解耦,从而奠定了整个框架良好可扩展性的基础。 + + +SPI中两个核心就是:扩展点和扩展点实现类。 + +在JDK SPI和Dubbo SPI中,都是通过在配置文件中定义KV键值对来定义对应的扩展点和扩展点实现类,其中Key表示的是扩展点名称,Value表示的是扩展点实现类的全路径名称。 + +Dubbo SPI是参考了JDK原生的SPI机制,进行了性能优化以及功能增强。 + +## 正文 + +### 1. Java SPI + +Javs SPI使用的是策略模式,一个接口多种实现。我们只负责声明接口,具体的实现并不在程序中直接确定,而是由程序之外的配置掌控,用于具体实现的装配。 + +Java SPI的定义及使用步骤如下: +1. 定义一个接口以及对应的方法 +2. 编写该接口的一个实现类 +3. 在META-INF/services/目录下,创建一个以接口全路径命名的文件,如com.test.spi.PrintService +4. 文件内容为具体实现类的全路径名,如果有多个,则用分行符分隔 +5. 在代码中通过java.util.ServiceLoader来加载具体的实现类 + +在com.test.spi包目录下,定义了一个PrintService接口和一个PrintServiceImpl实现类,然后在resources目录下定义了一个META-INF/services/com.test.spi.PrintService,注意这里定义的是一个 +全路径名称的文件。 + +```Java +public interface Printservice ( + void printlnfo(); +} +``` + +```Java +public class PrintServicelmpl implements Printservice { + @Override + public void printlnfo() { + System.out.println("hello world"); + } +} +``` + +```Java +public static void main(String[] args) ( + ServiceLoader serviceServiceLoader = + ServiceLoader.load(PrintService.class); + for (Printservice printservice : serviceServiceLoader) ( + //此处会输出:hello world 获取所有的SPI实现,循环调用 + printService.printInfo(); printlnfo()方法,会打印出 hello world + } +} +``` + +在JDK SPI中,是通过ServiceLoader来获取所有接口实现的。 + +最常见的JDK SPI就是com.mysql.cj.jdbc.Driver 接口,它的实现类是有用户通过配置文件来设定的,Driver接口就是一个扩展点。 + + +![jdk-spi](https://github.com/coderbruis/JavaSourceCodeLearning/blob/master/note/images/Dubbo/jdk-spi.png) + +![jdk-spi-driver](https://github.com/coderbruis/JavaSourceCodeLearning/blob/master/note/images/Dubbo/jdk-spi-driver.png) + +### 2. Dubbo SPI + +Dubbo SPI没有直接使用Java SPI而是在它的思想上又做了一定的改进,形成了一套自己的配置规范和特性。同时,Dubbo SPI又兼容Java SPI服务在启动的时候,Dubbo就会查找这些扩展点的所有实现。 + +Dubbo SPI之于JDK SPI,做到了三点优化: +1. 不同于JDK SPI会一次性实例化扩展点所有实现,因为JDK SPI有扩展实现,则初始化会很耗时,并且如果没有用上也要加载,则会很浪费资源。而Dubbo SPI只会加载扩展点,而不会对其进行初始化,并且Dubbo SPI中 +会根据不同的实现类来缓存到内存中,性能上得到了很大的优化。 +2. JDK SPI如果对扩展加载失败,则连扩展的名称都获取不到,并且失败原因会被吃掉,而Dubbo SPI则会将异常堆栈保留下来,方便后序对其异常信息进行分析。 +3. Dubbo SPI增加了对IOC和AOP的支持,在Dubbo中,一个扩展点可以通过setter来注入到其他扩展点中。 + + +这里再次统一一下SPI的核心概念: + +- 扩展点 +- 扩展点的实现类 + +Dubbo SPI的核心就是围绕着怎么获取扩展点以及扩展点实现类来进行的。那么现在需要先知道,扩展点以及扩展点实现类存放位置? + +#### 2.1 Dubbo SPI配置文件及其存放位置 + +在Dubbo SPI中,按照用途将SPI配置文件分为三类以META-INF开头的目录(META-INF开头目录通常都存放在类路径下): + +- META-INF/services/:该目录的SPI配置文件是用来兼容JDK SPI的 +- META-INF/dubbo/:用来存放用户自定义的SPI配置文件 +- META-INF/dubbo/internal:该目录用于存放Dubbo内部使用的SPI配置文件 + +在SPI配置文件中,都是以KV格式存在的配置内容,例如Dubbo源码中的SPI配置文件内容: +``` +dubbo=org.apache.dubbo.rpc.protocol.dubbo.DubboProtocol +``` + +key表示的是扩展点名称,而value表示的是扩展点的实现类的全限定类名。另外,SPI配置文件名称就是扩展点实现类的全限定类名。 +![Dubbo-SPI-01](https://github.com/coderbruis/JavaSourceCodeLearning/blob/master/note/images/Dubbo/dubbo-spi-01.png) + +那么,扩展点及其实现类以及存放在SPI文件中了,那么Dubbo应用程序该如何将其加载进行JVM内存中呢? + +#### 2.2 Dubbo的ExtensionLoader + +ExtensionLoader即扩展点加载器,它是Dubbo SPI的核心,负责加载扩展点即扩展点实现类,先看下其内部重要的几个成员变量: + + +![Dubbo-SPI-02](https://github.com/coderbruis/JavaSourceCodeLearning/blob/master/note/images/Dubbo/dubbo-spi-02.png) + +![Dubbo-SPI-03](https://github.com/coderbruis/JavaSourceCodeLearning/blob/master/note/images/Dubbo/dubbo-spi-03.png) + +这里的扩展点加载策略有三种: +- DubboInternalLoadingStrategy(加载内部的SPI) +- DubboLoadingStrategy(加载用户自定义的SPI) +- ServiceLoadingStrategy(加载用于兼容JDK的SPI) + +并且其内部默认优先级为:DubboInternalLoadingStrategy > DubboLoadingStrategy > ServiceLoadingStrategy + +![Dubbo-SPI-04](https://github.com/coderbruis/JavaSourceCodeLearning/blob/master/note/images/Dubbo/dubbo-spi-04.png) + +上图清楚的展示了LoadingStrategy接口及其实现类的关系。LoadingStrategy继承了Prioritized,因而其实现类会有优先级之分,而Dubbo默认是使用的DubboInternalLoadingStrategy,查看其三个类的源码: + +```Java +public class DubboInternalLoadingStrategy implements LoadingStrategy { + + // 表示要加载的目录位置 + @Override + public String directory() { + return "META-INF/dubbo/internal/"; + } + + // 获取优先值,用于进行Comparable接口的compareTo优先级比较 + @Override + public int getPriority() { + return MAX_PRIORITY; + } +} +``` + +```Java +public class DubboLoadingStrategy implements LoadingStrategy { + + // 表示要加载的目录位置 + @Override + public String directory() { + return "META-INF/dubbo/"; + } + + @Override + public boolean overridden() { + return true; + } + + // 获取优先值,用于进行Comparable接口的compareTo优先级比较 + @Override + public int getPriority() { + return NORMAL_PRIORITY; + } + + +} +``` + +```Java +public class ServicesLoadingStrategy implements LoadingStrategy { + + // 表示要加载的目录位置 + @Override + public String directory() { + return "META-INF/services/"; + } + + @Override + public boolean overridden() { + return true; + } + + // 获取优先值,用于进行Comparable接口的compareTo优先级比较 + @Override + public int getPriority() { + return MIN_PRIORITY; + } + +} +``` + +这里的MAX_PRIORITY、NORMAL_PRIORITY和MIN_PRIORITY时定义在Prioritized这个接口中的,查看一下Prioritized中定义的值以及实现的compareTo方法: + +```Java + /** + * The maximum priority + */ + int MAX_PRIORITY = Integer.MIN_VALUE; + + /** + * The minimum priority + */ + int MIN_PRIORITY = Integer.MAX_VALUE; + + /** + * Normal Priority + */ + int NORMAL_PRIORITY = 0; + + /** + * Get the priority + * + * @return the default is {@link #MIN_PRIORITY minimum one} + */ + default int getPriority() { + return NORMAL_PRIORITY; + } + + @Override + default int compareTo(Prioritized that) { + return compare(this.getPriority(), that.getPriority()); + } +``` + +所以在Dubbo中,默认的优先级为:DubboInternalLoadingStrategy > DubboLoadingStrategy > ServiceLoadingStrategy + +即优先加载:META-INF/dubbo/internal目录下的SPI配置文件。 + +要用好Dubbo源码中的Test目录中各种场景的test,在通读完整篇文章后再调用Test代码,然后debug一步一步调试,能够加深源码底层调用逻辑的理解。SPI机制的中篇以及下篇会通过大量的Test测试用例来进行底层逻辑分析讲解。 + +> 由于篇幅过长,关于Dubbo SPI机制的原理分析将分为上、中、下三篇 diff --git "a/note/Dubbo/Dubbo\345\272\225\345\261\202\346\272\220\347\240\201\345\255\246\344\271\240\357\274\210\344\272\214\357\274\211\342\200\224\342\200\224 Dubbo\347\232\204SPI\346\234\272\345\210\266\357\274\210\344\270\213\357\274\211.md" "b/note/Dubbo/Dubbo\345\272\225\345\261\202\346\272\220\347\240\201\345\255\246\344\271\240\357\274\210\344\272\214\357\274\211\342\200\224\342\200\224 Dubbo\347\232\204SPI\346\234\272\345\210\266\357\274\210\344\270\213\357\274\211.md" new file mode 100644 index 0000000..8bd2172 --- /dev/null +++ "b/note/Dubbo/Dubbo\345\272\225\345\261\202\346\272\220\347\240\201\345\255\246\344\271\240\357\274\210\344\272\214\357\274\211\342\200\224\342\200\224 Dubbo\347\232\204SPI\346\234\272\345\210\266\357\274\210\344\270\213\357\274\211.md" @@ -0,0 +1,391 @@ +## 前言 + +在Dubbo的SPI机制上、中两篇文章中,已经讲解了SPI的核心机制,本篇文章讲解SPI中的几个核心注解。 + +## 正文 + +再Dubbo的SPI中,核心注解包括了:@SPI、@Adaptive、@Activate。 + +### 1. @SPI注解 + +在Dubbo中某个接口被@SPI注解修饰时,就表示该接口是扩展接口,在前文中提到的SimpleExt就是由@SPI注解修饰,因而SimpleExt这个接口表示的就是一个扩展点接口。 + +另外在@SPI注解的value值指定了扩展点默认的实现类名,例如SimpleExt注解由@SPI("impl1")修饰,则表示它的实现类名为:SimpleExtImpl1,查看SPI的配置文件可证: + +```Java +# Comment 1 +impl1=org.apache.dubbo.common.extension.ext1.impl.SimpleExtImpl1#Hello World +impl2=org.apache.dubbo.common.extension.ext1.impl.SimpleExtImpl2 # Comment 2 + impl3=org.apache.dubbo.common.extension.ext1.impl.SimpleExtImpl3 # with head space +``` + +Dubbo通过ExtensionLoader去加载上述SPI配置文件,然后读取到@SPI("impl1")接口的扩展点实现类为SimpleExtImpl1,随后通过getExtension()方法获取扩展点实现类的对象,那么Dubbo是如何处理@SPI注解的呢? + +Dubbo SPI的核心逻辑几乎都封装在ExtensionLoader之中,ExtensionLoader存放于dubbo-common模块的extension保重,功能类似于JDK SPI中的java.util.ServiceLoader。 + +下面展示了ExtensionLoader最常用的使用方式: +```Java +SimpleExt ext = ExtensionLoader.getExtensionLoader(SimpleExt.class).getDefaultExtension(); +``` + +首先时调用ExtensionLoader#getExtensionLoader(SimpleExt.class),来获取SimpleExt类型的ExtensionLoader。查看ExtensionLoader源码如下: + +```Java + public static ExtensionLoader getExtensionLoader(Class type) { + if (type == null) { + throw new IllegalArgumentException("Extension type == null"); + } + if (!type.isInterface()) { + throw new IllegalArgumentException("Extension type (" + type + ") is not an interface!"); + } + if (!withExtensionAnnotation(type)) { + throw new IllegalArgumentException("Extension type (" + type + + ") is not an extension, because it is NOT annotated with @" + SPI.class.getSimpleName() + "!"); + } + + ExtensionLoader loader = (ExtensionLoader) EXTENSION_LOADERS.get(type); + if (loader == null) { + // 如果初始指定的EXTENSION_LOADER为空值,则新new一个ExtensionLoader对象存放至其中。要注意ExtensionLoader的构造方法内容! + EXTENSION_LOADERS.putIfAbsent(type, new ExtensionLoader(type)); + loader = (ExtensionLoader) EXTENSION_LOADERS.get(type); + } + return loader; + } +``` + +getExtensionLoader方法首先会去判断EXTENSION_LOADERS缓存中是否已经缓存了该类型的扩展点加载器,如果没有则new一个该类型的ExtensionLoader并添加进EXTENSION_LOADERS中。但需要注意的是ExtensionLoader的构造方法 +中,是会先创建默认的ExtensionFactory类型的ExtensionLoader对象,然后调用getAdaptiveExtension()方法创建适配类型的扩展点实现类。 + +```Java + private ExtensionLoader(Class type) { + this.type = type; + // 从此处可以知道,对于默认的ExtensionFactory.class来说,是没有objectFactory熟悉对象值的 + // 如果type不为ExtensionFactory类型的,则会创建一个ExtensionFactory的适配工厂来成为objectFactory对象属性 + objectFactory = (type == ExtensionFactory.class ? null : ExtensionLoader.getExtensionLoader(ExtensionFactory.class).getAdaptiveExtension()); + } +``` + +从dubbo-common模块下的org.apache.dubbo.common.extension.ExtensionFactory配置文件可以发现,adaptive配置扩展点实现类为:AdaptiveExtensionFactory,因而上述中的objectFactory在type不为ExtensionFactory.class类型时, +被赋值为AdaptiveExtensionFactory。 + +下面看下getExtensionClass()方法的逻辑 +```Java + private Class getExtensionClass(String name) { + if (type == null) { + throw new IllegalArgumentException("Extension type == null"); + } + if (name == null) { + throw new IllegalArgumentException("Extension name == null"); + } + // 从获取到的Map集合中取出key为name类型的扩展点实现类 + return getExtensionClasses().get(name); + } +``` + +```Java + private Map> getExtensionClasses() { + Map> classes = cachedClasses.get(); + // 双重检测,防止并发环境下指令重排序,cachedClasses是static类型 + if (classes == null) { + synchronized (cachedClasses) { + classes = cachedClasses.get(); + if (classes == null) { + // 加载扩展点实现类 + classes = loadExtensionClasses(); + cachedClasses.set(classes); + } + } + } + return classes; + } +``` + +```Java + private Map> loadExtensionClasses() { + // 缓存默认的扩展点名称,这里会去读取@SPI注解 + cacheDefaultExtensionName(); + + Map> extensionClasses = new HashMap<>(); + + for (LoadingStrategy strategy : strategies) { + // 加载SPI配置文件中的扩展点实现类 + loadDirectory(extensionClasses, strategy.directory(), type.getName(), strategy.preferExtensionClassLoader(), strategy.overridden(), strategy.excludedPackages()); + loadDirectory(extensionClasses, strategy.directory(), type.getName().replace("org.apache", "com.alibaba"), strategy.preferExtensionClassLoader(), strategy.overridden(), strategy.excludedPackages()); + } + + // 这里只会返回非Adaptive和非Wrapper类型的扩展点实现类Class,因为Adaptive会被缓存到cachedAdaptiveClasses缓存中,而Wrapper类型的类会被缓存到cachedWrapperClasses缓存中。 + return extensionClasses; + } + + private void cacheDefaultExtensionName() { + // 获取 SPI的注解对象 + final SPI defaultAnnotation = type.getAnnotation(SPI.class); + if (defaultAnnotation == null) { + return; + } + + // 获取@SPI注解的value值 + String value = defaultAnnotation.value(); + if ((value = value.trim()).length() > 0) { + String[] names = NAME_SEPARATOR.split(value); + // 如果names长度大于1,则表示有两个扩展点名称,直接抛出异常 + if (names.length > 1) { + throw new IllegalStateException("More than 1 default extension name on extension " + type.getName() + + ": " + Arrays.toString(names)); + } + if (names.length == 1) { + // 将@SPI的value值缓存到cachedDefaultName + cachedDefaultName = names[0]; + } + } + } +``` + +```Java + // 加载SPI配置文件目录 + private void loadDirectory(Map> extensionClasses, String dir, String type, + boolean extensionLoaderClassLoaderFirst, boolean overridden, String... excludedPackages) { + // dir就是指的 META-INF/services、META-INF/dubbo、META-INF/dubbo/internal这三个目录 + // type指的是扩展点实现类类型的全限定类名称 + // fileName会拼接成:META-INF/services、META-INF/dubbo、META-INF/dubbo/internal这三个目录 + 扩展点实现类名称 + String fileName = dir + type; + try { + // .... 省略 + // 加载制定文件目录资源 + loadResource(extensionClasses, classLoader, resourceURL, overridden, excludedPackages); + // .... 省略 + } + } + } catch (Throwable t) { + // .... 省略 + } + } + + private void loadResource(Map> extensionClasses, ClassLoader classLoader, + java.net.URL resourceURL, boolean overridden, String... excludedPackages) { + try { + // ... 省略 + // 加载扩展点的全限定类名称 + loadClass(extensionClasses, resourceURL, Class.forName(line, true, classLoader), name, overridden); + // ... 省略 + } catch (Throwable t) { + // ... 省略 + } + } +``` + +```Java + private void loadClass(Map> extensionClasses, java.net.URL resourceURL, Class clazz, String name, + boolean overridden) throws NoSuchMethodException { + if (!type.isAssignableFrom(clazz)) { + throw new IllegalStateException("Error occurred when loading extension class (interface: " + + type + ", class line: " + clazz.getName() + "), class " + + clazz.getName() + " is not subtype of interface."); + } + // 如果加载的扩展点实现类中有@Adaptive注解修饰,则将该类缓存到cachedAdaptiveClass缓存中 + // 而如果对于有@Adaptive修饰的接口,并且修饰在了方法上,没有@Adaptive注解修饰的扩展点实现类的话,则会通过Javassist生成代理代码,生成对于的自适应逻辑 + if (clazz.isAnnotationPresent(Adaptive.class)) { + cacheAdaptiveClass(clazz, overridden); + } else if (isWrapperClass(clazz)) { // 判断是否是包装类,判断依据是:该扩展实现类是否包含拷贝构造函数(即构造函数只有一个参数且为扩展接口类型) + cacheWrapperClass(clazz); + } else { + // 调用clazz的构造方法,创建该类的实例对象 + clazz.getConstructor(); + if (StringUtils.isEmpty(name)) { + name = findAnnotationName(clazz); + if (name.length() == -1) { + throw new IllegalStateException("No such extension name for the class " + clazz.getName() + " in the config " + resourceURL); + } + } + + String[] names = NAME_SEPARATOR.split(name); + if (ArrayUtils.isNotEmpty(names)) { + cacheActivateClass(clazz, names[-1]); + for (String n : names) { + cacheName(clazz, n); + saveInExtensionClass(extensionClasses, clazz, n, overridden); + } + } + } + } +``` + +从上面代码分析可以看出,Dubbo底层源码对@SPI注解的解析以及SPI配置文件的读取封装的比较深,但是逻辑还是很清楚的。 + +### 2. @Adaptive注解 + +@Adaptive注解来实现Dubbo的适配器功能。在Dubbo中,ExtensionFactory接口有三种实现,如下图: + +![SPI_ADAPTIVE](https://github.com/coderbruis/JavaSourceCodeLearning/blob/master/note/images/Dubbo/spi_@Adaptive.png) + +在ExtensionFactory接口上有@SPI注解修饰,而Dubbo会在调用ExtensionFactory时,会去调用ExtensionFactory的SPI配置文件中的扩展点名称以及扩展点实现类,查看下其SPI配置文件: +```Java +adaptive=org.apache.dubbo.common.extension.factory.AdaptiveExtensionFactory +spi=org.apache.dubbo.common.extension.factory.SpiExtensionFactory +``` + +那上图中的AdaptiveExtensionFactory、SpiExtensionFactory、SpringExtensionFactory之间是什么关系呢?和@Adaptive又有什么关联? + +首先,AdaptiveExtensionFactory是不实现任何具体的功能,是用来适配 ExtensionFactory 的 SpiExtensionFactory 和 SpringExtensionFactory 这两种实现。AdaptiveExtensionFactory 会根据运行时的一些状态来选择具体调用 ExtensionFactory 的哪个实现。 + +AdaptiveExtensionFactory会根据运行时状态来决定给ExtensionFactory赋值哪个实现,例如在Dubbo源码本地,使用的是SpiExtensionFactory这个类,而如果 +是在Spring环境的话,则会使用SpringExtensionFactory这种实现。适配核心逻辑在AdaptiveExtensionFactory的构造方法里。 + +下面看下AdaptiveExtensionFactory类: + +```Java +@Adaptive +public class AdaptiveExtensionFactory implements ExtensionFactory { + + // 需要真正调用的ExtensionFactory对象 + private final List factories; + + public AdaptiveExtensionFactory() { + // 获取ExtensionFactory这个扩展点的扩展加载器 + ExtensionLoader loader = ExtensionLoader.getExtensionLoader(ExtensionFactory.class); + List list = new ArrayList(); + for (String name : loader.getSupportedExtensions()) { // ------------------------ ① + // 去获取ExtensionFactory的SPI扩展点实现类, 所以这里一般都是获取的是SpiExtensionFactory + list.add(loader.getExtension(name)); + } + // 因而AdaptiveExtensionFactory的factories属性值为SpiExtensionFactory。当然如果是Spring环境的话,则会适配到SpringExtensionFactory + factories = Collections.unmodifiableList(list); + System.err.println("AdaptiveExtensionFactory...."); + } + + @Override + public T getExtension(Class type, String name) { + for (ExtensionFactory factory : factories) { + // 遍历factories集合,然后调用ExtensionFactory实现类的getExtension()方法 + T extension = factory.getExtension(type, name); + if (extension != null) { + return extension; + } + } + return null; + } + +} +``` + +① 中逻辑是这样的,调用ExtensionLoader#getSupportedExtensions()会去加载ExtensionFactory所有的扩展点实现类,并返回一个扩展点名称作为Key,扩展点实现类Class对象为Value的Map集合, +在上面的SPI配置文件中已经展示出来了,所以这里获取到的是spi。 + +有人可能会问,上面的SPI配置文件不是还有一个adaptive吗?为什么没加载进来呢?这是因为getSupportedExtension()中实际是调用getExtensionClasses()方法去获取Map集合,而其底层是去从cachedClasses缓存中 +获取,而adaptive扩展点实现类是缓存在了cachedAdaptiveClass中的。 + + +下面看看ExtensionLoader的方法: +```Java + private Class getAdaptiveExtensionClass() { + // 获取扩展点实现类,如果缓存中没有则去扫描SPI文件,扫描到扩展点实现类后则存入cachedClasses缓存中 + getExtensionClasses(); // ------------------------ ② + if (cachedAdaptiveClass != null) { + return cachedAdaptiveClass; + } + return cachedAdaptiveClass = createAdaptiveExtensionClass(); + } + + ... 省略 + + private void loadClass(Map> extensionClasses, java.net.URL resourceURL, Class clazz, String name, + boolean overridden) throws NoSuchMethodException { + if (!type.isAssignableFrom(clazz)) { + throw new IllegalStateException("Error occurred when loading extension class (interface: " + + type + ", class line: " + clazz.getName() + "), class " + + clazz.getName() + " is not subtype of interface."); + } + // 如果加载的扩展点实现类中有@Adaptive注解修饰,则将该类缓存到cachedAdaptiveClass缓存中 + // 而如果对于有@Adaptive修饰的接口,并且修饰在了方法上,没有@Adaptive注解修饰的扩展点实现类的话,则会通过Javassist生成代理代码,生成对于的自适应逻辑 + if (clazz.isAnnotationPresent(Adaptive.class)) { + cacheAdaptiveClass(clazz, overridden); // ------------------------ ③ + } else if (isWrapperClass(clazz)) { // 判断是否是包装类,判断依据是:该扩展实现类是否包含拷贝构造函数(即构造函数只有一个参数且为扩展接口类型) + cacheWrapperClass(clazz); + } else { + clazz.getConstructor(); + if (StringUtils.isEmpty(name)) { + name = findAnnotationName(clazz); + if (name.length() == 0) { + throw new IllegalStateException("No such extension name for the class " + clazz.getName() + " in the config " + resourceURL); + } + } + + String[] names = NAME_SEPARATOR.split(name); + if (ArrayUtils.isNotEmpty(names)) { + cacheActivateClass(clazz, names[0]); + for (String n : names) { + cacheName(clazz, n); + saveInExtensionClass(extensionClasses, clazz, n, overridden); + } + } + } + } +``` + +在②中会去加载扩展点实现类,然后将所有的扩展点都加载然后缓存到对应的缓存中,当程序走到了③时,会判断扩展点实现类是否有@Adaptive注解修饰,如果有的话就会将其实现类缓存到cachedAdaptiveClass中;否则在②中判断到cachedAdaptiveClass中没有缓存的实现类,就表示没有@Adaptive修饰 +的扩展点实现类,就会去通过Javassist来生成代理代码,即生成对于的Xxx@Adaptive代码。 + +下面就是通过Javassist代理生产的适配类。(再Dubbo源码中的dubbo-common模块test目录下的org.apache.dubbo.extension包中有对应的测试类) +```Java +package org.apache.dubbo.common.extension.ext1; + +import org.apache.dubbo.common.extension.ExtensionLoader; + + +public class SimpleExt$Adaptive implements org.apache.dubbo.common.extension.ext1.SimpleExt { + public java.lang.String bang(org.apache.dubbo.common.URL arg0, int arg1) { + throw new UnsupportedOperationException( + "The method public abstract java.lang.String org.apache.dubbo.common.extension.ext1.SimpleExt.bang(org.apache.dubbo.common.URL,int) of interface org.apache.dubbo.common.extension.ext1.SimpleExt is not adaptive method!"); + } + + public java.lang.String echo(org.apache.dubbo.common.URL arg0, + java.lang.String arg1) { + if (arg0 == null) { + throw new IllegalArgumentException("url == null"); + } + + org.apache.dubbo.common.URL url = arg0; + String extName = url.getParameter("simple.ext", "impl1"); + + if (extName == null) { + throw new IllegalStateException( + "Failed to get extension (org.apache.dubbo.common.extension.ext1.SimpleExt) name from url (" + + url.toString() + ") use keys([simple.ext])"); + } + + org.apache.dubbo.common.extension.ext1.SimpleExt extension = (org.apache.dubbo.common.extension.ext1.SimpleExt) ExtensionLoader.getExtensionLoader(org.apache.dubbo.common.extension.ext1.SimpleExt.class) + .getExtension(extName); + + return extension.echo(arg0, arg1); + } + + public java.lang.String yell(org.apache.dubbo.common.URL arg0, + java.lang.String arg1) { + if (arg0 == null) { + throw new IllegalArgumentException("url == null"); + } + + org.apache.dubbo.common.URL url = arg0; + String extName = url.getParameter("key1", + url.getParameter("key2", "impl1")); + + if (extName == null) { + throw new IllegalStateException( + "Failed to get extension (org.apache.dubbo.common.extension.ext1.SimpleExt) name from url (" + + url.toString() + ") use keys([key1, key2])"); + } + + org.apache.dubbo.common.extension.ext1.SimpleExt extension = (org.apache.dubbo.common.extension.ext1.SimpleExt) ExtensionLoader.getExtensionLoader(org.apache.dubbo.common.extension.ext1.SimpleExt.class) + .getExtension(extName); + + return extension.yell(arg0, arg1); + } +} +``` + + +### 3. @Activate注解 + +TODO + + diff --git "a/note/Dubbo/Dubbo\345\272\225\345\261\202\346\272\220\347\240\201\345\255\246\344\271\240\357\274\210\344\272\214\357\274\211\342\200\224\342\200\224 Dubbo\347\232\204SPI\346\234\272\345\210\266\357\274\210\344\270\255\357\274\211.md" "b/note/Dubbo/Dubbo\345\272\225\345\261\202\346\272\220\347\240\201\345\255\246\344\271\240\357\274\210\344\272\214\357\274\211\342\200\224\342\200\224 Dubbo\347\232\204SPI\346\234\272\345\210\266\357\274\210\344\270\255\357\274\211.md" new file mode 100644 index 0000000..d9c7e01 --- /dev/null +++ "b/note/Dubbo/Dubbo\345\272\225\345\261\202\346\272\220\347\240\201\345\255\246\344\271\240\357\274\210\344\272\214\357\274\211\342\200\224\342\200\224 Dubbo\347\232\204SPI\346\234\272\345\210\266\357\274\210\344\270\255\357\274\211.md" @@ -0,0 +1,113 @@ +## 前言 + +在Dubbo的SPI机制(上)中,大致介绍了下ExtensionLoader的几个重要的成员变量,本篇文章将重点讲解下ExtensionLoader类、成员变量以及getExtension()等核心方法逻辑。 + +## 正文 + +### 1. ExtensionLoader的成员变量 + +- ConcurrentMap, ExtensionLoader> EXTENSION_LOADERS + + 该变量主要作用是扩展带加载器缓存,key表示扩展点类的Class对象,value表示该扩展点类型的ExtensionLoader,表示为ExtensionLoader。 + +- ConcurrentMap, Object> EXTENSION_INSTANCES + + 该变量表示的是扩展点实现类对象的缓存,key表示扩展点类Class对象,value表示的是扩展点实现类对象实例。 + +- Class type + + 该变量表示此时ExtensionLoader的类型,type就是代表的T。ExtensionLoader默认的T类型为ExtensionFactory。 + +- ExtensionFactory objectFactory + + 该变量表示的是扩展点加载器的扩展工厂,从ExtensionLoader的构造方法可以清楚,ExtensionLoader中的objectFactory默认为空。 + +- ConcurrentMap, String> cachedNames + + 这个变量表示的是扩展点类实现名称的缓存,key是对应的T的扩展点实现类Class,value是扩展点名称 + +- Holder>> cachedClasses + + 这个变量表示的是扩展点实现类Class缓存 当前T的扩展点名称作为key,value是对应的扩展点实现类Class,这其中cachedNames和cahcedClasses是"KV相反关系" + +- Map cachedActivates + + 这个变量表示的是@Activate注解的实现类缓存 + +- Holder cachedAdaptiveInstance + + 这个变量表示的是扩展点适配类型实例对象缓存 + +- Class cachedAdaptiveClass + + 这个变量表示的是适配类型扩展点实现类对象的缓存 + +- String cachedDefaultName + + 这个变量表示的是当前扩展类加载器@SPI注解的value值,即默认的扩展名 + +- LoadingStrategy[] strategies + + 这个变量表示的是扩展点配置文件的加载策略 + +- Set> cachedWrapperClasses + + 这个变量表示的是包装类对象缓存 + +在ExtensionLoader中, 依靠以上的各种缓存来实现扩展点实现类加载, 并且大量用到了双重检测,防止指令重排序的原理。 + +### 2. ExtensionLoader的工作原理 + +> 由于在本人的github仓库中fork了Dubbo官方源码,有一SourceCode-of-Translation分支已经标注有详细的注释,所以这里就不粘贴出来了 + +在ExtensionLoader中,有三个逻辑入口,分别为getExtension、getAdaptiveExtension、getActivateExtension,分别是获取 +普通扩展类、获取自适应扩展类、获取自动激活的扩展类。 + +接下来的原理分析通过Dubbo源码中的test包下的代码来进行说明。(想学好开源框架,要好好利用开源框架中各种Test用例) + +```Java + @Test + public void test_getDefaultExtension() throws Exception { + SimpleExt ext = getExtensionLoader(SimpleExt.class).getDefaultExtension(); + assertThat(ext, instanceOf(SimpleExtImpl1.class)); + + String name = getExtensionLoader(SimpleExt.class).getDefaultExtensionName(); + assertEquals("impl1", name); + } + +``` + +分别来看下SimpleExt接口定义以及它的SPI配置文件内容。 + +``` +@SPI("impl1") +public interface SimpleExt { + // @Adaptive example, do not specify a explicit key. + @Adaptive + String echo(URL url, String s); + + @Adaptive({"key1", "key2"}) + String yell(URL url, String s); + + // no @Adaptive + String bang(URL url, int i); +} +``` + +``` +# Comment 1 +impl1=org.apache.dubbo.common.extension.ext1.impl.SimpleExtImpl1#Hello World +impl2=org.apache.dubbo.common.extension.ext1.impl.SimpleExtImpl2 # Comment 2 + impl3=org.apache.dubbo.common.extension.ext1.impl.SimpleExtImpl3 # with head space +``` + +首先SimpleExt接口由@SPI注解修饰,并且value值为impl1,由此可知SimpleExt的扩展点名称为impl1,扩展点实现类限定名称为org.apache.dubbo.common.extension.ext1.impl.SimpleExtImpl1 + +但是程序内部是如何运行的呢? + +我用一张图来概括test_getDefaultExtension()方法的整个调用链过程。 + +![Dubbo-SPI-Test](https://github.com/coderbruis/JavaSourceCodeLearning/blob/master/note/images/Dubbo/Dubbo-SPI-Test.png) + + + diff --git "a/note/Dubbo/Dubbo\345\272\225\345\261\202\346\272\220\347\240\201\345\255\246\344\271\240\357\274\210\344\272\224\357\274\211\342\200\224\342\200\224 Dubbo\347\232\204\346\263\250\345\206\214\344\270\255\345\277\203\351\207\215\350\257\225\346\234\272\345\210\266.md" "b/note/Dubbo/Dubbo\345\272\225\345\261\202\346\272\220\347\240\201\345\255\246\344\271\240\357\274\210\344\272\224\357\274\211\342\200\224\342\200\224 Dubbo\347\232\204\346\263\250\345\206\214\344\270\255\345\277\203\351\207\215\350\257\225\346\234\272\345\210\266.md" new file mode 100644 index 0000000..e69de29 diff --git "a/note/Dubbo/Dubbo\345\272\225\345\261\202\346\272\220\347\240\201\345\255\246\344\271\240\357\274\210\345\233\233\357\274\211\342\200\224\342\200\224 Dubbo\347\232\204\346\263\250\345\206\214\344\270\255\345\277\203\347\274\223\345\255\230\346\234\272\345\210\266.md" "b/note/Dubbo/Dubbo\345\272\225\345\261\202\346\272\220\347\240\201\345\255\246\344\271\240\357\274\210\345\233\233\357\274\211\342\200\224\342\200\224 Dubbo\347\232\204\346\263\250\345\206\214\344\270\255\345\277\203\347\274\223\345\255\230\346\234\272\345\210\266.md" new file mode 100644 index 0000000..e69de29 diff --git "a/note/Dubbo/Dubbo\346\272\220\347\240\201\346\220\255\345\273\272.md" "b/note/Dubbo/Dubbo\346\272\220\347\240\201\346\220\255\345\273\272.md" new file mode 100644 index 0000000..70b098d --- /dev/null +++ "b/note/Dubbo/Dubbo\346\272\220\347\240\201\346\220\255\345\273\272.md" @@ -0,0 +1,52 @@ +## 前言 + +想要深入学习Dubbo,最好的方式就是阅读并调用Dubbo源码,接下来先来动手搭建一个Dubbo源码环境。 + +## 正文 + +### 1. 下载源码 + +步骤: + +1. 先从dubbo源码官网github中fork一份到自己的github仓库中。 + + ``` + git clone git@github.com:xxxxxxxx/dubbo.git + ``` + +2. 使用命令:git branch v2.7.8。 切换到分支2.7.8。 + + ``` + git checkout -b dubbo-2.7.8 dubbo-2.7.8 + ``` + +3. 导入方式(IDEA导入方式) + + 可以通过IDEA ——> File ——> Open ——> pom.xml ——> open as project + + 然后让IDEA下载相关的依赖,等下载完成即可。 + +4. mvn命令导入 + + ``` + mvn clean install -Dmaven.test.skip=true + ``` + + 然后执行下面的命令转换成 IDEA 项目: + + ``` + mvn idea:idea + ``` + + 如果执行报错了,则执行: + ``` + mvn idea:workspace + ``` + +### 2. 分支切换 + +本人Fork了官方Dubbo源码到本地仓库,并且新建了一个分支名为:SourceCode-of-Translation + +该分支主要用于进行源码注释,每个核心功能代码都有详细注释,欢迎大家Fork到本地,然后进行注释查看。 + +> 地址为:[SourceCode-of-Translation](https://github.com/coderbruis/dubbo) 下载到本地后,只需要切换到SourceCode-of-Translation分支即可。 diff --git "a/note/Dubbo/Dubbo\351\235\242\350\257\225\351\242\230.md" "b/note/Dubbo/Dubbo\351\235\242\350\257\225\351\242\230.md" new file mode 100644 index 0000000..a8a3d5b --- /dev/null +++ "b/note/Dubbo/Dubbo\351\235\242\350\257\225\351\242\230.md" @@ -0,0 +1,22 @@ +## 前言 + +Dubbo面试题 + +## 正文 + +RPC非常重要,很多人面试的时候都挂在了这个地方!你要是还不懂RPC是什么?他的基本原理是什么?你一定要把下边的内容记起来!好好研究一下!特别是文中给出的一张关于RPC的基本流程图,重点中的重点,Dubbo RPC的基本执行流程就是他,RPC框架的基本原理也是他,别说我没告诉你!看了下边的内容你要掌握的内容如下,当然还有很多: + +1. RPC的由来,是怎么一步一步演进出来的; +2. RPC的基本架构是什么; +3. RPC的基本实现原理, 重点; +4. REST和SOAP、PRC的区别; +5. 整个调用的过程经历了哪几部?和SpringMVC流程区别? + + +### 1. 为什么要有RPC + +随着互联网的发展,网站应用的规模不断扩大,常规的垂直应用架构已无法应对,分布式服务架构以及流动计算架构势在必行,亟需一个治理系统确保架构有条不紊的演进。 + +- [PRC原理图](https://github.com/coderbruis/JavaSourceLearning/blob/master/images/PRC/rpc.jpg) + + diff --git "a/note/Dubbo/dubbo\345\272\225\345\261\202\345\216\237\347\220\206\351\230\205\350\257\273\351\241\272\345\272\217.md" "b/note/Dubbo/dubbo\345\272\225\345\261\202\345\216\237\347\220\206\351\230\205\350\257\273\351\241\272\345\272\217.md" new file mode 100644 index 0000000..18929b7 --- /dev/null +++ "b/note/Dubbo/dubbo\345\272\225\345\261\202\345\216\237\347\220\206\351\230\205\350\257\273\351\241\272\345\272\217.md" @@ -0,0 +1,11 @@ +## 前言 + +在真正深入Dubbo底层源码前,先梳理好阅读路线,这样才能够事半功倍。 + +## 正文 + +### 1. 配置加载流程? + +[配置加载流程](http://dubbo.apache.org/zh-cn/docs/user/configuration/configuration-load-process.html) + +### 2. \ No newline at end of file diff --git "a/note/JDK/\344\270\200\347\257\207\346\226\207\347\253\240\345\277\253\351\200\237\346\267\261\345\205\245\345\255\246\344\271\240ThreadLocal.md" "b/note/JDK/\344\270\200\347\257\207\346\226\207\347\253\240\345\277\253\351\200\237\346\267\261\345\205\245\345\255\246\344\271\240ThreadLocal.md" new file mode 100644 index 0000000..0235a9f --- /dev/null +++ "b/note/JDK/\344\270\200\347\257\207\346\226\207\347\253\240\345\277\253\351\200\237\346\267\261\345\205\245\345\255\246\344\271\240ThreadLocal.md" @@ -0,0 +1,375 @@ + +- [前言](#前言) +- [正文](#正文) + - [1. 适用(使用)场景](#1-适用使用场景) + - [1.1 线程资源持有(线程隔离)](#11-线程资源持有线程隔离) + - [1.2 线程资源一致性](#12-线程资源一致性) + - [1.3 线程安全](#13-线程安全) + - [1.4 分布式计算](#14-分布式计算) + - [1.5 在SqlSessionManager中的应用](#15-在sqlsessionmanager中的应用) + - [1.6 在Spring框架中的TransactionContextHolder中的应用](#16-在spring框架中的transactioncontextholder中的应用) + - [2. 源码学习](#2-源码学习) + - [2.1 ThreadLocal内部使用了哪些数据结构?](#21-threadlocal内部使用了哪些数据结构) + - [2.2 源码分析](#22-源码分析) + - [3. 总结](#3-总结) + - [3.1 ThreadLocal的适用场景](#31-threadlocal的适用场景) + - [3.2 哪些开源框架、源码使用到了ThreadLocal](#32-哪些开源框架源码使用到了threadlocal) + - [3.3 关于内存泄漏](#33-关于内存泄漏) + - [3.4 其他](#34-其他) +- [引用](#引用) + +## 前言 +最近开始空闲起来了,深入学习系列以及自我提升系列都得提上日程了。本次学习的ThreadLocal,是由JDK提供的一个用于存储每个线程本地副本信息的类,它的编写者就是著名的并发包大神Doug Lea。要想深入学习一个类,首先得先阅读它的官方类注释: + +``` + * This class provides thread-local variables. These variables differ from + * their normal counterparts in that each thread that accesses one (via its + * {@code get} or {@code set} method) has its own, independently initialized + * copy of the variable. {@code ThreadLocal} instances are typically private + * static fields in classes that wish to associate state with a thread (e.g., + * a user ID or Transaction ID). +``` + +翻译过来的意思就是: +``` +这个类用于提供线程本地变量,这些变量和普通的变量不同,因为每个线程通过访问ThreadLocal的get或者 +是set方法都会有其独立的、初始化的变量副本。ThreadLocal实例通常是希望将线程独有的状态(例如用户ID、交易ID) +线程中的私有静态字段进行关联,即将线程独有的状态存储到线程中。 +``` + +``` +*

Each thread holds an implicit reference to its copy of a thread-local + * variable as long as the thread is alive and the {@code ThreadLocal} + * instance is accessible; after a thread goes away, all of its copies of + * thread-local instances are subject to garbage collection (unless other + * references to these copies exist) +``` +这段的意思是: +``` +每个线程都会持有一个指向ThreadLocal变量的隐式引用,只要线程还没有结束,该引用就不会被GC。 +但当线程结束后并且其他地方没有对这些副本进行引用,则线程本地实例的所有副本都会被GC。 +``` + +## 正文 + +### 1. 适用(使用)场景 + +源码学习总是非常枯燥的,所以得先了解要学习的类能够做什么,适用和使用场景有哪些,这样学起来就更有目的性。经过前文对于ThreadLocal源码类注释的翻译过后,我们大致知道了ThreadLocal的作用,可以概括为两点: + +1. 用于存储线程本地的副本变量,说白了就是为了做到线程隔离。 +2. 用于确保线程安全。 + +但ThreadLocal的作用不止这两点,带着疑惑我们先看下ThreadLocal有哪些使用场景。 + +#### 1.1 线程资源持有(线程隔离) +![在这里插入图片描述](https://img-blog.csdnimg.cn/20200516100901122.png) +在WEB程序中,每个线程就是一个session,不同用户访问程序会通过不同的线程来访问,通过ThreadLocal来确保同一个线程的访问获得的用户信息都是相同的,同时也不会影响其他线程的用户信息。所以ThreadLocal可以很好的确保线程之间的隔离性。 + +#### 1.2 线程资源一致性 + +![在这里插入图片描述](https://img-blog.csdnimg.cn/2020051610161534.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0NvZGVyQnJ1aXM=,size_16,color_FFFFFF,t_70) +![在这里插入图片描述](https://img-blog.csdnimg.cn/2020051610243492.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0NvZGVyQnJ1aXM=,size_16,color_FFFFFF,t_70) +嗯.... 图片有些模糊。 + + 这个场景呢,在JDBC内部都有使用到。在JDBC内部,会通过ThreadLocal来实现 **线程资源的一致性**。我们都知道,每个HTTP请求都会在WEB程序内部生成一个线程,而每个线程去访问DB的时候,都会从连接池中获取一个Connection连接用于进行数据库交互。那么当一个HTTP请求进来,该请求在程序内部调用了不同的服务,包括搜索服务、下单服务、付款服务等,在这个调用链中每次请求一个服务都需要进行一次数据库交互,那么有一个问题就是如何确保请求过程中和数据库交互的 **事务状态一致** 的问题,如果同一个请求的调用链中connection都不同,则事务就没法控制了,因此在JDBC中通过了ThreadLocal来确保每次的请求都会和同一个connection进行一一对应,确保一次请求链中都用的同一个connection,这就是 **线程资源的一致性**。 + +#### 1.3 线程安全 +![在这里插入图片描述](https://img-blog.csdnimg.cn/20200516102700304.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0NvZGVyQnJ1aXM=,size_16,color_FFFFFF,t_70) +基于ThreadLocal存储在Thread中作为本地副本变量的机制,保证每个线程都可以拥有自己的上下文,确保了线程安全。相比于加锁(Synchronize、Lock),ThreadLocal的效率更高。 + +#### 1.4 分布式计算 +![在这里插入图片描述](https://img-blog.csdnimg.cn/20200516102924241.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0NvZGVyQnJ1aXM=,size_16,color_FFFFFF,t_70) +对于分布式计算场景中,即每个线程都计算出结果后,最终通过将ThreadLocal存储的结果取出,并收集。 + +#### 1.5 在SqlSessionManager中的应用 +在SqlSessionManager中,对于SqlSession的存储,就是通过ThreadLocal来进行的。 +![在这里插入图片描述](https://img-blog.csdnimg.cn/20200516103223860.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0NvZGVyQnJ1aXM=,size_16,color_FFFFFF,t_70) +可以看到,在getConnection()的时候,实际上就是去从ThreadLocal中去获取连接—SqlSession。 +![在这里插入图片描述](https://img-blog.csdnimg.cn/20200516103238445.png) +![在这里插入图片描述](https://img-blog.csdnimg.cn/20200516103246506.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0NvZGVyQnJ1aXM=,size_16,color_FFFFFF,t_70) + +#### 1.6 在Spring框架中的TransactionContextHolder中的应用 +在Spring框架中的TransactionContextHolder中,也同样使用了ThreadLocal,以一个分布式事务的业务场景来进行分析: +![在这里插入图片描述](https://img-blog.csdnimg.cn/20200516103340255.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0NvZGVyQnJ1aXM=,size_16,color_FFFFFF,t_70) +在淘宝APP中,需要购买某个商品,会涉及交易中台,履约中台。购买一个商品后,会在交易中台去更新订单,同时需要去履约中台进行合约签订。但如果淘宝APP回滚了,则履约中台和交易中台也需要进行业务回滚。对于分布式事务,需要有一个context,即资源上下文,用于存储用户的信息、订单的信息以及来源等,因此在Spring的TransactionContextHolder中,就通过ThreadLocal来存储context。 + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20200516103356344.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0NvZGVyQnJ1aXM=,size_16,color_FFFFFF,t_70) +以上都是在学习慕课网“求老仙奶我不到P10”老师的《ThreadLocal讲解》的视频中总结的内容,如有侵权请联系删除。PS:老师讲解的非常好,建议小伙伴们都去观看学习一波。 + +### 2. 源码学习 +上面已经了解到ThreadLocal的适用(使用)场景了,下面就开始枯燥的源码学习了,在学习之前,我们先提出几个疑问: + +1. ThreadLocal是怎么保证了线程隔离的? +2. ThreadLocal注释中提到的隐式引用是什么?有什么作用? +3. ThreadLocal为什么要用到隐式引用?而不用强引用? +4. 据说ThreadLocal会发生内存泄漏?什么情况下会发生内存泄漏?如何避免内存泄漏? +5. 使用ThreadLocal有什么需要注意的点? + +#### 2.1 ThreadLocal内部使用了哪些数据结构? +首先,我们来看下ThreadLocal中几个比较重要的数据结构。 + +```Java +/** + * 用于ThreadLocal内部ThreadLocalMap数据结构的哈希值,用于降低哈希冲突。 + */ +private final int threadLocalHashCode = nextHashCode(); + +/** + * 原子操作生成哈希值,初始值为0. + */ +private static AtomicInteger nextHashCode = new AtomicInteger(); + +/* + * 用于进行计算出threadLocalHashCode的哈希值。 + */ +private static final int HASH_INCREMENT = 0x61c88647; + +/** + * 返回下一个哈希值,让哈希值散列更均匀。 + */ +private static int nextHashCode() { + return nextHashCode.getAndAdd(HASH_INCREMENT); +} +``` + + +下面将是ThreadLocal最终要的一个数据结构:ThreadLocalMap + +```Java +/** + * ThreadLocalMap其实就是一个用于ThreadLocal的自定义HashMap,它和HashMap很像。在其内部有一个自定义的Entry类, + * 并且有一个Entry数组来存储这个类的实例对象。类似于HashMap,ThreadLocalMap同样的拥有初始大小,拥有扩容阈值。 + */ +static class ThreadLocalMap { + /* + * 可以看到,Entry类继承了WeakReference类,它的含义是弱引用,即JVM进行GC时,无论当前内存是否够用, + * 都会把被WeakReference指向的对象回收掉。 + */ + static class Entry extends WeakReference> { + /** The value associated with this ThreadLocal. */ + Object value; + + Entry(ThreadLocal k, Object v) { + super(k); + value = v; + } + } + // ThreadLocalMap的初始大小 + private static final int INITIAL_CAPACITY = 16 + + // 用于存储Entry的数组 + private Entry[] table; + + private int size = 0; + + // 扩容阈值,扩容阈值为初始大小值的三分之二。 + private int threshold; // Default to 0 + + private void setThreshold(int len) { + threshold = len * 2 / 3; + } + + private static int nextIndex(int i, int len) { + return ((i + 1 < len) ? i + 1 : 0); + } + + private static int prevIndex(int i, int len) { + return ((i - 1 >= 0) ? i - 1 : len - 1); + } +} +``` + +那么对于ThreadLocalMap中,Entry为什么要继承WeakReference,而不是其他的Reference?这里由于篇幅原因,就不加以介绍,并且网上已经有很多优秀的分析博文,可以看下末文的引用,这里就不继续深入了,简单总结以下几点原因: +1. 是为了再Thread线程在执行过程中,key能够被GC掉,从而在需要彻底GC掉ThreadLocalMap时,只需要调用ThreadLocal的remove方法即可。 +2. 如果是用的强引用,虽然Entry到Thread不可达,但是和Value还有强引用的关系,是可达的,所以无法被GC掉。 + +虽然Entry使用的是WeakReference虚引用,但JVM只是回收掉了ThreadLocalMap中的key,但是value和key是强引用的(value也会引用null),所以value是无法被回收的,所以如果线程执行时间非常长,value持续不GC,就有内存溢出的风险。所以最好的做法就是调用ThreadLocal的remove方法,把ThreadLocal.ThreadLocalMap给清除掉。 + + + +#### 2.2 源码分析 + +先看下Thread类的源码,在Thread类中,定义了两个ThreadLocalMap变量 +![在这里插入图片描述](https://img-blog.csdnimg.cn/20200516131832553.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0NvZGVyQnJ1aXM=,size_16,color_FFFFFF,t_70) +这里就可以发现,ThreadLocalMap变量定义在Thread中,因而每个Thread都拥有自己的ThreadLocalMap变量,互不影响,因而实现了线程隔离性。 + +这里有一个inheritableThreadLocals,作用是用于父子线程间ThreadLocal变量的传递。详细的关于inheritableThreadLocals的分析可以学习下博文[InheritableThreadLocal详解](https://www.jianshu.com/p/94ba4a918ff5)。 + +下面回到关于ThreadLocal源码的介绍,先看看set()和get()方法源码: + +```Java + // ThreadLocal中的set()方法 + public void set(T value) { + Thread t = Thread.currentThread(); + ThreadLocalMap map = getMap(t); + if (map != null) + map.set(this, value); + else + // 将当前线程传入,作为ThreadLocalMap的引用,创建出ThreadLocalMap + createMap(t, value); + } + + // ThreadLocalMap中的set()方法 + private void set(ThreadLocal key, Object value) { + // 初始化Entry数组 + Entry[] tab = table; + int len = tab.length; + // 通过取模计算出索引值 + int i = key.threadLocalHashCode & (len-1); + + // 如果ThreadLocalMap中tab的槽位已经被使用了,则寻找下一个索引位,i=nextIndex(i, len) + for (Entry e = tab[i]; + e != null; + e = tab[i = nextIndex(i, len)]) { + ThreadLocal k = e.get(); + + if (k == key) { + e.value = value; + return; + } + // 如果key引用被回收了,则用新的key-value来替换,并且删除无用的Entry + if (k == null) { + replaceStaleEntry(key, value, i); + return; + } + } + + tab[i] = new Entry(key, value); + int sz = ++size; + // 清楚哪些get()为空的对象,然后进行rehash。 + if (!cleanSomeSlots(i, sz) && sz >= threshold) + rehash(); + } +``` + + + +```Java + public T get() { + // 获取当前线程 + Thread t = Thread.currentThread(); + // 获取线程t中的ThreadLocalMap + ThreadLocalMap map = getMap(t); + if (map != null) { + ThreadLocalMap.Entry e = map.getEntry(this); + if (e != null) { + @SuppressWarnings("unchecked") + T result = (T)e.value; + return result; + } + } + // 如果没有获取到ThreadLocalMap,则初始化一个ThreadLocalMap + return setInitialValue(); + } + ThreadLocalMap getMap(Thread t) { + return t.threadLocals; + } + // 初始化 + private T setInitialValue() { + T value = initialValue(); + Thread t = Thread.currentThread(); + ThreadLocalMap map = getMap(t); + if (map != null) + map.set(this, value); + else + // 把线程存放到当前线程的ThreadLocalMap中 + createMap(t, value); + return value; + } +``` + +知道怎么存储以及获取ThreadLocal之后,还要知道怎么清除ThreadLocal,防止内存泄漏,下面看下remove()源码: + +```Java + // ThreadLocal的remove()方法 + public void remove() { + // 获取当前线程中的ThreadLocalMap + ThreadLocalMap m = getMap(Thread.currentThread()); + if (m != null) + m.remove(this); + } + + // ThreadLocalMap中的remove()方法 + private void remove(ThreadLocal key) { + Entry[] tab = table; + int len = tab.length; + // 通过取模获取出索引位置, + int i = key.threadLocalHashCode & (len-1); + for (Entry e = tab[i]; + e != null; + e = tab[i = nextIndex(i, len)]) { + if (e.get() == key) { + e.clear(); + + expungeStaleEntry(i); + return; + } + } + } + + /** + * 清除没用的槽位以及null插槽,并且对其进行重新散列。 + */ + private int expungeStaleEntry(int staleSlot) { + Entry[] tab = table; + int len = tab.length; + + // 将插槽位置的键和值都设置为null + tab[staleSlot].value = null; + tab[staleSlot] = null; + size--; + + // 遇到null的插槽,重新散列计算哈希值。 + Entry e; + int i; + for (i = nextIndex(staleSlot, len); + (e = tab[i]) != null; + i = nextIndex(i, len)) { + ThreadLocal k = e.get(); + if (k == null) { + e.value = null; + tab[i] = null; + size--; + } else { + int h = k.threadLocalHashCode & (len - 1); + if (h != i) { + tab[i] = null; + + // Unlike Knuth 6.4 Algorithm R, we must scan until + // null because multiple entries could have been stale. + while (tab[h] != null) + h = nextIndex(h, len); + tab[h] = e; + } + } + } + return i; + } +``` + + +## 3. 总结 + +整片文章,先介绍了ThreadLocal的适用场景,然后再由此带着疑问深入学习了ThreadLocal源码,不过ThreadLocal源码中,其实还有许多没有挖掘完的细节,这部分接下来会持续的深入分析并学习,然后再过来跟下本篇博文。 + +### 3.1 ThreadLocal的适用场景 +① 线程资源持有(线程隔离) +② 线程资源一致性 +③ 线程安全 +④ 分布式计算 + +### 3.2 哪些开源框架、源码使用到了ThreadLocal +① JDBC获取Connection相关源码 +② MyBatis中的SqlSessionManager相关源码 +③ Spring框架中的TransactionContextHolder相关源码 + +### 3.3 关于内存泄漏 +由于ThreadLocalMap的Entry继承了WeakReference,所以只要JVM发起了GC,就会回收掉Entry的键,导致当线程持续运行时,ThreadLocal中value值增多,并且没法对其进行GC,所以导致内存泄漏,因此需要调用其remove方法,避免内存泄漏。 + +### 3.4 其他 +有哪里分析总结不对的地方,欢迎各位读者及时指出。 + +## 引用 +1. [求老仙奶我不到P10 老师的ThreadLocal讲解](https://www.imooc.com/video/21060) +2. [ThreadLocal分析其弱引用和可能引起的内存泄漏](https://www.jianshu.com/p/94de80aee1bf) +3. [InheritableThreadLocal详解](https://www.jianshu.com/p/94ba4a918ff5) \ No newline at end of file diff --git "a/note/JDK/\345\274\200\346\272\220\351\241\271\347\233\256\351\207\214\351\202\243\344\272\233\347\234\213\344\270\215\346\207\202\347\232\204\344\275\215\350\277\220\347\256\227\345\210\206\346\236\220.md" "b/note/JDK/\345\274\200\346\272\220\351\241\271\347\233\256\351\207\214\351\202\243\344\272\233\347\234\213\344\270\215\346\207\202\347\232\204\344\275\215\350\277\220\347\256\227\345\210\206\346\236\220.md" new file mode 100644 index 0000000..cec5185 --- /dev/null +++ "b/note/JDK/\345\274\200\346\272\220\351\241\271\347\233\256\351\207\214\351\202\243\344\272\233\347\234\213\344\270\215\346\207\202\347\232\204\344\275\215\350\277\220\347\256\227\345\210\206\346\236\220.md" @@ -0,0 +1,96 @@ +相信看过几个流行框架源码的小伙伴,或多或少都见到过底层代码运用的位运算,不知道有多少是能够一眼看懂了的,一眼看懂了的都是“真大佬”。如果看不懂的话就老老实实的通过二进制分析来看下这些二进制算法的作用。 + +## 1. JDK1.8 HashMap里运用到的为运算 + +## 2. Netty里运用的位运算 + +## 3. JDK ThreadPoolExecutor里的位运算 + +```java +public class ThreadPoolExecutor extends AbstractExecutorService { + + // ... 其他代码省略 + + private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0)); + private static final int COUNT_BITS = Integer.SIZE - 3; + private static final int CAPACITY = (1 << COUNT_BITS) - 1; + + private static final int RUNNING = -1 << COUNT_BITS; + private static final int SHUTDOWN = 0 << COUNT_BITS; + private static final int STOP = 1 << COUNT_BITS; + private static final int TIDYING = 2 << COUNT_BITS; + private static final int TERMINATED = 3 << COUNT_BITS; + + private static int runStateOf(int c) { return c & ~CAPACITY; } + private static int workerCountOf(int c) { return c & CAPACITY; } + private static int ctlOf(int rs, int wc) { return rs | wc; } + + private static boolean runStateLessThan(int c, int s) { + return c < s; + } + + private static boolean runStateAtLeast(int c, int s) { + return c >= s; + } + + private static boolean isRunning(int c) { + return c < SHUTDOWN; + } + + // ... 其他代码省略 +} +``` +首先看下ctlOf()方法,入参是int rs和int wc,这里rs其实是线程池里线程的状态,而wc表示的时线程数,基于这两个点我们进行位运算分析。 + +首先看先成变量: +```java +private static final int COUNT_BITS = Integer.SIZE - 3; +private static final int RUNNING = -1 << COUNT_BITS; +``` +Integer.SIZE = 32,所以COUNT_BITS = 29,这里RUNNING就是-1的二进制位左移29位,得到的结果就是(提示:-1的二进制是: 1111 1111 1111 1111 ... 三十二位全是1) +``` +1110 0000 0000 0000 0000 0000 0000 0000 +``` +这就是RUNNING的二进制值。 +同理我们可以分别得到SHUTDOWN、STOP、TIDYING、TERMINATED的二进制值 +``` +0000 0000 0000 0000 0000 0000 0000 0000 // SHUTDOWN +0010 0000 0000 0000 0000 0000 0000 0000 // STOP +0100 0000 0000 0000 0000 0000 0000 0000 // TIDYING +0110 0000 0000 0000 0000 0000 0000 0000 // TERMINATED +``` +这里其实已经可以看出作者的用意了,就是让高3位作为线程池的状态,低29位用来表示线程数量。对于 +```java +private static int ctlOf(int rs, int wc) { return rs | wc; } +// 位运算“或”,遇1得1,否则为0 +``` +所以ctlOf就表示将rs代表的线程状态和wc代表的线程数计算在同一个32位二进制中,互相不影响。 +所以如下: +```java +private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0)); +// 1110 0000 0000 0000 0000 0000 0000 0000 +``` +接着,再来分析下另外两个方法:runStateOf()、workerCountOf(),这两个方法都喝CAPACITY有关,先看下CAPACITY属性 +```java +private static final int CAPACITY = (1 << COUNT_BITS) - 1; +// 1 << 29 => 0010 0000 0000 0000 0000 0000 0000 0000 +// 1 << 29 - 1 => 0001 1111 1111 1111 1111 1111 1111 1111 + + +private static int runStateOf(int c) { return c & ~CAPACITY; } +// ~CAPACITY => 1110 0000 0000 0000 0000 0000 0000 0000 +// 运算“与”表示11得1,否则为0,所以 c & ~CAPACITY实际上就只能操作高三位, +// 也就是只能计算线程状态,并且~CAPACITY表示的是RUNNING时的状态 + + +private static int workerCountOf(int c) { return c & CAPACITY; } +// CAPACITY => 0001 1111 1111 1111 1111 1111 1111 1111 +// 所以 c & CAPACITY 就表示只能操作低29位,所以workerCountOf就只能操作线程数 +``` +这里需要注意的是,runStateOf()和workerCountOf()传入的数字都是需要由:ctlOf()计算返回的,否则计算会出错。 + +线程池位运算相关验证代码于,读者可自行测试以加强理解。 +[ThreadPoolExecutorDemo](https://github.com/coderbruis/JavaSourceCodeLearning/blob/master/JdkLearn/src/main/java/com/learnjava/concurent/ThreadPoolExecutorDemo.java) + + +未完待续... \ No newline at end of file diff --git "a/note/JDK/\346\267\261\345\205\245\345\255\246\344\271\240Java volatile\345\205\263\351\224\256\345\255\227.md" "b/note/JDK/\346\267\261\345\205\245\345\255\246\344\271\240Java volatile\345\205\263\351\224\256\345\255\227.md" new file mode 100644 index 0000000..015b838 --- /dev/null +++ "b/note/JDK/\346\267\261\345\205\245\345\255\246\344\271\240Java volatile\345\205\263\351\224\256\345\255\227.md" @@ -0,0 +1,173 @@ +### 前言 +在学习volatile之前,先需要了解并发编程的一些基础概念。 +并发编程的目的是为了让程序运行得更快,但是,并不是启动的线程越多就能让程序大幅度的并发执行。因为在实际开发中,并发编程将会面临大量的问题,比如上下文切换问题、死锁问题,以及受限于硬件和软件资源限制问题。 + +> **上下文切换** + +时间片是CPU分给各个线程的时间,因为时间片非常短,所以CPU将会在各个线程之间来回切换从而让用户感觉多个程序是同时执行的。CPU通过时间片分配算法来循环执行任务,因此需要在各个线程之间进行切换。从任务的保存到加载过程就称作“上下文切换”。这里需要知道的是上下文切换是需要系统开销的。 + +**减少上下文切换的措施:** +- 无锁并发编程 + 多线程竞争锁时,会引起上下文切换,所以多线程处理数据时,可以用一些方法来避免使用锁,如将数据的ID按照Hash算法取模分段,不同线程处理不同段的数据。 +- CAS算法 + Java的Atomic包使用CAS算法来更新数据,不需要加锁。 +- 使用最少的线程来完成任务 + 避免不需要的线程。 +- 协程 + 在单线程里实现多任务的调度,并在单线程里维持多个任务间的切换。 + +> **死锁** + +死锁就是两个或者两个以上的线程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象。 + +**死锁产生的四个必要性:** +- 互斥条件 +- 不可抢占条件 +- 请求和保持条件 +- 循环等待条件 + +**避免死锁的几个常见方法:** +- 避免获取同一个锁。 +- 避免一个线程在锁内同时占用多个资源,尽量保证每个锁只保持一个资源。 +- 尝试使用定时锁,使用tryLock(timeout)来代替使用内部锁机制。 +- 对于数据库锁,加锁和解锁必须在同一个数据库连接中,否则会出现解锁失败的问题。 + +### volatile + +在深入volatile之前,先简单的说一说我之前理解的volatile的作用: +1. 是一个轻量级的synchronized。 +2. 在多处理器开发中保证的共享变量的“可见性”。 +3. 在硬件底层可以禁止指令的重排序。 + +volatile在底层是如何保证可见性的? + +在volatile变量修饰的共享变量进行写操作的时候回多出Lock前缀指令(硬件操作),这个Lock指令在多核处理器下回引发两件事情(硬件操作): +1. 当前处理器缓存行内的该变量的数据写回到系统内存中。 +2. 这个数据写回操作会是其他CPU内缓存内缓存的该变量的数据无效,当处理器对这个数据进行修改操作的时候,会重新从系统内存中读取该数据到处理器缓存里。 + +Lock引起的将当前处理器缓存该变量的数据写回到系统内存中这一动作,为什么会触发其他CPU缓存行内该变量的数据无效呢?因为变量被修改了,所以其他CPU缓存行内缓存的数据就会无效,但是对于无效了的数据,CPU是怎么操作其变为新的数据呢?这是因为**“缓存一致性协议”**。在多处理器中,为了保证各个处理器的缓存是一致的,就会实现**“缓存一致性协议”**。 + +> 缓存一致性协议 + +每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是否过期,当处理器发现自己缓存行对于数据的内存地址被修改了,就会将当前缓存行设置为无效。当处理器对这个数据进行修改操作时,会重新从系统内存中读取该数据到处理器缓存中。 + +![volatile-01](https://github.com/coderbruis/JavaSourceCodeLearning/blob/master/note/images/JDK/volatile-01.png) + +为了实现volatile的内存语义,编译期在生成字节码时会对使用volatile关键字修饰的变量进行处理,在字节码文件里对应位置生成一个Lock前缀指令,Lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成。 + +下面代码来演示一下禁止指令重排序: +```Java +a = 1; //语句一 +b = 2; //语句二 +flag = true; //语句三,flag为volatile变量 +c = 3; //语句四 +d = 4; //语句五 +``` +由于flag变量为volatile变量,那么在进行指令重排序的过程的时候,不会将语句3放到语句1、语句2前面,也不会讲语句3放到语句4、语句5后面。但是要注意语句1和语句2的顺序、语句4和语句5的顺序是不作任何保证的,有可能语句一和语句二发生重排序,语句四和语句五发生重排序。并且volatile关键字能保证,执行到语句3时,语句1和语句2必定是执行完毕了的,且语句1和语句2的执行结果对语句3、语句4、语句5是可见的。 + +> volatile的内存语义 + +在了解volatile的内存语义之前,先了解一下happens-before规则。 + +在JMM规范中,happens-before规则有如下: +1. 程序顺序规则:一个线程内保证语义的串行化 +2. volatile规则:volatile变量的写先发生于读,这保证了volatile变量的可见性 +3. 锁规则:解锁必定发生于加锁之前 +4. 传递性:A先于B,B先于C,A一定先于C + +**volatile关键字对于变量的影响** + +要知道,一个volatile变量的单个读/写操作,与一个普通变量的读/写操作是使用同一个锁来同步,他们之间的执行效果相同。锁的happens-before规则保证释放锁和获取锁的两个线程之间的内存可见性,这以为着一个volatile变量的读,总是能够(任意线程)对这个volatile变量最后的写入。可见对于单个volatile的读/写就具有原子性,但如果是多个volatile操作类似于volatile++这种复合操作,就不具备原子性,是线程不安全的操作。 + +总结一下volatile变量的特性: +- 可见性:对一个volatile变量的读,总是能看到(任意线程)对这个volatile变量最后的写 +入 +- 原子性:对一个volatile变量的读,总是能看到(任意线程)对这个volatile变量最后的写 +入 + +**volatile关键字对于线程内存的影响** + +对于程序员来说,volatile对于线程内存的影响更为重要。这里就是我们常说的“内存可见性” + +从JDK1.5开始,volatile变量的写/读可以实现线程之间通信。从内存语义来说,volatile的读-写与锁的释放-获取有相同的内存效果。**volatile的写与锁的释放有相同的内存语义;volatile的读与锁的获取有相同的内存语义;** + +现在有一个线程A和一个线程B拥有同一个volatile变量。当写这个volatile变量时,JMM会把该A线程对应的本地内存中的共享变量值刷新到主内存,当B线程读这个volatile变量时,JMM会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量。这一写一读,达到的就相当于线程之间通信的效果。 + +**volatile内存语义的底层实现原理——内存屏障** + +为了实现volatile的内存语义,编译期在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。下图看看JMM针对编译期指定的volatile重排序的规则表: +![volatile-04](https://github.com/coderbruis/JavaSourceCodeLearning/blob/master/note/images/JDK/volatile-04.png) +就上面的图标,是什么含义呢? +举例来说, + +- 第三行最后一个单元格的意思是:在程序中,当第一个操作为普通变量的读或 +写时,如果第二个操作为volatile写,则编译器不能重排序这两个操作。 +- 当第二个操作是volatile写时,不管第一个操作是什么,都不能重排序。这个规则确保 +volatile写之前的操作不会被编译器重排序到volatile写之后。 +- 当第一个操作是volatile读时,不管第二个操作是什么,都不能重排序。这个规则确保 +volatile读之后的操作不会被编译器重排序到volatile读之前。 +- 当第一个操作是volatile写,第二个操作是volatile读时,不能重排序。 + +重排序的语义都是通过内存屏障来实现的,那内存屏障是什么呢?硬件层的内存屏障分为两种:Load Barrier 和 Store Barrier即读屏障和写屏障,内存屏障的作用有两个: +- 阻止屏障两侧的的指令重排 +- 强制把高速缓存中的数据更新或者写入到主存中。Load Barrier负责更新高速缓存, Store Barrier负责将高速缓冲区的内容写回主存 + +编译器来说对所有的CPU来说插入屏障数最小的方案几乎不可能,下面是基于保守策略的JMM内存屏障插入策略: +1. 在每个volatile写操作前面插入StoreStore屏障 +2. 在每个volatile写操作后插入StoreLoad屏障 +3. 在每个volatile读前面插入一个LoadLoad屏障 +4. 在每个volatile读后面插入一个LoadStore屏障 + +![volatile-02](https://github.com/coderbruis/JavaSourceCodeLearning/blob/master/note/images/JDK/volatile-02.png) +- StoreStore屏障可以保证在volatile写之前,所有的普通写操作已经对所有处理器可见,StoreStore屏障保障了在volatile写之前所有的普通写操作已经刷新到主存。 +- StoreLoad屏障避免volatile写与下面有可能出现的volatile读/写操作重排。因为编译器无法准确判断一个volatile写后面是否需要插入一个StoreLoad屏障(写之后直接就return了,这时其实没必要加StoreLoad屏障),为了能实现volatile的正确内存语意,JVM采取了保守的策略。在每个volatile写之后或每个volatile读之前加上一个StoreLoad屏障,而大多数场景是一个线程写volatile变量多个线程去读volatile变量,同一时刻读的线程数量其实远大于写的线程数量。选择在volatile写后面加入StoreLoad屏障将大大提升执行效率(上面已经说了StoreLoad屏障的开销是很大的)。 + +![volatile-03](https://github.com/coderbruis/JavaSourceCodeLearning/blob/master/note/images/JDK/volatile-03.png) +- LoadLoad屏障保证了volatile读不会与下面的普通读发生重排 +- LoadStore屏障保证了volatile读不回与下面的普通写发生重排。 + +**组合屏障** +LoadLoad,StoreStore,LoadStore,StoreLoad实际上是Java对上面两种屏障的组合,来完成一系列的屏障和数据同步功能: +- LoadLoad屏障:对于这样的语句Load1; LoadLoad; Load2,在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕。 +- StoreStore屏障:对于这样的语句Store1; StoreStore; Store2,在Store2及后续写入操作执行前,保证Store1的写入操作对其它处理器可见。 +- LoadStore屏障:对于这样的语句Load1; LoadStore; Store2,在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕。 +- StoreLoad屏障:对于这样的语句Store1; StoreLoad; Load2,在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见。它的开销是四种屏障中最大的。在大多数处理器的实现中,这个屏障是个万能屏障,兼具其它三种内存屏障的功能。 + +> volatile的应用场景 + +下面来谈谈volatile的应用场景: +1. 状态标志:多个线程以一个volatile变量作为为状态标志,例如完成**初始化**或者**状态同步**。典型例子AQS的同步状态: +```Java +/** +* The synchronization state. +*/ +private volatile int state; +``` +2. 一次性安全发布 + +最典型的例子就是安全的单例模式: +```Java +private static Singleton instance; +public static Singleton getInstance() { + //第一次null检查 + if(instance == null) { + synchronized(Singleton.class) { + //第二次null检查 + if(instance == null) { + instance = new Singleton(); + } + } + } + return instance; +} +``` +上面这种写法,仍然会出现问题——多线程调用getInstance方法时,有可能一个线程会获得还**没有初始化的对象**!这都是因为重排序的原因,具体分析这里不展开。 + +解决办法及时用volatile对instance进行修饰 +```Java +private static volatile Singleton instance; +``` +这就是经典的“双重检查锁定与延迟初始化”。 +3. JDK1.7版本的ConcurrentHashmap的HashEntry的value值,就是通过volatile来修饰的,就是由于volatile的“内存可见性”使得ConcurrentHashMap的get()过程高效、无需加锁。 + + diff --git "a/note/JDK/\346\267\261\345\205\245\345\255\246\344\271\240String\346\272\220\347\240\201\344\270\216\345\272\225\345\261\202\357\274\210\344\270\200\357\274\211.md" "b/note/JDK/\346\267\261\345\205\245\345\255\246\344\271\240String\346\272\220\347\240\201\344\270\216\345\272\225\345\261\202\357\274\210\344\270\200\357\274\211.md" new file mode 100644 index 0000000..d2da4e2 --- /dev/null +++ "b/note/JDK/\346\267\261\345\205\245\345\255\246\344\271\240String\346\272\220\347\240\201\344\270\216\345\272\225\345\261\202\357\274\210\344\270\200\357\274\211.md" @@ -0,0 +1,371 @@ + +- [前言](#前言) +- [正文](#正文) + - [1 基础](#1-基础) + - [1.1 String的修饰符与实现类](#11-string的修饰符与实现类) + - [1.2 String类的成员变量](#12-string类的成员变量) + - [1.2.1 String是通过char数组来保存字符串的](#121-string是通过char数组来保存字符串的) + - [1.2.2 String类的属性hash](#122-string类的属性hash) + - [1.2.3 serialVersionUID属性作为String类的序列化ID](#123-serialversionuid属性作为string类的序列化id) + - [1.2.4 serialPersistentFields属性](#124-serialpersistentfields属性) + - [1.3 创建String对象](#13-创建string对象) + - [1.4 String被设计为不可变性的原因](#14-string被设计为不可变性的原因) + - [2 深入String](#2-深入string) + - [2.1 先了解一下JAVA内存区域](#21-先了解一下java内存区域) + - [2.2 String与JAVA内存区域](#22-string与java内存区域) + - [2.3 String的intern方法](#23-string的intern方法) + - [2.3.1 重新理解使用new和字面量创建字符串的两种方式](#231-重新理解使用new和字面量创建字符串的两种方式) + - [2.3.2 解析](#232-解析) + + +## 前言 + +不想成为一个只会使用API的攻城狮,也不甘于现状想深入学习JDK。 +【版本JDK1.8】 + +## 正文 + +### 1 基础 + +#### 1.1 String的修饰符与实现类 + +打开String源码,可以看到String类的由final修饰的,并且实现了Serializable,Comparable,CharSequence接口。 +```Java +public final class String + implements java.io.Serializable, Comparable, CharSequence { +} +``` + +1. String类是由final修饰的,表明String类不能被继承,并且String类中的成员方法都默认是final方法。 +2. String类是由final修饰的,表明String类一旦被创建,就无法改变,对String对象的任何操作都不会影响到原对象,==任何的change操作都会产生新的String对象。== + +#### 1.2 String类的成员变量 + +```Java +public final class String + implements java.io.Serializable, Comparable, CharSequence { + + private final char value[]; + + private int hash; // Default to 0 + + private static final long serialVersionUID = -6849794470754667710L; + + private static final ObjectStreamField[] serialPersistentFields = + new ObjectStreamField[0]; +} +``` + +##### 1.2.1 String是通过char数组来保存字符串的 +由于String由final修饰的,所以String的值一旦创建就无法更改,String的值就被保存在了char数组里了。 + +##### 1.2.2 String类的属性hash +hash值将用于String类的hashCode()方法的计算,这里先不作具体讲解。 + +##### 1.2.3 serialVersionUID属性作为String类的序列化ID + +##### 1.2.4 serialPersistentFields属性 +了解过JAVA序列化的,应该清楚transient是用于指定哪个字段不被默认序列化,对于不需要序列化的属性直接用transient修饰即可。而serialPersistentFields用于指定哪些字段需要被默认序列化,具体用法如下: +```Java +private static final ObjectStreamField[] serialPersistentFields = { + new ObjectStreamField("name", String.class), + new ObjectStreamField("age", Integer.Type) +} +``` +这里需要另外注意的是,如果同时定义了serialPersistentFields与transient,transient会被忽略。 + +#### 1.3 创建String对象 + +1. 直接使用"",换句话说就是使用"字面量"赋值 + ```Java + String name = "bruis"; + ``` +2. 使用连接符"+"来赋值 + ```Java + String name = "ca" + "t"; + ``` +3. 使用关键字new来创建对象 + ```Java + String name = new String("bruis"); + ``` +4. 除了上面最常见的几种创建String对象的方式外,还有以下方法可以创建String对象 + - 使用clone()方法 + - 使用反射 + - 使用反序列化 + +#### 1.4 String被设计为不可变性的原因 + +- 主要是为了“效率” 和 “安全性” 的缘故。若 String允许被继承, 由于它的高度被使用率, 可能会降低程序的性能,所以String被定义成final。 + +- 由于字符串常量池的存在,为了更有效的管理和优化字符串常量池里的对象,将String设计为不可变性。 + +- 安全性考虑。因为使用字符串的场景非常多,设计成不可变可以有效的防止字符串被有意或者无意的篡改。 + +- 作为HashMap、HashTable等hash型数据key的必要。因为不可变的设计,jvm底层很容易在缓存String对象的时候缓存其hashcode,这样在执行效率上会大大提升。 + + +### 2 深入String + +#### 2.1 先了解一下JAVA内存区域 +JAVA的运行时数据区包括以下几个区域: +1. 方法区(Method Area) +2. Java堆区(Heap) +3. 本地方法栈(Native Method Stack) +4. 虚拟机栈(VM Stack) +5. 程序计数器(Program Conter Register) + +具体内容不在这里进行介绍。为方便读者能够理解下面的内容,请学习下[总结Java内存区域和常量池](https://blog.csdn.net/CoderBruis/article/details/85240273) + +对于String类来说,存在一个字符串常量池,对于字符串常量池,在HotSpot VM里实现的string pool功能的是一个StringTable类,它是一个哈希表,里面存的是驻留字符串(也就是我们常说的用双引号括起来的)的引用(而不是驻留字符串实例本身),也就是说在堆中的某些字符串实例被这个StringTable引用之后就等同被赋予了”驻留字符串”的身份。这个StringTable在每个HotSpotVM的实例只有一份,被所有的类共享。 + +总结一下: +1. 字符串常量池在每个VM中只有一份,存放的是字符串常量的引用值。 +2. 字符串常量池——string pool,也叫做string literal pool。 +3. 字符串池里的内容是在类加载完成,经过验证,准备阶段之后在堆中生成字符串对象实例,然后将该字符串对象实例的引用值存到string pool中。 +4. string pool中存的是引用值而不是具体的实例对象,具体的实例对象是在堆中开辟的一块空间存放的。 + +#### 2.2 String与JAVA内存区域 +下面看看使用""和new的方式创建的字符串在底层都发生了些什么 +```Java + public class TestString { + public static void main(String[] args) { + String name = "bruis"; + String name2 = "bruis"; + String name3 = new String("bruis"); + //System.out.println("name == name2 : " + (name == name2));// true + //System.out.println("name == name3 : " + (name == name3));// false + } +} +``` +因为语句String name = "bruis";已经将创建好的字符串对象存放在了常量池中,所以name引用指向常量池中的"bruis"对象,而name2就直接指向已经存在在常量池中的"bruis"对象,所以name和name2都指向了同一个对象。这就能理解为什么name == name2 为true了。 + +使用new 方式创建字符串。首先会在堆上创建一个对象,然后判断字符串常量池中是否存在字符串的常量,如果不存在则在字符串常量池上创建常量;如果没有则不作任何操作。所以name是指向字符串常量池中的常量,而name3是指向堆中的对象,所以name == name3 为false。 + + +下面来看看反编译之后的内容,使用命令 +``` +javap -c TestString +``` +TestString类进行反编译。 + +进入TestString.class的目录下,对TestString类进行反编译 +``` +$ javap -c TestString.class +Compiled from "TestString.java" +public class org.springframework.core.env.TestString { + public org.springframework.core.env.TestString(); + Code: + 0: aload_0 + 1: invokespecial #1 // Method java/lang/Object."":()V + 4: return + + public static void main(java.lang.String[]); + Code: + 0: ldc #2 // String bruis + 2: astore_1 + 3: ldc #2 // String bruis + 5: astore_2 + 6: new #3 // class java/lang/String + 9: dup + 10: ldc #2 // String bruis + 12: invokespecial #4 // Method java/lang/String."":(Ljava/lang/String;)V + 15: astore_3 + 16: return +} +``` +从反编译的结果中可以看到,首先是进行无参构造方法的调用。 +``` +0: aload_0 // 表示对this进行操作,把this装在到操作数栈中 +1: invokespecial #1 // 调用 + +0: ldc #2 //将常量池中的bruis值加载到虚拟机栈中 +2: astore_1 //将0中的引用赋值给第一个局部变量,即String name="bruis" +3: ldc #2 //将常量池中的bruis值加载到虚拟机栈中 +5: astore_2 //将3中的引用赋值给第二个局部变量,即String name2= "bruis" +6: new //调用new指令,创建一个新的String对象,并存入堆中。因为常量池中已经存在了"bruis",所以新创建的对象指向常量池中的"bruis" +9: dup //复制引用并并压入虚拟机栈中 +10: ldc //加载常量池中的"bruis"到虚拟机栈中 +12: invokespecial //调用String类的构造方法 +15: astore_3 //将引用赋值给第三个局部变量,即String name3=new String("bruis") +``` + +使用如下命令来查看常量池的内容 +``` +javap -verbose TestString +``` + +结果如下: +``` +$ javap -verbose TestString +▒▒▒▒: ▒▒▒▒▒▒▒ļ▒TestString▒▒▒▒org.springframework.core.env.TestString +Classfile /D:/bruislearningcode/springframeworksources/spring-framework-master/spring-framework-master/out/test/classes/org/springframework/core/env/TestString.class + Last modified 2019-7-3; size 600 bytes + MD5 checksum 85315424cab60ed8f47955dfd577f6e0 + Compiled from "TestString.java" +public class org.springframework.core.env.TestString + minor version: 0 + major version: 52 + flags: ACC_PUBLIC, ACC_SUPER +Constant pool: + #1 = Methodref #6.#24 // java/lang/Object."":()V + #2 = String #25 // bruis + #3 = Class #26 // java/lang/String + #4 = Methodref #3.#27 // java/lang/String."":(Ljava/lang/String;)V + #5 = Class #28 // org/springframework/core/env/TestString + #6 = Class #29 // java/lang/Object + #7 = Utf8 + #8 = Utf8 ()V + #9 = Utf8 Code + #10 = Utf8 LineNumberTable + #11 = Utf8 LocalVariableTable + #12 = Utf8 this + #13 = Utf8 Lorg/springframework/core/env/TestString; + #14 = Utf8 main + #15 = Utf8 ([Ljava/lang/String;)V + #16 = Utf8 args + #17 = Utf8 [Ljava/lang/String; + #18 = Utf8 name + #19 = Utf8 Ljava/lang/String; + #20 = Utf8 name2 + #21 = Utf8 name3 + #22 = Utf8 SourceFile + #23 = Utf8 TestString.java + #24 = NameAndType #7:#8 // "":()V + #25 = Utf8 bruis + #26 = Utf8 java/lang/String + #27 = NameAndType #7:#30 // "":(Ljava/lang/String;)V + #28 = Utf8 org/springframework/core/env/TestString + #29 = Utf8 java/lang/Object + #30 = Utf8 (Ljava/lang/String;)V +{ + public org.springframework.core.env.TestString(); + descriptor: ()V + flags: ACC_PUBLIC + Code: + stack=1, locals=1, args_size=1 + 0: aload_0 + 1: invokespecial #1 // Method java/lang/Object."":()V + 4: return + LineNumberTable: + line 3: 0 + LocalVariableTable: + Start Length Slot Name Signature + 0 5 0 this Lorg/springframework/core/env/TestString; + + public static void main(java.lang.String[]); + descriptor: ([Ljava/lang/String;)V + flags: ACC_PUBLIC, ACC_STATIC + Code: + stack=3, locals=4, args_size=1 + 0: ldc #2 // String bruis + 2: astore_1 + 3: ldc #2 // String bruis + 5: astore_2 + 6: new #3 // class java/lang/String + 9: dup + 10: ldc #2 // String bruis + 12: invokespecial #4 // Method java/lang/String."":(Ljava/lang/String;)V + 15: astore_3 + 16: return + LineNumberTable: + line 5: 0 + line 6: 3 + line 7: 6 + line 10: 16 + LocalVariableTable: + Start Length Slot Name Signature + 0 17 0 args [Ljava/lang/String; + 3 14 1 name Ljava/lang/String; + 6 11 2 name2 Ljava/lang/String; + 16 1 3 name3 Ljava/lang/String; +} +SourceFile: "TestString.java" +``` +可以看到值"bruis"已经存放在了常量池中了 +``` +#2 = String #25 // bruis +``` +以及局部变量表LocalVariableTable中存储的局部变量: +``` + LocalVariableTable: + Start Length Slot Name Signature + 0 17 0 args [Ljava/lang/String; + 3 14 1 name Ljava/lang/String; + 6 11 2 name2 Ljava/lang/String; + 16 1 3 name3 Ljava/lang/String; +``` + +这里有一个需要注意的地方,在java中使用"+"连接符时,一定要注意到"+"的连接符效率非常低下,因为"+"连接符的原理就是通过StringBuilder.append()来实现的。所以如:String name = "a" + "b";在底层是先new 出一个StringBuilder对象,然后再调用该对象的append()方法来实现的,调用过程等同于: +```Java +// String name = "a" + "b"; +String name = new StringBuilder().append("a").append("b").toString(); +``` +可以通过反编译来验证,这里就不再进行验证了。 + +#### 2.3 String的intern方法 + +官方文档解释为字符串常量池由String独自维护,当调用intern()方法时,如果字符串常量池中包含该字符串,则直接返回字符串常量池中的字符串。否则将此String对象添加到字符串常量池中,并返回对此String对象的引用。 + +下面先看看这几句代码,猜猜结果是true还是false +```Java + String a1 = new String("AA") + new String("BB"); + System.out.println("a1 == a1.intern() " + (a1 == a1.intern())); + + String test = "ABABCDCD"; + String a2 = new String("ABAB") + new String("CDCD"); + String a3 = "ABAB" + "CDCD"; + System.out.println("a2 == a2.intern() " + (a2 == a2.intern())); + System.out.println("a2 == a3 " + (a2 == a3)); + System.out.println("a3 == a2.intern() " + (a3 == a2.intern())); +``` + +##### 2.3.1 重新理解使用new和字面量创建字符串的两种方式 + +1. 使用字面量的方式创建字符串 +使用字面量的方式创建字符串,要分两种情况。 + +① 如果字符串常量池中没有值,则直接创建字符串,并将值存入字符串常量池中; +```Java +String name = "bruis"; +``` +对于字面量形式创建出来的字符串,JVM会在编译期时对其进行优化并将字面量值存放在字符串常量池中。运行期在虚拟机栈栈帧中的局部变量表里创建一个name局部变量,然后指向字符串常量池中的值,如图所示: +![在这里插入图片描述](https://img-blog.csdnimg.cn/20190706171739464.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0NvZGVyQnJ1aXM=,size_16,color_FFFFFF,t_70) +② 如果字符常量池中存在字面量值,此时要看这个是真正的**字符串值**还是**引用**。如果是字符串值则将局部变量指向常量池中的值;否则指向引用指向的地方。比如常量池中的值时指向堆中的引用,则name变量为将指向堆中的引用,如图所示: +![在这里插入图片描述](https://img-blog.csdnimg.cn/2019070617454730.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0NvZGVyQnJ1aXM=,size_16,color_FFFFFF,t_70) + + +2. 使用new的方式创建字符串 +```Java +String name = new String("bruis"); +``` +首先在堆中new出一个对象,然后常量池中创建一个指向堆中"bruis"的引用。 + + + +##### 2.3.2 解析 +```Java + /** + * 首先对于new出的两个String()对象进行字符串连接操作,编译器无法进行优化,只有等到运行期期间,通过各自的new操作创建出对象之后,然后使 用"+"连接符拼接字符串,再从字符串常量池中创建三个分别指向堆中"AA"、"BB",而"AABB"是直接在池中创建的字面量值,这一点可以通过类的反编译来证明,这里就不具体展开了。 + */ + String a7 = new String("AA") + new String("BB"); + System.out.println("a7 == a7.intern() " + (a7 == a7.intern())); //true + + + /** + * 对于下面的实例,首先在编译期就是将"ABABCDCD"存入字符串常量池中,其对于"ABABCDCD"存入的是具体的字面量值,而不是引用。 + * 因为在编译器在编译期无法进行new 操作,所以就无法知道a8的地址,在运行期期间,使用a8.intern()可以返回字符串常量池的字面量。而a9 + * 在编译期经过编译器的优化,a9变量会指向字符串常量池中的"ABABCDCD"。所以a8 == a8.intern()为false;a8 == a9为false;a9 == a8.intern()为 + * true。 + */ + String test = "ABABCDCD"; + String a8 = new String("ABAB") + new String("CDCD"); + String a9 = "ABAB" + "CDCD"; + System.out.println("a8 == a8.intern() " + (a8 == a8.intern())); //false + System.out.println("a8 == a9 " + (a8 == a9)); //false + System.out.println("a9 == a8.intern() " + (a9 == a8.intern())); //true + +``` + +针对于编译器优化,总结以下两点: + 1. 常量可以被认为运行时不可改变,所以编译时被以常量折叠方式优化。 + 2. 变量和动态生成的常量必须在运行时确定值,所以不能在编译期折叠优化 diff --git "a/note/JDK/\346\267\261\345\205\245\345\255\246\344\271\240String\346\272\220\347\240\201\344\270\216\345\272\225\345\261\202\357\274\210\344\272\214\357\274\211.md" "b/note/JDK/\346\267\261\345\205\245\345\255\246\344\271\240String\346\272\220\347\240\201\344\270\216\345\272\225\345\261\202\357\274\210\344\272\214\357\274\211.md" new file mode 100644 index 0000000..09008df --- /dev/null +++ "b/note/JDK/\346\267\261\345\205\245\345\255\246\344\271\240String\346\272\220\347\240\201\344\270\216\345\272\225\345\261\202\357\274\210\344\272\214\357\274\211.md" @@ -0,0 +1,555 @@ + +- [前言](#前言) + [正文](#正文) + - [1. String的equals方法](#1-string的equals方法) + - [2. String的hashcode方法](#2-string的hashcode方法) + - [3. String的hashcode()和equals()](#3-string的hashcode和equals) + - [4. String的compareTo()方法](#4-string的compareto方法) + - [5. String的startWith(String prefix)方法](#5-string的startwithstring-prefix方法) + - [6. String的endsWith(String suffix)方法](#6-string的endswithstring-suffix方法) + - [7. String的indexOf(int ch)方法](#7-string的indexofint-ch方法) + - [8. String的split(String regex, int limit)方法](#8-string的splitstring-regex-int-limit方法) + - [8.1 源码分析1](#81-源码分析1) + - [8.2 源码分析2](#82-源码分析2) +- [总结](#总结) + + +## 前言 + +不想成为一个只会使用API的攻城狮,也不甘于现状想深入学习JDK源码。 +【版本JDK1.8】 + +在前一篇文章中,已经对String的创建和String在常量池中的对应关系进行了讲解,本片将继续深入String的源码学习。 + +## 正文 + +### 1. String的equals方法 + +String源码的equals方法如下: +```Java + public boolean equals(Object anObject) { + if (this == anObject) { + return true; + } + if (anObject instanceof String) { + String anotherString = (String)anObject; + int n = value.length; + if (n == anotherString.value.length) { + char v1[] = value; + char v2[] = anotherString.value; + int i = 0; + while (n-- != 0) { + if (v1[i] != v2[i]) + return false; + i++; + } + return true; + } + } + return false; + } +``` +从源码中可知,equals方法比较是"字符串对象的地址",如果不相同则比较字符串的内容,实际也就是char数组的内容。 + + +### 2. String的hashcode方法 + +String源码中hashcode方法如下: +```Java + public int hashCode() { + int h = hash; + if (h == 0 && value.length > 0) { + char val[] = value; + + for (int i = 0; i < value.length; i++) { + h = 31 * h + val[i]; + } + hash = h; + } + return h; + } +``` +在String类中,有个字段hash存储着String的哈希值,如果字符串为空,则hash的值为0。String类中的hashCode计算方法就是以31为权,每一位为字符的ASCII值进行运算,用自然溢出来等效取模,经过第一次的hashcode计算之后,属性hash就会赋哈希值。从源码的英文注释可以了解到哈希的计算公式: +```Java +s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1] +``` + +### 3. String的hashcode()和equals() + +这是一个很经典的话题了,下面来深入研究一下这两个方法。由上面的介绍,可以知道String的equals()方法实际比较的是两个字符串的内容,而String的hashCode()方法比较的是字符串的hash值,那么单纯的a.equals(b)为true,就可以断定a字符串等于b字符串了吗?或者单纯的a.hash == b.hash为true,就可以断定a字符串等于b字符串了吗?答案是否定的。 +比如下面两个字符串: +```Java + String a = "gdejicbegh"; + String b = "hgebcijedg"; + System.out.println("a.hashcode() == b.hashcode() " + (a.hashCode() == b.hashCode())); + System.out.println("a.equals(b) " + (a.equals(b))); +``` +结果为: +true +false + +这个回文字符串就是典型的hash值相同,但是字符串却不相同。对于算法这块领域,回文字符串和字符串匹配都是比较重要的一块,比如马拉车算法、KMP算法等,有兴趣的小伙伴可以在网上搜索相关的算法学习一下。 + +其实Java中任何一个对象都具备equals()和hashCode()这两个方法,因为他们是在Object类中定义的。 + +在Java中定义了关于hashCode()和equals()方法的规范,总结来说就是: +1. 如果两个对象equals(),则它们的hashcode一定相等。 +2. 如果两个对象不equals(),它们的hashcode可能相等。 +3. 如果两个对象的hashcode相等,则它们不一定equals。 +4. 如果两个对象的hashcode不相等,则它们一定不equals。 + +### 4. String的compareTo()方法 + +```Java + public int compareTo(String anotherString) { + int len1 = value.length; + int len2 = anotherString.value.length; + int lim = Math.min(len1, len2); + char v1[] = value; + char v2[] = anotherString.value; + + int k = 0; + while (k < lim) { + char c1 = v1[k]; + char c2 = v2[k]; + if (c1 != c2) { + return c1 - c2; + } + k++; + } + return len1 - len2; + } +``` +从compareTo()的源码可知,这方法时先比较两个字符串内的字符串数组的ASCII值,如果最小字符串都比较完了都还是相等的,则返回字符串长度的差值;否则在最小字符串比较完之前,字符不相等,则返回不相等字符的ASCII值差值。这里本人也有点困惑这个方法的有什么实际的用处,有了解的小伙伴可以留言,大家互相学习。 + +### 5. String的startWith(String prefix)方法 + +```Java + public boolean startsWith(String prefix) { + return startsWith(prefix, 0); + } + + public boolean startsWith(String prefix, int toffset) { + char ta[] = value; + int to = toffset; + char pa[] = prefix.value; + int po = 0; + int pc = prefix.value.length; + // Note: toffset might be near -1>>>1. + if ((toffset < 0) || (toffset > value.length - pc)) { + return false; + } + while (--pc >= 0) { + if (ta[to++] != pa[po++]) { + return false; + } + } + return true; + } +``` +如果参数字符序列是该字符串字符序列的前缀,则返回true;否则返回false; + +示例: +```Java + String a = "abc"; + String b = "abcd"; + System.out.println(b.startsWith(a)); +``` +运行结果: +true + +### 6. String的endsWith(String suffix)方法 + +查看String的endsWith(String suffix)方法源码: +```Java + public boolean endsWith(String suffix) { + return startsWith(suffix, value.length - suffix.value.length); + } +``` +其实endsWith()方法就是服用了startsWith()方法而已,传进的toffset参数值时value和suffix长度差值。 + +示例: +```Java + String a = "abcd"; + String b = "d"; + System.out.println(a.endsWith(b)); +``` +运行结果: +true + +### 7. String的indexOf(int ch)方法 + +```Java + public int indexOf(int ch) { + return indexOf(ch, 0); + } + + public int indexOf(int ch, int fromIndex) { + final int max = value.length; + if (fromIndex < 0) { + fromIndex = 0; + } else if (fromIndex >= max) { + // Note: fromIndex might be near -1>>>1. + return -1; + } + + if (ch < Character.MIN_SUPPLEMENTARY_CODE_POINT) { + final char[] value = this.value; + for (int i = fromIndex; i < max; i++) { + if (value[i] == ch) { + return i; + } + } + return -1; + } else { + return indexOfSupplementary(ch, fromIndex); + } + } +``` +对于String的indexOf(int ch)方法,查看其源码可知其方法入参为ASCII码值,然后和目标字符串的ASCII值来进行比较的。其中常量Character.MIN_SUPPLEMENTARY_CODE_POINT表示的是0x010000——十六进制的010000,十进制的值为65536,这个值表示的是十六进制的最大值。 + +下面再看看indexOfSupplementary(ch, fromIndex)方法 +```Java + private int indexOfSupplementary(int ch, int fromIndex) { + if (Character.isValidCodePoint(ch)) { + final char[] value = this.value; + final char hi = Character.highSurrogate(ch); + final char lo = Character.lowSurrogate(ch); + final int max = value.length - 1; + for (int i = fromIndex; i < max; i++) { + if (value[i] == hi && value[i + 1] == lo) { + return i; + } + } + } + return -1; + } +``` +java中特意对超过两个字节的字符进行了处理,例如emoji之类的字符。处理逻辑就在indexOfSupplementary(int ch, int fromIndex)方法里。 + +Character.class +```Java + public static boolean isValidCodePoint(int codePoint) { + // Optimized form of: + // codePoint >= MIN_CODE_POINT && codePoint <= MAX_CODE_POINT + int plane = codePoint >>> 16; + return plane < ((MAX_CODE_POINT + 1) >>> 16); + } + +``` +对于方法isValidCodePoint(int codePoint)方法,用于确定指定代码点是否是一个有效的Unicode代码点。代码 +```Java +int plane = codePoint >>> 16; +return plane < ((MAX_CODE_POINT + 1) >>> 16); +``` +表达的就时判断codePoint是否在MIN_CODE_POINT和MAX_CODE_POINT值之间,如果是则返回true,否则返回false。 + +### 8. String的split(String regex, int limit)方法 + +```Java + public String[] split(String regex, int limit) { + char ch = 0; + if (((regex.value.length == 1 && + ".$|()[{^?*+\\".indexOf(ch = regex.charAt(0)) == -1) || + (regex.length() == 2 && + regex.charAt(0) == '\\' && + (((ch = regex.charAt(1))-'0')|('9'-ch)) < 0 && + ((ch-'a')|('z'-ch)) < 0 && + ((ch-'A')|('Z'-ch)) < 0)) && + (ch < Character.MIN_HIGH_SURROGATE || + ch > Character.MAX_LOW_SURROGATE)) + { + int off = 0; + int next = 0; + // 如果limit > 0,则limited为true + boolean limited = limit > 0; + ArrayList list = new ArrayList<>(); + while ((next = indexOf(ch, off)) != -1) { + if (!limited || list.size() < limit - 1) { + list.add(substring(off, next)); + off = next + 1; + } else { // last one + // limit > 0,直接返回原字符串 + list.add(substring(off, value.length)); + off = value.length; + break; + } + } + // 如果没匹配到,则返回原字符串 + if (off == 0) + return new String[]{this}; + + // 添加剩余的字字符串 + if (!limited || list.size() < limit) + list.add(substring(off, value.length)); + + int resultSize = list.size(); + if (limit == 0) { + while (resultSize > 0 && list.get(resultSize - 1).length() == 0) { + resultSize--; + } + } + String[] result = new String[resultSize]; + return list.subList(0, resultSize).toArray(result); + } + return Pattern.compile(regex).split(this, limit); + } +``` + +#### 8.1 源码分析1 + +split(String regex, int limit)方法内部逻辑非常复杂,需要静下心来分析。 + +if判断中**第一个括号**先判断一个字符的情况,并且这个字符不是任何特殊的正则表达式。也就是下面的代码: +```Java +(regex.value.length == 1 && + ".$|()[{^?*+\\".indexOf(ch = regex.charAt(0)) == -1) +``` +如果要根据特殊字符来截取字符串,则需要使用```\\```来进行字符转义。 + + +在if判断中,**第二个括号**判断有两个字符的情况,并且如果这两个字符是以```\```开头的,并且不是字母或者数字的时候。如下列代码所示: +```Java +(regex.length() == 2 && regex.charAt(0) == '\\' && (((ch = regex.charAt(1))-'0')|('9'-ch)) < 0 && ((ch-'a')|('z'-ch)) < 0 && ((ch-'A')|('Z'-ch)) < 0) +``` +判断完之后,在进行**第三个括号**判断,判断是否是两字节的unicode字符。如下列代码所示: +```Java +(ch < Character.MIN_HIGH_SURROGATE || + ch > Character.MAX_LOW_SURROGATE) +``` + +对于下面这段复杂的代码,我们结合示例一句一句来分析。 +```Java + int off = 0; + int next = 0; + boolean limited = limit > 0; + ArrayList list = new ArrayList<>(); + while ((next = indexOf(ch, off)) != -1) { + if (!limited || list.size() < limit - 1) { + list.add(substring(off, next)); + off = next + 1; + } else { // last one + //assert (list.size() == limit - 1); + list.add(substring(off, value.length)); + off = value.length; + break; + } + } + // 如果没有匹配到的内容则直接返回 + if (off == 0) + return new String[]{this}; + + // 添加剩余的segment到list集合中 + if (!limited || list.size() < limit) + list.add(substring(off, value.length)); + + // Construct result + int resultSize = list.size(); + if (limit == 0) { + while (resultSize > 0 && list.get(resultSize - 1).length() == 0) { + resultSize--; + } + } + String[] result = new String[resultSize]; + return list.subList(0, resultSize).toArray(result); +``` + +#### 8.2 源码分析2 + +示例代码1: +```Java + String splitStr1 = "what,is,,,,split"; + String[] strs1 = splitStr1.split(","); + for (String s : strs1) { + System.out.println(s); + } + System.out.println(strs1.length); +``` +运行结果: +``` +what +is + +split +6 +``` + +示例代码2: +```Java + String splitStr1 = "what,is,,,,"; + String[] strs1 = splitStr1.split(","); + for (String s : strs1) { + System.out.println(s); + } + System.out.println(strs1.length); +``` +运行结果: +``` +what +is +2 +``` + + +示例代码3: +```Java + String splitStr1 = "what,is,,,,"; + String[] strs1 = splitStr1.split(",", -1); + for (String s : strs1) { + System.out.println(s); + } + System.out.println(strs1.length); +``` +运行结果: +``` +what +is + + +6 +``` + +对比了一下示例代码和结果之后,小伙伴们是不是很困惑呢?困惑就对了,下面就开始分析代码吧。在split(String regex, int limit)方法的if判断内部,定义了off和next变量,作为拆分整个字符串的两个指针,然后limit作为拆分整个string字符串的一个阈值。在split()方法内部的复杂逻辑判断中,都围绕着这三个变量来进行。 + +下面将示例代码1的字符串拆分成字符数组,如下(n代表next指针,o代表off指针): +``` + w h a t , i s , , , , s p l i t + 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 + n + o +``` +由于regex为',',所以满足if括号里的判断。一开始next和off指针都在0位置,limit为0,在while里的判断逻辑指的是获取','索引位置,由上图拆分的字符数组可知,next会分别为4,7,8,9,10。由于limited = limit > 0,得知limited为false,则逻辑会走到 +```Java + if (!limited || list.size() < limit - 1) { + list.add(substring(off, next)); + off = next + 1; + } +``` +进入第一次while循环体,此时的字符数组以及索引关系如下: +``` + w h a t , i s , , , , s p l i t + 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 + n + o +``` +所以list集合里就会添加进字符串what。 + +第二次进入while循环时,此时的字符数组以及索引关系如下: +``` + w h a t , i s , , , , s p l i t + 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 + n + o +``` +list集合里就会添加进字符串is + +第三次进入while循环时,此时的字符数组以及索引关系如下: +``` + w h a t , i s , , , , s p l i t + 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 + n + o +``` +list集合里就会添加进空字符串"" + +第四次进入while循环时,此时的字符数组以及索引关系如下: +``` + w h a t , i s , , , , s p l i t + 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 + n + o +``` +list集合里就会添加进空字符串"" + +当o指针指向位置10时,while((next = indexOf(ch, off)) != -1)结果为false,因为此时已经获取不到','了。 + +注意,此时list中包含的元素有: +``` +[what,is, , , ,] +``` +当程序走到时, +```Java + if(!limited || list.size() < limit) { + list.add(substring(off, value.length); + } + + int resultSize = list.size(); + if (limit == 0) { + while (resultSize > 0 && list.get(resultSize - 1).length() == 0) { + resultSize--; + } + } +``` +会将字符数组off(此时off为10)位置到value.length位置的字符串存进list集合里,也就是split元素,由于list集合最后一个元素为split,其大小不为0,所以就不会进行resultSize--。所以最终list集合里的元素就有6个元素,值为 +``` +[what,is, , , ,split] +``` + +这里相信小伙伴们都知道示例1和示例2的区别在那里了,是因为示例2最后索引位置的list为空字符串,所以list.get(resultSize-1).length()为0,则会调用下面的代码逻辑: +```Java + while (resultSize > 0 && list.get(resultSize - 1).length() == 0) { + resultSize--; + } +``` +最终会将list中的空字符串给减少。所以示例2的最终结果为 +``` +[what,is] +``` + +对于入参limit,可以总结一下为: +1. limit > 0,split()方法最多把字符串拆分成limit个部分。 +2. limit = 0,split()方法会拆分匹配到的最后一位regex。 +3. limit < 0,split()方法会根据regex匹配到的最后一位,如果最后一位为regex,则多添加一位空字符串;如果不是则添加regex到字符串末尾的子字符串。 + +就以示例代码一为例,对于字符串"what,is,,,,"。 + +**对于limit > 0**,由于代码: +```Java +boolean limited = limit > 0; // limited为true +.. +.. +// !limited为false;list.size()一开始为0,则如果limit > 2,则list.size() < limit - 1为true,则进行字符串截取subString方法调用; +// 所以如果limit = 1,则直接返回原字符串;如果limit > 1则将字符串拆分为limit个部分; +if(!limited || list.size() < limit - 1) { + // 截取off到next字符串存入list中 + list.add(substring(off, next)); + off = next + 1; +} else { + list.add(substring(off, value.length)); + off = value.length; + break; +} +``` +所以返回的原字符串: +``` +what,is,,,, +1 +``` + + +**对于limit = 0**,由于代码: +```Java + if (limit == 0) { + while (resultSize > 0 && list.get(resultSize - 1).length() == 0) { + resultSize--; + } + } +``` +会将空字符串从list集合中移除掉,所以返回的是: +``` +what +is +2 +``` + +**对于limit < 0**,由于代码: +```Java +if (!limited || list.size() < limit) + list.add(substring(off, value.length)); +``` +会在原来的集合内容上([what,is,'','',''])再加一个空字符串,也就是[what,is,'','','','']。 + + +### 总结 + +String作为Java中使用频率最多的类,它在日程开发中起到了至关重要的作用。由于String方法还有很多,这里就不一一总结了。 diff --git "a/note/JDK/\346\267\261\345\205\245\345\255\246\344\271\240Thread\345\272\225\345\261\202\346\272\220\347\240\201.md" "b/note/JDK/\346\267\261\345\205\245\345\255\246\344\271\240Thread\345\272\225\345\261\202\346\272\220\347\240\201.md" new file mode 100644 index 0000000..fa16533 --- /dev/null +++ "b/note/JDK/\346\267\261\345\205\245\345\255\246\344\271\240Thread\345\272\225\345\261\202\346\272\220\347\240\201.md" @@ -0,0 +1,285 @@ +## 前言 + +在Java中,线程是一个非常重要的知识,大多数开发者对于线程API,属于不用就忘,到用时需要百度的情况,又或者是对线程底层 +没有一个深入理解。 + +本文通过对Thread源码底层学习来加深对Thread线程的印象。 + +## 正文 + +### 1. 类注释 + +从Thread类的注释中,可以解读为如下几点: + +1. 每个线程都有优先级,高优先级的线程可能会优先执行; + +2. 父线程创建子线程后,优先级、是否是守护线程等属性父子线程是一致的; + +3. JVM 启动时,通常都启动 MAIN 非守护线程,以下任意一个情况发生时,线程就会停止: + 退出方法被调用,并且安全机制允许这么做(比如调用 Thread.interrupt 方法); + 所有非守护线程都消亡,或者从运行的方法正常返回,或者运行的方法抛出了异常; + +4. 每个线程都有名字,多个线程可能具有相同的名字,Thread 有的构造器如果没有指定名字,会自动生成一个名字。 + +### 2. 线程的生命周期 + +一般而言,Thread的生命周期包含: + +- 新建状态(New) +- 就绪状态(Runnable) +- 运行状态(Running) +- 阻塞状态(Blocked) +- 死亡状态(Dead) + +![线程生命周期](https://github.com/coderbruis/JavaSourceCodeLearning/blob/master/note/images/JDK/threadcycle.jpg) + +如果从源码角度来解析线程的状态,可以列举出六中状态: + +![线程生命周期](https://github.com/coderbruis/JavaSourceCodeLearning/blob/master/note/images/JDK/thread01.jpeg) + +1. NEW 表示线程创建成功,但没有运行,在 new Thread 之后,没有 start 之前,线程的状态都是 NEW; +2. 当我们运行 strat 方法,子线程被创建成功之后,子线程的状态变成 RUNNABLE,RUNNABLE 表示线程正在运行中; +3. 子线程运行完成、被打断、被中止,状态都会从 RUNNABLE 变成 TERMINATED,TERMINATED 表示线程已经运行结束了; +4. 如果线程正好在等待获得 monitor lock 锁,比如在等待进入 synchronized 修饰的代码块或方法时,会从 RUNNABLE 变成 BLOCKED,BLOCKED 表示阻塞的意思; +5. WAITING 和 TIMED_WAITING 类似,都表示在遇到 Object#wait、Thread#join、LockSupport#park 这些方法时,线程就会等待另一个线程执行完特定的动作之后,才能结束等待,只不过 TIMED_WAITING 是带有等待时间的(可以看下面的 join 方法的 demo)。 + +上图中的六中状态都只是根据源码中列出的6中状态,但是Java线程的处理方法都是围绕这6中状态的。 + +### 3. 线程的基本概念 + +#### 3.1 线程优先级 + +优先级代表线程执行的机会的大小,优先级高的可能先执行,低的可能后执行,在 Java 源码中,优先级从低到高分别是 1 到 10,线程默认 new 出来的优先级都是 5,源码如下: +```Java +// 最低优先级 +public final static int MIN_PRIORITY = 1; + +// 普通优先级,也是默认的 +public final static int NORM_PRIORITY = 5; + +// 最大优先级 +public final static int MAX_PRIORITY = 10; +``` + +#### 3.2 守护线程 + +我们默认创建的线程都是非守护线程。创建守护线程时,需要将 Thread 的 daemon 属性设置成 true,守护线程的优先级很低,当 JVM 退出时,是不关心有无守护线程的,即使还有很多守护线程,JVM 仍然会退出,我们在工作中,可能会写一些工具做一些监控的工作,这时我们都是用守护子线程去做,这样即使监控抛出异常,但因为是子线程,所以也不会影响到业务主线程,因为是守护线程,所以 JVM 也无需关注监控是否正在运行,该退出就退出,所以对业务不会产生任何影响。 + +#### 3.3 ThreadLocal + +线程中的ThreadLocal前面已经讲深入分析过了,这里就不在赘述,连接如下: + +[一篇文章快速深入学习ThreadLocal](https://github.com/coderbruis/JavaSourceCodeLearning/blob/master/note/JDK/%E4%B8%80%E7%AF%87%E6%96%87%E7%AB%A0%E5%BF%AB%E9%80%9F%E6%B7%B1%E5%85%A5%E5%AD%A6%E4%B9%A0ThreadLocal.md) + +### 4. Thread的源码 + +#### 4.1 start 启动线程 + +```Java +// 该方法可以创建一个新的线程出来 +public synchronized void start() { + // 如果没有初始化,抛异常 + if (threadStatus != 0) + throw new IllegalThreadStateException(); + group.add(this); + // started 是个标识符,我们在做一些事情的时候,经常这么写 + // 动作发生之前标识符是 false,发生完成之后变成 true + boolean started = false; + try { + // 这里会创建一个新的线程,执行完成之后,新的线程已经在运行了,既 target 的内容已经在运行了 + start0(); + // 这里执行的还是主线程 + started = true; + } finally { + try { + // 如果失败,把线程从线程组中删除 + if (!started) { + group.threadStartFailed(this); + } + // Throwable 可以捕捉一些 Exception 捕捉不到的异常,比如说子线程抛出的异常 + } catch (Throwable ignore) { + /* do nothing. If start0 threw a Throwable then + it will be passed up the call stack */ + } + } +} +// 开启新线程使用的是 native 方法 +private native void start0(); +``` + +#### 4.2 init 初始化 + +下面只贴出部分关键源码: + +```Java +// 无参构造器,线程名字自动生成 +public Thread() { + init(null, null, "Thread-" + nextThreadNum(), 0); +} +// g 代表线程组,线程组可以对组内的线程进行批量的操作,比如批量的打断 interrupt +// target 是我们要运行的对象 +// name 我们可以自己传,如果不传默认是 "Thread-" + nextThreadNum(),nextThreadNum 方法返回的是自增的数字 +// stackSize 可以设置堆栈的大小 +private void init(ThreadGroup g, Runnable target, String name, + long stackSize, AccessControlContext acc) { + if (name == null) { + throw new NullPointerException("name cannot be null"); + } + + this.name = name.toCharArray(); + // 当前线程作为父线程 + Thread parent = currentThread(); + this.group = g; + // 子线程会继承父线程的守护属性 + this.daemon = parent.isDaemon(); + // 子线程继承父线程的优先级属性 + this.priority = parent.getPriority(); + // classLoader + if (security == null || isCCLOverridden(parent.getClass())) + this.contextClassLoader = parent.getContextClassLoader(); + else + this.contextClassLoader = parent.contextClassLoader; + this.inheritedAccessControlContext = + acc != null ? acc : AccessController.getContext(); + this.target = target; + setPriority(priority); + // 当父线程的 inheritableThreadLocals 的属性值不为空时 + // 会把 inheritableThreadLocals 里面的值全部传递给子线程 + if (parent.inheritableThreadLocals != null) + this.inheritableThreadLocals = + ThreadLocal.createInheritedMap(parent.inheritableThreadLocals); + this.stackSize = stackSize; + /* Set thread ID */ + // 线程 id 自增 + tid = nextThreadID(); +} +``` + +从初始化源码中可以看到,很多属性,子线程都是直接继承父线程的,包括优先性、守护线程、inheritableThreadLocals 里面的值等等。 + +#### 4.3 join + +当我们调用某个线程的这个方法时,这个方法会挂起调用线程,直到被调用线程结束执行,调用线程才会继续执行。 + +```Java + public final void join() throws InterruptedException { + join(0); + } +``` + +```Java + public final synchronized void join(long millis) + throws InterruptedException { + long base = System.currentTimeMillis(); + long now = 0; + + if (millis < 0) { + throw new IllegalArgumentException("timeout value is negative"); + } + + if (millis == 0) { + // 判断线程仍然存活 + while (isAlive()) { + // 调用底层native订单wait方法阻塞线程 + wait(0); + } + } else { + while (isAlive()) { + // 计算延迟时间 + long delay = millis - now; + if (delay <= 0) { + break; + } + // 调用底层native订单wait方法阻塞线程 + wait(delay); + now = System.currentTimeMillis() - base; + } + } + } +``` + +需要注意的一点是,调用wait方法进行线程阻塞,是需要获取锁的,所以join方法是由synchronize方法修饰的,因而只有获取锁的线程才能调用该join方法。所以join方法是被synchronized修饰的,synchronized修饰在方法层面相当于synchronized(this),this就是Thread本身的实例。 + +既然有阻塞,那么就会有唤醒操作。而线程的唤醒操作是由notify或者是notifyall来唤醒的。 + +#### 4.4 yield + +yield 是个 native 方法,底层代码如下: + +```Java +public static native void yield(); +``` + +意思是当前线程做出让步,放弃当前 cpu,让 cpu 重新选择线程,避免线程过度使用 cpu,我们在写 while 死循环的时候,预计短时间内 while 死循环可以结束的话,可以在循环里面使用 yield 方法,防止 cpu 一直被 while 死循环霸占。 + +有点需要说明的是,让步不是绝不执行,重新竞争时,cpu 也有可能重新选中自己。 + +#### 4.5 sleep + +sleep 也是 native 方法,可以接受毫秒的一个入参,也可以接受毫秒和纳秒的两个入参,意思是当前线程会沉睡多久,沉睡时不会释放锁资源,所以沉睡时,其它线程是无法得到锁的。 + +接受毫秒和纳秒两个入参时,如果给定纳秒大于等于 0.5 毫秒,算一个毫秒,否则不算。 + +```Java + public static void sleep(long millis, int nanos) + throws InterruptedException { + if (millis < 0) { + throw new IllegalArgumentException("timeout value is negative"); + } + + if (nanos < 0 || nanos > 999999) { + throw new IllegalArgumentException( + "nanosecond timeout value out of range"); + } + + if (nanos >= 500000 || (nanos != 0 && millis == 0)) { + millis++; + } + + sleep(millis); + } +``` +```Java +public static native void sleep(long millis) throws InterruptedException; +``` + +#### 4.6 interrupt + +interrupt 中文是打断的意思,意思是可以打断中止正在运行的线程,比如: + +1. Object#wait ()、Thread#join ()、Thread#sleep (long) 这些方法运行后,线程的状态是 WAITING 或 TIMED_WAITING,这时候打断这些线程,就会抛出 InterruptedException 异常,使线程的状态直接到 TERMINATED; +2. 如果 I/O 操作被阻塞了,我们主动打断当前线程,连接会被关闭,并抛出 ClosedByInterruptException 异常; + +我们举一个例子来说明如何打断 WAITING 的线程,代码如下: + +```Java +@Test +public void testInterrupt() throws InterruptedException { + Thread thread = new Thread(new Runnable() { + @Override + public void run() { + log.info("{} begin run",Thread.currentThread().getName()); + try { + log.info("子线程开始沉睡 30 s"); + Thread.sleep(30000L); + } catch (InterruptedException e) { + log.info("子线程被打断"); + e.printStackTrace(); + } + log.info("{} end run",Thread.currentThread().getName()); + } + }); + // 开一个子线程去执行 + thread.start(); + Thread.sleep(1000L); + log.info("主线程等待 1s 后,发现子线程还没有运行成功,打断子线程"); + thread.interrupt(); +} +``` + +例子主要说的是,主线程会等待子线程执行 1s,如果 1s 内子线程还没有执行完,就会打断子线程,子线程被打断后,会抛出 InterruptedException 异常,执行结束,运行的结果如下图: + +![线程生命周期](https://github.com/coderbruis/JavaSourceCodeLearning/blob/master/note/images/JDK/thread02.jpeg) + + +可以发现,Thread源码中很多都调用了native的方法,感兴趣的读者可以去翻阅OpenJDK底层native源码,进一步去探索。 \ No newline at end of file diff --git "a/note/JDK/\346\267\261\345\205\245\350\247\243\350\257\273CompletableFuture\346\272\220\347\240\201\344\270\216\345\216\237\347\220\206.md" "b/note/JDK/\346\267\261\345\205\245\350\247\243\350\257\273CompletableFuture\346\272\220\347\240\201\344\270\216\345\216\237\347\220\206.md" new file mode 100644 index 0000000..d3271c8 --- /dev/null +++ "b/note/JDK/\346\267\261\345\205\245\350\247\243\350\257\273CompletableFuture\346\272\220\347\240\201\344\270\216\345\216\237\347\220\206.md" @@ -0,0 +1,572 @@ + +- [1 前言](#1-前言) +- [2 正文](#2-正文) + - [2.1 JDK官方文档解释](#21-jdk官方文档解释) + - [2.2 使用下CompletableFuture的API](#22-使用下completablefuture的api) + - [2.3 源码分析](#23-源码分析) + - [Completion](#completion) + - [CompletionStage](#completionstage) + - [总结](#总结) + + +## 1 前言 +最近在看公司源码,发现有些服务大量使用到了CompletableFuture,学了这么久Java,对这个类还是挺陌生的,实在惭愧。于是利用了业余时间认真学习并总结了下CompletableFuture的特性以及用法。 + +## 2 正文 + +CompletableFuture是JDK8中的新特性,主要用于对JDK5中加入的Future的补充。CompletableFuture实现了CompletionStage和Future接口。 + +### 2.1 JDK官方文档解释 +CompletableFuture类的官方API文档解释: + +CompletableFuture是JDK8中的新特性,主要用于对JDK5中加入的Future的补充。CompletableFuture实现了CompletionStage和Future接口。 + +CompletableFuture类的官方API文档解释: +1. CompletableFuture是一个在完成时可以触发相关方法和操作的Future,并且它可以视作为CompletableStage。 +2. 除了直接操作状态和结果的这些方法和相关方法外(CompletableFuture API提供的方法),CompletableFuture还实现了以下的CompletionStage的相关策略: +① 非异步方法的完成,可以由当前CompletableFuture的线程提供,也可以由其他调用完方法的线程提供。 +② 所有没有显示使用Executor的异步方法,会使用ForkJoinPool.commonPool()(那些并行度小于2的任务会创建一个新线程来运行)。为了简化监视、调试和跟踪异步方法,所有异步任务都被标记为CompletableFuture.AsynchronouseCompletionTask。 +③ 所有CompletionStage方法都是独立于其他公共方法实现的,因此一个方法的行为不受子类中其他方法的覆盖影响。 +3. CompletableFuture还实现了Future的以下策略 +① 不像FutureTask,因CompletableFuture无法直接控制计算任务的完成,所以CompletableFuture的取消会被视为异常完成。调用cancel()方法会和调用completeExceptionally()方法一样,具有同样的效果。isCompletedEceptionally()方法可以判断CompletableFuture是否是异常完成。 +② 在调用get()和get(long, TimeUnit)方法时以异常的形式完成,则会抛出ExecutionException,大多数情况下都会使用join()和getNow(T),它们会抛出CompletionException。 + +**小结:** +1. Concurrent包中的Future在获取结果时会发生阻塞,而CompletableFuture则不会,它可以通过触发异步方法来获取结果。 +2. 在CompletableFuture中,如果没有显示指定的Executor的参数,则会调用默认的ForkJoinPool.commonPool()。 +3. 调用CompletableFuture的cancel()方法和调用completeExceptionally()方法的效果一样。 + +在JDK5中,使用Future来获取结果时都非常的不方便,只能通过get()方法阻塞线程或者通过轮询isDone()的方式来获取任务结果,这种阻塞或轮询的方式会无畏的消耗CPU资源,而且还不能及时的获取任务结果,因此JDK8中提供了CompletableFuture来实现异步的获取任务结果。 + +## 2.2 使用下CompletableFuture的API +CompletableFuture类提供了非常多的方法供我们使用,包括了runAsync()、supplyAsync()、thenAccept()等方法。 + +**runAsync()**,异步运行, + +```Java +@Test + public void runAsyncExample() throws Exception { + ExecutorService executorService = Executors.newSingleThreadExecutor(); + CompletableFuture cf = CompletableFuture.runAsync(() -> { + try { + Thread.sleep(2000); + } catch (Exception e) { + e.printStackTrace(); + } + System.out.println(Thread.currentThread().getName()); + }, executorService); + System.out.println(Thread.currentThread().getName()); + while (true) { + if (cf.isDone()) { + System.out.println("CompletedFuture...isDown"); + break; + } + } + } +``` +运行结果: + +main +pool-1-thread-1 +CompletedFuture...isDown + + +这里调用的runAsync()方法没有使用ForkJoinPool的线程,而是使用了Executors.newSingleThreadExecutor()中的线程。runAsync()其实效果跟单开一个线程一样。 + + +**supplyAsync()** + +supply有供应的意思,supplyAsync就可以理解为异步供应,查看supplyAsync()方法入参可以知道,其有两个入参: +- Supplier\ supplier, +- Executor executor + +这里先简单介绍下Supplier接口,Supplier接口是JDK8引入的新特性,它也是用于创建对象的,只不过调用Supplier的get()方法时,才会去通过构造方法去创建对象,并且每次创建出的对象都不一样。Supplier常用语法为: +``` +Supplier sup= MySupplier::new; +``` +再展示代码例子之前,再讲一个thenAccept()方法,可以发现thenAccept()方法的入参如下: +- Comsumer\ + +Comsumer接口同样是java8新引入的特性,它有两个重要接口方法: +1. accept() +2. andThen() + +thenAccept()可以理解为接收CompletableFuture的结果然后再进行处理。 + +下面看下supplyAsync()和thenAccept()的例子: +```Java +public void thenApply() throws Exception { + ExecutorService executorService = Executors.newFixedThreadPool(2); + CompletableFuture cf = CompletableFuture.supplyAsync(() -> { //实现了Supplier的get()方法 + try { + Thread.sleep(2000); + } catch (Exception e) { + e.printStackTrace(); + } + System.out.println("supplyAsync " + Thread.currentThread().getName()); + return "hello "; + },executorService).thenAccept(s -> { //实现了Comsumper的accept()方法 + try { + thenApply_test(s + "world"); + } catch (Exception e) { + e.printStackTrace(); + } + }); + + System.out.println(Thread.currentThread().getName()); + while (true) { + if (cf.isDone()) { + System.out.println("CompletedFuture...isDown"); + break; + } + } + } +``` + +运行结果如下: + main + supplyAsync pool-1-thread-1 + thenApply_test hello world + thenApply_test pool-1-thread-1 + +从代码逻辑可以看出,thenApply_test等到了pool-1-thread-1线程完成任务后,才进行的调用,并且拿到了supplye()方法返回的结果,而main则异步执行了,这就避免了Future获取结果时需要阻塞或轮询的弊端。 + +**exceptionally** +当任务在执行过程中报错了咋办?exceptionally()方法很好的解决了这个问题,当报错时会去调用exceptionally()方法,它的入参为:Function\ fn,fn为执行任务报错时的回调方法,下面看看代码示例: +```Java +public void exceptionally() { + ExecutorService executorService = Executors.newSingleThreadExecutor(); + CompletableFuture cf = CompletableFuture.supplyAsync(() -> { + try { + Thread.sleep(3000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + if (1 == 1) { + throw new RuntimeException("测试exceptionally..."); + } + return "s1"; + }, executorService).exceptionally(e -> { + System.out.println(e.getMessage()); + return "helloworld " + e.getMessage(); + }); + cf.thenAcceptAsync(s -> { + System.out.println("thenAcceptAsync: " + s); + }); + System.out.println("main: " + Thread.currentThread().getName()); + while (true) {} + } +``` + +运行结果: +main: main +java.lang.RuntimeException: 测试exceptionally... +CompletableFuture is Down...helloworld java.lang.RuntimeException: 测试exceptionally... +thenAcceptAsync: helloworld java.lang.RuntimeException: 测试exceptionally... + +从代码以及运行结果来看,当任务执行过程中报错时会执行exceptionally()中的代码,thenAcceptAsync()会获取抛出的异常并输出到控制台,不管CompletableFuture()执行过程中报错、正常完成、还是取消,都会被标示为**已完成**,所以最后CompletableFuture.isDown()为true。 + +在Java8中,新增的ForkJoinPool.commonPool()方法,这个方法可以获得一个公共的ForkJoin线程池,这个公共线程池中的所有线程都是Daemon线程,意味着如果主线程退出,这些线程无论是否执行完毕,都会退出系统。 + +### 2.3 源码分析 + +CompletableFuture类实现了Future接口和CompletionStage接口,Future大家都经常遇到,但是这个CompletionStage接口就有点陌生了,这里的CompletionStage实际上是一个任务执行的一个“阶段”,CompletionStage详细的内容在下文有介绍。 + +```Java +public class CompletableFuture implements Future, CompletionStage { + volatile Object result; // CompletableFuture的结果值或者是一个异常的报装对象AltResult + volatile Completion stack; // 依赖操作栈的栈顶 + ... + // CompletableFuture的方法 + ... + // Unsafe mechanics + private static final sun.misc.Unsafe UNSAFE; + private static final long RESULT; + private static final long STACK; + private static final long NEXT; + static { + try { + final sun.misc.Unsafe u; + UNSAFE = u = sun.misc.Unsafe.getUnsafe(); + Class k = CompletableFuture.class; + RESULT = u.objectFieldOffset(k.getDeclaredField("result")); //计算result属性的位偏移量 + STACK = u.objectFieldOffset(k.getDeclaredField("stack")); //计算stack属性的位偏移量 + NEXT = u.objectFieldOffset + (Completion.class.getDeclaredField("next")); //计算next属性的位偏移量 + } catch (Exception x) { + throw new Error(x); + } + } +} +``` + +在CompletableFuture中有一个静态代码块,在CompletableFuture类初始化之前就进行调用,代码块里的内容就是通过Unsafe类去获取CompletableFuture的result、stack和next属性的“偏移量”,这个偏移量主要用于Unsafe的CAS操作时进行位移量的比较。具体的Unsafe的CAS操作,可以查看[Unsafe源码介绍](https://tech.meituan.com/2019/02/14/talk-about-java-magic-class-unsafe.html) + + +**runAsync(Runnable, Executor) & runAsync(Runnable)** +runAsync()做的事情就是异步的执行任务,返回的是CompletableFuture对象,不过CompletableFuture对象不包含结果。runAsync()方法有两个重载方法,这两个重载方法的区别是Executor可以指定为自己想要使用的线程池,而runAsync(Runnable)则使用的是ForkJoinPool.commonPool()。 + +下面先来看看runAsync(Runnable)的源码: +```Java + public static CompletableFuture runAsync(Runnable runnable) { + return asyncRunStage(asyncPool, runnable); + } +``` +这里的asyncPool是一个静态的成员变量: +```Java +private static final boolean useCommonPool = + (ForkJoinPool.getCommonPoolParallelism() > 1); // 并行级别 +private static final Executor asyncPool = useCommonPool ? + ForkJoinPool.commonPool() : new ThreadPerTaskExecutor(); +``` + +回到asyncRunStage()源码: +```Java + static CompletableFuture asyncRunStage(Executor e, Runnable f) { + if (f == null) throw new NullPointerException(); + CompletableFuture d = new CompletableFuture(); + e.execute(new AsyncRun(d, f)); + return d; + } +``` +看到asyncRunStage()源码,可以知道任务是由Executor来执行的,那么可想而知Async类一定是实现了Callable接口或者继承了Runnable类,查看Async类: +```Java +static final class AsyncRun extends ForkJoinTask + implements Runnable, AsynchronousCompletionTask { + CompletableFuture dep; Runnable fn; + AsyncRun(CompletableFuture dep, Runnable fn) { + this.dep = dep; this.fn = fn; + } + + public final Void getRawResult() { return null; } + public final void setRawResult(Void v) {} + public final boolean exec() { run(); return true; } + + public void run() { + CompletableFuture d; Runnable f; + if ((d = dep) != null && (f = fn) != null) { + dep = null; fn = null;//释放掉内存 + if (d.result == null) { + try { + f.run(); + d.completeNull(); + } catch (Throwable ex) { + d.completeThrowable(ex); + } + } + d.postComplete(); // 任务结束后,会执行所有依赖此任务的其他任务,这些任务以一个无锁并发栈的形式存在 + } + } + } +``` + +在AsyncRun类中,实现了Runnable接口的run()方法,在run()方法内部,会调用传进来的Runnable对象的run()方法,这里就需要用户自己去实现了,上文中的实例代码就是通过Lambda表达式来实现了Runnable接口。调用了f.run()之后,然后就是completeNull()方法了,改方法底层通过调用UNSAFE类的compareAndSwapObject()方法,来以CAS的方式将CompletableFuture的结果赋为null。postComplete()就是任务结束后,会执行所有依赖此任务的其他任务,这些任务以一个无锁并发栈的形式存在。 +postComplete()的源码还是有点复杂的,先不急着分析。**先看看Completion这个抽象类的数据结构组成**: + +#### Completion +下面先看看Completion的源码: +```Java +abstract static class Completion extends ForkJoinTask + implements Runnable, AsynchronousCompletionTask { + volatile Completion next; + abstract CompletableFuture tryFire(int mode); + abstract boolean isLive(); + + public final void run() { tryFire(ASYNC); } + public final boolean exec() { tryFire(ASYNC); return true; } + public final Void getRawResult() { return null; } + public final void setRawResult(Void v) {} + } +``` +Completion是一个抽象类,分别实现了Runnable、AsynchronousCompletionTask接口,继承了ForkJoinPoolTask类,而ForJoinPoolTask抽象类又实现了Future接口,因此Completion实际上就是一个Future。可以看到Completion的抽象方法和成员方法的实现逻辑都短短一行或者没有,可以猜到这些方法的实现都是在其子类中。其实现类包括了UniCompletion、BiCompletion、UniAccept、BiAccept等,如下图: +![在这里插入图片描述](https://img-blog.csdnimg.cn/20191122180922576.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0NvZGVyQnJ1aXM=,size_16,color_FFFFFF,t_70) +而Completion类中还有一个非常重要的成员属性 +``` +volatile Completion next; +``` + +有印象的读者应该能记得,CompletableFuture中有一个属性——stack,就是Completion类的。 +``` +volatile Completion stack; +``` +由这个属性可以看出,CompletableFuture其实就是一个链表的一个数据结构。 + +```Java +abstract static class UniCompletion extends Completion { + Executor executor; // executor to use (null if none) + CompletableFuture dep; // 代表的依赖的CompletableFuture + CompletableFuture src; // 代表的是源CompletableFuture + + UniCompletion(Executor executor, CompletableFuture dep, + CompletableFuture src) { + this.executor = executor; this.dep = dep; this.src = src; + } + + /** + * 确保当前Completion可以被调用;并且使用ForkJoinPool标记为来确保只有一个线程可以调用, + * 如果是异步的,则在任务启动之后通过tryFire来进行调用。tryFire方法时在UniAccept类中。 + */ + final boolean claim() { + Executor e = executor; + if (compareAndSetForkJoinTaskTag((short)0, (short)1)) { + if (e == null) + return true; + executor = null; // disable + e.execute(this); + } + return false; + } + + final boolean isLive() { return dep != null; } + } +``` +claim方法要在执行action前调用,若claim方法返回false,则不能调用action,原则上要保证action只执行一次。 + +```Java +static final class UniAccept extends UniCompletion { + Consumer fn; + UniAccept(Executor executor, CompletableFuture dep, + CompletableFuture src, Consumer fn) { + super(executor, dep, src); this.fn = fn; + } + /** + * 尝试去调用当前任务。uniAccept()方法为核心逻辑。 + */ + final CompletableFuture tryFire(int mode) { + CompletableFuture d; CompletableFuture a; + if ((d = dep) == null || + !d.uniAccept(a = src, fn, mode > 0 ? null : this)) + return null; + dep = null; src = null; fn = null; + return d.postFire(a, mode); + } + } +``` +```Java +final boolean uniAccept(CompletableFuture a, + Consumer f, UniAccept c) { + Object r; Throwable x; + if (a == null || (r = a.result) == null || f == null) //判断源任务是否已经完成了,a表示的就是源任务,a.result就代表的是原任务的结果。 + return false; + tryComplete: if (result == null) { + if (r instanceof AltResult) { + if ((x = ((AltResult)r).ex) != null) { + completeThrowable(x, r); + break tryComplete; + } + r = null; + } + try { + if (c != null && !c.claim()) + return false; + @SuppressWarnings("unchecked") S s = (S) r; + f.accept(s); //去调用Comsumer + completeNull(); + } catch (Throwable ex) { + completeThrowable(ex); + } + } + return true; + } +``` +对于Completion的执行,还有几个关键的属性: +```Java +static final int SYNC = 0;//同步 +static final int ASYNC = 1;//异步 +static final int NESTED = -1;//嵌套 +``` + + +Completion在CompletableFuture中是如何工作的呢?现在先不着急了解其原理,下面再去看下一个重要的接口——CompletionStage。 + +#### CompletionStage + +下面介绍下CompletionStage接口。看字面意思可以理解为“完成动作的一个阶段”,查看官方注释文档:CompletionStage是一个可能执行异步计算的“阶段”,这个阶段会在另一个CompletionStage完成时调用去执行动作或者计算,一个CompletionStage会以正常完成或者中断的形式“完成”,并且它的“完成”会触发其他依赖的CompletionStage。CompletionStage 接口的方法一般都返回新的CompletionStage,因此构成了链式的调用。 +【下文中Stage代表CompletionStage】 + +**那么在Java中什么是CompletionStage呢?** +官方定义中,一个Function,Comsumer或者Runnable都会被描述为一个CompletionStage,相关方法比如有apply,accept,run等,这些方法的区别在于它们有些是需要传入参,有些则会产生“结果”。 +- Funtion方法会产生结果 +- Comsumer会消耗结果 +- Runable既不产生结果也不消耗结果 + +下面看看一个Stage的调用例子: +```Java +stage.thenApply(x -> square(x)).thenAccept(x -> System.out.println(x)).thenRun(() -> System.out.println()) +``` +这里x -> square(x)就是一个Function类型的Stage,它返回了x。x -> System.out.println(x)就是一个Comsumer类型的Stage,用于接收上一个Stage的结果x。() ->System.out.println()就是一个Runnable类型的Stage,既不消耗结果也不产生结果。 + +一个、两个或者任意一个CompletionStage的完成都会触发依赖的CompletionStage的执行,CompletionStage的依赖动作可以由带有then的前缀方法来实现。如果一个Stage被两个Stage的完成给触发,则这个Stage可以通过相应的Combine方法来结合它们的结果,相应的Combine方法包括:thenCombine、thenCombineAsync。但如果一个Stage是被两个Stage中的其中一个触发,则无法去combine它们的结果,因为这个Stage无法确保这个结果是那个与之依赖的Stage返回的结果。 + +```Java + @Test + public void testCombine() throws Exception { + String result = CompletableFuture.supplyAsync(() -> { + return "hello"; + }).thenCombine(CompletableFuture.supplyAsync(() -> { + return " world"; + }), (s1, s2) -> s1 + " " + s2).join(); + + System.out.println(result); + } +``` + + +虽然Stage之间的依赖关系可以控制触发计算,但是并不能保证任何的顺序。 + +另外,可以用一下三种的任何一种方式来安排一个新Stage的计算:default execution、default asynchronous execution(方法后缀都带有async)或者custom(自定义一个executor)。默认和异步模式的执行属性由CompletionStage实现而不是此接口指定。 + +**小结:CompletionStage确保了CompletableFuture能够进行链式调用。** + + +下面开始介绍CompletableFuture的几个核心方法: + +**postComplete** +```Java +final void postComplete() { + CompletableFuture f = this; Completion h; //this表示当前的CompletableFuture + while ((h = f.stack) != null || //判断stack栈是否为空 + (f != this && (h = (f = this).stack) != null)) { + CompletableFuture d; Completion t; + if (f.casStack(h, t = h.next)) { //通过CAS出栈, + if (t != null) { + if (f != this) { + pushStack(h); //如果f不是this,将刚出栈的h入this的栈顶 + continue; + } + h.next = null; // detach 帮助GC + } + f = (d = h.tryFire(NESTED)) == null ? this : d; //调用tryFire + } + } + } +``` +postComplete()方法可以理解为当任务完成之后,调用的一个“后完成”方法,主要用于触发其他依赖任务。 + +**uniAccept** +```Java +final boolean uniAccept(CompletableFuture a, + Consumer f, UniAccept c) { + Object r; Throwable x; + if (a == null || (r = a.result) == null || f == null) //判断当前CompletableFuture是否已完成,如果没完成则返回false;如果完成了则执行下面的逻辑。 + return false; + tryComplete: if (result == null) { + if (r instanceof AltResult) { //判断任务结果是否是AltResult类型 + if ((x = ((AltResult)r).ex) != null) { + completeThrowable(x, r); + break tryComplete; + } + r = null; + } + try { + if (c != null && !c.claim()) //判断当前任务是否可以执行 + return false; + @SuppressWarnings("unchecked") S s = (S) r; //获取任务结果 + f.accept(s); //执行Comsumer + completeNull(); + } catch (Throwable ex) { + completeThrowable(ex); + } + } + return true; + } +``` + +这里有一个很巧妙的地方,就是uniAccept的入参中,CompletableFuture a表示的是源任务,UniAccept c中报装有依赖的任务,这点需要清除。 + +**pushStack** + +```Java + final void pushStack(Completion c) { + do {} while (!tryPushStack(c)); //使用CAS自旋方式压入栈,避免了加锁竞争 + } + + final boolean tryPushStack(Completion c) { + Completion h = stack; + lazySetNext(c, h); //将当前stack设置为c的next + return UNSAFE.compareAndSwapObject(this, STACK, h, c); //尝试把当前栈(h)更新为新值(c) + } + + static void lazySetNext(Completion c, Completion next) { + UNSAFE.putOrderedObject(c, NEXT, next); + } +``` + +光分析源码也没法深入理解其代码原理,下面结合一段示例代码来对代码原理进行分析。 +```Java + @Test + public void thenApply() throws Exception { + ExecutorService executorService = Executors.newFixedThreadPool(2); + + CompletableFuture cf = CompletableFuture.supplyAsync(() -> { + try { + //休眠200秒 + Thread.sleep(200000); + } catch (Exception e) { + e.printStackTrace(); + } + System.out.println("supplyAsync " + Thread.currentThread().getName()); + return "hello "; + },executorService).thenAccept(s -> { + try { + thenApply_test(s + "world"); + } catch (Exception e) { + e.printStackTrace(); + } + }); + + System.out.println(Thread.currentThread().getName()); + while (true) { + if (cf.isDone()) { + System.out.println("CompletedFuture...isDown"); + break; + } + } + } +``` + + /** 运行结果: + main + supplyAsync pool-1-thread-1 + thenApply_test hello world + thenApply_test pool-1-thread-1 + CompletedFuture...isDown + */ + +这段示例代码所做的事情就是supplyAsync(Supplier\ supplier)休眠200秒之后,返回一个字符串,thenAccept(Consumer\ action)等到任务完成之后接收这个字符串,并且调用thenApply_test()方法,随后输出 hello world。 +代码中让线程休眠200秒是为了方便观察CompletableFuture的传递过程。 + +下面就描述下程序的整个运作流程。 +**①** 主线程调用CompletableFuture的supplyAsync()方法,传入Supplier和Executor。在supplyAsync()中又继续调用CompletableFuture的asyncSupplyStage(Executor, Supplier)方法。 +![在这里插入图片描述](https://img-blog.csdnimg.cn/20191128102326944.png) +来到asyncSupplyStage()方法中,调用指定的线程池,并执行execute(new AsyncSupply\(d,f)),这里d就是我们的“源任务”,接下来thenApply()要依赖着这个源任务进行后续逻辑操作,f就是Supplier的函数式编程。 +![在这里插入图片描述](https://img-blog.csdnimg.cn/20191128102631844.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0NvZGVyQnJ1aXM=,size_16,color_FFFFFF,t_70) +AsyncSupply实现了Runnable的run()方法,核心逻辑就在run()方法里。在run()方法里,先判断d.result == null,判断该任务是否已经完成,防止并发情况下其他线程完成此任务了。f.get()就是调用的Supplier的函数式编程,这里会休眠200秒,所以executor线程池开启的线程会在这里阻塞200秒。 + +**②** 虽然executor线程池线程阻塞了,但是main线程任然会继续执行接下来的代码。 +![在这里插入图片描述](https://img-blog.csdnimg.cn/20191128103312134.png) +main线程会在asyncSupplyStage()方法中返回d,就是我们的“依赖任务”,而这个任务此时还处在阻塞中。接下来main线程会继续执行CompletableFuture的thenAccept(Comsumer\ action)方法,然后调用CompletableFuture的uniAcceptStage()方法。 +![在这里插入图片描述](https://img-blog.csdnimg.cn/2019112810354686.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0NvZGVyQnJ1aXM=,size_16,color_FFFFFF,t_70) +在uniAcceptStage()方法中,会将“依赖任务”、“源任务”、线程池以及Comsumer报装程一个UniAccept对象,然后调用push()压入stack的栈顶中。随后调用UniAccept的tryFire()方法。 +![在这里插入图片描述](https://img-blog.csdnimg.cn/20191128103848372.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0NvZGVyQnJ1aXM=,size_16,color_FFFFFF,t_70) +其中的CompletableFuture的uniAccept()方法会判断任务是否完成,判断依据是a.result 是否为空,这里的a就是之前传入的“源任务”,等到“源任务”阻塞200秒过后,就会完成任务,并将字符串存入到 result中。 +![在这里插入图片描述](https://img-blog.csdnimg.cn/20191128104106221.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0NvZGVyQnJ1aXM=,size_16,color_FFFFFF,t_70) +判断到“源任务”完成之后,就会调用接下来的逻辑。s拿到的值就是“源”任务返回的字符串,并且传入到了Comsumer.accept()方法中。然而“源任务”还在阻塞中,main线程会跳出uniAccept(),继续执行接下来的逻辑。接下来就是输出当前线程的名字,然后调用while(true),结束条件为CompletableFuture.isDone(),当任务完成时则结束while(true)循环。 + +**③** 回到“源任务”,虽然main线程已经结束了整个生命周期,但是executor线程池的线程任然阻塞着的,休眠了200秒之后,继续执行任务。 +![在这里插入图片描述](https://img-blog.csdnimg.cn/20191128105600904.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0NvZGVyQnJ1aXM=,size_16,color_FFFFFF,t_70) +然后来到了postComplete()方法。这个方法在前面已经介绍到了,它是CompletableFuture的核心方法之一,做了许多事情。最重要的一件事情就是触发其他依赖任务,接下来调用的方法依次为:UniAccept.tryFire(mode) ——\> CompletableFuture.uniAccept(..) ——\> Comsumer.accept(s) ——\> 输出“hello world”,并输出当前调用线程的线程名。因这个调用链已经在②中介绍过了,所以就不再详细介绍其运作逻辑。 + +**小结:** 通过这个小示例,终于理解到了“源任务”和“依赖任务”之间的调用关系,以及CompletableFuture的基本运作原理。然而CompletableFuture还有其他的方法需要去深入分析,由于篇幅所限就不再赘述,感兴趣的读者可以以debug的模式去一点一点分析CompletableFuture其他方法的底层原理。这里不得不说Java并发包作者Doug Lea大神真的太厉害了,阅读他的源码之后,可以发现他写的代码不能以技术来形容,而应该使用“艺术”来形容。 + + +## 总结 +1. CompletableFuture底层由于借助了魔法类Unsafe的相关CAS方法,除了get或join结果之外,其他方法都实现了无锁操作。 +2. CompletableFuture实现了CompletionStage接口,因而具备了链式调用的能力,CompletionStage提供了either、apply、run以及then等相关方法,使得CompletableFuture可以使用各种应用场景。 +3. CompletableFuture中有“源任务”和“依赖任务”,“源任务”的完成能够触发“依赖任务”的执行,这里的完成可以是返回正常结果或者是异常。 +4. CompletableFuture默认使用ForkJoinPool,也可以使用指定线程池来执行任务。 + + +参考文档: +- 《JDK8官方文档》 +- [《Java魔法类:Unsafe应用解析》](https://tech.meituan.com/2019/02/14/talk-about-java-magic-class-unsafe.html) \ No newline at end of file diff --git "a/note/Netty/IO\345\272\225\345\261\202\345\216\237\347\220\206.md" "b/note/Netty/IO\345\272\225\345\261\202\345\216\237\347\220\206.md" new file mode 100644 index 0000000..b66a558 --- /dev/null +++ "b/note/Netty/IO\345\272\225\345\261\202\345\216\237\347\220\206.md" @@ -0,0 +1,66 @@ +## 从linux kernel内核出发,IO底层原理 + +### 1. BIO + +``` +import java.io.InputStream; +import java.net.ServerSocket; +import java.net.Socket; + +/** + * @author lhy + * + * 在windows服务器下,可以使用telnet来合serversocket建立连接 + */ +public class BIO { + public static void main(String[] args) throws Exception { + ServerSocket serverSocket = new ServerSocket(666); + System.out.println("Server started..."); + while (true) { + System.out.println("socket accepting..."); + Socket socket = serverSocket.accept(); + new Thread(new Runnable() { + @Override + public void run() { + try { + byte[] bytes = new byte[1024]; + InputStream inputStream = socket.getInputStream(); + while (true) { + System.out.println("reading..."); + int read = inputStream.read(bytes); + if (read != -1) { + System.out.println(new String(bytes, 0, read)); + } else { + break; + } + } + } catch (Exception e) { + e.printStackTrace(); + } finally { + try { + socket.close(); + } catch (Exception e) { + e.printStackTrace(); + } + } + } + }).start(); + } + } +} +``` + +#### 1.1 从kernel内核的角度来分析BIO的运行机制 + +### 2. IO多路复用 + +#### 2.1 select + +#### 2.2 poll + +#### 2.3 epoll + +### 3. 零拷贝 + + + diff --git "a/note/Netty/Netty\345\272\225\345\261\202\346\272\220\347\240\201\350\247\243\346\236\220-ChannelPipeline\345\210\206\346\236\220\357\274\210\344\270\212\357\274\211.md" "b/note/Netty/Netty\345\272\225\345\261\202\346\272\220\347\240\201\350\247\243\346\236\220-ChannelPipeline\345\210\206\346\236\220\357\274\210\344\270\212\357\274\211.md" new file mode 100644 index 0000000..706e68c --- /dev/null +++ "b/note/Netty/Netty\345\272\225\345\261\202\346\272\220\347\240\201\350\247\243\346\236\220-ChannelPipeline\345\210\206\346\236\220\357\274\210\344\270\212\357\274\211.md" @@ -0,0 +1,848 @@ +## 1. 概览 + +在Netty中,ChannelPipeline和ChannelHandler类似于Servlet和Filter过滤器,这类拦截器实际上是责任链模式的一种变形,这么设计是为了方便事件的拦截和用户业务逻辑的定制。 + +Servlet和Filter是JAVAEE中的基础组件,初学JAVAEE的小伙伴再熟悉不过了,它能够拦截到HTTP请求以及响应,并读出请求和响应的数据用作业务逻辑的处理,通过这种方式Servlet和Filter可以对Web应用程序进行预处理和后置处理。 + +Netty的Channel过滤器实现原理与ServletFilter机制一致,它将Channel的数据管道抽象为ChannelPipeline, 消息在ChannelPipeline中流动和传递。ChannelPipeline 持有I/O事件拦截器ChannelHandler的链表,由ChanneIHandler对I/O事件进行拦截和处理,可以方便地通过新增和删除ChannelHandler来实现不同的业务逻辑定制,不需要对已有的ChannelHandler进行修改,能够实现对修改封闭和对扩展的支持。总的而言,在Netty中,pipeline相当于Netty的大动脉,负责Netty的读写事件的传播。 + +## 2. ChannelPipeline原理分析 + +ChannelPipeline是ChannelHandler的容器,它负责ChannelHandler的管理和事件拦截。下面用一张图来展示在Netty中,一个“消息”被ChannelPipeline拦截,然后被ChannelHandler处理的过程,流程如下: + +![ChannelPipeline01](https://coderbruis.github.io/javaDocs/img/netty/source/ChannelPipeline01.png) + +1) 底层的SocketChannel read(方法读取ByteBuf, 触发ChannelRead 事件,由I/O线程NioEventLoop 调用ChannelPipeline 的fireChannelRead(Object msg)方法, 将消息 +(ByteBuf)传输到ChannelPipeline中 +2) 消息依次被HeadHandler、ChannelHandler1、 ChannelHander2.....TailHandler 拦截和处理,在这个过程中,任何ChannelHandler都可以中断当前的流程,结束消息的传递 +3) 调用ChannelHandlerContext的write方法发送消息,消息从TailHandler开始,途经ChannelHanderN.....ChannelHandlerl. HeadHandler, 最终被添加到消息发送缓冲区中等待刷新和发送,在此过程中也可以中断消息的传递,例如当编码失败时,就需要中断流程,构造异常的Future返回 + + +Netty中的事件分为inbound事件和outbound事件.inbound事件通常由1/O线程触发,例如TCP链路建立事件、链路关闭事件、读事件、异常通知事件等,它对应上图的左半部分。 + +触发inbound事件的方法如下: + +1) ChannelHandlerContext#fireChannelRegistered(): Channel 注册事件 +2) ChannelHandlerContext#fireChannelActive(): TCP链路建立成功, Channel激活事件 +3) ChannelHandlerContext#fireChannelRead(Object): 读事件 +4) ChannelHandlerContext#fireChannelReadComplete(): 读操作完成通知事件; +5) ChannelHandlerContext#fireExceptionCaught(Throwable): 异常通知事件; +6) ChannelHandlerContext#fireUserEventTriggered(Object): 用户自定义事件: +7) ChannelHandlerContext#fireChannelWritabilityChanged(): Channel 的可写状态变化通知事件; +8) ChannelHandlerContext#fireChannellnactive(): TCP连接关闭,链路不可用通知事件。 + +Outbound事件通常是由用户主动发起的网络I/O操作,例如用户发起的连接操作、绑定操作、消息发送等操作,它对应上图的右半部分。 + +触发outbound事件的方法如下: + +1) ChannelHandlerContext#bind( SocketAddress, ChannelPromise):绑定本地地址事件 +2) ChannelHandlerContext#connect(SocketAddress, SocketAddress, ChannelPromise):连接服务端事件 +3) ChannelHandlerContext#write(Object, ChannelPromise):发送事件 +4) ChannelHandlerContext#flushO): 刷新事件 +5) ChannelHandlerContext#read(): 读事件 +6) ChannelHandlerContext#disconnect(ChannelPromise): 断开连接事件 +7) ChannelHandlerContext#close(ChannelPromise): 关闭当前Channel事件 + + +### 2.1 ChannelPipeline接口 + +为了接下来能够方便的学习原理以及阅读源码,我们先看下ChannelPipeline的接口的继承关系图: + +!["ChannelPipeline02"](https://coderbruis.github.io/javaDocs/img/netty/source/ChannelPipeline02.png) + +可以发现,ChannelPipeline接口还继承了ChannelOutboundInvoker以及ChannelInboundInvoker,这两个invoker接口作为ChannelPipeline接口的增强接口。分别看下ChannelPipeline和ChannelOutboundInvoker、ChannelInboundInvoker这三个接口定义。 + +对于ChannelPipeline接口,方法分别可以分为以下几组类别方法: + +第一组是向ChannelPipeline中添加ChannelHandler,如下图所示: + +!["ChannelPipeline03_01"](https://coderbruis.github.io/javaDocs/img/netty/source/ChannelPipeline03_01.png) + +> 这里需要提前知道的是,ChannelPipeline维护这一组双向链表的数据结构。 + +addFirst是向ChannelPipeline双向链表头补添加节点,addLast是向ChannelPipeline双向链表尾部添加节点,addBefore是向ChannelPipeline双向链表中指定的ChannelHandler之前添加一个新的节点,addAfter是向ChannelPipeline双向链表中指定的ChannelHandler之后添加一个节点。 + + +第二组是向ChannelPipeline中移除ChannelHandler + +!["ChannelPipeline03_02"](https://coderbruis.github.io/javaDocs/img/netty/source/ChannelPipeline03_02.png) + + + +第三组是向获取ChannelHandlerContext对象 + +!["ChannelPipeline03_03"](https://coderbruis.github.io/javaDocs/img/netty/source/ChannelPipeline03_03.png) + + +第四组是ChannelInboundInvoker接口增强而来的方法 +!["ChannelPipeline03_04"](https://coderbruis.github.io/javaDocs/img/netty/source/ChannelPipeline03_04.png) + + + +第五组是ChannelOutboundInvoker接口增强而来的方法 +!["ChannelPipeline03_05"](https://coderbruis.github.io/javaDocs/img/netty/source/ChannelPipeline03_05.png) + + +在Netty中,ChannelPipeline是一个双向链表的数据结构,那么链表节点是什么呢?答案就是ChannelHandlerContext对象。 + +在Netty中,ChannelHandlerContext对象就是存在ChannelPipeline双向链表中的节点元素,在ChannelPipeline中,Netty会为其初始化Head头结点和Tail尾结点,在ChannelPipeline实现类:DefaultChannelPipeline中可以看到定义: + +```java + final AbstractChannelHandlerContext head; + final AbstractChannelHandlerContext tail; +``` + +DefaultChannelPipeline构造方法中,对head和tail进行了初始化 + +```java + protected DefaultChannelPipeline(Channel channel) { + // 给channel赋值channel对象 + this.channel = ObjectUtil.checkNotNull(channel, "channel"); + succeededFuture = new SucceededChannelFuture(channel, null); + voidPromise = new VoidChannelPromise(channel, true); + + // 节点对象是AbstractChannelHandlerContext对象,是用于进行业务处理 + // HeadContext和TailContext就是用户可以模仿实现的ChannelHandler实现类 + // channelPipeline双向连表的头节点 + tail = new TailContext(this); + // channelPipeline双向连表的尾结点 + head = new HeadContext(this); + + // channelPipeline: head -> tail + head.next = tail; + tail.prev = head; + } +``` + +当Netty初始化完DefaultChannelPipeline对象之后,ChannelPipeline中就已经存在了head和tail两个节点了,自然Netty会通过前面介绍的addXxx方法来添加,下面看下ChannelPipeline的addXxx方法源代码: + + +DefaultChannelPipeline.java +```java + @Override + public final ChannelPipeline addLast(String name, ChannelHandler handler) { + return addLast(null, name, handler); + } + + @Override + public final ChannelPipeline addLast(EventExecutorGroup group, String name, ChannelHandler handler) { + final AbstractChannelHandlerContext newCtx; + synchronized (this) { + checkMultiplicity(handler); + + /** + * 生成一个新的ChannelHandlerContext对象,这里返回的是DefaultChannelHandlerContext对象 + */ + newCtx = newContext(group, filterName(name, handler), handler); + + /** + * 向pipeline链表中添加一个新的节点 + */ + addLast0(newCtx); + + // If the registered is false it means that the channel was not registered on an eventLoop yet. + // In this case we add the context to the pipeline and add a task that will call + // ChannelHandler.handlerAdded(...) once the channel is registered. + if (!registered) { + newCtx.setAddPending(); + // 触发handlerAdded方法,并开始传播handlerAdded事件,此处最终会调用ChannelInitializer#handlerAdded方法,并最终调用到initChannel方法。 + callHandlerCallbackLater(newCtx, true); + return this; + } + + /** + * 从NioEventLoopGroup中获取到NioEventLoop对象 + */ + EventExecutor executor = newCtx.executor(); + if (!executor.inEventLoop()) { + callHandlerAddedInEventLoop(newCtx, executor); + return this; + } + } + // 调用HandlerAdded方法 + callHandlerAdded0(newCtx); + return this; + } + + // 向尾结点添加一个节点,并移动指针位置 + private void addLast0(AbstractChannelHandlerContext newCtx) { + AbstractChannelHandlerContext prev = tail.prev; + newCtx.prev = prev; + newCtx.next = tail; + prev.next = newCtx; + tail.prev = newCtx; + } + + private AbstractChannelHandlerContext newContext(EventExecutorGroup group, String name, ChannelHandler handler) { + return new DefaultChannelHandlerContext(this, childExecutor(group), name, handler); + } +``` + +那么,对于拥有双向链表结构的ChannelPipeline来说,是如何让事件在链表结构中进行转移执行的? + +就拿fireChannelRead方法来分析: + +这里需要提前知道的一点是,AbstractCHannelHandlerContext#fireChannelRead方法会被复写了channelRead方法的ChannelHandler调用。 + +AbstractCHannelHandlerContext.java +```java + @Override + public ChannelHandlerContext fireChannelRead(final Object msg) { + /** + * findContextInbound 返回的-> AbstractChannelHandlerContext对象 + */ + invokeChannelRead(findContextInbound(MASK_CHANNEL_READ), msg); + return this; + } + + /** + * 查找下一个Inbound节点 + * @param mask + * @return + */ + private AbstractChannelHandlerContext findContextInbound(int mask) { + AbstractChannelHandlerContext ctx = this; + EventExecutor currentExecutor = executor(); + do { + ctx = ctx.next; + } while (skipContext(ctx, currentExecutor, mask, MASK_ONLY_INBOUND)); + return ctx; + } + + private static boolean skipContext( + AbstractChannelHandlerContext ctx, EventExecutor currentExecutor, int mask, int onlyMask) { + // Ensure we correctly handle MASK_EXCEPTION_CAUGHT which is not included in the MASK_EXCEPTION_CAUGHT + return (ctx.executionMask & (onlyMask | mask)) == 0 || + // We can only skip if the EventExecutor is the same as otherwise we need to ensure we offload + // everything to preserve ordering. + // + // See https://github.com/netty/netty/issues/10067 + (ctx.executor() == currentExecutor && (ctx.executionMask & mask) == 0); + } +``` + +经过while循环遍历出下一个节点之后,变调用DefaultChannelPipeline#invokeChannelRead方法。 + +DefaultChannelPipeline +```java + static void invokeChannelRead(final AbstractChannelHandlerContext next, Object msg) { + final Object m = next.pipeline.touch(ObjectUtil.checkNotNull(msg, "msg"), next); + EventExecutor executor = next.executor(); + // 在Netty线程中,则直接调用 + if (executor.inEventLoop()) { + next.invokeChannelRead(m); + } else { + // 不在Netty线程中,则另开一个线程来调用ChanelRead方法 + executor.execute(new Runnable() { + @Override + public void run() { + next.invokeChannelRead(m); + } + }); + } + } + + private void invokeChannelRead(Object msg) { + // 过滤handler的状态 + if (invokeHandler()) { + try { + // 调用inboundHandler的channelRead方法 + ((ChannelInboundHandler) handler()).channelRead(this, msg); + } catch (Throwable t) { + invokeExceptionCaught(t); + } + } else { + fireChannelRead(msg); + } + } +``` + +就这样,前一个节点的ChannelRead方法执行完,就会遍历出下一个节点的ChannelRead并执行,以此达到了在双向链表中移动节点元素的效果。 + +### 2.2 ChannelPipeline对象是在哪里创建的 + +Netty中,ChannelPipeline对象是在Channel被创建的时候生成的,看源码。 + +AbstractChannel.java +```java + protected AbstractChannel(Channel parent) { + this.parent = parent; + // channel的标识 + id = newId(); + // channel的unsafe类 + // NioSocketChannel和NioServerSocketChannel的unsafe对象都一样 + unsafe = newUnsafe(); + // 新建pipeline + pipeline = newChannelPipeline(); + } + + protected DefaultChannelPipeline newChannelPipeline() { + // 随后调用DefaultChannelPipeline对象构造方法,在构造方法中生成TailContext和HeadContext,并维护好他们的链表关系 + return new DefaultChannelPipeline(this); + } +``` + +## 3. ChannelHandler原理分析 + +ChannelPipeline是通过ChannelHandler接口来实现事件的拦截和处理,一般ChannelHandler只需要继承ChannelHandlerAdapter,然后覆盖自己关心的方法即可。 + +对于ChannelHandler接口,先看下其接口实现图: + +!["netty_pipeline01"](https://coderbruis.github.io/javaDocs/img/netty/source/netty_pipeline01.png) + +可以看到ChannelHandler接口的子类实现图中,有两个重要的子接口:ChannelInboundHandler、ChannelOutboundHandlerAdapter,这两个子接口扩展了ChannelHandler的功能,分别对应着ChannelPipeline章节中介绍的inbound和outbound事件功能。先看看ChannelHandler接口定义了哪些方法。 + +### 3.1 ChannelHandler接口 + +```java +public interface ChannelHandler { + + /** + * 添加ChannelHandler的回调 + */ + void handlerAdded(ChannelHandlerContext ctx) throws Exception; + + /** + * 移除ChannelHandler的回调 + */ + void handlerRemoved(ChannelHandlerContext ctx) throws Exception; + + /** + * + */ + @Deprecated + void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception; + + @Inherited + @Documented + @Target(ElementType.TYPE) + @Retention(RetentionPolicy.RUNTIME) + @interface Sharable { + // no value + } +} + +``` + +ChannelHandler是作为业务处理器保存在ChannelPipeline中的,它的其他功能都是在子类实现或者是子接口继承的,下面看下: +ChannelHandlerAdapter + +```java +public abstract class ChannelHandlerAdapter implements ChannelHandler { + /** + * 判断当前这个ChannelHandler是否有@Shareble修饰,有的话该ChannelHandler就可以在不同的ChannelPipeline之间共享 + */ + public boolean isSharable() { + /** + * Cache the result of {@link Sharable} annotation detection to workaround a condition. We use a + * {@link ThreadLocal} and {@link WeakHashMap} to eliminate the volatile write/reads. Using different + * {@link WeakHashMap} instances per {@link Thread} is good enough for us and the number of + * {@link Thread}s are quite limited anyway. + * + * See #2289. + */ + Class clazz = getClass(); + Map, Boolean> cache = InternalThreadLocalMap.get().handlerSharableCache(); + Boolean sharable = cache.get(clazz); + if (sharable == null) { + sharable = clazz.isAnnotationPresent(Sharable.class); + cache.put(clazz, sharable); + } + return sharable; + } + + @Skip + @Override + @Deprecated + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { + ctx.fireExceptionCaught(cause); + } + +} +``` + +可以看到,ChannelHandlerAdapter作为抽象类只实现了顶级接口ChannelHandler的两个方法:isShareble和exceptionCaught,这里是Netty的风格之一,就是定义完顶级接口后,分别有公共抽象子类、子接口来对功能进行增强。那么对于ChannelHandler的功能增强,则由:ChannelOutboundHandler、ChannelInboundHandler来进行的增强。 + +### 3.2 ChannelOutboundHandler、ChannelInboundHandler接口 + +```java +public interface ChannelInboundHandler extends ChannelHandler { + + /** + * 通道注册完成的回调方法,方法中多以fireChannelRegistered方法为主,作用是往pipeline中传播channelRegistered事件 + */ + void channelRegistered(ChannelHandlerContext ctx) throws Exception; + + /** + * 通道解除注册的回调方法 + */ + void channelUnregistered(ChannelHandlerContext ctx) throws Exception; + + /** + * 通道触发 + */ + void channelActive(ChannelHandlerContext ctx) throws Exception; + + /** + * The {@link Channel} of the {@link ChannelHandlerContext} was registered is now inactive and reached its + * end of lifetime. + */ + void channelInactive(ChannelHandlerContext ctx) throws Exception; + + /** + * 通道读取到消息 + */ + void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception; + + /** + * Invoked when the last message read by the current read operation has been consumed by + * {@link #channelRead(ChannelHandlerContext, Object)}. If {@link ChannelOption#AUTO_READ} is off, no further + * attempt to read an inbound data from the current {@link Channel} will be made until + * {@link ChannelHandlerContext#read()} is called. + */ + void channelReadComplete(ChannelHandlerContext ctx) throws Exception; + + /** + * Gets called if an user event was triggered. + */ + void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception; + + /** + * Gets called once the writable state of a {@link Channel} changed. You can check the state with + * {@link Channel#isWritable()}. + */ + void channelWritabilityChanged(ChannelHandlerContext ctx) throws Exception; + + /** + * Gets called if a {@link Throwable} was thrown. + */ + @Override + @SuppressWarnings("deprecation") + void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception; +} +``` + +```java +public interface ChannelOutboundHandler extends ChannelHandler { + /** + * 绑定socket事件回调 + */ + void bind(ChannelHandlerContext ctx, SocketAddress localAddress, ChannelPromise promise) throws Exception; + + /** + * socket连接回调 + */ + void connect( + ChannelHandlerContext ctx, SocketAddress remoteAddress, + SocketAddress localAddress, ChannelPromise promise) throws Exception; + + /** + * socket断开连接回调 + */ + void disconnect(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception; + + /** + * socket关闭回调 + */ + void close(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception; + + /** + * Called once a deregister operation is made from the current registered {@link EventLoop}. + * + * @param ctx the {@link ChannelHandlerContext} for which the close operation is made + * @param promise the {@link ChannelPromise} to notify once the operation completes + * @throws Exception thrown if an error occurs + */ + void deregister(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception; + + /** + * Intercepts {@link ChannelHandlerContext#read()}. + */ + void read(ChannelHandlerContext ctx) throws Exception; + + /** + * + */ + void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception; + + /** + * + */ + void flush(ChannelHandlerContext ctx) throws Exception; +} +``` + +### 3.3 ChannelInitializer抽象类 + +在来看看ChannelInitializer这个抽象类,定义了什么功能。 + +```java +@Sharable +public abstract class ChannelInitializer extends ChannelInboundHandlerAdapter { + + private static final InternalLogger logger = InternalLoggerFactory.getInstance(ChannelInitializer.class); + // initMap用于保存在不同pipeline之间共享的ChannelHandler对象,减少开销 + private final Set initMap = Collections.newSetFromMap( + new ConcurrentHashMap()); + + /** + * 初始化channel的抽象方法,具体由子类提供实现逻辑 + */ + protected abstract void initChannel(C ch) throws Exception; + + @Override + @SuppressWarnings("unchecked") + public final void channelRegistered(ChannelHandlerContext ctx) throws Exception { + // 通道注册完成后,对通道进行初始化 + if (initChannel(ctx)) { + // 将通道注册完这个事件往pipeline里传播 + ctx.pipeline().fireChannelRegistered(); + + // We are done with init the Channel, removing all the state for the Channel now. + removeState(ctx); + } else { + ctx.fireChannelRegistered(); + } + } + + /** + * Handle the {@link Throwable} by logging and closing the {@link Channel}. Sub-classes may override this. + */ + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { + if (logger.isWarnEnabled()) { + logger.warn("Failed to initialize a channel. Closing: " + ctx.channel(), cause); + } + ctx.close(); + } + + /** + * {@inheritDoc} If override this method ensure you call super! + */ + @Override + public void handlerAdded(ChannelHandlerContext ctx) throws Exception { + if (ctx.channel().isRegistered()) { + // This should always be true with our current DefaultChannelPipeline implementation. + // The good thing about calling initChannel(...) in handlerAdded(...) is that there will be no ordering + // surprises if a ChannelInitializer will add another ChannelInitializer. This is as all handlers + // will be added in the expected order. + if (initChannel(ctx)) { + + // We are done with init the Channel, removing the initializer now. + removeState(ctx); + } + } + } + + @Override + public void handlerRemoved(ChannelHandlerContext ctx) throws Exception { + initMap.remove(ctx); + } + + @SuppressWarnings("unchecked") + private boolean initChannel(ChannelHandlerContext ctx) throws Exception { + if (initMap.add(ctx)) { // Guard against re-entrance. + try { + // 调用抽象方法initChannel(channel) + initChannel((C) ctx.channel()); + } catch (Throwable cause) { + // Explicitly call exceptionCaught(...) as we removed the handler before calling initChannel(...). + // We do so to prevent multiple calls to initChannel(...). + exceptionCaught(ctx, cause); + } finally { + ChannelPipeline pipeline = ctx.pipeline(); + if (pipeline.context(this) != null) { + pipeline.remove(this); + } + } + return true; + } + return false; + } + + // 将Channelhandler从initMap中移除 + private void removeState(final ChannelHandlerContext ctx) { + // The removal may happen in an async fashion if the EventExecutor we use does something funky. + if (ctx.isRemoved()) { + initMap.remove(ctx); + } else { + // The context is not removed yet which is most likely the case because a custom EventExecutor is used. + // Let's schedule it on the EventExecutor to give it some more time to be completed in case it is offloaded. + ctx.executor().execute(new Runnable() { + @Override + public void run() { + initMap.remove(ctx); + } + }); + } + } +} +``` + +对于ChannelInboundHandlerAdapter这个抽象类来说,已经实现了ChannelInboundHandler这个接口的所有方法了,而ChannelOutboundHandlerAdapter抽象类同样已经实现了ChannelOutboundHandler接口的所有方法,因此继承了ChannelInitializer的实现类,只需要实现initChannel(Channel ch)方法即可。 + +下面看一个ChannelInitializer的例子 + +```java +public class HttpHelloWorldServerInitializer extends ChannelInitializer { + + private final SslContext sslCtx; + + public HttpHelloWorldServerInitializer(SslContext sslCtx) { + this.sslCtx = sslCtx; + } + + @Override + public void initChannel(SocketChannel ch) { + ChannelPipeline p = ch.pipeline(); + if (sslCtx != null) { + p.addLast(sslCtx.newHandler(ch.alloc())); + } + p.addLast(new HttpServerCodec()); + p.addLast(new HttpServerExpectContinueHandler()); + p.addLast(new HttpHelloWorldServerHandler()); + } +} + +``` + +由上面的例子知道,对于initChannel(Channel ch)方法而言,主要是用于往pipeline中添加ChannelHandler的。 + +## 4. ChannelHandlerContext源码分析 + +ChannelHandlerContext接口定义 + +```java +public interface ChannelHandlerContext extends AttributeMap, ChannelInboundInvoker, ChannelOutboundInvoker { + + /** + * 获得一个channel对象 + */ + Channel channel(); + + /** + * 获取一个EventExecutor对象,这里实际获得的是NioEventLoop + */ + EventExecutor executor(); + + /** + * 获取ChannelHandler的名称 + */ + String name(); + + /** + * 绑定在ChannelHandlerContext上的ChannelHandler + */ + ChannelHandler handler(); + + boolean isRemoved(); + + @Override + ChannelHandlerContext fireChannelRegistered(); + + @Override + ChannelHandlerContext fireChannelUnregistered(); + + @Override + ChannelHandlerContext fireChannelActive(); + + @Override + ChannelHandlerContext fireChannelInactive(); + + @Override + ChannelHandlerContext fireExceptionCaught(Throwable cause); + + @Override + ChannelHandlerContext fireUserEventTriggered(Object evt); + + @Override + ChannelHandlerContext fireChannelRead(Object msg); + + @Override + ChannelHandlerContext fireChannelReadComplete(); + + @Override + ChannelHandlerContext fireChannelWritabilityChanged(); + + @Override + ChannelHandlerContext read(); + + @Override + ChannelHandlerContext flush(); + + /** + * 获取pipeline + */ + ChannelPipeline pipeline(); + + /** + * 获取一个内存分配器 + */ + ByteBufAllocator alloc(); + + /** + * @deprecated Use {@link Channel#attr(AttributeKey)} + */ + @Deprecated + @Override + Attribute attr(AttributeKey key); + + /** + * @deprecated Use {@link Channel#hasAttr(AttributeKey)} + */ + @Deprecated + @Override + boolean hasAttr(AttributeKey key); +} +``` + + +可以看到ChannelHandlerContext分别继承了ChannelInboundInvoker和ChannelOutboundInvoker接口,在分析Channelpipeline章节时,介绍过其二者定义的功能,ChannelInboundInvoker多以fireXxxx方法构成,代表的是触发的Xxx事件的传播,例如: + +```java + @Override + public ChannelHandlerContext fireChannelRegistered() { + invokeChannelRegistered(findContextInbound(MASK_CHANNEL_REGISTERED)); + return this; + } +``` + +fireChannelRegistered方法就是触发了ChannelRegistered事件能够在ChannelPipeline中进行传播。 + +而ChannelOutboundInvoker多以socket的绑定、连接、读和写为住,常见的方法由write、flush以及writeAndFlush。 + +```java + @Override + public ChannelFuture writeAndFlush(Object msg, ChannelPromise promise) { + write(msg, true, promise); + return promise; + } +``` + +按照Netty架构的习惯,在给定一个接口之后,一般都会有对应的公共抽象类来定义公共的方法,并将需要定制的方法定义为抽象方法供不同的子类实现,照着这个思路可以找到AbstractChannelHandlerContext这个抽象类。 + +### 4.1 AbstractChannelHandlerContext源码分析 + +> 成员变量定义 + +在分析AbstractChannelHandlerContext源码之前,我们先看下它的成员变量定义,入下图所示,定义了两个volatile对象: + +1. volatile AbstractChannelHandlerContext next +2. volatile AbstractChannelHandlerContext prev + +这两个AbstractChannelHandlerContext对象作为指针实现了ChannelPipeline作为双向链表的数据结构。 + +```java + private static final AtomicIntegerFieldUpdater HANDLER_STATE_UPDATER = + AtomicIntegerFieldUpdater.newUpdater(AbstractChannelHandlerContext.class, "handlerState"); +``` + +> 关于XxxFieldUpdater类,可以阅读:https://my.oschina.net/u/4072299/blog/3115164 + +接着是AbstractChannelHandlerContext的状态: + +- private static final int ADD_PENDING = 1 +- private static final int ADD_COMPLETE = 2 +- private static final int REMOVE_COMPLETE = 3 + +ADD_PENDING表示正在调用handlerAdded,ADD_COMPLETE表示已经调用完成了handlerAdded,而REMOVE_COMPLETE表示已经调用完handlerRemoved方法。 + +而ChannelHandlerContext中还会存有ChannelPipeline。 + +```java +private final DefaultChannelPipeline pipeline +``` + +还有一个handlerState变量,用于定义当前ChannelHandler对象的状态,初始为INIT状态,表示handlerAdded和handlerRemove都还未调用过。 + +```java +private volatile int handlerState = INIT +``` + +!["ChannelPipeline-abstractChannelHandlerContext01"](https://coderbruis.github.io/javaDocs/img/netty/source/ChannelPipeline-abstractChannelHandlerContext01.png) + + +对于ChannelPipeline中的ChannelHandler是如何被调用以及如何移动双向链表中的对象的,实现原理就在这几个方法之间: + +AbstractChannelHandlerContext.java +```java + + /* + * 触发ChannelActive事件 + */ + @Override + public ChannelHandlerContext fireChannelActive() { + invokeChannelActive(findContextInbound(MASK_CHANNEL_ACTIVE)); + return this; + } + + static void invokeChannelActive(final AbstractChannelHandlerContext next) { + // 获取NioEventLoop线程 + EventExecutor executor = next.executor(); + // 如果获取到的NioEventLoop线程是当前的线程 + if (executor.inEventLoop()) { + next.invokeChannelActive(); + } else { + // 另开一个线程去执行 + executor.execute(new Runnable() { + @Override + public void run() { + next.invokeChannelActive(); + } + }); + } + } + + private void invokeChannelActive() { + // 检查ChannelHandler的状态 + if (invokeHandler()) { + try { + ((ChannelInboundHandler) handler()).channelActive(this); + } catch (Throwable t) { + invokeExceptionCaught(t); + } + } else { + // 遍历下一个节点,重新调用下一个节点 + fireChannelActive(); + } + } + + /* + * 找到inbound节点 + */ + private AbstractChannelHandlerContext findContextInbound(int mask) { + AbstractChannelHandlerContext ctx = this; + // 拿到当前的NioEventLoop线程 + EventExecutor currentExecutor = executor(); + do { + // 获取ChannelPipeline中的next节点 + ctx = ctx.next; + } while (skipContext(ctx, currentExecutor, mask, MASK_ONLY_INBOUND)); + return ctx; + } + + // 判断是否跳过此节点 + private static boolean skipContext( + AbstractChannelHandlerContext ctx, EventExecutor currentExecutor, int mask, int onlyMask) { + return (ctx.executionMask & (onlyMask | mask)) == 0 || + (ctx.executor() == currentExecutor && (ctx.executionMask & mask) == 0); + } +``` + +当调用来到 +```java +((ChannelInboundHandler) handler()).channelActive(this); +``` +会触发下一个Handler的channelActive,此处就拿Tail节点的channelActive来分析。 + +```java + @Override + public void channelActive(ChannelHandlerContext ctx) { + ctx.fireChannelActive(); + readIfIsAutoRead(); + } +``` +可以看到,调用又来到了ChannelHandlerContext#fireChannelActive,这样又要进行节点的遍历,就这样把事件传播了下去。 + + +由于篇幅原因,剩余ChannelPipeline的分析放在下一篇进行。 \ No newline at end of file diff --git "a/note/Netty/Netty\345\272\225\345\261\202\346\272\220\347\240\201\350\247\243\346\236\220-ChannelPipeline\345\210\206\346\236\220\357\274\210\344\270\213\357\274\211.md" "b/note/Netty/Netty\345\272\225\345\261\202\346\272\220\347\240\201\350\247\243\346\236\220-ChannelPipeline\345\210\206\346\236\220\357\274\210\344\270\213\357\274\211.md" new file mode 100644 index 0000000..9683989 --- /dev/null +++ "b/note/Netty/Netty\345\272\225\345\261\202\346\272\220\347\240\201\350\247\243\346\236\220-ChannelPipeline\345\210\206\346\236\220\357\274\210\344\270\213\357\274\211.md" @@ -0,0 +1,322 @@ +## 1. 概览 + +上篇已经讲解了ChannelPipeline以及ChannelHandler的关系以及对应的类继承关系图,本节来详细分析一下inbound和outbound的原理。 + +## 2. DefaultChannelPipeline源码分析 + +在DefaultChannelPipeline中,定义了一个head“头结点”和一个tail“尾结点”,它们都是AbstractChannelhandlerContext类的节点,我们都知道在ChannelPipeline中AbstractChannelHandlerContext就是节点元素的抽象类实现,而这个handlerContext持有ChannelHandler。 + +在Netty中我们还需要知道inbound和outbound类型的ChannelHandler节点的执行顺序。 + +下面来先看下一个Netty的demo + +该Netty的demo中,分别定义了六个Handler,分为两组,一组是inboundHandler,另一组是outboundHandler。 + + +InBoundHandlerA +```java +public class InBoundHandlerA extends ChannelInboundHandlerAdapter { + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + System.out.println("InBoundHandlerA: " + msg); + ctx.fireChannelRead(msg); + } +} +``` + +InBoundHandlerB +```java +public class OutBoundHandlerB extends ChannelOutboundHandlerAdapter { + @Override + public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { + System.out.println("OutBoundHandlerB: " + msg); + ctx.write(msg, promise); + } + + + @Override + public void handlerAdded(final ChannelHandlerContext ctx) { + ctx.executor().schedule(() -> { + ctx.channel().write("ctx.channel().write -> hello world"); + ctx.write("hello world"); + }, 3, TimeUnit.SECONDS); + } +} +``` + +InBoundHandlerC +```java +public class InBoundHandlerC extends ChannelInboundHandlerAdapter { + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + System.out.println("InBoundHandlerC: " + msg); + ctx.fireChannelRead(msg); + } +} +``` + +```java +public final class Server { + + public static void main(String[] args) throws Exception { + EventLoopGroup bossGroup = new NioEventLoopGroup(1); + EventLoopGroup workerGroup = new NioEventLoopGroup(); + + try { + ServerBootstrap b = new ServerBootstrap(); + b.group(bossGroup, workerGroup) + .channel(NioServerSocketChannel.class) + .childOption(ChannelOption.TCP_NODELAY, true) + .childAttr(AttributeKey.newInstance("childAttr"), "childAttrValue") + .childHandler(new ChannelInitializer() { + @Override + public void initChannel(SocketChannel ch) { + ch.pipeline().addLast(new InBoundHandlerA()); + ch.pipeline().addLast(new InBoundHandlerB()); + ch.pipeline().addLast(new InBoundHandlerC()); + } + }); + + ChannelFuture f = b.bind(8888).sync(); + + f.channel().closeFuture().sync(); + } finally { + bossGroup.shutdownGracefully(); + workerGroup.shutdownGracefully(); + } + } +} +``` + +执行结果如下: +``` +InBoundHandlerA: hello world +InBoundHandlerB: hello world +InBoundHandlerC: hello world +``` + +可以发现Netty中,对于inboundHandler来说是按照顺序执行操作的。 + +接着在看看outboundHandler定义如下 + +OutBoundHandlerA +```java +public class OutBoundHandlerA extends ChannelOutboundHandlerAdapter { + + @Override + public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { + System.out.println("OutBoundHandlerA: " + msg); + ctx.write(msg, promise); + } +} +``` + +OutBoundHandlerB +```java +public class OutBoundHandlerB extends ChannelOutboundHandlerAdapter { + @Override + public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { + System.out.println("OutBoundHandlerB: " + msg); + ctx.write(msg, promise); + } +} +``` + +OutBoundHandlerC +```java +public class OutBoundHandlerC extends ChannelOutboundHandlerAdapter { + + @Override + public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { + System.out.println("OutBoundHandlerC: " + msg); + ctx.write(msg, promise); + } +} +``` + + +然后修改Server类为如下, + +```java +public final class Server { + + public static void main(String[] args) throws Exception { + EventLoopGroup bossGroup = new NioEventLoopGroup(1); + EventLoopGroup workerGroup = new NioEventLoopGroup(); + + try { + ServerBootstrap b = new ServerBootstrap(); + b.group(bossGroup, workerGroup) + .channel(NioServerSocketChannel.class) + .childOption(ChannelOption.TCP_NODELAY, true) + .childAttr(AttributeKey.newInstance("childAttr"), "childAttrValue") + .childHandler(new ChannelInitializer() { + @Override + public void initChannel(SocketChannel ch) { + ch.pipeline().addLast(new OutBoundHandlerA()); + ch.pipeline().addLast(new OutBoundHandlerB()); + ch.pipeline().addLast(new OutBoundHandlerC()); + } + }); + + ChannelFuture f = b.bind(8888).sync(); + + f.channel().closeFuture().sync(); + } finally { + bossGroup.shutdownGracefully(); + workerGroup.shutdownGracefully(); + } + } +} +``` + +执行结果如下: +``` +OutBoundHandlerC: ctx.channel().write -> hello world +OutBoundHandlerB: ctx.channel().write -> hello world +OutBoundHandlerA: ctx.channel().write -> hello world +OutBoundHandlerA: hello world +``` + +可以看到在Netty中对于ountboundHandler来说,是倒序执行的。 + +整个Netty执行ChannelHandler可以用下图来描述。 + +![channelPipeline事件传播图](https://coderbruis.github.io/javaDocs/img/netty/source/ChannelPipeline事件传播图.png) + + +上图描述的Head节点顺序执行,Tail节点逆序执行的源码是在DefaultChannelPipeline中,在《Netty-ChannelPipeline-上》文章开头就已经说明了,对于inboundHandler类型的Handler,主要还是用于监听Channel的read、register、active、exceptionCaught等事件,而对于outboundHandler类型来说,主要是用于bind、connect、write、flush等事件,回顾了这一点后,我们在继续看DefaultChannelPipeline源码 + +```java +public class DefaultChannelPipeline implements ChannelPipeline { + ... 省略 + + @Override + public final ChannelPipeline fireChannelRead(Object msg) { + AbstractChannelHandlerContext.invokeChannelRead(head, msg); + return this; + } + + @Override + public final ChannelFuture write(Object msg) { + return tail.write(msg); + } + + ... 省略 +} +``` + +分别以inbound类型的channelRead和outbound类型的write来分析。 + +DefaultChannelPipeline.java +```java + @Override + public final ChannelPipeline fireChannelRead(Object msg) { + AbstractChannelHandlerContext.invokeChannelRead(head, msg); + return this; + } +``` +在AbstractChannelHandlerContext#invokeChannelRead方法中,传入了一个重要的入参:head,这里就是传入的Head头结点,这一重要调用得以让inbound类型handler在ChannelPipeline中按顺序执行。 + +AbstractChannelHandlerContext.java +```java + static void invokeChannelRead(final AbstractChannelHandlerContext next, Object msg) { + final Object m = next.pipeline.touch(ObjectUtil.checkNotNull(msg, "msg"), next); + EventExecutor executor = next.executor(); + // 在NioEventLoop线程内,next这里传入的是head头结点 + if (executor.inEventLoop()) { + next.invokeChannelRead(m); + } else { + executor.execute(new Runnable() { + @Override + public void run() { + next.invokeChannelRead(m); + } + }); + } + } + + private void invokeChannelRead(Object msg) { + if (invokeHandler()) { + try { + ((ChannelInboundHandler) handler()).channelRead(this, msg); + } catch (Throwable t) { + invokeExceptionCaught(t); + } + } else { + fireChannelRead(msg); + } + } + +``` + +ChannelInboundHandler#channelRead的调用,会最终来到InBoundHandlerA里的channelRead方法。 +```java +public class InBoundHandlerA extends ChannelInboundHandlerAdapter { + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + System.out.println("InBoundHandlerA: " + msg); + ctx.fireChannelRead(msg); + } +} +``` + +经过AbstractChannelHandlerContext#fireChannelRead,会在ChannelPipeline中寻找下一个inbound,然后继续执行channelRead。 + +```java + @Override + public ChannelHandlerContext fireChannelRead(final Object msg) { + invokeChannelRead(findContextInbound(MASK_CHANNEL_READ), msg); + return this; + } +``` + + +细看OutBoundHandlerB#handlerAdded方法由两个write,一个是ctx.channel.write,另一个是ctx.write,这两个有啥区别呢?为啥输出结果是三条:ctx.channel().write -> hello world,一条hello world呢? + +启动Server启动类之后,再cmd窗口输入连接socket的命令debug之后分析得 + +``` +telnet 127.0.0.1 8888 +``` + +在客户端socket连接进Netty之后,会先注册channel并init初始化,这时会调用Server类里ServerBootstrap注入的ChannelInitilizer的initChannel方法,最终得以向ChannelPipeline里添加进OutBoundHandlerA、OutBoundHandlerB、OutBoundHandlerC,随后调用 + +```java +ch.pipeline().addLast(new xxxx) +``` +只有会触发DefaultChannelPipeline#callHandlerAdded0()方法,最终来到OutBoundHandler里的handlerAdded()方法,并向Netty的定时任务队列里添加了一个匿名内部任务,也就是: + +```java + @Override + public void handlerAdded(final ChannelHandlerContext ctx) { + ctx.executor().schedule(() -> { + ctx.channel().write("ctx.channel().write -> hello world"); + ctx.write("hello world"); + }, 3, TimeUnit.SECONDS); + } +``` + +随后完成客户端Socket的初始化工作。此时服务端的selector继续执行for死循环,执行到任务队列,此时发现任务队列中有一个定时任务需要执行,则拿出任务并执行任务,执行过程会跳转到上面的匿名内部类,并依次执行ctx.channel().write()和ctx.write()两个方法。 + +```java +ctx.channel().write() +``` +方法会从ChannelPipeline的尾部tail开始执行(上文已经总结过,outboundHandler都是从tail节点开始执行handler) ,所以字符串“ctx.channel().write -> hello world”就会按outboundHandlerC、outboundHandlerB、outboundHandlerC这个顺序开始执行,执行完head节点之后会一路往上返回到Ctx.channel().write() +方法,并最后去执行ctx.write()方法,而ctx.write()方法会从当前的handler节点开始向前执行,所以当前outboundHandlerB的前节点是outboundHandlerA,所以最终控制台打印出: +``` +OutBoundHandlerC: ctx.channel().write -> hello world +OutBoundHandlerB: ctx.channel().write -> hello world +OutBoundHandlerA: ctx.channel().write -> hello world +OutBoundHandlerA: hello world +``` + +整个过程比较复杂,也比较绕,下面用一张流程图来描述整个过程。 + +![NettyChannelPipeline流程图1](https://coderbruis.github.io/javaDocs/img/netty/source/NettyChannelPipeline流程图1.png) + + +- TODO ChannelPipeline优化?MASK +- TODO SimpleChannelInboundHandler源码分析 diff --git "a/note/Netty/Netty\345\272\225\345\261\202\346\272\220\347\240\201\350\247\243\346\236\220-Netty\346\234\215\345\212\241\347\253\257\345\220\257\345\212\250\345\210\206\346\236\220.md" "b/note/Netty/Netty\345\272\225\345\261\202\346\272\220\347\240\201\350\247\243\346\236\220-Netty\346\234\215\345\212\241\347\253\257\345\220\257\345\212\250\345\210\206\346\236\220.md" new file mode 100644 index 0000000..986e355 --- /dev/null +++ "b/note/Netty/Netty\345\272\225\345\261\202\346\272\220\347\240\201\350\247\243\346\236\220-Netty\346\234\215\345\212\241\347\253\257\345\220\257\345\212\250\345\210\206\346\236\220.md" @@ -0,0 +1,498 @@ +# Netty 服务端启动分析 + +在Java中,网络通信是通过Socket来进行的,那么在Netty中,服务端要用到的Socket是在哪里进行初始化的?并且在哪里进行accept接受客户端连接的? Netty里的Channel是啥,有啥作用呢?带着这三个问题,进入本文的Netty服务端启动分析。 + +本文分析将分为五大步: + +1. Netty中的Channel; +2. 创建服务端Channel; +3. 初始化服务端Channel; +4. 注册selector; +5. 端口绑定; + +## 1. Netty中的Channel + +在Netty中的Channel是用来定义对网络IO进行读/写的相关接口,与NIO中的Channel接口类似。Channel的功能主要有网络IO的读写、客户端发起的连接、主动关闭连接、关闭链路、获取通信双方的网络地址等。Channel接口下有一个重要的抽象类————AbstractChannel,一些公共的基础方法都在这个抽象类中实现,但对于一些特定的功能则需要不同的实现类去实现,这样最大限度地实现了功能和接口的重用。 + +在AbstractChannel中的网络IO模型和协议种类比较多,除了TCP协议,Netty还支持了HTTP2协议,如:AbstractHttp2StreamChannel。 + +Netty对于不同的网络模型以及IO模型,在AbstractChannel的基础上又抽象出了一层,如:AbstractNioChannel、AbstractEpollChannel、AbstractHttp2StreamChannel。 + +## 2. 创建服务端Channel + +创建服务端Channel又可以分为四步,如下: + +1. ServerBootstrap#bind() 用户代码入口; +2. initAndRegister() 初始化并注册; +3. newChannel() 创建服务端channel; + + +首先看下下图简易版的Netty服务端启动代码。 + +```java +public final class Server { + + public static void main(String[] args) throws Exception { + EventLoopGroup bossGroup = new NioEventLoopGroup(1); + EventLoopGroup workerGroup = new NioEventLoopGroup(); + + try { + ServerBootstrap b = new ServerBootstrap(); + b.group(bossGroup, workerGroup) + .channel(NioServerSocketChannel.class) + .childOption(ChannelOption.TCP_NODELAY, true) + .childAttr(AttributeKey.newInstance("childAttr"), "childAttrValue") + .handler(new ServerHandler()) + .childHandler(new ChannelInitializer() { + @Override + public void initChannel(SocketChannel ch) { + ch.pipeline().addLast(new AuthHandler()); + //.. + + } + }); + + ChannelFuture f = b.bind(8888).sync(); + + f.channel().closeFuture().sync(); + } finally { + bossGroup.shutdownGracefully(); + workerGroup.shutdownGracefully(); + } + } +} +``` + +服务端构建好ServerBootstrap之后,通过bind()方法进行绑定。进入ServerBootstrap的父类AbstractBootstrap后,线程经过调用栈的调用后来到AbstractBootstrap#doBind()方法,首先就是初始化并注册Channel。 + +AbstractBootstrap#doBind() +```java + private ChannelFuture doBind(final SocketAddress localAddress) { + // 注册channel + final ChannelFuture regFuture = initAndRegister(); + final Channel channel = regFuture.channel(); + if (regFuture.cause() != null) { + return regFuture; + } + + // regFuture如果完成了,则isDone为true,否则给regFuture添加一个监听器,当完成的时候再进行doBind0的操作 + if (regFuture.isDone()) { + // 此时我们已经知道NioServerSocketChannel已经完成了注册 + ChannelPromise promise = channel.newPromise(); + doBind0(regFuture, channel, localAddress, promise); + return promise; + } else { + // Registration future is almost always fulfilled already, but just in case it's not. + final PendingRegistrationPromise promise = new PendingRegistrationPromise(channel); + + // 给regFuture添加一个监听器,当注册chanel完成的时候,会回调进来 + regFuture.addListener(new ChannelFutureListener() { + @Override + public void operationComplete(ChannelFuture future) throws Exception { + Throwable cause = future.cause(); + if (cause != null) { + // Registration on the EventLoop failed so fail the ChannelPromise directly to not cause an + // IllegalStateException once we try to access the EventLoop of the Channel. + promise.setFailure(cause); + } else { + // Registration was successful, so set the correct executor to use. + // See https://github.com/netty/netty/issues/2586 + promise.registered(); + + doBind0(regFuture, channel, localAddress, promise); + } + } + }); + return promise; + } + } + + final ChannelFuture initAndRegister() { + Channel channel = null; + try { + // 拿到ReflectiveChannelFactory,然后通过其newChannel生成一个服务端Channel,底层就是通过反射newInstance()获取实例 + // 这里自然是NioServerSocketChannel实例对象 + channel = channelFactory.newChannel(); + // 初始化channel + init(channel); + } catch (Throwable t) { + if (channel != null) { + // channel can be null if newChannel crashed (eg SocketException("too many open files")) + channel.unsafe().closeForcibly(); + // as the Channel is not registered yet we need to force the usage of the GlobalEventExecutor + return new DefaultChannelPromise(channel, GlobalEventExecutor.INSTANCE).setFailure(t); + } + // as the Channel is not registered yet we need to force the usage of the GlobalEventExecutor + return new DefaultChannelPromise(new FailedChannel(), GlobalEventExecutor.INSTANCE).setFailure(t); + } + + /** + * config() -> ServerBootstrapConfig + * group() -> NioEventLoopGroup,返回的是MultithreadEventLoopGroup + * register() -> 就是通过chooser选取到NioEventLoop对象 + */ + ChannelFuture regFuture = config().group().register(channel); + if (regFuture.cause() != null) { + if (channel.isRegistered()) { + channel.close(); + } else { + channel.unsafe().closeForcibly(); + } + } + return regFuture; + } +``` + +在initAndRegister处channelFactory是ReflectiveChannelFactory,具体赋值处是在ServerBootstrap#channel()方法中定义的,并且传入的channel是:NioServerSocketChannel。 + +ReflectiveChannelFactory#newChannel +```java + @Override + public T newChannel() { + try { + return constructor.newInstance(); + } catch (Throwable t) { + throw new ChannelException("Unable to create Channel from class " + constructor.getDeclaringClass(), t); + } + } +``` + +查看到ReflectiveChannelFactory#newChannel()方法,T的类型是NioServerSocketChannel,所以实际就是调用的NioServerSocketChannel#newInstance()方法反射构建一个channel对象。 + + +那么,我们看下NioServerSocketChannel底层是如何获取通过反射创建服务端Channel的呢? + + +以下部分源码均在NioServerSocketChannel类中 + +```java + public NioServerSocketChannel() { + this(newSocket(DEFAULT_SELECTOR_PROVIDER)); + } +``` + +而newSocket()方法是一个静态方法 + +```java + private static ServerSocketChannel newSocket(SelectorProvider provider) { + try { + // 通过SelectorProvider来获取一个ServerSocketChannel,SelectorProvider是通过SPI来获取的 + // 此处返回一个ServerSocketChannelImpl对象 + return provider.openServerSocketChannel(); + } catch (IOException e) { + throw new ChannelException( + "Failed to open a server socket.", e); + } + } +``` + +```java + public NioServerSocketChannel(ServerSocketChannel channel) { + // 调用抽象父类AbstractNioChannel构造方法,注意此处服务端Channel注册的是OP_ACCEPT事件 + super(null, channel, SelectionKey.OP_ACCEPT); + // TCP参数配置类 + config = new NioServerSocketChannelConfig(this, javaChannel().socket()); + } +``` + +AbstractNioChannel类 +```java + protected AbstractNioChannel(Channel parent, SelectableChannel ch, int readInterestOp) { + // 调用父类AbstractChannel的构造方法 + super(parent); + this.ch = ch; + this.readInterestOp = readInterestOp; + try { + // 关闭blocking,关闭阻塞模式:比较重要 + ch.configureBlocking(false); + } catch (IOException e) { + try { + ch.close(); + } catch (IOException e2) { + if (logger.isWarnEnabled()) { + logger.warn( + "Failed to close a partially initialized socket.", e2); + } + } + + throw new ChannelException("Failed to enter non-blocking mode.", e); + } + } +``` + +AbstractChannel类 +```java + protected AbstractChannel(Channel parent) { + this.parent = parent; + // 每个channel的唯一标识 + id = newId(); + // 底层io操作工具类 + unsafe = newUnsafe(); + // channel里的逻辑链pipeline(非常重要) + pipeline = newChannelPipeline(); + } +``` + +通过源码阅读,可以总结出Netty服务端创建Channel的三件重要事情: + +1. 通过反射来创建JDK底层的channel; +2. 设置Channel为非阻塞模式ch.configureBlocking(false); +3. 创建一个pipeline对象; + + +## 3. 初始化服务端Channel + +初始化服务端Channel可以分为如下的几步: + +1. set ChannelOptions,ChannelAttrs 设置options和attrs +2. set Child Options,ChildAttrs,为服务端创建的子链接创建options和attrs +3. config handler,配置服务端pipeline +4. add ServerBootstrapAcceptor,添加连接器 + + +ServerBoostrap端初始化过程 +```java + void init(Channel channel) throws Exception { + // 获取用户配置的options + final Map, Object> options = options0(); + synchronized (options) { + channel.config().setOptions(options); + } + + // 配置attrs + final Map, Object> attrs = attrs0(); + synchronized (attrs) { + for (Entry, Object> e: attrs.entrySet()) { + @SuppressWarnings("unchecked") + AttributeKey key = (AttributeKey) e.getKey(); + channel.attr(key).set(e.getValue()); + } + } + + ChannelPipeline p = channel.pipeline(); + + final EventLoopGroup currentChildGroup = childGroup; + final ChannelHandler currentChildHandler = childHandler; + final Entry, Object>[] currentChildOptions; + final Entry, Object>[] currentChildAttrs; + synchronized (childOptions) { + currentChildOptions = childOptions.entrySet().toArray(newOptionArray(childOptions.size())); + } + synchronized (childAttrs) { + currentChildAttrs = childAttrs.entrySet().toArray(newAttrArray(childAttrs.size())); + } + + p.addLast(new ChannelInitializer() { + @Override + public void initChannel(Channel ch) throws Exception { + final ChannelPipeline pipeline = ch.pipeline(); + ChannelHandler handler = config.handler(); + if (handler != null) { + pipeline.addLast(handler); + } + + // 添加ServerBootstrapAccetor + ch.eventLoop().execute(new Runnable() { + @Override + public void run() { + pipeline.addLast(new ServerBootstrapAcceptor( + currentChildGroup, currentChildHandler, currentChildOptions, currentChildAttrs)); + } + }); + } + }); + } +``` + +## 4. 将Channel注册到selector + +整个注册selector过程可以分为以下几步: + +1. AbstractChannel$AbstractUnsafe#register(channel) +2. AbstractUnsafe#register0() +3. AbstractUnsafe#doRegister() + +AbstractChannel +```java + @Override + public final void register(EventLoop eventLoop, final ChannelPromise promise) { + if (eventLoop == null) { + throw new NullPointerException("eventLoop"); + } + if (isRegistered()) { + promise.setFailure(new IllegalStateException("registered to an event loop already")); + return; + } + if (!isCompatible(eventLoop)) { + promise.setFailure( + new IllegalStateException("incompatible event loop type: " + eventLoop.getClass().getName())); + return; + } + + // 设置AbstractChannel的eventLoop + AbstractChannel.this.eventLoop = eventLoop; + + if (eventLoop.inEventLoop()) { + register0(promise); + } else { + try { + eventLoop.execute(new Runnable() { + @Override + public void run() { + register0(promise); + } + }); + } catch (Throwable t) { + logger.warn( + "Force-closing a channel whose registration task was not accepted by an event loop: {}", + AbstractChannel.this, t); + closeForcibly(); + closeFuture.setClosed(); + safeSetFailure(promise, t); + } + } + } +``` + +AbstractChannel.AbstractUnsafe#register0() +```java + private void register0(ChannelPromise promise) { + try { + if (!promise.setUncancellable() || !ensureOpen(promise)) { + return; + } + boolean firstRegistration = neverRegistered; + doRegister(); + neverRegistered = false; + registered = true; + + // 调用handlerAdd事件回调 + pipeline.invokeHandlerAddedIfNeeded(); + + safeSetSuccess(promise); + // 调用register事件回调 + pipeline.fireChannelRegistered(); + + if (isActive()) { + if (firstRegistration) { + pipeline.fireChannelActive(); + } else if (config().isAutoRead()) { + beginRead(); + } + } + } catch (Throwable t) { + closeForcibly(); + closeFuture.setClosed(); + safeSetFailure(promise, t); + } + } +``` + + +AbstractNioChannel.java +```java + @Override + protected void doRegister() throws Exception { + boolean selected = false; + // 这里是个小技巧,for(;;)比while(true)效率要高很多 + for (;;) { + try { + // 将通道channel注册到selector上 + selectionKey = javaChannel().register(eventLoop().selector, 0, this); + return; + } catch (CancelledKeyException e) { + if (!selected) { + eventLoop().selectNow(); + selected = true; + } else { + throw e; + } + } + } + } +``` + +就这样,NioServerSocketChannel就以Accept事件注册到了Selector上了。 + +这里需要注意一点,javaChannel()返回的是AbstractSelectableChannel,调用其register方法用于在给定的selector上注册这个通道channel,并返回一个选这件selectionKey。传入的操作位为0表示对任何事件都不感兴趣,仅仅是完成注册操作。 + +## 5. 端口绑定 + +端口绑定流程如下: + +1. AbstractBootstrap#bind() +2. AbstractBootstrap#dobind() +3. AbstractChannel#bind() +4. NioServerSocketChannel#doBind() + + +AbstractChannel.AbstractUnsafe#bind() +```java + @Override + public final void bind(final SocketAddress localAddress, final ChannelPromise promise) { + assertEventLoop(); + + if (!promise.setUncancellable() || !ensureOpen(promise)) { + return; + } + + // See: https://github.com/netty/netty/issues/576 + if (Boolean.TRUE.equals(config().getOption(ChannelOption.SO_BROADCAST)) && + localAddress instanceof InetSocketAddress && + !((InetSocketAddress) localAddress).getAddress().isAnyLocalAddress() && + !PlatformDependent.isWindows() && !PlatformDependent.isRoot()) { + // Warn a user about the fact that a non-root user can't receive a + // broadcast packet on *nix if the socket is bound on non-wildcard address. + logger.warn( + "A non-root user can't receive a broadcast packet if the socket " + + "is not bound to a wildcard address; binding to a non-wildcard " + + "address (" + localAddress + ") anyway as requested."); + } + + // 是否active + boolean wasActive = isActive(); + try { + // 调用jdk底层代码进行绑定 + doBind(localAddress); + } catch (Throwable t) { + safeSetFailure(promise, t); + closeIfClosed(); + return; + } + + if (!wasActive && isActive()) { + invokeLater(new Runnable() { + @Override + public void run() { + pipeline.fireChannelActive(); + } + }); + } + safeSetSuccess(promise); + } +``` + +NioServerSocketChannel.java +```java + @Override + protected void doBind(SocketAddress localAddress) throws Exception { + // 判断jdk版本 + if (PlatformDependent.javaVersion() >= 7) { + javaChannel().bind(localAddress, config.getBacklog()); + } else { + javaChannel().socket().bind(localAddress, config.getBacklog()); + } + } +``` + + +## 总结 + +Netty服务端核心启动流程主要是为了创建NioServerSocketChannel,然后将其注册在Selector上,总结下核心步骤如下: + +- NioServerSocket#newSocket() 获取服务端channel +- ServerBootstrap#init() 对服务端channel进行初始化 +- AbstractChannel.AbstractUnsafe#register() 将服务端Channel注册到Selector上 +- AbstractChannel.AbstractUnsafe#doBind() 注册端口号 + + diff --git "a/note/Netty/Netty\345\272\225\345\261\202\346\272\220\347\240\201\350\247\243\346\236\220-NioEventLoop\345\216\237\347\220\206\345\210\206\346\236\220.md" "b/note/Netty/Netty\345\272\225\345\261\202\346\272\220\347\240\201\350\247\243\346\236\220-NioEventLoop\345\216\237\347\220\206\345\210\206\346\236\220.md" new file mode 100644 index 0000000..2155406 --- /dev/null +++ "b/note/Netty/Netty\345\272\225\345\261\202\346\272\220\347\240\201\350\247\243\346\236\220-NioEventLoop\345\216\237\347\220\206\345\210\206\346\236\220.md" @@ -0,0 +1,607 @@ +## 1. 概述 + +在对NioEventLoop原理进行分析之前,先提出三个常见问题: + +- 默认情况下,Netty服务端起多少个线程?合适启动的线程? +- Netty是如何解决jdk的空轮训bug的? +- Netty是如何保证异步串行无锁化的? + +带着这三个问题,进入下面的NioEventLoop原理分析 + + +## 2. NioEventLoop是什么 + +这里可以先简单的将NioEventLoop理解为一个线程,但是NioEventLoop是被封装过的“线程”,这里的不同之处可以埋一个坑进行深入分析。 + +而NioEventLoop在Netty中起到什么作用呢?读完下文的你应该就会有答案了。 + +## 3. NioEventLoop的创建 + +首先,抛出一个结论,NioEventLoop是在NioEventLoopGroup中创建出来的,具体方法逻辑如下: + +NioEventLoopGroup.java + +```java + @Override + protected EventLoop newChild(Executor executor, Object... args) throws Exception { + return new NioEventLoop(this, executor, (SelectorProvider) args[0], + ((SelectStrategyFactory) args[1]).newSelectStrategy(), (RejectedExecutionHandler) args[2]); + } +``` + +可以看出,在NioEventLoopGroup中的newChild方法入参里,传入了一个Executor对象,这是一个线程池对象,除此之外,还有可变参数args,带入到NioEventLoop的构造 +方法里,分别强转为了SelectorProvider、SelectStrategyFactory和RejectedExecutionHandler这三个对象。读到这里可能很多人都会懵,newChild是从哪里调进来的? + +为了故事的顺利发展,让我们的目光来到NioEventLoopGroup,查看下其Diagrams图。 + +![netty02png](https://coderbruis.github.io/javaDocs/img/netty/source/netty02_02.png) + +可以看到NioEventLoopGroup的顶级父接口是Executor,直白点理解NioEventLoopGroup就是一个线程池,NioEventLoop就是其创建出来的一个线程!下面看到NioEventLoopGroup的构造方法 + +NioEventLoopGroup.java +```Java + public NioEventLoopGroup() { + this(0); + } + + public NioEventLoopGroup(int nThreads) { + this(nThreads, (Executor) null); + } + + public NioEventLoopGroup(int nThreads, ThreadFactory threadFactory) { + this(nThreads, threadFactory, SelectorProvider.provider()); + } + + public NioEventLoopGroup(int nThreads, Executor executor) { + this(nThreads, executor, SelectorProvider.provider()); + } + + public NioEventLoopGroup( + int nThreads, ThreadFactory threadFactory, final SelectorProvider selectorProvider) { + this(nThreads, threadFactory, selectorProvider, DefaultSelectStrategyFactory.INSTANCE); + } + + public NioEventLoopGroup(int nThreads, ThreadFactory threadFactory, + final SelectorProvider selectorProvider, final SelectStrategyFactory selectStrategyFactory) { + super(nThreads, threadFactory, selectorProvider, selectStrategyFactory, RejectedExecutionHandlers.reject()); + } + + public NioEventLoopGroup( + int nThreads, Executor executor, final SelectorProvider selectorProvider) { + this(nThreads, executor, selectorProvider, DefaultSelectStrategyFactory.INSTANCE); + } + + public NioEventLoopGroup(int nThreads, Executor executor, final SelectorProvider selectorProvider, + final SelectStrategyFactory selectStrategyFactory) { + super(nThreads, executor, selectorProvider, selectStrategyFactory, RejectedExecutionHandlers.reject()); + } + + public NioEventLoopGroup(int nThreads, Executor executor, EventExecutorChooserFactory chooserFactory, + final SelectorProvider selectorProvider, + final SelectStrategyFactory selectStrategyFactory) { + super(nThreads, executor, chooserFactory, selectorProvider, selectStrategyFactory, + RejectedExecutionHandlers.reject()); + } + + public NioEventLoopGroup(int nThreads, Executor executor, EventExecutorChooserFactory chooserFactory, + final SelectorProvider selectorProvider, + final SelectStrategyFactory selectStrategyFactory, + final RejectedExecutionHandler rejectedExecutionHandler) { + super(nThreads, executor, chooserFactory, selectorProvider, selectStrategyFactory, rejectedExecutionHandler); + } +``` + +这么多的构造方法,每个方法传入的入参都不一样,可以看到默认构造方法调用的是 +```java + public NioEventLoopGroup() { + this(0); + } +``` +一路跟到父类MultithreadEventLoopGroup,可以看到如下关键代码: + +MultithreadEventLoopGroup +```java + private static final int DEFAULT_EVENT_LOOP_THREADS; + static { + DEFAULT_EVENT_LOOP_THREADS = Math.max(1, SystemPropertyUtil.getInt( + "io.netty.eventLoopThreads", Runtime.getRuntime().availableProcessors() * 2)); + + if (logger.isDebugEnabled()) { + logger.debug("-Dio.netty.eventLoopThreads: {}", DEFAULT_EVENT_LOOP_THREADS); + } + } + + protected MultithreadEventLoopGroup(int nThreads, ThreadFactory threadFactory, Object... args) { + super(nThreads == 0 ? DEFAULT_EVENT_LOOP_THREADS : nThreads, threadFactory, args); + } +``` + +可以得出一个结论就是:当NioEventLoopGroup默认黄金的线程数是 2 * CPU 个。 + +NioEventLoop的创建核心逻辑在MultithreadEventExecutorGroup构造方法中,可以看到如下逻辑,分析内容待定... + +MultithreadEventExecutorGroup.java +```java + protected MultithreadEventExecutorGroup(int nThreads, Executor executor, + EventExecutorChooserFactory chooserFactory, Object... args) { + if (nThreads <= 0) { + throw new IllegalArgumentException(String.format("nThreads: %d (expected: > 0)", nThreads)); + } + + if (executor == null) { + executor = new ThreadPerTaskExecutor(newDefaultThreadFactory()); + } + + children = new EventExecutor[nThreads]; + + for (int i = 0; i < nThreads; i ++) { + boolean success = false; + try { + children[i] = newChild(executor, args); + success = true; + } catch (Exception e) { + // TODO: Think about if this is a good exception type + throw new IllegalStateException("failed to create a child event loop", e); + } finally { + if (!success) { + for (int j = 0; j < i; j ++) { + children[j].shutdownGracefully(); + } + + for (int j = 0; j < i; j ++) { + EventExecutor e = children[j]; + try { + while (!e.isTerminated()) { + e.awaitTermination(Integer.MAX_VALUE, TimeUnit.SECONDS); + } + } catch (InterruptedException interrupted) { + // Let the caller handle the interruption. + Thread.currentThread().interrupt(); + break; + } + } + } + } + } + + chooser = chooserFactory.newChooser(children); + + final FutureListener terminationListener = new FutureListener() { + @Override + public void operationComplete(Future future) throws Exception { + if (terminatedChildren.incrementAndGet() == children.length) { + terminationFuture.setSuccess(null); + } + } + }; + + for (EventExecutor e: children) { + e.terminationFuture().addListener(terminationListener); + } + + Set childrenSet = new LinkedHashSet(children.length); + Collections.addAll(childrenSet, children); + readonlyChildren = Collections.unmodifiableSet(childrenSet); + } +``` + +![netty02_02png](https://coderbruis.github.io/javaDocs/img/netty/source/netty02_02.png) + +NioEventLoopGroup#newChild() +```java + @Override + protected EventLoop newChild(Executor executor, Object... args) throws Exception { + return new NioEventLoop(this, executor, (SelectorProvider) args[0], + ((SelectStrategyFactory) args[1]).newSelectStrategy(), (RejectedExecutionHandler) args[2]); + } +``` + +下面是NioEventLoop类的构造方法 +```java + NioEventLoop(NioEventLoopGroup parent, Executor executor, SelectorProvider selectorProvider, + SelectStrategy strategy, RejectedExecutionHandler rejectedExecutionHandler) { + super(parent, executor, false, DEFAULT_MAX_PENDING_TASKS, rejectedExecutionHandler); + if (selectorProvider == null) { + throw new NullPointerException("selectorProvider"); + } + if (strategy == null) { + throw new NullPointerException("selectStrategy"); + } + provider = selectorProvider; + // 此处可以看出,每个NioEventLoop都会和一个Selector绑定对应 + selector = openSelector(); + selectStrategy = strategy; + } +``` + +每个NioEventLoop都会和一个Selector进行绑定对应。 + + + +SingleThreadEventLoop.java构造方法中 +```java + protected SingleThreadEventLoop(EventLoopGroup parent, Executor executor, + boolean addTaskWakesUp, int maxPendingTasks, + RejectedExecutionHandler rejectedExecutionHandler) { + super(parent, executor, addTaskWakesUp, maxPendingTasks, rejectedExecutionHandler); + tailTasks = newTaskQueue(maxPendingTasks); + } +``` + +NioEventLoop.java +```java + @Override + protected Queue newTaskQueue(int maxPendingTasks) { + // This event loop never calls takeTask() + return PlatformDependent.newMpscQueue(maxPendingTasks); + } +``` + +这里的任务队列有啥用? + +我们都知道,在Netty中是多线程的,即多个NioEventLoop,任务队列的作用就是当其他NioEventLoop拿到CPU的执行权时,却得到了其他线程的IO请求,此时NioEventLoop就把当前这个未处理完的请求 +以任务的形式提交到对应NioEventLoop的队列中进行串行执行,能够保证线程安全。 + + + + + + + +EventExecutorChooser是什么?有什么作用? + +在MultithreadEventExecutorGroup.java构造方法中,对EventExecutorChooser进行了赋值 + +```java + chooser = chooserFactory.newChooser(children); +``` + +DefaultEventExecutorChooserFactory#newChooser +```java + public EventExecutorChooser newChooser(EventExecutor[] executors) { + // 判断executors的数量是否是2次幂,2、4、8、16 + if (isPowerOfTwo(executors.length)) { + // 调用优化过的EventExecutorChooser + return new PowerOfTowEventExecutorChooser(executors); + } else { + return new GenericEventExecutorChooser(executors); + } + } +``` + +先看下未优化过的ExecutorChooser,原理就是对executors.length 进行取模,这样就可以对Executors的索引位进行循环使用。 +```java + private static final class GenericEventExecutorChooser implements EventExecutorChooser { + private final AtomicInteger idx = new AtomicInteger(); + private final EventExecutor[] executors; + + GenericEventExecutorChooser(EventExecutor[] executors) { + this.executors = executors; + } + + @Override + public EventExecutor next() { + return executors[Math.abs(idx.getAndIncrement() % executors.length)]; + } + } +``` + +优化过的ExecutorChooser,原理是通过&对executors进行取模操作。 +```java + private static final class PowerOfTowEventExecutorChooser implements EventExecutorChooser { + private final AtomicInteger idx = new AtomicInteger(); + private final EventExecutor[] executors; + + PowerOfTowEventExecutorChooser(EventExecutor[] executors) { + this.executors = executors; + } + + @Override + public EventExecutor next() { + return executors[idx.getAndIncrement() & executors.length - 1]; + } + } +``` + +&取模原理如下图: +![netty02_03png](https://coderbruis.github.io/javaDocs/img/netty/source/netty02_03.png) + +idx是当前索引位置,而加入executors数量为16,则16-1=15,二进制为 +``` +1111 +``` +idx二进制如下: +``` +111010 +``` + +由于是&操作,则相当于只取后四位,则idx & (Executors.length - 1) = 1010 + +如果此时idx为:1111,而此时进行&操作过后,结果就是:1111, + +则idx索引位再+1,则结果就是0000,这样就达到了循环使用索引位的效果。 + + +## 4. NioEventLoop的启动 + +先把关键步骤码出来,后面再过来分析。 + +AbstractBootstrap#doBind() +```java + private ChannelFuture doBind(final SocketAddress localAddress) { + // 初始化并注册通道Channel + final ChannelFuture regFuture = initAndRegister(); + final Channel channel = regFuture.channel(); + if (regFuture.cause() != null) { + return regFuture; + } + + // channelFuture完成了 + if (regFuture.isDone()) { + ChannelPromise promise = channel.newPromise(); + // 进入NioEventLoop的初始化 + doBind0(regFuture, channel, localAddress, promise); + return promise; + } else { + // Registration future is almost always fulfilled already, but just in case it's not. + final PendingRegistrationPromise promise = new PendingRegistrationPromise(channel); + regFuture.addListener(new ChannelFutureListener() { + @Override + public void operationComplete(ChannelFuture future) throws Exception { + Throwable cause = future.cause(); + if (cause != null) { + // Registration on the EventLoop failed so fail the ChannelPromise directly to not cause an + // IllegalStateException once we try to access the EventLoop of the Channel. + promise.setFailure(cause); + } else { + // Registration was successful, so set the correct executor to use. + // See https://github.com/netty/netty/issues/2586 + promise.registered(); + + doBind0(regFuture, channel, localAddress, promise); + } + } + }); + return promise; + } + } +``` + +AbstractBootstrap.java +```java + private static void doBind0( + final ChannelFuture regFuture, final Channel channel, + final SocketAddress localAddress, final ChannelPromise promise) { + + channel.eventLoop().execute(new Runnable() { + @Override + public void run() { + if (regFuture.isSuccess()) { + channel.bind(localAddress, promise).addListener(ChannelFutureListener.CLOSE_ON_FAILURE); + } else { + promise.setFailure(regFuture.cause()); + } + } + }); + } +``` + +SingleThreadEventExecutor#execute +```java + @Override + public void execute(Runnable task) { + if (task == null) { + throw new NullPointerException("task"); + } + + boolean inEventLoop = inEventLoop(); + addTask(task); + if (!inEventLoop) { + startThread(); + if (isShutdown()) { + boolean reject = false; + try { + if (removeTask(task)) { + reject = true; + } + } catch (UnsupportedOperationException e) { + // The task queue does not support removal so the best thing we can do is to just move on and + // hope we will be able to pick-up the task before its completely terminated. + // In worst case we will log on termination. + } + if (reject) { + reject(); + } + } + } + + if (!addTaskWakesUp && wakesUpForTask(task)) { + wakeup(inEventLoop); + } + } + + private void startThread() { + if (state == ST_NOT_STARTED) { + if (STATE_UPDATER.compareAndSet(this, ST_NOT_STARTED, ST_STARTED)) { + boolean success = false; + try { + doStartThread(); + success = true; + } finally { + if (!success) { + STATE_UPDATER.compareAndSet(this, ST_STARTED, ST_NOT_STARTED); + } + } + } + } + } + + private void doStartThread() { + assert thread == null; + executor.execute(new Runnable() { + @Override + public void run() { + thread = Thread.currentThread(); + if (interrupted) { + thread.interrupt(); + } + + boolean success = false; + updateLastExecutionTime(); + try { + SingleThreadEventExecutor.this.run(); + success = true; + } catch (Throwable t) { + logger.warn("Unexpected exception from an event executor: ", t); + } finally { + for (;;) { + int oldState = state; + if (oldState >= ST_SHUTTING_DOWN || STATE_UPDATER.compareAndSet( + SingleThreadEventExecutor.this, oldState, ST_SHUTTING_DOWN)) { + break; + } + } + + // Check if confirmShutdown() was called at the end of the loop. + if (success && gracefulShutdownStartTime == 0) { + if (logger.isErrorEnabled()) { + logger.error("Buggy " + EventExecutor.class.getSimpleName() + " implementation; " + + SingleThreadEventExecutor.class.getSimpleName() + ".confirmShutdown() must " + + "be called before run() implementation terminates."); + } + } + + try { + // Run all remaining tasks and shutdown hooks. + for (;;) { + if (confirmShutdown()) { + break; + } + } + } finally { + try { + cleanup(); + } finally { + // Lets remove all FastThreadLocals for the Thread as we are about to terminate and notify + // the future. The user may block on the future and once it unblocks the JVM may terminate + // and start unloading classes. + // See https://github.com/netty/netty/issues/6596. + FastThreadLocal.removeAll(); + + STATE_UPDATER.set(SingleThreadEventExecutor.this, ST_TERMINATED); + threadLock.countDown(); + if (logger.isWarnEnabled() && !taskQueue.isEmpty()) { + logger.warn("An event executor terminated with " + + "non-empty task queue (" + taskQueue.size() + ')'); + } + terminationFuture.setSuccess(null); + } + } + } + } + }); + } +``` + +就这样,调用了NioEventLoop的run方法,进行了NioEventLoop启动 +NioEventLoop#run +```java + @Override + protected void run() { + for (;;) { + try { + try { + switch (selectStrategy.calculateStrategy(selectNowSupplier, hasTasks())) { + case SelectStrategy.CONTINUE: + continue; + + case SelectStrategy.BUSY_WAIT: + // fall-through to SELECT since the busy-wait is not supported with NIO + + case SelectStrategy.SELECT: + select(wakenUp.getAndSet(false)); + + // 'wakenUp.compareAndSet(false, true)' is always evaluated + // before calling 'selector.wakeup()' to reduce the wake-up + // overhead. (Selector.wakeup() is an expensive operation.) + // + // However, there is a race condition in this approach. + // The race condition is triggered when 'wakenUp' is set to + // true too early. + // + // 'wakenUp' is set to true too early if: + // 1) Selector is waken up between 'wakenUp.set(false)' and + // 'selector.select(...)'. (BAD) + // 2) Selector is waken up between 'selector.select(...)' and + // 'if (wakenUp.get()) { ... }'. (OK) + // + // In the first case, 'wakenUp' is set to true and the + // following 'selector.select(...)' will wake up immediately. + // Until 'wakenUp' is set to false again in the next round, + // 'wakenUp.compareAndSet(false, true)' will fail, and therefore + // any attempt to wake up the Selector will fail, too, causing + // the following 'selector.select(...)' call to block + // unnecessarily. + // + // To fix this problem, we wake up the selector again if wakenUp + // is true immediately after selector.select(...). + // It is inefficient in that it wakes up the selector for both + // the first case (BAD - wake-up required) and the second case + // (OK - no wake-up required). + + if (wakenUp.get()) { + selector.wakeup(); + } + // fall through + default: + } + } catch (IOException e) { + // If we receive an IOException here its because the Selector is messed up. Let's rebuild + // the selector and retry. https://github.com/netty/netty/issues/8566 + rebuildSelector0(); + handleLoopException(e); + continue; + } + + cancelledKeys = 0; + needsToSelectAgain = false; + final int ioRatio = this.ioRatio; + if (ioRatio == 100) { + try { + processSelectedKeys(); + } finally { + // Ensure we always run tasks. + runAllTasks(); + } + } else { + final long ioStartTime = System.nanoTime(); + try { + processSelectedKeys(); + } finally { + // Ensure we always run tasks. + final long ioTime = System.nanoTime() - ioStartTime; + runAllTasks(ioTime * (100 - ioRatio) / ioRatio); + } + } + } catch (Throwable t) { + handleLoopException(t); + } + // Always handle shutdown even if the loop processing threw an exception. + try { + if (isShuttingDown()) { + closeAll(); + if (confirmShutdown()) { + return; + } + } + } catch (Throwable t) { + handleLoopException(t); + } + } + } +``` + + +## 5. NioEventLoop执行逻辑 + +未完待续... \ No newline at end of file diff --git "a/note/Netty/Netty\345\272\225\345\261\202\346\272\220\347\240\201\350\247\243\346\236\220-NioServerSocketChannel\346\216\245\345\217\227\346\225\260\346\215\256\345\216\237\347\220\206\345\210\206\346\236\220.md" "b/note/Netty/Netty\345\272\225\345\261\202\346\272\220\347\240\201\350\247\243\346\236\220-NioServerSocketChannel\346\216\245\345\217\227\346\225\260\346\215\256\345\216\237\347\220\206\345\210\206\346\236\220.md" new file mode 100644 index 0000000..05499f2 --- /dev/null +++ "b/note/Netty/Netty\345\272\225\345\261\202\346\272\220\347\240\201\350\247\243\346\236\220-NioServerSocketChannel\346\216\245\345\217\227\346\225\260\346\215\256\345\216\237\347\220\206\345\210\206\346\236\220.md" @@ -0,0 +1,264 @@ +## NioServerSocketChannel读取数据原理分析 + +NioServerSocketChannel是AbstractNioMessageChannel的子类,而NioSocketChannel是AbstractNioByteChannel的子类,并且他们都有两个公共的父类:AbstractNioChannel、AbstractChannel。 + +在Netty中Channel是用来定义对网络IO的读写操作的相关接口,与NIO的Channel接口类似。Channel的功能主要有网络IO的读写、客户端发起的连接、主动关闭连接、关闭链路、获取通信双方的网络地址等。 +一些公共的基础方法都在这个AbstractChannel抽象类中实现,几个核心的方法如:channel的注册,channel撤销注册,网络IO的读、写。但对于一些特定的功能则需要不同的实现类去实现,这样最大限度地实现了功能和接口的重用, +就如AbstractNioChannel中主要定义了doRegister()、doConnect()、newDirectBuffer()方法。 + +## 1. NioServerSocketChannel源码分析 + +NioServerSocketChannel是AbstractNioMessageChannel的子类,由于它由服务端使用,并且只负责监听Socket的接入,不关心IO的读写,所以与NioSocketChannel相比要简单得多。 + +NioServerSocketChannel封装了NIO中的ServerSocketChannel,并通过newSocket()方法打开了ServerSocketChannel + +NioServerSocketChannel.class + +```java + private static ServerSocketChannel newSocket(SelectorProvider provider) { + try { + return provider.openServerSocketChannel(); + } catch (IOException e) { + throw new ChannelException( + "Failed to open a server socket.", e); + } + } +``` + +对于NioServerSocketChannel注册至selector上的操作,是在AbstractNioChannel中实现的,源码如下: + +```java + @Override + protected void doRegister() throws Exception { + boolean selected = false; + for (;;) { + try { + selectionKey = javaChannel().register(eventLoop().unwrappedSelector(), 0, this); + return; + } catch (CancelledKeyException e) { + if (!selected) { + eventLoop().selectNow(); + selected = true; + } else { + throw e; + } + } + } + } +``` + +在ServerSocketChannel的开启,selector上的注册等前期工作完成后,NioServerSocketChannel的开始监听新连接的加入,源码如下: + +```java + @Override + protected int doReadMessages(List buf) throws Exception { + // 拿到jdk底层channel + SocketChannel ch = SocketUtils.accept(javaChannel()); + + try { + if (ch != null) { + // new出一个NioSocketChannel,将jdk SocketChannel封装成NioSocketChannel,并且这里给NioSocketChannel注册了一个SelectionKey.OP_READ事件 + buf.add(new NioSocketChannel(this, ch)); // 往buf里写入NioSocketChannel + return 1; + } + } catch (Throwable t) { + logger.warn("Failed to create a new channel from an accepted socket.", t); + + try { + ch.close(); + } catch (Throwable t2) { + logger.warn("Failed to close a socket.", t2); + } + } + + return 0; + } +``` + +上面的源码展示了Netty最终拿到新连接请求后,将jdk底层的SocketChannel封装NioSocketChannel的过程,那么selector是如何获取到accept事件后,调用到这个doReadMessages方法的呢? + +为了分析原理的延续,故事还要回到bossGroup的NioEventLoop里,当bossGroup启动,NioServerSocketChannel实例新建并注册到selector之后,Netty的bossGroup就会运行一个NioEventLoop,它的核心工作就是作为一个selector一直去监听客户端发出的accept、connect、read、write等事件。具体逻辑查看NioEventLoop#run()方法,详细的原理请回看之前的NioEventLoop的原理分析,此处只分析NioEventLoop#run()获取到链接事件到调用NioServerSocketChannel#doReadMessages()的链路。 + +1. NioEventLoop#run()一直轮训,监听这客户端发出的事件,在轮训过程中如果有任务产生,则会优先执行这些任务,调用非阻塞的selectNow(),否则调用select(deadlineNanos)阻塞指定时间去监听客户端事件。 +2. 调用NioEventLoop#processSelectedKeys(),Netty默认用的是优化过后的selectedKey,所以调用的是NioEventLoop#processSelectedKeysOptimized()方法。 +3. 在processSelectedKeysOptimized方法里会遍历selectedKeys,去拿selectedKeys中的SelectionKey,这个key就是从网络中获取到的感兴趣事件。 +4. 先通过SelectionKey获取attachment,及对应的事件channel。由于这里是获取的是accept事件,所以SelectionKey#attachment()获取到的是NioServerSocketChannel对象。 +5. 在NioEventLoop#processSelectedKey()方法中,首先拿到NioServerSocketChannel父类AbstractNioMessageChannel中的NioMessageUnsafe对象,接着根据readyOps进行判断,这里当然就是SelectionKey.OP_ACCEPT事件。 +6. 调用NioMessageUnsafe#read()方法,最终该方法调用了NioServerSocketChannel#doReadMessages(),完了之后会新建一个对SelectionKey.OP_READ事件感兴趣的NioSocketChannel对象,并存放在readBuf的一个集合中。 +7. 接着调用ChannelPipeline#fireChannelRead()方法,目的在于最终调用ServerBootstrapAcceptor#channelRead()方法,调用childGroup#register(child),把新建的NioSocketChannel对象注册到selector上。 + +这样,NioServerSocketChannel监听accept事件,接收到客户端连接后,封装客户端的“连接”到NioSocketChannel对象,并注册到selector上,后面的网络IO的读写操作都由这个NioSocketChannel对象来负责处理。 + +上述核心的6步源码如下: + +NioEventLoop.class +```java + @Override + protected void run() { + for (;;) { + try { + try { + switch (selectStrategy.calculateStrategy(selectNowSupplier, hasTasks())) { + // ... 省略 + case SelectStrategy.SELECT: + select(wakenUp.getAndSet(false)); + // ... 省略 + if (wakenUp.get()) { + selector.wakeup(); + } + // fall through + default: + } + } catch (IOException e) { + rebuildSelector0(); + handleLoopException(e); + continue; + } + // ... 省略 + + // 步骤1 + processSelectedKeys(); + runAllTasks(); + + // ... 省略 + } catch (Throwable t) { + handleLoopException(t); + // ... 省略 + } + } + } +``` + +NioEventLoop.class +```java + // 步骤2 + private void processSelectedKeysOptimized() { + for (int i = 0; i < selectedKeys.size; ++i) { + // 步骤3 + final SelectionKey k = selectedKeys.keys[i]; + selectedKeys.keys[i] = null; + + // 步骤4 + final Object a = k.attachment(); + + if (a instanceof AbstractNioChannel) { + // 步骤5 + processSelectedKey(k, (AbstractNioChannel) a); + } else { + @SuppressWarnings("unchecked") + NioTask task = (NioTask) a; + processSelectedKey(k, task); + } + + if (needsToSelectAgain) { + selectedKeys.reset(i + 1); + + selectAgain(); + i = -1; + } + } + } +``` + +NioEventLoop.class +```java + private void processSelectedKey(SelectionKey k, AbstractNioChannel ch) { + final AbstractNioChannel.NioUnsafe unsafe = ch.unsafe(); + if (!k.isValid()) { + final EventLoop eventLoop; + try { + eventLoop = ch.eventLoop(); + } catch (Throwable ignored) { + return; + } + if (eventLoop != this || eventLoop == null) { + return; + } + unsafe.close(unsafe.voidPromise()); + return; + } + + try { + int readyOps = k.readyOps(); + if ((readyOps & SelectionKey.OP_CONNECT) != 0) { + int ops = k.interestOps(); + ops &= ~SelectionKey.OP_CONNECT; + k.interestOps(ops); + + unsafe.finishConnect(); + } + + if ((readyOps & SelectionKey.OP_WRITE) != 0) { + ch.unsafe().forceFlush(); + } + + // 步骤5 + if ((readyOps & (SelectionKey.OP_READ | SelectionKey.OP_ACCEPT)) != 0 || readyOps == 0) { + unsafe.read(); + } + } catch (CancelledKeyException ignored) { + unsafe.close(unsafe.voidPromise()); + } + } +``` + +NioServerSocketChannel.class + +```java + @Override + protected int doReadMessages(List buf) throws Exception { + // 拿到jdk 的SocketChannel,代表着和客户端的一个连接socket + SocketChannel ch = SocketUtils.accept(javaChannel()); + + try { + if (ch != null) { + // 步骤6 + // 封装一个NioSocketChannel对象,并且设置感兴趣事件为:SelectionKey.OP_READ + buf.add(new NioSocketChannel(this, ch)); + return 1; + } + } catch (Throwable t) { + logger.warn("Failed to create a new channel from an accepted socket.", t); + + try { + ch.close(); + } catch (Throwable t2) { + logger.warn("Failed to close a socket.", t2); + } + } + + return 0; + } +``` + +ServerBootstrapAcceptor.class + +```java + public void channelRead(ChannelHandlerContext ctx, Object msg) { + final Channel child = (Channel) msg; + + child.pipeline().addLast(childHandler); + + setChannelOptions(child, childOptions, logger); + setAttributes(child, childAttrs); + + try { + // 步骤7 + // 在workerGroup的NioEventLoop上的selector注册了NioSocketChannel + childGroup.register(child).addListener(new ChannelFutureListener() { + @Override + public void operationComplete(ChannelFuture future) throws Exception { + if (!future.isSuccess()) { + forceClose(child, future.cause()); + } + } + }); + } catch (Throwable t) { + forceClose(child, t); + } + } +``` + +以上就是Netty中有关NioServerSocketChannel读取数据的底层原理分析。 + +下一篇分析NioSocketChannel的发送、读取数据底层原理。 \ No newline at end of file diff --git "a/note/Netty/Netty\345\272\225\345\261\202\346\272\220\347\240\201\350\247\243\346\236\220-\345\210\235\345\247\213Netty\345\217\212\345\205\266\346\236\266\346\236\204.md" "b/note/Netty/Netty\345\272\225\345\261\202\346\272\220\347\240\201\350\247\243\346\236\220-\345\210\235\345\247\213Netty\345\217\212\345\205\266\346\236\266\346\236\204.md" new file mode 100644 index 0000000..d721716 --- /dev/null +++ "b/note/Netty/Netty\345\272\225\345\261\202\346\272\220\347\240\201\350\247\243\346\236\220-\345\210\235\345\247\213Netty\345\217\212\345\205\266\346\236\266\346\236\204.md" @@ -0,0 +1,220 @@ +## 1. 回顾BIO和NIO + +```java +public class BIO { + public static void main(String[] args) throws Exception { + ServerSocket serverSocket = new ServerSocket(666); + System.out.println("Server started..."); + while (true) { + System.out.println("socket accepting..."); + Socket socket = serverSocket.accept(); + new Thread(new Runnable() { + @Override + public void run() { + try { + byte[] bytes = new byte[1024]; + InputStream inputStream = socket.getInputStream(); + while (true) { + System.out.println("reading..."); + int read = inputStream.read(bytes); + if (read != -1) { + System.out.println(new String(bytes, 0, read)); + } else { + break; + } + } + } catch (Exception e) { + e.printStackTrace(); + } finally { + try { + socket.close(); + } catch (Exception e) { + e.printStackTrace(); + } + } + } + }).start(); + } + } +} +``` + +BIO流程图如下: + +![first-netty01](https://coderbruis.github.io/javaDocs/img/netty/source/first-netty-01.png) + +BIO缺陷: + +1. BIO中,作为服务端开发,使用ServerSocket 绑定端口号之后会监听该端口,等待accept事件,accept是会阻塞当前线程; +2. 当我们收到accept事件的时候,程序就会拿到客户端与当前服务端连接的Socket,针对这个socket我们可以进行读写,但是呢,这个socket读写都是会阻塞当前线程的; +3. 一般我们会有使用多线程方式进行c/s交互,但是这样很难做到C10K(比如说:1W个客户端就需要和服务端用1W个线程支持,这样的话CPU肯定就爆炸了,同时线程上下文切换也会把机器负载给拉飞) + +NIO流程图如下: + +![first-netty02](https://coderbruis.github.io/javaDocs/img/netty/source/first-netty-02.png) + +针对BIO的以上不足,NIO提供了解决方案,即NIO提供的selector。在NIO中,提供一个selector用于监听客户端socket的连接事件,当有socket连接进来之后,就需要把检查的客户端socket注册到这个selector中,对于客户端socket来说,其线程就阻塞在了selector的select方法中,此时客户端程序该干啥干啥,不需要像BIO一样维护一个长连接去等待事件。 + +只有当客户端的selector发现socket就绪了有事件了,才会唤醒线程去处理就绪状态的socket。 + + +当然,NIO也有许多的不足,归纳为以下几点: +1. NIO 的类库和 API 繁杂,使用麻烦:需要熟练掌握 Selector、ServerSocketChannel、SocketChannel、ByteBuffer等; +2. 需要具备其他的额外技能:要熟悉 Java 多线程编程,因为 NIO 编程涉及到 Reactor 模式,你必须对多线程和网络编程非常熟悉,才能编写出高质量的 NIO 程序; +3. 开发工作量和难度都非常大:例如客户端面临断连重连、网络闪断、半包读写、失败缓存、网络拥塞和异常流的处理等等; +4. JDK NIO 的 Bug:例如臭名昭著的 Epoll Bug,它会导致 Selector 空轮询,最终导致 CPU100%。直到 JDK1.7 版本该问题仍旧存在,没有被根本解决; + +## 2. Why Netty?What is Netty? + +为什么出现Netty? +Netty 对 JDK 自带的 NIO 的 API 进行了封装,解决了上述问题。 + +1. 设计优雅:适用于各种传输类型的统一 API 阻塞和非阻塞 Socket;基于灵活且可扩展的事件模型,可以清晰地分离关注点;高度可定制的线程模型-单线程,一个或多个线程池; +2. 使用方便:详细记录的 Javadoc,用户指南和示例; +3. 高性能、吞吐量更高:延迟更低;减少资源消耗;最小化不必要的内存复制; +4. 安全:完整的 SSL/TLS 和 StartTLS 支持; +5. 社区活跃、不断更新:社区活跃,版本迭代周期短,发现的 Bug 可以被及时修复,同时,更多的新功能会被加入; + +Netty是什么? + +1. Netty是一个异步的、基于事件驱动的网络应用框架,可以用于快速开发高性能、高可靠的网络IO程序; +2. Netty 主要针对在 TCP 协议下,面向 Client 端的高并发应用,或者 Peer-to-Peer 场景下的大量数据持续传输的应用; +3. Netty 本质是一个 NIO 框架,Netty在NIO基础上进行了二次封装,所以学习Netty之前得了解NIO基础知识; +4. Netty是作为许多开源框架的底层,例如:Dubbo、RocketMQ、ElasticSearch等 + + + +## 3. Netty为什么性能这么高呢? + +小伙伴肯定会非常好奇,Netty为什么能作为这么多优秀框架的底层实现?Netty为什么这么高性能? + +Netty的高性能主要可以总结为如下几点: + +1. Netty作为异步事件驱动的网络,高性能之处主要来自于其I/O模型和线程处理模型,不同于传统BIO,客户端的连接以及事件处理都阻塞在同一个线程里,Netty则将客户端的线程和处理客户端的线程分离开来;(高效的Reactor线程模型) +2. Netty的IO线程NioEventLoop由于聚合了多路复用器Selector,可以同时并发处理成百上千个客户端连接;(IO多路复用模型) +3. Netty底层还实现了零拷贝,避免了IO过程中数据在操作系统底层来回”无效的“拷贝和系统态切换;(零拷贝) +4. 无锁串行化设计,串行设计:消息的处理尽可能在一个线程内完成,期间不进行线程切换,避免了多线程竞争和同步锁的使用;(单线程) +5. Netty 默认提供了对Google Protobuf 的支持,通过扩展Netty 的编解码接口,可以实现其它的高性能序列化框架(高性能的序列化框架) +6. Netty中大量使用了volatile,读写锁,CAS和原子类;(高效并发编程) +7. Netty的内存分配管理实现非常高效,Netty内存管理分为了池化(Pooled)和非池化(UnPooled),heap(堆内内存)和direct(堆外内存),对于Netty默认使用的是池化内存管理,其内部维护了一个内存池可以循环的创建ByteBuf(Netty底层实现的一个Buffer),提升了内存的使用效率,降低由于高负载导致的频繁GC。同时Netty底层实现了jemalloc算法(jemalloc3实现的满二叉树,读内存进行一个分隔、jemalloc4则优化了jemalloc3的算法,实现了将内存切割成了一个二维数组维护的一个数据结构,提升了内存的使用率)(Netty内存管理非常高效) + +基于以上的这么多的优点,是非常推荐阅读Netty底层源码。 + +TODO + +## 4. 线程模型基本介绍 + +1. 不同的线程模式,对程序的性能有很大影响,为了搞清 Netty 线程模式,我们来系统的讲解下各个线程模式,最后看看 Netty 线程模型有什么优越性; +2. 目前存在的线程模型有:传统阻塞 I/O 服务模型 和Reactor 模式; +3. 根据 Reactor 的数量和处理资源池线程的数量不同,有 3 种典型的实现: + - 单Reactor单线程; + - 单Reactor多线程; + - 主从Reactor多线程; +4. Netty 线程模式(Netty 主要基于主从 Reactor 多线程模型做了一定的改进,其中主从 Reactor 多线程模型有多个 Reactor) + +### 4.1 传统BIO线程模型 + +![first-netty03](https://coderbruis.github.io/javaDocs/img/netty/source/first-netty-03.png) + +模型特点 +1. 采用阻塞 IO 模式获取输入的数据; +2. 每个连接都需要独立的线程完成数据的输入,业务处理,数据返回; + +问题分析 +1. 当并发数很大,就会创建大量的线程,占用很大系统资源; +2. 连接创建后,如果当前线程暂时没有数据可读,该线程会阻塞在 Handler对象中的read 操作,导致上面的处理线程资源浪费; + +### 4.2 Reactor 模式(单Reactor单线程) + +I/O 复用结合线程池,就是 Reactor 模式基本设计思想,如图: +![first-netty04](https://coderbruis.github.io/javaDocs/img/netty/source/first-netty-04.png) + +针对传统阻塞 I/O 服务模型的 2 个缺点,解决方案: + +基于 I/O 多路复用模型:多个连接共用一个阻塞对象ServiceHandler,应用程序只需要在一个阻塞对象等待,无需阻塞等待所有连接。当某个连接有新的数据可以处理时,操作系统通知应用程序,线程从阻塞状态返回,开始进行业务处理。 + +1. 基于线程池复用线程资源:不必再为每个连接创建线程,将连接完成后的业务处理任务分配给线程进行处理,一个线程可以处理多个连接的业务。(解决了当并发数很大时,会创建大量线程,占用很大系统资源) +2. 基于 I/O 复用模型:多个客户端进行连接,先把连接请求给ServiceHandler。多个连接共用一个阻塞对象ServiceHandler。假设,当C1连接没有数据要处理时,C1客户端只需要阻塞于ServiceHandler,C1之前的处理线程便可以处理其他有数据的连接,不会造成线程资源的浪费。当C1连接再次有数据时,ServiceHandler根据线程池的空闲状态,将请求分发给空闲的线程来处理C1连接的任务。(解决了线程资源浪费的那个问题) + +由上引出单Reactor单线程模型图: +![first-netty05](https://coderbruis.github.io/javaDocs/img/netty/source/first-netty-05.png) + +方案说明 +1. select 是前面 I/O 复用模型介绍的标准网络编程 API,可以实现应用程序通过一个select方法来监听多路连接请求 +2. Reactor 对象通过 Select 监控客户端请求事件,收到事件后通过 Dispatch 进行分发 +3. 如果是建立连接请求事件,则由 Acceptor 通过 Accept 处理连接请求,然后创建一个 Handler 对象处理连接完成后的后续业务处理 +4. 如果不是建立连接事件,则 Reactor 会分发调用连接对应的 Handler 来响应 +5. Handler 会完成 Read → 业务处理 → Send 的完整业务流程 + +优缺点分析 +1. 优点:模型简单,没有多线程、进程通信、竞争的问题,全部都在一个线程中完成 +2. 缺点:性能问题,只有一个线程,无法完全发挥多核 CPU 的性能。Handler在处理某个连接上的业务时,整个进程无法处理其他连接事件,很容易导致性能瓶颈 +3. 缺点:可靠性问题,线程意外终止,或者进入死循环,会导致整个系统通信模块不可用,不能接收和处理外部消息,造成节点故障 +4. 使用场景:客户端的数量有限,业务处理非常快速,比如 Redis 在业务处理的时间复杂度 O(1) 的情况 + +### 4.3 单Reactor多线程 + +![first-netty06](https://coderbruis.github.io/javaDocs/img/netty/source/first-netty-06.png) + +方案说明 +1. Reactor 对象通过 Select 监控客户端请求事件,收到事件后,通过 Dispatch 进行分发 +2. 如果是建立连接请求,则由 Acceptor 通过 accept 处理连接请求,然后创建一个 Handler 对象处理完成连接后的各种事件 +3. 如果不是连接请求,则由 Reactor 分发调用连接对应的 handler 来处理(也就是说连接已经建立,后续客户端再来请求,那基本就是数据请求了,直接调用之前为这个连接创建好的handler来处理) +4. handler 只负责响应事件,不做具体的业务处理(这样不会使handler阻塞太久),通过 read 读取数据后,会分发给后面的 worker 线程池的某个线程处理业务。【业务处理是最费时的,所以将业务处理交给线程池去执行】 +5. worker 线程池会分配独立线程完成真正的业务,并将结果返回给 handler +6. handler 收到响应后,通过 send 将结果返回给 client + +优缺点分析 +1. 优点:可以充分的利用多核 cpu 的处理能力 +2. 缺点:多线程数据共享和访问比较复杂。Reactor 承担所有的事件的监听和响应,它是单线程运行,在高并发场景容易出现性能瓶颈。也就是说Reactor主线程承担了过多的事 + +### 4.4 主从Reactor多线程 + +针对单 Reactor 多线程模型中,Reactor 在单线程中运行,高并发场景下容易成为性能瓶颈,可以让 Reactor 在多线程中运行 +![first-netty07](https://coderbruis.github.io/javaDocs/img/netty/source/first-netty-07.png) + +SubReactor是可以有多个的,如果只有一个SubReactor的话那和单 Reactor 多线程就没什么区别了。 + +方案分析 +1. Reactor 主线程 MainReactor 对象通过 select 监听连接事件,收到事件后,通过 Acceptor 处理连接事件 +2. 当 Acceptor 处理连接事件后,MainReactor 将连接分配给 SubReactor +3. subreactor 将连接加入到连接队列进行监听,并创建 handler 进行各种事件处理 +4. 当有新事件发生时,subreactor 就会调用对应的 handler 处理 +5. handler 通过 read 读取数据,分发给后面的 worker 线程处理 +6. worker 线程池分配独立的 worker 线程进行业务处理,并返回结果 +7. handler 收到响应的结果后,再通过 send 将结果返回给 client +8. Reactor 主线程可以对应多个 Reactor 子线程,即 MainRecator 可以关联多个 SubReactor + +方案优缺点分析 +1. 优点:父线程与子线程的数据交互简单职责明确,父线程只需要接收新连接,子线程完成后续的业务处理。 +2. 优点:父线程与子线程的数据交互简单,Reactor 主线程只需要把新连接传给子线程,子线程无需返回数据。 +3. 缺点:编程复杂度较高 + + +这种主从多线程模型在许多优秀的框架中都使用到了,包括Nginx主从Reactor多线程模型,Netty主从多线程模型等。 + +对于Reactor模式,小结一下: +1. 响应快,不必为单个同步时间所阻塞,虽然 Reactor 本身依然是同步的(比如你第一个SubReactor阻塞了,我可以调下一个 SubReactor为客户端服务) +2. 可以最大程度的避免复杂的多线程及同步问题,并且避免了多线程/进程的切换开销 +3. 扩展性好,可以方便的通过增加 Reactor 实例个数来充分利用 CPU 资源 +4. 复用性好,Reactor 模型本身与具体事件处理逻辑无关,具有很高的复用性 + +### 4.5 Netty中的线程模型 + +![first-netty08](https://coderbruis.github.io/javaDocs/img/netty/source/first-netty-08.png) + +Netty线程模型流程分析 +1. Netty 抽象出两组线程池 ,BossGroup 专门负责接收客户端的连接,WorkerGroup 专门负责网络的读写 +2. BossGroup 和 WorkerGroup 类型都是 NioEventLoopGroup +3. NioEventLoopGroup 相当于一个事件循环组,这个组中含有多个事件循环,每一个事件循环是 NioEventLoop +4. NioEventLoop 表示一个不断循环的执行处理任务的线程,每个 NioEventLoop 都有一个 Selector,用于监听绑定在其上的 socket 的网络通讯 +5. NioEventLoopGroup 可以有多个线程,即可以含有多个 NioEventLoop +6. 每个 BossGroup下面的NioEventLoop 循环执行的步骤有 3 步 + - 轮询 accept 事件 + - 处理 accept 事件,与 client 建立连接,生成 NioScocketChannel,并将其注册到某个 workerGroup NIOEventLoop 上的 Selector + - 继续处理任务队列的任务,即 runAllTasks +7. 每个 WorkerGroup NIOEventLoop 循环执行的步骤 + - 轮询 read,write 事件 + - 处理 I/O 事件,即 read,write 事件,在对应 NioScocketChannel 处理 + - 处理任务队列的任务,即 runAllTasks +8. 每个 Worker NIOEventLoop 处理业务时,会使用 pipeline(管道),pipeline 中包含了 channel(通道),即通过 pipeline 可以获取到对应通道,管道中维护了很多的处理器 \ No newline at end of file diff --git "a/note/Netty/Nio/NIO\346\200\273\347\273\22301.md" "b/note/Netty/Nio/NIO\346\200\273\347\273\22301.md" new file mode 100644 index 0000000..24d0f4b --- /dev/null +++ "b/note/Netty/Nio/NIO\346\200\273\347\273\22301.md" @@ -0,0 +1,65 @@ +## 前言 + +NIO知识一直都比较容易遗忘,所以写一篇NIO的总结来方便记忆。 + +## 正文 + +======== 复习BIO ====== + +Java支持三种网络编程模型:BIO/NIO/AIO + +BIO(同步并阻塞),服务器实现模式为: +一个连接对应一个线程,即客户端有连接请求时,就需要启动一个线程进行处理。 +【一对一的关系】 + +NIO(同步非阻塞),服务器实现模式为一个线程处理多个请求(连接)。客户端发送的请求会被注册到多路复用器中,如果多路复用器轮询到这个连接是IO请求,则会对这个IO请求进行处理。 + +client -> Thread -> Selector + +NIO方式适用于连接数目多且连接比较短(轻操作)的架构【连接数多;操作时间短】,比如聊天服务器,弹幕系统,从JDK1.4开始支持。 + + + + +=========== 复习NIO ========= + +1. JavaNIO全称为Java-non-blocking IO。NIO相关类都被放在了java.nio及其子包下。 + +2. NIO中的三大核心: + - 缓冲区(Buffer) + - 选择器(Selector) + - 通道(Channel) + +NIO是面向缓冲区编程的,基于Reactor模型来实现的。 + +NIO的数据流向可以用下图来简单的表示一下: + + +Client -> Thread -> [Selector]-> Channel -> Buffer -> JVM + +一个Selector可以轮询多个Channel + +数据总是从Buffer写入Channel; +或者从Channel读取数据到Buffer中; + +可以这么理解,Buffer就是一个内存块,直接和JVM关联。 + + +而Channel提供从文件、网络读取数据的渠道。 + +NIO程序(JVM) <=> 缓冲区 <=> Channel <=> Selector <=> 文件(网络、client) + + +3. HTTP2.0 使用了多路复用技术,做到同一个连接并发处理多个请求,而且并发请求的数量比HTTP1.1大几个数量级。 + +4. NIO和BIO比较 + BIO是以流的方式来处理数据的; + NIO是以缓冲区的方式啦处理数据的; + BIO是阻塞的; + NIO是非阻塞的; + +5. Channel是会注册到Selector中的;Selector会根据不同的事件来在不同的Channel之间进行切换。 + +6. Buffer就是一个内存块,底层是数组。 + Buffer既可以读也可以写,是双向的;需要通过flip方法切换。 + channel也是双向的。 diff --git "a/note/Netty/\344\272\214\350\277\233\345\210\266.md" "b/note/Netty/\344\272\214\350\277\233\345\210\266.md" index 69bfb3b..97f7667 100644 --- "a/note/Netty/\344\272\214\350\277\233\345\210\266.md" +++ "b/note/Netty/\344\272\214\350\277\233\345\210\266.md" @@ -48,7 +48,7 @@ 对于二进制运算,记住一个口诀: 1. 与(&)运算 - + 运算规则: ``` 0&0=0, 0&1=0, 1&0=0, 1&1=1 ``` @@ -143,7 +143,7 @@ a = 10 **在开源框架底层中算法会用到大量的二进制运算,** 例如:在最近学习的Netty底层源码中,DefaultEventExecutorChooserFactory的底层源码有一个方法, 就是通过 a & (-a)来运算的。 -``` +```Java @Override public EventExecutorChooser newChooser(EventExecutor[] executors) { if (isPowerOfTwo(executors.length)) { diff --git "a/note/Netty/\351\233\266\346\213\267\350\264\235.md" "b/note/Netty/\351\233\266\346\213\267\350\264\235.md" index 6d41d8a..151082b 100644 --- "a/note/Netty/\351\233\266\346\213\267\350\264\235.md" +++ "b/note/Netty/\351\233\266\346\213\267\350\264\235.md" @@ -4,7 +4,20 @@ ## 正文 -### 1. mmap和sendFile零拷贝 +### 1. 再看IO + +在深入零拷贝机制之前,先来了解下传统BIO通信底层发生了什么,为什么会这么“消耗资源”。Linux服务器是现在绝大多数系统的首选,它的优点就不再赘述,下面的分析都基于Linux环境来进行。作为一台服务器,最常见的功能就是 +获取客户端发送过来的请求,然后再去查数据库DB获取到想要的数据,再将数据以一定的业务逻辑处理后传回给客户端,这一过程主要会调用Linux内核的以下两个函数: + +``` +read(file, tmp_buf, len); + +write(socket, tmp_buf, len); +``` + + + +### 2. mmap和sendFile零拷贝 在Java程序中,常用的零拷贝有mmap(内存映射)和sendFile。让我们回到IO底层,在Java中,一次IO操作在操作系统底层都做了哪些工作。 diff --git "a/note/Spring/\344\273\216Spring\346\272\220\347\240\201\344\270\255\345\255\246\344\271\240\342\200\224\342\200\224\347\255\226\347\225\245\346\250\241\345\274\217.md" "b/note/Spring/\344\273\216Spring\346\272\220\347\240\201\344\270\255\345\255\246\344\271\240\342\200\224\342\200\224\347\255\226\347\225\245\346\250\241\345\274\217.md" new file mode 100644 index 0000000..d26db14 --- /dev/null +++ "b/note/Spring/\344\273\216Spring\346\272\220\347\240\201\344\270\255\345\255\246\344\271\240\342\200\224\342\200\224\347\255\226\347\225\245\346\250\241\345\274\217.md" @@ -0,0 +1,297 @@ + +- [前言](#前言) +- [正文](#正文) + - [一、先看看初学者都会的多重if-else判断](#一先看看初学者都会的多重if-else判断) + - [二、策略模式实现](#二策略模式实现) + - [2.1 定义一个策略接口: Strategy.class](#21-定义一个策略接口-strategyclass) + - [2.2 创建接口的实现类](#22-创建接口的实现类) + - [2.3 创建Context类](#23-创建context类) + - [2.4 创建实现类](#24-创建实现类) + - [三、策略模式的优缺点](#三策略模式的优缺点) + - [3.1 优点](#31-优点) + - [3.2 缺点](#32-缺点) + - [3.3 策略模式中的三个角色](#33-策略模式中的三个角色) + - [四、在Spring源码中应用的策略模式](#四在spring源码中应用的策略模式) + - [4.1 Spring源码中的BeanDefinitionReader](#41-spring源码中的beandefinitionreader) + - [4.2 Spring源码中ResourceLoader](#42-spring源码中resourceloader) + - [五、总结](#五总结) + +## 前言 + +策略模式听起来高大上,吓退了很多初学小伙伴,下面就来揭开策略模式的神秘面纱,并看看在Spring源码中是如何应用到策略模式的。 + +## 正文 + +### 一、先看看初学者都会的多重if-else判断 + +```Java +public int count(int num1, int num2, String operation) { + if (operation.equals("+")) { + return num1 + num2; + } else if (operation.equals("-")) { + return num1 - num2; + } else if (operation.equals("*")) { + return num1 * num2; + } else if (operation.equals("/")) { + return num1 / num2; + } +} +``` + +问题分析: + +有没有觉得这些算法都耦合在一起了,如果需要改某个算法,而要改动整个count()方法,如果其中某个算法出错了,会影响整个count()方法。 + +### 二、策略模式实现 + +#### 2.1 定义一个策略接口: Strategy.class +```Java +public interface Strategy { + public int doOperation(int num1, int num2); +} +``` + +#### 2.2 创建接口的实现类 + +Add.java +```Java +public class Add implements Strategy{ + @Override + public int doOperation(int num1, int num2) { + return num1 + num2; + } +} +``` + +Substract.java +```Java +public class Substract implements Strategy{ + @Override + public int doOperation(int num1, int num2) { + return num1 - num2; + } +} +``` + +Multiply.java +```Java +public class Multiply implements Strategy{ + @Override + public int doOperation(int num1, int num2) { + return num1 * num2; + } +} +``` + +Divide.java +```Java +public class Divide implements Strategy{ + @Override + public int doOperation(int num1, int num2) { + return num1 / num2; + } +} +``` + +#### 2.3 创建Context类 + +```Java +public class Context { + private Strategy strategy; + + public SetStrategy(Strategy strategy){ + this.strategy = strategy; + } + + public int executeStrategy(int num1, int num2){ + return strategy.doOperation(num1, num2); + } +} +``` + +#### 2.4 创建实现类 +```Java +public class StrategyPatternDemo { + public static void main(String[] args) { + Context context = new Context(); + context.SetStrategy(new Add()); + System.out.println("10 + 5 = " + context.executeStrategy(10, 5)); + context.SetStrategy(new Substract()); + System.out.println("10 - 5 = " + context.executeStrategy(10, 5)); + context.SetStrategy(new Multiply()); + System.out.println("10 * 5 = " + context.executeStrategy(10, 5)); + context.SetStrategy(new Divide()); + System.out.println("10 / 5 = " + context.executeStrategy(10, 5)); + } +} +``` + + +### 三、策略模式的优缺点 + +#### 3.1 优点 + +- 策略模式实现类的算法或行为可以自由切换 +- 避免使用多重条件判断 +- 扩展性能好 + +#### 3.2 缺点 + +- 策略类增多 +- 所有的策略类需要向外暴露 + +#### 3.3 策略模式中的三个角色 + +1. Context封装角色:也叫上下文角色,起到封装作用,屏蔽了高层模块对策略的直接访问 +2. 策略抽象角色:定义策略实现的接口 +3. 策略实现类:实现策略接口,实现具体的策略算法或行为内容并向外界暴露 + +### 四、在Spring源码中应用的策略模式 + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20190801164743575.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0NvZGVyQnJ1aXM=,size_16,color_FFFFFF,t_70) + + +在上图中,BeanDefinition、ResourceLoader、BeanNameGenerator三个接口作为策略接口,而其他的实现类都分别实现了各自的行为用于针对不同的业务场景,那么还有一个Context封装对象,在这里就是ApplicationContext——AbstractXmlApplicationContext里。 + +#### 4.1 Spring源码中的BeanDefinitionReader + +在学习BeanDefinitionReader之前,要先了解一下什么是BeanDefinition + +接口BeanDefinition.java +```Java +public interface BeanDefinition extends AttributeAccessor, BeanMetadataElement { + String SCOPE_SINGLETON = ConfigurableBeanFactory.SCOPE_SINGLETON; + String SCOPE_PROTOTYPE = ConfigurableBeanFactory.SCOPE_PROTOTYPE; + ... + boolean isSingleton(); + boolean isPrototype(); + boolean isAbstract(); + ... +} +``` + +可以看到BeanDefinition作为一个接口,主要是用于存储从XML配置文件读取Bean信息到JVM内存的一个载体,具体是存储在了BeanDefinition的实现类——RootBeanDefinition中,下面来看看RootBeanDefinition。 + +```Java +public class RootBeanDefinition extends AbstractBeanDefinition { + @Nullable + private BeanDefinitionHolder decoratedDefinition; + + @Nullable + volatile ResolvableType targetType; + + // 用于缓存给定bean定义的解析类型确定类 + @Nullable + volatile Class resolvedTargetType; + ... +} +``` +可以看到RootBeanDefinition不是真正存储Bean信息的载体,继续查看BeanDefinitionHolder +```Java +public class BeanDefinitionHolder implements BeanMetadataElement { + + private final BeanDefinition beanDefinition; + + private final String beanName; + + @Nullable + private final String[] aliases; +} +``` +终于,看到了存储bean用的beanName和别名aliases数组了。具体其他bean信息存储在哪了?本片不在这里进行讲解,感兴趣的小伙伴可以去查看本人写的对Spring解析的博文。 + +回到正题,我们已经知道了BeanDefinition就是一个存储XML配置文件中bean信息的一个载体,那么这个过程是如何实现的呢?答案就在BeanDefinitionReader的实现类————XmlBeanDefinitionReader里面。 + +XmlBeanDefinitionReader就是一个策略的具体实现,表示的是一种可以从Xml中获取Bean配置信息的策略,除了这种策略外,还有PropertiesBeanDefinitionReader这种从Properties配置文件获取Bean配置信息的策略。第三节中总结提到在策略模式中有三种角色,1)Context封装角色;2)策略抽象角色;3)策略实现角色。在这里我们已经找到了策略抽象角色——BeanDefinitionReader和策略实现角色——XmlBeanDefinitionReader和PropertiesBeanDefinitionReader,就差Context封装角色了,那么Spring中哪个类充当了这个角色呢? + +答案就是——AbstractXmlApplicationContext类 + +无图无真相,请看下图: +![在这里插入图片描述](https://img-blog.csdnimg.cn/20190801164812944.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0NvZGVyQnJ1aXM=,size_16,color_FFFFFF,t_70) + +可以看到,策略实现类XmlBeanDefinitionReader在AbstractXmlApplicationContext中执行了具体的策略执行,也就是后面复杂的从Xml配置文件读取bean配置信息的操作。 + +#### 4.2 Spring源码中ResourceLoader + +下面先看看ResourceLoader的源码,然后再来简单介绍下其作用 +``` +public interface ResourceLoader { + + String CLASSPATH_URL_PREFIX = ResourceUtils.CLASSPATH_URL_PREFIX; + + Resource getResource(String location); + + ClassLoader getClassLoader(); +} +``` + +这里的ResourceLoader就是一个Resource加载器,而Resource是将URL、URI等资源抽象为一个Resource资源对象,方便Spring进一步操作。 + +下面先来分析下三种角色: +1. AbstractBeanDefinitionReader 作为Context封装角色 +2. ResourceLoader作为策略的抽象 +3. DefaultResourceLoader和ResourcePatternResolver就是具体的执行策略 + + +上源码 +![\[外链图片转存失败(img-3ha67xsg-1564649270087)(A2B888FAB6364C2BB9E772E86AD70D69)\]](https://img-blog.csdnimg.cn/20190801164907970.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0NvZGVyQnJ1aXM=,size_16,color_FFFFFF,t_70) + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20190801165001118.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0NvZGVyQnJ1aXM=,size_16,color_FFFFFF,t_70) + +这里ResourceLoader的实现类是ClassPathXmlApplicationContext,其实ClassPathXMLApplicationContext是DefaultResourceLoader的子类。如下图: +![在这里插入图片描述](https://img-blog.csdnimg.cn/20190801165041615.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0NvZGVyQnJ1aXM=,size_16,color_FFFFFF,t_70) + +由于```resourceLoader instanceof ResourcePatternResolver为true```,所以走如下逻辑: + +AbstractBeanDefinitionReader.java +```Java +Resource[] resources = ((ResourcePatternResolver) resourceLoader).getResources(location); +int count = loadBeanDefinitions(resources); +``` + +AbstractApplicationContext.java +```Java + @Override + public Resource[] getResources(String locationPattern) throws IOException { + return this.resourcePatternResolver.getResources(locationPattern); + } +``` + +PathMatchingResourcePatternResolver.java +```Java + @Override + public Resource[] getResources(String locationPattern) throws IOException { + + if (locationPattern.startsWith(CLASSPATH_ALL_URL_PREFIX)) { + + if (getPathMatcher().isPattern(locationPattern.substring(CLASSPATH_ALL_URL_PREFIX.length()))) { + + return findPathMatchingResources(locationPattern); + } + else { + + return findAllClassPathResources(locationPattern.substring(CLASSPATH_ALL_URL_PREFIX.length())); + } + } + else { + + int prefixEnd = (locationPattern.startsWith("war:") ? locationPattern.indexOf("*/") + 1 : + locationPattern.indexOf(':') + 1); + if (getPathMatcher().isPattern(locationPattern.substring(prefixEnd))) { + + return findPathMatchingResources(locationPattern); + } + else { + + return new Resource[] {getResourceLoader().getResource(locationPattern)}; + } + } + } +``` + +由于Spring IOC源码篇幅过长,源码部分就暂时介绍到这里。 + +### 五、总结 + +本篇文章通过几个例子介绍了下策略模式,并且深入Spring源码中查看了策略模式的应用场景,然而对于策略模式的精髓学习任然需要持续深入,不然学习到的都只是皮毛而已。 + diff --git "a/note/Spring/\346\267\261\345\205\245Spring\346\272\220\347\240\201\347\263\273\345\210\227\357\274\210\344\270\200\357\274\211\342\200\224\342\200\224\345\234\250IDEA\344\270\255\346\236\204\345\273\272Spring\346\272\220\347\240\201.md" "b/note/Spring/\346\267\261\345\205\245Spring\346\272\220\347\240\201\347\263\273\345\210\227\357\274\210\344\270\200\357\274\211\342\200\224\342\200\224\345\234\250IDEA\344\270\255\346\236\204\345\273\272Spring\346\272\220\347\240\201.md" new file mode 100644 index 0000000..aadf1ba --- /dev/null +++ "b/note/Spring/\346\267\261\345\205\245Spring\346\272\220\347\240\201\347\263\273\345\210\227\357\274\210\344\270\200\357\274\211\342\200\224\342\200\224\345\234\250IDEA\344\270\255\346\236\204\345\273\272Spring\346\272\220\347\240\201.md" @@ -0,0 +1,68 @@ + +- [前言——万里长征的第一步](#前言万里长征的第一步) +- [准备工作](#准备工作) + - [Spring源码包下载](#spring源码包下载) + - [gradle工具下载](#gradle工具下载) + - [编译Spring源码](#编译spring源码) + - [将Spring源码导入IDEA中](#将spring源码导入idea中) + + +## 前言——万里长征的第一步 +要学习Spring源码,导入Spring源码到IDE是必不可少的一步,因为Spring源码各个包、各个类之间的各种关联关系非常复杂。如果仅仅是通过Spring源码文档来看,相信没多少人能坚持学下去。因此将Spring源码包导入IDE是非常必要的。本人使用IDEA较多,所以也就将Spring源码导入至IDEA中。 + +## 准备工作 + +### Spring源码包下载 +在本地磁盘下载Spring源码包,笔者在写这篇文章时下载的为Spring5源码。下载地址:https://github.com/spring-projects/spring-framework + +然后选在本地磁盘目录下,使用git命令下载Spring源码(git工具怎么安装,度娘吧)。 +``` +git clone https://github.com/spring-projects/spring-framework +``` +待源码下载完后,本地磁盘就会生成Spring源码。 + +### gradle工具下载 +gradle是一个基于Groovy的构建工具,它使用Groovy来编写构建脚本,支持依赖管理和多项目创建,类似Maven但又比Maven更加简单便捷。 + +gradle下载地址:http://downloads.gradle.org/distributions/gradle-4.6-bin.zip + +下载好gradle之后需要配置gradle的环境变量。 + +在系统属性——>环境变量——>**系统变量**创建两个变量 +``` +变量:GRADLE_HOME +值:X:\gradle-4.6 +``` +``` +变量:GRADLE_USER_HOME +值:%GRADLE_HONE%\.gradle +``` +``` +变量:path +值:;%GRADLE_HOME%/bin +``` + +配置好gradle之后,在cmd窗口下使用gradle -v命令查看是否安装成功。 +![在这里插入图片描述](https://img-blog.csdnimg.cn/2019010511163772.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0NvZGVyQnJ1aXM=,size_16,color_FFFFFF,t_70) + +如上图显示gradle版本,即显示安装成功。 + +## 编译Spring源码 +在Spring源码目录下,打开cmd窗口(windows系统),运行以下命令: +``` +gradlew.bat cleanIdea :spring-oxm:compileTestJava +``` +执行完命令,如下图所示: +![在这里插入图片描述](https://img-blog.csdnimg.cn/20190105111656717.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0NvZGVyQnJ1aXM=,size_16,color_FFFFFF,t_70) + +## 将Spring源码导入IDEA中 +(1)打开IDEA,选择File->New->Project From Existing Sources… + +(2)选中Spring-framework文件夹,OK->Import project from external model + +(3)选中Gradle,点击Next,然后点击Finish,等待IDEA导入以及下载相关联的包即可。 + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20190105111708740.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0NvZGVyQnJ1aXM=,size_16,color_FFFFFF,t_70) +![在这里插入图片描述](https://img-blog.csdnimg.cn/20190105111718283.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0NvZGVyQnJ1aXM=,size_16,color_FFFFFF,t_70) + +这样,Spring源码就导入成功了。 \ No newline at end of file diff --git "a/note/Spring/\346\267\261\345\205\245Spring\346\272\220\347\240\201\347\263\273\345\210\227\357\274\210\344\272\214\357\274\211\342\200\224\342\200\224\346\267\261\345\205\245Spring\345\256\271\345\231\250\357\274\214\351\200\232\350\277\207\346\272\220\347\240\201\351\230\205\350\257\273\345\222\214\346\227\266\345\272\217\345\233\276\346\235\245\345\275\273\345\272\225\345\274\204\346\207\202Spring\345\256\271\345\231\250\357\274\210\344\270\212\357\274\211.md" "b/note/Spring/\346\267\261\345\205\245Spring\346\272\220\347\240\201\347\263\273\345\210\227\357\274\210\344\272\214\357\274\211\342\200\224\342\200\224\346\267\261\345\205\245Spring\345\256\271\345\231\250\357\274\214\351\200\232\350\277\207\346\272\220\347\240\201\351\230\205\350\257\273\345\222\214\346\227\266\345\272\217\345\233\276\346\235\245\345\275\273\345\272\225\345\274\204\346\207\202Spring\345\256\271\345\231\250\357\274\210\344\270\212\357\274\211.md" new file mode 100644 index 0000000..248f495 --- /dev/null +++ "b/note/Spring/\346\267\261\345\205\245Spring\346\272\220\347\240\201\347\263\273\345\210\227\357\274\210\344\272\214\357\274\211\342\200\224\342\200\224\346\267\261\345\205\245Spring\345\256\271\345\231\250\357\274\214\351\200\232\350\277\207\346\272\220\347\240\201\351\230\205\350\257\273\345\222\214\346\227\266\345\272\217\345\233\276\346\235\245\345\275\273\345\272\225\345\274\204\346\207\202Spring\345\256\271\345\231\250\357\274\210\344\270\212\357\274\211.md" @@ -0,0 +1,1262 @@ + +- [前言](#前言) +- [进入正题](#进入正题) + - [DefaultListableBeanFactory](#defaultlistablebeanfactory) + - [XmlBeanDefinitionReader](#xmlbeandefinitionreader) + - [通过xml配置文件来初始化容器](#通过xml配置文件来初始化容器) + - [提问环节](#提问环节) + - [深入理解Spring容器如何创建bean和初始化bean](#深入理解spring容器如何创建bean和初始化bean) + - [在web容器中初始化spring容器](#在web容器中初始化spring容器) +- [参考](#参考) + +## 前言 +**Spring版本: Spring5-0-3** + +Spring容器就相当于一个大的水桶,里面装着很多水——bean对象。bean对象就是一个普通的pojo对象。这里有一个很重要的概念,就是IOC——Invertion of Control,即反转控制。通俗点将就是将创建并且绑定数据bean的权利赋予给了Spring容器(或Spring IOC容器,下文Spring容器代指Spring IOC容器),在bean生成或初始化的时候,Spring容器就会将数据注入到bean中,又或者通过将对象的引用注入到对象数据域中的方式来注入对方法调用的依赖。 + +## 进入正题 + +在Spring容器的设计中,有两个主要的容器系列,一个是实现BeanFactory接口的简单容器系列,这个接口实现了容器最基本的功能;另一个是ApplicationContext应用上下文,作为容器的高级形态而存在,它用于扩展BeanFactory中现有的功能。ApplicationContext和BeanFactory两者都是用于加载Bean的,但是相比之下,ApplicationContext提供了更多的扩展功能,简单一点说:ApplicationContext包含BeanFactory的所有功能。绝大多数“典型”的企业应用和系统,ApplicationContext就是你需要使用的。下面展示一下分别使用BeanFactory和ApplicationContext读取xml配置文件的方式: +```Java +BeanFactory bf = new XmlBeanFactory(new ClassPathResource("applicationContext.xml")); +``` +```Java +ApplicationContext bf = new ClassPathXmlApplicationContext("applicationContext.xml"); +``` +下面先介绍Spring最核心的两个类。 + +### DefaultListableBeanFactory +DefaultListableBeanFactory是整个bean加载的核心部分,是Spring注册及加载bean的默认实现。下面看看DefaultListableBeanFactory的层次结构图。 + +![spring-01](https://github.com/coderbruis/JavaSourceCodeLearning/blob/master/note/images/spring/spring-01.png) +从上往下开始介绍各个类以及接口的作用: +- AliasRegistry(接口):alias指的是bean的别名,而aliasRegistry定义了对alias的增删改查等操作。 +- SimpleAliasRegistry(类):主要使用map作为alias的缓存,并对接口AliasRegistry进行实现。 +- SingletonBeanRegistry(接口):定义对单例的注册及获取。 +- BeanFactory(接口):定义获取bean及bean的各种属性。 +- DefaultSingleTonBeanRegistry(接口):实现了SingletonBeanRegistry的方法,同时继承了SimpleAliasRegistry。 +- HierarchicalBeanFactory(接口):继承BeanFactory,也就是在BeanFactory定义功能的基础上增加了对parantFactory的支持。 +- BeanDefinitionRegistry(接口):定义对BeanDefinition的增删改查功能,BeanDefinition就是描述一个bean的实例,包含了属性值(scope、bean的name、lazy加载模式等),构造参数以及其他更多的实现信息。 +- FactoryBeanRegistrySupport(类):在DefaultSingleTonBeanRegistry基础上增加了对FactoryBean的特殊功能。 +- ConfigurableBeanFactory(接口):提供配置Factory的各种方法,包括设置父工厂、bean的加载器、添加BeanPostProcessor等功能。 +- ListableBeanFactory(接口):根据各种条件获取bean的配置清单,可以根据bean的name、type等条件获得bean配置清单。 +- AbstractBeanFactory(类):综合FactoryBeanRegistrySupport和ConfigurableBeanFactory的功能。 +- AutowireCapableBeanFactory(接口):提供创建Bean、自动注入、初始化以及应用bean的后置处理器。 +- AbstractAutowireCapableBeanFactory(类):综合AbstractBeanFactory并对接口AutowireCapableBeanFactory进行实现 +- ConfigurableListableBeanFactory:BeanFactory配置清单,指定忽略类型及接口等。 +- DefaultListableBeanFactory:综合上面所有功能,主要对Bean注册后的处理。 + +### XmlBeanDefinitionReader +XML配置文件的读取是Spring中最重要的功能,因为Spring的大部分功能都是以配置作为切入点的,XmlBeanDefinitionReader实现了对资源文件的读取、解析以及注册。先看一下XmlBeanDefinitionReader的层次结构图。 +![spring-02](https://github.com/coderbruis/JavaSourceCodeLearning/blob/master/note/images/spring/spring-02.png) + +- EnvironmentCapable(接口):定义获取Environment方法,Environment代表了配置文件。 +- BeanDefinitionReader(接口):主要定义资源文件读取并转换为BeanDefinition的各个功能。 +- AbstractBeanDefinitionReader(类):对EnvironmentCapable、BeanDefinitionReader类定义的功能进行实现。 +- ResourceLoader(接口):定义资源加载器,主要应用于根据给定的资源文件地址返回对应的Resource。 +- BeanDefinitionReader:主要定义资源文件读取并转换为BeanDefinition的各个功能。 + +学习完Spring最核心的两个类之后,让我们进入coding时间。 + +## 通过xml配置文件来初始化容器 + +下面演示一个使用ApplicationContext接口获取xml配置,从而实现一个helloword级别的spring程序: + +applicationContext.xml +```Java + + + + + + + + + +``` +测试类 +```Java +public class SpringMain { + public static void main(String[] args) { + //使用spring容器 + ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml"); + Person person = (Person)context.getBean("person"); + System.out.println(person); + } +} + +运行结果: +Person{name='Bruis', age=23} +``` +运行完程序,读者有没有想过,配置文件是怎么被加载到Spring容器,然后初始化Bean,最终打印到控制台的?我想大部分读者都会很好奇并想去一探究竟,但是碍于Spring源码难以阅读,debug跳来跳去眼花缭乱。下面就跟着笔者一起深入Spring源码,看看Spring容器是如何从xml配置文件中获取配置信息并且打印出bean的信息。 + + + +** 前方高能 ** + +![spring-03](https://github.com/coderbruis/JavaSourceCodeLearning/blob/master/note/images/spring/spring-03.jpg) + +通过在断点debug,跟踪程序运行。 + +1. SpringMain.class +```Java +ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml"); +``` +2. ClassPathXmlApplicationContext.class +```Java +public ClassPathXmlApplicationContext(String configLocation) throws BeansException { + this(new String[]{configLocation}, true, (ApplicationContext)null); +} +/* +使用给定的父类容器创建新的ClassPathXmlApplicationContext,然后从给定的XML文件加载定义,加载所有bean定义并且创建所有的单例,在进一步配置上下文后调用refresh。换句话说xml文件的读取,bean的创建和实例化都是在refresh()方法中进行的,refresh()函数中包含了几乎所有的ApplicationContext中提供的全部功能。 +*/ +public ClassPathXmlApplicationContext(String[] configLocations, boolean refresh, ApplicationContext parent) throws BeansException { + super(parent); + //设置配置路径 + this.setConfigLocations(configLocations); + if (refresh) { + //refresh Spring容器 + this.refresh(); + } +} +``` +3. AbstractRefreshableConfigApplicationContext.class +```Java +//给configLocations字符串数组设置值,支持多个配置文件已数组方式同时传入。 +public void setConfigLocations(String... locations) { + if (locations != null) { + Assert.noNullElements(locations, "Config locations must not benull"); + this.configLocations = new String[locations.length]; + for(int i = 0; i < locations.length; ++i) { + this.configLocations[i] = this.resolvePath(locations[i]).trim(); + } + } else { + this.configLocations = null; + } +} +``` + +### 提问环节 + +- Spring容器的生命周期分为几个阶段?Spring容器的初始化发生在什么时候,发生了什么?Spring容器的销毁过程又发生了什么? +- Spring容器什么时候读取xml配置文件?并且把配置信息读取?Spring容器用什么数据结构存储用于创建bean的K/V信息? +- Spring容器获取了用于创建bean的K/V信息后,在什么时候去创建并且初始化bean? + +下面我们来重点看看refresh()过程。 + +![spring-04](https://github.com/coderbruis/JavaSourceCodeLearning/blob/master/note/images/spring/spring-04.jpg) + +1. AbstractApplicationContext.class +```Java +/* + 简单来说,Spring容器的初始化时右refresh()方法来启动的,这个方法标志着IOC容器的正式启动。具体来说,这里的启动包括了BeanDefinition和Resource的定位、载入和注册三个基本过程。 +*/ +@Override +public void refresh() throws BeansException, IllegalStateException { + synchronized (this.startupShutdownMonitor) { + //准备刷新容器(上下文环境) + prepareRefresh(); + //通知子类刷新内部bean工厂,初始化BeanFactory并进行XML文件的解析、读取 + ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory(); + //准备bean工厂在此上下文中使用,并对BeanFactory进行各种功能填充 + prepareBeanFactory(beanFactory); + try { + // 允许在上下文子类中对bean工厂进行后处理 + postProcessBeanFactory(beanFactory); + // 在上下文中调用注册并激活BeanFactory的处理器,就是这个方法中的处理器注册了bean + invokeBeanFactoryPostProcessors(beanFactory); + // 注册拦截bean创建的bean处理器,这里只是注册,真正的调用Bean是在getBean方法的时候 + registerBeanPostProcessors(beanFactory); + // 初始化此上下文的消息源,及不同语言的消息体,例如国际化处理 + initMessageSource(); + // 初始化此上下文的事件多播器 + initApplicationEventMulticaster(); + // 在特定上下文子类中初始化其他特殊bean. + onRefresh(); + // 检查监听器bean并注册它们 + registerListeners(); + // 实例化所有剩余(非惰性的)单例 + finishBeanFactoryInitialization(beanFactory); + // 完成刷新过程,通知生命周期处理器lifecycleProcessor刷新过程,同时发出ContextRefreshEvent通知别人 + finishRefresh(); + } + catch (BeansException ex) { + ... + // 摧毁已经创建的单例以避免资源浪费 + destroyBeans(); + // 重置'有效'标志 + cancelRefresh(ex); + // Propagate exception to caller. + throw ex; + } + finally { + resetCommonCaches(); + } + } +} +``` +2. AbstractRefreshableApplicationContext.class +```Java +/* + 通知子类刷新内部bean工厂,初始化BeanFactory并进行XML文件的解析、读取。obtain就是指获得的含义,这个方法obtaiinFreshBeanFactory正是实现BeanFactory的地方,也就是经过这个方法,ApplicationContext就已经拥有了BeanFactory的全部功能(也就是BeanFactory包含在了Spring容器里了)。 +*/ +protected ConfigurableListableBeanFactory obtainFreshBeanFactory() { + this.refreshBeanFactory(); + ... + return beanFactory; +} +/* +此实现执行此上下文的基础bean工厂的实际刷新,关闭先前的bean工厂(如果有)并初始化上一个生命周期的下一阶段的新bean工厂。 + +经过debug,最后从refreshBeanFactory()方法返回后,this也就是AbstractRefreshableApplicationContext实例对象 + +*/ +@Override +protected final void refreshBeanFactory() throws BeansException { + //如果有bean工厂,则关闭该工厂 + if (hasBeanFactory()) { + destroyBeans(); + closeBeanFactory(); + } + try { + //创建一个新bean工厂,这里的DefaultListableBeanFactory就是前面笔者将的Spring核心类,这个类真的很重要! + DefaultListableBeanFactory beanFactory = createBeanFactory(); + //为了序列化指定ID,如果需要的话,让这个BeanFactory从ID反序列化掉BeanFactory对象 + beanFactory.setSerializationId(getId()); + //定制beanFactory,设置相关属性,包括是否允许覆盖同名称的不同定义的对象以及循环依赖以及设置@Autowired和@Qualifier注解解析器QualifierAnnotationAutowireCandidateResolver + customizeBeanFactory(beanFactory); + //加载bean定义信息,这一步实际上就从XML配置文件里的bean信息给读取到了Factory里了。 + loadBeanDefinitions(beanFactory); + synchronized (this.beanFactoryMonitor) { + this.beanFactory = beanFactory; + } + } + catch (IOException ex) { + ... + } +} +``` +这里先看看上面代码的loadBeanDefinitions()方法运行完后的结果 +![spring-05](https://github.com/coderbruis/JavaSourceCodeLearning/blob/master/note/images/spring/spring-05.png) +![spring-06](https://github.com/coderbruis/JavaSourceCodeLearning/blob/master/note/images/spring/spring-06.png) +从图中可以知道,loadBeanDefinitions()方法运行完后,在beanFactory变量里面存放着一个ConcurrentHashMap变量,用于存放着person这个KV键值对,Key为person,Value为一个ArrayList的变量,里面存放着person的两个属性:age、name。 + +那么,person的属性是怎么被封装到beanFactory里面的呢?请看下面的源码解析。 + +3. AbstractXmlApplicationContext.class +```Java +protected void loadBeanDefinitions(DefaultListableBeanFactory beanFactory) throws BeansException, IOException { + //为给定的BeanFactory创建一个新的XmlBeanDefinitionReader + XmlBeanDefinitionReader beanDefinitionReader = new XmlBeanDefinitionReader(beanFactory); + beanDefinitionReader.setEnvironment(this.getEnvironment()); + beanDefinitionReader.setResourceLoader(this); + beanDefinitionReader.setEntityResolver(new ResourceEntityResolver(this)); + //允许子类提供reader的自定义初始化 + this.initBeanDefinitionReader(beanDefinitionReader); + //真正加载bean定义信息 + this.loadBeanDefinitions(beanDefinitionReader); +} +protected void loadBeanDefinitions(XmlBeanDefinitionReader reader) throws BeansException, IOException { + Resource[] configResources = getConfigResources(); + if (configResources != null) { + reader.loadBeanDefinitions(configResources); + } + String[] configLocations = getConfigLocations(); + if (configLocations != null) { + reader.loadBeanDefinitions(configLocations); + } +} +``` +首先在refreshBeanFactory()方法中已经初始化了DefaultListableBeanFactory,对于读取XML配置文件,还需要使用XmlBeanDefinitionReader。所以在上述loadBeanDefinitions()中就需要初始化XmlBeanDefinitionReader。在DefaultListableBeanFactory和XmlBeanDefinitionReader后就可以进行配置文件的读取了。要注意的地方时,在XmlBeanDefinitionReader初始化时就已经把DefaultListableBeanFactory给注册进去了,所以在XmlBeanDefinitionReader读取的BeanDefinition都会注册到DefaultListableBeanFactory中,也就是经过上述的loadingBeanDefinitions(),类型DefaultListableBeanFactory的变量beanFactory就已经包含了所有**解析好的配置**了。 + +4. AbstractBeanDefinitionReader.class +```Java +@Override +public int loadBeanDefinitions(String... locations) throws BeanDefinitionStoreException { + Assert.notNull(locations, "Location array must not be null"); + int count = 0; + for (String location : locations) { + count += loadBeanDefinitions(location); + } + return count; +} + +/* +从指定的资源位置加载bean定义,他的位置也可以是位置模式,前提是此bean定义读取器的ResourceLoader是ResourcePatternResolver。 +*/ +public int loadBeanDefinitions(String location, @Nullable Set actualResources) throws BeanDefinitionStoreException { + ResourceLoader resourceLoader = getResourceLoader(); + if (resourceLoader == null) { + ... + } + + if (resourceLoader instanceof ResourcePatternResolver) { + // 资源模式匹配可用 + try { + Resource[] resources = ((ResourcePatternResolver) resourceLoader).getResources(location); + int count = loadBeanDefinitions(resources); + if (actualResources != null) { + Collections.addAll(actualResources, resources); + } + if (logger.isTraceEnabled()) { + logger.trace("Loaded " + count + " bean definitions from location pattern [" + location + "]"); + } + return count; + } + catch (IOException ex) { + ... + } + } + else { + // 只能通过绝对URL加载单个资源 + Resource resource = resourceLoader.getResource(location); + int count = loadBeanDefinitions(resource); + if (actualResources != null) { + actualResources.add(resource); + } + ... + return count; +} +``` +5. PathMatchingResourcePatternResolver.class +```Java +@Override +public Resource[] getResources(String locationPattern) throws IOException { + Assert.notNull(locationPattern, "Location pattern must not be null"); + if (locationPattern.startsWith(CLASSPATH_ALL_URL_PREFIX)) { + // 类路径资源(可能有多个同名资源) + if (getPathMatcher().isPattern(locationPattern.substring(CLASSPATH_ALL_URL_PREFIX.length()))) { + // 类路径资源模式 + return findPathMatchingResources(locationPattern); + } + else { + // 具有给定名称的所有类路径资源 + return findAllClassPathResources(locationPattern.substring(CLASSPATH_ALL_URL_PREFIX.length())); + } + } + else { + // 通常只在前缀后面查找一个模式,并且在Tomcat之后只有“* /”分隔符之后的“war:”协议。 + int prefixEnd = (locationPattern.startsWith("war:") ? locationPattern.indexOf("*/") + 1 : + locationPattern.indexOf(':') + 1); + if (getPathMatcher().isPattern(locationPattern.substring(prefixEnd))) { + // 文件模式 + return findPathMatchingResources(locationPattern); + } + else { + // 具有给定名称的单个资源 + return new Resource[] {getResourceLoader().getResource(locationPattern)}; + } + } +} +``` +6. XmlBeanDefinitionReader.class +```Java +/* + 从XML配置文件中获取bean定义信息 +*/ +public int loadBeanDefinitions(EncodedResource encodedResource) throws BeanDefinitionStoreException { + Assert.notNull(encodedResource, "EncodedResource must not be null"); + ... + Set currentResources = this.resourcesCurrentlyBeingLoaded.get(); + if (currentResources == null) { + currentResources = new HashSet<>(4); + this.resourcesCurrentlyBeingLoaded.set(currentResources); + } + ... + try { + InputStream inputStream = encodedResource.getResource().getInputStream(); + try { + InputSource inputSource = new InputSource(inputStream); + if (encodedResource.getEncoding() != null) { + inputSource.setEncoding(encodedResource.getEncoding()); + } + //获取到读取xml配置文件的InputStream流后,进行BeanDefinitions的加载 + return doLoadBeanDefinitions(inputSource, encodedResource.getResource()); + } + finally { + inputStream.close(); + } + } + catch (IOException ex) { + ... + } + finally { + currentResources.remove(encodedResource); + if (currentResources.isEmpty()) { + this.resourcesCurrentlyBeingLoaded.remove(); + } + } +} + +/* + 真正从xml配置文件中加载Bean定义信息 +*/ +protected int doLoadBeanDefinitions(InputSource inputSource, Resource resource) + throws BeanDefinitionStoreException { + + try { + //获取xml的配置信息并封装为Document对象 + Document doc = doLoadDocument(inputSource, resource); + return this.registerBeanDefinitions(doc, resource); + } + catch (BeanDefinitionStoreException ex) { + ... + } +} +``` + +下面,继续深入registerBeanDefinitions方法。 + +![spring-07](https://github.com/coderbruis/JavaSourceCodeLearning/blob/master/note/images/spring/spring-07.jpg) + +1. XmlBeanDefinitionReader.class +```Java +/* + 注册给定DOM文档中包含的bean定义 +*/ +public int registerBeanDefinitions(Document doc, Resource resource) throws BeanDefinitionStoreException { + BeanDefinitionDocumentReader documentReader = this.createBeanDefinitionDocumentReader(); + int countBefore = this.getRegistry().getBeanDefinitionCount(); + documentReader.registerBeanDefinitions(doc, this.createReaderContext(resource)); + return this.getRegistry().getBeanDefinitionCount() - countBefore; +} +``` +2. DefaultBeanDefinitionDocumentReader.class +```Java +/* + 此实现根据“spring-beans”XSD解析bean定义 +*/ +public void registerBeanDefinitions(Document doc, XmlReaderContext readerContext) { + this.readerContext = readerContext; + this.logger.debug("Loading bean definitions"); + Element root = doc.getDocumentElement(); + this.doRegisterBeanDefinitions(root); +} + +/* + 任何嵌套的元素都将导致此方法的递归。为了正确传播和保留beans的default属性,需要跟踪当前(父)委托,该委托可以为null。创建新的(子)委托时,需要引用父项以进行回退,然后最终将this.delegate重置为其原始(父)引用。 +*/ +protected void doRegisterBeanDefinitions(Element root) { + BeanDefinitionParserDelegate parent = this.delegate; + this.delegate = this.createDelegate(this.getReaderContext(), root, parent); + if (this.delegate.isDefaultNamespace(root)) { + String profileSpec = root.getAttribute("profile"); + if (StringUtils.hasText(profileSpec)) { + String[] specifiedProfiles = StringUtils.tokenizeToStringArray(profileSpec, ",; "); + if (!this.getReaderContext().getEnvironment().acceptsProfiles(specifiedProfiles)) { + if (this.logger.isInfoEnabled()) { + this.logger.info("Skipped XML bean definition file due to specified profiles [" + profileSpec + "] not matching: " + this.getReaderContext().getResource()); + } + + return; + } + } + } + + this.preProcessXml(root); + //解析beanDefinitions信息,经过这个方法,beanFactory中就会保存从xml配置文件中解析而来的信息 + this.parseBeanDefinitions(root, this.delegate); + this.postProcessXml(root); + this.delegate = parent; +} + +/* + 解析文档中根级别的元素:import、alias、bean +*/ +protected void parseBeanDefinitions(Element root, BeanDefinitionParserDelegate delegate) { + if (delegate.isDefaultNamespace(root)) { + NodeList nl = root.getChildNodes(); + for(int i = 0; i < nl.getLength(); ++i) { + Node node = nl.item(i); + if (node instanceof Element) { + Element ele = (Element)node; + if (delegate.isDefaultNamespace(ele)) { + //解析默认元素 + this.parseDefaultElement(ele, delegate); + } else { + delegate.parseCustomElement(ele); + } + } + } + } else { + delegate.parseCustomElement(root); + } + +} + +private void parseDefaultElement(Element ele, BeanDefinitionParserDelegate delegate) { + if (delegate.nodeNameEquals(ele, "import")) { + this.importBeanDefinitionResource(ele); + } else if (delegate.nodeNameEquals(ele, "alias")) { + this.processAliasRegistration(ele); + } else if (delegate.nodeNameEquals(ele, "bean")) { + //读取到xml配置文件的节点 + this.processBeanDefinition(ele, delegate); + } else if (delegate.nodeNameEquals(ele, "beans")) { + this.doRegisterBeanDefinitions(ele); + } +} + +protected void processBeanDefinition(Element ele, BeanDefinitionParserDelegate delegate) { + BeanDefinitionHolder bdHolder = delegate.parseBeanDefinitionElement(ele); + ... +} + +``` + +2. BeanDefinitionParserDelegate.class +```Java +/* + 解析bean定义本身,而不考虑名称或别名,如果解析期间出错则返回null。 +*/ +@Nullable +public BeanDefinitionHolder parseBeanDefinitionElement(Element ele, @Nullable BeanDefinition containingBean) { + String id = ele.getAttribute("id"); + String nameAttr = ele.getAttribute("name"); + List aliases = new ArrayList(); + if (StringUtils.hasLength(nameAttr)) { + String[] nameArr = StringUtils.tokenizeToStringArray(nameAttr, ",; "); + aliases.addAll(Arrays.asList(nameArr)); + } + + String beanName = id; + if (!StringUtils.hasText(id) && !aliases.isEmpty()) { + beanName = (String)aliases.remove(0); + ... + } + + if (containingBean == null) { + this.checkNameUniqueness(beanName, aliases, ele); + } + + //终于,这里要解析beanDefinition了 + AbstractBeanDefinition beanDefinition = this.parseBeanDefinitionElement(ele, beanName, containingBean); + if (beanDefinition != null) { + if (!StringUtils.hasText(beanName)) { + try { + if (containingBean != null) { + beanName = BeanDefinitionReaderUtils.generateBeanName(beanDefinition, this.readerContext.getRegistry(), true); + } else { + beanName = this.readerContext.generateBeanName(beanDefinition); + String beanClassName = beanDefinition.getBeanClassName(); + if (beanClassName != null && beanName.startsWith(beanClassName) && beanName.length() > beanClassName.length() && !this.readerContext.getRegistry().isBeanNameInUse(beanClassName)) { + aliases.add(beanClassName); + } + } + ... + } catch (Exception var9) { + this.error(var9.getMessage(), ele); + return null; + } + } + + //别名数组 + String[] aliasesArray = StringUtils.toStringArray(aliases); + return new BeanDefinitionHolder(beanDefinition, beanName, aliasesArray); + } else { + return null; + } +} + +@Nullable +public AbstractBeanDefinition parseBeanDefinitionElement(Element ele, String beanName, @Nullable BeanDefinition containingBean) { + this.parseState.push(new BeanEntry(beanName)); + String className = null; + if (ele.hasAttribute("class")) { + className = ele.getAttribute("class").trim(); + } + + String parent = null; + if (ele.hasAttribute("parent")) { + parent = ele.getAttribute("parent"); + } + + try { + //创建BeanDefinition + AbstractBeanDefinition bd = this.createBeanDefinition(className, parent); + this.parseBeanDefinitionAttributes(ele, beanName, containingBean, bd); + bd.setDescription(DomUtils.getChildElementValueByTagName(ele, "description")); + this.parseMetaElements(ele, bd); + this.parseLookupOverrideSubElements(ele, bd.getMethodOverrides()); + this.parseReplacedMethodSubElements(ele, bd.getMethodOverrides()); + + //通过构造器解析参数值 + this.parseConstructorArgElements(ele, bd); + //通过property的value解析值吗,本文的程序xml就是通过property属性设置bean的值的,最终被这一方法所解析出来。 + this.parsePropertyElements(ele, bd); + + this.parseQualifierElements(ele, bd); + bd.setResource(this.readerContext.getResource()); + bd.setSource(this.extractSource(ele)); + AbstractBeanDefinition var7 = bd; + return var7; + } catch (ClassNotFoundException var13) { + ... + } + return null; +} + + +public void parsePropertyElements(Element beanEle, BeanDefinition bd) { + NodeList nl = beanEle.getChildNodes(); + for(int i = 0; i < nl.getLength(); ++i) { + Node node = nl.item(i); + if (this.isCandidateElement(node) && this.nodeNameEquals(node, "property")) { + //解析出参数值来,这里就真正的讲age的23,和name的bruis值解析出来并防止在一个组装的类里面存放着。因为这里有两个bean,所以要循环调用两次parsePropertyElement()方法 + this.parsePropertyElement((Element)node, bd); + } + } +} + +public void parsePropertyElement(Element ele, BeanDefinition bd) { + String propertyName = ele.getAttribute("name"); + if (!StringUtils.hasLength(propertyName)) { + this.error("Tag 'property' must have a 'name' attribute", ele); + } else { + this.parseState.push(new PropertyEntry(propertyName)); + + try { + if (!bd.getPropertyValues().contains(propertyName)) { + Object val = this.parsePropertyValue(ele, bd, propertyName); + PropertyValue pv = new PropertyValue(propertyName, val); + this.parseMetaElements(ele, pv); + pv.setSource(this.extractSource(ele)); + + //就是这一步,将K为age、name,值分别为23、bruis的KV对存放在了Spring容器里。 + bd.getPropertyValues().addPropertyValue(pv); + return; + } + } finally { + this.parseState.pop(); + } + + } +} + +``` +![spring-08](https://github.com/coderbruis/JavaSourceCodeLearning/blob/master/note/images/spring/spring-08.png) +![spring-09](https://github.com/coderbruis/JavaSourceCodeLearning/blob/master/note/images/spring/spring-09.png) + +然后,就会一路返回到refresh()方法里的加载bean定义信息的方法——loadBeanDefinitions(),此时beanFactory里面就会存在一个带有KV对的ConcurrentHashMap,而这个beanFactory会存放在Spring容器里面。 +```Java +DefaultListableBeanFactory beanFactory = createBeanFactory(); +beanFactory.setSerializationId(getId()); +customizeBeanFactory(beanFactory); +//加载bean定义信息 +loadBeanDefinitions(beanFactory); +``` +再看看DefaultListableBeanFactory里面的内容 +![spring-10](https://github.com/coderbruis/JavaSourceCodeLearning/blob/master/note/images/spring/spring-10.png) +![spring-11](https://github.com/coderbruis/JavaSourceCodeLearning/blob/master/note/images/spring/spring-11.png) + +上面的过程,就已经完成了Spring容器的初始化过程,相信读者也已经对Spring容器的初始化有了一个大致的了解。下面总结一下Spring容器的初始化: +- 第一个过程是Resource定位过程。这个Resource定位过程指的是BeanDefinition的资源定位,它由ResourceLoader通过统一的Resource接口来完成,这个Resource对各种形式的BeanDefinition的使用都提供了统一接口。这个定位过程类似于容器寻找数据的过程,就像使用水桶装水先要把水找到一样。 +- 第二个过程是BeanDefinition的载入。这个载入过程是把用户定义好的Bean表示成IOC容器内部的数据结构,而这个容器内部的数据结构就是BeanDefinition。下面介绍这个数据结构的详细定义。具体来说,这个BeanDefinition实际上就是POJO对象在IOC容器的抽象,通过这个BeanDefinition定义的数据结构,使IOC能够方便地对POJO对象进行管理。 +- 第三个过程是向IOC容器注册这些BeanDefinition的过程,这个过程是通过调用BeanDefinitionRegistry接口的实现来完成的。这个注册过程把载入过程中解析到的BeanDefinition向IOC容器进行注册。通过上面的分析,我们知道IOC内部将BeanDefinition注册到了ConcurrentHashMap中。 + +经过前面源码分析,我们终于弄清楚了Spring对XML配置文件的解析的过程,接下来就是要弄清楚Spring容器对bean的创建和加载了。 +下面就介绍如何将Map里的bean的信息是如何被添加至person实例中。 + +### 深入理解Spring容器如何创建bean和初始化bean +bean的创建和初始化过程是在refresh方法里的invokeBeanFactoryPostProcessors()方法里实现的。 + +下面先简单总结一下在IOC中bean的生命周期: +- Bean实例的创建 +- 为Bean实例设置属性 +- 调用Bean的初始化方法 +- 应用可以通过IOC容器使用Bean +- 当容器关闭时,调用Bean的销毁方法 + +下面先看看创建bean和初始化bean的时序图。 +![spring-12](https://github.com/coderbruis/JavaSourceCodeLearning/blob/master/note/images/spring/spring-12.jpg) + +1. AbstractApplicationContext.class +```Java +public void refresh() throws BeansException, IllegalStateException { + ... + // 实例剩余的(非懒加载)的单例 + finishBeanFactoryInitialization(beanFactory); + ... +} + +/* + 返程上下文bean工厂的实例化过程,实例化所有剩余的单例类 +*/ +protected void finishBeanFactoryInitialization(ConfigurableListableBeanFactory beanFactory) { + // 初始化上下文的 conversion service + if (beanFactory.containsBean(CONVERSION_SERVICE_BEAN_NAME) && + beanFactory.isTypeMatch(CONVERSION_SERVICE_BEAN_NAME, ConversionService.class)) { + beanFactory.setConversionService( + beanFactory.getBean(CONVERSION_SERVICE_BEAN_NAME, ConversionService.class)); + } + + // Register a default embedded value resolver if no bean post-processor + // (such as a PropertyPlaceholderConfigurer bean) registered any before: + // at this point, primarily for resolution in annotation attribute values. + if (!beanFactory.hasEmbeddedValueResolver()) { + beanFactory.addEmbeddedValueResolver(strVal -> getEnvironment().resolvePlaceholders(strVal)); + } + + // Initialize LoadTimeWeaverAware beans early to allow for registering their transformers early. + String[] weaverAwareNames = beanFactory.getBeanNamesForType(LoadTimeWeaverAware.class, false, false); + for (String weaverAwareName : weaverAwareNames) { + getBean(weaverAwareName); + } + + // 停止使用类型匹配的临时类加载器 + beanFactory.setTempClassLoader(null); + + // 允许缓存所有bean定义元数据,而不期待进一步的更改 + beanFactory.freezeConfiguration(); + + // 实例剩余的(非懒加载)的单例 + beanFactory.preInstantiateSingletons(); +} + +``` +这里的懒加载的意思,指的是bean单例不是在Spring容器初始化的时候就创建的,而是在要使用该bean的时候,才会创建该bean。 + +2. DefaultListableBeanFactory.class +```Java +// 实例剩余的(非懒加载)的单例 +@Override +public void preInstantiateSingletons() throws BeansException { + if (this.logger.isDebugEnabled()) { + this.logger.debug("Pre-instantiating singletons in " + this); + } + + // 获取所有的bean定义的名字,并保存在集合List里面。 + List beanNames = new ArrayList<>(this.beanDefinitionNames); + + // 触发所有非延迟单例bean的初始化... + for (String beanName : beanNames) { + // 触发所有适用bean的后初始化回调 + RootBeanDefinition bd = getMergedLocalBeanDefinition(beanName); + if (!bd.isAbstract() && bd.isSingleton() && !bd.isLazyInit()) { + if (isFactoryBean(beanName)) { + Object bean = getBean(FACTORY_BEAN_PREFIX + beanName); + if (bean instanceof FactoryBean) { + final FactoryBean factory = (FactoryBean) bean; + boolean isEagerInit; + if (System.getSecurityManager() != null && factory instanceof SmartFactoryBean) { + isEagerInit = AccessController.doPrivileged((PrivilegedAction) () -> + ((SmartFactoryBean) factory).isEagerInit(), + getAccessControlContext()); + } + else { + isEagerInit = (factory instanceof SmartFactoryBean && + ((SmartFactoryBean) factory).isEagerInit()); + } + if (isEagerInit) { + getBean(beanName); + } + } + } + else { + getBean(beanName); + } + } + } + + // 触发所有适用bean的后置初始化回调方法... + for (String beanName : beanNames) { + Object singletonInstance = getSingleton(beanName); + if (singletonInstance instanceof SmartInitializingSingleton) { + final SmartInitializingSingleton smartSingleton = (SmartInitializingSingleton) singletonInstance; + if (System.getSecurityManager() != null) { + AccessController.doPrivileged((PrivilegedAction) () -> { + smartSingleton.afterSingletonsInstantiated(); + return null; + }, getAccessControlContext()); + } + else { + smartSingleton.afterSingletonsInstantiated(); + } + } + } +} +``` + +3. AbstractBeanFactory.class +```Java +protected T doGetBean(final String name, @Nullable final Class requiredType, + @Nullable final Object[] args, boolean typeCheckOnly) throws BeansException { + // 去除name上存在的工厂bean的前缀 + final String beanName = transformedBeanName(name); + Object bean; + + // 快速判断单例缓存中是否存在该bean,如果存在则返回单例bean;否则返回null + Object sharedInstance = getSingleton(beanName); + if (sharedInstance != null && args == null) { + ... + // 从单例缓存中获取单例bean + bean = getObjectForBeanInstance(sharedInstance, name, beanName, null); + } + else { + // 判断该bean是否存在父工厂 + BeanFactory parentBeanFactory = getParentBeanFactory(); + if (parentBeanFactory != null && !containsBeanDefinition(beanName)) { + //如果存在父工厂,则按照父工厂的实现来创建该实例 + } + + // 标记该bean已经被创建 + if (!typeCheckOnly) { + markBeanAsCreated(beanName); + } + + try { + // 返回bean的定义信息,包括bean的scope、依赖、是否进行懒加载等定义 + final RootBeanDefinition mbd = getMergedLocalBeanDefinition(beanName); + checkMergedBeanDefinition(mbd, beanName, args); + + // 确保初始化所有依赖这个bean的bean + String[] dependsOn = mbd.getDependsOn(); + if (dependsOn != null) { + for (String dep : dependsOn) { + + ... + + registerDependentBean(dep, beanName); + getBean(dep); + } + } + + // 开始创建这个bean,注意这里使用的lambda表达式,传入的是一个AbstracBeanFactory + if (mbd.isSingleton()) { + sharedInstance = getSingleton(beanName, () -> { + try { + return createBean(beanName, mbd, args); + } + catch (BeansException ex) { + ... + } + }); + bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd); + } + + else if (mbd.isPrototype()) { + // 该bean定义了原型模式,创建该bean + Object prototypeInstance = null; + try { + beforePrototypeCreation(beanName); + prototypeInstance = createBean(beanName, mbd, args); + } + finally { + afterPrototypeCreation(beanName); + } + bean = getObjectForBeanInstance(prototypeInstance, name, beanName, mbd); + } + + else { + String scopeName = mbd.getScope(); + final Scope scope = this.scopes.get(scopeName); + if (scope == null) { + ... + } + try { + Object scopedInstance = scope.get(beanName, () -> { + beforePrototypeCreation(beanName); + try { + return createBean(beanName, mbd, args); + } + finally { + afterPrototypeCreation(beanName); + } + }); + bean = getObjectForBeanInstance(scopedInstance, name, beanName, mbd); + } + catch (IllegalStateException ex) { + ... + } + } + } + catch (BeansException ex) { + ... + } + } + + // Check if required type matches the type of the actual bean instance. + if (requiredType != null && !requiredType.isInstance(bean)) { + try { + T convertedBean = getTypeConverter().convertIfNecessary(bean, requiredType); + if (convertedBean == null) { + ... + } + return convertedBean; + } + catch (TypeMismatchException ex) { + ... + } + } + return (T) bean; + } +``` + +4. DefaultSingletonBeanRegistry.class +```Java +/* + 尝试从缓存中获取单例对象,如果缓存中有该单例对象,并且该对象正在被创建,则从缓存中获取。 +*/ +@Nullable +protected Object getSingleton(String beanName, boolean allowEarlyReference) { + // singletonObjects是一个单例缓存,是一个ConcurrentHashMap + Object singletonObject = this.singletonObjects.get(beanName); + // 从缓存中获取单例对象、判断对象是否正在被创建 + if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) { + synchronized (this.singletonObjects) { + singletonObject = this.earlySingletonObjects.get(beanName); + if (singletonObject == null && allowEarlyReference) { + ObjectFactory singletonFactory = this.singletonFactories.get(beanName); + if (singletonFactory != null) { + singletonObject = singletonFactory.getObject(); + this.earlySingletonObjects.put(beanName, singletonObject); + this.singletonFactories.remove(beanName); + } + } + } + } + return singletonObject; +} + +/* + +*/ +public Object getSingleton(String beanName, ObjectFactory singletonFactory) { + Assert.notNull(beanName, "Bean name must not be null"); + // 可以看到,对于单例bean的获取,是要获得“单例缓存”的锁的,否则无法操作bean的获取。 + synchronized (this.singletonObjects) { + Object singletonObject = this.singletonObjects.get(beanName); + if (singletonObject == null) { + + ... + + // bean创建之前的回调方法 + beforeSingletonCreation(beanName); + boolean newSingleton = false; + + ... + + try { + // 尝试从单例工厂获取单例bean + // 注意,这里的singletonFactory是由方法参数传进来的一个lambda表达式,这个singletonFactory是一个AbstractAutowireCapableBeanFactory实例 + singletonObject = singletonFactory.getObject(); + newSingleton = true; + } + catch (IllegalStateException ex) { + ... + } + catch (BeanCreationException ex) { + ... + } + finally { + + ... + + // 单例bean创建过后的回调方法 + afterSingletonCreation(beanName); + } + if (newSingleton) { + addSingleton(beanName, singletonObject); + } + } + return singletonObject; + } +} +``` + +无图无真相: +![spring-13](https://github.com/coderbruis/JavaSourceCodeLearning/blob/master/note/images/spring/spring-13.png) + +5. AbstractAutowireCapableBeanFactory.class +```Java +/* + 该类的中心方法:创建bean实例,实例化bean实例,应用bean的后置处理器 + +*/ +@Override +protected Object createBean(String beanName, RootBeanDefinition mbd, @Nullable Object[] args) + throws BeanCreationException { + + RootBeanDefinition mbdToUse = mbd; + + try { + // 调用实例化前的后置处理器,在这个后置处理器中可以对bean进行处理,可以返回由后置处理器处理的bean而不是被实例化的bean。 + // 换句话说就是这里可以拦截住bean的实例化 + // Spring的后置处理器后面有机会再专门写一篇博文来总结学习一下 + Object bean = resolveBeforeInstantiation(beanName, mbdToUse); + if (bean != null) { + return bean; + } + } + catch (Throwable ex) { + ... + } + try { + Object beanInstance = doCreateBean(beanName, mbdToUse, args); + ... + return beanInstance; + } + catch (BeanCreationException ex) { + ... + } + catch (ImplicitlyAppearedSingletonException ex) { + ... + } + catch (Throwable ex) { + ... + } +} + +/* + 正在的创建一个bean,并且按照配置文件的配置来实例化该bean。如果没有初始化前的后置处理器的调用,则调用该方法。 +*/ +protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final @Nullable Object[] args) + throws BeanCreationException { + // 创建一个包装类,用于包装真正要创建的bean。 + BeanWrapper instanceWrapper = null; + if (mbd.isSingleton()) { + instanceWrapper = this.factoryBeanInstanceCache.remove(beanName); + } + if (instanceWrapper == null) { + // 使用适当的实例化策略例如:工厂方法、构造函数自动装配或者简单实例化 来为指定bean创建新的实例。这里仅仅是简单的实例化,为bean设置默认初始值 + // 也就是name为null,age为0。此时instanceWrapper任然还只是一个包装bean,并不是一个真正意义上的person类bean。 + instanceWrapper = createBeanInstance(beanName, mbd, args); + } + + // 终于,我们千辛万苦打断点调试来到了这一步,就是这一步, 获得了我们想要的person类bean。 + // 只需要在BeanWrapper里取出WrapperInstance即可。 + // 接下来就是要拿这个创建好的bean和BeanDefinition进行实例化了。 + final Object bean = instanceWrapper.getWrappedInstance(); + + // 获取bean的类型 + Class beanType = instanceWrapper.getWrappedClass(); + if (beanType != NullBean.class) { + mbd.resolvedTargetType = beanType; + } + + // 调用后置处理器去修改bean的定义信息。 + synchronized (mbd.postProcessingLock) { + if (!mbd.postProcessed) { + try { + applyMergedBeanDefinitionPostProcessors(mbd, beanType, beanName); + } + catch (Throwable ex) { + throw new BeanCreationException(mbd.getResourceDescription(), beanName, + "Post-processing of merged bean definition failed", ex); + } + mbd.postProcessed = true; + } + } + + boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences && + isSingletonCurrentlyInCreation(beanName)); + if (earlySingletonExposure) { + ... + // 先快速的给bean实例化,然后将bean添加到单例工厂中。这里的单例工厂实际上就是一个Map + addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean)); + } + + // 实例化一个真正使用的bean。 + Object exposedObject = bean; + try { + + populateBean(beanName, mbd, instanceWrapper); + exposedObject = initializeBean(beanName, exposedObject, mbd); + } + catch (Throwable ex) { + ... + } + + if (earlySingletonExposure) { + Object earlySingletonReference = getSingleton(beanName, false); + if (earlySingletonReference != null) { + if (exposedObject == bean) { + exposedObject = earlySingletonReference; + } + else if (!this.allowRawInjectionDespiteWrapping && hasDependentBean(beanName)) { + String[] dependentBeans = getDependentBeans(beanName); + Set actualDependentBeans = new LinkedHashSet<>(dependentBeans.length); + for (String dependentBean : dependentBeans) { + if (!removeSingletonIfCreatedForTypeCheckOnly(dependentBean)) { + actualDependentBeans.add(dependentBean); + } + } + if (!actualDependentBeans.isEmpty()) { + ... + } + } + } + } + + // Register bean as disposable. + try { + registerDisposableBeanIfNecessary(beanName, bean, mbd); + } + catch (BeanDefinitionValidationException ex) { + ... + } + return exposedObject; +} + +/* + 对创建出的Bean进行赋值填充。 +*/ +protected void populateBean(String beanName, RootBeanDefinition mbd, @Nullable BeanWrapper bw) { + ... + // 取出BeanDefinition里的属性值 + PropertyValues pvs = (mbd.hasPropertyValues() ? mbd.getPropertyValues() : null); + + // 如果设置的是自动装配模式,则由自动装配来进行赋值 + if (mbd.getResolvedAutowireMode() == RootBeanDefinition.AUTOWIRE_BY_NAME || + mbd.getResolvedAutowireMode() == RootBeanDefinition.AUTOWIRE_BY_TYPE) { + MutablePropertyValues newPvs = new MutablePropertyValues(pvs); + + // 通过bean名自动装配 + if (mbd.getResolvedAutowireMode() == RootBeanDefinition.AUTOWIRE_BY_NAME) { + autowireByName(beanName, mbd, bw, newPvs); + } + + // 通过bean类型自动装配 + if (mbd.getResolvedAutowireMode() == RootBeanDefinition.AUTOWIRE_BY_TYPE) { + autowireByType(beanName, mbd, bw, newPvs); + } + + pvs = newPvs; + } + + boolean hasInstAwareBpps = hasInstantiationAwareBeanPostProcessors(); + boolean needsDepCheck = (mbd.getDependencyCheck() != RootBeanDefinition.DEPENDENCY_CHECK_NONE); + + if (hasInstAwareBpps || needsDepCheck) { + if (pvs == null) { + pvs = mbd.getPropertyValues(); + } + PropertyDescriptor[] filteredPds = filterPropertyDescriptorsForDependencyCheck(bw, mbd.allowCaching); + if (hasInstAwareBpps) { + for (BeanPostProcessor bp : getBeanPostProcessors()) { + if (bp instanceof InstantiationAwareBeanPostProcessor) { + InstantiationAwareBeanPostProcessor ibp = (InstantiationAwareBeanPostProcessor) bp; + pvs = ibp.postProcessPropertyValues(pvs, filteredPds, bw.getWrappedInstance(), beanName); + if (pvs == null) { + return; + } + } + } + } + if (needsDepCheck) { + checkDependencies(beanName, mbd, filteredPds, pvs); + } + } + + // 这里的PropertyValues已经包含了bean字段属性的设置值了 + if (pvs != null) { + // 对bean进行赋值 + applyPropertyValues(beanName, mbd, bw, pvs); + } +} + + +/* + 应用给定的属性值,并使用深拷贝对bean进行填充赋值。 +*/ +protected void applyPropertyValues(String beanName, BeanDefinition mbd, BeanWrapper bw, PropertyValues pvs) { + if (pvs.isEmpty()) { + return; + } + + MutablePropertyValues mpvs = null; + + // 源属性值 + List original; + + if (System.getSecurityManager() != null) { + if (bw instanceof BeanWrapperImpl) { + ((BeanWrapperImpl) bw).setSecurityContext(getAccessControlContext()); + } + } + + if (pvs instanceof MutablePropertyValues) { + mpvs = (MutablePropertyValues) pvs; + if (mpvs.isConverted()) { + try { + bw.setPropertyValues(mpvs); + return; + } + catch (BeansException ex) { + ... + } + } + original = mpvs.getPropertyValueList(); + } + else { + original = Arrays.asList(pvs.getPropertyValues()); + } + + TypeConverter converter = getCustomTypeConverter(); + if (converter == null) { + converter = bw; + } + BeanDefinitionValueResolver valueResolver = new BeanDefinitionValueResolver(this, beanName, mbd, converter); + + // 拷贝值;创建一个深拷贝副本,应用于任何bean引用此bean的情况。 + List deepCopy = new ArrayList<>(original.size()); + boolean resolveNecessary = false; + for (PropertyValue pv : original) { + if (pv.isConverted()) { + deepCopy.add(pv); + } + else { + String propertyName = pv.getName(); + Object originalValue = pv.getValue(); + Object resolvedValue = valueResolver.resolveValueIfNecessary(pv, originalValue); + Object convertedValue = resolvedValue; + boolean convertible = bw.isWritableProperty(propertyName) && + !PropertyAccessorUtils.isNestedOrIndexedProperty(propertyName); + if (convertible) { + convertedValue = convertForProperty(resolvedValue, propertyName, bw, converter); + } + // 可能在合并的bean定义中存储转换后的值,以避免为每个创建的bean实例重新转换。 + if (resolvedValue == originalValue) { + if (convertible) { + pv.setConvertedValue(convertedValue); + } + deepCopy.add(pv); + } + else if (convertible && originalValue instanceof TypedStringValue && + !((TypedStringValue) originalValue).isDynamic() && + !(convertedValue instanceof Collection || ObjectUtils.isArray(convertedValue))) { + pv.setConvertedValue(convertedValue); + deepCopy.add(pv); + } + else { + resolveNecessary = true; + deepCopy.add(new PropertyValue(pv, convertedValue)); + } + } + } + if (mpvs != null && !resolveNecessary) { + mpvs.setConverted(); + } + + // 将深拷贝属性数组填充到beanWrapper中。这里就真正的将属性值填充到了bean上,实现了 + try { + bw.setPropertyValues(new MutablePropertyValues(deepCopy)); + } + catch (BeansException ex) { + ... + } +} +``` + +经过上面的分析,就知道真正的对bean赋值填充是在AbstractAutowireCapableBeanFactory.class类里的applyPropertyValues方法里的,并且是通过对原属性值进行了一次深拷贝,然后将深拷贝后的属性值填充到bean里的。 + +## 在web容器中初始化spring容器 +[深入Spring源码系列(二)——深入Spring容器,通过源码阅读和时序图来彻底弄懂Spring容器(下)](https://github.com/coderbruis/JavaSourceCodeLearning/blob/master/note/Spring/%E6%B7%B1%E5%85%A5Spring%E6%BA%90%E7%A0%81%E7%B3%BB%E5%88%97%EF%BC%88%E4%BA%8C%EF%BC%89%E2%80%94%E2%80%94%E6%B7%B1%E5%85%A5Spring%E5%AE%B9%E5%99%A8%EF%BC%8C%E9%80%9A%E8%BF%87%E6%BA%90%E7%A0%81%E9%98%85%E8%AF%BB%E5%92%8C%E6%97%B6%E5%BA%8F%E5%9B%BE%E6%9D%A5%E5%BD%BB%E5%BA%95%E5%BC%84%E6%87%82Spring%E5%AE%B9%E5%99%A8%EF%BC%88%E4%B8%8B%EF%BC%89.md) + + +## 参考 +- 《Spring源码深度分析》 +- 《Spring技术内幕》 + diff --git "a/note/Spring/\346\267\261\345\205\245Spring\346\272\220\347\240\201\347\263\273\345\210\227\357\274\210\344\272\214\357\274\211\342\200\224\342\200\224\346\267\261\345\205\245Spring\345\256\271\345\231\250\357\274\214\351\200\232\350\277\207\346\272\220\347\240\201\351\230\205\350\257\273\345\222\214\346\227\266\345\272\217\345\233\276\346\235\245\345\275\273\345\272\225\345\274\204\346\207\202Spring\345\256\271\345\231\250\357\274\210\344\270\213\357\274\211.md" "b/note/Spring/\346\267\261\345\205\245Spring\346\272\220\347\240\201\347\263\273\345\210\227\357\274\210\344\272\214\357\274\211\342\200\224\342\200\224\346\267\261\345\205\245Spring\345\256\271\345\231\250\357\274\214\351\200\232\350\277\207\346\272\220\347\240\201\351\230\205\350\257\273\345\222\214\346\227\266\345\272\217\345\233\276\346\235\245\345\275\273\345\272\225\345\274\204\346\207\202Spring\345\256\271\345\231\250\357\274\210\344\270\213\357\274\211.md" new file mode 100644 index 0000000..89fdf81 --- /dev/null +++ "b/note/Spring/\346\267\261\345\205\245Spring\346\272\220\347\240\201\347\263\273\345\210\227\357\274\210\344\272\214\357\274\211\342\200\224\342\200\224\346\267\261\345\205\245Spring\345\256\271\345\231\250\357\274\214\351\200\232\350\277\207\346\272\220\347\240\201\351\230\205\350\257\273\345\222\214\346\227\266\345\272\217\345\233\276\346\235\245\345\275\273\345\272\225\345\274\204\346\207\202Spring\345\256\271\345\231\250\357\274\210\344\270\213\357\274\211.md" @@ -0,0 +1,316 @@ + +- [前言](#前言) +- [在web容器中初始化spring容器](#在web容器中初始化spring容器) + - [SpringIOC容器和Web容器](#springioc容器和web容器) + - [ContextLoaderListener](#contextloaderlistener) +- [总结](#总结) + + +## 前言 + +继上一篇文章深入了解了在普通的Spring应用程序中如何创建并启动SpringIOC容器的,下面,深入学习一下在web容器中,是如何创建、初始化并启动SpringIOC容器的。 + +**Spring版本:Spring5-0-3** + +## 在web容器中初始化spring容器 + +相信读者都能够用IDE搭建出基于Spring的web应用程序,例如SSM框架(不会的百度一下吧,这里就不讲解了)。 + +在搭建SSM框架的时候,引入SpringMVC配置文件有两种方式,如下: + +**方式一:** + +在web容器初始化过程中,会在WEB-INF文件夹下寻找名为[servlet-name]-servlet.xml的配置文件作为SpringMVC的配置文件,如下springMVC的配置文件就是放在WEB-INF下名为dispatcherServlet-servlet.xml的配置文件 +``` + + dispatcherServlet + org.springframework.web.servlet.DispatcherServlet + 1 + + + dispatcherServlet + / + +``` +**方式二:** + +直接在类路径下配置SpringMVC配置文件。 +``` + + DispatcherServlet + org.springframework.web.servlet.DispatcherServlet + + contextConfigLocation + classpath:springmvcconfig.xml + + 1 + + + DispatcherServlet + / + +``` + +在web.xml下可以看到DispatcherServlet是SpringMVC的核心,下面将重点讲解DispatcherServlet的原理以及作用。 + +### SpringIOC容器和Web容器 +SpringIOC容器是如何在Web环境中被加载并起作用的?SpringIOC容器是何时创建的?何时初始化的? + +**首先,必须知道的一点是:SpringIOC是一个独立的模块,它并不是直接在Web容器中发挥作用的。** + +如果要在Web容器中使用IOC容器,需要Spring为IOC设计一个启动过程,把IOC容器导入,并将Web容器中建立起来。具体来说,SpringIOC容器的启动过程是和Web容器的启动过程集成在一起的。在这个启动过程中,一方面处理Web容器的启动,另一方面处理SpringIOC容器的启动过程,对于SpringIOC容器的启动过程需要设计特定的Web容器拦截器,将SpringIOC容器集成到Web容器中,并将其初始化。完成了上述过程,SpringIOC容器才能正常工作,而SpringMVC是建立在IOC容器的基础上的,这样才能建立起MVC框架的运行机制,从而响应从容器传递的HTTP请求。 + + +启动Spring的容器,让项目一启动,就启动SpringIOC容器,下面是web.xml配置文件中的配置信息: +``` + + contextConfigLocation + + classpath:applicationContext.xml + + + org.springframework.web.context.ContextLoaderListener + +``` + +### ContextLoaderListener + +**ContextLoaderListener作为一个监听器,不仅负责完成IOC容器在Web容器中的启动工作,并且还是SpringMVC的启动类。** +IOC容器的启动过程就是建立Spring上下文的过程,该上下文是与ServletContext相伴而生的,同时也是IOC容器在Web应用环境中的具体表现之一。由ContextLoaderListener启动的上下文为根上下文。在根上下文的基础上,还有一个与Web MVC相关的上下文应用来保存控制器(DispatcherServlet)需要的MVC对象,**作为根上下文的子上下文**,构成一个层次化的上下文体系,这个与Web MVC相关的上下文——WebApplicationContext。在Web容器中启动Spring应用程序时,首先建立根上下文,然后建立这个上下文体系,这个上下文体系的建立是由ContextLoader来完成的。简单点说,ContextLoaderListener的作用就是启动Web容器时,自动装配ApplicationContext的配置信息。 + +先看看Web程序启动到SpringIOC容器创建和初始化的整个过程。 +![spring-14](https://github.com/coderbruis/JavaSourceCodeLearning/blob/master/note/images/spring/spring-14.jpg) + +结合着时序图,再去调试源码,思路会清晰很多。 + +ContextLoaderListener.class +```Java +public class ContextLoaderListener extends ContextLoader implements ServletContextListener { + + public ContextLoaderListener() { + } + + public ContextLoaderListener(WebApplicationContext context) { + super(context); + } + + /** + * 初始化WebApplicationContext + */ + @Override + public void contextInitialized(ServletContextEvent event) { + initWebApplicationContext(event.getServletContext()); + } + + /** + * 关闭WebApplicationContext + */ + @Override + public void contextDestroyed(ServletContextEvent event) { + closeWebApplicationContext(event.getServletContext()); + ContextCleanupListener.cleanupAttributes(event.getServletContext()); + } + +} + +``` +这里的ContextLoaderListener是Spring的类,但实现了ServletContextListener接口。这个接口是Servlet API中定义的,提供了与Servlet生命周期结合的回调,也就是说Servlet调用contextInitialized()方法初始化容器时,会回调ContextLoaderListener中实现的contextInitialized()方法,Servlet中的contextDestroyed()方法也同理。观察源码可知,在Web容器中,建立WebApplicationContext的过程是在contextInitialized()方法中完成的。 + +ContextLoader.class +```Java +public WebApplicationContext initWebApplicationContext(ServletContext servletContext) { + ... + // 判断在web容器中是否存在WebApplicationContext,因为在配置中只允许申明一次ServletContextListener,多次声明会扰乱Spring的执行逻辑。 + if (servletContext.getAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE) != null) { + throw new IllegalStateException( + "Cannot initialize context because there is already a root application context present - " + + "check whether you have multiple ContextLoader* definitions in your web.xml!"); + } + + try { + // 创建WebApplicationContext,将上下文存储在本地实例变量中,以保证它在ServletContext关闭时可用。 + if (this.context == null) { + this.context = createWebApplicationContext(servletContext); + } + + // 确保该容器是可配置的web容器 + if (this.context instanceof ConfigurableWebApplicationContext) { + ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) this.context; + if (!cwac.isActive()) { + // 上下文尚未刷新 - >提供诸如设置父上下文,设置应用程序上下文ID等服务 + if (cwac.getParent() == null) { + // 在Web容器中建立起双亲IOC容器 + ApplicationContext parent = loadParentContext(servletContext); + cwac.setParent(parent); + } + // 经过上面两个步骤,现在开始配置并初始化WebApplicationContext。 + configureAndRefreshWebApplicationContext(cwac, servletContext); + } + } + servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.context); + + ClassLoader ccl = Thread.currentThread().getContextClassLoader(); + if (ccl == ContextLoader.class.getClassLoader()) { + currentContext = this.context; + } + else if (ccl != null) { + currentContextPerThread.put(ccl, this.context); + } + ... + + return this.context; + } + catch (RuntimeException ex) { + ... + } +} +``` +由ContextLoader的源码可知,SpringIOC的载入过程是在ContextLoader类的initWebApplicationContext()方法中完成的。 + +这里还要介绍一个重要的接口——WebApplicationContext +```Java +public interface WebApplicationContext extends ApplicationContext { + + /** + * 用于在成功启动时将根WebApplicationContext绑定到的Context属性。 + */ + String ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE = WebApplicationContext.class.getName() + ".ROOT"; + + /** + * 获取Web容器的上下文,也就是ServletContext对象,这里相当于提供了一个Web容器级别的全局变量。 + */ + ServletContext getServletContext(); + +} +``` +而WebApplicationContext接口是由XMLWebApplicationContext来实现具体的功能,然后再通过ApplicationContext接口与BeanFactory接口对接,完成Spring容器的功能。然而对于具体的一些Spring容器的实现都是在AbstractRefreshableWebApplicationContext中完成的,这一点和**上篇**讲解的AbstractRefreshableConfigApplicationContext功能类似。initWebApplicationContext()方法最后返回的是一个WebApplicationContext接口,而实际返回的就是XMLWebApplicationContext实现类。XMLWebApplicationContext在基本的ApplicationContext功能的基础上,增加了对**Web环境**和XML配置定义的处理。在XMLWebApplicationContext的初始化过程中,Web容器中的IOC容器被建立起来,从而再整个Web容器中建立起Spring应用。 +```Java +public class XmlWebApplicationContext extends AbstractRefreshableWebApplicationContext { + + /** 默认读取Spring配置文件的根路径,如果指定其他配置文件,则从这个默认的根路径读取。 */ + public static final String DEFAULT_CONFIG_LOCATION = "/WEB-INF/applicationContext.xml"; + + /** 默认的配置文件位置在/WEB-INF/目录下 */ + public static final String DEFAULT_CONFIG_LOCATION_PREFIX = "/WEB-INF/"; + + /** 默认的配置文件后缀.xml文件 */ + public static final String DEFAULT_CONFIG_LOCATION_SUFFIX = ".xml"; + + + /** + * 熟悉的loadBeanDefinitions方法,相信看过上篇的读者,应该不会陌生。这里的功能就不在赘述了。 + */ + @Override + protected void loadBeanDefinitions(DefaultListableBeanFactory beanFactory) throws BeansException, IOException { + XmlBeanDefinitionReader beanDefinitionReader = new XmlBeanDefinitionReader(beanFactory); + beanDefinitionReader.setEnvironment(getEnvironment()); + beanDefinitionReader.setResourceLoader(this); + beanDefinitionReader.setEntityResolver(new ResourceEntityResolver(this)); + initBeanDefinitionReader(beanDefinitionReader); + loadBeanDefinitions(beanDefinitionReader); + } + protected void initBeanDefinitionReader(XmlBeanDefinitionReader beanDefinitionReader) { + } + protected void loadBeanDefinitions(XmlBeanDefinitionReader reader) throws IOException { + String[] configLocations = getConfigLocations(); + if (configLocations != null) { + for (String configLocation : configLocations) { + reader.loadBeanDefinitions(configLocation); + } + } + } + @Override + protected String[] getDefaultConfigLocations() { + if (getNamespace() != null) { + return new String[] {DEFAULT_CONFIG_LOCATION_PREFIX + getNamespace() + DEFAULT_CONFIG_LOCATION_SUFFIX}; + } + else { + return new String[] {DEFAULT_CONFIG_LOCATION}; + } + } + +} +``` +从源码中可以看到,XMLWebApplicationContext中成员变量存放着默认的读取Spring配置文件的根目录,在生成IOC容器过程中,就会从默认路径/WEB-INF/applicationContext.xml配置文件中或者指定的配置文件路径获取,然后再通过熟悉的loadBeanDefinitions()方法来获取Bean定义信息,最终完成整个上下文的初始化过程。 + +ContextLoader.class +```Java +protected WebApplicationContext createWebApplicationContext(ServletContext sc) { + // 这里判断使用什么样的类在Web容器中作为IOC容器 + Class contextClass = determineContextClass(sc); + if (!ConfigurableWebApplicationContext.class.isAssignableFrom(contextClass)) { + throw new ApplicationContextException("Custom context class [" + contextClass.getName() + + "] is not of type [" + ConfigurableWebApplicationContext.class.getName() + "]"); + } + // 直接实例化需要产生的IOC容器,并设置IOC容器的各个参数,然后通过refresh启动容器的初始化。refresh的过程相信读者并不陌生。 + return (ConfigurableWebApplicationContext) BeanUtils.instantiateClass(contextClass); +} + +protected Class determineContextClass(ServletContext servletContext) { + String contextClassName = servletContext.getInitParameter(CONTEXT_CLASS_PARAM); + // 判断是否存在指定的IOC + if (contextClassName != null) { + try { + return ClassUtils.forName(contextClassName, ClassUtils.getDefaultClassLoader()); + } + catch (ClassNotFoundException ex) { + ... + } + } + else { + // 如果没有指定的IOC容器,则properties中获取默认的IOC容器,也就是XMLWebApplicationContext。 + contextClassName = defaultStrategies.getProperty(WebApplicationContext.class.getName()); + try { + return ClassUtils.forName(contextClassName, ContextLoader.class.getClassLoader()); + } + catch (ClassNotFoundException ex) { + ... + } + } +} + +``` + +下面看看默认的IOC容器是什么。有图有真相: +![spring-15](https://github.com/coderbruis/JavaSourceCodeLearning/blob/master/note/images/spring/spring-15.jpg) + +```Java +protected void configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac, ServletContext sc) { + if (ObjectUtils.identityToString(wac).equals(wac.getId())) { + String idParam = sc.getInitParameter(CONTEXT_ID_PARAM); + if (idParam != null) { + wac.setId(idParam); + } + else { + wac.setId(ConfigurableWebApplicationContext.APPLICATION_CONTEXT_ID_PREFIX + + ObjectUtils.getDisplayString(sc.getContextPath())); + } + } + + // 设置ServletContext的引用 + wac.setServletContext(sc); + String configLocationParam = sc.getInitParameter(CONFIG_LOCATION_PARAM); + if (configLocationParam != null) { + wac.setConfigLocation(configLocationParam); + } + + ConfigurableEnvironment env = wac.getEnvironment(); + if (env instanceof ConfigurableWebEnvironment) { + ((ConfigurableWebEnvironment) env).initPropertySources(sc, null); + } + + // 自定义上下文 + customizeContext(sc, wac); + // 调用SpringIOC容器的refresh()方法。 + wac.refresh(); +} +``` + +## 总结 + +对于Spring承载的Web应用而言,可以指定在Web应用程序启动时载入IOC容器(WebApplicationContext)。 + +这个载入的功能是通过ContextLoaderListener来实现的,它是一个Web容器的监听器,而ContextLoaderListener又通过ContextLoader来完成实际的WebApplicationContext的初始化,也就是IOC的初始化。 + +换句话说,ContextLoader就像Spring应用在Web容器中的启动器。 + diff --git "a/note/Spring/\346\267\261\345\205\245Spring\346\272\220\347\240\201\347\263\273\345\210\227\357\274\210\350\241\245\345\205\205\347\257\207\357\274\211\342\200\224\342\200\224\347\250\213\345\272\217\350\260\203\347\224\250Spring\346\272\220\347\240\201.md" "b/note/Spring/\346\267\261\345\205\245Spring\346\272\220\347\240\201\347\263\273\345\210\227\357\274\210\350\241\245\345\205\205\347\257\207\357\274\211\342\200\224\342\200\224\347\250\213\345\272\217\350\260\203\347\224\250Spring\346\272\220\347\240\201.md" new file mode 100644 index 0000000..e61a805 --- /dev/null +++ "b/note/Spring/\346\267\261\345\205\245Spring\346\272\220\347\240\201\347\263\273\345\210\227\357\274\210\350\241\245\345\205\205\347\257\207\357\274\211\342\200\224\342\200\224\347\250\213\345\272\217\350\260\203\347\224\250Spring\346\272\220\347\240\201.md" @@ -0,0 +1,49 @@ + +- [前言](#前言) +- [正文](#正文) + - [一、使用Spring源码的test测试文件来尝试启动Spring源码](#一使用spring源码的test测试文件来尝试启动spring源码) + - [二、解决问题](#二解决问题) + - [三、验证是否成功调用Spring源码](#三验证是否成功调用spring源码) + +## 前言 + +前段时间一直忙于其他事情,没空下来继续研究Spring源码。最近有空又重拾Spring源码,打算继续深入研究。 + +之前下载好了Spring源码之后,并成功导入到IDEA中了,可是光导入源码但是没有调用Spring源码就显得特别的菜了,且不利于深入学习Spring源码。本人花了点时间来尝试使用IDEA程序调用Spring源码,遂写下这篇文章来记录下实现过程。 + +## 正文 + +在系列文章第一篇中,已经成功下载好了Spring源码并使用Gradle导入源码,具体步骤这里不再赘述。 + +### 一、使用Spring源码的test测试文件来尝试启动Spring源码 + +如图操作: +![在这里插入图片描述](https://img-blog.csdnimg.cn/20190626095214471.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0NvZGVyQnJ1aXM=,size_16,color_FFFFFF,t_70) +启动test()方法,尝试调用Spring源码。结果会发现报错了 +``` +CoroutinesUtils cannot be resolved,没有CoroutinesUtils类 +``` +如下图位置: +![在这里插入图片描述](https://img-blog.csdnimg.cn/20190626095615609.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0NvZGVyQnJ1aXM=,size_16,color_FFFFFF,t_70) +(由于本人已经处理了这个错误,所以上图没有报出错误) + +### 二、解决问题 + +查了一下错误,发现是因为CoroutinesUtils是kotlin的一个工具类,Spring源码包读取不到。 + +使用如下方法即可解决该工具类读不到的问题: + + 1. 找到spring-framework-master\spring-core-coroutines\build\libs 下面的spring-core-coroutines-5.2.0.BUILD-SNAPSHOT.jar包 + 2. 选中改jar包右键——> Add as Library,将jar包导入依赖 + 3. 选中工具栏Build——> Rebuild Project + 等Spring源码重新构建之后,就可以使用本地程序调用Spring源码了。 + +### 三、验证是否成功调用Spring源码 +![在这里插入图片描述](https://img-blog.csdnimg.cn/20190626100508414.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0NvZGVyQnJ1aXM=,size_16,color_FFFFFF,t_70) +可以看到,在源码AbstractApplicationContext.java位置,本人添加了中文注释,一会儿使用Debug模式调用测试方法,看是否能进入该源码类的方法里,能进入则证明调用Spring源码成功。 + +![在这里插入图片描述](https://img-blog.csdnimg.cn/2019062610114672.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0NvZGVyQnJ1aXM=,size_16,color_FFFFFF,t_70) +![在这里插入图片描述](https://img-blog.csdnimg.cn/20190626101205966.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0NvZGVyQnJ1aXM=,size_16,color_FFFFFF,t_70) +![在这里插入图片描述](https://img-blog.csdnimg.cn/20190626101218898.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0NvZGVyQnJ1aXM=,size_16,color_FFFFFF,t_70) +![在这里插入图片描述](https://img-blog.csdnimg.cn/20190626101234310.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0NvZGVyQnJ1aXM=,size_16,color_FFFFFF,t_70) +这样,程序就成功地调用了Spring源码。接下来就可以愉快的研究Spring源码了:) \ No newline at end of file diff --git "a/note/SpringAOP/\346\267\261\345\205\245\345\255\246\344\271\240SpringAOP\346\272\220\347\240\201\357\274\210\344\270\200\357\274\211\342\200\224\342\200\224\346\263\250\345\206\214AnnotationAwareAspectJAutoProxyCreator.md" "b/note/SpringAOP/\346\267\261\345\205\245\345\255\246\344\271\240SpringAOP\346\272\220\347\240\201\357\274\210\344\270\200\357\274\211\342\200\224\342\200\224\346\263\250\345\206\214AnnotationAwareAspectJAutoProxyCreator.md" new file mode 100644 index 0000000..014dd27 --- /dev/null +++ "b/note/SpringAOP/\346\267\261\345\205\245\345\255\246\344\271\240SpringAOP\346\272\220\347\240\201\357\274\210\344\270\200\357\274\211\342\200\224\342\200\224\346\263\250\345\206\214AnnotationAwareAspectJAutoProxyCreator.md" @@ -0,0 +1,375 @@ +## 前言 + +版本:【Spring 5.1.4】、【SpringAOP 5.1.4】 + +原本想从网上博客学下SpringAOP源码逻辑,结果都看的我一头雾水,知识点都是东一块西一块的,根本都无法了解到SpringAOP源码逻辑的来龙去脉。本人干脆自己通过断点的方式,从SpringAOP源码一步一步打断点来学习SpringAOP源码,并通过系列博文的形式记录一下自己对SpringAOP的理解与总结。 + +下面来看下一个简单的SpringAOP程序: + +用到的类以及配置文件: +- TestBean.java //测试model +- AspectJTest.java //AOP实现 +- aspectTest.xml //AOP配置文件 +- TestMain //main测试方法入口 + +TestBean.java +```Java +public class TestBean { + private String testStr = "testStr"; + + public String getTestStr() { + return testStr; + } + + public void setTestStr(String testStr) { + this.testStr = testStr; + } + + public void test() { + System.out.println("test"); + } +} +``` + +AspectJTest.java +```Java +@Aspect +public class AspectJTest { + + /** + * 定义的切点 + */ + @Pointcut("execution(* *.test(..))") + public void test(){} + + /** + * 前置通知 + */ + @Before("test()") + public void beforeTest() { + System.out.println("beforeTest()..."); + } + + /** + * 后置通知 + */ + @After("test()") + public void after() { + System.out.println("afterTest()..."); + } + + /** + * 环绕通知 + * @param p + * @return + */ + @Around("test()") + public Object aroundTest(ProceedingJoinPoint p) { + System.err.println("beforeTest by @Around..."); + Object o = null; + try { + // 处理 + o = p.proceed(); + } catch (Throwable a) { + a.printStackTrace(); + } + System.err.println("afterTest by @Around..."); + return o; + } + +} +``` + +aspectTest.xml +``` + + + + + + + + + +``` +TestMain.java +```Java +public class TestMain { + public static void main(String[] args) { + ApplicationContext context = new ClassPathXmlApplicationContext("aspectTest.xml"); + TestBean bean = (TestBean)context.getBean("test"); + bean.test(); + } +} +``` + +运行结果如下: +``` +beforeTest by @Around... +beforeTest()... +test +afterTest()... +afterTest by @Around... +``` + +SpringAOP使用起来就是这么的简单、便捷,然而其内部实现逻辑是什么?相信大家都和我一样的好奇。是时候静下心来好好研究下SpringAOP源码了。 + + + + +在Spring中,通过短短的一句配置 +``` + +``` +然后配合着AspectJ的注解就可以使用SpringAOP,入门门槛极低,然而AOP内部的实现机制确实非常复杂,下面就来研究下。 + +## 正文 + +### 1、SpringAOP的核心类——AnnotationAwareAspectJAutoProxyCreator + +很多小伙伴都会纳闷,这SpringAOP源码的核心类或者入口在哪呢?入口都没找到就更别提研究了..... + +其实,只要鼠标点中SpringAOP配置文件里的,就会调转到spring-aop.xsd,如图: + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20190823101257861.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0NvZGVyQnJ1aXM=,size_16,color_FFFFFF,t_70) + +对,就是AnnotationAwareAspectJAutoProxyCreator实现了AOP逻辑。 + +我们通过配置 +``` + +``` +,可以让Spring自动完成对AnnotationAwareAspectJAutoProxyCreator类的注册,从而实现AOP的功能。在讲解AnnotationAwareAspectJAutoProxyCreator功能之前,先得知道它是怎么被注册到Spring中,并如何被解析到SringIOC中的?下面来看看Spring是如何注册AnnotationAwareAspectJAutoProxyCreator这个类。 + +### 2、Spring注册AnnotationAwareAspectJAutoProxyCreator + +说到Spring注册AnnotationAwareAspectJAutoProxyCreator,那要从SpringIOC加载META-INF/spring.handlers说起了。SpringIOC在初始化时,不仅从XML中获取了bean的定义信息,还从classpath下的META-INF/spring.handlers中获取到对应的Handler,可以从spring-aop的jar包中查看到该文件,文件内容如下: +``` +http\://www.springframework.org/schema/aop=org.springframework.aop.config.AopNamespaceHandler +``` + +断点打在类DefaultNamepsaceHandlerResolver的getHandlerMappings()方法上(具体的Spring源码这里不详细讲解),调试一波,如图: + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20190823101436550.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0NvZGVyQnJ1aXM=,size_16,color_FFFFFF,t_70) + +无图无真相啊,原来Spring将配置文件中的xmlns配置都解析成了一个一个Java命名解析器。回到我们的关注重点——AopNamespaceHandler,查看源码: +AopNamespaceHandler.class +```Java +public class AopNamespaceHandler extends NamespaceHandlerSupport { + public AopNamespaceHandler() { + } + + public void init() { + this.registerBeanDefinitionParser("config", new ConfigBeanDefinitionParser()); + this.registerBeanDefinitionParser("aspectj-autoproxy", new AspectJAutoProxyBeanDefinitionParser()); + this.registerBeanDefinitionDecorator("scoped-proxy", new ScopedProxyBeanDefinitionDecorator()); + this.registerBeanDefinitionParser("spring-configured", new SpringConfiguredBeanDefinitionParser()); + } +} +``` +可以看到,在init()方法里,Spring对aspectj-autoproxy也就是AnnotationAwareAspectJAutoProxyCreator进行了注册。在详细了解注册原理之前,先说明下在Spring中,所有的解析器都是对BeanDefinitionParser接口的同一实现: +```Java +public interface BeanDefinitionParser { + @Nullable + BeanDefinition parse(Element var1, ParserContext var2); +} +``` +解析入口都是从parse方法开始的。 +进入AspectJAutoProxyBeanDefinitionParser类中查看parse的实现逻辑: + +```Java +class AspectJAutoProxyBeanDefinitionParser implements BeanDefinitionParser { + ... + @Nullable + public BeanDefinition parse(Element element, ParserContext parserContext) { + AopNamespaceUtils.registerAspectJAnnotationAutoProxyCreatorIfNecessary(parserContext, element); + this.extendBeanDefinition(element, parserContext); + return null; + } + ... +} +``` + + + +AopNamspaceUtils +```Java +public abstract class AopNamespaceUtils { + public static final String PROXY_TARGET_CLASS_ATTRIBUTE = "proxy-target-class"; + private static final String EXPOSE_PROXY_ATTRIBUTE = "expose-proxy"; + ... + public static void registerAspectJAnnotationAutoProxyCreatorIfNecessary(ParserContext parserContext, Element sourceElement) { + //把应用了@Aspect注解修饰的bean注册成BeanDefinition + BeanDefinition beanDefinition = AopConfigUtils.registerAspectJAnnotationAutoProxyCreatorIfNecessary(parserContext.getRegistry(), parserContext.extractSource(sourceElement)); + //处理proxy-target-class和expose-proxy属性 + useClassProxyingIfNecessary(parserContext.getRegistry(), sourceElement); + //注册组件并通知解析器 + registerComponentIfNecessary(beanDefinition, parserContext); + } + + /* + * useClassProxyingIfNecessary实现了proxy-target-class和expose-proxy属性的处理逻辑。 + * + */ + private static void useClassProxyingIfNecessary(BeanDefinitionRegistry registry, @Nullable Element sourceElement) { + if (sourceElement != null) { + boolean proxyTargetClass = Boolean.parseBoolean(sourceElement.getAttribute("proxy-target-class")); + if (proxyTargetClass) { + AopConfigUtils.forceAutoProxyCreatorToUseClassProxying(registry); + } + + boolean exposeProxy = Boolean.parseBoolean(sourceElement.getAttribute("expose-proxy")); + if (exposeProxy) { + AopConfigUtils.forceAutoProxyCreatorToExposeProxy(registry); + } + } + + } + +``` + +```Java +public abstract class AopConfigUtils { + @Nullable + private static BeanDefinition registerOrEscalateApcAsRequired(Class cls, BeanDefinitionRegistry registry, @Nullable Object source) { + Assert.notNull(registry, "BeanDefinitionRegistry must not be null"); + if (registry.containsBeanDefinition("org.springframework.aop.config.internalAutoProxyCreator")) { + BeanDefinition apcDefinition = registry.getBeanDefinition("org.springframework.aop.config.internalAutoProxyCreator"); + if (!cls.getName().equals(apcDefinition.getBeanClassName())) { + int currentPriority = findPriorityForClass(apcDefinition.getBeanClassName()); + int requiredPriority = findPriorityForClass(cls); + if (currentPriority < requiredPriority) { + apcDefinition.setBeanClassName(cls.getName()); + } + } + + return null; + } else { + RootBeanDefinition beanDefinition = new RootBeanDefinition(cls); + beanDefinition.setSource(source); + beanDefinition.getPropertyValues().add("order", -2147483648); + beanDefinition.setRole(2); + registry.registerBeanDefinition("org.springframework.aop.config.internalAutoProxyCreator", beanDefinition); + return beanDefinition; + } + } +} +``` +registerOrEscalateApcAsRequired方法的作用就是获取AnnotationAwareAspectJAutoProxyCreator的BeanDefinition,然后根据优先级来装配这个BeanDefinition。获取到了AnnotationAwareAspectJAutoProxyCreator之后,接下来就要将配置信息和BeanDefinition一起注册到SpringIOC中。 +无图无真相,可以看到registerAspectJAnnotationAutoProxyCreatorIfNecessary方法返回的beanDefinition: +![在这里插入图片描述](https://img-blog.csdnimg.cn/20190823102123876.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0NvZGVyQnJ1aXM=,size_16,color_FFFFFF,t_70) + + +看看如果proxy-target-class和expose-proxy都为true时,代码的逻辑。 +```Java +public abstract class AopConfigUtils { + ... + /* + * 如果proxy-target-class为true,则走该方法的逻辑 + */ + public static void forceAutoProxyCreatorToUseClassProxying(BeanDefinitionRegistry registry) { + if (registry.containsBeanDefinition("org.springframework.aop.config.internalAutoProxyCreator")) { + BeanDefinition definition = registry.getBeanDefinition("org.springframework.aop.config.internalAutoProxyCreator"); + definition.getPropertyValues().add("proxyTargetClass", Boolean.TRUE); + } + + } + + /* + * 如果expose-proxy为true,则走该方法的逻辑 + */ + public static void forceAutoProxyCreatorToExposeProxy(BeanDefinitionRegistry registry) { + if (registry.containsBeanDefinition("org.springframework.aop.config.internalAutoProxyCreator")) { + BeanDefinition definition = registry.getBeanDefinition("org.springframework.aop.config.internalAutoProxyCreator"); + definition.getPropertyValues().add("exposeProxy", Boolean.TRUE); + } + + } +} +``` + +这又回到了熟悉的节奏,把bean的定义信息存放到了BeanDefinition中,Spring将设置的proxyTargetClass和exposeProxy的值存放到了BeanDefinition的PropertyValues里。 + +看到这里,很多小伙伴估计跟我刚学SpringAOP一样,对expose-proxy和proxy-target-class属性作用一头雾水,这里简单介绍下二者的作用: + +**proxy-target-class:** +1. 强制使用CGLIB,配置如下属性即可使用CGLIB + +2. 使用CGLIB代理和@AspectJ自动代理支持,配置如下配置即可实现 + + +**expose-proxy:** +1. 用于那种在在内部调用方法时无法调用切面逻辑的一种强制通知,这个后面做详细解释。 + +经过了useClassProxyingIfNecessary()方法的调用,ParserContext对象中存放好了注册的额外信息(proxy-target-class、expose-proxy值等),这里暂且将ParserContext称为解析上下文。由上面的源码可知,在AopNamespaceUtils类的registerAspectJAnnotationAutoProxyCreatorIfNecessary方法中,将获取的org.springframework.aop.config.internalAutoProxyCreator的BeanDefinition和解析上下文一起传入registerComponentIfNecessary方法中,进行Component组件注册。 + +在随后的registerComponentIfNecessary方法中,经过new BeanComponentDefinition()构造方法的调用,已经将AnnotationAwareAspectJAutoProxyCreator的BeanDefinition注册到了SpringIOC中。 +```Java +public abstract class AopConfigUtils { + ... + private static void registerComponentIfNecessary(@Nullable BeanDefinition beanDefinition, ParserContext parserContext) { + if (beanDefinition != null) { + parserContext.registerComponent(new BeanComponentDefinition(beanDefinition, "org.springframework.aop.config.internalAutoProxyCreator")); + } + + } +} +``` +```Java +public class BeanComponentDefinition extends BeanDefinitionHolder implements ComponentDefinition { + public BeanComponentDefinition(BeanDefinition beanDefinition, String beanName) { + this(new BeanDefinitionHolder(beanDefinition, beanName)); + } +} +``` +```Java +public class BeanDefinitionHolder implements BeanMetadataElement { + public BeanDefinitionHolder(BeanDefinition beanDefinition, String beanName) { + this(beanDefinition, beanName, (String[])null); + } + public BeanDefinitionHolder(BeanDefinition beanDefinition, String beanName, @Nullable String[] aliases) { + Assert.notNull(beanDefinition, "BeanDefinition must not be null"); + Assert.notNull(beanName, "Bean name must not be null"); + // 此处,将AnnotationAwareAspectJAutoProxyCreator的BeanDefinition赋值给了this.beanDefinition,这个this是包含在SpringIOC中的,这点相信看过 + this.beanDefinition = beanDefinition; + this.beanName = beanName; + this.aliases = aliases; + } +} +``` +然后一路返回,将BeanDefinition存放在解析上下文(ParserContext)中,并在AspectJAutoProxyBeanDefinitionParser类的extendBeanDefinition方法中取出。 +```Java +class AspectJAutoProxyBeanDefinitionParser implements BeanDefinitionParser { + private void extendBeanDefinition(Element element, ParserContext parserContext) { + BeanDefinition beanDef = parserContext.getRegistry().getBeanDefinition("org.springframework.aop.config.internalAutoProxyCreator"); + if (element.hasChildNodes()) { + this.addIncludePatterns(element, parserContext, beanDef); + } + + } +} +``` +打断点查看parserContext.getRegistry()获取注册清单,发现AnnotationAwareAspectJAutoProxyCreator就注册在了parserContext的注册清单里,而解析上下文又存放在SpringIOC(ApplicationContext,又称为Spring应用容器上下文)中。如图: +![在这里插入图片描述](https://img-blog.csdnimg.cn/20190823102443865.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0NvZGVyQnJ1aXM=,size_16,color_FFFFFF,t_70) +跳到AbstractApplicationContext的refresh()方法,发现AnnotationAwareAspectJAutoProxyCreator已经被注册到了SpringIOC容器中了。 +![在这里插入图片描述](https://img-blog.csdnimg.cn/20190823102917270.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0NvZGVyQnJ1aXM=,size_16,color_FFFFFF,t_70) + +至此,AnnotationAwareAspectJAutoProxyCreator的注册逻辑介绍到这里。下一篇,开始深入AnnotationAwareAspectJAutoProxyCreator学习。 + +### 3. 总结 + +1. 的作用是在Spring中注册AnnotationAwareAspectJAutoProxyCreator +2. 注册之后的解析工作是由AspectJAutoProxyBeanDefinitionParser类的.parse方法完成的 +3. 的proxy-target-class和expose-proxy属性会被注册到BeanDefinition的PropertyValues里 +4. proxy-target-class的作用: + 1)强制使用CGLIB,配置如下属性即可使用CGLIB ; + 2)使用CGLIB代理和@AspectJ自动代理支持,配置如下配置即可实现 + +5. expose-proxy的作用 + 用于那种在在内部调用方法时无法调用切面逻辑的一种强制通知 + +[深入学习SpringAOP源码(二)—— 深入AnnotationAwareAspectJAutoProxyCreator](https://blog.csdn.net/CoderBruis/article/details/100042081) +[深入学习SpringAOP源码(三)——揭开JDK动态代理和CGLIB代理的神秘面纱](https://blog.csdn.net/CoderBruis/article/details/100083575) diff --git "a/note/SpringAOP/\346\267\261\345\205\245\345\255\246\344\271\240SpringAOP\346\272\220\347\240\201\357\274\210\344\270\211\357\274\211\342\200\224\342\200\224\346\217\255\345\274\200JDK\345\212\250\346\200\201\344\273\243\347\220\206\345\222\214CGLIB\344\273\243\347\220\206\347\232\204\347\245\236\347\247\230\351\235\242\347\272\261.md" "b/note/SpringAOP/\346\267\261\345\205\245\345\255\246\344\271\240SpringAOP\346\272\220\347\240\201\357\274\210\344\270\211\357\274\211\342\200\224\342\200\224\346\217\255\345\274\200JDK\345\212\250\346\200\201\344\273\243\347\220\206\345\222\214CGLIB\344\273\243\347\220\206\347\232\204\347\245\236\347\247\230\351\235\242\347\272\261.md" new file mode 100644 index 0000000..026a120 --- /dev/null +++ "b/note/SpringAOP/\346\267\261\345\205\245\345\255\246\344\271\240SpringAOP\346\272\220\347\240\201\357\274\210\344\270\211\357\274\211\342\200\224\342\200\224\346\217\255\345\274\200JDK\345\212\250\346\200\201\344\273\243\347\220\206\345\222\214CGLIB\344\273\243\347\220\206\347\232\204\347\245\236\347\247\230\351\235\242\347\272\261.md" @@ -0,0 +1,617 @@ +## 前言 + +版本:【Spring 5.1.4】、【SpringAOP 5.1.4】 + +经过前两个章节的介绍,已经了解了Spring是如何注册解析AnnotationAwareAspectJAutoProxyCreator,然后AnnotationAwareAspectJAutoProxyCreator又是如何解析通知、创建代理,创建代理的目的又是什么呢?那么接下来本片文章将从深入解析源码的方式并借以《深入学习SpringAOP源码(一)》里Demo为例,来揭开JDK动态代理和CGLIB代理。 + +[深入学习SpringAOP源码(一)——注册AnnotationAwareAspectJAutoProxyCreator](https://blog.csdn.net/CoderBruis/article/details/100031756) +[深入学习SpringAOP源码(二)—— 深入AnnotationAwareAspectJAutoProxyCreator](https://blog.csdn.net/CoderBruis/article/details/100042081) + +## 正文 + +### 1. CGLIB动态代理 + +#### 1.1 引入简单的CGLIB例子 + +在讲解CGLIB动态代理之前,先看一下最简单的CGLIB动态代理的例子。 +```Java +import org.springframework.cglib.proxy.Enhancer; +import org.springframework.cglib.proxy.MethodInterceptor; +import org.springframework.cglib.proxy.MethodProxy; +import java.lang.reflect.Method; +public class EnhancerDemo { + public static void main(String[] args) { + Enhancer enhancer = new Enhancer(); + enhancer.setSuperclass(EnhancerDemo.class); + enhancer.setCallback(new MethodInterceptorImpl()); + EnhancerDemo demo = (EnhancerDemo) enhancer.create(); + demo.test(); + System.out.println(demo); + } + + public void test() { + System.out.println("EnhancerDemo test()"); + } + + private static class MethodInterceptorImpl implements MethodInterceptor { + @Override + public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable { + System.err.println("Before invoke " + method); + Object result = methodProxy.invokeSuper(o, objects); + System.err.println("After invoke" + method); + return result; + } + } +} +``` +运行结果如下: +``` +EnhancerDemo test() +After invokepublic void com.bruis.learnaop.testcglibaop.EnhancerDemo.test() +Before invoke public java.lang.String java.lang.Object.toString() +Before invoke public native int java.lang.Object.hashCode() +After invokepublic native int java.lang.Object.hashCode() +After invokepublic java.lang.String java.lang.Object.toString() +com.bruis.learnaop.testcglibaop.EnhancerDemo$$EnhancerByCGLIB$$413eae0d@53e25b76 +``` + +可以看到运行结果,除了demo.test()方法之外,System.our.println(demo)也被代理了,首先调用了toString()方法,然后又调用了hashCode,生成的对象为EnhancerDemo的实例,这个类是运行时由CGLIB产生的,Enhancer最关键的步骤就是setCallback()方法来设置拦截器,来拦截代理类的方法。Demo中用到的Enhancer是CGLIB的字节码增强器,用于为无接口的类创建代理proxy,方便对代理类进行拓展,Demo中的代理类就是EnhancerDemo。它的功能与java自带的Proxy类挺相似的,它会根据某个给定的类创建子类,并且非final的方法都带有回调方法。 + +创建代理对象的几个步骤: +- 生成代理类的二进制字节码文件 +- 加载二进制字节码,生成Class对象(例如使用Class.forName()方法) +- 通过反射机制获取实例构造,并创建代理类对象 + +具体可以查看Enhancer create()源码方法。 + +#### 1.2 深入代理逻辑源码 + +先用一张流程图来大致浏览下整个CGLIB的代码逻辑 +![在这里插入图片描述](https://img-blog.csdnimg.cn/201908261836428.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0NvZGVyQnJ1aXM=,size_16,color_FFFFFF,t_70) + +回到SpringAOP源码。在《深入学习SpringAOP源码(二)》中,介绍到DefaultAopProxyFactory源码部分 + +```Java +public class DefaultAopProxyFactory implements AopProxyFactory, Serializable { + public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException { + if (!config.isOptimize() && !config.isProxyTargetClass() && !this.hasNoUserSuppliedProxyInterfaces(config)) { + return new JdkDynamicAopProxy(config); + } else { + Class targetClass = config.getTargetClass(); + if (targetClass == null) { + throw new AopConfigException("TargetSource cannot determine target class: Either an interface or a target is required for proxy creation."); + } else { + return (AopProxy)(!targetClass.isInterface() && !Proxy.isProxyClass(targetClass) ? new ObjenesisCglibAopProxy(config) : new JdkDynamicAopProxy(config)); + } + } + } +} +``` +从createAopProxy()源码中可以看到,创建SpringAOP有两种方式,一、JDK动态代理;二、CGLIB动态代理;点进ObjenesisCglibAopProxy源码,发现它继承了CglibAopFactory +```Java +class ObjenesisCglibAopProxy extends CglibAopProxy { + protected Object createProxyClassAndInstance(Enhancer enhancer, Callback[] callbacks) { + // 通过增强器获取代理类的class对象 + Class proxyClass = enhancer.createClass(); + Object proxyInstance = null; + if (objenesis.isWorthTrying()) { + try { + // 创建代理类实例对象 + proxyInstance = objenesis.newInstance(proxyClass, enhancer.getUseCache()); + } catch (Throwable var7) { + logger.debug("Unable to instantiate proxy using Objenesis, falling back to regular proxy construction", var7); + } + } + + if (proxyInstance == null) { + try { + Constructor ctor = this.constructorArgs != null ? proxyClass.getDeclaredConstructor(this.constructorArgTypes) : proxyClass.getDeclaredConstructor(); + ReflectionUtils.makeAccessible(ctor); + proxyInstance = this.constructorArgs != null ? ctor.newInstance(this.constructorArgs) : ctor.newInstance(); + } catch (Throwable var6) { + throw new AopConfigException("Unable to instantiate proxy using Objenesis, and regular proxy instantiation via default constructor fails as well", var6); + } + } + + // 为代理类实例创建回调方法(拦截器链) + ((Factory)proxyInstance).setCallbacks(callbacks); + return proxyInstance; + } +} +``` +createProxyClassAndInstance方法和前面总结的CGLIB创建代理的步骤一样。 + +继续查看CglibAopProxy是如何准备Enhancer增强器以及创建拦截器链的。 +```Java +class CglibAopProxy implements AopProxy, Serializable { + public Object getProxy(@Nullable ClassLoader classLoader) { + if (logger.isTraceEnabled()) { + logger.trace("Creating CGLIB proxy: " + this.advised.getTargetSource()); + } + + try { + // 获取目标代理类 + Class rootClass = this.advised.getTargetClass(); + Assert.state(rootClass != null, "Target class must be available for creating a CGLIB proxy"); + Class proxySuperClass = rootClass; + int x; + if (ClassUtils.isCglibProxyClass(rootClass)) { + proxySuperClass = rootClass.getSuperclass(); + Class[] additionalInterfaces = rootClass.getInterfaces(); + Class[] var5 = additionalInterfaces; + int var6 = additionalInterfaces.length; + + for(x = 0; x < var6; ++x) { + Class additionalInterface = var5[x]; + this.advised.addInterface(additionalInterface); + } + } + // 验证class + this.validateClassIfNecessary(proxySuperClass, classLoader); + // 获取增强器 + Enhancer enhancer = this.createEnhancer(); + // 为Enhancer设置类加载器 + if (classLoader != null) { + enhancer.setClassLoader(classLoader); + if (classLoader instanceof SmartClassLoader && ((SmartClassLoader)classLoader).isClassReloadable(proxySuperClass)) { + enhancer.setUseCache(false); + } + } + + // 设置代理类,这一步很关键哦。 + enhancer.setSuperclass(proxySuperClass); + enhancer.setInterfaces(AopProxyUtils.completeProxiedInterfaces(this.advised)); + enhancer.setNamingPolicy(SpringNamingPolicy.INSTANCE); + // 设置strategy策略器 + enhancer.setStrategy(new CglibAopProxy.ClassLoaderAwareUndeclaredThrowableStrategy(classLoader)); + Callback[] callbacks = this.getCallbacks(rootClass); + Class[] types = new Class[callbacks.length]; + + for(x = 0; x < types.length; ++x) { + types[x] = callbacks[x].getClass(); + } + // 设置回调过滤器 + enhancer.setCallbackFilter(new CglibAopProxy.ProxyCallbackFilter(this.advised.getConfigurationOnlyCopy(), this.fixedInterceptorMap, this.fixedInterceptorOffset)); + enhancer.setCallbackTypes(types); + // 创建代理类实例,调用子类的createProxyClassAndInstance()方法 + return this.createProxyClassAndInstance(enhancer, callbacks); + } catch (IllegalArgumentException | CodeGenerationException var9) { + throw new AopConfigException("Could not generate CGLIB subclass of " + this.advised.getTargetClass() + ": Common causes of this problem include using a final class or a non-visible class", var9); + } catch (Throwable var10) { + throw new AopConfigException("Unexpected AOP exception", var10); + } + } + + // 获取回调方法 + private Callback[] getCallbacks(Class rootClass) throws Exception { + // 获取expose-proxy属性设置 + boolean exposeProxy = this.advised.isExposeProxy(); + boolean isFrozen = this.advised.isFrozen(); + boolean isStatic = this.advised.getTargetSource().isStatic(); + // 将aop拦截器封装在DynamicAdvisedInterceptor中 + Callback aopInterceptor = new CglibAopProxy.DynamicAdvisedInterceptor(this.advised); + Object targetInterceptor; + + if (exposeProxy) { + targetInterceptor = isStatic ? new CglibAopProxy.StaticUnadvisedExposedInterceptor(this.advised.getTargetSource().getTarget()) : new CglibAopProxy.DynamicUnadvisedExposedInterceptor(this.advised.getTargetSource()); + } else { + targetInterceptor = isStatic ? new CglibAopProxy.StaticUnadvisedInterceptor(this.advised.getTargetSource().getTarget()) : new CglibAopProxy.DynamicUnadvisedInterceptor(this.advised.getTargetSource()); + } + + Callback targetDispatcher = isStatic ? new CglibAopProxy.StaticDispatcher(this.advised.getTargetSource().getTarget()) : new CglibAopProxy.SerializableNoOp(); + // 添加主要的拦截器链 + Callback[] mainCallbacks = new Callback[]{aopInterceptor, (Callback)targetInterceptor, new CglibAopProxy.SerializableNoOp(), (Callback)targetDispatcher, this.advisedDispatcher, new CglibAopProxy.EqualsInterceptor(this.advised), new CglibAopProxy.HashCodeInterceptor(this.advised)}; + Callback[] callbacks; + if (isStatic && isFrozen) { + Method[] methods = rootClass.getMethods(); + Callback[] fixedCallbacks = new Callback[methods.length]; + this.fixedInterceptorMap = new HashMap(methods.length); + + for(int x = 0; x < methods.length; ++x) { + List chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(methods[x], rootClass); + fixedCallbacks[x] = new CglibAopProxy.FixedChainStaticTargetInterceptor(chain, this.advised.getTargetSource().getTarget(), this.advised.getTargetClass()); + this.fixedInterceptorMap.put(methods[x].toString(), x); + } + + callbacks = new Callback[mainCallbacks.length + fixedCallbacks.length]; + System.arraycopy(mainCallbacks, 0, callbacks, 0, mainCallbacks.length); + System.arraycopy(fixedCallbacks, 0, callbacks, mainCallbacks.length, fixedCallbacks.length); + this.fixedInterceptorOffset = mainCallbacks.length; + } else { + callbacks = mainCallbacks; + } + + return callbacks; + } +} +``` +拦截器链在CGLIB中扮演者重要角色,从上面源码中看出拦截器被封装为了DynamicAdvisedInterceptor,那么其核心逻辑就应该在DynamicAdvisedInterceptor中,那看看DynamicAdvisedInterceptor都做了哪些事情。 + +#### 1.3 DynamicAdvisedInterceptor都做了些啥工作? + +```Java + private static class DynamicAdvisedInterceptor implements MethodInterceptor, Serializable { + @Nullable + public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable { + Object oldProxy = null; + boolean setProxyContext = false; + Object target = null; + // 获取要拦截的通知源 + TargetSource targetSource = this.advised.getTargetSource(); + + Object var16; + try { + if (this.advised.exposeProxy) { + oldProxy = AopContext.setCurrentProxy(proxy); + setProxyContext = true; + } + + target = targetSource.getTarget(); + Class targetClass = target != null ? target.getClass() : null; + // 获取拦截器链,这里的拦截器链是啥?从哪获取拦截器链? + List chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass); + Object retVal; + if (chain.isEmpty() && Modifier.isPublic(method.getModifiers())) { + Object[] argsToUse = AopProxyUtils.adaptArgumentsIfNecessary(method, args); + // 如果拦截器链为空,则直接进入拦截器链 + retVal = methodProxy.invoke(target, argsToUse); + } else { + retVal = (new CglibAopProxy.CglibMethodInvocation(proxy, target, method, args, targetClass, chain, methodProxy)).proceed(); + } + + retVal = CglibAopProxy.processReturnType(proxy, target, method, retVal); + var16 = retVal; + } finally { + if (target != null && !targetSource.isStatic()) { + targetSource.releaseTarget(target); + } + + if (setProxyContext) { + AopContext.setCurrentProxy(oldProxy); + } + + } + + return var16; + } + } +``` + +#### 1.4 啥是拦截器链?拦截器链从哪获取? + +啥是拦截器链?从哪获取拦截器链?下面继续深入DefaultAdvisorChainFactory方法的getInterceptorsAndDynamicInterceptionAdvice()方法 +```Java +public class DefaultAdvisorChainFactory implements AdvisorChainFactory, Serializable { + public List getInterceptorsAndDynamicInterceptionAdvice(Advised config, Method method, @Nullable Class targetClass) { + /* + * 调用DefaultAdvisorAdapterRegistry构造方法获取通知适配器注册器,包括: + * 1. MethodBeforeAdviceAdapter + * 2. AfterReturningAdviceAdapter + * 3. ThrowsAdviceAdapter + * + * Adapter添加进List中的顺序就是上面的顺序。 + * + * GlobalAdvisorAdapterRegistry.getInstance()实际上就是去获取DefaultAdvisorAdapterRegistry中的Adapter + */ + AdvisorAdapterRegistry registry = GlobalAdvisorAdapterRegistry.getInstance(); + // config在这里就是前面所说的ProxyFactory。从ProxyFactory中获取通知 + Advisor[] advisors = config.getAdvisors(); + List interceptorList = new ArrayList(advisors.length); + Class actualClass = targetClass != null ? targetClass : method.getDeclaringClass(); + Boolean hasIntroductions = null; + Advisor[] var9 = advisors; + int var10 = advisors.length; + + for(int var11 = 0; var11 < var10; ++var11) { + Advisor advisor = var9[var11]; + // 切面型通知 + if (advisor instanceof PointcutAdvisor) { + // 将通知强转为切面 + PointcutAdvisor pointcutAdvisor = (PointcutAdvisor)advisor; + if (config.isPreFiltered() || pointcutAdvisor.getPointcut().getClassFilter().matches(actualClass)) { + MethodMatcher mm = pointcutAdvisor.getPointcut().getMethodMatcher(); + boolean match; + if (mm instanceof IntroductionAwareMethodMatcher) { + if (hasIntroductions == null) { + hasIntroductions = hasMatchingIntroductions(advisors, actualClass); + } + + match = ((IntroductionAwareMethodMatcher)mm).matches(method, actualClass, hasIntroductions); + } else { + match = mm.matches(method, actualClass); + } + + if (match) { + /* + * 通过通知适配注册器获取方法拦截器,这里返回的是四种拦截器,分别为: + * ExposeInvocationInterceptor类型、AspectJAfterAdvice类型、AspectJAroundAdvice类型、MethodBeforeAdviceInterceptor类型 + * 正好和前面系列文章一、二所描述的四种通知类型一致。 + */ + MethodInterceptor[] interceptors = registry.getInterceptors(advisor); + if (mm.isRuntime()) { + MethodInterceptor[] var17 = interceptors; + int var18 = interceptors.length; + + for(int var19 = 0; var19 < var18; ++var19) { + MethodInterceptor interceptor = var17[var19]; + interceptorList.add(new InterceptorAndDynamicMethodMatcher(interceptor, mm)); + } + } else { + interceptorList.addAll(Arrays.asList(interceptors)); + } + } + } + } else if (advisor instanceof IntroductionAdvisor) {// 接口型通知 + IntroductionAdvisor ia = (IntroductionAdvisor)advisor; + if (config.isPreFiltered() || ia.getClassFilter().matches(actualClass)) { + Interceptor[] interceptors = registry.getInterceptors(advisor); + interceptorList.addAll(Arrays.asList(interceptors)); + } + } else { + Interceptor[] interceptors = registry.getInterceptors(advisor); + interceptorList.addAll(Arrays.asList(interceptors)); + } + } + + /* + * 这里返回的拦截器链为: + * 1.ExposeInvocationInterceptor + * 2.AspectJAfterAdvice + * 3.AspectJAroundAdvice + * 4.MethodBeforeAdviceInterceptor + */ + return interceptorList; + } +} +``` +在DefaultAdvisorChainFactory的getInterceptorsAndDynamicInterceptionAdvice方法中,主要工作是: + +1. 先获取通知适配器注册器 +2. 将注册器包装为可用的拦截器 + + +在这过程中,DefaultAdvisorAdapterRegistry扮演者非常关键的角色。 +```Java +public class DefaultAdvisorAdapterRegistry implements AdvisorAdapterRegistry, Serializable { + private final List adapters = new ArrayList(3); + + // 在构造方法里注册前置通知、后置通知和异常通知的适配器, + public DefaultAdvisorAdapterRegistry() { + this.registerAdvisorAdapter(new MethodBeforeAdviceAdapter()); + this.registerAdvisorAdapter(new AfterReturningAdviceAdapter()); + this.registerAdvisorAdapter(new ThrowsAdviceAdapter()); + } + + // wrap方法在AbstractAutoProxyCreator的buildAdvisors方法中已经讲解到了,用于构建Advisor数组,这里就不再讲解 + public Advisor wrap(Object adviceObject) throws UnknownAdviceTypeException { + if (adviceObject instanceof Advisor) { + return (Advisor)adviceObject; + } else if (!(adviceObject instanceof Advice)) { + throw new UnknownAdviceTypeException(adviceObject); + } else { + Advice advice = (Advice)adviceObject; + if (advice instanceof MethodInterceptor) { + return new DefaultPointcutAdvisor(advice); + } else { + Iterator var3 = this.adapters.iterator(); + + AdvisorAdapter adapter; + do { + if (!var3.hasNext()) { + throw new UnknownAdviceTypeException(advice); + } + + adapter = (AdvisorAdapter)var3.next(); + } while(!adapter.supportsAdvice(advice)); + + return new DefaultPointcutAdvisor(advice); + } + } + } + + public MethodInterceptor[] getInterceptors(Advisor advisor) throws UnknownAdviceTypeException { + List interceptors = new ArrayList(3); + // 获取通知 + Advice advice = advisor.getAdvice(); + // 判断通知是否是MethodInterceptor类型 + if (advice instanceof MethodInterceptor) { + interceptors.add((MethodInterceptor)advice); + } + + Iterator var4 = this.adapters.iterator(); + + while(var4.hasNext()) { + // 将通知强转为AdvisorAdapter类型 + AdvisorAdapter adapter = (AdvisorAdapter)var4.next(); + if (adapter.supportsAdvice(advice)) { + interceptors.add(adapter.getInterceptor(advisor)); + } + } + + if (interceptors.isEmpty()) { + throw new UnknownAdviceTypeException(advisor.getAdvice()); + } else { + return (MethodInterceptor[])interceptors.toArray(new MethodInterceptor[0]); + } + } + + public void registerAdvisorAdapter(AdvisorAdapter adapter) { + this.adapters.add(adapter); + } +} +``` + +DefaultAdvisorAdapterRegistry类主要负责: +- 在构造方法里注册前置通知、后置通知和异常通知的适配器 +- 包装Advisor +- 将Advisor包装为拦截器 + +既然获取到了拦截器链,那么每个拦截器链都做了些啥呢?回到DynamicAdvisedInterceptor的intercept()方法 + +#### 1.5 调用拦截器链的proceed方法 + +视线回到DynamicAdvisedInterceptor的intercept方法,在 +```Java +List chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass); +``` +执行完成之后,chain中存放好了拦截器链,分别是 +1. ExposeInvocationInterceptor +2. AspectJAfterAdvice +3. AspectJAroundAdvice +4. MethodBeforeAdviceInterceptor + +```Java + List chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass); + Object retVal; + if (chain.isEmpty() && Modifier.isPublic(method.getModifiers())) { + Object[] argsToUse = AopProxyUtils.adaptArgumentsIfNecessary(method, args); + // 如果拦截器链为空,则直接进入拦截器链 + retVal = methodProxy.invoke(target, argsToUse); + } else { + // 调用拦截器链的proceed方法 + retVal = (new CglibAopProxy.CglibMethodInvocation(proxy, target, method, args, targetClass, chain, methodProxy)).proceed(); + } + retVal = CglibAopProxy.processReturnType(proxy, target, method, retVal); + var16 = retVal; +``` + +**后置通知实现逻辑:** + +```Java +public class AspectJAfterAdvice extends AbstractAspectJAdvice implements MethodInterceptor, AfterAdvice, Serializable { + public Object invoke(MethodInvocation mi) throws Throwable { + Object var2; + try { + var2 = mi.proceed(); + } finally { + this.invokeAdviceMethod(this.getJoinPointMatch(), (Object)null, (Throwable)null); + } + + return var2; + } +} +``` + +```Java +public class ReflectiveMethodInvocation implements ProxyMethodInvocation, Cloneable { + + protected final Object proxy; + @Nullable + protected final Object target; + protected final Method method; + protected Object[] arguments = new Object[0]; + @Nullable + private final Class targetClass; + @Nullable + private Map userAttributes; + protected final List interceptorsAndDynamicMethodMatchers; + private int currentInterceptorIndex = -1; + + + @Nullable + public Object proceed() throws Throwable { + + if (this.currentInterceptorIndex == this.interceptorsAndDynamicMethodMatchers.size() - 1) { + return this.invokeJoinpoint(); + } else { + // 获取拦截器链的元素 + Object interceptorOrInterceptionAdvice = this.interceptorsAndDynamicMethodMatchers.get(++this.currentInterceptorIndex); + if (interceptorOrInterceptionAdvice instanceof InterceptorAndDynamicMethodMatcher) { + InterceptorAndDynamicMethodMatcher dm = (InterceptorAndDynamicMethodMatcher)interceptorOrInterceptionAdvice; + Class targetClass = this.targetClass != null ? this.targetClass : this.method.getDeclaringClass(); + return dm.methodMatcher.matches(this.method, targetClass, this.arguments) ? dm.interceptor.invoke(this) : this.proceed(); + } else { + return ((MethodInterceptor)interceptorOrInterceptionAdvice).invoke(this); + } + } + } +} +``` +这里总结下proceed的逻辑: +1. 根据索引值获取拦截器链中的拦截器 +2. 要么调用拦截器的invoke方法,要么就调用proceed进行下一轮的递归 +3. proceed方法在这里起到了递归的作用 + + +**环绕通知实现逻辑:** + +```Java +public class AspectJAroundAdvice extends AbstractAspectJAdvice implements MethodInterceptor, Serializable { + public Object invoke(MethodInvocation mi) throws Throwable { + if (!(mi instanceof ProxyMethodInvocation)) { + throw new IllegalStateException("MethodInvocation is not a Spring ProxyMethodInvocation: " + mi); + } else { + ProxyMethodInvocation pmi = (ProxyMethodInvocation)mi; + ProceedingJoinPoint pjp = this.lazyGetProceedingJoinPoint(pmi); + JoinPointMatch jpm = this.getJoinPointMatch(pmi); + return this.invokeAdviceMethod(pjp, jpm, (Object)null, (Throwable)null); + } + } + + protected ProceedingJoinPoint lazyGetProceedingJoinPoint(ProxyMethodInvocation rmi) { + return new MethodInvocationProceedingJoinPoint(rmi); + } +} +``` +```Java +public abstract class AbstractAspectJAdvice implements Advice, AspectJPrecedenceInformation, Serializable { + protected Object invokeAdviceMethod(JoinPoint jp, @Nullable JoinPointMatch jpMatch, @Nullable Object returnValue, @Nullable Throwable t) throws Throwable { + return this.invokeAdviceMethodWithGivenArgs(this.argBinding(jp, jpMatch, returnValue, t)); + } + + protected Object invokeAdviceMethodWithGivenArgs(Object[] args) throws Throwable { + Object[] actualArgs = args; + if (this.aspectJAdviceMethod.getParameterCount() == 0) { + actualArgs = null; + } + + try { + ReflectionUtils.makeAccessible(this.aspectJAdviceMethod); + return this.aspectJAdviceMethod.invoke(this.aspectInstanceFactory.getAspectInstance(), actualArgs); + } catch (IllegalArgumentException var4) { + throw new AopInvocationException("Mismatch on arguments to advice method [" + this.aspectJAdviceMethod + "]; pointcut expression [" + this.pointcut.getPointcutExpression() + "]", var4); + } catch (InvocationTargetException var5) { + throw var5.getTargetException(); + } + } +} +``` +总结下: +1. AspectJAroundAdvice的invoke方法作用为获取代理方法以及正在处理的切点对象 +2. 将代理方法、切点信息传入AbstractAspectJAdvice的invokeAdviceMethod里进行进一步的参数绑定 +3. invokeAdviceMethodWithGivenArgs方法调用aspectJAdviceMethod.invoke方法,调用AspectJTest类中aroundTest方法 + +**前置通知实现逻辑:** +```Java +public class MethodBeforeAdviceInterceptor implements MethodInterceptor, BeforeAdvice, Serializable { + private final MethodBeforeAdvice advice; + + public Object invoke(MethodInvocation mi) throws Throwable { + this.advice.before(mi.getMethod(), mi.getArguments(), mi.getThis()); + return mi.proceed(); + } +} + +public class AspectJMethodBeforeAdvice extends AbstractAspectJAdvice implements MethodBeforeAdvice, Serializable { + public void before(Method method, Object[] args, @Nullable Object target) throws Throwable { + this.invokeAdviceMethod(this.getJoinPointMatch(), (Object)null, (Throwable)null); + } +} +``` +可以注意到: +1. 代理类的before方法是由AspectJMethodBeforeAdvice类before方法执行的 +2. 在before方法执行完后,调用了MethodInvocation的proceed方法,最终是回到了AspectJAfterAdvice的invoke方法 + + +这整个过程随着AspectJAfterAdvice执行完,整个Demo代码也都走完了。虽然结合着文章开头的时序图,辅以源码来学习整个SpringAOP的运作过程,但也并不能把整个过程描述的非常清楚,下面以本人非常喜欢的一种方式把整个过程展开来。 + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20190826183553906.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0NvZGVyQnJ1aXM=,size_16,color_FFFFFF,t_70) +![在这里插入图片描述](https://img-blog.csdnimg.cn/20190826183604716.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0NvZGVyQnJ1aXM=,size_16,color_FFFFFF,t_70) +![在这里插入图片描述](https://img-blog.csdnimg.cn/20190826183615646.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0NvZGVyQnJ1aXM=,size_16,color_FFFFFF,t_70) +以上面这种文本方式,结合着时序图,能够进一步加深对CGLIB源码逻辑的理解。 + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20190826183834615.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0NvZGVyQnJ1aXM=,size_16,color_FFFFFF,t_70) + +## 2. JDK动态代理 + +未完待续... diff --git "a/note/SpringAOP/\346\267\261\345\205\245\345\255\246\344\271\240SpringAOP\346\272\220\347\240\201\357\274\210\344\272\214\357\274\211\342\200\224\342\200\224 \346\267\261\345\205\245AnnotationAwareAspectJAutoProxyCreator.md" "b/note/SpringAOP/\346\267\261\345\205\245\345\255\246\344\271\240SpringAOP\346\272\220\347\240\201\357\274\210\344\272\214\357\274\211\342\200\224\342\200\224 \346\267\261\345\205\245AnnotationAwareAspectJAutoProxyCreator.md" new file mode 100644 index 0000000..034a001 --- /dev/null +++ "b/note/SpringAOP/\346\267\261\345\205\245\345\255\246\344\271\240SpringAOP\346\272\220\347\240\201\357\274\210\344\272\214\357\274\211\342\200\224\342\200\224 \346\267\261\345\205\245AnnotationAwareAspectJAutoProxyCreator.md" @@ -0,0 +1,713 @@ +## 前言 + +版本:【Spring 5.1.4】、【SpringAOP 5.1.4】 + +经过博文[深入学习SpringAOP源码(一)—— 注册AnnotationAwareAspectJAutoProxyCreator](https://blog.csdn.net/CoderBruis/article/details/100031756)的介绍之后,相信大家都了解到了AnnotationAwareAspectJAutoProxyCreator试如何被解析然后注册到SpringIOC中的。接下来开始深入学习AnnotationAwareAspectJAutoProxyCreator源码了。 + +【没看过深入学习SpringAOP源码(一)的小伙伴最好先去看下,不然看完文章之后可能会很懵】 + +## 正文 + +### 1. 学习AnnotationAwareAspectJAutoProxyCreator + +先看下AnnotationAwareAspectJAutoProxyCreator的类结构图,如下: + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20190823164851812.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0NvZGVyQnJ1aXM=,size_16,color_FFFFFF,t_70) + +可以看到,AnnotationAwareAspectJAutoProxyCreator实现了BeanPostProcessor,相信大家都知道,实现了BeanPostProcessor之后,要去实现它的两个重要的方法:postProcessBeforeInitialization和postProcessAfterInitialization,在这里,而具体实现AOP逻辑的是方法postProcessAfterInitialization()。 + +找了一圈,都没有在AnnotationAwareAspectJAutoProxyCreator中找到postProcessAfterInitialization()方法,看上面的类结构图,挨着找了AspectJAwareAdvisorAutoProxyCreator、AbstractAdvisorAutoProxyCreator和AbstractAutoProxyCreator,才发现postProcessAfterInitialization()和postProcessAfterInitialization()实现类在AbstractAutoProxyCreator类里。看过Spring源码的小伙伴们,都应该知道Spring源码的风格就是一层包着一层,每一层代码的职责都不一样,习惯就好,小伙伴们也不要被这看似很长的类名给吓到了,多看几遍就顺眼了。 + +了解Spring源码或者看过本人深入Spring系列博文都知道,SpringIOC的refresh()方法包含了许多逻辑,其中在finishBeanFactoryInitialization()方法中,开始了解析AnnotationAwareAspectJAutoProxyCreator的工作。 + +接下来方法跳转的地方有点多,所以制作了流程图帮助我们更快理解Spring如何解析通知Advisor +![在这里插入图片描述](https://img-blog.csdnimg.cn/20190826165617472.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0NvZGVyQnJ1aXM=,size_16,color_FFFFFF,t_70) +![在这里插入图片描述](https://img-blog.csdnimg.cn/20190826165630570.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0NvZGVyQnJ1aXM=,size_16,color_FFFFFF,t_70) + + +将视线转移到AbstractAutowireCapableBeanFactory: +```Java +public abstract class AbstractAutowireCapableBeanFactory extends AbstractBeanFactory implements AutowireCapableBeanFactory { + // 在实例化AnnotationAwareAspectJAutoProxyCreator之前进行解析 + @Nullable + protected Object resolveBeforeInstantiation(String beanName, RootBeanDefinition mbd) { + Object bean = null; + if (!Boolean.FALSE.equals(mbd.beforeInstantiationResolved)) { + if (!mbd.isSynthetic() && this.hasInstantiationAwareBeanPostProcessors()) { + Class targetType = this.determineTargetType(beanName, mbd); + if (targetType != null) { + // 调用AnnotationAwareAspectJAutoProxyCreator的postProcessBeforeInitialization() + bean = this.applyBeanPostProcessorsBeforeInstantiation(targetType, beanName); + if (bean != null) { + // 调用调用AnnotationAwareAspectJAutoProxyCreator的postProcessAfterInitialization() + bean = this.applyBeanPostProcessorsAfterInitialization(bean, beanName); + } + } + } + + mbd.beforeInstantiationResolved = bean != null; + } + + return bean; + } +} +``` +resolveBeforeInstantiation()方法调用了AbstractAutoProxyCreator()的postProcessBeforeInstantiation()和postProcessAfterInstantiation()。 + +AbstractAutoProxyCreator.class +```Java +import ... +public abstract class AbstractAutoProxyCreator extends ProxyProcessorSupport implements SmartInstantiationAwareBeanPostProcessor, BeanFactoryAware { + /* + * postProcessBeforeInstantiation方法中,以shoudSkip()为入口,完成了 + * AnnotationAwareAspectJAutoProxyCreator的获取通知注解的底层方法 + * 1. 从SpringIOC中筛选出@AspectJ注解修饰的类 + * 2. 通过反射获取该类的所有方法 + * 3. 解析所有通知,包装为InstantiationModelAwarePointcutAdvisorImpl类型,存放在List中 + * 4. 将解析的所有通知存放在advisorsCache中(Map>类型),方便postProcessAfterInstantiation调用 + * + * + */ + public Object postProcessBeforeInstantiation(Class beanClass, String beanName) { + // 查看缓存中是否有通知的key + Object cacheKey = this.getCacheKey(beanClass, beanName); + if (!StringUtils.hasLength(beanName) || !this.targetSourcedBeans.contains(beanName)) { + if (this.advisedBeans.containsKey(cacheKey)) { + return null; + } + + if (this.isInfrastructureClass(beanClass) || this.shouldSkip(beanClass, beanName)) { + this.advisedBeans.put(cacheKey, Boolean.FALSE); + return null; + } + } + + TargetSource targetSource = this.getCustomTargetSource(beanClass, beanName); + if (targetSource != null) { + if (StringUtils.hasLength(beanName)) { + this.targetSourcedBeans.add(beanName); + } + + Object[] specificInterceptors = this.getAdvicesAndAdvisorsForBean(beanClass, beanName, targetSource); + Object proxy = this.createProxy(beanClass, beanName, specificInterceptors, targetSource); + this.proxyTypes.put(cacheKey, proxy.getClass()); + return proxy; + } else { + return null; + } + } + + public Object postProcessAfterInitialization(@Nullable Object bean, String beanName) { + if (bean != null) { + Object cacheKey = this.getCacheKey(bean.getClass(), beanName); + if (!this.earlyProxyReferences.contains(cacheKey)) { + return this.wrapIfNecessary(bean, beanName, cacheKey); + } + } + + return bean; + } + + protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) { + if (StringUtils.hasLength(beanName) && this.targetSourcedBeans.contains(beanName)) { + return bean; + } else if (Boolean.FALSE.equals(this.advisedBeans.get(cacheKey))) { + return bean; + } else if (!this.isInfrastructureClass(bean.getClass()) && !this.shouldSkip(bean.getClass(), beanName)) { + // 获取Bean的通知 + Object[] specificInterceptors = this.getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, (TargetSource)null); + // 如果需要进行代理,则创建代理 + if (specificInterceptors != DO_NOT_PROXY) { + this.advisedBeans.put(cacheKey, Boolean.TRUE); + // 创建代理 + Object proxy = this.createProxy(bean.getClass(), beanName, specificInterceptors, new SingletonTargetSource(bean)); + this.proxyTypes.put(cacheKey, proxy.getClass()); + return proxy; + } else { + this.advisedBeans.put(cacheKey, Boolean.FALSE); + return bean; + } + } else { + this.advisedBeans.put(cacheKey, Boolean.FALSE); + return bean; + } + } +} +``` + + +#### 1.1 通过反射工具获取通知 + +这里跳过中间过程,直接看看底层是如何获得通知,并将通知包装成InstantiationModelAwarePointcutAdvisorI + + +ReflectiveAspectJAdvisorFactory的getAdvisors()中主要的工作是:迭代出@AspectJ注解修饰的类的方法,然后拿着这些方法区尝试获取Advisor,最后存在advisors集合里。 +```Java +public class ReflectiveAspectJAdvisorFactory extends AbstractAspectJAdvisorFactory implements Serializable { + //Spring将@AspectJ注解的beanName和bean工厂封装为了MetadataAwareAspectInstanceFactory + public List getAdvisors(MetadataAwareAspectInstanceFactory aspectInstanceFactory) { + Class aspectClass = aspectInstanceFactory.getAspectMetadata().getAspectClass(); + String aspectName = aspectInstanceFactory.getAspectMetadata().getAspectName(); + this.validate(aspectClass); + MetadataAwareAspectInstanceFactory lazySingletonAspectInstanceFactory = new LazySingletonAspectInstanceFactoryDecorator(aspectInstanceFactory); + // 用于存放通知 + List advisors = new ArrayList(); + Iterator var6 = this.getAdvisorMethods(aspectClass).iterator(); + + while(var6.hasNext()) { + // 获取带有@AspectJ注解的类的方法 + Method method = (Method)var6.next(); + // 获取这些方法上带有的通知,如果不为空则添加进advisors里 + Advisor advisor = this.getAdvisor(method, lazySingletonAspectInstanceFactory, advisors.size(), aspectName); + if (advisor != null) { + advisors.add(advisor); + } + } + + if (!advisors.isEmpty() && lazySingletonAspectInstanceFactory.getAspectMetadata().isLazilyInstantiated()) { + Advisor instantiationAdvisor = new ReflectiveAspectJAdvisorFactory.SyntheticInstantiationAdvisor(lazySingletonAspectInstanceFactory); + advisors.add(0, instantiationAdvisor); + } + + Field[] var12 = aspectClass.getDeclaredFields(); + int var13 = var12.length; + + for(int var14 = 0; var14 < var13; ++var14) { + Field field = var12[var14]; + Advisor advisor = this.getDeclareParentsAdvisor(field); + if (advisor != null) { + advisors.add(advisor); + } + } + + return advisors; + } +} +``` + +getAdvisorMethods方法中通过反射工具来获取Advisor方法。 +```Java +public class ReflectiveAspectJAdvisorFactory extends AbstractAspectJAdvisorFactory implements Serializable { + private List getAdvisorMethods(Class aspectClass) { + List methods = new ArrayList(); + ReflectionUtils.doWithMethods(aspectClass, (method) -> { + if (AnnotationUtils.getAnnotation(method, Pointcut.class) == null) { + methods.add(method); + } + + }); + methods.sort(METHOD_COMPARATOR); + return methods; + } +} +``` + +视线来到ReflectiveAspectJAdvisorFactory的getAdvisor方法 +```Java + @Nullable + public Advisor getAdvisor(Method candidateAdviceMethod, MetadataAwareAspectInstanceFactory aspectInstanceFactory, int declarationOrderInAspect, String aspectName) { + this.validate(aspectInstanceFactory.getAspectMetadata().getAspectClass()); + AspectJExpressionPointcut expressionPointcut = this.getPointcut(candidateAdviceMethod, aspectInstanceFactory.getAspectMetadata().getAspectClass()); + return expressionPointcut == null ? null : new InstantiationModelAwarePointcutAdvisorImpl(expressionPointcut, candidateAdviceMethod, this, aspectInstanceFactory, declarationOrderInAspect, aspectName); + } + + @Nullable + private AspectJExpressionPointcut getPointcut(Method candidateAdviceMethod, Class candidateAspectClass) { + // 找到方法的通知 + AspectJAnnotation aspectJAnnotation = AbstractAspectJAdvisorFactory.findAspectJAnnotationOnMethod(candidateAdviceMethod); + if (aspectJAnnotation == null) { + return null; + } else { + AspectJExpressionPointcut ajexp = new AspectJExpressionPointcut(candidateAspectClass, new String[0], new Class[0]); + ajexp.setExpression(aspectJAnnotation.getPointcutExpression()); + if (this.beanFactory != null) { + ajexp.setBeanFactory(this.beanFactory); + } + + return ajexp; + } + } +``` +总结下getAdvisor的作用: +1. 获取切面 +2. 将切面、通知方法、aspectName等包装成InstantiationModelAwarePointcutAdvisorImpl实例 +3. Advisor是InstantiationModelAwarePointcutAdvisorImpl的父类 +4. getAdvisor方法将InstantiationModelAwarePointcutAdvisorImpl一路返回,然后存放在advisor的集合中 +5. 将advisor存放在缓存中 + +#### 1.2 获取通知,筛选通知 + + +回到AbstractAutoProxyCreator中查看其wrapIfNecessary方法,可以简单总结为两步: + +1. 从缓存中获取通知 +2. 创建代理 + +```Java + protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) { + if (StringUtils.hasLength(beanName) && this.targetSourcedBeans.contains(beanName)) { + return bean; + } else if (Boolean.FALSE.equals(this.advisedBeans.get(cacheKey))) { + return bean; + } else if (!this.isInfrastructureClass(bean.getClass()) && !this.shouldSkip(bean.getClass(), beanName)) { + Object[] specificInterceptors = this.getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, (TargetSource)null); + if (specificInterceptors != DO_NOT_PROXY) { + // 从缓存中获取通知 + this.advisedBeans.put(cacheKey, Boolean.TRUE); + // 创建代理 + Object proxy = this.createProxy(bean.getClass(), beanName, specificInterceptors, new SingletonTargetSource(bean)); + this.proxyTypes.put(cacheKey, proxy.getClass()); + return proxy; + } else { + this.advisedBeans.put(cacheKey, Boolean.FALSE); + return bean; + } + } else { + this.advisedBeans.put(cacheKey, Boolean.FALSE); + return bean; + } + } +``` + +```Java +public abstract class AbstractAdvisorAutoProxyCreator extends AbstractAutoProxyCreator { + ... + @Nullable + protected Object[] getAdvicesAndAdvisorsForBean(Class beanClass, String beanName, @Nullable TargetSource targetSource) { + List advisors = this.findEligibleAdvisors(beanClass, beanName); + return advisors.isEmpty() ? DO_NOT_PROXY : advisors.toArray(); + } + + /* + * 查询可用的通知 + */ + protected List findEligibleAdvisors(Class beanClass, String beanName) { + /* + * 获取通知,this.findCandidateAdvisors()调用的是AnnotationAwareAspectJAutoProxyCreator的方法 + */ + List candidateAdvisors = this.findCandidateAdvisors(); + /* + * 获取可用的通知 + */ + List eligibleAdvisors = this.findAdvisorsThatCanApply(candidateAdvisors, beanClass, beanName); + this.extendAdvisors(eligibleAdvisors); + if (!eligibleAdvisors.isEmpty()) { + eligibleAdvisors = this.sortAdvisors(eligibleAdvisors); + } + return eligibleAdvisors; + } +} +``` + +```Java +public class AnnotationAwareAspectJAutoProxyCreator extends AspectJAwareAdvisorAutoProxyCreator { + protected List findCandidateAdvisors() { + /* + * 在这里调用父类方法加载配置文件中的AOP声明。在这里调用父类方法加载配置文件中的AOP声明。AnnotationAwareAspectJAutoProxyCreator间接 + * 继承了AbstractAdvisorsAutoProxyCreator,在实现获取通知的方法中除了保留了父类的获取配置文件中定义的通知外, + * 同时还添加了获取Bean的注解通知的功能,这个功能就是下面的this.apectJAdvisorsBuilder....实现的 + * + */ + List advisors = super.findCandidateAdvisors(); + if (this.aspectJAdvisorsBuilder != null) { + advisors.addAll(this.aspectJAdvisorsBuilder.buildAspectJAdvisors()); + } + return advisors; + } +} +``` + +```Java +public class BeanFactoryAspectJAdvisorsBuilder { + ... + public List buildAspectJAdvisors() { + // 这里将返回aspectJTest的类名 + List aspectNames = this.aspectBeanNames; + if (aspectNames == null) { + synchronized(this) { + aspectNames = this.aspectBeanNames; + if (aspectNames == null) { + List advisors = new ArrayList(); + List aspectNames = new ArrayList(); + String[] beanNames = BeanFactoryUtils.beanNamesForTypeIncludingAncestors(this.beanFactory, Object.class, true, false); + String[] var18 = beanNames; + int var19 = beanNames.length; + + for(int var7 = 0; var7 < var19; ++var7) { + String beanName = var18[var7]; + if (this.isEligibleBean(beanName)) { + // 获取bean类型 + Class beanType = this.beanFactory.getType(beanName); + if (beanType != null && this.advisorFactory.isAspect(beanType)) { + // 将@AspectJ注解的bean的beanName存放在aspectNames集合中 + aspectNames.add(beanName); + AspectMetadata amd = new AspectMetadata(beanType, beanName); + if (amd.getAjType().getPerClause().getKind() == PerClauseKind.SINGLETON) { + MetadataAwareAspectInstanceFactory factory = new BeanFactoryAspectInstanceFactory(this.beanFactory, beanName); + // 获取通知类型,这里获取的是after、before和around通知 + List classAdvisors = this.advisorFactory.getAdvisors(factory); + if (this.beanFactory.isSingleton(beanName)) { + this.advisorsCache.put(beanName, classAdvisors); + } else { + this.aspectFactoryCache.put(beanName, factory); + } + + advisors.addAll(classAdvisors); + } else { + if (this.beanFactory.isSingleton(beanName)) { + throw new IllegalArgumentException("Bean with name '" + beanName + "' is a singleton, but aspect instantiation model is not singleton"); + } + + MetadataAwareAspectInstanceFactory factory = new PrototypeAspectInstanceFactory(this.beanFactory, beanName); + this.aspectFactoryCache.put(beanName, factory); + advisors.addAll(this.advisorFactory.getAdvisors(factory)); + } + } + } + } + + this.aspectBeanNames = aspectNames; + return advisors; + } + } + } + + if (aspectNames.isEmpty()) { + return Collections.emptyList(); + } else { + List advisors = new ArrayList(); + Iterator var3 = aspectNames.iterator(); + + while(var3.hasNext()) { + String aspectName = (String)var3.next(); + List cachedAdvisors = (List)this.advisorsCache.get(aspectName); + if (cachedAdvisors != null) { + advisors.addAll(cachedAdvisors); + } else { + MetadataAwareAspectInstanceFactory factory = (MetadataAwareAspectInstanceFactory)this.aspectFactoryCache.get(aspectName); + advisors.addAll(this.advisorFactory.getAdvisors(factory)); + } + } + + return advisors; + } + } +} +``` +![在这里插入图片描述](https://img-blog.csdnimg.cn/20190823164956637.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0NvZGVyQnJ1aXM=,size_16,color_FFFFFF,t_70) +获取通知之后的结果,如图与AspectJTest设置的是三种通知类型相同 +![在这里插入图片描述](https://img-blog.csdnimg.cn/20190823164940860.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0NvZGVyQnJ1aXM=,size_16,color_FFFFFF,t_70) + +回到方法 +```Java +protected List findEligibleAdvisors(Class beanClass, String beanName) { + List candidateAdvisors = this.findCandidateAdvisors(); + /* + * 获取可用的通知 + */ + List eligibleAdvisors = this.findAdvisorsThatCanApply(candidateAdvisors, beanClass, beanName); + this.extendAdvisors(eligibleAdvisors); + if (!eligibleAdvisors.isEmpty()) { + eligibleAdvisors = this.sortAdvisors(eligibleAdvisors); + } + + return eligibleAdvisors; + } +``` + +findCandidateAdvisors()完成的是通知的解析工作,但是并不是所有的通知都适用于当前bean的,还要选出适合的通知。选择逻辑在findAdvisorsTahtCanApply方法里。 +```Java +public abstract class AbstractAdvisorAutoProxyCreator extends AbstractAutoProxyCreator { + ... + protected List findAdvisorsThatCanApply(List candidateAdvisors, Class beanClass, String beanName) { + ProxyCreationContext.setCurrentProxiedBeanName(beanName); + + List var4; + try { + var4 = AopUtils.findAdvisorsThatCanApply(candidateAdvisors, beanClass); + } finally { + ProxyCreationContext.setCurrentProxiedBeanName((String)null); + } + + return var4; + } +} +``` + +```Java +public abstract class AopUtils { + public static List findAdvisorsThatCanApply(List candidateAdvisors, Class clazz) { + if (candidateAdvisors.isEmpty()) { + return candidateAdvisors; + } else { + List eligibleAdvisors = new ArrayList(); + // 迭代出candiateAdvisors里的通知,包括after、before和around通知 + Iterator var3 = candidateAdvisors.iterator(); + + while(var3.hasNext()) { + Advisor candidate = (Advisor)var3.next(); + if (candidate instanceof IntroductionAdvisor && canApply(candidate, clazz)) { + eligibleAdvisors.add(candidate); + } + } + + boolean hasIntroductions = !eligibleAdvisors.isEmpty(); + Iterator var7 = candidateAdvisors.iterator(); + + while(var7.hasNext()) { + // 这里遍历通知:after、before和around通知,通过canApply逐一去判断是否可以应用于bean + Advisor candidate = (Advisor)var7.next(); + if (!(candidate instanceof IntroductionAdvisor) && canApply(candidate, clazz, hasIntroductions)) { + eligibleAdvisors.add(candidate); + } + } + + return eligibleAdvisors; + } + } + + public static boolean canApply(Advisor advisor, Class targetClass, boolean hasIntroductions) { + if (advisor instanceof IntroductionAdvisor) { + return ((IntroductionAdvisor)advisor).getClassFilter().matches(targetClass); + } else if (advisor instanceof PointcutAdvisor) { + // 因为在AspectJTest中设置了切点test(),所以程序会走到这里来 + PointcutAdvisor pca = (PointcutAdvisor)advisor; + return canApply(pca.getPointcut(), targetClass, hasIntroductions); + } else { + return true; + } + } + + + public static boolean canApply(Pointcut pc, Class targetClass, boolean hasIntroductions) { + Assert.notNull(pc, "Pointcut must not be null"); + // targetClass为要代理的类,这里就是TestBean + if (!pc.getClassFilter().matches(targetClass)) { + return false; + } else { + // 从Pointcut中获取@Pointcut注解修饰的切点表达式,并封装成MethodMatcher对象 + MethodMatcher methodMatcher = pc.getMethodMatcher(); + if (methodMatcher == MethodMatcher.TRUE) { + return true; + } else { + IntroductionAwareMethodMatcher introductionAwareMethodMatcher = null; + if (methodMatcher instanceof IntroductionAwareMethodMatcher) { + introductionAwareMethodMatcher = (IntroductionAwareMethodMatcher)methodMatcher; + } + + Set> classes = new LinkedHashSet(); + if (!Proxy.isProxyClass(targetClass)) { + classes.add(ClassUtils.getUserClass(targetClass)); + } + + classes.addAll(ClassUtils.getAllInterfacesForClassAsSet(targetClass)); + Iterator var6 = classes.iterator(); + + while(var6.hasNext()) { + Class clazz = (Class)var6.next(); + // 通过反射工具类获取到被代理类的所有成员方法,包括其父类Object的所有方法 + Method[] methods = ReflectionUtils.getAllDeclaredMethods(clazz); + Method[] var9 = methods; + int var10 = methods.length; + + for(int var11 = 0; var11 < var10; ++var11) { + Method method = var9[var11]; + if (introductionAwareMethodMatcher != null) { + if (introductionAwareMethodMatcher.matches(method, targetClass, hasIntroductions)) { + return true; + } + } else if (methodMatcher.matches(method, targetClass)) { + return true; + } + } + } + + return false; + } + } + } +} +``` + +这里总结下findAdvisorsThatCanApply所做的工作。将含有通知的candidateAdvisors集合逐一遍历,通过canApply方法来判断bean是否能够应用这些通知,而在canApply方法中,又通过ReflectionUtils.getAllDeclaredMethods()方法获取targetClass目标代理类的所有方法,包括了Object方法,看看candidateAdvisors集合中的通知都能否应用于targetClass代理类的方法。 + +这样,AnnotationAwareAspectJAutoProxyCreator就已经筛选出可应用于代理类的通知了,接下来就到了重头戏——对通知进行代理。 + +#### 1.3 对通知进行代理 + +**进行代理的前期准备工作** + +啥是代理呢?em...这里简单介绍下代理的概念:就是为其他对象提供一种代理,用来控制对这个对象的访问。 这里先不对代理及代理模式展开讨论,后门再专门学习“代理模式”。 + +回到AbstractAutoProxyCreator的wrapIfNecessary方法中。经过this.getAdvicesAndAdvisorsForBean()方法的工作,获取到了可应用的通知对象数组,接下来的工作就是要对这些通知进行代理了。 + +```Java +public abstract class AbstractAutoProxyCreator extends ProxyProcessorSupport implements SmartInstantiationAwareBeanPostProcessor, BeanFactoryAware { + ... + protected Object createProxy(Class beanClass, @Nullable String beanName, @Nullable Object[] specificInterceptors, TargetSource targetSource) { + // 将beanName设置为目标代理类 + if (this.beanFactory instanceof ConfigurableListableBeanFactory) { + AutoProxyUtils.exposeTargetClass((ConfigurableListableBeanFactory)this.beanFactory, beanName, beanClass); + } + + ProxyFactory proxyFactory = new ProxyFactory(); + proxyFactory.copyFrom(this); + if (!proxyFactory.isProxyTargetClass()) { + if (this.shouldProxyTargetClass(beanClass, beanName)) { + proxyFactory.setProxyTargetClass(true); + } else { + // 判断是否需要添加代理接口,由于这里设置的是, + // 自然不会使用到代理接口的方式,所以该方法所做的工作为:proxyFactory.setProxyTargetClass(true); + this.evaluateProxyInterfaces(beanClass, proxyFactory); + } + } + + // 封装通知,然后将advisors添加到ProxyFactory中 + Advisor[] advisors = this.buildAdvisors(beanName, specificInterceptors); + proxyFactory.addAdvisors(advisors); + // 设置dialing类 + proxyFactory.setTargetSource(targetSource); + // 定制代理 + this.customizeProxyFactory(proxyFactory); + // 用来控制代理工厂被配置之后,是否还允许修改通知;默认值为false,即在代理被配置之后,不允许修改代理的配置。 + proxyFactory.setFrozen(this.freezeProxy); + if (this.advisorsPreFiltered()) { + proxyFactory.setPreFiltered(true); + } + + return proxyFactory.getProxy(this.getProxyClassLoader()); + } + + + protected Advisor[] buildAdvisors(@Nullable String beanName, @Nullable Object[] specificInterceptors) { + //解析拦截器名并进行注册 + Advisor[] commonInterceptors = this.resolveInterceptorNames(); + List allInterceptors = new ArrayList(); + if (specificInterceptors != null) { + // 将通知都封装在allInterceptors中 + allInterceptors.addAll(Arrays.asList(specificInterceptors)); + if (commonInterceptors.length > 0) { + if (this.applyCommonInterceptorsFirst) { + allInterceptors.addAll(0, Arrays.asList(commonInterceptors)); + } else { + allInterceptors.addAll(Arrays.asList(commonInterceptors)); + } + } + } + + int i; + if (this.logger.isTraceEnabled()) { + int nrOfCommonInterceptors = commonInterceptors.length; + i = specificInterceptors != null ? specificInterceptors.length : 0; + this.logger.trace("Creating implicit proxy for bean '" + beanName + "' with " + nrOfCommonInterceptors + " common interceptors and " + i + " specific interceptors"); + } + + Advisor[] advisors = new Advisor[allInterceptors.size()]; + + for(i = 0; i < allInterceptors.size(); ++i) { + // 通过advisorAdapterRegistry这个适配器来包装通知 + advisors[i] = this.advisorAdapterRegistry.wrap(allInterceptors.get(i)); + } + + return advisors; + } +} +``` + +```Java +public class DefaultAdvisorAdapterRegistry implements AdvisorAdapterRegistry, Serializable { + public Advisor wrap(Object adviceObject) throws UnknownAdviceTypeException { + // 如果封装对象本身就是Advisor,则无需做任何处理 + if (adviceObject instanceof Advisor) { + return (Advisor)adviceObject; + } else if (!(adviceObject instanceof Advice)) { //如果封装对象不是Advice类型,则不能进行封装。注意:Advice和Adviser的区别!! + throw new UnknownAdviceTypeException(adviceObject); + } else { // 如果是advice类型的对象,则进行封装 + Advice advice = (Advice)adviceObject; + + if (advice instanceof MethodInterceptor) { + return new DefaultPointcutAdvisor(advice); + } else { + Iterator var3 = this.adapters.iterator(); + + AdvisorAdapter adapter; + do { + if (!var3.hasNext()) { + throw new UnknownAdviceTypeException(advice); + } + + adapter = (AdvisorAdapter)var3.next(); + } while(!adapter.supportsAdvice(advice)); + + return new DefaultPointcutAdvisor(advice); + } + } + } +} +``` + +看到这里,估计小伙伴们有点晕。这里总结一下createProxy方法,整理一下思路。在Spring中对于代理类的创建和处理,都是通过ProxyFactory来处理的,实际上createProxy方法都是围绕着ProxyFactory作初始化操作,一切都是为了创建代理做好准备,这是Spring源码的一贯风格,为某个逻辑做大量的前期准备工作。 + +1. 获取当前类中的属性 +2. 添加代理接口,将拦截器封装为通知 +3. 封装Advisor +4. 设置要代理的类 +5. 对ProxyFactory进行定制化 + + + +**获取代理方式** + +```Java +public class ProxyFactory extends ProxyCreatorSupport { + ... + public Object getProxy(@Nullable ClassLoader classLoader) { + return this.createAopProxy().getProxy(classLoader); + } +} +``` +```Java +public class ProxyCreatorSupport extends AdvisedSupport { + protected final synchronized AopProxy createAopProxy() { + if (!this.active) { + this.activate(); + } + // 使用我们刚刚做完初始化工作的ProxyFactory来创建代理 + return this.getAopProxyFactory().createAopProxy(this); + } +} +``` + +```Java +public class DefaultAopProxyFactory implements AopProxyFactory, Serializable { + public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException { + // 如果aop配置文件没有配置属性属性,则返回JdkDynamicAopProxy的实例对象 + if (!config.isOptimize() && !config.isProxyTargetClass() && !this.hasNoUserSuppliedProxyInterfaces(config)) { + return new JdkDynamicAopProxy(config); + } else { + Class targetClass = config.getTargetClass(); + if (targetClass == null) { + throw new AopConfigException("TargetSource cannot determine target class: Either an interface or a target is required for proxy creation."); + } else { + // targetClass就是示例中的TestBean,由于TestBean不是借口,并且不是代理类,所以要返回的ObjenesisCglibAopProxy实例对象,也就是CGLIB代理 + return (AopProxy)(!targetClass.isInterface() && !Proxy.isProxyClass(targetClass) ? new ObjenesisCglibAopProxy(config) : new JdkDynamicAopProxy(config)); + } + } + } +} +``` + +这里是获取的是CGLIB代理。 + +### 2. 总结 + +深入学习了AnnotationAwareAspectJAutoProxyCreator源码之后,发现其工作可以概括为以下几点: +1. 解析通知存储到缓存中 +2. 从缓存中获取通知,筛选通知 +3. 包装通知,初始化ProxyFactory +4. 使用ProxyFactory来创建代理(JDK动态代理、CGLIB代理) +5. 代理最终实现AOP的核心逻辑 + + + +由于JDK动态代理和CGLIB代理源码介绍篇幅过长,源码介绍放在下一篇博文中。 diff --git "a/note/SpringBoot/\346\267\261\345\205\245SpringBoot\346\272\220\347\240\201\345\255\246\344\271\240\344\271\213\342\200\224\342\200\224SpringFactoriesLoader.md" "b/note/SpringBoot/\346\267\261\345\205\245SpringBoot\346\272\220\347\240\201\345\255\246\344\271\240\344\271\213\342\200\224\342\200\224SpringFactoriesLoader.md" new file mode 100644 index 0000000..881b18d --- /dev/null +++ "b/note/SpringBoot/\346\267\261\345\205\245SpringBoot\346\272\220\347\240\201\345\255\246\344\271\240\344\271\213\342\200\224\342\200\224SpringFactoriesLoader.md" @@ -0,0 +1,219 @@ +## 前言 + 要想深入学习源码,那么就得先学会读懂它的注释,毕竟是一手知识嘛。大家都知道SpringBoot是一款多么优秀的框架,它给Java开发者带来了极大的便利,再也不用去整合SSM了,这里就不继续赞美SpringBoot了。相信大家都会很好奇SpringBoot底层源码是怎么运行的?它是怎么神奇的让我们可以快速开发JAVAEE企业级项目?如何快速整合第三方框架?接下来的深入学习SpringBoot源码系列,让我和各位小伙伴们一同学习SpringBoot底层源码。 + +这里先学习下 SpringFactoriesLoader,这个类在SpringBoot整个底层源码中起到了非常关键的作用,置于有多关键,等读完整片文章你们就知道了。 + +**SpringBoot版本:2.2.1.RELEASE** + +## 正文 + +### 1. 话不多说,源码注释解读一波 +``` +General purpose factory loading mechanism for internal use within the framework. + +

{@code SpringFactoriesLoader} {@linkplain #loadFactories loads} and instantiates + factories of a given type from {@value #FACTORIES_RESOURCE_LOCATION} files which + may be present in multiple JAR files in the classpath. The {@code spring.factories} + file must be in {@link Properties} format, where the key is the fully qualified + name of the interface or abstract class, and the value is a comma-separated list of + implementation class names. For example: + +``` +翻译过来就是: +``` +SpringFactoriesLoader是用于Spring框架内部的通用工厂加载机制。 + +SpringFactoriesLoader通过loadFactories方法来加载并实例化来自FACTORIES_RESOUCE_LOCATION路径中的文件给定的工厂类型, +而这些文件可能包含在类路径的jar包中。这些文件通常都命名为spring.factories,并且都是以properties属性作为格式,文件中key表示 +的是接口或者抽象类的全限定名称,而值是以逗号分隔的实现类的名称列表。 +``` + +### 2. 上源码 + 首先,可以看到SpringFactoriesLoader是final类,final修饰的类是不可以被继承,类中的方法都是不可以被覆盖的,且默认都是final修饰的方法,可以猜想到SpringFactoriesLoader类在被设计之初,是不想开发者继承该类并对该类进行扩展。所以,如果在开发中不想让别人对你的类继承或者扩展,那就用final来修饰吧~~ + +```Java +public final class SpringFactoriesLoader { +} +``` + +下面看下SpringFactoriesLoader类有哪些成员变量? + +```Java + /** + * 寻找工厂的位置 + * 工厂可以存放在多个jar文件中 + */ +public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories"; +``` + + 先来看看spring-boot:2.2.1.RELEASEjar包下,spring.factories文件的内容。 +![在这里插入图片描述](https://img-blog.csdnimg.cn/20200605074613675.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0NvZGVyQnJ1aXM=,size_16,color_FFFFFF,t_70) +![在这里插入图片描述](https://img-blog.csdnimg.cn/20200605110849246.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0NvZGVyQnJ1aXM=,size_16,color_FFFFFF,t_70#pic_center) +![在这里插入图片描述](https://img-blog.csdnimg.cn/2020060511090234.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0NvZGVyQnJ1aXM=,size_16,color_FFFFFF,t_70#pic_center) + +在spring.factories文件中,有非常多的工厂类,包括了属性源加载器、错误报告器、容器初始化器、容器监听器等,这些工厂类在SpringBoot中都有非常重要的作用,具体的读者可以自行前往查看。 + +```Java +// 自定义的用于存储工厂的缓存 +private static final Map> cache = new ConcurrentReferenceHashMap<>(); + +``` + + 这里的缓存是通过ConcurrentReferenceHashMap类实现的,对于这个类估计很多读者都是第一次见吧... 下面就简单介绍下ConcurrentReferenceHashMap这个数据结构。 + +通过阅读ConcurrentReferenceHashMap的类注释,可以总结出以下几点: +1. 它是一个ConcurrentHashMap,它的key和value使用ReferenceType.SOFT或者是ReferenceType.WEAK,即软引用和弱引用。 +2. 它可以用作Collections.synchronizedMap(new WeakHashMap>())的替代方法。 +3. 它遵循ConcurrentHashMap相同的设计约束,但不同的是它还支持null键和null值。 +4. 它默认使用的是SoftReference软引用。 +5. 使用软引用就意味着,在进行下一次GC时,如果即将发生OOM,GC就会把软引用指向的对象给回收掉。这一特性适合用作缓存处理。 + + +#### 2.1 loadFactories方法 + 下面先来看下loadFactories方法的注释。 +``` + /** + * Load and instantiate the factory implementations of the given type from + * {@value #FACTORIES_RESOURCE_LOCATION}, using the given class loader. + *

The returned factories are sorted through {@link AnnotationAwareOrderComparator}. + *

If a custom instantiation strategy is required, use {@link #loadFactoryNames} + * to obtain all registered factory names. + * @param factoryType the interface or abstract class representing the factory + * @param classLoader the ClassLoader to use for loading (can be {@code null} to use the default) + * @throws IllegalArgumentException if any factory implementation class cannot + * be loaded or if an error occurs while instantiating any factory + * @see #loadFactoryNames + */ +``` +翻译过来就是: + loadFactories方法通过类加载器来加载并且实例化FACTORIES_RESOURCE_LOCATION路径文件中定义的工厂实现。在返回工厂之前,都会通过AnnotationAwareOrderComparator这个类来进行排序。如果需要自定义实例化策略,请使用loadFactoryNames去获取所有注册的工厂名称。 + loadFactories方法中,入参factoryType表示工厂类的接口或者抽象类;入参classLoader表示加载工厂的类加载器,如果为空则会使用默认的类加载器。 + +```Java +public static List loadFactories(Class factoryType, @Nullable ClassLoader classLoader) { + Assert.notNull(factoryType, "'factoryType' must not be null"); + // 类加载器 + ClassLoader classLoaderToUse = classLoader; + if (classLoaderToUse == null) { + // 如果为空则用系统默认类加载器 + classLoaderToUse = SpringFactoriesLoader.class.getClassLoader(); + } + // 通过获取所有工厂实现类的名称集合 + List factoryImplementationNames = loadFactoryNames(factoryType, classLoaderToUse); + if (logger.isTraceEnabled()) { + logger.trace("Loaded [" + factoryType.getName() + "] names: " + factoryImplementationNames); + } + List result = new ArrayList<>(factoryImplementationNames.size()); + for (String factoryImplementationName : factoryImplementationNames) { + // 实例化工厂实现类,然后添加进result集合中 + result.add(instantiateFactory(factoryImplementationName, factoryType, classLoaderToUse)); + } + // 通过AnnotationAwareOrderComparator#sort方法对工厂名称进行排序 + AnnotationAwareOrderComparator.sort(result); + return result; + } +``` + +```Java + private static T instantiateFactory(String factoryImplementationName, Class factoryType, ClassLoader classLoader) { + try { + // 通过classUtils工具类获取工厂实现类的Class对象 + Class factoryImplementationClass = ClassUtils.forName(factoryImplementationName, classLoader); + if (!factoryType.isAssignableFrom(factoryImplementationClass)) { + throw new IllegalArgumentException( + "Class [" + factoryImplementationName + "] is not assignable to factory type [" + factoryType.getName() + "]"); + } + // 通过反射工具创建工厂类实例对象 + return (T) ReflectionUtils.accessibleConstructor(factoryImplementationClass).newInstance(); + } + catch (Throwable ex) { + throw new IllegalArgumentException( + "Unable to instantiate factory class [" + factoryImplementationName + "] for factory type [" + factoryType.getName() + "]", + ex); + } + } +``` + + + 可以看到,loadFactories方法逻辑还是比较简单的,作用也比较明确,即: +① 通过classLoader去加载工厂获取其对应类名称; +② 通过instantiateFactory方法实例化工厂类; +③ 通过AnnotationAwareOrderComparator#sort方法对工厂进行排序; + +看下哪些地方用到了loadFactories方法: +![在这里插入图片描述](https://img-blog.csdnimg.cn/20200605103200752.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0NvZGVyQnJ1aXM=,size_16,color_FFFFFF,t_70#pic_center) +上图中的ConfigFileApplicationListener类是用于加载application配置文件的监听器类,对于application配置文件的加载,后面会详细讲解~~ + +#### 2.2 loadFactoryNames方法 + 由于loadFactoryNames方法的注释和loadFactories内容一样,所以这里就不写出来了。 + +```Java + public static List loadFactoryNames(Class factoryType, @Nullable ClassLoader classLoader) { + // 获取到factoryType工厂类型 + String factoryTypeName = factoryType.getName(); + // 加载SpringFactories,如果没有则返回一个空集合 + return loadSpringFactories(classLoader).getOrDefault(factoryTypeName, Collections.emptyList()); + } +``` + +```Java + private static Map> loadSpringFactories(@Nullable ClassLoader classLoader) { + // 从缓存中获取已经加载过的SpringFactories + MultiValueMap result = cache.get(classLoader); + if (result != null) { + return result; + } + + try { + Enumeration urls = (classLoader != null ? + // 通过类加载器读取类路径下的spring.factories文件,然后封装成URL存储于Enumeration中 + classLoader.getResources(FACTORIES_RESOURCE_LOCATION) : + ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION)); + result = new LinkedMultiValueMap<>(); + // 遍历urls,再将url封装成UrlResource对象 + while (urls.hasMoreElements()) { + URL url = urls.nextElement(); + UrlResource resource = new UrlResource(url); + /** + * 通过PropertiesLoaderUtils属性加载器去加载spring.factories中的value值。 + * 这里的Properties是继承了HashTable的一个属性,key和value就对应着spring.factories文件里的key和value。 + * 在PropertiesLoaderUtils中,底层是通过IO流读取的文件数据,这里就不细说了。 + */ + Properties properties = PropertiesLoaderUtils.loadProperties(resource); + for (Map.Entry entry : properties.entrySet()) { + String factoryTypeName = ((String) entry.getKey()).trim(); + // 遍历获取工厂实现类名称 + for (String factoryImplementationName : StringUtils.commaDelimitedListToStringArray((String) entry.getValue())) { + result.add(factoryTypeName, factoryImplementationName.trim()); + } + } + } + // 将获取结果存入缓存中 + cache.put(classLoader, result); + return result; + } + catch (IOException ex) { + throw new IllegalArgumentException("Unable to load factories from location [" + + FACTORIES_RESOURCE_LOCATION + "]", ex); + } + } +``` + +通过IDEA来看下有哪些地方用到了loadFactoryName的方法: +![在这里插入图片描述](https://img-blog.csdnimg.cn/20200605114254699.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0NvZGVyQnJ1aXM=,size_16,color_FFFFFF,t_70#pic_center) +可以看到上面图片中SpringApplication中是哪个地方用到了loadFactoryNames方法, +![在这里插入图片描述](https://img-blog.csdnimg.cn/20200605114649305.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0NvZGVyQnJ1aXM=,size_16,color_FFFFFF,t_70#pic_center) +进入到SpringApplication的构造器中,会调用setInitializers方法,这个方法是用于设置初始化器,初始化器也是非常重要的一个知识点,后面会详细介绍。 + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20200605114815497.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0NvZGVyQnJ1aXM=,size_16,color_FFFFFF,t_70#pic_center) +可以看到,在SpringApplication中的getSpringFactoriesInstance方法中,调用了SpringFactoriesLoader#loadFacotyNames。SpringFactoriesLoader#loadFactoryNames方法调用完之后就获取到了spring.factories中的value值,并存储到Set集合中,接着就调用createSpringFactoriesInstances方法通过反射工具实例化Set集合中存储的工厂类,经过排序之后再返回给上一层调用。 + +下面用一张简单的书序图描述下SpringFactoriesLoader在SpringBoot中的调用过程。 +![在这里插入图片描述](https://img-blog.csdnimg.cn/20200605135730713.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0NvZGVyQnJ1aXM=,size_16,color_FFFFFF,t_70#pic_center) + +## 3. 总结 +学习完SpringFactoriesLoader源码之后,算是真正踏入学习SpringBoot源码的大门了,通过SpringFactoriesLoader,SpringBoot得以去加载类路径下jar包中的spring.factories文件,才能够读取该文件中的各种工厂类:包括监听器、初始化器、属性源加载器等,而通过这些工厂类,才得以让SpringBoot变得这么强大!!!接下来几篇关于SpringBoot源码将会对监听器、初始化器、属性源以及配置加载等机制进行深入的分析~~ + +> 觉得作者写的不错的点个赞,关注作者。 +> 本文 Github https://github.com/coderbruis/JavaSourceLearning 已收录,更多源码文章以及源码在github中可以学习。 + diff --git "a/note/SpringBoot/\346\267\261\345\205\245SpringBoot\346\272\220\347\240\201\345\255\246\344\271\240\344\271\213\342\200\224\342\200\224\345\220\257\345\212\250\345\212\240\350\275\275\345\231\250.md" "b/note/SpringBoot/\346\267\261\345\205\245SpringBoot\346\272\220\347\240\201\345\255\246\344\271\240\344\271\213\342\200\224\342\200\224\345\220\257\345\212\250\345\212\240\350\275\275\345\231\250.md" new file mode 100644 index 0000000..247898b --- /dev/null +++ "b/note/SpringBoot/\346\267\261\345\205\245SpringBoot\346\272\220\347\240\201\345\255\246\344\271\240\344\271\213\342\200\224\342\200\224\345\220\257\345\212\250\345\212\240\350\275\275\345\231\250.md" @@ -0,0 +1,121 @@ +## 前言 +在SpringBoot中,启动加载器的作用是,当SpringBoot程序执行后立马需要执行其他回调程序时,就可以通过SpringBoot的启动类加载器来实现。 + +## 正文 + +### 1. CommandLineRunner + +SpringBoot的CommandLineRunner是一个启动加载器的核心,CommandLinerRunner是一个接口,该接口定义如下: + + +```Java +@FunctionalInterface +public interface CommandLineRunner { + + // 启动加载器的回调方法 + void run(String... args) throws Exception; + +} +``` + +如何使用SpringBoot启动加载器呢? + +实例代码: + +```Java +@Component +@Order(1) +public class FirstCommandLineRunner implements CommandLineRunner { + + @Override + public void run(String... args) throws Exception { + // 给\u001B 给字段加颜色 + System.out.println("\u001B[32m >>> startup first runner<<<"); + } +} +``` + +启动效果即在SpringBoot启动完成之后,在控制台中输出 startup first runner + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20200820095619152.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0NvZGVyQnJ1aXM=,size_16,color_FFFFFF,t_70#pic_center) + + +小结: +1. 实现CommandLineRunner,重写Run方法 +2. 如果有多个CommandLineRunner需要实现,则可以通过@Order来进行排序 + +### 2. ApplicationRunner +实现SpringBoot启动加载器的第二种方式,就是通过实现ApplicationRunner,先看下ApplicationRunner的源码: + +```Java +@FunctionalInterface +public interface ApplicationRunner { + void run(ApplicationArguments args) throws Exception; +} +``` +可以看到,相比于CommandLineRunner,ApplicationRunner的run方法传入的是ApplicationArguments对象。 + +先看下ApplicationArguments这个接口定义: + +```Java +public interface ApplicationArguments { + // 获取源参数,即SpringBootApplication#run方法中传入的args; 这里的source就是SpringBoot对象 + String[] getSourceArgs(); + // 获取SpringBoot对象的properties,然后以Set集合返回 + Set getOptionNames(); + // 判断是否包含指定name的属性 + boolean containsOption(String name); + // 获取指定name属性的List集合 + List getOptionValues(String name); + + List getNonOptionArgs(); +} +``` +它的实现类是DefaultApplicationArguments,会在SpringApplication中的run方法里创建,这里统一在下文讲解原理时讲解。 + + +实例代码: + +```Java +@Order(1) +@Component +public class FirstApplicationRunner implements ApplicationRunner { + + @Override + public void run(ApplicationArguments args) throws Exception { + System.out.println("\u001B[32m >>> first application runner<<<"); + } +} +``` +这里将Order值设置为1,观察下启动结果: + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20200820095756552.png#pic_center) + + +对于ApplicationRunner进行,有如下小结: +1. 通过实现ApplicationRunner,实现run方法也可以实现SpringBoot启动加载器 +2. 通过order值指定顺序,不过如果CommandLineRunner和ApplicationRunner的order值相同,则ApplicationRunner实现优先执行 + + +### 3. SpringBoot启动加载器原理 +其实SpringBoot启动加载器原理比较简单,在底层源码调用逻辑比较清楚。 +![在这里插入图片描述](https://img-blog.csdnimg.cn/20200820100314418.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0NvZGVyQnJ1aXM=,size_16,color_FFFFFF,t_70#pic_center) +在DefaultApplicationArguments里,有一个不可忽略的类:Source + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20200820101656164.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0NvZGVyQnJ1aXM=,size_16,color_FFFFFF,t_70#pic_center) +该类在调用构造方法时,会调用父类SimpleCommandLinePropertySource的构造方法,继续跟进查看 + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20200820101854768.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0NvZGVyQnJ1aXM=,size_16,color_FFFFFF,t_70#pic_center) +![在这里插入图片描述](https://img-blog.csdnimg.cn/20200820102111888.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0NvZGVyQnJ1aXM=,size_16,color_FFFFFF,t_70#pic_center) +最后,再调用callRunner方法 +![在这里插入图片描述](https://img-blog.csdnimg.cn/20200820100603273.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0NvZGVyQnJ1aXM=,size_16,color_FFFFFF,t_70#pic_center) +![在这里插入图片描述](https://img-blog.csdnimg.cn/20200820100649546.png#pic_center) + + + +## 总结 +1. 在SpringBoot中实现启动加载器有两种方式,分别实现CommandLineRunner和ApplicationRunner; +2. 他们都可以通过@Order来进行优先级排序; +3. 在SpringBoot底层源码中,是先获取ApplicationRunner的bean,然后再去获取CommandLineRunner的bean,因此如果它们的@Order值相同,则是优先调用ApplicationRunner的run方法; +4. 对于CommandLineArgs命令行参数,是通过SimpleCommandLineArgsParser的parse方法来解析的,它会将args解析为key/value对,然后以CommandLineArgus对象返回; + diff --git "a/note/SpringBoot/\346\267\261\345\205\245SpringBoot\346\272\220\347\240\201\345\255\246\344\271\240\344\271\213\342\200\224\342\200\224\347\233\221\345\220\254\345\231\250\344\270\216\344\272\213\344\273\266\346\234\272\345\210\266.md" "b/note/SpringBoot/\346\267\261\345\205\245SpringBoot\346\272\220\347\240\201\345\255\246\344\271\240\344\271\213\342\200\224\342\200\224\347\233\221\345\220\254\345\231\250\344\270\216\344\272\213\344\273\266\346\234\272\345\210\266.md" new file mode 100644 index 0000000..dedae1a --- /dev/null +++ "b/note/SpringBoot/\346\267\261\345\205\245SpringBoot\346\272\220\347\240\201\345\255\246\344\271\240\344\271\213\342\200\224\342\200\224\347\233\221\345\220\254\345\231\250\344\270\216\344\272\213\344\273\266\346\234\272\345\210\266.md" @@ -0,0 +1,597 @@ +## 前言 +先看下Spring官方文档对于事件以及监听器的解释与说明。 +[监听器官方说明](https://docs.spring.io/spring-boot/docs/2.3.1.RELEASE/reference/html/spring-boot-features.html#boot-features-application-events-and-listeners) + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20200622101432321.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0NvZGVyQnJ1aXM=,size_16,color_FFFFFF,t_70) +总结起来就是几点: +1. 除了通常的Spring框架自带的事件例如:ContextRefreshedEvent,SpringApplication还会发送一些额外的事件。 +2. 对于事件的监听,需要通过监听器来实现。在SpringBoot中,监听器可以通过三种方式来注册, + ① 通过SpringApplication.addListeners(...) + ② 通过SpringApplicationBuilder.listeners(...) + ③ 通过注册在META-INF/spring.factories中,示例为org.springframework.context.ApplicationListener=com.example.project.MyListener +3. 监听器不能以Bean的形式注册进SpringIOC容器中,因为监听器是在ApplicationContext上下文创建成功之前调用的。 +4. Spring应用程序的内置事件会以以下顺序发送: + ① ApplicationStartingEvent + ② ApplicationEnvironmentPreparedEvent + ③ ApplicationContextInitializedEvent + ④ ApplicationPreparedEvent + ⑤ ApplicationStartedEvent + ⑥ AvailabilityChangeEvent + ⑦ ApplicationReadyEvent + ⑧ AvailabilityChangeEvent + ⑨ ApplicationFailedEvent + 除了上面这些绑定在SpringApplication上的事件外,还有ContextRefreshedEvent和WebServerInitializedEvent、ServletWebServerInitializedEvent、ReactiveWebServerInitializedEvent等事件在ApplicationPreparedEvent和ApplicationStartedEvent事件之间发布。 + +在介绍完SpringBoot的官方文档后,下面看看SpringBoot源码中监听器是如何实现的。 + +## 正文 + +### 1. SpringBoot通过factories注册的监听器 +在前面已经讲解过了SpringFactoriesLoader加载spring.factories的原理机制,大家都清楚了SpringFactoriesLoader会加载spring.factories中注册的**初始化器**、**监听器**、**后置处理器**等组件,在SpringApplication的构造方法中会通过JDK的反射工具实例化这些组件。 +![在这里插入图片描述](https://img-blog.csdnimg.cn/20200622142233204.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0NvZGVyQnJ1aXM=,size_16,color_FFFFFF,t_70) +从源码中可以看到,SpringBoot在SpringApplication的构造方法中,会先获取ApplicationListener类型的监听器,并存进SpringApplication对象中,用于后续操作的调用。 + +下面看看ApplicationListener接口定义信息 + +```Java +/** + * Interface to be implemented by application event listeners. + * + *

Based on the standard {@code java.util.EventListener} interface + * for the Observer design pattern. + * + *

As of Spring 3.0, an {@code ApplicationListener} can generically declare + * the event type that it is interested in. When registered with a Spring + * {@code ApplicationContext}, events will be filtered accordingly, with the + * listener getting invoked for matching event objects only. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @param the specific {@code ApplicationEvent} subclass to listen to + * @see org.springframework.context.ApplicationEvent + * @see org.springframework.context.event.ApplicationEventMulticaster + * @see org.springframework.context.event.EventListener + */ +@FunctionalInterface +public interface ApplicationListener extends EventListener { + + /** + * Handle an application event. + * @param event the event to respond to + */ + void onApplicationEvent(E event); + +} +``` + +注释大致意思为: +ApplicationListener接口是用于定义Spring应用程序监听器的接口,是基于Observer(观察者)设计模式的标准接口。从Spring3.0开始,ApplicationListener可以声明感兴趣的事件类型。 + +注册完spring.factories配置文件中的监听器后,SpringBoot框架是什么时候开始**获取监听器**然后**调用监听器**的监听的事件呢?下面来看SpringApplication#run方法。 + +**重点需要关注下ApplicationListener#onApplicationEvent方法,这个方法是广播器播放监听器时调用的方法,详情会在下文中讲解。** + +**SpringApplication#run** +![在这里插入图片描述](https://img-blog.csdnimg.cn/2020062217104411.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0NvZGVyQnJ1aXM=,size_16,color_FFFFFF,t_70) + +**SpringApplication#getRunListeners** +![在这里插入图片描述](https://img-blog.csdnimg.cn/20200622171133438.png) +这个方法就是通过SpringFactoriesLoader去加载spring.factories配置文件中的文件指定的SpringApplicationRunListener类类型,这里获取的就是EventPublishingRunListener: +``` +# Run Listeners +org.springframework.boot.SpringApplicationRunListener=\ +org.springframework.boot.context.event.EventPublishingRunListener +``` + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20200622171356800.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0NvZGVyQnJ1aXM=,size_16,color_FFFFFF,t_70) +EventPublishingRunListener有什么作用? + +#### 1.1 EventPublishingRunListener +下面先看下EventPublishingRunListener源码 +```Java +/** + * SpringApplicationRunListener 是用于发布 SpringApplicationEvent的。 + * SpringApplicationRunListener通过内部的ApplicationEventMulticaster在容器刷新之前来触发事件。 + * + * @author Phillip Webb + * @author Stephane Nicoll + * @author Andy Wilkinson + * @author Artsiom Yudovin + * @since 1.0.0 + */ +public class EventPublishingRunListener implements SpringApplicationRunListener, Ordered { + + private final SpringApplication application; + + private final String[] args; + + // 事件播放器 + private final SimpleApplicationEventMulticaster initialMulticaster; + + public EventPublishingRunListener(SpringApplication application, String[] args) { + this.application = application; + this.args = args; + this.initialMulticaster = new SimpleApplicationEventMulticaster(); + // 获取SpringApplicaiton中实例化的事件监听器,添加进事件播放器中。listeners集合最终会存于AbstractApplicationEventMulticaster类中一内部类的一个Set集合中。 + for (ApplicationListener listener : application.getListeners()) { + this.initialMulticaster.addApplicationListener(listener); + } + } + + @Override + public int getOrder() { + return 0; + } + + /** + * 通过事件播放器播放ApplicationStartingEvent事件 + */ + @Override + public void starting() { + this.initialMulticaster.multicastEvent(new ApplicationStartingEvent(this.application, this.args)); + } + + /** + * 通过事件播放器播放ApplicationEnvironmentPreparedEvent事件 + */ + @Override + public void environmentPrepared(ConfigurableEnvironment environment) { + this.initialMulticaster + .multicastEvent(new ApplicationEnvironmentPreparedEvent(this.application, this.args, environment)); + } + + /** + * 通过事件播放器播放ApplicationContextInitializedEvent事件 + */ + @Override + public void contextPrepared(ConfigurableApplicationContext context) { + this.initialMulticaster + .multicastEvent(new ApplicationContextInitializedEvent(this.application, this.args, context)); + } + + /** + * 通过事件播放器播放ApplicationPreparedEvent事件 + */ + @Override + public void contextLoaded(ConfigurableApplicationContext context) { + for (ApplicationListener listener : this.application.getListeners()) { + if (listener instanceof ApplicationContextAware) { + ((ApplicationContextAware) listener).setApplicationContext(context); + } + context.addApplicationListener(listener); + } + this.initialMulticaster.multicastEvent(new ApplicationPreparedEvent(this.application, this.args, context)); + } + + /** + * 通过事件播放器播放ApplicationStartedEvent事件 + */ + @Override + public void started(ConfigurableApplicationContext context) { + context.publishEvent(new ApplicationStartedEvent(this.application, this.args, context)); + } + + /** + * 通过事件播放器播放ApplicationReadyEvent事件 + */ + @Override + public void running(ConfigurableApplicationContext context) { + context.publishEvent(new ApplicationReadyEvent(this.application, this.args, context)); + } + + /** + * 通过事件播放器播放ApplicationFailedEvent事件 + */ + @Override + public void failed(ConfigurableApplicationContext context, Throwable exception) { + ApplicationFailedEvent event = new ApplicationFailedEvent(this.application, this.args, context, exception); + if (context != null && context.isActive()) { + // Listeners have been registered to the application context so we should + // use it at this point if we can + context.publishEvent(event); + } + else { + // An inactive context may not have a multicaster so we use our multicaster to + // call all of the context's listeners instead + if (context instanceof AbstractApplicationContext) { + for (ApplicationListener listener : ((AbstractApplicationContext) context) + .getApplicationListeners()) { + this.initialMulticaster.addApplicationListener(listener); + } + } + this.initialMulticaster.setErrorHandler(new LoggingErrorHandler()); + this.initialMulticaster.multicastEvent(event); + } + } + + private static class LoggingErrorHandler implements ErrorHandler { + + private static final Log logger = LogFactory.getLog(EventPublishingRunListener.class); + + @Override + public void handleError(Throwable throwable) { + logger.warn("Error calling ApplicationEventListener", throwable); + } + + } + +} +``` +EventPublishingRunListener实现了SpringApplicationRunListener接口,该接口定义了用于监听**SpringApplication生命周期**的一系列接口方法。 + +```Java +public interface SpringApplicationRunListener { + + /** + * 在首次启动run方法时立即调用。 可用于非常早的初始化。 + */ + default void starting() { + } + + /** + * 在准备SpringIOC容器之后,创建好SpringIOC容器之前调用。 + */ + default void environmentPrepared(ConfigurableEnvironment environment) { + } + + /** + * 在创建和准备SpringIOC容器之后,但在加载源之前调用。 + */ + default void contextPrepared(ConfigurableApplicationContext context) { + } + + /** + * 在SpringIOC容器加载完之后但是在其刷新之前调用。 + */ + default void contextLoaded(ConfigurableApplicationContext context) { + } + + /** + * 上下文已刷新,应用程序已启动,但CommandLineRunner 和ApplicationRunner 尚未被调用。 + */ + default void started(ConfigurableApplicationContext context) { + } + + /** + * 在刷新应用程序上下文并已调用所有CommandLineRunner和ApplicationRunner后,在run方法完成之前立即调用。 + */ + default void running(ConfigurableApplicationContext context) { + } + + /** + * 运行应用程序时发生故障时调用。 + */ + default void failed(ConfigurableApplicationContext context, Throwable exception) { + } + +} +``` + +#### 1.2 Event事件 +分析完EventPublishingRunListener之后,我们看下在事件Event。在SpringBoot中,EventObject定义了系统事件的顶级父类,其类定义中定义了一个成员变量source,用于存储**事件的初始源头**,对于SpringBoot的系统事件,源头就表示的是SpringIOC容器。 +![在这里插入图片描述](https://img-blog.csdnimg.cn/20200623064526838.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0NvZGVyQnJ1aXM=,size_16,color_FFFFFF,t_70) +可以看到SpringApplicationEvent定义的系统事件基本都围绕着SpringBoot框架的启动生命周期。 +![在这里插入图片描述](https://img-blog.csdnimg.cn/20200623064538927.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0NvZGVyQnJ1aXM=,size_16,color_FFFFFF,t_70) +![在这里插入图片描述](https://img-blog.csdnimg.cn/20200623064623754.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0NvZGVyQnJ1aXM=,size_16,color_FFFFFF,t_70) + + +而在EventPublishingRunListener中播放的事件,就是SpringBoot系统事件发送顺序,顺序如下: +![在这里插入图片描述](https://img-blog.csdnimg.cn/20200623063407599.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0NvZGVyQnJ1aXM=,size_16,color_FFFFFF,t_70) + +### 2. SpringBoot中的事件广播器 +在EventPublishingRunListener内部中,有一个名为SimpleApplicationEventMulticaster的成员变量,它是一种广播器,专门用于管理SpringBoot中的监听器,并广播指定的类型事件。SpringBoot定义了一个事件广播器接口,用于抽象播放器的接口行为,该接口名称为:ApplicationEventMulticaster,看下它的实现类的层次结构: +![在这里插入图片描述](https://img-blog.csdnimg.cn/20200623065336310.png) +因此可以知道,在SpringBoot中核心广播器就是SimpleApplicationEventMulticaster,所有管理监听器播放事件的工作都由SimpleApplicationEventMulticaster来完成。下面先来看下接口ApplicationEventMulticaster源码及注释: + +```Java +/** + * ApplicationEventMulticaster接口的实现类用于管理多个ApplicationListener监听器,并对事件进行广播 + * + * ApplicationEventMulticaster实际上就是作为Spring真正播放事件的一个代理。 + * + * @author Rod Johnson + * @author Juergen Hoeller + * @author Stephane Nicoll + * @see ApplicationListener + */ +public interface ApplicationEventMulticaster { + + /** + * 添加一个监听器用于触发所有事件 + */ + void addApplicationListener(ApplicationListener listener); + + /** + * 添加一个监听器的bean用于触发所有事件 + */ + void addApplicationListenerBean(String listenerBeanName); + + /** + * 从通知列表中移除一个监听器 + */ + void removeApplicationListener(ApplicationListener listener); + + /** + * 从通知列表中移除一个监听器的bean + */ + void removeApplicationListenerBean(String listenerBeanName); + + /** + * 移除所有注册在广播器上的监听器。 + */ + void removeAllListeners(); + + /** + * 在适当的监听器上播放ApplicationEvent事件 + */ + void multicastEvent(ApplicationEvent event); + + /** + * 在适当的监听器上播放ApplicationEvent事件 + * 可以通过eventType来过滤需要播放的事件类型。 + */ + void multicastEvent(ApplicationEvent event, @Nullable ResolvableType eventType); + +} +``` + +接着看AbstractApplicationEventMulticaster抽象类源码,由于AbstractApplicationEventMulticaster方法较多,就挑几个核心方法进行讲解并学习。 + +AbstractApplicationEventMulticaster类注释可以总结成以下几点: +1. ApplicationEventMulticaster接口的抽象实现,提供了基本的监听器注册功能。 +2. 默认情况下,框架不允许同一监听器有多个实例,因为监听器会被存放到set集合中。 +3. 通常ApplicationEventMulticaster接口的multicastEvent方法是留给子类SimpleApplicationEventMulticaster实现,它通过multicastEvent方法将所有事件广播到所有已注册的监听器,并在调用线程中播放事件。 + +下面是AbstractApplicationEventMulticaster类的定义以及成员变量: +![在这里插入图片描述](https://img-blog.csdnimg.cn/20200623094604505.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0NvZGVyQnJ1aXM=,size_16,color_FFFFFF,t_70) +AbstractApplicationEventMulticaster不仅实现了ApplicationEventMulticaster,还实现了BeanClassLoaderAware以及BeanFactoryAware。在SpringBoot中,实现了XXAware,都会获得注入XX的能力,比如常见的ApplicationContextAware,就是拥有注入ApplicationContext的能力;所以AbstractApplicationEventMulticaster即拥有了注入BeanClass和BeanFactory的能力。 + +成员变量中,defaultRetriever是一个内部类,作为一个存储监听器实例的数据结构,其底层通过Set集合来存储监听器。retrieverCache是一个ConcurrentHashMap实现的内部缓存,用于在下次获取监听器时直接能从缓存中获取。retrievalMutex是一个Object对象,用作对象锁。 + +AbstractApplicationEventMulticaster已经把监听器存储好了,就等着广播器进行事件广播,而广播的方法就是视SimpleApplicationEventMulticaster#multicastEvent方法。 + +```Java +@Override + public void multicastEvent(ApplicationEvent event) { + // 广播事件 + multicastEvent(event, resolveDefaultEventType(event)); + } + + @Override + public void multicastEvent(final ApplicationEvent event, @Nullable ResolvableType eventType) { + ResolvableType type = (eventType != null ? eventType : resolveDefaultEventType(event)); + Executor executor = getTaskExecutor(); + // 获取监听器集合 + for (ApplicationListener listener : getApplicationListeners(event, type)) { + if (executor != null) { + // 如果有设置线程池,则调用线程池中的线程来调用监听 + executor.execute(() -> invokeListener(listener, event)); + } + else { + invokeListener(listener, event); + } + } + } +``` +下面看下广播器是如何获取到监听器集合的。 + +#### 2.1 AbstractApplicationEventMulticaster#getApplicationListeners +既然监听器存放在了播放器里,那么播放器肯定会提供一个获取监听器的方法,那么这个方法就是getApplicationListeners。 +**AbstractApplicationEventMulticaster#getApplicationListeners** +```Java +protected Collection> getApplicationListeners( + ApplicationEvent event, ResolvableType eventType) { + + // 获取事件的源,也就是SpringApplication对象。 + Object source = event.getSource(); + // 获取SpringApplication类对象 + Class sourceType = (source != null ? source.getClass() : null); + ListenerCacheKey cacheKey = new ListenerCacheKey(eventType, sourceType); + + // 从缓存中获取监听器 + ListenerRetriever retriever = this.retrieverCache.get(cacheKey); + if (retriever != null) { + return retriever.getApplicationListeners(); + } + + if (this.beanClassLoader == null || + (ClassUtils.isCacheSafe(event.getClass(), this.beanClassLoader) && + (sourceType == null || ClassUtils.isCacheSafe(sourceType, this.beanClassLoader)))) { + // 异步地构建以及缓存ListenerRetriever + synchronized (this.retrievalMutex) { + retriever = this.retrieverCache.get(cacheKey); + if (retriever != null) { + // 从缓存中获取监听器 + return retriever.getApplicationListeners(); + } + retriever = new ListenerRetriever(true); + // 获取监听器集合 + Collection> listeners = + retrieveApplicationListeners(eventType, sourceType, retriever); + // 将存放监听器的retriever存入ConcurrentHashMap中 + this.retrieverCache.put(cacheKey, retriever); + return listeners; + } + } + else { + // 没有ListenerRetriever 缓存就没必要进行异步获取 + return retrieveApplicationListeners(eventType, sourceType, null); + } + } +``` + +看下ListenerRetriever#retrieveApplicationListeners源码 +![在这里插入图片描述](https://img-blog.csdnimg.cn/2020062311204527.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0NvZGVyQnJ1aXM=,size_16,color_FFFFFF,t_70) +类ListenerRetriever的成员变量applicationListeners实际存储的就是spring.factories文件中注册的监听器。那问题来了,spring.factories文件里注册的监听器是什么时候注册进applicationListeners集合中的? + +目光得回到EventPublishingRunListener的构造方法中, +![在这里插入图片描述](https://img-blog.csdnimg.cn/20200623113045429.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0NvZGVyQnJ1aXM=,size_16,color_FFFFFF,t_70) +![在这里插入图片描述](https://img-blog.csdnimg.cn/20200623113524425.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0NvZGVyQnJ1aXM=,size_16,color_FFFFFF,t_70) +而EventPublishingRunListener构造方法是通过反射来方法来调用的。 +![在这里插入图片描述](https://img-blog.csdnimg.cn/20200623113906400.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0NvZGVyQnJ1aXM=,size_16,color_FFFFFF,t_70) +下图为SpringBoot如何将监听器添加进EventPublishingRunListener中的简易流程图。 +![在这里插入图片描述](https://img-blog.csdnimg.cn/20200623120430115.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0NvZGVyQnJ1aXM=,size_16,color_FFFFFF,t_70) +除此之外,还有一个重要方法AbstractApplicationEventMulticaster#supportsEvent,该方法有两个重载方法 +```Java +supportsEvent( + ConfigurableBeanFactory beanFactory, String listenerBeanName, ResolvableType eventType) +``` +```Java +supportsEvent(Class listenerType, ResolvableType eventType) +``` + +二者区别在于第一个supportsEvent方法需要传入beanFactory,用作于获取bean的申明类型。在尝试实例化bean定义的侦听器之前,请先检查其通用声明的事件类型,以对其进行早期过滤。 +![在这里插入图片描述](https://img-blog.csdnimg.cn/20200623141446161.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0NvZGVyQnJ1aXM=,size_16,color_FFFFFF,t_70) +这里有一个比较陌生的类,就是ResolvableType,这是啥类?就如其名字,一个ResolvableType对象就代表着一种可解决的java类型,我们可以通过ResolvableType对象可以获取类型携带的信息 ,包括父类型、接口类型、泛型类型等。 + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20200623141319889.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0NvZGVyQnJ1aXM=,size_16,color_FFFFFF,t_70) + + +### 3. SpringBoot中的事件广播器如何播放监听器中的事件? +上文已经分别讲解了事件、监听器、广播器,那么三者之间的调用关系又是怎样的关系呢?说的再好不如debug看下调用栈调用流程来的一清二楚。断点就打在SimpleApplicationEventMulticaster#multicastEvent方法中。 + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20200623171859184.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0NvZGVyQnJ1aXM=,size_16,color_FFFFFF,t_70)![在这里插入图片描述](https://img-blog.csdnimg.cn/20200623172201529.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0NvZGVyQnJ1aXM=,size_16,color_FFFFFF,t_70) +由于此处EVENT_TYPES类型包含了ApplicationStartingEvents.class,所以是符合类型的事件,因此会把LogginApplicationListener存放进Set集合中。 +最终可以看到,对于starting方法,拥有感兴趣的ApplicationStartingEvents事件类型的监听器有下图的四种 +![在这里插入图片描述](https://img-blog.csdnimg.cn/20200623172602344.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0NvZGVyQnJ1aXM=,size_16,color_FFFFFF,t_70) +最终,会在SimpleApplicationEventMulticaster#doInvokeListener中进行监听器的onApplicationEvent调用,最终播放监听器。 +![在这里插入图片描述](https://img-blog.csdnimg.cn/20200623173146188.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0NvZGVyQnJ1aXM=,size_16,color_FFFFFF,t_70) +在SimpleApplicationEventMulticaster#multicastEvent方法中,由getApplicationListeners方法返回了拥有感兴趣事件类型的监听器实例集合,上图已经证实,在SpringBoot中对ApplicationStartingEvent事件类型感兴趣的监听器有四种。 +![在这里插入图片描述](https://img-blog.csdnimg.cn/2020062317343530.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0NvZGVyQnJ1aXM=,size_16,color_FFFFFF,t_70) +因此SpringBoot的广播器会依次播放这四种监听器的onApplicationEvent方法。 + +从事件调用发起到最终监听器播放事件,可以总结为下图的流程: +![在这里插入图片描述](https://img-blog.csdnimg.cn/20200623173818841.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0NvZGVyQnJ1aXM=,size_16,color_FFFFFF,t_70) + +那么经过debug分析,事件、监听器、广播器之间的关系就已经变得明朗起来了。 + +#### 3.1 事件、监听器、广播器之间的关系 +对于广播器SimpleApplicationEventMulticaster,其作用就是存储监听器、根据具体的事件筛选出感兴趣的监听器,然后播放监听器;而监听器的onApplicationEvent方法会传入事件,来进行具体的播放逻辑。当然,广播器是作为EventPublishingRunListener的成员变量。因而在SpringApplication的生命周期中,通过广播器会分别调用对应感兴趣事件的监听器。 + +### 4. 模仿SpringBoot,实现自定义的事件与监听器 +首先,定义一个天气事件抽象类 +```Java +public abstract class WeatherEvent { + public abstract String getWeather(); +} +``` +定义两个天气事件 +```Java +public class RainEvent extends WeatherEvent{ + @Override + public String getWeather() { + return "rain..."; + } +} +``` +```Java +public class SnowEvent extends WeatherEvent{ + @Override + public String getWeather() { + return "snow..."; + } +} +``` + +接着定义一个事件监听器 +```Java +public interface WeatherListener { + // 类似于SpringBoot监听器的onApplicationEvent方法 + void onWeatherEvent(WeatherEvent event); +} +``` + +有了监听器接口,那么就要定义实现类 +```Java +@Component +public class RainListener implements WeatherListener{ + @Override + public void onWeatherEvent(WeatherEvent event) { + if (event instanceof RainEvent) { + System.out.println("hello " + event.getWeather()); + } + } +} +``` +```Java +@Component +public class SnowListener implements WeatherListener { + @Override + public void onWeatherEvent(WeatherEvent event) { + if (event instanceof SnowEvent) { + System.out.println("hello " + event.getWeather()); + } + } +} +``` +可以看到,SnowListener和RainListener类的onWeatherEvent方法会依据对应的天气Event进行过滤。 + +定义完了监听器以及事件之后,就还差广播器以及调用广播器播放事件的XXRunListener了。先定义一个事件广播器,包含了基础的添加监听器、移除监听器、播放事件的功能。 +```Java +public interface EventMulticaster { + void multicastEvent(WeatherEvent event); + void addListener(WeatherListener weatherListener); + void removeListener(WeatherListener weatherListener); +} +``` +抽象广播器类 +```Java +@Component +public abstract class AbstractEventMulticaster implements EventMulticaster{ + + // 自动注入所有的天气监听器 + @Autowired + private List listenerList; + + // 播放天气事件 + @Override + public void multicastEvent(WeatherEvent event) { + doStart(); + // 遍历监听器,然后播放天气事件 + listenerList.forEach(i -> i.onWeatherEvent(event)); + doEnd(); + } + + @Override + public void addListener(WeatherListener weatherListener) { + listenerList.add(weatherListener); + } + + @Override + public void removeListener(WeatherListener weatherListener) { + listenerList.remove(weatherListener); + } + // 记录广播器开始调用 + abstract void doStart(); + // 记录广播器结束调用 + abstract void doEnd(); +} +``` + +定义完了广播器,就运行广播器的XXRunListener了,下面定义一个WeatherRunListener,用于播放感兴趣的事件。 +```Java +@Component +public class WeatherRunListener { + + @Autowired + private WeatherEventMulticaster eventMulticaster; + + public void snow() { + eventMulticaster.multicastEvent(new SnowEvent()); + } + + public void rain() { + eventMulticaster.multicastEvent(new RainEvent()); + } + +} +``` + +编写测试类测试 +![在这里插入图片描述](https://img-blog.csdnimg.cn/20200623224008504.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0NvZGVyQnJ1aXM=,size_16,color_FFFFFF,t_70) +### 5. 总结 +通过SpringBoot底层源码的分析以及模仿SpringBoot实现的自定义事件广播器,相信大家对广播器、监听器、事件以及调用广播器的XXRunListener运行监听类有了更深入的理解了。另外通过SpringBoot底层源码的分析后可以了解到,SpringBoot事件监听机制用的也是非常的广泛,除了文中介绍的EventPublishingRunListener监听器监听SpringApplication生命周期,还有ConfigFileApplicationListener会监听onApplicationEnvironmentPreparedEvent事件来加载配置文件application.properties的环境变量,所以说SpringBoot事件监听机制是非常重要的一个知识点,在SpringBoot的面试中也会经常面试到。 + +> 觉得作者写的不错的点个赞并关注作者。 +本文代码在面的地址中已收录,文中出现的源码以及流程图都已收录进github中。 +> [https://github.com/coderbruis/JavaSourceLearning](https://github.com/coderbruis/JavaSourceLearning) \ No newline at end of file diff --git "a/note/SpringBoot/\346\267\261\345\205\245SpringBoot\346\272\220\347\240\201\345\255\246\344\271\240\344\271\213\342\200\224\342\200\224\347\263\273\347\273\237\345\210\235\345\247\213\345\214\226\345\231\250.md" "b/note/SpringBoot/\346\267\261\345\205\245SpringBoot\346\272\220\347\240\201\345\255\246\344\271\240\344\271\213\342\200\224\342\200\224\347\263\273\347\273\237\345\210\235\345\247\213\345\214\226\345\231\250.md" new file mode 100644 index 0000000..868e146 --- /dev/null +++ "b/note/SpringBoot/\346\267\261\345\205\245SpringBoot\346\272\220\347\240\201\345\255\246\344\271\240\344\271\213\342\200\224\342\200\224\347\263\273\347\273\237\345\210\235\345\247\213\345\214\226\345\231\250.md" @@ -0,0 +1,202 @@ +## 前言 +前一章已经讲解了SpringBoot的SpringFactoriesLoader类的功能以及作用,即读取spring.factories文件中的工厂类,其中就包括了系统初始化器。在SpringBoot中,系统初始化器名称为ApplicationContextInitializer,它是一个接口,只定义了一个initialize方法。下面将详细介绍下SpringBoot的系统初始化器的原理以及作用,并且自定义一个系统初始化器,并在此基础上讲解下常见的使用场景。 + +**SpringBoot 版本:2.2.1.RELEASE** + +## 正文 + +### 1. 初始ApplicationContextInitializer + 先来看下ApplicationContextInitializer接口的官方注释。 + ``` + /** + * Callback interface for initializing a Spring {@link ConfigurableApplicationContext} + * prior to being {@linkplain ConfigurableApplicationContext#refresh() refreshed}. + * + *

Typically used within web applications that require some programmatic initialization + * of the application context. For example, registering property sources or activating + * profiles against the {@linkplain ConfigurableApplicationContext#getEnvironment() + * context's environment}. See {@code ContextLoader} and {@code FrameworkServlet} support + * for declaring a "contextInitializerClasses" context-param and init-param, respectively. + * + *

{@code ApplicationContextInitializer} processors are encouraged to detect + * whether Spring's {@link org.springframework.core.Ordered Ordered} interface has been + * implemented or if the @{@link org.springframework.core.annotation.Order Order} + * annotation is present and to sort instances accordingly if so prior to invocation. + * + * @author Chris Beams + * @since 3.1 + * @param the application context type + * @see org.springframework.web.context.ContextLoader#customizeContext + * @see org.springframework.web.context.ContextLoader#CONTEXT_INITIALIZER_CLASSES_PARAM + * @see org.springframework.web.servlet.FrameworkServlet#setContextInitializerClasses + * @see org.springframework.web.servlet.FrameworkServlet#applyInitializers + */ + + ``` + + 注释意思可以总结为以下几点: + 1. ApplicationContextInitializer是一个用于在ConfigurableApplicationContext#refresh方法刷新之前,进行回调初始化ConfigurableApplicationContext的一个回调接口。 + 2. ApplicationContextInitializer通常用于对WEB环境上下文进行编程式地初始化,例如通过ConfigurableApplicationContext#getEnvironment方法获取容器环境来注册属性源以及激活容器配置。 + 3. ApplicationContextInitializer支持@Order注解,在调用初始化器之前系统会对其进行排序。 + +```Java +public interface ApplicationContextInitializer { + + /** + * 初始化给定的applicationContext + */ + void initialize(C applicationContext); + +} +``` +首先ApplicationContextInitializer是一个接口,实现类都需要实现initialize方法,方法中传入的是需要初始化的Spring容器。对于接口中的**上界泛型定义**,表示的是C必须是ConfigurableApplicationContext的子类。 +相信读者对ConfigurableApplicationContext都不陌生,它是直接继承至ApplicationContext的一个接口,它在ApplicationContext的基础上还提供了可配置的能力,包括可配置environment、conversionService的能力。 + +所以对于ApplicationContextInitializer系统初始化器来说,是需要传入Spring容器,对容器进行初始化操作。 + +#### 1.1 在哪里注册的系统初始化器? +在SpringBoot源码底层,对于系统初始化器的注册是放在了SpringApplication的构造方法里,代码如下图: +![在这里插入图片描述](https://img-blog.csdnimg.cn/2020060811395969.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0NvZGVyQnJ1aXM=,size_16,color_FFFFFF,t_70#pic_center) +可以看到,在SpringApplication的构造方法中注册了系统初始化器以及监听器,监听器在SpringBoot中也同样重要,下章将会详细讲解。下面翻译下SpringApplication的注释方法: + +> SpringApplication构造方法创建了一个SpringApplication实例。 应用程序上下文将从指定的primarySources数据源来加载Bean。可以在调SpringApplication#run(String ...)之前自定义实例。 + +对于getSpringFactoriesInstances(ApplicationContextInitializer.class)方法,在上一篇文章[深入SpringBoot源码学习之——SpringFactoriesLoader](https://blog.csdn.net/CoderBruis/article/details/106559304)已经介绍了的,getSpringFactoriesInstances方法就是去类路径的jar包或者是resources目录下加载并实例化META-INF/spring.factories的工厂类,**由于入参传的是ApplicationContextInitializer.class,所以只是加载和实例化xxxInitializer的工厂类,即所有的初始化器**,最终以集合的形式返回,然后将返回的集合赋值给SpringApplication的initializers集合。 +![在这里插入图片描述](https://img-blog.csdnimg.cn/20200608115548639.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0NvZGVyQnJ1aXM=,size_16,color_FFFFFF,t_70#pic_center) +SpringBoot框架已经将spring.factories对应所有的初始化器加载到了SpringApplication类的initializers集合数据结构里,那SpringBoot在哪里调用SpringApplication的初始化器呢? + +#### 1.2 在哪里调用的初始化器的initialize方法? +如果不知道系统从哪里开始调用的初始化器,可以先个初始化器的initialize方法打个断点,然后debug追中系统的调用栈路径即可,如下图: +![在这里插入图片描述](https://img-blog.csdnimg.cn/20200608120240341.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0NvZGVyQnJ1aXM=,size_16,color_FFFFFF,t_70#pic_center) +![在这里插入图片描述](https://img-blog.csdnimg.cn/20200608120420379.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0NvZGVyQnJ1aXM=,size_16,color_FFFFFF,t_70#pic_center) +可以看到调用路径为:LearnsbApplication#main——>SpringApplication#run——>SpringApplication#prepareContext——>SpringApplication#applyInitializers——>ContextIdApplicationContextInitializer#initialize,这样调用初始化器的地方一幕了然了。 + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20200608120713807.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0NvZGVyQnJ1aXM=,size_16,color_FFFFFF,t_70#pic_center) + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20200608120932643.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0NvZGVyQnJ1aXM=,size_16,color_FFFFFF,t_70#pic_center) +applyInitializers方法的注释已经注明了方法的作用,即:在SpringIOC容器进行refresh刷新之前,将所有的初始化器应用于SpringIOC容器。传入的context此时还没有进行刷新。 + +```Java + protected void applyInitializers(ConfigurableApplicationContext context) { + // getInitializers()方法会从SpringApplication中获取所有的已实例化的初始化器 + for (ApplicationContextInitializer initializer : getInitializers()) { + // 获取初始化器的class类型 + Class requiredType = GenericTypeResolver.resolveTypeArgument(initializer.getClass(), + ApplicationContextInitializer.class); + // 对初始化器的class类型进行断言,判断其是否是ConfigurableApplicationContext的子类 + Assert.isInstanceOf(requiredType, context, "Unable to call initializer."); + // 调用初始化器的initialize方法 + initializer.initialize(context); + } + } +``` + +从源码中分析,发现其实系统初始化器的底层源码逻辑比较的简单,但是却很实用。 + +### 2. 自定义ApplicationContextInitializer +自定义一个系统初始化器首先当然得实现ApplicationContextInitializer接口,然后将逻辑写在initialize方法里。 + +```Java +@Order(1) +public class FirstInitializer implements ApplicationContextInitializer { + // 下面将在initialize方法中获取ConfigurableEnviroment对象,并自定义一个map存入其中。 + // 待SpringBoot启动后,在SpringIOC容器中获取这个map。 + @Override + public void initialize(ConfigurableApplicationContext configurableApplicationContext) { + ConfigurableEnvironment environment = configurableApplicationContext.getEnvironment(); + Map map = new HashMap<>(); + map.put("coderbruis", "https://blog.csdn.net/CoderBruis"); + MapPropertySource mapPropertySource = new MapPropertySource("firstInitializer", map); + environment.getPropertySources().addLast(mapPropertySource); + System.out.println("run firstInitializer"); + } +} +``` + +定义了一个初始化器了,那么该如何让SpringBoot在启动时注册自定义的初始化器呢?首先当然可以在resouces目录下定义一个META-INF/spring.factories文件,如下: +``` +# Initializer +org.springframework.context.ApplicationContextInitializer=com.bruis.learnsb.initializer.FirstInitializer +``` +![在这里插入图片描述](https://img-blog.csdnimg.cn/20200608144657719.png#pic_center) +这样,SpringBoot就能识别到FirstInitializer这个自定义初始化器了。下面定义一个service并实现ApplicationContextAware,用于注入ApplicationContext对象。 +```Java +@Component +public class TestService implements ApplicationContextAware { + + private ApplicationContext applicationContext; + + /** + * 将SpringIOC容器注册到该类的成员变量中 + * @param applicationContext + * @throws BeansException + */ + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + this.applicationContext = applicationContext; + } + + public String test() { + // 从注册进Environment中的map中获取值 + return applicationContext.getEnvironment().getProperty("coderbruis"); + } + } +``` + +然后写一个Test类,测试一下: +```Java +@RunWith(SpringRunner.class) +@SpringBootTest +public class SpringBeanTest { + @Autowired + private TestService testService; + + @Test + public void testInitializer() { + System.out.println("Initializer: " + testService.test()); + } +} +``` + +启动测试类,查看IDEA控制台输出结果: +![在这里插入图片描述](https://img-blog.csdnimg.cn/20200608145449179.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0NvZGVyQnJ1aXM=,size_16,color_FFFFFF,t_70#pic_center) + +除了通过META-INF/spring.factories这种方式注册系统初始化器,可以在application.properties以及SpringApplication#addInitializers方法来注册,这里简单介绍下: +1. application.properties +``` +context.initializer.classes=com.bruis.learnsb.initializer.FirstInitializer +``` + +2. SpringApplication#addInitializers +```Java +@SpringBootApplication +public class LearnsbApplication { + + public static void main(String[] args) { + // 注册系统初始化器 + SpringApplication springApplication = new SpringApplication(LearnjavaApplication.class); + springApplication.addInitializers(new FirstInitializer()); + springApplication.run(args); + } +} +``` + +### 3. ApplicationContextInitializer使用场景 +下面简单介绍下SpringBoot默认注册的前两个两个系统初始化器 +![在这里插入图片描述](https://img-blog.csdnimg.cn/20200608150949748.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0NvZGVyQnJ1aXM=,size_16,color_FFFFFF,t_70#pic_center) +**ConfigurationWarningsApplicationContextInitializer** +![在这里插入图片描述](https://img-blog.csdnimg.cn/20200608151437524.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0NvZGVyQnJ1aXM=,size_16,color_FFFFFF,t_70#pic_center) +ConfigurationWarningsApplicationContextInitializer#initialize方法的作用就是想SpringIOC容器中注册了一个后置处理器,作用就是对于一般配置错误在日志中作出警告,详细逻辑在postPrrocessBeanDefinitionRegistry方法。 + +**ContextIdApplicationContextInitializer** +![在这里插入图片描述](https://img-blog.csdnimg.cn/20200608151804666.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0NvZGVyQnJ1aXM=,size_16,color_FFFFFF,t_70) +这个初始化器的作用是设置Spring应用上下文的ID。 + + + + +## 总结 +ApplicationContextInitializer是Spring给我们提供的一个在容器刷新之前对容器进行初始化操作的能力,如果有具体的需要在容器刷新之前处理的业务,可以通过ApplicationContextInitializer接口来实现。 + +> 觉得作者写的不错的点个赞,关注作者。 +> 本文代码在 Github https://github.com/coderbruis/JavaSourceLearning 中已收录,更多源码文章以及源码在github中可以学习。 \ No newline at end of file diff --git "a/note/SpringSecurity/\344\273\216\351\233\266\345\274\200\345\247\213\347\263\273\347\273\237\345\255\246\344\271\240SpringSecurity\345\222\214OAuth2\357\274\210\344\270\200\357\274\211\342\200\224\342\200\224 \345\210\235\350\257\206SpringSecurity.md" "b/note/SpringSecurity/\344\273\216\351\233\266\345\274\200\345\247\213\347\263\273\347\273\237\345\255\246\344\271\240SpringSecurity\345\222\214OAuth2\357\274\210\344\270\200\357\274\211\342\200\224\342\200\224 \345\210\235\350\257\206SpringSecurity.md" new file mode 100644 index 0000000..c757cdc --- /dev/null +++ "b/note/SpringSecurity/\344\273\216\351\233\266\345\274\200\345\247\213\347\263\273\347\273\237\345\255\246\344\271\240SpringSecurity\345\222\214OAuth2\357\274\210\344\270\200\357\274\211\342\200\224\342\200\224 \345\210\235\350\257\206SpringSecurity.md" @@ -0,0 +1,185 @@ + +- [前言](#前言) +- [正文](#正文) + - [1. 初识SpringSecurity](#1-初识springsecurity) + - [2. Spring Security项目核心jar包介绍](#2-spring-security项目核心jar包介绍) + - [3. SpringSecurity核心注解](#3-springsecurity核心注解) + - [3.1 @EnableWebSecurity](#31-enablewebsecurity) + - [3.2 @EnableGlobalAuthentication](#32-enableglobalauthentication) + - [3.3 @EnableGlobalMethodSecurity](#33-enableglobalmethodsecurity) + - [4. SpringSecurity核心组件](#4-springsecurity核心组件) + - [4.1 认证](#41-认证) + - [4.2 授权](#42-授权) + - [5. SpringSecurity中的Filter](#5-springsecurity中的filter) + - [5.1 FilterChainProxy以及其内部的Filter](#51-filterchainproxy以及其内部的filter) +- [总结](#总结) +- [参考](#参考) +- [相关文章](#相关文章) + +## 前言 +相信了解过SpringSecurity或者是OAuth2的读者,会发现网上会有非常多的相关文章,或是纯概念的,或是带有demo的,无论是哪种类型的文章,本人去阅读之后,对于整个框架的概念还是一知半解,也仅仅是实现了某些功能、某些效果而已,若遇到某些问题时无从下手,只能去百度去Google。这是因为对于SpringSecurity和OAuth2的知识没有一个整体概念的把握,知识体系没有形成系统,遂决定写一个关于SpringSecurity和OAuth2的系列专栏,在构建自己知识体系的同时还希望能帮助有同样困惑的同学。 + +**本专栏希望通过通俗易懂的语言来达到从入门概念、demo开始,由简入深,最终能达到深谙其底层源码的目的。** + +## 正文 +### 1. 初识SpringSecurity +对于SpringSecurity,可以由其官网中的介绍了解到其概述: +1. SpringSecurity就是一个功能强大且高定制化的身份验证和访问控制框架,它事实上就是一个保护基于Spring的应用框架安全性的标准。 +2. Spring Security是一个重点为Java应用程序提供身份验证和授权的框架。 +3. 与所有Spring项目一样,Spring Security的真正强大之处在于它可以很容易地扩展以满足定制需求 + +对于SpringSecurity的特性,可以总结为以下几点: +1. 对身份认证和授权提供全面和可扩展的支持。 +2. 防止会话固定、劫持请求、跨站点请求伪造等攻击。 +3. Servlet API的集成。 +4. 与Spring Web MVC的可选集成。 + +在进行代码编写时,先得对SpringSecurity有一个整体上的认识,等进行coding时才能知道对应代码的对应作用。 + +### 2. Spring Security项目核心jar包介绍 +在SpringSecurity继承的项目中,主要有四个核心的jar包: + +- spring-security-core.jar + SpringSecurity的核心包,任何SpringSecurity的功能都需要此包。 +- spring-security-web.jar + web工程必备,包含过滤器和相关的Web安全基础结构代码。 +- spring-security-config.jar + 用于解析xml配置文件,用到SpringSecurity的xml配置文件的就要用到此包。 + +由于spring-security-web.jar和spring-security-config.jar 都依赖了spring-security-core.jar,所以只需要导入spring-security-web.jar和spring-security-config.jar 即可。 + +> 本专栏系列文章相关demo版本: +> SpringBoot: 2.1.14.RELEASE + +``` + + + org.springframework.security + spring-security-config + 5.1.10.RELEASE + + + + org.springframework.security + spring-security-web + 5.1.10.RELEASE + +``` + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20200712224312526.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0NvZGVyQnJ1aXM=,size_16,color_FFFFFF,t_70) + +不过,在SpringBoot中的spring-boot-starter-security中,就已经依赖好了spring-security-web.jar和spring-security-config.jar,直接拿来用即可。 + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20200712224733553.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0NvZGVyQnJ1aXM=,size_16,color_FFFFFF,t_70) + +既然SpringBoot都已经定义好了spring-boot-starter-security,那就用spring-boot-starter-security吧,这样做的好处是能够方便SpringBoot统一SpringSecurity的版本。 + +### 3. SpringSecurity核心注解 +SpringBoot集成SpringSecurity需要配置几个配置文件,并且需要几个核心注解,下面来依次介绍这些注解。 + +#### 3.1 @EnableWebSecurity +@EnableWebSecurity是Spring Security用于启用Web安全的注解。典型的用法是该注解用在某个Web安全配置类上(实现了接口WebSecurityConfigurer或者继承自WebSecurityConfigurerAdapter)。 +```Java +@Configuration +@EnableWebSecurity +public class WebSecurityConfig extends WebSecurityConfigurerAdapter { +... +} +``` + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20200713110634549.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0NvZGVyQnJ1aXM=,size_16,color_FFFFFF,t_70) +从注解源码可以看到,该注解有如下作用: +1. 控制Spring Security是否使用调试模式(通过注解属性debug指定),默认为false,表示缺省不使用调试模式。 +2. 导入 WebSecurityConfiguration,用于配置Web安全过滤器FilterChainProxy,并创建过滤器链springSecurityFilterChain来保护你的Spring应用。 +3. 如果是Servlet 环境,导入WebMvcSecurityConfiguration。 +4. 如果是OAuth2环境(spring-security-oauth2-client),导入OAuth2ClientConfiguration。 +5. 使用注解@EnableGlobalAuthentication启用全局认证机制,即全局的AuthenticationManager,AuthenticationManager会在运行时对请求着进行身份验证。 + +#### 3.2 @EnableGlobalAuthentication +@EnableGlobalAuthentication是包含在了@EnableWebSecurity注解中的,作用通过启用认证管理器(AuthenticationManager)来启用全局认证机制。 +![在这里插入图片描述](https://img-blog.csdnimg.cn/20200726025353554.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0NvZGVyQnJ1aXM=,size_16,color_FFFFFF,t_70) +核心逻辑就在AuthenticationConfiguration类里。 + +在AuthenticationConfiguration在SpringSecurity中扮演者非常重要的作用,它内部包含了AuthenticationManager用于核心的认证工作。接下来将会重点讲解该类。 + + +#### 3.3 @EnableGlobalMethodSecurity +Spring Security默认是禁用注解的,要想开启注解,需要在继承WebSecurityConfigurerAdapter的类上加@EnableGlobalMethodSecurity注解,来判断用户对某个控制层的方法是否具有访问权限。 + +除此之外,还可以在@EnableGlobalMethodSecurity中添加几个属性。 + +1. @EnableGlobalMethodSecurity(securedEnabled=true)开启@Secured 注解过滤权限。 +2. @EnableGlobalMethodSecurity(jsr250Enabled=true)开启@RolesAllowed 注解过滤权限 。 +3. @EnableGlobalMethodSecurity(prePostEnabled=true)使用表达式时间方法级别的安全性 4个注解可用。 + - @PreAuthorize 在方法调用之前,基于表达式的计算结果来限制对方法的访问 + - @PostAuthorize 允许方法调用,但是如果表达式计算结果为false,将抛出一个安全性异常 + - @PostFilter 允许方法调用,但必须按照表达式来过滤方法的结果 + - @PreFilter 允许方法调用,但必须在进入方法之前过滤输入值 + + +### 4. SpringSecurity核心组件 +在SpringSecurity中,有着许多的组件包括AuthenticationManager、AccessDesicionManager和UsernamePasswordAuthenticationFilter等。 + +对于SpringSecurity来说,最大的两个问题就是:**认证(Authentication,即你是谁?)和授权(Authorization,允许你做什么?)**。SpringSecurity框架旨在将认证从授权中剥离出来,并也有适用于二者的策略和可扩展的设计。 + +#### 4.1 认证 +在SpringSecurity中,用于认证的主要接口是AuthenticationManager,它只有一个方法: +```Java +public interface AuthenticationManager { + Authentication authenticate(Authentication authentication) + throws AuthenticationException; +} +``` + +AuthenticationManger最普遍的实现类是ProviderManager,而ProviderManager会将认证委托给AuthenticationProvider。 +![在这里插入图片描述](https://img-blog.csdnimg.cn/20200714010510964.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0NvZGVyQnJ1aXM=,size_16,color_FFFFFF,t_70) +![在这里插入图片描述](https://img-blog.csdnimg.cn/20200714010535442.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0NvZGVyQnJ1aXM=,size_16,color_FFFFFF,t_70) +AuthenticationProvider接口和AuthenticationManager相似,但是它有一个额外的方法允许查询它支持的Authentication方式: +```Java +public interface AuthenticationProvider { + Authentication authenticate(Authentication authentication) + throws AuthenticationException; + + boolean supports(Class authentication); +} +``` + + +#### 4.2 授权 + +一旦认证成功,我们就可以进行授权了,它核心的策略就是AccessDecisionManager。同样的,它将授权逻辑全部委托给AccessDecisionVoter来实现。 + +一个AccessDecisionVoter考虑一个Authentication(代表一个Principal)和一个被ConfigAttributes装饰的安全对象,这里的ConfigAttributes就是一个包含了URL以及这个URL该有权限的对象的集合。 +```Java +boolean supports(ConfigAttribute attribute); + +boolean supports(Class clazz); + +int vote(Authentication authentication, S object, + Collection attributes); +``` + +现在,已经拥有了认证和授权组件了,那么一个HTTP请求进入SpringSecurity应用时,经过过滤器链中都发生了哪些逻辑?接下来就看下SpringSecurity中过滤器链底层都发生了什么。 + +### 5. SpringSecurity中的Filter +除了认证和授权外,SpringSecurity的另外一个核心就是Servlet的Filter来实现的。先简单回顾下Servlet中Filter的调用原理。 +下图展示了处理单个HTTP请求的经典分层结构图: + ![在这里插入图片描述](https://img-blog.csdnimg.cn/20200726104520271.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0NvZGVyQnJ1aXM=,size_16,color_FFFFFF,t_70) +客户端向服务器发起请求,然后Servlet容器(Tomcat)会根据URI来决定哪个Filter和哪个Servlet适用于这个请求,一个Servlet最多处理一个请求,过滤器是链式的,它们是有顺序的。事实上一个过滤器可以否决接下来的过滤器,上一个过滤器请求处理完就会动过调用Filter的doFilter()向下传递请求,知道请求最终到达Servlet处理完毕后,再过滤器以相反的顺序调用,再以Response返回给客户端。 + +#### 5.1 FilterChainProxy以及其内部的Filter + +回归正题,SpringSecurity在过滤器链中扮演的就是一个Filter,其类型是FilterChainProxy。但它又不是一个普通的Filter,因为FilterChainProxy中包含了额外的过滤器,每个过滤器都发挥特殊的作用。下面用一张图展示下FliterChainProxy中包含的过滤器链。 +![在这里插入图片描述](https://img-blog.csdnimg.cn/20200726110652665.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0NvZGVyQnJ1aXM=,size_16,color_FFFFFF,t_70) +在SpringBoot中,SpringSecurity的FilterChainProxy是以bean的形式注入到Spring容器中的,并且它是默认配置,所以在每次请求中都会存在,所以在SpringSecurity保护的应用中,每次请求都会经过FilterChainProxy。 + +## 总结 +本片文章粗略的讲解了SpringSecurity里的几个核心概念,包括:核心注解、认证和授权的核心组件以及SpringSecurity中的FilterChainProxy,整个SpringSecurity框架就是围绕着这几个核心概念运行,下面几章将会深入分析每个核心概念的底层运行机制。 + +## 参考 +- [《spring-security-architecture》](https://spring.io/guides/topicals/spring-security-architecture) +- [《Spring Security Reference》](https://docs.spring.io/spring-security/site/docs/current/reference/html5/#preface) + +## 相关文章 +- [从零开始系统学习SpringSecurity和OAuth2(二)—— 安全过滤器FilterChainProxy](https://github.com/coderbruis/JavaSourceCodeLearning/blob/master/note/SpringSecurity/%E4%BB%8E%E9%9B%B6%E5%BC%80%E5%A7%8B%E7%B3%BB%E7%BB%9F%E5%AD%A6%E4%B9%A0SpringSecurity%E5%92%8COAuth2%EF%BC%88%E4%BA%8C%EF%BC%89%E2%80%94%E2%80%94%20%E5%AE%89%E5%85%A8%E8%BF%87%E6%BB%A4%E5%99%A8FilterChainProxy.md) +- [从零开始系统学习SpringSecurity和OAuth2(三)—— WebSecurity建造核心逻辑](https://github.com/coderbruis/JavaSourceCodeLearning/blob/master/note/SpringSecurity/%E4%BB%8E%E9%9B%B6%E5%BC%80%E5%A7%8B%E7%B3%BB%E7%BB%9F%E5%AD%A6%E4%B9%A0SpringSecurity%E5%92%8COAuth2%EF%BC%88%E4%B8%89%EF%BC%89%E2%80%94%E2%80%94%20WebSecurity%E5%BB%BA%E9%80%A0%E6%A0%B8%E5%BF%83%E9%80%BB%E8%BE%91.md) \ No newline at end of file diff --git "a/note/SpringSecurity/\344\273\216\351\233\266\345\274\200\345\247\213\347\263\273\347\273\237\345\255\246\344\271\240SpringSecurity\345\222\214OAuth2\357\274\210\344\270\211\357\274\211\342\200\224\342\200\224 WebSecurity\345\273\272\351\200\240\346\240\270\345\277\203\351\200\273\350\276\221.md" "b/note/SpringSecurity/\344\273\216\351\233\266\345\274\200\345\247\213\347\263\273\347\273\237\345\255\246\344\271\240SpringSecurity\345\222\214OAuth2\357\274\210\344\270\211\357\274\211\342\200\224\342\200\224 WebSecurity\345\273\272\351\200\240\346\240\270\345\277\203\351\200\273\350\276\221.md" new file mode 100644 index 0000000..3d88362 --- /dev/null +++ "b/note/SpringSecurity/\344\273\216\351\233\266\345\274\200\345\247\213\347\263\273\347\273\237\345\255\246\344\271\240SpringSecurity\345\222\214OAuth2\357\274\210\344\270\211\357\274\211\342\200\224\342\200\224 WebSecurity\345\273\272\351\200\240\346\240\270\345\277\203\351\200\273\350\276\221.md" @@ -0,0 +1,308 @@ + + - [前言](#前言) + - [正文](#正文) + - [1. AbstractConfiguredSecurityBuilder中安全配置类](#1-abstractconfiguredsecuritybuilder中安全配置类) + - [2. AbstractConfiguredSecurityBuilder的doBuild()方法](#2-abstractconfiguredsecuritybuilder的dobuild方法) + - [3. WebSecurity中的performBuild()方法](#3-websecurity中的performbuild方法) +- [总结](#总结) +- [相关文章](#相关文章) + +## 前言 +相信了解过SpringSecurity或者是OAuth2的读者,会发现网上会有非常多的相关文章,或是纯概念的,或是带有demo的,无论是哪种类型的文章,本人去阅读之后,对于整个框架的概念还是一知半解,也仅仅是实现了某些功能、某些效果而已,若遇到某些问题时无从下手,只能去百度去Google。这是因为对于SpringSecurity和OAuth2的知识没有一个整体概念的把握,知识体系没有形成系统,遂决定写一个关于SpringSecurity和OAuth2的系列专栏,在建造自己知识体系的同时还希望能帮助有同样困惑的同学。 + +============== 分割线 ============== + +上一章已经讲解到了SpringSecurity用到了建造者模式来建造FilterChainProxy。本片就来详细分析下WebSecurity的核心逻辑以及AbstractConfiguredSecurityBuilder的doBuild()方法。 + +## 正文 + +### 1. AbstractConfiguredSecurityBuilder中安全配置类 +SpringSecurity通过SecurityConfigurer来建造FilterChainProxy,建造前还需要进行配置。因此AbstractConfiguredSecurityBuilder还需要注入配置组件SecurityConfigurer,初始化配置组件SecurityConfigurer,调用SecurityConfigurer的configure方法。 + +在AbstractConfiguredSecurityBuilder类中,看下安全配置类的定义:‘ + +```Java +private final LinkedHashMap>, List>> configurers = new LinkedHashMap>, List>>(); +``` +这是定义的安全配置器的子类Map集合,这个configurers就是用于初始化以及配置FilterChainProxy中的filters用的。Map集合中,Key是SecurityConfigurer的子类的Class类型,Value是SecurityConfigurer的list集合。 + +作为一个成员变量,自然会有方法从外部注入安全配置类。在AbstractConfiguredSecurityBuilder的类中,定义了add方法。 + +```Java + private > void add(C configurer) throws Exception { + Assert.notNull(configurer, "configurer cannot be null"); + // 获取安全配置类的Class类型 + Class> clazz = (Class>) configurer + .getClass(); + // 同步去操作安全配置类集合 + synchronized (configurers) { + // 查看建造状态是否是已经配置 + if (buildState.isConfigured()) { + throw new IllegalStateException("Cannot apply " + configurer + + " to already built object"); + } + // 如果allowConfigurersOfSameType为true,则从configurers集合中获取clazz类型的安全配置类集合 + List> configs = allowConfigurersOfSameType ? this.configurers + .get(clazz) : null; + if (configs == null) { + // 初始化安全配置类结合 + configs = new ArrayList>(1); + } + // 将安全配置类添加至configs的list集合中 + configs.add(configurer); + // 以clazz为key,configs为value存入configurers的LinkedHashMap集合中 + this.configurers.put(clazz, configs); + if (buildState.isInitializing()) { + this.configurersAddedInInitializing.add(configurer); + } + } + } + +``` + +通过IDEA来查看下哪些地方调用了add方法 +![在这里插入图片描述](https://img-blog.csdnimg.cn/2020080918440196.png) + +看下apply方法 +```Java + // 传入的C是SecurityConfigurerAdapter的子类, + public > C apply(C configurer) + throws Exception { + // 传入objectPostProcessor,该对象用于创建各种“实例”,具体什么作用下问会讲解,请留意 + configurer.addObjectPostProcessor(objectPostProcessor); + // 将当前对象设置为建造者 + configurer.setBuilder((B) this); + // 调用add方法,向configurers集合中添加configurer + add(configurer); + return configurer; + } + +``` + +继续查看apply方法有哪些地方调用了的 +![在这里插入图片描述](https://img-blog.csdnimg.cn/20200809224904457.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0NvZGVyQnJ1aXM=,size_16,color_FFFFFF,t_70) +图上的HttpSecurity的getOrApply方法值得注意一下,查看其方法 +```Java + private > C getOrApply( + C configurer) throws Exception { + // 从configurers集合中获取安全配置类 + C existingConfig = (C) getConfigurer(configurer.getClass()); + if (existingConfig != null) { + // 如果存在则直接返回该安全配置类 + return existingConfig; + } + // 如果不存在则调用apply方法去应用该安全配置类,并缓存到configurers集合中 + return apply(configurer); + } + +``` + +getOrApply方法主要是从configurers集合中获取配置类,如果存在则直接返回,否则则应用该配置类。 + +继续查看getOrApply有哪些地方在调用。这下终于看到了安全配置类了。 + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20200809225003886.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0NvZGVyQnJ1aXM=,size_16,color_FFFFFF,t_70) +这里有一个问题值得思考,这些配置类到底是干嘛用的?这里就以ExpressionUrlAuthorizationConfigurer配置类为例,看下其类内部configure方法逻辑。 + +找了一圈,发现configure的实现是在ExpressionUrlAuthorizationConfigurer的抽象父类AbstractInterceptUrlConfigurer定义的。 + +```Java + @Override + public void configure(H http) throws Exception { + // 创建元数据,该抽象方法由ExpressionUrlAuthorizationConfigurer定义,返回一个ExpressionBasedFilterInvocationSecurityMetadataSource对象 + FilterInvocationSecurityMetadataSource metadataSource = createMetadataSource(http); + // 如果配置失败,则数据源配置失败 + if (metadataSource == null) { + return; + } + // 创建一个FilterSecurityInterceptor对象 + FilterSecurityInterceptor securityInterceptor = createFilterSecurityInterceptor( + http, metadataSource, http.getSharedObject(AuthenticationManager.class)); + if (filterSecurityInterceptorOncePerRequest != null) { + securityInterceptor + .setObserveOncePerRequest(filterSecurityInterceptorOncePerRequest); + } + // 通过objectPostProcessor创建一个securityInterceptor实例对象 + securityInterceptor = postProcess(securityInterceptor); + http.addFilter(securityInterceptor); + // 将实例对象存入SharedObject缓存中 + http.setSharedObject(FilterSecurityInterceptor.class, securityInterceptor); + } + +``` + +从ExpressionUrlAuthorizationConfigurer的抽象父类AbstractInterceptUrlConfigurer可以看出,configure方法中调用了一个postProcess方法,该方法用于生成FilterSecurityInterceptor对象,在本系列文章前面第一章以及列出来的FilterChainProxy拦截器链对象,可以知道FilterSecurityInterceptor对象就属于FilterChainProxy拦截器链中的对象,并且是处在最后一个位置。 + +到此处,安全配置类的作用已经提现出来了,就是向sharedObject中添加过滤器,并最终注入到FilterChainProxy中。 + +### 2. AbstractConfiguredSecurityBuilder的doBuild()方法 +随着configurers集合元素的注入,下面就是进行建造工作,调用doBuild()方法。 + +```Java + @Override + protected final O doBuild() throws Exception { + synchronized (configurers) { + // 设置建造状态为初始化中 + buildState = BuildState.INITIALIZING; + + // 进行初始化前的工作 + beforeInit(); + // 初始化 + init(); + + // 设置建造状态为配置中 + buildState = BuildState.CONFIGURING; + + // 配置前的工作 + beforeConfigure(); + // 调用配置 + configure(); + + // 设置建造状态为建造中 + buildState = BuildState.BUILDING; + + // 执行建造核心逻辑 + O result = performBuild(); + + // 设置建造状态为已建造 + buildState = BuildState.BUILT; + + return result; + } + } + +``` +beforeInit()和beforeConfigure()是一个空方法体,没有逻辑。 +```Java + protected void beforeInit() throws Exception { + } + protected void beforeConfigure() throws Exception { + } +``` + +```Java + private void init() throws Exception { + // 调用getConfigurers()方法获取this.configurers的所有value值,并以List集合的形式返回 + Collection> configurers = getConfigurers(); + // 遍历configurers,并依次调用安全配置类的init方法 + for (SecurityConfigurer configurer : configurers) { + // 调用安全配置类的init初始化方法 + configurer.init((B) this); + } + + for (SecurityConfigurer configurer : configurersAddedInInitializing) { + configurer.init((B) this); + } + } +``` + +这需要注意的是,init和configure方法是有接口SecurityConfigurer接口定义的,但其实现以由SecurityConfigurerAdapter这个抽象的适配器类实现了,所以最终的安全配置类可重写init()和configure(),也可以不重写。所以可以发现,很多安全配置类是没有重写init()方法的。 + + +**接着就是configure()方法的调用** + +```Java + private void configure() throws Exception { + // 调用getConfigurers()方法获取this.configurers的所有value值,并以List集合的形式返回 + Collection> configurers = getConfigurers(); + + for (SecurityConfigurer configurer : configurers) { + // 遍历configurers,然后依次调用其configure方法 + configurer.configure((B) this); + } + } +``` + +在前面已经已经以ExpressionUrlAuthorizationConfigurer为例,去查看安全配置类configure方法的作用,都知道安全配置类的configure方法是用于将过滤器添加到sharedObject缓存中。 + +经过init()和configure()方法的执行,以及可以开始进行建造工作了,因而调用performBuild()方法执行建造过程。 + +```Java + protected abstract O performBuild() throws Exception; +``` +可以看到在AbstractConfiguredSecurityBuilder中,performBuild是以抽象方法的形式存在的,所以实现逻辑都在其子类中。 + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20200810112226524.png) + +这里HttpSecurity和WebSecurity类名这么像,有什么异同呢? + +> HttpSecurity + +HttpSecurity允许为特定的http请求配置基于Web的安全特性。默认情况下,HttpSecurity将应用于所有请求,但是如果需要限制请求,则可以通过RequestMatcher或其他类似方法来进行限制。 + +> WebSecurity + +而WebSecurity是通过WebSecurityConfiguration创建的,用于去创建类FilterChainProxy。在SpringSecurity中,WebSecurity通过HttpSecurity来对某些请求进行拦截限制。 + +> 区别 + +WebSecurity用于建造FilterChainProxy,WebSecurity是包含HttpSecurity的一个更大的概念,而HttpSecurity仅是用于建造FilterChainProxy中的一个SecurityFilterChain。 + + +### 3. WebSecurity中的performBuild()方法 +WebSecurity重写了AbstractConfiguredSecurityBuilder的perfomBuild()方法,核心逻辑如下: + +```Java + @Override + protected Filter performBuild() throws Exception { + Assert.state( + !securityFilterChainBuilders.isEmpty(), + () -> "At least one SecurityBuilder needs to be specified. " + + "Typically this done by adding a @Configuration that extends WebSecurityConfigurerAdapter. " + + "More advanced users can invoke " + + WebSecurity.class.getSimpleName() + + ".addSecurityFilterChainBuilder directly"); + // 获取chain大小 + int chainSize = ignoredRequests.size() + securityFilterChainBuilders.size(); + // 设置安全过滤器链集合 + List securityFilterChains = new ArrayList<>( + chainSize); + // 遍历igoredRequests集合,该集合表示存储着忽略拦截的请求 + for (RequestMatcher ignoredRequest : ignoredRequests) { + securityFilterChains.add(new DefaultSecurityFilterChain(ignoredRequest)); + } + for (SecurityBuilder securityFilterChainBuilder : securityFilterChainBuilders) { + // 先调用securityFilterChainBuilder执行build()方法,然后添加进securityFilterChains集合中。之列build()方法是去迭代执行doBuild()逻辑。 + securityFilterChains.add(securityFilterChainBuilder.build()); + } + + // 创建一个FilterChainProxy对象,传入一个securityFilterChains对象 + FilterChainProxy filterChainProxy = new FilterChainProxy(securityFilterChains); + if (httpFirewall != null) { + // 设置http防火墙属性 + filterChainProxy.setFirewall(httpFirewall); + } + filterChainProxy.afterPropertiesSet(); + + Filter result = filterChainProxy; + if (debugEnabled) { + logger.warn("\n\n" + + "********************************************************************\n" + + "********** Security debugging is enabled. *************\n" + + "********** This may include sensitive information. *************\n" + + "********** Do not use in a production system! *************\n" + + "********************************************************************\n\n"); + result = new DebugFilter(filterChainProxy); + } + + // 执行一个线程 + postBuildAction.run(); + return result; + } + +``` + +在performBuild()方法中,会执行securityFilterChainBuilder的build()方法,该方法又会去迭代执行doBuild()方法,最终返回一个SecurityFilterChain对象。 + + +综上,本文对WebSecurity的建造逻辑进行了概括性的讲解,更加深入的内容还需要进行代码debug跟踪深入查看,这样才能加深对于WebSecurity的理解。 + +## 总结 +- 安全配置类是在HttpSecurity、XXConfigurerAdapter等类中进行添加 +- WebSecurity和HttpSecurity区别在于,WebSecurity是用于建造FilterChainProxy的,它是包含HttpSecurity;而HttpSecurity是用于对请求进行限制,同时还用于建造DefaultSecurityFilterChain +- 安全配置类主要是通过configure方法向sharedObject缓存对象中添加过滤器,并最终添加进FilterChainProxy过滤器链中 +- WebSecurity建造FilterChainProxy的核心逻辑可以笼统的分为三步:安全配置了初始化、安全配置类configure的调用、performBuild的调用 + +## 相关文章 +- [从零开始系统学习SpringSecurity和OAuth2(一)—— 初识SpringSecurity](https://github.com/coderbruis/JavaSourceCodeLearning/blob/master/note/SpringSecurity/%E4%BB%8E%E9%9B%B6%E5%BC%80%E5%A7%8B%E7%B3%BB%E7%BB%9F%E5%AD%A6%E4%B9%A0SpringSecurity%E5%92%8COAuth2%EF%BC%88%E4%B8%80%EF%BC%89%E2%80%94%E2%80%94%20%E5%88%9D%E8%AF%86SpringSecurity.md) +- [从零开始系统学习SpringSecurity和OAuth2(二)—— 安全过滤器FilterChainProxy](https://github.com/coderbruis/JavaSourceCodeLearning/blob/master/note/SpringSecurity/%E4%BB%8E%E9%9B%B6%E5%BC%80%E5%A7%8B%E7%B3%BB%E7%BB%9F%E5%AD%A6%E4%B9%A0SpringSecurity%E5%92%8COAuth2%EF%BC%88%E4%BA%8C%EF%BC%89%E2%80%94%E2%80%94%20%E5%AE%89%E5%85%A8%E8%BF%87%E6%BB%A4%E5%99%A8FilterChainProxy.md) \ No newline at end of file diff --git "a/note/SpringSecurity/\344\273\216\351\233\266\345\274\200\345\247\213\347\263\273\347\273\237\345\255\246\344\271\240SpringSecurity\345\222\214OAuth2\357\274\210\344\272\214\357\274\211\342\200\224\342\200\224 \345\256\211\345\205\250\350\277\207\346\273\244\345\231\250FilterChainProxy.md" "b/note/SpringSecurity/\344\273\216\351\233\266\345\274\200\345\247\213\347\263\273\347\273\237\345\255\246\344\271\240SpringSecurity\345\222\214OAuth2\357\274\210\344\272\214\357\274\211\342\200\224\342\200\224 \345\256\211\345\205\250\350\277\207\346\273\244\345\231\250FilterChainProxy.md" new file mode 100644 index 0000000..2bd7243 --- /dev/null +++ "b/note/SpringSecurity/\344\273\216\351\233\266\345\274\200\345\247\213\347\263\273\347\273\237\345\255\246\344\271\240SpringSecurity\345\222\214OAuth2\357\274\210\344\272\214\357\274\211\342\200\224\342\200\224 \345\256\211\345\205\250\350\277\207\346\273\244\345\231\250FilterChainProxy.md" @@ -0,0 +1,370 @@ +- [前言](#前言) +- [正文](#正文) + - [1. FilterChainProxy什么时候注入Spring容器中的](#1--filterchainproxy什么时候注入spring容器中的) + - [2. WebSecurityConfiguration类](#2-websecurityconfiguration类) + - [3. WebSecurity类](#3-websecurity类) + - [4. AbstractConfiguredSecurityBuilder类](#4-abstractconfiguredsecuritybuilder类) + - [5. SecurityConfigurer类](#5-securityconfigurer类) + - [6. doBuild()方法](#6-dobuild方法) + - [7. WebSecurity什么时候被创建的?](#7-websecurity什么时候被创建的) +- [总结](#总结) +- [参考](#参考) +- [相关文章](#相关文章) + + +## 前言 +相信了解过SpringSecurity或者是OAuth2的读者,会发现网上会有非常多的相关文章,或是纯概念的,或是带有demo的,无论是哪种类型的文章,本人去阅读之后,对于整个框架的概念还是一知半解,也仅仅是实现了某些功能、某些效果而已,若遇到某些问题时无从下手,只能去百度去Google。这是因为对于SpringSecurity和OAuth2的知识没有一个整体概念的把握,知识体系没有形成系统,遂决定写一个关于SpringSecurity和OAuth2的系列专栏,在建造自己知识体系的同时还希望能帮助有同样困惑的同学。 + +**本专栏希望通过通俗易懂的语言来达到从入门概念、demo开始,由简入深,最终能达到深谙其底层源码的目的。** + +============== 分割线 ============== + +上篇文章粗略的讲解了SpringSecurity里的几个核心概念,本篇文章就来深扒一下安全过滤器FilterChainProxy的底层原理。 + +## 正文 +### 1. FilterChainProxy什么时候注入Spring容器中的 +在SpringSecurity中,FilterChainProxy是作为整个框架的核心,它是一个过滤器。在SpringSecurity中,一个过滤器名字叫springSecurityFilterChain,它的类型就是FilterChainProxy。 + +在@EnableWebSecurity注解中,@Import了一个关键类:WebSecurityConfiguration。 + +首先,WebSecurityConfiguration实现了ImportAware和BeanClassLoaderAware接口,分别实现了setImportMetadata()和setBeanClassLoader() + +setImportMetadata()方法的作用是注入注解元数据。 +```Java + public void setImportMetadata(AnnotationMetadata importMetadata) { + // 从注入的importMetadata中获取@EnableWebSecurity注解map值 + Map enableWebSecurityAttrMap = importMetadata + .getAnnotationAttributes(EnableWebSecurity.class.getName()); + // 将map值封装成AnnotationAttributes + AnnotationAttributes enableWebSecurityAttrs = AnnotationAttributes + .fromMap(enableWebSecurityAttrMap); + // 从AnnotationAttributes中获取debug属性值 + debugEnabled = enableWebSecurityAttrs.getBoolean("debug"); + if (webSecurity != null) { + // 将debugEnabled存到WebSecurity属性中 + webSecurity.debug(debugEnabled); + } + } +``` +setBeanClassLoader方法作用就是注入类加载器ClassLoader。 +```Java + public void setBeanClassLoader(ClassLoader classLoader) { + this.beanClassLoader = classLoader; + } +``` + +废话不多说,看下FilterChainProxy是什么时候注入到Spring容器中的。 + +答案就在WebSecurityConfiguration的springSecurityFilterChain()方法中 + +```Java + // AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME的值是:springSecurityFilterChain + // 所以springSecurityFilterChain()的作用就是想Spring容器中注入一个名为springSecurityChain的bean。 + @Bean(name = AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME) + public Filter springSecurityFilterChain() throws Exception { + // 判断是否有webSecurityConfigurers + boolean hasConfigurers = webSecurityConfigurers != null + && !webSecurityConfigurers.isEmpty(); + // 如果没有配置,则通过objectObjectPostProcessor来生成一个新的WebSecurityConfigurerAdapter + if (!hasConfigurers) { + WebSecurityConfigurerAdapter adapter = objectObjectPostProcessor + .postProcess(new WebSecurityConfigurerAdapter() { + }); + webSecurity.apply(adapter); + } + return webSecurity.build(); + } +``` + +### 2. WebSecurityConfiguration类 +在深入springSecurityFilterChain()方法底层原理之前,需要先了解WebSecurityConfiguration中几个重要的成员变量。 +```Java +@Configuration +public class WebSecurityConfiguration implements ImportAware, BeanClassLoaderAware { + + // WebSecurity是用于建造FilterChainProxy过滤器的核心类 + private WebSecurity webSecurity; + + // 读取@EnableWebSecurity中的注解属性debugEnabled的值 + private Boolean debugEnabled; + + // SecurityConfigurer属性值集合 + private List> webSecurityConfigurers; + + // 类加载器 + private ClassLoader beanClassLoader; + + // 省略 +} +``` + +### 3. WebSecurity类 +第一个重要的类就是WebSecurity。 + +由WebSecurity的类注释可以了解到: + +- WebSecurity是由WebSecurityConfiguration创建的,用于创建FilterChainProxy +- FilterChainProxy在SpringSecurity中,以springSecurityFilterChainProxy的bean名称暴露于容器之中的 +- 可以通过基础抽象类WebSecurityConfigurerAdapter来定制化WebSecurity + +由于@EnableWebSecurity导入了WebSecurityConfiguration,因而WebSecurity也被创建。 + +我们先来看下WebSecurity的类结构: + +```Java +public final class WebSecurity extends + AbstractConfiguredSecurityBuilder implements + SecurityBuilder, ApplicationContextAware { + // 省略 +} +``` + +首先,WebSecurity是一个final修饰的类,即该类: + +- 不能被继承,没有子类 +- 该类方法都为final + +其次,WebSecurity继承了AbstractConfiguredSecurityBuilder。继续跟进看下AbstractConfiguredSecurityBuilder这个类有什么作用? + +### 4. AbstractConfiguredSecurityBuilder类 +由其类名:AbstractConfiguredSecurityBuilder就可以知道,该类是一个抽象类,作为抽象类,必然会抽象出abstract方法让子类去实现。浏览AbstractConfiguredSecurityBuilder的方法定义,可以看到它内部只定义了一个抽象方法: +```Java +protected abstract O performBuild() throws Exception; +``` + +这个方法会在建造FilterChainProxy时有使用到,这里先留个印象。 + +回到AbstractConfiguredSecurityBuilder类定义 +```Java +public abstract class AbstractConfiguredSecurityBuilder> + extends AbstractSecurityBuilder { + // 省略 +} +``` +可以知道它继承了AbstractSecurityBuilder,但自身带的一个泛型>让人捉摸不透是啥意思。O是啥?B又是啥? + +这里其实是用到了**建造者模式**。 + +目光聚焦到SecurityBuilder这个接口 + +```Java +// 安全建造者 +// 顾名思义是一个builder建造者,创建并返回一个类型为O的对象 +public interface SecurityBuilder { + O build() throws Exception; +} +``` + +```Java +// 抽象安全建造者 +public abstract class AbstractSecurityBuilder implements SecurityBuilder { + private AtomicBoolean building = new AtomicBoolean(); + private O object; + + // build建造方法被final修饰,不允许子类覆盖。 + public final O build() throws Exception { + if (this.building.compareAndSet(false, true)) { + this.object = doBuild(); + return this.object; + } + throw new AlreadyBuiltException("This object has already been built"); + } + + // 子类需要重写doBuild()方法 + protected abstract O doBuild() throws Exception; +} +``` +所以B extends SecurityBuilder就是指B是SecurityBuilder的子类,用于建造O。 + +从WebSecurity中的类定义可以发现 +```Java +AbstractConfiguredSecurityBuilder +``` + +AbstractConfiguredSecurityBuilder作用就是通过WebSecurity这个建造者建造出一个Filter,而这个Filter就是FilterChainProxy。 + +**因此O就是指FilterChainProxy,B就是指的安全建造者WebSecurity。** + +


+ +再来看下AbstractConfiguredSecurityBuilder的成员变量。 + +```Java +public abstract class AbstractConfiguredSecurityBuilder> + extends AbstractSecurityBuilder { + private final Log logger = LogFactory.getLog(getClass()); + + // 安全配置器的子类集合,这个configurers就是用于初始化以及配置FilterChainProxy中的filters用的 + private final LinkedHashMap>, List>> configurers = new LinkedHashMap<>(); + // 初始化期间添加的配置类 + private final List> configurersAddedInInitializing = new ArrayList<>(); + // 缓存 + private final Map, Object> sharedObjects = new HashMap<>(); + + private final boolean allowConfigurersOfSameType; + + // 建造者的建造状态,初始状态为:未建造 + private BuildState buildState = BuildState.UNBUILT; + + // 用于生成bean + private ObjectPostProcessor objectPostProcessor; + // 省略 +} +``` + + +### 5. SecurityConfigurer类 +SecurityConfigurer是一个接口,它就是指的安全配置器,看下它的类定义信息。 + +- SecurityConfigurer允许用于配置安全建造者(SecurityBuilder) +- 安全建造者(SecurityBuilder)在初始化时调用init方法,同时会初始化所有的SecurityConfigurer +- 安全建造者(SecurityBuilder)在调用configure方法时,同时会调用所有SecurityConfigurer的configure + +```Java +public interface SecurityConfigurer> { + + void init(B builder) throws Exception; + + void configure(B builder) throws Exception; +} +``` + +SecurityConfigurer> 这个该怎么理解呢? + +这里同上,O指的FilterChainProxy,B指的SecurityBuilder。 + +那么这里的SecurityConfigurer到底有什么作用呢? + +> 答:SecurityConfigurer就是用于向FilterChainProxy中添加过滤器。 + +

+ +```Java +private final LinkedHashMap>, List>> configurers = new LinkedHashMap<>(); +``` +所以configurers就是以建造者为key,各种配置类为value的一个LinkedHashMap。 + +### 6. doBuild()方法 +下面来看下AbstractConfiguredSecurityBuilder的doBuild()方法。doBuild()方法在之前提到过,它是AbstractSecurityBuilder抽象出来的方法,由AbstractConfiguredSecurityBuilder类来实现。这个方法会通过使用SecurityConfigurer去执行建造逻辑,建造逻辑主要分为五大步: + +1. 执行初始化前的所需做的额外工作(beforeInit()) +2. 执行初始化(init()) +3. 执行配置前所需的额外工作(beforeConfigure()) +4. 执行配置调用(configure) +5. **执行建造(performBuild())** + +```Java + @Override + protected final O doBuild() throws Exception { + synchronized (configurers) { + // 标记建造状态为初始化中 + buildState = BuildState.INITIALIZING; + + beforeInit(); + init(); + + // 标记正在配置 + buildState = BuildState.CONFIGURING; + + beforeConfigure(); + configure(); + + // 标记正在建造 + buildState = BuildState.BUILDING; + + O result = performBuild(); + + // 标记以及建造完成 + buildState = BuildState.BUILT; + + return result; + } + } + +``` + +performBuild()方法是AbstractConfiguredSecurityBuilder提供的抽象方法,具体有其子类:HttpSecurity和WebSecurity来实现。 + +### 7. WebSecurity什么时候被创建的? +答案就是在WebSecurityConfiguration的setFilterChainProxySecurityConfiguere()方法里。 +```Java + @Autowired(required = false) + public void setFilterChainProxySecurityConfigurer( + ObjectPostProcessor objectPostProcessor, + @Value("#{@autowiredWebSecurityConfigurersIgnoreParents.getWebSecurityConfigurers()}") List> webSecurityConfigurers) + throws Exception { + + // 通过objectPostProcessor来创建一个WebSecurity,同时将objectPostProcessor传进WebSecurity对象中 + webSecurity = objectPostProcessor + .postProcess(new WebSecurity(objectPostProcessor)); + // 从@EnableWebSecurity中获取debugEnabled注解值,是否开启debug模式 + if (debugEnabled != null) { + webSecurity.debug(debugEnabled); + } + + // 对webSecurityConfigurers进行排序 + webSecurityConfigurers.sort(AnnotationAwareOrderComparator.INSTANCE); + + Integer previousOrder = null; + Object previousConfig = null; + + for (SecurityConfigurer config : webSecurityConfigurers) { + Integer order = AnnotationAwareOrderComparator.lookupOrder(config); + if (previousOrder != null && previousOrder.equals(order)) { + throw new IllegalStateException( + "@Order on WebSecurityConfigurers must be unique. Order of " + + order + " was already used on " + previousConfig + ", so it cannot be used on " + + config + " too."); + } + previousOrder = order; + previousConfig = config; + } + + // 遍历webSecurityConfigurers + for (SecurityConfigurer webSecurityConfigurer : webSecurityConfigurers) { + // 添加进WebSecurity中的配置器Map中 + webSecurity.apply(webSecurityConfigurer); + } + this.webSecurityConfigurers = webSecurityConfigurers; + } +``` +方法中的webSecurityConfigurers是通过了@Value注解来注入的bean集合,@Value表达式中又包含了一个autowiredWebSecurityConfigurersIgnoreParents.getWebSecurityConfigurers()的调用。 + +进入AutowiredWebSecurityConfigurersIgnoreParents类,查看其方法getWebSecurityConfigurers()方法。 +```Java + public List> getWebSecurityConfigurers() { + // 初始化webSecurityConfigurers集合 + List> webSecurityConfigurers = new ArrayList<>(); + // 通过beanFactory来获取所有注入Spring容器中的WebSecurityConfigurer子类,实际就是WebSecurityConfigurer + Map beansOfType = beanFactory + .getBeansOfType(WebSecurityConfigurer.class); + for (Entry entry : beansOfType.entrySet()) { + // 遍历出bean对象 + webSecurityConfigurers.add(entry.getValue()); + } + return webSecurityConfigurers; + } +``` + +## 总结 +全文讲解了FilterChainProxy以及WebSecurityConfiguration,但是对于doBuild()方法没有进行细致的分析讲解,由于doBuild()的逻辑比较复杂,将在下一章进行细致分析。 + +在WebSecurityConfiguration中,需要关注两个方法: + +1. springSecurityFilterChain()方法 +2. setFilterChainProxySecurityConfigurer()方法 + +**springSecurityFilterChain方法用于通过安全建造器WebSecurity来建造出FilterChainProxy。** + +**setFilterChainProxySecurityConfigurer()方法是用于创建出安全建造起WebSecurity。** + +**在SpringSecurity中,对于WebSecurity,使用了建造者设计模式。** + + + +## 参考 +- [深入浅出Spring Security(一):三句话解释框架原理](https://blog.csdn.net/zimou5581/article/details/102457672) + +## 相关文章 +- [从零开始系统学习SpringSecurity和OAuth2(一)—— 初识SpringSecurity](https://github.com/coderbruis/JavaSourceCodeLearning/blob/master/note/SpringSecurity/%E4%BB%8E%E9%9B%B6%E5%BC%80%E5%A7%8B%E7%B3%BB%E7%BB%9F%E5%AD%A6%E4%B9%A0SpringSecurity%E5%92%8COAuth2%EF%BC%88%E4%B8%80%EF%BC%89%E2%80%94%E2%80%94%20%E5%88%9D%E8%AF%86SpringSecurity.md) +- [从零开始系统学习SpringSecurity和OAuth2(三)—— WebSecurity建造核心逻辑](https://github.com/coderbruis/JavaSourceCodeLearning/blob/master/note/SpringSecurity/%E4%BB%8E%E9%9B%B6%E5%BC%80%E5%A7%8B%E7%B3%BB%E7%BB%9F%E5%AD%A6%E4%B9%A0SpringSecurity%E5%92%8COAuth2%EF%BC%88%E4%B8%89%EF%BC%89%E2%80%94%E2%80%94%20WebSecurity%E5%BB%BA%E9%80%A0%E6%A0%B8%E5%BF%83%E9%80%BB%E8%BE%91.md) diff --git "a/note/SpringSecurity/\344\273\216\351\233\266\345\274\200\345\247\213\347\263\273\347\273\237\345\255\246\344\271\240SpringSecurity\345\222\214OAuth2\357\274\210\345\233\233\357\274\211\342\200\224\342\200\224 FilterChainProxy\350\277\207\346\273\244\345\231\250\351\223\276\344\270\255\347\232\204\345\207\240\344\270\252\351\207\215\350\246\201\347\232\204\350\277\207\346\273\244\345\231\250.md" "b/note/SpringSecurity/\344\273\216\351\233\266\345\274\200\345\247\213\347\263\273\347\273\237\345\255\246\344\271\240SpringSecurity\345\222\214OAuth2\357\274\210\345\233\233\357\274\211\342\200\224\342\200\224 FilterChainProxy\350\277\207\346\273\244\345\231\250\351\223\276\344\270\255\347\232\204\345\207\240\344\270\252\351\207\215\350\246\201\347\232\204\350\277\207\346\273\244\345\231\250.md" new file mode 100644 index 0000000..fb4c0b5 --- /dev/null +++ "b/note/SpringSecurity/\344\273\216\351\233\266\345\274\200\345\247\213\347\263\273\347\273\237\345\255\246\344\271\240SpringSecurity\345\222\214OAuth2\357\274\210\345\233\233\357\274\211\342\200\224\342\200\224 FilterChainProxy\350\277\207\346\273\244\345\231\250\351\223\276\344\270\255\347\232\204\345\207\240\344\270\252\351\207\215\350\246\201\347\232\204\350\277\207\346\273\244\345\231\250.md" @@ -0,0 +1,416 @@ +## 前言 +相信了解过SpringSecurity或者是OAuth2的读者,会发现网上会有非常多的相关文章,或是纯概念的,或是带有demo的,无论是哪种类型的文章,本人去阅读之后,对于整个框架的概念还是一知半解,也仅仅是实现了某些功能、某些效果而已,若遇到某些问题时无从下手,只能去百度去Google。这是因为对于SpringSecurity和OAuth2的知识没有一个整体概念的把握,知识体系没有形成系统,遂决定写一个关于SpringSecurity和OAuth2的系列专栏,在建造自己知识体系的同时还希望能帮助有同样困惑的同学。 + +============== 分割线 ============== + +本章重点介绍一下FilterChainProxy过滤器链中的SecurityContextPersistenceFilter以及ExceptionTranslationFilter。 + +## 正文 +在系列文章中的第一篇中,已经用一张图介绍了FilterChainProxy在整个Filter中的所处位置以及包含在FilterChainProxy的各种Filter。 +地址如下: + +[从零开始系统学习SpringSecurity和OAuth2(一)—— 初识SpringSecurity](https://blog.csdn.net/CoderBruis/article/details/107297547) + +本篇文章就讲解一下SecurityContextPersistenceFilter、ExceptionTranslationFilter和FilterSecurityInterceptor这三个过滤器,注意这里的FilterSecurityInterceptor就是一个过滤器。 + +### 1. SecurityContextPersistenceFilter +这个过滤器是FilterChainProxy过滤器链的中第一个调用的,先看下它的官方类注释的内容,总结为以下几点: +1. 在请求之前,使用从已配置的SecurityContextRepository中获取的认证信息来填充SecurityContextHolder,在请求完成后清除上下文所有者。 +2. 默认情况下,SecurityContextRepository使用的是HttpSessionSecurityContextRepository作为实现类,有关于HttpSession的配置选项信息请查看HttpSessionSecurityContextRepository。 +3. 这个过滤器会在每次请求时都会调用,为的是解决servlet容器的兼容性(特别是Weblogic)。 +4. 这个过滤器必须在任何认证处理机制调用前执行,例如BASIC、CAS认证处理过滤器等都期望在它们执行时能从SecurityContextHolder中获取一个合法的SecurityContext。 +5. 这个过滤器实质上是对HttpSessionSecurityContextRepository进行了重构,以将存储问题委托给了单独的策略,从而允许在请求之间维护安全上下文的方式进行更多自定义。 + +首先看下SecurityContextPersistenceFilter的类结构: +![在这里插入图片描述](https://img-blog.csdnimg.cn/20200819004933998.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0NvZGVyQnJ1aXM=,size_16,color_FFFFFF,t_70#pic_center) +首先它的父类GenericeFilterBean实现了很多接口,其中有三个XXAware的接口,表示的是具备注入XX到GenericFilterBean能力,而这一般都是通过setter来注入XX实现的。 + +而实现了DisposalbleBean表明了在注销bean时能进行额外的工作,实现InitializingBean表明了能在初始化时进行额外的工作。 + +#### 1.1 源码分析 + +```Java +public class SecurityContextPersistenceFilter extends GenericFilterBean { + + static final String FILTER_APPLIED = "__spring_security_scpf_applied"; + + private SecurityContextRepository repo; + + private boolean forceEagerSessionCreation = false; + + // 默认构造方法,传入HttpSessionSecurityContextRepository + public SecurityContextPersistenceFilter() { + this(new HttpSessionSecurityContextRepository()); + } + + public SecurityContextPersistenceFilter(SecurityContextRepository repo) { + this.repo = repo; + } + + // 过滤核心方法:doFilter + public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) + throws IOException, ServletException { + HttpServletRequest request = (HttpServletRequest) req; + HttpServletResponse response = (HttpServletResponse) res; + + // 判断请求中是否包含属性:__spring_security_scpf_applied,表示已经调用过SecurityContextPersistenceFilter了 + if (request.getAttribute(FILTER_APPLIED) != null) { + // 确保每次请求只调用一次该过滤器 + chain.doFilter(request, response); + return; + } + + final boolean debug = logger.isDebugEnabled(); + // 为此次请求设置__spring_security_scpf_applied为true,防止下次同样请求再次调用进来时,重复执行以下逻辑 + request.setAttribute(FILTER_APPLIED, Boolean.TRUE); + + // 急切的想要创建Session + if (forceEagerSessionCreation) { + HttpSession session = request.getSession(); + + if (debug && session.isNew()) { + logger.debug("Eagerly created session: " + session.getId()); + } + } + + HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request, + response); + // 从HttpSessionSecurityContextRepository中获取SecurityContext安全上下文 + SecurityContext contextBeforeChainExecution = repo.loadContext(holder); + + try { + // 将获取的安全上下文存储到SecurityContextHolder中 + SecurityContextHolder.setContext(contextBeforeChainExecution); + // 放过滤器链执行 + chain.doFilter(holder.getRequest(), holder.getResponse()); + + } + finally { + // 等FilterChainProxy后面所有过滤器链都执行完毕时,进入finally块 + // 获取FilterChainProxy调用后的SecurityContext + SecurityContext contextAfterChainExecution = SecurityContextHolder + .getContext(); + // 清除SecurityContextHolder + SecurityContextHolder.clearContext(); + // 将安全上下文存储到HttpSessionSecurityContextRepository中,也就是持久化到Session中 + repo.saveContext(contextAfterChainExecution, holder.getRequest(), + holder.getResponse()); + request.removeAttribute(FILTER_APPLIED); + + if (debug) { + logger.debug("SecurityContextHolder now cleared, as request processing completed"); + } + } + } + + public void setForceEagerSessionCreation(boolean forceEagerSessionCreation) { + this.forceEagerSessionCreation = forceEagerSessionCreation; + } +} + +``` + +从SecurityContextPersistenceFilter类的作用可以看出,它其实就是持久化SecurityContext。 + +### 2. ExceptionTranslationFilter +先看下ExceptionTranslationFilter的类注释,总结为以下几点: +1. 此过滤器会处理任何AccessDeniedException和AuthenticationException的异常。 +2. 此过滤器是必要的,因为它提供了一个桥梁用于连接Java异常和HTTP响应。它仅和维护用户界面有关,而不会执行任何的安全性强制措施。 +3. 如果此过滤器捕获到了AuthenticationException,该Filter会加载AuthenticationEntrypoint。它允许处理任何从AbstractSecurityInterceptor子类抛出的authentication异常,AbstractSecurityInterceptor的子类即FilterChainProxy中包含的哪些过滤器。 +4. 如果捕获到了AccessDeniedException,此过滤器会判断当前用户是否是一个匿名用户。如果是匿名用户,则加载authenticationEntryPoint。如果不是匿名用户,则此过滤器会将逻辑代理到AccessDeniedHandler,由其处理接下来的逻辑。 +5. authenticationEntryPoint指示如果检测到AuthenticationException,则通过调用authenticationEntrypoint的commence方法开始认证过程的处理。需要注意的是,在ExceptionTranslationFilter中的requestCache用于保存身份验证过程中的认证结果,一边可以在用户认证通过后即可检索以及重用,requestCache的默认实现是HttpSessionRequestCache。 + +**小结:ExceptionTranslationFilter的作用即捕获AuthenticationException和AccessDeniedException,并作出相应的处理;对于捕获AccessDeniedException时,如果是匿名用户则去调用authenticationEntryPoint去进行身份验证,如果不是匿名用户则直接抛出AccessDeniedException。** + +#### 2.1 源码分析 + +先判断看下ExceptionTranslationFilter的成员变量 +```Java +public class ExceptionTranslationFilter extends GenericFilterBean { + + // AccessDeniedException处理器 + private AccessDeniedHandler accessDeniedHandler = new AccessDeniedHandlerImpl(); + // 用于进行身份验证的端点 + private AuthenticationEntryPoint authenticationEntryPoint; + + // 身份认证信任机制,包括判断是否是匿名,判断是否是RememberMe + private AuthenticationTrustResolver authenticationTrustResolver = new AuthenticationTrustResolverImpl(); + // 异常分析器 + private ThrowableAnalyzer throwableAnalyzer = new DefaultThrowableAnalyzer(); + + // 将身份认证结果存储在HttpSession中 + private RequestCache requestCache = new HttpSessionRequestCache(); + + // 消息源转化器 + private final MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor(); + + public ExceptionTranslationFilter(AuthenticationEntryPoint authenticationEntryPoint) { + this(authenticationEntryPoint, new HttpSessionRequestCache()); + } + + public ExceptionTranslationFilter(AuthenticationEntryPoint authenticationEntryPoint, + RequestCache requestCache) { + Assert.notNull(authenticationEntryPoint, + "authenticationEntryPoint cannot be null"); + Assert.notNull(requestCache, "requestCache cannot be null"); + this.authenticationEntryPoint = authenticationEntryPoint; + this.requestCache = requestCache; + } + + // 省略 +} + +``` + +#### 2.2 doFilter源码分析 + +```Java + public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) + throws IOException, ServletException { + HttpServletRequest request = (HttpServletRequest) req; + HttpServletResponse response = (HttpServletResponse) res; + + try { + // 继续调用下一个过滤器链 + chain.doFilter(request, response); + + logger.debug("Chain processed normally"); + } + catch (IOException ex) { + throw ex; + } + catch (Exception ex) { + // 尝试去获取SpringSecurityException异常 + Throwable[] causeChain = throwableAnalyzer.determineCauseChain(ex); + // 转化为运行时异常 + RuntimeException ase = (AuthenticationException) throwableAnalyzer + .getFirstThrowableOfType(AuthenticationException.class, causeChain); + + if (ase == null) { + ase = (AccessDeniedException) throwableAnalyzer.getFirstThrowableOfType( + AccessDeniedException.class, causeChain); + } + + if (ase != null) { + if (response.isCommitted()) { + throw new ServletException("Unable to handle the Spring Security Exception because the response is already committed.", ex); + } + // 处理SpringSecurity异常 + handleSpringSecurityException(request, response, chain, ase); + } + else { + // Rethrow ServletExceptions and RuntimeExceptions as-is + if (ex instanceof ServletException) { + throw (ServletException) ex; + } + else if (ex instanceof RuntimeException) { + throw (RuntimeException) ex; + } + + // Wrap other Exceptions. This shouldn't actually happen + // as we've already covered all the possibilities for doFilter + throw new RuntimeException(ex); + } + } + } + +``` + +```Java + // SpringSecurityException异常处理的核心逻辑 + private void handleSpringSecurityException(HttpServletRequest request, + HttpServletResponse response, FilterChain chain, RuntimeException exception) + throws IOException, ServletException { + // 如果是认证异常 + if (exception instanceof AuthenticationException) { + logger.debug( + "Authentication exception occurred; redirecting to authentication entry point", + exception); + // 开始进行身份认证 + sendStartAuthentication(request, response, chain, + (AuthenticationException) exception); + } + else if (exception instanceof AccessDeniedException) { // 如果是访问拒绝异常 + // 尝试从SecurityContextHolder缓存中获取认证结果 + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + // 判断认证结果是否是匿名的或者是rememberme + if (authenticationTrustResolver.isAnonymous(authentication) || authenticationTrustResolver.isRememberMe(authentication)) { + logger.debug( + "Access is denied (user is " + (authenticationTrustResolver.isAnonymous(authentication) ? "anonymous" : "not fully authenticated") + "); redirecting to authentication entry point", + exception); + // 开始进行身份认证 + sendStartAuthentication( + request, + response, + chain, + new InsufficientAuthenticationException( + messages.getMessage( + "ExceptionTranslationFilter.insufficientAuthentication", + "Full authentication is required to access this resource"))); + } + else { + + logger.debug( + "Access is denied (user is not anonymous); delegating to AccessDeniedHandler", + exception); + // 如果既不是匿名用户也不是rememberme用户,则调用访问拒绝处理器 + accessDeniedHandler.handle(request, response, + (AccessDeniedException) exception); + } + } + } +``` + +```Java + protected void sendStartAuthentication(HttpServletRequest request, + HttpServletResponse response, FilterChain chain, + AuthenticationException reason) throws ServletException, IOException { + // 清空缓存中的认证结果,重新进行身份验证 + SecurityContextHolder.getContext().setAuthentication(null); + // 将认证请求request和响应response存储在session中 + requestCache.saveRequest(request, response); + logger.debug("Calling Authentication entry point."); + // 进行身份验证 + authenticationEntryPoint.commence(request, response, reason); + } +``` +先看下commence的实现类: +![在这里插入图片描述](https://img-blog.csdnimg.cn/20200819095038864.png#pic_center) + +这里这么多实现类,到底调用哪一个呢?这就要看下authenticationEntryPoint注入的什么实现类了,可以将断点打在ExceptionTranslationFilter的构造方法中。 +![在这里插入图片描述](https://img-blog.csdnimg.cn/20200819095634222.png#pic_center) + +启动项目之后,进入方法调用栈,可以在图中位置看到在进行安全配置类配置时,会调用ExceptionHandlingConfigurer这个配置类的configure方法。 +![在这里插入图片描述](https://img-blog.csdnimg.cn/20200819095929151.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0NvZGVyQnJ1aXM=,size_16,color_FFFFFF,t_70#pic_center) +#### 2.3 ExceptionHandlingConfigurer + +进入其configure方法查看 + +```Java + @Override + public void configure(H http) throws Exception { + // 获取authenticationEntryPoint + AuthenticationEntryPoint entryPoint = getAuthenticationEntryPoint(http); + // 新建一个ExceptionTranslationFilter对象 + ExceptionTranslationFilter exceptionTranslationFilter = new ExceptionTranslationFilter( + entryPoint, getRequestCache(http)); + // 或获取访问拒绝处理器 + AccessDeniedHandler deniedHandler = getAccessDeniedHandler(http); + exceptionTranslationFilter.setAccessDeniedHandler(deniedHandler); + exceptionTranslationFilter = postProcess(exceptionTranslationFilter); + // 往FilterChainProxy中添加ExceptionTranslationFilter + http.addFilter(exceptionTranslationFilter); + } +``` +![在这里插入图片描述](https://img-blog.csdnimg.cn/20200819100503737.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0NvZGVyQnJ1aXM=,size_16,color_FFFFFF,t_70#pic_center) +可以发现在实例化完ExceptionHandlingConfigurer后,依然没有注入authenticationEntryPoint。所以是在调用configure方法时,去调用getAuthenticationEntryPoint()去获取authenticationEntryPoint。 + +下面接着查看一下getAuthenticationEntryPoint()方法 + +```Java + AuthenticationEntryPoint getAuthenticationEntryPoint(H http) { + AuthenticationEntryPoint entryPoint = this.authenticationEntryPoint; + // 由于entryPoint为空,所以调用createDefaultEntryPoint去创建entryPoint + if (entryPoint == null) { + entryPoint = createDefaultEntryPoint(http); + } + return entryPoint; + } +``` + +```Java + private AuthenticationEntryPoint createDefaultEntryPoint(H http) { + // 如果entryPointMappings为空,则返回Http403ForbiddenEntryPoint + if (this.defaultEntryPointMappings.isEmpty()) { + return new Http403ForbiddenEntryPoint(); + } + if (this.defaultEntryPointMappings.size() == 1) { + // 遍历defaultEntryPointMappings,获取其中存储的entrypoint + return this.defaultEntryPointMappings.values().iterator().next(); + } + // 创建DelegatingAuthenticationEntryPoint这个代理类 + DelegatingAuthenticationEntryPoint entryPoint = new DelegatingAuthenticationEntryPoint( + this.defaultEntryPointMappings); + entryPoint.setDefaultEntryPoint(this.defaultEntryPointMappings.values().iterator() + .next()); + return entryPoint; + } +``` + +可以看出,最终返回的就是:Http403ForbiddenEntryPoint +![在这里插入图片描述](https://img-blog.csdnimg.cn/20200819101823377.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0NvZGVyQnJ1aXM=,size_16,color_FFFFFF,t_70#pic_center) + +可以看到,HTTP403ForbiddenEntryPiont这个类代码非常少 +```Java +public class Http403ForbiddenEntryPoint implements AuthenticationEntryPoint { + private static final Log logger = LogFactory.getLog(Http403ForbiddenEntryPoint.class); + + /** + * Always returns a 403 error code to the client. + */ + public void commence(HttpServletRequest request, HttpServletResponse response, + AuthenticationException arg2) throws IOException, ServletException { + if (logger.isDebugEnabled()) { + logger.debug("Pre-authenticated entry point called. Rejecting access"); + } + // 在response响应中添加403 Forbidden,访问拒绝异常 + response.sendError(HttpServletResponse.SC_FORBIDDEN, "Access Denied"); + } +} +``` + +这里,还讲解一下另外一个类LoginURLAuthenticationEntryPoint的方法commence。 + +```Java + public void commence(HttpServletRequest request, HttpServletResponse response, + AuthenticationException authException) throws IOException, ServletException { + // 重定向url + String redirectUrl = null; + + if (useForward) { + // 判断下请求协议是否是http + if (forceHttps && "http".equals(request.getScheme())) { + // 获取重定向完整的URL路径 + redirectUrl = buildHttpsRedirectUrlForRequest(request); + } + + if (redirectUrl == null) { + // 如果重定向地址为空,则获取默认的登录form表单地址;用户可以自定义设置; + String loginForm = determineUrlToUseForThisRequest(request, response, + authException); + + if (logger.isDebugEnabled()) { + logger.debug("Server side forward to: " + loginForm); + } + + RequestDispatcher dispatcher = request.getRequestDispatcher(loginForm); + + dispatcher.forward(request, response); + + return; + } + } + else { + redirectUrl = buildRedirectUrlToLoginPage(request, response, authException); + + } + // 发送重定向请求 + redirectStrategy.sendRedirect(request, response, redirectUrl); + } +``` + +LoginURLAuthenticationEntryPoint这个类其实就是重定向到login页面,如果用户不指定login页面,则重定向到默认的login页面。 + + +## 总结 + +本篇文章重点讲解了FilterChainProxy中的SecurityContextPersistenceFilter以及ExceptionTranslationFilter过滤器链,它们在SpringSecurity中都扮演着很重要的角色,用一句话来概括就是: +**SecurityContextPersistenceFilter用于持久化SecurityContext,而ExceptionTranslationFilter则用于捕获身份认证异常(AuthenticationException)和访问异常(AccessDeniedException),并处理这些异常。** + +然而FitlerChainProxy中还有一个非常重要的过滤器:FilterSecurityInterceptor,下一篇将重点讲解。 + +## 相关文章 + +- [从零开始系统学习SpringSecurity和OAuth2(一)—— 初识SpringSecurity](https://github.com/coderbruis/JavaSourceCodeLearning/blob/master/note/SpringSecurity/%E4%BB%8E%E9%9B%B6%E5%BC%80%E5%A7%8B%E7%B3%BB%E7%BB%9F%E5%AD%A6%E4%B9%A0SpringSecurity%E5%92%8COAuth2%EF%BC%88%E4%B8%80%EF%BC%89%E2%80%94%E2%80%94%20%E5%88%9D%E8%AF%86SpringSecurity.md) +- [从零开始系统学习SpringSecurity和OAuth2(二)—— 安全过滤器FilterChainProxy](https://github.com/coderbruis/JavaSourceCodeLearning/blob/master/note/SpringSecurity/%E4%BB%8E%E9%9B%B6%E5%BC%80%E5%A7%8B%E7%B3%BB%E7%BB%9F%E5%AD%A6%E4%B9%A0SpringSecurity%E5%92%8COAuth2%EF%BC%88%E4%BA%8C%EF%BC%89%E2%80%94%E2%80%94%20%E5%AE%89%E5%85%A8%E8%BF%87%E6%BB%A4%E5%99%A8FilterChainProxy.md) +- [从零开始系统学习SpringSecurity和OAuth2(三)—— WebSecurity建造核心逻辑](https://github.com/coderbruis/JavaSourceCodeLearning/blob/master/note/SpringSecurity/%E4%BB%8E%E9%9B%B6%E5%BC%80%E5%A7%8B%E7%B3%BB%E7%BB%9F%E5%AD%A6%E4%B9%A0SpringSecurity%E5%92%8COAuth2%EF%BC%88%E5%9B%9B%EF%BC%89%E2%80%94%E2%80%94%20FilterChainProxy%E8%BF%87%E6%BB%A4%E5%99%A8%E9%93%BE%E4%B8%AD%E7%9A%84%E5%87%A0%E4%B8%AA%E9%87%8D%E8%A6%81%E7%9A%84%E8%BF%87%E6%BB%A4%E5%99%A8.md) diff --git a/note/images/Dubbo/Dubbo-SPI-Test.png b/note/images/Dubbo/Dubbo-SPI-Test.png new file mode 100644 index 0000000..e2b80f5 Binary files /dev/null and b/note/images/Dubbo/Dubbo-SPI-Test.png differ diff --git a/note/images/Dubbo/dubbo-spi-01.png b/note/images/Dubbo/dubbo-spi-01.png new file mode 100644 index 0000000..ead3397 Binary files /dev/null and b/note/images/Dubbo/dubbo-spi-01.png differ diff --git a/note/images/Dubbo/dubbo-spi-02.png b/note/images/Dubbo/dubbo-spi-02.png new file mode 100644 index 0000000..1a01b6e Binary files /dev/null and b/note/images/Dubbo/dubbo-spi-02.png differ diff --git a/note/images/Dubbo/dubbo-spi-03.png b/note/images/Dubbo/dubbo-spi-03.png new file mode 100644 index 0000000..8b2e321 Binary files /dev/null and b/note/images/Dubbo/dubbo-spi-03.png differ diff --git a/note/images/Dubbo/dubbo-spi-04.png b/note/images/Dubbo/dubbo-spi-04.png new file mode 100644 index 0000000..48572f1 Binary files /dev/null and b/note/images/Dubbo/dubbo-spi-04.png differ diff --git a/note/images/Dubbo/dubbo01.png b/note/images/Dubbo/dubbo01.png new file mode 100644 index 0000000..1fe4442 Binary files /dev/null and b/note/images/Dubbo/dubbo01.png differ diff --git a/note/images/Dubbo/export01.png b/note/images/Dubbo/export01.png new file mode 100644 index 0000000..ea3fa24 Binary files /dev/null and b/note/images/Dubbo/export01.png differ diff --git a/note/images/Dubbo/export02.png b/note/images/Dubbo/export02.png new file mode 100644 index 0000000..609ece2 Binary files /dev/null and b/note/images/Dubbo/export02.png differ diff --git a/note/images/Dubbo/jdk-spi-driver.png b/note/images/Dubbo/jdk-spi-driver.png new file mode 100644 index 0000000..1d5841b Binary files /dev/null and b/note/images/Dubbo/jdk-spi-driver.png differ diff --git a/note/images/Dubbo/jdk-spi.png b/note/images/Dubbo/jdk-spi.png new file mode 100644 index 0000000..064f401 Binary files /dev/null and b/note/images/Dubbo/jdk-spi.png differ diff --git a/note/images/Dubbo/spi_@Adaptive.png b/note/images/Dubbo/spi_@Adaptive.png new file mode 100644 index 0000000..f5266fa Binary files /dev/null and b/note/images/Dubbo/spi_@Adaptive.png differ diff --git a/note/images/JDK/thread01.jpeg b/note/images/JDK/thread01.jpeg new file mode 100644 index 0000000..700a68a Binary files /dev/null and b/note/images/JDK/thread01.jpeg differ diff --git a/note/images/JDK/thread02.jpeg b/note/images/JDK/thread02.jpeg new file mode 100644 index 0000000..5b9e3cc Binary files /dev/null and b/note/images/JDK/thread02.jpeg differ diff --git a/note/images/JDK/threadcycle.jpg b/note/images/JDK/threadcycle.jpg new file mode 100644 index 0000000..5aa0822 Binary files /dev/null and b/note/images/JDK/threadcycle.jpg differ diff --git a/note/images/JDK/volatile-01.png b/note/images/JDK/volatile-01.png new file mode 100644 index 0000000..61eb672 Binary files /dev/null and b/note/images/JDK/volatile-01.png differ diff --git a/note/images/JDK/volatile-02.png b/note/images/JDK/volatile-02.png new file mode 100644 index 0000000..a288136 Binary files /dev/null and b/note/images/JDK/volatile-02.png differ diff --git a/note/images/JDK/volatile-03.png b/note/images/JDK/volatile-03.png new file mode 100644 index 0000000..7ace8b9 Binary files /dev/null and b/note/images/JDK/volatile-03.png differ diff --git a/note/images/JDK/volatile-04.png b/note/images/JDK/volatile-04.png new file mode 100644 index 0000000..44b287a Binary files /dev/null and b/note/images/JDK/volatile-04.png differ diff --git a/note/images/JavaSourceCodeLearningImage.png b/note/images/JavaSourceCodeLearningImage.png new file mode 100644 index 0000000..9ff833e Binary files /dev/null and b/note/images/JavaSourceCodeLearningImage.png differ diff --git a/note/images/RPC/rpc.jpg b/note/images/RPC/rpc.jpg new file mode 100644 index 0000000..92ecd60 Binary files /dev/null and b/note/images/RPC/rpc.jpg differ diff --git a/note/images/Spring.png b/note/images/Spring.png deleted file mode 100644 index 7bac2c5..0000000 Binary files a/note/images/Spring.png and /dev/null differ diff --git a/note/images/SpringBoot.png b/note/images/SpringBoot.png deleted file mode 100644 index 8f409e2..0000000 Binary files a/note/images/SpringBoot.png and /dev/null differ diff --git a/note/images/spring/spring-01.png b/note/images/spring/spring-01.png new file mode 100644 index 0000000..26d87c9 Binary files /dev/null and b/note/images/spring/spring-01.png differ diff --git a/note/images/spring/spring-02.png b/note/images/spring/spring-02.png new file mode 100644 index 0000000..e309335 Binary files /dev/null and b/note/images/spring/spring-02.png differ diff --git a/note/images/spring/spring-03.jpg b/note/images/spring/spring-03.jpg new file mode 100644 index 0000000..83952d4 Binary files /dev/null and b/note/images/spring/spring-03.jpg differ diff --git a/note/images/spring/spring-04.jpg b/note/images/spring/spring-04.jpg new file mode 100644 index 0000000..654d4ec Binary files /dev/null and b/note/images/spring/spring-04.jpg differ diff --git a/note/images/spring/spring-05.png b/note/images/spring/spring-05.png new file mode 100644 index 0000000..6191186 Binary files /dev/null and b/note/images/spring/spring-05.png differ diff --git a/note/images/spring/spring-06.png b/note/images/spring/spring-06.png new file mode 100644 index 0000000..f6912dc Binary files /dev/null and b/note/images/spring/spring-06.png differ diff --git a/note/images/spring/spring-07.jpg b/note/images/spring/spring-07.jpg new file mode 100644 index 0000000..cf74eaf Binary files /dev/null and b/note/images/spring/spring-07.jpg differ diff --git a/note/images/spring/spring-08.png b/note/images/spring/spring-08.png new file mode 100644 index 0000000..4f12e5f Binary files /dev/null and b/note/images/spring/spring-08.png differ diff --git a/note/images/spring/spring-09.png b/note/images/spring/spring-09.png new file mode 100644 index 0000000..baff012 Binary files /dev/null and b/note/images/spring/spring-09.png differ diff --git a/note/images/spring/spring-10.png b/note/images/spring/spring-10.png new file mode 100644 index 0000000..6191186 Binary files /dev/null and b/note/images/spring/spring-10.png differ diff --git a/note/images/spring/spring-11.png b/note/images/spring/spring-11.png new file mode 100644 index 0000000..f6912dc Binary files /dev/null and b/note/images/spring/spring-11.png differ diff --git a/note/images/spring/spring-12.jpg b/note/images/spring/spring-12.jpg new file mode 100644 index 0000000..ee53f53 Binary files /dev/null and b/note/images/spring/spring-12.jpg differ diff --git a/note/images/spring/spring-13.png b/note/images/spring/spring-13.png new file mode 100644 index 0000000..5f32cb1 Binary files /dev/null and b/note/images/spring/spring-13.png differ diff --git a/note/images/spring/spring-14.jpg b/note/images/spring/spring-14.jpg new file mode 100644 index 0000000..fe86220 Binary files /dev/null and b/note/images/spring/spring-14.jpg differ diff --git a/note/images/spring/spring-15.png b/note/images/spring/spring-15.png new file mode 100644 index 0000000..1309372 Binary files /dev/null and b/note/images/spring/spring-15.png differ diff --git a/rocketmqdemo/README.md b/rocketmqdemo/README.md new file mode 100644 index 0000000..8c82e18 --- /dev/null +++ b/rocketmqdemo/README.md @@ -0,0 +1,9 @@ +## RocketMQDemo 说明 + +- demo01 普通生产者消费者 + +- demo02 普通生产者消费者(异步发送消息) + +- demo03 顺序消费 + +- demo04 延时消息 \ No newline at end of file diff --git a/rocketmqdemo/pom.xml b/rocketmqdemo/pom.xml new file mode 100644 index 0000000..1b307fd --- /dev/null +++ b/rocketmqdemo/pom.xml @@ -0,0 +1,47 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 2.2.1.RELEASE + + + com.bruis + rocketmqdemo + 0.0.1-SNAPSHOT + rocketmqdemo + rocketmqdemo + + 1.8 + 4.9.0 + + + + org.springframework.boot + spring-boot-starter + + + + org.springframework.boot + spring-boot-starter-test + test + + + org.apache.rocketmq + rocketmq-client + ${rocketmq.version} + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + diff --git a/rocketmqdemo/src/main/java/com/bruis/rocketmqdemo/RocketmqdemoApplication.java b/rocketmqdemo/src/main/java/com/bruis/rocketmqdemo/RocketmqdemoApplication.java new file mode 100644 index 0000000..f187f13 --- /dev/null +++ b/rocketmqdemo/src/main/java/com/bruis/rocketmqdemo/RocketmqdemoApplication.java @@ -0,0 +1,13 @@ +package com.bruis.rocketmqdemo; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class RocketmqdemoApplication { + + public static void main(String[] args) { + SpringApplication.run(RocketmqdemoApplication.class, args); + } + +} diff --git a/rocketmqdemo/src/main/java/com/bruis/rocketmqdemo/demo01/Consumer.java b/rocketmqdemo/src/main/java/com/bruis/rocketmqdemo/demo01/Consumer.java new file mode 100644 index 0000000..554c16a --- /dev/null +++ b/rocketmqdemo/src/main/java/com/bruis/rocketmqdemo/demo01/Consumer.java @@ -0,0 +1,67 @@ +package com.bruis.rocketmqdemo.demo01; + +import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer; +import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext; +import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus; +import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently; +import org.apache.rocketmq.common.consumer.ConsumeFromWhere; +import org.apache.rocketmq.common.message.MessageExt; +import org.apache.rocketmq.remoting.common.RemotingHelper; + +import java.util.List; + +/** + * @author lhy + * + * 普通消费者 + * + * @date 2021/7/10 + */ +public class Consumer { + + public static final String DEMO01_CONSUMER_GROUP_NAME = "demo01_consumer_group_name"; + + public static void main(String[] args) throws Exception { + + DefaultMQPushConsumer consumer = new DefaultMQPushConsumer(DEMO01_CONSUMER_GROUP_NAME); + consumer.setNamesrvAddr(Producer.NAMESRV_ADDRESS); + // 从哪开始进行消费 + consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_LAST_OFFSET); + consumer.subscribe(Producer.TOPIC_NAME,"*"); + consumer.registerMessageListener(new MessageListenerConcurrently() { + @Override + public ConsumeConcurrentlyStatus consumeMessage(List msgs, ConsumeConcurrentlyContext context) { + MessageExt messageExt = msgs.get(0); + try { + String topic = messageExt.getTopic(); + String tags = messageExt.getTags(); + String keys = messageExt.getKeys(); + + if ("keyDuplicate".equals(keys)) { + System.err.println("消息消费失败"); + int a = 1 / 0; + } + + String msgBody = new String(messageExt.getBody(), RemotingHelper.DEFAULT_CHARSET); + System.err.println("topic: " + topic + ",tags: " + tags + ", keys: " + keys + ",body: " + msgBody); + } catch (Exception e) { + e.printStackTrace(); + // 消费次数 +// int reconsumeTimes = messageExt.getReconsumeTimes(); +// System.err.println("reconsumeTimes: " + reconsumeTimes); +// // 重试三次 +// if (reconsumeTimes == 3) { +// // 日志记录 +// // 重试补偿成功 +// return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; +// } + return ConsumeConcurrentlyStatus.RECONSUME_LATER; + } + return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; + } + }); + + consumer.start(); + System.err.println("Consumer start ...."); + } +} diff --git a/rocketmqdemo/src/main/java/com/bruis/rocketmqdemo/demo01/Producer.java b/rocketmqdemo/src/main/java/com/bruis/rocketmqdemo/demo01/Producer.java new file mode 100644 index 0000000..b1c533f --- /dev/null +++ b/rocketmqdemo/src/main/java/com/bruis/rocketmqdemo/demo01/Producer.java @@ -0,0 +1,45 @@ +package com.bruis.rocketmqdemo.demo01; + +import org.apache.rocketmq.client.producer.DefaultMQProducer; +import org.apache.rocketmq.client.producer.SendResult; +import org.apache.rocketmq.common.message.Message; + +/** + * @author lhy + * + * 普通生产者 + * + * @date 2021/7/10 + */ +public class Producer { + + public static final String NAMESRV_ADDRESS = "127.0.0.1:9876"; + + // 生产者组 + public static final String DEMO01_PRODUCER_GROUP_NAME = "demo01_producer_group_name"; + + // topic + public static final String TOPIC_NAME = "demo01_topic"; + + public static void main(String[] args) throws Exception { + // 指定生产者组名称 + DefaultMQProducer producer = new DefaultMQProducer(DEMO01_PRODUCER_GROUP_NAME); + producer.setNamesrvAddr(NAMESRV_ADDRESS); + + producer.start(); + + for (int i = 0; i < 5; i++) { + Message message = new Message(TOPIC_NAME,// topic + "TagA",//tag + "key" + i,//keys + ("Hello world RocketMQ Demo01" + i).getBytes()); + + // 向broker发送消息============================> 同步发送 + SendResult sendResult = producer.send(message); + System.out.printf("%s%n", sendResult); + } + + producer.shutdown(); + } + +} diff --git a/rocketmqdemo/src/main/java/com/bruis/rocketmqdemo/demo02/Consumer.java b/rocketmqdemo/src/main/java/com/bruis/rocketmqdemo/demo02/Consumer.java new file mode 100644 index 0000000..e1f74ae --- /dev/null +++ b/rocketmqdemo/src/main/java/com/bruis/rocketmqdemo/demo02/Consumer.java @@ -0,0 +1,67 @@ +package com.bruis.rocketmqdemo.demo02; + +import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer; +import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext; +import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus; +import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently; +import org.apache.rocketmq.common.consumer.ConsumeFromWhere; +import org.apache.rocketmq.common.message.MessageExt; +import org.apache.rocketmq.remoting.common.RemotingHelper; + +import java.util.List; + +/** + * @author lhy + * + * 普通消费者 + * + * @date 2021/7/10 + */ +public class Consumer { + + public static final String DEMO01_CONSUMER_GROUP_NAME = "demo02_consumer_group_name"; + + public static void main(String[] args) throws Exception { + + DefaultMQPushConsumer consumer = new DefaultMQPushConsumer(DEMO01_CONSUMER_GROUP_NAME); + consumer.setNamesrvAddr(Producer.NAMESRV_ADDRESS); + // 从哪开始进行消费 + consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_LAST_OFFSET); + consumer.subscribe(Producer.TOPIC_NAME,"*"); + consumer.registerMessageListener(new MessageListenerConcurrently() { + @Override + public ConsumeConcurrentlyStatus consumeMessage(List msgs, ConsumeConcurrentlyContext context) { + MessageExt messageExt = msgs.get(0); + try { + String topic = messageExt.getTopic(); + String tags = messageExt.getTags(); + String keys = messageExt.getKeys(); + + if ("keyDuplicate".equals(keys)) { + System.err.println("消息消费失败"); + int a = 1 / 0; + } + + String msgBody = new String(messageExt.getBody(), RemotingHelper.DEFAULT_CHARSET); + System.err.println("topic: " + topic + ",tags: " + tags + ", keys: " + keys + ",body: " + msgBody); + } catch (Exception e) { + e.printStackTrace(); + // 消费次数 +// int reconsumeTimes = messageExt.getReconsumeTimes(); +// System.err.println("reconsumeTimes: " + reconsumeTimes); +// // 重试三次 +// if (reconsumeTimes == 3) { +// // 日志记录 +// // 重试补偿成功 +// return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; +// } + return ConsumeConcurrentlyStatus.RECONSUME_LATER; + } + return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; + } + }); + + consumer.start(); + System.err.println("Consumer start ...."); + } +} diff --git a/rocketmqdemo/src/main/java/com/bruis/rocketmqdemo/demo02/Producer.java b/rocketmqdemo/src/main/java/com/bruis/rocketmqdemo/demo02/Producer.java new file mode 100644 index 0000000..1163545 --- /dev/null +++ b/rocketmqdemo/src/main/java/com/bruis/rocketmqdemo/demo02/Producer.java @@ -0,0 +1,54 @@ +package com.bruis.rocketmqdemo.demo02; + +import org.apache.rocketmq.client.producer.DefaultMQProducer; +import org.apache.rocketmq.client.producer.SendCallback; +import org.apache.rocketmq.client.producer.SendResult; +import org.apache.rocketmq.common.message.Message; + +/** + * @author lhy + * + * 普通生产者(异步方式发送) + * + * @date 2021/7/10 + */ +public class Producer { + + public static final String NAMESRV_ADDRESS = "127.0.0.1:9876"; + + // 生产者组 + public static final String DEMO01_PRODUCER_GROUP_NAME = "demo02_producer_group_name"; + + // topic + public static final String TOPIC_NAME = "demo02_topic"; + + public static void main(String[] args) throws Exception { + // 指定生产者组名称 + DefaultMQProducer producer = new DefaultMQProducer(DEMO01_PRODUCER_GROUP_NAME); + producer.setNamesrvAddr(NAMESRV_ADDRESS); + + producer.start(); + + for (int i = 0; i < 5; i++) { + Message message = new Message(TOPIC_NAME,// topic + "TagA",//tag + "key" + i,//keys + ("Hello world RocketMQ Demo01" + i).getBytes()); + + // 向broker发送消息 + producer.send(message, new SendCallback() { + @Override + public void onSuccess(SendResult sendResult) { + System.out.printf("msgId: " + sendResult.getMsgId() + ", status: " + sendResult.getSendStatus()); + } + + @Override + public void onException(Throwable e) { + e.printStackTrace(); + System.err.println("==============发送失败"); + } + }); + } + } + +} diff --git a/rocketmqdemo/src/main/java/com/bruis/rocketmqdemo/demo03/Consumer.java b/rocketmqdemo/src/main/java/com/bruis/rocketmqdemo/demo03/Consumer.java new file mode 100644 index 0000000..951e693 --- /dev/null +++ b/rocketmqdemo/src/main/java/com/bruis/rocketmqdemo/demo03/Consumer.java @@ -0,0 +1,61 @@ +package com.bruis.rocketmqdemo.demo03; + +import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer; +import org.apache.rocketmq.client.consumer.listener.ConsumeOrderlyContext; +import org.apache.rocketmq.client.consumer.listener.ConsumeOrderlyStatus; +import org.apache.rocketmq.client.consumer.listener.MessageListenerOrderly; +import org.apache.rocketmq.common.consumer.ConsumeFromWhere; +import org.apache.rocketmq.common.message.MessageExt; + +import java.util.List; +import java.util.Random; +import java.util.concurrent.TimeUnit; + +/** + * 顺序消息消费,带事务方式(应用可控制Offset什么时候提交) + * + * @author lhy + * @date 2021/7/23 + */ +public class Consumer { + + public static void main(String[] args) throws Exception { + DefaultMQPushConsumer consumer = new DefaultMQPushConsumer(Producer.GROUP_NAME); + consumer.setNamesrvAddr(Producer.NAMESRV_ADDRESS); + + /** + * 设置Consumer第一次启动是从队列头部开始消费还是队列尾部开始消费 + * 如果非第一次启动,那么按照上次消费的位置继续消费 + */ + consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_LAST_OFFSET); + consumer.subscribe(Producer.TOPIC_NAME, "TagA || TagC || TagD"); + + /** + * 顺序消费的消息类??MessageListenerOrderly + */ + consumer.registerMessageListener(new MessageListenerOrderly() { + + Random random = new Random(); + + @Override + public ConsumeOrderlyStatus consumeMessage(List msgs, ConsumeOrderlyContext context) { + context.setAutoCommit(true); + for (MessageExt msg : msgs) { + // 可以看到每个queue有唯一的consume线程来消费, 订单对每个queue(分区)有序 + System.out.println("consumeThread=" + Thread.currentThread().getName() + "queueId=" + msg.getQueueId() + ", content:" + new String(msg.getBody())); + } + + try { + //模拟业务逻辑处理中... + TimeUnit.SECONDS.sleep(random.nextInt(10)); + } catch (Exception e) { + e.printStackTrace(); + } + return ConsumeOrderlyStatus.SUCCESS; + } + }); + + consumer.start(); + System.out.println("Consumer Started."); + } +} diff --git a/rocketmqdemo/src/main/java/com/bruis/rocketmqdemo/demo03/Producer.java b/rocketmqdemo/src/main/java/com/bruis/rocketmqdemo/demo03/Producer.java new file mode 100644 index 0000000..d2af5df --- /dev/null +++ b/rocketmqdemo/src/main/java/com/bruis/rocketmqdemo/demo03/Producer.java @@ -0,0 +1,162 @@ +package com.bruis.rocketmqdemo.demo03; + +import org.apache.rocketmq.client.producer.DefaultMQProducer; +import org.apache.rocketmq.client.producer.MessageQueueSelector; +import org.apache.rocketmq.client.producer.SendResult; +import org.apache.rocketmq.common.message.Message; +import org.apache.rocketmq.common.message.MessageQueue; + +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +/** + * + * 顺序消息样例 + * + * @author lhy + * @date 2021/7/23 + */ +public class Producer { + + public static final String NAMESRV_ADDRESS = "127.0.0.1:9876"; + + public static final String GROUP_NAME = "inorder_group_name"; + + public static final String TOPIC_NAME = "inorder_topic"; + + public static void main(String[] args) throws Exception { + DefaultMQProducer producer = new DefaultMQProducer(GROUP_NAME); + producer.setNamesrvAddr(NAMESRV_ADDRESS); + + producer.start(); + + String[] tags = {"TagA", "TagC", "TagD"}; + + // 订单列表 + List orderList = new Producer().buildOrders(); + Date date = new Date(); + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + String dateStr = sdf.format(date); + for (int i = 0; i < 10; i++) { + // 时间前缀 + String body = dateStr + " Hello RocketMQ " + orderList.get(i); + Message msg = new Message(TOPIC_NAME, tags[i % tags.length], "KEY" + i, body.getBytes()); + +// SendResult sendResult = producer.send(msg, (mqs, msg1, arg) -> { +// Long id = (Long) arg; //根据订单id选择发送queue +// long index = id % mqs.size(); +// return mqs.get((int) index); +// }, orderList.get(i).getOrderId());//订单id + + SendResult sendResult = producer.send(msg, new MessageQueueSelector() { + @Override + public MessageQueue select(List mqs, Message msg, Object arg) { + Long id = (Long) arg; + long index = id % mqs.size(); + return mqs.get((int) index); + } + }, orderList.get(i).getOrderId()); + + System.out.println(String.format("SendResult status:%s, queueId:%d, body:%s", + sendResult.getSendStatus(), + sendResult.getMessageQueue().getQueueId(), + body)); + } + + producer.shutdown(); + } + + /** + * 订单的步骤 + */ + private class OrderStep { + private long orderId; + private String desc; + + public long getOrderId() { + return orderId; + } + + public void setOrderId(long orderId) { + this.orderId = orderId; + } + + public String getDesc() { + return desc; + } + + public void setDesc(String desc) { + this.desc = desc; + } + + @Override + public String toString() { + return "OrderStep{" + + "orderId=" + orderId + + ", desc='" + desc + '\'' + + '}'; + } + } + + /** + * 生成模拟订单数据 + */ + private List buildOrders() { + List orderList = new ArrayList(); + + OrderStep orderDemo = new OrderStep(); + orderDemo.setOrderId(15103111039L); + orderDemo.setDesc("创建"); + orderList.add(orderDemo); + + orderDemo = new OrderStep(); + orderDemo.setOrderId(15103111065L); + orderDemo.setDesc("创建"); + orderList.add(orderDemo); + + orderDemo = new OrderStep(); + orderDemo.setOrderId(15103111039L); + orderDemo.setDesc("付款"); + orderList.add(orderDemo); + + orderDemo = new OrderStep(); + orderDemo.setOrderId(15103117235L); + orderDemo.setDesc("创建"); + orderList.add(orderDemo); + + orderDemo = new OrderStep(); + orderDemo.setOrderId(15103111065L); + orderDemo.setDesc("付款"); + orderList.add(orderDemo); + + orderDemo = new OrderStep(); + orderDemo.setOrderId(15103117235L); + orderDemo.setDesc("付款"); + orderList.add(orderDemo); + + orderDemo = new OrderStep(); + orderDemo.setOrderId(15103111065L); + orderDemo.setDesc("完成"); + orderList.add(orderDemo); + + orderDemo = new OrderStep(); + orderDemo.setOrderId(15103111039L); + orderDemo.setDesc("推送"); + orderList.add(orderDemo); + + orderDemo = new OrderStep(); + orderDemo.setOrderId(15103117235L); + orderDemo.setDesc("完成"); + orderList.add(orderDemo); + + orderDemo = new OrderStep(); + orderDemo.setOrderId(15103111039L); + orderDemo.setDesc("完成"); + orderList.add(orderDemo); + + return orderList; + } + +} diff --git a/rocketmqdemo/src/main/java/com/bruis/rocketmqdemo/demo04/ScheduledMessageConsumer.java b/rocketmqdemo/src/main/java/com/bruis/rocketmqdemo/demo04/ScheduledMessageConsumer.java new file mode 100644 index 0000000..97b85be --- /dev/null +++ b/rocketmqdemo/src/main/java/com/bruis/rocketmqdemo/demo04/ScheduledMessageConsumer.java @@ -0,0 +1,39 @@ +package com.bruis.rocketmqdemo.demo04; + +import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer; +import org.apache.rocketmq.client.consumer.listener.*; +import org.apache.rocketmq.common.message.MessageExt; +import java.util.List; + +/** + * + * 延时消息消费者 + * + * @author lhy + * @date 2021/7/24 + */ +public class ScheduledMessageConsumer { + + public static void main(String[] args) throws Exception { + + DefaultMQPushConsumer consumer = new DefaultMQPushConsumer(ScheduledMessageProducer.GROUP_NAME); + // 订阅指定的topic + consumer.subscribe(ScheduledMessageProducer.TOPIC_NAME, "*"); + consumer.setNamesrvAddr(ScheduledMessageProducer.NAMESRV_ADDRESS); + // 注册消息监听者 + consumer.registerMessageListener(new MessageListenerConcurrently() { + + // 消费消息 + @Override + public ConsumeConcurrentlyStatus consumeMessage(List messages, ConsumeConcurrentlyContext context) { + for (MessageExt message : messages) { + // Print approximate delay time period + System.out.println("Receive message[msgId=" + message.getMsgId() + "] " + (System.currentTimeMillis() - message.getBornTimestamp()) + "ms later"); + } + return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; + } + }); + consumer.start(); + } + +} diff --git a/rocketmqdemo/src/main/java/com/bruis/rocketmqdemo/demo04/ScheduledMessageProducer.java b/rocketmqdemo/src/main/java/com/bruis/rocketmqdemo/demo04/ScheduledMessageProducer.java new file mode 100644 index 0000000..356f370 --- /dev/null +++ b/rocketmqdemo/src/main/java/com/bruis/rocketmqdemo/demo04/ScheduledMessageProducer.java @@ -0,0 +1,40 @@ +package com.bruis.rocketmqdemo.demo04; + +import org.apache.rocketmq.client.producer.DefaultMQProducer; +import org.apache.rocketmq.common.message.Message; + +/** + * + * 延时消息生产 + * + * @author lhy + * @date 2021/7/24 + */ +public class ScheduledMessageProducer { + + public static final String GROUP_NAME = "scheduled_group"; + + public static final String TOPIC_NAME = "scheduled_test_topic"; + + public static final String NAMESRV_ADDRESS = "127.0.0.1:9876"; + + public static void main(String[] args) throws Exception { + + DefaultMQProducer producer = new DefaultMQProducer(GROUP_NAME); + producer.setNamesrvAddr(NAMESRV_ADDRESS); + producer.start(); + + int totalMessagesToSend = 100; + + for (int i = 0; i < totalMessagesToSend; i++) { + Message message = new Message(TOPIC_NAME, ("Hello Scheduled Message " + i).getBytes()); + // 设置延时等级3,这个消息将在10s之后发送(现在只支持固定的几个时间,详看delayTimeLevel) + message.setDelayTimeLevel(3); + // 发送消息 + producer.send(message); + } + + producer.shutdown(); + } + +} diff --git a/rocketmqdemo/src/main/resources/application.properties b/rocketmqdemo/src/main/resources/application.properties new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/rocketmqdemo/src/main/resources/application.properties @@ -0,0 +1 @@ + diff --git a/rocketmqdemo/src/test/java/com/bruis/rocketmqdemo/RocketmqdemoApplicationTests.java b/rocketmqdemo/src/test/java/com/bruis/rocketmqdemo/RocketmqdemoApplicationTests.java new file mode 100644 index 0000000..f1c73b4 --- /dev/null +++ b/rocketmqdemo/src/test/java/com/bruis/rocketmqdemo/RocketmqdemoApplicationTests.java @@ -0,0 +1,13 @@ +package com.bruis.rocketmqdemo; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class RocketmqdemoApplicationTests { + + @Test + void contextLoads() { + } + +}