ThreadLocal+Filter处理事务
demo整体结构
事务过滤器类实现Filter
接口,重写doFilter
方法,主要的连接设置和绑定都在这里面进行,当对请求前的数据库连接设置完毕之后,一定要调用chain.doFilter()
方法放行,然后根据结果来判断本次事务操作是否提交。捕获chain.doFilter()
(主要关注)或者其他连接相关异常的时候,需要将异常范围变大。
public class TransactionFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
System.out.println("TransactionFilter init...");
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
Connection connection = JdbcUtil.getConnection();
//将当前线程和Connection绑定,确保后继dao使用的是同一个数据库连接
try {
connection.setAutoCommit(false);
ConnectionContext.getInstance().bind(connection);
//放行,必须try-catch语句块包括,确保事务得到控制
chain.doFilter(request, response);
//提交事务
connection.commit();
System.out.println("事务已提交");
} catch (Exception e) {
try {
//回滚事务
System.out.println("事务已回滚");
connection.rollback();
response.getWriter().write(e.getMessage());
} catch (SQLException ex) {
ex.printStackTrace();
}
e.printStackTrace();
} finally {
try {
//提交事务关闭连接
connection.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
@Override
public void destroy() {
}
}
上下文连接处理类,这个类使用ThreadLocal
将特定的线程与数据库连接绑定,然后get的时候从本地线程中取,确保取得的连接属于当前线程。
/**
* Created by WuJiXian on 2020/10/24 21:19
*/
public class ConnectionContext {
// 构造函数私有化,单例
private ConnectionContext() {}
private static ConnectionContext connectionContext = new ConnectionContext();
public static ConnectionContext getInstance() {
return connectionContext;
}
private ThreadLocal<Connection> connectionThreadLocal = new ThreadLocal<>();
public Connection getConnection() {
return connectionThreadLocal.get();
}
public void bind(Connection conn) {
connectionThreadLocal.set(conn);
}
public void remove() {
Connection connection = connectionThreadLocal.get();
if (connection != null) {
connectionThreadLocal.remove();
}
}
}
MainFrame.java
public class MainFrame extends HttpServlet {
private static MainService mainService = new MainService();
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
int money = Integer.parseInt(req.getParameter("money"));
try {
boolean transfer = mainService.transfer(1, 2, money);
if (transfer)
resp.getWriter().write("转账成功!");
else
throw new ServletException("余额不足!");
} catch (SQLException e) {
throw new ServletException(e.getMessage());
}
}
}
MainService.java
public class MainService {
private static MainDao mainDao = new MainDao();
public boolean transfer(int srcId, int destId, int money) throws SQLException {
int select = mainDao.select(srcId);
if (select >= money) {
int i = mainDao.update(srcId, -money);
int j = mainDao.update(destId, money);
return i==j;
} else {
// 余额不足
return false;
}
}
}
MainDao.java
public class MainDao {
/**
* 查询账户金额
* @param accountID
* @return
*/
public int select(int accountID) throws SQLException {
String sql = "select balance from account where id=?";
Connection connection = ConnectionContext.getInstance().getConnection();
System.out.println("select:current conn->" + connection);
PreparedStatement ps = connection.prepareStatement(sql);
ps.setInt(1, accountID);
ResultSet rs = ps.executeQuery();
if (rs.next())
return rs.getInt(1);
return 0;
}
/**
* 更新账户金额
* @param accountID
* @param money
* @return
*/
public int update(int accountID, int money) throws SQLException {
String sql = "update account set balance = balance + ? where id=?";
Connection connection = ConnectionContext.getInstance().getConnection();
System.out.println("update:current conn->" + connection);
PreparedStatement ps = connection.prepareStatement(sql);
ps.setInt(1, money);
ps.setInt(2, accountID);
int effectNums = ps.executeUpdate();
return effectNums;
}
}
将dao底层的异常向上抛出到service层,service继续抛给UI,UI继续抛出,最终异常在TransactionFilter类的doFilter方法中得到处理。
Filter
Filter称作过滤器,其基本功能就是对Servlet容器调用Servlet的过程进行拦截,从而在Servlet进行响应处理前后实现一些特殊功能。Filter可以对请求进行预处理,对请求后的资源在进行后处理的一种拦截器。
Filter的主要应用场景有:Jsp, Servlet, 静态图片文件或静态 html 文件等进行拦截,从而实现一些特殊的功能。实现URL级别的权限访问控制、过滤敏感词汇、压缩响应信息等一些高级功能。
执行流程
tomcat启动加载web.xml文件,实例化各个Filter(web服务器启动创建Filter),调用Filter中的init方法初始化,通常一次请求到达时,servlet才会初始化,除非配置了<load-on-startup>
属性,会在容器启动后像Filter那样立即实例化,但是还是它的顺序会在Filter实例化之后的,当请求被Filter拦截之后,在执行servlet的service
方法之前执行Filter的doFilter
方法对请求进行拦截处理,方法有一个FilterChain
类型的chain参数,它控制着本次请求是否继续向下交给servlet处理,如果继续则调用FilterChain.doFilter()
对请求放行,否则拦截则不会向下传递。
Servlet、Filter 和 Listener 调用顺序、生命周期的实验分析
FilterConfig接口
用户在配置filter时,可以使用<init-param>
为filter配置一些初始化参数,当web容器实例化Filter对象,调用其init方法时,会把封装了filter初始化参数的filterConfig对象传递进来。因此开发人员在编写filter时,通过filterConfig对象的方法,就可获得:
String getFilterName():得到filter的名称。
String getInitParameter(String name): 返回在部署描述中指定名称的初始化参数的值。如果不存在返回null.
Enumeration getInitParameterNames():返回过滤器的所有初始化参数的名字的枚举集合。
public ServletContext getServletContext():返回Servlet上下文对象的引用。
Filter实现类实例:
public class TransactionFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
//write your code
}
@Override
public void destroy() {
}
}
Filter开发步骤
Filter开发分为二个步骤:
- 编写java类实现Filter接口,并实现其doFilter方法。
- 在 web.xml 文件中使用
<filter>
和<filter-mapping>
元素对编写的filter类进行注册,并设置它所能拦截的资源。
Filter映射
在web.xml文件中注册了Filter之后,还要在web.xml文件中映射Filter
<!--映射过滤器-->
<filter-mapping>
<filter-name>FilterDemo02</filter-name>
<!--“/*”表示拦截所有的请求 -->
<url-pattern>/*</url-pattern>
</filter-mapping>
<filter-mapping>
元素用于设置一个 Filter 所负责拦截的资源。一个Filter拦截的资源可通过两种方式来指定:Servlet 名称和资源访问的请求路径
<filter-name>
子元素用于设置filter的注册名称。该值必须是在<filter>
元素中声明过的过滤器的名字
<url-pattern>
设置 filter 所拦截的请求路径(过滤器关联的URL样式)
<dispatcher>
指定过滤器所拦截的资源被 Servlet 容器调用的方式,可以是REQUEST,INCLUDE,FORWARD和ERROR之一,默认REQUEST。用户可以设置多个<dispatcher>
子元素用来指定 Filter 对资源的多种调用方式进行拦截。如下:
<filter-mapping>
<filter-name>testFilter</filter-name>
<url-pattern>/index.jsp</url-pattern>
<dispatcher>REQUEST</dispatcher>
<dispatcher>FORWARD</dispatcher>
</filter-mapping>
<dispatcher>
子元素可以设置的值及其意义:
- REQUEST:当用户直接访问页面时,Web容器将会调用过滤器。如果目标资源是通过RequestDispatcher的include()或forward()方法访问时,那么该过滤器就不会被调用。
- INCLUDE:如果目标资源是通过RequestDispatcher的include()方法访问时,那么该过滤器将被调用。除此之外,该过滤器不会被调用。
- FORWARD:如果目标资源是通过RequestDispatcher的forward()方法访问时,那么该过滤器将被调用,除此之外,该过滤器不会被调用。
- ERROR:如果目标资源是通过声明式异常处理机制调用时,那么该过滤器将被调用。除此之外,过滤器不会被调用。
Filter链
Filter链:同一个URL有多个Filter进行拦截,这些Filter就会组成一个过滤器链。
分界线chain.doFilter()
之前可以对请求实现预处理,之后对响应进行处理
Listener
监听器概念
监听器是一个专门用于对其他对象身上发生的事件或状态改变进行监听和相应处理的对象,当被监视的对象发生情况时,立即采取相应的行动。监听器其实就是一个实现特定接口的普通java程序,这个程序专门用于监听另一个java对象的方法调用或属性改变,当被监听对象发生上述事件后,监听器某个方法立即被执行。
java的事件监听机制
1、事件监听涉及到三个组件:事件源、事件对象、事件监听器
2、当事件源上发生某一个动作时,它会调用事件监听器的一个方法,并在调用该方法时把事件对象传递进去,
开发人员在监听器中通过事件对象,就可以拿到事件源,从而对事件源进行操作。
如何设计一个可以被别的对象监听的对象:围绕事件源、事件对象、事件监听器去做
实例
/**
* Created by WuJiXian on 2020/10/25 16:03
* 被监听的对象:
* 1.类内保留一个监听器接口的引用
* 2.在具体的方法调用监听器
* 监听器接口的实现留给外部实现,这样可以回调到外部的实现
* Mouse mouse = new Mouse();
* mouse.addListener(new ClickListener() {
* @Override
* public void clickLeft(Event event) {
*
* }
*
* @Override
* public void clickRight(Event event) {
*
* }
* });
*/
public class Mouse {
private ClickListener listener;
public void addListener(ClickListener listener) {
this.listener = listener;
}
public void leftClick() {
listener.clickLeft(new Event(this));
}
public void rightClick() {
listener.clickRight(new Event(this));
}
public void doubleClick() {
}
}
class Event {
private Mouse mouse;
public Event(Mouse mouse) {
this.mouse = mouse;
}
}
监听器接口
public interface ClickListener {
void clickLeft(Event event);
void clickRight(Event event);
}
基本概念
JavaWeb中的监听器是Servlet规范中定义的一种特殊类,它用于监听web应用程序中的ServletContext, HttpSession和 ServletRequest等域对象的创建与销毁事件,以及监听这些域对象中的属性发生修改的事件。
Servlet监听器的分类
在Servlet规范中定义了多种类型的监听器,它们用于监听的事件源分别为ServletContext,HttpSession和ServletRequest这三个域对象
Servlet规范针对这三个对象上的操作,又把多种类型的监听器划分为三种类型:
- 监听域对象自身的创建和销毁的事件监听器。
- 监听域对象中的属性的增加和删除的事件监听器。
- 监听绑定到HttpSession域中的某个对象的状态的事件监听器。
自定义Session扫描器
当一个Web应用创建的Session很多时,为了避免Session占用太多的内存,我们可以选择手动将这些内存中的session销毁,那么此时也可以借助监听器技术来实现。
public class SessionScanerListener implements HttpSessionListener,ServletContextListener {
/**
* @Field: list
* 定义一个集合存储服务器创建的HttpSession
* LinkedList不是一个线程安全的集合
*/
/**
* private List<HttpSession> list = new LinkedList<HttpSession>();
* 这样写涉及到线程安全问题,SessionScanerListener对象在内存中只有一个
* sessionCreated可能会被多个人同时调用,
* 当有多个人并发访问站点时,服务器同时为这些并发访问的人创建session
* 那么sessionCreated方法在某一时刻内会被几个线程同时调用,几个线程并发调用sessionCreated方法
* sessionCreated方法的内部处理是往一个集合中添加创建好的session,那么在加session的时候就会
* 涉及到几个Session同时抢夺集合中一个位置的情况,所以往集合中添加session时,一定要保证集合是线程安全的才行
* 如何把一个集合做成线程安全的集合呢?
* 可以使用使用 Collections.synchronizedList(List<T> list)方法将不是线程安全的list集合包装线程安全的list集合
*/
//使用 Collections.synchronizedList(List<T> list)方法将LinkedList包装成一个线程安全的集合
private List<HttpSession> list = Collections.synchronizedList(new LinkedList<HttpSession>());
//定义一个对象,让这个对象充当一把锁,用这把锁来保证往list集合添加的新的session和遍历list集合中的session这两个操作达到同步
private Object lock = new Object();
@Override
public void sessionCreated(HttpSessionEvent se) {
System.out.println("session被创建了!!");
HttpSession session = se.getSession();
synchronized (lock){
/**
*将该操作加锁进行锁定,当有一个thread-1(线程1)在调用这段代码时,会先拿到lock这把锁,然后往集合中添加session,
*在添加session的这个过程中假设有另外一个thread-2(线程2)来访问了,thread-2可能是执行定时器任务的,
*当thread-2要调用run方法遍历list集合中的session时,结果发现遍历list集合中的session的那段代码被锁住了,
*而这把锁正在被往集合中添加session的那个thread-1占用着,因此thread-2只能等待thread-1操作完成之后才能够进行操作
*当thread-1添加完session之后,就把lock放开了,此时thread-2拿到lock,就可以执行遍历list集合中的session的那段代码了
*通过这把锁就保证了往集合中添加session和变量集合中的session这两步操作不能同时进行,必须按照先来后到的顺序来进行。
*/
list.add(session);
}
}
@Override
public void sessionDestroyed(HttpSessionEvent se) {
System.out.println("session被销毁了了!!");
}
/* Web应用启动时触发这个事件
* @see javax.servlet.ServletContextListener#contextInitialized(javax.servlet.ServletContextEvent)
*/
@Override
public void contextInitialized(ServletContextEvent sce) {
System.out.println("web应用初始化");
//创建定时器
Timer timer = new Timer();
//每隔30秒就定时执行任务
timer.schedule(new MyTask(list,lock), 0, 1000*30);
}
@Override
public void contextDestroyed(ServletContextEvent sce) {
System.out.println("web应用关闭");
}
}
/**
* @ClassName: MyTask
* @Description:定时器要定时执行的任务
* @author: 孤傲苍狼
* @date: 2014-9-11 上午12:02:36
*
*/
class MyTask extends TimerTask {
//存储HttpSession的list集合
private List<HttpSession> list;
//存储传递过来的锁
private Object lock;
public MyTask(List<HttpSession> list,Object lock){
this.list = list;
this.lock = lock;
}
/* run方法指明了任务要做的事情
* @see java.util.TimerTask#run()
*/
@Override
public void run() {
//将该操作加锁进行锁定
synchronized (lock) {
System.out.println("定时器执行!!");
ListIterator<HttpSession> it = list.listIterator();
/**
* 迭代list集合中的session,在迭代list集合中的session的过程中可能有别的用户来访问,
* 用户一访问,服务器就会为该用户创建一个session,此时就会调用sessionCreated往list集合中添加新的session,
* 然而定时器在定时执行扫描遍历list集合中的session时是无法知道正在遍历的list集合又添加的新的session进来了,
* 这样就导致了往list集合添加的新的session和遍历list集合中的session这两个操作无法达到同步
* 那么解决的办法就是把"list.add(session)和while(it.hasNext()){//迭代list集合}"这两段代码做成同步,
* 保证当有一个线程在访问"list.add(session)"这段代码时,另一个线程就不能访问"while(it.hasNext()){//迭代list集合}"这段代码
* 为了能够将这两段不相干的代码做成同步,只能定义一把锁(Object lock),然后给这两步操作加上同一把锁,
* 用这把锁来保证往list集合添加的新的session和遍历list集合中的session这两个操作达到同步
* 当在执行往list集合添加的新的session操作时,就必须等添加完成之后才能够对list集合进行迭代操作,
* 当在执行对list集合进行迭代操作时,那么必须等到迭代操作结束之后才能够往往list集合添加的新的session
*/
while(it.hasNext()){
HttpSession session = (HttpSession) it.next();
/**
* 如果当前时间-session的最后访问时间>1000*15(15秒)
* session.getLastAccessedTime()获取session的最后访问时间
*/
if(System.currentTimeMillis()-session.getLastAccessedTime()>1000*30){
//手动销毁session
session.invalidate();
//移除集合中已经被销毁的session
it.remove();
}
}
}
}
}