java 切面_实用|AOP切面编程手段大汇总
前言
首先說一下什么是AOP?
????????AOP就是面向切面編程,它是一個思想,通過切面,我們可以將那些反復出現的代碼抽取出來,放在一個地方統一處理,提高代碼的復用性。AOP的一大好處就是解耦。以下幾種方式實現AOP:
1自定義注解+@Aspect
2攔截器
3過濾器
4.JDK動態代理和CGlib
5.設計模型--靜態代理
*.基于非侵入式運行時AOP方案(篇幅問題,不細說,感興趣的朋友可以自行百度阿里開源的jvm-Sandbox)
自定義注解+@Aspect 實現日志記錄1.首先你需要先引入pom依賴。(springboot2.x默認使用的代理是cglib代理)
<dependency> <groupId>org.springframework.bootgroupId> <artifactId>spring-boot-starter-aopartifactId>dependency><dependency> <groupId>com.google.code.gsongroupId> <artifactId>gsonartifactId> <version>2.8.5version>dependency>注意:?
? ? 在application.properties中也不需要添加spring.aop.auto=true,這個默認就是true,值為true就是啟用@EnableAspectJAutoProxy注解了。?
???你不需要手工添加在啟動類上添加 @EnableAspectJAutoProxy 注解。?
? 當你需要使用CGLIB來實現AOP的時候,需要配置spring.aop.proxy-target-class=true,這個默認值是false,不然默認使用的是標準Java的實現(JDK動態代理基于接口代理)。
????2.自定義日志注解(使用Java元注解,Java5.0定義了4個標準的meta-annotation類型)
@Retention(RetentionPolicy.RUNTIME)?//定義為運行時使用注解@Target({ElementType.METHOD})//在方法上使用注解@Documented//注解將包含javaDoc中public @interface WebLog { /**?????*?日志描述信息 * @return */?????//定義一個屬性,默認作為空字符串 String description() default "";}3.配置AOP切面類
@Aspect@Component?//將這個類交給Spring管理public class WebLogAspect {????private?final?static?Logger?logger?=?LoggerFactory.getLogger(WebLogAspect.class); /** 換行符 */ private static final String LINE_SEPARATOR = System.lineSeparator(); /** 以自定義 @WebLog 注解為切點 */????@Pointcut("@annotation(site.exception.aspect.WebLog)") //<=全路徑 public void webLog() {} /** * 在切點之前織入 * @param joinPoint * @throws Throwable */ @Before("webLog()") public void doBefore(JoinPoint joinPoint) throws Throwable { // 開始打印請求日志 ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();????????HttpServletRequest?request?=?attributes.getRequest(); // 獲取 @WebLog 注解的描述信息????????String?methodDescription?=?getAspectLogDescription(joinPoint); // 打印請求相關參數 logger.info("========================================== Start =========================================="); // 打印請求 url????????logger.info("URL:?{}",?request.getRequestURL().toString()); // 打印描述信息????????logger.info("Description?:?{}",?methodDescription); // 打印 Http method????????logger.info("HTTP?Method?:?{}",?request.getMethod()); // 打印調用 controller 的全路徑以及執行方法????????logger.info("Class?Method?:?{}.{}",?joinPoint.getSignature().getDeclaringTypeName(),?joinPoint.getSignature().getName()); // 打印請求的 IP????????logger.info("IP?:?{}",?request.getRemoteAddr()); // 打印請求入參????????logger.info("Request?Args?:?{}",?new?Gson().toJson(joinPoint.getArgs())); } /** * 在切點之后織入 * @throws Throwable */ @After("webLog()") public void doAfter() throws Throwable { // 接口結束后換行,方便分割查看 logger.info("=========================================== End ===========================================" + LINE_SEPARATOR); } /** * 環繞 * @param proceedingJoinPoint * @return * @throws Throwable */ @Around("webLog()") public Object doAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable { long startTime = System.currentTimeMillis();???????//執行切點后,會去依次調用?@Before ->?接口邏輯代碼(之后,執行完doAround方法)?->?@After ->?@AfterReturning; Object result = proceedingJoinPoint.proceed(); // 打印出參 logger.info("Response Args : {}", new Gson().toJson(result)); // 執行耗時 logger.info("Time-Consuming : {} ms", System.currentTimeMillis() - startTime); return result;????} /** * 獲取切面注解的描述 * * @param joinPoint 切點 * @return 描述信息 * @throws Exception */ public String getAspectLogDescription(JoinPoint joinPoint) throws Exception { String targetName = joinPoint.getTarget().getClass().getName(); String methodName = joinPoint.getSignature().getName(); Object[] arguments = joinPoint.getArgs(); Class targetClass = Class.forName(targetName); Method[] methods = targetClass.getMethods(); StringBuilder description = new StringBuilder(""); for (Method method : methods) { if (method.getName().equals(methodName)) { Class[] clazzs = method.getParameterTypes(); if (clazzs.length == arguments.length) { description.append(method.getAnnotation(WebLog.class).description()); break; } } } return description.toString(); }}4.使用注解
@PostMapping("/user")@WebLog(description="用戶請求接口")public?User?userLogin(@RequestBody?User user){??logger.info("user login ...");??return?user;?}特別說明
多切面如何指定優先級?
假設說我們的服務中不止定義了一個切面,比如說我們針對 Web 層的接口,不止要打印日志,還要校驗 token 等。要如何指定切面的優先級呢?也就是如何指定切面的執行順序?
我們可以通過 @Order(i)注解來指定優先級,注意:i 值越小,優先級則越高。
假設說我們定義上面這個日志切面的優先級為 @Order(10), 然后我們還有個校驗 token 的切面 CheckTokenAspect.java,我們定義為了 @Order(11), 那么它們之間的執行順序如下
?????????spring借鑒了AspectJ的切面,以提供注解驅動的AOP,本質上它依然是Spring基于代理的AOP,只是編程模型與AspectJ完全一致,這種風格的好處就是不需要使用XML進行配置。
通過攔截器實現
攔截器攔截的是URL
攔截器有三個方法,相對于過濾器更加細致,有被攔截邏輯執行前、后等。Spring中攔截器有三個方法:preHandle,postHandle,afterCompletion。
@Configurationpublic class HomeOpenHandlerConfigration extends WebMvcConfigurerAdapter { //關鍵,將攔截器作為bean寫入配置中 @Bean public HomeOpenInterceptor myInterceptor(){ return new HomeOpenInterceptor(); } @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(myInterceptor()).addPathPatterns("/api/open/portal/**") .excludePathPatterns("/api/open/footerInfo").excludePathPatterns("/api/open/portal/template/default"); super.addInterceptors(registry); }}/**?*?首頁外放攔截器 * */@Componentpublic class HomeOpenInterceptor extends HandlerInterceptorAdapter { @Autowired private PortalCommonService portalCommonService; @Autowired private ApplicationProperties applicationProperties; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { //判斷是否需要攔截 Boolean flag = false; if(flag){ //判斷是否允許不登錄的情況下 訪問主頁 //如果不允許匿名訪問返回401 throw new UnauthenticatedException(); } //否則允許直接放過,不進行任何攔截 return true; }}過濾器的實現
過濾器攔截的是URL
Spring中自定義過濾器(Filter)一般只有一個方法,返回值是void,當請求到達web容器時,會探測當前請求地址是否配置有過濾器,有則調用該過濾器的方法(可能會有多個過濾器),然后才調用真實的業務邏輯,至此過濾器任務完成。過濾器并沒有定義業務邏輯執行前、后等,僅僅是請求到達就執行。
特別注意:過濾器方法的入參有request,response,FilterChain,其中FilterChain是過濾器鏈,使用比較簡單,而request,response則關聯到請求流程,因此可以對請求參數做過濾和修改,同時FilterChain過濾鏈執行完,并且完成業務流程后,會返回到過濾器,此時也可以對請求的返回數據做處理。
@Component@Order(1)?//注解,配合?@WebFilter 注解使用,用于多個過濾器時定義執行順序,值越小越先執行。@WebFilter(urlPatterns = "/*", filterName = "test")public class TestFilter implements Filter { @Override public void init(FilterConfig arg0) throws ServletException { System.out.println("過濾器初始化"); } @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { System.out.printf("過濾器實現"); System.out.println(((HttpServletRequest) servletRequest).getRequestURI()); filterChain.doFilter(servletRequest, servletResponse); } @Override public void destroy() { System.out.println("過濾器銷毀了"); } }3.JDK動態代理?
@SuppressWarnings("restriction")public class JavaProxyTest { public static void main(String[] args) throws Exception { JavaProxyInterface javaProxyInterface = new ConcreteClass(); JavaProxyInterface newJavaProxyInterface = (JavaProxyInterface) Proxy.newProxyInstance( JavaProxyTest.class.getClassLoader(), new Class[] { JavaProxyInterface.class }, new MyInvocationHandler(javaProxyInterface)); //這里可以看到這個類以及被代理,在執行方法前會執行aopMethod()。這里需要注意的是oneDay()方法和oneDayFinal()的區別。oneDayFinal的方法aopMethod執行1次,oneDay的aopMethod執行1次 newJavaProxyInterface.gotoSchool(); newJavaProxyInterface.gotoWork(); newJavaProxyInterface.oneDayFinal(); newJavaProxyInterface.oneDay(); }}/*** InvocationHandler 的一個實現,實際上處理代理的邏輯在這里*/class MyInvocationHandler implements InvocationHandler { JavaProxyInterface javaProxy; public MyInvocationHandler(JavaProxyInterface javaProxy) { this.javaProxy = javaProxy; } private void aopMethod() { System.out.println("before method"); } //繼承方法,代理時實際執行的犯法,如果要實現原方法,則需要調用method.invoke(javaProxy, args),這里還調用了一個aopMethod(),可以類比于Spring中的切面before注解。 @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { aopMethod(); return method.invoke(javaProxy, args); }}/*** 需要一個最頂層接口,必須*/interface JavaProxyInterface { void gotoSchool(); void gotoWork(); void oneDay(); void oneDayFinal();}/*** 需要被代理的類,實現了頂層接口,非必須*/class ConcreteClass implements JavaProxyInterface { @Override public void gotoSchool() { System.out.println("gotoSchool"); } @Override public void gotoWork() { System.out.println("gotoWork"); } @Override public void oneDay() { gotoSchool(); gotoWork(); } @Override public final void oneDayFinal() { gotoSchool(); gotoWork(); }}底層實現部分代碼:
// proxyName 為類名,interfaces為頂層接口Classbyte[]?proxyClassFile?=?ProxyGenerator.generateProxyClass(proxyName,?interfaces);?File file = new File("D:/testProxy/Ddd.class");FileOutputStream fileOutputStream = new FileOutputStream(file);fileOutputStream.write(bs);fileOutputStream.flush();fileOutputStream.close();CGlib動態代理
public class CglibProxyTest { public static void main(String[] args) throws Exception { CglibTestSon CglibTestSon = new CglibTestSon(); Enhancer enhancer = new Enhancer(); Callback s = new MthdInvoker(CglibTestSon); enhancer.setSuperclass(CglibTestSon.class); Callback callbacks[] = new Callback[] { s }; enhancer.setCallbacks(callbacks); CglibTestSon CglibTestSon2 = (CglibTestSon) enhancer.create(); CglibTestSon2.gotoHome(); CglibTestSon2.gotoSchool();????//這里可以看到這個類以及被代理,在執行方法前會執行aopMethod()。這里需要注意的是oneDay()方法和onedayFinal()的區別。onedayFinal的方法aopMethod執行2次,oneDay的aopMethod執行1次?,注意這里和jdk的代理的區別 CglibTestSon2.oneday(); CglibTestSon2.onedayFinal(); }}/*** 需要被代理的類,不需要實現頂層接口*/class CglibTestSon { public CglibTestSon() { } public void gotoHome() { System.out.println("============gotoHome============"); } public void gotoSchool() { System.out.println("===========gotoSchool============"); } public void oneday() { gotoHome(); gotoSchool(); } public final void onedayFinal() { gotoHome(); gotoSchool(); }}/*** 可以類比于jdk動態代理中的InvocationHandler ,實際上被代理后重要的類,實際上后續執行的就是intercept里的方法,如果需要執行原來的方法,則調用 method.invoke(s, args);這里也加了一個aopMethod();*/class MthdInvoker implements MethodInterceptor { private CglibTestSon s; public MthdInvoker(CglibTestSon s) { this.s = s; } private void aopMethod() { System.out.println("i am aopMethod"); } public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable { aopMethod(); Object a = method.invoke(s, args); return a; }}CGlib底層實現部分代碼:
byte[] bs = DefaultGeneratorStrategy.INSTANCE.generate(enhancer);FileOutputStream fileOutputStream = new FileOutputStream("D:/testProxy/Cc.class");fileOutputStream.write(bs);fileOutputStream.flush();fileOutputStream.close();????簡單來看就是先生成新的class文件,然后加載到jvm中,然后使用反射,先用class取得他的構造方法,然后使用構造方法反射得到他的一個實例。
標紅的是最復雜的。然后cglib的實現原理基本一致,唯一的區別在于生成新的class文件方式和結果不一樣。
4.靜態代理模式的實現AOP
Font.java
package com.java.proxy; import lombok.Data; @Datapublic class Font { private String name;}FontProvider.java
package com.java.proxy; public interface FontProvider { Font getFont(String name); void printName(String name);}代理類CachedFontProvider.java
/**?*?給FontProvider的getFont添加緩存功能,用靜態代理來實現 * */public class CachedFontProvider implements FontProvider { private FontProvider fontProvider; private Map cached = new HashMap(); public CachedFontProvider() { fontProvider = new FontProviderFromDisk(); } @Override public Font getFont(String name) { System.out.println("靜態代理getFont()"); Font font = cached.get(name); if(font == null) { font = fontProvider.getFont(name); cached.put(name, font); } return font; } @Override public void printName(String name) { System.out.println("靜態代理printName()"); fontProvider.printName(name);; } }?
工廠類ProviderFactory.java
/** * 每個字體都增加了緩存功能,其實工廠就是用的緩存字體提供器,跟io一樣 * 使用代理(靜態),已經避免了再去修改每個字體提供器(這違反了開閉原則,而且工作量很大,容易出錯;而且如果要增加別的功能 * 比如日志打印,權限檢查,異常處理,每個都要去修改,代碼重復,而且很麻煩) * * ② 然而為什么要用動態代理? *考慮以下各種情況,有多個提供類,每個類都有getXxx(String name)方法,?*每個類都要加入緩存功能,使用靜態代理雖然也能實現,但是也是略顯繁瑣,需要手動一一創建代理類。 */public class ProviderFactory { public static FontProvider getFontProvider() { return new CachedFontProvider(); }??}?
測試類;
public?class?Business?{ public static void main(String[] args) { FontProvider fontProvider = ProviderFactory.getFontProvider(); Font font = fontProvider.getFont("微軟雅黑"); System.out.println(font); fontProvider.printName("代理模式實現AOP"); } }?
總結????????三者功能類似,但各有優勢,從過濾器--》攔截器--》切面,攔截規則越來越細致,執行順序依次是過濾器、攔截器、切面。一般情況下數據被過濾的時機越早對服務的性能影響越小,因此我們在編寫相對比較公用的代碼時,優先考慮過濾器,然后是攔截器,最后是aop。比如權限校驗,一般情況下,所有的請求都需要做登陸校驗,此時就應該使用過濾器在最頂層做校驗;日志記錄,一般日志只會針對部分邏輯做日志記錄,而且牽扯到業務邏輯完成前后的日志記錄,因此使用過濾器不能細致地劃分模塊,此時應該考慮攔截器,然而攔截器也是依據URL做規則匹配,因此相對來說不夠細致,因此我們會考慮到使用AOP實現,AOP可以針對代碼的方法級別做攔截,很適合日志功能。
點個“在看”表示朕
已閱
總結
以上是生活随笔為你收集整理的java 切面_实用|AOP切面编程手段大汇总的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: linux代码段映射,bss,data,
- 下一篇: 商品进销差价_商品进销差价概述