這些事情SUV可以輕鬆做到,轎車完全不行

、所說SUV有着以上優點,但是上下車對老人不友好以及盲區大、油耗大這些缺點還是存在的,關鍵還是根據自身需求出發選擇SUV還是轎車。



所說SUV有着以上優點,但是上下車對老人不友好以及盲區大、油耗大這些缺點還是存在的,關鍵還是根據自身需求出發選擇SUV還是轎車。本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※超省錢租車方案

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

※回頭車貨運收費標準

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

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

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

@Autowired 註解詳解

前言

我們平時使用 Spring 時,想要 依賴注入 時使用最多的是 @Autowired 註解了,本文主要講解 Spring 是如何處理該註解並實現 依賴注入 的功能的。

正文

首先我們看一個測試用例:

User 實體類:

public class User {

    private Long id;
    private String name;

	// 省略 get 和 set 方法
}

測試類:

public class AnnotationDependencyInjectTest {

    /**
     * @Autowired 字段注入
     */
    @Autowired
    private User user;

    private City city;

    /**
     * @Autowired 方法注入
     */
    @Autowired
    public void initCity(City city) {
        this.city = city;
    }

    public static void main(String[] args) {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
        context.register(AnnotationDependencyInjectTest.class);
        context.refresh();
        AnnotationDependencyInjectTest bean = context.getBean(AnnotationDependencyInjectTest.class);
        // @Autowired 字段注入
        System.out.println(bean.user);
        // @Autowired 方法注入
        System.out.println(bean.city);
        UserHolder userHolder = context.getBean(UserHolder.class);
        // @Autowired 構造器注入
        System.out.println(userHolder.getUser());
        context.close();
    }

    @Bean
    public User user() {
        User user = new User();
        user.setId(1L);
        user.setName("leisurexi");
        return user;
    }

    @Bean
    public City city() {
        City city = new City();
        city.setId(1L);
        city.setName("北京");
        return city;
    }
    
    /**
     * @Autowired 構造函數注入
     */
    static class UserHolder {

        private User user;

        @Autowired
        public UserHolder(User user) {
            this.user = user;
        }

        public User getUser() {
            return user;
        }

        public void setUser(User user) {
            this.user = user;
        }
    }

}

上面分別展示了 @Autowired 註解的字段注入和方法注入,下面我們開始分析 Spring 是如何實現的。

首先使 @Autowired 註解生效的一個關鍵類是 AutowiredAnnotationBeanPostProcessor,該類實現了 InstantiationAwareBeanPostProcessorAdapter 抽象類;該抽象類就是一個適配器的作用提供了接口方法的默認實現,InstantiationAwareBeanPostProcessorAdapter 又實現了 SmartInstantiationAwareBeanPostProcessor 接口,同時實現該接口的 determineCandidateConstructors() 方法可以指定 bean 的候選構造函數;然後 SmartInstantiationAwareBeanPostProcessor 接口又繼承了 InstantiationAwareBeanPostProcessor 接口,該接口提供了 bean 實例化前後的生命周期回調以及屬性賦值前的後置處理方法,@Autowired 註解的屬性注入就是通過重寫該接口的 postProcessProperties() 實現的。這兩個接口都在 在 Spring IoC createBean 方法詳解 一文中有介紹過。下面我們看一下 AutowiredAnnotationBeanProcessor 的繼承關係圖:

關於 AutowiredAnnotationBeanPostProcessor 這個後置處理器是怎麼加入到 beanFactory 中的,我們在 Spring IoC component-scan 節點詳解 一文中介紹過主要是通過 AnnotationConfigUtils#registerAnnotationConfigProcessors() 實現的。

public static Set<BeanDefinitionHolder> registerAnnotationConfigProcessors(
    BeanDefinitionRegistry registry, @Nullable Object source) {
    
    // 忽略其它代碼
    
    // 註冊用於處理@Autowired、@Value、@Inject註解的後置處理器
    if (!registry.containsBeanDefinition(AUTOWIRED_ANNOTATION_PROCESSOR_BEAN_NAME)) {
        RootBeanDefinition def = new
            RootBeanDefinition(AutowiredAnnotationBeanPostProcessor.class);
        def.setSource(source);
        beanDefs.add(registerPostProcessor(registry, def,
                                           AUTOWIRED_ANNOTATION_PROCESSOR_BEAN_NAME));
    }
    // 忽略其它代碼
    
}

屬性和方法注入

AutowiredAnnotationBeanPostProcessor 中跟屬性注入有關的方法有兩個:postProcessMergedBeanDefinitionpostProcessPropertyValues

前者是 MergedBeanDefinitionPostProcessor 接口中的方法,定義如下:

public interface MergedBeanDefinitionPostProcessor extends BeanPostProcessor {

	/**
	 * 對指定bean的BeanDefinition合併后的處理方法回調
	 */
	void postProcessMergedBeanDefinition(RootBeanDefinition beanDefinition, Class<?> beanType, String beanName);

	/**
	 * @since 5.1
	 * 通知已重新設置指定beanName的BeanDefinition,如果實現該方法應該清除受影響的bean的所有元數據
	 */
	default void resetBeanDefinition(String beanName) {
	}

}

後者是 InstantiationAwareBeanPostProcessor 接口中的方法,定義如下:

public interface InstantiationAwareBeanPostProcessor extends BeanPostProcessor {

	/**
	 * Bean 實例化前調用,返回非 {@code null} IoC 容器不會對 Bean 進行實例化 並且後續的生命周期回調方	  *	法不會調用,返回 {@code null} 則進行 IoC 容器對 Bean 的實例化
	 */
	@Nullable
	default Object postProcessBeforeInstantiation(Class<?> beanClass, String beanName) throws BeansException {
		return null;
	}

	/**
	 * Bean 實例化之後,屬性填充之前調用,返回 {@code true} 則進行默認的屬性填充步驟,返回 {@code 		 * false} 會跳過屬性填充階段。
	 */
	default boolean postProcessAfterInstantiation(Object bean, String beanName) throws BeansException {
		return true;
	}

	/**
	 * Bean 實例化后屬性賦值前調用,PropertyValues 是已經封裝好的設置的屬性值,返回 {@code null} 繼續
	 * 使用現有屬性,否則會替換 PropertyValues。
	 * @since 5.1版本新加的和底下的方法一樣
	 */
	@Nullable
	default PropertyValues postProcessProperties(PropertyValues pvs, Object bean, String beanName)
			throws BeansException {
		return null;
	}

	/**
	 * 跟上面方法一樣的功能,只不過是5.1以前版本所使用的
	 * 返回 {@code null} 會跳過屬性填充階段
	 */
	@Deprecated
	@Nullable
	default PropertyValues postProcessPropertyValues(
			PropertyValues pvs, PropertyDescriptor[] pds, Object bean, String beanName) throws BeansException {

		return pvs;
	}

}

關於這兩個方法的調用時機,可以查看 Spring IoC createBean 方法詳解。

AutowiredAnnotationBeanPostProcessor#postProcessMergedBeanDefinition

首先執行的是 postProcessMergedBeanDefinition()

public void postProcessMergedBeanDefinition(RootBeanDefinition beanDefinition, Class<?> beanType, String beanName) {
    // 尋找需要注入的字段或方法,並封裝成 InjectionMetadata,見下文詳解
    InjectionMetadata metadata = findAutowiringMetadata(beanName, beanType, null);
    // 檢查元數據中的註解信息
    metadata.checkConfigMembers(beanDefinition);
}

InjectionMetadata 就是注入的元信息描述,主要字段如下:

public class InjectionMetadata {
    
    // 需要依賴注入的目標類
    private final Class<?> targetClass;
    // 注入元素的集合
    private final Collection<InjectedElement> injectedElements;
    
    // 忽略其它代碼
    
}

InjectedElement 就是注入的元素,主要字段如下:

public abstract static class InjectedElement {
    
    // 注入的屬性或方法
    protected final Member member;
    // 需要注入的是否是字段
    protected final boolean isField;
    
}

AutowiredAnnotationBeanPostProcessor#findAutowiringMetadata

private InjectionMetadata findAutowiringMetadata(String beanName, Class<?> clazz, @Nullable PropertyValues pvs) {
    // Fall back to class name as cache key, for backwards compatibility with custom callers.
    String cacheKey = (StringUtils.hasLength(beanName) ? beanName : clazz.getName());
    // Quick check on the concurrent map first, with minimal locking.
    // 首先從緩存中獲取
    InjectionMetadata metadata = this.injectionMetadataCache.get(cacheKey);
    // 判斷是否需要刷新,即metadata為null或者metadata中存儲的targetClass和當前clazz不等
    if (InjectionMetadata.needsRefresh(metadata, clazz)) {
        // 這裏相當於是一個double check,防止多線程出現的併發問題
        synchronized (this.injectionMetadataCache) {
            metadata = this.injectionMetadataCache.get(cacheKey);
            if (InjectionMetadata.needsRefresh(metadata, clazz)) {
                if (metadata != null) {
                    metadata.clear(pvs);
                }
                // 構建注入元信息,見下文詳解
                metadata = buildAutowiringMetadata(clazz);
                // 放入緩存中
                this.injectionMetadataCache.put(cacheKey, metadata);
            }
        }
    }
    // 返回注入元信息
    return metadata;
}

AutowiredAnnotationBeanPostProcessor#buildAutowiringMetadata

