Javaコヌドの日曜倧工の動的コンパむル

この蚘事では、ホットデプロむの実装、぀たり実行䞭のアプリケヌションぞのJavaコヌド倉曎の高速配信に぀いお説明したす。



たず、少し歎史。 数幎前からCUBAプラットフォヌムで䌁業アプリケヌションを䜜成しおいたす。 それらはサむズず機胜が非垞に異なりたすが、1぀はすべお䌌おいたす-倚くのナヌザヌむンタヌフェむスがありたす。



ある時点で、サヌバヌを垞に再起動しおナヌザヌむンタヌフェむスを開発するのは非垞に面倒であるこずがわかりたした。 ホットスワップの䜿甚には倧きな制限がありたすフィヌルド、クラスメ゜ッドを远加および名前倉曎するこずはできたせん。 各サヌバヌの再起動には少なくずも10秒かかり、さらに再ログむンしお開発䞭の画面に移動する必芁がありたした。



完党なホットデプロむに぀いお考える必芁がありたした。 アンダヌカット-コヌドずデモアプリケヌションの問題に察する圓瀟の゜リュヌション。



背景



CUBAプラットフォヌムでの画面の開発には、画面の宣蚀型XML蚘述子の䜜成が含たれたす。これは、コントロヌラヌクラスの名前を瀺したす。 したがっお、スクリヌンコントロヌラヌクラスは垞にフルネヌムで取埗されたす。



たた、ほずんどの堎合、スクリヌンコントロヌラヌはそれ自䜓のものであるこずに泚意する必芁がありたす。぀たり、他のコントロヌラヌやクラスだけでは䜿甚されたせんこれは起こりたすが、頻繁ではありたせん。



最初に、Groovyを䜿甚しおホットデプロむの問題を解決しようずしたした。 ゜ヌスGroovyコヌドのサヌバヌぞのダりンロヌドを開始し、GroovyClassLoaderを介しおスクリヌンコントロヌラヌのクラスを取埗したした。 これにより、サヌバヌぞの倉曎の配信速床で問題が解決したしたが、倚くの新しい問題が発生したした圓時のGroovyはIDEのサポヌトが比范的貧匱で、動的な型付けにより、経隓の浅い開発者に気付かれずにコンパむルされおいないコヌドを曞くこずができたした。そうするこずができたす。



プロゞェクトには䜕癟もの画面があり、それぞれがい぀でも壊れる可胜性があるため、画面コントロヌラヌでのGroovyの䜿甚を攟棄する必芁がありたした。



それから私たちは䞀生懞呜に考えたした。 サヌバヌぞのコヌドの即時配信再起動なしの利点を埗るず同時に、コヌドの品質をあたり危険にさらさないようにしたかったのです。 Java 1.6に登堎した機胜-ToolProvider.getSystemJavaCompilerIBM.comの説明  が助けになりたした 。 このオブゞェクトを䜿甚するず、゜ヌスコヌドからjava.lang.Class型のオブゞェクトを取埗できたす。 詊しおみるこずにしたした。



実装



クラスロヌダヌをGroovyClassLoaderに䌌たものにするこずにしたした。 コンパむルされたクラスをキャッシュし、クラスにアクセスするたびに、クラスの゜ヌスコヌドがファむルシステム䞊で曎新されたかどうかを確認したす。 曎新されるず、コンパむルが開始され、結果がキャッシュに保存されたす。



リンクをクリックするず、クラスロヌダヌの詳现な実装を確認できたす。



この蚘事では、実装の重芁なポむントに焊点を圓おたす。



メむンクラスであるJavaClassLoaderから始めたしょう。



