豪翔天下

Change My World by Program

0%

LAMP

Linux+Apache+MySQL+PHP,这是最流行也是我最熟悉的服务器架构了,其实网上有很多一键安装的版本,有些版本的linux甚至提供了_apt
-get install lamp
_这样的一键安装程序,但是虽然他们是一个整体,但为了保证其独立性,我还是每次安装都会分别安装的。特别是某些框架或者是l
inux下的管理软件(比如现在所使用的禅道管理软件)在安装的时候经常都会有独立的打包程序,但我觉得那样会破坏apache等服务的独立性,所以独立安装,感觉舒
服一点。下面介绍一下安装过程:

CentOS 6.x + Apache + Mariadb + PHP

阅读全文 »

web开发约定

web部署结构

仅用于轻量级web框架,来自于TornadoWheel

.工程目录
├── server.py            # 启动服务
├── application.py    # 服务基本设置
├── url.py            # 路由结构
├── handler/            # 各种请求处理类文件
├── static/            # 静态文件
├── optsql/            # 与数据库读写相关的文件
└── template            # 模板文件

我不知道有多少人因为池老师的一篇《先有Mac还是先有钱》而去买的苹果电脑,我现在也在用rmbp,但我可不是因为这个原因。从大一到大三,我那台宏碁的笔记本一直陪伴在我身边,经历了我各种摧残,更换了无数的操作系统(windows、linux系列的),曾经最长用了半年的deepin单系统。虽说我自认为我用电脑一定比其他人用得好,毕竟一台三千多的电脑能用出人家五六千电脑的流畅度,但是依然不尽如我意。折腾累了,最终在大三结束刚进入实习期的我就找父母拿钱买了一台15年乞丐版的rmbp。刚好快一年了,这一年,mac带给我最大的感受就是我几乎没有任何感受…我已经完全忘记了去折腾电脑了,重装系统、电脑卡顿、木马病毒,统统都没有,我甚至都忘记了他们的存在了。买windows是买电脑送系统,买mac则是买系统送电脑。mac带给我们更多的是一种享受,我愿意为这样的服务买单。

作为一本talk mac的书籍,当然不乏众多的mac使用技巧,osx的强大,远比我当前的使用方式更强大,还需要我去探索。当然,现在的我已经不再是工具控了,因为我已经找到了一套自己认为最适合自己的工具,不用再为新工具的出现而浪费时间了。除了一些使用技巧外,池健强老师还谈了一些对编程的看法。让我了解了一个人,王小波,对他几乎没有什么认识,我想,我也该去拜读一下他的书了。

关于盗版,我的博客很早就有一篇《学生应该尽量购买正版书籍》,上个月从学校回来,收拾了一箱子舍不得卖的书,几乎全是在当当或者亚马逊上买的,这,也算是我对这些作者表达的基本的尊重。同时,在使用了mac过后,我的电脑/手机都与盗版彻底绝缘了。从此可以问心无愧地进行开发,也希望自己开发的东西有一天能让大众享受,能让用户主动掏钱。以下是池老师关于盗版的言论:

1
1.盗版肯定是不对的,如果用了盗版软件,至少要有愧疚之心。如果你是个穷学生,学习软件开发用了盗版软件,谁忍心责备你呢?但大家千万不能无耻到开篇提到的那位父亲那样,不仅误己,而且误人。 2.程序员也是要吃饭的,你们每个人在自己的电脑上使用的每个软件都是程序员一行行的代码敲出来的。 3.在经济实力允许的基础上,尽可能用正版,尤其是程序员。程序员不支持程序员,还怎么指望别人呢?

池老师也算是很多新一代程序员的领路人,不知道他现在怎么样了,不过,我算是真正走上了这条路了,一切,才刚刚开始。

语录

  • 反观观国内,很多公司把企业文化作为一种“秀”或“工具”,这就比较扯淡了。我觉得搞好企业文化,就两点: 1.利益,把公司利益和员工联系在一起,好员工钱得给足。 2.人文,少搞或不搞办公室政治,让员工自由一点、开放一点、平等一点,你会获得回报。 对于员工本身来说,不管企业是什么文化,
  • 是啊,人怎么会有那么多时间学习那么多东西呢?其实这个不可能的设定,是在保证你有足够时间看电视、看美剧、刷微博、上网闲逛的基础之上的。只要把上述这些事情消费的时间减少一半,拿来持续学习,你就会发现学习效果是惊人的。
  • 有一些穿高跟鞋走不到的路,有一些喷着香水闻不到的空气,有一些在楼宇里永远遇不到的人。
  • 很多时候我们初入江湖,不知深浅,不知道什么事能做,怎么做,为什么要这么做,即使怯生生问了,得到的答复往往是,That’s just the way it’s done(我们向来这么做),于是我们慢慢也变得成熟、圆滑和懒惰,不再去从深层次思考『为什么要这么做?』因为别人也这么做。慢慢的,这些东西就成为了folklore(陈规陋习)。 一个坏的习惯或传统,可能延续十年、百年、千年,直到那个打破陈规陋习的人出现! 提问、思考和努力工作,你就会抛弃这些陈规陋习,找到布满荆棘也满是鲜花的另一条鲜活的路。
  • 经验告诉我,优秀的人才是那些一心想着产品的人,而不是关注管理和流程本身。
  • 乔布斯认为,人活着是为了追求极致并分享美好的东西给人类,而不是做三流产品并赚钱。这样社会才能进步,让更多的人欣赏到更美好的东西。微软不过是另一个麦当劳,哈哈。
  • 我以为我是个盖世程序猿,有一天我的程序会奔跑在千万台服务器上。我猜中了前头,可是我猜不着这结局……我们说,呸,你丫从头就错了!
  • 有人问,你为什么要从事IT技术研发工作?如果是乔布斯,可能的答案是改变世界;如果是人生导师,可能的答案是跟随你心。如果是我回答呢,答案就是如果不从事这个行业的话呢,我还真不知道该如何养家糊口