private InjectionMetadata buildAutowiringMetadata(final Class<?> clazz) {
    if (!AnnotationUtils.isCandidateClass(clazz, this.autowiredAnnotationTypes)) {
        return InjectionMetadata.EMPTY;
    }
    // 判斷當前類或其字段或其方法是否標註了autowiredAnnotationTypes中的註解,沒有的話直接返回空的
    List<InjectionMetadata.InjectedElement> elements = new ArrayList<>();
    Class<?> targetClass = clazz;

    do {
        final List<InjectionMetadata.InjectedElement> currElements = new ArrayList<>();
        // 遍歷targetClass中的字段
        ReflectionUtils.doWithLocalFields(targetClass, field -> {
            // 獲取field上的@Autowired註解信息
            MergedAnnotation<?> ann = findAutowiredAnnotation(field);
            if (ann != null) {
                // 如果字段是靜態類型是不會進行注入的
                if (Modifier.isStatic(field.getModifiers())) {
                    if (logger.isInfoEnabled()) {
                        logger.info("Autowired annotation is not supported on static fields: " + field);
                    }
                    return;
                }
                // 獲取@Autowired註解中的required屬性
                boolean required = determineRequiredStatus(ann);
                // 將裝成AutowiredFieldElement添加進currElements
                currElements.add(new AutowiredFieldElement(field, required));
            }
        });
        // 遍歷targetClass中的方法
        ReflectionUtils.doWithLocalMethods(targetClass, method -> {
            // 找到橋接方法
            Method bridgedMethod = BridgeMethodResolver.findBridgedMethod(method);
            // 判斷方法的可見性,如果不可見則直接返回
            if (!BridgeMethodResolver.isVisibilityBridgeMethodPair(method, bridgedMethod)) {
                return;
            }
            // 獲取method上的@Autowired註解信息
            MergedAnnotation<?> ann = findAutowiredAnnotation(bridgedMethod);
            if (ann != null && method.equals(ClassUtils.getMostSpecificMethod(method, clazz))) {
                // 如果是靜態方法是不會進行注入的
                if (Modifier.isStatic(method.getModifiers())) {
                    if (logger.isInfoEnabled()) {
                        logger.info("Autowired annotation is not supported on static methods: " + method);
                    }
                    return;
                }
                // 方法注入沒有參數就違背了初衷,就是在脫褲子放屁
                if (method.getParameterCount() == 0) {
                    if (logger.isInfoEnabled()) {
                        logger.info("Autowired annotation should only be used on methods with parameters: " + method);
                    }
                }
                // 獲取@Autowired註解中的required屬性
                boolean required = determineRequiredStatus(ann);
                // 將方法和目標類型封裝成屬性描述符
                PropertyDescriptor pd = BeanUtils.findPropertyForMethod(bridgedMethod, clazz);
                // 封裝成AutowiredMethodElement添加進currElements
                currElements.add(new AutowiredMethodElement(method, required, pd));
            }
        });
        // 將currElements整個添加進elements
        elements.addAll(0, currElements);
        // 獲取targetClass的父類,進行下一次循環
        targetClass = targetClass.getSuperclass();
    }
    // 當targetClass為空或者targetClass等於Object.class時會退出循環
    while (targetClass != null && targetClass != Object.class);
    // 將elements和clazz封裝成InjectionMetadata返回
    return InjectionMetadata.forElements(elements, clazz);
}

上面代碼中的 findAutowiredAnnotation() 就是在遍歷 autowiredAnnotationTypes 屬性,看字段或者方法上的註解是否存在於 autowiredAnnotationTypes 中,或者其派生註解,找到第一個就返回,不會再繼續遍歷了。

public class AutowiredAnnotationBeanPostProcessor extends InstantiationAwareBeanPostProcessorAdapter
		implements MergedBeanDefinitionPostProcessor, PriorityOrdered, BeanFactoryAware {
    
    private final Set<Class<? extends Annotation>> autowiredAnnotationTypes = new LinkedHashSet<>(4);
    
    public AutowiredAnnotationBeanPostProcessor() {
        this.autowiredAnnotationTypes.add(Autowired.class);
        this.autowiredAnnotationTypes.add(Value.class);
        try {
            this.autowiredAnnotationTypes.add((Class<? extends Annotation>)
                                              ClassUtils.forName("javax.inject.Inject", AutowiredAnnotationBeanPostProcessor.class.getClassLoader()));
            logger.trace("JSR-330 'javax.inject.Inject' annotation found and supported for autowiring");
        }
        catch (ClassNotFoundException ex) {
            // JSR-330 API not available - simply skip.
        }
	}
    
}

AutowiredAnnotationBeanPostProcessor 類的構造函數中,我們可以發現 autowiredAnnotationTypes 默認添加了 @Autowired@Value 以及 @Inject (在 JSR-330 的jar包存在於當前環境時)。

至此,使用 @Autowired 修飾的字段和方法已經封裝成 InjectionMetadata 並放在 injectionMetadataCache 緩存中,便於後續使用。

AutowireAnnotationBeanPostProcessor#postProcessProperties

postProcessMergedBeanDefinition() 調用后 bean 就會進行實例化接着調用 postProcessProperties()

public PropertyValues postProcessProperties(PropertyValues pvs, Object bean, String beanName) {
    // 獲取緩存中的 InjectionMetadata
    InjectionMetadata metadata = findAutowiringMetadata(beanName, bean.getClass(), pvs);
    try {
        // 進行屬性的注入
        metadata.inject(bean, beanName, pvs);
    }
    catch (BeanCreationException ex) {
        throw ex;
    }
    catch (Throwable ex) {
        throw new BeanCreationException(beanName, "Injection of autowired dependencies failed", ex);
    }
    // 返回注入的屬性
    return pvs;
}

// InjectMetadata.java
public void inject(Object target, @Nullable String beanName, @Nullable PropertyValues pvs) throws Throwable {
    // 獲取檢查后的元素
    Collection<InjectedElement> checkedElements = this.checkedElements;
    // 如果checkedElements不為空就使用checkedElements,否則使用injectedElements
    Collection<InjectedElement> elementsToIterate =
        (checkedElements != null ? checkedElements : this.injectedElements);
    if (!elementsToIterate.isEmpty()) {
        // 遍歷elementsToIterate
        for (InjectedElement element : elementsToIterate) {
            if (logger.isTraceEnabled()) {
                logger.trace("Processing injected element of bean '" + beanName + "': " + element);
            }
            // AutowiredFieldElement、AutowiredMethodElement這兩個類繼承InjectionMetadata.InjectedElement,各自重寫了inject方法
            element.inject(target, beanName, pvs);
        }
    }
}

AutowiredFieldElement#inject

protected void inject(Object bean, @Nullable String beanName, @Nullable PropertyValues pvs) throws Throwable {
    // 強轉成Field類型
    Field field = (Field) this.member;
    Object value;
    if (this.cached) {
        // 如果緩存過,直接使用緩存的值,一般第一次注入都是false
        value = resolvedCachedArgument(beanName, this.cachedFieldValue);
    }
    else {
        // 構建依賴描述符
        DependencyDescriptor desc = new DependencyDescriptor(field, this.required);
        desc.setContainingClass(bean.getClass());
        Set<String> autowiredBeanNames = new LinkedHashSet<>(1);
        Assert.state(beanFactory != null, "No BeanFactory available");
        // 獲取類型轉換器
        TypeConverter typeConverter = beanFactory.getTypeConverter();
        try {
            // 進行依賴解決,獲取符合條件的bean
            value = beanFactory.resolveDependency(desc, beanName, autowiredBeanNames, typeConverter);
        }
        catch (BeansException ex) {
            throw new UnsatisfiedDependencyException(null, beanName, new InjectionPoint(field), ex);
        }
        // 加鎖
        synchronized (this) {
            // 如果沒有被緩存
            if (!this.cached) {
                // 找到了需要的bean || 該字段是必要的
                if (value != null || this.required) {
                    // 將依賴描述符賦值給cachedFieldValue
                    this.cachedFieldValue = desc;
                    // 註冊bean的依賴關係,用於檢測是否循環依賴
                    registerDependentBeans(beanName, autowiredBeanNames);
                    // 如果符合條件的bean只有一個
                    if (autowiredBeanNames.size() == 1) {
                        String autowiredBeanName = autowiredBeanNames.iterator().next();
                        // beanFactory含有名為autowiredBeanName的bean && 類型是匹配的
                        if (beanFactory.containsBean(autowiredBeanName) &&
                            beanFactory.isTypeMatch(autowiredBeanName, field.getType())) {
                            // 將該屬性解析到的bean的信息封裝成ShortcutDependencyDescriptor
                            // 之後可以通過調用resolveShortcut()來間接調beanFactory.getBean()快速獲取bean
                            this.cachedFieldValue = new ShortcutDependencyDescriptor(
                                desc, autowiredBeanName, field.getType());
                        }
                    }
                }
                else {
                    this.cachedFieldValue = null;
                }
                // 緩存標識設置為true
                this.cached = true;
            }
        }
    }
    // 如果找到了符合的bean,設置字段可訪問,利用反射設置值
    if (value != null) {
        ReflectionUtils.makeAccessible(field);
        field.set(bean, value);
    }
}

上面代碼中的 beanFactory.resolveDependency() 在 Spring IoC createBean 方法詳解 一文中有介紹過,這裏不再贅述;同樣 registerDependentBeans() 最終會調用 DefaultSingletonBeanRegistry.registerDependentBean() ,該方法在 Spring IoC getBean 方法詳解 一文中有介紹過,這裏也不再贅述。

AutowiredMethodElement#inject

