2018年9月13日 星期四

AP防禦實作

1.Filter
@Override
 public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
   throws IOException, ServletException {
  // TODO Auto-generated method stub
  if (loginStatusCache.getDefendStatus().equals("Y")) {
   // loginStatusCache = LoginStatusCache.getInstance(limitVisitTime,
   // measurementTimeInSeconds, blockDurationTimeInSeconds);
   String customerIP = HostAddressUtils.getRealIPAddresses((HttpServletRequest) request);
   Map<String, String> newUserInfo = null;
   Map<String, String> userInfo = null;
   if (loginStatusCache.get(customerIP) == null) {
    try {
     // lock.writeLock().lock();
     lock.lock();
     // 建立新使用者info.
     newUserInfo = createNewUserInfo(customerIP);
     userInfo = (Map<String, String>) loginStatusCache.putIfAbsent(customerIP, newUserInfo);

    } finally {
     lock.unlock();
    }
    // 此行不需加入鎖定
    checkIfBlock(userInfo == null ? newUserInfo.get("blockStatus") : userInfo.get("blockStatus"), request,
      response, chain, customerIP);
   } else {
    String blockStatus = null;
    try {
     lock.lock();
     blockStatus = loginStatusCache.addVisitTimes(customerIP);
    } finally {
     lock.unlock();
    }
    checkIfBlock(blockStatus, request, response, chain, customerIP);
   }
  } else {
   chain.doFilter(request, response);
  }

 }

 /***
  * 建立連入使用者資訊,包括登入時間、瀏覽次數、是否被鎖定等資訊
  * 
  * @return Map<String, String> userInfo
  */
 private Map<String, String> createNewUserInfo(String ip) {
  // 塞入userInfo的資訊(loginTime, visitTimes, blockStatus)
  Map<String, String> userInfo = new HashMap<String, String>();
  userInfo.put("ip", ip);
  userInfo.put("loginTime", String.valueOf(System.currentTimeMillis()));
  userInfo.put("visitTimes", "1");
  userInfo.put("blockStatus", "false");
  return userInfo;

 }

 /***
  * 依據blockStatus決定是否回應正常的頁面 "false":正常流程; "true":回應limit字串
  * 
  * @param blockStatus
  * @param request
  * @param response
  * @param chain
  * @throws IOException
  * @throws ServletException
  */
 private void checkIfBlock(String blockStatus, ServletRequest request, ServletResponse response, FilterChain chain, String ip)
   throws IOException, ServletException {
  response.setContentType("application/json;charset=utf-8");
  if (blockStatus.equalsIgnoreCase("true")) {
   PrintWriter pw = response.getWriter();
   //pw.println(String.format(new String("此IP[%s]已被锁定".getBytes("UTF-8"),"UTF-8") , ip));
   pw.println(String.format("此IP[%s]已被锁定" , ip));
   pw.flush();
   pw.close();
  } else
   chain.doFilter(request, response);

 }
2.LoginStatus
public class LoginStatusCache {
 private static Logger log = LogUtils.pay;
 private ReadWriteLock lock = new ReentrantReadWriteLock();
 private static volatile LoginStatusCache instance = null;
 private volatile Map<String, Map<String, String>> cacheMap = null; // key:登入的IP,
                  // value:
                  // Map:一分鐘內瀏覽次數,是否block狀態,第一次進入頁面時間
 private String defendStatus = "N";
 private int limitVisitTime = 0;
 private int measurementTimeInSeconds = 0; 
 private int blockDurationTimeInSeconds = 0; 
 private int reloadDefendParamsTimeInSeconds = 0; 
 private String nextReloadTime = null;

 private LoginStatusCache(int limitVisitTime, int measurementTimeInSeconds, int blockDurationTimeInSeconds,
   String defendStatus, int reloadDefendParamsTimeInSeconds) {
  this.defendStatus = defendStatus;
  this.limitVisitTime = limitVisitTime;
  this.measurementTimeInSeconds = measurementTimeInSeconds;
  this.blockDurationTimeInSeconds = blockDurationTimeInSeconds;
  this.reloadDefendParamsTimeInSeconds = reloadDefendParamsTimeInSeconds;
  

  log.debug("[LOG]defendStatus:" + defendStatus);
  log.debug("[LOG]limitVisitTime:" + limitVisitTime + " 次");
  log.debug("[LOG]measurementTimeInSeconds:" + measurementTimeInSeconds + " 秒");
  log.debug("[LOG]blockDurationTimeInSeconds:" + blockDurationTimeInSeconds + " 秒");
  log.debug("[LOG]reloadDefendParamsTimeInSeconds:" + reloadDefendParamsTimeInSeconds + " 秒");

  cacheMap = new ConcurrentHashMap<String, Map<String, String>>();
  Thread cleanerJob = new Thread(new UserInfoMapCleaner());
  cleanerJob.setDaemon(true);
  cleanerJob.start();
 }

 public static LoginStatusCache getInstance(int limitVisitTime, int measurementTimeInSeconds,
   int blockDurationTimeInSeconds, String defendStatus, int reloadDefendParamsTimeInSeconds) {
  if (instance == null) {
   synchronized (LoginStatusCache.class) {
    if (instance == null) {
     instance = new LoginStatusCache(limitVisitTime, measurementTimeInSeconds,
       blockDurationTimeInSeconds, defendStatus, reloadDefendParamsTimeInSeconds);
    }
   }
  }
  return instance;
 }

 
 public static LoginStatusCache getInstance() {
  return instance;
 }

 /***
  * 若指定的key沒有指定的value(userInfo Map),就將userInfo存入cacheMap中(ip當成key)
  * 並且回傳null,若存在則取出指定key對應的userInfo並將visitTimes累加1
  */
 public Object putIfAbsent(String ip, Map<String, String> newUserInfo) {
  @SuppressWarnings("unchecked")
  Map<String, String> userInfo = (Map<String, String>) cacheMap.putIfAbsent(ip, newUserInfo);

  if (userInfo != null) {
   this.addVisitTimes(ip);
  }
  return userInfo;
 }

