NO END FOR LEARNING

Writing blog if you feel tired | 学海无涯 苦写博客

Spring Boot 深入浅出系列(一) - 习惯使用注解

| Comments

Spring Boot从一开始就告诉你,它更喜欢基于Java的配置,即注解的方式。所以它提供了你一大堆注解,并让你习惯使用注解。

@Bean

Indicates that a method produces a bean to be managed by the Spring container.

@Configuration

Indicates that a class declares one or more @Bean methods and may be processed by the Spring container to generate bean definitions and service requests for those beans at runtime

@EnableAutoConfiguration

Enable auto-configuration of the Spring Application Context, attempting to guess and configure beans that you are likely to need. Auto-configuration classes are usually applied based on your classpath and what beans you have defined.

@ComponentScan

Configures component scanning directives for use with @Configuration classes. Provides support parallel with Spring XML’s <context:component-scan> element.

指定main application class的位置

SpringBoot建议你将主应用class(main application class)放在包根路径上,即其他子包之上。@EnableAutoConfiguration通常放在你的main class上,这样也隐含的指定了对某些配置项的搜索路径。比如,对@Entity的搜索。

在主应用class上指定@ComponentScan,同样也隐式的指定了扫描时basePackage的路径。

1
2
3
4
5
6
7
8
@Configuration
@EnableAutoConfiguration
@ComponentScan
public class Application {
  public static void main(String[] args) {
      SpringApplication.run(Application.class, args);
  }
}

如果你的main application class的位置确实在包的根路径上,上面的三个注解,可以用@SpringBootApplication这一个注解代替。

多种方式加载Bean

你必然不会在main application class定义很多的@Bean,Spring提供两种方式将定义在另外一个带有@Configuration的类中的Bean加载,第一种,在Application类中使用@Import指定该类,第二种,让@ComponentScan扫描到该类。大部分情况都会选择第二种。

加载XML的配置

如果你必须使用XML的配置,你可以使用@ImportResource来加载指定的XML配置。

Bean的自动配置

SpringBoot有一个非常神秘的注解@EnableAutoConfiguration,官方的解释已经在上面的部分给出,简单点说就是它会根据定义在classpath下的类,自动的给你生成一些Bean,并加载到Spring的Context中。

它的神秘之处,不在于它能做什么,而在于它会生成什么样的Bean对于开发人员是不可预知(或者说不容易预知)。举个例子:

要开发一个基于Spring JPA的应用,会涉及到下面三个Bean的配置,DataSource,EntityManagerFactory,PlatformTransactionManager。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Configuration
@EnableJpaRepositories
@EnableTransactionManagement
public class ApplicationConfig {
  @Bean
  public DataSource dataSource() {
      ...
  }

  @Bean
  public EntityManagerFactory entityManagerFactory() {
      ..
      factory.setDataSource(dataSource());
      return factory.getObject();
  }

  @Bean
  public PlatformTransactionManager transactionManager() {
      JpaTransactionManager txManager = new JpaTransactionManager();
      txManager.setEntityManagerFactory(entityManagerFactory());
      return txManager;
  }
}

@EnableJpaRepositories会查找满足作为Repository条件(继承父类或者使用注解)的类。

@EnableTransactionManagement的作用:Enables Spring’s annotation-driven transaction management capability, similar to the support found in Spring’s <tx:*> XML namespace。

但是,如果你使用了@EnableAutoConfiguration,那么上面三个Bean,你都不需要配置。在classpath下面只引入了MySQL的驱动和SpringJpa。

1
2
compile 'mysql:mysql-connector-java:5.1.18'
compile 'org.springframework.boot:spring-boot-starter-data-jpa'

在Application类中写下下面这段代码,可以查看SpringBoot给你生成了这些Bean:

1
2
3
4
5
6
7
8
9
10
11
ConfigurableApplicationContext ctx = SpringApplication.run(Application.class, args);
System.out.println("Let's inspect the beans provided by Spring Boot:");

Object dataSource = ctx.getBean("dataSource");
Object transactionManager = ctx.getBean("transactionManager");
Object entityManagerFactory = ctx.getBean("entityManagerFactory");
System.out.println(dataSource);
System.out.println(entityManagerFactory);
System.out.println(transactionManager);
System.out.println(((JpaTransactionManager)transactionManager).getDataSource());
System.out.println(((JpaTransactionManager)transactionManager).getEntityManagerFactory());

输出如下:

