在牛某网看见了牛肉哥的帖子之后,打算向牛肉大佬学习,故开始书写优快云博客,通过博客的方式来巩固自身知识学习。
因为之前有粗略的学习了Java Web的基础课程,所以博客内容主要是巩固之前学习当中的模糊点,以及一些自己认为重要的内容,用于自己进一步的掌握开发技能。
课程内容:
-
公共字段自动填充
-
新增菜品
-
菜品分页查询
-
删除菜品
-
修改菜品
公共字段自动填充:
在每一次的创建与更新操作中,总会产生以下的一些步骤:
setCreateTime(LocalDateTime.now());
setUpdateTime(LocalDateTime.now());setCreateUser(BaseContext.getCurrentId());
setUpdateUser(BaseContext.getCurrentId());
那么有没有什么方法可以来简化这些共有的操作呢?-------------------AOP切面编程,实现功能增强,来完成公共字段自动填充功能。
实现步骤:
1). 自定义注解 AutoFill,用于标识需要进行公共字段自动填充的方法
2). 自定义切面类 AutoFillAspect,统一拦截加入了 AutoFill 注解的方法,通过反射为公共字段赋值
3). 在 Mapper 的方法上加入 AutoFill 注解
新建一个注解类:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AutoFill {
//数据库操作类型:UPDATE INSERT
OperationType value();
}
注意点:
- Target......:这是一个元注解:用于指定自定义注解可以用在什么地方,整体表示:只能用在方法上
- Retention...:这也是一个元注解,用于指定注解的保留策略,整体表示:在程序运行时仍然存在,故可以通过反射来获取方法上的这个注解信息。
- OperationType value():是自定义注解的属性,名称是value,类型是OperationType枚举:里面是更新与插入。
新建一个切面类:
@Slf4j
@Component
@Aspect
public class AutoFillAspect {
/**
* 切入点:标识需要进行自动填充处理的方法
*/
@Pointcut("execution(* com.sky.mapper.*.*(..)) && @annotation(com.sky.annotation.AutoFill)")
public void autoFillPointcut() {}
/**
* 前置通知:在目标方法执行前进行自动填充处理
*/
@Before("autoFillPointcut()")
public void autoFillBefore(JoinPoint joinPoint) {
log.info("自动填充开始");
}
}
注意点:
- 切入点:告诉Spring AOP 框架:哪些方法是我们需要 “拦截” 并进行额外处理(自动填充)的目标。
-
com.sky.mapper.*: 表示匹配com.sky.mapper包下的任意类。 -
.*:该类下的任何方法
-
(. .):匹配方法的任意参数列表
-
@annotation(com.sky.annotation.AutoFill):这个部分会匹配到所有带有@AutoFill注解的方法。 -
前置通知:Spring AOP 会在目标方法执行之前(在切入点匹配到时,执行之前)调用这个通知方法。
-
autoFillPointcut():这是一个引用,它告诉 Spring,这个通知要应用autoFillPointcut()切入点所匹配的那些方法上。
完善代码:
@Before("autoFillPointcut()")
public void autoFillBefore(JoinPoint joinPoint) {
log.info("自动填充开始");
//获取到当前被拦截的方法上的数据库操作类型
MethodSignature signature = (MethodSignature) joinPoint.getSignature();//获取到当前被拦截的方法的签名对象
AutoFill autoFill = signature.getMethod().getAnnotation(AutoFill.class);//获取到当前被拦截的方法上的AutoFill注解对象
OperationType operationType = autoFill.value();//获得注解里的value:即数据库操作类型
//获取到当前被拦截的方法的参数--实体对象
Object[] args = joinPoint.getArgs();
if (args == null || args.length == 0) {
return;
}
Object entity = args[0];
//准备赋值的数据
LocalDateTime now = LocalDateTime.now();
Long currentId = BaseContext.getCurrentId();
//根据当前不同的操作类型,为对应的属性通过反射来赋值
if (operationType == OperationType.INSERT) {
//为4个公共字段赋值
try {
Method setCreateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_TIME, LocalDateTime.class);
Method setCreateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_USER, Long.class);
Method setUpdateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_TIME, LocalDateTime.class);
Method setUpdateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_USER, Long.class);
//通过反射为对象属性赋值
setCreateTime.invoke(entity, now);
setCreateUser.invoke(entity, currentId);
setUpdateTime.invoke(entity, now);
setUpdateUser.invoke(entity, currentId);
} catch (Exception e) {
e.printStackTrace();
}
}else if(operationType == OperationType.UPDATE){
try {
Method setUpdateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_TIME, LocalDateTime.class);
Method setUpdateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_USER, Long.class);
//通过反射为对象属性赋值
setUpdateTime.invoke(entity, now);
setUpdateUser.invoke(entity, currentId);
} catch (Exception e) {
e.printStackTrace();
}
}
}
注意点:
- 要实现公共字段自动填充功能:主要是三步
- 获取到当前被拦截的方法上的数据库操作类型--从注解上获取
- 获取到当前被拦截的方法的参数--实体对象--从传入参数里获取
- 准备赋值的数据
- 根据当前不同的操作类型,为对应的属性通过反射来赋值
MethodSignature:方法签名的子类,专门用于获取方法的注解、返回值类型等;- Mapper 方法的参数是实体对象(如
(Employee employee)),因此直接取第一个参数即可 getDeclaredMethod(methodName, paramTypes):获取类中声明的方法(包括私有方法),需要指定方法名和参数类型;method.invoke(entity, now):调用方法,第一个参数是 “要操作的对象”,后面是方法的参数。-
新增菜品:
文件上传(菜品图片):
文件上传:是指将本地图片、视频、音频等文件上传到服务器上,可以供其他用户浏览或下载的过程。
定义OSS相关配置:
在application.yml中利用${}型,而在application-dev.yml中填写数据。
sky:
alioss:
endpoint: ${sky.alioss.endpoint}
access-key-id: ${sky.alioss.access-key-id}
access-key-secret: ${sky.alioss.access-key-secret}
bucket-name: ${sky.alioss.bucket-name}
@Component
@ConfigurationProperties(prefix = "sky.alioss")
@Data
public class AliOssProperties {
private String endpoint;
private String accessKeyId;
private String accessKeySecret;
private String bucketName;
}
@ConfigurationProperties(prefix = "sky.alioss"):将配置文件application.yml中以 sky.alioss 为前缀的配置项,批量绑定到一个 Java Bean 的属性上
public class AliOssUtil {
private String endpoint;
private String accessKeyId;
private String accessKeySecret;
private String bucketName;
/**
* 文件上传
*
* @param bytes
* @param objectName
* @return
*/
public String upload(byte[] bytes, String objectName) {
// 创建OSSClient实例。
OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
try {
// 创建PutObject请求。
ossClient.putObject(bucketName, objectName, new ByteArrayInputStream(bytes));
} catch (OSSException oe) {
System.out.println("Caught an OSSException, which means your request made it to OSS, "
+ "but was rejected with an error response for some reason.");
System.out.println("Error Message:" + oe.getErrorMessage());
System.out.println("Error Code:" + oe.getErrorCode());
System.out.println("Request ID:" + oe.getRequestId());
System.out.println("Host ID:" + oe.getHostId());
} catch (ClientException ce) {
System.out.println("Caught an ClientException, which means the client encountered "
+ "a serious internal problem while trying to communicate with OSS, "
+ "such as not being able to access the network.");
System.out.println("Error Message:" + ce.getMessage());
} finally {
if (ossClient != null) {
ossClient.shutdown();
}
}
//文件访问路径规则 https://BucketName.Endpoint/ObjectName
StringBuilder stringBuilder = new StringBuilder("https://");
stringBuilder
.append(bucketName)
.append(".")
.append(endpoint)
.append("/")
.append(objectName);
log.info("文件上传到:{}", stringBuilder.toString());
return stringBuilder.toString();
}
}
新增:阿里云OSS文件上传代码,同时拼接文件访问URL,用于返回
@Slf4j
@Configuration
public class OssConfiguration {
@Bean
@ConditionalOnMissingBean
public AliOssUtil aliOssUtil(AliOssProperties aliOssProperties) {
log.info("新建AliOssUtil对象: {}", aliOssProperties);
return new AliOssUtil(aliOssProperties.getEndpoint(),
aliOssProperties.getAccessKeyId(),
aliOssProperties.getAccessKeySecret(),
aliOssProperties.getBucketName());
}
}
- @ConditionalOnMissingBean:Spring 容器,如果你现在还没有一个
AliOssUtil类型的 Bean,那么就调用aliOssUtil()方法来创建一个,并把它注册到容器中。如果你已经有了一个AliOssUtilBean,那就什么都不用做,直接使用已有的那个。
@RestController
@Slf4j
@RequestMapping("/admin/common")
@Api(tags = "公共接口")
public class CommonController {
@Autowired
private AliOssUtil aliOssUtil;
/**
* 文件上传
*
* @param file
* @return
*/
@PostMapping("/upload")
@ApiOperation("文件上传")
public Result<String> upload(MultipartFile file) {
log.info("文件上传:{}", file);
try {
String originalFilename = file.getOriginalFilename();//获得文件名
String extention = originalFilename.substring(originalFilename.lastIndexOf("."));//获得文件后缀
String objectName = UUID.randomUUID().toString() + extention;//构造新文件名称
String filePath = aliOssUtil.upload(file.getBytes(),objectName);
return Result.success(filePath);
} catch (Exception e) {
log.info("文件上传失败:{}", e);
}
return Result.error(MessageConstant.UPLOAD_FAILED);
}
}
文件上传的整个流程:
- Spring Boot 应用启动时,会扫描带有
@Configuration注解的类。它发现了OssConfiguration。 - Spring 首先执行配置类中的方法,特别是带有@Bean的
- 然后,Spring 执行
OssConfiguration中的aliOssUtil()方法。 - 由于该方法的参数是
AliOssProperties,Spring 会自动从容器中找到这个 Bean 并传入。注意:在properties类里已经使用了@Component了,所以能找到 - properties在application.yml中填充了具体值,
- 回到配置类的方法内部,通过
new AliOssUtil(...)手动创建了AliOssUtil的实例,并将AliOssProperties中的属性作为构造函数参数传递进去。 - 这个新创建的
AliOssUtil实例被注册为 Spring Bean。 - 随后在CommonController中利用@AutoWired来声明依赖,所以此时的
AliOssUtil实例aliOssUtil已经有了具体的bucketname等值,随后在进行aliOssUtil.upload(file.getBytes(),objectName)时也并不会报错
新增菜品实现:
逻辑外键:在业务逻辑层面,一个表的某个字段的值,逻辑上 引用了另一个表的主键或唯一索引字段的值。
@Override
@Transactional
public void saveWithFlavor(DishDTO dishDTO) {
// 1. 转换为 Dish 实体
Dish dish = new Dish();
BeanUtils.copyProperties(dishDTO, dish);
dishMapper.insert(dish);
Long dishId = dish.getId();//获取主键
List<DishFlavor> flavors = dishDTO.getFlavors();
if(flavors != null && flavors.size() > 0) {
flavors.forEach(flavor -> {
flavor.setDishId(dishId);//设置外键
});
dishFlavorMapper.insertBatch(flavors);
}
}
在原先学习的基础上,此处在save方法上进行了新的操作,由于DishDTO中有DishFlavor的列表,那么在新增菜品即操作菜品表的同时就有可能会操作到味道表。故需要用到逻辑外键。
flavors.forEach(flavor -> {
flavor.setDishId(dishId);//设置外键
});
forEach是 集合遍历方法,作用是:依次取出集合中的每个元素,执行指定的操作。- 设置外键:将设置的味道都归到该新增Dish的ID下
- Transactional:事务处理,连续处理两个表的时候要确保一致性


