210509-spring boot启动类启动 错误: 找不到或无法加载主类 xxx.xxxx.Application 的解决方法

SpringBoot项目启动,忽然提示找不到或者无法加载主类,记录两种常用的方式

方法一:清空idea缓存

idea清理缓存:

  • file -> Invalidate Cache & Restart IDEA

方法二:maven重新install

执行命令

1
mvn clean install -DskipTests=true

190401-SpringBoot远程debug设置

记录下SpringBoot jar启动方式,开启远程debug的命令

1
java -server -Xms512m -Xmx512m -XX:AutoBoxCacheMax=20480 -Xdebug -Xrunjdwp:server=y,transport=dt_socket,address=9999,suspend=n -jar web.jar

181112-springboot应用下线配置

本篇记录SpringBoot 2.x借助Actuator实现优雅的下线的配置方式

181011-SpringCloud之DiscoverClient无法获取Service记录

最终解决问题之后,才发现是自己走进盲角了,花了不少时间,特此记录

I. 问题说明

在测试使用SpringCloud全家桶时,服务向注册中心注册,然后client就可以相互之间实现RPC调用(其实还是http访问)

如果我想看看当前注册中心获取了哪些服务,可以怎么办?

一个简单的方法就是借助DiscoverClient来做,然后问题就来了

1
2
3
4
public Application(DiscoveryClient discoveryClient) {
List<String> list = discoveryClient.getServices();
System.out.println(list);
}

上面的代码执行之后,发现返回是空数组;蛋疼的是换另外一个工程,同样的方式,却可以拿到注册的服务名

查了半天,最好才发现拿不到是因为在配置中添加了

1
eureka.client.fetchRegistry=false

上面就表示不会去主动获取注册的服务,因此也就拿不到服务了;改成true之后就ok了

II. 其他

1. 一灰灰Bloghttps://liuyueyi.github.io/hexblog

一灰灰的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛

2. 声明

尽信书则不如,已上内容,纯属一家之言,因个人能力有限,难免有疏漏和错误之处,如发现bug或者有更好的建议,欢迎批评指正,不吝感激

3. 扫描关注

一灰灰blog

QrCode

知识星球

goals

180905-Spring返回Json对象

Spring之返回Json串

使用Spring搭建的web后端服务,与前端通过Json串进行交互,特此记录下使用姿势

1. 直接返回JsonString

这个可以说是最简单和最常见的一种使用姿势了,直接返回String,如下

1
2
3
4
5
@ResponseBody
@RequestMapping(value = "/body")
public String body(@RequestBody Req req) {
return JSON.toJSONString(req);
}

2. 使用JsonInclude注解

直接返回的是一个对象,然后交给框架来将对象转换为String丢给前端

1
2
3
4
5
@RequestMapping(path = {"/index", "/"})
public ResponseWrapper<List<PoetryDTO>> index() {
List<PoetryDTO> ans = poetryReadService.getIndex();
return ResponseWrapper.successReturn(ans);
}

直接用上面的方式时,可能会抛出,提示没有对应的HttpMessageConverter来转换ResponseWrapper对象

一个简单的使用姿势就是直接使用注解 com.fasterxml.jackson.annotation.JsonInclude

如下即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
JsonInclude
@Data
@NoArgsConstructor
public class ResponseWrapper<T> {
private int code;
private String msg;
private T data;

public ResponseWrapper(int code, String msg, T data) {
this.code = code;
this.msg = msg;
this.data = data;
}

public static <T> ResponseWrapper<T> buildSuccess(T data) {
return new ResponseWrapper<>(200, "success", data);
}

public static <T> ResponseWrapper<T> buildError(int code, String msg, T data) {
return new ResponseWrapper<>(code, msg, data);
}
}

因此关键就是HttpMessageConverter在起作用了,下一篇重点关注下这个是什么东西,干嘛用,以及如何自己实现一个特定需求的转换器

II. 其他

1. 一灰灰Bloghttps://liuyueyi.github.io/hexblog

一灰灰的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛

2. 声明

尽信书则不如,已上内容,纯属一家之言,因个人能力有限,难免有疏漏和错误之处,如发现bug或者有更好的建议,欢迎批评指正,不吝感激

3. 扫描关注

一灰灰blog

QrCode

知识星球

goals

180830-SpringBoot之获取application.yml配置参数

SpringBoot之获取Application.yml配置参数

需要获取配置文件中的配置参数的场景挺多的,常见的一种方式就是直接从Enironment对象中获取,或者使用 @Value 注解的方式注入,但是这都有一个前提,需要确切的知道配置的name

如果某些场景下,我需要遍历配置参数可以怎么办?

180815-Spring之RestTemplate使用小结二:中级使用篇

Spring之RestTemplate中级使用篇

前面一篇介绍了如何使用RestTemplate发起post和get请求,然而也只能满足一些基本的场景,对于一些特殊的如需要设置请求头,添加认证信息等场景,却没有提及可以怎么做,这一篇则相当于进阶版,将主要介绍

  • get/post请求如何携带 header
  • post传文件可以怎么玩, post提交json串怎么处理
  • exchange方法的使用姿势

180813-Spring之RestTemplate使用小结一

Spring之RestTemplate初级使用篇

作为一个Java后端,需要通过HTTP请求其他的网络资源可以说是一个比较常见的case了;一般怎么做呢?

可能大部分的小伙伴直接捞起Apache的HttpClient开始做,或者用其他的一些知名的开源库如OkHttp, 当然原生的HttpURLConnection也是没问题的

本篇博文则主要关注点放在Spring的生态下,利用RestTemplate来发起Http请求的使用姿势

180804-Spring之动态注册bean

Spring之动态注册bean

什么场景下,需要主动向Spring容器注册bean呢?

如我之前做个的一个支持扫表的基础平台,使用者只需要添加基础配置 + Groovy任务,就可以丢到这个平台上面来运行了,而这个基础平台是一直都在运行的,所以在新来任务时,最直观需要注册的就是 DataSource 数据源这个bean了,那么可以怎么玩?

I. 主动注册Bean支持

借助BeanDefinition来实现bean的定义,从最终的使用来看,代码比较少,几行而已

1
2
3
4
5
6
7
8
9
10
11
12
13
public <T> T registerBean(String name, Class<T> clazz, Object... args) {
BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(clazz);
if (args.length > 0) {
for (Object arg : args) {
beanDefinitionBuilder.addConstructorArgValue(arg);
}
}
BeanDefinition beanDefinition = beanDefinitionBuilder.getRawBeanDefinition();

BeanDefinitionRegistry beanFactory = (BeanDefinitionRegistry) applicationContext.getBeanFactory();
beanFactory.registerBeanDefinition(name, beanDefinition);
return applicationContext.getBean(name, clazz);
}

测试如下

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
import com.github.hui.story.quickstory.server.VisitService;
import lombok.ToString;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.stereotype.Component;

@Component
public class BeanHolder {
private final ConfigurableApplicationContext applicationContext;

public BeanHolder(ConfigurableApplicationContext applicationContext) {
this.applicationContext = applicationContext;
initSer();
}

public void initSer() {
InrSer ser = registerBean("test", InrSer.class);
ser.name = "一灰";
ser.uid = 22;
System.out.println(ser);

InrSer ser2 = registerBean("test2", InrSer.class, "一灰灰Blog", 20);
System.out.println(ser2);
}

@ToString
public static class InrSer {
private String name;
private Integer uid;

@Autowired
private VisitService visitService;

public InrSer() {
}

public InrSer(String name, Integer uid) {
this.name = name;
this.uid = uid;
}
}

private <T> T registerBean(String name, Class<T> clazz, Object... args) {
BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(clazz);
if (args.length > 0) {
for (Object arg : args) {
beanDefinitionBuilder.addConstructorArgValue(arg);
}
}
BeanDefinition beanDefinition = beanDefinitionBuilder.getRawBeanDefinition();

BeanDefinitionRegistry beanFactory = (BeanDefinitionRegistry) applicationContext.getBeanFactory();
beanFactory.registerBeanDefinition(name, beanDefinition);
return applicationContext.getBean(name, clazz);
}
}

输出如下

show

II. 其他

1. 一灰灰Bloghttps://liuyueyi.github.io/hexblog

一灰灰的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛

2. 声明

尽信书则不如,已上内容,纯属一家之言,因个人能力有限,难免有疏漏和错误之处,如发现bug或者有更好的建议,欢迎批评指正,不吝感激

3. 扫描关注

一灰灰blog

QrCode

知识星球

goals

180803-Spring定时任务高级使用篇

前面一篇博文 《Spring之定时任务基本使用篇》 介绍了Spring环境下,定时任务的简单使用姿势,也留了一些问题,这一篇则希望能针对这些问题给个答案

I. 定时任务进阶篇

1. 问题小结

前面一篇博文,抛出了下面的几个问题,接下来则围绕问题进行分析

  • 一个项目中有多个定时任务时,他们是并行执行的还是串行执行的?
  • 如果默认是串行的
    • 那么有相同的crond表达式的定时任务之间,有先后顺序么?
    • 某个任务的阻塞是否会影响后面的任务?
    • 如果需要他们并行执行,可以怎么做?
  • 如果是并发执行的
    • 是新创建线程还是采用线程池来复用呢?
    • 在并发执行时,假设有个每秒执行一次的任务,但是它执行一次消耗的时间大于1s时,这个任务的表现时怎样的呢?不断地新增线程来执行还是等执行完毕之后再执行下一次的呢?

180801-Spring之定时任务基本使用篇

Spring之定时任务基本使用篇

spring-boot项目中,想添加一个定时任务,可以怎么办?

  • 不管什么项目,都是可以直接用JDK原生的定时任务来实现
  • 借助@Scheduled注解来使用

本篇博文则主要集中在在SpringBoot项目中,怎么使用定时任务

180730-Spring之RequestBody的使用姿势小结

logo

Spring之RequestBody的使用姿势小结

SpringMVC中处理请求参数有好几种不同的方式,如我们常见的下面几种

  • 根据 HttpServletRequest 对象获取
  • 根据 @PathVariable 注解获取url参数
  • 根据 @RequestParam 注解获取请求参数
  • 根据Bean的方式获取请求参数
  • 根据 @ModelAttribute 注解获取请求参数

对上面几种方式有兴趣的可以看一下这篇博文: SpringMVC之请求参数的获取方式

除了上面的几种方式之外,还有一种 @RequestBody 的使用方式,本文则主要介绍这种传参的使用姿势和相关注意事项

180713-Spring之借助Redis设计访问计数器之扩展篇

logo

之前写了一篇博文,简单的介绍了下如何利用Redis配合Spring搭建一个web的访问计数器,之前的内容比较初级,现在考虑对其进行扩展,新增访问者记录

  • 记录当前站点的总访问人数(根据Ip或则设备号)
  • 记录当前访问者在总访问人数中的排名
  • 记录每个子页面的访问计数,记录站点的总访问计数

180626-Spring之借助Redis设计一个简单访问计数器

logo

Spring之借助Redis设计一个简单访问计数器

为什么要做一个访问计数?之前的个人博客用得是卜算子做站点访问计数,用起来挺好,但出现较多次的响应很慢,再其次就是个人博客实在是访问太少,数据不好看😢…

前面一篇博文简单介绍了Spring中的RedisTemplate的配置与使用,那么这篇算是一个简单的应用case了,主要基于Redis的计数器来实现统计

180623-SpringBoot之logback配置文件

SpringBoot配置logback

项目的日志配置属于比较常见的case了,之前接触和使用的都是Spring结合xml的方式,引入几个依赖,然后写个 logback.xml 配置文件即可,那么在SpringBoot中可以怎么做?

180612-Spring之Yml配置文件加载问题

Yml配置文件加载问题

在resource目录下有一个application.yml文件,希望是通过@PropertySource注解,将配置文件数据读取到Environment中,然而调试发现数据始终读取不到,google之后,记录下解决方法

在测试用例中,指定初始化方式 @ContextConfiguration(classes = RedisConf.class, initializers = ConfigFileApplicationContextInitializer.class)

1
2
3
4
5
6
7
8
9
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = RedisConf.class, initializers = ConfigFileApplicationContextInitializer.class)
public class RedisTest {
@Test
public void testRedis() {
String ans = JedisClient.getStr("hello");
System.out.println(ans);
}
}

对应的配置类

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
@Configuration
@PropertySource(value = "classpath:application.yml")
public class RedisConf {

@Autowired
private Environment environment;

@Autowired
public RedisTemplate<String, String> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<String, String> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);

DefaultStrSerializer serializer = new DefaultStrSerializer();
redisTemplate.setValueSerializer(serializer);
redisTemplate.setHashValueSerializer(serializer);
redisTemplate.setKeySerializer(serializer);
redisTemplate.setHashKeySerializer(serializer);

redisTemplate.afterPropertiesSet();

JedisClient.register(redisTemplate);
return redisTemplate;
}

@Bean
public RedisConnectionFactory redisConnectionFactory() {
LettuceConnectionFactory fac = new LettuceConnectionFactory();
fac.getStandaloneConfiguration().setHostName(environment.getProperty("spring.redis.host"));
fac.getStandaloneConfiguration().setPort(Integer.parseInt(environment.getProperty("spring.redis.port")));
fac.getStandaloneConfiguration()
.setPassword(RedisPassword.of(environment.getProperty("spring.redis.password")));
fac.afterPropertiesSet();
return fac;
}
}

II. 其他

1. 一灰灰Bloghttps://liuyueyi.github.io/hexblog

一灰灰的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛

2. 声明

尽信书则不如,已上内容,纯属一家之言,因个人能力有限,难免有疏漏和错误之处,如发现bug或者有更好的建议,欢迎批评指正,不吝感激

3. 扫描关注

QrCode

180611-Spring之RedisTemplate配置与使用

Spring之RedisTemplate配置与使用

Spring针对Redis的使用,封装了一个比较强大的Template以方便使用;之前在Spring的生态圈中也使用过redis,但直接使用Jedis进行相应的交互操作,现在正好来看一下RedisTemplate是怎么实现的,以及使用起来是否更加便利

180609-Spring之事件驱动机制的简单使用

Spring之事件驱动机制的简单使用

关于事件的发起与相应,在客户端的交互中可算是非常频繁的事情了,关于事件的发布订阅,在Java生态中,EventBus可谓是非常有名了,而Spring也提供了事件机制,本文则主要介绍后端如何在Spring的环境中,使用事件机制

180531-Spring中JavaConfig知识小结

Sring中JavaConfig使用姿势

去掉xml的配置方式,改成用Java来配置,最常见的就是将xml中的 bean定义, scanner包扫描,属性文件的配置信息读取等

I. 几个基本注解

1. Configuration注解

在javaConfig中注解@Configuration用来代替一个xml文件,可以简单的理解他们的作用是相等的,一般bean的定义也都是放在被这个注解修饰的类中

如一个基本的配置文件如下

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
@Configuration
@ComponentScan("com.git.hui.rabbit.spring")
public class SpringConfig {
private Environment environment;

@Autowired
public void setEnvironment(Environment environment) {
this.environment = environment;
System.out.println("then env: " + environment);
}

@Bean(name="connectionFactory")
public ConnectionFactory connectionFactory() {
CachingConnectionFactory factory = new CachingConnectionFactory();
factory.setHost("127.0.0.1");
factory.setPort(5672);
factory.setUsername("admin");
factory.setPassword("admin");
factory.setVirtualHost("/");
return factory;
}

@Bean
public RabbitAdmin rabbitAdmin(ConnectionFactory connectionFactory) {
return new RabbitAdmin(connectionFactory);
}
}

2. Bean 注解

上面的例子中,在方法上添加了@Bean注解,这个就相当于传统的

1
<bean name="rabbitAdmin" class="org.springframework.amqp.rabbit.core.RabbitAdmin"/>

因此在需要引入rabbitAdmin实例的地方,可以如下使用

a. 属性字段上添加 @Autowired注解

1
2
3
4
public class RConsumer {
@Autowired
private RabbitAdmin rabbitAdmin;
}

b. 设置方法上添加 @Autowired注解

1
2
3
4
5
6
7
8
public class RConsumer {
private RabbitAdmin rabbitAdmin;

@Autowired
public void setRabbitAdmin(RabbitAdmin rabbitAdmin) {
this.rabbitAdmin = rabbitAdmin;
}
}

c. 使用构造器的方式

1
2
3
4
5
6
public class RConsumer {
private RabbitAdmin rabbitAdmin;
public RConsumer(RabbitAdmin rabbitAdmin) {
this.rabbitAdmin = rabbitAdmin;
}
}

上面就是Spring容器支持的几种典型的IoC方式

3. ComponentScan

这个类似于xml中的 <context:component-scan"/> 标签

1
2
3
@ComponentScan("com.git.hui.rabbit.spring")
public class SpringConfig {
}

上面的这个配置,表示自动扫描包 com.git.hui.rabbit.spring 下面的bean (要求类上添加了 @Component, @Repository, @Service)

那么一个问题来了,如果一个类既被自动扫描加载,又显示定义了bean,会怎样?

1
2
3
4
5
6
7
8
9
10
11
12
13
package com.git.hui.rabbit.spring;

import org.springframework.stereotype.Component;
import java.util.concurrent.atomic.AtomicInteger;

@Component
public class TestBean {
private static AtomicInteger count = new AtomicInteger(1);

public TestBean() {
System.out.println("testBean count: " + count.getAndAdd(1));
}
}

对应的JavaConfig

1
2
3
4
5
6
7
8
@Configuration
@ComponentScan("com.git.hui.rabbit.spring")
public class SpringConfig {
@Bean
public TestBean testBean() {
return new TestBean();
}
}

实际测试,发现这个bean只会有一个实例,即输出计数只会有一条,实际查看ApplicationContext中的内容,TestBean的实例,也确实只有一个,如果改成下面这种场景呢

1
2
3
4
@Bean(name="testBean2")
public TestBean testBean() {
return new TestBean();
}

会有两条记录输出,实际查看容器中的Bean对象,会有两个实例如下

image1

这和我们的预期也是一样的,因为一个类我可能需要多个不同的Bean实例来干一些事情

那么出现这种JavaConfig定义的beanName与自动扫描的冲突的情况会怎样呢?

新增一个NewBean对象,

1
2
3
4
5
6
7
public class NewBean {
private static AtomicInteger count = new AtomicInteger(1);

public NewBean() {
System.out.println(" newbean count: " + count.getAndAdd(1));
}
}

在JavaConfig中新加一个bean定义,但是BeanName与自动扫描的TestBean重复了

1
2
3
4
@Bean(name="testBean")
public NewBean newBean() {
return new NewBean();
}

此时发现有意思的事情了,从Spring容器中,将查不到TestBean的实例,但是可以查到NewBean的实例

image1

这个的表现是:

  • 当beanName出现冲突时,JavaConfig的优先级会高于自动加载的,导致自动加载的Bean不会被加载到容器内

那么跟着来的一个问题就是如果JavaConfig中定义了两个相同的BeanName的bean呢?

1
2
3
4
5
6
7
8
9
@Bean(name = "testBean2")
public NewBean newBean() {
return new NewBean();
}

@Bean(name = "testBean2")
public TestBean testBean() {
return new TestBean();
}

因为我们TestBean上加了@Component注解,因此容器中至少有一个,但是否会有testBean2这个实例呢? 通过实际查看是没有的,testBean2这个名被 NewBean 占领了

image1

so,表现上看,加上实测,将上面的定义换个位置,得出下面的结论

  • 当出现beanName重名时,先定义的Bean占优

然后就是最后一个问题了,当自动扫描时,两个类包不同,但是类名相同,会怎样?

1
2
3
4
5
6
7
8
9
10
11
12
13
package com.git.hui.rabbit.spring.demo;

import org.springframework.stereotype.Component;
import java.util.concurrent.atomic.AtomicInteger;

@Component
public class TestBean {
private static AtomicInteger count = new AtomicInteger(1);

public TestBean() {
System.out.println(" demo.TestBean count: " + count.getAndAdd(1));
}
}

实测,会抛出一个异常,在使用xml的配置方式时,经常见到的一个BeanName冲突的异常

1
org.springframework.context.annotation.ConflictingBeanDefinitionException: Annotation-specified bean name 'testBean' for bean class [com.git.hui.rabbit.spring.demo.TestBean] conflicts with existing, non-compatible bean definition of same name and class [com.git.hui.rabbit.spring.TestBean]

小结:

  • JavaConfig 定义的BeanName与自动扫描的BeanName冲突时,JavaConfig的定义的会被实例化
  • JavaConfig 中定义了BeanName相同的Bean时,优先定义的有效(这里不抛异常不太能理解)
  • 自动扫描的Bean,不支持类名相同,但是包路径不同的场景(会抛异常)

4. Import

在xml配置中,另一个常见的case就是引入另一个xml配置,在JavaConfig中代替的就是Import注解

1
2
3
4
5
@Configuration
@ComponentScan("com.git.hui.rabbit.spring")
@Import({DirectConsumerConfig.class, FanoutConsumerConfig.class, TopicConsumerConfig.class})
public class SpringConfig {
}

这个就等同于xml中常见的:

1
<import resource="service.xml" />

II. 实例测试

1. xml单测姿势

上面说了用JavaConfig代替xml配置的方式,另一个关键的地方就是测试用例的写法了,对于之前的xml,有两种常见的使用姿势

case1: 注解方式

1
2
3
4
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath*:*.xml")
public class BeanTest {
}

case2: 主动加载容器方式

1
2
3
4
5
6
7
private ServiceA serviceA;

@Before
public void init() {
ApplicationContext apc = new ClassPathXmlApplicationContext("classpath:*.xml");
serviceA = (ServiceA) apc.getBean("serviceA");
}

2. JavaConfig单测使用姿势

那么替换成JavaConfig的用法,也有两种

case1: 注解方式,指定内部classes值

1
2
3
4
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = SpringConfig.class)
public class SprintUnit {
}

case2: 主动加载容器,改为AnnotationConfigApplicationContext

1
2
3
4
5
6
@Test
public void testServiceA() {
ApplicationContext context = new AnnotationConfigApplicationContext(BeansConfiguration.class);
ServiceA serviceA = (ServiceA) context.getBean("serviceA");
serviceA.print();
}

III. 小结

1. 注解映射关系

JavaConfig方式基本上采用的是替换的思路来取代xml,即原xml中的一些东西,可以直接通过注解来代替,如

  • @Configuration 修饰类,与传统的xml文件作用相同
  • @Bean注解,修饰方法,表示声明一个Bean,与原来的xml中的 <bean> 标签作用相同
  • @ComponentScan注解,自动扫描包,类似xml中的 <context:component-scan>
  • @Import注解,与xml中的<import>标签类似,引入其他的配置信息

2. BeanName重名规则

在实际使用中,有一点需要额外注意,对于beanName相同的情况,通过测试的规则如下(没有看源码,不保证完全准确,仅为测试后得出的依据):

  • JavaConfig 定义的BeanName与自动扫描的BeanName冲突时,JavaConfig的定义的会被实例化
  • JavaConfig 中定义了BeanName相同的Bean时,优先定义的有效(这里不抛异常不太能理解)
  • 自动扫描的Bean,不支持类名相同,但是包路径不同的场景(会抛异常)

3. 测试姿势

最简单的就是修改原来的注解@ContextConfiguration中的值

1
@ContextConfiguration(classes = SpringConfig.class)

II. 其他

一灰灰Bloghttps://liuyueyi.github.io/hexblog

一灰灰的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛

声明

尽信书则不如,已上内容,纯属一家之言,因个人能力有限,难免有疏漏和错误之处,如发现bug或者有更好的建议,欢迎批评指正,不吝感激

扫描关注

QrCode

Spring学习之事务管理与传播属性

Spring 事务管理与传播属性

在博文 《Spring学习之事务的使用姿势》 中,演示了基于注解和xml的事务使用姿势,以@Transactional注解为例,其中很多的参数都没有详细说明

本篇博文,则主要目的是弄懂这些参数有啥用,以及在实际项目中如何选择

Spring学习之事务的使用姿势

Spring + mybatis + mysql 使用事务的几种姿势

主要记录下spring是如何支持事务的,以及在Spring结合mybatis时,可以怎么简单的实现数据库的事务功能

I. 前提

case1:两张表的的事务支持情况

首先准备两张表,一个user表,一个story表,结构如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
CREATE TABLE `user` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(20) NOT NULL DEFAULT '' COMMENT '用户名',
`pwd` varchar(26) NOT NULL DEFAULT '' COMMENT '密码',
`isDeleted` tinyint(1) NOT NULL DEFAULT '0',
`created` varchar(13) NOT NULL DEFAULT '0',
`updated` varchar(13) NOT NULL DEFAULT '0',
PRIMARY KEY (`id`),
KEY `name` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

CREATE TABLE `story` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`userId` int(20) unsigned NOT NULL DEFAULT '0' COMMENT '作者的userID',
`name` varchar(20) NOT NULL DEFAULT '' COMMENT '作者名',
`title` varchar(26) NOT NULL DEFAULT '' COMMENT '密码',
`story` text COMMENT '故事内容',
`isDeleted` tinyint(1) NOT NULL DEFAULT '0',
`created` varchar(13) NOT NULL DEFAULT '0',
`updated` varchar(13) NOT NULL DEFAULT '0',
PRIMARY KEY (`id`),
KEY `userId` (`userId`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

我们的事务场景在于用户修改name时,要求两张表的name都需要一起修改,不允许出现不一致的情况

case2:单表的事务支持

转账,一个用户减钱,另一个用户加钱

1
2
3
4
5
6
7
8
9
10
CREATE TABLE `money` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(20) NOT NULL DEFAULT '' COMMENT '用户名',
`money` int(26) NOT NULL DEFAULT '0' COMMENT '钱',
`isDeleted` tinyint(1) NOT NULL DEFAULT '0',
`created` varchar(13) NOT NULL DEFAULT '0',
`updated` varchar(13) NOT NULL DEFAULT '0',
PRIMARY KEY (`id`),
KEY `name` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

相比上面那个case,这个更加简单了,下面的实例则主要根据这个进行说明,至于case1,则留待扩展里面进行

首先是实现对应的dao和entity

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
@Data
public class MoneyEntity implements Serializable {

private static final long serialVersionUID = -7074788842783160025L;

private int id;

private String name;

private int money;

private int isDeleted;

private int created;

private int updated;
}


public interface MoneyDao {
MoneyEntity queryMoney(@Param("id") int userId);

// 加钱,负数时表示减钱
int incrementMoney(@Param("id") int userId, @Param("addMoney") int addMoney);
}

对应的mapper文件为

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
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.git.hui.demo.mybatis.mapper.MoneyDao">

<sql id="moneyEntity">
id, `name`, `money`, `isDeleted`, `created`, `updated`
</sql>


<select id="queryMoney" resultType="com.git.hui.demo.mybatis.entity.MoneyEntity">
select
<include refid="moneyEntity"/>
from money
where id=#{id}

</select>

<update id="incrementMoney">
update money
set money=money + #{addMoney}
where id=#{id}
</update>
</mapper>

对应的mybatis连接数据源的相关配置

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
<bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
<property name="locations">
<value>classpath*:jdbc.properties</value>
</property>
</bean>


<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
<property name="driverClassName" value="${driver}"/>
<property name="url" value="${url}"/>
<property name="username" value="${username}"/>
<property name="password" value="${password}"/>

<property name="filters" value="stat"/>

<property name="maxActive" value="20"/>
<property name="initialSize" value="1"/>
<property name="maxWait" value="60000"/>
<property name="minIdle" value="1"/>

<property name="timeBetweenEvictionRunsMillis" value="60000"/>
<property name="minEvictableIdleTimeMillis" value="300000"/>

<property name="validationQuery" value="SELECT 'x'"/>
<property name="testWhileIdle" value="true"/>
<property name="testOnBorrow" value="false"/>
<property name="testOnReturn" value="false"/>

<property name="poolPreparedStatements" value="true"/>
<property name="maxPoolPreparedStatementPerConnectionSize" value="50"/>
</bean>


<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource"/>
<!-- 指定mapper文件 -->
<property name="mapperLocations" value="classpath*:mapper/*.xml"/>
</bean>


<!-- 指定扫描dao -->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="com.git.hui.demo.mybatis"/>
</bean>

II. 实例演示

通过网上查询,Spring事务管理总共有四种方式,下面逐一进行演示,每种方式是怎么玩的,然后看实际项目中应该如何抉择

1. 硬编码方式

编程式事务管理,既通过TransactionTemplate来实现多个db操作的事务管理

a. 实现

那么,我们的转账case可以如下实现

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
@Repository
public class CodeDemo1 {

@Autowired
private MoneyDao moneyDao;


@Autowired
private TransactionTemplate transactionTemplate;


/**
* 转账
*
* @param inUserId
* @param outUserId
* @param payMoney
* @param status 0 表示正常转账, 1 表示内部抛出一个异常, 2 表示新开一个线程,修改inUserId的钱 +200, 3 表示新开一个线程,修改outUserId的钱 + 200
*/
public void transfor(final int inUserId, final int outUserId, final int payMoney, final int status) {
transactionTemplate.execute(new TransactionCallbackWithoutResult() {
protected void doInTransactionWithoutResult(TransactionStatus transactionStatus) {
MoneyEntity entity = moneyDao.queryMoney(outUserId);
if (entity.getMoney() > payMoney) { // 可以转账

// 先减钱
moneyDao.incrementMoney(outUserId, -payMoney);


testCase(inUserId, outUserId, status);

// 再加钱
moneyDao.incrementMoney(inUserId, payMoney);
System.out.println("转账完成! now: " + System.currentTimeMillis());
}
}
});
}


// 下面都是测试用例相关
private void testCase(final int inUserId, final int outUserId, final int status) {
if (status == 1) {
throw new IllegalArgumentException("转账异常!!!");
} else if(status == 2) {
addMoney(inUserId);
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
} else if (status == 3) {
addMoney(outUserId);
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}


public void addMoney(final int userId) {
System.out.printf("内部加钱: " + System.currentTimeMillis());
new Thread(new Runnable() {
public void run() {
moneyDao.incrementMoney(userId, 200);
System.out.println(" sub modify success! now: " + System.currentTimeMillis());
}
}).start();
}
}

主要看上面的transfor方法,内部通过 transactionTemplate 来实现事务的封装,内部有三个db操作,一个查询,两个更新,具体分析后面说明

上面的代码比较简单了,唯一需要关注的就是transactionTemplate这个bean如何定义的,xml文件中与前面重复的就不贴了,直接贴上关键代码, 一个是根据DataSource创建的TransactionManager,一个则是根据TransactionManager创建的TransactionTemplate

1
2
3
4
5
6
7
8
<!--编程式事务-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>

<bean id="transactionTemplate" class="org.springframework.transaction.support.TransactionTemplate">
<property name="transactionManager" ref="transactionManager"/>
</bean>

b. 测试用例

正常演示情况, 演示没有任何异常,不考虑并发的情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration({"classpath*:spring/service.xml", "classpath*:test-datasource1.xml"})
public class CodeDemo1Test {
@Autowired
private CodeDemo1 codeDemo1;

@Autowired
private MoneyDao moneyDao;

@Test
public void testTransfor() {

System.out.println("---------before----------");
System.out.println("id: 1 money = " + moneyDao.queryMoney(1).getMoney());
System.out.println("id: 2 money = " + moneyDao.queryMoney(2).getMoney());


codeDemo1.transfor(1, 2, 10, 0);

System.out.println("---------after----------");
System.out.println("id: 1 money = " + moneyDao.queryMoney(1).getMoney());
System.out.println("id: 2 money = " + moneyDao.queryMoney(2).getMoney());
}
}

输出如下,两个账号的钱都没有问题

1
2
3
4
5
6
7
---------before----------
id: 1 money = 10000
id: 2 money = 50000
转账完成! now: 1526130394266
---------after----------
id: 1 money = 10010
id: 2 money = 49990

转账过程中出现异常,特别是转账方钱已扣,收款方还没收到钱时,也就是case中的status为1的场景

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 内部抛异常的情况
@Test
public void testTransforException() {

System.out.println("---------before----------");
System.out.println("id: 1 money = " + moneyDao.queryMoney(1).getMoney());
System.out.println("id: 2 money = " + moneyDao.queryMoney(2).getMoney());


try {
codeDemo1.transfor(1, 2, 10, 1);
} catch (Exception e) {
e.printStackTrace();
}

System.out.println("---------after----------");
System.out.println("id: 1 money = " + moneyDao.queryMoney(1).getMoney());
System.out.println("id: 2 money = " + moneyDao.queryMoney(2).getMoney());
}

对此,我们希望把转账方的钱还回去, 输出如下,发现两个的钱都没有变化

1
2
3
4
5
6
7
8
---------before----------
id: 1 money = 10010
id: 2 money = 49990
---------after----------
id: 1 money = 10010
java.lang.IllegalArgumentException: 转账异常!!!
... // 省略异常信息
id: 2 money = 49990

当status为2,表示在转账人钱已扣,收款人钱没收到之间,又有人给收款人转了200,此时根据mysql的锁机制,另外人的转账应该是立马到的(因为收款人账号没有被锁住),且金额不应该有问题

输出结果如下:

1
2
3
4
5
6
7
8
9
10
11
---------before----------
id: 1 money = 10010
id: 2 money = 49990
## 右边是注释: 转账过程中,另外存钱立马到账,没有被锁住
内部加钱: 1526130827480
sub modify success! now: 1526130827500
## 存钱结束
转账完成! now: 1526130830488
---------after----------
id: 1 money = 10220
id: 2 money = 49980

当status为3, 表示在转账人钱已扣,收款人钱没收到之间,又有人给转账人转了200,这时因为转账人的记录以及被加了写锁,因此只能等待转账的事务提交之后,才有可能+200成功,当然最终的金额也得一致

输出结果如下

1
2
3
4
5
6
7
8
9
10
11
---------before----------
id: 1 money = 10220
id: 2 money = 49980
## 右边是注释:内部存钱了,但没有马上成功
## 直到转账完成后,才立马存成功,注意两个时间戳
内部加钱: 1526131101046
转账完成! now: 1526131104051
sub modify success! now: 1526131104053
---------after----------
id: 1 money = 10230
id: 2 money = 50170

c. 小结

至此,编程式事务已经实例演示ok,从上面的过程,给人的感觉就和直接写事务相关的sql一样,

1
2
3
4
5
6
start transaction;

-- 这中间就是 TransactionTemplate#execute 方法内部的逻辑
-- 也就是需要事务管理的一组sql

commit;

2. 基于TransactionProxyFactoryBean方式

接下来的三个就是声明式事务管理,这种用得也比较少,因为需要每个事务管理类,添加一个TransactionProxyFactoryBean

a. 实现

除了将 TransactionTemplate 干掉,并将内部的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
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
public class FactoryBeanDemo2 {

@Autowired
private MoneyDao moneyDao;

/**
* 转账
*
* @param inUserId
* @param outUserId
* @param payMoney
* @param status 0 表示正常转账, 1 表示内部抛出一个异常, 2 表示新开一个线程,修改inUserId的钱 +200, 3 表示新开一个线程,修改outUserId的钱 + 200
*/
public void transfor(final int inUserId, final int outUserId, final int payMoney, final int status) {

MoneyEntity entity = moneyDao.queryMoney(outUserId);
if (entity.getMoney() > payMoney) { // 可以转账

// 先减钱
moneyDao.incrementMoney(outUserId, -payMoney);


testCase(inUserId, outUserId, status);


// 再加钱
moneyDao.incrementMoney(inUserId, payMoney);
System.out.println("转账完成! now: " + System.currentTimeMillis());
}


}


private void testCase(final int inUserId, final int outUserId, final int status) {
if (status == 1) {
throw new IllegalArgumentException("转账异常!!!");
} else if (status == 2) {
addMoney(inUserId);
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
} else if (status == 3) {
addMoney(outUserId);
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}


public void addMoney(final int userId) {
System.out.println("内部加钱: " + System.currentTimeMillis());
new Thread(new Runnable() {
public void run() {
moneyDao.incrementMoney(userId, 200);
System.out.println("sub modify success! now: " + System.currentTimeMillis());
}
}).start();
}
}

重点来了,主要是需要配置一个 TransactionProxyBeanFactory,我们知道BeanFactory就是我们自己来创建Bean的一种手段,相关的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
<!--编程式事务-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>

<bean id="factoryBeanDemo2" class="com.git.hui.demo.mybatis.repository.transaction.FactoryBeanDemo2"/>

<!-- 配置业务层的代理 -->
<bean id="factoryBeanDemoProxy" class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean">
<!-- 配置目标对象 -->
<property name="target" ref="factoryBeanDemo2" />
<!-- 注入事务管理器 -->
<property name="transactionManager" ref="transactionManager"/>
<!-- 注入事务的属性 -->
<property name="transactionAttributes">
<props>
<!--
prop的格式:
* PROPAGATION :事务的传播行为
* ISOTATION :事务的隔离级别
* readOnly :只读
* -EXCEPTION :发生哪些异常回滚事务
* +EXCEPTION :发生哪些异常不回滚事务
-->
<!-- 这个key对应的就是目标类中的方法-->
<prop key="transfor">PROPAGATION_REQUIRED</prop>
<!-- <prop key="transfer">PROPAGATION_REQUIRED,readOnly</prop> -->
<!-- <prop key="transfer">PROPAGATION_REQUIRED,+java.lang.ArithmeticException</prop> -->
</props>
</property>
</bean>

通过上面的配置,大致可以了解到这个通过TransactionProxyFactoryBean就是创建了一个FactoryBeanDemo2的代理类,这个代理类内部封装好事务相关的逻辑,可以看做是前面编程式的一种简单通用抽象

b. 测试

测试代码与前面基本相同,唯一的区别就是我们使用的应该是上面BeanFactory生成的Bean,而不是直接使用FactoryBeanDemo2

正常演示case:

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
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration({"classpath*:spring/service.xml", "classpath*:test-datasource2.xml"})
public class FactoryBeanDemo1Test {

@Resource(name = "factoryBeanDemoProxy")
private FactoryBeanDemo2 factoryBeanDemo2;

@Autowired
private MoneyDao moneyDao;


@Test
public void testTransfor() {

System.out.println("---------before----------");
System.out.println("id: 1 money = " + moneyDao.queryMoney(1).getMoney());
System.out.println("id: 2 money = " + moneyDao.queryMoney(2).getMoney());


factoryBeanDemo2.transfor(1, 2, 10, 0);

System.out.println("---------after----------");
System.out.println("id: 1 money = " + moneyDao.queryMoney(1).getMoney());
System.out.println("id: 2 money = " + moneyDao.queryMoney(2).getMoney());
}
}

输出

1
2
3
4
5
6
7
---------before----------
id: 1 money = 10000
id: 2 money = 50000
转账完成! now: 1526132058886
---------after----------
id: 1 money = 10010
id: 2 money = 49990

status为1,内部异常的情况下,我们希望钱也不会有问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Test
public void testTransforException() {

System.out.println("---------before----------");
System.out.println("id: 1 money = " + moneyDao.queryMoney(1).getMoney());
System.out.println("id: 2 money = " + moneyDao.queryMoney(2).getMoney());


try {
factoryBeanDemo2.transfor(1, 2, 10, 1);
} catch (Exception e) {
System.out.println(e.getMessage());;
}

System.out.println("---------after----------");
System.out.println("id: 1 money = " + moneyDao.queryMoney(1).getMoney());
System.out.println("id: 2 money = " + moneyDao.queryMoney(2).getMoney());
}

输出为

1
2
3
4
5
6
7
---------before----------
id: 1 money = 10010
id: 2 money = 49990
转账异常!!!
---------after----------
id: 1 money = 10010
id: 2 money = 49990

status为2 时,分析结果与上面应该相同,输出如下

1
2
3
4
5
6
7
8
9
---------before----------
id: 1 money = 10010
id: 2 money = 49950
内部加钱: 1526133325376
sub modify success! now: 1526133325387
转账完成! now: 1526133328381
---------after----------
id: 1 money = 10220
id: 2 money = 49940

status为3时,输出

1
2
3
4
5
6
7
8
9
---------before----------
id: 1 money = 10220
id: 2 money = 49940
内部加钱: 1526133373466
转账完成! now: 1526133376476
sub modify success! now: 1526133376480
---------after----------
id: 1 money = 10230
id: 2 money = 50130

c. 小结

TransactionProxyFactoryBean 的思路就是利用代理模式来实现事务管理,生成一个代理类,拦截目标方法,将一组sql的操作封装到事务中进行;相比较于硬编码,无侵入,而且支持灵活的配置方式

缺点也显而易见,每个都要进行配置,比较繁琐

3. xml使用方式

Spring有两大特点,IoC和AOP,对于事务这种情况而言,我们可不可以使用AOP来做呢?

对于需要开启事务的方法,拦截掉,执行前开始事务,执行完毕之后提交事务,出现异常时回滚

这样一看,感觉还是蛮有希望的,而下面两种姿势正是这么玩的,因此需要加上aspect的依赖

1
2
3
4
5
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.8.7</version>
</dependency>

a. 实现

java类与第二种完全一致,变动的只有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
<!-- 首先添加命名空间 -->
xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="...
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd"



<!--对应的事务通知和切面配置-->
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<tx:attributes>
<!--
propagation :事务传播行为
isolation :事务的隔离级别
read-only :只读
rollback-for:发生哪些异常回滚
no-rollback-for :发生哪些异常不回滚
timeout :过期信息
-->
<tx:method name="transfor" propagation="REQUIRED"/>
</tx:attributes>
</tx:advice>



<!-- 配置切面 -->
<aop:config>
<!-- 配置切入点 -->
<aop:pointcut expression="execution(* com.git.hui.demo.mybatis.repository.transaction.XmlDemo3.*(..))" id="pointcut1"/>
<!-- 配置切面 -->
<aop:advisor advice-ref="txAdvice" pointcut-ref="pointcut1"/>
</aop:config>

观察上面的配置,再想想第二种方式,思路都差不多了,但是这种方式明显更加通用,通过切面和切点,可以减少大量的配置

b. 测试

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
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration({"classpath*:spring/service.xml", "classpath*:test-datasource3.xml"})
public class XmlBeanTest {
@Autowired
private XmlDemo3 xmlDemo;

@Autowired
private MoneyDao moneyDao;


@Test
public void testTransfor() {

System.out.println("---------before----------");
System.out.println("id: 1 money = " + moneyDao.queryMoney(1).getMoney());
System.out.println("id: 2 money = " + moneyDao.queryMoney(2).getMoney());


xmlDemo.transfor(1, 2, 10, 0);

System.out.println("---------after----------");
System.out.println("id: 1 money = " + moneyDao.queryMoney(1).getMoney());
System.out.println("id: 2 money = " + moneyDao.queryMoney(2).getMoney());
}
}

这个测试起来,和一般的写法就没啥两样了,比第二种的FactoryBean的注入方式简单点

正常输出

1
2
3
4
5
6
7
---------before----------
id: 1 money = 10000
id: 2 money = 50000
转账完成! now: 1526135301273
---------after----------
id: 1 money = 10010
id: 2 money = 49990

status=1 出现异常时,输出

1
2
3
4
5
6
7
---------before----------
id: 1 money = 10010
id: 2 money = 49990
转账异常!!!
---------after----------
id: 1 money = 10010
id: 2 money = 49990

status=2 转账过程中,又存钱的场景,输出,与前面预期一致

1
2
3
4
5
6
7
8
9
---------before----------
id: 1 money = 10010
id: 2 money = 49990
内部加钱: 1526135438403
sub modify success! now: 1526135438421
转账完成! now: 1526135441410
---------after----------
id: 1 money = 10220
id: 2 money = 49980

status=3 的输出,与前面预期一致

1
2
3
4
5
6
7
8
9
---------before----------
id: 1 money = 10220
id: 2 money = 49980
内部加钱: 1526135464341
转账完成! now: 1526135467349
sub modify success! now: 1526135467352
---------after----------
id: 1 money = 10230
id: 2 money = 50170

4. 注解方式

这个就是消灭xml,用注解来做的方式,就是将前面xml中的配置用 @Transactional注解替换

a. 实现

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
@Repository
public class AnnoDemo4 {

@Autowired
private MoneyDao moneyDao;


/**
* 转账
*
* @param inUserId
* @param outUserId
* @param payMoney
* @param status 0 表示正常转账, 1 表示内部抛出一个异常, 2 表示新开一个线程,修改inUserId的钱 +200, 3 表示新开一个线程,修改outUserId的钱 + 200
*
*
* Transactional注解中的的属性 propagation :事务的传播行为 isolation :事务的隔离级别 readOnly :只读
* rollbackFor :发生哪些异常回滚 noRollbackFor :发生哪些异常不回滚
* rollbackForClassName 根据异常类名回滚
*/
@Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.DEFAULT, readOnly = false)
public void transfor(final int inUserId, final int outUserId, final int payMoney, final int status) {

MoneyEntity entity = moneyDao.queryMoney(outUserId);
if (entity.getMoney() > payMoney) { // 可以转账

// 先减钱
moneyDao.incrementMoney(outUserId, -payMoney);


testCase(inUserId, outUserId, status);


// 再加钱
moneyDao.incrementMoney(inUserId, payMoney);
System.out.println("转账完成! now: " + System.currentTimeMillis());
}


}


private void testCase(final int inUserId, final int outUserId, final int status) {
if (status == 1) {
throw new IllegalArgumentException("转账异常!!!");
} else if (status == 2) {
addMoney(inUserId);
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
} else if (status == 3) {
addMoney(outUserId);
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}


private void addMoney(final int userId) {
System.out.println("内部加钱: " + System.currentTimeMillis());
new Thread(new Runnable() {
public void run() {
moneyDao.incrementMoney(userId, 200);
System.out.println("sub modify success! now: " + System.currentTimeMillis());
}
}).start();
}

}

因此需要在xml中配置,开启事务注解

1
2
3
4
5
6
7
8

<!--编程式事务-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>


<tx:annotation-driven transaction-manager="transactionManager"/>

这样一看,就更加清晰了,实际项目中,xml和注解方式也是用得最多的场景了

b. 测试case

和第三种测试case完全相同, 输出结果也一样,直接省略

III. 小结

上面说了Spring中四种使用事务的姿势,其中硬编码方式可能是最好理解的,就相当于将我们写sql中,使用事务的方式直接翻译成对应的java代码了;而FactoryBean方式相当于特殊情况特殊对待,为每个事务来一个代理类来增强事务功能;后面的两个则原理差不多都是利用事务通知(AOP)来实现,定义切点及相关信息

编程式:

  • 注入 TransactionTemplate
  • 将利用事务的逻辑封装到 transactionTemplate#execute方法内

代理BeanFactory:

  • 利用 TransactionProxyFactoryBean 为事务相关类生成代理
  • 使用方通过FactoryBean获取代理类,作为使用的Bean

xml配置:

  • 利用 tx标签 + aop方式来实现
  • <tx:advice> 标签定义事务通知,内部可有较多的配置信息
  • <aop:config> 配置切点,切面

注解方式:

  • 在开启事务的方法or类上添加 @Transactional 注解即可
  • 开启事务注解 <tx:annotation-driven transaction-manager="transactionManager"/>

IV. 其他

1. 参考

文档

源码

2. 个人博客: 一灰灰Blog

基于hexo + github pages搭建的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛

3. 声明

尽信书则不如,已上内容,纯属一家之言,因本人能力一般,见识有限,如发现bug或者有更好的建议,随时欢迎批评指正

4. 扫描关注

QrCode

SpringMVC统一异常处理

统一异常拦截处理方式

项目中不可避免会出现一些异常情况,而一个web项目,若不拦截异常,糟糕的情况下可能直接将堆栈抛给前端,从而导致各种鬼畜的问题

SpringMVC支持跨域的几种姿势

SpringMVC支持跨域的几种姿势

跨域好像是一个前端的问题,通常是a域名下向b域名的服务发起请求,然后处于浏览器的安全原则,被拦截了,而这种场景,在实际的项目中并不少见,那么作为后端可以怎么去支持跨域的case呢?

后端需要支持跨域,一个是支持jsonp请求;还有一个就是设置responseHeader中crossOrigin等相关参数

SpringMVC返回图片的几种方式

SpringMVC返回图片的几种方式

后端提供服务,通常返回的json串,但是某些场景下可能需要直接返回二进制流,如一个图片编辑接口,希望直接将图片流返回给前端;如果要求返回base64,此时可以怎么处理?

SpringMVC之请求参数的获取方式

SpringMVC之请求参数的获取方式

常见的一个web服务,如何获取请求参数?

一般最常见的请求为GET和POST,get请求的参数在url上可以获取,post请求参数除了url上还有可能在表单中,文件上传时,获取方式又和一般的参数获取不一样

本篇则主要集中在不同请求方式下,获取参数的使用姿势

首先需要搭建一个后端的请求,为了快速演示

利用spring-boot创建了一个机器简单的工程,依赖版本 1.5.4.RELEASE

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×