深入学习MyBatis框架(2)

1、log4j 简介

程序编写完成之后,打为war包,war包最后运行在服务器上, 假设现在有客户反馈说,注册功能特别得慢?我们需要查看注册功能得具体日志信息。

  • 日志对于程序得调优,问题得排查,监测都具有非常大得意义;
  • 日志处理解决方案: apache LOG4J,但是不局限于log4j
  • 简单的说 log4j 就是帮助开发人员进行日志输出管理的API类库。它最重要的特点就可以配置文件,灵活的设置日志信息的优先级、日志信息的输出目的地、日志信息的输出格式;
  • log4j 除了可以记录程序运行日志信息外还有一重要的功能就是用来显示调试信息
  • 程序员经常会遇到脱离java idea环境调试程序的情况,这时大多数人会选择使用最普通的System.out.println语句输出某个变量值的方法进行调试。这样会带来一个非常麻烦的问题:一旦哪天程序员决定不要显示这些System.out.println的东西了就只能一行行的把这些垃圾语句注释掉,若哪天又需调试变量值,则只能再次恢复System.out.println语句,并且IO的操作会影响程序运行得速度,能不能不输出在控制台,而是发个邮件、存储在一个文件中,使用 log4j 则可以很好的处理类似情况。
  • 几乎所有得框架都有日志得记录,通过这个框架日志,就可以查看框架执行得过程,细节;我们目前可以通过它查看mybatis中的执行SQL和参数;

2、log4j 的使用

log4j 的使用步骤:

1、 maven 依赖 log4j 的 jar 包;
2、编写 log4j 的核心配置文件,编译过后默认是在 classes:log4j.properties 取得日志记录器;
3、记录日志注意级别;

1、定义 log4j 的配置文件:

首先使用配置文件将使我们的应用程序更加灵活配置log日志输出方式包括输出优先级、输出目的地、输出格式。

  • log4j支持两种配置文件格式:

1、XML格式的文件;
2、Java特性文件log4j.properties(键=值)。

  • 下面将介绍使用log4j.properties文件作为配置文件的方法:

① 配置根 logger,其语法为:

log4j.rootLogger = [ level ] , appenderName, appenderName, …

其中,level 是日志记录的优先级,分为OFF、FATAL、ERROR、WARN、INFO、DEBUG、ALL或者自定义的级别


log4j建议只使用四个级别,优先级从高到低分别是 ERROR、WARN、INFO、DEBUG


通过在这里定义的级别,你可以控制到应用程序中相应级别的日志信息的开关。比如在这里定义了INFO级别,则应用程序中所有DEBUG级别的日志信息将不被打印出来。appenderName就是指定日志信息输出到哪个地方,可同时指定多个输出目的地。

② 配置日志信息输出目的地Appender,其语法为:

log4j.appender.appenderName = fully.qualified.name.of.appender.class 
log4j.appender.appenderName.option1 = value1 
… 
log4j.appender.appenderName.option = valueN
  • 其中,Log4j提供的appender有以下几种:
org.apache.log4j.ConsoleAppender(控制台)
org.apache.log4j.FileAppender(文件)
org.apache.log4j.DailyRollingFileAppender(每天产生一个日志文件)
org.apache.log4j.RollingFileAppender(文件大小到达指定尺寸的时候产生一个新的文件)
org.apache.log4j.WriterAppender(将日志信息以流格式发送到任意指定的地方)

③ 配置日志信息的格式(布局),其语法为:

log4j.appender.appenderName.layout = fully.qualified.name.of.layout.class 
log4j.appender.appenderName.layout.option1 = value1 
… 
log4j.appender.appenderName.layout.option = valueN
  • 其中,Log4j提供的layout有以下几种:
org.apache.log4j.HTMLLayout(以HTML表格形式布局), 
org.apache.log4j.PatternLayout(可以灵活地指定布局模式), 
org.apache.log4j.SimpleLayout(包含日志信息的级别和信息字符串), 
org.apache.log4j.TTCCLayout(包含日志产生的时间、线程、类别等等信息)
  • Log4J采用类似C语言中的printf函数的打印格式格式化日志信息,打印参数如下:
%m 输出代码中指定的消息

%p 输出优先级,即DEBUG,INFO,WARN,ERROR,FATAL 

%r 输出自应用启动到输出该log信息耗费的毫秒数 

%c 输出所属的类目,通常就是所在类的全名 

%t 输出产生该日志事件的线程名 

%n 输出一个回车换行符,Windows平台为“\r\n”,Unix平台为“\n” 

%d 输出日志时间点的日期或时间,默认格式为ISO8601,也可以在其后指定格式,比如:%d{yyy MMM dd HH:mm:ss,SSS},输出类似:2002年10月18日 22:10:28,921 

%l 输出日志事件的发生位置,包括类目名、发生的线程,以及在代码中的行数。举例:Testlog4.main(TestLog4.java:10)

log4j.properties文件内容:

#USE THIS SETTING FOR OUTPUT MYBATIS`s SQL ON THE CONSOLE
log4j.rootLogger=debug, Console

#Console
log4j.appender.Console=org.apache.log4j.ConsoleAppender
log4j.appender.Console.layout=org.apache.log4j.PatternLayout
#log4j.appender.Console.layout.ConversionPattern=%d [%t] %-5p [%c] - %m%n
log4j.appender.Console.layout.ConversionPattern=[ %l ] %d{yyyy/MM/dd HH:mm:ss} %-5p [%c{1}] - %l - %m%n

注意:文件名字是规定好的,不能随意命名;

2、在代码中使用 log4j:

① 得到记录器:

使用 Log4j,第一步就是获取日志记录器,这个记录器将负责控制日志信息。其语法为:

public static Logger getLogger( String name)

通过指定的名字获得记录器,如果必要的话,则为这个名字创建一个新的记录器。Name一般取本类的名字,比如:

Logger logger = Logger.getLogger(MyTest1.class.getName());

② 读取配置文件

当获得了日志记录器之后,第二步将配置Log4j环境,其语法为:

//自动快速地使用缺省log4j环境
BasicConfigurator.configure();
//读取使用Java的特性文件编写的配置文件
PropertyConfigurator.configure("src/main/resources/log4j.properties");

DOMConfigurator.configure ( String filename ) :读取XML形式的配置文件

③ 插入记录信息(格式化日志信息)

当上两个必要步骤执行完毕,就可轻松地使用不同优先级别的日志记录语句插入到你想记录日志的任何地方,其语法如下:

Logger.debug ( Object message ) ;
Logger.info ( Object message ) ;
Logger.warn ( Object message ) ;
Logger.error ( Object message ) ;
  • log4j 范例程序:
@Test
public void loggerTest(){
    //1、得到记录器
    Logger logger = Logger.getLogger(MyTest1.class.getName());
    //2、读取配置文件

    //自动快速地使用缺省log4j环境
    BasicConfigurator.configure();
    //读取使用Java的特性文件编写的配置文件
    PropertyConfigurator.configure("src/main/resources/log4j.properties");

    //DOMConfigurator.configure ( String filename ) :读取XML形式的配置文件

    logger.debug("Debug ...");
    logger.info("Info ...");
    logger.warn("Warn ...");
    logger.error("Error ...");
}

在这里插入图片描述
3、在web程序中使用log4j注意问题:

  • 由于jsp或servlet在执行状态时没有当前路径概念,所有使用PropertyConfigurator.configure(String)语句找log4j.properties文件时要给出相对于当前jsp或servlet的路径转化成为一个绝对的文件系统路径:
//得到当前jsp路径
String prefix =   getServletContext().getRealPath("/");
//读取log4j.properties
PropertyConfigurator.configure(prefix+"\\WEB-INF\\log4j.properties");
  • 相应的log4j.properties设置某个属性时也要在程序中设置绝对路径。例:

log4j.appender.R.File属性设置日志文件存放位置,我们可以用读写.properties配置文件的方法进行灵活设置。

log4j详细用法链接

3、mybatis中使用log4j

  • 将配置文件log4j.properties中的日志优先级分别设置为:ERROR、WARN、INFO、DEBUG
