Programming blog

This is a programming blog


  • Home

  • Tags

  • Categories

  • Archives

  • Sitemap

  • Search

[AOP] Monitor Legacy System

Posted on 2016-12-09 | In Java |

最近要在一個舊的scala系統加入一些監控的程式碼,找出影響效能的地方.

想了兩個做法可以解決:

方法一: 在懷疑的程式碼地方上下夾log,計算出執行時間.

方法二: 利用AOP方式攔截每個function執行時開始點和結束點,計算出執行時間.

來分析一下這兩個方法的優缺點:

方法一:

優點:不用大腦就可以解決

缺點:需要在所有的點插入log,造成程式充滿一堆log

方法二:

優點:可以不用動到原本的程式碼

缺點:需要寫一些程式碼

後來採用Aspectj來解決這個問題.

  • 新增一個Monitor Aspect

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    package aspectj;

    import org.aspectj.lang.JoinPoint;
    import org.aspectj.lang.annotation.After;
    import org.aspectj.lang.annotation.Aspect;
    import org.aspectj.lang.annotation.Before;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;

    @Aspect
    public class MonitorAspect {
    private long startTime;
    private Logger logger = LoggerFactory.getLogger(MonitorAspect.class);

    @Before(value = "execution (* *(..))", argNames = "joinPoint")
    public void before(JoinPoint joinPoint) {
    startTime = System.currentTimeMillis();
    }

    @After(value = "execution (* *(..))", argNames = "joinPoint")
    public void after(JoinPoint joinPoint) {
    long executionTime = System.currentTimeMillis() - startTime;
    logger.debug("Signature : {}, Source Line : {}, Execute Time : {}",
    joinPoint.getSignature(),
    joinPoint.getSourceLocation(),
    executionTime);
    }
    }
  • 加入aspectj jar

    1
    2
    "org.aspectj" % "aspectjweaver" % "1.8.7",
    "org.aspectj" % "aspectjrt" % "1.8.7"
  • 利用load time weaving方式.需要在resource資料夾下新增META-INF/aop.xml

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    <aspectj>
    <aspects>
    <aspect name="aspectj.MonitorAspect"/>
    </aspects>

    <weaver options="-verbose -showWeaveInfo">
    <include within="xxx.yyy.*"/>
    <include within="xxx.zzz.*"/>
    <include within="aspectj.*"/>
    </weaver>
    </aspectj>

詳細參數可以參考:Chapter 5. Load-Time Weaving

  • 因為舊系統是用java啟動,所以需要加入javaagent參數,指定aspectjweaver.jar的位置,例如:java -javaagent=/jars/aspectjweaver-1.8.7.jar ...

在實作過程中遇到了不少的坑,紀錄一下:

  • 第一個坑:

在實作Aspect時,Annotation中可以准許不用對每個參數給值,但是會噴錯…

以下寫法是准許的,但是load time weaving時會噴錯

1
@Before("execution (* *(..))")

後來改成以下寫法就沒事了

1
@Before(value = "execution (* *(..))", argNames = "joinPoint")

  • 第二個坑:

為了可以動態修改aop.xml,必須將這個檔案從jar拉出來放.可以透過-Dorg.aspectj.weaver.loadtime.configuration=file:{aop.xml位置}
來指定aop位置.

但是啟動程式時卻又噴錯java.lang.RuntimeException: Cannot register non aspect:...,
原因是javax.management.remote.rmi.NoCallStackClassLoader
不能載入我寫好的aspect.(這邊日後有時間需要來研究一下,應該是執行順序的問題…)

透過-Daj.weaving.loadersToSkip=javax.management.remote.rmi.NoCallStackClassLoader
略過它就可以執行了.
詳細可參考:AspectJ: ClassLoading issue when trying to use external aop.xml file

  • 第三個坑:

一開始沒有在aop.xml的weaver tag中加入MonitorAspect的位置,又噴錯
java.lang.NoSuchMethodError: aspectj.MonitorAspect.aspectOf()Laspectj/MonitorAspect
加入MonitorAspect的位置就正常了!
詳細可參考:AspectJ java.lang.NoSuchMethodError: aspectOf

  • 第四個坑:

為了可以在aop.xml動態指定要waving的type,千萬不能在annotation的value欄位寫死.

例如:

1
@Before(value = "execution (* xxx.yyy.*(..))", argNames = "joinPoint")

1
2
3
4
5
6
7
8
9
10
11
<aspectj>
<aspects>
<aspect name="aspectj.MonitorAspect"/>
</aspects>

<weaver options="-verbose -showWeaveInfo">
<include within="xxx.yyy.*"/>
<include within="xxx.zzz.*"/>
<include within="aspectj.*"/>
</weaver>
</aspectj>

