导学
文章目录
一、MyBatis介绍
1. 什么是框架?
框架即一个半成品软件。开发者从头开发一个软件需要花费大量精力,于是有一些项目组开发出半成品软件,开发者在这些软件的基础上进行开发,这样的软件就称之为框架。
如果将开发完成的软件比作是一套已经装修完毕的新房,框架就好比是一套已经修建好的毛坯房。用户直接购买毛坯房,保证建筑质量和户型合理的同时可以进行风格的自由装修。
使用框架开发的好处:
- 省去大量的代码编写、减少开发时间、降低开发难度。
- 限制程序员必须使用框架规范开发,增强代码的规范性,降低程序员之间沟通及日后维护的成本。
- 将程序员的注意力从技术中抽离出来,更集中在业务层面。
使用框架就好比和世界上最优秀的软件工程师共同完成一个项目,并且他们完成的还是基础、全局的工作。
2. 什么是ORM框架?
ORM(Object Relationl Mapping),对象关系映射,即在数据库和对象之间作映射处理。
之前我们使用JDBC操作数据库,必须手动进行数据库和对象间的数据转换。
public class test {
// 新增方法,将对象转为sql语句字段
public void AddUser(User user) throws Exception {
Class.forName("com.mysql.jdbc.Driver");
Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/mybatis?characterEncoding=utf-8", "root", "root");
String sql = "INSERT INTO user values (null,?,?,?,?)";
PreparedStatement preparedStatement = connection.prepareStatement(sql);
preparedStatement.setString(1, user.getName());
preparedStatement.setInt(2, user.getAge());
preparedStatement.setString(3, user.getAddress());
preparedStatement.setString(4, user.getSex());
preparedStatement.executeUpdate();
// 省略资源关闭...
}
// 查询方法,将数据库结果集转为对象
public List<User> findAllUser() throws Exception {
Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/mybatis?characterEncoding=utf-8","root","root");
PreparedStatement preparedStatement = connection.prepareStatement("select * from user");
ResultSet resultSet = preparedStatement.executeQuery();
//遍历查询结果集
List<User> users = new ArrayList<>();
while (resultSet.next()) {
// 拿到每一列数据
int id = resultSet.getInt("id");
String name = resultSet.getString("name");
int age = resultSet.getInt("age");
String address = resultSet.getString("address");
String sex = resultSet.getString("sex");
// 将数据封装到对象中
User user = new User();
user.setId(id);
user.setName(name);
user.setAge(age);
user.setAddress(address);
user.setSex(sex);
users.add(user);
}
// 省略资源关闭...
return users;
}
}
这段代码中,数据库数据与对象数据的转换代码繁琐、无技术含量。而使用ORM框架代替JDBC后,框架可以帮助程序员自动进行转换,只要像平时一样操作对象,ORM框架就会根据映射完成对数据库的操作,极大的增强了开发效率。
3. 什么是MyBatis?
MyBatis是一个半自动的ORM框架,其本质是对JDBC的封装。使用MyBatis不需要写JDBC代码,但需要程序员编写SQL语句。之前是apache的一个开源项目iBatis,2010年改名为MyBatis。
补充:
Hibernate也是一款持久层ORM框架,多年前的市场占有率很高,但近年来市场占有率越来越低。MyBatis与Hibernate的比较:
- MyBatis是一个半自动的ORM框架,需要手写SQL语句。
- Hibernate是一个全自动的ORM框架,不需要手写SQL语句。
- 使用MyBatis的开发量要大于Hibernate。
为什么Hibernate市场占有率越来越低:
- 对于新手学习Hibernate时间成本比MyBatis大很多,MyBatis上手很快。
- Hibernate不需要写SQL语句是因为框架来生成SQL语句。对于复杂查询,开发者很难控制生成的SQL语句,这就导致SQL调优很难进行。
- 之前的项目功能简单,数据量小,所以使用Hibernate可以快速完成开发。而近年来项目的数据量越来越大,而互联网项目对查询速度要求也很高,这就要求我们一定要精细化的调整SQL语句。此时灵活性更强,手动编写SQL语句的MyBatis慢慢代替了Hibernate使用。
- 在高并发、大数据、高性能、高响应的互联网项目中,MyBatis是首选的持久框架。而对于对性能要求不高的比如内部管理系统等可以使用Hibernate。
二、MyBatis入门
1. 环境搭建
-
将SQL文件导入数据库
-
创建maven工程,引入依赖
<dependencies> <!--mybatis--> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.5.7</version> </dependency> <!--mysql驱动包--> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.26</version> </dependency> <!--junit--> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> </dependency> <!--log4j日志--> <dependency> <groupId>log4j</groupId> <artifactId>log4j</artifactId> <version>1.2.12</version> </dependency> </dependencies>
-
创建mybatis核心配置文件SqlMapConfig.xml
如果dtd报红色,可以在设置中搜索dtds新增该dtd
&是&的转义
<?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> <!--配置环境--> <environments default="mysql"> <environment id="mysql"> <!--事务类型--> <transactionManager type="JDBC"></transactionManager> <!--数据源--> <dataSource type="POOLED"> <property name="driver" value="com.mysql.jdbc.Driver"/> <property name="url" value="jdbc:mysql:///mybatis?useUnicode=true&characterEncoding=utf8"/> <property name="username" value="root"/> <property name="password" value="123456"/> </dataSource> </environment> </environments> </configuration>
-
将log4j.properties文件放入resources中,让控制台打印SQL语句。
# Set root category priority to INFO and its only appender to CONSOLE. #log4j.rootCategory=INFO, CONSOLE debug info warn error fatal log4j.rootCategory=debug, CONSOLE # Set the enterprise logger category to FATAL and its only appender to CONSOLE. #log4j.logger.org.apache.axis.enterprise=FATAL, CONSOLE # CONSOLE is set to be a ConsoleAppender using a PatternLayout. log4j.appender.CONSOLE=org.apache.log4j.ConsoleAppender log4j.appender.CONSOLE.layout=org.apache.log4j.PatternLayout log4j.appender.CONSOLE.layout.ConversionPattern=[%d{MM/dd HH:mm:ss}] %-6r [%15.15t] %-5p %30.30c %x - %m\n
-
创建实体类
package com.sxt.pojo; public class User { private int id; private String username; private String sex; private String address; public User() { } public User(int id, String username, String sex, String address) { this.id = id; this.username = username; this.sex = sex; this.address = address; } @Override public String toString() { return "User{" + "id=" + id + ", username='" + username + '\'' + ", sex='" + sex + '\'' + ", address='" + address + '\'' + '}'; } public int getId() { return id; } public void setId(int id) { this.id = id; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getSex() { return sex; } public void setSex(String sex) { this.sex = sex; } public String getAddress() { return address; } public void setAddress(String address) { this.address = address; } }
2. 创建持久层接口和映射文件
-
在java目录创建持久层接口
public interface UserMapper { List<User> findAll(); }
-
在resource目录创建映射文件
<?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.sxt.mapper.UserMapper"> <select id="findAll" resultType="com.sxt.pojo.User"> select * from user </select> </mapper>
-
将映射文件配置到mybatis核心配置文件中
<?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> <!--配置环境--> <environments default="mysql"> <environment id="mysql"> <!--事务类型--> <transactionManager type="JDBC"></transactionManager> <!--数据源--> <dataSource type="POOLED"> <property name="driver" value="com.mysql.jdbc.Driver"/> <property name="url" value="jdbc:mysql///mybatis"/> <property name="username" value="root"/> <property name="password" value="123456"/> </dataSource> </environment> </environments> <!--注册映射文件--> <mappers> <mapper resource="com/sxt/mapper/UserMapper.xml"></mapper> </mappers> </configuration>
映射文件注意事项:
映射文件要和接口名称相同。
映射文件要和接口的目录结构相同。
映射文件中namespace属性要写接口的全名。名称空间,对应的接口的全限定名
映射文件中标签的id属性是接口方法的方法名。
映射文件中标签的resultType属性是接口方法的返回值类型。
映射文件中标签的parameterType属性是接口方法的参数类型。
映射文件中resultType、parameterType属性要写全类名,如果是集合类型,则写其泛型的全类名。
3. 测试持久层接口方法
import com.sxt.mapper.UserMapper;
import com.sxt.pojo.User;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.junit.Test;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
public class TestUserMapper {
@Test
public void testFindAll() throws IOException {
//【1】读取核心配置文件
InputStream is = Resources.getResourceAsStream("SqlMapConfig.xml");
//【2】创建SqlSessionFactoryBuilder对象
SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
//【3】SqlSessionFactoryBuilder对象获取SqlSessionFactory对象
SqlSessionFactory factory = builder.build(is);
//【4】SqlSessionFactory对象获取SqlSession对象
SqlSession session = factory.openSession();
//【5】SqlSession对象获取代理对象
UserMapper mapper = session.getMapper(UserMapper.class);
//【6】代理对象执行方法
List<User> list = mapper.findAll();
list.forEach(System.out::println);
//【7】释放资源
session.close();
is.close();
}
}
4. MyBatis核心对象及工作流程
4.1 MyBatis核心对象
-
SqlSessionFactoryBuilder
SqlSession工厂构建者对象,使用构造者模式创建SqlSession工厂对象。
-
SqlSessionFactory
SqlSession工厂,使用工厂模式创建SqlSession对象。
-
SqlSession
该对象可以操作数据库,也可以使用动态代理模式创建持久层接口的代理对象操作数据库。
-
Mapper
持久层接口的代理对象,他具体实现了持久层接口,用来操作数据库。
4.2 MyBatis工作流程
- 创建SqlSessionFactoryBuilder对象
- SqlSessionFactoryBuilder对象构建了SqlSessionFactory对象:构造者模式
- SqlSessionFactory对象生产了SqlSession对象:工厂模式
- SqlSession对象创建了持久层接口的代理对象:动态代理模式
- 代理对象操作数据库
5. 使用SqlSession操作数据库
除了代理对象能够操作数据库,SqlSession也能操作数据库。只是这种方式在开发中使用的较少,接下来我们使用SqlSession操作数据库:
SqlSession调用的方法中参数应传入接口的全类名和方法名
@Test
public void testFindAll2() throws IOException {
//【1】读取核心配置文件
InputStream is = Resources.getResourceAsStream("SqlMapConfig.xml");
//【2】创建SqlSessionFactoryBuilder对象
SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
//【3】SqlSessionFactoryBuilder对象获取SqlSessionFactory对象
SqlSessionFactory factory = builder.build(is);
//【4】SqlSessionFactory对象获取SqlSession对象
SqlSession session = factory.openSession();
//【5】SqlSession直接操作数据库 需要传入接口的全类名和方法名
List<User> list = session.selectList("com.sxt.mapper.UserMapper.findAll");
list.forEach(System.out::println);
//【6】关闭资源
session.close();
is.close();
}
6. Mapper动态代理原理
接下来我们通过源码,了解MyBatis的Mapper对象究竟是怎么生成的,他又是如何代理接口的方法。
6.1 获取代理对象
点开测试类的 getMapper 方法,查看该方法最终调用了什么方法。
当看到 Proxy.newProxyInstance 时,可以确定 getMapper 方法最终调用的是JDK动态代理方法,且使用MapperProxy类定义代理方式
6.2 查看代理方式
点开MapperProxy类,查看invoke方法,查看代理对象是如何工作的。
可以看到,MapperProxy调用了MapperMethod的execute方法定义了代理方式,且底层调用的是SqlSession的方法,根据映射文件标签不同调用不同的SqlSession方法。
结论:
- SqlSession的getMapper方法,最终是调用的是JDK动态代理方法,生成一个代理对象,类型就是传入的接口类型。
- MapperProxy对象通过调用MapperMethod的execute方法定义了代理方式,该方法的底层调用的是SqlSession的方法。
三、MyBatis增删改查
1. MyBatis新增
新增用户
-
持久层接口添加方法
void add(User user);
-
映射文件添加标签
<insert id="add" parameterType="com.sxt.pojo.User" > insert into user(username,sex,address) values(#{username},#{sex},#{address}) </insert>
-
编写测试方法
@Test public void testAdd() throws Exception{ InputStream is = Resources.getResourceAsStream("SqlMapConfig.xml"); SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder(); SqlSessionFactory factory = builder.build(is); SqlSession sqlSession = factory.openSession(); UserMapper mapper = sqlSession.getMapper(UserMapper.class); User user = new User("哈哈", "男", "天津"); mapper.add(user); //默认事务是不会自动提交的 sqlSession.commit(); sqlSession.close(); is.close(); }
注意:
- 当接口方法的参数类型为POJO类型时,SQL语句中绑定参数时使用 #{POJO的属性名} 即可。
- MyBatis事务默认手动提交,所以在执行完增删改方法后,需要手动调用SqlSession对象的事务提交方法,否则数据库将不发生改变。
- dml操作需要手动提交事务,查询则不需要手动提交,因为提交并没有改变数据库
2. MyBatis修改
优化测试类
我们发现MyBatis的测试方法在操作数据库前都需要获取代理对象,操作数据库后都需要释放资源,可以利用Junit的前置后置方法,优化测试类代码。
import com.sxt.mapper.UserMapper;
import com.sxt.pojo.User;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import java.io.IOException;
import java.io.InputStream;
public class TestUserMapper2 {
InputStream is = null;
SqlSession session = null;
UserMapper mapper = null;
@Before
public void before() throws IOException {
//【1】读取核心配置文件
is = Resources.getResourceAsStream("SqlMapConfig.xml");
//【2】创建SqlSessionFactoryBuilder对象
SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
//【3】SqlSessionFactoryBuilder对象获取SqlSessionFactory对象
SqlSessionFactory factory = builder.build(is);
//【4】SqlSessionFactory对象获取SqlSession对象
session = factory.openSession();
//【5】SqlSession对象获取代理对象
mapper = session.getMapper(UserMapper.class);
}
@After
public void after() throws IOException {
//释放资源
session.close();
is.close();
}
}
这样Junit就会自动执行获取代理对象和释放资源的方法。
修改用户
-
持久层接口添加方法
void update(User user);
-
映射文件添加标签
<update id="update" parameterType="com.sxt.pojo.User"> update user set username = #{username},sex=#{sex},address=#{address} where id = #{id} </update>
-
编写测试方法
@Test public void testUpdate(){ User user = new User(7,"程序员","女","山西"); mapper.update(user); session.commit(); }
3. MyBatis删除、根据Id查询
删除用户
-
持久层接口添加方法
void delete(int userId);
-
映射文件添加标签
<delete id="delete" parameterType="int"> delete from user where id = #{id} </delete>
注:当方法的参数类型是简单数据类型时,#{}中可以写任意名称
- 简单数据类型:基本数据类型、字符串等
-
编写测试方法
@Test public void testDelete(){ mapper.delete(7); session.commit(); }
根据ID查询用户
查询是不需要提交事务的,因为没有改变数据库
-
持久层接口添加方法
User findById(int userId);
-
映射文件添加标签
<select id="findById" resultType="com.sxt.pojo.User"> select * from user where id = #{userId} </select>
-
编写测试方法
@Test public void testFindByUserId(){ User user = mapper.findById(6); System.out.println(user); }
4. MyBatis模糊查询
4.1 使用#定义参数
-
持久层接口添加方法
List<User> findByNameLike(String username);
-
映射文件添加标签
<select id="findByUsernameLike" parameterType="string" resultType="com.sxt.pojo.User"> select * from user where username like #{username} </select>
-
编写测试方法
@Test public void testFindByUsernameLike(){ List<User> list = mapper.findByUsernameLike("%尚学堂%"); list.forEach(System.out::println); }
我们看到在映射文件中,parameterType的值为 string 而没有写java.lang.String ,这是为什么呢?
- 参数/返回值类型为基本数据类型/包装类/String等类型时,我们可以写全类名,也可以写别名。
4.2 使用$定义参数
模糊查询如果不想在调用方法时参数加%,可以使用拼接参数的方式设置Sql:
<select id="findByUsernameLike" parameterType="string" resultType="com.sxt.pojo.User">
select * from user where username like '%${value}%'
</select>
测试方法写法如下:
@Test
public void testFindByUsernameLike(){
List<User> list = mapper.findByUsernameLike("尚学堂");
list.forEach(System.out::println);
}
#和$的区别:
- #表示sql模板的占位符,$表示将字符串拼接到sql模板中。
- #可以防止sql注入,一般能用#就不用KaTeX parse error: Expected 'EOF', got '#' at position 2: 。#̲相当于是preparedSta…则是statement
- ${}内部的参数名必须写value。当parameterType 是唯一属性时 ${ } 可以写任意值的 但作为 见名知义 建议使用 实体的属性名称
4.3 使用<bind>定义参数
如果使用 # 还不想在调用方法的参数中添加 % ,可以使用 <bind> ,<bind> 允许我们在 Sql语句以外创建一个变量,并可以将其绑定到当前的Sql语句中。用法如下:
<select id="findByUsernameLike" parameterType="string" resultType="com.sxt.pojo.User">
<bind name="likeName" value="'%'+username+'%'"/>
select * from user where username like #{likeName}
</select>
测试方法写法如下:
@Test
public void testFindByUsernameLike(){
List<User> list = mapper.findByUsernameLike("尚学堂");
list.forEach(System.out::println);
}
5. MyBatis分页查询
分页查询时,Sql语句使用limit关键字,需要传入开始索引和每页条数两个参数。MyBatis的多参数处理有以下方式:
5.1 顺序传参
Sql中的参数使用arg0,arg1…或param1,param2…表示参数的顺序。此方法可读性较低,在开发中不建议使用。注意只能是arg或param,别的会报错
-
持久层接口方法
/** * 分页查询 * @param startIndex 开始索引 * @param pageSize 每页条数 * @return */ List<User> findPage(int startIndex,int pageSize);
-
映射文件
<select id="findPage" resultType="com.sxt.pojo.User"> select * from user limit #{arg0},#{arg1} </select> <!--或者:--> <select id="findPage" resultType="com.sxt.pojo.User"> select * from user limit #{param1},#{param2} </select>
-
测试类
@Test public void testFindPage(){ List<User> list = mapper.findPage(0, 3); list.forEach(System.out::println); }
5.2 @Param传参
在接口方法的参数列表中通过==@Param定义参数名称==,在Sql语句中通过注解中所定义的参数名称指定参数位置。此方式参数比较直观的,推荐使用。
-
持久层接口方法
List<User> findPage1(@Param("startIndex") int startIndex, @Param("pageSize") int pageSize);
-
映射文件
<select id="findPage1" resultType="com.sxt.pojo.User"> select * from user limit #{startIndex},#{pageSize} </select>
-
测试类
@Test public void testFindPage1(){ List<User> list = mapper.findPage1(0, 3); list.forEach(System.out::println); }
5.3 POJO传参
自定义POJO类,该类的属性就是要传递的参数,在SQL语句中绑定参数时使用POJO的属性名作为参数名即可。此方式推荐使用。
-
自定义POJO
public class PageQuery { private int startIndex; private int pageSize; // 省略getter/setter/构造方法 }
-
持久层接口方法
List<User> findPage2(PageQuery pageQuery);
-
映射文件
<select id="findPage2" resultType="com.sxt.pojo.User" parameterType="com.sxt.pojo.PageQuery"> select * from user limit #{startIndex},#{pageSize} </select>
-
测试类
@Test public void testFindPage2(){ PageQuery pageQuery = new PageQuery(3,2); List<User> list = mapper.findPage2(pageQuery); list.forEach(System.out::println); }
5.4 Map传参
如果不想自定义POJO,可以使用Map作为传递参数的载体,在SQL语句中绑定参数时使用Map的Key作为参数名即可。此方法推荐使用。
-
持久层接口方法
List<User> findPage3(Map<String,Object> params);
-
映射文件
<select id="findPage3" resultType="com.sxt.pojo.User" parameterType="map"> select * from user limit #{startIndex},#{pageSize} </select>
-
测试类
@Test public void testFindPage3(){ HashMap<String, Object> map = new HashMap<>(); map.put("startIndex",0); map.put("pageSize",4); List<User> list = mapper.findPage3(map); list.forEach(System.out::println); }
6. MyBatis聚合查询、主键回填
6.1 查询用户总数
-
持久层接口方法
int findCount();
-
映射文件
<select id="findCount" resultType="int"> select count(id) from user </select>
-
测试类
@Test public void testFindCount(){ int count = mapper.findCount(); System.out.println(count); }
6.2 主键回填
有时我们需要获取新插入数据的主键值。如果数据库中主键是自增的,这时我们就需要使用MyBatis的主键回填功能。
-
持久层接口方法
void add2(User user);
-
映射文件
<insert id="add2" parameterType="com.sxt.pojo.User"> <!--keyProperty:主键属性名 keyColumn:主键列名 resultType:主键类型 order:执行时机--> <selectKey keyProperty="id" keyColumn="id" resultType="int" order="AFTER"> select last_insert_id(); </selectKey> insert into user(username,sex,address) values(#{username},#{sex},#{address}) </insert>
SELECT LAST_INSERT_ID():查询刚刚插入的记录的主键值,只适用于自增主键,且必须和insert语句一起执行。
-
测试类
@Test public void testAdd2(){ User user = new User("程序员", "男", "天津"); System.out.println(user); mapper.add2(user); session.commit(); System.out.println(user); }
四、MyBatis配置文件
1. properties标签
MyBatis配置文件结构:
-configuration
-properties(属性)
-property
-settings(全局配置参数)
-setting
-plugins(插件)
-plugin
-typeAliases(别名)
-typeAliase
-package
-environments(环境)
-environment
-transactionManager(事务管理)
-dataSource(数据源)
-mappers(映射器)
-mapper
-package
properties
属性值定义。properties标签中可以定义属性值,也可以引入外部配置文件。无论是内部定义还是外部引入,都可以使用==${name}获取值==。
例如:我们可以将数据源配置写到外部的db.properties中,再使用properties标签引入外部配置文件,这样可以做到动态配置数据源。
-
编写db.properties
jdbc.driver = com.mysql.jdbc.Driver jdbc.url = jdbc:mysql:///mybatis jdbc.username = root jdbc.password = 123456
-
在配置文件中引入db.properties
<!--动态配置数据源--> <properties resource="db.properties"></properties> <!--配置环境--> <environments default="mysql"> <environment id="mysql"> <!--事务类型--> <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>
当然我们也可以将数据源数据通过 <properties> 配置到MyBatis配置文件内,但这样做没什么意义。
<properties> <property name="jdbc" value="com.mysql.jdbc.Driver"/> <property name="jdbc" value="jdbc.url"/> <property name="jdbc" value="jdbc.username"/> <property name="jdbc" value="jdbc.password"/> </properties> <!--配置环境--> <environments default="mysql"> <environment id="mysql"> <!--事务类型--> <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>
2. settings标签
<settings> 是配置MyBatis运行时的一些行为的,例如缓存、延迟加载、命名规则等一系列控制性参数。后期我们会使用该标签配置缓存和延迟加载等。
3. plugins标签
<plugins> 是配置MyBatis插件的。插件可以增强MyBatis功能,比如进行sql增强,打印日志,异常处理等。后期我们会使用该标签配置分页插件。
4. typeAliases标签
MyBatis对常用类有默认别名支持,比如java.lang.Stirng的别名为string。除此之外,我们也可以使用 <typeAliases> 设置自定义别名。
4.1 为一个类配置别名
<typeAliases>
<typeAlias type="全类名" alias="别名"></typeAlias>
</typeAliases>
此时我们即可在映射文件中使用自定义别名,如:
-
配置文件:
<typeAliases> <!--type:全类名,alias:别名--> <typeAlias type="com.sxt.pojo.User" alias="user"></typeAlias> </typeAliases>
-
映射文件:
别名小写
<select id="findAll" resultType="user"> select * from user </select>
4.2 为一个所有包下的所有类配置别名
<typeAliases>
<package name="包名"></package>
</typeAliases>
此时该包下的所有类都有了别名,别名省略包名,和类名相同。
如:
-
配置文件:
<typeAliases> <!--为该包下的所有类配置别名,别名省略包名,和类名相同--> <package name="com.sxt.pojo"/> </typeAliases>
-
映射文件:
注意:和类名相同,首字母大写
<select id="findAll" resultType="User"> select * from user </select> <select id="findPage2" resultType="User" parameterType="PageQuery"> select * from user limit #{startIndex},#{pageSize} </select>
5. environments标签
<environments> 可以为MyBatis配置数据环境。
5.1 事务管理
不推荐使用MANAGED
<environments default="mysql">
<environment id="mysql">
<!--JDBC:使用JDBC的提交和回滚 MANAGED:不做事务处理-->
<transactionManager type="JDBC"></transactionManager>
</environment>
</environments>
5.2 连接池
<environments default="mysql">
<environment id="mysql">
<transactionManager type="JDBC"></transactionManager>
<!-- 连接池设置 -->
<dataSource type="POOLED">
<!-- 数据源设置... -->
</dataSource>
</environment>
</environments>
dataSource的type属性:
- POOLED:使用连接池管理连接,使用MyBatis自带的连接池。
- UNPOOLED:不使用连接池,直接由JDBC连接。
- JNDI:由JAVAEE服务器管理连接,如果使用Tomcat作为服务器则使用Tomcat自带的连接池管理。
6. mappers标签
<mappers> 用于注册映射文件或持久层接口,只有注册的映射文件才能使用,共有四种方式都可以完成注册:
-
使用相对路径注册映射文件
<mappers> <!--相对路径:使用resource--> <mapper resource="com/sxt/mapper/UserMapper.xml"></mapper> </mappers>
-
使用绝对路径注册映射文件
注意:要在url加上file:///
<mappers> <!--绝对路径:使用url,注意要在路径前面加上file:///--> <mapper url="file:///C:\JavaStudy\IdeaProjects\mybatiscase\mybatisDemo1\src\main\resources\com\sxt\mapper\UserMapper.xml"></mapper> </mappers>
-
注册持久层接口
<mappers> <!--注册持久层接口--> <mapper class="com.sxt.mapper.UserMapper"></mapper> </mappers>
-
注册一个包下的所有持久层接口
<mappers> <!--注册一个包下的所有持久层接口--> <package name="com.sxt.mapper"/> </mappers>
五、MyBatis映射文件
1. resultMap标签自定义映射关系
MyBatis映射文件中除了 <insert> 、 <delete> 、 <update> 、 <select> 外,还有一些标签可以使用:
resultMap
标签的作用的自定义映射关系。
MyBatis可以将数据库结果集封装到对象中,是因为结果集的列名和对象属性名相同:
当POJO属性名和数据库列名不一致时,MyBatis无法自动完成映射关系。如:
使用idea自带的连接数据库:
此时有两种解决方案:
-
Sql语句的查询字段起与POJO属性相同的别名。
在select语句中使用as起别名,和teacher对象中字段名相同的别名
<select id="findAll" resultType="com.sxt.pojo.Teacher"> select tid as id,tname as teacherName from teacher; </select>
-
自定义映射关系
-
在映射文件中,使用 <resultMap> 自定义映射关系:
id定义主键列 property:POJO属性名 column:数据库列名
result定义普通列 property:POJO属性名 column:数据库列名
<!--id:自定义映射名 type:自定义映射的对象类型--> <resultMap id="teacherMapper" type="com.sxt.pojo.Teacher"> <!--id定义主键列 property:POJO属性名 column:数据库列名--> <id property="id" column="tid"></id> <!--result定义普通列 property:POJO属性名 column:数据库列名--> <result property="teacherName" column="tname"></result> </resultMap>
-
在 <select> 标签中,使用 resultMap 属性代替 resultType 属性,使用自定义映射关系。
<select id="findAll" resultMap="teacherMapper"> select * from teacher; </select>
-
2. sql、include标签重用sql
<sql> 用来定义可重用的Sql片段,通过 <include> 引入该片段。如:Sql语句的查询字段起与POJO属性相同的别名,该Sql片段就可以重用。
<sql id="selectAllField">
select tid as id,tname as teacherName
</sql>
<select id="findAll" resultType="com.sxt.pojo.Teacher">
<include refid="selectAllField"></include>
from teacher;
</select>
<select id="findById" resultType="com.sxt.pojo.Teacher">
<include refid="selectAllField"></include>
from teacher
where tid = #{id}
</select>
3. 特殊字符处理
在Mybatis映射文件中尽量不要使用一些特殊字符,如: < , > 等。我们可以使用符号的实体来表示:
如:
<select id="findById2" resultType="com.sxt.pojo.Teacher">
<include refid="selectAllField"></include>
from Teacher
where tid > #{id}
</select>
六、动态SQL
1. if标签
一个查询的方法的Sql语句不一定是固定的。比如电商网站的查询商品,用户使用不同条件查询,Sql语句就会添加不同的查询条件。此时就需要在方法中使用动态Sql语句。
<if>
<if> 标签内的Sql片段在满足条件后才会添加,用法为: <if test=“条件”> 。例如:根据不同条件查询用户:
-
持久层接口添加方法
// 用户通用查询 List<User> findByCondition(User user);
-
映射文件添加标签
<select id="findByCondition" parameterType="com.sxt.pojo.User" resultType="com.sxt.pojo.User"> select * from user where 1 = 1 <if test="username!=null and username.length()!=0"> and username like #{username} </if> <if test="sex!=null and sex.length()!=0"> and sex = #{sex} </if> <if test="address!=null and address.length()!=0"> and address = #{address} </if> </select>
-
编写测试方法
@Test public void testFindByCondition(){ User user = new User(); // List<User> list = mapper.findByCondition(user); // list.forEach(System.out::println); user.setUsername("%尚学堂%"); // List<User> list1 = mapper.findByCondition(user); // list1.forEach(System.out::println); user.setUsername("%尚学堂%"); user.setAddress("北京"); List<User> list2 = mapper.findByCondition(user); list2.forEach(System.out::println); }
- if中的条件不能使用&&/||,而应该使用and/or
- if中的条件可以直接通过属性名获取参数POJO的属性值,并且该值可以调用方法。
- where后为什么要加1=1?
任意条件都可能拼接到Sql中。如果有多个条件,从第二个条件开始前都需要加And关键字。加上1=1这个永久成立的条件,就不需要考虑后面的条件哪个是第一个条件,后面的条件前都加And关键字即可。
2. where标签
<where> 可以代替sql中的where 1=1 和第一个and,更符合程序员的开发习惯,使用 <where> 后的映射文件如下:
<select id="findByCondition" parameterType="com.sxt.pojo.User" resultType="com.sxt.pojo.User">
select * from user
<where>
<if test="username!=null and username.length()!=0">
username like #{username}
</if>
<if test="sex!=null and sex.length()!=0">
and sex = #{sex}
</if>
<if test="address!=null and address.length()!=0">
and address = #{address}
</if>
</where>
</select>
3. set标签
<set> 标签用在update语句中。借助 <if> ,可以只对有具体值的字段进行更新。 <set> 会自动添加set关键字,并去掉最后一个if语句中多余的逗号。
<update id="updateUser" parameterType="com.sxt.pojo.User">
update user
<set>
<if test="username !=null and username.length()!=0">
username = #{username},
</if>
<if test="sex!=null and sex.length()!=0">
sex = #{sex},
</if>
<if test="address!=null and address.length()!=0">
addresss = #{address}
</if>
</set>
<where>
id = #{id}
</where>
</update>
4. choose、when、otherwise标签
这些标签表示多条件分支,类似JAVA中的 switch…case 。 <choose> 类似switch , <when> 类似 case , <otherwise> 类似 default ,用法如下:
<select id="findByUsername" resultType="com.sxt.pojo.User" parameterType="com.sxt.pojo.User">
select * from user
<where>
<choose>
<when test="username.length() < 5">
<bind name="likeName" value="'%'+username+'%'"/>
username like #{likeName}
</when>
<when test="username.length() < 10">
username like #{userName}
</when>
<otherwise>
id = 1
</otherwise>
</choose>
</where>
</select>
这段代码的含义为:用户名<5时使用模糊查询,用户名>=5并且<10时使用精确查询,否则查询id为1的用户
5. foreach标签
<foreach> 类似JAVA中的for循环,可以遍历集合或数组。 <foreach> 有如下属性:
- collection:遍历的对象类型
- open:开始的sql语句
- close:结束的sql语句
- separator:遍历每项间的分隔符
- item:表示本次遍历获取的元素,遍历List、Set、数组时表示每项元素,遍历map时表示键值对的值。
- index:遍历List、数组时表示遍历的索引,遍历map时表示键值对的键。
5.1 遍历数组
我们使用 <foreach> 遍历数组进行批量删除。
-
持久层接口添加方法
//批量删除 delete from user where id in( , , ) void deleteBatch(int[] ids);
-
映射文件添加标签
<delete id="deleteBatch" parameterType="int"> delete from user <where> <foreach open="id in(" close=")" collection="array" item="id" separator="," > #{id} </foreach> </where> </delete>
-
编写测试方法
@Test public void testDeleteBatch(){ int[] ids = {9,10}; mapper.deleteBatch(ids); session.commit(); }
5.2 遍历Collection
<foreach> 遍历List和Set的方法是一样的,我们使用 <foreach> 遍历List进行批量添加。
-
持久层接口添加方法
//批量新增 insert into user(username,sex,address) values(),(),() void insertBatch(List<User> user);
-
映射文件添加标签
<insert id="insertBatch" parameterType="com.sxt.pojo.User"> insert into user(username,sex,address) values <foreach collection="list" item="user" separator=","> (#{user.username},#{user.sex},#{user.address}) </foreach> </insert>
-
编写测试方法
@Test public void testInsertBatch(){ User user = new User("程序猿1", "女", "北京"); User user1 = new User("程序猿2", "男", "天津"); List<User> list = new ArrayList<>(); list.add(user); list.add(user1); mapper.insertBatch(list); session.commit(); }
5.3 遍历Map
我们使用 <foreach> 遍历Map进行多条件查询。
-
持久层接口添加方法
遍历map需要设置参数名
/** * 多条件查询 * @param map 查询的条件键值对 键:属性名 值:属性值 * select * from user where username = ? and sex = ? * @return */ List<User> findUser(@Param("queryMap") Map<String,Object> map);
-
映射文件添加标签
index代表键,item代表值
key需要使用$拼接,value使用#占位符
属性名不能是占位符,必须是拼接进来的,属性值可以是占位符
<select id="findUser" parameterType="map" resultType="com.sxt.pojo.User"> select * from user <where> <foreach collection="queryMap" separator="and" index="key" item="value"> ${key} = #{value} </foreach> </where> </select>
-
编写测试方法
@Test public void testFindUser(){ Map<String,Object> map = new HashMap<>(); map.put("sex","男"); map.put("address","北京"); List<User> list = mapper.findUser(map); list.forEach(System.out::println); }
七、MyBatis缓存
1. 缓存介绍
缓存是内存当中一块存储数据的区域,目的是提高查询效率。MyBatis会将查询结果存储在缓存当中,当下次执行相同的SQL时不访问数据库,而是直接从缓存中获取结果,从而减少服务器的压力。
-
什么是缓存?
存在于内存中的一块数据。
-
缓存有什么作用?
减少程序和数据库的交互,提高查询效率,降低服务器和数据库的压力。
-
什么样的数据使用缓存?
经常查询但不常改变的,改变后对结果影响不大的数据。
-
MyBatis缓存分为哪几类?
一级缓存和二级缓存
-
如何判断两次Sql是相同的?
- 查询的Sql语句相同
- 传递的参数值相同
- 对结果集的要求相同
- 预编译的模板Id相同
2. MyBatis一级缓存
- MyBatis一级缓存也叫本地缓存。SqlSession对象中包含一个Executor对象,Executor对象中包含一个PerpetualCache对象,在该对象存放一级缓存数据。
- 由于一级缓存是在SqlSession对象中,所以只有使用同一个SqlSession对象操作数据库时才能共享一级缓存。
- MyBatis的一级缓存是默认开启的,不需要任何的配置。
测试一级缓存
第一个findById查询数据库
第二个findById从缓存中拿结果
同一个sqlSession对象可以共享
两个sqlSession对象不能共享
@Test
public void testCache1() throws IOException {
//【1】读取mybatis核心配置文件
InputStream is = Resources.getResourceAsStream("SqlMapConfig.xml");
//【2】创建构建者对象
SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
//【3】创建工厂
SqlSessionFactory factory = builder.build(is);
//【4】获取SqlSession对象
SqlSession sqlSession = factory.openSession();
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
UserMapper mapper1 = sqlSession.getMapper(UserMapper.class);
User user = mapper.findById(1);
System.out.println(user);
System.out.println("-----------------------------------");
User user1 = mapper1.findById(1);
System.out.println(user1);
}
@Test
public void testCache2() throws IOException {
//【1】读取mybatis核心配置文件
InputStream is = Resources.getResourceAsStream("SqlMapConfig.xml");
//【2】创建构建者对象
SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
//【3】创建工厂
SqlSessionFactory factory = builder.build(is);
//【4】获取SqlSession对象
SqlSession sqlSession1 = factory.openSession();
SqlSession sqlSession2 = factory.openSession();
UserMapper mapper1 = sqlSession1.getMapper(UserMapper.class);
UserMapper mapper2 = sqlSession2.getMapper(UserMapper.class);
User user = mapper1.findById(1);
System.out.println(user);
System.out.println("-----------------------------------");
User user1 = mapper2.findById(1);
System.out.println(user1);
}
3. MyBatis清空一级缓存
进行以下操作可以清空MyBatis一级缓存:
- SqlSession 调用 close() :操作后SqlSession对象不可用,该对象的缓存数据也不可用。
- SqlSession 调用 clearCache() / commit() :操作会清空一级缓存数据。
- SqlSession 调用增删改方法:操作会清空一级缓存数据,因为增删改后数据库发生改变,缓存数据将不准确。
@Test
public void testCache3() throws IOException {
//【1】读取mybatis核心配置文件
InputStream is = Resources.getResourceAsStream("SqlMapConfig.xml");
//【2】创建构建者对象
SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
//【3】创建工厂
SqlSessionFactory factory = builder.build(is);
//【4】获取SqlSession对象
SqlSession sqlSession = factory.openSession();
UserMapper mapper1 = sqlSession.getMapper(UserMapper.class);
UserMapper mapper2 = sqlSession.getMapper(UserMapper.class);
User user = mapper1.findById(1);
System.out.println(user);
//清空方法一:
// sqlSession.close();
//清空方法二:
// sqlSession.clearCache();
// sqlSession.commit();
//清空方法三:
mapper1.delete(2);
System.out.println("-----------------------------------");
User user1 = mapper2.findById(1);
System.out.println(user1);
}
4. MyBatis二级缓存
-
MyBatis二级缓存也叫全局缓存。数据存放在SqlSessionFactory中,只要是同一个工厂对象创建的SqlSession,在进行查询时都能共享数据。一般在项目中只有一个SqlSessionFactory对象,所以二级缓存的数据是全项目共享的。
-
MyBatis一级缓存存放的是对象,二级缓存存放的是对象的数据。所以要求二级缓存存放的POJO必须是可序列化的,也就是要实现Serializable接口。
-
MyBatis二级缓存默认不开启,手动开启后数据先存放在一级缓存中,只有一级缓存数据清空后,数据才会存到二级缓存中。
SqlSession 调用 clearCache() 无法将数据存到二级缓存中。
开启二级缓存
-
POJO类实现Serializable接口。
public class User implements Serializable { private int id; private String username; private String sex; private String address; }
-
在MyBatis核心配置文件添加如下设置:
<!--开启二级缓存--> <settings> <setting name="cacheEnabled" value="true"/> </settings>
由于cacheEnabled默认值是true,所以该设置可以省略。
-
在映射文件添加 <cache /> 标签,该映射文件下的所有方法都支持二级缓存。
如果不加cache标签,则无法使用二级缓存
如果查询到的集合中对象过多,二级缓存只能缓存1024个对象引用。可以通过 <cache /> 标签的size属性修改该数量。
<cache size="2048"/>
-
测试二级缓存
@Test public void testCache4() throws IOException { //【1】读取mybatis核心配置文件 InputStream is = Resources.getResourceAsStream("SqlMapConfig.xml"); //【2】创建构建者对象 SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder(); //【3】创建工厂 SqlSessionFactory factory = builder.build(is); //【4】获取SqlSession对象 //这两个sqlSession是同一个工厂创造的,是可以共享二级缓存的 SqlSession sqlSession1 = factory.openSession(); SqlSession sqlSession2 = factory.openSession(); UserMapper mapper1 = sqlSession1.getMapper(UserMapper.class); UserMapper mapper2 = sqlSession2.getMapper(UserMapper.class); User user = mapper1.findById(1); System.out.println(user); //让一级缓存失效 sqlSession1.close(); System.out.println("-----------------------------------"); User user1 = mapper2.findById(1); System.out.println(user1); }
八、MyBatis关联查询
MyBatis的关联查询分为一对一关联查询和一对多关联查询。
- 查询对象时,将关联的另一个对象查询出来,就是一对一关联查询。
- 查询对象时,将关联的另一个对象的集合查询出来,就是一对多关联查询。
例如有学生类和班级类:
一个学生对应一个班级,也就是学生类中有一个班级属性,这就是一对一关系。
一个班级对应多个学生,也就是班级类中有一个学生集合属性,这就是一对多关系。
实体类设计如下:
public class Student {
private int sid;
private String name;
private int age;
private String sex;
//一
private Classes classes;
// 省略getter/setter/toString
}
public class Classes {
private int cid;
private String className;
//多
private List<Student> studentList;
// 省略getter/setter/toString
}
数据库设计如下:
在多方表设置一方的外键
1. MyBatis一对一关联查询
查询学生时,将关联的一个班级对象查询出来,就是一对一关联查询。
1.1 创建持久层接口
public interface StudentMapper {
List<Student> findAll();
}
1.2 创建映射文件
<association>关联对象列
一对一对象列
property:属性名 column:关联列名 javaType:对象类型
<resultMap id="studentMapper" type="com.sxt.pojo.Student">
<!--主键列-->
<id property="sid" column="sid"></id>
<!--普通列-->
<result property="name" column="name"></result>
<result property="age" column="age"></result>
<result property="sex" column="sex"></result>
<!--一对一对象列 property:属性名 column:关联列名 javaType:对象类型-->
<!--班级对象-->
<association property="classes" column="classId" javaType="com.sxt.pojo.Classes">
<!--关联对象主键列-->
<id property="cid" column="cid"></id>
<!--关联对象普通列-->
<result property="className" column="className"></result>
</association>
</resultMap>
<select id="findAll" resultMap="studentMapper">
select * from student left join classes on student.classId = classes.cid;
</select>
1.3 配置文件注册映射文件
<!--注册映射文件-->
<mappers>
<package name="com.sxt.mapper"/>
</mappers>
1.4 测试一对一关联查询
@Test
public void testFindAllStudent(){
StudentMapper mapper = session.getMapper(StudentMapper.class);
List<Student> list = mapper.findAll();
list.forEach(System.out::println);
}
2. MyBatis一对多关联查询
查询班级时,将关联的学生集合查询出来,就是一对多关联查询。
2.1 创建持久层接口
public interface ClassesMapper {
List<Classes> findAll();
}
2.2 创建映射文件
<collection>集合列
property:属性名 column:关联列 ofType:集合的泛型
<resultMap id="classesMapper" type="com.sxt.pojo.Classes">
<id property="cid" column="cid"></id>
<result property="className" column="className"></result>
<!--集合列 property:属性名 column:关联列 ofType:集合的泛型-->
<!--学生集合-->
<collection property="students" column="classId" ofType="com.sxt.pojo.Student">
<id property="sid" column="sid"></id>
<result property="name" column="name"></result>
<result property="age" column="age"></result>
<result property="sex" column="sex"></result>
</collection>
</resultMap>
<select id="findAll" resultMap="classesMapper">
select * from classes left join student on classes.cid = student.classId;
</select>
2.3 测试一对多关联查询
@Test
public void testFindAllClasses(){
ClassesMapper mapper = session.getMapper(ClassesMapper.class);
List<Classes> list = mapper.findAll();
list.forEach(System.out::println);
}
3. MyBatis多对多关联查询
MyBatis多对多关联查询本质就是两个一对多关联查询。
例如有老师类和班级类:
一个老师对应多个班级,也就是老师类中有一个班级集合属性。
一个班级对应多个老师,也就是班级类中有一个老师集合属性。
实体类设计如下:
public class Teacher {
private Integer tid;
private String tname;
private List<Classes> classes;
// 省略getter/setter/toString
}
public class Classes {
private Integer cid;
private String className;
private List<Student> studentList;
private List<Teacher> teacherList;
// 省略getter/setter/toString
}
在数据库设计中,需要建立中间表,双方与中间表均为一对多关系。
接下来测试查询老师时,将关联的班级集合查询出来。
3.1 创建持久层接口
public interface TeacherMapper {
List<Teacher> findAll();
}
3.2 创建映射文件
<!--封装老师-->
<resultMap id="TeacherMapper" type="com.sxt.pojo.Teacher">
<id property="tid" column="tid"></id>
<result property="tname" column="tname"></result>
<!--班级集合 column:关联列-->
<collection property="classes" column="tid" ofType="com.sxt.pojo.Classes">
<id column="cid" property="cid"></id>
<result column="className" property="className"></result>
</collection>
</resultMap>
<select id="findAll" resultMap="TeacherMapper">
select *
from teacher
left join classes_teacher
on teacher.tid = classes_teacher.tid
left join classes
on classes_teacher.cid = classes.cid
</select>
3.3 测试多对多关联查询
@Test
public void testFindAllTeacher(){
TeacherMapper mapper = session.getMapper(TeacherMapper.class);
List<Teacher> list = mapper.findAll();
list.forEach(System.out::println);
}
如果想查询班级时,将关联的老师集合查询出来,只需要修改班级映射文件的Sql语句和 <resultMap> 即可:
<resultMap id="classesMapper" type="com.sxt.pojo.Classes">
<id property="cid" column="cid"></id>
<result property="className" column="className"></result>
<!--集合列 property:属性名 column:关联列 ofType:集合的泛型-->
<!--学生-->
<collection property="students" column="classId" ofType="com.sxt.pojo.Student">
<id property="sid" column="sid"></id>
<result property="name" column="name"></result>
<result property="age" column="age"></result>
<result property="sex" column="sex"></result>
</collection>
<!--老师-->
<collection property="teachers" column="cid" ofType="com.sxt.pojo.Teacher">
<id property="tid" column="tid"></id>
<result property="tname" column="tname"></result>
</collection>
</resultMap>
<select id="findAll" resultMap="classesMapper">
select *
from classes
left join student
on classes.cid = student.classId
left join classes_teacher
on classes.cid = classes_teacher.cid
left join teacher
on teacher.tid = classes_teacher.tid
</select>
4. MyBatis分解式查询_一对多
在MyBatis多表查询中,使用连接查询时一个Sql语句就可以查询出所有的数据。如:
# 查询班级时关联查询出学生
select *
from classes
left join student
on student.classId = classes.cid
也可以使用分解式查询,即将一个连接Sql语句分解为多条Sql语句,如:
# 查询班级时关联查询出学生
select * from classes;
select * from student where classId = 1;
select * from student where classId = 2;
这种写法也叫N+1查询。
连接查询:
- 优点:降低查询次数,从而提高查询效率。
- 缺点:如果查询返回的结果集较多会消耗内存空间。
N+1查询:
- 优点:结果集分步获取,节省内存空间。
- 缺点:由于需要执行多次查询,相比连接查询效率低。
我们以查询班级时关联查询出学生为例,使用N+1查询:
4.1 创建每个查询语句的持久层方法
public interface ClassesMapper {
// 查询所有班级
List<Classes> findAll();
}
public interface StudentMapper {
// 根据班级Id查询学生
List<Student> findByClassId(int classId);
}
4.2 在映射文件中进行配置
在ClassMapper2中定义findAll方法
在StudentMapper2中定义findByClassId方法
<select id="findAll" resultType="com.sxt.pojo.Classes">
select * from clasees
</select>
<select id="findByClassId" resultType="com.sxt.pojo.Student" parameterType="int">
select * from student where classId = #{classId}
</select>
4.3 修改主表映射文件中的查询方法
调用从表的查询方法,通过调用学生的findByClassId方法查询所有班级的学生信息
在collection标签中添加select属性,通过这个select属性来调用findByClassId方法
<!--自定义映射关系-->
<resultMap id="myClassesMapper" type="com.sxt.pojo.Classes">
<id property="cid" column="cid"></id>
<result property="className" column="className"></result>
<!--property:属性名,ofType:泛型名,select:从表查询调用的方法,column:查询传入的字段-->
<collection property="students"
ofType="com.sxt.pojo.Student"
select="com.sxt.mapper2.StudentMapper2.findByClassId"
column="cid">
</collection>
</resultMap>
<select id="findAll" resultMap="myClassesMapper">
select * from classes
</select>
4.4 测试查询方法
@Test
public void testFindAllClasses2(){
ClassesMapper2 mapper2 = session.getMapper(ClassesMapper2.class);
List<Classes> list = mapper2.findAll();
list.forEach(System.out::println);
}
5. MyBatis分解式查询_一对一
查询学生时关联查询出班级也可以使用分解式查询,首先将查询语句分开:
select * from student;
select * from classes where cid = ?;
5.1 创建每个查询语句的持久层方法
//查询所有学生
List<Student> findAll();
//根据id查询班级
Classes findByCid(int cid);
5.2 在映射文件中进行配置
<select id="findByCid" resultType="com.sxt.pojo.Classes" parameterType="int">
select * from classes where cid = #{cid}
</select>
<select id="findAll" resultMap="myStudentMapper">
select * from student
</select>
5.3 修改主表映射文件中的查询方法
<resultMap id="myStudentMapper" type="com.sxt.pojo.Student">
<id property="sid" column="sid"></id>
<result property="name" column="name"></result>
<result property="age" column="age"></result>
<result property="sex" column="sex"></result>
<association property="classes"
javaType="com.sxt.pojo.Classes" select="com.sxt.mapper2.ClassesMapper2.findByCid"
column="classId">
</association>
</resultMap>
<select id="findAll" resultMap="myStudentMapper">
select * from student
</select>
5.4 测试查询方法
@Test
public void testFindAllStudent2(){
StudentMapper2 mapper2 = session.getMapper(StudentMapper2.class);
List<Student> list = mapper2.findAll();
list.forEach(System.out::println);
}
6. MyBatis延迟加载
==分解式查询==又分为两种加载方式:
- 立即加载:在查询主表时就执行所有的Sql语句。
- 延迟加载:又叫懒加载,首先执行主表的查询语句,使用从表数据时才触发从表的查询语句。
延迟加载在获取关联数据时速度较慢,但可以节约资源,即用即取。
6.1 开启延迟加载
-
设置所有的N+1查询都为延迟加载:
<settings> <setting name="lazyLoadingEnabled" value="true"/> </settings>
-
设置某个方法为延迟加载:
在 <association> 、 <collection> 中添加fetchType属性设置加载方式。lazy:延迟加载;eager:立即加载。
6.2 测试延迟加载
先查询出所有班级,学生集合延迟加载,只有用到学生集合的时候才去加载
@Test
public void testFindAllClasses2(){
ClassesMapper2 mapper2 = session.getMapper(ClassesMapper2.class);
List<Classes> list = mapper2.findAll();
list.forEach(System.out::println);
System.out.println("---------------------");
System.out.println(list.get(0).getStudents());
}
由于打印对象时会调用对象的 toString 方法, toString 方法默认会触发延迟加载的查询,所以我们无法测试出延迟加载的效果。
我们在配置文件设置lazyLoadTriggerMethods属性,该属性指定对象的什么方法触发延迟加载,设置为空字符串即可。
<!--任何方法都触发不了延迟加载-->
<settings>
<setting name="lazyLoadTriggerMethods" value=""/>
</settings>
一般情况下,一对多查询使用延迟加载,一对一查询使用立即加载。
九、MyBatis注解开发
1. 环境搭建
MyBatis可以使用注解替代映射文件。映射文件的作用就是定义Sql语句,可以在持久层接口上使用
@Select/@Delete/@Insert/@Update定义Sql语句,这样就不需要使用映射文件了。
-
创建maven工程,引入依赖
-
创建mybatis核心配置文件SqlMapConfig.xml
-
将log4j.properties文件放入resources中,让控制台打印SQL语句。
-
创建实体类
-
创建持久层接口,并在接口方法上定义Sql语句
public interface UserMapper { @Select("select * from user") List<User> findAll(); }
由于注解在方法上方,而方法中就有参数类型和返回值类型,所以使用注解开发不需要定义参数类型和返回值类型
-
在核心配置文件注册持久层接口,由于没有映射文件,所以只能采用注册接口或注册包的方法。
<mappers> <package name="com.itbaizhan.mapper"/> </mappers>
-
测试方法
@Test public void testFindAll(){ UserMapper mapper = session.getMapper(UserMapper.class); List<User> list = mapper.findAll(); list.forEach(System.out::println); }
2. 增删改查
接下来写一套基于MyBatis注解的增删改查方法:
public interface UserMapper {
@Select("select * from user")
List<User> findAll();
//主键回填:
@SelectKey(keyColumn = "id",keyProperty = "id",resultType = int.class,before = false,statement = "select last_insert_id();")
@Insert("insert into user(username,sex,address) values(#{username},#{sex},#{address})")
void add(User user);
@Update("update user set username = #{username},sex = #{sex},address = #{address} where id = #{id}")
void update(User user);
@Delete("delete from user where id = #{id}")
void delete(int id);
@Select("select * from user where username like #{username}")
List<User> findByUsernameLike(String username);
}
3. 动态sql
MyBatis注解开发中有两种方式构建动态Sql:
3.1 使用脚本标签
将Sql嵌套在 <script> 内即可使用动态Sql标签:
//根据任意条件查询
@Select("<script>" +
" select * from user\n" +
" <where>\n" +
" <if test=\"username!=null and username.length()!=0\">\n" +
" username like #{username}\n" +
" </if>\n" +
" <if test=\"sex!=null and sex.length()!=0\">\n" +
" and sex = #{sex}\n" +
" </if>\n" +
" <if test=\"address!=null and address.length()!=0\">\n" +
" and address = #{address}\n" +
" </if>\n" +
" </where> " +
"</script>")
List<User> findByCondition(User user);
3.2 在方法中构建动态Sql
在MyBatis中有 @SelectProvider 、 @UpdateProvider 、 @DeleteProvider 、@InsertProvider 注解。当使用这些注解时将不在注解中直接编写SQL,而是调用某个类的方法来生成SQL。
public class UserProvider {
//生成根据任意条件查询的sql语句
public String findByConditionSql(User user){
StringBuffer sb = new StringBuffer("select * from user where 1=1");
if(user.getUsername()!=null && user.getUsername().length()!=0){
sb.append(" and username like #{username}");
}
if(user.getSex()!=null && user.getSex().length()!=0){
sb.append(" and sex = #{sex}");
}
if(user.getAddress()!=null && user.getAddress().length()!=0){
sb.append(" and address = #{address}");
}
return sb.toString();
}
}
UserMapper.java接口:
@SelectProvider(type = UserProvider.class,method = "findByConditionSql")
List<User> findByCondition(User user);
4. 自定义映射关系
当POJO属性名与数据库列名不一致时,需要自定义实体类和结果集的映射关系,在MyBatis注解开发中,使用 @Results 定义并使用自定义映射,使用 @ResultMap 使用自定义映射,用法如下:
property:pojo属性名
column:数据库列名
//自定义映射
@Results(id = "userMapper",value = {
@Result(id = true,property = "id",column = "id"),
@Result(property = "username",column = "username1"),
@Result(property = "sex",column = "sex1"),
@Result(property = "address",column = "address1")
})
@Select("select * from user")
List<User> findAll();
//使用自定义映射
@ResultMap("userMapper")
@Select("select * from user where id = #{id}")
User findById(int id);
5. 二级缓存
MyBatis默认开启一级缓存,接下来我们学习如何在注解开发时使用二级缓存:
-
POJO类实现Serializable接口。
-
在MyBatis配置文件添加如下设置:
该配置默认是true,可以不加
<settings> <setting name="cacheEnabled" value="true"/> </settings>
-
在持久层接口上方加注解@CacheNamespace(blocking=true),该接口的所有方法都支持二级缓存。
-
测试二级缓存
@Test public void testCache() throws IOException { //【1】读取核心配置文件 InputStream is = Resources.getResourceAsStream("SqlMapConfig.xml"); //【2】创建SqlSessionFactoryBuilder对象 SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder(); //【3】SqlSessionFactoryBuilder对象获取SqlSessionFactory对象 SqlSessionFactory factory = builder.build(is); //【4】SqlSessionFactory对象获取SqlSession对象 SqlSession session1 = factory.openSession(); SqlSession session2 = factory.openSession(); UserMapper mapper1 = session1.getMapper(UserMapper.class); User user1 = mapper1.findById(1); System.out.println(user1); System.out.println(user1.hashCode()); session1.commit();//清空一级缓存,将数据存到二级缓存 UserMapper mapper2 = session2.getMapper(UserMapper.class); User user2 = mapper2.findById(1); System.out.println(user2); System.out.println(user2.hashCode()); }
开启二级缓存,直接从工厂的缓存中读取:
关闭二级缓存,重新查询:
6. 一对一关联查询
在MyBatis的注解开发中对于多表查询==只支持分解查询==,不支持连接查询。
查询全部学生,每个学生中包含一个班级对象,属于一对一关系
查询全部学生,根据学生的班级id就可以查询每个班级
-
创建实体类
public class Student { private int sid; private String name; private int age; private String sex; private Classes classes; // 省略getter/setter/toString } public class Classes { private int cid; private String className; private List<Student> students; // 省略getter/setter/toString }
-
创建分解后的查询方法
public interface StudentMapper { @Select("select * from student") List<Student> findAll(); } public interface ClassesMapper { // 根据id查询班级 @Select("select * from classes where cid = #{cid}") Classes findByCid(Integer cid); }
-
主表的查询配置自定义映射关系
在@Result内定义one属性表示该字段为对象类型
@Select("select * from student") //自定义映射关系 @Results(id = "studentMapper",value = { @Result(id = true,property = "sid",column = "sid"), @Result(property = "name",column = "name"), @Result(property = "age",column = "age"), @Result(property = "sex",column = "sex"), /** * property:属性名 * column:调用从表方法时传入的参数列 * one:一对一关联,表示该属性是一个对象 * select:调用从表方法 * fetchType:加载方式 */ @Result(property = "classes",column = "classId", one = @One(select = "com.sxt.mapper.ClassesMapper.findByCid", fetchType = FetchType.EAGER)) }) List<Student> findAll();
-
测试
@Test public void findAllStudent(){ StudentMapper mapper = session.getMapper(StudentMapper.class); List<Student> list = mapper.findAll(); list.forEach(System.out::println); }
7. 一对多关联查询
查询所有班级,每个班级中都包含一个学生集合,属性一对多关系
查询所有班级,根据每个班级的ID查询学生
-
创建分解后的查询方法
public interface ClassesMapper { // 查询所有班级 @Select("select * from classes") List<Classes> findAll(); } public interface StudentMapper { // 根据班级id查询学生 @Select("select * from student where classId = #{classId}") List<Student> findByClassId(int classId); }
-
主表的查询配置自定义映射关系
在@Result中many表示该属性是一个集合
//查询所有班级 @Select("select * from classes") @Results(id = "classMapper",value = { @Result(id = true,property = "cid",column = "cid"), @Result(property = "className",column = "className"), /** * many:表示该属性是一个集合 */ @Result(property = "students",column = "cid", many = @Many(select = "com.sxt.mapper.StudentMapper.findByClassId", fetchType = FetchType.LAZY)) }) List<Classes> findAll();
-
测试
@Test public void findAllClasses(){ ClassesMapper mapper = session.getMapper(ClassesMapper.class); List<Classes> list = mapper.findAll(); list.forEach(System.out::println); }
8. 注解开发与映射文件开发的对比
MyBatis中更推荐使用映射文件开发,Spring、SpringBoot更推荐注解方式。具体使用要视项目情况而定。它们的优点对比如下:
映射文件:
- 代码与Sql语句是解耦的,修改时只需修改配置文件,无需修改源码。
- Sql语句集中,利于快速了解和维护项目。
- 级联查询支持连接查询和分解查询两种方式,注解开发只支持分解查询。
注解:
- 配置简单,开发效率高。
- 类型安全,在编译期即可进行校验,不用等到运行时才发现错误。
十、PageHelper分页插件
开发过程中如果要进行分页查询,需要传入页数和每页条数。返回页面数据,总条数,总页数,当前页面,每页条数等数据。此时使用PageHelper插件可以快速帮助我们获取这些数据。
PageHelper是一款非常好用的开源免费的Mybatis第三方分页插件。使用该插件时,只要传入分页参数,即可自动生成页面对象。我们使用该插件分页查询所有用户:
-
引入依赖
<!-- PageHelper --> <dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper</artifactId> <version>5.3.0</version> </dependency>
-
Mybatis配置文件中配置PageHelper插件
<plugins> <plugin interceptor="com.github.pagehelper.PageInterceptor"> <!-- 设置数据库类型--> <property name="helperDialect" value="mysql"/> </plugin> </plugins>
-
使用PageHelper插件
PageHelper.startPage()
中的两个参数分别代表:页数(从1开始),每页条数//测试分页插件 @Test public void testFindPage(){ //1.设置分页参数 参数1:页数,从1开始。参数2:每页条数 PageHelper.startPage(1,3); //2.正常查询 List<User> list = mapper.findAll(); //3.封装查询结果,生成页面对象 PageInfo pageInfo = new PageInfo(list); //4.打印页面对象的属性 System.out.println("结果集:"+pageInfo.getList()); System.out.println("总条数:"+pageInfo.getTotal()); System.out.println("总页数:"+pageInfo.getPages()); System.out.println("当前页:"+pageInfo.getPageNum()); System.out.println("每页条数:"+pageInfo.getSize()); }
十一、MyBatis Generator
1. 工具引入
MyBatis Generator(MBG)是MyBatis官方提供的代码生成器。它可以根据数据库的表结构自动生成POJO类、持久层接口与映射文件,极大减少了代码的编写量,提高开发效率。
MBG可以作为项目引入使用,也可以作为Maven插件使用,其中作为Maven插件使用更加方便快捷。
-
准备数据库表
-
在pom文件中配置MBG插件
<build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <configuration> <source>8</source> <target>8</target> </configuration> </plugin> <plugin> <groupId>org.mybatis.generator</groupId> <artifactId>mybatis-generator-maven-plugin</artifactId> <version>1.3.7</version> <!--MBG配置--> <configuration> <!--MBG配置文件位置--> <configurationFile>src/main/resources/generatorConfig.xml</configurationFile> <!--运行显示详情--> <verbose>true</verbose> <!--允许覆盖文件--> <overwrite>true</overwrite> </configuration> </plugin> </plugins> </build>
-
编写MBG配置文件
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE generatorConfiguration PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN" "http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd"> <generatorConfiguration> <!-- JDBC的jar包位置,用于连接数据库 --> <classPathEntry location="C:\Developers\repository\mysql\mysql-connector-java\8.0.26\mysql-connector-java-8.0.26.jar"/> <context id="default" targetRuntime="MyBatis3"> <!-- 是否去除自动生成的注释--> <commentGenerator> <property name="suppressAllComments" value="true"/> </commentGenerator> <!--数据库连接参数--> <jdbcConnection driverClass="com.mysql.jdbc.Driver" connectionURL="jdbc:mysql://localhost:3306/mybatis" userId="root" password="123456"> </jdbcConnection> <!-- 类型处理器,在数据库类型和java类型之间的转换控制--> <javaTypeResolver> <property name="forceBigDecimals" value="false"/> </javaTypeResolver> <!-- targetProject:JAVA类路径 targetProject:生成的POJO类的包--> <javaModelGenerator targetProject="src/main/java" targetPackage="com.itbaizhan.pojo"> <!-- 是否生成子包 --> <property name="enableSubPackages" value="false"/> <!-- 设置是否在getter方法中,对String类型字段调用trim()方法 --> <property name="trimStrings" value="true"/> </javaModelGenerator> <!-- targetProject:配置文件路径 targetPackage:生成映射文件的位置 --> <sqlMapGenerator targetProject="src/main/resources" targetPackage="com.itbaizhan.mapper"> <!-- 是否生成子包 --> <property name="enableSubPackages" value="false"/> </sqlMapGenerator> <!-- targetPackage:JAVA类路径 targetProject:生成的持久层接口包 --> <javaClientGenerator targetProject="src/main/java" targetPackage="com.itbaizhan.mapper" type="XMLMAPPER"> <!-- 是否生成子包 --> <property name="enableSubPackages" value="false"/> </javaClientGenerator> <!-- 数据库表,表名不要和其他库中的表名一样 --> <table tableName="product"></table> </context> </generatorConfiguration>
-
运行插件,自动生成POJO,持久层接口,映射文件:
- Product.java:POJO类
- ProductMapper.java:持久层接口
- ProductMapper.xml:映射文件
- ProductExample.java:查询扩展类,该类可以构造复杂的查询条件。
- Criterion:代表一个字段。
- GeneratedCriteria:抽象类,生成查询条件的工具。
- Criteria:GeneratedCriteria的子类,生成查询条件的工具。
-
在配置文件中注册生成的映射文件
<mappers> <mapper class="com.sxt.mapper.ProductMapper"></mapper> </mappers>
2. 增删改方法
import com.sxt.mapper.ProductMapper;
import com.sxt.pojo.Product;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import java.io.IOException;
import java.io.InputStream;
public class TestMBG {
InputStream is = null;
SqlSession session = null;
ProductMapper mapper = null;
@Before
public void before() throws IOException {
//【1】读取核心配置文件
is = Resources.getResourceAsStream("SqlMapConfig.xml");
//【2】创建SqlSessionFactoryBuilder对象
SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
//【3】SqlSessionFactoryBuilder对象获取SqlSessionFactory对象
SqlSessionFactory factory = builder.build(is);
//【4】SqlSessionFactory对象获取SqlSession对象
session = factory.openSession();
mapper = session.getMapper(ProductMapper.class);
}
@After
public void after() throws IOException {
//释放资源
session.close();
is.close();
}
//新增
@Test
public void testAdd(){
Product product = new Product("百战python",15000.0);
mapper.insert(product);
session.commit();
}
//修改
@Test
public void testUpdate(){
Product product = new Product(4,"百战python",25000.0);
mapper.updateByPrimaryKey(product);
session.commit();
}
//删除
@Test
public void testDelete(){
mapper.deleteByPrimaryKey(4);
session.commit();
}
}
3. 查询方法
//根据ID查询
@Test
public void testFindById(){
Product product = mapper.selectByPrimaryKey(1);
System.out.println(product);
}
//查询所有
@Test
public void testFindByAll(){
//查询扩展对象,可以构建查询条件
ProductExample productExample = new ProductExample();
List<Product> products = mapper.selectByExample(productExample);
products.forEach(System.out::println);
}
//根据商品名查询
@Test
public void testFindByName(){
//查询扩展对象,可以构建查询条件
ProductExample productExample = new ProductExample();
//构建查询条件 内部类
ProductExample.Criteria criteria = productExample.createCriteria();
criteria.andProductnameLike("%尚学堂%");
//查询
List<Product> products = mapper.selectByExample(productExample);
products.forEach(System.out::println);
}
4. 复杂查询
//多条件and查询
@Test
public void testFindAnd(){
//查询扩展对象,可以构建查询条件
ProductExample productExample = new ProductExample();
//构建查询条件
ProductExample.Criteria criteria = productExample.createCriteria();
criteria.andProductnameLike("%百战%");
criteria.andPriceBetween(0.0,20000.0);
//查询
List<Product> products = mapper.selectByExample(productExample);
products.forEach(System.out::println);
}
//多条件or查询
@Test
public void testFindOr(){
//查询扩展对象,可以构建查询条件
ProductExample productExample = new ProductExample();
//构建查询条件
ProductExample.Criteria criteria = productExample.createCriteria();
criteria.andProductnameLike("%百战%");
ProductExample.Criteria criteria1 = productExample.createCriteria();
criteria1.andPriceBetween(0.0,10000.0);
productExample.or(criteria1);
//查询
List<Product> products = mapper.selectByExample(productExample);
products.forEach(System.out::println);
}
十二、动态代理
1. 代理模式简介
代理模式是23种设计模式之一。设计模式是前人总结的,在软件开发过程遇到常用问题的解决方案,常见的设计模式有单例模式、工厂模式、适配器模式等等。
代理模式的作用是在==不修改原对象的基础上增强该对象的方法==。比如官方购买苹果手机不赠送充电头,此时京东平台作为苹果的代理商,可以在代理销售苹果手机时赠送充电头。
代理模式分为静态代理、动态代理。静态代理会生成一个代理类,动态代理不会生成代理类,直接生成代理对象。
2. JDK动态代理
JDK动态代理是针对接口进行代理,所以我们要写被代理的接口和该接口的实现类。
-
被代理接口Apple.java
package com.sxt.dynamic; /** * 被代理接口 */ public interface Apple { String sell(double price);//卖产品 void repair();//维修 }
-
苹果接口实现类AppleImpl.java
package com.sxt.dynamic; /** * 被代理接口的实现类 */ public class AppleImpl implements Apple { @Override public String sell(double price) { System.out.println("产品卖了"+price+"元"); return "iphone13"; } @Override public void repair() { System.out.println("苹果售后维修"); } }
-
代理方式类,定义被代理类的增强方式
package com.sxt.dynamic; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; /** * 代理方式类,定义被代理类的增强方式 * 该类实现InvocationHandler接口,重写invoke方法,定义方法的增强方式 */ public class ShoppingProxy implements InvocationHandler { private Apple apple;//被代理对象 public ShoppingProxy(Apple apple) { this.apple = apple; } /** * 定义原方法的增强方式 * @param proxy 被代理对象 * @param method 被代理对象调用的方法 * @param args 被代理对象调用方法时传入的参数 * @return 方法的返回值 * @throws Throwable */ @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { String name = method.getName();//被代理对象执行的方法名 if("sell".equals(name)){ double price = (double) args[0] * 0.9;//增强参数 Object result = method.invoke(apple, price);//执行方法 return result + "和充电头";//增强返回值 }else if("repair".equals(name)){ System.out.println("专属客服为您服务!");//增强方法流程 return method.invoke(apple,args); }else{ return method.invoke(apple,args); //什么都不增强 } } }
-
测试类
针对接口,调用newProxyInstance传入的是被代理接口
package com.sxt.dynamic; import java.lang.reflect.Proxy; public class Test { public static void main(String[] args) { //被代理对象 Apple apple = new AppleImpl(); //代理方式对象 ShoppingProxy shoppingProxy = new ShoppingProxy(apple); //生成代理对象 Apple appleJD = (Apple) Proxy.newProxyInstance( apple.getClass().getClassLoader(),//代理类的加载器 apple.getClass().getInterfaces(),//被代理接口 shoppingProxy //代理方式对象 ); //执行增强后的方法 String sell = appleJD.sell(6000); System.out.println(sell); appleJD.repair(); } }
3. CGLib动态代理
CGLib动态代理简化了JDK动态代理的写法,JDK是针对接口代理,而CGLib是针对类代理。
<!-- 引入cglib依赖 -->
<dependencies>
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>3.3.0</version>
</dependency>
</dependencies>
被代理类Apple.java
package com.sxt.cglibdynamic;
/**
* 被代理类
*/
public class Apple {
public String sell(double price) {
System.out.println("产品卖了"+price+"元");
return "iphone13";
}
public void repair() {
System.out.println("苹果售后维修");
}
}
代理方式类ShoppingProxy,实现MethodInterceptor接口,重写intercept方法
package com.sxt.cglibdynamic;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;
/**
* 代理方式类,实现MethodInterceptor接口,重写intercept方法
*/
public class ShoppingProxy implements MethodInterceptor {
private Apple apple;//被代理对象
public ShoppingProxy(Apple apple){
this.apple = apple;
}
/**
* 定义原方法的增强方式
* @param o 被代理对象
* @param method 被代理对象调用的方法
* @param objects 被代理对象调用方法时传入的参数
* @param methodProxy 底层生成的代理类的应用
* @return 方法的返回值
* @throws Throwable
*/
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
String name = method.getName();
if("sell".equals(name)){
double price = (double)objects[0]*0.8;
Object result = method.invoke(apple, price);
return result+"和数据线";
}else if("repair".equals(name)){
System.out.println("专属客服为您服务!");
return method.invoke(apple,objects);
}else{
return method.invoke(apple,objects);
}
}
}
测试类Test
package com.sxt.cglibdynamic;
import net.sf.cglib.proxy.Enhancer;
public class Test {
public static void main(String[] args) {
//被代理对象
Apple apple = new Apple();
//代理方式
ShoppingProxy shoppingProxy = new ShoppingProxy(apple);
//生成代理对象
Apple appleTB = (Apple) Enhancer.create(Apple.class, shoppingProxy);
//执行增强后的方法
String sell = appleTB.sell(9000);
System.out.println(sell);
appleTB.repair();
}
}