主頁 > 後端開發 > 愛上原始碼,重學Spring AOP深入

愛上原始碼,重學Spring AOP深入

2022-10-26 07:35:07 後端開發

AOP (Aspect Orient Programming):直譯過來就是 面向切面編程,AOP 是一種編程思想

用途:Transactions (事務呼叫方法前開啟事務, 呼叫方法后提交關閉事務 )、日志、性能(監控方法運行時間)、權限控制等

也就是對業務方法做了增強

1.1 Spring AOP環境介紹

目標:認識AOP基礎環境,后面講使用這個基礎環境進行原始碼講解

tips:

沿用ioC的工廠

1)引入起步依賴

    compile(project(':spring-aop'))
    compile(project(':spring-context'))
    compile 'org.aspectj:aspectjweaver:1.9.2'

2)新建介面和實作

public interface Slaver {
	void work();
}

import org.springframework.stereotype.Service;

@Service
public class SlaverImpl implements Slaver {
	public void work() {
		System.out.println("進入實作類 work.....");
		try {
			Thread.sleep(1000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}

	}

}

3)新建切面類

package com.spring.test.aop.aop;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

import java.util.Arrays;

//將這個類宣告為一個切面,需要將其放入IOC容器中
@Aspect//宣告這是一個切面
@Component//宣告這是一個組件
/**
 * 執行順序
 * @Around進入環繞通知...
 * @Before進入前置通知:[]
 * 進入實作類 work.....
 * @Around方法執行耗時>>>>>: 1001
 * @After進入后置通知...
 * @AfterReturning進入最終通知...End!
 */
public class SlaverAspect {

	//環繞通知(連接到切入點開始執行,下一步進入前置通知,在下一步才是執行操作方法)
	@Around(value = "https://www.cnblogs.com/jiagooushi/p/pointCut()")
	public void around(ProceedingJoinPoint joinPoint) throws Throwable {
		System.out.println("@Around進入環繞通知...");
		long startTime = System.currentTimeMillis();
		joinPoint.proceed();
		long endTime = System.currentTimeMillis();
		System.out.println(String.format("@Around方法執行耗時>>>>>: %s", endTime - startTime));
	}

	//前置通知(進入環繞后執行,下一步執行方法)
	@Before(value = "https://www.cnblogs.com/jiagooushi/p/pointCut()")
	public void before(JoinPoint joinPoint) {
		System.out.println("@Before進入前置通知:" + Arrays.toString(joinPoint.getArgs()));
	}

	//例外通知(出錯時執行)
	@AfterThrowing(value = "https://www.cnblogs.com/jiagooushi/p/pointCut()", throwing = "ex")
	public void afterThrowing(JoinPoint joinPoint, Throwable ex) {
		System.out.println("@AfterThrowing進入例外通知" + Arrays.toString(joinPoint.getArgs()));
		System.out.println("@AfterThrowing例外資訊:" + ex);
	}

	//后置通知(回傳之前執行)
	@After(value = "https://www.cnblogs.com/jiagooushi/p/pointCut()")
	public void after() {

		System.out.println("@After進入后置通知...");
	}

	//最終通知(正常回傳通知,最后執行)
	@AfterReturning(value = "https://www.cnblogs.com/jiagooushi/p/pointCut()")
	public void afterReturning() {
		System.out.println("@AfterReturning進入最終通知...End!");
	}

	//定義一個切入點 后面的通知直接引入切入點方法pointCut即可
//	@Pointcut("execution(public * com.spring.test.aop.impl.SlaverImpl.work())")
	@Pointcut(value = "https://www.cnblogs.com/jiagooushi/p/execution(* com.spring.test.aop.impl.SlaverImpl.*(..))")
	public void pointCut() {
	}

}

AspectJ 支持 5 種型別的通知注解:
@Before: 前置通知, 在方法執行之前執行
@After: 后置通知, 在方法執行之后執行
@AfterRunning: 回傳通知, 在方法回傳結果之后執行
@AfterThrowing: 例外通知, 在方法拋出例外之后
@Around: 環繞通知, 圍繞著方法執行

execution:用于匹配方法執行的連接點;

4)新建組態檔

resources/application-aop.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	   xmlns:aop="http://www.springframework.org/schema/aop"
	   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	   xmlns:context="http://www.springframework.org/schema/context"
	   xsi:schemaLocation="http://www.springframework.org/schema/beans
	   http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/aop
       http://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"
>
	<!-- 使 AspectJ 的注解起作用 -->
	<aop:aspectj-autoproxy/>
	<!-- 掃描帶有注解的類,交給ioc容器管理 -->
	<context:component-scan base-package="com.spring.test.aop"/>
</beans>

常見錯誤

組態檔缺乏xsd的參考

file

5)新建入口類

public class Main {

	public static void main(String[] args) {
		ApplicationContext context =
				new ClassPathXmlApplicationContext("classpath*:application-aop.xml");
		Slaver slaver=(Slaver)context.getBean("slaverImpl");
		slaver.work();
	}

}

6)運行效果

file

1.2 SpringAOP和AspectJ聯系

tips:

十個人有九 個人弄不懂的關系

Spring AOP:

Spring AOP旨在通過Spring IoC提供一個簡單的AOP實作,以解決編碼人員面臨的最常出現的問題,這并不是完整的AOP解決方案,它只能用于Spring容器管理的beans,

AspectJ:

AspectJ是最原始的AOP實作技術,提供了完整的AOP解決方案,AspectJ更為健壯,相對于Spring AOP也顯得更為復雜

file

總結

AOP是面向切面的一個思想
他有兩種實作
1、Spring AOP
2、Aspectj
Spring AOP的實作沒有AspectJ強大
所以,Spring把Aspectj給集成(如果用,還需要單獨引jar)進來了
但是;spring aop已能滿足我們的需求
在進行開發時候,這兩個框架是完全兼容的

說白了,就是兩個框架能一起使用,就看你專案需求用到的哪種程度了
簡單的;spirng aop夠用了,但是spring aop借助了aspectj的注解功能,
在高級點,比如切面很多,上萬個,這是就要用到aspectj的高級功能了

區別:AspectJ使用的是編譯期和類加載時進行織入,Spring AOP利用的是運行時織入

依賴:如果使用@Aspect注解,在xml里加上<aop:aspectj-autoproxy />,但是這需要額外的jar包( aspectjweaver.jar)

因為spring直接使用AspectJ的注解功能,注意只是使用了它 的注解功能而已,并不是核心功能 !

