一、管理后台
1、分类管理
(1)功能概述
实现一个分类维护功能,来维护修改分类信息
(2)分类管理开发流程
(3)显示后台分类编码实现
声明持久层接口方法:
/**
* 查询所有商品类别
*
* @return 所有商品类别
*/
List<Category> selectAll();
持久层sql语句:
<!--查询所有商品类别-->
<select id="selectAll" resultMap="CategoryMapper">
select *
from category
where display = 1
order by order_num
</select>
创建VO
/**
* 商品类别视图对象 封装和前台页面交互的数据
*/
public class CategoryVO {
private Long id;
private String name;
省略...
}
创建service层:
/**
* 查询管理页面商品分类
* @return 管理页面商品分类
*/
List<CategoryVO> findHomepageCategories();
service层实现类:
@Autowired
private CategoryMapper categoryMapper;
@Override
public List<CategoryVO> findHomepageCategories() {
//查询所有商品类别
List<Category> categories = categoryMapper.selectAll();
//用stream快速构建一个新的集合
//把商品类别对象转换为商品视图对象(Entity->VO)
List<CategoryVO> categoryVOS = categories.stream()
.filter(Category::getDisplay)
.map(category -> new CategoryVO(category.getId(), category.getName()))
.collect(Collectors.toList());
return categoryVOS;
}
测试service层:
@Test
void findHomepageCategories() {
categoryService.findHomepageCategories().forEach(
categoryVO -> {
logger.info("CategoryVo:{}", categoryVO);
}
);
}
测试结果:
controller层:
@RestController
public class CategoryController {
private static final Logger logger = LoggerFactory.getLogger(CategoryController.class);
@Autowired
private CategoryService categoryService;
@GetMapping("/categories")
public List<CategoryVO> findHomePageCategories() {
logger.info("查询首页商品类别");
List<CategoryVO> categoryVOS = categoryService.findHomepageCategories();
return categoryVOS; // 以JSON格式返回数据
}
}
测试类:
@Test
void findHomePageCategories() {
String url = "/categories"; // 请求地址
String json = restTemplate.getForObject(url, String.class); // 发送GET请求 返回JSON字符串
logger.info("返回结果: {}", json);
}
测试效果:
2、删除分类功能
后台分类管理界面:在分类管理界面中可以查看所有分类信息,点击删除按钮对需要删除的分类进行操作。
分类控制器:在分类控制器中,需要编写相应的方法来处理删除分类的请求。该方法需要从前端接收要删除的分类 ID,并将请求传递给分类管理业务层。
分类管理业务层:在分类管理业务层中,需要编写相应的业务逻辑来处理删除分类的请求。
- 需要检查该分类是否存在,并且是否有与之相关的商品。
- 如果有相关商品,则需要提示管理员先删除商品,然后再删除该分类。
- 如果该分类没有相关商品,则需要将该分类从数据库中删除。
分类数据访问层:在分类数据访问层中,需要编写相应的 SQL 语句来实现删除分类的操作。该 SQL 语句需要根据分类 ID 来删除数据库中对应的记录。
mapper层
mapper接口方法:
/**
* 根据id删除商品类别
* @param id 商品类别id
* @return 返回受影响的行数
*/
int deleteById(Long id);
mapper.xml配置文件:
<!--根据id删除商品类别-->
<delete id="deleteById">
delete
from category
where id = #{id}
</delete>
测试代码:
@Test
@Transactional //事务操作 用于测试删除操作后回滚数据
void deleteById() {
int i = categoryMapper.deleteById(7L);
logger.info("删除了"+i+"行数据");
//断言
assertEquals(1,i,"删除失败");
}
测试效果:
service层
接口添加方法:
/**
* 根据id删除商品类别
* @param id 删除的id
*/
void deleteById(Long id);
实现类:
@Service
public class CategoryServiceImpl implements CategoryService {
@Autowired
private CategoryMapper categoryMapper;
@Override
public void deleteById(Long id) {
if (id == null || id < 1) {
throw new BlankParameterException("删除商品类别失败,商品类别id不合法");
}
//执行删除
int result = categoryMapper.deleteById(id);
if (result < 1) {
throw new EntityNotFoundException("删除商品类别失败,商品不存在");
}
}
}
测试类:
@Test
@Transactional
void deleteById() {
logger.info("删除id为5的商品类别");
categoryService.deleteById(5L);
//断言测试抛异常
assertThrows(BlankParameterException.class,()->categoryService.deleteById(null),"没有异常");
}
二、全局统一异常处理
统一异常处理
(1)统一异常处理概述
统一异常处理是一种软件开发的最佳实践,旨在捕获并处理程序运行时可能出现的异常和错误,以提高程序的可靠性和稳定性。
将异常和错误信息处理代码从主要业务逻辑中分离出来
(2)Spring MVC统一异常处理
这个功能可以使用两个注解:@RestControllerAdvice 和 @ControllerAdvice。
@ControllerAdvice 注解(已过时)
- 通常用于处理传统的 MVC 应用程序中的异常,例如使用 Thymeleaf、JSP 或者 FreeMarker 等视图技术构建的 Web 应用程序。
- @ControllerAdvice 注解中定义的异常处理方法可以返回 ModelAndView 对象,以实现跳转到错误页面的功能。
@RestControllerAdvice 注解
- 通常用于处理 RESTful 服务中的异常
- @RestControllerAdvice 注解中定义的异常处理方法则通常返回响应体,返回 JSON 格式的数据。
/**
* @document: 全局异常处理器
* 用于处理所有控制器遇到的☆各种☆异常
* @Author:SmallG
* @CreateTime:2023/9/7+14:47
*/
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(EntityNotFoundException.class) //声明异常
public String handleBlankEntityNotFoundException(EntityNotFoundException e) {
System.out.println("----------------");
return e.getMessage();
}
@ExceptionHandler(BlankParameterException.class) //声明异常
public String handleBlankParameterException(BlankParameterException e) {
return e.getMessage();
}
@ExceptionHandler(UserNotFoundException.class) //声明异常
public String handleUserNotFoundException(UserNotFoundException e) {
return e.getMessage();
}
}
三、RESTful
1、RESTful概述
RESTful是一种设计Web API的架构风格,它的核心思想是将Web资源作为唯一的标识符,并通过HTTP协议中的标准方法(例如GET、POST、PUT、DELETE)来实现对这些资源的操作。
具有优点:
- 松散耦合
- 可伸缩性
(1)RESTful编程建议
(2)RESTful URL风格
- 使用名词表示资源:RESTful API中的URL应该使用名词来表示资源,例如/users、/orders等等,这样可以使URL更加清晰和易于理解。
- 使用复数形式:URL中的名词应该使用复数形式,例如/users而不是/user,这是因为RESTful API中的URL通常表示一个资源的集合,而不是单个资源。
- 不使用动词:URL中不应该使用动词,因为HTTP方法已经表示了动作,例如,使用PUT方法来更新资源,而不是使用/update动词来表示更新操作。
- 避免使用大写字母:URL中的字母应该使用小写字母,以避免大小写不一致的问题。
- 使用层次结构来表示关系:当资源之间存在关系时,应该使用层次结构来表示,例如,/users/1234/orders表示用户1234的订单列表。
2、Spring MVC REST
(1)Spring MVC 对RESTful API提供的支持
- 支持HTTP请求方法:Spring MVC框架支持常见的HTTP方法,包括GET、POST、PUT、DELETE等等。通过使用@XXXMapping系列注解支持各种HTTP请求方法,如: @GetMapping注解支持HTTP GET方法、@PostMapping注解支持HTTP GET方法、@DeleteMapping注解支持HTTP DELETE方法等。
- 支持URI模板参数:Spring MVC框架支持URI模板参数,这使得可以在URL中使用变量占位符,例如/users/{id},其中{id}是一个变量占位符。通过使用@PathVariable注解,可以将URI中的变量占位符的值映射到控制器中的方法参数中。
- 支持消息转换:@RequestBody和@ResponseBody注解,可以将请求消息的JSON格式转换为Java对象,或将Java对象转换为响应消息的JSON格式。
- 支持返回状态码:在Spring MVC中,@ResponseStatus注解用于指定控制器方法的响应状态码和响应消息。如:@ResponseStatus(HttpStatus.NO_CONTENT)
- 支持异常处理:Spring MVC框架支持异常处理,可以捕获和处理应用程序中的异常,并返回相应的HTTP错误码和错误信息。通过使用@RestControllerAdvice注解,可以为应用程序中所有的控制器统一处理异常。
(2)@PathVariable
@PathVariable注解用于从URI路径中获取变量的值,禁止使用基本类型接收
在usermapper接口中添加删除用户方法:
/**
* 根据id删除用户
* @param id
* @return 返回受影响的行数
*/
int deleteById(Long id);
添加usermapper.xml文件中的sql语句:
<!--根据id删除用户-->
<delete id="deleteById">
delete
from user
where id = #{id}
</delete>
编写service层接口:
/**
* 根据用户id删除用户
* @param id
*/
void deleteUser(Long id);
编写service层实现类;
@Override
public void deleteUser(Long id) {
logger.info("删除用户id:{}", id);
if (id == null || id < 0) {
throw new BlankParameterException("用户id不合法");
}
int result = userMapper.deleteById(id);
if (result < 1) {
throw new UserNotFoundException("用户id不存在");
}
logger.info("成功删除了{}条数据",result);
}
cotroller层实现:
/**
* 删除用户
*
* @return
*/
@DeleteMapping("/users/{id}") //{id} 表示参数地址中的占位符
@ResponseStatus(HttpStatus.NO_CONTENT) //删除成功状态码是204
public String deleteUser(@PathVariable Long id) {
//参数中的id要和占位符的id保持一致
logger.info("控制层执行删除操作:id={}", id);
userService.deleteUser(id);
logger.info("控制层执行删除成功");
return "删除成功";
}
测试类:
@Test
void deleteUser() {
String url = "/users/15";
testRestTemplate.delete(url,String.class);
}
测试效果:
3、重构删除分类功能为REST风格
(1)重构统一异常处理返回错误状态码
- 200 OK:表示请求成功。
- 201 Created:表示请求已经被成功处理,并且在服务器上创建了新的资源。
- 204 No Content:表示请求成功,但是没有返回任何内容。
- 400 Bad Request:表示请求不合法,服务器无法处理该请求。
- 401 Unauthorized:表示请求需要用户认证。
- 403 Forbidden:表示请求被服务器拒绝,通常是因为用户没有权限执行该操作。
- 404 Not Found:表示请求的资源不存在。
- 405 Method Not Allowed:表示请求使用了不被支持的HTTP方法。
- 500 Internal Server Error:表示服务器内部错误。
(2)重构统一异常处理
修改全局异常处理器:
/**
* @document: 全局异常处理器
* 用于处理所有控制器遇到的☆各种☆异常
* @Author:SmallG
* @CreateTime:2023/9/7+14:47
*/
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(EntityNotFoundException.class) //声明异常
@ResponseStatus(HttpStatus.NOT_FOUND) //404
public String handleBlankEntityNotFoundException(EntityNotFoundException e) {
System.out.println("----------------");
return e.getMessage();
}
@ExceptionHandler(BlankParameterException.class) //声明异常
@ResponseStatus(HttpStatus.BAD_REQUEST) //400 请求不合法
public String handleBlankParameterException(BlankParameterException e) {
return e.getMessage();
}
@ExceptionHandler(UserNotFoundException.class) //声明异常
@ResponseStatus(HttpStatus.NOT_FOUND) //404
public String handleUserNotFoundException(UserNotFoundException e) {
return e.getMessage();
}
//处理自定义异常以外的异常
@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) //500 服务器错误
public String handlerException(Exception e){
return e.getMessage();
}
}
(3)重构CategoryController删除分类功能
/**
* 删除商品类别
*/
@DeleteMapping("/api/categories/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT) //204
public String deleteById(@PathVariable Long id) {
logger.info("根据商品类别id删除商品类别,id:{}", id);
categoryService.deleteById(id);
return "删除成功";
}
测试类:
@Test
void deleteById() {
//请求地址
String url = "/api/categories/{0}"; //{0}表示一个占位符
restTemplate.delete(url,5);
}
4、使用MockMVC测试
(1) TestRestTemplate 的事务问题
使用@Transactional注解可以将一个@Test方法标记为事务,使得该方法执行期间的所有数据库操作都在同一个事务中执行,测试结束后事务将被回滚,这样可以恢复测试对数据的影响。但是不能用于controller测试
(2)MockMvc测试
用于controller测试
使用步骤:
1、在测试类上添加@AutoConfigureMockMvc注解,以便自动配置MockMvc。
2、使用@Autowired注解将MockMvc注入到测试类中,以便在测试方法中使用MockMvc。
3、使用 MockMvcRequestBuilders 创建HTTP请求,使用MockMvc.perform方法执行HTTP请求。
4、使用MockMvcResultMatchers执行断言测试。
测试类:
//静态导入
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; //发送状态码的
//随机端口号
@SpringBootTest
@AutoConfigureMockMvc //自动配置,MockMvc
class CategoryControllerTest {
private static final Logger logger = LoggerFactory.getLogger(CategoryControllerTest.class);
@Autowired(required = false) //屏蔽错误 尽量不要加这个(required = false)
MockMvc mockMvc;
@Test
@Transactional
//事务回滚
void deleteById() throws Exception {
//请求地址
String url = "/api/categories/{0}"; //{0}表示一个占位符
String result = mockMvc.perform(delete(url, 5))
.andExpect(status().isNoContent()) //状态码
.andReturn().getResponse().getContentAsString(); //内容转换成字符串
logger.info("result:{}",result);
}
}
测试效果:
(3)MockMvcRequestBuilders
MockMvcRequestBuilders 是 Spring Framework 提供的一组工具类,用于创建 MockHttpServletRequestBuilder 对象,这些对象用于模拟 HTTP 请求,并在测试 Spring MVC 控制器时使用。
常用方法
- get(String urlTemplate, Object... urlVariables):创建一个 HTTP GET 请求
- post(String urlTemplate, Object... urlVariables):创建一个 HTTP POST 请求
- put(String urlTemplate, Object... urlVariables):创建一个 HTTP PUT 请求
- delete(String urlTemplate, Object... urlVariables):创建一个 HTTP DELETE 请求
(4)MockMvcResultMatchers
MockMvcResultMatchers 是 Spring Framework 提供的一组工具类,用于验证 Spring MVC 控制器的返回结果是否符合预期。它提供了许多方法,可用于验证返回结果的状态码、响应头、响应体等内容。
常用方法
- status():用于验证响应的状态码
- header(String name, String... values):用于验证响应头
- content():用于验证响应体
- jsonPath(String expression, Object... args):用于验证 JSON 响应体中的值是否符合预期
四、后台管理功能
1、增加分类
1、表单发送RESTAjax请求:前端表单发送Ajax请求到后端的Spring MVC控制器。
2、Spring MVC控制器处理接受CategoyeDTO数据:Spring MVC控制器接受前端传递的CategoryDTO数据。
3、业务层处理表单数据检查:业务层对表单数据进行检查,包括数据格式、数据类型、数据长度等等。如果数据有误,则返回错误信息给前端。
4、业务层将DTO转换为Category实体:业务层将DTO对象转换为Category实体对象,将表单数据存储到实体对象中。
5、Mapper负责存储数据:最后,Mapper负责将Category实体对象存储到数据库中。如果存储成功,则返回成功信息给前端。如果存储失败,则返回失败信息给前端。
mapper层添加方法
CategoryMapper接口添加方法
/**
* 新增商品类别
* @param category 商品类别实体类对象
* @return 受影响的行数
*/
int insertCategory(Category category);
添加sql
<!--新增商品类别-->
<insert id="insertCategory">
insert into category (name, display, order_num)
values (#{name}, #{display}, #{order_num})
</insert>
service层
首先创建DTO
/**
* @document: 商品分类数据访问对象 用于service接收controller层传来的数据
* @Author:SmallG
* @CreateTime:2023/9/7+22:20
*/
public class CategoryDTO {
private String name;
private Boolean display;
private Integer orderNum;
省略...
}
添加接口中的方法:
/**
* 新增商品类别
* @param categoryDTO 商品类别数据传输对象
*/
void addCategory(CategoryDTO categoryDTO);
实现方法:
@Override
public void addCategory(CategoryDTO categoryDTO) {
if (categoryDTO == null ||
categoryDTO.getName() == null ||
categoryDTO.getName().trim().isEmpty()) {
throw new BlankParameterException("添加商品类别失败,商品类别名不能为空");
}
//将DTO转化为Entity
Category category = new Category(null,
categoryDTO.getName(),
categoryDTO.getDisplay(),
categoryDTO.getOrderNum(),
null, null);
//保存商品类别信息
int result = categoryMapper.insertCategory(category);
if (result<1){
throw new EntityNotFoundException("添加商品失败");
}
}
controller层
/**
* 添加商品类别
* @return
*/
@PostMapping("/api/categories")
@ResponseStatus(HttpStatus.CREATED) //201 新增成功
//保存数据,参数要保存在请求体中,所以使用@RequestBody,获取请求体中的数据
public String addCategory(@RequestBody CategoryDTO categoryDTO){
logger.info("控制层执行操作:添加商品类别");
categoryService.addCategory(categoryDTO);
logger.info("控制层添加商品类别成功");
return "添加成功";
}
2、后台显示轮播图功能
创建实体类
/**
* @document: 轮播图实体类 对应数据库表banner
* @Author:SmallG
* @CreateTime:2023/9/8+9:09
*/
public class Banner {
private Long id;
private String image; //图片路径+图片名
private Integer orderNum;
private LocalDateTime created; //创建时间
private LocalDateTime updated;
...
}
创建持久层
@Mapper
public interface BannerMapper {
/**
* 查询所有轮播图
* @return 所有轮播图
*/
List<Banner> selectAll();
}
sql编码
<?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="cn.highedu.coolsharkhub.mapper.BannerMapper">
<resultMap id="BannerMap" type="cn.highedu.coolsharkhub.pojo.entity.Banner">
<id column="id" property="id"></id>
<result column="name" property="name"></result>
<result column="order_num" property="orderNum"></result>
<result column="created" property="created"></result>
<result column="updated" property="updated"></result>
</resultMap>
<!--查询所有轮播图-->
<select id="selectAll" resultMap="BannerMap">
SELECT *
FROM banner
ORDER BY order_num
</select>
</mapper>
创建业务层
创建VO和视图层进行数据交互
public class BannerVO {
private Long id;
private String image;
private Integer orderNum;
...
}
service层接口:
public interface BannerService {
/**
* 查询所有轮播图
*
* @return
*/
List<BannerVO> findAll();
}
实现类:
@Service
public class BannerServiceImpl implements BannerService {
private static final Logger logger = LoggerFactory.getLogger(BannerServiceImpl.class);
@Autowired
BannerMapper bannerMapper;
@Override
public List<BannerVO> findAll() {
//查询所有轮播图对象
List<Banner> banners = bannerMapper.selectAll();
//数据转换
List<BannerVO> bannerVOS = banners.stream().map(banner -> new BannerVO(
banner.getId(), banner.getImage(), banner.getOrderNum()
)).collect(Collectors.toList());
return bannerVOS;
}
}
创建控制层
@RestController
public class BannerController {
private static final Logger logger = LoggerFactory.getLogger(BannerController.class);
@Autowired
private BannerService bannerService;
/**
* 查询所有轮播图
*
* @return
*/
@GetMapping("/api/banners")
public List<BannerVO> findAll() {
logger.info("控制层查询所有轮播图");
List<BannerVO> bannerVOS = bannerService.findAll();
return bannerVOS;
}
}
测试类:
//静态导入
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; //发送状态码的
/**
* @document:
* @Author:SmallG
* @CreateTime:2023/9/8+10:11
*/
@SpringBootTest
@AutoConfigureMockMvc
class BannerControllerTest {
private static final Logger logger = LoggerFactory.getLogger(BannerControllerTest.class);
@Autowired(required = false)
MockMvc mockMvc;
@Test
void findAll() throws Exception {
String url = "/api/banners";
//发送请求
String json = mockMvc.perform(get(url)).andExpect(status().isOk())
.andReturn().getResponse().getContentAsString(); // 状态码200;
logger.info("轮播图数据:{}", json);
}
}
测试效果: