Spring总结(1/2):概念

0、一些概念

  • AppConfig:使用Annotation注入时的配置类,也是main()方法所在类

  • AOP:Aspect Oriented Programming,面向切面编程。

  • DAO:Data Access Object

  • IoC:Inversion of Control,控制反转;又称依赖注入(DI:Dependency Injection)。它用来解决组件创建+配置使用分离的问题,同时负责管理组件的生命周期。
  • JDBC:Java DataBase Connectivity,Java程序访问数据库标准接口
  • JPA:Java Persistence API;JavaEE的一个ORM标准,与Hibernate的作用类似。
  • Mybatis:一种半自动ORM,它只负责把ResultSet自动映射为Java Bean,或者自动填充Java Bean参数,但仍需自己写出SQL
  • ORM:Object-Relational Mapping。一种将数据库表的一行记录映射为JavaBean的过程,二者可以互相转换。
  • Profile:程序开发时的不同环境。
  • Spring:一个支持快速开发JavaEE应用程序框架。它提供了一系列底层容器基础设施,而且可以和其他常用的开源框架无缝集成。Spring开发
  • 容器:一种为某种特定组件的运行提供必要支持软件环境底层服务。(Tomcat就是一个Servlet容器,为Servlet的运行提供了运行环境;且实现了TCP连接、HTTP解析底层服务)。
  • 组件:类似BookService、UserService这种以XxxService命名业务逻辑类/JavaBean
  • 注入:Injection;在一个组件内部通过setXxx()构造方法引入外部组件

1、Spring框架的构成

  • 支持IoCAOP容器
  • 支持JDBCORM数据访问模块;
  • 支持声明式事务的模块;
  • 支持基于ServletMVC开发
  • 支持基于ReactiveWeb开发
  • 集成JMS、JavaMail、JMX、缓存等其他模块。

Spring的核心在于提供了一个IOC容器,其作用是管理所有轻量级的JavaBean组件,提供的底层服务组件的生命周期管理、配置与组装服务、AOP支持、建立在AOP基础上的声明式服务等。

2、IoC容器

2022-06-06:IoC容器 - ShineLe - 博客园

IoC:Inversion of Control,控制反转;又称依赖注入(DI:Dependency Injection)。它用来解决组件创建+配置使用分离的问题,同时负责管理组件的生命周期

IoC解决以下问题:

  1. 谁负责创建组件
  2. 谁负责根据依赖关系组装组件
  3. 销毁时,如何按照依赖顺序正确销毁

 

传统设计模式下,控制权程序本身,程序的控制流程开发者控制

也就是说,CartServlet创建了BookService,在创建BookService的过程中,又创建了DataSource组件。这一系列创建过程,是由开发者控制的。这种模式下,一个组件使用另一组件,必须先知道如何正确创建它。

 

IoC模式下,控制权发生了反转,从应用程序转移到了IoC容器组件不再应用程序创建和配置,而是IoC容器负责。此时应用程序只需直接使用已经创建好且配置好组件

为了让组件可以在IOC容器装配出来,需要某种注入机制。注入机制是指,组件不在内部通过new创建,而是等待外部通过某种方式引入共享组件。

复制代码
//改动前,在程序内部new
public class BookService {
    private HikariConfig config = new HikariConfig();
    private DataSource dataSource = new HikariDataSource(config);
    ...
}

//改动后,等待外部注入
public class BookService {
    private DataSource dataSource;

    public void setDataSource(DataSource dataSource) {
        this.dataSource = dataSource;
    }
}
复制代码

这种改动的好处:

  1. BookService不关心如何创建DataSource不必编写读取数据库配置之类的代码;
  2. DataSource可以同时注入BookServiceUserService共享简单
  3. 测试简单,DataSource是被注入的,可以用内存数据库,而非真实的数据库配置。

IoC容器要负责实例化所有组件,因此需要告诉容器如何创建组件,以及各种依赖关系最简单的实现是通过配置XML来实现,例如:

复制代码
<beans>
    <bean id="dataSource" class="HikariDataSource" />
    <bean id="bookService" class="BookService">
        <property name="dataSource" ref="dataSource" />
    </bean>
    <bean id="userService" class="UserService">
        <property name="dataSource" ref="dataSource" />
    </bean>
</beans>
复制代码

这个XML配置文件中,

  • 3个<bean>块3个JavaBean组件

  • <bean>块property属性:将iddataSource组件注入该<bean>块对应的组件中。

在Spring的IoC容器中,组件被称为JavaBean,配置组件就是配置JavaBean。

接下来的2.1、2.2节都是讲通过XML配置文件(application.xml)实现注入的方法,如果要看通过注解的方式注入,请从2.3节开始看。

2.1、依赖注入方式

依赖可以通过setXxx()注入,也可以在构造方法中完成:

复制代码
//通过setXxx注入
public class BookService {
    private DataSource dataSource;

    public void setDataSource(DataSource dataSource) {
        this.dataSource = dataSource;
    }
}

//通过带参构造方法注入
public class BookService {
    private DataSource dataSource;

    public BookService(DataSource dataSource) {
        this.dataSource = dataSource;
    }
}
复制代码

Spring的IoC容器同时支持属性注入(setXxx)构造方法注入,并允许混合使用

2.2、装配Bean

在第2章先导部分和2.1节,介绍了使用Spring的IoC容器的优点

本节介绍如何使用IoC容器,如何使用装配好的Bean

我们通过一个用户注册登录的例子,来介绍IoC容器JavaBean用法

①工程结构

②创建Maven工程,引入spring-context依赖

    <dependencies>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>${spring.version}</version>
        </dependency>
    </dependencies>

③两个业务逻辑类JavaBean和一个Entity类:

User,用户Entity,代表用户的信息与属性

复制代码
package com.itranswarp.learnjava.service;

public class User {
    private long id;
    private String email;
    private String password;
    private String name;

    public User(long id, String email, String password, String name) {
        this.id = id;
        this.email = email;
        this.password = password;
        this.name = name;
    }

    public long getId() {
        return id;
    }

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

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}
复制代码

MailService,负责用户注册和登录成功后的邮件发送

复制代码
public class MailService {
    private ZoneId zoneId = ZoneId.systemDefault();

    public void setZoneId(ZoneId zoneId) {
        this.zoneId = zoneId;
    }

    public String getTime() {
        return ZonedDateTime.now(this.zoneId).format(DateTimeFormatter.ISO_ZONED_DATE_TIME);
    }

    public void sendLoginMail(User user) {
        System.err.println(String.format("Hi, %s! You are logged in at %s", user.getName(), getTime()));
    }

    public void sendRegistrationMail(User user) {
        System.err.println(String.format("Welcome, %s!", user.getName()));

    }
}
复制代码

UserService,实现用户注册与登录

复制代码
public class UserService {
    private MailService mailService;

    public void setMailService(MailService mailService) {
        this.mailService = mailService;
    }

    private List<User> users = new ArrayList<>(List.of( // users:
            new User(1, "bob@example.com", "password", "Bob"), // bob
            new User(2, "alice@example.com", "password", "Alice"), // alice
            new User(3, "tom@example.com", "password", "Tom"))); // tom

    public User login(String email, String password) {
        for (User user : users) {
            if (user.getEmail().qualsIgnoreCase(email) && user.getPassword().equals(password)) {
                mailService.sendLoginMail(user);
                return user;
            }
        }
        throw new RuntimeException("login failed.");
    }

    public User getUser(long id) {
        return this.users.stream().filter(user -> user.getId() == id).findFirst().orElseThrow();
    }

    public User register(String email, String password, String name) {
        users.forEach((user) -> {
            if (user.getEmail().equalsIgnoreCase(email)) {
                throw new RuntimeException("email exist.");
            }
        });
        User user = new User(users.stream().mapToLong(u -> u.getId()).max().getAsLong() + 1, email, password, name);
        users.add(user);
        mailService.sendRegistrationMail(user);
        return user;
    }
}
复制代码

UserService中,通过setMailService()注入了一个MailService

④Application.xml

告诉IoC容器如何创建组装Bean,以及如何将一个Bean注入另一个Bean中:

复制代码
 1 <?xml version="1.0" encoding="UTF-8"?>
 2 <beans xmlns="http://www.springframework.org/schema/beans"
 3        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 4        xsi:schemaLocation="http://www.springframework.org/schema/beans
 5         https://www.springframework.org/schema/beans/spring-beans.xsd">
 6     <bean id="userService" class="com.itranswarp.learnjava.service.UserService">
 7         <property name="mailService" ref="mailService"/>
 8     </bean>
 9     <bean id="mailService" class="com.itranswarp.learnjava.service.MailService" />
10 </beans>
复制代码

其中与XML结构相关的格式是固定的(即前5行),我们只关注两个<bean...>的配置:

  • 每个<bean ...>都有id属性,就是Bean唯一IDJavaBean的名字
  • userService Bean中,通过<property name="..." ref="..."/>注入另一个Bean
  • Bean顺序不重要,Spring会根据依赖关系自动正确初始化。

如果注入的不是Bean,而是int、String这种数据类型,则用value注入,例如:

<bean id="dataSource" class="com.zaxxer.hikari.HikariDataSource">
    <property name="jdbcUrl" value="jdbc:mysql://localhost:3306/test" />
    <property name="username" value="root" />
    <property name="password" value="password" />
    <property name="maximumPoolSize" value="10" />
    <property name="autoCommit" value="true" />
</bean>

注意:User类不是Bean/组件,而是一个Entity信息类,所以不需要写入XML文件中!

⑤创建IoC容器实例ApplicationContext与完整的main

利用这个IoC实例,加载配置文件application.xml,让它为我们创建并配置好该xml文件中指定的所有Bean:

ApplicationContext context = new 
            ClassPathXmlApplicationContext("application.xml")

从IoC实例中,取出装配好的Bean,使用它:

// 获取Bean:
UserService userService = context.getBean(UserService.class);
// 正常调用,就像调用UserService实例一样:
User user = userService.login("bob@example.com", "password");

完整的main()方法:

复制代码
package com.itranswarp.learnjava;
import com.itranswarp.learnjava.service.*;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class Main {
    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("application.xml");
        UserService userService = context.getBean(UserService.class);
        User user = userService.login("bob@example.com", "password");
        System.out.println(user.getName());
    }
}
复制代码

2.2*、IoC容器的本质

IoC容器就是ApplicationContext,它是个接口,有很多实现类,这里选择classPathXmlApplicationContext,表示它会自动从classpath中查找指定的xml配置文件,也就是application.xml

ApplicationContext context = new 
                    ClassPathXmlApplicationContext("application.xml")

持有IoC容器之后,可以用getBean(xxxService.class)获取具体的JavaBean实例:

UserService userService = context.getBean(UserService.class);

 

2.3、使用注解Annotation进行注入

再回顾一下之前两节所说的如何使用IoC容器

  1. 创建XML配置文件application.xml;2.2.④

  2. XML文件中描述Bean的依赖关系 <property name="..." ref="..."/>;2.2.④

  3. 创建IoC容器实例(ApplicationContext),让容器自己创建并装配Bean;2.2.⑤与2.2*

  4. IoC容器初始化完毕后,我们直接从容器获取并使用Bean(getBean(xxxService.class))。2.2.⑤与2.2*

 

通过XML配置Bean的优点

  1. 所有的Bean都通过<Bean...>列出来,对于Bean的观察一目了然;
  2. 通过<properties name="..." ref="..." />这种配置注入,可以直观看到Bean的依赖。

缺点:

  1. 写起来繁琐,每增加一个Bean,就要将其重新配置到XML中。

 

本节介绍更简单配置方式——Annotation配置

使用Annotation配置,无需XML文件,让Spring自动扫描组装Bean

这里我们还用第2.2节所写的两个Bean,包括UserService、MailService。还要提一下,UserEntity信息类并非Bean,也无需在XML中配置它

 

进行Annotation配置的过程:

0 工程结构

①删除XML配置文件;

 

②给两个Bean对应的类添加注解@Component

复制代码
@Component
public class MailService{   
    ...
}

@Component
public class UserService {
    ...
}
复制代码

添加了注解@Component就相当于定义了一个Bean,这个Bean的名称是小写字母开头的类名。上段代码定义了两个Bean:mailService与userService。

 

③在类中引入的外部组件前添加注解@Autowired

@Component
public class UserService {
    @Autowired
    MailService mailService;//mailService是引入的外部组件

    ...
}

使用注解@Autowired相当于把指定的Bean注入指定字段中。

注解@Autowired不仅可以写在字段上,还可以写在方法中,不过一般写在字段,且通常是package权限的字段便于测试

 

启动容器的类AppConfig,也是main()方法所在类

复制代码
@Configuration
@ComponentScan
public class AppConfig {
    public static void main(String[] args) {
        ApplicationContext context = new 
            AnnotationConfigApplicationContext(AppConfig.class); UserService userService = context.getBean(UserService.class); User user = userService.login("bob@example.com", "password"); System.out.println(user.getName()); } }
复制代码

两个注解@Configuration@ComponentScan

  • @Configuration:说明这是一个配置类,与第一行代码相匹配
    ApplicationContext context = new 
              AnnotationConfigApplicationContext(AppConfig.class);
            
  • @ComponentScan:有了这个注解,容器就会自动搜索当前类所在的子包,把所有注解@ComponenetBean自动创建出来,并根据@Autowired进行装配

除了这两个注解,以及IoC容器具体实现类AnnotataionConfigApplicationContext外,之后Bean的获取与使用与之前XML配置的语法和代码一样。

最后,AppConfig包必须放在最顶层,与其他Bean的所在子包位于同一目录

 

总结一下,使用Annotation配合自动扫描进行配置时,需要保证的若干项:

  • 所有Bean都用@Component标注

  • 在一个Bean中,需要注入的Bean权限应该为package,且用@Autowired标注

  • 配置类AppConfig也是main()方法所在类,要用@Configuration@ComponenetScan标注;

  • AppConfig要在Bean的上一级目录,与Bean所在的包同级

 

3、定制Bean

本节介绍一些定制Bean时所用到的一些其他的注解。

3.1、@Scope:原型Bean的注解

原型Bean是与单例Bean相对而言的。

  • 单例(Singleton)Bean:对于标记了@ComponentBean,如果没有其他标记,那么IoC容器初始化时通过getBean(XxxService.class)创建的Bean和容器关闭时销毁的Bean,都是同一个实例

  • 原型(Prototype)Bean:容器每次调用getBean(),都会获得新的实例,这种Bean叫原型Bean。声明原型Bean,还需要@Scope注解。

    @Component
    @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) // @Scope("prototype")
    public class MailSession {
        ...
    }

     

3.2、注入一个List<Bean>中的所有Bean

如果一个List中存放了多个Bean,这些Bean的接口相同实现类不同,如果要将这些Bean都注入进来,并不需要一个一个注入,而是直接注入这个List

复制代码
@Component
public class Validators {
    @Autowired
    List<Validator> validators;

    public void validate(String email, String password, String name) {
        for (var validator : this.validators) {
            validator.validate(email, password, name);
        }
    }
}
复制代码

这样做的好处是,每当外部新创建了一个Bean(即XxxValidator),我们只需要为其标注@Component,Spring就会自动将其装配validators中了。

Spring是通过扫描classpath获取到所有Bean的,如果要指定List中各个Bean的次序,可以在创建Bean的时候添加@Order注解:

复制代码
@Component
@Order(1)
public class EmailValidator implements Validator {
    ...
}

@Component
@Order(2)
public class PasswordValidator implements Validator {
    ...
}

@Component
@Order(3)
public class NameValidator implements Validator {
    ...
}
复制代码

 

3.3、可选注入

如果在标记@Autowired之后,Spring没找到对应的Bean,就会抛出NoSuchBeanDefinitionException异常。

如果要在找不到Bean时选择忽略,可以给@Autowired增加required = false参数,这就是可选注入

@Component
public class MailService {
    @Autowired(required = false)
    ZoneId zoneId = ZoneId.systemDefault();
    ...
}

其作用是,注入时有定义Bean就用,没有就用默认值

 

3.4、创建第三方Bean

以3.3中的ZoneId为例,这就是一个第三方Bean。

第三方Bean是指那些不是我们创建不在我们的package管理之内Bean

如何创建?

