原创作品,可以转载,但是请标注出处地址:http://www.cnblogs.com/V1haoge/p/6715063.html

1、回顾

  之前的两篇分别解析了类型别名注册器和类型处理器注册器,此二者皆是解析XML映射文件中参数类型与返回结果类型的基础,别名注册器用于通过别名找到对应的类类型,类型处理器注册器则用于通过类类型来找到对应的类型处理器与数据库类型,以此来完成进出数据库数据与java之间类型的转换。

  我们在类型处理器注册器一篇中已经简单介绍了类型处理器,那就是用于java类型与数据库类型之间进行映射处理的工具类,这一篇中要详细解析一下MyBatis中的类型处理器。

2、类型处理器

2.1 类架构

  

  从上面的图中可以看出MyBatis中整个类型处理器实现架构,TypeHandler接口定义了类型处理器,而TypeReference抽象类则定义了一个类型引用,用于引用一个泛型类型(此处很抽象,不好理解,详见后续解析),BaseTypeHandler则是类型处理器的基础,是所有类型处理器的公共模块,几乎所有的类型处理器都是通过直接继承BaseTypeHandler来实现的,这是很明显使用的是模板模式。

2.2 类型处理器接口:TypeHandler

  TypeHandler是用于定义类型处理器的接口,内部很简单:

 package org.apache.ibatis.type;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
/**
* 类型处理器
*
*/
public interface TypeHandler<T> { //设置参数
void setParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException; //取得结果,供普通select用
T getResult(ResultSet rs, String columnName) throws SQLException; //取得结果,供普通select用
T getResult(ResultSet rs, int columnIndex) throws SQLException; //取得结果,供SP用
T getResult(CallableStatement cs, int columnIndex) throws SQLException; }

  通过上述源码可以看到这个接口中定义了类型处理器基本的四个方法,其中分为两大类,第一类是设置参数的方法setParameter(),这个方法是用于设置数据库操作的参数,例如查询参数、删除参数、更新参数等;另一类是用于取得结果的方法,这一类方法又细分为两大种,第一种是从结果集中获取结果,按照获取的方式分为两种:一种是通过列名(columnName)来获取,另一种是通过列下标(columnIndex)来获取,这两种获取方式正对应我们直接使用JDBC进行数据库查询结果中获取数据的两种方式,第二种是针对存储过程而设,通过列下标的方式来获取存储过程输出结果中的数据。

  总的来说类型处理器就是两方面的作用,一方面将Java类型的参数(T prarameter)设置到数据库操作脚本中(匹配数据库类型jdbcType),另一种是获取操作结果到Java类型(T)中。

2.3 类型引用:TypeReference

  这个类型引用的作用是用于获取原生类型,Java中的原生类型又称为基本类型,即byte、short、int、long、float、double、boolean、char八大基本数据类型。

  这个类有必要重点讲解一下,同时也是为了加强一下Java中类型的概念,来看源码:

 package org.apache.ibatis.type;

 import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type; /**
* References a generic type.
*
* @param <T> the referenced type
* @author Simone Tripodi
* @since 3.1.0
* 3.1新加的类型引用,为了引用一个泛型类型
*/
public abstract class TypeReference<T> { //引用的原生类型
private final Type rawType; protected TypeReference() {
rawType = getSuperclassTypeParameter(getClass());
} Type getSuperclassTypeParameter(Class<?> clazz) {
//得到泛型T的实际类型
Type genericSuperclass = clazz.getGenericSuperclass();
if (genericSuperclass instanceof Class) {
// try to climb up the hierarchy until meet something useful
if (TypeReference.class != genericSuperclass) {
return getSuperclassTypeParameter(clazz.getSuperclass());
}
throw new TypeException("'" + getClass() + "' extends TypeReference but misses the type parameter. "
+ "Remove the extension or add a type parameter to it.");
}
//获取泛型<T>中的T类型
Type rawType = ((ParameterizedType) genericSuperclass).getActualTypeArguments()[0];
// TODO remove this when Reflector is fixed to return Types
if (rawType instanceof ParameterizedType) {
rawType = ((ParameterizedType) rawType).getRawType();
}
return rawType;
} public final Type getRawType() {
return rawType;
} @Override
public String toString() {
return rawType.toString();
} }

  这个抽象类也是被BaseTypeHandler所继承的,也就意味着几乎所有的内置类型处理器都继承了这个类,那么这个类型引用的目的到底是什么呢?

  这个问题稍后再说,我们先解析下源码:

  这个类在其无参构造器中通过调用getSuperclassTypeParameter()方法为其内部定义的final型字段rawType赋值,其参数是getClass()方法的结果,这是Object类中定义的方法,这个方法返回的是当前类(实例)的类类型。

  重点在于getSuperclassTypeParameter()方法中:

    第一步:通过给定参数clazz的getGenericSuperclass()方法来获取该类类型的上一级类型(直接超类,父类,即参数类类型继承的类的类型)并带有参数类型,即带泛型。如果要获取不带泛型的父类可使用getSuperclass()方法。

    第二步:判断第一步获取的类型是否是Class类的实例

  Class类的实例有哪些呢?

  其实每一个类都是Class类的实例,Class类是对Java中类的抽象,它本身也是一个类,但它是处于普通类上一层次的类,是类的顶层抽象。从JDK文档中可获知“Instances of the class represent classes and interfaces in a running Java application.”(意为:Class的实例表示的是在一个运行的应用中的所有类和接口)

