Runtime.getRuntime().exec异常

问题

Process process = Runtime.getRuntime().exec(“ifconfig | grep inet”); 此时会报如下异常。

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
java.io.IOException: Cannot run program "ifconfig | grep inet": error=2, No such file or directory
at java.lang.ProcessBuilder.start(ProcessBuilder.java:1048)
at java.lang.Runtime.exec(Runtime.java:620)
at java.lang.Runtime.exec(Runtime.java:485)
at com.uploader.util.DeviceUtil.getNetInfo(DeviceUtil.java:73)
at AddressAPITest.ip(AddressAPITest.java:14)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:78)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57)
at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:68)
at com.intellij.rt.execution.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:47)
at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:242)
at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:70)
Caused by: java.io.IOException: error=2, No such file or directory
at java.lang.UNIXProcess.forkAndExec(Native Method)
at java.lang.UNIXProcess.<init>(UNIXProcess.java:247)
at java.lang.ProcessImpl.start(ProcessImpl.java:134)
at java.lang.ProcessBuilder.start(ProcessBuilder.java:1029)
... 26 more

解决

使用sh -c

1
2
String[] command = {"/bin/sh", "-c", "ifconfig | grep inet"};
Process process = Runtime.getRuntime().exec(command);

成功执行。

1
2
3
4
$ man sh
...
-c Read commands from the command_string operand instead of from the standard input. Special param‐eter 0 will be set from the command_name operand and the positional parameters ($1, $2, etc.)set from the remaining argument operands.

获取阿里云STS凭证报错SDK.ServerUnreachable

问题

最近做捣鼓Web端OSS直传,比较安全的直传方式有两种,使用后端签名直传 和 使用STS凭证直传, OSS官方Web端直传例子中使用的是后端签名直传,移动端直传例子使用的STS凭证,相关SDK例子使用的是STS凭证。项目前端使用的是react + antd,因此打算参考使用STS凭证 + ali-oss 进行直传,以后也能复用在移动端上传。

完成相关代码后获取凭证报错。
获取阿里云STS凭证失败

1
2
3
4
5
org.apache.http.impl.execchain.RetryExec - I/O exception (org.apache.http.conn.UnsupportedSchemeException) caught when processing request to {s}->https://sts.aliyuncs.com:443: https protocol is not supported
org.apache.http.impl.execchain.RetryExec - Retrying request to {s}->https://sts.aliyuncs.com:443
me.gelu.base.framework.aliyun.oss.OSSService - {StatusCode=500, ErrorCode=SDK.ServerUnreachable, ErrorMessage=Server unreachable: org.apache.http.conn.UnsupportedSchemeException: https protocol is not supported}

解决

参考文档,对比POM文件,发现core依赖版本可能过高,最近正好升级过依赖版本。

阿里云STS POM版本配置

当前依赖版本

1
2
3
4
5
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-core</artifactId>
<version>4.0.2</version>
</dependency>

降级为截图中的3.5.0即可。

1
2
3
4
5
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-core</artifactId>
<version>3.5.0</version>
</dependency>

这应该不是唯一能用的版本,尝试降到3.7.1,依旧报错,降到升级前版本3.2.8则新编写的STS凭证代码无法使用,需要改写。因此直接参考截图进行配置,获取凭证成功。

阿里云STS凭证获取成功

Configuration property name 'xxx' is not valid

问题

程序出错:Configuration property name ‘xxx’ is not valid, Canonical names should be kebab-case (‘-‘ separated), lowercase alpha-numeric characters and must start with a letter。

1
2
3
4
5
6
7
8
9
Configuration property name 'cmsOss' is not valid:
Invalid characters: 'O'
Bean: CMSImageController
Reason: Canonical names should be kebab-case ('-' separated), lowercase alpha-numeric characters and must start with a letter
Action:
Modify 'cmsOss' so that it conforms to the canonical names requirements.

解决

原因是命名不规范,不要大写字母,使用小写字母,可加中划线-。

1
2
3
4
5
6
7
8
9
10
11
12
@Data
@Component
@ConfigurationProperties(prefix = "cmsOss")
public class CMSOSSConfig {
private String accessId;
private String accessKey;
private String endpoint;
private String bucket;
}

将cmsOss改成cms-oss, 对应配置文件也做相应调整即可。

Thymeleaf实现页面静态化

最近业余时间在折腾CMS框架,需要完成页面静态化。采用的方案是Thymeleaf。
大致流程是:首先需要编辑一些Thymeleaf模板,然后调用接口来向模板里渲染数据,同时输出到指定文件。

添加依赖

项目使用的是Spring Boot,所以直接使用spring-boot-starter-thymeleaf。

1
2
3
4
5
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
<version>2.0.2.RELEASE</version>
</dependency>

