技术点

枚举

如果通过switch来一个个查找的话,会因为查找的信息没有而发生错误,但是通过枚举的话就可以判断查找的值是否存在,不存在的话就会方法报错

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public enum testEnum {
MONDAY("星期一"),
TUESDAY("星期二"),
WEDNESDAY("星期三"),
THURSDAY("星期四"),
FRIDAY("星期五"),
SATURDAY("星期六"),
SUNDAY("星期日");

private String desc;

DayEnum(String desc){
this.desc = desc;
}

public String getDesc(){
return desc;
}
}

创建的要是enum枚举类

在java类中用switch加上变量.getDesc()来获取

自定义注解

类型为@Interface

@Target - 指定使用目标

1
2
3
4
5
// 常用取值:
@Target(ElementType.METHOD) // 只能用在方法上
@Target(ElementType.TYPE) // 类段
@Target(ElementType.PARAMETER) // 方法参数
@Target({ElementType.METHOD, ElementType.TYPE}) // 多个目标

@Retention - 指定保留策略

1
2
3
@Retention(RetentionPolicy.SOURCE)  // 仅源码级别,编译后丢弃
@Retention(RetentionPolicy.CLASS) // 编译到class文件,但运行时不可见
@Retention(RetentionPolicy.RUNTIME) // 运行时可通过反射读取(最常用)

其他元注解

1
2
3
@Documented    // 包含在JavaDoc中
@Inherited // 允许子类继承
@Repeatable // 可重复使用

注解属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public @interface MyAnnotation {
// 基本类型,必须有默认值
int value() default 0;

// 字符串类型
String name() default "";

// 枚举类型
Color color() default Color.RED;

// 数组类型
String[] tags() default {};

// 注解类型
NestedAnnotation nested() default @NestedAnnotation;

// Class类型
Class<?> clazz() default Object.class;
}

反射

image-20251116092235122

反射就是返回类里面的成员变量,构造方法,成员方法等等

image-20251116093008607

获取class对象

Class.forName("全类名") (最常用)

全类名 = 包名 + 类名 (直接在类中右键类名复制就可以了)

Class clazz = Class.forname(“全类名”);

类名.class(更多的当成参数传递)

Class clazz = Student.class;

对象.getClass()(当已经有了这个类的对象时才能使用)

Student s = new Student();

Class clazz = s.getClass();

获取构造方法Constructor

Class类中用于获取构造方法的方法

Constructor<?>[] getConstructors(): 返回所有公共构造方法对象的数组

Constructor<?>[] getDeclaredConstructors(): 返回所有构造方法对象的数组

Constructor<T> getConstructor(Class<?>... parameterTypes):返回单个公共构造方法对象(如果是空就返回空参的,要返回其他的就要加上对应参数类型+.class)

Constructor<T> getDeclaredConstructor(Class<?>... parameterTypes):返回单个构造方法对象

Constructor类中用于创建对象的方法

T newInstance(Object... initargs):根据指定的构造方法创建对象

setAccessible(boolean flag):设置为true,表示取消访问检查

Constructor类中用于操作构造方法的方法

  • T newInstance(Object... initargs):根据指定的构造方法创建对象实例。
  • void setAccessible(boolean flag):设置为true时,取消访问检查(用于访问私有构造方法)。
  • int getModifiers():获取构造方法的修饰符。
  • String getName():获取构造方法的名字(通常返回类名)。
  • Class<?>[] getParameterTypes():获取构造方法的形参类型数组。

成员变量(字段)Field

Class类中用于获取成员变量的方法

Field[] getFields(): 返回所有公共成员变量对象的数组

Field[] getDeclaredFields(): 返回所有成员变量对象的数组

Field getField(String name):返回单个公共成员变量对象

Field getDeclaredField(String name):返回单个成员变量对象

Field类中用于操作成员变量的方法

void set(Object obj, Object value):为指定对象的此成员变量赋值

Object get(Object obj):获取指定对象的此成员变量的值

Field类中用于操作字段的方法

  • int getModifiers():获取字段的修饰符(如public、private等)。
  • String getName():获取字段的名字。
  • Class<?> getType():获取字段的类型。
  • Object get(Object obj):获取指定对象中该字段的值。
  • void set(Object obj, Object value):设置指定对象中该字段的值。
  • void setAccessible(boolean flag):设置为true时,取消访问检查(用于访问私有字段)。

成员方法Method

Class类中用于获取成员方法的方法

  • Method[] getMethods():返回所有公共成员方法对象的数组(包括继承的公共方法)。
  • Method[] getDeclaredMethods():返回所有成员方法对象的数组(包括私有方法,但不包括继承的方法)。
  • Method getMethod(String name, Class<?>... parameterTypes):返回指定名称和参数类型的公共方法对象。
  • Method getDeclaredMethod(String name, Class<?>... parameterTypes):返回指定名称和参数类型的方法对象(包括私有方法)。

