踩坑记3—CXF ws服务端改用SpringWs实现

出于一些项目上的原因,此前将原有的CXF Webservice服务端改用SpringWs实现,中间遇到些琐碎的小问题,造成了一些困扰,在这里回顾总结一下。

实现Spring Ws的方式请参考官方指南
指南上的步骤相当于在白纸上作画,只需要定义一个符合标准的xsd,后续注入相关的Bean就可以了。但是对于需要保持原调用不变的我来说,就像是戴着镣铐跳舞了。
改写的大致步骤如下:

  1. 从原实现的wsdl中截取接口定义,放到xsd文件中;
  2. mvn xjc生成相应javaBean
  3. 写一下自己的Endpoint、WebServiceConfig
    运行之后,服务的.wsdl就可以访问了。
    但是呢,SoapUi根据这个.wsdl生成的用例必然会报错。
    开启调试模式,看一下报错信息,了解下SpringWs的请求处理机制:
    98hiuD.png
    在EndpointAdapter这里调试,发现Request对应的Bean没有被标记为XmlElement,就被拒绝掉了。
    手动在bean上面加上注解之后,继续调试,在SuffixBasedMessagesProvider.isMessageElement()返回false,又被拒了。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    @Override
    protected boolean isMessageElement(Element element) {
    if (super.isMessageElement(element)) {
    String elementName = getElementName(element);
    Assert.hasText(elementName, "Element has no name");
    return elementName.endsWith(getRequestSuffix()) || elementName.endsWith(getResponseSuffix()) ||
    elementName.endsWith(getFaultSuffix());
    }
    else {
    return false;
    }
    }

从代码上看出是因为请求没带默认后缀Request。但是又不能强制原调用方修改请求格式,所以只能在服务端把Request后缀匹配去掉。
看了下DefaultWsdl11Definition的方法,有个setRequestSuffix可以自定义后缀,但是比较尴尬的是:

1
Assert.hasText(requestSuffix, "'requestSuffix' must not be empty");

所以,如果使用DefaultWsdl11Definition的话,Request后缀必须有,而CXF是根据service中的方法名定义Request的,一般没有统一的后缀。
因此,需要自己实现一个Wsdl11Definition,替换掉关于后缀的几个方法。

1
2
3
private final SuffixBasedMessagesProvider messagesProvider = new SuffixBasedMessagesProvider();

private final SuffixBasedPortTypesProvider portTypesProvider = new SuffixBasedPortTypesProvider();

另外,关于xsd生成的javaBean没有xmlElement系列的注解的问题,最好根据demo中的格式改写下xsd。

除了这两个问题,如果原接口方法中还带有前缀,比如:

1
2
3
4
5
6
7
8
<soapenv:Body>
<ws:addDevice>
<!--Zero or more repetitions:-->
<ws:indexCodes>?</ws:indexCodes>
<!--Optional:-->
<ws:vagIndexCode>?</ws:vagIndexCode>
</ws:addDevice>
</soapenv:Body>

还需要在对应的EndPoint加上 @Namespace(prefix = “web”,uri = NAMESPACE_URI)注解

总结

从CXF Webservice迁移到SpringWs需要注意:

  • 自定义Wsdl11Definition,接收无后缀的Request
  • xsd格式改成标准的,以生成可用的带有@XmlRootElement的Bean
  • 注意加上相应的方法前缀prefix

SpringBoot下解决webservice与Restful接口路径冲突

先来解释下前因后果吧,虽然是一个Long Long story。从前,这个web项目的框架结构是Spring+Struts2+myBatis,对外接口用Apache CXF实现,既有webservice又有restful。
由于cxf.path默认值为/services,所有这些ws和rest接口全都在services路径下。后来由于struts2频繁暴露安全漏洞以及前后端技术的换代需要,项目的框架升级到Spring (Boot+MVC+JPA)。
重构时做了业务拆分,webservice接口不再保留,其他rest接口改为Spring @RestController实现,路径保持不变。
再后来业务又有变动,webservice接口又要原封不动的添加进来,作为优秀的代码搬运工,首先想到的是把CXF集成到SpringBoot中来,然后把CXF ws server端代码迁移过来即可。
做法如下:

