服务端安装并启动Openssh服务

1
2
3
sudo apt-get update
sudo apt-get install openssh-server
sudo service ssh start

服务端授权客户端访问

1
2
3
# 修改授权访问文件,若无此文件则新建
vim ~/.ssh/authorized_keys
# 按行输入须授权的客户端ssh公钥即可

客户端配置公私钥对

1
2
sudo ssh-keygen -t rsa -f ~/.ssh/id_rsa_xxx -C "..."
sudo service ssh restart

添加客户端ssh公钥到服务器授权访问列表

1
sudo ssh-copy-id -i ~/.ssh/id_rsa.pub user@server_ip

使用ssh进行免密登录

1
sudo ssh user@server_ip

客户端配置n个Key访问n个服务

https://appkfz.com/2015/06/18/git-ssh-key/

多站点使用不同的ssh key

1
2
3
4
5
6
7
8
Host company
HostName company.com
User git
IdentityFile ~/.ssh/user1
Host github
HostName github.com
User git
IdentityFile ~/.ssh/user2

同一站点使用不同的ssh key

1
2
3
4
5
6
7
8
Host gitcafe-site1
HostName gitcafe.com
User git
IdentityFile ~/.ssh/user1
Host gitcafe-site2
HostName gitcafe.com
User git
IdentityFile ~/.ssh/user2

Q&A

ssh-add报错

case1

1
Error connecting to agent: Connection refused

重启ssh-agent

1
2
exec ssh-agent zsh
eval `ssh-agent -s`

case2

1
No user exists for uid 501

重启终端即可

多客户端SSH KEY

配置了多个KEY,但是jenkins验证失败,尝试重启jenkins所在的web服务器

生产工作中,多人协作,为保证分支间工作尽然有序,最常见的方式就是创建开发(dev)、发布(release)、主干(master)三个持久分支,并约定其之间以及与特性(feature)、漏洞修复(bugfix)等临时性分支的操作规范。

开发分支(dev)

dev分支,持续集成,并发布至测试环境测试。

发布分支(release)

各个开发者在dev上合并产生的冲突解决掉,并且在测试环境测试通过后稳定的dev,就可以合并至release(release应当是一个随时可以合并至主干分支,而不会产生问题的分支)。另一方面release也应该发布至测试环境测试,此时测试的主要执行者是产品经理等角色。

主干分支(master)

master分支是稳定的可发布至线上的分支,发布上线之前合并最新的release分支,然后由项目负责人执行发布上线相关操作。

「原地」指的是除给出类之外,不借助其他的辅助结构

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
59
60
61
/** 带头指针的单链表原地逆序(逆序函数为reverse)
* Created by Willow on 1/5/17.
*/
public class List {
Node head;
public List(Node head) {
this.head = head;
}
//节点
static class Node {
int data;
Node next;
public Node() {}
public Node(int data, Node next) {
this.data = data;
this.next = next;
}
}
//逆序
public void reverse() {
if (null == head || null == head.next || null == head.next.next)
return;
Node pre = null, cur = this.head.next, nex = cur.next;
while (null != nex) {
//1. 逆置当前节点指向
cur.next = pre;
//2. 前置、当前、后置节点的三个指针向前移动
pre = cur;
cur = nex;
nex = nex.next;
}
cur.next = pre;//3. 逆置最后一个节点的指向
this.head.next = cur;//4. 头指针指向最后一个节点
}
//遍历
public void traverse() {
if (null == head.next)
return;
Node cur = head.next;
while (null != cur) {
System.out.println(cur.data);
cur = cur.next;
}
System.out.println("traverse end \n");
}
//测试入口
public static void main(String[] args) {
Node n7 = new Node(7, null);
Node n6 = new Node(6, n7);
Node n5 = new Node(5, n6);
Node n4 = new Node(4, n5);
Node n3 = new Node(3, n4);
Node n2 = new Node(2, n3);
Node n1 = new Node(1, n2);
Node head = new Node(0, n1);
List list = new List(head);
list.traverse();
list.reverse();
list.traverse();
}
}

树节点类

1
2
3
4
public class Node {
Object Data;
Node left,right,parent;
}

二叉树类

其中judge方法为判断是否是完全二叉树

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
public class BinTree {
Node root;
public boolean judge() {
int n0 = 0, n = 0, stage = 1;
if (this.root == null)
return true;
Queue<Node> queue = new LinkedList<>();
queue.offer(this.root);
while (!queue.isEmpty()) {
Node node = queue.poll();
n++;
//三个必要条件之一
if (node.left == null && node.right != null)
return false;
//三个必要条件之二
if (stage == 2 && (node.left != null || node.right != null))
return false;
stage = (node.left != null && node.right == null) ? 2 : stage;
if (node.left != null)
queue.offer(node.left);
if (node.right != null)
queue.offer(node.right);
if (node.left == null && node.right == null) {
n0++;
stage=2;
}
}
//三个必要条件之三:完全二叉树满足的公式n0=n/2,其中n0是度为0的节点,n是树总的节点数
return (n0 == (int) Math.ceil(n / 2.0)) ? true : false;
}
}

一、保护的过程

防御csrf的过程大致如下

img

1. 给页面表单/接口添加token

1
2
3
4
public String doAction(Map context, CGI cgi) {
CsrfTokenRepository.setToken(context, cgi);
//...
}

2. 在csrfFilter中检测token

1
2
3
4
5
6
7
8
9
10
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
//...
if (!csrfToken.getToken().equals(actualToken)) {
response.sendError(403);
return;
}
//...
filterChain.doFilter(request, response);
}

二、实现相关类

img

为了防御csrf攻击我们需要一个过滤器来对请求进行合法性检测,检测的标准是验证一个token,这个token由CsrfTokenRespository接口的实现类来生产和管理token

1. 实现CsrfFilter

参考org.springframework.security.web.csrf.CsrfFilter