MhA

很强大,不过我认为已经是老古董了,本身没有自带MySQL代理,配合Atlas能发挥很好的作用,不过由于很久没更新,导致很多问题至今未解决,比如,更换主从过后会宕掉,主机恢复后不能自动加入等特性。
安装教程1
安装教程2

[server default]
ssh_user=root
user=mha_test
password=mysql
secondary_check_script=masterha_secondary_check -s remote_host1 -s remote_host2 # 这里要修改名字哟
master_ip_failover_script=/script/masterha/master_ip_failover # 该脚本就是主机宕掉后的触发脚本

常用快捷键

Ctrl + u: 删除当前行的输入

实用命令

open 文件/文件夹:以默认方式打开文件或文件夹,太好用了
say "": 让终端说话,居然支持中文

安装方法

参考How To Install Java with Apt-Get on Ubuntu 16.04

  • JDK是Java开发的一个工具包,其他的工具包还有J2SE、JAVA SE

  • JDK8和JDK1.8是两种新旧的命名方式,其实是一个东西

    Java SE JDK 发布时间
    Java SE 8 JDK 1.8 2014
    Java SE 11 JDK11 2018
    Java SE 17 JDK 17 2021

数据类型

  • final关键字: 修饰类表示该类不能被继承,内部所有成员变量都是final的; 类的private方法也会隐式地指定为final方法。修饰变量时,如果是基本数据类型的变量,则其数值在初始化之后就不能更改; 如果是引用类型的变量,则初始化后不能被指向另一个对象。

  • object.getField() == 1: 这种比较可能出现空指针异常

  • 获取对象的类: object.getClass()

  • 使用Optional来减少空指针异常:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    public static Optional<List<String>> getA(boolean a) {
    if (a) {
    String [] results = {"a", "b", "c"};
    return Optional.of(Arrays.asList(results));
    }
    return Optional.empty(); // 不用返回null或者空数组了
    }

    public static void test() {
    Optional<List<String>> re = getA(true);
    re.isPresent(); // 对象是否存在
    re.ifPresent(result -> {
    console.log(result);
    });
    }

Integer/Long/Double/Float/BigDecimal/AtomicInteger数字

  • 千万不要用Double/Float来定义金额,因为经常会出现浮点数的精度问题,最好用大数类,例如BigDecimal/BigInteger
  • AtomicInteger是一个线程安全整数类,同时只有一个线程可以对其操作
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
a.longValue();	// 整型转长整型
longValue.intValue(); // long转换为int
1L; // 直接将数字转换成Long型
String.valueOf(123); // 整型转字符串,避免用toString出现空指针异常
(byte)1; // int to byte,int转字节
(int) myByte; // byte to int,字节转int
(float) 1; // int to float
new Long(12); // Integer转Long

Math.ceil(9.2); // 向上取整
Math.floor(9.2);// 向下取整
Math.round(9.2); // 四舍五入
Math.abs(-0.9); // 取绝对值

a == 0 ? false : true; // 整型转换为布尔
a ? 1 : 0; // 布尔转换为整型

BigDecimal.ZERO; // 直接就是BigDecimal类型的0
a.add(b); // BigDecimal加法
a.subtract(b); // BigDecimal减法
a.multiply(b); // BigDecimal乘法
a.divide(b); // BigDecimal除法
a.compareTo(b); // 比较BigDecimal,结果为0表示相当,为-1表示小于,为1表示大于,>-1表示大于等于,小于1表示小于等于。不要用equals方法来比较BigDecimal对象,如果scale不一样,会直接返回false
a.setScale(2); // 四舍五入保留两位小数
a.setScale(2, BigDecimal.ROUND_DOWN); // 向下取整
a.setScale(2, BigDecimal.ROUND_UP); // 向上取整

String/StringBuffer字符串

