乐趣区

Springboot整合Hibernate拦截器时无法向拦截器注入Bean

开发环境

  1. JDK 1.8
  2. Springboot 2.1.1.RELEASE

pom 配置

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.1.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.13</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

关键代码

实体类

@Entity
public class User implements Serializable {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;

    private String name;

    public Integer getId() {return id;}

    public void setId(Integer id) {this.id = id;}

    public String getName() {return name;}

    public void setName(String name) {this.name = name;}
}

Repository

public interface UserRepository extends JpaRepository<User,Integer> {}

自定义服务

@Service
public class MyService {public void print(){System.out.println(this.getClass().getSimpleName()+"call");
    }
}

拦截器

public class SimpleInterceptor extends EmptyInterceptor {

    @Resource
    private MyService myService;

    @Override
    public String onPrepareStatement(String sql) {myService.print();
        System.out.println("sql:"+sql);
        return super.onPrepareStatement(sql);
    }
}

启动类

@SpringBootApplication
public class BootHibernateInterceptorProblemApplication {public static void main(String[] args) {SpringApplication.run(BootHibernateInterceptorProblemApplication.class, args);
    }

}

配置

## DataSource
spring.datasource.url=jdbc:mysql://localhost:3306/demo?characterEncoding=utf8&useSSL=true
spring.datasource.username=root
spring.datasource.password=123456789

## hibernate
spring.jpa.hibernate.ddl-auto=update
## add interceptor
spring.jpa.properties.hibernate.ejb.interceptor=com.rjh.interceptor.SimpleInterceptor

单元测试类

@RunWith(SpringRunner.class)
@SpringBootTest
public class BootHibernateInterceptorProblemApplicationTests {

    @Resource
    private UserRepository userRepository;

    @Test
    public void contextLoads() {System.out.println(userRepository.findAll());
    }

}

运行结果

java.lang.NullPointerException
    at com.rjh.interceptor.SimpleInterceptor.onPrepareStatement(SimpleInterceptor.java:20)
    ...
    ...
    ...

分析

根据异常信息,猜测是注入 MyService 失败

修改单元测试

@RunWith(SpringRunner.class)
@SpringBootTest
public class BootHibernateInterceptorProblemApplicationTests {

    @Resource
    private UserRepository userRepository;

    @Resource
    private MyService myService;

    @Resource
    private SimpleInterceptor simpleInterceptor;

    @Test
    public void contextLoads() {Assert.assertNotNull(myService);
        Assert.assertNotNull(simpleInterceptor);
        System.out.println(userRepository.findAll());
    }

}

运行结果

Caused by: org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'com.rjh.interceptor.SimpleInterceptor' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {@javax.annotation.Resource(shareable=true, lookup=, name=, description=, authenticationType=CONTAINER, type=class java.lang.Object, mappedName=)}
    ...

分析

根据异常信息可知,Spring 的 IoC 容器中并没有 SimpleInterceptor 这个 Bean,从此处可知spring.jpa.properties.hibernate.ejb.interceptor=com.rjh.interceptor.SimpleInterceptor 并没有把这个拦截器注册到 Spring 容器中

失败方案

SimpleInterceptor 上添加 @Component 注解,将 SimpleInterceptor 注册到 Spring 容器中。同时注释 spring.jpa.properties.hibernate.ejb.interceptor 配置

失败:SimpleInterceptor的构造方法触发了两次,添加到 Hibernate 中的 SimpleInterceptor 实例和注册到 Spring 容器中的 SimpleInterceptor 实例并不是同一个实例

解决方法

增加一个获取 Spring 的 ApplicationContext 实例的工具类,通过这个工具类调用需要注入的服务的方法

工具类

@Component
public class SpringContextUtil implements ApplicationContextAware {

    private static ApplicationContext applicationContext;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {SpringContextUtil.applicationContext=applicationContext;}

    public static ApplicationContext getApplicationContext() {return applicationContext;}
}

修改拦截器

public class SimpleInterceptor extends EmptyInterceptor {

    @Override
    public String onPrepareStatement(String sql) {MyService myService= SpringContextUtil.getApplicationContext().getBean(MyService.class);
        myService.print();
        System.out.println("sql:"+sql);
        return super.onPrepareStatement(sql);
    }

}

执行结果

MyService call
sql:select user0_.id as id1_0_, user0_.name as name2_0_ from user user0_
[]
退出移动版