Spring Boot对静态方法进行打桩

Spring Boot对静态方法进行打桩

问题

在对Spring Boot项目进行测试的时候,会对业务逻辑service层进行测试。而service层的代码可能会使用util中的工具类,而工具方法通常来说都是static类型的方法。问题在于,当对service层的代码进行测试时,我们往往需要对静态方法打桩,返回我们需要的结果。然而,主流的测试框架Mockito并不支持对静态方法的打桩。对于此问题,我们需要寻求其他框架的解决方案

解决

1.

首先,通过mockito官方文档的描述,可以发现:

What are the limitations of Mockito

  • Cannot mock final classes
  • Cannot mock static methods
  • Cannot mock final methods - their real behavior is executed without any exception.
  • Mockito cannot warn you about mocking final methods so be vigilant.

可以看到,Mockito并不支持mock静态方法,同时也有以下的描述:

Can I mock static methods?

No. Mockito prefers object orientation and dependency injection over static, procedural code that is hard to understand & change. If you deal with scary legacy code you can use JMockit or Powermock to mock static methods.

通过这样的描述,我们发现其他框架提供了解决方案:

  • JMockit
  • Powermock

之后,由于网上的回答中,Powermock更加主流,且与Mockito的语法相近,于是考虑通过Powermock框架解决问题。

再次查阅了Powermock在github上的项目主页:,得到以下的说明:

urrently PowerMock supports JUnit and TestNG. There are three different JUnit test executors available, one for JUnit 4.4-4.12, one for JUnit 4.0-4.3. The test executor for JUnit 3 is not avaliable since PowerMock 2.0.

可以看出,Powermock当前最新版本仅支持到JUnit4.12,而无法对JUnit5提供支持。然而,spring-boot-starter-test中,整合的已是JUnit5. 在网上找寻了众多的回答,所有的例子都是通过JUnit4编写的测试脚本。

且通过Powermock测试的脚本结构如下:

1
2
3
4
5
@RunWith(PowerMockRunner.class)
@PrepareForTest( { YourClassWithEgStaticMethod.class })
public class YourTestCase {
...
}

该结构为JUnit4的写法,并不能在JUnit5中使用。我也尝试强行在Spring Boot项目中单独添加JUnit4依赖,然后单独通过JUnit4来运行通过Powermock编写的测试脚本,多次尝试之后依然无法运行。于是转向另一个框架–JMockit。

2.

尝试通过JMockit来测试静态方法。首先查看JMockit是否支持JUnit5,在官网寻找到了答案:

To run tests that use any of the JMockit APIs, use your Java IDE, Maven/Gradle build script, etc. the way you normally would. In principle, any JDK of version 1.7 or newer, on Windows, Mac OS X, or Linux, can be used. JMockit supports (and requires) the use of JUnit (version 4 or 5) or TestNG

既然JMockit支持JUnit5,且可以对静态方法进行打桩,这就是一种可行的解决方案。所以接下来我就去寻找对静态方法进行打桩的实现方案。

在一篇博客中,我发现了解决方案:

  • 首先,需要添加JMockit的依赖,通过mvnrepository添加最新版的JMockit依赖:

    1
    2
    3
    4
    5
    6
    7
    <!-- https://mvnrepository.com/artifact/org.jmockit/jmockit -->
    <dependency>
    <groupId>org.jmockit</groupId>
    <artifactId>jmockit</artifactId>
    <version>1.48</version>
    <scope>test</scope>
    </dependency>
  • 查看博客中给出的例子:

    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
    // 需要测试的类
    public class AppManager {

    public boolean managerResponse(String question) {
    return AppManager.isResponsePositive(question);
    }

    public static boolean isResponsePositive(String value) {
    if (value == null) {
    return false;
    }
    int length = value.length();
    int randomNumber = randomNumber();
    return length == randomNumber ? true : false;
    }

    private static int randomNumber() {
    return new Random().nextInt(7);
    }
    }

    // 对静态方法进行打桩
    @Test
    public void givenAppManager_whenStaticMethodCalled_thenValidateExpectedResponse() {
    new MockUp<AppManager>() {
    @Mock
    public boolean isResponsePositive(String value) {
    return false;
    }
    };

    assertFalse(appManager.managerResponse("Some string..."));
    }
  • 按照类似的写法,完成了我的测试脚本的编写

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    @Test
    @DisplayName("发送邮件内容正确")
    void shouldSendCorrectEmailWhenUserIsNew(){
    // 使用JMockit对静态方法进行打桩
    new MockUp<CheckCodeUtil>(){
    @mockit.Mock
    public String generateCheckCode(){
    return "123456";
    }
    };
    when(redisTemplate.opsForValue()).thenReturn(new ValueOperationsFake());
    ArgumentCaptor<String> emailCaptor = ArgumentCaptor.forClass(String.class);
    ArgumentCaptor<String> subjectCaptor = ArgumentCaptor.forClass(String.class);
    ArgumentCaptor<String> contentCaptor = ArgumentCaptor.forClass(String.class);
    customerService.sendCheckCode("10175101152@stu.ecnu.edu.cn");
    verify(mailService, times(1))
    .sendHtmlMail(emailCaptor.capture(), subjectCaptor.capture(), contentCaptor.capture());
    assertAll(
    () -> assertEquals("10175101152@stu.ecnu.edu.cn", emailCaptor.getValue()),
    () -> assertEquals("Registration from MeetHere", subjectCaptor.getValue()),
    () -> assertEquals("<h1>Welcome to MeetHere!</h1><p>Your check code is <u>" +
    "123456" + "</u></p>", contentCaptor.getValue())
    );
    }
  • 编写完毕后,尝试是否可以运行,但是依然报错:

    1
    java.lang.IllegalStateException: JMockit didn't get initialized; please check the -javaagent JVM initialization parameter was used

    针对报错信息去Google进行查找,大多数的解决方案是:

    1
    @RunWith(JMockit.class)

    这同样是JUnit4的写法。并不能解决我所遇到的问题

  • 再次回到JMockit的官方文档,查看官方文档中给出的运行方法,发现了潜在的解决方案:

    JMockit also requires the -javaagent JVM initialization parameter to be used; when using the Maven Surefire plugin for test execution, it’s specified as follows:

    所以看来想要运行JMockito编写的测试脚本,需要指定-javaagent的JVM参数才可以。

    官网给出了方案:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    <plugins>
    <plugin>
    <artifactId>maven-surefire-plugin</artifactId>
    <version>2.22.2</version> <!-- or some other version -->
    <configuration>
    <argLine>
    -javaagent:${settings.localRepository}/org/jmockit/jmockit/${jmockit.version}/jmockit-${jmockit.version}.jar
    </argLine>
    </configuration>
    </plugin>
    </plugins>

    将这段xml复制到pom文件中,并将${jmockit.version}替换为项目中使用的1.48

  • 再次运行测试脚本:

    屏幕快照 2019-12-10 下午6.12.39

    BINGO!

