這一次搞懂Spring代理創建及AOP鏈式調用過程

@

目錄

  • 前言
  • 正文
    • 基本概念
    • 代理對象的創建
    • 小結
    • AOP鏈式調用
    • AOP擴展知識
      • 一、自定義全局攔截器Interceptor
      • 二、循環依賴三級緩存存在的必要性
      • 三、如何在Bean創建之前提前創建代理對象
  • 總結

前言

AOP,也就是面向切面編程,它可以將公共的代碼抽離出來,動態的織入到目標類、目標方法中,大大提高我們編程的效率,也使程序變得更加優雅。如事務、操作日誌等都可以使用AOP實現。這種織入可以是在運行期動態生成代理對象實現,也可以在編譯期類加載時期靜態織入到代碼中。而Spring正是通過第一種方法實現,且在代理類的生成上也有兩種方式:JDK Proxy和CGLIB,默認當類實現了接口時使用前者,否則使用後者;另外Spring AOP只能實現對方法的增強。

正文

基本概念

AOP的術語很多,雖然不清楚術語我們也能很熟練地使用AOP,但是要理解分析源碼,術語就需要深刻體會其含義。

  • 增強(Advice):就是我們想要額外增加的功能
  • 目標對象(Target):就是我們想要增強的目標類,如果沒有AOP,我們需要在每個目標對象中實現日誌、事務管理等非業務邏輯
  • 連接點(JoinPoint):程序執行時的特定時機,如方法執行前、后以及拋出異常后等等。
  • 切點(Pointcut):連接點的導航,我們如何找到目標對象呢?切點的作用就在於此,在Spring中就是匹配表達式。
  • 引介(Introduction):引介是一種特殊的增強,它為類添加一些屬性和方法。這樣,即使一個業務類原本沒有實現某個接口,通過AOP的引介功能,我們可以動態地為該業務類添加接口的實現邏輯,讓業務類成為這個接口的實現類。
  • 織入(Weaving):即如何將增強添加到目標對象的連接點上,有動態(運行期生成代理)、靜態(編譯期、類加載時期)兩種方式。
  • 代理(Proxy):目標對象被織入增強后,就會產生一個代理對象,該對象可能是和原對象實現了同樣的一個接口(JDK),也可能是原對象的子類(CGLIB)。
  • 切面(Aspect、Advisor):切面由切點和增強組成,包含了這兩者的定義。

代理對象的創建

在熟悉了AOP術語后,下面就來看看Spring是如何創建代理對象的,是否還記得上一篇提到的AOP的入口呢?在AbstractAutowireCapableBeanFactory類的applyBeanPostProcessorsAfterInitialization方法中循環調用了BeanPostProcessorpostProcessAfterInitialization方法,其中一個就是我們創建代理對象的入口。這裡是Bean實例化完成去創建代理對象,理所當然應該這樣,但實際上在Bean實例化之前調用了一個resolveBeforeInstantiation方法,這裏實際上我們也是有機會可以提前創建代理對象的,這裏放到最後來分析,先來看主入口,進入到AbstractAutoProxyCreator類中:

	public Object postProcessAfterInitialization(@Nullable Object bean, String beanName) {
		if (bean != null) {
			Object cacheKey = getCacheKey(bean.getClass(), beanName);
			if (!this.earlyProxyReferences.contains(cacheKey)) {
				return wrapIfNecessary(bean, beanName, cacheKey);
			}
		}
		return bean;
	}

	protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) {
		//創建當前bean的代理,如果這個bean有advice的話,重點看
		// Create proxy if we have advice.
		Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, null);
		//如果有切面,則生成該bean的代理
		if (specificInterceptors != DO_NOT_PROXY) {
			this.advisedBeans.put(cacheKey, Boolean.TRUE);
			//把被代理對象bean實例封裝到SingletonTargetSource對象中
			Object proxy = createProxy(
					bean.getClass(), beanName, specificInterceptors, new SingletonTargetSource(bean));
			this.proxyTypes.put(cacheKey, proxy.getClass());
			return proxy;
		}

		this.advisedBeans.put(cacheKey, Boolean.FALSE);
		return bean;
	}

先從緩存中拿,沒有則調用wrapIfNecessary方法創建。在這個方法裏面主要看兩個地方:getAdvicesAndAdvisorsForBeancreateProxy。簡單一句話概括就是先掃描后創建,問題是掃描什麼呢?你可以先結合上面的概念思考下,換你會怎麼做。進入到子類AbstractAdvisorAutoProxyCreatorgetAdvicesAndAdvisorsForBean方法中:

	protected Object[] getAdvicesAndAdvisorsForBean(
			Class<?> beanClass, String beanName, @Nullable TargetSource targetSource) {

		//找到合格的切面
		List<Advisor> advisors = findEligibleAdvisors(beanClass, beanName);
		if (advisors.isEmpty()) {
			return DO_NOT_PROXY;
		}
		return advisors.toArray();
	}

	protected List<Advisor> findEligibleAdvisors(Class<?> beanClass, String beanName) {
		//找到候選的切面,其實就是一個尋找有@Aspectj註解的過程,把工程中所有有這個註解的類封裝成Advisor返回
		List<Advisor> candidateAdvisors = findCandidateAdvisors();

		//判斷候選的切面是否作用在當前beanClass上面,就是一個匹配過程。現在就是一個匹配
		List<Advisor> eligibleAdvisors = findAdvisorsThatCanApply(candidateAdvisors, beanClass, beanName);
		extendAdvisors(eligibleAdvisors);
		if (!eligibleAdvisors.isEmpty()) {
			//對有@Order@Priority進行排序
			eligibleAdvisors = sortAdvisors(eligibleAdvisors);
		}
		return eligibleAdvisors;
	}

findEligibleAdvisors方法中可以看到有兩個步驟,第一先找到所有的切面,即掃描所有帶有@Aspect註解的類,並將其中的切點(表達式)增強封裝為切面,掃描完成后,自然是要判斷哪些切面能夠連接到當前Bean實例上。下面一步步來分析,首先是掃描過程,進入到AnnotationAwareAspectJAutoProxyCreator類中:

	protected List<Advisor> findCandidateAdvisors() {
		// 先通過父類AbstractAdvisorAutoProxyCreator掃描,這裏不重要
		List<Advisor> advisors = super.findCandidateAdvisors();
		// 主要看這裏
		if (this.aspectJAdvisorsBuilder != null) {
			advisors.addAll(this.aspectJAdvisorsBuilder.buildAspectJAdvisors());
		}
		return advisors;
	}

這裏委託給了BeanFactoryAspectJAdvisorsBuilderAdapter類,並調用其父類的buildAspectJAdvisors方法創建切面對象:

	public List<Advisor> buildAspectJAdvisors() {
		List<String> aspectNames = this.aspectBeanNames;

		if (aspectNames == null) {
			synchronized (this) {
				aspectNames = this.aspectBeanNames;
				if (aspectNames == null) {
					List<Advisor> advisors = new ArrayList<>();
					aspectNames = new ArrayList<>();
					//獲取spring容器中的所有bean的名稱BeanName
					String[] beanNames = BeanFactoryUtils.beanNamesForTypeIncludingAncestors(
							this.beanFactory, Object.class, true, false);
					for (String beanName : beanNames) {
						if (!isEligibleBean(beanName)) {
							continue;
						}
						Class<?> beanType = this.beanFactory.getType(beanName);
						if (beanType == null) {
							continue;
						}
						//判斷類上是否有@Aspect註解
						if (this.advisorFactory.isAspect(beanType)) {
							aspectNames.add(beanName);
							AspectMetadata amd = new AspectMetadata(beanType, beanName);
							if (amd.getAjType().getPerClause().getKind() == PerClauseKind.SINGLETON) {
								// 當@Aspect的value屬性為""時才會進入到這裏
								// 創建獲取有@Aspect註解類的實例工廠,負責獲取有@Aspect註解類的實例
								MetadataAwareAspectInstanceFactory factory =
										new BeanFactoryAspectInstanceFactory(this.beanFactory, beanName);

								//創建切面advisor對象
								List<Advisor> classAdvisors = this.advisorFactory.getAdvisors(factory);
								if (this.beanFactory.isSingleton(beanName)) {
									this.advisorsCache.put(beanName, classAdvisors);
								}
								else {
									this.aspectFactoryCache.put(beanName, factory);
								}
								advisors.addAll(classAdvisors);
							}
							else {
								MetadataAwareAspectInstanceFactory factory =
										new PrototypeAspectInstanceFactory(this.beanFactory, beanName);
								this.aspectFactoryCache.put(beanName, factory);
								advisors.addAll(this.advisorFactory.getAdvisors(factory));
							}
						}
					}
					this.aspectBeanNames = aspectNames;
					return advisors;
				}
			}
		}
		return advisors;
	}

這個方法裏面首先從IOC中拿到所有Bean的名稱,並循環判斷該類上是否帶有@Aspect註解,如果有則將BeanName和Bean的Class類型封裝到BeanFactoryAspectInstanceFactory中,並調用ReflectiveAspectJAdvisorFactory.getAdvisors創建切面對象:

	public List<Advisor> getAdvisors(MetadataAwareAspectInstanceFactory aspectInstanceFactory) {
		//從工廠中獲取有@Aspect註解的類Class
		Class<?> aspectClass = aspectInstanceFactory.getAspectMetadata().getAspectClass();
		//從工廠中獲取有@Aspect註解的類的名稱
		String aspectName = aspectInstanceFactory.getAspectMetadata().getAspectName();
		validate(aspectClass);

		// 創建工廠的裝飾類,獲取實例只會獲取一次
		MetadataAwareAspectInstanceFactory lazySingletonAspectInstanceFactory =
				new LazySingletonAspectInstanceFactoryDecorator(aspectInstanceFactory);

		List<Advisor> advisors = new ArrayList<>();

		//這裏循環沒有@Pointcut註解的方法
		for (Method method : getAdvisorMethods(aspectClass)) {

			//非常重要重點看看
			Advisor advisor = getAdvisor(method, lazySingletonAspectInstanceFactory, advisors.size(), aspectName);
			if (advisor != null) {
				advisors.add(advisor);
			}
		}

		if (!advisors.isEmpty() && lazySingletonAspectInstanceFactory.getAspectMetadata().isLazilyInstantiated()) {
			Advisor instantiationAdvisor = new SyntheticInstantiationAdvisor(lazySingletonAspectInstanceFactory);
			advisors.add(0, instantiationAdvisor);
		}

		//判斷屬性上是否有引介註解,這裏可以不看
		for (Field field : aspectClass.getDeclaredFields()) {
			//判斷屬性上是否有DeclareParents註解,如果有返回切面
			Advisor advisor = getDeclareParentsAdvisor(field);
			if (advisor != null) {
				advisors.add(advisor);
			}
		}

		return advisors;
	}

	private List<Method> getAdvisorMethods(Class<?> aspectClass) {
		final List<Method> methods = new ArrayList<>();
		ReflectionUtils.doWithMethods(aspectClass, method -> {
			// Exclude pointcuts
			if (AnnotationUtils.getAnnotation(method, Pointcut.class) == null) {
				methods.add(method);
			}
		});
		methods.sort(METHOD_COMPARATOR);
		return methods;
	}

根據Aspect的Class拿到所有不帶@Pointcut註解的方法對象(為什麼是不帶@Pointcut註解的方法?仔細想想不難理解),另外要注意這裏對method進行了排序,看看這個METHOD_COMPARATOR比較器:

	private static final Comparator<Method> METHOD_COMPARATOR;

	static {
		Comparator<Method> adviceKindComparator = new ConvertingComparator<>(
				new InstanceComparator<>(
						Around.class, Before.class, After.class, AfterReturning.class, AfterThrowing.class),
				(Converter<Method, Annotation>) method -> {
					AspectJAnnotation<?> annotation =
						AbstractAspectJAdvisorFactory.findAspectJAnnotationOnMethod(method);
					return (annotation != null ? annotation.getAnnotation() : null);
				});
		Comparator<Method> methodNameComparator = new ConvertingComparator<>(Method::getName);
		METHOD_COMPARATOR = adviceKindComparator.thenComparing(methodNameComparator);
	}

關注InstanceComparator構造函數參數,記住它們的順序,這就是AOP鏈式調用中同一個@Aspect類中Advice的執行順序。接着往下看,在getAdvisors方法中循環獲取到的methods,分別調用getAdvisor方法,也就是根據方法逐個去創建切面:

	public Advisor getAdvisor(Method candidateAdviceMethod, MetadataAwareAspectInstanceFactory aspectInstanceFactory,
			int declarationOrderInAspect, String aspectName) {

		validate(aspectInstanceFactory.getAspectMetadata().getAspectClass());

		//獲取pointCut對象,最重要的是從註解中獲取表達式
		AspectJExpressionPointcut expressionPointcut = getPointcut(
				candidateAdviceMethod, aspectInstanceFactory.getAspectMetadata().getAspectClass());
		if (expressionPointcut == null) {
			return null;
		}

		//創建Advisor切面類,這才是真正的切面類,一個切面類裏面肯定要有1、pointCut 2、advice
		//這裏pointCut是expressionPointcut, advice 增強方法是 candidateAdviceMethod
		return new InstantiationModelAwarePointcutAdvisorImpl(expressionPointcut, candidateAdviceMethod,
				this, aspectInstanceFactory, declarationOrderInAspect, aspectName);
	}

	private static final Class<?>[] ASPECTJ_ANNOTATION_CLASSES = new Class<?>[] {
			Pointcut.class, Around.class, Before.class, After.class, AfterReturning.class, AfterThrowing.class};
			
	private AspectJExpressionPointcut getPointcut(Method candidateAdviceMethod, Class<?> candidateAspectClass) {
		//從候選的增強方法裏面 candidateAdviceMethod  找有有註解
		//Pointcut.class, Around.class, Before.class, After.class, AfterReturning.class, AfterThrowing.class
		//並把註解信息封裝成AspectJAnnotation對象
		AspectJAnnotation<?> aspectJAnnotation =
				AbstractAspectJAdvisorFactory.findAspectJAnnotationOnMethod(candidateAdviceMethod);
		if (aspectJAnnotation == null) {
			return null;
		}

		//創建一個PointCut類,並且把前面從註解裏面解析的表達式設置進去
		AspectJExpressionPointcut ajexp =
				new AspectJExpressionPointcut(candidateAspectClass, new String[0], new Class<?>[0]);
		ajexp.setExpression(aspectJAnnotation.getPointcutExpression());
		if (this.beanFactory != null) {
			ajexp.setBeanFactory(this.beanFactory);
		}
		return ajexp;
	}

