diff --git a/build.gradle b/build.gradle index 5456ed5..8424145 100644 --- a/build.gradle +++ b/build.gradle @@ -25,8 +25,8 @@ java { def junitJupiterVersion = '5.8.1' dependencies { - api "com.typesafe:config:1.4.1" - implementation 'net.bytebuddy:byte-buddy:1.12.1' + api "com.typesafe:config:1.4.2" + implementation 'net.bytebuddy:byte-buddy:1.12.14' testImplementation("org.junit.jupiter:junit-jupiter-api:${junitJupiterVersion}") testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:${junitJupiterVersion}") } diff --git a/src/main/java/cz/datadriven/utils/config/view/ConfigViewFactory.java b/src/main/java/cz/datadriven/utils/config/view/ConfigViewFactory.java index f24e350..d96e390 100644 --- a/src/main/java/cz/datadriven/utils/config/view/ConfigViewFactory.java +++ b/src/main/java/cz/datadriven/utils/config/view/ConfigViewFactory.java @@ -18,7 +18,10 @@ import com.typesafe.config.Config; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; +import java.util.Map; +import java.util.Objects; import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Collectors; import net.bytebuddy.ByteBuddy; import net.bytebuddy.ClassFileVersion; @@ -35,11 +38,40 @@ private ConfigViewFactory() { // no-op } + private static class ViewProxyKey { + Class viewClass; + Config rawConfig; + + public ViewProxyKey(Class configViewClass, Config config) { + this.viewClass = configViewClass; + this.rawConfig = config; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof ViewProxyKey)) { + return false; + } + ViewProxyKey that = (ViewProxyKey) o; + return viewClass.equals(that.viewClass) && rawConfig.equals(that.rawConfig); + } + + @Override + public int hashCode() { + return Objects.hash(viewClass, rawConfig); + } + } + private static final Set ANNOTATION_TYPE_DESCRIPTORS = ConfigViewProxy.ANNOTATIONS.stream() .map(TypeDescription.ForLoadedType::of) .collect(Collectors.toSet()); + private static final Map, Object> VIEW_PROXY_MAP = new ConcurrentHashMap<>(); + /** * Create config view from a given config. * @@ -54,7 +86,7 @@ public static T create(Class configViewClass, Config config, String baseP } /** - * Create config view from a given config. + * Create config view from a given config or return already cached instance. * * @param configViewClass class to materialize view into * @param config config to create view from @@ -68,14 +100,37 @@ public static T create(Class configViewClass, Config config) { "Can not instantiate ConfigView for class [%s]. Did you forget @ConfigView annotation?", configViewClass)); } + + ViewProxyKey proxyKey = new ViewProxyKey<>(configViewClass, config); + + Object proxiedView = + VIEW_PROXY_MAP.computeIfAbsent( + proxyKey, + viewProxyKey -> { + final ConfigViewProxy proxy = + new ConfigViewProxy(new ConfigViewProxy.Factory(config)); + return instantiateView(configViewClass, proxy); + }); + + return configViewClass.cast(proxiedView); + } + + /** + * Instatiates given class using provided invocation handler for respective method calls. + * + * @param configViewClass Class annotated with 'ConfigView' annotation. + * @param proxy interceptor for methods providing configuration properties. + * @return New instance of given class providing configuration properties by selected methods. + * @param Class to instantiate. + */ + private static T instantiateView(Class configViewClass, ConfigViewProxy proxy) { try { return new ByteBuddy(ClassFileVersion.JAVA_V8) .subclass(configViewClass) .method( ElementMatchers.isAnnotatedWith(ANNOTATION_TYPE_DESCRIPTORS::contains) .or(ElementMatchers.isDeclaredBy(RawConfigAware.class))) - .intercept( - InvocationHandlerAdapter.of(new ConfigViewProxy(new ConfigViewProxy.Factory(config)))) + .intercept(InvocationHandlerAdapter.of(proxy)) .make() .load( ConfigViewFactory.class.getClassLoader(), diff --git a/src/main/java/cz/datadriven/utils/config/view/ConfigViewProxy.java b/src/main/java/cz/datadriven/utils/config/view/ConfigViewProxy.java index 2f66128..0e28327 100644 --- a/src/main/java/cz/datadriven/utils/config/view/ConfigViewProxy.java +++ b/src/main/java/cz/datadriven/utils/config/view/ConfigViewProxy.java @@ -172,7 +172,6 @@ public Object invoke(Object proxy, Method method, Object[] args) { return factory.getConfig(); } else { throw new UnsupportedOperationException("Not implemented"); - // return methodProxy.invokeSuper(obj, args); } } @@ -200,12 +199,12 @@ private Optional getInstrumentAnnotation(Method method) { Arrays.stream(method.getDeclaredAnnotations()) .filter(a -> ANNOTATIONS.contains(a.annotationType())) .collect(Collectors.toList()); - if (annotations.size() == 0) { + if (annotations.isEmpty()) { return Optional.empty(); } else if (annotations.size() == 1) { return Optional.of(annotations.get(0)); } else { - throw new RuntimeException( + throw new IllegalArgumentException( "Method [ " + method + " ] has more than one instrument annotation."); } } diff --git a/src/test/java/cz/datadriven/utils/config/view/ConfigViewTest.java b/src/test/java/cz/datadriven/utils/config/view/ConfigViewTest.java index 8db01e3..1a812ea 100644 --- a/src/test/java/cz/datadriven/utils/config/view/ConfigViewTest.java +++ b/src/test/java/cz/datadriven/utils/config/view/ConfigViewTest.java @@ -167,9 +167,7 @@ void testWrapNonAnnotatedClass() { final Config config = ConfigFactory.empty(); assertThrows( IllegalArgumentException.class, - () -> { - ConfigViewFactory.create(NonAnnotatedTestConfigView.class, config); - }); + () -> ConfigViewFactory.create(NonAnnotatedTestConfigView.class, config)); } @Test @@ -194,4 +192,16 @@ void testQuotedKeyWithDotInAMap() { Assertions.assertEquals(20, mapConfig.data().get("quoted-apple")); Assertions.assertEquals(30, mapConfig.data().get("dotted.apple")); } + + @Test + void sameClassTest() { + final Config config = + ConfigFactory.empty() + .withValue("first", ConfigValueFactory.fromAnyRef("first_value")) + .withValue("second", ConfigValueFactory.fromAnyRef("second_value")); + final TestConfigView wrap = ConfigViewFactory.create(TestConfigView.class, config); + final TestConfigView anotherWrap = ConfigViewFactory.create(TestConfigView.class, config); + assertEquals(wrap.getClass(), anotherWrap.getClass()); + assertEquals(wrap, anotherWrap); + } }