0%

创建 JDBC 链接

项目的 pom 文件中添加驱动引用

1
2
// https://mvnrepository.com/artifact/mysql/mysql-connector-java
implementation group: 'mysql', name: 'mysql-connector-java', version: '8.0.28'

在 JDBC 实现中,我们通过类似如下代码得到连接信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class JdbcConnectionTest {
public static final String URL = "jdbc:mysql://localhost:3306/mybatis";
public static final String USER = "root";
public static final String PASSWORD = "123456";

public static void main(String[] args) throws Exception {
//1.加载驱动程序
Class.forName("com.mysql.jdbc.Driver");
//2. 获得数据库连接
Connection conn = DriverManager.getConnection(URL, USER, PASSWORD);
//3.操作数据库,实现增删改查
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("SELECT id, name, pwd FROM user");
//如果有数据,rs.next()返回true
while (rs.next()) {
System.out.println(rs.getInt("id") + ";" + rs.getString("name") + ";" + rs.getInt("pwd"));
}
}
}

在 mybatis 中,这些信息都是写在核心配置文件 xml 中的。样板如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/mybatis?..."/>
<property name="username" value="root"/>
<property name="password" value="12345678"/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="com/jzheng/mapper/UserMapper.xml"/>
</mappers>
</configuration>

加载相关的代码

1
2
3
4
5
String resource = "mybatis-config.xml";
// 获取文件流
InputStream inputStream = Resources.getResourceAsStream(resource);
// 构建工厂
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

想要了解的点:

  1. mybatis 是如何解析 xml 的 - 写一篇 Builder pattern 的文章,解析的时候重度使用这种模式
  2. 在解析的时候都塞了一些什么东西

记录一下代理模式的学习路径。代理模式常用的两种形式:静态代理,动态代理。其中,动态代理在两个国民级框架 mybatis 和 spring 中都有用到。

代理模式的定义:为其他对象提供一种代理以控制对这个对象的访问。在某些情况下,一个对象不适合或者不能直接引用另一个对象,而代理对象可以在客户端和目标对象之间起到中介的作用。

通过代理模式我们可以:

  1. 隐藏委托类的具体实现
  2. 客户和委托类解偶,在不改变委托类的情况下添加额外功能

插入类图 Here…

这里我们举一个生活中常见的例子,外卖小哥。在这个情境下,外卖小哥就是我们的代理。帮我们执行买餐这个动作。同时作为扩展,它还可以帮我们买烟买水,倒垃圾等。。。虽然我不提倡这种做法,只用于举例,无伤大雅。

PS: 最近更新了 Spring AOP 的 post,那边关于 Proxy 的 Java 实现会有更多的记载

静态代理

公共接口,用来点单

1
2
3
public interface Order {
void order();
}

客户实现,这个类代表叫外卖的人

1
2
3
4
5
6
7
public class Customer implements Order {

@Override
public void order() {
System.out.println("Order and pay money...");
}
}

外卖小哥类

1
2
3
4
5
6
7
8
9
10
11
12
public class DeliveryGuy implements Order {
private Customer customer;

public DeliveryGuy(Customer customer) {
this.customer = customer;
}

@Override
public void order() {
customer.order();
}
}

客户端调用

1
2
3
4
5
6
7
public class Client {
public static void main(String[] args) {
Customer customer = new Customer();
Order order = new DeliveryGuy(customer);
order.order();
}
}

优点:

  1. 简单直接
  2. 解偶
  3. 代理类扩展业务方便

缺点:

每个业务都需要一个代理类,冗余代码很多

动态代理

常见的有两种方式:JDK 原生动态代理和 CGLib 动态代理,这里只介绍第一种。

JDK 根据代理模式的特性,制定了一套规范,参照他的规范,可以在很方便的在运行时产生代理类代码,而不需要在编译器写源码,更方便,当然代价就是增加了学习成本,代码不像之前那么一目了然了。实现时主要依赖两个 reflect 下的原生类 Proxy 和 InvocationHandler。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class LogHandler implements InvocationHandler {
private Object target;

public LogHandler(Object target) {
this.target = target;
}

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
before();
Object obj = method.invoke(target, args);
after();
return obj;
}

private void before() {
System.out.println("buy something...");
}

private void after() {
System.out.println("take out the trash...");
}
}

public class Client {
public static void main(String[] args) {
LogHandler logHandler = new LogHandler(new Customer());
Order order = (Order) (Proxy.newProxyInstance(Order.class.getClassLoader(), new Class[] {Order.class}, logHandler));
order.order();
}
}

LogHandler 的实现中 invok 的是要要特别注意一下,method.invoke 的参数是target。我一开始直接把 proxy 但参数传入了,排查了好久 (; ̄ェ ̄)

中的来说没什么难度,最花时间的部分是熟悉这种使用方式,第一次理解起来可能花点时间。

  1. fork 项目,下载到本地
  2. 新建分支,修改代码
  3. 命令行提交改动,push 到远端
  4. 登陆到 github,查看 fork 的项目,手动选择将改动作为一个 PR 推送到你原始项目

打完收工~

MyBatis 是一款优秀的持久层框架,它支持自定义 SQL、存储过程以及高级映射。MyBatis 免除了几乎所有的 JDBC 代码以及设置参数和获取结果集的工作。MyBatis 可以通过简单的 XML 或注解来配置和映射原始类型、接口和 Java POJO(Plain Old Java Objects,普通老式 Java 对象)为数据库中的记录。

解释成白话:这是一个操作数据库的框架,就是把操作简化了,你之前用 JDBC 时的那些配置什么还是少不了只不过用起来更好使罢了。比如使用数据库你得配联接吧,得配驱动把,得写 SQL 把,mybatis 也需要你做这个,只不过人家帮你把这些事情总结出了一个套路,你用这个套路就可以少很多冗余代码,但是也增加了你自己学习这个框架的成本,少了自由度。当然就大部分人的编程水平,肯定是收益大于损失的 ╮( ̄▽ ̄””)╭

原型 JDBC 操作数据库

  1. 导入 mysql 包
  2. 编写实体类
  3. 编写驱动类
  4. 编写 Dao 类
  5. 测试
