GeXiangDong

精通Java、SQL、Spring的拼写,擅长Linux、Windows的开关机

0%

windows

可以使用Windows service wrapper来把任何命令行执行的程序做成windows service.

Windows Service Wrapper简称WinSW,可以在https://github.com/kohsuke/winsw/releases 下载到

编写一个配置文件

编写一个xml文件,可参照下例:

1
2
3
4
5
6
7
8
<configuration>
<id>tutorial-service</id>
<name>Tutorial Service</name>
<description></description>
<executable>java</executable>
<arguments>-jar c:\tutorial\tutorial-section-01-1.0-SNAPSHOT.jar</arguments>
<logmode>rotate</logmode>
</configuration>

文件保存为 tutorial-service.xml,然后把winsw.exe拷贝到相同目录下,并改成和xml一样的名字(扩展名仍旧是exe不变)tutorial-service.exe

安装service和卸载service

在命令行下执行

1
tutorial-service.exe install

可把前面设置的xml中命令安装为windows service(执行上述命名需要具有windows管理员权限。

如果需要卸载可用

1
tutorial-service.exe uninstall

linux

ubuntu/debian下可以用sytemd。

配置文件

创建配置文件,内容如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[Unit]  
Description=tutorial
After=syslog.target

[Service]
ExecStart=/usr/lib/jvm/java-8-oracle/bin/java -jar -Xms256m -Xmx1G -Dserver.port=8087 -Dautostart=true -Dlog.level.console=WARN /web/webapps/tutorial-1.0.0.jar --spring.profiles.active=development
SuccessExitStatus=143

User=www-data
Group=www-data
UMask=0007
RestartSec=10
Restart=always

[Install]
WantedBy=multi-user.target

配置文件放入/lib/systemd/system/tutorial.service

安装后台服务

然后执行:

1
2
systemctl enable tutorial.service
systemctl daemon-reload

管理后台服务

安装好之后可以使用

1
2
3
4
systemctl start tutorial
systemctl stop tutorial
systemctl restart tutorial
systemctl status tutorial

等命令来管理服务

如果你更习惯老版本linux下的service命令,也可以

1
service tutorial start

以上内容中tutorial应该替换成自己的项目名

实现Java打印功能有3种方式

javax.print.PrintService

使用javax.print包下的类来打印,可以打印文档,PDF等等文档

1
2
3
4
Doc doc = new SimpleDoc(new FileInputStream("tobeprint.pdf"), DocFlavor.BYTE_ARRAY.PDF, null);
PrintService ps = PrintServiceLookup.lookupDefaultPrintService();
DocPrintJob job = ps.createPrintJob();
job.print(doc, null);

java.awt.PrinterJob

awt的实现,可以通过Printable 打印 Graphics,自己程序绘制要打印的内容。

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 java.awt.*;
import java.awt.print.*;


public class PrintToPrinter {

public static void main(String[] args) throws Exception {
Book book = new Book();
PageFormat pf = new PageFormat();
pf.setOrientation(PageFormat.PORTRAIT);

Paper p = new Paper();
p.setSize(590, 840);
p.setImageableArea(10, 10, 590, 840);
pf.setPaper(p);
book.append(new OneLabel(), pf);

PrinterJob job = PrinterJob.getPrinterJob();
job.setPageable(book);
job.print();
}

/**
* 实现Printable即可打印
*/
public static class OneLabel implements Printable {
@Override
public int print(Graphics graphics, PageFormat pageFormat, int pageIndex) throws PrinterException {
if (pageIndex != 0) {
// 此例子只有一页;
return NO_SUCH_PAGE;
}
Graphics2D g2 = (Graphics2D) graphics;
double scale = 72.0 / 300; //屏幕DPI是72;打印机DPI:300; 可以实现打印的更精细,特别是图片
g2.scale(scale, scale);

g2.setColor(Color.black);

double x = pageFormat.getImageableX();
double y = pageFormat.getImageableY();
System.out.println("左上角:" + x + "," + y + " 宽高: " + pageFormat.getWidth() + "," + pageFormat.getHeight());

String str = "中文字符串";
Font font = new Font("微软雅黑", Font.PLAIN, 10);
g2.setFont(font);
g2.drawString(str, (float) x, (float) (y + 20));

Font font2 = new Font("微软雅黑", Font.PLAIN, 20);
g2.setFont(font2);
g2.drawString(str, (float) x, (float) (y + 80));

return PAGE_EXISTS;
}
}
}

javafx.print.PrinterJob

这是javafx的实现,可以打印Canvas,类似awt的功能

1
2
3
4
5
final Canvas canvas = new Canvas(250,250);
GraphicsContext gc = canvas.getGraphicsContext2D();
gc.fillText("hello, world.", 10, 10);
PrinterJob printerJob = PrinterJob.createPrinterJob();
printerJob.printPage(canvas);

@RequestMapping 注解的 produce 参数

produces参数指明方法可返回给客户端的内容格式,spring会去和request头的Accept部分比较,如果发现相符合,则把方法返回值转换成相符合的格式(例如json, xml等),如果没有符合的,则返回406

RestController的方法上这么写

1
2
@PostMapping(consumes = {MediaType.TEXT_XML_VALUE}, 
produces = {MediaType.TEXT_XML_VALUE})

用下面的命令测试没问题,因为Accept和上面的相符合,会返回xml

1
2
3
4
5
curl -v \
-d '<TvSeriesDto><id>1</id><name>West Wrold</name><originRelease>2016-10-02</originRelease></TvSeriesDto>' \
-X POST -H 'Content-type:text/xml' \
-H 'Accept:text/xml' \
http://localhost:8080/tvseries

如果用下面的命令,则会返回406, Could not find acceptable representation。因为request头accept设置的信息和方法注解produces参数设置没有相符合(applicaiton/xml和text/xml被认为是不同的)

1
2
3
4
5
curl -v  \
-d '<TvSeriesDto><id>1</id><name>West Wrold</name><originRelease>2016-10-02</originRelease></TvSeriesDto>' \
-X POST -H 'Content-type:text/xml' \
-H 'Accept:application/xml' \
http://localhost:8080/tvseries

多个produces参数的顺序的影响

RestController上的RequestMapping的produces参数可以设置多个MediaType, 多个MediaType的顺序也会对结果有些影响,例如:

1
2
@PostMapping(consumes = {MediaType.TEXT_XML_VALUE}, 
produces = {MediaType.APPLICATION_JSON_VALUE, MediaType.TEXT_XML_VALUE})
1
2
3
4
curl -v -d '<TvSeriesDto><id>1</id><name>West Wrold</name><originRelease>2016-10-02</originRelease></TvSeriesDto>' \
-X POST \
-H 'Content-type:text/xml' \
-H 'Accept:*/*' http://localhost:8080/tvseries

得到的是JSON格式,如果把produces参数顺序调整成

1
2
@PostMapping(consumes = {MediaType.TEXT_XML_VALUE}, 
produces = {MediaType.TEXT_XML_VALUE, MediaType.APPLICATION_JSON_VALUE})

用同样的curl命令,则得到的是XML格式。

设置默认的格式

可以通过配置,修改默认的格式

1
2
3
4
5
6
7
8
9
10
@Configuration
public class WebAppConfigurer extends WebMvcConfigurationSupport {

@Override
protected void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
// 这里设置默认的返回格式
configurer.defaultContentType(MediaType.APPLICATION_JSON);
}

}