String不同的是,StringBufferStringBuilder类的对戏那个能够被多次修改,并且不产生新的未使用的对戏那个。

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
// String
String a = "World";
String b = new String(array); // 将array拼接成字符串
String[] c = new String[] {"A", "B", "C"};
List<String> list = Arrays.asList(c); // String[] 转换为 List<String>
int len = b.length(); // 得到字符串长度
b.concat(a); // 连接字符串
b + a; // 连接字符串
System.out.printf("test %s", a);
String.format("test %s", a);// 格式化字符串
b.charAt(0); // 得到指定索引的字符
a.compareTo(Object o);
a.compareToIgnoreCase(String str);// 比较字符串
a.startsWith(String prefix);
a.endsWith(String suffix); // 验证字符串是否以某个子字符串结尾
a.indexOf(String str); // 返回子字符串首次出现的位置,验证是否包含某个子字符串,没找到返回-1
a.contains(str); // 直接检验是否包含某个子字符串
a.matches(".*?"); // 验证字符串是否复合正则表达式
a.replaceAll(String regex, String replacement); // 替换字符串
String[] strArr = a.split(String regex); // 拆分字符串,字符串分隔/字符串分割
a.trim(); // 移除首尾空白
Integer.parseInt(str); // 字符串转整型
Long.parseLong(str); // 字符串转Long型
String.join(",", new String[]{"foo", "bar"}) // 合并字符串,类似PHP的implode,字符串中间添加空格
new BigDecimal("1.00"); // String转BigDecimal

// 判断字符串是否为空
str == null;
"".equals(str);
str.length <= 0;
str.isEmpty();

// StringBuffer
StringBuffer c = new StringBuffer('Hello World');
c.append(String s); // 在字符串尾部追加
c.reverse(); // 反转字符串
c.capacity(); // 返回当前字符串的容量

// 日期时间
Date date = new Date();
System.out.println(date.toString());

// 字符数组转字符串,不用toString方法
char[] data = {'a', 'b', 'c'};
String str = new String(data);

// ArrayList<Chracter> to String
String getStringRepresentation(ArrayList<Character> list)
{
StringBuilder builder = new StringBuilder(list.size());
for(Character ch: list)
{
builder.append(ch);
}
return builder.toString();
}

// 字符串反转
StringBuilder sb = new StringBuilder("content");
StringBuilder re = sb.reverse();

List list = new ArrayList(myCollections); // Collections转list

// URLDecode/URLEncode,需要注意的是,如果出现特殊符号%,后面跟着中文,那么decode居然会报错
URLDecode.decode("test", "utf-8");
JSON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Json字符串转换为Dto
MyDto myDto = new Gson().fromJson(jsonString, MyDto.class);
new JsonParser().parse(jsonString).getAsJsonObject().get("key1").toString(); // 直接获取指定的key的值,而不用新建一个对象。但是有个坑是这样得到的字符串两边会带上引号。。。

// 验证是否是Json字符串
try {
JSONObject result = JSONObject.parseObject(string);
return null != result;
} catch (Exception e) {
return false;
}

// 任意对象转JSON字符串
import com.google.gson.Gson;
Gson gson = new Gson();
String jsonString = gson.toJson(myObj);
System.out.println(jsonString);
正则匹配
  • java的正则匹配没有findAll的概念,需要自己在正则中加入类似()*来实现多次匹配
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Pattern p = Pattern.compile("(a.*?a)*", Pattern.CASE_INSENSITIVE|Pattern.MULTILINE);	// 可以配置大小写不敏感;查找多行
Matcher matcher = p.matcher("content");
// 遍历匹配结果方式一
while (matcher.find()) {
System.out.println(matcher.group());
}
// 遍历匹配结果方式二
if (matcher.find() && matcher.groupCount() >= 1) {
matches = new ArrayList();
for (int i = 1; i <= matcher.groupCount(); i++) {
System.out.println(matcher.group(i));
}
}

// 正则替换
str.replaceAll(reg, "");

Array/Vector/Stack/Enumeration/Collections/ArrayList数组

  • 数组的大小是无法改变的,如果要实现改变数组长度,可以采取新建一个数组然后返回新数组的指针的方式。
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
// 初始化&赋值
typeName[] arrayName; // 声明数组的基本方式,也可以typeName arrayName[]
typeName arrayName[][] = new typeName[3][4]; // 定义多维数组
double[] myList = new double[5]; // 创建指定长度的数组
List<String> names = Arrays.asList("xxx","yyy","zzz"); // 直接初始化固定长度的数组,但是要超过一个元素才行
List<String> list1 = new ArrayList<>(); // 初始化一个空数组,之后用add添加元素
list1.addAll(list2); // 将数组2合并到数组1
List<String> names = new ArrayList<String>() {
{
for (int i = 0; i< 10; i++) {
add("add"+i);
}
}
};

Arrays.asList("a", "b").size(); // 获取数组长度length
Arrays.asList("a", "b").contains("c"); // 数组是否包含某个值

// 遍历数组
for (double element: myList) {}
for (int i = 0 ; i < myList.size(); i++) {}

// 遍历数组并移除元素
Iterator<String> iterator = myList.iterator();
while (iterator.hasNext()) {
String item = iterator.next();
if (item.equals("banana")) {
iterator.remove(); // 根据条件删除元素
}
}


// Vector类,动态数组
// Stack栈
Stack<Integer> d = new Stack<integer>();
d.push(int a);

// Enumeration枚举类型
Enumeration<String> days; // 定义枚举变量
Vector<String> dayNames = new Vector<StringL>();
dayNames.add("Sunday"); // 添加枚举元素
days = dayNames.elements();

// 数组反转
List<String> new = Lists.reverse(lists1);

