Spring Boot 3 Native with lombok immutable (#Value) - spring-boot

I've been trying to create a example project using spring boot 3, graalvm, lombok and jpa. Everything looks perfect when I execute the test using the main method. But I have a problem when I run the native binary.
First I create the binary:
mvn -Pnative native:build
Then I run the project:
./target/demo
The project start fine, but when I try to create a new record using curl:
curl --location --request POST 'localhost:8080' \
--header 'Content-Type: application/json' \
--data-raw '{
"name": "test",
"surname": "surname"
}'
I see this error in console:
2022-12-21T01:51:48.055+01:00 WARN 105751 --- [nio-8080-exec-2] .c.j.MappingJackson2HttpMessageConverter : Failed to evaluate Jackson deserialization for type [[simple type, class com.example.demo.dto.ExampleDto]]: com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Builder class `com.example.demo.dto.ExampleDto$ExampleDtoBuilder` does not have build method (name: 'build')
2022-12-21T01:51:48.055+01:00 WARN 105751 --- [nio-8080-exec-2] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.web.HttpMediaTypeNotSupportedException: Content-Type 'application/json;charset=UTF-8' is not supported]
My DTO looks like this:
#Value
#Builder
#Jacksonized
public class ExampleDto {
Integer id;
String name;
String surname;
}
The controller:
#RestController
#RequestMapping("/")
#AllArgsConstructor
#Slf4j
public class ExampleController {
private final ExampleService exampleService;
#GetMapping
public ExampleDto findByName(#RequestParam String name) {
return exampleService.getByName(name);
}
#PostMapping
public void save(#RequestBody ExampleDto dto) {
exampleService.save(dto);
}
}
The problem is pretty clear, there is a problem with the "build" method that is created by lombok. If I inspect the generated source I can see the method in the right place. Apparently GraalVM (or jackson) does not see the method.
I also tried to add some traces in a GET method just to be sure that the controller has access to to builder.build() method and the method exist in runtime. (2nd clue that gives me the idea that there is a configuration problem.
I removed the #Value,#Jacksonized and #Builder and put just #Data and everything works fine.
Any idea? Should I add an extra configuration?
The example project is in github: https://github.com/elysrivero99/spring-boot-3-native-demo

After a while I found the answer. You have to add the hints as:
#SpringBootApplication
#ImportRuntimeHints(DemoApplication.DemoApplicationRuntimeHints.class)
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
static class DemoApplicationRuntimeHints implements RuntimeHintsRegistrar {
#SneakyThrows
#Override
public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
hints.reflection()
.registerConstructor(ExampleDto.ExampleDtoBuilder.class.getDeclaredConstructor(), ExecutableMode.INVOKE)
.registerMethod(
ExampleDto.ExampleDtoBuilder.class.getMethod("build"), ExecutableMode.INVOKE);
}
}
}

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.

Properties in RestController is not accessible and Null in Junit

When i run my junit for Rest Controller class, properties in Rest controller is throwing Nullpointer exception. Below is my sample code. when i run the testFileStatus() method of TestingControllerTest calass, i am getting Nullpointer exception. Kindly help to resolve the issue. Any suggestion why i can't able to access the property even though it is available in test application.properties.
enter image description here
#RestController
#CrossOrigin(origins = "*", allowedHeaders = "*")
public class TestingController {
#Value("${file.dir.status}")
private String fileDirectoryStatus;
#GetMapping(value="/filedetails")
public String getFileDetails(.....){
if(fileDirectoryStatus.equalsIgnoreCase("true")) { //NullpointerException in Junit test
//code to process file
}
}
}
src/main/resources/application.properties:
file.dir.status=false
src/test/resources/application.properties:
file.dir.status=false
#SpringBootTest
#TestPropertySource(
locations = "classpath:resources/application.properties")
public class TestingControllerTest{
#InjectMocks
private TestingController testingController;
#Test
public void testFileStatus() {
String status=testingController.getFileDetails(...);
}
}

Spring boot 2: Fail to reach controller method