運行效果如下:

file

1.3 找到處理AOP的源頭

1、Spring處理AOP源頭在哪里(織入)

AspectJ編譯期和類加載時進行織入、Spring AOP利用的是運行時織入

猜想:
1. 在容器啟動時創建?

2.在getBean時創建?

容器啟動

2、代理物件到底長什么樣?

將斷點打在getBean的回傳物件上,發現這并不是一個我們定義的物件本身,而是一個Proxy

file

接下來,我們會找到$Proxy生成的始末

3、我的介面是怎么被A0P管理上的 ?

com.spring.test.aop.aop.SlaverAspect#pointCut

//定義一個切入點 后面的通知直接引入切入點方法pointCut即可
	//引數:
	//第一個”*“符號;表示回傳值的型別任意
	//.*(..)表示任何方法名,括號表示引數,兩個點表示任何引數型別
	@Pointcut(value = "https://www.cnblogs.com/jiagooushi/p/execution(* com.spring.test.aop.impl.SlaverImpl.*(..))")
	public void pointCut() {
		System.out.println("@進入切點...");
	}

tips:

當然是execution宣告,改成 SlaverImpl.aaaa*(..) , 再來看getBean的物件,不再是proxy

斷點一下……

1.4 代理物件是怎么生成的

1、AOP其實就是用的動態代理模式,創建代理

2、AOP織入源頭在哪里

目標:通過源頭找代理物件是如何生成的

? 找到生成代理的地方Proxy.newProxyInstance

0)回顧助學

簡單回顧一下,注意,這里不僅僅是學習代理模式,還必須搞懂它的實作方式,尤其是動態代理,

開始之前,我們先必須搞懂一件事情:

那就是:代理模式

設計模式【靜態代理】 【動態代理】 回顧 (見第2小節)

1)從源頭找代理物件生成

記住目標:

spring aop使用的就是代理模式,那我們的目標就明確了:找到生成代理的地方Proxy.newProxyInstance

先來張流程圖:

下面我們沿著圖中的呼叫鏈,找到aop 代理誕生的地方

file

tips:從后置處理器開始

為什么要從后置處理器入手?

很容易理解,沒初始化好沒法用,等你初始化好了功能齊備了,我再下手,代替你

找到后置處理器

重點關注postProcessAfterInitialization

此處最好使用斷點運算式,否則要回圈很多次

因為在refresh方法中的invokeBeanFactoryPostProcessors方法也會呼叫到這個地方

斷點運算式:

file

關注點:

tips:

在BeanPostProcessor回圈中,觀察AnnotationAwareAspectJAutoProxyCreator

這貨就是切面的后置處理器

AbstractAutowireCapableBeanFactory#applyBeanPostProcessorsAfterInitialization

	//目標:回圈所有的后置處理器進行呼叫
	//注意:
	//AOP除錯,此處最好使用斷點運算式,否則要回圈很多次
	//因為在refresh方法中的invokeBeanFactoryPostProcessors方法也會呼叫到這個地方
	@Override
	public Object applyBeanPostProcessorsAfterInitialization(Object existingBean, String beanName)
			throws BeansException {

		Object result = existingBean;
		//aop
		//此處getBeanPostProcessors()有8個內置后置處理器;生成代理會呼叫里面的 AnnotationAwareAspectJAutoProxyCreator
		for (BeanPostProcessor beanProcessor : getBeanPostProcessors()) {
			//Aop呼叫AbstractAutoProxyCreator#postProcessAfterInitialization,
			Object current = beanProcessor.postProcessAfterInitialization(result, beanName);
			if (current == null) {
				return result;
			}
			result = current;
		}
		return result;
	}

如上所見

也就是說AOP模塊是通過實作BeanPostProcessor集成進來的

2)進入后置處理器

tips:

aop這是spring內置的一個后置處理器,生效在postProcessAfterInitialization方法

AbstractAutoProxyCreator#postProcessAfterInitialization

重點關注wrapIfNecessary

	//如果當前的bean適合被代理,則需要包裝指定的bean
	@Override
	public Object postProcessAfterInitialization(@Nullable Object bean, String beanName) throws BeansException {
		if (bean != null) {
			// 根據給定的bean的class和name構建一個key
			Object cacheKey = getCacheKey(bean.getClass(), beanName);
			if (!this.earlyProxyReferences.contains(cacheKey)) {

				// 如果當前的bean適合被代理,則需要包裝指定的bean
				return wrapIfNecessary(bean, beanName, cacheKey);
			}
		}
		return bean;
	}

經歷wrapIfNecessary方法,重點關注點有兩個:

1是:getAdvicesAndAdvisorsForBean,找到哪些切面會作用在當前bean上,滿足條件的抓出來!

2是:createProxy,生成代理,替代slaverImpl去做事

	//目標
	//1、判斷當前bean是否已經生成過代理物件,或者是否是應該被略過的物件,是則直接回傳,否則進行下一步
	//2、拿到切面類中的所有增強方法(攔截器:環繞、前置、后置等)
	//3、生成代理物件
	protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) {
		// 判斷是否為空
		// 判斷當前bean是否在TargetSource快取中存在,如果存在,則直接回傳當前bean
		if (StringUtils.hasLength(beanName) && this.targetSourcedBeans.contains(beanName)) {
			return bean;
		}
		// 這里advisedBeans快取了不需要代理的bean(為false的),如果快取中存在,則可以直接回傳
		if (Boolean.FALSE.equals(this.advisedBeans.get(cacheKey))) {
			return bean;
		}
		//Infrastructure基礎設施
		// 用于判斷當前bean是否為Spring系統自帶的bean,自帶的bean是
		// 不用進行代理的;shouldSkip()則用于判斷當前bean是否應該被略過
		if (isInfrastructureClass(bean.getClass()) || shouldSkip(bean.getClass(), beanName)) {
			// 對當前bean進行快取
			this.advisedBeans.put(cacheKey, Boolean.FALSE);
			return bean;
		}
    
    
    
    
    
		//AOP:【關鍵點1】反射來過濾,看看哪些aspect的execution能匹配上當前bean
		// ===【【【【注意!這貨要分兩步除錯講解,修改切面運算式做對比】】】】====
		// 將SlaverAspect的 execution改成 SlaverImpl.aaa*(..)  試試,你將得到一個空陣列!!!
		// 匹配上的話列出前后和置換的方法(攔截器:環繞、前置、后置等)
		Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, null);

    
    
		//如果拿到的增強方法不為空
		if (specificInterceptors != DO_NOT_PROXY) {
			// 對當前bean的代理狀態進行快取
			this.advisedBeans.put(cacheKey, Boolean.TRUE);
      
      
      
			// 開始創建AOP代理
      //  ===【關鍵點2】===
			Object proxy = createProxy(
					bean.getClass(), beanName, specificInterceptors, new SingletonTargetSource(bean));
      
      
      
      
			// 快取生成的代理bean的型別,并且回傳生成的代理bean
			this.proxyTypes.put(cacheKey, proxy.getClass());
			//此處回傳的代理和在Main函式中回傳的是一樣的
			//說明此處代理成功創建
			return proxy;
		}
		//如果拿到的增強方法為空,快取起來(使用false標記不需要代理)
		this.advisedBeans.put(cacheKey, Boolean.FALSE);
		return bean;
	}

