http://habrahabr.ru/post/244015/
Описание проблемы
Пусть у нас есть некоторый класс X, параметризующийся из контейнера свойств PX и есть класс Y, расширяющий X, параметризующийся контейнером PY, расширяющим PX.
Если контейнерами свойств выступают аннотации, то мы имеем два класса:
@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 или взять их из какого-либо другого источника, например из спрингового Environment?
Если не прибегать к изощренным трюкам типа создания аннотированных классов «на лету» с подстановкой параметров аннотаций, то ничего.
Пример решения
Допустим нам надо создать абстрактный класс конфигурируемого сервиса, у которого есть некоторый Executor, выполняющий определенные задачи. Назовем его 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;
}
От данного сервиса мы хотим наследовать сервис DemoService, параметризуемый аннотацией @DemoServiceParams:
/*
* 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;
}
Ключевым элементом здесь является декларация <P extends CommonServiceParams & DemoServiceParams> для класса DemoService.
Для того, чтобы создавать инстансы P extends 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, который «просматривает» все задекларированные аннотации и мержит значения при конкретном вызове метода.
Данный класс также решает проблему с инжекцией свойств через проивольный интерфейс P, расширяющий интерфейсы аннотаций PA, PB, ..., PZ. Разумеется, теперь становится возможным получить эти аннотации
на стадии вызова конструктора из совершенно другого источника.
Рассмотрим конкретный пример инжекции свойств из спрингового 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;
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);
}
}
}
После запуска получаем:
------------------------------------------------------------------------
Building AnnotationServiceParameters 1.0-SNAPSHOT
------------------------------------------------------------------------
--- exec-maven-plugin:1.2.1:exec (default-cli) @ AnnotationServiceParameters ---
ноя 23, 2014 1:52:36 PM org.springframework.context.annotation.AnnotationConfigApplicationContext prepareRefresh
INFO: Refreshing org.springframework.context.annotation.AnnotationConfigApplicationContext@50040f0c: startup date [Sun Nov 23 13:52:36 FET 2014]; root of context hierarchy
ноя 23, 2014 1:52:36 PM org.dimitrovchi.conf.service.demo.DemoService <init>
INFO: demoService{shutdownTimeout=10, threadCount=24, keepAlive=1, timeUnit=SECONDS, queueType=ARRAY_BLOCKING_QUEUE, queueSize=1024, host=localhost, port=8080}
ноя 23, 2014 1:52:36 PM org.dimitrovchi.conf.service.demo.DemoService start
INFO: DemoService started
ноя 23, 2014 1:52:36 PM org.dimitrovchi.Demo main
INFO: Context org.springframework.context.annotation.AnnotationConfigApplicationContext@50040f0c: startup date [Sun Nov 23 13:52:36 FET 2014]; root of context hierarchy started
ноя 23, 2014 1:53:36 PM org.springframework.context.annotation.AnnotationConfigApplicationContext doClose
INFO: Closing org.springframework.context.annotation.AnnotationConfigApplicationContext@50040f0c: startup date [Sun Nov 23 13:52:36 FET 2014]; root of context hierarchy
ноя 23, 2014 1:53:46 PM org.dimitrovchi.conf.service.demo.DemoService stop
INFO: DemoService destroyed
------------------------------------------------------------------------
BUILD SUCCESS
------------------------------------------------------------------------
Total time: 01:10 min
Finished at: 2014-11-23T13:53:46+03:00
Final Memory: 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 {
}
}
}
После запуска:
------------------------------------------------------------------------
Building AnnotationServiceParameters 1.0-SNAPSHOT
------------------------------------------------------------------------
--- exec-maven-plugin:1.2.1:exec (default-cli) @ AnnotationServiceParameters ---
ноя 23, 2014 1:55:47 PM org.dimitrovchi.AnnotatedServiceDemo$AnnotatedDemoService <init>
INFO: demoService{host=localhost, port=8888, threadCount=1, shutdownTimeout=10, keepAlive=0, timeUnit=MILLISECONDS, queueType=LINKED_BLOCKING_QUEUE, queueSize=0}
ноя 23, 2014 1:55:47 PM org.dimitrovchi.AnnotatedServiceDemo$AnnotatedDemoService start
INFO: AnnotatedDemoService started
ноя 23, 2014 1:56:57 PM org.dimitrovchi.AnnotatedServiceDemo$AnnotatedDemoService stop
INFO: AnnotatedDemoService destroyed
------------------------------------------------------------------------
BUILD SUCCESS
------------------------------------------------------------------------
Total time: 01:10 min
Finished at: 2014-11-23T13:56:58+03:00
Final Memory: 8M/304M
------------------------------------------------------------------------
Как видно, сервис был запущен на порту 8888 с количеством потоков 1.
Вывод
Итак, мы получили небольшой фреймворк, чтобы создавать параметризуемые сервисы, причем имеется возможность передавать параметры как с использованием аннотаций, так и инжектировать свойства из других источников.