├── .gitignore ├── README.md ├── cm4j-hotswap ├── eval-output │ └── JavaEvalDemo.java ├── pom.xml └── src │ ├── main │ ├── java │ │ └── com │ │ │ └── cm4j │ │ │ ├── config │ │ │ └── ErrorCode.java │ │ │ ├── demo │ │ │ ├── UtilRegistry.java │ │ │ └── util │ │ │ │ ├── DemoUtil.java │ │ │ │ └── IUtil.java │ │ │ ├── eval │ │ │ ├── JavaEvalUtil.java │ │ │ └── compiler │ │ │ │ ├── CustomJavaFileObject.java │ │ │ │ ├── DynamicClassLoader.java │ │ │ │ ├── DynamicCompiler.java │ │ │ │ ├── DynamicCompilerException.java │ │ │ │ ├── DynamicJavaFileManager.java │ │ │ │ ├── MemoryByteCode.java │ │ │ │ ├── PackageInternalsFinder.java │ │ │ │ └── StringSource.java │ │ │ ├── grpc │ │ │ ├── client │ │ │ │ └── GrpcClient.java │ │ │ ├── config │ │ │ │ └── GrpcConfig.java │ │ │ ├── proto │ │ │ │ ├── MS_METHOD_GRPC.java │ │ │ │ └── MsMethodServiceGrpc.java │ │ │ ├── server │ │ │ │ └── GrpcServer.java │ │ │ └── service │ │ │ │ └── MsMethodServiceImpl.java │ │ │ ├── hotswap │ │ │ ├── agent │ │ │ │ └── JavaAgent.java │ │ │ └── recompile │ │ │ │ ├── RecompileClassLoader.java │ │ │ │ └── RecompileHotSwap.java │ │ │ ├── invoke │ │ │ ├── IRemotingClass.java │ │ │ ├── RemotingMethod.java │ │ │ ├── impl │ │ │ │ └── TestRpc.java │ │ │ ├── invoker │ │ │ │ ├── IRemotingInvoker.java │ │ │ │ ├── RemotingInvokerScanner.java │ │ │ │ └── RemotingParamVO.java │ │ │ └── proxy │ │ │ │ └── LocalProxyGenerator.java │ │ │ ├── lock │ │ │ ├── InternalLock.java │ │ │ └── Locker.java │ │ │ ├── registry │ │ │ ├── AbstractClassRegistry.java │ │ │ ├── AbstractRegistry.java │ │ │ ├── IHotswapCallback.java │ │ │ ├── RegistryManager.java │ │ │ ├── registered │ │ │ │ └── IRegistered.java │ │ │ └── registry │ │ │ │ └── InvokerRegistry.java │ │ │ ├── singleton │ │ │ ├── FutureResult.java │ │ │ ├── FutureSupport.java │ │ │ ├── FutureWrapper.java │ │ │ ├── SingletonEnum.java │ │ │ ├── SingletonModule.java │ │ │ ├── SingletonTask.java │ │ │ ├── SingletonTaskQueue.java │ │ │ └── impl │ │ │ │ └── NormalSingletonModule.java │ │ │ ├── thread │ │ │ ├── DefaultThreadFactory.java │ │ │ ├── ThreadPoolConfig.java │ │ │ ├── ThreadPoolFactory.java │ │ │ ├── ThreadPoolName.java │ │ │ ├── ThreadPoolRejectedPolicy.java │ │ │ └── ThreadPoolService.java │ │ │ └── util │ │ │ ├── ClassUtil.java │ │ │ ├── PackageUtil.java │ │ │ ├── ProtoStuffUtil.java │ │ │ ├── RemotingInvokerUtil.java │ │ │ └── ThreadUtil.java │ └── resources │ │ ├── config │ │ └── resource │ │ │ └── proto │ │ │ └── grpc │ │ │ └── MsMethod.proto │ │ ├── next.md │ │ ├── notes │ │ ├── cleanLastUpdated.bat │ │ ├── flame-graph.md │ │ ├── flame-graph1.png │ │ ├── maven-mechanism.md │ │ ├── maven-mechanism1.png │ │ ├── maven-mechanism2.png │ │ ├── maven-mechanism3.png │ │ ├── maven-mechanism4.png │ │ ├── maven-mechanism5.png │ │ ├── maven-mechanism6.png │ │ ├── tomcat_shutdown.md │ │ └── tomcat_shutdown1.png │ │ ├── protoc │ │ ├── grpc.bat │ │ ├── protoc-gen-grpc-java-1.34.1-windows-x86_64.exe │ │ └── protoc.exe │ │ └── solutions │ │ ├── arthas-classloader.md │ │ ├── arthas-classloader1.png │ │ ├── arthas-classloader2.png │ │ ├── arthas-command-category.md │ │ ├── arthas-command-category1.png │ │ ├── arthas-isolation.md │ │ ├── arthas-isolation1.png │ │ ├── arthas-isolation2.png │ │ ├── deadlock_solution.md │ │ ├── hotswap-agent.md │ │ ├── hotswap-compile-1.png │ │ ├── hotswap-compile-2.png │ │ ├── hotswap-compile.md │ │ ├── java_eval.md │ │ ├── remoting-invoke.md │ │ ├── remoting-invoke2.md │ │ ├── singleton-module.drawio │ │ ├── singleton-module.md │ │ ├── singleton-module1.jpg │ │ └── singleton-module2.png │ └── test │ └── java │ └── com │ └── cm4j │ ├── demo │ ├── Parent.java │ └── SubClass.java │ ├── eval │ ├── JavaEvalDemo.java │ └── JavaEvalUtilTest.java │ ├── hotswap │ └── JavaAgentTest.java │ ├── invoke │ └── impl │ │ └── TestRpcTest.java │ ├── lock │ └── LockerTest.java │ ├── registry │ └── RegistryManagerTest.java │ ├── singleton │ └── impl │ │ └── NormalSingletonModuleTest.java │ ├── thread │ └── ThreadPoolServiceTest.java │ └── util │ └── PackageUtilTest.java └── pom.xml /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled class file 2 | *.class 3 | 4 | # Log file 5 | *.log 6 | 7 | # BlueJ files 8 | *.ctxt 9 | 10 | # Mobile Tools for Java (J2ME) 11 | .mtj.tmp/ 12 | 13 | # Package Files # 14 | *.jar 15 | *.war 16 | *.nar 17 | *.ear 18 | *.zip 19 | *.tar.gz 20 | *.rar 21 | 22 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 23 | hs_err_pid* 24 | /.idea/ 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > 示例代码github:[https://github.com/cm4j/cm4j-all](https://github.com/cm4j/cm4j-all)
2 | 注意:本项目的agent热更方式需依赖于 cm4j-javaagent 的jar包
3 | 因此请先下载 [cm4j-javaagent](https://github.com/cm4j/cm4j-javaagent) 项目并执行 mvn clean install 命令,把cm4j-javaagent打个jar包安装到本地maven仓库中
4 | 5 | 从事游戏行业多年,一直使用Java做开发,不可避免的就经历了许多,其中也踩过不少坑。 6 | 7 | - 最早的游戏是不支持热更的,导致出了BUG就必须停服; 8 | - 后续项目引入热更,但也不是特别完美,再往后热更升级,引入第二版:动态加载子类热更; 9 | - 受第二版热更方式启发,后来又加入了线上动态代码执行,主要用于规整数据,处理线上BUG; 10 | - Arthas的横空出世,给线上解决方案打开了一个新思路。它的设计也非常巧妙,许多开源框架在它的基础上进行扩展,比如 [Bistoury](https://github.com/qunarcorp/bistoury) 的在线Debug等; 11 | - 因为游戏业务特殊性,会产生许多跨进程接口调用,因此我们就期望一种本地和远程代码是同一种写法的API调用方式,于是就衍化出跨服本服调用的一致化。 12 | 13 | 由此可以看出,我们的技术演进与迭代也是逐步过来的,这中间也参考了许多开源的实现,其中 [Arthas](https://github.com/alibaba/arthas) 的思路不可或缺,这里要感谢Arthas技术团队。 14 | 15 | 这中间过程走了不少弯路,因此我这里整理了一个系列文章,主要讲解下这么多年遇到的问题、使用到的线上解决方案,以及其背后的原理。 16 | 主要涉及到的技术点包括:Agent、JavaCompiler代码编译、字节码生成、ClassLoader原理、框架的代码隔离与互调思路等等 17 | 18 | 系列介绍目录:[Java线上解决方案系列目录](//yeas.fun/archives/solution-contents) 19 | 20 | - [JAVA热更新1:Agent方式热更](//yeas.fun/archives/hotswap-agent) 21 | - [JAVA热更新2:动态加载子类热更](//yeas.fun/archives/java-hotswap-compile) 22 | - [JAVA线上执行代码(动态代码执行)](//yeas.fun/archives/java-eval) 23 | - 在线调试Debug 24 | - [像本服一样调用远程代码(跨进程远程方法直调)](//yeas.fun/archives/remoting-invoke) 25 | - [像本服一样调用远程代码(优化版)](//yeas.fun/archives/remoting-invoke2) 26 | - [Arthas原理:理解ClassLoader](//yeas.fun/archives/arthas-classloader) 27 | - [Arthas原理:arthas如何做到与应用代码隔离?](//yeas.fun/archives/arthas-isolation) 28 | - [Arthas原理:Arthas的命令分类及原理](//yeas.fun/archives/arthas-command-category) 29 | 30 | 多线程系列目录: 31 | - [替换synchronized锁解决死锁](https://yeas.fun/archives/deadlock-solution) 32 | - [单线程执行解决复杂的并发场景](https://yeas.fun/archives/singleton-module) -------------------------------------------------------------------------------- /cm4j-hotswap/eval-output/JavaEvalDemo.java: -------------------------------------------------------------------------------- 1 | package com.cm4j.eval; 2 | 3 | /** 4 | * JavaEval模板 5 | */ 6 | public class JavaEvalDemo { 7 | 8 | public static Object calc() { 9 | return 1 + 2; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /cm4j-hotswap/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | cm4j-prj 7 | com.cm4j 8 | 1.0-SNAPSHOT 9 | 10 | 11 | 4.0.0 12 | cm4j-hotswap 13 | 14 | 15 | 16 | com.cm4j 17 | cm4j-javaagent 18 | 1.0-SNAPSHOT 19 | jar 20 | 21 | 22 | 23 | org.apache.commons 24 | commons-lang3 25 | 26 | 27 | commons-net 28 | commons-net 29 | 30 | 31 | commons-io 32 | commons-io 33 | 34 | 35 | org.apache.commons 36 | commons-math3 37 | 38 | 39 | 40 | 41 | 42 | 43 | ch.qos.logback 44 | logback-classic 45 | 46 | 47 | ch.qos.logback 48 | logback-core 49 | 50 | 51 | org.logback-extensions 52 | logback-ext-spring 53 | 54 | 55 | 56 | jdk.tools 57 | jdk.tools 58 | 59 | 60 | 61 | 62 | com.google.guava 63 | guava 64 | 65 | 66 | com.google.protobuf 67 | protobuf-java 68 | 69 | 70 | com.google.protobuf 71 | protobuf-java-util 72 | 73 | 78 | 79 | io.grpc 80 | grpc-netty-shaded 81 | 82 | 83 | io.grpc 84 | grpc-protobuf 85 | 86 | 87 | io.grpc 88 | grpc-stub 89 | 90 | 91 | com.github.ben-manes.caffeine 92 | caffeine 93 | 94 | 95 | 96 | org.javassist 97 | javassist 98 | 99 | 100 | 101 | com.alibaba 102 | fastjson 103 | 104 | 105 | 106 | org.ow2.asm 107 | asm 108 | 109 | 110 | 111 | com.esotericsoftware 112 | reflectasm 113 | 114 | 115 | io.protostuff 116 | protostuff-core 117 | 118 | 119 | io.protostuff 120 | protostuff-runtime 121 | 122 | 123 | 124 | 125 | cglib 126 | cglib-nodep 127 | 128 | 129 | 130 | 131 | junit 132 | junit 133 | test 134 | 135 | 136 | 137 | 138 | -------------------------------------------------------------------------------- /cm4j-hotswap/src/main/java/com/cm4j/config/ErrorCode.java: -------------------------------------------------------------------------------- 1 | package com.cm4j.config; 2 | 3 | import java.util.HashMap; 4 | import java.util.Map; 5 | 6 | /** 7 | * 错误码 8 | */ 9 | public enum ErrorCode { 10 | 11 | /** 12 | * 0-成功 13 | */ 14 | SUCCESS("0", "成功", "3"), 15 | 16 | ; 17 | 18 | private final String code; 19 | private final String mess; 20 | private final short shortCode; 21 | private final String showType; 22 | 23 | ErrorCode(String code, String mess) { 24 | this(code, mess, "0"); 25 | } 26 | 27 | ErrorCode(String code, String mess, String showType) { 28 | this.code = code; 29 | this.mess = mess; 30 | this.shortCode = Short.parseShort(code); 31 | this.showType = showType; 32 | } 33 | 34 | private static final Map MAP = new HashMap<>(); 35 | 36 | static { 37 | for (ErrorCode code : values()) { 38 | MAP.put(code.getCode(), code); 39 | } 40 | } 41 | 42 | public static ErrorCode getErrorCode(String errorCode) { 43 | return MAP.get(errorCode); 44 | } 45 | 46 | public String getMess() { 47 | return mess; 48 | } 49 | 50 | public String getCode() { 51 | return code; 52 | } 53 | 54 | public String getShowType() { 55 | return showType; 56 | } 57 | 58 | public short getShortCode() { 59 | return shortCode; 60 | } 61 | 62 | 63 | } 64 | -------------------------------------------------------------------------------- /cm4j-hotswap/src/main/java/com/cm4j/demo/UtilRegistry.java: -------------------------------------------------------------------------------- 1 | package com.cm4j.demo; 2 | 3 | import com.cm4j.demo.util.IUtil; 4 | import com.cm4j.registry.AbstractClassRegistry; 5 | 6 | /** 7 | * util类注册类 8 | * 注意:如果使用spring,这个类可以配置@Component进行扫描注册 9 | */ 10 | public class UtilRegistry extends AbstractClassRegistry, IUtil> { 11 | 12 | /** 13 | * 扫描包进行注册 14 | */ 15 | public UtilRegistry() { 16 | super(IUtil.class.getPackage().getName()); 17 | instance = this; 18 | } 19 | 20 | private static UtilRegistry instance; 21 | 22 | public static UtilRegistry getInstance() { 23 | return instance; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /cm4j-hotswap/src/main/java/com/cm4j/demo/util/DemoUtil.java: -------------------------------------------------------------------------------- 1 | package com.cm4j.demo.util; 2 | 3 | import com.cm4j.demo.UtilRegistry; 4 | 5 | /** 6 | * @author yeas.fun 7 | * @since 2021/11/4 8 | */ 9 | public class DemoUtil implements IUtil { 10 | 11 | public void hello() { 12 | System.out.println("hello world !"); 13 | } 14 | 15 | public static DemoUtil getInstance() { 16 | return (DemoUtil) UtilRegistry.getInstance().get(DemoUtil.class); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /cm4j-hotswap/src/main/java/com/cm4j/demo/util/IUtil.java: -------------------------------------------------------------------------------- 1 | package com.cm4j.demo.util; 2 | 3 | import com.cm4j.registry.registered.IRegistered; 4 | 5 | /** 6 | * @author yeas.fun 7 | * @since 2021/1/8 8 | */ 9 | public interface IUtil extends IRegistered { 10 | 11 | } 12 | -------------------------------------------------------------------------------- /cm4j-hotswap/src/main/java/com/cm4j/eval/JavaEvalUtil.java: -------------------------------------------------------------------------------- 1 | package com.cm4j.eval; 2 | 3 | import java.io.File; 4 | import java.io.IOException; 5 | import java.lang.reflect.Method; 6 | import java.lang.reflect.Modifier; 7 | import java.util.Map; 8 | 9 | import org.apache.commons.lang3.StringUtils; 10 | import org.slf4j.Logger; 11 | import org.slf4j.LoggerFactory; 12 | 13 | import com.alibaba.fastjson.JSON; 14 | import com.cm4j.eval.compiler.DynamicCompiler; 15 | import com.google.common.base.Charsets; 16 | import com.google.common.io.Files; 17 | 18 | /** 19 | * Java动态代码执行
20 | *

21 | * 使用方法:
22 | * 直接写一个类,里面必须要有一个 public static 方法,就可以调用该方法 23 | */ 24 | public class JavaEvalUtil { 25 | 26 | private static final Logger LOG = LoggerFactory.getLogger(JavaEvalUtil.class); 27 | 28 | private static void dump(String javaSource) throws IOException { 29 | String className = getClassName(javaSource); 30 | 31 | String basePath = new File("").getAbsolutePath(); 32 | String finalPath = StringUtils.join(new String[]{basePath, "eval-output", className + ".java"}, File.separator); 33 | 34 | // 创建上级目录 35 | File file = new File(finalPath); 36 | Files.createParentDirs(file); 37 | 38 | LOG.error("java eval output:{}", file.getAbsolutePath()); 39 | LOG.error("\n{}", javaSource); 40 | Files.write(javaSource.getBytes(Charsets.UTF_8), file); 41 | } 42 | 43 | /** 44 | * 编译并生成class 45 | * 46 | * @param sourceCode 源码内容 47 | * @return 48 | */ 49 | private static Class compile(String sourceCode) { 50 | String className = getClassName(sourceCode); 51 | 52 | DynamicCompiler dynamicCompiler = new DynamicCompiler(JavaEvalUtil.class.getClassLoader()); 53 | dynamicCompiler.addSource(className, sourceCode); 54 | 55 | Map> build = dynamicCompiler.build(); 56 | if (build.isEmpty()) { 57 | throw new RuntimeException("java eval compile error"); 58 | } 59 | return build.values().iterator().next(); 60 | } 61 | 62 | private static String getClassName(String sourceCode) { 63 | return StringUtils.trim(StringUtils.substringBetween(sourceCode, "public class ", "{")); 64 | } 65 | 66 | /** 67 | * 反射调用 68 | * 69 | * @param methtClass 70 | * @return 71 | * @throws Exception 72 | */ 73 | private static Object call(Class methtClass) throws Exception { 74 | Method[] declaredMethods = methtClass.getDeclaredMethods(); 75 | 76 | for (Method declaredMethod : declaredMethods) { 77 | if (Modifier.isPublic(declaredMethod.getModifiers()) && Modifier.isStatic(declaredMethod.getModifiers())) { 78 | Object result = declaredMethod.invoke(null); 79 | 80 | LOG.error("JavaEval return: {}", JSON.toJSON(result)); 81 | return result; 82 | } 83 | } 84 | 85 | throw new RuntimeException("NO method is [public static], cannot eval"); 86 | } 87 | 88 | /** 89 | * 对外API,执行文件得结果 90 | * 91 | * @param content 92 | * @return 93 | * @throws Exception 94 | */ 95 | public static Object eval(String content) throws Exception { 96 | // 类dump 97 | dump(content); 98 | 99 | // 编译生成内存二进制,并加载为class 100 | Class compile = compile(content); 101 | 102 | // 反射调用class 103 | return call(compile); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /cm4j-hotswap/src/main/java/com/cm4j/eval/compiler/CustomJavaFileObject.java: -------------------------------------------------------------------------------- 1 | package com.cm4j.eval.compiler; 2 | 3 | /*- 4 | * #%L 5 | * compiler 6 | * %% 7 | * Copyright (C) 2017 - 2018 SkaLogs 8 | * %% 9 | * Licensed under the Apache License, Version 2.0 (the "License"); 10 | * you may not use this file except in compliance with the License. 11 | * You may obtain a copy of the License at 12 | * 13 | * http://www.apache.org/licenses/LICENSE-2.0 14 | * 15 | * Unless required by applicable law or agreed to in writing, software 16 | * distributed under the License is distributed on an "AS IS" BASIS, 17 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | * See the License for the specific language governing permissions and 19 | * limitations under the License. 20 | * #L% 21 | */ 22 | 23 | import java.io.*; 24 | import java.net.URI; 25 | 26 | import javax.lang.model.element.Modifier; 27 | import javax.lang.model.element.NestingKind; 28 | import javax.tools.JavaFileObject; 29 | 30 | public class CustomJavaFileObject implements JavaFileObject { 31 | private final String binaryName; 32 | private final URI uri; 33 | private final String name; 34 | 35 | public CustomJavaFileObject(String binaryName, URI uri) { 36 | this.uri = uri; 37 | this.binaryName = binaryName; 38 | name = uri.getPath() == null ? uri.getSchemeSpecificPart() : uri.getPath(); // for FS based URI the path is not null, for JAR URI the scheme specific part is not null 39 | } 40 | 41 | public URI toUri() { 42 | return uri; 43 | } 44 | 45 | public InputStream openInputStream() throws IOException { 46 | return uri.toURL().openStream(); 47 | } 48 | 49 | public OutputStream openOutputStream() { 50 | throw new UnsupportedOperationException(); 51 | } 52 | 53 | public String getName() { 54 | return name; 55 | } 56 | 57 | public Reader openReader(boolean ignoreEncodingErrors) { 58 | throw new UnsupportedOperationException(); 59 | } 60 | 61 | public CharSequence getCharContent(boolean ignoreEncodingErrors) { 62 | throw new UnsupportedOperationException(); 63 | } 64 | 65 | public Writer openWriter() throws IOException { 66 | throw new UnsupportedOperationException(); 67 | } 68 | 69 | public long getLastModified() { 70 | return 0; 71 | } 72 | 73 | public boolean delete() { 74 | throw new UnsupportedOperationException(); 75 | } 76 | 77 | public Kind getKind() { 78 | return Kind.CLASS; 79 | } 80 | 81 | public boolean isNameCompatible(String simpleName, Kind kind) { 82 | String baseName = simpleName + kind.extension; 83 | return kind.equals(getKind()) 84 | && (baseName.equals(getName()) 85 | || getName().endsWith("/" + baseName)); 86 | } 87 | 88 | public NestingKind getNestingKind() { 89 | throw new UnsupportedOperationException(); 90 | } 91 | 92 | public Modifier getAccessLevel() { 93 | throw new UnsupportedOperationException(); 94 | } 95 | 96 | public String binaryName() { 97 | return binaryName; 98 | } 99 | 100 | 101 | public String toString() { 102 | return this.getClass().getName() + "[" + this.toUri() + "]"; 103 | } 104 | } 105 | 106 | -------------------------------------------------------------------------------- /cm4j-hotswap/src/main/java/com/cm4j/eval/compiler/DynamicClassLoader.java: -------------------------------------------------------------------------------- 1 | package com.cm4j.eval.compiler; 2 | 3 | /*- 4 | * #%L 5 | * compiler 6 | * %% 7 | * Copyright (C) 2017 - 2018 SkaLogs 8 | * %% 9 | * Licensed under the Apache License, Version 2.0 (the "License"); 10 | * you may not use this file except in compliance with the License. 11 | * You may obtain a copy of the License at 12 | * 13 | * http://www.apache.org/licenses/LICENSE-2.0 14 | * 15 | * Unless required by applicable law or agreed to in writing, software 16 | * distributed under the License is distributed on an "AS IS" BASIS, 17 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | * See the License for the specific language governing permissions and 19 | * limitations under the License. 20 | * #L% 21 | */ 22 | 23 | import java.util.HashMap; 24 | import java.util.Map; 25 | import java.util.Map.Entry; 26 | 27 | public class DynamicClassLoader extends ClassLoader { 28 | private final Map byteCodes = new HashMap(); 29 | 30 | public DynamicClassLoader(ClassLoader classLoader) { 31 | super(classLoader); 32 | } 33 | 34 | public void registerCompiledSource(MemoryByteCode byteCode) { 35 | byteCodes.put(byteCode.getClassName(), byteCode); 36 | } 37 | 38 | @Override 39 | protected Class findClass(String name) throws ClassNotFoundException { 40 | MemoryByteCode byteCode = byteCodes.get(name); 41 | if (byteCode == null) { 42 | return super.findClass(name); 43 | } 44 | 45 | return super.defineClass(name, byteCode.getByteCode(), 0, byteCode.getByteCode().length); 46 | } 47 | 48 | public Map> getClasses() throws ClassNotFoundException { 49 | Map> classes = new HashMap>(); 50 | for (MemoryByteCode byteCode : byteCodes.values()) { 51 | classes.put(byteCode.getClassName(), findClass(byteCode.getClassName())); 52 | } 53 | return classes; 54 | } 55 | 56 | public Map getByteCodes() { 57 | Map result = new HashMap(byteCodes.size()); 58 | for (Entry entry : byteCodes.entrySet()) { 59 | result.put(entry.getKey(), entry.getValue().getByteCode()); 60 | } 61 | return result; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /cm4j-hotswap/src/main/java/com/cm4j/eval/compiler/DynamicCompiler.java: -------------------------------------------------------------------------------- 1 | package com.cm4j.eval.compiler; 2 | 3 | import java.util.*; 4 | 5 | import javax.tools.*; 6 | 7 | public class DynamicCompiler { 8 | private final JavaCompiler javaCompiler = ToolProvider.getSystemJavaCompiler(); 9 | private final StandardJavaFileManager standardFileManager; 10 | private final List options = new ArrayList(); 11 | private final DynamicClassLoader dynamicClassLoader; 12 | 13 | private final Collection compilationUnits = new ArrayList(); 14 | private final List> errors = new ArrayList>(); 15 | private final List> warnings = new ArrayList>(); 16 | 17 | public DynamicCompiler(ClassLoader classLoader) { 18 | if (javaCompiler == null) { 19 | throw new IllegalStateException( 20 | "Can not load JavaCompiler from javax.tools.ToolProvider#getSystemJavaCompiler()," 21 | + " please confirm the application running in JDK not JRE."); 22 | } 23 | standardFileManager = javaCompiler.getStandardFileManager(null, null, null); 24 | 25 | options.add("-Xlint:unchecked"); 26 | dynamicClassLoader = new DynamicClassLoader(classLoader); 27 | } 28 | 29 | public void addSource(String className, String source) { 30 | addSource(new StringSource(className, source)); 31 | } 32 | 33 | public void addSource(JavaFileObject javaFileObject) { 34 | compilationUnits.add(javaFileObject); 35 | } 36 | 37 | public Map> build() { 38 | 39 | errors.clear(); 40 | warnings.clear(); 41 | 42 | JavaFileManager fileManager = new DynamicJavaFileManager(standardFileManager, dynamicClassLoader); 43 | 44 | DiagnosticCollector collector = new DiagnosticCollector(); 45 | JavaCompiler.CompilationTask task = javaCompiler.getTask(null, fileManager, collector, options, null, 46 | compilationUnits); 47 | 48 | try { 49 | 50 | if (!compilationUnits.isEmpty()) { 51 | boolean result = task.call(); 52 | 53 | if (!result || collector.getDiagnostics().size() > 0) { 54 | 55 | for (Diagnostic diagnostic : collector.getDiagnostics()) { 56 | switch (diagnostic.getKind()) { 57 | case NOTE: 58 | case MANDATORY_WARNING: 59 | case WARNING: 60 | warnings.add(diagnostic); 61 | break; 62 | case OTHER: 63 | case ERROR: 64 | default: 65 | errors.add(diagnostic); 66 | break; 67 | } 68 | 69 | } 70 | 71 | if (!errors.isEmpty()) { 72 | throw new DynamicCompilerException("Compilation Error", errors); 73 | } 74 | } 75 | } 76 | 77 | Map> classes = dynamicClassLoader.getClasses(); 78 | return classes; 79 | } catch (Throwable e) { 80 | throw new DynamicCompilerException(e, errors); 81 | } finally { 82 | compilationUnits.clear(); 83 | 84 | } 85 | 86 | } 87 | 88 | public Map buildByteCodes() { 89 | 90 | errors.clear(); 91 | warnings.clear(); 92 | 93 | JavaFileManager fileManager = new DynamicJavaFileManager(standardFileManager, dynamicClassLoader); 94 | 95 | DiagnosticCollector collector = new DiagnosticCollector(); 96 | JavaCompiler.CompilationTask task = javaCompiler.getTask(null, fileManager, collector, options, null, 97 | compilationUnits); 98 | 99 | try { 100 | 101 | if (!compilationUnits.isEmpty()) { 102 | boolean result = task.call(); 103 | 104 | if (!result || collector.getDiagnostics().size() > 0) { 105 | 106 | for (Diagnostic diagnostic : collector.getDiagnostics()) { 107 | switch (diagnostic.getKind()) { 108 | case NOTE: 109 | case MANDATORY_WARNING: 110 | case WARNING: 111 | warnings.add(diagnostic); 112 | break; 113 | case OTHER: 114 | case ERROR: 115 | default: 116 | errors.add(diagnostic); 117 | break; 118 | } 119 | 120 | } 121 | 122 | if (!errors.isEmpty()) { 123 | throw new DynamicCompilerException("Compilation Error", errors); 124 | } 125 | } 126 | } 127 | 128 | return dynamicClassLoader.getByteCodes(); 129 | } catch (ClassFormatError e) { 130 | throw new DynamicCompilerException(e, errors); 131 | } finally { 132 | compilationUnits.clear(); 133 | 134 | } 135 | 136 | } 137 | 138 | private List diagnosticToString(List> diagnostics) { 139 | 140 | List diagnosticMessages = new ArrayList(); 141 | 142 | for (Diagnostic diagnostic : diagnostics) { 143 | diagnosticMessages.add( 144 | "line: " + diagnostic.getLineNumber() + ", message: " + diagnostic.getMessage(Locale.US)); 145 | } 146 | 147 | return diagnosticMessages; 148 | 149 | } 150 | 151 | public List getErrors() { 152 | return diagnosticToString(errors); 153 | } 154 | 155 | public List getWarnings() { 156 | return diagnosticToString(warnings); 157 | } 158 | 159 | public ClassLoader getClassLoader() { 160 | return dynamicClassLoader; 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /cm4j-hotswap/src/main/java/com/cm4j/eval/compiler/DynamicCompilerException.java: -------------------------------------------------------------------------------- 1 | package com.cm4j.eval.compiler; 2 | 3 | /*- 4 | * #%L 5 | * compiler 6 | * %% 7 | * Copyright (C) 2017 - 2018 SkaLogs 8 | * %% 9 | * Licensed under the Apache License, Version 2.0 (the "License"); 10 | * you may not use this file except in compliance with the License. 11 | * You may obtain a copy of the License at 12 | * 13 | * http://www.apache.org/licenses/LICENSE-2.0 14 | * 15 | * Unless required by applicable law or agreed to in writing, software 16 | * distributed under the License is distributed on an "AS IS" BASIS, 17 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | * See the License for the specific language governing permissions and 19 | * limitations under the License. 20 | * #L% 21 | */ 22 | 23 | import java.util.*; 24 | 25 | import javax.tools.Diagnostic; 26 | import javax.tools.JavaFileObject; 27 | 28 | public class DynamicCompilerException extends RuntimeException { 29 | private static final long serialVersionUID = 1L; 30 | private List> diagnostics; 31 | 32 | public DynamicCompilerException(String message, List> diagnostics) { 33 | super(message); 34 | this.diagnostics = diagnostics; 35 | } 36 | 37 | public DynamicCompilerException(Throwable cause, List> diagnostics) { 38 | super(cause); 39 | this.diagnostics = diagnostics; 40 | } 41 | 42 | private List> getErrorList() { 43 | List> messages = new ArrayList>(); 44 | if (diagnostics != null) { 45 | for (Diagnostic diagnostic : diagnostics) { 46 | Map message = new HashMap(); 47 | message.put("line", diagnostic.getLineNumber()); 48 | message.put("message", diagnostic.getMessage(Locale.US)); 49 | messages.add(message); 50 | } 51 | 52 | } 53 | return messages; 54 | } 55 | 56 | private String getErrors() { 57 | StringBuilder errors = new StringBuilder(); 58 | 59 | for (Map message : getErrorList()) { 60 | for (Map.Entry entry : message.entrySet()) { 61 | Object value = entry.getValue(); 62 | if (value != null && !value.toString().isEmpty()) { 63 | errors.append(entry.getKey()); 64 | errors.append(": "); 65 | errors.append(value); 66 | } 67 | errors.append(" , "); 68 | } 69 | 70 | errors.append("\n"); 71 | } 72 | 73 | return errors.toString(); 74 | 75 | } 76 | 77 | @Override 78 | public String getMessage() { 79 | return super.getMessage() + "\n" + getErrors(); 80 | } 81 | 82 | } 83 | -------------------------------------------------------------------------------- /cm4j-hotswap/src/main/java/com/cm4j/eval/compiler/DynamicJavaFileManager.java: -------------------------------------------------------------------------------- 1 | package com.cm4j.eval.compiler; 2 | 3 | import java.io.IOException; 4 | import java.util.ArrayList; 5 | import java.util.Iterator; 6 | import java.util.List; 7 | import java.util.Set; 8 | 9 | import javax.tools.*; 10 | 11 | public class DynamicJavaFileManager extends ForwardingJavaFileManager { 12 | private static final String[] superLocationNames = { StandardLocation.PLATFORM_CLASS_PATH.name(), 13 | /** JPMS StandardLocation.SYSTEM_MODULES **/ 14 | "SYSTEM_MODULES" }; 15 | private final PackageInternalsFinder finder; 16 | 17 | private final DynamicClassLoader classLoader; 18 | private final List byteCodes = new ArrayList(); 19 | 20 | public DynamicJavaFileManager(JavaFileManager fileManager, DynamicClassLoader classLoader) { 21 | super(fileManager); 22 | this.classLoader = classLoader; 23 | 24 | finder = new PackageInternalsFinder(classLoader); 25 | } 26 | 27 | @Override 28 | public JavaFileObject getJavaFileForOutput(Location location, String className, 29 | JavaFileObject.Kind kind, FileObject sibling) throws IOException { 30 | 31 | for (MemoryByteCode byteCode : byteCodes) { 32 | if (byteCode.getClassName().equals(className)) { 33 | return byteCode; 34 | } 35 | } 36 | 37 | MemoryByteCode innerClass = new MemoryByteCode(className); 38 | byteCodes.add(innerClass); 39 | classLoader.registerCompiledSource(innerClass); 40 | return innerClass; 41 | 42 | } 43 | 44 | @Override 45 | public ClassLoader getClassLoader(Location location) { 46 | return classLoader; 47 | } 48 | 49 | @Override 50 | public String inferBinaryName(Location location, JavaFileObject file) { 51 | if (file instanceof CustomJavaFileObject) { 52 | return ((CustomJavaFileObject) file).binaryName(); 53 | } else { 54 | /** 55 | * if it's not CustomJavaFileObject, then it's coming from standard file manager 56 | * - let it handle the file 57 | */ 58 | return super.inferBinaryName(location, file); 59 | } 60 | } 61 | 62 | @Override 63 | public Iterable list(Location location, String packageName, Set kinds, 64 | boolean recurse) throws IOException { 65 | if (location instanceof StandardLocation) { 66 | String locationName = ((StandardLocation) location).name(); 67 | for (String name : superLocationNames) { 68 | if (name.equals(locationName)) { 69 | return super.list(location, packageName, kinds, recurse); 70 | } 71 | } 72 | } 73 | 74 | // merge JavaFileObjects from specified ClassLoader 75 | if (location == StandardLocation.CLASS_PATH && kinds.contains(JavaFileObject.Kind.CLASS)) { 76 | return new IterableJoin(super.list(location, packageName, kinds, recurse), 77 | finder.find(packageName)); 78 | } 79 | 80 | return super.list(location, packageName, kinds, recurse); 81 | } 82 | 83 | static class IterableJoin implements Iterable { 84 | private final Iterable first, next; 85 | 86 | public IterableJoin(Iterable first, Iterable next) { 87 | this.first = first; 88 | this.next = next; 89 | } 90 | 91 | @Override 92 | public Iterator iterator() { 93 | return new IteratorJoin(first.iterator(), next.iterator()); 94 | } 95 | } 96 | 97 | static class IteratorJoin implements Iterator { 98 | private final Iterator first, next; 99 | 100 | public IteratorJoin(Iterator first, Iterator next) { 101 | this.first = first; 102 | this.next = next; 103 | } 104 | 105 | @Override 106 | public boolean hasNext() { 107 | return first.hasNext() || next.hasNext(); 108 | } 109 | 110 | @Override 111 | public T next() { 112 | if (first.hasNext()) { 113 | return first.next(); 114 | } 115 | return next.next(); 116 | } 117 | 118 | @Override 119 | public void remove() { 120 | throw new UnsupportedOperationException("remove"); 121 | } 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /cm4j-hotswap/src/main/java/com/cm4j/eval/compiler/MemoryByteCode.java: -------------------------------------------------------------------------------- 1 | package com.cm4j.eval.compiler; 2 | 3 | /*- 4 | * #%L 5 | * compiler 6 | * %% 7 | * Copyright (C) 2017 - 2018 SkaLogs 8 | * %% 9 | * Licensed under the Apache License, Version 2.0 (the "License"); 10 | * you may not use this file except in compliance with the License. 11 | * You may obtain a copy of the License at 12 | * 13 | * http://www.apache.org/licenses/LICENSE-2.0 14 | * 15 | * Unless required by applicable law or agreed to in writing, software 16 | * distributed under the License is distributed on an "AS IS" BASIS, 17 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | * See the License for the specific language governing permissions and 19 | * limitations under the License. 20 | * #L% 21 | */ 22 | 23 | import java.io.ByteArrayOutputStream; 24 | import java.io.IOException; 25 | import java.io.OutputStream; 26 | import java.net.URI; 27 | import java.net.URISyntaxException; 28 | 29 | import javax.tools.SimpleJavaFileObject; 30 | 31 | 32 | public class MemoryByteCode extends SimpleJavaFileObject { 33 | private static final char PKG_SEPARATOR = '.'; 34 | private static final char DIR_SEPARATOR = '/'; 35 | private static final String CLASS_FILE_SUFFIX = ".class"; 36 | 37 | private ByteArrayOutputStream byteArrayOutputStream; 38 | 39 | public MemoryByteCode(String className) { 40 | super(URI.create("byte:///" + className.replace(PKG_SEPARATOR, DIR_SEPARATOR) 41 | + Kind.CLASS.extension), Kind.CLASS); 42 | } 43 | 44 | public MemoryByteCode(String className, ByteArrayOutputStream byteArrayOutputStream) 45 | throws URISyntaxException { 46 | this(className); 47 | this.byteArrayOutputStream = byteArrayOutputStream; 48 | } 49 | 50 | @Override 51 | public OutputStream openOutputStream() throws IOException { 52 | if (byteArrayOutputStream == null) { 53 | byteArrayOutputStream = new ByteArrayOutputStream(); 54 | } 55 | return byteArrayOutputStream; 56 | } 57 | 58 | public byte[] getByteCode() { 59 | return byteArrayOutputStream.toByteArray(); 60 | } 61 | 62 | public String getClassName() { 63 | String className = getName(); 64 | className = className.replace(DIR_SEPARATOR, PKG_SEPARATOR); 65 | className = className.substring(1, className.indexOf(CLASS_FILE_SUFFIX)); 66 | return className; 67 | } 68 | 69 | } 70 | -------------------------------------------------------------------------------- /cm4j-hotswap/src/main/java/com/cm4j/eval/compiler/PackageInternalsFinder.java: -------------------------------------------------------------------------------- 1 | package com.cm4j.eval.compiler; 2 | 3 | /*- 4 | * #%L 5 | * compiler 6 | * %% 7 | * Copyright (C) 2017 - 2018 SkaLogs 8 | * %% 9 | * Licensed under the Apache License, Version 2.0 (the "License"); 10 | * you may not use this file except in compliance with the License. 11 | * You may obtain a copy of the License at 12 | * 13 | * http://www.apache.org/licenses/LICENSE-2.0 14 | * 15 | * Unless required by applicable law or agreed to in writing, software 16 | * distributed under the License is distributed on an "AS IS" BASIS, 17 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | * See the License for the specific language governing permissions and 19 | * limitations under the License. 20 | * #L% 21 | */ 22 | 23 | import java.io.File; 24 | import java.io.IOException; 25 | import java.net.JarURLConnection; 26 | import java.net.URI; 27 | import java.net.URL; 28 | import java.util.ArrayList; 29 | import java.util.Collection; 30 | import java.util.Enumeration; 31 | import java.util.List; 32 | import java.util.jar.JarEntry; 33 | 34 | import javax.tools.JavaFileObject; 35 | 36 | public class PackageInternalsFinder { 37 | private final ClassLoader classLoader; 38 | private static final String CLASS_FILE_EXTENSION = ".class"; 39 | 40 | public PackageInternalsFinder(ClassLoader classLoader) { 41 | this.classLoader = classLoader; 42 | } 43 | 44 | public List find(String packageName) throws IOException { 45 | String javaPackageName = packageName.replaceAll("\\.", "/"); 46 | 47 | List result = new ArrayList(); 48 | 49 | Enumeration urlEnumeration = classLoader.getResources(javaPackageName); 50 | while (urlEnumeration.hasMoreElements()) { // one URL for each jar on the classpath that has the given package 51 | URL packageFolderURL = urlEnumeration.nextElement(); 52 | result.addAll(listUnder(packageName, packageFolderURL)); 53 | } 54 | 55 | return result; 56 | } 57 | 58 | private Collection listUnder(String packageName, URL packageFolderURL) { 59 | File directory = new File(packageFolderURL.getFile()); 60 | if (directory.isDirectory()) { // browse local .class files - useful for local execution 61 | return processDir(packageName, directory); 62 | } else { // browse a jar file 63 | return processJar(packageFolderURL); 64 | } // maybe there can be something else for more involved class loaders 65 | } 66 | 67 | private List processJar(URL packageFolderURL) { 68 | List result = new ArrayList(); 69 | try { 70 | String jarUri = packageFolderURL.toExternalForm().substring(0, packageFolderURL.toExternalForm().lastIndexOf("!/")); 71 | 72 | JarURLConnection jarConn = (JarURLConnection) packageFolderURL.openConnection(); 73 | String rootEntryName = jarConn.getEntryName(); 74 | int rootEnd = rootEntryName.length() + 1; 75 | 76 | Enumeration entryEnum = jarConn.getJarFile().entries(); 77 | while (entryEnum.hasMoreElements()) { 78 | JarEntry jarEntry = entryEnum.nextElement(); 79 | String name = jarEntry.getName(); 80 | if (name.startsWith(rootEntryName) && name.indexOf('/', rootEnd) == -1 && name.endsWith(CLASS_FILE_EXTENSION)) { 81 | URI uri = URI.create(jarUri + "!/" + name); 82 | String binaryName = name.replaceAll("/", "."); 83 | binaryName = binaryName.replaceAll(CLASS_FILE_EXTENSION + "$", ""); 84 | 85 | result.add(new CustomJavaFileObject(binaryName, uri)); 86 | } 87 | } 88 | } catch (Exception e) { 89 | throw new RuntimeException("Wasn't able to open " + packageFolderURL + " as a jar file", e); 90 | } 91 | return result; 92 | } 93 | 94 | private List processDir(String packageName, File directory) { 95 | List result = new ArrayList(); 96 | 97 | File[] childFiles = directory.listFiles(); 98 | for (File childFile : childFiles) { 99 | if (childFile.isFile()) { 100 | // We only want the .class files. 101 | if (childFile.getName().endsWith(CLASS_FILE_EXTENSION)) { 102 | String binaryName = packageName + "." + childFile.getName(); 103 | binaryName = binaryName.replaceAll(CLASS_FILE_EXTENSION + "$", ""); 104 | 105 | result.add(new CustomJavaFileObject(binaryName, childFile.toURI())); 106 | } 107 | } 108 | } 109 | 110 | return result; 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /cm4j-hotswap/src/main/java/com/cm4j/eval/compiler/StringSource.java: -------------------------------------------------------------------------------- 1 | package com.cm4j.eval.compiler; 2 | 3 | /*- 4 | * #%L 5 | * compiler 6 | * %% 7 | * Copyright (C) 2017 - 2018 SkaLogs 8 | * %% 9 | * Licensed under the Apache License, Version 2.0 (the "License"); 10 | * you may not use this file except in compliance with the License. 11 | * You may obtain a copy of the License at 12 | * 13 | * http://www.apache.org/licenses/LICENSE-2.0 14 | * 15 | * Unless required by applicable law or agreed to in writing, software 16 | * distributed under the License is distributed on an "AS IS" BASIS, 17 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | * See the License for the specific language governing permissions and 19 | * limitations under the License. 20 | * #L% 21 | */ 22 | 23 | import java.io.IOException; 24 | import java.net.URI; 25 | 26 | import javax.tools.SimpleJavaFileObject; 27 | 28 | public class StringSource extends SimpleJavaFileObject { 29 | private final String contents; 30 | 31 | public StringSource(String className, String contents) { 32 | super(URI.create("string:///" + className.replace('.', '/') + Kind.SOURCE.extension), Kind.SOURCE); 33 | this.contents = contents; 34 | } 35 | 36 | @Override 37 | public CharSequence getCharContent(boolean ignoreEncodingErrors) throws IOException { 38 | return contents; 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /cm4j-hotswap/src/main/java/com/cm4j/grpc/client/GrpcClient.java: -------------------------------------------------------------------------------- 1 | package com.cm4j.grpc.client; 2 | 3 | import com.cm4j.grpc.config.GrpcConfig; 4 | import com.cm4j.grpc.proto.MsMethodServiceGrpc; 5 | import com.google.common.collect.Maps; 6 | import io.grpc.Channel; 7 | import io.grpc.Deadline; 8 | import io.grpc.ManagedChannel; 9 | import io.grpc.ManagedChannelBuilder; 10 | import io.grpc.stub.AbstractBlockingStub; 11 | import org.apache.commons.lang3.tuple.Pair; 12 | import org.slf4j.Logger; 13 | import org.slf4j.LoggerFactory; 14 | 15 | import java.lang.reflect.Method; 16 | import java.util.Map; 17 | import java.util.concurrent.TimeUnit; 18 | 19 | public class GrpcClient { 20 | 21 | private static final Logger log = LoggerFactory.getLogger(GrpcClient.class); 22 | 23 | /** 24 | * gRPC blocking deadline time (seconds) 25 | **/ 26 | private static int GRPC_BLOCKING_DEADLINE_SECONDS = 4; 27 | 28 | /** 29 | * 同步RPC请求服务关系,K-grpcClass.getName(),V-newBlockingStub 30 | */ 31 | private static final Map GRPC_SERVICE_BLOCKING_METHODS = Maps.newConcurrentMap(); 32 | /** 33 | * RPC连接信息,K-mainSid,V-连接信息 34 | */ 35 | private static final Map GRPC_CHANNELS = Maps.newConcurrentMap(); 36 | 37 | /** 38 | * 获取channel 39 | * 40 | * @param serverId 41 | * @return 42 | */ 43 | private static ManagedChannel getChannel(int serverId) { 44 | ManagedChannel managedChannel = GRPC_CHANNELS.get(serverId); 45 | if (managedChannel == null) { 46 | Pair pair = GrpcConfig.getAddressPort(serverId); 47 | managedChannel = ManagedChannelBuilder.forAddress(pair.getLeft(), pair.getRight()).usePlaintext().build(); 48 | GRPC_CHANNELS.put(serverId, managedChannel); 49 | } 50 | return managedChannel; 51 | } 52 | 53 | /** 54 | * 获取阻塞调用的service 55 | * 56 | * @param grpcClass *Grpc.class 57 | * @return 58 | */ 59 | public static T getBlockingStub0(int serverId, Class grpcClass) { 60 | try { 61 | final ManagedChannel channel = getChannel(serverId); 62 | 63 | log.error("{}\r\nchannel isShutdown={} isTerminated={} state={}", channel, channel.isShutdown(), 64 | channel.isTerminated(), channel.getState(false)); 65 | Method method = GRPC_SERVICE_BLOCKING_METHODS.get(grpcClass.getName()); 66 | if (method == null) { 67 | method = grpcClass.getDeclaredMethod("newBlockingStub", Channel.class); 68 | Method absent = GRPC_SERVICE_BLOCKING_METHODS.putIfAbsent(grpcClass.getName(), method); 69 | if (absent != null) { 70 | method = absent; 71 | } 72 | } 73 | AbstractBlockingStub instance = (AbstractBlockingStub) method.invoke(null, channel); 74 | // 注意:超时时间是从withDeadline设置后就开始算了,所以不要提前获取Stub,在真正调用的时候再获取 75 | return (T) instance.withDeadline(Deadline.after(GRPC_BLOCKING_DEADLINE_SECONDS, TimeUnit.SECONDS)); 76 | } catch (Exception e) { 77 | log.error("getBlockingStub error, grpcClass={}, deadLineSec={}", grpcClass.getName(), 78 | GRPC_BLOCKING_DEADLINE_SECONDS, e); 79 | } 80 | return null; 81 | } 82 | 83 | } 84 | -------------------------------------------------------------------------------- /cm4j-hotswap/src/main/java/com/cm4j/grpc/config/GrpcConfig.java: -------------------------------------------------------------------------------- 1 | package com.cm4j.grpc.config; 2 | 3 | import com.google.common.collect.Maps; 4 | import org.apache.commons.lang3.tuple.Pair; 5 | 6 | import java.util.Map; 7 | 8 | /** 9 | * GRPC测试类 10 | * 主要是为了配置grpc的信息 11 | */ 12 | public class GrpcConfig { 13 | 14 | private static final Map> config = Maps.newHashMap(); 15 | 16 | public static Pair getAddressPort(int serverId) { 17 | return config.get(serverId); 18 | } 19 | 20 | public static void addAddressPort(int serverId, String address, int port) { 21 | config.put(serverId, Pair.of(address, port)); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /cm4j-hotswap/src/main/java/com/cm4j/grpc/server/GrpcServer.java: -------------------------------------------------------------------------------- 1 | package com.cm4j.grpc.server; 2 | 3 | import com.cm4j.grpc.service.MsMethodServiceImpl; 4 | import io.grpc.ServerBuilder; 5 | import io.grpc.util.MutableHandlerRegistry; 6 | 7 | import java.io.IOException; 8 | 9 | /** 10 | * 这里为了演示,只实现最简答的grpc用法 11 | */ 12 | public class GrpcServer { 13 | 14 | public GrpcServer(int port) throws IOException { 15 | MutableHandlerRegistry registry = new MutableHandlerRegistry(); 16 | // 硬编码注册处理类,可替换使用扫描注册 17 | registry.addService(new MsMethodServiceImpl()); 18 | 19 | // 服务器启动 20 | ServerBuilder.forPort(port).fallbackHandlerRegistry(registry).build().start(); 21 | } 22 | 23 | // 当前服务器ID 24 | private static int serverId; 25 | 26 | /** 27 | * 设置当前服务器ID 28 | * 29 | * @param serverId 30 | */ 31 | public static void setServerId(int serverId) { 32 | GrpcServer.serverId = serverId; 33 | } 34 | 35 | /** 36 | * 判断是否是当前服务器ID 37 | * 38 | * @param sid 39 | * @return 40 | */ 41 | public static boolean isSameServer(int sid) { 42 | return serverId == sid; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /cm4j-hotswap/src/main/java/com/cm4j/grpc/service/MsMethodServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.cm4j.grpc.service; 2 | 3 | import com.cm4j.grpc.proto.MS_METHOD_GRPC; 4 | import com.cm4j.grpc.proto.MsMethodServiceGrpc; 5 | import com.cm4j.util.RemotingInvokerUtil; 6 | import com.google.protobuf.MessageLite; 7 | import io.grpc.Status; 8 | import io.grpc.StatusRuntimeException; 9 | import io.grpc.stub.StreamObserver; 10 | import org.slf4j.Logger; 11 | import org.slf4j.LoggerFactory; 12 | 13 | /** 14 | * Description:远程方法调用 15 | * 16 | * @author yeas.fun 17 | * @since 2021/8/27 18 | */ 19 | public class MsMethodServiceImpl extends MsMethodServiceGrpc.MsMethodServiceImplBase { 20 | 21 | private static final Logger log = LoggerFactory.getLogger(MsMethodServiceImpl.class); 22 | 23 | @Override 24 | public void invoker(MS_METHOD_GRPC.MS_METHOD_REQ request, StreamObserver responseObserver) { 25 | String className = request.getClassName(); 26 | String methodName = request.getMethodName(); 27 | MS_METHOD_GRPC.MS_METHOD_RESP.Builder resp = MS_METHOD_GRPC.MS_METHOD_RESP.newBuilder(); 28 | try { 29 | Object invoke = RemotingInvokerUtil.remoteInvoke(className, methodName, request); 30 | if (invoke != null) { 31 | resp.setReback(RemotingInvokerUtil.encodeParams(new Object[]{invoke})); 32 | } 33 | responseObserver.onNext(resp.build()); 34 | } catch (StatusRuntimeException e) { 35 | String message = "异常经过服>>>" + e.getStatus().getDescription(); 36 | Status status = e.getStatus().withDescription(message); 37 | responseObserver.onError(status.asRuntimeException()); 38 | } catch (Exception e) { 39 | String description = "异常发生服 grpc 执行异常:" + e.getMessage(); 40 | responseObserver.onError( 41 | Status.INVALID_ARGUMENT.withDescription(description).withCause(e).asRuntimeException()); 42 | log.error("invoker[{}.{}] error", className, methodName, e); 43 | } finally { 44 | responseObserver.onCompleted(); 45 | } 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /cm4j-hotswap/src/main/java/com/cm4j/hotswap/agent/JavaAgent.java: -------------------------------------------------------------------------------- 1 | package com.cm4j.hotswap.agent; 2 | 3 | import java.io.File; 4 | import java.io.IOException; 5 | import java.io.InputStream; 6 | import java.lang.instrument.ClassDefinition; 7 | import java.lang.instrument.Instrumentation; 8 | import java.lang.instrument.UnmodifiableClassException; 9 | import java.lang.management.ManagementFactory; 10 | import java.util.ArrayList; 11 | import java.util.LinkedHashMap; 12 | import java.util.LinkedHashSet; 13 | import java.util.List; 14 | import java.util.Map.Entry; 15 | import java.util.jar.JarEntry; 16 | import java.util.jar.JarFile; 17 | 18 | import org.apache.commons.io.FileUtils; 19 | import org.apache.commons.io.IOUtils; 20 | import org.apache.commons.lang3.StringUtils; 21 | import org.slf4j.Logger; 22 | import org.slf4j.LoggerFactory; 23 | 24 | import com.cm4j.agent.JavaDynAgent; 25 | import com.cm4j.agent.JavaDynAgentLocation; 26 | import com.google.common.collect.Maps; 27 | import com.google.common.collect.Sets; 28 | import com.sun.tools.attach.AgentInitializationException; 29 | import com.sun.tools.attach.AgentLoadException; 30 | import com.sun.tools.attach.AttachNotSupportedException; 31 | import com.sun.tools.attach.VirtualMachine; 32 | 33 | /** 34 | * API:Agent方式热更新 35 | */ 36 | public class JavaAgent { 37 | 38 | public static final Logger log = LoggerFactory.getLogger(JavaAgent.class); 39 | 40 | private static String jarPath; 41 | private static VirtualMachine vm; 42 | private static String pid; 43 | 44 | static { 45 | jarPath = getJarPath(); 46 | log.error("java agent:jarPath:{}", jarPath); 47 | 48 | // 当前进程pid 49 | String name = ManagementFactory.getRuntimeMXBean().getName(); 50 | pid = StringUtils.substringBefore(name, "@"); 51 | log.error("current pid {}", pid); 52 | } 53 | 54 | /** 55 | * 获取jar包路径 56 | * 57 | * @return 58 | */ 59 | private static String getJarPath() { 60 | // 基于jar包中的类定位jar包位置 61 | String path = JavaDynAgentLocation.class.getProtectionDomain().getCodeSource().getLocation().getPath(); 62 | // 定位绝对路径 63 | return new File(path).getAbsolutePath(); 64 | } 65 | 66 | private static void init() 67 | throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException { 68 | if (JavaDynAgent.getInstrumentation() != null) { 69 | // 已经有此对象,则无需再次初始化获取 70 | return; 71 | } 72 | // 连接虚拟机,并attach当前agent的jar包 73 | // agentmain()方法会设置Instrumentation 74 | vm = VirtualMachine.attach(pid); 75 | vm.loadAgent(jarPath); 76 | 77 | // 从而获取到当前虚拟机 78 | Instrumentation instrumentation = JavaDynAgent.getInstrumentation(); 79 | if (instrumentation == null) { 80 | log.error("instrumentation is null"); 81 | } 82 | } 83 | 84 | private static void destroy() throws IOException { 85 | if (vm != null) { 86 | vm.detach(); 87 | } 88 | log.error("java agent redefine classes end"); 89 | } 90 | 91 | /** 92 | * 从jar包重新加载类 93 | * 94 | * @param classArr 95 | * @throws ClassNotFoundException 96 | * @throws IOException 97 | * @throws UnmodifiableClassException 98 | * @throws AttachNotSupportedException 99 | * @throws AgentLoadException 100 | * @throws AgentInitializationException 101 | */ 102 | public static void javaAgent(String[] classArr) 103 | throws ClassNotFoundException, IOException, UnmodifiableClassException, AttachNotSupportedException, AgentLoadException, AgentInitializationException { 104 | log.error("java agent redefine classes started"); 105 | init(); 106 | try { 107 | LinkedHashMap>> redefineMap = Maps.newLinkedHashMap(); 108 | // 1.整理需要重定义的类 109 | List classDefList = new ArrayList(); 110 | for (String className : classArr) { 111 | Class c = Class.forName(className); 112 | String classLocation = c.getProtectionDomain().getCodeSource().getLocation().getPath(); 113 | LinkedHashSet> classSet = redefineMap.computeIfAbsent(classLocation, 114 | k -> Sets.newLinkedHashSet()); 115 | classSet.add(c); 116 | } 117 | if (!redefineMap.isEmpty()) { 118 | for (Entry>> entry : redefineMap.entrySet()) { 119 | String classLocation = entry.getKey(); 120 | log.error("class read from:{}", classLocation); 121 | if (classLocation.endsWith(".jar")) { 122 | try (JarFile jf = new JarFile(classLocation)) { 123 | for (Class cls : entry.getValue()) { 124 | String clazz = cls.getName().replace('.', '/') + ".class"; 125 | JarEntry je = jf.getJarEntry(clazz); 126 | if (je != null) { 127 | log.error("class redefined:\t{}", clazz); 128 | try (InputStream stream = jf.getInputStream(je)) { 129 | byte[] data = IOUtils.toByteArray(stream); 130 | classDefList.add(new ClassDefinition(cls, data)); 131 | } 132 | } else { 133 | throw new IOException("JarEntry " + clazz + " not found"); 134 | } 135 | } 136 | } 137 | } else { 138 | File file; 139 | for (Class cls : entry.getValue()) { 140 | String clazz = cls.getName().replace('.', '/') + ".class"; 141 | file = new File(classLocation, clazz); 142 | log.error("class redefined:{}", file.getAbsolutePath()); 143 | byte[] data = FileUtils.readFileToByteArray(file); 144 | classDefList.add(new ClassDefinition(cls, data)); 145 | } 146 | } 147 | } 148 | // 2.redefine 149 | JavaDynAgent.getInstrumentation().redefineClasses(classDefList.toArray(new ClassDefinition[0])); 150 | } 151 | } finally { 152 | destroy(); 153 | } 154 | } 155 | 156 | } 157 | -------------------------------------------------------------------------------- /cm4j-hotswap/src/main/java/com/cm4j/hotswap/recompile/RecompileClassLoader.java: -------------------------------------------------------------------------------- 1 | package com.cm4j.hotswap.recompile; 2 | 3 | /** 4 | * @author yeas.fun 5 | * @since 2020/12/15 6 | */ 7 | public class RecompileClassLoader extends ClassLoader { 8 | 9 | private final String className; 10 | private byte[] byteCodes; 11 | 12 | private Class defineClass; 13 | 14 | public RecompileClassLoader(ClassLoader parent, String className, byte[] byteCodes) { 15 | super(parent); 16 | this.className = className; 17 | this.byteCodes = byteCodes; 18 | } 19 | 20 | @Override 21 | public Class findClass(String name) throws ClassNotFoundException { 22 | if (!name.equals(className)) { 23 | return null; 24 | } 25 | 26 | if (this.defineClass != null) { 27 | return this.defineClass; 28 | } 29 | 30 | this.defineClass = super.defineClass(name, byteCodes, 0, byteCodes.length); 31 | // 清空字节数组的内存,释放内存 32 | this.byteCodes = null; 33 | return defineClass; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /cm4j-hotswap/src/main/java/com/cm4j/hotswap/recompile/RecompileHotSwap.java: -------------------------------------------------------------------------------- 1 | package com.cm4j.hotswap.recompile; 2 | 3 | import java.io.File; 4 | import java.io.IOException; 5 | import java.io.InputStream; 6 | import java.lang.reflect.Modifier; 7 | import java.time.LocalDateTime; 8 | 9 | import org.apache.commons.lang3.StringUtils; 10 | import org.slf4j.Logger; 11 | import org.slf4j.LoggerFactory; 12 | 13 | import com.cm4j.util.ClassUtil; 14 | import com.google.common.io.Files; 15 | 16 | import javassist.ClassClassPath; 17 | import javassist.ClassPool; 18 | import javassist.CtClass; 19 | import javassist.CtConstructor; 20 | import javassist.CtMethod; 21 | import javassist.NotFoundException; 22 | 23 | /** 24 | * 新版热更 25 | * 26 | *

 27 |  * 原理:
 28 |  * 1.动态生成子类,替换原有父类实现
 29 |  * 2.采用spring进行绑定,而非自定义实现
 30 |  *
 31 |  * 重点:
 32 |  * 1.因为采取继承的方式,则类必须要有构造函数!
 33 |  * 2.方法内的final方法无法覆写,则无法热更,动态生成的子类会忽略final方法
 34 |  * 
35 | * 36 | * @author yeas.fun 37 | * @since 2020/12/15 38 | */ 39 | public class RecompileHotSwap { 40 | 41 | private static final Logger logger = LoggerFactory.getLogger(RecompileHotSwap.class); 42 | 43 | /** 子类后缀名 */ 44 | public static final String SUBCLASS_SUFFIX = "$$$SUBCLASS"; 45 | 46 | /** 47 | * 获取需要替换的新类的Class 48 | * 49 | * @param oldClass 原class名字 50 | * @return 51 | * @throws IOException 52 | */ 53 | public static Class recompileClass(Class oldClass) throws Exception { 54 | ClassPool pool = ClassPool.getDefault(); 55 | CtClass oldCtClass; 56 | try { 57 | oldCtClass = pool.getCtClass(oldClass.getName()); 58 | } catch (NotFoundException e) { 59 | logger.error("javassist.NotFoundException: {}", oldClass.getName()); 60 | // https://www.javassist.org/tutorial/tutorial.html 61 | // 页面搜索"Class search path" 62 | // 运行在web容器(如tomcat)中的程序,可能存在多个ClassLoader,导致ClassPool.getDefault()找不到对应的class 63 | pool.insertClassPath(new ClassClassPath(oldClass)); 64 | oldCtClass = pool.getCtClass(oldClass.getName()); 65 | } 66 | 67 | // 从class文件中获取CtClass 68 | CtClass newCtClass; 69 | try (InputStream classInputStream = ClassUtil.getClassInputStream(oldClass)) { 70 | newCtClass = pool.makeClass(classInputStream); 71 | } 72 | 73 | String newClassName = oldClass.getSimpleName() + SUBCLASS_SUFFIX; 74 | String newFullClassName = oldClass.getName() + SUBCLASS_SUFFIX; 75 | 76 | // 新类改名,设置父类为原来的类 77 | newCtClass.replaceClassName(oldClass.getName(), newFullClassName); 78 | newCtClass.setSuperclass(oldCtClass); 79 | 80 | // 如果有默认构造函数,则调用父类构造函数 81 | CtConstructor constructor = newCtClass.getDeclaredConstructor(new CtClass[0]); 82 | if (constructor == null) { 83 | throw new RuntimeException("has no default constructor:" + oldClass.getName()); 84 | } 85 | 86 | if (Modifier.isPrivate(constructor.getModifiers())) { 87 | throw new RuntimeException("the constructor is private, cannot extend:" + oldClass.getName()); 88 | } 89 | 90 | // 设置子类的构造函数为public的,方便后面newInstance 91 | constructor.setModifiers(Modifier.PUBLIC); 92 | // 设置默认构造函数为 super(); 93 | constructor.setBody("super();"); 94 | 95 | // final的方法忽略 96 | CtMethod[] declaredMethods = newCtClass.getDeclaredMethods(); 97 | for (CtMethod declaredMethod : declaredMethods) { 98 | int modifiers = declaredMethod.getModifiers(); 99 | boolean isPrivate = Modifier.isPrivate(modifiers); 100 | boolean isFinal = Modifier.isFinal(modifiers); 101 | if (!isPrivate && isFinal) { 102 | // 从新类中移除 103 | logger.error("{}.{}(), isFinal:{}, method is removed in newClass", oldClass, declaredMethod.getName(), 104 | isFinal); 105 | newCtClass.removeMethod(declaredMethod); 106 | } 107 | } 108 | 109 | // 二进制内容 110 | byte[] targetBytes = newCtClass.toBytecode(); 111 | 112 | // 移除临时类,让它从CtPool中移除,以便多次热更 113 | newCtClass.detach(); 114 | 115 | dump(targetBytes, newClassName); 116 | 117 | // 重新获取新类 118 | RecompileClassLoader recompileClassLoader = new RecompileClassLoader(oldClass.getClassLoader(), newFullClassName, 119 | targetBytes); 120 | return recompileClassLoader.findClass(newFullClassName); 121 | } 122 | 123 | private static void dump(byte[] targetBytes, String className) throws IOException { 124 | // class dump到日志中 125 | String basePath = new File("").getAbsolutePath(); 126 | String finalPath = StringUtils.join( 127 | new String[]{basePath, "recompile-output", className + "-" + LocalDateTime.now() + ".class"}, 128 | File.separator); 129 | File to = new File(finalPath); 130 | Files.createParentDirs(to); 131 | logger.error("class dumpd: {}", to.getAbsolutePath()); 132 | Files.write(targetBytes, to); 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /cm4j-hotswap/src/main/java/com/cm4j/invoke/IRemotingClass.java: -------------------------------------------------------------------------------- 1 | package com.cm4j.invoke; 2 | 3 | import com.cm4j.registry.registered.IRegistered; 4 | 5 | /** 6 | * 远程类的标识 7 | * 8 | *
 9 |  *  注意点:
10 |  *  1.方法定义不允许重载(方法名相同,参数不同)
11 |  *  2.方法仅支持:原生类型+proto结构体+Errorcode
12 |  *  3.方法必须是public的
13 |  *  4.getInstance()内部必须返回的是代理类
14 |  *  5.方法上面需要使用@RemotingMethod注解
15 |  *  6.方法第一个参数标明调用服务器id(可以用0表示本地调用)
16 |  *  7.类名约定为XxxRpc(例如,JoanRpc)
17 |  *  8.需要支持grpc异步处理的方法,只支持无返回的方法
18 |  *  
19 | * 20 | * @author yeas.fun 21 | * @since 2021/8/21 22 | */ 23 | public interface IRemotingClass extends IRegistered { 24 | 25 | } 26 | -------------------------------------------------------------------------------- /cm4j-hotswap/src/main/java/com/cm4j/invoke/RemotingMethod.java: -------------------------------------------------------------------------------- 1 | package com.cm4j.invoke; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | /** 9 | * 远程方法标识 10 | * 11 | * @author yeas.fun 12 | * @since 2021/8/21 13 | */ 14 | @Retention(RetentionPolicy.RUNTIME) 15 | @Target(value = {ElementType.METHOD}) 16 | public @interface RemotingMethod { 17 | 18 | } 19 | -------------------------------------------------------------------------------- /cm4j-hotswap/src/main/java/com/cm4j/invoke/impl/TestRpc.java: -------------------------------------------------------------------------------- 1 | package com.cm4j.invoke.impl; 2 | 3 | import com.cm4j.invoke.IRemotingClass; 4 | import com.cm4j.invoke.RemotingMethod; 5 | import com.cm4j.invoke.proxy.LocalProxyGenerator; 6 | 7 | public class TestRpc implements IRemotingClass { 8 | 9 | @RemotingMethod 10 | public String rpcTest(int sid, String data) { 11 | System.out.println("执行线程:" + Thread.currentThread()); 12 | return "sid:" + sid + ",data:" + data; 13 | } 14 | 15 | public static TestRpc getInstance() { 16 | return LocalProxyGenerator.getProxy(TestRpc.class); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /cm4j-hotswap/src/main/java/com/cm4j/invoke/invoker/IRemotingInvoker.java: -------------------------------------------------------------------------------- 1 | package com.cm4j.invoke.invoker; 2 | 3 | /** 4 | * @author yeas.fun 5 | * @since 2021/8/23 6 | */ 7 | public interface IRemotingInvoker { 8 | 9 | /** 10 | * 方法调用 11 | * 12 | * @param methodName 方法定位标识 13 | * @param params 方法需要的参数 14 | * @return 方法的返回结果 15 | */ 16 | Object invokeInternal(String methodName, Object[] params) throws Exception; 17 | } 18 | -------------------------------------------------------------------------------- /cm4j-hotswap/src/main/java/com/cm4j/invoke/invoker/RemotingInvokerScanner.java: -------------------------------------------------------------------------------- 1 | package com.cm4j.invoke.invoker; 2 | 3 | import com.cm4j.hotswap.recompile.RecompileClassLoader; 4 | import com.cm4j.invoke.IRemotingClass; 5 | import com.cm4j.invoke.RemotingMethod; 6 | import com.cm4j.invoke.proxy.LocalProxyGenerator; 7 | import com.cm4j.util.PackageUtil; 8 | import com.cm4j.util.RemotingInvokerUtil; 9 | import com.google.common.collect.ArrayListMultimap; 10 | import com.google.common.collect.Lists; 11 | import com.google.common.collect.Maps; 12 | import com.google.common.collect.Multimap; 13 | import javassist.ClassClassPath; 14 | import javassist.ClassPool; 15 | import javassist.CtClass; 16 | import javassist.CtMethod; 17 | import org.apache.commons.lang3.StringUtils; 18 | import org.slf4j.Logger; 19 | import org.slf4j.LoggerFactory; 20 | 21 | import java.io.File; 22 | import java.io.IOException; 23 | import java.lang.reflect.Method; 24 | import java.lang.reflect.Modifier; 25 | import java.util.List; 26 | import java.util.Map; 27 | import java.util.Set; 28 | 29 | /** 30 | * 动态生成类,构建对象并设置到${@link RemotingInvokerUtil} 31 | */ 32 | public class RemotingInvokerScanner { 33 | 34 | private static Multimap> classMethodParamTypes = ArrayListMultimap.create(); 35 | 36 | private static Map> classMethodReturnType = Maps.newHashMap(); 37 | 38 | /** 39 | * 代码扫描,动态构建内部switch方法 40 | */ 41 | @SuppressWarnings("unchecked") 42 | public static void init(String packageScann) throws Exception { 43 | Multimap> tmpClassMethodParamTypes = ArrayListMultimap.create(); 44 | Map> tmpClassMethodReturnType = Maps.newHashMap(); 45 | 46 | // 扫描类 47 | final Set> clazzes = PackageUtil.findPackageClass(packageScann); 48 | for (Class clazz : clazzes) { 49 | if (IRemotingClass.class.isAssignableFrom(clazz)) { 50 | // 排除接口和抽象类 51 | if (clazz.isInterface() || Modifier.isAbstract(clazz.getModifiers())) { 52 | continue; 53 | } 54 | 55 | // 扫描类的相关信息 56 | scanAndCheckMethod(clazz, clazz.getName(), tmpClassMethodParamTypes, tmpClassMethodReturnType); 57 | 58 | // 生成代理类 59 | LocalProxyGenerator.proxy((Class) clazz); 60 | } 61 | } 62 | 63 | classMethodParamTypes = tmpClassMethodParamTypes; 64 | classMethodReturnType = tmpClassMethodReturnType; 65 | } 66 | 67 | 68 | /** 69 | * 生成方法的消息体 70 | * 71 | * @param clazz 72 | * @param className 73 | * @param tmpClassMethodParamTypes 74 | * @param tmpClassMethodReturnType 75 | * @return 76 | */ 77 | private static void scanAndCheckMethod(Class clazz, String className, 78 | Multimap> tmpClassMethodParamTypes, Map> tmpClassMethodReturnType) { 79 | 80 | Method[] methods = clazz.getMethods(); 81 | for (Method method : methods) { 82 | RemotingMethod annotation = method.getAnnotation(RemotingMethod.class); 83 | if (annotation == null) { 84 | continue; 85 | } 86 | 87 | // 1.校验 88 | boolean isPublic = Modifier.isPublic(method.getModifiers()); 89 | if (!isPublic) { 90 | throw new RuntimeException("方法修饰符非public, class:" + clazz + ",method:" + method); 91 | } 92 | 93 | // 2.校验 94 | String methodName = method.getName(); 95 | String uniqueTag = className + "#" + methodName; 96 | if (tmpClassMethodParamTypes.containsKey(uniqueTag)) { 97 | throw new RuntimeException("存在重复的同名函数, class:" + clazz + ",method:" + method); 98 | } 99 | 100 | // 3.扫描信息 101 | Class[] parameterTypes = method.getParameterTypes(); 102 | if (parameterTypes.length == 0 || !RemotingInvokerUtil.checkFirstParamType(parameterTypes[0])) { 103 | // 第一个参数不是id参数(int类型) 104 | throw new RuntimeException("第一个参数不是id参数类型不支持, class:" + clazz + ",method:" + method); 105 | } else { 106 | for (int i = 0; i < parameterTypes.length; i++) { 107 | if (!RemotingInvokerUtil.checkClassTypeSupport(parameterTypes[i])) { 108 | throw new RuntimeException("不支持的参数类型, class:" + clazz + ",method:" + method + ", param:" 109 | + parameterTypes[i].getSimpleName()); 110 | } 111 | 112 | tmpClassMethodParamTypes.put(uniqueTag, parameterTypes[i]); 113 | } 114 | } 115 | 116 | if (!RemotingInvokerUtil.checkClassTypeSupport(method.getReturnType())) { 117 | throw new RuntimeException( 118 | "不支持的返回值类型, class:" + clazz + ",method:" + method + ", param:" + method.getReturnType() 119 | .getSimpleName()); 120 | } 121 | 122 | tmpClassMethodReturnType.put(uniqueTag, method.getReturnType()); 123 | } 124 | } 125 | 126 | public static Multimap> getClassMethodParamTypes() { 127 | return classMethodParamTypes; 128 | } 129 | 130 | public static Map> getClassMethodReturnType() { 131 | return classMethodReturnType; 132 | } 133 | } -------------------------------------------------------------------------------- /cm4j-hotswap/src/main/java/com/cm4j/invoke/invoker/RemotingParamVO.java: -------------------------------------------------------------------------------- 1 | package com.cm4j.invoke.invoker; 2 | 3 | /** 4 | * Description:远程grpc传输参数 5 | * 6 | * @author yeas.fun 7 | * @date 2022/4/21 8 | */ 9 | public class RemotingParamVO { 10 | 11 | public Object[] params; 12 | 13 | public Object[] getParams() { 14 | return params; 15 | } 16 | 17 | public void setParams(Object[] params) { 18 | this.params = params; 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /cm4j-hotswap/src/main/java/com/cm4j/invoke/proxy/LocalProxyGenerator.java: -------------------------------------------------------------------------------- 1 | package com.cm4j.invoke.proxy; 2 | 3 | import com.cm4j.grpc.client.GrpcClient; 4 | import com.cm4j.grpc.proto.MS_METHOD_GRPC; 5 | import com.cm4j.grpc.proto.MsMethodServiceGrpc; 6 | import com.cm4j.grpc.server.GrpcServer; 7 | import com.cm4j.invoke.IRemotingClass; 8 | import com.cm4j.invoke.RemotingMethod; 9 | import com.cm4j.util.RemotingInvokerUtil; 10 | import com.google.common.collect.Maps; 11 | import io.grpc.StatusRuntimeException; 12 | import net.sf.cglib.proxy.Enhancer; 13 | import net.sf.cglib.proxy.MethodInterceptor; 14 | import org.slf4j.Logger; 15 | import org.slf4j.LoggerFactory; 16 | 17 | import java.util.Map; 18 | 19 | /** 20 | * 本服调用的代理类 21 | * 主要是判断是本地调用还是远程RPC 22 | * 23 | * @author yeas.fun 24 | * @since 2021/8/25 25 | */ 26 | @SuppressWarnings("unchecked") 27 | public class LocalProxyGenerator { 28 | 29 | private static final Logger log = LoggerFactory.getLogger(LocalProxyGenerator.class); 30 | 31 | /** 32 | * 映射关系 33 | */ 34 | private static final Map, IRemotingClass> proxyMap = Maps.newHashMap(); 35 | 36 | /** 37 | * 获取代理类 38 | * 39 | * @param clazz 40 | * @return 41 | */ 42 | public static T getProxy(Class clazz) { 43 | return (T) proxyMap.get(clazz); 44 | } 45 | 46 | /** 47 | * 生成代理类 48 | * 49 | * @param remotingClass 50 | */ 51 | public static void proxy(Class remotingClass) { 52 | proxyMap.put(remotingClass, generateProxy(remotingClass)); 53 | } 54 | 55 | /** 56 | * 生成代理类 57 | * 58 | * @param remotingClass 59 | * @param 60 | * @return 61 | */ 62 | static T generateProxy(Class remotingClass) { 63 | Enhancer enhancer = new Enhancer(); 64 | enhancer.setSuperclass(remotingClass); 65 | 66 | // 注意:这里不捕获异常,这样如果出现异常会直接上抛。 67 | // 外部可统一捕获进行逻辑处理 68 | enhancer.setCallback((MethodInterceptor) (target, method, params, methodProxy) -> { 69 | String methodName = method.getName(); 70 | 71 | // 仅处理代理的方法,其他方法则走正常调用 72 | RemotingMethod annotation = method.getAnnotation(RemotingMethod.class); 73 | if (annotation == null) { 74 | return methodProxy.invokeSuper(target, params); 75 | } 76 | // 非本服,直接远程RPC调用 77 | int sid = Integer.parseInt(String.valueOf(params[0])); 78 | if (sid > 0 && !GrpcServer.isSameServer(sid)) { 79 | return grpc(remotingClass, methodName, params); 80 | } 81 | 82 | // 本服直调,调用热更对象【非调用代理对象】 83 | return RemotingInvokerUtil.invoke(remotingClass, methodName, params); 84 | }); 85 | 86 | return (T) enhancer.create(); 87 | } 88 | 89 | /** 90 | * 发送远程rpc请求 91 | * 92 | * @param clazz 93 | * @param method 94 | * @param params 95 | * @return 96 | */ 97 | public static Object grpc(Class clazz, String method, Object[] params) 98 | throws Exception { 99 | try { 100 | final MS_METHOD_GRPC.MS_METHOD_REQ.Builder req = MS_METHOD_GRPC.MS_METHOD_REQ.newBuilder(); 101 | 102 | req.setClassName(clazz.getName()); 103 | req.setMethodName(method); 104 | req.setParams(RemotingInvokerUtil.encodeParams(params)); 105 | 106 | int sid = Integer.parseInt(String.valueOf(params[0])); 107 | MsMethodServiceGrpc.MsMethodServiceBlockingStub blockingStub = GrpcClient.getBlockingStub0(sid, MsMethodServiceGrpc.class); 108 | MS_METHOD_GRPC.MS_METHOD_RESP resp = blockingStub.invoker(req.build()); 109 | 110 | 111 | if (resp.hasReback()) { 112 | Object[] reback = RemotingInvokerUtil.decodeParams(resp.getReback()); 113 | return reback[0]; 114 | } 115 | } catch (StatusRuntimeException e) { 116 | // 远端异常 117 | log.error("调用远端异常 grpc[{}].[{}] error...", clazz.getName(), method, e); 118 | throw e; 119 | } catch (Exception e) { 120 | log.error("本服异常 grpc[{}].[{}] error...", clazz.getName(), method, e); 121 | // 异常上抛,外层逻辑处理 122 | throw e; 123 | } 124 | return null; 125 | } 126 | 127 | } 128 | -------------------------------------------------------------------------------- /cm4j-hotswap/src/main/java/com/cm4j/lock/InternalLock.java: -------------------------------------------------------------------------------- 1 | package com.cm4j.lock; 2 | 3 | import java.util.concurrent.TimeUnit; 4 | import java.util.concurrent.locks.Lock; 5 | import java.util.concurrent.locks.ReentrantLock; 6 | 7 | import org.slf4j.Logger; 8 | import org.slf4j.LoggerFactory; 9 | 10 | import com.cm4j.util.ThreadUtil; 11 | 12 | /** 13 | * @author yeas.fun 14 | * @since 2021/11/25 15 | */ 16 | public class InternalLock implements AutoCloseable { 17 | 18 | private static final Logger log = LoggerFactory.getLogger(InternalLock.class); 19 | 20 | /** 21 | * 获取锁的超时时间,单位:s 22 | */ 23 | private final int LOCK_TIME_OUT = 5; 24 | 25 | private final Lock lock = new ReentrantLock(); 26 | // 当前持有锁的线程 27 | private Thread holdThread; 28 | 29 | public void tryLock() { 30 | boolean success = false; 31 | try { 32 | success = lock.tryLock(LOCK_TIME_OUT, TimeUnit.SECONDS); 33 | } catch (InterruptedException ignored) { 34 | } 35 | 36 | if (success) { 37 | holdThread = Thread.currentThread(); 38 | } else { 39 | // 堆栈信息 40 | String currentStack = ThreadUtil.getThreadStackTrace(Thread.currentThread()); 41 | String holdStack = ThreadUtil.getThreadStackTrace(holdThread); 42 | 43 | log.error("=========== 获取InternalLock超时,可能发生死锁===========\n【当前线程】堆栈:{}\n{}\n【持有锁线程】堆栈:{}\n{}", 44 | Thread.currentThread(), currentStack, holdThread, holdStack); 45 | throw new RuntimeException("获取InternalLock超时,可能发生死锁"); 46 | } 47 | } 48 | 49 | @Override 50 | public void close() { 51 | holdThread = null; 52 | lock.unlock(); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /cm4j-hotswap/src/main/java/com/cm4j/lock/Locker.java: -------------------------------------------------------------------------------- 1 | package com.cm4j.lock; 2 | 3 | import com.github.benmanes.caffeine.cache.Caffeine; 4 | import com.github.benmanes.caffeine.cache.LoadingCache; 5 | import com.google.common.base.Preconditions; 6 | import com.google.common.base.Throwables; 7 | 8 | import java.util.concurrent.TimeUnit; 9 | 10 | /** 11 | * 替换synchronized锁,一旦出现死锁,会打印死锁线程和当前线程 12 | * 13 | *
14 |  *     // 新写法
15 |  *     Object obj = new Object();
16 |  *     try (InternalLock ignored = Locker.getLock(obj)) {
17 |  *          System.out.println("olai olai ooo...");
18 |  *          LockSupport.parkNanos(TimeUnit.SECONDS.toNanos(6));
19 |  *      }
20 |  *
21 |  *      // 原写法
22 |  *      synchronized (obj) {
23 |  *          System.out.println("olai olai ooo...");
24 |  *      }
25 |  * 
26 | * 27 | * @author yeas.fun 28 | * @since 2021/11/25 29 | */ 30 | public class Locker { 31 | 32 | // 锁对象缓存,仅1min 33 | private static final LoadingCache lockCache = Caffeine.newBuilder() 34 | .expireAfterAccess(1, TimeUnit.MINUTES) 35 | .build(key -> new InternalLock()); 36 | 37 | /** 38 | * 根据对象获取锁 39 | * 对象相同即是同一把锁 40 | * 41 | * @param lockTarget 42 | * @return 43 | */ 44 | public static InternalLock getLock(Object lockTarget) { 45 | Preconditions.checkNotNull(lockTarget, "获取锁不允许null"); 46 | Preconditions.checkArgument(!lockTarget.getClass().isPrimitive(),"对象不允许是primitive"); 47 | 48 | try { 49 | InternalLock lock = lockCache.get(lockTarget); 50 | lock.tryLock(); 51 | return lock; 52 | } catch (Exception e) { 53 | Throwables.throwIfUnchecked(e); 54 | } 55 | // 一般不会走到 56 | return null; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /cm4j-hotswap/src/main/java/com/cm4j/registry/AbstractClassRegistry.java: -------------------------------------------------------------------------------- 1 | package com.cm4j.registry; 2 | 3 | import com.cm4j.registry.registered.IRegistered; 4 | 5 | /** 6 | * 注册容器:类型为对象的class,值为对象本身 7 | * 8 | * @author yeas.fun 9 | * @since 2021/1/7 10 | */ 11 | public abstract class AbstractClassRegistry extends AbstractRegistry { 12 | 13 | /** 14 | * 扫描包进行注册 15 | * 16 | * @param packScan 待扫描的包,有多个包时以","分隔 17 | */ 18 | protected AbstractClassRegistry(String packScan) { 19 | super(packScan); 20 | } 21 | 22 | @Override 23 | protected K[] getRegistryKeys(V v) { 24 | K aClass = (K)v.getClass(); 25 | return (K[])new Object[] {aClass}; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /cm4j-hotswap/src/main/java/com/cm4j/registry/AbstractRegistry.java: -------------------------------------------------------------------------------- 1 | package com.cm4j.registry; 2 | 3 | import java.lang.reflect.Constructor; 4 | import java.lang.reflect.InvocationTargetException; 5 | import java.lang.reflect.Modifier; 6 | import java.lang.reflect.ParameterizedType; 7 | import java.lang.reflect.Type; 8 | import java.text.MessageFormat; 9 | import java.util.Map; 10 | import java.util.Set; 11 | 12 | import org.slf4j.Logger; 13 | import org.slf4j.LoggerFactory; 14 | 15 | import com.cm4j.registry.registered.IRegistered; 16 | import com.cm4j.util.PackageUtil; 17 | import com.google.common.base.Preconditions; 18 | import com.google.common.base.Throwables; 19 | import com.google.common.collect.Maps; 20 | 21 | /** 22 | * 注册容器 23 | * 24 | * @author yeas.fun 25 | * @since 2021/1/7 26 | */ 27 | public abstract class AbstractRegistry implements IHotswapCallback { 28 | 29 | protected static final Logger log = LoggerFactory.getLogger(AbstractRegistry.class); 30 | // private final Class keyType; 31 | private final Class valueType; 32 | 33 | /** 34 | * 扫描包进行注册 35 | * 36 | * @param packScan 待扫描的包,有多个包时以","分隔 37 | */ 38 | protected AbstractRegistry(String packScan) { 39 | // 泛型中的类型 40 | Type[] types = ((ParameterizedType) this.getClass().getGenericSuperclass()).getActualTypeArguments(); 41 | 42 | // keyType = (Class)types[0]; 43 | valueType = (Class) types[1]; 44 | 45 | initRegistry(packScan); 46 | 47 | RegistryManager.getInstance().addRegistry(this); 48 | } 49 | 50 | /** 51 | * 扫描给定包中满足条件的类,初始化 52 | * 53 | * @param packScan 待扫描的包,有多个包时以","分隔 54 | */ 55 | protected void initRegistry(String packScan) { 56 | Set> packageClass = PackageUtil.findPackageClass(packScan); 57 | packageClass.forEach(clazz ->{ 58 | // 排除接口和抽象类 59 | if (clazz.isInterface() || Modifier.isAbstract(clazz.getModifiers())) { 60 | return; 61 | } 62 | initRegistry(clazz); 63 | }); 64 | } 65 | 66 | /** 67 | * 初始化指定的class 68 | * 69 | * @param clazz 70 | */ 71 | protected void initRegistry(Class clazz) { 72 | // 排除接口和抽象类 73 | if (clazz.isInterface() || Modifier.isAbstract(clazz.getModifiers())) { 74 | return; 75 | } 76 | try { 77 | registerClass(clazz); 78 | } catch (Throwable e) { 79 | log.error("newInstance error: {}", clazz.getName(), e); 80 | Throwables.throwIfUnchecked(e); 81 | } 82 | } 83 | 84 | /** 85 | * 初始化指定的class 86 | * 87 | * @param clazz 88 | * @throws IllegalAccessException 89 | * @throws InvocationTargetException 90 | * @throws InstantiationException 91 | */ 92 | private void registerClass(Class clazz) 93 | throws IllegalAccessException, InvocationTargetException, InstantiationException { 94 | // 与注解类型相同,且类不是自己,则进行注册 95 | if (valueType.isAssignableFrom(clazz) && valueType != clazz) { 96 | try { 97 | // 要有默认构造函数 98 | Constructor c0 = clazz.getDeclaredConstructor(); 99 | c0.setAccessible(true); 100 | 101 | V obj = (V) c0.newInstance(); 102 | K[] registryKeys = getRegistryKeys(obj); 103 | for (K registryKey : registryKeys) { 104 | register(registryKey, obj); 105 | } 106 | } catch (NoSuchMethodException e) { 107 | // 没有默认构造函数,忽略该类,不注册 108 | } 109 | } 110 | } 111 | 112 | /** 113 | * 基于对象的注册,有可能多个key对应同一个对象 114 | * 115 | * @param v 116 | * @return 117 | */ 118 | protected abstract K[] getRegistryKeys(V v); 119 | 120 | /** 映射关系 */ 121 | protected final Map mapping = Maps.newConcurrentMap(); 122 | 123 | /** 124 | * 获取注册对象 125 | * 126 | * @param k 127 | * @return 128 | */ 129 | public V get(K k) { 130 | return mapping.get(k); 131 | } 132 | 133 | /** 134 | * 注册 135 | * 136 | * @param k 137 | * @param v 138 | */ 139 | protected void register(K k, V v) { 140 | Preconditions.checkNotNull(v, "RegistryManager is null"); 141 | V old = mapping.get(k); 142 | if (old != null) { 143 | throw new RuntimeException(MessageFormat.format("target is already existed:[{0}] -> [{1}]", k, v)); 144 | } 145 | mapping.put(k, v); 146 | doAfterRegister(k, v); 147 | if (log.isDebugEnabled()) { 148 | log.debug("[register] success:{} -> {}", k, v); 149 | } 150 | } 151 | 152 | /** 153 | * 热更:替换对应的映射对象 154 | * 155 | * @param originClass 原始的class 156 | * @param newObject 新构建的热更对象,可能是SUBCLASS 157 | * @return true-热更成功,false-热更失败(类型不匹配,代码问题等) 158 | */ 159 | public boolean hotswap(Class originClass, V newObject) 160 | throws NoSuchFieldException, IllegalAccessException, InstantiationException { 161 | // 值的类型不匹配,不是该注册类 162 | if (!valueType.isAssignableFrom(originClass)) { 163 | return false; 164 | } 165 | K[] registryKeys = getRegistryKeys(originClass.newInstance()); 166 | K registryKey = registryKeys[0]; 167 | V oldObject = get(registryKey); 168 | 169 | // 原来注册过的对象,才允许进行热更替换 170 | if (oldObject == null) { 171 | throw new RuntimeException("[hotswap] failed, cannot found oldObject:" + registryKey); 172 | } 173 | 174 | for (K k : registryKeys) { 175 | mapping.put(k, newObject); 176 | doAfterHotswap(k, oldObject, newObject); 177 | log.error("[hotswap] success:{} -> {}", k, newObject); 178 | } 179 | return true; 180 | } 181 | 182 | /** 183 | * 新注册对象 184 | * 185 | * @param v 186 | * @return true-注册成功,false-注册失败 187 | */ 188 | public boolean registerNewOne(V v) { 189 | Class originClass = v.getClass(); 190 | // 值的类型不匹配,不是该注册类 191 | if (!valueType.isAssignableFrom(originClass)) { 192 | return false; 193 | } 194 | 195 | K[] registryKeys = getRegistryKeys(v); 196 | for (K registryKey : registryKeys) { 197 | V existed = get(registryKey); 198 | 199 | // 已经有了,则无法新增对象,只能走hotswap 200 | if (existed != null) { 201 | throw new RuntimeException("registry is existed: " + registryKey); 202 | } 203 | } 204 | 205 | for (K registryKey : registryKeys) { 206 | mapping.put(registryKey, v); 207 | doAfterRegisterNewOne(registryKey, v); 208 | log.error("[registry NEW] success:{} -> {}", registryKey, v); 209 | } 210 | 211 | return true; 212 | } 213 | 214 | } 215 | -------------------------------------------------------------------------------- /cm4j-hotswap/src/main/java/com/cm4j/registry/IHotswapCallback.java: -------------------------------------------------------------------------------- 1 | package com.cm4j.registry; 2 | 3 | /** 4 | * 热更后回调 5 | * 6 | * @author yeas.fun 7 | * @since 2021/3/17 8 | */ 9 | public interface IHotswapCallback { 10 | 11 | /** 12 | * 初始化注册:回调 13 | * 14 | * @param registryKey 15 | * @param newObject 16 | */ 17 | default void doAfterRegister(K registryKey, V newObject){} 18 | 19 | /** 20 | * 热更:替换旧回调 21 | * 22 | * @param registryKey registry键 23 | * @param oldObject 旧对象 24 | * @param newObject 新对象 25 | */ 26 | default void doAfterHotswap(K registryKey, V oldObject, V newObject){} 27 | 28 | /** 29 | * 热更:新增对象后回调 30 | * 31 | * @param registryKey 32 | * @param newObject 33 | */ 34 | default void doAfterRegisterNewOne(K registryKey, V newObject){} 35 | } 36 | -------------------------------------------------------------------------------- /cm4j-hotswap/src/main/java/com/cm4j/registry/RegistryManager.java: -------------------------------------------------------------------------------- 1 | package com.cm4j.registry; 2 | 3 | import java.util.Map; 4 | import java.util.Set; 5 | 6 | import com.cm4j.hotswap.recompile.RecompileHotSwap; 7 | import com.cm4j.registry.registered.IRegistered; 8 | import com.google.common.collect.Maps; 9 | import com.google.common.collect.Sets; 10 | 11 | /** 12 | * 所有的注册容器管理类 13 | * 14 | * @author yeas.fun 15 | * @since 2021/1/9 16 | */ 17 | public class RegistryManager { 18 | 19 | private Set allRegistry = Sets.newConcurrentHashSet(); 20 | 21 | public void addRegistry(AbstractRegistry registry) { 22 | allRegistry.add(registry); 23 | } 24 | 25 | public Set getAllRegistry() { 26 | return allRegistry; 27 | } 28 | 29 | /** 30 | * 热更注册系统内的对象 31 | * 32 | * @param originClassNames 33 | * @throws Exception 34 | */ 35 | public void hotswap(String[] originClassNames) throws Exception { 36 | Map, IRegistered> map = Maps.newLinkedHashMap(); 37 | 38 | for (String originClassName : originClassNames) { 39 | Class originClass = Class.forName(originClassName); 40 | if (!IRegistered.class.isAssignableFrom(originClass)) { 41 | throw new RuntimeException("class is not IRegistered:" + originClassName); 42 | } 43 | 44 | Class newClass = RecompileHotSwap.recompileClass(originClass); 45 | IRegistered newObj = (IRegistered) newClass.newInstance(); 46 | 47 | map.put(originClass, newObj); 48 | } 49 | 50 | // 全部都成功了,再一个个进行热更替换 51 | 52 | // 注册对象替换 53 | map.forEach((originClass, newObj) -> { 54 | for (AbstractRegistry registry : RegistryManager.getInstance().getAllRegistry()) { 55 | try { 56 | registry.hotswap(originClass, newObj); 57 | } catch (Exception e) { 58 | throw new RuntimeException(e); 59 | } 60 | } 61 | }); 62 | } 63 | 64 | /** 65 | * 动态注册新对象 66 | * 67 | * @param originClassNames 68 | */ 69 | public void registerNewOne(String[] originClassNames) throws Exception { 70 | Set objects = Sets.newHashSet(); 71 | for (String originClassName : originClassNames) { 72 | Class originClass = Class.forName(originClassName); 73 | if (!IRegistered.class.isAssignableFrom(originClass)) { 74 | throw new RuntimeException("class is not IRegistered:" + originClassName); 75 | } 76 | 77 | objects.add((IRegistered) originClass.newInstance()); 78 | } 79 | 80 | for (IRegistered target : objects) { 81 | for (AbstractRegistry registry : RegistryManager.getInstance().getAllRegistry()) { 82 | try { 83 | registry.registerNewOne(target); 84 | } catch (Exception e) { 85 | throw new RuntimeException(e); 86 | } 87 | } 88 | } 89 | } 90 | 91 | private static class HOLDER { 92 | 93 | private static final RegistryManager instance = new RegistryManager(); 94 | } 95 | 96 | public static RegistryManager getInstance() { 97 | return HOLDER.instance; 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /cm4j-hotswap/src/main/java/com/cm4j/registry/registered/IRegistered.java: -------------------------------------------------------------------------------- 1 | package com.cm4j.registry.registered; 2 | 3 | /** 4 | * 被注册对象,基于此接口来进行对象扫描 5 | * 6 | * @author yeas.fun 7 | * @since 2021/1/7 8 | */ 9 | public interface IRegistered { 10 | 11 | } -------------------------------------------------------------------------------- /cm4j-hotswap/src/main/java/com/cm4j/registry/registry/InvokerRegistry.java: -------------------------------------------------------------------------------- 1 | package com.cm4j.registry.registry; 2 | 3 | import com.cm4j.invoke.IRemotingClass; 4 | import com.cm4j.registry.AbstractRegistry; 5 | import com.cm4j.util.RemotingInvokerUtil; 6 | import com.esotericsoftware.reflectasm.MethodAccess; 7 | 8 | /** 9 | * 条件注册 10 | * 11 | * @author yeas.fun 12 | * @since 2021/1/12 13 | */ 14 | public class InvokerRegistry extends AbstractRegistry { 15 | 16 | /** 17 | * 扫描包进行注册 18 | */ 19 | protected InvokerRegistry() { 20 | super(IRemotingClass.class.getPackage().getName()); 21 | instance = this; 22 | } 23 | 24 | @Override 25 | public void doAfterRegister(String registryKey, IRemotingClass newObject) { 26 | // 注册新的MethodAccess 27 | MethodAccess access = MethodAccess.get(newObject.getClass());//生成字节码的方式 28 | RemotingInvokerUtil.addInvoker(registryKey, access); 29 | } 30 | 31 | @Override 32 | public void doAfterHotswap(String registryKey, IRemotingClass oldObject, IRemotingClass newObject) { 33 | // 这里不要多调,会产生新的类无法被GC 34 | // 功能依然正常:reflectasm 是基于方法名字和参数来获取索引,从而调用target对应索引的方法,所以功能正常 35 | // doAfterRegister(registryKey, newObject); 36 | } 37 | 38 | @Override 39 | public void doAfterRegisterNewOne(String registryKey, IRemotingClass newObject) { 40 | // 注册新的MethodAccess 41 | doAfterRegister(registryKey, newObject); 42 | } 43 | 44 | private static InvokerRegistry instance; 45 | 46 | public static InvokerRegistry getInstance() { 47 | return instance; 48 | } 49 | 50 | @Override 51 | protected String[] getRegistryKeys(IRemotingClass iRemotingClass) { 52 | return new String[]{iRemotingClass.getClass().getName()}; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /cm4j-hotswap/src/main/java/com/cm4j/singleton/FutureResult.java: -------------------------------------------------------------------------------- 1 | package com.cm4j.singleton; 2 | 3 | /** 4 | * @author yeas.fun 5 | * @since 2019/9/20 6 | */ 7 | public class FutureResult { 8 | 9 | private RuntimeException exception; 10 | 11 | private final V result; 12 | 13 | public FutureResult(V result) { 14 | this.exception = null; 15 | this.result = result; 16 | } 17 | 18 | public static FutureResult newFutureResultWithException(Exception exception) { 19 | FutureResult result = new FutureResult<>(null); 20 | // 是RuntimeException,则直接返回,否则封装下 21 | if (exception instanceof RuntimeException) { 22 | result.exception = (RuntimeException)exception; 23 | } else { 24 | result.exception = new RuntimeException(exception); 25 | } 26 | return result; 27 | } 28 | 29 | /** 30 | * 是否正常返回【无异常】 31 | * 32 | * @return 33 | */ 34 | public boolean isSuccess() { 35 | return this.exception == null; 36 | } 37 | 38 | public RuntimeException getException() { 39 | return exception; 40 | } 41 | 42 | public V getResult() { 43 | return result; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /cm4j-hotswap/src/main/java/com/cm4j/singleton/FutureSupport.java: -------------------------------------------------------------------------------- 1 | package com.cm4j.singleton; 2 | 3 | import com.google.common.util.concurrent.ListenableFutureTask; 4 | import org.slf4j.Logger; 5 | import org.slf4j.LoggerFactory; 6 | 7 | import java.util.concurrent.Future; 8 | import java.util.concurrent.TimeUnit; 9 | 10 | /** 11 | * future 辅助类 12 | * 13 | * @author yeas.fun 14 | * @since 2019/9/18 15 | */ 16 | public class FutureSupport { 17 | 18 | private static final Logger log = LoggerFactory.getLogger(FutureSupport.class); 19 | 20 | // 默认超时:5s 21 | public static final int DEFAULT_TIMEOUT = 5; 22 | 23 | /** 24 | * 同步等待获取future数据 注意:如果callable内部发生异常,则get的时候会捕获到该异常,继续上抛 ---- 比较有用的就是上抛错误码异常 25 | * 26 | * @param futureWrapper 27 | * @param 28 | * @return 29 | */ 30 | public static V get(FutureWrapper futureWrapper) { 31 | ListenableFutureTask> futureTask = futureWrapper.getFutureTask(); 32 | FutureResult futureResult = get(futureTask, DEFAULT_TIMEOUT, TimeUnit.SECONDS); 33 | // 发生异常了,则继续上抛异常 34 | boolean success = futureResult.isSuccess(); 35 | if (!success) { 36 | RuntimeException ex = futureResult.getException(); 37 | log.error("future result exception", ex); 38 | throw ex; 39 | } 40 | return futureResult.getResult(); 41 | } 42 | 43 | /** 44 | * 同步等待获取future数据 45 | * 46 | * @param future 47 | * @param timeout 48 | * @param timeUnit 49 | * @param 50 | * @return 51 | */ 52 | private static V get(Future future, long timeout, TimeUnit timeUnit) { 53 | try { 54 | if (future.isCancelled()) { 55 | return (V) FutureResult.newFutureResultWithException(new RuntimeException("future cancelled")); 56 | } 57 | return future.get(timeout, timeUnit); 58 | } catch (Exception e) { 59 | log.error("future get error", e); 60 | throw new RuntimeException(e); 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /cm4j-hotswap/src/main/java/com/cm4j/singleton/FutureWrapper.java: -------------------------------------------------------------------------------- 1 | package com.cm4j.singleton; 2 | 3 | import com.google.common.util.concurrent.ListenableFutureTask; 4 | 5 | /** 6 | * 外部不需要直接调用到futureTask。必须通过 FutureSupport.get(futureWrapper) 获取值 7 | * 8 | * @author yeas.fun 9 | * @since 2022/2/24 10 | */ 11 | public class FutureWrapper { 12 | 13 | private final ListenableFutureTask> futureTask; 14 | 15 | public FutureWrapper(ListenableFutureTask> futureTask) { 16 | this.futureTask = futureTask; 17 | } 18 | 19 | /** 20 | * 同步等待获取结果,这里会走FutureSupport,默认等待5s 21 | * 22 | * @return 23 | */ 24 | public V getResult() { 25 | return FutureSupport.get(this); 26 | } 27 | 28 | ListenableFutureTask> getFutureTask() { 29 | return futureTask; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /cm4j-hotswap/src/main/java/com/cm4j/singleton/SingletonEnum.java: -------------------------------------------------------------------------------- 1 | package com.cm4j.singleton; 2 | 3 | /** 4 | * @author yeas.fun 5 | * @since 2019/10/11 6 | */ 7 | public enum SingletonEnum { 8 | 9 | /** 10 | * 测试业务 11 | */ 12 | TEST_BUSINESS; 13 | } 14 | -------------------------------------------------------------------------------- /cm4j-hotswap/src/main/java/com/cm4j/singleton/SingletonModule.java: -------------------------------------------------------------------------------- 1 | package com.cm4j.singleton; 2 | 3 | import com.cm4j.thread.ThreadPoolName; 4 | import org.apache.commons.lang3.StringUtils; 5 | 6 | import java.util.HashMap; 7 | import java.util.Map; 8 | import java.util.concurrent.Callable; 9 | 10 | /** 11 | * 单线程执行模块 12 | * 13 | * @author yeas.fun 14 | * @since 2019/10/10 15 | */ 16 | public abstract class SingletonModule { 17 | 18 | /** 必须是2的n次方,因为后面用它hash定位 */ 19 | private final int SIZE = 1 << 8; 20 | 21 | private final SingletonTaskQueue[] queues; 22 | 23 | protected SingletonModule(ThreadPoolName threadPoolName) { 24 | queues = new SingletonTaskQueue[SIZE]; 25 | for (int i = 0; i < SIZE; i++) { 26 | queues[i] = new SingletonTaskQueue(i, threadPoolName); 27 | } 28 | } 29 | 30 | /** 31 | * 添加任务 框架保证: 32 | * 33 | *
 34 |      *     1.function 作为唯一键,该任务仅有1个线程会执行,避免并发和加锁
 35 |      *     2.callable如果抛异常了,则同步方法FutureSupport.get()也会上抛同样的异常,一般用于直接返回错误码
 36 |      * 
37 | * 38 | * 注意:因为是单线程执行,所以不要把执行特别耗时的任务放进来,否则会卡住所有任务 39 | * 40 | * @param function 功能枚举 41 | * @param callable 执行逻辑 42 | * @param 返回值 43 | * @return future 44 | */ 45 | public FutureWrapper addTask(SingletonEnum function, Callable callable) { 46 | return addTask(function.name(), callable); 47 | } 48 | 49 | /** 50 | * 添加任务 框架保证: 51 | * 52 | *
 53 |      *     1.function+uniqueId 作为唯一键,同一键同时仅有1个线程会执行,避免并发和加锁
 54 |      *     2.callable如果抛异常了,则同步方法FutureSupport.get()也会上抛同样的异常,一般用于直接返回错误码
 55 |      * 
56 | * 57 | * 注意:因为是单线程执行,所以不要把执行特别耗时的任务放进来,否则会卡住所有任务 58 | * 59 | * @param function 功能枚举 60 | * @param uniqueId 唯一标识ID 61 | * @param callable 执行逻辑 62 | * @param 返回值 63 | * @return future 64 | */ 65 | public FutureWrapper addTask(SingletonEnum function, String uniqueId, 66 | Callable callable) { 67 | uniqueId = StringUtils.isBlank(uniqueId) ? "" : uniqueId; 68 | return addTask(function.name() + uniqueId, callable); 69 | } 70 | 71 | /** 72 | * 添加任务执行 73 | * 74 | * @param 75 | * @param hash 76 | * @param callable 77 | * @return 78 | */ 79 | private FutureWrapper addTask(String hash, Callable callable) { 80 | SingletonTask task = new SingletonTask<>(hash, callable); 81 | int rehash = rehash(task.getHash().hashCode()); 82 | 83 | int idx = rehash & (SIZE - 1); 84 | SingletonTaskQueue queue = queues[idx]; 85 | // 增加到对应的队列 86 | queue.addTask(task); 87 | // 每个事件,都有对应的future 88 | return new FutureWrapper(task.getFuture()); 89 | } 90 | 91 | /** 92 | * 队列任务数量 93 | * 94 | * @return 95 | */ 96 | public Map getTaskNum() { 97 | SingletonTaskQueue[] dealers = this.queues; 98 | Map result = new HashMap<>(dealers.length); 99 | for (int i = 0; i < dealers.length; i++) { 100 | SingletonTaskQueue dealer = dealers[i]; 101 | result.put(dealer.getIdx(), dealer.getQueueSize()); 102 | } 103 | return result; 104 | } 105 | 106 | /** 107 | * rehash算法,使hash值分布更均匀 108 | * 109 | * @param h 110 | * @return 111 | */ 112 | private static int rehash(int h) { 113 | h += (h << 15) ^ 0xffffcd7d; 114 | h ^= (h >>> 10); 115 | h += (h << 3); 116 | h ^= (h >>> 6); 117 | h += (h << 2) + (h << 14); 118 | return h ^ (h >>> 16); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /cm4j-hotswap/src/main/java/com/cm4j/singleton/SingletonTask.java: -------------------------------------------------------------------------------- 1 | package com.cm4j.singleton; 2 | 3 | import com.google.common.util.concurrent.ListenableFutureTask; 4 | import org.slf4j.Logger; 5 | import org.slf4j.LoggerFactory; 6 | 7 | import java.util.concurrent.Callable; 8 | 9 | /** 10 | * 任务封装类 11 | * 12 | * @author yeas.fun 13 | * @since 2019/10/10 14 | */ 15 | public class SingletonTask { 16 | 17 | private static final Logger log = LoggerFactory.getLogger(SingletonTask.class); 18 | 19 | private final String hash; 20 | private final ListenableFutureTask> future; 21 | 22 | public SingletonTask(String hash, Callable callable) { 23 | this.hash = hash; 24 | this.future = ListenableFutureTask.create(() -> { 25 | try { 26 | return new FutureResult<>(callable.call()); 27 | } catch (Exception e) { 28 | // 非错误码异常:则异常上报 29 | log.error("SingletonTask error", e); 30 | return FutureResult.newFutureResultWithException(e); 31 | } 32 | }); 33 | } 34 | 35 | /** 36 | * 加入队列已满 37 | */ 38 | public void onAddQueueFailed() { 39 | // 中止future执行 40 | this.future.cancel(true); 41 | } 42 | 43 | public String getHash() { 44 | return hash; 45 | } 46 | 47 | /** 48 | * 外部调用,有则返回,没有则为null 49 | * 50 | */ 51 | public ListenableFutureTask> getFuture() { 52 | return this.future; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /cm4j-hotswap/src/main/java/com/cm4j/singleton/SingletonTaskQueue.java: -------------------------------------------------------------------------------- 1 | package com.cm4j.singleton; 2 | 3 | import com.cm4j.thread.ThreadPoolName; 4 | import com.cm4j.thread.ThreadPoolService; 5 | import com.google.common.collect.Queues; 6 | import com.google.common.util.concurrent.ListenableFutureTask; 7 | import org.slf4j.Logger; 8 | import org.slf4j.LoggerFactory; 9 | 10 | import java.util.concurrent.LinkedBlockingQueue; 11 | import java.util.concurrent.atomic.AtomicBoolean; 12 | 13 | /** 14 | * @author yeas.fun 15 | * @since 2019/10/10 16 | */ 17 | public class SingletonTaskQueue implements Runnable { 18 | 19 | private static final Logger LOG = LoggerFactory.getLogger(SingletonTaskQueue.class); 20 | 21 | private final int idx; 22 | private final ThreadPoolName threadPoolName; 23 | /** 24 | * 状态标识位,同一个线程为true时才可执行 25 | */ 26 | private final AtomicBoolean isDealing; 27 | /** 28 | * 队列 29 | */ 30 | private final LinkedBlockingQueue queue; 31 | 32 | SingletonTaskQueue(int idx, ThreadPoolName threadPoolName) { 33 | this.idx = idx; 34 | this.threadPoolName = threadPoolName; 35 | this.queue = Queues.newLinkedBlockingQueue(2048); 36 | isDealing = new AtomicBoolean(false); 37 | } 38 | 39 | public void addTask(SingletonTask task) { 40 | boolean success = queue.offer(task); 41 | // 添加队列失败 42 | if (!success) { 43 | task.onAddQueueFailed(); 44 | return; 45 | } 46 | if (executable()) { 47 | ThreadPoolService.getInstance().runTask(this, threadPoolName); 48 | } 49 | } 50 | 51 | private boolean executable() { 52 | return isDealing.compareAndSet(false, true); 53 | } 54 | 55 | private void execute() { 56 | try { 57 | while (true) { 58 | try { 59 | SingletonTask task = this.queue.poll(); 60 | 61 | // 没有对象,则结束循环 62 | if (task == null) { 63 | // 并发问题:如果有断点在这里 64 | // 另一个线程把event放入队列中,且因为没有获取到锁,则快速失败,当前线程也break了,则有event无法消耗 65 | // 所以:在finally段队列二次检查 66 | break; 67 | } 68 | 69 | if (LOG.isWarnEnabled()) { 70 | LOG.warn("Singleton task[{}] triggered", task.getHash()); 71 | } 72 | ListenableFutureTask future = task.getFuture(); 73 | // 同步执行 74 | future.run(); 75 | } catch (Exception e) { 76 | LOG.error("SingletonTaskQueue[{}] error", SingletonTaskQueue.this.idx, e); 77 | } 78 | } 79 | } finally { 80 | // 最后一定要把标识位修改为false 81 | this.isDealing.set(false); 82 | 83 | // 因为上面已经已经把状态isDealing重置了,如果队列里有对象,则继续放到线程池执行 84 | if (!this.queue.isEmpty() && executable()) { 85 | execute(); 86 | } 87 | } 88 | } 89 | 90 | @Override 91 | public void run() { 92 | execute(); 93 | } 94 | 95 | public int getQueueSize() { 96 | return queue.size(); 97 | } 98 | 99 | public int getIdx() { 100 | return idx; 101 | } 102 | 103 | } 104 | -------------------------------------------------------------------------------- /cm4j-hotswap/src/main/java/com/cm4j/singleton/impl/NormalSingletonModule.java: -------------------------------------------------------------------------------- 1 | package com.cm4j.singleton.impl; 2 | 3 | import com.cm4j.singleton.SingletonModule; 4 | import com.cm4j.thread.ThreadPoolName; 5 | 6 | /** 7 | * 常规业务使用的单线程 8 | * 9 | * @author yeas.fun 10 | * @since 2021/7/19 11 | */ 12 | public class NormalSingletonModule extends SingletonModule { 13 | 14 | private NormalSingletonModule() { 15 | super(ThreadPoolName.SINGLETON); 16 | } 17 | 18 | private static class HOLDER { 19 | private static final SingletonModule instance = new NormalSingletonModule(); 20 | } 21 | 22 | public static SingletonModule getInstance() { 23 | return HOLDER.instance; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /cm4j-hotswap/src/main/java/com/cm4j/thread/DefaultThreadFactory.java: -------------------------------------------------------------------------------- 1 | package com.cm4j.thread; 2 | 3 | import java.util.concurrent.ThreadFactory; 4 | import java.util.concurrent.atomic.AtomicInteger; 5 | 6 | /** 7 | * DefaultThreadFactory 8 | * 9 | * @author yeas.fun 10 | * @since 2020-09-25 11 | */ 12 | public class DefaultThreadFactory implements ThreadFactory { 13 | 14 | private static final AtomicInteger POOL_SEQ = new AtomicInteger(); 15 | private static final AtomicInteger THREAD_SEQ = new AtomicInteger(); 16 | private static final String DEFAULT_POOL_NAME = "pool"; 17 | private static final String DEFAULT_THREAD_NAME = "thread"; 18 | private final ThreadGroup group; 19 | private final String namePrefix; 20 | private final boolean daemon; 21 | private final int priority; 22 | 23 | private DefaultThreadFactory(String poolName, String threadName, boolean daemon, int priority) { 24 | SecurityManager s = System.getSecurityManager(); 25 | this.group = (s != null) ? s.getThreadGroup() : Thread.currentThread().getThreadGroup(); 26 | this.namePrefix = poolName + "-" + POOL_SEQ.incrementAndGet() + "-" + threadName + "-"; 27 | this.daemon = daemon; 28 | this.priority = priority; 29 | } 30 | 31 | /** 32 | * 创建线程工厂 33 | * 34 | * @param poolName 线程池名称 35 | * @param threadName 线程名称前缀 36 | * @param daemon 是否是守护线程 37 | * @param priority 线程优先级 38 | * @return poolName-threadName-N 39 | */ 40 | public static ThreadFactory threadFactory(String poolName, String threadName, boolean daemon, int priority) { 41 | return new DefaultThreadFactory(poolName, threadName, daemon, priority); 42 | } 43 | 44 | /** 45 | * @param poolName 46 | * @param threadName 47 | * @return poolName-threadName-N 48 | */ 49 | public static ThreadFactory threadFactory(String poolName, String threadName) { 50 | return threadFactory(poolName, threadName, false, Thread.NORM_PRIORITY); 51 | } 52 | 53 | /** 54 | * @param poolName 55 | * @return poolName-thread-N 56 | */ 57 | public static ThreadFactory threadFactory(String poolName) { 58 | return threadFactory(poolName, DEFAULT_THREAD_NAME, false, Thread.NORM_PRIORITY); 59 | } 60 | 61 | /** 62 | * @param poolName 63 | * @param daemon 64 | * @return poolName-thread-N 65 | */ 66 | public static ThreadFactory threadFactory(String poolName, boolean daemon) { 67 | return threadFactory(poolName, DEFAULT_THREAD_NAME, daemon, Thread.NORM_PRIORITY); 68 | } 69 | 70 | public static ThreadFactory threadFactory(String poolName, int priority) { 71 | return threadFactory(poolName, DEFAULT_THREAD_NAME, false, priority); 72 | } 73 | 74 | /** 75 | * @return pool-thread-N 76 | */ 77 | public static ThreadFactory threadFactory() { 78 | return threadFactory(DEFAULT_POOL_NAME); 79 | } 80 | 81 | /** 82 | * @param daemon 83 | * @return pool-thread-N 84 | */ 85 | public static ThreadFactory threadFactory(boolean daemon) { 86 | return threadFactory(DEFAULT_POOL_NAME, daemon); 87 | } 88 | 89 | @Override 90 | public Thread newThread(Runnable r) { 91 | Thread t = new Thread(group, r, namePrefix + THREAD_SEQ.incrementAndGet(), 0); 92 | t.setDaemon(daemon); 93 | if (t.getPriority() != priority) { 94 | t.setPriority(priority); 95 | } 96 | return t; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /cm4j-hotswap/src/main/java/com/cm4j/thread/ThreadPoolConfig.java: -------------------------------------------------------------------------------- 1 | package com.cm4j.thread; 2 | 3 | /** 4 | * 线程池配置 5 | * 6 | * @author yeas.fun 7 | * @version 1.0 8 | */ 9 | public class ThreadPoolConfig { 10 | 11 | /** 12 | * 线程池名称 13 | */ 14 | private String poolName; 15 | /** 16 | * 最大线程数目 17 | */ 18 | private int maxThreads; 19 | /** 20 | * 最小线程数目 21 | */ 22 | private int minThreads; 23 | /** 24 | * 线程存活时间(秒) 25 | */ 26 | private long keepAlive; 27 | /** 28 | * 队列类型 1:LinkedBlockingQueue 2:SynchronousQueue (暂时为1) 29 | */ 30 | private int queueType; 31 | /** 32 | * 队列最大数 (空为Integer.MAX_VALUE)(最好是有值否则会出现溢出) 33 | */ 34 | private int maxQueues; 35 | /** 36 | * 优先级 37 | */ 38 | private int priority; 39 | /** 40 | * 拒绝策略 41 | */ 42 | private ThreadPoolRejectedPolicy.RejectedPolicy policy; 43 | 44 | public int getMaxQueues() { 45 | return maxQueues; 46 | } 47 | 48 | public void setMaxQueues(int maxQueues) { 49 | this.maxQueues = maxQueues; 50 | } 51 | 52 | public int getMaxThreads() { 53 | return maxThreads; 54 | } 55 | 56 | public void setMaxThreads(int maxThreads) { 57 | this.maxThreads = maxThreads; 58 | } 59 | 60 | public int getMinThreads() { 61 | return minThreads; 62 | } 63 | 64 | public void setMinThreads(int minThreads) { 65 | this.minThreads = minThreads; 66 | } 67 | 68 | public String getPoolName() { 69 | return poolName; 70 | } 71 | 72 | public void setPoolName(String poolName) { 73 | this.poolName = poolName; 74 | } 75 | 76 | public int getPriority() { 77 | return priority; 78 | } 79 | 80 | public void setPriority(int priority) { 81 | this.priority = priority; 82 | } 83 | 84 | public long getKeepAlive() { 85 | return keepAlive; 86 | } 87 | 88 | public void setKeepAlive(long keepAlive) { 89 | this.keepAlive = keepAlive; 90 | } 91 | 92 | public int getQueueType() { 93 | return queueType; 94 | } 95 | 96 | public void setQueueType(int queueType) { 97 | this.queueType = queueType; 98 | } 99 | 100 | public ThreadPoolRejectedPolicy.RejectedPolicy getPolicy() { 101 | return policy; 102 | } 103 | 104 | public void setPolicy(ThreadPoolRejectedPolicy.RejectedPolicy policy) { 105 | this.policy = policy; 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /cm4j-hotswap/src/main/java/com/cm4j/thread/ThreadPoolFactory.java: -------------------------------------------------------------------------------- 1 | package com.cm4j.thread; 2 | 3 | import java.util.concurrent.*; 4 | 5 | /** 6 | * 线程池创建工厂 7 | * 8 | * @author yeas.fun 9 | * @since 2021-05-14 10 | */ 11 | public class ThreadPoolFactory { 12 | 13 | public static ExecutorService newCachedThreadPool(String poolName) { 14 | return new ThreadPoolExecutor(0, 4096, 60L, TimeUnit.SECONDS, new SynchronousQueue<>(), 15 | DefaultThreadFactory.threadFactory(poolName)); 16 | } 17 | 18 | public static ExecutorService newSingleThreadExecutor(String poolName) { 19 | return new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>(), 20 | DefaultThreadFactory.threadFactory(poolName)); 21 | } 22 | 23 | public static ScheduledExecutorService newSingleThreadScheduledExecutor(String poolName) { 24 | return newScheduledThreadPool(1, poolName); 25 | } 26 | 27 | public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize, String poolName) { 28 | return new ScheduledThreadPoolExecutor(corePoolSize, DefaultThreadFactory.threadFactory(poolName)); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /cm4j-hotswap/src/main/java/com/cm4j/thread/ThreadPoolName.java: -------------------------------------------------------------------------------- 1 | package com.cm4j.thread; 2 | 3 | /** 4 | * @author yeas.fun 5 | * @since 2017/1/11 6 | */ 7 | public enum ThreadPoolName { 8 | /** 通用 */ 9 | COMMON, 10 | /** 单线程执行的任务 */ 11 | SINGLETON, 12 | } 13 | -------------------------------------------------------------------------------- /cm4j-hotswap/src/main/java/com/cm4j/thread/ThreadPoolRejectedPolicy.java: -------------------------------------------------------------------------------- 1 | package com.cm4j.thread; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | 6 | import java.util.concurrent.RejectedExecutionHandler; 7 | import java.util.concurrent.ThreadPoolExecutor; 8 | import java.util.concurrent.atomic.AtomicInteger; 9 | 10 | /** 11 | * 线程池拒绝策略 12 | *
 13 |  *     前10次触发拒绝都上报
 14 |  *     以后每100次上报一次
 15 |  * 
16 | * 17 | * @author yeas.fun 18 | * @since 2021-08-18 19 | */ 20 | public final class ThreadPoolRejectedPolicy implements RejectedExecutionHandler { 21 | 22 | private static final Logger log = LoggerFactory.getLogger(ThreadPoolRejectedPolicy.class); 23 | /** 24 | * 回车换行 25 | */ 26 | public static final String CRLF = "\r\n"; 27 | /** 28 | * 线程池名称 29 | */ 30 | private final String threadPoolName; 31 | /** 32 | * 被拒绝计数器 33 | */ 34 | private final AtomicInteger rejectedCounter; 35 | /** 36 | * 拒绝策略 37 | */ 38 | private final RejectedExecutionHandler handler; 39 | private final RejectedPolicy policy; 40 | 41 | public ThreadPoolRejectedPolicy(String threadPoolName, RejectedPolicy policy) { 42 | this.threadPoolName = threadPoolName; 43 | this.rejectedCounter = new AtomicInteger(); 44 | this.policy = policy; 45 | this.handler = createPolicy(policy); 46 | } 47 | 48 | public ThreadPoolRejectedPolicy(ThreadPoolName name, RejectedPolicy policy) { 49 | this(name.name(), policy); 50 | } 51 | 52 | /** 53 | * 根据拒绝策略创建不同的拒绝处理器 54 | *
 55 |      *     默认策略:CallerRunsPolicy
 56 |      * 
57 | * 58 | * @param policy 59 | * @return 60 | */ 61 | private RejectedExecutionHandler createPolicy(RejectedPolicy policy) { 62 | if (policy == RejectedPolicy.AbortPolicy) { 63 | return new ThreadPoolExecutor.AbortPolicy(); 64 | } 65 | if (policy == RejectedPolicy.DiscardPolicy) { 66 | return new ThreadPoolExecutor.DiscardPolicy(); 67 | } 68 | if (policy == RejectedPolicy.DiscardOldestPolicy) { 69 | return new ThreadPoolExecutor.DiscardOldestPolicy(); 70 | } 71 | return new ThreadPoolExecutor.CallerRunsPolicy(); 72 | } 73 | 74 | @Override 75 | public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) { 76 | int rejected = rejectedCounter.incrementAndGet(); 77 | if (needReport(rejected)) { 78 | reportRejected(r, executor, rejected); 79 | } 80 | handler.rejectedExecution(r, executor); 81 | } 82 | 83 | /** 84 | * 判断是否需要上报拒绝信息 85 | *
 86 |      *     前10次拒绝都上报
 87 |      *     以后每100次上报一次
 88 |      * 
89 | * 90 | * @param rejected 已触发的拒绝次数 91 | * @return 92 | */ 93 | private boolean needReport(int rejected) { 94 | return rejected <= 10 || rejected % 100 == 0; 95 | } 96 | 97 | /** 98 | * 上报拒绝信息 99 | * 100 | * @param r 101 | * @param executor 102 | * @param rejected 103 | */ 104 | private void reportRejected(Runnable r, ThreadPoolExecutor executor, int rejected) { 105 | StringBuilder sb = new StringBuilder(128); 106 | sb.append(threadPoolName).append(" rejected ").append(rejected).append(" times"); 107 | sb.append(" by ").append(policy).append(CRLF); 108 | sb.append("Task ").append(r).append(" rejected from ").append(executor).append(CRLF); 109 | for (StackTraceElement b : Thread.currentThread().getStackTrace()) { 110 | sb.append(b).append(CRLF); 111 | } 112 | String detail = sb.toString(); 113 | log.error(detail); 114 | } 115 | 116 | /** 117 | * 线程池拒绝策略 118 | */ 119 | public enum RejectedPolicy { 120 | /** 121 | * 抛异常 122 | */ 123 | AbortPolicy, 124 | /** 125 | * 直接丢弃 126 | */ 127 | DiscardPolicy, 128 | /** 129 | * 丢弃队列中最老的任务 130 | */ 131 | DiscardOldestPolicy, 132 | /** 133 | * 将任务分给调用线程来执行 134 | */ 135 | CallerRunsPolicy 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /cm4j-hotswap/src/main/java/com/cm4j/util/ClassUtil.java: -------------------------------------------------------------------------------- 1 | package com.cm4j.util; 2 | 3 | import java.io.File; 4 | import java.io.FileInputStream; 5 | import java.io.IOException; 6 | import java.io.InputStream; 7 | 8 | public class ClassUtil { 9 | 10 | public static InputStream getClassInputStream(Class clazz) throws Exception { 11 | String classLocation = clazz.getProtectionDomain().getCodeSource().getLocation().getPath(); 12 | 13 | if (classLocation.endsWith(".jar")) { 14 | throw new IOException("cannot recompile class from jar: " + clazz); 15 | } else { 16 | String clazzName = clazz.getName().replace('.', '/') + ".class"; 17 | return new FileInputStream(new File(classLocation, clazzName)); 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /cm4j-hotswap/src/main/java/com/cm4j/util/PackageUtil.java: -------------------------------------------------------------------------------- 1 | package com.cm4j.util; 2 | 3 | import java.io.File; 4 | import java.io.IOException; 5 | import java.net.JarURLConnection; 6 | import java.net.URL; 7 | import java.net.URLDecoder; 8 | import java.util.Enumeration; 9 | import java.util.LinkedHashSet; 10 | import java.util.Set; 11 | import java.util.jar.JarEntry; 12 | import java.util.jar.JarFile; 13 | 14 | import org.slf4j.Logger; 15 | import org.slf4j.LoggerFactory; 16 | 17 | public class PackageUtil { 18 | 19 | private static final Logger log = LoggerFactory.getLogger(PackageUtil.class); 20 | 21 | /** 22 | * 从包package中获取所有的Class 23 | * 24 | * @param packageName 25 | * @return 26 | */ 27 | public static Set> findPackageClass(String packageName) { 28 | // 第一个class类的集合 29 | Set> classes = new LinkedHashSet<>(); 30 | // 是否循环迭代 31 | boolean recursive = true; 32 | // 获取包的名字 并进行替换 33 | String packageDirName = packageName.replace('.', '/'); 34 | // 定义一个枚举的集合 并进行循环来处理这个目录下的things 35 | Enumeration dirs; 36 | try { 37 | dirs = Thread.currentThread().getContextClassLoader().getResources(packageDirName); 38 | // 循环迭代下去 39 | while (dirs.hasMoreElements()) { 40 | // 获取下一个元素 41 | URL url = dirs.nextElement(); 42 | // 得到协议的名称 43 | String protocol = url.getProtocol(); 44 | // 如果是以文件的形式保存在服务器上 45 | if ("file".equals(protocol)) { 46 | // 获取包的物理路径 47 | String filePath = URLDecoder.decode(url.getFile(), "UTF-8"); 48 | // 以文件的方式扫描整个包下的文件 并添加到集合中 49 | findAndAddClassesInPackageByFile(packageName, filePath, recursive, classes); 50 | } else if ("jar".equals(protocol)) { 51 | // 如果是jar包文件 52 | // 定义一个JarFile 53 | JarFile jar; 54 | try { 55 | // 获取jar 56 | jar = ((JarURLConnection) url.openConnection()).getJarFile(); 57 | // 从此jar包 得到一个枚举类 58 | Enumeration entries = jar.entries(); 59 | // 同样的进行循环迭代 60 | while (entries.hasMoreElements()) { 61 | // 获取jar里的一个实体 可以是目录 和一些jar包里的其他文件 如META-INF等文件 62 | JarEntry entry = entries.nextElement(); 63 | String name = entry.getName(); 64 | // 如果是以/开头的 65 | if (name.charAt(0) == '/') { 66 | // 获取后面的字符串 67 | name = name.substring(1); 68 | } 69 | // 如果前半部分和定义的包名相同 70 | if (name.startsWith(packageDirName)) { 71 | int idx = name.lastIndexOf('/'); 72 | // 如果以"/"结尾 是一个包 73 | if (idx != -1) { 74 | // 获取包名 把"/"替换成"." 75 | packageName = name.substring(0, idx).replace('/', '.'); 76 | } 77 | // 如果可以迭代下去 并且是一个包 78 | if ((idx != -1) || recursive) { 79 | // 如果是一个.class文件 而且不是目录 80 | if (name.endsWith(".class") && !entry.isDirectory()) { 81 | // 去掉后面的".class" 获取真正的类名 82 | String className = name.substring(packageName.length() + 1, name.length() - 6); 83 | try { 84 | // 添加到classes 85 | classes.add(Class.forName(packageName + '.' + className)); 86 | } catch (ClassNotFoundException e) { 87 | log.error("添加用户自定义视图类错误 找不到此类的.class文件", e); 88 | } 89 | } 90 | } 91 | } 92 | } 93 | } catch (IOException e) { 94 | log.error("在扫描用户定义视图时从jar包获取文件出错", e); 95 | } 96 | } 97 | } 98 | } catch (IOException e) { 99 | log.error("", e); 100 | } 101 | 102 | return classes; 103 | } 104 | 105 | /** 106 | * 以文件的形式来获取包下的所有Class 107 | * 108 | * @param packageName 109 | * @param packagePath 110 | * @param recursive 111 | * @param classes 112 | */ 113 | private static void findAndAddClassesInPackageByFile(String packageName, String packagePath, 114 | final boolean recursive, Set> classes) { 115 | // 获取此包的目录 建立一个File 116 | File dir = new File(packagePath); 117 | // 如果不存在或者 也不是目录就直接返回 118 | if (!dir.exists() || !dir.isDirectory()) { 119 | return; 120 | } 121 | // 如果存在 就获取包下的所有文件 包括目录 122 | // 自定义过滤规则 如果可以循环(包含子目录) 或则是以.class结尾的文件(编译好的java类文件) 123 | File[] dirfiles = dir.listFiles( 124 | file -> (recursive && file.isDirectory()) || (file.getName().endsWith(".class"))); 125 | // 循环所有文件 126 | for (File file : dirfiles) { 127 | // 如果是目录 则继续扫描 128 | if (file.isDirectory()) { 129 | findAndAddClassesInPackageByFile(packageName + "." + file.getName(), file.getAbsolutePath(), recursive, 130 | classes); 131 | } else { 132 | // 如果是java类文件 去掉后面的.class 只留下类名 133 | String className = file.getName().substring(0, file.getName().length() - 6); 134 | try { 135 | // 添加到集合中去 136 | // 经过同学的提醒,这里用forName有一些不好,会触发static方法,没有使用classLoader的load干净 137 | classes.add( 138 | Thread.currentThread().getContextClassLoader().loadClass(packageName + '.' + className)); 139 | } catch (ClassNotFoundException e) { 140 | log.error("添加用户自定义视图类错误 找不到此类的.class文件", e); 141 | } 142 | } 143 | } 144 | } 145 | 146 | } 147 | -------------------------------------------------------------------------------- /cm4j-hotswap/src/main/java/com/cm4j/util/ProtoStuffUtil.java: -------------------------------------------------------------------------------- 1 | package com.cm4j.util; 2 | 3 | import io.protostuff.LinkedBuffer; 4 | import io.protostuff.ProtostuffIOUtil; 5 | import io.protostuff.Schema; 6 | import io.protostuff.runtime.DefaultIdStrategy; 7 | import io.protostuff.runtime.IdStrategy; 8 | import io.protostuff.runtime.RuntimeSchema; 9 | 10 | import java.util.Map; 11 | import java.util.concurrent.ConcurrentHashMap; 12 | 13 | /** 14 | * protostuff缓存类 15 | * 16 | * @author yeas.fun 17 | * @since 2022/4/21 18 | */ 19 | public class ProtoStuffUtil { 20 | 21 | static final DefaultIdStrategy STRATEGY = new DefaultIdStrategy(IdStrategy.DEFAULT_FLAGS 22 | | IdStrategy.PRESERVE_NULL_ELEMENTS 23 | | IdStrategy.MORPH_COLLECTION_INTERFACES 24 | | IdStrategy.MORPH_MAP_INTERFACES 25 | | IdStrategy.MORPH_NON_FINAL_POJOS); 26 | 27 | private static Map, Schema> cachedSchema = new ConcurrentHashMap<>(); 28 | 29 | public static byte[] encode(T obj) { 30 | Class clazz = obj.getClass(); 31 | LinkedBuffer buffer = LinkedBuffer.allocate(LinkedBuffer.DEFAULT_BUFFER_SIZE); 32 | try { 33 | @SuppressWarnings("unchecked") Schema schema = (Schema) getSchema(clazz); 34 | return ProtostuffIOUtil.toByteArray(obj, schema, buffer); 35 | } catch (Exception e) { 36 | throw new IllegalStateException(e.getMessage(), e); 37 | } finally { 38 | buffer.clear(); 39 | } 40 | } 41 | 42 | public static T decode(byte[] bytes, Class clazz) { 43 | try { 44 | T t = clazz.newInstance(); 45 | @SuppressWarnings("unchecked") Schema schema = (Schema) getSchema(clazz); 46 | ProtostuffIOUtil.mergeFrom(bytes, t, schema); 47 | return t; 48 | } catch (Exception e) { 49 | throw new IllegalStateException(e.getMessage(), e); 50 | } 51 | } 52 | 53 | private static Schema getSchema(Class clazz) { 54 | return cachedSchema.computeIfAbsent(clazz, aClass -> RuntimeSchema.createFrom(aClass, STRATEGY)); 55 | } 56 | } -------------------------------------------------------------------------------- /cm4j-hotswap/src/main/java/com/cm4j/util/RemotingInvokerUtil.java: -------------------------------------------------------------------------------- 1 | package com.cm4j.util; 2 | 3 | import com.cm4j.config.ErrorCode; 4 | import com.cm4j.grpc.proto.MS_METHOD_GRPC.MS_METHOD_REQ; 5 | import com.cm4j.invoke.IRemotingClass; 6 | import com.cm4j.invoke.invoker.RemotingInvokerScanner; 7 | import com.cm4j.invoke.invoker.RemotingParamVO; 8 | import com.cm4j.registry.registry.InvokerRegistry; 9 | import com.esotericsoftware.reflectasm.MethodAccess; 10 | import com.google.common.base.Joiner; 11 | import com.google.common.base.Preconditions; 12 | import com.google.common.collect.Maps; 13 | import com.google.common.collect.Sets; 14 | import com.google.protobuf.ByteString; 15 | import com.google.protobuf.MessageLite; 16 | import org.slf4j.Logger; 17 | import org.slf4j.LoggerFactory; 18 | 19 | import java.lang.reflect.Modifier; 20 | import java.util.Map; 21 | import java.util.Set; 22 | 23 | /** 24 | * 远程方法调用工具类 25 | * 26 | * @author yeas.fun 27 | * @since 2021/8/21 28 | */ 29 | @SuppressWarnings("unchecked") 30 | public class RemotingInvokerUtil { 31 | 32 | private static final Logger logger = LoggerFactory.getLogger(RemotingInvokerUtil.class); 33 | 34 | private static final Map methodAccessMap = Maps.newHashMap(); 35 | 36 | /** 37 | * 初始化 38 | * 39 | * @throws Exception 40 | */ 41 | public static void init() throws Exception { 42 | RemotingInvokerScanner.init(IRemotingClass.class.getPackage().getName()); 43 | } 44 | 45 | /** 46 | * 方法调用,非反射 47 | * 48 | * @param remotingClass 49 | * @param method 50 | * @param params 51 | * @param 52 | * @return 53 | */ 54 | public static R invoke(Class remotingClass, String method, Object... params) 55 | throws Exception { 56 | return invoke(remotingClass.getName(), method, params); 57 | } 58 | 59 | /** 60 | * 方法调用,非反射 61 | * 62 | * @param className 63 | * @param method 64 | * @param params 65 | * @param 66 | * @return 67 | */ 68 | public static R invoke(String className, String method, Object... params) throws Exception { 69 | // 获取注册的对象 70 | IRemotingClass targetObject = InvokerRegistry.getInstance().get(className); 71 | Preconditions.checkNotNull(targetObject, "注册regitstry系统未找到该类:" + className); 72 | 73 | // 获取methodAccess 74 | MethodAccess methodAccess = methodAccessMap.get(className); 75 | Preconditions.checkNotNull(methodAccess, "远程调用未初始化,请调用:RemotingInvokerScanner#init()"); 76 | 77 | // 执行 78 | return (R) methodAccess.invoke(targetObject, method, params); 79 | } 80 | 81 | /** 82 | * 远端方法调用 83 | * 84 | * @param className 85 | * @param methodName 86 | * @param request 87 | * @return 88 | * @throws Exception 89 | */ 90 | public static Object remoteInvoke(String className, String methodName, MS_METHOD_REQ request) throws Exception { 91 | Object[] params = decodeParams(request.getParams()); 92 | return invoke(className, methodName, params); 93 | } 94 | 95 | /** 96 | * 检测是否支持该数据类型 97 | *

98 | * 一般对象可能会有人在方法内修改对象的信息,这个grpc无法支持修改原有的引用信息,所以目前只之前前面的指定类型 99 | *

100 | * protostuff理论上所有的pojo都能支持,这里限制主要是避免误用,(跨服方法调用只能传递值的信息,不能修改原有引用数据的信息) 101 | * 102 | * @return 是否是支持的数据类型 103 | */ 104 | public static boolean checkClassTypeSupport(Class classType) { 105 | if (classType.isPrimitive() || String.class.equals(classType)) { 106 | return true; 107 | } 108 | if (Number.class.isAssignableFrom(classType)) { 109 | return true; 110 | } 111 | 112 | // 不支持接口类和抽象类 113 | if (classType.isInterface() || Modifier.isAbstract(classType.getModifiers())) { 114 | return false; 115 | } 116 | 117 | if (ErrorCode.class.equals(classType)) { 118 | return true; 119 | } 120 | if (MessageLite.class.isAssignableFrom(classType)) { 121 | return true; 122 | } 123 | 124 | // 扩展其他类型 125 | return false; 126 | } 127 | 128 | /** 129 | * 第一个参数支持的数据类型, 用于判定方法执行的服务器 130 | */ 131 | private static final Set> firstParamType = Sets.newHashSet(int.class, Integer.class, long.class, 132 | Long.class, String.class); 133 | 134 | public static boolean checkFirstParamType(Class classType) { 135 | return firstParamType.contains(classType); 136 | } 137 | 138 | public static void addInvoker(String className, MethodAccess methodAccess) { 139 | MethodAccess old = methodAccessMap.put(className, methodAccess); 140 | if (old != null) { 141 | logger.error("class [{}] replace MethodAccess {} -> {}", className, old, methodAccess); 142 | } 143 | } 144 | 145 | /** 146 | * 转换远程传输参数 147 | * 148 | * @return protobuff的ByteString, 即proto文件中的bytes 149 | */ 150 | public static ByteString encodeParams(Object[] params) { 151 | RemotingParamVO remotingParamVO = new RemotingParamVO(); 152 | remotingParamVO.setParams(params); 153 | return ByteString.copyFrom(ProtoStuffUtil.encode(remotingParamVO)); 154 | } 155 | 156 | /** 157 | * 还原参数 158 | * 159 | * @param paramBytes 160 | * @return 161 | */ 162 | public static Object[] decodeParams(ByteString paramBytes) { 163 | RemotingParamVO remotingParamVO = ProtoStuffUtil.decode(paramBytes.toByteArray(), RemotingParamVO.class); 164 | return remotingParamVO.getParams(); 165 | } 166 | 167 | } -------------------------------------------------------------------------------- /cm4j-hotswap/src/main/java/com/cm4j/util/ThreadUtil.java: -------------------------------------------------------------------------------- 1 | package com.cm4j.util; 2 | 3 | import com.google.common.base.Joiner; 4 | 5 | /** 6 | * @author yeas.fun 7 | * @since 2021/11/29 8 | */ 9 | public class ThreadUtil { 10 | 11 | /** 12 | * 获取指定线程堆栈信息 13 | * 14 | * @param thread 15 | * @return 16 | */ 17 | public static String getThreadStackTrace(Thread thread) { 18 | return Joiner.on("\n").join(thread.getStackTrace()); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /cm4j-hotswap/src/main/resources/config/resource/proto/grpc/MsMethod.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | option java_package = "com.cm4j.grpc.proto"; 4 | option java_outer_classname = "MS_METHOD_GRPC"; 5 | 6 | service MsMethodService {//跨服调用方法 7 | 8 | // 调用方法 9 | rpc invoker(MS_METHOD_REQ) returns (MS_METHOD_RESP); 10 | 11 | } 12 | 13 | message MS_METHOD_REQ { 14 | optional int64 seqId = 1; // 协议唯一ID 15 | required string className = 2;// class名字 16 | required string methodName = 3;// 方法名 17 | required bytes params = 4; // 参数信息 18 | } 19 | 20 | message MS_METHOD_RESP { 21 | optional int64 seqId = 1; // 协议唯一ID 22 | optional bytes reback = 2; // 返回信息 23 | optional int32 errorCode = 3; // 错误码异常 24 | } 25 | 26 | message PRIMITIVE_PARAM { 27 | optional bool paramBool = 1; // bool值 28 | optional int32 paramNumber = 2; // 整型 29 | optional int64 paramLong = 3; // 长整型 30 | optional double paramDouble = 4; // 小数 31 | optional string paramString = 5; // 字符串 32 | } 33 | -------------------------------------------------------------------------------- /cm4j-hotswap/src/main/resources/next.md: -------------------------------------------------------------------------------- 1 | h1 接下来的文章 2 | - 行为树 3 | - SLG的视野同步:四叉树算法 -------------------------------------------------------------------------------- /cm4j-hotswap/src/main/resources/notes/cleanLastUpdated.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | rem create by NettQun 3 | 4 | rem 这里写你的仓库路径 5 | set REPOSITORY_PATH=D:\repository 6 | rem 正在搜索... 7 | for /f "delims=" %%i in ('dir /b /s "%REPOSITORY_PATH%\*lastUpdated*"') do ( 8 | echo %%i 9 | del /s /q "%%i" 10 | ) 11 | rem 搜索完毕 12 | pause 13 | -------------------------------------------------------------------------------- /cm4j-hotswap/src/main/resources/notes/flame-graph.md: -------------------------------------------------------------------------------- 1 | # 如何读懂火焰图(Flame Graph) 2 | 3 | 文末的参考文章说的已经很清楚了,我这里简单做个总结。 4 | 5 | ## 原理 6 | 7 | 火焰图就是CPU的抽样图。 8 | 9 | 系统定期抽样(通常频率是99Hz,即每秒99次),基于函数调用堆栈来画抽样图。 10 | 11 | - y轴代表方法的调用栈,越高代表方法堆栈深度越深; 12 | - x轴代表被抽样的次数(注意:不是时间),越宽代表该方法栈消耗的CPU次数越多。 13 | 14 | ### 示例 15 | 16 | 假设一定的抽样时间内,e()和i()都执行了一次,而g()执行了2次,则下图就是方法调用火焰图 17 | 18 | ![火焰图示例](https://oss.yeas.fun/halo-yeas/flame-graph1_1640182522237.png) 19 | 20 | ## 怎么看? 21 | 22 | 所以:火焰图就看最上面的方法哪个函数占用的宽度最大(平顶),就代表被抽样的次数最多,也就表示这个函数占用CPU最多,最有可能存在性能问题。 23 | 24 | ## 在哪看? 25 | 26 | - arthas框架中支持对应用生成火焰图 27 | - Java中开启Java Flight Recorder(JFR),也可通过Java Mission Control (JMC)工具查看火焰图,且性能消耗较小 28 | 29 | ## 文章参考 30 | 31 | https://www.ruanyifeng.com/blog/2017/09/flame-graph.html -------------------------------------------------------------------------------- /cm4j-hotswap/src/main/resources/notes/flame-graph1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cm4j/cm4j-all/1bfca750355adc6f36096d9cf68462dba03c87c7/cm4j-hotswap/src/main/resources/notes/flame-graph1.png -------------------------------------------------------------------------------- /cm4j-hotswap/src/main/resources/notes/maven-mechanism.md: -------------------------------------------------------------------------------- 1 | # Maven常见问题与原理技巧 2 | 3 | ## 背景 4 | 5 | 目前项目中主流的都是使用maven等构建工具,当然在使用过程中也会遇到各种各样的疑惑或问题,比如: 6 | 7 | - maven生命周期到底有啥用 8 | - jar包下载不了怎么办 9 | - 不知道配置怎么配,下载jar包的顺序是什么 10 | - jar包冲突又是怎么解决 11 | 12 | 问的人多了,也就形成了大家的共性问题,这里基于这些问题,本文着重梳理下maven的基本原理,并对一些常见问题做一些总结。 13 | 14 | ## Maven能干嘛? 15 | 16 | maven并不是第一个做构建工具的,在它之前还有ant等其他的工具,再早之前就是纯手工自己写脚本进行打包,这种方式就非常的原始了。几种方式都有各自的弊端,这里我就不一一赘述了。这里先简单介绍下maven能干啥: 17 | 18 | - 依赖管理:定义了jar包各版本之间的依赖关系 19 | - 生命周期管理:规范化构建的各个阶段,也是为了便于插件的运行 20 | - 仓库管理:建设了jar包仓库,管理所有的开源jar,同时可自架设私有仓库 21 | - 约定大于配置:标准的目录结构(web)、默认的输出位置(target)、默认的命令执行流程、预定义的生命周期阶段 22 | - 项目信息管理:项目的说明、版本等信息 23 | 24 | Tips:Maven虽然是java实现的,但并非java独有。它是一个项目管理工具,也可用于构建其他语言的项目,如C#、Ruby、Scala等 25 | 26 | ## Maven生命周期有啥用? 27 | 28 | Maven有三套相互独立的生命周期,分别是clean、default和site。每个生命周期包含一些阶段(phase),阶段是有顺序的,后面的阶段依赖于前面的阶段。 29 | 30 | 各个生命周期相互独立,但一个生命周期的阶段前后依赖。 31 | 32 | ![Maven生命周期](https://oss.yeas.fun/halo-yeas/maven-mechanism1_1641727766875.png) 33 | 34 | **例子:** 35 | 36 | - mvn clean 调用clean生命周期的clean阶段,实际执行pre-clean和clean阶段 37 | - mvn test 调用default生命周期的test阶段,实际执行test以及之前所有阶段 38 | - mvn clean install 调用clean生命周期的clean阶段和default的install阶段,实际执行pre-clean和clean,install以及之前所有阶段 39 | 40 | 具体Maven生命周期有啥用?这是和Maven的命令以及Maven的插件机制有关。 41 | 42 | ## Maven的命令格式 43 | 44 | 方式1:mvn compile:compile 【指名道姓】 45 | 46 | - mvn :[:]: 47 | - mvn ::执行 plugin-prefix 插件的 goal-name 目标(动作) 48 | 49 | 方式2:mvn compile 【绑定生命周期阶段】 50 | 51 | 将插件目标与生命周期阶段(lifecycle phase)绑定,这样用户在命令行只是输入生命周期阶段而已。 52 | 53 | 例如: Maven默认将maven-compiler-plugin的compile目标(goal)与生命周期的compile阶段绑定。 因此命令mvn 54 | compile实际上是先定位到compile这一生命周期阶段,然后再根据绑定关系调用maven-compiler-plugin的compile目标。 55 | 56 | Tips:这套路和Ant的target是不是很像? 57 | 58 | ## 经常遇到的问题 59 | 60 | ### 1. 内网、外网的配置文件不一致 61 | 62 | Maven中有一个特性profile,主要是可以根据不同环境激活不同的配置。这样我们就可以定义内网环境和正式环境,然后根据需要激活特定的配置 63 | 64 | 指定profile激活:mvn clean -P nw 65 | 66 | ![Maven的profile](https://oss.yeas.fun/halo-yeas/maven-mechanism2_1641727767641.png) 67 | 68 | 下面是几种激活的条件: 69 | 70 | ```xml 71 | 72 | true 73 | !1.8 74 | 75 | Window 10 76 | 77 | 78 | src/main/resources/config.xml 79 | 80 | 81 | ``` 82 | 83 | ### 2. Maven中的属性是怎么定义的 84 | 85 | Maven中属性都是从哪里来的?是哪里定义的?这里直接列了一个脑图给大家参考 86 | 87 | ![Maven属性来源](https://oss.yeas.fun/halo-yeas/maven-mechanism3_1641727767109.png) 88 | 89 | ### 3. 配置的优先级 90 | 91 | 原则:越靠近项目的,优先级越高 92 | 93 | - pom.xml 94 | - ${user}/.m2/settings.xml 95 | - ${maven_dir}/conf/settings.xml 96 | 97 | 推荐的做法: 98 | 99 | - 项目独有的配置,放在pom.xml里 100 | - 全局的配置,放在第2项。比如说本地仓库路径、远程仓库的密码、mirror镜像地址等等 101 | - 第3项可以少用,因为IDEA有内置的maven,如果配在第3项,则注意修改idea的配置 102 | 103 | ### 4. Maven是如何下载jar包的? 104 | 105 | jar包存储相关的概念 106 | 107 | - 本地仓库(推荐配置到settings.xml中) 108 | - 远程仓库 109 | 110 | ![Maven远程仓库](https://oss.yeas.fun/halo-yeas/maven-mechanism4_1641727766609.png) 111 | 112 | - 仓库镜像:mirror(可在settings.xml中配置) 113 | 114 | ![Maven仓库镜像](https://oss.yeas.fun/halo-yeas/maven-mechanism5_1641727767720.png) 115 | 116 | 下载Jar包流程图 117 | 118 | ![Maven下载jar包流程](https://oss.yeas.fun/halo-yeas/maven-mechanism6_1641727767234.png) 119 | 120 | ### 5.jar包下载不到或不对 121 | 122 | - 配置是否配到正确的仓库上 123 | - 网络是否通畅,尤其是在连官方maven仓库的时候 124 | - 网络不通产生.lastUpdated文件 125 | 126 | 解决方案: 127 | 128 | - 删掉下载不了的jar,执行 mvn compile 重试 129 | - 检查仓库的地址和镜像的地址 130 | - 对于第3点,用脚本删除 .lastUpdated 文件。脚本下载:[cleanLastUpdated.bat](https://oss.yeas.fun/halo-yeas/cleanLastUpdated_1641728476687.bat) 131 | - IDEA显示红色但实际能运行:清除缓存重启 File/Invalidate Caches。 132 | 133 | ### 6.jar包冲突 134 | 135 | **1. 现象** 136 | 137 | - MAVEN项目运行中如果报如下错误,十有八九是jar包冲突导致的: 138 | - Caused by: java.lang.NoSuchMethodError 139 | - Caused by: java.lang.ClassNotFoundException 140 | 141 | **2. 产生原因** 142 | 143 | - Maven的依赖传递: 144 | - A->B->C1(log 15.0) 145 | - D->C2(log 16.0) 146 | 147 | 假设C2再C1的基础上增加或删除了方法,那A、D包进行调用的时候,就会抛错:NoSuchMethodError,这就是jar包冲突 148 | 149 | **3. 如何查看冲突?** 150 | 151 | - mvn dependency:tree 152 | - eclipse:Maven Helper插件 153 | - IDEA:ctrl+shift+alt+U查看maven的依赖图或者用插件进行排查 154 | 155 | **4. 如何解决包冲突** 156 | 157 | #### 一个概念:选择一个jar包使用 158 | 159 | **1. Maven的默认策略** 160 | 161 | - 最短路径优先 E->F->D2 比 A->B->C->D1 路径短 1 位 162 | - 最先申明优先 A->B->C1, E->F->C2,路径一样,则C1先定义就用C1 163 | 164 | **2. 手动处理** 165 | 166 | - 手动排除,配置exclusion 167 | 168 | ### --- END --- -------------------------------------------------------------------------------- /cm4j-hotswap/src/main/resources/notes/maven-mechanism1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cm4j/cm4j-all/1bfca750355adc6f36096d9cf68462dba03c87c7/cm4j-hotswap/src/main/resources/notes/maven-mechanism1.png -------------------------------------------------------------------------------- /cm4j-hotswap/src/main/resources/notes/maven-mechanism2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cm4j/cm4j-all/1bfca750355adc6f36096d9cf68462dba03c87c7/cm4j-hotswap/src/main/resources/notes/maven-mechanism2.png -------------------------------------------------------------------------------- /cm4j-hotswap/src/main/resources/notes/maven-mechanism3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cm4j/cm4j-all/1bfca750355adc6f36096d9cf68462dba03c87c7/cm4j-hotswap/src/main/resources/notes/maven-mechanism3.png -------------------------------------------------------------------------------- /cm4j-hotswap/src/main/resources/notes/maven-mechanism4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cm4j/cm4j-all/1bfca750355adc6f36096d9cf68462dba03c87c7/cm4j-hotswap/src/main/resources/notes/maven-mechanism4.png -------------------------------------------------------------------------------- /cm4j-hotswap/src/main/resources/notes/maven-mechanism5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cm4j/cm4j-all/1bfca750355adc6f36096d9cf68462dba03c87c7/cm4j-hotswap/src/main/resources/notes/maven-mechanism5.png -------------------------------------------------------------------------------- /cm4j-hotswap/src/main/resources/notes/maven-mechanism6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cm4j/cm4j-all/1bfca750355adc6f36096d9cf68462dba03c87c7/cm4j-hotswap/src/main/resources/notes/maven-mechanism6.png -------------------------------------------------------------------------------- /cm4j-hotswap/src/main/resources/notes/tomcat_shutdown.md: -------------------------------------------------------------------------------- 1 | # Tomcat停止时无法正常关闭? 2 | 3 | ## 背景 4 | 5 | 在调tomcat的shutdown命令,或者在编辑器里点关闭web服务,可能会出现关不掉的情况,控制台可能也不报错,表现就是卡住了,那应该如何排查tomcat无法正常shudown的情况? 6 | 7 | ## 为什么tomcat不能正常关闭? 8 | 9 | 文末的参考文档说的很详细了,我这里再总结下:不是tomcat关不掉,而是tomcat启动的应用某些线程没正常关闭, 再明确点就是:项目中有非daemon的线程没关闭。具体原因大家再百度百度吧。 10 | 11 | 知道了什么原因,解决方案就很简单了:关闭命令调用后,看还有哪些非daemon线程还活着,然后下面二选一: 12 | 13 | - 把这个线程关闭,如果是属于线程池的线程,则关闭线程池 14 | - 更暴力点,把这个线程的daemon属性设为true。**如果是关键业务,则最好是使用正常的线程关闭** 15 | 16 | ## 应该怎么排查? 17 | 18 | 一旦调用了命令,断点的线程都被释放了,那怎么排查哪些非daemon还活着? 19 | 答案是arthas工具,虽然调用了tomcat的shutdown的命令,但jvm还存活着,此时arthas依然可以连接上,然后调用thread命令即可查看当前所有存活的线程。 20 | 21 | 所以一个很好的开发习惯是:**业务中创建的线程都必须自定义名字,包括走线程池创建的线程。** 22 | ![Tomcat停止时无法正常关闭](https://oss.yeas.fun/halo-yeas/tomcat_shutdown1_1641010543472.png) 23 | 然后,基于线程名字到对应业务中进行上面所说的二选一操作。处理完成,web即可正常关闭。 24 | 25 | ## 参考文档 26 | https://blog.csdn.net/u012454773/article/details/54584874 27 | 28 | ## --- END --- 29 | -------------------------------------------------------------------------------- /cm4j-hotswap/src/main/resources/notes/tomcat_shutdown1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cm4j/cm4j-all/1bfca750355adc6f36096d9cf68462dba03c87c7/cm4j-hotswap/src/main/resources/notes/tomcat_shutdown1.png -------------------------------------------------------------------------------- /cm4j-hotswap/src/main/resources/protoc/grpc.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | :: 修改编码为utf8un 3 | :: CHCP 65001 4 | TITLE PROTO-JAVA-GEN 5 | REM 设置接口文档路径 6 | SET CURR_PATH=%cd% 7 | CD ..\..\..\..\.. 8 | SET PROTO_PATH=%cd%\cm4j-hotswap\src\main\resources\config\resource\proto 9 | SET PROTO_PATH_GRPC=%PROTO_PATH%\grpc 10 | SET JAVA_OUT=%cd%\cm4j-hotswap\src\main\java 11 | CD %CURR_PATH% 12 | SET PROTOC_PATH=%CURR_PATH%\protoc.exe 13 | 14 | rd /s /q %JAVA_OUT%\com\cm4j\grpc\proto\ 15 | 16 | ECHO= 17 | ECHO ==================================== 18 | ECHO 1.编译proto文件 19 | ECHO PROTO_PATH =%PROTO_PATH% 20 | ECHO JAVA_OUT =%JAVA_OUT% 21 | ECHO PROTOC_PATH =%PROTOC_PATH% 22 | ECHO ==================================== 23 | FOR /R %PROTO_PATH_GRPC% %%f IN (*.proto) DO ( 24 | ECHO %%f 25 | %PROTOC_PATH% -I=%PROTO_PATH% --proto_path=%PROTO_PATH% --java_out=%JAVA_OUT% %%f 26 | %PROTOC_PATH% -I=%PROTO_PATH% --plugin=protoc-gen-grpc-java=%CURR_PATH%\protoc-gen-grpc-java-1.34.1-windows-x86_64.exe --grpc-java_out=%JAVA_OUT% --java_out=%JAVA_OUT% %%f 27 | ) 28 | 29 | PAUSE & EXIT /b -------------------------------------------------------------------------------- /cm4j-hotswap/src/main/resources/protoc/protoc-gen-grpc-java-1.34.1-windows-x86_64.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cm4j/cm4j-all/1bfca750355adc6f36096d9cf68462dba03c87c7/cm4j-hotswap/src/main/resources/protoc/protoc-gen-grpc-java-1.34.1-windows-x86_64.exe -------------------------------------------------------------------------------- /cm4j-hotswap/src/main/resources/protoc/protoc.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cm4j/cm4j-all/1bfca750355adc6f36096d9cf68462dba03c87c7/cm4j-hotswap/src/main/resources/protoc/protoc.exe -------------------------------------------------------------------------------- /cm4j-hotswap/src/main/resources/solutions/arthas-classloader.md: -------------------------------------------------------------------------------- 1 | # Arthas原理:理解ClassLoader 2 | 3 | ## 系列介绍目录: 4 | 5 | [Java线上解决方案系列目录](//yeas.fun/archives/solution-contents) 6 | 7 | 或者可以直接下一篇:[Arthas原理:如何做到与应用代码隔离?](https://yeas.fun/archives/arthas-isolation) 8 | 9 | ## 背景 10 | 11 | 阿里的arthas一经推出就大受好评,主要原因就是它提供了一套线上问题的解决方案,比如可以在线查看服务器状态;可以支持热更新,原理类似我们之前所讲的[JAVA热更新1:Agent方式热更](//yeas.fun/archives/hotswap-agent) 12 | ; 它还可以支持对线上的代码跟踪执行情况,打印执行参数和返回参数等功能。功能那是异常强大,关键的一点是它对应用是无侵入的,也就是不影响到目标应用的业务逻辑。 13 | 14 | 为了实现上述的一些功能,arthas需要解决以下几个问题: 15 | 16 | - 应用(目标进程)如何通过ClassLoader实现Arthas的代码加载? 17 | - 应用与Arthas如何隔离?也就是Arthas是如何做到无侵入的; 18 | - 既然是互相隔离的,那应用与arthas又是如何进行代码相互调用的? 19 | 20 | 而这一切的一切,都要从JDK提供的ClassLoader机制说起。 21 | 22 | ## ClassLoader作用 23 | 24 | ClassLoader主要作用就是通过一个类的全限定名来获取描述该类的二进制字节,他的来源不局限于从class类文件读取,可以从任意二进制字节来读取,甚至从http下载的二进制字节都可以。 25 | 26 | ### 为什么要进行数据隔离? 27 | 28 | 其实最早之前的ClassLoader是没有数据隔离的,这就会导致一种情况:假设JDK里面提供了String类,而我们也写一个同名同姓的String类,JVM加载类的时候就可能先加载到我们自己写的String类。 29 | 那我们就可以修改String类的逻辑,JDK的代码就不安全了。 30 | 于是就需要一种机制来保证JDK的代码优先加载,且业务层代码无法对JDK代码进行篡改。 31 | 32 | ### jvm对ClassLoader的保证 33 | 34 | 为了解决上述问题,JDK从1.2开始引入双亲委派模式。这是基于以下几个保证的: 35 | 36 | - JVM内部,ClassLoader类似于类的命名空间 37 | - 比较两个类是否相等,必须是在同一个类加载器上才有意义。例如以下几个方法判断:equals(),isAssignableFrom(),instanceof 38 | 39 | 结论:ClassLoader不同 > 类不同 > 对象不同。可以通过ClassLoader进行代码隔离。 40 | 41 | ## 怎么实现隔离呢? 42 | 43 | 答案就是:双亲委派机制。不知道为啥这么命名,但我们看下ClassLoader的源码大概也就知道类加载原理了。 44 | 45 | ```java 46 | public class ClassLoader{ 47 | protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException { 48 | synchronized (getClassLoadingLock(name)) { 49 | // 首先:检测类是否已经被加载过,加载过,直接返回 50 | Class c = findLoadedClass(name); 51 | if (c == null) { 52 | long t0 = System.nanoTime(); 53 | try { 54 | // 存在parent的ClassLoader 55 | if (parent != null) { 56 | c = parent.loadClass(name, false); // 调用parent的ClassLoader继续加载类 57 | } else { 58 | // 不存在parent,说明就是BootstrapClassLoader。JDK里BootstrapClassLoader是由jvm底层实现的,没有实际的类 59 | c = findBootstrapClassOrNull(name); 60 | } 61 | } catch (ClassNotFoundException e) { 62 | // 类没找到,说明是parent没加载到对应的类,这里不需要进行异常处理,继续后续逻辑 63 | } 64 | 65 | // 如果一层层往上都没加载到类,则本ClassLoader尝试findClass()查找类 66 | if (c == null) { 67 | long t1 = System.nanoTime(); 68 | c = findClass(name); 69 | 70 | // this is the defining class loader; record the stats 71 | sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0); 72 | sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); 73 | sun.misc.PerfCounter.getFindClasses().increment(); 74 | } 75 | } 76 | if (resolve) { 77 | resolveClass(c); 78 | } 79 | return c; 80 | } 81 | } 82 | } 83 | ``` 84 | 85 | 从源码可以看出: 86 | 87 | - 查找class:从当前ClassLoader开始查找,如果当前ClassLoader已经加载了,则子类就直接用了,不会再次加载;如果没有加载,则往上一层查找,一直查找到顶层; 88 | - 加载class:从顶层开始往下判断,看ClassLoader的搜索范围内是否包含这个class的path,如果包含则加载;如果没有,则往下一层继续判断,一直到当前的ClassLoader; 89 | 90 | **文字描述相对抽象,大家可以参照流程图:** 91 | 92 | ![Arthas原理:理解ClassLoader](https://oss.yeas.fun/halo-yeas/arthas-classloader1_1642839277016.png) 93 | 94 | 上图是ClassLoader的加载流程,还是有点复杂,我这里再简述下: 95 | 96 | - ClassLoader更像是一个树状结构 97 | - 查找类是从下往上的,也就是:子ClassLoader加载的类可以访问父ClassLoader的加载类,反之或平级则不行 98 | - 而加载是从上往下的,比如说BootstrapClassLoader就是加载jre\lib的所有包,ExtClassLoader就是加载ext目录下所有jar包 99 | - 不同的ClassLoader的加载类是互相隔离的 100 | 101 | 基于上述的机制,我们也就无法篡改JDK的源码,因为JDK的很多代码都是在最上层的ClassLoader去加载的,而我们应用更多的是下层ClassLoader加载的,JDK会优先加载自己的类,即使我们写了同名的String类也不会加载到。 102 | 同时这种机制也提供了一种类隔离的机制,接下来我们举个例子来说明。 103 | 104 | ## Tomcat的实现类的隔离与共享 105 | 106 | Tomcat是一个web容器,它可以同时启动多个web服务,那不能出现一个web的应用代码能够修改另一个web应用的逻辑,也就是多个web的代码就是需要隔离的。 107 | 但是都是web应用,里面有些jar包和逻辑是一样的,比如说tomcat-home/lib下的jar包,也不能每个web应用都去加载所有jar包,这样内存就太浪费了。 108 | 109 | 因此就产生这样的需求:web之间要数据隔离,web公用的jar包却要共享,那应该如何实现呢? 110 | 111 | ![Arthas原理:理解ClassLoader](https://oss.yeas.fun/halo-yeas/arthas-classloader2_1642839277016.png) 112 | 113 | 上图就是Tomcat的ClassLoader的树状图,Tomcat下可新增common、server、shared三组目录(默认不开放,需要指定配置),用于存放jar包。 下面列出不同ClassLoader对应加载的jar包内容: 114 | 115 | | ClassLoader | 加载目录或文件 | 说明 | 116 | |:--------------------|-----------------|-----------------------| 117 | | CommonClassLoader | /common | 所有应用共享(包括tomcat) | 118 | | CatalinaClassLoader | /server | tomcat的实现是独立隔离的 | 119 | | SharedClassLoader | /shared | 所有web应用共享,但对tomcat不可见 | 120 | | WebappClassLoader | /WebApp/WEB-INF | 不同web应用相互隔离 | 121 | | JasperLoader | jsp | 支持热更HotSwap | 122 | 123 | 从上面我们还可以看出不同的ClassLoader加载不同的目录,且父ClassLoader的类是被子ClassLoader共享的。如果需要隔离,那就下放不同的子ClassLoader去加载。 124 | 125 | 同时也解释了为什么jsp一保存就可以立即生效而不需要重启Tomcat,因为保存时Tomcat会使用JasperLoader重新加载新的jsp页面,从而实现jsp的实时热更新。 126 | 其原理类似于:[JAVA热更新2:动态加载子类热更](//yeas.fun/archives/java-hotswap-compile) 127 | 128 | ## 总结 129 | 130 | 上述主要讲了JDK的ClassLoader的双亲委派的类加载机制,核心就是如何实现代码的隔离与共享。 131 | 这个和Arthas的框架加载和隔离机制高度相关,同样原理的开源框架有不少,例如:[bistoury](https://github.com/qunarcorp/bistoury) 132 | 、[jvm-sandbox](https://github.com/alibaba/jvm-sandbox) 等, 在下一篇我们正式进入Arthas实现原理篇:Arthas的加载和隔离机制,以及如何对Arthas框架内的源码进行调试。 133 | 134 | ### --- END ---- -------------------------------------------------------------------------------- /cm4j-hotswap/src/main/resources/solutions/arthas-classloader1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cm4j/cm4j-all/1bfca750355adc6f36096d9cf68462dba03c87c7/cm4j-hotswap/src/main/resources/solutions/arthas-classloader1.png -------------------------------------------------------------------------------- /cm4j-hotswap/src/main/resources/solutions/arthas-classloader2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cm4j/cm4j-all/1bfca750355adc6f36096d9cf68462dba03c87c7/cm4j-hotswap/src/main/resources/solutions/arthas-classloader2.png -------------------------------------------------------------------------------- /cm4j-hotswap/src/main/resources/solutions/arthas-command-category.md: -------------------------------------------------------------------------------- 1 | # Arthas原理:Arthas的命令分类及原理 2 | 3 | ## 系列介绍目录: 4 | 5 | [Java线上解决方案系列目录](//yeas.fun/archives/solution-contents) 6 | 7 | ## 背景 8 | 在[Arthas原理:理解ClassLoader](//yeas.fun/archives/arthas-classloader)一文中,我们首先介绍了代码隔离的概念,并讲解了代码隔离的基础:ClassLoader; 9 | 10 | 在[Arthas原理:如何做到与应用代码隔离?](//yeas.fun/archives/arthas-isolation)一文中,我们介绍了利用ClassLoader实现代码隔离的原理,并讲解了Arthas与应用之间的代码在代码隔离的基础上如何进行代码互调的。 11 | 12 | ## 代码隔离的一个例子:jvm-sandbox 13 | 14 | [jvm-sandbox](https://github.com/alibaba/jvm-sandbox) 是alibaba的另一个框架,他是代码增强的基础框架。这里是两者的区别:[https://www.cnblogs.com/ttzzyy/p/11414051.html](https://www.cnblogs.com/ttzzyy/p/11414051.html) 。 15 | 16 | 两者的核心作用是一样的,只是jvm-sandbox是工具箱,而arthas更像是一个产成品。所以两者的核心原理也都是一样的,都是代码隔离和代码增强。 17 | 18 | ![Arthas原理:Arthas的命令分类及原理](https://oss.yeas.fun/halo-yeas/arthas-command-category1.png) 19 | 20 | 上图是jvm-sandbox官网给出的一个原理图,几个ClassLoader的关系如下: 21 | - BusinessClassLoader就是应用的ClassLoader; 22 | - SandboxClassLoader是jvm-sandbox的自定义ClassLoader,它加载了sandbox-core.jar与sandbox-api.jar; 23 | - Module其实就是各自实现的模块,标准的插件机制,我们可以不停服动态的增加和减少Module。IDEA和eclipse的插件机制原理和这个基本类似,对插件感兴趣的同学可以自行研究源码; 24 | - ModuleJar的父ClassLoader是SandboxClassLoader,也就是Module可以直接调用sandbox-core.jar与sandbox-api.jar的逻辑代码。 25 | 26 | 代码隔离主要体现在(insulate就是隔离的意思): 27 | - SandboxClassLoader与BusinessClassLoader是并行的,也就是sandbox的代码正常情况下与业务代码是互不相通的; 28 | - Module1与Module2的ClassLoader也是并行的,即各个模块之间也是互不相通的; 29 | - Module与应用如何作用的?上一篇已经讲过,通过中间桥梁:SpyAPI。 30 | 31 | ## 命令分类 32 | 33 | 至此,arthas通过agent连接到应用,可以实现如下命令: 34 | 35 | ### 原理1:Instrumentation: 36 | 因arthas是通过agent连接目标应用的,所以可以获取到Instrumentation对象,而Instrument对象提供如下核心API: 37 | 38 | **1. getAllLoadedClasses()**
39 | 作用:查找所有的加载的类
40 | 相关命令:sc、sm、classloader,以及用到类过滤,方法过滤 41 | 42 | **2. redefineClasses()**
43 | 作用:基于class文件重载实现,一般用于热更
44 | 相关命令:redefine 45 | 46 | **3. retransformClasses()**
47 | 作用1:可以获取甚至是修改类的二进制字节
48 | 相关命令:dump、jad
49 | 作用2:代码增强
50 | 一般都是EnhancerCommand的子类。其主要是对目标应用进行增强,在应用方法进入、退出、异常的调用插入SpyAPI的调用,而SpyAPI会回调arthas的逻辑,从而实现业务执行时触发arthas监控。
51 | 相关命令:AOP动态增强类:watch、stack、monitor、tt、trace等
52 | **TIPS:** 代码增强后运行逻辑并不能直观看到,这时可以打开arthas的参数:options dump true,这样在增强时会把增强后的class dump到本地,方便查看增强后的实际代码。 53 | 54 | ### 原理2:调用目标应用的类: 55 | 调用目标应用ExtClassLoader加载的类,而jmx的类就包含其中,从而可以直接获取到系统的一些运行情况。
56 | 相关命令:
57 | dashboard:查看当前系统的运行情况
58 | jvm:获取jvm相关信息
59 | mbean:获取jmx相关信息
60 | vmoption:通过jmx修改虚拟机参数
61 | sysenv:通过jmx修改虚拟机参数
62 | heapdump:直接修改系统的环境变量
63 | thread:获取到线程相关信息
64 | perfcounter:查看当前JVM的Perf Counter信息,jvm进程运行时,会记录一些实时的监控数据到perCounter文件中,可通过API获取这些数据 65 | 66 | ### 原理3:JDK内存编译: 67 | 作用:把java文件编译为class
68 | 相关命令:mc
69 | 相关源码:MemoryCompilerCommand 70 | 71 | ### 其他 72 | 还有一些命令是基于其他一些原理执行的
73 | 比如:
74 | 利用反射执行的:getstatic
75 | 利用OGNL框架执行的:ognl
76 | 火焰图:profiler,它使用async-profiler对应用采样,生成火焰图,从而可以监控应用的。可以参考这篇文章:[如何读懂火焰图](https://yeas.fun/archives/flame-graph) 77 | 78 | ## 总结 79 | 至此,我们就完成了arthas的核心原理的分析。本篇主要是针对底层设计思路分析,较少涉及具体的源码,建议有时间的同学多查看和断点源码以及参考其他网站进行深入分析。 80 | 81 | --- END --- -------------------------------------------------------------------------------- /cm4j-hotswap/src/main/resources/solutions/arthas-command-category1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cm4j/cm4j-all/1bfca750355adc6f36096d9cf68462dba03c87c7/cm4j-hotswap/src/main/resources/solutions/arthas-command-category1.png -------------------------------------------------------------------------------- /cm4j-hotswap/src/main/resources/solutions/arthas-isolation.md: -------------------------------------------------------------------------------- 1 | # Arthas原理:如何做到与应用代码隔离? 2 | 3 | ## 系列介绍目录: 4 | 5 | [Java线上解决方案系列目录](//yeas.fun/archives/solution-contents) 6 | 7 | ## 为什么要代码隔离? 8 | 9 | 在上一篇文章:[Arthas原理:理解ClassLoader](//yeas.fun/archives/arthas-classloader),我们讲了JDK的ClassLoader的原理,这是arthas实现代码隔离的理论基础。 10 | 11 | 代码隔离,也就是arthas所保证的:对业务代码无侵入,目标应用无需重启也无需感知arthas的存在。 12 | 13 | 那首先我们来讲下:arthas为什么要进行代码隔离? 14 | 15 | 我们都知道arthas是一个开源框架,它是在应用正在运行的时候,打通应用与arthas的连接通路。这就对arthas提出一个需求:arthas不能影响应用运行的代码,不能因为arthas的连接而导致业务逻辑的运行不正常。 16 | 同样的,应用里面的代码也不能影响arthas的正常工作。即:arthas的代码要和应用代码进行隔离,从机制上保证双方的互不干扰。 17 | 18 | ## Arthas是怎么连到我们的应用的? 19 | 其实JDK的也留了一个类似后门的方式:Agent,在这篇 [JAVA热更新1:Agent方式热更](//yeas.fun/archives/hotswap-agent) 文章中,也是利用了agent来实现的。agent网上资料特别多,大家自行了解。 20 | 21 | 简单点说:Agent就是可以在不停应用的情况下,把一个外部jar包代码动态加载到目标JVM中,再配合Instrumentation类,可以对目标应用做任何事,注意是任何事,包括动态修改JVM中运行的字节码, 22 | 从而实现对目标应用的逻辑修改 23 | 24 | 即:**arthas的代码最终被加载进目标JVM**,两者代码同时运行,且通过特殊方式可互相调用。那2份代码如何做到互不影响的? 25 | 26 | ## 怎么进行代码隔离?(核心思路) 27 | 28 | Arthas代码隔离的思路,和阿里的另一个框架 [jvm-sandbox](https://github.com/alibaba/jvm-sandbox) 实现原理是一样的,很多源码都差不多,有兴趣的同学也可以研究研究。 29 | 30 | 通过上一篇文章 [Arthas原理:理解ClassLoader](//yeas.fun/archives/arthas-classloader) ,我们应该知道如何进行代码隔离了,就是使用不同的ClassLoader去加载类; 31 | 同时因为arthas需要执行jvm等命令,所以它需要调用到目标应用的Runtime的运行数据,而这些类都是JDK的底层类,一般是由ExtClassLoader或者BootstrapClassLoader去加载的。 32 | 33 | ```java 34 | // Arthas源码:jvm命令中需要调用目标应用的JMX的相关代码 35 | public class JvmCommand { 36 | private final RuntimeMXBean runtimeMXBean = ManagementFactory.getRuntimeMXBean(); 37 | private final ClassLoadingMXBean classLoadingMXBean = ManagementFactory.getClassLoadingMXBean(); 38 | private final CompilationMXBean compilationMXBean = ManagementFactory.getCompilationMXBean(); 39 | private final Collection garbageCollectorMXBeans = ManagementFactory.getGarbageCollectorMXBeans(); 40 | private final Collection memoryManagerMXBeans = ManagementFactory.getMemoryManagerMXBeans(); 41 | private final MemoryMXBean memoryMXBean = ManagementFactory.getMemoryMXBean(); 42 | // private final Collection memoryPoolMXBeans = ManagementFactory.getMemoryPoolMXBeans(); 43 | private final OperatingSystemMXBean operatingSystemMXBean = ManagementFactory.getOperatingSystemMXBean(); 44 | private final ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean(); 45 | } 46 | ``` 47 | 48 | 基于上一篇我们讲的ClassLoader的原理,那我们脑中应该有这个一张Arthas的ClassLoader图: 49 | 50 | ![Arthas原理:如何做到与应用代码隔离?](arthas-isolation1.png) 51 | 52 | 想要Arthas代码与应用的代码可同时获取到JMX等系统级的API,同时两者之间的代码是完全隔离的, 53 | ArthasClassLoader与AppClassLoader就必须共用同一个parent:ExtClassLoader,且两者要互相分属不同的ClassLoader。 54 | 55 | 对应源码如下,SystemClassLoader一般就是启动服务的AppClassLoader,它的parent就是ExtClassLoader 56 | ```java 57 | // Arthas源码: 58 | public class ArthasClassloader extends URLClassLoader { 59 | public ArthasClassloader(URL[] urls) { 60 | super(urls, ClassLoader.getSystemClassLoader().getParent()); 61 | } 62 | } 63 | ``` 64 | 65 | ## 隔离之后双方是如何工作的? 66 | 67 | 上面的架构方式保证了Arthas和目标JVM之间共享了JMX等底层API,也就是Arthas可以调用JDK的一些API获取相应的运行数据,比如说dashboard、thread、mbean等一些命令。 68 | 但还有一些命令是属于增强型的,arthas会对目标应用的代码注入一些代码,当目标应用的代码运行时,就会调用到arthas的监控,从而实现对目标应用的代码执行的监控,比如watch、tt、monitor等。 69 | 70 | 那么问题来了,我们刚刚还说arthas还是与目标应用代码隔离的,按道理来说目标应用是无法直接调用到arthas的代码的,怎么反过来目标应用执行时还能触发Arthas的监控代码呢? 71 | 72 | 应用代码想要直接调用Arthas确实是无法实现的,所以Arthas采用了一种巧妙的方案,它在目标应用和Arthas之间架起了一座桥梁,这座桥梁就是SpyAPI,Arthas源码里arthas-spy就是一个独立的模块,里面总共也就几个类。 73 | 既然是桥梁,SpyAPI就要被双方代码都能调用,也就是SpyAPI得要被双方共同的ClassLoader去加载,具体看如下源码: 74 | 75 | ```java 76 | public class ArthasBootstrap{ 77 | // Arthas源码:初始化spy 78 | private static void initSpy(Instrumentation instrumentation) throws Throwable { 79 | // 部分代码省略 80 | if (spyClass == null) { 81 | CodeSource codeSource = ArthasBootstrap.class.getProtectionDomain().getCodeSource(); 82 | if (codeSource != null) { 83 | // 获取到spy的jar的位置 84 | File arthasCoreJarFile = new File(codeSource.getLocation().toURI().getSchemeSpecificPart()); 85 | File spyJarFile = new File(arthasCoreJarFile.getParentFile(), ARTHAS_SPY_JAR); 86 | // 把spyJar添加到目标应用的BootstrapClassLoader的搜索路径中 87 | instrumentation.appendToBootstrapClassLoaderSearch(new JarFile(spyJarFile)); 88 | } else { 89 | throw new IllegalStateException("can not find " + ARTHAS_SPY_JAR); 90 | } 91 | } 92 | } 93 | } 94 | ``` 95 | 96 | 从源码中可以看出:在Arthas启动时,Arthas会把spyJar添加到目标应用的BootstrapClassLoader的搜索路径中,这样不管是Arthas还是目标应用,他们加载的都是同一个SpyAPI。 97 | 98 | 然后,在执行watch命令时,Arthas会对目标源码进行修改,假设源代码如下: 99 | 100 | ```java 101 | public class Test{ 102 | public int printTest(int paramOne) { 103 | System.out.print("param:" + paramOne); 104 | return paramOne; 105 | } 106 | } 107 | ``` 108 | 109 | 增强后,实际我们的应用代码就被动态改变为下面的代码: 110 | 111 | ![Arthas原理:如何做到与应用代码隔离?](arthas-isolation2.png) 112 | 113 | 这样,原应用代码在执行printTest方法时,就通过SpyAPI调用到Arthas的监控代码,从而实现Arthas的监控的实现。 114 | 115 | ## 总结 116 | - Arthas如何进行代码隔离:Arthas与目标应用使用不同的ClassLoader进行加载各自的代码,但双方有共用parent:ExtClassLoader 117 | - Arthas如何调用目标应用的JMX等底层代码:通过ExtClassLoader 118 | - 目标应用如何调用Arthas的监控代码:通过SpyAPI和代码增强 119 | 120 | ## 扩展:代码隔离还有其他应用吗? 121 | 122 | 代码隔离其实是有很大的实用价值,一个最实用的功能就是实现代码的插件式增减功能,比如说eclipse和IDEA的插件。 123 | 124 | 不知道大家在日常使用工具的过程中,有没有思考过这样一个问题:IDEA的插件里面代码都是独自实现的,有的可能依赖于commons-lang的版本是3.1,而其他插件依赖的commons-lang的版本可能是3.2, 125 | 插件都是在同一个IDEA里面运行的,不同的jar包版本不会产生冲突吗?如果你看懂了上面的代码隔离,那么这个问题应该不难回答了。 126 | 127 | 我们也可以自己实现ClassLoader来动态加载自己的代码,这样可动态加载或卸载不同的模块,也就是可以自己实现插件机制了。 128 | 129 | ## 如何调试Arthas命令源码? 130 | 131 | 这个问题在我刚接触arthas的时候折腾了好久。不知道原理就不知道如何去进行调试,后来看了源码才知道,其实Arthas命令都是运行在目标应用上的。 132 | 133 | 所以想要调试Arthas的命令,就需要在目标应用上: 134 | - 方式1:添加arthas依赖包,断点加在依赖包里源码上 135 | - 方式2:把Arthas项目和目标应用放在一个项目中作为不同的模块,断点加在源码上 136 | 137 | 两者其实目的都一样,都是为了能在Arthas上加断点,这样目标应用执行到命令,IDEA就可以触发断点了。 138 | 139 | 另外还有一种方式也可以调试:就是在目标应用上增加远程调试,不会远程调试的可以百度 140 | 141 | ## 最后 142 | 143 | 上述一些流程,可能说的比较抽象,并没有展示多少源码,更多的是解释了Arthas的核心思路,如果有任何疑问的,都可以在文末给我留言,我会尽快给大家回复。 144 | 145 | ### --- END ---- -------------------------------------------------------------------------------- /cm4j-hotswap/src/main/resources/solutions/arthas-isolation1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cm4j/cm4j-all/1bfca750355adc6f36096d9cf68462dba03c87c7/cm4j-hotswap/src/main/resources/solutions/arthas-isolation1.png -------------------------------------------------------------------------------- /cm4j-hotswap/src/main/resources/solutions/arthas-isolation2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cm4j/cm4j-all/1bfca750355adc6f36096d9cf68462dba03c87c7/cm4j-hotswap/src/main/resources/solutions/arthas-isolation2.png -------------------------------------------------------------------------------- /cm4j-hotswap/src/main/resources/solutions/deadlock_solution.md: -------------------------------------------------------------------------------- 1 | # 多线程并发解决方案:替换synchronized锁解决死锁 2 | 3 | ## 背景 4 | 5 | 在游戏开发过程中,多线程技术是非常重要的技术,多线程的引入最大的好处就是能解决游戏中的性能问题。 6 | 在加锁的逻辑上java提供的synchronized锁是非常简单而实用的,但随着业务逐渐增多且复杂,即使是简单的synchronized锁使用不合理也会引发死锁导致巨大的灾难。 7 | 8 | 在经历过线上的几次事故,最终引入了synchronized锁替换的解决方案,从根本上解决死锁问题。 9 | 10 | ## 最终效果 11 | 12 | 话不多说,先上优化后的代码: 13 | 14 | ```java 15 | class Test { 16 | 17 | public void test() { 18 | Object obj = new Object(); 19 | 20 | // 优化后的写法 21 | try (InternalLock ignored = Locker.getLockObjected(obj)) { 22 | System.out.println("olai olai ooo..."); 23 | LockSupport.parkNanos(TimeUnit.SECONDS.toNanos(6)); 24 | } 25 | 26 | // synchronized写法 27 | synchronized (obj) { 28 | System.out.println("olai olai ooo..."); 29 | } 30 | } 31 | } 32 | ``` 33 | 34 | 从上面对比可以看出,优化后的代码基本和synchronized写法保持一致。功能上从根本上解决了死锁问题,而且一旦发生死锁,还可以打印死锁线程和当前线程的堆栈,可以辅助快速排查问题。 35 | 36 | ## 为什么业务中倾向于使用synchronized锁 37 | 38 | ```java 39 | class LockTest { 40 | 41 | public void test() { 42 | synchronized (this) { 43 | System.out.println("这里是业务逻辑"); 44 | } 45 | } 46 | } 47 | ``` 48 | 49 | 上面这段代码,对于java开发者应该都很熟悉,最主要的原因就是:java提供的synchronized锁非常简单且实用。 50 | 51 | - 使用简单:不用特意去加锁解锁,只要把逻辑放到锁块下面即可 52 | - 只要注意加锁对象和加锁顺序 53 | - 死锁一定是嵌套锁的顺序出问题了 54 | - JDK会对synchronized的性能优化 55 | 56 | 因此:在没有特殊需求的情况下,一般推荐使用synchronized锁。 57 | 58 | 弊端: 一旦出现死锁,则锁无法释放,也就是说必须要重启服务器。这对于游戏应用来说是致命的,死锁可能会导致玩家大批量掉线,运营事故也会导致玩家流失。 59 | 60 | ## 线上的宕机事故 61 | 62 | synchronized如果只锁单个对象,是不会出现死锁的,而一旦出现死锁,那基本上就是锁嵌套且锁的执行顺序不一致导致的。 例如下面的例子就会发生死锁,运行之后,t1和t2的线程状态就会进入BLOCKED状态。 63 | 64 | ```java 65 | class Test { 66 | 67 | @Test 68 | public void deadLock_Test() { 69 | TTT o1 = new TTT(); 70 | TTT o2 = new TTT(); 71 | 72 | // 下面代码运行时会出现嵌套锁 73 | Thread t1 = new Thread(() -> { 74 | // 出现锁的情况:嵌套锁+锁的执行顺序不一样 75 | synchronized (o1) { 76 | LockSupport.parkNanos(TimeUnit.SECONDS.toNanos(1)); 77 | synchronized (o2) { 78 | System.out.println(Thread.currentThread() + "111111>>> olai olai ooo..."); 79 | 80 | } 81 | } 82 | }); 83 | 84 | Thread t2 = new Thread(() -> { 85 | synchronized (o2) { 86 | LockSupport.parkNanos(TimeUnit.SECONDS.toNanos(1)); 87 | synchronized (o1) { 88 | System.out.println(Thread.currentThread() + "111111>>> olai olai ooo..."); 89 | } 90 | } 91 | }); 92 | t1.start(); 93 | t2.start(); 94 | 95 | LockSupport.parkNanos(TimeUnit.SECONDS.toNanos(3)); 96 | System.out.println("t1状态:" + t1.getState()); // 这里会打印 BLOCKED 97 | System.out.println("t2状态:" + t2.getState()); 98 | } 99 | } 100 | ``` 101 | 102 | 真实情况下,一般也不会这么写,线上出现死锁大部分都是无意识写出来的。因为游戏业务的复杂性,且各个模块之间有互相关联,为了解决并发问题可能会针对多个对象加锁。这就为死锁埋下了隐患。 103 | 104 | 一开始只是写了锁1,锁1调外部一个方法,随着业务越来越复杂,方法里面又调其他方法,N层之后,某一个方法因为业务需要对锁2进行加锁了。如果其他业务里面有先锁2再锁1,这种情况下也就形成的嵌套锁。 105 | 如果某些条件是有条件的加锁,那这种嵌套锁就更隐蔽,而且触发概率极低,即使测试环境下也不一定复现,一旦上线,就会酿成巨大灾难。 106 | 107 | ## synchronized锁替代方案 108 | 109 | 因为synchronized锁是由jvm提供的,无法中断,所以我们可以在lock上想办法。如果想避免死锁,我们可以使用tryLock的方式,如下面代码: 110 | 111 | ```java 112 | class Test { 113 | 114 | public void test() { 115 | Lock lock = new ReentrantLock(); 116 | boolean success = lock.tryLock(3, TimeUnit.SECONDS); 117 | if (success) { 118 | try { 119 | System.out.println("这里是业务逻辑"); 120 | } finally { 121 | lock.unlock(); 122 | } 123 | } 124 | } 125 | } 126 | ``` 127 | 128 | 如果业务逻辑都需要按照上面逻辑去写,那还是挺麻烦的,能不能有一种方法类似于synchronized的写法,又能达到同样的效果?答案就是jdk提供的try-with-resource机制。 129 | 130 | ### 利用try-with-resource机制来unlock 131 | 132 | 于是我们实现了一个自定义类InternalLock,实现AutoCloseable,在close中来进行unlock操作。 133 | 同时类里面我们加了一个holdThread对象,在加锁成功后把holdThread设置为当前线程,这样一旦发生死锁,就能定位到是哪里发生了死锁。 134 | 135 | ```java 136 | public class InternalLock implements AutoCloseable { 137 | 138 | /** 139 | * 获取锁的超时时间,单位:s 140 | */ 141 | private final int LOCK_TIME_OUT = 5; 142 | 143 | private final Lock lock = new ReentrantLock(); 144 | // 当前持有锁的线程,死锁时,用来标识当前锁被哪个线程锁住了 145 | private Thread holdThread; 146 | 147 | public void tryLock() { 148 | boolean success = false; 149 | try { 150 | success = lock.tryLock(LOCK_TIME_OUT, TimeUnit.SECONDS); 151 | } catch (InterruptedException ignored) { 152 | } 153 | 154 | if (success) { 155 | holdThread = Thread.currentThread(); 156 | } else { 157 | // 堆栈信息 158 | String currentStack = ThreadUtil.getThreadStackTrace(Thread.currentThread()); 159 | String holdStack = ThreadUtil.getThreadStackTrace(holdThread); 160 | 161 | log.error("=========== 获取InternalLock超时,可能发生死锁===========\n【当前线程】堆栈:{}\n{}\n【持有锁线程】堆栈:{}\n{}", 162 | Thread.currentThread(), currentStack, holdThread, holdStack); 163 | throw new RuntimeException("获取InternalLock超时,可能发生死锁"); 164 | } 165 | } 166 | 167 | @Override 168 | public void close() { 169 | holdThread = null; 170 | lock.unlock(); 171 | } 172 | } 173 | ``` 174 | 175 | ### 如何基于对象找到锁? 176 | 177 | JDK提供了一个方法:System.identityHashCode(),可以标识出对象的唯一hashCode,不管这个对象是否覆写hashCode()方法。基于此我们就可以得到对象在jvm中唯一的对象ID,具体代码如下: 178 | 179 | ```java 180 | class Test { 181 | 182 | private static String identity(Object obj) { 183 | return obj.getClass().getName() + "@" + Integer.toHexString(System.identityHashCode(obj)); 184 | } 185 | } 186 | ``` 187 | 188 | Caffeine缓存的对象判断,最终也是基于此进行判断的,因此我们可以新增一个Caffeine缓存,key就是对象,value就是我们上面的自定义锁对象InternalLock 189 | 190 | ```java 191 | class Test { 192 | 193 | // 锁对象缓存,仅1min 194 | private static final LoadingCache lockCache = Caffeine.newBuilder() 195 | .expireAfterAccess(1, TimeUnit.MINUTES) 196 | .build(key -> new InternalLock()); 197 | } 198 | ``` 199 | 200 | ## 串联所有流程 201 | 202 | 至此,我们所有细节点都实现了,剩下来只要把上述代码串联,即可实现文章开始的加锁效果。更多细节可参考下面的示例代码。 203 | 204 | ## 示例代码github: 205 | 206 | [https://github.com/cm4j/cm4j-all](https://github.com/cm4j/cm4j-all) 207 | 208 | ### 单元测试 209 | 210 | LockerTest:提供了几种多线程死锁的单元测试 211 | 212 | ## 总结 213 | 214 | 多线程并发情况下,既想要保留synchronized锁的简单,又期望解决死锁的问题,万一发生死锁还需要快速定位死锁线程,我们需要以下几步: 215 | 216 | - 利用try-with-resource机制来实现锁的unlock 217 | - 利用System.identityHashCode()可以唯一标识出对象 218 | - 增加lock缓存,key为对象唯一标识,value为自定义的锁对象 219 | 220 | ### ---END--- -------------------------------------------------------------------------------- /cm4j-hotswap/src/main/resources/solutions/hotswap-agent.md: -------------------------------------------------------------------------------- 1 | # JAVA热更新1:Agent方式热更 2 | 3 | ## 系列介绍目录: 4 | 5 | [Java线上解决方案系列目录](//yeas.fun/archives/solution-contents) 6 | 7 | ## 正文 8 | 9 | 线上问题的解决一直是java程序员头疼的一个问题。有的应用很敏感,例如游戏行业,可能需要做到1个月都不能停服,那线上问题出了问题怎么办呢?谁也不能保证更新线上的逻辑百分百不出问题,这就需要我们在不停应用的情况下在线解决。随着技术的逐渐成熟,java社区也逐渐提供了一些线上解决方案,比如说下面3个方面: 10 | 11 | - Java热更新(热部署):不停应用的情况下,动态热更java的类,以替换线上运行逻辑; 12 | - Java代码片段执行:就是编写一段代码,然后可以线上执行。可用于恢复某些异常数据,或者执行某些规整等。当然如果代码做一些调整,也可以做到代码的替换执行,略等于代码热更新; 13 | - Java在线Debug:在线上打断点,当逻辑执行到断点之后,打印当前的线程、调用堆栈、当前类的成员变量、当前行的局部变量等信息,一切就和在本地debug一样。关键是不会影响线上的运行逻辑。 14 | 15 | 本篇主要介绍方案1:Java热更新(热部署) 顺便提一句:阿里的arthas框架的热更新就是用的这个方式 16 | 17 | ## Instrumentation功能 18 | 19 | 从JDK6开始,Java提供了一个新特性:Instrumentation功能,虽然这个接口包含的内容不多,但功能却很强大,这个接口主要提供了2个核心方法: 20 | 21 | retransformClasses():类似给method()穿了一层外衣,把内容content给覆盖了,这样每次执行都是执行的外衣的内容,既然是衣服,则可以脱掉removeTransformer() 22 | ,这样代码又恢复原始状态。一般用这个方法来进行动态方法注入。 redefineClasses(): 直接修改了方法内容content 当然这2个修改类的实现方式是有限制的,例如不允许修改方法签名,不允许增加方法参数等等。 23 | 24 | 有关于Instrumentation,网上介绍也比较多,有兴趣的朋友可以再深入研究下,许多知名的开源框架都是基于这个类进行动态的代码修改和注入的,比如阿里著名的arthas、jvm-sanbox,去哪儿旅行网的bistoury等等。这里由于篇幅,就不展开叙说了。 25 | 26 | ## 如何进行Java热更新呢 27 | 28 | 有了Instrumentation的接口,那如何调用它呢?简单点说,我们如何获取Instrumentation的实现?这里就不得不提到JDK的“代理”(agent)。JDK的agent简单点就是说在应用启动前或者应用运行时,JDK可以加载外部的一个agent包的代码,来动态修改或增强现有的代码逻辑。Instrumentation就是在agent过程中,由JVM提供的,通过 29 | Instrumentation就可以修改代码。 30 | 31 | ### Agent的两种方式: 32 | 33 | - jvm启动前:premain方式,必须在命令行指定代理jar,并且代理类必须在main方法前启动,它要求开发者在应用启动前就必须确认代理的处理逻辑和参数内容等等 34 | - jvm启动后:agentmain方式,在应用程序的VM启动后再动态添加代理的方式 因为我们需要热更线上代码,所以需要采用agentmain的方式,这种方式需要提供一个agent jar,并且这个jar需要满足2个条件: 35 | 36 | 在manifest中指定Agent-Class属性,值为代理类全路径 代理类需要提供public static void agentmain(String args, Instrumentation inst)或public static 37 | void agentmain(String args)方法。并且再二者同时存在时以前者优先。args和inst和premain中的一致。 38 | 39 | ### 如何加载Agent的jar包呢 40 | 41 | 通过JDK提供的tools.jar(存放在$JAVA_HOME/lib/tools.jar),里面有个VirtualMachine类,代码如下: 42 | 43 | ``` 44 | VirtualMachine vm = VirtualMachine.attach(当前进程ID); 45 | vm.loadAgent("对应Agent的jar包路径"); 46 | ``` 47 | 48 | 注意:tools.jar在windows和linux环境下是不同的,所以如果程序跑在Linux下,需要添加Linux的tools.jar 49 | 50 | 如果Agent的jar包符合上面所说的2个条件,则虚拟机loadAgent的时候会调用到agentmain()方法 51 | 52 | ## 总结下代码Java热更新流程 53 | 54 | - 首先项目需要添加当前进程对应的jdk的tools.jar包,因为 tools.jar提供了VirtualMachine类 55 | - VirtualMachine类在加载agent的jar包时会触发agentmain方法,这个方法里面提供了Instrumentation 56 | - 程序获取到Instrumentation之后,可以通过它进行代码的redefine或者retransform,从而实现对代码的热更新 57 | 58 | ## 依赖项目 59 | 60 | 注意:本项目依赖于cm4j-javaagent项目,请先下载并安装jar包:[https://github.com/cm4j/cm4j-javaagent](https://github.com/cm4j/cm4j-javaagent) 61 | 62 | ## 示例代码github: 63 | 64 | [https://github.com/cm4j/cm4j-all](https://github.com/cm4j/cm4j-all) 65 | 66 | ### 运行测试 67 | 68 | 运行JavaAgentTest.javaAgentTest(),结果如下: 69 | 70 | ```text 71 | 72 | [main] ERROR com.cm4j.hotswap.agent.JavaAgent - java agent:jarPath:D:\repository\com\cm4j\cm4j-javaagent\1.0-SNAPSHOT\cm4j-javaagent-1.0-SNAPSHOT.jar 73 | [main] ERROR com.cm4j.hotswap.agent.JavaAgent - current pid 17064 74 | [main] ERROR com.cm4j.hotswap.agent.JavaAgent - java agent redefine classes started 75 | 0->sun.instrument.InstrumentationImpl@2bab9351 76 | [main] ERROR com.cm4j.hotswap.agent.JavaAgent - class read from:/D:/Projects/others/cm4j-projects/cm4j-all/cm4j-hotswap/target/classes/ 77 | [main] ERROR com.cm4j.hotswap.agent.JavaAgent - class redefined:D:\Projects\others\cm4j-projects\cm4j-all\cm4j-hotswap\target\classes\com\cm4j\demo\util\DemoUtil.class 78 | [main] ERROR com.cm4j.hotswap.agent.JavaAgent - java agent redefine classes end 79 | ``` 80 | 81 | ## 线上使用 82 | 83 | 如果线上出了问题,则本地先修改好逻辑,把最新的class文件上传到服务器,然后执行上述agent热更,则程序会读取服务器上最新class,并替换jvm内部实现,从而实现不停服更改代码逻辑。 84 | 85 | ## 最后 86 | JDK的热更新解决了一大问题,但也并不是唯一的热更新方式,因此这里介绍了另一种热更新方式:[JAVA热更新2:动态加载子类热更](//yeas.fun/archives/java-hotswap-compile) 87 | 88 | 尽管热更新能解决一部分问题,但已经发生的错误数据是无法通过热更新修复的,所以我们就期望直接在线上不停服执行代码。这就是 [JAVA不停服执行代码(动态代码执行)](//yeas.fun/archives/java-eval) 89 | 90 | ### -- END-- -------------------------------------------------------------------------------- /cm4j-hotswap/src/main/resources/solutions/hotswap-compile-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cm4j/cm4j-all/1bfca750355adc6f36096d9cf68462dba03c87c7/cm4j-hotswap/src/main/resources/solutions/hotswap-compile-1.png -------------------------------------------------------------------------------- /cm4j-hotswap/src/main/resources/solutions/hotswap-compile-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cm4j/cm4j-all/1bfca750355adc6f36096d9cf68462dba03c87c7/cm4j-hotswap/src/main/resources/solutions/hotswap-compile-2.png -------------------------------------------------------------------------------- /cm4j-hotswap/src/main/resources/solutions/hotswap-compile.md: -------------------------------------------------------------------------------- 1 | # JAVA热更新2:动态加载子类热更 2 | 3 | ## 系列介绍目录: 4 | 5 | [Java线上解决方案系列目录](//yeas.fun/archives/solution-contents) 6 | 7 | ## 正文 8 | 9 | 上一篇《[JAVA热更新1:Agent方式热更](//yeas.fun/archives/hotswap-agent)》我们讲解了JDK提供的Agent方式来实现代码不停服更新, 10 | 受限于JDK的Agent一些限制,这种方式无法实现以下功能:只能修改方法体,不能变更方法签名、不能增加和删除方法/类的成员属性。 11 | 12 | 对于Instrumentation和JVM的agent,网上有不少文章,大家可以自行参考,今天我们来了解下第二种热更方式:动态加载子类热更 13 | 14 | ## 核心思路 15 | 16 | 热更新,顾名思义就是要替换代码实现。根本需求就是我怎么把改好的class替换进JVM中并实现逻辑调用。 17 | 18 | 一种办法就是直接替换代码逻辑,上一篇JVM提供的agent方式就属于此方法。那如果我们想从代码层级上实现代码替换有什么方式呢? 19 | 设计模式中有一种模式:代理模式,它的原理是对原类生成一个代理类并注册到系统中,应用层使用的是代理类,从而在代理层可以增加许多逻辑,Spring框架就是典型的应用者。 20 | 21 | 我们这里可以参考代理的思路,采用子类的方式来进行代码实现的逻辑替换。 22 | 23 | ![hotswap-compile-1.png](hotswap-compile-1.png) 24 | 25 | 如上图,Parent类中有一个方法method1(),如果我们想改变里面逻辑,可以写一个Parent的子类SubClass,然后覆写方法method1(), 26 | 这样外部调用还是Parent,但实际调用的对象替换为SubClass,即可实现代码的替换。 27 | 28 | ## 几个细节点 29 | 30 | 目前我们有了大概的思路,具体实现还有以下几个细节需要考虑: 31 | 32 | - 如何生成子类? 33 | - 生成的类如何加载进入jvm? 34 | - 代码中如何调用才能实现调用的替换? 35 | 36 | ### 如何生成子类? 37 | 38 | 我们期望的热更方式是把修改后的class上传到原路径下并覆盖,那应该如何动态生成子类呢? 39 | 40 | 关于动态生成类的开源框架有几种:asm、cglib、javaassit,各有利弊。 41 | 这里应用场景是热更新,所以对性能要求不高,但考虑到可读性和维护性,项目中尽量也不考虑直接操作字节码, 42 | 所以最终我们选择了javaassist框架,它是可以直接通过java代码来构建新类。 43 | 44 | 具体做法: 45 | 46 | 从原路径上读取修改后的class文件的二进制字节流,并通过javaassist框架构建新的class,对新class进行如下操作: 47 | - 改名,新类名为:原名+$$$SUBCLASS 48 | - 让新类继承原类 49 | - 设置子类的构造函数为public,且调用父类的默认构造函数,方便后续反射构建对象 50 | - 忽略父类里的final方法,因为final是无法继承的,覆写会导致语法报错 51 | 52 | ### 生成的类如何加载进入jvm? 53 | 54 | class想要加载进入jvm,唯一途径就是通过ClassLoader,因此这里我们自实现RecompileClassLoader继承于ClassLoader,实现二进制字节加载class进入JVM 55 | 56 | ### 对象注册机制? 57 | 58 | 现在我们已经有了一个新子类,它继承于原类,且覆写了原类的方法,那业务层怎么才能不修改代码的情况下能自动实现SubClass替换Parent实现逻辑? 59 | 60 | 解决方案就是对象注册机制,简单理解就是对象的映射关系。 我们应用层用的都是从注册机制获取的,这样进行热更时,我们只要把当前注册的对象替换为新对象,因为新对象是原对象的子类,可覆写方法,从而实现逻辑的替换。 61 | 62 | 具体类图如下: 63 | 64 | ![hotswapcompile2.png](//oss.yeas.fun/halo-yeas/hotswap-compile-2_1636702423442.png) 65 | 66 | ### 如何不停服新增功能? 67 | 68 | 通过上面流程,我们知道本方法原理就是:读取一个class文件,并动态加载进入jvm虚拟机,从而实现代码替换。 69 | 70 | 那基于上面的注册机制,那附带就有了一个新功能:动态新增注册类(也就是RegistryManager.registerNewOne())。 71 | 比如新写一个注册类,调用注册系统接口可编译新类并注册进系统中, 72 | 尤其对于游戏服务,一般的逻辑都是走消息号映射逻辑的,天然适合注册机制,这样线上可动态新增消息号和对应的实现逻辑,从而达到不停服增加功能的目的。 73 | 74 | ## 优缺点对比 75 | 76 | 两者:都支持对特定逻辑进行热更 77 | 78 | | 热更类型|优点|缺点| 79 | | --- | --- | --- | 80 | | Agent方式|对于JVM的类基本都可以热更 |只能修改方法体,不能变更方法签名、不能增加和删除方法/类的成员属性。
某些特定情况下,有极低机率导致JVM崩溃(可能是JVM的BUG,暂无法复现)| 81 | | 动态编译新类| 因为采用的是新生成,所以支持修改签名,新增方法甚至新增实现等|需要把热更的逻辑按照注册机制编写,否则无法热更| 82 | 83 | ## 总结 84 | 85 | 最终我们形成了这样的流程: 86 | 87 | - 本地修改bug,并把修改后的class上传服务器 88 | - 热更时,读取修改后的class文件,按照一定流程对原class进行重新构建,生成子类 89 | - 将子类注册到注册系统,从而实现子类的替换 90 | - 业务层基于注册系统获取的对象是原来子类,则实际调用的API是子类的实现 91 | - 基于此,还可以动态注册新的逻辑到系统中,实现不停服新增功能 92 | 93 | ## 示例代码github: 94 | 95 | [https://github.com/cm4j/cm4j-all](https://github.com/cm4j/cm4j-all) 96 | 97 | ## 运行测试 98 | 99 | 运行 RegistryManagerTest.hotswapTest(),结果如下: 100 | 101 | ```text 102 | 103 | [main] ERROR com.cm4j.registry.RegistryManagerTest - 热更新前的类:class com.cm4j.demo.util.DemoUtil 104 | [main] ERROR com.cm4j.hotswap.recompile.RecompileHotSwap - class dumpd: D:\Projects\others\cm4j-projects\cm4j-all\cm4j-hotswap\recompile-output\DemoUtil$$$SUBCLASS-20211111161652.class 105 | [main] ERROR com.cm4j.registry.AbstractRegistry - [hotswap] success:class com.cm4j.demo.util.DemoUtil -> com.cm4j.demo.util.DemoUtil$$$SUBCLASS@148080bb 106 | [main] ERROR com.cm4j.registry.RegistryManagerTest - 热更新后的类(已替换为原类的子类):class com.cm4j.demo.util.DemoUtil$$$SUBCLASS 107 | ``` 108 | 109 | 由此可见,经过热更之后,业务调用类已由DemoUtil替换为DemoUtil$$$SUBCLASS,且两者是父子关系。可有效解决外部基于对象类型判断的问题 110 | 111 | ## 最后 112 | 尽管热更新能解决一部分问题,但已经发生的错误数据是无法通过热更新修复的,所以我们就期望直接在线上不停服执行代码。这就是 [JAVA不停服执行代码(动态代码执行)](//yeas.fun/archives/java-eval) 113 | 114 | ### -- END-- -------------------------------------------------------------------------------- /cm4j-hotswap/src/main/resources/solutions/java_eval.md: -------------------------------------------------------------------------------- 1 | # JAVA不停服执行代码(动态代码执行) 2 | 3 | ## 系列介绍目录: 4 | 5 | [Java线上解决方案系列目录](//yeas.fun/archives/solution-contents) 6 | 7 | ## 背景 8 | 9 | 尽管我们有了[JAVA热更新1:Agent方式热更](//yeas.fun/archives/hotswap-agent)、[JAVA热更新2:动态加载子类热更](//yeas.fun/archives/java-hotswap-compile),能修复大部分线上的BUG,在项目上线之后,不可避免的会遇到出数据错乱的情况。之前的做法可能是提前写好一段代码,然后通过后台接口来进行调用,用以解决线上数据规整。但这种方式必须得提前写好规整逻辑,但不能覆盖所有情况。 10 | 因此我们就期望直接在线上执行一段代码,来进行我们业务数据的规整。 11 | 12 | 例如:我们直接获取用户1234的信息,然后把用户年龄改为15,然后把修改后的值返回出来。 13 | 14 | ```java 15 | public class ChangeInfoTest { 16 | 17 | public static int changeUserInfo() { 18 | UserInfoVO info = UserInfoCache.getUserInfo(1234); 19 | info.setAge(15); 20 | return info.getAge(); 21 | } 22 | } 23 | ``` 24 | 25 | ## 设计思路 26 | 27 | 如果要实现上述功能,本质上也就是我们期望写一段代码然后后在应用上执行。其实JDK的底层本身就提供了动态加载类文件的能力,它就是JavaCompiler。 28 | 29 | 如果使用JavaCompiler动态加载类文件内容,那就需要经过下述流程: 30 | 31 | - 把Java代码组装成一个格式正确的java源码,编译为class字节流 32 | - 利用ClassLoader将class字节流加载进入JVM,得到对应的class 33 | - 基于class则可以反射调用对应的逻辑 34 | 35 | ### JavaCompiler的标准工作流程 36 | 37 | 如果代码片段格式正确,我们就通过Java编译器动态编译源代码得到了class。 38 | 39 | ```java 40 | // 以下仅为示例代码,具体实际可运行代码可参考文末的示例代码 41 | public class JavaCompilerUsage { 42 | 43 | public void compileTest() throws ClassNotFoundException { 44 | // 这里设置类名和源码,content必须是符合语法规范的类文件内容 45 | String className = "", content = ""; 46 | 47 | // cl:是作为DynamicClassLoader的parent,一般是用当前应用的classloader 48 | // 主要作用是通过它来实现线上的代码对代码片段的可见性(双亲委派) 49 | ClassLoader cl = ClassLoader.getSystemClassLoader(); 50 | // 动态ClassLoader,主要用它来加载编译好的class文件 51 | DynamicClassLoader dynamicClassLoader = new DynamicClassLoader(cl); 52 | 53 | JavaCompiler javaCompiler = ToolProvider.getSystemJavaCompiler(); 54 | // 文件管理器 55 | StandardJavaFileManager standardFileManager = javaCompiler.getStandardFileManager(null, null, null); 56 | JavaFileManager fileManager = new DynamicJavaFileManager(standardFileManager, dynamicClassLoader); 57 | DiagnosticCollector collector = new DiagnosticCollector<>(); 58 | 59 | // 添加类名和对应的源码 60 | List compilationUnits = new ArrayList<>(new StringSource(className, content)); 61 | 62 | // 构建编译任务 63 | JavaCompiler.CompilationTask task = javaCompiler.getTask(null, fileManager, collector, new ArrayList<>(), null, 64 | compilationUnits); 65 | 66 | // 执行编译过程 67 | boolean result = task.call(); 68 | if (result) { 69 | // 通过dynamicClassLoader获取编译之后的类文件 70 | Map> classes = dynamicClassLoader.getClasses(); 71 | } 72 | } 73 | } 74 | ``` 75 | 76 | ### 线上如何执行代码? 77 | 78 | 得到class之后,我们想要调用class的方法,最直接的就是反射调用,相对就比较简单了,下面就是一段示例代码,直接调用类中第一个 public static 方法 79 | 80 | ```java 81 | // 以下仅为示例代码,具体实际可运行代码可参考文末的示例代码 82 | public class ClassCaller { 83 | 84 | private static Object call(Class methtClass) throws Exception { 85 | Method[] declaredMethods = methtClass.getDeclaredMethods(); 86 | 87 | for (Method declaredMethod : declaredMethods) { 88 | // 调用类中第一个public static的方法 89 | if (Modifier.isPublic(declaredMethod.getModifiers()) && Modifier.isStatic(declaredMethod.getModifiers())) { 90 | Object result = declaredMethod.invoke(null); 91 | 92 | LOG.error("JavaEval return: {}", JSON.toJSON(result)); 93 | return result; 94 | } 95 | } 96 | 97 | throw new RuntimeException("NO method is [public static], cannot eval"); 98 | } 99 | } 100 | ``` 101 | 102 | ## 问题:为什么我们写的类能调用到的目标jvm的代码? 103 | 104 | 上面我们看到 new DynamicClassLoader(cl)的时候传递了一个参数:cl,这是DynamicClassLoader的parent,也就是它的父ClassLoader。 105 | 基于ClassLoader的双亲委派的原则,子ClassLoader是可以访问父ClassLoader里面的类的,所以我们写的代码是可以直接访问到线上的代码逻辑,而不会报类不存在。 106 | 107 | 关于ClassLoader的实现细节,我们在讲Arthas的原理时会详细再讲解。 108 | 109 | ## 示例代码github: 110 | 111 | [https://github.com/cm4j/cm4j-all](https://github.com/cm4j/cm4j-all) 112 | 113 | ### 运行测试 114 | 115 | JavaEvalUtilTest.evalTest1():直接运行java源码,运行即可计算1+2得到结果3 116 | 117 | JavaEvalUtilTest.evalTest2():读取本地的一个类文件,并执行运行第一个public static 方法,结果与上一个方法同样 118 | 119 | ## 总结 120 | 121 | 我们想要线上动态执行代码来进行业务调整,需要经过以下步骤: 122 | 123 | - 实现端代码片段,里面包含自己的业务逻辑,组装成一个格式正确的java源码 124 | - 使用JavaCompiler,编译上述的字符串,并利用ClassLoader加载出对应的class 125 | - 利用反射动态调用class里面的逻辑 126 | 127 | ## 最后 128 | 129 | 当前我们有多种方式对本服线上问题进行处理,但涉及到跨服调用的时候API总是很丑,不够方便,最好的是我们能 [像本服一样调用远程代码(跨进程远程方法直调)](//yeas.fun/archives/remoting-invoke),这就是下一篇的文章内容。 130 | 131 | ### ---END--- 132 | -------------------------------------------------------------------------------- /cm4j-hotswap/src/main/resources/solutions/remoting-invoke.md: -------------------------------------------------------------------------------- 1 | # 像本服一样调用远程代码(跨进程远程方法直调) 2 | 3 | ## 系列介绍目录: 4 | 5 | [Java线上解决方案系列目录](//yeas.fun/archives/solution-contents) 6 | 7 | ## 背景 8 | 9 | 目前市面上游戏已经趋于存量竞争,玩家要求也越来越高,已经不再满足一个服内的生态,于是就越来越趋向于跨服竞争。这就对技术方面提出一个需求:像本服一样调用远程代码?也就是跨进程远程方法直调? 10 | 11 | ## 效果展示 12 | 13 | 还是老规矩,先上结果: 14 | 15 | ```java 16 | class Test { 17 | public void test() { 18 | // 第一个参数固定是服务器id,用于标识发往哪个服务器进行逻辑处理 19 | String result = TestRpc.getInstance().handle(serverId, userId, data); 20 | } 21 | } 22 | ``` 23 | 24 | ## 核心思路 25 | 26 | 想要调用远程方法,传统的方式需要以下几个步骤: 27 | 28 | - 需要先判断是否是本服请求。如果是本服,则执行本服逻辑;如果是跨服,则需要把请求发到对应服务器上; 29 | - 跨服务之间的消息通信; 30 | - 消息发送到跨服之后,如何根据参数来调用对应的代码? 31 | 32 | 于是问题就简化为: 33 | 34 | - 本服:假设要保持效果展示中的代码写法,如何来判断请求是本服调用还是发往跨服? 35 | - 传输:可采用各种远程调用方式,这里采用开源框架grpc。 36 | - 远程:消息收到后,怎么定位到具体代码并执行,还要兼顾性能? 37 | 38 | ### 本服 39 | 40 | 首先我们需要标识出哪些类的哪些方法是支持远程调用的,我们可以编写接口IRemotingClass来标识类,用注解@RemotingMethod来标识方法。 41 | 42 | ```java 43 | public class TestRpc implements IRemotingClass { 44 | 45 | @RemotingMethod 46 | public String rpcTest(int sid, String data) { 47 | System.out.println("执行线程:" + Thread.currentThread()); 48 | return "sid:" + sid + ",data:" + data; 49 | } 50 | 51 | // 获取TestRpc类的代理对象 52 | public static TestRpc getInstance() { 53 | return LocalProxyGenerator.getProxy(TestRpc.class); 54 | } 55 | } 56 | ``` 57 | 58 | 那如何标识请求是发往本服还是跨服?那我们约定:方法的第一个参数就是服务器ID。 59 | 60 | 如何在真正业务逻辑执行前加是否为本服的判断?这是典型的代理的使用场景。 61 | 所以我们在服务启动时,进行代码扫描,采用cglib框架对IRemotingClass的子类生成代理类。具体实现代码:RemotingInvokerGenerator#init(String packageScann) 62 | 63 | #### 生成代理类示例: 64 | 65 | ```java 66 | class LocalProxyGenerator { 67 | 68 | static T generateProxy(Class remotingClass) { 69 | Enhancer enhancer = new Enhancer(); 70 | enhancer.setSuperclass(remotingClass); 71 | 72 | // 注意:这里不捕获异常,这样如果出现异常会直接上抛。 73 | // 外部可统一捕获进行逻辑处理 74 | enhancer.setCallback((MethodInterceptor) (target, method, params, methodProxy) -> { 75 | String methodName = method.getName(); 76 | 77 | // 仅处理代理的方法,其他方法则走正常调用 78 | RemotingMethod annotation = method.getAnnotation(RemotingMethod.class); 79 | if (annotation == null) { 80 | return methodProxy.invokeSuper(target, params); 81 | } 82 | 83 | // 请求的第一个参数,固定为服务器ID。下面基于服务器ID来判断是否为本服请求 84 | int sid = Integer.parseInt(String.valueOf(params[0])); 85 | // 非本服:直接远程RPC调用 86 | if (sid > 0 && !GrpcServer.isSameServer(sid)) { 87 | return grpc(remotingClass, methodName, params); 88 | } 89 | 90 | // 本服,调用热更对象【非调用代理对象】 91 | return RemotingInvokerUtil.invoke(remotingClass, methodName, params); 92 | }); 93 | 94 | return (T) enhancer.create(); 95 | } 96 | } 97 | ``` 98 | 99 | 至此:基于服务器ID进行是否本服还是跨服逻辑则实现完成。 100 | 101 | ### 传输 102 | 103 | 方式有很多,可以自己实现协议传输。也可以使用开源框架,示例里面使用的是grpc。选择它原因如下: 104 | 105 | - 成熟的开源产品,使用广泛 106 | - 支持protobuf,游戏内协议也是protobuf,刚好无缝对接 107 | 108 | ### 远程 109 | 110 | 通过传输,请求类、方法、参数都传输到远程服务器上,那如何使用类和方法名字来调用方法? 111 | 112 | - 方案1:反射调用,频繁业务不建议 113 | - 方案2:参考开源的一些实现,在服务启动时,进行代码扫描,采用javaassist框架动态代码生成类和对象,基于判断来进行方法直调。 114 | 相关代码生成类:RemotingInvokerGenerator,具体实现就不细讲了,大家有兴趣可以去看下源码。注意:为了支持子类热更,我们这里调用的是***Registry类, 115 | 有不清楚原理的可以参照:[JAVA热更新2:动态加载子类热更](https://yeas.fun/archives/java-hotswap-compile) 116 | 117 | 以下是生成的class(服务启动后会dump在项目的invoker-output目录下): 118 | 119 | ```java 120 | public class TestRpcInvoker implements IRemotingInvoker { 121 | public Object invokeInternal(String var1, Object[] var2) throws Exception { 122 | if ("rpcTest".equals(var1)) { 123 | int var3 = (Integer) var2[0]; 124 | String var4 = (String) var2[1]; 125 | TestRpc var5 = (TestRpc) InvokerRegistry.getInstance().get("com.cm4j.invoke.impl.TestRpc"); 126 | String var6 = var5.rpcTest(var3, var4); 127 | return var6; 128 | } else { 129 | throw new RuntimeException("RemotingInvoker 方法没查询到:" + var1); 130 | } 131 | } 132 | 133 | public TestRpcInvoker() { 134 | } 135 | } 136 | ``` 137 | 138 | ## 关于性能 139 | 140 | 至此,三大块内容都已实现,那性能方面如何? 141 | 142 | - 根据原理方面,所有动态类和代理类都是在启动服务器时生成的,且cglib也是生成class方式直接调用的,所以理论上没有反射方面的性能消耗, 143 | - 性能消耗点还是GRPC传输 144 | - 结论:此方式的性能和GRPC的调用方式一致 145 | 146 | ## 示例代码github: 147 | 148 | [https://github.com/cm4j/cm4j-all](https://github.com/cm4j/cm4j-all) 149 | 150 | ### 单元测试代码 151 | 152 | TestRpcTest:单元测试里启动了2服的服务端,设置当前应用是1服,则可看到运行结果是跨服走grpc调用到2服逻辑,且执行线程与启动线程不一样,代表是走到grpc了 153 | 154 | ## 总结 155 | 156 | 想要像本服一样调用远程代码(跨进程远程方法直调),得有3个流程: 157 | 158 | - 本服:构建代理类 159 | - 传输:grpc 160 | - 远程:通过javaassist动态生成类来调用需要执行的逻辑 161 | 162 | ## 后续优化 163 | 164 | 此文中部分实现有点原始,后续已对其中2点进行优化,示例代码也被替换为优化后的方案,具体优化请查看:[像本服一样调用远程代码(优化版)](//yeas.fun/archives/remoting-invoke2) 165 | 166 | ## --- END --- -------------------------------------------------------------------------------- /cm4j-hotswap/src/main/resources/solutions/remoting-invoke2.md: -------------------------------------------------------------------------------- 1 | # 像本服一样调用远程代码(优化版) 2 | 3 | ## 系列介绍目录: 4 | 5 | [Java线上解决方案系列目录](//yeas.fun/archives/solution-contents) 6 | 7 | ## 背景 8 | 9 | 在上一篇 [像本服一样调用远程代码(跨进程远程方法直调)](//yeas.fun/archives/remoting-invoke)中,我们已经详细讲解了跨服方法调用的原理。 10 | 但示例代码里有一些实现方式是自己实现的方式,相对比较原始。这里对其中2点的实现进行优化 11 | 12 | ## 优化1:本地方法直调改为reflectasm框架 13 | 14 | 上一篇中调用本地方法因为性能原因,并不是利用反射,而是通过cglib框架对原有类生成字节码来进行调用,这样基本和直接调用本地方法没有区别。 15 | 16 | 对于这个功能是有成熟框架的:reflectasm,它的原理和我们的实现是一样的,也是基于asm框架来生成字节码。 17 | 源码也很简单,就5个类,有兴趣的同学可自行研究。它号称是性能更高的反射框架,这里有一篇文章是介绍其用法的:https://houbb.github.io/2018/07/20/asm-14-reflectasm 18 | 19 | ## 优化2:参数序列化方式调整为protostuff 20 | 21 | 前一篇我们对于参数的序列化和反序列化也是自实现的,代码相对比较丑陋,这里可利用protostuff去实现。我们都知道protobuf在传输前是需要先生成proto类的,但在远程方法调用时我们根本不知道传的参数是什么,而protostuff就是为了解决这个痛点。它在发送Object[] 22 | 数组时,会把参数的类型同时传过去, 而且对于一些已知类型,它会把类型映射为特定字符,这样可以极大的压缩数据量。 23 | 24 | ## 示例代码github: 25 | 26 | 因为2个优化相对比较简单,这里我就不贴源码了,大家有兴趣可以直接到示例代码去查看 27 | 28 | [https://github.com/cm4j/cm4j-all](https://github.com/cm4j/cm4j-all) 29 | 30 | ## --- END --- -------------------------------------------------------------------------------- /cm4j-hotswap/src/main/resources/solutions/singleton-module.drawio: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /cm4j-hotswap/src/main/resources/solutions/singleton-module.md: -------------------------------------------------------------------------------- 1 | # 多线程并发解决方案:单线程执行解决复杂的并发场景 2 | 3 | ## 背景:如何对复杂的业务环境加锁? 4 | 5 | 在JAVA体系中,多线程是一个比较重要的模块,同时也是一个饱受争议的模块。 6 | 一方面,合理利用多线程确实能提高计算性能,确保程序合理运行。但并非所有人都能深入理解多线程,可能不恰当的使用反而导致业务复杂,难以维护,bug滋生,这在业务复杂的环境下尤为明显。 7 | 8 | 以SLG游戏《战火与秩序》为例: 9 | 10 | ![多线程并发解决方案:单线程执行](https://oss.yeas.fun/halo-yeas/singleton-module1.jpg) 11 | 12 | 假设有1个城池,它有如下业务逻辑: 13 | - 对外出兵,则从当前城池中扣减兵力,增加出发部队的兵力 14 | - 被外部城池攻打,则战斗结算时,需要扣除自身城池损耗兵力和攻击部队的损失兵力 15 | - 盟军增援,则增援部队到达时,需要实时增援到战场中,并在战斗结束后参与结算 16 | 17 | 当然实际的游戏逻辑远比上述复杂的多,一个城池上可能同时发生的事很多,那如何保证逻辑不出错? 18 | 直观的想法就是加锁,但在一个城池上不只是本城池的兵力在变化,还涉及到其他城池的攻打部队、增援部队等。如果直接加锁,一方面可能需要对多个对象同时加锁,另一方面还会影响效率。 19 | 20 | ## 一种解决思路:整游戏单线程执行 21 | 22 | 在JAVA中,如果想要保证线程安全,就必须是同一时刻仅允许一个线程执行逻辑,不管是加锁也好,放入队列慢慢消费也罢,最终都还是单线程执行。 23 | 因此像游戏这种复杂逻辑的环境,有的框架在设计的时候就会牺牲一部分性能,从而降低业务复杂度。比如说:整个游戏就是单线程执行。 24 | 游戏毕竟不是像淘宝那种超高并发的业务,有时这种以性能换简洁的做法也是可以的。目前市面上不少游戏基本就是纯单线程执行。 25 | 26 | 整个游戏单线程执行,其实现有利有弊,这不是我们今天讨论的重点。我们需要考虑的是,在多线程的架构下,是否也能够像单线程一样编写业务逻辑而不出错? 27 | 28 | ## 另一种思路:将多线程业务转为单线程 29 | 30 | 就上述SLG的例子,因为业务都是在城池上触发的,那假设我们可以把所有的触发事件都排队放到一个单线程里面依次执行,不管是对外出兵、被攻打还是盟军增援,只要是涉及到该城池的事件, 31 | 都是按次序一个个执行,那就可以避免加锁,且逻辑也比较容易控制。于是上面的问题就转化为怎么将多线程转为单线程执行? 32 | 33 | ### 大概的思路 34 | 35 | 1. 将多线程转为单线程,我们要把不同的任务分发到不同队列(保证同名的任务一定是分发到同一个队列)。 36 | 2. 其次,队列可能设几十个,如果为每个队列单独创建一个线程消耗太大,最好是后台由一个线程池去消费队列(需控制线程池同一时刻仅有一个线程消费同一个队列)。 37 | 3. 第三,如果我们想要同步获取到异步执行的结果,这里就需要支持Future方式返回结果 38 | 39 | **整体方案如下:** 40 | 41 | ![多线程并发解决方案:单线程执行](https://oss.yeas.fun/halo-yeas/singleton-module2.png) 42 | 43 | ### 核心点1: 44 | 关于点1,我们需要把所有任务分配到不同的队列中,其中如果同名的,一定是映射到同一个队列上。这个需求就是基本的hash映射。 45 | 46 | 于是我们可以基于任务的名字,计算hash,然后拿hash与队列数量-1取模,即可得到 0到SIZE-1 之间的一个数,这就是队列的下标。 47 | 48 | ```java 49 | public class SingletonModule{ 50 | private FutureWrapper addTask(String key, Callable callable) { 51 | // 基于任务的名字,计算hash 52 | SingletonTask task = new SingletonTask<>(key, callable); 53 | int rehash = rehash(task.getHash().hashCode()); 54 | 55 | // hash与队列数量-1按位与运算,即可得到 0到SIZE-1 之间的一个数 56 | int idx = rehash & (SIZE - 1); 57 | 58 | // 根据idx一定可以获取到一个队列 59 | SingletonTaskQueue queue = queues[idx]; 60 | 61 | // 增加任务到对应的队列 62 | queue.addTask(task); 63 | // 每个事件,都有对应的future 64 | return new FutureWrapper(task.getFuture()); 65 | } 66 | } 67 | ``` 68 | 69 | ### 核心点2: 70 | 点2就是要控制线程池消费队列,但同一时刻仅允许一个线程消费同一个线程,这个状态是由isDealing变量控制。 SingletonTaskQueue就是线程池消费队列的线程执行逻辑。 71 | 72 | - 在向Queue添加任务时,如果当前队列不在被其他线程消费,则往线程池中提交消费任务 73 | - 线程的run()方法就是执行的execute()。其中有一个while循环,持续从队列获取任务进行消费,一直把当前队列消费空。最后finally会把isDealing状态设为false,代表当前队列没有线程正在消费。 74 | - 要注意finally的最后一段:有可能在 isDealing=true的时候,又往队列中新增了任务,所以需要再次检测队列是否为空,不为空则要继续消费。否则可能出现最后一个任务碰巧无法执行的情况。 75 | 76 | ```java 77 | public class SingletonTaskQueue implements Runnable { 78 | 79 | public void addTask(SingletonTask task) { 80 | boolean success = queue.offer(task); 81 | // 添加队列失败 82 | if (!success) { 83 | task.onAddQueueFailed(); 84 | return; 85 | } 86 | 87 | // 如果当前队列不在被其他线程消费,则往线程池中提交消费任务 88 | if (executable()) { 89 | ThreadPoolService.getInstance().runTask(this, threadPoolName); 90 | } 91 | } 92 | 93 | @Override 94 | public void run() { 95 | execute(); 96 | } 97 | 98 | private void execute() { 99 | try { 100 | while (true) { 101 | try { 102 | SingletonTask task = this.queue.poll(); 103 | 104 | // 没有对象,则结束循环 105 | if (task == null) { 106 | // 并发问题:如果有断点在这里 107 | // 另一个线程把event放入队列中,且因为没有获取到锁,则快速失败,当前线程也break了,则有event无法消耗 108 | // 所以:在finally段队列二次检查 109 | break; 110 | } 111 | 112 | if (LOG.isWarnEnabled()) { 113 | LOG.warn("Singleton task[{}] triggered", task.getHash()); 114 | } 115 | ListenableFutureTask future = task.getFuture(); 116 | // 同步执行 117 | future.run(); 118 | } catch (Exception e) { 119 | LOG.error("SingletonTaskQueue[{}] error", SingletonTaskQueue.this.idx, e); 120 | } 121 | } 122 | } finally { 123 | // 最后一定要把标识位修改为false 124 | this.isDealing.set(false); 125 | 126 | // 因为上面已经已经把状态isDealing重置了,如果队列里有对象,则继续放到线程池执行 127 | if (!this.queue.isEmpty() && executable()) { 128 | execute(); 129 | } 130 | } 131 | } 132 | } 133 | ``` 134 | 135 | ### 核心点3: 136 | 137 | 可能有业务中需要同步或异步获取单线程执行后的结果,这是典型的Future应用场景。所以在Task外部封装了一个future,这样task被执行后,通过Future就可以获取到结果。 138 | 139 | ```java 140 | public class SingleonTask{ 141 | public SingletonTask(String hash, Callable callable) { 142 | this.hash = hash; 143 | this.future = ListenableFutureTask.create(() -> { 144 | try { 145 | return new FutureResult<>(callable.call()); 146 | } catch (Exception e) { 147 | // 非错误码异常:则异常上报 148 | log.error("SingletonTask error", e); 149 | return FutureResult.newFutureResultWithException(e); 150 | } 151 | }); 152 | } 153 | } 154 | ``` 155 | 156 | ### 示例代码github: 157 | 158 | [https://github.com/cm4j/cm4j-all](https://github.com/cm4j/cm4j-all) 159 | 160 | #### 单元测试代码 161 | 162 | NormalSingletonModuleTest:运行可看到,我们提交了3个任务,其中2个同名的任务会单线程执行,属于同一个线程执行。而另一个不同名的任务是由单独的一个线程执行。 163 | 164 | ## 总结 165 | 166 | 通过将多线程转变为单线程执行,我们在业务中可以不进行加锁也可以控制业务逻辑高并发执行,这是应对复杂业务的一种解决思路。 167 | 168 | ## --- END --- -------------------------------------------------------------------------------- /cm4j-hotswap/src/main/resources/solutions/singleton-module1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cm4j/cm4j-all/1bfca750355adc6f36096d9cf68462dba03c87c7/cm4j-hotswap/src/main/resources/solutions/singleton-module1.jpg -------------------------------------------------------------------------------- /cm4j-hotswap/src/main/resources/solutions/singleton-module2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cm4j/cm4j-all/1bfca750355adc6f36096d9cf68462dba03c87c7/cm4j-hotswap/src/main/resources/solutions/singleton-module2.png -------------------------------------------------------------------------------- /cm4j-hotswap/src/test/java/com/cm4j/demo/Parent.java: -------------------------------------------------------------------------------- 1 | package com.cm4j.demo; 2 | 3 | /** 4 | * @author yeas.fun 5 | * @since 2021/11/3 6 | */ 7 | public class Parent { 8 | 9 | public void method1() { 10 | System.out.println("Parent"); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /cm4j-hotswap/src/test/java/com/cm4j/demo/SubClass.java: -------------------------------------------------------------------------------- 1 | package com.cm4j.demo; 2 | 3 | /** 4 | * @author yeas.fun 5 | * @since 2021/11/3 6 | */ 7 | public class SubClass extends Parent{ 8 | 9 | @Override 10 | public void method1() { 11 | System.out.println("SubClass"); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /cm4j-hotswap/src/test/java/com/cm4j/eval/JavaEvalDemo.java: -------------------------------------------------------------------------------- 1 | package com.cm4j.eval; 2 | 3 | /** 4 | * JavaEval模板 5 | */ 6 | public class JavaEvalDemo { 7 | 8 | public static Object calc() { 9 | return 1 + 2; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /cm4j-hotswap/src/test/java/com/cm4j/eval/JavaEvalUtilTest.java: -------------------------------------------------------------------------------- 1 | package com.cm4j.eval; 2 | 3 | import java.io.File; 4 | 5 | import org.apache.commons.lang3.StringUtils; 6 | import org.junit.Test; 7 | 8 | import com.google.common.base.Charsets; 9 | import com.google.common.base.Joiner; 10 | import com.google.common.io.Files; 11 | 12 | public class JavaEvalUtilTest { 13 | 14 | /** 15 | * 直接执行代码 16 | * 17 | * @throws Exception 18 | */ 19 | @Test 20 | public void evalTest1() throws Exception { 21 | String src = "public class JavaEvalDemo {\n" + "\n" + " public static Object calc() {\n" 22 | + " return 1 + 2;\n" + " }\n" + "}"; 23 | 24 | System.out.println("执行结果为:" + JavaEvalUtil.eval(src)); 25 | } 26 | 27 | /** 28 | * 读取本地的类文件,然后执行代码 29 | * 30 | * @throws Exception 31 | */ 32 | @Test 33 | public void evalTest2() throws Exception { 34 | String absolutePath = new File("").getAbsolutePath(); 35 | absolutePath = Joiner.on(File.separator).join(absolutePath, "src", "test", "java"); 36 | absolutePath += 37 | File.separator + StringUtils.replace(JavaEvalDemo.class.getName(), ".", File.separator) + ".java"; 38 | 39 | String content = Files.toString(new File(absolutePath), Charsets.UTF_8); 40 | 41 | System.out.println("执行结果为:" + JavaEvalUtil.eval(content)); 42 | } 43 | } -------------------------------------------------------------------------------- /cm4j-hotswap/src/test/java/com/cm4j/hotswap/JavaAgentTest.java: -------------------------------------------------------------------------------- 1 | package com.cm4j.hotswap; 2 | 3 | import java.io.IOException; 4 | import java.lang.instrument.UnmodifiableClassException; 5 | 6 | import org.junit.Test; 7 | 8 | import com.cm4j.demo.util.DemoUtil; 9 | import com.cm4j.hotswap.agent.JavaAgent; 10 | import com.sun.tools.attach.AgentInitializationException; 11 | import com.sun.tools.attach.AgentLoadException; 12 | import com.sun.tools.attach.AttachNotSupportedException; 13 | 14 | /** 15 | * @author yeas.fun 16 | * @since 2021/11/4 17 | */ 18 | public class JavaAgentTest { 19 | 20 | @Test 21 | public void javaAgentTest() 22 | throws UnmodifiableClassException, AgentLoadException, IOException, AttachNotSupportedException, ClassNotFoundException, AgentInitializationException { 23 | JavaAgent.javaAgent(new String[]{DemoUtil.class.getName()}); 24 | } 25 | 26 | } -------------------------------------------------------------------------------- /cm4j-hotswap/src/test/java/com/cm4j/invoke/impl/TestRpcTest.java: -------------------------------------------------------------------------------- 1 | package com.cm4j.invoke.impl; 2 | 3 | import com.cm4j.grpc.config.GrpcConfig; 4 | import com.cm4j.grpc.server.GrpcServer; 5 | import com.cm4j.registry.AbstractRegistry; 6 | import com.cm4j.registry.registry.InvokerRegistry; 7 | import com.cm4j.util.RemotingInvokerUtil; 8 | import org.junit.Before; 9 | import org.junit.Test; 10 | 11 | import java.lang.reflect.Constructor; 12 | 13 | public class TestRpcTest { 14 | 15 | @Before 16 | public void init() throws Exception { 17 | // 手动配置grpc参数 18 | GrpcConfig.addAddressPort(1, "127.0.0.1", 6666); 19 | GrpcConfig.addAddressPort(2, "127.0.0.1", 8888); 20 | 21 | // 启动InvokerRegistry注册,这里是反射调用 22 | // 正常业务:应该用spring注册,或者包扫描进行注册 23 | startRegistry(InvokerRegistry.class); 24 | 25 | // 启动2服的服务器 26 | GrpcServer grpcServer = new GrpcServer(8888); 27 | 28 | // 远程方法调用初始化 29 | RemotingInvokerUtil.init(); 30 | } 31 | 32 | /** 33 | * 启动注册类,用于测试 34 | * 35 | * @param clazz 36 | */ 37 | public static void startRegistry(Class clazz) { 38 | try { 39 | Constructor constructor = clazz.getDeclaredConstructor(); 40 | constructor.setAccessible(true); 41 | constructor.newInstance(); 42 | } catch (Exception e) { 43 | e.printStackTrace(); 44 | } 45 | } 46 | 47 | @Test 48 | public void rpcTest() { 49 | // 假设当前是1服 50 | GrpcServer.setServerId(1); 51 | 52 | System.out.println("启动线程:" + Thread.currentThread()); 53 | 54 | // 本服:直接执行逻辑。打印中方法执行线程就是启动线程 55 | TestRpc.getInstance().rpcTest(1, "1234"); 56 | 57 | // 跨服:请求2服的数据。打印方法中执行线程是另一个线程 58 | String result = TestRpc.getInstance().rpcTest(2, "1234"); 59 | System.out.println(result); 60 | } 61 | } -------------------------------------------------------------------------------- /cm4j-hotswap/src/test/java/com/cm4j/lock/LockerTest.java: -------------------------------------------------------------------------------- 1 | package com.cm4j.lock; 2 | 3 | import org.junit.Test; 4 | 5 | import java.util.concurrent.TimeUnit; 6 | import java.util.concurrent.locks.LockSupport; 7 | 8 | /** 9 | * @author yeas.fun 10 | * @since 2021/11/29 11 | */ 12 | public class LockerTest { 13 | 14 | /** 15 | * 基于对象使用锁 16 | */ 17 | @Test 18 | public void test2() { 19 | TTT obj = new TTT(); 20 | 21 | new Thread(() -> { 22 | try (InternalLock ignored = Locker.getLock(obj)) { 23 | System.out.println(Thread.currentThread() + ">>> olai olai ooo..."); 24 | LockSupport.parkNanos(TimeUnit.SECONDS.toNanos(6)); 25 | } 26 | }).start(); 27 | new Thread(() -> { 28 | try (InternalLock ignored = Locker.getLock(obj)) { 29 | System.out.println(Thread.currentThread() + ">>> olai olai ooo..."); 30 | LockSupport.parkNanos(TimeUnit.SECONDS.toNanos(6)); 31 | } 32 | }).start(); 33 | LockSupport.park(); 34 | } 35 | 36 | @Test 37 | public void test3() { 38 | System.out.println(System.identityHashCode(new TTT())); 39 | System.out.println(System.identityHashCode(new TTT())); 40 | } 41 | 42 | /** 43 | * 嵌套锁测试 44 | */ 45 | @Test 46 | public void deadLockTest() { 47 | TTT o1 = new TTT(); 48 | TTT o2 = new TTT(); 49 | 50 | // 下面代码运行时会出现嵌套锁 51 | Thread t1 = new Thread(() -> { 52 | try (InternalLock ignored = Locker.getLock(o1)) { 53 | LockSupport.parkNanos(TimeUnit.SECONDS.toNanos(1)); 54 | try (InternalLock ignored2 = Locker.getLock(o2)) { 55 | System.out.println(Thread.currentThread() + "111111>>> olai olai ooo..."); 56 | } 57 | } 58 | }); 59 | 60 | Thread t2 = new Thread(() -> { 61 | try (InternalLock ignored = Locker.getLock(o2)) { 62 | LockSupport.parkNanos(TimeUnit.SECONDS.toNanos(1)); 63 | try (InternalLock ignored2 = Locker.getLock(o1)) { 64 | System.out.println(Thread.currentThread() + "222222>>> olai olai ooo..."); 65 | } 66 | } 67 | }); 68 | t1.start(); 69 | t2.start(); 70 | 71 | LockSupport.parkNanos(TimeUnit.SECONDS.toNanos(10)); 72 | System.out.println("t1状态:" + t1.getState()); 73 | System.out.println("t2状态:" + t2.getState()); 74 | } 75 | 76 | private static class TTT { 77 | 78 | @Override 79 | public int hashCode() { 80 | return 123456; 81 | } 82 | } 83 | } -------------------------------------------------------------------------------- /cm4j-hotswap/src/test/java/com/cm4j/registry/RegistryManagerTest.java: -------------------------------------------------------------------------------- 1 | package com.cm4j.registry; 2 | 3 | import java.lang.reflect.Constructor; 4 | 5 | import org.junit.Assert; 6 | import org.junit.Before; 7 | import org.junit.Test; 8 | import org.slf4j.Logger; 9 | import org.slf4j.LoggerFactory; 10 | 11 | import com.cm4j.demo.UtilRegistry; 12 | import com.cm4j.demo.util.DemoUtil; 13 | 14 | /** 15 | * @author yeas.fun 16 | * @since 2021/11/8 17 | */ 18 | public class RegistryManagerTest { 19 | 20 | private static final Logger log = LoggerFactory.getLogger(RegistryManagerTest.class); 21 | 22 | @Before 23 | public void init() { 24 | startRegistry(UtilRegistry.class); 25 | } 26 | 27 | @Test 28 | public void hotswapTest() throws Exception { 29 | Class oldClass = DemoUtil.getInstance().getClass(); 30 | log.error("热更新前的类:{}" , oldClass); 31 | 32 | // 热更新 33 | RegistryManager.getInstance().hotswap(new String[]{DemoUtil.class.getName()}); 34 | 35 | Class newClass = DemoUtil.getInstance().getClass(); 36 | log.error("热更新后的类(已替换为原类的子类):{}" , newClass); 37 | 38 | // newClass是oldClass的子类 39 | Assert.assertTrue(oldClass.isAssignableFrom(newClass)); 40 | } 41 | 42 | /** 43 | * 启动注册类,用于测试

44 |      * 注意:一般该类需使用spring注解进行构建,当前测试类没配置spring启动,所以这里直接初始化
45 |      *
46 |      * @param clazz
47 |      */
48 |     private static void startRegistry(Class clazz) {
49 |         try {
50 |             Constructor constructor = clazz.getDeclaredConstructor();
51 |             constructor.setAccessible(true);
52 |             constructor.newInstance();
53 |         } catch (Exception e) {
54 |             e.printStackTrace();
55 |         }
56 |     }
57 | }


--------------------------------------------------------------------------------
/cm4j-hotswap/src/test/java/com/cm4j/singleton/impl/NormalSingletonModuleTest.java:
--------------------------------------------------------------------------------
 1 | package com.cm4j.singleton.impl;
 2 | 
 3 | import com.cm4j.singleton.FutureSupport;
 4 | import com.cm4j.singleton.FutureWrapper;
 5 | import com.cm4j.singleton.SingletonEnum;
 6 | import com.cm4j.thread.ThreadPoolServiceTest;
 7 | import org.junit.BeforeClass;
 8 | import org.junit.Test;
 9 | 
10 | import java.util.concurrent.TimeUnit;
11 | 
12 | /**
13 |  * 单线程执行:单元测试
14 |  *
15 |  * @author yeas.fun
16 |  * @since 2022/3/17
17 |  */
18 | public class NormalSingletonModuleTest {
19 | 
20 |     @BeforeClass
21 |     public static void before() {
22 |         // 启动线程池
23 |         ThreadPoolServiceTest.initThreadPoolService();
24 |     }
25 | 
26 |     @Test
27 |     public void test() throws InterruptedException {
28 |         NormalSingletonModule.getInstance().addTask(SingletonEnum.TEST_BUSINESS, "1234", () -> {
29 |             System.out.println("当前线程1:" + Thread.currentThread());
30 |             TimeUnit.SECONDS.sleep(1);
31 |             return null;
32 |         });
33 | 
34 | 
35 |         NormalSingletonModule.getInstance().addTask(SingletonEnum.TEST_BUSINESS, "1234", () -> {
36 |             System.out.println("当前线程2:" + Thread.currentThread());
37 |             TimeUnit.SECONDS.sleep(1);
38 |             return null;
39 |         });
40 | 
41 |         // 上述2个单线程执行,因为模块一样,执行线程一致
42 | 
43 |         // 下面的key不一样,则线程不一样
44 |         FutureWrapper future = NormalSingletonModule.getInstance().addTask(SingletonEnum.TEST_BUSINESS, "1111", () -> {
45 |             System.out.println("当前线程3:" + Thread.currentThread());
46 |             TimeUnit.SECONDS.sleep(1);
47 |             return "abcd";
48 |         });
49 | 
50 |         // 同步获取结果
51 |         String result = FutureSupport.get(future);
52 |         System.out.println("result:" + result);
53 | 
54 |         TimeUnit.SECONDS.sleep(3);
55 |     }
56 | }


--------------------------------------------------------------------------------
/cm4j-hotswap/src/test/java/com/cm4j/thread/ThreadPoolServiceTest.java:
--------------------------------------------------------------------------------
 1 | package com.cm4j.thread;
 2 | 
 3 | import com.google.common.collect.Lists;
 4 | 
 5 | import static org.junit.Assert.*;
 6 | 
 7 | /**
 8 |  * @author yeas.fun
 9 |  * @since 2022/3/17
10 |  */
11 | public class ThreadPoolServiceTest {
12 | 
13 |     /**
14 |      * 初始化线程池
15 |      */
16 |     public static void initThreadPoolService() {
17 |         ThreadPoolConfig config = new ThreadPoolConfig();
18 |         config.setKeepAlive(60);
19 |         config.setPoolName(ThreadPoolName.SINGLETON.name());
20 |         config.setMinThreads(10);
21 |         config.setMaxThreads(80);
22 |         config.setQueueType(1);
23 |         config.setMaxQueues(100);
24 |         config.setPriority(5);
25 |         config.setPolicy(ThreadPoolRejectedPolicy.RejectedPolicy.DiscardPolicy);
26 | 
27 |         ThreadPoolService.getInstance().setConfigPools(Lists.newArrayList(config));
28 |         ThreadPoolService.getInstance().init();
29 |     }
30 | }


--------------------------------------------------------------------------------
/cm4j-hotswap/src/test/java/com/cm4j/util/PackageUtilTest.java:
--------------------------------------------------------------------------------
 1 | package com.cm4j.util;
 2 | 
 3 | import static org.junit.Assert.*;
 4 | 
 5 | import java.util.Set;
 6 | 
 7 | import org.junit.Assert;
 8 | import org.junit.Test;
 9 | 
10 | /**
11 |  * @author yeas.fun
12 |  * @since 2021/11/5
13 |  */
14 | public class PackageUtilTest {
15 | 
16 |     @Test
17 |     public void findPackageClass() {
18 |         Set> packageClass = PackageUtil.findPackageClass("com.cm4j");
19 |         assertFalse(packageClass.isEmpty());
20 |     }
21 | }


--------------------------------------------------------------------------------
/pom.xml:
--------------------------------------------------------------------------------
  1 | 
  2 | 
  5 |   4.0.0
  6 | 
  7 |   com.cm4j
  8 |   cm4j-prj
  9 |   pom
 10 |   1.0-SNAPSHOT
 11 | 
 12 |   
 13 |     cm4j-hotswap
 14 |   
 15 | 
 16 |   
 17 |     1.8
 18 |     8
 19 |     8
 20 | 
 21 |     1.34.1
 22 |     2.8.5
 23 |   
 24 | 
 25 |   
 26 |     
 27 |       
 28 |       
 29 |         commons-beanutils
 30 |         commons-beanutils
 31 |         1.8.0
 32 |       
 33 |       
 34 |         commons-codec
 35 |         commons-codec
 36 |         1.6
 37 |       
 38 |       
 39 |         commons-collections
 40 |         commons-collections
 41 |         3.2
 42 |       
 43 |       
 44 |         commons-io
 45 |         commons-io
 46 |         2.1
 47 |       
 48 |       
 49 |         org.apache.commons
 50 |         commons-pool2
 51 |         2.4.2
 52 |       
 53 |       
 54 |         commons-logging
 55 |         commons-logging
 56 |         1.2
 57 |       
 58 |       
 59 |         org.apache.commons
 60 |         commons-lang3
 61 |         3.4
 62 |       
 63 | 
 64 |       
 65 |         commons-net
 66 |         commons-net
 67 |         3.6
 68 |       
 69 |       
 70 |         org.apache.commons
 71 |         commons-math3
 72 |         3.6.1
 73 |       
 74 | 
 75 |       
 76 |       
 77 |         ch.qos.logback
 78 |         logback-classic
 79 |         1.2.3
 80 |       
 81 |       
 82 |         ch.qos.logback
 83 |         logback-core
 84 |         1.2.3
 85 |       
 86 |       
 87 |         org.logback-extensions
 88 |         logback-ext-spring
 89 |         0.1.4
 90 |       
 91 | 
 92 |       
 93 |         jdk.tools
 94 |         jdk.tools
 95 |         ${jdk.tools.version}
 96 |         system
 97 |         ${java.home}/../lib/tools.jar
 98 |       
 99 | 
100 |       
101 |       
102 |         com.google.guava
103 |         guava
104 |         28.2-jre
105 |       
106 |       
107 |         com.google.protobuf
108 |         protobuf-java
109 |         3.2.0
110 |       
111 |       
112 |         com.google.protobuf
113 |         protobuf-java-util
114 |         3.2.0
115 |       
116 |       
121 |       
122 |         io.grpc
123 |         grpc-netty-shaded
124 |         ${grpc.version}
125 |       
126 |       
127 |         io.grpc
128 |         grpc-protobuf
129 |         ${grpc.version}
130 |       
131 |       
132 |         io.grpc
133 |         grpc-stub
134 |         ${grpc.version}
135 |       
136 |       
137 |         com.github.ben-manes.caffeine
138 |         caffeine
139 |         ${caffeine.version}
140 |       
141 | 
142 |       
143 |         org.javassist
144 |         javassist
145 |         3.21.0-GA
146 |       
147 | 
148 |       
149 |       
150 |         com.alibaba
151 |         fastjson
152 |         1.2.75
153 |       
154 | 
155 |       
156 |         org.ow2.asm
157 |         asm
158 |         7.0
159 |       
160 |       
161 |         com.github.ben-manes.caffeine
162 |         caffeine
163 |         2.8.5
164 |       
165 |       
166 |         com.esotericsoftware
167 |         reflectasm
168 |         1.11.9
169 |       
170 |       
171 |         io.protostuff
172 |         protostuff-core
173 |         1.8.0
174 |       
175 |       
176 |         io.protostuff
177 |         protostuff-runtime
178 |         1.8.0
179 |       
180 | 
181 |       
182 |       
183 |         cglib
184 |         cglib-nodep
185 |         3.3.0
186 |       
187 | 
188 |       
189 |       
190 |         junit
191 |         junit
192 |         4.12
193 |         test
194 |       
195 | 
196 |     
197 |   
198 | 
199 |   
200 |     
201 |       
202 |         
203 |           org.apache.maven.plugins
204 |           maven-jar-plugin
205 |           2.5
206 |         
207 |       
208 |     
209 |   
210 | 
211 | 


--------------------------------------------------------------------------------