@Test
public void selectById(){
        AccountDao accountDao = new AccountDao();
        System.out.println(JSON.toJSONString(accountDao.selectById(2), true));
}
public class AccountDao {
    public Account selectById(Integer aid){
        //opensession()中给true代表自动提交事务,默认值为false
        SqlSession sqlSession = MyBatisUtil.getSqlSessionFactory().openSession();
        try {
            return sqlSession.selectOne("com.oracle.mapper.AccountMapper.selectById", aid);
        } finally {
            sqlSession.close();
        }
    }
}
<select id="selectById" resultType="account">
        select * from account where aid = ${value}
</select>
#USE THIS SETTING FOR OUTPUT MYBATIS`s SQL ON THE CONSOLE
log4j.rootLogger=debug, Console

#Console
log4j.appender.Console=org.apache.log4j.ConsoleAppender
log4j.appender.Console.layout=org.apache.log4j.PatternLayout
#log4j.appender.Console.layout.ConversionPattern=%d [%t] %-5p [%c] - %m%n
log4j.appender.Console.layout.ConversionPattern=[ %l ] %d{yyyy/MM/dd HH:mm:ss} %-5p [%c{1}] - %l - %m%n

1、error、warn、info 级别:
在这里插入图片描述
2、debug级别:
在这里插入图片描述

  • mybatis使用log4j.properties,将该文件放在src目录下,最终编译生成在classes路径下,也就是说该文件的路径和名称都是固定的,是log4j默认加载的文件路径,不允许改变;

以下这行配置是mybatis中的默认配置,该配置就是用来查找log4j的配置文件,并读取其中内容的;也就是只要有了名字为log4j.properties的文件以及log4j的jar包依赖,mybatis就会默认帮我们读取配置文件,并打印日志信息;

<configuration>
    <settings>
        <!-- 开启log4j来记录日志 -->
        <setting name="logImpl" value="log4j"/>
    </settings>
</configuration>

4、resultset怎么转换为Java对象

  • 首先mybatis是一个持久层的框架,框架考虑得内容很多;比如 mybatis封装了一组API接口,让我们通过接口就可以和数据库交互,它屏蔽了jdbc api
  • 提供功能:参数映射,结果集映射,缓存……
  • 参数映射:

传递参数的时候,我们Java代码传入对象,XML文件中:

mapper sql #{属性} / ${属性}

mybatis 通过OGNL(对象图导航语言)获得对象的属性值;

  • 以前我们编写sql语句发送给数据库执行,一定要使用JDBC API,执行后得到结果对象 ResultSet,我们就要手动实现 resultset 到 java对象是如何转换得。

  • 数据库元数据:就是描述数据的数据,比如:connection是数据库连接信息,在connection连接数据的基础上还有这个连接以外的很多数据,例如数据库名称,数据库版本,数据库驱动……等数据,描述这些数据的对象 我们就称之为数据库元数据

  • Resultset:是数据库结果集信息,关于这个对象我们通常使用得比较多的方法是 next、getxxx、close

  • 其实结果集中还有一些其他的数据,比如该结果集中有多少列,列的名称,列的类型是什么…… 描述这些数据的数据就是结果集 元数据

  • 获取到了元数据之后,就可以一键生成实体类,也可以编写工具将resultset与Java对象关联。

resultset转换为java对象 = resultset元数据 + java反射

手动实现转换过程:

import com.alibaba.fastjson.JSON;
import com.oracle.model.Account;
import org.apache.log4j.Logger;

import java.lang.reflect.Method;
import java.sql.*;

public class MyTest2 {
    //获取log4j日志记录 工具
    public static Logger logger = Logger.getLogger(MyTest2.class.getName());