1
2
3
4
5
6
7
8
9
org.apache.tomcat.jdbc.pool.DataSource@4f0e94db{ConnectionPool[defaultAutoCommit=null; defaultReadOnly=null; defaultTransactionIsolation=-1; defaultCatalog=null; driverClassName=com.mysql.jdbc.Driver; maxActive=100; maxIdle=100; minIdle=10; initialSize=10; maxWait=30000; testOnBorrow=true; testOnReturn=false; timeBetweenEvictionRunsMillis=5000; numTestsPerEvictionRun=0; minEvictableIdleTimeMillis=60000; testWhileIdle=false; testOnConnect=false; password=********; ... }

org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean@5109d386

org.springframework.orm.jpa.JpaTransactionManager@5c1e2bfa

org.apache.tomcat.jdbc.pool.DataSource@4f0e94db{ConnectionPool[defaultAutoCommit=null; defaultReadOnly=null; defaultTransactionIsolation=-1; defaultCatalog=null; driverClassName=com.mysql.jdbc.Driver; maxActive=100; maxIdle=100; minIdle=10; initialSize=10; maxWait=30000; testOnBorrow=true; testOnReturn=false; timeBetweenEvictionRunsMillis=5000; numTestsPerEvictionRun=0; minEvictableIdleTimeMillis=60000; testWhileIdle=false; testOnConnect=false; password=********;... }

org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean@5109d386

Bean中的URL,username和password是在属性文件中配置的:

1
2
3
4
#Database
spring.datasource.url=jdbc:mysql://localhost:3306/xxxx      
spring.datasource.username=root
spring.datasource.password=

Disable自动配置

如果你发现自动转配的Bean不是你想要的,你也可以disable它。比如说,我不想要自动装配Database的那些Bean

1
2
3
4
5
6
7
8
9
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.context.annotation.Configuration;

@Configuration
@EnableAutoConfiguration(exclude = {DataSourceAutoConfiguration.class})
public class MyConfiguration {
  
}

此时,就会报下面的错了

1
Caused by: org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type [javax.sql.DataSource] found for dependency

习惯使用和正确使用上面这些注解,是正确使用Spring Boot的重要起步。

参考资料:
1.Spring Boot Reference
2.Spring JPA Reference

当Entity继承遇到Hibernate的@PrePersist和@PreUpdate

| Comments

上周做项目的时候遇到的关于实现审计日志方式的问题,这里记录一下。

假设你的数据库是这样设计的:

1
2
3
4
5
6
7
8
9
10
{
    "Customer": {
        "id": 1,
        "name": "benwei"
    },
    "AdvancedCustomer": {
        "customerId": 1,
        "level": 1
    }
}

一个Customer表和一个AdvancedCustomer表,AdvancedCustomer表中含有CustomerId作为外键。

在Java中的Entity实现是这样的:AdvancedCustomer继承自Customer,父类定义的继承策略是Joind。

1
2
3
4
5
6
7
8
9
10
11
12
@Entity
@Table
@Inheritance(Strategy=InheritanceType.JOINED)
public class Customer {
  
}

@Entity
@Table
public class AdvancedCustomer extends Customer {
  
}

Joind策略的含义是:A strategy in which fields that are specific to a subclass are mapped to a separate table than the fields that are common to the parent class, and a join is performed to instantiate the subclass.

通用的属性定义在父类中表,特殊的属性映射到另一个独立的表。

此时,你想要给应用添加一个审计功能。

你给Customer表添加LastModifiedBy和LastModifiedDate两个字段。

1
2
3
4
5
6
7
8
9
10
11
12
{
    "Customer": {
        "id": 1,
        "name": "benwei",
        "lastModifiedBy": "benwei",
        "lastModifiedDate": "21/6/2015 23:01:11"
    },
    "AdvancedCustomer": {
        "customerId": 1,
        "level": 1
    }
}

做法是使用EntityListener,在Listener中使用Hibernate的@PrePersist和@PreUpdate来监听事件的发生。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class AuditListener {

    @PrePersist
    public void prePersist(Object entity) {
      if(entity instanceOf AdvancedCustomer) {
          ((AdvancedCustomer)entity).setLastModifiedDate(Datetime.now());  
      }
    }

    @PreUpdate
    public void preUpdate(Object entity) {
      ...
    }
}

@Entity
@Table
@EntityListeners([AuditListener.class])
public class AdvancedCustomer extends Customer {
  
}

问题来了,这个prePersist或者preUpdate方法会在什么时候触发呢?当修改AdvancedCustomer中的任意变量时,比如name,level,都会触发prePersist或者preUpdate。

但是,你到数据库中去查看审计事件变化时会发现,当创建一个新的Customer,或者更新Customer的名字字段都没有问题。

当update子类中的变量level的时候,lastModifiedDate并没有发生变化。这是为什么?

要找到原因,必须打开showSql属性,来查看Hibernate到底产生的SQL语句是什么。

你会发现,当修改level变量时,Hibernate只产生了一条update语句来更新AdvancedCustomer这张表。而创建Customer会同时更新AdvancedCustomer和Customer两张表,更新name字段,会更新Customer这张表。

也就是说,在PreUpdate触发之前,Hibernate在策略上已经决定了只更新AdvancedCustomer。即便之后改变了Customer中的lastModifiedDate,也没有改变它的行为。这里并不是说PreUpdate没有起到作用,而是Hibernate之决定更新一张表,至于更新什么内容,要等到PreUpdate之后决定(这一点可以从更新name时,lastModifiedDate发生了改变来证明)。

如何解决:

目前,我们没有完美的解决方案可以在仍然使用PreUpdate的情况下,保证审计信息更新正确。

出现这个问题的主要原因是因为我们的实现受到框架实现机制的限制。

所以,我们改变了实现的策略,既然受到框架本身实现策略的限制,我们就脱离框架,在还未计入框架管理范围之内,就将审计信息写入Entity内,那么可行的一种方式就是AOP。在触发Hibernate的save方法之前,将审计信息写入Entity。