#Timed not working despite registering TimedAspect explicitly - spring boot 2.1 - spring-boot

I need to measure method-metrics using micrometer #Timed annotation. As it doesn't work on arbitrary methods; i added the configuration of #TimedAspect explicitly in my spring config. Have referred to this post for exact config
Note: have tried adding a separate config class JUST for this, as well as including the TimedAspect bean as part of my existing configuration bean
How to measure service methods using spring boot 2 and micrometer
Yet, it unfortunately doesn't work. The Bean is registred and the invocation from config class goes thru successfully on startup. Found this while debugging. However, the code in the #Around never seems to execute.
No error is thrown; and im able to view the default 'system' metrics on the /metrics and /prometheus endpoint.
Note: This is AFTER getting the 'method' to be invoked several times by executing a business flow. I'm aware that it probably doesn't show up in the metrics if the method isn't invoked at all
Versions: spring-boot 2.1.1, spring 5.3, micrometer 1.1.4, actuator 2.1
Tried everything going by the below posts:
How to measure service methods using spring boot 2 and micrometer
https://github.com/izeye/sample-micrometer-spring-boot/tree/timed-annotation
https://github.com/micrometer-metrics/micrometer/issues/361
Update: So, the issue seems to be ONLY when the Timed is on an abstract method, which is called via another method. Was able to reproduce it via a simple example. Refer to the #Timed("say_hello_example") annotation. It simply gets ignored and doesnt show up when i hit the prometheus endpoint.
Code:
Abstract Class
public abstract class AbstractUtil {
public abstract void sayhello();
public void sayhellowithtimed(String passedVar) {
System.out.println("Passed var =>"+passedVar);
System.out.println("Calling abstract sayhello....");
sayhello();
}
}
Impl Class
#Component
#Scope("prototype")
public class ExampleUtil extends AbstractUtil {
public static final String HELLO = "HELLO";
#Timed("dirwatcher_handler")
public void handleDirectoryWatcherChange(WatchEvent event){
System.out.println("Event kind:" + event.kind() + ". File affected: " + event.context());
}
#Timed("say_hello_example")
#Override
public void sayhello() {
System.out.println(HELLO);
try {
Thread.sleep(1000);
}
catch (InterruptedException e) {
e.printStackTrace();
}
}
}
A simple DirWatcher implementation class...
package com.example;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.event.ApplicationStartedEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.context.annotation.Scope;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.nio.file.*;
#Component
#Scope("prototype")
public class StartDirWatcher implements ApplicationListener<ApplicationStartedEvent> {
#Value("${directory.path:/apps}")
public String directoryPath;
#Autowired
private ExampleUtil util;
private void monitorDirectoryForChanges() throws IOException, InterruptedException {
WatchService watchService = FileSystems.getDefault().newWatchService();
Path path = Paths.get(directoryPath);
path.register(
watchService,
StandardWatchEventKinds.ENTRY_CREATE,
StandardWatchEventKinds.ENTRY_DELETE,
StandardWatchEventKinds.ENTRY_MODIFY);
WatchKey key;
while ((key = watchService.take()) != null) {
for (WatchEvent<?> event : key.pollEvents()) {
util.handleDirectoryWatcherChange(event);
util.sayhellowithtimed("GOD_OF_SMALL_THINGS_onAPPEvent");
}
key.reset();
}
}
#Override
public void onApplicationEvent(ApplicationStartedEvent applicationStartedEvent) {
try {
monitorDirectoryForChanges();
} catch (Throwable e) {
System.err.println("ERROR!! "+e.getMessage());
e.printStackTrace();
}
}
}
The Spring Boot Application Class
package com.example;
import io.micrometer.core.aop.TimedAspect;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.prometheus.PrometheusMeterRegistry;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.actuate.autoconfigure.metrics.MeterRegistryCustomizer;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
#EnableAspectJAutoProxy
#ComponentScan
#Configuration
#SpringBootApplication
public class ExampleStarter{
#Bean
MeterRegistryCustomizer<PrometheusMeterRegistry> metricsCommonTags() {
return registry -> registry.config().commonTags("app.name", "example.app");
}
#Bean
TimedAspect timedAspect(MeterRegistry reg) {
return new TimedAspect(reg);
}
public static void main(String[] args) {
SpringApplication.run(ExampleStarter.class, args);
}
}
The main pom.xml file
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.metrics.timed.example</groupId>
<artifactId>example-app</artifactId>
<version>1.0-SNAPSHOT</version>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.1.1.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
<version>2.1.1.RELEASE</version>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
<version>1.1.2</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
<version>2.1.1.RELEASE</version>
</dependency>
</dependencies>

I use spring boot 2.2.6.RELEASE and this MetricConfig works for me
#Configuration
public class MetricConfig {
#Bean
MeterRegistryCustomizer<MeterRegistry> metricsCommonTags() {
return registry -> registry.config().commonTags("application", "my app");
}
#Bean
TimedAspect timedAspect(MeterRegistry registry) {
return new TimedAspect(registry);
}
}
In application.yml
management:
endpoints:
web:
exposure:
include: ["health", "prometheus"]
endpoint:
beans:
cache:
time-to-live: 10s