protected void inject(Object bean, @Nullable String beanName, @Nullable PropertyValues pvs) throws Throwable {
    // 檢查是否需要跳過
    if (checkPropertySkipping(pvs)) {
        return;
    }
    // 強轉成Method類型
    Method method = (Method) this.member;
    Object[] arguments;
    if (this.cached) {
        // Shortcut for avoiding synchronization...
        // 如果緩存過,直接調用beanFactory.resolveDependency()返回符合的bean
        arguments = resolveCachedArguments(beanName);
    }
    else {
        // 獲取參數數量
        int argumentCount = method.getParameterCount();
        arguments = new Object[argumentCount];
        // 創建依賴描述符數組
        DependencyDescriptor[] descriptors = new DependencyDescriptor[argumentCount];
        // 記錄用於自動注入bean的名稱集合
        Set<String> autowiredBeans = new LinkedHashSet<>(argumentCount);
        Assert.state(beanFactory != null, "No BeanFactory available");
        // 獲取類型轉換器
        TypeConverter typeConverter = beanFactory.getTypeConverter();
        // 遍歷參數
        for (int i = 0; i < arguments.length; i++) {
            // 將方法和參數的下標構建成MethodParameter,這裏面主要記錄了參數的下標和類型
            MethodParameter methodParam = new MethodParameter(method, i);
            // 將MethodParameter構建成DependencyDescriptor
            DependencyDescriptor currDesc = new DependencyDescriptor(methodParam, this.required);
            currDesc.setContainingClass(bean.getClass());
            descriptors[i] = currDesc;
            try {
                // 進行依賴解決,找到符合條件的bean
                Object arg = beanFactory.resolveDependency(currDesc, beanName, autowiredBeans, typeConverter);
                if (arg == null && !this.required) {
                    arguments = null;
                    break;
                }
                arguments[i] = arg;
            }
            catch (BeansException ex) {
                throw new UnsatisfiedDependencyException(null, beanName, new InjectionPoint(methodParam), ex);
            }
        }
        // 這裏跟字段注入差不多,就是註冊bean的依賴關係,並且緩存每個參數的ShortcutDependencyDescriptor
        synchronized (this) {
            if (!this.cached) {
                if (arguments != null) {
                    DependencyDescriptor[] cachedMethodArguments = Arrays.copyOf(descriptors, arguments.length);
                    registerDependentBeans(beanName, autowiredBeans);
                    if (autowiredBeans.size() == argumentCount) {
                        Iterator<String> it = autowiredBeans.iterator();
                        Class<?>[] paramTypes = method.getParameterTypes();
                        for (int i = 0; i < paramTypes.length; i++) {
                            String autowiredBeanName = it.next();
                            if (beanFactory.containsBean(autowiredBeanName) &&
                                beanFactory.isTypeMatch(autowiredBeanName, paramTypes[i])) {
                                cachedMethodArguments[i] = new ShortcutDependencyDescriptor(descriptors[i], autowiredBeanName, paramTypes[i]);
                            }
                        }
                    }
                    this.cachedMethodArguments = cachedMethodArguments;
                }
                else {
                    this.cachedMethodArguments = null;
                }
                this.cached = true;
            }
        }
    }
    // 找到了符合條件的bean
    if (arguments != null) {
        try {
            // 設置方法可訪問,利用反射進行方法調用,傳入參數
            ReflectionUtils.makeAccessible(method);
            method.invoke(bean, arguments);
        }
        catch (InvocationTargetException ex) {
            throw ex.getTargetException();
        }
    }
}

構造器注入

構造器注入就是通過調用 determineCandidateConstructors() 來返回合適的構造器。

public Constructor<?>[] determineCandidateConstructors(Class<?> beanClass, final String beanName) throws BeanCreationException {

    // Quick check on the concurrent map first, with minimal locking.
    // 首先從緩存中獲取
    Constructor<?>[] candidateConstructors = this.candidateConstructorsCache.get(beanClass);
    // 緩存為空
    if (candidateConstructors == null) {
        // Fully synchronized resolution now...
        // 這裏相當於double check
        synchronized (this.candidateConstructorsCache) {
            candidateConstructors = this.candidateConstructorsCache.get(beanClass);
            if (candidateConstructors == null) {
                Constructor<?>[] rawCandidates;
                try {
                    // 獲取beanClass的所有構造函數
                    rawCandidates = beanClass.getDeclaredConstructors();
                }
                catch (Throwable ex) {
                    throw new BeanCreationException(beanName, "Resolution of declared constructors on bean Class [" + beanClass.getName() +"] from ClassLoader [" + beanClass.getClassLoader() + "] failed", ex);
                }
                // 存放標註了@Autowired註解的構造器
                List<Constructor<?>> candidates = new ArrayList<>(rawCandidates.length);
                // 存放標註了@Autowired註解,並且required為true的構造器
                Constructor<?> requiredConstructor = null;
                Constructor<?> defaultConstructor = null;
                for (Constructor<?> candidate : rawCandidates) {
                    // 獲取構造器上的@Autowired註解信息
                    MergedAnnotation<?> ann = findAutowiredAnnotation(candidate);
                    if (ann == null) {
                        // 如果沒有從候選者找到註解,則嘗試解析beanClass的原始類(針對CGLIB代理)
                        Class<?> userClass = ClassUtils.getUserClass(beanClass);
                        if (userClass != beanClass) {
                            try {
                                Constructor<?> superCtor =
userClass.getDeclaredConstructor(candidate.getParameterTypes());
                                ann = findAutowiredAnnotation(superCtor);
                            }
                            catch (NoSuchMethodException ex) {
                                // Simply proceed, no equivalent superclass constructor found...
                            }
                        }
                    }
                    if (ann != null) {
                        // 如果requiredConstructor不為空,代表有多個標註了@Autowired且required為true的構造器,此時Spring不知道選擇哪個拋出異常
                        if (requiredConstructor != null) {
                            throw new BeanCreationException(beanName, "Invalid autowire-marked constructor: " + candidate +". Found constructor with 'required' Autowired annotation already: " + requiredConstructor);
                        }
                        // 獲取@Autowired註解的reuired屬性的值
                        boolean required = determineRequiredStatus(ann);
                        if (required) {
                            // 如果當前候選者是@Autowired(required = true),則之前不能存在其他使用@Autowire註解的構造函數,否則拋異常
                            if (!candidates.isEmpty()) {
                                throw new BeanCreationException(beanName,"Invalid autowire-marked constructors: " + candidates +". Found constructor with 'required' Autowired annotation: " + candidate);
                            }
                            // required為true將當前構造器賦值給requiredConstructor
                            requiredConstructor = candidate;
                        }
                        // 將當前構造器加入進候選構造器中
                        candidates.add(candidate);
                    }
                    // 沒有標註了@Autowired註解且參數長度為0,賦值為默認構造器
                    else if (candidate.getParameterCount() == 0) {
                        defaultConstructor = candidate;
                    }
                }
                // 有標註了@Autowired註解的構造器
                if (!candidates.isEmpty()) {
                    // Add default constructor to list of optional constructors, as fallback.
                    // 沒有標註了@Autowired且required為true的構造器
                    if (requiredConstructor == null) {
                        // 默認構造器不為空
                        if (defaultConstructor != null) {
                            // 將默認構造器加入進候選構造器中
                            candidates.add(defaultConstructor);
                        }
                    }
                    // 將候選者賦值給candidateConstructors
                    candidateConstructors = candidates.toArray(new Constructor<?>[0]);
                }
                // 只有1個構造器 && 參數長度大於0(非默認構造器),只能用它當做候選者了
                else if (rawCandidates.length == 1 && rawCandidates[0].getParameterCount() > 0) {
                    candidateConstructors = new Constructor<?>[] {rawCandidates[0]};
                }
                // 只有1個構造器 && 參數長度大於0,只能用它當做候選者了
                else if (nonSyntheticConstructors == 2 && primaryConstructor != null &&
                         defaultConstructor != null && !primaryConstructor.equals(defaultConstructor)) {
                    candidateConstructors = new Constructor<?>[] {primaryConstructor, defaultConstructor};
                }
                else if (nonSyntheticConstructors == 1 && primaryConstructor != null) {
                    candidateConstructors = new Constructor<?>[] {primaryConstructor};
                }
                // 返回一個空的Constructor
                else {
                    candidateConstructors = new Constructor<?>[0];
                }
                // 緩存候選的構造器
                this.candidateConstructorsCache.put(beanClass, candidateConstructors);
            }
        }
    }
    // 如果候選構造器長度大於0,直接返回,否則返回null
    return (candidateConstructors.length > 0 ? candidateConstructors : null);
}

關於 SmartInstantiationAwareBeanPostProcessor 接口的調用時機,在 Spring IoC createBean 方法詳解 一文中有介紹過,這裏就不再贅述了。

總結

本文主要介紹了 Spring 對 @Autowired 註解的主要處理過程,結合前面的 Spring IoC getBean 方法詳解 和 Spring IoC createBean 方法詳解 文章一起看才能更好的理解。

最後,我模仿 Spring 寫了一個精簡版,代碼會持續更新。地址:https://github.com/leisurexi/tiny-spring。

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

【其他文章推薦】

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

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

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

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

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

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

03 . Django之騰訊雲短信

簡介

由於項目在註冊、登錄、找回密碼 時需要發送短信驗證的功能,我們使用騰訊雲短信做。

為什麼要用騰訊雲短信呢? 因為註冊就送 100條免費短信 的額度。

實現

註冊騰訊雲

註冊一個騰訊雲賬戶,騰訊雲中提供了很多功能:雲服務器、雲存儲你、雲直播、雲短信等很多功能。

註冊地址:https://cloud.tencent.com/

註冊並要實名認證

開通雲短信

創建應用

創建應用並將應用中生成的 SDK AppIDApp Key 複製下來,之後通過python發送短信時需要用到。

創建簽名

在騰訊雲短信簽名時需要認證,認證需要填寫簽名類型:網站、APP、小程序、公眾號,前三種需要提供企業資質等複雜的東西,個人公眾號認證會比較便捷,所以推薦個人開發的話使用 公眾號 進行簽名。

so,咱們需要先 申請一個公眾號 然後 創建簽名

申請微信訂閱號

註冊地址:https://mp.weixin.qq.com/

創建簽名

創建模板

發送短信

上述的準備工作做完中我們開通相關服務並獲取到如下幾個值:

# 創建應用,獲取到 appid 和 appkey
# 創建簽名,獲取 簽名內容
# 創建模板,獲取 模板ID

接下來開始使用Python發送短信。

安裝SDK

 pip install qcloudsms_py

基於SDK發送短信