 /***
  * 對傳入的ip將其一分鐘內的瀏覽次數(visitTimes)+1,若超過一分鐘內的瀏覽限制次數, 則將
  * blockStatus設成"true",並且填入開始block的時間(startBlockTime)
 
  */
 public String addVisitTimes(String ip) {
  Map<String, String> userInfo = (Map<String, String>) cacheMap.get(ip);

  if (userInfo != null) {
   if (userInfo.get("blockStatus").equalsIgnoreCase("true"))
    return userInfo.get("blockStatus");

   int visitTimes = Integer.parseInt(userInfo.get("visitTimes")) + 1;
   userInfo.put("visitTimes", String.valueOf(visitTimes));
   if (visitTimes > this.limitVisitTime) {
    userInfo.put("blockStatus", "true");
    userInfo.put("startBlockTime", String.valueOf(System.currentTimeMillis()));
   }
  }
  return userInfo.get("blockStatus");
 }

 public Map<String, String> get(String key) {
  return cacheMap.get(key);
 }

 public void put(String key, Map<String, String> value) {
  cacheMap.put(key, value);
 }

 public Map<String, String> getAndRemove(String key) {
  return cacheMap.remove(key);
 }

 public Map<String, Map<String, String>> getUserLoginCacheMap() {
  return cacheMap;
 }

 public String getDefendStatus() {
  try{
   lock.readLock().lock();
   return defendStatus;
  }finally{
   lock.readLock().unlock();
  }
  
 }
 
 public int getLimitVisitTime() {
  return limitVisitTime;
 }
 
 public int getMeasurementTimeInSeconds() {
  return measurementTimeInSeconds;
 }

 public int getBlockDurationTimeInSeconds() {
  return blockDurationTimeInSeconds;
 }

 public String getNextReloadTime() {
  try{
   lock.readLock().lock();
   return nextReloadTime;
  }finally{
   lock.readLock().unlock();
  }
  
 }

 public int getReloadDefendParamsTimeInSeconds() {
  return reloadDefendParamsTimeInSeconds;
 }

 public void setReloadDefendParamsTimeInSeconds(int reloadDefendParamsTimeInSeconds) {
  this.reloadDefendParamsTimeInSeconds = reloadDefendParamsTimeInSeconds;
 }






 class UserInfoMapCleaner implements Runnable {
  // private boolean isCheck = true;

  long checkDurationInMillis = (measurementTimeInSeconds) * 1000; // 設定job多久去檢查一次cacheMap(設定60秒,基本上至少要大於等於measurementTimeInSeconds)
  long lastLoadingParamsTime = System.currentTimeMillis(); // 進來前剛載入過參數,所以假設最近一次重載時間是現在
  long nextLoadingParamsTime = lastLoadingParamsTime + reloadDefendParamsTimeInSeconds * 1000;

  private final String DEFENSE_ENABLED = "N";
  private final int DEFAULT_DEFENSE_MAX_ACCESS_TIMES = 60; // 
  private final int DEFAULT_MEASUREMENT_TIME_IN_SECONDS = 60; // 
  private final int DEFAULT_BLOCK_DURATION_TIME_IN_SECONDS = 1 * 60; // 

  @Override
  public void run() {
   // TODO Auto-generated method stub
   FunctionExecuteTimer timer = new FunctionExecuteTimer();
   
   while (true) {
    //檢查是否該重新載入防禦參數(每10分鐘重載一次)
    checkIfReloadDefendParams();
    try {
     Thread.sleep(checkDurationInMillis);
    } catch (InterruptedException e1) {
     // TODO Auto-generated catch block
     e1.printStackTrace();
    }
    
    if (defendStatus.equals("Y")){
     FunctionExecuteTimer.setLogMessage("清除cacheMap花費時間:");
     log.debug("==============開始清除防禦快取 cacheMap==============");
     timer.start();
     Collection<Map<String, String>> tempUsers = cacheMap.values();
     log.debug("開始清除前,cacheMap Size:" + cacheMap.size());
     long now = System.currentTimeMillis();
     tempUsers.forEach(obj -> {
      Map<String, String> userInfo = (Map<String, String>) obj;
      String ip = userInfo.get("ip");
      int visitTimes = Integer.parseInt(userInfo.get("visitTimes"));
      long loginTime = Long.parseLong(userInfo.get("loginTime"));
      long duration;
      if (userInfo.get("blockStatus").equalsIgnoreCase("false")) {
       // duration:從使用者第一次登入後至目前為止,瀏覽次數不能超過limitVisitTimeInOneMinute,duration即為登入期間
       // 檢查是否有超過量測時間,visitTimes還沒超過限制次數的,將其從cacheMap移除
       if ((duration = DateUtil.secondsBetween(loginTime, now)) > measurementTimeInSeconds) {
        if (visitTimes <= limitVisitTime) {
         log.debug(String.format("%s 已經登入%s次(%s秒)還未超過瀏覽次數限制[%s]", ip, visitTimes, duration,
           limitVisitTime));
         tempUsers.remove(obj);
         log.debug(String.format("從cacheMap移除IP:%s的UserInfo,cacheMap Size:%s", ip,
           cacheMap.size()));
        } else {
         // 登入時間超過時間限制,且瀏覽次數也超過限制
         // 在addVisitTimes裡面就會判斷是否超過limitVisitTimeInOneMinute,超過就將blockStatus改成true
        }
       } else { // do nothing.
        // 在addVisitTimes裡面就會判斷是否超過limitVisitTimeInOneMinute,超過就將blockStatus改成true
       }
      } else {
       // 檢查block時間到的使用者,將其從cacheMap中移除
       long startBlockTime = Long.parseLong(userInfo.get("startBlockTime"));
       long actualBlockTime = DateUtil.secondsBetween(startBlockTime, now);
       if (actualBlockTime >= blockDurationTimeInSeconds) {
        tempUsers.remove(obj);
        log.debug(String.format("%s已經被block %s分鐘,從cacheMap移除IP:%s的UserInfo,cacheMap Size:%s",
          ip, actualBlockTime / 60, ip, cacheMap.size()));
       }
      }
     });
     log.debug(String.format("==============結束清除防禦快取 cacheMap,cacheMap Size:%s ==============", cacheMap.size()));
     timer.stop();
    }else{
     cacheMap.clear();
     log.info(String.format("防禦關閉,清除所有用戶連線記錄,cacheMap Size:%s", cacheMap.size()));
    }
   }

  }

  /***
   * 檢查是否該重新載入防禦參數(每隔10分鐘重新載入一次)
   */
  private void checkIfReloadDefendParams() {
   Date now = new Date();
   String nowTime = DateUtil.formatDate(now, "yyyy-MM-dd HH:mm:ss");
   
   if (nextReloadTime == null){
    try{
     lock.writeLock().lock();
     nextReloadTime = DateUtil.formatDate(new Date(nextLoadingParamsTime), "yyyy-MM-dd HH:mm:ss");
    }finally{
     lock.writeLock().unlock();
    }
   }
   
   log.info(String.format("目前檢查時間:%s, 預估於%s 重新載入防禦參數", nowTime, nextReloadTime));
   if (now.getTime() >= nextLoadingParamsTime) {
    try{
     lock.writeLock().lock();
     String thisReloadTime = DateUtil.formatDate(new Date(nextLoadingParamsTime), "yyyy-MM-dd HH:mm:ss");
     lastLoadingParamsTime = nextLoadingParamsTime;
     nextLoadingParamsTime = lastLoadingParamsTime + reloadDefendParamsTimeInSeconds*1000;
     nextReloadTime = DateUtil.formatDate(new Date(nextLoadingParamsTime), "yyyy-MM-dd HH:mm:ss");
     //log.info("目前時間:"+ nowTime);
     log.info(String.format("開始重新載入防禦參數, 下一次重新載入時間%s", thisReloadTime, nextReloadTime));
     Map<String, String> defendParams = OrderPageBO.getDefendsParam();
     defendStatus = StringUtils.isEmpty(defendParams.get("DEFENSE_ENABLED")) ? DEFENSE_ENABLED
       : defendParams.get("DEFENSE_ENABLED");

     log.info("重新載入防禦參數:"+ defendParams);
     
     try {
      limitVisitTime = Integer.parseInt(defendParams.get("DEFENSE_MAX_ACCESS_TIMES"));
     } catch (NumberFormatException e) {
      limitVisitTime = DEFAULT_DEFENSE_MAX_ACCESS_TIMES;
     }

     try {
      measurementTimeInSeconds = Integer.parseInt(defendParams.get("DEFENSE_MEASURE_TIME")) * 60; // 單位是分鐘
     } catch (NumberFormatException e) {
      measurementTimeInSeconds = DEFAULT_MEASUREMENT_TIME_IN_SECONDS;
     }

     try {
      blockDurationTimeInSeconds = Integer.parseInt(defendParams.get("DEFENSE_BLOCK_TIME")) * 60; // 單位是分鐘
     } catch (NumberFormatException e) {
      blockDurationTimeInSeconds = DEFAULT_BLOCK_DURATION_TIME_IN_SECONDS;
     }

     //lastLoadingParamsTime = nextLoadingParamsTime;
     
    }finally{
     lock.writeLock().unlock();
    }
    
   }/*else{
    if (nextReloadTime == null){
     try{
      lock.writeLock().lock();
      nextReloadTime = DateUtil.formatDate(new Date(nextLoadingParamsTime), "yyyy-MM-dd HH:mm:ss");
     }finally{
      lock.writeLock().unlock();
     }
    }
   }*/
  }

 }
}

2018年9月12日 星期三

EasyCache:輕量化、簡單使用的 記憶體暫存元件

EasyCache
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.gorilla.util.DateUtil;
import com.gorilla.util.cache.exception.LoadingCacheDataException;

public class EasyCache<K, V> implements Cache<K, V> {
 // private Logger logger = Logger.getLogger(this.getClass());
 private Logger logger = LoggerFactory.getLogger(this.getClass());
 private ConcurrentHashMap<K, ValueHolder<V>> cacheMap = null;
 private CacheLoader<K, V> loader;
 // the default time(2 hours) which data to live for the duration.
 private Long duration = 3600 * 1 * 1000L; 
 private Object loadingKey = new Object();
 //private Object removeKey  = new Object();
 private CacheCleaner cacheCleaner;
 private ScheduledThreadPoolExecutor scheduleExecutor = (ScheduledThreadPoolExecutor) Executors
   .newScheduledThreadPool(1);
 private final Long SCHEDULE_SLEEP_TIME_MIILLISECOND = 1 * 60 * 1000L;