1
2
3
4
5
6
7
8
public class CsrfFilter extends OncePerRequestFilter {
//生产token的货
private final CsrfTokenRepository tokenRepository;
//做放行列表检测的货
private RequestMatcher requireCsrfProtectionMatcher = new CsrfFilter.DefaultRequiresCsrfMatcher();
//做检测结果处理逻辑的货
private AccessDeniedHandler accessDeniedHandler = new AccessDeniedHandlerImpl();
//....

2. 实现CsrfTokenRepository

改写org.springframework.security.web.csrf.CsrfTokenRepository

主要需要结合自己生产环境的模板引擎产出token

1
2
3
4
5
6
7
8
9
10
11
public class HttpSessionCsrfTokenRepository implements CsrfTokenRepository {
//结合模板引擎生产csrf token
public static void setToken(HttpServletRequest request, ModelMap context) {
//尝试拿csrf filter中设置的token
CsrfToken csrfToken = (CsrfToken) request.getAttribute(CsrfToken.class.getName());
if (null != csrfToken) {
context.put("_csrf", csrfToken);
context.put("_csrf_header", "X-CSRF-TOKEN");
}
}
}

三、配置

我们编写的CsrfFilter如果是一个bean,那么实际上我们不能按普通的过滤器那样直接加入到容器中,而应该把它加入到spring-security的filterChain中,并且使用org.springframework.web.filter.DelegatingFilterProxy这个spring为我们提供的代理过滤器来将spring-security的filterChain嫁接到web容器的filterChain当中。

img

1. 添加依赖jar包

导入 spring-security-web 及 spring-security-config 两个jar包依赖

2. 配置web.xml

1
2
3
4
5
6
7
8
9
10
<!--配置DelegatingFilterProxy来代理spring-security的filterChain-->
<filter>
<filter-name>csrfFilter</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<!--需要保护的请求路径-->
<filter-mapping>
<filter-name>csrfFilter</filter-name>
<url-pattern>{path}</url-pattern>
</filter-mapping>

3. 新增配置spring-security.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<beans:beans xmlns="http://www.springframework.org/schema/security"
xmlns:beans="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/security
http://www.springframework.org/schema/security/spring-security-3.2.xsd">
<http auto-config="true" authentication-manager-ref="fake">
<!-- csrf -->
<csrf/>
<custom-filter ref="csrfFilter" after="ANONYMOUS_FILTER"/>
</http>

<!--注入CsrfFilter-->
<beans:bean id="csrfFilter" class="com.willowspace.security.CsrfFilter">
<beans:constructor-arg ref="csrfTokenRepository"/>
</beans:bean>

<!--注入HttpSessionCsrfTokenRepository-->
<beans:bean id="csrfTokenRepository" class="org.springframework.security.web.csrf.HttpSessionCsrfTokenRepository"/>
<authentication-manager id="fake"/>
</beans:beans>

四、抛弃spreing-security过重的filterChain

spring-security的filterChain中有很多filter,可以做很丰富的事情,一旦你使用了它,那么这些filter将都会被按顺序执行。如果你仅仅需要csrf防御,那么使用spring-security的filterChain对项目来说就太重了。我们可以如下使用自定义的原生filter来达到同样的目的。

img

1. 使用原生Filter

1
2
3
4
5
6
7
8
<filter>
<filter-name>csrfFilter</filter-name>
<filter-class>framework.security.SimpleCsrfFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>csrfFilter</filter-name>
<url-pattern>{path}</url-pattern>
</filter-mapping>

2. 由web容器初始化CsrfFilter

1
2
3
4
5
6
7
8
9
10
public class CsrfFilter implements Filter {
private final CsrfTokenRepository tokenRepository;
//...
public SimpleCsrfFilter() {
CsrfTokenRepository csrfTokenRepository = new HttpSessionCsrfTokenRepository();
Assert.notNull(csrfTokenRepository, "csrfTokenRepository cannot be null");
this.tokenRepository = csrfTokenRepository;
}
//...
}

参考文档

  1. Using Spring Security CSRF Protection
  2. Cross Site Request Forgery (CSRF)

slf4j是日志框架抽象的接口,slf4j-lo4j12(Facade模式设计)来统一底层接口,使用slf4j-api作为提供接口,屏蔽了日志框架实现。

slf4j-api

日志框架的抽象/接口,使用了外观模式(Facade) 屏蔽底层日志框架的实现,提供了一套更优质的日志api

slf4j-log4j12

使用适配器模式(Adapter)对底层日志框架(log4j等)进行转接口,对接至slf4j-api

三者关系如下

img

使用slf4j占位符方式打印日志

1. What is the fastest way of (not) logging?

SLF4J supports an advanced feature called parameterized logging which can significantly boost logging performance fordisabled logging statement.

For some Logger logger, writing,

1
logger.debug("Entry number: " + i + " is " + String.valueOf(entry[i]));

incurs the cost of constructing the message parameter, that is converting both integer i and entry[i] to a String, and concatenating intermediate strings. This, regardless of whether the message will be logged or not.

One possible way to avoid the cost of parameter construction is by surrounding the log statement with a test. Here is an example.

1
2
3
if(logger.isDebugEnabled()) {
logger.debug("Entry number: " + i + " is " + String.valueOf(entry[i]));
}

This way you will not incur the cost of parameter construction if debugging is disabled for logger. On the other hand, if the logger is enabled for the DEBUG level, you will incur the cost of evaluating whether the logger is enabled or not, twice: once in debugEnabled and once in debug. This is an insignificant overhead because evaluating a logger takes less than 1% of the time it takes to actually log a statement.

2. Better yet, use parameterized messages

There exists a very convenient alternative based on message formats. Assuming entry is an object, you can write:

1
2
Object entry = new SomeObject();
logger.debug("The entry is {}.", entry);

After evaluating whether to log or not, and only if the decision is affirmative, will the logger implementation format the message and replace the ‘{}’ pair with the string value of entry. In other words, this form does not incur the cost of parameter construction in case the log statement is disabled.

The following two lines will yield the exact same output. However, the second form will outperform the first form by a factor of at least 30, in case of a disabled logging statement.

1
2
logger.debug("The new entry is "+entry+".");
logger.debug("The new entry is {}.", entry);

two argument variant is also available. For example, you can write:

1
logger.debug("The new entry is {}. It replaces {}.", entry, oldEntry);

If three or more arguments need to be passed, you can make use of the Object... variant of the printing methods. For example, you can write:

1
logger.debug("Value {} was inserted between {} and {}.", newVal, below, above);

This form incurs the hidden cost of construction of an Object[] (object array) which is usually very small. The one and two argument variants do not incur this hidden cost and exist solely for this reason (efficiency). The slf4j-api would be smaller/cleaner with only the Object… variant.

Array type arguments, including multi-dimensional arrays, are also supported.

SLF4J uses its own message formatting implementation which differs from that of the Java platform. This is justified by the fact that SLF4J’s implementation performs about 10 times faster but at the cost of being non-standard and less flexible.

Escaping the “{}” pair

The “{}” pair is called the formatting anchor. It serves to designate the location where arguments need to be substituted within the message pattern.

SLF4J only cares about the formatting anchor, that is the ‘{‘ character immediately followed by ‘}’. Thus, in case your message contains the ‘{‘ or the ‘}’ character, you do not have to do anything special unless the ‘}’ character immediately follows ‘}’. For example,

