ZB-038-02MyBatis的xml方式

Mapper核心有两种实现

  • Mapper:接口由MyBatis动态代理 (上一篇就是这种方式)
    • 优点:方便
    • 缺点:SQL复杂的时候不够方便
  • Mapper:用XML编写复杂SQL
    • 优点:可以方便地使用MyBatis强大功能
    • 缺点:SQL和代码分离

使用xml写复杂sql

  • 继续看文档的Exploring Mapped SQL Statements部分
  • 新建 src/main/resources/db/myBatis/UserMapper.xml
    内容如下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    <?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>
  • 修改内容

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    <?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-->
    <mapper namespace="com.sql.xml.UserMapper">
    <select id="selectUser" resultType="map">
    select * from user
    </select>
    </mapper>
  • 把我们的UserMapper.xml 添加到config.xml里

    1
    2
    3
    4
    <mappers>
    <!--<mapper class="com.github.hcsp.sql.Sql$UserMapper"/>-->
    <mapper resource="db/myBatis/UserMapper.xml"/>
    </mappers>
  • 如何用这个xml呢? 很明显 xml里的sql和代码分离了,让你来回找目录

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    public static void main(String[] args) throws SQLException, IOException {
    String resource = "db/myBatis/config.xml";
    InputStream inputStream = Resources.getResourceAsStream(resource);
    SqlSessionFactory sqlSessionFactory =
    new SqlSessionFactoryBuilder().build(inputStream);

    try (SqlSession session = sqlSessionFactory.openSession()) {

    // xml方式
    System.out.println( session.selectList("com.sql.xml.UserMapper.selectUser"));
    }
    }

    // 如果报错可能你没添加这个 UserMapper.xml到 config.xml里

Mapper的一切

  • parameterType
    • 参数的 ${} 和 #{}
    • 参数是按照 JavaBean约定读取的 getter/setter
  • resultType

    • typeAlias 简化 resultType

      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
      # 在UserMapper.xml resultType 很长
      <mapper namespace="com.sql.xml.UserMapper">
      <select id="selectUser" resultType="com.github.hcsp.sql.Sql$User">
      select * from user
      </select>
      </mapper>


      # typeAlias 帮你做一个映射 别名 注意它必须放在前面
      # 第一步修改 config.xml
      <configuration>
      <settings>
      <setting name="logImpl" value="LOG4J"/>
      </settings>
      <!-- 添加别名映射 -->
      <typeAliases>
      <typeAlias alias="User" type="com.github.hcsp.sql.Sql$User"/>
      </typeAliases>
      <environments default="development">
      <environment id="development">
      <transactionManager type="JDBC"/>
      <dataSource type="POOLED">
      <property name="driver" value="org.h2.Driver"/>
      <property name="url" value="jdbc:h2:file:/Users/huangjiaxi/Downloads/practise-select-sql/target/test"/>
      <property name="username" value="root"/>
      <property name="password" value="Jxi1Oxc92qSj"/>
      </dataSource>
      </environment>
      </environments>
      <mappers>
      <mapper resource="db/myBatis/UserMapper.xml"/>
      </mappers>
      </configuration>

      # 第二步修改 UserMapper.xml
      <mapper namespace="com.sql.xml.UserMapper">
      <select id="selectUser" resultType="User">
      select * from user
      </select>
      </mapper>
    • 写参数是按照 Java Bean约定的 getter/setter

  • Association

返回为map

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 修改UserMapper.xml
<mapper namespace="com.sql.xml.UserMapper">
<select id="selectUser" resultType="map">
select * from user
</select>
</mapper>

# 此时返回的是 map
# 如下
[
{CREATED_AT=2019-10-31 21:30:05.072, ADDRESS=beijing, TEL=tel1, UPDATED_AT=2019-10-31 21:30:05.072, ID=1, NAME=zhangsan},
{CREATED_AT=2019-10-31 21:30:05.072, ADDRESS=shanghai, TEL=tel2, UPDATED_AT=2019-10-31 21:30:05.072, ID=2, NAME=lisi},
{CREATED_AT=2019-10-31 21:30:05.072, ADDRESS=shanghai, TEL=tel3, UPDATED_AT=2019-10-31 21:30:05.072, ID=3, NAME=wangwu},
{CREATED_AT=2019-10-31 21:30:05.072, ADDRESS=shenzhen, TEL=tel4, UPDATED_AT=2019-10-31 21:30:05.072, ID=4, NAME=zhangsan}
]

我想返回的是一个User

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 修改UserMapper.xml
# com.github.hcsp.sql.Sql$User "$"代表连接的是内部类
<mapper namespace="com.sql.xml.UserMapper">
<select id="selectUser" resultType="com.github.hcsp.sql.Sql$User">
select * from user
</select>
</mapper>

# 返回数据为
[
User{id=1, name='zhangsan', tel='tel1', address='beijing'},
User{id=2, name='lisi', tel='tel2', address='shanghai'},
User{id=3, name='wangwu', tel='tel3', address='shanghai'},
User{id=4, name='zhangsan', tel='tel4', address='shenzhen'}
]

他们的值是怎么设置进去的?

答案是:Java Bean

1
2
3
如果没有 setId() 会咋样?

答案是 扫这个 Bean 是否是 id 这个 field

需求变更,我要找id=1的用户

修改 UserMapper.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
27
28
29
30
31
<mapper namespace="com.sql.xml.UserMapper">
<select id="selectUser" resultType="User">
select * from user where id=#{id}
</select>
</mapper>

修改 之前的查询语句
public static void main(String[] args) throws SQLException, IOException {
// ...

try (SqlSession session = sqlSessionFactory.openSession()) {

User user = new User();
user.id = 3;
// 传递一个 user 对象进去
System.out.println( session.selectList(
"com.sql.xml.UserMapper.selectUser",
user
)
);

// ...

}
}

// 返回
DEBUG [main] - ==> Preparing: select * from user where id=?
DEBUG [main] - ==> Parameters: 3(Integer)
DEBUG [main] - <== Total: 1
[User{id=3, name='wangwu', tel='tel3', address='shanghai'}]

参数的 ${} 和 #{}的区别

  • #{} 把传入的值带入, 防sql注入
  • ${} 替换这个值,它会带来 “注入” 问题
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
User user = new User();
user.name = "wangwu";
// 传递一个 user 对象进去
System.out.println( session.selectList(
"com.sql.xml.UserMapper.selectUser",
user
)
);

# 如果 sql是这样
select * from user where name=#{name}
select * from user where name="wangwu"


# 如果 sql是这样
select * from user where name=${name}
select * from user where name=wangwu // 这会报错的
你只能这样
select * from user where name='${name}'

这个User对象是必须的吗?

不是:你可以用map

1
2
3
4
5
6
7
Map<String,String> param = new HashMap<>();
param.put("name","wangwu");
System.out.println( session.selectList(
"com.sql.xml.UserMapper.selectUser",
param
)
);

ZB-038-01MyBatis

MyBatis是什么

  • 一个ORM框架
  • 国内基本都在用
  • 好处就是:简单/方便

为什么需要MyBatis?

之前的JDBC sql练习

注意是裸的sql 还用了PreparedStatement

我的PR

  • 过程非常痛苦
  • 不感受到痛苦就没动力学新技术

ORM

  • 对象关系映射
    • 自动完成对象到数据库的映射
  • Association
    • 自动装配对象

从零开始MyBatis

  • 官网就够了
  • 首先配置日志框架,可以极大的提高排查问题的效率
  • 然后配置数据源
  • Mapper:接口由MyBatis动态代理
    • 优点:方便
    • 缺点:SQL复杂的时候不够方便
  • Mapper:用XML编写复杂SQL
    • 优点:可以方便地使用MyBatis强大功能
    • 缺点:SQL和代码分离

数据库到底是什么

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
数据库实际就是一个URL

你的webApp应用
-------------
| JVM |
| | url
| |-------> Database
| | 连接串
-------------

那 MyBatis呢?

你的webApp应用
-------------
| JVM |
| | url
| |-------> Database
| MyBatis| 连接串
-------------
MyBatis它只是JVM里运行的一小段程序而已
它通过底层调用JDBC和数据库进行交互


---------
obj --> | |
|MyBatis| --> sql语句
obj <-- | |
---------

MyBatis从零开始

  • 拷贝项目在这个项目的基础上进行MyBatis配置
  • 打开官网 直接看英文
  • 点击 Getting Started
  • 看到第一段就是让你添加依赖到 pom.xml里

    1
    2
    3
    4
    5
    <dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis</artifactId>
    <version>x.x.x</version>
    </dependency>
  • 然后 搜索 mybatis maven

  • 选择最近版的就行

    1
    2
    3
    4
    5
    6
    7
    添加到pom.xml里 刷新一下
    <!-- https://mvnrepository.com/artifact/org.mybatis/mybatis -->
    <dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis</artifactId>
    <version>3.5.2</version>
    </dependency>
  • Building SqlSessionFactory from 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
    27
    // 文档提供了例子代码
    String resource = "org/mybatis/example/mybatis-config.xml";
    InputStream inputStream = Resources.getResourceAsStream(resource);
    SqlSessionFactory sqlSessionFactory =
    new SqlSessionFactoryBuilder().build(inputStream);

    // 新建一个配置文件
    <?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="${driver}"/>
    <property name="url" value="${url}"/>
    <property name="username" value="${username}"/>
    <property name="password" value="${password}"/>
    </dataSource>
    </environment>
    </environments>
    <mappers>
    <mapper resource="org/mybatis/example/BlogMapper.xml"/>
    </mappers>
    </configuration>
  • 注意 maven的项目约定就是一切资源放在 resources 下 (图片/配置等)

  • 所以我们在 src/main/resources/db/myBatis/config.xml 内容同上
  • 运行 mvn initialize 初始化数据
  • 修改我们的配置文件src/main/resources/db/myBatis/config.xml 的内容
  • 因为我们的数据库是 h2
  • 搜h2 database driver classname 得到 org.h2.Driver
  • 修改配置为
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?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="org.h2.Driver"/>
<!--
<property name="url" value="jdbc:h2:file:<你的项目文件夹地址>/target/test"/>
-->
<property name="username" value="root"/>
<property name="password" value="Jxi1Oxc92qSj"/>
</dataSource>
</environment>
</environments>
<mappers>
<!-- 先注释掉这句 因为你还没配置 -->
<!-- <mapper resource="org/mybatis/example/BlogMapper.xml"/> -->
</mappers>
</configuration>
  • src/main/java/com.github.hcsp.sql.Sql.java
  • 修改一下
  • 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
    public class Sql {
    // 用户信息
    public static class User {
    Integer id;
    String name;
    String tel;
    String address;

    @Override
    public String toString() {
    return "User{" + "id=" + id + ", name='" + name + '\'' + ", tel='" + tel + '\'' + ", address='" + address + '\'' + '}';
    }
    }

    interface UserMapper{
    @Select("select * from user")
    List<User> getUsers();
    }

    // 注意,运行这个方法之前,请先运行mvn initialize把测试数据灌入数据库
    public static void main(String[] args) throws SQLException, IOException {
    String resource = "db/myBatis/config.xml";
    InputStream inputStream = Resources.getResourceAsStream(resource);
    SqlSessionFactory sqlSessionFactory =
    new SqlSessionFactoryBuilder().build(inputStream);

    try (SqlSession session = sqlSessionFactory.openSession()) {
    UserMapper mapper = session.getMapper(UserMapper.class);
    System.out.println(mapper.getUsers());
    }
    }

    }

    // 报错了
    Exception in thread "main" org.apache.ibatis.binding.BindingException: Type interface com.github.hcsp.sql.Sql$UserMapper is not known to the MapperRegistry.
    at org.apache.ibatis.binding.MapperRegistry.getMapper(MapperRegistry.java:47)
    at org.apache.ibatis.session.Configuration.getMapper(Configuration.java:779)
    at org.apache.ibatis.session.defaults.DefaultSqlSession.getMapper(DefaultSqlSession.java:291)
    at com.github.hcsp.sql.Sql.main(Sql.java:233)

    直接搜索这个错误 得到答案 https://stackoverflow.com/questions/4263832/type-interface-is-not-known-to-the-mapperregistry-exception-using-mybatis?r=SearchResults
    意思是你要注册一下在xml里
    <mappers>
    <mapper class="com.github.hcsp.sql.Sql$UserMapper"/>
    </mappers>

    注意 内部类要用 $

    此时成功查询出了数据