1
2
3
4
5
6
@Data
public class User {
private int id;
private String name;
private String pwd;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public class DBUtils {

private static final String URL = "jdbc:mysql://localhost:3306/mybatis";
private static final String NAME = "root";
private static final String PASSWORD = "12345678";

private static Connection conn = null;

static {
//1.加载驱动程序
try {
Class.forName("com.mysql.jdbc.Driver");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}

//2.获得数据库的连接
try {
conn = DriverManager.getConnection(URL, NAME, PASSWORD);
} catch (SQLException throwables) {
throwables.printStackTrace();
}
}

public static Connection getConnection() {
return conn;
}

public static void main(String[] args) throws Exception {
//3.通过数据库的连接操作数据库,实现增删改查
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("select id, name, pwd from user");//选择import java.sql.ResultSet;
while (rs.next()) {//如果对象中有数据,就会循环打印出来
System.out.println("Result: [" + rs.getInt("id") + ", " + rs.getString("name") + ", " + rs.getString("pwd") + "]");
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class UserDao {

List<User> getUsers() throws SQLException {
List<User> users = new ArrayList<>();
Connection connection = DBUtils.getConnection();
Statement statement = connection.createStatement();
ResultSet rs = statement.executeQuery("select id, name, pwd from user");

while (rs.next()) {
User tmp = new User();
tmp.setId(rs.getInt("id"));
tmp.setName(rs.getString("name"));
tmp.setPwd(rs.getString("pwd"));
users.add(tmp);
}
return users;
}
}
1
2
3
4
5
6
7
8
9
public class UserDaoTest {
public static void main(String[] args) throws SQLException {
UserDao dao = new UserDao();
List<User> users = dao.getUsers();
for (User user : users) {
System.out.println(user);
}
}
}

mybatis 为我们做的只不过是把上面的这些步骤简化了,通过配置文件管理连接信息,通过 factory, SqlSession 等来管理 SQL 执行等。按照这样的思路去理解记忆应该会更加有效率。

搭建环境 mybatis-01-setup

对照官方文档的入门篇

创建测试表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
-- 创建测试数据库
CREATE DATABASE mybatis;
USE mybatis;

-- 创建测试表
CREATE TABLE user (
id INT(20) NOT NULL PRIMARY KEY,
name VARCHAR(30) DEFAULT NULL,
pwd VARCHAR(30) DEFAULT NULL
)ENGINE=INNODB DEFAULT CHARSET=utf8;

-- 插入数据
INSERT INTO user (id, name, pwd) VALUES
(1, 'jack', '123'), (2, 'jack02', '123');

最简项目树

1
2
3
4
5
6
7
8
9
10
11
.
├── java
│ └── com
│ └── jzheng
│ ├── dao
│ │ ├── UserMapper.java
│ │ └── UserMapper.xml
│ └── pojo
│ └── User.java
└── resources
└── mybatis-config.xml

新建测试项目

  1. 新建 maven 项目
  2. 删除 src 目录,通过 module 的方式管理,条理更清楚
  3. 配置依赖
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
<?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">
<modelVersion>4.0.0</modelVersion>

<groupId>com.jzheng</groupId>
<artifactId>mybatis-note</artifactId>
<packaging>pom</packaging>
<version>1.0-SNAPSHOT</version>
<modules>
<module>mybatis-01-setup</module>
</modules>

<!-- java 8 compiler 配置,和下面的 build plugin 配合使用 -->
<properties>
<maven.compiler.target>1.8</maven.compiler.target>
<maven.compiler.source>1.8</maven.compiler.source>
</properties>

<!-- mybatis 基础包,包括 DB 驱动,连接,测试的 jar 包 -->
<dependencies>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.46</version>
</dependency>

<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.2</version>
</dependency>

<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>

</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
</plugins>
<!-- 在 build 的时候将工程中的配置文件也一并 copy 到编译文件中,即 target 文件夹下 -->
<resources>
<resource>
<directory>src/main/resources</directory>
<includes>
<include>**/*.properties</include>
<include>**/*.xml</include>
</includes>
</resource>
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.properties</include>
<include>**/*.xml</include>
</includes>
<filtering>true</filtering>
</resource>
</resources>
</build>

</project>

配置 idea 链接本地 mysql 报错 Server returns invalid timezone. Go to 'Advanced' tab and set 'serverTimezone' property manually.

时区错误,MySQL默认的时区是UTC时区,比北京时间晚8个小时。在mysql的命令模式下,输入 set global time_zone='+8:00'; 即可

连接后点击扳手图标可以拿到 url 信息

mybatis 核心配置文件,这个文件中配置 DB 连接,驱动等信息,算是 mybatis 的入口配置文件了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<!-- 核心配置文件 -->
<configuration>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/mybatis?useSSL=false&amp;useUnicode=true&amp;characterEncoding=UTF-8&amp;serverTime=UTC"/>
<property name="username" value="root"/>
<property name="password" value="root"/>
</dataSource>
</environment>
</environments>

<mappers>
<mapper resource="com/jzheng/mapper/UserMapper.xml"/>
</mappers>
</configuration>

编写工具类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class MybatisUtils {
private static SqlSessionFactory sqlSessionFactory;
static {
String resource = "mybatis-config.xml";
InputStream inputStream = null;
try {
inputStream = Resources.getResourceAsStream(resource);
} catch (IOException e) {
e.printStackTrace();
}
sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
}

public static SqlSession getSqlSession() {
return sqlSessionFactory.openSession();
}

}

生成实体类 pojo

1
2
3
4
5
6
7
8
public class User {
private int id;
private String name;
private String pwd;

// ...
// 省略构造函数和 getter/setter
}

定义 Dao 接口

1
2
3
4
5
6
7
8
9
10
public interface UserMapper {
// CURD user
int addUser(User user);
int deleteUser(int id);
int updateUser(User user);
User getUserById(int id);

// First sample
List<User> getUsers();
}

配置 Mapper xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<?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.jzheng.mapper.UserMapper">
<insert id="addUser" parameterType="com.jzheng.pojo.User">
insert into mybatis.user (id, name, pwd) values (#{id}, #{name}, #{pwd})
</insert>

<delete id="deleteUser">
delete from mybatis.user where id=#{id};
</delete>

<update id="updateUser" parameterType="com.jzheng.pojo.User">
update mybatis.user set name=#{name}, pwd=#{pwd} where id=#{id};
</update>

<select id="getUserById" resultType="com.jzheng.pojo.User">
select * from mybatis.user where id=#{id};
</select>

<!-- 查询所有用户 -->
<select id="getUsers" resultType="com.jzheng.pojo.User">
select * from mybatis.user;
</select>
</mapper>

编写测试类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
public class UserMapperTest {
@Test
public void test_official_sample() throws IOException {
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession session = sqlSessionFactory.openSession();
UserMapper mapper = session.getMapper(UserMapper.class);
List<User> users = mapper.getUsers();
for (User user : users) {
System.out.println(user);
}
session.close();
}

@Test
public void test_util() {
SqlSession sqlSession = MybatisUtils.getSqlSession();
List<User> users = sqlSession.getMapper(UserMapper.class).getUsers();
for (User user : users) {
System.out.println(user);
}
sqlSession.close();
}

@Test
public void test_add() {
SqlSession sqlSession = MybatisUtils.getSqlSession();
User user = new User(5, "t0928", "pwd");

int ret = sqlSession.getMapper(UserMapper.class).addUser(user);
System.out.println(ret);
sqlSession.commit();
sqlSession.close();
}

@Test
public void test_delete() {
SqlSession sqlSession = MybatisUtils.getSqlSession();

int ret = sqlSession.getMapper(UserMapper.class).deleteUser(5);
System.out.println(ret);
sqlSession.commit();
sqlSession.close();
}

@Test
public void test_update() {
SqlSession sqlSession = MybatisUtils.getSqlSession();
User user = new User(2, "change", "pwdchange");
int ret = sqlSession.getMapper(UserMapper.class).updateUser(user);
System.out.println(ret);
sqlSession.commit();
sqlSession.close();
}

@Test
public void test_getUserById() {
SqlSession sqlSession = MybatisUtils.getSqlSession();
User ret = sqlSession.getMapper(UserMapper.class).getUserById(1);
System.out.println(ret);
sqlSession.close();
}
}

常见错误

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
org.apache.ibatis.binding.BindingException: Type interface com.jzheng.dao.UserDao is not known to the MapperRegistry.

-- 核心配置文件没有配置 mapper 路径

Caused by: java.io.IOException: Could not find resource com/jzheng/dao/UserMapper.xml
at org.apache.ibatis.io.Resources.getResourceAsStream(Resources.java:114)
at org.apache.ibatis.io.Resources.getResourceAsStream(Resources.java:100)
at org.apache.ibatis.builder.xml.XMLConfigBuilder.mapperElement(XMLConfigBuilder.java:372)
at org.apache.ibatis.builder.xml.XMLConfigBuilder.parseConfiguration(XMLConfigBuilder.java:119)
... 27 more

-- maven 约定大于配置,默认指挥将 resources 下面的 xml 导出到 target, 如果需要将 java 下的配置文件到处需要再 pom.xml 下的 build tag 里加点配置

<build>
<resources>
<resource>
<directory>src/main/resources</directory>
<includes>
<include>**/*.properties</include>
<include>**/*.xml</include>
</includes>
</resource>
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.properties</include>
<include>**/*.xml</include>
</includes>
<filtering>true</filtering>
</resource>
</resources>
</build>

java.security.cert.CertPathValidatorException: Path does not chain with any of the trust anchors

-- 链接配置问题,可以把 useSSL 改为 false

[Attention]:

  1. 当进行增删改操作时需调用 commit 方法将修改提交才能生效
  2. namespace 中的包名要和 Dao/mapper 保持一致

万能 map

如果实体类的属性过多,可以考虑使用 map 传递参数, 这是一种可定制性很高的用法

1
2
// Mapper interface
User getUserByMap(Map map);
1
2
3
4
<!-- 通过 map 查询 -->
<select id="getUserByMap" parameterType="map" resultType="com.jzheng.pojo.User">
select * from mybatis.user where id=#{id};
</select>

测试用例

1
2
3
4
5
6
7
8
9
10
@Test
public void test_getUserByMap() {
SqlSession sqlSession = MybatisUtils.getSqlSession();

Map<String, Object> map = new HashMap<>();
map.put("id", 1);
User ret = sqlSession.getMapper(UserMapper.class).getUserByMap(map);
System.out.println(ret);
sqlSession.close();
}

分页功能 limit

通过 map 来实现分页功能

1
select * from table limit startIndex, size;
1
2
// Limit query
List<User> getUsersWithLimit(Map map);
1
2
3
4
<!-- 分页 -->
<select id="getUsersWithLimit" parameterType="map" resultType="com.jzheng.pojo.User">
select * from mybatis.user limit #{startIndex}, #{pageSize};
</select>

常用变量的作用域

SqlSessionFactoryBuilder: 一用完就可以丢了,局部变量

SqlSessionFactory: 应用起了就要应该存在,所以应用作用域(Application)最合适。而且只需要一份,使用单列或者静态单列模式

SqlSession: 线程不安全,不能共享。最佳作用域是请求或方法层。响应结束后,一定要关闭,所以最佳时间是把它放到 finally 代码块中,或者用自动关闭资源的 try block。

疑问记录

  1. 项目中我即使把 pojo 的构造函数和 getter/setter 都注视掉了,值还是被塞进去了,和 spring 不一样,他是怎么实现的?
  2. 核心配置文件中的 mapper setting,resource tag 不支持匹配符?类似 com/jzheng/mapper/*.xml 并不能生效
  3. mapper.xml 中 resultType 怎么简写,每次都全路径很费事
  4. mybatis 中是不支持方法重载的

疑问解答

  1. mybatis 会通过 DefaultResultSetHandler 处理结果集,applyAutomaticMappings 就是进行映射的地方,这个方法下面会通过反射对 field 进行赋值,并没有调用 set 方法,别和 spring 搞混了。
  2. TBD
  3. 参见 配置 -> typeAlias

Lombok 偷懒神器

Lombok 可以省去你很多冗余代码,在测试项目的时候很好用。是否使用看个人,但是就个人小项目来说我还是很愿意使用的。

  1. Idea 安装 lombok 插件
  2. 安装依赖的 jar 包
  3. 在 pojo 类中添加注解使用

调试技巧:在 pojo 上添加注解后,你可以在 idea 的 Structure tab 里看到新生产的方法

配置解析 mybatis-02-configuration

对应 配置 章节

核心配置文件:mybatis-config.xml

1
2
3
4
5
6
7
8
9
10
11
12
properties(属性)
settings(设置)
typeAliases(类型别名)
typeHandlers(类型处理器)
objectFactory(对象工厂)
plugins(插件)
environments(环境配置)
environment(环境变量)
transactionManager(事务管理器)
dataSource(数据源)
databaseIdProvider(数据库厂商标识)
mappers(映射器)

environments 环境变量

尽管可以配置多个环境,但每个 SqlSessionFactory 实例只能选择一种环境。如果想连接两个数据库就需要创建两个 SqlSessionFactory 实例。

事务管理器(transactionManager)有 JDBC 和 MANAGED 两种,默认使用 JDBC,另一种几乎很少用,权作了解。

数据源(dataSource)用来配置数据库连接对象的资源,有 [UNPOOLED|POOLED|JNDI] 三种。JNDI 是为了支持 EJB 应用,现在应该已经过时了。

DB Pool 的常见实现方式:jdbc,c3p0, dbcp

properties 属性

引用配置文件,可以和 .properties 文件交互

文件目录如下:

1
2
3
resources
├── db.properties
└── mybatis-config.xml

db.properties

1
2
driver=com.mysql.jdbc.Driver
url=jdbc:mysql://localhost:3306/mybatis?useSSL=false&useUnicode=true&characterEncoding=UTF-8&serverTime=UTC

mybatis-config 配置如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<?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 resource="db.properties">
<property name="uname" value="root"/>
<!-- priority rank: parameter > properties file > property tab -->
<property name="url" value="tmp_url"/>
</properties>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="${driver}"/>
<property name="url" value="${url}"/>
<property name="username" value="${uname}"/>
<property name="password" value="12345678"/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="com/jzheng/mapper/UserMapper.xml"/>
</mappers>
</configuration>

xml 中的 properties tag + resource 属性可以将配置文件加载进来。另外还有一种属性配置方式是直接在构建 session factory 或者 factory builder 的时候通过参数的形式传入。

1
2
3
sqlSessionFactoryBuilder.build(reader, props);
// ... or ...
new SqlSessionFactoryBuilder.build(reader, environment, props);

三种属性添加方式优先级:parameter > properties 文件 > property 标签

typeAlias 类型别名

设置短的名字,减少类完全限定名的冗余

1
2
3
4
5
6
7
<typeAliases>
<typeAlias type="com.jzheng.pojo.User" alias="User"/>
</typeAliases>

<typeAliases>
<package name="com.jzheng.pojo"/>
</typeAliases>

也可以在实体类上添加 Alias 注解

1
2
@Alias("user")
public class User {}

三种添加别名的方式 typeAliases+typeAlias, typeAliases+package 和 类名+@Alias。想要使用缩写必须在配置文件中加上 typeAliases 的 tag 直接在类上使用注解是不会生效的。

typeAliases 使用时,是忽略大小写的,官方提倡使用首字母小写的命名方式。一旦类傻上加了注解,则严格匹配类注解

setting 设置

比较常用的设置为:

  • cacheEnabled:开启缓存配置
  • logImpl:开启日志配置

mapper 映射器

映射器用来告诉 mybatis 到哪里去找到映射文件

方式一:资源文件

1
2
3
<mappers>
<mapper resource="com/jzheng/dao/UserMapper.xml"/>
</mappers>

方式二:使用 class 绑定

1
2
3
<mappers>
<mapper class="com.jzheng.dao.UserMapper"/>
</mappers>

限制:

  1. 接口和 mapper 必须重名
  2. 接口和 mapper 必须要同意路径下

方式三:包扫描

1
2
3
<mappers>
<package name="com.jzheng.dao"/>
</mappers>

缺陷也是要在同一路径下

每个 Mapper 代表一个具体的业务,比如 UserMapper。

解决属性名和字段名字不一样的问题

将 User 的 pwd 改为 password, 和 DB 产生歧义

1
2
3
4
5
6
@Data
public class User {
private int id;
private String name;
private String password;
}

解决方案01, 在 Sql 中使用 as 关键字重新指定 column name 为 property name(pwd as password)。

1
2
3
<select id="getUserById" parameterType="int" resultType="user">
select id, name, pwd as password from mybatis.user where id = #{id};
</select>

解决方案02, 使用 resultMap 映射结果集

1
2
3
4
5
6
7
8
9
<resultMap id="UserMap" type="User">
<!-- column: db 字段, property: 实体类属性 -->
<result column="id" property="id"/>
<result column="name" property="name"/>
<result column="pwd" property="password"/>
</resultMap>
<select id="getUserById" parameterType="int" resultMap="UserMap">
select * from mybatis.user where id = #{id};
</select>

ResultMap 的设计思想是,对于简单的语句根本不需要配置显示的结果集映射,对于复杂的语句只需要描述他们的关系就行了。

上面的方案还可以将 id, name 的描述简化掉,框架会帮你处理,只保留不一致的即可

疑问记录

  1. 在测试属性和数据库名字不一样的案例的时候发现,就算不一样,但是如果有构造函数的话,还是会被赋值,但是顺序会被强制指定,如果我构造为 User(id,password) 则 User 的 name 会被赋值成 pwd, 应该和底层实现有关系

日志 mybatis-03-logging

支持的 log framework 类型

  • SLF4J [Y]
  • LOG4J
  • LOG4J2 [Y]
  • JDK_LOGGING
  • COMMONS_LOGGING
  • STDOUT_LOGGING [Y]
  • NO_LOGGING

STDOUT_LOGGING 是自带的 log 包,直接 enable 就能使用,使能后可以在 log 中看到运行的 SQL。

1
2
3
<settings>
<setting name="logImpl" value="STDOUT_LOGGING"/>
</settings>
1
2
3
4
5
6
7
8
9
10
11
12
Opening JDBC Connection
Created connection 477376212.
Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@1c742ed4]
==> Preparing: select * from mybatis.user where id = ?;
==> Parameters: 1(Integer)
<== Columns: id, name, pwd
<== Row: 1, jack, 123
<== Total: 1
User{id=1, name='jack', password='123'}
Resetting autocommit to true on JDBC Connection [com.mysql.jdbc.JDBC4Connection@1c742ed4]
Closing JDBC Connection [com.mysql.jdbc.JDBC4Connection@1c742ed4]
Returned connection 477376212 to pool.

开启 log4j 支持

log4j 是一个比较常用的日志框架,有很多功能,比如定制格式,指定存到文件等

  1. 导包
  2. 添加 log4j.properties
  3. 添加配置到核心配置文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 全局日志配置
log4j.rootLogger=DEBUG,console,file

#控制台输出的相关设置
log4j.appender.console = org.apache.log4j.ConsoleAppender
log4j.appender.console.Target = System.out
log4j.appender.console.Threshold=DEBUG
log4j.appender.console.layout = org.apache.log4j.PatternLayout
log4j.appender.console.layout.ConversionPattern=[%c]-%m%n

#文件输出的相关设置
log4j.appender.file = org.apache.log4j.RollingFileAppender
log4j.appender.file.File=./log/mybatis-03-logging.log
log4j.appender.file.MaxFileSize=10mb
log4j.appender.file.Threshold=DEBUG
log4j.appender.file.layout=org.apache.log4j.PatternLayout
log4j.appender.file.layout.ConversionPattern=[%p][%d{yy-MM-dd}][%c]%m%n

#日志输出级别
log4j.logger.org.mybatis=DEBUG
log4j.logger.java.sql=DEBUG
log4j.logger.java.sql.Statement=DEBUG
log4j.logger.java.sql.ResultSet=DEBUG
log4j.logger.java.sql.PreparedStatement=DEBUG

使能配置

1
2
3
<settings>
<setting name="logImpl" value="LOG4J"/>
</settings>

基于注解开发

基于注解开发,在应对简单的需求时还是很高效的,但是不能处理复杂的 SQL。

面向接口编程:

  • 接口定义和实现分离
  • 反映出设计人员对系统的抽象理解

接口有两类:一类是对一个个体的抽象,可以对应为一个抽象个体,另一类是对一个个体的某一方面抽象,即形成一个抽象面

个体可能有多个抽象面,抽象提与抽象面是有区别的

  1. 在接口方法上添加注解
  2. 在核心配置文件中添加配置
1
2
3
4
public interface UserMapper {
@Select("select * from user")
List<User> getUsers();
}
1
2
3
<mappers>
<mapper class="com.jzheng.dao.UserMapper"/>
</mappers>

PS: 注解和 xml 中对同一个接口只能有一种实现,如果重复实现,会抛异常

1
Caused by: java.lang.IllegalArgumentException: Mapped Statements collection already contains value for com.jzheng.mapper.UserMapper.getUserById. please check com/jzheng/mapper/UserMapper.xml and com/jzheng/mapper/UserMapper.java (best guess)

注解模式的实现机制:反射 + 动态代理

注解和配置文件是可以共存的,只要命名相同,并且实现方法没有冲突就行。

注解版 CRUD

工具类自动提交事务可以通过 Utils 类中,指定参数实现。注解版的 CRUD 基本上和 xml 版本的一样,只不过在注解版中,他的参数类型通过 @Param 指定。

1
2
3
public static SqlSession getSqlSession() {
return sqlSessionFactory.openSession(true);
}

实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public interface UserMapper {

@Select("select * from user")
List<User> getUsers();

// 方法存在多个参数,所有参数前面必须加上 @Param
@Select("select * from user where id=#{id}")
User getUserById(@Param("id") int id);

// 当参数是对象时,直接传入即可,保证属性名一致
@Insert("insert into user (id, name, pwd) values (#{id}, #{name}, #{password})")
int addUser(User user);

@Update("update user set name=#{name}, pwd=#{password} where id=#{id}")
int updateUser(User user);

@Delete("delete from user where id=#{id}")
int deleteUser(@Param("id") int id);
}

关于 @Param 注解

  • 基本类型 + String 类型需要加
  • 引用类型不需要
  • 如果只有一个基本类型,可以不加,但还是建议加上
  • Sql 中引用的属性名和 Param 中的名字保持一致

‘#’ 前缀可以防注入,’$’ 不行

Mybatis 执行流程解析

  1. Resources 获取加载全局配置文件
  2. 实例化 SqlSessionFactoryBuilder 构造器
  3. 解析配置文件流 XMLConfigBulder
  4. Configuration 所有的配置信息
  5. SqlSessionFactory 实例化
  6. Transaction 事务管理器
  7. 创建 executor 执行器
  8. 创建 SQLSession
  9. 实现 CRUD
  10. 查看是否成功

高级结果映射

多对一 - 关联 - association

一对多 - 集合 - collection

创建测试表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
CREATE TABLE `teacher` (
`id` INT(10) NOT NULL,
`name` VARCHAR(30) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=INNODB DEFAULT CHARSET=utf8;

INSERT INTO teacher(`id`, `name`) VALUES (1, '秦老师');

CREATE TABLE `student` (
`id` INT(10) NOT NULL,
`name` VARCHAR(30) DEFAULT NULL,
`tid` INT(10) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `fktid` (`tid`),
CONSTRAINT `fktid` FOREIGN KEY (`tid`) REFERENCES `teacher` (`id`)
) ENGINE=INNODB DEFAULT CHARSET=utf8;

INSERT INTO `student` (`id`, `name`, `tid`) VALUES ('1', '小明', '1');
INSERT INTO `student` (`id`, `name`, `tid`) VALUES ('2', '小红', '1');
INSERT INTO `student` (`id`, `name`, `tid`) VALUES ('3', '小张', '1');
INSERT INTO `student` (`id`, `name`, `tid`) VALUES ('4', '小李', '1');
INSERT INTO `student` (`id`, `name`, `tid`) VALUES ('5', '小王', '1');

测试环境搭建

  1. 新建表,准备测试数据
  2. 新建 teacher/student 实体类
  3. 创建 mapper 接口
  4. 创建 mapper xml 文件
  5. 核心配置类注册接口或 xml
  6. 测试查询

多对一 mybatis-05-resultmap

在这里采用多个学生对应一个老师的情况作为案例,为了更好的面向对象 Student pojo 需要做一些修改,我们把 teach id 用对象来代替

1
2
3
4
5
6
@Data
public class Student {
private int id;
private String name;
private Teacher teacher;
}

我们想要实现的效果其实就是子查询 SELECT st.id, st.name, te.name as tname from student st, teacher te where st.tid = te.id;

关键点:使用 association tag 作为连接键

按照查询嵌套处理

  1. 直接写查询所有学生信息的语句,结果集自定义
  2. 根据自定义的结果集,将 teacher 对象和 tid 绑定
  3. 定义根据 tid 查询 teacher 的语句
  4. Mybatis 会自动将查询到的 teacher 对象整合到学生的查询结果中
1
2
3
4
5
6
7
8
9
10
11
12
<select id="getStudent" resultMap="StudentTeacher">
select * from student;
</select>

<resultMap id="StudentTeacher" type="Student">
<!-- obj use association, collection use collection -->
<association property="teacher" column="tid" javaType="Teacher" select="getTeacher"/>
</resultMap>

<select id="getTeacher" resultType="Teacher">
select * from teacher where id=#{id}
</select>

按照结果嵌套处理

这种方法的查询更加直接,和上面给出的 SQL 基本一致,就是 association 部分的匹配看着有点懵,大概是 mybatis 底层都会根据 column name 做匹配的,但是这里查询的时候 teacher 的 name 字段重命名为 tname 了所以要显示的重新匹配一下。

1
2
3
4
5
6
7
8
9
10
11
12
<select id="getStudent2" resultMap="StudentTeacher2">
select s.id sid, s.name sname, t.name tname from student s, teacher t
where s.tid = tid;
</select>

<resultMap id="StudentTeacher2" type="Student">
<result property="id" column="sid"/>
<result property="name" column="sname"/>
<association property="teacher" javaType="Teacher">
<result property="name" column="tname"/>
</association>
</resultMap>

对应 SQL 的子查询和联表查询

一对多 mybatis-05-resultmap02

一个老师对应多个学生为案例, 代码和思路和上面的多对一其实没什么区别,就是关键字变了一下。。。

关键字:collection tag

实体类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Data
public class Teacher {
private int id;
private String name;

private List<Student> students;
}

@Data
public class Student {
private int id;
private String name;
private int tid;
}

按照结果嵌套处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<select id="getTeacher" resultMap="TeacherStudent">
select s.id sid, s.name sname, t.name tname, t.id tid from student s, teacher t
where s.tid = t.id and t.id=#{tid};
</select>

<resultMap id="TeacherStudent" type="Teacher">
<result property="id" column="tid"/>
<result property="name" column="tname"/>
<collection property="students" ofType="Student">
<result property="id" column="sid"/>
<result property="name" column="sname"/>
<result property="tid" column="tid"/>
</collection>
</resultMap>

按照查询嵌套处理

1
2
3
4
5
6
7
8
9
10
<select id="getTeachers" resultMap="TeacherStudent">
select * from mybatis.teacher;
</select>
<resultMap id="TeacherStudent" type="Teacher">
<collection property="students" ofType="Student" column="id" javaType="ArrayList" select="getStudents"/>
</resultMap>

<select id="getStudents" resultType="Student">
select * from mybatis.student where tid=#{id}
</select>

小结:

  • 关联 - 一对多 - associate
  • 集合 - 多对一 - collection
  • javaType & ofType
    • javaType 指定实体类中的属性
    • ofType 指定映射到集合中的 pojo 类型,泛型中的约束类型

注意点:

  • 保证SQL可读性,尽量通俗易懂
  • 注意一对多和多对一属性名和字段的问题
  • 排错时善用 log

面试高频

  • Mysql 引擎
  • InnoDB 底层原理
  • 索引
  • 索引优化

动态 SQL mybatis-06-dynamic-sql

根据不同的条件生成不同的 SQL 语句

  • if
  • choose (when, otherwise)
  • trim (where, set)
  • foreach

搭建环境

1
2
3
4
5
6
7
CREATE TABLE `blog`(
`id` VARCHAR(50) NOT NULL COMMENT '博客id',
`title` VARCHAR(100) NOT NULL COMMENT '博客标题',
`author` VARCHAR(30) NOT NULL COMMENT '博客作者',
`create_time` DATETIME NOT NULL COMMENT '创建时间',
`views` INT(30) NOT NULL COMMENT '浏览量'
)ENGINE=INNODB DEFAULT CHARSET=utf8
  1. 导包
  2. 编写配置
  3. 编写实体类
  4. 编写 mapper + 测试
1
2
3
4
5
6
7
8
@Data
public class Blog {
private String id;
private String title;
private String author;
private Date createTime;
private int views;
}

if

1
2
3
4
5
6
7
8
9
<select id="queryBlogIf" parameterType="map" resultType="blog">
select * from mybatis.blog where 1=1
<if test="title != null">
and title=#{title}
</if>
<if test="author != null">
and author=#{author}
</if>
</select>

choose (when, otherwise), 这种判断语句更贴近 java 中的 switch-case,在 if 中,所有符合 test 判断的条件都会被添加进去,但是在 choose 中,他只会从众多条件中选择一种

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<select id="queryBlogChoose" parameterType="map" resultType="blog">
select * from mybatis.blog
<where>
<choose>
<when test="title != null">
title = #{title}
</when>
<when test="author != null">
and author = #{author}
</when>
<otherwise>
and views = #{views}
</otherwise>
</choose>
</where>
</select>

trim (where, set), where 可以对 xml 中定义的 and + where 冗余情况进行判断,只在需要的时候才添加这个关键字,同理 set 会处理 set + ,的情况

PS: 添加 set 标签的时候 , 是一定要加的,多余的 , 框架会帮你去掉,少了可不行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<select id="queryBlogIf" parameterType="map" resultType="blog">
select * from mybatis.blog
<where>
<if test="title != null">
and title=#{title}
</if>
<if test="author != null">
and author=#{author}
</if>
</where>
</select>

<update id="updateBlog" parameterType="map">
update mybatis.blog
<set>
<if test="title != null">
title=#{title},
</if>
<if test="author!=null">
author = #{author}
</if>
</set>
where id=#{id}
</update>

foreach 可以用来处理类似 SELECT * from blog where id in ("1", "2", "3"); 的 SQL

1
2
3
4
5
6
7
8
<select id="queryBlogs" parameterType="map" resultType="blog">
select * from mybatis.blog
<where>
<foreach collection="ids" item="id" open="and (" close=")" separator="or">
id=#{id}
</foreach>
</where>
</select>

所谓的动态 SQL,本质还是 SQL 语句,只是我们可以在 SQL 层面去执行一个逻辑代码

SQL片段

  1. 将公共部分抽取出来
  2. 通过 include 标签引用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<sql id="if-title-author">
<if test="title != null">
and title=#{title}
</if>
<if test="author != null">
and author=#{author}
</if>
</sql>

<select id="queryBlogIf" parameterType="map" resultType="blog">
select * from mybatis.blog
<where>
<include refid="if-title-author"></include>
</where>
</select>
  • 最好基于单表来定义 SQL 片段
  • 不要存在 where 标签

Cache 缓存 - mybatis-07-cache

在 DB 操作中连接数据库是非常消耗资源的,所以有了缓存机制来减少重复的查询操作消耗

缓存:一次查询的结果,给他暂存在内存中,再次查询的时候直接走取结果

一级缓存

一级缓存默认开启,且不能关闭,只在一次 SqlSession 中有用

  1. 开启日志
  2. 测试一次 session 中查询两次相同结果
  3. 查看日志输出

缓存失效的几种情况:

  1. 查询不同的东西
  2. 增删改可能会改变原来的数据,所以必定要刷新缓存
  3. 查询不同的 mapper.xml
  4. 手动清理缓存

测试 p1

1
2
3
4
5
6
7
8
9
10
11
12
@Test
public void getUsers() {
SqlSession session = MybatisUtils.getSqlSession();
System.out.println("-----> query user1 the first time <-----");
session.getMapper(UserMapper.class).getUserById(1);
System.out.println("-----> query user1 the second time <-----");
session.getMapper(UserMapper.class).getUserById(1);
System.out.println("-----> query user2 the second time <-----");
session.getMapper(UserMapper.class).getUserById(2);

session.close();
}

输出 log

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
-----> query user1 the first time <-----
Opening JDBC Connection
Created connection 1866875501.
Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@6f46426d]
==> Preparing: select * from mybatis.user where id=?;
==> Parameters: 1(Integer)
<== Columns: id, name, pwd
<== Row: 1, jack, 123
<== Total: 1
-----> query user1 the second time <-----
-----> query user2 the second time <-----
==> Preparing: select * from mybatis.user where id=?;
==> Parameters: 2(Integer)
<== Columns: id, name, pwd
<== Row: 2, change, pwdchange
<== Total: 1
Resetting autocommit to true on JDBC Connection [com.mysql.jdbc.JDBC4Connection@6f46426d]
Closing JDBC Connection [com.mysql.jdbc.JDBC4Connection@6f46426d]
Returned connection 1866875501 to pool.

user1 在第一次 query 的时候有访问 DB,第二次则直接从内存拿,在同一个 session 中访问 user2 也会从 DB 拿

测试 p4

1
2
3
4
5
6
7
8
9
10
@Test
public void getUsers() {
SqlSession session = MybatisUtils.getSqlSession();
System.out.println("-----> query user1 the first time <-----");
session.getMapper(UserMapper.class).getUserById(1);
session.clearCache(); // 手动清 cache !!!
System.out.println("-----> query user1 the second time <-----");
session.getMapper(UserMapper.class).getUserById(1);
session.close();
}

输出 log

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
-----> query user1 the first time <-----
Opening JDBC Connection
Created connection 1936722816.
Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@73700b80]
==> Preparing: select * from mybatis.user where id=?;
==> Parameters: 1(Integer)
<== Columns: id, name, pwd
<== Row: 1, jack, 123
<== Total: 1
-----> query user1 the second time <-----
==> Preparing: select * from mybatis.user where id=?;
==> Parameters: 1(Integer)
<== Columns: id, name, pwd
<== Row: 1, jack, 123
<== Total: 1

添加了清理 cache 的语句后,第二次访问同一个 user 也会从 DB 拿

二级缓存

  1. 开启全局缓存 cacheEnabled -> true
  2. 在 mapper.xml 中加入 标签

为了支持 标签需要 pojo 类实现序列化接口不然会报错 Cause: java.io.NotSerializableException: com.jzheng.pojo.User

  • 一级缓存作用域太低了,所以诞生了二级缓存
  • 基于 namespace 级别的缓存,一个命名空间对应一个二级缓存
  • 工作机制
    • 一个会话查询一条数据,数据被存放在一级缓存中
    • 当前会话关闭,对应的一级缓存就没了,一级缓存中的数据会被保存到二级缓存中
    • 新会话查询信息,会从二级缓存中获取内容
    • 不同 mapper 查出的数据会放在自己对应的缓存中

测试用例

1
2
3
4
5
6
7
8
9
10
11
12
@Test
public void getUsers_diff_session() {
SqlSession session1 = MybatisUtils.getSqlSession();
System.out.println("-----> query user1 the first time <-----");
session1.getMapper(UserMapper.class).getUserById(1);
session1.close();

SqlSession session2 = MybatisUtils.getSqlSession();
System.out.println("-----> query user1 the second time <-----");
session2.getMapper(UserMapper.class).getUserById(1);
session2.close();
}

当 mapper 中没有添加 标签时,输出如下,两个 session 查询同一个 user 的时候都进行了 DB 访问

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
-----> query user1 the first time <-----
Opening JDBC Connection
Created connection 1936722816.
Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@73700b80]
==> Preparing: select * from mybatis.user where id=?;
==> Parameters: 1(Integer)
<== Columns: id, name, pwd
<== Row: 1, jack, 123
<== Total: 1
Resetting autocommit to true on JDBC Connection [com.mysql.jdbc.JDBC4Connection@73700b80]
Closing JDBC Connection [com.mysql.jdbc.JDBC4Connection@73700b80]
Returned connection 1936722816 to pool.
-----> query user1 the second time <-----
Opening JDBC Connection
Checked out connection 1936722816 from pool.
Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@73700b80]
==> Preparing: select * from mybatis.user where id=?;
==> Parameters: 1(Integer)
<== Columns: id, name, pwd
<== Row: 1, jack, 123
<== Total: 1
Resetting autocommit to true on JDBC Connection [com.mysql.jdbc.JDBC4Connection@73700b80]

当 mapper 中添加 标签时,输出如下,第二次查询 user 时是从 cache 中查找的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
-----> query user1 the first time <-----
Cache Hit Ratio [com.jzheng.mapper.UserMapper]: 0.0
Opening JDBC Connection
Created connection 379645464.
Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@16a0ee18]
==> Preparing: select * from mybatis.user where id=?;
==> Parameters: 1(Integer)
<== Columns: id, name, pwd
<== Row: 1, jack, 123
<== Total: 1
Resetting autocommit to true on JDBC Connection [com.mysql.jdbc.JDBC4Connection@16a0ee18]
Closing JDBC Connection [com.mysql.jdbc.JDBC4Connection@16a0ee18]
Returned connection 379645464 to pool.
-----> query user1 the second time <-----
Cache Hit Ratio [com.jzheng.mapper.UserMapper]: 0.5

小结:

  • 只要开启二级缓存,在同一个 Mapper 下就有效
  • 所有的数据都会先放在一级缓存中
  • 只有当会话提交或者关闭,才会提交到二级缓存中

缓存原理

  1. 先看二级缓存中有没有
  2. 再看一级缓存中有没有
  3. 最后才查DB

自定义缓存 ehcache

一种广泛使用的开源 Java 分布式缓存,主要面向通用缓存

使用:

  1. 导包
  2. config 中配置 type

不过这样的功能现在都用类似 redis 的工具代替了,应该不是主流用法了

没事儿别瞎折腾,docker + mysql 香的不得了

Docker 安装

直接跟着官方文档走就行了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
docker pull mysql

# -e MYSQL_ROOT_PASSWORD=my-secret-pw # 按官方镜像文档提示,启动容器时设置密码
# -p 主机(宿主)端口:容器端口
# mac 上查看端口是否关联成功
# * netstat -vanp tcp | grep 3306
# * lsof -i tcp:3306
# 必须指定 -p,不指定连不上,还以为会默认匹配呢,着了半天才发现的
docker run -d -p 3306:3306 -v /Users/id/tmp/mysql/conf:/etc/mysql/conf.d -v /Users/id/tmp/mysql/data:/var/lib/mysql -e MYSQL_ROOT_PASSWORD=123456 --name=mysql221130 mysql

# 启动 DBeaver,链接数据库,报错:`Unable to load authentication plugin 'caching_sha2_password'.`
# 搜索之后,发现是 mysql 驱动有跟新,需要修改客户端的 pom, 升级到 8.x 就行。DBeaver 直接就在创建选项里给了方案,选 8.x 那个就行 [GitIssue](https://github.com/dbeaver/dbeaver/issues/4691)
# Q: 使用高版本的 Mysql connection 还是有问题,不过 msg 变了:`Public Key Retrieval is not allowed`
# A: 右键数据库,选择 Edit Connection, connection setting -> Driver properties -> 'allowPlblicKeyRetrieval' 改为 true
# 还有问题。。。继续抛错:`Access denied for user 'root'@'localhost' (using password: YES)`
docker exec -it mysql01 /bin/bash # 进去容器,输入 `mysql -u root -p` 尝试登陆,成功。推测是链接客户端的问题
ps -ef | grep mysql # 查看了一下,突然想起来,本地我也有安装 mysql 可能有冲突。果断将之前安装的 docker mysql 删除,重新指定一个新的端口,用 DBeaver 链接,成功!

# 通过客户端创建一个新的数据库 new_test, 在本地映射的 data 目录下 ls 一下,可以看到新数据库文件可以同步创建
# > ~/tmp/mydb/data ls
# auto.cnf ca.pem client-key.pem ib_logfile0 ibdata1 mysql performance_schema public_key.pem server-key.pem
# ca-key.pem client-cert.pem ib_buffer_pool ib_logfile1 ibtmp1 new_test private_key.pem server-cert.pem sys

# 删除容器,本地文件依然存在!

有了 Docker 版本的之后没有必要我果断不会再用其他安装方式了,希碎而且卸载不干净。

测试数据

有一个叫 test_db 的 git 项目提供了百万级别的 mysql 测试数据集,有 3.3k 的 star 以后可以拿它来做练手的数据源,挺方便,貌似还是 mysql 的官方推荐。

git repo 有将近 300M,直接将文件夹 copy 到容器中貌似不怎么合适,试试共享文件夹的形式。好像不能在容器启动之后再 share,得重新创建一遍了 (; ̄ェ ̄)

官方推荐的 share 方式是通过 volumn 完成

  • docker rm -f mysql221130 删除原有项目
  • git clone 下载测试 repo
  • 新建容器,带上测试文件夹
  • cd 到 share 文件夹下,运行 mysql -uroot -p < employees.sql 导入数据
  • mysql -uroot -p -t < test_employees_md5.sql 查看数据是否导入成功
  • IDE 中可以看到新的数据库 employees, 他下面有 6 张表,数据还挺多
1
2
3
4
5
docker run -d -p 3306:3306 \
-v /Users/<uid>/tmp/mysql/conf:/etc/mysql/conf.d \
-v /Users/<uid>/tmp/mysql/data:/var/lib/mysql \
-v /Users/<uid>/tmp/mysql/test_db:/share \
-e MYSQL_ROOT_PASSWORD=123456 --name=mysql221130 mysql

Windows 版本安装

  1. 下载安装包 官方地址 下载比较小的,不到测试套件的版本即可
  2. C 盘下新建 Mysql 文件夹,将下载的压缩包解压
  3. 进去解压文件夹下,新建一个 my.ini 配置文件并添加配置
  4. 将对应的 bin 路径添加到系统的 path 中去,做法和添加 JAVA_HOME 一样
  5. 管理员模式打开终端,输入命令 mysqld --initialize-insecure --user=mysql 初始化,并且用户密码为空
  6. 输入 mysqld -install 安装数据库,终端出现 Service successfully installed 表示安装成功
  7. net start mysql 启动服务器
  8. 输入 mysql -u root -p 不用输入密码直接回车, 出现mysql>表示配置完成
  9. 输入 alter user user() identified by "your-password"; 修改 root 用户密码
  10. 输入 net stop mysql 关闭数据库
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
[mysqld]
# 设置3306端口
port=3306
# 设置mysql的安装目录
basedir=C:\Mysql\mysql-8.0.21-winx64
# 设置mysql数据库的数据的存放目录
datadir=C:\Mysql\mysql-8.0.21-winx64\data
# 允许最大连接数
max_connections=200
# 允许连接失败的次数。这是为了防止有人从该主机试图攻击数据库系统
max_connect_errors=10
# 服务端使用的字符集默认为UTF8
character-set-server=utf8
# 创建新表时将使用的默认存储引擎
default-storage-engine=INNODB
# 默认使用“mysql_native_password”插件认证
default_authentication_plugin=mysql_native_password

Windows part issues

Windows 运行 mysqld --initialize-insecure --user=mysql 配置时报错 由于找不到vcruntime140_1.dll,无法继续执行代码 可以去 官网 下载 dll 文件放到 C:\Windows\System32 下即可

Idea 链接 mysql 后报错 Server returns invalid timezone. Go to 'Advanced' tab and set 'serverTimezone' property manually,可以通过设置 mysql 时区解决

  1. cmd -> mysql -uroot -p 登录 DB
  2. show variables like'%time_zone'; 查看时区, Value 为 SYSTEM 则表示没有设置过
  3. set global time_zone = '+8:00'; 修改时区为东八区
  4. 重试链接,问题解决

这只是临时方案,重启 DB 后时区会重置,可以去 my.ini 配置文件中添加配置

1
2
3
[mysqld]
# 设置默认时区
default-time_zone='+8:00'

MacOS 版本安装

1
brew install mysql # 使用 homebrew 安装

安装完毕的时候,终端回给出提示,最后的那段话比较值得注意

1
2
3
4
5
6
7
8
9
10
11
12
We've installed your MySQL database without a root password. To secure it run:
mysql_secure_installation

MySQL is configured to only allow connections from localhost by default

To connect run:
mysql -uroot

To have launchd start mysql now and restart at login:
brew services start mysql
Or, if you don't want/need a background service you can just run:
mysql.server start

翻译成人话就是

  1. DB 安装成功,但是数据库 root 用户是没有密码的,你直接登陆会失败
  2. 运行 mysql_secure_installation 给数据库设置密码
  3. 使用命令 mysql -uroot 联接数据库
  4. 后台启动使用 brew services start mysql 前台启动使用 mysql.server start

PS: 想要改密码得先启动服务,即运行 brew services start mysql 命令

在安全设置脚本中,mysql 会让你进行如重设 root 密码,删除匿名用户等操作,按照提示操作即可。以下是提示样本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
Jack > ~ > mysql_secure_installation

## 开始进行设置
Securing the MySQL server deployment.

Connecting to MySQL using a blank password.

VALIDATE PASSWORD COMPONENT can be used to test passwords
and improve security. It checks the strength of password
and allows the users to set only those passwords which are
secure enough. Would you like to setup VALIDATE PASSWORD component?

## 是否进行安全设置
Press y|Y for Yes, any other key for No: y

There are three levels of password validation policy:

LOW Length >= 8
MEDIUM Length >= 8, numeric, mixed case, and special characters
STRONG Length >= 8, numeric, mixed case, special characters and dictionary file

## 设置密码复杂度,最低也要 *8* 位密码起步
Please enter 0 = LOW, 1 = MEDIUM and 2 = STRONG: 0
Please set the password for root here.

New password:

Re-enter new password:

Estimated strength of the password: 50
Do you wish to continue with the password provided?(Press y|Y for Yes, any other key for No) : y
By default, a MySQL installation has an anonymous user,
allowing anyone to log into MySQL without having to have
a user account created for them. This is intended only for
testing, and to make the installation go a bit smoother.
You should remove them before moving into a production
environment.

## 是否删除匿名用户
Remove anonymous users? (Press y|Y for Yes, any other key for No) : y
Success.


Normally, root should only be allowed to connect from
'localhost'. This ensures that someone cannot guess at
the root password from the network.

## 是否开放 root 用户远程访问
Disallow root login remotely? (Press y|Y for Yes, any other key for No) :

... skipping.
By default, MySQL comes with a database named 'test' that
anyone can access. This is also intended only for testing,
and should be removed before moving into a production
environment.

## 是否删除测试表
Remove test database and access to it? (Press y|Y for Yes, any other key for No) :

... skipping.
Reloading the privilege tables will ensure that all changes
made so far will take effect immediately.

## 是否重新加载使得配置生效
Reload privilege tables now? (Press y|Y for Yes, any other key for No) : y
Success.

All done!

设置完毕之后就可以使用 mysql -uroot -p 登陆测试了。没有遇到其他问题,还挺顺利的 ε-(´∀`; )

下载一些破解软件的时候,windows 会自动将他们查杀可以通过:

开始 -> 搜索’病毒和威胁防护’ -> 点击 ‘病毒和威胁防护’设置下的 管理设置 -> 关闭 实时防护

将保护暂时关掉,等你破解完后,再开启

测试环境 setup

官方给出了 Splunk 的 docker image 我们可以通过它来创建本地测试环境 docker hub link。就两条命令,官方也给了很详细的命令解释,赞。

1
2
3
docker pull splunk/splunk:latest

docker run -d -p 8000:8000 -e "SPLUNK_START_ARGS=--accept-license" -e "SPLUNK_PASSWORD=<password>" --name splunk splunk/splunk:latest

PS: 如果需要测试 Splunk REST API, 还需要开放其端口 -p 8089:8089。登陆系统后 Settings -> Server settings -> General settings 查看端口映射

本地测试环境运行测试用例体验比公司的快很多,体验很好

创建测试数据

发现一个很有意思的函数,可以用它创建简单的测试数据,使用之前最好对照官方文档看看例子,新版有支持 format 为 json, csv 的数据,旧版不行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 创建 100 条数据只显示  5% < index < 95%
| makeresults count=100
| eval random_num=random()
| streamstats count as index
| eventstats max(index) as m_idx
| where index > m_idx*0.05 AND index < m_idx*0.95

# 创建多行
# makemv: 通过分隔符创建多个数据
# mvexpand: 多个数据展开成 event
# mv 是 multi value 的缩写
| makeresults
| eval test="buttercup rarity tenderhoof dash mcintosh fleetfoot mistmane"
| makemv delim=" " test
| mvexpand test

# random() 会产生 0-(2^31-1) 之间的随机数
# 可以使用取模的方式产生 n 以内的随机数
| makeresults
| eval n=(random() % 100)

最常用的 case - search with keyword

直接输入 keyword + enter

常用统计操作 stats

对整个数据集做统计,可以结合 average,count 和 sum 使用,支持 by 做分组

1
2
3
4
5
6
7
8
# 创建 name-score 测试集
| makeresults count=5
| eval name="Jerry Tom"
| makemv delim=" " name
| mvexpand name
| eval score=(random() % 100)
# 统计每个人的 score 总和,平均值
| stats sum(score) avg(score) by name

stats vs eventstats vs streamstats

  • Command Type

  • stats: transforming command, 数据集会发生改变

  • eventstats: Dataset processing command, 需要所有 event 都找出来后才能工作

  • streamstats: streaming command, 针对每一个 event 做操作

1
2
3
4
5
6
7
8
9
10
11
12
13
# 创建测试集
| makeresults count=4
| streamstats count
| eval age = case(count=1, 25, count=2, 39, count=3, 31, count=4, null())
| eval city = case(count=1 OR count=3, "San Francisco", count=2 OR count=4, "Seattle")
# stats 类似 sql 中的聚合函数,计算之后,只会产出一行数据
| stats sum(age)
# eventstats 工作时并不会改变原始数据,而是在原有的 raw 数据外新增一列数据
| eventstats avg(age) by city
# streamstats 和 eventstats 很像,也是不改变原数据去新增一列的操作。
# 区别就在 stream 这个关键字,它是用流的方式处理。只在每条数据出现的节点做计算
# 本例中第一个 San Francisco 均值为自身,第二个则为两个之和做平均了,很神奇。
| streamstats avg(age) by city

此外 streamstats count 还可以用来显示行号,很方便

1
2
3
4
5
| makeresults count=10
| eval x=random()
| streamstats count as index
# eventstats count 可以在每一行上增加一个总计 field
| eventstats count

eval

可以对每一行做计算,支持数学计算,字符串操作和布尔表达式

1
2
3
4
# 生产 4 条数据并对 count 列做平方操纵
| makeresults count=4
| streamstats count
| eval pow=pow(count, 2)

和 eventstats 的区别

总觉得 eval 和 eventstats 很相似,特意查了一下定义,还是有很明显的区别的。

eval 是针对每一行做计算,比如新增 field = field1 - field2

eventstats 突出以总体概念,可以做 by group 的操作,比如 sum(field1) by field2

Comparison and Conditional functions

eval 配合条件函数使用可以衍生出很多效果

1
2
3
4
5
6
7
8
9
10
11
# 根据 status 匹配 description 信息
| makeresults
| eval status="400 300 500 200 200 201 404"
| makemv delim=" " status
| mvexpand status
| eval description=case(status == 200, "OK", status ==404, "Not found", status == 500, "Internal Server Error")
# 检测 status 是否匹配 4xx 格式
| eval matches = if(match(status,"^4\d{2}"), 1, 0)

# 根据是否包含关键字对 query type 的 log 做细分
search cmd | eval ReqType=if(like(_raw, "%lastModified%") AND ReqType="query", "query - lastModified", ReqType)

rex 抽取特定的 field

splunk 支持字符串匹配,常用案例如下,需要注意的是,他只会从 _raw 中抽取信息

1
2
3
4
5
| makeresults
| eval _raw="[4211fb51-6ae6-41eb-a0bf-e6dd693bbdb2] [EngineX Perf] Request takes 1116 ms"
| rex "Request takes (?<time_cost>.*) ms"
# 我们还可以通过添加 (?i) 达到忽略大小写的效果
| rex "(?i)request takes (?<time_cost>.*) ms"

收集分散在多个 event 中的数据

有两种解决方案,一种是子查询,一种是 eventstats,两种方案对向能消耗都很大。

看到一个从多条分散的 event 中收集数据的例子,刚好是我现在需要的。核心思路是通过 eventstats 为每个 event 计算一个新的 field 做跳板

这里还有一个很神奇的语法,通过 values 统计出来的结果,在 if 条件中,我们可以直接通过使用 field=values 的语法达到类似 in 的效果。找了半天文档,没发现有这种个语法说明 (; ̄ェ ̄)

相似的还有一个 command 叫做 subsearch,通过自查询缩小范围,然后进一步查询

就我处理的案例来说,subsearch performance 要小很多,通过子查询可以很快的缩小处理范围

行转列 transpose

如果统计结果为

A B C
1 2 3

转化为饼图的时候,只会显示 A 类型的数据,因为 Splunk 默认使用 X 轴作为分类标的。这时可以使用 search cmd | transpose 达到行专列的效果

1
2
3
4
5
| makeresults
| eval _raw="A=2,B=5,C=8,D=1"
| extract
| table A B C D
| transpose

去重

1
2
3
4
5
6
7
8
9
| makeresults count=4 
| streamstats count
| eval age = case(count=1, 25, count=2, 39, count=3, 31, count=4, null())
| eval city = case(count=1 OR count=3, "San Francisco", count=2 OR count=4, "Seattle")
| dedup city
# 或者使用 values
| stats values(city)
# 当然也可以使用 stats 计算 count 达到曲线救国的效果
| stats count by city

使用 where 达到 filter 的效果

1
2
3
4
5
| makeresults count=4 
| streamstats count
| eval age = case(count=1, 25, count=2, 39, count=3, 31, count=4, null())
| eval city = case(count=1 OR count=3, "San Francisco", count=2 OR count=4, "Seattle")
| where age > 31

查询 event 的日均量

1
2
3
4
eventtype="searchAccountLocked" | timechart span=1d count | stats avg(count)

# 在此基础上,计算 7 天的平均值
eventtype="searchAccountLocked" | timechart span=1d count | stats avg(count) as avgc | eval n=exact(1 * avgc)

取整数

使用 ceil 和 round 分别达到向上和向下取整

1
2
3
4
| makeresults 
| eval n=10.3
| eval n_round=round(n)
| eval n_ceil=ceil(n)

更多计算函数,参考 math

显示时移除 fields

指定显示结果 search cmd | fields host, src, 从结果集中 remove 某个 field search cmd | fields - host, src

复制 field

eval 和 rename 都能达到类似的效果,只不过 rename 之后原来的 field 不在了,eval 的话还在,相当于 copy

1
2
3
4
5
search cmd
| eval field1=field2

# or
| rename field1 as field2

Splunk SDK

尝试了 python 版本的 SDK,香!

参考 官方文档 下载依赖,在本地配置 .splunkrc 文件写入连接信息方便调用。第一次用的时候密码配错了,还以为内网不可用,需要用 vlab,再测试的时候发现了这个问题。总的来说很可以。

Steps:

  1. clone git 开源项目 Splunk SDK Python
  2. 用户目录下创建 .splunkrc 文件
  3. cd 到 splunk-sdk-python/examples folder 下,运行命令 python search.py "search * | head 10" --earliest_time="2011-08-10T17:15:00.000-07:00" --rf="desc" --output_mode=json 可以看到对应时间戳下的前 10 条记录

.splunkrc 文件模板

1
2
3
4
5
6
7
8
9
10
11
12
# Splunk host (default: localhost)
host=xxx.xxx.xxx
# Splunk admin port (default: 8089)
port=8089
# Splunk username
username=jack
# Splunk password
password=mypwd
# Access scheme (default: https)
scheme=https
# Your version of Splunk (default: 5.0)
version=7.1.2

Splunk REST API

本地测试过了,但是产品上失败,可能公司用的 Splunk 有特殊限制

1
2
3
4
5
6
# 创建搜索 job
curl -u admin:Splunkpwd0001$ -k https://localhost:8089/services/search/jobs -d search="search ScimPerformanceInterceptor"
# 查看 job 状态
curl -u admin:Splunkpwd0001$ -k https://localhost:8089/services/search/jobs/1658910321.148
# 查看 job 结果
curl -u admin:Splunkpwd0001$ -k https://localhost:8089/services/search/jobs/1658910321.148/results --get -d output_mode=json

Splunk dashboard sample

官方制作了一个 dashboard 插件,里面有大量的精美 bashboard 案例 dashboard examples。需要注册账号,不过是免费的,下载完成后还会给出安装步骤。

  1. Log into Splunk Enterprise.
  2. On the Apps menu, click Manage Apps.
  3. Click Install app from file.
  4. In the Upload app window, click Choose File.
  5. Locate the .tar.gz file you just downloaded, and then click Open or Choose.
  6. Click Upload.
  7. Click Restart Splunk, and then confirm that you want to restart.

To install apps and add-ons directly into Splunk Enterprise
Put the downloaded file in the $SPLUNK_HOME/etc/apps directory.
Untar and ungzip your app or add-on, using a tool like tar -xvf (on *nix) or WinZip (on Windows).
Restart Splunk.

After you install a Splunk app, you will find it on Splunk Home. If you have questions or need more information, see Manage app and add-on objects.

三个小例子快速入门

搜索 event 并通过饼图展示

  1. 输入时间节点和关键词:MessageBox topic=com.successfactors.usermanagement.event.UserChangeEvent | stats count by servername
  2. 选择可视化 tab
  3. 选择饼图

饼图

显示每天的 event 量

  1. 选择时间
  2. 输入搜索条件: MessageBox topic=com.successfactors.usermanagement.event.UserChangeEvent | timechart count span=1d
  3. 选择图形

柱状图

通过正则删选 event 并计算百分比

  1. 选择时间
  2. 输入删选条件: MessageBox topic=com.successfactors.usermanagement.event.UserChangeEvent | stats count as total count(eval(match(field1, "companyId"))) as containsCID | eval CID_PCT=round(containsCID/total*100, 2)

百分比表

GoF 定义: Allows for one or more operation to be applied to a set of objects at runtime, decoupling the operations from the object structure.

访问者模式讲的是表示一个作用于某对象结构中的各元素的操作。它使你可以在不改变各元素的类的前提下定义作用于这些元素的新操作。

行为模式之一,目的是将行为对象分开。

缺点:每增加一种支持的 object,你就必须在 visitor 及其实现类中添加新的方法支持这个改动。

描述

被访问者就是上文中的 object,他持有数据,我们想把他和数据运算分离,保持其独立性

访问者代表着 operations,通过它可以实现数据运算

UML

实例

From DZone

抽象一个邮寄业务,计算购物车中所有的物件总的邮费。每样物件都有自己的属性,比如价格,重量之类的。我们将邮费计算的规则单独封装在 Visitor 中,在物件类中通过调用 accept 实现计算。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
// 代表 object 的接口
public interface Visitable {
void accept(Visitor visitor);
}

// 实现 accept 的实例
public class Book implements Visitable {
private double price = 8.0;
private double weight = 3.2;

@Override
public void accept(Visitor visitor) {
visitor.visit(this);
}

public double getPrice() {
return price;
}

public double getWeight() {
return weight;
}
}

// visitor 接口
public interface Visitor {
void visit(Book book);

void visit(Shoes shoes);
}

// visitor 实例
public class PostageVisitor implements Visitor {
private double totalPostageForCart;

@Override
public void visit(Book book) {
// rule to calculate book postage cost
// if price over 10, free postage.
if(book.getPrice() < 10.0) {
totalPostageForCart += book.getWeight() * 2;
}
}

@Override
public void visit(Shoes shoes) { //TODO }

public double getTotalPostageForCart() {
return this.totalPostageForCart;
}
}

// 客户端调用
public class Client {
public static void main(String[] args) {
Book book = new Book();
Shoes shoes = new Shoes();
PostageVisitor postageVisitor = new PostageVisitor();

book.accept(postageVisitor);
shoes.accept(postageVisitor);

System.out.println("Total cost: " + postageVisitor.getTotalPostageForCart());
}
}

From Refactoring Guru

根据定义的图形打印信息到 XML 文件中,这个例子本质上和前一个没什么区别,但是他提供了组合类型的 object 支持,并且输出 xml, 还有 format 都让我眼前一亮。反正感觉很赞!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
// 定义持有 accept 的接口
public interface Shape {
void move(int x, int y);

void draw();

String accept(Visitor visitor);
}

// 定义接口实现
public class Dot implements Shape {
private int id;
private int x;
private int y;

public Dot() {
}

public Dot(int id, int x, int y) {
this.id = id;
this.x = x;
this.y = y;
}

@Override
public void move(int x, int y) {
// move shape
}

@Override
public void draw() {
// draw shape
}

public String accept(Visitor visitor) {
return visitor.visitDot(this);
}
// getter + setter
}

// 定义组合类型的实现
public class CompoundShape implements Shape {
public int id;
public List<Shape> children = new ArrayList<>();

public CompoundShape(int id) {
this.id = id;
}

@Override
public void move(int x, int y) {
// move shape
}

@Override
public void draw() {
// draw shape
}

public int getId() {
return id;
}

@Override
public String accept(Visitor visitor) {
return visitor.visitCompoundGraphic(this);
}

public void add(Shape shape) {
children.add(shape);
}
}

// 定义 visitor 接口
public interface Visitor {
String visitDot(Dot dot);

String visitCircle(Circle circle);

String visitRectangle(Rectangle rectangle);

String visitCompoundGraphic(CompoundShape cg);
}

// visitor 实现
public class XMLExportVisitor implements Visitor {

public String export(Shape... args) {
StringBuilder sb = new StringBuilder();
sb.append("<?xml version=\"1.0\" encoding=\"utf-8\"?>" + "\n");
for (Shape shape : args) {
sb.append(shape.accept(this)).append("\n");
}
return sb.toString();
}

public String visitDot(Dot d) {
return "<dot>" + "\n" +
" <id>" + d.getId() + "</id>" + "\n" +
" <x>" + d.getX() + "</x>" + "\n" +
" <y>" + d.getY() + "</y>" + "\n" +
"</dot>";
}

...

public String visitCompoundGraphic(CompoundShape cg) {
return "<compound_graphic>" + "\n" +
" <id>" + cg.getId() + "</id>" + "\n" +
_visitCompoundGraphic(cg) +
"</compound_graphic>";
}

private String _visitCompoundGraphic(CompoundShape cg) {
StringBuilder sb = new StringBuilder();
for (Shape shape : cg.children) {
String obj = shape.accept(this);
// Proper indentation for sub-objects.
obj = " " + obj.replace("\n", "\n ") + "\n";
sb.append(obj);
}
return sb.toString();
}

}

// 客户端调用
public class Client {
public static void main(String[] args) {
Dot dot = new Dot(1, 10, 55);
Circle circle = new Circle(2, 23, 15, 10);
Rectangle rectangle = new Rectangle(3, 10, 17, 20, 30);

CompoundShape compoundShape = new CompoundShape(4);
compoundShape.add(dot);
compoundShape.add(circle);
compoundShape.add(rectangle);

CompoundShape c = new CompoundShape(5);
c.add(dot);
compoundShape.add(c);

export(circle, compoundShape);
}

private static void export(Shape... shapes) {
XMLExportVisitor exportVisitor = new XMLExportVisitor();
System.out.println(exportVisitor.export(shapes));
}
}

B 站狂神 Spring5 教程笔记

Spring 基本概念

七大组成

  1. AOP
  2. ORM
  3. Web
  4. DAO
  5. Context
  6. Web MVC
  7. Core
  • Spring Boot
    • 快速开发脚手架
    • 快速开发单个微服务
    • 约定大于配置
  • Spring Cloud
    • 基于 SpringBoot 实现的

弊端:发展太久,违背原来的理念。配置繁琐,人称 ‘配置地狱’

Spring 和 SpringMVC 的区别:都是容器,spring 用来管理 dao 和 service,springmvc 用来管理 controller

IoC 理论推导 (Inversion of Control)

原来的实现

  1. UserDao 接口
  2. UserDaoImpl 实现类
  3. UserService 业务接口
  4. UserServiceImpl 业务实现类

用户的需求可能影响到原来的代码,我们需要根据用户需求修改源代码(修改 UserDaoImpl 中的 Dao 生成)

通过 set 方法注入后,实现被动接受对象,需求由外部决定。不在管理对象创建,专注于扩展业务。

1
2
3
4
5
6
7
// UserServiceImpl 中对 UserDao 的引用
private UserDao userDao;

// 利用 set 动态注入实现
public void setUserDao(UserDao userDao) {
this.userDao = userDao;
}

IoC 的本质

控制反转是一种设计思想,DI(Dependency Injection) 是 IoC 的一种实现方式,将对象的创建交给第三方,获取对象的方式的反转。

Spring 是一种实现控制反转的 IoC 容器,常见的有两种对象控制方式,XML 和 注解。XML 配置 Bean, 定义和实现是分离的。注解方式则把两者结合在了一起,从而达到零配置。

Spring Framework 官方文档

IoC创建对象的方式

  1. 默认使用无参构造创建对象
  2. 通过 constructor-arg 标签实现带参构造器功能

在 xml 加载完后,配置的对象就已经被创建了

Spring 配置说明

  1. alias 别名,和 bean 的 name 属性重复,而且 name 更灵活
  2. bean 对象生成配置
  3. import 合并多个 xml 配置文件

DI - 依赖注入

  1. 构造器注入
  2. Set方式注入 - 即依赖注入
  3. 其他注入

依赖: bean 对象的创建依赖容器
注入: bean 对象的所有属性由容器来注入

P/C命名空间注入

在 xml 中导入约束即可使用

1
2
xmlns:p="http://www.springframework.org/schema/p"
xmlns:c="http://www.springframework.org/schema/c"

P 可以扩展属性注入,一个 tag 解决,不用嵌套xml了

C 可以扩展构造器

Bean 的 作用域(scope)

  1. singleton - 默认域
  2. prototype - 每次取 bean 都会产生新对象
1
2
<bean id="accountService" class="com.something.DefaultAccountService" scope="singleton"/>
<bean id="accountService" class="com.something.DefaultAccountService" scope="prototype"/>

Bean 的自动装配

  • 自动装配是 Spring 满足 bean 依赖的一种方式
  • Spring 在上下文中自动寻找,并自动给 bean 装配属性

Spring 三种装配方式:

  1. xml
  2. 注解
  3. 隐式的自动装配 bean

使用注解开发

Spring4 之后,要使用注解需要保证 AOP 包已经导入。XML 也需要添加特殊的约束 <context:annotation-config/>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
https://www.springframework.org/schema/context/spring-context.xsd">

<!-- 设置扫描路径 -->
<context:component-scan base-package="com.jzheng.pojo"/>
<context:annotation-config/>

</beans>
  1. bean - @Component
  2. 属性 - @Value
  3. 衍生的注解 - @Repository - for dao/@Service - for service/@Controller - for controller 作用都是将对象注入到容器
  4. 自动装配
    1. @Autowired 通过类型,名字装配。如果不能自动装配属性,可以通过 @Qualifier(value=”xxx)
    2. @Nullable,允许为空
    3. @Resource,通过名字,类型装配
  5. 作用域 - @Scope
  6. 小结: XML 更加万能,使用任何场合;注解只能在自己的class 里使用。

推荐做法:XML 用来管理 Bean,注解只用来注入属性

使用 Java 的方式配置 Spring

JavaConfig 是 Spring 一个子项目, Spring4 之后成为核心项目。通过 @Configuration 注解来实现,可以代替 xml。也有像 Import 这样的东西,可以包含其他配置类。

代理模式

Spring 必问题 - SpringAOP 和 SpringMVC

代理模式分类

  • 静态代理
  • 动态代理

静态代理

角色分析

  • 抽象角色:一般是接口或抽象类
  • 真实角色:被代理的角色
  • 代理角色:代理真实角色,代理后做一些操作
  • 客户:访问代理对象的人

优点:

  • 使真实对象操作更纯粹,不用去关注公共业务
  • 公共业务交给代理,业务分工
  • 公共业务扩展方便

缺点: 一个真实角色产生一个代理角色,代码量翻倍

动态代理

  • 动态代理和静态代理角色一样
  • 动态代理的代理类使动态生成,不是直接写好的
  • 动态代理分两大类:基于接口的动态代理/基于类的动态代理
    • 接口 - JDK动态代理
    • 类 - cglib
    • java字节码 - javasist

两个类: Proxy / InvocationHandler

Proxy: 在 handler 中被调用,产生代理的实例

InvocationHandler: 自定义调用过程,返回执行结果

优点:静态的有点 + 一个动态代理类代理的使一个接口,一般对应一类业务

AOP - 横向扩展功能

配置实现01

目标业务点:pointcut, 需要额外添加的附属动作:adviser(MethodBeforeAdvice/AfterReturningAdvice)

然后添加配置文件

1
2
3
4
5
6
7
8
9
10
11
12
<bean id="userService" class="com.jzheng.service.UserServiceImpl"/>
<bean id="log" class="com.jzheng.log.Log"/>
<bean id="afterLog" class="com.jzheng.log.AfterLog"/>

<!--config AOP -->
<aop:config>
<!-- point cut -->
<aop:pointcut id="pointcut" expression="execution(* com.jzheng.service.UserServiceImpl.*(..))"/>
<!-- 执行环绕增强 -->
<aop:advisor advice-ref="log" pointcut-ref="pointcut"/>
<aop:advisor advice-ref="afterLog" pointcut-ref="pointcut"/>
</aop:config>

配置实现02

也可以用自定义类,使用更简单,但是功能比之前的弱,不能操作 Method 之类的属性

1
2
3
4
5
6
7
8
9
public class DiyPointCut {
public void before() {
System.out.println("----------> before method");
}

public void after() {
System.out.println("-----------> after method");
}
}
1
2
3
4
5
6
7
8
9
10
11
<bean id="diy" class="com.jzheng.diy.DiyPointCut"/>
<aop:config>
<!-- 自定义切面 ref 要引用的类-->
<aop:aspect ref="diy">
<!-- 切入点 -->
<aop:pointcut id="point" expression="execution(* com.jzheng.service.UserServiceImpl.*(..))"/>
<!-- 通知 -->
<aop:before method="before" pointcut-ref="point"/>
<aop:after method="after" pointcut-ref="point"/>
</aop:aspect>
</aop:config>

注解实现

类添加 Aspect 注解, 在方法上添加注解, 方法注解中可以指定切点。执行顺序:环绕前 -> 方法前 -> 方法 -> 环绕后 -> 方法后

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Aspect
public class AnnotationPointCut {
@Before("execution(* com.jzheng.service.UserServiceImpl.*(..))")
public void before() {
System.out.println("-------------> before [Anno type]");
}

@After("execution(* com.jzheng.service.UserServiceImpl.*(..))")
public void after() {
System.out.println("-------------> after [Anno type]");
}

@Around("execution(* com.jzheng.service.UserServiceImpl.*(..))")
public void around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
System.out.println("-------------> around before [Anno type]");
System.out.println("Signature: " + proceedingJoinPoint.getSignature());
Object proceed = proceedingJoinPoint.proceed();
System.out.println("-------------> around after [Anno type]");
}
}
1
2
3
4
<!-- 方式3, 注解方式 -->
<bean id="annotationPointCut" class="com.jzheng.diy.AnnotationPointCut"/>
<!-- 开启注解支持 -->
<aop:aspectj-autoproxy/>

整合 Mybatis

  1. 导入包
    1. junit
    2. mybatis
    3. mysql数据库
    4. spring相关的jar
    5. aop织入
    6. mybatis-spring [new]
  2. 编写配置文件
  3. 测试

Mybatis

学到第 23 课,跳出去先把 Mybatis 看完再回来。。。。

  1. 导包
    • junit
    • mybatis
    • mysql
    • spring
    • aop织入
    • mybatis-spring
  2. 配置文件
  3. 写测试

mybatis 回忆

  1. 编写实体类
  2. 编写核心配置文件
  3. 编写接口
  4. 编写 Mapper.xml
  5. 测试

mybatis-spring

  1. 编写数据源配置
  2. sqlSessionFactory
  3. sqlSessionTemplate

事务回顾

  • 要么都成功,要么都失败
  • 十分重要,涉及一致性,不能马虎
  • 确保完整性和一致性

事务 ACID 原则:

  • 原子性
  • 一致性
  • 隔离性,多个业务可能操作一个资源,防止数据损坏
  • 持久性,十五一旦提交,无论系统发生什么问题,结果都不会被影响,被持久化的写到存储器中

spring 中的事务管理

  • 声明式事务 - AOP
  • 编程式事务 - 需要在代码中进行事务管理,侵入性太强,不推荐

为什么需要事务:

  1. 不添加事务管理,可能存在事务提交不一致
  2. 如果不在 spring 中配置声明式事务,我们就需要在代码中手动配置事务
  3. 在项目的开发中十分重要,涉及到数据的一致性

通过本次实验对 ASM 这个字节码框架有一个基本的了解。实验必须是简单明了,方便重现的。引用一段话很好的概括了 ASM 的功能

可以负责任的告诉大家,ASM只不过是通过 “Visitor” 模式将 “.class” 类文件的内容从头到尾扫描一遍。因此如果你抱着任何更苛刻的要求最后都将失望而归。

实验平台信息:
MacOS + IDEA + ASM Bytecode Outline 插件

输出 Class 方法

准备测试用 class,通过 ASM 输出 class 中的方法名称

1
2
3
4
5
6
7
8
9
10
11

public interface MyInterface01 {}

public interface MyInterface02 {}

public class SayHello implements MyInterface01, MyInterface02 {
public void say() {
String name = "Jack";
System.out.println("Hello" + name);
}
}

右键准备的测试文件,选中 ‘Show bytecode outline’ 选项,点击 Bytecode tab, 查看内容可以看到字节码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
// class version 52.0 (52)
// access flags 0x21
public class sorra/tracesonar/mytest/SayHello implements sorra/tracesonar/mytest/MyInterface01 sorra/tracesonar/mytest/MyInterface02 {

// compiled from: SayHello.java

// access flags 0x1
public <init>()V
L0
LINENUMBER 3 L0
ALOAD 0
INVOKESPECIAL java/lang/Object.<init> ()V
RETURN
L1
LOCALVARIABLE this Lsorra/tracesonar/mytest/SayHello; L0 L1 0
MAXSTACK = 1
MAXLOCALS = 1

// access flags 0x1
public say()V
L0
LINENUMBER 5 L0
LDC "Jack"
ASTORE 1
L1
LINENUMBER 6 L1
GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
NEW java/lang/StringBuilder
DUP
INVOKESPECIAL java/lang/StringBuilder.<init> ()V
LDC "Hello"
INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
ALOAD 1
INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;
INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
L2
LINENUMBER 7 L2
RETURN
L3
LOCALVARIABLE this Lsorra/tracesonar/mytest/SayHello; L0 L3 0
LOCALVARIABLE name Ljava/lang/String; L1 L3 1
MAXSTACK = 3
MAXLOCALS = 2
}

测试用例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
public class ASMTest {
public static void main(String[] args) throws IOException {
System.out.println("--- START ---");
ClassReader cr = new ClassReader(SayHello.class.getName());
cr.accept(new DemoClassVisitor(), 0);
System.out.println("--- END ---");
}
}

class DemoClassVisitor extends ClassVisitor {
public DemoClassVisitor() {
super(Opcodes.ASM5);
}

// Called when access file header, so it will called only once for each class
/**
* access: 方法的 modifier, 就是 public/private 的那些修饰词
* name: class name
* signature: 不是很确定,但是好像不重要
* superName: 父类的名字,该例子中是 object
* interfaces: 实现的接口
*/
@Override
public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
super.visit(version, access, name, signature, superName, interfaces);
System.out.println("invoke visit method, params: " + version + ", " + access + ", " + name + ", " + signature + ", " + superName + ", " + Arrays.toString(interfaces));
}

// Called when access method
@Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
System.out.println("at Method " + name);
//
MethodVisitor superMV = super.visitMethod(access, name, desc, signature, exceptions);
return new DemoMethodVisitor(superMV, name);
}
}

class DemoMethodVisitor extends MethodVisitor {
private String methodName;
public DemoMethodVisitor(MethodVisitor mv, String methodName) {
super(Opcodes.ASM5, mv);
this.methodName = methodName;
}
public void visitCode() {
System.out.println("at Method ‘" + methodName + "’ Begin...");
super.visitCode();
}

@Override
public void visitLocalVariable(String name, String desc, String signature, Label start, Label end, int index) {
super.visitLocalVariable(name, desc, signature, start, end, index);
System.out.println("Params in visitLocalVariable: " + name + ", " + desc + ", " + signature + ", " + start + ", " + end + ", " + index);
}

public void visitEnd() {
System.out.println("at Method ‘" + methodName + "’End.");
super.visitEnd();
}
}

终端输出

1
2
3
4
5
6
7
8
9
10
11
12
--- START ---
invoke visit method, params: 52, 33, sorra/tracesonar/mytest/SayHello, null, java/lang/Object, [sorra/tracesonar/mytest/MyInterface01, sorra/tracesonar/mytest/MyInterface02]
at Method <init>
at Method ‘<init>’ Begin...
Params in visitLocalVariable: this, Lsorra/tracesonar/mytest/SayHello;, null, L662441761, L1618212626, 0
at Method ‘<init>’End.
at Method say
at Method ‘say’ Begin...
Params in visitLocalVariable: this, Lsorra/tracesonar/mytest/SayHello;, null, L1129670968, L1023714065, 0
Params in visitLocalVariable: name, Ljava/lang/String;, null, L2051450519, L1023714065, 1
at Method ‘say’End.
--- END ---

想要理解 ASM 运行方式,需要结合前面的 bytecode 内容。比如 visitLocalVariable 方法其实就是将 bytecode 里面对应的 LOCALVARIABLE 信息打印出来。

MethodVisitor 的 visitMethodInsn 方法简单例子

基本使用

根据查到的资料,该方法可以知道当前的方法调用了其他类的什么方法,设计用例如下: Class A 有 method a, Class B 有 method b, a 中包含对 b 的调用,使用 visitMethodInsn 解析 a 方法是应该可以拿到这层关系

1
2
3
4
5
6
7
8
9
10
11
12
13
public class ClassA {
ClassB b = new ClassB();

public void methodA() {
b.methodB();
}
}

public class ClassB {
public void methodB() {
System.out.println("Method B called...");
}
}

class A 的 bytecode 显示如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// class version 52.0 (52)
// access flags 0x21
public class com/jzheng/asmtest/ClassA {

// compiled from: ClassA.java

// access flags 0x0
Lcom/jzheng/asmtest/ClassB; b

// access flags 0x1
public <init>()V
L0
LINENUMBER 3 L0
ALOAD 0
INVOKESPECIAL java/lang/Object.<init> ()V
L1
LINENUMBER 4 L1
ALOAD 0
NEW com/jzheng/asmtest/ClassB
DUP
INVOKESPECIAL com/jzheng/asmtest/ClassB.<init> ()V
PUTFIELD com/jzheng/asmtest/ClassA.b : Lcom/jzheng/asmtest/ClassB;
RETURN
L2
LOCALVARIABLE this Lcom/jzheng/asmtest/ClassA; L0 L2 0
MAXSTACK = 3
MAXLOCALS = 1

// access flags 0x1
public methodA()V
L0
LINENUMBER 7 L0
ALOAD 0
GETFIELD com/jzheng/asmtest/ClassA.b : Lcom/jzheng/asmtest/ClassB;
INVOKEVIRTUAL com/jzheng/asmtest/ClassB.methodB ()V
L1
LINENUMBER 8 L1
RETURN
L2
LOCALVARIABLE this Lcom/jzheng/asmtest/ClassA; L0 L2 0
MAXSTACK = 1
MAXLOCALS = 1
}

可以看到在 methodA()V block 里有对 ClassB 的方法调用说明 INVOKEVIRTUAL com/jzheng/asmtest/ClassB.methodB ()V,通过它我们可以知道当前方法对其他类方法的调用

测试用例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public class ASMTest {
public static void main(String[] args) throws IOException {
System.out.println("--- START ---");
ClassReader cr = new ClassReader(ClassA.class.getName());
cr.accept(new DemoClassVisitor(), 0);
System.out.println("--- END ---");
}
}

class DemoClassVisitor extends ClassVisitor {
public DemoClassVisitor() {
super(Opcodes.ASM5);
}

// Called when access method
@Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
System.out.println("at Method " + name);

super.visitMethod(access, name, desc, signature, exceptions);
return new MethodVisitor(Opcodes.ASM5) {
@Override
public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf) {
super.visitMethodInsn(opcode, owner, name, desc, itf);
System.out.println(String.format("opcode: %s, owner: %s, name: %s, desc: %s, itf: %s", opcode, owner, name, desc, itf));
}
};
}
}
// output:
// --- START ---
// at Method <init>
// opcode: 183, owner: java/lang/Object, name: <init>, desc: ()V, itf: false
// opcode: 183, owner: com/jzheng/asmtest/ClassB, name: <init>, desc: ()V, itf: false
// at Method methodA
// opcode: 182, owner: com/jzheng/asmtest/ClassB, name: methodB, desc: ()V, itf: false
// --- END ---

测试 itf 参数

visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf)

  • desc: 方法参数和返回值类型,() 内为参数,外面是返回值
  • itf 方法是否来自接口,如下面所示的例子,当子类实现接口,通过子类调用方法时,值为 false,当强转为接口时值为 true。 值的注意的是,继承的方法也是 false。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;

import java.io.IOException;

/**
* Scenario:
* <p>
* {@link org.objectweb.asm.MethodVisitor#visitMethodInsn(int, String, String, String, boolean)}, 测试方法是继承自父类或者接口时该接口中的参数表现形式
*/
public class TestVisitMethodInsn {
public static void main(String[] args) throws IOException {
System.out.println("--- START ---");
ClassReader cr = new ClassReader(Client.class.getName());
cr.accept(new DemoClassVisitor(), 0);
System.out.println("--- END ---");
}
}

class Client {
Sub sub = new Sub();

public void test() {
sub.methodOfSuper();
sub.methodOfInterface();
((Super)sub).methodOfSuper();
((MyInterface)sub).methodOfInterface();
}
}

abstract class Super {
abstract void methodOfSuper();
}

interface MyInterface {
boolean methodOfInterface();
}

class Sub extends Super implements MyInterface {
@Override
void methodOfSuper() {

}

@Override
public boolean methodOfInterface() {
return false;
}
}

class DemoClassVisitor extends ClassVisitor {
public DemoClassVisitor() {
super(Opcodes.ASM5);
}

// Called when access method
@Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
System.out.println("In Method " + name);

super.visitMethod(access, name, desc, signature, exceptions);
return new MethodVisitor(Opcodes.ASM5) {
@Override
public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf) {
super.visitMethodInsn(opcode, owner, name, desc, itf);
System.out.println(String.format("opcode: %s, owner: %s, name: %s, desc: %s, itf: %s", opcode, owner, name, desc, itf));
}
};
}
}

visitInvokeDynamicInsn 用以检测 lambda 表达式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
/**
* Scenario:
* {@link org.objectweb.asm.MethodVisitor#visitInvokeDynamicInsn(String, String, Handle, Object...)}, 这个方法可以用来检测动态生成的方法,比如 lambada 表达式
*/
public class TestVisitInvokeDynamicInsn extends ClassVisitor {
public TestVisitInvokeDynamicInsn() {
super(Opcodes.ASM5);
}

@Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
System.out.println("Parse Method: " + name);

super.visitMethod(access, name, desc, signature, exceptions);
return new MethodVisitor(Opcodes.ASM5) {
@Override
public void visitInvokeDynamicInsn(String name, String desc, Handle bsm, Object... bsmArgs) {
for (Object sub : bsmArgs) {
if (sub instanceof Handle) {
System.out.println("Handle info: " + sub);
System.out.printf("name: %s, desc: %s, owner: %s, tag: %s%n", ((Handle) sub).getName(), ((Handle) sub).getDesc(), ((Handle) sub).getOwner(), ((Handle) sub).getTag() );
}
}
super.visitInvokeDynamicInsn(name, desc, bsm, bsmArgs);
System.out.printf("Output from [visitInvokeDynamicInsn]%nname: %s%n desc: %s%n bsm: %s%n bsmArgs: %s%n", name, desc, bsm, Arrays.asList(bsmArgs));
}
};
}

