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

相同类中方法互相调用事务嵌套无效

问题

项目开发中class A中有B、C方法均有自己的事务,在方法B中调用C后发现C的事务不生效。

解决

相同类中方法互相调用事务嵌套无效,因此由外部类D分别调用A中的B、C方法事务生效。

supervisor启动报can't find command问题

问题

supervisor启动java应用不成功,调用supervisorctl status查看进程情况,发现出现
can’t find command ‘java’错误信息。发现可能是环境变量未生效。

解决

修改supervisor配置。

原配置:

1
command=java -jar account.jar

修改后的配置:

1
command=/bin/bash -c 'source "$0" && exec "$@"' /etc/profile java -jar account.jar

修改后的命令先激活环境变量,再执行原命令,此时Java应用启动成功。

Supervisor类似can’t find command的错误均能使用类似方法解决。

Hibernate 启动慢

问题

项目使用PostgreSQL+Hibernate+Spring Boot, 发现启动过慢,有如下日志。

1
2
3
2018-01-18 21:49:31.625 [main] INFO  org.hibernate.dialect.Dialect - HHH000400: Using dialect: org.hibernate.dialect.PostgreSQL95Dialect

2018-01-18 21:50:02.964 [main] INFO o.h.engine.jdbc.env.internal.LobCreatorBuilderImpl - HHH000424: Disabling contextual LOB creation as createClob() method threw error : java.lang.reflect.InvocationTargetException

Disabling contextual LOB creation as createClob() method threw error 费时且没用。

解决

Spring Boot项目中添加如下配置

1
spring.jpa.properties.hibernate.temp.use_jdbc_metadata_defaults: false

添加配置后启动,启动时间可大大缩短。

1
2
3
2018-01-18 21:45:43.852 [main] INFO  org.hibernate.dialect.Dialect - HHH000400: Using dialect: org.hibernate.dialect.PostgreSQLDialect

2018-01-18 21:45:43.873 [main] INFO o.h.engine.jdbc.env.internal.LobCreatorBuilderImpl - HHH000422: Disabling contextual LOB creation as connection was null

设置docker时区

docker容器时区修改方法

方法一 使用数据卷volume:

容器启动时加上 -v /etc/localtime:/etc/localtime, 容器就会与宿主机同一时区
若使用编排工具如docker-compose等时可在配置文件指定volume

方法二 直接更新容器配置:

在Dockerfile里指定

1
RUN echo "Asia/Shanghai" > /etc/timezone

Linux时区参考

Git修改远程仓库地址

1.直接修改

1
git remote set-url origin [url]

2.先删后加

1
2
git remote rm origin
git remote add origin [url]

3.修改配置

直接修改对应项目下.git/config中的origin配置