之前就說過切面的定義,是切點和增強的組合,所以這裏首先通過getPointcut獲取到註解對象,然後new了一個Pointcut對象,並將表達式設置進去。然後在getAdvisor方法中最後new了一個InstantiationModelAwarePointcutAdvisorImpl對象:

	public InstantiationModelAwarePointcutAdvisorImpl(AspectJExpressionPointcut declaredPointcut,
			Method aspectJAdviceMethod, AspectJAdvisorFactory aspectJAdvisorFactory,
			MetadataAwareAspectInstanceFactory aspectInstanceFactory, int declarationOrder, String aspectName) {

		this.declaredPointcut = declaredPointcut;
		this.declaringClass = aspectJAdviceMethod.getDeclaringClass();
		this.methodName = aspectJAdviceMethod.getName();
		this.parameterTypes = aspectJAdviceMethod.getParameterTypes();
		this.aspectJAdviceMethod = aspectJAdviceMethod;
		this.aspectJAdvisorFactory = aspectJAdvisorFactory;
		this.aspectInstanceFactory = aspectInstanceFactory;
		this.declarationOrder = declarationOrder;
		this.aspectName = aspectName;

		if (aspectInstanceFactory.getAspectMetadata().isLazilyInstantiated()) {
			// Static part of the pointcut is a lazy type.
			Pointcut preInstantiationPointcut = Pointcuts.union(
					aspectInstanceFactory.getAspectMetadata().getPerClausePointcut(), this.declaredPointcut);

			// Make it dynamic: must mutate from pre-instantiation to post-instantiation state.
			// If it's not a dynamic pointcut, it may be optimized out
			// by the Spring AOP infrastructure after the first evaluation.
			this.pointcut = new PerTargetInstantiationModelPointcut(
					this.declaredPointcut, preInstantiationPointcut, aspectInstanceFactory);
			this.lazy = true;
		}
		else {
			// A singleton aspect.
			this.pointcut = this.declaredPointcut;
			this.lazy = false;
			//這個方法重點看看,創建advice對象
			this.instantiatedAdvice = instantiateAdvice(this.declaredPointcut);
		}
	}

這個就是我們的切面類,在其構造方法的最後通過instantiateAdvice創建了Advice對象。注意這裏傳進來的declarationOrder參數,它就是循環method時的序號,其作用就是賦值給這裏的declarationOrder屬性以及Advice的declarationOrder屬性,在後面排序時就會通過這個序號來比較,因此Advice的執行順序是固定的,至於為什麼要固定,後面分析完AOP鏈式調用過程自然就明白了。

	public Advice getAdvice(Method candidateAdviceMethod, AspectJExpressionPointcut expressionPointcut,
			MetadataAwareAspectInstanceFactory aspectInstanceFactory, int declarationOrder, String aspectName) {

		//獲取有@Aspect註解的類
		Class<?> candidateAspectClass = aspectInstanceFactory.getAspectMetadata().getAspectClass();
		validate(candidateAspectClass);

		//找到candidateAdviceMethod方法上面的註解,並且包裝成AspectJAnnotation對象,這個對象中就有註解類型
		AspectJAnnotation<?> aspectJAnnotation =
				AbstractAspectJAdvisorFactory.findAspectJAnnotationOnMethod(candidateAdviceMethod);
		if (aspectJAnnotation == null) {
			return null;
		}
		
		AbstractAspectJAdvice springAdvice;

		//根據不同的註解類型創建不同的advice類實例
		switch (aspectJAnnotation.getAnnotationType()) {
			case AtPointcut:
				if (logger.isDebugEnabled()) {
					logger.debug("Processing pointcut '" + candidateAdviceMethod.getName() + "'");
				}
				return null;
			case AtAround:
				//實現了MethodInterceptor接口
				springAdvice = new AspectJAroundAdvice(
						candidateAdviceMethod, expressionPointcut, aspectInstanceFactory);
				break;
			case AtBefore:
				//實現了MethodBeforeAdvice接口,沒有實現MethodInterceptor接口
				springAdvice = new AspectJMethodBeforeAdvice(
						candidateAdviceMethod, expressionPointcut, aspectInstanceFactory);
				break;
			case AtAfter:
				//實現了MethodInterceptor接口
				springAdvice = new AspectJAfterAdvice(
						candidateAdviceMethod, expressionPointcut, aspectInstanceFactory);
				break;
			case AtAfterReturning:
				//實現了AfterReturningAdvice接口,沒有實現MethodInterceptor接口
				springAdvice = new AspectJAfterReturningAdvice(
						candidateAdviceMethod, expressionPointcut, aspectInstanceFactory);
				AfterReturning afterReturningAnnotation = (AfterReturning) aspectJAnnotation.getAnnotation();
				if (StringUtils.hasText(afterReturningAnnotation.returning())) {
					springAdvice.setReturningName(afterReturningAnnotation.returning());
				}
				break;
			case AtAfterThrowing:
				//實現了MethodInterceptor接口
				springAdvice = new AspectJAfterThrowingAdvice(
						candidateAdviceMethod, expressionPointcut, aspectInstanceFactory);
				AfterThrowing afterThrowingAnnotation = (AfterThrowing) aspectJAnnotation.getAnnotation();
				if (StringUtils.hasText(afterThrowingAnnotation.throwing())) {
					springAdvice.setThrowingName(afterThrowingAnnotation.throwing());
				}
				break;
			default:
				throw new UnsupportedOperationException(
						"Unsupported advice type on method: " + candidateAdviceMethod);
		}

		// Now to configure the advice...
		springAdvice.setAspectName(aspectName);
		springAdvice.setDeclarationOrder(declarationOrder);
		String[] argNames = this.parameterNameDiscoverer.getParameterNames(candidateAdviceMethod);
		if (argNames != null) {
			springAdvice.setArgumentNamesFromStringArray(argNames);
		}

		//計算argNames和類型的對應關係
		springAdvice.calculateArgumentBindings();

		return springAdvice;
	}

這裏邏輯很清晰,就是拿到方法上的註解類型,根據類型創建不同的增強Advice對象:AspectJAroundAdvice、AspectJMethodBeforeAdvice、AspectJAfterAdvice、AspectJAfterReturningAdvice、AspectJAfterThrowingAdvice。完成之後通過calculateArgumentBindings方法進行參數綁定,感興趣的可自行研究。這裏主要看看幾個Advice的繼承體系:

可以看到有兩個Advice是沒有實現MethodInterceptor接口的:AspectJMethodBeforeAdvice和AspectJAfterReturningAdvice。而MethodInterceptor有一個invoke方法,這個方法就是鏈式調用的核心方法,但那兩個沒有實現該方法的Advice怎麼處理呢?稍後會分析。
到這裏切面對象就創建完成了,接下來就是判斷當前創建的Bean實例是否和這些切面匹配以及對切面排序。匹配過程比較複雜,對理解主流程也沒什麼幫助,所以這裏就不展開分析,感興趣的自行分析(AbstractAdvisorAutoProxyCreator.findAdvisorsThatCanApply())。下面看看排序的過程,回到AbstractAdvisorAutoProxyCreator.findEligibleAdvisors方法:

	protected List<Advisor> findEligibleAdvisors(Class<?> beanClass, String beanName) {
		//找到候選的切面,其實就是一個尋找有@Aspectj註解的過程,把工程中所有有這個註解的類封裝成Advisor返回
		List<Advisor> candidateAdvisors = findCandidateAdvisors();

		//判斷候選的切面是否作用在當前beanClass上面,就是一個匹配過程。。現在就是一個匹配
		List<Advisor> eligibleAdvisors = findAdvisorsThatCanApply(candidateAdvisors, beanClass, beanName);
		extendAdvisors(eligibleAdvisors);
		if (!eligibleAdvisors.isEmpty()) {
			//對有@Order@Priority進行排序
			eligibleAdvisors = sortAdvisors(eligibleAdvisors);
		}
		return eligibleAdvisors;
	}

sortAdvisors方法就是排序,但這個方法有兩個實現:當前類AbstractAdvisorAutoProxyCreator和子類AspectJAwareAdvisorAutoProxyCreator,應該走哪個呢?

通過類圖我們可以肯定是進入的AspectJAwareAdvisorAutoProxyCreator類,因為AnnotationAwareAspectJAutoProxyCreator的父類是它。

	protected List<Advisor> sortAdvisors(List<Advisor> advisors) {
		List<PartiallyComparableAdvisorHolder> partiallyComparableAdvisors = new ArrayList<>(advisors.size());
		for (Advisor element : advisors) {
			partiallyComparableAdvisors.add(
					new PartiallyComparableAdvisorHolder(element, DEFAULT_PRECEDENCE_COMPARATOR));
		}
		List<PartiallyComparableAdvisorHolder> sorted = PartialOrder.sort(partiallyComparableAdvisors);
		if (sorted != null) {
			List<Advisor> result = new ArrayList<>(advisors.size());
			for (PartiallyComparableAdvisorHolder pcAdvisor : sorted) {
				result.add(pcAdvisor.getAdvisor());
			}
			return result;
		}
		else {
			return super.sortAdvisors(advisors);
		}
	}

這裏排序主要是委託給PartialOrder進行的,而在此之前將所有的切面都封裝成了PartiallyComparableAdvisorHolder對象,注意傳入的DEFAULT_PRECEDENCE_COMPARATOR參數,這個就是比較器對象:

	private static final Comparator<Advisor> DEFAULT_PRECEDENCE_COMPARATOR = new AspectJPrecedenceComparator();

所以我們直接看這個比較器的compare方法:

	public int compare(Advisor o1, Advisor o2) {
		int advisorPrecedence = this.advisorComparator.compare(o1, o2);
		if (advisorPrecedence == SAME_PRECEDENCE && declaredInSameAspect(o1, o2)) {
			advisorPrecedence = comparePrecedenceWithinAspect(o1, o2);
		}
		return advisorPrecedence;
	}

	private final Comparator<? super Advisor> advisorComparator;
	public AspectJPrecedenceComparator() {
		this.advisorComparator = AnnotationAwareOrderComparator.INSTANCE;
	}

第一步先通過AnnotationAwareOrderComparator去比較,點進去看可以發現是對實現了PriorityOrderedOrdered接口以及標記了PriorityOrder註解的非同一個@Aspect類中的切面進行排序。這個和之前分析BeanFacotryPostProcessor類是一樣的原理。而對同一個@Aspect類中的切面排序主要是comparePrecedenceWithinAspect方法:

	private int comparePrecedenceWithinAspect(Advisor advisor1, Advisor advisor2) {
		boolean oneOrOtherIsAfterAdvice =
				(AspectJAopUtils.isAfterAdvice(advisor1) || AspectJAopUtils.isAfterAdvice(advisor2));
		int adviceDeclarationOrderDelta = getAspectDeclarationOrder(advisor1) - getAspectDeclarationOrder(advisor2);

		if (oneOrOtherIsAfterAdvice) {
			// the advice declared last has higher precedence
			if (adviceDeclarationOrderDelta < 0) {
				// advice1 was declared before advice2
				// so advice1 has lower precedence
				return LOWER_PRECEDENCE;
			}
			else if (adviceDeclarationOrderDelta == 0) {
				return SAME_PRECEDENCE;
			}
			else {
				return HIGHER_PRECEDENCE;
			}
		}
		else {
			// the advice declared first has higher precedence
			if (adviceDeclarationOrderDelta < 0) {
				// advice1 was declared before advice2
				// so advice1 has higher precedence
				return HIGHER_PRECEDENCE;
			}
			else if (adviceDeclarationOrderDelta == 0) {
				return SAME_PRECEDENCE;
			}
			else {
				return LOWER_PRECEDENCE;
			}
		}
	}

	private int getAspectDeclarationOrder(Advisor anAdvisor) {
		AspectJPrecedenceInformation precedenceInfo =
			AspectJAopUtils.getAspectJPrecedenceInformationFor(anAdvisor);
		if (precedenceInfo != null) {
			return precedenceInfo.getDeclarationOrder();
		}
		else {
			return 0;
		}
	}

這裏就是通過precedenceInfo.getDeclarationOrder拿到在創建InstantiationModelAwarePointcutAdvisorImpl對象時設置的declarationOrder屬性,這就驗證了之前的說法(實際上這裏排序過程非常複雜,不是簡單的按照這個屬性進行排序)。
當上面的一切都進行完成后,就該創建代理對象了,回到AbstractAutoProxyCreator.wrapIfNecessary,看關鍵部分代碼:

	//如果有切面,則生成該bean的代理
	if (specificInterceptors != DO_NOT_PROXY) {
		this.advisedBeans.put(cacheKey, Boolean.TRUE);
		//把被代理對象bean實例封裝到SingletonTargetSource對象中
		Object proxy = createProxy(
				bean.getClass(), beanName, specificInterceptors, new SingletonTargetSource(bean));
		this.proxyTypes.put(cacheKey, proxy.getClass());
		return proxy;
	}