Method类中用于操作成员方法的方法

  • int getModifiers():获取方法的修饰符。
  • String getName():获取方法的名字。
  • Class<?>[] getParameterTypes():获取方法的形参类型数组。
  • Class<?> getReturnType():获取方法的返回值类型。
  • Class<?>[] getExceptionTypes():获取方法抛出的异常类型数组。
  • Annotation[] getAnnotations():获取方法上的注解数组。
  • Object invoke(Object obj, Object... args):运行(调用)指定对象上的方法,并返回结果。

invoke例子:

Student s = new Student();

m.invoke(s,“kfc”)

参数一:方法的调用者

参数二:调用方法时传递的实际参数

AOP

面向切面编程,也可理解为面向特定方法编程

步骤

  • 导入aop依赖
1
2
3
4
5
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
<version>3.5.6</version>
</dependency>
  • 编写aop程序
1
2
3
@Aspect		//声明当前类是一个aop类
@Component //把这个类交给ioc
public class ...
1
2
3
4
5
6
7
8
9
10
11
@Aspect
@Component
public class record111 {
@Around("execution(* *..*.*(..))")
public Object record111(ProceedingJoinPoint pjp) throws Throwable {

Object result = pjp.proceed();

return result;
}
}

result表示原始代码,在其上下部分可添加代码

@Around中设定范围

核心概念

image-20251116133945043

底层就是通过动态代理

先生成一个和目标对象同一接口的代理对象,复制方法,再把aop的代码放进去,最后在controller中注入service的就是代理对象,引用的方法也是代理对象中的

这样做就可以不污染源代码,还能实现各种操作,比如记录代码运行时间等等

image-20251116134216915

通知类型

image-20251116135358311

通知顺序

  • 当有多个切面的切入点都匹配到了目标方法,目标方法运行时,多个通知方法都会被执行。

  • 在不同切面类中,默认按照切面类的类名字母排序

    • 目标方法前的通知方法:字母排名靠前的先执行。

    • 目标方法后的通知方法:字母排名靠前的后执行。

  • @Order(数字)加在切面类上控制顺序

    • 目标方法前的通知方法:数字小的先执行。

    • 目标方法后的通知方法:数字小的后执行。

切入点表达式

image-20251116142424267

  • execution

主要根据方法的返回值、包名、类名、方法名、方法参数等信息来匹配,语法为:

execution(访问修饰符? 返回值 包名.类名.?方法名(方法参数) throws 异常?)

  • 其中带 ?的部分表示可以省略。

访问修饰符:可省略(例如:public、protected)

包名.类名:可省略(但不推荐)

throws 异常:可省略(此处指方法声明上抛出的异常,并非实际运行时的异常)

  • 通配符说明

\*:表示任意单个符号,可用于通配返回值、包名、类名、方法名,或任意类型的一个参数,也可用于通配包、类、方法名的一部分。

示例execution(* com.*.service.*.update*(*))

..:表示任意多个连续的符号,可用于通配任意层级的包,或任意类型、任意个数的参数。

示例execution(* com.itheima.DeptService.*(..))

  • @annotation

用于识别标识有特定注解的方法

这个其实是自定义注解

image-20251116145045229

LogOpration需要自己创建一个自定义注解,方法上的@LogOpration就是调用的自定义注解

连接点

在Spring中用JoinPoint抽象了连接点,用它可以获得方法执行时的相关信息,如目标类名、方法名、方法参数等。

  • 对于@Around通知,获取连接点信息只能使用 ProceedingJoinPoint。
  • 对于其它四种通知,获取连接点信息只能使用JoinPoint,它是 ProceedingJoinPoint的父类型。

image-20251116150042542

公共字段自动填充

因为在搭建项目的时候有很多重复的操作,且维护起来不方便,因此就要根据以上复习的技术点来搭建公共字段

image-20251116151410078

枚举

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package com.sky.enumeration;

/**
* 数据库操作类型
*/
public enum OperationType {

/**
* 更新操作
*/
UPDATE,

/**
* 插入操作
*/
INSERT

}

自定义注解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AutoFill {
// 数据库操作类型,update insert
// 为什么用枚举?
// 为了保证程序的安全性和可读性。它限定了 @AutoFill注解的
// value只能取几个预定义好的值,而不是随便写一个字符串或数字。

/**
* value的作用
* 就是把枚举里的值取出来,作为注解的参数
* 例如@AutoFill(value = OperationType.INSERT)
*/
OperationType value();
}

AOP

1
2
3
4
5
6
7
8
9
10
11
12
13
@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("开始进行数据填充");
}
}

定义切入点autoFillPointCut的范围,再把切入点给autoFill来进行操作

在mapper对应的方法上加上以下代码即可