答:在我们的配置类(即AppConfig编写一个方法返回这个Bean,并为该方法添加@Bean注解

复制代码
@Configuration
@ComponentScan
public class AppConfig {
    // 创建一个Bean:
    @Bean
    ZoneId createZoneId() {
        return ZoneId.of("Z");
    }
}
复制代码

第三方Bean单例Spring标记@Bean的方法调用一次

 

3.5、Bean的初始化与销毁

 

Bean注入依赖后,有时需要进行初始化(监听消息等)。容器关闭时,有时还需要清理资源(关闭连接池等)。

  • 初始化方法叫init()注解@PostConstruct

        @PostConstruct
        public void init() {
            System.out.println("Init mail service with zoneId = " 
                      + this.zoneId); }
  • 清理方法叫shutdown()注解@PreDestroy;

        @PreDestroy
        public void shutdown() {
            System.out.println("Shutdown mail service");
        }
  • 在使用这两个注解前,要先在xml文件中引入依赖——JSR-250定义的Annotation:

    <dependency>
        <groupId>javax.annotation</groupId>
        <artifactId>javax.annotation-api</artifactId>
        <version>1.3.2</version>
    </dependency>

     

完整的Bean写法为:

复制代码
@Component
public class MailService {
    @Autowired(required = false)
    ZoneId zoneId = ZoneId.systemDefault();

    @PostConstruct
    public void init() {
        System.out.println("Init mail service with zoneId = " + this.zoneId);
    }

    @PreDestroy
    public void shutdown() {
        System.out.println("Shutdown mail service");
    }
}
复制代码

Spring会对该Bean做以下初始化流程:

  • 调用构造方法创建MailService实例;
  • 根据@Autowired进行注入
  • 调用标记@PostConstructinit()方法进行初始化

销毁时,IoC容器会先调用标记@PreDestroyshutdown()方法。

补充:名称不一定是initshutdown,Spring只根据注解判断哪个是初始化,哪个是销毁

 

3.6、使用别名

通常情况下,IoC容器是根据类型创建Bean的,一个类型的Bean只会创建一个实例。但是有些时候,我们需要为一个类型的Bean创建多个实例。例如,连接多个数据库的时候,就要有多个DataSource实例

如果我们用@Bean创建了多个同类型的Bean,就会报错NoUniqueBeanDefinitionException,即出现了重复的Bean定义

复制代码
@Configuration
@ComponentScan
public class AppConfig {
    @Bean
    ZoneId createZoneOfZ() {
        return ZoneId.of("Z");
    }

    @Bean
    ZoneId createZoneOfUTC8() {
        return ZoneId.of("UTC+08:00");
    }
}
复制代码
NoUniqueBeanDefinitionException: No qualifying bean of type 
'java.time.ZoneId' available: expected single matching bean but found 2

这时,就需要为Bean指定别名,有3种方法:

  • @Bean("name");
  • @Bean + @Qualifier("name")
  • @Bean + @Primary
复制代码
@Configuration
@ComponentScan
public class AppConfig {
    @Bean("z")
    ZoneId createZoneOfZ() {
        return ZoneId.of("Z");
    }

    @Bean
    @Qualifier("utc8")
    ZoneId createZoneOfUTC8() {
        return ZoneId.of("UTC+08:00");
    }
@Bean @Primary
// 指定为主要Bean @Qualifier("z") ZoneId createZoneOfZ() { return ZoneId.of("Z"); } }
复制代码

注入的时候,也需要指定Bean名称:

@Component
public class MailService {
    @Autowired(required = false)
    @Qualifier("z") // 指定注入名称为"z"的ZoneId
    ZoneId zoneId = ZoneId.systemDefault();
    ...
}

而在指定@Primary时,如果在注入没有指定Bean的名字,Spring会注入@Primary的Bean。如果要注入别的Bean,需要在注入时指定名称。

@Primary常用于主从两个数据源的情况。

 

4、使用Resource

Resource是指项目的,Spring提供了org.springframework.core.io.Resource,它可以像String、int一样用@Value注入

@Component
public class AppService {
    @Value("classpath:/logo.txt")
    private Resource resource;
    
    ....
}

注入资源时,最常用的路径是classpath:/资源文件名,表示在classpath中搜索该资源文件

注入Resource之后,可以通过Resource.getInputStream()获取输出流,避免自己搜索文件的路径:

@PostConstruct
public void init() throws IOException {
    try (var reader = new BufferedReader(
            new InputStreamReader(resource.getInputStream(), StandardCharsets.UTF_8))) {
        this.logo = reader.lines().collect(Collectors.joining("\n"));
    }
}

 

上边的classpath:/资源文件名,是注入最常用也是最简单的路径。其实也可以指定文件路径

@Value("file:/path/to/logo.txt")
private Resource resource;

如果使用了Maven标准结构,那么所有Resource文件要放入src/main/resource目录下,最终的工程结构为:

 

5、注入配置

配置文件是开发应用程序常用的文件,最常用的配置文件结构是将各种配置项Key=Value的形式放入.properties文件中。

虽然可以把配置文件作为Resource放入classpath中读取,但是这样仍然比较繁琐。

可以用注入的方式获取具体的配置项,有两种方法:

方法一:注解@PropertySource——${Key:DefaultValue}

IoC容器提供了更简单的@PropertySource自动读取配置文件,它是@Configuration配置类注解

@Configuration
@ComponentScan
@PropertySource("app.properties") // 表示读取classpath的app.properties
public class AppConfig {
    ...
}

 

IoC容器看到@PropertySource("app.properties")注解后,会自动读取该配置文件配置文件中的各项Key@Value正常注入

@Value("${app.zone:Z}")
String zoneId;

注入时的语法格式为:

  • "${Key}":读取Key的Value,如果Key不存在,启动将报错
  • "${Key:DefaultValue}":读取Key的Value,如果Key不存在,就是用默认值DefaultValue

还可以把注入的Key写入方法参数中:

@Bean
ZoneId createZoneId(@Value("${app.zone:Z}") String zoneId) {
    return ZoneId.of(zoneId);
}

 

方法二:通过JavaBean持有配置——#{Bean.Key} 

①仍然需要先在AppConfig前用@PropertySource说明配置文件的名称

②通过创建一个简单的JavaBean持有所有配置,这里仍然是通过${Key:DefaultValue}的方式注入

复制代码
@Component
public class SmtpConfig {
    @Value("${smtp.host}")
    private String host;

    @Value("${smtp.port:25}")
    private int port;

    public String getHost() {
        return host;
    }

    public int getPort() {
        return port;
    }
}
复制代码

在需要传入Key的地方,通过#{smtpConfig.host}注入:

@Component
public class MailService {
    @Value("#{smtpConfig.host}")
    private String smtpHost;

    @Value("#{smtpConfig.port}")
    private int smtpPort;
}

利用#{Bean.Key} JavaBean读取属性Key,也就是调用这个Bean的getKey()方法。一个名为XxxConfigBean,它在IoC容器中的默认名称就是xxxConfig(首字母小写),除非用@Qualifier指定了别名

使用一个独立JavaBean持有所有配置,然后在其他Bean中以#{Bean.Key}注入的好处在于:多个Bean可以引用同一个Bean某个属性。

例如,如果SmtpConfig决定从数据库中读取相关配置项,那么MailService注入的@Value("#{smtpConfig.host}")可以不做修改正常运行。

 

6、使用条件装配

6.1、@profile

Spring中,用@profile表示不同的环境

举例说明它的用法,假设我们分别定义了开发、测试、生产三个环境,写作native、test、production

如果我们在创建Bean时加入注解@Profile,那么Spring容器会根据@Profile决定是否创建这个Bean

复制代码
@Configuration
@ComponentScan
public class AppConfig {
    @Bean
    @Profile("!test")
    ZoneId createZoneId() {
        return ZoneId.systemDefault();
    }

    @Bean
    @Profile("test")
    ZoneId createZoneIdForTest() {
        return ZoneId.of("America/New_York");
    }
}
复制代码

在上段代码中,注解为@Profile("test")Bean,Spring容器会用createZoneIdForTest()来创建,否则调用createZoneId()创建。

环境为"!test"表示非test环境

如果要指定以test环境运行程序,可以加上如下JVM参数

-Dspring.profiles.active=test

Spring允许多环境,要满足多个Profile条件,可以写为:

@Bean
@Profile({ "test", "master" }) // 同时满足test和master
ZoneId createZoneId() {
    ...
}

6.2、@Conditional

Spring通过注解@Conditional也可以决定是否创建了某个Bean,与@Profile的区别在于:

  • @Profile根据环境决定是否创建某个Bean;
  • @Conditional根据条件逻辑判断是否创建某个Bean

例如,我们对SmtpMailService添加如下注解:

@Component
@Conditional(OnSmtpEnvCondition.class)
public class SmtpMailService implements MailService {
    ...
}

其含义是,只有满足OnSmtpEnvCondition的条件,才会创建SmtpMailService这个Bean。而这个条件是由我们编写的代码决定的:

public class OnSmtpEnvCondition implements Condition {
    public boolean matches(ConditionContext context, 
                AnnotatedTypeMetadata metadata) {
        return "true".equalsIgnoreCase(System.getenv("smtp"));
    }
}

它返回true的条件是存在环境变量smtp。本例实现了通过环境变量控制是否创建SmtpMailService

Spring只是提供了@Conditional注解,具体的逻辑判断还要我们实现。

Spring Boot中提供了许多用起来很简单的条件注解,例如,用@ConditionalOnProperty判断配置文件中是否存在某个Key=Value;用@ConditionalOnClass判断当前classpath中是否存在某个class;如果存在,就创建MailService

 

复制代码
@Component
@ConditionalOnProperty(name="app.smtp", havingValue="true")
public class MailService {
    ...
}

@Component
@ConditionalOnClass(name = "javax.mail.Transport")
public class MailService {
    ...
}
复制代码

7、AOP

7.1、AOP简介

AOP:Aspect Oriented Programming,面向切面编程。

面向对象编程(OOP)将系统视为多个对象的交互,AOP则把系统分解为不同关注点(切面 Aspect)。其本质就是动态代理

以业务组件BookService为例,其中有几个业务方法

  • createBook:添加新的Book;
  • updateBook:修改Book;
  • deleteBook:删除Book.

每个业务方法中除了实现各自的业务逻辑,还有一些共同的公共事务代码,如安全检查、日志记录、事务处理

如果我们暂时忽略各自的业务逻辑代码,会发现它们在事务代码上是重复的

复制代码
public class BookService {
    public void createBook(Book book) {
        securityCheck();
        Transaction tx = startTransaction();
        try {
            // 核心业务逻辑
            tx.commit();
        } catch (RuntimeException e) {
            tx.rollback();
            throw e;
        }
        log("created book: " + book);
    }
public void updateBook(Book book) {
        securityCheck();
        Transaction tx = startTransaction();
        try {
            // 核心业务逻辑
            tx.commit();
        } catch (RuntimeException e) {
            tx.rollback();
            throw e;
        }
        log("updated book: " + book);
    }
}
复制代码

为了解决重复编写事务代码的问题,可以把事务代码视为切面(Aspect),以某种自动化的方式,将切面织入逻辑代码中,实现Proxy模式

以AOP的视角编写上述业务,需要依次实现核心逻辑(BookService)3个切面逻辑(权限检查、日志、事务。之后再用某种方式,让框架上述3个AspectProxy的方式“织入”BookService中。

7.2、AOP原理

AOP解决的问题是把切面织入核心逻辑中。换句话说,当执行业务逻辑时,先对调用方法进行拦截,在拦截前后进行各项事务处理,以此完成所有业务功能

对AOP的织入,有3种方式:

  • 编译期:编译时,由编译器切面编译进字节码,这种方式需要重新定义新关键字扩展编译器,AspectJ扩展了Java编译器,使用关键字aspect来实现织入

  • 类加载器:在目标类装载JVM时,通过一个特殊的类加载器,对目标类的字节码重新“增强”;

  • 运行期目标对象切面都是普通Java类,通过JVM动态代理功能第三方库实现运行期动态织入

运行期织入是最简单的方式,Spring的AOP就是基于JVM动态代理。由于JVM的动态代理要求实现接口,而如果一个普通类没有业务接口,就要通过CGLIB或者Javassist这些第三方库实现。

AOP本质上就是动态代理,从我们的业务方法中把权限检查、日志、事务等事务方法剥离出来。

AOP对解决某些问题,如事务管理很有用,因为分散的事务代码几乎完全相同,而且他们的参数(如JDBC的Connection)也是固定的。而对另一些问题,如日志就不是那么容易实现,因为打印时经常需要捕获局部变量,而用AOP实现日志只能输出固定格式日志,因此使用AOP必须适合特定场景。

7.3、装配AOP

AOP本质上就是代理模式实现方式。

AOP编程中的一些概念:

  • Aspect:切面,即一个横跨多个核心逻辑的功能,也叫系统关注点
  • Joinpoint:连接点,即定义在应用程序流程的何处插入切面的执行;
  • Pointcut:切入点,即一组连接点的集合;
  • Advice:增强,指特定连接点执行的动作
  • Introduction:引介,指为一个已有的Java对象动态增加新的接口
  • Weaving:织入,指将切面整合程序的执行流程中;
  • Interceptor:拦截器,是一种实现增强的方式
  • Target Object:目标对象,即真正执行业务核心逻辑对象
  • AOP Proxy:AOP代理,是客户端持有增强后的对象引用

 以UserService和MailService为例,这两个Bean属于业务逻辑类,现在我们需要①UserService每个业务方法执行前添加日志;②MailService每个业务方法执行前后添加日志

具体步骤:

①通过Maven在pom.xml中引入对AOP的支持,即spring-aspects依赖:

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aspects</artifactId>
    <version>${spring.version}</version>
</dependency>

引入该依赖后,会自动引入AspectJ,我们可以用它提供的@Aspect注解比较方便地实现AOP。

②定义组件LoggingAspect

复制代码
@Aspect
@Component
public class LoggingAspect {
    // 在执行UserService的每个方法前执行:
    @Before("execution(public * com.itranswarp.learnjava.service.UserService.*(..))")
    public void doAccessCheck() {
        System.err.println("[Before] do access check...");
    }

    // 在执行MailService的每个方法前后执行:
    @Around("execution(public * com.itranswarp.learnjava.service.MailService.*(..))")
    public Object doLogging(ProceedingJoinPoint pjp) throws Throwable {
        System.err.println("[Around] start " + pjp.getSignature());
        Object retVal = pjp.proceed();
        System.err.println("[Around] done " + pjp.getSignature());
        return retVal;
    }
}
复制代码

观察doAccessCheck()方法,我们定义一个@Before注解,注解中的字符串 "execution(public ...)"是告诉AspectJ执行该方法的位置(Where),这里的意思是,在UserService每个public方法前执行以下代码。

观察doLogging()方法,我们定义了一个@Around注解,它在每个public方法前后执行,在这个方法内部先打印日志,再调用方法,在打印日志后返回结果

再看LoggingAspect类,除了用@Component表明它是一个Bean外,还有@Aspect注解,表示需要将@Before@Around标注的方法注入我们上文所说的位置处。

③给@Configuration类加上@EnableAspectJAutoProxy注解:

@Configuration
@ComponentScan
@EnableAspectJAutoProxy
public class AppConfig {
    ...
}

IoC容器看到该注解,就会自动查找带@Aspect注解Bean,之后会根据@Aspect Bean中的@Before、@Around注解方法AOP注入到特定的Bean@Before、@Around字符串中指定的Bean)

 

执行代码后,可以看到如下输出:

[Before] do access check...
[Around] start void com.itranswarp.learnjava.service.MailService.sendLoginMail(User)
Hi, Bob! You are logged in at 2022-06-17T16:34:58.5968565+08:00[Asia/Shanghai]
[Around] done void com.itranswarp.learnjava.service.MailService.sendLoginMail(User)
Bob

这表明业务逻辑执行前后,确实执行了我们定义的Aspect(即LoggingAspect中的方法)。

 

Spring内部实现AOP的逻辑

或者说,LoggingAspect中的方法是如何注入到其他Bean中的?

Spring容器在启动时,为我们自动创建注入了AspectBean的子类,取代了原始的Bean

UserService为例,生成的子类UserServiceAopProxy(原始的UserService实例变成了内部成员变量):

 

复制代码
public UserServiceAopProxy extends UserService {
    private UserService target;
    private LoggingAspect aspect;//原始实例的引用

    public UserServiceAopProxy(UserService target, LoggingAspect aspect) {
        this.target = target;
        this.aspect = aspect;
    }

    public User login(String email, String password) {
        // 先执行Aspect的代码:
        aspect.doAccessCheck();
        // 再执行UserService的逻辑:
        return target.login(email, password);
    }

    public User register(String email, String password, String name) {
        aspect.doAccessCheck();
        return target.register(email, password, name);
    }

    ...
}
复制代码

 

如果我们打印IoC容器中的UserService实例类型,结果类似UserService$$EnhancerBySpringCGLIB$$1f44e01c,它实际上是SpringCGLIB动态创建子类,但对调用方而言感受不到区别

Spring对不同对象的策略不同:

  • 接口类型:使用JDK动态代理
  • 普通Bean:使用CGLIB创建子类
  • final Bean:无法创建子类。

虽然IoC容器内部实现AOP的逻辑比较复杂,但使用时却很简单,再总结一下:

 

  1. 定义事务类(上文的LoggingAspect)和事务执行方法(上文的doAccessCheck()doLogging());

  2. 事务类注解@Aspect事务执行方法注解@Before@Around,(注解指示在何处调用这些方法);

  3. @Configuration类上标记@EnableAspectJAutoProxy

AspectJ的注入语法则比较复杂,参考Spring文档

 

7.4、使用注解装配AOP

在7.3节所讲的注解@Before和@Around后的语句为:

@Before("execution(public * com.itranswarp.learnjava.service.UserService.*(..))")
@Around("execution(public * com.itranswarp.learnjava.service.MailService.*(..))")

这个字符串,还可以写为:

@Before("execution(public * com.itranswarp.learnjava.service.*.*(..))")

表明无差别全覆盖,即包service下的所有Bean的所有方法都被拦截

 

在通过AOP规则进行自动装配时,如果范围不恰当,会导致一些不需要被代理的Bean也被自动代理,也可能导致后续新增的Bean被强制装配。因此,使用AOP时,最好让被装配的Bean知道自己被安排了。

Spring提供了@Transactional来帮助我们确定那些在事务中被调用Bean

复制代码
@Component
public class UserService {
    // 有事务:
    @Transactional
    public User createUser(String name) {
        ...
    }

    // 无事务:
    public boolean isValidName(String name) {
        ...
    }

    // 有事务:
    @Transactional
    public void updateUser(User user) {
        ...
    }
}
复制代码

或者直接在class级别注解,表示所有public方法都被安排

@Component
@Transactional
public class UserService {
    ...
}

通过@Transactional,某个方法是否启用了事务就清楚了。因此在装配AOP时,使用注解是最好的方式。

接下来以一个程序性能监控的例子演示注解实现AOP装配

①定义注解MetricTime

@Target(METHOD)
@Retention(RUNTIME)
public @interface MetricTime {
    String value();
}

②该注解使用位置需要被监控的关键方法

复制代码
@Component
public class UserService {
    // 监控register()方法性能:
    @MetricTime("register")
    public User register(String email, String password, String name) {
        ...
    }
    ...
}
复制代码

③定义@Aspect组件MetricAspect

复制代码
@Aspect
@Component
public class MetricAspect {
    @Around("@annotation(metricTime)")
    public Object metric(ProceedingJoinPoint joinPoint, MetricTime metricTime) throws Throwable {
        String name = metricTime.value();
        long start = System.currentTimeMillis();
        try {
            return joinPoint.proceed();
        } finally {
            long t = System.currentTimeMillis() - start;
            // 写入日志或发送至JMX:
            System.err.println("[Metrics] " + name + ": " + t + "ms");
        }
    }
}
复制代码

@Around("@annotation(metricTime)")的意思是,符合条件的方法@MetricTime注解方法

由于metric()参数类型MetricTime名字metricTime,所以我们通过该参数获取性能监控的名称

@MetricTime注解+MetricAspect可以帮我们自动实现性能监控,只需要方法标注了@MetricTime即可

7.5、AOP易错点

AOP本质上就是一个代理模式。这一点在我们使用AOP时要时刻谨记。

以上所说的不管是AspectJ还是@Annotation,使用AOP都是Spring自动为我们创建一个Proxy,使调用方可以无感知地调用指定方法,而运行期动态织入其他逻辑业务方法。

使用AOP时可能存在一些问题,特别是访问字段使用public final方法

具体的问题这里不展开了,只说注意事项:

  1. 访问被注入的Bean时,总是调用方法非直接访问字段

  2. 编写Bean时,如果可能会被代理,就不要编写public final方法

 

8、访问数据库

JDBC总结一节中已经介绍了一般Java程序访问数据库标准接口JDBC。使用JDBC虽然简单,但是代码比较繁琐,Spring为了简化数据库访问,做了以下工作:

  • 提供了简化的访问JDBC的模板类不必手动释放资源

  • 提供了一个统一的DAO类以实现Data Access Object模式

  • SQLException封装为DataAccessException,这个异常是一个RuntimeException,并且能让我们区分SQL异常的原因,例如,DuplicateKeyException表示违反了一个唯一约束;

  • 能方便集成Hibernate、JPA、MyBatis这些数据库访问框架。

8.1、使用JDBC

JDBC总结一节,Java程序用JDBC接口访问数据库的步骤为:

  1. 创建全局DataSource实例,表示数据库连接池
  2. DataSource实例获取Connection实例
  3. Connection实例创建PreparedStatement实例
  4. 利用PreparedStatement实例执行SQL语句,返回ResultSet——如果是查询,就是查询结果集;如果是修改,就是受影响的行数

在编写JDBC代码时,需要使用try...catch...finally句式,保证正确提交/回滚事务

 

在Spring中,DataSource实例IoC容器创建和管理;之后,Spring提供了JdbcTemplate,可以让我们方便地操作JDBC。通常情况下,我们会实例化一个JdbcTemplate,这个类使用的是Template模式

编写示例/测试代码时,最好用HSQLDB这个数据库,它是用Java编写的关系数据库,可以以内存模式文件模式运行,只有一个jar包,测试时很方便。

以一个例子说明Spring使用JDBC的步骤:

①创建工程spring-data-jdbc,引入如下依赖:

复制代码
<dependencies>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>5.2.0.RELEASE</version>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-jdbc</artifactId>
        <version>5.2.0.RELEASE</version>
    </dependency>
    <dependency>
        <groupId>javax.annotation</groupId>
        <artifactId>javax.annotation-api</artifactId>
        <version>1.3.2</version>
    </dependency>
    <dependency>
        <groupId>com.zaxxer</groupId>
        <artifactId>HikariCP</artifactId>
        <version>3.4.2</version>
    </dependency>
    <dependency>
        <groupId>org.hsqldb</groupId>
        <artifactId>hsqldb</artifactId>
        <version>2.5.0</version>
    </dependency>
</dependencies>
复制代码

②在AppConfig中,构造两个必须的Bean——HikariDataSource、JdbcTemplate

复制代码
@Configuration
@ComponentScan
@PropertySource("jdbc.properties")
public class AppConfig {

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

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

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

    @Bean
    DataSource createDataSource() {
        HikariConfig config = new HikariConfig();
        config.setJdbcUrl(jdbcUrl);
        config.setUsername(jdbcUsername);
        config.setPassword(jdbcPassword);
        config.addDataSourceProperty("autoCommit", "true");
        config.addDataSourceProperty("connectionTimeout", "5");
        config.addDataSourceProperty("idleTimeout", "60");
        return new HikariDataSource(config);
    }

    @Bean
    JdbcTemplate createJdbcTemplate(@Autowired DataSource dataSource) {
        return new JdbcTemplate(dataSource);
    }
}
复制代码

上述配置的一些说明:

  1. @PropertySource("jdbc.properties"):读取数据库配置文件

  2. @Value("${jdbc.url}"):注入配置文件jdbc.properties中的相关配置

  3. 创建一个DataSource实例,它的实际类型是HikariDataSource,创建时需要用到第2项中注入的配置

  4. 创建一个JdbcTemplate实例,它需要在参数中注入DataSource

③配置文件jdbc.properties

# 数据库文件名为testdb:
jdbc.url=jdbc:hsqldb:file:testdb

# Hsqldb默认的用户名是sa,口令是空字符串:
jdbc.username=sa
jdbc.password=

④写一个Bean DatabaseInitializer,在Spring启动初始化数据库表User

复制代码
@Component
public class DatabaseInitializer {
    @Autowired
    JdbcTemplate jdbcTemplate;

    @PostConstruct
    public void init() {
        jdbcTemplate.update("CREATE TABLE IF NOT EXISTS users (" //
                + "id BIGINT IDENTITY NOT NULL PRIMARY KEY, " //
                + "email VARCHAR(100) NOT NULL, " //
                + "password VARCHAR(100) NOT NULL, " //
                + "name VARCHAR(100) NOT NULL, " //
                + "UNIQUE (email))");
    }
}
复制代码

⑤完善访问数据库Bean UserService注入JdbcTemplate

@Component
public class UserService {
    @Autowired
    JdbcTemplate jdbcTemplate;
    ...
}

8.2、JdbcTemplate的用法

Spring提供的JdbcTemplate采用Template模式,提供了一系列以回调为特点的工具方法,目的是避免繁琐的try...catch语句。

 

我们以具体的示例来说明JdbcTemplate的用法。

8.2.1、查询 QUERY

1、T execute(ConnectionCallback<T> action)方法

该方法允许获取Connection,之后做任何基于Connection的操作

复制代码
public User getUserById(long id) {
    // 注意传入的是ConnectionCallback:
    return jdbcTemplate.execute((Connection conn) -> {
        // 可以直接使用conn实例,不要释放它,回调结束后JdbcTemplate自动释放:
        // 在内部手动创建的PreparedStatement、ResultSet必须用try(...)释放:
        try (var ps = conn.prepareStatement("SELECT * FROM users WHERE id = ?")) {
            ps.setObject(1, id);
            try (var rs = ps.executeQuery()) {
                if (rs.next()) {
                    return new User( // new User object:
                            rs.getLong("id"), // id
                            rs.getString("email"), // email
                            rs.getString("password"), // password
                            rs.getString("name")); // name
                }
                throw new RuntimeException("user not found by id.");
            }
        }
    });
}
复制代码

2、T execute(String sql , PreparedStatementCallback<T> action)

复制代码
public User getUserByName(String name) {
    // 需要传入SQL语句,以及PreparedStatementCallback:
    return jdbcTemplate.execute("SELECT * FROM users WHERE name = ?", (PreparedStatement ps) -> {
        // PreparedStatement实例已经由JdbcTemplate创建,并在回调后自动释放:
        ps.setObject(1, name);
        try (var rs = ps.executeQuery()) {
            if (rs.next()) {
                return new User( // new User object:
                        rs.getLong("id"), // id
                        rs.getString("email"), // email
                        rs.getString("password"), // password
                        rs.getString("name")); // name
            }
            throw new RuntimeException("user not found by id.");
        }
    });
}
复制代码

3、T queryForObject(String sql , Object [ ] args , RowMapper<T> rowMapper)

复制代码
public User getUserByEmail(String email) {
    // 传入SQL,参数和RowMapper实例:
  String SQL ="SELECT * FROM users WHERE email = ?"
    return jdbcTemplate.queryForObject(SQL, new Object[] { email },
            (ResultSet rs, int rowNum) -> {
                // 将ResultSet的当前行映射为一个JavaBean:
                return new User( // new User object:
                        rs.getLong("id"), // id
                        rs.getString("email"), // email
                        rs.getString("password"), // password
                        rs.getString("name")); // name
            });
}
复制代码

该方法中,传入SQL以及SQL参数之后,JdbcTemplate会自动创建PreparedStatement自动执行查询并返回ResultSet,我们提供的RowMapper需要做的事情就是把ResultSet的当前行映射成一个JavaBean返回。整个过程中,使用Connection、PreparedStatement、ResultSet不需要我们手动管理

RowMapper(方法的第三个参数)不一定返回JavaBean,实际上它可以返回任何Java对象。例如,使用SELECT COUNT(*)查询时,可以返回Long

public long getUsers() {
    String SQL = "SELECT COUNT(*) FROM users";
    return jdbcTemplate.queryForObject(SQL, null, (ResultSet rs, int rowNum) -> {
        // SELECT COUNT(*)查询只有一列,取第一列数据:
        return rs.getLong(1);
    });
}

4、T query(String sql , Object [ ] args , RowMapper<T> rowMapper)

queryForObject()的区别在于,query()可以返回多行记录

public List<User> getUsers(int pageIndex) {
    int limit = 100;
    int offset = limit * (pageIndex - 1);
    String SQL = "SELECT * FROM users LIMIT ? OFFSET ?";
    return jdbcTemplate.query(SQL, new Object[] { limit, offset },
            new BeanPropertyRowMapper<>(User.class));
}

这个例子中的RowMapper实例,直接用Spring提供的BeanPropertyRowMapper。如果DB结构JavaBean属性名一致,则BeanPropertyRowMapper就可以直接把一行记录转为JavaBean。

 

8.2.2、增 INSERT、删 DELETE、改 UPDATE

int update(SQL , v1 , v2 , ...)

v1、v2、……是SQL语句语句中占位符的填充值

public void updateUser(User user) {
    // 传入SQL,SQL参数,返回更新的行数:
    String SQL = "UPDATE user SET name = ? WHERE id=?";
    if (1 != jdbcTemplate.update(SQL, user.getName(), user.getId())) {
        throw new RuntimeException("User not found by id");
    }
}

 

JdbcTemplate还有许多重载方法,这里不再一一介绍。需要强调的是,JdbcTemplate只是对JDBC的简单封装,目的是减少手动编写的try(resource){...}的代码。对于查询,主要通过RowMapper实现了JDBC结果集Java对象的转换。

8.2.3、JdbcTemplate总结

  • 简单QUERYquery()和queryForObject(),因为只需要提供SQL语句参数RowMapper

  • UPDATEupdate(),因为只需要提供SQL语句参数

  • 复杂操作:execute(ConnectionCallback),因为拿到了Connection就可以做任何JDBC操作。换句话说,任何操作都可以用该方法!

我们使用最多的是各种查询。在设计表结构的时候,能够保证各列JavaBean属性一一对应,那么直接使用BeanPropertyRowMapper就很方便。如果二者不一致,那就需要修改查询语句(一般是指定别名),使结果集的结构JavaBean保持一致

例如,表的列名office_address,而JavaBean属性workAddress,就需要指定别名,改写查询如下:

SELECT id, email, office_address AS workAddress, name 
FROM users WHERE email = ?

 

8.3、使用声明式事务

在JDBC一节曾说过SQL事务,就是一组先后执行的SQL语句,要么全执行(提交),要么全不执行(回滚)。

如果要在Spring中操作事务,没必要手写JDBC事务,可以用Spring提供的高级接口来操作事务。

Spring提供了两个类来处理事务:

  • PlatformTransactionManager:事务管理器

  • TransanctionStatus:事务

手写事务代码的try...catch...语句块步骤为:

①声明事务实例TransanctionStatus tx

TransactionStatus tx = null;

②开启事务,用①中声明的事务实例tx承接tx=txManager.getTransaction(DefaultTransactionDefinition)

③具体JDBC操作,多个jdbcTemplate.xxx()作为一个事务,具体的xxx方法参考8.2节所讲

④提交事务,txManager.commit(tx)

②③④语句放在try语句块中:

复制代码
try {
    // 开启事务:
    tx = txManager.getTransaction(new DefaultTransactionDefinition());
    // 相关JDBC操作:
    jdbcTemplate.update("...");
    jdbcTemplate.update("...");
    // 提交事务:
    txManager.commit(tx);
}
复制代码

⑤以上只是提交事务的过程,如果提交失败SQL执行失败,就需要捕获异常,并回滚事务,这是catch的内容:

catch (RuntimeException e) {
    // 回滚事务:
    txManager.rollback(tx);
    throw e;
}

综合起来就是

复制代码
TransactionStatus tx = null;
try {
    // 开启事务:
    tx = txManager.getTransaction(new DefaultTransactionDefinition());
    // 相关JDBC操作:
    jdbcTemplate.update("...");
    jdbcTemplate.update("...");
    // 提交事务:
    txManager.commit(tx);
} catch (RuntimeException e) {
    // 回滚事务:
    txManager.rollback(tx);
    throw e;
}
复制代码

Spring抽象出PlatformTransactionManagerTransactionStatus的原因是,Spring除了支持JDBC事务外,还支持分布式事务JTA(Java Transaction API)。分布式事务是指多个数据源在分布式环境下实现事务,使用率没JDBC那么高。

PlatformTransactionManager并非现成的,我们需要在AppConfig中定义它所对应的Bean,实际类型为DataSourceTransactionManager:

复制代码
@Configuration
@ComponentScan
@PropertySource("jdbc.properties")
public class AppConfig {
    ...
    @Bean
    PlatformTransactionManager createTxManager(@Autowired DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }
}
复制代码

8.3.1、使用声明式事务

在8.3一开始所说的以编程的方式实现Spring事务仍比较繁琐,更好的方法是通过声明式事务来实现:

①启动声明式事务:在AppConfig中追加PlatformTransactionManager(上一段代码),并添加注解@EnableTransactionManagement

声明了@EnableTransactionManagement之后,就不用再添加@EnableAspectJAutoProxy了(7.3节所讲的注解)。

复制代码
@Configuration
@ComponentScan
@EnableTransactionManagement // 启用声明式事务
@PropertySource("jdbc.properties")
public class AppConfig {
    ...
    @Bean
    PlatformTransactionManager createTxManager(@Autowired DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }
}
复制代码

②对需要事务支持的方法,添加@Transactional注解:

@Component
public class UserService {
    // 此public方法自动具有事务支持:
    @Transactional
    public User register(String email, String password, String name) {
       ...
    }
}

或者更直接的方法,在Bean上添加@Transactional注解,表示所有public方法都有事务支持

@Component
@Transactional
public class UserService {
    ...
}

Spring是如何对②中需要声明式事务的方法,开启事务支持的?原理仍然是AOP代理,即通过自动创建Bean的Proxy实现。

8.3.2、回滚事务

默认情况下,如果发生了RuntimeException,Spring的声明式事务自动回滚。因此在一个事务方法中,如果程序需要回滚事务,只需要抛出RuntimeException

复制代码
@Transactional
public buyProducts(long productId, int num) {
    ...
    if (store < num) {
        // 库存不够,购买失败:
        throw new IllegalArgumentException("No enough products");
    }
    ...
}
复制代码

如果要针对Checked Exception回滚事务,则要在@Transactional中写出来:

@Transactional(rollbackFor = {RuntimeException.class, IOException.class})
public buyProducts(long productId, int num) throws IOException {
    ...
}

上述代码表示在抛出RuntimeException或IOException时,事务将回滚。

为了简化代码,我们可以把业务异常类RuntimeException派生,这样就不必声明任何特殊异常即可让Spring的声明式事务正常工作

复制代码
public class BusinessException extends RuntimeException {
    ...
}

public class LoginException extends BusinessException {
    ...
}

public class PaymentException extends BusinessException {
    ...
}
复制代码

8.3.3、事务边界

在使用事务时,明确事务边界很重要。

通常情况下的事务边界@Transactional注解事务方法开始与结束

现实世界中问题总要复杂一些,比如一个事务方法中又调用了另一个事务方法,如果内部方法抛出异常需要回滚,那么外层方法是否需要回滚呢?

8.3.4、事务传播

要解决上边的问题,先要定义事务传播模型

Spring的声明式事务事务传播定义了几个级别,默认是REQUIRED,即如果当前没有事务就创建新事务,有事务,就加入当前事务中执行。

在定义事务传播时,不能把@Transactional事务方法去掉@Transactional变为普通方法,这样会导致其他地方对该方法的调用变成非事务模式,引起同步性问题

默认的事务传播级别是REQUIRED,它满足绝大部分的需求。还有一些其他的传播级别:

  • SUPPORTS:表示如果有事务,就加入到当前事务,如果没有,那也不开启事务执行。这种传播级别可用于查询方法,因为SELECT语句既可以在事务内执行,也可以不需要事务;
  • MANDATORY:表示必须要存在当前事务并加入执行,否则将抛出异常。这种传播级别可用于核心更新逻辑,比如用户余额变更,它总是被其他事务方法调用,不能直接由非事务方法调用;
  • REQUIRES_NEW:表示不管当前有没有事务,都必须开启一个新的事务执行。如果当前已经有事务,那么当前事务会挂起,等新事务完成后,再恢复执行;
  • NOT_SUPPORTED:表示不支持事务,如果当前有事务,那么当前事务会挂起,等这个方法执行完成后,再恢复执行;
  • NEVER:和NOT_SUPPORTED相比,它不但不支持事务,而且在监测到当前有事务时,会抛出异常拒绝执行;
  • NESTED:表示如果当前有事务,则开启一个嵌套级别事务,如果当前没有事务,则开启一个新事务。

上边这么多事务的传播级别,其实默认的REQUIRED已经满足绝大部分需求,SUPPORT和REQUIRED_NEW在少数情况下会用到,其他基本用不到。

定义传播级别是写在@Transactional中的:

@Transactional( propagation = Propagation.REQUIRES_NEW)

Spring是如何传播事务的呢?

答案是使用ThreadLocal。Spring总是把JDBC相关的ConnectionTransactionStatus实例绑定ThreadLocal。事务正确传播的前提是,不同方法调用同一个线程内。

如果一个事务方法ThreadLocal未取到事务,那么它会打开一个新的JDBC连接,同时开启一个新的事务;否则,它就直接使用ThreadLocal获取到的JDBC连接以及TransactionStatus

 

9、Dao

传统多层应用程序的组成:

  • Web层

  •     ↓调用

  • 业务层:各种业务逻辑

  •     ↓调用

  • 数据访问层:增删改查数据

实现数据访问层就是用JdbcTemplate实现对数据库操作;

编写数据访问层,可以用DAO模式(Data Access Object)。DAO就是一个抽象泛型类XxxDao,继承JdbcDaoSupport,其中持有JdbcTemplate和一些数据修改通用方法get、delete

一个经典的DAO模型:

复制代码
public class UserDao{
    @Autowired
    JdbcTemplate jdbcTemplate;
    
    User getById(long id){
        ...
    }
    
    List<User> getUsers(int pages){
        ...
    }

    User createUser(User user){
        ...
    }

    User updateUser(User user){
        ...
    }

    void deleteUser(user user){
        ...
    }
}
复制代码

这是我们人工编写DAO模型,为了简化DAO的时间,Spring提供了JdbcDaoSupport类,它的核心代码就是持有JdbcTemplate

复制代码
public abstract class JdbcDaoSupport extends DaoSupport {

    private JdbcTemplate jdbcTemplate;

    public final void setJdbcTemplate(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
        initTemplateConfig();
    }

    public final JdbcTemplate getJdbcTemplate() {
        return this.jdbcTemplate;
    }

    ...
}
复制代码

子类JdbcDaoSupport继承后,可以随时调用getJdbcTemplate()获得JdbcTemplate实例。因为JdbcDaoSupportjdbcTemplate字段没有标记@Autowired,所以,子类要想注入JdbcTemplate,还得自己想个方法:

复制代码
@Componenet
@Transactional

public class UserDao extends JdbcDaoSupport{
    @Autowired
    JdbcTemplate jdbcTemplate;

    @PostConstruct
    public void init(){
        super.setIdbcTemplate(jdbcTemplate);
    }
}
复制代码

既然UserDao都已经注入JdbcTemplate,那再把它放到父类中,通过getJdbcTemplate()访问岂不是多此一举?

如果使用传统XML配置,并不需要编写@Autowired JdbcTemplate jdbcTemplate,但是考虑到现在基本上是使用注解的方式,所以我们可以特意编写一个AbstractDao专门负责注入JdbcTemplate

复制代码
public abstract class AbstractDao extends JdbcDaoSupport
    @Autowired
    private JdbcTemplate jdbcTemplate;
    
    @PostConstruct
    public void init(){
        super.setJdbcTemplate(jdbcTemplate);
    }
}
复制代码

这样,子类代码就很干净,可以直接调用getJdbcTemplate():

复制代码
@Component
@Transactional
public class UserDao extends AbstractDao{
    public User getById(long id){
        return getJdbcTemplate().queryForObject(
                "SELECT * FROM users WHERE id = ?",
                new BeanPropertyRowMapper<>(User.class),
                id
                );
    }
    ...
}
复制代码

如果多写一些样版代码,把AbstractDao改写为泛型AbstractDao<T>,并实现getById(),getAll(),deleteById()这样的通用方法

复制代码
public abstract class AbstractDao<T> extends JdbcDaoSupport{
    private String table;
    private Class<T> entityClass;
    private RowMapper<T> rowMapper;
    
    public AbstractDao(){
        //获取当前类型的泛型类型
        this.entityClass = getParameterizedTpe();
        this.table = this.entityClass.getSimpleName().toLowerCase()+"s";
        this.rowMapper = new BeanPropertyRowMapper<>(entityClass);
    }
    
    public T getById(long id){
        return getJdbcTemplate().queryForObject("SELECT * FROM "+ table + " WHERE id = ? ",this.rowMapper,id);
    }

    public List<T> getAll(int pageIndex){
        int limit = 100;
        int offset = limit * (pageIndex - 1);
        return getJdbcTemplate().query("SELECT * FROM "+ table + " LIMIT ? OFFSET ?", new Object[]{limit,offset},this.rowMapper);
    }

    public void deleteById(long id){
        getJdbcTemplate().update("DELETE FROM "+table+"WHERE id = ?",id);
    }
    ...
}
复制代码

这样,从AbstractDao<Xxx>继承的子类XxxDao,就自动获得了这些通用方法

复制代码
@Component
@Transactional
public class UserDao extends AbstractDao<User>{
    //已经有了:
    //User getById(long)
    //List<User> getAll(int)
    //void deleteById(long)
}
@Component
@Transactional
public class BookDao extends AbstractDao<Book>{
    //已经有了:
    //Book getById(long)
    //List<Book> getAll(int)
    //void deleteById(long)
}
复制代码

总之,DAO模式就是一个简单的数据访问模型,但是不一定非要用DAO,有时直接在Service层操作DB也是完全OK的。

 

10、Hibernate

之前在8.2节讲过使用JdbcTemplate的时候,我们用的最多的方法就是List<T> query(String sql,Object[ ] args ,RowMapper rowMapper)。这个RowMapper的作用就是把ResultSet的一行映射为Java Bean

这种把数据库表的一行记录映射为Java对象的过程就是ORMObject-Relational Mapping。ORM既可以把记录转换为Java对象,也可以把Java对象转换为行记录。

使用JdbcTemplate+RowMapper可以看做是最原始的ORM。如果要实现更自动化ORM,可以选择成熟的ORM框架,例如Hibernate

在Spring中集成Hibernate的过程如下:

①pom.xml

加入相关依赖,以引入JDBC驱动连接池Hibernate本身

复制代码
<!-- JDBC驱动,这里使用HSQLDB -->
<dependency>
    <groupId>org.hsqldb</groupId>
    <artifactId>hsqldb</artifactId>
    <version>2.5.0</version>
</dependency>

<!-- JDBC连接池 -->
<dependency>
    <groupId>com.zaxxer</groupId>
    <artifactId>HikariCP</artifactId>
    <version>3.4.2</version>
</dependency>

<!-- Hibernate -->
<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-core</artifactId>
    <version>5.4.2.Final</version>
</dependency>

<!-- Spring Context和Spring ORM -->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>5.2.0.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-orm</artifactId>
    <version>5.2.0.RELEASE</version>
</dependency>
复制代码

②AppConfig

创建DataSource、引入JDBC配置文件、启用声明式事务

复制代码
@Configuration
@Component
@EnableTransactionManagement
@ProperytySource('jdbc.properties')
public class AppConfig{
    @Bean
    DataSource createDataSource(){
        ...
    }
}
复制代码

创建Bean LocalSessionFactoryBean

复制代码
 1 public class AppConfig{
 2     @Bean
 3     LocalSessionFactoryBean createSessionFactory(@Autowired DataSource dataSource){
 4         var props = new Properties();
 5         props.setProperty("bibernate.hbm2ddl.auto","update");//生产环境不要使用         
 6         props.setProperty("hibernate.dialect","org.hibernate.dialect.HSQLDialect");
 7         props.setProperty("hibernate.show_sql","true");
 8         var sessionFactoryBean = new LocalSessionFactoryBean();
 9         sessionFactoryBean.setDataSource(dataSource);
10 
11         //扫描指定的package获取所有的entity class:
12         sessionFactoryBean.setPackagesToScan("com.itranswarp.learnjava.entity");
13         sessionFactoryBean.setHibernateProperties(props);
14         return sessionFactoryBean;
15     }
16 }
复制代码

 

LocalSessionFactoryBean是一个FactoryBean,它会再创建一个SessionFactory

在Hibernate中,

  • Session封装了一个JDBC Connection实例

  • SessionFactory封装JDBC DataSource实例

SessionFactory持有连接池,每次操作数据库的时候,SessionFactory创建一个新Session,相当于从连接池获取一个新的Connection

SessionFactory就是Hibernate提供的最核心的一个对象,但LocalSessionFactoryBean是Spring提供的为了让我们方便创建SessionFactory的类

对上边创建LocalSessionFactoryBean代码的解释:

4~7行,用Properties持有Hibernate初始化SessionFactory时用到的所有设置,常用的设置可以参考Hibernate文档,这里我们只定义了3个设置:

  • hibernate.hbm2ddl.auto = update:表示自动创建数据库的表结构,注意不要在生产环境中启用;
  • hibernate.dialect = org.hibernate.dialect.HSQLDialect:指示Hibernate使用的数据库是HSQLDB。Hibernate使用HQL查询语句,与SQL类似,但在翻译成SQL时,会根据设定的数据库dialect来生成针对数据库优化的SQL;
  • hibernate.show_sql = true:让Hibernate打印执行的SQL,这对于调试很有用,我们可以方便看到Hibernate生成的SQL语句是否符合我们的预期。

8~9行设置了DataSource和Properties;

12、13行,setPackagesToScan()传入了一个package名称,它指示Hibernate扫描这个包下的所有Java类,自动找出能映射为数据表记录的JavaBean。随后我们会仔细讨论如何编写符合Hibernate要求的JavaBean。

创建Bean HibernateTemplateHibernateTransactionManager

复制代码
public class AppConfig{
    @Bean
    HibernateTemplate createHibernateTemplate(@Autowired SessionFactory sessionFactory){
        return new HibernateTemplate(sessionFactory);
    }

    @Bean
    PlatformTransactionManager createTxManager(@Autowired SessionFactory sessionFactory){
        return new HibernateTransactionManager(sessionFactory);
    }
}
复制代码
  • HibernateTransactionManager:Hibernate使用声明式服务所必须的;

  • HibernateTemplate:Spring为了便于我们使用Hibernate提供的工具类,不是非用不可,但推荐使用以简化代码。

③将表结构映射为JavaBean;

1)一个数据库表:

复制代码
CREATE TABLE user
    id BIGINT NOT NULL AUTO_INCREMENT,
    email VARCHAR(100) NOT NULL,
    password VARCHAR(100) NOT NULL,
    name VARCHAR(100) NOT NULL,
    createdAt BIGINT NOT NULL,
    PRIMARY KEY (`id`),
    UNIQUE KEY `email` (`email`)
);
复制代码

各属性列为:

  • id:BIGINT类型,自增主键
  • email:varchar类型,唯一性
  • password:varchar类型
  • name:varchar类型
  • createdAt:varchar类型

2)对应的JavaBean:

复制代码
public class User {
    private Long id;
    private String email;
    private String password;
    private String name;
    private Long createdAt;

    // getters and setters
    ...
}
复制代码

3)给JavaBean添加注解,告诉Hibernate如何将User类映射为user表记录

复制代码
@Entity
public class User{
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(nullable = false, updatable = false)
    public Long getId() { ... }

    @Column(nullable = false, unique = true, length = 100)
    public String getEmail() { ... }

    @Column(nullable = false, length = 100)
    public String getPassword() { ... }

    @Column(nullable = false, length = 100)
    public String getName() { ... }

    @Column(nullable = false, updatable = false)
    public Long getCreatedAt() { ... }
}
复制代码
  • @Entity:标识JavaBean将被用于映射Table。如果JavaBean名Xxx,那么默认情况下的表名应为xxx(首字母小写)。如果实际表名不同,可以追加注解@Table(name="实际表名")

  • @Column:标识属性的映射;nullable指示列是否允许为NULLupdatable指示该列是否允许UPDATElength指示String类型的列每个元素的长度(如果未指定,默认是255)。

  • @Id:标识主键;如果是自增主键,还要追加@GeneratedValue