    public static void main(String[] args) throws Exception {
        logger.debug("开始加载驱动");

        Class.forName("com.mysql.cj.jdbc.Driver");
        logger.debug("加载驱动成功!");

        //获取连接对象
        Connection conn = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/db2020?serverTimezone=UTC&characterEncoding=utf-8", "root", "123456");
        String sql = "select * from account where aid=?";

        PreparedStatement preparedStatement = conn.prepareStatement(sql);
        preparedStatement.setInt(1, 2);

        //获取结果集对象
        ResultSet resultSet = preparedStatement.executeQuery();

        //获取解析后的对象
        Object account = resultSetToBean(resultSet, Account.class);
        //打印该对象
        System.out.println("将结果集转为Java对象的结果为:" + JSON.toJSONString(account, true));
    }

    private static Object resultSetToBean(ResultSet resultSet, Class accountClass) throws Exception {
        //创建要转换的类型的Java对象
        Object result = accountClass.newInstance();

        //获取结果集元数据
        ResultSetMetaData metaData = resultSet.getMetaData();
        //获取结果集中列的个数
        int columnCount = metaData.getColumnCount();
        System.out.println("结果集中列的个数为:" + columnCount);

        //创建我们希望最终封装成的Java对象
        Account account = new Account();
        //通过反射获取该对象中的方法
        Method[] methods = account.getClass().getDeclaredMethods();

        //遍历结果集对象取出数据
        while (resultSet.next()) {
            for (int i = 1; i <= columnCount; i++) {
                //获取结果集中该列的名称
                String columnName = metaData.getColumnName(i);
                //获取该结果集中当前列的数据类型
                String columnTypeName = metaData.getColumnTypeName(i);

                //获取当前列的值
                Object conlumnVal = resultSet.getObject(columnName);

                for (Method method : methods) {
                    //获取该方法的名字
                    String methodName = method.getName();
                    if (methodName.equalsIgnoreCase("set" + columnName)) {
                        method.invoke(result, conlumnVal);
                    }
                }
            }
        }

        return result;
    }
}

在这里插入图片描述

  • mybatis底层源码分析之代理方法中访问数据库的细节详解
  • 以上的代码只是简单的将根据主键查询出来的一个对象进行封装,其实我们还可以自己手动实现查询多条、删除、修改等操作,增强对mybatis底层的认识;
  • 其实,Apache早就封装好了第三方的工具类DBUtils,帮助我们将查询出来的结果集封装起来,我们也可以查看他的底层源码然后写出自己的一套工具类;

5、mapper代理

  • 通过mybatis API,封装工具类创建全局的sessionfactory,首先编写Mapper的实现类;
public class AccountMapper {
    public Account selectById(Integer aid) {
        SqlSession sqlSession = MyBatisUtil.getSqlSessionFactory().openSession();
        try {
            return sqlSession.selectOne("com.oracle.mapper.AccountMapper.selectById", aid);
        } finally {
            sqlSession.close();
        }
    }

    public List<Account> selectAll() {
        SqlSession sqlSession = MyBatisUtil.getSqlSessionFactory().openSession();
        try {
            return sqlSession.selectList("com.oracle.mapper.AccountMapper.selectAll");
        } finally {
            sqlSession.close();
        }
    }
}

测试类调用:

public class MyTest3 {
    public static void main(String[] args) {
        AccountService accountService = new AccountService();
        System.out.println(JSON.toJSONString(accountService.selectById(2), true));

        System.out.println(JSON.toJSONString(accountService.selectAll(), true));
    }
}

在这里插入图片描述

  • 以上代码存在的问题是:在Java代码中需要硬编码SQLID,才能够获取mapper文件中的sql语句;还有就是每一个方法的开头和结构都是获取session,finally块中关闭 session。像这样的一些代码,我们可以通过模板方法(代理方式)实现;
  • 什么是代理模式

细说Java代理模式

CGLIB动态代理详解

  • mybatis中提供Mapper接口开发模式,这种模式的意思是:编写一个接口。比如AccountMapper,再编写一个AccountMapper.xml,符合一定的要求之后,在程序中我们可以直接调用接口的方法编程,而无需关注接口的实现类,因为mybatis帮我们自动完成了查找SQL的功能。
  • 具体Mapper接口开发模式需要符合:

