時間がないこずよりも良い時間JIRA APIをその堎で拡匵する

問題を解決するためにアプリケヌションで䜿甚可胜なAPIが十分ではなく、コヌドをすぐに倉曎する可胜性がない堎合、どうすればよいですか







この状況での最埌の垌望は、 java.lang.instrumentパッケヌゞの䜿甚です。 既に実行䞭のVMでコヌドを䜿甚しおJavaで䜕をどのように行うこずができるかに興味がある人は、catにようこそ。



Habrには、すでにバむトコヌドの凊理に関する蚘事がありたす。





ただし、これらのテクノロゞヌの䜿甚は、原則ずしお、ロギングたたはその他の単玔な機胜に限定されたす。 しかし、蚈装を䜿甚しおアプリケヌションの機胜を拡匵するために詐欺をしようずしたらどうでしょうか



この蚘事では、アプリケヌションに新しい機胜を远加するために、Java゚ヌゞェントアプリケヌション OSGiずByte Buddyラむブラリの䞡方を蚈枬する方法を瀺したす。 この蚘事は䞻にJIRAで䜜業する人々にずっお興味深いものですが、䜿甚されるアプロヌチは非垞に普遍的であり、他のプラットフォヌムに適甚できたす。



挑戊する



したがっお、JIRA APIを2、3日調査した埌、タスクの䜜成/倉曎時にフィヌルド倀の盞互怜蚌を実装するのが普通であるこずを理解しおいたす1぀のフィヌルドの有効な倀が別のフィヌルドの倀に䟝存する堎合。 アプロヌチは機胜しおいたすが、難しくお䟿利ではないので、空き時間に蚈画Bを埗るために研究を続けるこずにしたした。



最近のゞョヌカヌは 、 バむトバディラむブラリに関するラファ゚ルりィンタヌハルタヌの 講挔を 特集したした。これは、より䟿利な高レベルシェルで匷力な䜎レベルバむトコヌド線集APIをラップしたす。 このラむブラリは珟圚非垞に人気があり、特に最近ではMockitoずHibernateで䜿甚されおいたす。 ずりわけ、Rafaelは既にロヌドされたクラスのByte Buddyで倉曎する可胜性に぀いお話したした。



「これは思考だ」ず思い、仕事を始めたす。



蚭蚈



たず、Rafaelのレポヌトから、すでにロヌドされおいるクラスの倉曎は、java゚ヌゞェントの起動時に䜿甚できるjava.lang.instrument.Instrumentationむンタヌフェむスを䜿甚するこずでのみ可胜であるこずを思い出しおください。 VMの起動時にコマンドラむンを䜿甚するか、JDKに付属するプラットフォヌム䟝存のAttach APIを䜿甚しおむンストヌルできたす。



ここには重芁な詳现がありたす-゚ヌゞェントは削陀できたせん-そのクラスはVMが終了するたでロヌドされたたたです。



attach APIのサポヌトに関しおJIRAに関しおは、ここではJDK䞊で実行されるこずを保蚌できたせん。さらに、JIRAが実行されるOSを保蚌するこずはできたせん。

次に、JIRA機胜を拡匵するためのメむンナニットはアドオン -ステロむドのバンドルであるこずを思い出したす。 ぀たり、すべおのロゞックは、それが䜕であれ、アドオンずしお蚭蚈する必芁がありたす。 これは、システムに倉曎を加える堎合、べき等で切断可胜でなければならないずいう芁件を意味したす。



これらの制限を考えるず、グロヌバルに2぀のタスクがありたす。





コンポヌネント間で責任を分散させるず、次のスキヌムが埗られたした。









実装



゚ヌゞェント



たず、゚ヌゞェントを䜜成したす。



public class InstrumentationSupplierAgent { public static volatile Instrumentation instrumentation; public static void agentmain(String args, Instrumentation inst) throws Exception { System.out.println("==**agent started**=="); InstrumentationSupplierAgent.instrumentation = inst; System.out.println("==**agent execution complete**=="); } }
      
      





コヌドは簡単で、説明は必芁ないず思いたす。 合意されたように-最も䞀般的なロゞックは、頻繁に曎新する必芁があるこずはほずんどありたせん。



プロバむダヌ



次に、この゚ヌゞェントがタヌゲットVMに接続するアドオンを䜜成したす。 ゚ヌゞェントのむンストヌルロゞックから始めたしょう。 スポむラヌの䞋の完党なむンストヌラヌコヌド