1
2
@AutoFill(value = Ope@AutoFill(value = OperationType.INSERT)
@AutoFill(value = OperationType.INSERT)rationType.UPDATE)

补充AOP

  • 获取到当前被拦截的方法上的数据库操作类型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();

//joinPoint:就是被拦截到的那个“快递包裹”。
//.getSignature():查看包裹上的“运单”,上面有寄件人、收件人等基本信息。
//(MethodSignature):确认这个包裹是“方法类型”的快递(因为还有其他的切入点类型)。
//结果:我们拿到了一个详细的“运单信息”(methodSignature)

AutoFill autoFill = methodSignature.getMethod().getAnnotation(AutoFill.class);

//methodSignature.getMethod():根据运单信息,找到具体的“包裹内容”(被拦截的Method对象)。
//.getAnnotation(AutoFill.class):在这个包裹上寻找有没有贴@AutoFill这个特殊的“标签”。
//结果:我们找到了那个标签(autoFill注解对象)。

OperationType operationType = autoFill.value();
//autoFill.value():仔细看标签上写的具体内容。
//结果:读到了内容是OperationType.INSERT(插入操作)或者OperationType.UPDATE(更新操作)。
  • 获取到当前被拦截的方法的参数–实体对象
1
2
3
4
5
6
Object[] args = joinPoint.getArgs();
if (args == null || args.length == 0){
return;
}
//约定mapper中的第一个参数是实体对象
Object object = args[0];
  • 准备赋值的数据
1
2
LocalDateTime now = LocalDateTime.now();
Long currentId = BaseContext.getCurrentId();
  • 根据不同的操作类型,为对应的属性通过反射来复制
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
if (operationType == OperationType.INSERT){ //判断添加
try {
Method setUpdateTime = object.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_TIME, LocalDateTime.class);//这里用了设置的常量
Method setCreateUser = object.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_USER, Long.class);
Method setUpdateUser = object.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_USER, Long.class);
Method setCreateTime = object.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_TIME, LocalDateTime.class);
//通过反射赋值
setCreateTime.invoke(object,now);
setUpdateTime.invoke(object,now);
setCreateUser.invoke(object,currentId);
setUpdateUser.invoke(object,currentId);

} catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) {
throw new RuntimeException(e);
}
}else if(operationType == OperationType.UPDATE){ //判断更新
Method setUpdateTime = object.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_TIME, LocalDateTime.class);
Method setUpdateUser = object.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_USER, Long.class);

setUpdateTime.invoke(object,now);
setUpdateUser.invoke(object,currentId);
}
}

菜品功能

新增菜品

  • 文件上传

先在application.yml中配置阿里云oss

1
2
3
4
5
alioss:
endpoint: oss-cn-beijing.aliyuncs.com
access-key-id: LTAI5tEnxippfj9dn2iynJBe
access-key-secret: qY7L9R4516VLV8WjqXpzvYU89q9phy
bucket-name: sky-take-out

根据接口文档的要求创建新的controller来进行文件上传

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Autowired
private AliOssUtil aliOssUtil;

