千寻

道路很长, 开始了就别停下!

0%

这里分4种情况讨论

整个配置文件是

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
server{
port 80,
server name 192.168.1.123

location /static{
proxy_pass 192.168.2.321:81
}

location /static{
proxy_pass 192.168.2.321:81/
}

location /static/{
proxy_pass 192.168.2.321:81
}

location /static/{
proxy_pass 192.168.2.321:81/
}
}

1. 第一种: location后没有/ 转发网站没有/

1
2
3
4
5
6
7
8
#192.168.1.123->server name
# :80 ---------> port
#/statc ------->location
#/a.html ------>proxy_pass

location /static{
proxy_pass 192.168.2.321:81
}

最后网址经过nginx转向到的网址是 192.168.2.321:81/static/a.html

2. 第二种: location后没有/ 转发网站有/

1
2
3
4
5
6
7
8
#192.168.1.123---->server name
# :80 ------------> port
#/statc ---------->location
#/a.html --------->proxy_pass

location /static{
proxy_pass 192.168.2.321:81/
}

最后网址经过nginx转向到的网址是 192.168.2.321:81/a.html

3. 第三种: location后有/ 转发网站没有/

1
2
3
4
5
6
7
8
#192.168.1.123-->server name
# :80 ------------> port
#/statc/ ---------->location
#a.html --------->proxy_pass

location /static/{
proxy_pass 192.168.2.321:81
}

最后网址经过nginx转向到的网址是 192.168.2.321:81/static/a.html

4. 第四种: location后有/ 转发网站有/

1
2
3
4
5
6
7
8
#192.168.1.123-->server name
# :80 ------------> port
#/statc/ ---------->location(path1)
#a.html --------->proxy_pass (path2)

location /static/{
proxy_pass 192.168.2.321:81/
}

最后网址经过nginx转向到的网址是 192.168.2.321:81/a.html

总结:

从这四种我们可以的看出,当nginx里面匹配时可以把端口后的参数分为path1+path2(其中我在上方标注的location属于path1,proxy_pass属于path2)

proxy_pass

  • 里面是ip:port+/ 时nginx最后匹配的网址是 proxy_pass的内容加上path2

  • 里面是ip:port 时nginx最后匹配的网址是 proxy_pass的内容加上path1+path2

一、四句敏捷宣言

  1. 个体与交互 胜过 过程与工具

  2. 可以工作的软件 胜过 面面俱到的文档

  3. 客户协作 胜过 合同谈判

  4. 响应变化 胜过 遵循计划

二、12条敏捷原则

四句敏捷宣言中讲了敏捷开发的价值观, 从这些价值观中可以引出下面的12条原则, 它们是敏捷实践区别于重型过程的特征所在。
在Agile Software Development - Principles,Patterns,and Practices(中文书名: 敏捷软件开发-原则、模式与实践)中对这12条原则分别进行了阐述。

1. 我们最优先要做的是通过尽早的、持续的交付有价值的软件来使客户满意。

规划迭代故事时必须按照优先级安排,为客户先提供最有价值的功能。通过频繁迭代能与客户形成早期的良好合作,及时反馈提高产品质量。敏捷小组关注完成和交 付具有用户价值的功能,而不是孤立的任务。以前我们都用需求规格说明书或者用例来编写详细的需求,敏捷使用用户故事来罗列需求。用户故事是一种表示需求的 轻量级技术,它没有

固定的形式和强制性的语法。但是有一些固定的形式可以用来参考还是比较有益的。敏捷估算中使用了这个模板:“作为【用户的类型】,我希 望可以【能力】以便【业务价值】“。使用基于用户故事的需求分析方法时,仍可能需要原型和编写文档,只是工作重点更多的转移到了口头交流。

2. 即使到了开发的后期,也欢迎改变需求。敏捷过程利用变化来为客户创造竞争优势。

敏捷过程参与者不怕变化,他们认为改变需求是好事情,因为这些改变意味着我们更了解市场需求。

*3. 经常性的交付可以工作的软件,交付的间隔可以从几周到几个月,交付的时间间隔越短越好. *

迭代是受实践框限制的,意味着即使放弃一些功能也必须按时结束迭代。只要我们可以保证交付的软件可以很好的工作,那么交付时间越短,我们和客户协作就越 紧密,对产品质量就更有益。虽然我们多次迭代,但并不是每次迭代的结果都需要交付给用户,敏捷开发的目标是让他们可以交付。这意味着开发小组在每次迭代中 都会增加一些功能,增加的每个功能都是经过编码、测试,达到了可发布的质量标准的。  

另外敏捷开发项目中对开发阶段没有什么重要的分割,没有先期的需求阶段,然后是分析阶段,架构设计阶段,编码测试阶段等,在项目真正开始后,每次迭代中都会同时进行所有的上述阶段工作。

4. 在整个项目开发期间,业务人员和开发人员必须天天都在一起工作。