注意這裏將被代理對象封裝成了一個SingletonTargetSource對象,它是TargetSource的實現類。

	protected Object createProxy(Class<?> beanClass, @Nullable String beanName,
			@Nullable Object[] specificInterceptors, TargetSource targetSource) {

		if (this.beanFactory instanceof ConfigurableListableBeanFactory) {
			AutoProxyUtils.exposeTargetClass((ConfigurableListableBeanFactory) this.beanFactory, beanName, beanClass);
		}

		//創建代理工廠
		ProxyFactory proxyFactory = new ProxyFactory();
		proxyFactory.copyFrom(this);

		if (!proxyFactory.isProxyTargetClass()) {
			if (shouldProxyTargetClass(beanClass, beanName)) {
				//proxyTargetClass 是否對類進行代理,而不是對接口進行代理,設置為true時,使用CGLib代理。
				proxyFactory.setProxyTargetClass(true);
			}
			else {
				evaluateProxyInterfaces(beanClass, proxyFactory);
			}
		}

		//把advice類型的增強包裝成advisor切面
		Advisor[] advisors = buildAdvisors(beanName, specificInterceptors);
		proxyFactory.addAdvisors(advisors);
		proxyFactory.setTargetSource(targetSource);
		customizeProxyFactory(proxyFactory);

		////用來控制代理工廠被配置后,是否還允許修改代理的配置,默認為false
		proxyFactory.setFrozen(this.freezeProxy);
		if (advisorsPreFiltered()) {
			proxyFactory.setPreFiltered(true);
		}

		//獲取代理實例
		return proxyFactory.getProxy(getProxyClassLoader());
	}

這裏通過ProxyFactory對象去創建代理實例,這是工廠模式的體現,但在創建代理對象之前還有幾個準備動作:需要判斷是JDK代理還是CGLIB代理以及通過buildAdvisors方法將擴展的Advice封裝成Advisor切面。準備完成則通過getProxy創建代理對象:

	public Object getProxy(@Nullable ClassLoader classLoader) {
		//根據目標對象是否有接口來判斷採用什麼代理方式,cglib代理還是jdk動態代理
		return createAopProxy().getProxy(classLoader);
	}

	protected final synchronized AopProxy createAopProxy() {
		if (!this.active) {
			activate();
		}
		return getAopProxyFactory().createAopProxy(this);
	}

	public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException {
		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.");
			}
			if (targetClass.isInterface() || Proxy.isProxyClass(targetClass)) {
				return new JdkDynamicAopProxy(config);
			}
			return new ObjenesisCglibAopProxy(config);
		}
		else {
			return new JdkDynamicAopProxy(config);
		}
	}

首先通過配置拿到對應的代理類:ObjenesisCglibAopProxy和JdkDynamicAopProxy,然後再通過getProxy創建Bean的代理,這裏以JdkDynamicAopProxy為例:

	public Object getProxy(@Nullable ClassLoader classLoader) {
		//advised是代理工廠對象
		Class<?>[] proxiedInterfaces = AopProxyUtils.completeProxiedInterfaces(this.advised, true);
		findDefinedEqualsAndHashCodeMethods(proxiedInterfaces);
		return Proxy.newProxyInstance(classLoader, proxiedInterfaces, this);
	}

這裏的代碼你應該不陌生了,就是JDK的原生API,newProxyInstance方法傳入的InvocationHandler對象是this,因此,最終AOP代理的調用就是從該類中的invoke方法開始。至此,代理對象的創建就完成了,下面來看下整個過程的時序圖:

小結

代理對象的創建過程整體來說並不複雜,首先找到所有帶有@Aspect註解的類,並獲取其中沒有@Pointcut註解的方法,循環創建切面,而創建切面需要切點增強兩個元素,其中切點可簡單理解為我們寫的表達式,增強則是根據@Before、@Around、@After等註解創建的對應的Advice類。切面創建好后則需要循環判斷哪些切面能對當前的Bean實例的方法進行增強並排序,最後通過ProxyFactory創建代理對象。

AOP鏈式調用

熟悉JDK動態代理的都知道通過代理對象調用方法時,會進入到InvocationHandler對象的invoke方法,所以我們直接從JdkDynamicAopProxy的這個方法開始:

	public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
		MethodInvocation invocation;
		Object oldProxy = null;
		boolean setProxyContext = false;

		//從代理工廠中拿到TargetSource對象,該對象包裝了被代理實例bean
		TargetSource targetSource = this.advised.targetSource;
		Object target = null;

		try {
			//被代理對象的equals方法和hashCode方法是不能被代理的,不會走切面
			.......
			
			Object retVal;

			// 可以從當前線程中拿到代理對象
			if (this.advised.exposeProxy) {
				// Make invocation available if necessary.
				oldProxy = AopContext.setCurrentProxy(proxy);
				setProxyContext = true;
			}

			//這個target就是被代理實例
			target = targetSource.getTarget();
			Class<?> targetClass = (target != null ? target.getClass() : null);
			
			//從代理工廠中拿過濾器鏈 Object是一個MethodInterceptor類型的對象,其實就是一個advice對象
			List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass);

			//如果該方法沒有執行鏈,則說明這個方法不需要被攔截,則直接反射調用
			if (chain.isEmpty()) {
				Object[] argsToUse = AopProxyUtils.adaptArgumentsIfNecessary(method, args);
				retVal = AopUtils.invokeJoinpointUsingReflection(target, method, argsToUse);
			}
			else {
				invocation = new ReflectiveMethodInvocation(proxy, target, method, args, targetClass, chain);
				retVal = invocation.proceed();
			}

			// Massage return value if necessary.
			Class<?> returnType = method.getReturnType();
			if (retVal != null && retVal == target &&
					returnType != Object.class && returnType.isInstance(proxy) &&
					!RawTargetAccess.class.isAssignableFrom(method.getDeclaringClass())) {
				retVal = proxy;
			}
			return retVal;
		}
		finally {
			if (target != null && !targetSource.isStatic()) {
				// Must have come from TargetSource.
				targetSource.releaseTarget(target);
			}
			if (setProxyContext) {
				// Restore old proxy.
				AopContext.setCurrentProxy(oldProxy);
			}
		}
	}

這段代碼比較長,我刪掉了不關鍵的地方。首先來看this.advised.exposeProxy這個屬性,這在@EnableAspectJAutoProxy註解中可以配置,當為true時,會將該代理對象設置到當前線程的ThreadLocal對象中,這樣就可以通過AopContext.currentProxy拿到代理對象。這個有什麼用呢?我相信有經驗的Java開發都遇到過這樣一個BUG,在Service實現類中調用本類中的另一個方法時,事務不會生效,這是因為直接通過this調用就不會調用到代理對象的方法,而是原對象的,所以事務切面就沒有生效。因此這種情況下就可以從當前線程的ThreadLocal對象拿到代理對象,不過實際上直接使用@Autowired注入自己本身也可以拿到代理對象。
接下來就是通過getInterceptorsAndDynamicInterceptionAdvice拿到執行鏈,看看具體做了哪些事情:

	public List<Object> getInterceptorsAndDynamicInterceptionAdvice(
			Advised config, Method method, @Nullable Class<?> targetClass) {

		AdvisorAdapterRegistry registry = GlobalAdvisorAdapterRegistry.getInstance();
		//從代理工廠中獲得該被代理類的所有切面advisor,config就是代理工廠對象
		Advisor[] advisors = config.getAdvisors();
		List<Object> interceptorList = new ArrayList<>(advisors.length);
		Class<?> actualClass = (targetClass != null ? targetClass : method.getDeclaringClass());
		Boolean hasIntroductions = null;

		for (Advisor advisor : advisors) {
			//大部分走這裏
			if (advisor instanceof PointcutAdvisor) {
				// Add it conditionally.
				PointcutAdvisor pointcutAdvisor = (PointcutAdvisor) advisor;
				//如果切面的pointCut和被代理對象是匹配的,說明是切面要攔截的對象
				if (config.isPreFiltered() || pointcutAdvisor.getPointcut().getClassFilter().matches(actualClass)) {
					MethodMatcher mm = pointcutAdvisor.getPointcut().getMethodMatcher();
					boolean match;
					if (mm instanceof IntroductionAwareMethodMatcher) {
						if (hasIntroductions == null) {
							hasIntroductions = hasMatchingIntroductions(advisors, actualClass);
						}
						match = ((IntroductionAwareMethodMatcher) mm).matches(method, actualClass, hasIntroductions);
					}
					else {
						//接下來判斷方法是否是切面pointcut需要攔截的方法
						match = mm.matches(method, actualClass);
					}
					//如果類和方法都匹配
					if (match) {

						//獲取到切面advisor中的advice,並且包裝成MethodInterceptor類型的對象
						MethodInterceptor[] interceptors = registry.getInterceptors(advisor);
						if (mm.isRuntime()) {
							for (MethodInterceptor interceptor : interceptors) {
								interceptorList.add(new InterceptorAndDynamicMethodMatcher(interceptor, mm));
							}
						}
						else {
							interceptorList.addAll(Arrays.asList(interceptors));
						}
					}
				}
			}
			//如果是引介切面
			else if (advisor instanceof IntroductionAdvisor) {
				IntroductionAdvisor ia = (IntroductionAdvisor) advisor;
				if (config.isPreFiltered() || ia.getClassFilter().matches(actualClass)) {
					Interceptor[] interceptors = registry.getInterceptors(advisor);
					interceptorList.addAll(Arrays.asList(interceptors));
				}
			}
			else {
				Interceptor[] interceptors = registry.getInterceptors(advisor);
				interceptorList.addAll(Arrays.asList(interceptors));
			}
		}

		return interceptorList;
	}

這也是個長方法,看關鍵的部分,因為之前我們創建的基本上都是InstantiationModelAwarePointcutAdvisorImpl對象,該類是PointcutAdvisor的實現類,所以會進入第一個if判斷里,這裏首先進行匹配,看切點當前對象以及該對象的哪些方法匹配,如果能匹配上,則調用getInterceptors獲取執行鏈:

	private final List<AdvisorAdapter> adapters = new ArrayList<>(3);
	public DefaultAdvisorAdapterRegistry() {
		registerAdvisorAdapter(new MethodBeforeAdviceAdapter());
		registerAdvisorAdapter(new AfterReturningAdviceAdapter());
		registerAdvisorAdapter(new ThrowsAdviceAdapter());
	}

	public MethodInterceptor[] getInterceptors(Advisor advisor) throws UnknownAdviceTypeException {
		List<MethodInterceptor> interceptors = new ArrayList<>(3);
		Advice advice = advisor.getAdvice();
		//如果是MethodInterceptor類型的,如:AspectJAroundAdvice
		//AspectJAfterAdvice
		//AspectJAfterThrowingAdvice
		if (advice instanceof MethodInterceptor) {
			interceptors.add((MethodInterceptor) advice);
		}

		//處理 AspectJMethodBeforeAdvice  AspectJAfterReturningAdvice
		for (AdvisorAdapter adapter : this.adapters) {
			if (adapter.supportsAdvice(advice)) {
				interceptors.add(adapter.getInterceptor(advisor));
			}
		}
		if (interceptors.isEmpty()) {
			throw new UnknownAdviceTypeException(advisor.getAdvice());
		}
		return interceptors.toArray(new MethodInterceptor[0]);
	}

這裏我們可以看到如果是MethodInterceptor的實現類,則直接添加到鏈中,如果不是,則需要通過適配器去包裝后添加,剛好這裡有MethodBeforeAdviceAdapterAfterReturningAdviceAdapter兩個適配器對應上文兩個沒有實現MethodInterceptor接口的類。最後將Interceptors返回。

if (chain.isEmpty()) {
	Object[] argsToUse = AopProxyUtils.adaptArgumentsIfNecessary(method, args);
	retVal = AopUtils.invokeJoinpointUsingReflection(target, method, argsToUse);
}
else {
	// We need to create a method invocation...
	invocation = new ReflectiveMethodInvocation(proxy, target, method, args, targetClass, chain);
	// Proceed to the joinpoint through the interceptor chain.
	retVal = invocation.proceed();
}

返回到invoke方法后,如果執行鏈為空,說明該方法不需要被增強,所以直接反射調用原對象的方法(注意傳入的是TargetSource封裝的被代理對象);反之,則通過ReflectiveMethodInvocation類進行鏈式調用,關鍵方法就是proceed

	private int currentInterceptorIndex = -1;
	
	public Object proceed() throws Throwable {
		//如果執行鏈中的advice全部執行完,則直接調用joinPoint方法,就是被代理方法
		if (this.currentInterceptorIndex == this.interceptorsAndDynamicMethodMatchers.size() - 1) {
			return invokeJoinpoint();
		}

		Object interceptorOrInterceptionAdvice =
				this.interceptorsAndDynamicMethodMatchers.get(++this.currentInterceptorIndex);
		if (interceptorOrInterceptionAdvice instanceof InterceptorAndDynamicMethodMatcher) {
			InterceptorAndDynamicMethodMatcher dm =
					(InterceptorAndDynamicMethodMatcher) interceptorOrInterceptionAdvice;
			Class<?> targetClass = (this.targetClass != null ? this.targetClass : this.method.getDeclaringClass());
			if (dm.methodMatcher.matches(this.method, targetClass, this.arguments)) {
				return dm.interceptor.invoke(this);
			}
			else {
				return proceed();
			}
		}
		else {
			//調用MethodInterceptor中的invoke方法
			return ((MethodInterceptor) interceptorOrInterceptionAdvice).invoke(this);
		}
	}