// 数组分片
List<E> subList(fromIndex, toIndex);

Set/HashSet/Stream集合

1
2
String[] myList = new String[] { "a", "b" };
Set<String> mySet = new HashSet<String>(Arrays.asList(myList)); // 初始化
Stream API
  • 是一系列对集合便利操作的工具集,类似Laravel里面的Collection
  • Concat: 合并两个流: Stream.concat(stream1, stream2)
  • foreach: 遍历
  • map: 映射,返回新的元素
  • mapToInt/mapToDouble/mapToLong: 映射成指定的数字类型,映射完成后可以使用summaryStatistics方法得到统计的结果然后使用getMax/getMin/getSum/getAverage等方法
  • filter: 过滤,仅保留返回true的元素
  • limit: 仅保留指定数量的元素
  • sorted: 排序
  • Collectors: 用于返回列表或字符串
1
2
3
4
5
6
7
8
9
10
11
// 过滤
Record record = list.stream()
.filter(record -> "name".equals(record.getName()))
.findFirst()
.orElse(null);

// 排序
Record record = list.stream()
.sorted(Comparator.comparingInt(Record::getTime))
.reversed()
.collect(Collectors.toList());

Dictionary/Hashtable/Map/ConcurrentHashMap字典

  • ConcurrentHashMap是线程安全的
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
// Dictionary字典
// Hashtable

// map的初始化
HashMap<String, String> map = new HashMap<String, String>();
map.put("key", "value");
HashMap<String, String> map = new HashMap<String, String>() {
{
map.put("key1", "value1");
map.put("key2", "value2");
}
};

map.containsKey(Object key); // 是否包含某个key
map.containsValue(Object value); // 是否包含某个value
map.equals(Object o); // 比较指定的对象与此映射是否相等个
map.get(Object key); // 获取某个key的值
map.isEmpty(); // 是否为空
map.put(K key, V value); // 设置值
map.remove(Object key); // 移除某个键值对
map.size(); // 获取键值对数量
map.values(); // 返回所有的value,是一个Collection对象

// Map的遍历
Map<String, String> map = new HashMap<String, String>();
// 遍历方法一
for (Map.Entry<String, String> entry : map.entrySet()) {
System.out.println(entry.getKey(), entry.getValue);
}
// 遍历方法二
for (String key : map.keySet()) {String value = map.get(Key);}
for (String value : map.values()) {}

// Map转为Json格式字符串
String jsonStr = new Gson().toJson(myMap);

Queue队列

1
2
3
4
5
6
7
Queue<String> queue = new LinkedList<String>();	// 定义队列
queue.offer("a"); // 添加元素,如果无法添加会返回false
queue.add("a"); // 添加元素,如果无法添加会抛出来异常
queue.poll(); // 返回第一个元素,并在队列中删除,没有会返回null
queue.remove(); // 从队列删除第一个元素,没有会抛出异常
queue.element(); // 返回第一个元素,没有会抛出异常
queue.peek(); // 返回第一个元素,没有会返回null

时间处理

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
Date date = new Date();	// 获取时间对象
Long timestamp = date.getTime(); // 获取时间戳(毫秒)
System.currentTimeMillis(); // 毫秒级时间戳
Date date = new Date(1234567890000); // 毫秒级时间戳转Date对象
Date date = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse("2020-05-01 00:00:00"); // 获取指定日期的date

// 获取今天开始的时间
Calendar calendar = Calendar.getInstance();
calendar.setTime(new Date());
calendar.set(Calendar.HOUR_OF_DAY, 0);
calendar.set(Calendar.MINUTE, 0);
calendar.set(Calendar.SECOND, 0);
Date zero = calendar.getTime();

// 获取ISO8601格式的时间,类似2019-12-12T12:12:12Z
TimeZone tz = TimeZone.getTimeZone("UTC");
DateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'");
df.setTimeZone(tz);
return df.format(new Date());

// 解析CST格式的时间
String dateStr = "Wed Sep 11 10:10:10 CST 2020";
Date date = (Date) df.parse(df);

// 时间计算
date1.before(date2); // 判断date1是否在date2之前

Calendar now = Calendar.getInstance()
now.setTime(date); // 可以指定其他的date,不用非要是进Tina
now.set(Calendar.DATE, now.get(Calendar.DATE) + 7); // 计算7天后的时间

类/对象/方法

  • 类中可以使用static {}设置静态代码块,有助于优化程序性能,static块可以放置于类中的任何地方,当类初次被加载的时候,会按照static块的顺序来执行每个块,并且只会执行一次。
  • 泛型类使用<T>来表示,? extends 类名(上边界限定)表示只要继承某个类的都可以,? super 类名(下边界限定)表示只要是某个类的父类都可以,单独的?(无边界限定)表示没有任何限制
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
// 一个类可以有多个构造方法
public class Sample {
static { // 静态代码块
long a = System.nanoTime();
testMethod();
}
public Sample() {} // 不带参数的构造方法
public Sample(String param1) {} // 带参数的构造方法
private static void testMethod() {}
}

// 泛型类,T可以传入任意类型
public class MyClass<T> {
// 成员变量
private T t;
public MyClass(T t) {
super();
this.t = t;
}
public T getT() {return t;}
}

