【进阶篇】Java 实际开发中积累的几个小技巧(二)

目录

前言

笔者目前从事一线 Java 开发今年是第 3 个年头了,从 0-1 的 SaaS、PaaS 的项目做过,基于多租户的标准化开发项目也做过,项目的 PM 也做过...

在实际的开发中积累了一些技巧和经验,包括线上 bug 处理、日常业务开发、团队开发规范等等。现在在这里分享出来,作为成长的记录和知识的更新,希望与大家共勉。

免责声明:以下所有demo、代码和测试都是出自笔者本人的构思和实践,不涉及企业隐私和商业机密,属于个人的知识积累分享。

六、自定义注解

Spring 中的自定义注解可以灵活地定制项目开发时需要的切面 AOP 操作,一般来说在接口处设置的自定义注解是使用的最多的。下面笔者以一个项目全局通用的接口请求操作日志持久化为例子,分享一下自定义注解开发的一些小技巧。

6.1定义注解

这一步先定义出具体的注解状态和属性:

java 复制代码
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Inherited
public @interface OperateLog {

    /**
     * 线索Id
     */
    String trackId() default "";

    /**
     * 具体操作行为
     */
    OperationEnum operation();
}

其中的具体行为操作枚举需要提前准备好,方便后续切面内的日志操作持久化:

java 复制代码
@Getter
@RequiredArgsConstructor
public enum OperationEnum {

    XX_MODULE_ADD("xx模块","新增xx"),
    XX_MODULE_UPDATE("xx模块","修改xx");

    private final String module;

    private final String detail;
}

6.2切面实现

这一步是具体的切面实现,切面实现的关键在于:切面在注解声明方法的哪种顺序执行,即选择 5 种通知的哪一种。

对于日志记录这种类型的,一般来说切面会在方法返回结果之后执行(@AfterReturning),即操作有结果后再记录日志;而像用户登录或者接口权限校验的自定义注解,一般来说切面会在方法调用前(@Before)就执行。具体切面里的逻辑如下:

java 复制代码
@Aspect
@Component
public class OperateLogAOP {

    @Resource
    private OperationLogService operationLogService;

    /**
     * 切面在方法返回结果之后执行,即操作有结果后再记录日志
     * @param joinPoint
     * @param operateLog
     */
    @AfterReturning(value = "@annotation(operateLog)")
    public void operateLogAopMethod(JoinPoint joinPoint, OperateLog operateLog){
        //从自定义注解中取出参数
        String trackId = operateLog.trackId();
        Assert.hasText(trackId, "trackId param error!");
        //处理参数的值,即输入的业务id值
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        Object[] args = joinPoint.getArgs();
        String businessLogId = (String) AopUtils.getFieldValue(args, methodSignature, trackId);
        //操作描述
        String module = operateLog.operation().getModule();
        String detail = operateLog.operation().getDetail();
        //获取请求 http request
        HttpServletRequest request = ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();
        //持久化入库
        OperationLog operationLog = OperationLog.builder()
                .trackId(businessLogId).module(module).detail(detail)
                .ip(IpUtil.getUserIp(request)).createTime(new Date())
                .operatorUuid(UserDataBuilder.get().getUserUuid())
                .operatorName(UserDataBuilder.get().getUserName())
                .build();
        operationLogService.save(operationLog);
    }
}

6.3业务使用

前面两步完成后,就到最后的业务使用了。一般来说日志类型的自定义注解会放在 Controller 层的接口前,具体示例如下:

java 复制代码
    /**
     * 编辑
     * @return 是否成功
     */
    @PostMapping("update")
    @OperateLog(trackId = "studyDTO.id", operation = OperationEnum.XX_MODULE_UPDATE)
    public BaseResponse<Boolean> updateStudy(@RequestBody StudyDTO studyDTO) {
        return ResultUtils.success(studyService.updateStudy(studyDTO));
    }

七、抽象类和接口

为什么在业务设计的时候需要注意抽象类和接口的运用呢?如果只是依靠类的单一范围原则,那么业务的实现会拧成一大坨,并且代码的耦合会变紧。

抽象类非常适合多个子类共享共同特征和属性,但也兼容自己独有的行为情况,同时为子类的定制实现留出空间。

而接口则是解耦的最基本工具,接口允许将方法的定义与其实现分开,这种分离使得多个不相关的类能够实现同一组方法,从而保证了项目中不同部分之间的相互通信。

7.1隔离业务层与 ORM 层

  • Mongo 示例

    抽象类的继承关系如下:

    java 复制代码
    @Service
    public class WorkerServiceImpl extends AbstractWorkerServiceImpl implements WorkerService {}
    java 复制代码
    public abstract class AbstractWorkerServiceImpl extends BaseServiceImpl<Worker, String> implements IWorkerService {}

    接口的继承关系如下:

    java 复制代码
    public interface WorkerService extends IWorkerService {}
    java' 复制代码
    public interface IWorkerService extends BaseService<Worker, String> {}

    底层的继承和实现:

    java 复制代码
    /**
     * 以下抽象类和接口中还有自定义的一些数据库方法,与 MongoTemplate 和 MongoRepository 形成互补
     */
    public abstract class BaseServiceImpl<T, ID> implements BaseService<T, ID> {}
  • MySQL 示例

    至于 MySQL 可以直接引用 mybaitisplus 的包,里面有现成的实现,都是一些数据库语句的 Java 实现。必要的情况下还可以同时引入 mybaitis 包来处理一些复杂的 sql 语句。

    抽象类的继承关系如下:

    java 复制代码
    @Service
    public class StudyServiceImpl extends ServiceImpl<StudyMapper, Study> implements StudyService {}

    接口的继承关系如下:

    java 复制代码
    public interface StudyService extends IService<Study> {}

    底层的继承和实现:

    java 复制代码
    /**
     * 以下抽象类和接口都来源于 com.baomidou.mybatisplus 包
     */
    public class ServiceImpl<M extends BaseMapper<T>, T> implements IService<T> {}

7.2隔离子系统的业务实现

  • facade模式

    facade 称为外观模式:为子系统中的各类(或方法)提供简洁一致的入口,隐藏子系统的复杂性。facade 层也通常充当一个中介的角色,为上层的调用者提供统一接口的同时,不直接暴露底层的实现细节。

    例如在远程调用时,facade 层可以提供一个颗粒度比较粗的接口,它负责将外部请求转发给合适的服务进行处理。

    service层,只关心数据,在 service 内直接注入mapper

    java 复制代码
    /**
     * 只关心数据,本质上是数据库的一些操作
     */
    @Service
    public class PersonService extends ServiceImpl<PersonMapper, Person> {
        @Resource
        private PersonMapper mapper;
        //其它数据库语句
        ...
    }

    facade 层,只关心业务,在 facade内直接注入 service

    java 复制代码
    /**
     * 只关心业务,不继承也不实现,被 controller 层引用
     */
    @Service
    public class PersonFacade {
        @Resource
        private PersonService service;
        //业务具体方法逻辑
        ...
    }

    上述模式的优点是将数据处理和业务处理明确地分开,业务、数据与视图层的通信靠的是 Bean 注入的方式,并不是强依赖于类的继承和接口实现,对于外部来说很好地屏蔽了具体的实现逻辑。

    但是可能潜在的缺点也有:当业务简单的时候,facade 与 service 之间的边界会比较模糊,即 facade 层的存在可能是没有必要的。

7.3选择对比

如果在实际项目里的话,这两者只能选其一。

笔者对于两者在不同的项目中都使用过,实践下来的建议是:选择抽象类和接口做业务与数据的隔离。

原因无它:抽象类和接口的搭配使用从本质上诠释了 Java 的继承、封装和多态,与面向对象的思想一脉相承。


文章小结

作为开发技巧系列文章的第二篇,本文的内容不多但贵在实用。在之后的文章中我会分享一些关于真实项目中处理高并发、缓存的使用、异步/解耦等内容,敬请期待。

那么今天的分享到这里就暂时结束了,如有不足和错误,还请大家指正。或者你有其它想说的,也欢迎大家在评论区交流!

相关推荐
忆~遂愿3 分钟前
GE 引擎进阶:依赖图的原子性管理与异构算子协作调度
java·开发语言·人工智能
MZ_ZXD0017 分钟前
springboot旅游信息管理系统-计算机毕业设计源码21675
java·c++·vue.js·spring boot·python·django·php
PP东10 分钟前
Flowable学习(二)——Flowable概念学习
java·后端·学习·flowable
ManThink Technology15 分钟前
如何使用EBHelper 简化EdgeBus的代码编写?
java·前端·网络
invicinble19 分钟前
springboot的核心实现机制原理
java·spring boot·后端
人道领域27 分钟前
SSM框架从入门到入土(AOP面向切面编程)
java·开发语言
大模型玩家七七1 小时前
梯度累积真的省显存吗?它换走的是什么成本
java·javascript·数据库·人工智能·深度学习
space62123271 小时前
在SpringBoot项目中集成MongoDB
spring boot·后端·mongodb
CodeToGym1 小时前
【Java 办公自动化】Apache POI 入门:手把手教你实现 Excel 导入与导出
java·apache·excel
凡人叶枫1 小时前
C++中智能指针详解(Linux实战版)| 彻底解决内存泄漏,新手也能吃透
java·linux·c语言·开发语言·c++·嵌入式开发