苍穹外卖
菜品管理
公共字段自动填充
技术栈
- 基于切面编程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 INSERTOperationType 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.Driverhost: localhostport: 3306database: sky_take_outusername: rootpassword: rootalioss:endpoint: oss-cn-beijing.aliyuncs.comaccess-key-id: LTAI5t5hEbLq6RPAc5ihjWEHaccess-key-secret: vY9VD0hh9q7TcFU2H9AP9x8FPEA8hrbucket-name: sky-itcast-david-ai
3.application.yml(40-44行)
server:port: 8080spring:profiles:active: devmain:allow-circular-references: truedatasource:url: jdbc:mysql://localhost:3306/sky_take_outdriver-class-name: com.mysql.cj.jdbc.Driverusername: rootpassword: 1234mybatis:#mapper配置文件mapper-locations: classpath:mapper/*.xmltype-aliases-package: com.sky.entityconfiguration:#开启驼峰命名map-underscore-to-camel-case: truelogging:level:com:sky:mapper: debugservice: infocontroller: infosky:jwt:# 设置jwt签名加密时使用的秘钥admin-secret-key: itcast# 设置jwt过期时间admin-ttl: 7200000# 设置前端传递过来的令牌名称admin-token-name: token#阿里云OSSalioss: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/ObjectNameStringBuilder 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 {@Autowiredprivate 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