1
logger.debug("Set {1,2} differs from {}", "3");

which will print as “Set {1,2} differs from 3”.

You could have even written,

1
logger.debug("Set {1,2} differs from {{}}", "3");

which would have printed as “Set {1,2} differs from {3}”.

In the extremely rare case where the the “{}” pair occurs naturally within your text and you wish to disable the special meaning of the formatting anchor, then you need to escape the ‘{‘ character with ‘\’, that is the backslash character. Only the ‘{‘ character should be escaped. There is no need to escape the ‘}’ character. For example,

1
logger.debug("Set \\{} differs from {}", "3");

will print as “Set {} differs from 3”. Note that within Java code, the backslash character needs to be written as ‘\‘.

In the rare case where the “{}” occurs naturally in the message, you can double escape the formatting anchor so that it retains its original meaning. For example,

1
logger.debug("File name is C:\\\\{}.", "file.zip");

will print as “File name is C:\file.zip”.

3. How can I log the string contents of a single (possibly complex) object ?

In relatively rare cases where the message to be logged is the string form of an object, then the parameterized printing method of the appropriate level can be used. Assuming complexObjectis an object of certain complexity, for a log statement of level DEBUG, you can write:

1
logger.debug("{}", complexObject);

The logging system will invoke complexObject.toString() method only after it has ascertained that the log statement was enabled. Otherwise, the cost of complexObject.toString()conversion will be advantageously avoided.

面向对象设计有几个原则(开闭、单一等),如果能够按照这些原则来开发软件,就能充分发挥面向对象的好处(低耦合,易扩展)。设计模式正式这些原则的体现,它是前人编码过程中的经验总结,针对某些问题而提出的解决方案。

最经典的设计模式出自《设计模式-可复用面向对象软件的基础》,针对不同具体问题,书中提出了23个设计模式,可大致分为三类:创建模式__、__结构模式行为模式

一、创建模式

「单例、工厂、抽象工厂、原型、建造者」

开发中会用到很多对象、最佳的使用对象的方式,是遵循面向对象的一个原则“==针对接口编程==”,即在程序代码中出现的对象,都是接口类型,而不要出现具体的类,创建型模式大部分都是为了解决这个问题,即:对象创建交给专门的几个类去负责,创建完之后交给用户使用,用户得到的是一个接口类型,既不必知道这些类具体是什么(只要知道它们都是某个接口的实现类),也不必知道是如何创建它们的。

1. 单例模式(Singleton)

「创建唯一的一个对象」

  • 设计:
1
2
3
4
5
6
7
8
9
class A{
private static A single=null;
public static A getSingleton(){
if(null==single){
single=new A();
}
return single;
}
}
  • 客户端:
1
A a = A.getSingleton();

注意

如果是并发环境中、或者在好几个虚拟机之间创建唯一的对象,则要再用其他的方法,如并发控制、rmi等。

2. 工厂模式(Factory)

「最常见的一种模式,可在java类库中大量见到,用户只需要调用工厂方法类的相应方法即可创建对象」

  • 设计:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//Product 产品接口
interface Product{}

//具体产品A
class ProductA implements Product{}

//Factory 工厂接口
interface Factory{
Product create();
}

//class FactoryA implements Factory{
@Override
public Product create(){
return new ProductA();
}
}
  • 客户端:
1
2
Factory factory = new FactoryA();
Product product = factory.create();

说明

  1. 要修改Product对象的创建方式,客户端代码并不受影响
  2. 要创建另一种Product对象(如ProductB),则创建一个Facrory的子类FactoryB,然后用这个对象来创建ProductB。虽然也修改了代码,但是仅仅限于创建Factory对象的地方,其他的业务逻辑部分并没有修改(业务逻辑指用到了Product类型的地方)
  3. 工厂方法的一种改进,叫做静态工厂方法,如果要创建另一种Product对象,把该过程写在工厂类的静态方法里,这在jdk类库里大量使用

3. 抽象工厂(Abstract Factory)

「创建一组对象」

上面的工厂方法其实并没有体现出好处,因为每个工厂只创建一种对象,但是如果需要创建一批对象,好处就体现出来了。抽象工厂方法适用于类似下列情形:比如要更换程序界面的显示风格,如窗口、按钮、滚动条等。如果不用抽象工厂,就得一个个对象都替换掉,这显然不简单。实际上高层业务逻辑也只是针对这些窗口或者按钮的接口进行编程,抽象工厂可以很容地把这些对象立刻换成另外一批。

  • 设计:
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
//窗口对象接口
interface Window{}