3)開始創建代理物件

重點關注最下面的關鍵點:proxyFactory.getProxy(getProxyClassLoader())

	//beanClass:目標物件class
	//beanaName
	//specificInterceptors:攔截器里面的攔截方法
	//targetSource:目標資源
	//目標:開始為bean創建代理
	protected Object createProxy(Class<?> beanClass, @Nullable String beanName,
								 @Nullable Object[] specificInterceptors, TargetSource targetSource) {
		//為true,DefaultListableBeanFactory
		if (this.beanFactory instanceof ConfigurableListableBeanFactory) {
			//給當前的bd設定屬性setAttribute("originalTargetClass",bean的class),進入
			AutoProxyUtils.exposeTargetClass((ConfigurableListableBeanFactory) this.beanFactory, beanName, beanClass);
		}
		//創建一個默認的代理工廠DefaultAopProxyFactory,父類無參構造器
		ProxyFactory proxyFactory = new ProxyFactory();


		//proxyFactory通過復制配置進行初始化
		//this為AbstractAutoProxyCreator物件,說明AbstractAutoProxyCreator繼承引數實際型別
		proxyFactory.copyFrom(this);
		/**
		 * isProxyTargetClass(),默認false
		 * true
		 *目標物件沒有介面(只有實作類) – 使用CGLIB代理機制
		 * false
		 * 目標物件實作了介面 – 使用JDK代理機制(代理所有實作了的介面)
		 */
		if (!proxyFactory.isProxyTargetClass()) {
			//來判斷@EnableAspectJAutoProxy注解或者XML的proxyTargetClass引數(true或者false)
			//看看用戶有沒有指定什么方式生成代理
			// 如果沒配置就為空,此處回傳false
			if (shouldProxyTargetClass(beanClass, beanName)) {
				proxyFactory.setProxyTargetClass(true);
			} else {
				//評估介面的合理性,一些內部回呼介面,比如InitializingBean等,不會被實作jdk代理
				evaluateProxyInterfaces(beanClass, proxyFactory);
			}
		}
		// 把advice(增強)型別的增強包裝成advisor型別(強制型別轉換)
		Advisor[] advisors = buildAdvisors(beanName, specificInterceptors);
		//加入到代理工廠
		proxyFactory.addAdvisors(advisors);
		//設定要代理的類(目標類)
		proxyFactory.setTargetSource(targetSource);
		//子類實作, 定制代理
		customizeProxyFactory(proxyFactory);
		//用來控制代理工廠被設定后是否還允許修改通知,預設值為false
		proxyFactory.setFrozen(this.freezeProxy);
		//明明是false?? 此處注意,他是在子類AbstractAdvisorAutoProxyCreator重寫了advisorsPreFiltered方法
		if (advisorsPreFiltered()) {
			//設定預過濾
			proxyFactory.setPreFiltered(true);
		}

		//【關鍵點】通過類加載期獲取代理;getProxyClassLoader為默認的類加載器
		return proxyFactory.getProxy(getProxyClassLoader());
	}

進入getProxy

//通過類加載期獲取代理
public Object getProxy(@Nullable ClassLoader classLoader) {
   //分別進入createAopProxy 和getProxy
   return createAopProxy().getProxy(classLoader);
}

先看createAopProxy

查看回傳jdk代理還是cglib代理

public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException {
		// isOptimize:是否對代理進行優化
		// isProxyTargetClass:值為true,使用CGLIB代理,默認false
		// hasNoUserSuppliedProxyInterfaces:
		//1、如果長度為0;也就是介面為空,回傳false
		//or(或的關系)
		//2、如果介面型別不是SpringProxy型別的;回傳flase

		//如果條件不滿足;直接走JDK動態代理(return)
		if (config.isOptimize() || config.isProxyTargetClass() || hasNoUserSuppliedProxyInterfaces(config)) {
			Class<?> targetClass = config.getTargetClass();
			if (targetClass == null) {
				throw new AopConfigException("TargetSource cannot determine target class: " +
						"Either an interface or a target is required for proxy creation.");
			}
			// 如果targetClass是介面類,使用JDK來生成Proxy
			//Tips
			//如果目標物件實作了介面,默認情況下會采用JDK動態代理,
			// 但也可以通過配置(proxy-target-class=true)強制使用CGLIB,
			if (targetClass.isInterface() || Proxy.isProxyClass(targetClass)) {
				//jdk
				return new JdkDynamicAopProxy(config);
			}
			//cglib
			return new ObjenesisCglibAopProxy(config);
		} else {
			//jdk
			return new JdkDynamicAopProxy(config);
		}
	}

再看getProxy

	//獲取最終的代理物件(由JDK生成;運行時織入)
	@Override
	public Object getProxy(@Nullable ClassLoader classLoader) {
		if (logger.isDebugEnabled()) {
			logger.debug("Creating JDK dynamic proxy: target source is " + this.advised.getTargetSource());
		}
		//獲取代理物件需要實作的介面(業務介面和內置介面)
		Class<?>[] proxiedInterfaces = AopProxyUtils.completeProxiedInterfaces(this.advised, true);
		//判斷介面中是否重寫了equals和hashCode方法
		findDefinedEqualsAndHashCodeMethods(proxiedInterfaces);

		/**
		 * 第一個引數是類加載器(目標類)
		 * 第二個引數是代理類需要實作的介面,即目標類實作的介面(含系統介面)(陣列)
		 * 第三個是InvocationHandler,本類實作了此介面
		 */
		return Proxy.newProxyInstance(classLoader, proxiedInterfaces, this);
	}

