Spring Boot
Source: (https://github.com/arvindkgs/spring-rest-api-demo)
- Properties like jdbc (url, username, password) should be passed as environment variables to the container, rather than hardcoding into application.properties
- Add @RestController to controller class name
{To set common sub domain to all }
@RequestMapping(path = "/api")
- To parse result to POJO, create POJO (example: Input) then **public **Result add(@RequestBody Input input){
@ResultBody parses request body json and produces input object
- Using Service implementation
Any controller should call the service interface that should handle the business logic. This is achieved as:
@RequestMapping(path = "/api", produces = "application/json")
**public class **ApiController {
@Autowired
CalculateService calculateService;
@PostMapping("/add")
**public **Result add(@RequestBody Input input){
...
**return new **Result(**calculateService**.add(x,y) +**""**);
}
}
- @RequestBody parses the json and populates the Java POJO object. This is the simplest/handy way to consuming JSON using a Java class that resembles your JSON: https://stackoverflow.com/a/6019761
But if you can't use a Java class or you need to perform some pre-processing of the request data, you can use one of these two solutions.
Solution 1: you can do it receiving a Map<String, Object> from your controller:
@PostMapping("/process")
public void process(@RequestBody Map<String, Object> payload)
throws Exception {
System.out.println(payload);
}
Using your request:
Solution 2: otherwise you can get the POST payload as a String:
@PostMapping("/process", consumes = "text/plain")
public void process(@RequestBody String payload) throws Exception {
System.out.println(payload);
}
Then parse the string as you want. Note that must be specified consumes = "text/plain" on your controller. In this case you must change your request with Content-type: text/plain.
This annotation is used mostly in Spring MVC, with views rendered using Thymeleaf. This is used in two ways:
- As param to @RequestMapping method. This is used to capture an object sent via a http-form element. For example:
<form:form commandName="Book" action="" methon="post"> <form:input type="text" path="title"></form:input> </form:form>
Corresponding request mapping on controller is:
@PostMapping
public String controllerPost(@ModelAttribute("Book") Book book)
- If it is added to a method, then the return object is added to the Model and can be accessed in the View. For example:
If you want to have a Person object referenced in the Model you can use the following method:
@ModelAttribute("person")
public Person getPerson(){
return new Person();
}
This annotated method will allow access to the Person object in your View, since it gets automatically added to the Models by Spring.
More information : here
- Rest Controller exception handling can be done at global, class, method level. Let’s see for each level-
1. Global (@ControllerAdvice)
A class annotated with @ControllerAdvice and extending ResponseEntityExceptionHandler defines handlers at a global level.
For example :
@ControllerAdvice
**public class **CustomExceptionHandler **extends **ResponseEntityExceptionHandler {
}
@ControllerAdvice beans are @Component beans which are served on ordering. So you can use @Order(#number) to define the ordering in which beans should be handled. More here
This class can define individual handlers for each exception type. For example:
@Order(Ordered.HIGHEST_PRECEDENCE)
@ControllerAdvice
**public class **CustomExceptionHandler **extends **ResponseEntityExceptionHandler {
@ExceptionHandler(JsonParseException.class)
**public **ResponseEntity handleJsonParseException(JsonParseException ex) **throws **IOException {
**return new **ResponseEntity<Object>(**"Invalid input"**, HttpStatus.**_BAD_REQUEST_**);
}
@ExceptionHandler(NumberFormatException.class)
**public **ResponseEntity handleNumberFormatException(NumberFormatException ex) **throws **IOException {
**return new **ResponseEntity<Object>(**"Only numbers allowed"**, HttpStatus.**_BAD_REQUEST_**);
}
}
To restrict ControllerAdvice to certain controllers, set value for assignableTypes
@ControllerAdvice(assignableTypes = {Controller.class})
To customize the error response returned, check : https://www.baeldung.com/global-error-handler-in-a-spring-rest-api
- Extend WebSecurityConfigurerAdapter
**public class **WebSecurityConfiguration **extends **WebSecurityConfigurerAdapter{
-
Permit all
- Disable csrf
@Configuration
**public class **WebSecurityConfiguration **extends **WebSecurityConfigurerAdapter{
@Override
**public void **configure(HttpSecurity http) **throws **Exception {
http.csrf().disable().authorizeRequests().antMatchers(**"/api/**"**).permitAll();
}
}
- Add basic auth
add following to subclass of WebSecurityConfigurerAdapter
@Configuration
**public class **WebSecurityConfiguration **extends **WebSecurityConfigurerAdapter{
@Override
**public void **configure(HttpSecurity http) **throws **Exception {
http.csrf().disable().authorizeRequests()
.anyRequest().authenticated()
.and()
.httpBasic();
}
@Autowired
**public void **configureGlobal(AuthenticationManagerBuilder auth)
**throws **Exception
{
auth.inMemoryAuthentication()
.withUser(**"user"**)
.password(passwordEncoder().encode(**"password"**))
.authorities(**"ROLE_USER"**);
}
@Bean
**public **PasswordEncoder passwordEncoder() {
**return new **BCryptPasswordEncoder();
}
}
- To test rest api use MockMVC on Junit 5
@SpringBootTest by default starts searching in the current package of the test class and then searches upwards through the package structure, looking for a class annotated with @SpringBootConfiguration from which it then reads the configuration to create an application context. This class is usually our main application class since the @SpringBootApplication annotation includes the @SpringBootConfiguration annotation. It then creates an application context very similar to the one that would be started in a production environment.
More details here: https://reflectoring.io/spring-boot-test/
@SpringBootTest
@AutoConfigureMockMvc
**class **SpringRestApiDemoApplicationTests {
@Autowired
**private **MockMvc mockMvc;
@Test
**void **contextLoads() {
}
@Test
**public void **should_Add_When_APIAddRequest() **throws **Exception {
mockMvc.perform(post("api")
.contentType(MediaType.**_APPLICATION_JSON_**)
.content(**"input json"**)
.accept(MediaType.**_APPLICATION_JSON_**))
.andExpect(*status*().isOk())
.andExpect(*content*().contentType(MediaType.**_APPLICATION_JSON_**))
.andExpect(*jsonPath*(**"$"**).value(**"output json"**));
}
-
Reason for using @SpringBootTest instead of @WebMvcTest is defined here: https://stackoverflow.com/questions/55170423/error-while-injecting-mockmvc-with-spring-boot
-
Also @AutoConfigureMockMvc is used to instantiate the mockMvc object with all the dependent beans.
- If above api has basic authentication, then it can be mocked by using running tests with @WithMockUser **public void **should_Add_When_APIAddRequest() **throws **Exception {
mockMvc.perform(post("/api/add"));
}
-
@WebMvcTest can also be used, instead of @SpringBootTest and @AutoConfigureMockMvc. However, @WebMvcTest will not load any @Service bean referred to in the @RestController. So @TestConfiguration should be used to inject the specific bean. For example:
@WebMvcTest
class SpringRestApiDemoApplicationTests {
@Autowired
private MockMvc mockMvc;
@TestConfiguration
Static class TestConfig {
@Bean
CalculateService getCalculateService() {
return new CalculateServiceImpl();
}
}
@Test
void contextLoads() {
}
}
More details here : https://mkyong.com/spring-boot/spring-boot-how-to-init-a-bean-for-testing/
Or @MockBean can also be used to mock the @Service bean. But then you will need to mock the methods and return sample values.
An Interceptor intercepts an incoming HTTP request before it reaches your Spring MVC controller class, or conversely, intercepts the outgoing HTTP response after it leaves your controller, but before it’s fed back to the browser.
The interceptor can be used for cross-cutting concerns and to avoid repetitive handler code like: logging, changing globally used parameters in Spring model etc.
And in order to understand the interceptor, let's take a step back and look at the* HandlerMapping*. This maps a method to a URL, so that the DispatcherServlet will be able to invoke it when processing a request.
And the DispatcherServlet uses the HandlerAdapter to actually invoke the method.
Interceptors working with the HandlerMapping on the framework must implement the HandlerInterceptor interface.
This interface contains three main methods:
-
prehandle() – called before the actual handler is executed
-
postHandle() – called after the handler is executed, but before the view is generated
-
*afterCompletion() – *called after the complete request has finished and view was generated
In order to implement interceptor in your code, following steps are required:
-
Add custom implementation of HandlerInterceptor
-
Register the custom Handler
-
Add custom implementation of HandlerInterceptor
- preHandle() sample code
public class MyRequestInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response, Object handler) throws Exception {
try {
// Do some changes to the incoming request object
return true;
} catch (SystemException e) {
logger.info("request update failed");
return false;
}
}
2. postHandle() sample code
@Override
public void postHandle(
HttpServletRequest request,
HttpServletResponse response,
Object handler,
ModelAndView modelAndView) throws Exception {
// your code
}
3. afterCompletion() sample code
@Override
public void afterCompletion(
HttpServletRequest request,
HttpServletResponse response,
Object handler, Exception ex) {
// your code
}
- Register the custom Handler
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new MyRequestInterceptor());
}
}
You can also add interceptor to only certain urls by replacing above line with:
registry.addInterceptor(new MyRequestInterceptor()).addPathPatterns("/").excludePathPatterns("/admin/")
A filter dynamically intercepts requests and responses to transform or use the information contained in the requests or responses. Filters typically do not themselves create responses, but instead provide universal functions that can be "attached" to any type of servlet or JSP page.
Filter is used for,
-
Authentication-Blocking requests based on user identity.
-
Logging and auditing-Tracking users of a web application.
-
Image conversion-Scaling maps, and so on.
-
Data compression-Making downloads smaller.
-
Localization-Targeting the request and response to a particular locale.
-
Request Filters can:
-
perform security checks
-
reformat request headers or bodies
-
audit or log requests
-
-
Response Filters can:
-
Compress the response stream
-
append or alter the response stream
-
create a different response altogether
-
Examples that have been identified for this design are:
-
Authentication Filters
-
Logging and Auditing Filters
-
Image conversion Filters
-
Data compression Filters
-
Encryption Filters
-
Tokenizing Filters
-
Filters that trigger resource access events
-
XSL/T filters
-
Mime-type chain Filter
A Servlet Filter is used in the web layer only, you can’t use it outside of a web context. Interceptors can be used anywhere. That’s the main difference between an Interceptor and Filter.
Let’s consider an example of modifying the request body .
RestController
@PostMapping
public void addAudioBook(@RequestBody AudioBook audioBook){
audiobookService.add(audioBook);
}
The POST request body is a JSON whose values are UrlEncodeded strings.
{
"author":"Susan%20Mallery",
"title":"%0AMeant%20to%20Be%20Yours"
}
I would like to decode the strings before persisting as
{
"author":"Susan Mallery",
"title":"Meant to Be Yours"
}
I looked into adding @JsonDeserialize at each @Entity attribute as,
@Entity
public class AudioBook {
@NonNull
private String author;
@NonNull
private String title;
}
So decided on using a Filter to intercept and replace the request body with the decoded JSON, before it reaches the controller.
I am using a Filter to intercept the request as
@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class RequestDecodeFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
MyRequestWrapper requestWrapper = new MyRequestWrapper(request);
//decode request body
chain.doFilter(requestWrapper, response);
}
The modified requestWrapper is passed along in chain.doFilter().
MyRequestWrapper
public class MyRequestWrapper extends HttpServletRequestWrapper {
private String body;
public MyRequestWrapper(ServletRequest request) throws IOException {
super((HttpServletRequest)request);
...
}
}
NOTE: Complete file MyRequestWrapper.java can be found here
More details of modifying the request body can be found here and here
This filter applies to all urls. If you want to restrict access to certain urls then,
-
Remove @Component from the custom Filter implementation
-
Add this to your controller.
@Bean
public FilterRegistrationBean decodeFilter(){
FilterRegistrationBean registrationBean
= new FilterRegistrationBean<>();
registrationBean.setFilter(new RequestDecodeFilter());
registrationBean.addUrlPatterns("/audiobooks");
return registrationBean;
}
Now only url: /audiobooks, will enter the filter
To enable swagger documentation:
- Add following dependencies to build.grade
implementation("io.springfox:springfox-swagger2:2.9.2")
implementation("io.springfox:springfox-swagger-ui:2.9.2")
- Add following configuration:
@Configuration
@EnableSwagger2
public class SpringFoxConfig {
@Bean
public Docket api(){
return new Docket(DocumentationType.*SWAGGER_2*)
.select()
.apis(RequestHandlerSelectors.*any*())
.paths(PathSelectors.*any*())
.build();
}
}
-
Restart the app and navigate to http://localhost:8080/swagger-ui.html
-
For more info check: https://www.baeldung.com/swagger-2-documentation-for-spring-rest-api
Flyway you can easily version your database: create, migrate and ascertain its state, structure, together with its contents. Any modifications to the schema is performed by Flyway. It does so as follows:
-
Applying the schema changes specified in the SQL scripts and
-
Keeping an internal meta-data table named SCHEMA_VERSION through which it keeps track of various information regarding the applied scripts: when it was applied, by whom it was applied, description of the migration applied, its version number etc.
To include Flyway add following to pom.xml
<groupId>org.flywaydb</groupId>
<artifactId>flyway-core</artifactId>
<version>${Flyway.version}</version>
Creating Spring jpa repositories is as simple as extending the CrudRepository. Example:
public interface AudioBookRepository extends CrudRepository<AudioBook, Long>
This implements the CRUD operations on Entity AudioBook. Like find(), save() etc.
QueryByExampleExecutor
There is another helper interface QueryByExampleExecutor that can be used to create either-or queries. For example consider entity:
@Entity
Class AudioBook{
@Id
@GeneratedValue
Long id;
String title;
String author;
}
Now if I have a GET REST API that is as follows:
@GetMapping
public List get(@RequestParam(required = false) String title, @RequestParam(required = false) String author){}
This method takes optional params title and author. Now with traditional SQL, I would need to create multiple SQL as:
String getAudioBookSql = "select * from AudioBook";
Bool where = false;
if(title != null){
Where = true;
getAudioBookSql+=" where title=’”+title+”’”
}
if(author != null){
if(!where)
getAudioBookSql+=" where author=’”+author+”’”
Else
getAudioBookSql+=" and title=’”+title+”’”
}
But with QueryWithExample, you can query as,
AudioBook book = new AudioBook();
if(title != null)
book.setTitle(title);
if(author != null)
book.setAuthor(author);
return (List)audioBookRepository.findAll(Example.of(book));
You can also ignore certain fields as:
ExampleMatcher matcher = ExampleMatcher.matching().withIgnoreNullValues().withIgnorePaths( "id");
(List)audioBookRepository.findAll(Example.of(book, matcher));
More information can be found:
There is a nice discussion on passing the env variable as an argument rather than hardcoding in the application.prooerties. this is a really cool practice and also mentioned in the 12-factor app.
https://stackoverflow.com/questions/35531661/using-env-variable-in-spring-boots-application-properties