写点什么

Java 实际开发中积累的几个小技巧(二)

作者:EquatorCoco
  • 2024-11-22
    福建
  • 本文字数:3029 字

    阅读完需:约 10 分钟

六、自定义注解


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


6.1 定义注解


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


@Retention(RetentionPolicy.RUNTIME)@Target(ElementType.METHOD)@Inheritedpublic @interface OperateLog {
/** * 线索Id */ String trackId() default "";
/** * 具体操作行为 */ OperationEnum operation();}
复制代码


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


@Getter@RequiredArgsConstructorpublic 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)就执行。具体切面里的逻辑如下:


@Aspect@Componentpublic 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 层的接口前,具体示例如下:


    /**     * 编辑     * @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 示例


抽象类的继承关系如下:

@Servicepublic class WorkerServiceImpl extends AbstractWorkerServiceImpl implements WorkerService {}
复制代码


接口的继承关系如下:

public interface WorkerService extends IWorkerService {}
复制代码


底层的继承和实现:

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


  • MySQL 示例


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


抽象类的继承关系如下:

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


接口的继承关系如下:

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


底层的继承和实现:

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


7.2 隔离子系统的业务实现


  • facade 模式


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


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


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


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


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


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


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


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


7.3 选择对比


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


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

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


文章小结


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


文章转载自:CodeBlogMan

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

体验地址:http://www.jnpfsoft.com/?from=infoq

用户头像

EquatorCoco

关注

还未添加个人签名 2023-06-19 加入

还未添加个人简介

评论

发布
暂无评论
Java 实际开发中积累的几个小技巧(二)_Java_EquatorCoco_InfoQ写作社区