Application development divided into 2 parts
1) Backend Development 2) Frontend Development
Backend contains the business logic of our application (webservice calls, validations, email logic, db communication)
Frontend contains user interface of the application (presentation logic)
End users interact with frontend of our application. Frontend will interact with Backend of our application. Backend will execute business logic and it will communicate with database also.
The developers who can do both backend development and frontend development they are called as Fullstack Developers.
Programming Languages used by humans to communicate with computers
Programming Language contains set of instructions & syntaxes
Ex: C, C++, Java, C#, Python etc...
Framework means semi-developed software
Framework provides common logics which are required for application development
Ex: Hibernate, Struts, Spring etc...
If we use framework in our application development then we need to focus only on business logic and frameworks will provide common logics required for our application
Hibernate : It is an ORM framework. It is used to develop only persistence layer of our application.
Struts : It is a web framework. It is used to develop only web layer of our application.
Note: Frameworks like Hibernate and Struts can't be used to develop entire application.
Note: To create a servlet we need to implement Servlet
interface or we need to extend HttpServlet
or GenericServlet
class. That means servlet is forcing us to use servlets specific interface or classes. Hence the project will become tightly coupled with Servlets.
The current version of Spring is 6.x version
Spring works based on POJO and POJI model
POJO : Plain old java object POJI : Plain old java interface
In Spring we can create a simple POJO and we can ask spring to execute our POJO.
Note: Reactive Programming support added in Spring Framework 5.x version
Note: In Spring framework we have to do the configurations manually.
Note: Spring Boot is an extension for Spring Framework. It is not an independent framework, so it can't be used without spring.
Spring is not a single framework. It is collection of modules
Spring framework is loosely coupled. It will not force to use all modules.
Based on project requirement we can choose which modules we need to use from Spring.
Spring Core is the base module for all other modules in Spring. To work with any module in spring first we need to know Spring Core module.
Spring framework has following modules
1) Spring Core 2) Spring Context 3) Spring DAO / Spring JDBC 4) Spring AOP 5) Spring ORM 6) Spring Web MVC 7) Spring Security 8) Spring REST 9) Spring Data 10) Spring Cloud 11) Spring Batch etc...
Spring Core : Spring Core is base module in Spring Framework. It is providing IOC container & Dependency Injection.
Note: IOC & DI are fundamental concepts of Spring Framework.
Spring Context : Spring Context module will deal with configuration related stuff
Spring AOP : AOP stands for Aspect Oriented Programming. Spring AOP is used to deal with Cross Cutting logics in application.
Application = Business Logic + Cross Cutting Logic
Note: We can separate business logic and cross cutting logic using AOP module.
Spring DAO : Spring DAO / Spring JDBC module used to develop Persistence Layer
Spring ORM : Spring ORM module is used to develop Persistence Layer with ORM features
Spring Web MVC : Spring Web MVC module is used to develop Web Applications
Spring Security : Spring Security module is used to implement Security Features in our application
1) Authentication 2) Authorization
Spring REST : Spring REST is used to develop RESTFul services (REST API)
Spring Data : Spring Data is used to develop persistence layer. It provided pre-defined repositories to simplify CRUD operations
Spring Cloud : Spring Cloud providing cloud concepts like Service Registry, Cloud Gateway, Filters, Routing, FeignClient etc...
Spring Batch : Spring Batch is used to implement Batch Processing in our application. Batch Processing means bulk operation.
In our application several classes will be available
One class method wants to talk another class method
We can establish communication among the classes in 2 ways
1) Inheritence 2) Composition
If we use Inheritance or Composition then our classes will become tightly coupled (changes in one class will directly affect its related classes)
UserService
is talking to 6 classes (Java doesn't support multiple inheritance)Note: In both approaches classes will become tightly coupled.
Strategy Design Pattern
Strategy Design Pattern
to develop classes so that Spring Core can easily manage dependencies among the classes with loosely coupling. // Strategy Design Pattern
// IPayment interface
package in.ashokit;
public interface IPayment {
public String pay(double amount);
}
// BillCollector class
package in.ashokit;
public class BillCollector {
private IPayment payment;
public void setPayment(IPayment payment) {
this.payment = payment;
}
public BillCollector() { }
public BillCollector(IPayment payment) {
this.payment = payment;
}
public void payBill(double amount) {
String status = payment.pay(amount);
System.out.println(status);
}
}
The process of injecting one class object into another class object is called as Dependency Injection
We can perform Dependency Injection in 3 ways
1) Setter Injection 2) Constructor Injection 3) Field Injection
// injecting dependent obj into target obj using setter method
BillCollector bc = new BillCollector();
bc.setPayment(new CreditCardPayment()); // setter injection
bc.payBill(1400.0);
// injecting dependent obj into target obj using constructor
BillCollector bc = new BillCollector(new DebitCardPayment()); // constructor injection
bc.payBill(1500.0);
// injecting dependent obj into target obj using field
Class<?> clz = Class.forName("in.ashokit.BillCollector");
Field field = clz.getDeclaredField("payment");
field.setAccessible(true);
Object obj = clz.newInstance();
field.set(obj, new UpiPayment()); // field injection
BillCollector bc = (BillCollector) obj;
bc.payBill(2000.0);
Note: If variable is declared as private then we can't access that variable outside of the class directly. To access private variables outside of the class we have to use Reflection API.
Note: Field injection is strictly not recommended to use because it is violating oops principle.
Note: IOC stands for Inversion of Control. DI stands for Dependency Injection.
In spring framework IOC will perform DI.
We need to provide java classes and configuration as input for IOC
Configuration will provide following information to the IOC container
- name of target class - name of dependent class - type of dependency injection
We can provide configuration to IOC in 3 ways
XML Config Java Config Annotations
IOC container will provide spring beans.
Note: XML approach is not supported in spring boot. Spring boot supports only Java config and annotations.
Spring-Context
dependency in project pom.xml
file <!-- Spring-Context Dependency -->
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.3.20</version>
</dependency>
</dependencies>
Create required classes in our application using Strategy Design Pattern
IPayment.java (I) CreditCardPayment.java DebitCardPayment.java AmexCardPayment.java UpiPayment.java BillCollector.java Test.java
Beans Configuration file
and configure our classes as Spring Beans <!-- Spring-Beans.xml -->
<bean id="creditCard" class="in.ashokit.CreditcardPayment" />
<bean id="debitCard" class="in.ashokit.DebitcardPayment" />
<bean id="amexCard" class="in.ashokit.AmexCard" />
<bean id="upi" class="in.ashokit.UpiPayment" />
<bean id="billCollector" class="in.ashokit.BillCollector">
<property name="payment" ref="creditCard" />
</bean>
// Test class
public class Test {
public static void main(String[] args) throws Exception {
ApplicationContext context = new ClassPathXmlApplicationContext("Spring-Beans.xml");
BillCollector bc = context.getBean("billCollector", BillCollector.class);
bc.collectPayment(1400.00);
}
}
<property />
tag <!-- Setter injection -->
<bean id="billCollector" class="in.ashokit.BillCollector">
<property name="payment" ref="upi" />
</bean>
<constructor-arg />
tag <!-- Constructor injection -->
<bean id="billCollector" class="in.ashokit.BillCollector">
<constructor-arg name="payment" ref="upi" />
</bean>
Note: If a spring bean does not have a non-parameterized constructor in that case performing constructor injection is mandatory because IOC container needs the required parameter(s) to create the object for the spring bean.
<!-- Setter injection overriding constructor injection -->
<bean id="billCollector" class="in.ashokit.BillCollector">
<property name="payment" ref="creditCard" />
<constructor-arg name="payment" ref="upi" />
</bean>
Note: Using XML approach we can't perform field injection.
Bean scope will decide how many objects should be created for a spring bean
The default scope of spring bean is singleton (that means only one object will be created)
We can configure below scopes for spring bean
1) singleton 2) prototype 3) request 4) session
For singleton beans objects will be created when IOC starts (Eager Initialization)
For prototype beans when we call getBean()
method then the object will be created (Lazy Initialization)
For prototype beans every time new object will be created
<!-- Bean Scopes -->
<bean id="car" class="in.ashokit.Car" />
<bean id="engine" class="in.ashokit.Engine" scope="prototype" />
Q) Why default scope for Spring Beans is Singleton ?
Ans: To save memory spring framework made the default scope as singleton. Another reason is most of the beans are stateless (they doesn't hold any per-user or per-request state)
ref
attributeref
attribute we are telling to IOC which object it has to inject <!-- Manual wiring -->
<bean id="billCollector" class="in.ashokit.BillCollector">
<constructor-arg name="payment" ref="upi" />
</bean>
Spring IOC supports Autowiring concept that means Spring IOC having capability to identify the dependent object and inject dependent object into target object
Autowiring having 4 modes
1) byName 2) byType 3) constructor 4) no
byName
mode if target class variable name matched with any bean id/name in bean configuration file then IOC will consider that as dependent bean and it will inject that dependent bean object into target object. <!-- byName mode -->
<bean id="dieselEng" class="in.ashokit.beans.DieselEngine" />
<bean id="car" class="in.ashokit.beans.Car" autowire="byName" />
Note: Every spring bean should have a unique id. We can't configure more than one bean with same id.
byType
mode IOC will check data type of the target class variable. With that datatype if any bean class is configured in the bean configuration file then IOC will identify that as dependent bean and it will inject that dependent bean object into target object. <!-- byType mode-->
<bean id="xyz" class="in.ashokit.beans.DieselEngine" />
<bean id="car" class="in.ashokit.beans.Car" autowire="byType" />
byType
mode. <!-- Ambiguity problem in byType mode -->
<bean id="abc" class="in.ashokit.beans.DieselEngine" />
<bean id="xyz" class="in.ashokit.beans.DieselEngine" />
<bean id="car" class="in.ashokit.beans.Car" autowire="byType" />
autowire-candidate
attribute <!-- How to resolve ambiguity problem -->
<bean id="abc" class="in.ashokit.beans.DieselEngine" />
<bean id="xyz" class="in.ashokit.beans.DieselEngine" autowire-candidate="false" />
<bean id="car" class="in.ashokit.beans.Car" autowire="byType" />
Note: When we configure autowiring with byName or byType
mode then it is performing setter injection by default. If setter method is not available in target class then IOC will not perform dependency injection.
constructor
<!-- constructor mode -->
<bean id="abc" class="in.ashokit.beans.DieselEngine" />
<bean id="car" class="in.ashokit.beans.Car" autowire="constructor" />
constructor
mode will also internally use byType
mode onlyno
<!-- no mode -->
<bean id="car" class="in.ashokit.beans.Car" autowire="no" />
Note: By default autowiring is disabled in spring because deafult mode for autowire is no
Spring is an application development framework
By using spring framework we can develop below types of applications
1) standalone applications 2) web applications 3) distributed applications
Spring framework developed in modular fashion
Spring framework came into market in 2004
The current version of Spring is 6.x
Spring is versatile framework
Spring is non-invasive framework
Note: When we develop application by using spring, programmer is responsible to take care of configurations required for the application.
It is another approach to develop spring based applications with less configurations
Spring Boot is an enhancement of Spring framework
Spring Boot internally uses Spring framework only (Spring Boot is not an replacement for Spring framework)
What type of applications we can develop using spring framework, same type of applications can be developed by using Spring boot also
Spring Boot = Spring Framework - XML Configurations
Spring Boot having below advantages
1) Auto Configuration 2) POM Starters 3) Embedded Servers 4) Rapid Development 5) Actuators
Auto Configuration : Autoconfiguration means boot will identify the configurations required for our application and it will provide that configurations
- Starting IOC container - Creating Connection Pool - Creating SMTP Connections - Web Application deployment to server - Depedency Injections etc....
POM Starters : POM starters are nothing but dependencies that we use to develop our application. POM starters will simplify maven configuration.
- web-starter - jdbc-starter - security-starter - mail-starter
Embedded Servers : Boot will provide web server to run our web applications. Those servers are called as Embedded Server
- Tomcat (default) - Jetty - Netty
Rapid Development : Rapid Development means fast development. We can focus only on business logic because Boot will take care of configurations.
Actuators : Actuators are used to monitor and manage our application. Actuators providing production-ready features for our application.
- How many classes loaded - How many objects created - How many threads are running - URL Mappings Available - Health of the project etc...
We can create boot application in 2 ways
1) using Spring Initializr website (start.spring.io) 2) using IDE
Note: To create spring boot application internet connection is mandatory.
Go to start.spring.io website and generate the project
It will download project zip file
Extract that zip file
Go to IDE -> New -> Import -> Maven -> Existing Maven Project -> Select Project path upto pom.xml file location
Open STS IDE
New -> Spring Starter Project -> Fill the details and create the project
Note: STS IDE will use start.spring.io
internally to create the project
src/main/java
foldersrc/main/resources
foldersrc/test/java
folderpom.xml
fileProject Object Model
Application.java
i.e called as start class of spring boot // Spring Boot start class
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
@SpringBootApplication
annotation is equal to below 3 annotations
@SpringBootConfiguration @EnableAutoConfiguration @ComponentScan
SpringApplication.run()
method contains bootstrapping logic (logic to start the application). It is the entry point for boot application execution.
- Create Bootstrap Context - Start Initializers - Start Listeners - Prepare Environment - Print Banner - Create Context - Refresh Context - Print Time Taken - Call Runners - Return IOC Reference
Create Bootstrap Context : It will load pre-defined classes of spring boot (initializers & listeners)
Start Initializers : Initializers will start and perform initialization.
Start Listeners : Listeners will start and perform actions based on events.
Prepare Environment : It will load configuration provided by us and prepare environment to run our application.
Print Banner : Banner will be printed.
Create Context : IOC container will be created.
Refresh Context : IOC will refresh the beans and create objects to perform dependency injection (making IOC ready to use)
Print Time Taken : Time taken to start the application will be printed.
Call Runners : Runners will be called.
Return IOC Reference : IOC reference will be returned.
Note: All Spring Boot applications must have a start class that contains a main() method and the @SpringBootApplication
annotation.
Note: In start class main() we must call run(), If we don't call run() then our application will executed as a normal Java application i.e Auto Configuration will not be performed.
In Boot application console we can see spring logo that is called as Banner in Spring Boot
We can customize banner text by creating banner.txt
file under src/main/resources
We can keep our company name or project name as banner text in ASCII
format
Spring Boot banner having below 3 modes
1) console ---- > it is default mode 2) log ---- > banner text will be printed on log file 3) off ---- > banner will not be printed
Setting banner mode in application.properties
# application.properties
spring.main.banner-mode = banner-mode
application.yml
# application.yml
spring:
main:
banner-mode: 'banner-mode'
boot-starter
, run() method using AnnotationConfigApplicationContext
class to start IOC containerboot-starter-web
, run() method using AnnotationConfigServletWebServerApplicationContext
class to start IOC containerboot-starter-webflux
, run() method using AnnotationConfigReactiveWebServerApplicationContext
class to start IOC containerboot-starter
dependency is used to develop standalone applicationsboot-starter-web
dependency is used to develop web applicationsboot-starter-webflux
dependency is used to develop applications with Reactive ProgrammingNote: Based on the pom starters, run() will choose the appropriate class to start the IOC.
Runners are getting called at the end of run() method
If we want to execute any logic only once when our application got started then we can use Runners concept
In Spring Boot we have 2 types of Runners
1) ApplicationRunner 2) CommandLineRunner
Both runners are functional interface. They have only one abstract method i.e run() method
main( ) will pass the command line args to run( ), and run( ) will pass these args to runners.
ApplicationRunner
receives the parsed arguments (ApplicationArguments args)
.
CommandLineRunner
receives the raw arguments (String... args)
.
1) send email to management once application started 2) read data from static db tables and store into cache memory
// Spring Boot app using ApplicationRunner
@Component
public class CacheManager implements ApplicationRunner {
@Override
public void run(ApplicationArguments args) throws Exception {
System.out.println("Logic executing to load data into cache....");
}
}
// Spring Boot app using CommandLineRunner
@Component
public class SendAppStartMail implements CommandLineRunner {
@Override
public void run(String... args) throws Exception {
System.out.println("Logic executing to send email....");
}
}
Note: @Component
annotation is used to represent our java class as Spring Bean.
Note: @Order
annotation is used to configure the order of execution of runners.
This annotation used at start class of spring boot
This annotation is equal to below 3 annotations
1) @SpringBootConfiguration 2) @EnableAutoConfiguration 3) @ComponentScan
Note: The package which contains start class is called as base package.
Note: The package names which are starting with base package name are called as sub packages.
in.ashokit (base package) in.ashokit.dao (sub package) in.ashokit.service (sub package) in.ashokit.config (sub package) in.ashokit.rest (sub package) in.ashokit.util (sub package) com.wallmart.security ----> This is not sub package
Note: Only base package and its sub packages will participate in component scan. Any other package will not participate in component scan.
// Configuring multiple base packages for component scan
@SpringBootApplication
@ComponentScan(basePackages = {"in.ashokit", "com.wallmart"})
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
Note: It is highly recommended to follow proper package naming conventions
companyDomainName.projectName.moduleName.layerName
com.tcs.passport (base package) com.tcs.passport.user.dao (sub package) com.tcs.passport.user.service (sub package)
@Bean
is a method level annotation@Bean
annotation@Bean
annotation then IOC will call the method and consider the returned object as a spring bean@Bean
method inside @Configuration
class@Configuration
class is used for configuration purpose // Spring Boot app using @Bean annotation
@Configuration
public class AppConfig {
@Bean
public AppSecurity createInstance() {
AppSecurity as = new AppSecurity();
// custom logic to secure our app
return as;
}
}
Note: @Bean
method can be written inside any spring bean class but it is not recommended. Every spring bean class participates in component scan so during the component scan of the bean class @Bean
method will be discovered by IOC.
Note: We can write @Bean
method inside start class but it is also not recommended. It is possible because start class contains @SpringBootApplication
annotation which implicitly includes @Configuration
annotation.
@SpringBootApplication ---> @SpringBootConfiguration ---> @Configuration
@SpringBootConfiguration
annotation implicitly includes @Configuration
annotation. Due to this start class also acts like a configuration class i.e it can contain @Bean
methods for configuration.pom.xml
file, it will provide auto configuration for our application.@Bean
methods@Component @Service @Repository @Controller @RestController @Configuration @Bean
Note: All the above annotations are class level annotations except @Bean
annotation. @Bean
is a method level annotation.
@Component
is a general purpose annotation to represent java class as Spring Bean
@Service
annotation is a specialization of @Component
annotation. This is also used to represent java class as spring bean. It is allowing for implementation classes to be autodetected through classpath scanning.
For business logic classes we will use @Service annotation
@Repository
annotation is a specialization of @Component
annotation. This is also used to represent java class as spring bean. It is having Data Access Exception translation functionality.
For dao classes we will use @Repository annotation
@Controller
annotation. It is used for C2B communication.@RestController
annotation. It is used for B2B communication.If we want to perform customized configurations then we will use @Configuration
annotation along with @Bean
methods. Based on requirement we can have multiple @Configuration
classes in the project.
Ex: Swagger Config, DB Config, RestTemplate Config, Kafka Config, Redis Config, Security Config etc...
Note: @Controller, @Service, @Repository and @Component
annotations are called stereotype annotations.
Note: Any spring bean class is either @Controller, @Service or @Repository
If it doesn't belongs to any of these then its a @Component
.
Autowiring is used to perform dependency injection
The process of injecting one class object into another class object is called as dependency injection.
In Spring framework IOC container will perform dependency injection
We will provide instructions to IOC to perform DI in 2 ways
1) Manual Wiring (using ref attribute in beans config xml file) 2) Autowiring
Autowiring means IOC will identify dependent object and it will inject into target object
Autowiring will use below modes to perform Dependency Injection
1) byName 2) byType 3) constructor (internally it will use byType only)
To perform autowiring we will use @Autowired
annotation
@Autowired
annotation we can use at 3 places
1) setter method level (Setter Injection - SI) 2) constructor level (Constructor Injection - CI) 3) variable level (Field Injection - FI)
Note: Autowiring supports only referenced types (No support for primitive types)
// ReportDao interface
public interface ReportDao {
public String findData();
}
// ReportDaoImpl class
@Repository
public class ReportDaoImpl implements ReportDao {
public ReportDaoImpl() {
System.out.println("ReportDaoImpl :: Constructor");
}
@Override
public String findData() {
System.out.println("Fetching report data from DB .. !!");
return "Report data";
}
}
// ReportService class
@Service
public class ReportService {
private ReportDao reportDao;
public ReportService() {
System.out.println("ReportService :: Constructor");
}
public void generateReport() {
reportDao.findData();
System.out.println("Generating report .. !!");
}
}
// Start class
@SpringBootApplication
public class Application {
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(Application.class, args);
ReportService reportService = context.getBean(ReportService.class);
reportService.generateReport();
}
}
Note: In the above code IOC will not perform DI.
@Autowired
annotation at setter method level then IOC will perform Setter Injection // Setter Injection using @Autowired
@Service
public class ReportService {
private ReportDao reportDao;
@Autowired
public void setReportDao(ReportDao reportDao) {
System.out.println("setReportDao() method called");
this.reportDao = reportDao;
}
public void generateReport() {
reportDao.findData();
System.out.println("Generating report .. !!");
}
}
required = false
attribute at the optional dependency // Partial Injection using Setter
@Service
public class ReportService {
private OracleReportDaoImpl oracleReportDao;
private MySQLReportDaoImpl mysqlReportDao;
@Autowired
public void setOracleReportDao(OracleReportDaoImpl oracleReportDao) {
System.out.println("setOracleReportDao() method called");
this.oracleReportDao = oracleReportDao;
}
@Autowired(required = false) // optional
public void setMysqlReportDao(MySQLReportDaoImpl mysqlReportDao) {
System.out.println("setMysqlReportDao() method called");
this.mysqlReportDao = mysqlReportDao;
}
public void generateReport() {
oracleReportDao.findData();
if (mysqlReportDao != null) {
mysqlReportDao.findData();
}
System.out.println("Generating report .. !!");
}
}
@Autowired
annotation at constructor level then IOC will perform Constructor Injection // Constructor Injection using @Autowired
@Service
public class ReportService {
private ReportDao reportDao;
@Autowired
public ReportService(ReportDao reportDao) {
System.out.println("ReportService :: Param Constructor");
this.reportDao = reportDao;
}
public void generateReport() {
reportDao.findData();
System.out.println("Generating report .. !!");
}
}
Note: If our class is having exactly one parameterized constructor then @Autowired
annotation is optional because IOC will call the constructor to create the object for the bean, when the constructor is called DI will also be performed.
@Autowired
annotation is mandatory @Service
public class ReportService {
private ReportDao reportDao;
public ReportService() {
System.out.println("ReportService :: Non-Param Constructor");
}
@Autowired
public ReportService(ReportDao reportDao) {
System.out.println("ReportService :: Param Constructor");
this.reportDao = reportDao;
}
public void generateReport() {
reportDao.findData();
System.out.println("Generating report .. !!");
}
}
Note: By default IOC will call non-parameterized constructor to create the object for the bean. To tell the IOC to use parameterized constructor we need to use @Autowired
annotation.
@Autowired
annotation at field level then IOC will perform Field Injection // Field Injection using @Autowired
@Service
public class ReportService {
@Autowired
private ReportDao reportDao;
public void generateReport() {
reportDao.findData();
System.out.println("Generating report .. !!");
}
}
@Component
public class Car {
@Autowired
private IEngine engine;
@Autowired
private IFuel fuel;
@Autowired
private IBreak break;
// logic to handle multiple tasks
}
Note: Above class is responsible for more than one tasks hence it is violating the Single Responsibility Principle.
@Autowired
annotation at setter method@Autowired
annotation then DI will not happenNullPointerException
@Autowired
at constructor level@Autowired
is optionalNote: Out of all these Dependency Injections, Constructor Injection is recommended because Partial Injection not possible and target bean will be created only if dependent bean is available.
@Autowired
annotation it will use byType
mode to identify dependent object // ReportDao interface
public interface ReportDao {
public String findData();
}
// OracleReportDaoImpl class
@Repository
public class OracleReportDaoImpl implements ReportDao {
public OracleReportDaoImpl() {
System.out.println("OracleReportDaoImpl :: Constructor");
}
@Override
public String findData() {
System.out.println("Fetching report data from Oracle DB .. !!");
return "Report data";
}
}
// MySQLReportDaoImpl class
@Repository
public class MySQLReportDaoImpl implements ReportDao {
public MySQLReportDaoImpl() {
System.out.println("MySQLReportDaoImpl :: Constructor");
}
@Override
public String findData() {
System.out.println("Fetching report data from MySQL DB .. !!");
return "Report data";
}
}
// ReportService class
@Service
public class ReportService {
private ReportDao reportDao;
public ReportService() {
System.out.println("ReportService :: Constructor");
}
@Autowired
public void setReportDao(ReportDao reportDao) {
System.out.println("setReportDao() method called");
this.reportDao = reportDao;
}
public void generateReport() {
reportDao.findData();
System.out.println("Generating report .. !!");
}
}
To resolve ambiguity problem we can use below annotations
1) @Qualifier 2) @Primary
@Qualifier
annotation to specify bean name to inject // OracleReportDaoImpl class
@Repository("oracle")
public class OracleReportDaoImpl implements ReportDao {
public OracleReportDaoImpl() {
System.out.println("OracleReportDaoImpl :: Constructor");
}
@Override
public String findData() {
System.out.println("Fetching report data from Oracle DB .. !!");
return "Report data";
}
}
// MySQLReportDaoImpl class
@Repository("mysql")
public class MySQLReportDaoImpl implements ReportDao {
public MySQLReportDaoImpl() {
System.out.println("MySQLReportDaoImpl :: Constructor");
}
@Override
public String findData() {
System.out.println("Fetching report data from MySQL DB .. !!");
return "Report data";
}
}
// ReportService class
@Service
public class ReportService {
private ReportDao reportDao;
public ReportService() {
System.out.println("ReportService :: Constructor");
}
@Autowired
@Qualifier("oracle")
public void setReportDao(ReportDao reportDao) {
System.out.println("setReportDao() method called");
this.reportDao = reportDao;
}
public void generateReport() {
reportDao.findData();
System.out.println("Generating report .. !!");
}
}
OracleReportDaoImpl
will be used by IOC to perform DI.Note: When we use @Qualifier
annotation IOC will use byName
mode to perform DI.
@Qualifier
annotation (byName mode
) then we can use @Primary
annotation@Primary
annotation will be used by IOC to perform DI // MySQLReportDaoImpl class
@Repository
@Primary
public class MySQLReportDaoImpl implements ReportDao {
public MySQLReportDaoImpl() {
System.out.println("MySQLReportDaoImpl :: Constructor");
}
@Override
public String findData() {
System.out.println("Fetching report data from MySQL DB .. !!");
return "Report data";
}
}
// OracleReportDaoImpl class
@Repository
public class OracleReportDaoImpl implements ReportDao {
public OracleReportDaoImpl() {
System.out.println("OracleReportDaoImpl :: Constructor");
}
@Override
public String findData() {
System.out.println("Fetching report data from Oracle DB .. !!");
return "Report data";
}
}
MySQLReportDaoImpl
will be used by IOC to perform DI.Note: @Qualifier
takes precedence over @Primary
. @Primary
means default implementation while @Qualifier
is the specific implementation.
The java class which is managed by IOC is called as Spring Bean
Spring Beans life cycle will be managed by IOC
For Spring Beans obj creation and obj destruction will be taken care by IOC container
We can execute life cycle methods for Spring Bean
In Spring Boot, we can work with bean lifecycle methods in 2 ways
1) By Implementing Interfaces (InitializingBean & DisposableBean) 2) By Using Annotations (@PostConstruct & @PreDestroy)
// Interface approach
@Component
public class Car implements InitializingBean, DisposableBean {
public Car() {
System.out.println("Car :: Constructor");
}
@Override
public void afterPropertiesSet() throws Exception {
System.out.println("afterPropertiesSet() method called");
}
@Override
public void destroy() throws Exception {
System.out.println("destroy() method called");
}
}
Note: If we use interface approach then our bean class will become tightly coupled with spring framework (invasive). Hence interface approach is not recommended to use.
// Annotation approach
@Component
public class Engine {
public Engine() {
System.out.println("Engine :: Constructor");
}
@PostConstruct
public void init() {
System.out.println("Start Engine .. !!");
}
@PreDestroy
public void destroy() {
System.out.println("Stop Engine .. !!");
}
}
Note: In annotation approach method name can be any.
Run Project Lombok jar file using below command
java -jar lombok.jar
Note: We can run Lombok jar file by double clicking also.
STS.exe / Eclipse.exe
file then click on Install
pom.xml
file <!-- Lombok Maven Dependency -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.38</version>
</dependency>
Note: In Spring Boot applications, Lombok can be added to the project by selecting the Lombok dependency at the time of project creation.
@Setter : To generate setter methods for the variables in the class
@Getter : To generate getter methods for the variables in the class
Note: @Setter and @Getter annotations can be used at field (variable) level also.
@NoArgsConstructor : It is used to generate zero-param constructor
@ToString : It is used to generate toString()
method
@EqualsAndHashCode : It is used to generate equals()
method and hashCode()
method
Note: Instead of writing all the above annotations we can use @Data
@Data = @Setter + @Getter + @NoArgsConstructor + @ToString + @EqualsAndHashCode
@AllArgsConstructor : It is used to generate constructor with all variables as constructor arguments.
@RequiredArgsConstructor : It is used to generate constructor with all the final and @NonNull
variables as constructor arguments.
@SneakyThrows : It is used to throw checked exceptions. It is a method level annotation.
Note: If we use @Data
and @AllArgsConstructor
together, then @Data
will not generate a zero-param constructor. So, in this case, to generate a zero-param constructor, we need to use @NoArgsConstructor
as well.
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Person {
private String name;
private Integer age;
}
Project Lombok provided Delombok utility for replacing the Lombok annotations with equivalent source code.
We can delombok an entire source directory by using command line
java -jar lombok.jar delombok src -d src-delomboked
Note: Generally, we don’t Delombok our code in realtime.
public class Person {
private String name;
private Integer age;
public Person(String name, Integer age) {
this.name = name;
this.age = age;
}
}
// This class will not compile
@Data
public class Student extends Person {
private Integer rollNo;
}
Note: Lombok is criticized for using annotations to generate code instead of just storing metadata. If Lombok is removed, the code may not compile, which concerns some developers.
It is used to develop persistence logic in our application
The logic which is responsible to communicate with database is called as persistence logic
Already we have JDBC, Spring JDBC, Hibernate, Spring ORM to develop persistence logic
If we use JDBC or Spring JDBC or Hibernate or Spring ORM then we have to write common logics in all DAO classes to perform CRUD operations
Data JPA simplified persistence logic development by providing pre-defined repository interfaces
Repository interfaces contains methods to perform CRUD operations
Data JPA provided below repository interfaces to perform CRUD operations
1) CrudRepository (Methods to perform CRUD Operations) 2) JpaRepository (Methods to perform CRUD Operations + Sorting + Pagination + QBE)
Note: If we use Data JPA then we don't need to write any methods to perform CRUD operations. Data JPA provided repository interfaces which contains methods to perform CRUD operations.
@Entity : It represents our class as Entity class (It is mandatory annotation)
@Table : It is used to map our class with DB table
Note: @Table
is optional if our class name and table name is same. If we don't give @Table
then it will consider class name as table name.
@Id : It is used to represent our class variable as primary key column (It is mandatory annotation)
@Column : It is used to map our class variables with DB table columns
Note: @Column
is optional if variable name and column name is same. If we don't give @Column
then it will consider variable name as column name.
// Entity class
@Entity
@Table(name="CRICKET_PLAYERS")
public class Player {
@Id
@Column(name="PLAYER_ID")
private Integer playerId;
@Column(name="PLAYER_NAME")
private String playerName;
@Column(name="PLAYER_AGE")
private Integer playerAge;
@Column(name="LOCATION")
private String location;
}
Note: For every DB table we should create one Entity class. One entity class will be mapped with only one DB table.
Note: If we use JDBC then entity classes and mapping between entity classes and DB tables are not required because JDBC uses SQL queries which already defines table structure and data is handled manually using ResultSet
.
// Syntax to write repository interface
public interface EntityRepository extends CrudRepository<Entity, ID> {
}
// Repository interface
public interface PlayerRepository extends CrudRepository<Player, Serializable> {
}
Note: If the type of primary key is not known then we can use Serializable
to make our code generic. But it is not recommended.
Note: When our interface extending properties from JPA repository interface then JPA will provide implementation for our interface during runtime using Proxy Design Pattern.
Datasource properties represents with which database we want connect
- DB URL - DB Uname - DB Pwd - DB Driver Class
We will configure datasource properties in application.properties
or application.yml
file
# datasource properties
spring.datasource.url=jdbc:mysql://localhost:3306/sbms
spring.datasource.username=ashokit
spring.datasource.password=AshokIT@123
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
application.properties
or application.yml
fileauto-ddl
propertyshow-sql
propertyformat-sql
propertyNote: In realtime, it is not recommended to use ddl-auto
because if it's not configured properly, it can accidentally drop or modify database tables.
# ORM properties
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true
Create spring boot application with below dependencies
a) starter-data-jpa b) mysql-connector
Create entity class using annotations
Create repository interface by extending one of the JPA repository interface
Configure DataSource & ORM properties in application.properties
file
Call repository methods in start class to perform DB operations
// Player entity class
@Entity
@Table(name="CRICKET_PLAYERS")
public class PlayerEntity {
@Id
@Column(name="PLAYER_ID")
private Integer playerId;
@Column(name="PLAYER_NAME")
private String playerName;
@Column(name="PLAYER_AGE")
private Integer playerAge;
private String location;
// getters & setters
}
// PlayerRepository interface
public interface PlayerRepository extends CrudRepository<PlayerEntity, Integer> {
}
# application.properties file
# DataSource properties
spring.datasource.url=jdbc:mysql://localhost:3306/sbms
spring.datasource.username=ashokit
spring.datasource.password=AshokIT@123
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
# ORM properties
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true
// Start class
@SpringBootApplication
public class Application {
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(Application.class, args);
PlayerRepository playerRepository = context.getBean(PlayerRepository.class);
PlayerEntity p = new PlayerEntity();
p.setPlayerId(101);
p.setPlayerName("Sachin");
p.setPlayerAge(54);
p.setLocation("Mumbai");
playerRepository.save(p); // upsert method
}
}
Note: @Repository
is optional on a repository interface because it extends one of the JPA repository interfaces so it will discovered when JPA scans for repository interfaces.
Note: Data JPA will scan for repository interfaces only if our project contains data-jpa starter.
CrudRepository
is an JPA repository interfaceCrudRepository
provided several methods to perform CRUD operationssave() : upsert single record (insert and update)
User u1 = new User(101, "Ramu", "Male", 25, "India");
repository.save(u1);
saveAll() : upsert multiple records (insert and update)
User u2 = new User(102, "Raju", "Male", 26, "India");
User u3 = new User(103, "John", "Male", 30, "USA");
User u4 = new User(104, "Smith", "Male", 32, "Canada");
repository.saveAll(Arrays.asList(u2, u3, u4));
findById() : To retrieve single record using primary key
Optional<User> findById = repository.findById(103);
if(findById.isPresent()) {
System.out.println(findById.get());
}
findAllById() : To retrieve multiple records using primary keys
Iterable<User> allById = repository.findAllById(Arrays.asList(101,102,103));
allById.forEach(user -> {
System.out.println(user);
});
findAll() : To retrieve all records from table
Iterable<User> findAll = repository.findAll();
findAll.forEach(user -> {
System.out.println(user);
});
count() : To get total records count
long count = repository.count();
System.out.println("Total Records in table :: "+ count);
existsById() : To check record presence in table using primary key
boolean existsById = repository.existsById(101);
System.out.println("Record Presence with id - 101 :: " + existsById);
deleteById() : To delete single record using primary key
repository.deleteById(104);
deleteAllById() : To delete multiple records using primary keys
repository.deleteAllById(Arrays.asList(102,103));
delete() : To delete single record using entity object
User u1 = new User(101, "Ramu", "Male", 25, "India");
repository.delete(u1);
deleteAll(..) : To delete multiple records from table using entity objects
User u2 = new User(102, "Raju", "Male", 26, "India");
User u3 = new User(103, "John", "Male", 30, "USA");
User u4 = new User(104, "Smith", "Male", 32, "Canada");
repository.deleteAll(Arrays.asList(u2, u3, u4));
deleteAll() : To delete all records from table
repository.deleteAll();
In CrudRepository
interface we have following methods to retrieve records
1) findById(ID id) ----> to retrieve record based on primary key 2) findAllById(Iterable<ID> ids) ----> to retrieve records based on multiple primary keys 3) findAll() -----> to retrieve all records
Requirement - 1 : Retrieve all user records who belongs to India
SQL : select * from user_master where user_country = 'India';
Requirement - 2 : Retrieve all user records whose age is below 30 years
SQL : select * from user_master where user_age <= 30;
Note: user_age
and user_country
are non-primary columns in the table
To retrieve data based on non-primary key columns we don't have any pre-defined methods
To implement these kind of requirements in Data JPA we have below 2 options
1) findBy Methods 2) Custom Queries
// findBy method syntax
findBy + entityClassVariableName (parameters ...)
Note: findBy methods are written inside repository interfaces (abstract methods)
// Retrieve user data based on country
findByCountry(String cname);
// Retrieve user data based on age
findByAge(Integer uage);
Note: findBy methods are used for only retrieval (select) operations.
Note: If no keyword is specified then by default findBy methods will use Equals
keyword.
// select * from user_master where user_country = ?;
public List<User> findByCountry(String cname);
// select * from user_master where user_age = ?;
public List<User> findByAge(Integer age);
// select * from user_master where user_age >= ?;
public List<User> findByAgeGreaterThanEqual(Integer age);
// select * from user_master where user_country in (?, ?, ? ...);
public List<User> findByCountryIn(List<String> countries);
// select * from user_master where user_country = ? and user_age = ?;
public List<User> findByCountryAndAge(String cname, Integer age);
// select * from user_master where user_country = ? and user_age = ? and user_gender = ?;
public List<User> findByCountryAndAgeAndGender(String cname, Integer age, String gender);
Note: If we are using more then one columns in findBy method then parameters order should be same as column order in the method name.
// It will retrieve data using country and age
public List<User> findByCountryAndAge(String cname, Integer age);
// It will throw exception
public List<User> findByCountryAndAge(Integer age, String cname);
We can write our own query and we can execute that query using JPA that is called as Custom Query
Custom Queries we can write in 2 ways
1) HQL Queries 2) Native SQL Queries
To write custom queries we will use @Query
annotation in repository interface
Note: Using custom queries we can perform all types of CRUD operations.
Note: Hibernate has dialect classes for every database
Note: SQL queries are executed directly on the DB, whereas HQL queries need to be converted into SQL before execution which makes HQL queries are slightly slower than SQL queries.
-- Retrieve all records from table
SQL : select * from user_master
HQL : from User
-- Retrieve users who belongs to country 'India'
SQL : select * from user_master where user_country = 'India'
HQL : from User where country = 'India'
-- Retrieve users who belongs to 'India' and their age is 25
SQL : select * from user_master where user_country = 'India' and user_age = 25
HQL : from User where country = 'India' and age = 25
-- Retrieve userid and username based on country
SQL : select user_id, user_name from user_master where user_country = 'India';
HQL : select userid, username from User where country = 'India'
Note: If we are retrieving specific columns data then we need to start the HQL query with select
keyword. Retrieving specific columns data is called as Projection. If we are not using projection then we can start the HQL query with from
keyword also.
@Query(value = "from User")
public List<User> getAllUsers();
@Query(value = "select * from user_master", nativeQuery = true)
public List<User> getAllUsersNative();
@Query(value = "from User where country = :cname")
public List<User> getUsersByCountry(String cname);
@Query(value = "select * from user_master where user_country = :cname", nativeQuery = true)
public List<User> getUsersByCountryNative(String cname);
@Query(value = "from User where country = :cname and age = :uage")
public List<User> getUsersByCountryAndAge(String cname, Integer uage);
@Query(value = "select * from user_master where user_country = :cname and user_age = :uage", nativeQuery = true)
public List<User> getUsersByCountryAndAgeNative(String cname, Integer uage);
@Query(value = "select username from User where userid = :uid")
public String getUsernamebyUserid(Integer uid);
We can perform DB operations using Data JPA in below 3 ways
1) Predefined Methods 2) findBy Methods 3) Custom Queries
JpaRepository
interface also we can perform CRUD operations on DB tablesJpaRepository
also having several methods to perform CRUD operationsJpaRepository
having support for Sorting + Pagination + QBE (Query By Example)
Note: CrudRepository
interface doesn't support Sorting + Pagination + QBE
findAll()
method like below // Sorting
List<User> users = repository.findAll(Sort.by("username"));
users.forEach(user -> System.out.println(user));
List<User> users = repository.findAll(Sort.by("age").descending());
users.forEach(user -> System.out.println(user));
List<User> users = repository.findAll(Sort.by("gender", "age"));
users.forEach(user -> System.out.println(user));
The process of dividing the records into multiple pages is called as Pagination
If we retrieve all the records at once then we will get performance issues in the application
So if we have lots of records to display then we will divide those records into multiple pages and we will display in frontend
Data will be displayed based on below parameters
Page No --> Current Page (It will came from UI) Page Size --> How many records should be displayed in single page
// Pagination
int pageSize = 5;
int pageNo = 0;
PageRequest pageRequest = PageRequest.of(pageNo, pageSize);
Page<User> page = repository.findAll(pageRequest);
List<User> users = page.getContent();
System.out.println("Total pages :: "+page.getTotalPages());
users.forEach(user -> System.out.println(user));
// Pagination with sorting
int pageSize = 5;
int pageNo = 1;
PageRequest pageRequest = PageRequest.of(pageNo, pageSize, Sort.by("age").descending());
Page<User> page = repository.findAll(pageRequest);
List<User> users = page.getContent();
System.out.println("Total pages :: " + page.getTotalPages());
users.forEach(user -> System.out.println(user));
// Query By Example
User entity = new User();
entity.setGender("Male");
entity.setCountry("India");
Example<User> example = Example.of(entity);
List<User> users = repository.findAll(example);
users.forEach(user -> System.out.println(user));
// QBE with sorting
User entity = new User();
entity.setGender("Male");
List<User> users = repository.findAll(Example.of(entity), Sort.by("age").descending());
users.forEach(user -> System.out.println(user));
// QBE with pagination
User entity = new User();
entity.setGender("Male");
Page<User> page = repository.findAll(Example.of(entity), PageRequest.of(1, 3));
List<User> users = page.getContent();
System.out.println("Total pages :: " + page.getTotalPages());
users.forEach(user -> System.out.println(user));
For every table we need to maintain below 4 columns to analyze the table data
CREATED_ON CREATED_BY UPDATED_ON UPDATED_BY
Note: In realtime we will maintain these 4 columns for every table.
CREATED_BY & UPDATED_BY
columns represents which user created the record and which user updated the record. In web applications we will use logged in username and we will set for these 2 columns.
CREATED_ON & UPDATED_ON
columns represents when record got created and when record got updated.
Instead of setting CREATED_ON & UPDATE_ON
columns values manually we can use below annotations
1) @CreationTimestamp 2) @UpdateTimestamp
// Timestamping
@Data
@Entity
@Table(name="PRODUCT_MASTER")
public class Product {
@Id
@Column(name="PRODUCT_ID")
private Integer pid;
@Column(name="PRODUCT_NAME")
private String pname;
@Column(name="PRODUCT_PRICE")
private Double price;
@CreationTimestamp
@Column(name="CREATED_ON", updatable = false)
private LocalDateTime createdOn;
@UpdateTimestamp
@Column(name="UPDATED_ON", insertable = false)
private LocalDateTime updatedOn;
}
Note: updatable = false
means that column value should not updated when we perform update
operation.
Note: insertable = false
means that column value should not inserted when we perform insert
operation.
Q) Why to use Wrapper classes instead of Primitive data types in Entity class ?
Ans: We use wrapper classes instead of primitive types for entity attributes in Java because wrapper classes have a default value of null. This ensures that if no value is provided, null will be stored in the database.
It is not recommended to set value for primary key column manually
We will use Generators to generate the value for primary key column
To use Generator we will specify @GeneratedValue
annotation on primary key column
@GeneratedValue
annotation is having following predefined generators
1) GenerationType.AUTO (default) 2) GenerationType.SEQUENCE 3) GenerationType.TABLE 4) GenerationType.IDENTITY
For MySQL DB predefined generators will internally use following mechanisms to generate primary keys
GenerationType.AUTO ---> GenerationType.SEQUENCE GenerationType.SEQUENCE ---> table_name_seq (table) GenerationType.TABLE ---> hibernate_sequence (table) GenerationType.IDENTITY ---> Auto Increment
@Id
@Column(name="PRODUCT_ID")
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer pid;
Note: For MySQL DB, GenerationType.IDENTITY
is the recommended generation strategy.
For Oracle DB predefined generators will internally use following mechanisms to generate primary keys
GenerationType.AUTO ---> GenerationType.SEQUENCE GenerationType.SEQUENCE ---> table_name_seq (sequence) GenerationType.TABLE ---> hibernate_sequence (table) GenerationType.IDENTITY ---> Not Supported
@Id
@Column(name="PRODUCT_ID")
@GeneratedValue(strategy = GenerationType.SEQUENCE)
private Integer pid;
Note: For Oracle DB, GenerationType.SEQUENCE
is the recommended generation strategy.
@SequenceGenerator
annotationsequenceName
attribute is used to specify sequence name inside @SequenceGenerator
// Primary key generation using custom sequence
@Data
@Entity
@Table(name="PRODUCT_MASTER")
public class Product {
@Id
@Column(name="PRODUCT_ID")
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "id_gen")
@SequenceGenerator(name = "id_gen", sequenceName = "product_id_seq", allocationSize = 1)
private Integer productid;
@Column(name="PRODUCT_NAME")
private String productName;
@Column(name="PRODUCT_PRICE")
private Double productPrice;
}
Note: Here id_gen
is alias name for our custom sequence. It should be same in @GeneratedValue
and @SequenceGenerator
annotations.
Note: allocationSize
must be equal to Increment by. Default value for allocation size is 50.
IdentifierGenerator
interface and override generate()
methodgenerate()
method contains logic to generate primary key column value according to our requirementProduct Ids ---> PID1, PID2, PID3 ........ Emp Ids ---> TCS1, TCS2, TCS3 ........ Order Ids ---> OD1, OD2, OD3 ........
This primary key value can be divided into two parts
a) Prefix ---> Constant b) Suffix ---> Number
As prefix is fixed we can create a constant for that
For suffix value we can create a sequence in DB
We can map custom generator to primary key column using below two annotations
1) @GenericGenerator 2) @IdGeneratorType
@GenericGenerator
annotation tells Hibernate to use our generator for generating primary keysstrategy
attribute is used to specify our custom generator class inside @GenericGenerator
Note: @GenericGenerator
annotation is deprecated. Instead of this we can use @IdGeneratorType
annotation.
// Entity class
@Data
@Entity
@Table(name="PRODUCT_MASTER")
public class Product {
@Id
@Column(name="PRODUCT_ID")
@GeneratedValue(generator = "id_gen")
@GenericGenerator(name = "id_gen", strategy = "in.ashokit.generator.ProductIdGenerator")
private String productid;
@Column(name="PRODUCT_NAME")
private String productName;
@Column(name="PRODUCT_PRICE")
private Double productPrice;
}
Note: Here id_gen
is alias name for our custom generator. It should be same in @GeneratedValue
and @GenericGenerator
annotations.
// Custom Generator
public class ProductIdGenerator implements IdentifierGenerator {
@Override
public Object generate(SharedSessionContractImplementor session, Object object) {
String prefix = "PID";
String suffix = "";
try {
JdbcConnectionAccess ca = session.getJdbcConnectionAccess();
Connection conn = ca.obtainConnection();
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("select product_id_seq.nextval from dual");
if(rs.next()) {
int seqval = rs.getInt(1);
suffix = String.valueOf(seqval);
}
} catch (Exception e) {
e.printStackTrace();
}
return prefix + suffix;
}
}
@GenericGenerator
annotation (deprecated)@IdGeneratorType
which points to our generator. // Custom annotation
@Retention(RUNTIME)
@Target(FIELD)
@IdGeneratorType(ProductIdGenerator.class)
public @interface ProductIdGenerated {
}
// Entity class
@Data
@Entity
@Table(name="PRODUCT_MASTER")
public class Product {
@Id
@Column(name="PRODUCT_ID")
@ProductIdGenerated
private String productid;
@Column(name="PRODUCT_NAME")
private String productName;
@Column(name="PRODUCT_PRICE")
private Double productPrice;
}
Note: In this approach no changes are required in ProductIdGenerator
.
Table can have more than one column as primary key
If table contains more than one column as primary key then it is called as Composite Primary Key
To work with Composite Primary Key in Data JPA, we have to create two classes
1) Embeddable class (@Embeddable) 2) Entity class with embedded id (@EmbeddedId)
Embeddable class should contain the variables to map with Composite Primary Key column. This class must implement Serializable
interface.
// Embeddable class
@Data
@AllArgsConstructor
@NoArgsConstructor
@Embeddable
public class AccountCompoositeKey implements Serializable {
@Column(name="ACCOUNT_NO")
private Integer accountNo;
@Column(name="IFSC_CODE")
private String ifsc;
}
// Entity class
@Data
@Entity
@Table(name="ACCOUNT__MASTER")
public class Account {
@EmbeddedId
private AccountCompoositeKey compositeKey;
@Column(name="BRAMCH_NAME")
private String branch;
@Column(name="ACCOUNT_BALANCE")
private Double balance;
}
Note: For Composite Primary Key's we can't use Generator. We have to set the values manually.
// How to rollback failed tx
@Transactional(rollbackOn = Exception.class)
public void saveData() {
Employee emp = new Employee();
emp.setEmpId(101);
emp.setEmpName("Raju");
emp.setSalary(25000.0);
empRepo.save(emp);
int i = 10 / 0;
Address add = new Address();
add.setAddId(501);
add.setCity("Pune");
add.setState("MH");
add.setCountry("India");
add.setEmpId(101);
addRepo.save(add);
}
byte[]
in entity class and mark it with @Lob
@Lob
tells JPA that the field should be treated as large object (LOB)columnDefinition = "LONGBLOB"
ensures field can store large binary databyte[]
and we are storing it in output img fileRequirement : Develop Data JPA Application to insert image file into DB and retrieve it from DB.
// Entity class
@Setter
@Getter
@Entity
@Table(name="USER_TBL")
public class User {
@Id
@Column(name="USER_ID")
private Integer userId;
@Column(name="USER_NAME")
private String userName;
@Column(name="USER_EMAIL")
private String email;
@Lob
@Column(name="USER_IMG", columnDefinition = "LONGBLOB")
private byte[] img;
@Override
public String toString() {
return "User [userId=" + userId + ", userName=" + userName + ", email=" + email + "]";
}
}
// Service class
@Service
@AllArgsConstructor
public class UserService {
private UserRepository repo;
@SneakyThrows
public void saveUser() {
String imgPath = "input-img-path";
User user = new User();
user.setUserId(101);
user.setUserName("Raju");
user.setEmail("raju@gmail.com");
user.setImg(Files.readAllBytes(Path.of(imgPath)));
repo.save(user);
System.out.println("Record Inserted .. !!");
}
@SneakyThrows
public void findUser() {
String imgPath = "output-img-path";
User user = repo.findById(101).get();
System.out.println(user);
Files.write(Path.of(imgPath), user.getImg());
System.out.println("Output img stored .. !!");
}
}
Note: Instead of using byte[]
, we can use Blob
to store images. If we use Blob
, we don’t need to add @Lob
or specify columnDefinition = "LONGBLOB"
because JPA automatically treats Blob
as a large object.
Note: Environment means platform to run our application (Linux VM, DB Server, Log Server etc...)
We will have below Environments in the realtime
1) DEV 2) QA 3) UAT 4) PILOT 5) PROD
If we want to deploy our code into multiple environments then we have to change configuration properties in application.properties
file for every deployment (It is not recommended).
1) DB properties 2) SMTP properties 3) Logging properties 4) Redis Server properties 5) Apache Kafka properties 6) Webservices/REST API Endpoint URL
To avoid this problem we will use Profiles in Spring Boot
Profiles are used to configure environment specific properties. For every environment we will create one separate properties file.
application-dev.properties application-qa.properties application-uat.properties application-pilot.properties application-prod.properties
application.properties
we will activate the profile based on our requirement # Activating dev profile
spring.profiles.active=dev
application.yml
file for configuration then we can create multiple profiles in single yml file and activate any profile according to our requirement.Spring Web MVC is one module available in the Spring framework
Using Spring Web MVC module we can develop 2 types of applications
1) Web Applications 2) Distributed Applications (Webservices)
Web Applications will have user interface (UI)
Customers can access web applications directly using internet
Web Applications are meant for customer to business communication (C2B)
Ex: facebook, gmail, linkedin, naukri etc...
Distributed Applications are meant for business to business communication (B2B)
If one application is communicating with another application then we call them as Distributed Applications
Distributed applications we are developing to reuse the logic of one application in another application.
MakeMyTrip -----------> IRCTC Passport -----------> AADHAR Gpay ---------------> Banking Apps
Distributed Applications we can develop in 2 ways
1) SOAP Webservices 2) RESTFul Services
Note: SOAP Webservices & RESTFul Services can be developed using Spring Web MVC.
Spring Web MVC Architecture having below components
1) DispatcherServlet (Front Controller) 2) HandlerMapper 3) Controller (Request Handler) 4) ModelAndView 5) ViewResolver 6) View
DispatcherServlet
is a predefined servlet class in Spring Web MVCDispatcherServlet
is also called as Front Controller / Framework Servlet
HandlerMapper
is a predefined class in Spring Web MVCHandlerMapper
is used to identify Request Handler
Controller
classHandlerMapper
will identify Request Handler
based on URL PatternController
is a class which contains logic to handle request and responseController
is also called as Request Handler
Controller
classes using @Controller
annotationController
will return data to DispatcherServlet
in the form of ModelAndView
objectModelAndView
object Model
represents data in key-value pair format and View
represents presentation logic file nameModelAndView
objectViewResolver
is used to identify where view files are available in our projectViewResolver
is responsible to identify physical location of view filesView
is used to render model data on physical view file to displayCreate Spring Starter application with below dependencies
a) spring-boot-starter-web b) tomcat-embed-jasper c) devtools
Create Controller class and write required methods
Create view files which contains the presentation logic
Configure ViewResolver
in application.properties
file with prefix and suffix
Run the application and test it
Note: web-starter
will provide the support to build web apps with MVC architecture and it provides Tomcat
as default embedded container (we no need to setup server manually)
Note: tomcat-embed-jasper
will provide the support to work with JSP files in Spring Web MVC
Note: devtools
is used to re-start the server when changes happened in the code
Note: Java class will be represented as a Spring Controller using @Controller
annotation
Note: Controller class methods should be binded to HTTP Protocol methods to handle HTTP Requests
Note: If we select Packaging
as Jar
then webapp
folder will not be created. If we select Packaging
as War
then it will be created automatically.
<!-- tomcat-embed-jasper dependency -->
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-jasper</artifactId>
</dependency>
// Controller class
@Controller
public class WelcomeController {
@GetMapping("/welcome")
public ModelAndView getWelcomeMsg() {
ModelAndView mav = new ModelAndView();
// setting model data
mav.addObject("msg", "Welcome to Ashok IT !!");
// setting view name
mav.setViewName("index");
return mav;
}
}
Note: Controller is returning just view name (without extension) which makes it loosely coupled with the presentation technology.
# view resolver configuration
spring.mvc.view.prefix=/views/
spring.mvc.view.suffix=.jsp
<!-- Index Page -->
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Home</title>
</head>
<body>
<h2>${msg}</h2>
</body>
</html>
Context-Path represents name of our application
In Spring Boot, the default context-path is empty
In Spring Boot we can set our own context-path using below property in application.properties
file
server.servlet.context-path=/webapp
When we set context-path we have to access our application using context-path in the URL.
Note: By default Embedded Tomcat Server will run on the port number 8080.
We can change Embedded Tomcat Server port number using below property in application.properties
file
server.port=9090
Note: Every controller class method should be mapped to a unique url-pattern. If the url-pattern is same then the request type must be different otherwise it will result url-pattern collision.
We can send data from controller to UI in multiple ways
1) ModelAndView 2) Model 3) @ResponseBody
ModelAndView
object is returnedModelAndView
object contains both model data and the view name // Using ModelAndView object
@Controller
public class WelcomeController {
@GetMapping("/welcome")
public ModelAndView getWelcomeMsg() {
ModelAndView mav = new ModelAndView();
// setting model data
mav.addObject("msg", "Welcome to SBMS !!");
// setting view name
mav.setViewName("welcome");
return mav;
}
}
Model
object and returns the view name // Using Model object
@Controller
public class GreetController {
@GetMapping("/greet")
public String getGreetMsg(Model model) {
// setting model data
model.addAttribute("msg", "Good Afternoon !!");
// returning view name
return "greet";
}
}
Note: Generally returning view name is more common instead of ModelAndView
object.
@ResponeBody
annotationNote: In realtime we will use this approach to develop REST API's (distributed apps)
// Using @ResponseBody annotation
@Controller
public class WishController {
@ResponseBody
@GetMapping("/wish")
public String getWishMsg() {
// returning response directly
return "All the Best !!";
}
}
Note: If we want to develop the presentation logic within the same app (JSP, Thymeleaf) then the controllers should return view name. But if we want to develop presentation logic as a separate project (Angular, React) or presentation logic is not required for our app (B2B app) then the controllers should return response directly.
Note: By default, the value returned by controller methods is considered as view name, unless we use @RestController
or @ResponseBody
.
@RestController = @Controller + @ResponseBody
key-value
pair using Model
objectkey
value // Binding class
@Data
@AllArgsConstructor
public class Book {
private Integer bookId;
private String bookName;
private Double bookPrice;
}
// Controller class
@Controller
public class BookController {
@GetMapping("/book")
public String getBookData(Model model) {
// creating binding object
Book obj = new Book(101, "Spring", 650.0);
// seting model data
model.addAttribute("book", obj);
// returning view name
return "book";
}
}
<!-- Book Page -->
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Book Page</title>
</head>
<body>
<h2>Book Details</h2>
<table border="1">
<thead>
<tr>
<th>Book Id</th>
<th>Book Name</th>
<th>Book Price</th>
</tr>
</thead>
<tbody>
<tr>
<td>${book.bookId}</td>
<td>${book.bookName}</td>
<td>${book.bookPrice}</td>
</tr>
</tbody>
</table>
</body>
</html>
Assignment: Develop Spring Boot web application to display multiple books in a table format.
<!-- JSTL dependencies -->
<dependency>
<groupId>jakarta.servlet.jsp.jstl</groupId>
<artifactId>jakarta.servlet.jsp.jstl-api</artifactId>
</dependency>
<dependency>
<groupId>org.glassfish.web</groupId>
<artifactId>jakarta.servlet.jsp.jstl</artifactId>
</dependency>
Note: We have added JSTL dependency to iterate the List in UI.
// Binding class
@Data
@AllArgsConstructor
public class Book {
private Integer bookId;
private String bookName;
private Double bookPrice;
}
// Controller class
@Controller
public class BookController {
@GetMapping("/books")
public String getBooks(Model model) {
Book b1 = new Book(101, "Spring", 650.0);
Book b2 = new Book(102, "Hibernate", 450.0);
Book b3 = new Book(103, "DevOps", 850.0);
// setting data
model.addAttribute("books", Arrays.asList(b1, b2, b3));
// returning view name
return "books";
}
}
<!-- Books Page -->
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="jakarta.tags.core" %>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Books Page</title>
</head>
<body>
<h2>Book Details</h2>
<table border="1">
<thead>
<tr>
<th>Book Id</th>
<th>Book Name</th>
<th>Book Price</th>
</tr>
</thead>
<tbody>
<c:forEach items="${books}" var="book">
<tr>
<td>${book.bookId}</td>
<td>${book.bookName}</td>
<td>${book.bookPrice}</td>
</tr>
</c:forEach>
</tbody>
</table>
</body>
</html>
We used JSP as a presentation technology in our Spring Web MVC based applications
JSP can't be executed in browser directly
When the request comes to JSP then internally JSP will be converted to Servlet and that Servlet will send response to browser
When we use JSP for presentation then burden will be increased on server because every JSP should be converted into Servlet to produce the response
To overcome problems of JSP we can use Thymeleaf as a presentation technology
Thymeleaf is a template engine that can be used in HTML pages directly
HTML pages will execute in browser directly hence Thymeleaf performance will be fast as compared to JSP
In general, HTML pages are used for static data. If we use thymeleaf in HTML then we can add dynamic nature to HTML pages.
We can develop spring boot application with thymeleaf as a presentation technology
To use Thymeleaf in spring boot we have below starter
spring-boot-starter-thymeleaf
Create Spring Starter Project with below dependencies
a) web-starter b) thymeleaf-starter c) devtools
Create Controller with required methods
Create Thymeleaf templates in src/main/resources/templates
folder (file extension .html)
Run the application and test it
Note: No need to configure view resolver because Spring Boot will auto detect Thymeleaf template files and will process them.
// Controller class
@Controller
public class WelcomeController {
@GetMapping("/welcome")
public String getWelcomeMsg(Model model) {
model.addAttribute("msg", "Welcome to Ashok IT !!");
return "welcome";
}
}
<!-- Welcome Page -->
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Welcome Page</title>
</head>
<body>
<p th:text=${msg}/>
</body>
</html>
Note: Below line is used to tell the browser that we are using Thymeleaf in this page and th:
is the prefix.
<html xmlns:th="http://www.thymeleaf.org">
Create boot app with below dependencies
a) web-starter b) devtools c) lombok d) thymeleaf-starter
Create Form Binding class
Create a controller class with required methods
a) method to display empty form (GET Request Method) b) method to handle form submission (POST Request Method)
Create view files with presentation logic
Run the application and test it
// Binding class
@Data
public class Product {
private Integer productId;
private String productName;
private Double productPrice;
}
// Controller class
@Controller
public class ProductController {
// method to display empty form
@GetMapping("/")
public String loadForm(Model model) {
Product obj = new Product();
model.addAttribute("product", obj);
return "index";
}
// method to handle form submission
@PostMapping("/product")
public String submitForm(Product product) {
System.out.println(product);
return "success";
}
}
<!-- Product Page -->
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Product Page</title>
</head>
<body>
<h2>Product Form</h2>
<form th:action="@{/product}" th:object="${product}" method="POST">
<table>
<tr>
<td>Product Id</td>
<td><input type="text" th:field="*{productId}" /></td>
</tr>
<tr>
<td>Product Name</td>
<td><input type="text" th:field="*{productName}" /></td>
</tr>
<tr>
<td>Product Price</td>
<td><input type="text" th:field="*{productPrice}" /></td>
</tr>
<tr>
<td></td>
<td><input type="submit" value="Save" /></td>
</tr>
</table>
</form>
</body>
</html>
<!-- Success Page -->
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Success</title>
</head>
<body>
<h2>Product Saved Successfully</h2>
<a href="/">Go Back</a>
</body>
</html>
Forms are used to capture data from the user
To make sure users are entering valid data we will perform validations on the data
Spring Web MVC having support to perform form validations
We can perform form validations by using below annotations
@NotEmpty @NotNull @Size @Min @Max
To use form validations in spring boot we have below starter
spring-boot-starter-validations
Note: It is not recommended to use primitive types for binding class variables because they can't hold null values. Instead, they have default values like 0 or false, which makes it difficult to perform validations.
// Binding class
@Data
public class Person {
@Size(min = 4, max = 16, message = "Name should be 4 to 16 characters")
@NotEmpty(message = "Name is required")
private String name;
@Min(value = 18, message = "Minimum age should be 18")
@NotNull(message = "Age is required")
private Integer age;
}
// Controller class
@Controller
public class PersonController {
@GetMapping("/")
public String loadForm(Model model) {
Person obj = new Person();
model.addAttribute("person", obj);
return "index";
}
@PostMapping("/person")
public String submitForm(@Valid Person person, BindingResult result, Model model) {
// to load same page, if validation fails
if(result.hasErrors()) {
return "index";
}
System.out.println(person);
model.addAttribute("msg", "Person Recod Saved !!");
return "success";
}
}
Note: To validate the form data @Valid
annotation is used. It will validate the data and store the result into BindingResult
object. BindingResult
object is used to check errors.
<!-- Person Form -->
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Person Page</title>
<style>
.error {
color: red
}
</style>
</head>
<body>
<h2>Person Form</h2>
<form th:action="@{/person}" th:object="${person}" method="POST">
<table>
<tr>
<td>Name</td>
<td><input type="text" th:field="*{name}" /></td>
<!-- To print validation message -->
<td class="error" th:errors="*{name}"></td>
</tr>
<tr>
<td>Age</td>
<td><input type="text" th:field="*{age}" /></td>
<!-- To print validation message -->
<td class="error" th:errors="*{age}"></td>
</tr>
<tr>
<td></td>
<td><input type="submit" value="Save" /></td>
</tr>
</table>
</form>
</body>
</html>
<!-- Success Page -->
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Success Page</title>
</head>
<body>
<p th:text="${msg}" />
<a href="/">Go Back</a>
</body>
</html>
Assignment : Create a User registration form with below fields and perform validations.
1) Username 2) Password 3) Email 4) Phno 5) Age
// Binding class
@Data
public class User {
@Size(min = 4, max = 16, message = "Username must be 4 to 16 characters")
@NotEmpty(message = "Username is required")
private String uname;
@Size(min = 8, max = 16, message = "Password must be 8 to 16 characters")
@NotEmpty(message = "Password is required")
private String pwd;
@Email(message = "Enter valid email")
@NotEmpty(message = "Email is required")
private String email;
@Size(min = 10, message = "Phone No atleast have 10 digits")
@NotEmpty(message = "Phone No is required")
private String phno;
@Min(value = 12, message = "Minimum age should be 12")
@NotNull(message = "Age is required")
private Integer age;
}
Note: @Email
annotation is used to validate email.
// Controller class
@Controller
public class UserController {
@GetMapping("/")
public String loadForm(Model model) {
User obj = new User();
model.addAttribute("user", obj);
return "index";
}
@PostMapping("/user")
public String submitForm(@Valid User user, BindingResult result, Model model) {
if(result.hasErrors()) {
return "index";
}
System.out.println(user);
model.addAttribute("msg", "User Record Saved !!");
return "success";
}
}
<!-- User Form -->
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>User Page</title>
<style type="text/css">
.error{
color : red
}
</style>
</head>
<body>
<h2>User Form</h2>
<form th:action="@{/user}" th:object="${user}" method="POST" >
<table>
<tr>
<td>Username</td>
<td><input type="text" th:field="*{uname}" /></td>
<td class="error" th:errors="*{uname}"></td>
</tr>
<tr>
<td>Password</td>
<td><input type="password" th:field="*{pwd}" /></td>
<td class="error" th:errors="*{pwd}"></td>
</tr>
<tr>
<td>Email</td>
<td><input type="text" th:field="*{email}" /></td>
<td class="error" th:errors="*{email}"></td>
</tr>
<tr>
<td>Phone No</td>
<td><input type="text" th:field="*{phno}" /></td>
<td class="error" th:errors="*{phno}"></td>
</tr>
<tr>
<td>Age</td>
<td><input type="number" th:field="*{age}" /></td>
<td class="error" th:errors="*{age}"></td>
</tr>
<tr>
<td></td>
<td><input type="submit" value="Save" /></td>
</tr>
</table>
</form>
</body>
</html>
Note: We have taken email field type="text"
to avoid the client side validation.
<!-- Success Page -->
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Success Page</title>
</head>
<body>
<p th:text="${msg}" />
<a href="/">Go Back</a>
</body>
</html>
Spring Web MVC provided Form Tag library to develop forms easily
Spring Form Tag Library contains several tags
<form:form> <form:input> <form:password> <form:radioButton> & <form:radioButtons> <form:select> <form:option> & <form:options> <form:checkbox> & <form:checkboxes> <form:hidden> <form:error>
To work with Spring Form Tag library we need to use below taglib directive
<%@ taglib uri="http://www.springframework.org/tags/form" prefix="form" %>
Note: To map Binding class with From we use modelAttribute
attribute.
Note: To map Binding class variable with Form field we use path
attribute.
// Binding class
@Data
public class Product {
private Integer productId;
private String productName;
private Double productPrice;
}
// Controller class
@Controller
public class ProductController {
@GetMapping("/")
public String loadForm(Model model) {
Product obj = new Product();
model.addAttribute("product", obj);
return "index";
}
@PostMapping("/product")
public String submitForm(Product p, Model model) {
System.out.println(p);
model.addAttribute("msg", "Product Saved Successfully");
return "success";
}
}
<!-- Home Page -->
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ taglib uri="http://www.springframework.org/tags/form" prefix="form"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Product Page</title>
</head>
<body>
<h2>Product Form</h2>
<form:form action="product" modelAttribute="product" method="POST">
<table>
<tr>
<td>Product Id</td>
<td><form:input path="productId"/></td>
</tr>
<tr>
<td>Product Name</td>
<td><form:input path="productName"/></td>
</tr>
<tr>
<td>Product Price</td>
<td><form:input path="productPrice"/></td>
</tr>
<tr>
<td></td>
<td><input type="submit" value="Save" /></td>
</tr>
</table>
</form:form>
</body>
</html>
<!-- Success Page -->
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Success Page</title>
</head>
<body>
<h2>${msg}</h2>
<a href="/">Go Back</a>
</body>
</html>
Spring Boot provided embedded containers to run our web applications
When we add web-starter
by default it provides tomcat
as default embedded container
In spring boot we have multiple embedded containers
1) Tomcat 2) Jetty 3) Netty 4) Undertow etc...
Note: We can deploy spring boot application into external servers also as a war file.
pom.xml
file <dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jetty</artifactId>
</dependency>
REST stands for Representational State Transfer
RESTFul services are used to develop Distributed Applications with interoperability
If one application is communicating with another application then those are called as Distributed Applications
Interoperability means irrespective of the platform and language applications can communicate
Java App <-------> .Net App .Net App <-------> Python App Python App <-------> Java App
Distributed applications are used for B2B communications
B2B means Business to Business Communication
Distributed Applications will re-use the services of one application in another application
RESTFul Services are providing interoperability
Two actors will be involved in Distributed Applications development
1) Provider 2) Consumer
The application which is providing business services is called as Provider Application
The application which is accessing business services is called as Consumer Application
Note: One provider application can have multiple consumer applications and one consumer application can also have multiple provider applications.
Text/XML/JSON
formatNote: XML and JSON are interoperable (platform independent & language independent)
Note: JSON is recommended format to exchange the data from one application to another application. XML is outdated.
Q) Why we don't use objects to exchange data between Consumer and Provider application ?
Ans: Objects are language dependent. If we use objects to exchange data then we can't achieve interoperability.
To start our journey with RESTFul services development we should be good in below areas
1) HTTP Protocol (Methods, Status Codes, Request Structure & Response Structure) 2) XML and JAX-B API 3) JSON and Jackson API
Hyper Text Transfer Protocol
Consumer application will send HTTP request to Provider application using HTTP methods
HTTP method will represent what type of operation Consumer application wants to perform with Provider application
a) GET b) POST c) PUT d) DELETE
Note: Path Params & Query Params data will be displayed in the URL hence it is not recommended to send sensitive data using Path Params & Query Params.
Q) Why GET request does not have request body ?
Ans: GET request is not meant for sending the data it is meant for retrieving the data hence it does not contain the request body.
Note: Request Body is the recommended approach to send sensitive data.
HTTP Request Structure will have
Initial Request Line ---> HTTP Method + URL + HTTP Version Request Header ---> Request metadata in key-value pair format Blank Line ---> To separate request header & body Request Body ---> It will contain request payload (business data)
HTTP Response Structure will have
Initial Response Line ---> HTTP Version + Status Code + Status Msg Response Header ---> Response metadata in key-value pair format Blank Line ---> To separate response header & body Response Body ---> It will contain response payload (business data)
HTTP Status Codes will represent how the request is processed by the provider
1xx ---> Information Codes (100 - 199) 2xx ---> Success Codes (200 - 299) 3xx ---> Redirection Codes (300 - 399) 4xx ---> Client Error Codes (400 - 499) 5xx ---> Server Error Codes (500 - 599)
Extensible Markup Language
W3C (World Wide Web Consortium)
<!-- XML element -->
<name> Ashok IT </name>
We will have two types of elements in the XML
1) Simple Element 2) Compound Element (Complex Element)
The element which contains data directly is called as Simple Element
<!-- Simple element -->
<name> Ashok IT </name>
<type> Educational </type>
<!-- Compound element -->
<person>
<id> 101 </id>
<name> Raju </name>
</person>
Note: Here person
is a compound element and it contains id
& name
as child elements.
<!-- XML element with attribute -->
<student branch="CSE">
<id> 101 </id>
<name> Mahesh </name>
</student>
Note: Here student
is the compound element, branch
is the attribute and id
& name
are the simple elements.
<!-- Root element with multiple child elements -->
<persons>
<person>
<id> 101 </id>
<name> Raju </name>
</person>
<person>
<id> 102 </id>
<name> Rani </name>
</person>
</persons>
Note: Here persons
is the root element.
Java Architecture for XML Binding
Sun Microsystems
Note: Marshalling & Un-marshalling will be performed at both Consumer and Provider sides hence binding classes are also required at both sides.
Note: Earlier XSD is used to generate binding classes but XSD became outdated. Nowadays we are writing binding classes directly without XSD.
Note: XSD stands for XML Schema Definition
which is used to represent the structure of XML document.
@XmlRootElement : It is used to represent the Java class as the root element of the XML document.
@XmlAccessorType : It s used to specify whether to use fields or setters & getters to perform marshalling and un-marshalling. By default setters & getters will be used to perform marshalling & un-marshalling.
XmlAccessType.FIELD ---> To use fields for marshall and un-marshall XmlAccessType.PROPERTY ---> To use setters & getters for marshall & un-marshall
Note: If you are using fields to perform marshalling & un-marshalling then the order of elements in the XML document will be same as the order of variables in the Java class.
Note: If you are using setters & getters to perform marshalling & un-marshalling then the XML document will have elements in alphabetical order.
@XmlAccessorOrder : It is used to specify the order in which elements are accessed during marshalling and un-marshalling.
@XmlAccessorOrder(XmlAccessOrder.ALPHABETICAL)
@XmlElement : It is used to change the name of an element in the XML document.
@XmlElement(name = "elementName")
@XmlAttribute : It is used to represents a variable as an attribute in the XML document.
@XmlTransient : It is used to skip a variable in marshalling and un-marshalling.
Note: By default every variable of Java class will be considered as element and variable name will be considered as element name in the XML document.
Note: If we are using JDK 1.9 or above then we need to add JAX-B dependency in pom.xml file because JAX-B is part of JDK only upto 1.8 version.
<!-- JAX-B dependency -->
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<version>2.3.1</version>
</dependency>
// Binding class
@Data
@AllArgsConstructor
@NoArgsConstructor
@XmlRootElement
@XmlAccessorType(XmlAccessType.FIELD)
public class Person {
private Integer id;
private String name;
@XmlAttribute
private Integer age;
@XmlElement(name = "phoneNum")
private Long phno;
@XmlTransient
private Address address;
}
// Java to XML converter
public class JavaToXml {
public void convertJavaToXml(Person person) throws Exception {
JAXBContext context = JAXBContext.newInstance(Person.class);
Marshaller marshaller = context.createMarshaller();
marshaller.marshal(person, new File("Person.xml"));
System.out.println("Marshalling completed .. !!");
}
}
// XML to Java converter
public class XmlToJava {
public void convertXmlToJava() throws Exception {
JAXBContext context = JAXBContext.newInstance(Person.class);
Unmarshaller unmarshaller = context.createUnmarshaller();
Person person = (Person) unmarshaller.unmarshal(new File("Person.xml"));
System.out.println(person);
System.out.println("Un-marshalling completed .. !!");
}
}
JavaScript Object Notation
{
"id": 101,
"name": "Raju",
"age": 24
}
To work with JSON data we have below third party APIs
1) Jackson API 2) Gson API
Note: JSON is preferred over XML to exchange the data between applications.
Q) Why JSON is preferred over XML for exchanging the data between applications ?
Ans: XML uses opening and closing tags for each element which increases the size of the data and consumes more memory whereas JSON will represent the data in key-value pair format which is more simple and light weight. Hence JSON is preferred over XML.
It is used to convert Java object to JSON data and vice versa
The process of converting Java object into JSON data is called as Serialization
The process of converting JSON data into Java object is called as De-serialization
Spring Boot will internally use Jackson API only
Jackson API provided ObjectMapper
class to perform conversions
ObjectMapper
class contains below methods
writeValue() ---> To convert Java object to Json data readValue() ---> To convert Json data to Java object
<!-- Jackson dependency -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.19.1</version>
</dependency>
// Author class
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Author {
private Integer authorId;
private String authorName;
private String authorEmail;
}
// Binding class
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Book {
private Integer bookId;
private String bookName;
private Double bookPrice;
private Author author;
}
// Converter class
public class Converter {
public void javaToJson(Book book) throws Exception {
ObjectMapper mapper = new ObjectMapper();
mapper.writeValue(new File("book.json"), book);
System.out.println("Java object converted to Json .. !!");
}
public void jsonToJava() throws Exception {
ObjectMapper mapper = new ObjectMapper();
Book book = mapper.readValue(new File("book.json"), Book.class);
System.out.println("Json converted to Java object .. !!");
System.out.println(book);
}
}
Gson API is provided by Google
It is used to convert Java object to JSON data and vice versa
Gson API provided Gson
class to perform conversions
Gson
class contains below methods
toJson() ---> To convert Java object to Json data fromJson() ---> To convert Json data to Java object
Note: fromJson()
method will return Json data in String format.
<!-- Gson dependency -->
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.13.1</version>
</dependency>
// Category class
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Category {
private Integer categoryId;
private String categoryName;
}
// Binding class
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Product {
private Integer productId;
private String productName;
private Double productPrice;
private Category category;
}
// Converter class
public class Converter {
public void javaToJson(Product product) throws Exception {
Gson gson = new Gson();
String jsonData = gson.toJson(product);
FileWriter writer = new FileWriter("product.json");
writer.write(jsonData);
writer.close();
System.out.println("Java object converted to Json !!");
}
public void jsonToJava() throws Exception {
Gson gson = new Gson();
Product product = gson.fromJson(new FileReader("product.json"), Product.class);
System.out.println("Json converted to Java object !!");
System.out.println(product);
}
}
To develop RESTFul Services / REST APIs / RESTFul Webservices using Java Sun Microsystems
provided JAX-RS API
JAX-RS API
having two implementations
1) Jersey (Sun Microsystems) 2) REST Easy (JBOSS)
Spring framework also provided support to develop RESTFul Services using Spring Web MVC
module
Note: It is recommended to develop RESTFul Services using Spring Web MVC
module. Developing RESTFul Services using JAX-RS API
is legacy approach.
We will have two actors in RESTFul Services
1) Provider / Resource 2) Consumer / Client
The application which is providing services to other applications is called as Provider or Resource application
The application which is accessing services from other applications is called as Consumer or Client application
Client application and Resource application will exchange the data in interoperable format (XML / JSON)
Note: RESTFul Services are used for B2B communications hence presentation logic & view resolver are not required.
Create Spring starter application with below dependencies
a) web-starter b) devtools
Create Rest Controller with required methods
Run the application and test it
Note: To represent Java class as Rest Controller we will use @RestController
annotation.
@RestController = @Controller + @ResponseBody
Note: Every Rest Controller method should be bind to a HTTP Protocol method using one of the below annotations.
@GetMapping ---> HTTP GET Method ---> To retrieve the data @PostMapping ---> HTTP POST Method ---> To send the data @PutMapping ---> HTTP PUT Method ---> To update the data @DeleteMapping ---> HTTP DELETE Method ---> To delete the data
Note: To test the RESTFul Services we will use Postman
tool.
// Approach-1 (Using ResponseEntity)
@RestController
public class WelcomeRestController {
@GetMapping("/welcome")
public ResponseEntity<String> getWelcomeMsg() {
String respPayload = "Welcome to Ashok IT !!";
return new ResponseEntity<>(respPayload, HttpStatus.OK);
}
}
Note: ResponseEntity
uses generic type parameter to specify the type of the response body.
// Approach-2 (Returning payload directly)
@RestController
public class GreetRestController {
@GetMapping("/greet")
public String getGreetMsg() {
String respPayload = "Good Afternoon !!";
return respPayload;
}
}
ResponseEntity
objectNote: First approach is the recommended approach to develop the Rest Controllers because it offers more control over the HTTP response.
@GetMapping
annotation?
symbol&
symbol@RequestParam
annotationNote: If our controller method does not define any query params and we supply query params in the URL then the controller will simply ignore the query params.
// REST API using query param
@RestController
public class WelcomeRestController {
@GetMapping("/welcome")
public ResponseEntity<String> getWelcomeMsg(@RequestParam("name") String name) {
String respPayload = name + ", Welcome to Ashok IT !!";
return new ResponseEntity<>(respPayload, HttpStatus.OK);
}
}
URL: http://localhost:8080/welcome?name=Raju
// Working with multiple query params
@RestController
public class CourseRestController {
@GetMapping("/course")
public ResponseEntity<String> getCourseFee(@RequestParam("cname") String courseName,
@RequestParam("tname") String trainerName) {
String respPayload = courseName + " By " + trainerName + " Fee is 8000 INR";
return new ResponseEntity<>(respPayload, HttpStatus.OK);
}
}
URL: http://localhost:8080/course?cname=SBMS&tname=Ashok
/
symbol@PathVariable
annotationNote: If we don't specify Path Params explicitly in the URL pattern then the server will not be able to identify them.
// REST API using path params
@RestController
public class BookRestController {
@GetMapping("/book/{name}")
public ResponseEntity<String> getBookPriceByName(@PathVariable("name") String name) {
String respPayload = name + " Book Price is 4000 INR";
return new ResponseEntity<>(respPayload, HttpStatus.OK);
}
@GetMapping("/book/name/{bname}/author/{aname}")
public ResponseEntity<String> getBookPriceByNameAndAuthor(@PathVariable("bname") String name,
@PathVariable("aname") String author) {
String respPayload = name + " By " + author + " Price is 4000 INR";
return new ResponseEntity<>(respPayload, HttpStatus.OK);
}
@GetMapping("/book/{name}/{author}/{category}")
public ResponseEntity<String> getBookPriceByNameAndAuthorAndCategory(@PathVariable("name") String name,
@PathVariable("author") String author, @PathVariable("category") String category) {
String str1 = name + " By " + author + " Price is 4000 INR.";
String str2 = " It is a " + category + " Book.";
String respPayload = str1 + str2;
return new ResponseEntity<>(respPayload, HttpStatus.OK);
}
}
URL-1: http://localhost:8080/book/SpringBoot URL-2: http://localhost:8080/book/name/SpringBoot/author/RodJohnson URL-3: http://localhost:8080/book/SpringBoot/RodJohnson/Educational
@RestController
public class BookRestController {
@GetMapping("/book/{name}")
public ResponseEntity<String> getBookPrice(@PathVariable("name") String name) {
String respPayload = name + " Book Price is 4000 INR";
return new ResponseEntity<>(respPayload, HttpStatus.OK);
}
@GetMapping("/book/Python")
public ResponseEntity<String> getPythonBookPrice() {
String respPayload = "Python Book Price is 5000 INR";
return new ResponseEntity<>(respPayload, HttpStatus.OK);
}
}
Note: If the value of the path param is Python
then only the getPythonBookPrice()
controller will be executed. For all other values the getBookPrice()
controller will be executed.
Q) When to use Path Params & Query Params ?
Ans: If we want to retrieve more than one resource then we will use Query Params and if we want to retrieve specific / unique resource then we will use Path Params.
Produces is a media type (MIME type)
It represents in which formats the Rest Controller method can send the response payload
One Rest Controller method can support multiple response payload formats (like XML and JSON)
produces = { "application/json", "application/xml" }
Client should send a request with Accept
HTTP header
Accept
header represents in which format the client is expecting the response payload from the Rest Controller method
Based on Accept
header value Message Converter
will convert the response payload into client expected format
Note: If we write the code in the Rest Controller method to manually convert the response payload then the Rest Controller becomes tightly coupled with the response payload format. To avoid this Spring provided Message Converter
that automatically handle the conversions.
<!-- JAXB support -->
<dependency>
<groupId>jakarta.xml.bind</groupId>
<artifactId>jakarta.xml.bind-api</artifactId>
</dependency>
<dependency>
<groupId>org.glassfish.jaxb</groupId>
<artifactId>jaxb-runtime</artifactId>
</dependency>
// Binding class
@Data
@AllArgsConstructor
@NoArgsConstructor
@XmlRootElement
public class Product {
private Integer pid;
private String pname;
private Double price;
}
// ProductRestController
@RestController
public class ProductRestController {
@GetMapping(
value = "/product",
produces = { "application/json", "application/xml" }
)
public ResponseEntity<Product> getProduct() {
Product product = new Product(101, "Laptop", 45000.0);
return new ResponseEntity<>(product, HttpStatus.OK);
}
@GetMapping(value = "/products")
public ResponseEntity<List<Product>> getProducts() {
Product p1 = new Product(101, "Laptop", 45000.0);
Product p2 = new Product(102, "Mobile", 22000.0);
Product p3 = new Product(103, "Mouse", 240.0);
return new ResponseEntity<>(Arrays.asList(p1, p2, p3), HttpStatus.OK);
}
}
Note: The produces attribute can have multiple values and the first value is used as the default response format when the client doesn't provide an Accept
header.
@XmlRootElement
annotation. <!-- JAXB support -->
<dependency>
<groupId>jakarta.xml.bind</groupId>
<artifactId>jakarta.xml.bind-api</artifactId>
</dependency>
<dependency>
<groupId>org.glassfish.jaxb</groupId>
<artifactId>jaxb-runtime</artifactId>
</dependency>
// Product class
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Product {
private Integer pid;
private String pname;
private Double price;
}
// Binding class wrapping list of products
@Data
@AllArgsConstructor
@NoArgsConstructor
@XmlRootElement
public class ProductList {
List<Product> products;
}
// ProductRestController
@RestController
public class ProductRestController {
@GetMapping(
value = "/products",
produces = { "application/json", "application/xml" }
)
public ResponseEntity<ProductList> getProducts() {
Product p1 = new Product(101, "Laptop", 45000.0);
Product p2 = new Product(102, "Mobile", 22000.0);
Product p3 = new Product(103, "Mouse", 240.0);
ProductList list = new ProductList(Arrays.asList(p1, p2, p3));
return new ResponseEntity<>(list, HttpStatus.OK);
}
}
The default response format for Spring-based REST API's is JSON
But if the response payload is of String type then the default response format is plain text because Spring treats it as plain text not JSON
Spring uses HttpMessageConverters
to determine how to serialize the response payload based on the below factors
-> Type of response payload (String, Object etc) -> The Accept header in the request -> The available converters in the classpath
@RequestBody
annotation@PostMapping
annotationconsumes
attribute represents in which formats the Rest Controller method can accept the request payloadContent-Type
header represents in which format the client is sending the request payload to the Rest Controller <!-- JAXB support -->
<dependency>
<groupId>jakarta.xml.bind</groupId>
<artifactId>jakarta.xml.bind-api</artifactId>
</dependency>
<dependency>
<groupId>org.glassfish.jaxb</groupId>
<artifactId>jaxb-runtime</artifactId>
</dependency>
// Binding class
@Data
@XmlRootElement
public class Book {
private Integer id;
private String name;
private Double price;
}
// BookRestController
@RestController
public class BookRestController {
@PostMapping(
value = "/book",
consumes = { "application/json", "application/xml" }
)
public ResponseEntity<String> addBook(@RequestBody Book book) {
String resp = "Book Added Successfully !!";
System.out.println(book);
return new ResponseEntity<>(resp, HttpStatus.CREATED);
}
}
{
"id": 101,
"name": "Spring Boot",
"price": 450.0
}
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<book>
<id>101</id>
<name>Spring Boot</name>
<price>450</price>
</book>
produces
attribute represents in which formats the Rest Controller method can provide the response payload to the clientconsumes
attribute represents in which formats the Rest Controller method can accept the request payload from the clientNote: We can use both produces & consumes
attributes in a single Rest Controller method.
Accept
header represents in which format the client is expecting the response payload from the Rest Controller methodContent-Type
header represents in which format the client is sending the request payload to the Rest Controller methodNote: We can use both Accept & Content-Type
header in a single client.
Postman
// PassengerInfo class
@Data
public class PassengerInfo {
private String name;
private Long phno;
private String date;
private String from;
private String to;
private Integer trainNo;
}
// TicketInfo class
@Data
@AllArgsConstructor
@NoArgsConstructor
public class TicketInfo {
private Integer id;
private String pnr;
private String status;
}
// TicketRestController
@RestController
public class TicketRestController {
@PostMapping(
value = "/ticket",
produces = "application/json",
consumes = "application/json"
)
public ResponseEntity<TicketInfo> bookTicket(@RequestBody PassengerInfo passenger) {
System.out.println(passenger);
// logic to book ticket
TicketInfo ticket = new TicketInfo(1234, "JLSD45231", "CONFIRMED");
return new ResponseEntity<>(ticket, HttpStatus.CREATED);
}
}
{
"name": "Ashok",
"phno": 9876543210,
"date": "2025-08-15",
"from": "Delhi",
"to": "Pune",
"trainNo": 90123
}
@PutMapping
annotation // REST API using PUT request
@PutMapping("/ticket")
public ResponseEntity<String> updateTicket(@RequestBody PassengerInfo passenger) {
System.out.println(passenger);
// logic to update ticket
return new ResponseEntity<>("Ticket Updated !!", HttpStatus.OK);
}
@DeleteMapping
annotation // REST API using DELETE request
@DeleteMapping("/ticket/{ticketId}")
public ResponseEntity<String> deleteTicket(@PathVariable("ticketId") Integer tid) {
System.out.println("Ticket ID : " + tid);
// logic to delete ticket
return new ResponseEntity<>("Ticket Deleted !!", HttpStatus.OK);
}
Q) Can we write the logic to update a record in POST request method ?
Ans: Yes, we can perform any CRUD operation(s) inside any HTTP protocol method but doing so is strictly not recommended. We should follow HTTP Protocol standards while developing REST API.
Note: More than one Rest Controller method can have the same URL pattern (REST endpoint) but the HTTP method must be different so that the server can identify the correct controller method and route the request properly.
In realtime we will develop REST API using layered architecture
In layered architecture we will have below layers
1) Web Layer 2) Business / Service Layer 3) DAO Layer
// Binding class
@Data
@Entity
@Table(name = "BOOK_TABLE")
public class Book {
@Id
@Column(name = "BOOK_ID")
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer bookId;
@Column(name = "BOOK_NAME")
private String bookName;
@Column(name = "BOOK_PRICE")
private Double bookPrice;
}
// BookRepository interface
public interface BookRepository extends JpaRepository<Book, Serializable> {
}
// BookService interface
public interface BookService {
public String upsertBook(Book book);
public List<Book> getAllBooks();
public String deleteBook(Integer bookId);
}
// BookService class
@Service
public class BookServiceImpl implements BookService {
private BookRepository repository;
public BookServiceImpl(BookRepository repository) {
this.repository = repository;
}
@Override
public String upsertBook(Book book) {
Integer bookId = book.getBookId();
repository.save(book);
if(bookId == null) {
return "Book Inserted !!";
} else {
return "Book Updated !!";
}
}
@Override
public List<Book> getAllBooks() {
List<Book> books = repository.findAll();
return books;
}
@Override
public String deleteBook(Integer bookId) {
repository.deleteById(bookId);
return "Book Deleted !!";
}
}
// BookRestController class
@RestController
public class BookRestController {
private BookService service;
public BookRestController(BookService service) {
this.service = service;
}
@PostMapping("/book")
public ResponseEntity<String> addBook(@RequestBody Book book) {
String resp = service.upsertBook(book);
return new ResponseEntity<>(resp, HttpStatus.CREATED);
}
@GetMapping("/books")
public ResponseEntity<List<Book>> getBooks() {
List<Book> books = service.getAllBooks();
return new ResponseEntity<>(books, HttpStatus.OK);
}
@PutMapping("/book")
public ResponseEntity<String> updateBook(@RequestBody Book book) {
String resp = service.upsertBook(book);
return new ResponseEntity<>(resp, HttpStatus.OK);
}
@DeleteMapping("/book/{bookId}")
public ResponseEntity<String> deleteBook(@PathVariable("bookId") Integer bid) {
String resp = service.deleteBook(bid);
return new ResponseEntity<>(resp, HttpStatus.OK);
}
}
Note: Swagger will generate documentation in JSON format.
pom.xml
file <!-- Swagger dependency -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.5.0</version>
</dependency>
SwaggerConfig
class like below // SwaggerConfig class
@Configuration
public class SwaggerConfig {
@Bean
public GroupedOpenApi apiDoc() {
return GroupedOpenApi.builder()
.group("ashokit-api")
.packagesToScan("in.ashokit.rest")
.build();
}
}
Note: Configuration class is optional.
We can access Swagger UI using below URL
http://localhost:8080/swagger-ui/index.html
We can access Swagger documentation using below URL
http://localhost:8080/v3/api-docs/ashokit-api
Note: We can't store the data permanently in embedded database (when we stop the application then the data will be lost)
Note: Embedded databases are used for practice or testing purpose. We don't use embedded databases in realtime projects.
Note: Spring Boot provides H2 database as embedded database.
pom.xml
file <!-- h2 DB dependency -->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
</dependency>
# DataSource properties
spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.username=sa
spring.datasource.password=sa
spring.datasource.driver-class-name=org.h2.Driver
Run the application and access h2-console using below URL
http://localhost:8080/h2-console
As of now, all our Rest APIs are executed in localhost (we can access them only within our machine)
All our Rest API's are running in the server which is running in our local machine that's why we can access them only within our local machine (they are not available for public access)
To provide public access to our Rest API's we need to deploy them into a cloud platform
Cloud means getting resources over the web based on our demand
We have several cloud platforms in the market
1) AWS 2) Azure 3) GCP 4) VM Ware 5) Heroku
Deploy
sectionNote: We can deploy our app to Heroku cloud by using Heroku CLI or GitHub Repo
. Here we are using Heroku CLI
for deployment.
$ heroku login
$ cd <project-location>
$ git init
$ heroku git:remote -a <heroku-app-name>
$ git add .
$ git commit -am "make it better"
$ git push heroku master
Note: Heroku cloud will provide Platform as a Service (PaaS)
MobaXterm / PuTTY
Note: AWS cloud will provide Infrastructure as a Service (IaaS)
Note: In realtime we will deploy our apps to AWS cloud.
Using Spring Boot we can develop the Rest Client in two ways
1) RestTemplate (Outdated) 2) WebClient (Latest)
RestTemplate
is a predefined class which is a part of spring-boot-starter-web
dependency. It supports only synchronous communication.
WebClient
is a interface which is a part of spring-boot-starter-webflux
dependency. It supports both synchronous & asynchronous communication.
Note: WebClient
is introduced in Spring 5.x version.
To develop a Rest Client we need to know the below details about the Rest API
1) API URL 2) HTTP Method 3) Request Format 4) Response Format
Note: Rest API team will send swagger documentation to Rest Client side team. Using this swagger documentation client side team will understand the rest API and develop the Rest Client logic to access the Rest API.
Note: We will write Rest Client logic inside Service classes using @Service
annotation.
// Sending HTTP GET Request using RestTemplate
@Service
public class WelcomeRestClient {
public void invokeGetWelcomeMsg() {
String apiURL = "api-url";
RestTemplate rt = new RestTemplate();
ResponseEntity<String> responseEntity = rt.getForEntity(apiURL, String.class);
String body = responseEntity.getBody();
HttpStatusCode statusCode = responseEntity.getStatusCode();
System.out.println("Body : "+body);
System.out.println("Status Code : "+statusCode);
}
}
// Binding class
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Book {
private Integer bookId;
private String bookName;
private Double bookPrice;
}
Note: In our rest client binding class we can create only the fields we need and left the others.
// Sending HTTP POST Request using RestTemplate
@Service
public class BookClient {
public void invokeAddBook() {
String apiURL = "api-url";
Book book = new Book(101, "Spring Boot", 650.0);
RestTemplate rt = new RestTemplate();
ResponseEntity<String> responseEntity = rt.postForEntity(apiURL, book, String.class);
String body = responseEntity.getBody();
System.out.println(body);
}
public void invokeGetBooks() {
String apiURL = "api-url";
RestTemplate rt = new RestTemplate();
ResponseEntity<String> responseEntity = rt.getForEntity(apiURL, String.class);
String body = responseEntity.getBody();
System.out.println(body);
}
}
// Sending GET Request and binding Response JSON to Binding Obj
@Service
public class BookClient {
public void invokeGetBooksJson() {
String apiURL = "api-url";
RestTemplate rt = new RestTemplate();
ResponseEntity<Book[]> responseEntity = rt.getForEntity(apiURL, Book[].class);
Book[] books = responseEntity.getBody();
for(Book book : books) {
System.out.println(book);
}
}
}
Note: Binding the response data to binding class object is recommended approach.
RestTemplate
supports only Synchronous communicationsWebClient
supports both Sync & Async communications // Binding class
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Book {
private Integer bookId;
private String bookName;
private Double bookPrice;
}
// HTTP Post Request with WebClient
@Service
public class BookClient {
public void invokeAddBook() {
String apiURL = "api-url";
Book book = new Book(101, "Spring Boot", 650.0);
WebClient client = WebClient.create();
String body = client.post() // post request
.uri(apiURL) // endpoint url
.bodyValue(book) // setting request body .retrieve() // retrieving response data
.bodyToMono(String.class) // binding response data
.block(); // make it sync client
System.out.println(body);
}
}
// HTTP GET Request using WebClient
@Service
public class BookClient {
public void invokeGetBooks() {
String apiURL = "api-url";
WebClient client = WebClient.create();
Book[] books = client.get() // get request
.uri(apiURL) // endpoint url
.retrieve() // retrieving response data
.bodyToMono(Book[].class) // binding response data
.block(); // make it sync client
for(Book book : books) {
System.out.println(book);
}
}
}
// Asynchronus call using Webclient
@Service
public class BookClient {
public static void invokeGetBooksAsync() {
String apiURL = "api-url";
WebClient client = WebClient.create();
client.get()
.uri(apiURL)
.retrieve()
.bodyToMono(Book[].class)
.subscribe(BookClient::respHandler); // make it async client
System.out.println("Request Sent !!");
}
public static void respHandler(Book[] books) {
for(Book book : books) {
System.out.println(book);
}
}
}
Note: For every async call we need to create one response handler.
Note: A REST API can act as both a provider and a consumer at the same time for different REST APIs.
application.properties
file will be createdapplication.properties
fileNote: We need to create separate properties file for every profile.
# application.properties file
server.port = 9090
spring.application.name = my-app
spring.mvc.view.prefix = /views/
spring.mvc.view.suffix = .jsp
YAML Ain't Markup Language
(originally stood for Yet Another Markup Language
)application.yml
fileNote: We can configure all the profiles in single YML file.
Note: Indent spaces are crucial in YML files.
# application.yml file
server:
port: 9090
spring:
application:
name: my-app
mvc:
view:
prefix: /views/
suffix: .jsp
Note: In realtime we will use application.yml
file because it is more simple and readable.
application.properties or application.yml
fileapplication.properties or application.yml
file then we no need to compile and build the entire project just server re-start is requiredPredefined Properties : Spring Boot will read these properties automatically to configure our app.
User-defined Properties : We will define these properties and use them in our application.
Spring Boot provided below two approaches to read user-defined properties
1) @Value annotation 2) @ConfigurationProperties annotation
@Value : This approach is suitable when we have less no of properties to configure.
@ConfigurationProperties : This approach is used when we have a large no of properties to configure.
# application.properties file
# predefined properties
server.port = 9090
# user-defined properties
messages.welcome = Welcome to Ashok IT !!
messages.greet = Good Evening !!
# application.yml file
# predefined properties
server:
port: 9090
# user-defined properties
messages:
welcome: Welcome to Ashok IT !!
greet: Good Evening !!
// MessageRestController class
@RestController
public class MessageRestController {
@Value("${messages.welcome}")
private String welcomeMsg;
@Value("${messages.greet}")
private String greetMsg;
@GetMapping("/welcome")
public String getWelcomeMsg() {
return welcomeMsg;
}
@GetMapping("/greet")
public String getGreetMsg() {
return greetMsg;
}
}
Note: A project can have both application.properties file & application.yml
file but it is not recommended because it can cause confusions.
# application.properties file
# predefined properties
server.port = 9090
# user-defined properties
my-app.messages.welcomeMsg = Welcome to Ashok IT !!
my-app.messages.greetMsg = Good Evening !!
my-app.messages.wishMsg = All the Best !!
# application.yml file
# predefined properties
server:
port: 9090
# user-defined properties
my-app:
messages:
welcomeMsg: Welcome to Ashok IT !!
greetMsg: Good Evening !!
wishMsg: All the Best !!
// AppProperties class
@Data
@Configuration
@EnableConfigurationProperties
@ConfigurationProperties(prefix = "my-app")
public class AppProperties {
private Map<String, String> messages = new HashMap<>();
}
// MessageRestController class
@RestController
public class MessageRestController {
private AppProperties props;
public MessageRestController(AppProperties props) {
this.props = props;
}
@GetMapping("/welcome")
public String getWelcomeMsg() {
Map<String, String> messages = props.getMessages();
return messages.get("welcomeMsg");
}
@GetMapping("/greet")
public String getGreetMsg() {
Map<String, String> messages = props.getMessages();
return messages.get("greetMsg");
}
@GetMapping("/wish")
public String getWishMsg() {
return props.getMessages().get("wishMsg");
}
}
/health : To get application health status
/info : To get application information
/beans : To get which beans loaded by our application
/mappings : To get which URL patterns available in our application
/configProps : To get which configuration properties loaded by our application
/heapdump : To download heap data
/threaddump : To get threads information
/shutdown : To stop our application (it is bind to POST request)
Note: All the actuator endpoints are bind to GET request except /shutdown
spring-boot-starter-actuator
dependency <!-- Spring Boot actuator dependency -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
We can check actuator exposed endpoints using below URL
URL : http://localhost:8080/actuator
Note: /health
is a default endpoint which we can access directly.
# application.yml file
management:
endpoints:
web:
exposure:
include:
- beans
- mappings
Note: If we expose actuator endpoints then /health
endpoint will not be available by default. It also need to be exposed manually.
# application.yml file
management:
endpoints:
web:
exposure:
include: '*'
# application.yml file
management:
endpoints:
web:
exposure:
include: '*'
exclude:
- beans
- mappings
# application.yml file
management:
endpoints:
web:
exposure:
include: '*'
endpoint:
shutdown:
enabled: true
Applications can be developed in two ways
1) Monolithic Architecture 2) Microservices Architecture
1) Development is easy 2) Deployment is easy 3) Performance is good 4) Easy Testing 5) Easy Debugging 6) KT is easy (Knowledge Transfer)
1) Single point of failure 2) Whole application re-deployment 3) Scalability (increasing & decreasing resources based on demand) 4) Reliability (strong) 5) Availability (zero downtime)
Note: It is not recommended to develop applications using monolithic architecture because it is difficult to maintain monolithic architecture based applications.
1) Loosely Coupled 2) Fast development 3) Quick Releases 4) Flexibility 5) Scalability 6) No single point of failure 7) Technology Independence
1) Bounded context (identifying no of services to develop) 2) Lots of Configurations 3) Visibility (you may not get the chance to work with all the services) 4) Testing is difficult 5) Debugging is difficult
Microservices is an architectural design pattern to develop our applications
There is no fixed architecture for Microservices based applications
People are customizing Microservices Architecture according to their requirement
Let us see generalized architecture of Microservices
1) Service Registry 2) Admin Server 3) Zipkin Server 4) Services (REST APIs) 5) FeignClient 6) API Gateway
Note: Eureka Server is provided by Spring Cloud Netflix Library
.
Note: Admin Server and Admin Client provided by Code Centric
company (we can integrate it with Spring Boot)
Note: Zipkin is third party open source server (it can be integrated with Spring Boot)
Service Registry + Admin Server + Zipkin Server
Note: Based on requirement our backend APIs can communicate with 3rd party APIs also by using RestTemplate or WebClient
.
Spring Cloud Gateway
we can use as API Gateway for our applicationNote: Earlier we use Zuul Proxy
as API Gateway but it got removed from latest version of Spring Boot.
Create Spring Boot app with below dependencies
a) web-starter b) devtools c) eureka-server
Configure @EnableEurekaServer
annotation at boot start class
Configure below properties in application.yml
file
# Configures Eureka Server port
server:
port: 8761
# To prevent Eureka Server self-registration
eureka:
client:
register-with-eureka: false
Run the application and access it in the browser
URL : http://localhost:8761/
Note: The default port for Eureka Server is 8761 but we can run it on any other port as well. If we use a custom port for Eureka Server then our services will not register with Eureka Server automatically. In this case we need to configure Eureka Server URL manually in each service.
Create Spring Boot application with below dependencies
a) web-starter b) devtools c) admin-server (codecentric)
Configure @EnableAdminServer
annotation at boot start class
Configure Admin Server port (we can use any port)
Run the application and access it in the browser
URL : http://localhost:port/
Download Zipkin Server jar from below URL
URL : https://zipkin.io/pages/quickstart.html
Run Zipkin Server jar using below command
java -jar <zipkin-server-jar>
Note: The default port for Zipkin Server is 9411 but we can run it on any other port as well. If we use a custom port for Zipkin Server then our services will not register with Zipkin Server automatically to send traces. In this case we need to configure Zipkin Server URL manually in each service.
Create Spring Boot application with below dependencies
a) web-starter b) devtools c) eureka-client d) admin-client e) zipkin-client f) actuator
Configure @EnableDiscoveryClient
annotation at boot start class
Create Rest Controller with required methods
Configure below properties in application.yml
a) server port b) application name c) admin server url d) eureka server url (optional) e) actuator endpoints
# Configures server port
server:
port: 8081
# Configures app name & Admin Server URL
spring:
application:
name: WELCOME-API
boot:
admin:
client:
url: http://localhost:1111/
# Configures Eureka Server URL (optional)
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
# Configures actuator endpoints
management:
endpoints:
web:
exposure:
include: '*'
Note: @EnableDiscoveryClient
annotation is used to enable service registration and discovery in a Spring Boot application.
Note: If Eureka Server is running on the default port and at the default URL then we no need to configure Eureka Server URL in the application.yml
file of client applications.
Create Spring Boot application with below dependencies
a) web-starter b) devtools c) eureka-client d) admin-client e) zipkin-client f) actuator g) open-feign
Configure @EnableDiscoveryClient
& @EnableFeignClients
annotations at boot start class
Create FeignClient to access WELCOME-API
@FeignClient(name = "WELCOME-API")
public interface WelcomeApiClient {
@GetMapping("/welcome")
public String invokeWelcomeApi();
}
Create Rest Controller with required methods
Configure below properties in application.yml
a) server port b) application name c) admin server url d) actuator endpoints
# Configures server port
server:
port: 9091
# Configures app name & Admin Server URL
spring:
application:
name: GREET-API
boot:
admin:
client:
url: http://localhost:1111/
# Configures actuator endpoints
management:
endpoints:
web:
exposure:
include: '*'
Note: @EnableFeignClients
annotation is used enable FeignClient support which allows us to call other services using simple Java interfaces.
Note: We don't need to provide implementation for FeignClient interface Spring Cloud will provide implementation for FeignClient interface during runtime.
Note: For inter-service communication we only specify the service name in the @FeignClient
annotation. FeignClient will automatically contact the service registry (Eureka Server) to get the actual service URL.
Note: FeignClient can also be used to call external services (not registered in the service registry). In this case we must provide the full URL of the external service in the @FeignClient
annotation.
@FeignClient(name = "api-name", url = "http://api.example.com")
Note: We can use above approach for inter-service communication also but it is not recommended because service URL can be change in future.
Create Spring Boot application with below dependencies
a) devtools b) eureka-client c) reactive-gateway
Configure @EnableDiscoveryClient
annotation at boot start class
Configure port no & API Routings in application.yml
file (we can use any port)
# Configures API Gateway port
server:
port: 3333
# Configures app name
spring:
application:
name: API-GATEWAY
# Configures API Routes
cloud:
gateway:
server:
webflux:
routes:
- id: api-1
uri: lb://WELCOME-API
predicates:
- Path= /welcome
- id: api-2
uri: lb://GREET-API
predicates:
- Path= /greet
Note: API Gateway will act as a client for Eureka Server to get the API service URLs from Eureka Server for routing requests.
// Accessing request information using Filter
@Component
public class MyPreFilter implements GlobalFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
System.out.println("Filter executed..!!");
// Accessing request information
ServerHttpRequest request = exchange.getRequest();
HttpHeaders headers = request.getHeaders();
Set<String> keySet = headers.keySet();
keySet.forEach(key -> {
List<String> values = headers.get(key);
System.out.println(key + " :: " + values);
});
return chain.filter(exchange);
}
}
Note: Since this filter implements the GlobalFilter
interface hence it will be executed for every request that comes through the API Gateway.
Note: chain.filter()
method passes the request to the next filter. If there is no next filter then the request goes to the routing.
Note: When we use Postman to send the request, it will add Postman-Token
in request headers. We can use this token to identify whether the request came from Postman or from any other app.
// Validating requests using Filter
@Component
public class MyPreFilter implements GlobalFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
System.out.println("Filter executed..!!");
// Accessing request information
ServerHttpRequest request = exchange.getRequest();
HttpHeaders headers = request.getHeaders();
List<String> values = headers.get("security_token");
// Validating request
if (values.contains("AshokIT@123")) {
System.out.println("Token: " + values);
return chain.filter(exchange);
} else {
System.out.println("Invalid Request..!!");
// Set 401 Unauthorized response
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
}
}
}
Note: Spring Cloud will internally use Spring Cloud LoadBalancer
to perform load balancing. Earlier Netflix Ribbon
library is used for load balancing.
Note: If we use service URL instead of service name in API-Gateway or FeignClient then we can't achieve load balancing because in that case all the incoming requests goes to the same fixed instance.
Note: Spring Cloud LoadBalancer
is used to perform client-side load balancing.
WelcomeRestController
@RestController
public class WelcomeRestController {
@Autowired
private Environment env;
@GetMapping("/welcome")
public String getWelcomeMsg() {
String port = env.getProperty("server.port");
String msg = "Welcome to Ashok IT..!! (Port :: " + port + ")";
return msg;
}
}
Run multiple instances of Welcome-API using below steps
- Right Click on API - Run As -> Run Configurations - Select Application - Arguments - VM Arguments (-Dserver.port=8082) - Apply & Run it
Check Eureka Dashboard
Note: VM Arguments will take priority over application.yml
configuration.
Scaling means increasing the capacity of the service which is getting more traffic
Scaling can be done in two ways
1) Vertical Scaling 2) Horizontal Scaling
Vertical Scaling: Increasing the system resources of a single server (Ex: RAM, CPU etc...)
Horizontal Scaling: Increasing the no of servers to run our application
Auto Scaling means increasing or decreasing the no of servers based on traffic
Scale Up: Increasing the no of servers when traffic increases Scale Down: Reducing the no of servers when traffic is low
Note: Vertical Scaling can be done upto a certain level hence we use Horizontal Scaling in realtime.
Note: To implement Auto Scaling we have to use cloud platforms like AWS, Azure and GCP.
Note: If m1() method fails to fetch the data from Redis server then we have to execute m2() method to fetch the data from DB server. This is called as fallback logic execution.
@RestController
public class MyRestController {
@GetMapping("/data")
public String m1() {
System.out.println("********** m1() executed **********");
String msg = "Data accessed from Redis !!";
try {
int i = 10 / 0;
} catch (Exception e) {
msg = m2();
}
return msg;
}
public String m2() {
System.out.println("********** m2() executed **********");
String msg = "Data accessed from DB !!";
return msg;
}
}
Circuit Breaker works based on three states
1) CLOSED 2) OPEN 3) HALF_OPEN
CLOSED: System working as expected & failure rate is being monitored.
OPEN: When the failure rate exceeds the threshold then Circuit Breaker goes to the OPEN
state and directly executes the fallback logic for a specific amount of time.
Note: After this specific amount of time Circuit Breaker will goes to the HALF_OPEN
state.
HALF_OPEN: Circuit Breaker allows a few requests to check if the system has recovered. If these requests succeed, it moves back to the CLOSED
state and if they fail again, it returns to the OPEN
state.
We can implement Circuit Breaker using Spring Boot in two ways
1) Hystrix (outdated) 2) Resilience4j (trending)
Create Spring Boot application with below dependencies
a) web-starter b) devtools c) actuator d) resilience4j e) aop
<!-- aop dependency -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
Note: We need the AOP dependency because the circuit breaker works by intercepting method calls. AOP lets Spring create a proxy around our methods, so it can decide whether to run the method normally or call the fallback logic. Without AOP this interception and circuit breaker logic will not work.
@RestController
public class MyRestController {
@CircuitBreaker(name = "myCircuitBreaker", fallbackMethod = "getDataFromDB")
@GetMapping("/data")
public String getDataFromRedis() {
System.out.println("*********** getDataFromRedis() executed ***********");
String msg = "Data accessed from Redis !!";
if(new Random().nextInt(10) <= 10) {
throw new RuntimeException("Redis Server Down");
}
return msg;
}
public String getDataFromDB(Throwable t) {
System.out.println("*********** getDataFromDB() executed ***********");
String msg = "Data accessed from DB !!";
return msg;
}
}
application.yml
file # actuators config
management:
endpoints.web.exposure.include: '*'
endpoint.health.show-details: always
health.circuitbreakers.enabled: true
# circuit breaker config
resilience4j:
circuitbreaker:
configs:
default:
registerHealthIndicator: true
slidingWindowSize: 10
minimumNumberOfCalls: 5
permittedNumberOfCallsInHalfOpenState: 3
automaticTransitionFromOpenToHalfOpenEnabled: true
waitDurationInOpenState: 30s
failureRateThreshold: 50
eventConsumerBufferSize: 10
Run the application and test it
URL: http://localhost:8080/actuator/health URL: http://localhost:8080/actuator/circuitbreakerevents
Note: To reduce the no of round trips between application and database we will use Cache.
Every application will contain two types of tables
1) Transactional tables 2) Non-Transactional tables
If our application is performing DML operations on the table then those tables are called as Tx tables
Ex: user_table, product_table, book_table
If our application is performing only DQL / DRL (Select)
operations on the table then those tables are called as Non-Tx tables. These are also called as static tables
Ex: country_table, state_table, plan_table
Note: It is not recommended to read static data from DB table everytime. We will use Cache to store the static data.
We will use RDBMS for below scenarios
1) Structured data 2) Tables, rows, columns 3) Complex queries 4) Joins 5) Complex Transactions 6) Relationships are critical
We will use Redis for below scenarios
1) Fast Read and Write operations 2) Key-Value format 3) Data Structures like Strings, hashes, lists, sets 4) Relationships are not critical 5) Transactions are not crictical 6) Performance is more important 7) Real-Time Data Analysis
Create Spring Boot app with below dependencies
a) web-starter b) devtools c) lombok d) starter-data-redis e) jedis
<!-- jedis dependency -->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
application.properties
file # Redis Config
spring.data.redis.url=redis-11429.crce179.ap-south-1-1.ec2.redns.redis-cloud.com
spring.data.redis.port=11429
spring.data.redis.username=default
spring.data.redis.password=AshokIT@123
RedisConfig
class to connect with Redis Server // RedisConfig class
@Configuration
public class RedisConfig {
@Value("${spring.data.redis.url}")
private String url;
@Value("${spring.data.redis.port}")
private Integer port;
@Value("${spring.data.redis.username}")
private String username;
@Value("${spring.data.redis.password}")
private String password;
@Bean
public JedisConnectionFactory getJedisConnectionFactory() {
RedisStandaloneConfiguration serverConfig = new RedisStandaloneConfiguration(url, port);
serverConfig.setUsername(username);
serverConfig.setPassword(password);
JedisClientConfiguration clientConfig = JedisClientConfiguration.builder().build();
return new JedisConnectionFactory(serverConfig, clientConfig);
}
}
@RedisHash
annotation // Entity class
@Data
@RedisHash("USER_CACHE")
public class User {
@Id
private Integer userId;
private String userName;
private Integer userAge;
}
Create Spring Boot app with below dependencies
a) web-starter b) devtools c) lombok d) data-jpa e) mysql-driver f) starter-data-redis g) jedis
<!-- jedis dependency -->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
application.properties
file # Redis Config
spring.data.redis.url=redis-11429.crce179.ap-south-1-1.ec2.redns.redis-cloud.com
spring.data.redis.port=11429
spring.data.redis.username=default
spring.data.redis.password=AshokIT@123
# MySQL properties
spring.datasource.url=jdbc:mysql://localhost:3306/sbms
spring.datasource.username=ashokit
spring.datasource.password=AshokIT@123
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
# Hibernate properties
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true
@EnableCaching
annotation in start class @EnableCaching
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
RedisConfig
class like below // RedisConfig class
@Configuration
public class RedisConfig {
@Value("${spring.data.redis.url}")
private String url;
@Value("${spring.data.redis.port}")
private Integer port;
@Value("${spring.data.redis.username}")
private String username;
@Value("${spring.data.redis.password}")
private String password;
@Bean
public JedisConnectionFactory getJedisConnectionFactory() {
RedisStandaloneConfiguration serverConfig = new RedisStandaloneConfiguration(url, port);
serverConfig.setUsername(username);
serverConfig.setPassword(password);
JedisClientConfiguration clientConfig = JedisClientConfiguration.builder().build();
return new JedisConnectionFactory(serverConfig, clientConfig);
}
@Bean
public RedisCacheConfiguration getRedisCacheConfiguration() {
return RedisCacheConfiguration.defaultCacheConfig()
.disableCachingNullValues()
.entryTtl(Duration.ofMinutes(4))
.serializeValuesWith(
SerializationPair
.fromSerializer(new Jackson2JsonRedisSerializer<>(User.class))
);
}
}
// User Service class
@Service
public class UserServiceImpl implements UserService {
private UserRepository repo;
public UserServiceImpl(UserRepository repo) {
this.repo = repo;
}
@Override
@CachePut(value = "USER_CACHE", key = "#user.userId")
public User upsertUserData(User user) {
repo.save(user);
return user;
}
@Override
@Cacheable(value = "USER_CACHE", key = "#userId")
public User getUserData(Integer userId) {
return repo.findById(userId).get();
}
@Override
@CacheEvict(value = "USER_CACHE", key = "#userId")
public void deleteUserData(Integer userId) {
repo.deleteById(userId);
}
}
Note: We use Spring Expression Language (SpEL)
to define the cache key.
Note: API calls are used for accessing another application logic. For sharing large amounts of data to multiple systems we use Message Queues.
Apache Kafka is an open source distributed streaming platform
We will use Apache Kafka as a message broker for our applications
Apache Kafka is used to process real time data with high throughput and low latency
Ex: flights data, stocks data, social media data etc
Kafka works based on Publisher and Subscriber model
The application which is publishing message to kafka is called Publisher
The application which is subscribing message from kafka is called Subscriber
Note: Kafka will act as a mediator / broker between Publisher and Subscriber.
Note: Using Apache Kafka we can develop Event Driven Microservices.
1) Zomato orders processing 2) Swiggy orders processing 3) Ola rides booking process 4) Rapido rides booking process
1) Zookeeper (Provides runtime environment to run kafka server) 2) Kafka Server (Message Broker) 3) Kafka Topic (A place to store msgs) 4) Publisher App (The application which sends msg to topic) 5) Subscriber App (The application which reads msg from topic)
Download Zookeeper and set path to Zookeeper in Environment variables upto bin folder (optional)
Download Apache Kafka from Apache
Copy zookeeper.properties
and server.properties
files from kafka/config
folder to kafka/bin/windows
folder
Set temp directory path in zookeeper.properties
and server.properties
files
Start Zookeeper server using below command from kafka/bin/windows
folder
zookeeper-server-start.bat zookeeper.properties
Start Kafka Server using below command from kafka/bin/windows
folder
kafka-server-start.bat server.properties
Note: If kafka server is getting stopped, delete kafka logs from temp folder.
Create Kakfa topic using below command from kafka/bin/windows
folder (optional)
kafka-topics.bat --create --bootstrap-server localhost:9092 --replication-factor 1 --partitions 1 --topic user_topic
Note: When a publisher sends a message to a topic, the topic will be created automatically if it does not already exist.
View created topics using below command
kafka-topics.bat --list --bootstrap-server localhost:9092
Create Spring Boot application with below dependencies
a) web-starter b) devtools c) lombok d) spring-kafka e) kafka-streams
Configure server port in application.properties
file
server.port=7070
KafkaConstants
class like below public class KafkaConstants {
public static final String TOPIC = "user";
public static final String HOST = "localhost:9092";
}
@Data
public class User {
private Integer userId;
private String userName;
private Integer userAge;
}
KafkaProducerConfig
class like below @Configuration
public class KafkaProducerConfig {
@Bean
public ProducerFactory<String, User> getProducerFactory() {
Map<String, Object> configProps = new HashMap<>();
configProps.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, KafkaConstants.HOST);
configProps.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
configProps.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, JsonSerializer.class);
return new DefaultKafkaProducerFactory<>(configProps);
}
@Bean
public KafkaTemplate<String, User> getKafkaTemplate() {
return new KafkaTemplate<>(getProducerFactory());
}
}
@Service
public class UserServiceImpl implements UserService {
private KafkaTemplate<String, User> kafkaTemplate;
public UserServiceImpl(KafkaTemplate<String, User> kafkaTemplate) {
this.kafkaTemplate = kafkaTemplate;
}
@Override
public String publishUserMsg(User user) {
kafkaTemplate.send(KafkaConstants.TOPIC, user);
System.out.println("****** Message Published to Kafka Topic ******");
return "User Record Added to Kafka Queue Successfully !!";
}
}
RestController
with required methods @RestController
public class UserRestController {
private UserService service;
public UserRestController(UserService service) {
this.service = service;
}
@PostMapping("/user")
public String saveUser(@RequestBody User user) {
return service.publishUserMsg(user);
}
}
Create Spring Boot application with below dependencies
a) web-starter b) devtools c) lombok d) spring-kafka e) kafka-streams
Configure server port in application.properties
file
server.port=9090
KafkaConstants
class like below public class KafkaConstants {
public static final String TOPIC = "user";
public static final String GROUP_ID = "group_user";
public static final String HOST = "localhost:9092";
}
@Data
public class User {
private Integer userId;
private String userName;
private Integer userAge;
}
KafkaConsumerConfig
class like below @Configuration
public class KafkaConsumerConfig {
@Bean
public ConsumerFactory<String, User> getConsumerFactory() {
Map<String, Object> configProps = new HashMap<>();
configProps.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, KafkaConstants.HOST);
configProps.put(ConsumerConfig.GROUP_ID_CONFIG, KafkaConstants.GROUP_ID);
configProps.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
configProps.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, JsonDeserializer.class);
return new DefaultKafkaConsumerFactory<>(configProps, new StringDeserializer(),
new JsonDeserializer<>(User.class));
}
@Bean
public ConcurrentKafkaListenerContainerFactory<String, User> userContainerFactory() {
ConcurrentKafkaListenerContainerFactory<String, User> factory = new ConcurrentKafkaListenerContainerFactory<>();
factory.setConsumerFactory(getConsumerFactory());
return factory;
}
}
@KafkaListener
annotation @Service
public class UserServiceImpl implements UserService {
@Override
@KafkaListener(
topics = KafkaConstants.TOPIC,
groupId = KafkaConstants.GROUP_ID,
containerFactory = "userContainerFactory"
)
public void subscribeUserMsg(User user) {
System.out.println("Message Received from Kafka Topic : "+user);
}
}
Note: An application can act as Publisher for one topic and at the same time it can act as Subscriber for another topic. Below app has two API's and each API is doing both it publishes messages to one topic and subscribes messages from another topic.
GitHub URL: https://github.com/arpitaggarwal135/kafka-mini-project
application.properties / application.yml
fileapplication.properties / application.yml
file will also be packaged along with our application (it will be part of our app jar file)Spring Cloud Library
Note: Configuration properties file name and application-name (spring.application.name)
must be same so that the Config Server can identify the application and provide correct configuration.
app-name : welcome ---> config-file : welcome.yml app-name : greet ---> config-file : greet.yml app-name : admin ---> config-file : admin.yml app-name : cart ---> config-file : cart.yml
Note: Config Server also supports profiles. If our application uses a profiles, it can detect the profile-specific configuration and provide it to that application.
welcome.yml welcome-dev.yml welcome-prod.yml greet.yml greet-dev.yml greet-prod.yml
Note: Our microservices will act as client for the config-server.
Create a Git repository and keep all the required configuration properties files in it
Create Spring Boot application with below dependencies
a) config-server b) devtools
Enable Config Server using @EnableConfigServer
annotation in start class
@EnableConfigServer
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
application.yml
file # Config Server properties
spring:
cloud:
config:
server:
git:
uri: https://github.com/arpitaggarwal135/config_props
clone-on-start: true
Note: clone-on-start
property forces the config server to clone the Git repo immediately at startup, instead of lazily when the first request comes.
Create Spring Boot application with below dependencies
a) web-starter b) devtools c) config-client d) actuators
Configure below properties in application.yml
file
# Set server port
server:
port: 9090
# Set application name
spring:
application:
name: welcome
# Set config server url
config:
import: optional:configserver:http://localhost:8080
# Enables refresh actuator endpoint
management:
endpoints:
web:
exposure:
include: refresh
@RefreshScope
@RestController
public class WelcomeRestController {
@Value("${msg:Config Server Not Working !!}")
private String msg;
@GetMapping("/")
public String getWelcomeMsg() {
return msg;
}
}
Note: If we make any changes in the configuration properties then we need to restart our application to pick-up the latest configuration.
@RefreshScope
annotation at all the spring-managed beans that uses configuration properties@RefreshScope
annotation tells Spring to recreate the bean with the updated configuration propertiesrefresh
actuator endpoint in application.yml
refresh
actuator endpoint using PostmanSpring Boot loads configuration properties in below order (highest priority first)
Command-line arguments External config files (outside the jar) Packaged config files (inside src/main/resources in the jar)
Note: If a property is defined in more than one place then the one with higher priority overrides the others.
Create Spring Boot application with below dependencies
a) web-starter b) devtools c) lombok d) webflux
Create binding class to represent the data
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
private Integer userId;
private String userName;
private Integer userAge;
}
@RestController
public class UserRestController {
@GetMapping(value = "/user", produces = "application/json")
public ResponseEntity<Mono<User>> getUser() {
User user = new User(101, "Ashok", 38);
Mono<User> userMono = Mono.just(user);
return new ResponseEntity<>(userMono, HttpStatus.OK);
}
@GetMapping(value = "/users", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public ResponseEntity<Flux<User>> getUsers() {
// creating binding object with data
User user = new User(101, "Ashok", 38);
// creating stream for binding object
Stream<User> stream = Stream.generate(() -> user);
// creating flux object using stream
Flux<User> userFlux = Flux.fromStream(stream);
// setting response interval
Flux<Long> interval = Flux.interval(Duration.ofSeconds(3));
// combining user flux and interval flux
Flux<Tuple2<Long,User>> zip = Flux.zip(interval, userFlux);
// creating map for response
Flux<User> map = zip.map(Tuple2::getT2);
return new ResponseEntity<>(map, HttpStatus.OK);
}
}
Exception is an unexpected and unwanted situation which disturbs normal flow of our application execution
Exception will cause abnormal termination of our program
To achieve graceful termination we need to handle exceptions in our application
The process of handling exception is called as Exception Handling
In Java we have below keywords to handle the exceptions
1) try 2) catch 3) throw 4) throws 5) finally
Note: In REST APIs, it is not recommended to handle exceptions directly using above keywords.
{
code : "SBI0004",
msg : "Exception Reason"
}
Note: We will assign a unique exception code and a message that describes the reason to each Exception in the application.
We have below two approaches to handle Exceptions in REST API's
1) Local Exception Handling (Controller Specific) 2) Global Exception Handling (Application Specific) (Recommended)
@ExceptionHandler
annotation // ExceptionInfo class
@Data
public class ExceptionInfo {
private String code;
private String msg;
}
// Controller specific exception handling
@RestController
public class WelcomeRestController {
private Logger logger = LoggerFactory.getLogger(WelcomeRestController.class);
@GetMapping("/welcome")
public String getWelcomeMsg() {
String msg = "Welcome to Ashok IT !!";
try {
int i = 10 / 0;
} catch (Exception e) {
logger.error("Exception Occurred : " + e.getMessage());
throw new ArithmeticException(e.getMessage());
}
return msg;
}
@ExceptionHandler(value = ArithmeticException.class)
public ResponseEntity<ExceptionInfo> handleArithmeticException(ArithmeticException ae) {
ExceptionInfo exception = new ExceptionInfo();
exception.setCode("AE0001024");
exception.setMsg(ae.getMessage());
return new ResponseEntity<>(exception, HttpStatus.INTERNAL_SERVER_ERROR);
}
}
Note: We use loggers to save exception details in a log file so that programmers can review them later.
Global Exception Handling is applicable for all the classes available in the application
To represent our class as Global Exception Handler we will use @RestControllerAdvice
annotation
Global Exception Handler contains methods to handle exceptions across the entire application
@ExceptionHandler ---> To map our method to particular exception @RestControllerAdvice ---> To represent class as global exception handler
// ExceptionInfo class
@Data
public class ExceptionInfo {
private String code;
private String msg;
}
// WelcomeRestController class
@RestController
public class WelcomeRestController {
@GetMapping("/welcome")
public String getWelcomeMsg() {
String msg = "Welcome to Ashok IT !!";
int i = 10 / 0;
return msg;
}
}
// GreetRestController class
@RestController
public class GreetRestController {
@GetMapping("/greet")
public String getGreetMsg() {
String msg = "Good Morning !!";
String str = null;
str.length();
return msg;
}
}
// Global Exception Handler class
@RestControllerAdvice
public class AppExceptionHandler {
private Logger logger = LoggerFactory.getLogger(AppExceptionHandler.class);
@ExceptionHandler(value = ArithmeticException.class)
public ResponseEntity<ExceptionInfo> handleArithmeticException(ArithmeticException ae) {
ExceptionInfo exception = new ExceptionInfo();
exception.setCode("AE0001024");
exception.setMsg(ae.getMessage());
logger.error("Exception Occurred : " + ae.getMessage());
return new ResponseEntity<>(exception, HttpStatus.INTERNAL_SERVER_ERROR);
}
@ExceptionHandler(value = NullPointerException.class)
public ResponseEntity<ExceptionInfo> handleNullPointerException(NullPointerException npe) {
ExceptionInfo exception = new ExceptionInfo();
exception.setCode("NPE0001028");
exception.setMsg(npe.getMessage());
logger.error("Exception Occurred : " + npe, npe);
return new ResponseEntity<>(exception, HttpStatus.INTERNAL_SERVER_ERROR);
}
}
Note: If Local Exception Handler and Global Exception Handler both exist for the same exception then Local Handler will take priority and execute.
Exception
class or RuntimeException
class public class MyException extends RuntimeException {
public MyException(String msg) {
super(msg);
}
}
@Data
public class ExceptionInfo {
private String code;
private String msg;
}
public class BookNotFoundException extends RuntimeException {
public BookNotFoundException(String msg) {
super(msg);
}
}
RestControIler
and throw the exception based on condition @RestController
public class BookRestController {
@GetMapping("/book/{isbn}")
public String getBookName(@PathVariable Integer isbn) {
if(isbn == 1001) {
return "Spring Boot";
} else if(isbn == 1002) {
return "AWS";
} else {
throw new BookNotFoundException("Invalid ISBN");
}
}
}
@RestControllerAdvice
public class AppExceptionHandler {
private Logger logger = LoggerFactory.getLogger(AppExceptionHandler.class);
@ExceptionHandler(value = BookNotFoundException.class)
public ResponseEntity<ExceptionInfo> handleBookNotFoundException(BookNotFoundException be) {
ExceptionInfo exception = new ExceptionInfo();
exception.setCode("AIT0005062");
exception.setMsg(be.getMessage());
logger.error("Exception Occurred : " + be);
return new ResponseEntity<>(exception, HttpStatus.BAD_REQUEST);
}
}
// Welcome Service class
@Service
public class WelcomeService {
public String getMsg() {
String msg = "Good Evening !!";
return msg;
}
}
// Welcome Rest Controller class
@RestController
public class WelcomeRestController {
@Autowired
private WelcomeService service;
@GetMapping("/welcome")
public String getWelcomeMsg() {
String msg = service.getMsg();
return msg;
}
}
// Welcome Rest Controller Test class
@WebMvcTest(value = WelcomeRestController.class)
public class WelcomeRestControllerTest {
@MockitoBean
private WelcomeService service;
@Autowired
private MockMvc mockMvc;
@Test
public void getWelcomeMsgTest() throws Exception {
// defining mock obj behaviour
when(service.getMsg()).thenReturn("Welcome to Ashok IT !!");
// preparing request
MockHttpServletRequestBuilder requestBuilder = MockMvcRequestBuilders.get("/welcome");
// sending request
MvcResult mvcResult = mockMvc.perform(requestBuilder).andReturn();
// getting response
MockHttpServletResponse response = mvcResult.getResponse();
// validating response status code
int actuaStatus = response.getStatus();
assertEquals(200, actuaStatus);
}
}
Security is very important for every application
To protect our application & application data we need to implement security logic
Spring Security is one of the module available in Spring framework
Spring Security concept we can use to secure our web applications / REST APIs
To implement security, we need to know about two concepts
1) Authentication 2) Authorization
Authentication means verifying who can access our application
Authorization means verifying which user can access which functionality
To secure our Spring Boot application we need to add security-starter
in pom.xml
file
When we access our application url in browser then it will display login form to authenticate our request and we need to use below credentials to access our application
Username : user Password : <copy the pwd from console>
To access secured REST API from Postman we need to set Auth values in Postman to send the request
Auth : Basic Auth Username : user Password : <copy-from-console>
To access secured REST API using client app we need to send basic auth credentials in request header
Note: By adding security-starter
our application will become secured with HTTP Basic Auth
and when we run our application everytime it will generate one random password to access our application. This generated random password will be printed on console.
Note: When we send credentials from Postman (or any client app) using Basic Auth, it automatically generates an HTTP header in below format
Key ---> Authorization Value ---> Basic Base64.encode(username:password)
application.properties or application.yml
file like below # Security Credentials
spring.security.user.name=ashokit
spring.security.user.password=ashokit@123
Note: We can set a single username and password in application.properties or application.yml
file hence same credentials will be used by all users which is not recommended.
When we add security-starter
in pom.xml
then by default it will apply security filter to all the url patterns in our application
But in realtime we don't need to secure all the url patterns we need to secure only few url patterns
/login-page : security not required (anyone can access) /transfer : security required /balance : security required /about-us : security not required /contact-us : security not required
To secure specific url patterns we need to customize security configuration like below
// Securing specific url patterns
@Configuration
public class AppSecurityConfig {
@Bean
public SecurityFilterChain securityConfig(HttpSecurity httpSecurity) throws Exception {
httpSecurity.authorizeHttpRequests(req -> req
.requestMatchers("/welcome").permitAll()
.anyRequest().authenticated()
).httpBasic(Customizer.withDefaults())
.formLogin(Customizer.withDefaults());
return httpSecurity.build();
}
}
Note: formLogin()
is optional it is used to display login form in the browser.
// Rest Template with Basic Authentication Header
HttpHeaders headers = new HttpHeaders();
headers.setBasicAuth("ashokit", "ashokit@123");
HttpEntity<String> entity = new HttpEntity<>(headers);
RestTemplate rt = new RestTemplate();
ResponseEntity<String> responseEntity = rt.exchange(apiUrl, HttpMethod.GET, entity, String.class);
String body = responseEntity.getBody();
System.out.println("Rest Template : "+body);
// WebClient with Basic Authentication Header
WebClient webClient = WebClient.create();
String body = webClient.get()
.uri(apiUrl)
.headers(header -> header.setBasicAuth("ashokit", "ashokit@123"))
.retrieve()
.bodyToMono(String.class)
.block();
System.out.println("Web Client : "+body);
Note: It is not recommended in realtime because it is insecure and not scalable.
// In-Memory Authentication
@Configuration
public class AppSecurityConfig {
@Bean
public InMemoryUserDetailsManager inMemoryUsers() {
UserDetails u1 = User.withDefaultPasswordEncoder()
.username("ashokit")
.password("ashokit@123")
.build();
UserDetails u2 = User.withDefaultPasswordEncoder()
.username("raju")
.password("raju@123")
.build();
UserDetails u3 = User.withDefaultPasswordEncoder()
.username("rani")
.password("rani@123")
.build();
return new InMemoryUserDetailsManager(u1, u2, u3);
}
}
Develop a Spring Boot REST API with below two functionalities using Spring Security
1) Customer Registration (name, email, pwd and phn) 2) Customer Login (email, pwd)
Note-1: When customer register, we need to store customer data in database table by encrypting password.
Note-2: When customer try to login, if credentials are valid send welcome msg as response. If credentials are invalid then send "Invalid Credential" msg as response.
Create Spring Boot app with below dependencies
a) web-starter b) devtools c) lombok d) data-jpa e) mysql f) security-starter
Configure below properties in application.properties
file
# DataSource properties
spring.datasource.url=jdbc:mysql://localhost:3306/sbms
spring.datasource.username=ashokit
spring.datasource.password=AshokIT@123
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
# ORM properties
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true
// Customer entity class
@Data
@Entity
@Table(
name = "CUST_TBL",
uniqueConstraints = @UniqueConstraint(columnNames = "CUST_EMAIL")
)
public class Customer {
@Id
@Column(name = "CUST_ID")
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
@Column(name = "CUST_NAME")
private String name;
@Column(name = "CUST_EMAIL")
private String email;
@Column(name = "CUST_PWD")
private String pwd;
@Column(name = "CUST_PHN")
private Long phn;
}
// Customer Repository interface
public interface CustomerRepository extends JpaRepository<Customer, Integer> {
public Customer findByEmail(String email);
}
CustomerService
class by implementing UserDetailsService
interface // Customer service class
@Service
public class CustomerService implements UserDetailsService {
@Autowired
private CustomerRepository custRepo;
@Override
public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
Customer customer = custRepo.findByEmail(email);
return new User(customer.getEmail(), customer.getPwd(), Collections.emptyList());
}
}
SecurityConfig
class // SecurityConfig class
@Configuration
public class SecurityConfig {
@Autowired
private CustomerService custService;
@Bean
public PasswordEncoder getPasswordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public AuthenticationProvider getAuthProvider() {
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider(custService);
authProvider.setPasswordEncoder(getPasswordEncoder());
return authProvider;
}
@Bean
@SneakyThrows
public AuthenticationManager getAuthManager(AuthenticationConfiguration config) {
return config.getAuthenticationManager();
}
@Bean
@SneakyThrows
public SecurityFilterChain getSecurityFilterChain(HttpSecurity httpSecurity) {
httpSecurity.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(req -> req
.requestMatchers("/register", "/login")
.permitAll()
.anyRequest()
.authenticated()
);
return httpSecurity.build();
}
}
RestController
with required methods // Customer Rest Controller
@RestController
public class CustomerRestController {
@Autowired
private PasswordEncoder pwdEncoder;
@Autowired
private CustomerRepository custRepo;
@Autowired
private AuthenticationManager authManager;
@PostMapping("/register")
public ResponseEntity<String> registerCustomer(@RequestBody Customer customer) {
customer.setPwd(pwdEncoder.encode(customer.getPwd()));
custRepo.save(customer);
return new ResponseEntity<>("Registration Successful !!", HttpStatus.CREATED);
}
@PostMapping("/login")
public ResponseEntity<String> customerLogin(@RequestBody Customer cust) {
ResponseEntity<String> respEntity = null;
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(cust.getEmail(), cust.getPwd());
try {
Authentication authenticate = authManager.authenticate(token);
if(authenticate.isAuthenticated()) {
respEntity = new ResponseEntity<>("Welcome to Ashok IT !!", HttpStatus.OK);
}
} catch (Exception e) {
e.printStackTrace();
respEntity = new ResponseEntity<>("Invalid Credentials !!", HttpStatus.BAD_REQUEST);
}
return respEntity;
}
}
{
"name": "Ashok",
"email": "ashok@gmail.com",
"pwd": "ashokit@123",
"phn": 122383872
}
Create OAuth app in github and generate Client Id & Client Secret
Login --> Profile -> Settings Developer Settings --> OAuth Apps Create App --> Copy Client ID & Client Secret
Create Spring Boot application with below dependencies
a) web-starter b) security-starter c) oauth-client
Create Rest Controller with required methods
@RestController
public class WelcomeRestController {
@GetMapping("/")
public String getWelcomeMsg() {
return "Welcome to Ashok IT !!";
}
}
application.yml
file like below # OAuth client config
spring:
security:
oauth2:
client:
registration:
github:
client-id: Ov23lizkyVTGdFnoVnfB
client-secret: 0f2d63799e4a925b14d244f57264b21412115697
Note: OAuth 2.0 is an authorization protocol not an authentication protocol.
Requirement: Develop a Spring Boot app with OAuth using Google. Get profile info also from Google and display that in response.
Go to Google Cloud Console and create OAuth App
Generate Client Id & Client Secret
Configure Authorized URL
http://localhost:8080/login/oauth2/code/google
Create Rest Controller
@RestController
public class MyRestController {
@GetMapping("/")
public Map<String, Object> getProfileDetails(@AuthenticationPrincipal OAuth2User principal) {
return principal.getAttributes();
}
}
# OAuth client config
spring:
security:
oauth2:
client:
registration:
google:
client-id: 703761262972-qks94ugn2n4j5e1ou24tspuue3q66ehj.apps.googleusercontent.com
client-secret: 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
JWT stands for JSON Web Token
JWT is a standard way to securely transmit information between two applications
JWT is compact, lightweight and URL-safe
JWT contains below three parts which are separated by period .
Header ---> contains metadata (token type and signing algorithm) Payload ---> contains the actual data (claims) Signature ---> ensures the token has not been tampered
Note: Every JWT has a configurable expiry time after which it becomes invalid.
Consumer App will send credentials to the Provider App for authentication
Provider App will validate these credentials
If the credentials are valid Provider App will send JWT Token to the Consumer App
Consumer App will send this JWT Token in request header to access the secured endpoints of the Provider App
Authorization = Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJhc2hva0BnbWFpbC5jb20iLCJpYXQiOjE3NTcxNDgzNDUsImV4cCI6MTc1NzE0ODk0NX0.XMMPgntlmfykf0rkng05ZQ3CG0_N-WjIHXpV24QQfEo
Create JWT Service
- generate token - validate token
JWT Token validation using filter - OncePerRequest
- check Authorization header presence - retrieve bearer token from header - validate token - if token is valid, update security context to process req
Customize SecurityFilterChain
- permit /register & /login urls - authenticate any other request
In realtime we will have multiple microservices in one project
It is not recommended to implement security in each microservice separately
Instead we implement security at only one place which is API Gateway using JWT
GitHub URL: https://github.com/arpitaggarwal135/Microservices-Security-App
Note: Microservices are stateless hence we maintain session info at frontend by storing the JWT token into session storage (or local storage) and in every subsequent request we will send this JWT token in request header to prove authentication.
Auth-API contains user registration and user login functionality
If user login successfully then Auth-API will generate JWT token and will send it as response
API-Gateway contains the logic to validate the JWT token using filter
/welcome ---> AuthFilter ---> Welcome-API /greet ---> AuthFilter ---> Greet-API /auth/** ---> Auth-API
Q) What happen if we send a request directly to a microservice URL instead of the API Gateway ?
Spring Batch is a powerful framework for processing large volumes of data in a batch-oriented way
It’s part of the Spring Framework and provides reusable functions essential for processing data, such as logging, transaction management and job processing statistics
Here are some key components and concepts of Spring Batch
- ItemReader - ItemProcessor - ItemWriter - Step - Job - JobRepository - JobLauncher
ItemReader : It is used to read the data from the source
ItemProcessor : It is used to process the data
ItemWriter : It is used to write the data to the destination
Step : It represents sequence of our batch execution (reader + processor + writer)
Job : It is used to represent set of steps for batch processing
JobRepository : It maintains job execution history and steps info
JobLauncher : It is used to start the job execution
- bulk msg - bulk email - sending emps salaries - sending bank stmt to acc holders - sending post paid bill reports - sending payment reminder notifications
Requirement : Read the data from csv file and write into database table using spring batch framework.
Create Spring Boot application with below dependencies
a) web-starter b) devtools c) data-jpa d) mysql-driver e) lombok f) batch
Configure below properties in application.properties
file
# Datasource properties
spring.datasource.url=jdbc:mysql://localhost:3306/sbms
spring.datasource.username=ashokit
spring.datasource.password=AshokIT@123
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
# ORM properties
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
src/main/resources
folder (customers.csv) // Customer entity class
@Data
@Entity
@Table(name = "CUST_TBL")
public class Customer {
@Id
@Column(name = "CUST_ID")
private Integer id;
@Column(name = "FIRST_NAME")
private String firstName;
@Column(name = "LAST_NAME")
private String lastName;
@Column(name = "EMAIL")
private String email;
@Column(name = "GENDER")
private String gender;
@Column(name = "CONTACT_NO")
private String contactNo;
@Column(name = "COUNTRY")
private String country;
@Column(name = "DOB")
private String dob;
}
// Customer Repositoty interface
public interface CustomerRepository extends JpaRepository<Customer, Serializable> {
}
ItemProcessor
to process the data // Customer Item Processor
public class CustomerItemProcessor implements ItemProcessor<Customer, Customer>{
@Override
public Customer process(Customer item) throws Exception {
// logic to process data
return item;
}
}
// Spring Batch Config class
@Configuration
public class SpringBatchConfig {
@Autowired
private CustomerRepository customerRepo;
@Bean
public FlatFileItemReader<Customer> itemReader() {
return new FlatFileItemReaderBuilder<Customer>()
.name("csv-reader")
.linesToSkip(1)
.resource(new ClassPathResource("customers.csv"))
.delimited()
.names("id", "firstName", "lastName", "email", "gender", "contactNo", "country", "dob")
.targetType(Customer.class)
.build();
}
@Bean
public CustomerItemProcessor itemProcessor() {
return new CustomerItemProcessor();
}
@Bean
public RepositoryItemWriter<Customer> customerWriter() {
RepositoryItemWriter<Customer> writer = new RepositoryItemWriter<>();
writer.setRepository(customerRepo);
writer.setMethodName("save");
return writer;
}
@Bean
public Step step1(JobRepository jobRepository, PlatformTransactionManager transactionManager,
FlatFileItemReader<Customer> reader, CustomerItemProcessor processor, RepositoryItemWriter<Customer> writer) {
return new StepBuilder("step1", jobRepository)
.<Customer, Customer>chunk(25, transactionManager)
.reader(reader)
.processor(processor)
.writer(writer)
.build();
}
@Bean
public Job importUserJob(JobRepository jobRepository, Step step1) {
return new JobBuilder("importUserJob", jobRepository)
.start(step1)
.build();
}
}
// Customer Rest Controller
@RestController
public class CustomerRestController {
@Autowired
private JobLauncher jobLauncher;
@Autowired
private Job job;
@GetMapping("/import")
public String loadDataToDB() throws Exception{
JobParameters jobParams = new JobParametersBuilder()
.addLong("startAt", System.currentTimeMillis()).toJobParameters();
jobLauncher.run(job, jobParams);
return "Data Imported !!";
}
}