1. 程式人生 > >最新的dubbo 2.6.2版本中@Service註解的parameters配置BUG和解決方案

最新的dubbo 2.6.2版本中@Service註解的parameters配置BUG和解決方案

在描述這個BUG之前,我想先說一個需求場景,假設我們有一個DemoService介面:

public interface DemoService {
    String sayHello(String name);
    String sayHello2(String name);
}

我們想單獨設定這兩個方法的超時時間,該如何設定呢?

當然我們可以在consumer端通過@Reference註解的parameters註解實現(PS:基於XML的配置可以配置MethodConfig元素,but基於註解的配置沒有單獨設定某個method的配置,只能曲線救國,我們只要清楚一點就是所有的配置都會作為url引數的一部分就OK了,所以我們可以通過配置註解的parameters引數來往url上附加額外引數來達到相同的目的):

@RestController
public class DemoConsumerController {


    @Reference(

            version = "${demo.service.version}",
            application = "${dubbo.application.id}",
            registry = "${dubbo.registry.id}",
            parameters = {"sayHello.timeout", "3000", "sayHello2.timeout", "5000"}

    )
    private DemoService demoService;


    @RequestMapping("/sayHello")
    public String sayHello(@RequestParam String name) throws ExecutionException, InterruptedException {

        return demoService.sayHello(name);
    }

    @RequestMapping("/sayHello2")
    public String sayHello2(@RequestParam String name) throws ExecutionException, InterruptedException {

        return demoService.sayHello2(name);
    }
}

這裡我們將sayHello.timeout設定了3000毫秒,sayHello2.timeout設定了5000毫秒。執行程式,沒有啥毛病。

但是dubbo官方推薦做法是儘量在provider端多做配置,比如timeout這種配置,應該在服務提供者端配置,而不是在消費者端配置,因為提供者更清楚他提供的方法大致會花費多長時間。(雖然按照dubbo的配置覆蓋規則,在consumer端的配置會覆蓋provider端的配置)。

好吧,那我們還是按官方的建議來調整這個配置,把timeout配置到@Service註解上去:

@Service(
        version = "${demo.service.version}",
        application = "${dubbo.application.id}",
        protocol = "${dubbo.protocol.id}",
        registry = "${dubbo.registry.id}"
        , parameters = {"sayHello.timeout", "3100", "sayHello2.timeout", "5000"}
)
public class DefaultDemoService implements DemoService {
    @Override
    public String sayHello(String name) {
        System.out.println("get request");
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return "sayHello Hello, " + name + " (from Spring Boot)";
    }

    @Override
    public String sayHello2(String name) {
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return "sayHello2 Hello, " + name + " (from Spring Boot)";
    }
}

啟動程式你會發現竟然報錯了:

org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'ServiceBean:defaultDemoService:com.kingnet.blockdata.service.DemoService:${demo.service.version}': Initialization of bean failed; nested exception is org.springframework.beans.ConversionNotSupportedException: Failed to convert property value of type 'java.lang.String[]' to required type 'java.util.Map' for property 'parameters'; nested exception is java.lang.IllegalStateException: Cannot convert value of type 'java.lang.String[]' to required type 'java.util.Map' for property 'parameters': no matching editors or conversion strategy found
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:589) ~[spring-beans-5.0.7.RELEASE.jar:5.0.7.RELEASE]
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:503) ~[spring-beans-5.0.7.RELEASE.jar:5.0.7.RELEASE]
	at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:317) ~[spring-beans-5.0.7.RELEASE.jar:5.0.7.RELEASE]
	at org.springframework.beans.factory.support.AbstractBeanFactory$$Lambda$97/1279271200.getObject(Unknown Source) ~[na:na]
	at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:222) ~[spring-beans-5.0.7.RELEASE.jar:5.0.7.RELEASE]
	at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:315) ~[spring-beans-5.0.7.RELEASE.jar:5.0.7.RELEASE]
	at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:199) ~[spring-beans-5.0.7.RELEASE.jar:5.0.7.RELEASE]
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:760) ~[spring-beans-5.0.7.RELEASE.jar:5.0.7.RELEASE]
	at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:869) ~[spring-context-5.0.7.RELEASE.jar:5.0.7.RELEASE]
	at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:550) ~[spring-context-5.0.7.RELEASE.jar:5.0.7.RELEASE]
	at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:140) ~[spring-boot-2.0.3.RELEASE.jar:2.0.3.RELEASE]
	at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:759) ~[spring-boot-2.0.3.RELEASE.jar:2.0.3.RELEASE]
	at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:395) ~[spring-boot-2.0.3.RELEASE.jar:2.0.3.RELEASE]
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:327) ~[spring-boot-2.0.3.RELEASE.jar:2.0.3.RELEASE]
	at org.springframework.boot.builder.SpringApplicationBuilder.run(SpringApplicationBuilder.java:137) [spring-boot-2.0.3.RELEASE.jar:2.0.3.RELEASE]
	at com.kingnet.blockdata.DubboProviderDemo.main(DubboProviderDemo.java:20) [classes/:na]
Caused by: org.springframework.beans.ConversionNotSupportedException: Failed to convert property value of type 'java.lang.String[]' to required type 'java.util.Map' for property 'parameters'; nested exception is java.lang.IllegalStateException: Cannot convert value of type 'java.lang.String[]' to required type 'java.util.Map' for property 'parameters': no matching editors or conversion strategy found
	at org.springframework.beans.AbstractNestablePropertyAccessor.convertIfNecessary(AbstractNestablePropertyAccessor.java:590) ~[spring-beans-5.0.7.RELEASE.jar:5.0.7.RELEASE]
	at org.springframework.beans.AbstractNestablePropertyAccessor.convertForProperty(AbstractNestablePropertyAccessor.java:604) ~[spring-beans-5.0.7.RELEASE.jar:5.0.7.RELEASE]
	at org.springframework.beans.BeanWrapperImpl.convertForProperty(BeanWrapperImpl.java:219) ~[spring-beans-5.0.7.RELEASE.jar:5.0.7.RELEASE]
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.convertForProperty(AbstractAutowireCapableBeanFactory.java:1660) ~[spring-beans-5.0.7.RELEASE.jar:5.0.7.RELEASE]
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.applyPropertyValues(AbstractAutowireCapableBeanFactory.java:1616) ~[spring-beans-5.0.7.RELEASE.jar:5.0.7.RELEASE]
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.populateBean(AbstractAutowireCapableBeanFactory.java:1363) ~[spring-beans-5.0.7.RELEASE.jar:5.0.7.RELEASE]
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:580) ~[spring-beans-5.0.7.RELEASE.jar:5.0.7.RELEASE]
	... 15 common frames omitted
Caused by: java.lang.IllegalStateException: Cannot convert value of type 'java.lang.String[]' to required type 'java.util.Map' for property 'parameters': no matching editors or conversion strategy found
	at org.springframework.beans.TypeConverterDelegate.convertIfNecessary(TypeConverterDelegate.java:299) ~[spring-beans-5.0.7.RELEASE.jar:5.0.7.RELEASE]
	at org.springframework.beans.AbstractNestablePropertyAccessor.convertIfNecessary(AbstractNestablePropertyAccessor.java:585) ~[spring-beans-5.0.7.RELEASE.jar:5.0.7.RELEASE]
	... 21 common frames omitted

原因很明顯,@Service裡面的parameters是String[]型別,但是要對映到ServiceBean的parameters屬性是一個Map<String,String>型別,沒法自動轉換對映上去,所以報了這個錯誤。

那麼如何解決這個問題呢?

我們檢視原始碼發現ServiceBean是通過:

com.alibaba.dubbo.config.spring.beans.factory.annotation.ServiceAnnotationBeanPostProcessor#registerServiceBean

