web项目
- 1、准备
- 2、分析
- 3、 MyBatis对象作用域以及事务问题
- 4、问题
实现一个转账系统
1、准备
①准备一个web模块 在这里使用了maven archetype,选择web
之后会生成 一个web模块,但是不同的版本可能不同,在这里我就没有java和resources目录,记得自己创建一个。
②之后导入依赖,因为是mybatis,肯定要导入mybatis和mysql依赖,web项目,导入servlet依赖。
<?xml version="1.0" encoding="UTF-8"?><project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><groupId>org.example</groupId><artifactId>web-app01</artifactId><version>1.0-SNAPSHOT</version><packaging>war</packaging><name>web-app01 Maven Webapp</name><!-- FIXME change it to the project's website --><url>http://www.example.com</url><properties><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding><maven.compiler.source>1.7</maven.compiler.source><maven.compiler.target>1.7</maven.compiler.target></properties><dependencies><!-- mybatis--><dependency><groupId>org.mybatis</groupId><artifactId>mybatis</artifactId><version>3.5.10</version></dependency><!-- mysql--><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>8.0.30</version></dependency><!-- logback--><dependency><groupId>ch.qos.logback</groupId><artifactId>logback-classic</artifactId><version>1.2.11</version></dependency><!-- servlet--><dependency><groupId>jakarta.servlet</groupId><artifactId>jakarta.servlet-api</artifactId><version>6.0.0</version></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><version>1.18.30</version></dependency></dependencies><build><finalName>web-app01</finalName><pluginManagement><!-- lock down plugins versions to avoid using Maven defaults (may be moved to parent pom) --><plugins><plugin><artifactId>maven-clean-plugin</artifactId><version>3.1.0</version></plugin><!-- see http://maven.apache.org/ref/current/maven-core/default-bindings.html#Plugin_bindings_for_war_packaging --><plugin><artifactId>maven-resources-plugin</artifactId><version>3.0.2</version></plugin><plugin><artifactId>maven-compiler-plugin</artifactId><version>3.8.0</version></plugin><plugin><artifactId>maven-surefire-plugin</artifactId><version>2.22.1</version></plugin><plugin><artifactId>maven-war-plugin</artifactId><version>3.2.2</version></plugin><plugin><artifactId>maven-install-plugin</artifactId><version>2.5.2</version></plugin><plugin><artifactId>maven-deploy-plugin</artifactId><version>2.8.2</version></plugin></plugins></pluginManagement></build>
</project>
③web.xml文件
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="https://jakarta.ee/xml/ns/jakartaee"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaeehttps://jakarta.ee/xml/ns/jakartaee/web-app_5_0.xsd"version="5.0"metadata-complete="false"></web-app>
注意 metadata-complete 这个属性 如果为true的话意味着只支持 web.xml 文件,不支持注解。
④准备其他配置文件
首先看一下我的项目目录
jdbc.properties
jdbc.url=jdbc:mysql://localhost:3306/test
jdbc.driver=com.mysql.cj.jdbc.Driver
jdbc.name=ckytest
jdbc.pwd=123456
logbackxml
<?xml version="1.0" encoding="UTF-8"?><configuration debug="false"><!-- 控制台输出 --><appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"><encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder"><!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符--><pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern></encoder></appender><!-- 按照每天生成日志文件 --><appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"><rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"><!--日志文件输出的文件名--><FileNamePattern>${LOG_HOME}/TestWeb.log.%d{yyyy-MM-dd}.log</FileNamePattern><!--日志文件保留天数--><MaxHistory>30</MaxHistory></rollingPolicy><encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder"><!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符--><pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern></encoder><!--日志文件最大的大小--><triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy"><MaxFileSize>100MB</MaxFileSize></triggeringPolicy></appender><!--mybatis log configure--><logger name="com.apache.ibatis" level="TRACE"/><logger name="java.sql.Connection" level="DEBUG"/><logger name="java.sql.Statement" level="DEBUG"/><logger name="java.sql.PreparedStatement" level="DEBUG"/><!-- 日志输出级别,logback日志级别包括五个:TRACE < DEBUG < INFO < WARN < ERROR --><root level="DEBUG"><appender-ref ref="STDOUT"/><appender-ref ref="FILE"/></root></configuration>
mybatis-config.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configurationPUBLIC "-//mybatis.org//DTD Config 3.0//EN""http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration><properties url="file:///D:/jdbc.properties"></properties>
<!--一个数据库对应一个一个环境,一个数据库对应一个sqlSessionFactory对象-->
<!-- 即一个环境 对应一个sqlSessionFactory对象-->
<!-- 默认 使用的是defalut的数据库--><environments default="mybatisDB"><environment id="mybatisDB"><transactionManager type="JDBC"/><dataSource type="POOLED"><property name="driver" value="${jdbc.driver}"/><property name="url" value="${jdbc.url}"/><property name="username" value="${jdbc.name}"/><property name="password" value="${jdbc.pwd}"/></dataSource></environment></environments><mappers><!--sql映射文件创建好之后,需要将该文件路径配置到这里--><mapper resource="ActMapping.xml"/></mappers>
</configuration>
ActMapping.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapperPUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!--namespace先随意写一个-->
<mapper namespace="aaa"><select id="selectAct" resultType="com.cky.beans.Account">select * from t_act where actno=#{actno};
</select><update id="updateAct">update t_act set balance = #{balance} where actno = #{actno}</update>
</mapper>
⑤准备java类
使用MVC模式
不全放出来了,只写几个重要的。
工具类
package com.cky.util;import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;import java.io.IOException;public class MysqlsessionUtils {private static SqlSessionFactory sqlSessionFactory;/*** 类加载时初始化sqlSessionFactory对象*/static {try {SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();sqlSessionFactory = sqlSessionFactoryBuilder.build(Resources.getResourceAsStream("mybatis-config.xml"));} catch (Exception e) {e.printStackTrace();}}private static ThreadLocal<SqlSession> local = new ThreadLocal<>();/*** 每调用一次openSession()可获取一个新的会话,该会话支持自动提交。** @return 新的会话对象*/public static SqlSession getSqlSession() {SqlSession sqlSession = local.get();if (sqlSession == null) {sqlSession = sqlSessionFactory.openSession();local.set(sqlSession);}return sqlSession;}public static void closeSession(){SqlSession sqlSession = local.get();if (sqlSession != null) {sqlSession.close();}local.remove();}}
web层
package com.cky.web;import com.cky.beans.Account;
import com.cky.exception.AppException;
import com.cky.exception.MoneyNotEnoughException;
import com.cky.service.ActService;
import com.cky.service.impl.ActServiceimpl;
import jakarta.servlet.Servlet;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;import java.io.IOException;
@WebServlet(value = "/transfer")
public class ActServlet extends HttpServlet {@Overrideprotected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {String fromActno = req.getParameter("fromActno");String toActno = req.getParameter("toActno");double money = Double.parseDouble(req.getParameter("money"));//业务层 转帐逻辑ActService actService=new ActServiceimpl();//视图层
// resp.sendRedirect();try {actService.transfer(fromActno,toActno,money);resp.sendRedirect(req.getContextPath()+"/sussess.html");} catch (MoneyNotEnoughException e) {resp.sendRedirect(req.getContextPath()+"/error.html");} catch (AppException e) {resp.sendRedirect(req.getContextPath()+"/error.html");}}
}
Serviceimpl
package com.cky.service.impl;import com.cky.beans.Account;
import com.cky.dao.Actdao;
import com.cky.dao.impl.ActDaoimpl;
import com.cky.exception.AppException;
import com.cky.exception.MoneyNotEnoughException;
import com.cky.service.ActService;
import com.cky.util.MysqlsessionUtils;
import org.apache.ibatis.session.SqlSession;public class ActServiceimpl implements ActService {Actdao actdao=new ActDaoimpl();private Actdao accountDao = new ActDaoimpl();@Overridepublic void transfer(String fromActno, String toActno, double money) throws MoneyNotEnoughException, AppException {// 查询转出账户的余额Account fromAct = accountDao.queryAct(fromActno);if (fromAct.getBalance() < money) {throw new MoneyNotEnoughException("对不起,您的余额不足。");}SqlSession sqlSession=null;try {// 程序如果执行到这里说明余额充足// 修改账户余额Account toAct = accountDao.queryAct(toActno);fromAct.setBalance(fromAct.getBalance() - money);toAct.setBalance(toAct.getBalance() + money);// 更新数据库(添加事务)sqlSession = MysqlsessionUtils.getSqlSession();accountDao.actUpdate(fromAct);// 模拟异常String s = null;s.toString();accountDao.actUpdate(toAct);sqlSession.commit();} catch (Exception e) {sqlSession.rollback(); // 回滚事务throw new AppException("转账失败,未知原因!");}finally {MysqlsessionUtils.closeSession(); // 只修改了这一行代码。}}
}
2、分析
1、
首先是
private static ThreadLocal<SqlSession> local = new ThreadLocal<>();
这是相当于是一个map集合,key是线程的名字,value是该线程要存储的内容,在这里就是一个连接,一个线程对应一个连接。get()是根据当前的线程获取连接,set()是给当前的线程设置连接。
2、
在弄这个简单的web项目时,出来一个很大的bug,就是我的表,我用的是MyISAM存储引擎,该引擎默认就不支持事务,所以即使我把使Autocommit设置为关闭,MyISAM表仍然无法支持事务,因为它本身不支持事务的概念。所以要用INnoDB引擎啊!!!注意。
3、
在这里,我模拟了一个异常
// 模拟异常
String s = null;
s.toString();
就是看当异常出现时,会不会导致一条记录更新,一条记录不更新。这也是为什么要用事务的原因,记得,事务一定要在一个连接里,而一个线程也对应一条连接
。
4、
dao层就是专门用来与数据库连接的,而业务层就是专门做业务逻辑,web层则是像个司令官,收集到请求的信息后,要求service层来实现业务,之后调用视图层将内容返还给用户。
3、 MyBatis对象作用域以及事务问题
MyBatis核心对象的作用域
SqlSessionFactoryBuilder
这个类可以被实例化、使用和丢弃,一旦创建了 SqlSessionFactory,就不再需要它了。 因此 SqlSessionFactoryBuilder 实例的最佳作用域是方法作用域(也就是局部方法变量)。 你可以重用 SqlSessionFactoryBuilder 来创建多个 SqlSessionFactory 实例,但最好还是不要一直保留着它,以保证所有的 XML 解析资源可以被释放给更重要的事情。
SqlSessionFactory
SqlSessionFactory 一旦被创建就应该在应用的运行期间一直存在,没有任何理由丢弃它或重新创建另一个实例。 使用 SqlSessionFactory 的最佳实践是在应用运行期间不要重复创建多次,多次重建 SqlSessionFactory 被视为一种代码“坏习惯”。因此 SqlSessionFactory 的最佳作用域是应用作用域。 有很多方法可以做到,最简单的就是使用单例模式或者静态单例模式。
SqlSession
每个线程都应该有它自己的 SqlSession 实例。SqlSession 的实例不是线程安全的,因此是不能被共享的,所以它的最佳的作用域是请求或方法作用域。 绝对不能将 SqlSession 实例的引用放在一个类的静态域,甚至一个类的实例变量也不行。 也绝不能将 SqlSession 实例的引用放在任何类型的托管作用域中,比如 Servlet 框架中的 HttpSession。 如果你现在正在使用一种 Web 框架,考虑将 SqlSession 放在一个和 HTTP 请求相似的作用域中。 换句话说,每次收到 HTTP 请求,就可以打开一个 SqlSession,返回一个响应后,就关闭它。 这个关闭操作很重要,为了确保每次都能执行关闭操作,你应该把这个关闭操作放到 finally 块中。 下面的示例就是一个确保 SqlSession 关闭的标准模式。
4、问题
DAO层实现类
package com.cky.dao.impl;import com.cky.beans.Account;
import com.cky.dao.Actdao;
import com.cky.util.MysqlsessionUtils;
import org.apache.ibatis.session.SqlSession;public class ActDaoimpl implements Actdao {@Overridepublic Account queryAct(String act) {SqlSession sqlSession = MysqlsessionUtils.getSqlSession();Account selectAct = (Account) sqlSession.selectOne("aaa.selectAct", act);return selectAct;}@Overridepublic int actUpdate(Account account) {SqlSession sqlSession = MysqlsessionUtils.getSqlSession();int update = sqlSession.update("aaa.updateAct", account);return update;}
}
我们不难发现,这个dao实现类中的方法代码很固定,基本上就是一行代码,通过SqlSession对象调用insert、delete、update、select等方法,这个类中的方法没有任何业务逻辑,既然是这样,这个类我们能不能动态的生成,以后可以不写这个类吗?答案:可以。
之后再讲~~