Skip to content

Instantly share code, notes, and snippets.

@asufana
Last active December 14, 2015 03:00
Show Gist options
  • Save asufana/f3b05974c3e98794a34c to your computer and use it in GitHub Desktop.
Save asufana/f3b05974c3e98794a34c to your computer and use it in GitHub Desktop.
PlayFramework1 プラグイン開発

PlayFramework1 プラグイン開発

公開プラグインリスト

勉強によさげな簡単なプラグイン

プラグイン構成

src
├ play.plugins
└ play
     └ modules
          └ MODULE_NAME
               ├ FooPlugin.java
               └ FooEnhancer.java

プラグイン実装

play-accesslog https://github.com/briannesbitt/play-accesslog より

public class AccessLogPlugin extends PlayPlugin
{
   //vhost remoteAddress - requestUser [time] "requestUrl" status bytes "referrer" "userAgent" requestTime "POST"
   private static final String FORMAT = "%v %h - %u [%t] \"%r\" %s %b \"%ref\" \"%ua\" %rt \"%post\"";
   private static final String CONFIG_PREFIX = "accesslog.";
   private static final String DEFAULT_PATH = "logs/access.log";
   private static boolean _canLog = false;
   private static PrintWriter _writer;
   private static File _logFile;
   private boolean _shouldLog2Play;
   private boolean _shouldLogPost;

   @Override
   //初期化処理
   public void onConfigurationRead() {
      _shouldLog2Play = Boolean.parseBoolean(Play.configuration.getProperty(CONFIG_PREFIX+"log2play", "false"));
      _shouldLogPost = Boolean.parseBoolean(Play.configuration.getProperty(CONFIG_PREFIX+"logpost", "false"));
      _logFile = new File(Play.configuration.getProperty(CONFIG_PREFIX+"path", DEFAULT_PATH));

      if (!_logFile.isAbsolute()) {
         _logFile = new File(play.Play.applicationPath, _logFile.getPath());
      }
   }

   @Override
   //アプリケーション起動時
   public void onApplicationStart() {
      //ファイル書き込みハンドラ生成
      _canLog = createWriter();
   }

   @Override
   //アプリケーション終了時
   public void onApplicationStop() {
      if (_writer != null) {
         //ファイル書き込みハンドラクローズ
         _writer.close();
      }
   }

   @Override
   //リクエスト処理終了時
   public void invocationFinally() {
      //ログ書き込み
      log();
   }

   //ログ書き込み
   private synchronized void log() {
      //リクエスト・レスポンスオブジェクト取得
      Http.Request request = Http.Request.current();
      Http.Response response = Http.Response.current();
      if (request == null || response == null) {
         return;
      }

      long requestProcessingTime = System.currentTimeMillis() - request.date.getTime(); //処理時間
      Http.Header referrer = request.headers.get(HttpHeaders.Names.REFERER.toLowerCase()); //リファラ
      Http.Header userAgent = request.headers.get(HttpHeaders.Names.USER_AGENT.toLowerCase()); //userAgent

      String bytes = "-";
      String status = "-";

      /* It seems as though the Response.current() is only valid when the request is handled by a controller
         Serving static files, static 404's and 500's etc don't populate the same Response.current()
         This prevents us from getting the bytes sent and response status all of the time
       */
      if (request.action != null && response.out.size() > 0) {
         bytes = String.valueOf(response.out.size());
         status = response.status.toString();
      }

      //アクセスログ文字列作成
      String line = FORMAT;
      line = StringUtils.replaceOnce(line, "%v", request.host);
      line = StringUtils.replaceOnce(line, "%h", request.remoteAddress);
      line = StringUtils.replaceOnce(line, "%u", (StringUtils.isEmpty(request.user)) ? "-" : request.user);
      line = StringUtils.replaceOnce(line, "%t", request.date.toString());
      line = StringUtils.replaceOnce(line, "%r", request.url);
      line = StringUtils.replaceOnce(line, "%s", status );
      line = StringUtils.replaceOnce(line, "%b", bytes);
      line = StringUtils.replaceOnce(line, "%ref", (referrer != null) ? referrer.value() : "");
      line = StringUtils.replaceOnce(line, "%ua", (userAgent != null) ? userAgent.value() : "");
      line = StringUtils.replaceOnce(line, "%rt", String.valueOf(requestProcessingTime));

      if (_shouldLogPost && request.method.equals("POST")) {
         String body = request.params.get("body");

         if (StringUtils.isNotEmpty(body)) {
            line = StringUtils.replaceOnce(line, "%post", body);
         } else {
            // leave quotes in the logged string to show it was an empty POST request
            line = StringUtils.remove(line, "%post");
         }
      } else {
         line = StringUtils.remove(line, "\"%post\"");
      }

      line = StringUtils.trim(line);

      if (_canLog) {
         _writer.println(line);
      }

      if (_shouldLog2Play) {
         play.Logger.info(line);
      }
   }
}