需要注意的是,以上所有方法返回值都是包装类型,使用Hibernate时,不要用基本类型的属性。

由于不同的JavaBean有许多共同属性,比如id、createdAt等,我们不用在每个JavaBean中重复定义它们,而是把共同字段提取为一个抽象类

复制代码
@MappedSuperclass
public abstract class AbstractEntity{
    private Long id;
    private Long createdAt;

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(nullable = false , updatable = false)
    public Long getId() {...}

    @Column(nullable = false, updatable = false)
    public Long getCreatedAt() { ... }

    @Transient
    public ZonedDateTime getCreatedDateTime(){
        return Instant.ofEpochMilli(this.createAt).atZone(ZoneId.systemDefault());
    }
    
    @PrePersist
    public void preInsert(){
        setCreatedAt(System.currentTimeMillis());
    }
}
复制代码
  • @MappedSuperclassAbstractEntity必须的注解,表示它用于继承

  • @Transient:标注一个方法,它返回一个“虚拟”的属性,即不是从数据库表读取的值;如果没有标注@Transient,否则Hibernate会尝试从数据库读取名为createdDateTime这个不存在的字段从而出错。

  • @PrePersist:标注一个方法,它表示我们将在一个JavaBean持久化到数据库之前(即执行INSERT语句),Hibernate会先执行该方法。

