当前位置:   article > 正文

Spring4.3整合dubbo纯注解配置以及解决dubbo中@Reference为null的问题_dubbo 消费者配置找不到类 @reference

dubbo 消费者配置找不到类 @reference

前言

环境为jdk1.8,maven 3.3.3 , tomcat 8,dubbo 2.5.3

本文中会提到AnnotationBean,该bean在dubbo 2.5.7开始被@Deprecated,替代的可以使用@EnableDubbo注解,在2.5.8开始可以使用@DubboComponentScan注解

接口

public interface IUserService {  
    User selectUser(int id);
}
  • 1
  • 2
  • 3

注意User实体需要序列化实现Serializable接口。接口和实体抽出为公共jar包,供服务端和客户端使用。

服务端配置

实现类
package com.chengli.dubbo.service;
import com.alibaba.dubbo.config.annotation.Service;
import javax.annotation.PostConstruct;
import java.util.ArrayList;
import java.util.List;

/**
 * @Author: chengli
 * @Date: 2018/8/16 17:41
 */
 
/**注意这里的Service注解是dubbo的注解*/
@Service  
public class UserService implements IUserService {
    private final List<User> list = new ArrayList() {{
        add(new User(1, "张三", 10));
        add(new User(2, "李四", 23));
        add(new User(3, "王五", 34));
        add(new User(4, "赵六", 43));
        add(new User(5, "田七", 36));
        add(new User(6, "黄八", 52));
    }};

