在我以前的文章 springcloud如何實現服務的平滑發佈 裏介紹了基於pause的發佈方案。html
平滑發佈的核心思想就是:全部服務的調用者再也不調用該服務了就表示安全的將服務kill掉。java
另外actuator提供了優雅停機方式的endpoint:shutdown,那咱們就能夠結合 pause + 等待服務感知下線 + shutdown到一個endpoint裏來提供優雅的停機發布方案。web
以前的方案有一個不完美的地方,那就是IP白名單的filter是要在應用的application里加spring
@ServletComponentScan
註解,這樣對應用程序的將是不透明的(有侵入性)。json
故有了下面這我認爲是相對完美的方案:緩存
先建一個common模塊,其餘項目引用該模塊。安全
在src/main/resources下新建 META-INF文件夾,而後新建:spring.factories文件app
# Auto Configure org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ com.longge.config.PublishEndpointAutoConfiguration
PublishEndpointAutoConfiguration.java
package com.longge.config; import javax.servlet.Filter; import org.springframework.boot.actuate.autoconfigure.EndpointAutoConfiguration; import org.springframework.boot.actuate.endpoint.Endpoint; import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import com.longge.endpoint.PublishEndpoint; import com.longge.filter.PublishFilter; import com.longge.filter.PublishProperties; @Configuration @ConditionalOnClass(Endpoint.class) @AutoConfigureAfter(EndpointAutoConfiguration.class) public class PublishEndpointAutoConfiguration { @Bean public PublishEndpoint publishEndpoint() { return new PublishEndpoint(); } @Bean @ConditionalOnProperty("publish.ip-white-list") public PublishProperties publishProperties() { return new PublishProperties(); } @Bean @ConditionalOnProperty("publish.ip-white-list") @ConditionalOnClass(Filter.class) public FilterRegistrationBean testFilterRegistration() { FilterRegistrationBean registration = new FilterRegistrationBean(); registration.setFilter(new PublishFilter(publishProperties())); registration.addUrlPatterns("/publish"); registration.setName("publishFilter"); registration.setOrder(1); return registration; } }
PublishEndpoint.javaide
package com.longge.endpoint; import java.util.Collections; import java.util.Map; import org.springframework.boot.actuate.endpoint.AbstractEndpoint; import org.springframework.boot.context.event.ApplicationPreparedEvent; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.ApplicationListener; import org.springframework.context.ConfigurableApplicationContext; import lombok.Getter; import lombok.Setter; @ConfigurationProperties(prefix = "endpoints.publish") public class PublishEndpoint extends AbstractEndpoint<Map<String, Object>> implements ApplicationListener<ApplicationPreparedEvent> { private static final Map<String, Object> NO_CONTEXT_MESSAGE = Collections .unmodifiableMap(Collections.<String, String>singletonMap("message", "No context to publish.")); /** * 等待多時秒 */ @Getter @Setter private Integer waitSeconds; public PublishEndpoint() { super("publish", true, false); } private ConfigurableApplicationContext context; @Override public Map<String, Object> invoke() { if (this.context == null) { return NO_CONTEXT_MESSAGE; } try { if(null == waitSeconds) { waitSeconds = 10; } Map<String, Object> shutdownMessage = Collections .unmodifiableMap(Collections.<String, Object>singletonMap("message", "Service will exit after "+waitSeconds+" seconds")); return shutdownMessage; } finally { Thread thread = new Thread(new Runnable() { @Override public void run() { try { PublishEndpoint.this.context.stop(); Thread.sleep(waitSeconds * 1000); } catch (InterruptedException ex) { Thread.currentThread().interrupt(); } PublishEndpoint.this.context.close(); } }); thread.setContextClassLoader(getClass().getClassLoader()); thread.start(); } } @Override public void onApplicationEvent(ApplicationPreparedEvent input) { if (this.context == null) { this.context = input.getApplicationContext(); } } }
PublishFilter.javapost
package com.longge.filter; import java.io.IOException; import java.io.PrintWriter; import java.util.Arrays; import java.util.List; import javax.servlet.Filter; import javax.servlet.FilterChain; import javax.servlet.FilterConfig; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import lombok.extern.slf4j.Slf4j; /** * shutdown和pause的管理端點的ip白名單過濾 * @author yangzhilong * */ @Slf4j public class PublishFilter implements Filter { private static final String UNKNOWN = "unknown"; /** * 本機ip */ private List<String> localIp = Arrays.asList("0:0:0:0:0:0:0:1","127.0.0.1"); private PublishProperties properties; @Override public void destroy() { } public PublishFilter() {} public PublishFilter(PublishProperties properties) { super(); this.properties = properties; } @Override public void doFilter(ServletRequest srequest, ServletResponse sresponse, FilterChain filterChain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) srequest; String ip = this.getIpAddress(request); log.info("訪問publish的機器的原始IP:{}", ip); if (!isMatchWhiteList(ip)) { sresponse.setContentType("application/json"); sresponse.setCharacterEncoding("UTF-8"); PrintWriter writer = sresponse.getWriter(); writer.write("{\"code\":401}"); writer.flush(); writer.close(); return; } filterChain.doFilter(srequest, sresponse); } @Override public void init(FilterConfig arg0) throws ServletException { log.info("shutdown filter is init....."); } /** * 匹配是不是白名單 * @param ip * @return */ private boolean isMatchWhiteList(String ip) { if(localIp.contains(ip)) { return true; } if(null == properties.getIpWhiteList() || 0 == properties.getIpWhiteList().length) { return false; } List<String> list = Arrays.asList(properties.getIpWhiteList()); return list.stream().anyMatch(item -> ip.startsWith(item)); } /** * 獲取用戶真實IP地址,不使用request.getRemoteAddr();的緣由是有可能用戶使用了代理軟件方式避免真實IP地址, * 但是,若是經過了多級反向代理的話,X-Forwarded-For的值並不止一個,而是一串IP值,究竟哪一個纔是真正的用戶端的真實IP呢? * 答案是取X-Forwarded-For中第一個非unknown的有效IP字符串。 * * 如:X-Forwarded-For:192.168.1.110, 192.168.1.120, 192.168.1.130, 192.168.1.100 * * 用戶真實IP爲: 192.168.1.110 * * @param request * @return */ private String getIpAddress(HttpServletRequest request) { String ip = request.getHeader("x-forwarded-for"); if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) { ip = request.getHeader("Proxy-Client-IP"); } if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) { ip = request.getHeader("WL-Proxy-Client-IP"); } if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) { ip = request.getHeader("HTTP_CLIENT_IP"); } if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) { ip = request.getHeader("HTTP_X_FORWARDED_FOR"); } if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) { ip = request.getRemoteAddr(); } return ip; } }
PublishProperties.java
package com.longge.filter; import org.springframework.boot.context.properties.ConfigurationProperties; import lombok.Getter; import lombok.Setter; /** * publish白名單配置 * @author yangzl * @data 2019年4月26日 * */ @Getter @Setter @ConfigurationProperties(prefix="publish") public class PublishProperties { /** * 白名單 */ private String[] ipWhiteList; }
properties裏配置以下:
management.security.enabled = false # 開啓自定義的publish端點 endpoints.publish.enabled = true # 警用密碼校驗 endpoints.publish.sensitive = false # 服務暫停時間 endpoints.publish.waitSeconds = 7 # 發佈endpoint白名單 publish.ip-white-list=172.17.,172.16. # 2秒拉取最新的註冊信息 eureka.client.registry-fetch-interval-seconds=2 # 2秒刷新ribbon中的緩存信息 ribbon.ServerListRefreshInterval=2000
而後GET訪問 http://IP:端口/publish,服務將先成註冊中心下線,而後等待waitSeconds秒,而後再shutdown