#!/usr/bin/env python
# -*- coding:utf-8 -*-
import ssl
# ssl._create_default_https_context = ssl._create_unverified_context
from qcloudsms_py import SmsMultiSender, SmsSingleSender
from qcloudsms_py.httpclient import HTTPError
def send_sms_single(phone_num, template_id, template_param_list):
    """
    單條發送短信
    :param phone_num: 手機號
    :param template_id: 騰訊雲短信模板ID
    :param template_param_list: 短信模板所需參數列表,例如:【驗證碼:{1},描述:{2}】,則傳遞參數 [888,666]按順序去格式化模板
    :return:
    """
    appid = 112142311  # 自己應用ID
    appkey = "8cc5b87123y423433412387930004"  # 自己應用Key
    sms_sign = "幽夢DevOps"  # 自己騰訊雲創建簽名時填寫的簽名內容(使用公眾號的話這個值一般是公眾號全稱或簡稱)
    sender = SmsSingleSender(appid, appkey)
    try:
        response = sender.send_with_param(86, phone_num, template_id, template_param_list, sign=sms_sign)
    except HTTPError as e:
        response = {'result': 1000, 'errmsg': "網絡異常發送失敗"}
    return response
def send_sms_multi(phone_num_list, template_id, param_list):
    """
    批量發送短信
    :param phone_num_list:手機號列表
    :param template_id:騰訊雲短信模板ID
    :param param_list:短信模板所需參數列表,例如:【驗證碼:{1},描述:{2}】,則傳遞參數 [888,666]按順序去格式化模板
    :return:
    """
    appid = 112142311
    appkey = "8cc5b87123y423423412387930004"
    sms_sign = "幽夢DevOps"
    sender = SmsMultiSender(appid, appkey)
    try:
        response = sender.send_with_param(86, phone_num_list, template_id, param_list, sign=sms_sign)
    except HTTPError as e:
        response = {'result': 1000, 'errmsg': "網絡異常發送失敗"}
    return response
if __name__ == '__main__':
    result1 = send_sms_single("18621048481", 548760, [666, ])
    print(result1)
    result2 = send_sms_single( ["18621048481", "15131255089", "15131255089", ],548760, [999, ])
    print(result2)
關於頻率限制

騰訊雲短信後台可以進行 短信頻率 的限制。

但是,由於我們是免費用戶所以無法進行設置,只能使用默認的配置(30秒發1條/1小時發5條/1天發10條)。

!

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

【其他文章推薦】

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

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

※超省錢租車方案

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

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

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

SpringCloud之Security

Spring Security是Spring提供的一個安全框架,提供認證和授權功能,最主要的是它提供了簡單的使用方式,同時又有很高的靈活性,簡單,靈活,強大。

我個人博客系統採用的權限框架就是Spring Security,正好整合到SpringCloud裏面。

一般系統里關於角色方面通常有這麼幾張表,角色表、用戶-角色表、菜單表、角色-菜單表等。

不過我個人博客系統主要以wordpress作為參考,沿用其12張表,如圖:

 

 

一、導入Maven依賴

<properties>
       <jjwt.version>0.9.0</jjwt.version>
       <spring-security-jwt.version>1.0.9.RELEASE</spring-security-jwt.version>
</properties>
 <!-- springsecurity-->
       <dependency>
           <groupId>org.springframework.boot</groupId>
           <artifactId>spring-boot-starter-security</artifactId>
       </dependency>
       <dependency>
           <groupId>org.springframework.security</groupId>
           <artifactId>spring-security-jwt</artifactId>
           <version>${spring-security-jwt.version}</version>
       </dependency>
       <dependency>
           <groupId>io.jsonwebtoken</groupId>
           <artifactId>jjwt</artifactId>
           <version>${jjwt.version}</version>
       </dependency>

二、編寫Spring Security配置類

package com.springcloud.blog.admin.config;
import com.springcloud.blog.admin.security.UserAuthenticationProvider;
import com.springcloud.blog.admin.security.UserPermissionEvaluator;
import com.springcloud.blog.admin.security.handler.*;
import com.springcloud.blog.admin.security.jwt.JWTAuthenticationTokenFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.access.expression.DefaultWebSecurityExpressionHandler;

/**
 * SpringSecurity配置類
 * @Author youcong
 */
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true) //開啟權限註解,默認是關閉的
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    /**
     * 自定義登錄成功處理器
     */
    @Autowired
    private UserLoginSuccessHandler userLoginSuccessHandler;
    /**
     * 自定義登錄失敗處理器
     */
    @Autowired
    private UserLoginFailureHandler userLoginFailureHandler;
    /**
     * 自定義註銷成功處理器
     */
    @Autowired
    private UserLogoutSuccessHandler userLogoutSuccessHandler;
    /**
     * 自定義暫無權限處理器
     */
    @Autowired
    private UserAuthAccessDeniedHandler userAuthAccessDeniedHandler;
    /**
     * 自定義未登錄的處理器
     */
    @Autowired
    private UserAuthenticationEntryPointHandler userAuthenticationEntryPointHandler;
    /**
     * 自定義登錄邏輯驗證器
     */
    @Autowired
    private UserAuthenticationProvider userAuthenticationProvider;

    /**
     * 加密方式
     * @Author youcong
     */
    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder(){
        return new BCryptPasswordEncoder();
    }
    /**
     * 注入自定義PermissionEvaluator
     */
    @Bean
    public DefaultWebSecurityExpressionHandler userSecurityExpressionHandler(){
        DefaultWebSecurityExpressionHandler handler = new DefaultWebSecurityExpressionHandler();
        handler.setPermissionEvaluator(new UserPermissionEvaluator());
        return handler;
    }

    /**
     * 配置登錄驗證邏輯
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth){
        //這裏可啟用我們自己的登陸驗證邏輯
        auth.authenticationProvider(userAuthenticationProvider);
    }
    /**
     * 配置security的控制邏輯
     * @Author youcong
     * @Param  http 請求
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {

        http.authorizeRequests()
                // 不進行權限驗證的請求或資源(從配置文件中讀取)
                .antMatchers(JWTConfig.antMatchers.split(",")).permitAll()
                // .antMatchers("/*").permitAll()
                // 其他的需要登陸后才能訪問
                .anyRequest().authenticated()
                .and()
                // 配置未登錄自定義處理類
                .httpBasic().authenticationEntryPoint(userAuthenticationEntryPointHandler)
                .and()
                // 配置登錄地址
                .formLogin()
                .loginProcessingUrl("/login/userLogin")
                // 配置登錄成功自定義處理類
                .successHandler(userLoginSuccessHandler)
                // 配置登錄失敗自定義處理類
                .failureHandler(userLoginFailureHandler)
                .and()
                // 配置登出地址
                .logout()
                .logoutUrl("/login/userLogout")
                // 配置用戶登出自定義處理類
                .logoutSuccessHandler(userLogoutSuccessHandler)
                .and()
                // 配置沒有權限自定義處理類
                .exceptionHandling().accessDeniedHandler(userAuthAccessDeniedHandler)
                .and()
                // 開啟跨域
                .cors()
                .and()
                // 取消跨站請求偽造防護
                .csrf().disable();
        // 基於Token不需要session
        http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
        // 禁用緩存
        http.headers().cacheControl();
        // 添加JWT過濾器
        http.addFilter(new JWTAuthenticationTokenFilter(authenticationManager()));
    }
}

三、編寫JWTConfig和application.yml增加jwt相關配置

package com.springcloud.blog.admin.config;
import lombok.Getter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

/**
 * JWT配置類
 * @Author youcong
 */
@Getter
@Component
@ConfigurationProperties(prefix = "jwt")
public class JWTConfig {
    /**
     * 密鑰KEY
     */
    public static String secret;
    /**
     * TokenKey
     */
    public static String tokenHeader;
    /**
     * Token前綴字符
     */
    public static String tokenPrefix;
    /**
     * 過期時間
     */
    public static Integer expiration;
    /**
     * 不需要認證的接口
     */
    public static String antMatchers;


    public void setSecret(String secret) {
        this.secret = secret;
    }

    public void setTokenHeader(String tokenHeader) {
        this.tokenHeader = tokenHeader;
    }

    public void setTokenPrefix(String tokenPrefix) {
        this.tokenPrefix = tokenPrefix;
    }

    public void setExpiration(Integer expiration) {
        this.expiration = expiration * 1000;
    }

    public void setAntMatchers(String antMatchers) {
        this.antMatchers = antMatchers;
    }


}

application.yml增加如下內容:

# JWT配置
jwt:
  # 密匙KEY
  secret: JWTSecret
  # HeaderKEY
  tokenHeader: Authorization
  # Token前綴字符
  tokenPrefix: challenger-
  # 過期時間 單位秒 1天後過期=86400 7天後過期=604800
  expiration: 86400
  # 配置不需要認證的接口
  antMatchers: /index/**,/login/**,/favicon.ico
  # 有效時間
  validTime: 7

四、編寫過濾器處理類

1.UserLoginSuccessHandler.java

package com.springcloud.blog.admin.security.handler;


import com.springcloud.blog.admin.config.JWTConfig;
import com.springcloud.blog.admin.security.entity.SelfUserEntity;
import com.springcloud.blog.admin.utils.AccessAddressUtil;
import com.springcloud.blog.admin.utils.JWTTokenUtil;
import com.springcloud.blog.admin.utils.RedisUtil;
import com.springcloud.blog.admin.utils.ResultUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.HashMap;
import java.util.Map;

/**
 * @Description 登錄成功處理類
 * @Author youcong
 */
@Component
public class UserLoginSuccessHandler implements AuthenticationSuccessHandler {


    /**
     * 登錄成功返回結果
     * @Author youcong
     */
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication){
        // 組裝JWT
        SelfUserEntity selfUserEntity =  (SelfUserEntity) authentication.getPrincipal();
        String token = JWTTokenUtil.createAccessToken(selfUserEntity);
        token = JWTConfig.tokenPrefix + token;

        // 封裝返回參數
        Map<String,Object> resultData = new HashMap<>();
        resultData.put("code","200");
        resultData.put("msg", "登錄成功");
        resultData.put("token",token);
        ResultUtil.responseJson(response,resultData);
    }
}

2.UserLoginFailureHandler.java

package com.springcloud.blog.admin.security.handler;


import com.springcloud.blog.admin.utils.ResultUtil;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.LockedException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * @Description 登錄失敗處理類
 * @Author youcong
 */
