Tomcat7 使用 Scheduler

Java EE Server 上寫一個排程工作,只要加一個 @Schedule 就可以了,Tomcat 除了用 Java SE 的 TimerTask 來排程外的另一個選擇就是 quartz,看了一下有支援 Thread Pool,設定檔不用寫在程式碼上,之後要寫 ant 來置換比較方便,就決定用 quartz。

使用版本

Tomcat7 + quartz 2.1.7

步驟

http://www.quartz-scheduler.org/documentation/quartz-2.1.x/quick-start 是一個官網的簡單使用說明,下面是我自己使用的筆記。

1) 安裝

官網下載,解壓縮後 lib 下的 jar 檔都加入專案的 build path。

2) 啟用 quartz

web.xml 加入 QuartzInitializerListener,QuartzInitializerListener 實作了 javax.servlet.ServletContextListener,QuartzInitializerListener 在 Web App 啟動後就會初始化 quartz 相關的設定。

WebContent/WEB-INF/web.xml
<servlet>
<servlet-name>QuartzInitializer</servlet-name>
<servlet-class>org.quartz.ee.servlet.QuartzInitializerServlet</servlet-class>
<init-param>
<param-name>shutdown-on-unload</param-name>
<param-value>true</param-value>
</init-param>
<init-param>
<param-name>wait-on-shutdown</param-name>
<param-value>true</param-value>
</init-param>
<init-param>
<param-name>start-scheduler-on-load</param-name>
<param-value>true</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>


3) 設定 quartz

WebContent/WEB-INF/classes/quartz.properties
org.quartz.threadPool.threadCount=2 # 用2個 Thread
org.quartz.plugin.jobInitializer.class=org.quartz.plugins.xml.XMLSchedulingDataProcessorPlugin # 用 XML(quartz_data.xml) 設定排程工作

4) 設定排程工作

quartz_data.xml 內,
  • 建立一個 Job,取名並指定實作類別。
  • 建立一個 Trigger,取名,指定 Job 和執行時間。
關於 Trigger 的執行時間設定可參考:http://www.quartz-scheduler.org/documentation/quartz-2.x/tutorials/crontrigger

WebContent/WEB-INF/classes/quartz_data.xml
<?xml version="1.0" encoding="UTF-8"?>
<job-scheduling-data
xmlns="http://www.quartz-scheduler.org/xml/JobSchedulingData"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.quartz-scheduler.org/xml/JobSchedulingData http://www.quartz-scheduler.org/xml/job_scheduling_data_2_0.xsd"
version="1.8">
<schedule>
<job>
<name>FooJob</name>
<job-class>com.tangblack.quartz.example.FooJob</job-class>
</job>
<!-- cron-expression: Seconds Minutes Hours DayOfMonth Month DayOfWeek Year -->
<trigger>
<cron>
<name>FileJobTrigger</name>
<job-name>FooJob</job-name>
<cron-expression>0 0/1 * * * ? *</cron-expression>
</cron>
</trigger>
</schedule>
</job-scheduling-data>

5) 實作排程工作

FooJob.java
public class FooJob implements Job
{
  @Override
  public void execute(final JobExecutionContext ctx) throws JobExecutionException 
  {
    // To something here!
    }
}

問題

1) 執行時發生 java.lang.NoClassDefFoundError: javax/transaction/UserTransaction

嚴重: Exception sending context initialized event to listener instance of class org.quartz.ee.servlet.QuartzInitializerListener

java.lang.NoClassDefFoundError: javax/transaction/UserTransaction

下載 jta-1.1.jar 後,加入專案的 build path。


2) Tomcat7 關閉 Web App 出現 memory leak 警告。

[DefaultQuartzScheduler_Worker-5] but has failed to stop it. This is very likely to create a memory leak

這是因為 Tomcat7 關閉 Web App 時,沒有正確關閉 quartz 的關係,在QuartzInitializerListener 加入下列初始參數。