 EasyCache(CacheLoader<K, V> loader, Long duration) {
  if (cacheMap == null)
   cacheMap = new ConcurrentHashMap<K, ValueHolder<V>>();

  if (duration != null && duration > 0)
   this.duration = duration;

  this.loader = loader;

  scheduleExecutor.schedule(new Runnable() {

   @Override
   public void run() {
    // TODO Auto-generated method stub
    while (true) {
     if (cacheMap.size() > 0) {
      if (cacheCleaner == null) {
       cacheCleaner = new ExpiredDurationCacheCleaner(duration);
      }
      
      Map<K, ValueHolder<V>> copyOfMap = new HashMap<K, ValueHolder<V>>(cacheMap);
      Set<Entry<K, ValueHolder<V>>> copySets = copyOfMap.entrySet();
      Iterator<Entry<K, ValueHolder<V>>> copyItors = copySets.iterator();
      List<K> removeKeys = new ArrayList<K>();
      Entry<K, ValueHolder<V>> entry;
      while (copyItors.hasNext()) {
       entry = copyItors.next();
       if (cacheCleaner.cleanExpiredValue(entry.getValue().lastAccessTimeSecond))
        removeKeys.add(entry.getKey());
      }
      if (removeKeys.size() > 0) {
       synchronized (loadingKey){
        for (K key : removeKeys) {
         cacheMap.remove(key);
         logger.info(String.format("Remove the Key:%s's data at %s", key,
           DateUtil.getInstance().getYYYYMMDDWithTimeFormat().format(new Date())));
        }
       }
       
      }
     }

     try {
      Thread.sleep(SCHEDULE_SLEEP_TIME_MIILLISECOND);
     } catch (InterruptedException e) {
      // TODO Auto-generated catch block
      logger.error("thread sleep exception, e:" + e);
     }
    }

   }

  }, 1000, TimeUnit.MILLISECONDS);
 }

 @Override
 public V get(K key) throws NullPointerException, LoadingCacheDataException {
  // TODO Auto-generated method stub
  if (cacheMap.get(key) == null) {
   synchronized (loadingKey) {
    if (cacheMap.get(key) == null) {
     try {
      V value = loader.load(key);
      ValueHolder<V> holder = new ValueHolder<V>(value);
      cacheMap.put(key, holder);
      logger.info(String.format("The Key:%s's  channelCategoryViews is loading to cache", key.toString()));
      return value;
     } catch (LoadingCacheDataException e) {
      logger.error("get key:" + key.toString() + "'s data exception:" + e);
      throw new LoadingCacheDataException(e.getMessage(), e);
     } catch (NullPointerException e) {
      logger.error("put data into cache Map exception", e);
      throw new NullPointerException(e.getMessage());
     }

    } else {
     ValueHolder<V> holder = cacheMap.get(key);
     //holder.lastAccessTimeSecond = System.currentTimeMillis();
     logger.debug(String.format("The Key:%s's new expired date is %s", key.toString(),
       this.newExpiredDate(holder.lastAccessTimeSecond)));
     return cacheMap.get(key).value;
    }
   }
  } else {
   ValueHolder<V> holder = cacheMap.get(key);
   //holder.lastAccessTimeSecond = System.currentTimeMillis();
   return holder.value;
  }
 }

 @Override
 public Boolean removeAll() {
  // TODO Auto-generated method stub
  Boolean result = true;
  try {
   if (cacheMap.size() > 0) {
    synchronized (loadingKey){
     cacheMap.clear();
    }
   }
  } catch (Exception e) {
   logger.error("clear cache error.", e);
   result = false;
  }
  return result;
 }

 private Date newExpiredDate(Long lastAccessTimeSecond) {
  Date date = new Date(this.duration + lastAccessTimeSecond);
  DateUtil.getInstance().getYYYYMMDDWithTimeFormat().format(date);
  return date;
 }

 private class ExpiredDurationCacheCleaner extends CacheCleaner {

  public ExpiredDurationCacheCleaner(Long duration) {
   super(duration);
   // TODO Auto-generated constructor stub
  }

  /*
   * param :time: the last access time result : return true if the data
   * has expired and has to be remove from cache.
   */
  @Override
  public boolean cleanExpiredValue(long time) {
   // TODO Auto-generated method stub
   boolean result = false;
   if ((System.currentTimeMillis() - time) > this.duration)
    result = true; // clean the cache data.
   else
    result = false;

   return result;
  }

 }

 protected class ValueHolder<V> {

  public Long lastAccessTimeSecond;
  public V value;

  public ValueHolder(V value) {
   this.value = value;
   this.lastAccessTimeSecond = System.currentTimeMillis();
  }

 }

 public static void main(String[] args) {
  Long newExpired = 60000L + System.currentTimeMillis();
  Date d = new Date(newExpired);
  Integer a = 100;
  String test = String.format("The Key:%s's new expired date is %s", a.toString(),
    DateUtil.getInstance().getYYYYMMDDWithTimeFormat().format(d));

  out.println(test);
  Map<String, Integer> aMap = new HashMap<String, Integer>();

 }

}

多執行序的應用-for fun

1. 產生樹狀選單搜尋
用來執行搜尋工作的thread
public class SubChannelCategoryFinder implements Runnable {
 //private Logger logger = Logger.getLogger(getClass());
 private Logger logger = LoggerFactory.getLogger(this.getClass());
 private Integer parentId;
 private ChannelCategoryDao channelCategoryDao;
 private List<ChannelCategoryView> subChannelCategoryView;
 
 

 public List<ChannelCategoryView> getSubChannelCategoryView() {
  return subChannelCategoryView;
 }

 public void setSubChannelCategoryView(List<ChannelCategoryView> subChannelCategoryView) {
  this.subChannelCategoryView = subChannelCategoryView;
 }

 public Integer getParentId() {
  return parentId;
 }

 public void setParentId(Integer parentId) {
  this.parentId = parentId;
 }

 public SubChannelCategoryFinder(ChannelCategoryDao channelCategoryDao, Integer parentId) {
  this.channelCategoryDao = channelCategoryDao;
  this.parentId = parentId;
 }
 
