在这篇文章 spring 说明了自己在 Spring MVC 中所提供的 CORS 的支持。文中提到了三种 Spring 支持 CORS 的方式:
对于单个方法的跨域支持,可以使用 @CrossOrigin
的注解实现,这种情况我基本没有用到过,也从来没有使用过
采用 JavaConfig,尤其是在使用 Spring Boot 的时候,可以采用如下方式:
@Configuration
public class MyConfiguration {
@Bean
public WebMvcConfigurer corsConfigurer() {
return new WebMvcConfigurerAdapter() {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**");
}
};
}
}
采用 Filter
@Configuration
public class MyConfiguration {
@Bean
public FilterRegistrationBean corsFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true);
config.addAllowedOrigin("http://domain1.com");
config.addAllowedHeader("*");
config.addAllowedMethod("*");
source.registerCorsConfiguration("/**", config);
FilterRegistrationBean bean = new FilterRegistrationBean(new CorsFilter(source));
bean.setOrder(0);
return bean;
}
}
看起来还是挺简单的?但是这里有个坑,在第二种方法后面的叙述中会强调:
If you are using Spring Security, make sure to enable CORS at Spring Security level as well to allow it to leverage the configuration defined at Spring MVC level.
Spring Security 本身是通过 Filter 实现的,如果没有对其单独做 CORS 的处理,在 Web Security 报错 401 的时候是不会返回相应的 CORS 的字段的。这会导致命名应该出现的 401 错误成为了一个无法进行跨域的错误,导致前端程序无法正常的处理 401 相应。21
为了解决这个问题,自然是需要添加 Spring Security 支持的 CORS 代码:
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
// by default uses a Bean by the name of corsConfigurationSource
.cors().and()
...
}
@Bean
CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Arrays.asList("https://example.com"));
configuration.setAllowedMethods(Arrays.asList("GET","POST"));
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
}
当然这种边角旮旯的东西确实引起了不少的问题,甚至在 spring boot 上还有人报了 issue,并且很多人还讨论了半天...可见隐藏之深。
这里提供一个处理 CORS 的 GitHub 项目,这也是我发现这个问题的出处。
很多 POST 请求会返回 status code 201,并且包含一个 Response Header Location
指向新创建的资源的地址。在 CORS 请求时,如果没有设置 Access-Control-Expose-Headers 会导致 Ajax 请求无法获取 Location
这样的 Response Header,详细信息可以在 developer.mozilla.org 看到:
在跨域访问时,XMLHttpRequest对象的getResponseHeader()方法只能拿到一些最基本的响应头,Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma,如果要访问其他头,则需要服务器设置本响应头。Access-Control-Expose-Headers 头让服务器把允许浏览器访问的头放入白名单。
因此,为了使得 Location
字段可以被访问,需要进行额外的设置:
@Bean
CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Arrays.asList("https://example.com"));
configuration.setAllowedMethods(Arrays.asList("GET","POST"));
configuration.addExposedHeader("Location"); // 暴露 Location header
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
HackerEarch 是我最近发觉的一个不错的算法练习的平台。这里首先对这个平台做一些介绍,包括它与其他平台相比的优缺点。然后介绍一个简单的图算法题的解法。
虽然自己的工作和复杂的算法关系不是很大,但是始终觉得这种对基础的熟练掌握和了解是做各种计算机行业相关工作的基础,然后自己也是总觉得自己在这方面有着一些兴趣,所有会经常找一些练习算法的平台刷刷题。之前有使用过 TopCoder、HackerRank、LeetCode 等。和 HackerRank 类似,HackerEarth 也是出自印度小哥之手。
虽然它本身有很多的问题,比如测试的执行不够稳定,比如很多题目的测试用例很少,比如 web page 存在各种展示的 bug等。但是 HackerEarth 有一个功能让我眼前一亮:它对各种题目进行了清晰的分门别类,让我这种练习者可以更针对性的进行训练。
另外一个对练习者友好的功能是它可以随意的查看一个 test cases 的输入和输出。如果我的一个提交出错了可以直接点击链接就能看到具体的输入和输出,虽然 HackerRank 也可以查看具体某一个 TestCase 但是需要自己通过在其网站上获取的点券购买,相比这里直接点击还是稍稍麻烦了一点。下面则是介绍一个我在这里写的一道图算法练习题。
题目在这里 题目大概翻译一下如下:
你有一个包含 N 个节点的树 T,其中根为节点 1,还有一个长度为 M 的无重复整数的数组 A。
需要你去找到所有符合这样条件的三元组 (U, V, K),其中 V 属于 U 节点的子树,然后他们之间的距离恰好是 A[K]。而两个节点之间的距离就是他们之间的最短路径,即边的个数。
输入:第一行包含空格分割的两个整数 N 和 M 第二行包含 M 个空格分离的整数 第三行包含 N - 1 个空格分隔的整数,其中第 i 个整数是 i + 1 节点的父节点(注意,前者是第 i 个整数,而后者是指 节点 i + 1)
输出:T 中产生的符合上述描述的三元组的个数
首先可是知道这是一个以 DFS (深度遍历)为中心的算法:每次深度遍历增加一个节点之后将其所有的祖先节点记录下来,并且检查数组 A 是否存在对应的距离。如果存在则计数 +1。于是有了第一个版本的提交:
import java.util.*;
class TestClass {
private static int cnt = 0;
public static void main(String args[] ) throws Exception {
Scanner scan = new Scanner(System.in);
int n = scan.nextInt();
int m = scan.nextInt();
// 用 HashSet 存储数组 A
Set<Integer> lens = new HashSet<Integer>();
for (int i = 0; i < m; i++) {
lens.add(scan.nextInt());
}
// 用邻接表建立图
LinkedList<Integer>[] g = new LinkedList[n];
for (int i = 0; i < n; i++) {
g[i] = new LinkedList<Integer>();
}
for (int i = 0; i < n - 1; i++) {
int node = scan.nextInt() - 1;
g[node].add(i + 1);
}
int[] dist = new int[n];
Set<Integer> ancestors = new HashSet<>();
ancestors.add(0);
dfs(g, 0, dist, ancestors, lens);
System.out.println(cnt);
}
public static void dfs(LinkedList<Integer>[] g,
int u,
int[] dist,
Set<Integer> ancestors,
Set<Integer> lens) {
for (int v : g[u]) {
dist[v] = dist[u] + 1;
// 每遍历到一个新的节点就去检查其与所有祖先的距离
for (int a : ancestors) {
// 计算与祖先的距离
int distance = dist[v] - dist[a];
// 如果这个距离在 A 中存在就计数 +1
if (lens.contains(distance)) {
cnt++;
}
}
// 在下一次遍历之前添加已经处理完的节点
ancestors.add(v);
dfs(g, v, dist, ancestors, lens);
// 在处理兄弟节点之前将当前节点从祖先列表前删除
ancestors.remove(v);
}
}
}
然后只有三分之一左右的用例过了...其中一大部分超时,还有一部分报错。虽然我觉得这个算法的效率可能有点问题,但是我没想到居然有些是报错了。点开其中的一个报错的 Case 一看,发现自己没有处理 A 中包含 0 的情况。
知道错在哪里就好修改了。距离为零意味着三元组 [U, V, K] 中 U 和 V 应该一样。那么就是 N 的个数。所以我就做一个特殊检查:如果 A.contains(0)
那么 cnt += n
:
import java.util.*;
class TestClass {
private static int cnt = 0;
public static void main(String args[] ) throws Exception {
Scanner scan = new Scanner(System.in);
int n = scan.nextInt();
int m = scan.nextInt();
// 用 HashSet 存储数组 A
Set<Integer> lens = new HashSet<Integer>();
for (int i = 0; i < m; i++) {
lens.add(scan.nextInt());
}
// 如果 A 中包含 0 计数 +n
if (lens.contains(0)) {
cnt += n;
}
// 用邻接表建立图
LinkedList<Integer>[] g = new LinkedList[n];
for (int i = 0; i < n; i++) {
g[i] = new LinkedList<Integer>();
}
for (int i = 0; i < n - 1; i++) {
int node = scan.nextInt() - 1;
g[node].add(i + 1);
}
int[] dist = new int[n];
Set<Integer> ancestors = new HashSet<>();
ancestors.add(0);
dfs(g, 0, dist, ancestors, lens);
System.out.println(cnt);
}
// 和尝试 1 一样,就省略了
}
这次所有通过的就是真的通过了,但是还是有大量的超时和系统错误。大概分析一下,超时肯定是说算法的复杂度太高了,可以看到默认的 DFS 的复杂度是 O(V + E)
对于树来说,就是 O(V + V - 1)
而已。但是每次处理新的节点时都有一个过程:
// 每遍历到一个新的节点就去检查其与所有祖先的距离
for (int a : ancestors) {
// 计算与祖先的距离
int distance = dist[v] - dist[a];
// 如果这个距离在 A 中存在就计数 +1
if (lens.contains(distance)) {
cnt++;
}
}
是一个 O(V)
的复杂度,所以整体的复杂度有 O(V^2)
。
而报错则很有可能是栈溢出了,那么需要将目前的递归变成遍历。
O(v)
提升到 O(log(V))
计算所有祖先中存在的距离与 A 数组中的距离耗费的时间是 O(v)
,但是仔细想想,对于一个没有权重的树来说,其实每一个路径中,如果当前节点 v 有 k 个祖先,那么出现的距离一定是 0 到 k 了。我们不用每次都去检查有哪些祖先,其祖先的个数就是 d[v]
而已,那么上面的那部分可以改成如下的样子:
Set<Integer> set = range(dist[v]);
set.retainAll(lens);
cnt += set.size();
其中 range 为:
public static Set<Integer> range(int n) {
Set<Integer> s = new HashSet<>();
for (int i = 1; i <= n; i++) {
s.add(i);
}
return s;
}
但是很遗憾,这样的复杂度并没有减少,retainAll
和 range
依然需要 O(V)
的复杂度。不过我觉得重点就是如何将目前这个 O(V)
做提升了。这个时候我想到了 binary search...我们当前的问题是在 range(dist[v])
和数组 A
之间找并集,那么如果将 A 排序,我们找的就是在 A
中最大的那个 小于等于 dist[v]
的索引,这就是可以用 binary search 解决的问题了。于是,最终的提交如下:
import java.util.*;
class TestClass {
public static void main(String args[] ) throws Exception {
Scanner scan = new Scanner(System.in);
int n = scan.nextInt();
int m = scan.nextInt();
int[] lens = new int[m];
for (int i = 0; i < m; i++) {
lens[i] = scan.nextInt();
}
Arrays.sort(lens);
LinkedList<Integer>[] g = new LinkedList[n];
for (int i = 0; i < n; i++) {
g[i] = new LinkedList<Integer>();
}
for (int i = 0; i < n - 1; i++) {
int node = scan.nextInt() - 1;
g[node].add(i + 1);
}
long cnt = 0L;
int[] dist = new int[n];
// 同时用遍历替代递归,引入一个 stack 处理 DFS
LinkedList<Integer> stack = new LinkedList<>();
stack.add(0);
int idx = bsearch(dist[0], lens);
cnt += idx + 1;
while (!stack.isEmpty()) {
int u = stack.pop();
for (int v : g[u]) {
dist[v] = dist[u] + 1;
idx = bsearch(dist[v], lens);
// 索引 + 1 就是并集的个数,如果没找到 -1 + 1 就是 0
cnt += idx + 1;
stack.add(v);
}
}
System.out.println(cnt);
}
// 找到最大的那个小于等 value 的索引
public static int bsearch(int value, int[] lens) {
int low = 0;
int high = lens.length - 1;
while (low < high) {
int mid = (low + high + 1) / 2;
if (lens[mid] <= value) {
low = mid;
} else {
high = mid - 1;
}
}
if (lens[low] > value) {
return -1;
}
return low;
}
}
这样修改之后就有了 O(Vlog(V))
的复杂度,成功的 AC 了。
当然,其实中间还是有很多的坑,比如之前采用的计数是 int
现在改成了 long
,再比如搜索索引之后就不再需要考虑有没有 0 距离的特殊情况了。
虽然看起来是一个不算难的 DFS 算法题,HackerEarth 也把它标记成简单,但是依然花了一番心思才优化成这个最终的版本。
不论是否采用微服务的架构,我们都有将自己的服务与其他的服务集成的需求。比如我这里有一个需求就是在系统中创建一个项目的时候通过其所提供的 GitHub 项目的地址获取其默认的 README.md
文件内容作为项目的描述。再比如现在很多的项目都将其用户管理系统作为一个独立的系统,当我自己的系统需要用户认证的时候需要从用户系统特定的接口获取用户信息。这篇文章就介绍如何使用 Feign,Hystrix 这些 spring cloud 所使用的依赖与其他服务做集成,当然,为了更好的保证服务的可靠性,我这里还展示了通过 wiremock 建立了一系列测试保证我们可以覆盖各种特殊的情况。
项目代码在 GitHub。
Feign 是一个声明式的 Http 客户端。其优势自然是它的"声明式":可以更清晰更简单的对其他服务进行请求。虽然目前 Feign 类库已经放在了 OpenFeign 这个 Github 账号之下,但是鉴于更多的人是将其作为 spring cloud 的一环一起使用的,所以这里我所使用的代码也都是引入的 spring cloud 的 org.springframework.cloud:spring-cloud-starter-feign
而不是其 io.github.openfeign:feign-core
的独立依赖。
build.gradle
:
...
dependencies {
compile('org.springframework.cloud:spring-cloud-starter-feign')
compile('org.springframework.boot:spring-boot-starter-web')
compileOnly('org.projectlombok:lombok')
testCompile('org.springframework.boot:spring-boot-starter-test')
}
...
然后,创建我们的 GitHubService:
@FeignClient(value = "github", url = "${github.url}")
public interface GitHubService {
@GetMapping("/{git}/master/{filename}")
String fetchRawFile(
@PathVariable("git") String git,
@PathVariable("filename") String filename);
}
然后在 @SpringBootApplication
注解的类添加注解 @EnableFeignClients
:
@SpringBootApplication
@EnableFeignClients
public class DemoForFeignAndHystrixApplication {
public static void main(String[] args) {
SpringApplication.run(DemoForFeignAndHystrixApplication.class, args);
}
}
Feign 本身是可以支持很多套语法的,在 spring cloud 下默认使用的 SpringMVC 的方式。
在有了 GitHubService
之后,如果想要对其进行测试就需要一种 mock 服务端请求的方式。这里我们采用 WireMock 来实现。
首先在 build.gradle
引入依赖:
...
dependencies {
compile('org.springframework.cloud:spring-cloud-starter-feign')
compile('org.springframework.boot:spring-boot-starter-web')
compileOnly('org.projectlombok:lombok')
testCompile('org.springframework.boot:spring-boot-starter-test')
+ testCompile('com.github.tomakehurst:wiremock-standalone:2.7.1')
}
...
然后在 resources 目录下定义 application-test.yml 文件,定义 github.url
在测试环境下的地址:
github:
url: http://localhost:10087
最后我们编写相应的测试:
@ActiveProfiles("test")
@RunWith(SpringRunner.class)
@SpringBootTest // 1
public class GitHubServiceTest {
@Rule
public WireMockRule wireMockRule = new WireMockRule(wireMockConfig().port(10087)); // 3
@Autowired
private GitHubService gitHubService; // 2
@Test
public void should_fetch_meta_file_success() throws Exception {
String rawMetaFileContent = "content";
stubFor(
get(urlEqualTo("/aisensiy/hello-project/master/meta.yml"))
.willReturn(ok(rawMetaFileContent))); // 4
String metaFile = gitHubService.fetchRawFile("aisensiy/hello-project", "meta.yml"); // 5
assertThat(metaFile, is(rawMetaFileContent)); // 6
}
}
GitHubService
,因此添加了 @SpringBootTest
的注解,并指定 profile 为 test
以便加载 application-test.yml
的配置application-test.yml
相同可以看到 Feign 定义客户端是非常简单的,但是只做到上面的那些是不够的。对于这种网络请求为了保证系统的鲁棒性,还需要处理超时,请求错误等问题。正如 Hystrix 的文档中所提到的,如果你所访问的服务直接挂了,那没什么可怕的,你就直接报错就好了;最怕的是它没有挂但是它的访问速度比预期的要慢很多,这会导致你自身的服务也出现相应的延时,最终可能会导致你自身的一些异步调用的线程池被用尽。
为了增加 Feign 的鲁棒性,我们可以引入 hystrix 的依赖。
...
dependencies {
compile('org.springframework.cloud:spring-cloud-starter-feign')
+ compile('org.springframework.cloud:spring-cloud-starter-hystrix')
compile('org.springframework.boot:spring-boot-starter-web')
compileOnly('org.projectlombok:lombok')
testCompile('org.springframework.boot:spring-boot-starter-test')
testCompile('com.github.tomakehurst:wiremock-standalone:2.7.1')
}
...
在添加 hystrix 依赖之后,如果在 @FeignClient
中不添加 Hystrix 的 fallback Hystrix 是默认不使用的,可以在 application.yml
添加配置启动:
feign:
hystrix:
enabled: true
这样 feign 会默认使用 HystrixFeign
的 builder 构建 FeignClient,并为其添加默认的超时处理。其默认的超时时间为 1000 毫秒。我们可以通过为 WireMock 增加默认的延迟返回来测试这个超时处理:
@ActiveProfiles("test")
@RunWith(SpringRunner.class)
@SpringBootTest
public class GitHubServiceTest {
...
@Test(expected = HystrixRuntimeException.class) // 2
public void should_fail_for_fetching_file() throws Exception {
String rawMetaFileContent = "content";
stubFor(
get(urlEqualTo("/aisensiy/hello-project/master/meta.yml"))
.willReturn(ok(rawMetaFileContent).withFixedDelay(5000))); // 1
gitHubService.fetchRawFile("aisensiy/hello-project", "meta.yml");
}
}
HystrixRuntimeException
对于一些应用,有了超时处理并在超时或请求失败的时候抛出异常就可以了。但是有的时候我们需要为请求添加一个默认的 fallback:也就是说如果请求失败了,我们需要给客户端返回点默认的结果,这个时候就可以使用 Hystrix 的 fallback 机制:
@Component
public class GitHubServiceFallback implements AnotherGitHubService {
@Override
public String fetchRawFile(@PathVariable("git") String git, @PathVariable("filename") String filename) {
return "NONE";
}
}
我们声明一个和 GitHubService
一模一样的接口 AnotherGitHubService
用于测试 fallback 机制,然后创建一个 GitHubServiceFallback
类并实现相应的接口。
然后,我们在 AnotherGitHubService
中声明需要的 fallback 类:
@FeignClient(
value = "github",
url = "${github.url}",
fallback = GitHubServiceFallback.class)
public interface AnotherGitHubService {
@GetMapping("/{git}/master/{filename}")
String fetchRawFile(
@PathVariable("git") String git,
@PathVariable("filename") String filename);
}
然后我们在添加一个测试验证这个 fallback 是否工作:
@Test
public void should_get_fallback_result() throws Exception {
String rawMetaFileContent = "content";
stubFor(
get(urlEqualTo("/aisensiy/hello-project/master/meta.yml"))
.willReturn(ok(rawMetaFileContent).withFixedDelay(5000)));
String result = serviceWithFallback.fetchRawFile("aisensiy/hello-project", "meta.yml");
assertThat(result, is(new GitHubServiceFallback().fetchRawFile("aisensiy/hello-project", "meta.yml")));
}
这次虽然添加了 5000 毫秒的延迟,但是一旦超过默认的 1 秒延时后就会使用默认的结果返回而不会抛出任何异常了。
讲到这里才了解到了 hystrix 的一些基本用法,Hystrix 自己实现了断路器的机制:通过请求窗口周期性判定调用服务的可靠性并按照一定的策略屏蔽不健康的系统,从而保证了自身系统的可靠性。
这里我们还是用过一些接口来展示其断路器的效果:
首先我们定义一个假的第三方服务:
@Component
public class OtherService {
public String run() {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "OK";
}
}
然后定义两个 API 对其进行调用:
@RestController
public class Api {
private OtherService otherService;
public Api(OtherService otherService) {
this.otherService = otherService;
}
@GetMapping("/safe")
public String safe() {
return new com.netflix.hystrix.HystrixCommand<String>(setter()) {
@Override
protected String run() throws Exception {
otherService.run();
return "OK";
}
}.execute();
}
@GetMapping("/unsafe")
public String unsafe() {
return otherService.run();
}
private com.netflix.hystrix.HystrixCommand.Setter setter() {
return HystrixCommand.Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("External"))
.andCommandKey(HystrixCommandKey.Factory.asKey("/safe"));
}
}
其中 /unsafe
接口直接请求 OtherService 而 /safe
通过 Hystrix 包装后请求 OtherService。我们启动这个 Spring Boot 项目,并用 apachebench 分别对两个接口进行压测看看效果。
首先访问 /unsafe 接口
$ ab -n 200 -c 5 http://localhost:8080/unsafe
Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 0 0.1 0 1
Processing: 2002 2011 29.7 2006 2196
Waiting: 2001 2011 29.1 2006 2192
Total: 2002 2011 29.7 2007 2197
Percentage of the requests served within a certain time (ms)
50% 2007
66% 2008
75% 2008
80% 2008
90% 2010
95% 2012
98% 2195
99% 2196
100% 2197 (longest request)
可以看到所有的请求都保持了 2 秒以上的请求时间。
然后我们再请求一下 /safe 接口:
$ ab -n 200 -c 5 http://localhost:8080/safe
Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 0 0.4 0 2
Processing: 3 137 351.8 5 1248
Waiting: 3 137 351.7 4 1248
Total: 3 137 351.8 5 1248
Percentage of the requests served within a certain time (ms)
50% 5
66% 6
75% 7
80% 8
90% 1014
95% 1016
98% 1248
99% 1248
100% 1248 (longest request)
你会发现并不是所有的请求都会是在超过 1 秒后进行超时处理并返回,这就是断路器的效果:当 Hystrix 发现所访问的请求不能达到预期的时候其依据自己周期内请求的成功比例定义是否开启断路器功能。一旦开启断路功能,外部服务将被默认是失败的,在此期间 Hystrix 不再尝试请求服务而是直接返回 fallback 结果(或者抛出异常)。在 Hystrix – managing failures in distributed systems 中,演讲者给出了一个很详细的示例展示了这个功能,如果想要更进一步了解断路器可以看看这个演讲。