找到了!代理物件在bean初始化階段裝配進了spring

4)代理物件驗證

tips:

前面創建完成了代理物件,下面我們看下它呼叫的時候,是不是走了代理

這是我們一開始的結果,下面我們來再debug到work方法時,點擊debug into試試,發現調到哪里去了???

file

結論:

沒錯!代理物件精準的呼叫了JdkDynamicAopProxy里面的invoke方法

這說明jdk動態代理生效,但是它生成的proxy位元組碼在jvm里,我們是看不到的,怎么破?

arthas上!

2)Arthas代理類驗證

arthas,阿里神器,主頁:https://arthas.aliyun.com/zh-cn/

獲取之前,我們需要做點小事情,打出我們的 slaver的類路徑,讓arthas能找到它!

file
原始碼:

package com.aoptest;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class Main {

	public static void main(String[] args) throws InterruptedException {
		ApplicationContext context =
				new ClassPathXmlApplicationContext("classpath*:application-aop.xml");
		Slaver slaver=(Slaver)context.getBean("slaverImpl");
		slaver.work();
		System.out.printf(slaver.getClass().getName());  //列印當前物件的類名稱:com.sun.proxy.$Proxy17
		Thread.sleep(Integer.MAX_VALUE); // 必須停留在這里,debug是不行的,arthas會連不上
	}

}

開啟arthas:

  • 很簡單, java -jar arthas-boot.jar
  • 啟動后,在arthas里執行:jad com.sun.proxy.$Proxy17 反編譯我們的代理類

注意,jad后面的,換成你上一步控制臺列印出來的

見證奇跡的時刻……

找到里面的work方法,結果它長這樣……

file

那么,this.h呢?

殺死行程,回到debug模式,看看

file

沒錯,就是我們的jdk動態代理類!

現在呼叫關系明確了,接下來,我們就來分析下這個invoke

1.5 代理物件如何呼叫

1)先看張圖

代理物件呼叫鏈如下圖 (責任鏈,先有個印象,很長,很長……)

file

2)理解責任鏈

代碼中給大家準備了一個責任鏈小demo,它就是我們spring aop切面呼叫鏈的縮影

spring-aop-test 專案下的 com.spring.test.aop.chain 包,

執行里面的main方法

生活中的例子,類似于:

  • 全公司站成一隊,從隊首開始簽名

    如果先簽完再交給下一個,你就是前置攔截,(簽名 = 執行切面任務)

    如果先交給下一個,等傳回來的時候再簽,你就實作了后置,回來的時候再補

  • 一個個傳到底后,到達隊尾,老板蓋章(真正的業務邏輯得到執行)

  • 然后一個個往回傳(return),前面沒簽的那些家伙補上(后置切面任務得到執行)

3)invoke方法原始碼分析

通過上小節我們知道,代理模式下執行的是以下invoke

org.springframework.aop.framework.JdkDynamicAopProxy#invoke

代碼重點關注

// 2.從ProxyFactory(this.advised)中構建攔截器鏈,包含了目標方法的所有切面方法
List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass)
		
//責任鏈開始呼叫:ReflectiveMethodInvocation.proceed();重點關注!!!
retVal = invocation.proceed();

責任鏈構建:chain里面為責任鏈的具體任務

接下來,我們具體看看責任鏈具體的處理邏輯

4)AOP核心之責任鏈

思考:

責任鏈呼叫,呼叫的什么?

責任鏈目標:

file

AOP責任鏈呼叫流程簡圖

file

注意:上圖的顏色區分

黑色:表示正向呼叫,invoke的時候,在前或后執行自己的切面邏輯,然后推動責任鏈往下走

紅色:表示當前切面任務觸發的點

備注:ExposeInvocationInterceptor 是spring幫我們加上做背景關系傳遞的,本圖不涉及(它在最前面)

注意,我們的鏈條如下:

file

除錯技巧:

從 ReflectiveMethodInvocation.invoke() 開始,對照上面的責任鏈嵌套呼叫圖

每次通過debug into 進行查看,每到一環,注意對照圖上的節點,看到那個invocation了!

file
案例:第一環,遇proceed,再 debug into

file

依次回圈,步步跟進,其樂無窮~~~

2 AOP基礎 - 代理模式(助學)

2.1 設計模式之代理

背景
舉個例子
假設我們想邀請一位明星,不聯系明星
而是聯系明星的經紀人,來達到邀請的的目的
明星就是一個目標物件,他只要負責活動中的節目,而其他瑣碎的事情就交給他的代理人(經紀人)來解決.
這就是代理思想在現實中的一個例子

什么是代理?

代理(Proxy)是一種設計模式

提供了對目標物件另外的訪問方式;即通過代理物件訪問目標物件.

這樣做的好處是:可以在目標物件實作的基礎上,增強額外的功能操作

file
代理模式分類

1、靜態代理

2、動態代理

2.2 靜態代理模式

靜態代理在使用時:

需要定義介面、目標物件與代理物件

重要特點:

靜態代理是由程式員創建或工具生成代理類的原始碼,再編譯代理類,

介面

//介面
public interface IStartBusiness {
	//邀請明星唱歌
	void sing();
}

目標物件,實作類

//目標物件,實作類
public class StartBusinessImpl implements IStartBusiness {
	@Override
	public void sing() {
		System.out.println("sing>>>>>>>>>>>>>>>>>>>>>>>>>>>");
	}
}

代理物件,靜態代理

package com.spring.test.aop.pattern.proxy.staticproxy;

//代理物件,靜態代理
public class AgentProxy implements IStartBusiness {

	//代理類持有一個目標類的物件參考
	private IStartBusiness iStartBusiness;
	//構造注入目標物件
	public AgentProxy(IStartBusiness iStartBusiness) {
		this.iStartBusiness = iStartBusiness;
	}

	@Override
	public void sing() {

		//**********方法前增強****************


		//do something
		//將請求分派給目標類執行;通過注入進入來的目標物件進行訪問
		this.iStartBusiness.sing();

		//do after

		//**********方法后增強****************
	}
}

靜態代理測驗

package com.spring.test.aop.pattern.proxy.staticproxy;

//靜態代理測驗
public class Test {
	public static void main(String[] args) {
		//目標物件
		IStartBusiness target = new StartBusinessImpl();
		//代理物件,把目標物件傳給代理物件,建立代理關系
		IStartBusiness proxy = new AgentProxy(target);
		//呼叫的時候通過呼叫代理物件的方法來呼叫目標物件
		proxy.sing();
	}

}