1、接口的名称和mapper.xml文件的名称保持一致,比如:AccountMapper.java 接口与AccountMapper.xml 文件;
2、接口的全路径作为 AccountMapper.xml 中 namespace 的值;
3、接口中的方法名称为 AccountMapper.xml 中 SQL 的 ID 值;
4、接口中方法的参数类型、返回值类型和 AccountMapper.xml 中定义的 resultType、parameterType保持一致 ;
(保持一致,具体是指 parameterType 可以省略不写,mybatis会利用 TypeHandler 根据传入的参数类型自动推定)
5、查询方法接口中定义的是集合类型,但是xml文件中定义别的是集合中的某一条数据的类型 (resultType设置的是单条结果数据类型)。
6、Java中的定义接口中的方法可以重载,但是在Mapper映射中不可以重载。
7、在 mybatis.xml 中扫描mapper接口包,加载xml文件,或者逐一加载接口全路径都可以。

<!--加载mapper映射文件--> 
<!--当使用了mapper代理方式开发,在mybatis-config.xml中加载 xml 或者加载接口,或者 扫描包 都可以被mybatis识别--> 
<mappers> 
	<!--mapper文件的全路径,注意使用/分割--> 
	<mapper resource="com/oracle/mapper/AccountMapper.xml"/> 
	<!--class加载mapper接口--> 
	<!--<mapper class="com.oracle.mapper.AccountMapper" />--> 
	<!--package包扫描,加载的是接口 (mapper代理开发模式)--> 
	<!--这是推荐方式--> 
	<!--<package name="com.oracle.mapper"/>--> 
</mappers>
  • 使用Mapper接口开发模式需要的步骤为:

1、在 mybais.xml 中加载接口文件或 mapper 配置文件

<mappers>
        <!--mapper文件的全路径,注意使用 / 分割-->
        <mapper resource="com/oracle/mapper/AccountMapper.xml"/>
        <!--class加载 mapper 接口-->
        <!--<mapper class="com.oracle.mapper.AccountMapper" />-->
        <!--package包扫描,加载的是接口 (mapper代理开发模式)-->
        <!--这是推荐方式-->
        <!--<package name="com.oracle.mapper"/>-->
</mappers>

这里值得注意的是:加载接口与加载XML文件只能存在一个,否则会报出已经加载过了的异常,关于这部分的源码这里有详解;

2、将 mapper 接口交给 mybatis ,使用 mapper 代理方式进行 mybatis 开发

import com.alibaba.fastjson.JSON;
import com.oracle.mapper.AccountMapper;
import com.oracle.utils.MyBatisUtil;
import org.apache.ibatis.session.SqlSession;

public class MyTest4 {
    public static void main(String[] args) {
        //创建sqlSession会话对象
        SqlSession sqlSession = MyBatisUtil.getSqlSessionFactory().openSession();
        //使用mybatis动态生成AccountMapper接口的实现类
        AccountMapper mapper = sqlSession.getMapper(AccountMapper.class);
        //我们可以打印输出该类,此时打印的是该对象的地址值,可以看到该对象的所属类名称
        System.out.println(mapper);
        //org.apache.ibatis.binding.MapperProxy@548e7350

        System.out.println(JSON.toJSONString(mapper.selectById(2), true));
        /*{
            "aid":2,
             "aname":"zhangcuishan",
             "apass":"shanshan"
        }*/
        System.out.println(JSON.toJSONString(mapper.selectAll(), true));
        /*[
        {
            "aid":2,
                "aname":"zhangcuishan",
                "apass":"shanshan"
        },……]*/
    }
}
  • mapper.xml不支持重写
public interface AccountMapper {
    public Account selectById(Integer aid);
    public Account selectById();
    public List<Account> selectAll();
}

在这里插入图片描述

报错信息如下:

Exception in thread "main" org.apache.ibatis.exceptions.PersistenceException: 
### Error building SqlSession.
### The error may exist in com/oracle/mapper/AccountMapper.xml
### Cause: org.apache.ibatis.builder.BuilderException: Error parsing SQL Mapper Configuration. 
Cause: org.apache.ibatis.builder.BuilderException: Error parsing Mapper XML. The XML location is 'com/oracle/mapper/AccountMapper.xml'. 
Cause: java.lang.IllegalArgumentException: Mapped Statements collection already contains value for com.oracle.mapper.AccountMapper.selectById