public static void main(String[] args) throws IOException {
System.out.println("--- START ---");
ClassReader cr = new ClassReader(Client02.class.getName());
cr.accept(new TestVisitInvokeDynamicInsn(), 0);
System.out.println("--- END ---");
}
}

class Client02 {
public void test() {
String[] names = new String[]{"A", "B"};
Arrays.stream(names).forEach(System.out::println);

BinaryOperator<Long> addLongs = Long::sum;
addLongs.apply(1L,2L);
}
}


// --- START ---
// Parse Method: <init>
// Parse Method: test
// Handle info: java/io/PrintStream.println(Ljava/lang/String;)V (5)
// name: println, desc: (Ljava/lang/String;)V, owner: java/io/PrintStream, tag: 5
// Output from [visitInvokeDynamicInsn]
// name: accept
// desc: (Ljava/io/PrintStream;)Ljava/util/function/Consumer;
// bsm: java/lang/invoke/LambdaMetafactory.metafactory(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite; (6)
// bsmArgs: [(Ljava/lang/Object;)V, java/io/PrintStream.println(Ljava/lang/String;)V (5), (Ljava/lang/String;)V]
// Handle info: java/lang/Long.sum(JJ)J (6)
// name: sum, desc: (JJ)J, owner: java/lang/Long, tag: 6
// Output from [visitInvokeDynamicInsn]
// name: apply
// desc: ()Ljava/util/function/BinaryOperator;
// bsm: java/lang/invoke/LambdaMetafactory.metafactory(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite; (6)
// bsmArgs: [(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;, java/lang/Long.sum(JJ)J (6), (Ljava/lang/Long;Ljava/lang/Long;)Ljava/lang/Long;]
// --- END ---