#Timed use AOP(Aspect oriented programming) concept, in which proxy doesn't pass on to the second level of the method.
you can define the second level of method in new bean/class. this way #Timed will work for second level of method call.

I had the same problem, in my case I realised that the metric got visible under actuator/metrics only after the method had been called at least once.
Unlike with manually created timers/counters, where they get visible directly after startup.

Related

Spring Boot application - not reading application.properties in Tomcat, but works under Spring Tool Suite

I wrote a Spring Boot web application. The application runs under Spring Tool Suite fine, however, when I deploy in Tomcat, it is not able to read the configuration values. I think the difference is how the application being run. Any help or suggestions will be highly appreciated. I know this question has been asked many times - I did try all the solutions suggested, but did not have any luck.
Here is the code/config I have:
TestApplication.java
package com.example.test;
import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;
import org.springframework.context.ApplicationContext;
#SpringBootApplication
public class TestApplication extends SpringBootServletInitializer
{
#Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder application)
{
return application.sources(TestApplication.class);
}
public static void main(String[] args)
{
ApplicationContext context = SpringApplication.run(TestApplication.class, args);
ServiceConfig bean = ServiceConfig.getServiceConfig();
AutowireCapableBeanFactory factory = context.getAutowireCapableBeanFactory();
factory.autowireBean( bean );
factory.initializeBean( bean, "ServiceConfig" );
}
}
TestController.java
package com.example.test;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.GetMapping;
#RestController
public class TestController
{
#GetMapping(value = "/")
public String index()
{
return "Hello from " + ServiceConfig.getServiceConfig().getName();
}
}
ServletInitializer.java
package com.example.test;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;
public class ServletInitializer extends SpringBootServletInitializer {
#Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
return application.sources(TestApplication.class);
}
}
ServiceConfig.java
package com.example.test;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
#Component("ServiceConfig")
public class ServiceConfig
{
static private ServiceConfig serviceConfig = null;
#Value("${spring.application.name}")
private String name;
public String getName()
{
return name;
}
private ServiceConfig() {}
public static ServiceConfig getServiceConfig()
{
if (serviceConfig == null) {
serviceConfig = new ServiceConfig();
}
return serviceConfig;
}
}
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.0.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>test</artifactId>
<version>1.0.0</version>
<packaging>war</packaging>
<name>test</name>
<description>Spring Boot Test Application</description>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
<start-class>com.example.test.TestApplication</start-class>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<finalName>test</finalName>
</configuration>
</plugin>
</plugins>
</build>
</project>
application.properties
spring.application.name = Test-Service
Deployment Steps:
(1) Build the war file: mvn clean install
(2) Copy the generated test.war under Tomcat /webapps directory.
(3) Start tomcat using /bin/start.sh
Issue:
When I browse http://localhost:8080/test, I see the configuration parameter is not being populated:
Hello from null
However, I tried to run under Spring Tool Suite, I see the service running at http://localhost:8080 and the code works:
Hello from Test-Service
UPDATED WITH RESOLUTION
As pointed out by Andy Wilkinson, the main class was not being called by the war file, and therefore the the configurations were not loaded.
The reason I added the adhoc initilization of the ServiceConfig bean is that the #Component annotation was not automatically loading the application.properties in the ServiceConfig class. Anyway, I finally used Java's #PostConstruct annotation that works both for independent Java application or war based Tomcat application:
Here is the updated code:
TestApplication.java
package com.example.test;
import javax.annotation.PostConstruct;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;
import org.springframework.context.ApplicationContext;
#SpringBootApplication
public class TestApplication extends SpringBootServletInitializer
{
#Autowired
private ApplicationContext appContext;
#Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder application)
{
return application.sources(TestApplication.class);
}
#PostConstruct
public void init()
{
ServiceConfig bean = ServiceConfig.getServiceConfig();
AutowireCapableBeanFactory factory = appContext.getAutowireCapableBeanFactory();
factory.autowireBean( bean );
factory.initializeBean( bean, "ServiceConfig" );
}
public static void main(String[] args)
{
SpringApplication.run(TestApplication.class, args);
}
}
Thanks #Andy Wilkinson again for your help!
Best Regards,
MN
When deployed as a war file, your application’s main method isn’t called. Crucially in this case, this means that the following code doesn’t get called:
ServiceConfig bean = ServiceConfig.getServiceConfig();
AutowireCapableBeanFactory factory = context.getAutowireCapableBeanFactory();
factory.autowireBean( bean );
factory.initializeBean( bean, "ServiceConfig" );
Without this code being called, the ServiceConfig that TestController retrieves hasn’t been initialised and, therefore, its name field is null.
The code to initialise ServiceConfig is unconventional for a Spring Boot application. I would remove it entirely and rely on a ServiceConfig bean being created and initialised automatically by virtue of its #Component annotation. With this code removed, you then need to update your controller to inject the dependency. I’d use constructor injection for that:
#RestController
public class TestController
{
private final ServiceConfig serviceConfig;
TestController(ServiceConfig ServiceConfig) {
this.serviceConfig = serviceConfig;
}
#GetMapping(value = "/")
public String index()
{
return "Hello from " + this.serviceConfig.getName();
}
}
Move your application.properties file into /webapps directory will solve this issue.

