您正在查看: 2015年1月

关于Ehcache

Ehcache简介

Ehcache是一个纯Java的进程内缓存框架,是Hibernate中默认的CacheProvider,主要面向通用缓存,Java EE和轻量级容器。主要特性有:

  1. 简单快速
  2. 多种缓存策略,支持LRU、LFU和FIFO。
  3. 支持内存和硬盘作为缓存存储
  4. 缓存数据会在虚拟机重启的过程中写入磁盘
  5. 具有缓存和缓存管理器的侦听接口
  6. 可以通过RMI、可插入API等方式进行分布式缓存
  7. 可与spring、Hibernate、shiro等框架组件进行整合

项目中Ehcache的配置

Jeesite中Ehcache的配置文件主要在资源目录下的cache文件夹中,默认使用的是ehcache-local.xmlehcahce-hibernate-xml,前者是ehcache的配置文件,后者是针对hibernate的配置文件。下面先看一下ehcache-local.xml的内容:

<ehcache updateCheck="false" name="defaultCache">

<diskStore path="java.io.tmpdir/jeesite/ehcache/default" />

<!-- DefaultCache setting. -->
<defaultCache maxEntriesLocalHeap="100" eternal="false" timeToIdleSeconds="300" timeToLiveSeconds="600"
    overflowToDisk="true" maxEntriesLocalDisk="100000" />

<cache name="sysCache" maxElementsInMemory="100" eternal="true" overflowToDisk="true"/>

<cache name="cmsCache" maxElementsInMemory="100" eternal="true" overflowToDisk="true"/>
    
<cache name="shiro-activeSessionCache" maxElementsInMemory="100" overflowToDisk="true"
       eternal="true" timeToLiveSeconds="0" timeToIdleSeconds="0"
       diskPersistent="true" diskExpiryThreadIntervalSeconds="600"/>

<cache name="org.apache.shiro.realm.text.PropertiesRealm-0-accounts"
       maxElementsInMemory="100" eternal="true" overflowToDisk="true"/>

<cache name="SimplePageCachingFilter" maxElementsInMemory="100" eternal="false" overflowToDisk="true"
    timeToIdleSeconds="120" timeToLiveSeconds="120" memoryStoreEvictionPolicy="LFU"/>
    
</ehcache>

其中,syscache和cmscache分别是针对系统管理模块和内容管理模块的缓存。SimplePageCachingFilter是页面缓存。各缓存项的含义如下:

  • diskStore path: 缓存保存到磁盘的位置
  • maxElementsInMemory:缓存中允许创建的最大对象数
  • eternal:缓存中对象是否为永久的,如果是,超时设置将被忽略,对象从不过期。
  • timeToIdleSeconds:缓存数据的钝化时间,也就是在一个元素消亡之前,两次访问时间的最大时间间隔值,这只能在元素不是永久驻留时有效。
  • timeToLiveSeconds:缓存数据的生存时间,也就是一个元素从构建到消亡的最大时间间隔值,这只能在元素不是永久驻留时有效。
  • overflowToDisk:内存不足时,是否启用磁盘缓存。

ehcahce-hibernate-xml则是针对sys模块中的实体类通过hibernate来缓存,包括User、Role、Menu、Office、Area、Dict以及userList、MenuList、roleList等实体类。下面列举几个配置,其他的以此类推:

<cache name="com.thinkgem.jeesite.modules.sys.entity.Area" maxEntriesLocalHeap="100" eternal="false" overflowToDisk="true" maxEntriesLocalDisk="100000" />
<cache name="com.thinkgem.jeesite.modules.sys.entity.Area.childList" maxEntriesLocalHeap="100" eternal="false" overflowToDisk="true" maxEntriesLocalDisk="100000" />
<cache name="com.thinkgem.jeesite.modules.sys.entity.Area.officeList" maxEntriesLocalHeap="100" eternal="false" overflowToDisk="true" maxEntriesLocalDisk="100000" />

对于页面缓存来说,还要在Servlet容器的web.xml这个配置文件中配置:

<!-- Ehcache 页面缓存,仅缓存首页和html为后缀的页面 (需要时取消注释)
<filter>  
<filter-name>PageCacheFilter</filter-name>  
    <filter-class>com.thinkgem.jeesite.common.filter.PageCachingFilter  
</filter-class>
</filter>

<filter-mapping>  
    <filter-name>PageCacheFilter</filter-name>
    <url-pattern>/</url-pattern>