//String是因为返回的必须的data是字符串
//MultipartFile指文件类型
public Result<String> upload(MultipartFile file) throws IOException {
log.info("文件上传:{}",file);


String originalFilename = file.getOriginalFilename();


String extension = originalFilename.substring(originalFilename.lastIndexOf("."));


String objectName = UUID.randomUUID().toString() + extension;

// 调用阿里云OSS工具类上传文件
aliOssUtil.upload(file.getBytes(),objectName);

return Result.success(objectName);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
@Autowired
private AliOssUtil aliOssUtil;

@PostMapping("/upload")
public Result<String> upload(MultipartFile file) throws IOException {
log.info("文件上传:{}", file.getOriginalFilename());

try {
// 验证文件
if (file.isEmpty()) {
return Result.error("文件不能为空");
}

// 原始文件名,比如 "avatar.jpg"
String originalFilename = file.getOriginalFilename();

// 截取文件后缀,比如 ".jpg"
String extension = originalFilename.substring(originalFilename.lastIndexOf("."));

// 生成唯一文件名,比如 "a1b2c3d4-e5f6-7890.jpg"
String objectName = UUID.randomUUID().toString() + extension;

// 调用阿里云OSS工具类上传文件,并获取完整的URL
String fileUrl = aliOssUtil.upload(file.getBytes(), objectName);

log.info("文件上传成功,URL:{}", fileUrl);

// 返回完整的URL,而不是文件名
return Result.success(fileUrl);

} catch (Exception e) {
log.error("文件上传失败", e);
return Result.error("上传失败:" + e.getMessage());
}
}
  • 新增菜品

controller

1
2
3
4
5
public Result addDish(@RequestBody DishDTO dishDTO){
log.info("新增菜品:{}",dishDTO);
dishService.addDish(dishDTO);
return Result.success();
}

service

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Override
@Transactional //添加事务注解 因为有多个表要插入,若一个错误,将不在运行
public void addDish(DishDTO dishDTO) {
//向菜品表插入一条数据
Dish dish = new Dish();
BeanUtils.copyProperties(dishDTO,dish);
dishMapper.addDish(dish);

//获取addDish生成的主键值(因为口味表那里需要主键值
Long dishId = dish.getId();

//向口味表插入n条数据
List<DishFlavor> flavors = dishDTO.getFlavors();
if (flavors != null && flavors.size() > 0){
flavors.forEach(dishFlavor -> dishFlavor.setDishId(dishId));
dishF.add(flavors);
}
}

mapper

  • 菜品
1
2
3
4
5
6
7
8
<!--    因为在菜品口味的时候需要菜品的id,所以这里要返回id-->
<insert id="addDish" useGeneratedKeys="true" keyProperty="id">
insert into dish
(name, category_id, description, price, image, status, create_time, update_time, create_user, update_user)
values
(#{name}, #{categoryId}, #{description},#{price},
#{image}, #{status}, #{createTime}, #{updateTime}, #{createUser}, #{updateUser})
</insert>
  • 口味
1
2
3
4
5
6
<insert id="add">
insert into dish_flavor (name, dish_id,value) values
<foreach collection="flavors" item="df" separator=","> //关于xml的遍历语法
(#{df.name}, #{df.dishId}, #{df.value})
</foreach>
</insert>

页面查询

其他部分与之前都大差不差,只有一些小区别

service

1
Page<DishVO> page = dishMapper.pageQuery(dishPageQueryDto);

这里的Page的泛型用了DishVO,因为相比Dish,DishVO中有变量category

mapper

1
2
3
4
5
6
7
8
9
10
11
12
13
select d.*,c.name as categoryName from dish d left outer join category c on d.category_id = c.id //这里用了左外连接
<where>
<if test="name != null and name != ''">
and d.name like '%${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.update_time desc

删除菜品

业务规则

  • 可以一次或批量删除菜品
  • 起售中的菜品不能删除
  • 被套餐关联的菜品不能删除
  • 删除菜品后,关联的口味数据也要删除

service

这里的ListLong<Long>是将前端传回来的String通过RequestParam把ids根据逗号进行分割并把元素添加到list当中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@Transactional
@Override
public void delete(List<Long> ids) {
//判断是否为起售中的菜品
//这里用了for循环,但更有效的方法应该是在xml用foreach
for (Long id : ids) {
Dish dish = dishMapper.getById(id);
if (dish.getStatus() == StatusConstant.ENABLE){
//菜品处于起售状态,不能删除
throw new DeletionNotAllowedException(MessageConstant.DISH_ON_SALE);
}
}

//判断是否为被套餐关联的菜品
//查找套餐里对应的菜品的id
List<Long> setmealIds = dishMapper.getSetmealIdsByDishIds(ids);
if(setmealIds != null && setmealIds.size() > 0){
//说明当前菜品被套餐关联了,不能删除
throw new DeletionNotAllowedException(MessageConstant.DISH_BE_RELATED_BY_SETMEAL);

}

//删除菜品
dishMapper.delete(ids);
//删除菜品后,关联的口味数据也要删除
dishF.deleteByDishId(ids);
}

mapper

1
2
3
4
5
6
<delete id="delete">
delete from dish where id in
<foreach collection="ids" item="id" separator="," open="(" close=")"> //加()是因为foreach就需要()
#{id}
</foreach>
</delete>

删除口味也大同小异

修改菜品

接口设计

  • 根据id查询菜品
  • 根据类型查询分类(已实现)
  • 文件上传(已实现)
  • 修改菜品

根据id查询菜品

service

1
2
3
4
5
6
7
8
9
10
11
12
13
@Override
public DishVO searchById(Long id) {
//根据id查找菜品
Dish byId = dishMapper.getById(id); //之前已经写过
//根据id查找口味
List<DishFlavor> dishFlavors = dishF.getByDishId(id);
//组装vo
DishVO dishVO = new DishVO();
BeanUtils.copyProperties(byId,dishVO);
dishVO.setFlavors(dishFlavors);

return dishVO;
}

修改菜品

service

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Override
public void update(DishDTO dishDTO) {
//更新基础数据
Dish dish = new Dish();
BeanUtils.copyProperties(dishDTO,dish);
dishMapper.update(dish);
//删除原有的口味
dishF.deleteByDishId(Collections.singletonList(dishDTO.getId()));
//新增新的口味
//向口味表插入n条数据
List<DishFlavor> flavors = dishDTO.getFlavors();
if (flavors != null && flavors.size() > 0){
flavors.forEach(dishFlavor -> dishFlavor.setDishId(dishDTO.getId()));
dishF.add(flavors);
}
}

与之前都差不多