我们使用Mybatis动态生成的代理实现类调用接口中的方法时,Mybatis底层会默认帮我们去mapper配置文件中查找SQL ID与该方法名一样的SQL语句然后执行并返回结果,如果此时我们写了两个ID一样的SQL,Mybatis会变得无所适从,他也不知道该查找哪一个SQL语句了;

6、代理设计模式

  • 代理:一个就是核心的事情,一个是一些围绕着核心事情展开的一些其他事情;

程序:是 完成某一个业务,有核心业务,和非核心业务之分。所谓代理设计模式,就是将非核心业务与核心业务相分离,使得代码容易维护,有的时候还可以减少代码的冗余,让程序具有更高的扩展性。

  • 代理设计模式中的几个概念:

目标角色:target 被代理者【卖房者】
代理角色:proxy 代理角色【中介】
抽象角色:是指目标和代理的共同约束,方法【接口,抽象类】
客户端:调用者

  • 代理的Java实现方式:

静态代理:有一个类,就要写一个代理类, 类太多了,太繁琐了;
动态代理:程序运行过程中动态产生的接口实现类

  • 动态代理的实现1

基于接口的JDK的proxy,要求被代理类【目标类】一定要实现接口;

目标类:

public class IAccountServiceImpl implements IAccountService {
    @Override
    public void register(Account account) {
        System.out.println("account service 要保存数据了");
    }

    @Override
    public void show() {
        System.out.println("account service 查询数据了");
    }
}

动态生成类并测试:

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.text.SimpleDateFormat;
import java.util.Date;

public class ProxyUtil implements InvocationHandler {
    //目标类
    private Object target;

    private ProxyUtil(Object target) {
        this.target = target;
    }

    //动态产生一个代理类
    public Object getProxy() {
        Object result = null;
        //JDK Proxy要求目标类必须要有接口
        result = Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), this);
        return result;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        Object result = null;
        long start = System.currentTimeMillis();
        System.out.println("====" + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()) + "正在访问:" + method.getName() + "====");
        //调用目标类的业务逻辑
        result = method.invoke(target, args);

        long end = System.currentTimeMillis();
        System.out.println("方法耗时:" + (end - start) + "毫秒");
        return result;
    }

    public static void main(String[] args) {
        IAccountService iAccountService = new IAccountServiceImpl();
        IAccountService service = (IAccountService) new ProxyUtil(iAccountService).getProxy();
        service.register(null);
        service.show();
    }
    
    /*====2020-09-10 13:22:02正在访问:register====
    account service 要保存数据了
    方法耗时:90毫秒
    ====2020-09-10 13:22:02正在访问:show====
    account service 查询数据了
    方法耗时:0毫秒*/
}
  • 动态代理的实现2
import net.sf.cglib.proxy.*;
import java.lang.reflect.Method;
public class ProxyCGLibUtil implements Callback {
    private Object obj;

    public ProxyCGLibUtil(Object o) {
        this.obj = o;
    }

    public Object getProxy() {
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(obj.getClass());
        enhancer.setCallback(new MethodInterceptor() {
            @Override
            public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
                System.out.println("代理业务的代码");
                return methodProxy.invokeSuper(o, objects);
            }
        });

        return enhancer.create();
    }
}
public class MyTest6 {
    public static void main(String[] args) {
        IAccountService accountService=new IAccountServiceImpl();
        IAccountService service = (IAccountService) new ProxyCGLibUtil(accountService).getProxy();

        System.out.println(service);
        //org.westos.demo.IAccountServiceImpl$$EnhancerByCGLIB$$1027855d@4bf558aa
        service.register(null);
        service.show();
//        代理业务的代码
//        account service 要保存数据了
//        代理业务的代码
//        account service 查询数据了
    }
}
©️2020 CSDN 皮肤主题: 技术工厂 设计师:CSDN官方博客 返回首页