软件项目不会依照之前设定的计划原路执行,中间对业务的理解、软件的解决方案肯定会存在偏差,所以客户、需求人员、开发人员以及涉众之间必须进行有意义的、频繁的交互,这样就可以在早期及时的发现并解决问题。

5. 围绕被激励起来的人个来构建项目。给他们提供所需要的环境和支持,并且信任他们能够完成工作。

业务和技术是引起不确定的二个主要方面,人是第三个方面。而业务和技术又必须由人来执行,所以能够激励人来解决这些问题是解决不确定性的关键。只要个人的目标和团队的目标一致,我们就需要鼓舞起每个人的积极性,以个人为中心构建项目,提供所需的环境、支持与信任。

6. 在团队内部,最具有效果并且富有效率的传递信息的方法,就是面对面的交谈。

在十几或者二十几个人组成的大团队中,文档是一种比较合适的传递知识和交流的途径。而敏捷团队一般不会很多人(大团队实施敏捷时也会分成多个小的敏捷团队),所以大量的文档交流其实并不是很经济的做法。此时面对面的交谈反而更快速有效。

7、可工作的软件是首要进度度量标准。

一般的工作都比较容易衡量任务进展,比如让你去搬运1吨的石头,我只要去称一下你已经搬运的石头重量就知道你完成多少了。而对于软件来说,在软件没有编 码、测试完成之前,我们都不能因为代码编写了多少行,测试用例跑了多少个就去度量这个功能是否完成了。

衡量这个功能是否完成的首要标准就是这个功能可以工 作了,对用户来说已经可以应用了.

8. 敏捷过程提可持续的开发速度。责任人、开发者和用户应该能够保持一个长期的、恒定的开发速度。

很多人都认为软件开发中加班是很正常的,不加班反而不正常,我对此有点不理解,这个可能是国情所致吧。敏捷过程希望能够可持续的进行开发,开发速度不会 随着迭代的任务不同而不同,不欣赏所谓的拼一拼也能完成的态度,开发工作不应该是突击行为。

我们不能指望说突击这个项目后就可以轻松了,因为完成一个项目 后会接踵而来下一个项目,而只要还是拼拼的态度,下一个项目依旧会让你的组员再次突击。这时不知道有人会不会说,那我们就一直加班,也是“持续的开发速 度”啊,这时可要注意了,持续加班智慧导致人疲劳、厌倦,保持长期恒定的速度也只是一种理想而已。

9. 不断地关注优秀的技能和好的设计会增强敏捷能力。

敏捷过程有很多好的技术实践可以加强产品敏捷能力,很多原则、模式和实践也可以增强敏捷开发能力。 《敏捷软件开发-原则、模式与实践》一书中介绍了很多设计,感兴趣的可以去仔细看看。

10. 简单—-使未完成的工作最大化的艺术—-是根本的。

我们不可能预期后面需求会如何变化,所以不可能一开始就构建一个完美的架构来适应以后的所有变化。敏捷团队不会去构建明天的软件,而把注意力放在如何通 过最简单的方法完成现在需要解决的问题。这时有人会说,我已经预计到了肯定存在哪些需求扩展点,我们在一开始是否需要考虑呢?这时团队需要根据自己的理解 去决定是否考虑,如果深信在明天发生了这个问题也可以轻易处理的话,那么就最好先不考虑。

11. 最好的构架、需求和设计出自与自组织的团队。

敏捷中有很多种实践,大家都知道,迭代式开发是主要的实践方法,而自组织团队也是主要的实践之一。在自组织团队中,管理者不再发号施令,而是让团队自身寻找最佳的工作方式来完成工作。要形成一个自组织团队其实比较难。CSDN采访Mishkin Berteig中说到 自组织团队的第一个要素就是必须有一个团队,而不仅仅是一群人。一群人是一帮在一起工作的人,他们彼此之间并没有太多的沟通,他们也并不视彼此为一体。

项目一开始,我们就会组建“团队”,但很多时候由构架师、需求人员、开发人员和测试人员组成的是一群人而已。他还认为,团队的形成必须经历几个时期。在 经历了初期的磨合后,成员才会开始对团队共同的工作理念与文化形成一个基本的认识和理解。团队内会逐渐形成规矩,而且这些规矩是不言而喻的。比如,每个人 都知道上午九点来上班,都会主动询问别人是否需要帮助,也都会去主动和别人探讨问题。

如果团队成员之间能够达成这样的默契,那么这个团队将成为一个真正高 效的工作团队。在这样团队中,成员之间相互理解,工作效率非常高。在自组织团队中,团队成员不需要遵从别人的详细指令。他们需要更高层次的指导,这种指 导更像是一个目标,一个致力于开发出更好的软件的目标。总之,自组织团队是一个自动自发、有着共同目标和工作文化的团队,这样的团队总是在向它的组织做出 承诺。但是,实现这些承诺对于自组织团队来说非常重要。否则,一旦出现问题,团队成员之间就会出现信任危机。

