Use configuration properties in Spring expression language - spring

I have a class with configuration properties:
#Data
#ConfigurationProperties("message-starter")
public class JobProperties {
public Duration kafkaSendingPeriod;
public Duration rabbitSendingPeriod;
}
And class that aggregates this properties and uses them in #Scheduled annotation using Spring expression language:
#Component
#EnableConfigurationProperties(JobProperties.class)
#RequiredArgsConstructor
public class Job {
public final JobProperties jobProperties;
#Scheduled(fixedRateString="#{#jobProperties.getKafkaSendingPeriod()}")
public void fun(){
System.out.println("Time: " + LocalDateTime.now());
}
}
But when I run it I get the following exception:
Description:
A component required a bean named 'jobProperties' that could not be found.
Action:
Consider defining a bean named 'jobProperties' in your configuration.
Please tell me how to fix this exception so that I can get the value of the fields of the JobProperties class?

Related

Custom YML configuration file String conversion Enum

In Spring Boot project, the configuration in the YML file can be automatically converted to an #ConfigurationProperties annotated bean, I found from the official documents and source code in ApplicationConversionService#addApplicationConverters() method to add A default LenientStringToEnumConverterFactory to handle all the String conversion to Enum, it through the Enum.valueOf() implementation,But I want to use other rules to turn strings into examples of enum,Just like the fromAlias() method below,
#ConfigurationProperties(prefix = "head")
#Component
#Data
public class HeadProperties {
private PayType payType;
private Integer cast;
#Getter
#RequiredArgsConstructor
enum PayType {
GOLD("GOLD", "金币"), DIAMOND("DIAMOND", "钻石"), VIP_FREE("VIP_FREE", "会员免费");
private final String val;
private final String alias;
static PayType fromAlias(String alias) {
return Arrays.stream(values())
.filter(type -> alias.equals(type.getAlias()))
.findAny()
.orElse(null);
}
}
}
The following is the YML file configuration
head:
payType: "金币"
cast: 10
I don't know where the entry is, so I get an error as soon as the program runs
code:
#SpringBootApplication
#Slf4j
public class DemoApplication{
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
#Bean
ApplicationRunner runner(HeadProperties headConfig) {
return arg -> log.info("head config:{}", headConfig);
}
}
the following is error message:
APPLICATION FAILED TO START
***************************
Description:
Failed to bind properties under 'head.pay-type' to com.example.demo.HeadProperties$PayType:
Property: head.pay-type
Value: 金币
Origin: class path resource [application.yml] - 2:12
Reason: failed to convert java.lang.String to com.example.demo.HeadProperties$PayType (caused by java.lang.IllegalArgumentException: No enum constant com.example.demo.HeadProperties.PayType.金币)
Action:
Update your application's configuration. The following values are valid:
DIAMOND
GOLD
VIP_FREE
I tried injecting various converters like the one below into the container,but it still didn't work.
#Component
public class PayTypeConverter implements Converter<String, HeadProperties.PayType> {
#Override
public HeadProperties.PayType convert(String source) {
return HeadProperties.PayType.fromAlias(source);
}
}
#Component
public class PayTypeConverter implements Converter<String, Enum<HeadProperties.PayType>> {
#Override
public Enum<HeadProperties.PayType> convert(String source) {
return HeadProperties.PayType.fromAlias(source);
}
}
How can this requirement be fulfilled?
The converters that are used for #ConfigurationProperties binding need a special qualifier that tells Spring that they are to be used for that purpose.
An annotation exists for this- #ConfigurationPropertiesBinding. The Javadoc is as follows:
Qualifier for beans that are needed to configure the binding of #ConfigurationProperties (e.g. Converters).
So all that's needed is to add that annotation to your converter, then Spring will use it during the binding process:
#Component
#ConfigurationPropertiesBinding
public class PayTypeConverter implements Converter<String, HeadProperties.PayType> {
#Override
public HeadProperties.PayType convert(String source) {
return HeadProperties.PayType.fromAlias(source);
}
}
That then produces the expected output:
head config:HeadProperties(payType=GOLD, cast=10)
And a minor note, when writing custom converters, be aware that returning null will not trigger an error (assuming there are no other measures configured to prevent that). That means that unlike the out-of-the-box enum converter, your custom one does not produce an error if no matching enum can be found. You can remedy this by instead throwing an exception instead of returning null.

