spring-core-1.10~1.16

1.10 Classpath扫描和组件管理
1.10.1 @Component和其他模型注解
  • @Repository
    标记数据存储角色的类或接口,如DAO,它的作用之一是自动翻译异常。
  • Controller
  • Service
  • Component
1.10.2 元注解

元注解是一个简单的注解,可以用来定义其他的注解。如@Component就是一个元注解。如@Service就是在它基础上定义的。
元注解还可以被用来创建组合注解。如@RestController就是·@Controller@ResponseBody`的组合注解。

1.10.3 自动扫描并注解bean

想自动扫描,要在@Configuration类上配置@ComponentScan注解,并指定basePackages属性,在多个时用逗号,分号或空格分隔。它相当于xml配置中的<context:component-scan base-package="org.example"/>(它自动的开启了<context:annotation-config/>配置)。

1.10.4 在自动扫描中使用Filter

在定义@ComponentScan时,可以能过includeFilters或者excludeFilters进行筛选,这两个属性与xml配置中<context:component-scan/>元素的子元素<include-filter/><exclude-filter/>`分别对应。
Filter类型:

Filter类型 示例 说明
annotation org.example.SomeAnnotation 目标组件类级别上显示的注解
assignable org.example.SomeClass
aspectj org.example..*Service+ 匹配AspectJ类型表达式的目标组件
regex org.example.Default.* 正则表达式所匹配的目标组件
custom org.example.MyTypeFilter 自定义的o.s.c.type.TypeFilter接口的实现

示例:

@Configuration
@ComponentScan(basePackages = "org.example",
        includeFilters = @Filter(type = FilterType.REGEX, pattern = ".*Stub.*Repository"),
        excludeFilters = @Filter(Repository.class))
public class AppConfig {
    ...
}
// 对应的xml
<beans>
    <context:component-scan base-package="org.example">
        <context:include-filter type="regex"
                expression=".*Stub.*Repository"/>
        <context:exclude-filter type="annotation"
                expression="org.springframework.stereotype.Repository"/>
    </context:component-scan>
</beans>

也可通过useDefaultFilters=falseuse-default-filters=false来禁用默认filters.这实际上是禁用了自动发现带有@Component, @Repository, @Service, @Controller, or @Configuration注解的类。

1.10.5 在组件内定义bean元数据

spring的组件可以为容器提供bean的配置元数据.即在@Configuration类中使用@Bean注解定义工厂方法定义bean.还可以与其组合使用@Qualifier,Scope, @Lazy及自定义注解.
示例:

@Component
public class FactoryMethodComponent {

    private static int i;

    @Bean
    @Qualifier("public")
    public TestBean publicInstance() {
        return new TestBean("publicInstance");
    }

    // use of a custom qualifier and autowiring of method parameters
    // 将privateInstance bean的age属性值注入给了country参数.
    // 对于@Value注解,在解析表达式时表达式解析器会预先查找bean的名称
    @Bean
    protected TestBean protectedInstance(
            @Qualifier("public") TestBean spouse,
            @Value("#{privateInstance.age}") String country) {
        TestBean tb = new TestBean("protectedInstance", 1);
        tb.setSpouse(spouse);
        tb.setCountry(country);
        return tb;
    }

    @Bean
    private TestBean privateInstance() {
        return new TestBean("privateInstance", i++);
    }

    @Bean
    @RequestScope
    public TestBean requestScopedInstance() {
        return new TestBean("requestScopedInstance", 3);
    }
}

从spring 4.3开始,你可以声明一个InjectionPoin类型(或其子类型)的参数,以便在创建这个bean时你能够访问请求注入点.但这仅用于bean实例的创建.因此这对于创建prototype类型的bean很有意义.对于其他作用域,这个工厂方法永远都只能在创建新的bean时才能看见这个注入点.
示例:

@Component
public class FactoryMethodComponent {

    @Bean @Scope("prototype")
    public TestBean prototypeInstance(InjectionPoint injectionPoint) {
        return new TestBean("prototypeInstance for " + injectionPoint.getMember());
    }
}

@Bean注解的方法在常规组件和@Configuration类中的处理方法是不同的.即在@Component类中是不会用CGLIB增强来拦截属性或方法的调用. CGLIB代理是通过调用@Configuration类中的@Bean方法中的字段或方法来创建协作对象的bean元数据引用的.

  1. 可以将@Bean方法声明为静态方法,这样无需创建包含它的类的实例就可以创建它们.这对于BeanFactoryPostProcessorBeanPostProcessor是有意义的,因为这些bean要在容器生命周期的早期被实例化.
  2. 静态的@Bean方法不会被拦截,即使在@Configuration类也一样,这是由CGLIB的技术设计所决定的,即CGLIB只会代理非静态的public方法.
  3. @Configuration类中的常规@Bean方法不能声明为privatefinal.
  4. 在指定的组件或配置类的基类中, @Bean方法也能被发现,这就与JAVA 8接口(被组件或配置类实现的接口)中的默认方法一样.这为复杂的组件实现提供了很大的灵活性.
  5. 一个类中可以包含同一个bean的多个@Bean方法,这时容器会选择有最多个参数的@Bean方法.这与选择构造函数时的算法一样.
1.10.6 定义组件的bean名称

spring容器在自动扫描组件时, 其bean名称是由BeanNameGenerator生成的, 默认情况下, 所有包含name属性的组件注解在被扫描时,其name属性的值都将作为bean名称.

自定义bean名称生成策略

  • 继承BeanNameGenerator接口, 注意必须要提供无参构造方法.
  • 注册
@Configuration
@ComponentScan(basePackages = "org.example", nameGenerator = MyNameGenerator.class)
public class AppConfig {
    ...
}
// xml:
<beans>
    <context:component-scan base-package="org.example"
        name-generator="org.example.MyNameGenerator" />
</beans>
1.10.7 定义组件的作用域