// 方法中使用Optinal表示可能为null,很大程度上能帮助调用者了解内部可能返回null
public Optinal<User> getUser(Long id) {
if (null != id) {return Optinal.of(new User());}
return Optinal.empty();
}
Optional<user> userOp = getUser(1L);
if (userOp.isPresent()) {...} else {...}

Function接口

  • Java8新增的函数式编程方法,主要用来做lambda表达式

  • 任何标注了@FunctionalInterfaced都接口都表示是一个函数式的接口

  • Functiond源码简介:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    @FunctionalInterface
    public interface Function<T, R> { // T表示入参,R表示出参
    R apply(T t);
    // compose接收一个Function参数,返回时先用传入的逻辑执行apply,然后在执行当前Function的apply,
    // 相当于 a.compose(b).apply(1) = a.apply(b.apply(1))
    default <V> Function<V, R> compose(Function<? super V, ? extends T> before) {
    Objects.requireNonNull(before);
    return (V v) -> apply(before.apply(v));
    };
    // andTthen是先执行当前的逻辑,再执行传入的逻辑。
    // 相当于a.andThen(b).apply(1) = b.apply(a.apply(1))
    default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {
    Objects.requireNonNull(after);
    return (T t) -> after.apply(apply(t));
    };
    static <T> Function<T, T> identity() {
    return t -> t;
    };
    }
  • 例子:

1
2
Function<Integer,Integer> test=i->i+1;
test.apply(1); // 会得到2

异常处理

  • 异常类的getMessage()toString()方法的区别,前者仅仅返回错误的信息,如果是空指针异常,一般返回的是null,而后者则会包含异常类的类型信息。建议如果是打印日志则使用toString() 方法,因为更详细,如果是给用户看的信息可以使用getMessage方法仅展示给用户关键信息

文件/文件夹

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
// 以行为单位读取文件
File file = new File(fileName);
BufferedReader reader = null;
reader = new BufferedReader(new FileReader(file));
String tempString = null;
int line = 1;
while ((tempString = reader.readLine()) != null) {
System.out.println("line " + line + ": " + tempString);
line++;
}
reader.close();

// 读取整个文件
File file = new File(fileName);
if (file.isFile() && file.exists()) {
long fileLength = file.length();
byte[] fileContent = new byte[(int) fileLength];
FileInputStream in = new FileInputStream(file);
in.read(fileContent);
in.close();
String[] fileContentArr = new String(fileContent); // 结果字符串数组
}

// 写入文件/新建文件
File file = new File(path); // new File第二个参数如果为true,表示追加
if (!file.exists()) {
file.createNewFile();
}
FileWriter fw = new FileWriter(file.getAbsoluteFile());
BufferedWriter bw = new BufferedWriter(fw);
bw.write(content);
bw.close();

// 新建文件夹
File dir = new File("/tmp/test/deep");
if (!dir.exists()) {
dir.mkdirs();
}

Shell

1
2
3
4
5
6
7
8
// Java执行shell命令
String cmd = "ls | grep abc";
String[] commands = {"/bin/sh", "-c", cmd}; // 加入/bin/sh可以防止很多命令执行出错或者转义出错
Process process = Runtime.getRuntime().exec(commands);
BufferedReader stdInput = new BufferedReader(new InputStreamReader(process.getInputStream()));
while ((s = stdInput.readline()) != null) {
System.out.println(s);
}

  • JavaSE程序可以打包成Jar包(与平台无关的格式),JavaWeb程序可以打包成War包
1
2
3
import java.io.*;		// 导入java_installation/java/io下的所有类
java -jar myjar.jar; // 直接用命令行运行jar包
java -cp myjar.jar com.example.MainClass // 指定jar入口

线程/进程

  • ThreadLocal: 保证线程安全(一次HTTP请求从开始到响应都是在一个线程内部,其他用户是在其他的线程里面)
1
2
Thread current = THread.currentThread();	// 获取当前进程
current.getId(); // 获取当前进程Id

多线程

  • 线程池只能放入实现Runnable/callable类的线程,不能放入继承Thread的类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 方法一、继承Thread类,缺点是无法多重继承
public class MyThread extends Thread {
@Override
public void run()
{
System.out.println("线程执行");
}
}
new MyThread().start();

// 方法二、实现Runnable接口,适合多线程共享资源
public class MyThread implements Runnable {
public void run () {
System.out.println("线程执行");
}
}

MyThread mythread = new MyThread();
new Thread(mythread).start();

三方库

Jsch SSH连接工具

一个很老很久没有更新的工具,文档example比较全,但是只有这个工具用户量大一点,其他的用户量太少不敢用(Apache sshd则是因为文档太少,官方文档是针对它的server端)。执行shell命令的时候建议使用ChannelExec而不是ChannelShell(因为后者的输出流里面类似于一个终端,会包含一些没用的命令提示符).

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
Jsch jSch = new JSch();
jSch.addIdentity("name", prvKeyStr.getBytes, pubKeyStr.getBytes, keyPass.getBytes); // 加载私钥公钥和私钥密码
Session session = jSch.getSession(username, ip, port); // 新建session
session.setConfig("StrictHostKeyChecking", "no"); // 不进行静态host-key校验,否则可能出现UnknownHostKey错误
session.setTimeout(10000); // 设置连接超时时间毫秒
session.connect(); // 连接