輸出

file

優點:

可以在被代理方法的執行前或后加入別的代碼,實作諸如權限及日志的操作,

缺點:

1、如果介面增加一個方法,除了所有實作類需要實作這個方法外,所有代理類也需要實作此方法

?

總結:(記住兩點)

1、只需要知道靜態代理是在運行前代理類就已經被織入進去了

2、大規模使用靜態代理難以維護(增加方法)

有沒有其他的方式可以減少代碼的維護,那就是動態代理?

2.3 動態代理模式

什么是動態代理?

動態代理類的原始碼是在程式運行期間由JVM根據反射等機制動態織入的

所以;不存在代理類的位元組碼檔案,直接進了虛擬機,(但是有辦法給抓到他)

JDK中生成代理物件的API最重要類和介面有兩個,如下

Proxy

InvocationHandler

1)代理父類Proxy回顧

所在包:java.lang.reflect.Proxy

這是 Java 動態代理機制生成的所有動態代理類的父類,

提供了一組靜態方法來為一組介面動態地生成代理類及其物件,

Proxy類的靜態方法(了解)

// 方法 1: 該方法用于獲取指定代理物件所關聯的呼叫處理器  
static InvocationHandler getInvocationHandler(Object proxy)   
  
// 方法 2:該方法用于獲取關聯于指定類裝載器和一組介面的動態代理類的類物件  
static Class getProxyClass(ClassLoader loader, Class[] interfaces)   
  
// 方法 3:該方法用于判斷指定類物件是否是一個動態代理類  
static boolean isProxyClass(Class cl)   
  
// 方法 4:該方法用于為指定類裝載器、一組介面及呼叫處理器生成動態代理類實體  
static Object newProxyInstance(ClassLoader loader, Class[] interfaces, InvocationHandler h)

tips:重要

重點關注newProxyInstance

這三個引數非常重要

Spring Aop也是使用這個機制

    public static Object newProxyInstance(ClassLoader loader,
                                          Class<?>[] interfaces,
                                          InvocationHandler h)
        throws IllegalArgumentException

注意該方法是在Proxy類中是靜態方法,且接收的三個引數依次為:

  • ClassLoader loader,:指定當前目標物件使用類加載器 ;負責將類的位元組碼裝載到 Java 虛擬機(JVM)中并為其定義類物件
  • Class<?>[] interfaces,:目標物件實作的介面的型別,使用泛型方式確認型別
  • InvocationHandler h:事件處理,執行目標物件的方法時,會觸發事件處理器的方法,會把當前執行目標物件的方法作為引數傳入

2)呼叫處理器介面回顧

java.lang.reflect.InvocationHandler

這是呼叫處理器介面,它自定義了一個 invoke 方法(只有一個)

用于集中處理在動態代理類物件上的方法呼叫,通常在該方法中實作對目標類的代理訪問,

每次生成動態代理類物件時都要指定一個對應的呼叫處理器物件,

file

1、目標物件(委托類)通過jdk的Proxy生成了代理物件、

2、客戶端訪問代理物件,代理物件通過呼叫于處理器介面反射呼叫了目標物件方法

InvocationHandler的核心方法

僅僅一個方法

public interface InvocationHandler {

//第一個引數既是代理類實體
 //第二個引數是被呼叫的方法物件  
// 第三個方法是呼叫引數   
Object invoke(Object proxy, Method method, Object[] args) 

}

3)動態代理代碼撰寫

沿用上面的 例子

tips

代理物件不需要實作介面(業務),但是目標物件一定要實作介面,否則不能用動態代理

介面

目標物件介面

package com.spring.test.aop.pattern.proxy.dynamic;
//介面
public interface IStartBusiness {
	//邀請明星唱歌
	void sing();
}

目標物件

//目標物件
public class StartBusinessImpl implements IStartBusiness {
	@Override
	public void sing() {
		System.out.println("sing>>>>>>>>>>>>>>>>>>>>>>>>>>>");
	}
}

創建動態代理物件

package com.spring.test.aop.pattern.proxy.dynamic;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

 //動態代理實作
public class DynamicProxy implements InvocationHandler {
	// 這個就是我們要代理的真實物件
	private Object obj;

	// 構造方法,給我們要代理的真實物件賦初值
	public DynamicProxy(Object object) {
		this.obj = object;
	}

	//相比靜態代理,動態代理減只需要實作一個介面即可完成,而靜態代理每次都要實作新加的方法以及維護被代理方法
    
    //第一個引數既是代理類實體
 //第二個引數是被呼叫的方法物件  
// 第三個方法是呼叫引數   
	@Override
	public Object invoke(Object object, Method method, Object[] args)
			throws Throwable {

		//********************方法前增強***************************
		// 反射呼叫目標方法
		return method.invoke(obj, args);
	

		//********************方法后增強***************************
	}

}

測驗

package com.spring.test.aop.pattern.proxy.dynamic;

import com.spring.test.aop.pattern.proxy.staticproxy.IStartBusiness;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;

//動態代理測驗
//目標:
//1、知道如何創建動態代理
//2、如何呼叫了invoke方法(攔截方法會用到當前知識點)
public class Test {

	public static void main(String[] args) {
		// 目標物件;要代理的真實物件
		IStartBusiness target = new StartBusinessImpl();
      // 我們要代理哪個真實物件,就將該物件傳進去,最后是通過該真實物件來呼叫其方法的
		InvocationHandler handler = new DynamicProxy(target);
		/*
		 * 第一個引數:目標物件加載器
		 * 第二個引數:目標物件介面
		 * 第三個引數:實作InvocationHandler的代理類
		 */

      //生成代理物件
		IStartBusiness iStartBusiness = (IStartBusiness) Proxy.newProxyInstance(target.getClass().getClassLoader(), target
				.getClass().getInterfaces(), handler);
		iStartBusiness.sing();


	}
}


輸出

file

總結

1、相比靜態代理,動態代理減只需要實作一個介面即可完成,而靜態代理每次都要實作新加的方法以及維護被代理方法

2、動態代理是靠Proxy.newProxyInstance() 生成的

3、動態代理在呼叫(iStartBusiness.sing())的時候,呼叫到了 implements InvocationHandler 的invoke

目標明確了:spring的代理就需要我們找到 Proxy.newProxyInstance() 在哪里……

4)動態代理原理(了解)

tips:

涉及JDK位元組碼,不在spring的原始碼范圍內,感興趣的同學自己了解一下

參考資料:https://www.baiyp.ren/JAVA動態代理原始碼分析.html

代理物件內部生成流程呼叫鏈,如下圖

file

物件生成程序(核心)

file

Proxy.newProxyInstance(classLoader, proxiedInterfaces, this);
進入newProxyInstance方法內部

java.lang.reflect.Proxy#newProxyInstance

重點關注核心方法getProxyClass0

 
//使用jdk代理工廠創建新的代理實體
@CallerSensitive
    public static Object newProxyInstance(ClassLoader loader,
                                          Class<?>[] interfaces,
                                          InvocationHandler h)
        throws IllegalArgumentException
    {
         //判斷是否實作InvocationHandler介面,如果此處為空拋出例外
        //當前h非常重要,因為在代理物件呼叫目標方法的時候,就是通過d的invoke方法反射呼叫的目標方法
        //稍后會講解
        Objects.requireNonNull(h);
        //克隆引數傳來的 介面
        final Class<?>[] intfs = interfaces.clone();
        //系統內部的安全
        final SecurityManager sm = System.getSecurityManager();
        if (sm != null) {
            //訪問權限的驗證
            checkProxyAccess(Reflection.getCallerClass(), loader, intfs);
        }

        /*
         *查找或生成指定的代理類;非常重要,重點關注
         */
        Class<?> cl = getProxyClass0(loader, intfs);

        /*
         * Invoke its constructor with the designated invocation handler.
         */
        try {
            if (sm != null) {
                //代理權限的檢查(此處是新生產的代理物件c1)
                checkNewProxyPermission(Reflection.getCallerClass(), cl);
            }
            //從生成的代理物件(只是class物件,還不是實體化物件)中取出構造器
            final Constructor<?> cons = cl.getConstructor(constructorParams);
            final InvocationHandler ih = h;
            if (!Modifier.isPublic(cl.getModifiers())) {
                AccessController.doPrivileged(new PrivilegedAction<Void>() {
                    public Void run() {
                        cons.setAccessible(true);
                        return null;
                    }
                });
            }
            //使用構造器實體化(實體化物件)
            return cons.newInstance(new Object[]{h});
        } catch (IllegalAccessException|InstantiationException e) {
            throw new InternalError(e.toString(), e);
        } catch (InvocationTargetException e) {
            Throwable t = e.getCause();
            if (t instanceof RuntimeException) {
                throw (RuntimeException) t;
            } else {
                throw new InternalError(t.toString(), t);
            }
        } catch (NoSuchMethodException e) {
            throw new InternalError(e.toString(), e);
        }
    }

進入getProxyClass0

java.lang.reflect.Proxy#getProxyClass0

    private static Class<?> getProxyClass0(ClassLoader loader,
                                           Class<?>... interfaces) {
        //判斷介面陣列大小,別大于65535,如果大于,提示介面超出限制
        if (interfaces.length > 65535) {
            throw new IllegalArgumentException("interface limit exceeded");
        }

        //通過介面和類加載器創建代理

        //給定的介面存在,這將簡單地回傳快取副本;

         //否則,它將通過ProxyClassFactory創建代理類          
        return proxyClassCache.get(loader, interfaces);//也就是查詢+創建的程序
    }

繼續進入get方法

java.lang.reflect.WeakCache#get

其他不要看

重點關注apply方法

    public V get(K key, P parameter) {
        Objects.requireNonNull(parameter);

        expungeStaleEntries();

        Object cacheKey = CacheKey.valueOf(key, refQueue);

        // lazily install the 2nd level valuesMap for the particular cacheKey
        ConcurrentMap<Object, Supplier<V>> valuesMap = map.get(cacheKey);
        if (valuesMap == null) {
            ConcurrentMap<Object, Supplier<V>> oldValuesMap
                = map.putIfAbsent(cacheKey,
                                  valuesMap = new ConcurrentHashMap<>());
            if (oldValuesMap != null) {
                valuesMap = oldValuesMap;
            }
        }

        // create subKey and retrieve the possible Supplier<V> stored by that
        // subKey from valuesMap
        Object subKey = Objects.requireNonNull(subKeyFactory.apply(key, parameter));
        Supplier<V> supplier = valuesMap.get(subKey);
        Factory factory = null;

        while (true) {
            if (supplier != null) {
                // supplier might be a Factory or a CacheValue<V> instance
                V value = https://www.cnblogs.com/jiagooushi/p/supplier.get();
                if (value != null) {
                    return value;
                }
            }
            // else no supplier in cache
            // or a supplier that returned null (could be a cleared CacheValue
            // or a Factory that wasn't successful in installing the CacheValue)

            // lazily construct a Factory
            if (factory == null) {
                factory = new Factory(key, parameter, subKey, valuesMap);
            }

            if (supplier == null) {
                supplier = valuesMap.putIfAbsent(subKey, factory);
                if (supplier == null) {
                    // successfully installed Factory
                    supplier = factory;
                }
                // else retry with winning supplier
            } else {
                if (valuesMap.replace(subKey, supplier, factory)) {
                    // successfully replaced
                    // cleared CacheEntry / unsuccessful Factory
                    // with our Factory
                    supplier = factory;
                } else {
                    // retry with current supplier
                    supplier = valuesMap.get(subKey);
                }
            }
        }
    }

進入apply方法(內部類)

java.lang.reflect.Proxy.ProxyClassFactory#apply

 //目的
//1、判斷;比如當前的class是否對加載器可見;訪問權限,是否public的
//2、生成class位元組碼檔案
//3、呼叫本地方法;通過class位元組碼檔案生成class物件