WebContent/WEB-INF/web.xml
<servlet>
<servlet-name>QuartzInitializer</servlet-name>
<servlet-class>org.quartz.ee.servlet.QuartzInitializerServlet</servlet-class>
<init-param>
<param-name>shutdown-on-unload</param-name>
<param-value>true</param-value>
</init-param>
<init-param>
<param-name>wait-on-shutdown</param-name>
<param-value>true</param-value>
</init-param>
<init-param>
<param-name>start-scheduler-on-load</param-name>
<param-value>true</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>

參考

Tomcat7 使用 Logging

專案有需要產生 Log 檔案的需求,以便未來服務發生錯誤有個記錄,所以又要看看 Tomcat7 是如何產生 Log。Tomcat7 預設使用 java.util.logging,目前開發的功能很小,就不打算要另外安裝 log4j,才不會每台機器要裝一次 log4j,PG 兼 SA 就只好挑重點做。

1) 程式碼取得 Logger

Logger logger = Logger.getLogger("com.xyz.foo");

2) 程式碼中使用 Logger

logger.info("Hi!");

3) 替專案設定 Handler

到 ${專案路徑}/WebContent/WEB-INF/classes/ 下,新增或編輯 logging.properties,加入 Handler 並設定相關資訊,是要用 Console 還是要 File,等級多少要印出來。

logging.properties

#https://www.packtpub.com/books/content/introduction-logging-tomcat-7
#http://tomcat.apache.org/tomcat-7.0-doc/logging.html
handlers = org.apache.juli.FileHandler, java.util.logging.ConsoleHandler # 有 File 和 Console 兩個 Handler
############################################################
# Handler specific properties.
# Describes specific configuration info for Handlers.
############################################################
org.apache.juli.FileHandler.level = INFO
org.apache.juli.FileHandler.directory = ${catalina.base}/logs # 產生的檔案放在 Tomcat 的 log 資料夾下
org.apache.juli.FileHandler.prefix = foo. # 檔案產生的前綴名稱,這裡就是 foo.log
java.util.logging.ConsoleHandler.level = FINE
java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter

4) 替專案設定 Logger

通過 Logger Level 的訊息才傳給上一步設定的 Handler。

經過測試,有點討厭的是,Logger 要到負責啟動專案的 JRE 上來設定才有作用。以我使用 macbook 舉例,編輯  Library/Java/JavaVirtualMachines/jdk1.7.0_51.jdk/Contents/Home/jre/lib/logging.properties,拉到最下面可以替 Logger 設定等級。

比如你專案中取得 Logger 是用

Logger.getLogger("com.xyz.foo");

那你可以在此新增

com.xyz.foo.level = FINE

這樣專案中透過 com.xyz.foo Logger,訊息的等級是 FINE 以上才會送給 Handler。而在 logging.properties 可以發現預設的 Logger Level 是 INFO。

.level = INFO

補充

JAVA Logging Level

SEVERE > WARNING > INFO > CONFIG > FINE > FINER > FINEST

Log 輸出

一條 Log 會
  • 第一關,先檢查 Logger Level 後才送到 Handler。
  • 第二關,再檢查 Handler Level 才決定是否要印在螢幕或是檔案上。
我個人習慣是
  • Handler Level 開到 FINEST(第二關永遠放行)。
  • 然後用 Logger Level 來決定是否要印在螢幕或是檔案上(第一關卡控)。
  • 如此,只要檢查 Logger Level 就能判斷會不會被印出來。

Tomcat7 使用 DataSource(MySQL)

之前比較好命有 JBoss 可以用,現在只有 Tomcat ,有些預設沒開的功能就要想辦法整合進來了,這陣子弄了些有的沒的,趕快記錄一下,免得忘記。

1) 在 Tomcat 下設定

到 $tomcat安裝路徑/conf/context.xml 新增 DataSource 的設定。

context.xml

<?xml version="1.0" encoding="UTF-8"?>
<!--
Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements. See the NOTICE file distributed with
this work for additional information regarding copyright ownership.
The ASF licenses this file to You under the Apache License, Version 2.0
(the "License"); you may not use this file except in compliance with
the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
--><!-- The contents of this file will be loaded for each web application --><Context>
<!-- Default set of monitored resources -->
<WatchedResource>WEB-INF/web.xml</WatchedResource>
<!-- Uncomment this to disable session persistence across Tomcat restarts -->
<!--
<Manager pathname="" />
-->
<!-- Uncomment this to enable Comet connection tacking (provides events
on session expiration as well as webapp lifecycle) -->
<!--
<Valve className="org.apache.catalina.valves.CometConnectionManagerValve" />
-->
<!--
Data source of MAD database.
maxActive: Connection Pool 同時所能建立的連線最大數量。若為負值,則無上限。
maxIdle: Connection Pool 同時所能允許閒置的連線最大數量。若為負值,則無上限。
maxWait: 等待可用 Connect 的 ms 數,超過時間就丟出例外。
removeAbandonedTimeout: 超過閒置秒數的連線會被移除。
dbcp config: http://commons.apache.org/proper/commons-dbcp/configuration.html
http://simon-tech.blogspot.tw/2012/02/tomcat-datasource.html
-->
<Resource name="jdbc/TestDB"
auth="Container"
type="javax.sql.DataSource"
factory="org.apache.tomcat.jdbc.pool.DataSourceFactory"
testWhileIdle="true"
testOnBorrow="true"
testOnReturn="false"
validationQuery="SELECT 1"
validationInterval="30000"
timeBetweenEvictionRunsMillis="30000"
maxActive="5"
maxIDle="4"
minIDle="4"
maxWait="10000"
initialSize="2"
removeAbandonedTimeout="300"
removeAbandoned="true"
logAbandoned="true"
minEvictableIdleTimeMillis="30000"
jmxEnabled="true"
jdbcInterceptors="org.apache.tomcat.jdbc.pool.interceptor.ConnectionState;
org.apache.tomcat.jdbc.pool.interceptor.StatementFinalizer"
username="${mysql_user}"
password="${mysql_password}"
driverClassName="com.mysql.jdbc.Driver"
url="jdbc:mysql://${mysql_server_ip}:3306/${database_name}"
connectionProperties="useUnicode=true;autoReconnect=true;characterEncoding=utf-8;"/>
</Context>

2) 記得下載 mysql-connector.jar 放到 $tomcat安裝路徑/lib 底下。

3) 重新啟動 Tomcat 就應該可以看到有無連上 MySQL 。

4) 程式碼就可以透過 JNDI 用 jdbc/TestDB (上面設定檔替 DataSource 取的名稱)拿到 DataSource。

Eclipse 整合 Tomcat 遇到問題

但是從 Eclipse 下直接啟動 Tomcat 就會發現一直拿不到 DaraSource,是因為 Eclipse 開啓 Tomcat 時,會用預設的設定檔去覆蓋 Tomcat 的設定檔造成的。

其中一個解法是,直接修改 $eclipse使用的workspace/Servers/Tomcat/context.xml,把上一段做的變動改寫過來即可。

但這樣 Tomcat 和 Eclipse 會有兩個 context.xml (Don't Repeat Yourself),而用 Eclipse 啟動又會覆寫 Tomcat 原本的 context.xml (容易讓人搞不清楚到底以哪個為準)。所以就用了下面的做法。

綁定專案設定

直接修改 $project路徑/WebContent/META-INF/context.xml,把上一段做的變動改寫過來即可。Tomcat 再載入專案時就會加上專案裡新增的 DataSource。

這樣做之後接手的人,只需要部署 war 檔,不用特別到 Tomcat 下做設定,比較符合我個人開發專案的習慣。