📜 ⬆️ ⬇️

Configuring Beans with Annotations: Solving the Inheritance Problem for Annotation Interfaces

Description of the problem


Suppose we have some class X parametrized from the PX property container and there is a class Y extending X parametrizing the PY container extending PX.

If the containers of properties are annotations, then we have two classes:

@PX(propertyX1 = <valueX1>, ..., propertyXN = <valueXN>) class X { ... } 

And there is a class:
 @PY(propertyY1 = <valueY1>, ..., propertyYN = <valueYN>) class Y extends X { ... } 

Java (including Java 8) does not provide the ability to inherit annotations, so you cannot write something like an example below:
')
 public @interface PX extends PY { } 

Of course, this is not a problem, here is the solution:

 @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(); } } 


What is the disadvantage here? The disadvantage is that we doom ourselves that if a class does not have annotations, it will be configured with default values ​​(the PX and PY annotations must be @Inherited for this).

What if we, for example, need to inject a property from a .properties file or take them from some other source, for example from a Spring Environment?

If you do not resort to sophisticated tricks such as creating annotated classes on the fly with the substitution of annotation parameters, then nothing.

Solution example


Suppose we need to create an abstract class of a configurable service that has some Executor that performs certain tasks. Let's call it AbstractService.
The container for storing the properties of this service is the @CommonServiceParams annotation.

 /* * 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; } 


From this service we want to inherit the DemoService service, parameterized by the @DemoServiceParams annotation:

 /* * 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; } 


The key element here is the <P extends CommonServiceParams & DemoServiceParams> declaration for the DemoService class.

In order to create instances of P extends PA & PB & ... & PZ, we need the following class:

 /* * 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(); } } 


As it is easy to guess from the code, the annotationParameters method gets the current call stack (this is necessary so that at the call stage this (...) or super (...) in the constructor to know which class we are dealing with.
Then a list of annotation classes is created that can annotate this abstract class or any of its descendants.
Then all these annotations are found and an instance of the Proxy type is formed, which “looks through” all the declared annotations and merges the values ​​for a particular method call.

This class also solves the problem with the property injection through the pro-field interface P, which extends the annotation interfaces PA, PB, ..., PZ. Of course, it now becomes possible to get these annotations at the stage of invoking the constructor from a completely different source.

Consider a specific example of property injection from a Spring 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); } } 


Here we get the properties from the application.properties file:

 # 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 


Write the entry point for the application:

 /* * 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); } } } 


After launch we get:

 -------------------------------------------------- ----------------------
 Building AnnotationServiceParameters 1.0-SNAPSHOT
 -------------------------------------------------- ----------------------

 --- exec-maven-plugin: 1.2.1: exec (default-cli) @ AnnotationServiceParameters ---
 Nov 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
 Nov 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}
 Nov 23, 2014 1:52:36 PM org.dimitrovchi.conf.service.demo.DemoService start
 INFO: DemoService started
 Nov 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
 Nov 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
 Nov 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
 -------------------------------------------------- ----------------------


As you can see, all the parameters “stretched” correctly from the prop.

Now make an entry point for the annotated class:

 /* * 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 { } } } 


After launch:

 -------------------------------------------------- ----------------------
 Building AnnotationServiceParameters 1.0-SNAPSHOT
 -------------------------------------------------- ----------------------

 --- exec-maven-plugin: 1.2.1: exec (default-cli) @ AnnotationServiceParameters ---
 Nov 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}
 Nov 23, 2014 1:55:47 PM org.dimitrovchi.AnnotatedServiceDemo $ AnnotatedDemoService start
 INFO: AnnotatedDemoService started
 Nov 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
 -------------------------------------------------- ----------------------


As you can see, the service was launched on port 8888 with the number of threads 1.

Conclusion


So, we have a small framework to create parameterized services, and it is possible to transfer parameters using annotations as well as inject properties from other sources.

Source: https://habr.com/ru/post/244015/


All Articles