Spring Boot 端口设置

本文介绍了Spring Boot设置Http端口的几种方式
Spring Boot默认端口为8080

指定端口

在Spring Boot配置文件application.propertites中设置server.port

1
server.port=8888

添加VM配置 -Dserver.port=8888 亦可指定配置

随机端口

1
server.port=0

获取端口号

使用@LocalServerPort注解可在运行时获取程序的Http端口号

1
2
3
4
5
6
7
8
9
10
11
12
13
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(webEnvironment=WebEnvironment.RANDOM_PORT)
public class MyWebIntegrationTests {

@Autowired
EmbeddedWebApplicationContext server;

@LocalServerPort
int port;

// ...

}

官方文档详见

Java时间转换-JodaTime转Date,Java8 JavaTime转Date

JodaTime转换

  • org.joda.time.LocalDate转java.util.Date
1
Date date = localDate.toDateTimeAtStartOfDay().toDate();
  • org.joda.time.LocalDateTime转java.util.Date
1
Date date = localDateTime.toDate();

Java8 JavaTime转换

  • java.time.localDate转java.util.Date
1
Date date = Date.from(localDate.atStartOfDay().atZone(ZoneId.systemDefault()).toInstant());
  • java.time.LocalDateTime转java.util.Date
1
Date date = Date.from(localDateTime.atZone(ZoneId.systemDefault()).toInstant());
  • java.time.ZonedDateTime转java.util.Date
1
Date date = Date.from(zonedDateTime.toInstant());

Hibernate枚举类型的映射

Hibernate枚举类型的映射

本文介绍一下Hibernate映射枚举类型的几种方式.

首先定义一个枚举.

1
2
3
4
5
6
7
8
9
10
11
12
13
public enum Gender {
male("男"),
female("女");

private String name;
private Gender(String name){
this.name = name;
}

public char getValue() {
return name;
}
}

基本 - 按枚举的ordinal属性映射

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Entity
public class Person {

@Id
@GeneratedValue
private Long id;

@Column
private String name;

// 没有额外注解 或 添加注解 @Enumerated(EnumType.ORDINAL)
@Column
// @Enumerated(EnumType.ORDINAL) // 可省略
private Gender gender;

// ...
}

按照这种写法, Hibernate会将Enum类型在声明中的顺序即ordinal属性映射成int型存入数据库, 对于枚举Gender来说, male在数据库中存的是 0, 而female是 1.
好处是简单, 缺点是枚举值多时可读性差, 若声明顺序不小心发生变化映射会出错且不易发现.

基本 - 按枚举的name属性映射

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Entity
public class Person {

@Id
@GeneratedValue
private Long id;

@Column
private String name;

@Column
@Enumerated(EnumType.STRING)
private Gender gender;

// ...
}

使用此方法数据库中存储的是枚举类型的name属性, 即male在数据库中存的就是’male’, female在数据库中存的就是’female’.
好处是同样也比较简单,可读性大于第一种

附EnumType源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* Defines mapping for enumerated types. The constants of this
* enumerated type specify how a persistent property or
* field of an enumerated type should be persisted.
*
* @since Java Persistence 1.0
*/
public enum EnumType {
/** Persist enumerated type property or field as an integer. */
ORDINAL,

/** Persist enumerated type property or field as a string. */
STRING
}

灵活映射 - 使用Hibernate自定义类型

自定义类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class GenderType extends AbstractSingleColumnStandardBasicType<Gender> {

public static final GenderType INSTANCE = new GenderType();

public GenderType() {
super(
CharTypeDescriptor.INSTANCE,
GenderJavaTypeDescriptor.INSTANCE
);
}

public String getName() {
return "gender";
}

@Override
protected boolean registerUnderJavaType() {
return true;
}
}
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
public class GenderJavaTypeDescriptor extends AbstractTypeDescriptor<Gender> {

public static final GenderJavaTypeDescriptor INSTANCE =
new GenderJavaTypeDescriptor();

protected GenderJavaTypeDescriptor() {
super( Gender.class );
}

public String toString(Gender value) {
return value == null ? null : value.name();
}

public Gender fromString(String string) {
return string == null ? null : Gender.valueOf( string );
}

public <X> X unwrap(Gender value, Class<X> type, WrapperOptions options) {
return CharacterTypeDescriptor.INSTANCE.unwrap(
value == null ? null : value.getValue(),
type,
options
);
}

public <X> Gender wrap(X value, WrapperOptions options) {
return Gender.fromString(
CharacterTypeDescriptor.INSTANCE.wrap( value, options )
);
}
}

