Building Simple Web Service with Spring Boot
Make sure you have your development environment configured. |
In this exercise you are going to create a simple REST web service using Spring Boot.
You can skip this step if you feel you have a good understanding of Spring Boot. The rest of the course assume you have a good knowledge of the principles of Spring Boot. |
Step-by-step instructions to bootstrap the project
-
Go to Spring Starter https://start.spring.io/
-
Select the following options:
-
Gradle Project
-
Spring Boot 1.3.2
-
Group: msvcdojo
-
Artifact: mysvc
-
Select in "Search for Dependencies":
-
Web
-
Actuator
-
Actuator Docs
-
-
Click button Generate Project
-
-
Download project and expand the archive
-
Open in IntelliJ as a Gradle project
Let’s explore. modify and configure
The project contains an entry point in the MysvcApplication.java file:
@SpringBootApplication
public class MysvcApplication {
public static void main(String[] args) {
SpringApplication.run(MysvcApplication.class, args);
}
}
We’re going to add a basic REST controller by creating the following class in MysvcApplication.java file:
@RestController
class HomeController {
@RequestMapping("/")
String home() {
return "Hello World!";
}
}
Going forward we’re going to use YAML files to provide properties for our services. Rename the mysvc/src/main/resources/application.properties file into mysvc/src/main/resources/application.yml. Let’s expose additional information in our healthcheck output. Put the following in the application.yml file:
server:
port: 8100
endpoints:
health:
sensitive: false
shutdown:
enabled: true
Let’s replace Tomcat, that was provided as a default application container, with Jetty. Replace the following line in mysvc/build.gradle:
compile('org.springframework.boot:spring-boot-starter-web')
With the following block:
compile("org.springframework.boot:spring-boot-starter-web:$springBootVersion") {
exclude group: 'org.springframework.boot', module: 'spring-boot-starter-tomcat'
}
compile ('org.springframework.boot:spring-boot-starter-jetty') {
exclude group: 'org.eclipse.jetty.websocket'
}
And add the Actuator and the Actuator Docs lines:
compile('org.springframework.boot:spring-boot-starter-actuator')
compile('org.springframework.boot:spring-boot-actuator-docs')
What about tests?
Let’s add a unit test that validates the controller:
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = MockServletContext.class)
@WebAppConfiguration
public class HomeControllerTest {
private MockMvc mvc;
@Before
public void setUp() throws Exception {
mvc = MockMvcBuilders.standaloneSetup(new HomeController()).build();
}
@Test
public void getHello() throws Exception {
mvc.perform(MockMvcRequestBuilders.get("/").accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(content().string(equalTo("Hello World!")));
}
}
Let’s add some integration test:
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = MysvcApplication.class)
@WebAppConfiguration
@IntegrationTest({"server.port=0"})
public class HomeControllerIT {
@Value("${local.server.port}")
private int port;
private URL base;
private RestTemplate template;
@Before
public void setUp() throws Exception {
this.base = new URL("http://localhost:" + port + "/");
template = new TestRestTemplate();
}
@Test
public void getHello() throws Exception {
ResponseEntity<String> response = template.getForEntity(base.toString(), String.class);
assertThat(response.getBody(), equalTo("Hello World!"));
}
}
We’d like to ask JUnit to generate reports for us. Add the following to build.gradle:
test {
testLogging {
afterSuite { desc, result ->
if (!desc.parent) {
println "Results: ${result.resultType} (${result.testCount} tests, ${result.successfulTestCount} successes, ${result.failedTestCount} failures, ${result.skippedTestCount} skipped)"
}
}
}
reports {
junitXml.enabled = false
html.enabled = true
}
}
Now it’s time to run the tests:
$ gradlew test ... Results: SUCCESS (2 tests, 2 successes, 0 failures, 0 skipped)
And, in case you’d like to get a Web report, open the following file in your browser:
build\reports\tests\index.html
Build and launch
Build the service:
$ cd mysvc $ gradlew build
Note that this command will build the code and run all the necessary tests.
Lauch it the service:
$ java -jar build\libs\mysvc-0.0.1.jar
The following output you’ll on the screen.
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v1.3.2.RELEASE)
2016-01-23 [...] msvcdojo.MysvcApplication : Starting MysvcApplication on DESKTOP-OF8L4HQ with PID 14404 (C:\Dev\accordance\microservice-dojo\kata1\solution\mysvc\build\classes\main started by igorm in C:\Dev\accordance\microservice-dojo\kata1\solution\mysvc)
2016-01-23 [...] msvcdojo.MysvcApplication : No active profile set, falling back to default profiles: default
2016-01-23 [...] ationConfigEmbeddedWebApplicationContext : Refreshing org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@548a102f: startup date [Sat Jan 23 15:05:00 EST 2016]; root of context hierarchy
2016-01-23 [...] o.s.b.f.s.DefaultListableBeanFactory : Overriding bean definition for bean 'beanNameViewResolver' with a different definition: replacing [Root bean: class [null]; scope=; abstract=false; lazyInit=false; autowireMode=3; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=org.springframework.boot.autoconfigure.web.ErrorMvcAutoConfiguration$WhitelabelErrorViewConfiguration; factoryMethodName=beanNameViewResolver; initMethodName=null; destroyMethodName=(inferred); defined in class path resource [org/springframework/boot/autoconfigure/web/ErrorMvcAutoConfiguration$WhitelabelErrorViewConfiguration.class]] with [Root bean: class [null]; scope=; abstract=false; lazyInit=false; autowireMode=3; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration$WebMvcAutoConfigurationAdapter; factoryMethodName=beanNameViewResolver; initMethodName=null; destroyMethodName=(inferred); defined in class path resource [org/springframework/boot/autoconfigure/web/WebMvcAutoConfiguration$WebMvcAutoConfigurationAdapter.class]]
2016-01-23 [...] o.s.b.f.s.DefaultListableBeanFactory : Overriding bean definition for bean 'managementServletContext' with a different definition: replacing [Root bean: class [null]; scope=; abstract=false; lazyInit=false; autowireMode=3; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=org.springframework.boot.actuate.autoconfigure.EndpointWebMvcHypermediaManagementContextConfiguration; factoryMethodName=managementServletContext; initMethodName=null; destroyMethodName=(inferred); defined in class path resource [org/springframework/boot/actuate/autoconfigure/EndpointWebMvcHypermediaManagementContextConfiguration.class]] with [Root bean: class [null]; scope=; abstract=false; lazyInit=false; autowireMode=3; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=org.springframework.boot.actuate.autoconfigure.EndpointWebMvcAutoConfiguration; factoryMethodName=managementServletContext; initMethodName=null; destroyMethodName=(inferred); defined in class path resource [org/springframework/boot/actuate/autoconfigure/EndpointWebMvcAutoConfiguration.class]]
2016-01-23 [...] org.eclipse.jetty.util.log : Logging initialized @3152ms
(1)
2016-01-23 [...] e.j.JettyEmbeddedServletContainerFactory : Server initialized with port: 8100
2016-01-23 [...] org.eclipse.jetty.server.Server : jetty-9.2.14.v20151106
2016-01-23 [...] application : Initializing Spring embedded WebApplicationContext
2016-01-23 [...] o.s.web.context.ContextLoader : Root WebApplicationContext: initialization completed in 2438 ms
2016-01-23 [...] o.s.b.c.e.ServletRegistrationBean : Mapping servlet: 'dispatcherServlet' to [/]
2016-01-23 [...] o.s.b.c.embedded.FilterRegistrationBean : Mapping filter: 'metricFilter' to: [/*]
2016-01-23 [...] o.s.b.c.embedded.FilterRegistrationBean : Mapping filter: 'characterEncodingFilter' to: [/*]
2016-01-23 [...] o.s.b.c.embedded.FilterRegistrationBean : Mapping filter: 'hiddenHttpMethodFilter' to: [/*]
2016-01-23 [...] o.s.b.c.embedded.FilterRegistrationBean : Mapping filter: 'httpPutFormContentFilter' to: [/*]
2016-01-23 [...] o.s.b.c.embedded.FilterRegistrationBean : Mapping filter: 'requestContextFilter' to: [/*]
2016-01-23 [...] o.s.b.c.embedded.FilterRegistrationBean : Mapping filter: 'webRequestLoggingFilter' to: [/*]
2016-01-23 [...] o.s.b.c.embedded.FilterRegistrationBean : Mapping filter: 'applicationContextIdFilter' to: [/*]
2016-01-23 [...] o.e.jetty.server.handler.ContextHandler : Started o.s.b.c.e.j.JettyEmbeddedWebAppContext@150d80c4{/,file:/C:/Users/igorm/AppData/Local/Temp/jetty-docbase.1081708721977203444.8080/,AVAILABLE}
2016-01-23 [...] org.eclipse.jetty.server.Server : Started @4211ms
2016-01-23 [...] s.w.s.m.m.a.RequestMappingHandlerAdapter : Looking for @ControllerAdvice: org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@548a102f: startup date [Sat Jan 23 15:05:00 EST 2016]; root of context hierarchy
2016-01-23 [...] s.w.s.m.m.a.RequestMappingHandlerAdapter : Detected ResponseBodyAdvice bean in org.springframework.boot.actuate.autoconfigure.EndpointWebMvcHypermediaManagementContextConfiguration$ActuatorEndpointLinksAdvice
(2)
2016-01-23 [...] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error]}" onto public org.springframework.http.ResponseEntity<java.util.Map<java.lang.String, java.lang.Object>> org.springframework.boot.autoconfigure.web.BasicErrorController.error(javax.servlet.http.HttpServletRequest)
2016-01-23 [...] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error],produces=[text/html]}" onto public org.springframework.web.servlet.ModelAndView org.springframework.boot.autoconfigure.web.BasicErrorController.errorHtml(javax.servlet.http.HttpServletRequest,javax.servlet.http.HttpServletResponse)
2016-01-23 [...] o.s.w.s.handler.SimpleUrlHandlerMapping : Mapped URL path [/webjars/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2016-01-23 [...] o.s.w.s.handler.SimpleUrlHandlerMapping : Mapped URL path [/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2016-01-23 [...] o.s.w.s.handler.SimpleUrlHandlerMapping : Mapped URL path [/docs/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2016-01-23 [...] .m.m.a.ExceptionHandlerExceptionResolver : Detected ResponseBodyAdvice implementation in org.springframework.boot.actuate.autoconfigure.EndpointWebMvcHypermediaManagementContextConfiguration$ActuatorEndpointLinksAdvice
2016-01-23 [...] o.s.w.s.handler.SimpleUrlHandlerMapping : Mapped URL path [/**/favicon.ico] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2016-01-23 [...] o.s.b.a.e.mvc.EndpointHandlerMapping : Mapped "{[/actuator || /actuator.json],produces=[application/json]}" onto public org.springframework.hateoas.ResourceSupport org.springframework.boot.actuate.endpoint.mvc.HalJsonMvcEndpoint.links()
2016-01-23 [...] o.s.b.a.e.mvc.EndpointHandlerMapping : Mapped "{[/env/{name:.*}],methods=[GET],produces=[application/json]}" onto public java.lang.Object org.springframework.boot.actuate.endpoint.mvc.EnvironmentMvcEndpoint.value(java.lang.String)
2016-01-23 [...] o.s.b.a.e.mvc.EndpointHandlerMapping : Mapped "{[/env || /env.json],methods=[GET],produces=[application/json]}" onto public java.lang.Object org.springframework.boot.actuate.endpoint.mvc.EndpointMvcAdapter.invoke()
2016-01-23 [...] o.s.b.a.e.mvc.EndpointHandlerMapping : Mapped "{[/metrics/{name:.*}],methods=[GET],produces=[application/json]}" onto public java.lang.Object org.springframework.boot.actuate.endpoint.mvc.MetricsMvcEndpoint.value(java.lang.String)
2016-01-23 [...] o.s.b.a.e.mvc.EndpointHandlerMapping : Mapped "{[/metrics || /metrics.json],methods=[GET],produces=[application/json]}" onto public java.lang.Object org.springframework.boot.actuate.endpoint.mvc.EndpointMvcAdapter.invoke()
2016-01-23 [...] o.s.b.a.e.mvc.EndpointHandlerMapping : Mapped "{[/info || /info.json],methods=[GET],produces=[application/json]}" onto public java.lang.Object org.springframework.boot.actuate.endpoint.mvc.EndpointMvcAdapter.invoke()
2016-01-23 [...] o.s.b.a.e.mvc.EndpointHandlerMapping : Mapped "{[/configprops || /configprops.json],methods=[GET],produces=[application/json]}" onto public java.lang.Object org.springframework.boot.actuate.endpoint.mvc.EndpointMvcAdapter.invoke()
2016-01-23 [...] o.s.b.a.e.mvc.EndpointHandlerMapping : Mapped "{[/health || /health.json],produces=[application/json]}" onto public java.lang.Object org.springframework.boot.actuate.endpoint.mvc.HealthMvcEndpoint.invoke(java.security.Principal)
2016-01-23 [...] o.s.b.a.e.mvc.EndpointHandlerMapping : Mapped "{[/dump || /dump.json],methods=[GET],produces=[application/json]}" onto public java.lang.Object org.springframework.boot.actuate.endpoint.mvc.EndpointMvcAdapter.invoke()
2016-01-23 [...] o.s.b.a.e.mvc.EndpointHandlerMapping : Mapped "{[/docs],produces=[text/html]}" onto public java.lang.String org.springframework.boot.actuate.endpoint.mvc.DocsMvcEndpoint.redirect()
2016-01-23 [...] o.s.b.a.e.mvc.EndpointHandlerMapping : Mapped "{[/docs/],produces=[text/html]}" onto public java.lang.String org.springframework.boot.actuate.endpoint.mvc.DocsMvcEndpoint.browse()
2016-01-23 [...] o.s.b.a.e.mvc.EndpointHandlerMapping : Mapped "{[/beans || /beans.json],methods=[GET],produces=[application/json]}" onto public java.lang.Object org.springframework.boot.actuate.endpoint.mvc.EndpointMvcAdapter.invoke()
2016-01-23 [...] o.s.b.a.e.mvc.EndpointHandlerMapping : Mapped "{[/autoconfig || /autoconfig.json],methods=[GET],produces=[application/json]}" onto public java.lang.Object org.springframework.boot.actuate.endpoint.mvc.EndpointMvcAdapter.invoke()
2016-01-23 [...] o.s.b.a.e.mvc.EndpointHandlerMapping : Mapped "{[/mappings || /mappings.json],methods=[GET],produces=[application/json]}" onto public java.lang.Object org.springframework.boot.actuate.endpoint.mvc.EndpointMvcAdapter.invoke()
2016-01-23 [...] o.s.b.a.e.mvc.EndpointHandlerMapping : Mapped "{[/trace || /trace.json],methods=[GET],produces=[application/json]}" onto public java.lang.Object org.springframework.boot.actuate.endpoint.mvc.EndpointMvcAdapter.invoke()
2016-01-23 [...] o.s.j.e.a.AnnotationMBeanExporter : Registering beans for JMX exposure on startup
2016-01-23 [...] o.s.c.support.DefaultLifecycleProcessor : Starting beans in phase 0
2016-01-23 [...] application : Initializing Spring FrameworkServlet 'dispatcherServlet'
2016-01-23 [...] o.s.web.servlet.DispatcherServlet : FrameworkServlet 'dispatcherServlet': initialization started
2016-01-23 [...] o.s.web.servlet.DispatcherServlet : FrameworkServlet 'dispatcherServlet': initialization completed in 27 ms
(1)
2016-01-23 [...] o.eclipse.jetty.server.ServerConnector : Started ServerConnector@4f67e3df{HTTP/1.1}{0.0.0.0:8100}
2016-01-23 [...] .s.b.c.e.j.JettyEmbeddedServletContainer : Jetty started on port(s) 8100 (http/1.1)
2016-01-23 [...] msvcdojo.MysvcApplication : Started MysvcApplication in 5.586 seconds (JVM running for 6.144)
1 | Listening port of the web service |
2 | List of available end-points that Actuator provides for you for free. |
Play time
Healthcheck
Let’s health-check our brand new service:
$ curl http://localhost:8080/health
The output of this CURL command will look like this:
{
"status": "UP",
"diskSpace": {
"status": "UP",
"total": 255219200000,
"free": 70534836224,
"threshold": 10485760
}
}
See that the status value is UP. This will be our indication that the service is healthy. This endpoint can be extended and your application can provide more or less information based on the needs of your projects.
Shutdown
Actuator is not enabling a couple of "dangerous" end-points. One of them is the one that the automation infrastructure may use to shutdown the service.
You can send an empty POST request to this endpoint for the service gracefully shutdown itself.
$ curl -d {} http://localhost:8080/shutdown
End-point documentation
The fact that the Actuator-docs package was included in your artifact, your service has received an additional end point that provides embedded documentation for the other actuator-provided end points.
You can access this documentation by navigating to the following url: http://localhost:8080/docs/
Going forward you will not need this dependency so you can remove this line from the build.gradle:
compile('org.springframework.boot:spring-boot-actuator-docs')
Bonus
You can read more about all this: