問題の説明
プロパティPXのコンテナからパラメータ化するクラスXがあり、Xを拡張するクラスYがあり、PXを拡張するコンテナPYをパラメータ化するとします。
アノテーションがプロパティコンテナの場合、2つのクラスがあります。
@PX(propertyX1 = <valueX1>, ..., propertyXN = <valueXN>) class X { ... }
そして、クラスがあります:
@PY(propertyY1 = <valueY1>, ..., propertyYN = <valueYN>) class Y extends X { ... }
Java(Java 8を含む)は注釈を継承する機能を提供しないため、以下の例のような記述はできません。
public @interface PX extends PY { }
もちろん、これは問題ではありません。解決策は次のとおりです。
@PX class X { protected final ... propertyX1; ... protected final ... propertyY1; X() { final PX px = getClass().getAnnotation(PX.class); propertyX1 = px.propertyX1(); ... propertyXN = px.propertyXN(); } } @PY class Y extends X { Y() { final PY py = getClass().getAnnotation(PY.class); propertyY1 = py.propertyY1(); ... propertyYN = py.propertyYN(); } }
ここでの欠点は何ですか? 欠点は、クラスに注釈がない場合、デフォルト値で構成されるという事実に自分自身を運命づけることです(注釈PXおよびPYは、このために@Inheritedする必要があります)。
たとえば、.propertiesファイルからプロパティを挿入したり、他のソース(Spring Environmentなど)からプロパティを取得したりする必要がある場合はどうでしょうか。
注釈パラメーターを置換して、その場で注釈付きクラスを作成するなどの高度なトリックに頼らない場合は、何もしません。
ソリューション例
特定のタスクを実行するいくつかのエグゼキューターを持つ構成可能なサービスの抽象クラスを作成する必要があるとします。 それをAbstractServiceと呼びましょう。
このサービスのプロパティを保存するコンテナは、@ CommonServiceParamsアノテーションになります。
/* * Copyright 2014 Dmitry Ovchinnikov. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.dimitrovchi.conf.service; import java.util.concurrent.ThreadPoolExecutor; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.core.env.Environment; /** * Abstract demo service. * @author Dmitry Ovchinnikov * @param <P> Service parameters container type. */ public abstract class AbstractService<P extends CommonServiceParams> implements AutoCloseable { protected final Log log = LogFactory.getLog(getClass()); protected final P parameters; protected final ThreadPoolExecutor executor; public AbstractService(P parameters) { this.parameters = parameters; final int threadCount = parameters.threadCount() == 0 ? Runtime.getRuntime().availableProcessors() : parameters.threadCount(); this.executor = new ThreadPoolExecutor( threadCount, parameters.threadCount(), parameters.keepAlive(), parameters.timeUnit(), parameters.queueType().createBlockingQueue(parameters.queueSize()), new ThreadPoolExecutor.CallerRunsPolicy() ); } @Override public void close() throws Exception { executor.shutdown(); } /** * Merges annotated parameters from class annotations. * @param <P> Parameters type. * @return Merged parameters. */ protected static <P> P mergeAnnotationParameters() { return ServiceParameterUtils.mergeAnnotationParameters(); } /** * Get parameters from Spring environment. * @param <P> Parameters type. * @param prefix Environment prefix. * @param environment Spring environment. * @return Parameters parsed from the environment. */ protected static <P> P parameters(String prefix, Environment environment) { return ServiceParameterUtils.parameters(prefix, environment); } }
/* * Copyright 2014 Dmitry Ovchinnikov. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.dimitrovchi.conf.service; import java.lang.annotation.ElementType; import java.lang.annotation.Inherited; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.util.concurrent.TimeUnit; import org.dimitrovchi.concurrent.BlockingQueueType; /** * Common service parameters. * @author Dmitry Ovchinnikov */ @Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Inherited public @interface CommonServiceParams { int threadCount() default 0; long keepAlive() default 0L; TimeUnit timeUnit() default TimeUnit.MILLISECONDS; int queueSize() default 0; BlockingQueueType queueType() default BlockingQueueType.LINKED_BLOCKING_QUEUE; }
このサービスから、@ DemoServiceParamsアノテーションでパラメーター化されたDemoServiceサービスを継承します。
/* * Copyright 2014 Dmitry Ovchinnikov. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.dimitrovchi.conf.service.demo; import com.sun.net.httpserver.HttpExchange; import com.sun.net.httpserver.HttpHandler; import com.sun.net.httpserver.HttpServer; import java.io.IOException; import java.net.HttpURLConnection; import java.net.InetSocketAddress; import java.nio.charset.StandardCharsets; import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; import org.dimitrovchi.conf.service.AbstractService; import org.dimitrovchi.conf.service.CommonServiceParams; import org.dimitrovchi.conf.service.ServiceParameterUtils; import org.springframework.core.env.Environment; /** * Demo service. * @author Dmitry Ovchinnikov * @param <P> Demo service parameters container type. */ public class DemoService<P extends CommonServiceParams & DemoServiceParams> extends AbstractService<P> { protected final HttpServer httpServer; public DemoService(P parameters) throws IOException { super(parameters); this.httpServer = HttpServer.create(new InetSocketAddress(parameters.host(), parameters.port()), 0); this.httpServer.setExecutor(executor); this.httpServer.createContext("/", new HttpHandler() { @Override public void handle(HttpExchange he) throws IOException { he.getResponseHeaders().add("Content-Type", "text/plain; charset=utf-8"); final byte[] message = "hello!".getBytes(StandardCharsets.UTF_8); he.sendResponseHeaders(HttpURLConnection.HTTP_OK, message.length); he.getResponseBody().write(message); } }); log.info(ServiceParameterUtils.reflectToString("demoService", parameters)); } public DemoService() throws IOException { this(DemoService.<P>mergeAnnotationParameters()); // In Java 8 just call mergeAnnotationParameters() ;-) } public DemoService(String prefix, Environment environment) throws IOException { this(DemoService.<P>parameters(prefix, environment)); } @PostConstruct public void start() { httpServer.start(); log.info(getClass().getSimpleName() + " started"); } @PreDestroy public void stop() { httpServer.stop(parameters.shutdownTimeout()); log.info(getClass().getSimpleName() + " destroyed"); } }
/* * Copyright 2014 Dmitry Ovchinnikov. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.dimitrovchi.conf.service.demo; import java.lang.annotation.ElementType; import java.lang.annotation.Inherited; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * Demo service parameters. * @author Dmitry Ovchinnikov */ @Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Inherited public @interface DemoServiceParams { String host() default "localhost"; int port() default 8080; int shutdownTimeout() default 10; }
ここで重要な要素は、DemoServiceクラスの<P extends CommonServiceParams&DemoServiceParams>宣言です。
Pのインスタンスを作成してPA&PB&...&PZを拡張するには、このクラスが必要です。
/* * Copyright 2014 Dmitry Ovchinnikov. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.dimitrovchi.conf.service; import java.lang.annotation.Annotation; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; import java.lang.reflect.Type; import java.lang.reflect.TypeVariable; import java.util.ArrayList; import java.util.Collections; import java.util.IdentityHashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Objects; import org.springframework.core.env.Environment; /** * * @author Dmitry Ovchinnikov */ @SuppressWarnings("unchecked") public class ServiceParameterUtils { static AnnotationParameters annotationParameters() { final Class<?>[] stack = ClassResolver.CLASS_RESOLVER.getClassContext(); final Class<?> caller = stack[3]; final List<Class<? extends Annotation>> interfaces = new ArrayList<>(); Class<?> topCaller = null; for (int i = 3; i < stack.length && caller.isAssignableFrom(stack[i]); i++) { final Class<?> c = stack[i]; topCaller = stack[i]; if (c.getTypeParameters().length != 0) { final TypeVariable<? extends Class<?>> var = c.getTypeParameters()[0]; final List<Class<? extends Annotation>> bounds = new ArrayList<>(var.getBounds().length); for (final Type type : var.getBounds()) { if (type instanceof Class<?> && ((Class<?>) type).isAnnotation()) { bounds.add((Class) type); } } if (bounds.size() > interfaces.size()) { interfaces.clear(); interfaces.addAll(bounds); } } } final Map<Class<? extends Annotation>, List<Annotation>> annotationMap = new IdentityHashMap<>(); for (int i = 3; i < stack.length && caller.isAssignableFrom(stack[i]); i++) { final Class<?> c = stack[i]; for (final Class<? extends Annotation> itf : interfaces) { final Annotation annotation = c.getAnnotation(itf); if (annotation != null) { List<Annotation> annotationList = annotationMap.get(itf); if (annotationList == null) { annotationMap.put(itf, annotationList = new ArrayList<>()); } annotationList.add(0, annotation); } } } return new AnnotationParameters(topCaller, interfaces, annotationMap); } @SuppressWarnings({"element-type-mismatch"}) public static <P> P mergeAnnotationParameters() { final AnnotationParameters aParameters = annotationParameters(); return (P) Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(), aParameters.annotations.toArray(new Class[aParameters.annotations.size()]), new InvocationHandler() { @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { if ("toString".equals(method.getName())) { return reflectToString(aParameters.topCaller.getSimpleName(), proxy); } final Class<?> annotationClass = method.getDeclaringClass(); final List<Annotation> annotations = aParameters.annotationMap.containsKey(annotationClass) ? aParameters.annotationMap.get(annotationClass) : Collections.<Annotation>emptyList(); for (final Annotation annotation : annotations) { final Object value = method.invoke(annotation, args); if (!Objects.deepEquals(method.getDefaultValue(), value)) { return value; } } return method.getDefaultValue(); } }); } public static <P> P parameters(final String prefix, final Environment environment) { final AnnotationParameters aParameters = annotationParameters(); return (P) Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(), aParameters.annotations.toArray(new Class[aParameters.annotations.size()]), new InvocationHandler() { @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { if ("toString".equals(method.getName())) { return reflectToString(prefix, proxy); } return environment.getProperty( prefix + "." + method.getName(), (Class) method.getReturnType(), method.getDefaultValue()); } }); } public static String reflectToString(String name, Object proxy) { final Map<String, Object> map = new LinkedHashMap<>(); for (final Method method : proxy.getClass().getMethods()) { if (method.getDeclaringClass() == Object.class || method.getDeclaringClass() == Proxy.class) { continue; } switch (method.getName()) { case "toString": case "hashCode": case "annotationType": continue; } if (method.getParameterCount() == 0) { try { map.put(method.getName(), method.invoke(proxy)); } catch (ReflectiveOperationException x) { throw new IllegalStateException(x); } } } return name + map; } static class AnnotationParameters { final Class<?> topCaller; final List<Class<? extends Annotation>> annotations; final Map<Class<? extends Annotation>, List<Annotation>> annotationMap; AnnotationParameters( Class<?> topCaller, List<Class<? extends Annotation>> annotations, Map<Class<? extends Annotation>, List<Annotation>> annotationMap) { this.topCaller = topCaller; this.annotations = annotations; this.annotationMap = annotationMap; } } static final class ClassResolver extends SecurityManager { @Override protected Class[] getClassContext() { return super.getClassContext(); } static final ClassResolver CLASS_RESOLVER = new ClassResolver(); } }
コードから簡単に推測できるように、annotationParametersメソッドは現在の呼び出しスタックを取得します(これは、コンストラクターでthis(...)またはsuper(...)を呼び出す段階で、どのクラスを扱っているかを知るために必要です。
次に、特定の抽象クラスまたはその子孫に注釈を付けることができる注釈クラスのリストが形成されます。
次に、これらのすべての注釈が見つかり、Proxy型のインスタンスが形成されます。これは、宣言されたすべての注釈を「調べ」、特定のメソッド呼び出しの値を保持します。
このクラスは、注釈インターフェースPA、PB、...、PZを拡張する任意のインターフェースPを介してプロパティを注入する問題も解決します。 もちろん、完全に異なるソースからコンストラクターを呼び出す段階でこれらの注釈を取得することが可能になりました 。
湧き出す環境からのプロパティの注入の特定の例を考えてみましょう。
/* * Copyright 2014 Dmitry Ovchinnikov. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.dimitrovchi.conf; import java.io.IOException; import org.dimitrovchi.conf.service.demo.DemoService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; import org.springframework.core.env.Environment; /** * Demo application configuration. * @author Dmitry Ovchinnikov */ @Configuration @PropertySource("classpath:/application.properties") public class DemoApplicationConfiguration { @Autowired private Environment environment; @Bean public DemoService demoService() throws IOException { return new DemoService("demoService", environment); } }
ここでは、application.propertiesファイルからプロパティを取得します。
# Copyright 2014 Dmitry Ovchinnikov. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. demoService.threadCount = 24 demoService.timeUnit = SECONDS demoService.keepAlive = 1 demoService.queueSize = 1024 demoService.queueType = ARRAY_BLOCKING_QUEUE demoService.port = 8080
アプリケーションのエントリポイントを記述しましょう。
/* * Copyright 2014 Dmitry Ovchinnikov. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.dimitrovchi; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.dimitrovchi.conf.DemoApplicationConfiguration; import org.springframework.context.annotation.AnnotationConfigApplicationContext; /** * Demo entry-point class. * @author Dmitry Ovchinnikov */ public class Demo { private static final Log LOG = LogFactory.getLog(Demo.class); public static void main(String... args) throws Exception { final String confPkgName = DemoApplicationConfiguration.class.getPackage().getName(); try (final AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(confPkgName)) { LOG.info("Context " + context + " started"); context.start(); Thread.sleep(60_000L); } } }
開始後、以下が得られます。
-------------------------------------------------- ---------------------- AnnotationServiceParameters 1.0-SNAPSHOTの構築 -------------------------------------------------- ---------------------- --- exec-maven-plugin:1.2.1:exec(default-cli)@ AnnotationServiceParameters --- 2014年11月23日1:52:36 PM org.springframework.context.annotation.AnnotationConfigApplicationContext prepareRefresh 情報:org.springframework.context.annotation.AnnotationConfigApplicationContext@50040f0cの更新:開始日[Sun Nov 23 13:52:36 FET 2014]; コンテキスト階層のルート 2014年11月23日1:52:36 PM org.dimitrovchi.conf.service.demo.DemoService <init> 情報:demoService {shutdownTimeout = 10、threadCount = 24、keepAlive = 1、timeUnit = SECONDS、queueType = ARRAY_BLOCKING_QUEUE、queueSize = 1024、host = localhost、port = 8080} 2014年11月23日1:52:36 org.dimitrovchi.conf.service.demo.DemoServiceの開始 情報:DemoServiceが開始されました 2014年11月23日1:52:36 PM org.dimitrovchi.Demoメイン 情報:コンテキストorg.springframework.context.annotation.AnnotationConfigApplicationContext@50040f0c:開始日[Sun Nov 23 13:52:36 FET 2014]; コンテキスト階層のルートが開始されました 2014年11月23日1:53:36 PM org.springframework.context.annotation.AnnotationConfigApplicationContext doClose 情報:クロージングorg.springframework.context.annotation.AnnotationConfigApplicationContext@50040f0c:起動日[Sun Nov 23 13:52:36 FET 2014]; コンテキスト階層のルート 2014年11月23日1:53:46 org.dimitrovchi.conf.service.demo.DemoServiceの停止 情報:DemoServiceが破壊されました -------------------------------------------------- ---------------------- 成功を築く -------------------------------------------------- ---------------------- 合計時間:01:10分 終了:2014-11-23T13:53:46 + 03:00 最終メモリ:8M / 304M -------------------------------------------------- ----------------------
ご覧のとおり、すべてのパラメーターはプロパティから正しく「ストレッチ」されています。
次に、注釈付きクラスのエントリポイントを作成します。
/* * Copyright 2014 Dmitry Ovchinnikov. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.dimitrovchi; import java.io.IOException; import org.dimitrovchi.conf.service.CommonServiceParams; import org.dimitrovchi.conf.service.demo.DemoService; import org.dimitrovchi.conf.service.demo.DemoServiceParams; /** * Annotated service demo. * @author Dmitry Ovchinnikov */ public class AnnotatedServiceDemo { public static void main(String... args) throws Exception { try (final AnnotatedDemoService service = new AnnotatedDemoService()) { service.start(); Thread.sleep(60_000L); service.stop(); } } @CommonServiceParams(threadCount = 1) @DemoServiceParams(port = 8888) static class AnnotatedDemoService extends DemoService { public AnnotatedDemoService() throws IOException { } } }
起動後:
-------------------------------------------------- ---------------------- AnnotationServiceParameters 1.0-SNAPSHOTの構築 -------------------------------------------------- ---------------------- --- exec-maven-plugin:1.2.1:exec(default-cli)@ AnnotationServiceParameters --- 2014年11月23日1:55:47 org.dimitrovchi.AnnotatedServiceDemo $ AnnotatedDemoService <init> 情報:demoService {ホスト= localhost、ポート= 8888、threadCount = 1、shutdownTimeout = 10、keepAlive = 0、timeUnit = MILLISECONDS、queueType = LINKED_BLOCKING_QUEUE、queueSize = 0} 2014年11月23日1:55:47 org.dimitrovchi.AnnotatedServiceDemo $ AnnotatedDemoService start 情報:AnnotatedDemoServiceが開始されました 2014年11月23日1:56:57 PM org.dimitrovchi.AnnotatedServiceDemo $ AnnotatedDemoService stop 情報:AnnotatedDemoServiceが破壊されました -------------------------------------------------- ---------------------- 成功を築く -------------------------------------------------- ---------------------- 合計時間:01:10分 終了:2014-11-23T13:56:58 + 03:00 最終メモリ:8M / 304M -------------------------------------------------- ----------------------
ご覧のとおり、サービスはスレッド数1でポート8888で起動されました。
おわりに
そのため、パラメータ化可能なサービスを作成するための小さなフレームワークを取得し、アノテーションを使用してパラメータを渡したり、他のソースからプロパティを注入したりすることができます。