    @Override
    public User selectUser(int id) {
        return list.stream().findAny().get();
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
dubbo配置
package com.chengli.dubbo.env;

import com.alibaba.dubbo.config.ApplicationConfig;
import com.alibaba.dubbo.config.ProtocolConfig;
import com.alibaba.dubbo.config.RegistryConfig;
import com.alibaba.dubbo.config.spring.AnnotationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * @Author: chengli
 * @Date: 2018/8/18 17:39
 */
@Configuration
public class DubboConfig {

    @Bean
    public ApplicationConfig applicationConfig(){
        ApplicationConfig applicationConfig = new ApplicationConfig();
        applicationConfig.setName("spring-dubbo-server");
        return applicationConfig;
    }

    @Bean
    public AnnotationBean annotationBean() {
        AnnotationBean annotationBean = new AnnotationBean();
        annotationBean.setPackage("com.chengli.dubbo.service");
        return annotationBean;
    }


    @Bean
    public RegistryConfig registryConfig(){
        RegistryConfig registryConfig = new RegistryConfig();
        registryConfig.setAddress("N/A");
        return registryConfig;
    }

	/**默认就是dubbo协议,可以不用配置此Bean*/
    @Bean    
    public ProtocolConfig protocolConfig(){
        ProtocolConfig protocolConfig = new ProtocolConfig();
        protocolConfig.setName("dubbo");
        protocolConfig.setPort(20880);
        return protocolConfig;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
spring配置

该测试中没有使用数据库,服务端从list中取出数据返回,如果使用数据库,加上以下配置(此示例中可以省略)。

package com.chengli.dubbo.env;

import org.apache.ibatis.datasource.pooled.PooledDataSource;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.mapper.MapperScannerConfigurer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

import javax.sql.DataSource;

/**
 * @Author: chengli
 * @Date: 2018/8/16 17:41
 */
@Configuration
@ComponentScan(basePackages = {"com.chengli.dubbo.service"})
public class RootConfig {

    @Bean
    public DataSource createDataSource() {
        PooledDataSource dataSource = new PooledDataSource();
        dataSource.setDriver("com.mysql.jdbc.Driver");
        dataSource.setUrl("jdbc:mysql://localhost:3306/mybatis");
        dataSource.setUsername("root");
        dataSource.setPassword("000000");
        return dataSource;
    }


    @Bean(name = "sqlSessionFactory")
    public SqlSessionFactoryBean createSqlSessionFactory(DataSource dataSource) {
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(dataSource);
        return sqlSessionFactoryBean;
    }

    @Bean
    public MapperScannerConfigurer createMapperScannerConfigurer() {
        MapperScannerConfigurer mapperScannerConfigurer = new MapperScannerConfigurer();
        mapperScannerConfigurer.setBasePackage("com.chengli.dubbo.mapper");
        mapperScannerConfigurer.setSqlSessionFactoryBeanName("sqlSessionFactory");
        return mapperScannerConfigurer;
    }

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
配置加载spring配置
package com.chengli.dubbo.env;

import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;

public class MyApplicationContextInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {

    protected Class<?>[] getRootConfigClasses() {
        return new Class[]{DubboConfig.class, RootConfig.class};
    }

    protected Class<?>[] getServletConfigClasses() {
        return new Class[0];
    }

    protected String[] getServletMappings() {
        return new String[]{"/"};
    }

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

客户端配置

dubbo配置
package com.chengli.dubbo.env;

import com.alibaba.dubbo.config.ApplicationConfig;
import com.alibaba.dubbo.config.RegistryConfig;
import com.alibaba.dubbo.config.spring.AnnotationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * @Author: chengli
 * @Date: 2018/8/18 18:19
 */
@Configuration
public class DubboConfig {

    @Bean
    public ApplicationConfig applicationConfig() {
        ApplicationConfig applicationConfig = new ApplicationConfig();
        applicationConfig.setName("spring-dubbo-client");
        return applicationConfig;
    }

    @Bean
    public AnnotationBean annotationBean() {
        AnnotationBean annotationBean = new AnnotationBean();
        annotationBean.setPackage("com.chengli.dubbo.controller");
        return annotationBean;
    }

    @Bean
    public RegistryConfig registryConfig(){
        RegistryConfig registryConfig = new RegistryConfig();
        registryConfig.setAddress("N/A");
        return registryConfig;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
spring mvc配置
package com.chengli.dubbo.env;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.web.servlet.config.annotation.ContentNegotiationConfigurer;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.ViewResolverRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;

/**
 * @Author: chengli
 * @Date: 2018/8/16 17:41
 */
@Configuration
@EnableWebMvc
@ComponentScan(basePackages = {"com.chengli.dubbo.controller"})
public class MvcConfig extends WebMvcConfigurerAdapter {

    @Override
    public void configureViewResolvers(ViewResolverRegistry registry) {
        registry.jsp("/WEB-INF/jsp/", ".jsp");
    }

    @Override
    public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
        configurer.defaultContentType(MediaType.TEXT_HTML)
                .mediaType(".json", MediaType.APPLICATION_JSON)
                .mediaType(".html",MediaType.TEXT_HTML);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
加载spring容器和springmvc容器
package com.chengli.dubbo.env;

import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;

/**
 * @Author: chengli
 * @Date: 2018/8/16 17:35
 */
public class MyApplicationContextInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {

    protected Class<?>[] getRootConfigClasses() {
        return new Class[]{};
    }

/** 这里的配置非常重要,DubboConfig.class, MvcConfig.class要么都配置在springmvc容器中,
 *  要么都配置在spring容器中,否则会出现reference空指针问题
 */
    protected Class<?>[] getServletConfigClasses() {
        return new Class[]{DubboConfig.class, MvcConfig.class};
    }

    protected String[] getServletMappings() {
        return new String[]{"/"};
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
reference配置
package com.chengli.dubbo.controller;

import com.alibaba.dubbo.config.annotation.Reference;
import com.chengli.dubbo.service.IUserService;
import com.chengli.dubbo.service.User;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;

@Controller
public class IndexController {

/**注意这里的注解是dubbo的注解,这里没有使用注册中心,为直连方式*/
    @Reference(url = "dubbo://localhost:20880", interfaceClass = IUserService.class, check = true)
    private IUserService userService;

    @RequestMapping("/")
    public ModelAndView index() {
        ModelAndView mv = new ModelAndView("success");
        User user = userService.selectUser(1);
        System.out.println("user : " + user);
        mv.addObject("user", user);
        return mv;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25

以上就是dubbo纯注解配置,配置非常简单,只是把原来xml中的配置,全部都转换成java类表示而已。上面只是引子,实际想说的是reference为空的问题。刚开始测试的时候一直有这个问题。网上查了很多资料,但是将的都不是很清楚。这里作一个深入的剖析。

@Reference空指针问题

具体来说就是在客户端, private IUserService userService; userService为null,当时出现此异常时客户端的配置如下:

public class MyApplicationContextInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {

    protected Class<?>[] getRootConfigClasses() {
        return new Class[]{DubboConfig.class};
    }

    protected Class<?>[] getServletConfigClasses() {
        return new Class[]{ MvcConfig.class};
    }

    protected String[] getServletMappings() {
        return new String[]{"/"};
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

DubboConfig使用spring父容器加载,MvcConfig使用springmvc容器加载,实际上spring官网也是推荐我们在使用的时候用父子容器的配置。但是这里为什么就出现问题了呢?也许我们搞清楚几个问题,就会清楚:

  • spring父容器和springmvc子容器的加载顺序是怎么样的?

  • dubbo的注解是何时生效的?
    带着这两个问题,我们继续往下讲:

    */
    public abstract class AbstractDispatcherServletInitializer extends AbstractContextLoaderInitializer {

    /**
     * The default servlet name. Can be customized by overriding {@link #getServletName}.
     */
    public static final String DEFAULT_SERVLET_NAME = "dispatcher";
    
    
    /**从这里的代码可以看出,会先初始化spring容器,再初始化springmvc容器*/
    @Override
    public void onStartup(ServletContext servletContext) throws ServletException {
    	/**这一行代码实际上就是向web.xml的容器中一样,向servetContext中注册一个ContextLoaderListener,listener中持有rootAppContext父容器*/
    	super.onStartup(servletContext);
    	/**这里注册DispatchServlet,它持有servletAppContext子容器*/
    	registerDispatcherServlet(servletContext);
    }
    
    ......
    ......
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    }

可能说到这里还会有点晕乎?这两行代码只不过是分别向ServletContext 容器中分别放了一个listener和一个servlet而已,怎么能证明两个容器的加载顺序? 别急
还记得DispatcherServlet中配置的loadOnStartup参数吗?我们看一下它的注释:

/**
  * Sets the <code>loadOnStartup</code> priority on the Servlet
  * represented by this dynamic ServletRegistration.
  *
  * <p>A <tt>loadOnStartup</tt> value of greater than or equal to
  * zero indicates to the container the initialization priority of
  * the Servlet. In this case, the container must instantiate and
  * initialize the Servlet during the initialization phase of the
  * ServletContext, that is, after it has invoked all of the
  * ServletContextListener objects configured for the ServletContext
  * at their {@link ServletContextListener#contextInitialized}
  * method.
  *
  * <p>If <tt>loadOnStartup</tt> is a negative integer, the container
  * is free to instantiate and initialize the Servlet lazily.
  *
  * <p>The default value for <tt>loadOnStartup</tt> is <code>-1</code>.
  *
  * <p>A call to this method overrides any previous setting.
  *
  * @param loadOnStartup the initialization priority of the Servlet
  *
  * @throws IllegalStateException if the ServletContext from which
  * this ServletRegistration was obtained has already been initialized
  */
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25

一般我们在配置DispatcherServlet的时候,会将loadOnStartup的参数配置为1,表示在容器启动时ServletContext初始化的时候就实例化DispatcherServlet并初始化,然而这一切都都在所有listener执行完毕之后。所以两个容器的加载顺序显而易见。
那么还是没有解决我们的问题,现在知道了两个容器的加载顺序又怎么样?我们继续研究:
还记得我们客户端的配置吗?有一个AnnotationBean

   @Bean
    public AnnotationBean annotationBean() {
        AnnotationBean annotationBean = new AnnotationBean();
        annotationBean.setPackage("com.chengli.dubbo.controller");
        return annotationBean;
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

它就是用来扫描所有带有dubbo注解的类,然后做一些小动作来完成dubbo的功能。
我们仔细看一下这个类:

public class AnnotationBean extends AbstractConfig implements DisposableBean, BeanFactoryPostProcessor, BeanPostProcessor, ApplicationContextAware {

   /**省略类实现内容*/
}
  • 1
  • 2
  • 3
  • 4

我们可以看到,该AnnotationBean实现了BeanPostProcessor接口,BeanPostProcessor是干嘛的? 它提供了两个接口:

public interface BeanPostProcessor {

/**bean初始化方法执行之前执行*/
	Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException;
/**bean初始化方法执行之后执行*/
	Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException;

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

在spring容器初始化时,每个Bean初始化时,在初始化方法执行之前会执行BeanPostProcessorpostProcessBeforeInitialization方法,来对Bean做一些自定义操作。这就是dubbo和spring勾搭在一起的地方。
我们看一下AnnotationBeanpostProcessBeforeInitialization做了什么?

public Object postProcessBeforeInitialization(Object bean, String beanName)
            throws BeansException {
        if (! isMatchPackage(bean)) {
            return bean;
        }
        Method[] methods = bean.getClass().getMethods();
        for (Method method : methods) {
            String name = method.getName();
            if (name.length() > 3 && name.startsWith("set")
                    && method.getParameterTypes().length == 1
                    && Modifier.isPublic(method.getModifiers())
                    && ! Modifier.isStatic(method.getModifiers())) {
                try {
                	Reference reference = method.getAnnotation(Reference.class);
                	if (reference != null) {
	                	Object value = refer(reference, method.getParameterTypes()[0]);
	                	if (value != null) {
	                		method.invoke(bean, new Object[] {  });
	                	}
                	}
                } catch (Throwable e) {
                    logger.error("Failed to init remote service reference at method " + name + " in class " + bean.getClass().getName() + ", cause: " + e.getMessage(), e);
                }
            }
        }
        Field[] fields = bean.getClass().getDeclaredFields();
        for (Field field : fields) {
            try {
                if (! field.isAccessible()) {
                    field.setAccessible(true);
                }
                Reference reference = field.getAnnotation(Reference.class);
            	if (reference != null) {
	                Object value = refer(reference, field.getType());
	                if (value != null) {
	                	field.set(bean, value);
	                }
            	}
            } catch (Throwable e) {
            	logger.error("Failed to init remote service reference at filed " + field.getName() + " in class " + bean.getClass().getName() + ", cause: " + e.getMessage(), e);
            }
        }
        return bean;
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44

实际上就是利用java反射机制,对Bean中@Reference标注的属性赋值。其实在这里我们已经可以想到为什么会出现null空指针异常了,因为在如上错误配置的时候,在spring容器所有bean初始化的时候,springmvc容器还没初始化,还没有controller,所以userService为null。

延伸

我们看一下dubbo的@Service注解是如何生效的,实际上@Service是生成代理类,与@Reference不同,代理必须在类初始化之后,所以是在postProcessAfterInitialization方法中完成的。

public Object postProcessAfterInitialization(Object bean, String beanName)
            throws BeansException {
        if (! isMatchPackage(bean)) {
            return bean;
        }
        Service service = bean.getClass().getAnnotation(Service.class);
        if (service != null) {
            ServiceBean<Object> serviceConfig = new ServiceBean<Object>(service);
            if (void.class.equals(service.interfaceClass())
                    && "".equals(service.interfaceName())) {
                if (bean.getClass().getInterfaces().length > 0) {
                    serviceConfig.setInterface(bean.getClass().getInterfaces()[0]);
                } else {
                    throw new IllegalStateException("Failed to export remote service class " + bean.getClass().getName() + ", cause: The @Service undefined interfaceClass or interfaceName, and the service class unimplemented any interfaces.");
                }
            }
            if (applicationContext != null) {
                serviceConfig.setApplicationContext(applicationContext);
                if (service.registry() != null && service.registry().length > 0) {
                    List<RegistryConfig> registryConfigs = new ArrayList<RegistryConfig>();
                    for (String registryId : service.registry()) {
                        if (registryId != null && registryId.length() > 0) {
                            registryConfigs.add((RegistryConfig)applicationContext.getBean(registryId, RegistryConfig.class));
                        }
                    }
                    serviceConfig.setRegistries(registryConfigs);
                }
                if (service.provider() != null && service.provider().length() > 0) {
                    serviceConfig.setProvider((ProviderConfig)applicationContext.getBean(service.provider(),ProviderConfig.class));
                }
                if (service.monitor() != null && service.monitor().length() > 0) {
                    serviceConfig.setMonitor((MonitorConfig)applicationContext.getBean(service.monitor(), MonitorConfig.class));
                }
                if (service.application() != null && service.application().length() > 0) {
                    serviceConfig.setApplication((ApplicationConfig)applicationContext.getBean(service.application(), ApplicationConfig.class));
                }
                if (service.module() != null && service.module().length() > 0) {
                    serviceConfig.setModule((ModuleConfig)applicationContext.getBean(service.module(), ModuleConfig.class));
                }
                if (service.provider() != null && service.provider().length() > 0) {
                    serviceConfig.setProvider((ProviderConfig)applicationContext.getBean(service.provider(), ProviderConfig.class));
                } else {
                    
                }
                if (service.protocol() != null && service.protocol().length > 0) {
                    List<ProtocolConfig> protocolConfigs = new ArrayList<ProtocolConfig>();
                    for (String protocolId : service.registry()) {
                        if (protocolId != null && protocolId.length() > 0) {
                            protocolConfigs.add((ProtocolConfig)applicationContext.getBean(protocolId, ProtocolConfig.class));
                        }
                    }
                    serviceConfig.setProtocols(protocolConfigs);
                }
                try {
                    serviceConfig.afterPropertiesSet();
                } catch (RuntimeException e) {
                    throw (RuntimeException) e;
                } catch (Exception e) {
                    throw new IllegalStateException(e.getMessage(), e);
                }
            }
            serviceConfig.setRef(bean);
            serviceConfigs.add(serviceConfig);
            serviceConfig.export();
        }
        return bean;
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67

本文使用的代码:
https://github.com/lchpersonal/spring-dubbo

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/花生_TL007/article/detail/165756
推荐阅读
相关标签
  

闽ICP备14008679号