遇到的问题

Spring boot项目启动过一段较长时间后,在使用上传文件相关功能时,会出现错误。错误信息和下面的类似,临时目录不存在了

Could not parse multipart servlet request; 
nested exception is java.io.IOException: 
The temporary upload location [/tmp/tomcat.7313397276953595407.8090/work/Tomcat/localhost/ROOT] is not valid。

Could not parse multipart servlet request; 
nested exception is java.io.IOException: 
The temporary upload location [C:\Users\Administrator\AppData\Local\Temp\tomcat.7174298170552445669.8002\work\Tomcat\localhost\server] is not valid

原因

这是由于tomcat默认使用的临时目录是在系统临时目录下创建的,而系统临时目录被系统清理了,删除了tomcat的临时目录。

解决方案

知道原因后解决办法也就有了:把tomcat的临时目录设置到一个固定的不会被清理的目录下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Configuration
public class MultipartConfig {

@Bean
MultipartConfigElement multipartConfigElement() {
MultipartConfigFactory factory = new MultipartConfigFactory();
String tempLocation = "c:\\temp";
File tmpFile = new File(tempLocation);
if (!tmpFile.exists()) {
tmpFile.mkdirs();
}
factory.setLocation(tempLocation);
return factory.createMultipartConfig();
}
}

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
import java.io.File;
import java.lang.reflect.Method;
import java.net.URI;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.Arrays;

