@Path
注解修饰.ps: 其实官方文档有这方面的描述, 但是目前还没有看出来需要对jar包做怎样的特殊处理. 等试出来之后再添加上来.
]]>在浏览器通过https访问网站时, 网站需要出示一个证书证明自己的身份. 因此在配置https访问之前, 我们需要生成一个密钥对. 我们为了省事, 只是简单的自己生成一个密钥对.
生成密钥对的命令如下:
1 | keytool -genkey -alias apple -keyalg RSA -sigalg SHA256withRSA -keysize 1024 -validity 36500 -keystore ./keystore |
输入上面的命令, 回车. 会提示如下内容. (注, 下面’#’号后面为解释)
1 | 输入密钥库口令: # 输入密钥库密码, 输入密码时不会回显. |
提示输入名字与姓氏时最好输入域名或者IP, 其他虽然为随意输入, 但是还是建议输入真实的信息
至此, 密钥就已经保存到了前面指定的文件中. 下面我们修改tomcat的配置文件
首先修改tomcat/conf/server.xml文件.
将其中port=”8443”对应的标签的注释取消掉, 并将密钥文件路径和密码加到配置中. 如下所示:
1 | <Connector port="8443" protocol="org.apache.coyote.http11.Http11NioProtocol" |
此时如果启动tomcat, 并访问http://localhost:8443
, 应该出现如下界面:
这时我们点击”继续浏览此网站(不推荐)。”, 即可看到tomcat的首页. 至此tomcat已经支持https方式访问了. 提示证书错误的原因我们后面再说.
虽然上面我们已经让tomcat支持https方式访问了. 但是有时候我们还希望用户通过http访问时, 自动切换为https协议. 这个功能也很好实现, 只需要将下面一段xml加入到tomcat/conf/web.xml或者项目的WEB-INF/web.xml文件中即可
1 | <security-constraint> |
web-app
标签里由于我们的证书只是我们自己生成的, 并没有提交给CA. 因此浏览器会认为我们的证书不安全. 要解决这个问题, 需要从CA机构购买证书. 或者我们可以向12306学习, 将证书导出, 然后交给用户, 让用户将证书加入到”受信任的根证书颁发机构”里即可.
导出证书的步骤:
安装证书的过程很简单, 这里就不再赘述了. 如果遇到问题, 可以参见12306的安装证书的步骤.
错误信息如下:
原因: 生成密钥文件提示”您的名字与姓氏是什么?”时输入的不是域名. 需要重新生成密钥文件, 并重复执行导出, 安装的过程.
虽然SHA-1算法目前尚未发现严重的弱点,但伪造证书所需费用正越来越低。因此浏览器都已经不在建议使用SHA-1算法对证书进行签名了. 解决办法就是使用更为安全的SHA-2. 在生成密钥文件时添加-sigalg SHA256withRSA
选项即可. 参见keytool文档
目前还没有从CA机构申请过证书, 流程还不清楚. 等尝试过一次以后, 再更新本博客.
]]>目前pdf转图片比较常用的类库有如下几个:
上面所说的四种工具, 只有pdfbox能支持项目中用到的中文pdf. 所以我使用的是pdfbox. 但是在解决过问题以后, 我使用其他的pdf文件测试时, 有些中文pdf文件转换过之后就只剩下乱码了. 如果需要完美的中文支持, 可能只有使用 jpedal 或者 ICEPDF 的商业版才行吧?
目前(2016/04/24)pdfbox的最新版本为2.0.0. 我使用的版本为1.8.11. maven坐标如下:
1 | <dependency> |
也可以直接从pdfbox的下载页面下载: http://pdfbox.apache.org/download.cgi
如果只是需要将pdf文件转成图片文件还是比较简单的, 只需要简单的6,7行代码就行了:
1 | private static final String BASE_PATH = "F:\\java\\"; |
过程很简单, 加载pdf文件. 创建一个输出流. 遍历pdf文件的每一页, 输出到图片文件中即可.
其中PDDocument.load
有多个重载方法, 最简单的四个重载方法分别接受String
类型的参数, File
类型的参数; URL
类型的参数, 或者InputStream
类型的参数.
而在输出时的imageWrite.writeImage
方法的第二个参数指定图片类型, 这个参数也决定了生成图片文件的缩略名.
我们可能需要将pdf转换成图片, 并存到内存中进行下一步处理(比如发送给浏览器客户端). 我们当然可以先存到文件中, 然后再读进内存, 但是这样效率太低, 尤其需要读写硬盘. 既然pdfbox有输出到内存缓冲区的功能. 我们当然不能浪费.
当然为了看到效果, 下面的示例程序还是将图片保存到文件中.
1 | private static final String BASE_PATH = "F:\\java\\"; |
非常简单吧! 今天就说这么多吧. 需要源码的朋友可以从这里下载源码. 如果各位朋友还有什么问题, 可以在下方的留言区留言, 我们相互讨论, 共同进步.
]]>在动手实现之前, 我们首先需要选择合适的数据结构来存储数据. 为了存储键值对, Map是最合适. 为了在删除过期数据时不用遍历整个Map, 我们需要一个优先队列来按照过期时间排序存储键值对.
但是使用Java提供的PriorityQueue
并不合适. 因为虽然PriorityQueue
类的remove()
方法提供了O(log(n))的时间效率. 但是remove(Object)
的效率是O(n)的. 而我们使用优先队列就是为了避免删除时O(n)的时间复杂度.
这里我们自己实现一个超级简化版的”优先队列”, 如果有更复杂的需求, 使用红黑树, 堆等数据结构可能会更合适一点.
既然需要实现一个简单的优先队列, 那么就需要先定义一个存储元素的类型. 由于我们不需要随机访问, 而需要方便的扩充容量, 所以使用链表的方式实现. 下面给出我们的优先队列需要存储的数据节点的定义:
1 | // 该类是内部类, 所以可以用static关键字修饰 |
我们的”优先队列”需要提供那些功能呢?
首先需要有添加功能, 并且添加后队列中的元素是按照过期时间排序的. 考虑到一般而言, 一个缓冲区中所有的元素的生存周期都是一样的(比如都是20分钟). 所以后面加入的元素的过期时间更靠后, 因此我们只需要提供一个 追加方法 即可.
需要有删除功能. 由于队列是按照过期时间排序的, 所以在删除时, 我们需要从头开始删除, 因此需要有 获取头部元素 和 删除头部元素 的功能.
还需要一个更新功能. 当用户使用了某个元素时, 我们需要修改该元素的过期时间, 然后把该元素放到队列的队尾. 当用户更新了和某个key关联的value后我们需要更新Entity
中的value
和timeout
字段, 然后放到队列的队尾. 而这两个需求都可以使用删除再添加实现. 所以我们需要提供一个 删除指定元素 的功能.
最后, 为了方便, 顺带再提供一个清空功能.
总结一下. 我们需要的功能有: 追加, 获取头部元素, 删除头部元素, 删除指定元素, 清空. 下面是实现代码:
1 | // 该类同样是内部类 |
终于到了实现缓存类的时候了. 其实经过上面的准备, 我们的缓存实现起来并不麻烦. 首先给出该类的属性以及构造方法:
1 | private long liveTime; |
接下来我们先实现删除过期数据的方法:
1 | private synchronized void removeTimeOut() { |
removeFirst
方法的实现就更简单了:
1 | // 注意, 调用该方法之前需要保证缓冲区中有元素 |
接着我们实现添加键值对功能. 该功能将一对键值对添加到缓冲区. 如果提供的键已经有与之关联的值了, 那么就用新值将旧值替换掉:
1 | public synchronized V add(K key, V value) { |
获取方法. 获取与指定键关联的值, 并更新该键值对的过期时间:
1 | public synchronized V get(K key) { |
access
方法用于更新实体的过期时间, 并调整该实体在队列中的位置:
1 | private synchronized void access(Entity<K, V> entity) { |
最后再提供两个工具方法:
1 | public synchronized void clear() { |
至此, 我们的简易版缓存类就算是完成了. 需要完整代码的朋友可以点击这里下载代码.
]]>在介绍junit 5的新特性之前先给出几个链接. 有兴趣的朋友可以去看看:
junit 5只支持java 8及以上. 按照《junit 5用户指南》的说法, junit 5也可以测试低版本jdk编译的java类.
junit 5已经不再是单个库了, 而是模块化结构的集合. 所有的模块可以参见《junit 5用户指南》. 这里我们给出一般情况下需要依赖的junit 5的包的Maven坐标:
Group ID: org.junit
Artifact ID: junit5-api
Version: 5.0.0-SNAPSHOT
org.junit.gen5.api
包中org.junit.gen5.api.Assertions
类中org.junit.gen5.api.Assumptions
类中@Test
注解已经不再包含任何的属性了.@Before
和@After
已经删除了, 与之对应的替代注解分别是@BeforeEach
和@AfterEach
@BeforeClass
和@AfterClass
也不存在了, 可以使用@BeforeAll
和@AfterAll
代替@Ignore
被@Disabled
代替@Category
使用@Tag
代替@RunWith
使用@ExtendWith
代替下面我们用junit 5编写一个最简单的单元测试:
1 | class Junit5FirstTest{ |
注意到了吗? 我们的测试类和方法都没有用public
修饰. 在junit 5中只要测试方法没有用private
修饰就可以.
由于junit 5需要的jre版本为java 8. 自然就可以在特定地方使用Lambda表达式. 这里我们在断言中使用一次Lambda表达式:
1 |
|
我们可以看一下assertTrue
方法的签名就清楚是怎么回事了:
1 | public static void assertTrue(boolean condition) |
注: 其中的Supplier
和BooleanSupplier
都在java.util.function
包中, 是java 8引入的接口. 这两个接口都是函数式接口, 所以可以使用Lambda表达式简化调用.
断言分组可以执行一组短信, 之后一起报告. 这样我们或许可以省掉不少的错误信息字符串.
1 |
|
在junit 4中如果想要判断异常, 就需要在@Test
注解中添加expected属性或者使用try-catch包裹. 但是try-catch太繁琐, 而有时候第一种方法又不是很合适. 在Junit 5可以使用异常断言解决:
1 |
|
在Junit 5中假设使用org.junit.gen5.api.Assumptions
类中的方法完成, 如果假设的条件没有满足, 那么就会跳过测试方法. @Tag
注解用来对测试分组, 之后可以方便的指定需要执行那个分组的测试方法. 禁用测试使用@Disabled
注解实现, 被该注解标注的方法/类将直接跳过执行.
1 |
|
junit 5还有一些有趣的新特性, 这里限于篇幅就不再说了, 感兴趣的朋友可以参见《junit 5用户指南》.
]]>其实使用Redis确实挺简单, 至少没有过于复杂的概念, 庞大的命令集. 基本上入门挺快的. 剩下的就是创造力和经验了. 这里我们使用Redis来完成前两篇:《发送短信–限制发送频率》、《发送短信–限制日发送次数》完成的功能.
当然, 如果读者并没有学过Redis, 可以参见《The Little Redis Book》快速入门,这本”书”基本上半个上午就可以看完.
这里我们就是简单用Redis限制”访问”频率:
注: 该脚本摘自《Redis入门指南》
1 | --[[ |
该脚本也比较直观:
在Redis的官网上有许多Redis的Java客户端的库. 这里我们使用Jedis.
我们来看看代码. 该程序中的ClassPathResource
和FileCopyUtils
类为Spring中的类, 因此这里的示例程序依赖于Spring
1 | public class RateLimit { |
这里的init
方法的作用就是将刚才我们写的脚本读取到script
变量中, 以便以后使用.
isExceedRate
方法将关键字和参数(过期时间和发送次数)分别封装到List
里, 之后使用Jedis调用脚本. 获取返回值, 判断频率是否过高.
下面我们使用上面的代码完成限制发送频率的功能(部分接口和类的声明请参见《[发送短信–限制发送频率][sms2]》). 限制日发送次数的代码基本相同, 这里就不贴了, 请下载源码查看.
1 | public class FrequencyFilter implements SmsFilter { |
到这里我们的主要代码就完成了, 可以看出使用Redis后代码确实非常的简单.
由于我现在还不会性能测试, 所以只是简单的使用for
循环测试了一下性能, 虽然可能不是很准确, 但是也有一定的可信度. 在限制发送频率时, 使用ConcurrentMap
的性能更高, 貌似比例还不小, 只是由于基数并不大, 所以并没有多费多少时间(十万条记录只多花费了十五秒).
但是在限制日发送次数时, 剩下了n多时间. 综合来看, 还是只使用Redis更省时省事.
而且, 个人猜测, 在扩展到集群时, 使用Redis应该会简单些.
由于需要记录整天的发送记录, 因此这里我们将数据保存到数据库中. 数据表结构如下:
我们这里需要用到上一篇中提到的接口和实体类.
1 | public class DailyCountFilter implements SmsFilter { |
主要代码很简单, 首先判断向指定的手机号发送的次数是否达到了日最大发送次数, 之后再判断指定的ip请求发送的次数是否达到了最大次数. 如果都没有, 则将本次发送的手机号, ip等信息保存到数据库中.
当然, 这个类存在一定的问题: 在判断是否超过最大次数到保存实体数据之间可能已经有其他线程保存了新的数据. 造成上面的两个判断并不是绝对的准确.
我们可以使用序列化等级的事务保证不会发生错误, 但是代价太高. 因此我们这里不做处理. 因为我们前面已经实现了限制发送频率. 如果先使用FrequencyFilter
过滤一次, 限制发送频率, 那么基本上不可能出现前面说的问题.
还有一个问题: 随着时间的推移, 这个表会越来越大, 造成查询的性能相当的差. 我们可以向上一篇中那样, 每隔一段时间就删除无用的数据; 也可以动态的创建表, 然后向新表中插入数据.
这里我们采用第二种方案: 数据表的名字为”sms_四位年_两位月”, 比如”sms_2016_02”. 插入数据时根据现在的时间获得表名, 然后再插入. 另外使用Quartz在每月的20号2点生成下个月以及下下个月的数据表:
我们首先修改DailyCountFilter
类, 在这个类中添加任务计划, 定时生成数据表:
1 | // 在上面代码的基础上, 再添加如下代码 |
接下来, 我们看看SmsDao
的部分代码:
1 | public class SmsDao { |
SmsDao
中的createTable
方法成功运行有个前提, 就是存在sms
数据表. createTable
方法会复制sms
表的结构创建新的数据表.
我们保留发送短信的数据(手机号, ip, 时间等), 而不是直接删除, 是因为以后可能需要分析这些数据, 获取我们想要的信息, 比如判断服务商短信的到达率、是否有人恶意发送短信等. 甚至可能获得意外的”惊喜”.
最后, 示例代码可以在这里下载.
发送短信文章:
]]>如果是web程序, 那么在session中记录上次发送的时间也可以, 但是可以被绕过去. 最简单的, 直接重启浏览器 或者 清除cache等可以标记session的数据, 那么就可以绕过session中的记录. 虽然很多人都不是计算机专业的, 也没学过这些. 但是我们需要注意的是, 之所以限制发送频率, 是为了防止”短信炸弹”, 也就是有人恶意的频繁的请求向某个手机号码发送短信. 所以这个人是有可能懂得这些知识的.
下面我们使用”全局”的数据限制向同一个用户发送频率. 我们先做一些”准备”工作
我们需要的实体类如下:
1 | public class SmsEntity{ |
过滤接口如下:
1 | public interface SmsFilter { |
限制发送频率, 需要记录某个手机号(IP)及上次发送短信的时间. 很适合Map
去完成, 这里我们先使用ConcurrentMap
实现:
1 | public class FrequencyFilter implements SmsFilter { |
这里, 主要的逻辑在setSendTime
方法中实现:
sendAddressMap
中. 如果putIfAbsent
返回null
, 那么说明用户确实是第一次发送短信, 而且现在的时间也已经放到了map中, 可以发送.这段代码算是实现了频率的限制, 但是如果只有”入”而没有”出”那么sendAddressMap
占用的内容会越来越大, 直到产生OutOfMemoryError
异常. 下面我们再添加代码定时清理过期的数据.
1 | /** |
这段程序不算复杂, 启动一个定时器, 每隔cleanMapInterval
毫秒执行一次cleanSendAddressMap
方法清理过期数据.
cleanSendAddressMap
方法中首先获取当前时间, 根据当前时间获得一个时间值: 所有在这个时间之后发送短信的, 现在不可以再次发送短信. 然后从整个map中删除所有value小于这个时间值的键值对.
当然, 添加上面的代码后, 最开始的代码又有bug了: 当最后一行sendAddressMap.replace(id, sendTime, currentTime)
执行失败时不一定是其他线程进行了替换, 也有可能是清理线程把数据删了. 所以我们需要修改setSendTime
方法最后几行:
1 | private boolean setSendTime(String id) { |
putIfAbsent
, 如果成功, 说明是第二种情况, 可以发送; 否则是第一种或者第三种情况, 不能发送.至此, 限制发送时间的代码就算是完成了. 当然, 这段程序还有一个小bug或者说”特性”:
假如, IP为”192.168.0.1”的客户请求向手机号”12345678900”发送短信, 然后在sendInterval
之内又在IP为”192.168.0.2”的机器上请求向手机号”12345678900”发送短信. 那么短信将不会发出去, 而且手机号”12345678900”的上次发送时间被置为当前时间.
下面我们提供一个Server层, 展示如何将上一篇以及这一篇中的代码整合到一起:
1 | public class SmsService{ |
之后将FrequencyFilter
以及上一篇中的AsyncSmsImpl
通过set方法”注入”进去即可.
最后, 点击这里下载代码.
发送短信文章:
]]>发送短信的方法可能不少, 我们的方法是使用服务商提供的服务. 一般来说, 这些服务都是和语言无关的, 这里我们使用java写示例程序.
这里我们介绍一些服务商:
可以都看看, 根据自己的情况选择其中的一款. 这里我使用的是互亿无线, 只是因为我最开始只知道互亿无线, 使用的也是它.
使用这些服务商提供的服务首先得先看开发文档. 文档在这里下载: http://h.ihuyi.com/bbs/thread-36-1-1.html . 主要的文档如下
从开发文档中我们可以看到. 可以直接使用http请求也可以使用WebService请求发送短信. 由于DEMO文件夹下的java和jsp文件夹中的代码都是使用http请求发送短信. 所以这里就不再细说了, 我们使用WebService的方式演示发送短信.
从接口文档中我们知道它的WebService的WSDL的url为: http://106.ihuyi.cn/webservice/sms.php?WSDL
那么我们可以执行下面的命令生成客户端代码:
1 | wsimport -keep http://106.ihuyi.cn/webservice/sms.php?WSDL |
其中wsimport
是JDK自带的工具, -keep url
选项是”保留生成的文件”. 该命令会在当前目录下生成sms.cn.ihuyi._106
包, 以及众多的类.
接下来开始编写我们自己的代码.
为了方便, 这里我们首先定义一个接口:
1 | public interface Sms { |
这个接口很简单, 只有一个方法. 这个方法用来发送短信.
接下来我们首先实现一个同步发送短信的类:
1 | public class IhuyiSmsImpl implements Sms { |
在第17行, 我们获得远程对象的一个代理对象. 之后就可以通过这个代理对象进行发送短信, 查询账户余额等操作.
第18行, 使用该代理对象的submit方法提交了短信内容. 该方法的参数信息及返回值含义在接口文档中有详细的说明.
第19行我们获得了结果的状态码. 根据文档上的说明, 状态码为2说明提交成功. 简单起见, 这里我们只关注提交成功的情况. 需要注意的是, 状态码为2只是说明提交成功. 根据官网上的”3-5秒内响应、100%到达”, 我们可以推测.
如果提交成功, 那么基本上3-5秒内,短信就会发送成功, 根据用户的网络情况, 可能稍有延迟用户就可以收到短信.
使用这段代码发送短信也很简单, 直接new
一个对象, 设置好账号和密码就可以发送短信了.
由于发送短信涉及到网络通信, 因此sendMessage
方法可能会有一些延迟. 为了改善用户体验, 我们可以使用异步发送短信的方法. 原理很简单: 如果用户请求发送短信, 我们不是直接调用IhuyiSmsImpl
的sendMessage
方法, 而是将请求保存起来(生产者), 然后告诉用户: 短信发送成功. 之后有若干个消费者取出任务, 调用sendMessage
方法发送短信.
这里, 我使用线程池完成上面的任务:
1 | public class AsyncSmsImpl implements Sms { |
代码很简单, 直接将Sms
接口的sendMessage(mobile, message)
方法作为一个任务加到线程池的任务队列中. 这样等到有空闲线程时, 就会执行sendSms.sendMessage(mobile, message)
发送短信. 这里我们假设只要保存到线程池就可以成功发送短信. 因为发送失败的情况实际上很罕见.
到这里同步/异步发送短信就算是完成了, 代码可以在这里下载. 接下来的几篇我们看看一些常见的限制的实现, 比如: 一分钟只能发1次, 一天只能发送5次等.
发送短信文章:
]]>如果要ssh免密码登录本机, 比如是伪分布模式安装hadoop的话, 就需要ssh免密码登录本机. 那么我们可以使用如下的方式实现免密码登录:
1 | ssh-keygen -t rsa -f ~/.ssh/id_rsa -P '' # 生成公私钥 |
第一行, 使用ssh-keygen
生成公私钥. -t rsa
指定生成公私钥的算法为rsa. -f ~/.ssh/id_rsa
指定生成的公私钥存放的文件路径, 其中id_rsa存放的是私钥, id_rsa.pub存放的是公钥. -P ''
指定密码为空.
第二行就不再解释了.
第三行是因为如果authorized_keys文件的权限允许所有者之外的人修改, 那么这个文件中的内容并不起作用. 也就是所有组和其他人都不能拥有写权限, 个人建议权限设置成600. 由于初始时没有authorized_keys文件, 如果authorized_keys文件是由重定向生成的, 那么权限很可能大于644, 所以这里手工修改一下文件权限.
在生成公私钥时不指定密码确实不是很安全. 例如, 如果有人登录到你的系统, 就可以免密码登录到所有经过授权的机器(这里是本机). 如果有人拿到了你的私钥, 那么也可以免密码登录到所有经过授权的机器.
但是由于这里是登录本机, 基本上也就不存在安全问题了: 都已经登录到这台机器了, 再ssh登录这台机器也没有多大的意义; 同样, 如果都拿到私钥了, 那么应该也是登录过这台机子了. 而且还有一点是, 这很可能是在开发环境中, 那么对安全性的要求可能就没有那么高了, 而对便利性的要求可能会较高.
这里我们假设我们有四台机器, 主机名分别为: master, slave1, slave2, slave3. 我们的目的是从master免密码登录到slave1-3主机上.
如果还是用上面的方式生成公私钥. 如果有人登录了master主机, 那他就可以免密码登录到所有的slave主机上. 同样如果他拷贝走master上的私钥, 也可以免密码登录到所有的slave主机上. 所以这里建议设置密码. 生成公私钥的命令如下:
1 | ssh-keygen -t rsa -f ~/.ssh/id_rsa |
当提示输入密码时, 输入一个密码即可. 下面我们把生成公私钥时输入的密码简称ssh密码.
这里我们先使用ssh-copy-id
完成共享公钥. 在master主机上执行如下命令:
1 | ssh-copy-id -i ~/.ssh/id_rsa.pub slave1 |
其中-i ~/.ssh/id_rsa.pub
指定我们需要共享的公钥文件. slave1为主机名, 也可以写成对应的ip. 执行这条命令时, 需要输入slave1主机上对应账号的密码.
之后我们就可以使用ssh密码登录slave1了. 为了免密码登录, 需要为每一个需要免密码登录的主机都执行一次这条命令.
为了记住密码, 我们需要在master主机中的~/.bash_profile中添加如下的命令:
1 | eval `ssh-agent` |
其中第一行是启动ssh-agent服务. 该服务的作用范围为当前shell. 第二行是把专用密钥添加到ssh-agent的高速缓存中. 需要注意的是, 在执行ssh-add
时需要输入ssh密码, 所以在开机输入用户名、密码登录后还会提示输入ssh密码. 如果觉得烦人, 可以在以后手动执行ssh-add
.
到这里我们就可以在这个shell中免密码登录slave1-3了. 可以使用ssh slave1
登录slave1试试, 如果还需要密码, 说明配置有一些问题. 可以在仔细看看上面的步骤.
如果需要多台主机到多台主机免密码登录, 那么只需要在多台主机间相互共享公钥就行了. 都配置好记住密码就行了.
我们假设有两个shell: tty1, tty2. 其中tty1上在登录过以后输入了ssh密码. 而tty2上在登录过没有输入ssh密码. 那么在tty2上登录slave1-3时还是需要输入ssh密码的. 所以, 即使有人登录到master主机上, 不知道ssh密码也是不能免密码登录到slave1-3主机的. 同样的, 就算拷贝走私钥, 也没有用. 所以这样是安全的.
在上面我们是使用ssh-copy-id
共享公钥的, 但是当主机很多时, 做好做对这件事将是一个很大的挑战; 当要修改密码, 重新生成公私钥时还得同步所有的公钥…
为了更加方便, 我们可以这样做:
在一台机器上创建好用户主目录, 然后生成公私钥, 然后将公钥添加到本机的授权文件中. 比如:
1 | cat ~/.ssh/id_rsa.pub >> ~/.ssh/authorized_keys |
接着将该用户的主目录设置为nfs共享. 最后其他的主机都不使用自己的主目录, 而是挂载刚才共享的主目录. 那么就可以非常方便的共享公钥了. 具体步骤如下
在一(多)台机器上生成公私钥. 然后创建nfs共享. 修改/etc/exports
文件. 添加如下内容:
1 | /home/hadoop 192.168.45.0/24(rw,sync) |
个人建议最少在两台机器上生成公私钥, 然后创建nfs共享. 这样万一其中一台由于网络等原因无法挂载时, 可以使用另外一台机器上的数据. 如果有多台机器, 那么需要注意同步数据.
这里应该是可以直接在/etc/fstab
中添加挂载点的. 但是autofs更加适合这里的情况, 所以我们使用autofs.
首先我们安装autofs. 然后修改/etc/auto.master
, 添加如下内容:
1 | /home/remoteuser /ect/auto.remoteuser |
其中/home/remoteuser
是需要挂载的父级目录, /ect/auto.remoteuser
是具体配置文件的位置.
接下来, 我们需要编辑/ect/auto.remoteuser
(这个文件原本没有, 需要手工创建). 添加如下内容:
1 | hadoop -fstype=nfs,rw,soft,intr nfsserver1,nfsserver2:/home/hadoop |
其中hadoop
是挂载点的最后一级目录, /home/remoteuser/hadoop
就是最终的挂载点.-fstype=nfs,rw,soft,intr
是相应的选项.nfsserver1,nfsserver2:/home/hadoop
中的nfsserver1
和nfsserver2
是nfs共享的主机名,/home/hadoop
是共享的目录.更加灵活的配置方式可以参见man 5 autofs
之后再修改用户的主目录为/home/remoteuser/hadoop
. 接下来, 在任意的主机上使用ssh-add
添加过密码后, 就可以在该主机该shell上免密码登录所有的主机了.
写这篇博客, 同样是因为csdn上的一个帖子. 这个帖子中要求从125个数中挑选出21个数. 这两个数字对于许多问题来说并不是很大. 但是如果这里采用穷举的方法解决的话, 需要尝试的次数大家可以计算一下…这里我们首先给出穷举法的实现, 之后针对这个题目进行改进, 使程序的运行时间在可以接受的范围内.
要使用穷举法解决这个问题, 就太简单了(虽然运行时间长的不能忍受). 我最先想到的就是使用递归. 废话不多说, 看代码:
1 | private boolean calNextNumber(int start, int sum, int count) { |
这里只给出了关键的代码, 完整代码可以在这里下载. 帖子中的数据太长了, 也没有贴出来, 各位可以在源码中看到, 或者到《求解求哪几个数字之和等于一个固定值》里面查看.
如果使用我自己随便输入的测试数据, 几乎一瞬间就可以计算出来, 而如果用这个程序去计算那个帖子中给的数据, 在我的电脑上跑了两个小时, 还没有计算出来…
首先感谢kinkon007的提醒:
可以看到sum的尾数是7,那只有几种情况存在,0+7,1+6,2+5,3+4,先固定匹配好两个尾数,然后再选择其他的数来凑和。
根据这个提示, 我们大致可以想到分而治之的思想. 把数据分成两组: 一组(a组)低三位至少有一个不为0, 一组(b组)低三位都是0. 那么, 我们就可先计算a组, 使计算的结果的低三位和目标的第三位一致. 具体来说就是计算结果为: ***127. 但是个人感觉这样数据量还是很大(没有去数, 纯属感觉). 那么我们就多分几组吧! 一组个位均不为0, 一组个位为0, 十位不为0, 一组个位和十位为0, 百位不为0…
然后依次计算.
这里我就在上面那个穷举程序的基础上改进. 首先需要修改穷举程序, 使其不是精确到达某个值, 而是低位相等即可.
其次, 需要支持”迭代”, 就是说, 调用一次calNextNumber
方法获得一组可能的结果, 再调用一次calNextNumber
方法, 可以获得下一组可能的结果. 主要代码如下:
1 | private boolean calNextNumber(int start, int sum, int maxCount) { |
接下来, 再通过另外一个类去将数据分组, 调用上面的方法计算. 主要代码如下:
1 | private void splitArray(Map<Integer, List<Integer>> map) { |
同样, 这里只有主要代码, 完整代码在这里下载.
这种方法并不是可以使用所有的情况.
首先, 所有的数字需要可以大致均匀的分解成较多的组, 至少不能全部都在一个或者两个组中, 那样的话, 我们进行分组就有没意义了.
其次, 由于这里可以分得组并不是太多, 所以如果数字过多, 即使可以均匀的分组, 但是如果一个组的数字过多, 同样不适用. 这也是需要改进的地方.
当然, 还有一个缺点: 这里的代码有点饶. 尤其是ArraySum1
中的calNextNumber
方法. 或许可以在《从N个数中取出任意个数,求和为指定值的解》的基础上进行修改. 由于这篇博客中精巧的设计, 记录上一次的状态非常简单, 可以让程序简单许多. 这里就不提供代码了, 各位可以试试.
"abc" + '/'
和 "abc" + "/"
的区别. 通过这个例子, 我们可以顺便练习一下JDK工具中javap的用法.把斜杠/当作字符或字符串有什么区别呢?
一个是当作基本数据类型char,一个是对象String。具体有什么区别呢?
当作字符效率会更高吗?String str = "abc" + '/';
和String str = "abc" + "/";
首先大家应该知道, 上面那两句效果是一样的, 因为编译器会把上面那两句都优化成下面的样子:
1 | String str = "abc/"; |
我们可以通过javap证明这一点. 关于javap, 可以参考《每个Java开发者都应该知道的5个JDK工具》.
我们首先创建一个类: StringOne
, 在主方法中填入下面的代码(下载源码):
1 | String str1 = "abc" + '/'; |
编译并运行, 输出结果为true
. 接下来, 该我们的javap登场了, 在命令行中输入下面的命令:
1 | javap -v -l StringOne.class > StringOne.s |
然后查看生成的StringOne.s文件. 就会发现其中有这么几行(注:javap生成文件中指令的含义可以参考这几篇博客:《Java栈和局部变量操作(一)》,《Java栈和局部变量操作(二)]》,《java指令集》):
1 | #2 = String #20 // abc/ |
说明str1
和str2
都引用字符串"abc\"
.
现在我们换一个问法, 下面的代码中stringAddString
和stringAddChar
方法有什么区别?
1 | public static String stringAddString(String str1, String str2){ |
这次再使用javap进行反编译, 生成文件的部分内容如下所示
1 | public java.lang.String stringAddString(java.lang.String, java.lang.String); |
现在, 我们已经可以很清楚的看出这两个方法执行的流程了:
stringAddString
StringBuilder
对象append
方法, 依次将两个参数添加到刚才创建的StringBuilder
中.toString
方法.return
toString
方法的返回值.stringAddChar
的过程和stringAddString
一样, 只是在第二次调用append
方法时stringAddString
的参数是String
类型, 而stringAddChar
的参数是char
类型.
这里,我们直接查看源码就好了(我的是jdk1.8.0_60附带的源码)。
注意,虽然文档上显示StringBuilder
继承自Object
, 但是从源码来看, 它是继承自抽象类AbstractStringBuilder
的。而且append
方法是由AbstractStringBuilder
实现的。
1 | public AbstractStringBuilder append(char c) { |
剩下的就不再贴出来了。String.getChars(int, int, char[], int)
最终依赖于public static native void arraycopy(Object, int, Object, int, int)
。也就是说有可能是C语言写的,在拷贝大型数组时效率应该会比java写的程序好一些。
那么,现在说说我的理解:
String
中包含char数组, 而数组应该是有长度字段的, 同时String
类还有一个int hash
属性, 再加上对象本身会占用额外的内存存储其他信息,所以字符串会多占用一些内存. 但是如果字符串非常长, 那么这些内存开销差不多就可以忽略了; 而如果像"/"
这种情况, 字符串比较(非常)短,那么就很可能有许多个共享引用来分担这些内存开销, 那么多余的内存开销还是可以忽略的.String
只比char
多了一两层函数调用,所以如果不考虑函数调用开销(包括时间和空间), 应该差不多;考虑函数调用开销, 应该 **"abc" + '/'
更好一些; 但是当需要连接若干个字符时(感觉这种情况应该更常见吧?), 由于使用char
需要循环好多次才能完成连接, 调用的函数次数只会比使用String
更多. 同时拷贝也不会比String
直接拷贝一个数组更快. 所以这个时候就变成了"abc" + "/"
**吞吐量更大.现在感觉这个问题像是在问: 读写文件时使用系统调用效率高, 还是使用标准函数库中的IO库效率高. 个人感觉, 虽然标准IO库最后还得调用系统调用, 而且这之间会产生一些临时变量, 以及更深层次的调用堆栈, 但是由于IO库的缓冲等机制, 所以IO库的吞吐量会更大, 而系统调用的实时性会好一些. 同样, 虽然String
类会多几个字段, 有更深层次的函数堆栈, 但是由于缓存以及更直接的拷贝, 吞吐量应该会更好一些.
从上面javap的反编译代码来看, 两个String
相加, 会变成向StringBuilder
中append
字符串. 那么理论上, 下面哪段代码的效率好呢?
1 | String str1 = "abc" + "123"; // 1 |
我们知道, spring的声明式事务是基于代理模式的. 那么说事务之前我们还是大致的介绍一下代理模式吧.
其实代理模式相当简单, 就是将另一个类包裹在我们的类外面, 在调用我们创建的方法之前,
先经过外面的方法, 进行一些处理, 返回之前, 再进行一些操作.
比如:
1 | public class UserService{ |
那么如果配置了事务, 就相当于又创建了一个类:
1 | public class UserServiceProxy extends UserService{ |
然后我们使用的是UserServiceProxy
类, 所以就可以”免费”得到事务的支持:
1 |
|
private
方法, final
方法 和 static
方法不能添加事务上面的东西并不难. 那么我们可以从上面知道些什么呢?
首先, 由于java继承时, 不能重写private
, final
, static
修饰的方法. 所以, 所有的private
方法, final
方法 和 static
方法
都无法直接添加spring的事务管理功能. 比如下面的代码(完整代码点击[这里][/downloads/code/2016/01/spring-transaction.zip]下载):
1 | /** |
测试代码如下:
1 | /** |
由于saveErrorPrivate
方法外面是无法调用的, 就暂时不去讨论了.
我们直接看testSaveErrorFinal
和testSaveErrorStatic
方法的运行结果:
很明显, 事务并没有生效. 也就是说private
方法, final
方法 和 static
方法都没有事务支持.
仔细看看代理模式中的代码, 就会发现不通过代理对象调用方法也会导致spring事务管理失效.
绕过代理对象最直接的方法就是自己new
一个对象, 虽然这种可能性非常小:
1 | new UserService().save(user); |
当然, 前面也说了, 这种可能性非常小. 那么我们看看第二种情况, 这种情况的可能性也不大:
1 | /** |
由于测试的代码基本上和上面一样, 所以这里我们就不贴测试的代码了. 再说一次, 点击这里下载完整代码).
实际上, 上面的saveByCallMethod
方法还是无法获得spring的事务支持. 因为它的调用堆栈如下图所示(从下向上):
最终结果就是spring的事务管理没有生效. 这是或许你会想了, 那为啥不直接给saveByCallMethod
方法添加事务支持呢? 所以我说这种情况的可能性也不大.
下面我们再看看事务管理和多线程缠在一起时的情况:
1 | /** |
测试代码请参见我提供的完整代码.
这样的代码已经有可能了吧? 那么事务管理会生效吗? 我们再看看调用堆栈就知道了.
结果和我们想的一样, spring的事务管理并没有生效.
好了, 现在我们来回顾一下, 在那些情况下spring的事务管理会失效:
private
方法无法添加事务管理.final
方法无法添加事务管理.static
方法无法添加事务管理.1 | public class ProductAndComsuer { |
这是学习多线程非常典型的生产者消费者. 那么这段代码有问题吗?
为什么输出几行以后就不动了呢?是死锁了吗?
这里我们先复习一下死锁的定义和产生条件.
死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。
而出现死锁必然满足四个条件:
再看看这个程序, 第二条就不满足: 一个线程同一时间只会处于保持锁或者请求锁的状态,
根本就没有出现请求和保持同时出现的情况. 换(shuo)句(ju)话(ren)说(hua), 这里只有一个锁, 怎么可能发生死锁呢?
要证明这一点很简单, 只需要在两个线程的while
和if
之间加一句打印的语句就知道了. 比如create
方法中:
1 | while(true) { |
在编译运行程序, 就会发现程序并没有死锁, 那么为什么程序就是不执行同步块中的程序呢? 仔细看一下刚才程序的输出就知道原因了
1 | create方法, created=true, 是否满足条件?false |
我们发现不管是create
方法还是consume
方法, 都不满足进入if
语句的条件. 怎么会这样呢? create
方法明明将created
赋值为true
了.
其实, 单独看create
方法和consume
方法是看不出问题的. 这两个方法很正确. 问题其实是出在多线程中变量的可见性上.
在《JAVA并发编程实践》(点击查看豆瓣评价)3.1节中说:
在没有同步的情况下, 编译器, 处理器, 运行时安排操作的执行顺序可能完全出人意料. 在没有进行适当同步的多线程程序中, 尝试推断那些”必然”发生在内存中的动作时, 你总是会判断错误.
换句话说, 即使create
方法将created
赋值为true
, 如果没有适当的同步, 那么consume
方法中看见的可能还是以前的false
.
同样, create
方法看到的也可能是以前的值. 结果, 两个方法就都无法进入自己的if
语句块了.
更糟糕的是, 过期情况并不一定会马上发生, 也不一定会发生在所有的变量上, 当然也不会完全不出现. 所以就有可能被忽略.
要解决这个问题, 其中一个方法是使用volatile
关键字修饰create
字段. 那么volatile
关键字是干什么的呢? 上面也说了, 过期情况只会发生在没有适当同步的多线程程序中.
说道同步, 首先想到的应该就是加锁了吧. 但是加锁的开销太大,
而且不合适的锁会导致基本上线性执行的多线程程序(就像这个例子中的程序). 那么有没有其他的方法呢? 那就得靠volatile
了.
《JAVA并发编程实践》中是这样说的:
Java语言也提供了其他的选择, 及一种同步的弱形式: volatile变量. 它确保对一个变量的更新会以可预见的方式告知其他的线程.
…
所以, 读一个volatile类型的变量时, 总会返回由某一线程所写入的最新值.
也就是说, 我们只需要将create
变量的声明前添加volatile
关键字即可解决问题:
1 | private volatile boolean created = false; |
另外, 需要注意《JAVA并发编程实践》里面还说到:
当验证正确性必须推断可见性问题时, 应该避免使用volatile变量. 正确使用volatile变量的方式包括: 用于确保他们所引用的对象状态的可见性, 或者用于标示重要的生命事件(比如初始化或者关闭)的方法
也就是说, volatile
虽好, 但不能到处随意的使用. 可能是因为volatile
容易用错, 所以这个关键字比较低调, 很多地方都没有提过这个关键字. 简单的说, 因为volatile
不能提供原子性, 所以使用volatile
的变量的所有读取/修改必须是原子修改, 比如x++
就不是, 因为是先读取又写入. 这里我们就不深入说了, 要了解更多信息可以翻翻《JAVA并发编程实践》或者看看这篇”文档”《Java 理论与实践: 正确使用 Volatile 变量》.
最后, 我们再说说如何使用同步块解决这个问题. 其实很简单, 只需要将同步块和if
语句对调即可:
1 | private int i = 0; |
首先, 下载需要的插件: octopress-tag-pages和octopress-tag-cloud. 使用git clone或者直接下载压缩包都行.
安装其实很简单, 只需要将下面几个文件复制到octopress中相应的目录中即可:
octopress-tag-pages中的
octopress-tag-cloud中的
例如, 把octopress-tag-pages中的plugins/tag_generator.rb复制到octopress安装目录下的plugins文件夹中.
最后, 修改_config.xml中的default_asides, 将custom/asides/tags.html添加进去:
1 | default_asides: [custom/asides/category_list.html, custom/asides/tags.html, ...] |
至此, 已经安装完毕. 可能已经有读者迫不及待的想要看看效果了. 可是执行rake generate
却发生了错误(这个错误并不是所有人都会遇到)
1 | Liquid Exception: comparison of Array with Array failed in _layouts/page.html |
我们就是按照文档写的. 那么到底哪里错了? 我们看看刚才安装的两个插件的源码就知道了(没学过ruby没关系, 这两个插件的注释写的很完整, 我也没学过ruby, 但是也大致看得懂这个程序). 在tag_cloud.rb中第74行是这么写的:
1 | if @limit > 0 and @sort != 'rand' |
weighted = weighted[0,@limit]
应该就是取指定数目的标签. 如果你的标签数量少于@limit
, 那么就会报错. 所以我们修改第74行为:
1 | if @limit > 0 and @sort != 'rand' and @limit < weighted.length |
同样的, 将第95行修改为:
1 | if @limit > 0 and @limit < weighted.length |
这时再执行rake generate
就不会报错了. 但是又出现了几条警告:
1 | Build Warning: Layout 'nil' requested in tags/github page/atom.xml does not exist.exist. |
这个警告让人很摸不着头脑, 而且并不影响使用, 但是让人感觉很不爽, 所以继续在源码中查找问题.
然后无意间发现tag_generator.rb中的代码结构和category_generator.rb中的代码结构一模一样.
而category_generator.rb是用来生成类别信息的. 那么为什么生成类别信息时没有警告, 而生成标签信息时就会有警告呢?
仔细对比之后也没有发现什么问题. 后来发现tag_generator.rb中加载了我们安装时复制过来的tag_feed.xml文件. tag_feed.xml里面有这么一句话:
1 | layout: nil |
而category_generator.rb加载了category_feed.xml文件. category_feed.xml里面对应的位置是:
1 | layout: null |
是不是这里呢? 改过来试试吧. 好吧, 就是这里.
到现在侧边栏的标签已经能正常使用了. 但是, 如果你跟我一样, 刚刚开始使用octopress, 只有一两篇博客, 而且所有的标签的博客数量都一样, 那么就会发现左侧的标签的源码是这样:
1 | <a style="font-size: NaN%" href="/tags/github-page/">github page</a> |
这并不影响使用, 而且相当的罕见. 所以我们直接说解决方案吧.
将tag_cloud.rb第69行改为:
1 | if max == min |
这时就可以正常使用了.
如果用户点击边栏上的标签, 会跳转到标签页. 而标签页的默认标题为:Tag: 标签名
.
如果想将其中的”Tag:”换成中文的, 可以在_config.yml中添加下面的内容:
1 | tag_title_prefix: "标签--" |
重新生成一下, 标题就会变成标签--标签名
[octopress-tag-cloud][]的文档上说可以使用limit限制显示标签的数量(刚开始遇到的错误也是因为配置的limit太大了造成的). 直接修改source/_includes/custom/asides/tags.html中的limit后面的数字即可:
1 | {% tag_cloud font-size: 60-165%, limit: 15 %} |
效果如图:
将octopress-tag-pages插件中的source/_includes/archive_post.html复制到octopress中的source/_includes/目录下, 该文件本来已经有了, 直接覆盖就可以.
或者如果你已经修改过octopress中的source/_includes/archive_post.html文件了, 那么可以参考插件中的文件, 直接将
1 | {% if tag != '0' %} |
复制到octopress下的文件中的特定位置即可.
还是先上个图:
参考octopress-tag-pages插件中的source/_layouts目录中的post.html, 修改octopress下的post.html, 比如我的post.html中相应的部分是这样的:
1 | <p class="meta"> |
1 | Liquid Exception: invalid byte sequence in GB2312 |
或者
1 | jekyll 2.5.3 | Error: invalid byte sequence in GB2312 |
我的错误信息在这个时候出现的: 按《windows7上面使用Octopress搭建GitHub博客》上的步骤, 到中文化的时候, 修改source/_includes/custom/navigation.html和source/_includes/custom/footer.html时都没有问题, 但是修改source/_includes/asides/recent_posts.html之后就会出现该错误信息.
上网查了N多解决办法,无奈大多数都是针对jekyll 1.4.x版本的, 而我的是2.5.3, 基本没有用, 最后在《[ruby中in `split’: invalid byte sequence in UTF-8 (ArgumentError)解决方法](http://blog.csdn.net/jiedushi/article/details/8529110)》上看到了这样一段话:
将arr=arr = url.split(“&”)修改为
arr = url.force_encoding(“gb2312”).split(“&”) 即可
就抱着最后一次的心态试了一下, 使用rake generate --trace
生成页面, 后面的--trace
是用来打印错误的堆栈信息的. 最后发现错误是在”ruby安装目录\lib\ruby\gems\1.9.1\gems\liquid-2.6.3\lib\liquid\template.rb”的第147行发生的. 然后按照《[ruby中in `split’: invalid byte sequence in UTF-8 (ArgumentError)解决方法](http://blog.csdn.net/jiedushi/article/details/8529110)》上面的说法, 将
1 | tokens = source.split(TemplateParser) |
改成
1 | tokens = source.force_encoding("gb2313").split(TemplateParser) |
错误依旧…
然后猛然间发现人家提示的是”invalid byte sequence in utf-8”, 而我的提醒的是”invalid byte sequence in GB2313”, 所以改成
1 | tokens = source.force_encoding("utf-8").split(TemplateParser) |
问题成功解决.我没学过ruby, 但是看这个路径修改的应该是ruby系统或者是ruby某个库的系统文件代码, 这并不是一个好习惯. 但也只能这样做了.
ps: 今天写这篇博客时, 为了还原”现场”, 又将那行改了回去:
1 | tokens = source.split(TemplateParser) |
然后惊奇的发现, 又没有问题了…这是个未解之谜.
]]>