员工表和分类表中公共字段可以优化,代码冗余后期修改时麻烦。
下面我们来重温一下用到的技术点
反射的作用:当我们编写程序时,通常我们在编译时就知道类的结构,可以直接使用类的方法和字段。但有些时候,我们可能希望在程序运行时,根据一些条件来决定使用哪个类,或者根据类的信息执行一些操作。这时候,反射就派上用场了。
简单来说,反射允许我们在程序运行时动态地了解和操作类的信息,比如创建对象、调用方法、访问字段等。这样我们就能够在不预先知道类结构的情况下,通过代码来处理和使用类。反射提供了一种动态性,但也需要注意使用时可能带来的性能损耗和一些潜在的安全问题。
其主要作用表现为:
动态加载类:
允许在运行时根据条件动态加载类,而不需要在编译时确定要加载的确切类。
动态创建对象:
允许在运行时通过类名创建对象实例,而不需要在编译时知道确切的类。
获取类的信息:
提供了获取类的各种信息(类名、字段、方法等)的能力,使得在运行时可以动态地了解类的结构。
调用方法:
允许在运行时通过方法名调用类的方法,包括私有方法。
访问和修改字段:
允许在运行时访问和修改类的字段,包括私有字段。
实现通用框架和库:
一些通用的框架和库,如 Spring 框架,利用反射处理用户定义的类和对象,提供了高度的灵活性和可扩展性。
注解处理:
允许在运行时获取类上的注解信息,并根据注解执行相应的逻辑。
动态代理:
提供了实现动态代理的能力,使得可以在运行时创建代理对象并拦截对这些代理对象的方法调用。
AOP的作用:
AOP 是一种编程范式,旨在通过将横切关注点(cross-cutting concerns)从主要业务逻辑中分离出来,使代码更模块化、易维护。横切关注点是那些涉及多个模块、不容易用传统的面向对象方法解决的问题,比如日志记录、事务管理、性能优化等。
在 AOP 中,横切关注点被封装成一个切面(Aspect),而切面是一组连接点(Join Point)和通知(Advice)的集合。连接点是在应用执行过程中可以插入切面的点,通知是在连接点上执行的代码。AOP 提供了一种将切面与主要业务逻辑分开的方式,使代码更清晰,易于维护。
AOP与反射的关联:
在某些 AOP 框架中,反射被用于实现切面的动态织入。织入是将切面与应用的主要业务逻辑结合的过程。AOP 框架通过使用反射来动态创建代理对象,将切面的代码插入到连接点上,从而实现在运行时对应用进行横切关注点的处理。
综合来看,AOP 与反射可以协同工作,通过反射来实现 AOP 中的横切关注点的动态织入。反射提供了在运行时获取和操作类信息的能力,而 AOP 则通过切面的概念将这些横切关注点模块化,使代码更易于维护和理解。
新建注解
/**
* 自定义注解 用于标识某个方法需要进行功能字段自动填充处理
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AutoFill {
//数据库操作类型 update insert
OperationType value();
}
新建切面类
/**
* 自定义切面,实现公共字段自动填充
*/
@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) throws NoSuchMethodException {
//进行公共字段赋值
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();
}
}
}
}
给各Mapper接口加上定义的注解
@AutoFill(value = OperationType.UPDATE)
void update(Employee employee);
@Insert("insert into employee(name, username, password, phone, sex, id_number, status, create_time, update_time, create_user, update_user) " +
"values " +
"(#{name},#{username},#{password},#{phone},#{sex},#{idNumber},#{status},#{createTime},#{updateTime},#{createUser},#{updateUser})")
@AutoFill(value = OperationType.INSERT)
void insert(Employee employee);
另外对应前文在各Service实现类里为公共字段赋值语句全部可以省略,自此完成该功能。
使用阿里云储存桶进行开发。
在application.yml中设置
sky:
jwt:
# 设置jwt签名加密时使用的秘钥
admin-secret-key: itcast
# 设置jwt过期时间
admin-ttl: 72000000
# 设置前端传递过来的令牌名称
admin-token-name: token
alioss:
access-key-id: ${sky.alioss.access-key-id}
access-key-secret: ${sky.alioss.access-key-secret}
bucket-name: ${sky.alioss.bucket-name}
endpoint: ${sky.alioss.endpoint}
在application-dev中配置具体参数(自己的OSS信息)
alioss:
endpoint: oss-cn-nanjing.aliyuncs.com
access-key-id: xxxxxxx
access-key-secret: xxxxxxx
bucket-name: xxxxxxxx
@Component
@ConfigurationProperties(prefix = "sky.alioss")
@Data
public class AliOssProperties {
private String endpoint;
private String accessKeyId;
private String accessKeySecret;
private String bucketName;
}
/**
* 配置类,用于创建AliOssUtil对象
*/
@Configuration
@Slf4j
public class OssConfiguration {
@Bean
public AliOssUtil aliOssUtil(AliOssProperties aliOssProperties){
log.info("开始创建阿里云上传工具对象:{}",aliOssProperties);
return new AliOssUtil(aliOssProperties.getEndpoint(),
aliOssProperties.getAccessKeyId(),
aliOssProperties.getAccessKeySecret(),
aliOssProperties.getBucketName());
}
}
这两段代码逻辑
OssConfiguration 这个配置类的主要作用是创建并配置 AliOssUtil 这个 Bean 对象,为其构造函数所需的参数赋值。具体来说:
新建一个Conroller层
package com.sky.controller.admin;
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.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.util.UUID;
/**
* 通用接口
*/
@RestController
@RequestMapping("/admin/common")
@Api(tags = "通用接口")
@Slf4j
public class CommonController {
@Autowired
private AliOssUtil aliOssUtil;
/**
* 文件上传
* @param file
* @return
*/
@ApiOperation("文件上传")
@PostMapping("/upload")
public Result<String> upload(MultipartFile 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.success();
}
}
新建DishController
package com.sky.controller.admin;
import com.sky.dto.DishDTO;
import com.sky.result.Result;
import com.sky.service.DishService;
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.*;
/**
* 菜品管理
*/
@RestController
@RequestMapping("/admin/dish")
@Slf4j
@Api(tags = "菜品相关接口")
public class DishController {
@Autowired
private DishService dishService;
/**
* 新增菜品
* @param dishDTO
* @return
*/
@ApiOperation("新增菜品")
@PostMapping
public Result save(@RequestBody DishDTO dishDTO){
log.info("新增菜品:{}",dishDTO);
dishService.saveWithFlavor(dishDTO);
return Result.success();
}
}
Service实现类
package com.sky.service.impl;
import com.sky.dto.DishDTO;
import com.sky.entity.Dish;
import com.sky.entity.DishFlavor;
import com.sky.mapper.DishFlavorMapper;
import com.sky.mapper.DishMapper;
import com.sky.service.DishService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Service
@Slf4j
public class DishServiceImpl implements DishService {
@Autowired
private DishMapper dishMapper;
@Autowired
private DishFlavorMapper dishFlavorMapper;
/**
* 新增菜品和对应口味
* @param dishDTO
*/
@Override
@Transactional
public void saveWithFlavor(DishDTO dishDTO) {
Dish dish=new Dish();
BeanUtils.copyProperties(dishDTO, dish);
//向菜品表插入一条数据并没有口味,所以不需要DTO
dishMapper.insert(dish);
//获取insert语句生成的主键值
Long dishId=dish.getId();
//向口味表插入N条数据,一道菜的口味可能有很多
List<DishFlavor> flavors = dishDTO.getFlavors();
if(flavors != null && flavors.size()>0){
//为菜品的dishId赋值,防止后面插入空值
flavors.forEach(dishFlavor -> {
dishFlavor.setDishId(dishId);
});
//在if里面插入多条数据
//sql支持批量插入
dishFlavorMapper.insertBatch(flavors);
}
}
}
因为除了有菜品数据库还有对应的口味数据库,为解耦合需要定义两个Mapper文件。
Mapper接口类
package com.sky.mapper;
import com.sky.annotation.AutoFill;
import com.sky.dto.DishDTO;
import com.sky.entity.Dish;
import com.sky.enumeration.OperationType;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
@Mapper
public interface DishMapper {
/**
* 根据分类id查询菜品数量
* @param categoryId
* @return
*/
@Select("select count(id) from dish where category_id = #{categoryId}")
Integer CountByCategoryId(Long categoryId);
/**
* 插入菜品数据
* @param dish
*/
@AutoFill(value = OperationType.INSERT)
void insert(Dish dish);
}
package com.sky.mapper;
import com.sky.entity.DishFlavor;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
@Mapper
public interface DishFlavorMapper {
/**
* 批量插入口味数据
* @param flavors
*/
void insertBatch(List<DishFlavor> flavors);
}
Mapper类
<?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语句执行完后产生的主键值会赋值给dish的id-->
<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>
<?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.DishFlavorMapper">
<insert id="insertBatch">
insert into dish_flavor (dish_id, name, value) VALUES
<foreach collection="flavors" item="df" separator=",">
(#{df.dishId},#{df.name},#{df.value})
</foreach>
</insert>
</mapper>
注意联表查询
Controller层
/**
* 菜品分页查询
* @param dishPageQueryDTO
* @return
*/
@GetMapping("/page")
@ApiOperation("菜品分页查询")
//通过URL传参,并不是json格式故不需要加@RequestBody
public Result<PageResult> page(DishPageQueryDTO dishPageQueryDTO){
log.info("菜品分页查询:{}",dishPageQueryDTO);
PageResult pageResult=dishService.pageQuery(dishPageQueryDTO);
return Result.success(pageResult);
}
Service实现类
/**
* 菜品分页查询
* @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层以及xml
/**
* 菜品分页查询
* @param dishPageQueryDTO
* @return
*/
Page<DishVO> pageQuery(DishPageQueryDTO dishPageQueryDTO);
<select id="pageQuery" resultType="com.sky.vo.DishVO">
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 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>
在此复习一下左外连接的链表查询
左外连接(Left Outer Join)是一种数据库查询操作,用于联接两个表并返回左表中的所有行以及与右表中匹配的行。如果在右表中找不到匹配的行,那么结果集中将包含右表中的列,但是这些列的值将为NULL。
SELECT *
FROM 表A
LEFT OUTER JOIN 表B ON 表A.共有列 = 表B.共有列;
在这里,共有列 是两个表中用于匹配的列。左外连接会返回表A中的所有行,同时匹配的表B中的行,如果没有匹配的行,那么右表中的列将包含NULL。
例如,假设有两个表 学生表 和 成绩表:
学生表 (Students):
学生ID | 姓名 |
---|---|
1 | 小明 |
2 | 小红 |
3 | 小刚 |
成绩表 (Grades):
学生ID | 科目 | 成绩 |
---|---|---|
1 | 数学 | 90 |
1 | 英语 | 85 |
3 | 数学 | 95 |
使用左外连接,可以得到一个包含所有学生以及其对应成绩(如果有的话)的结果集:
SELECT *
FROM 学生表
LEFT OUTER JOIN 成绩表 ON 学生表.学生ID = 成绩表.学生ID;
左外连接结果集:
学生表.学生ID | 学生表.姓名 | 成绩表.学生ID | 成绩表.科目 | 成绩表.成绩 |
---|---|---|---|---|
1 | 小明 | 1 | 数学 | 90 |
1 | 小明 | 1 | 英语 | 85 |
2 | 小红 | NULL | NULL | NULL |
3 | 小刚 | 3 | 数学 | 95 |
结果集将包含所有学生,以及他们的成绩(如果有的话)。对于没有成绩的学生,成绩表中的列将为NULL。
Controller层
/**
* 菜品批量删除
* @param ids
* @return
* @RequestParam 用于从HTTP请求中提取参数的注解
*/
@DeleteMapping
@ApiOperation("菜品批量删除")
public Result delete(@RequestParam List<Long> ids){
log.info("菜品批量删除:{}", ids);
dishService.deleteBatch(ids);
return Result.success();
}
Service实现类
/** @Transactional 保证事务一致性
* 菜品批量删除
* @param ids
*/
@Override
@Transactional
public void deleteBatch(List<Long> ids) {
//判断当前菜品是否能够删除,是否存在起售中的菜品?
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.deleteDishId(id);
}
}
由于要关联菜品与套餐表需要多个Mapper
@Mapper
public interface SetmealDishMapper {
/**
* 根据菜品id来查套餐id
* @param dishIds
* @return
*/
//select setmeal_id from setmeal_dish where dish_id in (1,2,3,4)
List<Long> getSetmealIdsByDishIds(List<Long> dishIds);
}
public interface DishFlavorMapper {
/**
* 批量插入口味数据
* @param flavors
*/
void insertBatch(List<DishFlavor> flavors);
/**
* 根据菜品Id删除对应口味数据
* @param dishId
*/
@Delete("delete from dish_flavor where dish_id = #{dishId}")
void deleteDishId(Long dishId);
}
/**
* 根据主键查询菜品数据
* @param id
* @return
*/
@Select("select * from dish where id = #{id}")
Dish getById(Long id);
/**
* 根据主键删除菜品数据
* @param id
*/
@Delete("delete from dish where id = #{id}")
void deleteById(Long id);
@Mapper
public interface DishMapper {
/**
* 根据主键查询菜品数据
* @param id
* @return
*/
@Select("select * from dish where id = #{id}")
Dish getById(Long id);
/**
* 根据主键删除菜品数据
* @param id
*/
@Delete("delete from dish where id = #{id}")
void deleteById(Long id);
}
<?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.SetmealDishMapper">
<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>
</mapper>
因为在删除时每次要执行两条删除的SQL语句,性能不佳,如果数量过多可能会造成卡顿
修改Service实现类
//删除菜品表中的菜品数据
// for (Long id: ids) {
// dishMapper.deleteById(id);
// //删除菜品中关联的口味数据
// dishFlavorMapper.deleteDishId(id);
// }
//根据菜品id集合批量删除菜品数据
//sql: delete from dish where id in (?,?,?)
dishMapper.deleteByIds(ids);
//根据菜品id集合批量删除关联的口味数据
//sql: delete from dish_flavor where dish_id in (?,?,?)
dishFlavorMapper.deleteByDishIds(ids);
修改Mapper对应的xml文件
<delete id="deleteByIds">
delete from dish where id in
<foreach collection="ids" open="(" close=")" separator="," item="id">
#{id}
</foreach>
</delete>
<delete id="deleteByDishIds">
delete from dish_flavor where dish_id
<foreach collection="dishIds" open="(" close=")" separator="," item="dishId">
#{dishId}
</foreach>
</delete>
Controller层
/**
* 根据id查询菜品
* @param id
* @return
*/
@GetMapping("/{id}")
@ApiOperation("根据id查询菜品")
public Result<DishVO> getById(@PathVariable Long id){
log.info("根据id查询菜品:{}",id);
DishVO dishVO = dishService.getByIdWithFlavor(id);
return Result.success(dishVO);
}
/**
* 修改菜品
* @param dishDTO
* @return
*/
@PutMapping
@ApiOperation("修改菜品")
public Result update(@RequestBody DishDTO dishDTO){
log.info("修改菜品:{}",dishDTO);
dishService.updateWithFlavor(dishDTO);
return Result.success();
}
Service层实现类
/**
* 根据id查询菜品和对应口味数据
* @param id
* @return
*/
@Override
public DishVO getByIdWithFlavor(Long id) {
//根据id查询到菜品数据
Dish dish = dishMapper.getById(id);
//根据菜品id查询到口味数据
List<DishFlavor> dishFlavors = dishFlavorMapper.getByDishId(id);
//将查询到的数据封装到VO
DishVO dishVO = new DishVO();
BeanUtils.copyProperties(dish, dishVO);
dishVO.setFlavors(dishFlavors);
return dishVO;
}
/**
* 根据id修改菜品基本信息和口味信息
* @param dishDTO
*/
@Override
public void updateWithFlavor(DishDTO dishDTO) {
//修改菜品表基本信息
Dish dish = new Dish();
BeanUtils.copyProperties(dishDTO, dish);
dishMapper.update(dish);
//删除原有的口味数据
dishFlavorMapper.deleteDishId(dishDTO.getId());
//重新插入口味数据
List<DishFlavor> flavors = dishDTO.getFlavors();
if(flavors != null && flavors.size() > 0){
flavors.forEach(dishFlavor -> {
dishFlavor.setDishId(dishDTO.getId());
});
//向口味表插入n条数据
dishFlavorMapper.insertBatch(flavors);
}
}
Mapper层
/**
* 根据菜品id查询对应的口味数据
* @param dishId
* @return
*/
@Select("select * from dish_flavor where dish_id = #{dishId}")
List<DishFlavor> getByDishId(Long dishId);
/**
* 根据id来动态修改菜品
* @param dish
*/
@AutoFill(value = OperationType.UPDATE)
void update(Dish dish);
<update id="update">
update dish
<set>
<if test="name != null">
name = #{name},
</if>
<if test="categoryId != null">
category_id = #{categoryId},
</if>
<if test="price != null">
price = #{price},
</if>
<if test="image != null">
image = #{image},
</if>
<if test="description != null">
description = #{description},
</if>
<if test="status != null">
status = #{status},
</if>
<if test="updateTime != null">
update_time = #{updateTime},
</if>
<if test="updateUser != null">
update_user = #{updateUser},
</if>
</set>
where id = #{id}
</update>