Oleg Atamanenko
Продолжая разговор о JAX RS Client API - предположим, что мы уже используем JAX-RS клиент
У нас есть класс, который умеет создавать прокси для любого REST-интерфейса в проекте. Теперь мы хотим сделать так, чтобы эти интерфейсы можно было автоматически создавать в контексте Spring и связывать с другими бинами.
Первое решение, которое приходит в лоб - объявить бин в конфигурации для каждого интерфейса:
@Configuration
public class SpringConfiguration {
@Bean
public UserRest userRest(){
List<?> providers = Arrays.asList(new JsonMessageHandler(new ObjectMapper()));
return JAXRSClientFactory.create("http://localhost:8080", UserRest.class, providers);
}
}
Написав объявление нескольких таких бинов можно задуматься - “Есть ли способ проще?”.
Есть, и я вам сейчас его покажу.
Для того, чтобы добавлять собственные бины в Spring контекст, мы воспользуемся возможностью расширения Spring контекста с использованием BeanFactoryPostProcessor
.
Метод postProcessBeanFactory
данного класса позволяет выполнить дополнительную обработку фабрики бинов Spring, например, удалить, добавить, переопределить бин.
Это именно то, что нам нужно - автоматически добавить новые бины в фабрику.
Итак, что нам нужно сделать:
JAXRSClientFactory
.По спецификации JAX-RS, чтобы класс распознавался как REST-ресурс, у него должна быть аннотация @Path()
.
Для поиска всех возможных классов/интерфейсов воспользуемся классом ClassPathScanningCandidateComponentProvider
, который умеет сканировать классы в выбранном пакете и применять фильтры, чтобы собрать только нужные. Также нам нужно учесть, что по умолчанию ClassPathScanningCandidateComponentProvider
пытается определить, может ли найденный класс быть и бином (например, проверяет, что это не абстрактный класс и не интерфейс), поэтому нам нужно написать подкласс, который позволит нам работать с интерфейсами.
Пишем класс:
public class RestClientPostProcessor implements BeanFactoryPostProcessor {
private static final Logger LOGGER = LoggerFactory.getLogger(RestClientPostProcessor.class);
protected String endpoint = "http://localhost:8080";
private Class<? extends Annotation> requiredAnnotation = Path.class;
private String basePackage = "<base package>";
private ObjectMapper objectMapper;
// getters/setters omitted.
protected Object createBean(Class<?> clazz) {
List<?> providers = Arrays.asList(new JsonMessageHandler(objectMapper));
return JAXRSClientFactory.create(endpoint, clazz, providers);
}
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
ClassPathScanningCandidateComponentProvider provider = new ClasspathScanner();
provider.addIncludeFilter(new AnnotationTypeFilter(requiredAnnotation));
Set<BeanDefinition> components = provider.findCandidateComponents(basePackage);
for (BeanDefinition component : components) {
createAndRegisterBean(beanFactory, component);
}
}
protected void createAndRegisterBean(ConfigurableListableBeanFactory beanFactory, BeanDefinition component) {
try {
String beanClassName = component.getBeanClassName();
Class<?> clazz = Class.forName(beanClassName);
Object o = createBean(clazz);
beanFactory.registerResolvableDependency(clazz, o);
} catch (ClassNotFoundException e) {
LOGGER.warn("Unable to find class: {}", component.getBeanClassName(), e);
}
}
private static class ClasspathScanner extends ClassPathScanningCandidateComponentProvider {
public ClasspathScanner() {
super(false);
}
@Override protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) {
// override this method, because our classes are interfaces, by default interfaces are
// not allowed.
return beanDefinition.getMetadata().isIndependent();
}
}
}
Теперь этот процессор необходимо зарегистрировать в контексте Spring:
@Configuration
class TestAutowiringConfiguration {
@Bean
public static RestClientPostProcessor autowiringRestApiProcessor() {
return new RestClientPostProcessor();
}
}
И теперь можно использовать UserRest
как обычный бин:
@Autowired
private UserShowRest userShowRest;