在 Mybatis 中,常用的作用就是讲数据库中的表的字段映射为对象的属性,在进入Mybatis之前,原生的 JDBC 有几个步骤:导入 JDBC 驱动包,通过 DriverManager 注册驱动,创建连接,创建 Statement,增删改查,操作结果集,关闭连接
过程详解
首先进行类的加载,通过 DriverManager 注册驱动
Class.forName("com.mysql.jdbc.Driver"); Connection connection = DriverManager.getConnection("");
为什么在这里可以直接注册进去,com.mysql.jdbc.Driver 被加载到 Driver.class ,在 DriverManager 中,首先有一个静态代码块来进行初始化加载 Driver
static { loadInitialDrivers(); println("JDBC DriverManager initialized"); }
通过 loadInitialDrivers(),来加载 Driver,拿出 jdbc.drivers,通过 ServiceLoader 读取 Driver.class,读取拿出 driver 和 所有迭代器,一直迭代
private static void loadInitialDrivers() { String drivers; // 访问修饰符,在这里把 jdbc.drivers 拿出来 try { drivers = AccessController.doPrivileged(new PrivilegedAction<String>() { public String run() { return System.getProperty("jdbc.drivers"); } }); } catch (Exception ex) { drivers = null; } AccessController.doPrivileged(new PrivilegedAction<Void>() { public Void run() { // 读取拿出 driver 和 所有迭代器 ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class); Iterator<Driver> driversIterator = loadedDrivers.iterator(); // 一直进行迭代 try{ while(driversIterator.hasNext()) { driversIterator.next(); } } catch(Throwable t) { // Do nothing } return null; } }); println("DriverManager.initialize: jdbc.drivers = " + drivers); if (drivers == null || drivers.equals("")) { return; } String[] driversList = drivers.split(":"); println("number of Drivers:" + driversList.length); for (String aDriver : driversList) { try { println("DriverManager.Initialize: loading " + aDriver); Class.forName(aDriver, true, ClassLoader.getSystemClassLoader()); } catch (Exception ex) { println("DriverManager.Initialize: load failed: " + ex); } } }
从 Driver 加载完后,就可以得到一个和数据库的连接 connection ,connection 就可以创建一个 Statement,Statement 就可以进行执行 sql 语句,将结果返回一个结果集,获取出来的结果集遍历放进一个 List 集合中
Statement statement = connection.createStatement(); ResultSet resultSet = statement.executeQuery("select * from mybatis.user"); while (resultSet.next()) { int id = resultSet.getInt(1); String username = resultSet.getString(2); list.add(new User(id,username)); }
在原生的 JDBC 直接操作中,繁杂的步骤在业务代码中不会使用,而 Mybatis 可以在更好的便利度上使用
在 JDK 动态代理中,利用了 Proxy 这个类来实现,在 Proxy 中,有着 newProxyInstance() 方法,创建一个动态代理实例
interface UserMapper { @Select("select * from mybatis.user where id =#{id}") List<User> selectUserList(); } public static void main(String[] args) { UserMapper userMapper = (UserMapper) Proxy.newProxyInstance( JDKMybatis.class.getClassLoader(), new Class<?>[]{UserMapper.class}, new InvocationHandler() { /** * 在 invoke() 方法中就可以进行查找 method,args * @param proxy 动态代理 * @param method 方法 * @param args 参数 */ @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { // 把注解类获取,可以查出注解的值等多种其他值 Select annotation = method.getAnnotation(Select.class); if (annotation != null) { String[] value = annotation.value(); System.out.println(Arrays.toString(value)); } return null; } }); userMapper.selectUserList(1); }
newProxyInstance() 的创建需要三个参数,查看源码,可以知道需要 ClassLoader 类加载器,interfaces 接口(Mapper 接口),InvocationHandler 处理器,来进行处理
public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)
把 sql 语句中的参数取出来放进 args,这时需要一个 Map 来进行传值
问题
当在通过反射获取方法的参数名,method.getParameters() 获取出来的参数都是 arg0,arg1...无意义参数
在Java8之前,代码编译为class文件后,方法参数的类型是固定的,但参数名称却丢失了,在编译的时候,需要有编译的选项,javac -parameters 默认是关闭的,需要在 idea 中设置开启,开启完成后,重新编译源文件
这种方式只能临时解决当前环境设置,在其他人运行代码时还是要重新设置
另一种解决方式,在pom文件中添加编译参数:
<plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <configuration> <compilerArgument>-parameters</compilerArgument> <source>1.8</source> <target>1.8</target> </configuration> </plugin> </plugins>
编译完成后,重新执行,再次通过method.getParameters()获取参数:
解析原来的 sql ,就要把 #{} 给替换掉,这时候可以使用 StringBuffer 类来实现替换
private static String parseSql(String sql, Map<String, Object> argsNameMap) { // 定义为常量数组 char[] str = {'#', '{'}; StringBuilder stringBuilder = new StringBuilder(); for (int i = 0; i < sql.length(); i++) { char aloneParseSql = sql.charAt(i); if (str[0] == aloneParseSql) { int nextIndex = i + 1; char nextChar = sql.charAt(nextIndex); // # 后应该是 { ,不匹配直接抛出异常 if (str[1] != nextChar) { throw new RuntimeException(String.format( "此处应该是:#{\n sql:%s\n index:%d", stringBuilder.toString(), nextIndex)); } /* 1 已经解析完的下标 2 解析完的 #{} 内的参数名 3 把对应的 argsName 的值 argsValue 取出来 4 追加到原来的 stringBuilder 中的 sql 语句后面 */ StringBuilder partStringBuilder = new StringBuilder(); i = partParseSql(partStringBuilder, sql, nextIndex); String argsName = partStringBuilder.toString(); Object argsValue = argsNameMap.get(argsName); stringBuilder.append(argsValue.toString()); } // 如果没有条件,直接追加 stringBuilder.append(aloneParseSql); } return stringBuilder.toString(); }
在其中需要把需要替换的值,再用 StringBuffer 类来实现
private static int partParseSql(StringBuilder partStringBuilder, String sql, int nextIndex) { // 由于 nextIndex 当前指针指向的是 { 所以要加一位,把后面内容解析 nextIndex++; char[] str = {'}'}; for (; nextIndex < sql.length(); nextIndex++) { char indexSql = sql.charAt(nextIndex); if (str[0] != indexSql) { partStringBuilder.append(indexSql); } if (str[0] == indexSql) { return nextIndex; } } throw new RuntimeException(String.format( "缺少:}\n index:%d", nextIndex)); }
再重新在 invoke 方法中进行调用,完成 sql 语句的动态拼装
@Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { // 把注解类获取,可以查出注解的值等多种其他值 Select annotation = method.getAnnotation(Select.class); Map<String, Object> argsNameMap = MapBuildArgsName(method, args); if (annotation != null) { String[] value = annotation.value(); String sql = value[0]; sql = parseSql(sql, argsNameMap); System.out.println(sql); } return null; }