@Override
        public Class<?> apply(ClassLoader loader, Class<?>[] interfaces) {

            Map<Class<?>, Boolean> interfaceSet = new IdentityHashMap<>(interfaces.length);
            for (Class<?> intf : interfaces) {
                /*
                 * Verify that the class loader resolves the name of this
                 * interface to the same Class object.
                 */
                Class<?> interfaceClass = null;
                try {
                    //當前的class是否對加載器可見
                    interfaceClass = Class.forName(intf.getName(), false, loader);
                } catch (ClassNotFoundException e) {
                }
                if (interfaceClass != intf) {
                    throw new IllegalArgumentException(
                        intf + " is not visible from class loader");
                }
                /*
                 * 驗證類物件是否實際表示介面
                 *  
                 */
                if (!interfaceClass.isInterface()) {
                    throw new IllegalArgumentException(
                        interfaceClass.getName() + " is not an interface");
                }
                /*
                 * 驗證此介面不是重復的,
                 */
                if (interfaceSet.put(interfaceClass, Boolean.TRUE) != null) {
                    throw new IllegalArgumentException(
                        "repeated interface: " + interfaceClass.getName());
                }
            }

            String proxyPkg = null;     // package to define proxy class in
            int accessFlags = Modifier.PUBLIC | Modifier.FINAL;

            /*
             * Record the package of a non-public proxy interface so that the
             * proxy class will be defined in the same package.  Verify that
             * all non-public proxy interfaces are in the same package.
             */
            for (Class<?> intf : interfaces) {
                int flags = intf.getModifiers();
          
                if (!Modifier.isPublic(flags)) {
                    accessFlags = Modifier.FINAL;
                    String name = intf.getName();
                    int n = name.lastIndexOf('.');
                    String pkg = ((n == -1) ? "" : name.substring(0, n + 1));
                    if (proxyPkg == null) {
                        proxyPkg = pkg;
                    } else if (!pkg.equals(proxyPkg)) {
                              //是否public的
                        throw new IllegalArgumentException(
                            "non-public interfaces from different packages");
                    }
                }
            }

            if (proxyPkg == null) {
                // 如果沒有非公共代理介面,請使用com.sun.proxy
                proxyPkg = ReflectUtil.PROXY_PACKAGE + ".";
            }

            /*
             * 這里需要注意下.他對應的是代理物件proxy后的數值;比如$proxy100
             *這里的100;就是此處原子生成
             */
            long num = nextUniqueNumber.getAndIncrement();
            //$proxy100;非常熟悉了;通過斷點查看代理物件看到的,就是從這里生成的
            //proxyClassNamePrefix這個前綴就是 $
            //這個proxyPkg就是com.sun.proxy
            String proxyName = proxyPkg + proxyClassNamePrefix + num;

            /*
             *使用ProxyGenerator里面的工具類,幫助我們生成代理類的class內容
             注意,此處存盤到byte陣列;目前還不是class物件
             */
            byte[] proxyClassFile = ProxyGenerator.generateProxyClass(
                proxyName, interfaces, accessFlags);
            try {
                //此處才是真正的class物件(注意;雖然是物件;但是還沒有實體化)
                return defineClass0(loader, proxyName,
                                    proxyClassFile, 0, proxyClassFile.length);
            } catch (ClassFormatError e) {
                /*
                 * A ClassFormatError here means that (barring bugs in the
                 * proxy class generation code) there was some other
                 * invalid aspect of the arguments supplied to the proxy
                 * class creation (such as virtual machine limitations
                 * exceeded).
                 */
                throw new IllegalArgumentException(e.toString());
            }
        }
    }

核心代碼參看以上注釋

斷點查看slaver.work()

		ApplicationContext context =
				new ClassPathXmlApplicationContext("classpath*:application-aop.xml");
		Slaver slaver = (Slaver) context.getBean("slaverImpl");
		//使用代理呼叫了JdkDynamicAopProxy.invoke
		slaver.work();
		System.out.println("over>>>>>>>>>>");

注意里面的 $proxy17,全部都是在上面生成的

file

最終這個才是實體化的物件(如上)

本文由傳智教育博學谷教研團隊發布,

如果本文對您有幫助,歡迎關注點贊;如果您有任何建議也可留言評論私信,您的支持是我堅持創作的動力,

轉載請注明出處!

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/519232.html

標籤:Java

上一篇:Javamelody監控不到sql(親測有效)

