slf4j + logback 환경에서 이니지스 결재 모듈(INIpay50.jar) 사용 시 이니시스 로깅에 문제가 발생한다.
이니시스측에서도 log4j 를 사용하라고 권고한 상황..
하지만 쓰고 싶다면...
'org.slf4j:log4j-over-slf4j' 부분을 exclude 하면 된다.
※ 'org.slf4j:log4j-over-slf4j' 는 기존 라이브러리가 log4j를 사용하고 있을때, slf4j로 넘겨주는 역할은 하는거란다.
slf4j + logback 환경에서 이니지스 결재 모듈(INIpay50.jar) 사용 시 이니시스 로깅에 문제가 발생한다.
이니시스측에서도 log4j 를 사용하라고 권고한 상황..
하지만 쓰고 싶다면...
'org.slf4j:log4j-over-slf4j' 부분을 exclude 하면 된다.
※ 'org.slf4j:log4j-over-slf4j' 는 기존 라이브러리가 log4j를 사용하고 있을때, slf4j로 넘겨주는 역할은 하는거란다.
Gradle 에서 jar를 생성할때 의존된 jar들 포함시켜서 말고 싶을때가 있다.
https://github.com/musketyr/gradle-fatjar-plugin
buildscript { repositories { jcenter() } dependencies { classpath 'eu.appsatori:gradle-fatjar-plugin:0.3' } } apply plugin: 'eu.appsatori.fatjar'
현재 겉모습은 제니퍼의 X-View 와 비슷하게 따라 하려는중이다.ㅎㅎ
1차적으로 요청 URI, 요청 시간, 종료 시간, 처리 시간, HttpStatus 값 정도만 차트에 표현하였다.
구조는 Tomat에 Agent를 띄운 후 Agent에서 모니터링툴 WAS에 WebSocket 로 보낸 후 데이터를 받은 모니터링툴에서 모니터링에 접속한 Client Browser 에 WebSocket로 데이터를 보내주는 방식이다.
모니터링툴에 붙은 Client가 많았을 경우 어떻게 될지 아직 잘 모르겠다.
상황봐서 Queue에 담아서 한다던가 고려해볼 생각이고 현재에는 비슷하게 구현만 하는게 목적이다.
인터넷의 자료를 통해서 javassist 를 활용하여 Byte Code Instrumentation 시도해보았지만, 일반적인 Java Main Program은 잘 되는데 Tomcat 의 특정 Class를 제어 하려고 했더니 잘 되지 않았다.
많은 삽질 끝에 ClassPool을 얻어 오는 부분을 조금 수정 하니 잘 되었다.
(이 부분때문에 몇일을 삽질 하다.. 포기도 할까 하고, Application Level Servlet Filter를 이용하는 방법으로 선회 할까도 했음...)
검색을 하두 해서 어디서 이 정보를 얻었는지 기억이 안나네요.
[해결 부분]
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
ClassPool pool = ClassPool.getDefault();
pool.insertClassPath(new LoaderClassPath(classLoader));
[2015. 7. 6.]
WAS Application 요청시 org.apache.catalina.core.StandardEngineValve.invoke(Request request, Response response) 부분을 잡아 처리 하였다.
(만들어 가며 바뀔 수도 있는 부분임..)
이렇게 함으로 인해서 어떠한 문제가 발생할지는 미지수임...
HttpStatus=302 케이스에서 문제 발생
[2015. 8. 10.]
javax.servlet.http.HttpServlet.service(javax.servlet.http.HttpServletRequest,javax.servlet.http.HttpServletResponse 으로 변경
이러면.. JSP Direct 호출이 안 잡히는군.. 쉽지 않구만...
다시 org.apache.catalina.core.StandardEngineValve.invoke(Request request, Response response) 원복!!
Spring Security LOGOUT HttpStatus 302일때문 문제가 발생!!
문제 발생 부분 request.getSession().getId()
이 부분을 Before 일때만 하고, After 일때는 주석
Before, After 요청 정보 맵핑을 Thread.currentThread().getId() 로 처리
우선 잘 된다...
[MonitorAgent.java]
package pe.kr.ddakker.monitor.agent; import java.lang.instrument.Instrumentation; import pe.kr.ddakker.monitor.agent.transformer.TomcatTransformer; public class MonitorAgent { public static void premain(String args, Instrumentation inst) throws Exception { inst.addTransformer(new TomcatTransformer()); } public static void agentmain(String args, Instrumentation inst) throws Exception { premain(args, inst); } }
[TomcatTransformer.java]
package pe.kr.ddakker.monitor.agent.transformer; import java.lang.instrument.ClassFileTransformer; import java.lang.instrument.IllegalClassFormatException; import java.security.ProtectionDomain; import javassist.CannotCompileException; import javassist.ClassPool; import javassist.CtBehavior; import javassist.CtClass; import javassist.LoaderClassPath; import javassist.NotFoundException; /** * Tomcat 요청 가로채서 수정!! * @author ddakker 2015. 6. 14. */ public class TomcatTransformer implements ClassFileTransformer { ClassPool pool = null; public TomcatTransformer() { this.pool = ClassPool.getDefault(); } public byte[] transform(ClassLoader loader, String className, Class redefiningClass, ProtectionDomain domain, byte[] bytes) throws IllegalClassFormatException { if (className.contains("StandardEngineValve")) { System.out.println("className: " + className); return transformClass(redefiningClass, bytes); } else { return bytes; } } private byte[] transformClass(Class classToTransform, byte[] b) { CtClass cl = null; try { ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); ClassPool pool = ClassPool.getDefault(); pool.insertClassPath(new LoaderClassPath(classLoader)); if (pool != null) { cl = pool.makeClass(new java.io.ByteArrayInputStream(b)); if (cl.isInterface() == false) { CtBehavior[] methods = cl.getDeclaredBehaviors(); for (int i = 0; i < methods.length; i++) { System.out.println("methods[" + i + "]: " + methods[i]); if (methods[i].isEmpty() == false) { doTransform(methods[i]); } } } b = cl.toBytecode(); } } catch (Exception e) { System.err.println("e111: " + e); } finally { if (cl != null) { cl.detach(); } } return b; } private void doTransform(CtBehavior method) throws NotFoundException, CannotCompileException { if (method.getName().equals("invoke")) { System.out.println("0"); try { System.out.println("1"); method.insertBefore("" + "String jvmRoute = System.getProperty(\"jvmRoute\");" + "String sessionId = $1.getSession().getId();" + "String uri = $1.getRequestURI();" + "pe.kr.ddakker.monitor.websocket.WSClient.send(\"{" + "server: '\" + jvmRoute + \"' " + ", sessionId: '\" + sessionId + \"' " + ", uri: '\" + uri + \"' " + ", stTime: '\" + System.currentTimeMillis() + \"' " + "}\");"); System.out.println("2"); method.insertAfter("" + "String jvmRoute = System.getProperty(\"jvmRoute\");" + "String sessionId = $1.getSession().getId();" + "String uri = $1.getRequestURI();" + "pe.kr.ddakker.monitor.websocket.WSClient.send(\"{" + "server: '\" + jvmRoute + \"' " + ", sessionId: '\" + sessionId + \"' " + ", uri: '\" + uri + \"' " + ", edTime: '\" + System.currentTimeMillis() + \"' " + ", status: '\" + $2.getStatus() + \"' " + "}\");"); System.out.println("3"); } catch (Exception e) { System.err.println("Aa e: " + e); } System.out.println("4"); } } }
[build.gradle]
jar { manifest { attributes 'Implementation-Title': 'Gradle Quickstart', 'Implementation-Version': version, 'Premain-Class': 'pe.kr.ddakker.monitor.agent.MonitorAgent', 'Agent-Class': 'pe.kr.ddakker.monitor.agent.MonitorAgent', 'Can-Redefine-Classes': true } }
instrument 시 필요한 외부 Library들은 Agent jar에 포함 시켰다.
[실행]
-javaagent:D:\..\tomcat-monitor-agent-1.0.jar
[context-cache.xml]
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:util="http://www.springframework.org/schema/util" xmlns:cache="http://www.springframework.org/schema/cache" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd http://www.springframework.org/schema/cache http://www.springframework.org/schema/cache/spring-cache.xsd"> <cache:annotation-driven /> <!-- EHCache Local 형 --> <bean id="ehCacheManager" class="org.springframework.cache.ehcache.EhCacheCacheManager"> <property name="cacheManager"> <bean class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean"> <property name="configLocation" value="classpath:config/cache/ehcache.xml"></property> </bean> </property> </bean> <!-- Infinispan Server 형 --> <util:properties id="hotrod_data" location="classpath:properties/hotrod_data.properties" /> <bean id="ispnCacheManager" class="org.infinispan.spring.provider.SpringRemoteCacheManagerFactoryBean"> <property name="configurationProperties" ref="hotrod_data" /> </bean> <bean id="cacheManager" class="org.springframework.cache.support.CompositeCacheManager"> <property name="cacheManagers"> <list> <ref bean="ehCacheManager" /> <ref bean="ispnCacheManager" /> </list> </property> </bean> </beans>
[ehcache.xml]
<?xml version="1.0" encoding="UTF-8"?> <ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd" updateCheck="false"> <diskStore path="java.io.tmpdir" /> <defaultCache maxElementsInMemory="50000" eternal="false" timeToIdleSeconds="300" timeToLiveSeconds="600" overflowToDisk="false" diskPersistent="false" diskExpiryThreadIntervalSeconds="120" memoryStoreEvictionPolicy="LRU"> </defaultCache> <cache name="EHCACHE_MENU" maxElementsInMemory="10000" eternal="false" timeToIdleSeconds="0" timeToLiveSeconds="600" overflowToDisk="false" diskPersistent="false" diskExpiryThreadIntervalSeconds="120" memoryStoreEvictionPolicy="LRU"> </cache> </ehcache>
[Service.java]
@Cacheable("ISPN_EVENT") public Map getEvent(String eventKey) { return query... } @Cacheable("EHCACHE_MENU") public Map getMenu(String menuKey) { return query... }
Jolokia를 알게되서 최근까지 Servlet Container가 지원되는 부분에서만 이용했었는데 Servlet Container가 지원되지 않는 JVM 기반 Daemon의 MBean 을 모니터링 할 일이 생겨서 봤더니, Jolokia 에 JVM-Agent 있었군..
#!/bin/sh .. .. PID=`ps -ef | grep java | grep "$DAEMON_NAME " | awk '{print $2}'` echo " +$PID" java -jar ...../jolokia-jvm-1.3.1-agent.jar start $PID --port=$MONITOR_PORT --host=$BIND_ADDR # -- log # +19102 # Started Jolokia for PID 19102 # http://---:---/jolokia/
최근 fastjson 이란 알리바바에서 공개한 JSON Parser 을 보게되어 현재 회사에서 사용중인 Library 들을 비교해 보았다.
jackson-databind-2.1.3.jar
json-lib-2.2.1-jdk15.jar
fastjson-1.2.5.jar
[테스트 상황]
Vo 안에 2개의 Vo가 있는 객체 활용하여, to JSON String, to JAVA Object 로 변환 및 역변환을 10,000 실행하였다.
서버 모니터링 시스템 구축 중 Tomcat Comet으로 했던걸 WebSocket로 변경 하였다.
단순함!
WebSocket Servlet 에서는 Client Open/Close 만 관리하는 public static Set 객체에 Client 정보를 관리하고, Queue Receive 에서 해당 Set 객체에 접근 하여 각각의 Client 에 데이터를 Push 한다.
Thread 간 통신에 public static 변수를 사용하는게 좋은건지 모르겠으나.. Thread 관련 지식이 부족한 관계로 현재는 우선 패스한다.
import java.util.Set; import java.util.concurrent.CopyOnWriteArraySet; import javax.websocket.OnClose; import javax.websocket.OnError; import javax.websocket.OnMessage; import javax.websocket.OnOpen; import javax.websocket.Session; import javax.websocket.server.ServerEndpoint; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @ServerEndpoint(value = "/ws/was") public class WasWebSocket { private static Logger log = LoggerFactory.getLogger(WasWebSocket.class); public static Setconnections = new CopyOnWriteArraySet<>(); private Session session; public WasWebSocket() { log.info("----------- thread start"); } public Session getSession() { return this.session; } @OnOpen public void onOpen(Session session) { log.info("onOpen this: {}, this.sessson: {}", this.getClass(), session); this.session = session; synchronized (connections){ connections.add(this); } } @OnClose public void onClose() { log.info("onClose"); synchronized (connections){ connections.remove(this); } } @OnMessage public void onMessage(String message) { log.info("@OnMessage"); log.info("websocket message: " + message); } @OnError public void onError(Throwable t) throws Throwable { log.error("e: ", t.toString()); synchronized (connections){ connections.remove(this); } } }
function wasStatusListen() { var oSocket = new WebSocket(WS_URL + "/ws/was"); oSocket.onmessage = function (e) { var json = eval("(" + e.data + ")"); if (json.result && json.result == "0000") { var wasNode = json.data.name; var heapUsedPercent = json.data.heapUsedPercent; .. .. 처리 } }; oSocket.onopen = function (e) { console.log("open"); }; oSocket.onclose = function (e) { console.log("close"); wasStatusListen(); }; window.unload = function() { if (oSocket.readyState != 3) oSocket.disconnect(); } } wasStatusListen();
Authentication Basic 로 인증되는 서버에서 제공하는 API를 이용 하는 중 Spring RestTemplate 활용 방법
[환경]
Spring-4.1.6
HttpClient-4.3.5
1. 호출 URL 에서 인증 하는 방법
Map<String, Object> resultMap = ezRestTemplate.get("http://admin:1234@localhost:12345/api/overview", HashMap.class);
2. 설정 부분에서 인증 하는 방법
[설정]
import java.security.KeyManagementException; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; import javax.net.ssl.SSLContext; import org.apache.http.auth.AuthScope; import org.apache.http.auth.UsernamePasswordCredentials; import org.apache.http.client.HttpClient; import org.apache.http.conn.ssl.AllowAllHostnameVerifier; import org.apache.http.conn.ssl.SSLConnectionSocketFactory; import org.apache.http.conn.ssl.SSLContexts; import org.apache.http.conn.ssl.TrustSelfSignedStrategy; import org.apache.http.impl.client.BasicCredentialsProvider; import org.apache.http.impl.client.HttpClientBuilder; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.ImportResource; import org.springframework.http.client.ClientHttpRequestFactory; import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; import org.springframework.web.client.RestTemplate; import pe.kr.ddakker.framework.support.wrapper.http.EzRestTemplate; @ImportResource("classpath:config/spring/context-*.xml") @Configuration public class ApplicationContext { @Bean(name = "ezRestTemplate") public EzRestTemplate getEzRestTemplate() throws KeyManagementException, NoSuchAlgorithmException, KeyStoreException { SSLContext sslContext = SSLContexts.custom().loadTrustMaterial(null, new TrustSelfSignedStrategy()).useTLS().build(); SSLConnectionSocketFactory connectionFactory = new SSLConnectionSocketFactory(sslContext, new AllowAllHostnameVerifier()); BasicCredentialsProvider credentialsProvider = new BasicCredentialsProvider(); credentialsProvider.setCredentials(AuthScope.ANY, new UsernamePasswordCredentials("admin", "1234")); HttpClient httpClient = HttpClientBuilder.create() .setSSLSocketFactory(connectionFactory) .setDefaultCredentialsProvider(credentialsProvider) .build(); ClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory(httpClient); return new EzRestTemplate(new RestTemplate(requestFactory)); } }
[사용]
// EzRestTemplate 는 RestTemplate 감쌓놓은 Wrapper Class임. @Resource EzRestTemplate ezRestTemplate; Map<String, Object> resultMap = ezRestTemplate.get("http://localhost:12345/api/overview", HashMap.class); System.out.println("resultMap: " + resultMap);
서버 모니터링 시스템을 만들고 있다.
구조는 서버의 MBean 및 Background 테스팅 도구가 실행한 정보를 각각의 N대의 서버에서 Message Qeue에 전달 하고, 모니터링 WAS에서 Queue 를 Receive 하여 Tomcat Long Pooling 을 활용하여 N대의 Client Browser 에 전송 해 준다.
public static List connections = new ArrayList<>(); 부분이 접속된 Client Browser 정보이므로, Queue Receive Event 시점에 connections Client 갯수만큼 PrintWriter 활용 해서 write 하고, connections 의 CometEvent 객체를 Close 하고, connections에서 Remove() 시킨다.
접속된 Client 정보를 public static 변수로 다른 Thread 에서 제어 하는게 잘 하는건지는 모르겠지만. 잘 된다...
사실 처음에 Tomcat Comet 방식을 이용했지만 지금은 WebSocket 방식으로 동일 하게 활용중이다.
다음 글은 동일한 상황에서 Comet 를 Websocket로 변환.. 거의 동일함..
import java.io.IOException; import java.io.PrintWriter; import java.util.ArrayList; import java.util.List; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.catalina.comet.CometEvent; import org.apache.catalina.comet.CometProcessor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @WebServlet("/comet/testCase") public class TestCaseServlet extends HttpServlet implements CometProcessor { private static Logger log = LoggerFactory.getLogger(TestCaseServlet.class); public static Listconnections = new ArrayList<>(); public void init() throws ServletException { log.info("----------- thread start"); } public void destroy() { log.info("---------- thread stop"); synchronized (connections) { connections.clear(); } } @Override public void event(CometEvent event) throws IOException, ServletException { log.info("testCase event: " + event.getEventType()); HttpServletRequest request = event.getHttpServletRequest(); HttpServletResponse response = event.getHttpServletResponse(); response.setCharacterEncoding("UTF-8"); event.setTimeout(1000*60*60); if (event.getEventType() == CometEvent.EventType.BEGIN) { log.info("Begin for request: {}, response: {}, session: {}", request, response, request.getSession(true).getId()); synchronized(connections) { connections.add(event); } } else if (event.getEventType() == CometEvent.EventType.ERROR) { log.info("Error for session: {}", request.getSession(true).getId()); synchronized(connections) { connections.remove(event); } PrintWriter writer = response.getWriter(); writer.println("{result: '9999', msg: 'timeout'}"); event.close(); } else if (event.getEventType() == CometEvent.EventType.END) { log.info("End for session: " + request.getSession(true).getId()); synchronized(connections) { connections.remove(event); } PrintWriter writer = response.getWriter(); writer.println("{result: '0000', msg: 'end'}"); event.close(); } else if (event.getEventType() == CometEvent.EventType.READ) { log.info("read"); while (true) { ; } } } }
WebSocket 관련 개발을 하다가 데이터 송/수신 관련 데이터만 보고 싶다면..
Chrome 웹 스토어에서 "Dark WebSocket Terminal" 를 활용!!