【進(jìn)階篇】Java 實際開發(fā)中積累的幾個小技巧(二)
前言
筆者目前從事一線 Java 開發(fā)今年是第 3 個年頭了,從 0-1 的 SaaS、PaaS 的項目做過,基于多租戶的標(biāo)準(zhǔn)化開發(fā)項目也做過,項目的 PM 也做過...
在實際的開發(fā)中積累了一些技巧和經(jīng)驗,包括線上 bug 處理、日常業(yè)務(wù)開發(fā)、團(tuán)隊開發(fā)規(guī)范等等。現(xiàn)在在這里分享出來,作為成長的記錄和知識的更新,希望與大家共勉。
免責(zé)聲明:以下所有demo、代碼和測試都是出自筆者本人的構(gòu)思和實踐,不涉及企業(yè)隱私和商業(yè)機(jī)密,屬于個人的知識積累分享。
六、自定義注解
Spring 中的自定義注解可以靈活地定制項目開發(fā)時需要的切面 AOP 操作,一般來說在接口處設(shè)置的自定義注解是使用的最多的。下面筆者以一個項目全局通用的接口請求操作日志持久化為例子,分享一下自定義注解開發(fā)的一些小技巧。
6.1定義注解
這一步先定義出具體的注解狀態(tài)和屬性:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Inherited
public @interface OperateLog {
/**
* 線索Id
*/
String trackId() default "";
/**
* 具體操作行為
*/
OperationEnum operation();
}
其中的具體行為操作枚舉需要提前準(zhǔn)備好,方便后續(xù)切面內(nèi)的日志操作持久化:
@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切面實現(xiàn)
這一步是具體的切面實現(xiàn),切面實現(xiàn)的關(guān)鍵在于:切面在注解聲明方法的哪種順序執(zhí)行,即選擇 5 種通知的哪一種。
對于日志記錄這種類型的,一般來說切面會在方法返回結(jié)果之后執(zhí)行(@AfterReturning),即操作有結(jié)果后再記錄日志;而像用戶登錄或者接口權(quán)限校驗的自定義注解,一般來說切面會在方法調(diào)用前(@Before)就執(zhí)行。具體切面里的邏輯如下:
@Aspect
@Component
public class OperateLogAOP {
@Resource
private OperationLogService operationLogService;
/**
* 切面在方法返回結(jié)果之后執(zhí)行,即操作有結(jié)果后再記錄日志
* @param joinPoint
* @param operateLog
*/
@AfterReturning(value = "@annotation(operateLog)")
public void operateLogAopMethod(JoinPoint joinPoint, OperateLog operateLog){
//從自定義注解中取出參數(shù)
String trackId = operateLog.trackId();
Assert.hasText(trackId, "trackId param error!");
//處理參數(shù)的值,即輸入的業(yè)務(wù)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業(yè)務(wù)使用
前面兩步完成后,就到最后的業(yè)務(wù)使用了。一般來說日志類型的自定義注解會放在 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));
}
七、抽象類和接口
為什么在業(yè)務(wù)設(shè)計的時候需要注意抽象類和接口的運(yùn)用呢?如果只是依靠類的單一范圍原則,那么業(yè)務(wù)的實現(xiàn)會擰成一大坨,并且代碼的耦合會變緊。
抽象類非常適合多個子類共享共同特征和屬性,但也兼容自己獨(dú)有的行為情況,同時為子類的定制實現(xiàn)留出空間。
而接口則是解耦的最基本工具,接口允許將方法的定義與其實現(xiàn)分開,這種分離使得多個不相關(guān)的類能夠?qū)崿F(xiàn)同一組方法,從而保證了項目中不同部分之間的相互通信。
7.1隔離業(yè)務(wù)層與 ORM 層
-
Mongo 示例
抽象類的繼承關(guān)系如下:
@Service public class WorkerServiceImpl extends AbstractWorkerServiceImpl implements WorkerService {}public abstract class AbstractWorkerServiceImpl extends BaseServiceImpl<Worker, String> implements IWorkerService {}接口的繼承關(guān)系如下:
public interface WorkerService extends IWorkerService {}public interface IWorkerService extends BaseService<Worker, String> {}底層的繼承和實現(xiàn):
/** * 以下抽象類和接口中還有自定義的一些數(shù)據(jù)庫方法,與 MongoTemplate 和 MongoRepository 形成互補(bǔ) */ public abstract class BaseServiceImpl<T, ID> implements BaseService<T, ID> {} -
MySQL 示例
至于 MySQL 可以直接引用 mybaitisplus 的包,里面有現(xiàn)成的實現(xiàn),都是一些數(shù)據(jù)庫語句的 Java 實現(xiàn)。必要的情況下還可以同時引入 mybaitis 包來處理一些復(fù)雜的 sql 語句。
抽象類的繼承關(guān)系如下:
@Service public class StudyServiceImpl extends ServiceImpl<StudyMapper, Study> implements StudyService {}接口的繼承關(guān)系如下:
public interface StudyService extends IService<Study> {}底層的繼承和實現(xiàn):
/** * 以下抽象類和接口都來源于 com.baomidou.mybatisplus 包 */ public class ServiceImpl<M extends BaseMapper<T>, T> implements IService<T> {}
7.2隔離子系統(tǒng)的業(yè)務(wù)實現(xiàn)
-
facade模式
facade 稱為外觀模式:為子系統(tǒng)中的各類(或方法)提供簡潔一致的入口,隱藏子系統(tǒng)的復(fù)雜性。facade 層也通常充當(dāng)一個中介的角色,為上層的調(diào)用者提供統(tǒng)一接口的同時,不直接暴露底層的實現(xiàn)細(xì)節(jié)。
例如在遠(yuǎn)程調(diào)用時,facade 層可以提供一個顆粒度比較粗的接口,它負(fù)責(zé)將外部請求轉(zhuǎn)發(fā)給合適的服務(wù)進(jìn)行處理。
service層,只關(guān)心數(shù)據(jù),在 service 內(nèi)直接注入mapper
/** * 只關(guān)心數(shù)據(jù),本質(zhì)上是數(shù)據(jù)庫的一些操作 */ @Service public class PersonService extends ServiceImpl<PersonMapper, Person> { @Resource private PersonMapper mapper; //其它數(shù)據(jù)庫語句 ... }facade 層,只關(guān)心業(yè)務(wù),在 facade內(nèi)直接注入 service
/** * 只關(guān)心業(yè)務(wù),不繼承也不實現(xiàn),被 controller 層引用 */ @Service public class PersonFacade { @Resource private PersonService service; //業(yè)務(wù)具體方法邏輯 ... }上述模式的優(yōu)點(diǎn)是將數(shù)據(jù)處理和業(yè)務(wù)處理明確地分開,業(yè)務(wù)、數(shù)據(jù)與視圖層的通信靠的是 Bean 注入的方式,并不是強(qiáng)依賴于類的繼承和接口實現(xiàn),對于外部來說很好地屏蔽了具體的實現(xiàn)邏輯。
但是可能潛在的缺點(diǎn)也有:當(dāng)業(yè)務(wù)簡單的時候,facade 與 service 之間的邊界會比較模糊,即 facade 層的存在可能是沒有必要的。
7.3選擇對比
如果在實際項目里的話,這兩者只能選其一。
筆者對于兩者在不同的項目中都使用過,實踐下來的建議是:選擇抽象類和接口做業(yè)務(wù)與數(shù)據(jù)的隔離。
原因無它:抽象類和接口的搭配使用從本質(zhì)上詮釋了 Java 的繼承、封裝和多態(tài),與面向?qū)ο蟮乃枷胍幻}相承。
文章小結(jié)
作為開發(fā)技巧系列文章的第二篇,本文的內(nèi)容不多但貴在實用。在之后的文章中我會分享一些關(guān)于真實項目中處理高并發(fā)、緩存的使用、異步/解耦等內(nèi)容,敬請期待。
那么今天的分享到這里就暫時結(jié)束了,如有不足和錯誤,還請大家指正。或者你有其它想說的,也歡迎大家在評論區(qū)交流!

筆者目前從事一線 Java 開發(fā)今年是第 3 個年頭了,從 0-1 的 SaaS、PaaS 的項目做過,基于多租戶的標(biāo)準(zhǔn)化開發(fā)項目也做過,項目的 PM 也做過...
在實際的開發(fā)中積累了一些技巧和經(jīng)驗,包括線上 bug 處理、日常業(yè)務(wù)開發(fā)、團(tuán)隊開發(fā)規(guī)范等等。現(xiàn)在在這里分享出來,作為成長的記錄和知識的更新,希望與大家共勉。
浙公網(wǎng)安備 33010602011771號