這個方法的核心就在兩個地方:invokeJoinpointinterceptorOrInterceptionAdvice.invoke(this)。當增強方法調用完后就會通過前者調用到被代理的方法,否則則是依次調用Interceptorinvoke方法。下面就分別看看每個Interceptor是怎麼實現的。

  • AspectJAroundAdvice
	public Object invoke(MethodInvocation mi) throws Throwable {
		if (!(mi instanceof ProxyMethodInvocation)) {
			throw new IllegalStateException("MethodInvocation is not a Spring ProxyMethodInvocation: " + mi);
		}
		ProxyMethodInvocation pmi = (ProxyMethodInvocation) mi;
		ProceedingJoinPoint pjp = lazyGetProceedingJoinPoint(pmi);
		JoinPointMatch jpm = getJoinPointMatch(pmi);
		return invokeAdviceMethod(pjp, jpm, null, null);
	}
  • MethodBeforeAdviceInterceptor -> AspectJMethodBeforeAdvice
	public Object invoke(MethodInvocation mi) throws Throwable {
		this.advice.before(mi.getMethod(), mi.getArguments(), mi.getThis());
		return mi.proceed();
	}

	public void before(Method method, Object[] args, @Nullable Object target) throws Throwable {
		invokeAdviceMethod(getJoinPointMatch(), null, null);
	}
  • AspectJAfterAdvice
	public Object invoke(MethodInvocation mi) throws Throwable {
		try {
			return mi.proceed();
		}
		finally {
			invokeAdviceMethod(getJoinPointMatch(), null, null);
		}
	}
  • AfterReturningAdviceInterceptor -> AspectJAfterReturningAdvice
	public Object invoke(MethodInvocation mi) throws Throwable {
		Object retVal = mi.proceed();
		this.advice.afterReturning(retVal, mi.getMethod(), mi.getArguments(), mi.getThis());
		return retVal;
	}

	public void afterReturning(@Nullable Object returnValue, Method method, Object[] args, @Nullable Object target) throws Throwable {
		if (shouldInvokeOnReturnValueOf(method, returnValue)) {
			invokeAdviceMethod(getJoinPointMatch(), returnValue, null);
		}
	}
  • AspectJAfterThrowingAdvice
	public Object invoke(MethodInvocation mi) throws Throwable {
		try {
			return mi.proceed();
		}
		catch (Throwable ex) {
			if (shouldInvokeOnThrowing(ex)) {
				invokeAdviceMethod(getJoinPointMatch(), null, ex);
			}
			throw ex;
		}
	}

這裏的調用順序是怎樣的呢?其核心就是通過proceed方法控制流程,每執行完一個Advice就會回到proceed方法中調用下一個Advice。可以思考一下,怎麼才能讓調用結果滿足如下圖的執行順序

以上就是AOP的鏈式調用過程,但是這隻是只有一個切面類的情況,如果有多個@Aspect類呢,這個調用過程又是怎樣的?其核心思想和“棧”一樣,就是“先進后出,後進先出”。

AOP擴展知識

一、自定義全局攔截器Interceptor

在上文創建代理對象的時候有這樣一個方法:

	protected Advisor[] buildAdvisors(@Nullable String beanName, @Nullable Object[] specificInterceptors) {
		//自定義MethodInterceptor.拿到setInterceptorNames方法注入的Interceptor對象
		Advisor[] commonInterceptors = resolveInterceptorNames();

		List<Object> allInterceptors = new ArrayList<>();
		if (specificInterceptors != null) {
			allInterceptors.addAll(Arrays.asList(specificInterceptors));
			if (commonInterceptors.length > 0) {
				if (this.applyCommonInterceptorsFirst) {
					allInterceptors.addAll(0, Arrays.asList(commonInterceptors));
				}
				else {
					allInterceptors.addAll(Arrays.asList(commonInterceptors));
				}
			}
		}

		Advisor[] advisors = new Advisor[allInterceptors.size()];
		for (int i = 0; i < allInterceptors.size(); i++) {
			//對自定義的advice要進行包裝,把advice包裝成advisor對象,切面對象
			advisors[i] = this.advisorAdapterRegistry.wrap(allInterceptors.get(i));
		}
		return advisors;
	}

這個方法的作用就在於我們可以擴展我們自己的Interceptor,首先通過resolveInterceptorNames方法獲取到通過setInterceptorNames方法設置的Interceptor,然後調用DefaultAdvisorAdapterRegistry.wrap方法將其包裝為DefaultPointcutAdvisor對象並返回:

	public Advisor wrap(Object adviceObject) throws UnknownAdviceTypeException {
		if (adviceObject instanceof Advisor) {
			return (Advisor) adviceObject;
		}
		if (!(adviceObject instanceof Advice)) {
			throw new UnknownAdviceTypeException(adviceObject);
		}
		Advice advice = (Advice) adviceObject;
		if (advice instanceof MethodInterceptor) {
			return new DefaultPointcutAdvisor(advice);
		}
		for (AdvisorAdapter adapter : this.adapters) {
			if (adapter.supportsAdvice(advice)) {
				return new DefaultPointcutAdvisor(advice);
			}
		}
		throw new UnknownAdviceTypeException(advice);
	}

	public DefaultPointcutAdvisor(Advice advice) {
		this(Pointcut.TRUE, advice);
	}

需要注意DefaultPointcutAdvisor構造器裏面傳入了一個Pointcut.TRUE,表示這種擴展的Interceptor是全局的攔截器。下面來看看如何使用:

public class MyMethodInterceptor implements MethodInterceptor {
    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {

        System.out.println("自定義攔截器");
        return invocation.proceed();
    }
}

首先寫一個類實現MethodInterceptor 接口,在invoke方法中實現我們的攔截邏輯,然後通過下面的方式測試,只要UserService 有AOP攔截就會發現自定義的MyMethodInterceptor也生效了。

    public void costomInterceptorTest() {
        AnnotationAwareAspectJAutoProxyCreator bean = applicationContext.getBean(AnnotationAwareAspectJAutoProxyCreator.class);
        bean.setInterceptorNames("myMethodInterceptor ");

        UserService userService = applicationContext.getBean(UserService.class);
        userService.queryUser("dark");
    }

但是如果換個順序,像下面這樣:

    public void costomInterceptorTest() {

        UserService userService = applicationContext.getBean(UserService.class);

        AnnotationAwareAspectJAutoProxyCreator bean = applicationContext.getBean(AnnotationAwareAspectJAutoProxyCreator.class);
        bean.setInterceptorNames("myMethodInterceptor ");

        userService.queryUser("dark");
    }

這時自定義的全局攔截器就沒有作用了,這是為什麼呢?因為當執行getBean的時候,如果有切面匹配就會通過ProxyFactory去創建代理對象,注意Interceptor是存到這個Factory對象中的,而這個對象和代理對象是一一對應的,因此調用getBean時,還沒有myMethodInterceptor這個對象,自定義攔截器就沒有效果了,也就是說要想自定義攔截器生效,就必須在代理對象生成之前註冊進去。

二、循環依賴三級緩存存在的必要性

在上一篇文章我分析了Spring是如何通過三級緩存來解決循環依賴的問題的,但你是否考慮過第三級緩存為什麼要存在?我直接將bean存到二級不就行了么,為什麼還要存一個ObjectFactory對象到第三級緩存中?這個在學習了AOP之後就很清楚了,因為我們在@Autowired對象時,想要注入的不一定是Bean本身,而是想要注入一個修改過後的對象,如代理對象。在AbstractAutowireCapableBeanFactory.getEarlyBeanReference方法中循環調用了SmartInstantiationAwareBeanPostProcessor.getEarlyBeanReference方法,AbstractAutoProxyCreator對象就實現了該方法:

	public Object getEarlyBeanReference(Object bean, String beanName) {
		Object cacheKey = getCacheKey(bean.getClass(), beanName);
		if (!this.earlyProxyReferences.contains(cacheKey)) {
			this.earlyProxyReferences.add(cacheKey);
		}
		// 創建代理對象
		return wrapIfNecessary(bean, beanName, cacheKey);
	}

因此,當我們想要對循壞依賴的Bean做出修改時,就可以像AOP這樣做。

三、如何在Bean創建之前提前創建代理對象

Spring的代理對象基本上都是在Bean實例化完成之後創建的,但在文章開始我就說過,Spring也提供了一個機會在創建Bean對象之前就創建代理對象,在AbstractAutowireCapableBeanFactory.resolveBeforeInstantiation方法中:

	protected Object resolveBeforeInstantiation(String beanName, RootBeanDefinition mbd) {
		Object bean = null;
		if (!Boolean.FALSE.equals(mbd.beforeInstantiationResolved)) {
			// Make sure bean class is actually resolved at this point.
			if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {
				Class<?> targetType = determineTargetType(beanName, mbd);
				if (targetType != null) {
					bean = applyBeanPostProcessorsBeforeInstantiation(targetType, beanName);
					if (bean != null) {
						bean = applyBeanPostProcessorsAfterInitialization(bean, beanName);
					}
				}
			}
			mbd.beforeInstantiationResolved = (bean != null);
		}
		return bean;
	}

	protected Object applyBeanPostProcessorsBeforeInstantiation(Class<?> beanClass, String beanName) {
		for (BeanPostProcessor bp : getBeanPostProcessors()) {
			if (bp instanceof InstantiationAwareBeanPostProcessor) {
				InstantiationAwareBeanPostProcessor ibp = (InstantiationAwareBeanPostProcessor) bp;
				Object result = ibp.postProcessBeforeInstantiation(beanClass, beanName);
				if (result != null) {
					return result;
				}
			}
		}
		return null;
	}

主要是InstantiationAwareBeanPostProcessor.postProcessBeforeInstantiation方法中,這裏又會進入到AbstractAutoProxyCreator類中:

	public Object postProcessBeforeInstantiation(Class<?> beanClass, String beanName) {
		TargetSource targetSource = getCustomTargetSource(beanClass, beanName);
		if (targetSource != null) {
			if (StringUtils.hasLength(beanName)) {
				this.targetSourcedBeans.add(beanName);
			}
			Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(beanClass, beanName, targetSource);
			Object proxy = createProxy(beanClass, beanName, specificInterceptors, targetSource);
			this.proxyTypes.put(cacheKey, proxy.getClass());
			return proxy;
		}

		return null;
	}

	protected TargetSource getCustomTargetSource(Class<?> beanClass, String beanName) {
		// We can't create fancy target sources for directly registered singletons.
		if (this.customTargetSourceCreators != null &&
				this.beanFactory != null && this.beanFactory.containsBean(beanName)) {
			for (TargetSourceCreator tsc : this.customTargetSourceCreators) {
				TargetSource ts = tsc.getTargetSource(beanClass, beanName);
				if (ts != null) {
					return ts;
				}
			}
		}

		// No custom TargetSource found.
		return null;
	}

看到這裏大致應該明白了,先是獲取到一個自定義的TargetSource對象,然後創建代理對象,所以我們首先需要自己實現一個TargetSource類,這裏直接繼承一個抽象類,getTarget方法則返回原始對象:

public class MyTargetSource extends AbstractBeanFactoryBasedTargetSource {
    @Override
    public Object getTarget() throws Exception {
        return getBeanFactory().getBean(getTargetBeanName());
    }
}

但這還不夠,上面首先判斷了customTargetSourceCreators!=null,而這個屬性是個數組,可以通過下面這個方法設置進來:

	public void setCustomTargetSourceCreators(TargetSourceCreator... targetSourceCreators) {
		this.customTargetSourceCreators = targetSourceCreators;
	}

所以我們還要實現一個TargetSourceCreator類,同樣繼承一個抽象類實現,並只對userServiceImpl對象進行攔截:

public class MyTargetSourceCreator extends AbstractBeanFactoryBasedTargetSourceCreator {
    @Override
    protected AbstractBeanFactoryBasedTargetSource createBeanFactoryBasedTargetSource(Class<?> beanClass, String beanName) {

        if (getBeanFactory() instanceof ConfigurableListableBeanFactory) {
            if(beanName.equalsIgnoreCase("userServiceImpl")) {
                return new MyTargetSource();
            }
        }

        return null;
    }
}

createBeanFactoryBasedTargetSource方法是在AbstractBeanFactoryBasedTargetSourceCreator.getTargetSource中調用的,而getTargetSource就是在上面getCustomTargetSource中調用的。以上工作做完后,還需要將其設置到AnnotationAwareAspectJAutoProxyCreator對象中,因此需要我們注入這個對象:

@Configuration
public class TargetSourceCreatorBean {

    @Autowired
    private BeanFactory beanFactory;

   @Bean
    public AnnotationAwareAspectJAutoProxyCreator annotationAwareAspectJAutoProxyCreator() {
        AnnotationAwareAspectJAutoProxyCreator creator = new AnnotationAwareAspectJAutoProxyCreator();
        MyTargetSourceCreator myTargetSourceCreator = new MyTargetSourceCreator();
        myTargetSourceCreator.setBeanFactory(beanFactory);
        creator.setCustomTargetSourceCreators(myTargetSourceCreator);
        return creator;
    }
}

這樣,當我們通過getBean獲取userServiceImpl的對象時,就會優先生成代理對象,然後在調用執行鏈的過程中再通過TargetSource.getTarget獲取到被代理對象。但是,為什麼我們在getTarget方法中調用getBean就能拿到被代理對象呢?
繼續探究,通過斷點我發現從getTarget進入時,在resolveBeforeInstantiation方法中返回的bean就是null了,而getBeanPostProcessors方法返回的Processors中也沒有了AnnotationAwareAspectJAutoProxyCreator對象,也就是沒有進入到AbstractAutoProxyCreator.postProcessBeforeInstantiation方法中,所以不會再次獲取到代理對象,那AnnotationAwareAspectJAutoProxyCreator對象是在什麼時候移除的呢?
帶着問題,我開始反推,發現在AbstractBeanFactoryBasedTargetSourceCreator類中有這樣一個方法buildInternalBeanFactory

	protected DefaultListableBeanFactory buildInternalBeanFactory(ConfigurableBeanFactory containingFactory) {
		DefaultListableBeanFactory internalBeanFactory = new DefaultListableBeanFactory(containingFactory);

		// Required so that all BeanPostProcessors, Scopes, etc become available.
		internalBeanFactory.copyConfigurationFrom(containingFactory);

		// Filter out BeanPostProcessors that are part of the AOP infrastructure,
		// since those are only meant to apply to beans defined in the original factory.
		internalBeanFactory.getBeanPostProcessors().removeIf(beanPostProcessor ->
				beanPostProcessor instanceof AopInfrastructureBean);

		return internalBeanFactory;
	}

