[JavaWeb]_[中级]_[Tomcat在关闭时报指向Jfinal框架的错误]

Posted infoworld

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[JavaWeb]_[中级]_[Tomcat在关闭时报指向Jfinal框架的错误]相关的知识,希望对你有一定的参考价值。

场景

  1. 在部署jfinal程序到tomcat的时候,使用startup.bat启动网站,之后用shutdown.bat关闭网站,发现命令行窗口无法终止退出。报以下错误, 怎么处理?
06-Mar-2023 11:09:24.534 严重 [localhost-startStop-2] org.apache.catalina.loader
.WebappClassLoaderBase.checkThreadLocalMapForLeaks web应用程序[ROOT]创建了一个Th
readLocal,其键类型为[com.jfinal.template.io.WriterBuffer$1](值为[com.jfinal.te
mplate.io.WriterBuffer$1@36d6d87d]),值类型为[com.jfinal.template.io.ByteWriter
](值为[com.jfinal.template.io.ByteWriter@24e46eae),但在停止web应用程序时未能
将其删除。线程将随着时间的推移而更新,以尝试避免可能的内存泄漏
09-Mar-2023 08:49:59.960 WARNING [localhost-startStop-2] org.apache.catalina.loader.WebappClassLoaderBase.clearReferencesThreads The web application [ROOT] appears to have started a thread named [CaptchaCache] but has failed to stop it. This is very likely to create a memory leak. Stack traceof thread:

说明

问题1.

  1. 第一个错误是jfinal里使用了WriterBuffer类,这个类用于缓存输出对象。 看它的源码,它创建了一个ThreadLocal线程本地变量,这个变量的特点有:
    • 变量byteWriters只有它所属的线程可以访问到它的值,也就是ByteWriter, 目的是为了在线程范围内重用ByteWriter缓存。

    • 注意: 有多少个线程调用getByteWriter()就有多少个ByteWriter实例存储在每个线程里。即使tomcat关闭,线程里仍然有这些实例的引用,因此这些实例就不会被释放。

    • 解决办法就是不使用这个线程本地变量,实现一个WriteBuffer的子类。

    • 注意: 这个线程本地变量重用缓存部分一般在服务器端控制大文件输出时才会用到, 输出html并不会重用,因此这个线程本地变量可以去掉。

WriterBuffer.java

package com.jfinal.template.io;

/**
 * WriterBuffer
 */
public class WriterBuffer 
	
	private static final int MIN_BUFFER_SIZE = 64;					// 缓冲区最小 64 字节
	private static final int MAX_BUFFER_SIZE = 1024 * 1024 * 2;		// 缓冲区最大 2M 字节
	
	private int bufferSize = 1024;									// 缓冲区大小
	private int reentrantBufferSize = 128;							// 可重入缓冲区大小
	
	private EncoderFactory encoderFactory = new EncoderFactory();
	
	private final ThreadLocal<ByteWriter> byteWriters = new ThreadLocal<ByteWriter>() 
		protected ByteWriter initialValue() 
			return new ByteWriter(encoderFactory.getEncoder(), bufferSize);
		
	;
	
	...
	
	public ByteWriter getByteWriter(java.io.OutputStream outputStream) 
		ByteWriter ret = byteWriters.get();
		if (ret.isInUse()) 
			ret = new ByteWriter(encoderFactory.getEncoder(), reentrantBufferSize);
		
		return ret.init(outputStream);
	
	
	...

MyWriterBuffer.java

import com.jfinal.template.io.*;

public class MyWriterBuffer extends WriterBuffer 

    private int bufferSize = 1024;									// 缓冲区大小

    private int reentrantBufferSize = 128;							// 可重入缓冲区大小
    protected EncoderFactory encoderFactory = new EncoderFactory();

    protected final ThreadLocal<ByteWriter> byteWriters = new ThreadLocal<ByteWriter>() 
        protected ByteWriter initialValue() 
            return new ByteWriter(encoderFactory.getEncoder(), bufferSize);
        
    ;

    public ByteWriter getByteWriter(java.io.OutputStream outputStream) 
        ByteWriter ret = new ByteWriter(encoderFactory.getEncoder(), reentrantBufferSize);
        return ret.init(outputStream);
    

    public void setEncoderFactory(EncoderFactory encoderFactory) 
        if (encoderFactory == null) 
            throw new IllegalArgumentException("encoderFactory can not be null");
        
        this.encoderFactory = encoderFactory;
    


问题2

  1. 使用了jfinal自带的验证码类缓存CaptchaCache就会报这个错. 它是通过CaptchaManager.getCaptchaCache()获取的。
    • 报错提示说它创建了一个线程,查看它的源码,发现它在内部创建了一个定时器Timer,而Timer就是一个线程。

    • 这个定时器不是一次性的,不会自己关闭,所以还需要手动关闭这个定时器。

    • timer属性是private的,因此也只能通过反射获取这个属性值。

    • 可以通过子类化重载removeAll方法来获取并关闭timer

CaptchaCache.java

public class CaptchaCache implements ICaptchaCache 
	
	private ConcurrentHashMap<String, Captcha> map = new ConcurrentHashMap<String, Captcha>();
	private int interval = 90 * 1000;	// timer 调度间隔为 90 秒
	private Timer timer;
	
	public CaptchaCache() 
		autoRemoveExpiredCaptcha();
	
	
	/**
	 * 定期移除过期的验证码
	 */
	private void autoRemoveExpiredCaptcha() 
		timer = new Timer("CaptchaCache", true);
		timer.schedule(
			new TimerTask() 
				public void run() 
					for (Entry<String, Captcha> e : map.entrySet()) 
						if (e.getValue().isExpired()) 
							map.remove(e.getKey());
						
					
				
			,
			interval,
			interval
		);
	
	...

MyCaptchaCache.java

import com.jfinal.captcha.CaptchaCache;

import java.lang.reflect.Field;
import java.util.Timer;

public class MyCaptchaCache extends CaptchaCache 

    public void removeAll() 
        super.removeAll();

        Field f= null;
        try 
            f = getClass().getSuperclass().getDeclaredField("timer");
            f.setAccessible(true);

            Timer timer = (Timer) f.get(this);
            if(timer != null)
                timer.cancel();
         catch (NoSuchFieldException e) 
            e.printStackTrace();
         catch (IllegalAccessException e) 
            e.printStackTrace();
        catch (Exception e)
            e.printStackTrace();
        
    


如何使用?

  1. 可以在配置类JFinalConfig的子类DemoConfig里使用,用两个新类的实例替换默认的实例。
MyWriterBuffer writerBuffer = new MyWriterBuffer();
MyCaptchaCache captchaCache = new MyCaptchaCache();


public void configEngine(Engine me) 
		...
		me.getEngineConfig().setWriterBuffer(writerBuffer);


public void configConstant(Constants me) 
		...
		me.setCaptchaCache(captchaCache);


@Override
public void onStop() 
	// 异步任务线程池
	//ThreadUtils.get().getExecutor().shutdown();

	// 数据库连接池
	//DemoConfig.stopDruid();

	//BUG: 1. 貌似Jfinal没有释放JDBCDriver.
	//clearJDBCDriver();

	CaptchaManager.me().getCaptchaCache().removeAll();

	// 日志服务
	//LogManager.shutdown();

其他

  1. 注意: 如果还有其他定时任务,线程池,Druid数据库连接池或其他线程本地变量也应该在onStop()方法清除掉。

  2. linux上, 可能会使用ubic stop tomcat8工具来关闭tomcat。这时候如果不处理好这些内存泄露的问题会关闭不了,造成网站已关闭的假象,导致如果再次ubic start tomcat8启动网站会报端口占用错误。

参考

  1. 开发JavaWeb网站精讲-基于JFinal框架

  2. JFinal 文档、资料、学习、API,引擎配置

[JavaWeb]_[中级]_[在jfinal里如何启用异步支持-异步模式]

场景

  1. JavaWebServlet 3.0规范已经支持异步ServletRequest, 有这个异步请求,容器(
    Tomcat)线程池只需要少量的线程即可处理大量的并发请求,因为处理请求的线程可以把异步AsyncContext交给业务线程池处理之后立即返回。Node.js的异步IO应该也是用的这种技术,用单线程处理异步请求IO。那么在使用Jfinal框架时如何引入异步ServletRequest?

说明

  1. JavaWeb的常规容器处理用户请求都是通过容器内定义的线程池进行处理。当所有线程处理的业务耗时比较长的时候,比如2秒,线程池里没有空余的线程响应用户的请求,就会造成容器阻塞,丢失接下来的所有用户请求。如果用异步IO就没这种情况,只要线程池的线程提交到业务线程足够快,那么就能一直处理用户请求数据,不会丢失用户请求。而业务线程使用完之后,新提交的任务会进入等待,直到内存耗尽才有可能丢失用户请求。

  2. 要使用异步Request,那么需要先配置部署描述符web.xml或者代码配置Filter的异步支持。添加<async-supported>元素值为true

    <?xml version="1.0" encoding="UTF-8"?>
    <web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee web-app_3_0.xsd"
             version="3.0">
        <filter>
            <filter-name>jfinal</filter-name>
            <filter-class>com.jfinal.core.JFinalFilter</filter-class>
            <init-param>
                <param-name>configClass</param-name>
                <param-value>com.demo.common.DemoConfig</param-value>
            </init-param>
            <async-supported>true</async-supported>
        </filter>
    
        <filter-mapping>
            <filter-name>jfinal</filter-name>
            <url-pattern>/*</url-pattern>
        </filter-mapping>
    </web-app>
    
    
  3. 如果使用undertow,那么通过编码在UndertowServer的子类重载configJFinalFilter加入异步支持。

    @Override
    protected void configJFinalFilter() 
        FilterInfo filterInfo = Servlets.filter("jfinal", getJFinalFilter());
        filterInfo.setAsyncSupported(true);
        filterInfo.addInitParam("configClass", config.getJFinalConfig());
        filterInfo.addInitParam("staticResourcePostfix","jpg;webp;gif;png;js;css;map;ttf;");
        deploymentInfo.addFilter(filterInfo).addFilterUrlMapping("jfinal", "/*", DispatcherType.REQUEST);
    
    
  4. 如果不加入这个异步支持的配置,会报以下错误:

    UT010026: Async is not supported for this request, as not all filters or Servlets were marked as supporting async
    
  5. 在获取异步IO,首先就要调用Servlet.startAsync得到一个AsyncContext异步上下文,他表示把请求放入异步模式。在将此请求放入异步模式后,在出站方向调用的任何过滤器都可以使用(AsyncContext.hasOriginalRequestAndResponse(), 一直返回true)这一点作为一个指示,即它们在入站调用期间添加的任何请求和响应包装器都不需要在异步操作期间存在,因此它们的任何关联资源都可以被释放。

  6. 通过asyncContext.getRequest()asyncContext.getResponse()来获取原始的ServletRequestServletResponse对象。比如获取参数request.getParameter()或者获取输出流response.getOutputStream().

  7. 在使用jfinal的时候,如果在Controller开始异步模式,需要执行renderNull();来执行一个空渲染,就是不调用response操作,避免response关闭。

  8. 在异步线程执行完之后任务之后,需要调用AsyncContext.complete()来关闭异步模式,这时候response会被关闭。 注意,异步模式启用关闭有可能会抛出异常,最好把它放在异常处理代码里。可以封装一个Controller子类来处理异步模式的启用和完成。

例子

  1. 这个例子是使用异步模式来删除博客文章。关于jfinal的博客例子项目教程参考学院课程.开发JavaWeb网站精讲-基于JFinal框架[5],这里有很多常用功能的例子. 当然也可以去jfinal官网下载原始的例子项目[6]

BaseController.java

package com.demo.common;

import com.jfinal.core.Controller;
import com.jfinal.kit.LogKit;

import javax.servlet.AsyncContext;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class BaseController extends Controller 

    protected AsyncContext asyncContext = null;

    protected AsyncContext startAsync()
        try 
            asyncContext = getRequest().startAsync();
            asyncContext.addListener(new MyAsyncListener());
            setHttpServletRequest((HttpServletRequest) asyncContext.getRequest());
            setHttpServletResponse((HttpServletResponse) asyncContext.getResponse());

            renderNull();
        catch (Exception e)
            if(asyncContext != null) 
                asyncContext.complete();
                asyncContext = null;
            
            LogKit.error(e.getMessage());
        
        return asyncContext;
    

    protected void completeAsync()
        if(asyncContext == null)
            return;

        try
            asyncContext.complete();
        catch (Exception e)
            LogKit.error(e.getMessage());
        
    

    public void render()
        if(asyncContext == null)
            return;

        try 
            getRender().setContext((HttpServletRequest) asyncContext.getRequest(),
                    (HttpServletResponse) asyncContext.getResponse()).render();
        catch (Exception e)
            LogKit.error(e.getMessage());
        
    


BlogController.java


public class BlogController extends BaseController 

...
public void delete() 

	startAsync();
	ThreadPoolKit.execute(()->
		service.deleteById(getParaToInt());
		redirect("/blog");
		render();
		completeAsync();
	);

DemoConfig

public void configConstant(Constants me) 

int processors = Runtime.getRuntime().availableProcessors();
ThreadPoolKit.init(processors);

参考

  1. 关于servlet3.0中的异步servlet

  2. jfinal如何配置异步的context

  3. Async 拦截器

  4. ServletRequest

  5. 开发JavaWeb网站精讲-基于JFinal框架-1

  6. JFinal 极速开发

以上是关于[JavaWeb]_[中级]_[Tomcat在关闭时报指向Jfinal框架的错误]的主要内容,如果未能解决你的问题,请参考以下文章

[JavaWeb]_[中级]_[在jfinal里如何启用异步支持-异步模式]

[JavaWeb]_[中级]_[在jfinal里如何启用异步请求-异步模式]

[JavaWeb]_[中级]_[在jfinal里如何启用异步请求-异步模式]

JSP_1_JavaWeb

得到JavaWeb项目在Tomcat中的运行路径

[JavaWeb]_[初级]_[使用IntelliJ IDEA进行远程Tomcat调试]