@Component
public class UserLoginFailureHandler implements AuthenticationFailureHandler {
    /**
     * 登錄失敗返回結果
     * @Author youcong
     */
    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception){
        // 這些對於操作的處理類可以根據不同異常進行不同處理
        if (exception instanceof UsernameNotFoundException){
            System.out.println("【登錄失敗】"+exception.getMessage());
            ResultUtil.responseJson(response,ResultUtil.resultCode(500,"用戶名不存在"));
        }
        if (exception instanceof LockedException){
            System.out.println("【登錄失敗】"+exception.getMessage());
            ResultUtil.responseJson(response,ResultUtil.resultCode(500,"用戶被凍結"));
        }
        if (exception instanceof BadCredentialsException){
            System.out.println("【登錄失敗】"+exception.getMessage());
            ResultUtil.responseJson(response,ResultUtil.resultCode(500,"密碼錯誤"));
        }
        ResultUtil.responseJson(response,ResultUtil.resultCode(500,"登錄失敗"));
    }
}

3.UserLogoutSuccessHandler.java

package com.springcloud.blog.admin.security.handler;


import com.springcloud.blog.admin.utils.DateUtil;
import com.springcloud.blog.admin.utils.RedisUtil;
import com.springcloud.blog.admin.utils.ResultUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.HashMap;
import java.util.Map;

/**
 * 登出成功處理類
 * @Author youcong
 */
@Component
public class UserLogoutSuccessHandler implements LogoutSuccessHandler {
    

    /**
     * 用戶登出返回結果
     * 這裏應該讓前端清除掉Token
     * @Author youcong
     */
    @Override
    public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication){

        Map<String,Object> resultData = new HashMap<>();
        resultData.put("code","200");
        resultData.put("msg", "登出成功");
        SecurityContextHolder.clearContext();
        ResultUtil.responseJson(response,ResultUtil.resultSuccess(resultData));
    }
}

4.UserAuthAccessDeniedHandler.java

package com.springcloud.blog.admin.security.handler;
import com.springcloud.blog.admin.utils.ResultUtil;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * @Description 暫無權限處理類
 * @Author youcong
 */
@Component
public class UserAuthAccessDeniedHandler implements AccessDeniedHandler {
    /**
     * 暫無權限返回結果
     * @Author youcong
     */
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException exception){
        ResultUtil.responseJson(response,ResultUtil.resultCode(403,"未授權"));
    }
}

5.UserAuthenticationEntryPointHandler.java

package com.springcloud.blog.admin.security.handler;


import com.springcloud.blog.admin.utils.ResultUtil;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * 用戶未登錄處理類
 * @Author youcong
 */
@Component
public class UserAuthenticationEntryPointHandler implements AuthenticationEntryPoint {
    /**
     * 用戶未登錄返回結果
     * @Author youcong
     */
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception){
        ResultUtil.responseJson(response,ResultUtil.resultCode(401,"未登錄"));
    }
}

6.UserAuthenticationProvider.java

自定義登錄驗證這個類,需要根據實際情況重寫。通常來說改動不大。

package com.springcloud.blog.admin.security;

import com.baomidou.mybatisplus.mapper.EntityWrapper;
import com.springcloud.blog.admin.entity.Usermeta;
import com.springcloud.blog.admin.entity.Users;
import com.springcloud.blog.admin.security.entity.SelfUserEntity;
import com.springcloud.blog.admin.service.UsermetaService;
import com.springcloud.blog.admin.service.UsersService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.LockedException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Component;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

/**
 * 自定義登錄驗證
 *
 * @Author youcong
 */
@Component
public class UserAuthenticationProvider implements AuthenticationProvider {


    @Autowired
    private UsersService usersService;

    @Autowired
    private UsermetaService usermetaService;

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        // 獲取表單輸入中返回的用戶名
        String userName = (String) authentication.getPrincipal();
        // 獲取表單中輸入的密碼
        String password = (String) authentication.getCredentials();
        // 查詢用戶是否存在
         SelfUserEntity userInfo = usersService.getUserInfo(userName);


        if (userInfo.getUsername() == null || userInfo.getUsername() == "") {
            throw new UsernameNotFoundException("用戶名不存在");
        }


        // 我們還要判斷密碼是否正確,這裏我們的密碼使用BCryptPasswordEncoder進行加密的
        if (!new BCryptPasswordEncoder().matches(password, userInfo.getPassword())) {
            throw new BadCredentialsException("密碼不正確");
        }
        // 還可以加一些其他信息的判斷,比如用戶賬號已停用等判斷
        if (userInfo.getStatus().equals("1")) {
            throw new LockedException("該用戶已被凍結");
        }
        // 角色集合
        Set<GrantedAuthority> authorities = new HashSet<>();

        EntityWrapper<Usermeta> roleWrapper = new EntityWrapper<>();
        roleWrapper.eq("user_id",userInfo.getUserId());
        roleWrapper.eq("meta_key","wp_user_level");
        // 查詢用戶角色
        List<Usermeta> sysRoleEntityList = usermetaService.selectList(roleWrapper);
        for (Usermeta sysRoleEntity: sysRoleEntityList){
            authorities.add(new SimpleGrantedAuthority("ROLE_" + sysRoleEntity.getMetaValue()));
        }
        userInfo.setAuthorities(authorities);
        // 進行登錄
        return new UsernamePasswordAuthenticationToken(userInfo, password, authorities);
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return true;
    }
}

7.UserPermissionEvaluator.java

package com.springcloud.blog.admin.security;


import com.baomidou.mybatisplus.mapper.EntityWrapper;
import com.springcloud.blog.admin.entity.Usermeta;
import com.springcloud.blog.admin.service.UsermetaService;
import org.apache.catalina.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.PermissionEvaluator;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Component;
import java.io.Serializable;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

/**
 * 自定義權限註解驗證
 * @Author youcong
 */
@Component
public class UserPermissionEvaluator implements PermissionEvaluator {

    @Autowired
    private UsermetaService usermetaService;

    /**
     * hasPermission鑒權方法
     * 這裏僅僅判斷PreAuthorize註解中的權限表達式
     * 實際中可以根據業務需求設計數據庫通過targetUrl和permission做更複雜鑒權
     * 當然targetUrl不一定是URL可以是數據Id還可以是管理員標識等,這裏根據需求自行設計
     * @Author youcong
     * @Param  authentication  用戶身份(在使用hasPermission表達式時Authentication參數默認會自動帶上)
     * @Param  targetUrl  請求路徑
     * @Param  permission 請求路徑權限
     * @Return boolean 是否通過
     */
    @Override
    public boolean hasPermission(Authentication authentication, Object targetUrl, Object permission) {
        // 獲取用戶信息
        Usermeta selfUserEntity =(Usermeta) authentication.getPrincipal();
        // 查詢用戶權限(這裏可以將權限放入緩存中提升效率)
        Set<String> permissions = new HashSet<>();
        EntityWrapper<Usermeta> roleWrapper = new EntityWrapper<>();
        roleWrapper.eq("user_id",selfUserEntity.getUserId());
        roleWrapper.eq("meta_key","wp_user_level");
        List<Usermeta> sysMenuEntityList = usermetaService.selectList(roleWrapper);
        for (Usermeta sysMenuEntity:sysMenuEntityList) {
            permissions.add(sysMenuEntity.getMetaValue());
        }
        // 權限對比
        if (permissions.contains(permission.toString())){
            return true;
        }
        return true;
    }
    @Override
    public boolean hasPermission(Authentication authentication, Serializable targetId, String targetType, Object permission) {
        return false;
    }
}

五、編寫實體類

package com.springcloud.blog.admin.security.entity;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.io.Serializable;
import java.util.Collection;
import java.util.Map;

/**
 * SpringSecurity用戶的實體
 * 注意:這裏必須要實現UserDetails接口
 *
 * @Author youcong
 */
public class SelfUserEntity implements Serializable, UserDetails {

    private static final long serialVersionUID = 1L;

    /**
     * 用戶ID
     */
    private Long userId;
    /**
     * 用戶名
     */
    private String username;
    /**
     * 密碼
     */
    private String password;
    /**
     * 狀態
     */
    private String status;


    /**
     * 显示名稱
     */
    private String displayName;


    /**
     * 用戶參數
     */
    private Map<String, String> userParamMap;


    /**
     * 用戶角色
     */
    private Collection<GrantedAuthority> authorities;
    /**
     * 賬戶是否過期
     */
    private boolean isAccountNonExpired = false;
    /**
     * 賬戶是否被鎖定
     */
    private boolean isAccountNonLocked = false;
    /**
     * 證書是否過期
     */
    private boolean isCredentialsNonExpired = false;
    /**
     * 賬戶是否有效
     */
    private boolean isEnabled = true;


    public static long getSerialVersionUID() {
        return serialVersionUID;
    }

    public Long getUserId() {
        return userId;
    }

    public void setUserId(Long userId) {
        this.userId = userId;
    }

    @Override
    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    @Override
    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }


    public void setAuthorities(Collection<GrantedAuthority> authorities) {
        this.authorities = authorities;
    }


    public void setEnabled(boolean enabled) {
        isEnabled = enabled;
    }

    public void setStatus(String status) {
        this.status = status;
    }

    public String getStatus() {
        return status;
    }

    public String getDisplayName() {
        return displayName;
    }

    public void setDisplayName(String displayName) {
        this.displayName = displayName;
    }

    public Map<String, String> getUserParamMap() {
        return userParamMap;
    }

    public void setUserParamMap(Map<String, String> userParamMap) {
        this.userParamMap = userParamMap;
    }

    @Override
    public Collection<GrantedAuthority> getAuthorities() {
        return authorities;
    }

    @Override
    public boolean isAccountNonExpired() {
        return isAccountNonExpired;
    }

    @Override
    public boolean isAccountNonLocked() {
        return isAccountNonLocked;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return isCredentialsNonExpired;
    }

    @Override
    public boolean isEnabled() {
        return isEnabled;
    }


}