結果:只能waving到xxx.yyy.*,不能waving xxx.zzz.*

改成以下:

1
@Before(value = "execution (* *(..))", argNames = "joinPoint")

1
2
3
4
5
6
7
8
9
10
11
<aspectj>
<aspects>
<aspect name="aspectj.MonitorAspect"/>
</aspects>

<weaver options="-verbose -showWeaveInfo">
<include within="xxx.yyy.*"/>
<include within="xxx.zzz.*"/>
<include within="aspectj.*"/>
</weaver>
</aspectj>

結果:可以waving到xxx.yyy.*和xxx.zzz.*

心得:

AOP的概念真的很強大,可以讓你不費吹灰之力去監控Legacy System.不用更改舊有程式,也不會有多餘且重複的程式碼,之後可以來想看看有什麼地方可以利用AOP.

因為scala版本太舊了(2.9.1, jdk 6),利用compile weaving方式失敗了,只好用load time weaving方式.
假如採用較新的scala版本或許可以考慮 sbt-aspectj.

參考:

  • AspectJ with Akka and Scala
  • AspectJ: ClassLoading issue when trying to use external aop.xml file
  • AspectJ java.lang.NoSuchMethodError: aspectOf
  • Chapter 5. Load-Time Weaving
  • sbt-aspectj

[Jersey] RESTful service starter-kit (2)

Posted on 2016-12-09 | Edited on 2016-12-13 | In Java |

如何設定Custom Injection

場景:
client發送get request,會在header帶一個userId,server會根據userId去資料庫查詢相關user.
例如:

1
curl -H 'userId: 89ce5a60-a88f-11e5-a837-0800200c9a66' http://localhost:8080/user

PS:正規的做法是把userId放在uri path中,這邊只是模擬從header抓取資料進行處理,之後可以透過annotation方式將結果放到resource

  • 定義annotation

    1
    2
    3
    4
    @Retention(RetentionPolicy.RUNTIME)
    @Target({ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD})
    public @interface UserParam {
    }
  • 定義ValueFactoryProvider

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    @Singleton
    @Slf4j //lombok annotation
    public final class UserParamValueFactoryProvider extends AbstractValueFactoryProvider {

    @Singleton
    public static final class InjectionResolver extends ParamInjectionResolver<UserParam> {
    public InjectionResolver() {
    super(UserParamValueFactoryProvider.class);
    }
    }

    @Inject
    public UserParamValueFactoryProvider(MultivaluedParameterExtractorProvider mpep, ServiceLocator injector) {
    super(mpep, injector, Parameter.Source.UNKNOWN);
    }

    @Override
    public AbstractContainerRequestValueFactory<?> createValueFactory(Parameter parameter) {
    Class<?> classType = parameter.getRawType();

    if (classType == null || (!classType.equals(User.class))) {
    log.warn("UserParam annotation was not placed on correct object type; Injection might not work correctly!");
    return null;
    }

    return new AbstractContainerRequestValueFactory<User> (){
    @Context
    private ResourceContext context;
    @Inject
    private Repo repo;

    public User provide() {
    final ContainerRequestContext context = this.context.getResource(ContainerRequestContext.class);
    final UUID userId = UUID.fromString(context.getHeaderString("userId"));
    return repo.getUser(userId);
    }
    };
    }
    }

Repo

1
2
3
public interface Repo {
User getUser(UUID uuid);
}

RepoFactory

1
2
3
4
5
6
7
8
9
10
11
public class RepoFactory implements Factory<Repo> {
@Override
public Repo provide() {
return new DummyRepo();
}

@Override
public void dispose(Repo instance) {

}
}

  • bind UserParamValueFactoryProvider

    bind(UserParamValueFactoryProvider.class).to(ValueFactoryProvider.class).in(Singleton.class);
    1
    2
    bind(UserParamValueFactoryProvider.InjectionResolver.class).to(new TypeLiteral<InjectionResolver<UserParam>>() {}).in(Singleton.class);
    bindFactory(RepoFactory.class).to(Repo.class).in(Singleton.class);
  • 透過annotation取得User

    1
    2
    3
    4
    5
    6
    @GET
    @ApiOperation(value = "Return user data", response = User.class)
    public Response user(@NotNull @Valid @UserParam User user) {
    return Response.ok(user, MediaType.APPLICATION_JSON)
    .build();
    }

如何設定Swagger API

因為是透過ResourceConfig去設定JAX-RS,所以只要將以下程式碼加入到ResourceConfig就完成Swagger API設定

1
2
3
4
5
6
7
8
9
10
11
12
register(ApiListingResourceJSON.class);
register(SwaggerSerializers.class);