</filter-mapping>
<filter-mapping>
    <filter-name>PageCacheFilter</filter-name>
    <url-pattern>*.html</url-pattern>
</filter-mapping>-->

这里只针对站点首页和html静态页面做了缓存,对应的缓存类是common.filter包下的PageCacheingFilter类。下面就看一下项目中的缓存工具类

项目中的ehcache缓存代码。

首先要针对页面缓存作一下说明,页面缓存主要用Filter过滤器对请求的url进行过滤,如果该url在缓存中出现。那么页面数据就从缓存对象中获取,并以gzip压缩后返回。其速度是没有压缩缓存时速度的3-5倍,效率相当之高!其中页面缓存的过滤器有CachingFilter,一般要扩展filter或是自定义Filter都继承该CachingFilter。CachingFilter功能可以对HTTP响应的内容进行缓存。这种方式缓存数据的粒度比较粗,例如缓存整张页面。EHCache使用SimplePageCachingFilter类实现Filter缓存。现在看一下Jeesite中的Filter:

public class PageCachingFilter extends SimplePageCachingFilter {

    @Override
    protected CacheManager getCacheManager() {
        return CacheUtils.getCacheManager();
    }   
}

这里的页面缓存过滤器继承了ehcache提供的SimplePageCachingFilter,通过自己写的缓存工具类CacheUtils来获得缓存管理器CacheManager,下面先介绍Ehcache的基本用法,然后对应分析一下CacheUtils类:

//获取缓存管理器
CacheManager cacheManager = CacheManager.create();
cacheManager = CacheManager.getInstance();
cacheManager = CacheManager.newInstance("/config/ehcache.xml");
//通过缓存管理器获取ehcache配置文件中的一个cache
Cache sample = cacheManager.getCache("sample");
//添加数据到缓存中
Element element = new Element("key", "val");
sample.put(element);
//获取缓存中的对象,注意添加到cache中对象要序列化 实现Serializable接口
Element result = sample.get("key");
// 删除缓存
sample.remove("key");
sample.removeAll();

CahceUtils类就是对这些Ehcache的基本缓存操作做了封装:

public class CacheUtils {
//通过Spring来获取缓存管理器
private static CacheManager cacheManager = ((CacheManager)SpringContextHolder.getBean("cacheManager"));
    //对应ehcache-local.xml配置的系统模块缓存
private static final String SYS_CACHE = "sysCache";
    //以下是针对系统模块缓存的操作
public static Object get(String key) {
    return get(SYS_CACHE, key);
}
public static void put(String key, Object value) {
    put(SYS_CACHE, key, value);
}
public static void remove(String key) {
    remove(SYS_CACHE, key);
}
//封装Ehcache的get put remove操作
public static Object get(String cacheName, String key) {
    Element element = getCache(cacheName).get(key);
    return element==null?null:element.getObjectValue();
}

public static void put(String cacheName, String key, Object value) {
    Element element = new Element(key, value);
    getCache(cacheName).put(element);
}

public static void remove(String cacheName, String key) {
    getCache(cacheName).remove(key);
}

/**
 * 获得一个Cache,没有则创建一个。
 * @param cacheName
 * @return
 */
private static Cache getCache(String cacheName){
    Cache cache = cacheManager.getCache(cacheName);
    if (cache == null){
        cacheManager.addCache(cacheName);
        cache = cacheManager.getCache(cacheName);
        cache.getCacheConfiguration().setEternal(true);
    }
    return cache;
}

public static CacheManager getCacheManager() {
    return cacheManager;
}   
}

Typecho博客相关

Overview

本文记录了在使用Typecho的过程中,遇到的一些小问题以及解决方法。

1.添加置顶插件

  1. 首先去Typecho的官网插件页面http://docs.typecho.org/plugins下载Sticky插件,下载结束后,解压缩,将此文件夹放至服务器上Typecho的plugin文件夹中,具体地址为blog/usr/plugins/中。

  2. 使用超级管理员admin登陆到后天,进入管理插件页面,就可以看到所有plugins文件夹中已经有的插件。如下图:
    plugin1.png

  3. 刚把插件放到plugin文件夹中的时候,默认是在禁用的插件中,没有启用的。点击启用,就会变成启用的插件。然后点击设置, 出现下图:
    sticky1.png

    注意其中的置顶文章的cid,用来设置需要置顶的文章,文章编号的获取方式看下图:
    Stciky2.png
    我们需要将jeesite中hibernate的应用这篇文章置顶,只需要像上图一样,将网址中标记的文章ID号56放在置顶文章的cid中即可。

  4. index.php$this->title(); 前面加上 $this->sticky(); 就会变成这段html代码:

    <h2 class="title"><a href="<?php $this->permalink() ?>"><?php $this->sticky(); $this->title() ?></a></h2>。
    

    如果你使用了主题,那么要修改的就是主题文件夹下面的index.php

2.添加代码高亮插件

  1. Typecho的官网插件页面http://docs.typecho.org/plugins下载Google Code Prettify插件。
  2. 使用方法同上,不再给出详细步骤。
  3. 效果如下图:
    google_pretty_code.png

3.去掉页面中代码框的滚动条

刚用上代码高亮插件,显示效果明显提升了一个档次,还没开心多久,发现当代码段太长的时候,代码段不会完整的显示出来,而是会出现滚动条,读代码非常的不方便。按以下步骤进行修改:

  1. 进入tpyecho文件夹的主题文件夹下,blog/usr/themes/default,找到style.css,查看该页面的代码,发现以下代码:

     pre{padding:0;border:1px solid #ccc;overflow:auto;max-height:400px;}
    
  2. 很明显,当代码段太长的时候,最大显示高度为400px,超过这个长度就会出现滚动条。

  3. 直接全部删掉这句代码即可。或者只删掉max-height:400px;

  4. 完成。可能需要重启服务器。PS:在做了如下修改之后,其他人看到的就没有滚动条了,我这边还是有滚动条,最后清空了浏览器缓存,才效果正常了。

4.博客首页文章自动截断

typecho的首页默认是将文章以列表的形式展示,每篇文章都会完整展示,如果文章太长的话,就很难一次看到很多篇文章,查找起来很不方便。

修改方法:

  1. 进入tpyecho文件夹的主题文件夹下,blog/usr/themes/default,找到index.php,查看该页面的代码,发现以下代码:

     <div class="post-content" itemprop="articleBody">
         <?php $this->content('- 阅读剩余部分 -'); ?>
     </div>
    
  2. 将上诉代码完整改为以下代码:

     <div class="post-content" itemprop="articleBody">
             <p><?php $this->excerpt(200, '...'); ?><a href="<?php $this->permalink() ?>" rel="bookmark" title="详细阅读关于<?php $this->title(); ?>">阅读全>文...</a></p>
     </div>
    
  3. 完成。可能需要重启服务器。

  4. 效果如下图:
    mainBrowser.png

5.添加文章内容的扫码阅读

  1. 直接去http://forum.typecho.org/viewtopic.php?f=6&t=8253下载文件QRCode.zip。解压之后,将QRCode文件夹上传到博客的plugins目录blog/usr/plugins中就可以了。
  2. 使用管理员账号登入,点击管理插件,启用QRCode就可以了,QRCode的配置选项可以配置生成的二维码大小,默认是200,有点大,我改为了100,看起来更美观,不过也别太小,不然网址太长了显示不正确。

关于日志部分的简单介绍

Java日志体系概览

关于Java的日志API可以概括为日志记录的封装API和日志记录实现两类。前一种的典型代表是Apache Commons Logging和SLF4J,后一种的典型代表有JDK自带的日志实现(java.util.logging 包,JUL)以及著名的Log4j。日志封装API是为日志使用者提供了一种统一的接口,使用者可以根据需求来切换具体日志实现方案。

Java日志API

一般来说,日志API一般包括:

  • 记录器(Logger):日志 API 的使用者通过记录器来发出日志记录请求,并提供日志的内容。在记录日志时,需要指定日志的严重性级别。
  • 格式化器(Formatter):对记录器所记录的文本进行格式化,并添加额外的元数据。
  • 处理器(Handler):把经过格式化之后的日志记录输出到不同的地方。常见的日志输出目标包括控制台、文件和数据库等。

Java日志封装API

在这主要介绍一下目前使用较多的SLF4J日志库,SLF4J 库中核心的 API 是提供工厂方法的 org.slf4j.LoggerFactory 类和记录日志的 org.slf4j.Logger 接口。通过 LoggerFactory 类的 getLogger 方法来获取日志记录器对象。Logger 接口中的方法也是按照不同的严重性级别来进行分组的。
参考:Java日志最佳实践

Jeesite中Log4j的使用分析

Jeesite中使用了Log4j日志记录作为实现,下面依次分析一下Log4j的配置文件和封装了Log4j的Manager类。

Log4j的配置

Log4j支持使用Java properties和xml作为配置文件。相对于xml的格式,Java properties更易读。下面以Jeesite中的log4j.properties为例,所有配置项均以log4j开头,主要配置项为rootlogger,输出终端,输出布局模式。

log4j.rootLogger=WARN, Console, RollingFile  

Logger是日志记录器,而rootLogger则是所有Logger的祖先,可以通过Logger.getRootLogger()来获取。

#Console
log4j.appender.Console=org.apache.log4j.ConsoleAppender
log4j.appender.Console.layout=org.apache.log4j.PatternLayout
log4j.appender.Console.layout.ConversionPattern=%d [%t] %-5p [%c] - %m%n

#RollingFile
log4j.appender.RollingFile=org.apache.log4j.DailyRollingFileAppender
log4j.appender.RollingFile.File=../logs/jeesite.log
log4j.appender.RollingFile.layout=org.apache.log4j.PatternLayout
log4j.appender.RollingFile.layout.ConversionPattern=%d [%t] %-5p [%c] - %m%n

Log4j的输出终端,由Appender接口定义。其有多种实现,不同实现就对应则不同的日志输出保存方式。例如:

  • ConsoleAppender(控制台)
  • FileAppender(文件)
  • DailyRollingFileAppender(每天都产生一个日志文件)
  • RollingFileAppender(文件大小达到指定尺寸时产生一个新的日志文件,文件名称上会自动添加数字序号。)
  • WriterAppender(将日志信息以流的格式发送到任意指定的地方)

而所有子类Logger均将继承父类Logger的Appender配置。 至于输出布局模式——由Layout接口定义——也有多种实现:

  • PatternLayout(可以灵活地指定布局模式)
  • HTMLLayout(以HTML表格形式布局)
  • SimpleLayout(包含日志信息的级别和信息字符串)
  • TTCCLayout(包含日志产生的时间、线程、类别等信息)

ConversionPattern使用的是类似于C语言中printf的格式化输出参数:

  • %c:输出所属的类目,通常就是所在类的全名。
  • %d:输出日志时间点的日期或时间,默认格式为ISO8601,推荐使用“%d{ABSOLUTE}”
  • %t:输出产生该日志线程的线程名。
  • %p:输出优先级。
  • %m:输出代码中指定的消息。
  • %n:输出一个回车换行符。Windows平台为“\r\n”,UNIX为“\n”。

所以jeesite中的日志输出格式为:

Output pattern : date [thread] priority category - message FATAL 0 ERROR 3 WARN 4 INFO 6 DEBUG 7

每个Logger都有一个日志记录级别,对应不同程度的信息,按照优先级可以排列如下:ALL<DEBUG<INFO<WARN<ERROR<FATAL<OFF

#Hibernate level
#log4j.logger.org.hibernate=ERROR
log4j.logger.org.hibernate.cache.ehcache.AbstractEhcacheRegionFactory=ERROR
log4j.logger.org.hibernate.search.impl.ConfigContext=ERROR
log4j.logger.net.sf.ehcache.config.CacheConfiguration=ERROR

#Project defalult level
log4j.logger.com.thinkgem.jeesite=DEBUG

这里分别配置了Hibernate操作数据库的日志记录级别和项目的默认日志记录级别,均为DEBUG,即与程序运行时的流程相关的详细信息。

Log4jManager类

Log4jManager类对于Log4j,主要是对于Log4j的日志记录级别进行管理配置。使用了叫JMX(Java Management Extensions,即Java管理扩展)——一个为应用程序、设备、系统等植入管理功能的框架——对日志记录级别进行管理。此处把Log4j作为Mbean(管理Bean组件)来管理。首先来看一些Getters/Setters函数

@ManagedAttribute(description = "Level of the root logger")
public String getRootLoggerLevel() {
    Logger logger = Logger.getRootLogger();
    return logger.getEffectiveLevel().toString();
}

@ManagedAttribute
public void setRootLoggerLevel(String newLevel) {
    Logger logger = Logger.getRootLogger();
    Level level = Level.toLevel(newLevel);
    logger.setLevel(level);
    managerLogger.info("设置Root Logger 级别为{}", newLevel);
}

这是对根日志记录器进行日志级别设置和获取。managerLogger对象是通过LoggerFactory.getLogger(Log4jManager.class)这一日志记录器工厂来获取的。

@ManagedOperation(description = "Get logging level of the logger")
@ManagedOperationParameters({ @ManagedOperationParameter(name = "loggerName", description = "Logger name") })
public String getLoggerLevel(String loggerName) {
    Logger logger = Logger.getLogger(loggerName);
    return logger.getEffectiveLevel().toString();
}

/**
 * 设置Logger的日志级别.
 * 如果日志级别名称错误, 设为DEBUG.
 */
@ManagedOperation(description = "Set new logging level to the logger")
@ManagedOperationParameters({ @ManagedOperationParameter(name = "loggerName", description = "Logger name"),
        @ManagedOperationParameter(name = "newlevel", description = "New level") })
public void setLoggerLevel(String loggerName, String newLevel) {
    Logger logger = Logger.getLogger(loggerName);
    Level level = Level.toLevel(newLevel);
    logger.setLevel(level);
    managerLogger.info("设置{}级别为{}", loggerName, newLevel);
}

同根Logger一样,此处的普通Logger和项目级别的Logger的Setter\Getter方法,也是大同小异的。

SYS模块中的Log部分

这部分的日志log,并未使用日志记录API,而是通过正常模块开发的形式,把一些操作信息存入数据库表(sys_log)中,其保存的信息包含:

private String id;          // 日志编号
private String type;        // 日志类型(1:接入日志;2:错误日志)
private User createBy;      // 创建者
private Date createDate;    // 日志创建时间
private String remoteAddr;  // 操作用户的IP地址
private String requestUri;  // 操作的URI
private String method;      // 操作的方式
private String params;      // 操作提交的数据
private String userAgent;   // 操作用户代理信息
private String exception;   // 异常信息

日志查询可以在登录后台系统后,通过host/sys/log这个url来访问。

jeesite中hibernate的应用

1.综述

总的来说,jeesite中hibernate的应用主要有2个方面,annotation和查询语句。前者主要是指定实体类与数据库表的各种关系,而后者则包括criteria,它以面向对象对方式来实现各种查询逻辑,以及HQL语句,hibernate自定的查询语句。

2.annotation

先说annotation,就拿User来举例,首先对与User类,有如下的实现:

@Entity
@Table(name = "sys_user")
@DynamicInsert @DynamicUpdate
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
public class User extends IdEntity<User> {
...
}

@Entity用来告诉系统,这个类是一个实体类,提醒系统将它们跟普通POJO类分开。
@Table指定该实体类所对应的数据库表名
@DynamicInsert和@DynamicUpdate两个注释主要目的是提升操作数据的效率。@DynamicUpdate说明在更新时只修改有改动的字段,否则数据库会修改该列的所有字段。@DynamicInsert则针对插入时。
@Cache指定一个数据库表的缓存,也是效率相关。

以上这部分是针对实体类本身的,比较简单,而且直接拓下来完全没问题。

@Id  
@GeneratedValue(strategy = GenerationType.IDENTITY) 
private Integer sys_id;

这句话在user类里没有用到,可是也特别常用。

@Id功能是指定主键。表示这个数据库表的主键是sys_id;
@GeneratedValue指定主键的生成方式,比如自增和手动等。

顺便一提,针对属性的注释可以选择在属性上或者在属性的get方法上。get方法会在把内容存入数据库这个过程中产生作用,而set方法会在把内容取出数据库这个过程中产生作用。

@ManyToOne
@JoinColumn(name="company_id")
@NotFound(action = NotFoundAction.IGNORE)
@JsonIgnore
@NotNull(message="归属公司不能为空")
@ExcelField(title="归属公司", align=2, sort=20)
public Office getCompany() {
    return company;
}

现在开始在难一点的annotation上,主要是在对@ManyToOne,@OneToMany以及@ManyToMany的理解上。

上述代码是User类的getCompany函数及其注释。对与User来说,一个User对应一个Company,而一个Company对应多个User。所以User和Company是多对一的关系。在数据库表中,这种关系的实现形式是在多的那方加入一的那方的外键。

例如User(id,company_id,...),Company(id,...)这种形式。

@ManyToOne就是告诉系统实体属性在数据库表里是以这种形式来组织的。

@JoinColum则是指定这个外键字段在数据库表里的名称。

@NotFound顾名思义,在找不到引用外键多时候采取什么行为,默认是抛出异常,这里采取的是忽略,意思就是允许User没有对应的Company。

@JsonIgnore标明该属性不会在Json进行序列化的时候包含在内。但是我还没有弄懂为什么User类会需要转换为Json。

@NotNull看名字大家都懂的。
@ExcelField自定义的注释,导出excel用的。

这样处理之后,当需要将User中的company存入数据库时,hibernate就会将company所属的另一个实体类office的属性存入office类所对应的数据库表中,并且与User表进行外键关联。

接下来跳过OneToMany,咱们直接来看ManyToMany:

@ManyToMany(fetch = FetchType.EAGER)
@JoinTable(name = "sys_user_role", joinColumns = { @JoinColumn(name = "user_id") }, inverseJoinColumns = { @JoinColumn(name = "role_id") })
@Where(clause="del_flag='"+DEL_FLAG_NORMAL+"'")
@OrderBy("id") @Fetch(FetchMode.SUBSELECT)
@NotFound(action = NotFoundAction.IGNORE)
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
@JsonIgnore
@ExcelField(title="拥有角色", align=1, sort=800, fieldType=RoleListType.class)
public List<Role> getRoleList() {
    return roleList;
}

@ManyToMany表示的是多对多的关系,一个用户可以有多个角色,一个角色也可以被多个用户所有。这种关系在数据库中一般是用一个关系数据库表来实现。

@JoinTable就是用来指定这个表的,在我们的系统中,user表和role表的关联表就是sys_user_role表,就在JoinTable的name字段里指定。其中joinColumns与inverseJoinColumns字段也是必须的字段,前者指定本类,也就是这里的User类在关联表中的字段名,既user_id,后者指的是对方类的字段名,这里便是role_id。

两个annotation组合起来,就构成了一个三张表的查询,当需要通过user查出rolelist的时候,先得到user_id,再在关系表中查询处多个role_id,最后取出多个role对象,返回List

以上便是数据库存取实体类的配置注释部分,通过这些注释,等同于构建了一个普通的java实体类,然后利用配置文件将之于某个数据库表关联起来,当然数据库多个表之间的关系也会反应在这个配置文件中。

3.查询方法

数据库配好了,我们可以轻松查询里面的数据了。不过hibernate提供了多种方法来进行查询,这些查询方法各有优劣。在jeesite里常用的查询方法有HQL,criteria,和普通SQL,criteria又分为普通criteria和DetachedCriteria。SQL的优点是学习成本低,反正大家多少懂一点,缺点是不能跨平台,换个数据库就不行了。HQL跟SQL差不多,但是hibernate进行了一部分封装,使用HQL就能够直接支持跨平台,缺点是查询语句要事先写好,动态性不强。criteria是面向对象的查询方法,能够动态的添加和删除条件。SQL基本不需要讲了,HQL其实也很简单,我会简单的列举jeesite中HQL的使用,criteria学习成本相对较高,接下来我主要会介绍这个部分。

首先看看HQL吧,jeesite的大部分DAO里,都有一个有一个具体的查询语句,调用find或者update函数。这些函数里通常有一段stirng字符串,这字符串是jeesite的作者自己加工的,最终这段字符串会被加工为HQL语句。

public List<Article> findByIdIn(String[] ids){
    return find("from Article where id in (:p1)", new Parameter(new Object[]{ids}));
}

这段代码的作用是根据Id查询出具体的Article实体,其中的string在find函数中被装配为hibernate.Query,然后通过query.list()函数返回查询结果。这里之所以用HQL的原因是这一查询要求使用频繁并且要求不会变化,所以直接写HQL语句,直观且高效。

criteria被实现在BaseDao类中,它是所有Dao类的基类。

public Page<T> find(Page<T> page, DetachedCriteria detachedCriteria)

上述函数就是使用critera的find函数,它以一个DetachedCriteria作为参数,之后引导查询。这样做的好处是当一个具体的service想执行特定的查询时,将会自己构建一个查询criteria,然后传给DAO里的find,就能进行想要的查询。

下面会以jeesite前台页面的主页面所用查询为例的梳理整个过程,该页面内容写在frontindex.jsp文件中。

<div class="span4">
    <h4>
        <small>
            <a href="${ctx}/list-2${urlSuffix}" class="pull-right">
            更多&gt;&gt;
            </a>
        </small>
        组织机构
    </h4>
    <ul>
        <c:forEach items="${fnc:getArticleList(site.id, 2, 8, '')}" var="article">
            <li>
                <span class="pull-right">
                    <fmt:formatDate value="${article.updateDate}" pattern="yyyy.MM.dd"/>
                </span>
                <a href="${article.url}" style="color:${article.color}">${fns:abbr(article.title,28)}
                </a>
            </li>
        </c:forEach>
    </ul>
</div>

这段代码中最重要的就是fnc:getArticleList(site.id, 2, 8, '')这个函数,整个函数的java实现在CmsUtils里。而这整个代码段的作用是从数据库中取出相应的8条记录,显示到登陆主页的展示框中。

public static List<Article> getArticleList(String siteId, String categoryId, int number, String param){
    Page<Article> page = new Page<Article>(1, number, -1);
    Article article = new Article(new Category(categoryId, new Site(siteId)));
    if (StringUtils.isNotBlank(param)){
        @SuppressWarnings({ "rawtypes" })
        Map map = JsonMapper.getInstance().fromJson("{"+param+"}", Map.class);
        if (new Integer(1).equals(map.get("posid")) || new Integer(2).equals(map.get("posid"))){
            article.setPosid(String.valueOf(map.get("posid")));
        }
        if (new Integer(1).equals(map.get("image"))){
            article.setImage(Article.YES);
        }
        if (StringUtils.isNotBlank((String)map.get("orderBy"))){
            page.setOrderBy((String)map.get("orderBy"));
        }
    }
    article.setDelFlag(Article.DEL_FLAG_NORMAL);
    page = articleService.find(page, article, false);
    return page.getList();
}

先看参数,第一个参数site.id参数不用关注,它的主页分为主网站和子网站,目前只有两种,这个ID是区分这两种网站的。第二个参数2是文章的分类id,从jsp文件可以看出这个div框所展现的是所有与组织机构相关的文章,数据库里也显示这一分类的分类ID是2。第三个参数是取出的文章个数,根据框的大小来设定,这里用的是8,也就是8条list。最后是预留参数,目前还没发现哪里在用。代码段里if之内的字段就是解析这一param之用,由于暂时没有使用所以不必关注。简单来讲,这个过程传入分类ID构建了一个新article类,传入数目构成了一个新的page类,然后将这两个类传到page = articleService.find(page, article, false)中。我们再来看articleService.find的代码。

public Page<Article> find(Page<Article> page, Article article, boolean isDataScopeFilter) {
    // 更新过期的权重,间隔为“6”个小时
    // 这部分我暂时未找到权重在哪里使用的,并且运行到现在所有权重都为0,不用更新,因此这部分可以不用关注。
    //目前看来应该是文章排序相关
    Date updateExpiredWeightDate =  (Date) CacheUtils.get ("updateExpiredWeightDateByArticle");
    if (updateExpiredWeightDate == null || (updateExpiredWeightDate != null && updateExpiredWeightDate.getTime() < new Date().getTime())){
        articleDao.updateExpiredWeight();
        CacheUtils.put("updateExpiredWeightDateByArticle", DateUtils.addHours(new Date(), 6));
    }
    // 通过articleDao创建了一个新的criteria
    // criteria的类别是Article,表示该criteria的返回值是Article
    DetachedCriteria dc = articleDao.createDetachedCriteria();

    // 在dc当中添加两个别名,所谓别名是在查询关联数据表的时候使用
    // hibernate会在Article实体类中寻找名为catergory的属性
    // 并在查询的时候将两个实体类关联到一起
    // Article与category的关系是一对多的关系
    // 他们在数据库里的关联字段是category_id
    // 这些都写在Article中get函数的annotation中
    dc.createAlias("category", "category");
    dc.createAlias("category.site", "category.site");

    //如果category存在并且合法
    if (article.getCategory()!=null && StringUtils.isNotBlank(article.getCategory().getId()) && !Category.isRoot(article.getCategory().getId())){
        
        //首先通过传入的id取出category的实体
        Category category = categoryDao.get(article.getCategory().getId());
        
        //接着再进行判断,如果成功取出,则开始构建存取逻辑
        if (category!=null){

            //最外层是一个或逻辑,逻辑1与逻辑2满足一个条件即可
            //接着是逻辑1:article中的category.id要与传入的分类id相等
            //接着是逻辑2:article中category的父类包含传入分类id
            //这一逻辑可以如下总结,对于我们传入的分类id,找到满足以下条件的文章
            //文章的分类与传入分类相同,或者文章的分类是传入分类的子类
            dc.add(Restrictions.or(
                    Restrictions.eq("category.id", category.getId()),
                    Restrictions.like("category.parentIds", "%,"+category.getId()+",%")));
        
            //然后添加第二个逻辑
            //文章所对应的网站id要与分类中的网站id相同
            //这一网站id也是由前台传入,保留在catergory中   
            dc.add(Restrictions.eq("category.site.id", category.getSite().getId()));

            //接着把已经取出的category存入article中
            article.setCategory(category);

        //如果不能取出category,说明数据库中不存在此分类?
        //我也不知道为何category合法却不能取出
        }else{
            //则只比较site,不比较category
            dc.add(Restrictions.eq("category.site.id", Site.getCurrentSiteId()));
        }

    //如果category不合法或者压根就不存在 
    }else{
        //还是只比较site就行了
        //我擦,category一旦有问题,解决方案就是把所有分类都取出来
       dc.add(Restrictions.eq("category.site.id", Site.getCurrentSiteId()));
    }

    //之后就是根据具体设置来添加逻辑
    //如果article的title存在
    //在我这条逻辑线里这东西不可能存在
    //在别的查询逻辑里可能存在title
    if (StringUtils.isNotEmpty(article.getTitle())){
        //那么找出包含此title的文章
        dc.add(Restrictions.like("title", "%"+article.getTitle()+"%"));
    }
    //如果posid存在
    //不知道是干什么的
    if (StringUtils.isNotEmpty(article.getPosid())){
        dc.add(Restrictions.like("posid", "%,"+article.getPosid()+",%"));
    }
    //如果图片地址存在
    if (StringUtils.isNotEmpty(article.getImage()) && Article.YES.equals(article.getImage())){

    //...
    dc.add (Restrictions.and (Restrictions.isNotNull ("image"), Restrictions.ne ("image","")));
    }
    //如果createby有记录
    if (article.getCreateBy()!=null && StringUtils.isNotBlank(article.getCreateBy().getId())){
        //...
        dc.add(Restrictions.eq("createBy.id", article.getCreateBy().getId()));
    }

    //如果需要进行范围过滤
    if (isDataScopeFilter){
        
        //再由dc创造两个别名,之前提到过别名是多表查询时使用的
        //第一个是别名涉及的是article->category->office
        //第二个就是article->createBy
        //注意offic实际上对应的是Office实体类
        //createBy对应的是User实体类
        dc.createAlias("category.office", "categoryOffice").createAlias("createBy", "createBy");
        
        //这里之后涉及office的查询实际上是三张表的查询了
        //文章表(1)->(N)分类表(N)->(1)部门表
        //各种关联由hibernate处理,我们不用处理细节
        //dataScopeFilter是权限过滤函数
        //它取出当前用户的所有角色
        //合并这些角色对应的权限
        //然后按照权限规则添加关于文章归属部门的查询条件
        dc.add(dataScopeFilter(UserUtils.getUser(), "categoryOffice", "createBy"));
    }
    dc.add(Restrictions.eq(Article.FIELD_DEL_FLAG, article.getDelFlag()));
    if (StringUtils.isBlank(page.getOrderBy())){
        //为dc添加排序规则
        dc.addOrder(Order.desc("weight"));
        dc.addOrder(Order.desc("updateDate"));
    }
    //最终交给DAO进行查询
    return articleDao.find(page, dc);
}

详细解析些在注释里了,其中包含了jeesite中criteria的最常见用法。

命令行下为常用的命令取别名

Overview

很多时候由于在命令行会经常使用到很长的命令或者进一个很长的目录,可能需要为常用的命令取一个别名。通常有两种别名的方式,一种是不带参数的别名,一种是可以自带参数的别名。
两种方式都是通过修改shell的配置文件实现,该配置目录在home目录下,默认是隐藏文件,在home目录使用ls -a就可以看到。如果使用bsh,则文件名为.bshrc文件,如果使用zsh,则文件名为.zshrc

不带参数的别名

通常使用alias命令来为长命令创建别名。

    alias sw="ssh wangjiawei@120.24.231.154"

shell配置文件中添加上述代码,就可以在命令行使用sw来代替每次ssh登陆时这一长串命令。

    alias cc="cd..;ls"

由于我习惯,每次返回上一级目录的时候,显示上一级目录的文件列表,因此使用了cc来代替每次执行两条命令。

带参数的命令

有时候我们需要别名也可以带参数,比如使用lsof -i tcp:8888可以查看8888端口是否被占用。如果想为这条命令取一个简短的别名port,需要可以使用port 8080来查看8080端口的占用情况。

    # belongs to port
    # usage: "port 8000"
    function port() { lsof -i tcp:$1 }

使用function的方式,可以指定自带参数。

    # all open ports
    # usage: "ports"
    function ports() { lsof -Pni4 | grep LISTEN }

使用function方式也可以起到alias的效果。上述语句使得用户使用命令ports就可以列出来所有被使用的参数,不用自带参数。