Spring Boot自动配置

1. Spring Boot自动配置简介

  Spring Boot会根据类路径中的jar包、类,为jar包里的类自动配置,这样可以极大的减少配置的数量。简单点说就是它会根据定义在classpath下的类,自动的给你生成一些Bean,并加载到Spring的Context中。自动配置充分的利用了spring 4.0的条件化配置特性,能够自动配置特定的Spring bean,用来启动某项特性。Spring Boot关于自动配置的源码在spring-boot-autoconfigure-1.3.0.x.jar内。

2. Spring Boot自动配置使用案例

  • 启动本地redis,端口为6379,通过keys * 查看
1 "ttlMap"
2 "test1"
  • 创建service,直接注入StringRedisTemplate
1 @Service
2 public class RedisExample {
3     @Autowired
4     StringRedisTemplate stringRedisTemplate;
5 
6     public Set<String> keys() {
7         return stringRedisTemplate.keys("*");
8     }
9 }    
  • 创建测试类
 1 @RunWith(SpringRunner.class)
 2 @SpringBootTest
 3 public class SpringbootRedisDemoApplicationTests {
 4     @Autowired
 5     RedisExample redisExample;
 6 
 7     @Test
 8     public void testRedis() {
 9         redisExample.keys().forEach(key->System.out.println(key));
10     }
11 }
  • 输出结果为
1 ttlMap
2 test1

  从上面的步骤可以看出,没有任何地方配置读取Redis客户端、Server、Port等信息,那么StringRedisTemplate如何知道怎么访问Redis呢?如果在application.properties中直接配置spring.redis.host, spring.redis.port等信息就会生效。StringRedisTemplate是如何读取配置的呢?答案就是“自动配置”。

3.条件注解(@Conditional)

  Spring Boot自动配置的实现就是基于Spring 4.x 根据条件的配置来创建Bean的能力。

  @Conditional根据满足某一个特定条件创建一个特定的Bean。比如当一个jar包在一个路径下的时候,自动配置一个或多个Bean;或者只有某个Bean被创建才会创建另外一个Bean。

  例子:根据运行的系统不同,创建不同的Bean完成不同的功能。比如是window系统则自动注入WindowsListService。

LinuxCondition.java

 1 package com.suns.springboot.ch3.conditional;
 2 
 3 import org.springframework.context.annotation.Condition;
 4 import org.springframework.context.annotation.ConditionContext;
 5 import org.springframework.core.type.AnnotatedTypeMetadata;
 6 
 7 public class LinuxCondition implements Condition {
 8     public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata annotatedTypeMetadata) {
 9         return conditionContext.getEnvironment().getProperty("os.name").contains("Linux");
10     }
11 }

 

WindowsCondition.java

 1 package com.suns.springboot.ch3.conditional;
 2 
 3 import org.springframework.context.annotation.Condition;
 4 import org.springframework.context.annotation.ConditionContext;
 5 import org.springframework.core.type.AnnotatedTypeMetadata;
 6 
 7 public class WindowsCondition implements Condition {
 8     public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata annotatedTypeMetadata) {
 9         return conditionContext.getEnvironment().getProperty("os.name").contains("Windows");
10     }
11 }

 

ListService.java

1 package com.suns.springboot.ch3.conditional;
2 
3 public interface ListService {
4     String showListCmd();
5 }

 

LinuxListService.java

1 package com.suns.springboot.ch3.conditional;
2 
3 public class LinuxListService implements ListService {
4     public String showListCmd() {
5         return "ls";
6     }
7 }

 

WindowsListService.java

 1 package com.suns.springboot.ch3.conditional;
 2 
 3 import org.springframework.context.annotation.Condition;
 4 import org.springframework.context.annotation.ConditionContext;
 5 import org.springframework.core.type.AnnotatedTypeMetadata;
 6 
 7 public class WindowsCondition implements Condition {
 8     public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata annotatedTypeMetadata) {
 9         return conditionContext.getEnvironment().getProperty("os.name").contains("Windows");
10     }
11 }

 

