面試必會之SpringBoot&SpringCloud

韩续贤發表於2024-07-05

01- 講一講SpringBoot自動裝配的原理

1.在SpringBoot專案的啟動引導類上都有一個註解@SpringBootApplication

@SpringBootApplication
@MapperScan("com.hxx.admin.dao")
public class AdminApplication {
    public static void main(String[] args) {
            SpringApplication.run(AdminApplication.class, args);
    }
    
}

這個註解是一個複合註解, 其中有三個註解構成 , 分別是

@SpringBootConfiguration 
@EnableAutoConfiguration
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
        @Filter (type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {
  • @SpringBootConfiguration : 是@Configuration的派生註解 , 標註當前類是一個SpringBoot的配置類
  • @ComponentScan : 開啟元件掃描, 預設掃描的是當前啟動引導了所在包以及子包
  • @EnableAutoConfiguration : 開啟自動配置(自動配置核心註解)

2.在@EnableAutoConfiguration註解的內容使用@Import註解匯入了一個AutoConfigurationImportSelector.class的類

@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class) 
public @interface EnableAutoConfiguration {

AutoConfigurationImportSelector.class中的selectImports方法內透過一系列的方法呼叫, 最終需要載入類載入路徑下META-INF下面的spring.factories配置檔案

3.在META-INF/spring.factories配置檔案中, 定義了很多的自動配置類的完全限定路徑

# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,\
org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,\
org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration,\
org.springframework.boot.autoconfigure.batch.BatchAutoConfiguration,\ 
org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration,\
org.springframework.boot.autoconfigure.cassandra.CassandraAutoConfiguration,\

這些配置類都會被載入

4.載入配置類之後, 會配置類或者配置方法上的@ConditionalOnXxxx條件化註解是否滿足條件

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ RabbitTemplate.class, Channel.class })
@EnableConfigurationProperties(RabbitProperties.class)
@Comport(RabbitAnnotationDrivenConfiguration.class)
public class RabbitAutoConfiguration {
    @Configuration(proxyBeanMethods = false)
    @ConditionalOnMissingBean(ConnectionFactory.class)
    protected static class RabbitConnectionFactoryCreator {...}
    @Configuration(proxyBeanMethods = false)
    @Import(RabbitConnectionFactoryCreator.class)
    protected static class RabbitTemplateConfiguration {...}
    @Configuration(proxyBeanMethods = false)
    @ConditionalOnClass(RabbitMessagingTemplate.class)
    @ConditionalOnMissingBean(RabbitMessagingTemplate.class)
    @Import(RabbitTemplateConfiguration.class)
protected static class MessagingTemplateConfiguration {

如果滿足條件就會從屬性配置類中讀取相關配置 , 執行配置類中的配置方法 , 完成自動配置

02- 講一講SpringBoot啟動流程

springboot專案在啟動的時候, 首先會執行啟動引導類裡面的SpringApplication.run(AdminApplication.class, args)方法

@SpringBootApplication
@MapperScan("com.hxx.admin.dao")
public class AdminApplication {
    public static void main(String[] args) {
            SpringApplication.run(AdminApplication.class, args);
    }
    
}

這個run方法主要做的事情可以分為三個部分 :

第一部分進行SpringApplication的初始化模組,配置一些基本的環境變數、資源、構造器、監聽器

第二部分實現了應用具體的啟動方案,包括啟動流程的監聽模組、載入配置環境模組、及核心的建立上下文環境模組

第三部分是自動化配置模組,該模組作為springboot自動配置核心,在後面的分析中會詳細討論

03- 你們常用的SpringBoot起步依賴有哪些

04- springBoot支援的配置檔案有哪些 ? 載入順序是什麼樣的

1 properties檔案
2 YAML檔案
3 系統環境變數
4 命令列引數

如果有相同的配置引數, 後載入的會覆蓋先載入的

05- 執行一個SpringBoot專案有哪些方式

  1. 直接使用jar -jar 執行

  2. 開發過程中執行main方法

  3. 可以配置外掛 , 將springboot專案打war包, 部署到Tomcat中執行

  4. 直接用maven外掛執行 maven spring-boot:run

07-Spring Boot的核心註解是哪個?他由哪幾個註解組成的?

Spring Boot的核心註解是@SpringBootApplication , 他由幾個註解組成 :