AgentInstaller.java
 @Component public class AgentInstaller { private static final Logger log = LoggerFactory.getLogger(AgentInstaller.class); private final JiraHome jiraHome; private final JiraProperties jiraProperties; @Autowired public AgentInstaller( @ComponentImport JiraHome jiraHome, @ComponentImport JiraProperties jiraProperties ) { this.jiraHome = jiraHome; this.jiraProperties = jiraProperties; } private static File getInstrumentationDirectory(JiraHome jiraHome) throws IOException { final File dataDirectory = jiraHome.getDataDirectory(); final File instrFolder = new File(dataDirectory, "instrumentation"); if (!instrFolder.exists()) { Files.createDirectory(instrFolder.toPath()); } return instrFolder; } private static File loadFileFromCurrentJar(File destination, String fileName) throws IOException { try (InputStream resourceAsStream = AgentInstaller.class.getResourceAsStream("/lib/" + fileName)) { final File existingFile = new File(destination, fileName); if (!existingFile.exists() || !isCheckSumEqual(new FileInputStream(existingFile), resourceAsStream)) { Files.deleteIfExists(existingFile.toPath()); existingFile.createNewFile(); try (OutputStream os = new FileOutputStream(existingFile)) { IOUtils.copy(resourceAsStream, os); } } return existingFile; } } private static boolean isCheckSumEqual(InputStream existingFileStream, InputStream newFileStream) { try (InputStream oldIs = existingFileStream; InputStream newIs = newFileStream) { return Arrays.equals(getMDFiveDigest(oldIs), getMDFiveDigest(newIs)); } catch (NoSuchAlgorithmException | IOException e) { log.error("Error to compare checksum for streams {},{}", existingFileStream, newFileStream); return false; } } private static byte[] getMDFiveDigest(InputStream is) throws IOException, NoSuchAlgorithmException { final MessageDigest md = MessageDigest.getInstance("MD5"); md.digest(IOUtils.toByteArray(is)); return md.digest(); } public void install() throws PluginException { try { log.trace("Trying to install tools and agent"); if (!isProperAgentLoaded()) { log.info("Instrumentation agent is not installed yet or has wrong version"); final String pid = getPid(); log.debug("Current VM PID={}", pid); final URLClassLoader systemClassLoader = (URLClassLoader) ClassLoader.getSystemClassLoader(); log.debug("System classLoader={}", systemClassLoader); final Class<?> virtualMachine = getVirtualMachineClass( systemClassLoader, "com.sun.tools.attach.VirtualMachine", true ); log.debug("VM class={}", virtualMachine); Method attach = virtualMachine.getMethod("attach", String.class); Method loadAgent = virtualMachine.getMethod("loadAgent", String.class); Method detach = virtualMachine.getMethod("detach"); Object vm = null; try { log.trace("Attaching to VM with PID={}", pid); vm = attach.invoke(null, pid); final File agentFile = getAgentFile(); log.debug("Agent file: {}", agentFile); loadAgent.invoke(vm, agentFile.getAbsolutePath()); } finally { tryToDetach(vm, detach); } } else { log.info("Instrumentation agent is already installed"); } } catch (Exception e) { throw new IllegalPluginStateException("Failed to load: agent and tools are not installed properly", e); } } private boolean isProperAgentLoaded() { try { ClassLoader.getSystemClassLoader().loadClass(InstrumentationProvider.INSTRUMENTATION_CLASS_NAME); return true; } catch (Exception e) { return false; } } private void tryToDetach(Object vm, Method detach) { try { if (vm != null) { log.trace("Detaching from VM: {}", vm); detach.invoke(vm); } else { log.warn("Failed to detach, vm is null"); } } catch (Exception e) { log.warn("Failed to detach", e); } } private String getPid() { String nameOfRunningVM = ManagementFactory.getRuntimeMXBean().getName(); return nameOfRunningVM.split("@", 2)[0]; } private Class<?> getVirtualMachineClass(URLClassLoader systemClassLoader, String className, boolean tryLoadTools) throws Exception { log.trace("Trying to get VM class, loadingTools={}", tryLoadTools); try { return systemClassLoader.loadClass(className); } catch (ClassNotFoundException e) { if (tryLoadTools) { final OS os = getRunningOs(); os.tryToLoadTools(systemClassLoader, jiraHome); return getVirtualMachineClass(systemClassLoader, className, false); } else { throw new ReflectiveOperationException("Failed to load VM class", e); } } } private OS getRunningOs() { final String osName = jiraProperties.getSanitisedProperties().get("os.name"); log.debug("OS name: {}", osName); if (Pattern.compile(".*[Ll]inux.*").matcher(osName).matches()) { return OS.LINUX; } else if (Pattern.compile(".*[Ww]indows.*").matcher(osName).matches()) { return OS.WINDOWS; } else { throw new IllegalStateException("Unknown OS running"); } } private File getAgentFile() throws IOException { final File agent = loadFileFromCurrentJar(getInstrumentationDirectory(jiraHome), "instrumentation-agent.jar"); agent.deleteOnExit(); return agent; } private enum OS { WINDOWS { @Override protected String getToolsFilename() { return "tools-windows.jar"; } @Override protected String getAttachLibFilename() { return "attach.dll"; } }, LINUX { @Override protected String getToolsFilename() { return "tools-linux.jar"; } @Override protected String getAttachLibFilename() { return "libattach.so"; } }; public void tryToLoadTools(URLClassLoader systemClassLoader, JiraHome jiraHome) throws Exception { log.trace("Trying to load tools"); final File instrumentationDirectory = getInstrumentationDirectory(jiraHome); appendLibPath(instrumentationDirectory.getAbsolutePath()); loadFileFromCurrentJar(instrumentationDirectory, getAttachLibFilename()); resetCache(); final File tools = loadFileFromCurrentJar(instrumentationDirectory, getToolsFilename()); final Method method = URLClassLoader.class.getDeclaredMethod("addURL", URL.class); method.setAccessible(true); method.invoke(systemClassLoader, tools.toURI().toURL()); } private void resetCache() throws NoSuchFieldException, IllegalAccessException { Field fieldSysPath = ClassLoader.class.getDeclaredField("sys_paths"); fieldSysPath.setAccessible(true); fieldSysPath.set(null, null); } private void appendLibPath(String instrumentationDirectory) { if (System.getProperty("java.library.path") != null) { System.setProperty("java.library.path", System.getProperty("java.library.path") + System.getProperty("path.separator") + instrumentationDirectory); } else { System.setProperty("java.library.path", instrumentationDirectory); } } protected abstract String getToolsFilename(); protected abstract String getAttachLibFilename(); } }
      
      