這個BeanPostProcessor註冊進來的,這裡面掃描了所有打了@Service的類,把他們註冊成了ServiceBean。

於是我們就有了曲線救國的方法,我們在定義一個BeanPostProcessor,在ServiceAnnotationBeanPostProcessor之後執行,然後在ServiceBean真正例項化之前轉換一下parameters這個引數為Map<String,String>就好了:


import com.alibaba.dubbo.config.spring.ServiceBean;
import com.alibaba.dubbo.config.spring.convert.converter.StringArrayToMapConverter;
import com.alibaba.dubbo.config.spring.convert.converter.StringArrayToStringConverter;
import org.springframework.beans.BeansException;
import org.springframework.beans.PropertyValue;
import org.springframework.beans.PropertyValues;
import org.springframework.beans.factory.config.InstantiationAwareBeanPostProcessorAdapter;
import org.springframework.core.PriorityOrdered;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.support.DefaultConversionService;

import java.beans.PropertyDescriptor;
import java.util.Map;

/**
 * 解決@Service註解配置parameters引數時無法將String[]轉化成Map<String,String>的bug
 *
 * @author : xiaojun
 * @since 13:16 2018/7/23
 */
public class ServiceParameterBeanPostProcessor extends InstantiationAwareBeanPostProcessorAdapter implements PriorityOrdered {

    @Override
    public int getOrder() {
        return PriorityOrdered.LOWEST_PRECEDENCE;
    }

    @Override
    public PropertyValues postProcessPropertyValues(PropertyValues pvs, PropertyDescriptor[] pds, Object bean, String beanName) throws BeansException {
//        pvs.getPropertyValue("parameter")
        if (bean instanceof ServiceBean) {
            PropertyValue propertyValue = pvs.getPropertyValue("parameters");
            ConversionService conversionService = getConversionService();

            if (propertyValue != null && propertyValue.getValue() != null && conversionService.canConvert(propertyValue.getValue().getClass(), Map.class)) {
                Map parameters = conversionService.convert(propertyValue.getValue(), Map.class);
                propertyValue.setConvertedValue(parameters);
            }
        }
        return pvs;
    }

    private ConversionService getConversionService() {
        DefaultConversionService conversionService = new DefaultConversionService();
        conversionService.addConverter(new StringArrayToStringConverter());
        conversionService.addConverter(new StringArrayToMapConverter());
        return conversionService;
    }
}

細心的讀者可能已經發現了點端倪,為毛@Reference註解裡面的parameters可以正確對映到ReferenceBean的Map<String,String> parameters上來。。。說來也有點坑,這兩個註解Bean的實現方式竟然不統一。但是dubbo提供了一個Converter:

com.alibaba.dubbo.config.spring.convert.converter.StringArrayToMapConverter;

我們也用這個converter將原始PropertySource裡面的String[] parameters轉換成Map<String,String> parameters就行了。

這裡還實現了Ordered介面,讓我們的這個BeanPostProcessor的優先順序最低,這樣可以保證在dubbo自己的那個BeanPostProcessor之後執行,才能順利轉換這個屬性。

當然,別忘了註冊bean:

    @Bean
    ServiceParameterBeanPostProcessor serviceParameterBeanPostProcessor() {
        return new ServiceParameterBeanPostProcessor();
    }

啟動程式檢視一下console的url資訊:

dubbo://192.168.56.1:12345/com.kingnet.blockdata.service.DemoService?anyhost=true&application=dubbo-provider-demo&dubbo=2.6.2&generic=false&interface=com.kingnet.blockdata.service.DemoService&methods=sayHello,sayHello2&pid=9052&revision=1.0.0&sayHello.timeout=3100&sayHello2.timeout=5000&side=provider&status=server&timestamp=1532588660430&version=1.0.0

發現parameters的配置資訊成功附加到url的引數裡面了。