在這裏移除掉了所有AopInfrastructureBean的子類,而AnnotationAwareAspectJAutoProxyCreator就是其子類,那這個方法是在哪裡調用的呢?繼續反推:

	protected DefaultListableBeanFactory getInternalBeanFactoryForBean(String beanName) {
		synchronized (this.internalBeanFactories) {
			DefaultListableBeanFactory internalBeanFactory = this.internalBeanFactories.get(beanName);
			if (internalBeanFactory == null) {
				internalBeanFactory = buildInternalBeanFactory(this.beanFactory);
				this.internalBeanFactories.put(beanName, internalBeanFactory);
			}
			return internalBeanFactory;
		}
	}

	public final TargetSource getTargetSource(Class<?> beanClass, String beanName) {
		AbstractBeanFactoryBasedTargetSource targetSource =
				createBeanFactoryBasedTargetSource(beanClass, beanName);
		
		// 創建完targetSource后就移除掉AopInfrastructureBean類型的BeanPostProcessor對象,如AnnotationAwareAspectJAutoProxyCreator
		DefaultListableBeanFactory internalBeanFactory = getInternalBeanFactoryForBean(beanName);

		......
		return targetSource;
	}

至此,關於TargetSource接口擴展的原理就搞明白了。

總結

本篇篇幅比較長,主要搞明白Spring代理對象是如何創建的以及AOP鏈式調用過程,而後面的擴展則是對AOP以及Bean創建過程中一些疑惑的補充,可根據實際情況學習掌握。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※Google地圖已可更新顯示潭子電動車充電站設置地點!!

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※別再煩惱如何寫文案,掌握八大原則!

Java筆試面試總結—try、catch、finally語句中有return 的各類情況

前言

之前在刷筆試題和面試的時候經常會遇到或者被問到 try-catch-finally 語法塊的執行順序等問題,今天就抽空整理了一下這個知識點,然後記錄下來。

正文

本篇文章主要是通過舉例的方式來闡述各種情況,我這裏根據 try-catch-finally 語法塊分為兩種大情況討論:try-catch 語法塊和 try-catch-finally 語句塊,然後再在每種情況里再去具體討論。

一、try-catch 語句塊

我們可以看看下面程序:

public static void main(String[] args) {

    System.out.println(handleException0());
  }

  /**
   * try,catch都有return
   * @return
   */
  private static String handleException0() {
    try{
      System.out.println("try開始");
      String s = null;
      int length = s.charAt(0);
      System.out.println("try結束");
      return "try塊的返回值";
    }catch (Exception e){
      System.out.println("捕獲到了異常");
      return "catch的返回值";
    }
  }

執行結果

try開始
捕獲到了異常
catch的返回值

分析:程序首先執行 try 塊裏面的代碼,try 塊裏面發現有異常,try 塊後面的代碼不會執行(自然也不會return),然後進入匹配異常的那個 catch 塊,然後進入 catch 塊裏面將代碼執行完畢,當執行到 catch 裏面的return 語句的時候,程序中止,然後將此 return 的最終結果返回回去。

二、try-catch-finally 語句塊

這種語法塊我分為了 4 種情況討論,下面進行一一列舉。

1、第一種情況,try 塊裏面有 return 的情況,並且捕獲到異常

例1:

public static void main(String[] args) {
  String result = handleException1();
  System.out.println(result);
}
private static String handleException1() {
  try{
    System.out.println("try開始");
    String str = null;
    int length = str.length();
    System.out.println("try結束");
  }catch (Exception e){
    System.out.println("捕獲到了異常");
  }finally {
    System.out.println("finally塊執行完畢了");
  }
  return "最終的結果";
}

例1執行的結果如下

try開始
捕獲到了異常
finally塊執行完畢了
最終的結果

例2:

public static void main(String[] args) {
  String result = handleException2();
  System.out.println(result);
}
private static String handleException2() {
  try{
    System.out.println("try開始");
    String str = null;
    int length = str.length();
    System.out.println("try結束");
    return "try塊的返回值";
  }catch (Exception e){
    System.out.println("捕獲到了異常");
  }finally {
    System.out.println("finally塊執行完畢了");
  }
  return "最終的結果";
}

例2的執行結果如下

try開始
捕獲到了異常
finally塊執行完畢了
最終的結果

分析:首先 例1 和 例2 的結果是很顯然的,當遇到異常的時候,直接進入匹配到相對應的 catch 塊,然後繼續執行 finallly 語句塊,最後將 return 結果返回回去。

第二種情況:try塊裏面有return的情況,但是不會捕獲到異常

例3:

思考:下面代碼try語句塊中有return語句,那麼是否執行完try語句塊就直接return退出方法了呢?

public static void main(String[] args) {
  String result = handleException3();
  System.out.println(result);
}
private static String handleException3() {
  try{
  	System.out.println("");
    return "try塊的返回值";
  }catch (Exception e){
    System.out.println("捕獲到了異常");
  }finally {
    System.out.println("finally塊執行完畢了");
  }
  return "最終的結果";
}

例3的執行結果如下

finally塊執行完畢了
try塊的返回值

分析:例3的結果其實我們可以通過打斷點的方式去看看程序的具體執行流程,通過打斷點我們可以發現,代碼先執行 try塊 里的代碼,當執行到 return 語句的時候,handleException3方法並沒有立刻結束,而是繼續執行finally塊里的代碼,finally塊里的代碼執行完后,緊接着回到 try 塊的 return 語句,再把最終結果返回回去, handleException 方法執行完畢。

第三種情況:try塊和finally裏面都有return的情況

例4:

public static void main(String[] args) {
    System.out.println(handleException4());
  }

  /**
   * 情況3:try和finally中均有return
   * @return
   */
  private static String handleException4() {
    try{
      System.out.println("");
      return "try塊的返回值";
    }catch (Exception e){
      System.out.println("捕獲到了異常");
    }finally {
      System.out.println("finally塊執行完畢了");
      return "finally的返回值";
    }
  //  return "最終的結果";//不能再有返回值
  }

例4的執行結果

finally塊執行完畢了
finally的返回值

分析:需要注意的是,當 try 塊和 finally 裏面都有 return 的時候,在 try/catch/finally 語法塊之外不允許再有return 關鍵字。我們還是通過在程序中打斷點的方式來看看代碼的具體執行流程。代碼首先執行 try 塊 里的代碼,當執行到 return 語句的時候,handleException4 方法並沒有立刻結束,而是繼續執行 finally 塊里的代碼,當發現 finally 塊里有 return 的時候,直接將 finally 里的返回值(也就是最終結果)返回回去, handleException4 方法執行完畢。

第四種情況:try塊,catch塊,finally塊都有return

例5:

public static void main(String[] args) {
    System.out.println(handleException5());
  }

  /**
   * 情況4:try,catch,finally都有return
   * @return
   */
  private static String handleException5() {
    try{
      System.out.println("try開始");
      int[] array = {1, 2, 3};
      int i = array[10];
      System.out.println("try結束");
      return "try塊的返回值";
    }catch (Exception e){
      e.printStackTrace();//這行代碼其實就是打印輸出異常的具體信息
      System.out.println("捕獲到了異常");
      return "catch的返回值";
    }finally {
      System.out.println("finally塊執行完畢了");
      return "finally的返回值";
    }
//    return "最終的結果";
  }

例5的執行結果

try開始
捕獲到了異常
finally塊執行完畢了
finally的返回值
java.lang.ArrayIndexOutOfBoundsException: 10
at com.example.javabasic.javabasic.ExceptionAndError.TryCatchFinally.handleException5(TryCatchFinally.java:25)
at com.example.javabasic.javabasic.ExceptionAndError.TryCatchFinally.main(TryCatchFinally.java:14)

分析:程序首先執行try塊裏面的代碼,try塊裏面發現有異常,try塊後面的代碼不會執行(自然也不會return),然後進入匹配異常的那個catch塊,然後進入catch塊裏面將代碼執行完畢,當執行到catch裏面的return語句的時候,程序不會馬上終止,而是繼續執行finally塊的代碼,最後執行finally裏面的return,然後將此return的最終結果返回回去。

總結

其實,我們通過以上例子我們可以發現,不管return關鍵字在哪,finally一定會執行完畢。理論上來說try、catch、finally塊中都允許書寫return關鍵字,但是執行優先級較低的塊中的return關鍵字定義的返回值將覆蓋執行優先級較高的塊中return關鍵字定義的返回值。也就是說finally塊中定義的返回值將會覆蓋catch塊、try塊中定義的返回值;catch塊中定義的返回值將會覆蓋try塊中定義的返回值。
再換句話說如果在finally塊中通過return關鍵字定義了返回值,那麼之前所有通過return關鍵字定義的返回值都將失效——因為finally塊中的代碼一定是會執行的。

公眾號:良許Linux

有收穫?希望老鐵們來個三連擊,給更多的人看到這篇文章

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※想知道最厲害的網頁設計公司"嚨底家"!

※幫你省時又省力,新北清潔一流服務好口碑

※別再煩惱如何寫文案,掌握八大原則!

神秘疾病侵襲 加勒比海珊瑚礁群拉警報

摘錄自2019年11月12日中央通訊社綜合報導

短短一年多來,墨西哥加勒比海地區的珊瑚因為遭到一種罕為人知「石珊瑚組織損失症」(SCTLD)侵襲,已經損失30%。這種疾病會造成珊瑚鈣化和死亡。

專家警告,這種疾病可能造成大部分中美洲珊瑚礁(Mesoamerican Reef)死亡。這處龐大的弧狀珊瑚礁群範圍廣達超過1000公里,為墨西哥、貝里斯、瓜地馬拉和宏都拉斯等國家共有。

SCTLD已使加勒比海地區陷入困境,這種疾病可能摧毀環礁地區民眾賴以為生的觀光產業。中美洲珊瑚礁是僅次於澳洲大堡礁的世界第2大珊瑚礁。科學家表示,旅遊業太發達非常有可能讓問題火上加油。

「健康珊瑚礁、健康人民」在墨西哥的協調人員蘇鐸(Melina Soto)說,SCTLD只需幾週時間,就可以殺死需要花費數十年生長起來的珊瑚組織。蘇鐸表示:「如果以這種速度繼續下去,這個生態系統將會在未來的5到10年內崩潰。」

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※Google地圖已可更新顯示潭子電動車充電站設置地點!!

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※別再煩惱如何寫文案,掌握八大原則!

稀有越南老鼠鹿 近30年首度現蹤

摘錄自2019年11月12日中央通訊社綜合報導

一種外觀似鹿、大小相當於兔子或貓的小型哺乳類動物銀背鼷鹿(越南老鼠鹿),在失去蹤跡近30年後,近日首度在越南被發現。專家多年來認為此物種已瀕臨絕種。

根據今天發布在「自然生態學與進化」(Nature Ecology and Evolution)期刊的研究報告,紀錄顯示,越南老鼠鹿(Silver-backed Chevrotain,或Mouse deer)最後一次現蹤是在1990年。由於自1990年起即未有發現越南老鼠鹿的紀錄,專家推測此物種已因人類過度獵捕而瀕臨絕種。

由於人口成長和經濟開發對東南亞的樹林構成巨大生存壓力,生物學家堤爾克指出,保育工作必須搶占先機。根據聯合國「政府間生物多樣性與生態系服務科學政策平台」(IPBES)5月發布的報告,人類活動對地球的衝擊,導致高達100萬個物種瀕臨絕種。

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※想知道最厲害的網頁設計公司"嚨底家"!

※幫你省時又省力,新北清潔一流服務好口碑

※別再煩惱如何寫文案,掌握八大原則!

北京掀新能源車熱:指標申請首破三千 純電動車也需搖號

受4月2日北京宣佈純電動小客車不尾號限行()的利好政策推動,據4月9日北京小客車指標調控管理辦公室公佈的資料顯示,本期搖號個人新能源車指標的申請者首次突破三千,達3874人,而此前申請指標的人數徘徊在2000多人左右。根據今年的指標配置方案,本期分配給個人的新能車指標為3333個,這意味著從這一期開始,在北京購買純電動車也要搖號分配了。   在北京剛剛結束的新能源汽車工作會議上,北京相關新能源小客車採取備案制的方案已經通過,知情人士稱,未來進口純電動汽車也有望通過備案進入免購置稅目錄,這無疑將大大增加北京純電動汽車的供應類型。   而針對充電樁安裝困難問題,北京市科委新能源與新材料處處長許心超透露,現在北京正著手做三件事:一是和百度合作,標識所有充電樁地圖並上線;二是印製北京充電樁地圖,發放給消費者和計程車司機;三是辦充電體驗周活動,聯合所有在京車企,做充電實際體驗。   北京市環保局機動車排放管理處處長李昆生在4月7日透露,未來北京將嚴格控制機動車總量,每年新增小客車指標由24萬輛削減到15萬輛,力爭在2017年底前,全市機動車保有量控制在600萬輛以內,而新能源和清潔能源汽車的應用規模將達到20萬輛。

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※Google地圖已可更新顯示潭子電動車充電站設置地點!!

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※別再煩惱如何寫文案,掌握八大原則!

聊聊算法–堆的構建和調整

先提個問題,完全二叉樹/滿二叉樹,區別?前者是指每一層都是緊湊靠左排列,最後一層可能未排滿,後者是一種特殊的完全二叉樹,

每層都是滿的,即節點總數和深度滿足N=(2^n) -1。堆Heap,一堆蘋果,為了賣相好,越好看的越往上放,就是大頂堆;為了蘋果堆

的穩定,質量越小越往上放,就是小頂堆;堆首先是完全二叉樹,但只確保父節點和子節點大小邏輯,不關心左右子節點的大小關係,

通常是一個可以被看做一棵樹的數組對象,是個很常見的結構,比如BST對象,都與堆有關係,今天就說下這個重要的數據結構和應用。

 

作者原創文章,謝絕一切轉載,違者必究!

本文只發表在”公眾號”和”博客園”,其他均屬複製粘貼!如果覺得排版不清晰,請查看公眾號文章。 

 