虽然敏捷开发小组是以小组为整体 来工作的,但是还是有必要指明一些承担一定任务的角色。第一个角色是产品所有者(Product Owner)。产品所有者的主要职责包括:确认小组所有成员都在追求一个共同的项目前景,确定功能的优先级以便总是在处理最具有价值的功能,以及作出决定 使得对项目的投入可以产生良好的回报。可以对应为以前开发中的“产品经理”。另一角色是开发团队(developer),这里的开发人员包括了架构师、设计师、程序员、需求人员、测试人员、文档编写者等,有时产品所有者也可以被看作是

开发人员。还有一个重要角色就是项目经理(project manager)。敏捷开发的项目经理会更多的关注领导而不是管理。在某些项目中,项目经理可能同时也是开发人员,少数时候也会担任产品所有者。   

12. 每隔一定时间,团队会在如何才能更有效地工作方面进行反省,然后相应地对自己的行为进行调整。

  由于很多不确定性因素会导致计划失效,比如项目成员增减、技术应用效果、用户需求的改变、竞争者对我们的影响等都会让我们作出不同的反应。 敏捷不是基于预定义的工作方式,而是基于经验性的方式,对以上这些变化,小组通过不断的反省调整来保持团队的敏捷性。

先实例对比说说什么面向切面,看下面代码:

1
2
3
4
5
6
7
8
 @Override
public void savePerson() {
//现在我想把每个保存数据库的语句前后都打印一句话,如下:
System.out.println("开始保存到数据库.....");
save(person); //请把这句看做是保存数据库的语句
System.out.println("...保存成功");

}

上面打印的语句,其实就相当于日志,监控我有没有保存成功,这里我保存的是person对象,如果我还有student,teacher,dog等等很多对象都需要做增删改查操作,是不是在每个增删改查的语句前后都加上这两句话呢?这样不是很繁琐。那么有没有办法让每有执行save操作时就自动前后打印日志呢?这里就应运而生了面向切面AOP

下面再看看面向切面的例子吧!

maven工程加jar包依赖:

1
2
3
4
5
6
7
<!-- spring aop -->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.6.8</version>
</dependency>
<dependency>

applicationContext.xml配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.0.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-2.5.xsd">

<!-- 配置扫描的包 -->
<context:component-scan base-package="redisCache.service"/>
<!-- 切面的声明 -->
<bean id="transaction" class="aop.Transaction"/>
<!--aop配置 -->
<aop:config>
<!-- 切点, 配置aop的切入点id; 是切入点的标识 ;expression 为切入点的表达式 -->
<aop:pointcut expression="execution(* redisCache.service.impl.PersonDaoImpl.*(..))" id="perform"/>
<!-- 切面,配置切面(切面里面配置通知)—— ref 指向声明切面的类 -->
<aop:aspect ref="transaction">
<!-- 前置通知pointcut-ref 引用一个切入点 -->
<aop:before method="beginTransaction" pointcut-ref="perform"/>
<!-- 后置通知
<aop:after-returning method="commit" pointcut-ref="perform" returning="val"/> -->

</aop:aspect>
</aop:config>

</beans>

切面工具类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
package aop;

import java.util.ArrayList;
import java.util.List;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;

import redisCache.entity.Person;

/**
* 切面(spring aop 就不需要拦截器啦)
* (模拟hibernate里面保存数据要打开事物,然后各种增删改之后,再提交事物。)
*/
public class Transaction {
public void beginTransaction() {//前置通知
//打开事物
System.out.println("begin Transaction");
}

/**
* @param joinPoint 通过joinPoint可以得到目标类和目标方法的一些信息
* @param val 目标方法的返回值
* 和<aop:after-returning returning="val"/>中returning的值保质一致
*/
public void commit(JoinPoint joinPoint, Object val) {//后置通知
String methodName = joinPoint.getSignature().getName();
System.out.println(methodName);
System.out.println(joinPoint.getTarget().getClass().getName());
//提交事物
System.out.println("commit");
List<Person> personList = (ArrayList<Person>) val;
for (Person person : personList) {
System.out.println(person.getPname());
}
}

public void finalMethod() {
System.out.println("最终通知");
}

public void aroundMethod(ProceedingJoinPoint joinPoint) {//环绕通知
try {
System.out.println("around method");
joinPoint.proceed();//调用目标类的目标方法
} catch (Throwable e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}

/**
* 异常通知
*/
public void throwingMethod(Throwable except) {
System.out.println(except.getMessage());
}
}

person实体类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package redisCache.entity;

public class Person {
private Long pid;
private String pname;

public Long getPid() {
return pid;
}

public void setPid(Long pid) {
this.pid = pid;
}

public String getPname() {
return pname;
}

public void setPname(String pname) {
this.pname = pname;
}
}

personDao接口类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package redisCache.service;

import java.util.List;

import redisCache.entity.Person;

/**
* 目标对象和代理对象都实现的接口
*/
public interface PersonDao {
void deletePerson();
List<Person> getPerson() throws Exception;
void savePerson();
void updatePerson();
}

personDaoImpl接口实现类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
package redisCache.service.impl;

import java.util.ArrayList;
import java.util.List;

import org.springframework.stereotype.Service;

import redisCache.entity.Person;
import redisCache.service.PersonDao;
/**
* 目标对象:实现目标接口
*/
@Service("personDao")
public class PersonDaoImpl implements PersonDao{
@Override
public void deletePerson() {
System.out.println("delete perosn");
}

@Override
public List<Person> getPerson() throws Exception {
List<Person> personList = new ArrayList<Person>();
Person person1 = new Person();
person1.setPid(1L);
person1.setPname("person1");
System.out.println("get person");
personList.add(person1);
Person person2 = new Person();
person2.setPid(2L);
person2.setPname("person2");
personList.add(person2);
return personList;
}

@Override
public void savePerson() {
System.out.println("delete perosn");
}

@Override
public void updatePerson() {
System.out.println("delete perosn");
}
}

TestAop测试类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package redisCache;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

import redisCache.service.PersonDao;

public class TestAop {

public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
PersonDao personDao=(PersonDao) context.getBean("personDao");
try {
personDao.getPerson();
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}

}

允许测试类的结果:

1
2
3
4
5
6
7
log4j:WARN No appenders could be found for logger (org.springframework.core.env.StandardEnvironment).
log4j:WARN Please initialize the log4j system properly.
log4j:WARN See http://logging.apache.org/log4j/1.2/faq.html#noconfig for more info.
begin Transaction
get person
begin Transaction
delete perosn

从打印结果中可以看出personDaoImpl实现类的所有方法只要执行就会先打印“begin Transcation” .

上面我们在执行方法前打印的方式称为前置通知,当然在面向切面的术语中还有其他诸如:后置通知,环绕通知,最终通知,异常通知。这里我们都在配置文件中加上,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<aop:config>
<!-- 切点, 配置aop的切入点id; 是切入点的标识 ;expression 为切入点的表达式 -->
<aop:pointcut expression="execution(* redisCache.service.impl.PersonDaoImpl.*(..))" id="perform"/>
<!-- 切面,配置切面(切面里面配置通知)—— ref 指向声明切面的类 -->
<aop:aspect ref="transaction">
<!-- 前置通知pointcut-ref 引用一个切入点 -->
<aop:before method="beginTransaction" pointcut-ref="perform"/>
<!-- 后置通知
<aop:after-returning method="commit" pointcut-ref="perform" returning="val"/> -->
<!--
最终通知
* 不能得到目标方法的返回值
* 无论目标方法是否有异常,最终通知都将执行
* 资源的关闭、连接的释放写在最终通知里
-->
<!--<aop:after pointcut-ref="perform" method="finalMethod"/>-->

<!--
环绕通知
* ProceedingJoinPoint的proceed方法就是目标对象的目标方法
* 环绕通知可以控制目标对象目标方法执行
-->
<!--
<aop:around method="aroundMethod" pointcut-ref="perform"/>
-->
<!--
异常通知
* 在异常通知中获取目标方法抛出的异常
-->
<!--<aop:after-throwing method="throwingMethod" pointcut-ref="perform" throwing="except"/>-->
</aop:aspect>
</aop:config>

总结一句话就是:AOP 在不修改源代码的情况下给程序动态统一添加功能。 这样就能够在一个项目及时要在中途需要这么一个功能,那也就只需修改配置文件和加一个类,而没有该已经写好的类的代码。aop明显增加了代码的复用性,也省去了重新测试的时间。

通过实例大概了解aop的用途和优势后我们再结合上面的实例理解aop的原理和各种术语。

先上图:

上面的三条红色的竖向框就是经常说的切面,在这个切面里面有很多的方法,你大可以吧a()看做上面的说道的前置通知,b()看做后置通知,c()看做最终通知等等。总而言之,这些方法都不需要我们去写的,而是aop自动帮我们做好的。我们只要触动了我们的比如“保存方法”就会执行切面里的一系列方法。这样就省去了很多开发时间,也精简了代码。

因为这个AOP–面向切面编程是基于动态代理模式的,所以,要想搞清楚这个AOP,就必须得先了解下,什么是代理模式什么又是动态代理模式动态代理模式的2种实现方式。

在小编的这篇博文中有简单解释代理:https://blog.csdn.net/csdnliuxin123524/article/details/81236007

下面重新写一下动态代理的实例步骤,代码转自一位很强的博主的博文:https://blog.csdn.net/qq_27093465/article/details/53351403

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
package proxy1;

import java.lang.reflect.Method;

import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;

public class MyInterceptor implements MethodInterceptor{
private Object target;//通用目标类

//有参构造器
public MyInterceptor(Object o) {
this.target=o;
}
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
if(method.getName().equals("getPerson")){//切入点,因为传入的object类中的方法不是都需要做打印通知的,所以只有满足我们条件的方法才行,这里我以方法名做判断。
System.out.println("aaaaa");//切面方法a();
//。。。
method.invoke(this.target, objects);//调用目标类的目标方法
//。。。
System.out.println("bbbbb");//切面方法f();
}
return null;
}

/**
* 返回代理对象
* 具体实现,暂时先不追究。
*/
public Object createProxy() {
Enhancer enhancer = new Enhancer();
enhancer.setCallback(this);//回调函数 拦截器
//设置代理对象的父类,可以看到代理对象是目标对象的子类。所以这个接口类就可以省略了。
enhancer.setSuperclass(this.target.getClass());
return enhancer.create();
}

}