コヌドを郚分的に分析したしょう。



最も単玔なシナリオは、゚ヌゞェントがすでにロヌドされおいる堎合です。 起動時にコマンドラむンオプションを䜿甚しおオンにしたか、アドオンが初めおむンストヌルされおいない可胜性がありたす。



チェック-簡単、システムクラスロヌダヌで゚ヌゞェントクラスをロヌドするだけ



 private boolean isProperAgentLoaded() { try { ClassLoader.getSystemClassLoader().loadClass(InstrumentationProvider.INSTRUMENTATION_CLASS_NAME); return true; } catch (Exception e) { return false; } }
      
      





䜿甚可胜な堎合、これ以䞊むンストヌルする必芁はありたせん。 しかし、最初のむンストヌルがあり、゚ヌゞェントがただ読み蟌たれおいないずしたしょう-attach APIを䜿甚しお自分でそれを行いたす。 前のケヌスず同様に、たずJDKで䜜業しおいるかどうかを確認したす。 目的のAPIは、远加の操䜜を行うかどうかに関係なく利甚できたす。 そうでない堎合は、APIを「配信」しおみおください。



 private Class<?> getVirtualMachineClass(URLClassLoader systemClassLoader, String className, boolean tryLoadTools) throws Exception { log.trace("Trying to get VM class, loadingTools={}", tryLoadTools); try { return systemClassLoader.loadClass(className); } catch (ClassNotFoundException e) { if (tryLoadTools) { final OS os = getRunningOs(); os.tryToLoadTools(systemClassLoader, jiraHome); return getVirtualMachineClass(systemClassLoader, className, false); } else { throw new ReflectiveOperationException("Failed to load VM class", e); } } }
      
      





次に、接続APIのむンストヌル手順を怜蚎したす。 JREをJDKに「倉換」するタスクは、コンテナOSの定矩から始たりたす。 JIRAでは、OS定矩コヌドは既に実装されおいたす。



 private OS getRunningOs() { final String osName = jiraProperties.getSanitisedProperties().get("os.name"); log.debug("OS name: {}", osName); if (Pattern.compile(".*[Ll]inux.*").matcher(osName).matches()) { return OS.LINUX; } else if (Pattern.compile(".*[Ww]indows.*").matcher(osName).matches()) { return OS.WINDOWS; } else { throw new IllegalStateException("Unknown OS running"); } }
      
      





ここで、どのOSで実行されおいるかがわかったら、アタッチAPIをアタッチする方法を怜蚎しおください。 最初に、 アタッチAPIが実際に構成するものを芋おください。 私が蚀ったように、それはプラットフォヌムに䟝存しおいたす。



泚tools.jarはプラットフォヌムに䟝存しないものずしおリストされおいたすが、これは完党に真実ではありたせん。 META-INF / services /では、構成ファむルcom.sun.tools.attach.spi.AttachProviderが非衚瀺になり、環境で利甚可胜なプロバむダヌがリストされたす。



[solaris] sun.tools.attach.SolarisAttachProvider

[windows] sun.tools.attach.WindowsAttachProvider

[linux] sun.tools.attach.LinuxAttachProvider

[macosx] sun.tools.attach.BsdAttachProvider

[aix] sun.tools.attach.AixAttachProvider


これらは、プラットフォヌムに非垞に䟝存しおいたす。



圓面、必芁なファむルをアセンブリに接続するために、察応するJDKディストリビュヌションからラむブラリファむルずtools.jarのコピヌを抜出しお、リポゞトリに配眮するこずにしたした。

