Skip to content

Commit

Permalink
feat: use beans if available (#2)
Browse files Browse the repository at this point in the history
* feat: use beans if available

* fix: remove redundant public modifier
  • Loading branch information
zskamljic authored Feb 2, 2022
1 parent cba7bfc commit a86b594
Show file tree
Hide file tree
Showing 8 changed files with 135 additions and 32 deletions.
4 changes: 2 additions & 2 deletions Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -345,8 +345,8 @@ public class SpringApplicationDemo {
Finally, to have services available as injectable beans add the `@RestAheadService` annotation to the service:

```java

@RestAheadService(url = "https://httpbin.org", converter = JacksonConverter.class)
// Instead of placeholder you can also use a hardcoded URL
@RestAheadService(url = "${placeholder.url}", converter = JacksonConverter.class)
public interface DemoService {
@Get("/get")
Map<String, Object> performGet();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,10 @@ interface AdapterService {
@Get("/get")
Supplier<Response> get();
}

@RestAheadService(url = "${placeholder.url}")
interface PlaceholderService {
@Get("/get")
Response get();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@
import java.util.concurrent.CompletableFuture;

public class DummyClient extends Client {
public static final List<ClientRequestPair> requests = new ArrayList<>();
public final List<Interceptor> interceptors = new ArrayList<>();
private final List<Interceptor> interceptors = new ArrayList<>();
protected static final List<ClientRequestPair> requests = new ArrayList<>();

public List<Interceptor> getInterceptors() {
return interceptors;
Expand Down
1 change: 1 addition & 0 deletions demo/src/main/resources/application.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
placeholder.url=https://httpbin.org
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import org.springframework.test.context.junit.jupiter.SpringExtension;

import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;

@SpringBootTest
Expand All @@ -25,6 +26,9 @@ class ConfigCombinationsTest {
@Autowired
private ConfigCombinations.AdapterService adapterService;

@Autowired
private ConfigCombinations.PlaceholderService placeholderService;

@AfterEach
void tearDown() {
DummyClient.requests.clear();
Expand All @@ -37,7 +41,7 @@ void clientOnlyServiceHasClient() {

assertFalse(requests.isEmpty());
var request = requests.get(0);
assertTrue(((DummyClient) request.client()).interceptors.isEmpty());
assertTrue(((DummyClient) request.client()).getInterceptors().isEmpty());
}

@Test
Expand All @@ -47,7 +51,7 @@ void clientAndInterceptorService() {

assertFalse(requests.isEmpty());
var request = requests.get(0);
assertTrue(((DummyClient) request.client()).interceptors.get(0) instanceof ConfigCombinations.PassThroughInterceptor);
assertTrue(((DummyClient) request.client()).getInterceptors().get(0) instanceof ConfigCombinations.PassThroughInterceptor);
}

@Test
Expand All @@ -65,4 +69,11 @@ void adapterService() {

assertTrue(requests.isEmpty());
}

@Test
void placeholderService() {
var response = placeholderService.get();

assertNotNull(response);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package io.github.zskamljic.restahead.demo.spring;

import io.github.zskamljic.restahead.client.Client;
import io.github.zskamljic.restahead.client.responses.Response;
import io.github.zskamljic.restahead.conversion.Converter;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.context.junit.jupiter.SpringExtension;

import java.io.InputStream;
import java.util.Map;
import java.util.concurrent.CompletableFuture;

import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doReturn;

@SpringBootTest
@ExtendWith(SpringExtension.class)
class PlaceholderServiceBeanTest {
@MockBean
private Client mockClient;

@MockBean
private Converter mockConverter;

@Autowired
private ConfigCombinations.PlaceholderService placeholderService;

@Test
void placeHolderServiceInjectsBeans() {
doReturn(CompletableFuture.completedFuture(new Response(200, Map.of(), InputStream.nullInputStream())))
.when(mockClient).execute(any());

var response = placeholderService.get();

assertNotNull(response);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@
import io.github.zskamljic.restahead.client.Client;
import io.github.zskamljic.restahead.conversion.Converter;
import io.github.zskamljic.restahead.intercepting.Interceptor;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition;
import org.springframework.beans.factory.config.BeanDefinitionHolder;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.beans.factory.support.AbstractBeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.BeanDefinitionReaderUtils;
Expand All @@ -18,6 +20,7 @@
import org.springframework.core.io.ResourceLoader;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.core.type.filter.AnnotationTypeFilter;
import org.springframework.lang.Nullable;
import org.springframework.util.ClassUtils;
import org.springframework.util.CollectionUtils;

Expand Down Expand Up @@ -84,7 +87,7 @@ private void registerService(AnnotatedBeanDefinition definition, BeanDefinitionR
var clazz = ClassUtils.resolveClassName(className, null);

var definitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(clazz,
() -> instantiateService(attributes, clazz)
() -> instantiateService(registry, attributes, clazz)
)
.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE)
.setLazyInit(true);
Expand All @@ -97,16 +100,18 @@ private void registerService(AnnotatedBeanDefinition definition, BeanDefinitionR
/**
* Instantiate a new service with appropriate url, converter etc.
*
* @param <T> the type to cast to (required since clazz has wildcard type
* @param registry the registry to which to add the new bean
* @param attributes the attributes from which to get the data used during creation
* @param clazz the service to instantiate
* @param <T> the type to cast to (required since clazz has wildcard type
* @return the instance of the service
*/
private <T> T instantiateService(Map<String, Object> attributes, Class<?> clazz) {
var url = (String) attributes.get(BASE_URL);
var converter = getConverterIfPossible(attributes);
var client = getClientIfPossible(attributes);
var adapters = getAdaptersIfPossible(attributes);
private <T> T instantiateService(BeanDefinitionRegistry registry, Map<String, Object> attributes, Class<?> clazz) {
var url = environment.resolvePlaceholders((String) attributes.get(BASE_URL));
var beanFactory = registry instanceof ConfigurableBeanFactory factory ? factory : null;
var converter = getConverterIfPossible(beanFactory, attributes);
var client = getClientIfPossible(beanFactory, attributes);
var adapters = getAdaptersIfPossible(beanFactory, attributes);
var builder = RestAhead.builder(url);
converter.ifPresent(builder::converter);
client.ifPresent(builder::client);
Expand All @@ -119,32 +124,40 @@ private <T> T instantiateService(Map<String, Object> attributes, Class<?> clazz)
/**
* Find a converter from attributes, or return empty optional if none was provided.
*
* @param attributes the attributes from which to extract the converter
* @param beanFactory the bean factory to fetch bean instances from
* @param attributes the attributes from which to extract the converter
* @return converter if a valid class was provided, empty if value was the default converter instance
* @throws IllegalArgumentException if converter is not a valid subclass or if no valid constructors are present.
*/
private Optional<Converter> getConverterIfPossible(Map<String, Object> attributes) {
private Optional<Converter> getConverterIfPossible(
@Nullable ConfigurableBeanFactory beanFactory,
Map<String, Object> attributes
) {
var value = attributes.get(CONVERTER);
return getInstanceForClass(value, CONVERTER, Converter.class);
return getInstanceForClass(beanFactory, value, CONVERTER, Converter.class);
}

/**
* Get instance of client if specified. If interceptors are found they are added to client instance as well.
*
* @param attributes the full set of attributes
* @param beanFactory the bean factory to fetch bean instances from
* @param attributes the full set of attributes
* @return client if one could be found from config, empty otherwise
*/
private Optional<Client> getClientIfPossible(Map<String, Object> attributes) {
private Optional<Client> getClientIfPossible(
@Nullable ConfigurableBeanFactory beanFactory,
Map<String, Object> attributes
) {
var value = attributes.get(CLIENT);
var client = getInstanceForClass(value, CLIENT, Client.class);
var client = getInstanceForClass(beanFactory, value, CLIENT, Client.class);
if (client.isEmpty()) return Optional.empty();

var interceptorValues = attributes.get(INTERCEPTORS);
if (!(interceptorValues instanceof Class<?>[] interceptorClasses)) {
throw new IllegalStateException(INTERCEPTORS + " must be an array of classes.");
}
var interceptors = Arrays.stream(interceptorClasses)
.flatMap(clazz -> getInstanceForClass(clazz, INTERCEPTORS, Interceptor.class).stream())
.flatMap(clazz -> getInstanceForClass(beanFactory, clazz, INTERCEPTORS, Interceptor.class).stream())
.toList();
var clientInstance = client.get();
interceptors.forEach(clientInstance::addInterceptor);
Expand All @@ -154,36 +167,50 @@ private Optional<Client> getClientIfPossible(Map<String, Object> attributes) {
/**
* Find and instantiate adapters from specified classes.
*
* @param attributes the attributes from which to get the adapters
* @param beanFactory the bean factory to fetch bean instances from
* @param attributes the attributes from which to get the adapters
* @return list of adapter instances
*/
private List<Object> getAdaptersIfPossible(Map<String, Object> attributes) {
private List<Object> getAdaptersIfPossible(
@Nullable ConfigurableBeanFactory beanFactory,
Map<String, Object> attributes
) {
var values = attributes.get(ADAPTERS);
if (!(values instanceof Class<?>[] classes)) {
throw new IllegalStateException(ADAPTERS + " must be an array of classes.");
}
return Arrays.stream(classes)
.flatMap(clazz -> getInstanceForClass(clazz, ADAPTERS, Object.class).stream())
.flatMap(clazz -> getInstanceForClass(beanFactory, clazz, ADAPTERS, Object.class).stream())
.toList();
}

/**
* Create a new instance of provided class and cast accordingly.
*
* @param beanFactory the bean factory to fetch existing bean from if possible
* @param value the attribute value
* @param name the name of the attribute
* @param targetClass class that needs to be instantiated
* @param <T> the type to return
* @return instance of type if possible, empty if class is not the default value
*/
private <T> Optional<T> getInstanceForClass(Object value, String name, Class<T> targetClass) {
private <T> Optional<T> getInstanceForClass(
@Nullable ConfigurableBeanFactory beanFactory,
Object value,
String name,
Class<T> targetClass
) {
if (!(value instanceof Class<?> candidateClass)) {
throw new IllegalStateException(name + " must be a subclass of " + targetClass.getCanonicalName());
}
var selectedClass = candidateClass.asSubclass(targetClass);
var beanInstance = findBeanInstance(beanFactory, selectedClass);
if (beanInstance.isPresent()) {
return beanInstance.map(selectedClass::cast);
}
if (targetClass.equals(candidateClass)) {
return Optional.empty();
}
var selectedClass = candidateClass.asSubclass(targetClass);
try {
var constructor = selectedClass.getConstructor();
return Optional.of(constructor.newInstance());
Expand All @@ -194,6 +221,21 @@ private <T> Optional<T> getInstanceForClass(Object value, String name, Class<T>
}
}

private <T> Optional<T> findBeanInstance(
@Nullable ConfigurableBeanFactory beanFactory,
Class<? extends T> selectedClass
) {
if (beanFactory == null) {
return Optional.empty();
}

try {
return Optional.of(beanFactory.getBean(selectedClass));
} catch (NoSuchBeanDefinitionException ignored) {
return Optional.empty();
}
}

/**
* Create a scanner that only finds independent interfaces from environment.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,32 +26,33 @@
String url();

/**
* Converter will be automatically instantiated. This requires the provided class to have a public,
* no-args constructor.
* Converter will be automatically instantiated. The class specified needs to be a bean or has to have
* a public no-args constructor.
*
* @return the Converter class to use with this service. Can be left empty if no converter is required.
*/
Class<? extends Converter> converter() default Converter.class;

/**
* Client will be automatically instantiated. This requires the provided client to have a public, no-args
* constructor.
* Client will be automatically instantiated. The class specified needs to be a bean or has to have
* a public no-args constructor.
*
* @return the Client to use with this service. Can be left empty for default client.
*/
Class<? extends Client> client() default Client.class;

/**
* Interceptors will be automatically instantiated. This requires them to have a public, no-args constructor.
* Interceptors will be automatically instantiated. The class specified needs to be a bean or has to have
* a public no-args constructor.
* This will only be honored if a client is provided.
*
* @return the Interceptor classes to use with the client.
*/
Class<? extends Interceptor>[] interceptors() default {};

/**
* Adapters will be automatically instantiated. This requires the provided classes to have a public,
* no-args constructor.
* Adapters will be automatically instantiated. The class specified needs to be a bean or has to have
* a public no-args constructor.
*
* @return the Adapter classes to use with this service. Can be left empty if no adapters are required.
*/
Expand Down

0 comments on commit a86b594

Please sign in to comment.