How to implement snake_case for Swagger documentation in spring-boot-starter-data-rest?

I am using swagger for the documentation of spring-boot-starter-data-rest project. In application.properties file, I have configured: spring.jackson.property-naming-strategy=SNAKE_CASE naming strategy but unfortunately, I am getting camelCase in swagger documentation. But, the same configuration is working if I change the project from spring-boot-starter-data-rest to spring-boot-starter-web. Below are the dependencies I am using with spring boot 2.1.1.RELEASE.
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.1.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>demo1</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>demo1</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.9.2</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.9.2</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-rest</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
application.properties
spring.jackson.property-naming-strategy=SNAKE_CASE
package com.example.demo1;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
#SpringBootApplication
public class Demo1Application {
public static void main(String[] args) {
SpringApplication.run(Demo1Application.class, args);
}
}
SwaggerConfig
package com.example.demo1;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.PropertyNamingStrategy;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import
org.springframework.data.web.config.SpringDataJacksonConfiguration;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.Contact;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
import java.util.ArrayList;
#Configuration
#EnableSwagger2
#Import({SpringDataJacksonConfiguration.class})
public class SwaggerConfig {
public static ApiInfo metaData(String info) {
return new ApiInfo(info,
"Th",
"1.0", "httn.html",
new Contact("Thd", "", "thoom"), "decense",
"https", new ArrayList());
}
#Bean
public Docket cashFlowApi() {
return new Docket(DocumentationType.SWAGGER_2).groupName("-caching").select()
.apis(RequestHandlerSelectors.basePackage("com.example.demo1"))
.paths(PathSelectors.any())
.build()
.apiInfo(SwaggerConfig.metaData("BOcPI"));
}
}
StoreController
package com.example.demo1;
import java.util.Arrays;
import java.util.List;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import io.swagger.annotations.ApiResponse;
import io.swagger.annotations.ApiResponses;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
#RestController
#CrossOrigin
public class StoreController {
#GetMapping(value = "/v1/storeMap")
#ApiOperation(value = "Returns the list of stores", notes = "Returns the list of stores with pagination.")
#ApiResponses(value = {#ApiResponse(code = 200, message = "Successfully retrieved the stores list"),
#ApiResponse(code = 204, message = "No content"), #ApiResponse(code = 206, message = "Partial Content"),
#ApiResponse(code = 401, message = "You are not authorized to view the resource"),
#ApiResponse(code = 403, message = "Accessing the resource you were trying to reach is forbidden"),
#ApiResponse(code = 404, message = "The resource you were trying to reach is not found"),
#ApiResponse(code = 500, message = "A technical error happened")})
public ResponseEntity<Store> getStore(
#RequestParam(name = "country_code", required = false) #ApiParam(value = "the code)") String countryCode
) {
return ResponseEntity.ok(new Store(1,"ZZ"));
}
}
Now with this configuration, in API POST method is expecting snake_case and in the documentation, swagger is showing camelCase. I don't have the option either to change from snake_case to camelCase or spring-boot-starter-data-rest to spring-boot-starter-web.
I found the solution, the issue was with object mapper :
#Configuration
public class ObjectMapperAutoConfiguration implements WebMvcConfigurer {
#Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
ObjectMapper objectMapper = null;
for (HttpMessageConverter converter : converters) {
if (converter instanceof MappingJackson2HttpMessageConverter ) {
MappingJackson2HttpMessageConverter jacksonConverter =
((MappingJackson2HttpMessageConverter) converter);
if (objectMapper == null) {
objectMapper = jacksonConverter.getObjectMapper();
} else {
jacksonConverter.setObjectMapper(objectMapper);
}
}
}
}
}
I also have a Spring Boot + Swagger project with the same issue, but using Spring the io.swagger packages without the Springfox libraries and using the Gradle plugin com.benjaminsproule.swagger to generate the docs. So to solve the issue I added this line ModelConverters.getInstance().addConverter(new ModelResolver(objectMapper)) (it's a hack, I'm not using an official API, but it worked):
package com.company.api;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.swagger.converter.ModelConverters;
import io.swagger.jackson.ModelResolver;
import io.swagger.jaxrs.config.BeanConfig;
import io.swagger.jaxrs.listing.*;
import org.glassfish.jersey.server.ResourceConfig;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
#Component
public class SwaggerConfig extends ResourceConfig {
#Autowired ObjectMapper objectMapper;
#PostConstruct public void configureSwagger() {
// Available at localhost:port/swagger.json
this.register(ApiListingResource.class);
this.register(SwaggerSerializers.class);
BeanConfig config = new BeanConfig();
config.setConfigId("api-springboot-jersey-swagger");
config.setTitle("Company API");
config.setResourcePackage("com.company.api");
config.setPrettyPrint(true);
config.setScan(true);
// With this hack we inject into Swagger the same object mapper
// used by Spring (spring.jackson.property-naming-strategy)
ModelConverters.getInstance().addConverter(new ModelResolver(objectMapper));
}
}
The annotation on the model helped me. #JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class)
https://www.baeldung.com/jackson-deserialize-snake-to-camel-case

