├── .gitignore ├── LICENSE ├── README.md ├── pom.xml └── src ├── main ├── java │ └── com │ │ └── khoubyari │ │ └── example │ │ ├── Application.java │ │ ├── RestControllerAspect.java │ │ ├── api │ │ └── rest │ │ │ ├── AbstractRestHandler.java │ │ │ ├── HotelController.java │ │ │ └── docs │ │ │ └── SwaggerConfig.java │ │ ├── dao │ │ └── jpa │ │ │ └── HotelRepository.java │ │ ├── domain │ │ ├── Hotel.java │ │ └── RestErrorInfo.java │ │ ├── exception │ │ ├── DataFormatException.java │ │ └── ResourceNotFoundException.java │ │ └── service │ │ ├── HotelService.java │ │ ├── HotelServiceEvent.java │ │ ├── HotelServiceHealth.java │ │ └── ServiceProperties.java └── resources │ └── application.yml └── test └── java └── com └── khoubyari └── example └── test └── HotelControllerTest.java /.gitignore: -------------------------------------------------------------------------------- 1 | *.DS_Store 2 | build/ 3 | target/ 4 | bin/ 5 | dependency-reduced-pom.xml 6 | .classpath 7 | .project 8 | .settings/ 9 | *.iml 10 | *.ipr 11 | *.iws 12 | *.idea 13 | *.log 14 | Thumbs.db 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | Copyright (c) 2014-2018 Siamak Khoubyari 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Spring Boot "Microservice" Example Project 2 | 3 | This is a sample Java / Maven / Spring Boot (version 1.5.6) application that can be used as a starter for creating a microservice complete with built-in health check, metrics and much more. I hope it helps you. 4 | 5 | ## How to Run 6 | 7 | This application is packaged as a war which has Tomcat 8 embedded. No Tomcat or JBoss installation is necessary. You run it using the ```java -jar``` command. 8 | 9 | * Clone this repository 10 | * Make sure you are using JDK 1.8 and Maven 3.x 11 | * You can build the project and run the tests by running ```mvn clean package``` 12 | * Once successfully built, you can run the service by one of these two methods: 13 | ``` 14 | java -jar -Dspring.profiles.active=test target/spring-boot-rest-example-0.5.0.war 15 | or 16 | mvn spring-boot:run -Drun.arguments="spring.profiles.active=test" 17 | ``` 18 | * Check the stdout or boot_example.log file to make sure no exceptions are thrown 19 | 20 | Once the application runs you should see something like this 21 | 22 | ``` 23 | 2017-08-29 17:31:23.091 INFO 19387 --- [ main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 8090 (http) 24 | 2017-08-29 17:31:23.097 INFO 19387 --- [ main] com.khoubyari.example.Application : Started Application in 22.285 seconds (JVM running for 23.032) 25 | ``` 26 | 27 | ## About the Service 28 | 29 | The service is just a simple hotel review REST service. It uses an in-memory database (H2) to store the data. You can also do with a relational database like MySQL or PostgreSQL. If your database connection properties work, you can call some REST endpoints defined in ```com.khoubyari.example.api.rest.hotelController``` on **port 8090**. (see below) 30 | 31 | More interestingly, you can start calling some of the operational endpoints (see full list below) like ```/metrics``` and ```/health``` (these are available on **port 8091**) 32 | 33 | You can use this sample service to understand the conventions and configurations that allow you to create a DB-backed RESTful service. Once you understand and get comfortable with the sample app you can add your own services following the same patterns as the sample service. 34 | 35 | Here is what this little application demonstrates: 36 | 37 | * Full integration with the latest **Spring** Framework: inversion of control, dependency injection, etc. 38 | * Packaging as a single war with embedded container (tomcat 8): No need to install a container separately on the host just run using the ``java -jar`` command 39 | * Demonstrates how to set up healthcheck, metrics, info, environment, etc. endpoints automatically on a configured port. Inject your own health / metrics info with a few lines of code. 40 | * Writing a RESTful service using annotation: supports both XML and JSON request / response; simply use desired ``Accept`` header in your request 41 | * Exception mapping from application exceptions to the right HTTP response with exception details in the body 42 | * *Spring Data* Integration with JPA/Hibernate with just a few lines of configuration and familiar annotations. 43 | * Automatic CRUD functionality against the data source using Spring *Repository* pattern 44 | * Demonstrates MockMVC test framework with associated libraries 45 | * All APIs are "self-documented" by Swagger2 using annotations 46 | 47 | Here are some endpoints you can call: 48 | 49 | ### Get information about system health, configurations, etc. 50 | 51 | ``` 52 | http://localhost:8091/env 53 | http://localhost:8091/health 54 | http://localhost:8091/info 55 | http://localhost:8091/metrics 56 | ``` 57 | 58 | ### Create a hotel resource 59 | 60 | ``` 61 | POST /example/v1/hotels 62 | Accept: application/json 63 | Content-Type: application/json 64 | 65 | { 66 | "name" : "Beds R Us", 67 | "description" : "Very basic, small rooms but clean", 68 | "city" : "Santa Ana", 69 | "rating" : 2 70 | } 71 | 72 | RESPONSE: HTTP 201 (Created) 73 | Location header: http://localhost:8090/example/v1/hotels/1 74 | ``` 75 | 76 | ### Retrieve a paginated list of hotels 77 | 78 | ``` 79 | http://localhost:8090/example/v1/hotels?page=0&size=10 80 | 81 | Response: HTTP 200 82 | Content: paginated list 83 | ``` 84 | 85 | ### Update a hotel resource 86 | 87 | ``` 88 | PUT /example/v1/hotels/1 89 | Accept: application/json 90 | Content-Type: application/json 91 | 92 | { 93 | "name" : "Beds R Us", 94 | "description" : "Very basic, small rooms but clean", 95 | "city" : "Santa Ana", 96 | "rating" : 3 97 | } 98 | 99 | RESPONSE: HTTP 204 (No Content) 100 | ``` 101 | ### To view Swagger 2 API docs 102 | 103 | Run the server and browse to localhost:8090/swagger-ui.html 104 | 105 | # About Spring Boot 106 | 107 | Spring Boot is an "opinionated" application bootstrapping framework that makes it easy to create new RESTful services (among other types of applications). It provides many of the usual Spring facilities that can be configured easily usually without any XML. In addition to easy set up of Spring Controllers, Spring Data, etc. Spring Boot comes with the Actuator module that gives the application the following endpoints helpful in monitoring and operating the service: 108 | 109 | **/metrics** Shows “metrics” information for the current application. 110 | 111 | **/health** Shows application health information. 112 | 113 | **/info** Displays arbitrary application info. 114 | 115 | **/configprops** Displays a collated list of all @ConfigurationProperties. 116 | 117 | **/mappings** Displays a collated list of all @RequestMapping paths. 118 | 119 | **/beans** Displays a complete list of all the Spring Beans in your application. 120 | 121 | **/env** Exposes properties from Spring’s ConfigurableEnvironment. 122 | 123 | **/trace** Displays trace information (by default the last few HTTP requests). 124 | 125 | ### To view your H2 in-memory datbase 126 | 127 | The 'test' profile runs on H2 in-memory database. To view and query the database you can browse to http://localhost:8090/h2-console. Default username is 'sa' with a blank password. Make sure you disable this in your production profiles. For more, see https://goo.gl/U8m62X 128 | 129 | # Running the project with MySQL 130 | 131 | This project uses an in-memory database so that you don't have to install a database in order to run it. However, converting it to run with another relational database such as MySQL or PostgreSQL is very easy. Since the project uses Spring Data and the Repository pattern, it's even fairly easy to back the same service with MongoDB. 132 | 133 | Here is what you would do to back the services with MySQL, for example: 134 | 135 | ### In pom.xml add: 136 | 137 | ``` 138 | 139 | mysql 140 | mysql-connector-java 141 | 142 | ``` 143 | 144 | ### Append this to the end of application.yml: 145 | 146 | ``` 147 | --- 148 | spring: 149 | profiles: mysql 150 | 151 | datasource: 152 | driverClassName: com.mysql.jdbc.Driver 153 | url: jdbc:mysql:///bootexample 154 | username: 155 | password: 156 | 157 | jpa: 158 | hibernate: 159 | dialect: org.hibernate.dialect.MySQLInnoDBDialect 160 | ddl-auto: update # todo: in non-dev environments, comment this out: 161 | 162 | 163 | hotel.service: 164 | name: 'test profile:' 165 | ``` 166 | 167 | ### Then run is using the 'mysql' profile: 168 | 169 | ``` 170 | java -jar -Dspring.profiles.active=mysql target/spring-boot-rest-example-0.5.0.war 171 | or 172 | mvn spring-boot:run -Drun.jvmArguments="-Dspring.profiles.active=mysql" 173 | ``` 174 | 175 | # Attaching to the app remotely from your IDE 176 | 177 | Run the service with these command line options: 178 | 179 | ``` 180 | mvn spring-boot:run -Drun.jvmArguments="-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=5005" 181 | or 182 | java -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005 -Dspring.profiles.active=test -Ddebug -jar target/spring-boot-rest-example-0.5.0.war 183 | ``` 184 | and then you can connect to it remotely using your IDE. For example, from IntelliJ You have to add remote debug configuration: Edit configuration -> Remote. 185 | 186 | # Star History 187 | 188 | [![Star History Chart](https://api.star-history.com/svg?repos=khoubyari/spring-boot-rest-example&type=Date)](https://star-history.com/#khoubyari/spring-boot-rest-example&Date) 189 | 190 | # Questions and Comments: khoubyari@gmail.com 191 | 192 | 193 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | com.khoubyari 7 | spring-boot-rest-example 8 | 0.5.0 9 | war 10 | Example project demonstrating REST APIs implemented using Spring Boot, in-memory database, embedded Tomcat, Swagger, JsonPath, Hamcrest and MockMVC 11 | 12 | 13 | org.springframework.boot 14 | spring-boot-starter-parent 15 | 1.5.9.RELEASE 16 | 17 | 18 | 19 | com.khoubyari.example.Application 20 | 21 | 22 | 23 | 24 | 25 | org.springframework.boot 26 | spring-boot-actuator 27 | 28 | 29 | 30 | 31 | org.springframework.boot 32 | spring-boot-starter-web 33 | 34 | 35 | 36 | 37 | org.springframework.boot 38 | spring-boot-starter-security 39 | 40 | 41 | 42 | org.springframework.boot 43 | spring-boot-starter-tomcat 44 | provided 45 | 46 | 47 | 48 | 49 | org.springframework.boot 50 | spring-boot-starter-data-jpa 51 | 52 | 53 | 54 | com.h2database 55 | h2 56 | 1.4.193 57 | 58 | 59 | 60 | 61 | org.springframework.boot 62 | spring-boot-starter-test 63 | test 64 | 65 | 66 | 67 | org.codehaus.jackson 68 | jackson-core-asl 69 | 1.9.13 70 | 71 | 72 | 73 | 74 | com.jayway.jsonpath 75 | json-path 76 | 2.4.0 77 | 78 | 79 | com.jayway.jsonpath 80 | json-path-assert 81 | 0.9.1 82 | test 83 | 84 | 85 | 86 | 87 | io.springfox 88 | springfox-swagger2 89 | 2.5.0 90 | 91 | 92 | io.springfox 93 | springfox-swagger-ui 94 | 2.5.0 95 | 96 | 97 | 98 | org.hsqldb 99 | hsqldb 100 | runtime 101 | 102 | 103 | 104 | 105 | javax.xml.bind 106 | jaxb-api 107 | 2.3.0 108 | 109 | 110 | 111 | 112 | 113 | 114 | src/main/resources 115 | true 116 | 117 | 118 | 119 | 120 | org.apache.maven.plugins 121 | maven-compiler-plugin 122 | 3.1 123 | 124 | 1.8 125 | 1.8 126 | 127 | 128 | 129 | 130 | org.springframework.boot 131 | spring-boot-maven-plugin 132 | 133 | false 134 | 135 | 136 | 137 | 138 | 139 | -------------------------------------------------------------------------------- /src/main/java/com/khoubyari/example/Application.java: -------------------------------------------------------------------------------- 1 | package com.khoubyari.example; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | import org.springframework.boot.SpringApplication; 6 | import org.springframework.boot.autoconfigure.EnableAutoConfiguration; 7 | import org.springframework.boot.autoconfigure.SpringBootApplication; 8 | import org.springframework.boot.builder.SpringApplicationBuilder; 9 | import org.springframework.boot.web.support.SpringBootServletInitializer; 10 | import org.springframework.context.annotation.ComponentScan; 11 | import org.springframework.context.annotation.Configuration; 12 | import org.springframework.data.jpa.repository.config.EnableJpaRepositories; 13 | 14 | import springfox.documentation.swagger2.annotations.EnableSwagger2; 15 | 16 | /* 17 | * This is the main Spring Boot application class. It configures Spring Boot, JPA, Swagger 18 | */ 19 | 20 | @SpringBootApplication 21 | @Configuration 22 | @EnableAutoConfiguration // Sprint Boot Auto Configuration 23 | @ComponentScan(basePackages = "com.khoubyari.example") 24 | @EnableJpaRepositories("com.khoubyari.example.dao.jpa") // To segregate MongoDB and JPA repositories. Otherwise not needed. 25 | public class Application extends SpringBootServletInitializer { 26 | 27 | private static final Class applicationClass = Application.class; 28 | private static final Logger log = LoggerFactory.getLogger(applicationClass); 29 | 30 | public static void main(String[] args) { 31 | SpringApplication.run(applicationClass, args); 32 | } 33 | 34 | @Override 35 | protected SpringApplicationBuilder configure(SpringApplicationBuilder application) { 36 | return application.sources(applicationClass); 37 | } 38 | 39 | 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/com/khoubyari/example/RestControllerAspect.java: -------------------------------------------------------------------------------- 1 | package com.khoubyari.example; 2 | 3 | import org.aspectj.lang.JoinPoint; 4 | import org.aspectj.lang.ProceedingJoinPoint; 5 | import org.aspectj.lang.annotation.Around; 6 | import org.aspectj.lang.annotation.Aspect; 7 | import org.aspectj.lang.annotation.Before; 8 | import org.aspectj.lang.annotation.Pointcut; 9 | import org.slf4j.Logger; 10 | import org.slf4j.LoggerFactory; 11 | import org.springframework.stereotype.Component; 12 | 13 | @Aspect 14 | @Component 15 | public class RestControllerAspect { 16 | 17 | protected final Logger log = LoggerFactory.getLogger(this.getClass()); 18 | 19 | @Before("execution(public * com.khoubyari.example.api.rest.*Controller.*(..))") 20 | public void logBeforeRestCall(JoinPoint pjp) throws Throwable { 21 | log.info(":::::AOP Before REST call:::::" + pjp); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/com/khoubyari/example/api/rest/AbstractRestHandler.java: -------------------------------------------------------------------------------- 1 | package com.khoubyari.example.api.rest; 2 | 3 | import com.khoubyari.example.domain.RestErrorInfo; 4 | import com.khoubyari.example.exception.DataFormatException; 5 | import com.khoubyari.example.exception.ResourceNotFoundException; 6 | import org.slf4j.Logger; 7 | import org.slf4j.LoggerFactory; 8 | import org.springframework.context.ApplicationEventPublisher; 9 | import org.springframework.context.ApplicationEventPublisherAware; 10 | import org.springframework.http.HttpStatus; 11 | import org.springframework.web.bind.annotation.ExceptionHandler; 12 | import org.springframework.web.bind.annotation.ResponseBody; 13 | import org.springframework.web.bind.annotation.ResponseStatus; 14 | import org.springframework.web.context.request.WebRequest; 15 | 16 | import javax.servlet.http.HttpServletResponse; 17 | 18 | /** 19 | * This class is meant to be extended by all REST resource "controllers". 20 | * It contains exception mapping and other common REST API functionality 21 | */ 22 | //@ControllerAdvice? 23 | public abstract class AbstractRestHandler implements ApplicationEventPublisherAware { 24 | 25 | protected final Logger log = LoggerFactory.getLogger(this.getClass()); 26 | protected ApplicationEventPublisher eventPublisher; 27 | 28 | protected static final String DEFAULT_PAGE_SIZE = "100"; 29 | protected static final String DEFAULT_PAGE_NUM = "0"; 30 | 31 | @ResponseStatus(HttpStatus.BAD_REQUEST) 32 | @ExceptionHandler(DataFormatException.class) 33 | public 34 | @ResponseBody 35 | RestErrorInfo handleDataStoreException(DataFormatException ex, WebRequest request, HttpServletResponse response) { 36 | log.info("Converting Data Store exception to RestResponse : " + ex.getMessage()); 37 | 38 | return new RestErrorInfo(ex, "You messed up."); 39 | } 40 | 41 | @ResponseStatus(HttpStatus.NOT_FOUND) 42 | @ExceptionHandler(ResourceNotFoundException.class) 43 | public 44 | @ResponseBody 45 | RestErrorInfo handleResourceNotFoundException(ResourceNotFoundException ex, WebRequest request, HttpServletResponse response) { 46 | log.info("ResourceNotFoundException handler:" + ex.getMessage()); 47 | 48 | return new RestErrorInfo(ex, "Sorry I couldn't find it."); 49 | } 50 | 51 | @Override 52 | public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) { 53 | this.eventPublisher = applicationEventPublisher; 54 | } 55 | 56 | //todo: replace with exception mapping 57 | public static T checkResourceFound(final T resource) { 58 | if (resource == null) { 59 | throw new ResourceNotFoundException("resource not found"); 60 | } 61 | return resource; 62 | } 63 | 64 | } -------------------------------------------------------------------------------- /src/main/java/com/khoubyari/example/api/rest/HotelController.java: -------------------------------------------------------------------------------- 1 | package com.khoubyari.example.api.rest; 2 | 3 | import io.swagger.annotations.Api; 4 | import io.swagger.annotations.ApiOperation; 5 | import io.swagger.annotations.ApiParam; 6 | 7 | import com.khoubyari.example.domain.Hotel; 8 | import com.khoubyari.example.exception.DataFormatException; 9 | import com.khoubyari.example.service.HotelService; 10 | 11 | import org.springframework.beans.factory.annotation.Autowired; 12 | import org.springframework.data.domain.Page; 13 | import org.springframework.http.HttpStatus; 14 | import org.springframework.web.bind.annotation.*; 15 | 16 | import javax.servlet.http.HttpServletRequest; 17 | import javax.servlet.http.HttpServletResponse; 18 | 19 | /* 20 | * Demonstrates how to set up RESTful API endpoints using Spring MVC 21 | */ 22 | 23 | @RestController 24 | @RequestMapping(value = "/example/v1/hotels") 25 | @Api(tags = {"hotels"}) 26 | public class HotelController extends AbstractRestHandler { 27 | 28 | @Autowired 29 | private HotelService hotelService; 30 | 31 | @RequestMapping(value = "", 32 | method = RequestMethod.POST, 33 | consumes = {"application/json", "application/xml"}, 34 | produces = {"application/json", "application/xml"}) 35 | @ResponseStatus(HttpStatus.CREATED) 36 | @ApiOperation(value = "Create a hotel resource.", notes = "Returns the URL of the new resource in the Location header.") 37 | public void createHotel(@RequestBody Hotel hotel, 38 | HttpServletRequest request, HttpServletResponse response) { 39 | Hotel createdHotel = this.hotelService.createHotel(hotel); 40 | response.setHeader("Location", request.getRequestURL().append("/").append(createdHotel.getId()).toString()); 41 | } 42 | 43 | @RequestMapping(value = "", 44 | method = RequestMethod.GET, 45 | produces = {"application/json", "application/xml"}) 46 | @ResponseStatus(HttpStatus.OK) 47 | @ApiOperation(value = "Get a paginated list of all hotels.", notes = "The list is paginated. You can provide a page number (default 0) and a page size (default 100)") 48 | public 49 | @ResponseBody 50 | Page getAllHotel(@ApiParam(value = "The page number (zero-based)", required = true) 51 | @RequestParam(value = "page", required = true, defaultValue = DEFAULT_PAGE_NUM) Integer page, 52 | @ApiParam(value = "Tha page size", required = true) 53 | @RequestParam(value = "size", required = true, defaultValue = DEFAULT_PAGE_SIZE) Integer size, 54 | HttpServletRequest request, HttpServletResponse response) { 55 | return this.hotelService.getAllHotels(page, size); 56 | } 57 | 58 | @RequestMapping(value = "/{id}", 59 | method = RequestMethod.GET, 60 | produces = {"application/json", "application/xml"}) 61 | @ResponseStatus(HttpStatus.OK) 62 | @ApiOperation(value = "Get a single hotel.", notes = "You have to provide a valid hotel ID.") 63 | public 64 | @ResponseBody 65 | Hotel getHotel(@ApiParam(value = "The ID of the hotel.", required = true) 66 | @PathVariable("id") Long id, 67 | HttpServletRequest request, HttpServletResponse response) throws Exception { 68 | Hotel hotel = this.hotelService.getHotel(id); 69 | checkResourceFound(hotel); 70 | //todo: http://goo.gl/6iNAkz 71 | return hotel; 72 | } 73 | 74 | @RequestMapping(value = "/{id}", 75 | method = RequestMethod.PUT, 76 | consumes = {"application/json", "application/xml"}, 77 | produces = {"application/json", "application/xml"}) 78 | @ResponseStatus(HttpStatus.NO_CONTENT) 79 | @ApiOperation(value = "Update a hotel resource.", notes = "You have to provide a valid hotel ID in the URL and in the payload. The ID attribute can not be updated.") 80 | public void updateHotel(@ApiParam(value = "The ID of the existing hotel resource.", required = true) 81 | @PathVariable("id") Long id, @RequestBody Hotel hotel, 82 | HttpServletRequest request, HttpServletResponse response) { 83 | checkResourceFound(this.hotelService.getHotel(id)); 84 | if (id != hotel.getId()) throw new DataFormatException("ID doesn't match!"); 85 | this.hotelService.updateHotel(hotel); 86 | } 87 | 88 | //todo: @ApiImplicitParams, @ApiResponses 89 | @RequestMapping(value = "/{id}", 90 | method = RequestMethod.DELETE, 91 | produces = {"application/json", "application/xml"}) 92 | @ResponseStatus(HttpStatus.NO_CONTENT) 93 | @ApiOperation(value = "Delete a hotel resource.", notes = "You have to provide a valid hotel ID in the URL. Once deleted the resource can not be recovered.") 94 | public void deleteHotel(@ApiParam(value = "The ID of the existing hotel resource.", required = true) 95 | @PathVariable("id") Long id, HttpServletRequest request, 96 | HttpServletResponse response) { 97 | checkResourceFound(this.hotelService.getHotel(id)); 98 | this.hotelService.deleteHotel(id); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/main/java/com/khoubyari/example/api/rest/docs/SwaggerConfig.java: -------------------------------------------------------------------------------- 1 | package com.khoubyari.example.api.rest.docs; 2 | 3 | import com.google.common.base.Predicates; 4 | 5 | import org.springframework.context.annotation.Bean; 6 | import org.springframework.context.annotation.ComponentScan; 7 | import org.springframework.context.annotation.Configuration; 8 | 9 | import springfox.documentation.builders.ApiInfoBuilder; 10 | import springfox.documentation.builders.PathSelectors; 11 | import springfox.documentation.builders.RequestHandlerSelectors; 12 | import springfox.documentation.service.ApiInfo; 13 | import springfox.documentation.spi.DocumentationType; 14 | import springfox.documentation.spring.web.plugins.Docket; 15 | import springfox.documentation.swagger2.annotations.EnableSwagger2; 16 | 17 | 18 | @Configuration 19 | @EnableSwagger2 20 | @ComponentScan("com.khoubyari.example.api.rest") 21 | public class SwaggerConfig { 22 | 23 | 24 | @Bean 25 | public Docket api() { 26 | return new Docket(DocumentationType.SWAGGER_2) 27 | .select() 28 | .apis(RequestHandlerSelectors.any()) 29 | .paths(Predicates.not(PathSelectors.regex("/error"))) 30 | .build() 31 | .apiInfo(apiInfo()); 32 | } 33 | 34 | 35 | private ApiInfo apiInfo() { 36 | String description = "REST example"; 37 | return new ApiInfoBuilder() 38 | .title("REST example") 39 | .description(description) 40 | .termsOfServiceUrl("github") 41 | .license("Siamak") 42 | .licenseUrl("") 43 | .version("1.0") 44 | // .contact(new Contact("siamak")) 45 | .build(); 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/com/khoubyari/example/dao/jpa/HotelRepository.java: -------------------------------------------------------------------------------- 1 | package com.khoubyari.example.dao.jpa; 2 | 3 | import com.khoubyari.example.domain.Hotel; 4 | import org.springframework.data.domain.Page; 5 | import org.springframework.data.domain.Pageable; 6 | import org.springframework.data.repository.PagingAndSortingRepository; 7 | 8 | /** 9 | * Repository can be used to delegate CRUD operations against the data source: http://goo.gl/P1J8QH 10 | */ 11 | public interface HotelRepository extends PagingAndSortingRepository { 12 | Hotel findHotelByCity(String city); 13 | Page findAll(Pageable pageable); 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/com/khoubyari/example/domain/Hotel.java: -------------------------------------------------------------------------------- 1 | package com.khoubyari.example.domain; 2 | 3 | import javax.persistence.*; 4 | import javax.xml.bind.annotation.*; 5 | 6 | /* 7 | * a simple domain entity doubling as a DTO 8 | */ 9 | @Entity 10 | @Table(name = "hotel") 11 | @XmlRootElement 12 | @XmlAccessorType(XmlAccessType.FIELD) 13 | public class Hotel { 14 | 15 | @Id 16 | @GeneratedValue() 17 | private long id; 18 | 19 | @Column(nullable = false) 20 | private String name; 21 | 22 | @Column() 23 | private String description; 24 | 25 | @Column() 26 | String city; 27 | 28 | @Column() 29 | private int rating; 30 | 31 | public Hotel() { 32 | } 33 | 34 | public Hotel(String name, String description, int rating) { 35 | this.name = name; 36 | this.description = description; 37 | this.rating = rating; 38 | } 39 | 40 | public long getId() { 41 | return this.id; 42 | } 43 | 44 | // for tests ONLY 45 | public void setId(long id) { 46 | this.id = id; 47 | } 48 | 49 | public String getName() { 50 | return name; 51 | } 52 | 53 | public void setName(String name) { 54 | this.name = name; 55 | } 56 | 57 | public String getDescription() { 58 | return description; 59 | } 60 | 61 | public void setDescription(String description) { 62 | this.description = description; 63 | } 64 | 65 | public int getRating() { 66 | return rating; 67 | } 68 | 69 | public void setRating(int rating) { 70 | this.rating = rating; 71 | } 72 | 73 | public String getCity() { 74 | return city; 75 | } 76 | 77 | public void setCity(String city) { 78 | this.city = city; 79 | } 80 | 81 | @Override 82 | public String toString() { 83 | return "Hotel {" + 84 | "id=" + id + 85 | ", name='" + name + '\'' + 86 | ", description='" + description + '\'' + 87 | ", city='" + city + '\'' + 88 | ", rating=" + rating + 89 | '}'; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/main/java/com/khoubyari/example/domain/RestErrorInfo.java: -------------------------------------------------------------------------------- 1 | package com.khoubyari.example.domain; 2 | 3 | import javax.xml.bind.annotation.XmlRootElement; 4 | 5 | /* 6 | * A sample class for adding error information in the response 7 | */ 8 | @XmlRootElement 9 | public class RestErrorInfo { 10 | public final String detail; 11 | public final String message; 12 | 13 | public RestErrorInfo(Exception ex, String detail) { 14 | this.message = ex.getLocalizedMessage(); 15 | this.detail = detail; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/khoubyari/example/exception/DataFormatException.java: -------------------------------------------------------------------------------- 1 | package com.khoubyari.example.exception; 2 | 3 | /** 4 | * for HTTP 400 errors 5 | */ 6 | public final class DataFormatException extends RuntimeException { 7 | public DataFormatException() { 8 | super(); 9 | } 10 | 11 | public DataFormatException(String message, Throwable cause) { 12 | super(message, cause); 13 | } 14 | 15 | public DataFormatException(String message) { 16 | super(message); 17 | } 18 | 19 | public DataFormatException(Throwable cause) { 20 | super(cause); 21 | } 22 | } -------------------------------------------------------------------------------- /src/main/java/com/khoubyari/example/exception/ResourceNotFoundException.java: -------------------------------------------------------------------------------- 1 | package com.khoubyari.example.exception; 2 | 3 | /** 4 | * For HTTP 404 errros 5 | */ 6 | public class ResourceNotFoundException extends RuntimeException { 7 | public ResourceNotFoundException() { 8 | super(); 9 | } 10 | 11 | public ResourceNotFoundException(String message, Throwable cause) { 12 | super(message, cause); 13 | } 14 | 15 | public ResourceNotFoundException(String message) { 16 | super(message); 17 | } 18 | 19 | public ResourceNotFoundException(Throwable cause) { 20 | super(cause); 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/com/khoubyari/example/service/HotelService.java: -------------------------------------------------------------------------------- 1 | package com.khoubyari.example.service; 2 | 3 | import com.khoubyari.example.domain.Hotel; 4 | import com.khoubyari.example.dao.jpa.HotelRepository; 5 | import org.slf4j.Logger; 6 | import org.slf4j.LoggerFactory; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.boot.actuate.metrics.CounterService; 9 | import org.springframework.boot.actuate.metrics.GaugeService; 10 | import org.springframework.data.domain.Page; 11 | import org.springframework.data.domain.PageRequest; 12 | import org.springframework.stereotype.Service; 13 | 14 | /* 15 | * Sample service to demonstrate what the API would use to get things done 16 | */ 17 | @Service 18 | public class HotelService { 19 | 20 | private static final Logger log = LoggerFactory.getLogger(HotelService.class); 21 | 22 | @Autowired 23 | private HotelRepository hotelRepository; 24 | 25 | @Autowired 26 | CounterService counterService; 27 | 28 | @Autowired 29 | GaugeService gaugeService; 30 | 31 | public HotelService() { 32 | } 33 | 34 | public Hotel createHotel(Hotel hotel) { 35 | return hotelRepository.save(hotel); 36 | } 37 | 38 | public Hotel getHotel(long id) { 39 | return hotelRepository.findOne(id); 40 | } 41 | 42 | public void updateHotel(Hotel hotel) { 43 | hotelRepository.save(hotel); 44 | } 45 | 46 | public void deleteHotel(Long id) { 47 | hotelRepository.delete(id); 48 | } 49 | 50 | //http://goo.gl/7fxvVf 51 | public Page getAllHotels(Integer page, Integer size) { 52 | Page pageOfHotels = hotelRepository.findAll(new PageRequest(page, size)); 53 | // example of adding to the /metrics 54 | if (size > 50) { 55 | counterService.increment("Khoubyari.HotelService.getAll.largePayload"); 56 | } 57 | return pageOfHotels; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/main/java/com/khoubyari/example/service/HotelServiceEvent.java: -------------------------------------------------------------------------------- 1 | package com.khoubyari.example.service; 2 | 3 | import org.springframework.context.ApplicationEvent; 4 | 5 | /** 6 | * This is an optional class used in publishing application events. 7 | * This can be used to inject events into the Spring Boot audit management endpoint. 8 | */ 9 | public class HotelServiceEvent extends ApplicationEvent { 10 | 11 | public HotelServiceEvent(Object source) { 12 | super(source); 13 | } 14 | 15 | public String toString() { 16 | return "My HotelService Event"; 17 | } 18 | } -------------------------------------------------------------------------------- /src/main/java/com/khoubyari/example/service/HotelServiceHealth.java: -------------------------------------------------------------------------------- 1 | package com.khoubyari.example.service; 2 | 3 | 4 | import org.springframework.beans.factory.annotation.Autowired; 5 | import org.springframework.boot.actuate.health.Health; 6 | import org.springframework.boot.actuate.health.HealthIndicator; 7 | import org.springframework.stereotype.Component; 8 | 9 | /** 10 | * This is an optional class used to inject application specific health check 11 | * into the Spring Boot health management endpoint. 12 | */ 13 | @Component 14 | public class HotelServiceHealth implements HealthIndicator { 15 | 16 | @Autowired 17 | private ServiceProperties configuration; 18 | 19 | // extend this to create an application-specific health check according to http://goo.gl/vt8I7O 20 | @Override 21 | public Health health() { 22 | return Health.up().withDetail("details", "{ 'internals' : 'getting close to limit', 'profile' : '" + this.configuration.getName() + "' }").status("itsok!").build(); 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/com/khoubyari/example/service/ServiceProperties.java: -------------------------------------------------------------------------------- 1 | package com.khoubyari.example.service; 2 | 3 | import org.springframework.boot.context.properties.ConfigurationProperties; 4 | import org.springframework.stereotype.Component; 5 | 6 | import javax.validation.constraints.NotNull; 7 | 8 | /* 9 | * demonstrates how service-specific properties can be injected 10 | */ 11 | @ConfigurationProperties(prefix = "hotel.service", ignoreUnknownFields = false) 12 | @Component 13 | public class ServiceProperties { 14 | 15 | @NotNull // you can also create configurationPropertiesValidator 16 | private String name = "Empty"; 17 | 18 | public String getName() { 19 | return this.name; 20 | } 21 | 22 | public void setName(String name) { 23 | this.name = name; 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | ### This is the main way to configure the application (other than annotations). 2 | ### This file is in Yaml format but you can also configure spring boot using the traditional 3 | ### Java properties file format. 4 | 5 | spring.jmx: 6 | enabled: false 7 | 8 | spring.datasource: 9 | driverClassName: org.h2.Driver 10 | url: jdbc:h2:mem:bootexample;MODE=MySQL 11 | 12 | server: 13 | port: 8090 14 | 15 | #todo: make sure to always enable security in production 16 | security: 17 | basic: 18 | enabled: false 19 | 20 | #management endpoints on a separate port 21 | management: 22 | port: 8091 23 | security: 24 | enabled: false # management port is internal only. no need to secure it. 25 | 26 | #default project info followed by actual injected pom-specified values. 27 | project: 28 | name: spring-boot-rest-example 29 | version: 0.1 30 | description: boot-example default description 31 | info: 32 | build: 33 | artifact: ${project.artifactId} 34 | name: ${project.name} 35 | description: ${project.description} 36 | version: ${project.version} 37 | 38 | hotel.service: 39 | name: 'default profile:' 40 | --- 41 | spring: 42 | profiles: test 43 | h2: 44 | console: 45 | enabled: true 46 | 47 | spring.jpa: 48 | hibernate.ddl-auto: create-drop 49 | 50 | hotel.service: 51 | name: 'test profile:' 52 | 53 | logging: 54 | file: boot_example.log 55 | org.hibernate: INFO 56 | 57 | -------------------------------------------------------------------------------- /src/test/java/com/khoubyari/example/test/HotelControllerTest.java: -------------------------------------------------------------------------------- 1 | package com.khoubyari.example.test; 2 | 3 | /** 4 | * Uses JsonPath: http://goo.gl/nwXpb, Hamcrest and MockMVC 5 | */ 6 | 7 | import com.fasterxml.jackson.databind.ObjectMapper; 8 | import com.khoubyari.example.Application; 9 | import com.khoubyari.example.api.rest.HotelController; 10 | import com.khoubyari.example.domain.Hotel; 11 | 12 | import org.junit.Before; 13 | import org.junit.Test; 14 | import org.junit.runner.RunWith; 15 | import org.mockito.InjectMocks; 16 | import org.mockito.MockitoAnnotations; 17 | import org.springframework.beans.factory.annotation.Autowired; 18 | import org.springframework.boot.test.context.SpringBootTest; 19 | import org.springframework.http.MediaType; 20 | import org.springframework.test.context.ActiveProfiles; 21 | import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; 22 | import org.springframework.test.web.servlet.MockMvc; 23 | import org.springframework.test.web.servlet.MvcResult; 24 | import org.springframework.test.web.servlet.ResultMatcher; 25 | import org.springframework.test.web.servlet.setup.MockMvcBuilders; 26 | import org.springframework.web.context.WebApplicationContext; 27 | 28 | import java.util.Random; 29 | import java.util.regex.Pattern; 30 | 31 | import static org.hamcrest.Matchers.*; 32 | import static org.junit.Assert.assertTrue; 33 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; 34 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; 35 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; 36 | 37 | @RunWith(SpringJUnit4ClassRunner.class) 38 | @SpringBootTest(classes = Application.class) 39 | @ActiveProfiles("test") 40 | public class HotelControllerTest { 41 | 42 | private static final String RESOURCE_LOCATION_PATTERN = "http://localhost/example/v1/hotels/[0-9]+"; 43 | 44 | @InjectMocks 45 | HotelController controller; 46 | 47 | @Autowired 48 | WebApplicationContext context; 49 | 50 | private MockMvc mvc; 51 | 52 | @Before 53 | public void initTests() { 54 | MockitoAnnotations.initMocks(this); 55 | mvc = MockMvcBuilders.webAppContextSetup(context).build(); 56 | } 57 | 58 | //@Test 59 | public void shouldHaveEmptyDB() throws Exception { 60 | mvc.perform(get("/example/v1/hotels") 61 | .accept(MediaType.APPLICATION_JSON)) 62 | .andExpect(status().isOk()) 63 | .andExpect(jsonPath("$", hasSize(0))); 64 | } 65 | 66 | @Test 67 | public void shouldCreateRetrieveDelete() throws Exception { 68 | Hotel r1 = mockHotel("shouldCreateRetrieveDelete"); 69 | byte[] r1Json = toJson(r1); 70 | 71 | //CREATE 72 | MvcResult result = mvc.perform(post("/example/v1/hotels") 73 | .content(r1Json) 74 | .contentType(MediaType.APPLICATION_JSON) 75 | .accept(MediaType.APPLICATION_JSON)) 76 | .andExpect(status().isCreated()) 77 | .andExpect(redirectedUrlPattern(RESOURCE_LOCATION_PATTERN)) 78 | .andReturn(); 79 | long id = getResourceIdFromUrl(result.getResponse().getRedirectedUrl()); 80 | 81 | //RETRIEVE 82 | mvc.perform(get("/example/v1/hotels/" + id) 83 | .accept(MediaType.APPLICATION_JSON)) 84 | .andExpect(status().isOk()) 85 | .andExpect(jsonPath("$.id", is((int) id))) 86 | .andExpect(jsonPath("$.name", is(r1.getName()))) 87 | .andExpect(jsonPath("$.city", is(r1.getCity()))) 88 | .andExpect(jsonPath("$.description", is(r1.getDescription()))) 89 | .andExpect(jsonPath("$.rating", is(r1.getRating()))); 90 | 91 | //DELETE 92 | mvc.perform(delete("/example/v1/hotels/" + id)) 93 | .andExpect(status().isNoContent()); 94 | 95 | //RETRIEVE should fail 96 | mvc.perform(get("/example/v1/hotels/" + id) 97 | .accept(MediaType.APPLICATION_JSON)) 98 | .andExpect(status().isNotFound()); 99 | 100 | //todo: you can test the 404 error body too. 101 | 102 | /* 103 | JSONAssert.assertEquals( 104 | "{foo: 'bar', baz: 'qux'}", 105 | JSONObject.fromObject("{foo: 'bar', baz: 'xyzzy'}")); 106 | */ 107 | } 108 | 109 | @Test 110 | public void shouldCreateAndUpdateAndDelete() throws Exception { 111 | Hotel r1 = mockHotel("shouldCreateAndUpdate"); 112 | byte[] r1Json = toJson(r1); 113 | //CREATE 114 | MvcResult result = mvc.perform(post("/example/v1/hotels") 115 | .content(r1Json) 116 | .contentType(MediaType.APPLICATION_JSON) 117 | .accept(MediaType.APPLICATION_JSON)) 118 | .andExpect(status().isCreated()) 119 | .andExpect(redirectedUrlPattern(RESOURCE_LOCATION_PATTERN)) 120 | .andReturn(); 121 | long id = getResourceIdFromUrl(result.getResponse().getRedirectedUrl()); 122 | 123 | Hotel r2 = mockHotel("shouldCreateAndUpdate2"); 124 | r2.setId(id); 125 | byte[] r2Json = toJson(r2); 126 | 127 | //UPDATE 128 | result = mvc.perform(put("/example/v1/hotels/" + id) 129 | .content(r2Json) 130 | .contentType(MediaType.APPLICATION_JSON) 131 | .accept(MediaType.APPLICATION_JSON)) 132 | .andExpect(status().isNoContent()) 133 | .andReturn(); 134 | 135 | //RETRIEVE updated 136 | mvc.perform(get("/example/v1/hotels/" + id) 137 | .accept(MediaType.APPLICATION_JSON)) 138 | .andExpect(status().isOk()) 139 | .andExpect(jsonPath("$.id", is((int) id))) 140 | .andExpect(jsonPath("$.name", is(r2.getName()))) 141 | .andExpect(jsonPath("$.city", is(r2.getCity()))) 142 | .andExpect(jsonPath("$.description", is(r2.getDescription()))) 143 | .andExpect(jsonPath("$.rating", is(r2.getRating()))); 144 | 145 | //DELETE 146 | mvc.perform(delete("/example/v1/hotels/" + id)) 147 | .andExpect(status().isNoContent()); 148 | } 149 | 150 | 151 | /* 152 | ****************************** 153 | */ 154 | 155 | private long getResourceIdFromUrl(String locationUrl) { 156 | String[] parts = locationUrl.split("/"); 157 | return Long.valueOf(parts[parts.length - 1]); 158 | } 159 | 160 | 161 | private Hotel mockHotel(String prefix) { 162 | Hotel r = new Hotel(); 163 | r.setCity(prefix + "_city"); 164 | r.setDescription(prefix + "_description"); 165 | r.setName(prefix + "_name"); 166 | r.setRating(new Random().nextInt(6)); 167 | return r; 168 | } 169 | 170 | private byte[] toJson(Object r) throws Exception { 171 | ObjectMapper map = new ObjectMapper(); 172 | return map.writeValueAsString(r).getBytes(); 173 | } 174 | 175 | // match redirect header URL (aka Location header) 176 | private static ResultMatcher redirectedUrlPattern(final String expectedUrlPattern) { 177 | return new ResultMatcher() { 178 | public void match(MvcResult result) { 179 | Pattern pattern = Pattern.compile("\\A" + expectedUrlPattern + "\\z"); 180 | assertTrue(pattern.matcher(result.getResponse().getRedirectedUrl()).find()); 181 | } 182 | }; 183 | } 184 | 185 | } 186 | --------------------------------------------------------------------------------