spring boot microservice product store application getting started
spring boot microservice product store application getting started
----------------------------------
coupon : 8085
customer: 8081
product :8082
order : 9090
configserver: 8071
Eureka server: 8070
api gateway: 8072
Zipkin: 9411
=> It is a library for creating REST API clients. It makes web service clients
easier.
The developers can use declarative annotations to call the REST
services instead of writing representative boilerplate code.
=> Spring Cloud OpenFeign provides OpenFeign integrations for Spring Boot
apps through auto-configuration and binding to the Spring Environment.
Without Feign, in Spring Boot application, we use RestTemplate to call the User
service.
To use the Feign, we need to add spring-cloud-starter-openfeign dependency in the
pom.xml file.
@EnableFeignClients("com.order.service.proxyservies")
@FeignClient(name="coupons", url="http://localhost:8085")
step 3.1 put openfeign dependency
@EnableFeignClients("com.order.service.proxyservies")
@SpringBootApplication
public class OrderApplication {}
@FeignClient(name="coupons", url="http://localhost:8085")
public interface CouponServiceProxy {
@GetMapping(path="couponbycode/{couponCode}")
public CouponDto getAnCouponByCode(@PathVariable(name="couponCode") String
couponCode);
}
@FeignClient(name="customers", url="http://localhost:8081")
public interface CustomerServiceProxy {
@GetMapping(path = "customers/{id}")
public CustomerDto getAnCustomer(@PathVariable(name = "id") int id);
}
@FeignClient(name="products", url="http://localhost:8082")
public interface ProductServiceProxy {
@GetMapping(path = "products/{id}")
public ProductDto getAnProduct(@PathVariable(name = "id") int id);
}
step 3.4: now inject service proxy in controller and start using it
@Service
public class OrderServiceImpl implements OrderService{
@Autowired
private CouponServiceProxy couponServiceProxy;
@Autowired
private CustomerServiceProxy customerServiceProxy;
@Autowired
private ProductServiceProxy productServiceProxy;
@Override
public OrderDto bookOrder(OrderRequest orderRequest) {
....
}
}
@RestController
public class OrderRestController {
@Autowired
private OrderService orderService;
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
now try:
---------
http://localhost:8071/accounts/default
http://localhost:8071/loans/default
http://localhost:8071/cards/default
http://localhost:8080/api/contact-info
3. what if config property changes?
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/
6. now replace hard coded url in Openfeign service to logical names and run the
examples
give logical name of service
@FeignClient(name="PRODUCTS")
spring:
cloud:
gateway:
discovery:
locator:
enabled: true
lower-case-service-id: true
@Configuration
public class GatewayConfig {
@Bean
public RouteLocator routeConfig(RouteLocatorBuilder routeLocatorBuilder) {
return routeLocatorBuilder.routes()
.route(p -> p
.path("/busycoder/products/shoppingapp/**")
.filters( f ->
f.rewritePath("/busycoder/products/shoppingapp/(?<segment>.*)","/${segment}")
.addResponseHeader("X-Response-Time",
LocalDateTime.now().toString()))
.uri("lb://PRODUCTS"))
.route(p -> p
.path("/busycoder/customers/shoppingapp/**")
.filters( f ->
f.rewritePath("/busycoder/customers/shoppingapp/(?<segment>.*)","/${segment}")
.addResponseHeader("X-Response-Time",
LocalDateTime.now().toString()))
.uri("lb://CUSTOMERS"))
.route(p -> p
.path("/busycoder/orders/shoppingapp/**")
.filters( f ->
f.rewritePath("/busycoder/orders/shoppingapp/(?<segment>.*)","/${segment}")
.addResponseHeader("X-Response-Time",
LocalDateTime.now().toString()))
.uri("lb://ORDERS"))
.route(p -> p
.path("/busycoder/coupons/shoppingapp/**")
.filters( f ->
f.rewritePath("/busycoder/coupons/shoppingapp/(?<segment>.*)","/${segment}")
.addResponseHeader("X-Response-Time",
LocalDateTime.now().toString()))
.uri("lb://COUPONS"))
.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
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
@RestController
public class FallbackController {
@RequestMapping("/contactSupport")
public Mono<String> contactSupport() {
return Mono.just("An error occurred. Please try after some time or contact
support team!!!");
}
}
<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
registerHealthIndicator=true
event-consumer-buffer-size=10
slidingWindowType=COUNT_BASED
slidingWindowSize=5
failureRateThreshold=50
waitDurationInOpenState=5s
permittedNumberOfCallsInHalfOpenState=3
automaticTransitionFromOpenToHalfOpenEnabled=true
@Service
public class CouponServiceProxyFallBack implements CouponServiceProxy{
@Override
public CouponDto getAnCouponByCode(String couponCode) {
//int id, String couponCode, int discountPercentage, LocalDate expiredOn
return new CouponDto(1,"SUP10",10, LocalDate.now());
}
}
http://localhost:8080/actuator
http://localhost:8080/actuator/circuitbreakerevents
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 {
@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
8. Spring boot grafana and prometheus
----------------------------------------
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 grafana:
wget https://dl.grafana.com/enterprise/release/grafana-enterprise-9.5.2.linux-
amd64.tar.gz
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
Microservice security:
-----------------------
spring sec:
basics auth
jwt auth
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
enable client authentication--->auth flow --> service accounts roles (other dont
select)
two application try to communicate each other
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
understand token formate
<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
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-*
String
couponUrl="http://localhost:8085/couponbycode/"+orderRequest.getCouponCode();
String
custUrl="http://localhost:8081/customers/"+orderRequest.getCid();
System.out.println(custUrl);
String
productUrl="http://localhost:8082/products/"+orderRequest.getPid();
System.out.println(productUrl);
CustomerDto customerDto = restTemplate.getForObject(custUrl,
CustomerDto.class);
CouponDto couponDto=restTemplate.getForObject(couponUrl,
CouponDto.class);