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