From 5ca7e69170cc9efde0106a25058bb62c046342c1 Mon Sep 17 00:00:00 2001 From: Jermaine Hua Date: Mon, 23 Sep 2024 18:25:36 +0800 Subject: [PATCH] Automatically exposes jdk modules (#1340) * dynamic export Signed-off-by: JermaineHua * Add test for export all Signed-off-by: JermaineHua * Support auto module switch and uts Signed-off-by: JermaineHua * Auto module export with reentrant protection Signed-off-by: JermaineHua --------- Signed-off-by: JermaineHua --- sofa-boot-project/sofa-boot/pom.xml | 6 + ...leExportApplicationContextInitializer.java | 48 ++++ .../com/alipay/sofa/boot/util/ModuleUtil.java | 253 ++++++++++++++++++ .../com/alipay/sofa/boot/util/UnsafeUtil.java | 61 +++++ .../main/resources/META-INF/spring.factories | 3 +- ...ortApplicationContextInitializerTests.java | 76 ++++++ .../sofa/boot/util/ModuleUtilTests.java | 50 ++++ 7 files changed, 496 insertions(+), 1 deletion(-) create mode 100644 sofa-boot-project/sofa-boot/src/main/java/com/alipay/sofa/boot/Initializer/AutoModuleExportApplicationContextInitializer.java create mode 100644 sofa-boot-project/sofa-boot/src/main/java/com/alipay/sofa/boot/util/ModuleUtil.java create mode 100644 sofa-boot-project/sofa-boot/src/main/java/com/alipay/sofa/boot/util/UnsafeUtil.java create mode 100644 sofa-boot-project/sofa-boot/src/test/java/com/alipay/sofa/boot/initializer/AutoModuleExportApplicationContextInitializerTests.java create mode 100644 sofa-boot-project/sofa-boot/src/test/java/com/alipay/sofa/boot/util/ModuleUtilTests.java diff --git a/sofa-boot-project/sofa-boot/pom.xml b/sofa-boot-project/sofa-boot/pom.xml index 46ea0a1d2..b4385cc3e 100644 --- a/sofa-boot-project/sofa-boot/pom.xml +++ b/sofa-boot-project/sofa-boot/pom.xml @@ -46,6 +46,12 @@ test + + org.mockito + mockito-inline + test + + diff --git a/sofa-boot-project/sofa-boot/src/main/java/com/alipay/sofa/boot/Initializer/AutoModuleExportApplicationContextInitializer.java b/sofa-boot-project/sofa-boot/src/main/java/com/alipay/sofa/boot/Initializer/AutoModuleExportApplicationContextInitializer.java new file mode 100644 index 000000000..3460426c9 --- /dev/null +++ b/sofa-boot-project/sofa-boot/src/main/java/com/alipay/sofa/boot/Initializer/AutoModuleExportApplicationContextInitializer.java @@ -0,0 +1,48 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alipay.sofa.boot.Initializer; + +import com.alipay.sofa.boot.util.ModuleUtil; +import org.springframework.context.ApplicationContextInitializer; +import org.springframework.context.ConfigurableApplicationContext; + +/** + * @author huazhongming + * @since 4.4.0 + */ +public class AutoModuleExportApplicationContextInitializer + implements + ApplicationContextInitializer { + + private static final String AUTO_MODULE_JDK_ENABLE_KEY = "sofa.boot.auto.module.export.jdk.enable"; + private static final String AUTO_MODULE_ALL_ENABLE_KEY = "sofa.boot.auto.module.export.all.enable"; + + @Override + public void initialize(ConfigurableApplicationContext applicationContext) { + if (isEnable(applicationContext, AUTO_MODULE_ALL_ENABLE_KEY, "false")) { + ModuleUtil.exportAllModulePackageToAll(); + } else if (isEnable(applicationContext, AUTO_MODULE_JDK_ENABLE_KEY, "true")) { + ModuleUtil.exportAllJDKModulePackageToAll(); + } + } + + protected boolean isEnable(ConfigurableApplicationContext applicationContext, String key, + String defaultValue) { + String switchStr = applicationContext.getEnvironment().getProperty(key, defaultValue); + return Boolean.parseBoolean(switchStr); + } +} diff --git a/sofa-boot-project/sofa-boot/src/main/java/com/alipay/sofa/boot/util/ModuleUtil.java b/sofa-boot-project/sofa-boot/src/main/java/com/alipay/sofa/boot/util/ModuleUtil.java new file mode 100644 index 000000000..5e2c20146 --- /dev/null +++ b/sofa-boot-project/sofa-boot/src/main/java/com/alipay/sofa/boot/util/ModuleUtil.java @@ -0,0 +1,253 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alipay.sofa.boot.util; + +import com.alipay.sofa.boot.log.SofaBootLoggerFactory; +import org.slf4j.Logger; + +import java.lang.invoke.MethodHandle; +import java.lang.reflect.Field; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * @author huazhongming + * @since 4.4.0 + */ +public class ModuleUtil { + + private static final Logger LOGGER = SofaBootLoggerFactory + .getLogger(ModuleUtil.class); + + private static final MethodHandle implAddOpensToAllUnnamed; + + private static final MethodHandle implAddOpens; + + private static final MethodHandle implAddExportsToAllUnnamed; + + private static final MethodHandle implAddExports; + + private static final Map nameToModules; + + private static final AtomicBoolean isExported = new AtomicBoolean(false); + + static { + implAddOpensToAllUnnamed = createModuleMethodHandle("implAddOpensToAllUnnamed", + String.class); + implAddOpens = createModuleMethodHandle("implAddOpens", String.class); + implAddExportsToAllUnnamed = createModuleMethodHandle("implAddExportsToAllUnnamed", + String.class); + implAddExports = createModuleMethodHandle("implAddExports", String.class); + nameToModules = getNameToModule(); + } + + /** + * Export all JDK module packages to all. + */ + public static void exportAllJDKModulePackageToAll() { + try { + if (isExported.compareAndSet(false,true) && nameToModules != null) { + nameToModules.forEach((name, module) -> module.getPackages().forEach(pkgName -> { + if (isJDKModulePackage(pkgName)) { + addOpensToAll(module, pkgName); + addExportsToAll(module, pkgName); + } + })); + } + } catch (Throwable t) { + LOGGER.error("Failed to export all JDK module package to all", t); + } + } + + private static boolean isJDKModulePackage(String modulePackageName) { + return modulePackageName.startsWith("java.") || modulePackageName.startsWith("jdk."); + } + + /** + * Export all module packages to all. + */ + public static void exportAllModulePackageToAll() { + try { + if (isExported.compareAndSet(false,true) && nameToModules != null) { + nameToModules.forEach((name, module) -> module.getPackages().forEach(pkgName -> { + addOpensToAll(module, pkgName); + addExportsToAll(module, pkgName); + })); + } + } catch (Throwable t) { + LOGGER.error("Failed to export all module package to all", t); + } + } + + /** + * Updates this module to open a package to all unnamed modules. + * + * @param moduleName + * @param packageName + */ + public static boolean addOpensToAllUnnamed(String moduleName, String packageName) { + return invokeModuleMethod(implAddOpensToAllUnnamed, moduleName, packageName); + } + + /** + * Updates this module to open a package to all unnamed modules. + * + * @param module + * @param packageName + */ + public static boolean addOpensToAllUnnamed(Module module, String packageName) { + return invokeModuleMethod(implAddOpensToAllUnnamed, module, packageName); + } + + /** + * Updates this module to export a package to all unnamed modules. + * + * @param moduleName + * @param packageName + */ + public static boolean addExportsToAllUnnamed(String moduleName, String packageName) { + return invokeModuleMethod(implAddExportsToAllUnnamed, moduleName, packageName); + } + + /** + * Updates this module to export a package to all unnamed modules. + * + * @param module + * @param packageName + */ + public static boolean addExportsToAllUnnamed(Module module, String packageName) { + return invokeModuleMethod(implAddExportsToAllUnnamed, module, packageName); + } + + /** + * Updates this module to open a package to another module. + * + * @param moduleName + * @param packageName + */ + public static boolean addOpensToAll(String moduleName, String packageName) { + + return invokeModuleMethod(implAddOpens, moduleName, packageName); + } + + /** + * Updates this module to open a package to another module. + * + * @param module + * @param packageName + */ + public static boolean addOpensToAll(Module module, String packageName) { + + return invokeModuleMethod(implAddOpens, module, packageName); + } + + /** + * Updates this module to export a package unconditionally. + * @param moduleName + * @param packageName + */ + public static boolean addExportsToAll(String moduleName, String packageName) { + return invokeModuleMethod(implAddExports, moduleName, packageName); + } + + /** + * Updates this module to export a package unconditionally. + * @param module + * @param packageName + */ + public static boolean addExportsToAll(Module module, String packageName) { + return invokeModuleMethod(implAddExports, module, packageName); + } + + /** + * invoke ModuleLayer method + * + * @param method + * @param moduleName + * @param packageName + * @return + */ + public static boolean invokeModuleMethod(MethodHandle method, String moduleName, + String packageName) { + Optional findModule = ModuleLayer.boot().findModule(moduleName); + if (findModule.isPresent()) { + try { + return invokeModuleMethod(method, findModule.get(), packageName); + } catch (Throwable t) { + LOGGER.error("Failed to invoke ModuleLayer method: {}", method, t); + } + } + return false; + } + + public static boolean invokeModuleMethod(MethodHandle method, Module module, String packageName) { + try { + method.invoke(module, packageName); + return true; + } catch (Throwable t) { + LOGGER.error("Failed to invoke Module method: {}", method, t); + } + return false; + } + + /** + * create MethodHandle from Module + * + * @param methodName + * @param parameterTypes + * @return MethodHandle + */ + private static MethodHandle createModuleMethodHandle(String methodName, + Class... parameterTypes) { + try { + return UnsafeUtil.implLookup().unreflect( + Module.class.getDeclaredMethod(methodName, parameterTypes)); + } catch (Throwable t) { + LOGGER.error("Failed to create Module method handle: {}", methodName, t); + } + return null; + } + + /** + * Get ModuleLayer.bootLayer field value + * + * @param fieldName + * @return field value + */ + private static Object getModuleLayerFieldsValue(String fieldName) { + ModuleLayer moduleLayer = ModuleLayer.boot(); + try { + Class moduleLayerClass = ModuleLayer.class; + Field field = moduleLayerClass.getDeclaredField(fieldName); + return UnsafeUtil.implLookup().unreflectVarHandle(field).get(moduleLayer); + } catch (Throwable t) { + LOGGER.error("Failed to get ModuleLayer field value: {}", fieldName, t); + } + return null; + } + + /** + * Get all modules from System.bootLayer + * + * @return modules + */ + @SuppressWarnings("unchecked") + public static Map getNameToModule() { + return (Map) getModuleLayerFieldsValue("nameToModule"); + } +} diff --git a/sofa-boot-project/sofa-boot/src/main/java/com/alipay/sofa/boot/util/UnsafeUtil.java b/sofa-boot-project/sofa-boot/src/main/java/com/alipay/sofa/boot/util/UnsafeUtil.java new file mode 100644 index 000000000..0bbbca199 --- /dev/null +++ b/sofa-boot-project/sofa-boot/src/main/java/com/alipay/sofa/boot/util/UnsafeUtil.java @@ -0,0 +1,61 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alipay.sofa.boot.util; + +import sun.misc.Unsafe; + +import java.lang.invoke.MethodHandles; +import java.lang.reflect.Field; + +/** + * @author huazhongming + * @since 4.4.0 + */ +public class UnsafeUtil { + private static Unsafe UNSAFE; + private static MethodHandles.Lookup IMPL_LOOKUP; + + public static Unsafe unsafe() { + if (UNSAFE == null) { + Unsafe unsafe = null; + try { + Field theUnsafeField = Unsafe.class.getDeclaredField("theUnsafe"); + theUnsafeField.setAccessible(true); + unsafe = (Unsafe) theUnsafeField.get(null); + } catch (Throwable ignored) { + } + UNSAFE = unsafe; + } + + return UNSAFE; + } + + public static MethodHandles.Lookup implLookup() { + if (IMPL_LOOKUP == null) { + Class lookupClass = MethodHandles.Lookup.class; + + try { + Field implLookupField = lookupClass.getDeclaredField("IMPL_LOOKUP"); + long offset = unsafe().staticFieldOffset(implLookupField); + IMPL_LOOKUP = (MethodHandles.Lookup) unsafe().getObject( + unsafe().staticFieldBase(implLookupField), offset); + } catch (Throwable ignored) { + } + } + return IMPL_LOOKUP; + } +} diff --git a/sofa-boot-project/sofa-boot/src/main/resources/META-INF/spring.factories b/sofa-boot-project/sofa-boot/src/main/resources/META-INF/spring.factories index 1aea1f8c4..9ed35930a 100644 --- a/sofa-boot-project/sofa-boot/src/main/resources/META-INF/spring.factories +++ b/sofa-boot-project/sofa-boot/src/main/resources/META-INF/spring.factories @@ -15,4 +15,5 @@ org.springframework.boot.SpringApplicationRunListener=\ # Initializers org.springframework.context.ApplicationContextInitializer=\ - com.alipay.sofa.boot.compatibility.CompatibilityVerifierApplicationContextInitializer + com.alipay.sofa.boot.compatibility.CompatibilityVerifierApplicationContextInitializer,\ + com.alipay.sofa.boot.Initializer.AutoModuleExportApplicationContextInitializer diff --git a/sofa-boot-project/sofa-boot/src/test/java/com/alipay/sofa/boot/initializer/AutoModuleExportApplicationContextInitializerTests.java b/sofa-boot-project/sofa-boot/src/test/java/com/alipay/sofa/boot/initializer/AutoModuleExportApplicationContextInitializerTests.java new file mode 100644 index 000000000..e9d30fa48 --- /dev/null +++ b/sofa-boot-project/sofa-boot/src/test/java/com/alipay/sofa/boot/initializer/AutoModuleExportApplicationContextInitializerTests.java @@ -0,0 +1,76 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alipay.sofa.boot.initializer; + +import com.alipay.sofa.boot.Initializer.AutoModuleExportApplicationContextInitializer; +import com.alipay.sofa.boot.util.ModuleUtil; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; + +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.times; + +/** + * @author huazhongming + * @since 4.4.0 + */ +public class AutoModuleExportApplicationContextInitializerTests { + + private ApplicationContextRunner contextRunner; + + @BeforeEach + void setUp() { + contextRunner = new ApplicationContextRunner() + .withInitializer(new AutoModuleExportApplicationContextInitializer()); + } + + @Test + void jdkDefaultTrue(){ + + try (MockedStatic mockedStatic = mockStatic(ModuleUtil.class)) { + contextRunner.withPropertyValues().run(applicationContext -> {}); + mockedStatic.verify(ModuleUtil::exportAllJDKModulePackageToAll, times(1)); + } + } + + @Test + void allDefaultFalse(){ + try (MockedStatic mockedStatic = mockStatic(ModuleUtil.class)) { + contextRunner.withPropertyValues().run(applicationContext -> {}); + mockedStatic.verify(ModuleUtil::exportAllModulePackageToAll, times(0)); + } + } + + @Test + void jdkDisable(){ + + try (MockedStatic mockedStatic = mockStatic(ModuleUtil.class)) { + contextRunner.withPropertyValues("sofa.boot.auto.module.export.jdk.enable=false").run(applicationContext -> {}); + mockedStatic.verify(ModuleUtil::exportAllJDKModulePackageToAll, times(0)); + } + } + + @Test + void allEnable(){ + try (MockedStatic mockedStatic = mockStatic(ModuleUtil.class)) { + contextRunner.withPropertyValues("sofa.boot.auto.module.export.all.enable=true").run(applicationContext -> {}); + mockedStatic.verify(ModuleUtil::exportAllModulePackageToAll, times(1)); + } + } +} diff --git a/sofa-boot-project/sofa-boot/src/test/java/com/alipay/sofa/boot/util/ModuleUtilTests.java b/sofa-boot-project/sofa-boot/src/test/java/com/alipay/sofa/boot/util/ModuleUtilTests.java new file mode 100644 index 000000000..d574a05ff --- /dev/null +++ b/sofa-boot-project/sofa-boot/src/test/java/com/alipay/sofa/boot/util/ModuleUtilTests.java @@ -0,0 +1,50 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alipay.sofa.boot.util; + +import org.junit.jupiter.api.Test; + +import java.lang.reflect.InaccessibleObjectException; +import java.lang.reflect.Method; +import java.nio.file.Files; +import java.nio.file.Path; + +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * @author huazhongming + * @since 4.4.0 + */ +public class ModuleUtilTests { + + @Test + public void testExportAllJDKModulePackageToAll() throws NoSuchMethodException { + + Exception exception = assertThrows(InaccessibleObjectException.class,() -> { + Method newByteChannel0Method = Files.class.getDeclaredMethod("provider", Path.class); + newByteChannel0Method.setAccessible(true); + }); + + assertTrue(exception.getMessage().contains("module java.base does not \"opens java.nio.file\" to unnamed module")); + + ModuleUtil.exportAllJDKModulePackageToAll(); + + Method newByteChannel0Method = Files.class.getDeclaredMethod("provider", Path.class); + newByteChannel0Method.setAccessible(true); + } +}