我们以这两图为例,当同时设置了甜味与温度,那么List<DishFlavor> flavors里就会有两个DishFlavor对象,而forEach就会去遍历这两个对象,将这两个对象的DishID进行设置。
@AutoFill(OperationType.INSERT)
void insert(Dish dish);
注意使用自定义注解来自动填充公共字段
菜品分页查询:
因为返回数据里有categoryName,其他数据都包含在了Dish表里,但categoryName,因此我们需要设计一个VO类:按需封装数据,用于特定场景的数据展示或传递
public class DishVO implements Serializable {
private Long id;
//菜品名称
private String name;
//菜品分类id
private Long categoryId;
//菜品价格
private BigDecimal price;
//图片
private String image;
//描述信息
private String description;
//0 停售 1 起售
private Integer status;
//更新时间
private LocalDateTime updateTime;
//分类名称
private String categoryName;
//菜品关联的口味
private List<DishFlavor> flavors = new ArrayList<>();
}
查询语句的写法:
- 要查到完整的DishVO,那么就需要查询dish和category两表
- 我的写法顺序
- 先查询dish表全部:select * from dish
- 还要查询category表的name字段,所以设置别名:select d.* , c.name from dish as d left outer join category as c on d.category_id =c.id
LEFT OUTER JOIN是 SQL 中一种常用的 表连接方式- 以左侧表为基准,返回左表的所有记录,同时将右表中与左表匹配的记录合并进来;如果右表中没有匹配的记录,则用
NULL填充右表的字段。- on 左表.关联字段 = 右表.关联字段;
- 因为d表里有字段name,c表查询返回也有name,为了防止映射错误,所以再做一步处理:c.name as categoryName
- 故总语句:
select d.*,c.name from dish as d left outer join category as c on d.category_id =c.id- 再加where处理:
<where> <if test="categoryId !=null"> and categoryId=#{categoryId} </if> <if test="name !=null and name != ' '" > and name like concat('%',#{name},'%') </if> <if test="status !=null"> and status=#{status} </if> </where>
删除菜品:
删除菜品并没有想象中的简单:操作会关联到三个数据表:dish,dish_flavor,setmeal_dish(套餐表)。因为:
-
起售中的菜品不能删除
-
被套餐关联的菜品不能删除
-
删除菜品后,关联的口味数据也需要删除掉
@DeleteMapping
@ApiOperation("菜品批量删除")
public Result delete(@RequestParam List<Long> ids) {
log.info("菜品批量删除:{}", ids);
dishService.deleteBatch(ids);//后绪步骤实现
return Result.success();
}