ConditionConfig.java

  如果WindowsCondition里面的match方法返回true则新建一个WindowsListService并通过@Bean注入到容器,如果LinuxCondition里面的match方法返回true则新建一个LinuxListService并通过@Bean注入到容器

 1 package com.suns.springboot.ch3.conditional;
 2 
 3 import org.springframework.context.annotation.Bean;
 4 import org.springframework.context.annotation.Conditional;
 5 import org.springframework.context.annotation.Configuration;
 6 
 7 @Configuration
 8 public class ConditionConfig {
 9 
10     @Bean
11     @Conditional(WindowsCondition.class)
12     public ListService windowsListService() {
13         return new WindowsListService();
14     }
15 
16     @Bean
17     @Conditional(LinuxCondition.class)
18     public ListService linuxListService() {
19         return new LinuxListService();
20     }
21 }

 

Main.java

 1 package com.suns.springboot.ch3.conditional;
 2 
 3 import org.springframework.context.annotation.AnnotationConfigApplicationContext;
 4 
 5 public class Main {
 6     public static void main(String[] args) {
 7         AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(ConditionConfig.class);
 8         ListService listService = context.getBean(ListService.class);
 9         System.out.println(context.getEnvironment().getProperty("os.name") + "系统下的列表命令为: " + listService.showListCmd());
10     }
11 }

 

4.组合注解与元注解 

  所谓元注解其实就是可以注解到别的注解上的注解,被注解的注解称之为组合注解,组合注解具备元注解的功能。如果对注解也不清楚可以参考我另外一篇博客:Java注解

  在SpringBoot的开发过程中,配置类(config)会经常使用@Configuration和@ComponentScan,我们可以把这2个注解组成一个组合注解。

 

SunsConfiguration.java

 1 package com.suns.springboot.ch3.combination_annotation;
 2 
 3 import org.springframework.context.annotation.ComponentScan;
 4 import org.springframework.context.annotation.Configuration;
 5 
 6 import java.lang.annotation.*;
 7 
 8 @Target(ElementType.TYPE)
 9 @Retention(RetentionPolicy.RUNTIME)
10 @Documented
11 @Configuration
12 @ComponentScan
13 public @interface SunsConfiguration {
14 
15     String[] value() default {};
16 }

DemoService.java

 1 package com.suns.springboot.ch3.combination_annotation;
 2 
 3 import org.springframework.stereotype.Service;
 4 
 5 @Service
 6 public class DemoService {
 7 
 8     public void outputResult() {
 9         System.out.println("从组合注解配置照样获得的bean");
10     }
11 }

 

DemoConfig.java

1 package com.suns.springboot.ch3.combination_annotation;
2 
3 @SunsConfiguration("com.suns.springboot.ch3.combination_annotation")
4 public class DemoConfig {
5 }

 

Main.java

 1 package com.suns.springboot.ch3.combination_annotation;
 2 
 3 import org.springframework.context.annotation.AnnotationConfigApplicationContext;
 4 
 5 public class Main {
 6     public static void main(String[] args) {
 7         AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(DemoConfig.class);
 8         DemoService demoService = context.getBean(DemoService.class);
 9         demoService.outputResult();
10 
11         context.close();
12     }
13 
14 }

 

5. Spring Boot自动化配置详解

   Spring Boot最重要的注解@SpringBootApplication,它其实是一个组合元注解(组合了SpringBootConfiguration和EnableAutoConfiguration、ComponentScan):

 1 @Target({ElementType.TYPE})
 2 @Retention(RetentionPolicy.RUNTIME)
 3 @Documented
 4 @Inherited
 5 @SpringBootConfiguration
 6 @EnableAutoConfiguration
 7 @ComponentScan(
 8     excludeFilters = {@Filter(
 9     type = FilterType.CUSTOM,
10     classes = {TypeExcludeFilter.class}
11 ), @Filter(
12     type = FilterType.CUSTOM,
13     classes = {AutoConfigurationExcludeFilter.class}
14 )}
15 )
16 public @interface SpringBootApplication {
17     ......
18 }

  

  @SpringBootConfiguration是Spring Boot配置类,进入SpringBootConfiguration注解,有个@Configuration是来标识某个类是配置类

