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. 4.18-token验证

    在postman编写的每一个叫测试用例,既然收测试用例,那么就会有结果对比 API测试断言tests(判断一个接口测试用例是否成功,或者说是通过,是根据断言的三个条件都成立的情况下得到的结果) 协议状 ...

  2. [AcWing 796] 子矩阵的和

    点击查看代码 #include<iostream> using namespace std; const int N = 1e3 + 10; int a[N][N], s[N][N]; i ...

  3. Docker系列教程02-操作Docker容器

    简介 通过前面的学习,相信您已经对镜像有所了解,是时候学习容器了. 容器是Docker的另一个核心概念.简单来说,容器是镜像的一个运行实例.正如从虚拟机模板上启动VM一样,用户也同样可以从单个镜像上启 ...

  4. 详解六大国产CPU处理器

    一个执着于技术的公众号 CPU作为计算机设备的运算和控制核心,负责指令读取.译码与执行,因研发门槛高.生态构建难,被认为是集成电路产业中的"珠穆朗玛峰". 纵观全球,Intel.A ...

  5. CVE-2021-35042

    CVE-2021-35042 漏洞介绍 Django 是 Python 语言驱动的一个开源模型-视图-控制器(MVC)风格的 Web 应用程序框架. 漏洞影响版本:django 3.1.3.2 202 ...

  6. python之三元表达式与生成式与匿名与内置函数(部分)

    目录 三元表达式 各种生成式 列表生成式(可同样作用于集合) 字典生成式 匿名函数 重要内置函数 map() zip() filter() reduce() 常见内置函数(部分) 三元表达式 三元表达 ...

  7. OpenWrt 20.02.2 小米路由器3G配置CP1025网络打印

    家里的施乐 CP116w 工作快五年了终于罢工了. 黑粉报错, 自己也不会拆, 只能搁置了. 后来换了个 HP CP1025. 这个打印机也不错, 墨盒便宜没什么废粉, 就是启动慢一点, 而且 -- ...

  8. ACM 刷题记录

    HDU Multi-University Training Contest 题目来源 题目 知识点 时间复杂度 完成情况 2019 Contest8 A Acesrc and Cube Hyperne ...

  9. 33个非常实用的JavaScript一行代码

    33个非常实用的JavaScript一行代码 一.日期处理 1. 检察日期是否有效 该方法用于检测给出的日期是否有效: const isDateValid = (...val) => !Numb ...

  10. Lombok - 快速入门

    1. val 自动识别循环变量类型 本地变量和foreach循环可用. import java.util.ArrayList; import java.util.HashMap; import lom ...