This site is used to record and share the informations & useful tips during the coding and research.
Ehcache简介 Ehcache是一个纯Java的进程内缓存框架,是Hibernate中默认的CacheProvider,主要面向通用缓存,Java EE和轻量级容器。主要特性有： 简单快速 多种缓存策略,支持LRU、LFU和FIFO。 支持内存和硬盘作为缓存存储 缓存数据会在虚拟机重启的过程中写入磁盘 具有缓存和缓存管理器的侦听接口 可以通过RMI、可插入API等方式进行分布式缓存 可与spring、Hibernate、shiro等框架组件进行整合 项目中Ehcache的配置 Jeesite中Ehcache的配置文件主要在资源目录下的cache文件夹中，默认使用的是ehcache-local.xml和ehcahce-hibernate-xml，前者是ehcache的配置文件，后者是针对hibernate的配置文件。下面先看一下ehcache-local.xml的内容： <ehcache updateCheck="false" name="defaultCache"> <diskStore path="java.io.tmpdir/jeesite/ehcache/default" /> <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; } }
Overview 本文记录了在使用Typecho的过程中，遇到的一些小问题以及解决方法。 1.添加置顶插件 首先去Typecho的官网插件页面http://docs.typecho.org/plugins下载Sticky插件，下载结束后，解压缩，将此文件夹放至服务器上Typecho的plugin文件夹中，具体地址为blog/usr/plugins/中。 使用超级管理员admin登陆到后天，进入管理插件页面，就可以看到所有plugins文件夹中已经有的插件。如下图: 刚把插件放到plugin文件夹中的时候，默认是在禁用的插件中，没有启用的。点击启用，就会变成启用的插件。然后点击设置, 出现下图： 注意其中的置顶文章的cid，用来设置需要置顶的文章，文章编号的获取方式看下图： 我们需要将jeesite中hibernate的应用这篇文章置顶，只需要像上图一样，将网址中标记的文章ID号56放在置顶文章的cid中即可。 在 index.php 的 $this->title(); 前面加上 $this->sticky(); 就会变成这段html代码： <a href="<?php $this->permalink() ?>"><?php $this->sticky(); $this->title() ?> 。 如果你使用了主题，那么要修改的就是主题文件夹下面的index.php。 2.添加代码高亮插件 去Typecho的官网插件页面http://docs.typecho.org/plugins下载Google Code Prettify插件。 使用方法同上，不再给出详细步骤。 效果如下图： 3.去掉页面中代码框的滚动条 刚用上代码高亮插件，显示效果明显提升了一个档次，还没开心多久，发现当代码段太长的时候，代码段不会完整的显示出来，而是会出现滚动条，读代码非常的不方便。按以下步骤进行修改： 进入tpyecho文件夹的主题文件夹下，blog/usr/themes/default，找到style.css，查看该页面的代码，发现以下代码： pre{padding:0;border:1px solid #ccc;overflow:auto;max-height:400px;} 很明显，当代码段太长的时候，最大显示高度为400px，超过这个长度就会出现滚动条。 直接全部删掉这句代码即可。或者只删掉max-height:400px;。 完成。可能需要重启服务器。PS：在做了如下修改之后，其他人看到的就没有滚动条了，我这边还是有滚动条，最后清空了浏览器缓存，才效果正常了。 4.博客首页文章自动截断 typecho的首页默认是将文章以列表的形式展示，每篇文章都会完整展示，如果文章太长的话，就很难一次看到很多篇文章，查找起来很不方便。 修改方法: 进入tpyecho文件夹的主题文件夹下，blog/usr/themes/default，找到index.php，查看该页面的代码，发现以下代码： <?php $this->content('- 阅读剩余部分 -'); ?> 将上诉代码完整改为以下代码： <?php $this->excerpt(200, '...'); ?><a href="<?php $this->permalink() ?>" rel="bookmark" title="详细阅读关于<?php $this->title(); ?>">阅读全>文... 完成。可能需要重启服务器。 效果如下图： 5.添加文章内容的扫码阅读 直接去http://forum.typecho.org/viewtopic.php?f=6&t=8253下载文件QRCode.zip。解压之后，将QRCode文件夹上传到博客的plugins目录blog/usr/plugins中就可以了。 使用管理员账号登入，点击管理插件，启用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来访问。
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文件中。 更多>> 组织机构 <c:forEach items="${fnc:getArticleList(site.id, 2, 8, '')}" var="article"> <fmt:formatDate value="${article.updateDate}" pattern="yyyy.MM.dd"/> ${fns:abbr(article.title,28)} </c:forEach> 这段代码中最重要的就是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就可以列出来所有被使用的参数，不用自带参数。
PNG相比GIF有更好的显示质量。 但是在IE6下面，透明背景无法被兼容。 下面记录下具体兼容的一种方法： 图片img采用div加背景图片的方式添加，在html文件中添加css代码： #logo { width: 300px; height: 300px; background:url(img/logo.png) no-repeat; /*以下为IE6设置PNG透明代码*/ _background:none; _filter:progid:DXImageTransform.Microsoft.AlphaImageLoader(src="img/logo.png"); } 这里需要注意，后面两行IE6的hack只有直接写在HTML文件中，才有效果。
Overview 在网站开发的过程中，通常一个网站会有一个整体的风格，页面都有很多共同的菜单，横栏的底部信息。以前我们会采用include标签在每个jsp页面中来不断的包含各种header, stylesheet, scripts and footer，现在，在sitemesh的帮助下，我们不必再使用这种方式来保持风格统一了。 关于SiteMesh概述 sitemesh的设计思想是装饰者(decorator)设计模式。 关于装饰者(decorator)设计模式 尝试着简短的几句话介绍清楚，发现很难..列一个官方的定义： The Decorator Pattern attaches additional responsibilities to an object dynamically. Decorators provide a flexible alternative to subclassing for extending functionality. (装饰者模式可以动态地给一个对象增加其他职责。就扩展对象功能来说，装饰者模式比生成子类更为灵活。) 详细的关于装饰者设计模式的资料，推荐下面两篇文章： 1.http://blog.chinaunix.net/uid-20761674-id-304542.html 2.http://www.cnblogs.com/rush/archive/2011/05/08/Decorator_DesignPattern_NETFramework.html SiteMesh 首先看一下SiteMesh的工作过程图： 从宏观上来看上图，Header和Footer都是在每个界面都是固定或者有细微变化的(但不会有结构上的变化)，改变的只是中间的元素。这时候我们可以编写一个decorator页面(上图中的browser-theme.jsp)，这样在编写实际的展示页面(左上角的Menu.jsp)时，只需要去展示实际上不同的界面，并指定它需要哪个decorator修饰，这样在返回结果时，实际上是decorator和实际的展示页面组合成的新页面，如上图中右下角的Menu.jsp界面。 看得出来，SiteMesh使得我们可以不必在每个界面都重复编写相同的模板性元素，而只需要专注于实际需要展示的内容。 而且，使用SiteMesh可以实现更大的灵活性，给界面指定不同的decorator，比如下图中，使用手机浏览器和网页浏览器访问Menu.jsp时，该页面是被不同的decorator修饰，最终返回不同的组合界面。 如何使用SiteMesh 1.在WEB-INF/web.xml中添加以下filter的定义: <filter> <filter-name>sitemeshFilter</filter-name> <filter-class> com.opensymphony.sitemesh.webapp.SiteMeshFilter </filter-class> </filter> <filter-mapping> <filter-name>sitemeshFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> sitemesh老版本的filter是PageFilter。 目前这个Filter已经过时，使用新的SiteMeshFilter。 2.将所需要的sitemesh.jar copy至WEB-INF/lib，如果使用Maven。不需要手动管理jar包，直接在pom.xml中添加以下依赖： <dependency> <groupId>opensymphony</groupId> <artifactId>sitemesh</artifactId> <version>${sitemesh.version}</version> <scope>runtime</scope> </dependency> 其中，${sitemesh.version}是一个pom中自定义的版本号，我们使用的为2.4.2。 3.建立WEB-INF/decorators.xml描述各装饰器页面，可仿照sitemesh例子。 如下所示： <decorators defaultdir="/WEB-INF/views> <decorator name="default" page="layouts/default.jsp"> <pattern>*</pattern> </decorator> </decorators> 这其中，我们使用了<decorator>标签来声明了一个叫default的装饰器，<pattern>子标签指定了该标签可以用来装饰所有的请求页面，这样，当客户端请求任何一个页面时，实际上都是由default装饰器修饰之后，返回一个组合界面。 既然需要default装饰器修饰，那么需要可以找到实际上这个装饰器页面default.jsp在哪里。这里注意decorators的defaultdir属性，该属性指定了所有装饰器的具体存放路径前缀，根据<decorators>的defaultdir属性和<decorator>的page属性，就可以准确定位到每一个decorator的装饰页地址。比如，上述default装饰器页面实际放在/WEB-INF/views/layouts/default.jsp中。 4.建立default.jsp界面： <%@ page contentType="text/html; charset=GBK"%> <%@ taglib uri="sitemesh-decorator" prefix="decorator" %> <decorator:title default="装饰器页面..." /> <decorator:head /> sitemesh的例子 <decorator:body /> chen56@msn.com 注意<%@ taglib uri="sitemesh-decorator" prefix="decorator" %>这句代码，为了在后面的jsp界面中使用decorator前缀，如<decorator:head />，需要声明该prefix并指定uri以让编译器知道应该去哪里寻找decorator的定义文件，上述的代码指定了一个本地定义文件，sitemesh-decorator，因此需要手动将sitemesh包种的sitemesh-decorator.tld和sitemesh-page.tld文件放到/WEB-INF/文件夹下。 实际上，新版本的sitemesh中，可以不必这么做，下面结合我们自己的项目讲解的时候会再说明。 装饰器jsp里使用的前缀还是decorator，新版本的已经变成了sitemesh，并且不再需要手动添加sitemesh-decorator.tld和sitemesh-page.tld文件到本地。后面一并解释。 5.建立一个的被装饰页面 /index.jsp(内容页面) <%@ page contentType="text/html; charset=GBK"%> Agent Test 本页只有一句，就是本句. 6.当客户端请求/index.jsp界面时，首先会被web.xml中的sitemesh filter拦截，然后去查看有没有decorator可以匹配/index.jsp，结果发现default.jsp声明可以匹配一切界面。当返回/index.jsp的结果时，实际上会将这两个界面拼起来，最后返回给客户端。 如下所示： 实际项目中使用到的SiteMesh 第1，2步与上文没有区别，因此下面直接从3开始讲解。 3.建立WEB-INF/decorators.xml描述各装饰器页面： decorators.xml： <?xml version="1.0" encoding="UTF-8"?> <decorators defaultdir="/WEB-INF/views"> <decorator name="default" page="layouts/default.jsp" /> <decorator name="default_mb" page="mobile/layouts/default.jsp" /> <decorator name="cms_default_basic" page="modules/cms/front/themes/basic/layouts/default.jsp" /> <decorator name="cms_default_zgxj_blue" page="modules/cms/front/themes/zgxj_blue/layouts/default.jsp" /> </decorators> 看得出来，跟上面例子唯一的区别在于decorator不再显式声明它要修饰的页面，而是改由要被修饰的页面主动制定要被谁修饰。在第5步中介绍。 4.建立default.jsp界面： default.jsp： <%@ page contentType="text/html;charset=UTF-8"%> <%@ include file="/WEB-INF/views/include/taglib.jsp"%> <%@ taglib prefix="sitemesh" uri="http://www.opensymphony.com/sitemesh/decorator" %> <sitemesh:title/> <%@include file="/WEB-INF/views/include/head.jsp" %> <sitemesh:head/> <sitemesh:body/> 可以看到，跟上文的区别主要有两点，主要在<%@ taglib prefix="sitemesh" uri="http://www.opensymphony.com/sitemesh/decorator" %>这句代码的区别。 首先前缀不再使用decorator，而是使用sitemesh，uri也不再使用本地的定义文件，而是使用远程站点的uri。 顺理成章，下面都使用了sitemesh前缀，如<sitemesh:body/>。 5.建立一个的被装饰页面 /sysIndex.jsp，地址为(src/main/webapp/WEB-INF/views/modules/sys/sysIndex.jsp)，代码如下： sysIndex 片段： <%@ page contentType="text/html;charset=UTF-8" %> <%@ include file="/WEB-INF/views/include/taglib.jsp"%> 利安管家卡后台 ...//省略代码 ...//省略代码 ...//省略代码 这里面需要注意一句代码： 显式指定了该页面需要被哪个decorator修饰。content 中指定的就是decorator的名字。 因此当请求该页面的时候，会在返回结果时自动找到default.jsp进行组装，最后返回完整的融合信息给客户端。 本文参考了： http://www.cnblogs.com/fangjian0423/p/3559270.html http://docs.huihoo.com/java/sitemesh/index.html 想要参考更多的话： 官方的文档（其实不是很全，缺失很多部分）： http://wiki.sitemesh.org/wiki/display/sitemesh/Learn+-+Getting+Started+with+SiteMesh 一个使用springMVC与SiteMesh结合的例子： https://fahadshaon.wordpress.com/2012/07/24/sitemesh-2-with-spring-mvc-3-multiple-layout-for-multiple-controller/
1.sysLogin.jsp 本文采用从前端到后台的形式，整体的阐述一下jeesite中login模块的交互流程，整个jeesite的重要功能我拟将他分为login，list，权限三个部分，应该包括了我们需要实现的大部分功能，计划将这三个部分逐一写出。 jsp这部分我并不太熟，详见嘉炜的jsp讲解，我这里只挑出重要部分介绍： 整个jsp页面其实就是一个表单，它的主要目的就是接受用户输入的用户名和密码字段，然后交给后台处理。action变量指定了该表达的提交方式，既是交由/a/login所对应的函数来处理。 以上就是表单里的两个属性，一个属性名为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"> <entry key="authc" value-ref="formAuthenticationFilter"/> </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较为详细的介绍可以参考这里。
浏览器种类 目前，前端需要专门测试的浏览器包括： IE6 IE7 IE8 IE9+ chrome firefox opera 360浏览器（on xp/windows7） 其中，IE6/IE7/360+xp作为低端浏览器，专门做兼容fix。 其他浏览器可以看作modern浏览器，兼容性问题相对较少。 开发过程 开发，从modern浏览器中的chrome作为标准，第一波测试在基于chrome/firefox/opera的开发中完成。 第二波测试IE9+，第三波测试IE8-。 在第一波过程中，修改完善css，html标签，初始开发保证标签css没有问题。 在后续测试中，通过hack的方法，加入补丁，将其他浏览器上的效果调整到modern浏览器的效果。 css hack及其他fix方法 这里整理下http://www.douban.com/note/163291324/ 的内容 IE8兼容问题 直接通过添加 <metahttp-equivmetahttp-equiv="x-ua-compatible"content="IE=7"/> 将IE8的显示模式调解成IE7的，这样将IE8的兼容问题转化为IE7的兼容问题 IE6兼容问题 float浮动在IE6下双倍margin问题： '_'只有IE6能够识别，比如： _maring: 0; Firefox问题 清除浮动 添加一个div，style为 clear:both; 其他辨识标签 更进一步，可以通过HTML判断浏览器，来编写针对特定浏览器的代码(from http://zxlyecf2.iteye.com/blog/2116974)： <!--[if !IE]><!-->除IE外都可识别<!--<![endif]--> <!--[if IE]> 所有的IE可识别<![endif]--> <!--[if IE 6]> 仅IE6可识别<![endif]--> <!--[if lt IE 6]> IE6以及IE6以下版本可识别<![endif]--> <!--[if gte IE 6]> IE6以及IE6以上版本可识别<![endif]--> <!--[if IE 7]> 仅IE7可识别<![endif]--> <!--[if lt IE 7]> IE7以及IE7以下版本可识别<![endif]--> <!--[if gte IE 7]> IE7以及IE7以上版本可识别<![endif]--> 测试工具 这里主要介绍 modern.IE ，这是微软出的前台测试的平台。 该平台可分为两个部分：一个是用于检测常见代码问题的Web扫描工具，另一个是与BrowserStack合作的虚拟测试服务。 其中，BrowerStack是一个云端的测试服务，可以提供所有系统和浏览器的组合。