此时你该惊叹它的强大

1
2
3
4
5
6
interface UserMapper{
@Select("select * from user")
List<User> getUsers();
}

你就写了一个接口,都没去写实现,就能拿到List<User>

配置日志

在我们的 config.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
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
<configuration>
<settings>
...
<setting name="logImpl" value="LOG4J"/>
...
</settings>
</configuration>
添加到我们的代码里

<configuration>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="org.h2.Driver"/>
<property name="url" value="jdbc:h2:file:/Users/admin/Desktop/practise-select-sql/target/test"/>
<property name="username" value="root"/>
<property name="password" value="Jxi1Oxc92qSj"/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper class="com.github.hcsp.sql.Sql$UserMapper"/>
<!--<mapper resource="org/db.myBatis/example/BlogMapper.xml"/>-->
</mappers>
<settings>
<setting name="logImpl" value="LOG4J"/>
</settings>
</configuration>

结果 <configuration> 飙红了 为什么?
踩坑不够多

答案是 settings要放在最前面
如下
<configuration>
<settings>
<setting name="logImpl" value="LOG4J"/>
</settings>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="org.h2.Driver"/>
<property name="url" value="jdbc:h2:file:/Users/admin/Desktop/practise-select-sql/target/test"/>
<property name="username" value="root"/>
<property name="password" value="Jxi1Oxc92qSj"/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper class="com.github.hcsp.sql.Sql$UserMapper"/>
<!--<mapper resource="org/db.myBatis/example/BlogMapper.xml"/>-->
</mappers>

</configuration>

运行我们的程序,结果报错了
Caused by: java.lang.ClassNotFoundException: org.apache.log4j.Priority
意思你引入了配置 但是没告诉这个类在哪 所以找不到
搜索 maven org.apache.log4j.Priority
得到 https://mvnrepository.com/artifact/log4j/log4j

复制它的坐标 到 pom.xml里
<!-- https://mvnrepository.com/artifact/log4j/log4j -->
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>

继续运行项目 成功 但是打印了警告信息

继续看文档,说要配置一下

1
2
3
4
5
6
7
8
9
10
新建 src/main/resources/log4j.properties 文件

# Global logging configuration
log4j.rootLogger=ERROR, stdout
# MyBatis logging configuration...
log4j.logger.org.mybatis.example.BlogMapper=TRACE
# Console output...
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%5p [%t] - %m%n

继续运行项目,发现警告消失了

日志是需要系统学习的,因为他有不同的等级

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
之所以刚刚没有任何信息打印是因为 日志等级是 ERROR

我们修改为 DEBUG

log4j.rootLogger=DEBUG, stdout

运行项目,此时能清楚的看到sql语句执行过程
DEBUG [main] - Logging initialized using 'class org.apache.ibatis.logging.log4j.Log4jImpl' adapter.
DEBUG [main] - Logging initialized using 'class org.apache.ibatis.logging.log4j.Log4jImpl' adapter.
DEBUG [main] - PooledDataSource forcefully closed/removed all connections.
DEBUG [main] - PooledDataSource forcefully closed/removed all connections.
DEBUG [main] - PooledDataSource forcefully closed/removed all connections.
DEBUG [main] - PooledDataSource forcefully closed/removed all connections.
DEBUG [main] - Opening JDBC Connection
DEBUG [main] - Created connection 1848125895.
DEBUG [main] - Setting autocommit to false on JDBC Connection [conn0: url=jdbc:h2:file:/Users/admin/Desktop/practise-select-sql/target/test user=ROOT]
DEBUG [main] - ==> Preparing: select * from user
DEBUG [main] - ==> Parameters:
DEBUG [main] - <== Total: 4
[User{id=1, name='zhangsan', tel='tel1', address='beijing'}, User{id=2, name='lisi', tel='tel2', address='shanghai'}, User{id=3, name='wangwu', tel='tel3', address='shanghai'}, User{id=4, name='zhangsan', tel='tel4', address='shenzhen'}]
DEBUG [main] - Resetting autocommit to true on JDBC Connection [conn0: url=jdbc:h2:file:/Users/admin/Desktop/practise-select-sql/target/test user=ROOT]
DEBUG [main] - Closing JDBC Connection [conn0: url=jdbc:h2:file:/Users/admin/Desktop/practise-select-sql/target/test user=ROOT]
DEBUG [main] - Returned connection 1848125895 to pool.

ZB-037-02注解如何工作的

注解是如何⼯作的?

  • 注解仅仅是⼀段信息,它⾃⼰⽆法⼯作
  • 换句话说,没有东⻄处理它们的话,注解没有任何卵⽤

打日志需求

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
public class Main {
public static void main(String[] args) {
MyService service = new MyService();
service.queryDatabase(1);
service.provideHttpResponse("123");

// 手动在方法 内部 加 start - end

// 假设你有 100个方法呢?
// 假设打印的内容修改了呢?
// 假设打印的地方修改了呢?
}
}

class MyService {
public void queryDatabase(int param){
System.out.println("start");
System.out.println("query db" + param);
System.out.println("end");
}

public void provideHttpResponse(String param){
System.out.println("start");
System.out.println("provide http service" + param);
System.out.println("end");
}
}

我们创建一个注解 @Log

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public @interface Log {
}


public class MyService {
@Log
public void queryDatabase(int param){
System.out.println("query db" + param);
}

@Log
public void provideHttpResponse(String param){
System.out.println("provide http service" + param);
}
}

public class Main {
public static void main(String[] args) {
MyService service = new MyService();
service.queryDatabase(1);
service.provideHttpResponse("123");
}
}
  • 运行后你发现,并没打印日志

动态字节码增强

  • 我们现在有一个 MyService 的类说明书
    • 它身上只有几个注解,但是没有触发任何的行为
  • 我们可以 动态生产一份新的说明书 如 MyServicePlus, 但是它并没有对应的字节码文件存在
  • MyServicePlus 完全是“运行时” 我们凭空根据 MyService 说明书 捏造出来的一份说明书
  • 这就是 动态字节码增强技术
    • 在此过程我们会 扫描这份说明书 提到@Log的地方,并把它的功能进行增强
    • 提供原先不存在的功能
    • 好处是:原先的代码不用改

我们用一个现有的轮子 —— bytebuddy

引入依赖

1
2
3
4
5
6
<!-- https://mvnrepository.com/artifact/net.bytebuddy/byte-buddy -->
<dependency>
<groupId>net.bytebuddy</groupId>
<artifactId>byte-buddy</artifactId>
<version>1.10.1</version>
</dependency>

抄文档

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
import net.bytebuddy.ByteBuddy;
import net.bytebuddy.description.method.MethodDescription;
import net.bytebuddy.implementation.MethodDelegation;
import net.bytebuddy.implementation.bind.annotation.SuperCall;
import net.bytebuddy.matcher.ElementMatcher;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class Main {
public static void main(String[] args) throws InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException {

// 通过注解
MyService service2 = enhanceByAnnotation();
service2.queryDatabase(1);
service2.provideHttpResponse("abc");
service2.noLog();
}

private static MyService enhanceByAnnotation() throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
return new ByteBuddy()
.subclass(MyService.class)
.method(new FilterMethodAnnotatedWithLogMatcher())
.intercept(MethodDelegation.to(LoggerInterceptor.class))
.make()
.load(Main.class.getClassLoader())
.getLoaded()
.getConstructor()
.newInstance();
}

public static class LoggerInterceptor {
public static List<String> log(@SuperCall Callable<List<String>> zuper)
throws Exception {
System.out.println("before method called");
try {
return zuper.call();
} finally {
System.out.println("method end");
}
}
}

static class FilterMethodAnnotatedWithLogMatcher implements ElementMatcher<MethodDescription>{

@Override
public boolean matches(MethodDescription target) {
List<String> methodsWithLog = Stream.of(MyService.class.getMethods())
.filter(FilterMethodAnnotatedWithLogMatcher::isAnnotatedWithLog)
.map(Method::getName)
.collect(Collectors.toList());

return methodsWithLog.contains(target.getName());
}

private static boolean isAnnotatedWithLog(Method method){
return method.getAnnotation(Log.class)!=null;
}
}
}
  • @Log
1
2
3
4
5
6
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

