SpringBoot is a really efficient framework for creating webservices and much more. For this reason I’m using it to design my backend applications.
This framework is also well referenced on Internet and many people are downloading it and using it. All of this sounds really good and you will see you can make a webservice in less than 5 minutes following the many examples existing on Internet. As usually in this kind of technology once you have made the classical HelloWorld and university classical practices you have a lot of difficulties to make your first real program coupling different simple use-cases. So As I spent a couple of hours searching solution on Internet, this post will give you a full example of a project getting data from a MongoDB instance to provide a simple webservice.
Initiate your project
To get start you can download the needed SpringBoot packages from https://start.spring.io/. I’m preferring Gradle project type because the gradlew tool allows to build locally the project with nothing special installed on my MAC. Select packages WEB and MongoDB and download the package.
Unzip it and create a new project in IntelliJ from this package. In intelliJ import as a gradle project and select the gradle file.
Let’s go with the MongoDB
In the main/resources directory you have a application.properties file to set some important parameters:
#mongodb spring.data.mongodb.host=localhost spring.data.mongodb.port=27017 spring.data.mongodb.database=myDatabaseName #logging logging.level.org.springframework.data=info logging.level.=info
This is for a local DB. As I do not use a local DB on my macbook I did a ssh tunnel during my development phase to a distant DB hosted on my server:
# ssh -l root -p 22 -L 27017:localhost:27017 db.myserver.com
My project is organized this way:
- main +- java +- com.acme +- myapi +- views // contains the bean views +- database // contains the db objects +- repositories // contains the db requests +- services // functional stuff on db data ApiHandler // the differents APIs
That’s done we can create the first database object mapping. In this exemple we are going to take some battery history
package com.acme.myapi.database;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Field;
import org.springframework.data.mongodb.core.mapping.Document;
@Document(collection = "battery")
public class BatteryElement {
@Id
private String id;
@Field("id")
protected String deviceId;
protected long time;
protected double volt;
public BatteryElement() {}
public BatteryElement(String id, String deviceId, long time, double volt) {
this.id = id;
this.deviceId = deviceId;
this.time = time;
this.volt = volt;
}
// Complete this with the autogenerated getter/setters/toString
}
In this Class, @Id is automatically mapped to the _Id field of the mongoDB collection. As I have a Field names “id” in my collection I needed to change the element name in the class by deviceId and use the @Field annotation to map the correct field name.
Now we can defined the type of search we can have in the collection. For this we defines a repository:
package com.acme.myapi.repositories; import java.util.List; import org.springframework.data.mongodb.repository.MongoRepository; import com.acme.myapi.database.BatteryElement; import org.springframework.stereotype.Repository; @Repository public interface BatteryRepository extends MongoRepository<BatteryElement, String> { public List<BatteryElement> findFirst10ByDeviceIdOrderByTimeDesc(String deviceId); }
There is no need to detail how the request will be implemented : it is automatically generated by the framework.
- find – this is a query
- First10 – limit to 10 first entries
- ByDeviceId – search for a specific device Id (note that the element name is used)
- OrderByTimeDesc – odered by Time Descending
The deviceId is given as a request parameter
Let’s go with the Webservice
Now we can create the webservice:
package com.acme.myapi; import com.acme.myapi.database.BatteryElement; import com.acme.myapi.repositories.BatteryRepository; import org.springframework.stereotype.*; import org.springframework.web.bind.annotation.*; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; import org.springframework.http.HttpStatus; import java.util.List; @RestController public class BatteryApi { @Autowired private BatteryRepository batteryRepository; @Autowired private RightManagementService rmService; @RequestMapping(value="/battery/{id}", method=RequestMethod.GET) public ResponseEntity<?> getBattery(@PathVariable String id){ if ( rmService.isAuthorized(id) ) { List<BatteryElement> ret = batteryRepository.findFirst10ByDeviceIdOrderByTimeDesc(id); return new ResponseEntity<>(ret,HttpStatus.OK); } else { return new ResponseEntity<>("Not Authorized", HttpStatus.BAD_REQUEST); } } }
The webservice is checking the Authorization, this part is not covered in this post yet but basically I kept it to illustrate the way to return different HttpStatus as it is also something I had to search for.
@Autowired is used to dynamically inject the Repositories or other services. This is globally corresponding to the injection of a Singleton coming from the related element.
The last part is the main class starting all that stuff :
package com.acme.myapi;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
//@EnableMongoRepositories("com.acme.myapi.repositories")
public class MyApiApplication {
public static void main(String[] args) {
SpringApplication.run(MyApiApplication.class, args);
}
}
The RED line cost me a couple of hours … as the repositories are dynamically created by the framework we have to ensure the path to find them is known. Usually in the exemple everything is in the same path so it works. Normally it is also working when your repositories are under the Main class path. But if you organize your code differently (it was my case) you need to specify it this way to have the introspection working correctly. Here the line is commented as in this example you do not need the line.
Filter some of the fields
You may not want to expose all the fields provided by the database to your webservice. You can do this the following way.
In your Object Bean you can add a JsonFilter annotation
@JsonFilter("com.acme.myapi.database.BatteryElement")
@Document(collection = "battery")
public class BatteryElement {
Then in the API service we can make a filter
public class BatteryApi { @Autowired private BatteryRepository batteryRepository; @Autowired private RightManagementService rmService; @RequestMapping(value="/battery/{id}", method=RequestMethod.GET) public ResponseEntity<?> getBattery(@PathVariable String id){ if ( rmService.isAuthorized(id) ) { List<BatteryElement> ret = batteryRepository.findFirst10ByDeviceIdOrderByTimeDesc(id); MappingJacksonValue mappingJacksonValue = new MappingJacksonValue(ret); FilterProvider filters = new SimpleFilterProvider() .addFilter("com.acme.myapi.database.BatteryElement", SimpleBeanPropertyFilter .filterOutAllExcept(new HashSet<String>(Arrays .asList(new String[] { "deviceId", "time", "volt" })))); mappingJacksonValue.setFilters(filters); return new ResponseEntity<>(mappingJacksonValue,HttpStatus.OK); } else { return new ResponseEntity<>("Not Authorized", HttpStatus.BAD_REQUEST); } } }
That was the old-fashion way. Now you can create views at bean level and use these view to do the same:
You can create a View interface
package com.acme.views; public class BatteryView { public interface Default {} public interface Full extends Default {} }
Now in the Bean you can annotate fields you want in the view
@Document(collection = "battery") public class BatteryElement { ... @JsonView(BatteryView.Default.class) @Field("id") protected String deviceId; @JsonView(BatteryView.Default.class) protected long time; @JsonView(BatteryView.Full.class) protected int batLevel; ...
In the API you can reference the View you want to output
@RequestMapping(value="/battery/{id}", method=RequestMethod.GET)
@JsonView(BatteryView.Default.class)
public ResponseEntity<?> getBattery(@PathVariable String id){
JsonView will report by default null when a value is not set. You can avoid this by adding a property in the application.property file.
spring.jackson.serialization-inclusion=non_null
Activate JSONP
JSONP allows to integrate the Json output into a procedure to be interpreted in as a javascript object. A callback function is created and return with a name given as a parameter.
SpringBoot allows to activate JSONP in a transparent way by adding a couple of lines directly in the main application class :
import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.data.mongodb.repository.config.EnableMongoRepositories; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.servlet.mvc.method.annotation.AbstractJsonpResponseBodyAdvice; @SpringBootApplication @EnableMongoRepositories("com.acme.myapi") public class MyApiApplication { @ControllerAdvice private static class JsonpAdvice extends AbstractJsonpResponseBodyAdvice { public JsonpAdvice() { super("callback"); } } public static void main(String[] args) { SpringApplication.run(MyApiApplication.class, args); } }
Now, as soon as you add a parameter name “callback” = value on you URL the result will be returned like this :
/**/value([{....}]);
The comment /**/ is automatically added by the framework for security reasons.
Use HATEOAS with JsonViews
The next step is to use HATEOAS Spring integration to add Links into your webservice. To create a link you can apply it on the Bean list you’ve got before returning it :
List<DeviceElement> devices=deviceRepository.findByKey(apikey); for (DeviceElement r : devices) { Link selfLink = linkTo(methodOn(DeviceApiV3.class).getDeviceDetailV3( r.getDeviceId(), apikey )).withSelfRel().expand(); r.add(selfLink); Link batLink = linkTo(methodOn(BatteryApiV3.class).getBatteryV3( r.getDeviceId(), apikey, null, null, null )).withRel("battery.history").expand(); r.add(batLink); }
This add two link automatically pointing to the right URL obtain from the associated method in your rest controller. Note the expand() call at end : it build an executable link otherwise in the second link the not required parameter would have been return like {?from,to,last} at end of the URL. Note these parameters are Optional<Long> passing null is the only way to unset the parameter because of some missing converters.
This could be really easy when used alone but as usual if you had JsonViews use it start to be a mess. JsonViews allow to indicate the field you want and as a consequence it hides the others. Hateoas add fields by default (links) you can easily apply de @JsonView so you don’t see them. After long search I found the solution…
The first step is to activate a default visibility for everything in the application.property file. This will show all fields you did not defined a JsonView annotation.
spring.jackson.mapper.default-view-inclusion=true
The problem is now to filter the field we do not want. This is easily done by creating a view we don’t want to see and associate these fields to this view.
public class BatteryView {
public interface Minimal {}
public interface None {}
public interface Default extends Minimal {}
public interface BatList extends Default {}
}
In this exemple None will concern the Fields I never want to print. Default is a detailed view for the battery and BatList is a list a Battery when I want to have a link with a detailed one.
Now let’s take a look to the Bean with Hateoap
@Document(collection = "battery") public class BatteryElement extends ResourceSupport { @Id private String id; @JsonView(BatteryView.Default.class) @Field("id") protected String deviceId; @JsonView(BatteryView.Minimal.class) protected double volt; @JsonView(BatteryView.Default.class) @Transient protected double percent; @Transient @JsonView(BatteryView.BatList.class) protected List<Link> links;
The bean now extends ResourceSupport to manage the links.
In the Default view I do not want to see the Link Field but I want to have it in the BatList view. The problem is this field is not defined in the BatteryElement class but in its super class ResourceSupport. For this reason I had to redefine it (in blue) to be able to apply a @JsonView on it.
By extending ResourceSupport I also had to redefine the getId method renaming it into getMongoId() it seems it can be removed also.
Jackson will take all the getter and list them into the final Json. When the getter is associated to a Field and this field hide it will not printed but in the Field is not found the getter will be called. This is the case for my getMongoId() as an example. We can avoid this by adding the @JsonIgnore annotation
@JsonIgnore
public String getMongoId() {
return id;
}
JsonFilter and JsonView are fully incompatible each other I do not really recommend to try to find a solution in this direction it make me headaches 😉
Kick it off
To start all that stuff with Gradle
#./gradlew bootRun
You can adjust the log level in the application.properties file.
To make a fat jar executable you can modify the build.gradle file adding these lines:
springBoot { executable = true }
Once done the
# ./gradlew build
will generate the fat executable JAR in the directory build/libs this jar file can be run directly, uploaded on your server and executed with no other dependency than a JRE.
Configure nginx for being a proxy to springboot application
to configure your springboot application behind a nginx webserver/proxy you can edit your nginx subdomain configuration file and add the following lines:
upstream springfront { server localhost:8080; } server { ... location /myMappInMySpringBootApplication/ { proxy_pass $scheme://springfront$request_uri; proxy_redirect off; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header Host $http_host; } ... }
It means its better if the springboot application map a specific base url.
Make it working with angular (at least angular 1) and CORS
CORS is Cross Origin configuration allowing to have an API request from a different origin than the API one. For this Spring must be correctly set to accept the POST requests. The following configuration is basic and wide open but allow your angular application to place post request and get a JWT from the headers.
Create a Configuration Class setting this property. Here all is open for all.
@Configuration public class CorsConfig { @Bean public WebMvcConfigurer corsConfigurer() { return new WebMvcConfigurerAdapter() { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .exposedHeaders("Origin, Authorization, Content-Type") ; } }; } }
In the API Controleur class, you can also use the @CrossOrigin annotation to allow cross origin call for a given handler
@Api(value="user", tags="User-management-api")
@RequestMapping(value = "/user")
@CrossOrigin
@RestController
public class UserApi {