注意参数列表中的 bsmArgs, 其中的 Handle 可能是你想要的, 列表中的 bsm 是一个固定值,看着像是 lambda 的指代

修改方法

实验内容:准备一个 HelloWorld.class 可以打印出 ‘Hello World’ 字样。通过 ASM 框架使他在打印之前和之后都输出一些 debug 信息,调用时可以使用反射简化实验。

测试用 class

1
2
3
4
5
public class HelloWorld {
public void sayHello() {
System.out.println("Hello World...");
}
}

测试用例,通过反射拿到测试方法并调用查看输出。

1
2
3
4
5
6
7
8
9
10
11
12
13
import java.lang.reflect.Method;

public class main {
public static void main(String[] args) throws Exception {
Class cls = Class.forName("sorra.tracesonar.main.aopsample.HelloWorld");

Method sayHello = cls.getDeclaredMethod("sayHello");
sayHello.invoke(cls.newInstance());
}
}

// run and get output:
// Hello World...

预期目标:通过 ASM 修改目标 class 使得输出为 ‘Test start \n Hello World… \n Test end’,对应的 java code:

1
2
3
4
5
6
7
public class Expected {
public void sayHello() {
System.out.println("Test start");
System.out.println("Hello World...");
System.out.println("Test end");
}
}

选中 java 文件,右键 -> Show Bytecode Outline 选中 ASMifield tab 可以看到转化后的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
package asm.sorra.tracesonar.main.aopsample;

