天天看点

第一阶段第六部分大话Spring

前言

很多新手小白在学习的时候总会遇到一个问题:

我们一运行程序,只能看到程序最后的结果,但是这个程序究竟是怎么一步步运行出这样的结果呢?如果有一个工具能够让我们看到我们程序的执行流程该有多好~

这就需要用到新手小白编程的利器——断点调试工具Debug了。

啥是Debug?

首先,对Debug做一个简单的描述:

Debug:是供程序员使用的程序调试工具,它可以用于查看程序的执行流程,也可以用于追踪程序执行过程来调试程序。

它的两大作用,查看程序执行流程,调试程序。

Debug怎么用?

我们将从以下几个角度来分析这个问题:

1.什么是断点?

你可以把断点理解成想让程序停止的然后来查看的位置

也就是我们人为指定的需要程序执行时停止的地方

2.怎么添加断点?

可以在目标代码行号前双击

第一阶段第六部分大话Spring

 注意:启动后会提示是否”确认试图切换“,选择switch

第一阶段第六部分大话Spring

4.看哪里?

我们可以按F6让程序继续执行下一步

变量变化的窗口Variables--可以查看变量值的变化情况

第一阶段第六部分大话Spring

控制台--可以查看输出的情况

第一阶段第六部分大话Spring

5.结束之后改怎么办?

1.选择左侧栏Debug中的Terminate and Remove关闭打开的Debug窗口

第一阶段第六部分大话Spring

 2.取消断点:双击想取消的断点即可取消

3.切换回Java视图

第一阶段第六部分大话Spring

 以上就是新手小白DEBUG入门小技巧

这个还是需要多多使用才能熟练哦~

希望能帮到你~

Spring提供了@Profile注解来解决程序在不同运行环境时候的配置差别。

项目开发时候大多包含:开发、测试、上线运行几个过程,在每个过程中软件的工作环境一般多少有些差别,比如:在开发阶段利用本地数据库、测试阶段采用测试数据库、在上线运行阶段使用生产数据库。这些差别如果采用了手工维护就会存在各种问题:效率低下、容易发生人为因素意外错误。

利用Spring提供的@Profile注解就可以定义程序不同的运行场景配置,配置以后在启动程序时候给定不同的启动参数就可以灵活的切换运行场景,不再需要人工干预,这样就可以大大提升开发效率。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9cQi9u3U-1626694107508)(profile.png)]

以配置开发环和生产境数据源为例子,具体说明使用步骤:

在Spring配置文件中利用@Profile声明开发环境和生产环境使用的数据源:

@Configuration public class DataSourceConfig { @Bean(name="dataSource") //重写BeanID @Profile("dev") //配置开发环境使用的数据源 public DataSource dataSourceForDev() { DruidDataSource dataSource = new DruidDataSource(); ... return dataSource; } @Bean(name="dataSource")//重写BeanID @Profile("production")//配置生产环境使用的数据源 public DataSource dataSourceForProd() { DruidDataSource dataSource = new DruidDataSource(); ... return dataSource; } }

其中“dev”表示开发环境,“production”表示生产环境,显然有两个BeanID是“dataSource”的数据源Bean对象,这两个对象不会同时初始化,Spring会根据激活的Profile属性初始化其中一个数据源Bean对象。

使用如下启动命令参数-Dspring.profiles.active=dev就可以设置当前激活的Profile是发环境“dev”,此时Spring会初始化属于开发环境的数据源Bean对象:

java -Dspring.profiles.active=dev -jar demo.jar
           
  • 1

或者在SpringBoot的启动类中使用系统属性设置激活的Profile:

System.setProperty("spring.profiles.active" , "dev"); SpringApplication.run(AppConfig.class);
           
  • 1

在测试时候可以使用 @ActiveProfiles注解设置当前激活的Profile。

大话Spring

因为2020年的疫情,公司就一直不景气,从2020年10月起,公司就开始只发80%工资,从今年2月份开始宣布“暂时”只发60%工资,或选择自愿离职,之前没发的“择日发放”,当时就挺犹豫的,已经积累了4个月的20%还没发,也是一笔不算少的收入,如果离职了,还不知道什么时候才能发下来,而且目前的大环境也不是很好,手里的项目也一时半会交不出去,听主管说公司的账上还有几千万,也在谈新的外包项目,是有转机的,于是选择了接收60%工资,结果还没坚持到5月份,公司就直接宣布破产,好在老板还不错,社保一直也没断,最后还给我们补了钱,算了一下,差不多之前没发的工资都补上了。

虽然工资都拿到手了,不过也没工作了,前公司的主管帮我推荐了一份工作,我前天就去面试了,面试之前还是很有信心的,一来,是熟人推荐,二来,我自己也有2年多的工作经验了,结果,没想到被一个我认为非常简单的题目给问倒了,这个题目是这样的:“在某个业务类中有2个更新数据的方法,且都是事务性的,如果第2个方法调用第1个方法,会有几个事务?”,如果把文字转换为成代码,大致就是这样的:

public class UserService {
  @Transactional
  public void update1() {
    // 执行某些操作
  }
  
  @Transactional
  public void update2() {
    update1(); // 调用当前类中另一个事务性的方法
  }
}
           
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

这明显考察的是事务的传播!老实说,虽然我有2年多的工作经验,但公司的规模也不大,参与的项目中也没有太复杂的事务,一般都是在需要事务性的业务方法上加

@Transactional

注解就完事了,只要测试结果没问题,一直没有纠结过事务的传播类型,好在面试的前一天晚上翻了翻当年培训时老师给的笔记,顺利的答了出来,事务的传播类型有这几种:

  • MANDATORY

  • NEVER

  • NOT_SUPPORTED

  • SUPPORTS

  • REQUIRED

    (默认)
  • REQUIRES_NEW

  • NESTED

所以,这个面试题的答案就是:“2个事务性的方法,一个调用另一个,由于事务传播的默认值是

REQUIRED

,则表现为:如果当前无事务,则创建,如果当前有事务,则使用”,为了完善我的答案,我还继续补充了:“如果将

@Transactional

注解的

propagation

属性配置为其它值,则会不同”。当我非常流利的把我脑中的答案说完之后,面试官笑了笑,说了两个字:“不对”,我当时就懵了,最后,面试官也没有告诉我答案,只是让我自己回去找答案……不过运气还算不错,由于只错了这一题,最后还是顺利入职了。

我觉得每个码农对技术都是有一定的执着的,前天面试完后,自己也上网看了一些文章,大多都只说了事务的传播类型,及各种类型的表现,根本没有我想要的答案,于是,昨天我联系了一下当年培训时的苍老师,他听了题目和我的答案后,也是“呵呵”一笑,说这是Spring认证考试中的原题,被考到这一题的概率至少有70%,而且,最近好多公司都直接拿Spring题库里的题当面试题……然后他就让我等着,过一会给我发了个压缩包,是一份Demo代码,果然是人狠话不多,直接拿代码讲道理,我看了看代码,按照苍老师在代码里留的注释改动了几下,基本上就有答案了!

虽然答案本身很简单,但是又领悟了不少东西,为了“纪念”一下这个错题,和大家分享一下Spring中

@Transactional

的细节!

首先,项目结构是这样的:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sr0TLW2I-1625749860880)(image-20210630170414935.png)]

这个项目中主要用到了Spring、Mybatis和单元测试,比较基础的环境搭建和配置就不说了,如果需要这个代码的,可以从 http:// 下载。

大概就是:项目中使用了

t_user

t_order

这2张表,且都有几条初始数据,在这2张表对应的持久层都编写了根据id修改数据的功能。

重点是业务部分,我们都知道,事务是在业务层进行管理的,业务层的结构是这样的(暂时不用的先不贴出来):

[src]
		[main]
				[java]
						[cn.tedu]
								[service]
										[impl]
												UserServiceImpl
										UserService
           
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

很显然,以

User

前缀开头的都是处理

t_user

表的数据的,在最初的实验中只需要观察这1张表就可以了。

关于

UserService

接口:

package cn.tedu.service;

public interface UserService {

    void update1();

    void update2();

}
           
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

关于

UserServiceImpl

类:

package cn.tedu.service.impl;

import cn.tedu.mapper.UserMapper;
import cn.tedu.service.OrderService;
import cn.tedu.service.UserService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class UserServiceImpl implements UserService {

    private UserMapper userMapper;
    private OrderService orderService;

    public UserServiceImpl(UserMapper userMapper, OrderService orderService) {
        this.userMapper = userMapper;
        this.orderService = orderService;
    }

    // TODO-01:调整是否使用以下@Transactional注解,并运行单元测试,以观察效果
  	// @Transactional
    public void update1() {
        int rows;
        // 更新id=1的数据,会成功
        rows = userMapper.updateUserNameById(1, "USER-1000001");
        if (rows != 1) {
            throw new RuntimeException("更新User:id=1数据失败!");
        }
        // 更新id=1000000,会失败
        rows = userMapper.updateUserNameById(1000000, "USER-1000001");
        if (rows != 1) {
            throw new RuntimeException("更新User:id=1000000数据失败!");
        }
    }

    // TODO-02:调整是否使用以下@Transactional注解,并运行单元测试,以观察效果
    @Transactional
    public void update2() {
        update1();
    }

}
           
  • 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

可以看到,以上

update1()

方法中有2次更新操作,第1次肯定会成功的,第2次则会因为id值不存在而失败,失败后抛出了

RuntimeException

对象,符合Spring管理事务的默认回滚规则,但是,

update1()

方法不一定有

@Transactional

注解,这是苍老师留着我自己测试效果的,下面的

update2()

就比较简单了,它直接调用了

update1()

方法。

苍老师写的测试也非常有趣,使用了

@Sql

注解处理初始化数据库与数据,使用了断言,和我们平时偷懒写的完全不同,那天我也问过他,他说Spring认证考试也会考这个,以后搞不好也会成为用人单位的面试题(毕竟有不少用人单位都是直接上网百度找面试题,根本不自己出错,大家都懂的)……他是这么写的:

package cn.tedu.service;

import cn.tedu.config.ApplicationConfig;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.jdbc.Sql;
import org.springframework.test.context.jdbc.SqlConfig;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;

import static org.junit.jupiter.api.Assertions.assertThrows;

@SpringJUnitConfig(ApplicationConfig.class)
@Sql(config = @SqlConfig(dataSource = "dataSource"),
        scripts = {"classpath:/sql/schema.sql", "classpath:/sql/data.sql"})
public class UserServiceTests {

    @Autowired
    UserService userService;

    @Test
    public void testUpdate1() {
        assertThrows(RuntimeException.class, () -> {
            userService.update1();
        });
    }

    @Test
    public void testUpdate2() {
        assertThrows(RuntimeException.class, () -> {
            userService.update2();
        });
    }

}
           
  • 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

其实,现在就可以测试出效果了,根据在业务类中的2个方法上是否使用

@Transactional

注解,观察数据是否回滚即可判断,我测试的结果如下:

是否在

update1()

上使用注解
是否在

update2()

上使用注解
是否回滚

可以看到,事务是否回滚完全取决于

update2()

方法有没有

@Transactional

注解,与

update1()

方法是否有注解无关!

苍老师说,Spring官方给出的文档中明确指出:Propagation Rules Are Enforced by a Proxy,即“传播规则是由代理强制执行的”。所以,Spring管理事务是基于接口进行代理的,在调用

@Transactional

注解的方法之前就会开启事务,并在过程中决定是否回滚或最终提交!

在以上代码中,由于

update1()

是在

update2()

内部调用的,不是由代理对象来调用的,所以,执行

update2()

方法的过程大致上是:

开启事务
		执行update2()方法
				调用update1()方法
    因update1()方法抛出异常且符合回滚规则,执行回滚事务
若未出现回滚,则提交事务(本例会回滚,不会执行这一步)
           
  • 1
  • 2
  • 3
  • 4
  • 5

所以,回到我面试的那个题目,正确的答案应该是:只会在调用

update2()

方法时开启1个事务,内部调用的

update1()

根本不是事务性的(不管有没有

@Transactional

注解),既然只有1个事务,也就不存在事务的传播了!

其实,到这里,我的问题已经解决了,但是苍老师还帮我写好了后续的Demo代码,让我更深刻的理解,这可能就是老师的职业病吧,要么不讲,要讲就一讲到底。

接下来就要涉及更新

t_order

表的数据了,对应的业务接口和业务实现类分别是

OrderService

OrderServiceImpl

,关于

OrderService

接口:

package cn.tedu.service;

public interface OrderService {

    void updateSuccessfully();

}
           
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

关于

OrderServiceImpl

类:

package cn.tedu.service.impl;

import cn.tedu.mapper.OrderMapper;
import cn.tedu.service.OrderService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

@Service
public class OrderServiceImpl implements OrderService {

    private OrderMapper orderMapper;

    public OrderServiceImpl(OrderMapper orderMapper) {
        this.orderMapper = orderMapper;
    }

    // TODO-08:直接运行单元测试,以观察效果
    // TODO-09:启用以下@Transactional注解,并运行单元测试,以观察效果
    // TODO-10:启用以下注解的参数,并运行单元测试,以观察效果
    // @Transactional //(propagation = Propagation.REQUIRES_NEW)
    public void updateSuccessfully() {
        // 更新id=1的数据,会成功
        int rows;
        rows = orderMapper.updateNumberById(1, 1000000);
        if (rows != 1) {
            throw new RuntimeException("更新Order:id=1数据失败!");
        }
    }

}
           
  • 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

显然以上业务非常简单,就是成功的更新某条数据,在方法之前预先写好了注解和参数,稍后进行调节以观察效果。

另外,在

UserServiceImpl

类的

update2()

方法中,根据老师留下的注释调整后,有效代码为:

@Transactional
public void update2() {
  	// 更新id=2的数据,会成功
    int rows;
    rows = userMapper.updateUserNameById(2, "USER-1000002");
    if (rows != 1) {
       throw new RuntimeException("更新id=2数据失败!");
    }

  	// 调用另一个业务对象的更新方法
    orderService.updateSuccessfully();

  	// 更新id=2000000的数据,会失败
    rows = userMapper.updateUserNameById(2000000, "USER-1000002");
    if (rows != 1) {
       throw new RuntimeException("更新id=2000000数据失败!");
    }
}
           
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

所以,此时调用以上

update2()

方法,过程会是:

更新id=2的数据,会成功
调用另一个业务对象的更新方法,会成功
更新id=2000000的数据,会失败
           
  • 1
  • 2
  • 3

几次测试下来,结果如下:

OrderServiceImpl

updateSuccessfully()

方法上的注解状态
回滚状态
无注解 完全回滚

@Transactional

完全回滚

@Transactional(propagation = Propagation.REQUIRES_NEW)

t_user

表回滚,

t_order

表已提交

可以看到以上最后一次使用

@Transactional(propagation = Propagation.REQUIRES_NEW)

时,

OrderServiceImpl

中的

updateSuccessfully()

方法是运行在一个新的事务(配置的注解参数决定的)上的,由于这个

updateSuccessfully()

方法运行没有出错,就直接提交了,而

UserServiceImpl

中的

update2()

因为最后尝试更新id=2000000的数据会失败导致了回滚,所以就出现了

t_user

表回滚了,而

t_order

表提交了的现象,也就体现了事务的传播!

这一次也是在

update2()

中调用另一个事务性的方法,为什么就是有效的呢?是因为这次调用的是另一个对象的方法,而这个对象也是Spring的事务管理机制产生的代理对象,其执行过程大致是:

开启事务
		执行update2()方法(UserServiceImpl类的)
				更新id=2的数据,且成功
				
				开启新事务
						调用updateSuccessfully()方法(OrderServiceImpl类的)
				未出现回滚,则提交事务
				
				更新id=2000000的数据,且失败,执行回滚事务
若未出现回滚,则提交事务(本例会回滚,不会执行这一步)
           
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

最后,苍老师还给我留了个

TODO-Final

,代码很简单,就是在

update2()

方法里输出了一下

orderService

的类名:

// TODO-Final:启用接下来的这行代码,并运行单元测试,以观察效果
System.out.println(orderService.getClass());
           
  • 1
  • 2

然后,在控制台就可以清楚的看到这个代理对象:

class com.sun.proxy.$Proxy37
           
  • 1

注意:不能只输出

orderService

,必须是

orderService.getClass()

,因为代理对象重写了

toString()

方法,如果没有调用

getClass()

的话,看到的就会是

[email protected]

,你就看不出它是个代理对象了,这个家伙是不是很狡猾?

通过这个Demo,可以总结出这几点:

  • Spring管理事务是基于接口代理的;
  • 当前类的方法之间的调用,并不存在事务的传播,被调用的方法之前是否添加

    @Transactional

    注解对结果没有影响;
  • 不同类的方法之间的调用,对于被调用的方法,可以通过

    @Transactional

    注解的

    propagation

    属性来配置事务传播类型。

另外,还有个附加的收获,以前每次写业务层代码的时候,都是先写业务接口,再写实现类,为什么要有业务接口呢?一直以来我也没有深究过这个问题,只当是一种开发规范来遵守,现在看来意义不仅于此!

在执行单元测试的时候,我还故意的试了一下,如果将业务对象声明为

UserServiceImpl

这种类型,启动过程中就会提示自动装配失败,项目根本无法运行,必须声明成

UserService

这样的接口类型,另外,如果声明为

UserServiceImpl

类型,只要全程没有

@Transactional

注解,启动项目并不会报错,至于道理嘛,相信大家已经猜到了,我就不解释了。

名师博客    地址

长安紫薯    https://blog.csdn.net/nutony

程序媛泡泡    https://blog.csdn.net/weixin_43884234

闪耀太阳    https://blog.csdn.net/qq_16804847

军军编程    https://blog.csdn.net/smlzhang

Wanght6    https://wanght.blog.csdn.net

雨田说码    https://blog.csdn.net/maitian_2008

cgblpx    https://blog.csdn.net/u012932876