準備:

Idea2019.03/Gradle6.0.1/Maven3.6.3/JDK11.0.4

難度: 新手–戰士–老兵–大師

目標:

1.堆的構建和調整算法

1 優先級隊列

為理解堆的原理,先看優先級隊列,它是一種數據結構,插入或者刪除元素的時候,元素會自動排序,(優先級不是狹義的數值大小,

但為了通俗理解,這裏以字母序為例),通常使用數組存儲,我們可以按照下圖進行轉換,序號 0 不用:

優先級隊列的實現(Java版):

public class PriorityQueue<Key extends Character> {
    /** 存儲元素的數組 */
    private Key[] keys;
    private int N = 0;

    public PriorityQueue(int capacity){
        // 下標0不用,多分配一個單位
        keys = (Key[]) new Character[capacity + 1];
    }

    public Key max(){
        return keys[1];
    }

    public void insert(Key e){
        N ++;
        keys[N] = e;
        swim(N);
    }
    public Key delMax(){
        Key max = keys[1];
        swap(1,N);
        keys[N] = null;
        N --;
        // 讓第一個元素下沉到合適的位置
        sink(1);
        return max;
    }
    /** 上浮第k個元素*/
    private void swim(int k){
        // 比父節點小,即進行交換,直到根
        while (k > 1 && less(parent(k),k)){
            swap(k,parent(k));
            k = parent(k);
        }
    }
    /** 下沉第 k 個元素*/
    private void sink(int k){
        while(k < N){
            int small = left(k);
            if (right(k) < N && less(right(k),left(k))){
                small = right(k);
            }
            if (less(k,small)){
                swap(k,small);
                k = small;
            }
        }
    }
    private void swap(int i,int j){
        Key temp = keys[i];
        keys[i] = keys[j];
        keys[j] = temp;
    }
    /** 元素i和j大小比較*/
    private boolean less(int i,int j){
//   'a' - 'b' = -1 ;
        return keys[i].compareTo(keys[j]) > 0;
    }
    /** 元素i的父節點*/
    private int parent(int i){
        return i/2;
    }
    /** 元素i的左子節點*/
    private int left(int i){
        return i * 2;
    }
    /** 元素i的右子節點*/
    private int right(int i){
        return i * 2 + 1;
    }
}
 

以上代碼解析:

1 swim 上浮,對於元素k,是否需要上浮,僅需與其父節點比較,大於父節點則交換,迭代直到根節點;

2 sink 下沉,對於元素k,是否需要下沉,需先比較其左右子節點,找出左右子節點中較小者,較小者若比父節點大,則交換,迭代直到末尾元素;

3 insert 插入,先將元素放到數組末尾位置,再對其進行上浮操作,直到合適位置;

4 delMax 刪除最大值,大根堆,故第一個元素最大,先將首末元素交換,再刪除末尾元素,再對首元素下沉操作,直到合適位置;

總結:以上只是Java簡化版,java.util.PriorityQueue 是JDK原版,客官可自行研究。但設計還是非常有技巧的,值得思考一番,假設 insert 插入

到首位,會導致數組大量元素移動。delMax 若直接刪除首位最大值,則需要進一步判斷左右子節點大小,並進行先子節點上浮再首元素下沉操作。

        有了這個堆結構,就可以進行堆排序了,將待排數全部加入此堆結構,然後依次取出,即成有序序列了!

2 堆排序

如要求不使用上述堆數據結構。思路(升序為例):將數組構建為一個大頂堆,首元素即為數組最大值,首尾元素交換;排除末尾元素后調整大頂堆,

則新的首元素即為次最大值,交換首尾並再排除末尾元素;如此循環,最後的數組即為升序排列

public class HeapSort02 {
    public static void main(String []args){
        int []arr = {2,1,8,6,4,7,3,0,9,5};
        sort(arr);
        System.out.println(Arrays.toString(arr));
    }

    public static void sort(int []arr){
        int len = arr.length;
        // 創建一個大頂堆
        for(int i = (int) Math.ceil(len/2 - 1); i >= 0; i--){
            //從第一個非恭弘=叶 恭弘子結點從下至上,從右至左調整結構
            adjustHeap(arr,i,len);
        }
        // 交換首尾元素,並重新調整大頂堆
        for(int j = len-1;j > 0;j--){
            swap(arr,0,j);
            adjustHeap(arr,0,j);
        }
    }

    /** 迭代寫法*/
    public static void adjustHeap(int []arr,int i,int length){
        int temp = arr[i];
        for (int k = 2*i + 1; k < length; k=k*2 + 1) {
        // 注意這裏的k + 1 < length
            // 如果右子節點大於左子節點,則比較對象為右子節點
            if (k + 1 < length && arr[k] < arr[k+1]){
                k++;
            }
            if (arr[k] > temp){
                // 不進行值交換
                arr[i] = arr[k];
                i = k;
            }
            else{
                break;
            }
        }
        arr[i] = temp;
    }

    /** 遞歸寫法*/
    private static void adjustHeap2(int[] arr, int i, int len){
        int left = 2 * i + 1;
        int right = 2 * i + 2;
        int maxIndex = i;
        // 注意這裏的 left < len
        if (left < len && arr[left] > arr[maxIndex]){
            maxIndex = left;
        }
        if (right < len && arr[right] > arr[maxIndex]){
            maxIndex = right;
        }
        if (maxIndex != i){
            swap(arr,i,maxIndex);
            adjustHeap2(arr,maxIndex,len);
        }
    }

    /** 交換元素 */
    public static void swap(int []arr,int a ,int b){
        int temp=arr[a];
        arr[a] = arr[b];
        arr[b] = temp;
    }
}
 

以上代碼解析:

1完全二叉樹結構中,如果根節點順序號為 0,總節點數為 N,則最末節點的父節點即為最後一個非恭弘=叶 恭弘子節點,順序號為 ceil(N/2 -1),

2 adjustHeap2 為啥使用三個參數,不用中間的參數可以?使用三個參數,是為了進行遞歸調用,因為遞歸肯定是縮小計算規模,而這裏的形參arr和len是固定不變的;

3 adjustHeap是非遞歸寫法,不用中間的參數可以?調用一在“構建大頂堆”處,可寫為函數體內初始化 i,並形成雙重 for 循環;調用二在“重新調整大頂堆”處,

    可見中間參數為 0,可直接去掉。故回答是可以!但需要調整寫法,且影響該方法復用,這裏直接寫為三個形參的函數更為優雅而已。

4非遞歸寫法理解:類似插入排序思想(依次移動並找到合適的位置再插入),先將 arr[i] 取出,然後此節點和左右子樹進行比較,如子樹更大則子節點上升一層,使

    用for循環迭代到最終位置,並進行賦值;

 

以 i=0 為例:

5遞歸方式理解:定位目標元素的左右子樹,若子樹值更大,則進行值交換,且因為子樹發生了變化,故需要對子樹進行遞歸處理;

3 前K個最大的數

在N個數中找出前K個最大的數: 思路:從N個數中取出前K個數,形成一個數組[K],將該數組調整為一個小頂堆,則可知堆頂為K個數中最小值,

然後依次將剩餘 N-K 個數與堆頂比較,若大於,則替換掉並調整堆,直到所有元素加入完畢,堆中元素即為目標集合。

public class HeapSort {
    public static void main(String[] args) {
        int[] arr = new int[100];
        for (int i = 0; i < 100; i++) {
            arr[i] = i + 1;
        }
        // 前10個最大的數
        int k = 10;
        // 構造小頂堆
        for (int i = (int) Math.ceil(k/2 - 1); i >= 0; i--) {
            adjustHeap(arr,i,k);
        }
        // 依次比較剩餘元素
        for (int i = 10; i < arr.length; i++) {
            if (arr[i] > arr[0]){
                swap(arr,0,i);
                adjustHeap(arr,0,k);
            }
        }
        // 輸出結果
        for (int i = 0; i < 10; i++) {
            System.out.print(arr[i]+"-");
        }
    }

    /** 非迭代寫法 ,對arr[i]進行調整 */
    private static void adjustHeap(int[] arr,int i,int length){
        int temp = arr[i];
        for (int k = i * 2 + 1; k < length; k = k * 2 + 1) {
            // 因第一次循環中可能越界,故需要 k+1 < length
            if (k + 1 < length && arr[k] > arr[k + 1]){
                k++;
            }
            if (arr[k] < temp){
                arr[i] = arr[k];
                i = k;
            }
            else {
                break;
            }
        }
        arr[i] = temp;
    }
    /** 遞歸寫法 */
    private static void adjustHeap2(int[] arr,int i,int length){
        int left = i * 2 + 1;
        int right = i * 2 + 2;
        int samller = i;
        if (left < length && arr[left] > arr[samller]){
            samller = right;
        }
        if (right < length && arr[right] > arr[samller]){
            samller = right;
        }
        if (samller != i){
            swap(arr,i,samller);
            adjustHeap2(arr,samller,length);
        }
    }

    /** 交換元素 */
    public static void swap(int []arr,int a ,int b){
        int temp=arr[a];
        arr[a] = arr[b];
        arr[b] = temp;
    }
}
 

以上代碼解析:按照”初始化—構建小頂堆—比較調整—輸出結果”執行。注意for循環中,因第一次循環中未使用for語句條件判斷,可能越界,故需要 k+1 < length

輸出結果如下:

請看官思考,如果需求變為找出N個數中找出前K個最小的數,該如何實現? 建議動腦且動手的寫一遍!因為魔鬼在細節!

全文完!

我近期其他文章:

  • 1 Dubbo學習系列之十九(Apollo配置中心)
  • 2 聊聊算法——二分查找算法深度分析
  • 3 DevOps系列——Jenkins/Gitlab自動打包部署
  • 4 DevOps系列——Jenkins私服
  • 5 DevOps系列——Gitlab私服

    只寫原創,敬請關注 

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※Google地圖已可更新顯示潭子電動車充電站設置地點!!

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※別再煩惱如何寫文案,掌握八大原則!

為什麼建議你使用枚舉?

枚舉是 JDK 1.5 新增的數據類型,使用枚舉我們可以很好的描述一些特定的業務場景,比如一年中的春、夏、秋、冬,還有每周的周一到周天,還有各種顏色,以及可以用它來描述一些狀態信息,比如錯誤碼等。

枚舉類型不止存在在 Java 語言中,在其它語言中也都能找到它的身影,例如 C# 和 Python 等,但我發現在實際的項目中使用枚舉的人很少,所以本文就來聊一聊枚舉的相關內容,好讓朋友們對枚舉有一個大概的印象,這樣在編程時起碼還能想到有“枚舉”這樣一個類型。

本文的結構目錄如下:

枚舉的 7 種使用方法

很多人不使用枚舉的一個重要的原因是對枚舉不夠熟悉,那麼我們就先從枚舉的 7 種使用方法說起。

用法一:常量

在 JDK 1.5 之前,我們定義常量都是 public static final... ,但有了枚舉,我們就可以把這些常量定義成一個枚舉類了,實現代碼如下:

public enum ColorEnum {  
  RED, GREEN, BLANK, YELLOW  
} 

用法二:switch

將枚舉用在 switch 判斷中,使得代碼可讀性更高了,實現代碼如下:

enum ColorEnum {
    GREEN, YELLOW, RED
}
public class ColorTest {
    ColorEnum color = ColorEnum.RED;

    public void change() {
        switch (color) {
            case RED:
                color = ColorEnum.GREEN;
                break;
            case YELLOW:
                color = ColorEnum.RED;
                break;
            case GREEN:
                color = ColorEnum.YELLOW;
                break;
        }
    }
}

用法三:枚舉中增加方法

我們可以在枚舉中增加一些方法,讓枚舉具備更多的特性,實現代碼如下:

public class EnumTest {
    public static void main(String[] args) {
        ErrorCodeEnum errorCode = ErrorCodeEnum.SUCCESS;
        System.out.println("狀態碼:" + errorCode.code() + 
                           " 狀態信息:" + errorCode.msg());
    }
}

enum ErrorCodeEnum {
    SUCCESS(1000, "success"),
    PARAM_ERROR(1001, "parameter error"),
    SYS_ERROR(1003, "system error"),
    NAMESPACE_NOT_FOUND(2001, "namespace not found"),
    NODE_NOT_EXIST(3002, "node not exist"),
    NODE_ALREADY_EXIST(3003, "node already exist"),
    UNKNOWN_ERROR(9999, "unknown error");

    private int code;
    private String msg;

    ErrorCodeEnum(int code, String msg) {
        this.code = code;
        this.msg = msg;
    }

    public int code() {
        return code;
    }

    public String msg() {
        return msg;
    }

    public static ErrorCodeEnum getErrorCode(int code) {
        for (ErrorCodeEnum it : ErrorCodeEnum.values()) {
            if (it.code() == code) {
                return it;
            }
        }
        return UNKNOWN_ERROR;
    }
}

以上程序的執行結果為:

狀態碼:1000 狀態信息:success

用法四:覆蓋枚舉方法

我們可以覆蓋一些枚舉中的方法用於實現自己的業務,比如我們可以覆蓋 toString() 方法,實現代碼如下:

public class EnumTest {
    public static void main(String[] args) {
        ColorEnum colorEnum = ColorEnum.RED;
        System.out.println(colorEnum.toString());
    }
}

enum ColorEnum {
    RED("紅色", 1), GREEN("綠色", 2), BLANK("白色", 3), YELLOW("黃色", 4);
    //  成員變量
    private String name;
    private int index;

    //  構造方法
    private ColorEnum(String name, int index) {
        this.name = name;
        this.index = index;
    }

    //覆蓋方法
    @Override
    public String toString() {
        return this.index + ":" + this.name;
    }
}

以上程序的執行結果為:

1:紅色

用法五:實現接口

枚舉類可以用來實現接口,但不能用於繼承類,因為枚舉默認繼承了 java.lang.Enum 類,在 Java 語言中允許實現多接口,但不能繼承多個父類,實現代碼如下:

public class EnumTest {
    public static void main(String[] args) {
        ColorEnum colorEnum = ColorEnum.RED;
        colorEnum.print();
        System.out.println("顏色:" + colorEnum.getInfo());
    }
}

interface Behaviour {
    void print();

    String getInfo();
}

enum ColorEnum implements Behaviour {
    RED("紅色", 1), GREEN("綠色", 2), BLANK("白色", 3), YELLOW("黃色", 4);
    private String name;
    private int index;

    private ColorEnum(String name, int index) {
        this.name = name;
        this.index = index;
    }

    @Override
    public void print() {
        System.out.println(this.index + ":" + this.name);
    }

    @Override
    public String getInfo() {
        return this.name;
    }
}

以上程序的執行結果為:

1:紅色

顏色:紅色

用法六:在接口中組織枚舉類

我們可以在一個接口中創建多個枚舉類,用它可以很好的實現“多態”,也就是說我們可以將擁有相同特性,但又有細微實現差別的枚舉類聚集在一個接口中,實現代碼如下:

public class EnumTest {
    public static void main(String[] args) {
        // 賦值第一個枚舉類
        ColorInterface colorEnum = ColorInterface.ColorEnum.RED;
        System.out.println(colorEnum);
        // 賦值第二個枚舉類
        colorEnum = ColorInterface.NewColorEnum.NEW_RED;
        System.out.println(colorEnum);
    }
}

interface ColorInterface {
    enum ColorEnum implements ColorInterface {
        GREEN, YELLOW, RED
    }
    enum NewColorEnum implements ColorInterface {
        NEW_GREEN, NEW_YELLOW, NEW_RED
    }
}

以上程序的執行結果為:

RED

NEW_RED

用法七:使用枚舉集合

在 Java 語言中和枚舉類相關的,還有兩個枚舉集合類 java.util.EnumSetjava.util.EnumMap,使用它們可以實現更多的功能。

使用 EnumSet 可以保證元素不重複,並且能獲取指定範圍內的元素,示例代碼如下:

import java.util.ArrayList;
import java.util.EnumSet;
import java.util.List;

public class EnumTest {
    public static void main(String[] args) {
        List<ColorEnum> list = new ArrayList<ColorEnum>();
        list.add(ColorEnum.RED);
        list.add(ColorEnum.RED);  // 重複元素
        list.add(ColorEnum.YELLOW);
        list.add(ColorEnum.GREEN);
        // 去掉重複數據
        EnumSet<ColorEnum> enumSet = EnumSet.copyOf(list);
        System.out.println("去重:" + enumSet);

        // 獲取指定範圍的枚舉(獲取所有的失敗狀態)
        EnumSet<ErrorCodeEnum> errorCodeEnums = EnumSet.range(ErrorCodeEnum.ERROR, ErrorCodeEnum.UNKNOWN_ERROR);
        System.out.println("所有失敗狀態:" + errorCodeEnums);
    }
}

enum ColorEnum {
    RED("紅色", 1), GREEN("綠色", 2), BLANK("白色", 3), YELLOW("黃色", 4);
    private String name;
    private int index;

    private ColorEnum(String name, int index) {
        this.name = name;
        this.index = index;
    }
}

enum ErrorCodeEnum {
    SUCCESS(1000, "success"),
    ERROR(2001, "parameter error"),
    SYS_ERROR(2002, "system error"),
    NAMESPACE_NOT_FOUND(2003, "namespace not found"),
    NODE_NOT_EXIST(3002, "node not exist"),
    NODE_ALREADY_EXIST(3003, "node already exist"),
    UNKNOWN_ERROR(9999, "unknown error");

    private int code;
    private String msg;

    ErrorCodeEnum(int code, String msg) {
        this.code = code;
        this.msg = msg;
    }

    public int code() {
        return code;
    }

    public String msg() {
        return msg;
    }
}

以上程序的執行結果為:

去重:[RED, GREEN, YELLOW]

所有失敗狀態:[ERROR, SYS_ERROR, NAMESPACE_NOT_FOUND, NODE_NOT_EXIST, NODE_ALREADY_EXIST, UNKNOWN_ERROR]

EnumMapHashMap 類似,不過它是一個專門為枚舉設計的 Map 集合,相比 HashMap 來說它的性能更高,因為它內部放棄使用鏈表和紅黑樹的結構,採用數組作為數據存儲的結構。

EnumMap 基本使用示例如下:

import java.util.EnumMap;

public class EnumTest {
    public static void main(String[] args) {
        EnumMap<ColorEnum, String> enumMap = new EnumMap<>(ColorEnum.class);
        enumMap.put(ColorEnum.RED, "紅色");
        enumMap.put(ColorEnum.GREEN, "綠色");
        enumMap.put(ColorEnum.BLANK, "白色");
        enumMap.put(ColorEnum.YELLOW, "黃色");
        System.out.println(ColorEnum.RED + ":" + enumMap.get(ColorEnum.RED));
    }
}

enum ColorEnum {
    RED, GREEN, BLANK, YELLOW;
}

以上程序的執行結果為:

RED:紅色

使用注意事項

阿里《Java開發手冊》對枚舉的相關規定如下,我們在使用時需要稍微注意一下。

【強制】所有的枚舉類型字段必須要有註釋,說明每個數據項的用途。

【參考】枚舉類名帶上 Enum 後綴,枚舉成員名稱需要全大寫,單詞間用下劃線隔開。說明:枚舉其實就是特殊的常量類,且構造方法被默認強制是私有。正例:枚舉名字為 ProcessStatusEnum 的成員名稱:SUCCESS / UNKNOWN_REASON。

假如不使用枚舉

在枚舉沒有誕生之前,也就是 JDK 1.5 版本之前,我們通常會使用 int 常量來表示枚舉,實現代碼如下:

public static final int COLOR_RED = 1;
public static final int COLOR_BLUE = 2;
public static final int COLOR_GREEN = 3;

但是使用 int 類型可能存在兩個問題:

第一, int 類型本身並不具備安全性,假如某個程序員在定義 int 時少些了一個 final 關鍵字,那麼就會存在被其他人修改的風險,而反觀枚舉類,它“天然”就是一個常量類,不存在被修改的風險(原因詳見下半部分);

第二,使用 int 類型的語義不夠明確,比如我們在控制台打印時如果只輸出 1…2…3 這樣的数字,我們肯定不知道它代表的是什麼含義。

那有人就說了,那就使用常量字符唄,這總不會還不知道語義吧?實現示例代碼如下:

public static final String COLOR_RED = "RED";
public static final String COLOR_BLUE = "BLUE";
public static final String COLOR_GREEN = "GREEN";

但是這樣同樣存在一個問題,有些初級程序員會不按套路出牌,他們可能會直接使用字符串的值進行比較,而不是直接使用枚舉的字段,實現示例代碼如下:

public class EnumTest {
    public static final String COLOR_RED = "RED";
    public static final String COLOR_BLUE = "BLUE";
    public static final String COLOR_GREEN = "GREEN";
    public static void main(String[] args) {
        String color = "BLUE";
        if ("BLUE".equals(color)) {
            System.out.println("藍色");
        }
    }
}

這樣當我們修改了枚舉中的值,那程序就涼涼了。

枚舉使用場景

枚舉的常見使用場景是單例,它的完整實現代碼如下:

public class Singleton {
    // 枚舉類型是線程安全的,並且只會裝載一次
    private enum SingletonEnum {
        INSTANCE;
        // 聲明單例對象
        private final Singleton instance;
        // 實例化
        SingletonEnum() {
            instance = new Singleton();
        }
        private Singleton getInstance() {
            return instance;
        }
    }
    // 獲取實例(單例對象)
    public static Singleton getInstance() {
        return SingletonEnum.INSTANCE.getInstance();
    }
    private Singleton() {
    }
    // 類方法
    public void sayHi() {
        System.out.println("Hi,Java.");
    }
}
class SingletonTest {
    public static void main(String[] args) {
        Singleton singleton = Singleton.getInstance();
        singleton.sayHi();
    }
}

因為枚舉只會在類加載時裝載一次,所以它是線程安全的,這也是《Effective Java》作者極力推薦使用枚舉來實現單例的主要原因。

知識擴展

枚舉為什麼是線程安全的?

這一點要從枚舉最終生成的字節碼說起,首先我們先來定義一個簡單的枚舉類:

public enum ColorEnumTest {
    RED, GREEN, BLANK, YELLOW;
}

然後我們再將上面的那段代碼編譯為字節碼,具體內容如下:

public final class ColorEnumTest extends java.lang.Enum<ColorEnumTest> {
  public static final ColorEnumTest RED;
  public static final ColorEnumTest GREEN;
  public static final ColorEnumTest BLANK;
  public static final ColorEnumTest YELLOW;
  public static ColorEnumTest[] values();
  public static ColorEnumTest valueOf(java.lang.String);
  static {};
}

從上述結果可以看出枚舉類最終會被編譯為被 final 修飾的普通類,它的所有屬性也都會被 staticfinal 關鍵字修飾,所以枚舉類在項目啟動時就會被 JVM 加載並初始化,而這個執行過程是線程安全的,所以枚舉類也是線程安全的類。

小貼士:代碼反編譯的過程是先用 javac 命令將 java 代碼編譯字節碼(.class),再使用 javap 命令查看編譯的字節碼。

枚舉比較小技巧

我們在枚舉比較時使用 == 就夠了,因為枚舉類是在程序加載時就創建了(它並不是 new 出來的),並且枚舉類不允許在外部直接使用 new 關鍵字來創建枚舉實例,所以我們在使用枚舉類時本質上只有一個對象,因此在枚舉比較時使用 == 就夠了。

並且我們在查看枚舉的 equlas() 源碼會發現,它的內部其實還是直接調用了 == 方法,源碼如下:

public final boolean equals(Object other) {
    return this==other;
}

總結

本文我們介紹了枚舉類的 7 種使用方法:常量、switch、枚舉中添加方法、覆蓋枚舉方法、實現接口、在接口中組織枚舉類和使用枚舉集合等,然後講了如果不使用枚舉類使用 int 類型和 String 類型存在的一些弊端:語義不夠清晰、容易被修改、存在被誤用的風險,所以我們在適合的環境下應該盡量使用枚舉類。並且我們還講了枚舉類的使用場景——單例,以及枚舉類為什麼是安全的,最後我們講了枚舉比較的小技巧,希望本文對你有幫助。

查看 & 鳴謝

https://www.iteye.com/blog/softbeta-1185573

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※想知道最厲害的網頁設計公司"嚨底家"!

※幫你省時又省力,新北清潔一流服務好口碑

※別再煩惱如何寫文案,掌握八大原則!

可拖拽圓形進度條組件(支持移動端)

好久之前寫過一個可拖拽圓形進度條的dome,中間有網友反饋過一些問題,最近比較閑有時間修改了一些問題也做了一些優化,並封裝成組件,基於canvas實現,只需傳入放置組件dom容器,任何框架均可直接使用;

codepen 示例如下:https://codepen.io/pangyongsheng/pen/XRmNRK

 

一、如何使用

npm下載

執行 npm i drag-arc -S 或 cnpm i drag-arc -S

 
import DragArc from 'drag-arc';
 new DragArc({
    el: dom,
    value: 10,
    change: (v) => {
        console.log(v)
    },
    ...
})
或者 也可從項目下載dist/dist/drag-arc.min.js,直接通過srcipt標籤引入

其中dom為放置組件HTML容器,可通過ref獲取;

主要屬性方法(詳見github/npm)

項目地址:https://github.com/pangyongsheng/canvas-arc-draw
npm地址:https://www.npmjs.com/package/drag-arc