BeanConfig beanConfig = new BeanConfig();
beanConfig.setVersion("1.0.1");
beanConfig.setResourcePackage("rest"); // replace with your packages
beanConfig.setBasePath("http://localhost:8080/jersey-starterkit/");
beanConfig.setDescription("My RESTful resources");
beanConfig.setTitle("My RESTful API");
beanConfig.setFilterClass(ApiAuthorizationFilterImpl.class.getName());
beanConfig.setPrettyPrint(true);
beanConfig.setScan(true);

假如想要用apiKey限制使用者存取swagger.json,可以繼承SwaggerSpecFilter,在beanConfig.setFilterClass設定路徑即可.

呼叫swagger.json:

1
curl http://localhost:8080/swagger.json

程式碼:jersey-starterkit

參考資料:

  • How to inject an object into jersey request context?
  • Chapter 22. Custom Injection and Lifecycle Management
  • How to bind custom context to Jersey request
  • Using HK2 with Jersey (with auto-scanning!)
  • Swagger

[Jersey] Using HK2 to inject services into background tasks

Posted on 2016-12-09 | In Java |

Jersey採用HK2作為Dependency Injection framework,可以透過簡單的設定將實作的程式inject到interface.有使用過這樣功能的使用者會發現到,僅限於Restful layer可以做inject,非Restful layer就不能inject.

例如:

Interface:

1
2
3
4
@Contract
public interface Bar {
void bar();
}

Implement Bar

1
2
3
4
5
6
7
@Service
public class BarImpl implements Bar {
@Override
public void bar() {
log.info("Bar");
}
}

Background task

1
2
3
4
5
6
7
8
9
10
11
12
public class CrazyThread {
@Inject
Bar bar;

public void run() {
new Thread(() -> {
while (true) {
bar.bar();
}
}).start();
}
}

Inject BarImpl into Bar

1
2
3
4
5
6
7
8
9
10
11
12
public class MyApplication extends ResourceConfig {
public MyApplication() {

this.register(new AbstractBinder() {
@Override
protected void configure() {
bind(BarImpl.class).to(Bar.class).in(Singleton.class);
});

new CrazyThread().run();
}
}

會發現沒有辦法順利將BarImpl注射到Bar,因為CraztThread並不在container中,所以沒有辦法順利注射.

首先,我們可以透過HK2 Metadata Generator幫我們達成,它會產生注射的設定檔到META-INF/hk2-locator/default下.要確認Jersey使用的hk2版本,再匯入相關的jar檔.例如:jersey 2.22.1使用的hk2版本為2.4.0-b31,hk2-utils,hk2-api,hk2-metadata-generator一定要為2.4.0-b31.

1
2
3
4
compile 'javax.inject:javax.inject:1'
compile group: 'org.glassfish.hk2', name: 'hk2-utils', version: '2.4.0-b31'
compile group: 'org.glassfish.hk2', name: 'hk2-api', version: '2.4.0-b31'
compile group: 'org.glassfish.hk2', name: 'hk2-metadata-generator', version: '2.4.0-b31'

修改CrazyThread,加入@Service

1
2
3
4
5
6
7
8
9
10
11
12
13
@Service
public class CrazyThread {
@Inject
Bar bar;

public void run() {
new Thread(() -> {
while (true) {
bar.bar();
}
}).start();
}
}

最後修改MyApplication,透過ServiceLocator取得CrazyThread

1
2
3
4
5
6
7
public class MyApplication extends ResourceConfig {
public MyApplication() {
ServiceLocator serviceLocator = ServiceLocatorUtilities.createAndPopulateServiceLocator();
final CrazyThread crazyThread = serviceLocator.getService(CrazyThread.class);
crazyThread.run();
}
}

大功告成!!!

後記

在Jersey中使用HK2這個問題困惱我很久,很多沒有在resource層使用到的程式,就沒有辦法透過hk2注入.Using HK2 with Jersey (with auto-scanning!)這篇文章有提到怎麼解決這個問題,可能是早期版本的hk2必須手動去hack,嘗試他的做法不能順利執行.後來參考了jersey + grizzly + hk2: Dependency injection, but not into resource和khasunuma/hk2-sample才解決這個問題…

參考資料:

  1. Using HK2 with Jersey (with auto-scanning!)
  2. jersey + grizzly + hk2: Dependency injection, but not into resource
  3. khasunuma/hk2-sample
  4. HK2 Metadata Generator
1…34

pandaforme

This is a programming blog

33 posts
7 categories
17 tags
RSS
GitHub Linkedin
© 2018 pandaforme
Powered by Hexo v3.7.1
|
Theme — NexT.Gemini v6.2.0