Proxyの生成手法を素振りしてみる。
最初に
さて、タイトルどおりなのですが
素振りするだけの記事だったりします。
素振りしたライブラリはこちら
Proxyと言えばjava.lang.reflect.Proxyですね。
今回は次の2つのクラスについて、Proxyを作ってみます。
public static class Some implements SomeInterface { @Override public void apply() { System.out.println("hello world"); } } interface SomeInterface { public void apply(); }
まず前提として、今回書くサンプルコードは全て次の出力文が出力されます。
apply:start hello world apply:end
標準API
まずはJDKについてるProxyクラスを使ってProxyを作ってみましょう。
@Test public void testCreateDynamicProxy() { SomeInterface impl = new Some(); SomeInterface proxy = (SomeInterface) Proxy.newProxyInstance( this.getClass().getClassLoader(), new Class[] {SomeInterface.class}, (Object _proxy, Method method, Object[] args) -> { System.out.println(method.getName() + ":start"); method.invoke(impl, args); System.out.println(method.getName() + ":end"); return null; }); proxy.apply(); }
implを生成しています。今回は引数も戻り値も特に使われないので記述は基本的に簡単になっていますね。
ところで、余談ですがラムダに型つけられるんですね。
最近全然使ってなかったので知りませんでした。
ではここからはライブラリを使っていきます。
Javassist編
Javassistを使ってみましょう。
@Test public void testCreateProxyWithJavassist() throws NoSuchMethodException, IllegalArgumentException, InstantiationException, IllegalAccessException, InvocationTargetException { ProxyFactory factory = new ProxyFactory(); factory.setSuperclass(Some.class); Some some = (Some) factory.createClass() .newInstance(); ProxyObject proxyObject = (ProxyObject) some; proxyObject.setHandler((Object self, Method thisMethod, Method proceed, Object[] args) -> { System.out.println(thisMethod.getName() + ":start"); proceed.invoke(self, args); System.out.println(thisMethod.getName() + ":end"); return null; }); some.apply(); }
Proxy作るためのクラスが用意されていますね。
ByteCode操作をするライブラリに入ってるクラスなので、setSuperClassといった形で、クラスを渡す形になってますね。
Bytecode操作してサブクラスを動的に生成して、メソッドにhandlerの処理とか追加するようですね。
ちなみにProxyFactoryクラスにはwriteDirecotryフィールドがあって
そこにパスを渡してやるとcreateClassした際に.classファイルが生成されます。
生成されたクラスをデコンパイルしてみると面白いかもしれません。
cglib
今度はcglibで書いてみます。
こちらもProxyを作るためのクラスが用意されています。
@Test public void testCreateProxyWithCglib() { Enhancer enhancer = new Enhancer(); enhancer.setSuperclass(Some.class); MethodInterceptor interceptor = (Object obj, Method method, Object[] args, MethodProxy proxy) -> { System.out.println(method.getName() + ":start"); proxy.invokeSuper(obj, args); System.out.println(method.getName() + ":end"); return null; }; enhancer.setCallback(interceptor); Some some = (Some) enhancer.create(); some.apply(); }
こちらもByteCode操作をするライブラリに入ってるクラスなのでなんだか似てますね。
特に難しいことはないかと思われるのですが
Callbackというマーカインターフェースが存在するのですが
その子クラスに、InvocationHandlerとMethodInterceptorというのがあります。
ぱっと見た感じだと引数がすごい似てるので、どっち使えばいいか分かりませんでしたが
superのメソッドを呼びたい、ただただInterceptしたいだけならMethodInterceptorで良さそうですね。
ちなみにCallbackインターフェースのJavadocを見ると分かるのですが
以下のインターフェースがCallbackインターフェースを継承してます。
色々できそうですね。
- MethodInterceptor
- NoOp
- LazyLoader
- Dispatcher
- InvocationHandler
- FixedValue
Byte Buddy
最後はByte Buddyを使ってみます。
少しこいつは毛並みが違います。
まずコードを晒してみます。
@Test public void testCreateProxyWithByteBuddy() throws InstantiationException, IllegalAccessException { Some some = (Some) new ByteBuddy().subclass(Some.class) .method(ElementMatchers.any()) .intercept(MethodDelegation.to(TestInterceptor.class)) .make() .load(this.getClass() .getClassLoader()) .getLoaded() .newInstance(); some.apply(); } public static class TestInterceptor { @RuntimeType public static Object intercept(@SuperCall Callable<?> zuper, @Origin Method method) throws Exception { System.out.println(method.getName() + ":start"); zuper.call(); System.out.println(method.getName() + ":end"); return null; } }
chain methodによる流れるようなインターフェースが特徴的です。
また、いくつかのstaticメソッドを提供するクラスがあるみたいですね。
また、@RuntimeType、や@SuperCall、@Originなどのアノテーションも出現しています。
リフレクションとバイトコード操作を組み合わせた感じのゴリゴリの黒魔術感が出ています。
個人的には今回始めて触ったので、はっきり言ってキモイのですが
すごく発想としては面白いのではないでしょうか。
まとめ
今まで、Javaの標準機能ではインターフェースを定義しないと
Proxy的な形でMethodのinterceptionが行えませんが
ライブラリを使って、ByteCodeを操作することによって、サブクラスを動的に生成し
インターフェースの定義をしなくても、Methodのinterceptionが行えるようになりました。
ちなみに、Spring 4.3.4ではcglibを使ってclass-based proxyを作っているようですね。
ScopedProxyMode (Spring Framework 4.3.5.RELEASE API)
旅行の準備しなきゃ。
とりあえず今日はここまで~
リポジトリはここに置いてます。
github.com
おまけ
Bytecode操作についてはひしだまさんがJavassistについて書いてくださってます。
Javassistメモ(Hishidama's Javassist Memo)
jyukutyoさんがJJUG 2016 Fallで話していたスライドも見てみると面白いです。
JJUG CCC 2016 fall バイトコードが君のトモダチになりたがっている
Bytecode操作を使った、DIコンテナについてのきしださんの記事を読むと非常に面白い上に為になります。
作って理解するDIコンテナ - きしだのはてな
Pluggable Annotation Processor APIでimport文を取る
APT、Pluggable Annotation Processorは皆さんお使いでしょうか。
タイトルの通りですが、APTでimport文を取ってみます。
gradleはとりあえずこんな感じです。
今回はAPTのProcessorを作るプロジェクトですがAPTを使います(グルグル目)
準備
apply plugin: 'java' apply plugin: "net.ltgt.apt" buildscript { repositories { maven { url "https://plugins.gradle.org/m2/" } } dependencies { classpath "net.ltgt.gradle:gradle-apt-plugin:0.9" } } sourceCompatibility = 1.8 repositories { jcenter() } dependencies { compileOnly group: 'com.google.auto.service', name: 'auto-service', version: '1.0-rc2' compileOnly files("${System.properties['java.home']}/../lib/tools.jar") testCompile 'junit:junit:4.12' }
少し解説:AutoServiceについて
AutoServiceはGoogleのライブラリのautoの中の一つです。
ServiceLoaderを楽に扱うためのライブラリです。
今回作るAnnotation ProcessorのコードはServiceLoader経由でロードされて使用されます。
META-INF/servicesの配下にServiceのインターフェース、もしくは抽象クラスのFQCNと同じ名前のファイルに
ロードしたいプロバイダクラス(ざっくりいうと実装クラス)を改行区切りで記述します。
Javadoc見る感じだと#でコメントも書けるそうです。
本来は自分でファイルを記述しないといけないところをAutoServiceを使って楽をします。
@AutoServiceをつけることでMETA-INF/services/(インターフェース名・抽象クラス名)に
自動的にアノテーションを付けたクラスを書いてくれる形になります。
では今回実装したソースはこちら。
import java.util.Set; import javax.annotation.processing.AbstractProcessor; import javax.annotation.processing.Processor; import javax.annotation.processing.RoundEnvironment; import javax.annotation.processing.SupportedAnnotationTypes; import javax.annotation.processing.SupportedSourceVersion; import javax.lang.model.SourceVersion; import javax.lang.model.element.TypeElement; import com.google.auto.service.AutoService; import com.sun.source.util.TreePath; import com.sun.source.util.Trees; @AutoService(Processor.class) @SupportedSourceVersion(SourceVersion.RELEASE_8) @SupportedAnnotationTypes("*") public class ImportShowProcessor extends AbstractProcessor { @Override public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) { roundEnv.getRootElements() .stream() .map(Trees.instance(this.processingEnv)::getPath) .map(TreePath::getCompilationUnit) .flatMap((unit) -> unit.getImports() .stream()) .forEach(System.out::println); return true; } }
そんなに長くはありませんね。
クラスについてるAnnotationの説明から
まず@AutoServiceですが、これは先ほど説明したAutoServiceですね。
説明は省きます。
@SupportedSourceVersion(SourceVersion.RELEASE_8)は
このAnnotation ProcessorはJava8からサポートしてますよ、というメタ要素ですね。
このアノテーションを付けてない、かつ親クラスの実装を変更してなければ、6からサポートしてます、という形になります。
@SupportedAnnotationTypes("*")
これは全てのコンパイル要素を対象とするようです。
上記二つのAnnotationはそのアノテーション名と似たような名前のメソッドがAbstractProcessorに定義されています。
もう一つSupportedOptionsというアノテーションもあって、こちらはコンパイル時のオプションから値を注入できるような仕組みだそう。
今回は使いません。
processメソッドについて
こちらが今回実装した部分ですね、と言っても
そんな大したことはしてなくて
まぁimport文取り出して、表示してるだけですね。
じゃあこちらのAnnotationProcessorを使ってコンパイルしてみます。
gradleのjarタスクで生成されたjarを使います。
javac -cp lombok-processor-example.jar Test.java
コンパイルするソースはこちらです。
import java.util.List; import java.awt.*; class Test{ List<String> yyy; String test; }
出力は・・・
import java.util.List; import java.awt.*;
import文が出ましたね。はい。
というわけで
jdeps使いましょう。
今回書いたソースはこちらに置いてあります。
github.com
Vue.js 2.1 with TypeScript 2.1 on Payara Micro
この記事はPayara Advent Calendar 2016の21日目の記事です。
この記事はPayara MicroといいつつPayara Microの話はあんまりありません。
Payara Microの細かい話は蓮沼さんが書いていらっしゃるので、そちらをご覧頂ければと思います。
この記事には何ら説明なく
単語が出てくる可能性がございますが
分からなければ、ご質問ください。
構成
サーバーサイド
- Payara Micro 4.1.1.164
- lombok
- Jackson (<-- MOXyの代わり)
- JPA
- h2 (とりあえずon memory)
フロントエンド
- TypeScript 2.1
- Vue.js 2.1.6 with av-ts
VueでTypeScriptを利用するのであれば以下の記事が参考になると思います。
Vue.jsとTypeScript - Qiita
機能
実装した機能をサーバーサイドとフロントエンドで
軽く列挙してみます。
フロントエンド
- タスク追加
- タスクツリー表示
- 認証フォーム(めっちゃださい)っぽい何か
認証
今回は認証付きのSPAっぽい何か、ということで
認証機能を作ります。
うらがみさんの資料を参考に書きます。
JAX-RS入門および実践
ContainerRequestFilterと独自アノテーションで認証の有無を設定します。
以下のような感じで設定します。
(一部抜粋)
@POST @Path("login") @Produces(MediaType.APPLICATION_JSON) public User login(User user) { return manager.fetch() .orElseGet(() -> { User result = userManager.authenticate(user) .orElseThrow(() -> new InvalidAccessException("cannot login; please check your password or user name")); manager.setUser(result); return new User().setName(result.getName()); }); } @GET @Path("task") @Authenticated @Produces(MediaType.APPLICATION_JSON) public List<Task> tasklist(){ return taskRepository.findAll(); }
フィルタ処理
ここはざっくり、認証されているかどうかの判断はSessionScopeなオブジェクトが
Userオブジェクトを持っているかどうかで判断します。
もう少しRoleManagerにコード書いてたほうが良かったかもしれません。
isLoggedIn的なメソッドですね。
@SessionScoped public class RoleManager implements Serializable { private static final long serialVersionUID = 1L; @Setter @Accessors(chain = true) private User user; public Optional<User> fetch() { return Optional.ofNullable(user); } /* 本当はあったら良かった public boolena isLoggedIn(){ return user!=null } */ } @Provider @Authenticated @ApplicationScoped public class AuthenticationProvider implements ContainerRequestFilter { @Inject RoleManager manager; @Override public void filter(ContainerRequestContext context) throws IOException { if (!manager.fetch() .isPresent()) context.abortWith(Response.status(Response.Status.UNAUTHORIZED) .entity("not permitted") .build()); } }
こんな感じですね。権限ないよって怒って固定文字列を返却するだけですね。
認証処理
では実際の認証処理ですが、こちらはBCryptを使ってDBと突合せします。
ざっくり引っ張ってきます。簡単でいいですね。
// 認証処理(findっておかしいですね。) public Optional<User> find(User user) { return findByName(user.getName()).flatMap((findUser) -> { if (BCrypt.checkpw(user.getPassword(), findUser.getPassword())) { return Optional.of(findUser); } else { return Optional.empty(); } }); } // 一応、ユーザ登録処理のほうも一緒に。 @Transactional public User register(User user) { if (!findByName(user.getName()).isPresent()) { User newUser = new User().setName(user.getName()) .setPassword(BCrypt.hashpw(user.getPassword(), BCrypt.gensalt())); em.persist(newUser); return user; } throw new InvalidAccessException("permission denied"); }
ユーザとその初期登録
ユーザの定義はこちらです。
passwordに入るのはBCryptによってencryptされたデータです。
@Data @Accessors(chain = true) @Entity @NamedQueries({ @NamedQuery(name="User.findByName",query="Select user from User user where user.name=:name") }) public class User implements Serializable { private static final long serialVersionUID = 1L; @Id @GeneratedValue private Long id; @Column(unique = true) String name; String password; }
初期登録の処理はこちらです。
今回は簡単に実装しているので、
とりあえずApplicationScopedにしておいて
@PostConstructでアプリケーション立ち上げ時に初期化することにします。
ユーザを起動時に登録しておきます。
また、今回はメモリDBなので気にせず毎回登録します。
@ApplicationScoped public class UserManager { @Inject UserRepository repository; // ...省略 @PostConstruct public void initialize() { User admin = new User().setName("admin") .setPassword("admin"); repository.register(admin); User user = new User().setName("user") .setPassword("user"); repository.register(user); } }
大体こんなところです。認証処理が完成しました。
DB回りの設定はリンクだけ張っておきます。
payara-micro-example/persistence.xml at master · Wreulicke/payara-micro-example · GitHub
payara-micro-example/glassfish-resources.xml at master · Wreulicke/payara-micro-example · GitHub
ではフロントエンドから認証処理を叩きましょう。
export interface LoginInfo { name: string, password: string, } export async function login(o: LoginInfo) { // URLどうにかしたほうがいいけどとりあえず放置 const response = await fetch("http://localhost:8080/api/example/login", { method: "POST", headers: { "content-type": "application/json", }, credentials: "include", body: JSON.stringify(o), }) return response.json() }
fetch APIを使っています。
また、少しですがES8 async/awaitを使っています。
解説については他の記事に任せます。(質問があれば答えられる範囲であれば答えます。)
認証本体についてはこんなところです。
画面はこんな感じになりました。
gyazo.com
今回は記事ボリューム的に認証周りは大したことをやっていないので
このぐらいにしておきます。
タスクツリー
残りの細かい機能をまとめて、今回はタスクツリー表示機能を
関連するAPIと絡めて説明します。
こちらは公式のサンプルをrewriteしたものになります。(少し見た目と動きを変えた)
再帰するコンポーネントですね。
機能一覧
タスクツリーの細かい機能としてはざっくり分類すると以下の3つです。
- タスクの追加ができる
- タスクの子供を追加できる。
- タスクの子供の表示・非表示が切り替わる。
タスクツリー実装
今回はVueをTypeScriptでうまく扱うために、av-tsを使って実装しています。
github.com
av-tsを使ったVueコンポーネントの実装について少し説明してみます。
以下の画像の部分の定義になります。
import { Component, Lifecycle, Prop, Vue } from "av-ts" import addTask, { Task } from "./module/addTask" import TaskInput from "./TaskInput" interface TaskNode { id: number name: string children?: TaskNode[] } @Component({ name: "tree", components: { "task-input": TaskInput }, // webpackでhtmlを読み込んでvueの関数にloaderで変換 ...require("./tree.html"), }) class Tree extends Vue { open = false name = "" isFolder = false @Prop model: TaskNode @Lifecycle mounted() { this.isFolder = this.model.children != null && this.model.children.length > 0 } // クリックされたら閉じたり開いたりするアコーディオン用の処理 toggleFolder() { this.open = !this.open } changeFolder() { if (!this.isFolder) { Vue.set(this.model, "children", []) this.isFolder = true this.open = true } } addTask(task: Task) { task.parent = this.model.id addTask(task).then((taskRes) => { this.model.children!.push({ id: taskRes.id, name: taskRes.name }) }) } } export default Tree // tree.html <li> <div v-on:click="toggleFolder" v-on:dblclick="changeFolder" v-bind:class="{item:!isFolder,folder:isFolder,tree:true}"> {{model.name}} </div> <transition name="fade"> <ul v-show="open" v-if="isFolder" class="none-style"> <tree :model="m" v-for="m in model.children"> </tree> <task-input :add="addTask" /> </ul> </transition> </li>
htmlはvue-template-compiler-loaderでloaderしているので
Vueのrender(と他にもなんかあるみたいですが)に変換されます。
av-tsのドキュメントを見た感じ
プロパティはdata()に
@Propでデコレートされているプロパティはpropsに
メソッドはmethodsに
getter/setterはcomputedに、といった形で
Vueの定義と等価になるようです。
@LifecycleでVueのライフサイクルのメソッドとして扱われます。
Vueの機能である、transitionを使ってfadein,fadeoutを実現しています。便利ですね。
https://jp.vuejs.org/v2/api/#transition
画面は以下のような感じになりました。
まとめ
Payara-Microを使ってJAX-RSを実装した上に
Vue.js with TypeScriptで再帰的なコンポーネントを使って
タスクのツリーを定義するような画面を作りました。
再帰コンポーネントで苦労しました。
実際ハマったのはprops/propsDataの使い方だったりするのですが
最終的には使わなくなりました。
また、Vueでwarningは出るけどReactみたいにドキュメントのURLが出ないので
その辺出ると嬉しいなーと感じました。
av-tsを使った感じ、Vueのドキュメントとav-tsのドキュメントを行き来する感じで
少し辛い感じになりました。
この辺は他のTypeScriptで取り扱うためのライブラリを使っても
変わらないんじゃないかと思います。
普通にJSで.vueファイルを書いたほうが良いような気がしています。
リポジトリはこちらに置いてます。
github.com
完全にVue.jsの解説があまりできなかった感がありますが
その辺はこの辺で・・・
qiita.com
と思いましたがav-tsやvue-template-compiler-loaderについての説明は
あるのかな・・・?ないのかな・・・?
分からないところがあれば記事として書きますんで
番外編
素振りぐらいの気持ちでSelenideを使ってテストを書きました。
Selenideについてはうらがみさんの資料が詳しいです。
jQueryライクで分かりやすいですね。(jQueryが分からない人はごめんなさい・・・)
@Test public void addTask() { login(); val add = Selenide.$(By.cssSelector(".add:last-child")); add.click(); add.$("input") .val("xxxx") .pressEnter(); Selenide.$$(".item.tree").last() .should(Text.text("xxxx")); }