//某种风格A的窗口
class WindowA implements Window{}

//按钮对象接口
interface Button{}

//某种风格A的按钮
class ButtonA implements Button{}

//工厂接口
interface Factory{
//创建窗口
Window createWindow();
//创建按钮
Button createButton();
}

//生产某种风格A对象的工厂
class FactoryA implements Factory{
@Override
public Window createWindow(){
return new WindowA();
}
@Override
public Button createButton(){
return new ButtonA();
}
}
  • 客户端
1
2
3
Factory facory = new FactoryA();
Window window = factory.createWindow();
Button button = factory.createButton();

4. 原型模式(Prototype)

「对象可以自身复制出新的对象」

适用场景:创建的对象很多属性都差不多,只需要在其他对象上修改一小部分。很多编程语言支持这种方式创建对象,比如java中每个对象都有clone()方法,那么在这样的语言里,不用专门写这种设计模式、利用自带方法即可。

  • 设计
1
2
3
4
class A{
//复制自己的实例
A clone(){}
}
  • 客户端
1
2
3
A a = new A();
A b = a.clone();
//下面b可修改一些属性,成为一个新的对象

5. 建造者模式(Builder)

对象的创建其实是一个非常复杂的过程,可能需要先创建一些零件,最后再把这些零件组装起来,建造者模式把组装这个行为分离出来,使得相同的零件,可以用不同的组装方法。

  • 设计
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/*创建一个个零件类*/

//假设A、B是零件的接口
class Builder{
A createA();
B createB();
}

//定义零件组装过程
class Director{
Builder builder;

Director(Builder builder){
this.builder = builder;
}

void assembly(){
A a = builder.createA();
B b = builder.createB();
//下面就是根据某种方法,将a和b组装起来
}
}
  • 客户端
1
2
3
Builder builder = new Builder();
Director director = new Director(builder);
director.assembly();

说明

这里的Builder和Director完全可以定义成接口,利用前面讲的工厂方法等来提供,也就是说设计模式之间是可以组合使用的,最终可以使得客户端中不存在new这种显示创建对象的方法。

小结

  • 创建型模式是设计模式的基础,只有把这个问题解决了,上层模块才能完全对接口进行编程,想要哪个对象,直接调用某个创建型模式对象的方法就可以了。细心看上面的例子可以发现:尽管业务对象可以通过工厂类对象等创建,没有显示出现new关键字,但是工厂类对象本身却出现了new关键字
  • 有==两种方法可以彻底消灭new==:
    1. 把创建这些工厂类的代码聚集到一起,使变化局限于局部;
    2. java中有“反射”机制,可以把创建工厂类的方式通过配置文件来提供,这样就完全避免了修改代码,完全避免了代码中出现new,这是最佳的解决方案:即工厂对象由配置文件创建,业务对象由工厂对象创建。很多框架,例如==Spring==,就是采取这种方式来提供对象,它有个时髦的名字叫做“==依赖注入==”。

二、结构模式

「适配器、桥接、组合、装饰、外观、享元、代理」

结构型模式最能体现设计的精妙,从中可以学到不少智慧。针对特定的问题,这些模式的解决思路很有借鉴启发的作用。

1. 适配器(Adapter)

「转接口」

适用于这样的情况:用户(或者高层业务逻辑)需要一种接口来调用某种功能,手头已经有能够实现这种功能的类,但是接口与用户需要的不一致,这时候就可以创建一个类似于“转接口”的类使得用户接口和已有库的接口能够对接,而不是仅仅因为接口不一致而重新开发。

  • 设计
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//客户想要的接口
interface A{ void function(); }

//已经存在的接口
interface Old{ void func(); }

//设计一个适配器
class Adapter implements A{
Old o;

@Override
function(){
o.func();
}
}
  • 客户端
1
2
3
A a = new Adapter();
a.function();
//对于客户来说,它并不知道,也没有必要知道这期间是否进行了转接,能用就行

2. 桥接(Bridge)

「解决类多方向发展带来的结构僵化问题」

桥接模式是相对比较晦涩的一个,它并不是明显针对某个问题,我觉得他的思想更重要。假设有这样的情形:一开始需要设计一个运行在PC上软件,能够显示文本和图像,则可以设计一个接口:interface Software { void show(); },以及两个子类:class TextSoftware implements Software{},class ImageSoftware implements Software{},这个时候结构并没有什么问题;下面又有了新需求,把这个功能移植到手机上,那么有人可能会这样设计:TextSoftware做两个子类:TextSoftware4Pc和TextSoftware4Phone,ImageSoftware也做两个子类:ImageSoftware4Pc,ImageSoftware4Phone,这样貌似也能解决问题,但是这会带来僵化问题:如果现在又有了新的需求,定制两个Software版本,一个为成人的,一个为儿童的,当然也要既有Pc的,也有手机的,那么在上面的基础上继续继承的话,可以这么做:TextSoftware4Pc下面派生两个子类TextSoftware4PcAdult和TextSoftware4PcChildren,TextSoftware4Phone派生两个子类TextSoftware4PhoneAdult和TextSoftware4PhoneChildren,ImageSoftware4Pc派生两个子类ImageSoftware4PcAdult和ImageSoftware4PcChildren,ImageSoftware4Phone派生两个子类ImageSoftware4PhoneAdult和ImageSoftware4PhoneChildren。从这里应该就可以隐约感觉到,这种基于继承的结构,已经开始存在危机了,如果再增加新的需求,这个类继承层次就会非常的复杂,给软件以后维护带来很大的难度。分析下需求的变化可以看到,需求同时朝着三个方向变化:1.文本显示功能和图像显示功能;2.Pc平台和手机平台;3.成人版本和儿童版本;这三个方向的高层逻辑变化各自是独立的,彼此之间并没有先后继承关系,只是在最后整合的时候才会关联。桥接模式就是针对这种一个类同时朝着几个方向发展的结构问题,它根据每个变化方向设计一个接口,分别演化,各方向之间通过组合的方式结合在一起,这样的结构就清晰多了。

  • 设计
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//软件功能
interface Software{ void show(); }
class TextSoftwareimplements Software{
@Overrride
void show(){}
}
class ImageSoftware implements Software{
@Override
void show(){}
}