六、編寫JWT接口請求攔截器

package com.springcloud.blog.admin.security.jwt;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.springcloud.blog.admin.config.JWTConfig;
import com.springcloud.blog.admin.security.entity.SelfUserEntity;
import com.springcloud.blog.admin.utils.CollectionUtil;
import com.springcloud.blog.admin.utils.JWTTokenUtil;
import com.springcloud.blog.admin.utils.RedisUtil;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jwts;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import org.springframework.util.StringUtils;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

/**
 * JWT接口請求校驗攔截器
 * 請求接口時會進入這裏驗證Token是否合法和過期
 *
 * @Author youcong
 */
public class JWTAuthenticationTokenFilter extends BasicAuthenticationFilter {

    public JWTAuthenticationTokenFilter(AuthenticationManager authenticationManager) {
        super(authenticationManager);
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        // 獲取請求頭中JWT的Token
        String tokenHeader = request.getHeader(JWTConfig.tokenHeader);

        if (null != tokenHeader && tokenHeader.startsWith(JWTConfig.tokenPrefix)) {
            try {


                // 截取JWT前綴
                String token = tokenHeader.replace(JWTConfig.tokenPrefix, "");
                // 解析JWT
                Claims claims = Jwts.parser()
                        .setSigningKey(JWTConfig.secret)
                        .parseClaimsJws(token)
                        .getBody();
                // 獲取用戶名
                String username = claims.getSubject();
                String userId = claims.getId();


                if (!StringUtils.isEmpty(username) && !StringUtils.isEmpty(userId)) {
                    // 獲取角色
                    List<GrantedAuthority> authorities = new ArrayList<>();
                    String authority = claims.get("authorities").toString();
                    if (!StringUtils.isEmpty(authority)) {
                        List<Map<String, String>> authorityMap = JSONObject.parseObject(authority, List.class);
                        for (Map<String, String> role : authorityMap) {
                            if (!StringUtils.isEmpty(role)) {
                                authorities.add(new SimpleGrantedAuthority(role.get("authority")));
                            }
                        }
                    }
                    //組裝參數
                    SelfUserEntity selfUserEntity = new SelfUserEntity();
                    selfUserEntity.setUsername(claims.getSubject());
                    selfUserEntity.setUserId(Long.parseLong(claims.getId()));
                    selfUserEntity.setAuthorities(authorities);
                    UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(selfUserEntity, userId, authorities);
                    SecurityContextHolder.getContext().setAuthentication(authentication);
                }
            } catch (ExpiredJwtException e) {
                System.out.println("Token過期");
            } catch (Exception e) {
                System.out.println("Token無效");
            }
        }
        filterChain.doFilter(request, response);
        return;
    }
}

七、SpringSecurity用戶的業務實現

package com.springcloud.blog.admin.security.service;


import com.baomidou.mybatisplus.mapper.EntityWrapper;
import com.springcloud.blog.admin.entity.Users;
import com.springcloud.blog.admin.security.entity.SelfUserEntity;
import com.springcloud.blog.admin.service.UsersService;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Component;

/**
 * SpringSecurity用戶的業務實現
 *
 * @Author youcong
 */
@Component
public class SelfUserDetailsService implements UserDetailsService {

    @Autowired
    private UsersService usersService;

    /**
     * 查詢用戶信息
     *
     * @Author youcong
     * @Param username  用戶名
     * @Return UserDetails SpringSecurity用戶信息
     */
    @Override
    public SelfUserEntity loadUserByUsername(String username) throws UsernameNotFoundException {

        EntityWrapper<Users> wrapper = new EntityWrapper<>();


        //郵箱正則表達式
        String expr = "^([a-zA-Z0-9_\\-\\.]+)@((\\[[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.)|(([a-zA-Z0-9\\-]+\\.)+))([a-zA-Z]{2,4}|[0-9]{1,3})$";

        //是否為郵箱
        if (username.matches(expr)) {
            wrapper.eq("user_email", username);
        } else {
            wrapper.eq("user_login", username);
        }

        // 查詢用戶信息
        Users sysUserEntity = usersService.selectOne(wrapper);
        if (sysUserEntity != null) {
            // 組裝參數
            SelfUserEntity selfUserEntity = new SelfUserEntity();
            BeanUtils.copyProperties(sysUserEntity, selfUserEntity);
            return selfUserEntity;
        }
        return null;
    }
}

八、Spring Security常用註解

1.@Secured

當@EnableGlobalMethodSecurity(securedEnabled=true)的時候,@Secured可以使用。

@PostMapping("/helloUser")
@Secured({"ROLE_normal","ROLE_admin"})
public Map<String, Object> initDashboard() {
        Map<String, Object> result = new HashMap<>();
        result.put(ResponseDict.RESPONSE_TITLE_KEY, "儀錶盤初始化");
        result.put(ResponseDict.RESPONSE_DATA_KEY, dashboardService.initDashboard());
        return ResultUtil.resultSuccess(result);
    }

說明:擁有normal或者admin角色的用戶都可以方法helloUser()方法。另外需要注意的是這裏匹配的字符串需要添加前綴“ROLE_“。

2.@PreAuthorize

Spring的 @PreAuthorize/@PostAuthorize 註解更適合方法級的安全,也支持Spring 表達式語言,提供了基於表達式的訪問控制。

當@EnableGlobalMethodSecurity(prePostEnabled=true)的時候,@PreAuthorize可以使用:

@PostMapping("/initDashboard")
@PreAuthorize("hasRole('100')")
public Map<String, Object> initDashboard() {
    Map<String, Object> result = new HashMap<>();
    result.put(ResponseDict.RESPONSE_TITLE_KEY, "儀錶盤初始化");
    result.put(ResponseDict.RESPONSE_DATA_KEY, dashboardService.initDashboard());
    return ResultUtil.resultSuccess(result);
}

3.@PostAuthorize

@PostAuthorize 註解使用並不多,在方法執行后再進行權限驗證,適合驗證帶有返回值的權限,Spring EL 提供 返回對象能夠在表達式語言中獲取返回的對象returnObject。

當@EnableGlobalMethodSecurity(prePostEnabled=true)的時候,@PostAuthorize可以使用:

@GetMapping("/getUserInfo")
@PostAuthorize(" returnObject!=null &&  returnObject.username == authentication.name")
public User getUserInfo() {
        Object pricipal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
        User user;
        if("anonymousUser".equals(pricipal)) {
            user = null;
        }else {
            user = (User) pricipal;
        }
        return user;
}

九、測試

(1)登錄測試,拿到token,如圖:

 

 

(2)請求中如果不攜帶token的話,請求其它接口就會显示沒有登錄的提示,如圖:

 

 

(3)正確的請求應當攜帶token,就像下面這樣,如圖:

 

 

(4)沒有權限請求,如圖:

 

 

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

【其他文章推薦】

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

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

※回頭車貨運收費標準

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

※超省錢租車方案

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

「從零單排canal 02」canal集群版 + admin控制台 最新搭建姿勢(基於1.1.4版本)

canal [kə’næl],譯意為水道/管道/溝渠,主要用途是基於 MySQL 數據庫增量日誌解析,提供增量數據 訂閱 和 消費。應該是阿里雲DTS(Data Transfer Service)的開源版本,開源地址:
https://github.com/alibaba/canal。

canal從1.1.4版本開始引入了admin控制台,有了很多不一樣的配置方式。在搭建過程中如果僅僅按照wiki的用戶手冊,還是容易踩很多坑的。因此,將筆者在搭建過程中的步驟記錄下來,作為官方wiki的 補充,希望能有所幫助。

根據本文內容與搭建順序 ,並搭配對應的官網文檔鏈接,應該就能快速搭建完成了,enjoy~

1. 部署canal-admin

1)部署服務

官方文檔地址:
https://github.com/alibaba/canal/wiki/Canal-Admin-QuickStart

主要配置application.yml文件

server:
  port: 8089

spring:
  jackson:
    date-format: yyyy-MM-dd HH:mm:ss
    time-zone: GMT+8

spring.datasource:
  address: 127.0.0.1:3306
  database: canal_manager
  username: xxxx
  assword: xxxxx
  driver-class-name: com.mysql.jdbc.Driver
  url: jdbc:mysql://${spring.datasource.address}/${spring.datasource.database}?useUnicode=true&characterEncoding=UTF-8&useSSL=false
  hikari:
    maximum-pool-size: 30
    minimum-idle: 1

canal:
  adminUser: admin
  adminPasswd: admin

 

這裏需要注意,canal的adminPasswd並不是登陸admin的密碼,登陸admin的密碼是設置在對應的數據庫中的,默認為123456。

另外,因為 Canal Admin 是一個管理系統,需要使用數據庫存放配置信息,只用在 MySQL 中執行 Canal Admin 提供的數據庫初始化文件即可,該文件在“conf/canal_manager.sql”路徑下面。

2)登陸瀏覽器訪問

上面的 Canal Admin 配置好了之後直接根據“/bin/startup.sh”啟動 Canal Admin 即可,在瀏覽器上面輸入 hostip:8089 即可進入到管理頁面,如果使用的默認的配置信息,用戶名入”admin”,密碼輸入”123456”即可訪問首頁。

進入到首頁點擊集群的菜單欄,然後選擇新建集群。

在裏面輸入集群的名稱以及 Zookeeper即可,這裏的集群目前還沒有任務節點,後續通過配置 Canal Server 的自動註冊功能,便可以查看該集群下面擁有的節點。

創建集群后,需要先配置集群 主配置,載入模板即可。

如果沒有載入這個模版,那麼在canal-server執行 sh bin/startup.sh local 命令,讀取canal_local.propeties配置啟動時,會報錯

1 Caused by: com.alibaba.otter.canal.common.CanalException: requestGet for canal config error: canal.properties config is empty

 

注意,在主配置中,還是需要記得加入zk的地址配置,跟上面配置集群名字的時候輸入的zk無關 (那個zk地址不知道有啥用):