1 @Target({ElementType.TYPE})
2 @Retention(RetentionPolicy.RUNTIME)
3 @Documented
4 @Configuration
5 public @interface SpringBootConfiguration {
6 }

  

  其中@EnableAutoConfiguration是Spring Boot自动化配置原理的核心,是开启自动配置功能

 1 @Target({ElementType.TYPE})
 2 @Retention(RetentionPolicy.RUNTIME)
 3 @Documented
 4 @Inherited
 5 @AutoConfigurationPackage
 6 @Import({EnableAutoConfigurationImportSelector.class})
 7 public @interface EnableAutoConfiguration {
 8     Class<?>[] exclude() default {};
 9 
10     String[] excludeName() default {};
11 }

  

  @AutoConfigurationPackage是自动配置包,是找到@SpringBootApplication主类当前路径(见下图)。其中@Import是向容器中导入组件,能够导入Configuration配置类,也能够导入实现了ImportSelector接口的类,还有动态注册Bean。在Registrar类中,获取到@SpringBootApplication主类路径

1 @Target({ElementType.TYPE})
2 @Retention(RetentionPolicy.RUNTIME)
3 @Documented
4 @Inherited
5 @Import({Registrar.class})
6 public @interface AutoConfigurationPackage {
7 }

 

  EnableAutoConfigurationImportSelector是将所有需要导入组件以全类名的方式返回,这些组件就会添加到Spring容器中。selectImports方法中获取configurations。List configurations = getCandidateConfigurations(metadata,attributes);

public class EnableAutoConfigurationImportSelector implements DeferredImportSelector, BeanClassLoaderAware, ResourceLoaderAware, BeanFactoryAware, EnvironmentAware {
    private ConfigurableListableBeanFactory beanFactory;
    private Environment environment;
    private ClassLoader beanClassLoader;
    private ResourceLoader resourceLoader;

    public EnableAutoConfigurationImportSelector() {
    }

    public String[] selectImports(AnnotationMetadata metadata) {
        try {
            AnnotationAttributes attributes = this.getAttributes(metadata);
            List<String> configurations = this.getCandidateConfigurations(metadata, attributes);
            configurations = this.removeDuplicates(configurations);
            Set<String> exclusions = this.getExclusions(metadata, attributes);
            configurations.removeAll(exclusions);
            configurations = this.sort(configurations);
            this.recordWithConditionEvaluationReport(configurations, exclusions);
            return (String[])configurations.toArray(new String[configurations.size()]);
        } catch (IOException var5) {
            throw new IllegalStateException(var5);
        }
    }

    .....
}

  

  getCandidateConfigurations方法获取configurations的方法是:List configurations = SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(), getBeanClassLoader());

 1 public class EnableAutoConfigurationImportSelector implements DeferredImportSelector, BeanClassLoaderAware, ResourceLoaderAware, BeanFactoryAware, EnvironmentAware {
 2    
 3    ......     
 4 
 5     protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
 6         return SpringFactoriesLoader.loadFactoryNames(this.getSpringFactoriesLoaderFactoryClass(), this.getBeanClassLoader());
 7     }
 8 
 9     ......
