Java commons-exec 执行外部命令

Java创建子进程(Process)执行外部命令底层的方法是new ProcessBuilder().start()或Runtime.getRuntime().exec()。

Apache commons-exec对底层进行封装,提供了更加详细的设置和监控方法。

pom.xml

1
2
3
4
5
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-exec</artifactId>
<version>1.3</version>
</dependency>

CmdHelper.java

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
import java.io.IOException;
import java.io.OutputStream;
import java.util.concurrent.CompletableFuture;

import org.apache.commons.exec.CommandLine;
import org.apache.commons.exec.DefaultExecutor;
import org.apache.commons.exec.ExecuteWatchdog;
import org.apache.commons.exec.PumpStreamHandler;
import org.apache.commons.exec.ShutdownHookProcessDestroyer;

public class CmdHelper {
/**
* 执行外部命令,等待返回结果
*
* @param commandLine: 命令行
* @param out: 输出流,为空默认标准输出
* @param timeout: 超时,不大于0则不限超时
* @return CmdHandler based on DefaultExecuteResultHandler
*/
public static CmdHandler run(CommandLine commandLine, OutputStream out, long timeout) {
PumpStreamHandler pumpStreamHandler = null;
if (null == out) {
pumpStreamHandler = new PumpStreamHandler();
} else {
pumpStreamHandler = new PumpStreamHandler(out);
}

DefaultExecutor executor = new DefaultExecutor();
CmdHandler cmdHandler = new CmdHandler(executor);
executor.setStreamHandler(pumpStreamHandler);
ShutdownHookProcessDestroyer processDestroyer = new ShutdownHookProcessDestroyer();
executor.setProcessDestroyer(processDestroyer);// 随主进程退出

if (timeout <= 0) {
timeout = ExecuteWatchdog.INFINITE_TIMEOUT;
}
ExecuteWatchdog watchdog = new ExecuteWatchdog(timeout);
executor.setWatchdog(watchdog);// 控制超时

try {
executor.execute(commandLine, cmdHandler);
cmdHandler.waitFor();// 等待返回结果
} catch (IOException | InterruptedException e) {
e.printStackTrace();
}
return cmdHandler;
}

/**
* 异步执行外部命令
*
* @param commandLine: 命令行
* @param out: 输出流,为空默认标准输出
* @param timeout: 超时,不大于0则不限超时
* @return CompletableFuture<CmdHandler>
*/
public static CompletableFuture<CmdHandler> exec(CommandLine commandLine, OutputStream out, long timeout) {
CompletableFuture<CmdHandler> cf = new CompletableFuture<>();
PumpStreamHandler pumpStreamHandler = null;
if (null == out) {
pumpStreamHandler = new PumpStreamHandler();
} else {
pumpStreamHandler = new PumpStreamHandler(out);
}

DefaultExecutor executor = new DefaultExecutor();
CmdHandler cmdHandler = new CmdHandler(executor);
cmdHandler.setCallback(() -> {
cf.complete(cmdHandler);// 执行完成后回调
});
executor.setStreamHandler(pumpStreamHandler);
ShutdownHookProcessDestroyer processDestroyer = new ShutdownHookProcessDestroyer();
executor.setProcessDestroyer(processDestroyer);// 随主进程退出

if (timeout <= 0) {
timeout = ExecuteWatchdog.INFINITE_TIMEOUT;
}
ExecuteWatchdog watchdog = new ExecuteWatchdog(timeout);
executor.setWatchdog(watchdog);// 控制超时

try {
executor.execute(commandLine, cmdHandler);
} catch (IOException e) {
e.printStackTrace();
}
return cf;
}

public static void main(String[] args) throws InterruptedException {
CommandLine command = CommandLine.parse("ping 127.0.0.1 -t");

// 测试同步执行
CmdHandler result = CmdHelper.run(command, null, 3000);
System.out.println(result.resultString());

// 测试异步执行
CmdHelper.exec(command, null, 0).thenAccept(cmdHandler -> {
System.out.println(cmdHandler.resultString());
});
}
}

CmdHandler.java

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
import org.apache.commons.exec.DefaultExecuteResultHandler;
import org.apache.commons.exec.ExecuteException;
import org.apache.commons.exec.ExecuteWatchdog;
import org.apache.commons.exec.Executor;

public class CmdHandler extends DefaultExecuteResultHandler {
Executor executor;
Runnable callback;

public CmdHandler(Executor executor) {
this.executor = executor;
}

public void setCallback(Runnable callback) {
this.callback = callback;
}

public Executor getExecutor() {
return this.executor;
}

public ExecuteWatchdog getWatchdog() {
if (this.executor == null) return null;
return this.executor.getWatchdog();
}

public String resultString() {
String retMsg = "complete";
if (this.getException() != null) {
ExecuteWatchdog watchdog = this.getWatchdog();
if (watchdog != null && watchdog.killedProcess()) {
retMsg = "timeout";
} else {
retMsg = this.getException().getMessage();
}
}
return this.getExitValue() + ":" + retMsg;
}

@Override
public void onProcessComplete(int exitValue) {
super.onProcessComplete(exitValue);
if (callback != null) {
callback.run();
}
}

@Override
public void onProcessFailed(ExecuteException e) {
super.onProcessFailed(e);
if (callback != null) {
callback.run();
}
}
}

参考

  1. https://www.cnblogs.com/kingcucumber/p/3180146.html
  2. https://www.jianshu.com/p/73aaec23009d

java微信开发常用方法

