Service-Framework

Abhinav Tripathi
7 min readJan 3, 2021

Service Framework makes it easier to build better Spring Boot applications more quickly and with less code.

Service Framework is a wrapper on top of the Spring Boot framework that encourages rapid development and clean, pragmatic design. It takes care of much of the hassle of Backend development, so you can focus on writing your app without needing to reinvent the wheel. It’s free and open source.

SF includes dozens of common libraries you use in day-to-day development. It includes support for CRUD APIs and Search API for every Entity you define in your application— right out of the box.

Following are some major attractions
1. gRPC — gRPC integration is inbuilt. Just define protos and start writing grpc services
2. Spring Data
3. Query DSL
4. Spring Caching
5. Swagger 2
6. Lombok
7. MapStruct Mapper
8. Log Tracing
9. Inbuilt CRUD APIs
10. Search API and many more

Spring Boot was designed to develop a stand-alone and production-grade spring application with minimal configurations. But it offers all from a configuration point of view to make the development simple. As a developer, we all write REST APIs in our day to day life. We have to write CRUD APIs for every Entity that we define in our application. In Addition, we do write search API for every entity. Every service has the implementation of its entity search. Service-Framework makes this part simple by implementing it beforehand.

SF has Swagger2 integration inbuilt. All the REST APIs present in the application will be already documented with Swagger2. It comes with QueryDSL which makes the data abstraction layer simpler and more readable even for complex queries.

SF provides the following Base classes
1. BaseEntity
2. BaseRepository
3. BaseResponse
4. BaseService
5. BaseController

BaseEntity is the base class of the Model layer which contains fields such as id, createdBy, createdAt, updatedBy, updatedAt. These fields correspond to columns in DB. BaseEntity is the abstract class that every model/entity in the application should inherit.

BaseRepository is the base class of the Data Abstraction layer which provides methods from JpaRepository, JpaSpecificationExecutor, QuerydslPredicateExecutor. It contains all the common query methods such as findById, save, saveAll, findAll, etc.

BaseResponse is the abstract class that contains Status information such as statusCode, status type, status message. The response of an API should inherit BaseResponse class.

BaseService is the core of the service layer which contains the implementation of all the CRUD and Search APIs. All the services should extend BaseService to get the inbuilt crud and search APIs implementation.

BaseController is a REST controller which exposes all the CRUD and Search APIs.

Service Framework Architecture

The brief description of the layers is given below.

  1. Presentation layer.
    It is the first layer of architecture. It is used to translate the JSON fields to objects and vice-versa, and also handles authentication and HTTP requests. After completing the authentication, it passes it to the business layer for further processes.
  2. Business Layer
    It handles all the business logic and also performs validation and authorization as part of the business logic. For example, only admins are allowed to modify the user’s account.
  3. Persistence Layer
    It contains all the storage logic, such as the database queries of the application. It also translates the business objects from and to database rows.
  4. Database Layer
    The database layer consists of the database such as MySQL, PostgreSQL, MongoDB, etc. It may contain multiple databases. All the database-related operations like CRUD (Create, Read/Retrieve, Update, and Delete) are performed in this layer.

The implementation of the above-layered architecture is performed in the following way:

Database Layer consists of Models which we call Entity. Following is a sample DemoEntity.

@Data
@Entity
@Table(name = “demo”)
@EqualsAndHashCode(callSuper = false)
public class DemoEntity extends BaseEntity {
@Column(name = “name”)
private String name;
@OneToMany(fetch = FetchType.EAGER, mappedBy = “demoEntity”)
private List<DemoDetail> demoDetailList;
}

Repositories handle persistence (storage logic). Following is a sample DemoRepository.

public interface DemoRepository extends BaseRepository<DemoEntity> {
List<
DemoEntity> findAllByName(String name);
DemoEntity findOneByName(String name);
}

Services control the business logic. Following is a sample DemoService.

@Service
@Transactional
public class DemoService extends BaseService<DemoEntity, DemoEntry>{
//
methods implementing business logic
}

The web requests are handled by Controllers in the Presentation Layer. Following is a sample DemoController.

@RestController
@RequestMapping("/demoService")
public class DemoController extends BaseController<DemoResponse, DemoEntity, DemoEntry> {
// other APIs declaration
}

Finally, Define Application class like

@SpringBootApplication
@ComponentScan({"com.livspace"})
public class DemoApplication {

public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}

}

Add @ComponentScan({“com.livspace”}) to enable Swagger2

Steps to integrate with Service Framework

Maven Dependency

<dependency>
<groupId>com.avail</groupId>
<artifactId>service-framework</artifactId>
<version>1.0.1</version>
</dependency>

Specify the s3 URL where the artifact is hosted.
Add the following code

<repositories>
<repository>
<url>https://jitpack.io</url>
</repository>
</repositories>

Gradle Dependency

Add the dependency to your project’s build.gradle file:

implementation 'com.github.ironman19933:service-framework:1.0.1'

Specify the s3 URL where the artifact is hosted. Add the following code under the repositories tag

repositories {
mavenLocal()
mavenCentral()
maven {
url 'https://jitpack.io'
}
}

Gradle Configs

Copy all the Gradle files present in the gradle_config package to the project’s root directory and include the below lines of code to project’s build.gradle file

apply from: 'querydsl.gradle'
apply from: 'publish.gradle'
apply from: 'grpc.gradle'

Enable Annotation Processor in IntelliJ for Lombok/MapStruct/QueryDSL to work. To configure annotation processing in IntelliJ IDEA, use dialog Preferences > Project Settings > Compiler > Annotation Processors

Once the above-mentioned classes and steps are done Following REST and Search APIs will be implemented

Create

URI: http://{host}/{serviceName}/save
HTTP Method: POST
Payload : DemoEntity
Response: DemoResponse

@RequestMapping(value = "/save", method = RequestMethod.POST)
public R save(@RequestBody E entry) {
}

Read

URI: http://{host}/{serviceName}/findById/{id}
HTTP Method: GET
Response: DemoResponse

@RequestMapping(value = "/findById/{id}", method=RequestMethod.GET)
public R findById(@PathVariable Long id) {
}

Update

URI: http://{host}/{serviceName}/update/{id}
HTTP Method: PUT
Payload : DemoEntity
Response: DemoResponse

@RequestMapping(value = "/update/{id}", method = RequestMethod.PUT)
public R update(@RequestBody E entry, @PathVariable Long id) {
}

Search with Query Param

URI: http://{host}/{serviceName}/search
HTTP Method: GET
Response: DemoResponse

@RequestMapping(value = "/search", method = RequestMethod.POST)
public R customSearch(@RequestParam("filters") String filters,
@RequestParam(value = "page", defaultValue = "0", required = false) Integer page,
@RequestParam(value = "fetchSize", defaultValue = "100", required = false) Integer fetchSize,
@RequestParam(value = "sortBy", defaultValue = "id", required = false) String sortBy,
@RequestParam(value = "sortOrder", defaultValue = "ASC", required = false) String sortOrder)

Search with SearchEntry Payload

URI: http://{host}/{serviceName}/search
HTTP Method: POST
Payload : SearchEnty
Response: DemoResponse