redis基本配置及开关

redis基本配置及开关

配置redis服务端

  • 允许远程访问

    注释掉 bind 127.0.0.1

  • 以守护进程运行

    daemonize no 改为 daemonize yes

  • Java 连接redis必须设置密码:

    requirepass <password>

查看redis是否在运行
1
ps -ef | grep -i redis
远程连接redis
1
redis-cli -h <ip> -p <port> -a <password>
redis关闭
  • 断电(非正常关闭,容易数据丢失)

    查询PID:

    1
    2
    3
    ps -ef | grep -i redis
    # Then
    kill -9 <PID>
  • 通过客户端关闭(正常关闭,数据保存)

    1
    SHUTDOWN
redis加载配置文件开启
1
redis-server /usr/local/redis/redis.conf

redis.conf

redis.conf

绑定的主机地址
1
bind 127.0.0.1

如果需要远程访问,则需要注释掉

守护进程
1
daemonize no

Redis默认以非守护进程运行,如果想要改为守护进程,则设置为yes

pidfile
1
pidfile /var/run/redis_6379.pid

设置当Redis以守护进程运行时,它的pid写入的文件

端口
1
port 6379

指定Redis的端口

日志记录级别
1
loglevel verbose

Redis总共支持四个级别:debug, verbose, notice, warning, 默认为verbose

设置数据库个数
1
databases 16
指定在多长时间内,有多少次更新操作,就将数据同步到数据文件,可以多个条件配合
1
save <seconds> <changes>

默认配置文件提供三种操作

1
2
3
save 900 1
save 300 10
save 60 10000

满足一个就持久化

指定本地数据库文件名,默认为dump.rdb
1
dbfilename dump.rdb
指定本地数据库存放目录
1
dir ./

默认当前目录

主从服务
1
slaveof <masterip> <masterport>

设置当本机为slave服务时,设置master服务的IP地址及端口,在Redis启动时,它会自动从master进行数据同步

master服务密码保护
1
masterauth <master-password>

当master服务设置了密码保护时,slave服务连接master服务的密码

设置redis连接密码
1
requirepass foobared

如果配置了连接密码,客户端在连接Redis时需要通过 AUTH <password>命令提供密码,默认关闭

Java连接redis必须设置密码

设置最大客户端连接数,默认无限制
1
maxclients 10000
指定redis最大内存限制
1
maxmemory <bytes>
  • 建议根据服务器实际情况设置

  • 建议不要超过1G

对于redis中的内存管理有两种方案:
  • 为数据设置超时事件

  • 采用LRU算法将不用的数据删除

    • volatile-lru: 设定超时时间的数据中,删除最不常使用的元素
    • allkeys-lru: 查询所有key中最近最不常使用的数据进行删除,应用最广泛
    • volatile-random: 在已经设定了超时的数据中随机删除
    • allkeys-random: 查询所有的key之后随机删除
    • volatile-ttl: 查询全部设定超时时间的数据,之后排序,将马上快要过期的数据进行删除操作
    • noevication: 不进行删除操作,内存溢出就报错返回

配置redis服务端

  • 允许远程访问

    注释掉 bind 127.0.0.1

  • 以守护进程运行

    daemonize no 改为 daemonize yes

  • Java 连接redis必须设置密码:

    requirepass <password>

查看redis是否在运行
1
ps -ef | grep -i redis
远程连接redis
1
redis-cli -h <ip> -p <port> -a <password>

Serializable接口

Serializable接口

Serializable接口概述

Serializablejava.io包中定义的、用于实现Java类的序列化操作而提供的一个语义级别的接口。Serializable序列化接口没有任何方法或者字段,只是用于标识可序列化的语义。实现了Serializable接口的类可以被ObjectOutputStream转换为字节流,同时也可以通过ObjectInputStream再将其解析为对象。例如,我们可以将序列化对象写入文件后,再次从文件中读取它并反序列化成对象,也就是说,可以使用表示对象及其数据的类型信息和字节在内存中重新创建对象。

而这一点对于面向对象的编程语言来说是非常重要的,因为无论什么编程语言,其底层涉及IO操作的部分还是由操作系统其帮其完成的,而底层IO操作都是以字节流的方式进行的,所以写操作都涉及将编程语言数据类型转换为字节流,而读操作则又涉及将字节流转化为编程语言类型的特定数据类型。而Java作为一门面向对象的编程语言,对象作为其主要数据的类型载体,为了完成对象数据的读写操作,也就需要一种方式来让JVM知道在进行IO操作时如何将对象数据转换为字节流,以及如何将字节流数据转换为特定的对象,而Serializable接口就承担了这样一个角色。

下面我们可以通过例子来实现将序列化的对象存储到文件,然后再将其从文件中反序列化为对象,代码示例如下:

先定义一个序列化对象User:

1
2
3
4
5
6
7
8
9
10
11
public class User implements Serializable { 
private static final long serialVersionUID = 1L;

private String userId;
private String userName;

public User(String userId, String userName) {
this.userId = userId;
this.userName = userName;
}
}

然后我们编写测试类,来对该对象进行读写操作,我们先测试将该对象写入一个文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class SerializableTest { 

/**
* 将User对象作为文本写入磁盘
*/
public static void writeObj() {
User user = new User("1001", "Joe");
try {
ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("/Users/guanliyuan/user.txt"));
objectOutputStream.writeObject(user);
objectOutputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}

public static void main(String args[]) {
writeObj();
}
}

运行上述代码,我们就将User对象及其携带的数据写入了文本user.txt中,我们可以看下user.txt中存储的数据此时是个什么格式:

1
2
3
4
5
java.io.NotSerializableException: cn.wudimanong.serializable.User 
at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1184)
at java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:348)
at cn.wudimanong.serializable.SerializableTest.writeObj(SerializableTest.java:19)
at cn.wudimanong.serializable.SerializableTest.main(SerializableTest.java:27)

我们看到对象数据以二进制文本的方式被持久化到了磁盘文件中。在进行反序列化测试之前,我们可以尝试下将User实现Serializable接口的代码部分去掉,看看此时写操作是否还能成功,结果如下:

结果不出所料,果然是不可以的,抛出了NotSerializableException异常,提示非可序列化异常,也就是说没有实现Serializable接口的对象是无法通过IO操作持久化的。

接下来,我们继续编写测试代码,尝试将之前持久化写入user.txt文件的对象数据再次转化为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
public class SerializableTest { 
/**
* 将类从文本中提取并赋值给内存中的类
*/
public static void readObj() {
try {
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("/Users/guanliyuan/user.txt"));
try {
Object object = objectInputStream.readObject();
User user = (User) object;
System.out.println(user);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
} catch (IOException e) {
e.printStackTrace();
}
}


public static void main(String args[]) {
readObj();
}
}