クラス拡張実装

BetterLogs https://github.com/sgodbillon/BetterLogs より

public class BetterLogsEnhancer extends Enhancer {

    @Override
    public void enhanceThisClass(final ApplicationClass applicationClass) throws Exception {
        final CtClass ctClass = makeClass(applicationClass);

        //javassist: getDeclaredBehaviros() からコンストラクタとメソッド一覧を取得
        for(final CtBehavior behavior : ctClass.getDeclaredBehaviors()) {
            behavior.instrument(new ExprEditor() {
                @Override
                public void edit(MethodCall m) throws CannotCompileException {
                    try {
                        //play.Loggerメソッドであったら
                        if("play.Logger".equals(m.getClassName())) {
                            String name = m.getMethodName();
                            if("trace".equals(name) || "debug".equals(name) || "info".equals(name) || "warn".equals(name) || "error".equals(name) || "fatal".equals(name)) {
                                String code = String.format("{play.modules.betterlogs.BetterLogsPlugin.log(\"%s\", \"%s\", \"%s\", \"%s\", \"%s\", \"%s\", \"%s\", \"%s\", %s, %s);}",
                                        name,
                                        ctClass.getName(), // canonical name
                                        ctClass.getSimpleName(), // simple name
                                        ctClass.getPackageName(), // package
                                        behavior.getName(),
                                        behavior.getSignature(),
                                        m.getFileName(),
                                        applicationClass.javaFile.relativePath(),
                                        m.getLineNumber(),
                                        "$args" // original args
                                );
                                //メソッド書き換え
                                m.replace(code);
                            }
                        }
                    } catch (Exception e) {
                        throw new RuntimeException(e);
                    }
                }
            });
        }
        //クラス書き換え
        applicationClass.enhancedByteCode = ctClass.toBytecode();
        ctClass.defrost();
    }
}

イベント一覧

すみません。全部網羅してません。

サーバ起動時イベント

void onLoad()

  • Called at plugin loading

void onConfigurationRead()

  • Called at application start (and at each reloading) Time to start stateful things.
  • 拡張ログプラグインでの使用例:ログファイル生成、あるいはオープン

void onApplicationStart()

  • Called at application start (and at each reloading) Time to start stateful things.
  • 拡張ログプラグインでの使用例:ログファイルハンドラの生成

void afterApplicationStart()

  • Called after the application start.

void onApplicationStop()

  • Called at application stop (and before each reloading) Time to shutdown stateful things.
  • 拡張ログプラグインでの使用例:ログファイルハンドラのクローズ

void onRoutesLoaded()

  • Called after routes loading.
  • Facebook認証プラグインでの使用例:OAuthコールバックURLの追加

void afterFixtureLoad()

void onApplicationReady()

リクエスト時イベント

void beforeInvocation()

  • Called before a Play! invocation. Time to prepare request specific things.

void afterInvocation()

  • Called after an invocation. (unless an excetion has been thrown). Time to close request specific things.

void onInvocationSuccess()

void onInvocationException(final Throwable e)

  • Called if an exception occured during the invocation.

void invocationFinally()

  • Called at the end of the invocation. (even if an exception occured). Time to close request specific things.
  • 拡張ログプラグインでの使用例:ログ書き込み

void beforeActionInvocation(final Method actionMethod)

  • Called before an 'action' invocation, ie an HTTP request processing.

void onActionInvocationResult(final Result result)

  • Called when the action method has thrown a result.

void onRequestRouting(final Route route)

  • Called when the request has been routed.

void afterActionInvocation()

  • Called at the end of the action invocation.

void onEvent(final String message, final Object context)

  • Event may be sent by plugins or other components

その他

void enhance(ApplicationClass applicationClass)

  • Enhance this class
  • クラス拡張(バイトコード修正)する場合は、ここから

boolean serveStatic(final VirtualFile file, final Request request, final Response response)

  • Let a chance to this plugin to manage a static resource
  • Coffeeスクリプト対応プラグインでの使用例:拡張子coffeeファイルであったらコンパイルしてhttpレスポンスを返す

Template loadTemplate(final VirtualFile file)

  • テンプレートファイル呼び出しイベント

void detectChange()

  • It's time for the plugin to detect changes. Throw an exception is the application must be reloaded.
  • route拡張プラグインでの使用例:onRouteLoaded()イベントで拡張したroute情報を、detectChange()時に再度拡張する

イベント呼び出し順(PROD時)

Play初期化

  • void onLoad() まだポートも開かれていない

プリコンパイル

  • void enhance(ApplicationClass applicationClass)

  • Template loadTemplate(final VirtualFile file) enhanceの次

  • List addTemplateExtensions() loadTemplateとセットで呼ばれる?

