经验首页 前端设计 程序设计 Java相关 移动开发 数据库/运维 软件/图像 大数据/云计算 其他经验
当前位置:技术经验 » Java相关 » Java » 查看文章
【进阶篇】Java 实际开发中积累的几个小技巧(二)
来源:cnblogs  作者:CodeBlogMan  时间:2024/4/17 15:31:38  对本文有异议

前言

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

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

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

六、自定义注解

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

6.1定义注解

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

  1. @Retention(RetentionPolicy.RUNTIME)
  2. @Target(ElementType.METHOD)
  3. @Inherited
  4. public @interface OperateLog {
  5. /**
  6. * 线索Id
  7. */
  8. String trackId() default "";
  9. /**
  10. * 具体操作行为
  11. */
  12. OperationEnum operation();
  13. }

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

  1. @Getter
  2. @RequiredArgsConstructor
  3. public enum OperationEnum {
  4. XX_MODULE_ADD("xx模块","新增xx"),
  5. XX_MODULE_UPDATE("xx模块","修改xx");
  6. private final String module;
  7. private final String detail;
  8. }

6.2切面实现

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

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

  1. @Aspect
  2. @Component
  3. public class OperateLogAOP {
  4. @Resource
  5. private OperationLogService operationLogService;
  6. /**
  7. * 切面在方法返回结果之后执行,即操作有结果后再记录日志
  8. * @param joinPoint
  9. * @param operateLog
  10. */
  11. @AfterReturning(value = "@annotation(operateLog)")
  12. public void operateLogAopMethod(JoinPoint joinPoint, OperateLog operateLog){
  13. //从自定义注解中取出参数
  14. String trackId = operateLog.trackId();
  15. Assert.hasText(trackId, "trackId param error!");
  16. //处理参数的值,即输入的业务id值
  17. MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
  18. Object[] args = joinPoint.getArgs();
  19. String businessLogId = (String) AopUtils.getFieldValue(args, methodSignature, trackId);
  20. //操作描述
  21. String module = operateLog.operation().getModule();
  22. String detail = operateLog.operation().getDetail();
  23. //获取请求 http request
  24. HttpServletRequest request = ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();
  25. //持久化入库
  26. OperationLog operationLog = OperationLog.builder()
  27. .trackId(businessLogId).module(module).detail(detail)
  28. .ip(IpUtil.getUserIp(request)).createTime(new Date())
  29. .operatorUuid(UserDataBuilder.get().getUserUuid())
  30. .operatorName(UserDataBuilder.get().getUserName())
  31. .build();
  32. operationLogService.save(operationLog);
  33. }
  34. }

6.3业务使用

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

  1. /**
  2. * 编辑
  3. * @return 是否成功
  4. */
  5. @PostMapping("update")
  6. @OperateLog(trackId = "studyDTO.id", operation = OperationEnum.XX_MODULE_UPDATE)
  7. public BaseResponse<Boolean> updateStudy(@RequestBody StudyDTO studyDTO) {
  8. return ResultUtils.success(studyService.updateStudy(studyDTO));
  9. }

七、抽象类和接口

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

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

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

7.1隔离业务层与 ORM 层

  • Mongo 示例

    抽象类的继承关系如下:

    1. @Service
    2. public class WorkerServiceImpl extends AbstractWorkerServiceImpl implements WorkerService {}
    1. public abstract class AbstractWorkerServiceImpl extends BaseServiceImpl<Worker, String> implements IWorkerService {}

    接口的继承关系如下:

    1. public interface WorkerService extends IWorkerService {}
    1. public interface IWorkerService extends BaseService<Worker, String> {}

    底层的继承和实现:

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

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

    抽象类的继承关系如下:

    1. @Service
    2. public class StudyServiceImpl extends ServiceImpl<StudyMapper, Study> implements StudyService {}

    接口的继承关系如下:

    1. public interface StudyService extends IService<Study> {}

    底层的继承和实现:

    1. /**
    2. * 以下抽象类和接口都来源于 com.baomidou.mybatisplus 包
    3. */
    4. public class ServiceImpl<M extends BaseMapper<T>, T> implements IService<T> {}

7.2隔离子系统的业务实现

  • facade模式

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

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

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

    1. /**
    2. * 只关心数据,本质上是数据库的一些操作
    3. */
    4. @Service
    5. public class PersonService extends ServiceImpl<PersonMapper, Person> {
    6. @Resource
    7. private PersonMapper mapper;
    8. //其它数据库语句
    9. ...
    10. }

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

    1. /**
    2. * 只关心业务,不继承也不实现,被 controller 层引用
    3. */
    4. @Service
    5. public class PersonFacade {
    6. @Resource
    7. private PersonService service;
    8. //业务具体方法逻辑
    9. ...
    10. }

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

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

7.3选择对比

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

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

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


文章小结

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

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

原文链接:https://www.cnblogs.com/CodeBlogMan/p/18135597

 友情链接:直通硅谷  点职佳  北美留学生论坛

本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728

W3xue 的所有内容仅供测试,对任何法律问题及风险不承担任何责任。通过使用本站内容随之而来的风险与本站无关。
关于我们  |  意见建议  |  捐助我们  |  报错有奖  |  广告合作、友情链接(目前9元/月)请联系QQ:27243702 沸活量
皖ICP备17017327号-2 皖公网安备34020702000426号