Rest, Mikroserwisy, Spring Cloud - rlip/java GitHub Wiki
- Rest
- Spring Cloud Config
- Feign proxy
- Ribbon Load Balancer
- Eureka Name Server
- Netflix Zuul Api Gateway
- Spring Cloud Sleuth - nadawanie indentyfikatorów requestom
- Actuator - narzędzia do monitorowania
- Hal Browser - przeglądarka requestów
- Hystrics Fault Tolerance
- Odpytanie innych serwisów
- Hateoas - linki w odpowiedzi json
- Swagger - dokumentacja
- Filtrowanie
- Wersjonowanie
- Basic Authentication
- Exception Handling
- Walidacja
- Spring Cloud Contract
Code | Meaning |
---|---|
200 Ok | Successful requests other than creations and deletions. |
201 Created | Successful creation of a queue, topic, temporary queue, temporary topic, session, producer, consumer, listener, queue browser, or message. |
204 No Content | Successful deletion of a queue, topic, session, producer, or listener. |
400 Bad Request | The path info doesn't have the right format, or a parameter or request body value doesn't have the right format, or a required parameter is missing, or values have the right format but are invalid in some way (for example, destination parameter does not exist, content is too big, or client ID is in use). |
403 Forbidden | The invoker is not authorized to invoke the operation. |
404 Not Found | The object referenced by the path does not exist. |
405 Method Not Allowed | The method is not one of those allowed for the path. |
409 Conflict | An attempt was made to create an object that already exists. |
500 Internal Server Error | The execution of the service failed in some way. |
200-ok, 404-not found
- HTTP GET http://www.appdomain.com/users
- HTTP GET http://www.appdomain.com/users?size=20&page=5
- HTTP GET http://www.appdomain.com/users/123
- HTTP GET http://www.appdomain.com/users/123/address
201 - jeśli utworzy, 200 (OK) or 204 (No Content)
- HTTP POST http://www.appdomain.com/users
- HTTP POST http://www.appdomain.com/users/123/accounts
201 - jeśli utworzy, jeśli zmodyfikowany: 200 (OK) - ze statusem or 204 (No Content) -bez treści
- HTTP PUT http://www.appdomain.com/users/123
- HTTP PUT http://www.appdomain.com/users/123/accounts/456
200 (OK) - jeśli zawiera status, 202 - Accepted - jeśli usuwanie w kolejce, 204 (No Content) - jeśli nic nie zawiera, 404 (NOT FOUND) jeśli nie ma
- HTTP DELETE http://www.appdomain.com/users/123
- HTTP DELETE http://www.appdomain.com/users/123/accounts/456
Consumer first Liczba mnoga w nazwach np. users
https://cloud.spring.io/spring-cloud-config/reference/html/
Przechowuje konfiguracje mikroserwisów i to dla różnych środowisk Potrzebny jest:
- klasa cloud server np. z springboota
- repozytorium gita
- spring.appication.name, server.port i spring.cloud.config.server.git.url
- @EnableConfigServer przy @SpringBootApplication
- zapisujemy w gicie nasze propertisy, domyślne + osobno dla każdego środowiska
spring.application.name=spring-cloud-config-server
server.port=8888
spring.cloud.config.server.git.uri=file://C:/p/spring-microservices/git-localconfig-repo
////
@EnableConfigServer
@SpringBootApplication
public class SpringCloudConfigServerApplication {
////
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config-server</artifactId>
</dependency>
Trzeba zrobić normalnie klasę do odczytu parametrów, oraz:
spring.application.name=limits-service
spring.cloud.config.uri=http://localhost:8888
spring.profiles.active=dev
///
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-config</artifactId>
</dependency>
Zrobienie feign proxy zamiast rest template
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-feign</artifactId>
<version>1.4.7.RELEASE</version>
</dependency>
///////
@FeignClient(name="currency-exchange-service", url="localhost:8000")
public interface CurrencyExchangeServiceProxy {
@GetMapping("/currency-exchange/from/{from}/to/{to}")
CurrencyConversionBeen retrieveExchangeValue(@PathVariable String from, @PathVariable String to);
}
//////
@Autowired
CurrencyExchangeServiceProxy currencyExchangeServiceProxy;
// i w metodzie:
CurrencyConversionBeen response = currencyExchangeServiceProxy.retrieveExchangeValue(from, to);
//////
Nowsze:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
--
@Configuration
@EnableFeignClients
public RestClientConfiguration { }
--
@FeignClient("ingredient-service")
public interface IngredientClient {
@GetMapping("/ingredients/{id}")
Ingredient getIngredient(@PathVariable("id") String id);
}
spring-cloud-starter-netflix-eureka-client to też dodaje ribona
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-ribbon</artifactId>
<version>1.4.7.RELEASE</version>
</dependency>
////////
@FeignClient(name="currency-exchange-service")
@RibbonClient(name="currency-exchange-service")
public interface CurrencyExchangeServiceProxy {
@GetMapping("/currency-exchange/from/{from}/to/{to}")
CurrencyConversionBeen retrieveExchangeValue(@PathVariable String from, @PathVariable String to);
}
////
currency-exchange-service.ribbon.listOfServers=http://localhost:8000,http://localhost:8001,http://localhost:8002
spring.application.name=netflix-eureka-naming-server
server.port=8761
eureka.client.register-with-eureka=false //czy zarejestrwowąć się jako usługa na innych egzemplarzach eureki
eureka.client.fetch-registry=false //czy pobierać rejestr z innych eurek (to i to wyżej, jest po to żeby niebyło błędu przy lokalnym uruchomieniu)
--
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
////
@EnableEurekaServer
@SpringBootApplication
public class NetflixEurekaNamingServerApplication {
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka</artifactId> //nazwa z netflix jest nowsza
<version>1.4.7.RELEASE</version>
</dependency>
--------------nowsze:
<properties>
<spring-cloud.version>Finchley.SR1</spring-cloud.version>
</properties>
---
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
/////
eureka.client.service-url.default-zone=http://localhost:8761/eureka/
/////
@EnableDiscoveryClient
@SpringBootApplication
public class CurrencyConversionServiceApplication {
eureka.server.enableSelfPreservation=false - wyłącza tryb utrzymania egzemplarza (serwisy powinny odnawiać rejestrację co 30 sekund, jak nie zrobią tego 3 razy albo usługa jest utrzymywana (przy true), lub wyrejestrowana (przy false). True jest dobre na produkcję, żeby w przypadku chwilowych problemów z siecią nie wyrejestrowywać serwisów.
jak przez nią będa przechodzic wszystkie requesty to można tu sobie zrobic jakieś logowanie, autoryzację, itp
można wywołać go przez http://localhost:8761/{nazwa_serwisu}/{url_serwisu}
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>
////
@EnableZuulProxy
@EnableDiscoveryClient
@SpringBootApplication
public class NetflixZuulApiGatewayServerApplication {
////
@Component
public class ZuulLoggingFilter extends ZuulFilter {
private Logger logger = LoggerFactory.getLogger(this.getClass());
@Override
public String filterType() {
return "pre"; //"pre","post","error"
}
@Override
public int filterOrder() {
return 1;
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() {
HttpServletRequest request = RequestContext.getCurrentContext().getRequest();
logger.info("request -> {} request uri -> {}", request, request.getRequestURI());
return null;
}
}
Jak używamy Feign proxy to trzeba podać w @FeignClient nazwę zuula, a w @GetMapping trzeba dodać nazwę serwisu przed urlem
@FeignClient(name="netflix-zuul-api-gateway-server")
@RibbonClient(name="currency-exchange-service")
public interface CurrencyExchangeServiceProxy {
@GetMapping("/currency-exchange-service/currency-exchange/from/{from}/to/{to}")
CurrencyConversionBeen retrieveExchangeValue(@PathVariable String from, @PathVariable String to);
}
W wersji co miałem wystarczyło dodać coś takiego, żeby requesty miały id
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-sleuth</artifactId>
<version>${spring-cloud-sleuth.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-sleuth</artifactId>
</dependency>
</dependencies>
Dużo różnych narzedzi monitorujących. Dobrze uruchomić /actuator ze stron głównej przez Hal Browser
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
Przeglądarka requestów REST - standardowo dostępna w głównej ścieżce
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-rest-hal-browser</artifactId>
</dependency>
Można zrobić, że jak będzie jakieś runtime exception to zwróci jakieś domyślne wartości
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-hystrix</artifactId>
<version>1.4.7.RELEASE</version>
</dependency>
////////////////
@GetMapping("/fault-tolerance-example")
@HystrixCommand(fallbackMethod = "fallbackRetrieveConfiguration")
public LimitConfiguration retrieveLimitConfigurations() {
throw new RuntimeException("Not available");
}
public LimitConfiguration fallbackRetrieveConfiguration() {
return new LimitConfiguration(9, 999);
}
Map<String, String> uriVariables = new HashMap<>();
uriVariables.put("from", from);
uriVariables.put("to", to);
ResponseEntity<CurrencyConversionBeen> responseEntity = new RestTemplate().getForEntity(
"http://localhost:8000/currency-exchange/from/{from}/to/{to}",
CurrencyConversionBeen.class,
uriVariables);
CurrencyConversionBeen response = responseEntity.getBody();
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-hateoas</artifactId>
</dependency>
//////////
@GetMapping("/users")
public List<User> retrieveAllUsers() {
return service.findAll();
}
@GetMapping("/users/{id}")
public Resource<User> retrieveUser(@PathVariable int id) {
User user = service.findOne(id);
Resource<User> resource = new Resource<>(user);
ControllerLinkBuilder linkTo = linkTo(methodOn(this.getClass()).retrieveAllUsers());
resource.add(linkTo.withRel("all-users"));
return resource;
}
/swagger-ui.html --html /v2/api-docs --json
swagger-codegen-maven-plugin - pozwala na wygenerowanie klienta api z jsona
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.4.0</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId> // to jest dla strony html
<version>2.4.0</version>
</dependency>
/////////////
@Configuration
@EnableSwagger2
public class SwaggerConfig {
@Bean
public Docket api() { //to służy do ustawiania opisów dokumentacji
return new Docket(DocumentationType.SWAGGER_2)
.select()
.paths(PathSelector.ant("/api/**")) //tu można wymienić jakie ścieżki nas interesują, jeśli nie wszystkie
.apis(RequestHandlerSelectors.basePackage("com.commarch.custormer.api.rest")) // to jak wyżej, tylko ogranicza do pakietu
.build()
.apiInfo(createApiInfo()));
}
private createApiInfo(){
return ApiInfo(
"Customer API",
"Customer database",
"1.01",
"comarch.com/termsOfService",
new Contact(
"Roman L",
"comarch.com",
"[email protected]"
),
"licencja",
"comarch.com/licence"
)
}
}
///////////////
//to nie obowiązkowe, dodaje do opisu
@GetMaping
@ApiOperation(value = "Find customer by id", notes = "Resturns by given id")
public Customer getCustomer(@ApiParam(value= "Unique id of customer", example = "145") @PathVariable int id) {
}
@JsonIgnoreProperties(value={"field1","field2"}) //nad encją
@JsonIgnore //nad zmienną
@JsonFilter("SomeBeanFilter") //nad encją
/////////
@GetMapping("/filtering")
public MappingJacksonValue retrieveSomeBen() {
SomeBean someBean = new SomeBean("value1", "value2", "value3");
MappingJacksonValue mapping = new MappingJacksonValue(someBean);
SimpleBeanPropertyFilter filter = SimpleBeanPropertyFilter.filterOutAllExcept("field1", "field3");
FilterProvider filters = new SimpleFilterProvider().addFilter("SomeBeanFilter", filter);
mapping.setFilters(filters);
return mapping;
}
@GetMapping("/filtering-list")
public MappingJacksonValue retrieveListOSomeBen() {
List<SomeBean> list = Arrays.asList(
new SomeBean("value1", "value2", "value3"),
new SomeBean("value21", "value22", "value23")
);
MappingJacksonValue mapping = new MappingJacksonValue(list);
SimpleBeanPropertyFilter filter = SimpleBeanPropertyFilter.filterOutAllExcept("field1", "field3");
FilterProvider filters = new SimpleFilterProvider().addFilter("SomeBeanFilter", filter);
mapping.setFilters(filters);
return mapping;
}
Te w adresie można cachować
// Można tak
// @GetMapping(value="/v1/person")
// @GetMapping(value="/v2/person")
/////Params -/person/param?version=1
@GetMapping(value="/person/param", params="version=1")
public PersonV1 paramV1() {
return new PersonV1("Bob Charlie");
}
@GetMapping(value="/person/param", params="version=2")
public PersonV2 paramV2() {
return new PersonV2(new Name("Bob", "Charlie"));
}
////Headers - po prostu do hedera
@GetMapping(value="/person/header", headers="X-API-VERSION=1")
public PersonV1 headerV1() {
return new PersonV1("Bob Charlie");
}
@GetMapping(value="/person/header", headers="X-API-VERSION=2")
public PersonV2 headerV2() {
return new PersonV2(new Name("Bob", "Charlie"));
}
////Produces - to do Accept Header
@GetMapping(value="/person/produces", produces="application/vnd.company.app-v1+json")
public PersonV1 producesV1() {
return new PersonV1("Bob Charlie");
}
@GetMapping(value="/person/produces", produces="application/vnd.company.app-v2+json")
public PersonV2 producesV2() {
return new PersonV2(new Name("Bob", "Charlie"));
}
standardowo po dodaniu dependecy spring security włacza się basic auth z loginem user i hasłem podawanym w konsoli, można to zmienić ustawiająć w propertisach:
spring.security.user.name=username
spring.security.user.password=123456
management.secutiry.enabled // to włacza auth w Actuatorze
///////
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
jak nie ma danego zasobu można zrobić np throw new CostamNotFofoundException("id = " + id)
i adnotacjąc ustawić 404
@ResponseStatus(HttpStatus.NOT_FOUND)
public class CostamNotFofoundExceptionextends RuntimeException {
public UserNotFoundException(String message) {
super(message);
}
}
Zamiast tego lepiej globalnie ustandaryzować zwracane exceptiony (zrobic klasę i kontroller przechwytujący):
@ControllerAdvice //dodaje mettody dostepne we innych controlerach
@RestController
public class CustomizesResponseEntityExceptionHandler extends ResponseEntityExceptionHandler {
@ExceptionHandler(Exception.class)
public final ResponseEntity<Object> handleAllException(Exception ex, WebRequest request) {
ExceptionResponse exceptionResponse = new ExceptionResponse(new Date(), ex.getMessage(), request.getDescription(false));
return new ResponseEntity<>(exceptionResponse, HttpStatus.INTERNAL_SERVER_ERROR);
}
@ExceptionHandler(UserNotFoundException.class)
public final ResponseEntity<Object> handleUserNotFoundException(Exception ex, WebRequest request) {
ExceptionResponse exceptionResponse = new ExceptionResponse(new Date(), ex.getMessage(), request.getDescription(false));
return new ResponseEntity<>(exceptionResponse, HttpStatus.NOT_FOUND);
}
}
//////////
public class ExceptionResponse {
private Date timestamp;
private String message;
private String details;
public ExceptionResponse(Date timestamp, String message, String details) {
this.timestamp = timestamp;
this.message = message;
this.details = details;
}
///getery, setery
@PostMapping("/users")
//poniżej dodać @Valid
public ResponseEntity<Object> createUser(@Valid @RequestBody User user){
/////
public class User {
//przez message - można dodać swoją wiadomość
@Size(min=2, message="Name should have at least 2 characters")
private String name;
@Past
private Date birthDate;
/////
//tak można ustawić co ma być zwracane gdy m=będzie bład
@ControllerAdvice
@RestController
public class CustomizesResponseEntityExceptionHandler extends ResponseEntityExceptionHandler {
@Override
protected ResponseEntity<Object> handleMethodArgumentNotValid(
MethodArgumentNotValidException ex,
HttpHeaders headers, HttpStatus status,
WebRequest request
) {
ExceptionResponse exceptionResponse = new ExceptionResponse(
new Date(),
"Validation Failed",
ex.getBindingResult().toString()
);
return new ResponseEntity<>(exceptionResponse, HttpStatus.BAD_REQUEST);
}
https://www.facebook.com/jvaconpoland/videos/755994141837289/