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框架的构成
- 支持IoC和AOP的容器;
- 支持JDBC和ORM的数据访问模块;
- 支持声明式事务的模块;
- 支持基于Servlet的MVC开发;
- 支持基于Reactive的Web开发;
- 集成JMS、JavaMail、JMX、缓存等其他模块。
Spring的核心在于提供了一个IOC容器,其作用是管理所有轻量级的JavaBean组件,提供的底层服务有组件的生命周期管理、配置与组装服务、AOP支持、建立在AOP基础上的声明式服务等。
2、IoC容器
2022-06-06:IoC容器 - ShineLe - 博客园
IoC:Inversion of Control,控制反转;又称依赖注入(DI:Dependency Injection)。它用来解决组件的创建+配置与使用分离的问题,同时负责管理组件的生命周期。
IoC解决以下问题:
- 谁负责创建组件;
- 谁负责根据依赖关系组装组件;
- 销毁时,如何按照依赖顺序正确销毁。
传统设计模式下,控制权在程序本身,程序的控制流程由开发者控制。
也就是说,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; } }
这种改动的好处:
- BookService不关心如何创建DataSource,不必编写读取数据库配置之类的代码;
- DataSource可以同时注入到BookService与UserService,共享简单;
- 测试简单,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属性:将id为dataSource的组件注入到该<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的唯一ID(JavaBean的名字);
- 在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容器:
-
创建XML配置文件,application.xml;2.2.④
-
在XML文件中描述Bean的依赖关系 <property name="..." ref="..."/>;2.2.④
-
创建IoC容器实例(ApplicationContext),让容器自己创建并装配Bean;2.2.⑤与2.2*
-
IoC容器初始化完毕后,我们直接从容器获取并使用Bean(getBean(xxxService.class))。2.2.⑤与2.2*
通过XML配置Bean的优点:
- 所有的Bean都通过<Bean...>列出来,对于Bean的观察一目了然;
- 通过<properties name="..." ref="..." />这种配置注入,可以直观看到Bean的依赖。
缺点:
- 写起来繁琐,每增加一个Bean,就要将其重新配置到XML中。
本节介绍更简单的配置方式——Annotation配置
使用Annotation配置,无需XML文件,让Spring自动扫描并组装Bean。
这里我们还用第2.2节所写的两个Bean,包括UserService、MailService。还要提一下,User是Entity信息类,并非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:有了这个注解,容器就会自动搜索当前类所在的包和子包,把所有注解@Componenet的Bean自动创建出来,并根据@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:对于标记了@Component的Bean,如果没有其他标记,那么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进行注入;
- 调用标记@PostConstruct的init()方法进行初始化。
销毁时,IoC容器会先调用标记@PreDestroy的shutdown()方法。
补充:名称不一定是init和shutdown,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()方法。一个名为XxxConfig的Bean,它在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个Aspect以Proxy的方式“织入”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容器在启动时,为我们自动创建了注入了Aspect的Bean的子类,取代了原始的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,它实际上是Spring用CGLIB动态创建的子类,但对调用方而言感受不到区别。
Spring对不同对象的策略不同:
- 对接口类型:使用JDK动态代理;
- 对普通Bean:使用CGLIB创建子类;
- 对final Bean:无法创建子类。
虽然IoC容器内部实现AOP的逻辑比较复杂,但使用时却很简单,再总结一下:
-
定义事务类(上文的LoggingAspect)和事务执行方法(上文的doAccessCheck()和doLogging());
-
事务类注解@Aspect,事务执行方法注解@Before与@Around,(注解指示在何处调用这些方法);
-
在@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方法。
具体的问题这里不展开了,只说注意事项:
-
访问被注入的Bean时,总是调用方法而非直接访问字段;
-
编写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接口访问数据库的步骤为:
- 创建全局DataSource实例,表示数据库连接池;
- 从DataSource实例获取Connection实例;
- 用Connection实例创建PreparedStatement实例;
- 利用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); } }
上述配置的一些说明:
-
@PropertySource("jdbc.properties"):读取数据库配置文件;
-
@Value("${jdbc.url}"):注入配置文件jdbc.properties中的相关配置;
-
创建一个DataSource实例,它的实际类型是HikariDataSource,创建时需要用到第2项中注入的配置;
-
创建一个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总结
-
简单QUERY:query()和queryForObject(),因为只需要提供SQL语句、参数和RowMapper;
-
UPDATE:update(),因为只需要提供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抽象出PlatformTransactionManager与TransactionStatus的原因是,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相关的Connection和TransactionStatus实例绑定在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实例。因为JdbcDaoSupport的jdbcTemplate字段没有标记@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对象的过程就是ORM:Object-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 HibernateTemplate和HibernateTransactionManager:
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指示列是否允许为NULL,updatable指示该列是否允许UPDATE,length指示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()); } }
-
@MappedSuperclass:AbstractEntity必须的注解,表示它用于继承;
-
@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这种基于ORM的JavaBean,称为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来删除记录,因此要正确设置User的id属性才能正常删除记录:
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实例只有email与password两个属性为非null,所以最终生成的WHERE语句为WHERE email = ? AND password = ?。
如果我们把User的createdAt的类型从Long改为long,findByExample()的查询将出问题,原因在于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 { ... }
注意到引入的NamedQuery是javax.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集成,并支持选择Hibernate或EclipseLink作为实现。我们仍以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提供的接口,关系如下:
JDBC | Hibernate | JPA |
---|---|---|
DataSource | SessionFactory | EntityManagerFactory |
Connection | Session | EntityManager |
-
SessionFactory与EntityManagerFactory相当于Source;
-
Session和EntityManager相当于Connection。
每次需要访问数据库的时候,需要获取新的Session与EntityManager,用完后再关闭。
但是注意到UserService注入的不是EntityManagerFactory,而是EntityManager,并且标注了@PersistenceContext。难道使用JPA可以允许多线程操作同一个EntityManager?
实际上这里注入的并非真正的EntityManager,而是一个EntityManager的代理类,相当于:
public class EntityManagerProxy implements EntityManager { private EntityManagerFactory emf; }
标注了@PersistenceContext的EntityManager可以被多线程安全地共享。这是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
使用Hibernate或JPA操作数据库时,这类ORM的工作是把ResultSet的每一行变为JavaBean,或者相反转换。
我们在JavaBean的属性上给了足够的注解作为元数据,ORM框架获取Java Bean的注解后,就知道如何进行双向映射。
介于全自动ORM如Hibernate与手写全部如JdbcTemplate之间,还有一种半自动ORM,它只负责把ResultSet自动映射为Java Bean,或者自动填充Java Bean参数,但仍需自己写出SQL。MyBatis就是这样一种半自动化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的DataSource与Connection为SqlSessionFactory与SqlSession:
可见,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并非JdbcTemplate的RowMapper的概念,它是定义访问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);
keyProperty与keyColumn分别指出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; } }
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· 百万级群聊的设计实践
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战
· 永远不要相信用户的输入:从 SQL 注入攻防看输入验证的重要性
2021-06-21 Java:Path与Paths