  • @SpringBootConfiguration: 組合了- @Configuration註解,實現配置檔案的功能;
  • @EnableAutoConfiguration:開啟自動配置的功能,也可以關閉某個自動配置的選項
  • @ComponentScan:Spring元件掃描

08-Spring Boot 中如何解決跨域問題 ?

SpringMVC專案中使用@CrossOrigin註解來解決跨域問題 , 本質是CORS

@RequestMapping("/hello")
@CrossOrigin(origins = "*")
//@CrossOrigin(value = "http://localhost:8081") //指定具體ip允許跨域
public String hello() {
    return "hello world";
}

SpringBoot專案採用自動配置的方式來配置CORS , 可以透過實現 WebMvcConfigurer介面然後重寫addCorsMappings方法解決跨域問題。

@Configuration
public class CorsConfig implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                //是否傳送Cookie
                .allowCredentials(true)
                //放行哪些原始域
                .allowedOrigins("*")
                .allowedMethods(new String[]{"GET", "POST", "PUT", "DELETE"})
                .allowedHeaders("*")
                .exposedHeaders("*");
    }
}

在SpringCloud專案中一般都會有閘道器 , 在閘道器中可以配置CORS跨域, 這樣所有透過閘道器的請求都解決了跨域問題

spring:
  cloud:
    gateway:
      globalcors:
        cors-configurations:
          '[/**]': # 匹配所有請求
            allowedOrigins: "*" #跨域處理 允許所有的域
            allowedMethods: # 支援的方法
              - GET
              - POST
              - PUT
              - DELETE

09- 你們專案中使用的SpringBoot是哪個版本 ?

  • SpringBoot : 2.3.4.RELEASE

  • SpringCloud : Hoxton.SR10

  • SpringCloudAlibaba : 2.2.5.RELEASE

10- Spring Cloud 5大元件有哪些?

早期我們一般認為的Spring Cloud五大元件是

  • Eureka : 註冊中心
  • Ribbon : 負載均衡
  • Feign : 遠端呼叫
  • Hystrix : 服務熔斷
  • Zuul/Gateway : 閘道器

隨著SpringCloudAlibba在國內興起 , 我們專案中使用了一些阿里巴巴的元件

  • 註冊中心/配置中心 Nacos

  • 負載均衡 Ribbon

  • 服務呼叫 Feign

  • 服務保護 sentinel

  • 服務閘道器 Gateway

11- 什麼是微服務?微服務的優缺點是什麼?

微服務就是一個獨立的職責單一的服務應用程式,一個模組

1.優點:松耦合,聚焦單一業務功能,無關開發語言,團隊規模降低 , 擴充套件性好, 天然支援分庫
2.缺點:隨著服務數量增加,管理複雜,部署複雜,伺服器需要增多,服務通訊和呼叫壓力增大

12- 你們專案中微服務之間是如何通訊的?

1.同步通訊:透過Feign傳送http請求呼叫

2.非同步:訊息佇列,如RabbitMq、KafKa等

13- 服務註冊和發現是什麼意思?Spring Cloud 如何實現服務註冊發現?

各種註冊中心元件的原理和流程其實大體上類似

核心的功能就一下幾個 :

  1. 服務註冊 : 服務啟動的時候會將服務的資訊註冊到註冊中心, 比如: 服務名稱 , 服務的IP , 埠號等
  2. 服務發現 : 服務呼叫方呼叫服務的時候, 根據服務名稱從註冊中心拉取服務列表 , 然後根據負載均衡策略 , 選擇一個服務, 獲取服務的IP和埠號, 發起遠端呼叫
  3. 服務狀態監控 : 服務提供者會定時向註冊中心傳送心跳 , 註冊中心也會主動向服務提供者傳送心跳探測, 如果長時間沒有接收到心跳, 就將服務例項從註冊中心下線或者移除

使用的話, 首先需要部署註冊中心服務 , 然後在我們自己的微服務中引入註冊中心依賴, 然後再配置檔案中配置註冊中心地址 就可以了

spring:
  application:
    name: leadnews-admin
  cloud:
    nacos:
      # 註冊中心地址
      discovery:
        server-addr: 124.221.75.8:8848
      # 配置中心地址
      config:
        server-addr: 124.221.75.8:8848
        file-extension: yml

14- 你們專案中使用的註冊中心是什麼 ? 有沒有了解過原理 ?