Google It,找到这篇官方指南,按照指南引入Spring Boot CXF JAX-WS Starter,配置一个javax.xml.ws.Endpoint,再参考官方Demo在原实现上加几个注解,最后跑一下测试用例,结果OK,感觉差不多完事儿了。

结果万万没想到,更新接口文档时,顺便在Swagger上试了下Rest接口,竟然返回404!看了下body中的错误信息:

1
<html><body>No service was found.</body></html>

接口是services路径下的,八成是被拦截丢给CXF处理了。
接下来进入调试模式,根据控制台打印的信息:

1
[http-nio-80-exec-7] WARN  o.a.c.t.servlet.ServletController- Can't find the the request for http://XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX's Observer

我在ServletController中相应位置打了个断点,然后再触发请求,进入断点后,查看线程执行的方法链,追溯到线程的起点,按调用顺序,逐步查看代码,必要时打断点调试。

93L48H.jpg

粗略的正常处理流程:
NIO监听到web请求->分配线程处理->Http处理器解析->CoyoteAdapter映射分发request->FilterChain过滤->调用相应的servlet分发到具体的业务代码->处理业务逻辑->返回

问题基本上是出在CoyoteAdapter映射请求的过程中,从CoyoteAdapter.service()开始断点排查,发现最终在Mapper.internalMapWrapper方法中完成request path与上下文信息的映射。

93XNhF.jpg