,那么我们就明白了,Class类的实例就是接口与类。那么Java中有哪些不是Class类的实例呢?泛型类,不错,如果一个类是泛型类,那么他就不再是Class类的实例,为什么呢?

  泛型类是Java中一种独特的存在,它一般用于传递类(更准确的说是传递类型),类似于一般方法中传递对象的概念,它不是简单的类,而是一种带有抽象概念性质的一种类,它会通过所传递的类(参数化类)来指定当前类所代表的是基于基本类型中的哪一类类型。(通过两种类型来确定具体的类型(最后这个类型表示的是泛型类型整体表达的类型))

    第二步:如果第一步获取的类型是带泛型的类型,那么判断不成立,则会直接执行第35行代码,将该类型强转为参数化类型,使用其getActualTypeArguments()方法来获取其参数类型(泛型类型),因为该方法获取的泛型类型可能不是一个,所以返回的是一个数组,但是我们这里只会获取到一个,所以取第一个即可。

    但是如果第一步获取的类型不带泛型,那么就会进入条件内部执行,再次判断,获取的类型是否是TypeReference类型,如果不是该类型,则有可能是多重继承导致目标类型并不是直接继承自TypeReference,那么我们通过getSuperclass()方法获取其父类,以这个类来进行递归;但如果获取到的是TypeReference类型,只是没有添加泛型,则抛出类型异常,提示丢失泛型。

    第三步:如果第二步判断不通过,则会执行地35行代码,来获取参数类型,然后对获取的参数类型进行判断,如果该类型还是参数化类型(仍然带有泛型,即泛型嵌套的模式),那么就需要再次执行getActualTypeArguments()方法来获取其泛型类型(参数类型),最后将该类型返回(赋值给字段)

  为什么只会获取两次呢?因为,通过之前的类架构我们已经明白,具体的类型处理器最多只会存在两层继承。

  最后说一下,这个类型引用的目的,它就是为了持有这个具体的类型处理器所处理的Java类型的原生类型。我们可以看到在该类中还有两个方法getRawType()和toString()方法,这两个方法都是public修饰的,是对外公开的方法,那么也就意味着这个原生类型是为了被外部调用而设。

  通过检索发现,getRawType()方法重点被调用的地方在TypeHandlerRegistry(类型处理器注册器)中,在没有指定JavaType而只有TypeHandler的情况下,调用该TypeHandler的getRawType()方法来获取其原生类型(即参数类型)来作为其JavaType来进行类型处理器的注册。

2.4 基础类型处理器:BaseTypeHandler

  BaseTypeHandler继承了TypeReference抽象类,实现了TypeHandler接口,它本身仍然是抽象类,在它内部简单的实现了TypeHandler接口中定义的四个方法中的部分功能,所谓部分功能是指只实现了所有类型处理器公共部分,具体的不同处理部分则还是交由具体的类型处理器来自己实现,所有它内部再次定义了四个抽象类,用来指导具体类型处理器的实现。

  BaseTypeHandler中主要对设置参数与获取返回结果时数据位null的情况进行了处理,具体的参数设置方法与结果获取方法都是由具体的类型处理器来实现的。

     //非NULL情况,怎么设参数还得交给不同的子类完成