canal.zkServers = xxx.xx.xx.xx:2181,xxx.xx.xx.xx:2181,xxx.xx.xx.xx:2181

 

如果希望使用canal的集群模式(推薦使用),記得更改配置使用default-instance.xml,不要使用file-instance.xml配置,如下:

#canal.instance.global.spring.xml = classpath:spring/memory-instance.xml
#canal.instance.global.spring.xml = classpath:spring/file-instance.xml
canal.instance.global.spring.xml = classpath:spring/default-instance.xml

2. 部署canal-server

注意,建議先部署好admin,再來部署canal-server,省事不少。

官方文檔地址:
https://github.com/alibaba/canal/wiki/QuickStart

注意,不同於單機版的properties配置,使用admin后,各種配置通過admin全局管理,所以只需要配置canal_local.properties即可。canal_local.properties內容如下:

# register ip
canal.register.ip =

# canal admin config
canal.admin.manager = 127.0.0.1:8089
canal.admin.port = 11110
canal.admin.user = admin
canal.admin.passwd = xxxxxxxxxxxxxx

# admin auto register
canal.admin.register.auto = true
canal.admin.register.cluster =

 

對各個參數說明一下:

  • canal.register.ip:用來指定當前 Canal Server 的 IP 信息,如果主機是多網卡,可以避免 IP 信息錯亂的問題。
  • canal.admin.passwd:這裏的密碼就是之前配置 Canal Admin 裏面配置的adminPasswd,只不過這裏並不是明文展示,使用 MySQL 的”select password(“admin”)”語句查詢處理過的密碼,注意查詢結果前面的”*”要去掉。
  • canal.admin.register.auto:這裡是自動註冊的意思,如果沒有配置,Canal Server 啟動后需要自行在 Canal Admin 上面添加。
  • canal.admin.register.cluster:這個配置如果不寫代表當前的 Canal Server 是一個單機節點,如果添加的名字在 Canal Admin 上面沒有提前註冊,Canal Server 啟動時會報錯。

啟動server,切記切記帶上參數local,這樣才會讀取canal_local.properties的配置

sh bin/startup.sh local
啟動完成后,可以在admin界面看到server的連接信息

 

同時,登陸zk,查看是否已經註冊成功。

 

3. 配置canal-server

採用admin的集群模式后,集群內的canal-server通過zk做HA,因此,canal-server的配置也只能通過集群做全局配置。

就是前面的集群配置-主配置進入。

 

另外,canal作為一個增量數據抓取模塊,抓到變更信息后需要投遞。

通過canal.sererMode配置

這裏也限制了,一個集群內的canal只能支持一種投遞模式。

我們目前暫時以投遞RocketMQ為例進行配置。

官方文檔地址:
https://github.com/alibaba/canal/wiki/Canal-Kafka-RocketMQ-QuickStart

最終配置文件的配置如下:

#################################################
#########         common argument        #############
#################################################

# tcp bind ip
canal.ip =

# register ip to zookeeper
canal.register.ip =
canal.port = 11111
canal.metrics.pull.port = 11112

# canal instance user/passwd
canal.user = xxxx
canal.passwd = xxxxxxxxxxxxxxxxxxx

# canal admin config
canal.admin.manager = 127.0.0.1:8089
canal.admin.port = 11110
canal.admin.user = xxxx
canal.admin.passwd = xxxxxxxxxxxxxx
canal.zkServers = xx.xx.xx.xx:xxxx,xx.xx.xx.xxx:xxxx,xx.xx.xx.xx:xxxx

# flush data to zk
canal.zookeeper.flush.period = 1000
canal.withoutNetty = false

# tcp, kafka, RocketMQ
canal.serverMode = RocketMQ

# flush meta cursor/parse position to file
canal.file.data.dir = ${canal.conf.dir}
canal.file.flush.period = 1000

## memory store RingBuffer size, should be Math.pow(2,n)
canal.instance.memory.buffer.size = 16384

## memory store RingBuffer used memory unit size , default 1kb
canal.instance.memory.buffer.memunit = 1024

## meory store gets mode used MEMSIZE or ITEMSIZE
canal.instance.memory.batch.mode = MEMSIZE
canal.instance.memory.rawEntry = true

## detecing config
canal.instance.detecting.enable = false

#canal.instance.detecting.sql = insert into retl.xdual values(1,now()) on duplicate key update x=now()
canal.instance.detecting.sql = select 1
canal.instance.detecting.interval.time = 3
canal.instance.detecting.retry.threshold = 3
canal.instance.detecting.heartbeatHaEnable = false

# support maximum transaction size, more than the size of the transaction will be cut into multiple transactions delivery
canal.instance.transaction.size =  1024

# mysql fallback connected to new master should fallback times
canal.instance.fallbackIntervalInSeconds = 60

# network config
canal.instance.network.receiveBufferSize = 16384
canal.instance.network.sendBufferSize = 16384
canal.instance.network.soTimeout = 30

# binlog filter config
# 這裏可以對訂閱的消息做過濾
canal.instance.filter.druid.ddl = true
canal.instance.filter.query.dcl = false
canal.instance.filter.query.dml = false
canal.instance.filter.query.ddl = false
canal.instance.filter.table.error = false
canal.instance.filter.rows = false
canal.instance.filter.transaction.entry = false

# binlog format/image check
canal.instance.binlog.format = ROW,STATEMENT,MIXED
canal.instance.binlog.image = FULL,MINIMAL,NOBLOB

# binlog ddl isolation
canal.instance.get.ddl.isolation = false

# parallel parser config
canal.instance.parser.parallel = true

## concurrent thread number, default 60% available processors, suggest not to exceed Runtime.getRuntime().availableProcessors()
#canal.instance.parser.parallelThreadSize = 16
## disruptor ringbuffer size, must be power of 2
canal.instance.parser.parallelBufferSize = 256

# table meta tsdb info
canal.instance.tsdb.enable = true
canal.instance.tsdb.dir = ${canal.file.data.dir:../conf}/${canal.instance.destination:}
canal.instance.tsdb.url = jdbc:h2:${canal.instance.tsdb.dir}/h2;CACHE_SIZE=1000;MODE=MYSQL;
canal.instance.tsdb.dbUsername = canal
canal.instance.tsdb.dbPassword = canal

# dump snapshot interval, default 24 hour
canal.instance.tsdb.snapshot.interval = 24

# purge snapshot expire , default 360 hour(15 days)
canal.instance.tsdb.snapshot.expire = 360

# aliyun ak/sk , support rds/mq
canal.aliyun.accessKey =
canal.aliyun.secretKey =

#################################################
#########         destinations        #############
#################################################
canal.destinations =

# conf root dir
canal.conf.dir = ../conf

# auto scan instance dir add/remove and start/stop instance
canal.auto.scan = true
canal.auto.scan.interval = 5
canal.instance.tsdb.spring.xml = classpath:spring/tsdb/h2-tsdb.xml

#canal.instance.tsdb.spring.xml = classpath:spring/tsdb/mysql-tsdb.xml
canal.instance.global.mode = manager
canal.instance.global.lazy = false
canal.instance.global.manager.address = ${canal.admin.manager}

#canal.instance.global.spring.xml = classpath:spring/memory-instance.xml

#canal.instance.global.spring.xml = classpath:spring/file-instance.xml
canal.instance.global.spring.xml = classpath:spring/default-instance.xml

##################################################
#########              MQ              #############
##################################################
canal.mq.servers = xx.xx.xx.xx:xxxx,xx.xx.xx.xxx:xxxx,xx.xx.xx.xx:xxxx
canal.mq.retries = 0
canal.mq.canalBatchSize = 50
canal.mq.canalGetTimeout = 100
canal.mq.flatMessage = true
canal.mq.compressionType = none
canal.mq.acks = all

#canal.mq.properties. =
canal.mq.producerGroup = test_canal_cluster

# Set this value to "cloud", if you want open message trace feature in aliyun.
canal.mq.accessChannel = local

# aliyun mq namespace

#canal.mq.namespace =

##################################################
#########     Kafka Kerberos Info    #############
##################################################
canal.mq.kafka.kerberos.enable = false
canal.mq.kafka.kerberos.krb5FilePath = "../conf/kerberos/krb5.conf"
canal.mq.kafka.kerberos.jaasFilePath = "../conf/kerberos/jaas.conf"

 

4. 配置canal instance(以投遞MQ為例)

Canal Admin 提供了 Canal Instance 的管理功能。

我們嘗試通過 UI 界面添加需要監聽的數據庫,讓該 Instance 消費 binlog 並將事件發送到 MQ。

  • 點擊“新建 Instance”按鈕創建 Instance
  • 點擊“載入模板”,進行配置修改。

主要修改以下配置:

  • ”canal.instance.mysql.slaveId”:目前1.1.4版本已經不需要配置,系統自動生成
  • ”canal.instance.master.address”:配置你的數據庫地址
  • canal.instance.dbUsername:數據庫用戶名
  • canal.instance.dbPassword:數據庫密碼
  • canal.mq.topic:mq的topic
  • canal.instance.filter.regex=訂閱的庫表名單(例如:dbvtest\\..*),具體參考https://github.com/alibaba/canal/wiki/AdminGuide

保存,啟動,觀察日誌,沒有報錯即可。

同時可以從 canal-server的 操作-詳情 進入,查看可以看到正在運行的instance

這裏需要注意,如果沒有正確註冊到zk,那麼每個canal-server都會有一個正在運行的instance,會導致變更投遞多次。

如果正確註冊了zk,同一個集群下,每個instance應該只在某一個cannal-server中運行。

可以查看zk路徑

/otter/canal/destinations/{xxxx}/running

 

xxxx就是instance的 名稱,running節點表示它運行在哪個server上。

另外,目前對binlog的訂閱支持gtid模式和position模式,通過以下參數設置

# enable gtid use true/false
canal.instance.gtidon=false

 

在demo過程中,發現高可用模式下,gtid無法正常更新到zk,而position模式使用正常。