配置测试模板 test.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Hello <span th:text="${title}">test</span></title>
</head>
<script th:remove="all" type="text/javascript" src="http://cdn.jsdelivr.net/thymol/latest/thymol.min.js"></script>
<body>
<section class="section">
<div class="container">
<h1 class="title">
Hello World
</h1>
<p class="subtitle">
My first website with <span th:text="${content}">test</span>!
</p>
</div>
</section>
</body>
</html>

将test.html放入resources/templates下即可。

配置Bean

生成Bean供业务使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Configuration
public class AppConfig {
@Bean
public TemplateEngine emailTemplateEngine() {
final SpringTemplateEngine templateEngine = new SpringTemplateEngine();
templateEngine.addTemplateResolver(springTemplateResolver());
return templateEngine;
}
private ITemplateResolver springTemplateResolver() {
SpringResourceTemplateResolver templateResolver = new SpringResourceTemplateResolver();
templateResolver.setOrder(1);
templateResolver.setPrefix("classpath:/templates");
templateResolver.setSuffix(".html");
templateResolver.setTemplateMode(TemplateMode.HTML);
templateResolver.setCharacterEncoding("UTF-8");
return templateResolver;
}
}

在对应的业务类中注入即可。

配置相关工具类

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
public class ThymeleafUtil {
// 隐藏Thymeleaf细节, 直到在渲染数据前都使用Map存储数据
public static Context getContext(Map<String, Object> dataMap) {
Context ctx = new Context();
for (Map.Entry<String, Object> dataEntry : dataMap.entrySet()) {
ctx.setVariable(dataEntry.getKey(), dataEntry.getValue());
}
return ctx;
}
}
import org.apache.commons.io.FileUtils;
public class FileUtil {
// 输出内容到文件
public static void writeStringToFile(String content, String filePath) {
File file = new File(filePath);
try {
FileUtils.writeStringToFile(file, content, Charset.defaultCharset());
} catch (IOException e) {
e.printStackTrace(); // TODO
}
}
}

业务编写

注入TemplateEngine

1
2
@Autowired
private TemplateEngine templateEngine;

1
2
3
4
5
Map<String, Object> dataMap = Maps.newHashMap();
dataMap.put("title", "test");
dataMap.put("content", "test");
Context ctx = ThymeleafUtil.getContext(dataMap);
FileUtil.writeStringToFile(templateEngine.process("test", ctx), "result.html");

完成。

Thymeleaf相关链接:

Thymeleaf List指定数量循环
Thymeleaf SAXParseException
Thymeleaf TemplateProcessingException
Thymeleaf比较判断枚举类型

freemarker出现NonNumericalException异常

问题

使用Freemarker作为模板引擎出现了NonNumericalException异常。错误如下:

1
2
ERROR freemarker.runtime - Error executing FreeMarker template
freemarker.core.NonNumericalException: For "#{...}" content: Expected a number, but this has evaluated to a string (wrapper: f.t.SimpleScalar):

确认了模板数据没有问题,模板及数据都是字符串类型。

解决

不应该使用 #{…}, 应该要使用 ${…}

平常使用较多的表示式有些混淆,这个问题当时找了好久,遂记之。

Spring Data Jpa findById,getOne找不到记录

问题

Spring Data JPA 使用findById(id),getOne(id),虽然数据库中存在记录,但查询不出来。

描述

最近升级项目框架,将Spring Boot版本升级至2.0.2。Spring Data JPA有不少变化,根据id查询 findOne(id)方法没有了,换成了findById(id),同时还有getOne(id)可以使用。

但使用新的根据id的查询方法找不到数据,虽然数据库中记录存在。

解决

查看sql日志发现新方法会将查询Entity中的关联Entity inner join进查询语句。示例如下:

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
// App.class
@Entity
@Table(name = "app")
@EqualsAndHashCode(callSuper = true)
@Data
@NoArgsConstructor
@SequenceGenerator(name = "app_id_seq", sequenceName = "app_id_seq", allocationSize = 1)
public class App extends SequenceIdentityEntity {
private static final long serialVersionUID = -259135240342702789L;
@Id
@Column(name = "id")
@GeneratedValue(generator = "app_id_seq", strategy = GenerationType.SEQUENCE)
private Integer id;
@Column(name = "name")
private String name;
@ManyToOne(optional = false)
@JoinColumn(name = "template_id", nullable = false)
private Template template;
@Column(name = "template_id", updatable = false, insertable = false)
private Integer templateId;
}
// Template.class 略
1
2
3
// 使用findById(id),getOne(id)时打印的sql
select app0.id as id1_0_0, app0.name as name_2_0_0, template1.app_id as app_id5_4_1, template1.name as name6_4_1
from app app0_ inner join template template1_ on app0.template_id=template1.id where app0_.id=?

