JavaWeb
28448字约95分钟
2025-08-31
启动命令
快速运行
java -jar 路径写入首字母后可以按 tab 键进行补全
指定环境运行
java -jar 路径 --spring.profile.active=环境名其他一些指令
##### windows指令
netstat -ano # 所有的端口占用情况
netstat -ano | findstr "80" # 查询包含80字符的端口情况
tasklist | findstr "9480" # 查询指定pid的进程信息
tasklist # 查询所有进程
taskkill -f -pid 9480 # 杀死指定pid的进程
taskkill -f -t -im "进程名称" # 根据进程名称杀死进程环境定义方法
application.yml 文件中
# 设置启用的环境
spring:
profiles:
active: test
server:
servlet:
context-path: /
---
# 开发环境
spring:
profiles: dev
server:
port: 80
---
# 生产环境
spring:
profiles: pro
server:
port: 81
---
# 测试环境
spring:
profiles: test
server:
port: 82修改端口
java -jar 路径 --server.port=88Maven-依赖管理
修改默认配置文件
1.进入到maven安装目录,找到conf文件夹,修改settings.xml配置文件
2.找到localrepository标签,去除注释,修改路径为仓库路径
配置阿里云私服
添加settings.xml中的mirror标签
<mirror>
<id>alimaven</id>
<name>aliyun maven</name>
<url>http://maven.aliyun.com/nexus/content/groups/public/</url>
<mirrorOf>central</mirrorOf>
</mirror>只可配置一个<mirror>(另一个要注释!) ,不然两个可能发生冲突,导致jar包无法下载
配置环境变量
添加计算机的环境变量,在系统变量处添加变量MAVEN_HOME,设置值为maven的安装路径
在Path变量中添加值为%MAVEN_HOME%\bin
打开命令行输入命令 mvn -v ,出现版本号表示成功
POM配置文件
指的是项目对象模型,用来描述当前的maven项目
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<!-- POM模型版本 -->
<modelVersion>4.0.0</modelVersion>
<!-- 当前项目坐标 -->
<groupId>com.itheima</groupId>
<artifactId>maven_project1</artifactId>
<version>1.0-SNAPSHOT</version>
<!-- 打包方式 -->
<packaging>jar</packaging>
</project>pom文件详解:
- <project> :pom文件的根标签,表示当前maven项目
- <modelVersion> :声明项目描述遵循哪一个POM模型版本
- 虽然模型本身的版本很少改变,但它仍然是必不可少的。目前POM模型版本是4.0.0
- 坐标 :<groupId>、<artifactId>、<version>
- 定位项目在本地仓库中的位置,由以上三个标签组成一个坐标
- <packaging> :maven项目的打包方式,通常设置为jar或war(默认值:jar)坐标
Maven坐标主要组成
- groupId:定义当前Maven项目隶属组织名称(通常是域名反写,例如:com.itheima)
- artifactId:定义当前Maven项目名称(通常是模块名称,例如 order-service、goods-service)
- version:定义当前项目版本号
排除依赖
<dependencies>
<dependency>
<groupId>xyz.jdynb</groupId>
<artifactId>maven-project01</artifactId>
<version>1.0-SNAPSHOT</version>
<exclusions>
<exclusion>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>设置 exclusion 可以排除不需要的依赖
依赖范围
scope
compile: 默认,主程序、测试、打包
test: 测试环境
provided:主程序、测试
runtime:测试、打包
<groupId>xyz.jdynb</groupId>
<artifactId>maven-project01</artifactId>
<version>1.0-SNAPSHOT</version>
<scope>test</scope>HTTP协议
请求协议
在HTTP1.1版本中,浏览器访问服务器的几种方式:
| 请求方式 | 请求说明 |
| :------: | :----------------------------------------------------------- |
| **GET** | 获取资源。<br/>向特定的资源发出请求。例:http://www.baidu.com/s?wd=itheima |
| **POST** | 传输实体主体。<br/>向指定资源提交数据进行处理请求(例:上传文件),数据被包含在请求体中。 |
| OPTIONS | 返回服务器针对特定资源所支持的HTTP请求方式。<br/>因为并不是所有的服务器都支持规定的方法,为了安全有些服务器可能会禁止掉一些方法,例如:DELETE、PUT等。那么OPTIONS就是用来询问服务器支持的方法。 |
| HEAD | 获得报文首部。<br/>HEAD方法类似GET方法,但是不同的是HEAD方法不要求返回数据。通常用于确认URI的有效性及资源更新时间等。 |
| PUT | 传输文件。<br/>PUT方法用来传输文件。类似FTP协议,文件内容包含在请求报文的实体中,然后请求保存到URL指定的服务器位置。 |
| DELETE | 删除文件。<br/>请求服务器删除Request-URI所标识的资源 |
| TRACE | 追踪路径。<br/>回显服务器收到的请求,主要用于测试或诊断 |
| CONNECT | 要求用隧道协议连接代理。<br/>HTTP/1.1协议中预留给能够将连接改为管道方式的代理服务器 |Host: 表示请求的主机名
User-Agent: 浏览器版本。 例如:Chrome浏览器的标识类似Mozilla/5.0 ...Chrome/79 ,IE浏览器的标识类似Mozilla/5.0 (Windows NT ...)like Gecko
Accept:表示浏览器能接收的资源类型,如text/*,image/或者/*表示所有;
Accept-Language:表示浏览器偏好的语言,服务器可以据此返回不同语言的网页;
Accept-Encoding:表示浏览器可以支持的压缩类型,例如gzip, deflate等。
Content-Type:请求主体的数据类型
Content-Length:数据主体的大小(单位:字节)
Cache-Control:指示客户端应如何缓存,例如max-age=300表示可以最多缓存300秒 ;
响应码
| 状态码分类 | 说明 |
|---|---|
| 1xx | 响应中 --- 临时状态码。表示请求已经接受,告诉客户端应该继续请求或者如果已经完成则忽略 |
| 2xx | 成功 --- 表示请求已经被成功接收,处理已完成 |
| 3xx | 重定向 --- 重定向到其它地方,让客户端再发起一个请求以完成整个处理 |
| 4xx | 客户端错误 --- 处理发生错误,责任在客户端,如:客户端的请求一个不存在的资源,客户端未被授权,禁止访问等 |
| 5xx | 服务器端错误 --- 处理发生错误,责任在服务端,如:服务端抛出异常,路由出错,HTTP版本不支持等 |
Tomcat
官方网站:https://tomcat.apache.org/
启动
进入bin目录
1.双击startup.bat(windows)
2.安装服务
service.bat install双击tomcat.ext运行
配置
解决控制台中文乱码问题:修改conf/logging.properties
java.util.logging.ConsoleHandler.level = FINE
java.util.logging.ConsoleHandler.encodeing = GBK # 将UTF-8改为GBK修改tomcat端口:/conf/server.xml
<Connector port="xxx" .../>MyBatis
中文网地址: MyBatis中文网
导入依赖
<!-- MyBatis依赖 -->
<!-- <dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.10</version>
</dependency> -->
<!-- mysql驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<!-- spring整合 -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>3.0.3</version>
</dependency>
<!-- 用于记录日志 -->
<!-- 添加slf4j日志api -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</dependency>
<!-- 添加logback-classic依赖 -->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
</dependency>配置文件
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>
<property name="username" value="root"/>
<property name="password" value="jdy200255"/>
</properties>
<settings>
<setting name="cacheEnabled" value="true"/>
</settings>-->
<!-- 类型别名 -->
<typeAliases>
<!-- user 代表全限定类名 -->
<typeAlias alias="user" type="com.example.pojo.User"/>
<!-- 指定包类名,该包下所有实体类将被自动扫描 -->
<package name="com.example.pojo"/>
</typeAliases>
<!-- 类型处理 -->
<!--<typeHandlers>
<typeHandler jdbcType="VARCHAR" javaType="String" handler="com.example.handler.ExampleTypeHandler"/>
</typeHandlers>-->
</configuration>application.yml
mybatis:
# 包别名,该包下的实体类将被扫描,用于返回类型
type-aliases-package: com.example.pojo
configuration:
# 开启缓存
cache-enabled: true
# log前缀
log-prefix: jdy
# 日志实现,未指定将自动查找
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
# 自动生成主键
use-generated-keys: false
# 配置自动映射 NONE, PARTIAL, FULL
auto-mapping-behavior: partial
# 如果自动映射位置列,是否显示警告日志信息 NONE, WARNING, FAILING
auto-mapping-unknown-column-behavior: none
# 语句执行超时时间
default-statement-timeout: 10
# 结果集获取数量设置一个建议值
# default-fetch-size: 100
# 开启驼峰自动映射
map-underscore-to-camel-case: true
# 本地缓存机制
local-cache-scope: session
# 当没有为参数指定jdbc类型,空值默认为哪种类型
jdbc-type-for-null: other
# 指定那些对象的方法触发一些延迟加载
# lazy-load-trigger-methods: equals,clone
# enum默认的类型处理
default-enum-type-handler: org.apache.ibatis.type.EnumTypeHandler
# 当返回结果集为空时,如果设置为true时,将默认返回一个空实例对象
return-instance-for-empty-row: false
# 设置typeHandlers包路径
type-handlers-package: com.example.handler
spring:
datasource:
username: root
password: jdy200255
url: jdbc:mysql://localhost:3306/db01?serverTimezone=UTC
driver-class-name: com.mysql.cj.jdbc.DriverTypeAlias
给java类型设置一个缩写名字
<!-- 类型别名 -->
<typeAliases>
<!-- user 代表全限定类名 -->
<typeAlias alias="user" type="com.example.pojo.User"/>
<!-- 指定包类名,该包下所有实体类将被自动扫描 -->
<package name="com.example.pojo"/>
</typeAliases>application.yml中
# 包别名,该包下的实体类将被扫描,用于返回类型
type-aliases-package: com.example.pojo在实体类中使用注解声明别名
// 注解方式生成别名
@Alias("author")
public class Author {}TypeHandler
新建类用于处理jdbc类型与java类型进行映射
// 映射数据库中的varchar类型
// 映射为java中的String类型
// 当在resultMap中使用时,如果没有指定jdbcType,则将不会调用此处方法映射,而会使用默认类型处理器
// 只需在此处声明includeNullJdbcType = true,则未使用jdbcType也将进行处理
@MappedJdbcTypes(JdbcType.VARCHAR, includeNullJdbcType = true)
public class ExampleTypeHandler extends BaseTypeHandler<String> {
@Override
public void setNonNullParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType) throws SQLException {
System.out.println("setNonNullParameter");
ps.setString(i, parameter);
}
@Override
public String getNullableResult(ResultSet rs, String columnName) throws SQLException {
System.out.println("getNullableResult");
return rs.getString(columnName);
}
@Override
public String getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
System.out.println("getNullableResult");
return rs.getString(columnIndex);
}
@Override
public String getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
System.out.println("getNullableResult");
return cs.getString(columnIndex);
}
}mybatis.config.xml中配置
<!-- 类型处理 -->
<typeHandlers>
<typeHandler jdbcType="VARCHAR" javaType="String" handler="com.example.handler.ExampleTypeHandler"/>
</typeHandlers>application.yaml中
# 设置typeHandlers包路径
type-handlers-package: com.example.handler在mapper.xml中的resultMap单独指定
<resultMap id="userMap" type="user">
<result column="username" property="username" typeHandler="com.example.handler.ExampleTypeHandler"/>
</resultMap>ObjectFactory
mybatis创建实例对象时,都会使用这个工厂对象创建
public class ExampleObjectFactory extends DefaultObjectFactory {
@Override
public <T> T create(Class<T> type) {
System.out.println("create Object");
return super.create(type);
}
@Override
public <T> T create(Class<T> type, List<Class<?>> constructorArgTypes, List<Object> constructorArgs) {
System.out.println("create Object");
return super.create(type, constructorArgTypes, constructorArgs);
}
@Override
public void setProperties(Properties properties) {
System.out.println("props: " + properties);
super.setProperties(properties);
}
@Override
public <T> boolean isCollection(Class<T> type) {
return super.isCollection(type);
}
}mybatis-config.xml
<objectFactory type="com.example.factory.ExampleObjectFactory">
<property name="someProp" value="100"/>
</objectFactory>Mappers
完整用例
<?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">
<!-- namespace 命名空间 -->
<mapper namespace="com.itheima.mapper.BrandMapper">
<!-- 抽取公共数据
引入sql片段<include refid=""/>
-->
<sql id="brand_column">
</sql>
<!--
id: 唯一标识
type:映射的类型,支持别名
-->
<resultMap id="brandResultMap" type="brand">
<!--
id 完成主键字段的映射
column 表的别名
property 类的字段名
result 完成一般字段的映射
-->
<result column="brand_name" property="brandName"/>
<result column="company_name" property="companyName"/>
</resultMap>
<select id="selectAll" resultMap="brandResultMap">
select *
from tb_brand;
</select>
<select id="selectById" resultMap="brandResultMap">
select *
from tb_brand
where id = #{id};
</select>
<!--<select id="selectByCondition" resultMap="brandResultMap">
select *
from tb_brand
where status = #{status}
and company_name like #{companyName}
and brand_name like #{brandName}
</select>-->
<select id="selectByCondition" resultMap="brandResultMap">
select *
from tb_brand
<where>
<if test="status != null">
and status = #{status}
</if>
<if test="companyName != null and companyName != '' ">
and company_name like #{companyName}
</if>
<if test="brandName != null and brandName != '' ">
and brand_name like #{brandName}
</if>
</where>
</select>
<!--<select id="selectByConditionSingle" resultMap="brandResultMap">
select *
from tb_brand
where
<choose>
<when test="status != null">
status = #{status}
</when>
<when test="companyName != null and companyName != '' ">
company_name like #{companyName}
</when>
<when test="brandName != null and brandName != '' ">
brand_name like #{brandName}
</when>
<otherwise>
1 = 1
</otherwise>
</choose>
</select>-->
<select id="selectByConditionSingle" resultMap="brandResultMap">
select *
from tb_brand
<where>
<choose>
<when test="status != null">
status = #{status}
</when>
<when test="companyName != null and companyName != '' ">
company_name like #{companyName}
</when>
<when test="brandName != null and brandName != '' ">
brand_name like #{brandName}
</when>
</choose>
</where>
</select>
<!-- 插入后返回主键值 -->
<insert id="add" useGeneratedKeys="true" keyProperty="id">
insert into tb_brand (brand_name, company_name, ordered, description, status)
VALUES (#{brandName}, #{companyName}, #{ordered}, #{description}, #{status});
</insert>
<!--<update id="update">
update tb_brand
set brand_name = #{brandName},
company_name = #{companyName},
ordered = #{ordered},
description = #{description},
status = #{status}
where id = #{id};
</update>-->
<update id="update">
update tb_brand
<set>
<if test="brandName != null and brandName != '' ">
brand_name = #{brandName},
</if>
<if test="companyName != null and companyName != '' ">
company_name = #{companyName},
</if>
<if test="ordered != null">
ordered = #{ordered},
</if>
<if test="description != null and description != '' ">
description = #{description},
</if>
<if test="status != null">
status = #{status}
</if>
</set>
where id = #{id};
</update>
<delete id="deleteById">
delete
from tb_brand
where id = #{id};
</delete>
<delete id="deleteByIds">
delete
from tb_brand
where id
in
<foreach collection="ids" item="id" separator="," open="(" close=")">
#{id}
</foreach>
;
</delete>
</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="com.example.mybatis.mapper.EmpMapper">
<!-- 结果映射 id:唯一标识,type:实体类类名,指定返回类型 -->
<resultMap id="empMap" type="emp">
<!--
设置对应关系 column:数据库中的列,property: 实体类属性
javaType:实体属性类型,jdbcType:数据库中的字段类型
-->
<result column="username" property="username" javaType="String" jdbcType="VARCHAR"/>
<!--
collection: 收集数据,select:指定需要调用的sql id,
调用selectSex查询数据,并将gender作为selectSex作为id参数进行传入
-->
<collection property="gender" javaType="string" column="id" select="selectSex"/>
</resultMap>
<!-- autoMapping: 自动映射(column和property相同时) -->
<resultMap id="orderMap" type="order" autoMapping="true">
<!--<id column="id" property="id"/>
<result column="uid" property="uid"/>
<result column="total" property="total"/>-->
<result column="create_at" property="createAt"/>
<!--<result column="username" property="user.username"/>-->
<!-- 关联数据,将关联表查询数据,关联给user属性(适用于1对1或多对1) -->
<association property="user" javaType="user" autoMapping="true">
<id column="uid" property="id"/>
</association>
</resultMap>
<resultMap id="userMap" type="user" autoMapping="true">
<!-- 收集关联表的数据,将查询到的集合指定ofType的类型,并赋值给(使用与1对多或多对多) -->
<collection property="orders" ofType="order" autoMapping="true">
<id column="oid" property="id"/>
<result column="create_at" property="createAt"/>
</collection>
</resultMap>
<!-- 启用继承extend -->
<resultMap id="userRoleMap" type="user" extends="userMap" autoMapping="true">
<collection property="roles" ofType="role" autoMapping="true">
</collection>
</resultMap>
<!-- parameterType 入参类型,指定resultMap 结果映射 -->
<select parameterType="int" id="selectById" resultMap="empMap">
select *
from tb_emp
where id = #{id}
</select>
<!-- resultType:返回结果类型 -->
<select id="selectSex" parameterType="int" resultType="string">
select name
from tb_sex
where id = #{id}
</select>
<select id="getOrders" resultMap="orderMap">
select *
from tb_order o,
tb_user u
where o.id = u.id
</select>
<select id="getUsers" resultMap="userMap">
select *, o.id oid
from tb_user
left join tb_order o on tb_user.id = o.uid
</select>
<select id="getUsersAndRole" resultMap="userRoleMap">
/*select *
from tb_user u
left join tb_order o on u.id = o.uid
left join tb_user_role ur on ur.user_id = u.id
inner join tb_role r on r.id = ur.role_id*/
select * from tb_user u, tb_user_role ur, tb_role r where u.id = ur.user_id and ur.role_id = r.id
</select>
</mapper>对应xxxmapper.java
public interface BrandMapper {
List<Brand> selectAll();
// 映射
@Results(
{
@Result(column = "", property = "")
}
)
Brand selectById(int id);
/* List<Brand> selectByCondition(@Param("status") int status,
@Param("companyName") String companyName,
@Param("brandName") String brandName);*/
/* List<Brand> selectByCondition(Brand brand);*/
// Map参数
List<Brand> selectByCondition(Map<String, String> map);
// 对象参数
List<Brand> selectByConditionSingle(Brand brand);
// 返回自增的主键值
@Options(keyProperty = "id", usegeneratedKeys = true)
int add(Brand brand);
int update(Brand brand);
void deleteById(int id);
// 数组参数
void deleteByIds(@Param("ids") int[] ids);
}javaAPI
@Test
public void testSelectAll() throws IOException {
InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession sqlSession = sqlSessionFactory.openSession();
BrandMapper brandMapper = sqlSession.getMapper(BrandMapper.class);
List<Brand> brands = brandMapper.selectAll();
System.out.println("brands = " + brands);
sqlSession.close();
}
@Test
public void testSelectById() throws IOException {
InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession sqlSession = sqlSessionFactory.openSession();
BrandMapper brandMapper = sqlSession.getMapper(BrandMapper.class);
Brand brand = brandMapper.selectById(1);
System.out.println("brand = " + brand);
sqlSession.close();
}
@Test
public void testSelectByCondition() throws IOException {
int status = 1;
String companyName = "华为";
String brandName = "华为";
companyName = "%" + companyName + "%";
/*Brand brand = new Brand();
brand.setStatus(status);
brand.setCompanyName(companyName);
brand.setBrandName(brandName);*/
Map<String, String> map = new HashMap<>();
// map.put("status", String.valueOf(status));
map.put("companyName", companyName);
// map.put("brandName", brandName);
InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession sqlSession = sqlSessionFactory.openSession();
BrandMapper brandMapper = sqlSession.getMapper(BrandMapper.class);
List<Brand> brands = brandMapper.selectByCondition(map);
System.out.println("brand = " + brands);
sqlSession.close();
}
@Test
public void testSelectByConditionSingle() throws IOException {
int status = 1;
String companyName = "华为";
String brandName = "华为";
companyName = "%" + companyName + "%";
Brand brand = new Brand();
// brand.setStatus(status);
// brand.setCompanyName(companyName);
// brand.setBrandName(brandName);
/* Map<String, String> map = new HashMap<>();
// map.put("status", String.valueOf(status));
map.put("companyName", companyName);
// map.put("brandName", brandName);*/
InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession sqlSession = sqlSessionFactory.openSession();
BrandMapper brandMapper = sqlSession.getMapper(BrandMapper.class);
List<Brand> brands = brandMapper.selectByConditionSingle(brand);
System.out.println("brand = " + brands);
sqlSession.close();
}
@Test
public void testAdd() throws IOException {
int status = 1;
String companyName = "波导手机";
String brandName = "波导";
Brand brand = new Brand();
brand.setStatus(status);
brand.setCompanyName(companyName);
brand.setBrandName(brandName);
brand.setOrdered(10);
brand.setDescription("手机中的战斗机");
InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession sqlSession = sqlSessionFactory.openSession(true);
BrandMapper brandMapper = sqlSession.getMapper(BrandMapper.class);
brandMapper.add(brand);
Integer id = brand.getId();
System.out.println("id = " + id);
// 提交事务
// sqlSession.commit();
sqlSession.close();
}
@Test
public void testUpdate() throws IOException {
int status = 1;
String companyName = "波导手机";
String brandName = "波导";
Brand brand = new Brand();
brand.setId(5);
brand.setStatus(0);
// brand.setCompanyName(companyName);
// brand.setBrandName(brandName);
// brand.setOrdered(200);
// brand.setDescription("波导手机,手机中的战斗机");
InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession sqlSession = sqlSessionFactory.openSession(true);
BrandMapper brandMapper = sqlSession.getMapper(BrandMapper.class);
int row = brandMapper.update(brand);
Integer id = brand.getId();
System.out.println("id = " + id + ", row = " + row);
// 提交事务
// sqlSession.commit();
sqlSession.close();
}
@Test
public void testDeleteById() throws IOException {
InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession sqlSession = sqlSessionFactory.openSession(true);
BrandMapper brandMapper = sqlSession.getMapper(BrandMapper.class);
brandMapper.deleteById(6);
sqlSession.close();
}
@Test
public void testDeleteByIds() throws IOException {
InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession sqlSession = sqlSessionFactory.openSession(true);
BrandMapper brandMapper = sqlSession.getMapper(BrandMapper.class);
brandMapper.deleteByIds(new int[]{5, 7, 8});
sqlSession.close();
}这些配置会告诉 MyBatis 去哪里找映射文件,剩下的细节就应该是每个 SQL 映射文件了.
mybatis-config.xml
<!-- 使用相对于类路径的资源引用 -->
<mappers>
<mapper resource="org/mybatis/builder/AuthorMapper.xml"/>
<mapper resource="org/mybatis/builder/BlogMapper.xml"/>
<mapper resource="org/mybatis/builder/PostMapper.xml"/>
</mappers>
<!-- 使用完全限定资源定位符(URL) -->
<mappers>
<mapper url="file:///var/mappers/AuthorMapper.xml"/>
<mapper url="file:///var/mappers/BlogMapper.xml"/>
<mapper url="file:///var/mappers/PostMapper.xml"/>
</mappers>
<!-- 使用映射器接口实现类的完全限定类名 -->
<mappers>
<mapper class="org.mybatis.builder.AuthorMapper"/>
<mapper class="org.mybatis.builder.BlogMapper"/>
<mapper class="org.mybatis.builder.PostMapper"/>
</mappers>
<!-- 将包内的映射器接口实现全部注册为映射器 -->
<mappers>
<package name="org.mybatis.builder"/>
</mappers>application.yml
mapper-locations: classpath:mapper/*xmlXML映射
SQL 映射文件只有很少的几个顶级元素(按照应被定义的顺序列出):
cache– 该命名空间的缓存配置。cache-ref– 引用其它命名空间的缓存配置。resultMap– 描述如何从数据库结果集中加载对象,是最复杂也是最强大的元素。parameterMap– 老式风格的参数映射。此元素已被废弃,并可能在将来被移除!请使用行内参数映射。文档中不会介绍此元素。sql– 可被其它语句引用的可重用语句块。insert– 映射插入语句。update– 映射更新语句。delete– 映射删除语句。select– 映射查询语句。
select
<select id="selectPerson" parameterType="int" resultType="hashmap">
SELECT * FROM PERSON WHERE ID = #{id}
</select>这个语句名为 selectPerson,接受一个 int(或 Integer)类型的参数,并返回一个 HashMap 类型的对象,其中的键是列名,值便是结果行中的对应值。
注意参数符号:
#{id}这就告诉 MyBatis 创建一个预处理语句(PreparedStatement)参数,在 JDBC 中,这样的一个参数在 SQL 中会由一个“?”来标识,并被传递到一个新的预处理语句中,就像这样:
// 近似的 JDBC 代码,非 MyBatis 代码...
String selectPerson = "SELECT * FROM PERSON WHERE ID=?";
PreparedStatement ps = conn.prepareStatement(selectPerson);
ps.setInt(1,id);当然,使用 JDBC 就意味着使用更多的代码,以便提取结果并将它们映射到对象实例中,而这就是 MyBatis 的拿手好戏。参数和结果映射的详细细节会分别在后面单独的小节中说明。
select 元素允许你配置很多属性来配置每条语句的行为细节。
<select
id="selectPerson"
parameterType="int"
parameterMap="deprecated"
resultType="hashmap"
resultMap="personResultMap"
flushCache="false"
useCache="true"
timeout="10"
fetchSize="256"
statementType="PREPARED"
resultSetType="FORWARD_ONLY">Select 元素的属性
| 属性 | 描述 |
|---|---|
id | 在命名空间中唯一的标识符,可以被用来引用这条语句。 |
parameterType | 将会传入这条语句的参数的类全限定名或别名。这个属性是可选的,因为 MyBatis 可以通过类型处理器(TypeHandler)推断出具体传入语句的参数,默认值为未设置(unset)。 |
| parameterMap | 用于引用外部 parameterMap 的属性,目前已被废弃。请使用行内参数映射和 parameterType 属性。 |
resultType | 期望从这条语句中返回结果的类全限定名或别名。 注意,如果返回的是集合,那应该设置为集合包含的类型,而不是集合本身的类型。 resultType 和 resultMap 之间只能同时使用一个。 |
resultMap | 对外部 resultMap 的命名引用。结果映射是 MyBatis 最强大的特性,如果你对其理解透彻,许多复杂的映射问题都能迎刃而解。 resultType 和 resultMap 之间只能同时使用一个。 |
flushCache | 将其设置为 true 后,只要语句被调用,都会导致本地缓存和二级缓存被清空,默认值:false。 |
useCache | 将其设置为 true 后,将会导致本条语句的结果被二级缓存缓存起来,默认值:对 select 元素为 true。 |
timeout | 这个设置是在抛出异常之前,驱动程序等待数据库返回请求结果的秒数。默认值为未设置(unset)(依赖数据库驱动)。 |
fetchSize | 这是一个给驱动的建议值,尝试让驱动程序每次批量返回的结果行数等于这个设置值。 默认值为未设置(unset)(依赖驱动)。 |
statementType | 可选 STATEMENT,PREPARED 或 CALLABLE。这会让 MyBatis 分别使用 Statement,PreparedStatement 或 CallableStatement,默认值:PREPARED。 |
resultSetType | FORWARD_ONLY,SCROLL_SENSITIVE, SCROLL_INSENSITIVE 或 DEFAULT(等价于 unset) 中的一个,默认值为 unset (依赖数据库驱动)。 |
databaseId | 如果配置了数据库厂商标识(databaseIdProvider),MyBatis 会加载所有不带 databaseId 或匹配当前 databaseId 的语句;如果带和不带的语句都有,则不带的会被忽略。 |
resultOrdered | 这个设置仅针对嵌套结果 select 语句:如果为 true,将会假设包含了嵌套结果集或是分组,当返回一个主结果行时,就不会产生对前面结果集的引用。 这就使得在获取嵌套结果集的时候不至于内存不够用。默认值:false。 |
resultSets | 这个设置仅适用于多结果集的情况。它将列出语句执行后返回的结果集并赋予每个结果集一个名称,多个名称之间以逗号分隔。 |
insert, update 和 delete
数据变更语句 insert,update 和 delete 的实现非常接近:
<insert
id="insertAuthor"
parameterType="domain.blog.Author"
flushCache="true"
statementType="PREPARED"
keyProperty=""
keyColumn=""
useGeneratedKeys=""
timeout="20">
<update
id="updateAuthor"
parameterType="domain.blog.Author"
flushCache="true"
statementType="PREPARED"
timeout="20">
<delete
id="deleteAuthor"
parameterType="domain.blog.Author"
flushCache="true"
statementType="PREPARED"
timeout="20">| 属性 | 描述 |
|---|---|
id | 在命名空间中唯一的标识符,可以被用来引用这条语句。 |
parameterType | 将会传入这条语句的参数的类全限定名或别名。这个属性是可选的,因为 MyBatis 可以通过类型处理器(TypeHandler)推断出具体传入语句的参数,默认值为未设置(unset)。 |
parameterMap | 用于引用外部 parameterMap 的属性,目前已被废弃。请使用行内参数映射和 parameterType 属性。 |
flushCache | 将其设置为 true 后,只要语句被调用,都会导致本地缓存和二级缓存被清空,默认值:(对 insert、update 和 delete 语句)true。 |
timeout | 这个设置是在抛出异常之前,驱动程序等待数据库返回请求结果的秒数。默认值为未设置(unset)(依赖数据库驱动)。 |
statementType | 可选 STATEMENT,PREPARED 或 CALLABLE。这会让 MyBatis 分别使用 Statement,PreparedStatement 或 CallableStatement,默认值:PREPARED。 |
useGeneratedKeys | (仅适用于 insert 和 update)这会令 MyBatis 使用 JDBC 的 getGeneratedKeys 方法来取出由数据库内部生成的主键(比如:像 MySQL 和 SQL Server 这样的关系型数据库管理系统的自动递增字段),默认值:false。 |
keyProperty | (仅适用于 insert 和 update)指定能够唯一识别对象的属性,MyBatis 会使用 getGeneratedKeys 的返回值或 insert 语句的 selectKey 子元素设置它的值,默认值:未设置(unset)。如果生成列不止一个,可以用逗号分隔多个属性名称。 |
keyColumn | (仅适用于 insert 和 update)设置生成键值在表中的列名,在某些数据库(像 PostgreSQL)中,当主键列不是表中的第一列的时候,是必须设置的。如果生成列不止一个,可以用逗号分隔多个属性名称。 |
databaseId | 如果配置了数据库厂商标识(databaseIdProvider),MyBatis 会加载所有不带 databaseId 或匹配当前 databaseId 的语句;如果带和不带的语句都有,则不带的会被忽略。 |
下面是 insert,update 和 delete 语句的示例:
<insert id="insertAuthor">
insert into Author (id,username,password,email,bio)
values (#{id},#{username},#{password},#{email},#{bio})
</insert>
<update id="updateAuthor">
update Author set
username = #{username},
password = #{password},
email = #{email},
bio = #{bio}
where id = #{id}
</update>
<delete id="deleteAuthor">
delete from Author where id = #{id}
</delete>如前所述,插入语句的配置规则更加丰富,在插入语句里面有一些额外的属性和子元素用来处理主键的生成,并且提供了多种生成方式。
首先,如果你的数据库支持自动生成主键的字段(比如 MySQL 和 SQL Server),那么你可以设置 useGeneratedKeys=”true”,然后再把 keyProperty 设置为目标属性就 OK 了。例如,如果上面的 Author 表已经在 id 列上使用了自动生成,那么语句可以修改为:
<insert id="insertAuthor" useGeneratedKeys="true"
keyProperty="id">
insert into Author (username,password,email,bio)
values (#{username},#{password},#{email},#{bio})
</insert>如果你的数据库还支持多行插入, 你也可以传入一个 Author 数组或集合,并返回自动生成的主键。
<insert id="insertAuthor" useGeneratedKeys="true"
keyProperty="id">
insert into Author (username, password, email, bio) values
<foreach item="item" collection="list" separator=",">
(#{item.username}, #{item.password}, #{item.email}, #{item.bio})
</foreach>
</insert>对于不支持自动生成主键列的数据库和可能不支持自动生成主键的 JDBC 驱动,MyBatis 有另外一种方法来生成主键。
这里有一个简单(也很傻)的示例,它可以生成一个随机 ID(不建议实际使用,这里只是为了展示 MyBatis 处理问题的灵活性和宽容度):
<insert id="insertAuthor">
<selectKey keyProperty="id" resultType="int" order="BEFORE">
select CAST(RANDOM()*1000000 as INTEGER) a from SYSIBM.SYSDUMMY1
</selectKey>
insert into Author
(id, username, password, email,bio, favourite_section)
values
(#{id}, #{username}, #{password}, #{email}, #{bio}, #{favouriteSection,jdbcType=VARCHAR})
</insert>在上面的示例中,首先会运行 selectKey 元素中的语句,并设置 Author 的 id,然后才会调用插入语句。这样就实现了数据库自动生成主键类似的行为,同时保持了 Java 代码的简洁。
selectKey 元素描述如下:
<selectKey
keyProperty="id"
resultType="int"
order="BEFORE"
statementType="PREPARED">selectKey 元素的属性
| 属性 | 描述 |
|---|---|
keyProperty | selectKey 语句结果应该被设置到的目标属性。如果生成列不止一个,可以用逗号分隔多个属性名称。 |
keyColumn | 返回结果集中生成列属性的列名。如果生成列不止一个,可以用逗号分隔多个属性名称。 |
resultType | 结果的类型。通常 MyBatis 可以推断出来,但是为了更加准确,写上也不会有什么问题。MyBatis 允许将任何简单类型用作主键的类型,包括字符串。如果生成列不止一个,则可以使用包含期望属性的 Object 或 Map。 |
order | 可以设置为 BEFORE 或 AFTER。如果设置为 BEFORE,那么它首先会生成主键,设置 keyProperty 再执行插入语句。如果设置为 AFTER,那么先执行插入语句,然后是 selectKey 中的语句 - 这和 Oracle 数据库的行为相似,在插入语句内部可能有嵌入索引调用。 |
statementType | 和前面一样,MyBatis 支持 STATEMENT,PREPARED 和 CALLABLE 类型的映射语句,分别代表 Statement, PreparedStatement 和 CallableStatement 类型。 |
sql
这个元素可以用来定义可重用的 SQL 代码片段,以便在其它语句中使用。 参数可以静态地(在加载的时候)确定下来,并且可以在不同的 include 元素中定义不同的参数值。比如:
<sql id="userColumns"> ${alias}.id,${alias}.username,${alias}.password </sql>这个 SQL 片段可以在其它语句中使用,例如:
<select id="selectUsers" resultType="map">
select
<include refid="userColumns"><property name="alias" value="t1"/></include>,
<include refid="userColumns"><property name="alias" value="t2"/></include>
from some_table t1
cross join some_table t2
</select>也可以在 include 元素的 refid 属性或内部语句中使用属性值,例如:
<sql id="sometable">
${prefix}Table
</sql>
<sql id="someinclude">
from
<include refid="${include_target}"/>
</sql>
<select id="select" resultType="map">
select
field1, field2, field3
<include refid="someinclude">
<property name="prefix" value="Some"/>
<property name="include_target" value="sometable"/>
</include>
</select>parameter
之前见到的所有语句都使用了简单的参数形式。但实际上,参数是 MyBatis 非常强大的元素。对于大多数简单的使用场景,你都不需要使用复杂的参数,比如:
<select id="selectUsers" resultType="User">
select id, username, password
from users
where id = #{id}
</select>上面的这个示例说明了一个非常简单的命名参数映射。鉴于参数类型(parameterType)会被自动设置为 int,这个参数可以随意命名。原始类型或简单数据类型(比如 Integer 和 String)因为没有其它属性,会用它们的值来作为参数。 然而,如果传入一个复杂的对象,行为就会有点不一样了。比如:
<insert id="insertUser" parameterType="User">
insert into users (id, username, password)
values (#{id}, #{username}, #{password})
</insert>如果 User 类型的参数对象传递到了语句中,会查找 id、username 和 password 属性,然后将它们的值传入预处理语句的参数中。
对传递语句参数来说,这种方式真是干脆利落。不过参数映射的功能远不止于此。
首先,和 MyBatis 的其它部分一样,参数也可以指定一个特殊的数据类型。
#{property,javaType=int,jdbcType=NUMERIC}和 MyBatis 的其它部分一样,几乎总是可以根据参数对象的类型确定 javaType,除非该对象是一个 HashMap。这个时候,你需要显式指定 javaType 来确保正确的类型处理器(TypeHandler)被使用。
提示 JDBC 要求,如果一个列允许使用 null 值,并且会使用值为 null 的参数,就必须要指定 JDBC 类型(jdbcType)。阅读 PreparedStatement.setNull()的 JavaDoc 来获取更多信息。
要更进一步地自定义类型处理方式,可以指定一个特殊的类型处理器类(或别名),比如:
#{age,javaType=int,jdbcType=NUMERIC,typeHandler=MyTypeHandler}参数的配置好像越来越繁琐了,但实际上,很少需要如此繁琐的配置。
对于数值类型,还可以设置 numericScale 指定小数点后保留的位数。
#{height,javaType=double,jdbcType=NUMERIC,numericScale=2}最后,mode 属性允许你指定 IN,OUT 或 INOUT 参数。如果参数的 mode 为 OUT 或 INOUT,将会修改参数对象的属性值,以便作为输出参数返回。 如果 mode 为 OUT(或 INOUT),而且 jdbcType 为 CURSOR(也就是 Oracle 的 REFCURSOR),你必须指定一个 resultMap 引用来将结果集 ResultMap 映射到参数的类型上。要注意这里的 javaType 属性是可选的,如果留空并且 jdbcType 是 CURSOR,它会被自动地被设为 ResultMap。
#{department, mode=OUT, jdbcType=CURSOR, javaType=ResultSet, resultMap=departmentResultMap}MyBatis 也支持很多高级的数据类型,比如结构体(structs),但是当使用 out 参数时,你必须显式设置类型的名称。比如(再次提示,在实际中要像这样不能换行):
#{middleInitial, mode=OUT, jdbcType=STRUCT, jdbcTypeName=MY_TYPE, resultMap=departmentResultMap}尽管上面这些选项很强大,但大多时候,你只须简单指定属性名,顶多要为可能为空的列指定 jdbcType,其他的事情交给 MyBatis 自己去推断就行了。
#{firstName}
#{middleInitial,jdbcType=VARCHAR}
#{lastName}字符串替换
默认情况下,使用 #{} 参数语法时,MyBatis 会创建 PreparedStatement 参数占位符,并通过占位符安全地设置参数(就像使用 ? 一样)。 这样做更安全,更迅速,通常也是首选做法,不过有时你就是想直接在 SQL 语句中直接插入一个不转义的字符串。 比如 ORDER BY 子句,这时候你可以:
ORDER BY ${columnName}这样,MyBatis 就不会修改或转义该字符串了。
当 SQL 语句中的元数据(如表名或列名)是动态生成的时候,字符串替换将会非常有用。 举个例子,如果你想 select 一个表任意一列的数据时,不需要这样写:
@Select("select * from user where id = #{id}")
User findById(@Param("id") long id);
@Select("select * from user where name = #{name}")
User findByName(@Param("name") String name);
@Select("select * from user where email = #{email}")
User findByEmail(@Param("email") String email);
// 其它的 "findByXxx" 方法而是可以只写这样一个方法:
@Select("select * from user where ${column} = #{value}")
User findByColumn(@Param("column") String column, @Param("value") String value);其中 ${column} 会被直接替换,而 #{value} 会使用 ? 预处理。 这样,就能完成同样的任务:
User userOfId1 = userMapper.findByColumn("id", 1L);
User userOfNameKid = userMapper.findByColumn("name", "kid");
User userOfEmail = userMapper.findByColumn("email", "noone@nowhere.com");这种方式也同样适用于替换表名的情况。
提示 用这种方式接受用户的输入,并用作语句参数是不安全的,会导致潜在的 SQL 注入攻击。因此,要么不允许用户输入这些字段,要么自行转义并检验这些参数。
ResultMapper
resultMap 元素是 MyBatis 中最重要最强大的元素。它可以让你从 90% 的 JDBC ResultSets 数据提取代码中解放出来,并在一些情形下允许你进行一些 JDBC 不支持的操作。实际上,在为一些比如连接的复杂语句编写映射代码的时候,一份 resultMap 能够代替实现同等功能的数千行代码。ResultMap 的设计思想是,对简单的语句做到零配置,对于复杂一点的语句,只需要描述语句之间的关系就行了。
之前你已经见过简单映射语句的示例,它们没有显式指定 resultMap。比如:
<select id="selectUsers" resultType="map">
select id, username, hashedPassword
from some_table
where id = #{id}
</select>上述语句只是简单地将所有的列映射到 HashMap 的键上,这由 resultType 属性指定。虽然在大部分情况下都够用,但是 HashMap 并不是一个很好的领域模型。你的程序更可能会使用 JavaBean 或 POJO(Plain Old Java Objects,普通老式 Java 对象)作为领域模型。MyBatis 对两者都提供了支持。看看下面这个 JavaBean:
package com.someapp.model;
public class User {
private int id;
private String username;
private String hashedPassword;
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 getHashedPassword() {
return hashedPassword;
}
public void setHashedPassword(String hashedPassword) {
this.hashedPassword = hashedPassword;
}
}基于 JavaBean 的规范,上面这个类有 3 个属性:id,username 和 hashedPassword。这些属性会对应到 select 语句中的列名。
这样的一个 JavaBean 可以被映射到 ResultSet,就像映射到 HashMap 一样简单。
<select id="selectUsers" resultType="com.someapp.model.User">
select id, username, hashedPassword
from some_table
where id = #{id}
</select>类型别名是你的好帮手。使用它们,你就可以不用输入类的全限定名了。
比如:
<!-- mybatis-config.xml 中 -->
<typeAlias type="com.someapp.model.User" alias="User"/>
<!-- SQL 映射 XML 中 -->
<select id="selectUsers" resultType="User">
select id, username, hashedPassword
from some_table
where id = #{id}
</select>在这些情况下,MyBatis 会在幕后自动创建一个 ResultMap,再根据属性名来映射列到 JavaBean 的属性上。如果列名和属性名不能匹配上,可以在 SELECT 语句中设置列别名(这是一个基本的 SQL 特性)来完成匹配。比如:
<select id="selectUsers" resultType="User">
select
user_id as "id",
user_name as "userName",
hashed_password as "hashedPassword"
from some_table
where id = #{id}
</select>在学习了上面的知识后,你会发现上面的例子没有一个需要显式配置 ResultMap,这就是 ResultMap 的优秀之处——你完全可以不用显式地配置它们。 虽然上面的例子不用显式配置 ResultMap。 但为了讲解,我们来看看如果在刚刚的示例中,显式使用外部的 resultMap 会怎样,这也是解决列名不匹配的另外一种方式。
<resultMap id="userResultMap" type="User">
<id property="id" column="user_id" />
<result property="username" column="user_name"/>
<result property="password" column="hashed_password"/>
</resultMap>然后在引用它的语句中设置 resultMap 属性就行了(注意我们去掉了 resultType 属性)。比如:
<select id="selectUsers" resultMap="userResultMap">
select user_id, user_name, hashed_password
from some_table
where id = #{id}
</select>如果这个世界总是这么简单就好了。
高级结果映射
MyBatis 创建时的一个思想是:数据库不可能永远是你所想或所需的那个样子。 我们希望每个数据库都具备良好的第三范式或 BCNF 范式,可惜它们并不都是那样。 如果能有一种数据库映射模式,完美适配所有的应用程序,那就太好了,但可惜也没有。 而 ResultMap 就是 MyBatis 对这个问题的答案。
<!-- 非常复杂的语句 -->
<select id="selectBlogDetails" resultMap="detailedBlogResultMap">
select
B.id as blog_id,
B.title as blog_title,
B.author_id as blog_author_id,
A.id as author_id,
A.username as author_username,
A.password as author_password,
A.email as author_email,
A.bio as author_bio,
A.favourite_section as author_favourite_section,
P.id as post_id,
P.blog_id as post_blog_id,
P.author_id as post_author_id,
P.created_on as post_created_on,
P.section as post_section,
P.subject as post_subject,
P.draft as draft,
P.body as post_body,
C.id as comment_id,
C.post_id as comment_post_id,
C.name as comment_name,
C.comment as comment_text,
T.id as tag_id,
T.name as tag_name
from Blog B
left outer join Author A on B.author_id = A.id
left outer join Post P on B.id = P.blog_id
left outer join Comment C on P.id = C.post_id
left outer join Post_Tag PT on PT.post_id = P.id
left outer join Tag T on PT.tag_id = T.id
where B.id = #{id}
</select>你可能想把它映射到一个智能的对象模型,这个对象表示了一篇博客,它由某位作者所写,有很多的博文,每篇博文有零或多条的评论和标签。 我们先来看看下面这个完整的例子,它是一个非常复杂的结果映射(假设作者,博客,博文,评论和标签都是类型别名)。 不用紧张,我们会一步一步地来说明。虽然它看起来令人望而生畏,但其实非常简单。
<!-- 非常复杂的结果映射 -->
<resultMap id="detailedBlogResultMap" type="Blog">
<constructor>
<idArg column="blog_id" javaType="int"/>
</constructor>
<result property="title" column="blog_title"/>
<association property="author" javaType="Author">
<id property="id" column="author_id"/>
<result property="username" column="author_username"/>
<result property="password" column="author_password"/>
<result property="email" column="author_email"/>
<result property="bio" column="author_bio"/>
<result property="favouriteSection" column="author_favourite_section"/>
</association>
<collection property="posts" ofType="Post">
<id property="id" column="post_id"/>
<result property="subject" column="post_subject"/>
<association property="author" javaType="Author"/>
<collection property="comments" ofType="Comment">
<id property="id" column="comment_id"/>
</collection>
<collection property="tags" ofType="Tag" >
<id property="id" column="tag_id"/>
</collection>
<discriminator javaType="int" column="draft">
<case value="1" resultType="DraftPost"/>
</discriminator>
</collection>
</resultMap>resultMap 元素有很多子元素和一个值得深入探讨的结构。 下面是resultMap 元素的概念视图。
结果映射(resultMap)
constructor - 用于在实例化类时,注入结果到构造方法中
idArg- ID 参数;标记出作为 ID 的结果可以帮助提高整体性能arg- 将被注入到构造方法的一个普通结果
id– 一个 ID 结果;标记出作为 ID 的结果可以帮助提高整体性能result– 注入到字段或 JavaBean 属性的普通结果association – 一个复杂类型的关联;许多结果将包装成这种类型
- 嵌套结果映射 – 关联可以是
resultMap元素,或是对其它结果映射的引用
- 嵌套结果映射 – 关联可以是
collection – 一个复杂类型的集合
- 嵌套结果映射 – 集合可以是
resultMap元素,或是对其它结果映射的引用
- 嵌套结果映射 – 集合可以是
discriminator – 使用结果值来决定使用哪个resultMap
case
– 基于某些值的结果映射
- 嵌套结果映射 –
case也是一个结果映射,因此具有相同的结构和元素;或者引用其它的结果映射
- 嵌套结果映射 –
ResultMap 的属性列表
| 属性 | 描述 |
|---|---|
id | 当前命名空间中的一个唯一标识,用于标识一个结果映射。 |
type | 类的完全限定名, 或者一个类型别名(关于内置的类型别名,可以参考上面的表格)。 |
autoMapping | 如果设置这个属性,MyBatis 将会为本结果映射开启或者关闭自动映射。 这个属性会覆盖全局的属性 autoMappingBehavior。默认值:未设置(unset)。 |
最佳实践 最好逐步建立结果映射。单元测试可以在这个过程中起到很大帮助。 如果你尝试一次性创建像上面示例那么巨大的结果映射,不仅容易出错,难度也会直线上升。 所以,从最简单的形态开始,逐步迭代。而且别忘了单元测试! 有时候,框架的行为像是一个黑盒子(无论是否开源)。因此,为了确保实现的行为与你的期望相一致,最好编写单元测试。 并且单元测试在提交 bug 时也能起到很大的作用。
id & result
<id property="id" column="post_id"/>
<result property="subject" column="post_subject"/>这些元素是结果映射的基础。id 和 result 元素都将一个列的值映射到一个简单数据类型(String, int, double, Date 等)的属性或字段。
这两者之间的唯一不同是,id 元素对应的属性会被标记为对象的标识符,在比较对象实例时使用。 这样可以提高整体的性能,尤其是进行缓存和嵌套结果映射(也就是连接映射)的时候。
两个元素都有一些属性:
| 属性 | 描述 |
|---|---|
property | 映射到列结果的字段或属性。如果 JavaBean 有这个名字的属性(property),会先使用该属性。否则 MyBatis 将会寻找给定名称的字段(field)。 无论是哪一种情形,你都可以使用常见的点式分隔形式进行复杂属性导航。 比如,你可以这样映射一些简单的东西:“username”,或者映射到一些复杂的东西上:“address.street.number”。 |
column | 数据库中的列名,或者是列的别名。一般情况下,这和传递给 resultSet.getString(columnName) 方法的参数一样。 |
javaType | 一个 Java 类的全限定名,或一个类型别名(关于内置的类型别名,可以参考上面的表格)。 如果你映射到一个 JavaBean,MyBatis 通常可以推断类型。然而,如果你映射到的是 HashMap,那么你应该明确地指定 javaType 来保证行为与期望的相一致。 |
jdbcType | JDBC 类型,所支持的 JDBC 类型参见这个表格之后的“支持的 JDBC 类型”。 只需要在可能执行插入、更新和删除的且允许空值的列上指定 JDBC 类型。这是 JDBC 的要求而非 MyBatis 的要求。如果你直接面向 JDBC 编程,你需要对可以为空值的列指定这个类型。 |
typeHandler | 我们在前面讨论过默认的类型处理器。使用这个属性,你可以覆盖默认的类型处理器。 这个属性值是一个类型处理器实现类的全限定名,或者是类型别名。 |
支持的 JDBC 类型
为了以后可能的使用场景,MyBatis 通过内置的 jdbcType 枚举类型支持下面的 JDBC 类型。
BIT | FLOAT | CHAR | TIMESTAMP | OTHER | UNDEFINED |
|---|---|---|---|---|---|
TINYINT | REAL | VARCHAR | BINARY | BLOB | NVARCHAR |
SMALLINT | DOUBLE | LONGVARCHAR | VARBINARY | CLOB | NCHAR |
INTEGER | NUMERIC | DATE | LONGVARBINARY | BOOLEAN | NCLOB |
BIGINT | DECIMAL | TIME | NULL | CURSOR | ARRAY |
constructor
通过修改对象属性的方式,可以满足大多数的数据传输对象(Data Transfer Object, DTO)以及绝大部分领域模型的要求。但有些情况下你想使用不可变类。 一般来说,很少改变或基本不变的包含引用或数据的表,很适合使用不可变类。 构造方法注入允许你在初始化时为类设置属性的值,而不用暴露出公有方法。MyBatis 也支持私有属性和私有 JavaBean 属性来完成注入,但有一些人更青睐于通过构造方法进行注入。 constructor 元素就是为此而生的。
看看下面这个构造方法:
public class User {
//...
public User(Integer id, String username, int age) {
//...
}
//...
}为了将结果注入构造方法,MyBatis 需要通过某种方式定位相应的构造方法。 在下面的例子中,MyBatis 搜索一个声明了三个形参的的构造方法,参数类型以 java.lang.Integer, java.lang.String 和 int 的顺序给出。
<constructor>
<idArg column="id" javaType="int"/>
<arg column="username" javaType="String"/>
<arg column="age" javaType="_int"/>
</constructor>当你在处理一个带有多个形参的构造方法时,很容易搞乱 arg 元素的顺序。 从版本 3.4.3 开始,可以在指定参数名称的前提下,以任意顺序编写 arg 元素。 为了通过名称来引用构造方法参数,你可以添加 @Param 注解,或者使用 '-parameters' 编译选项并启用 useActualParamName 选项(默认开启)来编译项目。下面是一个等价的例子,尽管函数签名中第二和第三个形参的顺序与 constructor 元素中参数声明的顺序不匹配。
<constructor>
<idArg column="id" javaType="int" name="id" />
<arg column="age" javaType="_int" name="age" />
<arg column="username" javaType="String" name="username" />
</constructor>如果存在名称和类型相同的属性,那么可以省略 javaType 。
剩余的属性和规则和普通的 id 和 result 元素是一样的。
| 属性 | 描述 |
|---|---|
column | 数据库中的列名,或者是列的别名。一般情况下,这和传递给 resultSet.getString(columnName) 方法的参数一样。 |
javaType | 一个 Java 类的完全限定名,或一个类型别名(关于内置的类型别名,可以参考上面的表格)。 如果你映射到一个 JavaBean,MyBatis 通常可以推断类型。然而,如果你映射到的是 HashMap,那么你应该明确地指定 javaType 来保证行为与期望的相一致。 |
jdbcType | JDBC 类型,所支持的 JDBC 类型参见这个表格之前的“支持的 JDBC 类型”。 只需要在可能执行插入、更新和删除的且允许空值的列上指定 JDBC 类型。这是 JDBC 的要求而非 MyBatis 的要求。如果你直接面向 JDBC 编程,你需要对可能存在空值的列指定这个类型。 |
typeHandler | 我们在前面讨论过默认的类型处理器。使用这个属性,你可以覆盖默认的类型处理器。 这个属性值是一个类型处理器实现类的完全限定名,或者是类型别名。 |
select | 用于加载复杂类型属性的映射语句的 ID,它会从 column 属性中指定的列检索数据,作为参数传递给此 select 语句。具体请参考关联元素。 |
resultMap | 结果映射的 ID,可以将嵌套的结果集映射到一个合适的对象树中。 它可以作为使用额外 select 语句的替代方案。它可以将多表连接操作的结果映射成一个单一的 ResultSet。这样的 ResultSet 将会将包含重复或部分数据重复的结果集。为了将结果集正确地映射到嵌套的对象树中,MyBatis 允许你 “串联”结果映射,以便解决嵌套结果集的问题。想了解更多内容,请参考下面的关联元素。 |
name | 构造方法形参的名字。从 3.4.3 版本开始,通过指定具体的参数名,你可以以任意顺序写入 arg 元素。参看上面的解释。 |
association
<association property="author" column="blog_author_id" javaType="Author">
<id property="id" column="author_id"/>
<result property="username" column="author_username"/>
</association>关联(association)元素处理“有一个”类型的关系。 比如,在我们的示例中,一个博客有一个用户。关联结果映射和其它类型的映射工作方式差不多。 你需要指定目标属性名以及属性的javaType(很多时候 MyBatis 可以自己推断出来),在必要的情况下你还可以设置 JDBC 类型,如果你想覆盖获取结果值的过程,还可以设置类型处理器。
关联的不同之处是,你需要告诉 MyBatis 如何加载关联。MyBatis 有两种不同的方式加载关联:
- 嵌套 Select 查询:通过执行另外一个 SQL 映射语句来加载期望的复杂类型。
- 嵌套结果映射:使用嵌套的结果映射来处理连接结果的重复子集。
首先,先让我们来看看这个元素的属性。你将会发现,和普通的结果映射相比,它只在 select 和 resultMap 属性上有所不同。
| 属性 | 描述 |
|---|---|
property | 映射到列结果的字段或属性。如果用来匹配的 JavaBean 存在给定名字的属性,那么它将会被使用。否则 MyBatis 将会寻找给定名称的字段。 无论是哪一种情形,你都可以使用通常的点式分隔形式进行复杂属性导航。 比如,你可以这样映射一些简单的东西:“username”,或者映射到一些复杂的东西上:“address.street.number”。 |
javaType | 一个 Java 类的完全限定名,或一个类型别名(关于内置的类型别名,可以参考上面的表格)。 如果你映射到一个 JavaBean,MyBatis 通常可以推断类型。然而,如果你映射到的是 HashMap,那么你应该明确地指定 javaType 来保证行为与期望的相一致。 |
jdbcType | JDBC 类型,所支持的 JDBC 类型参见这个表格之前的“支持的 JDBC 类型”。 只需要在可能执行插入、更新和删除的且允许空值的列上指定 JDBC 类型。这是 JDBC 的要求而非 MyBatis 的要求。如果你直接面向 JDBC 编程,你需要对可能存在空值的列指定这个类型。 |
typeHandler | 我们在前面讨论过默认的类型处理器。使用这个属性,你可以覆盖默认的类型处理器。 这个属性值是一个类型处理器实现类的完全限定名,或者是类型别名。 |
关联的嵌套 Select 查询
| 属性 | 描述 |
|---|---|
column | 数据库中的列名,或者是列的别名。一般情况下,这和传递给 resultSet.getString(columnName) 方法的参数一样。 注意:在使用复合主键的时候,你可以使用 column="{prop1=col1,prop2=col2}" 这样的语法来指定多个传递给嵌套 Select 查询语句的列名。这会使得 prop1 和 prop2 作为参数对象,被设置为对应嵌套 Select 语句的参数。 |
select | 用于加载复杂类型属性的映射语句的 ID,它会从 column 属性指定的列中检索数据,作为参数传递给目标 select 语句。 具体请参考下面的例子。注意:在使用复合主键的时候,你可以使用 column="{prop1=col1,prop2=col2}" 这样的语法来指定多个传递给嵌套 Select 查询语句的列名。这会使得 prop1 和 prop2 作为参数对象,被设置为对应嵌套 Select 语句的参数。 |
fetchType | 可选的。有效值为 lazy 和 eager。 指定属性后,将在映射中忽略全局配置参数 lazyLoadingEnabled,使用属性的值。 |
示例:
<resultMap id="blogResult" type="Blog">
<association property="author" column="author_id" javaType="Author" select="selectAuthor"/>
</resultMap>
<select id="selectBlog" resultMap="blogResult">
SELECT * FROM BLOG WHERE ID = #{id}
</select>
<select id="selectAuthor" resultType="Author">
SELECT * FROM AUTHOR WHERE ID = #{id}
</select>就是这么简单。我们有两个 select 查询语句:一个用来加载博客(Blog),另外一个用来加载作者(Author),而且博客的结果映射描述了应该使用 selectAuthor 语句加载它的 author 属性。
其它所有的属性将会被自动加载,只要它们的列名和属性名相匹配。
这种方式虽然很简单,但在大型数据集或大型数据表上表现不佳。这个问题被称为“N+1 查询问题”。 概括地讲,N+1 查询问题是这样子的:
- 你执行了一个单独的 SQL 语句来获取结果的一个列表(就是“+1”)。
- 对列表返回的每条记录,你执行一个 select 查询语句来为每条记录加载详细信息(就是“N”)。
这个问题会导致成百上千的 SQL 语句被执行。有时候,我们不希望产生这样的后果。
好消息是,MyBatis 能够对这样的查询进行延迟加载,因此可以将大量语句同时运行的开销分散开来。 然而,如果你加载记录列表之后立刻就遍历列表以获取嵌套的数据,就会触发所有的延迟加载查询,性能可能会变得很糟糕。
所以还有另外一种方法。
关联的嵌套结果映射
| 属性 | 描述 |
|---|---|
resultMap | 结果映射的 ID,可以将此关联的嵌套结果集映射到一个合适的对象树中。 它可以作为使用额外 select 语句的替代方案。它可以将多表连接操作的结果映射成一个单一的 ResultSet。这样的 ResultSet 有部分数据是重复的。 为了将结果集正确地映射到嵌套的对象树中, MyBatis 允许你“串联”结果映射,以便解决嵌套结果集的问题。使用嵌套结果映射的一个例子在表格以后。 |
columnPrefix | 当连接多个表时,你可能会不得不使用列别名来避免在 ResultSet 中产生重复的列名。指定 columnPrefix 列名前缀允许你将带有这些前缀的列映射到一个外部的结果映射中。 详细说明请参考后面的例子。 |
notNullColumn | 默认情况下,在至少一个被映射到属性的列不为空时,子对象才会被创建。 你可以在这个属性上指定非空的列来改变默认行为,指定后,Mybatis 将只在这些列非空时才创建一个子对象。可以使用逗号分隔来指定多个列。默认值:未设置(unset)。 |
autoMapping | 如果设置这个属性,MyBatis 将会为本结果映射开启或者关闭自动映射。 这个属性会覆盖全局的属性 autoMappingBehavior。注意,本属性对外部的结果映射无效,所以不能搭配 select 或 resultMap 元素使用。默认值:未设置(unset)。 |
之前,你已经看到了一个非常复杂的嵌套关联的例子。 下面的例子则是一个非常简单的例子,用于演示嵌套结果映射如何工作。 现在我们将博客表和作者表连接在一起,而不是执行一个独立的查询语句,就像这样:
<select id="selectBlog" resultMap="blogResult">
select
B.id as blog_id,
B.title as blog_title,
B.author_id as blog_author_id,
A.id as author_id,
A.username as author_username,
A.password as author_password,
A.email as author_email,
A.bio as author_bio
from Blog B left outer join Author A on B.author_id = A.id
where B.id = #{id}
</select>注意查询中的连接,以及为确保结果能够拥有唯一且清晰的名字,我们设置的别名。 这使得进行映射非常简单。现在我们可以映射这个结果:
<resultMap id="blogResult" type="Blog">
<id property="id" column="blog_id" />
<result property="title" column="blog_title"/>
<association property="author" column="blog_author_id" javaType="Author" resultMap="authorResult"/>
</resultMap>
<resultMap id="authorResult" type="Author">
<id property="id" column="author_id"/>
<result property="username" column="author_username"/>
<result property="password" column="author_password"/>
<result property="email" column="author_email"/>
<result property="bio" column="author_bio"/>
</resultMap>在上面的例子中,你可以看到,博客(Blog)作者(author)的关联元素委托名为 “authorResult” 的结果映射来加载作者对象的实例。
非常重要: id 元素在嵌套结果映射中扮演着非常重要的角色。你应该总是指定一个或多个可以唯一标识结果的属性。 虽然,即使不指定这个属性,MyBatis 仍然可以工作,但是会产生严重的性能问题。 只需要指定可以唯一标识结果的最少属性。显然,你可以选择主键(复合主键也可以)。
现在,上面的示例使用了外部的结果映射元素来映射关联。这使得 Author 的结果映射可以被重用。 然而,如果你不打算重用它,或者你更喜欢将你所有的结果映射放在一个具有描述性的结果映射元素中。 你可以直接将结果映射作为子元素嵌套在内。这里给出使用这种方式的等效例子:
<resultMap id="blogResult" type="Blog">
<id property="id" column="blog_id" />
<result property="title" column="blog_title"/>
<association property="author" javaType="Author">
<id property="id" column="author_id"/>
<result property="username" column="author_username"/>
<result property="password" column="author_password"/>
<result property="email" column="author_email"/>
<result property="bio" column="author_bio"/>
</association>
</resultMap>那如果博客(blog)有一个共同作者(co-author)该怎么办?select 语句看起来会是这样的:
<select id="selectBlog" resultMap="blogResult">
select
B.id as blog_id,
B.title as blog_title,
A.id as author_id,
A.username as author_username,
A.password as author_password,
A.email as author_email,
A.bio as author_bio,
CA.id as co_author_id,
CA.username as co_author_username,
CA.password as co_author_password,
CA.email as co_author_email,
CA.bio as co_author_bio
from Blog B
left outer join Author A on B.author_id = A.id
left outer join Author CA on B.co_author_id = CA.id
where B.id = #{id}
</select>回忆一下,Author 的结果映射定义如下:
<resultMap id="authorResult" type="Author">
<id property="id" column="author_id"/>
<result property="username" column="author_username"/>
<result property="password" column="author_password"/>
<result property="email" column="author_email"/>
<result property="bio" column="author_bio"/>
</resultMap>由于结果中的列名与结果映射中的列名不同。你需要指定 columnPrefix 以便重复使用该结果映射来映射 co-author 的结果。
<resultMap id="blogResult" type="Blog">
<id property="id" column="blog_id" />
<result property="title" column="blog_title"/>
<association property="author"
resultMap="authorResult" />
<association property="coAuthor"
resultMap="authorResult"
columnPrefix="co_" />
</resultMap>关联的多结果集(ResultSet)
| 属性 | 描述 |
|---|---|
column | 当使用多个结果集时,该属性指定结果集中用于与 foreignColumn 匹配的列(多个列名以逗号隔开),以识别关系中的父类型与子类型。 |
foreignColumn | 指定外键对应的列名,指定的列将与父类型中 column 的给出的列进行匹配。 |
resultSet | 指定用于加载复杂类型的结果集名字。 |
从版本 3.2.3 开始,MyBatis 提供了另一种解决 N+1 查询问题的方法。
某些数据库允许存储过程返回多个结果集,或一次性执行多个语句,每个语句返回一个结果集。 我们可以利用这个特性,在不使用连接的情况下,只访问数据库一次就能获得相关数据。
在例子中,存储过程执行下面的查询并返回两个结果集。第一个结果集会返回博客(Blog)的结果,第二个则返回作者(Author)的结果。
SELECT * FROM BLOG WHERE ID = #{id}
SELECT * FROM AUTHOR WHERE ID = #{id}在映射语句中,必须通过 resultSets 属性为每个结果集指定一个名字,多个名字使用逗号隔开。
<select id="selectBlog" resultSets="blogs,authors" resultMap="blogResult" statementType="CALLABLE">
{call getBlogsAndAuthors(#{id,jdbcType=INTEGER,mode=IN})}
</select>现在我们可以指定使用 “authors” 结果集的数据来填充 “author” 关联:
<resultMap id="blogResult" type="Blog">
<id property="id" column="id" />
<result property="title" column="title"/>
<association property="author" javaType="Author" resultSet="authors" column="author_id" foreignColumn="id">
<id property="id" column="id"/>
<result property="username" column="username"/>
<result property="password" column="password"/>
<result property="email" column="email"/>
<result property="bio" column="bio"/>
</association>
</resultMap>你已经在上面看到了如何处理“有一个”类型的关联。但是该怎么处理“有很多个”类型的关联呢?这就是我们接下来要介绍的
collection
<collection property="posts" ofType="domain.blog.Post">
<id property="id" column="post_id"/>
<result property="subject" column="post_subject"/>
<result property="body" column="post_body"/>
</collection>集合元素和关联元素几乎是一样的,它们相似的程度之高,以致于没有必要再介绍集合元素的相似部分。 所以让我们来关注它们的不同之处吧。
我们来继续上面的示例,一个博客(Blog)只有一个作者(Author)。但一个博客有很多文章(Post)。 在博客类中,这可以用下面的写法来表示:
private List<Post> posts;要像上面这样,映射嵌套结果集合到一个 List 中,可以使用集合元素。 和关联元素一样,我们可以使用嵌套 Select 查询,或基于连接的嵌套结果映射集合。
集合的嵌套 Select 查询
首先,让我们看看如何使用嵌套 Select 查询来为博客加载文章。
<resultMap id="blogResult" type="Blog">
<collection property="posts" javaType="ArrayList" column="id" ofType="Post" select="selectPostsForBlog"/>
</resultMap>
<select id="selectBlog" resultMap="blogResult">
SELECT * FROM BLOG WHERE ID = #{id}
</select>
<select id="selectPostsForBlog" resultType="Post">
SELECT * FROM POST WHERE BLOG_ID = #{id}
</select>你可能会立刻注意到几个不同,但大部分都和我们上面学习过的关联元素非常相似。 首先,你会注意到我们使用的是集合元素。 接下来你会注意到有一个新的 “ofType” 属性。这个属性非常重要,它用来将 JavaBean(或字段)属性的类型和集合存储的类型区分开来。 所以你可以按照下面这样来阅读映射:
<collection property="posts" javaType="ArrayList" column="id" ofType="Post" select="selectPostsForBlog"/>读作: “posts 是一个存储 Post 的 ArrayList 集合”
在一般情况下,MyBatis 可以推断 javaType 属性,因此并不需要填写。所以很多时候你可以简略成:
<collection property="posts" column="id" ofType="Post" select="selectPostsForBlog"/>集合的嵌套结果映射
现在你可能已经猜到了集合的嵌套结果映射是怎样工作的——除了新增的 “ofType” 属性,它和关联的完全相同。
首先, 让我们看看对应的 SQL 语句:
<select id="selectBlog" resultMap="blogResult">
select
B.id as blog_id,
B.title as blog_title,
B.author_id as blog_author_id,
P.id as post_id,
P.subject as post_subject,
P.body as post_body,
from Blog B
left outer join Post P on B.id = P.blog_id
where B.id = #{id}
</select>我们再次连接了博客表和文章表,并且为每一列都赋予了一个有意义的别名,以便映射保持简单。 要映射博客里面的文章集合,就这么简单:
<resultMap id="blogResult" type="Blog">
<id property="id" column="blog_id" />
<result property="title" column="blog_title"/>
<collection property="posts" ofType="Post">
<id property="id" column="post_id"/>
<result property="subject" column="post_subject"/>
<result property="body" column="post_body"/>
</collection>
</resultMap>再提醒一次,要记得上面 id 元素的重要性,如果你不记得了,请阅读关联部分的相关部分。
如果你喜欢更详略的、可重用的结果映射,你可以使用下面的等价形式:
<resultMap id="blogResult" type="Blog">
<id property="id" column="blog_id" />
<result property="title" column="blog_title"/>
<collection property="posts" ofType="Post" resultMap="blogPostResult" columnPrefix="post_"/>
</resultMap>
<resultMap id="blogPostResult" type="Post">
<id property="id" column="id"/>
<result property="subject" column="subject"/>
<result property="body" column="body"/>
</resultMap>集合的多结果集(ResultSet)
像关联元素那样,我们可以通过执行存储过程实现,它会执行两个查询并返回两个结果集,一个是博客的结果集,另一个是文章的结果集:
SELECT * FROM BLOG WHERE ID = #{id}
SELECT * FROM POST WHERE BLOG_ID = #{id}在映射语句中,必须通过 resultSets 属性为每个结果集指定一个名字,多个名字使用逗号隔开。
<select id="selectBlog" resultSets="blogs,posts" resultMap="blogResult">
{call getBlogsAndPosts(#{id,jdbcType=INTEGER,mode=IN})}
</select>我们指定 “posts” 集合将会使用存储在 “posts” 结果集中的数据进行填充:
<resultMap id="blogResult" type="Blog">
<id property="id" column="id" />
<result property="title" column="title"/>
<collection property="posts" ofType="Post" resultSet="posts" column="id" foreignColumn="blog_id">
<id property="id" column="id"/>
<result property="subject" column="subject"/>
<result property="body" column="body"/>
</collection>
</resultMap>对关联或集合的映射,并没有深度、广度或组合上的要求。但在映射时要留意性能问题。 在探索最佳实践的过程中,应用的单元测试和性能测试会是你的好帮手。 而 MyBatis 的好处在于,可以在不对你的代码引入重大变更(如果有)的情况下,允许你之后改变你的想法。
高级关联和集合映射是一个深度话题。文档的介绍只能到此为止。配合少许的实践,你会很快了解全部的用法。
discriminator
<discriminator javaType="int" column="draft">
<case value="1" resultType="DraftPost"/>
</discriminator>有时候,一个数据库查询可能会返回多个不同的结果集(但总体上还是有一定的联系的)。 鉴别器(discriminator)元素就是被设计来应对这种情况的,另外也能处理其它情况,例如类的继承层次结构。 鉴别器的概念很好理解——它很像 Java 语言中的 switch 语句。
一个鉴别器的定义需要指定 column 和 javaType 属性。column 指定了 MyBatis 查询被比较值的地方。 而 javaType 用来确保使用正确的相等测试(虽然很多情况下字符串的相等测试都可以工作)。例如:
<resultMap id="vehicleResult" type="Vehicle">
<id property="id" column="id" />
<result property="vin" column="vin"/>
<result property="year" column="year"/>
<result property="make" column="make"/>
<result property="model" column="model"/>
<result property="color" column="color"/>
<discriminator javaType="int" column="vehicle_type">
<case value="1" resultMap="carResult"/>
<case value="2" resultMap="truckResult"/>
<case value="3" resultMap="vanResult"/>
<case value="4" resultMap="suvResult"/>
</discriminator>
</resultMap>在这个示例中,MyBatis 会从结果集中得到每条记录,然后比较它的 vehicle type 值。 如果它匹配任意一个鉴别器的 case,就会使用这个 case 指定的结果映射。 这个过程是互斥的,也就是说,剩余的结果映射将被忽略(除非它是扩展的,我们将在稍后讨论它)。 如果不能匹配任何一个 case,MyBatis 就只会使用鉴别器块外定义的结果映射。 所以,如果 carResult 的声明如下:
<resultMap id="carResult" type="Car">
<result property="doorCount" column="door_count" />
</resultMap>那么只有 doorCount 属性会被加载。这是为了即使鉴别器的 case 之间都能分为完全独立的一组,尽管和父结果映射可能没有什么关系。在上面的例子中,我们当然知道 cars 和 vehicles 之间有关系,也就是 Car 是一个 Vehicle。因此,我们希望剩余的属性也能被加载。而这只需要一个小修改。
<resultMap id="carResult" type="Car" extends="vehicleResult">
<result property="doorCount" column="door_count" />
</resultMap>现在 vehicleResult 和 carResult 的属性都会被加载了。
可能有人又会觉得映射的外部定义有点太冗长了。 因此,对于那些更喜欢简洁的映射风格的人来说,还有另一种语法可以选择。例如:
<resultMap id="vehicleResult" type="Vehicle">
<id property="id" column="id" />
<result property="vin" column="vin"/>
<result property="year" column="year"/>
<result property="make" column="make"/>
<result property="model" column="model"/>
<result property="color" column="color"/>
<discriminator javaType="int" column="vehicle_type">
<case value="1" resultType="carResult">
<result property="doorCount" column="door_count" />
</case>
<case value="2" resultType="truckResult">
<result property="boxSize" column="box_size" />
<result property="extendedCab" column="extended_cab" />
</case>
<case value="3" resultType="vanResult">
<result property="powerSlidingDoor" column="power_sliding_door" />
</case>
<case value="4" resultType="suvResult">
<result property="allWheelDrive" column="all_wheel_drive" />
</case>
</discriminator>
</resultMap>请注意,这些都是结果映射,如果你完全不设置任何的 result 元素,MyBatis 将为你自动匹配列和属性。所以上面的例子大多都要比实际的更复杂。 这也表明,大多数数据库的复杂度都比较高,我们不太可能一直依赖于这种机制。
autoMapping
正如你在前面一节看到的,在简单的场景下,MyBatis 可以为你自动映射查询结果。但如果遇到复杂的场景,你需要构建一个结果映射。 但是在本节中,你将看到,你可以混合使用这两种策略。让我们深入了解一下自动映射是怎样工作的。
当自动映射查询结果时,MyBatis 会获取结果中返回的列名并在 Java 类中查找相同名字的属性(忽略大小写)。 这意味着如果发现了 ID 列和 id 属性,MyBatis 会将列 ID 的值赋给 id 属性。
通常数据库列使用大写字母组成的单词命名,单词间用下划线分隔;而 Java 属性一般遵循驼峰命名法约定。为了在这两种命名方式之间启用自动映射,需要将 mapUnderscoreToCamelCase 设置为 true。
甚至在提供了结果映射后,自动映射也能工作。在这种情况下,对于每一个结果映射,在 ResultSet 出现的列,如果没有设置手动映射,将被自动映射。在自动映射处理完毕后,再处理手动映射。 在下面的例子中,id 和 userName 列将被自动映射,hashed_password 列将根据配置进行映射。
<select id="selectUsers" resultMap="userResultMap">
select
user_id as "id",
user_name as "userName",
hashed_password
from some_table
where id = #{id}
</select>
<resultMap id="userResultMap" type="User">
<result property="password" column="hashed_password"/>
</resultMap>有三种自动映射等级:
NONE- 禁用自动映射。仅对手动映射的属性进行映射。PARTIAL- 对除在内部定义了嵌套结果映射(也就是连接的属性)以外的属性进行映射FULL- 自动映射所有属性。
默认值是 PARTIAL,这是有原因的。当对连接查询的结果使用 FULL 时,连接查询会在同一行中获取多个不同实体的数据,因此可能导致非预期的映射。 下面的例子将展示这种风险:
<select id="selectBlog" resultMap="blogResult">
select
B.id,
B.title,
A.username,
from Blog B left outer join Author A on B.author_id = A.id
where B.id = #{id}
</select>
<resultMap id="blogResult" type="Blog">
<association property="author" resultMap="authorResult"/>
</resultMap>
<resultMap id="authorResult" type="Author">
<result property="username" column="author_username"/>
</resultMap>在该结果映射中,Blog 和 Author 均将被自动映射。但是注意 Author 有一个 id 属性,在 ResultSet 中也有一个名为 id 的列,所以 Author 的 id 将填入 Blog 的 id,这可不是你期望的行为。 所以,要谨慎使用 FULL。
无论设置的自动映射等级是哪种,你都可以通过在结果映射上设置 autoMapping 属性来为指定的结果映射设置启用/禁用自动映射。
<resultMap id="userResultMap" type="User" autoMapping="false">
<result property="password" column="hashed_password"/>
</resultMap>cache
MyBatis 内置了一个强大的事务性查询缓存机制,它可以非常方便地配置和定制。 为了使它更加强大而且易于配置,我们对 MyBatis 3 中的缓存实现进行了许多改进。
默认情况下,只启用了本地的会话缓存,它仅仅对一个会话中的数据进行缓存。 要启用全局的二级缓存,只需要在你的 SQL 映射文件中添加一行:
<cache/>基本上就是这样。这个简单语句的效果如下:
- 映射语句文件中的所有 select 语句的结果将会被缓存。
- 映射语句文件中的所有 insert、update 和 delete 语句会刷新缓存。
- 缓存会使用最近最少使用算法(LRU, Least Recently Used)算法来清除不需要的缓存。
- 缓存不会定时进行刷新(也就是说,没有刷新间隔)。
- 缓存会保存列表或对象(无论查询方法返回哪种)的 1024 个引用。
缓存只作用于 cache 标签所在的映射文件中的语句。如果你混合使用 Java API 和 XML 映射文件,在共用接口中的语句将不会被默认缓存。你需要使用 @CacheNamespaceRef 注解指定缓存作用域。
这些属性可以通过 cache 元素的属性来修改。比如:
<cache
eviction="FIFO"
flushInterval="60000"
size="512"
readOnly="true"/>这个更高级的配置创建了一个 FIFO 缓存,每隔 60 秒刷新,最多可以存储结果对象或列表的 512 个引用,而且返回的对象被认为是只读的,因此对它们进行修改可能会在不同线程中的调用者产生冲突。
可用的清除策略有:
LRU– 最近最少使用:移除最长时间不被使用的对象。FIFO– 先进先出:按对象进入缓存的顺序来移除它们。SOFT– 软引用:基于垃圾回收器状态和软引用规则移除对象。WEAK– 弱引用:更积极地基于垃圾收集器状态和弱引用规则移除对象。
默认的清除策略是 LRU。
flushInterval(刷新间隔)属性可以被设置为任意的正整数,设置的值应该是一个以毫秒为单位的合理时间量。 默认情况是不设置,也就是没有刷新间隔,缓存仅仅会在调用语句时刷新。
size(引用数目)属性可以被设置为任意正整数,要注意欲缓存对象的大小和运行环境中可用的内存资源。默认值是 1024。
readOnly(只读)属性可以被设置为 true 或 false。只读的缓存会给所有调用者返回缓存对象的相同实例。 因此这些对象不能被修改。这就提供了可观的性能提升。而可读写的缓存会(通过序列化)返回缓存对象的拷贝。 速度上会慢一些,但是更安全,因此默认值是 false。
二级缓存是事务性的。这意味着,当 SqlSession 完成并提交时,或是完成并回滚,但没有执行 flushCache=true 的 insert/delete/update 语句时,缓存会获得更新。
动态SQL
- if
- choose (when, otherwise)
- trim (where, set)
- foreach
if
使用动态 SQL 最常见情景是根据条件包含 where 子句的一部分。比如:
<select id="findActiveBlogWithTitleLike"
resultType="Blog">
SELECT * FROM BLOG
WHERE state = ‘ACTIVE’
<if test="title != null">
AND title like #{title}
</if>
</select>这条语句提供了可选的查找文本功能。如果不传入 “title”,那么所有处于 “ACTIVE” 状态的 BLOG 都会返回;如果传入了 “title” 参数,那么就会对 “title” 一列进行模糊查找并返回对应的 BLOG 结果(细心的读者可能会发现,“title” 的参数值需要包含查找掩码或通配符字符)。
如果希望通过 “title” 和 “author” 两个参数进行可选搜索该怎么办呢?首先,我想先将语句名称修改成更名副其实的名称;接下来,只需要加入另一个条件即可。
<select id="findActiveBlogLike"
resultType="Blog">
SELECT * FROM BLOG WHERE state = ‘ACTIVE’
<if test="title != null">
AND title like #{title}
</if>
<if test="author != null and author.name != null">
AND author_name like #{author.name}
</if>
</select>choose、when、otherwise
有时候,我们不想使用所有的条件,而只是想从多个条件中选择一个使用。针对这种情况,MyBatis 提供了 choose 元素,它有点像 Java 中的 switch 语句。
还是上面的例子,但是策略变为:传入了 “title” 就按 “title” 查找,传入了 “author” 就按 “author” 查找的情形。若两者都没有传入,就返回标记为 featured 的 BLOG(这可能是管理员认为,与其返回大量的无意义随机 Blog,还不如返回一些由管理员挑选的 Blog)。
<select id="findActiveBlogLike"
resultType="Blog">
SELECT * FROM BLOG WHERE state = ‘ACTIVE’
<choose>
<when test="title != null">
AND title like #{title}
</when>
<when test="author != null and author.name != null">
AND author_name like #{author.name}
</when>
<otherwise>
AND featured = 1
</otherwise>
</choose>
</select>trim、where、set
前面几个例子已经合宜地解决了一个臭名昭著的动态 SQL 问题。现在回到之前的 “if” 示例,这次我们将 “state = ‘ACTIVE’” 设置成动态条件,看看会发生什么。
<select id="findActiveBlogLike"
resultType="Blog">
SELECT * FROM BLOG
WHERE
<if test="state != null">
state = #{state}
</if>
<if test="title != null">
AND title like #{title}
</if>
<if test="author != null and author.name != null">
AND author_name like #{author.name}
</if>
</select>如果没有匹配的条件会怎么样?最终这条 SQL 会变成这样:
SELECT * FROM BLOG
WHERE这会导致查询失败。如果匹配的只是第二个条件又会怎样?这条 SQL 会是这样:
SELECT * FROM BLOG
WHERE
AND title like ‘someTitle’这个查询也会失败。这个问题不能简单地用条件元素来解决。这个问题是如此的难以解决,以至于解决过的人不会再想碰到这种问题。
MyBatis 有一个简单且适合大多数场景的解决办法。而在其他场景中,可以对其进行自定义以符合需求。而这,只需要一处简单的改动:
<select id="findActiveBlogLike"
resultType="Blog">
SELECT * FROM BLOG
<where>
<if test="state != null">
state = #{state}
</if>
<if test="title != null">
AND title like #{title}
</if>
<if test="author != null and author.name != null">
AND author_name like #{author.name}
</if>
</where>
</select>where 元素只会在子元素返回任何内容的情况下才插入 “WHERE” 子句。而且,若子句的开头为 “AND” 或 “OR”,where 元素也会将它们去除。
如果 where 元素与你期望的不太一样,你也可以通过自定义 trim 元素来定制 where 元素的功能。比如,和 where 元素等价的自定义 trim 元素为:
<trim prefix="WHERE" prefixOverrides="AND |OR ">
...
</trim>prefixOverrides 属性会忽略通过管道符分隔的文本序列(注意此例中的空格是必要的)。上述例子会移除所有 prefixOverrides 属性中指定的内容,并且插入 prefix 属性中指定的内容。
用于动态更新语句的类似解决方案叫做 set。set 元素可以用于动态包含需要更新的列,忽略其它不更新的列。比如:
<update id="updateAuthorIfNecessary">
update Author
<set>
<if test="username != null">username=#{username},</if>
<if test="password != null">password=#{password},</if>
<if test="email != null">email=#{email},</if>
<if test="bio != null">bio=#{bio}</if>
</set>
where id=#{id}
</update>这个例子中,set 元素会动态地在行首插入 SET 关键字,并会删掉额外的逗号(这些逗号是在使用条件语句给列赋值时引入的)。
来看看与 set 元素等价的自定义 trim 元素吧:
<trim prefix="SET" suffixOverrides=",">
...
</trim>注意,我们覆盖了后缀值设置,并且自定义了前缀值。
foreach
动态 SQL 的另一个常见使用场景是对集合进行遍历(尤其是在构建 IN 条件语句的时候)。比如:
<select id="selectPostIn" resultType="domain.blog.Post">
SELECT *
FROM POST P
WHERE ID in
<foreach item="item" index="index" collection="list"
open="(" separator="," close=")">
#{item}
</foreach>
</select>foreach 元素的功能非常强大,它允许你指定一个集合,声明可以在元素体内使用的集合项(item)和索引(index)变量。它也允许你指定开头与结尾的字符串以及集合项迭代之间的分隔符。这个元素也不会错误地添加多余的分隔符,看它多智能!
提示 你可以将任何可迭代对象(如 List、Set 等)、Map 对象或者数组对象作为集合参数传递给 foreach。当使用可迭代对象或者数组时,index 是当前迭代的序号,item 的值是本次迭代获取到的元素。当使用 Map 对象(或者 Map.Entry 对象的集合)时,index 是键,item 是值。
script
要在带注解的映射器接口类中使用动态 SQL,可以使用 script 元素。
@Update({"<script>",
"update Author",
" <set>",
" <if test='username != null'>username=#{username},</if>",
" <if test='password != null'>password=#{password},</if>",
" <if test='email != null'>email=#{email},</if>",
" <if test='bio != null'>bio=#{bio}</if>",
" </set>",
"where id=#{id}",
"</script>"})
void updateAuthorValues(Author author);bind
bind 元素允许你在 OGNL 表达式以外创建一个变量,并将其绑定到当前的上下文。比如:
<select id="selectBlogsLike" resultType="Blog">
<bind name="pattern" value="'%' + _parameter.getTitle() + '%'" />
SELECT * FROM BLOG
WHERE title LIKE #{pattern}
</select>log
Mybatis 通过使用内置的日志工厂提供日志功能。内置日志工厂将会把日志工作委托给下面的实现之一:
- SLF4J
- Apache Commons Logging
- Log4j 2
- Log4j
- JDK logging
MyBatis 内置日志工厂会基于运行时检测信息选择日志委托实现。它会(按上面罗列的顺序)使用第一个查找到的实现。当没有找到这些实现时,将会禁用日志功能。
不少应用服务器(如 Tomcat 和 WebShpere)的类路径中已经包含 Commons Logging。注意,在这种配置环境下,MyBatis 会把 Commons Logging 作为日志工具。这就意味着在诸如 WebSphere 的环境中,由于提供了 Commons Logging 的私有实现,你的 Log4J 配置将被忽略。这个时候你就会感觉很郁闷:看起来 MyBatis 将你的 Log4J 配置忽略掉了(其实是因为在这种配置环境下,MyBatis 使用了 Commons Logging 作为日志实现)。如果你的应用部署在一个类路径已经包含 Commons Logging 的环境中,而你又想使用其它日志实现,你可以通过在 MyBatis 配置文件 mybatis-config.xml 里面添加一项 setting 来选择其它日志实现。
<configuration>
<settings>
...
<setting name="logImpl" value="LOG4J"/>
...
</settings>
</configuration>可选的值有:SLF4J、LOG4J、LOG4J2、JDK_LOGGING、COMMONS_LOGGING、STDOUT_LOGGING、NO_LOGGING,或者是实现了 org.apache.ibatis.logging.Log 接口,且构造方法以字符串为参数的类完全限定名。
你也可以调用以下任一方法来选择日志实现:
org.apache.ibatis.logging.LogFactory.useSlf4jLogging();
org.apache.ibatis.logging.LogFactory.useLog4JLogging();
org.apache.ibatis.logging.LogFactory.useJdkLogging();
org.apache.ibatis.logging.LogFactory.useCommonsLogging();
org.apache.ibatis.logging.LogFactory.useStdOutLogging();你应该在调用其它 MyBatis 方法之前调用以上的某个方法。另外,仅当运行时类路径中存在该日志实现时,日志实现的切换才会生效。如果你的环境中并不存在 Log4J,你却试图调用了相应的方法,MyBatis 就会忽略这一切换请求,并将以默认的查找顺序决定使用的日志实现。
关于 SLF4J、Apache Commons Logging、Apache Log4J 和 JDK Logging 的 API 介绍不在本文档介绍范围内。不过,下面的例子可以作为一个快速入门。有关这些日志框架的更多信息,可以参考以下链接:
配置 Log4J
配置 Log4J 比较简单。假设你需要记录这个映射器的日志:
package org.mybatis.example;
public interface BlogMapper {
@Select("SELECT * FROM blog WHERE id = #{id}")
Blog selectBlog(int id);
}在应用的类路径中创建一个名为 log4j.properties 的文件,文件的具体内容如下:
# 全局日志配置log4j.rootLogger=ERROR, stdout# MyBatis 日志配置log4j.logger.org.mybatis.example.BlogMapper=TRACE# 控制台输出log4j.appender.stdout=org.apache.log4j.ConsoleAppenderlog4j.appender.stdout.layout=org.apache.log4j.PatternLayoutlog4j.appender.stdout.layout.ConversionPattern=%5p [%t] - %m%nproperties上述配置将使 Log4J 详细打印 org.mybatis.example.BlogMapper 的日志,对于应用的其它部分,只打印错误信息。
为了实现更细粒度的日志输出,你也可以只打印特定语句的日志。以下配置将只打印语句 selectBlog 的日志:
log4j.logger.org.mybatis.example.BlogMapper.selectBlog=TRACE或者,你也可以打印一组映射器的日志,只需要打开映射器所在的包的日志功能即可:
log4j.logger.org.mybatis.example=TRACE某些查询可能会返回庞大的结果集。这时,你可能只想查看 SQL 语句,而忽略返回的结果集。为此,SQL 语句将会在 DEBUG 日志级别下记录(JDK 日志则为 FINE)。返回的结果集则会在 TRACE 日志级别下记录(JDK 日志则为 FINER)。因此,只要将日志级别调整为 DEBUG 即可:
log4j.logger.org.mybatis.example=DEBUG但如果你要为下面的映射器 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="org.mybatis.example.BlogMapper">
<select id="selectBlog" resultType="Blog">
select * from Blog where id = #{id}
</select>
</mapper>这时,你可以通过打开命名空间的日志功能来对整个 XML 记录日志:
log4j.logger.org.mybatis.example.BlogMapper=TRACE而要记录具体语句的日志,可以这样做:
log4j.logger.org.mybatis.example.BlogMapper.selectBlog=TRACE你应该会发现,为映射器和 XML 文件打开日志功能的语句毫无差别。
提示 如果你使用的是 SLF4J 或 Log4j 2,MyBatis 会设置 tag 为 MYBATIS。
配置文件 log4j.properties 的余下内容用来配置输出器(appender),这一内容已经超出本文档的范围。关于 Log4J 的更多内容,可以参考上面的 Log4J 网站。或者,你也可以简单地做个实验,看看不同的配置会产生怎样的效果。
配置logback
logback.xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!--
CONSOLE :表示当前的日志信息是可以输出到控制台的。
-->
<appender name="Console" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>[%level] %blue(%d{HH:mm:ss.SSS}) %cyan([%thread]) %boldGreen(%logger{15}) - %msg %n</pattern>
</encoder>
</appender>
<logger name="com.xxx.xxx" level="DEBUG" additivity="false">
<appender-ref ref="Console"/>
</logger>
<!--
level:用来设置打印级别,大小写无关:TRACE, DEBUG, INFO, WARN, ERROR, ALL 和 OFF
, 默认debug
<root>可以包含零个或多个<appender-ref>元素,标识这个输出位置将会被本日志级别控制。
-->
<root level="DEBUG">
<appender-ref ref="Console"/>
</root>
</configuration>java API
Mapper注解
设计初期的 MyBatis 是一个 XML 驱动的框架。配置信息是基于 XML 的,映射语句也是定义在 XML 中的。而在 MyBatis 3 中,我们提供了其它的配置方式。MyBatis 3 构建在全面且强大的基于 Java 语言的配置 API 之上。它是 XML 和注解配置的基础。注解提供了一种简单且低成本的方式来实现简单的映射语句。
提示 不幸的是,Java 注解的表达能力和灵活性十分有限。尽管我们花了很多时间在调查、设计和试验上,但最强大的 MyBatis 映射并不能用注解来构建——我们真没开玩笑。而 C# 属性就没有这些限制,因此 MyBatis.NET 的配置会比 XML 有更大的选择余地。虽说如此,基于 Java 注解的配置还是有它的好处的。
注解如下表所示:
| 注解 | 使用对象 | XML 等价形式 | 描述 |
|---|---|---|---|
@CacheNamespace | 类 | <cache> | 为给定的命名空间(比如类)配置缓存。属性:implemetation、eviction、flushInterval、size、readWrite、blocking、properties。 |
@Property | N/A | <property> | 指定参数值或占位符(placeholder)(该占位符能被 mybatis-config.xml 内的配置属性替换)。属性:name、value。(仅在 MyBatis 3.4.2 以上可用) |
@CacheNamespaceRef | 类 | <cacheRef> | 引用另外一个命名空间的缓存以供使用。注意,即使共享相同的全限定类名,在 XML 映射文件中声明的缓存仍被识别为一个独立的命名空间。属性:value、name。如果你使用了这个注解,你应设置 value 或者 name 属性的其中一个。value 属性用于指定能够表示该命名空间的 Java 类型(命名空间名就是该 Java 类型的全限定类名),name 属性(这个属性仅在 MyBatis 3.4.2 以上可用)则直接指定了命名空间的名字。 |
@ConstructorArgs | 方法 | <constructor> | 收集一组结果以传递给一个结果对象的构造方法。属性:value,它是一个 Arg 数组。 |
@Arg | N/A | <arg>``<idArg> | ConstructorArgs 集合的一部分,代表一个构造方法参数。属性:id、column、javaType、jdbcType、typeHandler、select、resultMap。id 属性和 XML 元素 <idArg> 相似,它是一个布尔值,表示该属性是否用于唯一标识和比较对象。从版本 3.5.4 开始,该注解变为可重复注解。 |
@TypeDiscriminator | 方法 | <discriminator> | 决定使用何种结果映射的一组取值(case)。属性:column、javaType、jdbcType、typeHandler、cases。cases 属性是一个 Case 的数组。 |
@Case | N/A | <case> | 表示某个值的一个取值以及该取值对应的映射。属性:value、type、results。results 属性是一个 Results 的数组,因此这个注解实际上和 ResultMap 很相似,由下面的 Results 注解指定。 |
@Results | 方法 | <resultMap> | 一组结果映射,指定了对某个特定结果列,映射到某个属性或字段的方式。属性:value、id。value 属性是一个 Result 注解的数组。而 id 属性则是结果映射的名称。从版本 3.5.4 开始,该注解变为可重复注解。 |
@Result | N/A | <result>``<id> | 在列和属性或字段之间的单个结果映射。属性:id、column、javaType、jdbcType、typeHandler、one、many。id 属性和 XML 元素 <id> 相似,它是一个布尔值,表示该属性是否用于唯一标识和比较对象。one 属性是一个关联,和 <association> 类似,而 many 属性则是集合关联,和 <collection> 类似。这样命名是为了避免产生名称冲突。 |
@One | N/A | <association> | 复杂类型的单个属性映射。属性:select,指定可加载合适类型实例的映射语句(也就是映射器方法)全限定名;fetchType,指定在该映射中覆盖全局配置参数 lazyLoadingEnabled。提示 注解 API 不支持联合映射。这是由于 Java 注解不允许产生循环引用。 |
@Many | N/A | <collection> | 复杂类型的集合属性映射。属性:select,指定可加载合适类型实例集合的映射语句(也就是映射器方法)全限定名;fetchType,指定在该映射中覆盖全局配置参数 lazyLoadingEnabled。提示 注解 API 不支持联合映射。这是由于 Java 注解不允许产生循环引用。 |
@MapKey | 方法 | 供返回值为 Map 的方法使用的注解。它使用对象的某个属性作为 key,将对象 List 转化为 Map。属性:value,指定作为 Map 的 key 值的对象属性名。 | |
@Options | 方法 | 映射语句的属性 | 该注解允许你指定大部分开关和配置选项,它们通常在映射语句上作为属性出现。与在注解上提供大量的属性相比,Options 注解提供了一致、清晰的方式来指定选项。属性:useCache=true、flushCache=FlushCachePolicy.DEFAULT、resultSetType=DEFAULT、statementType=PREPARED、fetchSize=-1、timeout=-1、useGeneratedKeys=false、keyProperty=""、keyColumn=""、resultSets=""。注意,Java 注解无法指定 null 值。因此,一旦你使用了 Options 注解,你的语句就会被上述属性的默认值所影响。要注意避免默认值带来的非预期行为。 注意:keyColumn 属性只在某些数据库中有效(如 Oracle、PostgreSQL 等)。要了解更多关于 keyColumn 和 keyProperty 可选值信息,请查看“insert, update 和 delete”一节。 |
@Insert``@Update@Delete``@Select | 方法 | <insert>``<update><delete>``<select> | 每个注解分别代表将会被执行的 SQL 语句。它们用字符串数组(或单个字符串)作为参数。如果传递的是字符串数组,字符串数组会被连接成单个完整的字符串,每个字符串之间加入一个空格。这有效地避免了用 Java 代码构建 SQL 语句时产生的“丢失空格”问题。当然,你也可以提前手动连接好字符串。属性:value,指定用来组成单个 SQL 语句的字符串数组。 |
@InsertProvider@UpdateProvider@DeleteProvider@SelectProvider | 方法 | <insert>``<update><delete>``<select> | 允许构建动态 SQL。这些备选的 SQL 注解允许你指定返回 SQL 语句的类和方法,以供运行时执行。(从 MyBatis 3.4.6 开始,可以使用 CharSequence 代替 String 来作为返回类型)。当执行映射语句时,MyBatis 会实例化注解指定的类,并调用注解指定的方法。你可以通过 ProviderContext 传递映射方法接收到的参数、"Mapper interface type" 和 "Mapper method"(仅在 MyBatis 3.4.5 以上支持)作为参数。(MyBatis 3.4 以上支持传入多个参数)属性:type、method。type 属性用于指定类名。method 用于指定该类的方法名(从版本 3.5.1 开始,可以省略 method 属性,MyBatis 将会使用 ProviderMethodResolver 接口解析方法的具体实现。如果解析失败,MyBatis 将会使用名为 provideSql 的降级实现)。提示 接下来的“SQL 语句构建器”一章将会讨论该话题,以帮助你以更清晰、更便于阅读的方式构建动态 SQL。 |
@Param | 参数 | N/A | 如果你的映射方法接受多个参数,就可以使用这个注解自定义每个参数的名字。否则在默认情况下,除 RowBounds 以外的参数会以 "param" 加参数位置被命名。例如 #{param1}, #{param2}。如果使用了 @Param("person"),参数就会被命名为 #{person}。 |
@SelectKey | 方法 | <selectKey> | 这个注解的功能与 <selectKey> 标签完全一致。该注解只能在 @Insert 或 @InsertProvider 或 @Update 或 @UpdateProvider 标注的方法上使用,否则将会被忽略。如果标注了 @SelectKey 注解,MyBatis 将会忽略掉由 @Options 注解所设置的生成主键或设置(configuration)属性。属性:statement 以字符串数组形式指定将会被执行的 SQL 语句,keyProperty 指定作为参数传入的对象对应属性的名称,该属性将会更新成新的值,before 可以指定为 true 或 false 以指明 SQL 语句应被在插入语句的之前还是之后执行。resultType 则指定 keyProperty 的 Java 类型。statementType 则用于选择语句类型,可以选择 STATEMENT、PREPARED 或 CALLABLE 之一,它们分别对应于 Statement、PreparedStatement 和 CallableStatement。默认值是 PREPARED。 |
@ResultMap | 方法 | N/A | 这个注解为 @Select 或者 @SelectProvider 注解指定 XML 映射中 <resultMap> 元素的 id。这使得注解的 select 可以复用已在 XML 中定义的 ResultMap。如果标注的 select 注解中存在 @Results 或者 @ConstructorArgs 注解,这两个注解将被此注解覆盖。 |
@ResultType | 方法 | N/A | 在使用了结果处理器的情况下,需要使用此注解。由于此时的返回类型为 void,所以 Mybatis 需要有一种方法来判断每一行返回的对象类型。如果在 XML 有对应的结果映射,请使用 @ResultMap 注解。如果结果类型在 XML 的 <select> 元素中指定了,就不需要使用其它注解了。否则就需要使用此注解。比如,如果一个标注了 @Select 的方法想要使用结果处理器,那么它的返回类型必须是 void,并且必须使用这个注解(或者 @ResultMap)。这个注解仅在方法返回类型是 void 的情况下生效。 |
@Flush | 方法 | N/A | 如果使用了这个注解,定义在 Mapper 接口中的方法就能够调用 SqlSession#flushStatements() 方法。(Mybatis 3.3 以上可用) |
映射注解示例
这个例子展示了如何使用 @SelectKey 注解来在插入前读取数据库序列的值:
@Insert("insert into table3 (id, name) values(#{nameId}, #{name})")
@SelectKey(statement="call next value for TestSequence", keyProperty="nameId", before=true, resultType=int.class)
int insertTable3(Name name);这个例子展示了如何使用 @SelectKey 注解来在插入后读取数据库自增列的值:
@Insert("insert into table2 (name) values(#{name})")
@SelectKey(statement="call identity()", keyProperty="nameId", before=false, resultType=int.class)
int insertTable2(Name name);这个例子展示了如何使用 @Flush 注解来调用 SqlSession#flushStatements():
@Flush
List<BatchResult> flush();这些例子展示了如何通过指定 @Result 的 id 属性来命名结果集:
@Results(id = "userResult", value = {
@Result(property = "id", column = "uid", id = true),
@Result(property = "firstName", column = "first_name"),
@Result(property = "lastName", column = "last_name")
})
@Select("select * from users where id = #{id}")
User getUserById(Integer id);
@Results(id = "companyResults")
@ConstructorArgs({
@Arg(column = "cid", javaType = Integer.class, id = true),
@Arg(column = "name", javaType = String.class)
})
@Select("select * from company where id = #{id}")
Company getCompanyById(Integer id);这个例子展示了如何使用单个参数的 @SqlProvider 注解:
@SelectProvider(type = UserSqlBuilder.class, method = "buildGetUsersByName")
List<User> getUsersByName(String name);
class UserSqlBuilder {
public static String buildGetUsersByName(final String name) {
return new SQL(){{
SELECT("*");
FROM("users");
if (name != null) {
WHERE("name like #{value} || '%'");
}
ORDER_BY("id");
}}.toString();
}
}这个例子展示了如何使用多个参数的 @SqlProvider 注解:
@SelectProvider(type = UserSqlBuilder.class, method = "buildGetUsersByName")
List<User> getUsersByName(
@Param("name") String name, @Param("orderByColumn") String orderByColumn);
class UserSqlBuilder {
// 如果不使用 @Param,就应该定义与 mapper 方法相同的参数
public static String buildGetUsersByName(
final String name, final String orderByColumn) {
return new SQL(){{
SELECT("*");
FROM("users");
WHERE("name like #{name} || '%'");
ORDER_BY(orderByColumn);
}}.toString();
}
// 如果使用 @Param,就可以只定义需要使用的参数
public static String buildGetUsersByName(@Param("orderByColumn") final String orderByColumn) {
return new SQL(){{
SELECT("*");
FROM("users");
WHERE("name like #{name} || '%'");
ORDER_BY(orderByColumn);
}}.toString();
}
}以下例子展示了 ProviderMethodResolver(3.5.1 后可用)的默认实现使用方法:
@SelectProvider(UserSqlProvider.class)
List<User> getUsersByName(String name);
// 在你的 provider 类中实现 ProviderMethodResolver 接口
class UserSqlProvider implements ProviderMethodResolver {
// 默认实现中,会将映射器方法的调用解析到实现的同名方法上
public static String getUsersByName(final String name) {
return new SQL(){{
SELECT("*");
FROM("users");
if (name != null) {
WHERE("name like #{value} || '%'");
}
ORDER_BY("id");
}}.toString();
}
}SpringBoot
配置
application.yml
server:
port: 80 #配置服务端口name: 123 # 配置name属性
# 数组格式
likes:
- name
- age
# map
users:
-
name: zhansan
age: 14
-
name: lisi
age: 15
baseDir: C:\windows
# 使用 ${属性名}的方式引用数据
tempDir: "${baseDir}\temp"
# 配置数据库信息
datasource:
username: root
password: 1234
url: jdbc:mysql://localhost:3306读取配置文件
package com.example.pojo;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* 封装yml文件中的数据
*/
@Component // 声明bean注解,加入ioc容器中,如果使用@EnableConfigurationProperties注解则无需此注解
// 配置读取配置文件的前缀
@ConfigurationProperties(prefix = "datasource")
public class MyDataSource {
// 与配置文件中的属性名相对应
private String username;
private String password;
private String url;
@Override
public String toString() {
return "MyDataSource{" +
"username='" + username + '\'' +
", password='" + password + '\'' +
", url='" + url + '\'' +
'}';
}
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;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
}开启配置
@EnableConfigurationProperties(XXX.class)
public class SpringbootWebQuickstartApplication {}在其他位置读取配置文件
@RestController
@RequestMapping("/books")
public class BookController {
// @Value获取
/*@Value("${lesson}")
private String lesson;
// 获取数组中的第一个
@Value("${enterprise.subject[0]}")
private String subject;
// 使用此对象获取配置
@Autowired
private Environment environment;
// 直接使用配置对象
@Autowired
private Enterprise enterprise;*/
@GetMapping("/{id}")
public String getById(@PathVariable Integer id) {
/*System.out.println(subject);
// 获取到对应的属性
System.out.println(environment.getProperty("lesson"));
System.out.println(enterprise);*/
return String.valueOf(id);
}
}参数校验
application.yml
servers:
ipAddress: 192.168.0.1
port: 8888
timeout: 1000
serverTimeout: 3
dataSize: 10GB
datasource:
driverClassName: com.mysql.driver
password: 0127
spring:
datasource:
hikari:
username: root
jdbc-url: jdbc:mysql://localhost:3306/db1?useSSL=false
# tomcat:
# username: root
# max-active: 1000添加依赖
<!-- 生成配置文件元数据 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<!-- 校验器 -->
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
</dependency>
<!-- 数据校验 -->
<!--<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
</dependency>-->实体类
package com.example.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.convert.DurationUnit;
import org.springframework.util.unit.DataSize;
import org.springframework.validation.annotation.Validated;
import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import java.time.Duration;
import java.time.temporal.ChronoUnit;
// @Component // 声明bean注解,加入ioc容器中,如果使用@EnableConfigurationProperties注解则无需此注解
@Data // lombok
@ConfigurationProperties(prefix = "servers")
// 2.开启当前bean的数据校验
@Validated
public class ServerConfig {
private String ipAddress;
@Max(value = 8888, message = "最大值不能超过8888")
@Min(value = 222, message = "最小值不能小于222")
private int port;
private long timeout;
@DurationUnit(ChronoUnit.HOURS)
private Duration serverTimeOut;
// @DataSizeUnit(DataUnit.KILOBYTES)
private DataSize dataSize;
}注入到bean中,配置文件中的数据将自动设置到bean中
@Bean
@ConfigurationProperties(prefix = "datasource")
public DruidDataSource druidDataSource() {
DruidDataSource druidDataSource = new DruidDataSource();
// druidDataSource.setDriverClassName("com.mysql.jdbc.Driver");
return druidDataSource;
}在测试环境中
# application.yml
# test:
# prop: testValue
#
testcase:
book:
id: ${random.int(0, 100)}// @Configuration // 如果使用了导入的方式,则无需使用此注解
public class MsgConfig {
@Bean
public String msg() {
return "bean msg";
}
}@SpringBootTest
// 导入
@Import(MsgConfig.class)
public class ConfigurationTest {
@Resource
private String msg;
@Test
void test() {
System.out.println(msg);
}
}临时配置
//临时的属性配置
//@SpringBootTest(properties = {"test.prop=testValue1"})
//@SpringBootTest(args = {"--test.prop=testValue2"})
@SpringBootTest(properties = {"test.prop=testValue1"}, args = {"--test.prop=testValue2"})
class Springboot4TestApplicationTests {
@Value("${test.prop}")
private String msg;
@Test
void contextLoads() {
System.out.println(msg);
}
}web环境模拟请求测试
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
// 开启虚拟MVC调用
@AutoConfigureMockMvc
public class WebTest {
@Test
void test(@Autowired MockMvc mockMvc) throws Exception {
// 创建了一个虚拟的请求
MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.get("/books");
ResultActions resultActions = mockMvc.perform(builder);
// 定义本次调用的预期值
/*StatusResultMatchers status = MockMvcResultMatchers.status();
ResultMatcher ok = status.isOk();*/
// 验证响应体
/*ContentResultMatchers content = MockMvcResultMatchers.content();
// ResultMatcher string = content.string("springboot1");
ResultMatcher json = content.json(
"{\"id\": 1,\"name\": \"springboot1\", \"type\": \"spring\"}"
);*/
HeaderResultMatchers header = MockMvcResultMatchers.header();
ResultMatcher matcher = header.string("Content-Type", "application/json");
// 添加本次预计值与本次调用过程进行匹配
resultActions.andExpect(matcher);
}
}环境配置
根据环境配置不同变量,引用方式@变量@
<!-- 配置环境 -->
<profiles>
<profile>
<id>dev</id>
<properties>
<profile.active>dev</profile.active>
</properties>
</profile>
<profile>
<id>pro</id>
<properties>
<profile.active>pro</profile.active>
</properties>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
</profile>
</profiles>
<build>
<!-- 设置需要打包的资源 -->
<resources>
<resource>
<directory>src/main/resources</directory>
<excludes>
<exclude>*.yml</exclude>
</excludes>
<includes>
<include>application.yml</include>
<include>application-${profile.active}.yml</include>
</includes>
</resource>
</resources>
</build>application.yml
根据不同环境选择不同的application-xxx.yml配置
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/db01?serverTimezone=UTC
username: root
password: jdy200255
profiles:
active: @profile.active@ # 当前的环境
mybatis:
mapper-locations: classpath:mapper/*xml
type-aliases-package: com.example.mybatis.pojo
configuration:
log-impl: org.apache.ibatis.logging.slf4j.Slf4jImpl
logging:
level:
root: info
config: classpath:logback.xml
file:
name: server.log
server:
port: 90@RestController
请求处理类,@Contoller与@ResponseBody结合
@ResponseBody
响应json数据
@RequestBody
接收json数据(请求体数据)
@RequestMapping
请求映射
@GetMapping
GET映射
@PostMapping
POST映射
@RequestParam
defaultValue: 默认值
required: 是否必须
接收普通参数
// 请求地址
@RequestMapping("/simpleParam")
public String simpleParam(@RequestParam(name = "name",/*参数名*/ required = false/*非必须传递*/) String username, Integer age) {
System.out.println(username);
System.out.println(age);
return "ok";
}接收pojo实体类
User.java
public class User {
String name;
Integer age;
Address address;
...
}Address.java
public class Address {
String province;
String city;
...
}@RequestMapping("/simpleParam")
public String simplePojo(User user) {
System.out.println(user.getName());
System.out.println(user.getAge());
return "ok";
}接收多个相同属性
使用数组接收
// 请求地址:http://localhost:8080/arrayParam?hobby=game&hobby=学习
@RequestMapping("/arrayParam")
public String arrayParam(String[] hobby) {
System.out.println(Arrays.toString(hobby));
return "ok";
}使用集合接收
// 需要加上@RequestParam才可以正常接收到
@RequestMapping("/listParam")
public String listParam(@RequestParam List<String> hobby) {
System.out.println(hobby);
return "ok";
}接收时间类型
@RequestMapping("/dateParam")
public String dateParam(@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime updateTime) {
System.out.println(updateTime);
return "ok";
}接收json数据
pojo接收
// 注意添加@RequestBody注解,解析body参数
@RequestMapping("/jsonParam")
public String jsonParam(@RequestBody User user) {
System.out.println(user);
return "ok";
}map接收
@RequestMapping("/jsonParam")
public String jsonParam(@RequestBody Map<String, Object> user) {
System.out.println(user);
return "ok";
}接收路径参数
使用@PathVariable注解接收参数
@RequestMapping("/path/{id}")
public String pathParam(@PathVariable("id") Integer id) {
System.out.println(id);
return "ok";
}接受文件参数
使用multipart对象接收
@RequestMapping("/upload")
public String upload(Multipart image) {
System.out.println(image);
return "ok";
}统一返回数据
public class Result {
private Integer code;
private String msg;
private Object data;
public Result(Integer code, String msg) {
this.code = code;
this.msg = msg;
}
public Result(Integer code, String msg, Object data) {
this.code = code;
this.msg = msg;
this.data = data;
}
public static Result success() {
return new Result(1, null);
}
public static Result success(Object data) {
return new Result(1, null, data);
}
public static Result success(Object data, String msg) {
return new Result(1, msg, data);
}
public static Result error(String msg) {
return new Result(0, msg);
}
public Integer getCode() {
return code;
}
public void setCode(Integer code) {
this.code = code;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
@Override
public String toString() {
return "Result{" +
"code=" + code +
", msg='" + msg + '\'' +
", data=" + data +
'}';
}
}解析XML
导入坐标
<dependency>
<groupId>org.dom4j</groupId>
<artifactId>dom4j</artifactId>
<version>2.1.3</version>
</dependency>设置解析工具类
public class XmlParserUtils {
public static <T> List<T> parse(String file , Class<T> targetClass) {
ArrayList<T> list = new ArrayList<T>(); //封装解析出来的数据
try {
//1.获取一个解析器对象
SAXReader saxReader = new SAXReader();
//2.利用解析器把xml文件加载到内存中,并返回一个文档对象
Document document = saxReader.read(new File(file));
//3.获取到根标签
Element rootElement = document.getRootElement();
//4.通过根标签来获取 user 标签
List<Element> elements = rootElement.elements("emp");
//5.遍历集合,得到每一个 user 标签
for (Element element : elements) {
//获取 name 属性
String name = element.element("name").getText();
//获取 age 属性
String age = element.element("age").getText();
//获取 image 属性
String image = element.element("image").getText();
//获取 gender 属性
String gender = element.element("gender").getText();
//获取 job 属性
String job = element.element("job").getText();
//组装数据
Constructor<T> constructor = targetClass.getDeclaredConstructor(String.class, Integer.class, String.class, String.class, String.class);
constructor.setAccessible(true);
T object = constructor.newInstance(name, Integer.parseInt(age), image, gender, job);
list.add(object);
}
} catch (Exception e) {
e.printStackTrace();
}
return list;
}
}在controller中解析数据
@RequestMapping("/listEmp")
public Result list() {
// 加载并解析xml文件
String file = this.getClass().getClassLoader().getResource("emp.xml").getFile();
System.out.println(file);
List<Emp> empList = XmlParserUtils.parse(file, Emp.class);
empList.forEach(emp -> {
if ("1".equals(emp.getGender())) {
emp.setGender("男");
} else if ("2".equals(emp.getGender())) {
emp.setGender("女");
}
});
return Result.success(empList);
}Bean声明
@Controller
@Component的衍生注解,标注在控制器类上
@Service
@Component的衍生注解,标注在业务类上
@Repository
@Component的衍生注解,标注在数据访问类上
@Component
声明bean的注解,不属于以上三类时,用此注解
@ComponentScan
组件扫描,配置在启动类上,设置包扫描的路径,默认扫描启动类所在包以及子包
@ComponentScan({"xyz.jdynb", "dao"}) // basePackages@Autowired
自动装配,运行时IOC容器会通过该类型的bean对象
@Primary
设置bean的优先级,如果存在两个以及以上相同类型的bean时,优先使用此注解的bean
@Qualifier
指定注入对应名字的bean,配合@Autowired使用
@Autowired
@Qualifier("empDao2")
private EmpDao empDao;@Resource
默认按照名字注入,没有名字按照类型
@Resource(name = "empDao2")
private EmpDao empDao;Bean加载
@Bean
// 默认将加载
@Bean("xxxx")
public Cat tom(xxxService service) {
return new Cat();
}@ConditionalOnClass
@ConditionalOnClass(Mouse.class) 当容器中有Mouse这个bean才会加载
@Bean
@ConditionalOnClass(Mouse.class)
public Cat tom() {
return new Cat();
}@ConditionalOnMissingClass
当容器中没有某个类时才会加载
具体使用
@Bean
@ConditionalOnClass(name = "com.mysql.cj.jdbc.Driver")
public DruidDataSource druidDataSource() {
return new DruidDataSource();
}@Configuration
配置项,集中对Bean进行配置,此注解包含@Component注解
// 配置此注解将注入到容器中,无需import
@Configuration // 也可使用import或@Component注解注入
public class DBConfig {
// 配置Bean
@Bean
public DruidDataSource druidDataSource() {
return new DruidDataSource();
}
}动态加载
动态加载配置项
ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
String[] names = ctx.getBeanDefinitionNames();
for (String name : names) {
System.out.println("name = " + name);
}
System.out.println(ctx.getBean("dog"));@ComponentScan
组件扫描
@ComponentScan({"com.example.adminclient.bean"})
public class SpringConfig {}@Import
@import(xxxx.class) 导入指定类到容器中
@Import({Dog.class, DbConfig.class})
public class SpringConfig4 {}ImportSelector
定义选择器
public class MyImportSelector implements ImportSelector {
@Override
public String[] selectImports(AnnotationMetadata metadata) {
String className = metadata.getClassName();
System.out.println("className = " + className);
boolean hasAnnotation = metadata.hasAnnotation("org.springframework.context.annotation.Configuration");
System.out.println("hasAnnotation = " + hasAnnotation);
// 返回指定的类路径
return new String[]{"com.itheima.bean.Dog", "com.itheima.bean.Cat"};
}
}导入到容器中
@Configuration
@Import({MyImportSelector.class})
public class SpringConfig6 {}ImportBeanDefinitionRegistrar
注册Bean
public class MyRegistrar implements ImportBeanDefinitionRegistrar {
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
BeanDefinition beanDefinition = BeanDefinitionBuilder.rootBeanDefinition(BookServiceImpl2.class).getBeanDefinition();
registry.registerBeanDefinition("bookService", beanDefinition);
}
}导入
@Configuration
@Import({MyRegistrar.class})
public class SpringConfig7 {}PostProcessor
public class MyPostProcessor implements BeanDefinitionRegistryPostProcessor {
@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry beanDefinitionRegistry) throws BeansException {
BeanDefinition beanDefinition = BeanDefinitionBuilder.rootBeanDefinition(BookServiceImpl4.class).getBeanDefinition();
beanDefinitionRegistry.registerBeanDefinition("bookService", beanDefinition);
}
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory configurableListableBeanFactory) throws BeansException {
}
}@ImportResource
加载配置文件
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<!-- Xml申明自己开发的Bean -->
<bean id="cat" class="com.itheima.bean.Cat"/>
<bean class="com.itheima.bean.Dog"/>
<!-- 申明第三方bean -->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"/>
</beans>@Scope
设置Bean的作用域
@Lazy
延迟加载Bean对象,第一次使用的时候加载
Bean 生命周期
// 自定义初始化
@PostConstruct
public void springPostConstruct(){
System.out.println("@PostConstruct");
}
// 自定义销毁
@PreDestroy
public void springPreDestory(){
System.out.println("@PreDestory");
}BeanNameAware
public class Book implements BeanNameAware {
public void setBeanName(String name) {
System.out.println("Book.setBeanName invoke");
}
}BeanFactoryAware
public class Book implements BeanNameAware {
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
System.out.println("Book.setBeanFactory invoke");
}
}InitializingBean
加载Bean
注入ApplicationContext即可获取
...
@AutoWired
private ApplicationContext applicationContext;
...获取bean对象
applicationContext.getBean()使用接口获取
//@Component // 如果声明此注解,则无需使用@import进行导入
@Data
@EnableConfigurationProperties(CartoonProperties.class)
// 实现 ApplicationContextAware 接口,可以获取到applicationContext
public class CartoonCatAndMouse implements ApplicationContextAware {
private Cat cat;
private Mouse mouse;
// 配置文件,这里使用构造器方法进行引入
private CartoonProperties cartoonProperties;
public CartoonCatAndMouse(CartoonProperties cartoonProperties) {
this.cartoonProperties = cartoonProperties;
cat = new Cat();
cat.setName(StringUtils.hasText(cartoonProperties.getCat().getName()) ? cartoonProperties.getCat().getName(): "Tom");
cat.setAge(3);
mouse = new Mouse();
mouse.setAge(4);
mouse.setName("Jerry");
}
public void play() {
String[] names = applicationContext.getBeanDefinitionNames();
for (String name : names) {
System.out.println("name = " + name);
}
System.out.println(cat.getAge() + "岁的" + cat.getName() + "和"
+ mouse.getAge() + "岁的" + mouse.getName() + "打起来了");
}
private ApplicationContext applicationContext;
// 重写此方法以获取到ApplicationContext
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
}导入到容器中
@SpringBootApplication
// 对上面的配置进行导入到容器中
@Import(CartoonCatAndMouse.class)
public class App {
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(App.class, args);
// 获取到容器中指定的bean
CartoonCatAndMouse bean = context.getBean(CartoonCatAndMouse.class);
// 调用bean的方法
bean.play();
System.out.println("bean = " + bean);
}
}SpringBeanUtils
获取Bean工具类
@Component
public class SpringBeanUtils implements ApplicationContextAware {
private static ApplicationContext applicationContext;
@Override
public void setApplicationContext(@NonNull ApplicationContext applicationContext) throws BeansException {
if (SpringBeanUtils.applicationContext == null) {
SpringBeanUtils.applicationContext = applicationContext;
}
}
public static Object getBean(String name) {
return applicationContext.getBean(name);
}
public static <T> T getBean(Class<T> clazz) {
return applicationContext.getBean(clazz);
}
}异常处理
异常处理类
public class SystemException extends RuntimeException {
private Integer code;
public Integer getCode() {
return code;
}
public void setCode(Integer code) {
this.code = code;
}
public SystemException(Integer code, String message) {
super(message);
this.code = code;
}
public SystemException(Integer code, String message, Throwable cause) {
super(message, cause);
this.code = code;
}
}具体实现
/**
* 异常处理类
*/
@RestControllerAdvice
public class ProjectExceptionAdvice {
// 处理系统异常
@ExceptionHandler(SystemException.class)
public Result doSystemException(SystemException e) {
// 记录日志
// 发送信息给运维
// 发送邮件给开发人员
return new Result(e.getCode(), e.getMessage());
}
// 处理业务异常
@ExceptionHandler(BusinessException.class)
public Result doBusinessException(BusinessException e) {
return new Result(e.getCode(), e.getMessage());
}
// 拦截其他异常
@ExceptionHandler(Exception.class)
public Result doException(Exception exception) {
return new Result(Code.SYSTEM_UNKNOW_ERR, "嘿嘿出异常了");
}
}日志
导入坐标 lombok
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>获取日志对象
// 创建日志记录对象
private static final Logger log = LoggerFactory.getLogger(BookController.class);lombok注解获取
@Slf4j
@RestController
@RequestMapping("/books")
public class BookController {}具体使用
@GetMapping
public String getById() {
System.out.println("springboot is running...");
log.debug("debug...");
log.info("info...");
log.warn("warn...");
log.error("error...");
return "springboot is running...";
}配置文件application.properties
# 应用名称
spring.application.name=springboot-1-log
# 应用服务 WEB 访问端口
server.port=8080
# 开启调试
# debug=true
# 设置日志级别
logging.level.root=info
# 设置某个包的日志级别
# logging.level.com.example.controller=debug
# 设置分组对某个组设置日志级别
# 设置分组
logging.group.ebank=com.example.controller,com.example.service
logging.group.iservice=com.example
# 设置对应分组的日志级别
logging.level.ebank=debug
# 控制台输出格式
# logging.pattern.console=%d - %m %n
# %d 日期
# %m 输出信息
# %n 换行
# logging.pattern.console=%d %clr(%5p) %n
# %p 日志级别
# clr 彩色显示
#logging.pattern.console=%d %clr(%5p) --- [%16t] %n
# %t 线程名
#logging.pattern.console=%d %clr(%5p) --- [%16t] %40c %n
# %c 类名
#logging.pattern.console=%d %clr(%5p) --- [%16t] %clr(%-40.40c){cyan} : %m %n
# 日志文件名
logging.file.name=server.log
# 日志最大存储大小
logging.logback.rollingpolicy.max-file-size=4KB
# 日志命名规范
logging.logback.rollingpolicy.file-name-pattern=server.%d{yyyy-MM-dd}.%i.log热部署
导入坐标
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
</dependency>打开Idea设置,找到Compiler,勾选Build project automatically和compile independent module
按ctrl + shift + alt + /,点击registry,勾选compiler.automake.allow.when.app.running
修改启动设置,把on update action he on frame deactivation 都设置为"update classes and resources"
关闭热部署
public static void main(String[] args) {
System.setProperty("spring.devtools.restart.enabled", "false");
SpringApplication.run(Application.class, args);
}Redis
添加依赖
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>配置
spring:
redis:
client-type: lettuce # 客户端类型
lettuce:
pool:
max-active: 16
jedis:
pool:
max-active: 16使用
@Autowired
private RedisTemplate redisTemplate;
@Test
void contextLoads() {
}
@Test
void set() {
ValueOperations ops = redisTemplate.opsForValue();
ops.set("age", 41);
}
@Test
void get() {
ValueOperations ops = redisTemplate.opsForValue();
Object age = ops.get("name");
System.out.println("name = " + age);
}
@Test
void hSet() {
HashOperations ops = redisTemplate.opsForHash();
ops.put("info", "a", "aa");
}
@Test
void hGet() {
HashOperations ops = redisTemplate.opsForHash();
Object a = ops.get("info", "a");
System.out.println("a = " + a);
}StringRedisTemplate
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Test
void get() {
ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();
String name = ops.get("name");
System.out.println("name = " + name);
}Mongodb
添加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>配置
spring:
data:
mongodb:
uri: mongodb://localhost/itheima使用
@Autowired
private MongoTemplate mongoTemplate;
@Test
void contextLoads() {
Book book = new Book();
book.setId(2);
book.setName("springboot2");
book.setType("springboot2");
book.setDescription("springboot2");
mongoTemplate.save(book);
}
@Test
void find() {
List<Book> books = mongoTemplate.findAll(Book.class);
System.out.println("books = " + books);
}Elasticsearch
添加依赖
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-high-level-client</artifactId>
</dependency>使用
@Autowired
private BookDao bookDao;
/*@Autowired
private ElasticsearchRestTemplate restTemplate;*/
@Autowired
private RestHighLevelClient restHighLevelClient;
@BeforeEach
void setUp() {
/*HttpHost host = HttpHost.create("http://localhost:9200");
RestClientBuilder builder = RestClient.builder(host);
restHighLevelClient = new RestHighLevelClient(builder);*/
}
@AfterEach
void tearDown() throws IOException {
restHighLevelClient.close();
}
@Test
void contextLoads() throws IOException {
/*Book book = bookDao.selectById(3);
System.out.println("book = " + book);*/
IndicesClient indices = restHighLevelClient.indices();
CreateIndexRequest request = new CreateIndexRequest("books");
// 设置请求中的参数
String json = "{\n" +
" \"mappings\": {\n" +
" \"properties\": {\n" +
" \"id\": {\n" +
" \"type\": \"keyword\"\n" +
" },\n" +
" \"name\": {\n" +
" \"type\": \"text\",\n" +
" \"analyzer\": \"ik_max_word\",\n" +
" \"copy_to\": \"all\"\n" +
" },\n" +
" \"type\": {\n" +
" \"type\": \"keyword\"\n" +
" },\n" +
" \"description\": {\n" +
" \"type\": \"text\",\n" +
" \"analyzer\": \"ik_max_word\",\n" +
" \"copy_to\": \"all\"\n" +
" },\n" +
" \"all\": {\n" +
" \"type\": \"text\",\n" +
" \"analyzer\": \"ik_max_word\"\n" +
" }\n" +
" }\n" +
" }\n" +
"}";
request.source(json, XContentType.JSON);
indices.create(request, RequestOptions.DEFAULT);
}
@Test
void testCreateDoc() throws IOException {
Book book = bookDao.selectById(3);
IndexRequest request = new IndexRequest("books").id(String.valueOf(book.getId()));
String json = JSON.toJSONString(book);
request.source(json, XContentType.JSON);
restHighLevelClient.index(request, RequestOptions.DEFAULT);
}
/**
* 批处理
* @throws IOException
*/
@Test
void testCreateAllDoc() throws IOException {
List<Book> bookList = bookDao.selectList(null);
/*IndexRequest request = new IndexRequest("books").id(String.valueOf(book.getId()));
String json = JSON.toJSONString(book);
request.source(json, XContentType.JSON);
restHighLevelClient.index(request, RequestOptions.DEFAULT);*/
BulkRequest bulkRequest = new BulkRequest();
for (Book book : bookList) {
IndexRequest request = new IndexRequest("books").id(String.valueOf(book.getId()));
String json = JSON.toJSONString(book);
request.source(json, XContentType.JSON);
bulkRequest.add(request);
}
restHighLevelClient.bulk(bulkRequest, RequestOptions.DEFAULT);
}
@Test
void testGet() throws IOException {
GetRequest request = new GetRequest("books", "3");
GetResponse response = restHighLevelClient.get(request, RequestOptions.DEFAULT);
String source = response.getSourceAsString();
System.out.println("source = " + source);
}
@Test
void testSearch() throws IOException {
SearchRequest request = new SearchRequest("books");
SearchSourceBuilder builder = new SearchSourceBuilder();
builder.query(QueryBuilders.termQuery("name", "spring"));
request.source(builder);
SearchResponse response = restHighLevelClient.search(request, RequestOptions.DEFAULT);
SearchHits hits = response.getHits();
for (SearchHit hit : hits) {
String source = hit.getSourceAsString();
// System.out.println("source = " + source);
Book book = JSON.parseObject(source, Book.class);
System.out.println("book = " + book);
}
}缓存
开启缓存
@SpringBootApplication
// 开启缓存
@EnableCaching
public class Application {}Memcached
添加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>com.googlecode.xmemcached</groupId>
<artifactId>xmemcached</artifactId>
<version>2.4.7</version>
</dependency>配置
memcached:
servers: localhost:11211
poolSize: 10
opTimeOut: 3000使用
@Service
public class SMSCodeServiceImpl implements SMSCodeService {
@Autowired
private CodeUtils codeUtils;
@Autowired
private MemcachedClient memcachedClient;
/*
Xmemcached
*/
@Override
public String sendCodeToSMS(String tel) {
String code = codeUtils.generator2(tel);
try {
// exp 超时时间。单位s
memcachedClient.set(tel, 10, code);
} catch (TimeoutException | InterruptedException | MemcachedException e) {
e.printStackTrace();
}
return code;
}
@Override
public boolean checkCode(SMSCode smsCode) {
String code = null;
try {
code = memcachedClient.get(smsCode.getTel());
} catch (Exception e) {
e.printStackTrace();
}
return smsCode.getCode().equals(code);
}
}实体类
@Data
public class SMSCode {
private String tel;
private String code;
}Redis
添加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>配置
# cache:
# type: redis
# redis:
# use-key-prefix: true
# cache-null-values: false
# key-prefix: aa
# time-to-live: 10s
# redis:
# host: localhost
# port: 6379具体实现同上
Ehcache
添加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>net.sf.ehcache</groupId>
<artifactId>ehcache</artifactId>
</dependency>配置
#ehcache配置
# cache:
# type: ehcache
# ehcache:
# config: ehcache.xmlehcache.xml
<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd"
updateCheck="false">
<diskStore path="D:\ehcache" />
<!--默认缓存策略 -->
<!-- external:是否永久存在,设置为true则不会被清除,此时与timeout冲突,通常设置为false-->
<!-- diskPersistent:是否启用磁盘持久化-->
<!-- maxElementsInMemory:最大缓存数量-->
<!-- overflowToDisk:超过最大缓存数量是否持久化到磁盘-->
<!-- timeToIdleSeconds:最大不活动间隔,设置过长缓存容易溢出,设置过短无效果,可用于记录时效性数据,例如验证码-->
<!-- timeToLiveSeconds:最大存活时间-->
<!-- memoryStoreEvictionPolicy:缓存清除策略-->
<defaultCache
eternal="false"
diskPersistent="false"
maxElementsInMemory="1000"
overflowToDisk="false"
timeToIdleSeconds="10"
timeToLiveSeconds="10"
memoryStoreEvictionPolicy="LRU" />
<cache
name="smsCode"
eternal="false"
diskPersistent="false"
maxElementsInMemory="1000"
overflowToDisk="false"
timeToIdleSeconds="10"
timeToLiveSeconds="10"
memoryStoreEvictionPolicy="LRU" />
</ehcache>具体实现同上
定时任务
开启定时任务
@SpringBootApplication
@EnableScheduling // 开始定时任务
public class Springboot12TaskApplication {}添加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-quartz</artifactId>
</dependency>配置
spring:
task:
scheduling:
thread-name-prefix: spring_ # 线程前缀任务配置
@Configuration
public class QuartzConfig {
@Bean
public JobDetail jobDetail() {
// 绑定具体的工作
return JobBuilder.newJob(MyQuartz.class)
.storeDurably()
.build();
}
@Bean
public Trigger trigger() {
// 绑定具体的工作明细
ScheduleBuilder<? extends Trigger> schedBuilder = CronScheduleBuilder
.cronSchedule("0/5 * * * * ?");
return TriggerBuilder.newTrigger()
.forJob(jobDetail())
.withSchedule(schedBuilder)
.build();
}
}springboot提供的定时任务功能
@Component
public class MyBean {
@Scheduled(cron = "0/1 * * * * ?")
public void print() {
System.out.println(Thread.currentThread().getName() + "print...");
}
}Mail(邮件)
添加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>配置
spring:
mail:
username: 2475058223@qq.com
password: awxesgjpwgikdjjc
port: 587
host: smtp.qq.com具体实现
@Service
public class SendMailServiceImpl implements SendMailService {
@Autowired
private JavaMailSender javaMailSender;
private String from = "2475058223@qq.com";
private String to = "jiangdongyu18@163.com";
private String subject = "测试邮件";
private String content = "<img src='https://img1.baidu.com/it/u=2192429849,3208186763&fm=253&fmt=auto&app=138&f=JPEG?w=701&h=500'><a href=\"https://www.itcast.cn\">点开有惊喜</a>";
@Override
public void sendMail() {
/* SimpleMailMessage message = new SimpleMailMessage();
message.setFrom(from + "(小甜甜)");
message.setTo(to);
message.setSubject(subject);
message.setText(content);*/
try {
MimeMessage message = javaMailSender.createMimeMessage();
MimeMessageHelper mimeMessageHelper = new MimeMessageHelper(message, true);
mimeMessageHelper.setFrom(from + "(小甜甜)");
mimeMessageHelper.setTo(to);
mimeMessageHelper.setSubject(subject);
mimeMessageHelper.setText(content, true);
// 如何添加附件
File file = new File("D:\\jdy2002\\Idea\\SpringBoot2-Plus\\springboot-13-mail\\target\\springboot-13-mail-0.0.1-SNAPSHOT.jar");
mimeMessageHelper.addAttachment(file.getName(), file);
javaMailSender.send(message);
} catch (MessagingException e) {
e.printStackTrace();
}
}
}Admin
server
添加依赖
<dependency>
<groupId>de.codecentric</groupId>
<artifactId>spring-boot-admin-starter-server</artifactId>
<version>3.2.1</version>
</dependency>开启服务
@EnableAdminServer
public class SpringbootWebQuickstartApplication {}配置项
@Component
public class HealthConfig extends AbstractHealthIndicator {
// 显示运行时间
@Override
protected void doHealthCheck(Health.Builder builder) throws Exception {
builder.withDetail("runTime", System.currentTimeMillis())
.up();
}
}@Component
@Endpoint(id = "pay")
public class PayEndpoint {
@ReadOperation
public Object getPay() {
System.out.println("===================");
return "123";
}
}客户端
添加依赖
<dependency>
<groupId>de.codecentric</groupId>
<artifactId>spring-boot-admin-starter-client</artifactId>
<version>3.2.2</version>
</dependency>配置
server:
port: 80
spring:
boot:
admin:
client:
url: http://localhost:8080 # 客户端地址
management:
endpoint:
health:
show-details: always
info:
enabled: true
endpoints:
web:
exposure:
include: '*'
enabled-by-default: true
info:
author: itheima
appName: @project.artifactId@访问http://localhhost:8080即可
JWT验证码
导入依赖
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<version>2.3.1</version>
</dependency>生成jwt令牌
Map<String, Object> claims = new HashMap<>();
claims.put("id", 1);
claims.put("username", "jdy2002");
String jwt = Jwts.builder()
// 设置签名算法以及密钥
.signWith(SignatureAlgorithm.HS256, "jdy200255")
.setClaims(claims)
// 设置jwt令牌的有效期为一个小时
.setExpiration(new Date(System.currentTimeMillis() + 3600 * 1000))
.compact();
System.out.println(jwt);解析jwt
Claims payload = Jwts.parser()
// 设置密钥
.setSigningKey("jdy200255")
// 解析
.parseClaimsJws("eyJhbGciOiJIUzI1NiJ9.eyJpZCI6MSwiZXhwIjoxNzEwODUyNjg5LCJ1c2VybmFtZSI6ImpkeTIwMDIifQ.Vmng3ucSmBmJl6fGqM9qqMADzf25SAL2V4kVH7DqJyM")
// 获取payload
.getBody();
System.out.println(payload);Filter
配置过滤器
// 拦截所有请求
@WebFilter(urlPatterns = "/*")
public class DemoFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
System.out.println("初始化方法执行了");
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
System.out.println("我拦截到了请求");
// 放行
filterChain.doFilter(servletRequest, servletResponse);
}
@Override
public void destroy() {
System.out.println("销毁方法执行了");
}
}配置扫描
// 扫描servlet组件
@ServletComponentScan
@SpringBootApplication
public class StudyMybatisApplication {}Interceptor
拦截器配置类
@Component
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("执行前");
// 返回true放行
return HandlerInterceptor.super.preHandle(request, response, handler);
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
System.out.println("执行后");
HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
System.out.println("执行完成");
HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
}
}添加拦截器
@Configuration // 配置类
public class WebConfig implements WebMvcConfigurer {
@Autowired
private LoginInterceptor loginInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginInterceptor).addPathPatterns("/**");
// 排除
// .excludePathPatterns("");
}
}异常处理
定义异常处理类
/**
* 全局异常处理器
*/
@RestControllerAdvice
public class GlobalExceptionHandler {
// 捕获所有异常
@ExceptionHandler(Exception.class)
public Result ex(Exception e) {
e.printStackTrace();
return Result.error(e.getMessage());
}
}@Transcational
事务处理,Spring事务管理
显示日志
logging:
level:
org.springframework.jdbc.support.JdbcTransactionManager: debug只有RuntimeException才支持自动回滚
所有异常都自动回滚
@Transcational(rollbackFor=Exception.class)
事务传播行为
在事务中调用其他方法,是否开启一个新的事务,默认处于同个事务
@Transcational(propagation= Proppagation.REQUIRES_NEW)
AOP
导入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>execution
切入点:
"execution( * void com.example.*Service.delete(..) throws)"
使用||可以同时指定两个配置规则
*代表任意字符
..代表任意包或任意参数
@Slf4j
@Component
@Aspect // AOP类
public class TimeAspect {
// 切入点
@PointCut("execution(xxxxxx) || execution(xxx)")
// 设置private表示只有当前切面可以使用,设置public其他切面也可以使用
// @Before("xxx.xxx.xxx.TimeAspect.pt")
private void pt() {}
@Before("pt()")
public void before() {
}
// 记录方法执行时间
@Around("execution(* com.example.controller.*.*(..))") // 切入点表达式
public Object recordTime(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
long start = System.currentTimeMillis();
// 调用原始方阿飞
Object result = proceedingJoinPoint.proceed();
// 记录结束时间
long end = System.currentTimeMillis();
log.info(proceedingJoinPoint.getSignature() + "方法执行耗时 {}ms", end - start);
return result;
}
}annotation
"annotation(注解全路径类名)"