我們專案中註冊中心用的是Nacos , 基本上所有的註冊中心的核心功能都包括服務註冊 , 服務發現, 服務狀態監控 , 他的核心原理如下 :

  1. 客戶端啟動時會將當前服務的資訊包含ip、埠號、服務名、分組名、叢集名等資訊封裝為一個Instance物件,準備向Nacos伺服器註冊服務,在註冊服務之前,會根據Instance中的資訊建立一個BeatInfo物件,然後建立一個定時任務,每隔一段時間向Nacos伺服器傳送PUT請求並攜帶相關資訊,作為定時心跳連線,伺服器端在接收到心跳請求後,會去檢查當前服務列表中有沒有該例項,如果沒有的話將當前服務例項重新註冊,註冊完成後立即開啟一個非同步任務,更新客戶端例項的最後心跳時間,如果當前例項是非健康狀態則將其改為健康狀態
  2. 心跳定時任務建立完成後,透過POST請求將當前服務例項資訊註冊進Nacos伺服器,伺服器端在接收到註冊例項請求後,會將請求攜帶的資料封裝為一個Instance物件,然後為這個服務例項建立一個服務Service,一個Service下可能有多個服務例項,服務在Nacos儲存到一個ConcurrentHashMap中,格式為名稱空間為key,value為map,分組名和服務名為內層map的key,value為服務資料,Map(namespace,Map(group::serviceName, Service))
  3. 服務建立完成之後,開啟一個定時任務(5s執行一次),檢查當前服務中的各個例項是否線上,如果例項上次心跳時間大於15s就將其狀態設定為不健康,如果超出30s,則直接將該例項刪除;
  4. 然後將當前例項新增到對應服務列表中,這裡會透過synchronized鎖住當前服務,然後分兩種情況向叢集中新增例項,如果是持久化資料,則使用CP模型,透過leader節點將例項資料更新到記憶體和磁碟檔案中,然後同步寫入到其他節點 , 必須叢集半數以上節點寫入成功才會給客戶端返回成功;
  5. 如果是非持久話例項資料,使用的是AP模型,首先向任務阻塞佇列新增一個本地服務例項改變任務,去更新本地服務列表,然後在遍歷叢集中所有節點,分別建立資料同步任務放進阻塞佇列非同步進行叢集資料同步,不保證叢集節點資料同步完成即可返回;
  6. 在將服務例項更新到服務登錄檔中時,為了防止併發讀寫衝突,採用的是寫時複製的思想,將原登錄檔資料複製一份,新增完成之後再替換回真正的登錄檔,更新完成之後,透過釋出服務變化事件,將服務變動通知給客戶端,採用的是UDP通訊,客戶端接收到UDP訊息後會返回一個ACK訊號,如果一定時間內服務端沒有收到ACK訊號,還會嘗試重發,當超出重發時間後就不在重發,雖然透過UDP通訊不能保證訊息的可靠抵達,但是由於Nacos客戶端會開啟定時任務,每隔一段時間更新客戶端快取的服務列表,透過定時輪詢更新服務列表做兜底,所以不用擔心資料不會更新的情況,這樣既保證了實時性,又保證了資料更新的可靠性;
  7. 服務發現:客戶端透過定時任務定時從服務端拉取服務資料儲存在本地快取,服務端在發生心跳檢測、服務列表變更或者健康狀態改變時會觸發推送事件,在推送事件中會基於UDP通訊將服務列表推送到客戶端,同時開啟定時任務,每隔10s定時推送資料到客戶端

15- 你們專案負載均衡如何實現的 ?

服務呼叫過程中的負載均衡一般使用SpringCloud的Ribbon 元件實現 , Feign的底層已經自動整合了Ribbon , 使用起來非常簡單

客戶端呼叫的話一般會透過閘道器, 透過閘道器實現請求的路由和負載均衡