利用AbstractEntity简化User:

复制代码
@Entity
public class User extends AbstractEntity {

    @Column(nullable = false, unique = true, length = 100)
    public String getEmail() { ... }

    @Column(nullable = false, length = 100)
    public String getPassword() { ... }

    @Column(nullable = false, length = 100)
    public String getName() { ... }
}
复制代码

 

补充:

  • 注解来自javax.persistence,是JPA规范的一部分;

  • 通过Spring集成Hibernate时,无需额外XML配置文件。

类似User这种基于ORMJavaBean,称为Entity Bean

 

④对user表进行增删改查

因为使用了Hibernate,因此我们要做的,实际上是对User这个JavaBean进行增删改查,最后将修改后的JavaBean持久化到数据库即可

步骤:

1)编写一个UserService,注入HibernateTemplate以便简化代码:

@Component
@Transactional
public class UserService{
    @Autowired
    HibernateTemplate hibernateTemplate;
}

2)INSERT

INSERT就是持久化JavaBean实例,方法是hibernateTemplate.save(user);

复制代码
public User register(String email, String password, String name) {
    // 创建一个User对象:
    User user = new User();
    // 设置好各个属性:
    user.setEmail(email);
    user.setPassword(password);
    user.setName(name);
    // 不要设置id,因为使用了自增主键
    // 保存到数据库:
    hibernateTemplate.save(user);
    // 现在已经自动获得了id:
    System.out.println(user.getId());
    return user;
}
复制代码