import javax.tools.JavaCompiler;
import javax.tools.JavaFileObject;
import javax.tools.SimpleJavaFileObject;
import javax.tools.ToolProvider;

public class DynamicCompiler{

/**
* 动态编译传入的类源码并返回编译好的类
*/
public Class compile(String sourceCode) throws Exception{
File distDir = new File("target");
if (!distDir.exists()) {
distDir.mkdirs();
}
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
String className = getFullClassNameFromCode(sourceCode);
JavaFileObject javaFileObject = new CodeJavaFileObject(className, sourceCode);
JavaCompiler.CompilationTask task = compiler.getTask(null, null, null,
Arrays.asList("-d", distDir.getAbsolutePath()), null,
Arrays.asList(javaFileObject));
boolean compileSuccess = task.call();
if (!compileSuccess) {
System.out.println("compile failed");
return null;
} else {
//动态执行 (反射执行)
System.out.println("compile successed " + distDir.getAbsolutePath());
//URL 需要以 file:// 开始; 如果是目录需要以 / 结束;也可以是jar
URL[] urls = new URL[] {new URL("file://" + distDir.getAbsolutePath() + "/")};
URLClassLoader classLoader = new URLClassLoader(urls);
Class dynamicClass = classLoader.loadClass(className);
return dynamicClass;
}
}


class CodeJavaFileObject extends SimpleJavaFileObject{
private String code;

public CodeJavaFileObject(String className, String code){
super(URI.create(className.replaceAll("\\.", "/") + Kind.SOURCE.extension), Kind.SOURCE);
this.code = code;
}

@Override
public CharSequence getCharContent(boolean ignoreEncodingErrors) {
return code;
}
}

private String getFullClassNameFromCode(String code){
String t = code.substring(0, code.indexOf('{'));
String s = t.replaceAll("[\r\n\t]", " ").trim();
String[] ary = s.split("[;]");
String packageName = null;
String className = getLastPart((ary[ary.length - 1]));
if(ary[0].startsWith("package ")){
packageName = getLastPart(ary[0]);
return packageName + "." + className;
}else{
return className;
}
}

private String getLastPart(String s){
String[] ary = s.trim().split(" ");
return ary[ary.length - 1];
}

public static void main(String[] argvs) throws Exception{
StringBuffer buf = new StringBuffer();
buf.append("package a.b.c;\r\npublic class ANumber{")
.append("public int getNumber() {")
.append("System.out.println(\"Hello World in getNumber()!\"); return 999;")
.append("}")
.append("}");

DynamicCompiler dc = new DynamicCompiler();
Class cls = dc.compile(buf.toString());
System.out.println(cls.getName());
Object obj = cls.getDeclaredConstructor().newInstance();
Method method = cls.getDeclaredMethod("getNumber");
Object r = method.invoke(obj);
System.out.println("result is " + r);
}
}

使用java-jwt

创建和解析JWT,可以使用如下依赖:

1
2
3
4
5
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.3.0</version>
</dependency>

创建token

