Skip to content

Instantly share code, notes, and snippets.

@arvindkgs
Last active March 10, 2023 16:33
Show Gist options
  • Save arvindkgs/7332692d0b6fb686fc83b3e1f42440bf to your computer and use it in GitHub Desktop.
Save arvindkgs/7332692d0b6fb686fc83b3e1f42440bf to your computer and use it in GitHub Desktop.
[Spring-boot] Spring boot

Spring Boot


Source: (https://github.com/arvindkgs/spring-rest-api-demo)

Best practices

Containerized applications

  1. Properties like jdbc (url, username, password) should be passed as environment variables to the container, rather than hardcoding into application.properties

Spring Rest

Rest Controller


  1. Add @RestController to controller class name

{To set common sub domain to all }

@RequestMapping(path = "/api")

  1. 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

  1. Using Service implementation

Any controller should call the service interface that should handle the business logic. This is achieved as:

@RestController

@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) +**""**);

}

}

image_0

  1. @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.

@ModelAttribute

This annotation is used mostly in Spring MVC, with views rendered using Thymeleaf. This is used in two ways:
  1. 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)

  1. 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

Exception Handling

  1. Rest Controller exception handling can be done at global, class, method level. Let’s see for each level-

image_1

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

Security


  1. Extend WebSecurityConfigurerAdapter

**public class **WebSecurityConfiguration **extends **WebSecurityConfigurerAdapter{

  1. Permit all

    1. Disable csrf

@Configuration

**public class **WebSecurityConfiguration **extends **WebSecurityConfigurerAdapter{

@Override

**public void **configure(HttpSecurity http) **throws **Exception {

   http.csrf().disable().authorizeRequests().antMatchers(**"/api/**"**).permitAll();

}

}

  1. 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();

}

}

Testing

  1. 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"**));

}

  1. 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"));

}

  1. @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.

Interceptor

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:

  1. prehandle() – called before the actual handler is executed

  2. postHandle() – called after the handler is executed, but before the view is generated

  3. *afterCompletion() – *called after the complete request has finished and view was generated

image_2

In order to implement interceptor in your code, following steps are required:

  1. Add custom implementation of HandlerInterceptor

  2. Register the custom Handler

  3. Add custom implementation of HandlerInterceptor

    1. 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

}

  1. 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/")

Filter

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,

  1. Remove @Component from the custom Filter implementation

  2. 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

Swagger Docs

To enable swagger documentation:

  1. Add following dependencies to build.grade

implementation("io.springfox:springfox-swagger2:2.9.2")

implementation("io.springfox:springfox-swagger-ui:2.9.2")

  1. 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();

}

}

  1. Restart the app and navigate to http://localhost:8080/swagger-ui.html

  2. For more info check: https://www.baeldung.com/swagger-2-documentation-for-spring-rest-api

Spring Data

Data setup

Flyway

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:

  1. Applying the schema changes specified in the SQL scripts and

  2. 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>

JPA

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:

  1. http://www.kousenit.com/springboot/#_exercises

  2. https://github.com/kousen/spring-and-spring-boot/wiki/Source-code

@mukeshpilaniya
Copy link

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

@arvindkgs
Copy link
Author

I agree. Passing properties as environment variables is recommended.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment