10. 基于Spring JDBC的事务管理(续)

当需要方法是事务性的,可以使用@Transactional注解,此注解可以添加在:

  • 接口

    • 会使得此接口的实现类的所有实现方法都是事务性的
  • 接口中的抽象方法上
    • 会使得此接口的实现类中,重写的此方法是事务性的
    • 只作用于当前方法
    • 如果接口上也配置了此注解,并且接口和抽象方法的注解均配置了参数,以方法上的配置为准
  • 业务实现类
    • 会使得当前类中所有重写的方法都是事务性

      • 自定义的方法不会是事务性的
  • 业务实现类中的方法
    • 不可以添加在自定义的(不是重写的接口的)方法上

      • 语法上,可以添加,但执行时,不允许

Spring JDBC是通过接口代理的方式进行事务管理,所以,只对接口中声明的方法有效!

通常,应该将@Transactional添加在接口中的抽象方法上(如果偷懒,或为了避免遗漏,也可以直接添加在接口上)。

目前,由于csmall-product-service没有添加相关依赖,所以,并不能直接在接口中使用@Transactional注解(因为尚不可识别),则应该在此Module中添加依赖:

<!-- Mybatis Spring Boot:Mybatis及对Spring Boot的支持 -->
<!-- 仅需要保留spring-jdbc,使得业务接口可以使用@Transactional注解 -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<exclusions>
<exclusion>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-autoconfigure</artifactId>
</exclusion>
<exclusion>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
</exclusion>
<exclusion>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
</exclusion>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</exclusion>
<exclusion>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
</exclusion>
</exclusions>
</dependency>

11. 类别管理--添加类别--业务逻辑层(续)

目前,在业务实现中,视为“错误”时始终抛出ServiceException,且没有任何异常信息,是不合理的!

在略大规模的项目中,“错误”的种类可能较大,如果为每一种“错误”都创建一个对应的异常,则需要创建的异常类型就比较多,但是,这些异常类除了名称不一样以外,几乎没有不同,所以,存在不利于管理和维护的问题。

其实,也可以只使用1个异常类型(或者少量异常类型),但是,每次抛出时,也需要明确的表示“是哪一种错误”,则可以在异常类型中添加“业务状态码”。

则首先需要业务状态码的类型,可以从前缀项目中复制State文件,例如:

package cn.tedu.csmall.common.web;

public enum State {

    OK(20000),
ERR_CATEGORY_NAME_DUPLICATE(40100), // 客户端引起的--类别--名称冲突(被占用)
ERR_CATEGORY_NOT_FOUND(40101), // 客户端引起的--类别--数据不存在(查询参数值不正确)
ERR_INSERT(50000), // 服务端引起的--插入数据错误
ERR_UPDATE(50001); // 服务端引起的--更新数据错误 private Integer value; State(Integer value) {
this.value = value;
} public Integer getValue() {
return value;
} }

然后,在ServiceException中,自定义构造方法,强制要求传入State stateString message参数,并且,为State类型参数提供公有的获取值的方法:

public class ServiceException extends RuntimeException {
private State state; public ServiceException(State state, String message) {
super(message);
if (state == null) {
throw new IllegalArgumentException("使用ServiceException必须指定错误时的业务状态码!");
}
this.state = state;
} public State getState() {
return state;
}
}

在后续抛出异常时,应该传入State stateString message

throw new ServiceException(State.ERR_CATEGORY_NOT_FOUND, "添加类别失败,父级类别不存在!");

完成后,应该再次执行测试,以保证修改代码后,原有代码依然能正确运行。

12. 类别管理--添加类别--控制器层

12.1. 处理跨域(一次性配置)

csmall-product-webapi的根包下config包下创建SpringMvcConfiguration类,实现WebMvcConfigururer接口,重写其中的方法,以解决跨域问题:

package cn.tedu.csmall.product.webapi.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @Configuration
public class SpringMvcConfiguration implements WebMvcConfigurer { @Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOriginPatterns("*")
.allowedMethods("*")
.allowedHeaders("*")
.allowCredentials(true)
.maxAge(3600);
} }

提示:以上代码从此前的案例中复制过来即可。

12.2. 控制器类与处理请求的方法

先将此前项目中的JsonResult复制到csmall-common的根包下的web子包中(需要在csmall-common中补充依赖lombok)。

csmall-product-webapi的根包下创建controller.CategoryController类,在类上添加@RestController@RequestMapping(value = "/categories", produces = "application/json; charset=utf-8")这2个注解:

@RestController
@RequestMapping(value = "/categories", produces = "application/json; charset=utf-8")
public class CategoryController { }

然后,在类中添加处理请求的方法:

@Autowired
private ICategoryService categoryService; @PostMapping("/add-new")
public JsonResult<Void> addNew(CategoryAddNewDTO categoryAddNewDTO) {
categoryService.addNew(categoryAddNewDTO);
return JsonResult.ok();
}

12.3. 控制器层测试

csmall-product-webapi的测试的根包下创建controller.CategoryControllerTests测试类,编写并执行测试:

@SpringBootTest
@AutoConfigureMockMvc
public class CategoryControllerTests { @Autowired
MockMvc mockMvc; @Test
@Sql("classpath:truncate.sql")
public void testAddNewSuccessfully() throws Exception {
// 准备测试数据,不需要封装,应该全部声明为String类型
String name = "水果";
String parentId = "0"; // 即使目标类型是Long,参数值也不要加L
String keywords = "水果的关键字是啥";
String sort = "66";
String icon = "图标待定";
String isDisplay = "1";
// 请求路径,不需要写协议、服务器主机和端口号
String url = "/categories/add-new";
// 执行测试
// 以下代码相对比较固定
mockMvc.perform( // 执行发出请求
MockMvcRequestBuilders.post(url) // 根据请求方式决定调用的方法
.contentType(MediaType.APPLICATION_FORM_URLENCODED) // 请求数据的文档类型,例如:application/json; charset=utf-8
.param("name", name) // 请求参数,有多个时,多次调用param()方法
.param("parentId", parentId)
.param("keywords", keywords)
.param("icon", icon)
.param("sort", sort)
.param("isDisplay", isDisplay)
.accept(MediaType.APPLICATION_JSON)) // 接收的响应结果的文档类型,注意:perform()方法到此结束
.andExpect( // 预判结果,类似断言
MockMvcResultMatchers
.jsonPath("state") // 预判响应的JSON结果中将有名为state的属性
.value(20000)) // 预判响应的JSON结果中名为state的属性的值,注意:andExpect()方法到此结束
.andDo( // 需要执行某任务
MockMvcResultHandlers.print()); // 打印日志
} }

12.4. 处理异常

csmall-common中添加依赖项:

<!-- Spring Boot Web:支持Spring MVC -->
<!-- 需要使用到@RestControllerAdvice等注解 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</exclusion>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-json</artifactId>
</exclusion>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>

csmall-common根包下的ex包下创建handler.GlobalExceptionHandler,并在此类中处理异常:

@RestControllerAdvice
public class GlobalExceptionHandler { @ExceptionHandler(ServiceException.class)
public JsonResult<Void> handleServiceException(ServiceException ex) {
return JsonResult.fail(ex.getState(), ex.getMessage());
} }

完成后,使用错误的测试数据时,会发现根本不会处理异常,是因为在csmall-product-webapi中默认执行的组件扫描不会扫描到以上GlobalExceptionHandler所在的包,为了解决此问题,应该先在csmall-common的根包下创建config.CsmallCommonConfiguration类,此类应该是配置类,且配置组件扫描:

package cn.tedu.csmall.common.config;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration; @Configuration
@ComponentScan("cn.tedu.csmall.common.ex.handler")
public class CsmallCommonConfiguration {
}

然后,在csmall-product-webapi的启动类引用此配置类:

package cn.tedu.csmall.product.webapi;

@SpringBootApplication
@Import({CsmallCommonConfiguration.class}) // 新增
public class CsmallProductWebapiApplication { public static void main(String[] args) {
SpringApplication.run(CsmallProductWebapiApplication.class, args);
} }

接下来,即可执行测试:

@Test
@Sql({"classpath:truncate.sql", "classpath:insert_data.sql"})
public void testAddNewFailBecauseNameDuplicate() throws Exception {
// 准备测试数据,不需要封装,应该全部声明为String类型
String name = "类别001";
String parentId = "0"; // 即使目标类型是Long,参数值也不要加L
String keywords = "水果的关键字是啥";
String sort = "66";
String icon = "图标待定";
String isDisplay = "1";
// 请求路径,不需要写协议、服务器主机和端口号
String url = "/categories/add-new";
// 执行测试
// 以下代码相对比较固定
mockMvc.perform( // 执行发出请求
MockMvcRequestBuilders.post(url) // 根据请求方式决定调用的方法
.contentType(MediaType.APPLICATION_FORM_URLENCODED) // 请求数据的文档类型,例如:application/json; charset=utf-8
.param("name", name) // 请求参数,有多个时,多次调用param()方法
.param("parentId", parentId)
.param("keywords", keywords)
.param("icon", icon)
.param("sort", sort)
.param("isDisplay", isDisplay)
.accept(MediaType.APPLICATION_JSON)) // 接收的响应结果的文档类型,注意:perform()方法到此结束
.andExpect( // 预判结果,类似断言
MockMvcResultMatchers
.jsonPath("state") // 预判响应的JSON结果中将有名为state的属性
.value(State.ERR_CATEGORY_NAME_DUPLICATE.getValue())) // 预判响应的JSON结果中名为state的属性的值,注意:andExpect()方法到此结束
.andDo( // 需要执行某任务
MockMvcResultHandlers.print()); // 打印日志
} @Test
@Sql({"classpath:truncate.sql"})
public void testAddNewFailBecauseParentNotFound() throws Exception {
// 准备测试数据,不需要封装,应该全部声明为String类型
String name = "类别001";
String parentId = "-1"; // 即使目标类型是Long,参数值也不要加L
String keywords = "水果的关键字是啥";
String sort = "66";
String icon = "图标待定";
String isDisplay = "1";
// 请求路径,不需要写协议、服务器主机和端口号
String url = "/categories/add-new";
// 执行测试
// 以下代码相对比较固定
mockMvc.perform( // 执行发出请求
MockMvcRequestBuilders.post(url) // 根据请求方式决定调用的方法
.contentType(MediaType.APPLICATION_FORM_URLENCODED) // 请求数据的文档类型,例如:application/json; charset=utf-8
.param("name", name) // 请求参数,有多个时,多次调用param()方法
.param("parentId", parentId)
.param("keywords", keywords)
.param("icon", icon)
.param("sort", sort)
.param("isDisplay", isDisplay)
.accept(MediaType.APPLICATION_JSON)) // 接收的响应结果的文档类型,注意:perform()方法到此结束
.andExpect( // 预判结果,类似断言
MockMvcResultMatchers
.jsonPath("state") // 预判响应的JSON结果中将有名为state的属性
.value(State.ERR_CATEGORY_NOT_FOUND.getValue())) // 预判响应的JSON结果中名为state的属性的值,注意:andExpect()方法到此结束
.andDo( // 需要执行某任务
MockMvcResultHandlers.print()); // 打印日志
}

12.5. 验证请求参数格式的基本有效性

关于Validation框架的基本使用:

  • 添加依赖
  • 在控制器类中处理请求的方法的被验证的参数(封装的对象)之前添加@Validated / @Valid
  • 在参数的类型(封装的类型)的属性之前添加验证注解
  • 在统一处理异常的类中对BindException进行处理

先在csmall-product-webapi中添加依赖(如果已经添加,则不需要重复添加):

<!-- Spring Boot Validation:验证请求参数的基本格式 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>

CategoryController处理请求的方法的参数之前添加@Validated / @Valid注解:

@PostMapping("/add-new")
// ===== 在以下方法的参数前添加@Validated / @Valid注解 =====
public JsonResult<Void> addNew(@Validated CategoryAddNewDTO categoryAddNewDTO) {
categoryService.addNew(categoryAddNewDTO);
return JsonResult.ok();
}

由于CategoryAddNewDTO等类在csmall-pojo模块中的,要在此类中添加@NotNull等注解,则必须在csmall-pojo中添加依赖:

<!-- Spring Boot Validation:验证请求参数的基本格式 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
<scope>provided</scope>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</exclusion>
<exclusion>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-el</artifactId>
</exclusion>
</exclusions>
</dependency>

然后,CategoryAddNewDTOname属性上添加@NotNull约束(其它的约束等到测试通过之后再补充):

@Data
public class CategoryAddNewDTO implements Serializable { @NotNull(message = "添加类别失败,必须填写类别名称!") // 新增
private String name; // ===== 其它原有代码 ===== }

State中添加对应“请求参数格式错误”的枚举值:

public enum State {