因此若关联值templateId为空或值映射不上时,就会找不到app记录。

在@ManyToOne添加fetch = FetchType.LAZY即可解决问题,同理使用@OneToOne,@OneToMany等若不能保证 关联字段不为空且一定能找到关联数据 时,均需配置懒加载。

1
2
3
@ManyToOne(optional = false, fetch = FetchType.LAZY)
@JoinColumn(name = "template_id", nullable = false)
private Template template;

Java通过html模板(freemarker模板)生成PDF文件

需求

为方便定制,项目有了通过html模板生成动态生成PDF的需求。本文使用的是flying-saucer-pdf-itext5 + freemarker的方案。

解决

关键的Maven依赖配置

1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>org.xhtmlrenderer</groupId>
<artifactId>flying-saucer-pdf-itext5</artifactId>
<version>9.1.5</version>
</dependency>
<dependency>
<groupId>org.freemarker</groupId>
<artifactId>freemarker</artifactId>
<version>2.3.23</version>
</dependency>

关键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
public void generate(String templateContent, Map<String, String> dataMap, String fontPath, File file) {
FileOutputStream outputStream = null;
ITextRenderer renderer = new ITextRenderer();
try {
Configuration cfg = new Configuration();
StringTemplateLoader stringLoader = new StringTemplateLoader();
stringLoader.putTemplate("myTemplate", templateContent);
cfg.setTemplateLoader(stringLoader);
Template template = cfg.getTemplate("myTemplate", "utf-8");
String htmlData = FreeMarkerTemplateUtils.processTemplateIntoString(template, dataMap);
outputStream = new FileOutputStream(file);
ITextFontResolver fontResolver = renderer.getFontResolver();
// 解决中文乱码问题,fontPath为中文字体地址
fontResolver.addFont(fontPath, BaseFont.IDENTITY_H, BaseFont.NOT_EMBEDDED);
renderer.setDocumentFromString(htmlData);
renderer.layout();
renderer.createPDF(outputStream);
} catch (DocumentException | IOException | TemplateException e) {
log.error("生成失败", e);
} finally {
renderer.finishPDF();
IOUtils.closeQuietly(outputStream);
}
}

其中:

参数templateContent为freemarker模板内容,可直接从项目文件中读取或者定义在数据库中方便定制;

参数dataMap为freemark模板数据;

参数fontPath是为了解决生成的PDF乱码问题,将中文字体位置传入并进行配置同时在模板文件中也需要进行配置;

参数file则是为了将PDF生成至文件。