测试类

1
2
3
4
5
6
7
8
9
10
11
12
13
package redisCache;
import redisCache.service.impl.PersonDaoImpl;

public class TestAop2 {
public static void main(String[] args) throws Exception {
PersonDaoImpl personDaoImpl=new PersonDaoImpl();
MyInterceptor myInterceptor=new MyInterceptor(personDaoImpl);
PersonDaoImpl personDaoImpl2=(PersonDaoImpl) myInterceptor.createProxy();
personDaoImpl2.getPerson();

}

}

结果:

1
2
3
aaaaa
get person
bbbbb

上面代码,看完之后,就来对应AOP里面的各个概念到实际代码里面去

图上说了5个术语。加上下面的织入,也就是6个啦。

再加上代理对象,这个就比较简单了,测试代码有写注释啦。那么就是一共7个啦。

唯一漏掉的就是“引入”,这个是系统自己实现的,我们就没必要去深究了。

注意理解以下几个概念:

  • 代理对象的方法 = 目标对象的目标方法 + 所有切面的通知。
  • 织入:形成代理对象的方法的过程
  • 通知:实际上就是切面中的方法。
  • 切入点的理解:只有符合切入点的目标方法,才能加载通知。也就是调用切面的通知(方法)啦,看代码也就是说,切入点是控制代理对象内部的切面方法和目标对象的目标方法是否执行的条件。切面可以不止是一个。每个切面里面的通知即切面方法也是可以有很多的。
  • 连接点的理解:所谓连接点,也就是目标对象或者代理对象之中的方法。为什么说2个都 可以呢?因为如果是jdk实现的动态代理的话,那么目标对象和代理对象要实现共同的接口,如果是cglib实现的动态代理的话,那么代理对象类是目标对象类的子类。都是一个方法啦。所以这么理解就OK的啦。

上面的过程就可以理解为@Transcational注解所起的作用,因为spring事务的管理使用的就是aop动态代理的功能。

一、zuul网关过滤器

Zuul中提供了过滤器定义,可以用来过滤代理请求,提供额外功能逻辑。如:权限验证,日志记录等。

阅读全文 »

注意顺序,会与其他如后缀匹配 .*.(gif|jpg|jpeg|png|bmp|swf)$ 等冲突。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
server {
listen 80;
server_name api.yiqikj.cn;
location /resource/ {
alias /data/inkee/inkee-boot-os/;
autoindex on;
expires 30d;
}
location / {
#proxy_redirect off;
proxy_pass http://127.0.0.1:9898;
}
# location ~ .*\.(gif|jpg|jpeg|png|bmp|swf)$ {
# expires 30d;
# }
# location ~ .*\.(js|css)?$ {
# expires 12h;
# }
access_log logs/inkeebackstage.log main;
}

在上一篇文章详细的介绍了Gateway的Predict,Predict决定了请求由哪一个路由处理,在路由处理之前,需要经过“pre”类型的过滤器处理,处理返回响应之后,可以由“post”类型的过滤器处理。

filter的作用和生命周期

由filter工作流程点,可以知道filter有着非常重要的作用,在“pre”类型的过滤器可以做参数校验、权限校验、流量监控、日志输出、协议转换等,在“post”类型的过滤器中可以做响应内容、响应头的修改,日志的输出,流量监控等。首先需要弄清一点为什么需要网关这一层,这就不得不说下filter的作用了。

作用

当我们有很多个服务时,比如下图中的user-service、goods-service、sales-service等服务,客户端请求各个服务的Api时,每个服务都需要做相同的事情,比如鉴权、限流、日志输出等。

对于这样重复的工作,有没有办法做的更好,答案是肯定的。在微服务的上一层加一个全局的权限控制、限流、日志输出的Api Gatewat服务,然后再将请求转发到具体的业务服务层。这个Api Gateway服务就是起到一个服务边界的作用,外接的请求访问系统,必须先通过网关层。