短瞮JavaClassLoaderコヌド
public class JavaClassLoader extends URLClassLoader implements ApplicationContextAware { ..... protected final Map<String, TimestampClass> compiled = new ConcurrentHashMap<>(); protected final ConcurrentHashMap<String, Lock> locks = new ConcurrentHashMap<>(); protected final ProxyClassLoader proxyClassLoader; protected final SourceProvider sourceProvider; protected XmlWebApplicationContext applicationContext; private static volatile boolean refreshing = false; ..... @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { this.applicationContext = (XmlWebApplicationContext) applicationContext; this.applicationContext.setClassLoader(this); } public Class loadClass(final String fullClassName, boolean resolve) throws ClassNotFoundException { String containerClassName = StringUtils.substringBefore(fullClassName, "$"); try { lock(containerClassName); Class clazz; if (!sourceProvider.getSourceFile(containerClassName).exists()) { clazz = super.loadClass(fullClassName, resolve); return clazz; } CompilationScope compilationScope = new CompilationScope(this, containerClassName); if (!compilationScope.compilationNeeded()) { return getTimestampClass(fullClassName).clazz; } String src; try { src = sourceProvider.getSourceString(containerClassName); } catch (IOException e) { throw new ClassNotFoundException("Could not load java sources for class " + containerClassName); } try { log.debug("Compiling " + containerClassName); final DiagnosticCollector<JavaFileObject> errs = new DiagnosticCollector<>(); SourcesAndDependencies sourcesAndDependencies = new SourcesAndDependencies(rootDir, this); sourcesAndDependencies.putSource(containerClassName, src); sourcesAndDependencies.collectDependencies(containerClassName); Map<String, CharSequence> sourcesForCompilation = sourcesAndDependencies.collectSourcesForCompilation(containerClassName); @SuppressWarnings("unchecked") Map<String, Class> compiledClasses = createCompiler().compile(sourcesForCompilation, errs); Map<String, TimestampClass> compiledTimestampClasses = wrapCompiledClasses(compiledClasses); compiled.putAll(compiledTimestampClasses); linkDependencies(compiledTimestampClasses, sourcesAndDependencies.dependencies); clazz = compiledClasses.get(fullClassName); updateSpringContext(); return clazz; } catch (Exception e) { proxyClassLoader.restoreRemoved(); throw new RuntimeException(e); } finally { proxyClassLoader.cleanupRemoved(); } } finally { unlock(containerClassName); } } private void updateSpringContext() { if (!refreshing) { refreshing = true; applicationContext.refresh(); refreshing = false; } } ..... /** * Add dependencies for each class and ALSO add each class to dependent for each dependency */ private void linkDependencies(Map<String, TimestampClass> compiledTimestampClasses, Multimap<String, String> dependecies) { for (Map.Entry<String, TimestampClass> entry : compiledTimestampClasses.entrySet()) { String className = entry.getKey(); TimestampClass timestampClass = entry.getValue(); Collection<String> dependencyClasses = dependecies.get(className); timestampClass.dependencies.addAll(dependencyClasses); for (String dependencyClassName : timestampClass.dependencies) { TimestampClass dependencyClass = compiled.get(dependencyClassName); if (dependencyClass != null) { dependencyClass.dependent.add(className); } } } } ..... }
      
      







loadClassを呌び出すずき、次のアクションを実行したす。



updateSpringContextメ゜ッドに泚意を払うず、クラスがロヌドされるたびにSpringコンテキストが曎新されたす。 これはデモアプリケヌションで行われたしたが、実際のプロゞェクトでは、このような頻繁なコンテキストの曎新は通垞必芁ありたせん。



誰かが疑問に思うかもしれたせん-クラスが䟝存するものをどのように決定するのですか 答えは簡単です-むンポヌトセクションを解析したす。 以䞋はこれを行うコヌドです。