freemarker模板示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0"/>
<title></title>
<style type="text/css">
@page {
size: 210mm 297mm;
margin:0 auto;padding:0;
}
* {margin:0;padding:0;}
body {background: #fff;font-family: 'Microsoft YaHei'}
</style>
</head>
<body>
<h1>${name!''}标题</h1>
<#if isSigned?exists><span>内容</span></#if>
<img style="width="60" height="60" src="data:image/png;base64,${stamp!''}" />
</body>
</html>
一些需要注意的地方

style中font-family配合java程序中的字体设置可解决PDF中文乱码的问题。

${name!’’}的写法会将 dataMap中 key 为name的 value值渲染进freemarker模板。!’’的写法是为了设置空默认值,从而当dataMap中没有key为name时使用默认值且程序不会报错。

<#if isSigned?exists></#if>的写法让存在isSigned值时显示if里的内容。

img src的写法配合dataMap中图片转base64后stamp值 可用于显示图片。

页眉页脚及打印样式可通过css中的@page等打印属性进行设置。

SpringMVC参数校验的三种方案(普通、异常处理、AOP)

SpringMVC参数校验一般使用hibernate-validator校验框架,网上相关注解资料很多,本文不做介绍,本文主要介绍使用如何降低校验编程的复杂度。

方案一:@Valid + BindingResult

使用@Valid + BindingResult进行参数校验,网上大部分参数校验文章都采用这种方式,示例:

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
// VO
@Data
public class DeviceStorageRecordVO {
@NotNull(message = "数量不能为空")
private Integer count;
}
// Controller
@RestController
@RequestMapping("device")
public class DeviceCtrl {
@PostMapping("storageRecord/add")
public JsonResult addStorageRecord(@Valid @RequestBody DeviceStorageRecordVO deviceStorageRecordVO, BindingResult result) {
// 方法1: 处理result
StringBuilder errorBuilder = new StringBuilder();
List<ObjectError> errors = result.getAllErrors();
for(ObjectError error : errors){
if(StringUtils.hasText(errorBuilder)) {
errorBuilder.append(" ");
}
errorBuilder.append(error.getDefaultMessage());
}
if(StringUtils.hasText(errorBuilder.toString())){
throw new BusiException(ResultEnum.PARAMS_ILLEGAL.getCode(),errorBuilder.toString());
}
deviceServ.addStorageRecord(deviceStorageRecordVO);
return MVCUtil.success();
}
}
// 方法2 Controller AOP
@Aspect
@Component
@Slf4j
public class WebAspect {
// AOP的一些配置:控制器切面,切入点..
// 关键方法:参数校验
private final Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
private void checkParams(ProceedingJoinPoint joinPoint) {
Object[] args = joinPoint.getArgs();
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
Parameter[] ps = method.getParameters();
StringBuilder errorBuilder = new StringBuilder();
for (int i = 0; i < ps.length; i++) {
Parameter p = ps[i];
Annotation annotation = p.getAnnotation(Valid.class);
// Valid判断可以不要, 每个参数都会校验
if (annotation != null) {
Set violations = validator.validate(args[i]);
for (Object violation : violations) {
ConstraintViolation constraintViolation = (ConstraintViolation)violation;
if(StringUtils.isNotBlank(errorBuilder)) {
errorBuilder.append(" ");
}
errorBuilder.append(constraintViolation.getMessage());
}
}
}
if(StringUtils.isNotBlank(errorBuilder.toString())){
throw new BusiException(ResultEnum.PARAMS_ILLEGAL.getCode(),errorBuilder.toString());
}
}
}

在方法1处处理result值,可进行封装 或 如方法2一样使用AOP统一处理。

缺点:麻烦,遗忘添加@Valid 或 BindingResult未跟在@Valid标记的Bean后面均会导致校验无效或应用报错(MethodArgumentNotValidException异常)。

方案二:使用@Valid,不使用BindingResult。

当使用@Valid而未添加BindingResult进行参数校验时,应用会报MethodArgumentNotValidException异常,此时使用ControllerAdvice进行异常的统一处理即可,在方法里进行错误参数message的组装返回。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@ControllerAdvice(annotations = {RestController.class, Controller.class})
@ResponseBody
@Slf4j
public class ExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public JsonResult handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
BindingResult result = e.getBindingResult();
StringBuilder errorBuilder = new StringBuilder();
List<ObjectError> errors = result.getAllErrors();
for(ObjectError error : errors) {
if(StringUtils.hasText(errorBuilder)) {
errorBuilder.append(" ");
}
errorBuilder.append(error.getDefaultMessage());
}
if(StringUtils.hasText(errorBuilder.toString())){
return MVCUtil.error(ResultEnum.PARAMS_ILLEGAL, errorBuilder.toString());
}
return MVCUtil.error(ResultEnum.PARAMS_ILLEGAL, "参数非法");
}
}

优点:不需要添加BindingResult,错误统一处理,代码复杂度降低。

方案三:不使用@Valid和BindingResult

不使用@Valid和BindingResult而在AOP方法里手动进行校验。去掉@Valid注解及BindingResult参数并使用方案一中的AOP,去掉其中的@Valid注解判断(使得所有参数都进行校验)即可。

优点:代码复杂度进一步降低,只需要在需要校验的Bean中使用校验注解进行标记即可。


方案三代码量及复杂度最低,但由于历史原因项目使用了@Valid,担心大量修改导致系统出现一些故障,故项目采用方案二、方案三同时使用,老代码不做修改,新代码直接使用方案三。

Dubbo @Transactional 发布异常

问题

dubbo服务异常,在使用dubbo发布服务时,若服务使用@Transactional无法正常发布、引用

解决

在@Service中指定interfaceName,指定发布的服务接口名称。

1
2
3
4
5
@Service(version = "1.0.0",interfaceName = "me.gelu.dubbotest.api.HelloService")
@Transactional
public class HelloServiceImpl implements HelloService {
// ...
}

Ubuntu启动器配置问题

问题

Idea更新,删除原版本文件夹后再启动Idea发现Ubuntu启动器没有应用图标。

解决

在 ~/.local/share/applications文件夹下找到Idea相关配置,如 jetbrains-idea.desktop。

将相应目录改为新应用的目录即可。

这个问题很久没解决,主要是一般都去 /usr/share/applications/下进行配置,不知道当前用户的相关配置在用户目录 ~/.local/share/applications 中。

最后附一个Idea配置,以后其他应用配置启动器可进行参考:

1
2
3
4
5
6
7
8
9
10
[Desktop Entry]
Version=1.0
Type=Application
Name=IntelliJ IDEA Ultimate Edition
Icon=/home/gelu/env/idea-IU-181.4892.42/bin/idea.png
Exec="/home/gelu/env/idea-IU-181.4892.42/bin/idea.sh" %f
Comment=The Drive to Develop
Categories=Development;IDE;
Terminal=false
StartupWMClass=jetbrains-idea