SpringCloud 隨筆

++==(純手打,代碼可能有錯!)==++nginx


服務間通信

RestTemplate

  • 方式一
    直接使用restTemplate,url寫死
RestTemplate restTemplate = new RestTemplate();
    String responseStr = restTemplate.getForObject(「http://localhost:8080/msg」,String.class);
  • 方式二
    利用loadBalancerClient經過應用名獲取URL,而後再使用restTemplate
  1. 先給controller注入loadBalancerClient
@Autowired    
    private LoadBalancerClient loadBalancerClient;
  1. 獲取URL,再使用restTemplate
RestTemplate restTemplate = new RestTemplate();
    ServiceInstance serviceInstance = loadBalancerClient.choose(「PRODUCT」);
    String url = String.format(「http://%s:%s」, serviceInstance.getHost, serviceInstance.getPort()); 
    String responseStr = restTemplate.getForObject(url, String.class);
  • 方式三
    利用@LoadBalanced註解,可在restTemplate裏使用應用名字
  1. 利用@LoadBalanced註解
@Bean
    @LoadBalanced
    public RestTemplate restTemplate(){
            return new RestTemplate();
    }
String responseStr = restTemplate.getForObject(「http://PRODUCT/msg」, String.class);

Feign

  1. 添加依賴
<dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-feign</artifactId>
    </dependency>
  1. 在主類中添加註解
    @EnableFeignClients
  2. 編寫一個service連接其餘服務
    在controller包同級目錄下新建一個client包,在該包下新建ProductClient接口
// product對應的是服務名
@FeignClient(name="product")
public interface ProductClient{
    @GetMapping("/msg")
    String productMsg();
}

而後,在須要調用"/msg"接口的controller中這樣使用:git

@Autowired
private ProductClient productClient;

或添加其餘服務的pom直接訪問redis

統一配置中心

當要修改上線項目的一些配置、文案的時候,爲了避免用重複發行版本,只作一些小修改,就能夠用統一配置中心。算法

  • 問題一:Config是怎麼拿到碼雲上面的配置文件的?
  1. 建立一個springboot的配置中心服務端應用
  2. 引入依賴
<!-- 引入cloud依賴 -->
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>Finchley.RELEASE</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>
<!-- 引入cloud配置中心服務端依賴 -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-config-server</artifactId>
        </dependency>
        <!-- 引入Eureka客戶端依賴 -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
  1. 修改配置文件,此處把配置放到了碼雲上面
spring:
  application:
    name: config-server
  cloud:
      config:
        server:
          git:
            #  配置文件只搜索url目錄下的searchPaths
            uri: git@gitee.com:szliugx/spring_cloud_config.git
            # 指定搜索路徑,若是有多個路徑則使用,分隔
            searchPaths: infomation/
            # 對於使用git,svn作爲後端配置,從遠程庫獲取配置文件,須要存儲到本地文件
            basedir: /tmp/spring-cloud-repo
            # 配置中心經過git從遠程git庫,有時本地的拷貝被污染,這時配置中心沒法從遠程庫更新本地配置,設置force-pull=true,則強制從遠程庫中更新本地庫
            force-pull: true
            #username: username
            #password: password


#服務註冊中心端口號
server:
  port: 6130

#服務註冊中心實例的主機名、端口
#是否向服務註冊中心註冊本身
#是否檢索服務
#服務註冊中心的配置內容,指定服務註冊中心的位置
eureka:
  port:
  instance:
    hostname: localhost
  client:
    register-with-eureka: true
    fetch-registry: false
    serviceUrl:
      defaultZone: http://${eureka.instance.hostname}:${eureka.port}/eureka/
  1. 啓動類上添加註釋 @EnableConfigServer 和 @EnableEurekaClient
@SpringBoorApplication
@EnableConfigServer
@EnableEurekaClient
public class ConfigServerApplication {
    public static void main(String[] args){SpringApplication.run(ConfigServerApplication.class,args);}
}

最後,爲每一個服務添加一個屬於本身的配置文件在碼雲上
注意遠程配置的命名,如 user-dev.properties,服務名 -環境.properties/服務名 -環境.yml
/{name}-{profiles}.yml
/{label}/{name}-{profiles}.yml
Name: 服務名
Profiles:環境
Label:分支(branch)spring

Spring Cloud Bus

配置.YML文件,開放全部的接口;
使用註解:@RefreshScope數據庫

異步和消息

例如,用戶註冊後,須要發短信和加積分。用戶信息註冊寫入數據庫後,通知異步消息,通知短信服務和積分服務作事情。json

RabbitMQ

  1. 導入依賴Spring-boot-starter-amqp;
  2. 修改.YML,配置mq
rabbbitmq:
  host: localhost
  port: 5671
  username: guest
  password: guest
  1. 編寫代碼
    接收端:
/**
 * 水果商供應商服務 接收消息
 * @param message
 */
@RabbitListener(bindings = @QueueBinding(
    exchange = @Exchange("myOrder"),
    key = "fruit",
    value = @Queue("fruitOrder")
))
public void processFruit(String message){
    log.info("fruit MqReceiver: {}",message);
}
public class MqReceiver{
    // 1.@RabbitListener(queues = "myQueue")
    // 2.自動建立隊列
    @RabbitListener(queuesToDeclare = @Queue("myQueue"))
    public void process(String message){
        log.info("MqReceiver: {}",message);
    }
}

發送端:參數有(exchange、routingKey、message)後端

@Component
public class MqSenderTest extends OrderApplicationTests {
    @Autowired
    private AmqpTemplate amqpTemplate;
    
    @Test 
    public void send(){
        amqpTemplate.converAndSend("myQueue", "now");
    }
    
    @Test
    public void sendOrder(){
        amqpTemplate.convertAndSend("myOrder", "computer");
    }
}

Spring Cloud Stream

官方定義 Spring Cloud Stream 是一個構建消息驅動微服務的框架
應用程序經過 inputs 或者 outputs 來與 Spring Cloud Stream 中binder 交互,經過咱們配置來 binding ,而 Spring Cloud Stream 的 binder 負責與消息中間件交互。因此,咱們只須要搞清楚如何與 Spring Cloud Stream 交互就能夠方便使用消息驅動的方式。跨域

  1. 引入依賴:
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-stream-rabbit</artifactId>
</dependency>
  1. 配置文件中配置消息中間件RabbitMQ:
rabbbitmq:
  host: localhost
  port: 5671
  username: guest
  password: guest
  1. 使用spring cloud stream發送和接收消息
public interface StreamClient{
   String INPUT = "myMessage";
   
   @Input(StreamClient.INPUT)
   SubscribableChannel input();
   
   @Output(StreamClient.INPUT)
   MessageChannel output();
   
}

消息接收端:

@Component
@EnableBinding(StreamClient.class)
@Slf4j
public class StreamReceiver{
    
    @StreamListener(StreamClient.INPUT)
    public void process(Object message){
        log.info("StreamReceiver: {}",message);
    }
}

消息發送端:

@RestController 
public class SendMessageController{
    
    @Autowired
    private StreamClient streamClient;
    
    @GetMapping("/sendMessage")
    public void process(){
        String message = "now" + new Date();
        streamClient.output().send(MessageBuilder.withPayload(message).build());
    }
    
}

消息分組:
把一個服務放到一個組裏面,無論這個服務有多少個實例,只會由一個實例來處理一個消息。

spring:
  application:
    name: order
  cloud:
    config:
    discovery:
      enabled: true
      service-id: CONFIG
    profile: test
  stream:
    bindings:
      # 隊列名稱
      myMessage:
        # 組名稱
        group: order

爲了在MQ界面裏面看到消息中java對象的具體內容,加一個配置:

spring:
  application:
    name: order
  cloud:
    config:
    discovery:
      enabled: true
      service-id: CONFIG
    profile: test
  stream:
    bindings:
      # 隊列名稱
      myMessage:
        # 組名稱
        group: order
        # 設置消息內容類型(可查看java對象屬性)
        content-type: application/json

效果以下:

image

處理完消息以後,若是須要回應一下:

  1. 增長一個消息:
public interface StreamClient {
    String INPUT = "myMessage";   
    String INPUT2 = "myMessage2";  
    
    @Input(StreamClient.INPUT)
    SubscribableChannel input();
   
    @Output(StreamClient.INPUT)
    MessageChannel output();
   
    @Input(StreamClient.INPUT2)
    SubscribableChannel input2();
   
    @Output(StreamClient.INPUT2)
    MessageChannel output2();
}
  1. 用@SendTo註解返回信息:
@StreamListener(value = StreamClient.INPUT)
@SendTo(StreamClient.INPUT)
public String process(OrderDTO message){
    log.info("StreamReceiver: {}",message);
    return "received";
}

@StreamListener(value = StreamClient.INPU2T)
public String process2(OrderDTO message){
    log.info("StreamReceiver2: {}",message);
    return "received";
}

異步扣庫存分析

訂單生成的時候(此時訂單狀態爲等待中),向MQ發送信息,通知商品服務扣除庫存,商品服務不論成功仍是失敗,都要返回結果給訂單服務。訂單服務訂閱了商品服務,根據返回的信息,以爲這個訂單是否生成。
***

服務網關

建議使用Nginx和zuul混搭的方式,使用nginx對外暴露一個URL,nginx把請求轉發到多個zuul服務上,nginx繼續作負載均衡,這樣能夠作到nginx和zuul的取長補短。

常見的網關方案:

Nginx+Lua

Spring Cloud Zuul

Spring Cloud Zuul

路由+過濾器 = Zuul
Zuul核心是一系列的過濾器

四種標準過濾器類型:

  • 前置Pre
  • 路由Route
  • 後置Post
  • 錯誤Error

要實現路由轉發功能要加註解:@EnableZuulProxy

訪問路徑控制:(自定義、限制訪問)

zuul: 
  routes:
    # /myProduct/product/list -> /product/product/list
    # product:
    #   path: /myProduct/**
    #   serviceId: product
# 簡潔寫法
    product: /myProduct/**
# 排除某些路由
ignored-patterns:
  - /**/product/listForProduct

Cookie轉發(默認是不能傳cookie的):
默認限制了cookie、set-cookie、authorization的轉發:

ZuulProperties.java

該字段設置爲空便可放開:

zuul: 
  routes:
    # /myProduct/product/list -> /product/product/list
    product:
      path: /myProduct/**
      serviceId: product
      # 設置爲空便可
      sensitiveHeaders: 
# 簡潔寫法
    product: /myProduct/**
# 排除某些路由
ignored-patterns:
  - /**/product/listForProduct

Zuul過濾器:
自定義preFilter:

@Component
public class TokenFilter extends ZuulFilter {
    @Override
    public String filterType() {
        return PRE_TYPE;
    }

    @Override
    public int filterOrder() {
        return PRE_DECORATION_FILTER_ORDER - 1;
    }

    @Override
    public boolean shouldFilter() {
        return true;
    }

    @Override
    public Object run() {
        RequestContext requestContext = RequestContext.getCurrentContext();
        HttpServletRequest request = requestContext.getRequest();

        //這裏從url參數裏獲取, 也能夠從cookie, header裏獲取
        String token = request.getParameter("token");
        if (StringUtils.isEmpty(token)) {
            requestContext.setSendZuulResponse(false);
            requestContext.setResponseStatusCode(HttpStatus.UNAUTHORIZED.value());
        }
        return null;
    }
}

自定義postFilter:

@Component
public class AddResponseHeaderFilter extends ZuulFilter{
    @Override
    public String filterType() {
        return POST_TYPE;
    }

    @Override
    public int filterOrder() {
        return SEND_RESPONSE_FILTER_ORDER - 1;
    }

    @Override
    public boolean shouldFilter() {
        return true;
    }

    @Override
    public Object run() {
        RequestContext requestContext = RequestContext.getCurrentContext();
        HttpServletResponse response = requestContext.getResponse();
        response.setHeader("X-Foo", UUID.randomUUID().toString());
        return null;
    }
}

令牌桶限流(訪問攔截):

@Component
public class RateLimitFilter extends ZuulFilter{
    //Google開源工具包Guava提供了限流工具類RateLimiter,該類基於令牌桶算法實現流量限制,使用十分方便,並且十分高效。
    private static final RateLimiter RATE_LIMITER = RateLimiter.create(100);

    @Override
    public String filterType() {
        return PRE_TYPE;
    }

    @Override
    public int filterOrder() {
        return SERVLET_DETECTION_FILTER_ORDER - 1;
    }

    @Override
    public boolean shouldFilter() {
        return true;
    }

    @Override
    public Object run() {
        //若是沒有令牌,拋出異常
        if(!RATE_LIMITER.tryAcquire()){
            throw new RateLimitException();
        }
        return null;
    }
}

微信商城中買家端和賣家端登陸以後的區別:
買家端登陸以後往cookie裏寫了一個openid=」openid」;
賣家端登陸以後,在cookie中存了一個token="redisKey",這個redisKey通常是UUID,redis中存了一個redisKey="openid"

權限攔截:
例子:買家完成訂單

public class AuthSellerFilter extends ZuulFilter{
    
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    
    @Override
    public String filterType() {
        return PRE_TYPE;
    }

    @Override
    public int filterOrder() {
        return PRE_DETECTION_FILTER_ORDER - 1;
    }

    @Override
    public boolean shouldFilter() {
        RequestContext requestContext = RequestContext.getCurrentContext();
        HttpServletRequest request = requestContext.getRequest();
        
        if("/order/order/finish".equals(request.getRequest())){
            return true
        }
        return false;
    }

    @Override
    public Object run() {
        RequestContext requestContext = RequestContext.getCurrentContext();
        HttpServletRequest request = requestContext.getRequest();
        
        // /order/finish 只能賣家訪問(cookie裏面有token,而且對應redis中的值)
        
        Cookie cookie = CookieUtil.get(request,"token");
        if(cookie == null || StringUtils.isEmpty(cookie.getValue()) || StringUtils.isEmpty(stringRedisTemplate.opsForValue().get(String.format(RedisConstant.TOKEN_TEMPLATE, cookie.getValue())))){
            requestContext.setSendZuulResponse(false);
            requestContext.setResponseStatusCode(HttpStatus.UNAUTHORIZED.value());
        }
        return null;
    }
}

網關配置全部服務均可以傳遞cookie:

zuul:
  # 所有服務忽略敏感頭
  sensitive-headers:
  routes:

Zuul跨域:

@Configuration
public class CorsConfig {
    @Bean
    public CorsFilter corsFilter() {
        final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        final CorsConfiguration config = new CorsConfiguration();
        
        config.setAllowCredentials(true);
        config.setAllowedOrgins(Arrays.asList("*"));
        config.setAllowedHeaders(Arrays.asList("*"));
        config.setAllowedMethods(Arrays.asList("*"));
        config.setMaxAge(300l);
       
        source.registerCorsConfiguration("/**",config);
        return new CorsFilter(source);
    }
}

鏈路監控

Spring Cloud Sleuth

Spring Cloud Sleuth是Spring Cloud提供的分佈式系統服務鏈追蹤組件。
一個請求可能會通過多個服務纔會獲得結果,若是在這個過程當中出現了異常,就很難去定位問題。因此,必需要實現一個分佈式鏈路跟蹤的功能,直觀的展現出完整的調用過程。

  1. 引入依賴
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-sleuth</artifactId>
</dependency>
  1. 加入配置文件
sleuth:
    sampler:
      # 1表示100%,全部日誌都發送到外部程序展現,會消耗帶寬等資源,只能在開發中使用
      percentage: 1

Zipkin

官網有安裝方法:https://zipkin.io/pages/quickstart

重要概念:
TraceId: 全局跟蹤ID,是跟蹤的入口點。
SpanId: 下一層請求ID。一個traceId包含1個以上的spanId。
ParentId: 上一層請求跟蹤ID,用來將先後的請求串聯起來

  1. 引入依賴:
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-sleuth-zipkin</artifactId>
</dependency>

由於zipkin和sleuth須要一塊兒使用,因此都須要導入依賴:

<!--<dependency>-->
    <!--<groupId>org.springframework.cloud</groupId>-->
    <!--<artifactId>spring-cloud-starter-sleuth</artifactId>-->
<!--</dependency>-->

<!--<dependency>-->
    <!--<groupId>org.springframework.cloud</groupId>-->
    <!--<artifactId>spring-cloud-sleuth-zipkin</artifactId>-->
<!--</dependency>-->

<!-- 包含sleuth和zipkin -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-zipkin</artifactId>
</dependency>
  1. 修改配置文件:
zipkin:
    base-url: http://localhost:9411/
  sleuth:
    sampler:
      percentage: 1
相關文章
相關標籤/搜索