權限認證一直是比較複雜的問題,若是是實驗這種要求不嚴格的產品,直接逃避掉權限認證。html
軟件設計與編程實踐的實驗,後臺直接用Spring Data REST
,好使是好使,可是不能在實際項目中運用,直接把api
自動生成了,誰調用都行。java
在商業項目中,沒有權限是不行的。android
關於權限,一直沒有找到很好的解決方案。直到網上送檢項目,因功能簡單,且用戶角色單一,潘老師提出了利用註解實現權限認證的方案。git
兩個註解,AdminOnly
標註只能給管理員用的方法,Anonymous
標註對外的無需認證的接口,其餘的未標註的是給普通用戶使用的。github
示例代碼地址:auth-annotation - mengyunzhiweb
開發環境:Java 1.8
+ Spring Boot 2.1.2.RELEASE
spring
根據三類方法,對用戶權限進行攔截,使用攔截器 + AOP
的模式實現。編程
攔截器攔截下那些沒有AdminOnly
與Anonymous
註解標註的方法請求,並進行用戶認證。json
攔截器過完以後,去執行請求方法。設計模式
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
方法:
@Nullable public <A extends Annotation> A getMethodAnnotation(Class<A> annotationType) { return AnnotatedElementUtils.findMergedAnnotation(this.method, annotationType); }
該方法又調用了AnnotatedElementUtils.findMergedAnnotation
方法,咱們再點進去看看:
org.springframework.core.annotation.AnnotatedElementUtils
中的findMergedAnnotation
實現:
@Nullable public static <A extends Annotation> A findMergedAnnotation(AnnotatedElement element, Class<A> 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<String> 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<String> 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<ContextRefreshedEvent> { // 基礎包名 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<String> 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異常"); } } } }
學會了一個解決問題的新辦法:某個框架應該也遇到過你所遇到的問題,去找找框架中的工具類,這可能會頗有幫助。