Why #Configuration class required for #ConditionalOnEnabledEndpoint annotation?

I'm using Spring Boot 2.1.6 version. Can't we use the #ConditionalOnEnabledEndpoint annotation with #Component? I get
Caused by: java.lang.IllegalStateException: OnEnabledEndpointCondition must be be used on #Bean methods when the endpoint is not specified
I tried a class like below
#Component
#Endpoint(id="customendpoint")
#ConditionalOnEnabledEndpoint
class MyCustomEndpoint {
#ReadOperation
public String method1() { ....... }
}
#ConditionalOnEnabledEndpoint is working when I create a configuration class and #Bean method.
Why Configuration class is required ?
Is there any other way to stop the component exposing the actuator endpoint with out configuration class.
You have to set the endpoint attribute, see ConditionalOnEnabledEndpoint#endpoint
public abstract Class<?> endpoint
The endpoint type that should be checked. Inferred when the return type of the #Bean method is either an Endpoint or an EndpointExtension.
Returns:
the endpoint type to check
Since:
2.0.6
Default:
java.lang.Void.class
Your modified code:
#Component
#Endpoint(id="customendpoint")
#ConditionalOnEnabledEndpoint(endpoint = MyCustomEndpoint.class)
class MyCustomEndpoint {
#ReadOperation
public String method1() { ....... }
}

ConstraintValidator dependency injection leads to ValidationException when being validated at class level