通过反序列化操作,可以再次将持久化的对象字节流数据通过IO转化为Java对象,结果如下:

1
cn.wudimanong.serializable.User@6f496d9f

此时,如果我们再次尝试将User实现Serializable接口的代码部分去掉,发现也无法再文本转换为序列化对象,报错信息为:

1
2
3
4
5
6
7
8
ava.io.InvalidClassException: cn.wudimanong.serializable.User; class invalid for deserialization 
at java.io.ObjectStreamClass$ExceptionInfo.newInvalidClassException(ObjectStreamClass.java:157)
at java.io.ObjectStreamClass.checkDeserialize(ObjectStreamClass.java:862)
at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2038)
at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1568)
at java.io.ObjectInputStream.readObject(ObjectInputStream.java:428)
at cn.wudimanong.serializable.SerializableTest.readObj(SerializableTest.java:31)
at cn.wudimanong.serializable.SerializableTest.main(SerializableTest.java:44)

提示非法类型转换异常,说明在Java中如何要实现对象的IO读写操作,都必须实现Serializable接口,否则代码就会报错!

序列化&反序列化

通过上面的阐述和示例,相信大家对Serializable接口的作用是有了比较具体的体会了,接下来我们上层到理论层面,看下到底什么是序列化/反序列化。序列化是指把对象转换为字节序列的过程,我们称之为对象的序列化,就是把内存中的这些对象变成一连串的字节(bytes)描述的过程。

而反序列化则相反,就是把持久化的字节文件数据恢复为对象的过程。那么什么情况下需要序列化呢?

大概有这样两类比较常见的场景:

  • 需要把内存中的对象状态数据保存到一个文件或者数据库中的时候,这个场景是比较常见的,例如我们利用mybatis框架编写持久层insert对象数据到数据库中时
  • 网络通信时需要用套接字在网络中传送对象时,如我们使用RPC协议进行网络通信时

关于serialVersionUID

对于JVM来说,要进行持久化的类必须要有一个标记,只有持有这个标记JVM才允许类创建的对象可以通过其IO系统转换为字节数据,从而实现持久化,而这个标记就是Serializable接口。而在反序列化的过程中则需要使用serialVersionUID来确定由那个类来加载这个对象,所以我们在实现Serializable接口的时候,一般还会要去尽量显示地定义serialVersionUID,如:

1
private static final long serialVersionUID = 1L;

在反序列化的过程中,如果接收方为对象加载了一个类,如果该对象的serialVersionUID与对应持久化时的类不同,那么反序列化的过程中将会导致InvalidClassException异常。例如,在之前反序列化的例子中,我们故意将User类的serialVersionUID改为2L,如:

1
private static final long serialVersionUID = 2L;

那么此时,在反序例化时就会导致异常,如下:

1
2
3
4
5
6
7
8
9
java.io.InvalidClassException: cn.wudimanong.serializable.User; local class incompatible: stream classdesc serialVersionUID = 1, local class serialVersionUID = 2 
at java.io.ObjectStreamClass.initNonProxy(ObjectStreamClass.java:687)
at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1880)
at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1746)
at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2037)
at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1568)
at java.io.ObjectInputStream.readObject(ObjectInputStream.java:428)
at cn.wudimanong.serializable.SerializableTest.readObj(SerializableTest.java:31)
at cn.wudimanong.serializable.SerializableTest.main(SerializableTest.java:44)

如果我们在序列化中没有显示地声明serialVersionUID,则序列化运行时将会根据该类的各个方面计算该类默认的serialVersionUID值。但是,Java官方强烈建议所有要序列化的类都显示地声明serialVersionUID字段,因为如果高度依赖于JVM默认生成serialVersionUID,可能会导致其与编译器的实现细节耦合,这样可能会导致在反序列化的过程中发生意外的InvalidClassException异常。因此,为了保证跨不同Java编译器实现的serialVersionUID值的一致,实现Serializable接口的必须显示地声明serialVersionUID字段。

此外serialVersionUID字段地声明要尽可能使用private关键字修饰,这是因为该字段的声明只适用于声明的类,该字段作为成员变量被子类继承是没有用处的!有个特殊的地方需要注意的是,数组类是不能显示地声明serialVersionUID的,因为它们始终具有默认计算的值,不过数组类反序列化过程中也是放弃了匹配serialVersionUID值的要求。

Spring Boot-CORS

Spring Boot-CORS

什么是跨域?

定义:浏览器从一个域名的网页取请求另一个域名下的东西。通俗点说,浏览器直接从A域访问B域中的资源是不被允许的,如果想要访问,就需要进行一步操作,这操作就叫“跨域”。例如,你从百度的页面,点击一个按钮,请求了新浪的一个接口,这就进行了跨域。不单单只有域名不同就是跨域,域名、端口、协议其一不同就是不同的域,请求资源需要跨域。

为什么要跨域?

为什么需要跨域,而不直接访问其他域下的资源呢?这是浏览器的限制,专业点说叫浏览器同源策略限制。主要是为了安全考虑。现在的安全框架,一般请求的时候header中不是都存个token嘛,你要是用这个token去正常访问A域下的东西是没问题的,然后又去访问了B域,结果阴差阳错的还带着这个token,那么B域,或者说B网站是不是就可以拿着你的token去A域下做点什么呢,这就相当危险了。所以浏览器加上了所谓的浏览器同源策略限制。但是为了我们真的需要从A域下访问B的资源(正常访问),就需要用到跨域,跨越这个限制了。

SpringBoot解决跨域问题

SpringBoot可以基于Cors解决跨域问题,Cors是一种机制,告诉我们的后台,哪边(origin )来的请求可以访问服务器的数据。

全局配置

配置实例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Configuration
public class CorsConfig {
@Bean
public WebMvcConfigurer corsConfigurer(){
return new WebMvcConfigurer(){
@Override
public void addCorsMappings(CorsRegistry registry){
registry.addMapping("/**")
.allowedOrigins("*")
.allowedMethods("POST", "GET", "PUT", "OPTIONS", "DELETE")
.allowCredentials(true)
.maxAge(3600);
}
};
}
}

首先实现了WebMvcConfigurer接口,WebMvcConfigurer这个接口十分强大,里面还有很多可用的方法,在SpringBoot2.0里面可以解决WebMvcConfigurerAdapter曾经的部分任务。其中一个方法就是addCorsMappings(),是专门为开发人员解决跨域而诞生的接口。其中构造参数为CorsRegistry

CorsRegistry的源码

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

private final List<CorsRegistration> registrations = new ArrayList<>();

public CorsRegistration addMapping(String pathPattern) {
CorsRegistration registration = new CorsRegistration(pathPattern);
this.registrations.add(registration);
return registration;
}