Play起動

  • void onConfigurationRead() 設定ファイル読み込み

  • void onRoutesLoaded()

  • void onApplicationStart()

  • void afterApplicationStart()

起動時ジョブ起動

  • void beforeInvocation()

  • void afterInvocation()

  • void invocationFinally()

アプリ起動

  • void onApplicationReady() この後、ポートが開かれる

リクエスト発生

  • boolean rawInvocation(final Request request, final Response response)

  • void routeRequest(final Request request)

  • void onRequestRouting(Route route)

  • void beforeInvocation()

  • void beforeActionInvocation(Method actionMethod)

  • void onActionInvocationResult(Result result)

  • Map<String, String> addMimeTypes()

  • Template loadTemplate(final VirtualFile file)

  • List addTemplateExtensions()

  • void afterActionInvocation()

  • void afterInvocation()

  • void onInvocationSuccess()

  • void invocationFinally()

イベント呼び出し順(DEV時)

Play初期化

  • void onLoad() まだポートも開かれていない

アプリ起動

  • void onApplicationReady() この後、ポートが開かれる

最初のリクエスト

  • boolean rawInvocation(final Request request, final Response response)

  • void onRoutesLoaded()

  • void beforeDetectingChanges()

  • boolean detectClassesChange()

  • void onConfigurationRead() 設定ファイル読み込み

  • void onApplicationStart()

  • void afterApplicationStart()

  • void enhance(ApplicationClass applicationClass)

  • void beforeDetectingChanges()

  • boolean detectClassesChange()

  • void detectChange()

  • void beforeInvocation()

  • void afterInvocation()

  • void invocationFinally()

  • void routeRequest(final Request request)

  • void onRequestRouting(Route route)

  • void beforeInvocation()

  • void beforeActionInvocation(Method actionMethod)

  • void onActionInvocationResult(Result result)

  • Map<String, String> addMimeTypes()

  • Template loadTemplate(final VirtualFile file)

  • List addTemplateExtensions()

  • void afterActionInvocation()

  • void afterInvocation()

  • void onInvocationSuccess()

  • void invocationFinally()

リクエスト発生

  • boolean rawInvocation(final Request request, final Response response)

  • void beforeDetectingChanges()

  • boolean detectClassesChange()

  • void detectChange()

  • void routeRequest(final Request request)

  • void onRequestRouting(Route route)

  • void beforeInvocation()

  • void beforeActionInvocation(Method actionMethod)

  • void onActionInvocationResult(Result result)

  • Template loadTemplate(final VirtualFile file)

  • void afterActionInvocation()

  • void afterInvocation()

  • void onInvocationSuccess()

  • void invocationFinally()

使い方の調べ方

  • Google検索: "extends PlayPlugin" detectChange site:github.com

プラグイン開発

モジュール雛形作成

モジュール名はパッケージに引用されるので、すべて小文字で登録する。

# play new-module sampleplugin
# tree
.
├── app
│   ├── controllers
│   │   └── sampleplugin
│   ├── models
│   │   └── sampleplugin
│   └── views
│       ├── sampleplugin
│       └── tags
│           └── sampleplugin
├── build.xml
├── commands.py
├── conf
│   ├── dependencies.yml
│   ├── messages
│   └── routes
├── documentation
│   └── manual
│       └── home.textile
└── src
    ├── play
    │   └── modules
    │       └── sampleplugin
    └── play.plugins

初期処理

.gitignore追加

# cd sampleplugin
# touch .gitignore
# vim .gitignore
/.classpath
/.project
/.settings
/eclipse/
/logs/
/test-result/
/tmp/

フォルダ作成

ビルド時のjarファイルを配置するため

# mkdir lib

不要ファイル/フォルダ削除

# rm -rf app/* documentation/ conf/messages
# echo /app/ >> .gitignore

プラグインバージョン設定

# vim conf/dependencies.yml
self: play -> sampleplugin 1.0
require:
    - play

Eclipseにプロジェクトインポート

play ec してから、EclipseのImport機能でインポート

プラグイン開発

プラグインエントリポイントのクラスを配置

srcフォルダのパッケージ play.modules.sampleplugin に追加(appフォルダではないので注意)

public class SamplePlugin extends PlayPlugin {
    @Override
    public void onLoad() {
        System.out.println("Hello, SamplePlugin!");
    }
}

プラグイン呼び出し設定

上で作成したプラグインクラス名を指定する。頭の数字はプラグインの呼び出し優先順位値 (play statusコマンドで実行中プロジェクトのプラグイン優先順が確認できる)

# echo 100:play.modules.sampleplugin.SamplePlugin >> src/play.plugins

テストソースフォルダ追加

