苍穹外卖|项目日记(day03)

前言:今天干的东西实在太多, 要写明白也要很长时间, 所以今天给个大体概况,
在这里插入图片描述

今日收获:

1.公共字段填充(AutoFill)

2.阿里云文件上传

3.多表查询(逻辑外键与动态sql)

4.信息转换器

一. 公共字段填充

​ 这一部分是非常难理解的, 对于新手来说, 涉及到AOP切面, 自定义注解, 自定义枚举类, 我不认为我能把它讲明白.

1.先从AOP切面开始
AOP基本概念:

AOP(Aspect-Oriented Programming,面向切面编程)是OOP(面向对象编程)的补充,它允许开发者将横切关注点(如日志、事务、安全等)从业务逻辑中分离出来,实现模块化

简单来说, 就是对业务的增强, 将一个业务单拎出来, 给它加上工具, 增强它能干的事;

AOP核心概念:
  1. 切面(Aspect):横切关注点的模块化,包含通知和切点
  2. 连接点(Joinpoint):程序执行过程中的特定点(如方法调用、异常抛出等)
  3. 通知(Advice):在特定连接点执行的动作
  4. 切点(Pointcut):匹配连接点的表达式
  5. 目标对象(Target Object):被一个或多个切面通知的对象
  6. 织入(Weaving):将切面应用到目标对象创建新代理对象的过程

主要是前5个用的多

通知类类型:
  1. @Before:在方法执行前执行
  2. @After:在方法执行后执行(无论是否抛出异常)
  3. @AfterReturning:方法正常返回后执行
  4. @AfterThrowing:方法抛出异常后执行
  5. @Around:环绕通知,可以控制是否执行目标方法
切点表达式语法:

execution([修饰符] 返回类型 [类名].方法名(参数) [异常])

从苍穹外卖了解切面:
public class AutoFillAspect {
    /**
     * 切入点
     */
    // 配置切入点@Pointcut, 指定需要切入的包的路径, 并在被指定注解注释的业务上切入
    @Pointcut("execution(* com.sky.mapper.*.*(..)) && @annotation(com.sky.annotation.AutoFill)")
    public void autoFillPointcut() {}
}

[!note]

除了通过使用注解Pointcut, 也可直接在通知注解中指定切入点表达式

@Aspect
@Component
public class DirectAspect {
    
    // 前置通知直接指定切入点
    @Before("execution(* com.example.service.*.*(..))")
    public void beforeServiceMethod() {
        System.out.println("准备执行服务方法");
    }
    
    // 环绕通知直接指定切入点
    @Around("execution(* com.example.dao.*.*(..))")
    public Object aroundDaoMethod(ProceedingJoinPoint pjp) throws Throwable {
        System.out.println("DAO方法执行前");
        Object result = pjp.proceed();
        System.out.println("DAO方法执行后");
        return result;
    }
}
示例公共字段填充逻辑:
用**前置通知(Before)拦截要填充的业务, 通过枚举和自定义注解, 获得要填充的业务, 再根据枚举的不同执行不同的逻辑.**
//自定义注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AutoFill {
    //[访问修饰符] 数据类型 属性名() [default 默认值];
    // 如果仅有value一个方法, 可以不加修饰符
    //数据库类型: update, insert
    OperationType value();
}

//枚举类

/**
 * 数据库操作类型
 */
public enum OperationType {
    /**
     * 更新操作
     */
    UPDATE,

    /**
     * 插入操作
     */
    INSERT

}

//公共字段注入实现类
/**
     * 前置通知, 在通知中进行公共字段的赋值
     */
    @Before("autoFillPointcut()")
    public void autoFill(JoinPoint joinPoint) {
        log.info("开始进行公共字段自动填充");
        // 获取到当前被拦截的方法上的数据库操作类型
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature(); // 方法签名对象
        AutoFill autoFill = methodSignature.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) {
            //为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) {
                throw new RuntimeException(e);
            }
        }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) {
                throw new RuntimeException(e);
            }
        }

最后, 再通过在mapper层加上注解, 便可对特定业务特定添加字段

/**
     * 插入数据
     * @param category
     */
    @Insert("insert into category ...")
    @AutoFill(value = OperationType.INSERT)
    void insert(Category category);

二.文件上传

​ 这部分不太难, 只要在阿里云oss配置好, 可以根据官方文档copy, 最后改下配置就好

1.配置oss服务

点到为止, 大家可以去搜一下整合oss与springboot的教程.

配置bucket

在这里插入图片描述

如果可以上传成功, 则配置成功

在这里插入图片描述

2.配置yml文件

可以配置多个文件, 一个为基础文件, 其他的为在不同环境下的配置, spring会自动从环境配置文件中获取值注入基础配置文件

基础配置文件的oss配置:
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}
开发环境下的oss配置:
# 阿里云OSS配置
sky:
  alioss:
    endpoint: oss-cn-hangzhou.aliyuncs.com  # 根据你的Bucket所在地域填写
    access-key-id: your-access-key-id       # 替换为你的AccessKey ID
    access-key-secret: your-access-key-secret # 替换为你的AccessKey Secret
    bucket-name: your-bucket-name            # 替换为你的Bucket名称
aliossPropertis
@Component
可从yml配置文件中根据前缀自动注入
@ConfigurationProperties(prefix = "sky.alioss")
@Data
public class AliOssProperties {

    private String endpoint;
    private String accessKeyId;
    private String accessKeySecret;
    private String bucketName;

}

aliossUtils工具类
@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();
    }
}
OssConfiguration
// 在项目启动时就加载配置
@Configuration
@Slf4j
public class OssConfiguration {

    @Bean //交由IOC容器管理
    @ConditionalOnMissingBean
    public AliOssUtil aliOssUtil(AliOssProperties aliOssProperties) {
        log.info("开始创建阿里云文件上传工具类对象: {}", aliOssProperties);
        return new AliOssUtil(aliOssProperties.getEndpoint(),
                aliOssProperties.getAccessKeyId(),
                aliOssProperties.getAccessKeySecret(),
                aliOssProperties.getBucketName());
    }
}

三.多表查询(逻辑外键与动态sql)

1.基础实现

用户表(users)和订单表(orders)orders表中的user_id是users表的逻辑外键。

-- 用户表
CREATE TABLE users (
    id INT PRIMARY KEY,
    name VARCHAR(100),
    email VARCHAR(100)
);

-- 订单表(使用逻辑外键)
CREATE TABLE orders (
    id INT PRIMARY KEY,
    user_id INT,  -- 逻辑外键,没有FOREIGN KEY约束
    order_date DATE,
    amount DECIMAL(10,2)
);

2.多表查询示例

内连接查询示例
SELECT u.name, u.email, o.order_date, o.amount
FROM users u
INNER JOIN orders o ON u.id = o.user_id;
左外连接查询示例:
SELECT u.name, u.email, o.order_date, o.amount
FROM users u
LEFT JOIN orders o ON u.id = o.user_id;

3,动态sql

动态SQL是指根据不同的条件或参数动态生成SQL语句的技术,它能够灵活地构建适应不同场景的查询或操作。

为什么需要动态SQL
  1. 条件多样性:根据不同的查询条件组合生成不同的SQL
  2. 避免SQL注入:比字符串拼接更安全的方式
  3. 代码复用:一个SQL模板可以应对多种情况
  4. 性能优化:只包含必要的查询条件和字段
主流实现方式
1. MyBatis动态SQL

MyBatis提供了强大的动态SQL功能,主要通过XML标签实现:

常用标签
  • <if>:条件判断
  • <choose>/<when>/<otherwise>:多条件选择
  • <trim>/<where>/<set>:处理SQL片段
  • <foreach>:循环遍历集合
  • <bind>:创建变量

示例:

<select id="findUsers" resultType="User">
  SELECT * FROM users
  <where>
    <if test="name != null">
      AND name LIKE CONCAT('%', #{name}, '%')
    </if>
    <if test="email != null">
      AND email = #{email}
    </if>
    <if test="statusList != null and statusList.size() > 0">
      AND status IN
      <foreach item="status" collection="statusList" 
               open="(" separator="," close=")">
        #{status}
      </foreach>
    </if>
  </where>
  ORDER BY create_time DESC
</select>

四.信息转换器

消息转换器是在客户端和服务器之间传递消息时用于实现数据格式的转换,将消息的原始格式转换为具有特定数据结构的对象或从对象转换为消息的原始格式。消息转换器常用于处理 HTTP 请求和响应的数据格式转换,例如将 JSON 转换为 Java 对象或将 Java 对象转换为 JSON。

常见的消息转换器包括处理 JSON、XML、Protobuf、Properties 等格式的转换。Spring MVC 中提供了多个内置的消息转换器,并且也支持自定义消息转换器。

对象转换器是将一个对象转换为另一个对象的组件,通常在业务逻辑中使用。对象转换器用于将一个类型的对象转换到另一个类型的对象,可以处理属性的映射、类型转换、数据格式转换等操作。对象转换器通常用于模型对象与视图对象之间的转换,或者是实体对象和DTO(Data Transfer Object)之间的转换。

在实际应用中,消息转换器和对象转换器往往会结合使用。例如,首先通过消息转换器将 HTTP 请求中的 JSON 数据转换为 Java 对象,然后再通过对象转换器将这个 Java 对象转换为业务需要的对象类型。

总结一下:

消息转换器用于在客户端和服务器之间进行消息格式的转换,常用于处理 HTTP 请求和响应的数据格式转换。
对象转换器用于将一个类型的对象转换为另一个类型的对象,通常在业务逻辑中使用,用于处理对象之间的转换

  @Override
    protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
        log.info("扩展消息转换器");
        //创建一个消息转换器对象
        MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
        //需要为消息转换器指定对象转换器    对象转换器课可以将java对象序列化为Json对象
        converter.setObjectMapper(new JacksonObjectMapper());
        //将自定义的消息转换器加入到容器中
        converters.add(0, converter);
 
    }
}
Logo

火山引擎开发者社区是火山引擎打造的AI技术生态平台,聚焦Agent与大模型开发,提供豆包系列模型(图像/视频/视觉)、智能分析与会话工具,并配套评测集、动手实验室及行业案例库。社区通过技术沙龙、挑战赛等活动促进开发者成长,新用户可领50万Tokens权益,助力构建智能应用。

更多推荐