生命周期

Spring Cloud Gateway同zuul类似,有“pre”和“post”两种方式的filter。客户端的请求先经过“pre”类型的filter,然后将请求转发到具体的业务服务,比如上图中的user-service,收到业务服务的响应之后,再经过“post”类型的filter处理,最后返回响应到客户端。

与zuul不同的是,filter除了分为“pre”和“post”两种方式的filter外,在Spring Cloud Gateway中,filter从作用范围可分为另外两种,一种是针对于单个路由的gateway filter,它在配置文件中的写法同predict类似;另外一种是针对于所有路由的global gateway filer。现在从作用范围划分的维度来讲解这两种filter。

gateway filter

过滤器允许以某种方式修改传入的HTTP请求或传出的HTTP响应。过滤器可以限定作用在某些特定请求路径上。 Spring Cloud Gateway包含许多内置的GatewayFilter工厂。

GatewayFilter工厂同上一篇介绍的Predicate工厂类似,都是在配置文件application.yml中配置,遵循了约定大于配置的思想,只需要在配置文件配置GatewayFilter Factory的名称,而不需要写全部的类名,比如AddRequestHeaderGatewayFilterFactory只需要在配置文件中写AddRequestHeader,而不是全部类名。在配置文件中配置的GatewayFilter Factory最终都会相应的过滤器工厂类处理。

Spring Cloud Gateway 内置的过滤器工厂一览表如下:

现在挑几个常见的过滤器工厂来讲解,每一个过滤器工厂在官方文档都给出了详细的使用案例,如果不清楚的还可以在org.springframework.cloud.gateway.filter.factory看每一个过滤器工厂的源码。

AddRequestHeader GatewayFilter Factory

创建工程,引入相关的依赖,包括spring boot 版本2.0.5,spring Cloud版本Finchley,gateway依赖如下:

1
2
3
4
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>

在工程的配置文件中,加入以下的配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
server:
port: 8081
spring:
profiles:
active: add_request_header_route

---
spring:
cloud:
gateway:
routes:
- id: add_request_header_route
uri: http://httpbin.org:80/get
filters:
- AddRequestHeader=X-Request-Foo, Bar
predicates:
- After=2017-01-20T17:42:47.789-07:00[America/Denver]
profiles: add_request_header_route

在上述的配置中,工程的启动端口为8081,配置文件为add_request_header_route,在add_request_header_route配置中,配置了roter的id为add_request_header_route,路由地址为http://httpbin.org:80/get,该router有AfterPredictFactory,有一个filter为AddRequestHeaderGatewayFilterFactory(约定写成AddRequestHeader),AddRequestHeader过滤器工厂会在请求头加上一对请求头,名称为X-Request-Foo,值为Bar。为了验证AddRequestHeaderGatewayFilterFactory是怎么样工作的,查看它的源码,AddRequestHeaderGatewayFilterFactory的源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class AddRequestHeaderGatewayFilterFactory extends AbstractNameValueGatewayFilterFactory {

@Override
public GatewayFilter apply(NameValueConfig config) {
return (exchange, chain) -&gt; {
ServerHttpRequest request = exchange.getRequest().mutate()
.header(config.getName(), config.getValue())
.build();

return chain.filter(exchange.mutate().request(request).build());
};
}

}

由上面的代码可知,根据旧的ServerHttpRequest创建新的 ServerHttpRequest ,在新的ServerHttpRequest加了一个请求头,然后创建新的 ServerWebExchange ,提交过滤器链继续过滤。

启动工程,通过curl命令来模拟请求:

1
curl localhost:8081

最终显示了从 http://httpbin.org:80/get得到了请求,响应如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"args": {},
"headers": {
"Accept": "*/*",
"Connection": "close",
"Forwarded": "proto=http;host=\"localhost:8081\";for=\"0:0:0:0:0:0:0:1:56248\"",
"Host": "httpbin.org",
"User-Agent": "curl/7.58.0",
"X-Forwarded-Host": "localhost:8081",
"X-Request-Foo": "Bar"
},
"origin": "0:0:0:0:0:0:0:1, 210.22.21.66",
"url": "http://localhost:8081/get"
}

可以上面的响应可知,确实在请求头中加入了X-Request-Foo这样的一个请求头,在配置文件中配置的AddRequestHeader过滤器工厂生效。

跟AddRequestHeader过滤器工厂类似的还有AddResponseHeader过滤器工厂,在此就不再重复。

RewritePath GatewayFilter Factory