//使用群体
interface User{}
class AdultUser implements User{}
class ChildrenUser implements User{}

//平台
interface Platform{}
class PcPlatform implements Platform{
Software soft;
User user;
//下面可以自己组合这些业务元素
}

注意

我这里是以平台为基准进行组合,也可以以使用群体为基准进行组合,或者说组合的方式是灵活的,可以很容易变化。每个变化方向之间通过“组合”的方式,类似于在各个方向之间建立了一座桥梁,故这个模式叫做桥接模式。这个模式的意义在于:它说明了==继承的劣势==,==组合的优势==,实际上,每一个类都需要在其他类的基础上进行工作,有时候需要进行功能方面的继承,有时候需要进行结构方面的继承,正确的方式应该是:继承用于功能继承,组合用于结构继承,即继承是为了使用父类中的方法和字段,省的子类再重复地写,而组合是为了使用父类的结构,好像零件的组装,把一个个零件做好了之后装在一起,然后又可以拆卸,而不是一股脑把所有零件浇筑在一起。所谓组合优于继承,就是指这个。

3. 组合(Composite)

「表示具有层次关系的一类结构」

组合模式相对来说好理解一些,数据结构中的树就是典型的组合模式,树的每一个结点都可以包含另一些结点

  • 设计
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
//结点接口
interface Node{
void add(Node n);
void remove(Node n);
}

//叶子结点
class Leaf implements Node(){
@Override
public void add(Node n){}

@Override
public void remove(Node n){}
}

//非叶子结点
class Stem implements Node{
private List list;

@Override
public void add(Node n){
list.add(n);
}

@Override
public void remove(Node n){
list.remove(n);
}
}
  • 客户端
1
2
3
4
5
6
7
8
9
Node root = new Stem();
Node oneStem = new Stem();
Node leaf1 = new Leaf();
Node leaf2 = new Leaf();
Node leaf3 = new Leaf();
oneStem.add(leaf1);
oneStem.add(leaf2);
root.add(oneStem);
root.add(leaf3);

4. 装饰(Decorator)

「给子类方便地添加一些功能」

适用于这种情况:一个类要添加一点点功能,这种功能可以动态地随时添加,随时删除,这时候就不能用派生子类的方法,因为一旦派生一个子类,里面的结构就是静态的,而且本身也不鼓励使用继承。比如说有了一个窗口类,现在仅仅是要给这个窗口加个边框,或者换一个边框,但不能改变窗口类的接口,也就是说,加或者没加边框,用户看起来都是同一个接口。

  • 设计
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
//窗口
interface Window{ void display(); }
//具体的一个窗口
class AWindow inplements Window{
@Override
public void display(){}
}
//一个装饰类
class Decorator implements Window{
// 包含一个窗口
Window win;
//区别代理模式:往往被装饰的对象由构造器传入,而代理模式则完全隐藏内部对象的构造过程
public Decorator(Window win){
this.win = win;
}

@Override
public void display(){
//先加一个边框
addBorder();
//展示本来win里面的内容
win.display();
}
private void addBorder(){}
}
  • 客户端
1
2
Window a = new Decorator(); //这里不考虑怎么给内部win对象的赋值,这个属于创建型模式需要解决的问题,下同
a.display();

注意

从这里可以看到,a对象根本没觉察到赋给它的那个对象是原始对象还是装饰过的,反正接口是一样的。装饰模式在java库里有很多应用,比如说io包里的各个输入输出流都是包装过的,采用了装饰模式。这里的“装饰”可以有很多应用场景,比如说j2ee里的Servlet类是用来处理客户端http请求的,那么我们实际上可以设计一个Servlet的装饰类,让它做一些前期处理,或者后期处理,这个概念类似于过滤器(但过滤器自身的前置和后置处理是采用了职责链模式[以后介绍])。

5. 外观(Facade)

「把一组功能集中起来,提供一个统一的接口」

外观模式相对比较好理解,客户需要操作一组功能,需要牵涉到好几个类或者接口,那么我们可以把这些接口集中起来,通过一个类来向用户提供。这个模式的更大意义是,这个外观类可以更好地组织那些功能,以更好的方式提供给客户。

比如说需要构建一个软件,构建是一个系统工程,需要进行预处理、编译、链接、部署等等,但对于客户来说,他可以亲自动手一步步做,也可以交给一个构建程序(比如make)来做,他只要调用基本的命令(make)就行,以后的任务交给make来做,而且make做的比他本人一步步做可能效果更好

  • 设计
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
//预处理
interface Prepare{ void doPrepare(); }
//编译
interface Compile{ void doCompile(); }
//链接
interface Link{ void doLink(); }
//清除
interface Remove{ void doRemove(); }

//外观类
class Facade{
Prepare p;
Compile c;
Link l;
Remove r;

public void build(){
p.doPrepare();
c.doCompile();
l.doLink();
}

public void remove(){
r.doRemove();
}
}
  • 客户端
1
2
3
4
5
Facade f = new Facade();
//构建软件
f.build();
//清除软件
f.remove();

注意

Facade类与各个功能类之间是组合关系,是松耦合的,很容易变化

6. 享元(Flyweight)

「有效解决使用大量重复的小对象的问题」

享元模式适用于这种情况:比如说设计一个文档编辑器,里面的基本对象是字符,简单的想法是每个字符为一个对象,存储字符的值以及放置的位置。这样似乎解决了问题,但是还是有可以优化的地方:一篇文档中存在大量相同的字符,相对应的这些字符对象仅仅是位置不同,这里似乎可以用“共享”的方式:如果遇到一个字符,先看这个字符对象是不是存在,不存在就创建,如果已经存在了,就使用这个对象,修改一下位置的值,然后画出这个字符。