gtid模式在canal高可用模式下可能存在bug(參考issue:
https://github.com/alibaba/canal/issues/2616),暫時不建議使用。

5. 演練訂閱

在rds執行以下sql

UPDATE `album` SET `attribute`=’5′ WHERE `albumid`=’1′;

在MQ中的消息格式如下:

{

    "data":[

        {
            "albumid":"1",
            "picid":"1234",
            "attribute":"5",
            "lastmodified":"2020-05-15 18:13:35",
            "created":"2019-09-04 18:18:51"
        }
    ],
    "database":"dbvtest",
    "es":1589537615000,
    "id":75,
    "isDdl":false,
    "mysqlType":{
        "albumid":"int(10) unsigned",
        "picid":"int(10) unsigned",
        "attribute":"varchar(200)",
        "lastmodified":"timestamp",
        "created":"timestamp"
    },
    "old":[
        {
            "attribute":"2",
            "lastmodified":"2020-05-15 17:42:57"
        }
    ],
    "pkNames":[
        "albumid"
    ],
    "sql":"",
    "sqlType":{
        "albumid":4,
        "picid":4,
        "attribute":12,
        "lastmodified":93,
        "created":93
    },
    "table":"album",
    "ts":1589537615877,
    "type":"UPDATE"
}

 

6. 監控告警

官方文檔地址:
https://github.com/alibaba/canal/wiki/Prometheus-QuickStart

如果你已經有一套prometheus的監控體系,我們只需要導入模板(
canal/conf/metrics/Canal_instances_tmpl.json)即可,完美~

 

都看到最後了,原創不易,點個關注,點個贊吧~

知識碎片重新梳理,構建Java知識圖譜: github.com/saigu/JavaK…(歷史文章查閱非常方便)

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

【其他文章推薦】

※超省錢租車方案

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

※回頭車貨運收費標準

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

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

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

可能不到10萬就買到1.5T自動7座SUV,你還要考慮銳界?

內飾設計感更強中控台採用了對稱式的布局,中控屏周圍加以黑色鋼琴烤漆面板點綴,鍍鉻飾條包邊,整體的設計感很強,8英寸的中控大屏是比較搶眼的配置,除了一般多媒體功能外還支持導航和Carlife手機映射等功能,觸控的空調按鈕操作方便更顯檔次感,而且還會新增發動機啟停,胎壓監測、行車記錄儀等配置,省去了後面加裝的麻煩。

前言

作為長安首款7座SUV,CX70上市已經有大半年的時間了,實惠的價格能買到大7座SUV,寬敞的空間布局也比較符合現在家庭用戶的需求,但是其比較單一的1.6L+5MT的動力總成,對那些不會開手動擋或者對動力有追求的消費者來說,就比較糾結了,最終1.5T+6AT的解決方案出來了,能讓你滿意嗎?

外觀

更加霸氣

前臉的進氣格柵變化是比較明顯的,大小不一的鍍鉻飾塊錯落有致的分佈在中網上,看上去精緻了許多,門把手和側護板上增加了鍍鉻裝飾,提升了不少檔次感,外擴的后保險杠造型讓車身長度增加了25mm,並且覆蓋了金屬裝飾板,既起到保護作用,又使硬朗的造型中透露着細膩感。

內飾

設計感更強

中控台採用了對稱式的布局,中控屏周圍加以黑色鋼琴烤漆面板點綴,鍍鉻飾條包邊,整體的設計感很強,8英寸的中控大屏是比較搶眼的配置,除了一般多媒體功能外還支持導航和Carlife手機映射等功能,觸控的空調按鈕操作方便更顯檔次感,而且還會新增發動機啟停,胎壓監測、行車記錄儀等配置,省去了後面加裝的麻煩。

空間

一如既往的大

2780mm的軸距,加上調整后的保險杠樣式使其車身增加了25mm至4705mm,在同級別競爭對手來說有一定的優勢,採用2+3+2的座椅布局,雙色拼接的座椅設計視覺效果出色。

動力

新的組合

動力方面新增了一款1.5T發動機,最大功率為150馬力,在同排量車型中來說表現中規中矩,與之匹配的是6擋手動,或者是來自愛信的6擋手自一體變速箱,想購買該款車型的消費者終於等來了渦輪增壓車型。

總結:從產品力來看,長安CX70終於補齊了沒有自動擋車型的短板,在逐漸激烈的7座SUV市場里提升了不少的戰鬥力,但具體性能表現如何,還得上市后才見分曉,還有價格上小編推測應該是很有吸引力的一個價格。本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

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

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

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

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

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

※超省錢租車方案

日本疫情下曝核電廠群聚問題 作業人員無可避免的「三密」

文:宋瑞文(媽媽監督核電廠聯盟特約撰述)

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

【其他文章推薦】

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

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

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

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

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

射殺烏干達「明星」大猩猩拉飛奇 盜獵者判刑11年

摘錄自2020年7月30日自由時報報導

烏干達著名的「明星」大猩猩拉飛奇(Rafiki),6月遭盜獵者拜亞穆卡瑪(Felix Byamukama)刺殺,經當局深入調查,拜亞穆卡瑪認罪並坦承自己還曾獵殺其他野生動物,今被法院判處11年徒刑。

雖然拜亞穆卡瑪認罪,但其他3人否認相關指控,目前仍被羈押等待審判結果。山地大猩猩是瀕臨絕種的珍稀動物,全世界僅有約1000多隻。原本拜亞穆卡瑪被認為可能會面臨無期徒刑,但最後被判入獄11年,西澳大學一位發言人表示,這是因為拜亞穆卡瑪不是在特別的野生動物法庭受審。

生物多樣性
生態保育
物種保育
國際新聞
烏干達
銀背大猩猩

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

【其他文章推薦】

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

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

※回頭車貨運收費標準

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

※超省錢租車方案

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

穿著制服的盜伐集團 官商聯手摧毀柬埔寨保護區林地

環境資訊中心綜合外電;黃鈺婷 翻譯;林大利 審校;稿源:Mongabay

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

【其他文章推薦】

※超省錢租車方案

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

※回頭車貨運收費標準

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

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

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

全新蛻變 試駕東風風行景逸X5

0L發動機的車型只提供手動擋版本,值得一贊的是,全新一代景逸X5繼續延續了景逸家族加大號基因的優良傳統,軸距達到了2720mm,讓該車在同級別中擁有了十分可觀的使用空間和乘坐空間。在駕駛過程中,每個檔位的清晰,雖無吸入感,但各擋位之間的行程較短,掛檔很順手。

如今SUV車型在汽車市場上的火爆程度,相信大家也都有目共睹,自主品牌對合資SUV造成了很大的衝擊,很多人覺得自主汽車熱賣是因為便宜,這個理由當然是有的,但是它僅僅只是其中之一。事實上最近幾年自主品牌的內飾設計、動力匹配、整體品質、科技配置及整體工藝都有了巨大的提升,穩定的質量和口碑才是自主品牌SUV得以持續熱銷的核心。

放眼國內SUV市場,由東風汽車話旗下的景逸X3、景逸X5、景逸XV組成的景逸SUV陣形,攻打市場,各車型分別從不同層面滿足消費者需求,在SUV市場有着屬於自己的一片領地。

新景逸x5的上市,其7.99萬-10.89萬的價格更是显示出東風汽車十足的把握和搶佔SUV市場的決心。

景逸X5是東風風行推出的一款緊湊型SUV,第一代車型於2013年8月30日正式上市,但由於造型、和口碑原因,市場表現一直不溫不火,12月17號全新的東風風行景逸X5上市,與上一代產品相比,全新一代景逸X5在產品力上有了不小的提升,最為直觀的感受便是在外觀設計上,東風柳汽好像打通了“任督二脈”知道什麼樣的外觀能贏的市場。

全新景逸X5搭載1.6L和2.0L兩款自然吸氣發動機,1.6L發動機除匹配手動擋變速箱外,還提供匹配CVT變速箱的車型供消費者選擇,而搭載2.0L發動機的車型只提供手動擋版本,值得一贊的是,全新一代景逸X5繼續延續了景逸家族加大號基因的優良傳統,軸距達到了2720mm,讓該車在同級別中擁有了十分可觀的使用空間和乘坐空間。

在駕駛過程中,每個檔位的清晰,雖無吸入感,但各擋位之間的行程較短,掛檔很順手。全新景逸X5的離合器的結合點相對較寬且很容易找到,一般不會出現熄火的情況。

值得一提的是,新景逸X5還配備了自動駐車功能,在2.0L的車型都有此功能,對於手動檔操作還不是很熟練的人來說,這個功能很實用,特別是擁堵的斜坡上,大大降低操作難度。

全新景逸X5在內飾上的變化是最明顯的,採用了全新的設計風格,整個設計是符合當下整個設計潮流,而且採用T型橫向設計,在儀錶台主體上採用搪塑發泡工藝,特別是將主流的懸浮式显示屏及黑棕雙色內飾運用其中,顯得非常年輕運動感。

其實給我的感覺內飾最大的亮點還屬於中控上的8英寸懸浮式显示屏,智能互聯繫統不僅集車載導航、人機互聯、語音識別、藍牙等影音娛功能於一體,還可以實現與智能手機的完美連接,確實讓整體氛圍上,顯的更加高端了,賣一台這樣價位然後有B級車的科技體驗,這是非常吸引人的。

記得,在廣州車展上,東風柳汽車副總經理姚利文接受專訪時說,全新景逸X5這個價格很親民、極具誠意,這在十萬元級精品SUV市場里是非常具有競爭力的。而且它擁有‘8年或16萬公里’超長保修政策,在業內也很罕見,這是對新景逸X5品質的充分信心。

然後明年下半年全新景逸X5還會陸續推出1.5T和1.3T版,產品序列進一步豐富,我相信,在整個景逸家族的陣形中,景逸X5肯定是擔當著最重要的“開山劈嶺”角色,景逸X5也承載了東風汽車的造車最高水準,我們期待這個“大哥”景逸X5如何帶領眾兄弟,開闢屬於自己的領地。本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

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

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

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

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

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

※超省錢租車方案