下一篇:發現一款 SQL 自動檢查神器,再也不用擔心 SQL 出錯了!

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【C++】Microsoft C++、C 和匯編程式檔案

    ......

    uj5u.com 2020-09-10 00:57:23 more
  • 例外宣告

    相比于斷言適用于排除邏輯上不可能存在的狀態,例外通常是用于邏輯上可能發生的錯誤。 例外宣告 Item 1:當函式不可能拋出例外或不能接受拋出例外時,使用noexcept 理由 如果不打算拋出例外的話,程式就會認為無法處理這種錯誤,并且應當盡早終止,如此可以有效地阻止例外的傳播與擴散。 示例 //不可 ......

    uj5u.com 2020-09-10 00:57:27 more
  • Codeforces 1400E Clear the Multiset(貪心 + 分治)

    鏈接:https://codeforces.com/problemset/problem/1400/E 來源:Codeforces 思路:給你一個陣列,現在你可以進行兩種操作,操作1:將一段沒有 0 的區間進行減一的操作,操作2:將 i 位置上的元素歸零。最終問:將這個陣列的全部元素歸零后操作的最少 ......

    uj5u.com 2020-09-10 00:57:30 more
  • UVA11610 【Reverse Prime】

    本人看到此題沒有翻譯,就附帶了一個自己的翻譯版本 思考 這一題,它的第一個要求是找出所有 $7$ 位反向質數及其質因數的個數。 我們應該需要質數篩篩選1~$10^{7}$的所有數,這里就不慢慢介紹了。但是,重讀題,我們突然發現反向質數都是 $7$ 位,而將它反過來后的數字卻是 $6$ 位數,這就說明 ......

    uj5u.com 2020-09-10 00:57:36 more
  • 統計區間素數數量

    1 #pragma GCC optimize(2) 2 #include <bits/stdc++.h> 3 using namespace std; 4 bool isprime[1000000010]; 5 vector<int> prime; 6 inline int getlist(int ......

    uj5u.com 2020-09-10 00:57:47 more
  • C/C++編程筆記:C++中的 const 變數詳解,教你正確認識const用法

    1、C中的const 1、區域const變數存放在堆疊區中,會分配記憶體(也就是說可以通過地址間接修改變數的值)。測驗代碼如下: 運行結果: 2、全域const變數存放在只讀資料段(不能通過地址修改,會發生寫入錯誤), 默認為外部聯編,可以給其他源檔案使用(需要用extern關鍵字修飾) 運行結果: ......

    uj5u.com 2020-09-10 00:58:04 more
  • 【C++犯錯記錄】VS2019 MFC添加資源不懂如何修改資源宏ID

    1. 首先在資源視圖中,添加資源 2. 點擊新添加的資源,復制自動生成的ID 3. 在解決方案資源管理器中找到Resource.h檔案,編輯,使用整個專案搜索和替換的方式快速替換 宏宣告 4. Ctrl+Shift+F 全域搜索,點擊查找全部,然后逐個替換 5. 為什么使用搜索替換而不使用屬性視窗直 ......

    uj5u.com 2020-09-10 00:59:11 more
  • 【C++犯錯記錄】VS2019 MFC不懂的批量添加資源

    1. 打開資源頭檔案Resource.h,在其中預先定義好宏 ID(不清楚其實ID值應該設定多少,可以先新建一個相同的資源項,再在這個資源的ID值的基礎上遞增即可) 2. 在資源視圖中選中專案資源,按F7編輯資源檔案,按 ID 型別 相對路徑的形式添加 資源。(別忘了先把檔案拷貝到專案中的res檔案 ......

    uj5u.com 2020-09-10 01:00:19 more
  • C/C++編程筆記:關于C++的參考型別,專供新手入門使用

    今天要講的是C++中我最喜歡的一個用法——參考,也叫別名。 參考就是給一個變數名取一個變數名,方便我們間接地使用這個變數。我們可以給一個變數創建N個參考,這N + 1個變數共享了同一塊記憶體區域。(參考型別的變數會占用記憶體空間,占用的記憶體空間的大小和指標型別的大小是相同的。雖然參考是一個物件的別名,但 ......

    uj5u.com 2020-09-10 01:00:22 more
  • 【C/C++編程筆記】從頭開始學習C ++:初學者完整指南

    眾所周知,C ++的學習曲線陡峭,但是花時間學習這種語言將為您的職業帶來奇跡,并使您與其他開發人員區分開。您會更輕松地學習新語言,形成真正的解決問題的技能,并在編程的基礎上打下堅實的基礎。 C ++將幫助您養成良好的編程習慣(即清晰一致的編碼風格,在撰寫代碼時注釋代碼,并限制類內部的可見性),并且由 ......

    uj5u.com 2020-09-10 01:00:41 more
最新发布
  • Rust中的智能指標:Box<T> Rc<T> Arc<T> Cell<T> RefCell<T> Weak

    Rust中的智能指標是什么 智能指標(smart pointers)是一類資料結構,是擁有資料所有權和額外功能的指標。是指標的進一步發展 指標(pointer)是一個包含記憶體地址的變數的通用概念。這個地址參考,或 ” 指向”(points at)一些其 他資料 。參考以 & 符號為標志并借用了他們所 ......

    uj5u.com 2023-04-20 07:24:10 more
  • Java的值傳遞和參考傳遞

    值傳遞不會改變本身,參考傳遞(如果傳遞的值需要實體化到堆里)如果發生修改了會改變本身。 1.基本資料型別都是值傳遞 package com.example.basic; public class Test { public static void main(String[] args) { int ......

    uj5u.com 2023-04-20 07:24:04 more
  • [2]SpinalHDL教程——Scala簡單入門

    第一個 Scala 程式 shell里面輸入 $ scala scala> 1 + 1 res0: Int = 2 scala> println("Hello World!") Hello World! 檔案形式 object HelloWorld { /* 這是我的第一個 Scala 程式 * 以 ......

    uj5u.com 2023-04-20 07:23:58 more
  • 理解函式指標和回呼函式

    理解 函式指標 指向函式的指標。比如: 理解函式指標的偽代碼 void (*p)(int type, char *data); // 定義一個函式指標p void func(int type, char *data); // 宣告一個函式func p = func; // 將指標p指向函式func ......

    uj5u.com 2023-04-20 07:23:52 more
  • Django筆記二十五之資料庫函式之日期函式

    本文首發于公眾號:Hunter后端 原文鏈接:Django筆記二十五之資料庫函式之日期函式 日期函式主要介紹兩個大類,Extract() 和 Trunc() Extract() 函式作用是提取日期,比如我們可以提取一個日期欄位的年份,月份,日等資料 Trunc() 的作用則是截取,比如 2022-0 ......

    uj5u.com 2023-04-20 07:23:45 more
  • 一天吃透JVM面試八股文

    什么是JVM? JVM,全稱Java Virtual Machine(Java虛擬機),是通過在實際的計算機上仿真模擬各種計算機功能來實作的。由一套位元組碼指令集、一組暫存器、一個堆疊、一個垃圾回收堆和一個存盤方法域等組成。JVM屏蔽了與作業系統平臺相關的資訊,使得Java程式只需要生成在Java虛擬機 ......

    uj5u.com 2023-04-20 07:23:31 more
  • 使用Java接入小程式訂閱訊息!

    更新完微信服務號的模板訊息之后,我又趕緊把微信小程式的訂閱訊息給實作了!之前我一直以為微信小程式也是要企業才能申請,沒想到小程式個人就能申請。 訊息推送平臺🔥推送下發【郵件】【短信】【微信服務號】【微信小程式】【企業微信】【釘釘】等訊息型別。 https://gitee.com/zhongfuch ......

    uj5u.com 2023-04-20 07:22:59 more
  • java -- 緩沖流、轉換流、序列化流

    緩沖流 緩沖流, 也叫高效流, 按照資料型別分類: 位元組緩沖流:BufferedInputStream,BufferedOutputStream 字符緩沖流:BufferedReader,BufferedWriter 緩沖流的基本原理,是在創建流物件時,會創建一個內置的默認大小的緩沖區陣列,通過緩沖 ......

    uj5u.com 2023-04-20 07:22:49 more
  • Java-SpringBoot-Range請求頭設定實作視頻分段傳輸

    老實說,人太懶了,現在基本都不喜歡寫筆記了,但是網上有關Range請求頭的文章都太水了 下面是抄的一段StackOverflow的代碼...自己大修改過的,寫的注釋挺全的,應該直接看得懂,就不解釋了 寫的不好...只是希望能給視頻網站開發的新手一點點幫助吧. 業務場景:視頻分段傳輸、視頻多段傳輸(理 ......

    uj5u.com 2023-04-20 07:22:42 more
  • Windows 10開發教程_編程入門自學教程_菜鳥教程-免費教程分享

    教程簡介 Windows 10開發入門教程 - 從簡單的步驟了解Windows 10開發,從基本到高級概念,包括簡介,UWP,第一個應用程式,商店,XAML控制元件,資料系結,XAML性能,自適應設計,自適應UI,自適應代碼,檔案管理,SQLite資料庫,應用程式到應用程式通信,應用程式本地化,應用程式 ......

    uj5u.com 2023-04-20 07:22:35 more