// 执行命令并获取返回结果
ChannelExec channelExec = (ChannelExec) this.session.openChannel("exec");
ByteArrayOutputStream out = new ByteArrayOutputStream();
ByteArrayOutputStream error = new ByteArrayOutputStream();
channelExec.setCommand("ls"); // 实际执行的命令
channelExec.setOutputStream(out);
channelExec.setErrStream(error);
channelExec.connect();
int sleepCount = 0;
do { // 等待命令返回,官方手册是用的这种方法
try {
Thread.sleep(100);
} catch (InterruptedException e) {
result.setExitCode(1);
result.setStderr(SERVER_EXEC_ERROR + e.getMessage());
return result;
}
} while (!channelExec.isClosed() && sleepCount++ < 60);
out.toString(); // 标准输出
error.toString(); // 标准错误输出
channelExec.getExitStatus(); // 获取返回状态码

TroubleShooting

  • **Expected BEGIN_OBJECT but was STRING at line 1 column 1 path $**,这是在使用Gson解析字符串时的报错,一般是因为字符串非标准Json格式造成的

  • Unchecked assignment for ‘java.util.ArrayList’ to ‘java.util.ArrayList <…>: 可能是定义ArrayList的时候没有使用<>,可以用下面两种方法进行定义:

    1
    2
    ArrayList<MyList> myList = new ArrayList<>();
    ArrayList<MyList> myList = new ArrayList<MyList>();
  • **com.alibaba.fastjson.JSONException: default constructor not found. **: fastjson的坑,要求对应class必须有默认的构造函数(空参数)

  • fastjson出现$ref: $.data[2].indexs[0]: 又是fastjson的坑,如果是需要序列化的对象中有对同一个对象的依赖,那么在JSON序列化中可能会将后续的对象转成这种字符串

扩展阅读

LEMP: Linux, Nginx, MySQL, PHP环境

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
# 依赖安装
yum install epel-release gcc automake autoconf libtool make gcc gcc-c++ glibc-y

# MySQL
yum install mysql-server -y
/etc/init.d/mysqld restart
/usr/bin/mysql_secure_installation # 初始化设置数据库

# Nginx
yum install nginx -y
/etc/init.d/nginx start

# PHP
add-apt-repository universe && apt-get install php-fpm php-mysql # for ubuntu
yum install php-fpm php-mysql # for centos
vim /etc/php.ini,将cgi.fix_pathinfo=1改为cgi.fix_pathinfo=0
vim /etc/php-fpm.d/www.conf将apache用户更改为nginx用户
user = nginx
group = nginx

service php-fpm restart # 如果php-fpm: unrecognized service,可以在/usr/lib/systemd/system/目录下看具体的服务名
service nginx restart

# 开机启动
systemctl enable mysqld
systemctl enable php-fpm
systemctl enable nginx

# 老版本使用这个命令
chkconfig --levels 235 mysqld on
chkconfig --levels 235 nginx on
chkconfig --levels 235 php-fpm on

nginx配置php-fpm

  • 如果发现9000端口没有启动那么php-fpm应该是以socket的方式启动的
1
2
3
4
5
6
7
8
location ~ \.php$ {    
fastcgi_pass 127.0.0.1:9000; # 端口方式
fastcgi_pass unix:/run/php/php7.0-fpm.sock; # socket方式

fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}

参考理解OAuth 2.0

目前网络上的数据传输普遍基于HTTP/HTTPS,然而,他们都是无状态的协议,也就是说你本次的请求与下一次的请求是毫无关系的,为了使得我们能够保存用户的状态,于是诞生了cookie,诞生了session,诞生了token,当然,这三者的概念其实都差不多,总体思想就是,在第一次请求过后,给用户生成唯一的一个标识,用户在下一次请求时,携带这个标识,这样服务端就能通过它来判断用户的状态以及合法性。

注意: OAuth确实增加了安全性,但是也增加了应用的复杂性,对于安全要求没那么高的应用,依然可以使用简单的加密算法进行双向加解密认证。

OAuth

OAuth主要用于社会化登录,仅仅需要一个系统来存储用户的信息,其他的系统或者第三方系统均可以使用,其他系统不需要维护自己的用户系统,免去了用户注册账号的麻烦,并且用户的密码等重要信息都集中保存在信任方,提高了安全性。

根据阮一峰的博客,客户端获取授权有以下几种认证方式:

  1. 授权码模式(authorization code): 是功能最完整、流程最严密的授权模式。它的特点就是通过客户端的后台服务器,与”服务提供商”的认证服务器进行互动。
  2. 简化模式(implicit): 不通过第三方应用程序的服务器,直接在浏览器中向认证服务器申请令牌,跳过了”授权码”这个步骤,因此得名。所有步骤在浏览器中完成,令牌对访问者是可见的,且客户端不需要认证。
  3. 密码模式(resource owner password credentials):用户向客户端提供自己的用户名和密码。客户端使用这些信息,向”服务商提供商”索要授权。
  4. 客户端模式(client credentials):指客户端以自己的名义,而不是以用户的名义,向”服务提供商”进行认证。严格地说,客户端模式并不属于OAuth框架所要解决的问题。在这种模式中,用户直接向客户端注册,客户端以自己的名义要求”服务提供商”提供服务,其实不存在授权问题。