注意点:
- 请求参数ids,是一个字符串,只不过使用了逗号隔开。可以直接在Controller的参数中使用String ids,但需要我们做处理得到每一个id,因此这里使用@RequestParam注解来做处理,得到一个List<Long>型的ids。
当业务逻辑复杂的时候,可以先写好每一步的注解再去书写Java代码
//先判断该菜品是否正在售卖
for (Long id : ids) {
Dish dish =dishMapper.getById(id);
if(dish.getStatus()== StatusConstant.ENABLE){
throw new DeletionNotAllowedException(MessageConstant.DISH_ON_SALE);
}
}
判断该菜品是否被套餐关联
!!:为什么是列表型呢?---因为批量删除时,只要勾选的删除菜品里存在有关联套餐的就应该弹出提示,禁止删除
//为什么时列表形式呢?---因为批量删除时,只要勾选的删除菜品里存在有关联套餐的就应该弹出提示,禁止删除
List<Long> setmealIds = setmealDishMapper.getSetmealIdsByDishIds(ids);
if(setmealIds != null && setmealIds.size() > 0) {
throw new DeletionNotAllowedException(MessageConstant.DISH_BE_RELATED_BY_SETMEAL);
}
如果可以删除,删除菜品表数据,将其关联的口味数据也删除掉
for (Long id : ids) {
dishMapper.deleteById(id);
dishFlavorMapper.deleteByDishId(id);
}
@Delete("delete from dish_flavor where dish_id=#{dishId}")
void deleteByDishId(Long dishId);
为什么Service方法里传入的参数是id,但这里要修改为dishId呢?------因为这样更加明确,表明是根据菜名来删除而不是主键来删除
//select setmeal_id from setmeal_dish where dish_id in (1,2,3,4)
List<Long> getSetmealIdsByDishIds(List<Long> dishIds);
<select id="getSetmealIdsByDishIds" resultType="java.lang.Long">
select setmeal_id from setmeal_dish where dish_id in
<foreach collection="dishIds" item="dishId" separator="," open="(" close=")">
#{dishId}
</foreach>
</select>
在第二部查找关联套餐步骤中:因为我们要批量查找,例如要查找id=1,=2,=3,=4,那我们的sql语句应该是select setmeal_id from setmeal_dish where dish_id in (1,2,3,4),但我们无法确定id列表里到底是哪些Id,所以需要动态的去构建sql语句
采用foreach来实现:循环遍历集合中的元素,并将它们拼接成 SQL 语句的一部分。在你的场景中,它通常用于 “批量处理多个 ID”(比如批量查询、批量删除)。
-
collection:指定要遍历的集合参数名称。
-
item:定义遍历集合时当前元素的变量名。每次循环中,集合中的一个元素会被赋值给
dishId -
separator:指定元素之间的分隔符。这里用逗号,分隔,最终会在元素之间添加,(如 -
open:遍历结果的前缀。这里指定以(开头。 -
close:遍历结果的后缀。这里指定以)结尾
优化:
for (Long id : ids) {
dishMapper.deleteById(id);
dishFlavorMapper.deleteByDishId(id);
}
此代码,因ids的长度变化,而执行次数发生变化,修改代码,使能够得到固定执行
dishMapper.deleteByIds(ids);
dishFlavorMapper.deleteByDishIds(ids);
<delete id="deleteByDishIds">
delete from dish_flavor where dish_id in
<foreach collection="dishIds" open="(" close=")" separator="," item="dishId">
#{dishId}
</foreach>
</delete>
<delete id="deleteByIds">
delete from dish where id in
<foreach collection="ids" open="(" close=")" separator="," item="id">
#{id}
</foreach>
</delete>
修改菜品:
查询回显:
@GetMapping("/{id}")
@ApiOperation("根据id查询菜品")
public Result<DishVO> getById(@PathVariable Long id){
log.info("根据ID进行查询:",id);
DishVO dishVO =dishService.getByIdWithFlavor(id);
return Result.success(dishVO);
}
路径中有“/{id}”:所以记得添加@PathVariable注解
为什么要利用VO型,而不是DTO型呢:
-
VO用于后端向客户端返回数据,而DTO主要用于客户端向后端传递数据或服务间内部数据传输。 - 返回数据需要组合菜品基本信息和关联的口味信息,
VO可灵活封装这些数据;
@Override
public DishVO getByIdWithFlavor(Long id) {
//根据id查询菜品数据
//根据菜品id查询口味数据
Dish dish =dishMapper.getById(id);
List<DishFlavor> dishFlavors=dishFlavorMapper.getByDishId(id);
DishVO dishVO = new DishVO();
BeanUtils.copyProperties(dish,dishVO);
dishVO.setFlavors(dishFlavors);
return dishVO;
}
查询双表再进行组合
修改数据:
@Override
public void updateWithFlavor(DishDTO dishDTO) {
Dish dish = new Dish();
BeanUtils.copyProperties(dishDTO, dish);
dishMapper.update(dish);
//删除掉原来的口味,插入新的口味即可
dishFlavorMapper.deleteByDishId(dish.getId());
List<DishFlavor> flavors = dishDTO.getFlavors();
if(flavors != null && flavors.size() > 0) {
flavors.forEach(flavor -> {
flavor.setDishId(dish.getId());
});
dishFlavorMapper.insertBatch(flavors);
}
}
- 新建dish用于数据修改处理
- 这里使用先删除再添加的方式来处理口味表
-
添加操作与saveWithFlavor相同
6822

被折叠的 条评论
为什么被折叠?



