1-Spring Boot MS Bank App Step by Setp Jan 25
1-Spring Boot MS Bank App Step by Setp Jan 25
--------------------------------------
Microservices Architecture?
Adv MS? :)
=> Smaller code base is easy to maintain.
=> Easy to scale as individual component.
=> Technology diversity i.e. we can mix libraries, databases, frameworks etc.
=> Fault isolation i.e. a process failure should not bring whole system down.
=> Better support for smaller and parallel team.
=> Independent deployment
=> Deployment time reduce
Microservices Challenges :(
bank app
card
loan
account
Card:
http://localhost:9090/api/contact-info
http://localhost:9090/api/fetch?mobile=7088993300
loans:
http://localhost:8090/api/fetch?mobile=7088993300
http://localhost:8090/api/contact-info
account:
http://localhost:8080/api/fetch?mobile=7088993300
http://localhost:8080/api/contact-info
http://localhost:8080/api/accountsdetails?mobile=7088993300
http://localhost:8070/
Card
private int cardId;
private String cardNumber;
private LocalDate issueDate;
private int totalLimit;
private String mobile;
Loan
private int loanId;
private String mobile;
private String loanNumber;
private String loanType;
private int totalLoan;
private int amountPaid;
private int outstandingAmount;
Account
private int accId;
private String name;
private double balance;
private String email;
private String mobile;
@Override
public void run(String... args) throws Exception {
//Card(String cardNumber, LocalDate issueDate, int totalLimit, String
mobile)
cardRepo.save(new Card(getCardNumber(), LocalDate.now(),
10000,"7788993300"));
cardRepo.save(new Card(getCardNumber(), LocalDate.now(),
20000,"7988223300"));
}
private String getCardNumber(){
long val=new Random().nextLong(1000_0000_0000_000L);
Long value=1000_0000_0000_0000L+val;
return value.toString();
}
repo layer:
-------------
@Data
@NoArgsConstructor
@Entity
@Table(name = "card")
public class Card {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int cardId;
private String cardNumber;
private LocalDate issueDate;
private int totalLimit;
private String mobile;
@Repository
public interface CardRepo extends JpaRepository<Card,Integer> {
Card findByMobile(String mobile);
}
create CardDto
-------------
@Data
@NoArgsConstructor
public class CardDto {
//....
}
create DtoConverter
---------------------
public class DtoConverter {
public static CardDto entityToDto(Card card){
//
}
public static Card dtoToEntity(CardDto cardDto){
//
}
}
service layer:
-------------
public interface CardService {
public CardDto findByMobileNumber(String mobile);
}
@Service
@Transactional
public class CardServiceImpl implements CardService{
@Autowired
private CardRepo cardRepo;
@Override
public CardDto findByMobileNumber(String mobile) {
Card card= cardRepo.findByMobile(mobile);
return DtoConverter.entityToDto(card);
}
}
@Data
@ConfigurationProperties(prefix = "info")
public class InfoDto {
private String message;
private String name;
}
@EnableConfigurationProperties(InfoDto.class)
info:
message: "Welcome to busycoder card default profile"
name: "Raj: Product Owner card default profile"
controller layer:
-------------
@RequestMapping(path = "api")
@RestController
public class CardController {
@Autowired
private CardService cardService;
@Autowired
private InfoDto infoDto;
@GetMapping(path = "fetch")
public CardDto findByMobileNumber(@RequestParam(name="mobile") String mobile){
return cardService.findByMobileNumber(mobile);
}
@GetMapping("contact-info")
public InfoDto getInfoDto(){
return infoDto;
}
}
@Autowired
private CardRepo cardRepo;
@Override
public void run(String... args) throws Exception {
//Card(String cardNumber, LocalDate issueDate, int totalLimit, String
mobile)
cardRepo.save(new Card(getCardNumber(), LocalDate.now(),
10000,"7788993300"));
cardRepo.save(new Card(getCardNumber(), LocalDate.now(),
20000,"7988223300"));
}
private String getCardNumber(){
long val=new Random().nextLong(1000_0000_0000_000L);
Long value=1000_0000_0000_0000L+val;
return value.toString();
}
}
application.yaml
-------------------
server:
port: 9090
spring:
profiles:
active:
- "qa"
jpa:
show-sql: true
application:
name: cards
management:
endpoints:
web:
exposure:
include: "*"
endpoint:
shutdown:
enabled: true
info:
env:
enabled: true
build:
version: "1.0"
info:
message: "Welcome to busycoder card default profile"
name: "Raj: Product Owner card default profile"
application-prod.yml
---------------------
build:
version: "2.0"
info:
message: "Welcome to busycoder card dev profile"
name: "Ravi: Product Owner card dev profile"
application-qa.yml
---------------------
build:
version: "2.0"
info:
message: "Welcome to busycoder card qa profile"
name: "Tarun: Product Owner card qa profile"
@Data
@NoArgsConstructor
@Entity
@Table(name = "loan_table")
public class Loan {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int loanId;
private String mobile;
private String loanNumber;
private String loanType;
private int totalLoan;
private int amountPaid;
private int outstandingAmount;
}
@Service
public class LoanServiceImpl implements LoanService{
@Autowired
private LoanRepo loanRepo;
@Override
public LoanDto findByMobile(String mobile) {
return DtoConverter.entityToDto(loanRepo.findByMobile(mobile));
}
}
@GetMapping("fetch")
public LoanDto getByMobile( @RequestParam(name="mobile") String mobile){
return loanService.findByMobile(mobile);
}
@GetMapping("contact-info")
public InfoDto getInfoDto(){
return infoDto;
}
}
@Autowired
private LoanRepo loanRepo;
@Override
public void run(String... args) throws Exception {
loanRepo.save(new Loan("7788223300","AAMS11","education",
2000,1000,1000));
}
}
application.yml
-----------------
server:
port: 8090
spring:
profiles:
active:
- qa
jpa:
show-sql: true
application:
name: loans
management:
endpoints:
web:
exposure:
include: "*"
endpoint:
shutdown:
enabled: true
info:
env:
enabled: true
build:
version: "1.0"
info:
message: "Welcome to busycoder loans default profile"
name: "Raj: Product Owner loans default profile"
application-prod.yml
--------------------
build:
version: "2.0"
info:
message: "Welcome to busycoder loans dev profile"
name: "Ravi: Product Owner loans dev profile"
application-qa.yml
-----------
build:
version: "2.0"
info:
message: "Welcome to busycoder loans qa profile"
name: "Tarun: Product Owner loans qa profile"
@Data
@NoArgsConstructor
@AllArgsConstructor
@ConfigurationProperties(prefix = "info")
public class AppInfoDto {
private String message;
private String name;
}
creating entities
-----------------
@Data
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "account_table")
public class Account {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int accId;
private String name;
private double balance;
private String email;
private String mobile;
}
@Repository
public interface AccountRepo extends JpaRepository<Account, Integer> {
public Account findByMobile(String mobile);
}
@Service
@AllArgsConstructor
public class AccountServiceImpl implements AccountService{
@Override
public List<AccountDto> getAll() {
return accountRepo.findAll().stream()
.map(DtoConvertor::entityToDto)
.collect(Collectors.toList());
}
@Override
public AccountDto getByMobile(String mobile) {
return DtoConvertor.entityToDto(accountRepo.findByMobile(mobile));
}
@Override
public AccountInfoDto getAccountDetails(String mobile) {
//somehow we should able to call the
// loans and cards ms and get the related inforation
//http://localhost:8090/loans?mobile=7088993300
// http://localhost:9090/cards?mobile=7088993300
AccountInfoDto accountInfoDto=new AccountInfoDto();
accountInfoDto.setAccountDto(getByMobile(mobile));
return accountInfoDto;
}
@Override
public String addAccount(AccountDto accountDto) {
Account account=DtoConvertor.dtoToEntity(accountDto);
accountRepo.save(account);
accountDto.setAccId(account.getAccId());
return "account is added successfully";
}
}
@RequestMapping(path = "api")
@RestController
@AllArgsConstructor
public class AccountController {
private final AccountService accountService;
private final InfoDto appInfoDto;
@GetMapping("contact-info")
public InfoDto appInfo(){
return appInfoDto;
}
@GetMapping("fetchall")
public List<AccountDto> getAll(){
return accountService.getAll();
}
@GetMapping("fetch")
public AccountDto getByMobile(@RequestParam(name="mobile") String mobile){
return accountService.getByMobile(mobile);
}
@GetMapping("accountsdetails")
public AccountInfoDto getAccountDetails(@RequestParam(name = "mobile") String
mobile){
return accountService.getAccountDetails(mobile);
}
@PostMapping(path = "add")
public String addAccount(@RequestBody AccountDto accountDto){
return accountService.addAccount(accountDto);
}
}
bootstrap class
-----------------
@EnableConfigurationProperties(AppInfoDto.class)
@SpringBootApplication
public class AccountsApplication implements CommandLineRunner {
@Autowired
private AccountService accountService;
@Override
public void run(String... args) throws Exception {
accountService.addAccount(new Account("raj",1000,"raj@gmail.com",
"7788993300"));
accountService.addAccount(new Account("ekta",1000,"ekta@gmail.com",
"7788223300"));
}
}
apply
-----
@EnableConfigServer to the bootstrap class
application.yml
----------------
server:
port: 8071
spring:
application:
name: configserver
cloud:
config:
server:
git:
uri: file:///C:/configfiles
clone-on-start: true
default-label: master
configfiles-main
now try:
---------
http://localhost:8071/accounts/default
http://localhost:8071/loans/default
http://localhost:8071/cards/default
http://localhost:8080/api/contact-info
http://localhost:8080/actuator/refresh
2. url pattern
http://localhost:8070/
spring:
application:
name: "eurekaserver"
config:
import: "optional:configserver:http://localhost:8071/"
management:
endpoints:
web:
exposure:
include: "*"
health:
readinessstate:
enabled: true
livenessstate:
enabled: true
endpoint:
health:
probes:
enabled: true
4. configure eureka client in all the projects accounts, cards and loans
--------------------------------------------------------------------------
add eureka client dep to all projects
eureka:
instance:
prefer-ip-address: true
client:
register-with-eureka: true
fetch-registry: true
service-url:
defaultZone: http://localhost:8070/eureka/
spring:
cloud:
gateway:
discovery:
locator:
enabled: true
lower-case-service-id: true
@Bean
public RouteLocator busycoderRouteConfig(RouteLocatorBuilder
routeLocatorBuilder) {
return routeLocatorBuilder.routes()
.route(p -> p
.path("/busycoder/accounts/**")
.filters( f -> f.rewritePath("/busycoder/accounts/(?
<segment>.*)","/${segment}")
.addResponseHeader("X-Response-Time",
LocalDateTime.now().toString()))
.uri("lb://ACCOUNTS"))
.route(p -> p
.path("/busycoder/loans/**")
.filters( f -> f.rewritePath("/busycoder/loans/(?
<segment>.*)","/${segment}")
.addResponseHeader("X-Response-Time",
LocalDateTime.now().toString()))
.uri("lb://LOANS"))
.route(p -> p
.path("/busycoder/cards/**")
.filters( f -> f.rewritePath("/busycoder/cards/(?
<segment>.*)","/${segment}")
.addResponseHeader("X-Response-Time",
LocalDateTime.now().toString()))
.uri("lb://CARDS")).build();
}
@Component
public class LoggingFilter implements GlobalFilter {
private Logger logger = LoggerFactory.getLogger(LoggingFilter.class);
@Override
public Mono<Void> filter(ServerWebExchange exchange,
GatewayFilterChain chain) {
logger.info("Path of the request received -> {}",
exchange.getRequest().getPath());
return chain.filter(exchange);
}
configuration.yml
--------------------
server:
port: 8072
spring:
config:
import: optional:configserver:http://localhost:8071
application:
name: gatewayserver
eureka:
instance:
prefer-ip-address: true
client:
register-with-eureka: true
fetch-registry: true
service-url:
defaultZone: http://localhost:8070/eureka/
management:
endpoints:
web:
exposure:
include: "*"
health:
readinessstate:
enabled: true
livenessstate:
enabled: true
endpoint:
gateway:
enabled: true
health:
probes:
enabled: true
step 7.Configure resilence 4j to bank application
-----------------------------------------------
We can apply circuitbreaker pattern to api gateway
and to indidual microservice
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-circuitbreaker-resilience4j</artifactId>
</dependency>
step 2:
spring:
cloud:
openfeign:
circuitbreaker:
enabled: true
resilience4j:
circuitbreaker:
configs:
default:
sliding-window-size: 10
permitted-number-of-calls-in-half-open-state: 2
failure-rate-threshold: 50 #percentage
wait-duration-in-open-state: 10s
@Component
public class CardFallBack implements CardServiceProxy{
@Override
public CardDto findByMobileNumber(String mobile) {
return new CardDto();
}
}
@Component
public class LoanFallBack implements LoanServiceProxy{
@Override
public LoanDto getByMobile(String mobile) {
return new LoanDto();
}
}
http://localhost:8080/actuator
http://localhost:8080/actuator/circuitbreakerevents
setp 2: config
resilience4j:
circuitbreaker:
configs:
default:
sliding-window-size: 10
permitted-number-of-calls-in-half-open-state: 2
failure-rate-threshold: 50 #percentage
wait-duration-in-open-state: 10s
step 3:
.route(p -> p
.path("/busycoder/accounts/**")
.filters( f -> f.rewritePath("/busycoder/accounts/(?<segment>.*)","/${segment}")
.addResponseHeader("X-Response-Time", LocalDateTime.now().toString())
.circuitBreaker(config -> config.setName("accountCircuitBreaker")
.setFallbackUri("forward:/contactSupport")))
.uri("lb://ACCOUNTS"))
http://localhost:8072/actuator/circuitbreakers
http://localhost:8072/actuator/circuitbreakerevents?name=accountCircuitBreaker
spring:
cloud:
gateway:
discovery:
locator:
enabled: false
lower-case-service-id: true
httpclient:
connect-timeout: 1000
response-timeout: 2s
.route(p -> p
.path("/busycoder/loans/**")
.filters( f -> f.rewritePath("/busycoder/loans/(?<segment>.*)","/${segment}")
.addResponseHeader("X-Response-Time", LocalDateTime.now().toString())
.retry(retryConfig -> retryConfig.setRetries(3)
.setMethods(HttpMethod.GET)
.setBackoff(Duration.ofMillis(100),Duration
.ofMillis(1000),2,true)))
.uri("lb://LOANS"))
@RestController
public class CircuitBreakerController {
private Logger logger =
LoggerFactory.getLogger(CircuitBreakerController.class);
@GetMapping("/sample-api")
//@Retry(name = "sample-api", fallbackMethod = "hardcodedResponse")
//@CircuitBreaker(name = "default", fallbackMethod = "hardcodedResponse")
//@RateLimiter(name="default")
@Bulkhead(name="sample-api")
//10s => 10000 calls to the sample api
public String sampleApi() {
logger.info("Sample api call received");
// ResponseEntity<String> forEntity = new
RestTemplate().getForEntity("http://localhost:8080/some-dummy-url",
// String.class);
// return forEntity.getBody();
return "sample-api";
}
resilience4j.retry.instances.sample-api.maxRetryAttempts=5
resilience4j.retry.instances.sample-api.waitDuration=1s
resilience4j.retry.instances.sample-api.enableExponentialBackoff=true
#resilience4j.circuitbreaker.instances.default.failureRateThreshold=90
resilience4j.ratelimiter.instances.default.limitForPeriod=2
resilience4j.ratelimiter.instances.default.limitRefreshPeriod=10s
resilience4j.bulkhead.instances.default.maxConcurrentCalls=10
resilience4j.bulkhead.instances.sample-api.maxConcurrentCalls=10
Observability?
how well do we understand what is happing in the system?
Step 1: gather data : materix logs or traces
step 2: get intelligence : AI/Ops and anomaly detection
Step 1:
docker pull openzipkin/zipkin:2.23
docker run -p 9411:9411 openzipkin/zipkin:2.23
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-observation</artifactId>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-tracing-bridge-otel</artifactId>
</dependency>
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-exporter-zipkin</artifactId>
</dependency>
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-micrometer</artifactId>
</dependency>
logging:
pattern:
level: "%5p [${spring.application.name:},%X{traceId:-},%X{spanId:-}]"
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-micrometer</artifactId>
</dependency>
management.tracing.sampling.probability=1.0
logging.pattern.level=%5p [${spring.application.name:},%X{traceId:-},%X{spanId:-}]
spring.config.import=optional:configserver:
##spring.zipkin.baseUrl=http://localhost:9411/
##management.zipkin.tracing.endpoint=http://localhost:9411/api/v2/spans
Step 1: create spring boot application with actuator, and prometheus dep
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
server:
port: 8080
management:
endpoints:
web:
base-path: /actuator
exposure:
include: "*"
endpoint:
prometheus:
enabled: true
metrics:
enabled: true
step 2: download sw
Download and configure Prometheus: run on port : 9090
https://prometheus.io/download/
download grafana:
wget https://dl.grafana.com/enterprise/release/grafana-enterprise-9.5.2.linux-
amd64.tar.gz
https://grafana.com/grafana/download/9.5.2?platform=windows
prometheus.yml
-----------------
global:
scrape_interval: 15s # By default, scrape targets every 15 seconds.
# Attach these labels to any time series or alerts when communicating with
# external systems (federation, remote storage, Alertmanager).
external_labels:
monitor: 'codelab-monitor'
# Override the global default and scrape targets from this job every 5 seconds.
scrape_interval: 5s
static_configs:
- targets: ['localhost:9090']
- job_name: 'spring-actuator'
metrics_path: '/actuator/prometheus'
scrape_interval: 5s
static_configs:
- targets: ['localhost:8080']
Start prometheus
./prometheus
4.start grafana:
bin/grafana-server
http://localhost:9090
up
grafana dashboard
http://localhost:3000/
Dashboard-> new import -> grafana dashboard id -->put that id---> ui is created
Open ID connect
----------------
OAuth2 was designed for authorization
Open ID connect is build on top of Oauth2
https://www.keycloak.org/
provide:
cc: client credential
step 3: getting access token form auth server in client credential grant flow:
------------------------------------------------------------------------------
go to relem setting-->open endpoint section
http://localhost:7080/realms/master/.well-known/openid-configuration
select and create new post request to following url to get token:
http://localhost:7080/realms/master/protocol/openid-connect/token
grant_type: client_credentials
client_id: busycoder-cc
client_secret:
scope: openid email profile
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-resource-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-jose</artifactId>
</dependency>
@Configuration
@EnableWebFluxSecurity
public class SecurityConfig {
@Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity
serverHttpSecurity) {
serverHttpSecurity.authorizeExchange(exchanges ->
exchanges.pathMatchers(HttpMethod.GET).authenticated()
.pathMatchers("/busycoder/accounts/**").authenticated()
.pathMatchers("/busycoder/cards/**").authenticated()
.pathMatchers("/busycoder/loans/**").authenticated())
.oauth2ResourceServer(oAuth2ResourceServerSpec ->
oAuth2ResourceServerSpec
.jwt(Customizer.withDefaults()));
serverHttpSecurity.csrf(csrfSpec -> csrfSpec.disable());
return serverHttpSecurity.build();
}
}
spring:
security:
oauth2:
resourceserver:
jwt:
jwk-set-uri: "http://localhost:7080/realms/master/protocol/openid-
connect/certs"
Step 3: get fresh access token and verify new role jwt.io now you can see new role
under realm_access
jwtSpec.jwtAuthenticationConverter(grantedAuthoritiesExtractor())));
serverHttpSecurity.csrf(csrfSpec -> csrfSpec.disable());
return serverHttpSecurity.build();
}
Step 6: now we can access account resource but for others we get 403 error
provide:
cc: client credential
client id: busycoder-ac
Access settings
Root URL blank
Home URL blank
Valid redirect URIs *
Valid post logout redirect URIs blank
Web origins *
Admin URL blank
7. ELK
=========
Step 1: download tools
---------------------------
https://www.elastic.co/downloads/past-releases/elasticsearch-6-5-1
https://www.elastic.co/downloads/past-releases/kibana-6-5-1
https://www.elastic.co/downloads/past-releases/logstash-6-5-1
Step 2:
Start elasticsearch(9200)
-------------------
./elasticsearch port No: localhost:9200
start kibana(5601)
--------------
Uncomment the file kibana.yml to point to the elasticsearch instance.
elasticsearch url: http://localhost:9200
./bin/kibana
logstash
-------------
Create a configuration file named logstash.conf
bin/logstash -f bin/logstash.conf
http://localhost:9200/_cat/indices/?v
http://localhost:9200/logstash-2022.08.02/_search
logstash-*