-
Notifications
You must be signed in to change notification settings - Fork 9
NeoStarter
Main starter for REST application development. It comprises of several enhancements over plain Spring Boot defaults in areas such as: Jackson, MVC, Handling Java 8 Date Time (JSR-310), etc.
The starter sets default time zone to UTC and provides Clock bean, which should be used for current time references.
Standard ISO formatting is now applied to all date/time types. Invokes setUseIsoFormat(true) on
JSR-310 DateTimeFormatter (was false by default).
Jackson’s default format for ZonedDateTime objects is set to DateTimeFormatter.ISO_INSTANT. To change the default, please provide DateTimeFormatter Bean named com.neoteric.starter.jackson-jsr310-dateFormat, i.e.:
@Bean(name = "com.neoteric.starter.jackson-jsr310-dateFormat")
public DateTimeFormatter dateTimeFormatter() {
return DateTimeFormatter.ISO_DATE;
}neo-starter-springmvc- comes with its own implementation of resolving Exceptions (via `RestExceptionResolver - inspired by
Spring Rest Exception Handler project).
General concept is to map exceptions thrown by the application to a standardized JSON response, as below:
{
"timestamp" : "2016-04-24T16:58:40.437Z", (1)
"requestId" : "a65dc528-cf6d-4a9f-9eba-f063de5bb151", (2)
"path" : "/api/endpoint", (3)
"status" : 500, (4)
"error" : "Internal Server Error", (5)
"cause" : "BAD_CODE", (6)
"exception" : "java.lang.RuntimeException", (7)
"message" : "Unknown error" (8)
}-
Time, when the exception occured.
-
Request id assigned to current request, value fetched from
MDC.get("REQUEST_ID"). It is skipped when no request id available. -
current request URI
-
HTTP Status code returned
-
HTTP Status phrase
-
(optional) Can return any String-typed value you specify in
@RestExceptionHandlerProviderparameter. -
Exception, which caused application to stop
-
Message with details regarding exception
To handle particular exception, create a class implementing RestExceptionHandler
and annotate it with @RestExceptionHandlerProvider. Example:
@RestExceptionHandlerProvider // (1)
public class FallbackExceptionHandler implements RestExceptionHandler<Exception> {
public static final String FALLBACK_ERROR_MSG = "Unknown error";
@Override
public Object errorMessage(Exception ex, HttpServletRequest req) { // (2)
return FALLBACK_ERROR_MSG;
}
}-
@RestExceptionHandlerProviderannotated classes are scanned and initialized on application startup. Scanning scope is deriven from@ComponentScan, hence no need to specify separate package(s) range. -
Provide an error message to return for given exception. Possible return types are:
String,ListandMap.
It is possible to add additional fields to error message. To do that you need to override default RestExceptionHandler additionalInfo method:
default Map<String, Object> additionalInfo(T ex, HttpServletRequest req) {
return null;
}If you need you can further customize HttpServletResponse (adding headers etc.) by overriding default RestExceptionHandler customizeResponse method:
default void customizeResponse(T ex, HttpServletRequest req, HttpServletResponse resp) {
}@RestExceptionHandlerProvider annotation comes with 4 parameters with sensible default values:
-
Level logLevel() default Level.ERROR- Specifies level of log statement to application server log. -
HttpStatus httpStatus() default HttpStatus.INTERNAL_SERVER_ERROR- Specifies HTTP status to be returned. -
String cause() default ""- If provided, value will be returned as cause field in error response. This is kind of a generic placeholder for additional value your application can return (which is easily set up via annotation parameter without the need for implementingadditionalInfomethod). -
boolean suppressStacktrace() default false- Should the stacktrace be always suppressed (despiteserver.error.includeStacktraceproperty. -
boolean suppressException() default false- Should the exception type information be suppressed.
Starter provides default handlers for common exceptions.
| Handler class | Exception type | HTTP Status | Log Level | Suppress exception |
|---|---|---|---|---|
|
|
500 |
ERROR |
true |
|
400 |
WARN |
false |
|
|
|
400 |
ERROR |
false |
|
|
415 |
ERROR |
false |
|
|
406 |
ERROR |
false |
|
|
405 |
ERROR |
false |
|
|
400 |
ERROR |
false |
When Spring Security is on the classpath, these handlers are available:
| Handler class | Exception type | HTTP Status | Log Level | Suppress exception |
|---|---|---|---|---|
|
|
401 |
ERROR |
true |
|
|
403 |
ERROR |
true |
There are also 2 abstract exceptions accompanied with Exception handlers. They can be used for common situations where a resource is not found or is in conflict. You can extend them for your particular resources.
| Handler class | Exception type | HTTP Status | Log Level | Suppress exception |
|---|---|---|---|---|
|
|
404 |
ERROR |
false |
|
|
409 |
ERROR |
false |
|
Note
|
All of the handlers don’t suppress stacktrace by default. |
You can disable default handlers by setting neostarter.mvc.errorHandling.defaultHandlersEnabled=false. With default mappers off (they provide FallbackExceptionHandler, which binds to Exception) if no handler found for given exception NoExceptionHandlerFoundException is thrown.
For development purposes showing stacktrace in error message can be really helpful. To do that you can use server.error.includeStacktrace property. If enabled (and the handler doesn’t explicitly suppress stacktrace for a given exception) it will add readable stacktrace to the end of the error response:
"stackTrace" : {
"[0]" : "java.lang.RuntimeException: some error",
"[1]" : " at com.neoteric.starter.mvc.errorhandling.StarterErrorHandlingAutoConfigurationTest$SwaggerConfiguration.hello(StarterErrorHandlingAutoConfigurationTest.java:138)",
...
"[51]" : " at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)",
"[52]" : " at java.lang.reflect.Method.invoke(Method.java:497)",
"[53]" : " at com.intellij.rt.execution.application.AppMain.main(AppMain.java:144)"
}If you use default handlers and need to provide 'cause' value for them you can do it by providing cause-mapping
map property via application property file.
The key is the handler full class name and value is what is going to be returned. Example in yaml format:
neostarter:
error-handling:
cause-mapping:
com.neoteric.starter.mvc.errorhandling.handlers.common.IllegalArgumentExceptionHandler: INVALID_CODE|
Tip
|
It is also possible to use same approach for your custom handlers if you find it convenient (instead of providing annotation parameter value). |
You can fully disable REST Exception handling behaviour with neostarter.mvc.errorHandling.enabled=false
In contrast to standard JSR-303 Bean Validation support, there is a property path resolution changed. It takes field’s @JsonProperty annotation to the equation - when it’s available the field name is retrieved from the annotation value.
There is a default handler available for resolving validation errors - MethodArgumentNotValidExceptionHandler
which adds JSON violations list, ex:
{
...
"exception": "org.springframework.web.bind.MethodArgumentNotValidException",
"message": "appointment has 2 validation errors", (1)
"violations": [
{
"property": "appointment", (2)
"type": "TEST_CLASS_VALIDATION", (3)
"message": "error_message"
},
{
"property": "id", (4)
"type": "SIZE",
"invalidValue": "ab", (5)
"message": "size must be between 3 and 2147483647"
}
]
}-
"appointment" is a name of the validated object.
-
This violation is a result of a failed class constraint without property path specified. Hence, property is the validated object name.
-
It is an upper underscored name of the constraint name (annotation) which produced the violation.
-
This violation is a result of a failed field constraint (or class constraint with provided property path). Hence, property is the validated field name.
-
Value which did not pass validation rule.
|
Caution
|
When providing property path in class constraint validator, ie.: context.buildConstraintViolationWithTemplate("message")
.addPropertyNode("fieldName")
.addConstraintViolation();fieldName has to be a Java field name (not |
There are JSON meta-annotations available to your convienience. They are built on top of standard @RequestMapping annotations, but providing JSON produces and consumes parameter values:
@GetJson @PostJson @PutJson @DeleteJson @PatchJson
This starter comes with a built in Swagger2 support (provided by SpringFox )
It comes with a default, configurable Docket bean. Swagger information is available at: '/swagger' endpoint.
There are 2 options to narrow down the scope of the Docket:
-
You can provide base package for scanning with
neostarter.swagger.basePackage. It defaults to com.neoteric. If you change it to empty String, all packages will be scanned. -
You can provide base prefix for your business API via
neostarter.mvc.apiPath(ex. '/api'). Ii defaults to no prefix.
You can provide Swagger Api info by populating neostarter.swagger.* properties.
To disable Swagger use neostarter.swagger.enabled=false
To avoid assigning same @RequestMapping on particular classes you can provide class name patterns
the mapping should follow. It is helpful i.e., when you want expose your business API with a given prefix.
To achieve that there is a meta-annotation called @ApiController (wraps @RestController annotation). It is correlated with a couple of configuration options:
| Property (prefixed neostarter.mvc.api) | Description |
|---|---|
|
Prefix path for your business API endpoints |
|
If there is no |
|
Specifies how class name should be parsed to create API resource name. Provide class name with '?' special character for marking the resource name. Example: Given class name: SomeReservationTypeEndpoint |
|
In which case format the API resource name should be created. Takes Guava’s |
Example controller class:
@ApiController
@RequestMapping("/mapping")
public class AppointmentTypeController {
@GetMapping("/hello")
public String hello() {
return "hi";
}
}would fulfill given test:
@RunWith(SpringRunner.class)
@SpringBootMockMvcTest(properties =
{"neostarter.mvc.api.path=/api",
"neostarter.mvc.api.resources.defaultPrefix=/v1",
"neostarter.mvc.api.resources.classNamePattern=?Controller",
"neostarter.mvc.api.resources.caseFormat=LOWER_UNDERSCORE"})
public class ClassNamingTest {
@Test
public void test() throws Exception {
get("/api/v1/appointment_type/mapping/hello")
.then()
.statusCode(200);
}
}If you add @ApiController parameter as follows:
@ApiController(prefix="/v2")
it would map to /api/v2/appointment_type/mapping/hello
Couple of additional configuration parameters is set by the starter:
spring.jackson.serialization.indent_output=true
spring.jackson.serialization.write_dates_as_timestamps=false
spring.jackson.time-zone=UTCFeign is our tool we chose to make external HTTP calls. If you decide to use Feign you can control the level of logging Feign does by setting neostarter.feign.loggerLevel parameter (defaults to BASIC). Valid options are:
-
NONE - No logging.
-
BASIC - Log only the request method and URL and the response status code and execution time.
-
HEADERS - Log the basic information along with request and response headers.
-
FULL - Log the headers, body, and metadata for both requests and responses.
To be able to trace single business action spanned over couple of services we augment request calls and add REQUEST_ID request header. It is propagated via MDC ('REQUEST_ID' key) and applied to two client mechanisms:
-
Feign- if Feign is availableRequestIdAppendInterceptoris being added to Feign interceptors. -
RestTemplate- all Rest Templates initialized on application start up will be provided withRestTemplateRequestIdInterceptor.
Starters provides access to request parameters that should be applied to GET request. Parameters are:
-
filters- field filtering represented as json object -
sort- sort definition represented as json object -
first- index of first element to be returned -
pageSize- number of elements to be returned
Example request can look like this:
It can be interpreted as: give me only those resources where 'name' starts with "John" and 'status' is "ACTIVE", sort results by 'name' and return 10 elements starting from index 20.
Filters are automatically parsed for every HTTP request, and they are accessible through 'RequestParametersHolder' ThreadLocal class.
There are several operators that can be applied to filters:
| Filter | Description | Example |
|---|---|---|
$eq |
equality |
|
$neq |
not equal |
|
$lt |
less than |
|
$lte |
less than or equal |
|
$gt |
greater than |
|
$gte |
greater than or equal |
|
$in (array) |
in |
|
$nin (array) |
not in |
|
$all (array) |
have all elements in list |
|
$startsWith |
Starts with regexp (case insensitive) |
|
$regex |
Full regexp (case insensitive) |
|
By default if there is more than one expression, they should be "AND-ed". There also exist logicalOperator "$or" which can be applied to fields:
"$or":{"name":{"$eq":"John"},"last":{"$eq":"Doe"}}
and field values:
{"name":{"$or":{"$startsWith":"J"},"$nin":["$John":"Jake"]}}
Sort definition should contain field name and value: asc / desc i.e.:
{"myField": "asc"}
It also can be sorted by multiple fields, which requires specifing the order of fields i.e.:
{"name": "asc","last": "desc","$order": ["name","last"]}
Every API endpoint method (it’s entry and exit point) will be logged - based on the method signature.
API endpoint methods are public methods in every class annotated with @ApiController.
If you specify resourceName param on @ApiController it will be prepended in each of mentioned log statements.
Example method signature:
public String searchWithParams(ZonedDateTime dateTime, int number)
will result in following entry point log statement:
09:58:16.569 [main] INFO com.neoteric.offers.JobOffersController - [JobOffer] Search with params [dateTime: 2010-01-10T10:00Z, number: 2010].
-
[JobOffer] prefix is added if:
@ApiController(resourceName="JobOffer") -
"Search with paramsis a method name converted to a human readable sentence.
-
Parameter names are taken as-is from method argument names
-
Only primitives and java standard types will be logged as parameters in initial log statement
If there are 'custom' type arguments in method signature, they will be logged into next log statement. Custom type arguments are all types which are not primitive and its package does not start with 'java'.
Example:
10:09:18.121 [main] DEBUG com.neoteric.offers.JobOffersController - [JobOffer] Details: [offer: Offer(name=abc, value=5)].
Exit point log statement is very similar with entry point one, with an addition of method time execution, example:
10:11:43.396 [main] INFO com.neoteric.offers.JobOffersController - [JobOffer] Search with params [dateTime: 2010-01-10T10:00Z, number: 2010] took 0.196 seconds.
There are two additional events log.
-
If method response is of
JsonApiObjecttype, returning details will be provided:
10:10:53.607 [main] DEBUG com.neoteric.offers.JobOffersController - [JobOffer] Returning [Offer(name=abc, value=5)].
-
If method response is of
JsonApiListtype, list size will be provided:10:11:43.396 [main] INFO com.neoteric.offers.JobOffersController - [JobOffer] Returning 1 item.
Level of each of these statements is configurable via neostarter.mvc.logging properties.
To disable API endpoint logging provide application property: neostarter.mvc.logging.enabled=false
neo-starter-springmvc provides convenient defaults for Logback configuration. For production it has few modifications over standard Spring Boot ones:
-
Adds time based rolling policy, which specifies daily rollovers, with single log file of 100MB maximum size and maximum history of 30 days -
<fileNamePattern>${LOG_FILE}.%d{yyyy-MM-dd}.%i</fileNamePattern> -
Adds
REQUEST_IDparameter for console and file log patterns (used for request tracing) -
Sets default root level to
DEBUGand suppress many of third party libraries level toINFO
To use it, just include com/neoteric/starter/logback-defaults.xml in your your logback.xml configuration file, example:
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<include resource="com/neoteric/starter/logback-defaults.xml"/>
</configuration>There is a similar configuration or test scope (without file appender). To use it include com/neoteric/starter/logback-test-defaults.xml in your your logback-test.xml configuration file.