WeixinService.java

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
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
public class WeixinSercice {
final static String URL_SNS_TOKEN = "https://api.weixin.qq.com/sns/oauth2/access_token?appid=%s&secret=%s&code=%s&grant_type=authorization_code";
final static String URL_ACCESS_TOKEN = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=%s&secret=%s";
final static String URL_JSAPI_TICKET = "https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token=%s&type=jsapi";

final static String URL_ORDER_QUERY = "https://api.mch.weixin.qq.com/pay/orderquery";
final static String URL_UNIFIED_ORDER = "https://api.mch.weixin.qq.com/pay/unifiedorder";

public static final String TRADE_TYPE_H5 = "MWEB";
public static final String TRADE_TYPE_JS = "JSAPI";

final static String KEY_SIGN = "sign";

@Value("${weixin.appid}")
private String appId;
@Value("${weixin.secret}")
private String appSecret;
@Value("${weixin.mch_id}")
private String mchId;
@Value("${weixin.mch_key}")
private String mchKey;
@Value("${weixin.notify_url}")
private String notifyUrl;

@Autowired
RestTemplate restTemplate;
@Autowired
OrderService orderService;
@Resource
private CacheManager cacheManager;
private static final String CACHE_NAME_WEIXIN = "myapp:weixin";
private static final String CACHE_KEY_ACCESS_TOKEN = "actoken";
private static final String CACHE_KEY_JSAPI_TICKET = "jsticket";
private Cache getCache() {
return cacheManager.getCache(CACHE_NAME_WEIXIN);
}

// 微信网页认证:通过code获取token
public WxWebToken fetchWebTokenByCode(String code) {
String url = String.format(URL_SNS_TOKEN, appId, appSecret, code);
String content = restTemplate.getForObject(url, String.class);
WxWebToken token = JSON.parse(content, WxWebToken.class);
return token;
}

// 微信JSSDK:获取指定url的config
public WxJsdkConfig genJsdkConfig(String url) {
WxJsdkConfig config = new WxJsdkConfig();
config.setAppId(appId);
config.setNonceStr(StringUtils.uuid());
config.setTimestamp(DateTimeUtils.secondsOf(LocalDateTime.now()));

String ticket = getJsapiTicket(url);
if (StringUtils.isNotBlank(ticket)) {
String string = "jsapi_ticket=" + ticket +
"&noncestr=" + config.getNonceStr() +
"&timestamp=" + config.getTimestamp() +
"&url=" + url;
String signature = StringUtils.SHA1(string);

if (StringUtils.isNotBlank(signature)) {
config.setSignature(signature);
}
} else {
config.setErrcode(1);
config.setErrmsg("invalid ticket");
}
log.debug("genJsdkConfig for {} return {}", url, config.getSignature());
return config;
}

//get cached jsapi_ticket
private String getJsapiTicket(String url) {
String cacheKey = CACHE_KEY_JSAPI_TICKET+StringUtils.MD5(url);
WxJsapiTicket ticket = getCache().get(cacheKey, WxJsapiTicket.class);
if (ticket == null || ticket.getExpired()) {
ticket = fetchJsapiTicket();
if (ticket != null && !ticket.getExpired()) {
getCache().put(cacheKey, ticket);
}else {
return null;
}
} else {
log.debug("return cached {} for key {}", CACHE_NAME_WEIXIN, cacheKey);
}
return ticket.getTicket();
}

private WxJsapiTicket fetchJsapiTicket() {
String accessToken = getAccessToken();
if (StringUtils.isBlank(accessToken)) {
return null;
}

String url = String.format(URL_JSAPI_TICKET, accessToken);
String content = restTemplate.getForObject(url, String.class);
WxJsapiTicket ticket = JSON.parse(content, WxJsapiTicket.class);
if (ticket != null) {
Long expires = ticket.getExpires_in();
if (expires != null) {//把过期秒数转化为世纪秒
expires += DateTimeUtils.secondsOf(LocalDateTime.now());
}else{
expires = 0L;
}
ticket.setExpires_in(expires);
log.debug("fetchJsapiTicket return {}-{}", ticket.getErrcode(), ticket.getErrmsg());
}
return ticket;
}

// get cached access_token
private String getAccessToken() {
WxAccessToken token = getCache().get(CACHE_KEY_ACCESS_TOKEN, WxAccessToken.class);
if (token == null || token.getExpired()) {
token = fetchAccessToken();
if (token != null && !token.getExpired()) {
getCache().put(CACHE_KEY_ACCESS_TOKEN, token);
}
} else {
log.debug("return cached {} for key {}", CACHE_NAME_WEIXIN, CACHE_KEY_ACCESS_TOKEN);
}
return token.getAccess_token();
}

private WxAccessToken fetchAccessToken() {
String url = String.format(URL_ACCESS_TOKEN, appId, appSecret);
String content = restTemplate.getForObject(url, String.class);
WxAccessToken token = JSON.parse(content, WxAccessToken.class);
if (token != null) {
Long expires = token.getExpires_in();
if (expires != null) {//把过期秒数转化为世纪秒
expires += DateTimeUtils.secondsOf(LocalDateTime.now());
}else{
expires = 0L;
}
token.setExpires_in(expires);
if (StringUtils.isNotBlank(token.getErrmsg())){
log.debug("fetchAccessToken return {}-{}", token.getErrcode(), token.getErrmsg());
}else{
log.debug("fetchAccessToken return {}", token.getAccess_token());
}
} else {
log.debug("fetchAccessToken return null");
}
return token;
}


//处理订单
public Order processOrder(Order order) {
if (StringUtils.isNotBlank(order.getId())) {
Order dbOrder = orderService.findById(order.getId());
if (dbOrder != null && dbOrder.getStatus()>=Order.STATUS_PAYED) {
return dbOrder;//已支付
}
}
order = orderService.upsert(order);
Map<String, String> map = placeOrder(order);
if (map != null) {
String returnCode = map.get("return_code");
order.setReturnCode(returnCode);
if ("SUCCESS".equals(returnCode)) {
order.setStatus(Order.STATUS_ORDER);
}
order.setReturnMsg(map.get("return_msg"));
order.setMwebUrl(map.get("mweb_url"));
order.setPrepayId(map.get("prepay_id"));
orderService.save(order);
orderService.sendNotify(order);
if (TRADE_TYPE_JS.equals(order.getTradeType())) {
Map<String, String> signs = new TreeMap<>();
signs.put("appId", appId);
signs.put("nonceStr", StringUtils.uuid());
signs.put("package", "prepay_id="+map.get("prepay_id"));
signs.put("signType", "MD5");
signs.put("timeStamp", String.valueOf(DateTimeUtils.secondsOf(LocalDateTime.now())));
signs.put("paySign", genSign(signs));
order.setSigns(signs);
}
}
return order;
}

//下单
private Map<String, String> placeOrder(Order order) {
Map<String, String> map = null;
String tradeType = order.getTradeType();
if (TRADE_TYPE_H5.equals(tradeType)) {
map = prepareH5Order(order.getTradeNo(), order.getProductId(), order.getProductName(), order.getTotalFee(), order.getIpaddr());
}else{
map = prepareJsOrder(order.getTradeNo(), order.getProductId(), order.getProductName(), order.getTotalFee(), order.getIpaddr(), order.getOpenid());
}
HttpEntity<String> request = genXmlRequest(map);
String res = restTemplate.postForObject(URL_UNIFIED_ORDER, request, String.class);
log.debug(res);
return XmlUtils.parse(res);
}

// H5支付下单数据
private Map<String, String> prepareH5Order(String tradeNo, String productId, String productName, Long amount, String ip) {
Map<String, String> order = newOrderMap();
order.put("trade_type", TRADE_TYPE_H5);//H5支付的交易类型为MWEB
order.put("notify_url", notifyUrl);//回调地址, 不能携带参数。
order.put("scene_info", "{\"h5_info\": {\"type\":\"WAP\",\"wap_url\": \"\",\"wap_name\": \"\"}}");//用于上报支付的场景信息
order.put("spbill_create_ip", ip);//用户端IP,支持ipv4、ipv6格式
order.put("out_trade_no", tradeNo);//自定义交易单号
order.put("product_id", productId);//自定义商品
order.put("body", productName);//网页的主页title名-商品概述
order.put("fee_type", "CNY");//境内只支持CNY,默认可不传
order.put("total_fee", String.valueOf(amount));//订单总金额,单位为分
//签名
order.put(KEY_SIGN, genSign(order));
return order;
}

// JSAPI支付下单数据
private Map<String, String> prepareJsOrder(String tradeNo, String productId, String productName, Long amount, String ip, String openid) {
Map<String, String> order = newOrderMap();
order.put("trade_type", TRADE_TYPE_JS);//交易类型为JSAPI
order.put("notify_url", notifyUrl);//回调地址, 不能携带参数。
//order.put("scene_info", "{\"h5_info\": {\"type\":\"WAP\",\"wap_url\": \"\",\"wap_name\": \"\"}}");//用于上报支付的场景信息
order.put("openid", openid);
order.put("spbill_create_ip", ip);//用户端IP,支持ipv4、ipv6格式
order.put("out_trade_no", tradeNo);//自定义交易单号
order.put("product_id", productId);//自定义商品
order.put("body", productName);//网页的主页title名-商品概述
order.put("fee_type", "CNY");//境内只支持CNY,默认可不传
order.put("total_fee", String.valueOf(amount));//订单总金额,单位为分
//签名
order.put(KEY_SIGN, genSign(order));
return order;
}

// 下单数据准备:公用部分
private Map<String, String> newOrderMap() {
Map<String, String> order = new TreeMap<>();
order.put("appid", appId);
order.put("mch_id", mchId);
order.put("nonce_str", StringUtils.uuid());
return order;
}

// 构造xml request
private HttpEntity<String> genXmlRequest(Object data) {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_XML);
String body = XmlUtils.stringify(data, "xml");
return new HttpEntity<String>(body, headers);
}

