Day67(4)-F:\硕士阶段\Java\课程资料\1、黑马程序员Java项目《苍穹外卖》企业级开发实战\sky-take-out-菜品管理
苍穹外卖
菜品管理
公共字段自动填充
技术栈
- 基于切面编程AOP,定义注解实现为公共字段赋值(定义在sky-server)
- 枚举类
- 定义AOP切面类(定义在sky-server,用aspect包)
- 基于反射获取和设置数据
1.基于切面编程AOP,定义注解实现为公共字段赋值(定义在sky-server)
package com.sky.annotation;
import com.sky.enumeration.OperationType;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 自定义注解,用于标识某个方法需要进行功能字段自动填充处理
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AutoFill {
//数据库操作类型:UPDATE INSERT
OperationType value();
}
2.枚举类
package com.sky.enumeration;
/**
* 数据库操作类型
*/
public enum OperationType {
/**
* 更新操作
*/
UPDATE,
/**
* 插入操作
*/
INSERT
}
3.定义AOP切面类(定义在sky-server,用aspect包)
4.基于反射获取和设置数据(62-65行,68-71行)
package com.sky.aspect;
import com.sky.annotation.AutoFill;
import com.sky.constant.AutoFillConstant;
import com.sky.context.BaseContext;
import com.sky.enumeration.OperationType;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
import java.time.LocalDateTime;
/**
* 自定义切面,实现公共字段的字段填充
*/
@Aspect
@Component
@Slf4j
public class AutoFillAspect {
/**
* 切入点
*/
@Pointcut("execution(* com.sky.mapper.*.*(..)) && @annotation(com.sky.annotation.AutoFill)")
public void autoFillPointCut(){
}
/**
* 前置通知,在通知中进行公共字段赋值
*/
@Before("autoFillPointCut()")
public void autoFill(JoinPoint joinPoint){
log.info("开始进行公共字段的自动填充");
//获取当前被拦截的方法上的数据库操作类型
MethodSignature signature = (MethodSignature) joinPoint.getSignature();//父接口强转为子接口,方法签名对象
AutoFill autoFill = signature.getMethod().getAnnotation(AutoFill.class);//获得方法上的注解对象
OperationType operationType = autoFill.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){
//为四个公共字段赋值
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();
}
}
}
}
通过AOP切面赋值后需要在mapper层加上对应注解
EmployeeMapper为例,第27和第43行
package com.sky.mapper;
import com.github.pagehelper.Page;
import com.sky.annotation.AutoFill;
import com.sky.dto.EmployeePageQueryDTO;
import com.sky.entity.Employee;
import com.sky.enumeration.OperationType;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
@Mapper
public interface EmployeeMapper {
/**
* 根据用户名查询员工
* @param username
* @return
*/
@Select("select * from employee where username = #{username}")
Employee getByUsername(String username);
/**
* 插入员工数据
* @param employee
*/
@AutoFill(value = OperationType.INSERT)//OperationType是枚举类
@Insert("insert into employee (name, username, password, phone, sex, id_number, create_time, update_time, create_user, update_user,status) " +
"values (#{name},#{username},#{password},#{phone},#{sex},#{idNumber},#{createTime},#{updateTime},#{createUser},#{updateUser},#{status})")
void insert(Employee employee);
/**
* 分页查询
* @param employeePageQueryDTO
* @return
*/
Page<Employee> pageQuery(EmployeePageQueryDTO employeePageQueryDTO);
/**
* 根据主键来动态修改属性
* @param employee
*/
@AutoFill(value = OperationType.UPDATE)//OperationType是枚举类
void update(Employee employee);
/**
* 根据id查询员工信息
* @param id
* @return
*/
@Select("select * from employee where id = #{id}")
Employee getById(Long id);
}
技术问题
问题1:@Pointcut 和 @Before 的作用及设计意图
@Pointcut(切入点):- 作用:定义“哪些方法”需要被拦截。它像是一个过滤器或搜索条件。你代码中的表达式指定了:只要是
mapper包下的方法,且贴了@AutoFill标签,就进入我们的视野。 - 设计意图:复用与统一管理。如果你有多个通知(比如
@Before、@After)都要拦截同样的逻辑,只需要引用这个切入点方法名即可,不用到处重复写长长的表达式。
- 作用:定义“哪些方法”需要被拦截。它像是一个过滤器或搜索条件。你代码中的表达式指定了:只要是
@Before(前置通知):- 作用:定义“什么时候”干活。它指定在目标方法(Mapper 里的 SQL 操作)执行之前,先执行这段自动填充逻辑。
- 设计意图:非侵入式增强。数据库操作需要完整的数据。在执行 SQL 之前把缺失的
create_time等补齐,这样 Mapper 层的代码就不用写这些琐碎的赋值,实现了业务逻辑和公共逻辑的分离。
问题2:JoinPoint 是什么?
- 定义:
JoinPoint代表“连接点”,通俗地说,它就是被拦截到的那个“犯罪现场”。它包含了被拦截方法的所有信息。 - 常用属性/方法:
getSignature():获取被拦截方法的签名(包括方法名、参数类型、返回类型等)。getArgs():获取调用该方法时传入的实际参数值(比如你传入的Employee对象)。getTarget():获取被拦截的目标对象(即具体的 Mapper 实例)。
问题3:MethodSignature 与 AutoFill
MethodSignature:- 它是
Signature的子接口。因为 AOP 可以拦截方法、构造函数等,Signature比较通用。 - 作用:专门用于获取方法层面的详细信息。
- 常用方法:
getMethod()(获取Method对象,从而拿到注解)、getReturnType()(返回类型)、getParameterNames()(参数名)。
- 它是
AutoFill:- 这就是你之前定义的那个自定义注解。
- 属性/方法:在你的代码里它有一个
value()方法,返回的是OperationType(INSERT 或 UPDATE)。 - 之所以在 getAnnotation() 后面加上 AutoFill.class,主要有两个原因:唯一性指向和类型安全(泛型转换)。
问题4:getDeclaredMethod 的原理
getDeclaredMethod是什么: 这是 Java 反射 API。它通过方法名在类中查找对应的Method对象。- 为什么要加
LocalDateTime.class: 在 Java 中,方法重载(方法名相同,参数不同)是允许的。- 比如类里可能有
setCreateTime(LocalDateTime t),也可能有setCreateTime(String t)。 - 只给方法名
SET_CREATE_TIME是不够的,必须告诉反射机制:“我要找的是那个参数类型为LocalDateTime的方法”。这就是唯一确定一个方法的方式。
- 比如类里可能有
问题5:反射赋值的其他方法
除了 method.invoke(entity, value) 这种调用 setter 方法的方式,确实还有其他手段:
-
直接修改成员变量(Field): 不找
set方法,直接找createTime属性:Java
Field field = entity.getClass().getDeclaredField("createTime"); field.setAccessible(true); // 暴力反射,允许访问私有变量 field.set(entity, now);- 对比:
invoke调用 setter 更安全、更符合 Java 规范;直接操作Field则更暴力,不需要类里定义 setter 方法也能成功。
- 对比:
-
使用 Spring 提供的工具类: Spring 封装了
BeanUtils或ReflectionUtils,可以让代码更简洁:Java
ReflectionUtils.invokeMethod(method, entity, now); -
使用第三方库(如 Hutool 或 BeanCopier): 这些库底层也是反射,但处理了缓存,性能更好。
新增菜品
文件上传(阿里云)
技术栈
- 阿里云配置,定义在配置属性类com.sky.properties里面(来加载),需要在yml配置文件中统一定义,并定义一个对应的工具类如AliOssUtil,配置类OssConfiguration用于配置AliOssUtil对象(来创建对象)
1.阿里云配置,定义在配置属性类文件com.sky.properties里面,需要在yml配置文见中统一定义
package com.sky.properties;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Component
@ConfigurationProperties(prefix = "sky.alioss")
@Data
public class AliOssProperties {
private String endpoint;
private String accessKeyId;
private String accessKeySecret;
private String bucketName;
}
1.yml配置文件一般不会暴露具体信息,会封装到生产yml也就是dev.yml
2.application-dev.yml
sky:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
host: localhost
port: 3306
database: sky_take_out
username: root
password: root
alioss:
endpoint: oss-cn-beijing.aliyuncs.com
access-key-id: LTAI5t5hEbLq6RPAc5ihjWEH
access-key-secret: vY9VD0hh9q7TcFU2H9AP9x8FPEA8hr
bucket-name: sky-itcast-david-ai
3.application.yml(40-44行)
server:
port: 8080
spring:
profiles:
active: dev
main:
allow-circular-references: true
datasource:
url: jdbc:mysql://localhost:3306/sky_take_out
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: 1234
mybatis:
#mapper配置文件
mapper-locations: classpath:mapper/*.xml
type-aliases-package: com.sky.entity
configuration:
#开启驼峰命名
map-underscore-to-camel-case: true
logging:
level:
com:
sky:
mapper: debug
service: info
controller: info
sky:
jwt:
# 设置jwt签名加密时使用的秘钥
admin-secret-key: itcast
# 设置jwt过期时间
admin-ttl: 7200000
# 设置前端传递过来的令牌名称
admin-token-name: token
#阿里云OSS
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}
4.配置类OssConfiguration用于配置AliOssUtil对象(来创建对象)
package com.sky.config;
import com.sky.properties.AliOssProperties;
import com.sky.utils.AliOssUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* 配置类:用于配置AliOssUtil对象
*/
@Configuration
@Slf4j
public class OssConfiguration {
@Bean
@ConditionalOnMissingBean//当没有这个bean再创建
public AliOssUtil aliOssUtil(AliOssProperties aliOssProperties){
log.info("开始创建阿里云文件上传工具类对象:{}",aliOssProperties);
return new AliOssUtil(aliOssProperties.getEndpoint(),
aliOssProperties.getAccessKeyId(),
aliOssProperties.getAccessKeySecret(),
aliOssProperties.getBucketName());
}
}
5.AliOssUtil对象(工具类)用于在controller直接执行上传操作(老师提供的)
package com.sky.utils;
import com.aliyun.oss.ClientException;
import com.aliyun.oss.OSS;
import com.aliyun.oss.OSSClientBuilder;
import com.aliyun.oss.OSSException;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import java.io.ByteArrayInputStream;
@Data
@AllArgsConstructor
@Slf4j
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();
}
}
6.controller层开发:涉及注入AliOssUtil,UUID使用
package com.sky.controller;
import com.sky.constant.MessageConstant;
import com.sky.result.Result;
import com.sky.utils.AliOssUtil;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.util.UUID;
/**
* 通用接口
*/
@Slf4j
@RestController
@RequestMapping("/admin/common")
@Api(tags = "通用接口")
public class CommonController {
@Autowired
private AliOssUtil aliOssUtil;
/**
* 文件上传
* @param file
* @return
*/
@ApiOperation("文件上传")
@PostMapping("/upload")
public Result<String> upload(MultipartFile file){//file参数名需要跟前端提交的参数名保持一致
log.info("文件上传:{}",file);
try {
//原始文件名
String originalFilename = file.getOriginalFilename();
//截取原始文件名的后缀
String extension = originalFilename.substring(originalFilename.lastIndexOf("."));
String objectName = UUID.randomUUID().toString() + extension;
//文件的请求路径
String filePath = aliOssUtil.upload(file.getBytes(), objectName);
return Result.success(filePath);
} catch (IOException e) {
log.error("文件上传失败:{}",e);
}
return Result.error(MessageConstant.UPLOAD_FAILED);
}
}
技术栈
- 事务管理@Transactional(新增菜品),需要在启动类通过@EnableTransactionManagement ,开启注解方式的事务管理
- 动态SQL,for循环插入
- 主键返回获得dishId,useGeneratedKeys="true" keyProperty="id"
1.事务管理@Transactional(新增菜品),需要在启动类通过@EnableTransactionManagement ,开启注解方式的事务管理
package com.sky;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.transaction.annotation.EnableTransactionManagement;
@SpringBootApplication
@EnableTransactionManagement //开启注解方式的事务管理
@Slf4j
public class SkyApplication {
public static void main(String[] args) {
SpringApplication.run(SkyApplication.class, args);
log.info("server started");
}
}
serviceimpl层进行事务管理
/**
* 新增菜品和对应的口味
* @param dishDTO
*/
@Transactional
@Override
public void saveWithFlavor(DishDTO dishDTO) {
Dish dish = new Dish();
BeanUtils.copyProperties(dishDTO,dish);
//向菜品表插入一条数据
dishMapper.insert(dish);//主键返回获得dishId
//获取insert语句里面生成的主键值
Long dishId = dish.getId();
List<DishFlavor> flavors = dishDTO.getFlavors();
if (flavors != null && flavors.size() >0){
flavors.forEach(dishFlavor -> {
dishFlavor.setDishId(dishId);
});
//向口味表插入n条数据
dishFlavorsMapper.insertBatch(flavors);
}
}
2.动态SQL,for循环插入在xml映射文件里面
<?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="com.sky.mapper.DishFlavorsMapper">
<insert id="insertBatch">
insert into dish_flavor (dish_id, name, value)
values
<foreach collection="flavors" item="flavor" separator=",">
(#{flavor.dishId},#{flavor.name},#{flavor.value})
</foreach>
</insert>
</mapper>
3.主键返回获得dishId,useGeneratedKeys="true" keyProperty="id"(第7行)
<?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="com.sky.mapper.DishMapper">
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
insert into dish (name, category_id, price, image, description, create_time, update_time, create_user, update_user, status)
values
(#{name},#{categoryId},#{price},#{image},#{description},#{createTime},#{updateTime},#{createUser},#{updateUser},#{status})
</insert>
</mapper>
菜品分页查询
技术栈
- 多表联查
菜品分页查询-controller层
/**
* 菜品分页查询
* @param dishPageQueryDTO
* @return
*/
@GetMapping("/page")
@ApiOperation("菜品分页查询")
public Result<PageResult> page(DishPageQueryDTO dishPageQueryDTO){
log.info("菜品分页查询");
PageResult pageResult = dishService.pageQuery(dishPageQueryDTO);
return Result.success(pageResult);
}
菜品分页查询-serviceimpl层
/**
* 菜品分页查询
* @param dishPageQueryDTO
* @return
*/
@Override
public PageResult pageQuery(DishPageQueryDTO dishPageQueryDTO) {
PageHelper.startPage(dishPageQueryDTO.getPage(),dishPageQueryDTO.getPageSize());
Page<DishVO> page = dishMapper.pageQuery(dishPageQueryDTO);
return new PageResult(page.getTotal(),page.getResult());
}
菜品分页查询-mapper层
/**
* 菜品分页查询
* @param dishPageQueryDTO
* @return
*/
Page<DishVO> pageQuery(DishPageQueryDTO dishPageQueryDTO);
1.多表联查
<select id="pageQuery" resultType="com.sky.vo.DishVO">
select d.*,c.name category_name from dish d left outer join category c on d.category_id = c.id
<where>
<if test="name != null">
and d.name like concat('%',#{name},'%')
</if>
<if test="categoryId != null">
and d.category_id = #{categoryId}
</if>
<if test="status != null">
and d.status = #{status}
</if>
</where>
order by d.create_time desc
</select>
删除菜品
技术栈
- 前端传来的String类型数字通过@RequestParam 转换为List
- foreach的SQL生成数组,通过open="(" close=")"
1.前端传来的String类型数字通过@RequestParam 转换为List
/**
* 菜品的批量删除
* @param ids
* @return
*/
@DeleteMapping
@ApiOperation("菜品的批量删除")
public Result delete(@RequestParam List<Long> ids){
//这里 @RequestParam 的作用就是将前端传来的字符串(如 1,2,3)或多个同名参数解析并填充到 List<Long> 集合中。
log.info("菜品批量删除:{}",ids);
dishService.deleteBatch(ids);
return Result.success();
}
2.foreach的SQL生成数组,通过open="(" close=")"
<delete id="deleteByIds">
delete from dish where id in
<foreach collection="ids" item="id" separator="," open="(" close=")">
#{id}
</foreach>
</delete>
修改菜品
修改菜品-controller层
/**
* 根据id修改菜品基本信息和对应的口味信息
* @param dishDTO
* @return
*/
@PutMapping
@ApiOperation("修改菜品")
public Result update(@RequestBody DishDTO dishDTO){
log.info("修改菜品:{}",dishDTO);
dishService.updateWithFlavor(dishDTO);
return Result.success();
}
修改菜品-serviceimpl层
/**
* 根据id修改菜品基本信息和对应的口味信息
* @param dishDTO
*/
@Transactional
@Override
public void updateWithFlavor(DishDTO dishDTO) {
Dish dish = new Dish();
BeanUtils.copyProperties(dishDTO,dish);
//修改菜品表基本信息
dishMapper.update(dish);
//删除原有的口味数据
dishFlavorsMapper.deleteByDishId(dishDTO.getId());
//重新插入口味数据
List<DishFlavor> flavors = dishDTO.getFlavors();
if (flavors != null && flavors.size() >0){
flavors.forEach(dishFlavor -> {
dishFlavor.setDishId(dishDTO.getId());
});
//向口味表插入n条数据
dishFlavorsMapper.insertBatch(flavors);
}
}
修改菜品-mapper层
老代码:flavor先删后添加,先delete然后insert

浙公网安备 33010602011771号