@Retention(RetentionPolicy.RUNTIME) // 这个非常重要,代表注解的保留期
public @interface Log {
}
  • MyService.class
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class MyService {
@Log
public void queryDatabase(int param){
System.out.println("query db" + param);
}

@Log
public void provideHttpResponse(String param){
System.out.println("provide http service" + param);
}

public void noLog(){
System.out.println("I have no log!");
}
}

代码仓库

ZB-037-01注解

注解是什么

注解和反射是分不开的
注解和反射是分不开的
注解和反射是分不开的

  • 还记得Class是什么么?
    • Class是Java类的说明书(堆里的某个地方存着)
    • 你(还可以通过反射)或者JVM通过阅读该说明书,创建类的实例
  • 注解就是说明书(class文件)中的⼀⼩段信息/⽂本/标记/标签
    • Annotation @xxx
    • 可以携带参数@xxx(a,b)
    • 可以在运⾏时被阅读

举例

1
2
3
4
5
6
7
你的衣服里,有个说明书写着 涤纶 不可水洗... 一些洗涤信息

狼人杀 的身份 狼人/村民...
王者荣耀的位置 打野/法师/射手/ADC/肉
渣男 ...

这些都是一种标签,它只提供一段“标签” ,怎么处理不由它决定

例子代码 :Spring中的大量注解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Controller
public class AuthController {
private UserService userService;

private AuthenticationManager authentionManager;

@Inject
public AuthController(UserService userService , AuthenticationManager authentionManager) {
this.userService = userService;
this.authentionManager = authentionManager;
}

@GetMapping("/auth")
@ResponseBody //方法的返回值限定在 response body里
public Object auth(){
...
}
}

这就是注解的一切。

为什么加了@xxx 之后就莫名其妙成功了?

  • 注解就是一小段信息,至于怎么处理注解这段信息,不是注解关心的问题

注解的写法

  • 新建⼀个类的时候选择注解 Annotation
  • 元注解(可以放在注解上的注解)
    • @Rentention 编译后可以被保留
    • @Target 可以标记的位置
    • @Documented
    • @Inherited 继承 子类还可以看到这个注解 没卵用
    • @Repeatable 可以重复 平时用不到

写一个我们的注解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com;
// 只提供一个标识,怎么处理 不由它决定
@interface SpringBootApplication {
}

@SpringBootApplication
public class MyApplication {

@SpringBootApplication
private String user;

@SpringBootApplication
public void OrderService(){

}
}
// 可以加在 Class上
// 可以加在 成员变量上
// 可以加在 方法上

注解 @Target@

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
// 只允许放在方法上
@Target(ElementType.METHOD)
@interface SpringBootApplication {
}

// 可以设置 那些地方 可以放置“注解”
@Target({
ElementType.METHOD, // 只允许放在方法上
ElementType.CONSTRUCTOR,// 只允许放在构造器上
ElementType.FIELD,// 只允许放在 字段上
ElementType.TYPE ,// 只允许放在 类型上
ElementType.PARAMETER, // 只允许放在 参数上
ElementType.TYPE_PARAMETER, // 类型参数
ElementType.PACKAGE , // 放在包上
})
@interface SpringBootApplication {
}

@SpringBootApplication
public class MyApplication {

@SpringBootApplication
public MyApplication(String user) {
this.user = user;
}

@SpringBootApplication
private String user;

@SpringBootApplication
public void OrderService(@SpringBootApplication String xx){

}
}

@Retention注解 :编译的时候被保留

1
2
3
4
5
6
7
8
9
@Retention(RetentionPolicy.CLASS) 
@interface SpringBootApplication {
}

// RetentionPolicy.SOURCE 注解会被编译器丢弃
// RetentionPolicy.CLASS 默认行为,会被虚拟机记录下来 记录在 class文件里 但不会被虚拟机运行的时候保留
// RetentionPolicy.RUNTIME 注解会被记录在编译器也会在 虚拟机运行时 保留

// 反射拿不到 SOURCE/CLASS 等级的注解信息

注解的属性

注解的参数可以有哪些?

  • 基本数据类型 + String + Class以及它们的数组
    1
    2


  • 默认值
  • 名为value的属性
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
比如一件外套 
它的说明书(注解)

@洗涤手段([干洗])
class 外套

@洗涤手段([水洗])
class 毛衣

看代码
package com;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({
ElementType.METHOD,
ElementType.CONSTRUCTOR,
ElementType.FIELD,
ElementType.TYPE,
ElementType.PARAMETER,
ElementType.TYPE_PARAMETER,
ElementType.PACKAGE
})
@Retention(RetentionPolicy.SOURCE)
@interface 洗涤手段 {

// 属性仅仅支持 基本数据类型 + String + 类 以及它们的数组
String value() default "干洗";
// 成分
String[] 成分() default "纯棉";

// 还可以这样 Class a();
}

@洗涤手段(value ="水洗",成分={"纯棉","涤纶"})
public class 毛衣 {

@洗涤手段
private String name;

@洗涤手段
public void 洗衣服(@洗涤手段 String xx){

}
}

JDK的⾃带注解(了解即可)

  • @Deprecated 提示你这是个被废弃的方法
  • @Override
  • @SuppressWarnings 忽略警告
  • @FunctionalInterface 标记警告 仅仅用来标记 删了也能正常工作

@Override 目的是防止你犯错误

  • 它不是强制的
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Main {
static class Animal{
void xxxx1234567890(){

}
}

static class Cat extends Animal{
@Override
void xxxx1234567890() {
}

// 没有注解 你拼错了 以为自己重写了 实际没有
void xxxx12345678901() {
}
}

public static void main(String[] args) {
}
}

@SuppressWarnings

1
2
3
4
5
6
7
8
// 忽略警告
@SuppressWarnings("unchecked")
public static void main(String[] args) {
List list = new ArrayList<Object>();

list.add(0);
list.get(0);
}

扩展链接

ZB-036-02泛型的绑定

先看这样的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
public static Integer max(Integer a , Integer b){
return a > b ? a : b;
}

public static Long max(Long a , Long b){
return a > b ? a : b;
}

public static Double max(Double a , Double b){
return a > b ? a : b;
}

// 很啰嗦很重复

有了泛型

1
2
3
4
5
6
7
8
9
10
11
// 第一步简化代码成这样
public static T max (T a, T b){
// 此时 泛型类型实际是 Object 因此 是不能用 > 或者 < 的
return a > b ? a : b;
}
// 第二步 你知道 Integer/Long/Double 都是 Number的子类型 它们都是实现了Comparable接口 因此是可以比较的

// 定义传递的泛型的一个约定 是 继承 Comparable 的因此可以 compareTo 的
public static <T extends Comparable> T max (T a, T b){
return a.compareTo(b) >= 0 ? a : b;
}

你可以这样

1
2
3
4
max(1.0,2.0); // 你传递的是 Double 因此返回是 Double
max(1,2); // 你传递的是 Integer 因此返回是 Integer
max(1L,2L); // 你传递的是 Long 因此返回是 Long
max("aaa","bbb"); // 你传递的是 String 因此返回是 String

让你疑惑的地方 max("aaa",1) 竟然通过了编译为什么?

  • 泛型的约束条件是 <T extends Comparable>
  • String/Integer 都是 Comparable 的实现类
1
2
3
4
5
6
7
public static <T extends Comparable> T max (T a, T b){
return a.compareTo(b) >= 0 ? a : b;
}


max("aaa",1)
// 等价于 max(Comparable,Comparable)

虽然能通过编译,但是运行会报错,在本文最后会解释

传递多个类型

1
2
3
public static <A extends Comparable,B extends Comparable,C extends Comparable> A max2 (A a,  B b, C c){
return null;
}

取第一个list第一个元素

1
2
3
4
5
6
7
public static String first(List<String> list){
return list.get(0);
}

public static Integer first(List<Integer> list){
return list.get(0);
}

使用泛型后

1
2
3
public static <T> T first(List<T> list){
return list.get(0);
}

泛型的绑定

  • ? extends要求泛型是某种类型及其子类型
  • ? super 要求泛型是某种类型及其父类型
  • Collections.sort

按照参数绑定

1
2
3
public static <T extends Comparable> T max (T a, T b){
return a.compareTo(b) >= 0 ? a : b;
}

按照返回值绑定

让一个方法的返回值根据接受者动态的去改变

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Main {

public static void main(String[] args) {

String s = cast("");
Object o = cast(new Object());
Integer i = cast(1);
}

public static <T> T cast(Object obj){
return (T) obj;
}
}

? super xx

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
package com.loder.demo;

import java.util.*;

public class Main {

static class Animal{}

static class Cat extends Animal{}

public static <T> void sort(List<T> list, Comparator<? super T> c){
list.sort(c);
}

static class AnimalComparator implements Comparator<Animal>{

@Override
public int compare(Animal o1, Animal o2) {
return 0;
}
}

static class CatComparator implements Comparator<Cat>{

@Override
public int compare(Cat o1, Cat o2) {
return 0;
}
}

public static void main(String[] args) {
// List<T> list, Comparator<? super T> c
// 除了 Cat 还可以是 Cat 的父类型
sort(new ArrayList<Cat>(),new CatComparator());
sort(new ArrayList<Cat>(),new AnimalComparator());
}

}

翻车问题重现

max("aa",1) 通过编译,但是运行报错?

  • 通过编译时因为刚才说了 String/Integer都是 Comparable 的子类型
  • 报错是因为Comparable没有泛型参数 导致传入的是一个裸类型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Main {

public static <T extends Comparable> T max (T a, T b){
return a.compareTo(b) >= 0 ? a : b;
}

public static void main(String[] args) {
max("aa",1);
}

}

// 报错
Exception in thread "main" java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String

修改代码

1
2
3
4
5
6
public static <T extends Comparable<T>> T max (T a, T b){
return a.compareTo(b) >= 0 ? a : b;
}

// 此时 编译期就报错了
max("aa",1);

练习

将方法泛型化

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
public class Main {
// 这里有四个结构、功能非常相似的方法,请尝试将其泛型化,以简化代码
// 泛型化之后的方法签名应该如下所示:
// public static boolean inAscOrder(T a, T b, T c)
public static boolean inAscOrder1(int a, int b, int c) {
return a <= b && b <= c;
}

public static boolean inAscOrder2(long a, long b, long c) {
return a <= b && b <= c;
}

public static boolean inAscOrder3(double a, double b, double c) {
return a <= b && b <= c;
}

public static <T extends Comparable<T>> boolean inAscOrder(T a, T b, T c){
return a.compareTo(b) <= 0 && b.compareTo(c) <= 0;
}

public static void main(String[] args) {
System.out.println(inAscOrder1(1, 2, 3));
System.out.println(inAscOrder2(1L, 2L, 3L));
System.out.println(inAscOrder3(1d, 2d, 3d));

System.out.println(inAscOrder(1, 2, 3));
System.out.println(inAscOrder(1L, 2L, 3L));
System.out.println(inAscOrder(1d, 2d, 3d));
}
}

泛型化的二叉树

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
package com.github.hcsp.generics;

import java.util.ArrayList;
import java.util.List;

public class Main {
static class IntBinaryTreeNode {
int value;
IntBinaryTreeNode left;
IntBinaryTreeNode right;
}

static class StringBinaryTreeNode {
String value;
StringBinaryTreeNode left;
StringBinaryTreeNode right;
}

static class DoubleBinaryTreeNode {
double value;
DoubleBinaryTreeNode left;
DoubleBinaryTreeNode right;
}

// 你看,上面三种"二叉树节点"结构相似,内容重复,请将其泛型化,以节省代码
static class BinaryTreeNode<T> {
T value;
BinaryTreeNode<T> left;
BinaryTreeNode<T> right;
}

// 泛型化之后,请再编写一个算法,对二叉树进行中序遍历,返回中序遍历的结果
public static <T> List<T> inorderTraversal(BinaryTreeNode<T> root) {
ArrayList<T> list = new ArrayList<>();
collectValue(root, list);
return list;
}

private static <T> void collectValue(BinaryTreeNode<T> node, ArrayList<T> list) {
if (node == null) {
return;
}
// 中序遍历递归 先左 再root 最后右
collectValue(node.left, list);
list.add(node.value);
collectValue(node.right, list);
}
}

ZB-036-01泛型的由来和泛型擦除

没有泛型之前实现类型安全

  • 利用组合
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
public class Main {

// 只能存放字符串 的 list
public static class StringList{
public List list = new ArrayList();

public void add(String s){
list.add(s);
}

public String get(int i){
return (String) list.get(i);
}

public int size(){
return list.size();
}
}

// 只能存放整数 的 list
public static class IntList{
public List list = new ArrayList();

public void add(Integer s){
list.add(s);
}

public Integer get(int i){
return (int) list.get(i);
}

public int size(){
return list.size();
}
}

// 从 String 到 Object 的映射
public static class StringObjectMap{
Map map = new HashMap<>();

public void put(String key,Object value){
map.put(key,value);
}

public Object get(String key){
return map.get(key);
}

}

public static void main(String[] args) {
StringList stringList = new StringList();
stringList.add("a");
stringList.add("b");

IntList intList = new IntList();
intList.add(1);
intList.add(2);

StringObjectMap map = new StringObjectMap();
map.put("name","xxx");
map.put("age",18);
}

}

java1.5之后泛型出现了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Main {
public static void main(String[] args) {


ArrayList<String> stringList2 = new ArrayList<>();
stringList.add("a");
stringList.add("b");

ArrayList<Integer> intList2 = new ArrayList<>();
intList2.add(1);
intList2.add(2);

HashMap<String,Object> map2 = new HashMap<>();
map.put("name","xxx");
map.put("age",18);

}

}

此时你再也不用写之前麻烦的代码

看一看泛型类 ArrayList 的定义

1
2
3
4
5
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
...
}

ArrayList<E> 里的E是什么

  • 一个泛型有一个类型声明(声明在类上) ArrayList<E> E
  • 可以通过传递一个参数进去,把这个类型变成全新的类型
    1
    2
    3
    4
    5
    ArrayList<String>
    ArrayList<Integer>
    ArrayList<Cat>
    ArrayList<Dog>
    // 你传递什么类型 它就变成什么类型

问题来了

1
2
3
4
5
6
7
ArrayList<String> list1 = new ArrayList<>();
ArrayList<Integer> list2 = new ArrayList<>();

list1.add(1); // 报错 只能添加 String 类型
list2.add("1"); // 报错 只能添加 Integer 类型
// 报错 不能相互赋值
ArrayList<Integer> list1 = new ArrayList<String>();
  • 它俩的区别,它们还是同一个类吗?

答案是:它们是同一个类,也不是同一个类

它是同一个类,因为有 擦除

它不是同一个类,因为不能相互赋值

泛型好处

  • 类型安全
  • 方便
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
HashMap hashMap = new HashMap();
// 它的 key是 String, value 也是 String
hashMap.put("name","abc");
hashMap.put("age","18");

for (Object key: hashMap.keySet()) {
String stringKey = (String)key;
String stringValue = (String)hashMap.get(key);
}

// 如果你用的是泛型就不需要这样转换类型
HashMap<String,String> hashMap2 = new HashMap();
// 它的 key是 String, value 也是 String
hashMap2.put("name","abc");
hashMap2.put("age","18");

for (String key: hashMap2.keySet()) {
String key2 = key;
String value2 = hashMap2.get(key);
}

因此有了泛型

  • 从此,我们可以省力的方法编写类型安全的代码
  • List
  • Map<String,Object>
  • Map<Stirng,List>

从无泛型的世界1.4到有泛型的世界1.5,那我们还要支持无泛型的“原始类型”的容器吗?

答案是:我们不想支持,但是不得不支持!

因为向后兼容。这样的好处是 JDK1.0可以无缝在最新的JDK上运行 而且已经过了20多年了

反观 Python2 / Python3 它为了更好的语法特性或改正一些缺陷放弃了向后兼容性,而java为了向后兼容性放弃了更好地语法特性

因为向后兼容性带来的问题

1
2
3
4
5
6
List list = new ArrayList();
list.add(new Object());
list.add(1);
list.add("123");
// 假设穿越到95年
// list 可以添加任何类型没有限制
  • java向后兼容性
  • 为了兼容性,只有两个选择
    • 1擦除 java的选择
    • 2搞一套全新的API,C#的选择

为什么选择擦除

1
2
3
4
5
6
7
JDK1.0的时候就存在的容器 
它们是被废弃的容器
Vector
HashTable
JDK1.2已经分裂了一次

JDK1.5 泛型的时候 如果在分裂 不是不行就是有点丑

怎么实现擦除的

Main.java

1
2
3
4
5
6
7
8
9
public class Main {
public static void main(String[] args) {

String[] arr = new String[2];
ArrayList<String> list = new ArrayList<>();


}
}

编译后 Main.class

1
2
3
4
5
6
7
8
9
public class Main {
public Main() {
}

public static void main(String[] args) {
String[] arr = new String[2];
new ArrayList();
}
}

此时查看它的字节码文件 (IDEA顶部工具栏view -> ShowByte code)

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 com/loder/demo/Main {

// compiled from: Main.java

// access flags 0x1
public <init>()V
L0
LINENUMBER 5 L0
ALOAD 0
INVOKESPECIAL java/lang/Object.<init> ()V
RETURN
L1
LOCALVARIABLE this Lcom/loder/demo/Main; L0 L1 0
MAXSTACK = 1
MAXLOCALS = 1

// access flags 0x9
public static main([Ljava/lang/String;)V
L0
LINENUMBER 9 L0
ICONST_2
ANEWARRAY java/lang/String
ASTORE 1
L1
LINENUMBER 10 L1
NEW java/util/ArrayList
DUP
INVOKESPECIAL java/util/ArrayList.<init> ()V
ASTORE 2
L2
LINENUMBER 12 L2
RETURN
L3
LOCALVARIABLE args [Ljava/lang/String; L0 L3 0
LOCALVARIABLE arr [Ljava/lang/String; L1 L3 1
LOCALVARIABLE list Ljava/util/ArrayList; L2 L3 2
// signature Ljava/util/ArrayList<Ljava/lang/String;>;
// declaration: list extends java.util.ArrayList<java.lang.String>
MAXSTACK = 2
MAXLOCALS = 3
}

// 你会惊讶的看到 ArrayList<String>上的 String 怎么没了

为什么说java的泛型是一个假泛型

  • 因为java的运行只看字节码,而字节码里ArrayList没有泛型信息 String (它被擦除了)

这就是为什么说java是假泛型的原因

因此称这个过程为 类型擦除

擦除带来的问题

  • Java的泛型是编译器的泛型
    • 泛型信息在运行期完全不保留
      1
      2
      3
      4
      5
      6
      7
      ArrayList<String> list1 = new ArrayList<>();
      list1.add(1); // 编译器报错 提示你不能添加 不是 String的类型到 list1

      List list = new ArrayList();
      list.add(new Object()); // 此时编译器会给你警告 说你声明的list没有泛型
      list.add(1);
      list.add("123");

编译器的警告

使用限定符List<?>

1
2
3
4
5
6
7
8
9
10
11
12
13
List list = new ArrayList(); 
list.add(new Object());
list.add(1);
list.add("123");

//你可以这样
List<?> list = new ArrayList();
// 此时编译器标红
list.add(new Object());

//你可以 alt + enter 代码就变成了这样
List<Object> list = new ArrayList();
list.add(new Object());

利用类型擦除绕过编译器检查

List<String>并不是 List<Object>的子类型

  • 类比String/Object,String[]/Object[]
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
public class Main {

static class Animal{}

static class Cat extends Animal{}

public static void foo(Animal a){}

public static void foo(Animal[] a){}

public static void foo(List<Animal> l){}

public static void main(String[] args) {
// 可以传递子类型进入
foo(new Animal());
foo(new Cat());

// 即使是数组也可以传递 子类型数组
foo(new Animal[2]);
foo(new Cat[2]);

foo(new ArrayList<Animal>());
foo(new ArrayList<Cat>()); // 报错 此时竟然不能传递子类型的容器
}

}

原因是:类型擦除你实际传递的是 new ArrayList()

另一个问题

1
2
3
4
5
6
7
public static void foo(List<Animal> l){}

public static void foo(List l){}

// 看上去不一样,但是会报错,因为泛型是有抹去的(擦除)
实际它们都是
public static void foo(List l){}

绕过编译器检查

1
2
3
4
5
6
7
8
9
10
ArrayList<Animal> list = new ArrayList<>();

// 你可以这样
ArrayList rawList = list;
rawList.add("");
rawList.add(1);
rawList.add(new Object());

// 还可以这样
ArrayList<Cat> catArrayList = (ArrayList) list;

数组是真泛型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Main {
public static void main(String[] args) {

String[] s = new String[2];
// 通过了检查
testStringArray(s);
}
// 数组是真泛型,即使你通过了编译期的类型检查,但是运行还是会报错

public static void testStringArray(Object[] o){
// 这里修改为 int 类型
o[0] = 1;
}

}

// 运行报错
Exception in thread "main" java.lang.ArrayStoreException: java.lang.Integer

但是如果是 List 就不会报错

  • 意思是只要通过编译器检查,就万事大吉了
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Main {

public static void main(String[] args) {

List<String> list = new ArrayList<>();
testListSafety((ArrayList)list);
}

public static void testListSafety(List<Object> o){
// 这里添加 int 类型
o.add(1);
}

}

// 不报错

结论

所谓的java泛型是编译期的,运行时就完全没了

ZB-035-05反射详解

一、Java 反射机制

Java 反射机制在程序运行时,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性。这种 动态的获取信息 以及 动态调用对象的方法 的功能称为 java 的反射机制

反射机制很重要的一点就是“运行时”,其使得我们可以在程序运行时加载、探索以及使用编译期间完全未知的 .class 文件。换句话说,Java 程序可以加载一个运行时才得知名称的 .class 文件,然后获悉其完整构造,并生成其对象实体、或对其 fields(变量)设值、或调用其 methods(方法)。

二、使用反射获取类的信息

为使得测试结果更加明显,我首先定义了一个 FatherClass 类(默认继承自 Object 类),然后定义一个继承自 FatherClass 类的 SonClass 类,如下所示。可以看到测试类中变量以及方法的访问权限不是很规范,是为了更明显得查看测试结果而故意设置的,实际项目中不提倡这么写。

FatherClass.java

1
2
3
4
5
6
public class FatherClass {
public String mFatherName;
public int mFatherAge;

public void printFatherMsg(){}
}

SonClass.java

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
public class SonClass extends FatherClass{

private String mSonName;
protected int mSonAge;
public String mSonBirthday;

public void printSonMsg(){
System.out.println("Son Msg - name : "
+ mSonName + "; age : " + mSonAge);
}

private void setSonName(String name){
mSonName = name;
}

private void setSonAge(int age){
mSonAge = age;
}

private int getSonAge(){
return mSonAge;
}

private String getSonName(){
return mSonName;
}
}

1.获取类的所有变量信息

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
/**
* 通过反射获取类的所有变量
*/
private static void printFields(){
//1.获取并输出类的名称
Class mClass = SonClass.class;
System.out.println("类的名称:" + mClass.getName());

//2.1 获取所有 public 访问权限的变量
// 包括本类声明的和从父类继承的
Field[] fields = mClass.getFields();

//2.2 获取所有本类声明的变量(不问访问权限)
//Field[] fields = mClass.getDeclaredFields();

//3. 遍历变量并输出变量信息
for (Field field :
fields) {
//获取访问权限并输出
int modifiers = field.getModifiers();
System.out.print(Modifier.toString(modifiers) + " ");
//输出变量的类型及变量名
System.out.println(field.getType().getName()
+ " " + field.getName());
}
}

以上代码注释很详细,就不再解释了。需要注意的是注释中 2.1 的 getFields() 与 2.2的 getDeclaredFields() 之间的区别,下面分别看一下两种情况下的输出。看之前强调一下:SonClass extends FatherClass extends Object

  • 调用 getFields() 方法,输出 SonClass 类以及其所继承的父类( 包括 FatherClass 和 Object ) 的 public 方法。注:Object 类中没有成员变量,所以没有输出

    1
    2
    3
    4
    类的名称:obj.SonClass
    public java.lang.String mSonBirthday
    public java.lang.String mFatherName
    public int mFatherAge
  • 调用 getDeclaredFields() , 输出 SonClass 类的所有成员变量,不问访问权限。

    1
    2
    3
    4
    类的名称:obj.SonClass
    private java.lang.String mSonName
    protected int mSonAge
    public java.lang.String mSonBirthday

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
/**
* 通过反射获取类的所有方法
*/
private static void printMethods(){
//1.获取并输出类的名称
Class mClass = SonClass.class;
System.out.println("类的名称:" + mClass.getName());

//2.1 获取所有 public 访问权限的方法
//包括自己声明和从父类继承的
Method[] mMethods = mClass.getMethods();

//2.2 获取所有本类的的方法(不问访问权限)
//Method[] mMethods = mClass.getDeclaredMethods();

//3.遍历所有方法
for (Method method :
mMethods) {
//获取并输出方法的访问权限(Modifiers:修饰符)
int modifiers = method.getModifiers();
System.out.print(Modifier.toString(modifiers) + " ");
//获取并输出方法的返回值类型
Class returnType = method.getReturnType();
System.out.print(returnType.getName() + " "
+ method.getName() + "( ");
//获取并输出方法的所有参数
Parameter[] parameters = method.getParameters();
for (Parameter parameter:
parameters) {
System.out.print(parameter.getType().getName()
+ " " + parameter.getName() + ",");
}
//获取并输出方法抛出的异常
Class[] exceptionTypes = method.getExceptionTypes();
if (exceptionTypes.length == 0){
System.out.println(" )");
}
else {
for (Class c : exceptionTypes) {
System.out.println(" ) throws "
+ c.getName());
}
}
}
}

同获取变量信息一样,需要注意注释中 2.1 与 2.2 的区别,下面看一下打印输出:

  • 调用 getMethods() 方法

    • 获取 SonClass 类所有 public 访问权限的方法,包括从父类继承的。打印信息中,printSonMsg() 方法来自 SonClass 类, printFatherMsg() 来自 FatherClass 类,其余方法来自 Object 类。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      类的名称:obj.SonClass
      public void printSonMsg( )
      public void printFatherMsg( )
      public final void wait( ) throws java.lang.InterruptedException
      public final void wait( long arg0,int arg1, ) throws java.lang.InterruptedException
      public final native void wait( long arg0, ) throws java.lang.InterruptedException
      public boolean equals( java.lang.Object arg0, )
      public java.lang.String toString( )
      public native int hashCode( )
      public final native java.lang.Class getClass( )
      public final native void notify( )
      public final native void notifyAll( )
  • 调用 getDeclaredMethods() 方法

    • 打印信息中,输出的都是 SonClass 类的方法,不问访问权限。
      1
      2
      3
      4
      5
      6
      类的名称:obj.SonClass
      private int getSonAge( )
      private void setSonAge( int arg0, )
      public void printSonMsg( )
      private void setSonName( java.lang.String arg0, )
      private java.lang.String getSonName( )

三、访问或操作类的私有变量和方法

在上面,我们成功获取了类的变量和方法信息,验证了在运行时 动态的获取信息 的观点。那么,仅仅是获取信息吗?我们接着往后看。

都知道,对象是无法访问或操作类的私有变量和方法的,但是,通过反射,我们就可以做到。没错,反射可以做到!下面,让我们一起探讨如何利用反射访问 类对象的私有方法 以及修改 私有变量或常量

老规矩,先上测试类。

注:

  1. 请注意看测试类中变量和方法的修饰符(访问权限);
  2. 测试类仅供测试,不提倡实际开发时这么写 : )

TestClass.java

1
2
3
4
5
6
7
8
9
10
11
12
public class TestClass {

private String MSG = "Original";

private void privateMethod(String head , int tail){
System.out.print(head + tail);
}

public String getMsg(){
return MSG;
}
}

3.1 访问私有方法

以访问 TestClass 类中的私有方法 privateMethod(...) 为例,方法加参数是为了考虑最全的情况,很贴心有木有?先贴代码,看注释,最后我会重点解释部分代码。

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
/**
* 访问对象的私有方法
* 为简洁代码,在方法上抛出总的异常,实际开发别这样
*/
private static void getPrivateMethod() throws Exception{
//1. 获取 Class 类实例
TestClass testClass = new TestClass();
Class mClass = testClass.getClass();

//2. 获取私有方法
//第一个参数为要获取的私有方法的名称
//第二个为要获取方法的参数的类型,参数为 Class...,没有参数就是null
//方法参数也可这么写 :new Class[]{String.class , int.class}
Method privateMethod =
mClass.getDeclaredMethod("privateMethod", String.class, int.class);

//3. 开始操作方法
if (privateMethod != null) {
//获取私有方法的访问权
//只是获取访问权,并不是修改实际权限
privateMethod.setAccessible(true);

//使用 invoke 反射调用私有方法
//privateMethod 是获取到的私有方法
//testClass 要操作的对象
//后面两个参数传实参
privateMethod.invoke(testClass, "Java Reflect ", 666);
}
}

需要注意的是,第3步中的 setAccessible(true) 方法,是获取私有方法的访问权限,如果不加会报异常 IllegalAccessException,因为当前方法访问权限是“private”的,如下:

1
java.lang.IllegalAccessException: Class MainClass can not access a member of class obj.TestClass with modifiers "private"

正常运行后,打印如下,调用私有方法成功:

1
Java Reflect 666

3.2 修改私有变量

以修改 TestClass 类中的私有变量 MSG 为例,其初始值为 “Original” ,我们要修改为 “Modified”。老规矩,先上代码看注释。

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
/**
* 修改对象私有变量的值
* 为简洁代码,在方法上抛出总的异常
*/
private static void modifyPrivateFiled() throws Exception {
//1. 获取 Class 类实例
TestClass testClass = new TestClass();
Class mClass = testClass.getClass();

//2. 获取私有变量
Field privateField = mClass.getDeclaredField("MSG");

//3. 操作私有变量
if (privateField != null) {
//获取私有变量的访问权
privateField.setAccessible(true);

//修改私有变量,并输出以测试
System.out.println("Before Modify:MSG = " + testClass.getMsg());

//调用 set(object , value) 修改变量的值
//privateField 是获取到的私有变量
//testClass 要操作的对象
//"Modified" 为要修改成的值
privateField.set(testClass, "Modified");
System.out.println("After Modify:MSG = " + testClass.getMsg());
}
}

此处代码和访问私有方法的逻辑差不多,就不再赘述,从输出信息看出 修改私有变量 成功:

1
2
Before Modify:MSG = Original
After Modify:MSG = Modified

3.3 修改私有常量

01.真的能修改吗?

常量是指使用 final 修饰符修饰的成员属性,与变量的区别就在于有无 final 关键字修饰。在说之前,先补充一个知识点。
Java 虚拟机(JVM)在编译 .java 文件得到 .class 文件时,会优化我们的代码以提升效率。其中一个优化就是:JVM 在编译阶段会把引用常量的代码替换成具体的常量值,如下所示(部分代码)。

编译前的 .java 文件:

1
2
3
4
5
6
//注意是 String  类型的值
private final String FINAL_VALUE = "hello";

if(FINAL_VALUE.equals("world")){
//do something
}

编译后得到的 .class 文件(当然,编译后是没有注释的):

1
2
3
4
5
private final String FINAL_VALUE = "hello";
//替换为"hello"
if("hello".equals("world")){
//do something
}

但是,并不是所有常量都会优化。经测试对于 int 、long 、boolean 以及 String 这些基本类型 JVM 会优化,而对于 Integer 、Long 、Boolean 这种包装类型,或者其他诸如 Date 、Object 类型则不会被优化。

总结来说:对于基本类型的静态常量,JVM 在编译阶段会把引用此常量的代码替换成具体的常量值。

这么说来,在实际开发中,如果我们想修改某个类的常量值,恰好那个常量是基本类型的,岂不是无能为力了?反正我个人认为除非修改源码,否则真没办法!

这里所谓的无能为力是指:我们在程序运行时刻依然可以使用反射修改常量的值(后面会代码验证),但是 JVM 在编译阶段得到的 .class 文件已经将常量优化为具体的值,在运行阶段就直接使用具体的值了,所以即使修改了常量的值也已经毫无意义了。

下面我们验证这一点,在测试类 TestClass 类中添加如下代码:

1
2
3
4
5
6
7
//String 会被 JVM 优化
private final String FINAL_VALUE = "FINAL";

public String getFinalValue(){
//剧透,会被优化为: return "FINAL" ,拭目以待吧
return FINAL_VALUE;
}

接下来,是修改常量的值,先上代码,请仔细看注释:

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
/**
* 修改对象私有常量的值
* 为简洁代码,在方法上抛出总的异常,实际开发别这样
*/
private static void modifyFinalFiled() throws Exception {
//1. 获取 Class 类实例
TestClass testClass = new TestClass();
Class mClass = testClass.getClass();

//2. 获取私有常量
Field finalField = mClass.getDeclaredField("FINAL_VALUE");

//3. 修改常量的值
if (finalField != null) {

//获取私有常量的访问权
finalField.setAccessible(true);

//调用 finalField 的 getter 方法
//输出 FINAL_VALUE 修改前的值
System.out.println("Before Modify:FINAL_VALUE = "
+ finalField.get(testClass));

//修改私有常量
finalField.set(testClass, "Modified");

//调用 finalField 的 getter 方法
//输出 FINAL_VALUE 修改后的值
System.out.println("After Modify:FINAL_VALUE = "
+ finalField.get(testClass));

//使用对象调用类的 getter 方法
//获取值并输出
System.out.println("Actually :FINAL_VALUE = "
+ testClass.getFinalValue());
}
}

上面的代码不解释了,注释巨详细有木有!特别注意一下第3步的注释,然后来看看输出,已经迫不及待了,擦亮双眼:

1
2
3
Before Modify:FINAL_VALUE = FINAL
After Modify:FINAL_VALUE = Modified
Actually :FINAL_VALUE = FINAL

结果出来了:

第一句打印修改前 FINAL_VALUE 的值,没有异议;

第二句打印修改后常量的值,说明FINAL_VALUE确实通过反射修改了;

第三句打印通过 getFinalValue() 方法获取的 FINAL_VALUE 的值,但还是初始值,导致修改无效!

这结果你觉得可信吗?什么,你还不信?问我怎么知道 JVM 编译后会优化代码?那要不这样吧,一起来看看 TestClass.java 文件编译后得到的 TestClass.class 文件

1
2
3
4
5
6
7
8
9
10
public class TestClass {
private final String FINAL_VALUE = "FINAL";

public TestClass() {
}

public String getFinalValue() {
return "FINAL";
}
}

看到了吧,getFinalValue() 方法直接 return “FINAL”!同时也说明了,程序运行时是根据编译后的 .class 来执行的

顺便提一下,如果你有时间,可以换几个数据类型试试,正如上面说的,有些数据类型是不会优化的。你可以修改数据类型后,根据我的思路试试,看输出觉得不靠谱就直接看 .classs 文件,一眼就能看出来哪些数据类型优化了 ,哪些没有优化。下面说下一个知识点。

02.我就是要修改常量!

方法一

事实上,Java 允许我们声明常量时不赋值,但必须在构造函数中赋值。你可能会问我为什么要说这个,这就解释:

我们修改一下 TestClass 类,在声明常量时不赋值,然后添加构造函数并为其赋值,大概看一下修改后的代码(部分代码 ):

1
2
3
4
5
6
7
8
9
10
11
public class TestClass {

//......
private final String FINAL_VALUE;

//构造函数内为常量赋值
public TestClass(){
this.FINAL_VALUE = "FINAL";
}
//......
}

现在,我们再调用上面贴出的修改常量的方法,发现输出是这样的:

1
2
3
Before Modify:FINAL_VALUE = FINAL
After Modify:FINAL_VALUE = Modified
Actually :FINAL_VALUE = Modified

纳尼,最后一句输出修改后的值了?对,修改成功了!还得看编译后的 TestClass.class

1
2
3
4
5
6
7
8
9
10
public class TestClass {
private final String FINAL_VALUE = "FINAL";

public TestClass() {
}

public String getFinalValue() {
return this.FINAL_VALUE;
}
}

解释一下:我们将赋值放在构造函数中,构造函数是我们运行时 new 对象才会调用的,所以就不会像之前直接为常量赋值那样,在编译阶段将 getFinalValue() 方法优化为返回常量值,而是指向 FINAL_VALUE ,这样我们在运行阶段通过反射修改敞亮的值就有意义啦。但是,看得出来,程序还是有优化的,将构造函数中的赋值语句优化了。再想想那句 程序运行时是根据编译后的 .class 来执行的 ,相信你一定明白为什么这么输出了!

方法二

请你务必将上面捋清楚了再往下看。接下来再说一种改法,不使用构造函数,也可以成功修改常量的值,但原理上都一样。去掉构造函数,将声明常量的语句改为使用三目表达式赋值:

1
2
private final String FINAL_VALUE
= null == null ? "FINAL" : null;

其实,上述代码等价于直接为 FINAL_VALUE 赋值 “FINAL”,但是他就是可以!至于为什么,你这么想:null == null ? "FINAL" : null 是在运行时刻计算的,在编译时刻不会计算,也就不会被优化,所以你懂得。

总结来说,不管使用构造函数还是三目表达式,根本上都是避免在编译时刻被优化,这样我们通过反射修改常量之后才有意义!好了,这一小部分到此结束!

最后的强调:

必须提醒你的是,无论直接为常量赋值 、 通过构造函数为常量赋值 还是 使用三目运算符,实际上我们都能通过反射成功修改常量的值。而我在上面说的修改”成功”与否是指:我们在程序运行阶段通过反射肯定能修改常量值,但是实际执行优化后的 .class 文件时,修改的后值真的起到作用了吗?换句话说,就是编译时是否将常量替换为具体的值了?如果替换了,再怎么修改常量的值都不会影响最终的结果了,不是吗?。

其实,你可以直接这么想:反射肯定能修改常量的值,但修改后的值是否有意义

原文链接

练习

ZB-035-04ClassLoader详解

ClassLoader 做什么的?

顾名思义,它是用来 加载 Class 的

它负责将 Class 的字节码形式转换成内存形式的 Class 对象。

字节码的来源

  • 来自于磁盘文件 *.class
  • jar 包里的 *.class
  • 来自远程服务器提供的字节流

字节码的本质就是一个字节数组 []byte,它有特定的复杂的内部格式。

占位

有很多字节码加密技术就是依靠定制 ClassLoader 来实现的。先使用工具对字节码文件进行加密,运行时使用定制的 ClassLoader 先解密文件内容再加载这些解密后的字节码。

每个 Class 对象的内部都有一个 classLoader 字段来标识自己是由哪个 ClassLoader 加载的。ClassLoader 就像一个容器,里面装了很多已经加载的 Class 对象。

1
2
3
4
5
class Class<T> {
...
private final ClassLoader classLoader;
...
}

延迟加载

JVM 运行并不是一次性加载所需要的全部类的,它是按需加载,也就是延迟加载。程序在运行的过程中会逐渐遇到很多不认识的新类,这时候就会调用 ClassLoader 来加载这些类。加载完成后就会将 Class 对象存在 ClassLoader 里面,下次就不需要重新加载了。

比如你在调用某个类的静态方法时,首先这个类肯定是需要被加载的,但是并不会触及这个类的实例字段,那么实例字段的类别 Class 就可以暂时不必去加载,但是它可能会加载静态字段相关的类别,因为静态方法会访问静态字段。而实例字段的类别需要等到你实例化对象的时候才可能会加载。

各司其职

JVM 运行实例中会存在多个 ClassLoader,不同的 ClassLoader 会从不同的地方加载字节码文件

  • 它可以从不同的文件目录加载,
  • 也可以从不同的 jar 文件中加载
  • 也可以从网络上不同的静态文件服务器来下载字节码再加载。

JVM 中内置了三个重要的 ClassLoader,分别是

  • BootstrapClassLoader
  • ExtensionClassLoader
  • AppClassLoader。
BootstrapClassLoader

负责加载 JVM 运行时核心类,这些类位于 $JAVA_HOME/lib/rt.jar 文件中,我们常用内置库 java.xxx.* 都在里面,比如 java.util.、java.io.、java.nio.、java.lang. 等等。这个 ClassLoader 比较特殊,它是由 C 代码实现的,我们将它称之为「根加载器」。

ExtensionClassLoader

负责加载 JVM 扩展类,比如 swing 系列、内置的 js 引擎、xml 解析器 等等,这些库名通常以 javax 开头,它们的 jar 包位于 $JAVA_HOME/lib/ext/*.jar 中,有很多 jar 包。

AppClassLoader

才是直接面向我们用户的加载器,它会加载 Classpath 环境变量里定义的路径中的 jar 包和目录。我们自己编写的代码以及使用的第三方 jar 包通常都是由它来加载的。

URLClassLoader

那些位于网络上静态文件服务器提供的 jar 包和 class文件,jdk 内置了一个 URLClassLoader,用户只需要传递规范的网络路径给构造器,就可以使用 URLClassLoader 来加载远程类库了。

URLClassLoader 不但可以加载远程类库,还可以加载本地路径的类库,取决于构造器中不同的地址形式。

ExtensionClassLoader 和 AppClassLoader 都是 URLClassLoader 的子类,它们都是从本地文件系统里加载类库。

AppClassLoader 可以由 ClassLoader 类提供的静态方法 getSystemClassLoader() 得到,它就是我们所说的「系统类加载器」,我们用户平时编写的类代码通常都是由它加载的。当我们的 main 方法执行的时候,这第一个用户类的加载器就是 AppClassLoader。

ClassLoader 传递性

程序在运行过程中,遇到了一个未知的类,它会选择哪个 ClassLoader 来加载它呢?虚拟机的策略是使用调用者 Class 对象的 ClassLoader 来加载当前未知的类。何为调用者 Class 对象?就是在遇到这个未知的类时,虚拟机肯定正在运行一个方法调用(静态方法或者实例方法),这个方法挂在哪个类上面,那这个类就是调用者 Class 对象。前面我们提到每个 Class 对象里面都有一个 classLoader 属性记录了当前的类是由谁来加载的。

因为 ClassLoader 的传递性,所有延迟加载的类都会由初始调用 main 方法的这个 ClassLoader 全全负责,它就是 AppClassLoader。

双亲委派

前面我们提到 AppClassLoader 只负责加载 Classpath 下面的类库,如果遇到没有加载的系统类库怎么办,AppClassLoader 必须将系统类库的加载工作交给 BootstrapClassLoader 和 ExtensionClassLoader 来做,这就是我们常说的「双亲委派」。

占位

加载过程

AppClassLoader 在加载一个未知的类名时,它并不是立即去搜寻 Classpath,它会首先将这个类名称交给 ExtensionClassLoader 来加载,如果 ExtensionClassLoader 可以加载,那么 AppClassLoader 就不用麻烦了。否则它就会搜索 Classpath 。
而 ExtensionClassLoader 在加载一个未知的类名时,它也并不是立即搜寻 ext 路径,它会首先将类名称交给 BootstrapClassLoader 来加载,如果 BootstrapClassLoader 可以加载,那么 ExtensionClassLoader 也就不用麻烦了。否则它就会搜索 ext 路径下的 jar 包。

这三个 ClassLoader 之间形成了级联的父子关系,每个 ClassLoader 都很懒,尽量把工作交给父亲做,父亲干不了了自己才会干。每个 ClassLoader 对象内部都会有一个 parent 属性指向它的父加载器。

1
2
3
4
5
class ClassLoader {
...
private final ClassLoader parent;
...
}

值得注意的是图中的 ExtensionClassLoader 的 parent 指针画了虚线,这是因为它的 parent 的值是 null,当 parent 字段是 null 时就表示它的父加载器是「根加载器」。如果某个 Class 对象的 classLoader 属性值是 null,那么就表示这个类也是「根加载器」加载的。注意这里的 parent 不是 super 不是父类,只是 ClassLoader 内部的字段。

Class.forName

当我们在使用 jdbc 驱动时,经常会使用 Class.forName 方法来动态加载驱动类。

1
Class.forName("com.mysql.cj.jdbc.Driver");

其原理是 mysql 驱动的 Driver 类里有一个静态代码块,它会在 Driver 类被加载的时候执行。这个静态代码块会将 mysql 驱动实例注册到全局的 jdbc 驱动管理器里。

1
2
3
4
5
6
7
8
9
10
class Driver {
static {
  try {
      java.sql.DriverManager.registerDriver(new Driver());
  } catch (SQLException E) {
      throw new RuntimeException("Can't register driver!");
  }
}
...
}

forName 方法同样也是使用调用者 Class 对象的 ClassLoader 来加载目标类。不过 forName 还提供了多参数版本,可以指定使用哪个 ClassLoader 来加载

1
Class<?> forName(String name, boolean initialize, ClassLoader cl)

通过这种形式的 forName 方法可以突破内置加载器的限制,通过使用自定类加载器允许我们自由加载其它任意来源的类库。根据 ClassLoader 的传递性,目标类库传递引用到的其它类库也将会使用自定义加载器加载。

自定义加载器

ClassLoader 里面有三个重要的方法 loadClass()、findClass() 和 defineClass()。

loadClass() 方法是加载目标类的入口,它首先会查找当前 ClassLoader 以及它的双亲里面是否已经加载了目标类,如果没有找到就会让双亲尝试加载,如果双亲都加载不了,就会调用 findClass() 让自定义加载器自己来加载目标类。ClassLoader 的 findClass() 方法是需要子类来覆盖的,不同的加载器将使用不同的逻辑来获取目标类的字节码。拿到这个字节码之后再调用 defineClass() 方法将字节码转换成 Class 对象。下面我使用伪代码表示一下基本过程

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
class ClassLoader {

// 加载入口,定义了双亲委派规则
Class loadClass(String name) {
// 是否已经加载了
Class t = this.findFromLoaded(name);
if(t == null) {
// 交给双亲
t = this.parent.loadClass(name)
}
if(t == null) {
// 双亲都不行,只能靠自己了
t = this.findClass(name);
}
return t;
}

// 交给子类自己去实现
Class findClass(String name) {
throw ClassNotFoundException();
}

// 组装Class对象
Class defineClass(byte[] code, String name) {
return buildClassFromCode(code, name);
}
}

class CustomClassLoader extends ClassLoader {

Class findClass(String name) {
// 寻找字节码
byte[] code = findCodeFromSomewhere(name);
// 组装Class对象
return this.defineClass(code, name);
}
}

自定义类加载器不易破坏双亲委派规则,不要轻易覆盖 loadClass 方法。否则可能会导致自定义加载器无法加载内置的核心类库。在使用自定义加载器时,要明确好它的父加载器是谁,将父加载器通过子类的构造器传入。如果父类加载器是 null,那就表示父加载器是「根加载器」。

1
2
// ClassLoader 构造器
protected ClassLoader(String name, ClassLoader parent);

双亲委派规则可能会变成三亲委派,四亲委派,取决于你使用的父加载器是谁,它会一直递归委派到根加载器.

Class.forName vs ClassLoader.loadClass

这两个方法都可以用来加载目标类,它们之间有一个小小的区别,那就是 Class.forName() 方法可以获取原生类型的 Class,而 ClassLoader.loadClass() 则会报错。

1
2
3
4
5
6
7
8
9
10
11
Class<?> x = Class.forName("[I");
System.out.println(x);

x = ClassLoader.getSystemClassLoader().loadClass("[I");
System.out.println(x);

---------------------
class [I

Exception in thread "main" java.lang.ClassNotFoundException: [I
...

钻石依赖

项目管理上有一个著名的概念叫着「钻石依赖」,是指软件依赖导致同一个软件包的两个版本需要共存而不能冲突。

我们平时使用的 maven 是这样解决钻石依赖的,它会从多个冲突的版本中选择一个来使用,如果不同的版本之间兼容性很糟糕,那么程序将无法正常编译运行。Maven 这种形式叫「扁平化」依赖管理。

使用 ClassLoader 可以解决钻石依赖问题。不同版本的软件包使用不同的 ClassLoader 来加载, 位于不同 ClassLoader 中名称一样的类实际上是不同的类。 下面让我们使用 URLClassLoader 来尝试一个简单的例子,它默认的父加载器是 AppClassLoader

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
$ cat ~/source/jcl/v1/Dep.java
public class Dep {
public void print() {
System.out.println("v1");
}
}

$ cat ~/source/jcl/v2/Dep.java
public class Dep {
public void print() {
System.out.println("v1");
}
}

$ cat ~/source/jcl/Test.java
public class Test {
public static void main(String[] args) throws Exception {
String v1dir = "file:///Users/qianwp/source/jcl/v1/";
String v2dir = "file:///Users/qianwp/source/jcl/v2/";
URLClassLoader v1 = new URLClassLoader(new URL[]{new URL(v1dir)});
URLClassLoader v2 = new URLClassLoader(new URL[]{new URL(v2dir)});

Class<?> depv1Class = v1.loadClass("Dep");
Object depv1 = depv1Class.getConstructor().newInstance();
depv1Class.getMethod("print").invoke(depv1);

Class<?> depv2Class = v2.loadClass("Dep");
Object depv2 = depv2Class.getConstructor().newInstance();
depv2Class.getMethod("print").invoke(depv2);

System.out.println(depv1Class.equals(depv2Class));
}
}

在运行之前,我们需要对依赖的类库进行编译

1
2
3
4
5
6
7
8
9
10
$ cd ~/source/jcl/v1
$ javac Dep.java
$ cd ~/source/jcl/v2
$ javac Dep.java
$ cd ~/source/jcl
$ javac Test.java
$ java Test
v1
v2
false

在这个例子中如果两个 URLClassLoader 指向的路径是一样的,下面这个表达式还是 false,因为即使是同样的字节码用不同的 ClassLoader 加载出来的类都不能算同一个类

1
depv1Class.equals(depv2Class)

我们还可以让两个不同版本的 Dep 类实现同一个接口,这样可以避免使用反射的方式来调用 Dep 类里面的方法。

1
2
3
Class<?> depv1Class = v1.loadClass("Dep");
IPrint depv1 = (IPrint)depv1Class.getConstructor().newInstance();
depv1.print()

ClassLoader 固然可以解决依赖冲突问题,不过它也限制了不同软件包的操作界面必须使用反射或接口的方式进行动态调用。Maven 没有这种限制,它依赖于虚拟机的默认懒惰加载策略,运行过程中如果没有显示使用定制的 ClassLoader,那么从头到尾都是在使用 AppClassLoader,而不同版本的同名类必须使用不同的 ClassLoader 加载,所以 Maven 不能完美解决钻石依赖。
如果你想知道有没有开源的包管理工具可以解决钻石依赖的,我推荐你了解一下 sofa-ark,它是蚂蚁金服开源的轻量级类隔离框架。

分工与合作

这里我们重新理解一下 ClassLoader 的意义,它相当于类的命名空间,起到了类隔离的作用。位于同一个 ClassLoader 里面的类名是唯一的,不同的 ClassLoader 可以持有同名的类。ClassLoader 是类名称的容器,是类的沙箱。

不同的 ClassLoader 之间也会有合作,它们之间的合作是通过 parent 属性和双亲委派机制来完成的。parent 具有更高的加载优先级。除此之外,parent 还表达了一种共享关系,当多个子 ClassLoader 共享同一个 parent 时,那么这个 parent 里面包含的类可以认为是所有子 ClassLoader 共享的。这也是为什么 BootstrapClassLoader 被所有的类加载器视为祖先加载器,JVM 核心类库自然应该被共享。

Thread.contextClassLoader

如果你稍微阅读过 Thread 的源代码,你会在它的实例字段中发现有一个字段非常特别

1
2
3
4
5
6
7
8
9
10
11
12
13
class Thread {
...
private ClassLoader contextClassLoader;

public ClassLoader getContextClassLoader() {
return contextClassLoader;
}

public void setContextClassLoader(ClassLoader cl) {
this.contextClassLoader = cl;
}
...
}

contextClassLoader「线程上下文类加载器」,这究竟是什么东西?

首先 contextClassLoader 是那种需要显示使用的类加载器,如果你没有显示使用它,也就永远不会在任何地方用到它。你可以使用下面这种方式来显示使用它

1
Thread.currentThread().getContextClassLoader().loadClass(name);

这意味着如果你使用 forName(string name) 方法加载目标类,它不会自动使用 contextClassLoader。那些因为代码上的依赖关系而懒惰加载的类也不会自动使用 contextClassLoader来加载。

其次线程的 contextClassLoader 默认是从父线程那里继承过来的,所谓父线程就是创建了当前线程的线程。程序启动时的 main 线程的 contextClassLoader 就是 AppClassLoader。这意味着如果没有人工去设置,那么所有的线程的 contextClassLoader 都是 AppClassLoader。

那这个 contextClassLoader 究竟是做什么用的?我们要使用前面提到了类加载器分工与合作的原理来解释它的用途。

它可以做到跨线程共享类,只要它们共享同一个 contextClassLoader。父子线程之间会自动传递 contextClassLoader,所以共享起来将是自动化的。

如果不同的线程使用不同的 contextClassLoader,那么不同的线程使用的类就可以隔离开来。

如果我们对业务进行划分,不同的业务使用不同的线程池,线程池内部共享同一个 contextClassLoader,线程池之间使用不同的 contextClassLoader,就可以很好的起到隔离保护的作用,避免类版本冲突。

如果我们不去定制 contextClassLoader,那么所有的线程将会默认使用 AppClassLoader,所有的类都将会是共享的。

线程的 contextClassLoader 使用场合比较罕见,如果上面的逻辑晦涩难懂也不必过于计较。

JDK9 增加了模块功能之后对类加载器的结构设计做了一定程度的修改,不过类加载器的原理还是类似的,作为类的容器,它起到类隔离的作用,同时还需要依靠双亲委派机制来建立不同的类加载器之间的合作关系。

原文链接

练习

-q实现一个自定义的ClassLoader

-a

ZB-035-03反射

反射

  • 根据参数创建一个对象?
  • 根据参数调用一个方法?
  • 根据参数动态获取一个属性?
1
2
3
4
5
6
7
public class WhiteCat extends Cat {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
String className = "java.lang.String";
Class c = Class.forName(className);
Object object = c.getConstructor().newInstance();
}
}

JVM的堆里有一个地方存了,Cat的Class对象(Cat的说明书)

你可以通过这样一个Class.forName(String className) 传递一个类的名字

注意这个类名是 全限定类名
注意这个类名是 全限定类名
注意这个类名是 全限定类名

此时虚拟机根据你的要求加载这个类进来,然后你就能进行一些诡异的操作了

1
2
3
4
// 比如这个
Object object = c.getConstructor().newInstance();

// 不建议用这个 c.newInstance(); jdk9里废弃了 考虑兼容性 不建议使用

这样创建意味着什么?

1
2
3
4
5
6
7
8
9
10
String className = "java.lang.String";
Class c = Class.forName(className);
Object object = c.getConstructor().newInstance();


// 意味着你不再需要这样的调用,
// 而是使用 一个传递的名字,意味着你拥有了无与伦比的灵活性
new String();
new Cat();
new ArrayList();

调用方法,访问成员属性

1
2
3
4
5
6
7
public class WhiteCat extends Cat {
public static void main(String[] args) throws NoSuchMethodException, NoSuchFieldException, InvocationTargetException, IllegalAccessException {
Cat cat = new Cat();
cat.getClass().getMethod(args[0]).invoke(cat);
cat.getClass().getField(args[1]).get(cat);
}
}

反射扩展

  • spring-framework里的 ReflectionUtils
    • invokeMethod
  • Method
  • Class
  • Field

练习

ZB-035-02类加载和ClassLoader

Class对象的生命周期

不止一份说明书

你的程序扩展了

此时 你有了如下多的类型

  • Cat
  • Dog
  • Pig
  • 或者内置的 ArrayList

它们都是从 对应的说明书创建的?

问题来了?这个 说明书,这个Class对象 那里来的

  • 绝不可能是凭空出现,一定是从什么地方拿到的

Class对象的生命周期

  • 它在第一次被使用的时候加载

Class对象就是一个 类型(如Cat) 的说明书

上篇文章的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
Animal
-|Cat
--|WhiteCat

在内存中初始的时候,
一个 WhiteCat 它的一部分区域是 Cat,在一部分是 Animal

意思就是
你要创建一个 WhiteCat 就必须 先创建 Cat,创建 Cat 就必须 先创建Animal

换句话说,
你想要一个 WhiteCat,你首先要用到 Cat,然后创建 Cat 又必须用到 Animal
这个过程是一层一层递归的

这就是:类加载的时机 在它第一次被使用的时候

HelloWorld 运行过程

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
public class Main {
public static void main(String[] args) {
System.out.println("HelloWorld");
new WhiteCat();
// new WhiteCat() 的时候必须要有 Cat说明书
// new Cat()的时候 必须有 Animal 说明书
// new Animal() 的时候 必须有 Object 说明书
// java.lang.Object 是排在第一个被加载的 它是jre自带的
// 然后造出来 obj -> animal -> cat -> whiteCat
}
}

// IDEA里 右键运行

你以为的
运行------》 HelloWorld

实际上

运行---》
第一步 编译 从 Main.java--> Main.class
第二步 帮你拼了一个 java命令 运行你的 Main.class
打印出 HelloWorld

// 复制这行拼接的命令
在终端里执行,你发现跟你点 运行 没区别

// 我们可以给这个命令行加参数

让他打印 类加载 顺序

-verbose:class

// 在这个后面
/Library/Java/JavaVirtualMachines/jdk1.8.0_191.jdk/Contents/Home/bin/java -verbose:class ...

但是非常啰嗦

你可以在命令的最后 加上 管道操作

/Library/Java/JavaVirtualMachines/jdk1.8.0_191.jdk/Contents/Home/bin/java -verbose:class
...

| grep 过滤词(我们自己类的包名如 com.xxx)


你会看到 形如
[Loaded com.io.demo.Animal from file:/Users/xxx/Desktop/xxx/target/classes/]
[Loaded com.io.demo.Cat from file:/Users/xxx/Desktop/xxx/target/classes/]
[Loaded com.io.demo.WhiteCat from file:/Users/xxx/Desktop/xxx/target/classes/]

意思就是这个类 从那里来

回到刚才的问题 我想创建一个 Cat/Dog/ArrayList,说明书放在一个地方存着。说明书从那里来的?

上面的过程解释了,说明书什么时候被加载。

那它是那里来的呢?

答案是你自己写的类 从你编写的 xxx.java 编译后的 xxx.class里来(target目录里)

你看到的是瞬间加载你的类,实际做了很多事

1
2
3
4
5
6
7
8
9
10
11
12
13
加载之前是 xxx.class 文件

加载 Loading (被jvm读取)===>
进行一系列操作

(链接Linking 包含 验证、准备、解析)
验证==》防止恶意代码搞坏jvm
准备==》
解析===》
初始化
(最终)
把一个 class文件 变到 内存里
使得根据对应的说明书 可以创建对应的对象

第三个问题?说明书被谁加载的

答案就是 Classloader

Class和 Classloader

Classloader 负责从外部系统中加载一个类

  • 这个类对应的Java文件并不需要存在 你可以没有Cat.java,你可以通过Cat.class
  • 这个类(字节码))并不一定需要存在你也可以没有Cat.class,你可以从网上下载,文件的本质是字节流,可以动态生成(内存里凭空捏造一个,它可以不对应一个 .class文件)
  • 这是java世界丰富多彩的基石

    1
    2
    3
    4
    5
    6
    比如我想要一个 五条腿的猫
    动态创建一个 Cat5
    Cat5 extends Cat
    这个Cat5的说明书 你可以直接在内存里捏造出来,不需要和外界打交道

    这个操作就是 动态字节码增强
  • Classloader的双亲委派加载模型

    • 首先 Classloader 负责从外部系统中加载一个类
    • 外部是不安全的,总会有恶意的代码,比如 他想搞坏你的String
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      // src/main下建立如下代码
      package java.lang;

      public class String {
      // 我是一个坏人
      // 我在自己定义的 String 里 做坏事

      public String() {
      throw new IllegalStateException();
      }
      }

      //
      package com.xxx.demo;

      public class Main {
      public static void main(String[] args) {
      new String();
      // 此时 点进去发现 会优先选择官方的 String
      }
      }

      // 答案就是 双亲委派加载模型

双亲委派加载模型

  • 每次加载类的时候 都会调用 ClassLoader.loadClass() 你可以给这行 加上断点 debug 运行一下
    • 当前的对象叫做 Launcher$AppClassLoader 启动类加载器
    • AppClassLoader它负责从外部系统 把说明书加载到jvm内部的人
    • AppClassLoader它的父亲是 Launcher$ExtClassLoader 扩展类加载器
      • ExtClassLoader 的父亲 是 null
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
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
// 它有父亲的话,首先从父亲加载
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}

// 父亲加载失败,尝试自己加载
if (c == null) {

// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name);

// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}

这个模型的好处?

比如刚刚的new String();这样的基础的类,出于安全性的考虑都是由最顶端的启动类加载器加载的,这样就保证了安全

所以我们自造的恶意的 类无法被加载。

  • 每次加载一个 类的时候 都先问他的父亲 加载了吗
  • 如果父亲加载了,就不会让你在继续加载,这就保证了 无法恶意的 覆盖修改 String 类

Java语言规范(JLS)和Java虚拟机规范(JVMS)

  • java 和 jvm实际是分离的,它们的联系就是字节码,而字节码可以不由java生成
  • 这种分离提供了JVM上运行其他语言的可能

查看字节码 命令

1
javap xxx