3)DELETE

从表中删除一行记录。由于Hibernate使用id删除记录,因此要正确设置Userid属性才能正常删除记录:

public boolean deleteUser(Long id) {
    User user = hibernateTemplate.get(User.class,id);
    if (user != null){
        hibernateTemplate.delete(user);
        return true;
    }
    return false;
}

4)UPDATE

先更新User的指定属性,然后调用update()方法:

public void updateUser(Long id, String name) {
    User user = hibernaateTemplate.load(User.class , id);
    user.setName(name);
    hibernateTemplate.update(user);
}

前面我们在定义User时,对有的属性标注@Column(updatable=false)。Hibernate在更新记录时,只会把@Column(updatable=true)的属性加入到UPDATE语句中,这样可以提供一层额外的安全性,即如果不小心修改了User的email、createdAt等属性,执行update()时并不会更新对应的数据库列。但也必须牢记,这个功能是Hibernate提供的,如果绕过Hibernate直接通过JDBC执行UPDATE语句仍然可以更新数据库的任意列的值。

综上,我们根据id查询时,可以直接用load()与get(),如果要用条件查询,可以用以下方法。

5)条件查询 WHERE

假设我们要执行条件查询:

SELECT * FROM user WHERE email = ? AND password = ?

有3种方法可以实现:

a、Example查询

第一种方法是使用findByExample(),给出一个User实例,Hibernate把该实例的所有非null属性拼成WHERE条件

public User login(String email , String password){
    User example = new User();
    example.setEmail(email);
    example.setPassword(password);
    List<User> list = hibernateTemplate.findByExample(example);
    return list.isEmpty() ? null : list.get(0);
}

因为example实例只有emailpassword两个属性为非null,所以最终生成的WHERE语句为WHERE email = ? AND password = ?

如果我们把User的createdAt的类型从Long改为longfindByExample()的查询将出问题,原因在于example实例的long类型字段默认值0,导致Hibernate最终生成的WHERE语句意外变成了WHERE email = ? AND password = ? AND createdAt = 0。显然,额外的查询条件将导致错误的查询结果。

这是需要注意的地方,基本类型字段总是会加入WHERE条件!

b、Criteria查询

public User login(String email, String password) {
    DetachedCriteria criteria = DetachedCriteria.forClass(User.class);
    criteria.add(Restrictions.eq("email", email))
            .add(Restrictions.eq("password", password));
    List<User> list = (List<User>) hibernateTemplate.findByCriteria(criteria);
    return list.isEmpty() ? null : list.get(0);
}

DetachedCriteria使用链式语句(即上段代码中的连续add)来添加多个AND条件。和findByExample()相比,findByCriteria()可以组装出更加灵活的WHERE条件,例如:

SELECT * FROM user WHERE (email = ? OR name = ?) AND password = ?

该查询没法用findByExample()实现,用Criteria查询可以实现如下:

复制代码
DetachedCriteria criteria = DetachedCriteria.forClass(User.class);
criteria.add(
    Restrictions.and(
        Restrictions.or(
            Restrictions.eq("email", email),
            Restrictions.eq("name", email)
        ),
        Restrictions.eq("password", password)
    )
);
复制代码

只要组织好Restrictions的嵌套关系,Criteria查询可以实现任意复杂的查询。

c、HQL查询

最后一种常用的查询是直接编写Hibernate内置HQL查询

String SQL= "FROM User WHERE email = ? AND password = ?";
List<User> list = (List<User>) hibernateTemplate.find(SQL,email,password);

和SQL相比,HQL使用类名属性名,由Hibernate自动转换实际表名列名,详细的HQL语法可以参考Hibernate文档

除了可以直接传入HQL字符串外,Hibernate还可以使用一种NamedQuery,它给查询起个名字,然后保存在注解中。使用NamedQuery时,我们要现在User类标注:

复制代码
@NamedQueries(
    @NamedQuery(
        // 查询名称:
        name = "login",
        // 查询语句:
        query = "SELECT u FROM User u WHERE u.email=?0 AND u.password=?1"
    )
)
@Entity
public class User extends AbstractEntity {
    ...
}
复制代码

注意到引入的NamedQueryjavax.persistence.NamedQuery,它和直接传入的HQL有所不同的是,占位符使用?0?1,并且索引从0开始的。

使用NamedQuery只需要引入查询名参数

public User login(String email , String password){
    List<User> list = (List<User>) hibernateTemplate.findByNamedQuery("login",email,password);
    return list.isEmpty() ? null : list.get(0)l;
}

直接写HQL和使用NamedQuery各有优劣。前者可以在代码中直观看到查询语句,后者可以在User类统一管理所有相关查询

 

11、JPA

Hibernate是第一个被广泛应用ORM框架,而JPA则是 Java Persistence API,也是一个ORM接口标准,用户如果使用了JPA,引用的就是javax.persistence这个“标准”包,而非org.hibernate这样的第三方包。

因为JPA只是接口,所以还需要选择一个实现类,我们使用JPA时也完全可以使用Hibernate作为底层实现,但也可以选择其他的JPA提供方,如EclipseLink

Spring内置了JPA集成,并支持选择HibernateEclipseLink作为实现。我们仍以Hibernate作为JPA实现为例子,演示JPA的基本用法。

步骤:

①引入依赖

  • org.springframework:spring-context:5.2.0.RELEASE
  • org.springframework:spring-orm:5.2.0.RELEASE
  • javax.annotation:javax.annotation-api:1.3.2
  • org.hibernate:hibernate-core:5.4.2.Final
  • com.zaxxer:HikariCP:3.4.2
  • org.hsqldb:hsqldb:2.5.0

②Appconifg.java

a、启用声明式事务管理,创建DataSource

@Configuration
@ComponentScan
@EnableTransactionManagement
@PropertySource("jdbc.properties")
public class AppConfig{
    @Bean
    DataSource createDataSource(){ ... }
}

b、创建LocalContainerEntityManagerFactoryBean,并让它再自动创建一个EntityManagerFactory:

复制代码
@Bean
LocalContainerEntityManagerFactoryBean createEntityManagerFactory(@Autowired DataSource dataSource) {
    var entityManagerFactoryBean = new LocalContainerEntityManagerFactoryBean();
    // 设置DataSource:
    entityManagerFactoryBean.setDataSource(dataSource);
    // 扫描指定的package获取所有entity class:
    entityManagerFactoryBean.setPackagesToScan("com.itranswarp.learnjava.entity");
    // 指定JPA的提供商是Hibernate:
    JpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
    entityManagerFactoryBean.setJpaVendorAdapter(vendorAdapter);
    // 设定特定提供商自己的配置:
    var props = new Properties();
    props.setProperty("hibernate.hbm2ddl.auto", "update");
    props.setProperty("hibernate.dialect", "org.hibernate.dialect.HSQLDialect");
    props.setProperty("hibernate.show_sql", "true");
    entityManagerFactoryBean.setJpaProperties(props);
    return entityManagerFactoryBean;
}
复制代码

观察上述代码,除了需要注入DataSource和设定自动扫描的package外,还需要指定JPA提供商,这里使用Spring提供的一个HibernateJpaVendorAdapter,最后,针对Hibernate需要的配置,以Properties的形式注入

c、实例化一个JpaTransactionManager,以实现声明式事务:

@Bean
PlatformTransactionManager createTxManager(@Autowired EntityManagerFactory entityManagerFactory){
    return new JpaTransactionManager(entityManagerFactory);
}

这样,我们就完成了JPA的初始化工作。使用Spring+Hibernate作为JPA实现无需XML配置文件

所有Entity Bean配置上一节相同,全部采用Annotation标注

业务类XxxService通过JPA接口操作数据库

仍然以UserService为例,除了标注@Component@Transactional外,我们需要注入一个EntityManager,但是不用Autowired,而是@PersistenceContext

@Component
@Transactional
public class UserService{
    @PersistenceContext
    EntityManager em;
}

JDBC、Hibernate和JPA提供的接口,关系如下: 

JDBCHibernateJPA
DataSource SessionFactory EntityManagerFactory
Connection Session EntityManager
  • SessionFactoryEntityManagerFactory相当于Source

  • SessionEntityManager相当于Connection

每次需要访问数据库的时候,需要获取新的SessionEntityManager用完后再关闭

但是注意到UserService注入不是EntityManagerFactory而是EntityManager,并且标注了@PersistenceContext。难道使用JPA可以允许多线程操作同一个EntityManager?

实际上这里注入的并非真正的EntityManager,而是一个EntityManager的代理类,相当于:

public class EntityManagerProxy implements EntityManager {
    private EntityManagerFactory emf;
}

标注了@PersistenceContextEntityManager可以被多线程安全地共享。这是JPA注入的核心资源

因此,在UserService的每个业务方法中,直接使用EntityManager就很方便。以主键查询为例:

public User getUserById(long id) {
    User user = this.em.find(User.class, id);
    if (user == null) {
        throw new RuntimeException("User not found by id: " + id);
    }
    return user;
}

JPA同样支持Criteria查询,比如我们需要的查询如下:

SELECT * FROM user WHERE email = ?

使用Criteria查询的代码如下:

复制代码
public User fetchUserByEmail(String email) {
    // CriteriaBuilder:
    var cb = em.getCriteriaBuilder();
    CriteriaQuery<User> q = cb.createQuery(User.class);
    Root<User> r = q.from(User.class);
    q.where(cb.equal(r.get("email"), cb.parameter(String.class, "e")));
    TypedQuery<User> query = em.createQuery(q);
    // 绑定参数:
    query.setParameter("e", email);
    // 执行查询:
    List<User> list = query.getResultList();
    return list.isEmpty() ? null : list.get(0);
}
复制代码

用Criteria写出来的语句比较复杂,很难读懂。

所以我们还是用JPQL查询,它的语法和HQL差不多:

复制代码
public User getUserByEmail(String email) {
    // JPQL查询:
    TypedQuery<User> query = em.createQuery("SELECT u FROM User u WHERE u.email = :e", User.class);
    query.setParameter("e", email);
    List<User> list = query.getResultList();
    if (list.isEmpty()) {
        throw new RuntimeException("User not found by email.");
    }
    return list.get(0);
}
复制代码

同样,JPA也支持NamedQuery,即先给查询起名,再按名字创建查询

public User login(String email, String password) {
    TypedQuery<User> query = em.createNamedQuery("login", User.class);
    query.setParameter("e", email);
    query.setParameter("p", password);
    List<User> list = query.getResultList();
    return list.isEmpty() ? null : list.get(0);
}

NamedQuery通过注解标注User类上,它的定义和上一节的User类一样:

复制代码
@NamedQueries(
    @NamedQuery(
        name = "login",
        query = "SELECT u FROM User u WHERE u.email=:e AND u.password=:p"
    )
)
@Entity
public class User {
    ...
}
复制代码

对数据库进行增删改操作,可以分别使用persist()、remove()、merge()方法,参数均为Entity Bean本身

12、Mybatis

使用HibernateJPA操作数据库时,这类ORM的工作是把ResultSet每一行变为JavaBean,或者相反转换。

我们在JavaBean的属性上给了足够的注解作为元数据ORM框架获取Java Bean的注解后,就知道如何进行双向映射

介于全自动ORMHibernate手写全部JdbcTemplate之间,还有一种半自动ORM,它只负责把ResultSet自动映射为Java Bean,或者自动填充Java Bean参数,但仍需自己写出SQLMyBatis就是这样一种半自动化ORM框架。

在Spring中集成MyBatis的步骤:

①引入MyBatis和MyBatis官方自己开发的一个与Spring集成的库:

  • org.mybatis:mybatis:3.5.4
  • org.mybatis:mybatis-spring:2.0.4

②AppConfig.java

创建DataSource

@Configuration
@ComponenetScan
@EnableTransactionManagement
@PropertySource("jdbc.properties")
public class AppConfig{
    @Bean
    DataSource createDataSource(){ ... }
}

Mybatis的DataSourceConnectionSqlSessionFactorySqlSession

可见,ORM的设计套路都是类似的。

使用MyBatis的核心就是创建SqlSessionFactory,这里我们需要创建的是SqlSessionFactoryBean

@Bean
SqlSessionFactoryBean createSqlSessionFactoryBean(@Autowired DataSource dataSource){
    var sqlSessionFactoryBean = new SqlSessionFactoryBean();
    sqlSessionFactoryBean.setDataSource(dataSource);
    return sqlSessionFactoryBean;
}

创建事务管理器和使用JDBC是一样的:

@Bean
PlatformTransactionManager createTxManager(@Autowired DataSource dataSource){
    return new DataSourceTransactionManager(dataSource);
}

③定义接口UserMapper实现映射

和Hibernate不同的是,MyBatis使用Mapper来实现映射,而且Mapper必须是接口

我们以User类为例,在User类user表之间映射的UserMapper编写如下:

public interface UserMapper{
    @Select("SELECT * FROM users WHERE id = #{id}")
    User getById(@Param("id") long id);
}

这里的Mapper并非JdbcTemplateRowMapper的概念,它是定义访问users表接口方法

1)查询 QUERY

比如主键查询方法User getById(long)中,不仅要定义接口方法本身,还要明确写出查询的SQL,这里用注解@Select标记

SQL语句任何参数,都与方法参数名称对应

例如,方法参数id的名字通过注解@Param()标记为id,则SQL语句中的占位符就是#{id}

如果有多个参数,那么每个参数命名后直接在@Select中的SQL写入对应的占位符即可:

@Select("SELECT * FROM users LIMIT #{offset} , #{maxResults}")
List<User> getAll(@Param("offset") int offset , @Param("maxResults") int maxResults);

注意:MyBatis执行查询后,将根据方法的返回类型自动把ResultSet的每一行转换为User实例,转换规则为按列名属性名相同。如果不同,最简单的方法是SQL中取别名

-- 列名是created_time,属性名是createdAt:
SELECT id, name, email, created_time AS createdAt FROM users

2)插入 INSERT

执行INSERT语句时要传入User实例,因此,定义的方法接口@Insert注解如下:

@Insert("INSERT INTO users (email, password, name, createdAt) VALUES (#{user.email}, #{user.password}, #{user.name}, #{user.createdAt})")
void insert(@Param("user") User user);

上述方法参数User user,在SQL引用的时候,以#{obj.property}的方式写占位符

Hibernate这样的全自动化ORM相比,MyBatis必须写出完整的INSERT语句

如果users表id自增主键,那么我们在SQL中不传入id,如果希望获取插入后的主键,需要再加一个@Options注解:

@Options(useGeneratedKeys = true, keyProperty = "id", keyColumn = "id")
@Insert("INSERT INTO users (email, password, name, createdAt) VALUES (#{user.email}, #{user.password}, #{user.name}, #{user.createdAt})")
void insert(@Param("user") User user);

keyPropertykeyColumn分别指出JavaBean属性数据库主键列名

3)UPDATE与DELETE

@Update("UPDATE users SET name = #{user.name}, createdAt = #{user.createdAt} WHERE id = #{user.id}")
void update(@Param("user") User user);

@Delete("DELETE FROM users WHERE id = #{id}")
void deleteById(@Param("id") long id);

 

④定义实现类,执行以上方法

有了UserMapper接口,还需要对应的实现类才能真正执行这些数据库操作的方法。

MyBatis提供了一个MapperFactoryBean自动创建所有Mapper的实现类。可以用一个简单的注解@MapperScan来启用它:

@MapperScan("com.itranswarp.learn.java.mapper")
...其他注解...
public class AppConfig{
    ...
}

有了@MapperScan,就可以让MyBatis自动扫描指定包所有Mapper创建实现类

在真正的业务逻辑XxxService中,我们可以直接注入

复制代码
@Component
@Transactional
public class UserService {
    // 注入UserMapper:
    @Autowired
    UserMapper userMapper;

    public User getUserById(long id) {
        // 调用Mapper方法:
        User user = userMapper.getById(id);
        if (user == null) {
            throw new RuntimeException("User not found by id.");
        }
        return user;
    }
}
复制代码

业务逻辑主要是通过XxxMapper定义的数据库方法访问数据库

使用MyBatis最大的问题是所有SQL都需要手写,优点是执行的SQL就是我们自己写的SQL,优化很简单,也可以编写复杂的SQL。

MyBatis是一个半自动化的ORM框架,需要手写SQL语句,没有自动加载一对多多对一关系的功能。

posted @   ShineLe  阅读(108)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· 百万级群聊的设计实践
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战
· 永远不要相信用户的输入:从 SQL 注入攻防看输入验证的重要性
历史上的今天:
2021-06-21 Java:Path与Paths
点击右上角即可分享
微信分享提示