10 }

  

  我们看下loadFactoryNames方法,从这里可以看出Spring Boot在启动的时候从类路径下加载所有的META-INF/spring.factories,包括其他starter里面的spring.factories(比如mybatis-plus-boot-starter)。并从中获取org.springframework.boot.autoconfigure.EnableAutoConfiguration值,将这些值注入到Spring容器中。

 1 public static List<String> loadFactoryNames(Class<?> factoryClass, ClassLoader classLoader) {
 2         String factoryClassName = factoryClass.getName();
 3 
 4         try {
 5             Enumeration<URL> urls = classLoader != null ? classLoader.getResources("META-INF/spring.factories") : ClassLoader.getSystemResources("META-INF/spring.factories");
 6             ArrayList result = new ArrayList();
 7 
 8             while(urls.hasMoreElements()) {
 9                 URL url = (URL)urls.nextElement();
10                 Properties properties = PropertiesLoaderUtils.loadProperties(new UrlResource(url));
11                 String factoryClassNames = properties.getProperty(factoryClassName);
12                 result.addAll(Arrays.asList(StringUtils.commaDelimitedListToStringArray(factoryClassNames)));
13             }
14 
15             return result;
16         } catch (IOException var8) {
17             throw new IllegalArgumentException("Unable to load [" + factoryClass.getName() + "] factories from location [" + "META-INF/spring.factories" + "]", var8);
18         }
19     }

 

  我们看一下spring-boot-autoconfigure-1.5.4.RELEASE.jar中的META-INF/spring.factories

  1 # Initializers
  2 org.springframework.context.ApplicationContextInitializer=\
  3 org.springframework.boot.autoconfigure.logging.AutoConfigurationReportLoggingInitializer
  4 
  5 # Application Listeners
  6 org.springframework.context.ApplicationListener=\
  7 org.springframework.boot.autoconfigure.BackgroundPreinitializer
  8 
  9 # Auto Configure
 10 org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
 11 org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,\
 12 org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,\
 13 org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration,\
 14 org.springframework.boot.autoconfigure.MessageSourceAutoConfiguration,\
 15 org.springframework.boot.autoconfigure.PropertyPlaceholderAutoConfiguration,\
 16 org.springframework.boot.autoconfigure.batch.BatchAutoConfiguration,\
 17 org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration,\
 18 org.springframework.boot.autoconfigure.cassandra.CassandraAutoConfiguration,\
 19 org.springframework.boot.autoconfigure.cloud.CloudAutoConfiguration,\
 20 org.springframework.boot.autoconfigure.context.ConfigurationPropertiesAutoConfiguration,\
 21 org.springframework.boot.autoconfigure.dao.PersistenceExceptionTranslationAutoConfiguration,\
 22 org.springframework.boot.autoconfigure.data.cassandra.CassandraDataAutoConfiguration,\
 23 org.springframework.boot.autoconfigure.data.cassandra.CassandraRepositoriesAutoConfiguration,\
 24 org.springframework.boot.autoconfigure.data.elasticsearch.ElasticsearchAutoConfiguration,\
 25 org.springframework.boot.autoconfigure.data.elasticsearch.ElasticsearchDataAutoConfiguration,\
 26 org.springframework.boot.autoconfigure.data.elasticsearch.ElasticsearchRepositoriesAutoConfiguration,\
 27 org.springframework.boot.autoconfigure.data.jpa.JpaRepositoriesAutoConfiguration,\
 28 org.springframework.boot.autoconfigure.data.mongo.MongoDataAutoConfiguration,\
 29 org.springframework.boot.autoconfigure.data.mongo.MongoRepositoriesAutoConfiguration,\
 30 org.springframework.boot.autoconfigure.data.solr.SolrRepositoriesAutoConfiguration,\
 31 org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration,\
 32 org.springframework.boot.autoconfigure.data.rest.RepositoryRestMvcAutoConfiguration,\
 33 org.springframework.boot.autoconfigure.data.web.SpringDataWebAutoConfiguration,\
 34 org.springframework.boot.autoconfigure.freemarker.FreeMarkerAutoConfiguration,\
 35 org.springframework.boot.autoconfigure.gson.GsonAutoConfiguration,\
 36 org.springframework.boot.autoconfigure.h2.H2ConsoleAutoConfiguration,\
 37 org.springframework.boot.autoconfigure.hateoas.HypermediaAutoConfiguration,\
 38 org.springframework.boot.autoconfigure.hazelcast.HazelcastAutoConfiguration,\
 39 org.springframework.boot.autoconfigure.hazelcast.HazelcastJpaDependencyAutoConfiguration,\
 40 org.springframework.boot.autoconfigure.integration.IntegrationAutoConfiguration,\
 41 org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration,\
 42 org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration,\
 43 org.springframework.boot.autoconfigure.jdbc.JndiDataSourceAutoConfiguration,\
 44 org.springframework.boot.autoconfigure.jdbc.XADataSourceAutoConfiguration,\
 45 org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration,\
 46 org.springframework.boot.autoconfigure.jms.JmsAutoConfiguration,\
 47 org.springframework.boot.autoconfigure.jmx.JmxAutoConfiguration,\
 48 org.springframework.boot.autoconfigure.jms.JndiConnectionFactoryAutoConfiguration,\
 49 org.springframework.boot.autoconfigure.jms.activemq.ActiveMQAutoConfiguration,\
 50 org.springframework.boot.autoconfigure.jms.artemis.ArtemisAutoConfiguration,\
 51 org.springframework.boot.autoconfigure.jms.hornetq.HornetQAutoConfiguration,\
 52 org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration,\
 53 org.springframework.boot.autoconfigure.groovy.template.GroovyTemplateAutoConfiguration,\
 54 org.springframework.boot.autoconfigure.jersey.JerseyAutoConfiguration,\
 55 org.springframework.boot.autoconfigure.jooq.JooqAutoConfiguration,\
 56 org.springframework.boot.autoconfigure.liquibase.LiquibaseAutoConfiguration,\
 57 org.springframework.boot.autoconfigure.mail.MailSenderAutoConfiguration,\
 58 org.springframework.boot.autoconfigure.mail.MailSenderValidatorAutoConfiguration,\
 59 org.springframework.boot.autoconfigure.mobile.DeviceResolverAutoConfiguration,\
 60 org.springframework.boot.autoconfigure.mobile.DeviceDelegatingViewResolverAutoConfiguration,\
 61 org.springframework.boot.autoconfigure.mobile.SitePreferenceAutoConfiguration,\
 62 org.springframework.boot.autoconfigure.mongo.embedded.EmbeddedMongoAutoConfiguration,\
 63 org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration,\
 64 org.springframework.boot.autoconfigure.mustache.MustacheAutoConfiguration,\
 65 org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration,\
 66 org.springframework.boot.autoconfigure.reactor.ReactorAutoConfiguration,\
 67 org.springframework.boot.autoconfigure.security.SecurityAutoConfiguration,\
 68 org.springframework.boot.autoconfigure.security.SecurityFilterAutoConfiguration,\
 69 org.springframework.boot.autoconfigure.security.FallbackWebSecurityAutoConfiguration,\
 70 org.springframework.boot.autoconfigure.security.oauth2.OAuth2AutoConfiguration,\
 71 org.springframework.boot.autoconfigure.sendgrid.SendGridAutoConfiguration,\
 72 org.springframework.boot.autoconfigure.session.SessionAutoConfiguration,\
 73 org.springframework.boot.autoconfigure.social.SocialWebAutoConfiguration,\
 74 org.springframework.boot.autoconfigure.social.FacebookAutoConfiguration,\
 75 org.springframework.boot.autoconfigure.social.LinkedInAutoConfiguration,\
 76 org.springframework.boot.autoconfigure.social.TwitterAutoConfiguration,\
 77 org.springframework.boot.autoconfigure.solr.SolrAutoConfiguration,\
 78 org.springframework.boot.autoconfigure.velocity.VelocityAutoConfiguration,\
 79 org.springframework.boot.autoconfigure.thymeleaf.ThymeleafAutoConfiguration,\
 80 org.springframework.boot.autoconfigure.transaction.TransactionAutoConfiguration,\
 81 org.springframework.boot.autoconfigure.transaction.jta.JtaAutoConfiguration,\
 82 org.springframework.boot.autoconfigure.web.DispatcherServletAutoConfiguration,\
 83 org.springframework.boot.autoconfigure.web.EmbeddedServletContainerAutoConfiguration,\
 84 org.springframework.boot.autoconfigure.web.ErrorMvcAutoConfiguration,\
 85 org.springframework.boot.autoconfigure.web.HttpEncodingAutoConfiguration,\
 86 org.springframework.boot.autoconfigure.web.HttpMessageConvertersAutoConfiguration,\
 87 org.springframework.boot.autoconfigure.web.MultipartAutoConfiguration,\
 88 org.springframework.boot.autoconfigure.web.ServerPropertiesAutoConfiguration,\
 89 org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration,\
 90 org.springframework.boot.autoconfigure.websocket.WebSocketAutoConfiguration,\
 91 org.springframework.boot.autoconfigure.websocket.WebSocketMessagingAutoConfiguration
 92 
 93 # Template availability providers
 94 org.springframework.boot.autoconfigure.template.TemplateAvailabilityProvider=\
 95 org.springframework.boot.autoconfigure.freemarker.FreeMarkerTemplateAvailabilityProvider,\
 96 org.springframework.boot.autoconfigure.mustache.MustacheTemplateAvailabilityProvider,\
 97 org.springframework.boot.autoconfigure.groovy.template.GroovyTemplateAvailabilityProvider,\
 98 org.springframework.boot.autoconfigure.thymeleaf.ThymeleafTemplateAvailabilityProvider,\
 99 org.springframework.boot.autoconfigure.velocity.VelocityTemplateAvailabilityProvider,\
