http://www.oracle.com/technetwork/articles/java/jaxrs20-1929352.html
この記事はJava EE 7の新機能を紹介するものです。詳細はJava.netのJava EE Platform Specificationから情報を入手して下さい。
Java EE Platform Specificationサンプルコード (Zip)
http://java.net/projects/javaee-spec/pages/Home
ほとんどのJava EE 6アプリケーションでリモートAPIと自由選択を要件とする場合、多かれ少なかれJAX-RS 1.0仕様のRESTfulな趣を使っています。Java EE 7とJAX-RS 2.0により、種々の有用な機能がもたらされ、さらに開発が簡単になり、もっと洗練された、しかも効率的なJava SE/EE RESTfulアプリケーションを作成することができます。
Roast House
Roast HouseとはJavaフレンドリーではあるもののシンプルなJAX-RS 2.0のサンプルで、コーヒー豆を管理し、煎るサンプルです。 このroast house自体はCoffeeBeansResource
として表現されています。"coffeebeans"
というURIは、CoffeeBeansResource
を一意に識別します(コード1を参照)。コード1以前のJAX-RS仕様では、リソースは
//... import javax.annotation.PostConstruct; import javax.enterprise.context.ApplicationScoped; import javax.ws.rs.DELETE; import javax.ws.rs.GET; import javax.ws.rs.POST; import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.container.ResourceContext; import javax.ws.rs.core.Context; import javax.ws.rs.core.Response; @ApplicationScoped @Path("coffeebeans") public class CoffeeBeansResource { @Context ResourceContext rc; Map<String, Bean> bc; @PostConstruct public void init() { this.bc = new ConcurrentHashMap<>(); } @GET public Collection<Bean> allBeans() { return bc.values(); } @GET @Path("{id}") public Bean bean(@PathParam("id") String id) { return bc.get(id); } @POST public Response add(Bean bean) { if (bean != null) { bc.put(bean.getName(), bean); } final URI id = URI.create(bean.getName()); return Response.created(id).build(); } @DELETE @Path("{id}") public void remove(@PathParam("id") String id) { bc.remove(id); } @Path("/roaster/{id}") public RoasterResource roaster(){ return this.rc.initResource(new RoasterResource()); } }
@Singleton
もしくは @Stateless
EJBとすることができます。さらに、すべてのルートリソースやプロバイダー、Application
のサブクラスをmanaged beanもしくはCDI-managed beanとしてデプロイすることができます。@Provider
アノテーションをつけたすべての拡張でInjectionも利用できます。これを使うと既存のコードとの統合が簡単になります。JAX-RS固有のコンポーネントをResourceContext
を使ってサブリソースに注入することも可能です。コード2興味深いことに、
@Context ResourceContext rc; @Path("/roaster/{id}") public RoasterResource roaster(){ return this.rc.initResource(new RoasterResource()); }
javax.ws.rs.container.ResourceContext
を使うと、JAX-RSの情報を既存インスタンスに注入できるだけでなく、ResourceContext#getResource(Class<T> resourceClass)
メソッドをもつリソースクラスにアクセスすることもできます。JAX-RSランタイムが現在のコンテキストからResourceContext#initResource
メソッドに渡されるインスタンスの注入ポイントを値で設定します。RoasterResource
クラス(コード3参照)のid
というString
型のフィールドは、親リソースのパスパラメータの値を受け取ります。コード3
public class RoasterResource { @PathParam("id") private String id; @POST public void roast(@Suspended AsyncResponse ar, Bean bean) { try { Thread.sleep(2000); } catch (InterruptedException ex) { } bean.setType(RoastType.DARK); bean.setName(id); bean.setBlend(bean.getBlend() + ": The dark side of the bean"); Response response = Response.ok(bean).header("x-roast-id", id).build(); ar.resume(response); } }
javax.ws.rs.container.AsyncResponse
というパラメータは、Servlet 3.0のjavax.servlet.AsyncContext
クラスと類似しており、非同期リクエストの実行が可能です。上の例では、リクエストを処理時間の間遅延させ、AsyncResponse#resume
メソッドの呼び出しによりクライアントへレスポンスを返しています。roast
メソッド自体は同期実行されるため、非同期実行による挙動は全く現れませんが、EJBの@javax.ejb.Asynchronous
アノテーションと@Suspended AsyncResponse
アノテーションの組み合わせを使うと、関心のあるクライアントの最終的な通知をする、業務ロジックを非同期で実行することができます。任意のJAX-RSルートリソースを@Stateless
or @Singleton
アノテーションで修飾することができ、事実上、EJBとして機能することができます(コード4を参照)。コード4
import javax.ejb.Asynchronous; import javax.ejb.Singleton; @Stateless @Path("roaster") public class RoasterResource { @POST @Asynchronous public void roast(@Suspended AsyncResponse ar, Bean bean) { //heavy lifting Response response = Response.ok(bean).build(); ar.resume(response); } }
@Suspended AsyncResponse
パラメータをもつ@Asynchronous
リソースのメソッドをfire-and-forgetの一方向非同期型で実行します。リクエスト処理スレッドは即座に解放されますが、AsyncResponse
によりは以前としてクライアントに対し便利なハンドルを提供します。時間のかかる作業が終わってから、結果をクライアントにタイミングよく返すことができます。通常の場合、JAX-RS固有の挙動を実際の業務ロジックから切り離したいと思うことでしょう。すべての業務ロジックは簡単に専用の境界のEJBに展開できますが、CDIのイベントを使うとこのfire-and-forgetの場合にずっとよく適合することができます。カスタムイベントクラスであるRoastRequest
は処理対象の入力として(Bean
クラスの)ペイロードをとり、結果の送信のためにAsyncResponse
とります(コード5参照)。コード5CDIイベントは業務ロジックとJAX-RSのAPIを切り離すだけでなく、JAX-RSのコードを非常にシンプルにします(コード6参照)。
public class RoastRequest { private Bean bean; private AsyncResponse ar; public RoastRequest(Bean bean, AsyncResponse ar) { this.bean = bean; this.ar = ar; } public Bean getBean() { return bean; } public void sendMessage(String result) { Response response = Response.ok(result).build(); ar.resume(response); } public void errorHappened(Exception ex) { ar.resume(ex); } }
コード6すべてのCDI managed beanやEJBはpublish-subscribe型の
public class RoasterResource { @Inject Event<RoastRequest> roastListeners; @POST public void roast(@Suspended AsyncResponse ar, Bean bean) { roastListeners.fire(new RoastRequest(bean, ar)); } }
RoastRequest
を受け取ることができ、同期・非同期とわず、簡単なvoid onRoastRequest(@Observes RoastRequest request){}
というobserverメソッドを使ってペイロードを処理することができます。AsyncResponse
クラスを使って、JAX-RS仕様はHTTPプロトコルでリアルタイムで情報をプッシュする簡単な方法を導入しています。クライアントの観点からすると、サーバーでの非同期リクエストは未だにブロックされているため同期型です。REST設計の立場からすれば、すべての長時間実行しているタスクは処理が完了した後に結果を入手する方法に関する追加情報とともに、HTTPステータスコード202で即座に返すべきでしょう。The Return of Aspects
人気のあるREST APIは多くの場合、クライアントがメッセージのフィンガープリントを計算し、リクエストと一緒に送信する必要があります。サーバー側では、フィンガープリントを計算し、添付された情報と比較します。両方が一致しない場合、メッセージを拒否します。JAX-RSが出てきて、javax.ws.rs.ext.ReaderInterceptor javax.ws.rs.ext.WriterInterceptor
の導入に伴い、トラフィックをクライアント側でもサーバ側でも傍受される可能性があります。サーバー上のReaderInterceptor
インタフェースの実装はMessageBodyReader#readFrom
をラップしており、実際のシリアル化前に実行されます。The
PayloadVerifier
はヘッダから署名を取り出し、ストリームからフィンガープリントを計算し、最終的にReaderInterceptorContext#proceed
メソッドを呼び出します。このメソッドはチェーンの次のインターセプターもしくは MessageBodyReader
インスタンスを呼び出します(コード7参照)。コード7変更されたコンテンツは結果として異なるフィンガープリントになるため、BAD_REQUEST (400) というレスポンスコードとともに
public class PayloadVerifier implements ReaderInterceptor{ public static final String SIGNATURE_HEADER = "x-signature"; @Override public Object aroundReadFrom(ReaderInterceptorContext ric) throws IOException, WebApplicationException { MultivaluedMap<String, String> headers = ric.getHeaders(); String headerSignagure = headers.getFirst(SIGNATURE_HEADER); InputStream inputStream = ric.getInputStream(); byte[] content = fetchBytes(inputStream); String payload = computeFingerprint(content); if (!payload.equals(headerSignagure)) { Response response = Response.status(Response.Status.BAD_REQUEST).header( SIGNATURE_HEADER, "Modified content").build(); throw new WebApplicationException(response); } ByteArrayInputStream buffer = new ByteArrayInputStream(content); ric.setInputStream(buffer); return ric.proceed(); } //... }
WebApplicationException
を例外として送出する原因になります。すべてのフィンガープリントの計算や送出するリクエストの計算を
WriterInterceptor
の実装により簡単に自動化できます。WriterInterceptor
の実装はMessageBodyWriter#writeTo
をラップしており、この実装はエンティティをシリアル化してストリームに投入する前に実行されます。フィンガープリントの計算については、送信中のエンティティの最終的な表現を必要とするので、バッファとしてのByteArrayOutputStream
に渡し、WriterInterceptorContext#proceed()
メソッドを呼び出し、生のコンテンツを取り出してフィンガープリントを計算します(コード8参照)。コード8最終的に、計算された署名をリクエストにヘッダとして追加し、バッファを元のストリームに書き出します。その上で、リクエスト全体をクライアントに送信します。当然ながら、一つのクラスで同時に両方のインターフェースを実装することもできます。
public class PayloadVerifier implements WriterInterceptor { public static final String SIGNATURE_HEADER = "x-signature"; @Override public void aroundWriteTo(WriterInterceptorContext wic) throws IOException, WebApplicationException { OutputStream oos = wic.getOutputStream(); ByteArrayOutputStream baos = new ByteArrayOutputStream(); wic.setOutputStream(baos); wic.proceed(); baos.flush(); byte[] content = baos.toByteArray(); MultivaluedMap<String, Object> headers = wic.getHeaders(); headers.add(SIGNATURE_HEADER, computeFingerprint(content)); oos.write(content); } //... }
コード9以前のJAX-RSリリースのように、カスタム拡張を自動的に発見し、
import javax.ws.rs.ext.Provider; @Provider public class PayloadVerifier implements ReaderInterceptor, WriterInterceptor { }
@Provider
アノテーションを使って登録します。MessageBodyWriter
とMessageBodyReader
インスタンスの傍受については、ReaderInterceptor
とWriterInterceptor
の実装のみを@Provider
アノテーションで修飾すればよく、追加の構成やAPIの呼び出しは不要です。Request Interception
ContainerRequestFilter
とContainerResponseFilter
の実装はエンティティの読み書き処理だけではなく、リクエスト全体を傍受します。両インターセプターの機能は、生のjavax.servlet.http.HttpServletRequest
インスタンスに含まれている情報のロギングよりも、はるかに便利です。TrafficLogger
クラスはHttpServletRequest
に含まれる情報をログに記録するだけでなく、特定のリクエストに一致するリソースに関する情報をトレースすることもできます(コード10参照)。コード10従って、登録された
@Provider public class TrafficLogger implements ContainerRequestFilter, ContainerResponseFilter { //ContainerRequestFilter public void filter(ContainerRequestContext requestContext) throws IOException { log(requestContext); } //ContainerResponseFilter public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext) throws IOException { log(responseContext); } void log(ContainerRequestContext requestContext) { SecurityContext securityContext = requestContext.getSecurityContext(); String authentication = securityContext.getAuthenticationScheme(); Principal userPrincipal = securityContext.getUserPrincipal(); UriInfo uriInfo = requestContext.getUriInfo(); String method = requestContext.getMethod(); List<Object> matchedResources = uriInfo.getMatchedResources(); //... } void log(ContainerResponseContext responseContext) { MultivaluedMap<String, String> stringHeaders = responseContext.getStringHeaders(); Object entity = responseContext.getEntity(); //... } }
ContainerResponseFilter
の実装はContainerResponseContext
のインスタンスを取得して、サーバーが生成したデータにアクセスすることができます。ステータスコードとヘッダーの内容、例えばLocation
ヘッダーは簡単にアクセスできます。ContainerRequestContext
とContainerResponseContext
はフィルターによって変更される可能性のある可変クラスです。追加の構成をせずに、
ContainerRequestFilter
をHTTPーリソース照合フェーズの後で実行します。この時点ではもはや入ってくるリクエストを変更してリソースバインディングをカスタマイズすることはできません。リクエストとリソース間のバインディングに影響を与えたい場合には、ContainerRequestFilter
を構成し、リソースバインディングフェーズの前に呼び出されるようにすればよいのです。javax.ws.rs.container.PreMatching
アノテーションで修飾されている任意のContainerRequestFilter
をリソースバインディングの前に実行するので、HTTPリクエストコンテンツを所望のマッピングに微調整することができます。よくある@PreMatching
フィルターのユースケースではHTTP同士を調整し、ネットワーク基盤の制限を克服しています。PUT
、OPTIONS
、HEAD
、DELETE
のような、より「難解」なメソッドをファイアウォールでフィルタリングしたり、HTTPクライアントでサポートしなかったりする可能性があります。@PreMatching ContainerRequestFilter
実装は所望のHTTP動詞が示しているヘッダーから情報を取り出し(例えば"X-HTTP-Method-Override
")たり、POST
リクエストを実行中にPUT
リクエストに変更したりすることが可能です(コード11参照)。コード11
@Provider @PreMatching public class HttpMethodOverrideEnabler implements ContainerRequestFilter { public void filter(ContainerRequestContext requestContext) throws IOException { String override = requestContext.getHeaders() .getFirst("X-HTTP-Method-Override"); if (override != null) { requestContext.setMethod(override); } } }
Configuration
@Provider
アノテーションで登録されているすべてのインターセプターおよびフィルターはすべてのリソースで利用できます。デプロイ時にサーバーは@Provider
アノテーションのデプロイメント単位をスキャンし、アプリケーションがアクティベートされる前に自動的にすべての拡張を登録します。すべての拡張を専用のJARにパッケージし、オンデマンドでWAR( WEB-INF/lib
フォルダの中に)でデプロイします。JAX-RSランタイムはJARをスキャンし、自動的に拡張を登録します。自己完結型のJARの一時的なデプロイメントはよいのですが、きめ細かい拡張のパッケージングを必要とします。JARに含まれるすべての拡張をすぐにアクティベートすることになるでしょう。JAX-RSでは選択的なリソースの装飾のためのバインディングアノテーションを導入しています。機構はCDIのqualifierに類似しています。
javax.ws.rs.NameBinding
メタアノテーションがついた
任意のカスタムアノテーションを使ってインターセプト・ポイントを宣言することができます。コード12
@NameBinding @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE, ElementType.METHOD}) public @interface Tracked { }
Tracked
アノテーションがついたすべてのインターセプターやフィルターをアプリケーションのクラスやメソッド、サブクラスに同じTracked
アノテーションをつけることで選択的にアクティベートすることができます。コード13アプリケーション開発者はカスタムの
@Tracked @Provider public class TrafficLogger implements ContainerRequestFilter, ContainerResponseFilter { }
NameBinding
アノテーションを対応するフィルターやインターセプターとともにパッケージングして、選択的にリソースへ適用することができます。アノテーション駆動アプローチによりすばらしく柔軟性が増し、より粗いプラグインパッケージを使用できますが、バインディングはまだ静的です。インターセプターまたはフィルタチェーンを変更するには、アプリケーションの再コンパイルし、効果的に再デプロイする必要があります。分野横断的な機能のグローバルおよびアノテーション駆動型の構成に加えて、JAX-RS2.0では動的な拡張登録のための新しいAPIを導入しました。
@Provider
アノテーションを付けたjavax.ws.rs.container.DynamicFeature
インタフェースの実装をインターセプターやフィルタの動的な登録のためのフックとしてコンテナが利用しますので、再コンパイルが不要になります。LoggerRegistration
の拡張は、条件付きで事前定義されたシステムプロパティの有無を照会することによってPayloadVerifier
インターセプターとTrafficLogger
フィルタを登録します(コード14参照)。コード14
@Provider public class LoggerRegistration implements DynamicFeature { @Override public void configure(ResourceInfo resourceInfo, FeatureContext context) { String debug = System.getProperty("jax-rs.traffic"); if (debug != null) { context.register(new TrafficLogger()); } String verification = System.getProperty("jax-rs.verification"); if (verification != null) { context.register(new PayloadVerifier()); } } }
The Client Side
JAX-RS 1.1仕様ではクライアントを考慮していませんでした。クライアントREST APIのプロプライエタリな実装、例えばRESTEasyやJerseyなどは(Java EEで実装されていないものであっても)任意のHTTPリソースと通信できましたが、クライアントコードは特定の実装に直接依存していました。JAX-RS 2.0では新たに標準化されたClient APIが導入されています。標準化されたブート処理を使うと、Service Provider Interface (SPI) は置き換え可能です。APIはいろいろな機能を有しており、大部分のRESTクライアントの独自実装に類似しています (コード15参照)。コード15上記の統合テストでは、デフォルトの
import java.util.Collection; import javax.ws.rs.client.Client; import javax.ws.rs.client.ClientBuilder; import javax.ws.rs.client.Entity; import javax.ws.rs.client.WebTarget; import javax.ws.rs.core.GenericType; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; public class CoffeeBeansResourceTest { Client client; WebTarget root; @Before public void initClient() { this.client = ClientBuilder.newClient().register(PayloadVerifier.class); this.root = this.client.target("http://localhost:8080/roast-house/api/coffeebeans"); } @Test public void crud() { Bean origin = new Bean("arabica", RoastType.DARK, "mexico"); final String mediaType = MediaType.APPLICATION_XML; final Entity<Bean> entity = Entity.entity(origin, mediaType); Response response = this.root.request().post(entity, Response.class); assertThat(response.getStatus(), is(201)); Bean result = this.root.path(origin.getName()).request(mediaType).get(Bean.class); assertThat(result, is(origin)); Collection<Bean> allBeans = this.root.request().get( new GenericType<Collection<Bean>>() { }); assertThat(allBeans.size(), is(1)); assertThat(allBeans, hasItem(origin)); response = this.root.path(origin.getName()).request(mediaType).delete(Response.class); assertThat(response.getStatus(), is(204)); response = this.root.path(origin.getName()).request(mediaType).get(Response.class); assertThat(response.getStatus(), is(204)); } //.. }
Client
インスタンスを、パラメータなしのClientFactory.newClient()
メソッドを使用して取得しています。ブートストラッププロセス自体はjavax.ws.rs.ext.RuntimeDelegate
という内部抽象ファクトリで標準化されています。RuntimeDelegate
の既存のインスタンスをClientFactory
に注入する(例えば、依存性注入フレームワークによって)か、ファイルMETA-INF/services/javax.ws.rs.ext.RuntimeDelegate
ファイルと${java.home}/lib/jaxrs.properties
ファイルのヒントを探し、最終的にはjavax.ws.rs.ext.RuntimeDelegate
システムプロパティを検索して取得します。発見できない場合にはデフォルト(Jersey)の実装では、初期化しようとします。javax.ws.rs.client.Client
の主要な目的はjavax.ws.rs.client.WebTarget
やjavax.ws.rs.client.Invocation
インスタンスにスムーズにアクセスできるようにすることです。WebTarget
はJAX−RSリソースを表し、Invocation
は発行を待つ、すぐ使えるリクエストです。WebTarget
はInvocation
ファクトリでもあります。CoffeBeansResourceTest#crud()
メソッドで、Bean
オブジェクトはクライアントとサーバーの間で送受信されます。MediaType.APPLICATION_XML
を選択すると、XMLドキュメントでシリアル化されたDTOの送受信のためにほんの数個のJAXBアノテーションを必要とします。コード16サーバーの表記でマーシャリングがうまくいくためにクラスや属性の名前は一致する必要がありますが、DTOがバイナリ互換である必要はありません。上の例では、両
@XmlRootElement @XmlAccessorType(XmlAccessType.FIELD) public class Bean { private String name; private RoastType type; private String blend; }
Bean
クラスを異なるパッケージに配置し、異なるメソッドを実装しています。所望のMediaType
をWebTarget#request()
メソッドに渡します(このメソッドは同期Invocation.Builder
クラスのインスタンスを返します)。HTTP動詞(GET
、POST
、PUT
、DELETE
、HEAD
、OPTIONS
, もしくはTRACE
)にちなんで名付けられたメソッドの最後の呼び出しは同期リクエストを開始します。この新しいクライアントAPIは非同期リソース呼び出しもサポートしています。先ほど述べたように、
Invocation
インスタンスはsubmissionからリクエストを分離します。非同期リクエストを連鎖した、(AsyncInvoker
インスタンスを返す)async()
メソッドの呼び出しにより開始することができます(コード17参照)。コード17上の例での「疑似非同期」の通信スタイルにはそれほどメリットはありません。クライアントは依然としてブロックしレスポンス到着を待つ必要があるからです。しかしながら、
@Test public void roasterFuture() throws Exception { //... Future<Response> future = this.root.path("roaster").path("roast-id").request().async().post(entity); Response response = future.get(5000, TimeUnit.SECONDS); Object result = response.getEntity(); assertNotNull(result); assertThat(roasted.getBlend(),containsString("The dark side of the bean")); }
Future
ベースの呼び出しはバッチ処理において非常に有用です。クライアントは様々なリクエストを一度に発行、Future
インスタンスを収集し、後で処理することが可能です。本当に非同期の実装はコールバックを登録することで可能です(コード18参照)。
コード18
@Test public void roasterAsync() throws InterruptedException { //... final Entity<Bean> entity = Entity.entity(origin, mediaType); this.root.path("roaster").path("roast-id").request().async().post( entity, new InvocationCallback<Bean>() { public void completed(Bean rspns) { } public void failed(Throwable thrwbl) { } }); }
Future
を返す各メソッドについて、対応するコールバックメソッドを利用することもできます。メソッド(上記の例ではpost()
)の最後のパラメータとしてInvocationCallback
インタフェースの実装を受け入れ、呼び出しに成功した場合にはペイロードを、失敗した場合には例外をそれぞれ非同期で通知します。組み込みのテンプレート機構を使い、URIの構築を自動化・合理化できます。事前定義されたプレースホルダはリクエストの実行の直前に置き換えられ、
WebTarget
インスタンスを繰り返し生成して保存することができます。コード19小さいけれども重要なことですが、クライアント側では、拡張を初期化時に発見するのではなく、明示的にClientインスタンスの
@Test public void templating() throws Exception { String rootPath = this.root.getUri().getPath(); URI uri = this.root.path("{0}/{last}"). resolveTemplate("0", "hello"). resolveTemplate("last", "REST"). getUri(); assertThat(uri.getPath(), is(rootPath + "/hello/REST")); }
ClientFactory.newClient().register(PayloadVerifier.class)
を使って登録する必要があります。ただし、同じエンティティのインターセプターの実装をクライアントとサーバーで共有することができます。これにより、テストを簡単にし、潜在的なバグを削減し、生産性を向上します。すでに導入されているPayloadVerifier
インターセプターも同様にクライアント側で変更せずに再利用できます。まとめ: Java EE or Not?
興味深いことに、JAX-RSは本格的なアプリケーションサーバーを必要としません。特定のContext Typeを満たした後、JAX-RS2.0に準拠したAPIを使用すると、何でもすることができます。ただし、EJB3.2との組み合わせにより、非同期処理、プーリング(スロットル)、監視が可能になります。Servlet 3との緊密な統合により、AsyncContext
のサポートを通じて@Suspended
のレスポンスの効率的な非同期処理が可能になり、CDIランタイムがイベント機能をもたらします。また、Bean Validationはよく統合されており、リソースパラメータの検証に使用することができます。他のJava EE7のAPIと一緒にJAX-RS2.0を使用すると、リモートシステムへオブジェクトを公開するための、最も便利(=構成不要)にして最も生産的(=再発明不要)な方法を入手することができます。参考資料
- RESTEasy
http://www.jboss.org/resteasy - Jersey
http://jersey.java.net/ - JAX-RS 2.0 specification
http://www.jcp.org/en/jsr/detail?id=339 - Real World Java EE Patterns—Rethinking Best Practices
http://realworldpatterns.com/
- HTTP RFC
http://www.w3.org/Protocols/rfc2616/rfc2616.html - Java EE 6 Observer
http://www.adam-bien.com/roller/abien/entry/java_ee_6_observer_with - Digester
http://digester.adam-bien.com/
著者について
Adam Bienはコンサルタントで、Java EE 6/7、EJB 3.x、JAX-RS、JPA 2.xのJSRのExpert Groupメンバーです。JDK 1.0のころからServlets/EJB 1.0を使ってJavaテクノロジーを扱う仕事をしており、今ではJava SEおよびJava EEプロジェクトのアーキテクト、開発者です。JavaFX、J2EE、Java EEに関する書籍を執筆しており、「Real World Java EE Pattrerns - Rethinking Best Practices」と「Real World Java EE Night Hacks」の著者でもあります。Real World Java EE Patterns—Rethinking Best PracticesAdamはJava Championにして、Top Java Ambassador 2012、JavaOne 2009、2011、2012のRock Starでもあります。Adamはミュンヘン空港で時々Java (EE)ワークショップを開催しています。
http://realworldpatterns.com/
Real World Java EE Night Hacks
http://press.adam-bien.com/real-world-java-ee-night-hacks-dissecting-the-business-tier.htm
Java EE 6/7 Workshops @Airport Munich, Germany
http://airhacks.com/
Join the Conversation
Facebook、Twitter、Oracle Java BlogでのJavaコミュニティの会話に参加して下さい!
https://www.facebook.com/ilovejava
https://twitter.com/java
Oracle Java Blog
https://blogs.oracle.com/java/
0 件のコメント:
コメントを投稿