protected Map<String, CorsConfiguration> getCorsConfigurations() {
Map<String, CorsConfiguration> configs = new LinkedHashMap<>(this.registrations.size());
for (CorsRegistration registration : this.registrations) {
configs.put(registration.getPathPattern(), registration.getCorsConfiguration());
}
return configs;
}
}

可以看出CorsRegistry有个属性registrations ,按道理可以根据不同的项目路径进行定制访问行为,但是我们示例直接将pathPattern 设置为/**,也就是说已覆盖项目所有路径,只需要创建一个CorsRegistration就好。getCorsConfigurations(),这个方法是获取所有CorsConfiguration的Map集合,key值为传入路径pathPattern
回到示例代码CorsConfig中,registry对象addMapping()增加完传入路径pathPattern之后,return了一个CorsRegistration对象,是进行更多的配置,看一下CorsRegistration的代码,看看我们能配些什么?

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
public class CorsRegistration {
//传入的路径
private final String pathPattern;
//配置信息实体类
private final CorsConfiguration config;
//构造方法
public CorsRegistration(String pathPattern) {
this.pathPattern = pathPattern;
//原生注释看到了一个 @CrossOrigin 这个注解,待会看看是什么
// Same implicit default values as the @CrossOrigin annotation + allows simple methods
this.config = new CorsConfiguration().applyPermitDefaultValues();
}
//允许哪些源网站访问,默认所有
public CorsRegistration allowedOrigins(String... origins) {
this.config.setAllowedOrigins(Arrays.asList(origins));
return this;
}
//允许何种方式访问,默认简单方式,即:GET,HEAD,POST
public CorsRegistration allowedMethods(String... methods) {
this.config.setAllowedMethods(Arrays.asList(methods));
return this;
}
//设置访问header,默认所有
public CorsRegistration allowedHeaders(String... headers) {
this.config.setAllowedHeaders(Arrays.asList(headers));
return this;
}
//设置response headers,默认没有(什么都不设置)
public CorsRegistration exposedHeaders(String... headers) {
this.config.setExposedHeaders(Arrays.asList(headers));
return this;
}
//是否浏览器应该发送credentials,例如cookies Access-Control-Allow-Credentials
public CorsRegistration allowCredentials(boolean allowCredentials) {
this.config.setAllowCredentials(allowCredentials);
return this;
}
//设置等待时间,默认1800秒
public CorsRegistration maxAge(long maxAge) {
this.config.setMaxAge(maxAge);
return this;
}

protected String getPathPattern() {
return this.pathPattern;
}

protected CorsConfiguration getCorsConfiguration() {
return this.config;
}

}

局部配置

在Controller上加入@CrossOrigin注解

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
@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface CrossOrigin {

/** @deprecated as of Spring 5.0, in favor of {@link CorsConfiguration#applyPermitDefaultValues} */
@Deprecated
String[] DEFAULT_ORIGINS = { "*" };

/** @deprecated as of Spring 5.0, in favor of {@link CorsConfiguration#applyPermitDefaultValues} */
@Deprecated
String[] DEFAULT_ALLOWED_HEADERS = { "*" };

/** @deprecated as of Spring 5.0, in favor of {@link CorsConfiguration#applyPermitDefaultValues} */
@Deprecated
boolean DEFAULT_ALLOW_CREDENTIALS = false;

/** @deprecated as of Spring 5.0, in favor of {@link CorsConfiguration#applyPermitDefaultValues} */
@Deprecated
long DEFAULT_MAX_AGE = 1800

/**
* Alias for {@link #origins}.
*/
@AliasFor("origins")
String[] value() default {};

@AliasFor("value")
String[] origins() default {};

String[] allowedHeaders() default {};

String[] exposedHeaders() default {};

RequestMethod[] methods() default {};

String allowCredentials() default "";

long maxAge() default -1;
}

这个注解可以作用于方法或者类上,实现局部跨域,你会发现除了设置路径(因为没必要了,都定位到局部了)其他的参数与全局类似。

C++-priority_queue

C+±priority_queue

C++ STL-priority_queue 用法

priority_queue存在于头文件<queue>中,以下代码解释了该容器如何使用

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
107
108
109
110
111
112
113
114
115
116
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <iostream>
#include <algorithm>
#include <queue>
#include <vector>

using namespace std;

struct Node{
int a,b;
Node(){}
Node(int aa,int bb):a(aa),b(bb){}

// 二级排序,a相同时按照b升序排序,否则按照a升序排序
// (a,b): (1,1)<(1,2)<(2,1)<(2,2)
// 这里重载的大于号和小于号在逻辑上等价
bool operator < (const Node &r) const{
if(a==r.a)
return b<r.b;
else
return a<r.a;
}
bool operator > (const Node &r) const{
if(a==r.a)
return b>r.b;
else
return a>r.a;
}
};

int main(){

//priority_queue 是基于堆实现的优先队列

// .size()返回当前优先队列的元素个数
// empty()为true表示当前队列为空,false表示不空
priority_queue<int> tmp_q;
cout<<tmp_q.size()<<endl; //0
cout<<tmp_q.empty()<<endl; //1
tmp_q.push(1);
cout<<tmp_q.size()<<endl; //1
cout<<tmp_q.empty()<<endl; //0
tmp_q.pop();
cout<<tmp_q.size()<<endl; //0
cout<<tmp_q.empty()<<endl; //1

// priority_queue<int> 表示队列中存储的元素是int类型(STL模板类)
// 默认priority_queue表示队列按照less出队
// 后出队的元素比先出队的元素要小,出队的元素越来越小,所以是less
// push 加入元素
// top 返回队首元素
// pop 队首元素出队
priority_queue<int> q;
q.push(5);
q.push(6);
cout<<q.top()<<endl; //6
q.pop();
cout<<q.top()<<endl; //5
q.pop();

// greater 表示后出队的元素比先出队的元素要大, 出队的元素越来越大,所以是greater
priority_queue<int, vector<int>, greater<int>> q_greater;
// <>中第二个参数vector<int>表示优先队列中使用vector<int> 存储元素,可以更换成deque
// 第二个参数中更换的其他类型应支持.begin(),.end(),.push(),.pop()等操作
// 绝大多数情况都使用vector<>,极少需要更换
// 例子:priority_queue<int, deque<int>, greater<int>> q_greater;
q_greater.push(5);
q_greater.push(6);
cout<<q_greater.top()<<endl; //5
q_greater.pop();
cout<<q_greater.top()<<endl; //6
q_greater.pop();

// 更换为double类型,出队法则是less
priority_queue<double, vector<double>, less<double>> q_less;
q_less.push(5.2);
q_less.push(6.6);
cout<<q_less.top()<<endl; //6.6
q_less.pop();
cout<<q_less.top()<<endl; //5.2
q_less.pop();

// 自定义类Node的priority_queue 使用greater需要对元素重载大于号
priority_queue<Node, vector<Node>, greater<Node>> q_greater_node;
q_greater_node.push(Node(1,1));
q_greater_node.push(Node(1,2));
q_greater_node.push(Node(2,1));
q_greater_node.push(Node(2,2));
cout<<q_greater_node.top().a<<" "<<q_greater_node.top().b<<endl; // 1 1
q_greater_node.pop();
cout<<q_greater_node.top().a<<" "<<q_greater_node.top().b<<endl; // 1 2
q_greater_node.pop();
cout<<q_greater_node.top().a<<" "<<q_greater_node.top().b<<endl; // 2 1
q_greater_node.pop();
cout<<q_greater_node.top().a<<" "<<q_greater_node.top().b<<endl; // 2 2
q_greater_node.pop();

// 自定义类Node的priority_queue 使用less需要对元素重载小于号
priority_queue<Node, vector<Node>, less<Node>> q_less_node;
q_less_node.push(Node(1,1));
q_less_node.push(Node(1,2));
q_less_node.push(Node(2,1));
q_less_node.push(Node(2,2));
cout<<q_less_node.top().a<<" "<<q_less_node.top().b<<endl; // 2 2
q_less_node.pop();
cout<<q_less_node.top().a<<" "<<q_less_node.top().b<<endl; // 2 1
q_less_node.pop();
cout<<q_less_node.top().a<<" "<<q_less_node.top().b<<endl; // 1 2
q_less_node.pop();
cout<<q_less_node.top().a<<" "<<q_less_node.top().b<<endl; // 1 1
q_less_node.pop();

return 0;
}

SpringBoot-Mybatis

Spring Boot-Mybatis

1. ORM框架选型

对比项 SPRING DATA JPA MYBATIS
单表操作方式 只需继承,代码量较少,非常方便。而且支持方法名用关键字生成SQL 可以使用代码生成工具,也很方便,但相对JPA单表弱很多。JPA单表操作非常简单
多表关联查询 友好,动态SQL使用不够方便,而且SQL和代码耦合到一起 非常友好,可以有非常直观的动态SQL
自定义SQL SQL写在注解里面,写动态SQL有些费劲 SQL可以写在XML里,独立管理,动态SQL语法也容易书写理解
学习成本 略高 较低,会写SQL就可以

JPA是规范,Hibernate是实现

  • Spring Data JPA 对开发人员更加友好,单表操作非常方便,多表关联也不麻烦
  • mybatis各方面都很优秀,使用范围更广
  • 大型项目建议mybatis

2. 整合MyBatis操作数据库

  • pom.xml
1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.1</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
  • mybatis-config.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<!-- 配置全局属性-->
<settings>
<!-- 使用Jdbc的getGeneratedKeys获取数据库自增主键值 -->
<setting name="useGeneratedKeys" value="true"/>
<!-- 使用列表签替换列别名 -->
<setting name="useColumnLabel" value="true"/>
<!-- 开启驼峰命名转换: Table{create_time} -> Entity{createTime} -->
<setting name="mapUnderscoreToCamelCase" value="true"/>
</settings>
</configuration>
  • 同样的内容也可以写在Spring Boot配置文件application.yml中
1
2
3
4
5
6
7
8
9
10
11
12
13
14
spring:
datasource:
username: root
password: Thwf1858
url: jdbc:mysql://localhost:3306/mybatis
driver-class-name: com.mysql.jdbc.Driver
logging:
level:
com.haven.mybatis.mapper: debug
mybatis:
configuration:
map-underscore-to-camel-case: true
use-generated-keys: true
use-column-label: true
  • 注解书写SQL
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
package com.hehe.mapper;
@Mapper
public interface UserMapper {
/**
* 方式1:使用注解编写SQL。
*/
@Select("select * from t_user")
List<User> list();

/**
* 方式2:使用注解指定某个工具类的方法来动态编写SQL.
*/
@SelectProvider(type = UserSqlProvider.class, method = "listByUsername")
List<User> listByUsername(String username);

/**
* 延伸:上述两种方式都可以附加@Results注解来指定结果集的映射关系.
*
* PS:如果符合下划线转驼峰的匹配项可以直接省略不写。
*/
@Results({
@Result(property = "userId", column = "USER_ID"),
@Result(property = "username", column = "USERNAME"),
@Result(property = "password", column = "PASSWORD"),
@Result(property = "mobileNum", column = "PHONE_NUM")
})
@Select("select * from t_user")
List<User> listSample();

/**
* 延伸:无论什么方式,如果涉及多个参数,则必须加上@Param注解,否则无法使用EL表达式获取参数。
*/
@Select("select * from t_user where username like #{username} and password like #{password}")
User get(@Param("username") String username, @Param("password") String password);

@SelectProvider(type = UserSqlProvider.class, method = "getBadUser")
User getBadUser(@Param("username") String username, @Param("password") String password);

}
  • 可以传入参数

    • JavaBean
    • Map
    • 多个参数,需要用@Param注解
  • @Results注解

    1
    2
    3
    4
    5
    6
    7
    8
    9
    @Select("select t_id, t_age, t_name  "
    + "from sys_user "
    + "where t_id = #{id} ")
    @Results(id="userResults", value={
    @Result(property="id", column="t_id"),
    @Result(property="age", column="t_age"),
    @Result(property="name", column="t_name"),
    })
       User selectUserById(@Param("id") String id);

    @Results可以给出一个id,其他方法根据该id可以通过@ResultMap重复使用:

    1
    2
    3
    4
    5
    @Select("select t_id, t_age, t_name  "
    + "from sys_user "
    + "where t_name = #{name} ")
    @ResultMap("userResults")
       User selectUserByName(@Param("name") String name);

Spring Boot-JDBC

Spring Boot & JDBC

1. Spring Boot整合JDBC操作数据库

JDBC操作数据库流程

  1. 加载数据库驱动
  2. 建立数据库连接
  3. 创建数据库操作对象
  4. 定义操作的SQL语句
  5. 执行数据库操作
  6. 获取并操作结果集
  7. 关闭对象,回收资源

不建议使用JDBC

将Spring JDBC整合到Spring Boot

  1. pom.xml引入依赖
1
2
3
4
5
6
7
8
9
10
11
12
13
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-jdbc -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
<version>2.1.8.RELEASE</version>
</dependency>

<!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.16</version>
</dependency>
  1. 修改application.yml
1
2
3
4
5
6
spring:
datasource:
url: jdbc:mysql://localhost:3306/jdbc
username: root
password: Thwf1858
driver-class-name: com.mysql.jdbc.Driver
  1. DAO层代码
  • jdbcTemplate.update适用于insert, update和delete操作

  • jdbcTemplate.queryForObject用于查询单条记录并返回结果

  • jdbcTemplate.query用于查询结果列表

  • BeanPropertyRowMapper可以将数据库字段的值向数据库映射,满足驼峰标识也可以自动映射

    e.x. 数据库create_time映射到createTime属性

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
package com.haven.dao;

import com.fasterxml.jackson.databind.BeanProperty;
import com.haven.model.Article;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;

import javax.annotation.Resource;
import java.util.List;

/**
* @author HavenTong
* @date 2019/10/30 10:39 下午
*/
@Repository // @Repository标注持久层
public class ArticleJDBCDAO {

@Resource
private JdbcTemplate jdbcTemplate;

// 保存文章
public void save(Article article){
jdbcTemplate.update("INSERT INTO article(author, title, content, create_time) values (?,?,?,?)",
article.getAuthor(),
article.getTitle(),
article.getContent(),
article.getCreateTime());
}

// 删除文章
// 传参可以用 new Object[]{}传,也可以一个一个设置
public void deleteById(int id){
jdbcTemplate.update("DELETE FROM article WHERE id=?", new Object[]{id});
}

// 更新文章
public void updateById(Article article){
jdbcTemplate.update("UPDATE article SET author=?, title=?, content=?, create_time=? WHERE id=?",
article.getAuthor(),
article.getTitle(),
article.getContent(),
article.getCreateTime(),
article.getId());
}

// 根据id查找文章
public Article findById(int id){
return (Article)jdbcTemplate.queryForObject("SELECT * FROM article WHERE id = ?",
new Object[]{id},
new BeanPropertyRowMapper(Article.class));
}

// 查询所有
public List<Article> findAll(){
return (List<Article>) jdbcTemplate.query("SELECT * FROM article", new BeanPropertyRowMapper(Article.class));
}

}

2. JDBC多数据源

(1) application.yml配置两个数据源,第一个叫primary, 第二个叫secondar,也可以自己取名:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
server:
port: 8080
spring:
jackson:
date-format: yyyy-MM-dd HH:mm:ss
time-zone: GMT+8
datasource:
primary:
jdbc-url: jdbc:mysql://localhost:3306/jdbc
username: root
password: Thwf1858
driver-class-name: com.mysql.jdbc.Driver
secondary:
jdbc-url: jdbc:mysql://localhost:3306/test
username: root
password: Thwf1858
driver-class-name: com.mysql.jdbc.Driver

(2) 通过Java Config将数据源注入到Spring上下文

primaryJdbcTemplate使用primaryDataSource数据源操作数据库jdbc

secondaryJdbcTemplate使用secondaryDataSource数据源操作数据库test

DataSourceConfig.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
package com.haven.config;

import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.jdbc.core.JdbcTemplate;

import javax.sql.DataSource;
import javax.xml.crypto.Data;

/**
* @author HavenTong
* @date 2019/10/31 12:08 上午
*/
@Configuration
public class DataSourceConfig {
@Primary
@Bean(name = "primaryDataSource")
@Qualifier("primaryDataSource")
@ConfigurationProperties(prefix = "spring.datasource.primary")
public DataSource primaryDataSource(){
return DataSourceBuilder.create().build();
}

@Bean(name = "secondaryDataSource")
@Qualifier("secondaryDataSource")
@ConfigurationProperties(prefix = "spring.datasource.secondary")
public DataSource secondaryDataSource(){
return DataSourceBuilder.create().build();
}

@Bean(name = "primaryJdbcTemplate")
public JdbcTemplate primaryJdbcTemplate(
@Qualifier("primaryDataSource") DataSource dataSource ){
return new JdbcTemplate(dataSource);
}

@Bean(name = "secondaryJdbcTemplate")
public JdbcTemplate secondaryTemplate(
@Qualifier("secondaryDataSource") DataSource dataSource){
return new JdbcTemplate(dataSource);
}
}

(3) 之后修改dao层代码:

ArticleJDBCDAO.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
package com.haven.dao;

import com.fasterxml.jackson.databind.BeanProperty;
import com.haven.model.Article;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;

import javax.annotation.Resource;
import java.util.List;

/**
* @author HavenTong
* @date 2019/10/30 10:39 下午
*/
@Repository
public class ArticleJDBCDAO {

// 保存文章
public void save(Article article, JdbcTemplate jdbcTemplate){
jdbcTemplate.update("INSERT INTO article(author, title, content, create_time) values (?,?,?,?)",
article.getAuthor(),
article.getTitle(),
article.getContent(),
article.getCreateTime());
}

// 删除文章
public void deleteById(int id, JdbcTemplate jdbcTemplate){

jdbcTemplate.update("DELETE FROM article WHERE id=?", new Object[]{id});
}

// 更新文章
public void updateById(Article article, JdbcTemplate jdbcTemplate){
jdbcTemplate.update("UPDATE article SET author=?, title=?, content=?, create_time=? WHERE id=?",
article.getAuthor(),
article.getTitle(),
article.getContent(),
article.getCreateTime(),
article.getId());
}

// 根据id查找文章
public Article findById(int id, JdbcTemplate jdbcTemplate){
return (Article)jdbcTemplate.queryForObject("SELECT * FROM article WHERE id = ?",
new Object[]{id},
new BeanPropertyRowMapper(Article.class));
}

// 查询所有
public List<Article> findAll(JdbcTemplate jdbcTemplate){
return (List<Article>) jdbcTemplate.query("SELECT * FROM article", new BeanPropertyRowMapper(Article.class));
}

}

(4) 修改service层代码

ArticleRestJDBCServiceImpl.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
package com.haven.service;

import com.haven.dao.ArticleJDBCDAO;
import com.haven.model.Article;
import lombok.extern.slf4j.Slf4j;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;
import java.util.List;

/**
* @author HavenTong
* @date 2019/10/30 10:56 下午
*/
@Slf4j
@Service
public class ArticleRestJDBCServiceImpl implements ArticleRestService {

@Resource
ArticleJDBCDAO articleJDBCDAO;

@Resource
JdbcTemplate primaryJdbcTemplate;

@Resource
JdbcTemplate secondaryJdbcTemplate;

@Transactional
@Override
public Article saveArticle(Article article) {
articleJDBCDAO.save(article, primaryJdbcTemplate);
articleJDBCDAO.save(article, secondaryJdbcTemplate);
return article;

}

@Override
public void deleteArticle(int id) {
articleJDBCDAO.deleteById(id, primaryJdbcTemplate);
articleJDBCDAO.deleteById(id, secondaryJdbcTemplate);
}

@Override
public void updateArticle(Article article) {
articleJDBCDAO.updateById(article, primaryJdbcTemplate);
}

@Override
public Article getArticle(int id) {
return articleJDBCDAO.findById(id, primaryJdbcTemplate);
}

@Override
public List<Article> getAll() {
return articleJDBCDAO.findAll(primaryJdbcTemplate);
}
}

3. Spring JDBC JTA实现分布式事务

@Transactional无法跨库完成分布式事务

通过整合JTA实现分布式事务

  • 引入maven依赖
1
2
3
4
5
6
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-jta-atomikos -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jta-atomikos</artifactId>
<version>2.2.0.RELEASE</version>
</dependency>
  • 修改application.yml配置文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
primarydb:
uniqueResourceName: primary
xaDataSourceClassName: com.mysql.jdbc.jdbc2.optional.MysqlXADataSource
xaProperties:
url: jdbc:mysql://localhost:3306/jdbc
user: root
password: Thwf1858
exclusiveConnectionMode: true
minPoolSize: 3
maxPoolSize: 10
testQuery: SELECT 1 FROM dual
secondarydb:
uniqueResourceName: secondary
xaDataSourceClassName: com.mysql.jdbc.jdbc2.optional.MysqlXADataSource
xaProperties:
url: jdbc:mysql://localhost:3306/test
user: root
password: Thwf1858
exclusiveConnectionMode: true
minPoolSize: 3
maxPoolSize: 10
testQuery: SELECT 1 FROM dual
  • 编写配置类DataSourceConfig.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
package com.haven.config;

import com.atomikos.jdbc.AtomikosDataSourceBean;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.jdbc.core.JdbcTemplate;

import javax.sql.DataSource;
import javax.xml.crypto.Data;

/**
* @author HavenTong
* @date 2019/10/31 12:08 上午
*/
@Configuration
public class DataSourceConfig {
// 多数据源,分布式
@Bean(initMethod = "init", destroyMethod = "close", name = "primaryDataSource")
@Primary
@ConfigurationProperties(prefix = "primarydb")
public DataSource primaryDataSource(){
return new AtomikosDataSourceBean();
}

@Bean(initMethod = "init", destroyMethod = "close", name = "secondaryDataSource")
@ConfigurationProperties(prefix = "secondarydb")
public DataSource secondaryDataSource(){
return new AtomikosDataSourceBean();
}

@Bean
public JdbcTemplate primaryJdbcTemplate(@Qualifier("primaryDataSource")
DataSource primaryDataSource){
return new JdbcTemplate(primaryDataSource);
}

@Bean
public JdbcTemplate secondaryJdbcTemplate(@Qualifier("secondaryDataSource")
DataSource secondaryDataSource){
return new JdbcTemplate(secondaryDataSource);
}

}
  • 配置事务管理器™ TransactionManagerConfig.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
package com.haven.config;

import com.atomikos.icatch.jta.UserTransactionImp;
import com.atomikos.icatch.jta.UserTransactionManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.jta.JtaTransactionManager;

import javax.transaction.SystemException;
import javax.transaction.TransactionManager;
import javax.transaction.UserTransaction;

/**
* @author HavenTong
* @date 2019/10/31 3:57 下午
*/
@Configuration
public class TransactionManagerConfig {

@Bean
public UserTransaction userTransaction() throws SystemException {
UserTransactionImp userTransactionImp = new UserTransactionImp();
userTransactionImp.setTransactionTimeout(10000);
return userTransactionImp;
}

@Bean(name = "atomikosTransactionManager", initMethod = "init", destroyMethod = "close")
public TransactionManager atomikosTransactionManager() throws Throwable{
UserTransactionManager userTransactionManager = new UserTransactionManager();
userTransactionManager.setForceShutdown(false);
return userTransactionManager;
}

@Bean(name = "transactionManager")
@DependsOn({"userTransaction", "atomikosTransactionManger"})
public PlatformTransactionManager transactionManager() throws Throwable{
UserTransaction userTransaction = userTransaction();
JtaTransactionManager manager = new JtaTransactionManager(userTransaction, atomikosTransactionManager());
return manager;
}
}

Spring Boot Bean自动装配

Spring Boot Bean自动装配

1. 全局配置文件

修改Spring Boot自动配置的默认值,Spring Boot在底层自动加载

  • application.yml

  • application.properties

2. Bean自动装配原理

(1) Spring Boot启动时加载主配置类,开启了自动配置功能@EnableAutoConfiguration

(2) @EnableAutoConfiguration

​ 作用: 将类路径下META-INF/spring.factories里面配置的所有EnableAutoConfiguration的值(自动装配类)加入到执行计划中

(3) 每一个自动装配类进行自动配置功能

3. YAML规则

(1) 双引号

​ 不会转义特殊字符,特殊字符或作为本身想要表达的意思

name: "zhangsan \n lisi" 输出 zhangsan 换行 lisi

单引号

​ 会转义特殊字符,特殊字符最终只是一个普通的字符串数据,如:

name: 'zhangsan \n lisi' 输出 zhangsan \n lisi

(2) 支持松散的结构

family-name = familyName = family_name

(3) 占位符

1
2
3
4
5
6
${random.value}
${random.int}
${random.long}
${random.int(10)}
${random.int[1024, 65536]}
${xxxx.yyyy: 默认值}

4. 获取自定义配置

(1) @Value("${}")

​ 实现了单个属性的注入

(2) 对于复杂的数据结构,使用@ConfigurationProperties获取配置值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.haven.model.yaml;

import lombok.Data;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

/**
* @author HavenTong
* @date 2019/10/29 5:44 下午
*/

@Data
@Component
@ConfigurationProperties(prefix = "family")
public class Family {
// @Value("${family.family-name}")
private String familyName;
private Father father;
private Mother mother;
private Child child;
}

(3) 两种注解区别

@ConfigurationProperties @Value
功能 批量注入属性 一个个指定
松散语法绑定 支持 不支持
SpEL 不支持 支持
复杂数据类型嵌套 支持 不支持
JSR303数据校验 支持 不支持

5. 配置文件注入值数据校验

(1) 在需要校验的属性装配类上加@Validated注解

(2) 数据校验注解列表

(3) 若验证失败,会出现BindValidationException异常

6. Profile不同环境下不同配置

优先级: 外部大于内部,特指大于泛指

  • application.yml 全局配置文件
  • application-dev.yml 开发环境配置文件
  • application-test.yml 测试环境配置文件
  • Application-prod.yml 生产环境配置文件

(1) 配置application.yml

设置spring.profiles.active指定使用哪一个配置文件。

优先级以下面的dev/test/prod yml为优先

1
2
3
spring:
profiles:
active: dev

(2) 通过命令行启动

1
java -jar SpringBoot01HelloWorld-1.0-SNAPSHOT.jar --spring.profiles.active=dev

(3) IDEA设置Program Arguments

​ (a) Edit Configuration --> Environment --> Program Argument

1
--spring.profiles.active=dev

​ (b) Edit Configuration --> Environment --> VM options