使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Entity(name = "Person")
public static class Person {

@Id
@GeneratedValue
private Long id;

@Column
private String name;

@Column
@Type( type = "org.hibernate.userguide.mapping.basic.GenderType" )
public Gender gender;

// ...

}

灵活映射 - 使用AttributeConverter

AttributeConverter可用于各种类型的自定义映射, 当然也包括了枚举类型.
AttributeConverter<X, Y>, 其中 X 是实体属性的类型, Y 是数据库字段的类型.

示例:
定义转换器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class GenderConverter implements AttributeConverter<Gender, String> {

// 将实体属性X转化为Y存储到数据库中
@Override
public String convertToDatabaseColumn(Gender attribute) {
if (attribute == null) {
return null;
}
return attribute.getValue();
}

// 将数据库中的字段Y转化为实体属性X
@Override
public Gender convertToEntityAttribute(String dbData) {
return Gender.fromString(dbData);
}
}

完善枚举类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public enum Gender {
male("男"),
female("女");

private String value;
private Gender(String value){
this.value = value;
}

public static Gender fromString(String value) {
Gender gender = null;
switch(value): {
case "男":
gender = male;
break;
case "女":
gender = female;
break;
}
return gender;
}
}

使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Entity
public class Person {

@Id
@GeneratedValue
private Long id;

@Column(nullable = false)
private String name;

@Column
@Convert(converter = GenderConverter.class)
private Gender gender;

// ...
}

属性转换器需要实现AttributeConverter接口,该接口可以完成实体中数据和数据库中数据的相互转换.

此时在数据库中对应存入的值为’男’,’女’

在实际项目中使用的枚举类型可能又有相似的地方,比如male(“男”), 有一个字符串参数, 需要使用该参数进行映射. 若有若干相似枚举类型, 每个枚举都写属性转换器太过麻烦. 此时可以定义通用接口, 相关枚举类型实现该接口, 然后将转换器AttributeConverter<X, Y>中的X设为该接口. 对应枚举再定义static class去继承通用转换器, 从而简化了代码.

Thymeleaf SAXParseException: The content of elements must consist of well-formed character data or markup

问题

使用Thymeleaf时遇到如下异常

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Caused by: org.xml.sax.SAXParseException: The content of elements must consist of well-formed character data or markup.
at com.sun.org.apache.xerces.internal.util.ErrorHandlerWrapper.createSAXParseException(ErrorHandlerWrapper.java:203)
at com.sun.org.apache.xerces.internal.util.ErrorHandlerWrapper.fatalError(ErrorHandlerWrapper.java:177)
at com.sun.org.apache.xerces.internal.impl.XMLErrorReporter.reportError(XMLErrorReporter.java:400)
at com.sun.org.apache.xerces.internal.impl.XMLErrorReporter.reportError(XMLErrorReporter.java:327)
at com.sun.org.apache.xerces.internal.impl.XMLScanner.reportFatalError(XMLScanner.java:1438)
at com.sun.org.apache.xerces.internal.impl.XMLDocumentFragmentScannerImpl$FragmentContentDriver.startOfMarkup(XMLDocumentFragmentScannerImpl.java:2637)
at com.sun.org.apache.xerces.internal.impl.XMLDocumentFragmentScannerImpl$FragmentContentDriver.next(XMLDocumentFragmentScannerImpl.java:2735)
at com.sun.org.apache.xerces.internal.impl.XMLDocumentScannerImpl.next(XMLDocumentScannerImpl.java:606)
at com.sun.org.apache.xerces.internal.impl.XMLDocumentFragmentScannerImpl.scanDocument(XMLDocumentFragmentScannerImpl.java:510)
at com.sun.org.apache.xerces.internal.parsers.XML11Configuration.parse(XML11Configuration.java:848)
at com.sun.org.apache.xerces.internal.parsers.XML11Configuration.parse(XML11Configuration.java:777)
at com.sun.org.apache.xerces.internal.parsers.XMLParser.parse(XMLParser.java:141)
at com.sun.org.apache.xerces.internal.parsers.AbstractSAXParser.parse(AbstractSAXParser.java:1213)
at com.sun.org.apache.xerces.internal.jaxp.SAXParserImpl$JAXPSAXParser.parse(SAXParserImpl.java:643)
at com.sun.org.apache.xerces.internal.jaxp.SAXParserImpl.parse(SAXParserImpl.java:327)
at org.thymeleaf.templateparser.xmlsax.AbstractNonValidatingSAXTemplateParser.doParse(AbstractNonValidatingSAXTemplateParser.java:209)
at org.thymeleaf.templateparser.xmlsax.AbstractNonValidatingSAXTemplateParser.parseTemplateUsingPool(AbstractNonValidatingSAXTemplateParser.java:134)
... 47 common frames omitted

