From c2f577d17f6a1fe213bdaec061c3f12482bde556 Mon Sep 17 00:00:00 2001 From: Woonduk Kang Date: Fri, 29 Mar 2013 04:39:25 +0000 Subject: [PATCH] =?UTF-8?q?[=EA=B0=95=EC=9A=B4=EB=8D=95]=20[LUCYSUS-1744]?= =?UTF-8?q?=20agent=EC=9D=98=20classLoader=EC=97=90=EC=84=9C=20slf4j=20log?= =?UTF-8?q?ger=EB=A5=BC=20=EB=A1=9C=EB=93=9C=ED=95=98=EB=8A=94=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EA=B0=9C=EB=B0=9C.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit git-svn-id: http://svn.bds.nhncorp.com/pe/hippo-tomcat-profiler/trunk@1369 84d0f5b1-2673-498c-a247-62c4ff18d310 --- .../java/com/profiler/AgentClassLoader.java | 72 ++++++++++++++---- .../java/com/profiler/ClassPathResolver.java | 27 ++++++- .../java/com/profiler/TomcatProfiler.java | 13 +++- .../java/com/profiler/logging/Logger.java | 13 ++++ .../com/profiler/logging/LoggerBinder.java | 10 +++ .../com/profiler/logging/LoggerFactory.java | 24 ++++++ .../com/profiler/sender/UdpDataSender.java | 11 +-- .../com/profiler/AgentClassLoaderTest.java | 52 ++++++++++--- src/test/lib/hippoClassLoaderTest-1.0.jar | Bin 3910 -> 5463 bytes 9 files changed, 180 insertions(+), 42 deletions(-) create mode 100644 src/main/java/com/profiler/logging/Logger.java create mode 100644 src/main/java/com/profiler/logging/LoggerBinder.java create mode 100644 src/main/java/com/profiler/logging/LoggerFactory.java diff --git a/src/main/java/com/profiler/AgentClassLoader.java b/src/main/java/com/profiler/AgentClassLoader.java index c7243f0ce..b13eb7416 100644 --- a/src/main/java/com/profiler/AgentClassLoader.java +++ b/src/main/java/com/profiler/AgentClassLoader.java @@ -1,9 +1,13 @@ package com.profiler; +import com.profiler.logging.LoggerBinder; + +import java.io.IOException; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.net.URL; import java.net.URLClassLoader; +import java.util.Enumeration; /** * @@ -13,6 +17,7 @@ public class AgentClassLoader { private URLClassLoader classLoader; private String bootClass = "com.profiler.AgentBootStrap"; private String bootMethod = "boot"; + private Object agentBootStrap; public AgentClassLoader(URL[] urls) { ClassLoader classLoader = AgentClassLoader.class.getClassLoader(); @@ -26,34 +31,69 @@ public class AgentClassLoader { public void setBootMethod(String bootMethod) { this.bootMethod = bootMethod; } + public void test() { + URL resource = this.classLoader.getResource("/log4j.xml"); + System.out.println("log4j.xml=" + resource); + URL resource1 = this.classLoader.getResource("/nhn_source/hippo_project/deploy/agent/agentlib/lib/log4j.xml"); + System.out.println("log4j.xml=" + resource1); + + URL[] aaa = this.classLoader.getURLs(); + for (URL url : aaa) { + System.out.println("" + url); + } + + URL resource2 = classLoader.getResource("."); + System.out.println("resource2:" + resource2); + try { + Enumeration resources = this.classLoader.getResources("."); + while (resources.hasMoreElements()) { + URL url = resources.nextElement(); + System.out.println("" + url); + } + } catch (IOException e) { + e.printStackTrace(); + } + } public void boot() { try{ Class bootStrap = this.classLoader.loadClass(bootClass); - Object agentBootStrap = bootStrap.newInstance(); - - Method bootMethod = bootStrap.getDeclaredMethod(this.bootMethod); - - Thread currentThread = Thread.currentThread(); - ClassLoader before = currentThread.getContextClassLoader(); - currentThread.setContextClassLoader(this.classLoader); - try { - bootMethod.invoke(agentBootStrap); - } finally { - currentThread.setContextClassLoader(before); - } + agentBootStrap = bootStrap.newInstance(); + invoke(bootStrap, this.bootMethod); } catch (ClassNotFoundException e) { throw new RuntimeException("boot class not found. Caused:" + e.getMessage(), e); - } catch (NoSuchMethodException e) { - throw new RuntimeException("boot method not found. Caused:" + e.getMessage(), e); - } catch (InvocationTargetException e) { - throw new RuntimeException(this.bootMethod + "() fail. Caused:" + e.getMessage(), e); } catch (InstantiationException e) { throw new RuntimeException("boot create fail. Caused:" + e.getMessage(), e); } catch (IllegalAccessException e) { throw new RuntimeException("boot method invoke fail. Caused:" + e.getMessage(), e); } + } + private Object invoke(Class clazz, String method) { + Method bootMethod = null; + try { + bootMethod = clazz.getDeclaredMethod(method); + } catch (NoSuchMethodException e) { + throw new RuntimeException("boot method not found. Caused:" + e.getMessage(), e); + } + + final Thread currentThread = Thread.currentThread(); + ClassLoader before = currentThread.getContextClassLoader(); + currentThread.setContextClassLoader(this.classLoader); + try { + return bootMethod.invoke(agentBootStrap); + } catch (InvocationTargetException e) { + throw new RuntimeException(this.bootMethod + "() fail. Caused:" + e.getMessage(), e); + } catch (IllegalAccessException e) { + throw new RuntimeException("boot method invoke fail. Caused:" + e.getMessage(), e); + } + finally { + currentThread.setContextClassLoader(before); + } + } + + public LoggerBinder initializeLoggerBinder() { + return (LoggerBinder) invoke(agentBootStrap.getClass(), "initializeLoggerBinder"); } } diff --git a/src/main/java/com/profiler/ClassPathResolver.java b/src/main/java/com/profiler/ClassPathResolver.java index 8677bd80c..b4537b499 100644 --- a/src/main/java/com/profiler/ClassPathResolver.java +++ b/src/main/java/com/profiler/ClassPathResolver.java @@ -29,15 +29,25 @@ public class ClassPathResolver { private String agentJarFullPath; private String agentDirPath; private Pattern agentPattern; + private List fileExtensionList; public ClassPathResolver() { - this.classPath = getClassPathFromSystemProperty(); - this.agentPattern = DEFAULT_AGENT_PATTERN; + this(getClassPathFromSystemProperty()); } + public ClassPathResolver(String classPath) { this.classPath = classPath; this.agentPattern = DEFAULT_AGENT_PATTERN; + this.fileExtensionList = getDefaultFileExtensionList(); + } + + public List getDefaultFileExtensionList() { + List extensionList = new ArrayList(); + extensionList.add("jar"); + extensionList.add("xml"); + extensionList.add("properties"); + return extensionList; } public ClassPathResolver(String classPath, String agentPattern) { @@ -53,7 +63,7 @@ public class ClassPathResolver { this.classPath = getClassPathFromSystemProperty(); } - public String getClassPathFromSystemProperty() { + public static String getClassPathFromSystemProperty() { return System.getProperty("java.class.path"); } @@ -119,6 +129,9 @@ public class ClassPathResolver { jarURLList.add(url); } } + // agentDir 패스도 넣어야 xml을 찾을 때 해당 패스에서 찾음. + URL agentDirUri = toURI(new File(agentLibPath)); + jarURLList.add(agentDirUri); return jarURLList; } @@ -136,7 +149,13 @@ public class ClassPathResolver { return libDir.listFiles(new FileFilter() { @Override public boolean accept(File pathname) { - return pathname.getName().lastIndexOf(".jar") != -1; + String path = pathname.getName(); + for (String extension : fileExtensionList) { + if (path.lastIndexOf("." + extension) != -1) { + return true; + } + } + return false; } }); } diff --git a/src/main/java/com/profiler/TomcatProfiler.java b/src/main/java/com/profiler/TomcatProfiler.java index dd55fd238..cccaec3b2 100644 --- a/src/main/java/com/profiler/TomcatProfiler.java +++ b/src/main/java/com/profiler/TomcatProfiler.java @@ -14,6 +14,8 @@ import java.util.logging.Logger; import com.profiler.common.ServiceType; import com.profiler.config.ProfilerConfig; +import com.profiler.logging.LoggerBinder; +import com.profiler.logging.LoggerFactory; import com.profiler.interceptor.bci.ByteCodeInstrumentor; import com.profiler.interceptor.bci.JavaAssistByteCodeInstrumentor; import com.profiler.modifier.DefaultModifierRegistry; @@ -53,11 +55,16 @@ public class TomcatProfiler implements ClassFileTransformer { List libUrlList = resolveLib(classPathResolver); AgentClassLoader agentClassLoader = new AgentClassLoader(libUrlList.toArray(new URL[libUrlList.size()])); agentClassLoader.setBootClass("com.profiler.boot.BootClassTest"); +// agentClassLoader.test(); try { agentClassLoader.boot(); + LoggerBinder loggerBinder = agentClassLoader.initializeLoggerBinder(); + loggerBinder.getLogger("LoggerFactory initialize start"); + LoggerFactory.initialize(loggerBinder); + com.profiler.logging.Logger tomcatLogger = LoggerFactory.getLogger(TomcatProfiler.class); + tomcatLogger.info("LoggerFactory initialize end"); } catch (Exception e) { - logger.info("test error"); - e.printStackTrace(); //To change body of catch statement use File | Settings | File Templates. + logger.log(Level.INFO, "boot class not found", e); } try { @@ -86,7 +93,7 @@ public class TomcatProfiler implements ClassFileTransformer { return profilerConfig; } - private static List resolveLib(ClassPathResolver classPathResolver) { + private static List resolveLib(ClassPathResolver classPathResolver) { String agentJarFullPath = classPathResolver.getAgentJarFullPath(); logger.info("agentJarPath:" + agentJarFullPath); diff --git a/src/main/java/com/profiler/logging/Logger.java b/src/main/java/com/profiler/logging/Logger.java new file mode 100644 index 000000000..444482695 --- /dev/null +++ b/src/main/java/com/profiler/logging/Logger.java @@ -0,0 +1,13 @@ +package com.profiler.logging; + +/** + * + */ +public interface Logger { + + public void info(String msg); + + + public void info(String format, Object arg); + +} diff --git a/src/main/java/com/profiler/logging/LoggerBinder.java b/src/main/java/com/profiler/logging/LoggerBinder.java new file mode 100644 index 000000000..76e35b52c --- /dev/null +++ b/src/main/java/com/profiler/logging/LoggerBinder.java @@ -0,0 +1,10 @@ +package com.profiler.logging; + +import com.profiler.logging.Logger; + +/** + * + */ +public interface LoggerBinder { + Logger getLogger(String name); +} diff --git a/src/main/java/com/profiler/logging/LoggerFactory.java b/src/main/java/com/profiler/logging/LoggerFactory.java new file mode 100644 index 000000000..9f5c131d7 --- /dev/null +++ b/src/main/java/com/profiler/logging/LoggerFactory.java @@ -0,0 +1,24 @@ +package com.profiler.logging; + +/** + * + */ +public final class LoggerFactory { + + private static LoggerBinder loggerBinder; + + public static void initialize(LoggerBinder loggerBinder) { + if (LoggerFactory.loggerBinder == null) { + System.out.println("set logger binder-------------"); + LoggerFactory.loggerBinder = loggerBinder; + } + } + + public static Logger getLogger(String name) { + return loggerBinder.getLogger(name); + } + + public static Logger getLogger(Class name) { + return getLogger(name.getName()); + } +} diff --git a/src/main/java/com/profiler/sender/UdpDataSender.java b/src/main/java/com/profiler/sender/UdpDataSender.java index ef218e5dd..7f97d2854 100644 --- a/src/main/java/com/profiler/sender/UdpDataSender.java +++ b/src/main/java/com/profiler/sender/UdpDataSender.java @@ -7,7 +7,6 @@ import java.net.InetSocketAddress; import java.net.SocketException; import java.util.ArrayList; import java.util.List; -import java.util.concurrent.CountDownLatch; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; @@ -45,7 +44,6 @@ public class UdpDataSender implements DataSender, Runnable { private HeaderTBaseSerializer serializer = new HeaderTBaseSerializer(); private AtomicBoolean allowInput = new AtomicBoolean(); - private CountDownLatch shutdownLatch = new CountDownLatch(1); public UdpDataSender(String host, int port) { Assert.notNull(host, "host must not be null"); @@ -115,8 +113,9 @@ public class UdpDataSender implements DataSender, Runnable { } try { - shutdownLatch.await(5, TimeUnit.SECONDS); + ioThread.join(5000); } catch (InterruptedException e) { + Thread.currentThread().interrupt(); logger.info("UdpDataSender stopped incompletely."); } @@ -132,7 +131,6 @@ public class UdpDataSender implements DataSender, Runnable { drain: while (true) { try { if (!allowInput.get() && isEmpty()) { - shutdownLatch.countDown(); break; } @@ -144,7 +142,6 @@ public class UdpDataSender implements DataSender, Runnable { while (true) { if (!allowInput.get() && isEmpty()) { - shutdownLatch.countDown(); break; } @@ -155,7 +152,7 @@ public class UdpDataSender implements DataSender, Runnable { } } } catch (Throwable th) { - logger.log(Level.WARNING, "Unexpected Error Cause:" + th.getMessage(), th); + logger.log(Level.WARNING, "Unexpected Error. Cause:" + th.getMessage(), th); } } } @@ -165,7 +162,7 @@ public class UdpDataSender implements DataSender, Runnable { try { sendPacket(dto); } catch (Throwable th) { - logger.log(Level.WARNING, "Unexpected Error Cause:" + th.getMessage(), th); + logger.log(Level.WARNING, "Unexpected Error. Cause:" + th.getMessage(), th); } } } diff --git a/src/test/java/com/profiler/AgentClassLoaderTest.java b/src/test/java/com/profiler/AgentClassLoaderTest.java index 1db3dff22..973d7dc7f 100644 --- a/src/test/java/com/profiler/AgentClassLoaderTest.java +++ b/src/test/java/com/profiler/AgentClassLoaderTest.java @@ -1,16 +1,19 @@ package com.profiler; -import org.junit.Assert; +import com.profiler.logging.LoggerBinder; import org.junit.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.File; -import java.net.MalformedURLException; +import java.io.IOException; import java.net.URL; import java.security.CodeSource; import java.security.ProtectionDomain; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; /** * @@ -20,22 +23,47 @@ public class AgentClassLoaderTest { private Logger logger = LoggerFactory.getLogger(this.getClass()); @Test - public void boot() throws MalformedURLException, ClassNotFoundException { + public void boot() throws IOException, ClassNotFoundException { String projectDir = getProjectLibDir(); logger.info("lib:" + projectDir); + URL[] lib = getLib(projectDir); + logger.info("lib list:" + Arrays.toString(lib)); +// String testJar = "hippoClassLoaderTest-1.0.jar"; +// logger.info("load lib:" + testJar); +// +// String testJarPath = projectDir + File.separator + testJar; +// logger.info("load testlib:" + testJarPath); +// File file = new File(testJarPath); +// Assert.assertTrue(file.exists()); +// String log4j = projectDir + File.separator + "log4j.xml"; +// File log4jFile = new File(log4j); - String testJar = "hippoClassLoaderTest-1.0.jar"; - logger.info("load lib:" + testJar); - - String testJarPath = projectDir + File.separator + testJar; - logger.info("load testlib:" + testJarPath); - File file = new File(testJarPath); - Assert.assertTrue(file.exists()); - AgentClassLoader agentClassLoader = new AgentClassLoader(new URL[]{file.toURI().toURL()}); + AgentClassLoader agentClassLoader = new AgentClassLoader(lib); agentClassLoader.setBootClass("com.profiler.boot.BootClassTest"); agentClassLoader.boot(); + + LoggerBinder loggerBinder = agentClassLoader.initializeLoggerBinder(); + com.profiler.logging.Logger test = loggerBinder.getLogger("test"); + test.info("slf4j logger test"); + + } + + private URL[] getLib(String libDir) throws IOException { + System.out.println(libDir); + File libFile = new File(libDir); + System.out.println("file ab:" + libFile.getAbsolutePath()); + System.out.println("file ca:" + libFile.getCanonicalPath()); + File[] list = libFile.listFiles(); + List arrayList = new ArrayList(); + for (File file : list) { + URL url = file.toURI().toURL(); + System.out.println(url); + arrayList.add(url); + } + + return arrayList.toArray(new URL[arrayList.size()]); } private String getProjectLibDir() { @@ -50,7 +78,7 @@ public class AgentClassLoaderTest { if (dirPath == -1) { throw new RuntimeException("target/classes/ not found"); } - String projectDir = path.substring(0, dirPath); + String projectDir = path.substring(1, dirPath); return projectDir + "src/test/lib"; } } diff --git a/src/test/lib/hippoClassLoaderTest-1.0.jar b/src/test/lib/hippoClassLoaderTest-1.0.jar index 529e5109ac4c847750272f09874c4a9b45cafa92..93e445a3fc2953c5a3dc98ca4e485739ff7c6dd8 100644 GIT binary patch delta 3750 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