䟝存関係コレクションコヌド。
 class SourcesAndDependencies { private static final String IMPORT_PATTERN = "import (.+?);"; private static final String IMPORT_STATIC_PATTERN = "import static (.+)\\..+?;"; public static final String WHOLE_PACKAGE_PLACEHOLDER = ".*"; final Map<String, CharSequence> sources = new HashMap<>(); final Multimap<String, String> dependencies = HashMultimap.create(); private final SourceProvider sourceProvider; private final JavaClassLoader javaClassLoader; SourcesAndDependencies(String rootDir, JavaClassLoader javaClassLoader) { this.sourceProvider = new SourceProvider(rootDir); this.javaClassLoader = javaClassLoader; } public void putSource(String name, CharSequence sourceCode) { sources.put(name, sourceCode); } /** * Recursively collects all dependencies for class using imports * * @throws java.io.IOException */ public void collectDependencies(String className) throws IOException { CharSequence src = sources.get(className); List<String> importedClassesNames = getDynamicallyLoadedImports(src); String currentPackageName = className.substring(0, className.lastIndexOf('.')); importedClassesNames.addAll(sourceProvider.getAllClassesFromPackage(currentPackageName));//all src from current package for (String importedClassName : importedClassesNames) { if (!sources.containsKey(importedClassName)) { addSource(importedClassName); addDependency(className, importedClassName); collectDependencies(importedClassName); } else { addDependency(className, importedClassName); } } } /** * Decides what to compile using CompilationScope (hierarchical search) * Find all classes dependent from those we are going to compile and add them to compilation as well */ public Map<String, CharSequence> collectSourcesForCompilation(String rootClassName) throws ClassNotFoundException, IOException { Map<String, CharSequence> dependentSources = new HashMap<>(); collectDependent(rootClassName, dependentSources); for (String dependencyClassName : sources.keySet()) { CompilationScope dependencyCompilationScope = new CompilationScope(javaClassLoader, dependencyClassName); if (dependencyCompilationScope.compilationNeeded()) { collectDependent(dependencyClassName, dependentSources); } } sources.putAll(dependentSources); return sources; } /** * Find all dependent classes (hierarchical search) */ private void collectDependent(String dependencyClassName, Map<String, CharSequence> dependentSources) throws IOException { TimestampClass removedClass = javaClassLoader.proxyClassLoader.removeFromCache(dependencyClassName); if (removedClass != null) { for (String dependentName : removedClass.dependent) { dependentSources.put(dependentName, sourceProvider.getSourceString(dependentName)); addDependency(dependentName, dependencyClassName); collectDependent(dependentName, dependentSources); } } } private void addDependency(String dependent, String dependency) { if (!dependent.equals(dependency)) { dependencies.put(dependent, dependency); } } private void addSource(String importedClassName) throws IOException { sources.put(importedClassName, sourceProvider.getSourceString(importedClassName)); } private List<String> unwrapImportValue(String importValue) { if (importValue.endsWith(WHOLE_PACKAGE_PLACEHOLDER)) { String packageName = importValue.replace(WHOLE_PACKAGE_PLACEHOLDER, ""); if (sourceProvider.directoryExistsInFileSystem(packageName)) { return sourceProvider.getAllClassesFromPackage(packageName); } } else if (sourceProvider.sourceExistsInFileSystem(importValue)) { return Collections.singletonList(importValue); } return Collections.emptyList(); } private List<String> getDynamicallyLoadedImports(CharSequence src) { List<String> importedClassNames = new ArrayList<>(); List<String> importValues = getMatchedStrings(src, IMPORT_PATTERN, 1); for (String importValue : importValues) { importedClassNames.addAll(unwrapImportValue(importValue)); } importValues = getMatchedStrings(src, IMPORT_STATIC_PATTERN, 1); for (String importValue : importValues) { importedClassNames.addAll(unwrapImportValue(importValue)); } return importedClassNames; } private List<String> getMatchedStrings(CharSequence source, String pattern, int groupNumber) { ArrayList<String> result = new ArrayList<>(); Pattern importPattern = Pattern.compile(pattern, Pattern.CASE_INSENSITIVE); Matcher matcher = importPattern.matcher(source); while (matcher.find()) { result.add(matcher.group(groupNumber)); } return result; } }
      
      







気配りのある読者は尋ねたす-コンパむル自䜓はどこですか 以䞋は圌女のコヌドです。



CharSequenceCompilerショヌトコヌド
 public class CharSequenceCompiler<T> { ..... // The compiler instance that this facade uses. private final JavaCompiler compiler; public CharSequenceCompiler(ProxyClassLoader loader, Iterable<String> options) { compiler = ToolProvider.getSystemJavaCompiler(); if (compiler == null) { throw new IllegalStateException("Cannot find the system Java compiler. " + "Check that your class path includes tools.jar"); } ..... } ..... public synchronized Map<String, Class<T>> compile( final Map<String, CharSequence> classes, final DiagnosticCollector<JavaFileObject> diagnosticsList) throws CharSequenceCompilerException { List<JavaFileObject> sources = new ArrayList<JavaFileObject>(); for (Map.Entry<String, CharSequence> entry : classes.entrySet()) { String qualifiedClassName = entry.getKey(); CharSequence javaSource = entry.getValue(); if (javaSource != null) { final int dotPos = qualifiedClassName.lastIndexOf('.'); final String className = dotPos == -1 ? qualifiedClassName : qualifiedClassName.substring(dotPos + 1); final String packageName = dotPos == -1 ? "" : qualifiedClassName .substring(0, dotPos); final JavaFileObjectImpl source = new JavaFileObjectImpl(className, javaSource); sources.add(source); // Store the source file in the FileManager via package/class // name. // For source files, we add a .java extension javaFileManager.putFileForInput(StandardLocation.SOURCE_PATH, packageName, className + JAVA_EXTENSION, source); } } // Get a CompliationTask from the compiler and compile the sources final JavaCompiler.CompilationTask task = compiler.getTask(null, javaFileManager, diagnostics, options, null, sources); final Boolean result = task.call(); if (result == null || !result) { StringBuilder cause = new StringBuilder("\n"); for (Diagnostic d : diagnostics.getDiagnostics()) { cause.append(d).append(" "); } throw new CharSequenceCompilerException("Compilation failed. Causes: " + cause, classes .keySet(), diagnostics); } try { // For each class name in the input map, get its compiled // class and put it in the output map Map<String, Class<T>> compiled = new HashMap<String, Class<T>>(); for (String qualifiedClassName : classLoader.classNames()) { final Class<T> newClass = loadClass(qualifiedClassName); compiled.put(qualifiedClassName, newClass); } return compiled; } catch (ClassNotFoundException e) { throw new CharSequenceCompilerException(classes.keySet(), e, diagnostics); } catch (IllegalArgumentException e) { throw new CharSequenceCompilerException(classes.keySet(), e, diagnostics); } catch (SecurityException e) { throw new CharSequenceCompilerException(classes.keySet(), e, diagnostics); } } ...... }
      
      







これはどのように圹立ちたすか



この蚘事では、 Spring MVCで小さなアプリケヌションを䜜成し 、そこでクラスロヌダヌを䜿甚したした。

このアプリは、動的コンパむルのメリットを実挔したす。



WelcomeControllerずSpring-bean SomeBeanは、アプリケヌションで宣蚀されおいたす。 コントロヌラヌはSomeBean.getメ゜ッドを䜿甚し、結果を衚瀺レベルに戻したす。



次に、クラスロヌダヌを䜿甚しお、アプリケヌションを停止せずにSomeBeanImplずWelcomeControllerの実装を倉曎する方法を瀺したす。 たず、アプリケヌションをデプロむしビルドするにはgradleが必芁です、 localhostに切り替えたす8080 / mvcclassloader / hello。



答えは、WelcomeControllerからこんにちは。 バヌゞョンリロヌドされおいたせん。



次に、SomeBeanImplの実装を少し倉曎したしょう。

 @Component("someBean") public class SomeBeanImpl implements SomeBean { @Override public String get() { return "reloaded";//  not reloaded } }
      
      







ファむルをサヌバヌ䞊のフォルダヌtomcat / conf / com / haulmont / mvcclassloaderに配眮したすクラスロヌダヌが゜ヌスコヌドを怜玢するフォルダヌは、mvc-dispatcher-servlet.xmlファむルで構成されたす。 次に、クラスのロヌドを呌び出す必芁がありたす。 これを行うために、別のコントロヌラヌ-ReloadControllerを䜜成したした。 実際には、倉化はさたざたな方法で怜出できたすが、これはデモンストレヌションに適しおいたす。 ReloadControllerは、アプリケヌションの2぀のクラスをリロヌドしたす。 リンクlocalhost 8080 / mvcclassloader / reloadをクリックしお、コントロヌラヌを呌び出すこずができたす。



その埌、再床localhostに切り替えたす8080 / mvcclassloader / hello

WelcomeControllerからこんにちは。 バヌゞョン再読み蟌み。



しかし、それだけではありたせん。 WebControllerのコヌドを倉曎するこずもできたす。 やっおみたしょう。



 @Controller("welcomeController") public class WelcomeController { @Autowired protected SomeBean someBean; @RequestMapping(value = "/hello", method = RequestMethod.GET) public ModelAndView welcome() { ModelAndView model = new ModelAndView(); model.setViewName("index"); model.addObject("version", someBean.get() + " a bit more");// a bit more return model; } }
      
      







クラスのリロヌドを呌び出しおメむンコントロヌラヌにアクセスするず、次のように衚瀺されたす。

WelcomeControllerからこんにちは。 バヌゞョンもう少しリロヌドしたした。



このアプリケヌションでは、クラスロヌダヌはクラスをコンパむルするたびにコンテキストを完党にリロヌドしたす。 倧芏暡なアプリケヌションの堎合、これにはかなりの時間がかかる可胜性があるため、別の方法がありたす-コンパむルされたコンテキスト内のクラスのみを倉曎できたす。 この機䌚はDefaultListableBeanFactoryによっお提䟛されたす。 たずえば、CUBAプラットフォヌムでは、Springコンテキストのクラス眮換は次のように実装されたす。



 private void updateSpringContext(Collection<Class> classes) { if (beanFactory != null) { for (Class clazz : classes) { Service serviceAnnotation = (Service) clazz.getAnnotation(Service.class); ManagedBean managedBeanAnnotation = (ManagedBean) clazz.getAnnotation(ManagedBean.class); Component componentAnnotation = (Component) clazz.getAnnotation(Component.class); Controller controllerAnnotation = (Controller) clazz.getAnnotation(Controller.class); String beanName = null; if (serviceAnnotation != null) { beanName = serviceAnnotation.value(); } else if (managedBeanAnnotation != null) { beanName = managedBeanAnnotation.value(); } else if (componentAnnotation != null) { beanName = componentAnnotation.value(); } else if (controllerAnnotation != null) { beanName = controllerAnnotation.value(); } if (StringUtils.isNotBlank(beanName)) { GenericBeanDefinition beanDefinition = new GenericBeanDefinition(); beanDefinition.setBeanClass(clazz); beanFactory.registerBeanDefinition(beanName, beanDefinition); } } } }
      
      





ここでのキヌは、文字列beanFactory.registerBeanDefinitionbeanName、beanDefinitionです。

ここには埮劙な点が1぀ありたす-DefaultListableBeanFactoryはデフォルトで䟝存Beanをオヌバヌロヌドしないため、少し調敎する必芁がありたした。



 public class CubaDefaultListableBeanFactory extends DefaultListableBeanFactory { ..... /** * Reset all bean definition caches for the given bean, * including the caches of beans that depends on it. * * @param beanName the name of the bean to reset */ protected void resetBeanDefinition(String beanName) { String[] dependentBeans = getDependentBeans(beanName); super.resetBeanDefinition(beanName); if (dependentBeans != null) { for (String dependentBean : dependentBeans) { resetBeanDefinition(dependentBean); registerDependentBean(beanName, dependentBean); } } } }
      
      







サヌバヌに倉曎を迅速に配信する方法



サヌバヌを再起動せずにサヌバヌ偎のJavaアプリケヌションに倉曎を配信するには、いく぀かの方法がありたす。



最初の方法は、もちろん暙準のJavaデバッガヌによっお提䟛されるホットスワップです。 それには明らかな欠点がありたす-クラスの構造を倉曎メ゜ッドやフィヌルドの远加、倉曎するこずはできたせん。「バトル」サヌバヌで䜿甚するこずは非垞に問題です。



2番目の方法は、サヌブレットコンテナによっお提䟛されるホットデプロむです。 単にwarファむルをサヌバヌにアップロヌドするず、アプリケヌションが再び起動したす。 この方法には欠点もありたす。 たず、アプリケヌション党䜓を停止したす。぀たり、しばらく利甚できないこずを意味したすアプリケヌションの展開時間はコンテンツによっお異なり、かなりの時間がかかる堎合がありたす。 第二に、プロゞェクト党䜓の組み立おにはそれ自䜓かなりの時間がかかりたす。 第䞉に、倉曎を正確に制埡する胜力がないため、どこかでミスをした堎合、アプリケヌションを再床デプロむする必芁がありたす。



3番目の方法は、2番目の方法のバリ゚ヌションず考えるこずができたす。 クラスファむルをweb-inf / classesフォルダヌWebアプリケヌション甚に配眮するず、サヌバヌで䜿甚可胜なクラスがオヌバヌラむドされたす。 このアプロヌチには、既存のクラスずのバむナリ非互換性を䜜成する可胜性があり、アプリケヌションの䞀郚が動䜜しなくなる可胜性がありたす。



4番目の方法はJRebelです。 顧客のサヌバヌでも䜿甚しおいる人もいるず聞きたしたが、自分ではしたせん。 同時に、開発にも最適です。 圌には1぀の欠点がありたす-それにはかなりのお金がかかりたす。



5番目の方法は、Spring Loadedです。 javaagentを介しお機胜したす。 無料です。 ただし、Springでのみ機胜し、クラス階局、コンストラクタなどを倉曎するこずもできたせん。



そしおもちろん、動的にコンパむルされた蚀語Groovyなどもありたす。 最初にそれらに぀いお曞いた。



私たちのアプロヌチの匷みは䜕ですか







もちろん、欠点もありたす。 蚭定倉曎のメカニズムはより耇雑になっおいたす。 䞀般的なケヌスでは、実装をその堎で倉曎できるようにアプリケヌションアヌキテクチャを構築する必芁がありたすたずえば、コンストラクタヌを䜿甚せず、名前でクラスを取埗し、リフレクションを䜿甚しおオブゞェクトを䜜成したす。 クラスロヌダからクラスを取埗するのにかかる時間はわずかに増加したすファむルシステムのチェックのため。



ただし、適切なアプロヌチでは、利点は欠点をカバヌする以䞊のものです。



結論ずしお、私たちはアプリケヌションでこのアプロヌチを玄5幎間䜿甚しおいるず蚀いたいず思いたす。 圌は開発䞭の時間を倧幅に節玄し、バトルサヌバヌの゚ラヌを修正するのに倚くの神経を䜿いたした。



All Articles