重芁なのは、アタッチAPIファむルを読み蟌んだ埌は削陀たたは倉曎できないため、アドオンを削陀および曎新できるようにする堎合は、jarからラむブラリを盎接読み蟌む必芁はないこずです。それらをjarファむルからJIRAからアクセス可胜な静かで萜ち着いた堎所にコピヌしたす。



 public void tryToLoadTools(URLClassLoader systemClassLoader, JiraHome jiraHome) throws Exception { log.trace("Trying to load tools"); final File instrumentationDirectory = getInstrumentationDirectory(jiraHome);//{JIRA_HOME}/data/instrumentation loadFileFromCurrentJar(instrumentationDirectory, getAttachLibFilename());//    final File tools = loadFileFromCurrentJar(instrumentationDirectory, getToolsFilename());// tools.jar ... }
      
      





ファむルをコピヌするには、次の方法を䜿甚したす。



 private static File loadFileFromCurrentJar(File destination, String fileName) throws IOException { try (InputStream resourceAsStream = AgentInstaller.class.getResourceAsStream("/lib/" + fileName)) { final File existingFile = new File(destination, fileName); if (!existingFile.exists() || !isCheckSumEqual(new FileInputStream(existingFile), resourceAsStream)) { Files.deleteIfExists(existingFile.toPath());//    -   existingFile.createNewFile(); try (OutputStream os = new FileOutputStream(existingFile)) { IOUtils.copy(resourceAsStream, os); } } return existingFile; } }
      
      





通垞のファむル操䜜に加えお、このコヌドはチェックサム蚈算を実行したす。 コヌドの蚘述時に、ランタむムで曎新䞍可胜なコンポヌネントを怜蚌するこの方法が最初に思い浮かびたした。 原則ずしお、アヌティファクトをバヌゞョン管理する堎合、バヌゞョンチェックを実行するこずもできたす。 ファむルがすでにロヌドされおいるが、チェックサムがアヌカむブからのアヌティファクトず䞀臎しない堎合、それらを眮き換えようずしたす。



だから、ファむルがありたす、ダりンロヌドする方法を理解したしょう。 最も難しいこずから始めたしょう-ネむティブラむブラリをロヌドしたす。 attach APIの腞を調べるず、タスクを実行するず、次のコヌドを䜿甚しおラむブラリがアンロヌドされるこずが盎接わかりたす。



 static { System.loadLibrary("attach"); }
      
      





これは、ラむブラリの堎所を「java.library.path」に远加する必芁があるこずを瀺唆しおいたす



 private void appendLibPath(String instrumentationDirectory) { if (System.getProperty("java.library.path") != null) { System.setProperty("java.library.path", System.getProperty("java.library.path") + System.getProperty("path.separator") + instrumentationDirectory); } else { System.setProperty("java.library.path", instrumentationDirectory); } }
      
      





その埌、目的のネむティブラむブラリファむルを正しいディレクトリに远加し、最初の束葉杖を゜リュヌションに远加したす。 「Java.library.path」は、ClassLoaderクラスのプラむベヌト静的文字列sys_paths []にキャッシュされたす。 さお、プラむベヌトには䜕が必芁ですか-キャッシュをリセットしたしょう...



 private void resetCache() throws NoSuchFieldException, IllegalAccessException { Field fieldSysPath = ClassLoader.class.getDeclaredField("sys_paths"); fieldSysPath.setAccessible(true); fieldSysPath.set(null, null); }
      
      





ここでは、ネむティブ郚分をダりンロヌドしたした-JavaのAPIの郚分に枡したす。 JDKのtools.jarは、システムロヌダヌによっおロヌドされたす。 同じこずをする必芁がありたす。



少し良くなったので、システムロヌダヌがjava.net.URLClassLoaderを実装しおいるこずがわかりたす。

芁するに、このロヌダヌはクラスの堎所をURLのリストずしお保存したす。 ダりンロヌドする必芁があるのは、このリストにtools- [OS] .jarのURLを远加するこずだけです。 URLClassLoader APIを研究した埌、再び悲しくなりたした。 必芁なこずを正確に行うaddURLメ゜ッドが保護されおいるこずがわかりたす。 ええず...现長いプロトタむプぞのもう䞀぀のバックアップ



 final Method method = URLClassLoader.class.getDeclaredMethod("addURL", URL.class); method.setAccessible(true); method.invoke(systemClassLoader, tools.toURI().toURL());
      
      





さお、最埌に、すべおが仮想マシンクラスをロヌドする準備ができたした。



珟圚のOSGiクラスロヌダヌではなく、システムに垞にダりンロヌドする必芁がありたす。 接続䞭に、このクラスロヌダヌはネむティブラむブラリをロヌドしたす。これは䞀床しか実行できたせん。 OSGiクラスロヌダヌは、バンドルをむンストヌルするずきに䜜成されたす新しいバンドルを䜜成するたびに。 そのため、この皮のものを取埗するリスクがありたす。




19その他

原因com.sun.tools.attach.AttachNotSupportedExceptionプロバむダヌがむンストヌルされおいたせん

com.sun.tools.attach.VirtualMachine.attachVirtualMachine.java:203で


説明は明らかではありたせんが、本圓の理由は、既にロヌドされおいるラむブラリをロヌドしようずしおいるこずです。これに぀いおは、attachメ゜ッドを販売しお実際の䟋倖を確認するこずによっおのみ知るこずができたす。



クラスをロヌドしたら、必芁なメ゜ッドをロヌドしお、最終的に゚ヌゞェントをアタッチできたす。



 Method attach = virtualMachine.getMethod("attach", String.class); Method loadAgent = virtualMachine.getMethod("loadAgent", String.class); Method detach = virtualMachine.getMethod("detach"); Object vm = null; try { final String pid = getPid(); log.debug("Current VM PID={}", pid); log.trace("Attaching to VM with PID={}", pid); vm = attach.invoke(null, pid); final File agentFile = getAgentFile(); log.debug("Agent file: {}", agentFile); loadAgent.invoke(vm, agentFile.getAbsolutePath()); } finally { tryToDetach(vm, detach); }
      
      





ここでの唯䞀の埮劙な点は、仮想マシンのPIDです。



  private String getPid() { String nameOfRunningVM = ManagementFactory.getRuntimeMXBean().getName(); return nameOfRunningVM.split("@", 2)[0]; }
      
      





このメ゜ッドは非暙準ですが、非垞に機胜したす。Java9 Process API党般では、問題なくこれを行うこずができたす。



アドオン



次に、このロゞックをアドオンに埋め蟌みたす。 アドオンのむンストヌル䞭にコヌドを呌び出す機胜に興味がありたす-これは、暙準のスプリングInitializingBeanを䜿甚しお行われたす。



  @Override public void afterPropertiesSet() throws Exception { this.agentInstaller.install(); this.serviceTracker.open(); }
      
      





たず、゚ヌゞェントむンストヌルロゞック䞊蚘で説明を呌び出し、次にServiceTrackerを開きたす。これは、OSGiでホワむトボヌドパタヌンを実装するための䞻芁なメカニズムの1぀です。 ぀たり、このこずにより、コンテナ内の特定のタむプのサヌビスを远加/倉曎するずきにロゞックを実行できたす。



  private ServiceTracker<InstrumentationConsumer, Void> initTracker(final BundleContext bundleContext, final InstrumentationProvider instrumentationProvider) { return new ServiceTracker<>(bundleContext, InstrumentationConsumer.class, new ServiceTrackerCustomizer<InstrumentationConsumer, Void>() { @Override public Void addingService(ServiceReference<InstrumentationConsumer> serviceReference) { try { log.trace("addingService called"); final InstrumentationConsumer consumer = bundleContext.getService(serviceReference); log.debug("Consumer: {}", consumer); if (consumer != null) { applyInstrumentation(consumer, instrumentationProvider); } } catch (Throwable t) { log.error("Error on 'addingService'", t); } return null; } @Override public void modifiedService(ServiceReference<InstrumentationConsumer> serviceReference, Void aVoid) { } @Override public void removedService(ServiceReference<InstrumentationConsumer> serviceReference, Void aVoid) { } }); }
      
      





これで、InstrumentationConsumerクラスを実装するサヌビスがコンテナに登録されるたびに、次のこずを行いたす。

 private void applyInstrumentation(InstrumentationConsumer consumer, InstrumentationProvider instrumentationProvider) { final Instrumentation instrumentation; try { instrumentation = instrumentationProvider.getInstrumentation(); consumer.applyInstrumentation(instrumentation); } catch (InstrumentationAgentException e) { log.error("Error on getting insrumentation", e); } }
      
      





java.lang.instrument.Instrumentationオブゞェクトは次のように受信されたす。



 @Component public class InstrumentationProviderImpl implements InstrumentationProvider { private static final Logger log = LoggerFactory.getLogger(InstrumentationProviderImpl.class); @Override public Instrumentation getInstrumentation() throws InstrumentationAgentException { try { final Class<?> agentClass = ClassLoader.getSystemClassLoader().loadClass(INSTRUMENTATION_CLASS_NAME);//     ,   javaagents log.debug("Agent class loaded from system classloader", agentClass); final Field instrumentation = agentClass.getDeclaredField(INSTRUMENTATION_FIELD_NAME);//   reflection log.debug("Instrumentation field: {}", instrumentation); final Object instrumentationValue = instrumentation.get(null); if (instrumentationValue == null) { throw new NullPointerException("instrumentation data is null. Seems agent is not installed"); } return (Instrumentation) instrumentationValue; } catch (Throwable e) { String msg = "Error getting instrumentation"; log.error(msg, e); throw new InstrumentationAgentException("Error getting instrumentation", e); } } }
      
      





怜蚌゚ンゞンの䜜成に移りたしょう。



怜蚌゚ンゞン



倉曎を行うこずが最も効果的なポむントを芋぀けたす-DefaultIssueServiceクラス実際、すべおの䜜成/倉曎呌び出しがこのポむントを通過するわけではありたせんが、これは別のトピックですずそのメ゜ッド



validateCreate



 IssueService.CreateValidationResult validateCreate(@Nullable ApplicationUser var1, IssueInputParameters var2);
      
      





およびvalidateUpdate



 IssueService.UpdateValidationResult validateUpdate(@Nullable ApplicationUser var1, Long var2, IssueInputParameters var3);
      
      





そしお、どのようなロゞックが欠萜しおいるのだろうず思いたす。



メむンロゞックを呌び出した埌、必芁に応じお結果を倉曎できる、コヌドによる初期パラメヌタヌのカスタム怜蚌が呌び出される必芁がありたす。



ByteBuddyは、アむデアを実装するための2぀のオプションを提䟛したす。割り蟌みの支揎ずアドバむスメカニズムの支揎です。 アプロヌチの違いは、Raphaelのプレれンテヌションのスラむドにはっきりず珟れおいたす。









Interceptor APIは十分に文曞化されおおり、どのパブリッククラスもその1぀ずしお機胜できたす 。詳现はこちらをご芧ください 。 Interceptor呌び出しは、元のメ゜ッドの元のバむトコヌドINSTEADに埋め蟌たれおいたす。



この方法を䜿甚しようずするず、2぀の重倧な欠点が芋぀かりたした。





プロゞェクトの䜜業の過皋で、この問題を解決するオプションが生たれたしたが、 アプリケヌション党䜓のクラスをロヌドするロゞックに干枉するため、培底的なテストをせずに䜿甚しおスポむラヌの䞋に眮くこずはお勧めしたせん。



ブヌトロヌダヌの問題を解決する
䞻なアむデアは、WebappClassLoaderの芪チェヌンを切断し、そこにClassLoaderプロキシを挿入するこずです。これは、WebappClassLoaderの実際の芪にダりンロヌドを委任する前に、BundleClassLoaderを䜿甚しおクラスをロヌドしようずしたす。



このように









アプロヌチの実装は次のようになりたす。



 private void tryToFixClassloader(ClassLoader originalClassLoader, BundleWiringImpl.BundleClassLoader bundleClassLoader) { try { final ClassLoader originalParent = originalClassLoader.getParent(); if (originalParent != null) { if (!(originalParent instanceof BundleProxyClassLoader)) { final BundleProxyClassLoader proxyClassLoader = new BundleProxyClassLoader<>(originalParent, bundleClassLoader); FieldUtils.writeDeclaredField(originalClassLoader, "parent", proxyClassLoader, true); } } } catch (IllegalAccessException e) { log.warn("Error on try to fix originalClassLoader {}", originalClassLoader, e); } }
      
      





蚈枬アプリケヌションブロックで䜿甚する必芁がありたす。



 ... .transform((builder, typeDescription, classloader) -> { builder.method(named("validateCreate").and(ElementMatchers.isPublic())).intercept(MethodDelegation.to(Interceptor.class)); if (!ClassUtils.isVisible(InstrumentationConsumer.class, classloader)) { tryToFixClassloader(classloader, (BundleWiringImpl.BundleClassLoader) Interceptor.class.getClassLoader()); } }) .installOn(instrumentation);
      
      





この堎合、WebappClassLoaderを介しおOSGiクラスをロヌドできたす。 泚意すべき唯䞀のこずは、OSGiを䜿甚しおクラスをロヌドしようずしないこずです。そのロヌドは、OSGiの倖郚に委任されたす。 これは明らかにルヌプず䟋倖に぀ながりたす。

BundleProxyClassLoaderコヌド



 class BundleProxyClassLoader<T extends BundleWiringImpl.BundleClassLoader> extends ClassLoader { private static final Logger log = LoggerFactory.getLogger(BundleProxyClassLoader.class); private final Set<T> proxies; private final Method loadClass; private final Method shouldDelegate; public BundleProxyClassLoader(ClassLoader parent, T proxy) { super(parent); this.loadClass = getLoadClassMethod(); this.shouldDelegate = getShouldDelegateMethod(); this.proxies = new HashSet<>(); proxies.add(proxy); } private Method getLoadClassMethod() throws IllegalStateException { try { Method loadClass = ClassLoader.class.getDeclaredMethod("loadClass", String.class, boolean.class); loadClass.setAccessible(true); return loadClass; } catch (NoSuchMethodException e) { throw new IllegalStateException("Failed to get loadClass method", e); } } private Method getShouldDelegateMethod() throws IllegalStateException { try { Method shouldDelegate = BundleWiringImpl.class.getDeclaredMethod("shouldBootDelegate", String.class); shouldDelegate.setAccessible(true); return shouldDelegate; } catch (NoSuchMethodException e) { throw new IllegalStateException("Failed to get shouldDelegate method", e); } } @Override public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { log.trace("Trying to find already loaded class {}", name); Class<?> c = findLoadedClass(name); if (c == null) { log.trace("This is new class. Trying to load {} with OSGi", name); c = tryToLoadWithProxies(name, resolve); if (c == null) { log.trace("Failed to load with OSGi. Trying to load {} with parent CL", name); c = super.loadClass(name, resolve); } } if (c == null) { throw new ClassNotFoundException(name); } return c; } } private Class<?> tryToLoadWithProxies(String name, boolean resolve) { for (T proxy : proxies) { try { final String pkgName = Util.getClassPackage(name); //avoid cycle if(!isShouldDelegatePackageLoad(proxy, pkgName)) { log.trace("The load of class {} should not be delegated to OSGI parent, so let's try to load with bundles", name); return (Class<?>) this.loadClass.invoke(proxy, name, resolve); } } catch (ReflectiveOperationException e) { log.trace("Class {} is not found with {}", name, proxy); } } return null; } private boolean isShouldDelegatePackageLoad(T proxy, String pkgName) throws IllegalAccessException, InvocationTargetException { return (boolean)this.shouldDelegate.invoke( FieldUtils.readDeclaredField(proxy, "m_wiring", true), pkgName ); } }
      
      





誰かがこのアむデアを開発したい堎合に備えお保存したした。



むンストルメンテヌションを実装するための2番目のオプションは、アドバむスを䜿甚するこずです。 この方法は文曞化されたものよりもずっず悪いです。実際、䟋はGithubのチケットずStackOverflowぞの応答でしか芋぀かりたせん。



しかし、すべおがそれほど悪いわけではありたせん
ここで私たちはラファ゚ルに敬意を衚する必芁がありたす-私が芋たすべおの質問ずチケットには詳现な説明ず䟋が提䟛されおいるので、それを理解するこずは難しくありたせん-これらの䜜品が実を結び、さらに倚くのプロゞェクトでバむトバディを芋るこずを願っおいたす。



デフォルトでは、アドバむスメ゜ッドがクラスコヌドに埋め蟌たれおいるずいう点で、最初のものず異なりたす。 私たちにずっお、これは次のこずを意味したす。





完璧に聞こえたす。元の匕数、元のコヌドの結果䟋倖を含む、さらに元のコヌドの前に機胜したアドバむスの結果を取埗できるシックなAPIが提䟛されおいたす。ただし、垞に「but」があり、埋め蟌みにはコヌドにいく぀かの制限があり、埋め蟌みが可胜です。





Byte Buddyのドキュメントにはこれらの制限の説明はありたせんでしたが

、アドバむスのスタむルでロゞックを蚘述しおみたしょう。芚えおいるように、必芁な蚈枬を最小限に抑える必芁がありたす。これは、特定の怜蚌チェックを無芖するこずを意味したす-新しいチェックが衚瀺されたずきに、validateCreate / validateUpdateが呌び出されたずきに実行されるチェックのリストに自動的に远加され、DefaultIssueServiceクラスのコヌドを倉曎する必芁がないこずを確認したす

OSGiでは、これは簡単ですが、DefaultIssueServiceはフレヌムワヌクの範囲倖であり、ここでOSGiテクニックを䜿甚するこずはできたせん。



突然、JIRA APIが圹立ちたす。各アドオンは、このプラグむンを怜玢できる特定のキヌを持぀Pluginクラス倚くの特別な機胜を備えたバンドルのラッパヌのオブゞェクトずしおJIRAで衚されたす。



キヌはアドオン構成で蚭定され、プラグむンAPIはDefaultIssueServiceず同じクラスロヌダヌでロヌドされたす-したがっお、アドバむスでプラグむンを呌び出し、このプラグむンに付属するクラスをロヌドするこずを劚げるものは䜕もありたせんたずえば、チェックアグリゲヌタヌなどです。



その埌、再び暙準のcom.atlassian.jira.component.ComponentAccessorgetOSGiComponentInstanceOfTypeを介しおこのクラスのむンスタンスを取埗できたす。

そしお魔法はありたせん



 public class DefaultIssueServiceValidateCreateAdvice { @Advice.OnMethodExit(onThrowable = IllegalArgumentException.class) public static void intercept( @Advice.Return(readOnly = false) CreateValidationResult originalResult,//        -   (readOnly = false) @Advice.Thrown Throwable throwable,//     -    @Advice.Argument(0) ApplicationUser user, @Advice.Argument(1) IssueInputParameters issueInputParameters ) { try { if (throwable == null) { //current plugin key final Plugin plugin = ComponentAccessor.getPluginAccessor().getEnabledPlugin("org.jrx.jira.instrumentation.issue-validation"); //related aggregator class final Class<?> issueValidatorClass = plugin != null ? plugin.getClassLoader().loadClass("org.jrx.jira.instrumentation.validation.spi.issueservice.IssueServiceValidateCreateValidatorAggregator") : null; final Object issueValidator = issueValidatorClass != null ? ComponentAccessor.getOSGiComponentInstanceOfType(issueValidatorClass) : null;//      API JIRA if (issueValidator != null) { final Method validate = issueValidator.getClass().getMethod("validate", CreateValidationResult.class, ApplicationUser.class, IssueInputParameters.class); if (validate != null) { final CreateValidationResult validationResult = (CreateValidationResult) validate .invoke(issueValidator, originalResult, user, issueInputParameters); if (validationResult != null) { originalResult = validationResult; } } else { System.err.println("==**Warn: method validate is not found on aggregator " + "**=="); } } } //Nothing should break service } catch (Throwable e) { System.err.println("==**Warn: Exception on additional logic of validateCreate " + e + "**=="); } } }
      
      





DefaultIssueServiceValidateUpdateAdviceは、クラス名ずメ゜ッド名に䌌おいたす。目的のメ゜ッドにアドバむスを適甚するInstrumentationConsumerを䜜成したす。



 @Component @ExportAsService public class DefaultIssueServiceTransformer implements InstrumentationConsumer { private static final Logger log = LoggerFactory.getLogger(DefaultIssueServiceTransformer.class); private static final AgentBuilder.Listener listener = new LogTransformListener(log); private final String DEFAULT_ISSUE_SERVICE_CLASS_NAME = "com.atlassian.jira.bc.issue.DefaultIssueService"; @Override public void applyInstrumentation(Instrumentation instrumentation) { new AgentBuilder.Default().disableClassFormatChanges() .with(new AgentBuilder.Listener.Filtering( new StringMatcher(DEFAULT_ISSUE_SERVICE_CLASS_NAME, EQUALS_FULLY), listener )) .with(AgentBuilder.TypeStrategy.Default.REDEFINE) .with(AgentBuilder.RedefinitionStrategy.REDEFINITION) .with(AgentBuilder.InitializationStrategy.NoOp.INSTANCE) .type(named(DEFAULT_ISSUE_SERVICE_CLASS_NAME)) .transform((builder, typeDescription, classloader) -> builder //transformation is idempotent!!! You can call it many times with same effect //no way to add advice on advice if it applies to original class //https://github.com/raphw/byte-buddy/issues/206 .visit(Advice.to(DefaultIssueServiceValidateCreateAdvice.class).on(named("validateCreate").and(ElementMatchers.isPublic()))) .visit(Advice.to(DefaultIssueServiceValidateUpdateAdvice.class).on(named("validateUpdate").and(ElementMatchers.isPublic())))) .installOn(instrumentation); } }
      
      





ここで、玠敵なボヌナスに぀いおお話ししたす。アドバむスの適甚はi等ですアドオンVMを再むンストヌルするず自動的に倉換が行われるため、倉換を2回適甚しないように泚意する必芁はありたせん。



远加機胜
, - , . , JRE (, JAXB ..), — , ..

, .. .



たあ、事は小さいです-アグリゲヌタヌを曞きたす。最初に、怜蚌APIを定矩したす。



 public interface IssueServiceValidateCreateValidator { @Nonnull CreateValidationResult validate( final @Nonnull CreateValidationResult originalResult, final ApplicationUser user, final IssueInputParameters issueInputParameters ); }
      
      





次に、呌び出し時に暙準のOSGiツヌルを䜿甚しお、䜿甚可胜なすべおの怜蚌を取埗しお実行したす。



 @Component @ExportAsService(IssueServiceValidateCreateValidatorAggregator.class) public class IssueServiceValidateCreateValidatorAggregator implements IssueServiceValidateCreateValidator { private static final Logger log = LoggerFactory.getLogger(IssueServiceValidateCreateValidatorAggregator.class); private final BundleContext bundleContext; @Autowired public IssueServiceValidateCreateValidatorAggregator(BundleContext bundleContext) { this.bundleContext = bundleContext; } @Nonnull @Override public IssueService.CreateValidationResult validate(@Nonnull final IssueService.CreateValidationResult originalResult, final ApplicationUser user, final IssueInputParameters issueInputParameters) { try { log.trace("Executing validate of IssueServiceValidateCreateValidatorAggregator"); final Collection<ServiceReference<IssueServiceValidateCreateValidator>> serviceReferences = bundleContext.getServiceReferences(IssueServiceValidateCreateValidator.class, null); log.debug("Found services: {}", serviceReferences); return applyValidations(originalResult, serviceReferences, user, issueInputParameters); } catch (InvalidSyntaxException e) { log.warn("Exception on getting IssueServiceValidateCreateValidator", e); return originalResult; } } private IssueService.CreateValidationResult applyValidations(@Nonnull IssueService.CreateValidationResult originalResult, Collection<ServiceReference<IssueServiceValidateCreateValidator>> serviceReferences, ApplicationUser user, IssueInputParameters issueInputParameters) { IssueService.CreateValidationResult result = originalResult; for (ServiceReference<IssueServiceValidateCreateValidator> serviceReference : serviceReferences) { final IssueServiceValidateCreateValidator service = bundleContext.getService(serviceReference); if (service != null) { result = service.validate(result, user, issueInputParameters); } else { log.debug("Failed to get service from {}", serviceReference); } } return result; } }
      
      





すべお準備完了-収集、蚭定



テスト怜蚌



アプロヌチをテストするために、最も単玔なテストを実装したす。



 @Component @ExportAsService public class TestIssueServiceCreateValidator implements IssueServiceValidateCreateValidator { @Nonnull @Override public IssueService.CreateValidationResult validate(@Nonnull IssueService.CreateValidationResult originalResult, ApplicationUser user, IssueInputParameters issueInputParameters) { originalResult.getErrorCollection().addError(IssueFieldConstants.ASSIGNEE, "This validation works", ErrorCollection.Reason.VALIDATION_FAILED); return originalResult; } }
      
      





新しいタスクを䜜成しおみおください









これで、開発されたアドオンからアドオンを削陀しお再むンストヌルできたす-JIRAの動䜜が正しく倉曎されたす。



おわりに



したがっお、アプリケヌションAPIこの堎合はJIRAを動的に拡匵するツヌルを入手したした。もちろん、このアプロヌチを実皌働で䜿甚する前に、培底的なテストが必芁ですが、私の意芋では、゜リュヌションは完党には匷化されおおらず、適切な研究で、このアプロヌチは「絶望的な問題」を解決するために䜿甚できたす-長呜のサヌドパヌティの欠陥を修正し、APIを拡匵するなど。



プロゞェクト自䜓の完党なコヌドはGithubで芋るこずができたす-健康のためにそれを䜿甚しおください



PS 蚘事を耇雑にしないために、プロゞェクトアセンブリの詳现ずJIRAのアドオンの開発の機胜に぀いおは説明したせんでした。これに぀いおは、こちらをご芧ください。



All Articles