public abstract void setNonNullParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException; //以下3个方法是取得可能为null的结果,具体交给子类完成
public abstract T getNullableResult(ResultSet rs, String columnName) throws SQLException; public abstract T getNullableResult(ResultSet rs, int columnIndex) throws SQLException; public abstract T getNullableResult(CallableStatement cs, int columnIndex) throws SQLException;

  上面的四个方法就是BaseTypeHandler中定义的抽象方法。MyBatis内置的类型处理器几乎都是通过继承实现上面的四个方法来完成最终定义的。

2.5 类型处理器:StringTypeHandler

  我们看个简单的例子来理解一下这个过程。下面是字符串类型处理器:StringTypeHandler的源码

 package org.apache.ibatis.type;

 import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
/**
* String类型处理器
* 调用PreparedStatement.setString, ResultSet.getString, CallableStatement.getString
*/
public class StringTypeHandler extends BaseTypeHandler<String> { @Override
public void setNonNullParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType)
throws SQLException {
ps.setString(i, parameter);
} @Override
public String getNullableResult(ResultSet rs, String columnName)
throws SQLException {
return rs.getString(columnName);
} @Override
public String getNullableResult(ResultSet rs, int columnIndex)
throws SQLException {
return rs.getString(columnIndex);
} @Override
public String getNullableResult(CallableStatement cs, int columnIndex)
throws SQLException {
return cs.getString(columnIndex);
}
}

  上面的源码完美的诠释了之前的解析,具体的类型处理器中只需要实现这四个方法即可,前提是其继承了BaseTypeHandler抽象类。

  其中设置参数的方法中具体的实现调用了PreparedStatement的setString()方法,这个是我们很熟悉的方法。同样的,在获取结果的方法中也是通过调用ResultSet的getString()方法,和CallableStatement的getString()方法来完成具体的功能。这已经是MyBatis中最为底层的逻辑了,因为它直接调用了JDK API来实现功能。

2.6  未知类型处理器:UnknownTypeHandler

  这个是MyBatis中定义的一个较为特殊的类型处理器,虽然其内部实现和普通的类型处理器如出一辙,但是它拥有一些特殊的地方,所以单独拿出来说一说。

  通过类型处理器注册器中的注册信息可以看出这种类型处理器所对应的JavaType是Object类型,对应的JdbcType是OTHER类型,这个OTHER是什么类型?我们可以这么理解,市面上数据库种类繁多,而且各有特点,这些数据库产品即满足SQL规范,同时也有各自的扩展和强化,每个数据库内部都有一些自定义的只在其内部起作用的数据类型,而这些类型反映到Java中之后是Object类型时,这里就将其统一定义为OTHER类型。

   private static final ObjectTypeHandler OBJECT_TYPE_HANDLER = new ObjectTypeHandler();

   private TypeHandlerRegistry typeHandlerRegistry;

   public UnknownTypeHandler(TypeHandlerRegistry typeHandlerRegistry) {
this.typeHandlerRegistry = typeHandlerRegistry;
} @Override
public void setNonNullParameter(PreparedStatement ps, int i, Object parameter, JdbcType jdbcType)
throws SQLException {
TypeHandler handler = resolveTypeHandler(parameter, jdbcType);
handler.setParameter(ps, i, parameter, jdbcType);
} @Override
public Object getNullableResult(ResultSet rs, String columnName)
throws SQLException {
TypeHandler<?> handler = resolveTypeHandler(rs, columnName);
return handler.getResult(rs, columnName);
} @Override
public Object getNullableResult(ResultSet rs, int columnIndex)
throws SQLException {
TypeHandler<?> handler = resolveTypeHandler(rs.getMetaData(), columnIndex);
if (handler == null || handler instanceof UnknownTypeHandler) {
handler = OBJECT_TYPE_HANDLER;
}
return handler.getResult(rs, columnIndex);
} @Override
public Object getNullableResult(CallableStatement cs, int columnIndex)
throws SQLException {
return cs.getObject(columnIndex);
}

  源码分析:在UnknownTypeHandler中的四个方法中,除针对存储过程结果取数据的情况之外,其余三个方法的实现均类似,都是先通过不同的resolveTypeHandler()方法来获取具体的TypeHandler,然后调用具体TypeHandler的对应方法来完成功能。那么UnknownTypeHandler中的重点就集中在这三个resolveTypeHandler()方法中了。

   private TypeHandler<? extends Object> resolveTypeHandler(Object parameter, JdbcType jdbcType) {
TypeHandler<? extends Object> handler;
if (parameter == null) {
handler = OBJECT_TYPE_HANDLER;
} else {
handler = typeHandlerRegistry.getTypeHandler(parameter.getClass(), jdbcType);
// check if handler is null (issue #270)
if (handler == null || handler instanceof UnknownTypeHandler) {
handler = OBJECT_TYPE_HANDLER;
}
}
return handler;
} private TypeHandler<?> resolveTypeHandler(ResultSet rs, String column) {
try {
Map<String,Integer> columnIndexLookup;
columnIndexLookup = new HashMap<String,Integer>();
ResultSetMetaData rsmd = rs.getMetaData();
int count = rsmd.getColumnCount();
for (int i=1; i <= count; i++) {
String name = rsmd.getColumnName(i);
columnIndexLookup.put(name,i);
}
Integer columnIndex = columnIndexLookup.get(column);
TypeHandler<?> handler = null;
if (columnIndex != null) {
handler = resolveTypeHandler(rsmd, columnIndex);
}
if (handler == null || handler instanceof UnknownTypeHandler) {
handler = OBJECT_TYPE_HANDLER;
}
return handler;
} catch (SQLException e) {
throw new TypeException("Error determining JDBC type for column " + column + ". Cause: " + e, e);
}
} private TypeHandler<?> resolveTypeHandler(ResultSetMetaData rsmd, Integer columnIndex) throws SQLException {
TypeHandler<?> handler = null;
JdbcType jdbcType = safeGetJdbcTypeForColumn(rsmd, columnIndex);
Class<?> javaType = safeGetClassForColumn(rsmd, columnIndex);
if (javaType != null && jdbcType != null) {
handler = typeHandlerRegistry.getTypeHandler(javaType, jdbcType);
} else if (javaType != null) {
handler = typeHandlerRegistry.getTypeHandler(javaType);
} else if (jdbcType != null) {
handler = typeHandlerRegistry.getTypeHandler(jdbcType);
}
return handler;
} private JdbcType safeGetJdbcTypeForColumn(ResultSetMetaData rsmd, Integer columnIndex) {
try {
return JdbcType.forCode(rsmd.getColumnType(columnIndex));
} catch (Exception e) {
return null;
}
} private Class<?> safeGetClassForColumn(ResultSetMetaData rsmd, Integer columnIndex) {
try {
return Resources.classForName(rsmd.getColumnClassName(columnIndex));
} catch (Exception e) {
return null;
}
}

  第一个resolveTypeHandler方法是由设置参数的方法调用的,目的在于获取真正的TypeHandler来进行类型处理。如果其参数parameter为null,那么直接将TypeHandler设定为ObjectTypeHandler,如果parameter不为null,则直接从类型处理器注册器中获取对应JavaType与JdbcType的类型处理器,这里存在一个#270BUG,针对无法再类型处理器注册器中获取TypeHandler获取获取到的是UnknownTypeHandler的情况进行再次处理:赋值ObjectTypeHandler。

  第二个resolveTypeHandler方法是被通过列名来获取结果数据的方法所调用的,目的同上。首先通过结果集原数据将结果集中的数据循环存放到一个HashMap集合中(以列名为键,列下标为值),然后从中获取给定列名的下标值,如果集合中存在该列名(即能获取到列下标),则调用第三个resolveTypeHandler()方法通过列下标方式来获取具体TypeHandler。当然如果不存在这个列名(亦即获取不到列下标),则直接赋值ObjectTypeHandler。

  第三个resolveTypeHandler方法是被通过列下标来获取结果数据的方法所调用的,同时也被第二个resolveTypeHandler方法所调用。分别通过safeGetJdbcTypeForColumn()方法和safeGetClassForColumn()方法来获取列下标所对应数据的JdbcType与JavaType,然后针对获取到的JdbcType和JavaType来从类型处理器注册器中获取具体的类型处理器。这里分三种情况来获取:jdbcType与JavaType均不为null的情况、只有JavaType不为null的情况和只有JdbcType不为null的情况,三者情况分别调用三种getTypeHandler()方法来完成获取功能。

  总结:由此可见UnknownTypeHandler是一种中间类型处理器,或者叫代理类型处理器,因为它本身并不会真正实现处理功能,它只是通过获取对应的类型处理器来调用其处理功能来完成功能。

3、自定义类型处理器

  有关自定义类型处理器,我们只做简单介绍,其实它也很是简单,我们只要继承BaseTypeHandler<T>抽象类即可,实现其中的四个方法。我们这里举个简单的例子,假如说MyBatis内置的StringTypeHandler无法满足我们的需求,我们可以对其进行扩展自定义,我们自定义一个新的字符串类型处理器:MyStringTypeHandler,代码如下:

 package org.apache.ibatis.type;

 import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException; public class MyStringTypeHandler extends BaseTypeHandler<String> { @Override
public void setNonNullParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType)
throws SQLException {
System.out.println("新的逻辑");
ps.setString(i, parameter);
System.out.println("新的逻辑");
} @Override
public String getNullableResult(ResultSet rs, String columnName) throws SQLException {
System.out.println("新的逻辑");
return rs.getString(columnName);
} @Override
public String getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
System.out.println("新的逻辑");
return rs.getString(columnIndex);
} @Override
public String getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
System.out.println("新的逻辑");
return cs.getString(columnIndex);
} }

  定义好类型处理器之后,然后我们需要的就是将自定义的类型处理器注册到TypeHandlerRegistry中,方法也简单。

   <typeHandlers>
<typeHandler handler="org.apache.ibatis.type.MyStringTypeHandler"/>
</typeHandlers>

  当然我们也可以指定JavaType与jdbcType,获取直接使用package方式进行设置,但是如果你只是自定义了很少的类型处理器,没有必要采用package方式设置,因为这种方式会扫描整个包下的类,无形中造成了时延。

  然后这个新的类型处理器就会添加到TypeHandlerRegistry中了,它会在背后默默实现功能。

4、总结

  至此我们将Type模块解析完毕,说的很是粗糙,但这是自己学习提高的过程,特此记录,期待下一篇。

