您正在查看: 标签 jeesite 下的文章

关于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;
}   
}

关于日志部分的简单介绍

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的最常见用法。

login模块的从头到尾整合

1.sysLogin.jsp

        本文采用从前端到后台的形式,整体的阐述一下jeesite中login模块的交互流程,整个jeesite的重要功能我拟将他分为login,list,权限三个部分,应该包括了我们需要实现的大部分功能,计划将这三个部分逐一写出。
        jsp这部分我并不太熟,详见嘉炜的jsp讲解,我这里只挑出重要部分介绍:

<form id="loginForm"  class="form login-form" action="${ctx}/login" method="post">

        整个jsp页面其实就是一个表单,它的主要目的就是接受用户输入的用户名和密码字段,然后交给后台处理。action变量指定了该表达的提交方式,既是交由/a/login所对应的函数来处理。

    <input type="text" id="username" name="username" class="required" value="${username}" placeholder="登录名">
    <input type="password" id="password" name="password" class="required" placeholder="密码"/>

        以上就是表单里的两个属性,一个属性名为username,一个名为password,表单会借由request属性传到函数当中,届时就能通过getUsername和getPassword两个函数从request中取出。这部分是在FormAuthenticationFilter中的createToken函数中实现,下文中会详细介绍。
        Login模块中的jsp非常简单,难点主要在于shiro的应用上,这也是该模块与普通list模块的区别之处。

2.shiro

        之前已经写过一篇shiro相关的文章,只是之前的shiro文章是作为简单的shiro教程来写的,内容大而全,所以知识点之间的串联难免会差一些,这次只做为jeesite应用来写,更注重在逻辑方面,若有问题可以参考前一篇文章。
        其实这里也设计到一些spring-mvc的内容,不过之前也已经写过了,而且也很简单,就不作为单独的一章了。

        上回合说到,jsp将username和password打包扔给后台,那么后台是由什么接受呢?在spring-mvc中,负责接受前台数据的是controller部分,而form中所指定的action是/a/login。

@RequestMapping(value = "${adminPath}/login", method = RequestMethod.GET)
public String login(HttpServletRequest request, HttpServletResponse response, Model model) {
    User user = UserUtils.getUser();
    // 如果已经登录,则跳转到管理首页
    if(user.getId() != null){
        return "redirect:"+Global.getAdminPath();
    }
    return "modules/sys/sysLogin";
}

@RequestMapping(value = "${adminPath}/login", method = RequestMethod.POST)
public String login(@RequestParam(FormAuthenticationFilter.DEFAULT_USERNAME_PARAM) String username, HttpServletRequest request, HttpServletResponse response, Model model) {
    User user = UserUtils.getUser();
    // 如果已经登录,则跳转到管理首页
    if(user.getId() != null){
        return "redirect:"+Global.getAdminPath();
    }
    model.addAttribute(FormAuthenticationFilter.DEFAULT_USERNAME_PARAM, username);
    model.addAttribute("isValidateCodeLogin", isValidateCodeLogin(username, true, false));
    return "modules/sys/sysLogin";
}

        很容易就能定位到,以上两个函数就是处理form的函数,或者说正常情况下是由这两个函数来处理。但是仔细看这两个函数,并没有进行逻辑处理,只是简单的检查和跳转。这是因为shiro的登陆功能在controller之前加入了一个filter。这个filter被配置在文件spring-context-shiro.xml文件里。

<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
    <property name="securityManager" ref="securityManager" />
    <property name="loginUrl" value="${adminPath}/login" />
    <property name="successUrl" value="${adminPath}" />
    <property name="filters">
        <map>
            <entry key="authc" value-ref="formAuthenticationFilter"/>
        </map>
    </property>
    <property name="filterChainDefinitions">
        <value>
            /static/** = anon
            /userfiles/** = anon
            ${adminPath}/login = authc
            ${adminPath}/logout = logout
            ${adminPath}/** = user
        </value>
    </property>
</bean>

        以上就是配置过程最关键的部分。loginuUrl属性所指定的url表示的是所有未通过验证的url所访问的位置,此处就是登录界面;successUrl表示的是成功登陆的url访问的位置,此处就是主页。filters则是配置具体验证方法的位置。在此处,${adminPath}/login = authc指定了/a/login,既登陆页面,所需要的验证权限名为authc,又配置了authc所用的filter为formAuthenticationFilter。
        因此整个逻辑是:如果任何地方未登陆,则访问/a/login页面,而/a/login页面的验证权限中又指定了formAuthenticationFilter做为过滤,如果过滤中验证成功,则访问/a这个主页。所以,login.jsp中的表单信息则首先交由formAuthenticationFilter首先处理。

        再来看formAuthenticationFilter中的处理,需要关注的类主要在com.thinkgem.jeesite.modules.sys.security这个包里。通常FormAuthenticationFilter是主要逻辑管理类,SystemAuthorizingRealm这个类则是数据处理类,相当于DAO。但是直接从代码里没发看出功能,这是因为jeesite中的这两个类都是继承于shiro的类,有很多逻辑并没有在jeesite中实现,所以我并不能贴出全部代码,某些地方只用语言来描述。
        大致来讲,首先表单的request被formAuthenticationFilter接收到,然后传给createToken函数,该函数从request中取出name和password,然后生成自定义的一个token传给SystemAuthorizingRealm中的doGetAuthenticationInfo验证。SystemAuthorizingRealm中有systemService的实例,该实例含有userDAO能取出数据库中的name和password。接着由这两个密码生成SimpleAuthenticationInfo,再由info中的逻辑来验证。以上过程中jeesite实现的代码分别如下:

protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) {
    String username = getUsername(request);
    String password = getPassword(request);
    if (password==null){
        password = "";
    }
    boolean rememberMe = isRememberMe(request);
    String host = getHost(request);
    String captcha = getCaptcha(request);
    return new UsernamePasswordToken(username, password.toCharArray(), rememberMe, host, captcha);
}

 

protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authcToken) throws AuthenticationException {
    UsernamePasswordToken token = (UsernamePasswordToken) authcToken;
    
    if (LoginController.isValidateCodeLogin(token.getUsername(), false, false)){
        // 判断验证码
        Session session = SecurityUtils.getSubject().getSession();
        String code = (String)session.getAttribute(ValidateCodeServlet.VALIDATE_CODE);
        if (token.getCaptcha() == null || !token.getCaptcha().toUpperCase().equals(code)){
            throw new CaptchaException("验证码错误.");
        }
    }

    User user = getSystemService().getUserByLoginName(token.getUsername());
    if (user != null) {
        byte[] salt = Encodes.decodeHex(user.getPassword().substring(0,16));
        return new SimpleAuthenticationInfo(new Principal(user), 
                user.getPassword().substring(16), ByteSource.Util.bytes(salt), getName());
    } else {
        return null;
    }
}

3.service+dao+entity

        还剩下一些传统SSH的内容,具体就是在doGetAuthenticationInfo中systemService是如何取出数据库里的数据的。
        这部分感觉大家都知道,先空在这里,有必要再补充。

关于代码生成器的使用和解析

代码生成器的使用

正如许多J2EE整合开发框架一样,Jeesite也提供了代码生成器,我们可以通过配置,利用它来生成一个独立模块的各层次的基础部分,其中包括:

  • Entity,即实体类。对应MVC的Model部分。其继承了Common模块中的模板类BaseEntity。
  • DAO层,封装了对实体类的CRUD操作,相应地继承了模板类BaseDAO。
  • Service层,封装底层,并加了一些基础功能,作为服务层。
  • Controller层,对应MVC的C部分。相应继承了抽象类BaseController。
  • JSP,对应MVC的View部分。代码生成器会生成xxxList和xxxForm两个JSP页面,分别用以展示和增删改查数据条目。此处的XXX为类名。

代码生成器的使用非常简单,首先找到com.thinkgem.jeesite.generate包下的generate类。首先根据注释,自行定义新模块的包名、模块名、类名、功能名、类作者名的对应变量。其中:

  • 包名.模块名.是生成的新模块各个层次的包,默认会生成entity、dao、service、web四个包。而WEB-INF/views/modules/下会生成一个模块名的文件夹。该文件夹为上面提到的两个JSP页面。
  • 类名就对应四个包下类名前缀,若类名为Sign,则四个包下的四个类分别为Sign、SignDAO、SignService、SignController。
  • 功能名是为了说明功能模块的作用,会在生成的JSP中包含对应信息。而类作者名则会加到生成类的注释中。
    配置好这些后,对generate类选择RUN AS - Java Application即可。由于该类包含Main()方法,是可执行的。然后它会读取com.thinkgem.jeesite.generate.template下的6个ftl文件——这是freemaker模板引擎使用的文件——最后填入对应的配置变量,生成如前所述的4个类和2个JSP页面。

最后要运行这个生成模块的话,还需手动在数据库中建立对应的数据表,数据表除了在实体类中定义的属性外,还必须包含del_flag、remarks、create_by、create_date、update_by、update_date这几个字段。这是因为实体类要继承DataEntity这个抽象类,此类中定义了这些属性,后续的查询、排序中要用到这些字段。

代码生成器各部分解析

代码生成器实际为我们生成了MVC的各个层次,下面结合代码,分析一下各部分的原理。

Entity实体层

首先,任何Entity都是继承了IdEntity这个抽象类,IdEntity主要提供实体类的id属性及其对应setter和getter方法。这样生成的entity就继承了这个属性。关于com.thinkgem.jeesite.common.persistence包下几个基础Entity类的关系可以表示如下:IdEntity--->DataEntity--->BaseEntity。这样每个生成的Entity就继承了包括id、del_flag、remarks、create_by、create_date、update_by、update_date在内的属性。其中id是利用util中的IdGens生成的uuid,即生成的id不会自增。而是一段字母+数字的唯一组合。若对id有auto_increment的要求,则需要考虑重写IdEntity类。

Dao层

实际上,所有代码生成的dao都是直接继承了common中的BaseDao类,继承后的子类并未添加任何属性和方法。在此就主要针对BaseDao类分析。总的来说,BaseDao主要是在Hibernate提供的数据库操作基础上,封装了Page、parameter参数。这里的Page是作为展示页面的对象,而Parameter是继承了HashMap<String, object>的哈希字典。BaseDao就针对实体对象,提供了find、get、save、update等几类操作,每种操作根据参数的不同有几种函数重载形式。而对删除操作,这里是通过deleteById来设置del_flag字段的布尔值进行逻辑删除的。如果要进行真实的表中某行数据删除,需要我们自己写对应的操作函数。

Service层

新模块的Service层一般都有一个dao对象的属性,然后在dao的基础上进一步封装,加入一些其它有用功能的服务。由于不同模块提供的服务不尽相同,这里就不好进行概括了。而BaseService类则是提供了根据用户角色的DATA_SCOPE进行数据过滤。

Controller层

新建模块的Controller主要使用Spring提供的Annotation,把访问Url与实际的Jsp页面映射起来,与此同时使用Service、Dao对象提供的数据库操作函数,这样就可以在页面上对数据库进行增删改查。而不同Controller继承的BaseController则主要通过封装BeanValidator提供对服务端参数有效性验证。例如我们通过页面向数据库添加实体对象时,可以利用BaseController进行Bean的校验,验证通过后再保存对象。关于BeanValidator较为详细的介绍可以参考这里