// 生成签名
private String genSign(Map<String, String> paramMap) {
StringBuilder sb = new StringBuilder();
paramMap.entrySet().stream().sorted(Map.Entry.comparingByKey()).forEach((a) -> {
if (StringUtils.isBlank(a.getKey()) || KEY_SIGN.equals(a.getKey())) {
return;
}
if (StringUtils.isBlank(a.getValue())) {
return;
}
sb.append(a.getKey()); sb.append("="); sb.append(a.getValue()); sb.append("&");
});
sb.append("key="); sb.append(mchKey);
String signStr = sb.toString();
log.debug(signStr);
return StringUtils.MD5(signStr).toUpperCase();
}

//微信支付结果回调处理
@Synchronized // TODO: 避免重入仅这样不够,还需要锁定订单记录
public String processCallback(String data) {
Map<String, String> map = XmlUtils.parse(data);
String sign = genSign(map);
if (!sign.equals(map.get(KEY_SIGN))) {
return returnCodeMsg("FAIL", "SIGNERROR");
};
String tradeNo = map.get("out_trade_no");
Order order = orderService.findByTradeNo(tradeNo);
if (order == null) {
return returnCodeMsg("FAIL", "NOTFOUND");
}
if (order.getStatus() >= Order.STATUS_PAYED) {
return returnCodeMsg("SUCCESS", "OK!");
}
String tradeType = map.get("trade_type");
long totalFee = NumberUtils.parse(map.get("total_fee"), Long.class, 0L);
if (totalFee != order.getTotalFee() || !StringUtils.equals(tradeType, order.getTradeType())) {
return returnCodeMsg("FAIL", "TRADEINFOERROR");
}

order.setTransactionId(map.get("transaction_id"));
order.setReturnCode(map.get("return_code"));
order.setResultCode(map.get("result_code"));
order.setBankType(map.get("bank_type"));
order.setTimeEnd(map.get("time_end"));
order.setStatus(Order.STATUS_PAYED);
orderService.save(order);

return returnCodeMsg("SUCCESS", "OK");
}

private String returnCodeMsg(String code, String msg) {
return String.format("<xml><return_code><![CDATA[%s]]></return_code><return_msg><![CDATA[%s]]></return_msg></xml>", code, msg);
}

}