具体在实现享元模式时,可以指定对象是否可以共享,如果不能共享,就每次都创建新对象。

  • 设计
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//字符接口
interface Character{}
//两类字符对象:可以共享的;不可以共享的
class ShareCharacter implements Character{}
class UnshareCharacter implements Character{}

//创建享元对象的工厂
class CharacterFactory{
Character getChar(char c){
Character c;
//根据某些条件判断,是共享一个对象,还是生产一个新的对象
...
return c;
}
}
  • 客户端
1
Character ch = CharacterFactory.getChar('a');

注意

这里客户端只是调用工厂方法来生产字符对象,但并不知道这个字符是新创建的对象,还是已经存在的对象

7. 代理模式(Proxy)

「控制对一个对象的访问」

适用于这种情况:要访问一个对象,但又要做一些限制或者控制,或者不能直接访问到这个对象,好比网络服务中的代理服务器。那么可以在访问者和被访问者之间建立一个代理类,访问者向代理类发出请求,由代理类负责来访问。

  • 设计
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//需要访问的对象接口
interface Subject{ void operation(); }
//实际访问的类
class RealSubject implements Subject{
@Override
public void operation(){}
}
//代理类,提供相同的接口
class Proxy implements Subject{
Subject real;

@Override
public void operation(){
//做一些连接等准备工作
...
//调用真正需要访问的类的功能
real.operation();
}
}
  • 客户端
1
2
Subject obj = new Proxy();
obj.operation();

三、行为模式

「职责链、命令、解释器、迭代器、中介者、备忘录、观察者、状态、策略、模板方法、访问者」

行为模式关注的是对象间的一些协作关系。

1. 职责链(Chain Of Responsibility)

「任务的转发」

适用于以下的场景:客户给出一个任务,但并不知道、也不想知道这个任务谁去执行,于是交给一个链实例,这个链实例会判断任务是否由其本身完成,如果不是,就转发给下一个实例,依次类推,直到有实例处理它或便利了所有的链实例都询问完成

网络中的数据包传递,就是这个原理,比如我要把一个消息发到某一个ip,我其实并不需要直到这个ip究竟在哪里,只要发给路由器,路由器判断这个数据包是否属于我管的这个范围,如果是就收下来,转发给下面的子网,如果不是就转发给另一个路由器;子网里面也是这个工作方式,数据包被一站一站地转发,直到最后到达目标ip的计算机。

更为贴切web开发的例子是web开发中的过滤器,FilterChain和chain.doFilter这样的特征代码就是典型的职责链模式

另一个例子是阿里的Druid数据库连接池,使用该模式对JDBC做了扩展

  • 设计
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//操作总接口
interface Handler{
//转发
void transfer(Handler next);

//处理消息
void operation();
}
//具体类
class HandlerA impleements Handler{
@Override
public void transfer(Handler next){}

@Override
public void operation(){
//先判断是否由自己来处理
if(isMyDuty){
doSomething();
}else{
//如果不是,则转发
transfer();
}
}
}
  • 客户端
1
2
Handler start = new HandlerA();
start.operation();

注意

可以看到,客户只要简单地把任务交给第一个类,下面就可以自动地进行转发和处理,客户无需关心具体是哪个类来处理的,或者是消息是如何发到那个类的,因为接口都是一致的。

2. 命令(Command)

「暂缺,该模式用的不多」

  • 设计
1

  • 客户端
1

注意

3. 解释器(Interpreter)

「给定一种语言,定义一个解释器来解释」

可以用在以下场景:如果要设计一个编程语言的解释器,该语言(简单点)具有两种符号:终结符和非终结符,非终结符中包含有其他的符合序列。

  • 设计
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 语言的上下文
class Context{}

// 解析接口
interface Expression { void interpret(Context text); }

// 终结符解释方法
class TerminalExpression implements Expression{
@Override
public void interpret(Context text){}
}

// 非终结符解释方法
class NonterminalExpression implements Expression{
// 非终结符里包含有其他符号
List<Expression> exp;

@Override
public void interpret(Context text){}
}

4. 迭代器(Iterator)

适用于以下场景:客户需要依次遍历一个数据集合中的元素,但并不清楚这个集合里元素的排列顺序,也不想知道。如果知道了这个集合中的结构,把遍历方法写死在代码里,那么反而不好。迭代器模式给出了遍历集合元素的一个接口,同时又不暴露集合内部的实现细节。

再进一步,采用直接访问集合元素的方法,即使集合结构没有变化,也会导致问题,比如说利用数组实现的线性表存储元素,一开始我们通过数组下面遍历里面的元素,后来需求变了,要求倒序访问,那么遍历的代码就要修改,这违反了开闭原则。而如果使用迭代器的话,客户代码无需变化,因为客户根本就不知道这些元素是什么顺序提供的。

  • 设计
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
// 迭代接口
interface Iterator{
Object next();
boolean hasNext();
}

// 数据元素集合
interface DataSet { Iterator getIterator(); }

// 数据元素具体的实现
class DataSetA implements{
// 具体里面的数据表示省略了
// 得到一个迭代器
@Override
Iterator getIterator(){
return new DataSetAIterator(this);
}
}

// 专门为DataSetA定义的迭代器
class DataSetAIterator implements Iterator{
DataSetA data;

DataSetAIterator(DataSetA data){
this.data=data;
}

@Override
public Object next(){
// 按某种方法取得this.data中的下一个元素
}

public boolean hasNext(){
// 根据某种方法判断有没有下一个元素
}
}

  • 客户端
1
2
3
DataSet a = new DataSetA();
Iterator ite = a.getIterator();
Object obj = ite.next();

