springboot通用分支處理---還在硬編碼特殊處理邏輯?超級管理員不應該被區別對待

語言: CN / TW / HK

前言

  • 當引入登入模組後我們需要做選單。而選單自然需要許可權的參與,我們在springboot中設計的許可權細粒度還算是比較細的。當我們查詢選單是需要根據許可權查詢對應的選單。但是在springboot中我設計了一個底層超級管理員
  • 先來看看我一開始實現這個超級管理員選單獲取的部分程式碼

if (SecurityUtils.getSubject().hasRole(RoleList.SUPERADMIN)) {      listemp = customMapper.selectRootMenusByRoleIdList(null,null, null, null);  } else {      listemp = customMapper.selectRootMenusByRoleIdList(roleList, oauthClientId,null, moduleCodes);  }

  • 這樣實現是很正常的思路,通過判斷角色是否是超級管理員來做分支執行思路,但是超級管理員可能涉及到多個地方如果在每個地方都這樣if else執行的,我覺得有點low, 所以我決定改造一下。不夠最終執行的思路依然是if else判斷 。 只不過讓我們在程式碼層面上功能間不在那麼雜糅在一起

自定義註解

  • 首先我需要兩個註解,SuperDirectionSuperDirectionHandler分別表示需要判斷超級管理員分支和具體管理員分支的目標函式 。 這句話說的還是有點抽象的,容我慢慢道來!

SuperDirection

@Target({ElementType.TYPE, ElementType.METHOD})  @Retention(RetentionPolicy.RUNTIME)  @Documented  public @interface SuperDirection {  ​      String value() default StringUtils.EMPTY;  ​  }

SuperDirectionHandler

@Target({ElementType.TYPE, ElementType.METHOD})  @Retention(RetentionPolicy.RUNTIME)  @Documented  @Component  public @interface SuperDirectionHandler {  }

作用

  • SuperDirection是用於表明該方法需要進行判斷超級管理員,而value值儲存的就是判斷的表示式。關於這個表示式我們後面介紹
  • SuperDirectionHandler我們不難發現他沒有實際屬性但是多了一個@Component註解。目的是方便Spring管理該註解;這樣我們就可以通過Spring來獲取被該註解標註的類了。

位置

  • 該註解釋放給全域性使用的,在maltcloud結構介紹中我們知道org.framework.core模組是所有模組的基石,所以這兩個註解我選擇在org.framework.core模組中

image-20211013155948064.png

切面

  • 我們想在方法執行前進行條件判斷,可選方案有很多我們可以在filter中攔截方法進行判斷選擇執行哪一個,但是過濾器中我們無法直接獲取到方法的相關資訊,注意這裡說的是無法直接獲取,如果你想在filter中實現也不是不行,這種方案感興趣的可以試試
  • spring的另外一個特性切面正好符合我們的需求,我們只需要在aroud環繞方法中實現我們的需要。
  • 首先我們定義一個切點,切點攔截所有被SuperDirection註解標註的類或者方法。

/**定義一個切點; 攔截所有帶有SuperDirection註解的類和方法*/  @Pointcut("@annotation(com.github.zxhtom.core.annotaion.SuperDirection) || @within(com.github.zxhtom.core.annotaion.SuperDirection)")  public void direction() {  ​  }

  • 這裡稍作一下解釋 @annotation用於標識方法上的SuperDirection@within用於標識在類上的SuperDirection
  • 正常我們的一個業務處理都是放在service層的,spring中的三層架構的service正常是實現一個介面然後實現。所以我們這裡在切面中先獲取被攔截物件實現的介面。獲取到介面資訊我們通過介面資訊獲取該介面在spring中的其它實現類
  • 在spring容器中提供了獲取bean集合的方法,加上我們maltcloud中實現了獲取ApplicationContext的工具類,所以我們通過如下來獲取bean集合