spring容器bean的默认作用域是单例的.可通过@Scope属性定义其他作用域.
可用于组件类或@Bean方法上.
示例:

@Scope("prototype")
@Repository
public class MovieFinderImpl implements MovieFinder {
    // ...
}

自定义作用域

  • 实现ScopeMetadataResolver接口, 实现类必须要包含默认的无参构造方法.
  • 注册
@Configuration
@ComponentScan(basePackages = "org.example", scopeResolver = MyScopeResolver.class)
public class AppConfig {
    ...
}
xml:
<beans>
    <context:component-scan base-package="org.example" scope-resolver="org.example.MyScopeResolver"/>
</beans>

当自定义非单例作用域时,要为作用域对象生成代理对象,这时要配置component-scan元素的scoped-proxy属性,它有三个选项: no, interfaces, targetClass.
示例:

@Configuration
@ComponentScan(basePackages = "org.example", scopedProxy = ScopedProxyMode.INTERFACES)
public class AppConfig {
    ...
}

<beans>
    <context:component-scan base-package="org.example" scoped-proxy="interfaces"/>
</beans>
1.10.8 @Qualifier
@Component
@Qualifier("Action")
public class ActionMovieCatalog implements MovieCatalog {
    // ...
}
1.10.9 生成组件索引

尽管通过类路径扫描组件是非常快的,在启动大型应用时,提供静态的组件列表会大大提高应用启动速度,但此时容器的所有模块都必须使用这种机制.这时,容器使用组件索引而不是从类路径去加载它.
添加依赖:

  <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context-indexer</artifactId>
        <version>5.0.10.BUILD-SNAPSHOT</version>
        <optional>true</optional>
    </dependency>

这将生成一个META-INF/spring.components文件来包含这个jar.
当一个索引只对部分库有用而不能在整个应用中构建,此时可通过设置spring.index.ignoreture来忽略索引(即回归到类路径).

1.11 使用JSR-330标准注解

从spring 3.0 开始, Spring提供了JSR-330标准注解支持, 它们的使用方法与Spring的注解一样. 但是这需要添加以下依赖:

<dependency>
    <groupId>javax.inject</groupId>
    <artifactId>javax.inject</artifactId>
    <version>1</version>
</dependency>
1.11.1 使用@Inject@Named进行依赖注入

使用@Inject代替@Autowired
@Inject注解可以用于字段,构造函数,方法级别.还可通过@Named注解指定依赖的bean名称.
示例:

public class SimpleMovieLister {

    private MovieFinder movieFinder;

    @Inject
    public void setMovieFinder(@Named("main") MovieFinder movieFinder) {
        this.movieFinder = movieFinder;
    }

    // ...
}
1.11.2 使用@Named@ManagedBean注解代替@Component

示例:

@Named("movieListener")  
// @ManagedBean("movieListener") could be used as well
public class SimpleMovieLister {

    private MovieFinder movieFinder;

    @Inject
    public void setMovieFinder(MovieFinder movieFinder) {
        this.movieFinder = movieFinder;
    }

    // ...
}

开启@Named@ManagedBean进行组件扫描与spring的注解一样,即:

@Configuration
@ComponentScan(basePackages = "org.example")
public class AppConfig  {
    ...
}

注: JSR-330和JSR-250的@ManagedBean注解是不能用来进行自定义注解的.

1.11.3 JSR-330注解的局限

使用标准注解时,有些特性是不可用的:

Spring JSR-330 注意
@Autowired @Inject @Inject没有required属性,可以与JAVA8的Optional一起使用
@Component @Named/@ManagedBean JSR-330不能作为元注解
@Scope("singleton") @Singleton JSR-330默认作用域是prototype, <br>但在spring容器中默认是单例的, <br>以与spring保持一致, 要使用其他作用域,请使用spring的@Scope注解
@Qualifier @Qualifier/@Named JSR-330的@Qualifier注解仅作为元注解使用
@Value -
@Required -
@Lazy -
ObjectFactory Provider 后者具有更短的get方法名
1.12 基于java的容器配置
1.12.1 基本概念: @Bean和@Configuration

@Bean相当于xml配置中的<bean/>元素.
@Configuration类中的@Bean方法可以调用另一个@Bean方法以定义它们之间的依赖关系,但是在非@Configuration类中不可以.

1.12.2 使用AnnotationConfigApplicationContext实例化Spring容器

AnnotationConfigApplicationContext不仅可以接收@Configuration类作为输入,也可以接收@Component类及JSR-330定义的类级别元注解.
当使用@Configuration注解时,这个类本身会作为bean定义被注册,类中的所有@Bean方法也会作为bean定义被注册.
当使用@Component或JSR-330注解时,它们也会被作为bean定义进行注册.但在类中的依赖要用@Autowired@Inject.

简单示例之构造函数实例化容器

public static void main(String[] args) {
    //可接收多个@Configuration类或@Component类或JSR-330注解类
    ApplicationContext ctx = new 
 AnnotationConfigApplicationContext(AppConfig.class);
    MyService myService = ctx.getBean(MyService.class);
    myService.doStuff();
}

简单示例之register方法实例化

public static void main(String[] args) {
    AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
    ctx.register(AppConfig.class, OtherConfig.class);
    ctx.register(AdditionalConfig.class);
    ctx.refresh();
    MyService myService = ctx.getBean(MyService.class);
    myService.doStuff();
}

启用组件扫描

@Configuration
@ComponentScan(basePackages = "com.acme")
public class AppConfig  {
    ...
}
// 等价xml:
<context:component-scan base-package="com.acme"/>

编程式启用组件扫描

public static void main(String[] args) {
    AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
    ctx.scan("com.acme"); //启用组件扫描
    ctx.refresh();
    MyService myService = ctx.getBean(MyService.class);
}

注: @Configuration注解基于@Component定义的

使用AnnotationConfigWebApplicationContext支持Web应用
web.xml配置:

<web-app>
    <!-- 使用 AnnotationConfigWebApplicationContext配置ContextLoaderListener 
        代替默认的XmlWebApplicationContext -->
    <context-param>
        <param-name>contextClass</param-name>
        <param-value>org.springframework.web.context.support.AnnotationConfigWebApplicationContext
        </param-value>
    </context-param>

    <!-- 配置路径: @Configuration 类的全限定类名,多个用逗号或空隔分隔. 也可用全限定包名指定配置类的路径 -->
    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>com.acme.AppConfig</param-value>
    </context-param>

    <!-- 用 ContextLoaderListener 引导根应用上下文-->
    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>

    <!-- 声明DispatcherServlet -->
    <servlet>
        <servlet-name>dispatcher</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <!-- 使用 AnnotationConfigWebApplicationContext配置DispatcherServlet代替默认的XmlWebApplicationContext -->
        <init-param>
            <param-name>contextClass</param-name>
            <param-value> org.springframework.web.context.support.AnnotationConfigWebApplicationContext
            </param-value>
        </init-param>
        <!--再次配置路径: @Configuration 类的全限定类名,多个用逗号或空隔分隔 -->
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>com.acme.web.MvcConfig</param-value>
        </init-param>
    </servlet>

    <!-- 映射所有的 /app/* 请求到dispatcher servlet -->
    <servlet-mapping>
        <servlet-name>dispatcher</servlet-name>
        <url-pattern>/app/*</url-pattern>
    </servlet-mapping>
</web-app>
1.12.3使用@Bean注解

@Bean注解是一个方法级别的注解,与<bean/>元素功能相同,所以它也支持<bean/>元素的一些属性.
示例:

@Configuration
public class AppConfig {

    @Bean
    public TransferServiceImpl transferService() {
        return new TransferServiceImpl();
    }
  // @Bean方法的返回类型也可以是接口或基类
    @Bean
    public TransferService transferService() {
        return new TransferServiceImpl();
    }
}

Bean依赖
@Bean方法可以有通过参数来定义其依赖, 这与构造函数注入类似.

@Configuration
public class AppConfig {

    @Bean
    public TransferService transferService(AccountRepository accountRepository) {
        return new TransferServiceImpl(accountRepository);
    }
}

接收生命周期回调

  • 在@Bean注解生成的bean类中通过@PostConstruct@PreDestory注解定义.
  • bean实现了InitializingBeanDisposableBeanLifecycle接口.
  • 标准的Aware接口.
  • init-method, destory-method属性
public class Foo {

    public void init() {
        // initialization logic
    }
}

public class Bar {

    public void cleanup() {
        // destruction logic
    }
}

@Configuration
public class AppConfig {

    @Bean(initMethod = "init")
    public Foo foo() {
        return new Foo();
    }

    @Bean(destroyMethod = "cleanup")
    public Bar bar() {
        return new Bar();
    }
}

// 对于初始化回调,也可以在@Bean方法中直接调用定义的初始化方法
    @Bean
    public Foo foo() {
        Foo foo = new Foo();
        foo.init();
        return foo;
    }

在使用JAVA配置时,你可以在对象上做任何事情而不必总是依赖容器的生命周期.

定义Bean的作用域
使用@Scope注解

    @Bean
    @Scope("prototype")
    public Encryptor encryptor() {
        // ...
    }

自定义bean名称
指定name属性

@Configuration
public class AppConfig {

    @Bean(name = "myFoo")
    public Foo foo() {
        return new Foo();
    }
}

定义bean的别名
通过name属性定义

@Configuration
public class AppConfig {

    @Bean(name = { "dataSource", "subsystemA-dataSource", "subsystemB-dataSource" })
    public DataSource dataSource() {
        // instantiate, configure and return DataSource bean...
    }
}

定义bean描述
有些情况下可能需要对bean添加一些文字描述.使用@Description注解.

@Configuration
public class AppConfig {

    @Bean
    @Description("Provides a basic example of a bean")
    public Foo foo() {
        return new Foo();
    }
}
1.12.4 使用@Configuration

@Configuration是一个类级注解.
跨bean注入

@Configuration
public class AppConfig {

    @Bean
    public Foo foo() {
        return new Foo(bar()); //相当于调用一个方法, 这只在@Configuration类中有效
    }

    @Bean
    public Bar bar() {
        return new Bar();
    }
}

查找方法注入

public abstract class CommandManager {
    public Object process(Object commandState) {
        // grab a new instance of the appropriate Command interface
        Command command = createCommand();
        // set the state on the (hopefully brand new) Command instance
        command.setState(commandState);
        return command.execute();
    }

    // okay... but where is the implementation of this method?
    protected abstract Command createCommand();
}

// JAVA配置:
@Bean
@Scope("prototype")
public AsyncCommand asyncCommand() {
    AsyncCommand command = new AsyncCommand();
    // inject dependencies here as required
    return command;
}

@Bean
public CommandManager commandManager() {
    // return new anonymous implementation of CommandManager with command() overridden
    // to return a new prototype Command object
    return new CommandManager() {
        protected Command createCommand() {
            return asyncCommand();
        }
    }
}

1.12.5 组件JAVA配置

使用@Import注解
与xml配置中的<import/>元素类似,@Import注解也可以从其他的配置类中引入bean定义.

@Configuration
public class ConfigA {

    @Bean
    public A a() {
        return new A();
    }
}

@Configuration
@Import(ConfigA.class)
public class ConfigB {

    @Bean
    public B b() {
        return new B();
    }
}

此时在注册配置类时只需要注册ConfigB即可.

public static void main(String[] args) {
    ApplicationContext ctx = new AnnotationConfigApplicationContext(ConfigB.class);

    // now both beans A and B will be available...
    A a = ctx.getBean(A.class);
    B b = ctx.getBean(B.class);
}

这种方法开发人员只需要注册一个配置类而无需关注大量的配置类.从spring 4.2开始,它也可以接收常规的@Component类.

在Import的bean定义上注入依赖项
上面的例子过于简单,在多数场景下,bean之间都有依赖关系,这通过ref来定义,因为这不涉及编译问题, 但是在@Configuration类时,对其他的bean必须是有效的java语法,这是由java编译机制决定的.这是很简单的:

@Configuration
public class ServiceConfig {

    @Bean
    public TransferService transferService(AccountRepository accountRepository) {
        return new TransferServiceImpl(accountRepository);
    }
}

@Configuration
public class RepositoryConfig {

    @Bean
    public AccountRepository accountRepository(DataSource dataSource) {
        return new JdbcAccountRepository(dataSource);
    }
}

@Configuration
@Import({ServiceConfig.class, RepositoryConfig.class})
public class SystemTestConfig {

    @Bean
    public DataSource dataSource() {
        // return new DataSource
    }
}

还可以用@Autowired@Value注入来达到同样的效果, 但是这可能会导致某些bean被过早的初始化.因为@Configuration类在容器的早期就被初始化了.

@Configuration
public class ServiceConfig {

    @Autowired
    private AccountRepository accountRepository;

    @Bean
    public TransferService transferService() {
        return new TransferServiceImpl(accountRepository);
    }
}

@Configuration
public class RepositoryConfig {

    private final DataSource dataSource;

//从spring4.3开始才支持@Configuration类中使用构造函数注入,如果只有一个构造函数,可以省略@Autowired
    @Autowired
    public RepositoryConfig(DataSource dataSource) {
        this.dataSource = dataSource;
    }

    @Bean
    public AccountRepository accountRepository() {
        return new JdbcAccountRepository(dataSource);
    }
}

@Configuration
@Import({ServiceConfig.class, RepositoryConfig.class})
public class SystemTestConfig {

    @Bean
    public DataSource dataSource() {
        // return new DataSource
    }
}

引用配置类本身
在我们使用@Autowired注入某个bean时,我们不能显式的看到它在哪里被定义(这个实际上IDE已经解决了这个问题).这时我们可以注入配置类本身.但是这将与另一个配置类完全耦合.

@Configuration
public class ServiceConfig {

    @Autowired
    private RepositoryConfig repositoryConfig;

    @Bean
    public TransferService transferService() {
        // navigate 'through' the config class to the @Bean method!
        return new TransferServiceImpl(repositoryConfig.accountRepository());
    }
}

有条件的包含@Configuration类或@Bean方法
有时需要根据系统的某些状态来启用或禁用某个@Configuration类或某个@Bean方法. 一个常见的例子是用@Profile注解来根据不同的环境激活相应的配置文件.
@Profile注解事实上是在@Conditional注解基础上实现的.
@Conditonal注解指示了接口org.springframework.context.annotation.Condition实现, @Bean在被注册之前都要访问它.这个接口提供了一个boolean matches(...)方法.
示例:

@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
    if (context.getEnvironment() != null) {
        // 读取 @Profile 注解属性
        MultiValueMap<String, Object> attrs = metadata.getAllAnnotationAttributes(Profile.class.getName());
        if (attrs != null) {
            for (Object value : attrs.get("value")) {
                if (context.getEnvironment().acceptsProfiles(((String[]) value))) {
                    return true;
                }
            }
            return false;
        }
    }
    return true;
}

组合java和xml配置
Spring对@Configuration类的支持并不是要100%的代替xml配置.
在必须要用XML配置的情况下有两种解决方式:一是用xml配置实例化容器并支持java配置,二是用java配置实例化容器并配合使用@ImportResource注解引入xml配置.

  • 用xml配置实例化容器并支持java配置.
    @Configuration类本质上也是一个bean定义,因此只需将其配置为一个bean即可.
    示例一:
// 配置类
@Configuration
public class AppConfig {

    @Autowired
    private DataSource dataSource;

    @Bean
    public AccountRepository accountRepository() {
        return new JdbcAccountRepository(dataSource);
    }

    @Bean
    public TransferService transferService() {
        return new TransferService(accountRepository());
    }
}

// xml定义
<beans>
    <!-- 开启注解配置>
    <context:annotation-config/>
    <context:property-placeholder location="classpath:/com/acme/jdbc.properties"/>

    <!--配置类-->
    <bean class="com.acme.AppConfig"/>

    <bean class="org.springframework.jdbc.datasource.DriverManagerDataSource">
        <property name="url" value="${jdbc.url}"/>
        <property name="username" value="${jdbc.username}"/>
        <property name="password" value="${jdbc.password}"/>
    </bean>
</beans>

// jdbc.properties:
    jdbc.url=jdbc:hsqldb:hsql://localhost/xdb
    jdbc.username=sa
    jdbc.password=

注: 因为@Configuration是以@Component为元注解,所以可以在xml中声明<context:component-scan base-package=""/>来开启注解扫描.

** 以java配置为基础组合xml配置**
通过@ImportResource引入xml配置
示例:

// java配置
@Configuration
@ImportResource("classpath:/com/acme/properties-config.xml")
public class AppConfig {

    @Value("${jdbc.url}")
    private String url;

    @Value("${jdbc.username}")
    private String username;

    @Value("${jdbc.password}")
    private String password;

    @Bean
    public DataSource dataSource() {
        return new DriverManagerDataSource(url, username, password);
    }
}

// properties-config.xml
<beans>
    <context:property-placeholder location="classpath:/com/acme/jdbc.properties"/>
</beans>

// jdbc.properties
jdbc.url=jdbc:hsqldb:hsql://localhost/xdb
jdbc.username=sa
jdbc.password=
1.13 环境抽象

Environment是spring容器中对profileproperties应用环境的抽象.
profile文件是一个命名的bean定义逻辑组,只有在给定概要文件处于活动状态时才向容器注册。与概要文件相关的环境对象的角色是确定哪些概要文件(如果有的话)当前是活动的,以及哪些概要文件(如果有的话)在默认情况下应该是活动的。
properties几乎在所有的应用中都扮演着重要的角色, 其来源有:properties文件, JVM系统属性, 系统环境变量, JNDI, servlet context参数等等.此时环境对象与属性之间的关系的作用是为用户提供一个方便的服务接口,以便从配置属性源中解析相关的属性。

1.13.1 bean定义profiles

profile是定义容器在不同的环境下拥有不同的bean定义.此时environment有以下几种含义:

  • 在测试或生产环境中加载不同的数据源(数据库).
  • 为不同的用户注册不同的bean定义.

@Profile
@Profile可以指定在不同的环境加载与其对应的配置.

@Configuration
@Profile("development")
public class StandaloneDataConfig {

    @Bean
    public DataSource dataSource() {
        return new EmbeddedDatabaseBuilder()
            .setType(EmbeddedDatabaseType.HSQL)
            .addScript("classpath:com/bank/config/sql/schema.sql")
            .addScript("classpath:com/bank/config/sql/test-data.sql")
            .build();
    }
}

@Configuration
@Profile("production")
public class JndiDataConfig {

    @Bean(destroyMethod="")
    public DataSource dataSource() throws Exception {
        Context ctx = new InitialContext();
        return (DataSource) ctx.lookup("java:comp/env/jdbc/datasource");
    }
}

@Profile还可以作为元注解来创建自定义的注解.

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Profile("production")
public @interface Production {
}

// 此时可以用@Producton代替@Profile("production").

注: @Profile注解还可以使用!, 如@Profile({"p1", "!p2"}).则表示在p1激活或p2没有激活时会注册它们

@Profile也可以用于方法级别.

@Configuration
public class AppConfig {

    @Bean("dataSource")
    @Profile("development")
    public DataSource standaloneDataSource() {
        return new EmbeddedDatabaseBuilder()
            .setType(EmbeddedDatabaseType.HSQL)
            .addScript("classpath:com/bank/config/sql/schema.sql")
            .addScript("classpath:com/bank/config/sql/test-data.sql")
            .build();
    }

    @Bean("dataSource")
    @Profile("production")
    public DataSource jndiDataSource() throws Exception {
        Context ctx = new InitialContext();
        return (DataSource) ctx.lookup("java:comp/env/jdbc/datasource");
    }
}

注:当@Profile注解用在@Bean方法上时,如果有多个重载方法,则所有重载的方法上的@Profile定义应当一致,如果不一致, 则只有第一个声明有效. 如果你想用不同的方法名定义相同的bean, 此时通过@Bean的name属性指定为同一个bean即可, 如上所示.

xml定义profile
通过beans元素的profile属性来定义.

<beans profile="development"
    xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:jdbc="http://www.springframework.org/schema/jdbc"
    xsi:schemaLocation="...">

    <jdbc:embedded-database id="dataSource">
        <jdbc:script location="classpath:com/bank/config/sql/schema.sql"/>
        <jdbc:script location="classpath:com/bank/config/sql/test-data.sql"/>
    </jdbc:embedded-database>
</beans>
<beans profile="production"
    xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:jee="http://www.springframework.org/schema/jee"
    xsi:schemaLocation="...">

    <jee:jndi-lookup id="dataSource" jndi-name="java:comp/env/jdbc/datasource"/>
</beans>

还可以将上述两个xml定义在一个xml中:

<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:jdbc="http://www.springframework.org/schema/jdbc"
    xmlns:jee="http://www.springframework.org/schema/jee"
    xsi:schemaLocation="...">

    <!-- other bean definitions -->

    <beans profile="development">
        <jdbc:embedded-database id="dataSource">
            <jdbc:script location="classpath:com/bank/config/sql/schema.sql"/>
            <jdbc:script location="classpath:com/bank/config/sql/test-data.sql"/>
        </jdbc:embedded-database>
    </beans>

    <beans profile="production">
        <jee:jndi-lookup id="dataSource" jndi-name="java:comp/env/jdbc/datasource"/>
    </beans>
</beans>

注意: 这类元素只能放在xml文件的最后.

激活profile

  • 通过Environment API.
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.getEnvironment().setActiveProfiles("development");
ctx.register(SomeConfig.class, StandaloneDataConfig.class, JndiDataConfig.class);
ctx.refresh();
  • 通过spring.profiles.active属性
    它可以通过系统环境变量, JVM系统属性, web.xml中的servlet context参数, JNDI实体来指定.
    在spring集成测试模块, 还可以通过@ActiveProfiles注解来指定.
    可以同时激活多个.
ctx.getEnvironment().setActiveProfiles("profile1", "profile2");
-Dspring.profiles.active="profile1,profile2"

默认profile
默认profile是指在没有指定的active的profile时,则使用它, 如果有active的profile, 则不使用它.
定义默认profile:

@Configuration
@Profile("default")
public class DefaultDataConfig {

    @Bean
    public DataSource dataSource() {
        return new EmbeddedDatabaseBuilder()
            .setType(EmbeddedDatabaseType.HSQL)
            .addScript("classpath:com/bank/config/sql/schema.sql")
            .build();
    }
}

注: @Profile注解的default属性值 可以通过EnvironmentsetDefaultProfiles()方法或spring.profiles.default属性进行自定义.

1.13.2 PropertySource抽象

spring的Environment抽象提供了对配置的属性源进行搜索的操作.如:

ApplicationContext ctx = new GenericApplicationContext();
Environment env = ctx.getEnvironment();
// PropertySource是对属性键值对的简单抽象, 下面的方法会在一个PropertySource集合中进行搜索
boolean containsFoo = env.containsProperty("foo");
System.out.println("Does my environment contain the 'foo' property? " + containsFoo);

StandardEnvironment配置了两个PorpertySource对象,一个是JVM系统属性(类似于System.getProperties()), 一个是系统环境变量(类似于System.getenv()).

  1. StandardEnvironment 用于独立应用程序, StandardServletEnviroment包含了servlet配置和servlet context参数.它还可以选择性的开启JndiPropertySource.
  2. 由于默认情况下, 系统属性优先于环境变量,因此当foo在两个地方都会设置时,env.getProperty("foo")会返回系统属性, 注意这个属性并没有合并,而是后面的被前面的覆盖了.
  3. 对于StandardServletEnvironment, 其优先级从高到低如下:
    • ServletConfig 参数,如DispatcherServlet上下文
    • ServletContext参数.如web.xml中的context-param
    • JNDI环境变量,如 java:comp/env/
    • JVM系统属性, 如 -D命令行参数
    • JVM系统环境变量,如操作系统环境变量

最重要的是这是可以配置的,如果你想将你自定义的属性源集成到env中,你只要实现你自己的PropertySource并实例化它,然后将其添加到PropertySources集合中即可.

ConfigurableApplicationContext ctx = new GenericApplicationContext();
MutablePropertySources sources = ctx.getEnvironment().getPropertySources();
sources.addFirst(new MyPropertySource());

在上面的示例中,自定义的PropertySource被添加到最顶级,如果存在某个属性,则会覆盖其他所有的配置.MutablePropertySources API提供了大量的方法来精准定义.

1.13.3 @PropertySource

@PropertySource注解可以很方便的将指定的propertySource添加到spring的Environment中.如:

@Configuration
@PropertySource("classpath:/com/myco/app.properties")
public class AppConfig {

    @Autowired
    Environment env;

    @Bean
    public TestBean testBean() {
        TestBean testBean = new TestBean();
        testBean.setName(env.getProperty("testbean.name"));
        return testBean;
    }
}

@PropertySource注解中可以使用${}占位符.如:

@PropertySource("classpath:/com/${my.placeholder:default/path}/app.properties"

以上假设my.placeholder已被注册, 它会被解析为相应的值, 如果没有,则会使用default/path作为默认值.如果二者都没有定义, 则会抛出IllegalArgumentException异常.

在java8中, @PropertySource是可重复的,但是都必须在同一个级别声明.

1.13.4 语句中的占位符解析

只要占位符中的值在环境中可用, 无论它定义在哪里,都能够被解析.如

<beans>
    <import resource="com/bank/service/${customer}-config.xml"/>
</beans>
// customer只要在环境中可用,无论它被定义在什么地方都能够被解析.
1.14 注册LoadTimeWeaver

Spring使用LoadTimeWeaver在类被加载到JVM时动态转换它们.
@EnableLoadTimeWeaving: 开启加载时织入功能.

@Configuration
@EnableLoadTimeWeaving
public class AppConfig {
}

对应的xml配置:

<beans>
    <context:load-time-weaver/>
</beans>

一旦给spirng容器作了上述配置, 容器中的任何bean都可以实现 LoadTimeWeaverAware, 因此可以获取到load-time weaver实例,这在使用JPA时是非常有用的.更多可查阅LocalContainerEntityManagerFactoryBean的文档.

1.15 ApplicationContext附加功能

ApplicationContext位于org.springframework.context包,它扩展了BeanFactory接口,提供了更多附加功能:

  • 通过MessageSource接口,可以处理i18n样式的消息.
  • 通过ResourceLoader接口, 可以访问网络及文件资源.
  • 通过ApplicationListenerApplicationEventPublisher接口, 实现事件发布.
  • 通过HierarchicalBeanFactory接口, 可以加载多个或多层次容器.
1.15.1 使用MessageSource国际化.

ApplicationContext接口扩展了MessageSource接口, 因此具备了国际化功能.Spring也提供了HierarchicalMessageSource接口, 用于分层解析消息, 以上接口结合在一起便实现了国际化功能.
这此接口定义了以下方法:

  • String getMessage(String code, Object[] args, String default, Locale loc): 从MessageSource中获取消息,如果指定的locale没有找到, 则使用默认值.传入的任何参数都将被标准库提供的MessageFormat的相应的值替换.
  • String getMessage(String code, Object[] args, Locale loc): 与前者一样,不同的是它没有提供默认址,如果没有找到相应的值, 会抛出NoSuchMessageException.
  • String getMessage(MessageSourceResolvable resolvable, Locale loc): 前面方法中的所有属性都将被封装在MessageSourceResolvalbe类中.

当一个ApplicationContext启动时,容器会自动搜索MessageSource bean定义,bean的名称必须是messageSource, 如果没有找到, 则会向上查找,如果也没有找到, 则会实例化一个空的DelegatingMessageSource, 以便于能被前面定义的方法调用 .

spring提供了两个MessageSource实现, 即ResourceBundleMessageSourceStaticMessageSource, 二者都实现了HierarchicalMessageSource, 以便于能处理嵌套消息.StaticMessageSource很少被使用, 但是它可以编程式的添加消息.
ResourceBundleMessageSource使用如下:

<beans>
    <bean id="messageSource"
            class="org.springframework.context.support.ResourceBundleMessageSource">
        <property name="basenames">
            <list>
                <value>format</value>
                <value>exceptions</value>
                <value>windows</value>
            </list>
        </property>
    </bean>
</beans>

在上面的例子中,假设你定义了format, exceptions, windows三个资源包. 则所有解析消息 的request都将通过资源包以JDK的标准方式解析消息.
对于上面的例子, 假设定义的两个资源包谁的如下:

# in format.properties
message=Alligators rock!
# in exceptions.properties
argument.required=The {0} argument is required.

则有:

public static void main(String[] args) {
    // 所有的ApplicationContext都扩展了MessageSource接口.
    MessageSource resources = new ClassPathXmlApplicationContext("beans.xml");
    String message = resources.getMessage("message", null, "Default", null);
    System.out.println(message); // Alligators rock!
}

接下来的示例将说明传递给消息的查找参数; 这些参数将转换为字符串并替换消息资源包中的占位符。

<beans>

    <!-- this MessageSource is being used in a web application -->
    <bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource">
        <property name="basename" value="exceptions"/>
    </bean>

    <!-- 将上面的MessageSource注入到这个bean中-->
    <bean id="example" class="com.foo.Example">
        <property name="messages" ref="messageSource"/>
    </bean>

</beans>
public class Example {

    private MessageSource messages;

    public void setMessages(MessageSource messages) {
        this.messages = messages;
    }

    public void execute() {
        String message = this.messages.getMessage("argument.required",
            new Object [] {"userDao"}, "Required", null);
        System.out.println(message); // The userDao argument is required.
    }
}

关于国际化, springg的各种MessageSource实现遵循与标准JDK ResourceBundle相同的语言环境解析和回退规则, 即就前面的例子而言,如果你想解析英国地区(en-GB)语言环境的消息, 则你应该分别创建format_en_GB.properties, exception_en_GB.properties, windows_en_GB.properties文件.

通常来说, 区域(Locale)的设置是由应用程序所处的环境管理的.
可以通过实现MesageSourceAware接口来获取已被定义的MessageSource的引用.

Spring还提供了一个ReloadableResourceBundleMessageSource类, 相对于基于标准JDK实现的ResourceBundleMessageSource类, 它更加灵活, 它可以从spring的任何资源路径读取资源包, 还支持资源包的热重载(资源包缓存).

1.15.2 标准事件和自定义事件

ApplicationContext中是通过ApplicationEvent类和ApplicationListener接口来处理事件的.如果某个bean实现了ApplicationListener接口并被部署到容器中, 那么每次ApplicationEvent被发布到容器中时,都会通过该bean.这是典型的观察者模式.
从spring4.2开始, 可以通过注解来实现发布任意事件, 即无需再继承ApplicationEvent.

spring提供的标准事件:

Event 说明
ContextRefreshedEvent 当容器被实例化或refreshed时发布.如调用configurableApplicationContext接口的refresh()方法, 此处的实例化是指所有的bean都已被加载,后置处理器都被激活,所有单例都已被实例化, 所有的容器对象都已准备好可使用. 如果容器支持热重载,则refresh可以被触发多次,如xmlWebApplicatonContext支持热刷新,而GenericApplicationContext则不支持
ContextStartedEvent 当容器被启动时发布,即调用了ConfigurableApplicationContext的start()方法, 已启用意味着所有的Lifecycle bean都已显式接收到了start信号
ContextStoppedEvent 当容器停止时发布,即调用了ConfigurableApplicationContext的stop()方法, 即所有的Lifecycle bean都已显式接收到了stop信号 , 关闭的容器可以通过start()方法重启
ContextClosedEvent 当容器被关闭时发布,即调用 了ConfigrableApplicationContext的close方法, 关闭意味着所有的单例bean都已被销毁.关闭的容器不能被重启或refresh
RequestHandledEvent 这只在使用spring的DispatcherServlet时有效,当一个请求被处理完成时发布

自定义事件
扩展ApplicationEvent接口.

public class BlackListEvent extends ApplicationEvent {

    private final String address;
    private final String content;

    public BlackListEvent(Object source, String address, String content) {
        super(source);
        this.address = address;
        this.content = content;
    }
}

通过调用ApplicationEventPublisherpublishEvent()方法发布自定义事件.通过实现ApplicationEventPublisherAware接口获取ApplicationEventPublisher实例.

public class EmailService implements ApplicationEventPublisherAware {

    private List<String> blackList;
    private ApplicationEventPublisher publisher;

    public void setBlackList(List<String> blackList) {
        this.blackList = blackList;
    }

    public void setApplicationEventPublisher(ApplicationEventPublisher publisher) {
        this.publisher = publisher;
    }

    public void sendEmail(String address, String content) {
        if (blackList.contains(address)) {
            publisher.publishEvent(new BlackListEvent(this, address, content));
            return;
        }
        // send email...
    }
}

接收自定义事件, 要实现ApplicationListener接口并将其注册为bean.

public class BlackListNotifier implements ApplicationListener<BlackListEvent> {

    private String notificationAddress;

    public void setNotificationAddress(String notificationAddress) {
        this.notificationAddress = notificationAddress;
    }

    public void onApplicationEvent(BlackListEvent event) {
        // notify appropriate parties via notificationAddress...
    }
}

注意: 通常使用自定事件类型作为ApplicationListener接口的泛型参数, 这意味onApplicationEvent()方法是类型安全的.默认事件的接收是同步的.即所有的publishEvent()方法会在所有事件被处理完之前处于阻塞状态.这种同步单线程的优势是,当前一个listener侦听到一个事件时,如果存在事务上下文,事件将会在事务上下文中处理. 另一种事件发布机制参考ApplicationEventMulticaster文档.

基于注解的事件监听
从spring 4.2开始, 可以通过@EventListener注解来注册bean中的public方法.

public class BlackListNotifier {

    private String notificationAddress;

    public void setNotificationAddress(String notificationAddress) {
        this.notificationAddress = notificationAddress;
    }

    @EventListener
    public void processBlackListEvent(BlackListEvent event) {
        // notify appropriate parties via notificationAddress...
    }
}

在上面的示例中, 方法的参数指明了被监听的事件类型, 而且没有实现特定的listener接口.

如果你不想定义参数或想监听多个事件,则可以通过@EventListener注解本身指定事件类型.

@EventListener({ContextStartedEvent.class, ContextRefreshedEvent.class})
public void handleContextStart() {
    ...
}

也可以通过@EventListener注解的condition属性结合SpEL指定过滤条件.

@EventListener(condition = "#blEvent.content == 'foo'")
public void processBlackListEvent(BlackListEvent blEvent) {
    // notify appropriate parties via notificationAddress...
}

事件Spring el表达式可用元数据

Name Location 说明 示例
Event root 实际的ApplicationEvent #root.event
args[] root 调用目标事件的参数 #root.args[0]
参数名 容器计算 方法的参数名,如果参数不可知,则可用索引 #blEvent或#a0

注意: #root.event允许你访问底层事件.

如果你想继续发布另一个事件,只需要将方法的返回值定义为你要发布的事件即可.若发布多个只需要返回一个事件集合即可.

@EventListener
public ListUpdateEvent handleBlackListEvent(BlackListEvent event) {
    // notify appropriate parties via notificationAddress and
    // then publish a ListUpdateEvent...
}

注:异步事件不支持此功能.

异步监听
使用@Async注解

@EventListener
@Async
public void processBlackListEvent(BlackListEvent event) {
    // BlackListEvent is processed in a separate thread
}

使用异步事件时有以下几点限制:

  • 如果事件监听器抛出一个异常, 它将不会被传播给调用者,此时检查AsyncUncaughtExceptionHandler获取详细信息.
  • 无法通过返回一个事件来自动发布事件,此时只能注入ApplicationEventPublisher来手动发布事件.

定义listeners的顺序
使用@Order接口

@EventListener
@Order(42)
public void processBlackListEvent(BlackListEvent event) {
    // notify appropriate parties via notificationAddress...
}

泛型事件
可以使用泛型来进一步定义事件, 例如EntityCreatedEvent<T>,

@EventListener
public void onPersonCreated(EntityCreatedEvent<Person> event) {
    ...
}

由于类型擦除, 只有在事件的泛型参数被解析后监听器才会工作.

1.15.3 访问底层资源

见Resource.

1.15.4 web应用实例化ApplicationContext

可以使用ContextLoader实例化ApplicationContext容器.当然,你也可以使用ApplicationContext接口的实现类实例化容器.
使用ContextLoaderListener注册容器

<context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>/WEB-INF/daoContext.xml /WEB-INF/applicationContext.xml</param-value>
</context-param>

<listener>
    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>

这个listener会检查contextConfigLocation参数, 如果没有定义, listener会默认使用`/WEB-INF/applicationContext.xml. 如果存在,listener会用逗号,分号,空格来拆分参数,使用拆分后的值作为资源路径.这个参数还支持Ant样式, 如/WEB-INF/Context.xml, 则会加载WEB-INF目录下的所有以Context结尾的xml. 又如/WEB-INF//Context.xml, 则会加载WEB-INF目录及其所有子目录下的以Context结尾的xml文件.

1.15.5 将spring ApplicationContext部署为java ee的RAR文件.

可以将spring ApplicationContext部署为rar文件, 相当于在JAVAEE环境中引导一个独立的ApplicationContext, 实际上, 这与没有任何HTTP入口的war文件中部署ApplicationContext类似.

rar文件部署非常适合于没有HTTP入口的应用程序(如任务调试程序).
可以查看SpringContextResourceAdapter类的javadoc,了解RAR部署中涉及的配置细节.

将Spring ApplicationContext部署为rar文件的简单方法: 将所有的应用class文件打包到一个rar文件中, 这是一个具有不同扩展名的标准jar文件. 再将其所有依赖的jar包放到rar存档的根目录.添加一个WEB-INF/ra.xml部署描述(具体参见SpringContextResourceAdapter 文档)和相应的spring xml bean定义文件(如`WEB-INF/applicationContext.xml). 将生成的jar文件放到应用服务器的部署目录下.

1.16 BeanFactory

BeanFactory的API提供了spring容器的最基本功能,它定义的方法主要用于与spring的其他模块或第三方框架集成, 它的DefaultListableBeanFactory实现在高级别容器中是一个关键部分.
BeanFactory及其相关接口(如BeanFactoryAware, InitializingBean, DisposableBean)是与其他框架组件集成的重要集成点, 它们之间无需任何注解或反射,就能实现很好的交互.
注意: BeanFactory的API及DefaultListableBeanFactory实现都不会对配置格式,组件注解有任何定义.而这些都是通过如`XmlBeanDefinitionReader, AutowiredAnnotationBeanPostProcessor等扩展实现的.这就是spring容器如此灵活和可扩展的根本原因.

1.16.1 BeanFactory与ApplicationContext

ApplicationContext包含了BeanFactory的所有功能.
二者的区别:

特性 BeanFactory ApplicationContext
bean实例化 Y Y
生命周期管理 N Y
自动注册BeanPostProcessor N Y
自动注册BeanFactoryPostProcessor N Y
消息处理 N Y
事件发布 N Y

DefaultListableBeanFactory显式注册bean post-processor.

DefaultListableBeanFactory factory = new DefaultListableBeanFactory();
// 给factory填充bean定义

// 注册 BeanPostProcessor 实例
factory.addBeanPostProcessor(new AutowiredAnnotationBeanPostProcessor());
factory.addBeanPostProcessor(new MyBeanPostProcessor());

// 现在可以使用这个factory了

BeanFactoryPostProcessor应用到DefaultListableeBeanFactory.

DefaultListableBeanFactory factory = new DefaultListableBeanFactory();
XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(factory);
reader.loadBeanDefinitions(new FileSystemResource("beans.xml"));

// 从一个属性文件中引入属性值
PropertyPlaceholderConfigurer cfg = new PropertyPlaceholderConfigurer();
cfg.setLocation(new FileSystemResource("jdbc.properties"));

// now actually do the replacement
cfg.postProcessBeanFactory(factory);

在上面的两个例子中,显式的注解步骤都是不方便的,这就是各种ApplicationContext变体实现优于DefaultListableBeanFactory的原因.

AnnotationConfigApplicationContext拥有所有开箱即用的公共注解后置处理器,它还可以通过配置注解(如@EnableTransactionManagement)引入额外的处理器.

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容