迭代器模式的实现相对是比较复杂的,所以还是最好参考UML类图为好,不过使用起来是很简单的,java语言内置了迭代器,比如说Collection框架中的所有数据结构类,都实现了Iterator接口,都可以直接使用jdk提供的迭代器。这23中设计模式里有一些是java语言内置提供的,从这个意义上说,这些模式应该是比较重要和常用的。

5. 中介者(Mediator)

「简化一系列对象间的通信交互」

面向对象中有一个单一职责原则,按照这个原则来编程,对象负责什么一目了然,但往往会形成很多个对象,对象间的通信会变得很复杂,因为每个对象都需要知道和它通信的是哪些对象,如果采取中介者,则对象与另一个对象的交互问题,可以交给这个中介者,对象之间可以不必知道对方是谁,在哪里。

  • 设计
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
// 中介者接口
interface Mediator{
void sendTo(String message, String name);
}

// 联合国是一个中介者
class MediatorUN implements Mediator{
//联合国需要知道每一个国家
List<Country> country;
public MediatorUn(List<Country> country){
this.country = country;
}

@Override
public sendTo(String message, String name){
//根据name选择一个Country,然后发送消息
}
}

// 国家抽象类
abstract class Country{
Mediator med;

// 把消息message发送给名字叫name的国家
void send(String message, String name);
}

// 中国
class China extends Country{
@Override
void send(String message, String name){
// 把消息和转发对象发给中介者,自己并不需要知道另一
// 国家的具体情况
this.med.sentTo(message, name);
}
}

// 美国
class USA extends Country{

@Override
void send(String message, String name){
this.med.sentTo(message, name);
}
}

有了中介者模式,对象间的交互就变得非常简单,简单地把消息交给中介者就行了。

6. 备忘录(Memento)

游戏都能存档读档,但你想过这个是如何实现的吗?备忘录模式就是来做这个的

  • 设计
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
// 备忘录,或者是存档
class Memento {
// 对象的状态,简单的用String表示
String state;

void setState(String state){
this.state=state;
}

String getState(){
return this.state;
}
}

// 存档管理者
class MemManage{
// 创建一个存档对象
Memento createMemento(){
return new Memento();
}

// 保存
void saveMem(Memento mem){}

// 获取某一个存档
void getMem(/*参数省略了,取决于实现方法*/){}
}

// 游戏程序,可以存档
class Game{
String state;

// 存档
void save(Memento mem, String state){
mem.setState(state);
}

// 读档
void load(Memento mem){
this.state=men.getState();
}
}

  • 客户端
1
2
3
4
5
6
7
Game game=new Game();
MemManager manager=new MemManager();
//存档
Memento m=manager.createMemento();
game.save(m, “存档1”);
manager.savaMen(m);
// 读档同上

7. 观察者(Observer)

监听

这个模式比较好理解,Swing里面的控件监听机制就是采用这个实现的。一个对象A需要监听另一个对象B的状态,而对象C也在监听B的状态,怎么采取一种好的方法,来实现B的状态变化之后,会通知到A和C,让它们做相应的动作,这便是观察者模式。A和C是观察者,B是被观察者。

  • 设计
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
// 观察接口,所以观察者都要实现该接口
interface Observer { void operation(); }

//观察者A
class ObserverA implements Observer{
@Override
public void operation(){
// 收到通知后做相应的事
}
}
//观察者B
class ObserverB implements Observer{
@Override
public void operation(){
// 收到通知后做相应的事
}
}

// 被观察者接口
interface Subject {
// 加入一个观察者
void add(Observer o);

// 删除一个观察者
void del(Observer o);

// 通知观察者
void notify();
}

// 被观察对象C
class SubjectC implements Subject{
// 比如用一个线性表存储观察者们
List<Observer> obs;

@Override
public void add(Observer o){
obs.add(o);
}

@Override
public void del(Observer o){
obs.remove(o);
}

@Override
public void notify(){
int len=this.obs.length;
for(int i=0; i<len; i++){
Observer tmp=this.obs.get(i);
tmp.operation();
}
}
}

  • 客户端
1
2
3
4
5
6
7
8
9
10
11
12
// 建立被观察对象
Suject sub=new SubjectC();

// 建立两个观察者
Observer a=new ObserverA();
Observer b=new ObserverB();
// 向被观察对象注册
sub.add(a);
sub.add(b);

// 发生了一些事件,被观察者通知各观察者进行各自相应的动作
sub.notify();

观察者模式是一个重要的接口,java swing里面的事件监听机制就是使用这个模式的典型代表,swing里面把“观察”叫做“监听”,比如说一个按钮按下之后,一个Label需要显示一些文本,另一个Label需要显示图像,那么按钮就是一个事件源,两个Label就是监听器。可以在按钮里这么写button.addActionListener(new ActionListener()); 这里的addActionListener()方法就相当于这里例子里的add,ActionListener就是这里的Observer接口。Java里面也自带了观察者模式的接口和类,可以直接使用,但我感觉并不实用,一来这些类的接口定死了,不适用于具体情况,二来这个模式很好写,没必要用系统带的。

8. 状态(State)

考虑这样一个应用:设计一个汽车前灯的开关程序,它有三个状态变化:关->近光灯->远光灯->关,也就说每按一下,就会变成下一个状态,你会如何设计?如果不考虑面向对象的设计,一般人都会这么设计:用if语句来判断,如果当前是“关”,则按下之后,变为近光,如果是近光,按下之后变远光。。。,这种做法有两个缺点:1、会有很多if语句,2、难以适应新的变化,比如中间又加了两个新状态(朝左照射和朝右照射),那么众多if语句就要做不少修改。

如果又加了一个新需求,要让用户能够自定义转换的顺序,比如说从关到远光,那么原来的if语句的代码,基本就要废弃重写,而状态模式可以轻松解决上述问题。

  • 设计
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
// 定义状态接口
interface State {
void operation(Switch s);
}

// 定义三种状态
class Off implements State{
@Override
public void operation(Switch s){
// 执行关灯的操作
s.doOff();
// 改变一下状态
s.state=new Near();
}

}