Map<String, ?> beansOfType = ApplicationContextUtil.getApplicationContext().getBeansOfType(介面class);

  • 獲取到集合了,此時我們還是無法確定哪一個實現類使我們替補執行超級管理員的bean 。 這就需要我們SuperDirectionHandler註解了。
  • 這個時候我們在通過Spring獲取被該註解標識的類。這個時候獲取到很多不想關類,我們在和上面的beansOfType進行比對。就可以確定哪一個實現bean是我們需要的。

@Around("direction()")  public Object aroud(ProceedingJoinPoint pjp) throws Throwable {      Class<?>[] inters = pjp.getTarget().getClass().getInterfaces();      for (Class<?> inter : inters) {          Map<String, ?> beansOfType = ApplicationContextUtil.getApplicationContext().getBeansOfType(inter);          Map<String, Object> beansWithAnnotation = ApplicationContextUtil.getApplicationContext().getBeansWithAnnotation(SuperDirectionHandler.class);          for (Map.Entry<String, ?> entry : beansOfType.entrySet()) {              if (beansWithAnnotation.containsKey(entry.getKey())) {                  try {                      return doOthersHandler(entry.getValue(), pjp);                 } catch (Exception e) {                      log.error("分支執行失敗,系統判定執行原有分支....");                 }             }         }     }      return pjp.proceed();  }

  • 當確定執行類之後,我們只需要攜帶著該bean ,在根據SuperDirection上的表示式進行判斷是執行超級管理員實現類還是原有實現類的方法了。

條件判斷

  • 在Aspect執行中原有方法的執行很簡單,只需要pjp.proceed()就可以了。所以這裡我們先獲取一下上面獲取到的超級管理員實現bean的對應方法吧。

MethodSignature msig = (MethodSignature) pjp.getSignature();  Method targetMethod = value.getClass().getDeclaredMethod(msig.getName(),msig.getParameterTypes());

  • 然後我們在獲取SuperDirection註解資訊,因為該註解可能在類上,也可能在方法上,所以這裡我們需要處理下

SuperDirection superDirection = null;  superDirection = targetMethod.getAnnotation(SuperDirection.class);  if (superDirection == null) {      superDirection = pjp.getTarget().getClass().getAnnotation(SuperDirection.class);  }

  • 最終我們通過註解表示式判斷執行情況

if(selectAnnotationChoiceDo(superDirection)){      //如果表示式驗證通過,則執行替補bean實現類      return targetMethod.invoke(value,pjp.getArgs());  }  //否則執行原有bean實現類  return pjp.proceed();

表示式解析

  • 表示式解析涉及兩個模組,一個是登入模組中獲取當前登入使用者的角色,另外一個是我們上面提到的表示式解析
  • 獲取當前登入使用者資訊類我在org.framework.web中提供了bean 。 具體的實現由各個登入子模組負責去實現,這裡我們只需要引入spirng bean使用就要可以了。這裡充分體現了模組拆分的好處了。
  • 至於表示式解析,我選擇放在本模組中org.framework.commons 。 這裡目前簡單提供了幾個表示式解析。

public interface RootChoiceExpression {      public boolean haslogined();      public boolean hasRole(String role);      public boolean hasAnyRole(String... roles);  }

  • 他們分別是驗證是否登入、是否擁有角色和角色組。關於他的視線最終也還是依賴上面提到的org.framework.web模組中的登入使用者的資訊類

@Service  public class DefaultChoiceExpression implements RootChoiceExpression {      @Autowired      OnlineSecurity onlineSecurity;  ​      @Override      public boolean haslogined() {          return onlineSecurity.getOnlinePrincipal()!=null;     }  ​      @Override      public boolean hasRole(String role) {          return onlineSecurity.hasAnyRole(role);     }  ​      @Override      public boolean hasAnyRole(String... roles) {          return onlineSecurity.hasAnyRole(roles);     }  }

  • 這裡也算是流出擴充套件吧,後面根據專案需求我們可以重寫該表示式解析,在根據自己的業務進行表示式新增。這裡僅作為基礎功能

private boolean selectAnnotationChoiceDo(SuperDirection superDirection) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {      String value = superDirection.value();      if (StringUtils.isEmpty(value)) {          return onlineSecurity.getRoleNames().contains(MaltcloudConstant.SUPERADMIN);     }      MethodInfo info = selectInfoFromExpression(value);      Method declaredMethod = expression.getClass().getDeclaredMethod(info.getMethodName(), String.class);      Object invoke = declaredMethod.invoke(expression, info.getArgs());      if (invoke != null && invoke.toString().equals("true")) {          return true;     }      return false;  }

  • 首先根據正則解析出方法名和引數。然後根據反射呼叫我們spring中表達式bean去執行我們在SuperDirection配置的表示式。通過上面我們又能發現目前表示式僅支援String傳參。因為在SuperDireciton傳遞過來的已經是String了,所以在這裡目前我還沒想到如何支援更多型別的資料。先埋坑吧!
  • 該方法最終決定執行原生方法還是替補方法。

演示使用

  • 上面說的那麼枯燥主要是因為是我的一個設計思路,下面我們來實操感受一下吧。

controller

  • 首先我在controller中開發一個介面 。這裡需要注意下因為我們上面會出現多個實現bean在spring中,所以我們在使用這些介面的時候就不能單純的使用@Autowired了, 而需要通過beanName來使用了。這裡名叫commonTestServiceImplCommonTestService介面的普通實現類,用於實現我們正常的操作。

@RestController  @RequestMapping(value = "/demo/common")  public class CommonController {          @Qualifier(value = "commonTestServiceImpl")      @Autowired      CommonTestService commonTestService;  ​      @RequestMapping(value = "/test",method = RequestMethod.GET)      public void test() {          commonTestService.test();     }  }

service

  • 這裡有兩個實現類分別是CommonTestServiceImplCommonTest2ServiceImpl

@Service  @SuperDirectionHandler  public class CommonTest2ServiceImpl implements CommonTestService {      @Override      public void test() {          System.out.println("hello test 2");     }  }

@Service  @SuperDirection(value = "")  public class CommonTestServiceImpl implements CommonTestService {      @Override      public void test() {          System.out.println("hello i am test ing ...");     }  }

  • 在controller層我們使用的是CommonTestServiceImpl用來實現正常的邏輯。而CommonTest2ServiceImpl是針對超級管理員做的操作。我們就可以進行如上的配置。在SuperDirection中配置空值標識判斷超級管理員進行分支執行。你也可以配置目前支援的表示式,我這裡簡單點了。
  • 然後通過SuperDirectionHandler標識ConmmonTest2ServiceImpl是替補執行。

測試

  • 由於為了簡單測試,我在org.framework.demo.common模組中還沒有引入login模組,所以此時登入使用者獲取類還是我們預設的OnlineSecurityImpl

image-20211013162826929.png

  • 通過上面程式碼我們可以看出來我們當前是沒有角色的,所以我們可以理解成呼叫介面是沒有超級管理員介面。那麼就會執行我們CommonTestServiceImpl中的方法。然後我們在將這裡的hasAnyRole改成true , 會發現就會執行CommonTest2ServiceImpl裡的方法。

總結

  • 經過上面這麼折騰,我們就可以在涉及到超級管理員的地方重新實現一下,然後再原有的實現類中只需要專注我們許可權架構中的規則進行資料庫查詢等操作了,而不需要向我一開始那樣為超級管理員進行特殊操作。如果後續我們需要為特殊使用者進行特殊開發。我們就可以擴充套件我們的表示式解析然後再開發我們備用介面就可以了。
  • 這個思路主要來自於Spring Cloud 中的OpenFeign 的容災降級的思路。

我正在參與掘金技術社群創作者簽約計劃招募活動,點選連結報名投稿。

「其他文章」