Eclipse上でプロジェクト名(sampleplugin)を選択し、NEW > Other > Java > Source Folder からフォルダ名 test を追加。

テストクラス

application.conf ファイルがないと UnitTest が動作しないため、追加しておく

# touch conf/application.conf

testフォルダのパッケージ play.modules.sampleplugin に追加

public class SamplePluginTest extends UnitTest {
    @Test
    public void test() {
        assertThat(new SamplePlugin(), is(not(nullValue())));
    }
}

play auto-test 利用条件

  • conf/routes と conf/application.conf が存在すること
  • URLのルートが1つ以上あること
  • GET /public/ staticDir:public などをconf/routesに追加しておけばOK

プラグインのビルド

プラグインにソースを含むよう、build.xmlを修正しておく

<jar destfile="lib/play-ddd.jar" basedir="tmp/classes">
    <!-- ソースを含める -->
    <fileset dir="src" includes="**/*.java" />
    <manifest>
        <section name="Play-module">
            <attribute name="Specification-Title" value="ddd"/>
        </section>
    </manifest>
</jar>

モジュール作成

# play build-module --require 1.2.5

Error: JAVA_HOME is not defined correctly. と言われたら、 JAVA_HOME 環境変数を設定する(下記はOSXの場合)

# export JAVA_HOME=`/usr/libexec/java_home -v 1.8`

BUILD FAILED ... sampleplugin/lib does not exist. と言われたら、libフォルダを作成する

# mkdir lib

プラグインの利用

新しいアプリケーションを作成して、作成したプラグインを利用してみる。

アプリケーション作成

# play new SamplePluginTest

依存性設定

# cd SamplePluginTest
# vim conf/dependencies.yml
require:
    - play
    - sampleplugin -> sampleplugin

repositories:
    - local_modules:
        type:       local
        artifact:   ${application.path}/../[module]
        contains:
            - sampleplugin

# play deps --clearcache

プラグイン動作確認

# play run
~        _            _ 
~  _ __ | | __ _ _  _| |
~ | '_ \| |/ _' | || |_|
~ |  __/|_|\____|\__ (_)
~ |_|            |__/   
~
~ play! 1.2.5, http://www.playframework.org
~
~ Ctrl+C to stop
~ 
JPDA port 8000 is already used. Will try to use any free port for debugging
CompilerOracle: exclude jregex/Pretokenizer.next
Listening for transport dt_socket at address: 55646
14:54:07,179 INFO  ~ Starting /SamplePluginTest
14:54:07,182 INFO  ~ Module sampleplugin is available (/SamplePluginTest/../sampleplugin)
14:54:07,530 WARN  ~ You're running Play! in DEV mode
14:54:07,580 INFO  ~ Listening for HTTP on port 9000 (Waiting a first request to start) ...
Hello, SamplePlugin!
14:54:18,071 INFO  ~ Application 'SamplePluginTest' is now started !

Hello, SamplePlugin! が表示されたらプラグイン読み込みOK

Githubへのプラグイン登録

Maven Central Repository に登録しなくとも、Github上にビルドしたZipファイルを配置することでプラグイン公開が可能

Githubへの登録

Github上で PlaySamplePlugin リポジトリを作成。ソースとビルド済みファイルをプッシュする

# cd sampleplugin
# git init
# git add .
# git status
  new file:   .gitignore
  new file:   build.xml
  new file:   commands.py
  new file:   commands.pyc
  new file:   conf/application.conf
  new file:   conf/dependencies.yml
  new file:   conf/routes
  new file:   dist/sampleplugin-1.0.zip
  new file:   lib/play-sampleplugin.jar
  new file:   src/play.plugins
  new file:   src/play/modules/sampleplugin/SamplePlugin.java
  new file:   test/play/modules/sampleplugin/SamplePluginTest.java

# git commit -m "first commit"
# git remote add origin https://github.com/{YOUR_NAME}/PlaySamplePlugin.git
# git push -u origin master

プラグインの利用

dependencies.yml を修正する。

# cd SamplePluginTest
# vim conf/dependencies.yml
require:
    - play
    - asufana -> sampleplugin 1.0 

repositories:
    - asufana_github_repo:
        type:       http
        artifact:   https://github.com/{YOUR_NAME}/PlaySamplePlugin/raw/master/dist/[module]-[revision].zip
        contains:
            - asufana -> *          

# play deps --clearcache

WARNING: These dependencies are missing, your application may not work properly (use --verbose for details), と言われた場合には、

# play deps --clearcache --verbose 

として、エラー詳細を確認する。

Server access Error: java.lang.RuntimeException: Unexpected error: java.security.InvalidAlgorithmParameterException: the trustAnchors parameter must be non-empty url=https://github.com... となっていたら、たぶん使っているJDKが古いため、SSL通信エラーになっていると思われる。JDKを更新すること。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment