Javaweb中ThreadLocal以及使用Filter和ThreadLocal组合管理事务
一、ThreadLocal
1.什么是ThreadLocal?
1.该类提供了线程局部变量。这些变量不同于他们的普通对应物,因为访问某个变量(通过set或get方法)的每个线程都有自己的局部变量,它独立于变量的初始化副本。ThreadLocal实例通常是类中的private static字段,它们希望状态与某一个线程(例如,用户ID或事务ID相关联)。
2.每个线程都保持对其线程局部变量副本的隐式引用,只要线程是活动的则ThreadLocal实例是可以访问的;在线程小时之后,其线程局部实例的所有副本都会被垃圾回收。
2.ThreadLocal的作用?
简而言之:ThreadLocal的作用就是解决多线程的数据安全问题。它可以给当前的线程关联一个数据(可以是普通变量,可以是对象,也可以是数组,集合)
3.ThreadLocal的特点
1.ThreadLocal可以为当前线程关联一个数据。(它可以像Map一样存取数据,key为当前线程)。
2.每一个ThreadLocal对象,只能为当前线程关联一个数据,如果要为当前线程关联多个数据,就需要使用多个ThreadLocal对象实例。
3.每个ThreadLocal对象实例定义的时候,一般都是static类型。
4.ThreadLocal中保存数据,在线程销毁后。会由JVM虚拟机自动释放。
二、ThreadLocal的使用
public class ThreadLocalTest {
public final static Map<String,Object>data=new Hashtable<String,Object>();
public static ThreadLocal<Object>threadLocal=new ThreadLocal<Object>();
private static Random random=new Random();
public static class Task implements Runnable{
@Override
public void run() {
//在run方法中,随机生成一个变量(线程要关联的数据),然后以当前线程名为key保存到map中
Integer i=random.nextInt(1000);
//获取当前线程名
String name=Thread.currentThread().getName();
System.out.println("线程["+name+"]生成的随机数是:"+i);
// data.put(name,i);
//和上面的效果一样,就是为每一个线程绑定一个数据,key值自动为线程名
threadLocal.set(i);
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//属于同一个线程的实例对象
new OrderService().createOrder();
//在Run方法结束之前,以当前线程名获取数据并打印,查看是否可以取出操作
// Object o=data.get(name);
Object o=threadLocal.get();
System.out.println("在线程["+name+"]快结束时取出关联的数据是:"+o);
}
}
public static void main(String[] args) {
for (int i=0;i<3;i++){
//这里同时开启三个线程,每个线程都是new Task();
new Thread(new Task()).start();
}
}
}
public class OrderService {
public void createOrder(){
//获取当前的线程名
String name=Thread.currentThread().getName();
System.out.println("OrderService 线程["+name+"]中保存的数据是:"+ThreadLocalTest.threadLocal.get());
}
}
三、使用Filter和ThreadLocal组合管理事务
1.事务管理
Jdbc的数据事务管理
Connection conn=Jdbc.getConnection();
try{
conn.setAutoCommit(false);//设置为手动管理事务
执行一系列的jdbc操作
conn.commit();//手动提交事务
}catch(Exception e){
conn.rollback();//回滚事务
}
要确保所有操作要么成功,要么都失败,就必须要使用数据库的事务。
要确保所有操作都在一个事务中,就必须保证所有操作都使用同一个Connection连接对象。
2.如何确保所有操作都使用同一个Connection连接对象?
我们可以使用ThreadLocal对象。来确保所有操作都使用同一个Connection对象。
ThreadLocal要确保所有操作都使用同一个Connection连接对象。那么操作的前提条件是所有操作都必须在同一个线程中完成。
3.ThreadLocal的使用
Jdbc使用ThreadLocal来连接数据库
public class jdbc {
private static DruidDataSource dataSource;
//ThreadLocal对象一般为static 因为保存的数据是一个Connection类型,所以这里的泛型使用Connection
private static ThreadLocal<Connection>conns=new ThreadLocal<Connection>();
static{
try{
Properties properties=new Properties();
//读取jdbc.properties属性配置文件
InputStream inputStream= JdbcUtils.class.getClassLoader().getResourceAsStream("jdbc.properties");
//从流中加载数据
properties.load(inputStream);
//创建数据库连接池
dataSource=(DruidDataSource) DruidDataSourceFactory.createDataSource(properties);
}catch (Exception e){
e.printStackTrace();
}
}
/**
* 获取数据库连接池中的连接
* @return
*/
public static Connection getConnection(){
Connection conn=conns.get();
if(conn==null){
try {
//若连接为空,则通过数据库连接池创建一个连接
conn=dataSource.getConnection();
//将连接保存在ThreadLocal中,供后面的jdbc使用
System.out.println(conn);
conns.set(conn);
conn.setAutoCommit(false);//设置为手动管理事务
} catch (SQLException e) {
e.printStackTrace();
}
}
return conn;
}
/**
* 提交事务,并关闭释放连接
*/
public static void commitAndClose(){
Connection connection=conns.get();
if(connection!=null){//如果不等于null,说明之前使用过连接,操作过数据库
try {
connection.commit();//提交事务
} catch (SQLException e) {
e.printStackTrace();
}finally{
try {
connection.close();//关闭连接,释放资源
} catch (SQLException e) {
e.printStackTrace();
}
}
}
//一定要执行remove操作,否则就会出错。(因为Tomcat服务器底层使用了线程池技术)
conns.remove();
}
public static void rollbackAndClose(){
Connection connection=conns.get();
if(connection!=null){//如果不等于null,说明之前使用过连接,操作过数据库
try {
connection.rollback();//回滚事务
} catch (SQLException e) {
e.printStackTrace();
}finally{
try {
connection.close();//关闭连接,释放资源
} catch (SQLException e) {
e.printStackTrace();
}
}
}
//一定要执行remove操作,否则就会出错。(因为Tomcat服务器底层使用了线程池技术)
conns.remove();
}
}
4.事务的提交
- 事务的提交我们设置为手动提交conn.setAutoCommit(false);
- 当然事务提交的前提是保证所有的业务都保证能够正确执行的情况下才能提交的。
- 获取错误信息很简单,我们只需要给相应的调用代码加上try-catch就可以了,但是要在最顶层(即提交事务之前)捕获错误信息,那么内层的Dao层,Service层需要使用try-catch中的throw将错误抛出,由最顶层进行错误捕获,如果出现错误,则进行事务回滚,若是无误则提交事务。
- 所以我们需要在Servlet层中将每个Service调用加上try-catch。(因为我们一般将一个Service中的方法视为一个业务逻辑即通过一个事务完成);
5.使用Filter过滤器统一给所有的Service方法都加上try-catch。来进行实现的管理。
可以使用一个Filter一次性,统一地给所有的Service.xx()方法加上 try-catch() 来实现事务的管理。
//在Filter类中
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
try {
//先允许完成请求 若请求报错则会抛出错误,不会执行下面的事务提交
filterChain.doFilter(servletRequest,servletResponse);
//只有当一个请求完全通过之后才可以提交事务
jdbc.commitAndClose();//提交事务
} catch (Exception e) {
jdbc.rollbackAndClose();//回滚事务
e.printStackTrace();
throw new RuntimeException(e);//把异常抛给Tomcat管理展示友好的错误页面
}
}
在web.xml中配置相应拦截信息。
<filter-name>TransactionFilter</filter-name>
<filter-class>com.filter.TransactionFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>TransactionFilter</filter-name>
<!-- 表示对所有的请求都进行拦截-->
<url-pattern>/*</url-pattern>
</filter-mapping>
四、补充
- 当服务程序出现错误时,虽然内部可以捕获到错误信息,回滚了事务。但是展示给用户的界面却是不友好的,这是需要给用户展示友好的错误提示界面。
- 将所有的异常都统一交给Tomcat,让Tomcat展示友好的错误信息页面。
- 在web.xml中我们可以通过错误页面配置来进行管理。
<!-- error-page标签配置,服务器出错之后,自动跳转的页面-->
<error-page>
<!-- error-code 是错误的类型 5开头代表服务端出现错误-->
<error-code>500</error-code>
<!-- location标签表示,要跳转去的页面路径-->
<location>/pages/error/error500.jsp</location>
</error-page>
<error-page>
<!--4 开头代表客户端出现错误-->
<error-code>404</error-code>
<location>/pages/error/error404.jsp</location>
</error-page>