100 org.springframework.boot.autoconfigure.web.JspTemplateAvailabilityProvider

  

  最终返回的Configuration如下:

 

  这里我们再自己跟进去看下前面例子中使用到的Redis到底是如何完成自动配置。我们找到RedisAutoConfiguration,我们看到这里使用了们前面讲到的条件注解,@ConditionOnClass表示classpath中拥有这些类时才会构建这个bean,在我们在maven引入Redis相关依赖包后,Spring Boot就会把RedisAutoConfiguration注入到Spring容器;@ConditionOnMissingBean表示不存在这个Bean的时候,Spring Boot就会Spring Boot就会把RedisProperties注入到Spring容器。

 1 @Configuration
 2 @ConditionalOnClass({JedisConnection.class, RedisOperations.class, Jedis.class})
 3 @EnableConfigurationProperties
 4 public class RedisAutoConfiguration {
 5     public RedisAutoConfiguration() {
 6     }
 7 
 8     @Bean(
 9         name = {"org.springframework.autoconfigure.redis.RedisProperties"}
10     )
11     @ConditionalOnMissingBean
12     public RedisProperties redisProperties() {
13         return new RedisProperties();
14     }
15   ...  
16 }

  

  接着我们看下RedisProperties,这里就有默认加载Redis的IP、端口等信息。到这里我们Redis相关的自动配置就已经配置完成。

 1 @ConfigurationProperties(
 2     prefix = "spring.redis"
 3 )
 4 public class RedisProperties {
 5     private int database = 0;
 6     private String host = "localhost";
 7     private String password;
 8     private int port = 6379;
 9     private int timeout;
10     private RedisProperties.Pool pool;
11     private RedisProperties.Sentinel sentinel;
12 
13     public RedisProperties() {
14     }
15     ...
16 
17 }

 

附件

在spring-boot-autoconfigure-1.3.0.x.jar的org.springframework.boot.autoconfigure.condition下常用的条件注解
名称 作用
@ConditionalOnBean 当容器里有指定的Bean的条件下
@ConditionalOnClass 当类路径下有指定的类条件下
@ConditionalOnExpression 基于SpEl表达式作为判断条件
@ConditionalOnJava 基于JVM版本作为判断条件
@ConditionalOnJndi 在JNDI存在的条件下查找指定的位置
@ConditionalOnMissingBean 当容器里没有指定Bean的情况下
@ConditionalOnMissingClass 当类路径下没有指定的类的条件下
@ConditionalOnNotWebApplication 当前项目不是Web项目的条件下
@ConditionalOnProperty 指定的属性是否有指定的值
@ConditionalOnResource 类路径是否有指定的值
@ConditionalOnSingleCandidate 当指定Bean在容器中只有一个,或者虽然有多个但是指定首选的Bean
@ConditionalOnWebApplication 当前项目为Web项目的条件下

 

posted @ 2020-02-14 20:24  KeSuns  阅读(892)  评论(0编辑  收藏  举报