MyBatis源码解析(十)——Type类型模块之类型处理器TypeHandler的更多相关文章

  1. 【MyBatis源码解析】MyBatis一二级缓存

    MyBatis缓存 我们知道,频繁的数据库操作是非常耗费性能的(主要是因为对于DB而言,数据是持久化在磁盘中的,因此查询操作需要通过IO,IO操作速度相比内存操作速度慢了好几个量级),尤其是对于一些相 ...

  2. mybatis源码-解析配置文件(四-1)之配置文件Mapper解析(cache)

    目录 1. 简介 2. 解析 3 StrictMap 3.1 区别HashMap:键必须为String 3.2 区别HashMap:多了成员变量 name 3.3 区别HashMap:key 的处理多 ...

  3. mybatis源码-解析配置文件(三)之配置文件Configuration解析

    目录 1. 简介 1.1 系列内容 1.2 适合对象 1.3 本文内容 2. 配置文件 2.1 mysql.properties 2.2 mybatis-config.xml 3. Configura ...

  4. Mybatis源码解析,一步一步从浅入深(二):按步骤解析源码

    在文章:Mybatis源码解析,一步一步从浅入深(一):创建准备工程,中我们为了解析mybatis源码创建了一个mybatis的简单工程(源码已上传github,链接在文章末尾),并实现了一个查询功能 ...

  5. Mybatis源码解析,一步一步从浅入深(三):实例化xml配置解析器(XMLConfigBuilder)

    在上一篇文章:Mybatis源码解析,一步一步从浅入深(二):按步骤解析源码 ,中我们看到 代码:XMLConfigBuilder parser = new XMLConfigBuilder(read ...

  6. Mybatis源码解析,一步一步从浅入深(四):将configuration.xml的解析到Configuration对象实例

    在Mybatis源码解析,一步一步从浅入深(二):按步骤解析源码中我们看到了XMLConfigBuilder(xml配置解析器)的实例化.而且这个实例化过程在文章:Mybatis源码解析,一步一步从浅 ...

  7. Mybatis源码解析(三) —— Mapper代理类的生成

    Mybatis源码解析(三) -- Mapper代理类的生成   在本系列第一篇文章已经讲述过在Mybatis-Spring项目中,是通过 MapperFactoryBean 的 getObject( ...

  8. Mybatis源码解析(二) —— 加载 Configuration

    Mybatis源码解析(二) -- 加载 Configuration    正如上文所看到的 Configuration 对象保存了所有Mybatis的配置信息,也就是说mybatis-config. ...

  9. mybatis源码-解析配置文件(四)之配置文件Mapper解析

    在 mybatis源码-解析配置文件(三)之配置文件Configuration解析 中, 讲解了 Configuration 是如何解析的. 其中, mappers作为configuration节点的 ...

  10. Mybatis源码解析,一步一步从浅入深(一):创建准备工程

    Spring SpringMVC Mybatis(简称ssm)是一个很流行的java web框架,而Mybatis作为ORM 持久层框架,因其灵活简单,深受青睐.而且现在的招聘职位中都要求应试者熟悉M ...

随机推荐

  1. How to setup Visual Studio without pain

    Visual Studio (VS) can be very hard to install. If you are lucky, one whole day may be enough to ins ...

  2. Java Web程序开发链接MySQL数据库

    显示错误:Access denied for user ''@'localhost' (using password: YES) 保证URL中没有空格 尝试用MySQL本地命令行登陆 显示错误:The ...

  3. U-Boot Makefile分析(1)配置脚本mkconfig分析

    我们在编译U-Boot之前,需要根据当前使用的板子进行配置,例如make s5p_goni_config,接着才能进行编译make.下面首先分析配置阶段U-Boot做了哪些事情. 由于执行这些命令是在 ...

  4. linux使用framebuffer的代码

    #include <linux/fb.h> #include <sys/mman.h> #include <sys/ioctl.h> #include <st ...

  5. Stacking Plates(存档待续.....(没有写思路和程序))

    问题描述 盘子装运公司是一家网络零售商,顾名思义,是一家只销售盘子的公司.该公司销售的盘子由不计其数的生产厂商提供,品种是全宇宙最多的,为此公司的员工倍感自豪. 在最近的一次成本分析中,公司员工发现, ...

  6. AbstractRoutingDataSource 实现动态切换数据源

    扩展AbstractRoutingDataSource类 package com.datasource.test.util.database; import org.springframework.j ...

  7. 感悟优化——Netty对JDK缓冲区的内存池零拷贝改造

    NIO中缓冲区是数据传输的基础,JDK通过ByteBuffer实现,Netty框架中并未采用JDK原生的ByteBuffer,而是构造了ByteBuf. ByteBuf对ByteBuffer做了大量的 ...

  8. Akka-CQRS(6)- read-side

    前面我们全面介绍了在akka-cluster环境下实现的CQRS写端write-side.简单来说就是把发生事件描述作为对象严格按发生时间顺序写入数据库.这些事件对象一般是按照二进制binary方式如 ...

  9. Javascript高级编程学习笔记(71)—— 模拟事件(1)DOM事件模拟

    事件,指的是网页中某个特定的交互时刻 一般来说事件由浏览器厂商负责提供,一般由用户操作或者其它浏览器功能来触发 但是有一类特殊的事件,那就是由我们开发人员通过JS触发的事件 这些事件和浏览器创建的事件 ...

  10. Javascript高级编程学习笔记(50)—— DOM2和DOM3(2)样式

    样式 在HTML中定义元素的方式有以下三种: 1.link标签引入外部样式表 2.style标签定义嵌入样式 3.通过JS中对style特性定义元素样式(行内样式) “DOM2级样式”围绕上述样式机制 ...