在Nginx服务启中有一个非常强大的功能就是重写路径,Spring Cloud Gateway默认也提供了这样的功能,这个功能是Zuul没有的。在配置文件中加上以下的配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
spring:
profiles:
active: rewritepath_route
---
spring:
cloud:
gateway:
routes:
- id: rewritepath_route
uri: https://blog.csdn.net
predicates:
- Path=/foo/**
filters:
- RewritePath=/foo/(?&lt;segment&gt;.*), /$\{segment}
profiles: rewritepath_route

上面的配置中,所有的/foo/*开始的路径都会命中配置的router,并执行过滤器的逻辑,在本案例中配置了RewritePath过滤器工厂,此工厂将/foo/(?.)重写为{segment},然后转发到https://blog.csdn.net。比如在网页上请求localhost:8081/foo/forezp,此时会将请求转发到https://blog.csdn.net/forezp的页面,比如在网页上请求localhost:8081/foo/forezp/1,页面显示404,就是因为不存在https://blog.csdn.net/forezp/1这个页面。

自定义过滤器

Spring Cloud Gateway内置了19种强大的过滤器工厂,能够满足很多场景的需求,那么能不能自定义自己的过滤器呢,当然是可以的。在spring Cloud Gateway中,过滤器需要实现GatewayFilter和Ordered2个接口。写一个RequestTimeFilter,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class RequestTimeFilter implements GatewayFilter, Ordered {

private static final Log log = LogFactory.getLog(GatewayFilter.class);
private static final String REQUEST_TIME_BEGIN = "requestTimeBegin";

@Override
public Mono&lt;Void&gt; filter(ServerWebExchange exchange, GatewayFilterChain chain) {

exchange.getAttributes().put(REQUEST_TIME_BEGIN, System.currentTimeMillis());
return chain.filter(exchange).then(
Mono.fromRunnable(() -&gt; {
Long startTime = exchange.getAttribute(REQUEST_TIME_BEGIN);
if (startTime != null) {
log.info(exchange.getRequest().getURI().getRawPath() + ": " + (System.currentTimeMillis() - startTime) + "ms");
}
})
);

}

@Override
public int getOrder() {
return 0;
}
}

在上面的代码中,Ordered中的int getOrder()方法是来给过滤器设定优先级别的,值越大则优先级越低。还有有一个filterI(exchange,chain)方法,在该方法中,先记录了请求的开始时间,并保存在ServerWebExchange中,此处是一个“pre”类型的过滤器,然后再chain.filter的内部类中的run()方法中相当于”post”过滤器,在此处打印了请求所消耗的时间。然后将该过滤器注册到router中,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Bean
public RouteLocator customerRouteLocator(RouteLocatorBuilder builder) {
// @formatter:off
return builder.routes()
.route(r -&gt; r.path("/customer/**")
.filters(f -&gt; f.filter(new RequestTimeFilter())
.addResponseHeader("X-Response-Default-Foo", "Default-Bar"))
.uri("http://httpbin.org:80/get")
.order(0)
.id("customer_filter_router")
)
.build();
// @formatter:on
}

重启程序,通过curl命令模拟请求:

1
curl localhost:8081/customer/123

在程序的控制台输出一下的请求信息的日志:

1
2018-11-16 15:02:20.177  INFO 20488 --- [ctor-http-nio-3] o.s.cloud.gateway.filter.GatewayFilter   : /customer/123: 152ms

自定义过滤器工厂

在上面的自定义过滤器中,有没有办法自定义过滤器工厂类呢?这样就可以在配置文件中配置过滤器了。现在需要实现一个过滤器工厂,在打印时间的时候,可以设置参数来决定是否打印请参数。查看GatewayFilterFactory的源码,可以发现GatewayFilterfactory的层级如下:

过滤器工厂的顶级接口是GatewayFilterFactory,我们可以直接继承它的两个抽象类来简化开发AbstractGatewayFilterFactory和AbstractNameValueGatewayFilterFactory,这两个抽象类的区别就是前者接收一个参数(像StripPrefix和我们创建的这种),后者接收两个参数(像AddResponseHeader)。

过滤器工厂的顶级接口是GatewayFilterFactory,有2个两个较接近具体实现的抽象类,分别为AbstractGatewayFilterFactory和AbstractNameValueGatewayFilterFactory,这2个类前者接收一个参数,比如它的实现类RedirectToGatewayFilterFactory;后者接收2个参数,比如它的实现类AddRequestHeaderGatewayFilterFactory类。现在需要将请求的日志打印出来,需要使用一个参数,这时可以参照RedirectToGatewayFilterFactory的写法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54

public class RequestTimeGatewayFilterFactory extends AbstractGatewayFilterFactory<RequestTimeGatewayFilterFactory.Config> {


private static final Log log = LogFactory.getLog(GatewayFilter.class);
private static final String REQUEST_TIME_BEGIN = "requestTimeBegin";
private static final String KEY = "withParams";

@Override
public List<String> shortcutFieldOrder() {
return Arrays.asList(KEY);
}

public RequestTimeGatewayFilterFactory() {
super(Config.class);
}

@Override
public GatewayFilter apply(Config config) {
return (exchange, chain) -> {
exchange.getAttributes().put(REQUEST_TIME_BEGIN, System.currentTimeMillis());
return chain.filter(exchange).then(
Mono.fromRunnable(() -> {
Long startTime = exchange.getAttribute(REQUEST_TIME_BEGIN);
if (startTime != null) {
StringBuilder sb = new StringBuilder(exchange.getRequest().getURI().getRawPath())
.append(": ")
.append(System.currentTimeMillis() - startTime)
.append("ms");
if (config.isWithParams()) {
sb.append(" params:").append(exchange.getRequest().getQueryParams());
}
log.info(sb.toString());
}
})
);
};
}


public static class Config {

private boolean withParams;

public boolean isWithParams() {
return withParams;
}

public void setWithParams(boolean withParams) {
this.withParams = withParams;
}

}
}

在上面的代码中 apply(Config config)方法内创建了一个GatewayFilter的匿名类,具体的实现逻辑跟之前一样,只不过加了是否打印请求参数的逻辑,而这个逻辑的开关是config.isWithParams()。静态内部类类Config就是为了接收那个boolean类型的参数服务的,里边的变量名可以随意写,但是要重写List shortcutFieldOrder()这个方法。

需要注意的是,在类的构造器中一定要调用下父类的构造器把Config类型传过去,否则会报ClassCastException

最后,需要在工程的启动文件Application类中,向Srping Ioc容器注册RequestTimeGatewayFilterFactory类的Bean。

1
2
3
4
@Bean
public RequestTimeGatewayFilterFactory elapsedGatewayFilterFactory() {
return new RequestTimeGatewayFilterFactory();
}

然后可以在配置文件中配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
spring:
profiles:
active: elapse_route

---
spring:
cloud:
gateway:
routes:
- id: elapse_route
uri: http://httpbin.org:80/get
filters:
- RequestTime=false
predicates:
- After=2017-01-20T17:42:47.789-07:00[America/Denver]
profiles: elapse_route

启动工程,在浏览器上访问localhost:8081?name=forezp,可以在控制台上看到,日志输出了请求消耗的时间和请求参数。

global filter

Spring Cloud Gateway根据作用范围划分为GatewayFilter和GlobalFilter,二者区别如下:

  • GatewayFilter : 需要通过spring.cloud.routes.filters 配置在具体路由下,只作用在当前路由上或通过spring.cloud.default-filters配置在全局,作用在所有路由上

  • GlobalFilter : 全局过滤器,不需要在配置文件中配置,作用在所有的路由上,最终通过GatewayFilterAdapter包装成GatewayFilterChain可识别的过滤器,它为请求业务以及路由的URI转换为真实业务服务的请求地址的核心过滤器,不需要配置,系统初始化时加载,并作用在每个路由上。

Spring Cloud Gateway框架内置的GlobalFilter如下:

上图中每一个GlobalFilter都作用在每一个router上,能够满足大多数的需求。但是如果遇到业务上的定制,可能需要编写满足自己需求的GlobalFilter。在下面的案例中将讲述如何编写自己GlobalFilter,该GlobalFilter会校验请求中是否包含了请求参数“token”,如何不包含请求参数“token”则不转发路由,否则执行正常的逻辑。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class TokenFilter implements GlobalFilter, Ordered {

Logger logger=LoggerFactory.getLogger( TokenFilter.class );
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String token = exchange.getRequest().getQueryParams().getFirst("token");
if (token == null || token.isEmpty()) {
logger.info( "token is empty..." );
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
}
return chain.filter(exchange);
}

@Override
public int getOrder() {
return -100;
}
}

在上面的TokenFilter需要实现GlobalFilter和Ordered接口,这和实现GatewayFilter很类似。然后根据ServerWebExchange获取ServerHttpRequest,然后根据ServerHttpRequest中是否含有参数token,如果没有则完成请求,终止转发,否则执行正常的逻辑。

然后需要将TokenFilter在工程的启动类中注入到Spring Ioc容器中,代码如下:

1
2
3
4
@Bean
public TokenFilter tokenFilter(){
return new TokenFilter();
}

启动工程,使用curl命令请求:

1
curl localhost:8081/customer/123

可以看到请没有被转发,请求被终止,并在控制台打印了如下日志:

1
2018-11-16 15:30:13.543  INFO 19372 --- [ctor-http-nio-2] gateway.TokenFilter                      : token is empty...

上面的日志显示了请求进入了没有传“token”的逻辑。

总结

本篇文章讲述了Spring Cloud Gateway中的过滤器,包括GatewayFilter和GlobalFilter。从官方文档的内置过滤器讲起,然后讲解自定义GatewayFilter、GatewayFilterFactory以及自定义的GlobalFilter。有很多内置的过滤器并没有讲述到,比如限流过滤器,这个我觉得是比较重要和大家关注的过滤器,将在之后的文章讲述。

更多阅读

史上最简单的 SpringCloud 教程汇总

SpringBoot教程汇总