Name Description Type Default Required
el 放置組件的DOM元素 Element none Y
change 當前值變化時觸發的事件,回調參數為當前進度值Number(0-100) Function ()=>{} N
startDeg 滑動圓弧的起始弧度 Number  0 N
endDeg 滑動圓弧的結束弧度 Number 1 N
value 默認值 Number (0-100) 0 N
textShow 显示文字 Boolean true N
color 外側圓弧顏色 String,Array [“#06dabc”, “#33aaff”] N
slider 滑塊半徑 Number #FFF N
innerColor 內側弧度的顏色 String #ccc N
outColor 外側圓弧背景顏色 String,Array #ccc N
innerLineWidth 內側弧線寬 Number 1 N
outLineWidth 外側弧線寬 Number 20 N
counterclockwise 逆時針方向 Boolean true N
sliderColor 滑塊顏色 String #CCC N
sliderBorderColor 滑塊邊框顏色 String #fff N

二、實現方法簡介

1、繪製位置幾何關係

如圖所示,以canvas畫布中心點建立坐標系,則有:

滑塊位置與弧度關係:

由圓的參數方程得出
x=rcosφ
y=rsinφ

鼠標移動位置與弧度關係:

通過事件回調參數 我們可以獲得 鼠標mousemove事件或者移動端touchmove事件的x,y坐標,可計算tan值為
tanφ = y/x;
再通過反三角函數有可得:
φ=arctan(tanφ)

以上基本的位置關係已經得出;

2、js實現中的幾個問題

(1)坐標的轉化方法

由於上述位置關係是基於中心坐標實現的,而canvas繪製坐標是以左上角為原點實現的,故需要實現兩種坐標的轉化關係;

(2)canvas弧度位置與正常弧度位置的轉化

下圖是canvas的弧度位置恰好與我們正常計算的方向是相反的,同樣需考慮弧度的轉換;

(3)Math.atan方法返回值與實際弧度的關係

由於Math.atan() 函數返回一個數值的反正切[- π/2 , π/2 ],
而實際中我們需要獲得到[0-2π]直接的值,所以在通過鼠標位置獲取弧度值時需要通過Math.atan(y/x)和xy在中心坐標的正負綜合判斷其所在象限從何獲取實際的獲取弧度值;

(4)弧度與進度條值得關係

由於鼠標移動觸發繪圖方法是較為連續的動畫效果,而進度是間隔的,
這裏我們需要實現個類似d3js中domain和range的比例關係。
這裏我們將值[0,100]對應弧度比例為[startDeg, endDeg]

(5)終點的判斷

由於鼠標移動的位置是任意的,可能導致滑塊到達終點後由於鼠標移動到了起點時,滑塊也直接從終點移動到起點,故需對起點終點做判斷,到達起點后不可再向後滑動,到達終點后不可再向前滑動;

3、詳細實現方法可以參考這篇文章

 https://www.cnblogs.com/pangys/p/6837344.html

 

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※Google地圖已可更新顯示潭子電動車充電站設置地點!!

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※別再煩惱如何寫文案,掌握八大原則!

多圖解釋Redis的整數集合intset升級過程

redis源碼分析系列文章

[Redis源碼系列]在Liunx安裝和常見API 

為什麼要從Redis源碼分析 

String底層實現——動態字符串SDS 

雙向鏈表都不懂,還說懂Redis?

面試官:說說Redis的Hash底層 我:……(來自閱文的面試題)

Redis的跳躍表確定不了解下

 

前言

大噶好,今天仍然是元氣滿滿的一天,拋開永遠寫不完的需求,拒絕要求賊變態的客戶,單純的學習技術,感受技術的魅力。(哈哈哈,皮一下很開森)

前面幾周我們一起看了Redis底層數據結構,如動態字符串SDS雙向鏈表Adlist字典Dict跳躍表,如果有對Redis常見的類型或底層數據結構不明白的請看上面傳送門。

今天來說下set的底層實現整數集合,如果有對set不明白的,常見的API使用這篇就不講了,看上面的傳送門哈。

整數集合概念

整數集合是Redis設計的一種底層結構,是set的底層實現,當集合中只包含整數值元素,並且這個集合元素數據不多時,會使用這種結構。但是如果不滿足剛才的條件,會使用其他結構,這邊暫時不講哈。

下圖為整數集合的實際組成,包括三個部分,分別是編碼格式encoding,包含元素數量length,保存元素的數組contents。(這邊只需要簡單看下,下面針對每個模塊詳細說明哈)

整數集合的實現

我們看下intset.h裏面關於整數集合的定義,上代碼哈:

//整數集合結構體
typedef struct intset {
    uint32_t encoding;  //編碼格式,有如下三種格式,初始值默認為INTSET_ENC_INT16
    uint32_t length;    //集合元素數量
    int8_t contents[];  //保存元素的數組,元素類型並不一定是ini8_t類型,柔性數組不佔intset結構體大小,並且數組中的元素從小到大排列。
} intset;               

#define INTSET_ENC_INT16 (sizeof(int16_t))   //16位,2個字節,表示範圍-32,768~32,767
#define INTSET_ENC_INT32 (sizeof(int32_t))   //32位,4個字節,表示範圍-2,147,483,648~2,147,483,647
#define INTSET_ENC_INT64 (sizeof(int64_t))   //64位,8個字節,表示範圍-9,223,372,036,854,775,808~9,223,372,036,854,775,807

 

 

編碼格式encoding

包括INTSET_ENC_INT16,INTSET_ENC_INT32,INTSET_ENC_INT64三種類型,其分別對應着不同的範圍,具體看上面代碼的註釋信息。

因為插入的數據的大小是不一樣的,為了盡可能的節約內存(畢竟都是錢,平時要省着點用),所以我們需要使用不同的類型來存儲數據。

集合元素數量length

記錄了保存數據contents的長度,即有多少個元素。

保存元素的數組contents

真正存儲數據的地方,數組是按照從小到大有序排序的,並且不包含任何重複項(因為set是不含重複項,所以其底層實現也是不含包含項的)。

整數集合升級過程(重點,手動標星)

上面的圖我們重新看下,編碼格式encoding為INTSET_ENC_INT16,即每個數據佔16位。長度length為4,即數組content裏面有四個元素,分別是1,2,3,4。如果我們要添加一個数字位40000,很明顯超過編碼格式為INTSET_ENC_INT16的範圍-32,768~32,767,應該是編碼格式為INTSET_ENC_INT32。那麼他是如何升級的呢,從INTSET_ENC_INT16升級到INTSET_ENC_INT32的呢?

1.了解舊的存儲格式

首先我們看下1,2,3,4這四個元素是如何存儲的。首先要知道一共有多少位,計算規則為length*編碼格式的位數,即4*16=64。所以每個元素佔用了16位。

2.確定新的編碼格式

新的元素為40000,已經超過了INTSET_ENC_INT16的範圍-32,768~32,767,所以新的編碼格式為INTSET_ENC_INT32。

3.根據新的編碼格式新增內存

上面已經說明了編碼格式為INTSET_ENC_INT32,計算規則為length*編碼格式的位數,即5*32=160。所以新增的位數為64-159。

4.根據編碼格式設置對應的值

從上面知道按照新的編碼格式,每個數據應該佔用32位,但是舊的編碼格式,每個數據佔用16位。所以我們從後面開始,每次獲取32位用來存儲數據。

這樣說太難懂了,看下圖。

首先,那最後32位,即128-159存儲40000。那麼第49-127是空着的。

接着,取空着的49-127最後的32位,即96到127這32位,用來存儲4。那麼之前4存儲的位置48-6349-127剩下的64-95這兩部分組成了一個大部分,即48-95,現在空着啦。

在接着在48-95這個大部分,再取后32位,即64-95,用來存儲3。那麼之前3存儲位置32-4748-95剩下的48-63這兩部分組成了一個大部分,即32-63,現在空着啦。

再接着,將32-63這個大部分,再取后32位,即還是32-63,用來存儲2。那麼之前2存儲位置16-31空着啦。

最後,將16-31和原來0-31合起來,存儲1。

至此,整個升級過程結束。整體來說,分為3步,確定新的編碼格式,新增需要的內存空間,從后往前調整數據。

這邊有個小問題,為啥要從后往前調整數據呢?

原因是如果從前往後,數據可能會覆蓋。也拿上面個例子來說,數據1在0-15位,數據2在16-31位,如果從前往後,我們知道新的編碼格式INTSET_ENC_INT32要求每個元素佔用32位,那麼數據1應該佔用0-31,這個時候數據2就被覆蓋了,以後就不知道數據2啦。

但是從后往前,因為後面新增了一些內存,所以不會發生覆蓋現象。

升級的優點

 節約內存

整數集合既可以讓集合保存三種不同類型的值,又可以確保升級操作只在有需要的時候進行,這樣就節省了內存。 

不支持降級

一旦對數組進行升級,編碼就會一直保存升級后的狀態。即使後面把40000刪掉了,編碼格式還是不會將會INTSET_ENC_INT16。

整數集合的源碼分析

創建一個空集合 intsetnew

這個方法比較簡單,是初始化整數集合的步驟,即下圖部分。

主要的步驟是分配內存空間,設置默認編碼格式,以及初始化數組長度length。

intset *intsetNew(void) {
    intset *is = zmalloc(sizeof(intset));//分配內存空間 
    is->encoding = intrev32ifbe(INTSET_ENC_INT16);//設置默認編碼格式INTSET_ENC_INT16 
    is->length = 0;//初始化length 
    return is;
}

添加元素並升級insetAdd流程圖(重點)

添加元素並升級insetAdd源碼分析

可以根據上面的流程圖,對照着下面的源碼分析,這邊就不寫啦哈。

//添加元素
//輸入參數*is為原整數集合
//value為要添加的元素
//*success為是否添加成功的標誌量 ,1表示成功,0表示失敗 
intset *intsetAdd(intset *is, int64_t value, uint8_t *success) {
    //確定要添加的元素的編碼格式 
    uint8_t valenc = _intsetValueEncoding(value);
    
    uint32_t pos;
    //如果success沒有初始值,則初始化為1 
    if (success) *success = 1;

   //如果新的編碼格式大於現在的編碼格式,則升級並添加元素 
    if (valenc > intrev32ifbe(is->encoding)) {
        //調用另一個方法 
        return intsetUpgradeAndAdd(is,value);
    } else {
        //如果編碼格式不變,則調用查詢方法 
        //輸入參數is為原整數集合 
        //value為要添加的數據
        //pos為位置 
        if (intsetSearch(is,value,&pos)) {//如果找到了,則直接返回,因為數據是不可重複的。 
            if (success) *success = 0;
            return is;
        }

        //設置length 
        is = intsetResize(is,intrev32ifbe(is->length)+1);
        if (pos < intrev32ifbe(is->length)) intsetMoveTail(is,pos,pos+1);
    }
    //設置數據 
    _intsetSet(is,pos,value);
    is->length = intrev32ifbe(intrev32ifbe(is->length)+1);
    return is;
}


//#define INT8_MAX 127
//#define INT16_MAX 32767
//#define INT32_MAX 2147483647
//#define INT64_MAX 9223372036854775807LL 
static uint8_t _intsetValueEncoding(int64_t v) {
    if (v < INT32_MIN || v > INT32_MAX)
        return INTSET_ENC_INT64;
    else if (v < INT16_MIN || v > INT16_MAX)
        return INTSET_ENC_INT32;
    else
        return INTSET_ENC_INT16;
}


//根據輸入參數value的編碼格式,對整數集合is的編碼格式升級 
static intset *intsetUpgradeAndAdd(intset *is, int64_t value) {
    //當前集合的編碼格式 
    uint8_t curenc = intrev32ifbe(is->encoding);
    //根據對value解析獲取新的編碼格式 
    uint8_t newenc = _intsetValueEncoding(value);
    //獲取集合元素數量 
    int length = intrev32ifbe(is->length);
    //如果要添加的數據小於0,則prepend為1,否則為0 
    int prepend = value < 0 ? 1 : 0;

   //設置集合為新的編碼格式,並根據編碼格式重新設置內存 
    is->encoding = intrev32ifbe(newenc);
    is = intsetResize(is,intrev32ifbe(is->length)+1);

    //逐步循環,直到length小於0,挨個重新設置每個值,從后往前 
    while(length--)
        _intsetSet(is,length+prepend,_intsetGetEncoded(is,length,curenc));

    //如果value為負數,則放在最前面 
    if (prepend)
        _intsetSet(is,0,value);
    else//如果value為整數,設置最末尾的元素為value 
        _intsetSet(is,intrev32ifbe(is->length),value);
    //重新設置length 
    is->length = intrev32ifbe(intrev32ifbe(is->length)+1);
    return is;
}


//找到is集合中值為value的下標,返回1,並保存在pos中,沒有找到返回0,並將pos設置為value可以插入到數組的位置
static uint8_t intsetSearch(intset *is, int64_t value, uint32_t *pos) {
    int min = 0, max = intrev32ifbe(is->length)-1, mid = -1;
    int64_t cur = -1;

    //如果集合為空,那麼位置pos為0 
    if (intrev32ifbe(is->length) == 0) { 
        if (pos) *pos = 0;
        return 0;
    } else {
        //因為數據是有序集合,如果要添加的數據大於最後一個数字,那麼直接把要添加的值放在最後即可,返回最大值下標 
        if (value > _intsetGet(is,intrev32ifbe(is->length)-1)) {
            if (pos) *pos = intrev32ifbe(is->length);
            return 0;
        } else if (value < _intsetGet(is,0)) { //如果這個數據小於數組下標為0的數據,即為最小值 ,返回0 
            if (pos) *pos = 0;
            return 0;
        }
    }
    //有序集合採用二分法 
    while(max >= min) {
        mid = ((unsigned int)min + (unsigned int)max) >> 1;
        cur = _intsetGet(is,mid);
        if (value > cur) {
            min = mid+1;
        } else if (value < cur) {
            max = mid-1;
        } else {
            break;
        }
    }

    //確定找到 
    if (value == cur) {
        if (pos) *pos = mid;//設置參數pos,返回1,即找到位置 
        return 1;
    } else {//如果沒找到,則min和max相鄰,隨便設置都行,並返回0 
        if (pos) *pos = min; 
        return 0;
    }
}

 

結語

該篇主要講了Redis的SET數據類型的底層實現整數集合,先從整數集合是什麼,,剖析了其主要組成部分,進而通過多幅過程圖解釋了intset是如何升級的,最後結合源碼對整數集合進行描述,如創建過程,升級過程,中間穿插例子和過程圖。

如果覺得寫得還行,麻煩給個贊,您的認可才是我寫作的動力!

如果覺得有說的不對的地方,歡迎評論指出。

好了,拜拜咯。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※想知道最厲害的網頁設計公司"嚨底家"!

※幫你省時又省力,新北清潔一流服務好口碑

※別再煩惱如何寫文案,掌握八大原則!

大自然工程師河狸將修築堤壩 助英格蘭抗水患

摘錄自2019年11月20日中央通訊社倫敦報導

業務涵蓋歷史古蹟與鄉村管理的英國保育組織「國家信託」(National Trust)今天(20日)宣布,預定明年初在英格蘭南部兩地施放天生會修築堤壩的歐亞河狸,協助對抗水患。其中一地的計畫經理伊爾德利(Ben Eardley)指出:「河狸修築的堤壩在乾季可儲水,此外還有助降低下游暴洪、減少河岸侵蝕,攔截淤泥也可改善水質。」

河狸素有「大自然工程師」美譽,牠創造的濕地環境可供小至昆蟲、大至野禽等許多物種棲息。這些河狸將生活在有柵欄隔離林地,專家將監測棲地變化。

「國家信託」計畫於2025年前讓2萬5000公頃土地重新成為大量野生動植物的棲地。英國氣象局(Met Office)資料顯示,英格蘭北部近幾週遭逢嚴重水患,部分地區創下有紀錄以來最潮濕秋季。英格蘭光是今天早上就有18起水患警報,另有58起可能淹水警告。

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※Google地圖已可更新顯示潭子電動車充電站設置地點!!

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※別再煩惱如何寫文案,掌握八大原則!