I am having some spring boot rest tutorial.
I fail to reach the controller method when I call:
http://localhost:8090/customers/stam
Tomcat log:
o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s):
8090 (http) with context path ''
t.s.SpringbootRestDemoApplication : Started SpringbootRestDemoApplication in 2.696 seconds (JVM running for 4.042)
The response I get:
{
"timestamp": "2019-06-02T12:25:03.400+0000",
"status": 404,
"error": "Not Found",
"message": "No message available",
"path": "/customers/stam"
}
Can you assist?
package ttt.springboot_rest_demo;
import ...
#SpringBootApplication
#ComponentScan({"springboot_rest_demo.controller", "springboot_rest_demo.data"})
public class SpringbootRestDemoApplication {
public static void main(String[] args) {
SpringApplication.run(SpringbootRestDemoApplication.class, args);
}
}
package ttt.springboot_rest_demo.controller;
import ...
#RestController
#RequestMapping("/customers")
public class CustomerController {
#RequestMapping(value = "/stam", method = RequestMethod.GET)
public ResponseEntity < Customer > getCustomer() {
return new ResponseEntity < >(new Customer(), HttpStatus.OK);
}
}
package ttt.springboot_rest_demo.data;
public class Customer {
private String name;
private int age;
private String email;
private Long id;
//getters and setters
}
This is only a part of the project. I use also a service class but because I have failed, I a added a simple controller method which doesn't need the service class for now, just to ease the example.
Your ComponentScan is incorrect, please check the packages (these package names do not exists):
#ComponentScan({"springboot_rest_demo.controller", "springboot_rest_demo.data"})
Your controller is in ttt.springboot_rest_demo.controller package. Change the package name in the ComponentScan to this package.
#ComponentScan({"ttt.springboot_rest_demo.controller", "springboot_rest_demo.data"})
Alternatively just leaving out the ComponentScan will also work for you, because then you will rely on the default behaviour of Spring Boot to scan all packages under the SpringBootApplication.
Note that if your controller is not a managed bean (for example not scanned by ComponentScan) any Spring annotation you add (like RequestMapping, RestController) is ignored.

Configuring Groovy MarkupTemplateEngine in Spring Boot leads to Cast Exception

I am trying to configure a custom Template class for the MarkupTemplateEngine in Spring Boot, which allows writing templates in Groovy. My configuration is pretty simple:
#Configuration
class TemplateConfiguration {
#Bean
public GroovyMarkupConfig groovyMarkupConfigurer() {
new GroovyMarkupConfigurer().tap {
resourceLoaderPath = 'classpath:/templates/'
baseTemplateClass = MainTemplate
}
}
}
When changing the resourceLoaderPath to a non-existent path I get an 404 error, which reveals, that that configuration is loaded properly. However, when using setBaseTemplateClass(Class<? extends BaseTemplate>) as in the snippet above, I get the following error:
org.codehaus.groovy.runtime.typehandling.GroovyCastException:
Cannot cast object
'org.springframework.web.servlet.view.groovy.GroovyMarkupConfigurer#111bb71a'
with class 'org.springframework.web.servlet.view.groovy.GroovyMarkupConfigurer'
to class 'TemplateConfiguration'
I have no clue, why this error comes up. My custom Template class looks like this:
abstract class MainTemplate extends BaseTemplate {
MainTemplate(MarkupTemplateEngine templateEngine, Map model, Map<String, String> modelTypes,
TemplateConfiguration configuration) {
super(templateEngine, model, modelTypes, configuration)
}
void doctype() {
yieldUnescaped '<!DOCTYPE html>'
}
}
Letting the Configuration extend GroovyMarkupConfigurer and overwriting the properties solved the issue:
#Configuration
class TemplateConfiguration extends GroovyMarkupConfigurer {
final String resourceLoaderPath = 'classpath:/templates/'
final Class<? extends BaseTemplate> baseTemplateClass = MainTemplate
}

MongoTemplate in MultiTenant Spring Data Mongo Application

This is a follow up to the question Making spring-data-mongodb multi-tenant
Oliver Gierke explained options how to set-up multi-tenancy for a SpringDataMongo application. I followed his "collection approach" and was quite successful. So far. Problems arise, when I want to customise the MongoTemplate used. Have a look on this example:
#SpringBootApplication
public class MultiTenantMongoApplication {
public static void main(String[] args) {
SpringApplication.run(MultiTenantMongoApplication.class, args);
}
#Bean
public MongoTemplate mongoTemplate(Mongo mongo, #Value("${random.name}") String randomName) throws Exception {
String dbname = "db_" + randomName;
MongoTemplate mongoTemplate = new MongoTemplate(mongo, dbname) {
#SuppressWarnings("unused")
public void shutdown() {
mongo.dropDatabase(dbname);
}
};
return mongoTemplate;
}
}
#Document(collection="#{tenantProvider.getTenantCollectionName('Metric')}")
public class Metric {
}
#Repository
public interface MetricRepository extends MongoRepository<Metric, ObjectId>{}
#Component
public class TenantProvider {
public String getTenantCollectionName(String collectionName) {
...
}
}
This yields the following error:
SpelEvaluationException: EL1007E: Property or field 'tenantProvider'
cannot be found on null
When I remove the definition of the MongoTemplate bean in the application class everything is fine and runs as desired.
Obviously the property provider gets not configured appropriately, when the MongoTemplate is customised. Why is this happening? And what can I do, to get the property in place?
I think the above error is because of the SpEL expression. You can try this way to access the TenantProvider class using the below SpEL expression.
#{T(TenantProvider).getTenantCollectionName('Metric')}
or you can add a fully qualified class name for TenantProvider in the above expression.

Resources