I've encountered an unexpected behaviour when using dependency injection in a ConstraintValidator which is getting evaluated at class level.
Entity class:
#Entity
#ValidDemoEntity
public class DemoEntity {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
}
Validation annotation:
#Target({ElementType.TYPE})
#Retention(RetentionPolicy.RUNTIME)
#Constraint(validatedBy = {DemoEntityValidator.class})
public #interface ValidDemoEntity {
String message() default "{some.demo.validator.message}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
Validator:
public class DemoEntityValidator implements ConstraintValidator<ValidDemoEntity, DemoEntity> {
private DemoEntityRepository demoEntityRepository;
public DemoEntityValidator(DemoEntityRepository demoEntityRepository) {
this.demoEntityRepository = demoEntityRepository;
}
#Override
public void initialize(ValidDemoEntity constraintAnnotation) {
}
#Override
public boolean isValid(DemoEntity demoEntity, ConstraintValidatorContext constraintValidatorContext) {
return true;
}
}
Test class:
#SpringBootTest
public class ValidatorInstantiationTest {
private Validator validator;
#Before
public void setUp() throws Exception {
ValidatorFactory validatorFactory = Validation.buildDefaultValidatorFactory();
validator = validatorFactory.getValidator();
}
#Test
public void shouldInitiateAndCallDemoEntityValidator() {
DemoEntity demoEntity = new DemoEntity();
validator.validate(demoEntity);
}
}
Validating the entity leads to:
javax.validation.ValidationException: HV000064: Unable to instantiate ConstraintValidator: com.example.demo.DemoEntityValidator.
and further down the stack trace:
Caused by: java.lang.NoSuchMethodException: com.example.demo.DemoEntityValidator.<init>()
which indicates that Hibernate tried to initiate the the class instead of letting Spring take care of that.
The strange thing about this is that dependency injection works fine for validations applied on field level.
The code is available at GitHub.
The exception says that there is no default constructor because Hibernate Validator tries to instantiate your validator.
You have to use Spring.
1 Make your validator a Spring Bean:
#Component
public class DemoEntityValidator implements ConstraintValidator<ValidDemoEntity, DemoEntity> {
2 Inject the Spring provided validator and use the SpringRunner for executing your tests:
#SpringBootTest
#RunWith(SpringRunner.class)
public class ValidatorInstantiationTest {
#Autowired
private Validator validator;
#Test
public void shouldInitiateAndCallDemoEntityValidator() {
DemoEntity demoEntity = new DemoEntity();
validator.validate(demoEntity);
}
}
1 Make your validator a Spring Bean
This site states:
The Spring framework automatically detects all classes which implement the ConstraintValidator interface. The framework instantiates them and wires all dependencies like the class was a regular Spring bean.
Which clearly works for validations applied on field level.
Nevertheless I've updated the code.
DemoEntityValidator is now a Spring component:
#Component
public class DemoEntityValidator implements ConstraintValidator<ValidDemoEntity, DemoEntity>
I've changed the test to:
#SpringBootTest
#RunWith(SpringRunner.class)
public class ValidatorInstantiationTest {
#Autowired
private DemoEntityRepository demoEntityRepository;
#Test
public void shouldInitiateAndCallDemoEntityValidator() {
DemoEntity demoEntity = new DemoEntity();
demoEntityRepository.save(demoEntity);
}
}
To make the usecase clearer, but the test still leads to the same exception.
Adding an empty constructor to the class DemoEntityValidator disables the error.
I think you answer is here:
https://docs.spring.io/spring/docs/current/spring-framework-reference/core.html#validation-beanvalidation-spring
You need to declare a LocalValidatorFactoryBean in your configuration class and it will just work.
From the documentation:
By default, the LocalValidatorFactoryBean configures a
SpringConstraintValidatorFactory that uses Spring to create
ConstraintValidator instances. This lets your custom
ConstraintValidators benefit from dependency injection like any other
Spring bean.
And an example from the same place:
import javax.validation.ConstraintValidator;
public class MyConstraintValidator implements ConstraintValidator {
#Autowired;
private Foo aDependency;
...
}
And this is how I declared that bean in a #Configuration annotated class:
/**
* Provides auto-wiring capabilities for validators Checkout: https://docs.spring.io/spring/docs/current/spring-framework-reference/core.html#validation-beanvalidation-spring
*/
#Bean
public LocalValidatorFactoryBean validatorFactoryBean() {
LocalValidatorFactoryBean bean = new LocalValidatorFactoryBean();
bean.setValidationMessageSource(validationMessageSource());
return bean;
}
There's nothing wrong with your validator class. I got this working by making two changes to the test configuration:
1. Run test with Spring
In order to have Spring manage your beans, you need to run your test with a test runner that sets up Spring. You can specify the test runner class using junit's #RunWith-annotation:
#RunWith(SpringRunner.class)
#SpringBootTest
public class ValidatorInstantiationTest { ... }
2. Inject a Spring managed validator bean
Since you're using Spring Boot, you can inject a Spring managed validator – it's already configured. This way, Spring will handle the initiation of your DemoEntityValidator.
#RunWith(SpringRunner.class)
#SpringBootTest
public class ValidatorInstantiationTest {
#Autowired
private Validator validator;
...
}
This is all that is needed. You should not annotate your DemoEntityValidator with #Component or similar.
Note that you need to provide Spring with a data source, since SpringRunner will set up a context based on your Spring Boot setup (I'm guessing it includes spring-boot-starter-data-jpa in your case). The easiest way to get going is just to put an in-memory DB such as h2 on the classpath.

Cannot resolve bean in SpEL for Spring Data MongoDB collection name

I am trying to customize the collection name where an entity class is saved into and indexed, using Spring Data MongoDB and Spring Batch. The class is declared as follows:
#Document
#CompoundIndex(name = "unique_source", def = "{'fid': 1, 'sid': 1}", unique = true, background = true)
public class VariantSource {
...
}
And the item writer:
public class VariantSourceMongoWriter extends MongoItemWriter<VariantSource> {
public VariantSourceEntityMongoWriter(MongoOperations mongoOperations, String collectionName) {
setTemplate(mongoOperations);
setCollection(collectionName);
}
}
Saving works fine: the objects are written into the collection provided as argument. The problem is that the indexes are created in the default collection, named after the class name (variantSource).
After reading this and this, I created the following:
public class MongoCollections {
public String getCollectionFilesName() {
return "my_custom_collection_name"; // TODO Dynamic value
}
}
#Configuration
public class MongoCollectionsConfiguration {
#Bean
public MongoCollections mongoCollections() {
return new MongoCollections();
}
#RunWith(SpringRunner.class)
#ContextConfiguration(classes = {MongoCollectionsConfiguration.class})
public class VariantSourceMongoWriterTest {
#Autowired
private MongoCollections mongoCollections;
}
I have checked the instance is correctly autowired into the unit tests, but I can't make it work with SpEL.
After changing the #Document annotation to look like this:
#Document(collection = "#{#mongoCollections.getCollectionFilesName()}")
the following exception is thrown:
org.springframework.expression.spel.SpelEvaluationException: EL1057E:(pos 1): No bean resolver registered in the context to resolve access to bean 'mongoCollections'
And if I use this:
#Document(collection = "#{mongoCollections.getCollectionFilesName()}")
the exception is this one:
org.springframework.expression.spel.SpelEvaluationException: EL1007E:(pos 0): Property or field 'mongoCollections' cannot be found on null
Finally, the following creates a collection with the name as specified, symbols included:
#Document(collection = "#mongoCollections.getCollectionFilesName()")
As pointed by this answer, to fix the injection:
#Document(collection = "#{mongoCollections.getCollectionFilesName()}")
SpelEvaluationException: EL1007E:(pos 0): Property or field 'mongoCollections' cannot be found on null
(or a direct method bean: #Document(collection = "#{getCollectionFilesName}")), try setting the ApplicationContext into the MongoMappingContext (which is used to instantiate the MongoConverter, and later the MongoTemplate):
#Bean
public MongoMappingContext MongoMappingContext() {
MongoMappingContext mappingContext = new MongoMappingContext();
mappingContext.setApplicationContext(applicationContext);
return mappingContext;
}
Make sure that your bean mongoCollections is registered in the application context,
and also correct the SpEL expression as below.
#Document(collection = "#{#mongoCollections.getCollectionFilesName()}")
I was able to get my #Document tag to access a bean by simply changing my MongoTemplate configuration file.
Previously, I had it set up like this:
#Configuration
public class MongoTemplateConfiguration {
...
#Bean
public MongoTemplate mongoTemplate() {
...
return new MongoTemplate(...);
}
}
Changing it to follow this (3.2 Java Configuration) format was all I needed in order to remove the "bean resolver" error:
#Configuration
public class MongoTemplateConfiguration extends AbstractMongoClientConfiguration {
...
#Override
#Bean
public com.mongodb.client.MongoClient mongoClient() {
MongoClientSettings settings = ...;
return MongoClients.create(settings);
}
}

Spring #Required properties when creating #Bean annotated beans

I'm developing a Spring Boot application and am trying out using Java annotation-based bean creation (using #Configuration and #Bean) rather than the familiar old XML-based bean creation. I'm puzzled though. If I attempt to create a bean in XML but fail to set an #Required property I get a BeanInitializationException when the application context is created. In my trials so far with annotation-based bean creation though this does not seem to be the case.
For example:
public class MyClass {
...
#Required
public void setSomeProp(String val){
}
}
Then in Spring XML:
<bean class="MyClass"/>
This will blow up during application startup (and IntelliJ flags it) because the required property is not set. But the same does not seem to be true of this:
#Configuration
public class MyConfig {
#Bean
public MyClass myClass() {
return new MyClass();
}
}
This application starts up just fine even though the required property is not ever set. I must be missing something here, because this seems like a pretty key feature in Spring.
UPDATE
I did some digging & debugging and it turns out that the bean definition is somehow being flagged to skip checking that #Required fields are set. In the Spring class 'RequiredAnnotationBeanPostProcessor' the boolean method 'shouldSkip()' is returning true for beans created this way. When I used the debugger to force that method to return false bean creation did indeed blow up with the expected exception.
Seeing as I'm making a pretty basic Spring Boot application I'm inclined (as Zergleb suggests) to submit this as a bug.
UPDATE 2
Some further debugging has revealed that even if the field is getting set forcing the check still throws the same exception, as if it hadn't been set. So perhaps dunni is correct and there is no way for this to work with #Bean notation.
As you said I also could not get #Required to run as expected this may be a bug and needs to be reported. I have a few other suggestions that did work for me.
Class annotated with #Configuration
//With the bean set up as usual These all worked
#Bean
public MyClass myClass() {
return new MyClass();
}
When you annotate the class #Component and load using component scanning works as expected.(The component scanning part is important you either need your #Configuration class to either have #ComponentScan or perhaps remove #Configuration and replace with #SpringBootApplication and this will enable scanning for components without needing to wire them up using #Bean configs)
#Component // Added this
public class MyClass {
...
#Required //Failed as expected
public void setSomeProp(String val){
}
}
Use #Autowired(required=true) //Fails with BeanCreationException //No qualifying bean of type [java.lang.String] found for dependency
//No more #Component
public class MyClass {
...
#Autowired(required=true) //Fails
public void setSomeProp(String val){
}
}
#Autowired required=false //Does not crash
public class MyClass {
...
#Autowired(required=false) //Simply never gets called if missing
public void setSomeProp(String val){
}
}
#Value //Does not work if test.property is missing // Could not resolve placeholder 'test.property' in string value "${test.property}
public class MyClass {
#Value("${test.property}")
String someProp;
//This getter is not neccesary neither is a setter
public String getSomeProp() {
return this.someProp;
}
}
#Value with default value//Does not crash // When getSomeProp is called it returns "My Default Value"(Unless you have test.property=Anything in your application.properties file then it returns "Anything"
public class MyClass {
#Value("${test.property:My Default Value}")
String someProp;
//This getter is not neccesary neither is a setter
public String getSomeProp() {
return this.someProp; //Returns "My Default Value"
}
}
Inside your #Configuration file also fails if it cannot find anything to populate String someProp in the myClass method
#Bean
public MyClass myClass(String someProp) { //Fails being unable to populate this arg
MyClass myObj = new MyClass();
myObj.setSomeProp(someProp);
return ;
}
If course this won't work, since you create the object of MyClass yourself (new MyClass()), thus the annotations are not evaluated. If you create a bean with a #Bean method, the container will only make sure, that all dependencies are there (method parameters) and that the bean scope is adhered to, meaning if it's a singleton bean, only one bean is created per application context. The creation of the bean/object itself is solely the responsibility of the developer.
The equivalent of the xml <bean> tag is annotating the class with #Component, where the bean is created completely by the container, thus the annotations are evaluated.
As it is being said that when you are having your own #Configuration class where you are creating the bean by itself, #Required doesn't apply there.
When you already have a #Component, let Spring Boot do the component scan and at the required setter property you can add #Autowired and it will work fine.
Found this link on web- https://www.boraji.com/spring-required-annotation-example
For example:
I have a Component called Employee having Id and Name.
#Component
public class Employee {
int id;
String name;
public int getId() {
return id;
}
#Autowired
#Required
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
I have a Configuration class called AppConfig.java
#Configuration
public class AppConfig {
#Bean
public int getId() {
return 1;
}
}
So now we see, that component Employee needs an Id property for binding during startup, so I wrote bean method of type Integer, which will get autowired during runtime. If you do not write a bean of type Integer, it will result a BeanCreationException.
And here is my main class file.
#SpringBootApplication
public class SingletonApplication {
public static void main(String[] args) {
ApplicationContext ctx =
SpringApplication.run(SingletonApplication.class, args);
Employee emp = (Employee)ctx.getBean(Employee.class);
System.out.println(emp.getId());
}
}

Resources