GeXiangDong

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

0%

背景

有时客户端给服务器传输的JSON比较大,几兆甚至几十兆,网络传输占用时间较多,而json都是文本格式,压缩率较高。因此考虑传输压缩后的格式。

HTTP协议在相应部分支持Content-Encoding:gzip,压缩response body,而没有压缩request body的设计,这也很合理,因为在客户端发起请求时并不知道服务器是否支持压缩。因此没法通过http自身解决,只能增加些程序。

压缩和解压都消耗CPU时间,需要根据实际情况决定是否使用压缩方案,一般内网(微服务)调用启用压缩后有可能得不偿失。在网速较慢的情况下才会有意义。

方案

考虑到通用性,仿效response的header Content-Encoding:gzip方式。

客户端把压缩过的json作为post-body传输,然后增加一个request header: Content-Encoding: gzip来告诉服务器端是压缩的格式。

服务端增加一个Filter,对request头进行检查,如果有Content-Encoding则解压缩后继续。这样不影响现有程序。

服务端还有另外一个解决方案,在NGINX反向代理处增加一个插件(需要去找实现这种功能的插件,官方未提供)把解压缩后的request body传给应用服务器(tomcat)。

参照 http://www.pataliebre.net/howto-make-nginx-decompress-a-gzipped-request.html#.XBToGi276i5

实现

服务器端(Spring)

增加2个类:

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
package cn.devmgr.springcloud;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;


@Service
public class ContentEncodingFilter extends OncePerRequestFilter {
Logger logger = LoggerFactory.getLogger(ContentEncodingFilter.class);

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain chain) throws ServletException, IOException {

String conentEncoding = request.getHeader("Content-Encoding");
if(conentEncoding != null && ("gzip".equalsIgnoreCase(conentEncoding) || "deflate".equalsIgnoreCase(conentEncoding))){
logger.trace("Content-Encoding: {}", conentEncoding);
chain.doFilter(new GZIPRequestWrapper(request), response);
return;
}

chain.doFilter(request, response);
}
}

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
package cn.devmgr.springcloud;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.util.zip.DeflaterInputStream;
import java.util.zip.GZIPInputStream;

public class GZIPRequestWrapper extends HttpServletRequestWrapper {
private final static Logger logger = LoggerFactory.getLogger(GZIPRequestWrapper.class);

protected HttpServletRequest request;

public GZIPRequestWrapper(HttpServletRequest request){
super(request);
this.request = request;
}

@Override
public ServletInputStream getInputStream() throws IOException {
ServletInputStream sis = request.getInputStream();
InputStream is = null;
String conentEncoding = request.getHeader("Content-Encoding");
if("gzip".equalsIgnoreCase(conentEncoding)){
is = new GZIPInputStream(sis);
}else if("deflate".equalsIgnoreCase(conentEncoding)){
is = new DeflaterInputStream(sis);
}else{
throw new UnsupportedEncodingException(conentEncoding + " is not supported.");
}
final InputStream compressInputStream = is;
return new ServletInputStream(){
ReadListener readListener;

@Override
public int read() throws IOException {
int b = compressInputStream.read();
if(b == -1 && readListener != null) {
readListener.onAllDataRead();
}
return b;
}

@Override
public boolean isFinished(){
try {
return compressInputStream.available() == 0;
} catch (IOException e) {
logger.error("error", e);
if(readListener != null) {
readListener.onError(e);
}
return false;
}
}

@Override
public boolean isReady() {
try {
return compressInputStream.available() > 0;
} catch (IOException e) {
logger.error("error", e);
if(readListener != null) {
readListener.onError(e);
}
return false;
}
}

@Override
public void setReadListener(final ReadListener readListener) {
this.readListener = readListener;
sis.setReadListener(new ReadListener() {
@Override
public void onDataAvailable() throws IOException {
logger.trace("onDataAvailable");
if(readListener != null){
readListener.onDataAvailable();
}
}

@Override
public void onAllDataRead() throws IOException {
logger.trace("onAllDataRead");
}

@Override
public void onError(Throwable throwable) {
logger.error("onError", throwable);
if(readListener != null){
readListener.onError(throwable);
}
}
});
}
};
}
}

客户端

CURL的测试命令

1
2
3
echo '{"type": "json", "length": 2222}' | gzip > body.gz
curl -v -i http://127.0.0.1:8080/ss -H 'Content-Encoding: gzip' -H 'Content-Type:application/json' --data-binary @body.gz

RestTemplate

使用RestTemplate可以通过配置interceptor来统一增加压缩。实现代码如下:

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
package cn.devmgr.springcloud;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpRequest;
import org.springframework.http.client.ClientHttpRequestExecution;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.util.CollectionUtils;
import org.springframework.web.client.RestTemplate;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.zip.GZIPOutputStream;

@Configuration
public class RestClientConfig {
private final static Logger logger = LoggerFactory.getLogger(RestClientConfig.class);

/**
* request body超过1M时,自动开启压缩模式;(需要服务端支持)
* 压缩和解压会增加程序处理时间,一般内网调用不压缩速度会更快;压缩只是在网络环境较差的情况下有使用意义。
* @return
*/
@Bean
public RestTemplate restTemplate() {
RestTemplate restTemplate = new RestTemplate();

List<ClientHttpRequestInterceptor> interceptors = restTemplate.getInterceptors();
if (CollectionUtils.isEmpty(interceptors)) {
interceptors = new ArrayList<>();
}
interceptors.add(new RestTemplateCompressInterceptor());

restTemplate.setInterceptors(interceptors);
return restTemplate;
}


class RestTemplateCompressInterceptor implements ClientHttpRequestInterceptor {

@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body,
ClientHttpRequestExecution execution) throws IOException {
logger.trace("RestTemplateCompressInterceptor:intercept() body-length: {}", body == null ? 0 : body.length);
byte[] newBody = null;
if(body == null || body.length < 1024 * 1024){
// 小于1M不开启压缩模式
logger.trace("{}不需要压缩", body == null ? 0 : body.length);
newBody = body;
}else{
logger.trace("{}开启压缩模式", body.length);
request.getHeaders().add("Content-Encoding", "gzip");
ByteArrayOutputStream baos = new ByteArrayOutputStream();
GZIPOutputStream gzip;
try {
gzip = new GZIPOutputStream(baos);
gzip.write(body);
gzip.close();
} catch (IOException e) {
logger.error("压缩request body出错.", e);
throw(e);
}
newBody = baos.toByteArray();
logger.trace("压缩后大小 {}", newBody.length);
}
ClientHttpResponse response = execution.execute(request, newBody);
return response;
}
}
}

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。