標籤: 網頁設計

  • Spring AOP學習筆記03:AOP的核心實現之獲取增強器

      上文講了spring是如何開啟AOP的,簡單點說就是將AnnotationAwareAspectJAutoProxyCreator這個類註冊到容器中,因為這個類最終實現了BeanPostProcessor接口,並且在其postProcessAfterInitialization()方法中完成了AOP代理對象的創建,創建時機則是在bean的init方法被執行之後即bean初始化完成之後。postProcessAfterInitialization()方法是重點,本文及下一篇文章都是圍繞着這個方法來的。

      該方法是實現在其父類中的AbstractAutoProxyCreator,我們先來看一下其實現:

    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        if (bean != null) {
                // 根據給定的bean的class和name構件key,格式:beanClassName_beanName
            Object cacheKey = getCacheKey(bean.getClass(), beanName);
            if (!this.earlyProxyReferences.containsKey(cacheKey)) {
                        // 如果它適合被代理,則在這裏面生成代理類
                return wrapIfNecessary(bean, beanName, cacheKey);
            }
        }
        return bean;
    }
    
    protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) {
        // 如果已經處理過則不再處理
        if (beanName != null && this.targetSourcedBeans.containsKey(beanName)) {
            return bean;
        }
        // 無需增強則直接返回
        if (Boolean.FALSE.equals(this.advisedBeans.get(cacheKey))) {
            return bean;
        }
        // 指定的bean類是否代表一個基礎設施類,基礎設施類不應被代理,或者配置了指定bean也不需要自動代理
        if (isInfrastructureClass(bean.getClass()) || shouldSkip(bean.getClass(), beanName)) {
            this.advisedBeans.put(cacheKey, Boolean.FALSE);
            return bean;
        }
    
        // 如果存在Advice則創建代理
        Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, null);
        // 如果獲取到了增強則需要針對增強創建代理
        if (specificInterceptors != DO_NOT_PROXY) {
            this.advisedBeans.put(cacheKey, Boolean.TRUE);
                // 創建代理
            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;
    }

      在上面的函數中我們已經可以看到代理創建的雛形,當然,在真正開始之前還需要經過一系列判斷,比如是否已經處理過或者是否是需要跳過的bean,而真正創建代理是從getAdvicesAndAdvisorsForBean開始的。
      創建代理主要包含了兩個步驟:

    • 獲取增強方法或者增強器;
    • 根據獲取的增強進行代理創建;

      這裏雖然看起來只有簡單的兩步,但是每一步中都有着大量複雜的邏輯。本文先來看看獲取增強方法的實現邏輯。

    protected Object[] getAdvicesAndAdvisorsForBean(Class beanClass, String beanName, TargetSource targetSource) {
        List advisors = findEligibleAdvisors(beanClass, beanName);
        if (advisors.isEmpty()) {
            return DO_NOT_PROXY;
        }
        return advisors.toArray();
    }
    
    protected List<Advisor> findEligibleAdvisors(Class beanClass, String beanName) {
        List<Advisor> candidateAdvisors = findCandidateAdvisors();
        List<Advisor> eligibleAdvisors = findAdvisorsThatCanApply(candidateAdvisors, beanClass, beanName);
        extendAdvisors(eligibleAdvisors);
        if (!eligibleAdvisors.isEmpty()) {
            eligibleAdvisors = sortAdvisors(eligibleAdvisors);
        }
        return eligibleAdvisors;
    }

      對於某個bean的增強方法的獲取也是包含兩個步驟的,獲取所有的增強以及尋找所有增強中適用於bean的增強並應用,而findCandidateAdvisors()與findAdvisorsThatCanApply()便是做了這兩件事情。這裏如果無法找到對應的增強器則直接返回DO_NOT_PROXY,也就是null。

    1. 獲取增強器

      這裏我們分析的是註解方式的AOP,對於findCandidateAdvisors的實現其實是由AnnotationAwareAspectJAutoProxyCreator類來完成的,繼續跟蹤:

    protected List<Advisor> findCandidateAdvisors() {
        // 使用註解方式配置AOP的時候並不會丟棄對XML配置的支持,在這裡會調用父類方法去加載配置文件中的AOP聲明
        List<Advisor> advisors = super.findCandidateAdvisors();
        // Build Advisors for all AspectJ aspects in the bean factory.
        advisors.addAll(this.aspectJAdvisorsBuilder.buildAspectJAdvisors());
        return advisors;
    }

      AnnotationAwareAspectJAutoProxyCreator間接繼承了AbstractAdvisorAutoProxyCreator,在實現獲取增強的方法中除了保留父類的獲取配置文件中定義的增強外,同時增加了獲取Bean的註解增強的功能,這部分實現是由this.aspectJAdvisorsBuilder.buildAspectJAdvisors()來完成的。

      這裏其實可以自己先嘗試想象一下解析思路,看看自己的實現與Spring是否有差別?我們可以先在自己頭腦中嘗試實現一下獲取增強這個功能點,看看是否有思路。實際上,Spring在實現的時候主要分成了四步:

    • 獲取所有beanName,這一步驟中所有在beanFactory中註冊的Bean都會被提取出來;
    • 遍歷所有beanName,並找出聲明AspectJ註解的類,進行進一步的處理;
    • 對標記為AspectJ註解的類進行增強器的提取;
    • 將提取結果加入緩存;

      我們來看看Spring如何實現對所有的類進行分析並提取Advisor:

    public List<Advisor> buildAspectJAdvisors() {
        List<String> aspectNames = null;
    
        synchronized (this) {
            aspectNames = this.aspectBeanNames;
            if (aspectNames == null) {
                List<Advisor> advisors = new LinkedList<Advisor>();
                aspectNames = new LinkedList<String>();
                // 獲取所有的beanName
                String[] beanNames =
                        BeanFactoryUtils.beanNamesForTypeIncludingAncestors(this.beanFactory, Object.class, true, false);
                // 遍歷所有的beanName找出對應的增強方法
                for (String beanName : beanNames) {
                    // 不合法的bean則略過,由子類定義規則,默認返回true
                    if (!isEligibleBean(beanName)) {
                        continue;
                    }
                    // 獲取對應的bean的類型
                    Class beanType = this.beanFactory.getType(beanName);
                    if (beanType == null) {
                        continue;
                    }
                    // 如果該bean上存在Aspect註解
                    if (this.advisorFactory.isAspect(beanType)) {
                        aspectNames.add(beanName);
                        AspectMetadata amd = new AspectMetadata(beanType, beanName);
                        if (amd.getAjType().getPerClause().getKind() == PerClauseKind.SINGLETON) {
                            MetadataAwareAspectInstanceFactory factory =
                                    new BeanFactoryAspectInstanceFactory(this.beanFactory, beanName);
                            // 解析增強方法
                            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 {
                            // Per target or per this.
                            if (this.beanFactory.isSingleton(beanName)) {
                                throw new IllegalArgumentException("Bean with name '" + beanName +
                                        "' is a singleton, but aspect instantiation model is not singleton");
                            }
                            MetadataAwareAspectInstanceFactory factory =
                                    new PrototypeAspectInstanceFactory(this.beanFactory, beanName);
                            this.aspectFactoryCache.put(beanName, factory);
                            advisors.addAll(this.advisorFactory.getAdvisors(factory));
                        }
                    }
                }
                this.aspectBeanNames = aspectNames;
                return advisors;
            }
        }
    
        if (aspectNames.isEmpty()) {
            return Collections.EMPTY_LIST;
        }
        // 記錄緩存
        List<Advisor> advisors = new LinkedList<Advisor>();
        for (String aspectName : aspectNames) {
            List<Advisor> cachedAdvisors = this.advisorsCache.get(aspectName);
            if (cachedAdvisors != null) {
                advisors.addAll(cachedAdvisors);
            }
            else {
                MetadataAwareAspectInstanceFactory factory = this.aspectFactoryCache.get(aspectName);
                advisors.addAll(this.advisorFactory.getAdvisors(factory));
            }
        }
        return advisors;
    }

      到這裏已經完成了Advisor的提取,在上面步驟中最為重要也最為複雜的就是增強器的獲取,這一功能是委託給getAdvisors方法來實現(this.advisorFactory.getAdvisors(factory))。 

    public List<Advisor> getAdvisors(MetadataAwareAspectInstanceFactory maaif) {
        // 獲取標記為AspectJ的類及其名字,並驗證
        final Class<?> aspectClass = maaif.getAspectMetadata().getAspectClass();
        final String aspectName = maaif.getAspectMetadata().getAspectName();
        validate(aspectClass);
    
        // We need to wrap the MetadataAwareAspectInstanceFactory with a decorator
        // so that it will only instantiate once.
        final MetadataAwareAspectInstanceFactory lazySingletonAspectInstanceFactory =
                new LazySingletonAspectInstanceFactoryDecorator(maaif);
    
        final List<Advisor> advisors = new LinkedList<Advisor>();
        for (Method method : getAdvisorMethods(aspectClass)) {
            // 獲取增強器
            Advisor advisor = getAdvisor(method, lazySingletonAspectInstanceFactory, advisors.size(), aspectName);
            if (advisor != null) {
                advisors.add(advisor);
            }
        }
    
        // If it's a per target aspect, emit the dummy instantiating aspect.
        if (!advisors.isEmpty() && lazySingletonAspectInstanceFactory.getAspectMetadata().isLazilyInstantiated()) {
            Advisor instantiationAdvisor = new SyntheticInstantiationAdvisor(lazySingletonAspectInstanceFactory);
            advisors.add(0, instantiationAdvisor);
        }
    
        // Find introduction fields.
        for (Field field : aspectClass.getDeclaredFields()) {
            Advisor advisor = getDeclareParentsAdvisor(field);
            if (advisor != null) {
                advisors.add(advisor);
            }
        }
    
        return advisors;
    }

      上面函數中首先完成了對增強器的獲取,包括獲取註解以及根據註解生成增強的步驟,然後考慮到在配置中可能會將增強配置成延遲初始化,那麼需要在首位加入同步實例化增強器以保證增強使用之前的實例化,最後是對DeclareParents註解的獲取,這裏着重分析對增強器的獲取。
      對普通增強器的獲取邏輯是實現在getAdvisor方法中的,實現步驟包括對切點的註解的獲取以及根據註解信息生成增強。

    public Advisor getAdvisor(Method candidateAdviceMethod, MetadataAwareAspectInstanceFactory aif,
            int declarationOrderInAspect, String aspectName) {
    
        validate(aif.getAspectMetadata().getAspectClass());
        // 切點信息的獲取
        AspectJExpressionPointcut ajexp =
                getPointcut(candidateAdviceMethod, aif.getAspectMetadata().getAspectClass());
        if (ajexp == null) {
            return null;
        }
        // 根據切點信息生成增強器
        return new InstantiationModelAwarePointcutAdvisorImpl(
                this, ajexp, aif, candidateAdviceMethod, declarationOrderInAspect, aspectName);
    }

    1.1 切點信息的獲取

      所謂獲取切點信息就是指定註解的表達式信息的獲取,如@Before(“test()”)。

    private AspectJExpressionPointcut getPointcut(Method candidateAdviceMethod, Class<?> candidateAspectClass) {
        // 獲取方法上的註解
        AspectJAnnotation<?> aspectJAnnotation =
                AbstractAspectJAdvisorFactory.findAspectJAnnotationOnMethod(candidateAdviceMethod);
        if (aspectJAnnotation == null) {
            return null;
        }
        // 使用AspectJExpressionPointcut實例封裝獲取的信息
        AspectJExpressionPointcut ajexp =
                new AspectJExpressionPointcut(candidateAspectClass, new String[0], new Class[0]);
        // 提取得到的註解中的表達式,如:
        // @Pointcut("execution(* *.*test*(..))")中的execution(* *.*test*(..))")
        ajexp.setExpression(aspectJAnnotation.getPointcutExpression());
        return ajexp;
    }
    
    protected static AspectJAnnotation findAspectJAnnotationOnMethod(Method method) {
        // 設置敏感的註解類
        Class<? extends Annotation>[] classesToLookFor = new Class[] {
                Before.class, Around.class, After.class, AfterReturning.class, AfterThrowing.class, Pointcut.class};
        for (Class<? extends Annotation> c : classesToLookFor) {
            AspectJAnnotation foundAnnotation = findAnnotation(method, c);
            if (foundAnnotation != null) {
                return foundAnnotation;
            }
        }
        return null;
    }
    
    // 獲取指定方法上的註解並使用AspectJAnnotation封裝
    private static <A extends Annotation> AspectJAnnotation<A> findAnnotation(Method method, Class<A> toLookFor) {
        A result = AnnotationUtils.findAnnotation(method, toLookFor);
        if (result != null) {
            return new AspectJAnnotation<A>(result);
        }
        else {
            return null;
        }
    }

    1.2 根據切點信息生成增強

      所有的增強都是由Advisor的實現類InstantiationModelAwarePointcutAdvisorImpl統一封裝的。

    public InstantiationModelAwarePointcutAdvisorImpl(AspectJAdvisorFactory af, AspectJExpressionPointcut ajexp,
            MetadataAwareAspectInstanceFactory aif, Method method, int declarationOrderInAspect, String aspectName) {
    
        this.declaredPointcut = ajexp;
        this.method = method;
        this.atAspectJAdvisorFactory = af;
        this.aspectInstanceFactory = aif;
        this.declarationOrder = declarationOrderInAspect;
        this.aspectName = aspectName;
    
        if (aif.getAspectMetadata().isLazilyInstantiated()) {
            // Static part of the pointcut is a lazy type.
            Pointcut preInstantiationPointcut =
                    Pointcuts.union(aif.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, aif);
            this.lazy = true;
        }
        else {
            // A singleton aspect.
            this.instantiatedAdvice = instantiateAdvice(this.declaredPointcut);
            this.pointcut = declaredPointcut;
            this.lazy = false;
        }
    }

      在封裝過程中只是簡單地將信息封裝在類的實例中,所有的信息單純地賦值,在實例初始化的過程中還完成了對於增強器的初始化。因為不同的增強所體現的邏輯是不同的,比如@Before(“test()”)與@After(“test()”)標籤的區別就是增強器增強的位置不同,所以就需要不同的增強器來完成不同的邏輯,而根據註解中的信息初始化對應的增強器就是在instantiateAdvice函數中實現的。

    private Advice instantiateAdvice(AspectJExpressionPointcut pcut) {
        return this.atAspectJAdvisorFactory.getAdvice(
                this.method, pcut, this.aspectInstanceFactory, this.declarationOrder, this.aspectName);
    }
    
        public Advice getAdvice(Method candidateAdviceMethod, AspectJExpressionPointcut ajexp,
            MetadataAwareAspectInstanceFactory aif, int declarationOrderInAspect, String aspectName) {
    
        Class<?> candidateAspectClass = aif.getAspectMetadata().getAspectClass();
        validate(candidateAspectClass);
    
        AspectJAnnotation<?> aspectJAnnotation =
                AbstractAspectJAdvisorFactory.findAspectJAnnotationOnMethod(candidateAdviceMethod);
        if (aspectJAnnotation == null) {
            return null;
        }
    
        // If we get here, we know we have an AspectJ method.
        // Check that it's an AspectJ-annotated class
        if (!isAspect(candidateAspectClass)) {
            throw new AopConfigException("Advice must be declared inside an aspect type: " +
                    "Offending method '" + candidateAdviceMethod + "' in class [" +
                    candidateAspectClass.getName() + "]");
        }
    
        if (logger.isDebugEnabled()) {
            logger.debug("Found AspectJ method: " + candidateAdviceMethod);
        }
    
        AbstractAspectJAdvice springAdvice;
        // 根據不同的註解類型封裝不同的增強器
        switch (aspectJAnnotation.getAnnotationType()) {
            case AtBefore:
                springAdvice = new AspectJMethodBeforeAdvice(candidateAdviceMethod, ajexp, aif);
                break;
            case AtAfter:
                springAdvice = new AspectJAfterAdvice(candidateAdviceMethod, ajexp, aif);
                break;
            case AtAfterReturning:
                springAdvice = new AspectJAfterReturningAdvice(candidateAdviceMethod, ajexp, aif);
                AfterReturning afterReturningAnnotation = (AfterReturning) aspectJAnnotation.getAnnotation();
                if (StringUtils.hasText(afterReturningAnnotation.returning())) {
                    springAdvice.setReturningName(afterReturningAnnotation.returning());
                }
                break;
            case AtAfterThrowing:
                springAdvice = new AspectJAfterThrowingAdvice(candidateAdviceMethod, ajexp, aif);
                AfterThrowing afterThrowingAnnotation = (AfterThrowing) aspectJAnnotation.getAnnotation();
                if (StringUtils.hasText(afterThrowingAnnotation.throwing())) {
                    springAdvice.setThrowingName(afterThrowingAnnotation.throwing());
                }
                break;
            case AtAround:
                springAdvice = new AspectJAroundAdvice(candidateAdviceMethod, ajexp, aif);
                break;
            case AtPointcut:
                if (logger.isDebugEnabled()) {
                    logger.debug("Processing pointcut '" + candidateAdviceMethod.getName() + "'");
                }
                return null;
            default:
                throw new UnsupportedOperationException(
                        "Unsupported advice type on method " + candidateAdviceMethod);
        }
    
        // Now to configure the advice...
        springAdvice.setAspectName(aspectName);
        springAdvice.setDeclarationOrder(declarationOrderInAspect);
        String[] argNames = this.parameterNameDiscoverer.getParameterNames(candidateAdviceMethod);
        if (argNames != null) {
            springAdvice.setArgumentNamesFromStringArray(argNames);
        }
        springAdvice.calculateArgumentBindings();
        return springAdvice;
    }

      從上面可以看到,Spring會根據不同的註解生成不同的增強器,例如AtBefore會對應AspectJMethodBeforeAdvice,在AspectJMethodBeforeAdvice中完成了增強方法的邏輯。本文嘗試分析幾個常用的增強器實現。

    AspectJMethodBeforeAdvice

      這是前置增強器,在Spring中,它會封裝到MethodBeforeAdviceInterceptor類的內部,這個是一個攔截器,會被放在攔截器鏈中,在創建代理bean時會用到:

    public class MethodBeforeAdviceInterceptor implements MethodInterceptor, Serializable {
    
        private MethodBeforeAdvice advice;
    
        /**
         * Create a new MethodBeforeAdviceInterceptor for the given advice.
         * @param advice the MethodBeforeAdvice to wrap
         */
        public MethodBeforeAdviceInterceptor(MethodBeforeAdvice advice) {
            Assert.notNull(advice, "Advice must not be null");
            this.advice = advice;
        }
    
        public Object invoke(MethodInvocation mi) throws Throwable {
            this.advice.before(mi.getMethod(), mi.getArguments(), mi.getThis() );
            return mi.proceed();
        }
    }

      其中的屬性MethodBeforeAdvicce就代表着前置增強的AspectJMethodBeforeAdvice,跟蹤其before()方法:

    public void before(Method method, Object[] args, Object target) throws Throwable {
        invokeAdviceMethod(getJoinPointMatch(), null, null);
    }
    
    protected Object invokeAdviceMethod(JoinPointMatch jpMatch, Object returnValue, Throwable ex) throws Throwable {
        return invokeAdviceMethodWithGivenArgs(argBinding(getJoinPoint(), jpMatch, returnValue, ex));
    }
    
    protected Object invokeAdviceMethodWithGivenArgs(Object[] args) throws Throwable {
        Object[] actualArgs = args;
        if (this.aspectJAdviceMethod.getParameterTypes().length == 0) {
            actualArgs = null;
        }
        try {
            ReflectionUtils.makeAccessible(this.aspectJAdviceMethod);
            // 激活增強方法
            return this.aspectJAdviceMethod.invoke(this.aspectInstanceFactory.getAspectInstance(), actualArgs);
        }
        catch (IllegalArgumentException ex) {
            throw new AopInvocationException("Mismatch on arguments to advice method [" +
                    this.aspectJAdviceMethod + "]; pointcut expression [" +
                    this.pointcut.getPointcutExpression() + "]", ex);
        }
        catch (InvocationTargetException ex) {
            throw ex.getTargetException();
        }
    }

       invokeAdviceMethodWithGivenArgs方法中的aspectJAdviceMethod正是對應於前置增強的方法,在這裏實現了調用。

    AspectJAfterAdvice

      後置增強與前置增強有稍許不一致的地方。回顧上面講過的前置增強,大致的結構是在攔截器鏈中放置MethodBeforeAdviceInterceptor,並在MethodBeforeAdviceInterceptor中又放置了AspectJMethodBeforeAdvice,在調用inovke時首先串聯調用。但是在後置增強的時候卻是不一樣的,沒有提供如MethodBeforeAdviceInterceptor的中間類,而是直接實現MethodInterceptor接口,並在攔截器鏈中使用了中間的AspectAfterAdvice。

    public class AspectJAfterAdvice extends AbstractAspectJAdvice implements MethodInterceptor, AfterAdvice {
    
        public AspectJAfterAdvice(Method aspectJBeforeAdviceMethod, AspectJExpressionPointcut pointcut, AspectInstanceFactory aif) {
            super(aspectJBeforeAdviceMethod, pointcut, aif);
        }
    
        public Object invoke(MethodInvocation mi) throws Throwable {
            try {
                return mi.proceed();
            }
            finally {
                // 激活增強方法
                invokeAdviceMethod(getJoinPointMatch(), null, null);
            }
        }
    }

    2. 尋找匹配的增強器

      前面已經把所有增強器的獲取分析完了,但是對於所有增強來說,並不一定都適用於當前的Bean,還要挑選出適合的增強器,也就是滿足我們配置的通配符的增強器。這部分的具體實現在findAdvisorsThatCanApply中。

    protected List<Advisor> findAdvisorsThatCanApply(
            List<Advisor> candidateAdvisors, Class beanClass, String beanName) {
    
        ProxyCreationContext.setCurrentProxiedBeanName(beanName);
        try {
            // 過濾已經得到的advisors
            return AopUtils.findAdvisorsThatCanApply(candidateAdvisors, beanClass);
        }
        finally {
            ProxyCreationContext.setCurrentProxiedBeanName(null);
        }
    }
    
    public static List<Advisor> findAdvisorsThatCanApply(List<Advisor> candidateAdvisors, Class<?> clazz) {
        if (candidateAdvisors.isEmpty()) {
            return candidateAdvisors;
        }
        List<Advisor> eligibleAdvisors = new LinkedList<Advisor>();
        for (Advisor candidate : candidateAdvisors) {
            if (candidate instanceof IntroductionAdvisor && canApply(candidate, clazz)) {
                eligibleAdvisors.add(candidate);
            }
        }
        boolean hasIntroductions = !eligibleAdvisors.isEmpty();
        for (Advisor candidate : candidateAdvisors) {
            if (candidate instanceof IntroductionAdvisor) {
                // already processed
                continue;
            }
            if (canApply(candidate, clazz, hasIntroductions)) {
                eligibleAdvisors.add(candidate);
            }
        }
        return eligibleAdvisors;
    }

      findAdvisorsThatCanApply函數的主要功能是尋找所有增強器中適用於當前class的增強器,而對於真正的匹配其實是在canApply中實現的。

    public static boolean canApply(Advisor advisor, Class<?> targetClass, boolean hasIntroductions) {
        if (advisor instanceof IntroductionAdvisor) {
            return ((IntroductionAdvisor) advisor).getClassFilter().matches(targetClass);
        }
        else if (advisor instanceof PointcutAdvisor) {
            PointcutAdvisor pca = (PointcutAdvisor) advisor;
            return canApply(pca.getPointcut(), targetClass, hasIntroductions);
        }
        else {
            // It doesn't have a pointcut so we assume it applies.
            return true;
        }
    }
    
    public static boolean canApply(Pointcut pc, Class<?> targetClass, boolean hasIntroductions) {
        Assert.notNull(pc, "Pointcut must not be null");
        if (!pc.getClassFilter().matches(targetClass)) {
            return false;
        }
    
        MethodMatcher methodMatcher = pc.getMethodMatcher();
        IntroductionAwareMethodMatcher introductionAwareMethodMatcher = null;
        if (methodMatcher instanceof IntroductionAwareMethodMatcher) {
            introductionAwareMethodMatcher = (IntroductionAwareMethodMatcher) methodMatcher;
        }
    
        Set<Class> classes = new LinkedHashSet<Class>(ClassUtils.getAllInterfacesForClassAsSet(targetClass));
        classes.add(targetClass);
        for (Class<?> clazz : classes) {
            Method[] methods = clazz.getMethods();
            for (Method method : methods) {
                if ((introductionAwareMethodMatcher != null &&
                        introductionAwareMethodMatcher.matches(method, targetClass, hasIntroductions)) ||
                        methodMatcher.matches(method, targetClass)) {
                    return true;
                }
            }
        }
    
        return false;
    }

      這裏的邏輯也不是很複雜,無非就是根據advisor中封裝的classFilter來判斷是否match對應的類。

     3. 總結

      本文主要學習了Spring AOP核心實現原理中的對增強方法的獲取,主要包含兩個步驟:

    • 獲取所有增強;
    • 尋找所有增強中適用於bean的增強;

      下文會聚焦在AOP核心實現原理的另一部分–代理的創建。

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

    【其他文章推薦】

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

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

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

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

    ※產品缺大量曝光嗎?你需要的是一流包裝設計!

  • 一篇文章搞懂filebeat(ELK)

    一篇文章搞懂filebeat(ELK)

    本文使用的filebeat是7.7.0的版本
    本文從如下幾個方面說明:

    • filebeat是什麼,可以用來幹嘛
    • filebeat的原理是怎樣的,怎麼構成的
    • filebeat應該怎麼玩

    一、filebeat是什麼

    1.1、filebeat和beats的關係

      首先filebeat是Beats中的一員。
      Beats在是一個輕量級日誌採集器,其實Beats家族有6個成員,早期的ELK架構中使用Logstash收集、解析日誌,但是Logstash對內存、cpu、io等資源消耗比較高。相比Logstash,Beats所佔系統的CPU和內存幾乎可以忽略不計。
    目前Beats包含六種工具:

    • Packetbeat:網絡數據(收集網絡流量數據)
    • Metricbeat:指標(收集系統、進程和文件系統級別的CPU和內存使用情況等數據)
    • Filebeat:日誌文件(收集文件數據)
    • Winlogbeat:windows事件日誌(收集Windows事件日誌數據)
    • Auditbeat:審計數據(收集審計日誌)
    • Heartbeat:運行時間監控(收集系統運行時的數據)

    1.2、filebeat是什麼

      Filebeat是用於轉發和集中日誌數據的輕量級傳送工具。Filebeat監視您指定的日誌文件或位置,收集日誌事件,並將它們轉發到Elasticsearch或 Logstash進行索引。

      Filebeat的工作方式如下:啟動Filebeat時,它將啟動一個或多個輸入,這些輸入將在為日誌數據指定的位置中查找。對於Filebeat所找到的每個日誌,Filebeat都會啟動收集器。每個收集器都讀取單個日誌以獲取新內容,並將新日誌數據發送到libbeat,libbeat將聚集事件,並將聚集的數據發送到為Filebeat配置的輸出。

           工作的流程圖如下:

     

    1.3、filebeat和logstash的關係

      因為logstash是jvm跑的,資源消耗比較大,所以後來作者又用golang寫了一個功能較少但是資源消耗也小的輕量級的logstash-forwarder。不過作者只是一個人,加入http://elastic.co公司以後,因為es公司本身還收購了另一個開源項目packetbeat,而這個項目專門就是用golang的,有整個團隊,所以es公司乾脆把logstash-forwarder的開發工作也合併到同一個golang團隊來搞,於是新的項目就叫filebeat了。

     

    二、filebeat原理是什麼

    2.1、filebeat的構成

      filebeat結構:由兩個組件構成,分別是inputs(輸入)和harvesters(收集器),這些組件一起工作來跟蹤文件並將事件數據發送到您指定的輸出,harvester負責讀取單個文件的內容。harvester逐行讀取每個文件,並將內容發送到輸出。為每個文件啟動一個harvester。harvester負責打開和關閉文件,這意味着文件描述符在harvester運行時保持打開狀態。如果在收集文件時刪除或重命名文件,Filebeat將繼續讀取該文件。這樣做的副作用是,磁盤上的空間一直保留到harvester關閉。默認情況下,Filebeat保持文件打開,直到達到close_inactive

    關閉harvester可以會產生的結果:

    • 文件處理程序關閉,如果harvester仍在讀取文件時被刪除,則釋放底層資源。
    • 只有在scan_frequency結束之後,才會再次啟動文件的收集。
    • 如果該文件在harvester關閉時被移動或刪除,該文件的收集將不會繼續

      一個input負責管理harvesters和尋找所有來源讀取。如果input類型是log,則input將查找驅動器上與定義的路徑匹配的所有文件,併為每個文件啟動一個harvester。每個input在它自己的Go進程中運行,Filebeat當前支持多種輸入類型。每個輸入類型可以定義多次。日誌輸入檢查每個文件,以查看是否需要啟動harvester、是否已經在運行harvester或是否可以忽略該文件

    2.2、filebeat如何保存文件的狀態

      Filebeat保留每個文件的狀態,並經常將狀態刷新到磁盤中的註冊表文件中。該狀態用於記住harvester讀取的最後一個偏移量,並確保發送所有日誌行。如果無法訪問輸出(如Elasticsearch或Logstash),Filebeat將跟蹤最後發送的行,並在輸出再次可用時繼續讀取文件。當Filebeat運行時,每個輸入的狀態信息也保存在內存中。當Filebeat重新啟動時,來自註冊表文件的數據用於重建狀態,Filebeat在最後一個已知位置繼續每個harvester。對於每個輸入,Filebeat都會保留它找到的每個文件的狀態。由於文件可以重命名或移動,文件名和路徑不足以標識文件。對於每個文件,Filebeat存儲唯一的標識符,以檢測文件是否以前被捕獲。

    2.3、filebeat何如保證至少一次數據消費

      Filebeat保證事件將至少傳遞到配置的輸出一次,並且不會丟失數據。是因為它將每個事件的傳遞狀態存儲在註冊表文件中。在已定義的輸出被阻止且未確認所有事件的情況下,Filebeat將繼續嘗試發送事件,直到輸出確認已接收到事件為止。如果Filebeat在發送事件的過程中關閉,它不會等待輸出確認所有事件后再關閉。當Filebeat重新啟動時,將再次將Filebeat關閉前未確認的所有事件發送到輸出。這樣可以確保每個事件至少發送一次,但最終可能會有重複的事件發送到輸出。通過設置shutdown_timeout選項,可以將Filebeat配置為在關機前等待特定時間

    三、filebeat怎麼玩

    3.1、壓縮包方式安裝

    本文採用壓縮包的方式安裝,linux版本,filebeat-7.7.0-linux-x86_64.tar.gz

    curl-L-Ohttps://artifacts.elastic.co/downloads/beats/filebeat/filebeat-7.7.0-linux-x86_64.tar.gz
    tar -xzvf filebeat-7.7.0-linux-x86_64.tar.gz

    配置示例文件:filebeat.reference.yml(包含所有未過時的配置項)
    配置文件:filebeat.yml

    3.2、基本命令

    詳情見官網:https://www.elastic.co/guide/en/beats/filebeat/current/command-line-options.html

    export   #導出
    run      #執行(默認執行)
    test     #測試配置
    keystore #秘鑰存儲
    modules  #模塊配置管理
    setup    #設置初始環境

    例如:./filebeat test config  #用來測試配置文件是否正確

    3.3、輸入輸出

    支持的輸入組件:

    Multilinemessages,Azureeventhub,CloudFoundry,Container,Docker,GooglePub/Sub,HTTPJSON,Kafka,Log,MQTT,NetFlow,Office365ManagementActivityAPI,Redis,s3,Stdin,Syslog,TCP,UDP(最常用的額就是log)

    支持的輸出組件:

    Elasticsearch,Logstash,Kafka,Redis,File,Console,ElasticCloud,Changetheoutputcodec(最常用的就是Elasticsearch,Logstash)

    3.4、keystore的使用

    keystore主要是防止敏感信息被泄露,比如密碼等,像ES的密碼,這裏可以生成一個key為ES_PWD,值為es的password的一個對應關係,在使用es的密碼的時候就可以使用${ES_PWD}使用

    創建一個存儲密碼的keystore:filebeat keystore create
    然後往其中添加鍵值對,例如:filebeatk eystore add ES_PWD
    使用覆蓋原來鍵的值:filebeat key store add ES_PWD–force
    刪除鍵值對:filebeat key store remove ES_PWD
    查看已有的鍵值對:filebeat key store list

    例如:後期就可以通過${ES_PWD}使用其值,例如:

    output.elasticsearch.password:”${ES_PWD}”

    3.5、filebeat.yml配置(log輸入類型為例)

    詳情見官網:https://www.elastic.co/guide/en/beats/filebeat/current/filebeat-input-log.html

    type: log #input類型為log
    enable: true #表示是該log類型配置生效
    paths:     #指定要監控的日誌,目前按照Go語言的glob函數處理。沒有對配置目錄做遞歸處理,比如配置的如果是:
    - /var/log/* /*.log  #則只會去/var/log目錄的所有子目錄中尋找以".log"結尾的文件,而不會尋找/var/log目錄下以".log"結尾的文件。
    recursive_glob.enabled: #啟用全局遞歸模式,例如/foo/**包括/foo, /foo/*, /foo/*/* encoding:#指定被監控的文件的編碼類型,使用plain和utf-8都是可以處理中文日誌的
    exclude_lines: ['^DBG'] #不包含匹配正則的行
    include_lines: ['^ERR', '^WARN']  #包含匹配正則的行
    harvester_buffer_size: 16384 #每個harvester在獲取文件時使用的緩衝區的字節大小
    max_bytes: 10485760 #單個日誌消息可以擁有的最大字節數。max_bytes之後的所有字節都被丟棄而不發送。默認值為10MB (10485760)
    exclude_files: ['\.gz$']  #用於匹配希望Filebeat忽略的文件的正則表達式列表
    ingore_older: 0 #默認為0,表示禁用,可以配置2h,2m等,注意ignore_older必須大於close_inactive的值.表示忽略超過設置值未更新的
    文件或者文件從來沒有被harvester收集
    close_* #close_ *配置選項用於在特定標準或時間之後關閉harvester。 關閉harvester意味着關閉文件處理程序。 如果在harvester關閉
    後文件被更新,則在scan_frequency過後,文件將被重新拾取。 但是,如果在harvester關閉時移動或刪除文件,Filebeat將無法再次接收文件
    ,並且harvester未讀取的任何數據都將丟失。
    close_inactive  #啟動選項時,如果在制定時間沒有被讀取,將關閉文件句柄
    讀取的最後一條日誌定義為下一次讀取的起始點,而不是基於文件的修改時間
    如果關閉的文件發生變化,一個新的harverster將在scan_frequency運行后被啟動
    建議至少設置一個大於讀取日誌頻率的值,配置多個prospector來實現針對不同更新速度的日誌文件
    使用內部時間戳機制,來反映記錄日誌的讀取,每次讀取到最後一行日誌時開始倒計時使用2h 5m 來表示
    close_rename #當選項啟動,如果文件被重命名和移動,filebeat關閉文件的處理讀取
    close_removed #當選項啟動,文件被刪除時,filebeat關閉文件的處理讀取這個選項啟動后,必須啟動clean_removed
    close_eof #適合只寫一次日誌的文件,然後filebeat關閉文件的處理讀取
    close_timeout #當選項啟動時,filebeat會給每個harvester設置預定義時間,不管這個文件是否被讀取,達到設定時間后,將被關閉
    close_timeout 不能等於ignore_older,會導致文件更新時,不會被讀取如果output一直沒有輸出日誌事件,這個timeout是不會被啟動的,
    至少要要有一個事件發送,然後haverter將被關閉
    設置0 表示不啟動
    clean_inactived #從註冊表文件中刪除先前收穫的文件的狀態
    設置必須大於ignore_older+scan_frequency,以確保在文件仍在收集時沒有刪除任何狀態
    配置選項有助於減小註冊表文件的大小,特別是如果每天都生成大量的新文件
    此配置選項也可用於防止在Linux上重用inode的Filebeat問題
    clean_removed #啟動選項后,如果文件在磁盤上找不到,將從註冊表中清除filebeat
    如果關閉close removed 必須關閉clean removed
    scan_frequency #prospector檢查指定用於收穫的路徑中的新文件的頻率,默認10s
    tail_files:#如果設置為true,Filebeat從文件尾開始監控文件新增內容,把新增的每一行文件作為一個事件依次發送,
    而不是從文件開始處重新發送所有內容。
    symlinks:#符號鏈接選項允許Filebeat除常規文件外,可以收集符號鏈接。收集符號鏈接時,即使報告了符號鏈接的路徑,
    Filebeat也會打開並讀取原始文件。
    backoff: #backoff選項指定Filebeat如何积極地抓取新文件進行更新。默認1s,backoff選項定義Filebeat在達到EOF之後
    再次檢查文件之間等待的時間。
    max_backoff: #在達到EOF之後再次檢查文件之前Filebeat等待的最長時間
    backoff_factor: #指定backoff嘗試等待時間幾次,默認是2
    harvester_limit:#harvester_limit選項限制一個prospector并行啟動的harvester數量,直接影響文件打開數
    
    tags #列表中添加標籤,用過過濾,例如:tags: ["json"]
    fields #可選字段,選擇額外的字段進行輸出可以是標量值,元組,字典等嵌套類型
    默認在sub-dictionary位置
    filebeat.inputs:
    fields:
    app_id: query_engine_12
    fields_under_root #如果值為ture,那麼fields存儲在輸出文檔的頂級位置
    
    multiline.pattern #必須匹配的regexp模式
    multiline.negate #定義上面的模式匹配條件的動作是 否定的,默認是false
    假如模式匹配條件'^b',默認是false模式,表示講按照模式匹配進行匹配 將不是以b開頭的日誌行進行合併
    如果是true,表示將不以b開頭的日誌行進行合併
    multiline.match # 指定Filebeat如何將匹配行組合成事件,在之前或者之後,取決於上面所指定的negate
    multiline.max_lines #可以組合成一個事件的最大行數,超過將丟棄,默認500
    multiline.timeout #定義超時時間,如果開始一個新的事件在超時時間內沒有發現匹配,也將發送日誌,默認是5s
    max_procs #設置可以同時執行的最大CPU數。默認值為系統中可用的邏輯CPU的數量。
    name #為該filebeat指定名字,默認為主機的hostname
     

    3.6、實例一:logstash作為輸出

    filebeat.yml配置

    #=========================== Filebeat inputs =============================
    
    filebeat.inputs:
    
    # Each - is an input. Most options can be set at the input level, so
    # you can use different inputs for various configurations.
    # Below are the input specific configurations.
    
    - type: log
    
      # Change to true to enable this input configuration.
      enabled: true
    
      # Paths that should be crawled and fetched. Glob based paths.
      paths:  #配置多個日誌路徑
        - /var/logs/es_aaa_index_search_slowlog.log
        - /var/logs/es_bbb_index_search_slowlog.log
        - /var/logs/es_ccc_index_search_slowlog.log
        - /var/logs/es_ddd_index_search_slowlog.log
        #- c:\programdata\elasticsearch\logs\*
    
      # Exclude lines. A list of regular expressions to match. It drops the lines that are
      # matching any regular expression from the list.
      #exclude_lines: ['^DBG']
    
      # Include lines. A list of regular expressions to match. It exports the lines that are
      # matching any regular expression from the list.
      #include_lines: ['^ERR', '^WARN']
    
      # Exclude files. A list of regular expressions to match. Filebeat drops the files that
      # are matching any regular expression from the list. By default, no files are dropped.
      #exclude_files: ['.gz$']
    
      # Optional additional fields. These fields can be freely picked
      # to add additional information to the crawled log files for filtering
      #fields:
      #  level: debug
      #  review: 1
    
      ### Multiline options
    
      # Multiline can be used for log messages spanning multiple lines. This is common
      # for Java Stack Traces or C-Line Continuation
    
      # The regexp Pattern that has to be matched. The example pattern matches all lines starting with [
      #multiline.pattern: ^\[
    
      # Defines if the pattern set under pattern should be negated or not. Default is false.
      #multiline.negate: false
    
      # Match can be set to "after" or "before". It is used to define if lines should be append to a pattern
      # that was (not) matched before or after or as long as a pattern is not matched based on negate.
      # Note: After is the equivalent to previous and before is the equivalent to to next in Logstash
      #multiline.match: after
    
    
    #================================ Outputs =====================================
    
    #----------------------------- Logstash output --------------------------------
    output.logstash:
      # The Logstash hosts #配多個logstash使用負載均衡機制
      hosts: ["192.168.110.130:5044","192.168.110.131:5044","192.168.110.132:5044","192.168.110.133:5044"]  
      loadbalance: true  #使用了負載均衡
    
      # Optional SSL. By default is off.
      # List of root certificates for HTTPS server verifications
      #ssl.certificate_authorities: ["/etc/pki/root/ca.pem"]
    
      # Certificate for SSL client authentication
      #ssl.certificate: "/etc/pki/client/cert.pem"
    
      # Client Certificate Key
      #ssl.key: "/etc/pki/client/cert.key"

    ./filebeat -e   #啟動filebeat

    logstash的配置

    input {
      beats {
        port => 5044   
      }
    }
    
    output {
      elasticsearch {
        hosts => ["http://192.168.110.130:9200"] #這裏可以配置多個
        index => "query-%{yyyyMMdd}" 
      }
    }

     

    3.7、實例二:elasticsearch作為輸出

    filebeat.yml的配置:

    ###################### Filebeat Configuration Example #########################
    
    # This file is an example configuration file highlighting only the most common
    # options. The filebeat.reference.yml file from the same directory contains all the
    # supported options with more comments. You can use it as a reference.
    #
    # You can find the full configuration reference here:
    # https://www.elastic.co/guide/en/beats/filebeat/index.html
    
    # For more available modules and options, please see the filebeat.reference.yml sample
    # configuration file.
    
    #=========================== Filebeat inputs =============================
    
    filebeat.inputs:
    
    # Each - is an input. Most options can be set at the input level, so
    # you can use different inputs for various configurations.
    # Below are the input specific configurations.
    
    - type: log
    
      # Change to true to enable this input configuration.
      enabled: true
    
      # Paths that should be crawled and fetched. Glob based paths.
      paths:
        - /var/logs/es_aaa_index_search_slowlog.log
        - /var/logs/es_bbb_index_search_slowlog.log
        - /var/logs/es_ccc_index_search_slowlog.log
        - /var/logs/es_dddd_index_search_slowlog.log
        #- c:\programdata\elasticsearch\logs\*
    
      # Exclude lines. A list of regular expressions to match. It drops the lines that are
      # matching any regular expression from the list.
      #exclude_lines: ['^DBG']
    
      # Include lines. A list of regular expressions to match. It exports the lines that are
      # matching any regular expression from the list.
      #include_lines: ['^ERR', '^WARN']
    
      # Exclude files. A list of regular expressions to match. Filebeat drops the files that
      # are matching any regular expression from the list. By default, no files are dropped.
      #exclude_files: ['.gz$']
    
      # Optional additional fields. These fields can be freely picked
      # to add additional information to the crawled log files for filtering
      #fields:
      #  level: debug
      #  review: 1
    
      ### Multiline options
    
      # Multiline can be used for log messages spanning multiple lines. This is common
      # for Java Stack Traces or C-Line Continuation
    
      # The regexp Pattern that has to be matched. The example pattern matches all lines starting with [
      #multiline.pattern: ^\[
    
      # Defines if the pattern set under pattern should be negated or not. Default is false.
      #multiline.negate: false
    
      # Match can be set to "after" or "before". It is used to define if lines should be append to a pattern
      # that was (not) matched before or after or as long as a pattern is not matched based on negate.
      # Note: After is the equivalent to previous and before is the equivalent to to next in Logstash
      #multiline.match: after
    
    
    #============================= Filebeat modules ===============================
    
    filebeat.config.modules:
      # Glob pattern for configuration loading
      path: ${path.config}/modules.d/*.yml
    
      # Set to true to enable config reloading
      reload.enabled: false
    
      # Period on which files under path should be checked for changes
      #reload.period: 10s
    
    #==================== Elasticsearch template setting ==========================
    
    
    #================================ General =====================================
    
    # The name of the shipper that publishes the network data. It can be used to group
    # all the transactions sent by a single shipper in the web interface.
    name: filebeat222
    
    # The tags of the shipper are included in their own field with each
    # transaction published.
    #tags: ["service-X", "web-tier"]
    
    # Optional fields that you can specify to add additional information to the
    # output.
    #fields:
    #  env: staging
    
    #cloud.auth:
    
    #================================ Outputs =====================================
    
    
    #-------------------------- Elasticsearch output ------------------------------
    output.elasticsearch:
      # Array of hosts to connect to.
      hosts: ["192.168.110.130:9200","92.168.110.131:9200"]
    
      # Protocol - either `http` (default) or `https`.
      #protocol: "https"
    
      # Authentication credentials - either API key or username/password.
      #api_key: "id:api_key"
      username: "elastic"
      password: "${ES_PWD}"   #通過keystore設置密碼

    ./filebeat -e   #啟動filebeat

    查看elasticsearch集群,有一個默認的索引名字filebeat-%{[beat.version]}-%{+yyyy.MM.dd}

     

     

     

    3.8、filebeat模塊

    官網:https://www.elastic.co/guide/en/beats/filebeat/current/filebeat-modules.html

    這裏我使用elasticsearch模式來解析es的慢日誌查詢,操作步驟如下,其他的模塊操作也一樣:

    前提: 安裝好Elasticsearch和kibana兩個軟件,然後使用filebeat

    具體的操作官網有:https://www.elastic.co/guide/en/beats/filebeat/current/filebeat-modules-quickstart.html

    第一步,配置filebeat.yml文件

    #============================== Kibana =====================================
    
    # Starting with Beats version 6.0.0, the dashboards are loaded via the Kibana API.
    # This requires a Kibana endpoint configuration.
    setup.kibana:
    
      # Kibana Host
      # Scheme and port can be left out and will be set to the default (http and 5601)
      # In case you specify and additional path, the scheme is required: http://localhost:5601/path
      # IPv6 addresses should always be defined as: https://[2001:db8::1]:5601
      host: "192.168.110.130:5601"  #指定kibana
      username: "elastic"   #用戶
      password: "${ES_PWD}"  #密碼,這裏使用了keystore,防止明文密碼
    
      # Kibana Space ID
      # ID of the Kibana Space into which the dashboards should be loaded. By default,
      # the Default Space will be used.
      #space.id:
    
    #================================ Outputs =====================================
    
    # Configure what output to use when sending the data collected by the beat.
    
    #-------------------------- Elasticsearch output ------------------------------
    output.elasticsearch:
      # Array of hosts to connect to.
      hosts: ["192.168.110.130:9200","192.168.110.131:9200"]
    
      # Protocol - either `http` (default) or `https`.
      #protocol: "https"
    
      # Authentication credentials - either API key or username/password.
      #api_key: "id:api_key"
      username: "elastic"  #es的用戶
      password: "${ES_PWD}" # es的密碼
      #這裏不能指定index,因為我沒有配置模板,會自動生成一個名為filebeat-%{[beat.version]}-%{+yyyy.MM.dd}的索引

    第二步:配置elasticsearch的慢日誌路徑

    cd filebeat-7.7.0-linux-x86_64/modules.d
    

    vim  elasticsearch.yml

     

     

     

    第三步:生效es模塊

    ./filebeat modules elasticsearch

    查看生效的模塊

    ./filebeat modules list

     

     

     

    第四步:初始化環境

    ./filebeat setup -e

     

     

     

     

     第五步:啟動filebeat

    ./filebeat -e

    查看elasticsearch集群,如下圖所示,把慢日誌查詢的日誌都自動解析出來了:

     

     到這裏,elasticsearch這個module就實驗成功了

     

    參考

    官網:https://www.elastic.co/guide/en/beats/filebeat/current/index.html

     

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

    【其他文章推薦】

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

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

    ※超省錢租車方案

    ※教你寫出一流的銷售文案?

    網頁設計最專業,超強功能平台可客製化

    ※產品缺大量曝光嗎?你需要的是一流包裝設計!

  • 如何獲取Apollo上項目下的所有namespace?

    如何獲取Apollo上項目下的所有namespace?

    背景

    項目配置遷移到Apollo之後,通過統一的配置管理及配置監聽使得項目配置修改的成本大大降低。

    但是,在使用Apollo的過程中,強哥也遇到一個問題:如果我們要獲取Apollo下的namespace信息需要通過ConfigServer.getConfig(String namespace)方法來獲取,但是使用這個方法的前提是我們必須知道當前項目下有哪些namespace,或者說我們只能使用我們已知的namespace。這就對我們的代碼擴展性產生了限制,假如項目已經上線,而之後我們又要新增namespace或者修改已有namespace名稱,就必須更改代碼將對應的namespace加入或修改,然後重新發布。

    雖然我們不會經常修改namespace,但是,有這麼一個痛點,就讓人很不舒服。而且從官方文檔中,強哥“並沒有”找到:通過項目app_id獲取到Apollo上對應的該項目下的所有namespace的方法。

    那麼這個問題要怎麼解決呢?強哥今天就帶大家通過Apollo源碼來看看如何找到解決思路。

    入手點

    按常理出牌,我們先在Google中搜索一下我們的問題(這裏提一下,別用百度,他么的根本定位不到要搜的點):

    第一條搜索結果點進去看看,是其他開發者在github上提的issue:

    我們可以看到,作者的回復是:通過open api來獲取所有namespace。也就是官方文檔中的這塊內容:

    額,這個……其實,官方文檔中是有提到如何獲取項目下的所有namespace的方法的,那麼強哥上面為什麼說沒有找到呢?這不是啪啪啪打臉嗎?

    強哥這麼說是因為官網提供的方式比較雞肋。我們可以看到,需要獲取項目下所有的namespace,需要接入Apollo開放平台。操作步驟如下:

    註冊第三方應用
    給已註冊的第三方應用授權
    第三方應用通過獲取的Token調用Apollo Open API
    這尼瑪,坑爹啊,這麼麻煩,還要註冊授權拿Token才能搞,這對於強哥這種懶人來說簡直沒法接受。

    Token是不可能用Token的,這輩子都不會用Token來獲取這玩意的。於是,從官方提供的Api來看是沒法了,只能另謀出路啦。

    追根溯源

    雖然官方文檔中沒有直接提供解決問題的方法,可是我們從提供的開放平台API倒是也可以發現一些信息:

    根據官網配置后調用如下:

    發現確實可以獲取到項目下的所有namespace信息,可是,信息有點太多了,將namespace下的配置也都返回了回來,而且請求中不加入Authorization屬性的Token信息,調用會返回401沒有權限。果然強扭的瓜不甜。

    那麼我們怎麼從上面的信息找突破點呢?沒錯,如果有強哥一樣思路的同學,應該會想到:既然開放平台提供了調用接口,那麼我們就去源碼里看看這個接口的具體實現,沒準能夠有所收穫呢!

    從上圖中我們可以看到,接口地址是:http://{portal_address},那麼源碼就從apollo-portal入手啦:

    直接進到Controller目錄下(別問我為什麼知道是這個目錄,有點基礎的點開項目自然就會這麼去找了):

    可以定位到我們調用的開放平台的方法是這個:

    代碼很簡單,可以看到,獲取namespace走的是namespaceService.findNamespaceBOs()方法,進去實現看看(這裏為github點個贊,點擊方法能夠直接跳轉到對應的實現,真的是方便):

    第一行就獲取了namespace:
    namespaceAPI.findNamespaceByCluster(appId, env, clusterName);
    進去看看:

    吼吼,原來走的也是api調用,可是,這個api的服務地址是哪裡呢?這就要小夥伴們對Apollo的架構有點熟悉了,上大圖:

    我們調用的接口是Portal進去的,而底層走的是Admin Service,所以,上面代碼的restTemplate調用走的就是apollo-adminservice項目啦,話不多說,進apollo-adminservice看看:

    其實到這裏已經差不多了,因為再往細的研究已經沒有了意義。我們已經可以通過調用上圖提供的Api來獲取到我們需要的內容了,試一下:

    試驗發現,確實是可以獲取到項目下的所有namespace,且不需要註冊第三方平台應用,也不需要在調用接口時傳遞Authorization參數,返回的結果也剛好是簡單的所有namespace信息。完美的解決了我們的問題。

    當然有些小夥伴可能會說,這樣還是要調用http接口,還是有點不方便。強哥只想說,自己本地封裝一個方法,獲取應該還是比較簡單的。而且,Apollo Client提供給我們的Api,比如:ConfigService.getConfig(String namespace)其實底層也是走的socket網絡調用,只是client為我們做了一層封裝對用戶屏蔽了而已,同時還額外加入了緩存機制來提高效率。

    當然,你也可以自己下載apollo-client的源碼,然後在裏面封裝調用這個api的邏輯,然後maven部署到自己的私服,這樣就能和其他Api一樣調用啦!不過太麻煩了,強哥就不帶大家試了。

    總結

    先總結一下解決方法:
    直接越過portal,調用底層admin-service的api
    http://{adminservice}/apps/{appId}/clusters/{clusterName}/namespaces
    {adminservice}這個地址根據自己項目配置的地址及端口去設置哦,默認端口8090~

    其實,我們發現,對於開源項目,很多東西只要我們願意去找,還是能找到解決的思路的。不過,首先還是要了解其架構原理先,否則在查找源碼的過程中,可能會無從下手。

    就拿為什麼強哥上面會知道apollo-client獲取namespace信息的時候有使用了緩存機制呢?因為強哥當時找這個問題的解決方法時,也簡單的研究了下client的源碼,想要看看官方是否有提供對應的Api,結果沒有找到,但是也對apollo-client的部分實現有所熟悉。所以,有時候,走一些“該走的彎路”也不是壞事。

    希望這篇文章對大家有用,好啦,今天就到這~
    關注公眾號獲取更多內容,有問題也可在公眾號提問哦:強哥叨逼叨

    叨逼叨編程、互聯網的見解和新鮮事

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

    【其他文章推薦】

    ※為什麼 USB CONNECTOR 是電子產業重要的元件?

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

    ※台北網頁設計公司全省服務真心推薦

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

    ※推薦評價好的iphone維修中心

  • 微服務技術棧:常見註冊中心組件,對比分析

    微服務技術棧:常見註冊中心組件,對比分析

    本文源碼:GitHub·點這裏 || GitEE·點這裏

    一、註冊中心簡介

    1、基礎概念

    在分佈式架構的系統中註冊中心這個概念就已經被提出了,最經典的就是Zookeeper中間件。

    微服務架構中,註冊中心是最核心的基礎服務之一,註冊中心可以看做是微服務架構中的通信中心,當一個服務去請求另一個服務時,通過註冊中心可以獲取該服務的狀態,地址等核心信息。

    服務註冊主要關係到三大角色:服務提供者、服務消費者、註冊中心。

    2、流程和原理

    基礎流程

    • 服務啟動時,將自身的網絡地址等信息註冊到註冊中心,註冊中心記錄服務註冊數據。
    • 服務消費者從註冊中心獲取服務提供者的地址,並通過地址和基於特定的方式調用服務提供者的接口。
    • 各個服務與註冊中心使用一定機制通信。如果註冊中心與服務長時間無法通信,就會註銷該實例,這也稱為服務下線,當服務重新連接之後,會基於一定的策略在線上線。
    • 服務地址相關信息發生變化時,會重新註冊到註冊中心。這樣,服務消費者就無需手工維護提供者的相關配置。

    核心功能

    通過上面的基本流程,不難發現一個註冊中心需要具備哪些核心功能:

    • 服務發現

    服務發現是指服務在啟動后,註冊到註冊中心,服務方提供自身的元數據,比如IP地址、端口、運行狀況指標的Uri 、主頁地址等信息。

    • 服務記錄

    記錄註冊中心的服務的信息,例如服務名稱、IP地址、端口等。服務消費方基於查詢獲取可用的服務實例列表。

    • 動態管理服務

    註冊中心基於特定的機制定時測試已註冊的服務,例如:默認的情況下會每隔30秒發送一次心跳來進行服務續約。通過服務續約來告知Server該Client仍然可用。正常情況下,如果Server在90 秒內沒有收到Client 的心跳,Server會將Client 實例從註冊列表中刪除。

    二、基礎組件對比

    1、Zookeeper組件

    1.1基礎描述

    ZooKeeper是非常經典的服務註冊中心中間件,在國內環境下,由於受到Dubbo框架的影響,大部分情況下認為Zookeeper是RPC服務框架下註冊中心最好選擇,隨着Dubbo框架的不斷開發優化,和各種註冊中心組件的誕生,即使是RPC框架,現在的註冊中心也逐步放棄了ZooKeeper。在常用的開發集群環境中,ZooKeeper依然起到十分重要的作用,Java體系中,大部分的集群環境都是依賴ZooKeeper管理服務的各個節點。

    1.2組件特點

    從Zookeeper的數據結構特點看,並不是基於服務註冊而設計的,ZooKeeper提供的命名空間與文件系統的名稱空間非常相似,在數據結構上高度抽象為K-V格式,十分通用,說到這裏不得不提一下Redis,也可以作為註冊中心使用,只是用的不多。

    ZooKeeper組件支持節點短暫存在,只要創建znode的會話處於活動狀態,這些znode就會存在,會話結束時,將刪除znode。Dubbo框架正是基於這個特點,服務啟動往Zookeeper註冊的就是臨時節點,需要定時發心跳到Zookeeper來續約節點,並允許服務下線時,將Zookeeper上相應的節點刪除,同時Zookeeper使用ZAB協議雖然保證了數據的強一致性。

    2、Eureka組件

    2.1基礎描述

    SpringCloud框架生態中最原生的深度結合組件,Eureka是Netflix開發的服務發現框架,基於REST的服務,主要用於服務註冊,管理,負載均衡和服務故障轉移。但是官方聲明在Eureka2.0版本停止維護,不建議使用。

    2.2組件特點

    Eureka包含兩個組件:EurekaServer和EurekaClient。

    EurekaServer提供服務註冊服務,各個節點啟動后,會在EurekaServer中進行註冊,這樣EurekaServer中的服務註冊表中將會存儲所有可用服務節點的信息,服務節點的信息可以在界面中直觀的看到。Eureka允許在註冊服務的時候,自定義實現檢查自身狀態的是否健康的方法,這在服務實例能夠保持心跳上報的場景下,是一種比較好的體驗。

    EurekaClient是一個java客戶端,用於簡化與EurekaServer的交互,客戶端同時也就是一個內置的、使用輪詢(round-robin)負載算法的負載均衡器。

    3、Consul組件

    3.1基礎描述

    Consul是用於服務發現和配置的工具。Consul是分佈式的,高度可用的,並且具有極高的可伸縮性,而且開發使用都很簡便。它提供了一個功能齊全的控制面板,主要特點是:服務發現、健康檢查、鍵值存儲、安全服務通信、多數據中心、ServiceMesh。Consul在設計上把很多分佈式服務治理上要用到的功能都包含在內了。

    3.2組件特點

    Consul提供多個數據中心的支持,基於Fabio做負載均衡,每個數據中心內,都有客戶端和服務端的混合構成。預計有三到五台服務端。可以在失敗和性能的可用性之間取得良好的平衡。數據中心中的所有節點都參与八卦協議。這意味着有一個八卦池,其中包含給定數據中心的所有節點。這有幾個目的:首先,不需要為客戶端配置服務器的地址;發現是自動完成的。其次,檢測節點故障的工作不是放在服務器上,而是分佈式的。這使得故障檢測比天真的心跳方案更具可擴展性。第三,它被用作消息傳遞層,用於在諸如領導者選舉等重要事件發生時進行通知。

    4、Nacos組件

    4.1基礎描述

    Nacos致力於發現、配置和管理微服務。Nacos提供了一組簡單易用的特性集,幫助您實現動態服務發現、服務配置管理、服務及流量管理。Nacos更敏捷和容易地構建、交付和管理微服務平台。 Nacos 是構建以“服務”為中心的現代應用架構(例如微服務範式、雲原生範式)的服務基礎設施。Nacos支持作為RPC註冊中心,例如:支持Dubbo框架;也具備微服務註冊中心的能力,例如:SpringCloud框架。

    4.2組件特點

    Nacos在經過多年生產經驗后提煉出的數據模型,則是一種服務-集群-實例的三層模型。如上文所說,這樣基本可以滿足服務在所有場景下的數據存儲和管理,數據模型雖然相對複雜,但是並不強制使用數據結構的風格,大多數應用場景下,和Eureka數據模型是類似的。

    Nacos提供數據邏輯隔離模型,用戶賬號可以新建多個命名空間,每個命名空間對應一個客戶端實例,這個命名空間對應的註冊中心物理集群是可以根據規則進行路由的,這樣可以讓註冊中心內部的升級和遷移對用戶是無感知的。

    三、組件選擇

    如下註冊中心對比圖。

    綜合上述幾種註冊中心對比,再從現在SpringCloud框架流行趨勢看,個人推薦後續微服務架構體系選擇Nacos組件,大致原因如下,社區活躍,經過大規模業務驗證,不但可以作為微服務註冊中心,也支持作RPC框架Dubbo的註冊中心,且有完善的中文文檔,總結下來就一句話:通用中間件,省時;文檔詳細,省心。

    四、源代碼地址

    GitHub·地址
    https://github.com/cicadasmile/husky-spring-cloud
    GitEE·地址
    https://gitee.com/cicadasmile/husky-spring-cloud
    

    推薦文章:微服務基礎系列

    序號 文章標題
    01 微服務基礎:Eureka組件,管理服務註冊發現
    02 微服務基礎:Ribbon和Feign組件,實現請求負載均衡
    03 微服務基礎:Hystrix組件,實現服務熔斷
    04 微服務基礎:Turbine組件,實現微服務集群監控
    05 微服務基礎:Zuul組件,實現路由網關控制
    06 微服務基礎:Config組件,實現配置統一管理
    07 微服務基礎:Zipkin組件,實現請求鏈路追蹤
    08 微服務基礎:與Dubbo框架、Boot框架對比分析
    09 微服務基礎:Nacos組件,服務和配置管理
    10 微服務基礎:Sentinel組件,服務限流和降級
    11 微服務應用:分庫分表模式下,數據庫擴容方案
    12 微服務應用:Shard-Jdbc分庫分表,擴容方案實現

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

    【其他文章推薦】

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

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

    ※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整

    南投搬家公司費用,距離,噸數怎麼算?達人教你簡易估價知識!

    ※教你寫出一流的銷售文案?

    ※超省錢租車方案

  • 您的單例模式,真的單例嗎?

    您的單例模式,真的單例嗎?

          單例模式,大家恐怕再熟悉不過了,其作用與實現方式有多種,這裏就不啰嗦了。但是,咱們在使用這些方式實現單例模式時,程序中就真的會只有一個實例嗎?

          聰明的你看到這樣的問話,一定猜到了答案是NO。這裏筆者就不賣關子了,開門見山吧!實際上,在有些場景下,如果程序處理不當,會無情地破壞掉單例模式,導致程序中出現多個實例對象。

          下面筆者介紹筆者已知的三種破壞單例模式的方式以及避免方法。

    1、反射對單例模式的破壞

          我們先通過一個例子,來直觀感受一下

        (1)案例

      DCL實現的單例模式:

     1 public class Singleton{
     2     private static volatile Singleton mInstance;
     3     private Singleton(){}
     4     public static Singleton getInstance(){
     5         if(mInstance == null){
     6             synchronized (Singleton.class) {
     7                 if(mInstance == null){
     8                     mInstance = new Singleton();
     9                 }
    10             }
    11         }
    12         return mInstance;
    13     }
    14 }

      測試代碼:

     1 public class SingletonDemo {
     2 
     3     public static void main(String[] args){
     4         Singleton singleton = Singleton.getInstance();
     5         try {
     6             Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();
     7             constructor.setAccessible(true);
     8             Singleton reflectSingleton = constructor.newInstance();
     9             System.out.println(reflectSingleton == singleton);
    10         } catch (Exception e) {
    11             // TODO Auto-generated catch block
    12             e.printStackTrace();
    13         }
    14     }
    15 }

      執行結果:

    false

          運行結果說明,採用反射的方式另闢蹊徑實例了該類,導致程序中會存在不止一個實例。

        (2)解決方案

          其思想就是採用一個全局變量,來標記是否已經實例化過了,如果已經實例化過了,第二次實例化的時候,拋出異常。實現代碼如下:

     1 public class Singleton{
     2     private static volatile Singleton mInstance;
     3     private static volatile boolean mIsInstantiated = false;
     4     private Singleton(){
     5         if (mIsInstantiated){
     6             throw new RuntimeException("Has been instantiated, can not do it again!");
     7         }
     8         mIsInstantiated = true;
     9     }
    10     public static Singleton getInstance(){
    11         if(mInstance == null){
    12             synchronized (Singleton.class) {
    13                 if(mInstance == null){
    14                     mInstance = new Singleton();
    15                 }
    16             }
    17         }
    18         return mInstance;
    19     }
    20 }

    執行結果:

     

         這種方式看起來比較暴力,運行時直接拋出異常。

     

    2、clone()對單例模式的破壞 

           當需要實現單例的類允許clone()時,如果處理不當,也會導致程序中出現不止一個實例。

        (1)案例

      一個實現了Cloneable接口單例類:

     1 public class Singleton implements Cloneable{
     2     private static volatile Singleton mInstance;
     3     private Singleton(){
     4     }
     5     public static Singleton getInstance(){
     6         if(mInstance == null){
     7             synchronized (Singleton.class) {
     8                 if(mInstance == null){
     9                     mInstance = new Singleton();
    10                 }
    11             }
    12         }
    13         return mInstance;
    14     }
    15     @Override
    16     protected Object clone() throws CloneNotSupportedException {
    17         // TODO Auto-generated method stub
    18         return super.clone();
    19     }
    20 }

      測試代碼:

     1 public class SingletonDemo {
     2 
     3     public static void main(String[] args){
     4         try {
     5             Singleton singleton = Singleton.getInstance();
     6             Singleton cloneSingleton;
     7             cloneSingleton = (Singleton) Singleton.getInstance().clone();
     8             System.out.println(cloneSingleton == singleton);
     9         } catch (CloneNotSupportedException e) {
    10             e.printStackTrace();
    11         }
    12     }
    13 }

    執行結果:

    false

      (2)解決方案:

         解決思想是,重寫clone()方法,調clone()時直接返回已經實例的對象

     1 public class Singleton implements Cloneable{
     2     private static volatile Singleton mInstance;
     3     private Singleton(){
     4     }
     5     public static Singleton getInstance(){
     6         if(mInstance == null){
     7             synchronized (Singleton.class) {
     8                 if(mInstance == null){
     9                     mInstance = new Singleton();
    10                 }
    11             }
    12         }
    13         return mInstance;
    14     }
    15     @Override
    16     protected Object clone() throws CloneNotSupportedException {
    17         return mInstance;
    18     }
    19 }

    執行結果:

    true

     

    3、序列化對單例模式的破壞

       在使用序列化/反序列化時,也會出現產生新實例對象的情況。

      (1)案例

          一個實現了序列化接口的單例類:

     1 public class Singleton implements Serializable{
     2     private static volatile Singleton mInstance;
     3     private Singleton(){
     4     }
     5     public static Singleton getInstance(){
     6         if(mInstance == null){
     7             synchronized (Singleton.class) {
     8                 if(mInstance == null){
     9                     mInstance = new Singleton();
    10                 }
    11             }
    12         }
    13         return mInstance;
    14     }
    15 }

        測試代碼:

     1 public class SingletonDemo {
     2 
     3     public static void main(String[] args){
     4         try {
     5             Singleton singleton = Singleton.getInstance();
     6             FileOutputStream fos = new FileOutputStream("singleton.txt");
     7             ObjectOutputStream oos = new ObjectOutputStream(fos);
     8             oos.writeObject(singleton);
     9             oos.close();
    10             fos.close();
    11 
    12             FileInputStream fis = new FileInputStream("singleton.txt");
    13             ObjectInputStream ois = new ObjectInputStream(fis);
    14             Singleton serializedSingleton = (Singleton) ois.readObject();
    15             fis.close();
    16             ois.close();
    17             System.out.println(serializedSingleton==singleton);
    18         } catch (Exception e) {
    19             e.printStackTrace();
    20         }
    21 
    22     }
    23 }

         運行結果:

    false

        (2)解決方案

        在反序列化時的回調方法 readResolve()中返回單例對象。

     1 public class Singleton implements Serializable{
     2     private static volatile Singleton mInstance;
     3     private Singleton(){
     4     }
     5     public static Singleton getInstance(){
     6         if(mInstance == null){
     7             synchronized (Singleton.class) {
     8                 if(mInstance == null){
     9                     mInstance = new Singleton();
    10                 }
    11             }
    12         }
    13         return mInstance;
    14     }
    15 
    16     protected Object readResolve() throws ObjectStreamException{
    17         return mInstance;
    18     }
    19 }

    結果:

    true

     

           以上就是筆者目前已知的三種可以破壞單例模式的場景以及對應的解決辦法,讀者如果知道還有其他的場景,記得一定要分享出來噢,正所謂“獨樂樂不如眾樂樂”!!!

           單例模式看起來是設計模式中最簡單的一個,但“麻雀雖小,五臟俱全”,其中有很多細節都是值得深究的。即便是本篇介紹的這幾個場景,也只是介紹了一些梗概而已,很多細節還需要讀者自己去試驗和推敲的,比如:通過枚舉方式實現單例模式,就不存在上述問題,而其它的實現方式似乎都存在上述問題!

     

           後記

           本篇參(剽)考(竊)了如下資料:

           高洪岩的《Java 多線程編程核心技術》

           博文:https://blog.csdn.net/fd2025/article/details/79711198

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

    【其他文章推薦】

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

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

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

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

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

    網頁設計最專業,超強功能平台可客製化

  • 月銷30000多輛,百公里6L油,這款10萬級家轎有這麼好?

    月銷30000多輛,百公里6L油,這款10萬級家轎有這麼好?

    在油耗方面,日產軒逸提供了1。6L以及1。8L兩款動力總成供消費者選擇,與之匹配的是一台CVT變速箱。而作為主銷車型的1。6L版本整體的動力表現並不會太過出色,但好在動力輸出平順,而且油耗水平做得相當出色,也是各大滴滴車主當中最火爆的車型之一。

    在10多萬的這個價位區間中,無論是合資品牌還是國產品牌都有着數十款車型供你選擇,而對於絕大多數消費者而言,都是希望能買到一台價格便宜、用得放心的車型,於是你會發現不少人都變得十分苦惱,究竟自己看上的車型到底值不值得購買呢?成為了當前最希望解決的一個難題。

    帶着問題,今天咱們先來討論一下日系三強當中熱度相當高的日產軒逸,究竟這一款車型的表現怎麼樣呢?

    在造型方面相信大家對其也是相當熟悉,也是採用當下最常規的日產家族式設計語言,造型設計較為年輕,符合到當下消費者的審美,而且每月均保持數萬台的銷量(3月份37672輛)也足以證明它受到眾多消費者所熱捧。

    有着沙發廠之稱的日產品牌,在軒逸上也做得相當出色,2700mm的軸距表現也賦予了它大空間舒適家用的亮點,而且車型本身也沒有明顯的短板,也是一款能夠滿足到各種家庭需求的車型。

    雖然很多人都認為合資車型總會出現價格高配置低的情況,而實際上隨着車型的更新換代,你會發現其實原本配置簡陋的合資車型已經變得更具競爭力;

    在配置水平方面日產軒逸雖說沒有過於突出的地方,在低配版本上配置可以說是相當低,所以也是不值得推薦的。但在主銷的中高配版本,豐富的配置水平也讓其性價比極高;而且在高配版本上頁新增加了併線輔助、車道偏離系統、主動剎車等科技含量比較高的配置,也是這個價位當中極其少有的存在。

    在油耗方面,日產軒逸提供了1.6L以及1.8L兩款動力總成供消費者選擇,與之匹配的是一台CVT變速箱。而作為主銷車型的1.6L版本整體的動力表現並不會太過出色,但好在動力輸出平順,而且油耗水平做得相當出色,也是各大滴滴車主當中最火爆的車型之一。

    而在保養方面,日系車最大的優勢便是保養費用不高,而且車型的耐久性相當高,開着不壞、用起來相當省心。

    優點:車型造型大氣、油耗出色、儲物和乘坐空間大,很適合家用

    缺點:沒有後排出風口、沒有標配倒車雷達、動力和操控較弱

    就像上文所說,軒逸更推薦考慮車價為13.78萬的1.6L尊享版或以上的車型版本,而車型本身的價格並不算太高,但是軒逸也是擁有着一個相當不錯的現金優惠,也幾乎可以用原車價去購車。

    關於軒逸而言,其實多年在市場上打滾也讓它在消費者心目當中有着非常不錯的口碑,舒適家用也是它的優勢之處,而車型本身並沒有明顯的短板,雖然表現平平但不失為一台及格的家用轎車。本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

    【其他文章推薦】

    USB CONNECTOR掌控什麼技術要點? 帶您認識其相關發展及效能

    台北網頁設計公司這麼多該如何選擇?

    ※智慧手機時代的來臨,RWD網頁設計為架站首選

    ※評比南投搬家公司費用收費行情懶人包大公開

    ※回頭車貨運收費標準

  • 同級最便宜,這款20多萬的超帥氣SUV車主卻說……

    同級最便宜,這款20多萬的超帥氣SUV車主卻說……

    0L混動最大馬力(pS):182/發動機:146 電動機:184最大扭矩(Nm):240/發動機:175 電動機:315變速箱:8DCT/E-CVT百公里加速(s):8。35/8。6百公里油耗(L):6。8/5車主百公里油耗(L):8。71/暫

    謳歌CDX肩負着在國內為謳歌品牌打開市場的重任,它具備極其前衛的設計風格,而且看到實車時你就會感受到其設計的精緻感!

    寬敞的空間,前衛設計讓它具有不錯的競爭力。而且它搭載一款1.5T高功率發動機,匹配一副8擋雙離合變速箱,動力系統匹配是高效的,它換擋平順,降擋的速度也相當快,動力表現可圈可點!下面我們就來看看這款豪華SUV車型的各方面表現怎麼樣吧!

    長寬高:4496*1840*1615mm

    軸距:2660mm

    定位:緊湊型SUV

    設計激進的謳歌CDX可謂是同級車型中的一股清流,前臉造型極其激進,鑽石形中網造型顯得相當有個性。同時車身線條緊湊,但車內卻能營造出足夠的乘坐空間。

    內飾方面它在細節處富有創意,造型類似“如意”的旋鈕擋把設計科技感強,而內飾的用料也是到位的。值得一提的是這套內飾的按鍵布局簡約、合理,方向盤上的按鍵也具備不錯手感,在行車過程中操作便利!

    發動機:1.5T/2.0L混動

    最大馬力(pS):182/發動機:146 電動機:184

    最大扭矩(Nm):240/發動機:175 電動機:315

    變速箱:8DCT/E-CVT

    百公里加速(s):8.35/8.6

    百公里油耗(L):6.8/5

    車主百公里油耗(L):8.71/暫本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

    【其他文章推薦】

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

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

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

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

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

    網頁設計最專業,超強功能平台可客製化

  • 都是30萬德系轎車,頂配邁騰和奧迪A4L到底差多少?

    都是30萬德系轎車,頂配邁騰和奧迪A4L到底差多少?

    其實邁騰多出來的配置不止這些,還有二十多項,全部寫出來太長,故略去。低配的奧迪A4L採用了低功率的2。0T發動機,最大馬力是190匹,最大扭矩是320牛•米。頂配的邁騰使用的2。0T發動機賬面數據更好,220匹的最大馬力和350牛•米的最大扭矩均優於A4L。

    隨着平台化,規模化生產的普及,很多大集團都會在自己的普通品牌和豪華品牌上通用零件,但是定價卻差了幾萬,那麼這幾萬的差價真的如大家所說,是LOGO的差異嗎?同一個集團,應該買TA的普通品牌高配車型還是豪華品牌的低配車型比較划算?

    今天,我們來對比一下大眾邁騰的380TSI 旗艦版和奧迪A4L的40TFSI 進取版,看看哪個更值得買。

    邁騰和奧迪A4L的外觀是同一個風格的兩份答卷。邁騰的橫貫式前臉把大燈連接起來,從視覺上來看顯得更寬更低矮。今天對比的邁騰380TSI 旗艦版配備的是18寸輪轂,所以邁騰的側面看上去更加大氣一些。

    奧迪A4L則是在原有的六邊形格柵的家族設計元素上再下功夫,把大燈、格柵、車身線條設計得再銳利一些,所以奧迪A4L的外觀顯得更加年輕。不過這17寸的輪轂嘛,說實話不是很能襯托出A4L的檔次,建議後期更換。

    這一局,邁騰略勝。

    這一代的邁騰是歐版帕薩特直接拿來生產的,所以這一代邁騰的風格反而是繼承了上一代帕薩特的商務風。在邁騰上,木紋飾板貫穿整个中控台,空調出風口被設計得更高,所以空間更加寬闊。奧迪A4L的內飾以科技感為主要設計導向,奧迪A4L 30TSI 進取版採用中控台採用了銀色拉絲面板,中控屏採用懸浮式設計。可惜的是低配車沒有使用全液晶显示屏,空調面板也不是高配的那款金屬質感控制面板。所以質感稍顯不足。

    這一局,也是邁騰勝出。

    無論是邁騰還是奧迪A4L,引進國內后,都進行了不同程度的加長,邁騰的軸距為2871mm,比海外版增加了80mm,奧迪A4L的軸距是2908mm,比海外版增加了88mm。雖然奧迪A4L的軸距比邁騰要長一點,但是乘坐空間方面,邁騰比奧迪A4L要寬敞一點。這是因為邁騰是MQB(發動機橫置平台)的產物,而奧迪A4L是MLB(發動機縱置平台)的產物。發動機橫置的結構比較緊湊,占乘坐空間較少,所以邁騰的乘坐空間比奧迪A4L稍微大一點。

    這一局,因為兩者的差距微乎其微,所以判定兩者打成平手。

    在配置上,邁騰完勝沒什麼懸念。其實邁騰多出來的配置不止這些,還有二十多項,全部寫出來太長,故略去。

    低配的奧迪A4L採用了低功率的2.0T發動機,最大馬力是190匹,最大扭矩是320牛•米。頂配的邁騰使用的2.0T發動機賬面數據更好,220匹的最大馬力和350牛•米的最大扭矩均優於A4L。從加速成績來看,邁騰比奧迪A4L更好,但也只是半秒的差距。奧迪A4L的發動機有AVS可變氣門升程技術和混合噴射技術。這兩項技術都可以提高燃油經濟性,所以從用戶反饋的數據來看,奧迪A4L比邁騰節油不少。

    所以這局,也只能判五五開。

    說到舒適性方面奧迪A4L的前後懸挂都使用了五連桿獨立懸挂,而邁騰則採用了前麥弗遜獨立懸挂后多連桿獨立懸挂的組合。這不僅是因為成本,還因為邁騰使用的MQB佔據了機艙空間,所以無法布局多連桿懸挂。麥弗遜式獨立懸挂有個缺點,那就是上下運動的時候對車輪外傾角變化明顯,而車輪外傾角變化會影響操縱的穩定性。所以,為了安全性考慮,麥弗遜懸挂一般都會比較硬。而使用五連桿的奧迪A4L可以把懸挂調得軟一些,所以奧迪A4L的濾震更好。在加上在隔音材料方面,豪華品牌比普通品牌的投入會更大。

    所以舒適性方面,奧迪A4L完勝邁騰是沒什麼懸念的。

    總結:縱觀兩車各個方面的對比,互有勝負。豪華品牌的車不是只掛着一個LOGO就賣這麼貴,如果你坐過邁騰后再坐A4L,你確確實實能夠感受到豪華車帶給你的那種舒適感,高級感。但是在靜態體驗的時候,邁騰的頂配版可以說是完虐奧迪A4L了。所以,我建議,如果你打算購買一台商務車,邁騰的頂配版更加合適。如果只是買來自用,奧迪A4L值得考慮。本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

    【其他文章推薦】

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

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

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

    南投搬家公司費用需注意的眉眉角角,別等搬了再說!

    ※教你寫出一流的銷售文案?

  • 花25.88萬買漢蘭達的人看見榮威RX8會相當後悔?

    花25.88萬買漢蘭達的人看見榮威RX8會相當後悔?

    通過尺寸對比表,我們可以看出榮威RX8的三圍比漢蘭達要更大一些,軸距也比漢蘭達長60mm,營造出充足的7座空間。而且榮威RX8的第三排的膝部/頭部空間比漢蘭達大一拳,全幅靠背的採用舒適性更好。230mm同級稱冠的第二排座椅可調行程以及特有的Easy–Entry一鍵放倒功能,進出第三排的便利性更高。

    談起7座的SUV,大家都會聯想到充沛的乘坐空間、良好的通過性等標籤,它們擁有着極高的實用性,在生活方式日益多樣化的今天已成為汽車消費升級的首選,漢蘭達可以說是其中最熱門的車型之一,但是7座SUV車型之間的競爭越來越激烈,全新打造的車型越來越多,漢蘭達不得不面臨一位強勁對手發起的挑戰!

    它就是來自上汽榮威的“全領域大7座豪華SUV”車型榮威RX8!它是一款基於上汽新一代IS(智能化SUV)平台打造,具備“ALL-DRIVE”智能全領域駕駛系統,也擁有寬敞7座空間的重點車型!而這兩款車型在價格上有所重疊,到底榮威RX8能不能戰勝目前7座SUV界最火車型呢?虎哥就來和大家一探究竟!

    不得不和大家先嘮嗑一下車型的價格,因為榮威RX8已經在北京車展正式上市,虎哥認為其售價富有誠意,吸引力絕對不低,給予消費者選擇的空間也更多。

    咱們選取售價為22.88萬的榮威RX8 2018款 30T 智聯網四驅至尊版車型和指導價為25.88萬的豐田漢蘭達2.0T四驅精英版進行直接對比。

    先來對比外觀造型,榮威RX8的前臉採用了“律動”設計語言,造型富有力量感。全LED騰雲大燈科技感強,視覺也相當舒展,4顆透鏡式LED燈源組成的獵戶座星雲霧燈,也顯得炯炯有神。

    (注:拍攝視頻車型為2017款漢蘭達)

    榮威RX8的車身還具備黃金分割設計短前懸長后懸的豪華四驅車身比例,看上去具備動感,側面也刻畫出強而有力的輪包曲面。尾部設計橫向舒展,它具備內切燕尾、梯形的尾門的設計特點,比較有意思的是定音錘尾燈造型帶有“一錘定音”的寓意,細節處理值得仔細欣賞!

    新款漢蘭達的外觀設計在原有基礎上有所升級,它進氣格柵的面積得到擴大,但是車身輪廓仍舊屬於中庸的設計風格。

    視覺上略顯沉悶,而大燈造型的變化不大,它融入了LED日間行車燈,相比榮威RX8的全LED燈組顯得精緻感不足。車尾的造型敦實,造型飽滿但精緻感略顯不足。

    榮威RX8品牌家族化設計相當舒展、座艙採用環繞式設計富有層次感、體現出精緻豪華也兼具優雅的設計風格。用料相當考究,大面積皮質軟包與淡雅木紋飾板帶來良好觸感,10.1英寸中控大屏也富有科技感,細節處理也進一步提升了精緻感!

    而漢蘭達的內飾質感則顯得比較遜色,它中控布局簡潔明了,採用的軟質材料不少,看上去比較大氣。但是軟包面積不及榮威RX8,6.1英寸中控屏多媒體娛樂系統也顯得有些小氣。

    在大家相當關心的配置方面,榮威RX8拉開了明顯的優勢!值得一提的是在用戶舒適性營造方面它十分到位,無線充電、真皮座椅、主駕駛電動調節、前排座椅加熱+通風、第二排座椅加熱、大面積採光的全景天窗、獨立式空氣凈化器它均有配備!

    而且科技配備也已相當齊全,它配備有車機手機雙向WIFI、AI人工智能語音系統、主動導航系統、智能操作系統、虛擬儀錶、loT手機遠程車控系統等潮流前端的科技配備,配置方面的優勢十分明顯!

    通過尺寸對比表,我們可以看出榮威RX8的三圍比漢蘭達要更大一些,軸距也比漢蘭達長60mm,營造出充足的7座空間。而且榮威RX8的第三排的膝部/頭部空間比漢蘭達大一拳,全幅靠背的採用舒適性更好!230mm同級稱冠的第二排座椅可調行程以及特有的Easy–Entry一鍵放倒功能,進出第三排的便利性更高。漢蘭達第二排僅有150mm的移動距離,便利性有待提高。

    榮威RX8採用了BBA豪華品牌供應商德國Bader的頂級頭層小公牛皮座椅,而且還應用階梯式劇院座椅布局,每排乘客能享受舒適坐姿、視野開闊,並且NVH靜音水準較高,還配備大採光面積全景天窗。

    舒適配置全面,榮威RX8的前排擁有電動調節、主駕迎賓記憶、遠程座椅加熱、三檔加熱和通風等功能,第二排乘客也能享受座椅加熱功能,這功能在冬天相當實用!而漢蘭達則採用織物座椅,缺乏座椅加熱、通風功能,前排座椅沒有沒有電動調節,實用性不及榮威RX8。

    榮威RX8搭載縱置布局的“藍芯”2.0TGI缸內直噴渦輪增壓發動機,它具備全鋁缸體、雙可變氣門正時技術、渦輪增壓等高效科技,輸出224馬力最大功率、360牛米最大扭矩,動力輸出優於漢蘭達。

    與之匹配的是一款縱置愛信6速手自一體變速箱,動力輸出具備平順性、高效率的特點,這讓榮威RX8具備9.1秒的百公里加速和9.1L/100km綜合油耗的出眾表現!

    此外榮威RX8全系標配雙活塞浮動式制動卡鉗和陶瓷剎車片,這讓它的100-0km/h制動距離僅38米,要知道這是一款大尺寸的7座SUV車型,日常駕駛富有底氣!而某主流汽車媒體實測的漢蘭達剎車距離已超過41米,與榮威RX8有着不少差距。

    咱們先來看看硬件配備,榮威RX8採用高強度非承載式車身、雙叉臂獨立前懸架+五連桿后懸架,車身剛性高、抗扭能力強。它還具備較大后傾角設定,比起採用承載式車身設計的漢蘭達更能適應各種道路情況!

    而且榮威RX8公路駕駛時擁有良好的操控性、舒適性,它還搭載全時四驅系統和專業級中央、後橋差速鎖,可實現2驅和4驅鎖止!自動机械後橋限滑差速器在左右車輪速差超過120轉/分鐘時會自動鎖死,在複雜路面脫困能力高。

    漢蘭達則屬於承載式車身的城市SUV,採用的是基於前橫置的適時四驅系統,而且它僅有中央差速器鎖止功能,通過性不強,更適合市區路段。

    而且在四驅系統標定這一重要的軟實力方面,榮威RX8 無疑更加全面,下面咱們通過一個表格來詳細對比:

    榮威RX8帶有六大地形駕駛模式,在各種路況上適應能力強不少,而且雪地、越野、低速這三個駕駛模式能針對複雜的越野路面,實用性比漢蘭達高不少。

    榮威RX8搭載全新一代互聯網汽車智能系統-,即基於AliOS的斑馬智行解決方案,各項功能符合時下潮流,實用性強!在硬件上榮威RX8搭載10.1英寸高清中控屏和7英寸虛擬儀錶,显示清晰,操控更加便利,提升駕駛安全性。同時也提供了車機手機雙向WiFi和基礎服務、基本流量終身免費。

    而在這一價位中的漢蘭達只有6.1英寸多媒體娛樂系統,導航、有手機映射、聯網功能欠奉,功能性上落後於時代。

    寫在最後:經過全面的對比,咱們可以發現榮威RX8的外觀設計、內飾豪華度、科技感、配置豐富程度、乘坐空間、複雜路面適應性能、四驅系統全面性上都具備優勢,其實這也在意料之中,現在車型迭代迅速,作為全新車型的榮威RX8明顯具有後來居上之優勢,也極有潛力成為7座SUV中最有競爭力的車型!本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

    【其他文章推薦】

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

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

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

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

    ※產品缺大量曝光嗎?你需要的是一流包裝設計!

  • 要成為合格的女司機,最大的挑戰是什麼?

    要成為合格的女司機,最大的挑戰是什麼?

    可是,要完成這一次挑戰,我必須要有一台車。這次和我一起挑戰的,是全新昂科威28T頂配車型,2。0T發動機搭配9AT變速箱,擁有260匹馬力,還配備了CDC主動懸挂。2。0T發動機符合我對動力跟經濟性的需求。相比以前,全新的9AT變速箱也足夠聰明,足以應付多種路況。

    大家好,我是小喬,說真的,我有點害怕。

    作為一個女司機,我覺得我的駕駛技術算挺好了,但為什麼,我的知名程度,遠遠不如那個長得跟我差不多的Jacky呢?

    到底是“7200干它”成就了他,還是他成就了“7200干它”,這個問題,在我腦海中迴旋,如同先有雞還是先有蛋的生物史拷問,令我疑惑不已。終於,我決定卧薪嘗膽,直接跟他當面對質爆紅的真諦!

    Jacky面對我的質疑,嘴角微微上揚,說到:“既然你誠心誠意想要爆紅,那我就大發慈悲地成全你, 別克SUV強者挑戰之旅,這個對精神以及體力都有極大考驗的活動,就決定讓你參加了!“

    挑戰內容

    挑戰一:體能挑戰:限時挑戰3公里高空棧道。優秀的司機必須擁有強壯的體能!

    挑戰二:耐力測試,獨自跑完4個小時高速全程,鍛煉作為司機的集中力和耐久力。

    挑戰三:膽量挑戰:攀登懸崖天梯,鍛煉司機膽大心細的危急情況處理能力。

    挑戰四:險中求勝:考驗司機越野路面的駕駛能力。

    手拿這份挑戰清單,我感到一絲恐懼,但誓要成為女車神的我,不會輕易認輸!

    可是,要完成這一次挑戰,我必須要有一台車。這次和我一起挑戰的,是全新昂科威28T頂配車型,2.0T發動機搭配9AT變速箱,擁有260匹馬力,還配備了CDC主動懸挂。

    2.0T發動機符合我對動力跟經濟性的需求;相比以前,全新的9AT變速箱也足夠聰明,足以應付多種路況;它的懸架還能主動變化,公路、越野我都能舒舒服服。這一次挑戰,我勢在必得。

    想知道到底Jacky給小喬的挑戰有多可怕?讓小喬這個沒心沒肺的女人都感到驚慌,而她又能否順利完成任務?趕緊點開視頻看一看!

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

    【其他文章推薦】

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

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

    ※教你寫出一流的銷售文案?

    ※超省錢租車方案

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

    ※產品缺大量曝光嗎?你需要的是一流包裝設計!