1
-Dspring.profiles.active=dev

7. 项目内部配置文件加载位置

spring boot会扫描以下位置的application.properties / application.yml文件作为spring boot的默认配置文件

1
2
3
4
-file:./config/
-file:./
-classpath:/config/
-classpath:./

以上优先级从高到低,所有位置的文件都会被加载;高优先级会覆盖低优先级

8. 配置文件敏感字段加密

(1) 与spring boot整合

1
2
3
4
5
6
<!-- https://mvnrepository.com/artifact/com.github.ulisesbocchio/jasypt-spring-boot-starter -->
<dependency>
<groupId>com.github.ulisesbocchio</groupId>
<artifactId>jasypt-spring-boot-starter</artifactId>
<version>1.18</version>
</dependency>

(2) 需要加密的地方,使用ENC()进行包裹处理

(3) 在配置文件中设置密钥

SpringBoot RESTful接口

SpringBoot RESTful接口

1.常用注解开发RESTful接口

  • @RestController

    1. 将注解的类注入到Spring的环境
    2. 相当于@Controller + @ResponseBody
    3. JSON格式的数据响应
  • @RequestMapping

    1. 类上的注解表示注解的Controller类的路径
  • @PathVariable

    1. 路径上的变量
  • @PathVariable & RequestParam

    1. @PathVariable用于接收URL上的{参数}
    2. @RequestParam用于接收普通方式提供的参数
  • @RequestBody & @RequestParam

    1. JSON数据建议用@RequestBody,会分配实体类中的所有属性

    2. @RequestParam适合接收单个的参数

    3. @RequestBody可以接收嵌套的数据结构

2.JSON数据处理和Postman测试

Spring Boot默认使用Jackson

(1) 常用注解

  • @JsonIgnore:加在属性上表示在序列化和反序列化的过程中将它忽略

  • @JsonProperty:为属性起别名

  • @JsonPropertyOrder:加在类上

  • @JsonInclude(JsonInclude.Include.NON_NULL): 当属性不为空的时候,进行序列化;否则不进行

  • @JsonFormat(pattern = "", timezone = ""):配置时间格式

    1
    2
    3
    4
    spring:
    jackson:
    date-format: yyyy-MM-dd HH:mm:ss
    time-zone: GMT+8

(2) 序列化与反序列化

把对象转成可传输、可存储的格式(json, xml, 二进制,甚至自定义的格式)叫序列化,反序列化为逆过程

3. Postman使用

4. 使用Swagger 2发布API文档

  • 代码变,文档变,只需要少量的注解,使用Swagger就可以根据代码自动生成API文档,很好地保持了文档的及时性
  • 跨语言性,支持40多种语言
  • Swagge UI呈现出一份可交互式的API文档,我们可以直接在文档页面尝试API的调用,省去了准备复杂的

调用参数的过程

  • 还可以将文档规范导入相关的工具(e.x. SoapUI),这些工具会为我们创建自动化的测试

整合Swagger 2

pom.xml

1
2
3
4
5
6
7
8
9
10
11
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.6.1</version>
</dependency>

<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.6.1</version>
</dependency>
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
// config/Swagger2.java
package com.haven.config;

import org.springframework.beans.factory.annotation.Configurable;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.w3c.dom.DocumentType;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;

/**
* @author HavenTong
* @date 2019/10/29 3:26 下午
*/
@Configuration
@EnableSwagger2
public class Swagger2 {

@Bean
public Docket createRestApi(){
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.select()
.apis(RequestHandlerSelectors.basePackage("com.haven"))
.paths(PathSelectors.regex("/rest/.*"))
.build();
}

private ApiInfo apiInfo(){
return new ApiInfoBuilder()
.title("springboot利用swagger构建api文档")
.description("简单优雅的restfun风格")
.termsOfServiceUrl("http://www.zimug.com")
.version("1.0")
.build();
}
}

之后启动项目,通过http://localhost:8080/swagger-ui.html即可访问swagger-ui

可以在方法上添加更详细的注解:

1
2
3
4
5
6
7
8
9
10
11
12
13
    @ApiOperation(value = "添加文章", notes = "添加新的文章", tags = "Article", httpMethod = "POST")
// @ApiImplicitParams({
// @ApiImplicitParam(name = "title", value = "文章标题", required = true, dataType = "String"),
// @ApiImplicitParam(name = "content", value = "文章内容", required = true, dataType = "String"),
// @ApiImplicitParam(name = "author", value = "文章作者", required = true, dataType = "String")
// })
@ApiResponses({
@ApiResponse(code = 200, message = "成功", response = AjaxResponse.class),
@ApiResponse(code = 400, message = "用户输入错误", response = AjaxResponse.class),
@ApiResponse(code = 500, message = "系统内部错误", response = AjaxResponse.class)
})
@RequestMapping(value = "/article", method = RequestMethod.POST, produces = "application/json")
public AjaxResponse saveArticle(@RequestBody Article article){
  • 由于采用@RequestBody去接收参数,这里就不需要使用@ApiImplicitParam注解,@ApiImplicitParam注解与@RequestParam注解是一一对应的。
  • 建议有Swagger 2的情况下,减少与此对应的代码注释或不写

Swagger 2常用注解

  • @Api:用在请求的类上,表示对类的说明

    tags=“说明该类的作用,可以在UI界面上看到的注解”

    value=“该参数没有什么意义,在UI界面上也能看到,所以不需要配置”

  • @ApiOperation: 用在请求的方法上,说明方法的用途、作用

    value=“说明方法的用途,作用”

    notes=“方法的备注说明”

  • @ApiImplicitParams: 用在请求的方法上,表示一组参数说明

    @ApiImplicitParam: 用在@ApiImplicitParams注解中,指定一个请求参数的各个方面

    name=“参数名”

    value=“参数的汉字说明,解释”

    required=“参数是否必须要传”

    paramType=“参数放在哪个地方”

    ​ - header --> 请求参数的获取:@RequestHeader

    ​ - query --> 请求参数的获取:@RequestParam

    ​ - path(用于restful接口) --> 请求参数的获取: @PathVariable

    ​ - body,form不常用

    dataType= “参数类型”,默认String, 其他值dataType="Integer"

    defaultValue= “参数的默认值”

  • @ApiResponses: 用在请求的方法上,表示一组响应

    @ApiResponse: 用在@ApiResponses中,一般用于表达一个错误的响应信息

    code= 数字,e.x. 400

    message= 信息,例如"请求参数没填好"

    response= 抛出异常的类

  • @ApiModel: 用于响应类上,表示一个返回响应数据的信息。(这种一般用在post创建的时候, 使用@RequestBody的场景,请求参数无法使用@ApiImplicitParam注解进行描述的时候)

    @ApiModelProperty: 用在属性上,描述响应类的属性