Configuring Swagger UI with Spring Boot

I am trying to configure Swagger UI with my Spring boot application. Although the v2/api-docs seems to be loading properly, the http://localhost:8080/swagger-ui.html does not load my annotated REST API.
Here is what I have:
pom.xml:
...
<!--Swagger UI-->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.6.1</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.4.0</version>
</dependency>
...
SwaggerConfig.java
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
import static springfox.documentation.builders.PathSelectors.regex;
#Configuration
#EnableSwagger2
public class SwaggerConfig
{
#Bean
public Docket api()
{
return new Docket(DocumentationType.SWAGGER_2)
.select()
.apis(RequestHandlerSelectors.any())
.paths(regex("/.*"))
.build().apiInfo(apiInfo());
}
private ApiInfo apiInfo()
{
ApiInfo apiInfo = new ApiInfo(
"My Project's REST API",
"This is a description of your API.",
"version-1",
"API TOS",
"me#wherever.com",
"API License",
"API License URL"
);
return apiInfo;
}
}
http://localhost:8080/v2/api-docs:
{"swagger":"2.0","info":{"description":"This is a description of your API.","version":"version-1","title":"My Project's REST API","termsOfService":"API TOS","contact":{"name":"me#wherever.com"},"license":{"name":"API License","url":"API License URL"}},"host":"localhost:8080","basePath":"/","tags":[{"name":"test-controller","description":"Test Controller"},{"name":"custom-field-controller","description":"Custom Field Controller"},{"name":"user-controller","description":"User Controller"},{"name":"users-controller","description":"Users Controller"},{"name":"crudapi-controller","description":"CRUDAPI Controller"},{"name":"basic-error-controller","description":"Basic Error Controller"}],"paths":{"/":{"get":{"tags":["crudapi-controller"],"summary":"greeting","operationId":"greetingUsingGET","consumes":["application/json"],"produces":["*/*"],"responses":{"200":{"description":"OK","schema":{"type":"string"}},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not Found"}}}},"/api/javainuse":{"get":{"tags":["test-controller"],"summary":"firstPage","operationId":"firstPageUsingGET","consumes":["application/json"],"produces":["*/*"],"responses":{"200":{"description":"OK","schema":{"type":"string"}},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not Found"}}}},"/error":{"get":{"tags":["basic-error-controller"],"summary":"errorHtml","operationId":"errorHtmlUsingGET","consumes":["application/json"],"produces":["text/html"],"responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/ModelAndView"}},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not Found"}}},"head":{"tags":["basic-error-controller"],"summary":"errorHtml","operationId":"errorHtmlUsingHEAD","consumes":["application/json"],"produces":["text/html"],"responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/ModelAndView"}},"401":{"description":"Unauthorized"},"204":{"description":"No Content"},"403":{"description":"Forbidden"}}},"post":{"tags":["basic-error-controller"],"summary":"errorHtml","operationId":"errorHtmlUsingPOST","consumes":["application/json"],"produces":["text/html"],"responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/ModelAndView"}},"201":{"description":"Created"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not Found"}}},"put":{"tags":["basic-error-controller"],"summary":"errorHtml","operationId":"errorHtmlUsingPUT","consumes":["application/json"],"produces":["text/html"],"responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/ModelAndView"}},"201":{"description":"Created"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not Found"}}},"delete":{"tags":["basic-error-controller"],"summary":"errorHtml","operationId":"errorHtmlUsingDELETE","consumes":["application/json"],"produces":["text/html"],"responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/ModelAndView"}},"401":{"description":"Unauthorized"},"204":{"description":"No Content"},"403":{"description":"Forbidden"}}},"options":{"tags":["basic-error-controller"],"summary":"errorHtml","operationId":"errorHtmlUsingOPTIONS","consumes":["application/json"],"produces":["text/html"],"responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/ModelAndView"}},"401":{"description":"Unauthorized"},"204":{"description":"No Content"},"403":{"description":"Forbidden"}}},"patch":{"tags":["basic-error-controller"],"summary":"errorHtml","operationId":"errorHtmlUsingPATCH","consumes":["application/json"],"produces":["text/html"],"responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/ModelAndView"}},"401":{"description":"Unauthorized"},"204":{"description":"No Content"},"403":{"description":"Forbidden"}}}},"/fields":{"get":{"tags":["custom-field-controller"],"summary":"greeting","operationId":"greetingUsingGET_1","consumes":["application/json"],"produces":["*/*"],"responses":{"200":{"description":"OK","schema":{"type":"string"}},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not Found"}}}},"/fields/{id}":{"get":{"tags":["custom-field-controller"],"summary":"fieldAPIController","operationId":"fieldAPIControllerUsingGET","consumes":["application/json"],"produces":["*/*"],"parameters":[{"name":"id","in":"path","description":"id","required":true,"type":"integer","format":"int32"}],"responses":{"200":{"description":"OK","schema":{"type":"array","items":{"$ref":"#/definitions/CustomField"}}},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not Found"}}}},"/users":{"get":{"tags":["user-controller"],"summary":"greeting","operationId":"greetingUsingGET_2","consumes":["application/json"],"produces":["*/*"],"responses":{"200":{"description":"OK","schema":{"type":"string"}},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not Found"}}}},"/users/":{"get":{"tags":["users-controller"],"summary":"listUsers","operationId":"listUsersUsingGET","consumes":["application/json"],"produces":["*/*"],"responses":{"200":{"description":"OK","schema":{"type":"array","items":{"$ref":"#/definitions/UserJPA"}}},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not Found"}}},"head":{"tags":["users-controller"],"summary":"listUsers","operationId":"listUsersUsingHEAD","consumes":["application/json"],"produces":["*/*"],"responses":{"200":{"description":"OK","schema":{"type":"array","items":{"$ref":"#/definitions/UserJPA"}}},"401":{"description":"Unauthorized"},"204":{"description":"No Content"},"403":{"description":"Forbidden"}}},"post":{"tags":["users-controller"],"summary":"listUsers","operationId":"listUsersUsingPOST","consumes":["application/json"],"produces":["*/*"],"responses":{"200":{"description":"OK","schema":{"type":"array","items":{"$ref":"#/definitions/UserJPA"}}},"201":{"description":"Created"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not Found"}}},"put":{"tags":["users-controller"],"summary":"listUsers","operationId":"listUsersUsingPUT","consumes":["application/json"],"produces":["*/*"],"responses":{"200":{"description":"OK","schema":{"type":"array","items":{"$ref":"#/definitions/UserJPA"}}},"201":{"description":"Created"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not Found"}}},"delete":{"tags":["users-controller"],"summary":"listUsers","operationId":"listUsersUsingDELETE","consumes":["application/json"],"produces":["*/*"],"responses":{"200":{"description":"OK","schema":{"type":"array","items":{"$ref":"#/definitions/UserJPA"}}},"401":{"description":"Unauthorized"},"204":{"description":"No Content"},"403":{"description":"Forbidden"}}},"options":{"tags":["users-controller"],"summary":"listUsers","operationId":"listUsersUsingOPTIONS","consumes":["application/json"],"produces":["*/*"],"responses":{"200":{"description":"OK","schema":{"type":"array","items":{"$ref":"#/definitions/UserJPA"}}},"401":{"description":"Unauthorized"},"204":{"description":"No Content"},"403":{"description":"Forbidden"}}},"patch":{"tags":["users-controller"],"summary":"listUsers","operationId":"listUsersUsingPATCH","consumes":["application/json"],"produces":["*/*"],"responses":{"200":{"description":"OK","schema":{"type":"array","items":{"$ref":"#/definitions/UserJPA"}}},"401":{"description":"Unauthorized"},"204":{"description":"No Content"},"403":{"description":"Forbidden"}}}},"/users/{id}":{"get":{"tags":["user-controller"],"summary":"userAPIController","operationId":"userAPIControllerUsingGET","consumes":["application/json"],"produces":["*/*"],"parameters":[{"name":"id","in":"path","description":"id","required":true,"type":"integer","format":"int32"}],"responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/Collection«UserJPA»"}},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not Found"}}}}},"definitions":{"UserJPA":{"type":"object"},"Collection«UserJPA»":{"type":"object"},"ModelAndView":{"type":"object","properties":{"empty":{"type":"boolean"},"model":{"type":"object"},"modelMap":{"type":"object","additionalProperties":{"type":"object"}},"reference":{"type":"boolean"},"status":{"type":"string","enum":["100","101","102","103","200","201","202","203","204","205","206","207","208","226","300","301","302","303","304","305","307","308","400","401","402","403","404","405","406","407","408","409","410","411","412","413","414","415","416","417","418","419","420","421","422","423","424","426","428","429","431","451","500","501","502","503","504","505","506","507","508","509","510","511"]},"view":{"$ref":"#/definitions/View"},"viewName":{"type":"string"}}},"CustomField":{"type":"object","properties":{"name":{"type":"string"}}},"View":{"type":"object","properties":{"contentType":{"type":"string"}}}}}
The swagger-ui.html (http://localhost:8080/swagger-ui.html) does not show the expected REST calls:
The error in swagger-ui.html from the code inspection:
Failed to load resource: the server responded with a status of 404 ().
I have googled around (tried web-config mvc too) but the error persists. Maybe I am missing a resource reference in the .iml file?
I had this issue today and fixed it by matching up the versions of my springfox-swagger2 and springfox-swagger-ui dependencies:
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.6.1</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.6.1</version>
</dependency>
There's very little other code to just get it up and running. One simple config class:
#Configuration
#EnableSwagger2
class SwaggerConfiguration {
#Bean
public Docket api() {
return new Docket(DocumentationType.SWAGGER_2)
.select()
.apis(RequestHandlerSelectors.basePackage("com.foo.samples.swaggersample"))
.paths(PathSelectors.any())
.build();
}
}
And my application.properties
# location of the swagger json
springfox.documentation.swagger.v2.path=/swagger.json
(This is in Spring Boot).
Statement : Generate Swagger UI for the listing of all the REST APIs through Spring Boot Application.
Follow the below steps to generate the Swagger UI through Spring Boot application:
1. Add following dependency in pom.xml –
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.6.1</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.6.1</version>
</dependency>
2. Add the following piece of code in your main application class having the #EnableSwagger2 annotation.
#EnableSwagger2
#SpringBootApplication
public class MyApp {
public static void main(String[] args) {
SpringApplication.run(MyApp.class, args);
}
#Bean
public Docket api() {
return new Docket(DocumentationType.SWAGGER_2).select()
.apis(RequestHandlerSelectors.withClassAnnotation(Api.class))
.paths(PathSelectors.any()).build().pathMapping("/")
.apiInfo(apiInfo()).useDefaultResponseMessages(false);
}
#Bean
public ApiInfo apiInfo() {
final ApiInfoBuilder builder = new ApiInfoBuilder();
builder.title("My Application API through Swagger UI").version("1.0").license("(C) Copyright Test")
.description("List of all the APIs of My Application App through Swagger UI");
return builder.build();
}
}
3. Add the below RootController class in your code to redirect to the Swagger UI page. In this way, you don’t need to put the dist folder of Swagger-UI in your resources directory.
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
#Controller
#RequestMapping("/")
public class RootController {
#RequestMapping(method = RequestMethod.GET)
public String swaggerUi() {
return "redirect:/swagger-ui.html";
}
}
4. Being the final steps, add the #Api and #ApiOperation notation in all your RESTControllers like below –
import static org.springframework.web.bind.annotation.RequestMethod.GET;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
#RestController
#RequestMapping("/hello")
#Api(value = "hello", description = "Sample hello world application")
public class TestController {
#ApiOperation(value = "Just to test the sample test api of My App Service")
#RequestMapping(method = RequestMethod.GET, value = "/test")
// #Produces(MediaType.APPLICATION_JSON)
public String test() {
return "Hello to check Swagger UI";
}
#ResponseStatus(HttpStatus.OK)
#RequestMapping(value = "/test1", method = GET)
#ApiOperation(value = "My App Service get test1 API", position = 1)
public String test1() {
System.out.println("Testing");
if (true) {
return "Tanuj";
}
return "Gupta";
}
}
Now your are done. Now to run your Spring Boot Application, go to browser and type localhost:8080. You will see Swagger UI having all the details of your REST APIs.
Happy Coding. 🙂
The source code of the above implementation is also on my blog if you feel like checking it out.
Swagger is Available with V2 and V3 version
More minimal config
Check this Answer - https://stackoverflow.com/a/64333853/410439
Add a config class like this
#Configuration
public class WebMvcConfiguration extends WebMvcConfigurationSupport {
#Override
public void addResourceHandlers(final ResourceHandlerRegistry registry) {
// Make Swagger meta-data available via <baseURL>/v2/api-docs/
registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/");
// Make Swagger UI available via <baseURL>/swagger-ui.html
registry.addResourceHandler("/**").addResourceLocations("classpath:/META-INF/resources/");
}
}
Nowadays, just set
springdoc.swagger-ui.disable-swagger-default-url=true

Spring pure annotation with autowired

I am trying to do a full annotations (no xml) implementation of Spring. The autowired members are not being populated. From my research, there are 3 things to do:
Set up a config file that manages the bean
Use #Autowired to get the bean to the file
Instantiate an application context to get the ball rolling
It is difficult to find a complete example of this which uses annotations only, so I don't have much to reference. Most examples use at least some xml.
There is no error message, so I don't have any idea where the problem is. The value is just null. Here are my files:
Trivial.java
public class Trivial {
public TrivialBean trivialBean;
#Autowired
public void setTrivialBean(TrivialBean trivialBean) {
this.trivialBean = trivialBean;
}
public static void main(String...args) {
ApplicationContext context
= new AnnotationConfigApplicationContext(
TrivialConfig.class);
new Trivial().go();
}
private void go() {
System.out.println("trivialBean: " + trivialBean);
}
}
TrivialBean.java
public class TrivialBean {
public String foo = "TEST TEST TEST";
#Override
public String toString() {
return foo;
}
}
TrivialConfig.java
#Configuration
public class TrivialConfig {
#Bean
public TrivialBean trivialBean() {
return new TrivialBean();
}
}
I would expect this to output trivialBean: TEST TEST TEST, but is just outputs trivialBean: null
For the #Autowired in Trivial to work, you need to have Trivial instantiated by Spring. new Trivial() won't work. For your sample to work, I think you need the following:
Configure Trivial as a bean.
Change new Trivial() to context.getBean(Trivial.class).
However, note that it is considered bad practice to use context.getBean under normal circumstances.
Regular autowiring in annotation-based container configuration
In order for autowiring to work, the lifecycle of the instance of Trivial has to be managed by the Spring container.
Example
TrivialBean.java is the same
public class TrivialBean {
public String foo = "TEST TEST TEST";
#Override
public String toString() {
return foo;
}
}
TrivialConfig.java
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
#Configuration
public class TrivialConfig {
#Bean
public TrivialBean trivialBean() {
return new TrivialBean();
}
#Bean
public Trivial trivial() {
return new Trivial();
}
}
Trivial.java
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class Trivial {
public TrivialBean trivialBean;
#Autowired
public void setTrivialBean(TrivialBean trivialBean) {
this.trivialBean = trivialBean;
}
public static void main(String... args) {
ApplicationContext context = new AnnotationConfigApplicationContext(TrivialConfig.class);
Trivial trivial = context.getBean(Trivial.class);
trivial.go();
}
private void go() {
System.out.println("trivialBean: " + trivialBean);
}
}
Output
trivialBean: TEST TEST TEST
Please consult Spring documentation for more information on Annotation-based container configuration.
AspectJ compile-time weaving and #Configurable
It is possible to autowire TrivialBean instance into Trivial instance created by new.
spring-aspects.jar contains an annotation-driven aspect that allows dependency injection for objects created outside of the control of the container. However, it should not be used in new Spring-based projects. It is intended to be used for legacy projects, where for some reason some instances are created outside of the Spring container.
Example for Spring 4.2.0 (the latest at the moment), AspectJ 1.8.6 (the latest at the moment), Maven and Java 1.8.
Additional dependencies on spring-aspects and aspectjrt
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>4.2.0.RELEASE</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>1.8.6</version>
</dependency>
Compile time weaving via AspectJ Maven plugin
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>aspectj-maven-plugin</artifactId>
<version>1.7</version>
<configuration>
<complianceLevel>1.8</complianceLevel>
<encoding>UTF-8</encoding>
<aspectLibraries>
<aspectLibrary>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
</aspectLibrary>
</aspectLibraries>
<Xlint>warning</Xlint>
</configuration>
<executions>
<execution>
<goals>
<goal>compile</goal>
<goal>test-compile</goal>
</goals>
</execution>
</executions>
</plugin>
TrivialBean.java is the same
public class TrivialBean {
public String foo = "TEST TEST TEST";
#Override
public String toString() {
return foo;
}
}
TrivialConfig.java
#EnableSpringConfigured is analogous to <context:spring-configured>. It signals the current application context to apply dependency injection to classes that are instantiated outside of the Spring bean factory.
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.aspectj.EnableSpringConfigured;
#Configuration
#EnableSpringConfigured
public class TrivialConfig {
#Bean
public TrivialBean trivialBean() {
return new TrivialBean();
}
}
Trivial.java
#Configurable applies Spring-driven configuration to Trivial
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Configurable;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
#Configurable
public class Trivial {
public TrivialBean trivialBean;
#Autowired
public void setTrivialBean(TrivialBean trivialBean) {
this.trivialBean = trivialBean;
}
public static void main(String... args) {
ApplicationContext context = new AnnotationConfigApplicationContext(TrivialConfig.class);
Trivial trivial = new Trivial();
trivial.go();
}
private void go() {
System.out.println("trivialBean: " + trivialBean);
}
}
Output
trivialBean: TEST TEST TEST
It works! Please consult Spring documentation for more information on AspectJ and #Configurable.

Why do I get Detached Entity exception when upgrading Spring Boot 1.1.4 to 1.1.5

On updating Spring Boot from 1.1.4 to 1.1.5 a simple web application started generating detached entity exceptions. Specifically, a post authentication inteceptor that bumped number of visits was causing the problem.
A quick check of loaded dependencies showed that Spring Data has been updated from 1.6.1 to 1.6.2 and a further check of the change log shows a couple of issues relating to optimistic locking, version fields and JPA issues that have been fixed.
Well I am using a version field and it starts out as Null following recommendation to not set in the specification.
I have produced a very simple test scenario where I get detached entity exceptions if the version field starts as null or zero. If I create an entity with version 1 however then I do not get these exceptions.
Is this expected behaviour or is there still something amiss?
Below is the test scenario I have for this condition. In the scenario the service layer that has been annotated #Transactional. Each test case makes multiple calls to the service layer - the tests are working with detached entities as this is the scenario I am working with in the full blown application.
The test case comprises four tests:
Test 1 - versionNullCausesAnExceptionOnUpdate()
In this test the version field in the detached object is Null. This is how I would usually create the object prior to passing to the service.
This test fails with a Detached Entity exception.
I would have expected this test to pass. If there is a flaw in the test then the rest of the scenario is probably moot.
Test 2 - versionZeroCausesExceptionOnUpdate()
In this test I have set the version to value Long(0L). This is an edge case test and included because I found reference to Zero values being used for version field in the Spring Data change log.
This test fails with a Detached Entity exception.
Of interest simply because the following two tests pass leaving this as an anomaly.
Test 3 - versionOneDoesNotCausesExceptionOnUpdate()
In this test the version field is set to value Long(1L). Not something I would usually do, but considering the notes in the Spring Data change log I decided to give it a go.
This test passes.
Would not usually set the version field, but this looks like a work-around until I figure out why the first test is failing.
Test 4 - versionOneDoesNotCausesExceptionWithMultipleUpdates()
Encouraged by the result of test 3 I pushed the scenario a step further and perform multiple updates on the entity that started life with a version of Long(1L).
This test passes.
Reinforcement that this may be a useable work-around.
The entity:
package com.mvmlabs.domain;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;
import javax.persistence.Version;
#Entity
#Table(name="user_details")
public class User {
#Id
#GeneratedValue(strategy=GenerationType.AUTO)
private Long id;
#Version
private Long version;
#Column(nullable = false, unique = true)
private String username;
#Column(nullable = false)
private Integer numberOfVisits;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Long getVersion() {
return version;
}
public void setVersion(Long version) {
this.version = version;
}
public Integer getNumberOfVisits() {
return numberOfVisits == null ? 0 : numberOfVisits;
}
public void setNumberOfVisits(Integer numberOfVisits) {
this.numberOfVisits = numberOfVisits;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
}
The repository:
package com.mvmlabs.dao;
import org.springframework.data.repository.CrudRepository;
import com.mvmlabs.domain.User;
public interface UserDao extends CrudRepository<User, Long>{
}
The service interface:
package com.mvmlabs.service;
import com.mvmlabs.domain.User;
public interface UserService {
User save(User user);
User loadUser(Long id);
User registerVisit(User user);
}
The service implementation:
package com.mvmlabs.service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import com.mvmlabs.dao.UserDao;
import com.mvmlabs.domain.User;
#Service
#Transactional(propagation=Propagation.REQUIRED, readOnly=false)
public class UserServiceJpaImpl implements UserService {
#Autowired
private UserDao userDao;
#Transactional(readOnly=true)
#Override
public User loadUser(Long id) {
return userDao.findOne(id);
}
#Override
public User registerVisit(User user) {
user.setNumberOfVisits(user.getNumberOfVisits() + 1);
return userDao.save(user);
}
#Override
public User save(User user) {
return userDao.save(user);
}
}
The application class:
package com.mvmlabs;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
#Configuration
#ComponentScan
#EnableAutoConfiguration
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
The POM:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.mvmlabs</groupId>
<artifactId>jpa-issue</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>spring-boot-jpa-issue</name>
<description>JPA Issue between spring boot 1.1.4 and 1.1.5</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.1.5.RELEASE</version>
<relativePath /> <!-- lookup parent from repository -->
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.hsqldb</groupId>
<artifactId>hsqldb</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<start-class>com.mvmlabs.Application</start-class>
<java.version>1.7</java.version>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
The application properties:
spring.jpa.hibernate.ddl-auto: create
spring.jpa.hibernate.naming_strategy: org.hibernate.cfg.ImprovedNamingStrategy
spring.jpa.database: HSQL
spring.jpa.show-sql: true
spring.datasource.url=jdbc:hsqldb:file:./target/testdb
spring.datasource.username=sa
spring.datasource.password=
spring.datasource.driverClassName=org.hsqldb.jdbcDriver
The test case:
package com.mvmlabs;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import com.mvmlabs.domain.User;
import com.mvmlabs.service.UserService;
#RunWith(SpringJUnit4ClassRunner.class)
#SpringApplicationConfiguration(classes = Application.class)
public class ApplicationTests {
#Autowired
UserService userService;
#Test
public void versionNullCausesAnExceptionOnUpdate() throws Exception {
User user = new User();
user.setUsername("Version Null");
user.setNumberOfVisits(0);
user.setVersion(null);
user = userService.save(user);
user = userService.registerVisit(user);
Assert.assertEquals(new Integer(1), user.getNumberOfVisits());
Assert.assertEquals(new Long(1L), user.getVersion());
}
#Test
public void versionZeroCausesExceptionOnUpdate() throws Exception {
User user = new User();
user.setUsername("Version Zero");
user.setNumberOfVisits(0);
user.setVersion(0L);
user = userService.save(user);
user = userService.registerVisit(user);
Assert.assertEquals(new Integer(1), user.getNumberOfVisits());
Assert.assertEquals(new Long(1L), user.getVersion());
}
#Test
public void versionOneDoesNotCausesExceptionOnUpdate() throws Exception {
User user = new User();
user.setUsername("Version One");
user.setNumberOfVisits(0);
user.setVersion(1L);
user = userService.save(user);
user = userService.registerVisit(user);
Assert.assertEquals(new Integer(1), user.getNumberOfVisits());
Assert.assertEquals(new Long(2L), user.getVersion());
}
#Test
public void versionOneDoesNotCausesExceptionWithMultipleUpdates() throws Exception {
User user = new User();
user.setUsername("Version One Multiple");
user.setNumberOfVisits(0);
user.setVersion(1L);
user = userService.save(user);
user = userService.registerVisit(user);
user = userService.registerVisit(user);
user = userService.registerVisit(user);
Assert.assertEquals(new Integer(3), user.getNumberOfVisits());
Assert.assertEquals(new Long(4L), user.getVersion());
}
}
The first two tests fail with detached entity exception. The last two tests pass as expected.
Now change Spring Boot version to 1.1.4 and rerun, all tests pass.
Are my expectations wrong?
Edit: This code saved to GitHub at https://github.com/mmeany/spring-boot-detached-entity-issue
There is a problem with spring-data-jpa in release 1.6.2, this has been resolved in the spring-data-jpa 1.6.4-RELEASE. As soon as a Spring Boot update pulls in the new version of spring data JPA this will become a non-issue, until then override the version of spring-data-jpa in the POM.
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-jpa</artifactId>
<version>1.6.4.RELEASE</version>
</dependency>
Adding this to the test case fixes all issues, all tests pass as expected.

Resources