import java.util.*;

import org.objectweb.asm.*;

public class ExpectedDump implements Opcodes {

public static byte[] dump() throws Exception {

ClassWriter cw = new ClassWriter(0);
FieldVisitor fv;
MethodVisitor mv;
AnnotationVisitor av0;

cw.visit(52, ACC_PUBLIC + ACC_SUPER, "sorra/tracesonar/main/aopsample/Expected", null, "java/lang/Object", null);

cw.visitSource("Expected.java", null);

{
mv = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
mv.visitCode();
Label l0 = new Label();
mv.visitLabel(l0);
mv.visitLineNumber(3, l0);
mv.visitVarInsn(ALOAD, 0);
mv.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
mv.visitInsn(RETURN);
Label l1 = new Label();
mv.visitLabel(l1);
mv.visitLocalVariable("this", "Lsorra/tracesonar/main/aopsample/Expected;", null, l0, l1, 0);
mv.visitMaxs(1, 1);
mv.visitEnd();
}
{
mv = cw.visitMethod(ACC_PUBLIC, "sayHello", "()V", null, null);
mv.visitCode();
Label l0 = new Label();
mv.visitLabel(l0);
mv.visitLineNumber(5, l0);
mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv.visitLdcInsn("Test start");
mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
Label l1 = new Label();
mv.visitLabel(l1);
mv.visitLineNumber(6, l1);
mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv.visitLdcInsn("Hello World...");
mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
Label l2 = new Label();
mv.visitLabel(l2);
mv.visitLineNumber(7, l2);
mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv.visitLdcInsn("Test end");
mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
Label l3 = new Label();
mv.visitLabel(l3);
mv.visitLineNumber(8, l3);
mv.visitInsn(RETURN);
Label l4 = new Label();
mv.visitLabel(l4);
mv.visitLocalVariable("this", "Lsorra/tracesonar/main/aopsample/Expected;", null, l0, l4, 0);
mv.visitMaxs(2, 1);
mv.visitEnd();
}
cw.visitEnd();

return cw.toByteArray();
}
}

其中类似如下的代码使一些行号和变量的处理,可以删掉不要,不影响结果

1
2
3
4
5
6
7
Label l0 = new Label();
mv.visitLabel(l0);
mv.visitLineNumber(3, l0);
...
Label l4 = new Label();
mv.visitLabel(l4);
mv.visitLocalVariable("this", "Lsorra/tracesonar/main/aopsample/Expected;", null, l0, l4, 0);

将自动生成的文件里的冗余语句删掉,加一个 main 方法,生成文件并存放到根目录下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class ExpectedDump {
public static byte[] dump() throws Exception {
...
return cw.toByteArray();
}

public static void main(String[] args) throws Exception {
byte[] updated = dump();

try (FileOutputStream fos = new FileOutputStream("Expected.class")) {
fos.write(updated);
}
System.out.println("Write success...");
}
}

运行该 Java 文件,可以看到 project 的根目录下有生成一个名为 ‘Expected.class’ 的文件,在 IDEA 里面浏览它,编辑器会自动给出反编译结果,可以发现,在目标语句前后已经加上了我们要的 ‘Test Start/End’ 的 debug 语句了。