摘要:攔截器攔截下那些沒有與注解標注的方法請求,并進行用戶認證。直接根據編寫的代碼生成原生的代碼,所以不會存在任何性能問題解決方案為了解決攔截器中使用反射的性能問題,我們學習的設計思路,在啟動時直接完成所有反射注解的讀取,存入內存。
問題描述 權限認證
權限認證一直是比較復雜的問題,如果是實驗這種要求不嚴格的產品,直接逃避掉權限認證。
軟件設計與編程實踐的實驗,后臺直接用Spring Data REST,好使是好使,但是不能在實際項目中運用,直接把api自動生成了,誰調用都行。
在商業項目中,沒有權限是不行的。
注解關于權限,一直沒有找到很好的解決方案。直到網上送檢項目,因功能簡單,且用戶角色單一,潘老師提出了利用注解實現權限認證的方案。
兩個注解,AdminOnly標注只能給管理員用的方法,Anonymous標注對外的無需認證的接口,其他的未標注的是給普通用戶使用的。
示例代碼示例代碼地址:auth-annotation - mengyunzhi
開發環境:Java 1.8 + Spring Boot 2.1.2.RELEASE
實現 攔截器根據三類方法,對用戶權限進行攔截,使用攔截器 + AOP的模式實現。
攔截器攔截下那些沒有AdminOnly與Anonymous注解標注的方法請求,并進行用戶認證。
攔截器過完之后,去執行請求方法。
AOP切AdminOnly注解的前置通知,植入一段管理員認證的切面邏輯。
對Anonymous注解不進行任何處理,實現了匿名用戶的訪問。
區別這樣一看,攔截器就和AOP很像。那是因為我們這個例子還遠沒有發揮出AOP的實際價值。
AOP比這個例子中看上去,強大得多。
最近學習了設計模式中的代理模式,與AOP息息相關,我會在以后的文章中與大家一同學習。
攔截器聲明攔截器,第三個參數就是當前被攔截的方法。
</>復制代碼
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
HandlerMethod handlerMethod = (HandlerMethod) handler;
}
基本思路
利用反射獲取當前方法中是否標注有AdminOnly與Anonymous注解,如果沒有,則進行普通用戶認證。
</>復制代碼
AdminOnly adminOnly = handlerMethod.getMethodAnnotation(AdminOnly.class);
Anonymous anonymous = handlerMethod.getMethodAnnotation(Anonymous.class);
if (adminOnly != null && anonymous != null) {
return true;
}
boolean result = false;
// 進行用戶認證
return result;
性能優化
反射
每次請求,都要走攔截器,調用getMethodAnnotation方法。
我們去看看getMethodAnnotation方法的源碼實現:
org.springframework.web.method.HandlerMethod中的getMethodAnnotation方法:
</>復制代碼
該方法又調用了AnnotatedElementUtils.findMergedAnnotation方法,我們再點進去看看:
org.springframework.core.annotation.AnnotatedElementUtils中的findMergedAnnotation實現:
</>復制代碼
@Nullable
public static A findMergedAnnotation(AnnotatedElement element, Class annotationType) {
// Shortcut: directly present on the element, with no merging needed?
A annotation = element.getDeclaredAnnotation(annotationType);
if (annotation != null) {
return AnnotationUtils.synthesizeAnnotation(annotation, element);
}
// Exhaustive retrieval of merged annotation attributes...
AnnotationAttributes attributes = findMergedAnnotationAttributes(element, annotationType, false, false);
return (attributes != null ? AnnotationUtils.synthesizeAnnotation(attributes, annotationType, element) : null);
}
該方法是調用AnnotatedElement接口中聲明的getDeclaredAnnotation方法進行注解獲取:
AnnotatedElement接口,存在于java反射包中:
話不多說,反射,就存在性能問題!
個人理解同樣是Java,我們看看Google對于Android反射的態度就好了。
我記得之前我去過Google Android的官網,官方不推薦在Android中使用框架,這可能帶來嚴重的性能問題,其中就有考慮到傳統Java框架中大量使用的反射。
這是國外一篇關于反射的文章,反射到底有多慢?:How Slow is Reflection in Android?
文中提到了一項規范,即用戶期待應用的啟動時間的平均值為2s。
NYTimes Android App中使用Google的Gson進行數據解析,這個在我們后臺使用的還是挺廣泛的,和阿里的fastjson齊名,都是非常火的json庫。
NYTimes的工程師發現Gson中使用反射來獲取數據類型,導致應用啟動時增加了大約700ms的延遲。
ActiveAndroid是一個使用反射實現的庫,特意去Github逛了一手,4000多star,這是相當流行的開源項目了!
Scribd:1093ms for call com.activeandroid.ActiveAndroid.initialize。
Myntra:1421ms for call com.activeandroid.ActiveAndroid.initialize。
Data-Binding
打臉?Android不是不推薦使用框架嗎?那為什么Google又推出了Data-Binding呢?
注意,Google考慮的是第三方框架高額的開銷而引發性能問題。
去看看Data-Binding的優點,最重要的一條就是該框架不使用反射,使用動態代碼生成技術,不會因為使用該框架而造成性能問題。
直接根據編寫的代碼生成原生Android的代碼,所以不會存在任何性能問題!
解決方案為了解決攔截器中使用反射的性能問題,我們學習SpringBoot的設計思路,在啟動時直接完成所有反射注解的讀取,存入內存。
之后每次攔截器直接從內存中讀取,提高性能。
監聽容器啟動事件,在容器啟動時執行以下代碼,掃描所有控制器,及其方法上的注解,如果符合條件,則放到HashMap中。
</>復制代碼
// 初始化組件掃描Scanner,禁用默認的filter
ClassPathScanningCandidateComponentProvider scanner =
new ClassPathScanningCandidateComponentProvider(false);
// 添加過濾條件,要求組件上有RestController注解
scanner.addIncludeFilter(new AnnotationTypeFilter(RestController.class));
// 在當前項目包下掃描所有符合條件的組件
for (BeanDefinition beanDefinition : scanner.findCandidateComponents(basePackageName)) {
// 獲取當前組件的完整類名
String name = beanDefinition.getBeanClassName();
try {
// 利用反射獲取相關類
Class clazz = Class.forName(name);
// 初始化方法名List
List methodNameList = new ArrayList<>();
// 獲取當前類(不包括父類,所以要求控制器間不能繼承)中所有聲明方法
for (Method method : clazz.getDeclaredMethods()) {
// 獲取方法上的注解
AdminOnly adminOnly = method.getAnnotation(AdminOnly.class);
Anonymous anonymous = method.getAnnotation(Anonymous.class);
// 如果該方法不存在AdminOnly和Anonymous注解
if (adminOnly == null && anonymous == null) {
// 添加到List中
methodNameList.add(method.getName());
}
}
// 添加到Map中
AuthAnnotationConfig.getAnnotationsMap().put(clazz, methodNameList);
} catch (ClassNotFoundException e) {
logger.error("掃描注解配置時,發生了ClassNotFoundException異常");
}
}
攔截器修改
原來的攔截器是這樣的:
</>復制代碼
AdminOnly adminOnly = handlerMethod.getMethodAnnotation(AdminOnly.class);
Anonymous anonymous = handlerMethod.getMethodAnnotation(Anonymous.class);
if (adminOnly != null && anonymous != null) {
return true;
}
boolean result = false;
// 進行用戶認證
return result;
現在是這樣的:
</>復制代碼
logger.debug("獲取當前請求方法的組件類型");
Class clazz = handlerMethod.getBeanType();
logger.debug("獲取當前處理請求的方法名");
String methodName = handlerMethod.getMethod().getName();
logger.debug("獲取當前類中需認證的方法名");
List authMethodNames = AuthAnnotationConfig.getAnnotationsMap().get(clazz);
logger.debug("如果List為空或者不包含在認證方法中,釋放攔截");
if (authMethodNames == null || !authMethodNames.contains(methodName)) {
return true;
}
logger.debug("進行用戶認證");
boolean result = false;
// 用戶認證
return result;
之前用了兩次反射,現在是調用了handlerMethod.getBeanType()和handlerMethod.getMethod().getName()。
再去看看這兩個的實現:
getBeanType
</>復制代碼
public Class getBeanType() {
return this.beanType;
}
getMethod
</>復制代碼
public Method getMethod() {
return this.method;
}
都是在org.springframework.web.method.HandlerMethod類中直接返回屬性,我們推斷:這個HandlerMethod,應該是Spring在容器啟動時就已經構造好的方法對象,在攔截器執行期間,沒有調用反射。
注解的注解現在是注解少,我們寫兩行,感覺問題不大:
</>復制代碼
// 獲取方法上的注解
AdminOnly adminOnly = method.getAnnotation(AdminOnly.class);
Anonymous anonymous = method.getAnnotation(Anonymous.class);
以后如果認證注解多了呢?
我們期待這樣,有一個通用的注解來判定當前方法是否要被攔截,而AdminOnly和Anonymous應繼承該注解的功能,這樣以后再想添加不被攔截器攔截的注解,就不需要修改啟動時掃描的方法了。
</>復制代碼
// 獲取授權注解
AdminAuth adminAuth = method.getAnnotation(AdminAuth.class);
我們期望像Spring Boot一樣,在注解上加注解,實現復合注解。
</>復制代碼
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@ControllerAdvice
@ResponseBody
public @interface RestControllerAdvice {
}
構造注解
如果對Java自定義注解不了解,可以去慕課網學習相關課程:全面解析Java注解 - 慕課網
</>復制代碼
@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface AdminAuth {
}
@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE}),該注解可以標注在方法上,也可以標注在其他注解上。
@Retention(RetentionPolicy.RUNTIME),該注解一直保留到程序運行期間。
給注解加注解AdminOnly:
</>復制代碼
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@AdminAuth
public @interface AdminOnly {
}
Anonymous:
</>復制代碼
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@AdminAuth
public @interface Anonymous {
}
解析注解
加注解很簡單,重要的是怎么解析該注解。
調用反射包中的Method類提供的getAnnotation方法,只會告訴我們當前標注了什么注解。
比如:
</>復制代碼
@AdminOnly
public void test() {
}
我們可以通過getAnnotation獲取AdminOnly,但是獲取不到注解在@AdminOnly上的@AdminAuth注解。
怎么獲取注解的注解呢?
找了一上午,不得不說,我解決這個問題還是靠一定的運氣的。在我要放棄的時候,在Google搜出了SpringFramework中的注解工具類AnnotationUtils:
隨手打開文檔:Class AnnotationUtils - Spring Core Docs
第四個方法就是我想要的:
使用該工具類,能直接獲取方法上標注在注解上的注解:
</>復制代碼
@AdminOnly
public void test() {
}
</>復制代碼
AdminAuth adminAuth = AnnotationUtils.getAnnotation(method, AdminAuth.class);
這種方法能獲取到標注在test方法上繼承而來的@AdminAuth注解。
最終代碼:
</>復制代碼
@Component
public class InitAnnotationsConfig implements ApplicationListener {
// 基礎包名
private static final String basePackageName = "com.mengyunzhi.checkApplyOnline";
// 日志
private static final Logger logger = LoggerFactory.getLogger(InitAnnotationsConfig.class);
@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
// 初始化組件掃描Scanner,禁用默認的filter
ClassPathScanningCandidateComponentProvider scanner =
new ClassPathScanningCandidateComponentProvider(false);
// 添加過濾條件,要求組件上有RestController注解
scanner.addIncludeFilter(new AnnotationTypeFilter(RestController.class));
// 在當前項目包下掃描所有符合條件的組件
for (BeanDefinition beanDefinition : scanner.findCandidateComponents(basePackageName)) {
// 獲取當前組件的完整類名
String name = beanDefinition.getBeanClassName();
try {
// 利用反射獲取相關類
Class clazz = Class.forName(name);
// 初始化方法名List
List methodNameList = new ArrayList<>();
// 獲取當前類(不包括父類,所以要求控制器間不能繼承)中所有聲明方法
for (Method method : clazz.getDeclaredMethods()) {
// 獲取授權注解
AdminAuth adminAuth = AnnotationUtils.getAnnotation(method, AdminAuth.class);
// 如果該方法不被授權,則需要認證
if (adminAuth == null) {
// 添加到List中
methodNameList.add(method.getName());
}
}
// 添加到Map中
AuthAnnotationConfig.getAnnotationsMap().put(clazz, methodNameList);
} catch (ClassNotFoundException e) {
logger.error("掃描注解配置時,發生了ClassNotFoundException異常");
}
}
}
}
總結
學會了一個解決問題的新辦法:某個框架應該也遇到過你所遇到的問題,去找找框架中的工具類,這可能會很有幫助。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://m.specialneedsforspecialkids.com/yun/73201.html
摘要:認證服務器和瀏覽器控制臺也沒有報錯信息。這里簡單介紹下如何查閱源碼,首先全局搜索自己的配置因為這個地址是認證服務器請求授權的,所以,請求認證的過濾器肯定包含他。未完待續,下一篇介紹資源服務器和認證服務器的集成。 基于spring-security-oauth2-實現單點登錄 文章代碼地址:鏈接描述可以下載直接運行,基于springboot2.1.5,springcloud Green...
摘要:為了達到很好的效果,我們使用來對的緩存進行管理配置會話管理器,對會話時間進行控制手動清空緩存由于驗證用戶名和密碼之前,一般需要驗證驗證碼的。 前言 本文主要講解的知識點有以下: Shiro授權過濾器使用 Shiro緩存 與Ehcache整合 Shiro應用->實現驗證碼功能 記住我功能 一、授權過濾器測試 我們的授權過濾器使用的是permissionsAuthorization...
摘要:的自身注解的用法。所以自定義注解的作用很廣。但是在這里,我僅僅基于的來實現適用于它的自定義注解。其他的自定義的注解的編寫思路和這個也是類似的。 基于shiro的自定義注解的擴展 根據我的上一篇文章,權限設計的雜談中,涉及到了有關于前后端分離中,頁面和api接口斷開表與表層面的關聯,另辟蹊徑從其他角度找到方式進行關聯。這里我們主要采取了shiro的自定義注解的方案。本篇文章主要解決以下的...
閱讀 3961·2021-09-22 10:02
閱讀 3379·2019-08-30 15:52
閱讀 3071·2019-08-30 12:51
閱讀 769·2019-08-30 11:08
閱讀 2072·2019-08-29 15:18
閱讀 3115·2019-08-29 12:13
閱讀 3605·2019-08-29 11:29
閱讀 1882·2019-08-29 11:13