最后的方法代码如下:

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
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
/**
* Wrapper mapping.
*/
private final void internalMapWrapper(ContextVersion contextVersion,
CharChunk path,
MappingData mappingData)
throws Exception {

int pathOffset = path.getOffset();
int pathEnd = path.getEnd();
int servletPath = pathOffset;
boolean noServletPath = false;

int length = contextVersion.path.length();
if (length != (pathEnd - pathOffset)) {
servletPath = pathOffset + length;
} else {
noServletPath = true;
path.append('/');
pathOffset = path.getOffset();
pathEnd = path.getEnd();
servletPath = pathOffset+length;
}

path.setOffset(servletPath);

// Rule 1 -- Exact Match 精确匹配
Wrapper[] exactWrappers = contextVersion.exactWrappers;
internalMapExactWrapper(exactWrappers, path, mappingData);

// Rule 2 -- Prefix Match 前缀匹配
boolean checkJspWelcomeFiles = false;
Wrapper[] wildcardWrappers = contextVersion.wildcardWrappers;
if (mappingData.wrapper == null) {
internalMapWildcardWrapper(wildcardWrappers, contextVersion.nesting,
path, mappingData);
if (mappingData.wrapper != null && mappingData.jspWildCard) {
char[] buf = path.getBuffer();
if (buf[pathEnd - 1] == '/') {
/*
* Path ending in '/' was mapped to JSP servlet based on
* wildcard match (e.g., as specified in url-pattern of a
* jsp-property-group.
* Force the context's welcome files, which are interpreted
* as JSP files (since they match the url-pattern), to be
* considered. See Bugzilla 27664.
*/
mappingData.wrapper = null;
checkJspWelcomeFiles = true;
} else {
// See Bugzilla 27704
mappingData.wrapperPath.setChars(buf, path.getStart(),
path.getLength());
mappingData.pathInfo.recycle();
}
}
}

if(mappingData.wrapper == null && noServletPath) {
// The path is empty, redirect to "/"
mappingData.redirectPath.setChars
(path.getBuffer(), pathOffset, pathEnd-pathOffset);
path.setEnd(pathEnd - 1);
return;
}

// Rule 3 -- Extension Match 后缀匹配
Wrapper[] extensionWrappers = contextVersion.extensionWrappers;
if (mappingData.wrapper == null && !checkJspWelcomeFiles) {
internalMapExtensionWrapper(extensionWrappers, path, mappingData,
true);
}

// Rule 4 -- Welcome resources processing for servlets 静态文件匹配
if (mappingData.wrapper == null) {
boolean checkWelcomeFiles = checkJspWelcomeFiles;
if (!checkWelcomeFiles) {
char[] buf = path.getBuffer();
checkWelcomeFiles = (buf[pathEnd - 1] == '/');
}
if (checkWelcomeFiles) {
for (int i = 0; (i < contextVersion.welcomeResources.length)
&& (mappingData.wrapper == null); i++) {
path.setOffset(pathOffset);
path.setEnd(pathEnd);
path.append(contextVersion.welcomeResources[i], 0,
contextVersion.welcomeResources[i].length());
path.setOffset(servletPath);

// Rule 4a -- Welcome resources processing for exact macth
internalMapExactWrapper(exactWrappers, path, mappingData);

// Rule 4b -- Welcome resources processing for prefix match
if (mappingData.wrapper == null) {
internalMapWildcardWrapper
(wildcardWrappers, contextVersion.nesting,
path, mappingData);
}

// Rule 4c -- Welcome resources processing
// for physical folder
if (mappingData.wrapper == null
&& contextVersion.resources != null) {
Object file = null;
String pathStr = path.toString();
try {
file = contextVersion.resources.lookup(pathStr);
} catch(NamingException nex) {
// Swallow not found, since this is normal
}
if (file != null && !(file instanceof DirContext) ) {
internalMapExtensionWrapper(extensionWrappers, path,
mappingData, true);
if (mappingData.wrapper == null
&& contextVersion.defaultWrapper != null) {
mappingData.wrapper =
contextVersion.defaultWrapper.object;
mappingData.requestPath.setChars
(path.getBuffer(), path.getStart(),
path.getLength());
mappingData.wrapperPath.setChars
(path.getBuffer(), path.getStart(),
path.getLength());
mappingData.requestPath.setString(pathStr);
mappingData.wrapperPath.setString(pathStr);
}
}
}
}

path.setOffset(servletPath);
path.setEnd(pathEnd);
}

}

/* welcome file processing - take 2
* Now that we have looked for welcome files with a physical
* backing, now look for an extension mapping listed
* but may not have a physical backing to it. This is for
* the case of index.jsf, index.do, etc.
* A watered down version of rule 4
*/
if (mappingData.wrapper == null) {
boolean checkWelcomeFiles = checkJspWelcomeFiles;
if (!checkWelcomeFiles) {
char[] buf = path.getBuffer();
checkWelcomeFiles = (buf[pathEnd - 1] == '/');
}
if (checkWelcomeFiles) {
for (int i = 0; (i < contextVersion.welcomeResources.length)
&& (mappingData.wrapper == null); i++) {
path.setOffset(pathOffset);
path.setEnd(pathEnd);
path.append(contextVersion.welcomeResources[i], 0,
contextVersion.welcomeResources[i].length());
path.setOffset(servletPath);
internalMapExtensionWrapper(extensionWrappers, path,
mappingData, false);
}

path.setOffset(servletPath);
path.setEnd(pathEnd);
}
}


// Rule 7 -- Default servlet
if (mappingData.wrapper == null && !checkJspWelcomeFiles) {
if (contextVersion.defaultWrapper != null) {
mappingData.wrapper = contextVersion.defaultWrapper.object;
mappingData.requestPath.setChars
(path.getBuffer(), path.getStart(), path.getLength());
mappingData.wrapperPath.setChars
(path.getBuffer(), path.getStart(), path.getLength());
}
// Redirection to a folder
char[] buf = path.getBuffer();
if (contextVersion.resources != null && buf[pathEnd -1 ] != '/') {
Object file = null;
String pathStr = path.toString();
try {
file = contextVersion.resources.lookup(pathStr);
} catch(NamingException nex) {
// Swallow, since someone else handles the 404
}
if (file != null && file instanceof DirContext) {
// Note: this mutates the path: do not do any processing
// after this (since we set the redirectPath, there
// shouldn't be any)
path.setOffset(pathOffset);
path.append('/');
mappingData.redirectPath.setChars
(path.getBuffer(), path.getStart(), path.getLength());
} else {
mappingData.requestPath.setString(pathStr);
mappingData.wrapperPath.setString(pathStr);
}
}
}

path.setOffset(pathOffset);
path.setEnd(pathEnd);

}