spring:
  cloud:
    gateway:
      routes:
        # 平臺管理
        - id: wemedia
          uri: lb://leadnews-wemedia
          predicates:
            - Path=/wemedia/**
          filters:
            - StripPrefix= 1

RIbbon負載均衡原理 :

SpringCloudRibbon的底層採用了一個攔截器,攔截了RestTemplate發出的請求,對地址做了修改。

基本流程如下:

  • 攔截我們的RestTemplate請求
  • RibbonLoadBalancerClient會從請求url中獲取服務名稱
  • DynamicServerListLoadBalancer根據服務名稱到註冊中心拉取服務列表
  • 註冊中心返回列表
  • IRule利用內建負載均衡規則,從列表中選擇一個服務例項
  • RibbonLoadBalancerClient用服務例項的IP和埠替換請求路徑中的服務名稱
  • 向服務例項發起http請求

16- Ribbon負載均衡策略有哪些 ? 如果想自定義負載均衡策略如何實現 ?

Ribbon預設的負載均衡策略有七種 :

**內建負載均衡規則類 ** 規則描述
RoundRobinRule 簡單輪詢服務列表來選擇伺服器。它是Ribbon預設的負載均衡規則。
AvailabilityFilteringRule 對以下兩種伺服器進行忽略: (1)在預設情況下,這臺伺服器如果3次連線失敗,這臺伺服器就會被設定為“短路”狀態。短路狀態將持續30秒,如果再次連線失敗,短路的持續時間就會幾何級地增加。 (2)併發數過高的伺服器。如果一個伺服器的併發連線數過高,配置了AvailabilityFilteringRule規則的客戶端也會將其忽略。併發連線數的上限,可以由客戶端的..ActiveConnectionsLimit屬性進行配置。
WeightedResponseTimeRule 為每一個伺服器賦予一個權重值。伺服器響應時間越長,這個伺服器的權重就越小。這個規則會隨機選擇伺服器,這個權重值會影響伺服器的選擇。
ZoneAvoidanceRule 以區域可用的伺服器為基礎進行伺服器的選擇。使用Zone對伺服器進行分類,這個Zone可以理解為一個機房、一個機架等。而後再對Zone內的多個服務做輪詢。
BestAvailableRule 忽略那些短路的伺服器,並選擇併發數較低的伺服器。
RandomRule 隨機選擇一個可用的伺服器。
RetryRule 重試機制的選擇邏輯

預設的實現就是ZoneAvoidanceRule,是一種輪詢方案

如果想要自定義負載均衡 , 可以自己建立類實現IRule介面 , 然後再透過配置類或者配置檔案配置即可 :

透過定義IRule實現可以修改負載均衡規則,有兩種方式:

  1. 程式碼方式:在order-service中的OrderApplication類中,定義一個新的IRule:
@Bean
public IRule randomRule(){
    return new RandomRule();
}
  1. 配置檔案方式:在order-service的application.yml檔案中,新增新的配置也可以修改規則:
userservice: # 給某個微服務配置負載均衡規則,這裡是userservice服務
  ribbon:
    NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule # 負載均衡規則 

17- 你們專案的配置檔案是怎麼管理的 ?

大部分的固定的配置檔案都放在服務本地 , 一些根據環境不同可能會變化的部分, 放到Nacos中

18- 你們專案中有沒有做過限流 ? 怎麼做的 ?

限流一般有二種方式設定 :

第一種 : 閘道器配置限流

spring:
  application:
    name: api-gateway
  redis:
    host: localhost
    port: 6379
    password:
  cloud:
    gateway:
      routes:
        - id: cloud-gateway
          uri: http://192.168.1.211:8088/
          predicates:
            - Path=/ytb/**
          filters:
            - StripPrefix=1
            - name: RequestRateLimiter
              args:
                redis-rate-limiter.replenishRate: 1   # 令牌桶每秒填充速率
                redis-rate-limiter.burstCapacity: 2   # 令牌桶總容量
                key-resolver: "#{@pathKeyResolver}"   # 使用 SpEL 表示式按名稱引用 bean

在上面的配置檔案,配置了 redis 的資訊,並配置了 RequestRateLimiter 的限流過濾器,該過濾器需要配置三個引數:

burstCapacity,令牌桶總容量。
replenishRate,令牌桶每秒填充平均速率。
key-resolver,用於限流的鍵的解析器的 Bean 物件的名字。它使用 SpEL 表示式根據 #{@beanName} 從 Spring 容器中獲取 Bean 物件

@Configuration
public class KeyResolverConfiguration {
 @Bean
 public KeyResolver pathKeyResolver(){
     return exchange -> Mono.just(exchange.getRequest().getURI().getPath());
 }
}

常見的限流演算法有:計數器演算法,漏桶(Leaky Bucket)演算法,令牌桶(Token Bucket)演算法。

Spring Cloud Gateway官方提供了RequestRateLimiterGatewayFilterFactory過濾器工廠,使用Redis 和Lua指令碼實現了 令牌桶 的方式。

令牌桶演算法 是對漏桶演算法的一種改進,漏桶演算法能夠限制請求呼叫的速率,而令牌桶演算法能夠在限制呼叫的平均速率的同時還允許一定程度的突發呼叫。在令牌桶演算法中,存在一個桶,用來存放固定數量的令牌。演算法中存在一種機制,以一定的速率往桶中放令牌。每次請求呼叫需要先獲取令牌,只有拿到令牌,才有機會繼續執行,否則選擇選擇等待可用的令牌、或者直接拒絕。

放令牌這個動作是持續不斷的進行,如果桶中令牌數達到上限,就丟棄令牌。所以就存在這種情況,桶中一直有大量的可用令牌,這時進來的請求就可以直接拿到令牌執行,比如設定qps為100,那麼限流器初始化完成一秒後,桶中就已經有100個令牌了,這時服務還沒完全啟動好,等啟動完成對外提供服務時,該限流器可以抵擋瞬時的100個請求。所以,只有桶中沒有令牌時,請求才會進行等待,最後相當於以一定的速率執行。

第二種 : 使用服務保護元件Sentinel實現限流

建議回去看看微服務保護課程中的限流配置

19- 斷路器/熔斷器用過嘛 ? 斷路器的狀態有哪些

我們專案中使用Hystrix/Sentinel實現的斷路器 , 斷路器狀態機包括三個狀態:

  • closed:關閉狀態,斷路器放行所有請求,並開始統計異常比例、慢請求比例。超過閾值則切換到open狀態
  • open:開啟狀態,服務呼叫被熔斷,訪問被熔斷服務的請求會被拒絕,快速失敗,直接走降級邏輯。Open狀態5秒後會進入half-open狀態
  • half-open:半開狀態,放行一次請求,根據執行結果來判斷接下來的操作。
    • 請求成功:則切換到closed狀態
    • 請求失敗:則切換到open狀態

20- 你們專案中有做過服務降級嘛 ?

我們專案中涉及到服務呼叫得地方都會定義降級, 一般降級邏輯就是返回預設值 , 降級的實現也非常簡單 , 就是建立一個類實現FallbackFactory介面 , 然後再對應的Feign客戶端介面上面 , 透過@FeignClient指定降級類

@Component
@Slf4j
public class OrderServiceFallbackFactory implements FallbackFactory<OrderService> {
    @Override
    public OrderService create(Throwable throwable) {
        log.error("呼叫訂單服務失敗",throwable);

        return new OrderService() {
            @Override
            public String weixinPay(PayVO payVO) {
                return null;
            }

            @Override
            public Pager<OrderVO> search(Integer pageIndex, Integer pageSize, String orderNo, String openId, String startDate, String endDate) {
                return new Pager<>();
            }

            @Override
            public List<Long> getBusinessTop10Skus(Integer businessId) {
                return Lists.newArrayList();
            }
        };
    }
}

21- 你們專案中異常是怎麼控制的 ?

我們會根據不同的異常情況定義異常類 , 實現RuntimeException介面 , 然後在需要進行異常處理的位置對外丟擲對應異常

在專案中使用@ControllerAdvice + @ExceptionHandler 捕獲指定異常 , 處理異常

@RestControllerAdvice
@SLf4j
public class GlobalExceptionHandler {
    @ResponseBody
    @ExceptionHandler(value = LogicException.class)
    public ResponseEntity<String> exceptionHandler(LogicException e) {
        log.error("controller call error", e);
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(e.getMessage());
    }

    @ResponseBody
    @ExceptionHandler(value = DuplicateKeyException.class)
    public ResponseEntity<String> exceptionHandler(DuplicateKeyException e){
        log.error("controller call error", e);
        return ResponseEntity.status(Httpstatus.INTERNAL_SERVER_ERROR).body("已存在此名稱的物件");
    }
}

22- SpringBoot專案讀取配置檔案的方式有哪些 ? 如何實現配置的熱更新 ?

SpringBoot專案讀取配置檔案常用的方式有二種 :

  1. 透過@Value註解透過屬性名稱讀取

  2. 透過@ConfigurationProperties屬性 , 批次讀取配置檔案配置到屬性配置類

實現熱更新的方式也有二種 : 首先需要將配置檔案配置到配置中心中 , 之後透過

  1. @Value + @RefreshScope註解實現熱更新
  2. 透過@ConfigurationProperties註解讀取的屬性, 自動會熱更新

相關文章