class Near implements State{
@Override
public void operation(Switch s){
// 执行近光灯的操作
s.doNear();
// 改变一下状态
s.state=new Far();
}
}

class Far implements State{
@Override
public void operation(Switch s){
// 执行远光灯的操作
s.doFar();
// 改变一下状态
s.state=new Off();
}
}

// 开关类
class Switch {
State st;

Switch(State st){
this.st=st;
}

// 定义按下开关的方法
void press(){
st.operation(this);
}

// 定义三个操作
void doOff(){}
void doNear(){}
void doFar(){}
}

  • 客户端
1
2
3
4
5
6
//初始状态为关
Switch swi=new Switch(new Off());
// 按下三次开关
swi.press();
swi.press();
swi.press();

状态模式把各个状态变成一个个局部实体,并把将来可能的变化都限制在局部范围里,把各个状态实体与客户端的代码解耦,使得状态类的修改,并不会引起客户端的变化。

注意:下面这种设计

  • 设计
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Switch{
void press(){
if(状态关){
关灯
改状态为近光灯
}
else if(状态近光灯){
开近光灯
改状态为远光灯
}
else if(状态远光灯){
开远光灯
改状态为关
}
}
}
  • 客户端
1
2
3
4
5
Switch swi=new Switch();
// 按下三次开关
swi.press();
swi.press();
swi.press();

看起来如果修改了Switch类,客户端的代码并没有动,对吗?这只是从代码书写层面看没动,但是实际上修改了Switch类,Switch类当然会被重新编译,但是客户端的代码也要因此重新链接,所以实际上客户端是被影响的,这一点注意。

反过来看上面的状态模式的例子,由于客户端的是针对接口编程的,因此并不需要重新链接,Switch里的press()方法中的operation()方法是“动态绑定”或者叫“延迟绑定”的(也就是多态),当运行时才会去寻找究竟是哪个对象。所以即使修改了State对象,客户端的代码并没有受影响。这才是真正的松耦合,也是为什么多态是面向对象的核心机制。

9. 策略(Strategy)

「把算法部分独立出来」

这是个很有价值的模式,考虑这样一个软件的设计:要编写一个商场收银软件,商场的商品定价机制是很灵活的,经常会根据节假日或者什么原因临时将商品打折,打折的幅度和计算方法也会经常变,那么就给代码编写带来了难度,按照以往常规的方法,会采用很多if语句来写,这显然是一种不好的方法,一个商场有成千上万种商品,每一种商品会有很多折扣方法,难道要写成千上万个if语句吗?面向对象设计的一个原则就是:把可能变化的部分独立成接口,进行灵活的扩展,策略模式正是体现了这一点。

  • 设计
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
// 算法接口
interface Strategy {
double getPrice();
}

// 正常收费
class Normal implements Strategy{
@Override
public double getPrice(){ }
}

// 打折价格
class Discount implements Strategy{
@Override
public double getPrice() { }
}

// 收费程序
class Casher{
Strategy strategy;

void setStrategy(Strategy strategy){
this.strategy=strategy;
}

// 得到商品总价
double getMoney(int n){
return n*this.strategy.getPrice();
}
}
  • 客户端
1
2
3
4
5
6
7
Casher cash=new Casher();
//正常收费
cash.setStrategy(new Noral());
cash.getMoney(2);
// 换成打折收费
cash.setStrategy(new Discount());
cash.getMoney();

策略模式的结构非常简单,所以往往用来作为设计模式入门的例子,当然这里的setStrategy方法还是比较原始的,如果结合工厂方法等,就更加好了。

10. 模板方法(Template Method)

适用于这样的情况:有个复杂算法,由许多小算法组成,大致的框架已经可以确定,但是这些小算法需要到运行时才能确定调用哪个,或者说需要临时更换成另一个小算法。简而言之就是框架已经定了,细节还没定。模板方法就是用来解决这个问题,这同时也是很多类库和框架的设计思路,在传统的面向过程程序设计中,由于是上层函数调用下层函数,因此实际上,上层框架逻辑函数是依赖于底层实现的,底层函数没写好,上层就不能完成编译,底层函数修改了,上层函数也要被重新链接,这明显是不合理的。面向对象由于采取了多态机制,因此上层框架可以独立出来,不受底层的影响,要替换掉底层,也不会影响上层框架。

考虑一个做菜的程序,比如要做鱼汤,大致步骤是确定的:先放油,再煎鱼,再放水,再放调料,但是具体放多少油,放多少水,用什么鱼,这个具体再说,或者也可以先把鱼定下来,其他部分以后再说,这个看具体设计。

  • 设计
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
// 抽象类,没定下来的操作作为抽象方法
abstract DoFish{
void operation(){
// 放油
putOil();

// 放鱼
putFish();

// 放水
putWater();

// 放调料
putFlavour();
}

abstract void putOil();
void putFish(){
// ...
}
abstract void putWater();

abstract void putFlavour();
}

// 一种具体的做法
class OneDoFish extends DoFish(){
@Override
void putOil(){}

@Override
void putWater(){}

@Override
void putFlavour(){}
}
  • 客户端
1
2
DoFish o=new OneDoFish();
o.operation();

11. 访问者(Visitor)

这应该是23中模式中最复杂的一种了,但是和桥接模式一样,可以从中受到很大的启发。

总结

现有的这23种模式已经介绍完了, 我们应该注重体会各个设计模式的使用场景以及设计初衷,然后在适当的地方使用它们,做到物尽其用。

虽然这里介绍的是代码的设计模式,但其实我们应该站在宏观的角度看___设计模式___这个词。宏观来看,我们在生活和工作中___反复实践___总结出的经验都可以是设计模式,这里强调了___反复实践___,在实际应用场景中反复实践求证,这样出来的经验总结才能称作是设计模式,指导我们做好我们要做的事情。

0%