上面的几种Wrapper都是根据web.xml或ServletRegisterBean中设置的Url Pattern产生的,用来根据请求路径映射相应的Servlet。对应规则如下:

  • path.endsWith(“/*”) : wildcardWrapper 通配符匹配
  • path.startsWith(“*.”) : extensionWrapper 扩展名匹配
  • path.equals(“/“) : defaultWrapper 默认Wrapper
  • 其他url Pattern : exactWrapper 精确匹配
    详见Mapper.addWrapper()方法。
    而实际的路径比对过程中,匹配规则的优先顺序是:
    exactWrapper>wildcardWrapper>extensionWrapper> welcomeResources(静态资源)>defaultWrapper。
    而此时的context中的只有defaultWrapper和值为/services的wildcardWrapper:

93XttU.md.jpg

根据path的值/context/services/xxxxServices/xxx/xxxx, 请求就被映射到了CXFServlet。

要保证接口路径不变的同时,解决路径冲突,比较好的办法就是从urlPattern着手,如果能将cxfServlet的匹配范围缩小,将rest接口排除在外就OK了。
在CXF合入SpringBoot的指南中有这样一句:

1
Use "cxf.path" property to customize a CXFServlet URL pattern

通过cxf.path自定义CXFServlet的匹配路径,在项目中搜索cxf.path,定位到mvn仓库的文件:

org/apache/cxf/cxf-spring-boot-autoconfigure/3.1.12/cxf-spring-boot-autoconfigure-3.1.12.jar!/META-INF/spring-configuration-metadata.json
内容:

1
2
3
4
5
6
7
{
"name": "cxf.path",
"type": "java.lang.String",
"description": "Path that serves as the base URI for the services.",
"sourceType": "org.apache.cxf.spring.boot.autoconfigure.CxfProperties",
"defaultValue": "/services"
}

找到CxfProperties类,查找path的引用,找到CxfAutoConfiguration:

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
@Configuration
@ConditionalOnWebApplication
@ConditionalOnClass({ SpringBus.class, CXFServlet.class })
@EnableConfigurationProperties(CxfProperties.class)
@AutoConfigureAfter(EmbeddedServletContainerAutoConfiguration.class)
public class CxfAutoConfiguration {

@Autowired
private CxfProperties properties;

@Bean
@ConditionalOnMissingBean(name = "cxfServletRegistration")
public ServletRegistrationBean cxfServletRegistration() {
String path = this.properties.getPath();
String urlMapping = path.endsWith("/") ? path + "*" : path + "/*";
ServletRegistrationBean registration = new ServletRegistrationBean(
new CXFServlet(), urlMapping);
CxfProperties.Servlet servletProperties = this.properties.getServlet();
registration.setLoadOnStartup(servletProperties.getLoadOnStartup());
for (Map.Entry<String, String> entry : servletProperties.getInit().entrySet()) {
registration.addInitParameter(entry.getKey(), entry.getValue());
}
return registration;
}

//......省略其余方法
}

我们可以看到CXF在c.path路径下注册了一个wildcardWrapper,所有该路径下的请求都会被拦截。
考虑到有ConditionalOnMissingBean的存在,可以注入一个cxfServletRegistration来覆盖cxf的默认实现,以下是我的代码:

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
@Configuration
public class WebServiceConfig {

private static final String SERVICE_PATH = "SysConfigService";

@Autowired
private CxfProperties properties;

@Bean(name = "cxfServletRegistration")
public ServletRegistrationBean cxfServletRegistration() {
String path = this.properties.getPath();

String urlMapping = path.endsWith("/") ? path + SERVICE_PATH : path + "/"+SERVICE_PATH;
String urlMappingWsdl = path.endsWith("/") ? path + SERVICE_PATH+"?wsdl" : path + "/"+SERVICE_PATH+"?wsdl";

ServletRegistrationBean registration = new ServletRegistrationBean(
new CXFServlet(), urlMapping,urlMappingWsdl);
CxfProperties.Servlet servletProperties = this.properties.getServlet();
registration.setLoadOnStartup(servletProperties.getLoadOnStartup());
for (Map.Entry<String, String> entry : servletProperties.getInit().entrySet()) {
registration.addInitParameter(entry.getKey(), entry.getValue());
}
return registration;
}

@Autowired
private Bus bus;

@Autowired
ICmsService cmsService;

@Bean
public Endpoint endpoint() {
EndpointImpl endpoint = new EndpointImpl(bus, new SysConfigService(cmsService));
endpoint.publish("/"+SERVICE_PATH);
return endpoint;
}
}

但是,发布的webservice地址变成了/services/SysConfigService/SysConfigService?wsdl,把Endpoint地址修改成endpoint.publish(“)之后,地址恢复正常。

然后,我们重写cxfServletRegistration的初衷是在不改变path的前提下修改cxfServlet的url Pattern。
显然,这个行不通,Endpoint发布的url地址并非由path决定,而是跟cxfServletRegistration的urlMapping有关。因此,这种方式虽然勉强解决了目前的问题,但仍有难以避免的缺陷:

  • 目前的方式相当于在services/SysConfigService路径下发布addr为””的Endpoint,且路径固定,无法添加新的webservice
  • 如果采取cxf.path =/services/SysConfigService的方式,则可以在SysConfigService的路径下扩展其它ws接口

调试了几遍Endpoint.publish的过程,没有发现与cxfServletRegistration有明显的关联,暂时放弃这方面的探索。
由于时间还算充足,没必要苟且妥协,我又研究了下SpringWs,发现对于每一个Webservice接口都可以通过Wsdl11Definition.setLocationUri(“/services/SysConfigService”)指定接口的地址,而不是必须置于某个指定路径下。
所以只需要在ServletRegistrationBean加入每个webservice的准确路径的urlMapping,即可避免拦截到Rest接口的请求。

1
2
3
4
5
6
7
@Bean
public ServletRegistrationBean messageDispatcherServlet(ApplicationContext applicationContext) {
MessageDispatcherServlet servlet = new MessageDispatcherServlet();
servlet.setApplicationContext(applicationContext);
servlet.setTransformWsdlLocations(true);
return new ServletRegistrationBean(servlet, "/services/SysConfigService/SysConfigService.wsdl", "/services/SysConfigService");
}

CXF Webservice切换到SpringWs的步骤改天再写。

读《深入理解Java虚拟机》1-内存管理

JVM中的内存区域划分

9ZHKpQ.png

  • 线程共享区
    • 堆 —— 存放Class实例和数组。
      • 新生代
        • Eden
        • Survivor
          • From
          • To
      • 老年代
    • 方法区(Permgen/MetaSpace)
      • 类信息
      • 运行时常量池
      • 静态变量
      • JIT编译代码等
  • 线程私有区
    • 程序计数器
    • 栈内存 默认-Xss1024K
      • 虚拟机栈
        • 栈帧
          • 局部变量表
          • 操作数栈
          • 动态链接
          • 方法返回地址等
      • 本地方法栈
  • 直接内存 (NIO DirectByteBuffer)

对象创建

  • 类加载检查
  • 分配内存
    • 指针碰撞:内存规整,以指针分界空闲内存,移动指针分配内存
    • 空闲列表:内存不规整,查列表分配、更新列表
    • 线程安全
      • (Thread Local Allocation Buffer)线程本地缓存分配,锁同步;-XX:+/-UseTLAB
      • CAS+失败重试
  • 生成对象头,filed置0,初始化
  • 对象引用入栈

垃圾收集GC

  • 哪些内存需要回收
    • 引用计数 (无法解决相互引用)
    • 可达性分析 - GCROOTS
      • 栈帧的局部变量表中的对象
      • 类静态属性引用的对象
      • 方法区常量引用的对象
      • 本地方法栈中引用的变量
    • 引用
      • 强->正常引用
      • 软->即将OOM时回收
      • 弱->有无弱引用都不影响回收
      • 虚->queue监听回收
    • 方法区回收
      • 废弃常量 - 没有引用
      • 无用的类 (-XX:+CMSClassUnloadingEnabled )
        • 无实例
        • classloader已回收
        • 其Class实例无引用
        • 没有任何反射调用
  • 什么时候回收
    • Minor GC : Eden空间不足以分配对象时触发
    • Major/Full GC : System.gc()或Minor GC解决不了问题时触发
  • 如何回收
    • 垃圾收集算法->分代收集
      • 新生代 :复制
        • Serial 单线程stw适用于新生代较小的场景,如桌面应用 (Client默认收集器)
        • ParNew 多线程版,一般与CMS共用,线程数=CPU数,-XX: ParallelGCThreads
        • Parallel Scavenge 与CMS不兼容
          • -XX:MaxGCPauseMillis 最大停顿时间->交互应用
          • -XX:GCTimeRadio x 吞吐量=x/(1+x) ->后台运算
          • -XX:+ UseAdaptiveSizePolicy自调节
      • 老年代 :标记 清除/整理
        • Serial Old 整理(Client默认收集器)
        • Parallel Old 多线程版,与Scavenge配合用
        • CMS(Concurrent Mark Sweep) 缩短停顿时间,适用于网站服务器
          • 线程数=(CPU+3)/4,适合多CPU服务器
          • 用户线程并发需预留空间 -XX:CMSInitiatingOccupancyFraction=80
          • 空间碎片问题 -XX:CMSFullGCsBeforeCompaction=1
            9GdyJH.md.jpg
    • G1 :标记整理+region间复制,实现可预测停顿
      • Region
      • 优先列表

类加载机制

java类加载简单说就是把.class文件转化为java.lang.Class对象的过程,这个过程由类加载器完成。

java.lang.ClassLoader 中与加载类相关的方法:

  • getParent() 返回该类加载器的父类加载器。
  • loadClass(String name) 加载名称为 name的类,返回的结果是 java.lang.Class类的实例。
  • findClass(String name) 查找名称为 name的类,返回的结果是 java.lang.Class类的实例。
  • findLoadedClass(String name) 查找名称为 name的已经被加载过的类,返回的结果是 java.lang.Class类的实例。
  • defineClass(String name, byte[] b, int off, int len) 把字节数组 b中的内容转换成 Java 类,返回的结果是 java.lang.Class类的实例。这个方法被声明为 final的。
  • resolveClass(Class<?> c) 链接指定的 Java 类。

loadClass和defineClass都可以动态加载class。

java类加载器的树状结构:

  • bootstrap class loader 加载java核心库rt.jar,由C实现
  • sun.misc.Launcher$ExtClassLoader 扩展类加载器,用于加载java扩展库
  • sun.misc.Launcher$AppClassLoader 系统类加载器,根据CLASSPATH加载java应用中的类,ClassLoader.getSystemClassLoader()。

9VJZz6.jpg

类加载机制特性:

  • 类加载器会缓存已加载的Class实例,多次调用loadClass(name),只会返回同一个实例
  • 相同name、相同ClassLoader的Class实例才是相同的Class
    • 保护了Java库的类型安全
    • 框架可以实现自己的类加载器来防止类名冲突(破坏双亲委托机制,部分委托java/sun等前缀)
  • 代理加载 调用父类的加载器进行加载
    • defineClass 定义加载器(defining loader)即 Class.getClassLoader();
    • loadClass 初始加载器(initiating loader)

Java8函数式编程学习

Java8中新增FunctionalInterface注解用来标记函数式接口,并且定义了4种基本函数式接口:

  • Function
    • R apply(T t); 接收T类型参数,返回R类型结果
    • default compose(Function before) 参数前处理
    • default andThen(Function after) 结果后处理
  • Predicate
    • boolean test(T t); 接收参数,返回布尔值
    • default and 逻辑与
    • default negate 逻辑非
    • default or 逻辑或
  • Supplier
    • T get();
  • Consumer
    • void accept(T t);
    • default andThen(Consumer after)

如果忽略 default方法的话,我们可以把function视为一个一元函数,单个输入,单个输出。
而Predicate则是固定结果类型为boolean的function,Supplier是一个无参函数,Consumer 无返回的函数。
其中,Function、Predicate、Consumer都有default方法支持连续式调用,而且都有Bi系列支持双参数。
另外,每种接口还有相应的基本类型接口。

示例

1
2
3
4
5
6
7
8
BiFunction<String,String,Integer> add =
(x,y)->Integer.parseInt(x)+Integer.parseInt(y);
Function<Integer,Integer> square = x-> x*x;
Predicate<Integer> predicate = x-> x>100;
BiPredicate<String,String> biPredicate = (x,y)->{
return predicate.test(add.andThen(square).apply(x,y));
};
System.out.println(biPredicate.test("5","6"));

这个例子没有加任何注释,但是应该还算通俗易懂。
相比于命令式编程,这种方式应该更符合我们的思维习惯。
通过函数式编程来定义运算的方式,而不再拘泥于过程中每一步对象的维护,
我们可以更多的关注、更清晰的体现运算逻辑,提升coding的体验和代码可读性。

写这个例子的过程,我甚至回想起了学生时代做数学题的感觉,如果现在的学生能写代码来解数学题,相信也是一种很有成就感的体验。