@RequestMapping(
value = {"/search"},
method = {RequestMethod.POST}
)
public R customSearch(@RequestBody SearchEntry searchEntry) throws Exception {

Search API Specifications

Following are the allowed Search Operators for Fields

    EQUAL_TO("eq"),
NOT_EQUAL_TO("ne"),
IS_NULL("isNull"),
IS_NOT_NULL("nn"),
GREATER_THAN("gt"),
GREATER_THAN_EQUAL_TO("ge"),
LESS_THAN("lt"),
LESS_THAN_EQUAL_TO("le"),
LIKE("like"),
NOT_LIKE("nl"),
IN("in"),
NOT_IN("nin"),
JSONB_PATH_EXISTS("jsonb_path_exists"), [only PostgreSQL]
JSONB_PATH_EQUALS("jsonb_path_equals"), [only PostgreSQL]
JSONB_PATH_CONTAINS("jsonb_path_contains"); [only PostgreSQL]

AND Operator between the fields is denoted by semicolon ‘;’and OR operator is denoted by Double Underscore ‘__

Search works on Collections (case of oneToMany associations) as well.

Convention for using the search API

{parameterName}.{searchOperator}:{value};

Following are the sample search filter examples on DemoEntity

  1. [equals]
    http://localhost:7076/demoService/search?filters=id.eq:1
    query — select * from demo where id = 1;
  2. [not-equal]
    http://localhost:7076/demoService/search?filters=id.ne:1
    query — select * from demo where id <>1;
  3. [isNull] http://localhost:7076/demoService/searchfilters=createdBy.isNull
    query — select * from demo where created_by is null;
  4. [not-null]
    http://localhost:7076/demoService/search?filters=createdBy.nn
    query — select * from demo where created by is not null;
  5. [greater-than]
    http://localhost:7076/demoService/search?filters=createdOn.gt:2019-01-01
  6. [less-than]
    http://localhost:7076/demoService/search?filters=createdOn.lt:2019-01-01
  7. [greater-than-equal-to]
    http://localhost:7076/demoService/search?filters=createdOn.ge:2019-01-01
  8. [less-than-equal-to]
    http://localhost:7076/demoService/search?filters=createdOn.le:2019-01-01
  9. [like]
    http://localhost:7076/demoService/search?filters=createdBy.like:abhi
  10. [not-like]
    http://localhost:7076/demoService/search?filters=createdBy.nl:abhinav
  11. [in]
    http://localhost:7076/demoService/search?filters=id.in:1,2,3,4,5
  12. [jsonb_path_exists]
    http://localhost:8081/api/v1/order/search?filters=id.eq:8;searchableMetadata.jsonb_path_exists:gst,code,0
Since {"code"} is array in above JSON, its followed by an index {0}

13. [jsonb_path_equals]
http://localhost:8081/api/v1/order/search?filters=id.eq:8;searchableMetadata.jsonb_path_equals:delivery_city|jaipur

{"%7C"} is url encoding for pipe {|}, delimiter for json path and value it should match to

14. [jsonb_path_contains]
http://localhost:8081/api/v1/order/search?filters=id.eq:8;searchableMetadata.jsonb_path_contains:code%7C18

{"%7C"} is url encoding for pipe {|}, delimiter for json path and value it should contain.
In above JSON {"code"} returns another JSON which contains 18.

15. Multiple Search params can be combined via AND ‘;’
[equals and like]
http://localhost:7076/demoService/search?filters=id.eq:1 ; createdBy.like:abhinav

16. OR operator can be applied between multiple search params
http://{{host}}/sku/search?filters=categoryId.in:8250,1234;skuCode.eq:LUC00001__version.eq:2;id.eq:22
This results in query filter where (category_id in (8250, 1234) and sku_code = ‘LUC00001’) or (version = 2 and id = 22)

16. In case of collection present in an Entity
http://localhost:7076/demoService/search?filters=demoDetail-id.eq:1;createdBy.like:abhinav

demoDetail is the name of collection field

17. DateTime Format Query
http://localhost:7076/demoService/search?filters=createdAt.ge:2020-08-01T00:00:00.000
http://localhost:7076/demoService/search?filters=createdAt.ge:2020-08-01T00:00:00.000Z

gRPC Integration

Before you read any further, Knowledge of protocol buffers and gRPC is a prerequisite. Please use the following links to develop a basic understanding.
https://developers.google.com/protocol-buffers/docs/proto3#generating
https://medium.com/@tripathi.abhinav01/grpc-for-spring-boot-807bd9483b8e

gRPC integration is inbuilt in Service Framework. All you have to do is define the configs

Make sure you have followed the Gradle config setup mentioned at the start of the article. In publish.gradle define the variable packageName which should be the package name of your project.

def packageName = 'com/abhinav/demo'

In application.properties define property grpc.port at which grpc server will run.

This port is different from server.port at which the REST server runs

Annotate all the GRPC controllers with @GrpcService annotation.

To publish the gRPC clients to S3. Use the following command

./gradlew publish

Swagger Setup

  • Annotate the Main Application class with @ComponentScan({“com.avail”})
@SpringBootApplication
@ComponentScan({"com.livspace"})
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
  • Once the above step is done. Open the following URL to check the auto-generated API Documentation
http://{host}:{port}/swagger-ui.html

[Example] http://localhost:7076/swagger-ui.html

Query DSL

  • To utilize the features of Query DSL. Follow the Gradle config steps mentioned at the start of the article
  • Query DSL creates a new Class in javaGeneratedSources directory on runtime for every @Entity annotated class
    For DemoEntity, QueryDSL generates a QDemoEntity.java class
    How to use queryDSL,
QDemoEntity demo = QDemoEntity.demoEntity;
BooleanExpression predicate = demo.id.eq(1).and(demo.createdBy.like(abhinav));
List<DemoEntity> data = demoRepository.findAll(predicate)

Ideally with Criteria query, we would have written the following code

CriteriaBuilder builder = em.getCriteriaBuilder();
CriteriaQuery<Customer> query = builder.createQuery(DemoEntity.class);
Root<Demo> root = query.from(Demo.class);
Predicate predicate1 = builder.equal(root.get(id), 1 );
Predicate predicate2 = builder.like(root.get(createdBy), abhinav);
query.where(Arrays.asList(predicate1, predicate2));
em.createQuery(query.select(root)).getResultList();

Query DSL simplifies the code and makes it readable

  • To fetch the data from DB, we can use spring data or query DSL based on the complexity of the query

--

--