    OK(20000),
// ===== 下行为新增 ======
ERR_BAD_REQUEST(40000), // 客户端引起的--请求参数格式错误 // ===== 其它原有代码 =====
}

GlobalExceptionHandler中添加新的处理异常的方法:

@ExceptionHandler(BindException.class)
public JsonResult<Void> handleBindException(BindException ex) {
List<FieldError> fieldErrors = ex.getBindingResult().getFieldErrors();
StringBuilder stringBuilder = new StringBuilder();
for (FieldError fieldError : fieldErrors) {
stringBuilder.append(";");
stringBuilder.append(fieldError.getDefaultMessage());
}
String message = stringBuilder.substring(1);
return JsonResult.fail(State.ERR_BAD_REQUEST, message);
}

最后,添加测试:

@Test
@Sql({"classpath:truncate.sql"})
public void testAddNewFailBecauseBadRequest() throws Exception {
// 准备测试数据,注意:此次没有提交必要的name属性值
String parentId = "0"; // 即使目标类型是Long,参数值也不要加L
String keywords = "水果的关键字是啥";
String sort = "66";
String icon = "图标待定";
String isDisplay = "1";
// 请求路径,不需要写协议、服务器主机和端口号
String url = "/categories/add-new";
// 执行测试
// 以下代码相对比较固定
mockMvc.perform( // 执行发出请求
MockMvcRequestBuilders.post(url) // 根据请求方式决定调用的方法
.contentType(MediaType.APPLICATION_FORM_URLENCODED) // 请求数据的文档类型,例如:application/json; charset=utf-8
// .param("name", name) // 注意:此处不提交必要的name属性
.param("parentId", parentId)
.param("keywords", keywords)
.param("icon", icon)
.param("sort", sort)
.param("isDisplay", isDisplay)
.accept(MediaType.APPLICATION_JSON)) // 接收的响应结果的文档类型,注意:perform()方法到此结束
.andExpect( // 预判结果,类似断言
MockMvcResultMatchers
.jsonPath("state") // 预判响应的JSON结果中将有名为state的属性
.value(State.ERR_BAD_REQUEST.getValue())) // 预判响应的JSON结果中名为state的属性的值,注意:andExpect()方法到此结束
.andDo( // 需要执行某任务
MockMvcResultHandlers.print()); // 打印日志
}

测试成功后,应该在CategoryAddNewDTO的各属性中补充更多的、必要的注解进行约束,并且,添加更多约束后,还应该编写更多的测试。

13. 类别管理--根据父级类别查询其所有子级类别--持久层

13.1. 规划SQL语句

本次需要执行的SQL语句大致是:

select * from pms_category where parent_id=? and enable=1 and is_display=1 order by sort desc, gmt_modified desc;

关于字段列表,应该包括:

id, name, sort, icon, is_parent

13.2. 抽象方法(可能需要创建VO类)

csmall-pojo的根包下的vo包下创建CategorySimpleListItemVO类,封装以上设计的5个字段对应的属性:


CategoryMapper接口中添加:

List<CategorySimpleListItemVO> listByParentId(Long parentId);

13.3. 在XML中配置SQL

CategoryMapper.xml中添加配置:

<select id="listByParentId" resultMap="SimpleListResultMap">
select
<include refid="SimpleListQueryFileds" />
from
......
</select> <sql id="SimpleListQueryFileds">
<if test="true">
id, name, sort, icon, is_parent
</if>
</sql> <resultMap id="SimpleListResultMap" type="xx.xx.xx.CategorySimpleListItemVO">
<id column="id" property="id" />
<result ......
</resultMap>

13.4. 测试

本次测试推荐使用人工检查查询结果。

14. 类别管理--根据父级类别查询其所有子级类别--业务逻辑层

14.1. 接口和抽象方法

ICategoryService中添加:

List<CategorySimpleListItemVO> listByParentId(Long parentId);

14.2. 实现

CategoryServiceImpl中直接调用categoryMapper执行查询并返回即可。

14.3. 测试

与持久层测试类似。

15. 类别管理--根据父级类别查询其所有子级类别--控制器层

CategoryController中添加:

@GetMapping("/list-by-parent")
public JsonResult<List<CategorySimpleListItemVO>> listByParentId(Long parentId) {
// 调用service并将结果封装到JsonResult中
}

CategoryControllerTests中测试:


4-9 基于Spring JDBC的事务管理(续)的更多相关文章

  1. Spring声明式事务管理基于@Transactional注解

    概述:我们已知道Spring声明式事务管理有两种常用的方式,一种是基于tx/aop命名空间的xml配置文件,另一种则是基于@Transactional 注解.         第一种方式我已在上文为大 ...

  2. spring声明式事务管理方式( 基于tx和aop名字空间的xml配置+@Transactional注解)

    1. 声明式事务管理分类 声明式事务管理也有两种常用的方式, 一种是基于tx和aop名字空间的xml配置文件,另一种就是基于@Transactional注解. 显然基于注解的方式更简单易用,更清爽. ...

  3. Spring声明式事务管理(基于注解方式实现)

    ----------------------siwuxie095                                 Spring 声明式事务管理(基于注解方式实现)         以转 ...