1
2
3
4
5
6
7
8
/** 生成JWT **/
public String createToken(String name, String userId, String email) throws IllegalArgumentException, UnsupportedEncodingException{
JWTCreator.Builder builder = JWT.create();
Algorithm algorithm = Algorithm.HMAC256("xxxxx"); //另外一端解析时也需要这个密码
String token = builder.withClaim("name", name.withClaim("user_id", userId).withClaim("email", email)
.withExpiresAt(new Date(new Date().getTime() + 24*3600*1000)).sign(algorithm);
return token;
}

解析token

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/** 解析JWT **/
public User parseJwt(String token){
User = new User();
try {
Algorithm algorithm = Algorithm.HMAC256("xxxxx"); // 另外一端解析时也需要这个密码
JWTVerifier verifier = JWT.require(algorithm).acceptExpiresAt(5).build(); // Reusable verifier instance
DecodedJWT jwt = verifier.verify(token);
String userId = jwt.getClaim("user_id").asString();
String name = jwt.getClaim("name").asString();
String email = jwt.getClaim("email").asString();
user.setId(userId);
user.setName(name);
user.setEmail(email);
} catch (JWTVerificationException | IllegalArgumentException | UnsupportedEncodingException e) {
log.error("无效的token" + token, e);
return null;
} catch (Throwable e) {
log.error("验证token异常:" + token, e);
return null;
}
return user;
}

上面是用了相同的密码做的签名,也可以用RSA的公钥私钥。

用CSS把SVG图片显示为背景图,可分为2种写法:

  1. 单独的.svg文件,css里用 background-image: url('xx.svg'); 去调用图片
  2. 使用url的data属性,把svg文件合并到css中

第2种写法直接把svg的xml内容拷贝到css中,有些浏览器会识别不出来,这是由于写法不规范造成的。

规范的写法把svg内容放到css内时需要encode,有两种encode方式: base64 和 URLEncode

base64

做base64的编码后,需要 background-image: url('data:image/svg+xml;base64,这里放base64后的svg')

urlencode

urlencode可以直接放 background-image: url('data:image/svg+xml;charset=utf-8,这里放urlencode后的svg)

但需要注意URLEncode有两种可能:

  • 空格变加号,这是在 content type 是 ‘application/x-www-form-urlencoded’时,也就是提交表单时,POST DATA的URLEncoding方式。
  • 空格变%20,这是标准的URL的encode, 在css中使用svg需要用这种encode方法。

如果你找到的工具编码出来空格是加号,可以把+都替换成%20即可。

例子

下面2个文件是个例子:

bg.svg

1
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0" y="0" width="100" height="100" viewBox="0, 0, 100, 100"><circle cx="50" cy="50" r="50"/></svg>

test.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<!DOCTYPE html>
<html>
<head>
<title>svg background</title>
<style type="text/css">
div {margin: 50px; border: 1px solid #CDCDCD; padding:5px; height:150px; width:300px; background-repeat: no-repeat; background-position: center center; float:left;}
.bg1{background-image: url('bg.svg');}
.bg2{background-image: url('data:image/svg+xml;charset=utf-8,%3Csvg%20version%3D%221.1%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20xmlns%3Axlink%3D%22http%3A%2F%2Fwww.w3.org%2F1999%2Fxlink%22%20x%3D%220%22%20y%3D%220%22%20width%3D%22100%22%20height%3D%22100%22%20viewBox%3D%220%2C%200%2C%20100%2C%20100%22%20fill%3D%22%23ECECEC%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%2F%3E%3C%2Fsvg%3E');}
.bg3{background-image: url('data:image/svg+xml;base64,PHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjAiIHk9IjAiIHdpZHRoPSIxMDAiIGhlaWdodD0iMTAwIiB2aWV3Qm94PSIwLCAwLCAxMDAsIDEwMCIgZmlsbD0iI0VDRUNFQyI+PGNpcmNsZSBjeD0iNTAiIGN5PSI1MCIgcj0iNTAiLz48L3N2Zz4=');}
.bg4{background-image: url('data:image/svg+xml;charset=utf-8,<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0" y="0" width="100" height="100" viewBox="0, 0, 100, 100" fill="#ECECEC"><circle cx="50" cy="50" r="50"/></svg>');}
</style>
</head>
<body>
<div class="bg1">使用独立的svg文件</div>
<div class="bg2">urlencode, urlencode编码,注意空格需要编码成%20,空格转义成加号的方式无效</div>
<div class="bg3">base64,用base64编码后的</div>
<div class="bg4">直接写,这是不规范的写法,可能有部分浏览器会生效,能看到背景图</div>
</body>
</html>

小工具

每次从网上下载的SVG的图片中,都带有一些注释、过多的无实际意义的 g 标签,id 属性等,这些也占用CSS空间,我这个有强迫症患者,每次都得手工清理,然后再找个工具base64编码,才能写到 CSS 内,今天我做了个小工具来把这些都自动化了,地址在: http://www.devmgr.cn/encodesvgtocssbackground.html

可以自动清理 SVG 中无实际意义的节点并 base64 encode,还把常用的css 属性也写到了一起。

出现的问题及原因

Spring boot 在 SpringMVC / REST 和Spring Security一起使用时,编写的RestController 或 Controller 中的方法,GET不出错, POST 403,这可能是CSRF保护导致的。

解决方法

可以通过 httpSecurity.csrf().disable() 关闭csrf防护。

但这不是推荐的做法。官方推荐的做法是增加csrf token

https://docs.spring.io/spring-security/site/docs/current/reference/htmlsingle/#csrf-include-csrf-token

SpringMVC中可以在form中增加:

1
<input type=”hidden” th:name=”${_csrf.parameterName}” th:value=”${_csrf.token}”/>

RESTful API需要把这2个值作为http header传递给后端。

CSRF

CSRF是网络攻击的一种,此处有详细说明

出现的问题

JDK8 调用 java.util.Base64解码时出现异常:java.lang.IllegalArgumentException: Illegal base64 character 20

原因

character 20 是空格,如果待解码的字符串没错,可能是遇到了编码标准的问题。 Base64有几个规范:

  • RFC4648 / RFC2045 这两个规范规定encode后的字符包括:0-9,a-z,A-Z,+/
  • RFC1521 这个规范除上述的0-9,a-z,A-Z,+/外,还包含空格符和换行符

JDK8的Base64遵守的是第一个无空格符和换行符的规范,所以遇到空格或换行会抛异常。

解决方法

知道了原因,解决起来就很简单了,换支持RFC1521标准的decoder解码就可以了,例如apache commons-codec。

动态继承一个类/抽象类(不是接口),(动态实现接口参照https://gist.github.com/gexiangdong/f7536a8d86a631b1c391acf13d334a90

此实现需要cglib, pom中增加依赖

1
2
3
4
5
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>3.2.10</version>
</dependency>

实现代码如下:

待创建的父类,可以是抽象类

1
2
3
4
5
6
7
8
9
10
11
12
/**
* 被创建的类的例子,也可以是抽象类
*/
public class MyClass {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}

动态创建

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
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import java.lang.reflect.Array;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
/**
* 使用CGLIB动态生成一个类的子类的例子
*/
public class MyClassProxy {
public static void main(String[] argvs) throws Exception{
MyClass mc = createDefaultImplementation(MyClass.class);
mc.setName("waytt");
System.out.println(mc.getName());
}
public static <A> A createDefaultImplementation(Class<A> abstractClass) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(abstractClass);
enhancer.setCallback(new MethodInterceptor() {
public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
System.out.println("MethodInterceptor.intercept(proxy, " + method.getName() + ",...)");
if (!Modifier.isAbstract(method.getModifiers())) {
//not abstract method, 可调用类的方法并返回
return methodProxy.invokeSuper(proxy, args);
} else {
// 抽象方法,不能执行父类的方法了,需要自己构建一个返回值
System.out.println("abstract method....");
Class type = method.getReturnType();
// 生成返回值
return null
}
}
});
return (A) enhancer.create();
}
public static <A> A createDefaultImplementation(String className) throws ClassNotFoundException{
return (A) createDefaultImplementation(Class.forName(className));
}
}