Spring读书笔记
1 Spring基本应用
1.1 Spring概述
1.1.1 什么是Spring
Spring是一个轻量级的开源框架,以IOC和AOP为内核,提供了各种整合方案,在表现层提供了Spring MVC的整合功能,在业务层可以管理事务、记录日志等,在持久层可以整合MyBatis等。
1.1.2 优势
- 非侵入式设计
- 方便解耦,简化开发
- 支持AOP,支持声明式事务处理
- 方便程序的测试
- 方便集成各种优秀框架
1.1.3 Spring的体系结构
-
Core Container(核心容器)
核心容器是其他模块建立的基础,主要由Beans模块、Core模块、Context模块、SpEL(spring表达式语言)
- Beans模块:提供了BeanFactory,是工厂模式的经典实现,Spring将管理对象称为Bean。
- Core模块:提供了Spring框架的基本组成部分,包括IoC和DI功能。
- Context上下文模块:建立在Core和Beans模块的基础之上,它是访问定义和配置的任何对象的媒介。
- SpEL模块:提供了Spring Expression Language支持。
-
Data Access/Integration(数据访问/集成)
数据访问集成包括JDBC、ORM、OXM、JMS和Transaction模块,具体介绍如下:
- JDBC模块:提供了一个JDBC的抽象层,大幅度地减少了在开发过程中对数据库操作的编码。
- ORM模块:对流行的对象关系映射API,包括JPA、Hibernate提供了集成层支持。
- OXM模块:提供了一个支持对象/XML映射的抽象层实现。
- JMS模块:指由Java消息传递服务。
- Transaction事务模块:支持对特殊接口以及所有POJO类的编程。
-
Web
Web层包括WebSocket、Servlet、Web和Portlet模块,具体介绍如下:
- WebSocket:提供了WebSocket和SockJS的实现,以及对STOMP的支持。
- Servlet模块:也称为Spring-webmvc模块,包含了Spring的模型-视图-控制器和REST Web Services实现的web应用程序。
- Web模块:提供了基本的Web开发集成特性,例如:多文件上传功能、使用Servlet监听器来出适合IOC和Web应用上下文。
-
其他模块
其他模块有AOP、Aspects、Instrumentation以及Test模块,具体介绍如下:
- AOP模块:提供了面向切面编程实现,允许定义方法拦截器和切入点,将代码按照功能进行分离,以降低耦合性。
- Aspects模块:提供了与AspectJ集成功能,AspectJ是一个功能强大且成熟的面向切面编程框架。
- Instrumentation模块:提供了类工具的支持和类加载器的实现,可以在特定的应用服务器上使用。
- Messaging模块:提供了对消息传递体系结构和协议的支持。
- Test模块:提供了对单元测试和集成测试的支持。
1.2 Spring核心容器
Spring框架提供了两种核心容器:BeanFactory和ApplicationContext。
1.2.1 BeanFactory
简单来说,BeanFactory就是一个管理Bean的工厂,它主要负责初始化各种Bean,并调用它们的声明周期方法。创建BeanFactory实例时,需要提供Spring所管理容器的详细配置信息,通常采用xml文件形式来管理,语法如下:
BeanFactory beanFactory = new XmlBeanFactory(new FileSystemResource("F:/applicationContext.xml"));
这种加载方式在实际开发中并不多用,了解即可。
1.2.2 ApplicationContext
ApplicationContext是BeanFactory的子接口,也被称为应用上下文,是另一种常用的Spring核心容器。不仅包含了BeanFactory的所有功能,还添加了对国际化、资源访问、事件传播等方面的支持。创建ApplicationContext接口实例的方法:
-
通过ClassPathXMLApplicationContext创建,从类路径下找到指定的xml配置文件,并装载。
ApplicationContext applicationContext = new ClassPathXMLApplicationContext(applicationContext.xml);
-
通过FileSystemXMLApplicationContext创建,从指定的文件系统(绝对路径)中寻找指定的XML配置文件。
ApplicationContext applicationContext = new FileSystemXMlApplicationContext(D:/workspace/applicationContext.xml);
该方法缺失灵活性,不推荐使用。
注:通常情况下,在Java项目中,会采用ClassPathXMLApplicationContext类来实例化ApplicationContext容器的方式;而在Web项目中,ApplicationContext容器的实例化工作会交由Web服务器来完成。Web服务器实例化ApplicationContext容器时,通常会使用基于ContextLoaderListener实现的方式,此种方式只需在web.xml中添加如下代码即可:
<!-- 指定Spring配置文件的位置, 多个配置文件时,以逗号分隔 --> <context-param> <param-name>contextConfigLocation</param-name> <!-- Spring将加载spring目录下的applicationcontext.xml文件 --> <param-value> classpath:spring/applicationContext.xml </param-value> </context-param> <!-- 指定以ContextLoaderListener方式启动Spring容器 --> <listener> <listener-class> org.springframework.web.context.ContextLoaderListener </listener-class> </listener>
创建Spring容器后,就可以获取Spring容器中的Bean了。Spring获取Bean的实例通常采用两种方法:
- Object getBean(String name):根据容器中Bean的id或者name来获取指定的Bean,获取之后需要强制转换。如
UserDao userDao = (UserDao) applicationContext.getBean("userDao");
- T getBean(Class requiredType):根据类的类型来获取Bean的实例。由于此方法为泛型方法,因此在获取Bean之后不需要进行强制类型转换。
- Object getBean(String name):根据容器中Bean的id或者name来获取指定的Bean,获取之后需要强制转换。如
1.3 依赖注入(DI/IoC)
依赖注入与控制反转的含义相同,只不过这两个称呼是从两个角度描述的同一个概念。当某个Java对象(调用者)需要调用另一个Java对象(被调用者)时,在传统模式下,调用者通常用采用new方式来创建对象,这种方式会导致调用者和被调用者之间的偶尔性增加,不利于后期项目的升级和维护。在使用Spring框架后,对象的实例不再由调用者来创建,而是由Spring容器来创建,这样控制权就发生了反转。
1.3.1 依赖注入的实现方式
依赖注入的作用就是在使用Spring创建对象时,动态地将其所依赖的对象注入Bean组件中,其实现方式通常有两种,一种是属性setter方法注入,另一种是构造方法注入,具体介绍如下:
-
属性setter方法注入:指IoC容器使用setter方法注入被依赖的实例。通过调用无参构造器或无参静态工厂方法实例化Bean后,调用该Bean的setter方法,即可实现基于setter方法的依赖注入。
-
构造方法注入:指IoC容器使用构造方法注入被依赖的实例。基于构造方法的依赖注入通过调用带参数的构造方法来实现,每个参数代表着一个依赖。
setter方法注入实例
public interface UserService { public void say(); } public class UserServiceImpl implements UserService { // 声明UserDao属性 private UserDao userDao; // 添加UserDao属性的setter方法,用于实现依赖注入 public void setUserDao(UserDao userDao) { this.userDao = userDao; } // 实现接口的方法 public void say() { userDao.say(); } }
xml配置如下:
<bean id="userDao" class="com.itheima.dao.impl.UserDaoImpl"/> <bean id="userService" class="com.itheima.service.impl.UserServiceImpl"> <property name="userDao" ref="userDao"/> </bean> <!--注:name指的是属性名(即set方法去掉set,U变成小写),ref指对象的引用 2. 还要注意ref是注入对象时写的,注入普通参数写要将ref换成value -->
构造方法注入实例
public class UserServiceImpl implements UserService { private UserDao userDao; // 容器当中的dao public UserServiceImpl(UserDao userDao) { this.userDao = userDao; } public UserServiceImpl() { } public void save() { userDao.save(); } }
xml配置
<bean id="userDao" class="com.itheima.dao.impl.userDaoImpl"/> <bean id="userService" class="com.itheima.service.impl.UserServiceImpl"> <constructor-arg name = "userDao" ref="userDao"></constructor-arg> </bean> <!-- 注:name有有参构造方法的参数名,ref是容器里的bean的id的引用 -->
2 Spring中的Bean
2.1 Bean的配置
Spring可以被看成一个大型工厂,这个工厂的作用就是生产和管理Spring容器中的Bean。如果想要在项目中使用这个工厂,就需要开发者对Spring的配置文件进行配置。
Spring容器支持XML和Properties两种格式的配置文件,在实际开发中,**最常使用的就是XML格式的配置方式。**在Spring中,xml配置文件的根元素就是 ,其中包含了多个bean子元素,每一个子元素定义一个Bean,并描述了该Bean如何被装配到Spring容器中。
元素中同样包含了多个属性以及子元素,其常用属性即子元素如下表:
属性或子元素名称 | 描述 |
---|---|
id | 是一个Bean的唯一标识符,Spring容器对Bean的配置、管理都通过该属性来完成 |
name | Spring容器同样可以通过此属性对容器中的Bean进行配置和管理,name属性中可以为Bean指定多个名称,每个名称之间用逗号或者分号隔开 |
class | 该属性指定了Bean的具体实现类,它必须是一个完整的类名,使用类的全限定名 |
scope | 用来设定Bean实例的作用域,其属性值有:singleton(单例)、prototype(原型)、request、session、global Session、application和websocket。其默认值为singleton |
constructor-arg | 元素中的子元素,可以使用此元素传入构造参数进行实例化。该元素的index属性指定构造参数的序号(从0开始),type属性指定构造参数的类型,参数值可以通过ref属性或value属性直接指定,也可以通过ref或value子元素指定 |
property | 元素的子元素,用于调用Bean实例中的setter方法完成属性赋值,从而完成依赖注入。该元素的name属性指定Bean实例中的相应属性名,ref属性或value属性用于指定参数值。 |
ref | 、 等元素的属性或子元素,可以用于指定对 Bean 工厂中 某个 Bean 实例的引用 |
value | 、 等元素的属性或子元素,可以用于直接指定一个常量值 |
list | 用于封装Li st 或数组类型的依赖注入 |
set | 用于封装 Set 类型属性的依赖注入 |
map | 用于封装 Map 类型属性的依赖注入 |
entry |
2.2 Bean的实例化
Bean的实例化有三种方式:
- 构造器实例化(最常用)
- 静态工厂方式实例化
- 实例工厂方式实例化
2.2.1 构造器实例化
构造器实例化是指Spring容器通过Bean对应类中默认的无参构造方法来实例化Bean。
public class Bean1 {
// 默认的无参构造器
}
2.2.2 静态工厂方式实例化
该方式要求开发者创建一个静态工厂的方法来创建Bean的实例,其Bean配置中的class属性所指定的不再是Bean实例的实现类,而是静态工厂类,同时还需要使用factory-method属性来指定所创建的静态工厂方法。
public class MyBean2Factory {
// 使用自己的工厂创建Bean2实例
public static Bean2 createBean() {
return new Bean2();
}
}
2.2.3 实例工厂方式实例化
public class MyBean3Factory {
public MyBean3Factory() {
System.out .prin tln( " bean3 工厂实例化中 ") ;
}
//创建 Bean3 实例的方法
public Bean3 createBean(){
return new Bean3();
}
}
2.3 Bean的作用域
取值范围 | 说明 |
---|---|
singleton | 默认值,表示单例的 |
prototype | 多例的 |
2.4 Bean的生命周期
根据上图,Bean的生命周期执行过程如下:
- 根据配置情况调用Bean的构造方法或工厂方法实例化Bean
- 利用依赖注入完成Bean中所有属性值的配置注入
- 如果Bean实现了BeanNameAware接口,则Spring调用setBeanName()方法传入当前Bean的id值
- 如果Bean实现了BeanFactoryAware接口,则Spring调用setBeanFactory()方法传入当前工厂实例的引用
- 如果Bean实现了ApplicationContextAware接口,则Spring调用setApplicationContext()方法传入当前ApplicationContext实例的引用。
- 如果BeanPostProcessor和Bean关联,则Spring将调用该接口的与初始化方法postProcessBeforelnitialzation()对Bean进行加工操作,这个非常重要,Spring的AOP就是用它实现的
- 如果Bean实现了InitializingBean接口,则Spring将调用afterPropertiesSet()方法
- 如果在配置文件中通过 init-method 属性指定了初始化方法,则调用该初始化方法。
- 如果有 BeanPostProcessor 和 Bean 关联,则Spring将调用该接口的初始化方法postProcessAfterlnitialization()。此时,Bean已经可以被应用系统使用了。
- 如果在中指定了该Bean的作用范围为scope=“singleton”,则将该Bean放入Spring IoC的缓存池中,将触发Spring对该Bean的生命周期管理;如果在中指定了该Bean的作用范围为scope=“prototype”,则将该Bean交给调用者,调用者管理该Bean的生命周期,Spring不在管理该Bean。
- 如果Bean实现了DisposableBean接口,则Spring会调用destory()方法将Spring中的Bean销毁;如果在配置文件中通过destory-method属性制定了Bean的销毁方法,则Spring将调用该方法进行销毁。
Spring 为 Bean 提供了细致全面的生命周期过程,通过实现特定的接口或通过 的属性设置,都可以对Bean的生命周期过程产生影响。我们可以随意地配置的属性,但是建议不要过多地使用Bean实现接口,因为这样会使代码和Spring聚合比较紧密。
2.5 Bean的装配方式
Bean的装配方式可以理解为依赖关系注入,Bean的装配方式即Bean依赖注入的方式。Spring容器支持多种形式的Bean的装配方式,如基于XML的装配、基于注解(Annotation)的装配和自动装配等(其中最常用的是基于注解的装配)。
2.5.1 基于XML的装配
Spring提供了两种基于XML的装配方式:设值注入(Setter Injection)和构造注入(Constructor Injection)。
在Spring实例化Bean的过程中,Spring会首先调用Bean的默认构造方法来实例化Bean对象,然后通过反射的方式调用setter方法来注入属性值。因此,设值注入要求一个Bean必须满足一下两点要求。
-
Bean类必须提供一个默认的无参构造方法。
-
Bean类必须为需要注入的属性提供对应的setter方法。
使用设值注入时,在Spring的配置文件中,需要使用的property子元素来为每个属性注入值;而使用构造方法注入时,在配置文件里,需要使用元素的子元素来定义构造方法的参数,可以使用其value属性来设置该参数的值。
案例演示:
-
先创建一个User类,并在类中定义username、password和list集合三个属性及其对应的setter方法。
public class User { private String username; private Integer password; private List<String> list; /** * 1. 使用构造注入 * 1.1 提供所有参数的有参构造方法。 */ public User(String username, Integer password, List<String> list) { super(); this.username = username; this.password = password; this.list = list; } /** * 2. 使用设值注入 * 2.1 提供默认的空参构造方法; * 2.2 为所有属性提供setter方法 */ public User() { } public void setUsername(String username) { this.username = username; } public void setPassword(Integer password) { this.password = password; } public void setList(List<String> list) { this.list = list; } @Override public String toString() { return "User{" + "username='" + username + '\'' + ", password=" + password + ", list=" + list + '}'; } }
-
配置ApplicationContext.xml
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"> <!--1. 使用构造注入方式装配User实例--> <bean id="user1" class="com.itheima.demo.User"> <constructor-arg index="0" value="tom"/> <constructor-arg index="1" value="123456"/> <constructor-arg index="2"> <list> <value>"value1</value> <value>"value2</value> </list> </constructor-arg> </bean> <!--2. 使用设值注入方式装配User实例--> <bean id="user2" class="com.itheima.demo.User"> <property name="username" value="张三"/> <property name="password" value="654321"/> <!--注入list集合--> <property name="list"> <list> <value>"setvalue1</value> <value>"setvalue2</value> </list> </property> </bean> </beans>
-
测试
public class XmlBeanTest { public static void main(String[] args) { // 1. 加载配置文件 ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml"); // 2. 构造方法输出结果 System.out.println(applicationContext.getBean("user1")); // 2. 设值方式输出结果 System.out.println(applicationContext.getBean("user2")); } }
-
2.5.2 基于Annotation的装配
在Spring中,尽管使用XML配置文件可以实现Bean的装配工作,但如果应用中有很多Bean时,会导致xml配置文件过于臃肿,给后续的升级带来一定的困难。为此,Spring提供了对Annotation技术的全面支持。常用注解如下:
注解 | 说明 |
---|---|
@Component | 使用在类上用于实例化Bean,是一个泛化的概念,仅仅表示一个组件, |
@Controller | 使用在web层类上用于实例化Bean |
@Service | 使用在service层类上用于实例化Bean |
@Repository | 使用在dao层类上用于实例化Bean |
@Autowired | 使用在字段上用于根据类型依赖注入 |
@Qualifier | 结合@Autowired一起使用用于根据名称进行依赖注入 |
@Resource | 相当于@Autowired+@Qualifier,按照名称进行注入 |
@Value | 注入普通属性 |
@Scope | 标注Bean的作用范围 |
@PostConstruct | 使用在方法上标注该方法是Bean的初始化方法 |
@PreDestroy | 使用在方法上标注该方法是Bean的销毁方法 |
注意:
使用注解进行开发时,需要在applicationContext.xml中配置组件扫描,作用是指定哪个包及其子包下的Bean需要进行扫描以便识别使用注解配置的类、字段和方法。
<!--注解的组件扫描-->
<context:component-scan base-package="com.itheima"></context:component-scan>
// 使用@Compont或@Repository标识UserDaoImpl需要Spring进行实例化。
// @Component("userDao")
@Repository("userDao") // 相当于<bean id="userDao" class="xxxxx"/>
public class UserDaoImpl implements UserDao {
public void save() {
System.out.println("save running......");
}
}
使用@Compont或@Service标识UserServiceImpl需要Spring进行实例化
使用@Autowired或者@Autowired+@Qulifier或者@Resource进行userDao的注入
// @Component("userService")
@Service("userService")
public class UserServiceImpl implements UserService {
// 注入普通属性
@Value("${jdbc.driver}")
private String driver;
// <property name="userDao" ref="userDao"></property>
//@Autowired
//@Qualifier("userDao")
@Resource(name = "userDao")
private UserDao userDao;
public void save() {
System.out.println(driver);
userDao.save();
}
}
- Spring的新注解
注解 | 说明 |
---|---|
@Configuration | 用于指定当前类是一个 Spring 配置类,当创建容器时会从该类上加载注解 |
@ComponentScan | 用于指定 Spring 在初始化容器时要扫描的包。 作用和在 Spring 的 xml 配置文件中的 <context:component-scan base-package=“com.itheima”/>一样 |
@Bean | 使用在方法上,标注将该方法的返回值存储到 Spring 容器中 |
@PropertySource | 用于加载.properties 文件中的配置 |
@Import | 用于导入其他配置类 |
2.5.3 自动装配
显然使用注解的方式装配Bean,在一定程度上减少了配置文件中的代码量,但也有企业项目中,是没用使用注解方式开发的,那么就有了自动装配。Spring中的中包含一个autowire属性,可以通过设置autowire的属性值来自动装配。
autowire属性有五个值,如下:
属性值 | 说明 |
---|---|
default(默认值) | 由的上级标签的default-autowire属性值确定。例如,则该元素中的autowire属性对应的属性值就为byName |
byName | 根据属性的名称自动装配 。 容器将根据名称查找与属性完全一致的 Bean ,并将其属性自动装配 |
byType | 根据属性的数据类型 (Type) 自动装配,如果一个 Bean 的数据类型,兼容另一个 Bean 中属性的数据类型,则自动装配 |
constructor | 根据构造函数参数的数据类型,进行byType模式的自动装配 |
no | 在默认情况下,不适用自动装配,Bean依赖必须通过ref元素定义 |
3 Spring AOP
3.1 AOP简介
IoC的目标就是为了管理Bean,而Bean是Java面向对象的基础设计,比如声明一个用户类等都是基于面向对象的概念。有些情况是面向对象没办法处理的,还需要面向切面的编程,AOP是对OOP的一种补充。
在传统的业务处理代码中,通常都会进行事务处理、日志记录等操作。AOP采用横向抽取机制,将分散在各个方法中的重复代码提取出来,然后在程序编译或运行时,再将这些提取出来的代码应用到需要执行的地方。这种采用横向抽取机制的方式,采用传统的OOP思想显然是无法办到的,因为OOP只能实现父子关系的纵向的重用。虽然AOP是一种新的编程思想,但却不是OOP的替代品,它只是OOP的延伸和补充。
在AOP思想中,类与切面的关系如下:
AOP的使用,是开发人员在编写业务逻辑时可以专心于核心业务,而不用过多地关注于其他业务逻辑的实现,这不但提高了开发效率,而且增强了代码的可维护性。目前最流行的AOP框架有Spring AOP和AspectJ。
3.1.1 AOP术语
术语包括Aspect、Joinpoint、Pointcut、Advice、Target Object、Proxy和Weaving。具体介绍如下:
- Target(目标对象):代理的目标对象,即需要进行方法增强的类对象。
- Proxy(代理):一个类被AOP织入增强后,就产生一个结果代理类。
- Joinpoint(连接点):所谓连接点是指那些被拦截到的点。在spring中,这些点指的是方法的调用,因为spring只支持方法类型的连接点。即可以被增强的方法。
- Pointcut(切入点):所谓切入点是指我们要对哪些Joinpoint进行拦截的定义,即真正被增强的方法。通常在程序中,切入点指的是类或者方法名,如某个通知要应用到所有以add开头的方法中,那么所有满足这一规则的方法都是切入点。
- Advice(通知/增强):所谓通知是指拦截到Joinpoint之后所要做的事情。即before,afterReturning等方法,可以理解为切面类中的具体实现。
- Aspect(切面):在实际应用中,切面通常是指封装的用于横向插入系统功能的类,是切入点和通知的结合。即切面=切入点+通知
- Weaving(织入):是指把增强应用到目标对象来创建新的代理对象的过程。spring采用动态代理织入,而AspectJ采用编译期织入和类装载期织入。
3.2 动态代理
AOP的底层是通过Spring提供的动态代理技术实现的。AOP的代理就是由AOP框架动态生成的一个对象,该对象可以作为目标对象使用。Spring中的AOP代理,可以是JDK动态代理,也可以是CGLIB代理。
3.2.1 JDK动态代理
基于接口的代理,对于适用业务接口的类,Spring默认会使用JDK动态代理来实现AOP。
步骤:
-
- 创建目标接口和目标类TargetInterface、Target
public interface TargetInterface { void save(); }
public class Target implements TargetInterface{ public void save() { System.out.println("save running......"); } }
-
- 编写增强类Advice
public void before() { System.out.println("前置增强......"); } public void afterReturning() { System.out.println("后置增强......"); }
-
- 编写动态代理代码ProxyTest
// 目标对象 final Target target = new Target(); // 增强对象 Advice advice = new Advice(); TargetInterface proxy = (TargetInterface) Proxy.newProxyInstance( target.getClass().getClassLoader(), //目标对象的类加载器 target.getClass().getInterfaces(), // 目标对象相同的接口字节码对象数组 new InvocationHandler() { // 调用对象的任何方法 public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { advice.before(); // 执行前置方法 Object invoke = method.invoke(target, args);// 执行目标方法 advice.afterReturning(); // 执行后置方法 return invoke; } } ); proxy.save(); }
3.2.2 CGLIB动态代理
JDK动态代理有一定的局限性——使用动态代理的对象必须实现一个或多个接口,如果要对没有实现接口的类进行代理,那么可以使用CGLIB代理。
步骤:
-
- 编写目标类
public void save() { System.out.println("save running......"); }
-
- 编写增强类
public void before() { System.out.println("前置增强......"); } public void afterReturning() { System.out.println("后置增强......"); }
-
- 编写动态代理测试类
// 目标对象 final Target target = new Target(); // 增强对象 Advice advice = new Advice(); // 返回值,动态代理生成的对象 基于cglib // 1. 创建增强器 Enhancer enhancer = new Enhancer(); // 2. 设置父类(目标) enhancer.setSuperclass(Target.class); // 3. 设置回调 enhancer.setCallback(new MethodInterceptor() { @Override public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable { // 1. 前置增强 advice.before(); // 2. 执行目标 Object invoke = method.invoke(target,args); // 3. 后置增强 advice.afterReturning(); return invoke; } }); // 4. 创建代理对象 Target proxy = (Target) enhancer.create(); proxy.save();
3.3 基于代理类的AOP实现
在Spring中,使用ProxyFactoryBean是创建AOP代理的最基本方式。
3.3.1 Spring的通知类型
Spring的通知类型可以分为以下5种类型:
- 环绕通知:在目标方法执行前后实施增强,可以应用于曰志、事务管理等功能 。
- 前置通知:在目标方法执行前实施增强,可以应用于权限管理等功能 。
- 后置通知:在目标方法执行后实施增强,可以应用于关闭流、上传文件、删除临时文件等功能 。
- 异常通知:在方法抛出异常后实施增强,可以应用于处理异常记录曰志等功能 。
- 引介通知:在目标类中添加一些新的方法和属性,可以应用于修改老版本程序(增强类)。
3.3.2 ProxyFactoryBean
ProxyFactoryBean是FactoryBean接口的实现类,FactoryBean负责实例化一个Bean,而ProxyFactoryBean负责为其他Bean创建代理实例。在Spring中,使用ProxyFactoryBean是创建AOP代理的基本方式。
ProxyFactoryBean的常用属性
属性名称 | 描述 |
---|---|
target | 代理的目标对象 |
ProxyInterface | 代理要实现的接口,如果是多个接口,可以使用以下格式赋值 … |
proxyTargetClass | 是否对类代理而不是接口,设置为true时,使用CGLIB代理 |
interceptorNames | 需要织入目标的Advice |
singleton | 返回的代理是否是单实例,默认为true |
optimize | 当设置为true时,强制使用CGLIB |
示例:
-
创建MyAspect切面类
public class MyAspect implements MethodInterceptor { @Override public Object invoke(MethodInvocation invocation) throws Throwable { check_Permissions(); // 执行目标方法 Object obj = invocation.proceed(); log(); return obj; } private void log() { System.out.println("模拟记录日志...."); } private void check_Permissions() { System.out.println(" 模拟检查权限 . . . "); } }
-
配置ApplicationContext.xml
<!--1. 目标类--> <bean id="userDao" class="com.itheima.proxy.factorybean.UserDaoImpl" /> <!--2. 切面类--> <bean id="myAspect1" class="com.itheima.proxy.factorybean.MyAspect" /> <!--3. 使用Spring代理工厂定义一个名称为userDaoProxy的代理对象--> <bean id="userDaoProxy" class="org.springframework.aop.framework.ProxyFactoryBean"> <!--3.1 指定代理实现的接口--> <property name="proxyInterfaces" value="com.itheima.proxy.factorybean.UserDao" /> <!--3.2 指定目标对象--> <property name="target" ref="userDao" /> <!--3.3 指定切面,植入环绕通知--> <property name="interceptorNames" value="myAspect1" /> <!--3.4 指定代理方式,true:使用cglib,false(默认:)使用jdk动态代理--> <property name="proxyTargetClass" value="true" /> </bean>
-
创建测试类
public class ProxyFactoryBean { public static void main(String[] args) { ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml"); UserDao userDaoProxy = (UserDao) applicationContext.getBean("userDaoProxy"); userDaoProxy.addUser(); userDaoProxy.deleteUser(); } }
3.4 AspectJ开发
AspectJ是一个基于Java语言的AOP框架,它提供了强大的AOP功能。Spring2.0以后,Spring AOP引入了对AspectJ的支持,新版本的Spring框架也建议使用AspectJ来开发AOP。使用它实现AOP有两种方式:一种是基于XML的声明式AspectJ,另一种是基于注解的声明式AspectJ。
3.4.1 基于XML的声明式AspectJ
通过XML文件来定义切面、切入点及通知,所有的切面、切入点和通知都必须定义在< aop:config>元素内。
在上图中,Spring配置文件中的元素下可以包含多个元素,一个<aop: config>又可以包含属性和子元素,其子元素包括、和。在配置时,这3个元素必须按照此顺序来定义。在元素下,同样包含了属性和多个子元素,通过元素及其子元素就可以在XML文件中配置切面、切入点和通知。常用元素的配置代码如下:
<!--1. 定义切面Bean-->
<bean id="myAspect" class="com.itheima.proxy.factorybean.MyAspect" />
<aop:config>
<!--2. 配置切面-->
<aop:aspect id="aspect" ref="myAspect" >
<!--3. 配置切入点 -->
<aop:pointcut id="myPointCut" expression="execution(* com.itheima.proxy.jdk.*.*(..))"/>
<!-- 前置通知 -->
<aop:before method="myBefore" pointcut-ref="myPointCut" />
<!-- 后置通知 -->
<aop:after-returning method="myAfterReturning" pointcut-ref="myPointCut" returning="returnVal" />
<!-- 环绕通知 -->
<aop:around method="myAround" pointcut-ref="myPointCut" />
<!-- 异常通知 -->
<aop:after-throwing method="myAfterThrowing" pointcut-ref="myPointCut" throwing="e" />
<!-- 最终通知 -->
<aop:after method="myAfter" pointcut-ref="myPointCut" />
</aop:aspect>
</aop:config>
详解:
-
配置切面
在Spring的配置文件中,配置切面使用的是元素,该元素会将一个已定义好的Spring Bean转化成为前面Bean,所以要在配置文件中先定义一个已定义好的Spring Bean(如上述代码中定义的myAspect)。定义完成后,通过元素的ref属性即可引用该Bean。配置元素时,通常会指定id和ref两个属性,如下表:
属性名称 描述 id 用于定义该切面的唯一标识符 ref 用于引用普通的Spring Bean -
配置切入点
在Spring的配置文件中,切入点是通过元素来定义的。当元素作为子元素定义时,表示该切入点是全局切入点,它可以被多个切面所共享;当元素作为元素的子元素时,表示该切入点只对当前切面有效。
属性名称 描述 id 用于指定切入点的唯一标识符 expression 用于指定切入点关联的切入点表达式 在上面的代码中
execution(* com.itheima.jdk.*.*(..))
是定义的切入点表达式,第一个*表示返回类型,使用 *表示所有的类型;com.itheima.jdk表示的是需要拦截的包名,后面第2个 *表示的是类名,使用 * 表示的是所有的类;第3个 * 表示的方法名,使用 * 表示的是所有方法;后面(…)表示方法的参数,其中的"…" 表示任意参数。需要注意的是第一个 * 与包名之间有一个空格。 -
配置通知
在配置代码中,分别使用子元素配置了5中常用的通知,如下:
属性名称 描述 pointcut 该属性用于指定一个切入点表达式,Spring将再匹配该表达式的连接点时织入该通知 pointcut-ref 该属性指定一个已经存在的切入点名称,如上述中的myPointCut。通常pointcut和pointcut-ref两个属性只需要使用其中一个即可 method 该属性指定一个方法名,指定将切面Bean中的该方法转换为增强处理 throwing 该属性只对元素有效,它用于指定一个形参名,异常通知方法可以通过该形参访问目标方法所抛出的异常 returning 该属性只对元素有效,它用于制定一个形参名,后置通知方法可以通过该形参访问目标方法的返回值 了解如何在XML中配置切面、切入点和通知后,接下来通过一个案例来演示如何在Spring中使用基于XML的声明式AspectJ,具体步骤如下:
-
创建myAspect切面类
public class MyAspect { // 前置通知 public void myBefore(JoinPoint joinPoint) { System.out.println("前置通知:模拟执行权限检查..."); System.out.print("目标类是:" + joinPoint.getTarget()); System.out.println(",被植入增强处理的目标方法是:" + joinPoint.getSignature().getName()); } // 后置通知 public void myAfterReturning(JoinPoint joinPoint, Object returnVal) { System.out.println("后置通知: 模拟记录日志"); System.out.println("被植入增强处理的目标方法是:" + joinPoint.getSignature().getName()); } /** * 环绕通知 * ProceedingJoinPoint 是JoinPoint子接口,表示可以执行的目标方法 * 1. 必须是Object类型的返回值 * 2. 必须接受一个参数,类型为ProceedingJoinPoint * 3. 必须throws Throwable */ public Object myAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable { // 开始 System.out.println("环绕开始:执行目标方法之前,模拟开启事务..."); // 执行当前目标方法 Object obj = proceedingJoinPoint.proceed(); // 结束 System.out.println("环绕结束:执行目标方法之后,模拟关闭事务..."); return obj; } // 异常通知 public void myAfterThrowing(JoinPoint joinPoint, Throwable e) { System.out.println("异常通知:" + "出错了" + e.getMessage()); } // 最终通知 public void myAfter() { System.out.println("最终通知:模拟方法结束后的释放资源..."); } }
-
配置applicationContex.xml
<!--1. 目标类--> <bean id="userDao" class="com.itheima.proxy.factorybean.UserDaoImpl" /> <!--2. 定义切面Bean--> <bean id="myAspect" class="com.itheima.aspectj.xml.MyAspect" /> <aop:config> <!--3. 配置切面--> <aop:aspect id="aspect" ref="myAspect" > <!--3.1 配置切入点 --> <aop:pointcut id="myPointCut" expression="execution(* com.itheima.proxy.factorybean.*.*(..))"/> <!-- 前置通知 --> <aop:before method="myBefore" pointcut-ref="myPointCut" /> <!-- 后置通知 returning属性:用于设置后置通知的第二个参数的名称,类型是Object --> <aop:after-returning method="myAfterReturning" pointcut-ref="myPointCut" returning="returnVal" /> <!-- 环绕通知 --> <aop:around method="myAround" pointcut-ref="myPointCut" /> <!-- 异常通知 --> <aop:after-throwing method="myAfterThrowing" pointcut-ref="myPointCut" throwing="e" /> <!-- 最终通知 --> <aop:after method="myAfter" pointcut-ref="myPointCut" /> </aop:aspect> </aop:config>
-
创建测试类
// 测试类 public class TestXmlAspectJ { public static void main(String[] args) { ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml"); UserDao userDao = (UserDao) applicationContext.getBean("userDao"); userDao.addUser(); } }
-
3.4.2 基于注解的声明式AspectJ
与基于代理类的AOP实现相比,基于XML的声明式AspectJ要便捷的多,但它仍需要在Spring文件中配置大量的代码信息。为了解决这个问题,AspectJ框架为AOP的实现提供了一套注解,用于取代Spring配置文件中为实现AOP功能所配置的臃肿代码。注解介绍如下:
注解名称 | 描述 |
---|---|
@Aspect | 用于定义一个切面 |
@Pointcut | 用于定义切入点表达式。在使用时还需定义一个包含名字和任意参数的方法签名来表示切入点名称。实际上,这个方法签名就是一个返回值为void,且方法体为空的普通的方法。 |
@Before | 用于定义前置通知,相当于BeforeAdvice。在使用时,通常需要指定一个value属性值,该属性值用于指定一个切入点表达式(可以是已有的切入点,也可以直接定义切入点表达式) |
@AfterReturning | 用于定义后置通知,相当于AfterReturningAdvice。在使用时,可以指定pointcut/value和returning属性,其中pointcut/value这两个属性的作用相同,都用于指定切入点表达式。returning属性值用于表示Advice方法中可定义与此同名的形参,该形参可用于访问目标方法的返回值。 |
@Around | 用于定义环绕通知,相当于MethodInterceptor。在使用时需要指定一个value属性,该属性用于指定该同志被植入的切入点。 |
@AfterThrowing | 用于定义异常通知来处理程序中未处理的异常,相当于ThrowAdvice。在使用时可指定pointcut/value和throwing属性。其中pointcut/value用于指定切入点表达式,而throwing属性值用于指定一个形参名来表示Advice方法中与此同名的参数,该形参可用于访问目标方法抛出的异常。 |
@After | 用于定义最终的final通知,不管是否异常,该通知都会执行。使用时需要指定一个value属性,该属性用于指定该通知被植入的切入点。 |
@DeclareParents | 用于定义引介通知,相当于IntoductionInterceptor(不需要掌握) |
用注解实现上一个小案例:
-
编写Myaspect类
@Aspect @Component public class MyAspect { // 定义切入点表达式 @Pointcut("execution(* com.itheima.aspectj.anno.*.*(..))") // 使用一个返回值为空方法体为空的方法来命名切入点 private void myPointCut() {}; // 前置通知 @Before("myPointCut()") public void myBefore(JoinPoint joinPoint) { System.out.println("前置通知:模拟执行权限检查..."); System.out.print("目标类是:" + joinPoint.getTarget()); System.out.println(",被植入增强处理的目标方法是:" + joinPoint.getSignature().getName()); } // 后置通知 @AfterReturning(value = "myPointCut()") public void myAfterReturning(JoinPoint joinPoint) { System.out.println("后置通知: 模拟记录日志"); System.out.println("被植入增强处理的目标方法是:" + joinPoint.getSignature().getName()); } /** * 环绕通知 * ProceedingJoinPoint 是JoinPoint子接口,表示可以执行的目标方法 * 1. 必须是Object类型的返回值 * 2. 必须接受一个参数,类型为ProceedingJoinPoint * 3. 必须throws Throwable */ @Around("myPointCut()") public Object myAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable { // 开始 System.out.println("环绕开始:执行目标方法之前,模拟开启事务..."); // 执行当前目标方法 Object obj = proceedingJoinPoint.proceed(); // 结束 System.out.println("环绕结束:执行目标方法之后,模拟关闭事务..."); return obj; } // 异常通知 @AfterThrowing(value = "myPointCut()", throwing = "e") public void myAfterThrowing(JoinPoint joinPoint, Throwable e) { System.out.println("异常通知:" + "出错了" + e.getMessage()); } // 最终通知 @After("myPointCut()") public void myAfter() { System.out.println("最终通知:模拟方法结束后的释放资源..."); } }
-
在目标类的UserDaoImpl中,添加注解@Repository(“userDao”)
-
配置applicationContext.xml
首先引入context约束信息
<!--指定需要扫描的包,使注解生效--> <context:component-scan base-package="com.itheima.aspectj" /> <!--启动基于注解的声明式AspectJ支持--> <aop:aspectj-autoproxy />
-
测试
// 测试类 public class TestXmlAspectJ { public static void main(String[] args) { ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml"); UserDao userDao = (UserDao) applicationContext.getBean("userDao"); userDao.addUser(); } }
3.5 总结
列举AOP专业术语步骤:脑子里先出现这几个类,即切面类Aspect、代理类Proxy、目标对象Target(UserDao)
其中Aspect类就是切面Aspect,Aspect中的每个方法是通知Advice,目标对象UserDao是目标对象Target,UserDao的所有方法是连接点Joinpoint可以被增强的方法,UserDao中真正被增强的方法是切入点pointcut,将通知应用到目标对象之后被动态创建的对象称为代理Proxy,将切面代码插入到目标对象上的过程是织入Weaving。
4 Spring的数据库开发
4.1 Spring JDBC
Spring的JDBC模块负责数据库资源管理和错误处理,大大简化了开发人员对数据库的操作。
4.1.1 Spring JdbcTemlate的解析
针对数据库的操作,Spring框架提供了JdbcTemplate类,该类是Spring框架数据抽象层的基础,其他更高层次的抽象类都是构建于JdbcTemplate类之上。可以说JdbcTemplate类是Spring JDBC的核心类。
从上图可以看出,JdbcTemplate类的直接父类是JdbcAccessor,该类为子类提供了一些访问数据库时使用的公共属性,如下:
-
DataSource:其主要功能是获取数据库连接,具体实现时还可以引入对数据库连接的缓冲池和分布式事务的支持,可以作为访问数据库资源的标准接口。
-
SQLExceptionTranslator:该接口负责对SQLException进行转译工作。
JdbcOperations接口定义了在JdbcTemplate类中可以使用的操作集合,包括添加、修改、查询和删除等操作。
4.1.2 Spring JDBC的配置
Spring JDBC模块由4个包组成,分别是core(核心包)、DataSource(数据源包)、object(对象包)和support(支持包),具体说明如下:
包名 | 说明 |
---|---|
core | 包含了JDBC的核心功能,包括JdbcTemplate类、SimpleJdbcInsert类、SimpleJdbcCall类以及NamedParameterJdbcTemplate类 |
dataSource | 访问数据源的实用工具类,它有多种数据源的实现,可以在Java EE容器外部测试JDBC代码 |
object | 以面向对象的方式访问数据库,它允许执行查询并将返回结果作为业务对象,可以在数据表的列和业务对象的属性之间映射查询结果 |
support | 包含了core和object包的支持类,例如提供异常转化功能的SQLException类 |
JDBC的配置在applicationContext.xml中完成,配制模板如下:
<!--1. 配置数据源 -->
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<!--数据库驱动-->
<property name="driverClassName" value="com.mysql.jdbc.Driver" />
<!--连接数据库的url-->
<property name="url" value="jdbc:mysql://localhost:3306/test" />
<!--连接数据库的用户名-->
<property name="username" value="root" />
<!--连接数据库的密码-->
<property name="password" value="root" />
</bean>
<!--2. 配置JDBC模板-->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<!--默认必须使用数据源-->
<property name="dataSource" ref="dataSource" />
</bean>
<!--3. 配置注入类-->
<bean id="xxx" class="xxx">
<property name="jdbcTemplate" ref="jdbcTemplate" />
</bean>
在上面代码中,定义了3个Bean,分别是DataSource、JdbcTemplate和需要注入类的Bean。其中dataSource对应DriverManagerDataSource类用于对数据源进行配置,JdbcTemplate对应JdbcTemplate类定义了JdbcTemplate相关的配置。
4.2 Spring JdbcTemplate的常用方法
#### 4.2.1 execute()
execute(String sql) 方法能够完成执行SQL语句的功能。
-
创建test数据库
-
配置applicationContext.xml文件
<!--1. 配置数据源 --> <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource"> <!--数据库驱动--> <property name="driverClassName" value="com.mysql.jdbc.Driver" /> <!--连接数据库的url--> <property name="url" value="jdbc:mysql://localhost:3306/test" /> <!--连接数据库的用户名--> <property name="username" value="root" /> <!--连接数据库的密码--> <property name="password" value="root" /> </bean> <!--2. 配置JDBC模板--> <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate"> <!--默认必须使用数据源--> <property name="dataSource" ref="dataSource" /> </bean>
-
创建JdbcTemplateTest测试类
public class JdbcTemplateTest { /* * 使用execute()建表 * */ public static void main(String[] args) { // 加载配置文件 ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml"); // 获取javaBean JdbcTemplate jdbcTemplate = (JdbcTemplate) applicationContext.getBean("jdbcTemplate"); // 使用execute()执行SQL语句 jdbcTemplate.execute("create table account1(id int primary key auto_increment," + "username varchar (50)," + "balance double )"); System.out.println("账户表account创建成功!"); } }
4.2.2 update()
update方法可以完成插入、更新和删除数据的操作。如下表:
方法 | 说明 |
---|---|
int update(String sql) | 该方法是最简单的update方法重载形式,它直接执行传入的SQL语句,并返回受影响的行数 |
int update(PreparedStatementCreator psc) | 该方法返回受影响的行数 |
int update(String sql, PreparedStatementSetter psc) | 该方法通过PreparedStatementSetter 设置SQL语句的参数,并返回受影响的行数 |
int update(String sql, Object… args) | 该方法使用Object…设置SQL语句中的参数,要求 参数不能为null |
案例如下:
-
创建一个Account类
public class Account1 { private Integer id; // 账户id private String username; // 用户名 private Double balance; // 账户余额 public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public Double getBalance() { return balance; } public void setBalance(Double balance) { this.balance = balance; } @Override public String toString() { return "Account1{" + "id=" + id + ", username='" + username + '\'' + ", balance=" + balance + '}'; } }
-
创建AccountDao和AccountDaoImpl类
public class Account1DaoImpl implements Account1Dao { // 声明JdbcTemplate属性及其setter方法(set注入) private JdbcTemplate jdbcTemplate; public void setJdbcTemplate(JdbcTemplate jdbcTemplate) { this.jdbcTemplate = jdbcTemplate; } // 添加账户 @Override public int addAccount(Account1 account) { // 1. 定义sql String sql = "insert into account1(username, balance) value(?, ?)"; // 2. 定义数组来存储SQL语句中的参数 Object[] obj = new Object[] { account.getUsername(), account.getBalance() }; // 3. 执行添加操作 int num = this.jdbcTemplate.update(sql, obj); return num; } // 更新账户 @Override public int updateAccount(Account1 account) { String sql = "update account1 set username = ?, balance=? where id = ?"; Object[] params = new Object[] { account.getUsername(), account.getBalance(), account.getId() }; int num = this.jdbcTemplate.update(sql, params); return num; } @Override public int deleteAccount(int id) { String sql = "delete from account1 where id = ?"; int num = this.jdbcTemplate.update(sql, id); return num; } }
-
修改applicationContext.xml配置
<!--3. 配置注入类accountDao--> <bean id="accountDao" class="com.itheima.jdbc.Account1DaoImpl"> <!--将jdbcTemplate注入到accountDao实例中--> <property name="jdbcTemplate" ref="jdbcTemplate" /> </bean>
-
在测试类JdbcTemplateTest中,进行测试
@Test public void addAccountTest() { // 加载配置文件 ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml"); Account1Dao account1Dao = (Account1Dao) applicationContext.getBean("accountDao"); // 创建Account1对象,并向对象中添加数据 Account1 account1 = new Account1(); account1.setUsername("pig"); account1.setBalance(10000.00); // 执行addAccount方法,并获取返回结果 int num = account1Dao.addAccount(account1); if (num > 0) { System.out.println("成功插入了" + num + "条数据!"); } else { System.out.println("插入操作执行失败!"); } } @Test public void updateAccountTest() { ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml"); Account1Dao accountDao = (Account1Dao) applicationContext.getBean("accountDao"); Account1 account1 = new Account1(); account1.setId(1); account1.setUsername("tom"); account1.setBalance(2000.00); int num = accountDao.updateAccount(account1); if (num > 0) { System.out.println("成功修改了" + num + "条数据!"); } else { System.out.println("修改操作执行失败!"); } }
4.2.3 query()
query方法来处理对数据库表的查询操作,常用的几个query()方法如下:
方法 | 说明 |
---|---|
List query(String sql, RowMapper rowMapper) | 执行String类型参数提供的SQL语句,并通过RowMapper返回一个List类型的结果 |
List query(String sql, PreparedStatementSetter pss, RowMapper rowMapper) | 根据String类型参数提供的SQL语句创建PreparedStatement对象,通过RowMapper将结果返回到List中 |
List query(String sql, Object[] args, RowMapper rowMapper) | 使用Object[]的值来设置SQL语句中的参数值,采用RowMapper回调方法可以直接返回List类型的数据 |
queryForObject(String sql, RowMapper rowMapper, Object… args) | 将args参数绑定到SQL语句中,并通过RowMapper返回一个Object类型的单行记录 |
queryForList(String sql, Object[] args, class elementType) | 该方法可以返回多行数据的结果,但必须是返回列表,elementType参数返回的是List元素类型 |
案例演示:
-
在AccountDao和AccountDaoImpl中添加查询代码
// 通过id查询 @Override public Account1 findAccountById(int id) { // 定义sql String sql = "select * from Account1 where id = ?"; // 定义rowMapper RowMapper<Account1> rowMapper = new BeanPropertyRowMapper<>(Account1.class); // 执行queryForObject,并返回 Account1 account1 = this.jdbcTemplate.queryForObject(sql, rowMapper, id); return account1; } @Override public List<Account1> findAllAccount() { // 定义sql String sql = "select * from Account1"; // 定义RowMapper RowMapper<Account1> rowMapper = new BeanPropertyRowMapper<>(Account1.class); // 执行query并返回 List<Account1> query = this.jdbcTemplate.query(sql, rowMapper); return query; }
-
测试
@Test public void queryById() { ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml"); Account1Dao accountDao = (Account1Dao) applicationContext.getBean("accountDao"); Account1 accountById = accountDao.findAccountById(1); System.out.println(accountById.toString()); } @Test public void queryAll() { ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml"); Account1Dao accountDao = (Account1Dao) applicationContext.getBean("accountDao"); List<Account1> allAccount = accountDao.findAllAccount(); for (Account1 account1 : allAccount) { System.out.println(account1.toString()); } }
5 Spring的事务管理
5.1 Spring事务管理概述
5.1.1 事务管理的核心接口
在Spring的所有jar包中,有个spring-tx的jar包,该包是Spring提供的用于事务管理的依赖包。在该包中有3个接口文件PlatformTransactionManager、TransactionDefinition和TransactionSatus。具体介绍:
-
PlatformTransactionManager
该接口主要用于管理事务,提供了3个事务操作的方法:
- TransactionStatus getTransaction(TransactionDefinition definition):用于获取事务状态信息。
- void commint(TransactionStatus status):用于提交事务
- void rollback(TransactionStatus status):用于回滚事务。
-
TransactionDefinition
该接口是事务定义的对象,该对象中定义了事务规则,并提供了获取事务相关信息的方法:
- String getName():获取事务对象名称。
- int getIsolationLevel():获取事务的隔离级别。
- int getPropagatonBehavior():获取事务的传播行为。
- int getTimeout():获取事务的超时时间。
- boolean isReadOnly():获取事务是否只读。
Spring的默认传播行为是REQUIRED
-
TransactionStatus
该接口是事务的状态,它描述了某一时间点上事务的状态信息。该接口中包含6个方法:
- void flush():刷新事务
- boolean hasSavepoint():获取是否存在保存点
- boolean isCompleted():获取事务是否完成
- boolean isNewTransaction():获取是否是新事务
- boolean isRollbackOnly():获取是否回滚
- void setRollbackOnly():设置事务回滚
5.1.2 事务管理的方式
Spring中事务管理分为两种方式:一种是传统的编程式事务管理,另一种是声明式事务管理。
-
编程式事务管理:是通过编写代码实现的事务管理,包含定义事务的开始、正常执行后的事务提交和异常时事物的回滚。
-
声明式事务管理:是通过AOP技术实现的事务管理,其主要思想是将事务管理作为一个“切面”代码单独编写,然后通过AOP技术将事务管理的切面代码织入到业务目标类中。
声明式事务管理最大的优点在于开发者无须通过编程的方式来管理事务,只需在配置文件中进行相关的事务规则声明,就可以将事务规则应用到业务逻辑中。
5.2 声明式事务管理
声明式事务管理可以通过两种方式来实现,一种是基于XML的方式,另一种是基于Annotation的方式。
5.2.1 基于XML方式的声明式事务管理
基于 XML 方式的声明式事务管理是通过在配置文件中配置事务规则的相关声明来实现的 。 Spring2.0之后,提供了tx命名空间来配置事务,元素来配置事务的通知。当使用<tx: advice>元素配置了事务的增强处理后,就可以通过编写的AOP配置,让Spring自动对目标生成代理。
配置<tx: advice>元素时,通常需要指定id和transaction-manager属性,其中id属性是配置文件中的唯一标识,transaction-manager属性用于指定事务管理器。除此之外还需配置<tx: attributes>子元素,该子元素可通过配置多个<tx: method>子元素来配置执行事务的细节。
上图中灰色标注的几个属性是<tx: method>元素中的常用属性。
属性名称 | 描述 |
---|---|
name | 该属性为必选属性,它指定了与事务属性相关的方法名。其属性值支持使用通配符,如’’、'get * '、'handle * ‘、’ order’等 |
propagation | 用于指定事务的传播行为,默认为REQUIRED |
isolation | 用于指定事务的隔离级别,其属性可以为DEFAULT、READ_UNCOMMITTED、READ_COMM作TED 、 REPEATABLE_READ 和 SERIALl ZABLE ,其默认值为 DEFAULT |
read-only | 该属性用于指定事务是否只读,默认值是false |
timeout | 该属性用于指定事务超时的时间,其默认值为-1,即永不超时 |
rollback-for | 该属性用于指定触发事务回滚的异常类,在指定多个异常类时,异常类之间以英文逗号分隔 |
no-rollback-for | 该属性指定不触发事务回滚的异常类,在指定多个异常类时,异常类之间以英文逗号分隔 |
案例:
- 引入tx命名空间
- 配置事务增强
<!--平台事务管理器-->
<bean id="transactionManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"></property>
</bean>
<!--事务增强配置-->
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<tx:attributes>
<tx:method name="*"/>
</tx:attributes>
</tx:advice>
- 配置事务aop织入
<!--事务的aop增强-->
<aop:config>
<aop:pointcut id="myPointcut" expression="execution(* com.itheima.service.impl.*.*
(..))"/>
<aop:advisor advice-ref="txAdvice" pointcut-ref="myPointcut"></aop:advisor>
</aop:config>
- 测试事务控制转账业务
@Override
public void transfer(String outMan, String inMan, double money) {
accountDao.out(outMan,money);
int i = 1/0;
accountDao.in(inMan,money);
}
5.2.2 基于注解的声明式事务控制
- 编写AccountDao
@Repository("accountDao")
public class AccountDaoImpl implements AccountDao {
@Autowired
private JdbcTemplate jdbcTemplate;
public void out(String outMan, double money) {
jdbcTemplate.update("update account set money=money-? where
name=?",money,outMan);
}
public void in(String inMan, double money) {
jdbcTemplate.update("update account set money=money+? where
name=?",money,inMan);
}
}
- 编写AccountService
@Service("accountService")
@Transactional
public class AccountServiceImpl implements AccountService {
@Autowired
private AccountDao accountDao;
@Transactional(isolation = Isolation.READ_COMMITTED,propagation =
Propagation.REQUIRED)
public void transfer(String outMan, String inMan, double money) {
accountDao.out(outMan,money);
int i = 1/0;
accountDao.in(inMan,money);
}
}
- 编写applicationContext.xml配置文件
<!—之前省略datsSource、jdbcTemplate、平台事务管理器的配置-->
<!--组件扫描-->
<context:component-scan base-package="com.itheima"/>
<!--事务的注解驱动-->
<tx:annotation-driven/>
注意:
①使用 @Transactional 在需要进行事务控制的类或是方法上修饰,注解可用的属性同 xml 配置方式,例如隔离级
别、传播行为等。
②注解使用在类上,那么该类下的所有方法都使用同一套注解参数配置。
③使用在方法上,不同的方法可以采用不同的事务参数配置。
④Xml配置文件中要开启事务的注解驱动<tx:annotation-driven />
6 MyBatis入门
6.1 什么是MyBatis
MyBatis是一个支持普通SQL查询、存储过程以及高级映射的持久层框架,它消除了几乎所有的JDBC代码和参数的手动设置以及对结果集的检索,并使用简单的XML或注解进行配置和原始映射,用以将接口和Java的POJO(普通Java对象)映射成数据库中的记录,使得Java开发人员可以使用面向对象的编程思想来操作数据库。MyBatis框架被称为ORM(对象关系映射)框架,ORM是一种为了解决面向对象与关系型数据库中数据类型不匹配的技术,它通过描述Java对象与数据库表中间的映射关系,自动将Java应用程序中的对象持久化到关系型数据库的表中。原理图如下:
6.2 MyBatis工作原理
- 读取MyBatis配置文件mybatis-config.xml。其中配置了获取数据库连接等
- 加载映射文件Mapper.xml,该文件中配置了操作数据库的SQL语句,需要在mybatis-config.xml中加载才能执行。mybatis-config.xml可以加载多个配置文件,每个配置文件对应数据库中的一张表。
- 构建会话工厂。通过MyBatis的环境等配置信息构建会话工厂SqlSessionFactory。
- 创建SqlSession对象。由会话工厂创建SqlSession对象,该对象中包含了执行SQL的所有方法。
- MyBatis底层定义一个Executor接口来操作数据库,它会根据SqlSession传递的参数动态地生成需要执行的SQL语句,同时负责查询缓存的维护。
- 在Executor接口的执行方法中,包含一个MappedStatement类型的参数,该参数是对映射信息的封装,用于存储要映射的SQL语句id、参数等。Mapper.xml中的一个SQL对应一个MappedStatement对象,SQL的id即MappedStatement的id。
- 输入参数映射。
- 输出结果映射。
6.3 MyBatis入门程序
6.3.1 查询客户
- 创建一个mybatis数据库和t_customer表
-
配置log4j.properties文件
-
创建持久化类Customers.java
public class Customer { private Integer id; private String username; private String jobs; private String phone; public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getJobs() { return jobs; } public void setJobs(String jobs) { this.jobs = jobs; } public String getPhone() { return phone; } public void setPhone(String phone) { this.phone = phone; } @Override public String toString() { return "Customer{" + "id=" + id + ", username='" + username + '\'' + ", jobs='" + jobs + '\'' + ", phone='" + phone + '\'' + '}'; } }
-
在resources下创建一个com.itheima.mapper宝,并创建CustomerMapper.xml
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="customerMapper"> <!--1. 根据客户编号获取客户信息--> <select id="findCustomerById" parameterType="Integer" resultType="com.itheima.domain.Customer"> select * from t_customer where id = #{id}; </select> </mapper>
-
创建mybatis核心配置文件mybatis-config.xml
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd"> <configuration> <!--通过properties加载外部的properties文件--> <properties resource="jdbc.properties"></properties> <!--自定义别名--> <typeAliases> <typeAlias type="com.itheima.domain.Customer" alias="customerMapper"></typeAlias> </typeAliases> <!--数据源环境--> <environments default="development"> <environment id="development"> <transactionManager type="JDBC"></transactionManager> <dataSource type="POOLED"> <property name="driver" value="${jdbc.driver}"/> <property name="url" value="${jdbc.url}"/> <property name="username" value="${jdbc.username}"/> <property name="password" value="${jdbc.password}"/> </dataSource> </environment> </environments> <!--加载映射文件--> <mappers> <mapper resource="com/itheima/mapper/CustomerMapper.xml"></mapper> </mappers> </configuration>
-
测试
public class MybatisTest { /** * * 根据客户编号查询客户信息 */ @Test public void findCustomerByIdTest() throws Exception { // 1. 读取配置文件 InputStream inputStream = Resources.getResourceAsStream("sqlMapConfig.xml"); // 2. 构建sqlSessionFactory SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); // 3. 创建sqlSession SqlSession sqlSession = sqlSessionFactory.openSession(); // 4. 执行映射文件定义的sql,返回映射结果 Customer customer = sqlSession.selectOne("customerMapper.findCustomerById", 1); System.out.println(customer.toString()); // 5. 关闭SqlSession sqlSession.close(); } }
6.3.2 添加、更新、删除(略)
7 MyBatis的核心配置
7.1 MyBatis的核心对象
在使用MyBatis框架时,主要涉及两个核心对象:SqlSessionFactory和SqlSession。
7.1.1 SqlSessionFactory
它是单个数据库映射关系经过编译后的内存镜像,其主要作用是创建SqlSession。SqlSessionFactory对象的实例可以通过SqlSessionFactoryBuilder对象来创建,而SqlSessionFactoryBuilder则可以通过xml配置文件或者预先定义好的Configuration实例构建出SqlSessionFactory的实例。实现代码如下:
// 1. 读取配置文件
InputStream inputStream = Resources.getResourceAsStream("配置文件位置);
// 2. 构建sqlSessionFactory
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSessionFactory对象是线程安全的,一旦被创建,在整个应用执行期间都会存在。如果我们多次创建同一个数据库的SqlSessionFactory,那么此数据库的资源将很容易被耗尽。为了解决该问题,通常每个数据库都会只对应一个SqlSessionFactory,所以在构建SqlSessionFactory实例时,建议使用单例模式。
7.1.2 SqlSession
SqlSession是MyBatis框架中另一个重要的对象,它是应用程序与持久层之间执行交互操作的一个单线程对象,其主要作用是执行持久化操作。SqlSession对象包含了数据库中所有执行SQL操作的方法,由于其底层封装了JDBC连接,所以可以直接使用其实例来执行已映射的SQL语句。
每个线程都应该有一个自己的SqlSession实例,并且该实例是不能被共享的。同时,SqlSession 实例也是线程不安全的,因此其使用范围最好在一次请求或一个方法中,绝不能将其放在一个类的静态字段、实例字段或任何类型的管理范围(如 Servlet 的 HttpSession )中使用 。 使用完 SqlSession 对象之后,要及时地关闭它,通常可以将其放在 finally 块中关闭,代码如下所示。
// 创建sqlSession
SqlSession sqlSession = sqlSessionFactory.openSession();
try {
// 持久化操作
} finally {
// 关闭SqlSession
sqlSession.close();
}
SqlSession对象中包含了很多方法,其常用方法如下:
- T selectOne(String statement):查询方法。参数statement是在配置文件中定义的元素的id。使用该方法后,会返回执行SQL语句查询结果的一条泛型对象。
- T selectOne(String statement, Object parameter):查询方法。参数statement是在配置文件中定义的元素的id,parameter是查询所需的参数。使用该方法后,会返回执行SQL语句查询结果的一条泛型对象。
- List selectList (String statement):查询方法。参数 statement 是在配置文件中定义的 元素的 id 。 使用该方法后,会返回执行 SQL 语句查询结果的泛型对象的集合。
- List selectList ( String statement , Object parameter ):查询方法 。 参数 statement 是在配置文件中定义的 元素的 id , parameter 是查询所需的参数。 使用该方法后,会返回执行 SQL 语句查询结果的泛型对象的集合。
- List selectList ( String statement , Object parameter , RowBounds rowBounds ):查询方法 。 参数 statement 是在配置文件中定义的 元素的 id , parameter 是查询所需的参数, rowBounds 是用于分页的参数对象 。 使用该方法后,会返回执行 SQL 语句查询结果的泛型对象的集合。
- void select ( String statement , Object parameter , ResultHandler handler ) :查询方法 。 参数 statement 是在配置文件中定义的 元素的 id,parameter 是查询所需的参数, ResultHandler 对象用于处理查询返回的复杂结果集,通常用于多表查询 。
- int insert(String statement):插入方法 。 参数 statement 是在配置文件中定义的 元素的 id 。 使用该方法后,会返回执行 SQL 语句所影响的行数。
- int insert ( String statement, Object parameter ) :插入方法 。 参数 statement 是在配置文件中定义的 <inserb元素的 id , parameter 是插入所需的参数。 使用该方法后,会返回执行 SQL 语句所影响的行数 。
- int update ( String statement ) :更新方法 。 参数 statement 是在配置文件中定义的 元素的 id 。 使用该方法后,会返回执行 SQL 语句所影响的行数。
- int update ( String statement , Object parameter ) :更新方法 。 参数 statement 是在配置文件中定义的 元素的 id , parameter 是更新所需的参数。 使用该方法后,会返回执行 SQL 语句所影响的行数。
- int delete ( String statement ) :删除方法 。 参数 statement 是在配置文件中定义的 元素的 id 。 使用该方法后,会返回执行 SQL 语句所影响的行数。
- int delete ( String statement , Object parameter ) :删除方法 。 参数 statement 是在配置文件中定义的 元素的 id , parameter 是删除所需的参数。 使用该方法后,会返回执行 SQL 语句所影响的行数 。
- void commit():提交事务的方法。
- void rollback():回滚事务的方法
- void close():关闭SqlSession对象。
- T getMap pe r(Class type) :该方法会返回Mapper接口的代理对象,该对象关联了SqlSession对象,开发人员可以使用该对象直接调用方法操作数据库。参数type是Mapper的接口类型。MyBatis官方推荐通过Mapper对象访问MyBatis。
- Connection getConnection():获取JDBC数据库连接对象的方法。
使用工具类创建SqlSession:为了简化开发可以将上述重复的代码封装到一个工具类中,然后通过工具类来创建SqlSession,如下:
public class MybatisUtils { private static SqlSessionFactory sqlSessionFactory = null; // 初始化SqlSessionFactory对象 static { try { // 使用MyBatis提供的Resources类加载配置文件 Reader reader = Resources.getResourceAsReader("sqlMapconfig.xml"); // 构建SqlSessionFactory工厂 sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader); } catch (Exception e) { e.printStackTrace(); } } // 获取SqlSession对象的静态方法 public static SqlSession getSession() { return sqlSessionFactory.openSession(); } }
7.2 配置文件
MyBatis配置文件中,包含了很多影响MyBatis行为的重要信息。这些信息通常在一个项目中只会在一个配置文件中编写,并且编写后也不会轻易改动。
7.2.1 主要元素
在MyBatis框架的核心配置文件中,元素时配置文件的根元素,其他元素都要在该元素内配置。主要元素如下图:
的子元素必须按照上图的由上到下顺序进行配置,否则MyBatis在解析xml配置文件的时候会报错。
7.2.2 常用标签元素
-
:是一个配置属性的元素,该元素通常用于将内部的配置外在化,即通过外部的配置来动态地替换内部定义的属性。具体方式如下:
-
在src下添加一个jdbc.properties
jdbc.driver=com.mysql.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/mybatis jdbc.username=root jdbc.password=root
-
在MyBatis的配置文件mybatis-config.xml中配置properties
<properties resource="jdbc.properties"></properties>
-
修改数据源信息
<!--数据源环境--> <environments default="development"> <environment id="development"> <transactionManager type="JDBC"></transactionManager> <dataSource type="POOLED"> <property name="driver" value="${jdbc.driver}"/> <property name="url" value="${jdbc.url}"/> <property name="username" value="${jdbc.username}"/> <property name="password" value="${jdbc.password}"/> </dataSource> </environment> </environments>
-
-
元素:用于改变mybatis运行时的行为,例如开启二级缓存、开启延迟加载等。元素中的常见配置如下:
具体使用方式如下:
<!--设置-->
<settings>
<setting name="cacheEnabled" value="true"/>
<setting name="lazyLoadingEnabled" value="true" />
<setting name="multipleResultSetsEnabled" value="true" />
<setting name="useColumnLabel " value="true" />
<setting name="useGeneratedKeys" value="false" />
<setting name= "autoMappingBehavior" value="PARTIAL" />
</settings>
-
元素:设置别名。
<!--自定义别名--> <typeAliases> <typeAlias type="com.itheima.domain.Customer" alias="customerMapper"></typeAlias> </typeAliases>
上述示例中,alias属性的属性值customerMapper就是自定义的别名,如果省略alias属性,MyBatis会默认将类名首字母小写后的名称作为别名。当POJO类过多时,还可以通过自定义包扫描自定义别名:
<!--使用自动扫描包来定义别名--> <typeAliases> <package name="com.itheima.domain" /> </typeAliases>
注意:如果在程序中使用了注解,则别名为其注解的值。
-
元素:MyBatis 在预处理语句( PreparedStatement )中设置一个参数或者从结果集( ResultSet ) 中取出一个值时,都会用其框架内部注册的typeHandler(类型处理器)进行相关处理。typeHandler的作用就是将与处理语句中传入的参数从javaType转换为jdbcType,或者从数据库中取出结果时将jdbcType转换成javaType。
-
元素:MyBatis 框架每次创建结果对象的新实例时,都会使用一个对象工厂( ObjectFactory )的实例来完成 。MyBatis中默认ObjectFactory 的作用就是实例化目标类,它既可以通过默认构造方法实例化,也可以在参数映射存在的时候通过参数构造方法来实例化。
-
元素:MyBatis 允许在已映射语句执行过程中的某一点进行拦截调用,这种拦截调用是通过插件来实现的 。 元素的作用就是配置用户所开发的插件。
-
元素:MyBatis的环境中配制实际上就是数据源配置,可以通过该元素配置多种数据源,即配置多种数据库。
-
元素:mappers元素用于指定MyBatis映射文件的位置,一般可以使用以下4种方法引入映射器文件,具体如下所示:
-
使用类路径引入
<mappers> <mapper resource="com/itheima/mapper/UserMapper.xml" /> </mappers>
-
使用本地文件引入
<mappers> <mapper url="file:///D:/com/itheima/mapper/UserMapper.xml" /> </mappers>
-
使用接口类引入
<mappers> <mapper class="com.itheima.mapper.UserMapper" /> </mappers>
-
使用包名引入
<mappers> <package name="com.itheima.mapper" /> </mappers>
-
7.3 映射文件
映射文件MyBatis框架中十分重要的文件,可以说,MyBatis的强大之处就体现在映射文件的编写上。
7.3.1 主要元素
在映射文件中,是映射文件的根元素,其他元素都是他的子元素。
8 动态SQL(略)
9 MyBatis的关联映射
- 一对一:在任意一方引入对方主键作为外键。
- 一对多:在“多”的一方,添加“一”的一方的主键作为外键。
- 多对多:产生中间关系表,引入两张表的主键作为外键,两个主键称为联合主键或使用新的字段作为主键。
9.1 一对一(案例)
一个人只能有一个身份证,同时一个身份证也只会对应一个人。在MyBatis的元素中,包含了一个子元素,MyBatis就是通过该元素来处理一对一关联关系的。
在中,通常可以配置以下属性。
- property:指定映射到的实体类对象属性,与表字段一一对应。
- column:指定表中对应的字段。
- JavaType:指定映射到实体对象属性的类型。
- select:指定引入嵌套查询的子SQL语句,该属性用于关联映射中的嵌套查询。
- fetchType:指定在关联查询时是否启动延迟加载。fetchType属性有lazy和eager两个属性值,默认值为lazy(即默认关联映射延迟加载)。
元素使用参考如下两种示例配置即可。
<!--方式一:嵌套查询-->
<association property="card" column="card_id"
javaType="com.itheima.po.IdCard"
select="com.itheima.mapper.IdCardMapper.findCodeById" />
<!--方式二:嵌套结果-->
<association property="card" javaType="com.itheima.po.IdCard">
<id property="id" column="card_id" />
<result property="code" column="code" />
</association>
MyBatis在映射文件中加载关联关系对象主要通过两种方式:嵌套查询和嵌套结果。嵌套查询是指通过执行另外一条SQL映射语句来返回预期的复杂类型;嵌套结果是使用嵌套结果映射来处理重复的联合结果的子集。
-
创建数据表
tb_idcard:
tb_person:
-
环境搭建
引入maven坐标、log4j、MyBatisUtils工具类以及mybatis-config.xml核心配置文件。
-
创建持久化类IdCard和Person类
public class IdCard { private Integer id; private String code; public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getCode() { return code; } public void setCode(String code) { this.code = code; } @Override public String toString() { return "IdCard{" + "id=" + id + ", code='" + code + '\'' + '}'; } }
public class Person { private Integer id; private String name; private Integer age; private String sex; private IdCard card; // 个人关联的证件 public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public Integer getAge() { return age; } public void setAge(Integer age) { this.age = age; } public String getSex() { return sex; } public void setSex(String sex) { this.sex = sex; } public IdCard getCard() { return card; } public void setCard(IdCard card) { this.card = card; } @Override public String toString() { return "Person{" + "id=" + id + ", name='" + name + '\'' + ", age=" + age + ", sex='" + sex + '\'' + ", card=" + card + '}'; } }
-
在resources下创建com.itheima.mapper包,创建证件映射文件IdCardMapper.xml和个人映射文件PersonMapper.xml.
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="idCardMapper"> <!--根据id查询证件信息--> <select id="findCodeById" parameterType="Integer" resultType="IdCard"> select * from tb_idcard where id=#{id} </select> </mapper>
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="personMapper"> <!--嵌套查询:通过执行另一个SQL映射语句来返回预期的特殊类型--> <select id="findPersonById" parameterType="Integer" resultMap="IdCardWithPersonResult" > select * from tb_person where id=#{id} </select> <resultMap id="IdCardWithPersonResult" type="Person"> <id property="id" column="id" /> <result property="name" column="name" /> <result property="age" column="age" /> <result property="sex" column="sex" /> <!--一对一:association使用select属性引入另外一条SQL语句--> <association property="card" column="card_id" javaType="IdCard" select="idCardMapper.findCodeById" /> </resultMap> </mapper>
-
在核心配置文件mybatis-config.xml中引入mapper映射文件并定义别名。
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd"> <configuration> <!--通过properties加载外部的properties文件--> <properties resource="jdbc.properties"/> <!--自定义别名--> <typeAliases> <package name="com.itheima.po"/> </typeAliases> <!--数据源环境--> <environments default="development"> <environment id="development"> <transactionManager type="JDBC"/> <dataSource type="POOLED"> <property name="driver" value="${jdbc.driver}"/> <property name="url" value="${jdbc.url}"/> <property name="username" value="${jdbc.username}"/> <property name="password" value="${jdbc.password}"/> </dataSource> </environment> </environments> <!--加载映射文件--> <mappers> <mapper resource="com/itheima/mapper/IdCardMapper.xml"/> <mapper resource="com/itheima/mapper/PersonMapper.xml"/> </mappers> </configuration>
-
测试
public class MybatisAssociatedTest { @Test public void findPersonByIdTest() { // 1. 通过工具类生成SqlSession对象 SqlSession session = MybatisUtils.getSession(); // 2. 使用MyBatis嵌套查询方式查询id为1的人的信息 Person person = session.selectOne("personMapper.findPersonById", 1); // 3. 输出查询结果 System.out.println(person); // 4. 关闭SqlSession session.close(); } }
从上图可以看出mybatis的嵌套查询要执行多条SQL语句,这对于大型数据集合和列表展示不是很好,因为这样可能会导致成百上千条关联的SQL语句被执行,从而极大地消耗数据库性能和降低查询效率。为此,我们可以使用MyBatis提供的嵌套查询结果方式,来进行关联查询。
<select id="findPersonById2" parameterType="Integer" resultMap="IdCardWithPersonResult2" >
select p.*, idcard.code
from tb_person p, tb_idcard idcard
where p.card_id=idcard.id
and p.id=#{id}
</select>
<resultMap id="IdCardWithPersonResult2" type="Person">
<id property="id" column="id" />
<result property="name" column="name" />
<result property="age" column="age" />
<result property="sex" column="sex" />
<!--一对一:association使用select属性引入另外一条SQL语句-->
<association property="card" javaType="IdCard" >
<id property="id" column="card_id" />
<result property="code" column="code" />
</association>
</resultMap>
这样再测试后结果如下,可以看出使用嵌套结果的方式只执行了一条SQL语句。
可以打开延迟加载提高性能
<!--打开延迟加载--> <settings> <!--打开延迟加载开关--> <setting name="lazyLoadingEnabled" value="true"/> <!--将积极加载改为消息加载,即按需加载--> <setting name="aggressiveLazyLoading" value="false"/> </settings>
9.2 一对多(案例)
例如一个用户可以有多个订单,同时多个订单归一个用户所有。在MyBatis的元素中,包含了一个collection子元素,MyBatis就是通过该元素来处理一对多关联关系的。子元素的属性大部分与元素相同,但其还包含一个特殊属性——ofType。ofType属性与javaType属性对应,它用于指定实体对象中集合类属性所包含的元素类型。
实力参考配置如下:
<!--方式一:嵌套查询-->
<collection property="orderList" column="id"
ofType="com.itheima.po.Orders"
select="OrdersMapper.selectOrders"/>
<!--方式二:嵌套结果-->
<collection property="ordersList" ofType="com.itheima.po.Orders">
<id property="id" column="orders_id" />
<result property="number" column="number" />
</collection>
-
创建数据表
tb_user:
tb_orders:
-
创建持久化类Orders和User
public class Orders { private Integer id; private String number; public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getNumber() { return number; } public void setNumber(String number) { this.number = number; } @Override public String toString() { return "Orders{" + "id=" + id + ", number='" + number + '\'' + '}'; } } public class User { private Integer id; private String username; private String address; private List<Orders> ordersList; // 一个用户对多个订单 public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getAddress() { return address; } public void setAddress(String address) { this.address = address; } public List<Orders> getOrdersList() { return ordersList; } public void setOrdersList(List<Orders> ordersList) { this.ordersList = ordersList; } @Override public String toString() { return "User{" + "id=" + id + ", username='" + username + '\'' + ", address='" + address + '\'' + ", ordersList=" + ordersList + '}'; } }
-
创建用于实体映射文件UserMapper.xml
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="userMapper"> <!--一对多:查询某一用户及对应的多条订单信息,当关联查询列名相同时需要使用别名--> <select id="findUserWithOrders" parameterType="Integer" resultMap="UserWithOrdersResult"> select u.*, o.id as order_id, o.number from tb_user u, tb_orders o where u.id = o.user_id and u.id = #{id} </select> <resultMap id="UserWithOrdersResult" type="User"> <id property="id" column="id" /> <result property="username" column="username" /> <result property="address" column="address" /> <!--一对多--> <collection property="ordersList" ofType="Orders"> <id property="id" column="order_id" /> <result property="number" column="number" /> </collection> </resultMap> </mapper>
-
将UserMapper.xml路径配置到核心配置文件mybatis-config.xml
<mapper resource="com/itheima/mapper/UserMapper.xml"/>
-
编写测试方法findUserTest()
@Test public void findUserWithOrders() { // 1. 通过工具类生成SqlSession对象 SqlSession session = MybatisUtils.getSession(); // 2. 使用MyBatis嵌套查询方式查询id为1的人的信息 User user = session.selectOne("userMapper.findUserWithOrders", 1); // 3. 输出查询结果 System.out.println(user); // 4. 关闭SqlSession session.close(); }
9.3 多对多(案例)
多对多的例子以订单和商品为例,一个订单可以包含多种商品,而一种商品又可以属于多个订单,订单和商品就属于多对多的关联关系。
在数据库中,多对多的关联关系通常使用一个中间表来维护,中间表的订单id作为外键参照订单表的id,商品id作为外键参照商品表的id。
- 创建数据表:tb_product和中间表tb_ordersitem
-
创建持久化类Product,修改Order类
public class Product { private Integer id; private String name; private Double price; private List<Orders> ordersList; public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public Double getPrice() { return price; } public void setPrice(Double price) { this.price = price; } public List<Orders> getOrdersList() { return ordersList; } public void setOrdersList(List<Orders> ordersList) { this.ordersList = ordersList; } @Override public String toString() { return "Product{" + "id=" + id + ", name='" + name + '\'' + ", price=" + price + ", ordersList=" + ordersList + '}'; } } public class Orders { private Integer id; private String number; private List<Product> productList; public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getNumber() { return number; } public void setNumber(String number) { this.number = number; } public List<Product> getProductList() { return productList; } public void setProductList(List<Product> productList) { this.productList = productList; } @Override public String toString() { return "Orders{" + "id=" + id + ", number='" + number + '\'' + ", productList=" + productList + '}'; } }
-
创建订单实体映射文件OrderMapper.xml和商品实体映射文件ProductMapper.xml。
<mapper namespace="ordersMapper"> <!--嵌套查询--> <select id="findOrderWithProducts" parameterType="Integer" resultMap="OrdersWithProductsResult"> select * from tb_orders where id=#{id}; </select> <resultMap id="OrdersWithProductsResult" type="Orders"> <id column="id" property="id" /> <result property="number" column="id" /> <collection property="productList" column="id" ofType="Product" select="productMapper.findProductById" /> </resultMap> </mapper>
<mapper namespace="productMapper"> <!--嵌套查询--> <select id="findProductById" parameterType="Integer" resultType="Product"> select * from tb_product where id in(select product_id from tb_ordersitem where order_id = #{id}) </select> </mapper>
注:这里可以使用一次SQL语句来进行优化
-
将新创建的映射文件配置到核心文件mybatis-config.xml中
<mapper resource="com/itheima/mapper/OrdersMapper.xml"/> <mapper resource="com/itheima/mapper/ProductMapper.xml"/>
-
测试
@Test public void findOrdersWithProduct() { // 1. 通过工具类生成SqlSession对象 SqlSession session = MybatisUtils.getSession(); // 2. 使用MyBatis嵌套查询方式查询id为1的人的信息 Orders orders = session.selectOne("ordersMapper.findOrderWithProducts", 1); // 3. 输出查询结果 System.out.println(orders); // 4. 关闭SqlSession session.close(); }
10 MyBatis与Spring整合
在实际的开发中,Spring与MyBatis都是整合在一起使用的。这里对整合内容进行详细的讲解。
10.1 整合环境搭建
10.1.1 maven坐标导入
<dependencies>
<!--spring框架所需的依赖-->
<dependency>
<groupId>aopalliance</groupId>
<artifactId>aopalliance</artifactId>
<version>1.0</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.8.10</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>4.3.8.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>4.3.8.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>4.3.8.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>4.3.8.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>4.3.8.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-expression</artifactId>
<version>4.3.8.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>4.3.8.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<version>4.3.8.RELEASE</version>
</dependency>
<!--MyBatis所需依赖-->
<dependency>
<groupId>org.apache.ant</groupId>
<artifactId>ant</artifactId>
<version>1.10.3</version>
</dependency>
<dependency>
<groupId>org.apache.ant</groupId>
<artifactId>ant-launcher</artifactId>
<version>1.10.3</version>
</dependency>
<dependency>
<groupId>org.ow2.asm</groupId>
<artifactId>asm</artifactId>
<version>5.1</version>
</dependency>
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>3.2.4</version>
</dependency>
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>1.2</version>
</dependency>
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.21.0-GA</version>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
<version>2.13.3</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.4.5</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.21</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.22</version>
</dependency>
<dependency>
<groupId>c3p0</groupId>
<artifactId>c3p0</artifactId>
<version>0.9.1.2</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.10</version>
</dependency>
<!--MyBatis和Spring整合中间件-->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>1.3.1</version>
</dependency>
<!--mysql数据库驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.1</version>
</dependency>
<!--数据源·-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
<version>2.4.2</version>
</dependency>
</dependencies>
10.1.2 编写配置文件
分别创建编写jdbc.properties,Spring的配置文件applicationContext.xml,MyBatis核心配置文件mybatis-config.xml。
jdbc.properties:
jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/mybatis
jdbc.username=root
jdbc.password=root
jdbc.maxTotal=30
jdbc.maxIdle=10
jdbc.initialSize=5
applicationContext.xml:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:conext="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx-4.3.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-4.3.xsd
">
<!--1. 读取jdbc配置文件-->
<conext:property-placeholder location="classpath:jdbc.properties" />
<!--2. 配置数据源 -->
<!--2. 配置数据源-->
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
<property name="driverClass" value="${jdbc.driver}"/>
<property name="jdbcUrl" value="${jdbc.url}"/>
<property name="user" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
<property name="maxPoolSize" value="${jdbc.maxTotal}"/>
<property name="maxIdleTime" value="${jdbc.maxIdle}"/>
<property name="initialPoolSize" value="${jdbc.initialSize}"/>
</bean>
<!--2. 事务管理器,依赖于数据源-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource" />
</bean>
<!--开启事务注解-->
<tx:annotation-driven transaction-manager="transactionManager" />
<!--3. 配置MyBatis工厂-->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<!--注入数据源-->
<property name="dataSource" ref="dataSource" />
<!--指定核心配置文件-->
<property name="configLocation" value="classpath:mybatis-config.xml" />
</bean>
<!--开启注解扫描-->
<conext:component-scan base-package="com.itheima.service" />
</beans>
mybatis-config.xml:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<!--打开延迟加载-->
<settings>
<!--打开延迟加载开关-->
<setting name="lazyLoadingEnabled" value="true"/>
<!--将积极加载改为消息加载,即按需加载-->
<setting name="aggressiveLazyLoading" value="false"/>
</settings>
<!--自定义别名-->
<typeAliases>
<package name="com.itheima.po"/>
</typeAliases>
<!--加载映射文件-->
<mappers>
...
</mappers>
</configuration>
10.2 传统DAO方式开发整合(了解)
采用传统DAO开发方式进行MyBatis与Spring整合时,需要编写DAO接口以及接口的实现类,并且需要向DAO实现类注入SqlSessionFactory,然后在方法体中通过SqlSessionFactory创建SqlSession。为此,我们可以mybatis-spring包中所提供的SqlSessionTemplate类或SqlSessionDaoSupport类来实现此功能。
- SqlSessionTemplate:是mybatis-spring的核心类,负责管理MyBatis的SqlSession,调用MyBatis的SQL方法。当调用SQL方法时,SqlSessionTemplate将会保证使用的SqlSession和当前Spring的事务是相关的。它还管理SqlSession的生命周期,包含必要的关闭、提交和回滚操作。
- SqlSessionDaoSupport:是一个抽象支持类,它继承了DaoSupport类,主要是作为DAO的基类来使用。可以通过SqlSessionDaoSupport类的getSqlSession()方法来获取所需的SqlSession。
步骤如下:
-
实现持久层的编写:Customer类
public class Customer { private Integer id; private String username; private String jobs; private String phone; public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getJobs() { return jobs; } public void setJobs(String jobs) { this.jobs = jobs; } public String getPhone() { return phone; } public void setPhone(String phone) { this.phone = phone; } @Override public String toString() { return "Customer{" + "id=" + id + ", username='" + username + '\'' + ", jobs='" + jobs + '\'' + ", phone='" + phone + '\'' + '}'; } }
-
创建映射文件CustomerMapper.xml
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.itheima.po.CustomerMapper"> <!--嵌套查询--> <select id="findCustomerById" parameterType="Integer" resultType="Customer"> select * from t_customer where id=#{id}; </select> </mapper>
-
在MyBatis的配置文件mybatis-config.xml中添加映射
<mapper resource="com/itheima/po/CustomerMapper.xml" />
-
实现DAO层的CustomerDao和CustomerDaoImpl
public class CustomerDaoImpl extends SqlSessionDaoSupport implements CustomerDao { // 根据id查询客户 @Override public Customer findCustomerById(Integer id) { return this.getSqlSession().selectOne("com.itheima.po.CustomerMapper.findCustomerById", id); } }
-
在Spring的配置文件applicationContext.xml中编写实例化CustomerDaoImpl的配置
<!--注入Dao--> <bean id="customerDao" class="com.itheima.dao.impl.CustomerDaoImpl"> <!--注入SqlSessionFactory对象--> <property name="sqlSessionFactory" ref="sqlSessionFactory" /> </bean>
-
测试
@Test public void findCustomerByIdDaoTest() { ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml"); CustomerDao customerDao = applicationContext.getBean(CustomerDao.class); Customer customer = customerDao.findCustomerById(1); System.out.println(customer); }
注:关于namespace的疑问:这里的namespace表示唯一标识符,名称可以随意,只要sql语句执行的时候加上namespace前缀即可。但是如果使用接下来的mapper接口方式开发整合就必须是对应的mapper接口的全限定类名了!!!(具体看下面演示)
10.3 Mapper接口方式开发整合(掌握)
采用传统方式整合会出现大量重复代码,在方法中也需要指定映射文件中执行语句的id,为此,我们可以使用MyBatis提供的另一种编程方式,即使用Mapper接口编程。
10.3.1 基于MapperFactoryBean的整合
MapperFactoryBean是MyBatis-Spring团队提供的一个用于根据mapper接口生成对象的类,该类在Spring配置文件中使用时可以配置以下参数。
- mapperInterface:用于指定接口
- SqlSessionFactory:用于指定SqlSessionFactory
- SqlSessionTemplate:用于指定SqlSessionTemplate。如果与SqlSessionFactory同时设定,则只会启用SqlSessionTemplate
整合步骤:
-
在com.itheima.mapper包下创建CustomerMapper接口以及对应的映射文件
public interface CustomerMapper { // 通过id查询客户 Customer findCustomerById(Integer id); }
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.itheima.mapper.CustomerMapper"> <!--这里要对应mapper的接口才会生效--> <!--嵌套查询--> <select id="findCustomerById" parameterType="Integer" resultType="customer"> select * from t_customer where id=#{id}; </select> </mapper>
-
添加映射到mybatis-config.xml中
-
在Spring配置文件中,创建一个customerMapper的Bean:
<bean id="customerMapper" class="org.mybatis.spring.mapper.MapperFactoryBean"> <property name="mapperInterface" value="com.itheima.mapper.CustomerMapper" /> <property name="sqlSessionFactory" ref="sqlSessionFactory" /> </bean>
-
测试
@Test public void findCustomerByIdMapperTest() { ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml"); CustomerMapper customerMapper = applicationContext.getBean(CustomerMapper.class); Customer customer = customerMapper.findCustomerById(1); System.out.println(customer); }
注意事项:
- Mapper接口的名称必行与Mapper.xml映射文件的名称一致。
- mapper.xml文件中的namespace必须与mapper接口的类路径相同!
- Mapper接口中的方法名和映射文件中的id相同。
- Mapper接口方法的输入参数类型要和映射文件中每个sql的parameterType相同。
- Mapper接口方法的输出类型要和映射文件的resultType相同。
10.3.2 基于MapperScannerConfigurer的整合
在实际的项目中,会有很多dao接口,上面的配置还是臃肿,因此使用基于MapperScannerConfigurer类整合。
只需要在spring的配置文件中编写如下代码即可:
<!--Mapper代理开发-->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="com.itheima.mapper" />
</bean>
然后把上面的2和3步注释掉。
11 Spring MVC入门
11.1 Spring MVC概述
Spring MVC是Spring提供的一个实现了web MVC设计模式的轻量级Web框架。属于MVC框架,具有如下特点:
- 是Spring的一部分,可以方便的利用Spring所提供的其他功能。
- 灵活性强,易于与其他框架集成。
- 提供了一个前端控制器DispatchServlet,使开发人员无须额外开发控制器对象。
- 等等…
11.2 第一个Spring MVC应用
步骤:
①导入SpringMVC相关坐标
②配置SpringMVC核心控制器DispathcerServlet
③创建Controller类和视图页面
④使用注解配置Controller类中业务方法的映射地址
⑤配置SpringMVC核心文件 spring-mvc.xml
⑥客户端发起请求测试
具体实现:
- 创建web项目(添加org.apache.maven下的webapp),引入相关依赖。
<!--Spring坐标-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.0.5.RELEASE</version>
</dependency>
<!--SpringMVC坐标-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.0.5.RELEASE</version>
</dependency>
<!--Servlet坐标-->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
<version>2.5</version>
</dependency>
<!--Jsp坐标-->
<dependency>
<groupId>javax.servlet.jsp</groupId>
<artifactId>jsp-api</artifactId>
<version>2.0</version>
</dependency>
-
在web.xml中配置前端控制器
<?xml version="1.0" encoding="UTF-8"?> <web-app version="3.0" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"> <!--配置SpringMVC前端控制器--> <servlet> <servlet-name>DispatcherServlet</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <!--初始化时加载配置文件--> <init-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:spring-mvc.xml</param-value> </init-param> <!--表示容器在启动时立即加载Servlet--> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>DispatcherServlet</servlet-name> <!--会将所有的url拦截并交给DispatcherServlet处理--> <url-pattern>/</url-pattern> </servlet-mapping> </web-app>
-
创建Contrller类
@Controller public class UserController { @RequestMapping("/quick") // RequestMapping是一个用来处理请求地址映射的注解,可用于类或方法上。用于类上,表示类中的所有响应请求的方法都是以该地址作为父路径。 public String save() { System.out.println("Controller save running......"); return "success.jsp"; } }
-
创建sucess.jsp
<html> <head> <title>Title</title> </head> <body> <h1>Success!</h1> </body> </html>
-
配置核心文件spring-mvc.xml
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:mvc="http://www.springframework.org/schema/mvc" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd "> <!--Controller组件扫描--> <context:component-scan base-package="com.itheima.controller"/> </beans>
-
配置tomcat并在浏览器中访问
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LyUFEdCk-1626342903607)(img/image-20210616093946234.png)]
10.3 Spring MVC的工作流程
- 用户通过浏览器向服务器发送请求,请求会被前端控制器DispatcherServlet所拦截。
- DispatcherServlet拦截到请求后,会调用处理器映射器HandlerMapping,处理器映射器找到具体的处理器生成处理器对象返回给DispatcherServlet。
- DispatcherServlet会通过返回信息选择合适的处理器适配器HandlerAdpter,处理器适配器调用并执行处理器Handler,这里指的是Controller类,也被称为后端控制器。Controller执行完会返回一个ModelAndView对象,该对象包含视图名或包含视图模型和视图名,最后HandlerAdapter会将ModelAndView对象返回给DispatcherServlet。
- DispatcherServlet会根据ModelAndView对象选择合适的视图解析器ViewReslover,视图解析器解析后,给DispatcherServlet返回一个具体的视图View。
- DispatcherServlet对view进行渲染(即将模型数据填充至视图中),然后渲染结果就会在客户端浏览器中显示。
在上面的过程中出现了DispatcherServlet、handlerMapping、HandlerAdapt和ViewReslover。这些对象的工作都是在框架内部执行的,开发人员并不需要关心内部的实现过程,只需要配置前端控制器完成Controller的业务处理,并在视图中(View)显示相关信息即可。
12 Spring MVC核心类和注解
12.1 DispatcherServlet
它在程序中充当前端控制器的角色,在使用时只需将其配置在web.xml文件中即可,配置如下:
<!--配置SpringMVC前端控制器-->
<servlet>
<servlet-name>DispatcherServlet</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<!--初始化时加载配置文件-->
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring-mvc.xml</param-value>
</init-param>
<!--表示容器在启动时立即加载Servlet-->
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>DispatcherServlet</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
在上述代码中,元素和元素都是可选的。如果元素的值为1,则在应用程序启动时会立即加载该servlet;若不存在则会在第一个servlet请求是加载该servlet。存在并通过子元素配置了springmvc配置文件的路径,则会在启动时加载配置路径下的配置文件,如果没有,则应用程序会默认到web-inf目录下找servletName-servlet.xml
形式的文件。
12.2 Controller注解类型
将该注解加到Controller类上,然后通过Spring的扫描机制就可以找的该注解的控制器了。需要在springmvc配置文件中添加一个注解扫描:
<!--Controller组件扫描-->
<context:component-scan base-package="com.itheima.controller"/>
12.3 RequestMapping注解类型
12.3.1 @RequestMapping注解的使用
- 标注到方法上
- 标注到类上
12.3.2 @RequestMapping注解的属性
name、value、method、params等
12.3.3 组合注解
- @GetMapping
- @PostMapping
- @PutMapping
- @DeleteMapping
- @PatchMapping
12.4 ViewResolver(视图解析器)
SpringMVC中的视图解析器负责解析视图,可以通过在配置文件中定义一个ViewResovler来配置视图解析器,配置实例如下:
<!--配置资源内部视图解析器-->
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/jsp/"></property>
<property name="suffix" value=".jsp"></property>
</bean>
13 拦截器
Spring MVC中拦截器类似于Servlet中的过滤器,它主要用于拦截用户请求。将拦截器按一定的顺序联结成一条链,这条链称为拦截器链(Interceptor Chain)。在访问被拦截的方法或字段时,拦截器链中的拦截器就会按其之前定义的顺序被调用。拦截器也是AOP思想的具体实现。
13.1 单个拦截器的执行流程
从上图可以看出,程序首先会执行拦截器类中的preHandler()方法,如果该方法的返回值为true,则程序会继续向下执行处理器中的方法,否则将不再向下执行;在业务处理器(即controller类)处理请求后,会执行postHandle()方法,然后会通过DispatcherServlet向客户端返回响应;在DispatcherServlet处理完请求后,才会执行afterCompletion()方法。
13.2 自定义拦截器
- 创建拦截器类实现HandlerInterceptor接口
- 配置拦截器
- 测试拦截器的拦截效果
- 在spring-mvc.xml中配置权限拦截器
<!--配置权限拦截器-->
<mvc:interceptors>
<mvc:interceptor>
<!--配置对哪些资源执行拦截操作-->
<mvc:mapping path="/**"/>
<!--配置哪些资源排除拦截操作-->
<mvc:exclude-mapping path="/user/login"/>
<bean class="com.itheima.interceptor.MyInterceptor"/>
</mvc:interceptor>
</mvc:interceptors>
- 创建拦截器类
public class MyInterceptor1 implements HandlerInterceptor {
// 在目标方法执行之前 执行
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("preHandle");
String param = request.getParameter("param");
if("yes".equals(param)) {
return true;
}else {
request.getRequestDispatcher("/error.jsp").forward(request,response);
return false;
}
}
// 在目标方法执行之后,视图返回之前
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
modelAndView.addObject("name","itheima");
System.out.println("postHandle");
}
// 在整个流程都执行完毕后执行
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
System.out.println("afterCompletion");
}
}
13.3 拦截器总结
当拦截器的preHandle方法返回true则会执行目标资源,如果返回false则不执行目标资源
多个拦截器情况下,配置在前的先执行,配置在后的后执行
拦截器中的方法执行顺序是:preHandler-------目标资源----postHandle---- afterCompletion
三个方法说明:
方法名 | 说明 |
---|---|
preHandle() | 方法将在请求处理之前进行调用,该方法的返回值是布尔值Boolean类型的,当它返回为false 时,表示请求结束,后续的Interceptor 和Controller 都不会再执行;当返回值为true 时就会继续调用下一个Interceptor 的preHandle 方法 |
postHandle() | 该方法是在当前请求进行处理之后被调用,前提是preHandle 方法的返回值为true 时才能被调用,且它会在DispatcherServlet 进行视图返回渲染之前被调用,所以我们可以在这个方法中对Controller 处理之后的ModelAndView 对象进行操作 |
afterCompletion() | 该方法将在整个请求结束之后,也就是在DispatcherServlet 渲染了对应的视图之后执行,前提是preHandle 方法的返回值为true 时才能被调用 |
13.4 应用案例-实现用户登录权限验证
本案例中,只有登录后的用户才能访问系统的主页面,如果没有登录系统直接访问主页面,则拦截器会将请求拦截,并转发到登录页面,同时在登录页面给出提示信息。如果用户名或密码错误,也会在登录页面给出相应的提示信息。当已登录的用于在系统首页单击“退出”链接时,系统同样会回到登录页面。流程图如下:
-
导入maven坐标
<dependencies> <!--Spring相关--> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>5.0.5.RELEASE</version> </dependency> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> <version>1.8.7</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> <version>5.0.5.RELEASE</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-tx</artifactId> <version>5.0.5.RELEASE</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-test</artifactId> <version>5.0.5.RELEASE</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>5.0.5.RELEASE</version> </dependency> <!--Servlet和jsp--> <dependency> <groupId>javax.servlet</groupId> <artifactId>servlet-api</artifactId> <version>2.5</version> </dependency> <dependency> <groupId>javax.servlet.jsp</groupId> <artifactId>jsp-api</artifactId> <version>2.0</version> </dependency> <!--mybatis相关--> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.4.5</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis-spring</artifactId> <version>1.3.1</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.6</version> </dependency> <dependency> <groupId>c3p0</groupId> <artifactId>c3p0</artifactId> <version>0.9.1.2</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> </dependency> <dependency> <groupId>jstl</groupId> <artifactId>jstl</artifactId> <version>1.2</version> </dependency> </dependencies>
-
创建数据库表
-
配置文件配置(参考:15.3)
-
创建User实体类。
public class User { private Integer id; private String username; private String password; public User() { } public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } @Override public String toString() { return "User{" + "id=" + id + ", username='" + username + '\'' + ", password='" + password + '\'' + '}'; } }
-
创建UserController类
@Controller public class UserController { private UserService userService; public UserController() { } @Autowired public UserController(UserService userService) { this.userService = userService; } /** * 向用户登录页面跳转 */ @RequestMapping(value = "/login", method = RequestMethod.GET) public String toLogin() { System.out.println("===="); return "login"; } /** * 用户登录 */ @RequestMapping(value = "/login", method = RequestMethod.POST) public String login(User user, Model model, HttpSession session) { // 获取用户名和密码 //String username = user.getUsername(); //String password = user.getPassword(); user.setId(1); boolean flag = userService.findUserAndPassword(user); if (flag) { // 登录成功,将对用添加到session session.setAttribute("USER_SESSION", user); // 重定向到主页面 return "redirect:main"; } model.addAttribute("msg", "用户名或密码错误,请重新登陆!"); return "login"; } /** * 向用户主页面跳转 */ @RequestMapping(value = "/main") public String toMain() { System.out.println("====="); return "main"; } /** * 退出登录 */ @RequestMapping(value = "/logout") public String logout(HttpSession session) { // 清除session session.invalidate(); // 重定向到登录页面 return "redirect:login"; } }
-
创建拦截器类
public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 获取请求的URL String url = request.getRequestURI(); // 除过login.jsp都拦截 if (url.contains("/login") || url.contains("/index")) { return true; } // 获取session HttpSession session = request.getSession(); User user = (User) session.getAttribute("USER_SESSION"); if (user != null) { return true; } // 不符合条件的给出提示信息,并转发到登录页面 request.setAttribute("msg", "您还没有登录,请先登录!"); request.getRequestDispatcher("/WEB-INF/jsp/login.jsp").forward(request, response); return false; } @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { } }
-
配置元素,
<!--配置拦截器--> <mvc:interceptors> <mvc:interceptor> <!--对哪些资源进行拦截操作--> <mvc:mapping path="/**"/> <bean class="com.itheima.interceptor.LoginInterceptor"/> </mvc:interceptor> </mvc:interceptors>
-
创建main.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %> <html> <head> <title>系统主页</title> </head> <body> 当前用户:${USER_SESSION.username} <a href="${pageContext.request.contextPath}/login">退出</a> </body> </html>
-
创建login.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %> <html> <head> <title>用户登录</title> </head> <body> ${msg} <form action="${pageContext.request.contextPath}/login" method="post"> 用户名:<input type="text" name="username" /> <br /> 密 码:<input type="password" name="password" /> <br /> <input type="submit" value="登录"> </form> </body> </html>
13.5 总结
-
请简述Spring MVC拦截器的定义方式
答:通常拦截器类可以通过两种方式来定义。一种是通过实现HandlerInterceptor接口,另一种是通过实现WebRequestinterceptor接口。
-
请简述单个拦截器和多个拦截器的执行流程
答:单个拦截器时程序会首先执行类中的preHandle()方法,如果该方法的返回值为true,则程序会继续向下执行处理器的方法,否则将不再向下执行;在业务处理器(即Controller类)处理完请求后会执行postHandler()方法,然后会通过DispatcherServlet向客户端返回响应;在DispatcherServlet处理完请求后,才会执行afterCompetion()方法。多个拦截器同时工作时,他们的preHandler()方法会按照配置文件中拦截器的配置顺序执行,而postHandler()和afterCompletion()方法会按照配置的顺序反序执行。
14 文件上传和下载
文件的上传和下载是项目开发中最常用的功能,例如图片的上传和下载、邮件福建的上传和下载等。
14.1 文件上传
14.1.1 文件上传概述
多数文件上传都是通过表单形式提交给后台服务器的,因此,要实现文件上传功能,就需要提供一个文件上传的表单,而该表单必须满足以下3个条件。
- form表单的method属性设置为post
- form表单的enctype属性设置为multipart/form-data
- 提供的文件上传输入框
示例代码如下:
<form action = "uploadUrl" method="post" enctype="multipart/form-data">
<input type="file" name="filename" multiple="multiple" />
<input type="submit" value="文件上传" />
</form>
当客户端form表单的enctype属性为multipart/form-data时,浏览器就会采用二进制流的方式来处理表单数据,服务器端就会对文件上传的请求进行解析处理。在Spring MVC中是通过MultipartResolver对象来实现对文件上传的支持的,具体只需要在配置文件中定义该接口的Bean即可。
<bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
<!--设置请求的编码格式,必须与jsp中的pageEncoding属性一致,默认为ISO-8859-1-->
<property name="defaultEncoding" value="UTF-8" />
<!--设置允许上传文件的最大值,单位字节-->
<property name="maxUploadSize" value="2097152" />
</bean>
其他常用属性:
- maxUploadSize:上传文件最大长度
- maxInMemorySize:缓存中的最大尺寸
- defaultEncoding:默认编码格式
- resolveLazily:推迟文件解析,以便在Controller中捕获文件大小异常。
所需jar包:commons-fileupload-1.3.2.jar、commons-io-2.5.jar
15 SSM整合
15.1 整合思路
由于Spring MVC是Spring框架中的一个模块,所以这两者间不存在整合的问题。因此SSM框架的整合就只涉及到Spring与MyBatis的整合,已经Spring MVC和MyBatis的整合,如图:
15.2 准备所需jar包(maven依赖)
15.3 编写配置文件
db.properties
jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/ssm
jdbc.username=root
jdbc.password=root
applicationContext.xml:配置数据源、开启service注解扫描、开启Mapper代码开发MapperScannerConfigurer、配置事务管理器、开启事务注解、配置MyBatis工厂
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx-4.3.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
">
<!--1. 读取jdbc配置文件,配置数据源-->
<context:property-placeholder location="classpath:jdbc.properties" />
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
<property name="driverClass" value="${jdbc.driver}"/>
<property name="jdbcUrl" value="${jdbc.url}"/>
<property name="user" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</bean>
<!--2. 开启注解扫描-->
<context:component-scan base-package="com.itheima.service" />
<!--3. Mapper代理开发-->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="com.itheima.mapper" />
</bean>
<!--4. 配置事务管理器,依赖于数据源-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource" />
</bean>
<!--5. 开启事务注解-->
<tx:annotation-driven/>
<!--6. 配置MyBatis工厂-->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<!--注入数据源-->
<property name="dataSource" ref="dataSource" />
<!--指定核心配置文件-->
<property name="configLocation" value="classpath:mybatis-config.xml" />
</bean>
</beans>
注:在实际开发中,为了避免配置文件信息过于臃肿,通常会将Spring配置文件中的信息按照不同功能分散在多个配置文件中。例如可以将事务配置在名称为applicationcontext-transaction.xml文件中,将数据源信息放置在ApplicationContext-db.xml中。这样web.xml配置Spring文件信息时,只需要通过applicationContext-*.xml的方式即可自动加载全部配置信息。
mybatis-config.xml:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<!--自定义别名-->
<typeAliases>
<package name="com.itheima.po"/>
</typeAliases>
<!--加载映射文件-->
<mappers>
<mapper resource="mapper/UserMapper.xml"/>
</mappers>
</configuration>
由于在Spring中已经配置了数据源信息以及mapper接口文件扫描器,所以在MyBatis的配置文件中只需要进行别名配置即可。
springmvc-config.xml:配置包扫描器,扫描@Controller注解、加载注解驱动、配置视图解析器
<?xml version="1.0" encoding="UTF-8" ?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:conext="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<!--1. 扫描controller-->
<conext:component-scan base-package="com.itheima.controller" />
<!--2. 配置视图解析器-->
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/jsp/"/>
<property name="suffix" value=".jsp"/>
</bean>
<!--3. mvc注解驱动-->
<mvc:annotation-driven/>
</beans>
web.xml:配置文件监听器、编码过滤器,以及Spring MVC的前端控制器等信息
<!--spring监听器-->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:applicationContext.xml</param-value>
</context-param>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<!--springmvc前端控制器-->
<servlet>
<servlet-name>DispatcherServlet</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring-mvc.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>DispatcherServlet</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
<!--乱码过滤器-->
<filter>
<filter-name>CharacterEncodingFilter</filter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
<init-param>
<param-name>encoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>CharacterEncodingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>