举个例子
具体流程(感觉不用图也能说清楚呀):

  1. 第三方站点获取未授权的Request Token
  2. 获取用户授权的Request Token
  3. 用授权的Request Token换取Access Token
  4. 通过Access Token访问该用户的资源

另外,对于客户端的每一次获取token的请求,都对应着一定的权限,比如微博的第三方认证,通常仅有获取用户基本信息的权限,或者如github的权限就更加详细一点,包括获取基本信息,公开库的访问权限和私有库的访问权限(github的认证请求权限使用的是X-OAuth-Scopes作为参数放在请求头部,例如user,表示能获取用户的所有信息,而user.email则表示只能获取用户的邮箱)。

对于第三方应用,仅需要维护token的值,一般来说,有一种用户基本信息表,多张token存储表对应着多个公开应用。例如接入了微博的第三方可以有这么一张表: (id, user_id, weibo_id, weibo_access_token, weibo_expires)

注: 社会化登录的回调地址(callback_url)与返回地址(next)的区别,回调地址指用户点击第三方登录过后,由资源服务器回调到的地址,而返回地址则是用户欲访问的地址,一般为应用自己的首页,但也有可能是用户直接知道url后想去的地址,这时候可以在请求的时候带上next参数,然后由资源服务器原样返回过去。

最后,几乎所有关于OAuth的文章都会遗漏的一点,那就是资源服务器和认证服务器之间的认证。当访问者获取了token过后,向资源服务器发送请求时,资源服务器肯定会向认证服务器验证这个token的合法性,这也是一个很危险的步骤,在实际项目中,我将token与访问者的appkey一起进行认证,并且作为认证服务器同样要对资源服务器的认证请求一起认证。具体可参考stackoverflow

SSO(单点登录)

解决的是跨域的用户校验问题,常用于一个公司内部有多个子网站,然后提供统一的登录,一个站点登录过后其他站点不用重复登录。多用于存在多个子系统的公司。
具体流程:

  1. 用户请求任意一个子站的login页面
  2. 用户输入用户名密码,子站将它传递给认证中心SSO Server
  3. SSO Server验证成功后

虽然SSO和OAuth是不一样的东西,但是我认为SSO只是OAuth的一种简化模式,可以归为OAuth的一类。通常由于子系统的域名不一样,不方便设置各自的cookie,于是这里又有两种解决方案:

  1. 使用公共加解密函数进行双向加解密(加密字符串直接放在GET请求中)
  2. 同样使用公共的加解密函数,但是是使用JSOPN解决跨域问题(用户登录子应用时,会带上父应用域名下的cookie访问父应用提供的JSONP接口,父应用验证登录状态,最后返回加密后的用户信息)

例如,本人在一个项目中所使用的认证流程:

图片

签名算法的设计

无论是OAuth或是SSO都涉及到认证的过程,一般都不推荐使用可逆的加密算法,而使用单向的加密算法,只要双方对数据进行加密后的结果一致就表示该请求是合法的。参考网上很多的签名算法,最后总结了一下使用最广也是最安全的一种签名算法步骤:

  1. 将请求的参数转换为’key=value’形式的字符串,如: “k1=v1”, “k2=v2”
  2. 将格式化后的字符串以字典升序进行排序,然后拼接在一起,如: “k1=v1k2=v2”
  3. 在拼接后的字符串的前后加上Secret Key,如:”keyk1=v1k2=v2key”
  4. 对拼接后的字符串进行MD5加密得到最终的结果

Python实现

1
2
3
4
5
6
7
8
9
10
import hashlib
def apiSign(parameters, secret):
keys = list(parameters.keys())
keys.sort()

parameters_str = "%s%s%s" % (
secret,
str().join('%s%s' % (key, parameters[key]) for key in keys),
secret)
return hashlib.md5(parameters_str.encode('utf8')).hexdigest()j

PHP实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function apiSign($parameters, $secret)
{
$str = '';

if (gettype($parameters) === 'array') {
ksort($parameters);
foreach ($parameters as $k => $v) {
$str .= $k.$v;
}
} elseif (gettype($parameters) === 'string') {
$str = $parameters;
} else {
return false;
}
$str = $secret.$str.$secret;

return md5($str);
}

Java实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private static String apiSign(List<NameValuePair> nvps) throws NoSuchAlgorithmException, IOException {
List<String> nvs = Lists.newArrayList();
for (NameValuePair nvp: nvps) {
boolean noSignValue = nvp == null || nvp.getValue() == null ||
SIGN_EXCEPTIONS.contains(nvp.getName());
if (noSignValue) continue;
nvs.add(String.format("%s=%s", nvp.getName(), nvp.getValue()));
}
Collections.sort(nvs);
StringBuilder sb = new StringBuilder();
for (String nv: nvs) sb.append(String.format("%s", nv));
String encodeSource = String.format("%s%s", sb.toString(), APP_SECRET);
return MD5Util.MD5Encode(encodeSource, ENCRYPT_CHARSET).toLowerCase();
}

