从一小段java代码乱码怎么解决看 Clojure 和 Java 解决问题的差异

Clojure Web 编程之安全篇 - 庄周梦蝶
生活、程序、未来
Clojure Web 编程之安全篇
at 10:53PM
最近关注这方面稍微多了点,大概总结下。
浏览器的安全机制:
:host、port、protocol、sub domain都能影响。
沙箱模型,比如 Chrome 的。
恶意网址拦截,现代浏览器基本上都有提供,Google也提供了查询黑名单。
Clojure的Web开发本质上是基于 Java 的 Servlet 模型,因此也同样遵循 Java 的安全编程模型。这里主要描述 Clojure 里的常见防御策略,具体的漏洞请参考《白帽子讲Web安全》等书籍。
包括 SQL 注入和其他类型的注入,比如XML、JavaScript甚至HTTP头。
任何情况下都应该避免拼接SQL语句,而应该使用参数化的SQL语句
如果使用[clojure.java.jdbc],使用占位符?替代参数:
(require '[clojure.java.jdbc :as j])
(j/query mysql-db
[&select * from fruit where appearance = ?& &rosy&]
:row-fn :cost)
如果使用,只要避免使用exec-raw执行拼接SQL语句,默认都是参数化SQL语句:
(select users
(fields :id :first (raw &users.last&))
(where {:first [like &%_test5&]}))
;;Or when all else fails, you can simply use exec raw
(exec-raw [&SELECT * FROM users WHERE age & ?& [5]] :results)
避免调用任何形式的eval函数,包括eval,read-string,read等函数。如果要读取客户端 clojure 结构的数据,请使用clojure.edn/read-string替代clojure.core/read-string,因为 edn 有明确的。
跟 XSS 攻击有关,要对任何用户输入并且需要渲染的内容做检测、清除或者转义危险标签等。
CRLF注入,通过提交带有CR和LF换行标记的字符串,达到XSS攻击的目的,通常是允许用户输出HTTP应答头的时候才会发生。因此,禁止任何形式的允许用户自定义HTTP头,如果非要这样,请注意替换\r和\n字符
本质上是利用用户输入漏洞,通过提交恶意脚本来达到攻击目的。因此,
对输入做处理
使用一个服务端 validator 框架,Clojure 里可以选择或者
对输入做 escape,每个模板库都有提供这样的方式,比如hiccup.util/escape-html等。
对输出做处理
目前大多数 Clojure 模板引擎都会对变量输出做 escape 处理。
确实需要富文本输出的,使用 的
库是最佳选择,它提供了多种可配置的策略来提供不同的安全级别需求。在Clojure里使用也不麻烦,一个简单封装:
(import '(org.owasp.validator.html Policy AntiSamy CleanResults))
(require '[clojure.java.io :as io])
(defonce ^Policy antisamy-policy (Policy/getInstance (io/input-stream (io/resource &antisamy-ebay-1.4.4.xml&))))
(defn sanitize-html
(let [^AntiSamy as (AntiSamy.)
^CleanResults cr (.scan as html antisamy-policy AntiSamy/SAX)]
(.getCleanHTML cr)))
Cookie处理
Cookie必须做到几点:
启用 ,这可以防止大多数 XSS 攻击,因为没办法简单地获取 cookie 值了。
不要设置 Domain 属性,防止网站下的其他子域名的安全漏洞影响到主站。
尽量不要设置 Expire 和 Max-Age 属性,防止浏览器本地磁盘保存 cookie。但是我们为了避免用户在一定时间重复登录,还是会设置的,不要设置太长的时间最好。
使用简单的 cookie 名称,比如id,防止泄露业务逻辑。
如果使用 HTTPS,设置 Secure 属性为true来加密传输 cookie 。
在 ring 里,使用ring.middleware.cookies/wrap-cookies中间件就可以启用 cookie,cookie就是一个普通的 map 结构,推荐的设置如下:
:http-only true
:secure true
:value &加密后的 cookie 值&
Cookie 的加密可以采用一些对称机密算法,但是密钥应该随机产生并且每个用户、每次登录都应该不同,还需要设置一个合理的有效期。更好的策略是 cookie 值本身不带有业务信息(防止直接被解密),而是随机产生的标示符,在服务端根据这个标示符从数据库或者缓存里获取有效的用户信息做认证和授权。
(跨站请求攻击)也是常见的安全漏洞。
在 Clojure 里我们需要注意这么几点:
避免ANY路由,尽量使用明确的语义的route,查询走GET,创建走POST,更新走PUT,删除走DELETE等。
防止CSRF攻击的常见手段:验证码、Refer头检测、token检测。最靠谱的还是Token检测,每次提交都在 Form 带上一个服务端随机产生的 csrf_token,在服务端检测提交的 token 是否有效,如果有效才允许做创建、更新或者删除等变更性的操作。当然这个检测依赖的前提是网站没有 XSS 漏洞。如果 cookie 都暴露给攻击者了,那防御也没有意义。
Clojure 里可以使用
这个 middleware 来做自动化检测。它提供了一些辅助方法来产生和检测 token。
对于 Ajax 请求来说,Angular 这个 JavaScript 前端框架内置了一套 ,只要你在 cookie 里种上X-CSRFToken值,那么他会在每次 AJAX 的 POST、PUT等请求上从 cookie 里获取并设置X-XSRF-TOKEN的 HTTP 头,你只需要在服务端检测这个 header 值是否跟 cookie 中的值是否一致就ok,我们可以写个 middleware:
(defn wrap-xsrf-token [handler]
(if (#{:post :put :delete} (:request-method req))
(if-let [xsrf (-& req :headers (get &x-xsrf-token&))]
(if (= xsrf (-& req :cookies (get &XSRF-TOKEN&) :value))
(handler req)
(fail 403 &Invalid access, invalid xsrf token.&))
(fail 403 &Invalid access, missing xsrf token.&))
(handler req))))
其中 fail 是一个返回错误应答状态码和信息的函数。当没有找到X-XSRF-TOKEN头,或者它的值跟 cookie 里的值不匹配的时候,我们都返回 403 状态码和错误信息。
本质上是利用隐藏的 iframe 框架实现的。关于他的客户端防御可以参考 owasp 的 。
针对服务端来讲,我们可以为服务端渲染的模板添加X-Frame-Options的,它可以是三个值:
DENY,完全禁止框架
SAMEORIGIN 允许当前同域的 frame 加载。
ALLOW-FROM uri
允许指定 uri 的 frame 加载。
根据你的实际需要来设置,通常推荐设置为SAMEORIGIN:
{:status 200
:headers {&Content-Type& &text/charset=utf-8&
&X-Frame-Options& &SAMEORIGIN&
&Cache-Control& &no-no-store&}
:body &html....&
在 nginx 里也可以为任何应答添加这个头:
add_header X-Frame-Options SAMEORIGIN;
跨域 AJAX 调用
HTML5 提出了 机制来支持跨域 AJAX 请求。它将 HTTP 跨域请求分为两类:
Simple 请求: 没有设置自定义HTTP头,并且Content-Type类型限制在application/x-www-form-urlencoded, multipart/form-data或者text/plain的范围内。这类请求可以直接发起,目标服务端通过origin头认证来决定是否接收请求,如果接受处理并应答,在应答里设置Access-Control-Allow-Origin头为通配符*或者请求里的origin值。
Preflighted 请求: 需要自定义HTTP头,或者Content-Type类型在application/x-www-form-urlencoded, multipart/form-data或者text/plain的范围之外,比如提交 JSON 或者 XML 数据,那么需要预先发起一次OPTIONS请求,查询服务端是否接收这次请求。服务端根据 method,path,origin等信息来决定是否接收这次请求,如果接受,响应 options 应答里要设置Access-Control-Allow-Origin,Access-Control-Allow-Methods,Access-Control-Allow-Headers,Access-Control-Max-Age这几个头信息,然后客户端再发起真正的请求,具体见 Mozilla 的
跨域是很方便,特别是对于提供 Open API 的服务来说,但是也需要注意几点:
不要响应所有路径的options应答,慎重选择应答的路径,(OPTIONS "/*" ...)是最差的安全实践。只应该允许对外开放的 API 的 options 请求。
Access-Control-Allow-Origin 的值尽量不要设置为*,而应该根据 options 请求的附带的信息来决定是否授权本次访问,如果授权访问,那么也应该设置成请求里的origin头的值,而非通配符。
限定Access-Control-Allow-Methods的范围在GET,POST,OPTIONS内,PUT和DELETE要不要开放需要仔细考量。
不要设置Access-Control-Allow-Credentials头,如果允许,那么跨域 AJAX 请求将被允许带上 cookie 和 Http Basic 认证信息,这通常是不合理的。
对于请求来源的origin判断,可以使用一些第三方服务来判断来源网站是否合法,比如
认证和授权
Clojure 类似于 Spring Security 的类库就是 ,基于角色的 ACL 权限模型。我没有研究仔细学习过。
如果没有那么复杂的权限模型,采用
机制就足够实现一个简单的认证和授权模型。 Session 的存储可以使用 cookie。如果 session 存储的数据比较多(比较差的实践),在没有集群服务情况下,内存或者磁盘存储也足够了,如果是集群模型,可以考虑使用 memcached 或者 redis,过去写过一个
就是基于 memcached 做存储的 ring session 中间件。
这篇里提到的def-signin-handler宏就是通过判断 cookie 的合法性来决定请求是否经过授权。
Clojure 里实现 OAuth2 服务端
综合考察下来
应该是最成熟的方案。提供了三种授权类型:
并且存储也完全可以自定义,适配自己的业务数据存储,提供了简单的页面实现,可自主定制。整体来讲,完成度和成熟度比较高。
Clojure 里实现 OAuth2 客户端
在实现 OAuth2 认证客户端的时候,需要注意:
必须通过 HTTPS 加密传输,无论是 OAuth2 提供方,还是自己的 callback 回调请求。
必须使用state参数,并且传入一个随机产生值,对 callback 返回的 state 中的值做检测是否相等。这个随机值应该有过期时间(通常在3 ~ 5分钟内),并且每次请求都应该不同。这是为了防范 。当然,前提是 OAuth2 Provider 必须能正确返回 state 参数,国内不少网站提供的 OAuth2 协议就没有很好地支持 state 参数。
Redirect和Forward
简单来讲,应该避免任何依赖用户输入的 redirect 和 forward 请求。
不应该使用用户传入的 URL 做跳转之类的 3xx 请求,这很容易被滥用。
注意几点:
文件类型采用白名单机制,只有符合指定 MimeType 的文件才允许上传。简单的办法是通过检测文件后缀,更安全的做法是检测文件内容,比如使用
指定上传目录,默认 会存储临时文件在系统的临时路径,通常是/tmp目录,你也可以通过(wrap-multipart-params handler :store (fn [file] ...))来自行决定存储文件到哪里。
对上传目录做安全限制,不允许执行权限。chmod -R a-x /upload-dir。
定期删除上传目录内的过期文件。
限制上传文件大小,可以在 Nginx 里配置(比如设置最大请求体为 10m):
client_max_body_size 10m;
Clojure的 rand 函数调用的是Math.random方法,实际调用的是java.util.Random类。
而在 Java 里产生更安全的随机数的推荐做法是使用 ,这里就自荐下我刚放出去的 。
它内部使用 ThreadLocal 缓存的 SecureRandom 做随机整数、字符串、byte数组生成,具体使用请看项目描述吧。
本来有一个开源的 ,不过它每次调用都重新创建 SecureRandom,并且没有提供 clojure.core 里的rand、rand-int和rand-nth的替代方法。
尽量使用 HMAC 算法替换 MD5 做加密签名或者数据完整性检查,参考 。
使用 java.security 提供的加密算法,而不是自己实现。
加密用的 salt 应该随机产生,每次不同,并且使用安全的随机数发生器。
更多请参考 《白帽子讲web安全》第11章。
书籍 《白帽子讲web安全》 《Java 加密与解密的艺术》
Posted by Dennis Zhuang
at 10:53PM
Copyright & 2016 - Dennis Zhuang -
Powered by开放所有函数,超时时间10秒,请使用最新版浏览器获取最好体验。
还原到默认code
import java.io.*;
class test
public static void main (String[] args) throws java.lang.Exception
System.out.println("hi");
import java.io.*;
class test
public static void main (String[] args) throws java.lang.Exception
System.out.println("hi");
run (ctrl+r)
分享当前代码出现故障,请使用这个
文本方式显示
html方式显示
浏览器支持
The desktop versions of the following browsers,
in standards mode (HTML5 &!doctype html>
recommended) are supported:
Firefoxversion 4 and up
Chromeany version
Safariversion 5.2 and up
Internet Explorerversion 8 and up
Operaversion 9 and up
& Company 2014原来还能这样评价编程语言!-马海祥博客
新型SEO思维就是从一个全新的层次上提升seo优化的水平,达到网络信息最佳化的展示效果!
> 原来还能这样评价编程语言!
原来还能这样评价编程语言!
时间:&&&文章来源:马海祥博客&&&访问次数:
最近看到微博上又开始评价编程语言的问题了,说实话有些人对编程语言的评价要么就是太刻板了,要么就是太专业了,一般人根本就听不懂,如果你不服气的话,那不妨来马海祥博客上看下真正的IT牛人是如何评价编程语言,而又不失幽默和艺术水准的。
如果编程语言是女人
PHP是你的豆蔻年华的心上人,她是情窦初开的你今年夏天傻乎乎的追求的目标。玩一玩可以,但千万不要投入过深,因为这个女孩有严重的问题。
Perl是PHP的姐姐。她对你来说年龄稍微大了一点,但在90年代,她是相当受欢迎的。她和Larry Wall(译注:Perl语言创始人)长期保持着亲密关系,因此她的审美一落千丈,如今她看起来是丑陋不堪。&无论你们怎么评论,我仍然爱她!&,Larry Wall说。没有第二个人会像他这样。
Ruby是脚本家族中一个非常漂亮的孩子。第一眼看她,你的心魄就会被她的美丽摄走。她还很有有趣。起初她看起来有点慢,不怎么稳定,但近些年来她已经成熟了很多。
Python是Ruby的一个更懂事的姐姐。她优雅,新潮,成熟。她也许太过优秀。很多小伙都会说&嘿,兄弟,你怎么可能不爱上Python呢!?&。没错,你喜欢Python。你把她当成了一个脾气和浪漫都退烧了的Ruby。
Java是一个事业成功的女人。很多在她手下干过的人都感觉她的能力跟她的地位并不般配,她更多的是通过技巧打动了中层管理人员。你也许会认为她是很有智慧的人,你愿意跟随她。但你要准备好在数年里不断的听到&你用错了接口,你遗漏了一个分号&这样的责备。
C++是Java的表姐。她在很多地方跟Java类似,不同的是她成长于一个天真的年代,不认为需要使用&保护措施&。当然,&保护措施&是指自动内存管理。你以为我指的是什么?
C语言是C++的妈妈。对一些头发花白的老程序员说起这个名称,会让他们眼睛一亮,产生无限回忆。
Objective C 是C语言家族的另外一个成员。她加入了一个奇怪的教会,不愿意和任何教会之外的人约会。
Haskell, Clojure, Scheme 以及她们的朋友们都是一些时髦的,附庸风雅的,很聪明的女孩,你也许在多年前和她们曾度过了一个很Happy的暑假。她们是第一次让你感到有压力的女孩。当然,你可能从来没有对她们很认真&&尽管你总是在问自己&如果&.会如何?&
也许你会拖延着不去认识C# ,因为她们家族不好的名声。但最近几年他们已经改邪归正了&&他们会这样告诉你。一旦你加入我们,你就是我们的人了,你听到了没有?你需要一个数据库?她的哥哥MSSQL会罩着你。需要一个安身的地方?这有何难,她老爸甚至可以在Azure大道你给买一套公寓。什么?你觉得这样的关系过于亲密了?不,你别想离开我们。你已经是我们家族的一员了,现在,听见了没有?
Javascript && 嗨,这不是你的初吻的那个女孩吗,她甚至比PHP更早进入你的视线。我不知道她现在在干什么。我听说她的事业近几年来非常成功。旧时的人也可以赶上潮流变得很酷(你发现她从头到脚穿的都是设计师jQuery的作品)&,哇,有人从丑小鸭变成了美丽的天鹅&
如果编程语言脱离程序
一个有过BASIC编程经历的人是很难学会好的编程习惯的。作为一个潜在的程序员,他们已经被脑残并且无法修复。
  -- Edsger Wybe Dijkstra,Dijkstra算法发明者
C语言程序就像一群拿着刀的人在刚刚打过蜡的地板上快速的跳舞。
  -- Waldi Ravens
罗马帝国衰败的主要原因之一是因为他们缺少0,他们没有办法知道他们的C程序已经成功的执行完了。
  -- Robert Firth
C很容易让你朝自己的脚开枪。在C++中,这么做变的困难了,但是你要不注意就会崩掉自己的整条腿。
  -- Bjarne Stroustrup,C++发明者
我发明了&面向对象&,但是我可以明确的是,我不知道什么是C++。
  -- Alan Kay,Smalltalk发明者
C++的最新功能是用来修正之前的最新功能的。
  -- David Jameson
50年的编程语言研究就搞出来一个C++?
  -- Richard A. O'Keefe
只要你花一点时间研究C++,你就会发现C++的用户都在寻找一门更好的语言。
  -- R. William Beckwith.
Java就是去掉了枪炮,刀剑,还有黑帮的C++。
  -- James Gosling,Java 联合发明人
C++是一门恐怖的语言。即使选择C没有任何优势,只是为了逃避C++,那这个理由也足够了。
  -- Linus Torvalds,Linux发明者
使用COBOL会让你变的脑残,所以教别人使用COBOL就是犯罪。
  -- E.W. Dijkstra,Dijkstra 算法发明者
如果Java真的有垃圾收集的话,大部分程序在刚开始执行的时候就会把他们自己删了。
  -- Robert Sewell
Lisp不是一门语言,它只是一种构建材料。
  -- Alan Kay,Smalltalk发明者
Perl是唯一一门在RSA加密前后看起来一样的语言。
  -- Keith Bostic
PHP是由不称职的业余选手发明和操纵的,而Perl是伟大而阴险的,由一帮高水平的变态专业人士操纵。
  -- Jon Ribbens
Bash以及其他shell
很明显,移植shell要比移植shell脚本更容易。
  -- Larry Wall,Perl发明者
马海祥博客点评:
世界上只有两种编程语言:一种是整天被人喷的语言,另外一种就是没有人用的语言。
本文为原创文章,如想转载,请注明原文网址摘自于/bcyy/349.html,注明出处;否则,禁止转载;谢谢配合!
您可能还会对以下这些文章感兴趣!
今天一早打开网站就看到很多的圈内人士在网上讨论,说百度昨晚在搜索引擎搜索页面做出的一个小变化,在搜索……
今天早上我一如既往的打开电脑查找我所需要的资料,在使用360搜索时却发现个奇怪的现象,在通过360综合搜索……
百度百家的问题在于,它还基本上是从百度新闻首页倒流给作者,但随着作者越来越多,狼多肉少,显然并非长久……
链接也称超级链接,是指从一个网页指向一个目标的连接关系,而在……
搜索引擎Spider系统的目标就是发现并抓取互联网中一切有价值的网……
为什么客户在打开网页之后很快关闭了页面?为什么我的网站停留时……
目前,在中国做seo的在大多数时候,我们都是以百度和Google作为……
当一个搜索蜘蛛访问一个站点时,它会首先检查该站点根目录下是否……
最近我在给公司的编辑和优化人员培训时,在讲到文章关键词的密度……
本月热点文章Clojure 编程语言
充分利用 Eclipse 的 Clojure 插件
Lisp 是一种编程语言,以表达性和功能强大著称,但人们通常认为它不太适合应用于一般情况。Clojure 是一种运行在 Java(TM) 平台上的 Lisp 方言,它的出现彻底改变了这一现状。如今,在任何具备 Java 虚拟机的地方,您都可以利用 Lisp 的强大功能。在本文中,了解如何开始使用 Clojure,学习它的一些语法,同时利用 Eclipse 的 Clojure 插件提供帮助。
本文介绍了 Clojure 编程语言。Clojure 是一种 Lisp 方言。本文假设您不熟悉 Lisp,但需要您具备 Java 技术方面的知识。要编写 Clojure 程序,需要 Java Development Kit V5 或更高版本以及 Clojure 库。本文使用的是 JDK V1.6.0_13 和 Clojure V1。此外,您还需要利用 Eclipse 的 Clojure 插件(clojure-dev),因此还要用到 Eclipse。在本文中,我们使用了 Eclipse V3.5 和 clojure-dev 0.0.34。相关链接,请参见 。
什么是 Clojure?
不久前,要想在 Java Virtual Machine (JVM) 上运行程序,还需要使用 Java 编程语言来编写程序。但那个时代已经一去不复返了。现在更多的选择,比如 Groovy、Ruby(通过 JRuby)及 Python (通过 Jython),带来了一种更为过程化的脚本式的编程风格,或者它们各自拥有独特的面向对象编程特色。这两种都是 Java 程序员所熟悉的。也许有人会说用这些语言与用 Java 语言编写程序没什么区别,只要习惯不同的语法就可以了。
虽然 Clojure 还不算是 JVM 的一种新的编程语言,但它与 Java 技术及前面提到过的其他任何 JVM 语言都有很大的区别。它是一种 Lisp 方言。从 20 世纪 50 年代开始至今,Lisp 语言家族已经存在很长时间了。Lisp 使用的是截然不同的 S-表达式或前缀 注释。这个注释可以被归结为(function arguments...)。通常总是从函数名开始,然后列出要向这个函数添加的零个或多个参数。函数及其参数通过圆括号组织在一起。数量众多的括号也成为了 Lisp 的一大特征。
您可能已经发现,Clojure 是一种函数式编程语言。专业人士可能会说它太过单一,但实际上它却囊括了函数式编程的所有精华:避免了不稳定状态、递归、更高阶的函数等。Clojure 还是一个动态类型的语言,您可以选择添加类型信息来提高代码中的关键路径的性能。Clojure 不仅可在 JVM 上运行,而且在设计上还兼顾了 Java 的互操作性。最后,Clojure 在设计上也考虑了并发性,并具有并发编程的一些独特特性。
Clojure 示例
对大多数人来说,学习一种新的编程语言的最佳方法是从练习编写代码开始。按照这个思路,我们将提出一些简单的编程问题,然后用 Clojure 来解决这些问题。我们将深入剖析每种解决方案以便您能更好地理解 Clojure 是如何工作的、该如何使用它、它最擅长什么。不过,像其他语言一样,要想使用 Clojure,我们需要先为它建立一个开发环境。幸好,建立 Clojure 环境非常容易。
要建立 Clojure 语言环境,您所需要的就是一个 JDK 和 Clojure 库(一个 JAR 文件)。开发和运行 Clojure 程序有两种常用方式。其中最常用的一种方法是使用它的 REPL(read-eval-print-loop)。
清单 1. Clojure REPL
$ java -cp clojure-1.0.0.jar clojure.lang.Repl
Clojure 1.0.0-
此命令从 Clojure JAR 所在的目录运行。按需要,将路径调整到 JAR。您还可以创建一个脚本并执行此脚本。为此,需要执行一个名为 clojure.main 的 Java 类。
清单 2. Clojure main
$ java -cp clojure-1.0.0.jar clojure.main /some/path/to/Euler1.clj
同样,您需要将路径调整到 Clojure JAR 及脚本。Clojure 终于有了 IDE 支持。Eclipse 用户可以通过 Eclipse 升级网站来安装 clojure-dev 插件。安装完毕且确保处于 Java 透视图中后,就可以创建一个新的 Clojure 项目和新的 Clojure 文件了,如下所示。
图 1. 使用 Eclipse 的 Clojure 插件 clojure-dev
有了 clojure-dev,您就能够获得一些基本语法的亮点,包括圆括号匹配(Lisp 所必需的)。您还可以在被直接嵌入到 Eclipse 的一个 REPL 中放入任意脚本。这个插件还很新,在本文写作之时,它的特性还在不断发展。现在,我们已经解决了基础设置的问题,接下来让我们通过编写一些 Clojure 程序来进一步研究这个编程语言。
示例 1:处理序列
Lisp 这一名字来自于 “列表处理”,人们常说 Lisp 中的任何东西都是一个列表。在 Clojure 中,列表被统一成了序列。在第一个示例中,我们将处理下述的编程问题。
如果我们要列出 10 以下且为 3 或 5 的倍数的所有自然数,我们将得到 3、5、6 和 9。这几个数的和是 23。我们的题目是求出 1,000 以下且为 3 或 5 的倍数的自然数的和。
这个题目取自 Project Euler,Project Euler 是一些可以通过巧妙(有时也不是很巧妙)的计算机编程解决的数学题集。实际上,这就是我们的问题 1。清单 3 给出了这个问题的解决方案,其中使用了 Clojure。
清单 3. Project Euler 中的示例 1
(defn divisible-by-3-or-5? [num] (or (== (mod num 3) 0)(== (mod num 5) 0)))
(println (reduce + (filter divisible-by-3-or-5? (range 1000))))
第一行定义了一个函数。记住:函数是 Clojure 程序的基石。大多数 Java 编程员都习惯于把对象作为其程序的基石,所以一些人可能需要一些时间才能习惯使用函数。您可能会认为 defn 是此语言的关键字,但它实际上是个宏。一个宏允许您对 Clojure 做扩展以向该语言中添加新的关键字。也就是说,defn 并不是此语言规范的一部分,而是通过此语言的核心库被添加上的。
在本例中,第一行实际上是创建了一个名为 divisible-by-3-or-5? 的函数。这遵循了 Clojure 的命名约定。单词均以连字符分隔,并且此函数的名字是以一个问号结尾的,用以表示此函数是一个断言,因它会返回 true 或 false。此函数只接受一个名为 num 的单一参数。如果有更多的输入参数,它们将显示在这个方括号内,以空格分隔。
下面是这个函数的主体。首先,我们调用 or 函数。这是常用的 or 逻辑;它是一个函数,而不是一个操作符。我们将它传递给参数。而每个参数也是一个表达式。第一个表达式是以 == 函数开始的。它对传递进来的这些参数的值进行比较。传递给它的有两个参数。第一个参数是另一个表达式;这个表达式调用 mod 函数。这是数学里的模运算符或 Java 语言里的 % 运算符。它返回的是余数,所以在本示例中,余数是 num 被 3 除后的余数。该余数与 0 比较(如果余数是 0,那么 num 可以被 3 整除)。同样地,我们检查 num 被 5 除后的余数是否是 0。如果这两种情况的余数有一个是 0,那么此函数返回 true。
在接下来的一行,我们创建一个表达式并把它打印出来。让我们从圆括号的最里面开始。在这里,我们调用了 range 函数并将数 1,000 传递给它。这会创建一个由 0 开始,所有小于 1,000 的数组成的序列。这组数正是我们想要检查是否可被 3 或 5 整除的那些数。向外移,我们会调用filter 函数。此函数接受两个参数:第一个是另一个函数,该函数必须是一个断言,因它必须要返回 true 或 false;第二个参数是一个序列 — 在本例中,此序列是 (0, 1, 2, ... 999)。filter 函数被应用到这个断言,如果该断言返回 true,序列中的元素就被加到此结果。这个断言就是在上一行中定义的 divisible-by-3-or-5? 函数。
因此,这个过滤器表达式的结果是一个整数序列,其中每个整数都小于 1,000 且能被 3 或 5 整除。而这也正好是我们感兴趣的那组整数,现在,我们只需将它们相加。为此,我们使用 reduce 函数。这个函数接受两个参数:一个函数和一个序列。它将此函数应用到序列中的前两个元素。然后再将此函数应用到之前的结果以及序列中的下一个元素。在本例中,该函数就是 + 函数,即加函数。它能将该序列中的所有元素都加起来。
从清单 3 中,不难看出在这一小段代码中发生了很多事情。而这也恰好是 Clojure 吸引人之处。发生的操作虽然很多,但如果熟悉了这些注释,代码将很容易读懂。同样的事情,若用 Java 代码去做,则需要用到更多的代码量。让我们接下来看看另一个例子。
示例 2:惰性成了长处
通过这个例子,我们来探讨一下 Clojure 内的递归和惰性。这对于很多 Java 程序员而言是另一个新概念。Clojure 允许定义 “懒惰” 的序列,即其中的元素只有在需要的时候才进行计算。借此,您就可以定义无穷序列,这在 Java 语言中是从未有过的。要了解这一点是多么地有用,可以看看下面这个例子,该例涉及到了函数式语言的另一个十分重要的方面:递归。同样地,我们仍然使用 Project Euler 中的一个编程问题,但是这次,它是问题 2。
Fibonacci 序列中的每个新的项都是其前面两项相加的结果。从 1 和 2 开始,前 10 项将是:1, 2, 3, 5, 8, 13, 21, 34, 55, 89, …
现在,我们要找到此序列中所有偶数项之和,且不超过 400 万。为了解决这个问题,Java 程序员一般会想到要定义一个函数来给出第 n 个 Fibonacci 数。这个问题的一个简单实现如下所示。
清单 4. 一个简单的 Fibonacci 函数
(defn fib [n]
(if (= n 0) 0
(if (= n 1) 1
(+ (fib (- n 1)) (fib (- n 2))))))
它检查 n 是否为 0;如果是,就返回 0。然后检查 n 是否为 1。如果是,就返回 1。否则,计算第 (n-1) 个 Fibonacci 数和第 (n-2) 个 Fibonacci 数并将二者加起来。这当然很正确,但是如果您已经进行了很多的 Java 编程,就会看到问题。像这样的递归定义很快就会填满堆栈,从而导致堆栈溢出。Fibonacci 数形成了一个无穷序列,所以应该用 Clojure 的无穷惰性序列描述它,如清单 5 所示。请注意虽然 Clojure 具有一个更为有效的 Fibonacci 实现,是标准库(clojure-contrib)的一部分,但它较为复杂,因此这里所示的这个 Fibonacci 序列来自于 Stuart Halloway 的一本书(更多信息,请参见 )。
清单 5. 针对 Fibonacci 数的一个惰性序列
(defn lazy-seq-fibo
(concat [0 1] (lazy-seq-fibo 0 1)))
(let [n (+ a b)]
(cons n (lazy-seq-fibo b n))))))
在清单 5 中,lazy-seq-fibo 函数具有两个定义。第一个定义没有参数,因此方括号是空的。第二个定义有两个参数 [a b]。对于没有参数的情况,我们获取序列 [0 1] 并将它连到一个表达式。该表达式是对 lazy-seq-fibo 的一个递归调用,不过这次,它调用的是有两个参数的情况,并向其传递 0 和 1。
两个参数的情况从 let 表达式开始。这是 Clojure 内的变量赋值。表达式 [n (+ a b)] 设置变量 n 并将其设为等于 a+b。然后它再使用 lazy-seq 宏。正如其名字所暗示的,lazy-seq 宏被用来创建一个惰性序列。其主体是一个表达式。在本例中,它使用了 cons 函数。该函数是 Lisp 内的一个典型函数。它接受一个元素和一个序列并通过将元素添加在序列之前来返回一个新序列。在本例中,此序列就是调用 lazy-seq-fibo函数后的结果。如果这个序列不是惰性的,lazy-seq-fibo 函数就会一次又一次地被调用。不过,lazy-seq 宏确保了此函数将只在元素被访问的时候调用。为了查看此序列的实际处理,可以使用 REPL,如清单 6 所示。
清单 6. 生成 Fibonacci 数
1:1 user=& (defn lazy-seq-fibo
(concat [0 1] (lazy-seq-fibo 0 1)))
(let [n (+ a b)]
(cons n (lazy-seq-fibo b n))))))
#'user/lazy-seq-fibo
1:8 user=& (take 10 (lazy-seq-fibo))
(0 1 1 2 3 5 8 13 21 34)
take 函数用来从一个序列中取得一定数量(在本例中是 10)的元素。我们已经具备了一种很好的生成 Fibonacci 数的方式,让我们来解决这个问题。
清单 7. 示例 2
(defn less-than-four-million? [n] (& n 4000000))
(println (reduce +
(filter even?
(take-while less-than-four-million? (lazy-seq-fibo)))))
在清单 7 中,我们定义了一个函数,称为 less-than-four-million?。它测试的是其输入是否小于 400 万。在接下来的表达式中,从最里面的表达式开始会很有用。我们首先获得一个无穷的 Fibonacci 序列。然后使用 take-while 函数。它类似于 take 函数,但它接受一个断言。一旦断言返回 false,它就停止从这个序列中获取。所以在本例中,Fibonacci 数一旦大于 400 万,我们就停止获取。我们取得这个结果并应用一个过滤器。此过滤器使用内置的 even? 函数。该函数的功能正如您所想:它测试一个数是否是偶数。结果得到的是所有小于 400 万且为偶数的 Fibonacci 数。现在我们对它们进行求和,使用 reduce,正如我们在第一个例子中所做的。
清单 7 虽然能解决这个问题,但是并不完全令人满意。要使用 take-while 函数,我们必须要定义一个十分简单的函数,称为 less-than-four-million?。而结果表明,这并非必需。Clojure 具备对闭包的支持,这没什么稀奇。这能简化代码,如清单 8 中所示。
Clojure 中的闭包
闭包在很多编程语言中非常常见,特别是在 Clojure 等函数语言中。这不仅仅是因为函数 “级别高” 且可被作为参数传递给其他函数,还因为它们可被内联定义或匿名定义。清单 8 是清单 7 的一个简化版,其中使用了闭包。
清单 8. 简化的解决方案
(println (reduce +
(filter even?
(take-while (fn [n] (& n 4000000)) (lazy-seq-fibo)))))
在清单 8 中,我们使用了 fn 宏。这会创建一个匿名函数并返回此函数。对函数使用断言通常很简单,而且最好使用闭包定义。而 Clojure 具有一种更为简化的方式来定义闭包。
清单 9. 简短的闭包
(println (reduce +
(filter even?
(take-while #(& % 4000000) (lazy-seq-fibo)))))
我们曾使用 # 创建闭包,而不是借助 fn 宏。而且我们还为传递给此函数的第一个参数使用了 % 符号。您也可以为第一个参数使用 %1,如果此函数接受多个参数,还可以使用类似的 %2、%3 等。
通过上述这两个简单的例子,我们已经看到了 Clojure 的很多特性。Clojure 的另一个重要的方面是其与 Java 语言的紧密集成。让我们看另外一个例子来了解从 Clojure 使用 Java 是多么有帮助。
示例 3:使用 Java 技术
Java 平台能提供的功能很多。JVM 的性能以及丰富的核心 API 和以 Java 语言编写的第三方库都是功能强大的工具,能够帮助避免大量重复的工作。Clojure 正是围绕这些理念构建的。在 Clojure 中,很容易调用 Java 方法、创建 Java 对象、实现 Java 界面以及扩展 Java 类。为了举例说明,让我们来看看另一个 Project Euler 问题。
清单 10. Project Euler 的问题 8
Find the greatest product of five consecutive digits in the 1000-digit number.
在这个问题中,有一个 1,000-位的数字。在 Java 技术里,该数可以通过 BigInteger 表示。但是,我们无需在整个数上进行计算 — 只需每次计算 5 位。因而,将它视为字符串会更为简单。不过,为了进行计算,我们需要将这些数位视为整数。所幸的是,在 Java 语言中,已经有了一些 API,可用来在字符串和整数之间来回转换。作为开始,我们首先需要处理上面这一大段不规则的文本。
清单 11. 解析文本
(def big-num-str
这里,我们利用了 Clojure 对多行字符串的支持。我们使用了 str 函数来解析这个多行字符串。之后,使用 def 宏来定义一个常量,称为 big-num-str。不过,将其转换为一个整数序列将十分有用。这在清单 12 中完成。
清单 12. 创建一个数值序列
(def the-digits
(map #(Integer. (str %))
(filter #(Character/isDigit %) (seq big-num-str))))
同样地,让我们从最里面的表达式开始。我们使用 seq 函数来将 big-num-str 转变为一个序列。不过,结果表明此序列并非我们所想要的。REPL 可以帮助我们看出这一点,如下所示。
清单 13. 查看这个 big-num-str 序列
user=& (seq big-num-str)
(\7 \3 \1 \6 \7 \1 \7 \6 \5 \3 \1 \3 \3 \0 \6 \2 \4 \9 \1 \9 \2 \2 \5 \1 \1 \9
\6 \7 \4 \4 \2 \6 \5 \7 \4 \7 \4 \2 \3 \5 \5 \3 \4 \9 \1 \9 \4 \9 \3 \4
\newline...
REPL 将字符(一个 Java char)显示为 \c。因此 \7 就是 char 7,而 \newline 则为 char \n(一个换行)。这也是直接解析文本所得到的结果。显然,我们需要消除换行并转换为整数,然后才能进行有用的计算。这也正是我们在清单 11 中所做的。在那里,我们使用了一个过滤器来消除换行。请再次注意,我们为传递给 filter 函数的断言函数使用了一个简短的闭包。闭包使用的是 Character/isDigit。这是来自java.lang.Character 的静态方法 isDigit。因此,这个过滤器只允许数值 char,而会丢弃换行字符。
现在,消除了换行之后,我们需要进行到整数的转换。浏览清单 12,会注意到我们使用了 map 函数,它接受两个参数:一个函数和一个序列。它返回一个新的序列,该序列的第 n 个元素是将此函数应用到原始序列的第 n 个元素后的结果。对于这个函数,我们再次使用了一个简短的闭包注释。首先,我们使用 Clojure 的 str 函数来将这个 char 转变为字符串。我们为什么要这么做呢?因为,接下来,我们要使用java.lang.Integer 的构造函数来创建一个整数。而这是由 Integer 注释的。应该将这个表达式视为新的 java.lang.Integer(str(%))。联合使用它与 map 函数,我们就能如愿地得到一个整数序列。现在,我们来解决这个问题。
清单 14. 示例 3
(println (apply max
(map #(reduce * %)
(for [idx (range (count the-digits))]
(take 5 (drop idx the-digits))))))
要理解这段代码的含义,让我们从 for 宏开始。它不同于 Java 语言中的 for 循环,它是一个序列推导式。首先,我们使用一些方括号来创建一个绑定。在本例中,我们将变量 idx 绑定到一个从 0 到 N-1 的序列,其中 N 是序列 the-digits 中的元素的数量(N = 1,000,因为原始的数值具有 1,000 位)。接下来,for 宏获取一个表达式来生成一个新的序列。它将迭代 idx 序列中的每个元素,求得表达式的值,并将结果添加到这个返回序列。不难看出,这在某些方面充当了 for 循环的功能。在此推导式中所使用的表达式首先使用 drop 函数来向下移(drop)此序列的第一个 M 元素,然后使用 take 函数来取得此截短序列的前五个元素。M 将是 0,然后是 1,2,以此类推,所以结果将是一个包含序列的序列,其中的前五个元素将是 (e1, e2, e3, e4, e5),下一个元素将是 (e2, e3, e4, e5, e6),以此类推,其中的 e1、e2 等均是来自 the-digits 的元素。
有了这个包含序列的序列之后,我们就可以使用 map 函数了。我们使用 reduce 函数将五个数组成的每个序列转换成这五个数的乘积。现在,我们就得到了一个整数序列,其中的第一个元素是元素 1-5 的乘积,第二个元素是元素 2-6 的乘积,以此类推。我们想要得到最大的乘积。为此,我们使用 max 函数。然而,max 一般接受传递给它的多个元素,而不是单一一个序列。为了将这个序列转变为多个元素后再传递给 max,我们使用 apply 函数。这会产生我们想要的最大数,当然还会打印出结果。现在,您已经解决了几个问题,并同时掌握了该如何使用 Clojure。
在本文中,我们介绍了 Clojure 编程语言并从 Eclipse 的 Clojure 插件的使用中受益匪浅。我们先是简单查看了其原理和特性,之后重点介绍了几个代码示例。在这些代码示例中,我们逐渐了解了该语言的核心特性:函数、宏、绑定、递归、惰性序列、闭包、推导式以及与 Java 技术的集成。Clojure 还有很多其他的方面。我希望,该语言已经吸引了您,以至于您有兴趣借助本文所列的一些参考资料来深入了解它。
文章来自:
Clojure 编程语言

我要回帖

更多关于 clojure 代码 的文章

 

随机推荐