## WxResponse.java
```java
@Data
public class WxResponse implements Serializable {
private Integer errcode;
private String errmsg;
}

WxResexpire.java

1
2
3
4
5
6
7
@Data
public class WxResexpire extends WxResponse implements Serializable {
Long expires_in;
public Boolean getExpired() {
return expires_in == null || expires_in <= DateTimeUtils.secondsOf(LocalDateTime.now());
}
}

WxAccessToken.java

1
2
3
4
@Data
public class WxAccessToken extends WxResexpire implements Serializable {
String access_token;
}

WxWebToken.java

1
2
3
4
5
6
@Data
public class WxWebToken extends WxAccessToken implements Serializable {
String refresh_token;
String openid;
String scope;
}

WxJsapiTicket.java

1
2
3
4
@Data
public class WxJsapiTicket extends WxResexpire implements Serializable {
String ticket;
}

WxJsdkConfig.java

1
2
3
4
5
6
7
8
@Data
@JsonInclude(value = Include.NON_NULL)
public class WxJsdkConfig extends WxResponse implements Serializable {
String appId;
Long timestamp;
String nonceStr;
String signature;
}

Spring boot thymeleaf使用外置template和static路径

Spring boot开发的web项目打jar包部署时,如果希望template模板及static文件(js/css/img等)能单独更新,可以用下面的方法把它们从jar包分离出来。

工程目录结构调整

把static和template移到resources之外,比如和java目录平级。

1
2
3
4
5
6
7
8
├─src
│ ├─main
│ │ ├─java
│ │ ├─resources
│ │ │ └─application.properties
│ │ ├─static
│ │ └─templates
│ └─test

在 application.properties 分别指定static和template的位置

1
2
3
4
spring.mvc.static-path-pattern=/static/**
spring.resources.static-locations=file:/pathto/yourproject/src/main/static/
spring.thymeleaf.prefix=file:/pathto/yourproject/src/main/templates/
#spring.thymeleaf.cache=false

html模板里引用static文件的方式

1
2
3
<link rel="stylesheet" th:href="@{/static/subpath/afile.css}" />
<script th:src="@{/static/subpath/afile.js}"></script>
<img th:src="@{/static/subpath/afile.png}"/>

注:link favicon不知为何不能加static:(

把static和template合并,方便一起编辑

实践中发现,html模板和对应的静态文件(js,css,images等)分开在两处不方便编辑。尝试把他们合并到同一个文件夹下。首先只要修改下面两个配置都指向一个地方(比如webroot):

1
2
spring.resources.static-locations=file:/pathto/yourproject/src/main/webroot/
spring.thymeleaf.prefix=file:/pathto/yourproject/src/main/webroot/

只是这样会导致html模板也可以被用户直接访问,不太安全。需要调整一下security配置:

  • 配置方法configure(WebSecurity webSecurity) :
    1
    2
    3
    4
    5
    6
    // 忽略静态资源改为只忽略特定类型的静态资源,目的是不忽略*.html模板
    //webSecurity.ignoring().antMatchers("/static/**");
    webSecurity.ignoring().antMatchers("/static/**/*.js");
    webSecurity.ignoring().antMatchers("/static/**/*.css");
    webSecurity.ignoring().antMatchers("/static/**/*.jpg");
    webSecurity.ignoring().antMatchers("/static/**/*.png");
  • 配置方法configure(HttpSecurity httpSecurity):
    1
    2
    //禁止直接访问html模板
    httpSecurity.authorizeRequests().antMatchers("/static/**/*.html").denyAll()

Spring boot 处理 error

参考: https://www.cnblogs.com/hyl8218/p/10754894.html
Spring boot 处理 error 的基本流程:
Controller -> 发生错误 -> BasicErrorController -> 根据 @RequestMapping(produces) 判断调用 errorHtml 或者 error 方法,然后:
errorHtml -> getErrorAttributes -> ErrorViewResolver -> 错误显示页面
error -> getErrorAttributes -> @ResponseBody (直接返回JSON)

附:为静态文件加md5(避免浏览器缓存旧文件)

1
2
spring.resources.chain.strategy.content.enabled=true
spring.resources.chain.strategy.content.paths=/**

附一:content-path 相关处理

配置项

1
server.servlet.context-path=/myweb

后端获取basePath

1
2
3
4
5
6
7
public static String getWebBasePath() {
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
if (requestAttributes == null) return null;//防止意外
HttpServletRequest request = ((ServletRequestAttributes) requestAttributes).getResponse();
if (request == null) return null;//防止意外
return String.format("%s://%s:%s%s/", request.getScheme(), request.getServerName(), request.getServerPort(), request.getContextPath());
}

在js里获取contentPath

1
2
3
<script th:inline="javascript">
var contextPath = /*[[${#request.contextPath}]]*/'';
</script>

注:在html模板里使用th:href或th:src时带”@”符号会自动处理contentPath

附二:为thymeleaf模板设置全局静态变量

以配置第三方库路径为例

配置项

1
basepath.lib=https://cdnjs.cloudflare.com/ajax/

配置 ThymeleafViewResolver

1
2
3
4
5
6
7
8
9
10
11
@Resource
private Environment env; //环境变量

@Resource
private void configureThymeleafStaticVars(ThymeleafViewResolver viewResolver) {
if(viewResolver != null) {
Map<String, Object> vars = new HashMap<>();
vars.put("libBasePath", env.getProperty("basepath.lib"));//静态库配置
viewResolver.setStaticVariables(vars);
}
}

使用libBasePath引入第三方库(比如vue.js)

1
<script th:src="${libBasePath}+'libs/vue/2.6.10/vue.js'"></script>

附三:html模板共用head

common.html 里定义公用head

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!DOCTYPE html>
<html lang="zh" xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<head th:fragment="head">
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, shrink-to-fit=no"/>
<link rel="icon" type="image/x-icon" th:href="@{/favicon.ico}">
<link rel="stylesheet" th:href="${libBasePath}+'libs/twitter-bootstrap/4.3.1/css/bootstrap.min.css'"/>
<script th:src="${libBasePath}+'libs/vue/2.6.10/vue.js'"></script>
<link th:href="@{/static/base.css}" rel="stylesheet"/>
<script th:src="@{/static/base.js}"></script>
<script th:inline="javascript">var contentPath = /*[[${#request.contextPath}]]*/'';</script>
</head>
<body>
</body>
<html>

index.html 里引用common::head

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!DOCTYPE html>
<html lang="zh" xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<head>
<title>我的页面</title>
<th:block th:include="common::head"></th:block>
<link rel="stylesheet" th:href="@{/static/index.css}" />
</head>
<body>
<script th:inline="javascript">
var varForJs = /*[[${varForJs}]]*/{};
</script>
<script th:src="@{/static/index.js}"></script>
<body>
</html>

附四:使用thymeleaf简单制作vue组件

用tab-bar做例子

tabar.html

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
<!DOCTYPE html>
<html lang="zh" xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<body>
<th:block th:fragment="tabar">
<script type="text/x-template" id="tabar-tpl">
<div class="nav nav-tabs">
<a class="nav-item nav-link" href="#"
v-for="(tabName,tabKey) in tabs" v-show="!!tabName"
:class="{active: active == tabKey}"
@click="onClick(tabKey, tabName)">{{tabName}}</a>
<slot></slot>
</div>
</script>
<script th:inline="javascript">
Vue.component('tabar', {
template: '#tabar-tpl',
props:{
tabs: [Object, Array],
active: [String, Number],
},
data() {
return {}
},
methods: {
onClick(tabKey, tabName){
this.$emit('click-tab', {key: tabKey, name: tabName})
},
}
});
</script>
</th:block>
</body>
</html>

引入和使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<head>
<th:block th:include="tabar::tabar"></th:block>
</head>
<body>
<div id="root">
<tabar :tabs="tabs" :active="activeTab" @click-tab="activeTab=$event.key"></tabar>
</div>
</body>
<script>
new Vue({
el: '#root',
data: {
tabs: {a:'Tab A', b:'Tab B'},
activeTab: 'a',
},
methods: {
},
});
</script>

Java转码服务

记录用java实现转码服务的核心代码。功能包括:把视频转为mp4、把文档转为pdf、生成视频和文档的缩略图等。

ConvertService.java

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
import java.time.Instant;
import java.time.LocalDateTime;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import lombok.Synchronized;

@Service
public class ConvertService {
@Autowired
ConvertConfig convertConfig;
@Autowired
ConvertTaskDao convertTaskDao;
@Autowired
ConvertHelper convertHelper;
@Autowired
RestTemplate restTemplate;

private static Thread convertThread = null;
private static Thread notifyThread = null;
private static ConcurrentLinkedQueue<ConvertTask> queueTasks = new ConcurrentLinkedQueue<>();
private static ConcurrentHashMap<String, ConvertTask> convertingTasks = new ConcurrentHashMap<>();
private static ConcurrentLinkedQueue<ConvertTask> notifyTasks = new ConcurrentLinkedQueue<>();

@Synchronized
public ConvertTask addTask(ConvertTask task) {
ConvertTask save = convertTaskDao.save(task);
queueTasks.add(save);
startConvert();
return task;
}

@Synchronized
private int doConvert() {
if (convertingTasks.size() >= 4) {
return 1;
}
ConvertTask task = queueTasks.poll();
if (task == null) {
return 10;
}
try {
if (convertingTasks.containsKey(task.getSrc())) {//重复
task.setStatus(ConvertTask.STATUS_IGNORE);
convertTaskDao.save(task);
return 0;
}
convertingTasks.put(task.getSrc(), task);
task.setStatus(ConvertTask.STATUS_START);
task.setStartAt(Instant.now());
convertTaskDao.save(task);

CompletableFuture<ConvertTask> process = convertHelper.process(task);
process.thenAccept((retTask) -> {
retTask.setStatus(ConvertTask.STATUS_FINISH);
retTask.setEndAt(Instant.now());
convertTaskDao.save(retTask);
convertingTasks.remove(task.getSrc());
notifyTasks.add(retTask);
startNotify();
System.out.println(retTask);
});
} catch(Exception e) {
e.printStackTrace();
}
return 0;
}

@Synchronized
private int doNotify() {
ConvertTask task = notifyTasks.poll();
if (task == null) {
return 10;
}
int sleep = 0;
try {
ResponseEntity<String> response = restTemplate.postForEntity(convertConfig.getNotify(), task, String.class);
String success = response.getBody();
System.out.println(success);
if (!StringUtils.isNullOrEmpty(success) && success.contains("success")) {
task.setStatus(ConvertTask.STATUS_NOTIFIED);
task.setNotify(LocalDateTime.now()+","+success);
} else {
task.setNotify(LocalDateTime.now()+",empty");
notifyTasks.add(task);
sleep = 1;
}
} catch(Exception e) {
e.printStackTrace();
task.setNotify(LocalDateTime.now()+","+e.getMessage());
notifyTasks.add(task);
sleep = 2;
}
convertTaskDao.save(task);
return sleep;
}

public void startConvert() {
if (convertThread != null) {
convertThread.interrupt();
return;
}
convertThread = new Thread(() -> {
while (convertThread == Thread.currentThread()) {
System.out.println("Convert "+convertThread + " running at " + LocalDateTime.now());
int sleep = doConvert();
try {
Thread.sleep(sleep*1000);
} catch (InterruptedException e) {
System.out.println("Convert "+Thread.currentThread() +" "+ e.getMessage());
}
}
System.out.println("Convert "+Thread.currentThread()+" stoped at " + LocalDateTime.now());
});
convertThread.start();
System.out.println("Convert "+convertThread+" started at " + LocalDateTime.now());

}

public void startNotify() {
if (notifyThread != null) {
notifyThread.interrupt();
return;
}
notifyThread = new Thread(() -> {
while (notifyThread == Thread.currentThread()) {
System.out.println("Notify "+notifyThread + " running at " + LocalDateTime.now());
int sleep = doNotify();
try {
Thread.sleep(sleep*1000);
} catch (InterruptedException e) {
System.out.println("Notify "+Thread.currentThread() +" "+ e.getMessage());
}
}
System.out.println("Notify "+Thread.currentThread()+" stoped at " + LocalDateTime.now());
});
notifyThread.start();
System.out.println("Notify "+notifyThread+" started at " + LocalDateTime.now());
}

@PostConstruct
private void init() {
List<ConvertTask> tasks = convertTaskDao.findByStatus(ConvertTask.STATUS_START);
tasks.forEach(task -> {
queueTasks.add(task);
});
tasks = convertTaskDao.findByStatus(ConvertTask.STATUS_INITIAL);
tasks.forEach(task -> {
queueTasks.add(task);
});
tasks = convertTaskDao.findByStatus(ConvertTask.STATUS_FINISH);
tasks.forEach(task -> {
notifyTasks.add(task);
});
startConvert();
startNotify();
System.out.println(convertConfig);
}

@PreDestroy
public void stop() {
if (convertThread != null) {
convertThread.interrupt();
convertThread = null;
}
if (notifyThread != null) {
notifyThread.interrupt();
notifyThread = null;
}
}
}

ConvertTaskDao.java

1
2
3
4
5
6
7
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.repository.query.Param;

public interface ConvertTaskDao extends JpaRepository<ConvertTask, Integer>{
List<ConvertTask> findByStatus(@Param("status") Integer status);
}

ConvertTask实体类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class ConvertTask implements Serializable{
private static final long serialVersionUID = 1418649194096365375L;
//状态常量
public static final Integer STATUS_IGNORE = -1;
public static final Integer STATUS_INITIAL = 0;
public static final Integer STATUS_START = 1;
public static final Integer STATUS_FINISH = 2;
public static final Integer STATUS_NOTIFIED = 3;

@Id
Integer id;
String name;
Instant createdAt;
Instant startAt;
Instant endAt;
Integer status = STATUS_INITIAL;
String src;//源文件
String out;//输出文件
String img;//缩略图
String ret;//转换结果
String notify;//通知结果
}

ConvertHelper.java

基于CmdHelper.java实现,见:https://www.zybuluo.com/TedZhou/note/1716823

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
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.concurrent.CompletableFuture;

import javax.imageio.ImageIO;

import org.apache.commons.exec.CommandLine;
import org.apache.pdfbox.io.IOUtils;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.rendering.PDFRenderer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class ConvertHelper {
static final String EXT_PDF = ".pdf";
static final String EXT_MP4 = ".mp4";
static final String EXT_AVI = ".avi";
static final String EXT_DOC_STR = ".doc,.docx,.ppt,.pptx,.xls,.xlsx,";// 文档后缀
static final String EXT_VIDEO_FFMPEG_STR = ".avi,.mpg,.wmv,.3gp,.mov,.asf,.asx,.flv,.f4v,.vob,.mkv,.ts,";// ffmpeg能解码的文件格式
static final String EXT_VIDEO_MENCODER_STR = ".wmv9,.rm,.rmvb,.mpeg,";// 需要先mencoder解码的文件格式。.mpeg无法直接截取图片,所以需要中转

@Autowired
ConvertConfig convertConfig;

public static String getExt(String filename) {
if (StringUtils.isNullOrEmpty(filename)) return "";
int pos = filename.lastIndexOf(".");
if (pos < 0) return "";
return filename.substring(pos).toLowerCase();
}

public static boolean isDoc(String ext) {
return EXT_DOC_STR.contains(ext + ',');
}

public static boolean isVideo(String ext) {
return EXT_VIDEO_FFMPEG_STR.contains(ext + ',') || needMEncoder(ext);
}

public static boolean needMEncoder(String ext) {
return EXT_VIDEO_MENCODER_STR.contains(ext + ',');
}

public CompletableFuture<ConvertTask> process(ConvertTask task) {
CompletableFuture<ConvertTask> cf = new CompletableFuture<>();
new Thread(() -> {
String srcFile = task.getSrc();
String dotExt = getExt(srcFile);
String outFile = null;
CmdHandler cmdHandler = null;
FileOutputStream logStream = null;
try {
File logFile = new File(convertConfig.getLog() + srcFile + ".log");
logStream = new FileOutputStream(logFile);
if (isDoc(dotExt)) {
outFile = srcFile + EXT_PDF;
cmdHandler = doc2pdf(convertConfig.getSrc() + srcFile, convertConfig.getOut() + outFile, logStream);
if (cmdHandler.getException() == null) {
if (pdf2png(convertConfig.getOut() + outFile, convertConfig.getOut() + task.getSrc() + ".png")) {
task.setImg(task.getSrc() + ".png");
}
}
} else if (isVideo(dotExt)) {
if (needMEncoder(dotExt)) {
outFile = srcFile + EXT_AVI;
cmdHandler = video2avi(convertConfig.getSrc() + srcFile, convertConfig.getOut() + outFile, logStream);
task.setRet(cmdHandler.resultString());
if (cmdHandler.getException() != null) {
cf.complete(task);
return;
}
srcFile = outFile;
}
cmdHandler = video2jpg(convertConfig.getSrc() + srcFile, convertConfig.getOut() + task.getSrc() + ".jpg", logStream);
if (cmdHandler.getException() == null) {
task.setImg(task.getSrc() + ".jpg");
}
outFile = task.getSrc() + EXT_MP4;
cmdHandler = video2mp4(convertConfig.getSrc() + srcFile, convertConfig.getOut() + outFile, logStream);
} else {
task.setRet("-1:unsupport");
cf.complete(task);
return;
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} finally {
IOUtils.closeQuietly(logStream);
}

task.setOut(outFile);
task.setRet(cmdHandler.resultString());
if (cmdHandler.getException() != null) {
task.setOut(null);
}
cf.complete(task);
}).start();
return cf;
}

CmdHandler doc2pdf(String srcFile, String outFile, OutputStream cmdOut) {
CommandLine cmd = new CommandLine(convertConfig.getUnoconv());
cmd.addArgument("-f");
cmd.addArgument("pdf");
cmd.addArgument("-o");
cmd.addArgument(outFile);
cmd.addArgument(srcFile);
return CmdHelper.run(cmd, cmdOut, 0);
}

boolean pdf2png(String srcFile, String outFile) {
try {
File file = new File(srcFile);
PDDocument doc;
doc = PDDocument.load(file);
PDFRenderer renderer = new PDFRenderer(doc);
BufferedImage image = renderer.renderImageWithDPI(1, 144); // Windows native DPI
BufferedImage tag = new BufferedImage(64, 64, BufferedImage.TYPE_INT_RGB);
tag.getGraphics().drawImage(image, 0, 0, 64, 64, null);
ImageIO.write(tag, "PNG", new File(outFile));
return true;
} catch (IOException e) {
e.printStackTrace();
return false;
}
}

CmdHandler video2mp4(String srcFile, String outFile, OutputStream cmdOut) {
CommandLine cmd = new CommandLine(convertConfig.getFfmpeg());
cmd.addArgument("-i");
cmd.addArgument(srcFile);
cmd.addArgument("-ar");
cmd.addArgument("22050");
cmd.addArgument("-vcodec");
cmd.addArgument("libx264");
cmd.addArgument("-q:v");
cmd.addArgument("6");
cmd.addArgument("-r");
cmd.addArgument("25");
cmd.addArgument("-flags");
cmd.addArgument("+loop");
cmd.addArgument("-crf");
cmd.addArgument("24");
cmd.addArgument("-bt");
cmd.addArgument("256k");
cmd.addArgument("-af");
cmd.addArgument("volume=2");
cmd.addArgument("-y");
cmd.addArgument(outFile);
return CmdHelper.run(cmd, cmdOut, 0);
}

CmdHandler video2jpg(String srcFile, String outFile, OutputStream cmdOut) {
CommandLine cmd = new CommandLine(convertConfig.getFfmpeg());
cmd.addArgument("-i");
cmd.addArgument(srcFile);
cmd.addArgument("-f");
cmd.addArgument("image2");
cmd.addArgument("-ss");
cmd.addArgument("15");
cmd.addArgument("-t");
cmd.addArgument("0.001");
cmd.addArgument("-s");
cmd.addArgument("64x64");
cmd.addArgument("-y");
cmd.addArgument(outFile);
return CmdHelper.run(cmd, cmdOut, 0);
}

CmdHandler video2avi(String srcFile, String outFile, OutputStream cmdOut) {
CommandLine cmd = new CommandLine(convertConfig.getMencoder());
cmd.addArgument(srcFile);
cmd.addArgument("-oac");
cmd.addArgument("mp3lame");
cmd.addArgument("-lameopts");
cmd.addArgument("preset=64");
cmd.addArgument("-ovc");
cmd.addArgument("xvid");
cmd.addArgument("-xvidencopts");
cmd.addArgument("bitrate=600");
cmd.addArgument("-of");
cmd.addArgument("avi");
cmd.addArgument("-o");
cmd.addArgument(outFile);
return CmdHelper.run(cmd, cmdOut, 0);
}
}

配置项

1
2
3
4
5
6
7
convert.src=/mnt/file/upload/
convert.out=/mnt/file/download/
convert.log=/mnt/file/download/
convert.ffmpeg=/opt/ffmpeg/ffmpeg
convert.unoconv=/opt/libreoffice6.4/unoconv.py
convert.mencoder=/usr/bin/mencoder
convert.notify=http://127.0.0.1:8080/api/convert/notify
1
2
3
4
5
6
7
8
9
10
11
12
@Data
@Configuration
@ConfigurationProperties(prefix = "convert")
public class ConvertConfig {
String src;
String out;
String log;
String ffmpeg;
String unoconv;
String mencoder;
String notify;
}

依赖

pom.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-exec</artifactId>
<version>1.3</version>
</dependency>
<dependency>
<groupId>org.apache.pdfbox</groupId>
<artifactId>pdfbox</artifactId>
<version>2.0.9</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>

ffmpeg && mencoder

1
2
3
4
5
wget https://download1.rpmfusion.org/free/el/rpmfusion-free-release-7.noarch.rpm
yum install epel-release
rpm -Uvh rpmfusion-free-release-7.noarch.rpm
wget https://johnvansickle.com/ffmpeg/builds/ffmpeg-git-amd64-static.tar.xz
yum install mencoder

libreoffice && unoconv

1
2
https://www.libreoffice.org/donate/dl/rpm-x86_64/6.4.4/zh-CN/LibreOffice_6.4.4_Linux_x86-64_rpm.tar.gz
https://codeload.github.com/unoconv/unoconv/zip/0.8.2

简单实现Web页面元素拖放功能(Web Drag & Drop)

之前,想在浏览器中实现拖放似乎很困难,现在有了HTML5就简单了。下面实现一个例子:简单的拖拽排序。

拖放测试页面(DragDrop.html)

页面使用bootstrap展示几张图片(缩略图)
注:省略号部分表示你可以自己在div.row下复制更多个div.thumbnail

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
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Drag & Drop sample</title>
<link rel="stylesheet" href="http://cdn.staticfile.org/twitter-bootstrap/3.1.1/css/bootstrap.min.css">
</head>
<body>
<div class="container">
<div class="row">
<div class="thumbnail col-sm-6 col-md-4">
<img class="img-responsive" src="img1.png">
<div class="caption text-center"><h5>Image1</h5></div>
</div>
<div class="thumbnail col-sm-6 col-md-4">
<img class="img-responsive" src="img2.png">
<div class="caption text-center"><h5>Image2</h5></div>
</div>
<!-- ...... -->
</div>
</div>
<script src="http://cdn.staticfile.org/jquery/1.10.2/jquery.min.js"></script>
<script src="DragDrop.js"></script>
</body>
</html>

拖放测试脚本(DragDrop.js)

注:脚本依赖的jquery已经在页面里引入。

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
$(function(){
//拖拽的对象是这些缩略图
var dragable = '.thumbnail';
var $dragable = $(dragable);
//设置可拖拽属性
$dragable.attr('draggable', true);
//响应开始拖拽事件
$dragable.on('dragstart', setDragData);
//拖放的目标也是这些缩略图
var $dropable = $dragable;
//让目标允许拖放
$dropable.on('dragover', allowDrop);
//响应拖放事件
$dropable.on('drop', dropDragData);

//开始拖拽事件处理
function setDragData(e){
var $target = getDragable(e.target);
//传递被拖拽对象的位置索引
e.originalEvent.dataTransfer.setData("text",$target.index());
}

function allowDrop(e){
e.preventDefault();//这样就能允许拖放
}

//拖放事件处理:把被拖拽对象放到目标对象所在的位置,即实现拖拽排序。
function dropDragData(e){
e.preventDefault();//阻止默认的放置行为
var $target = getDragable(e.target);
var dragIdx = e.originalEvent.dataTransfer.getData("text");
if (dragIdx !== $target.index()){
var $draged = $(dragable).eq(dragIdx);
if ($draged.index() > $target.index()){
$target.before($draged);//向前拖:放在目标对象的前面
}else{
$target.after($draged);//向后拖:放在目标对象的后面
}
}
}
//获取可拖拽对象
function getDragable(dom){
var $dragable = $(dom);
if (!$dragable.is(dragable)){
//拖拽子元素时定位到父元素
$dragable = $dragable.closest(dragable);
}
return $dragable;
}
});

总结

上例的拖放效果在最新的ChromeIEQQ浏览器下测试了都很正常,但在FireFox下拖放图片会激活一个新的页签(有人说使用e.preventDefault()可以解决,但我试了没有效果)。

让table固定表头并冻结列的一种简单实现方法

标题已经说明需求,就不说废话了,直接上code;)

.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<!--把table放到div容器里。-->
<div class="table-container">
<table class="table-fixed">
<thead>
<tr>
<th class="col-fixed">fixed0</th>
<th>column0</th>
...省略其它th...
</tr>
</thead>
<tbody>
<tr>
<td class="col-fixed">fixed1</td>
<td>column1</td>
....省略其它td...
</tr>
....省略其它tr...
</tbody>
</table>
<div>

.css

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<style>
/*限制容器宽高并允许滚动*/
.table-container{
width:100%;
max-height:200px;
overflow:auto;
}
/*表头cell设为相对定位*/
.table-fixed>thead>tr>th{
position:relative;
z-index:100;
background-color:#eee;
}
/*冻结列设为相对定位*/
.table-fixed>tbody>tr>td.col-fixed{
position:relative;
background-color:#eee;
}
/*设置z-index避免被覆盖*/
.table-fixed>thead>tr>th.col-fixed{
z-index:110;
}
</style>

.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<script>
//需先引入jquery
$(function(){
var $table = $('.table-fixed');
var $topFixes = $table.find('thead>tr>th');
var $colFixes = $table.find('.col-fixed');
//容器滚动时,为实现fixed冻结效果:
//1、把表头cell的top设为scrollTop
//2、冻结列的left设为scrollLeft
$table.closest('.table-container').scroll(function(){
$topFixes.css('top',this.scrollTop);
$colFixes.css('left',this.scrollLeft);
});
});
</script>

##总结
相较于表头和内容分离的做法,这种实现方式不会出现对不齐的问题,而且比较简单(:不想写js的请忽略)

##已知问题

  1. 桌面Chrome下的效果最好,其他浏览器fixed内容有点闪烁。
  2. 移动端浏览器下的延迟比较严重,只能改为用多个table实现。
  3. 如果冻结列超出容器(.table-container)的范围,会导致水平滚动条滚不到头(无限滚动)。

Mongodb MapReduce 介绍、示例及特别说明

介绍

Map-Reduce是一种计算模型,简单的说就是将大批量的工作(数据)分解(MAP)执行,然后再将结果合并成最终结果(REDUCE)。
MongoDB提供的Map-Reduce非常灵活,可以高效的进行大规模数据的统计分析。

语法

格式一

1
2
3
4
5
6
7
8
9
10
11
12
13
db.collection.mapReduce(
function() {emit(key,value)},//map 函数
function(key,values) {return reduced}, //reduce 函数
<collection>//out collection
)
//第三个参数也可以传入更多选项:
{
finalize: function(key, reduced){return finalized}
out: <collection>|{inline:true},
query: <document>,
sort: <document>,
limit: <number>,
}

格式二

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
db.runCommand({
mapReduce: <collection>,
map: <function>,
reduce: <function>,
finalize: <function>,
out: <output>,
query: <document>,
sort: <document>,
limit: <number>,
scope: <document>,
jsMode: <boolean>,
verbose: <boolean>,
bypassDocumentValidation: <boolean>,
collation: <document>
})

示例

示例集合person存储各省市居民的姓名、性别等记录

1
2
3
4
5
6
7
8
9
10
11
12
13
1.{
"name" : "姓名1",
"gender" : "男",
"city" : "城市a",
"province" : "省份A"
}
2.{
"name" : "姓名2",
"gender" : "女",
"city" : "城市b",
"province" : "省份B"
}
3...

现用mapReduce统计各省市人口性别比例:

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
db.person.mapReduce(
function(){//map
var key = {province:this.province, city:this.city}
var value = {total: 1}
if (this.gender == '男'){
value.male = 1
}else if (this.gender == '女'){
value.female = 1
}else{
value.unknown = 1
}
emit(key, value)
},
function(key, values){//reduce
var value = {}//累计各性别的数量
values.forEach(function(item){
//item里可能是单个值,也可能已经是累计值
for (var k in item){
value[k] = (value[k]||0) + item[k]
}
})
return value//reduce返回值不支持数组,若需多值请用对象格式
},{
query:{},//指定过滤源数据的查询条件
sort:{province:1, city:1},//按key排序可减少reduce的次数,加快执行速度
finalize: function(key, rValue){
for (var k in rValue){
if (k !== 'total'){//数量转为比例
rValue[k] = rValue[k]/rValue.total
}
}
return rValue
},
out:{inline:true},
}
).find()

执行结果如下:

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
[
{
"_id" : {
"province" : "省份A",
"city" : "城市a"
},
"value" : {
"total" : 40.0,
"male" : 0.425,
"female" : 0.575
}
},
{
"_id" : {
"province" : "省份A",
"city" : "城市b"
},
"value" : {
"total" : 1.0,
"male" : 1.0
}
},
{
"_id" : {
"province" : "省份A",
"city" : "城市c"
},
"value" : {
"total" : 150.0,
"male" : 0.526,
"female" : 0.473
}
},
...
]

特别说明

  1. 当key对应的value只有一组时不会触发reduce。所以,map函数emit的value值的格式最好和reduce函数return值一致,避免最终的结果不一致。
  2. 同一个key可能会触发多次reduce,即reduce接收的values中有些item元素可能是已经reduced的,累积时要考虑这种情况(即values包含直接emit的value,也包含reduce返回的value)。
  3. reduce返回值不支持数组,若需多值请用对象格式。
  4. 按key排序可以减少reduce的次数,提高处理效率。
  5. out可以指定输出到一个collection集合,但这样会导致写操作;指定为{inline:true}时mongodb会用runReadCommand代替runCommand——if inline output is specified, we need to apply readPreference on the command as it could be run on a secondary.

JS的“缺陷”

JS从诞生到现在已经二十几年了,在其演进中难免引入了一些bug,且积习难改。理解这些缺陷有助于避开那些新手陷阱。

typeof相关

Null和Undefined

他们都是基本类型,也都只对应一个常量值(分别是null和undefined)。

1
2
3
4
5
6
//已知:
typeof undefined === 'undefined'
//你可能会以为:
typeof null === 'null'//但,不是这样
//真相是:
typeof null === 'object'//null是指空对象,空对象也是对象!

Object、Array、Function、RegExp、Date、Error、…

这些本质上都是对象类型。

1
2
3
4
5
6
7
//已知:
typeof {} === 'object'
typeof function(){} === 'function'
//你可能会以为:
typeof [] === 'array'//但,不是这样的
//真相是:
typeof [] === 'object'//数组也是对象,基本类型外的除了函数都是对象。

typeof一览表

类型 Type typeof
布尔 Boolean true ‘boolean’
数值 Number 1 ‘number’
字符串 String ‘’ ‘string’
符号 Symbol Symbol() ‘symbol’
未定义 Undefined undefined ‘undefined’
空* Null null ‘object’
对象 Object {} ‘object’
下列都属于 对象的派生类型
数组 Array [] ‘object’
函数* Function function(){} ‘function’
Function class{} ‘function’

相等比较

宽松比较==

相等操作符比较两个值是否可能相等,即如果类型不同则进行可能的类型转换。

1
2
3
4
5
6
7
8
9
0==''//true
0=='0'//true
0==false//true
[1,2]=='1,2'//true
null==undefined//true
!null && !undefined//true
//注意:null和undefined既不等于true也不等于false
null==true || null==false//false
undefined==true || undefined==false//false

严格比较===

全等操作符比较两个值是否严格相等,即要求类型一样,也要求值相同。

1
2
3
4
5
6
null===null//true
undefined===undefined//true
null===undefined//false
0==='0'//false
0===-0//true
NaN===NaN//false?--这个比较有性格

同值相等Object.is()比较

Object.js()比较的结果同全等操作符,除了下面这两个相反:

1
2
Objecct.is(NaN,NaN)//true
Objecct.is(0,-0)//false

A链接打开的新窗口无法显示内容?

通常,我们想要一个A链接跳转到新窗口的话,会这样写:

1
<a href="http://www.zybuluo.com" target="_blank">link to a new window</a>

如果href的url需要动态的话,需要用到javascript,会改写成这样:

1
<a href="javascript:openUrl();" target="_blank">link to a new window</a>

当然还需要额外的script定义openUrl()函数:

1
2
3
4
function openUrl(){
var url = 'http://www.zybuluo.com';
window.open(url, '_blank');
}

似乎一切OK。——我是在chrome下测试的,确实没问题。
但发现,在IE和Firefox下试不行:新窗口无法显示内容。

原因是忽视了a标签里的target="_blank",去掉它就行了。

也就是说,借用A链接执行js的最好不要加target属性(虽然Chrome支持但其它浏览器不一定支持)。所以,还是在脚本里决定是否打开新窗口。

感想

  • 前端测试需顾及所有主流浏览器;
  • Chrome(google)的兼容性做得太好了:)

JS对象的克隆Clone

浅克隆[Plain Clone]:

1
2
3
4
5
6
var obj1 = {foo: "foo", bar: "bar"};
var obj2 = {foo: "foo2", bar: "bar2"};
var copy1 = {...obj1}; // Object {foo: "foo", bar: "bar"}
var copy2 = Object.assign({}, obj); //Object {foo: "foo2", bar: "bar2"}
var copySpread = {...obj1, ...obj2}; //Object {foo: "foo2", bar: "bar2"}
var copyAssign = Object.assign({}, obj2, obj1); //Object {foo: "foo", bar: "bar"}

JSON克隆[Json Clone]

1
2
var obj = { a: 0, b: { c: 0 } };
var copy = JSON.parse(JSON.stringify(obj));

深度克隆[Deep Clone]

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
function deepClone(obj) {
var copy;
// Handle the 3 simple types, and null or undefined
if (null == obj || "object" != typeof obj) return obj;
// Handle Date
if (obj instanceof Date) {
copy = new Date();
copy.setTime(obj.getTime());
return copy;
}
// Handle Array
if (obj instanceof Array) {
copy = [];
for (var i = 0, len = obj.length; i < len; i++) {
copy[i] = deepClone(obj[i]);
}
return copy;
}
// Handle Function
if (obj instanceof Function) {
copy = function() {
return obj.apply(this, arguments);
}
return copy;
}
// Handle Object
if (obj instanceof Object) {
copy = {};
for (var attr in obj) {
if (obj.hasOwnProperty(attr)) copy[attr] = deepClone(obj[attr]);
}
return copy;
}
throw new Error("Unable to copy obj as type isn't supported " + obj.constructor.name);
}