travis-ci

与github紧密联系的自动化持续集成工具。需要注意的是它仅是一个测试工具,并不能代替webhook的功能。

.travis.yml文件

travis.yml是travis-ci的配置文件,具体语法如下:

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
sudo: required		# 是否需要sudo权限
dist: trusty # 目标操作系统,这是ubutnu 14.04

notifications: # 发送构建结果的通知
email:
- haoflynet@gmail.com

addons:
apt:
sources: # 源,但只能是白名单内的https://github.com/travis-ci/apt-source-whitelist/blob/master/ubuntu.json
- google-chrome
packages:
- google-chrome-stable

branches: # 指定需要跑travis-ci的分支
only:
- master

language: node_js # 项目所使用的语言
node_js: # 该语言所运行的版本,可多个版本同时测试
'4'
'6'

# 下面几条指令会按顺序执行
before_install:
- nvm install 6.0.0

install: npm install

before_script: # 在开始测试前需要运行的指令,也可在这里执行sudo apt-get 安装一些基础trusty没有的包,当然,安装包可以直接使用addons的apt
- npm install
- cd tests

script: phpunit -v # 执行脚本

after_failure:
- cat /home/travis/build/haoflynet/haoflynet-repo/abc.log

after_success:
- ...

before_deploy: # 一些部署操作,比如生成APK包
- ...

deploy:
provider: script
script: ci/deploy.sh
skip_cleanup: true

after_deploy: # 发布的一些操作,比如将包发布到fir.im上去
- ...

说实话,李笑来的这本《把时间当作朋友》确实绕过去绕过来,把我饶得有点晕了,但是书中的主要观点我还是非常认同的。把时间当作朋友。从来都只听过如何去管理时间,却从来没听说过要去和时间做朋友。上帝给每个人的时间都是一样的,不是说你想怎么管理就怎么管理,从小学到大学到工作,每次考试前你不都有很多的复习计划,每次假期前不都有很多的出游计划吗,但是,有多少我们真正实现过呢。很多时候计划越详细,我们反而越不会执行。为什么?因为我们不了解时间,也不了解自己。就像你想和一个人做朋友一样,你们都不互相了解,非要在一起,那也是互相折磨而已。所以,要想与时间和睦相处,首先是要了解自己,自己是一个怎么样的人,自己喜欢做什么样的事情,什么样的事情会让自己有动力去做,到底为什么而活?

没有人不懒惰,没有人不爱做计划,只是那些成功的人普遍都是做了计划就立马行动的,这真的真的是我目前为止发现的最大的缺点。想太多,做得少。基本上从高中就开始意识到这个缺点了,也一直在改,在改的过程中又发现了另一个缺点,那就是只要我心里有一块石头,就不能放下它去做其他的事情了。我在外面实习都快一年了,这一年里感觉还没我大一一个暑假成长得快,我知道自己遇到了一个瓶颈,也做了很多的计划去冲破它,但就是碍于学校里数不清的杂事,一直没放心大胆地去做。最后导致两边都互相拖着,然后两边都没做好。其实专注于一件事没有什么不好,但是心中担心一件事情,就直接把那件事情做了就好呀,这有什么好犹豫的呢,有什么好纠结的呢。那件事情制约着我,我干嘛就不把他做了呢,一定要拖到最后。不过还好,毕业答辩做了充足的准备,还是顺利通过了。在昨天那个不存在的日子里进行的答辩,而且在答辩前几天我就已经开始我的全栈实践之路了。马上进入社会,希望能真的和时间做好朋友,然后和它并肩作战。

语录

当你把时间花在一个人身上的时候,相当于在他的身上倾注了你生命的一段,不管最终结果如何,反正,那个人、那件事都成了你生命的一部分——不管最后你是喜欢还是不喜欢。

一个人遗忘痛苦的能力特别强的一个具体表现就是,这个人会很轻易地原谅自己。

往往并不是有兴趣才能做好,而是做好了才有兴趣。

浪费生命、虚度年华的人,有个共同的特征——他们拼命想控制自己完全不能控制的,却在自己真正能掌控的地方彻底失控。

当我们不停地鼓励所有人的时候,最大的受益者其实是我们自己,因为最终我们会发现,自己开始进入一种他人无法想象的状态,成为一个不需要他人鼓励的人。

心智真正成熟的人在一些情况下能够做到无须亲自经历,仅凭思考就得到深刻的体会。

所谓的幸运,就是当你准备好了的时候,机会来了。

你比别人强一点根本没用,真正有用的是你比别人强很多很多。

学东西前不用定义一定要有用——别觉得知识现在没用就拒绝去学它。

人是没办法管理时间的,时间也不听从任何人的管理,它只会自顾自一如既往地流逝。”管理时间”只不过是人们的一厢情愿而已。

这些人其实并不是对自己正在做的事情没有兴趣,而是没有能力把目前正在做的事情做好。

本科教育之”本”在于培养学生的自学能力。从理论上讲,一个人本科毕业之后,应该有能力自学他所需要的任何知识。