笔者目前从事一线 Java 开发已有三年经验,曾参与SaaS、PaaS项目的开发,也积累了一些技巧和经验,包括线上bug处理、日常业务开发、团队开发规范等。在这里分享出来,作为成长的记录和知识的更新,希望与大家共勉。
免责声明:以下所有demo、代码和测试都是出自笔者本人的构思和实践,不涉及企业隐私和商业机密,属于个人的知识积累分享。
Spring中的自定义注解可以灵活地定制项目开发时需要的切面AOP操作,一般在接口处设置的自定义注解使用最多。以下以一个项目全局通用的接口请求操作日志持久化为例,分享自定义注解开发的一些小技巧。
这一步先定义出具体的注解状态和属性:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Inherited
public @interface OperateLog {
/**
* 线索Id
*/
String trackId() default "";
/**
* 具体操作行为
*/
OperationEnum operation();
}
其中的具体行为操作枚举需要提前准备好,方便后续切面内的日志操作持久化:
@Getter
@RequiredArgsConstructor
public enum OperationEnum {
XX_MODULE_ADD("xx模块","新增xx"),
XX_MODULE_UPDATE("xx模块","修改xx");
private final String module;
private final String detail;
}
这一步是具体的切面实现,切面实现的关键在于: 切面在注解声明方法的哪种顺序执行,即选择5种通知的哪一种。
对于日志记录这种类型的,一般来说切面会在方法返回结果之后执行(@AfterReturning),即操作有结果后再记录日志;而像用户登录或者接口权限校验的自定义注解,一般来说切面会在方法调用前(@Before)就执行。具体切面里的逻辑如下:
@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);
}
}
前面两步完成后,就到最后的业务使用了。一般来说日志类型的自定义注解会放在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));
}
为什么在业务设计的时候需要注意抽象类和接口的运用呢?如果只是依靠类的单一范围原则,那么业务的实现会拧成一大坨,并且代码的耦合会变紧。
抽象类非常适合多个子类共享共同特征和属性,但也兼容自己独有的行为情况,同时为子类的定制实现留出空间。
而接口则是解耦的最基本工具,接口允许将方法的定义与其实现分开,这种分离使得多个不相关的类能够实现同一组方法,从而保证了项目中不同部分之间的相互通信。
Mongo示例
抽象类的继承关系如下:
@Service
public class WorkerServiceImpl extends AbstractWorkerServiceImpl implements WorkerService {}
public abstract class AbstractWorkerServiceImpl extends BaseServiceImpl<Worker, String> implements IWorkerService {}
接口的继承关系如下:
public interface WorkerService extends IWorkerService {}
public interface IWorkerService extends BaseService<Worker, String> {}
底层的继承和实现:
/**
* 以下抽象类和接口中还有自定义的一些数据库方法,与MongoTemplate和MongoRepository形成互补
*/
public abstract class BaseServiceImpl<T, ID> implements BaseService<T, ID> {}
MySQL示例
至于MySQL可以直接引用mybaitisplus的包,里面有现成的实现,都是一些数据库语句的Java实现。必要的情况下还可以同时引入mybaitis包来处理一些复杂的sql语句。
抽象类的继承关系如下:
@Service
public 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> {}
facade模式
facade称为外观模式:为子系统中的各类(或方法)提供简洁一致的入口,隐藏子系统的复杂性。facade层也通常充当一个中介的角色,为上层的调用者提供统一接口的同时,不直接暴露底层的实现细节。
例如在远程调用时,facade层可以提供一个颗粒度比较粗的接口,它负责将外部请求转发给合适的服务进行处理。
service层,只关心数据,在service内直接注入mapper
/**
* 只关心数据,本质上是数据库的一些操作
*/
@Service
public class PersonService extends ServiceImpl<PersonMapper, Person> {
@Resource
private PersonMapper mapper;
//其它数据库语句
...
}
facade层,只关心业务,在facade内直接注入service
/**
* 只关心业务,不继承也不实现,被controller层引用
*/
@Service
public class PersonFacade {
@Resource
private PersonService service;
//业务具体方法逻辑
...
}
上述模式的优点是将数据处理和业务处理明确地分开,业务、数据与视图层的通信靠的是Bean注入的方式,并不是强依赖于类的继承和接口实现,对于外部来说很好地屏蔽了具体的实现逻辑。
但是可能潜在的缺点也有:当业务简单的时候,facade与service之间的边界会比较模糊,即facade层的存在可能是没有必要的。
如果在实际项目里的话,这两者只能选其一。
笔者对于两者在不同的项目中都使用过,实践下来的建议是: 选择抽象类和接口做业务与数据的隔离。
原因无它:抽象类和接口的搭配使用从本质上诠释了Java的继承、封装和多态,与面向对象的思想一脉相承。
作为开发技巧系列文章的第二篇,本文的内容不多但贵在实用。在之后的 文章中我会分享一些关于真实项目中处理高并发、缓存的使用、异步/解耦等内容,敬请期待。
那么今天的分享到这里就暂时结束了,如有不足和错误,还请大家指正。或者你有其它想说的,也欢迎大家在评论区交流!