如何寫一個RPC框架(二):利用Bean容器和動態代理簡化客戶端代碼

在後續一段時間裏, 我會寫一系列文章來講述如何實現一個RPC框架(我已經實現了一個示例框架, 代碼在我的github上)。 這是系列第二篇文章, 主要講述瞭如何利用Spring以及Java的動態代理簡化調用別的服務的代碼。

在本系列第一篇文章中,我們說到了RPC框架需要關注的第一個點,通過創建代理的方式來簡化客戶端代碼。

如果不使用代理?

如果我們不用代理去幫我們操心那些服務尋址、網絡通信的問題,我們的代碼會怎樣?

我們每調用一次遠端服務,就要在業務代碼中重複一遍那些複雜的邏輯,這肯定是不能接受的!

目標代碼

而我們的目標是寫出簡潔的代碼,就像這樣:

//這個接口應該被單獨打成一個jar包,同時被server和client所依賴
@RPCService(HelloService.class)
public interface HelloService {

    String hello(String name);
}

@Component
@Slf4j
public class AnotherService {
    @Autowired
    HelloService helloService;

    public void callHelloService() {
        //就像調用本地方法一樣自如!
        log.info("Result of callHelloService: {}", helloService.hello("world"));
    }
}

@EnableRPCClients(basePackages = {"pw.hshen.hrpc"})
public class HelloClient {

    public static void main(String[] args) throws Exception {
        ApplicationContext context = new ClassPathXmlApplicationContext("spring.xml");
        AnotherService anotherService = context.getBean(AnotherService.class);
        anotherService.callHelloService();
    }
}

代碼中的AnotherService可以簡單調用遠端的HelloService的方法,就像調用本地的service一樣簡單! 在這段代碼中,HelloService可以視作server, 而AnotherService則是它的調用者,可以視作是client。

實現思路

1.獲取要被創建代理的接口

首先,我們要知道需要爲哪些接口來創建代理。

我們需要爲這種特殊的接口創建一個註解來標註,即RPCService。然後我們就可以通過掃描某個包下面所有包含這個註解的interface來獲取了。

那麼,怎麼知道要掃描哪個包呢?方法就是獲取MainClass的EnableRPCClients註解的basePackages的值。

2.爲這些接口創建動態代理

我們可以利用jdk的動態代理來做這件事兒:

// Interface是需要被創建代理的那個接口
Proxy.newProxyInstance(
            interface.getClassLoader(),
            new Class<?>[]{interface},
            new InvocationHandler() {
                @Override
                public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            // TODO: Do RPC action here and return the result 
                }

3.將創建出來的代理對象註冊到bean容器中

關於如何動態向spring容器中註冊自定義的bean, 可以參考這篇文章
在我的框架中, 我選擇了使用BeanDefinitionRegistryPostProcessor所提供的hook。

注入到bean容器之後,我們就可以在代碼中愉快的用Autowired等註解來獲取所創建的代理啦!

完整代碼

定義需要的註解

/**
 * @author hongbin
 * Created on 22/10/2017
 */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface EnableRPCClients {
    String[] basePackages() default {};
}
/**
 * @author hongbin
 * Created on 21/10/2017
 */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Component
@Inherited
public @interface RPCService {

    Class<?> value();
}

利用Spring的hook機制, 向容器中註冊我們自己的proxy bean:

/**
 * Register proxy bean for required client in bean container.
 * 1. Get interfaces with annotation RPCService
 * 2. Create proxy bean for the interfaces and register them
 *
 * @author hongbin
 * Created on 21/10/2017
 */
@Slf4j
@RequiredArgsConstructor
public class ServiceProxyProvider extends PropertySourcesPlaceholderConfigurer implements BeanDefinitionRegistryPostProcessor {

    @NonNull
    private ServiceDiscovery serviceDiscovery;

    @Override
    public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
        log.info("register beans");
        ClassPathScanningCandidateComponentProvider scanner = getScanner();
        scanner.addIncludeFilter(new AnnotationTypeFilter(RPCService.class));

        for (String basePackage: getBasePackages()) {
            Set<BeanDefinition> candidateComponents = scanner
                    .findCandidateComponents(basePackage);
            for (BeanDefinition candidateComponent : candidateComponents) {
                if (candidateComponent instanceof AnnotatedBeanDefinition) {
                    AnnotatedBeanDefinition beanDefinition = (AnnotatedBeanDefinition) candidateComponent;
                    AnnotationMetadata annotationMetadata = beanDefinition.getMetadata();

                    BeanDefinitionHolder holder = createBeanDefinition(annotationMetadata);
                    BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);
                }
            }
        }
    }

    private ClassPathScanningCandidateComponentProvider getScanner() {
        return new ClassPathScanningCandidateComponentProvider(false) {

            @Override
            protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) {
                if (beanDefinition.getMetadata().isIndependent()) {

                    if (beanDefinition.getMetadata().isInterface()
                            && beanDefinition.getMetadata().getInterfaceNames().length == 1
                            && Annotation.class.getName().equals(beanDefinition.getMetadata().getInterfaceNames()[0])) {

                        try {
                            Class<?> target = Class.forName(beanDefinition.getMetadata().getClassName());
                            return !target.isAnnotation();
                        } catch (Exception ex) {

                            log.error("Could not load target class: {}, {}",
                                    beanDefinition.getMetadata().getClassName(), ex);
                        }
                    }
                    return true;
                }
                return false;
            }
        };
    }

    private BeanDefinitionHolder createBeanDefinition(AnnotationMetadata annotationMetadata) {
        String className = annotationMetadata.getClassName();
        log.info("Creating bean definition for class: {}", className);

        BeanDefinitionBuilder definition = BeanDefinitionBuilder.genericBeanDefinition(ProxyFactoryBean.class);
        String beanName = StringUtils.uncapitalize(className.substring(className.lastIndexOf('.') + 1));

        definition.addPropertyValue("type", className);
        definition.addPropertyValue("serviceDiscovery", serviceDiscovery);

        return new BeanDefinitionHolder(definition.getBeanDefinition(), beanName);
    }

    private Set<String> getBasePackages() {
        String[] basePackages = getMainClass().getAnnotation(EnableRPCClients.class).basePackages();
        Set set = new HashSet<>();
        Collections.addAll(set, basePackages);
        return set;
    }

    private Class<?> getMainClass() {
        for (final Map.Entry<String, String> entry : System.getenv().entrySet()) {
            if (entry.getKey().startsWith("JAVA_MAIN_CLASS")) {
                String mainClass = entry.getValue();
                log.debug("Main class: {}", mainClass);
                try {
                    return Class.forName(mainClass);
                } catch (ClassNotFoundException e) {
                    throw new IllegalStateException("Cannot determine main class.");
                }
            }
        }
        throw new IllegalStateException("Cannot determine main class.");
    }

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {

    }
}

對應的ProxyBeanFactory:

/**
 * FactoryBean for service proxy
 *
 * @author hongbin
 * Created on 24/10/2017
 */
@Slf4j
@Data
public class ProxyFactoryBean implements FactoryBean<Object> {
    private Class<?> type;

    private ServiceDiscovery serviceDiscovery;

    @SuppressWarnings("unchecked")
    @Override
    public Object getObject() throws Exception {
        return Proxy.newProxyInstance(type.getClassLoader(), new Class<?>[]{type}, this::doInvoke);
    }

    @Override
    public Class<?> getObjectType() {
        return this.type;
    }

    @Override
    public boolean isSingleton() {
        return true;
    }

    private Object doInvoke(Object proxy, Method method, Object[] args) throws Throwable {
        // TODO:這裏處理服務發現、負載均衡、網絡通信等邏輯
    }
}

就這樣, 我們實現了客戶端啓動時的掃包、創建代理的過程,接下來要做的事情就只是填充代理的邏輯了。 完整代碼請看我的github

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章