  4. Spring声明式事务管理(基于XML方式实现)

    --------------------siwuxie095                             Spring 声明式事务管理(基于 XML 方式实现)         以转账为例 ...

  5. Spring入门6事务管理2 基于Annotation方式的声明式事务管理机制

    Spring入门6事务管理2 基于Annotation方式的声明式事务管理机制 201311.27 代码下载 链接: http://pan.baidu.com/s/1kYc6c 密码: 233t 前言 ...

  6. Spring声明式事务管理基于tx/aop命名空间

    目的:通过Spring AOP 实现Spring声明式事务管理; Spring支持编程式事务管理和声明式事务管理两种方式. 而声明式事务管理也有两种常用的方式,一种是基于tx/aop命名空间的xml配 ...

  7. Spring框架——JDBC与事务管理

    JDBC JDBCTemplate简介 XML配置JDBCTemplate 简化JDBC模板查询 事务管理 事务简介 Spring中的事务管理器 Spring中的事务管理器的不同实现 用事务通知声明式 ...

  8. Spring学习8-Spring事务管理(AOP/声明式式事务管理)

    一.基础知识普及 声明式事务的事务属性: 一:传播行为 二:隔离级别 三:只读提示 四:事务超时间隔 五:异常:指定除去RuntimeException其他回滚异常.  传播行为: 所谓事务的传播行为 ...

  9. spring深入学习(五)-----spring dao、事务管理

    访问数据库基本是所有java web项目必备的,不论是oracle.mysql,或者是nosql,肯定需要和数据库打交道.一开始学java的时候,肯定是以jdbc为基础,如下: private sta ...

随机推荐

  1. CoreWCF 1.0.0 发布,微软正式支持WCF

    2022年4月28日,我们达到了一个重要的里程碑,并发布了CoreWCF的1.0.0版本.对Matt Connew (微软WCF团队成员)来说,这是5年前即 2017年1月开始的漫长旅程的结束.Mat ...

  2. 题解0011:图书管理(哈希、vector)

    信奥一本通--哈希 里的例题2 题目链接:http://ybt.ssoier.cn:8088/problem_show.php?pid=1456 题目描述:两个命令,一个是进一本名字为s的图书,一个是 ...

  3. js项目案例

    2021.04.12 --mouseover抖动情况之一

  4. TS 自学笔记(一)

    TS 自学笔记(一) 本文写于 2020 年 5 月 6 日 日常废话两句 有几天没有更新了,最近学的比较乱,休息了两天感觉好一些了.这两天玩了几个设计软件,过几天也写篇文章分享分享. 为啥要学 TS ...

  5. TinyMCE简介

    TinyMCE是一款开源.易用.UI时新的富文本编辑器. 插件丰富,自带插件基本满足要求 可扩展性强,可自定义功能 界面好看,符合现代审美 提供经典.内联.沉浸无干扰三种模式 官网:https://w ...

  6. SpringBoot官方支持任务调度框架,轻量级用起来也挺香!

    大家好,我是二哥呀.定时任务的应用场景其实蛮常见的,比如说: 数据备份 订单未支付则自动取消 定时爬取数据 定时推送信息 定时发布文章 等等(想不出来了,只能等等来凑,,反正只要等的都需要定时,怎么样 ...

  7. anaconda遇到:Solving environment: failed with initial frozen solve. Retrying with flexible solve.问题

    Solving environment: failed with initial frozen solve. Retrying with flexible solve. 遇到上述问题: 解决方案: # ...

  8. SpringCloud微服务实战——搭建企业级开发框架(四十一):扩展JustAuth+SpringSecurity+Vue实现多租户系统微信扫码、钉钉扫码等第三方登录

      前面我们详细介绍了SSO.OAuth2的定义和实现原理,也举例说明了如何在微服务框架中使用spring-security-oauth2实现单点登录授权服务器和单点登录客户端.目前很多平台都提供了单 ...

  9. docker-compose 搭建 Prometheus+Grafana监控系统

    有关监控选型之前有写过一篇文章: 监控系统选型,一文轻松搞定! 监控对象 Linux服务器 Docker Redis MySQL 数据采集 1).prometheus: 采集数据 2).node-ex ...

  10. 1.2 操作系统的第二个功能——并发功能 -《zobolの操作系统学习札记》

    1.2 操作系统的第二个功能--并发功能 目录 1.2 操作系统的第二个功能--并发功能 问1:什么是并发功能?并发功能是必要的吗? 问2:并发功能必须要求拥有多核CPU吗? 问3:多核CPU和单核C ...