I'm trying to create system like #Repository.
I have lots of interfaces like:
#Client(uri = "http://example.com", username = "httpBasicUsername", password = "httpBasicPassword")
public interface RequestRepository {
#Request(method = Method.POST, uri = "/mono")
Mono<Response> ex1(Object body);
#Request(method = Method.POST, uri = "/flux")
Flux<Response> ex2(Object body);
}
Right now, I'm creating bean with using this function:
#Bean
public RequestRepository requestRepository(WebClient.Builder builder) {
return (RequestRepository) Proxy.newProxyInstance(
RequestRepository.class.getClassLoader(),
new Class[]{RequestRepository.class},
new MyDynamicInvocationHandler(builder)
);
}
But I have lots of these interfaces. For every new interface I need to create another bean function. But I don't want to do that.
Is there a way to say spring (spring boot) if there is #Client annotation then create bean like this etc?
I've solved with creating custom interface scanner.
For more details: https://stackoverflow.com/a/43651431/6841566
#Target(ElementType.TYPE)
#Retention(RetentionPolicy.RUNTIME)
#Import({InterfaceScanner.class})
public #interface InterfaceScan {
String[] value() default {};
}
public class InterfaceScanner implements ImportBeanDefinitionRegistrar, EnvironmentAware {
private Environment environment;
#Override
public void setEnvironment(Environment environment) {
this.environment = environment;
}
#Override
public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
Map<String, Object> annotationAttributes = metadata.getAnnotationAttributes(InterfaceScan.class.getCanonicalName());
if (annotationAttributes != null) {
String[] basePackages = (String[]) annotationAttributes.get("value");
if (basePackages.length == 0)
basePackages = new String[]{((StandardAnnotationMetadata) metadata)
.getIntrospectedClass().getPackage().getName()};
ClassPathScanningCandidateComponentProvider provider =
new ClassPathScanningCandidateComponentProvider(false, environment) {
#Override
protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) {
AnnotationMetadata meta = beanDefinition.getMetadata();
return meta.isIndependent() && meta.isInterface();
}
};
provider.addIncludeFilter(new AnnotationTypeFilter(Client.class));
for (String basePackage : basePackages)
for (BeanDefinition beanDefinition : provider.findCandidateComponents(basePackage))
registry.registerBeanDefinition(
generateName(beanDefinition.getBeanClassName()),
getProxyBeanDefinition(beanDefinition.getBeanClassName()));
}
}
}
#InterfaceScan
#SpringBootApplication
public class ExampleApplication {
...
}
Related
I want to have enum as a field for my entity.
My application is look like:
Spring boot version
plugins {
id 'org.springframework.boot' version '2.6.2' apply false
repository:
#Repository
public interface MyEntityRepository extends PagingAndSortingRepository<MyEntity, UUID> {
...
entity:
#Table("my_entity")
public class MyEntity{
...
private FileType fileType;
// get + set
}
enum declaration:
public enum FileType {
TYPE_1(1),
TYPE_2(2);
int databaseId;
public static FileType byDatabaseId(Integer databaseId){
return Arrays.stream(values()).findFirst().orElse(null);
}
FileType(int databaseId) {
this.databaseId = databaseId;
}
public int getDatabaseId() {
return databaseId;
}
}
My attempt:
I've found following answer and try to follow it : https://stackoverflow.com/a/53296199/2674303
So I've added bean
#Bean
public JdbcCustomConversions jdbcCustomConversions() {
return new JdbcCustomConversions(asList(new DatabaseIdToFileTypeConverter(), new FileTypeToDatabaseIdConverter()));
}
converters:
#WritingConverter
public class FileTypeToDatabaseIdConverter implements Converter<FileType, Integer> {
#Override
public Integer convert(FileType source) {
return source.getDatabaseId();
}
}
#ReadingConverter
public class DatabaseIdToFileTypeConverter implements Converter<Integer, FileType> {
#Override
public FileType convert(Integer databaseId) {
return FileType.byDatabaseId(databaseId);
}
}
But I see error:
The bean 'jdbcCustomConversions', defined in class path resource
[org/springframework/boot/autoconfigure/data/jdbc/JdbcRepositoriesAutoConfiguration$SpringBootJdbcConfiguration.class],
could not be registered. A bean with that name has already been
defined in my.pack.Main and overriding is disabled.
I've tried to rename method jdbcCustomConversions() to myJdbcCustomConversions(). It helped to avoid error above but converter is not invoked during entity persistence and I see another error that application tries to save String but database type is bigint.
20:39:10.689 DEBUG [main] o.s.jdbc.core.StatementCreatorUtils: JDBC getParameterType call failed - using fallback method instead: org.postgresql.util.PSQLException: ERROR: column "file_type" is of type bigint but expression is of type character varying
Hint: You will need to rewrite or cast the expression.
Position: 174
I also tried to use the latest(currently) version of spring boot:
id 'org.springframework.boot' version '2.6.2' apply false
But it didn't help.
What have I missed ?
How can I map enum to integer column properly ?
P.S.
I use following code for testing:
#SpringBootApplication
#EnableJdbcAuditing
#EnableScheduling
public class Main {
public static void main(String[] args) {
ConfigurableApplicationContext applicationContext = SpringApplication.run(Main.class, args);
MyEntityRepositoryrepository = applicationContext.getBean(MyEntityRepository.class);
MyEntity entity = new MyEntity();
...
entity.setFileType(FileType.TYPE_2);
repository.save(entity);
}
#Bean
public ModelMapper modelMapper() {
ModelMapper mapper = new ModelMapper();
mapper.getConfiguration()
.setMatchingStrategy(MatchingStrategies.STRICT)
.setFieldMatchingEnabled(true)
.setSkipNullEnabled(true)
.setFieldAccessLevel(PRIVATE);
return mapper;
}
#Bean
public AbstractJdbcConfiguration jdbcConfiguration() {
return new MySpringBootJdbcConfiguration();
}
#Configuration
static class MySpringBootJdbcConfiguration extends AbstractJdbcConfiguration {
#Override
protected List<?> userConverters() {
return asList(new DatabaseIdToFileTypeConverter(), new FileTypeToDatabaseIdConverter());
}
}
}
UPDATE
My code is:
#SpringBootApplication
#EnableJdbcAuditing
#EnableScheduling
public class Main {
public static void main(String[] args) {
ConfigurableApplicationContext applicationContext = SpringApplication.run(Main.class, args);
MyEntityRepositoryrepository = applicationContext.getBean(MyEntityRepository.class);
MyEntity entity = new MyEntity();
...
entity.setFileType(FileType.TYPE_2);
repository.save(entity);
}
#Bean
public ModelMapper modelMapper() {
ModelMapper mapper = new ModelMapper();
mapper.getConfiguration()
.setMatchingStrategy(MatchingStrategies.STRICT)
.setFieldMatchingEnabled(true)
.setSkipNullEnabled(true)
.setFieldAccessLevel(PRIVATE);
return mapper;
}
#Bean
public AbstractJdbcConfiguration jdbcConfiguration() {
return new MySpringBootJdbcConfiguration();
}
#Configuration
static class MySpringBootJdbcConfiguration extends AbstractJdbcConfiguration {
#Override
protected List<?> userConverters() {
return asList(new DatabaseIdToFileTypeConverter(), new FileTypeToDatabaseIdConverter());
}
#Bean
public JdbcConverter jdbcConverter(JdbcMappingContext mappingContext,
NamedParameterJdbcOperations operations,
#Lazy RelationResolver relationResolver,
JdbcCustomConversions conversions,
Dialect dialect) {
JdbcArrayColumns arrayColumns = dialect instanceof JdbcDialect ? ((JdbcDialect) dialect).getArraySupport()
: JdbcArrayColumns.DefaultSupport.INSTANCE;
DefaultJdbcTypeFactory jdbcTypeFactory = new DefaultJdbcTypeFactory(operations.getJdbcOperations(),
arrayColumns);
return new MyJdbcConverter(
mappingContext,
relationResolver,
conversions,
jdbcTypeFactory,
dialect.getIdentifierProcessing()
);
}
}
static class MyJdbcConverter extends BasicJdbcConverter {
MyJdbcConverter(
MappingContext<? extends RelationalPersistentEntity<?>, ? extends RelationalPersistentProperty> context,
RelationResolver relationResolver,
CustomConversions conversions,
JdbcTypeFactory typeFactory,
IdentifierProcessing identifierProcessing) {
super(context, relationResolver, conversions, typeFactory, identifierProcessing);
}
#Override
public int getSqlType(RelationalPersistentProperty property) {
if (FileType.class.equals(property.getActualType())) {
return Types.BIGINT;
} else {
return super.getSqlType(property);
}
}
#Override
public Class<?> getColumnType(RelationalPersistentProperty property) {
if (FileType.class.equals(property.getActualType())) {
return Long.class;
} else {
return super.getColumnType(property);
}
}
}
}
But I experience error:
Caused by: org.postgresql.util.PSQLException: Cannot convert an instance of java.lang.String to type long
at org.postgresql.jdbc.PgPreparedStatement.cannotCastException(PgPreparedStatement.java:925)
at org.postgresql.jdbc.PgPreparedStatement.castToLong(PgPreparedStatement.java:810)
at org.postgresql.jdbc.PgPreparedStatement.setObject(PgPreparedStatement.java:561)
at org.postgresql.jdbc.PgPreparedStatement.setObject(PgPreparedStatement.java:931)
at com.zaxxer.hikari.pool.HikariProxyPreparedStatement.setObject(HikariProxyPreparedStatement.java)
at org.springframework.jdbc.core.StatementCreatorUtils.setValue(StatementCreatorUtils.java:414)
at org.springframework.jdbc.core.StatementCreatorUtils.setParameterValueInternal(StatementCreatorUtils.java:231)
at org.springframework.jdbc.core.StatementCreatorUtils.setParameterValue(StatementCreatorUtils.java:146)
at org.springframework.jdbc.core.PreparedStatementCreatorFactory$PreparedStatementCreatorImpl.setValues(PreparedStatementCreatorFactory.java:283)
at org.springframework.jdbc.core.PreparedStatementCreatorFactory$PreparedStatementCreatorImpl.createPreparedStatement(PreparedStatementCreatorFactory.java:241)
at org.springframework.jdbc.core.JdbcTemplate.execute(JdbcTemplate.java:649)
... 50 more
Caused by: java.lang.NumberFormatException: For input string: "TYPE_2"
at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
at java.lang.Long.parseLong(Long.java:589)
at java.lang.Long.parseLong(Long.java:631)
at org.postgresql.jdbc.PgPreparedStatement.castToLong(PgPreparedStatement.java:792)
... 59 more
Try the following instead:
#Bean
public AbstractJdbcConfiguration jdbcConfiguration() {
return new MySpringBootJdbcConfiguration();
}
#Configuration
static class MySpringBootJdbcConfiguration extends AbstractJdbcConfiguration {
#Override
protected List<?> userConverters() {
return List.of(new DatabaseIdToFileTypeConverter(), new FileTypeToDatabaseIdConverter());
}
}
Explanation:
Spring complains that JdbcCustomConversions in auto-configuration class is already defined (by your bean) and you don't have bean overriding enabled.
JdbcRepositoriesAutoConfiguration has changed a few times, in Spring 2.6.2 it has:
#Configuration(proxyBeanMethods = false)
#ConditionalOnMissingBean(AbstractJdbcConfiguration.class)
static class SpringBootJdbcConfiguration extends AbstractJdbcConfiguration {
}
In turn, AbstractJdbcConfiguration has:
#Bean
public JdbcCustomConversions jdbcCustomConversions() {
try {
Dialect dialect = applicationContext.getBean(Dialect.class);
SimpleTypeHolder simpleTypeHolder = dialect.simpleTypes().isEmpty() ? JdbcSimpleTypes.HOLDER
: new SimpleTypeHolder(dialect.simpleTypes(), JdbcSimpleTypes.HOLDER);
return new JdbcCustomConversions(
CustomConversions.StoreConversions.of(simpleTypeHolder, storeConverters(dialect)), userConverters());
} catch (NoSuchBeanDefinitionException exception) {
LOG.warn("No dialect found. CustomConversions will be configured without dialect specific conversions.");
return new JdbcCustomConversions();
}
}
As you can see, JdbcCustomConversions is not conditional in any way, so defining your own caused a conflict. Fortunately, it provides an extension point userConverters() which can be overriden to provide your own converters.
Update
As discussed in comments:
FileType.byDatabaseId is broken - it ignores its input param
as the column type in db is BIGINT, your converters must convert from Long, not from Integer, this addresses read queries
for writes, there is an open bug https://github.com/spring-projects/spring-data-jdbc/issues/629 There is a hardcoded assumption that Enums are converted to Strings, and only Enum -> String converters are checked.
As we want to convert to Long, we need to make amendments to BasicJdbcConverter by subclassing it and registering subclassed converter with as a #Bean.
You need to override two methods
public int getSqlType(RelationalPersistentProperty property)
public Class<?> getColumnType(RelationalPersistentProperty property)
I hardcoded the Enum type and corresponding column types, but you may want to get more fancy with that.
#Bean
public AbstractJdbcConfiguration jdbcConfiguration() {
return new MySpringBootJdbcConfiguration();
}
#Configuration
static class MySpringBootJdbcConfiguration extends AbstractJdbcConfiguration {
#Override
protected List<?> userConverters() {
return List.of(new DatabaseIdToFileTypeConverter(), new FileTypeToDatabaseIdConverter());
}
#Bean
public JdbcConverter jdbcConverter(JdbcMappingContext mappingContext,
NamedParameterJdbcOperations operations,
#Lazy RelationResolver relationResolver,
JdbcCustomConversions conversions,
Dialect dialect) {
JdbcArrayColumns arrayColumns = dialect instanceof JdbcDialect ? ((JdbcDialect) dialect).getArraySupport()
: JdbcArrayColumns.DefaultSupport.INSTANCE;
DefaultJdbcTypeFactory jdbcTypeFactory = new DefaultJdbcTypeFactory(operations.getJdbcOperations(),
arrayColumns);
return new MyJdbcConverter(
mappingContext,
relationResolver,
conversions,
jdbcTypeFactory,
dialect.getIdentifierProcessing()
);
}
}
static class MyJdbcConverter extends BasicJdbcConverter {
MyJdbcConverter(
MappingContext<? extends RelationalPersistentEntity<?>, ? extends RelationalPersistentProperty> context,
RelationResolver relationResolver,
CustomConversions conversions,
JdbcTypeFactory typeFactory,
IdentifierProcessing identifierProcessing) {
super(context, relationResolver, conversions, typeFactory, identifierProcessing);
}
#Override
public int getSqlType(RelationalPersistentProperty property) {
if (FileType.class.equals(property.getActualType())) {
return Types.BIGINT;
} else {
return super.getSqlType(property);
}
}
#Override
public Class<?> getColumnType(RelationalPersistentProperty property) {
if (FileType.class.equals(property.getActualType())) {
return Long.class;
} else {
return super.getColumnType(property);
}
}
}
I have a config and it's #Import-ed by an annotation. I want the values on the annotation to be accessible by the config. Is this possible?
Config:
#Configuration
public class MyConfig
{
#Bean
public CacheManager cacheManager(net.sf.ehcache.CacheManager cacheManager)
{
//Get the values in here
return new EhCacheCacheManager(cacheManager);
}
#Bean
public EhCacheManagerFactoryBean ehcache() {
EhCacheManagerFactoryBean ehCacheManagerFactoryBean = new EhCacheManagerFactoryBean();
ehCacheManagerFactoryBean.setShared(true);
return ehCacheManagerFactoryBean;
}
}
The annotation
#Retention(RetentionPolicy.RUNTIME)
#Target(ElementType.TYPE)
#Import(MyConfig.class)
public #interface EnableMyCaches
{
String value() default "";
String cacheName() default "my-cache";
}
How would I get the value passed below in my config?
#SpringBootApplication
#EnableMyCaches(cacheName = "the-cache")
public class MyServiceApplication {
public static void main(String[] args) {
SpringApplication.run(MyServiceApplication.class, args);
}
}
Use simple Java reflection:
Class c = MyServiceApplication.getClass();
EnableMyCaches enableMyCaches = c.getAnnotation(EnableMyCaches.class);
String value = enableMyCaches.value();
Consider how things like #EnableConfigurationProperties are implemented.
The annotation has #Import(EnableConfigurationPropertiesImportSelector.class) which then imports ImportBeanDefinitionRegistrars. These registrars are passed annotation metadata:
public interface ImportBeanDefinitionRegistrar {
public void registerBeanDefinitions(
AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry);
}
You can then get annotation attributes from annotation metadata:
MultiValueMap<String, Object> attributes = metadata
.getAllAnnotationAttributes(
EnableMyCaches.class.getName(), false);
attributes.get("cacheName");
I am porting XML Spring configuration to JavaConfig.
My bean definition with interceptor looks like:
#Autowired
private MyService myServiceImpl;
#Bean
MyService myService() {
final ProxyFactoryBean proxy = new ProxyFactoryBean();
final Class<?>[] proxyInterfaces = { MyService.class };
proxy.setProxyInterfaces(proxyInterfaces);
proxy.setTarget(this.myServiceImpl);
final String[] interceptorNames = { "myInterceptor" };
proxy.setInterceptorNames(interceptorNames);
return (MyService) proxy.getObject();
}
, where "myInterceptor" name is not validated at compile-time.
Is there a better way to configure interceptors using JavaConfig?
A better way to configure interceptors:
#Configuration
public class MyServiceConfig {
#Autowired
private BeanFactory beanFactory;
#Autowired
private IMyService myService;
#Autowired
private MyInterceptor myInterceptor;
#Bean
public IMyService myServiceIntercepted() {
final Class<?>[] proxyInterfaces = { IMyService.class };
final Advice[] advices = { this.myInterceptor };
return createProxy(proxyInterfaces, this.myService, this.beanFactory,
advices);
}
<T> T createProxy(final Class<?>[] proxyInterfaces, final T target,
final BeanFactory beanFactory) {
final ProxyFactoryBean proxy =
createProxyReturnFactoryBean(proxyInterfaces, target, beanFactory);
return (T) proxy.getObject();
}
<T> ProxyFactoryBean createProxyReturnFactoryBean(
final Class<?>[] proxyInterfaces, final T target, final BeanFactory beanFactory) {
final ProxyFactoryBean proxy = new ProxyFactoryBean();
proxy.setBeanFactory(beanFactory);
if (proxyInterfaces != null) {
proxy.setProxyInterfaces(proxyInterfaces);
}
proxy.setTarget(target);
return proxy;
}
<T> T createProxy(final Class<?>[] proxyInterfaces, final T target,
final BeanFactory beanFactory, final Advice[] advices) {
final ProxyFactoryBean proxy =
createProxyReturnFactoryBean(proxyInterfaces, target, beanFactory);
for (final Advice advice : advices) {
proxy.addAdvice(advice);
}
return (T) proxy.getObject();
}
}
We're using Custom Qualifier Annotations to create and inject beans. How can we select a bean dynamically at runtime by just specifying the Custom Qualifiers.
Custom Qualifier :
#Target({ ElementType.FIELD, ElementType.METHOD, ElementType.TYPE,
ElementType.PARAMETER })
#Retention(RetentionPolicy.RUNTIME)
#Qualifier
public #interface PlatformQualifiers {
public static enum OperatingSystems {
IOS, ANDROID
}
OperatingSystems operatingSystem() default OperatingSystems.IOS;
public enum DeviceTypes {
Mobile, Tablet, ANY, Other
}
DeviceTypes[] deviceType() default { DeviceTypes.ANY };
}
Bean Interface :
#FunctionalInterface
public interface Platform {
String getDeviceDetails();
}
Bean Configs :
#Configuration
public class PlatformConfig {
#Bean
#PlatformQualifiers(operatingSystem = OperatingSystems.IOS, deviceType = DeviceTypes.Mobile)
public Platform getIphone6() {
return () -> "iphone6";
}
#Bean
#PlatformQualifiers(operatingSystem = OperatingSystems.IOS, deviceType = DeviceTypes.Tablet)
public Platform getIpad() {
return () -> "ipad3";
}
#Bean
#PlatformQualifiers(operatingSystem = OperatingSystems.ANDROID, deviceType = DeviceTypes.Mobile)
public Platform getAndroidPhone() {
return () -> "AndroidPhoneSamsung";
}
}
Current Application Code :
#Configuration
#ComponentScan
public class MainApplication {
#Autowired
#PlatformQualifiers(operatingSystem = OperatingSystems.IOS, deviceType = DeviceTypes.Mobile)
Platform iphone;
#Autowired
#PlatformQualifiers(operatingSystem = OperatingSystems.ANDROID, deviceType = DeviceTypes.Mobile)
Platform androidPhone;
public void getDevice(String osType, String deviceType ) {
if(osType == "ios" && deviceType == "mobile") {
System.out.println(iphone.getDeviceDetails());
}
if(osType == "android" && deviceType == "mobile") {
System.out.println(androidPhone.getDeviceDetails());
}
}
public static void main(String[] args) {
ApplicationContext context = new AnnotationConfigApplicationContext(MainApplication.class);
MainApplication mainApplication = context.getBean(MainApplication.class);
mainApplication.getDevice("ios" ,"mobile");
mainApplication.getDevice("android" , "mobile");
}
}
I am looking for a solution like where on runtime I can access bean using the qualifiers, something like this :
#Configuration
#ComponentScan
public class MainApplication2 {
#Autowired
ApplicationContext context;
public void getDevice(DeviceTypes deviceType, OperatingSystems osType ) {
>>>>>>>>>>> Looking of something of type following :
Platform p = context.getBean(some input consisting to identify bean by deviceType and osType)
System.out.println(p.getDeviceDetails());
}
public static void main(String[] args) {
ApplicationContext context = new AnnotationConfigApplicationContext(MainApplication2.class);
MainApplication2 application = context.getBean(MainApplication2.class);
application.getDevice(DeviceTypes.Mobile, OperatingSystems.ANDROID);
}
}
In this case, How can I get a bean from applicationContext on runtime based on the DeviceTypes and OperatingSystems ?
One of the approaches could be to create a Map of Platform beans and inject it into the bean which calls getDevice. The key for the map could be device type.
Another approach along the same lines could be to make your bean implement InitializingBean and ApplicationContextAware and use 'getBeansWithAnnotation' in 'afterPropertiesSetin conjuction withfindAnnotationOnBean` to populate the Map or lookup the bean dynamically.
I have AnnotationConfigApplicationContext created with #Configuration annotated class as a param:
#Configuration
class Config {
#Bean
public LocalValidatorFactoryBean localValidatorFactoryBean() {
return new LocalValidatorFactoryBean();
}
#Bean
public A aBean() {
return new A();
}
#Bean
public B aBean() {
return new B();
}
}
Where A and B are:
class A {
#Min(1)
public int myInt;
}
class B {
#Autowire(required = true)
#Valid
public A aBean;
}
Q: Is it possible to make Spring to process #Valid annotation in this case?
PS: Currently I have following working implementation of B:
class B {
public A aBean;
public void setABean(A aBean, Validator validator) {
if (validator.validate(aBean).size() > 0) {
throw new ValidationException();
}
this.aBean = aBean;
}
}
This impl seems a bit clumsy to me and I want to replace it. Please help :)
It looks like you want to validate your bean in the process of injection.
You can read
here.
Here is an example:
public class BeanValidator implements org.springframework.validation.Validator, InitializingBean {
private Validator validator;
public void afterPropertiesSet() throws Exception {
ValidatorFactory validatorFactory = Validation.buildDefaultValidatorFactory();
validator = validatorFactory.usingContext().getValidator();
}
public boolean supports(Class clazz) {
return true;
}
public void validate(Object target, Errors errors) {
Set<ConstraintViolation<Object>> constraintViolations = validator.validate(target);
for (ConstraintViolation<Object> constraintViolation : constraintViolations) {
String propertyPath = constraintViolation.getPropertyPath().toString();
String message = constraintViolation.getMessage();
errors.rejectValue(propertyPath, "", message);
}
}
}
You will need to implement InitializingBean, to be able to validate the bean after it was set.