 @Override
 public void run() {
  // TODO Auto-generated method stub
  
  Calendar startCal = Calendar.getInstance();
  
  logger.debug("Root finder(Id="+ parentId +") start at "
    + DateUtil.getInstance().getYYYYMMDDWithMilliFormat().format(startCal.getTime()));
  
  //because the session is out of the service control, so here we have to control the 
  //session's close manually.
  Session session = channelCategoryDao.getSession();
  try{
   subChannelCategoryView = channelCategoryDao.getChannelCategory(parentId, session); 
  }finally {
   channelCategoryDao.closeSession(session);
  }
  
  //subChannelCategoryView = indexService.getSubChannelCategory(parentId);
  if (subChannelCategoryView.size() > 0) {
   List<SubChannelCategoryFinder> subFinders = new ArrayList<SubChannelCategoryFinder>();
   
   for (ChannelCategoryView view : subChannelCategoryView) {
    //SubChannelCategoryFinder finder = new SubChannelCategoryFinder(channelCategoryDao, view.getId());
    SubChannelCategoryFinder finder = new SubChannelCategoryFinder(channelCategoryDao, view.getId());
    subFinders.add(finder);
    Thread thread = new Thread(finder);
    String finderName = view.getName() + "(id=" + view.getId() + ") subCategory finder";
    thread.setName(finderName);
    thread.setDaemon(true);
    logger.debug("subFinder " + finderName + "start at "
      + DateUtil.getInstance().getYYYYMMDDWithMilliFormat().format(new Date()));
    thread.start();
    try {
     thread.join();
    } catch (InterruptedException e) {
     // TODO Auto-generated catch block
     e.printStackTrace();
    }
   }
   
   Calendar endCal = Calendar.getInstance();
   long difference = endCal.getTimeInMillis() - startCal.getTimeInMillis(); 
   logger.debug("all finder  has execute complete at "
     + DateUtil.getInstance().getYYYYMMDDWithMilliFormat().format(endCal.getTime()));
   logger.debug("Totally consume " + difference + " milli seconds");
   
   for (ChannelCategoryView view : subChannelCategoryView) {
    for (SubChannelCategoryFinder finder : subFinders) {
     if (view.getId().intValue() == finder.getParentId().intValue()
       && finder.getSubChannelCategoryView().size() > 0) {
      view.setSubChannelCategoryView(finder.getSubChannelCategoryView());
      break;
     }
    }
   }
  }
 }

}



叫用Finder產生Menu
public List<ChannelCategoryView> getChannelCategoryMenu(ChannelCategoryModel model) {
  // TODO Auto-generated method stub

  SubChannelCategoryFinder finder = new SubChannelCategoryFinder(channelCategoryDao,
    model.getChannelCategoryId());
  Thread thread = new Thread(finder);
  String finderName = "id=" + model.getChannelCategoryId() + "'s subCategory finder";
  thread.setName(finderName);
  thread.setDaemon(true);
  logger.info("RootId= " + model.getChannelCategoryId() + ", subCategory as follow:");
  thread.start();
  try {
   thread.join();
  } catch (InterruptedException e) {
   // TODO Auto-generated catch block
   e.printStackTrace();
  }
  List<ChannelCategoryView> result = new ArrayList<ChannelCategoryView>();
  result = finder.getSubChannelCategoryView();

  return result;
}

非同步Servlet3.0實作聊天室-整合FB登入 記錄

1.後端Servlet
import java.io.IOException;
import java.io.PrintWriter;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Queue;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ConcurrentMap;

import javax.servlet.AsyncContext;
import javax.servlet.AsyncEvent;
import javax.servlet.AsyncListener;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.json.JSONException;
import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.gorilla.entity.LEStreamChannelHistory;
import com.gorilla.webPortal.WebPortalConstant;
import com.gorilla.webPortal.web.api.view.object.StreamChannel;
import com.gorilla.webPortal.web.defaultPage.service.DefaultPageService;
import com.gorilla.webPortal.web.utils.ServiceLocator;
import com.gorilla.webPortal.web.utils.chat.ChatRecorder;
import com.gorilla.webPortal.web.utils.chat.CheckChatRoomStatus;
import com.gorilla.webPortal.web.utils.chat.MessageBatchSenderNormal;
import com.gorilla.webPortal.web.utils.chat.obj.ChatMessage;

@WebServlet(urlPatterns = { "/chat" }, asyncSupported = true)
public class ChatServlet extends HttpServlet {

 protected Logger logger = null;
 private static final Integer TIMEOUT = 240 * 60 * 1000; //milliSeconds

 protected static final ConcurrentMap<Integer, Queue<AsyncContext>> chatRooms = new ConcurrentHashMap<Integer, Queue<AsyncContext>>();
 private static final ConcurrentMap<Integer, Object> roomKeys = new ConcurrentHashMap<Integer, Object>();
 private static final ConcurrentMap<Integer, Thread> chatRecorderJobs = new ConcurrentHashMap<Integer, Thread>();
 private static final ConcurrentMap<Integer, ChatRecorder> chatRecorders = new ConcurrentHashMap<Integer, ChatRecorder>(); //key:直播頻道id, value:對應的 聊天室紀錄器
 private static final ConcurrentMap<Integer, Thread> messageSenderJobs = new ConcurrentHashMap<Integer, Thread>();
 private static final ConcurrentMap<Integer, MessageSender> messageSenders = new ConcurrentHashMap<Integer, MessageSender>(); //key:直播頻道id, value:用來發送訊息給該頻道的所有用戶 
 private static final ConcurrentMap<Integer, MessageBatchSenderNormal> messageBatchSenders = new ConcurrentHashMap<Integer, MessageBatchSenderNormal>(); //key:直播頻道id, value:批次顯示之前討論訊息給新登入的使用者
 