原因

The content of elements must consist of well-formed character data or markup.通常是因为代码不符合XML语法规范,像是元素名称使用数字开头、包含空格等,如:

1
2
<0></0>
< a></a>

查看代码后, 发现在JavaScript代码中有下面的用法

1
if (width < 960) { 

Thymeleaf在执行到 < 时会尝试去解析从而出错。

解决办法

使用CDATA避免其中的文本被解析

1
2
3
4
5
<script type="text/javascript">
/*<![CDATA[*/
...
/*]]>*/
</script>

Thymeleaf相关链接:

Thymeleaf List指定数量循环
Thymeleaf实现页面静态化
Thymeleaf TemplateProcessingException
Thymeleaf比较判断枚举类型

Thymeleaf比较判断枚举类型

问题

使用Thymeleaf试图比较、判断枚举类型

1
2
<span th:if="${source.sourceType == 0}>test</span>
<span th:if="${source.sourceType == 'MOVIE'">test</span>

以上两种方式均不能成功

解决办法

1
2
3
<span th:if="${source.sourceType.name() == 'MOVIE'}>test</span>
// 或
<span th:if="${source.sourceType == T(me.gelu.SourceType).MOVIE}">test</span>

Thymeleaf相关链接:

Thymeleaf List指定数量循环
Thymeleaf SAXParseException
Thymeleaf实现页面静态化
Thymeleaf TemplateProcessingException

Java多环境配置文件的管理(使用Maven Profile)

例子

pom.xml

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
<project ...>
...
<build>
<resources>
<directory>src/main/resources</directory>
<excludes>
<exclude>filters/*</exclude>
...
</excludes>
<filtering>true</filtering> <!-- 会用filter里的配置对resources文件里配置进行替换 -->
</resources>
<filters>
<filter>${basedir}/src/main/resources/application-${env}.properties</filter>
</filters>
</build>
<profiles>
<profile>
<id>dev</id>
<properties>
<env>dev</env>
</properties>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
</profile>
<profile>
<id>prod</id>
<properties>
<env>prod</env>
</properties>
</profile>
</profiles>
</project>

application.properties

1
jdbc.password=${jdbc.config.url}

filters/application-dev.properties

1
jdbc.config.url=123456

编译后target下application.properties里

1
jdbc.properties=123456

其他环境同理,编译时指定profile即可。如mvn clean install -P prod即会使用filters/application-prod.properties下的配置进行替换。

Java信号量-Semaphore

简介

信号量常常用于多线程的代码中,它维护了一个许可集合。提供同步机制来控制同时访问的个数。java.util.concurrent包下的Semaphore可以很容易的进行信号量控制。

Java 中Semaphore提供了两中构造函数:

1
2
3
Semaphore(int permits);
Semaphore(int permits, boolean fair);
// permits表示最大的许可数量。fair表示是否公平模式(即true表示线程要按FIFO获取机会,无该字段的构造函数或该值为false则表示线程随机顺序获取许可)。

Semaphore提供acquire()、release()来获取和释放许可。程序可通过acquire()获取一个许可,没有则等待直到release()释放一个许可。

例子

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
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;

public class SemaphoreDemo {

public static void main(String[] args) {
ExecutorService exec = Executors.newCachedThreadPool();
// 只有5个线程能同时访问
final Semaphore semaphore = new Semaphore(5);
// 模拟10个客户端访问
for (int index = 0; index < 10; index++) {
final int NO = index;
Runnable run = () -> {
try {
// 获取许可
semaphore.acquire();
System.out.println("Accessing: " + NO);
TimeUnit.SECONDS.sleep(10);
// 释放许可
semaphore.release();
//availablePermits()返回当前有多少个许可可被获取
System.out.println("-----------------" + semaphore.availablePermits());
} catch (InterruptedException e) {
e.printStackTrace();
}
};
exec.execute(run);
}
exec.shutdown();
}

}

结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Accessing: 0
Accessing: 1
Accessing: 3
Accessing: 2
Accessing: 4
-----------------1
Accessing: 5
-----------------1
Accessing: 6
-----------------1
-----------------1
Accessing: 7
-----------------1
Accessing: 9
Accessing: 8
-----------------1
-----------------2
-----------------3
-----------------4
-----------------5

模板方法模式

问题

现实中有流程确定而具体执行不知道的情况,此时可使用模板模式。

实现思路

定义一个抽象类实现部分逻辑并声明一些抽象方法给子类去实现(定义一个操作算法的骨架),当其子类以不同逻辑实现了这些抽象方法时就形成不同的行为(步骤延迟到子类)。使得子类可不改变一个算法的结构即可重定义该算法的某些特定步骤。

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 模板类
public abstract class AbstractTemplate {
final void method1() {
...
method2();
...
}
abstract void method2();
final void method3() {...}
}

// 子类
public class ChildClass extends AbstractTemplate {
void method2() {
// TODO 实现之
}
}

Java TimeUnit的使用

简易的日期工具类,位于java.util.concurrent包下。

1
2
3
4
5
6
7
8
9
10
11
// 一些用法
TimeUnit.SECONDS.toMillis(1) 1秒转换为毫秒数
TimeUnit.SECONDS.toMinutes(60) 60秒转换为分钟数
TimeUnit.SECONDS.sleep(5) 线程休眠5秒
TimeUnit.SECONDS.convert(1, TimeUnit.MINUTES) 1分钟转换为秒数

//TimeUnit.DAYS 日的工具类
//TimeUnit.HOURS 时的工具类
//TimeUnit.MINUTES 分的工具类
//TimeUnit.SECONDS 秒的工具类
//TimeUnit.MILLISECONDS 毫秒的工具类

TimeUnit提供了更优雅的线程sleep、timeJoin操作,通常用来替换Thread.sleep(),Thread.join()。
例子:

1
2
3
4
5
// 使用Thread sleep:
Thread.sleep(4 * 60 * 1000);

// 使用TimeUnit
TimeUnit.MINUTES.sleep(4);

TimeUnit中的sleep方法内部调用的也是Thread.sleep(..), timeJoin同理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public void sleep(long timeout) throws InterruptedException {
if (timeout > 0) {
long ms = toMillis(timeout);
int ns = excessNanos(timeout, ms);
Thread.sleep(ms, ns);
}
}

public void timedJoin(Thread thread, long timeout)
throws InterruptedException {
if (timeout > 0) {
long ms = toMillis(timeout);
int ns = excessNanos(timeout, ms);
thread.join(ms, ns);
}
}

常用用户认证方式简介

Http Basic Auth

简单,每次请求都需要提供用户username、password,暴露的风险大不安全,生产环境不要使用。

基于Cookie认证

一次登录请求中在服务器端创建Session对象,保存用户相关信息并返回唯一sessionId,客户端浏览器会创建一个Cookie对象存放sessionId,以后的请求会携带sessionId用于认证。存在CSRF风险,可通过设置Refer来避免。

Token认证

和基于Cookie认证相比的好处:

  • 支持跨域访问
    Cookie是不允许垮域访问的,这一点对Token机制是不存在的,前提是传输的用户认证信息通过HTTP头传输.
  • 无状态
    在服务器端无需存储Session信息,因为Token本身包含了所有登录用户的信息,只需在客户端的cookie或local storage、 session storage中存储即可。
  • 更适用于移动应用
    原生应用不支持Cookie,需进行特殊处理。
  • CSRF
    因为不再依赖于Cookie,所以就不需要考虑对CSRF(跨站请求伪造)的防范。
  • 性能
    使用Token认证仅需要对Token进行验证和解析。基于Cookie则需要服务器存储Session信息,甚至认证还需要去查询数据库。
  • 基于标准化
    API可以采用标准化的JSON Web Token (JWT)。这个标准已经有多个后端库(.NET, Ruby, Java, Python, PHP)的支持。

OAuth认证

是一种开放授权标准,允许让第三方应用访问该用户在某web服务上存储的资源,不需将用户名密码提供给第三方应用。