 @Override
 public void init(ServletConfig config) throws ServletException {
     super.init(config);
     this.logger = LoggerFactory.getLogger(getClass());
     
     DefaultPageService defaultPageService = ServiceLocator.getInstance().getDefaultPageService();
     

     List<StreamChannel> streamChannels = defaultPageService.getStreamChannels();
     for (StreamChannel item : streamChannels)
     {
       roomKeys.put(item.getId(), new Object());
       messageBatchSenders.put(item.getId(), new MessageBatchSenderNormal());
     }
     CheckChatRoomStatus checkChatRoomStatus = new CheckChatRoomStatus(chatRooms, chatRecorderJobs, chatRecorders, messageSenderJobs, messageSenders, messageBatchSenders);
     
     //專門用來檢查各個聊天室狀態的thread,當直播頻道被關閉或進入replay時,聊天室id對應的Queue<AsyncContext>、聊天紀錄器、須被移除
     Thread checkChatRoomStatusJob = new Thread(checkChatRoomStatus, "checkChatRoomStatusJob");
     
     checkChatRoomStatusJob.setDaemon(true);
     checkChatRoomStatusJob.start();
   }
 @Override
 protected void doGet(HttpServletRequest req, HttpServletResponse res)
   throws IOException, ServletException {
  this.doPost(req, res);
 }

 @Override
 protected void doPost(HttpServletRequest request,
   HttpServletResponse response) throws IOException, ServletException {

  DefaultPageService defaultPageService = ServiceLocator.getInstance()
    .getDefaultPageService();
  Integer streamChannelId = Integer.parseInt(request
    .getParameter("floor"));

  if (chatRooms.get(streamChannelId) == null) {
   synchronized (roomKeys.get(streamChannelId)) {
    Queue<AsyncContext> asyncContextQueue = chatRooms
      .get(streamChannelId);
    if (asyncContextQueue == null) {
     StreamChannel streamChannel = defaultPageService
       .getStreamChannel(streamChannelId, "ONLINE");
     if (streamChannel == null) {
      PrintWriter pw = response.getWriter();
      pw.write("The Stream Channel " + streamChannelId
        + "F has not started yet");
      pw.flush();
      return;
     }

     // new a specific streamChannelId's ConcurrentLinkedQueue
     asyncContextQueue = new ConcurrentLinkedQueue<AsyncContext>();
     chatRooms.put(streamChannelId, asyncContextQueue);

     // new MessageBatchSenderNormal
     MessageBatchSenderNormal messageBatchSender = new MessageBatchSenderNormal();
     messageBatchSenders
       .put(streamChannelId, messageBatchSender);

     String filePath = "";
     if (WebPortalConstant.CHATROOM_FILE_PATH
       .charAt(WebPortalConstant.CHATROOM_FILE_PATH
         .length() - 1) == '/') {
      filePath = WebPortalConstant.CHATROOM_FILE_PATH;
     } else {
      filePath = WebPortalConstant.CHATROOM_FILE_PATH + "/";
     }

     LEStreamChannelHistory leStreamChannelHistory = defaultPageService
       .getLatestStreamChannelHistory(streamChannelId,
         streamChannel.getPublishDate());

     ChatRecorder chatRecorder = new ChatRecorder(
       "D:/ContentStorage" + filePath
         + leStreamChannelHistory.getLehid() + ".txt");

     chatRecorders.put(streamChannelId, chatRecorder);
     Thread chatRecorderJob = new Thread(chatRecorder,
       "ChatRecorder[" + streamChannelId + "F]");
     chatRecorderJob.setDaemon(true);
     chatRecorderJob.start();
     chatRecorderJobs.put(streamChannelId, chatRecorderJob);

     // start a messageSender job,用來發送訊息給同一頻道中的Queue<AsyncContext>
     MessageSender messageSender = new MessageSender(
       asyncContextQueue);
     messageSenders.put(streamChannelId, messageSender);
     Thread messageSenderThread = new Thread(messageSender,
       "MessageSender[" + streamChannelId + "F]");
     messageSenderThread.setDaemon(true);
     messageSenderThread.start();
     messageSenderJobs.put(streamChannelId, messageSenderThread);

    }
   }
  }

  if ("sendMessage".equalsIgnoreCase(request.getParameter("actionType"))) {
   String message = request.getParameter("message");
   String name = request.getParameter("name");
   ChatMessage chatMessage = new ChatMessage(name, message);
   messageBatchSenders.get(streamChannelId).addMessage(chatMessage);
   messageSenders.get(streamChannelId).send(name, message);
   
   try {
    chatRecorders.get(streamChannelId).addChatMessage(chatMessage);
    // chatRecorder1F.addChatMessage(chatMessage);
   } catch (InterruptedException e) {
    // TODO Auto-generated catch block
    logger.error("add Message to chatRecorder Map occur error, error info",e);
   }
   
  } else {

   response.setContentType("text/event-stream, charset=UTF-8");
   response.setHeader("Cache-Control", "no-cache");
   response.setHeader("Connection", "keep-alive");
   response.setHeader("Access-Control-Allow-Origin", "*"); //for IE8,9 important
   // String floor = request.getParameter("floor");
   String name = request.getParameter("name");
   Boolean firstIn = Boolean.parseBoolean(request
     .getParameter("firstIn"));

   final AsyncContext ac = request.startAsync();
   ac.setTimeout(TIMEOUT);
   ac.addListener(new AsyncListener() {
    public void onComplete(AsyncEvent event) throws IOException {
     Integer streamChannelId = Integer.parseInt(ac.getRequest()
       .getParameter("floor"));
     
     chatRooms.get(streamChannelId).remove(ac);
     System.out.println(streamChannelId + " Asyncontext size:"
       + chatRooms.get(streamChannelId).size());
    }

    public void onTimeout(AsyncEvent event) throws IOException {
     Integer streamChannelId = Integer.parseInt(ac.getRequest()
       .getParameter("floor"));

     chatRooms.get(streamChannelId).remove(ac);
     System.out.println(streamChannelId + " Asyncontext size:"
       + chatRooms.get(streamChannelId).size());
    }

    public void onError(AsyncEvent event) throws IOException {
     Integer streamChannelId = Integer.parseInt(ac.getRequest()
       .getParameter("floor"));

     chatRooms.get(streamChannelId).remove(ac);
     System.out.println(streamChannelId + " Asyncontext size:"
       + chatRooms.get(streamChannelId).size());
    }

    public void onStartAsync(AsyncEvent event) throws IOException {
     // System.out.println("Asyncontext size:"+
     // asyncContextQueue.size());
    }
   });

   if (firstIn) {
    // messageBatchSender1F.addAsyncContext(ac);
    messageBatchSenders.get(streamChannelId).sendAllMessage(ac);
    // messageBatchSender1F.sendAllMessage(ac);
   }
   chatRooms.get(streamChannelId).add(ac);
   logger.info(streamChannelId + " Asyncontext size:"
     + chatRooms.get(streamChannelId).size());

  }
 }

 public class MessageSender implements Runnable {
  protected boolean running = true;
  protected ArrayList<String> messages = new ArrayList<String>(); //=>改成用 currentLinkedQueue or blockQueue
  private Queue<AsyncContext> asyncContextQueue;

  public MessageSender(Queue<AsyncContext> asyncContextQueue) {
   this.asyncContextQueue = asyncContextQueue;
  }

  public void stop() {
   running = false;
  }

  /**
   * Add message for sending.
   */
  public void send(String user, String message) {

   synchronized (messages) { //這邊就不需要synchronized
    JSONObject result = new JSONObject();

    try {
     result.put("message", message);
     result.put("name", user);
     result.put("sendTime", new Date().getTime());
    } catch (JSONException e) {
     // TODO Auto-generated catch block
     logger.error("add message occur error", e.getMessage());
    }
    messages.add(result.toString());
    messages.notify();
   }
  }

  public void run() {
   while (running) {
    PrintWriter out;

    if (messages.size() == 0) {
     try {
      synchronized (messages) {
       messages.wait();
      }
     } catch (InterruptedException e) {
      // Ignore
     }
    }
    synchronized (asyncContextQueue) {
     String[] pendingMessages = null;
     synchronized (messages) { //=>這邊也不需要了synchronized
      pendingMessages = messages.toArray(new String[messages.size()]); //直接queue.poll
      messages.clear();
     }
     for (AsyncContext ac : asyncContextQueue) {
      try {
       HttpServletRequest request = (HttpServletRequest) ac
         .getRequest();
       String ua = request.getHeader("User-Agent");
       boolean isMSIE = (ua != null && (ua.indexOf("MSIE") != -1 || ua
         .indexOf("Trident") != -1));
       PrintWriter acWriter = ac.getResponse().getWriter();
       for (int j = 0; j < pendingMessages.length; j++) {
        if (isMSIE)
         acWriter.println(URLEncoder.encode(
           pendingMessages[j], "UTF-8"));
        else
         acWriter.println("data: "
           + URLEncoder
             .encode(pendingMessages[j],
               "UTF-8") + "\n\n");
       }
       acWriter.flush();
       
       // ac.complete();
      } catch (IOException ex) {
       logger.error("out put message occur error:"
         + ex.getMessage());
       asyncContextQueue.remove(ac);
       logger.info("After error remove asyncContextQueue'size:"
         + asyncContextQueue.size());
      }
     }
    }

   }

  }

 }
}


2.前端js
joinChat: function(){
  fbName = '';
  FB.getLoginStatus(function(response) {
   if (response.status === 'connected') {
    $("#btnChatSwitch").off("click").val("離開討論");
          $("#btnChatSwitch").on("click", WP.LiveStream.leaveChat);
          $('#btnSendMessage').prop("disabled", false);
          
          $("#btnSendMessage").off("click");
          $("#btnSendMessage").on("click", WP.LiveStream.sendMessage);
          $("#inputBox").off("keyup");
          $("#inputBox").on("keyup", function(event){
            if (event.keyCode == 13 && event.shiftKey) {
             //alert('test');
             WP.LiveStream.sendMessage();
            }
          });
          
    $("#btnLogin").prop("disabled", true);
    
    
     FB.api('/me', function(response) {
            if (window.console){
             console.log('Successful login for: ' + response.name + "; userID ="+ response.id );
            }
             
            WP.LiveStream.fbName = response.name;
            
            //the receiveServerSend procedure has execute at click button #btnChatSwitch first
            if (typeof(EventSource) !== "undefined"){
             /*if (typeof(eventSource) !== "undefined")
              eventSource.close();
             else
              WP.LiveStream.receiveServerSend(response.name);*/
             //never build the comet connection
             if (WP.LiveStream.eventSource == undefined){
              WP.LiveStream.receiveServerSend(response.name, WP.LiveStream.firstIn);
                 WP.LiveStream.firstIn =false;
              
             }
             else{
              //has builed, do nothing.
             }
            }
            else{
             /*if (typeof(http) !== 'undefined' )
              http.abort();
             else
              WP.LiveStream.pollingChat(response.name);*/
             if (WP.LiveStream.http == undefined){
              WP.LiveStream.pollingChat(response.name , WP.LiveStream.firstIn);
               WP.LiveStream.firstIn = false;
             }
              
            }
          });
     
    
   }else if (response.status ==='unknown' || response.status ==='not_authorized'){
    $("#btnChatSwitch").off("click").val("離開討論");
    $("#btnChatSwitch").on("click", WP.LiveStream.leaveChat);
    $('#btnSendMessage').prop("disabled", true);
    
    $("#btnSendMessage").off("click");
    
    $("#btnLogin").prop("disabled", false);
    FB.Event.subscribe('auth.login', WP.LiveStream.joinChat); //when after login fb, call the callback "joinChat" method  
    //WP.LiveStream.receiveServerSend("", WP.LiveStream.firstIn);
    //WP.LiveStream.firstIn = false;
    //the receiveServerSend procedure has execute at click button #btnChatSwitch first
          if (typeof(EventSource) !== "undefined"){
           /*if (typeof(eventSource) !== "undefined")
            eventSource.close();
           else
            WP.LiveStream.receiveServerSend(response.name);*/
           //never build the comet connection
           if (WP.LiveStream.eventSource == undefined){
            WP.LiveStream.receiveServerSend("", WP.LiveStream.firstIn);
               WP.LiveStream.firstIn =false;
            
           }
           else{
            //has builed, do nothing.
           }
          }
          else{
           /*if (typeof(http) !== 'undefined' )
            http.abort();
           else
            WP.LiveStream.pollingChat(response.name);*/
           if (WP.LiveStream.http == undefined){
            WP.LiveStream.pollingChat("" , WP.LiveStream.firstIn);
             WP.LiveStream.firstIn = false;
           }
            
          }
   }
   WP.LiveStream.switchUISize("chatRoom");
  });
  
  
 },
 leaveChat: function(){
  if (typeof(WP.LiveStream.http) !== 'undefined' ){
   WP.LiveStream.http.abort();
   WP.LiveStream.http = undefined;
  }else if (typeof(WP.LiveStream.eventSource) !== 'undefined'){
   WP.LiveStream.eventSource.close();
   WP.LiveStream.eventSource = undefined;
  }
  WP.LiveStream.firstIn = true;
  $(".messages > li").remove();
  WP.LiveStream.switchUISize("normal");
  $("#btnChatSwitch").off("click").val("加入討論");
        $("#btnChatSwitch").on("click", WP.LiveStream.joinChat);
 },
 receiveServerSend: function(name, firstIn){
  var name = name || "";
  var url = "/"+ WP.webContext +"/chat/"; 
  var params = "name="+name+"&floor="+ WP.LiveStream.floor+"&firstIn="+ firstIn;
  if (typeof(EventSource) !== "undefined") {
        WP.LiveStream.eventSource = new EventSource(url+"?"+params);
        WP.LiveStream.eventSource.onmessage = function(event) {
         var decodeResponseText = decodeURIComponent(event.data);
         decodeResponseText = decodeResponseText.replace(/\+/g, " ");
        var result = $.parseJSON(decodeResponseText);
        renderChatMessage(result);
       
        };
     } else {
      //alert("Sorry, Server-Sent Event is not supported in your browser");
     }
  
 },
 pollingChat: function(name, firstIn){
   
   var url = "/"+ WP.webContext +"/chat/";
   var params = "name="+name+"&floor="+ WP.LiveStream.floor + "&firstIn="+ firstIn;
   var currentIndex = 0;
   //alert(params);
  setTimeout(function() {
      
   if (IE.actualVersion == "9"){ //for IE8,9
    WP.LiveStream.http = new XDomainRequest();
    //WP.LiveStream.http.timeout = WP.LiveStream.timeout;
    WP.LiveStream.http.open("get", url+"?"+params);
    WP.LiveStream.http.send();
    
    WP.LiveStream.http.onload = function(){
     //alert("come onload");
     WP.LiveStream.pollingChat(name, false);
    };
    WP.LiveStream.http.onerror = function() { 
     //alert("comet occur error"); 
    
    };
    WP.LiveStream.http.onprogress = function(){
     //alert("come progress");
      var kb = 2 * 1024;
     //var kb = 0;
         var currentResponseText = WP.LiveStream.http.responseText.substr(kb+currentIndex, WP.LiveStream.http.responseText.length);
         currentIndex = currentResponseText.length + currentIndex;
         var decodeResponseText = decodeURIComponent(currentResponseText);
         //currentIndex = decodeResponseText.length + currentIndex;
         decodeResponseText = decodeResponseText.replace(/\+/g, " ");
         var result = $.parseJSON(decodeResponseText);
         //result = result.replaceAll("+"," ");
         
         renderChatMessage(result);
        
    };    
   }else{
    WP.LiveStream.http = new XMLHttpRequest();
    WP.LiveStream.http.open("GET", url+"?"+params, true);
    WP.LiveStream.http.timeout = WP.LiveStream.timeout;
    WP.LiveStream.http.setRequestHeader("Content-type", "application/x-www-form-urlencoded; charset=UTF-8");
    
        WP.LiveStream.http.onreadystatechange = function() {//Call a function when the state changes.
        if(WP.LiveStream.http.readyState == 1 ) {
        //$(".messages").after("<li class='message'>"+http.responseText+"</li>");
        } 
        if(WP.LiveStream.http.readyState == 3 && WP.LiveStream.http.status == 200) {
            
         var currentResponseText = WP.LiveStream.http.responseText.substr(currentIndex, WP.LiveStream.http.responseText.length);
         currentIndex = currentResponseText.length + currentIndex;
         var decodeResponseText = decodeURIComponent(currentResponseText);
         //currentIndex = decodeResponseText.length + currentIndex;
         decodeResponseText = decodeResponseText.replace(/\+/g, " ");
         var result = $.parseJSON(decodeResponseText);
         //result = result.replaceAll("+"," ");
         
         renderChatMessage(result);
         
           }else if (WP.LiveStream.http.readyState == 4 && WP.LiveStream.http.status == 200 ){
           //alert(http.responseText);
           WP.LiveStream.pollingChat(name, false);
           }
          };
        WP.LiveStream.http.send();
   }
   
  },1000);
 },
 sendMessage: function(){
  $("#btnSendMessage").prop("disabled",true);
  var message = $("#inputBox").val();
   $("#inputBox").val('');
  $.ajax({
   type: "POST",
   url: "/"+ WP.webContext +"/chat/",
   data: {
    'floor'    : WP.LiveStream.floor,
    'message'  : message,
    'name'     : WP.LiveStream.fbName,
    'actionType': 'sendMessage'
    //'categoryId'  : WP.API.VODChannel.categoryId,
   },
   dataType: "html",
  }).done(function(result){
   $("#btnSendMessage").prop("disabled",false);
  });
 },