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; } }
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的最常见用法。
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较为详细的介绍可以参考这里。
框架前端页面涉及到的标签以及数据传输
Overview 本文针对以JSP为主的前端界面，如何展示数据以及接收后台传输的数据做出了一点说明。 目前框架前端页面所使用到的标签，主要为三种：普通的JSP标签，JSP标准标签库(JSTL)，标签库描述符。下面分别介绍这三种不同的形式，并以框架中的src/main/webapp/WEB-INF/views/modules/sys文件夹中的后台管理主页面sysIndex.jsp为例进行分析。 JSP标签 普通的JSP标签几乎是在HTML标签之外，添加了一些传输数据和控制显示的功能，在此不再做过多的解释。请参考以下网址详细了解：http://www.w3cschool.cc/jsp JSP标准标签库(JSTL) JSP标准标签库（JSTL）是一个JSP标签集合，它封装了JSP应用的通用核心功能。 JSTL支持通用的、结构化的任务，比如迭代，条件判断，XML文档操作，国际化标签，SQL标签。 除了这些，它还提供了一个框架来使用集成JSTL的自定义标签。 JSTL标签按照功能可以分为核心标签，格式化标签，SQL 标签，XML标签，JSTL函数。 根据我们的需求，这里只介绍一下核心标签，并省略了JSTL库的安装。 核心标签是最常用的JSTL标签。引用核心标签库的语法如下： <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <c:out> 用于在JSP中显示数据，就像<%= ... > <c:set> 用于保存数据 <c:remove> 用于删除数据 <c:catch> 用来处理产生错误的异常状况，并且将错误信息储存起来 <c:if> 与我们在一般程序中用的if一样 <c:choose> 本身只当做<c:when>和<c:otherwise>的父标签 <c:when> <c:choose>的子标签，用来判断条件是否成立 <c:otherwise> <c:choose>的子标签，接在<c:when>标签后，当<c:when>标签判断为false时被执行 <c:import> 检索一个绝对或相对 URL，然后将其内容暴露给页面 <c:forEach> 基础迭代标签，接受多种集合类型 <c:forTokens> 根据指定的分隔符来分隔内容并迭代输出 <c:param> 用来给包含或重定向的页面传递参数 <c:redirect> 重定向至一个新的URL. <c:url> 使用可选的查询参数来创造一个URL 以<c:forEach>为例，对该标签详细说明： <c:forEach>标签封装了Java中的for，while，do-while循环。它迭代一个集合中的对象。 属性 <c:forEach>标签有如下属性： 属性 | 描述 items 要被循环的信息 begin 开始的元素（0=第一个元素，1=第二个元素） end 最后一个元素（0=第一个元素，1=第二个元素） step 每一次迭代的步长 var 代表当前条目的变量名称 varStatus 代表循环状态的变量名称 该标签主要用来在JSP页面中循环显示一个列表型栏目，比如从后台取到一个用户list数据，使用此标签可以循环将用户list中的每一个用户信息显示在JSP页面。 <c:forEach>实例演示 <%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> c:forEach 标签实例 <c:forEach var="i" begin="1" end="5"> Item <c:out value="${i}"/> </c:forEach> 结果显示如下： Item 1 Item 2 Item 3 Item 4 Item 5 关于所有这些标签的更详细信息，请参见：http://www.w3cschool.cc/jsp/jsp-jstl.html 标签库描述符 tld是taglib description 的缩写。 标签库描述文件，如要在JSP页面中实现JSP标签，必须首先定义实现标签的类，然后在标签库描述文件（TLD）中将写好的类映射成jsp标签,最后在JSP文件中使用定义好的标签，就可以生成动态的JSP内容。 下面结合sysIndex.jsp完整说明整个流程。 sysIndex.jsp代码片段： <%@ page contentType="text/html;charset=UTF-8" %> <%@ include file="/WEB-INF/views/include/taglib.jsp"%> ... <%@ include file="/WEB-INF/views/include/taglib.jsp"%> 此代码将taglib.jsp中的内容完整引入到sysIndex.jsp中，先看一下taglib.jsp中的代码： <%@ taglib prefix="shiro" uri="/WEB-INF/tlds/shiros.tld" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %> <%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> <%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %> <%@ taglib prefix="fns" uri="/WEB-INF/tlds/fns.tld" %> <%@ taglib prefix="fnc" uri="/WEB-INF/tlds/fnc.tld" %> <%@ taglib prefix="tags" tagdir="/WEB-INF/tags" %> <c:set var="ctx" value="${pageContext.request.contextPath}${fns:getAdminPath()}"/> <c:set var="ctxStatic" value="${pageContext.request.contextPath}/static"/> 可见taglib.jsp主要是为了引入要再JSP中使用的名字前缀，这类名字前缀主要是分两类，一类是标准标签，如c,form,fmt,fn标签，他们的uri属性是一个网址，实质上这个网址上指向的是一个标准定义的tld文件，一类是自定义标签，包含第三方库的自定义标签shiro，自己定义的自定义标签fns,fnc，他们的uri指向的是本地自定义的tld文件。prefix属性是使用此标签时的标签名。 以<%@ taglib prefix="fns" uri="/WEB-INF/tlds/fns.tld" %>为例，我们看一下fns.tld文件： fns.tld 代码片段： <?xml version="1.0" encoding="UTF-8" ?> <taglib xmlns="http://java.sun.com/xml/ns/j2ee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-jsptaglibrary_2_0.xsd" version="2.0"> <description>JSTL 1.1 functions library</description> <display-name>JSTL functions sys</display-name> <tlib-version>1.1</tlib-version> <short-name>fns</short-name> <uri>http://java.sun.com/jsp/jstl/functionss</uri> <function> <description>获取管理路径</description> <name>getAdminPath</name> <function-class> com.thinkgem.jeesite.common.config.Global </function-class> <function-signature>j java.lang.String getAdminPath() </function-signature> <example>${fns:getAdminPath()}</example> </function> ...//省略代码 该文件实际上是一个前后台连接的枢纽，<short-name>中的标签就是JSP中可以使用的标签前缀。<function>中定义的是所需要映射到后台实现类的具体方法，这部分下面会详细解释。 回到sysIndex.jsp看一段代码： sysIndex.jsp代码片段： ...//省略代码 <c:set var="firstMenu" value="true"/> <c:forEach items="${fns:getMenuList()}" var="menu" varStatus="idxStatus"> <c:if test="${menu.parent.id eq '1' && menu.isShow eq '1'}"> ${menu.name} <c:if test="${firstMenu}"> <c:set var="firstMenuId" value="${menu.id}"/> </c:if> <c:set var="firstMenu" value="false"/> </c:if> </c:forEach> <shiro:hasPermission name="cms:site:select"> ${fnc:getSite(fnc:getCurrentSiteId()).name} <c:forEach items="${fnc:getSiteList()}" var="site"> ${site.name} </c:forEach> </shiro:hasPermission> ...//省略代码 <c:set var="firstMenu" value="true"/> 此标签使用了jstl，声明了一个变量firstMenu，值为value，其中c即由taglib.jsp中<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>中声明。 <c:forEach items="${fns:getMenuList()}" var="menu" varStatus="idxStatus"> 此标签定义了循环，用来显示从后台获取的菜单，其中，items存储了后台读取来的menu list，var中存储了每次遍历时，遍历到的菜单对象，用于显示在页面上。 其中fns由taglib.jsp中<%@ taglib prefix="fns" uri="/WEB-INF/tlds/fns.tld" %>声明，并在fns.tld中定义，我们来看一下其中关于getMenuList()中的定义： fns.tld代码片段： ...//省略代码 <function> <description>获取当前用户的菜单对象列表</description> <name>getMenuList</name> <function-class> com.thinkgem.jeesite.modules.sys.utils.UserUtils </function-class> <function-signature> java.util.List getMenuList() </function-signature> <example>${fns:getMenuList()}</example> </function> ...//省略代码 <function>： 对应于一个具体的函数，声明了JSP中可以使用的函数名，以及跟后台具体实现类的映射。具体如下： <description>： 可以理解为函数的注释，用来说明函数的用途。 <name>： 前端JSP中可以使用的函数名，${fns:getMenuList()}中的getMenuList()就是这个名字。 <function-class>： 指定了一个具体的后台实现类，来真正实现该方法。 <function-signature>： 通常一个tld文件对应一个实现类，里面会有很多方法，函数签名唯一的映射到了后台类的具体某一个函数上。 <example>： 说明了JSP中如何使用该函数，给出了一个示范用法。 接下来我们看一下com/thinkgem/jeesite/modules/sys/utils/中的UserUtils.java文件： UserUtils.java代码片段： public class UserUtils extends BaseService { ... //代码省略 private static MenuDao menuDao = SpringContextHolder.getBean(MenuDao.class); ... //代码省略 public static List<Menu> getMenuList(){ @SuppressWarnings("unchecked") List<Menu> menuList = (List<Menu>)getCache(CACHE_MENU_LIST); if (menuList == null){ User user = getUser(); if (user.isAdmin()){ menuList = menuDao.findAllList(); }else{ menuList = menuDao.findByUserId(user.getId()); } putCache(CACHE_MENU_LIST, menuList); } return menuList; } ... //代码省略 } 可以看到，UserUtils.java中的 getMenuList()方法首先会检查是否缓存了该menu list，如果没有，调用了menuDAO来进行后台数据获取，然后存一份到缓存，并返回一个menu list给前台。具体的DAO层不再深入说明。 至此，整个前端取后台数据并展示到前台的流程就到这里了。
一、shiro访问权限 1.如何规定访问权限？ @RequiresPermissions 参数是否只是简单的字符串匹配？（未解决） 2.如何指定对应用户的访问权限？ （1）doGetAuthorizationInfo中有getMenuList()函数，该函数获取当前user所拥有的menu。 1当前user何时指定？ 在doGetAuthenticationInfo中生成Principal(user)。 从此就能通过subject.getPrincipal取出，也能在doGetAuthorizationInfo当参数中。 （2）接着通过addStringPermission添加到subject里 （3）当@RequriesPerminssions时从subject中取出验证 二、cache 1.在UserUtils中 三、page 四、取rolelist和menulist 1.各种hibernate的annotation @ManyToMany(fetch = FetchType.LAZY) @JoinTable(name = "sys_role_menu", joinColumns = { @JoinColumn(name = "role_id") }, inverseJoinColumns = { @JoinColumn(name = "menu_id") }) @Where(clause="del_flag='"+DEL_FLAG_NORMAL+"'") @OrderBy("id") @Fetch(FetchMode.SUBSELECT) @NotFound(action = NotFoundAction.IGNORE) @Cache(usage = CacheConcurrencyStrategy.READ_WRITE) 五、aop 六、多个filter或realm //1不同url对应不同的realm？？ 2定义两个shirofilter 七、log日志 1.LogInterceptor中有简单的日志记录
1.整体框架和流程 首先是跟spring－mvc没有直接关系但是是系统整体的流程的说明。 以user管理为例，最外层是UserController结构，这个结构是跟spring－mvc直接关联的，之后会细讲。这个结构的主要是分配url的处理函数。当用户在浏览器中输入url时，spring－mvc把url和表单信息引导到UserController的某个函数中处理，然后返回对应的view表示物。该view有多种返回形式。权限验证都在这一层，能够判断哪些view有权限访问。 controller中有一个SystemService，基本就是传统ssh框架中的service了，负责某一功能的业务逻辑实现。service中还有各种DAO的实体，能够通过DAO访问数据库。除此之外，还有SystemAuthorizingRealm以及IdentityService，前者能在执行处理的过程中管理认证信息，特别是一些缓存信息；后者则跟Activiti，也就是工作流有关，具体细节还待摸索。总而言之，service直接操作数据实体，提供所需的各种业务逻辑实现。这些相关类都通过spring的自动装配导入。Transaction都在这一层。 此外就是User类和UserDAO，hibernate的内容，不用赘述。 2.spring－mvc的annotation 2.1 annotaton @Autowired： @Autowired注解标签可以对成员变量、方法和构造函数进行标注，来完成自动装配的工作。可以写在成员变量之上，也可以在变量对应的set方法上。 public class RoleServicesLogic implements RoleServices { @Autowired private RoleDao roleDao; } 也可以对方法进行注解 public class RoleServicesLogic implements RoleServices { private RoleDao roleDao; @Autowired public void setRoleDao(RoleDao roleDao) { this.roleDao = roleDao; } } 以上两种不同实现方式中，@Autowired的标注位置不同，它们都会在Spring在初始化RoleManagerImpl这个bean时， 自动装配roleDao这个属性，区别是：第一种实现中，Spring会直接将RoleDao类型的唯一一个bean赋值给roleDao这个成员变量； 第二种实现中，Spring会调用setRoleDao方法来将RoleDao类型的唯一一个bean装配到RoleDao这个属性。 2.2 常用spring的anootation @Service用于标注业务层组件 @Repository用于标注数据访问组件，即DAO组件 @Controller用于标注控制层组件，如Struts中的Action或者springmvc的controller @Component泛指组件，当组件不好归类时，可以使用这个注解进行标注 @Entity 实体类注解标签 @Table 实体类对应的数据库实际的表名，不设置表名默认数据库表名与类名一致。 2.3 RequestMapping 1）最基本的，方法级别上应用，例如： @RequestMapping(value="/departments") public String simplePattern(){ System.out.println("simplePattern method was called"); return "someResult"; } 则访问http://localhost/xxxx/departments的时候，会调用 simplePattern方法了 2）参数绑定 @RequestMapping(value="/departments") public String findDepatment( @RequestParam("departmentId") String departmentId){ System.out.println("Find department with ID: " + departmentId); return "someResult"; } 形如这样的访问形式： /departments?departmentId=23就可以触发访问findDepatment方法了 3) REST风格的参数 @RequestMapping(value="/departments/{departmentId}") public String findDepatment(@PathVariable String departmentId){ System.out.println("Find department with ID: " + departmentId); return "someResult"; } 形如REST风格的地址访问，比如： /departments/23，其中用(@PathVariable接收rest风格的参数 4) REST风格的参数绑定形式之2 先看例子，这个有点象之前的： @RequestMapping(value="/departments/{departmentId}") public String findDepatmentAlternative( @PathVariable("departmentId") String someDepartmentId){ System.out.println("Find department with ID: " + someDepartmentId); return "someResult"; } 这个有点不同，就是接收形如/departments/23的URL访问，把23作为传入的departmetnId,，但是在实际的方法findDepatmentAlternative中，使用 @PathVariable("departmentId") String someDepartmentId，将其绑定为 someDepartmentId,所以这里someDepartmentId为23 5) url中同时绑定多个id @RequestMapping(value="/departments/{departmentId}/employees/{employeeId}") public String findEmployee( @PathVariable String departmentId, @PathVariable String employeeId){ System.out.println("Find employee with ID: " + employeeId + " from department: " + departmentId); return "someResult"; } 这个其实也比较好理解了。 6) 支持正则表达式 @RequestMapping(value="/{textualPart:[a-z-]+}.{numericPart:[\\d]+}") public String regularExpression( @PathVariable String textualPart, @PathVariable String numericPart){ System.out.println("Textual part: " + textualPart + ", numeric part: " + numericPart); return "someResult"; } 比如如下的URL：/sometext.123，则输出： Textual part: sometext, numeric part: 123 2.4 RequestParam springmvc提供了@RequestParam注释帮助我们获取参数。 用法@RequestParam("接收的参数名") @RequestMapping(params="servlet=login") public String login(@RequestParam("username")String username,@RequestParam("password")String password,HttpServletRequest request,ModelMap map){ //处理登录逻辑，省略 return "success"; } 当客户端的URL提交了username参数，password参数，那么我们的Controller就可以接收并处理了。 要注意，提交的username参数和password参数不可以是null，即一定要传这两个参数，不然会抛异常。 另外，@RequestParam可以省略参数名，那么就会以它注释的变量名作为参数名。 2.5 ModelAttribute @ModelAttribute一个具有如下三个作用： ①绑定请求参数到命令对象：放在功能处理方法的入参上时，用于将多个请求参数绑定到一个命令对象，从而简化绑定流程，而且自动暴露为模型数据用于视图页面展示时使用； ②暴露表单引用对象为模型数据：放在处理器的一般方法（非功能处理方法）上时，是为表单准备要展示的表单引用对象，如注册时需要选择的所在城市等，而且在执行功能处理方法（@RequestMapping注解的方法）之前，自动添加到模型对象中，用于视图页面展示时使用； ③暴露@RequestMapping方法返回值为模型数据：放在功能处理方法的返回值上时，是暴露功能处理方法的返回值为模型数据，用于视图页面展示时使用。 2.5.1 绑定请求参数到命令对象 如用户登录，我们需要捕获用户登录的请求参数（用户名、密码）并封装为用户对象，此时我们可以使用@ModelAttribute绑定多个请求参数到我们的命令对象。 public String test1(@ModelAttribute("user") UserModel user) 只是此处多了一个注解@ModelAttribute("user")，它的作用是将该绑定的命令对象以“user”为名称添加到模型对象中供视图页面展示使用。我们此时可以在视图页面使用${user.username}来获取绑定的命令对象的属性。 绑定请求参数到命令对象支持对象图导航式的绑定，如请求参数包含“?username=zhang&password=123&workInfo.city=bj”自动绑定到user中的workInfo属性的city属性中。 @RequestMapping(value="/model2/{username}") public String test2(@ModelAttribute("model") DataBinderTestModel model) { URI模板变量也能自动绑定到命令对象中，当你请求的URL中包含“bool=yes&schooInfo.specialty=computer&hobbyList[ 2.5.2 暴露表单引用对象为模型数据 @ModelAttribute("cityList") public List<String> cityList() { return Arrays.asList("北京", "山东"); } 如上代码会在执行功能处理方法之前执行，并将其自动添加到模型对象中，在功能处理方法中调用Model 入参的containsAttribute("cityList")将会返回true。 @ModelAttribute("user") //① public UserModel getUser(@RequestParam(value="username", defaultValue="") String username) { //TODO 去数据库根据用户名查找用户对象 UserModel user = new UserModel(); user.setRealname("zhang"); return user; } 如你要修改用户资料时一般需要根据用户的编号/用户名查找用户来进行编辑，此时可以通过如上代码查找要编辑的用户。也可以进行一些默认值的处理。 @RequestMapping(value="/model1") //② public String test1(@ModelAttribute("user") UserModel user, Model model) 此处我们看到①和②有同名的命令对象，那Spring Web MVC内部如何处理的呢： 首先执行@ModelAttribute注解的方法，准备视图展示时所需要的模型数据；@ModelAttribute注解方法形式参数规则和@RequestMapping规则一样，如可以有@RequestParam等； 执行@RequestMapping注解方法，进行模型绑定时首先查找模型数据中是否含有同名对象，如果有直接使用，如果没有通过反射创建一个，因此②处的user将使用①处返回的命令对象。即②处的user等于①处的user。 2.5.3 暴露@RequestMapping方法返回值为模型数据 public @ModelAttribute("user2") UserModel test3(@ModelAttribute("user2") UserModel user) 大家可以看到返回值类型是命令对象类型，而且通过@ModelAttribute("user2")注解，此时会暴露返回值到模型数据（名字为user2）中供视图展示使用。那哪个视图应该展示呢？此时Spring Web MVC会根据RequestToViewNameTranslator进行逻辑视图名的翻译。 此时又有问题了，@RequestMapping注解方法的入参user暴露到模型数据中的名字也是user2，其实我们能猜到： @ModelAttribute注解的返回值会覆盖@RequestMapping注解方法中的@ModelAttribute注解的同名命令对象。 2.5.4 匿名绑定命令参数 public String test4(@ModelAttribute UserModel user, Model model) 或 public String test5(UserModel user, Model model) 此时我们没有为命令对象提供暴露到模型数据中的名字，此时的名字是什么呢？Spring Web MVC自动将简单类名（首字母小写）作为名字暴露，如“cn.javass.chapter6.model.UserModel”暴露的名字为“userModel”。 public @ModelAttribute List<String> test6() 或 public @ModelAttribute List<UserModel> test7() 对于集合类型（Collection接口的实现者们，包括数组），生成的模型对象属性名为“简单类名（首字母小写）”+“List”，如List生成的模型对象属性名为“stringList”，List生成的模型对象属性名为“userModelList”。 其他情况一律都是使用简单类名（首字母小写）作为模型对象属性名，如Map<String, UserModel>类型的模型对象属性名为“map”。 3.前后台交互 3.1 通过注释ModelAttribute来进行。这部分已经在上文介绍过。 3.2 方法返回参数 在低版本的 Spring MVC 中，请求处理方法的返回值类型都必须是 ModelAndView。而在 Spring 2.5 中，你拥有多种灵活的选择。通过下表进行说明： void： 此时逻辑视图名由请求处理方法对应的 URL 确定，如以下的方法： @RequestMapping("/welcome.do") public void welcomeHandler() { } 对应的逻辑视图名为“welcome” String： 此时逻辑视图名为返回的字符，如以下的方法： @RequestMapping(method = RequestMethod.GET) public String setupForm(@RequestParam("ownerId") int ownerId, ModelMap model) { Owner owner = this.clinic.loadOwner(ownerId); model.addAttribute(owner); return "ownerForm"; } 对应的逻辑视图名为“ownerForm” org.springframework.ui.ModelMap： 和返回类型为 void 一样，逻辑视图名取决于对应请求的 URL，如下面的例子： @RequestMapping("/vets.do") public ModelMap vetsHandler() { return new ModelMap(this.clinic.getVets()); } 对应的逻辑视图名为“vets”，返回的 ModelMap 将被作为请求对应的模型对象，可以在 JSP 视图页面中访问到。 ModelAndView： 应该说使用 String 作为请求处理方法的返回值类型是比较通用的方法，这样返回的逻辑视图名不会和请求 URL 绑定，具有很大的灵活性，而模型数据又可以通过 ModelMap 控制。当然直接使用传统的 ModelAndView 也不失为一个好的选择。 3.3 按契约绑定URL 参数 Controller 的方法标注了 @RequestMapping 注解后，它就能处理特定的 URL 请求。我们不禁要问：请求处理方法入参是如何绑定 URL 参数的呢？在回答这个问题之前先来看下面的代码： @RequestMapping(params = "method=listBoardTopic") //<—— ① topicId入参是如何绑定URL请求参数的？ public String listBoardTopic(int topicId) { bbtForumService.getBoardTopics(topicId); System.out.println("call listBoardTopic method."); return "listTopic"; } 当我们发送 http://localhost//bbtForum.do?method=listBoardTopic&topicId=10 的 URL 请求时，Spring 不但让 listBoardTopic() 方法处理这个请求，而且还将 topicId 请求参数在类型转换后绑定到 listBoardTopic() 方法的 topicId 入参上。而 listBoardTopic() 方法的返回类型是 String，它将被解析为逻辑视图的名称。也就是说 Spring 在如何给处理方法入参自动赋值以及如何将处理方法返回值转化为 ModelAndView 中的过程中存在一套潜在的规则，不熟悉这个规则就不可能很好地开发基于注解的请求处理方法，因此了解这个潜在规则无疑成为理解 Spring MVC 框架基于注解功能的核心问题。 我们不妨从最常见的开始说起：请求处理方法入参的类型可以是 Java 基本数据类型或 String 类型，这时方法入参按参数名匹配的原则绑定到 URL 请求参数，同时还自动完成 String 类型的 URL 请求参数到请求处理方法参数类型的转换。下面给出几个例子： listBoardTopic(int topicId)：和 topicId URL 请求参数绑定； listBoardTopic(int topicId,String boardName)：分别和 topicId、boardName URL 请求参数绑定； 特别的，如果入参是基本数据类型（如 int、long、float 等），URL 请求参数中一定要有对应的参数，否则将抛出 TypeMismatchException 异常，提示无法将 null 转换为基本数据类型。 另外，请求处理方法的入参也可以一个 JavaBean，如下面的 User 对象就可以作为一个入参： User.java：一个 JavaBean package com.baobaotao.web; public class User { private int userId; private String userName; //省略get/setter方法 public String toString(){ return this.userName +","+this.userId; } } 下面是将 User 作为 listBoardTopic() 请求处理方法的入参： 使用 JavaBean 作为请求处理方法的入参 @RequestMapping(params = "method=listBoardTopic") public String listBoardTopic(int topicId,User user) { bbtForumService.getBoardTopics(topicId); System.out.println("topicId:"+topicId); System.out.println("user:"+user); System.out.println("call listBoardTopic method."); return "listTopic"; } 这时，如果我们使用以下的 URL 请求：http://localhost/bbtForum.do?method=listBoardTopic&topicId=1&userId=10&userName=tom topicId URL 参数将绑定到 topicId 入参上，而 userId 和 userName URL 参数将绑定到 user 对象的 userId 和 userName 属性中。和 URL 请求中不允许没有 topicId 参数不同，虽然 User 的 userId 属性的类型是基本数据类型，但如果 URL 中不存在 userId 参数，Spring 也不会报错，此时 user.userId 值为 0。如果 User 对象拥有一个 dept.deptId 的级联属性，那么它将和 dept.deptId URL 参数绑定。 3.3 通过注解指定绑定 URL 参数 如果我们想改变这种默认的按名称匹配的策略，比如让 listBoardTopic(int topicId,User user) 中的 topicId 绑定到 id 这个 URL 参数，那么可以通过对入参使用 @RequestParam 注解来达到目的： package com.baobaotao.web; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; @Controller @RequestMapping("/bbtForum.do") public class BbtForumController { @RequestMapping(params = "method=listBoardTopic") public String listBoardTopic(@RequestParam("id") int topicId,User user) { bbtForumService.getBoardTopics(topicId); System.out.println("topicId:"+topicId); System.out.println("user:"+user); System.out.println("call listBoardTopic method."); return "listTopic"; } } 这里，对 listBoardTopic() 请求处理方法的 topicId 入参标注了 @RequestParam("id") 注解，所以它将和 id 的 URL 参数绑定。 3.4 绑定模型对象中某个属性 Spring 2.0 定义了一个 org.springframework.ui.ModelMap 类，它作为通用的模型数据承载对象，传递数据供视图所用。我们可以在请求处理方法中声明一个 ModelMap 类型的入参，Spring 会将本次请求模型对象引用通过该入参传递进来，这样就可以在请求处理方法内部访问模型对象了。来看下面的例子： 使用 ModelMap 访问请示对应的隐含模型对象 @RequestMapping(params = "method=listBoardTopic") public String listBoardTopic(@RequestParam("id")int topicId,User user,ModelMap model) { bbtForumService.getBoardTopics(topicId); System.out.println("topicId:" + topicId); System.out.println("user:" + user); //① 将user对象以currUser为键放入到model中 model.addAttribute("currUser",user); return "listTopic"; } 对于当次请求所对应的模型对象来说，其所有属性都将存放到 request 的属性列表中。象上面的例子，ModelMap 中的 currUser 属性将放到 request 的属性列表中，所以可以在 JSP 视图页面中通过 request.getAttribute(“currUser”) 或者通过 ${currUser} EL 表达式访问模型对象中的 user 对象。从这个角度上看， ModelMap 相当于是一个向 request 属性列表中添加对象的一条管道，借由 ModelMap 对象的支持，我们可以在一个不依赖 Servlet API 的 Controller 中向 request 中添加属性。 在默认情况下，ModelMap 中的属性作用域是 request 级别是，也就是说，当本次请求结束后，ModelMap 中的属性将销毁。如果希望在多个请求中共享 ModelMap 中的属性，必须将其属性转存到 session 中，这样 ModelMap 的属性才可以被跨请求访问。
前言 Apache Shiro 是一个框架，可用于身份验证和授权。虽然这两个术语代表的是不同的含义，但出于它们在应用程序安全性方面各自的角色考虑，它们有时会被交换使用。 身份验证 指的是验证用户的身份。在验证用户身份时，需要确认用户的身份的确如他们所声称的那样。在大多数应用程序中，身份验证是通过用户名和密码的组合完成的。只要用户选择了他人很难猜到的密码，那么用户名和密码的组合通常就足以确立身份。但是，还有其他的身份验证方式可用，比如指纹、证书和生成键。 一旦身份验证过程成功地建立起身份，授权 就会接管以便进行访问的限制或允许。 所以，有这样的可能性：用户虽然通过了身份验证可以登录到一个系统，但是未经过授权，不准做任何事情。还有一种可能是用户虽然具有了某种程度的授权，却并未经过身份验证。 在为应用程序规划安全性模型时，必须处理好这两个元素以确保系统具有足够的安全性。身份验证是应用程序常见的问题（特别是在只有用户和密码组合的情况下），所以让框架来处理这项工作是一个很好的做法。合理的框架可提供经过测试和维护的优势，让您可以集中精力处理业务问题，而不是解决其解决方案已经实现的问题。 Apache Shiro 提供了一个可用的安全性框架，各种客户机都可将这个框架应用于它们的应用程序。本文中的这些例子旨在介绍 Shiro 并着重展示对用户进行身份验证的基本任务。 本文只针对Jeesite中shiro的用法进行整理，不会包括shiro环境配置和搭建等内容。 Jeesite中的shiro 2.1 spring-context-shiro.xml spring-context-shiro.xml是shiro的主配置文件，配置信息是重点主要是安全认证过滤器的配置。它规定哪些url需要进行哪些方面的认证和过滤 <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean"> <property name="securityManager" ref="securityManager" /> <property name="p" 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> shiroFilter是shiro的安全认证过滤器，其中， securityManager：指定一个负责管理的bean，这个新的bean在接下来会定义，其中包含了认证的主要逻辑。 loginUrl：没有登录的用户请求需要登录的页面时自动跳转到登录页面，不是必须的属性，不输入地址的话会自动寻找项目web项目的根目录下的”/login.jsp”页面。 successUrl：登录成功默认跳转页面，不配置则跳转至”/”。如果登陆前点击的一个需要登录的页面，则在登录自动跳转到那个需要登录的页面。不跳转到此。 unauthorizedUrl：没有权限默认跳转的页面。 map中的entry指定了authc权限所对应的过滤器实体 而属性中的filterChainDefinitions则详细规定啦不同的url的对应权限 anon:例子/admins/**=anon 没有参数，表示可以匿名使用。 authc:例如/admins/user/**=authc表示需要认证(登录)才能使用，没有参数 roles：例子/admins/user/=roles[admin],参数可以写多个，多个时必须加上引号，并且参数之间用逗号分割，当有多个参数时，例如admins/user/=roles["admin,guest"],每个参数通过才算通过，相当于hasAllRoles()方法。 perms：例子/admins/user/**=perms[user:add:],参数可以写多个，多个时必须加上引号，并且参数之间用逗号分割，例如/admins/user/=perms["user:add:,user:modify:*"]，当有多个参数时必须每个参数都通过才通过，想当于isPermitedAll()方法。 rest：例子/admins/user/=rest[user],根据请求的方法，相当于/admins/user/=perms[user:method] ,其中method为post，get，delete等。 port：例子/admins/user/**=port[8081],当请求的url的端口不是8081是跳转到schemal://serverName:8081?queryString,其中schmal是协议http或https等，serverName是你访问的host,8081是url配置里port的端口，queryString 是你访问的url里的？后面的参数。 authcBasic：例如/admins/user/**=authcBasic没有参数表示httpBasic认证 ssl:例子/admins/user/**=ssl没有参数，表示安全的url请求，协议为https user:例如/admins/user/**=user没有参数表示必须存在用户，当登入操作时不做检查 注：anon，authcBasic，auchc，user是认证过滤器，perms，roles，ssl，rest，port是授权过滤器 <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager"> <!-- <property name="sessionManager" ref="sessionManager" /> --> <property name="realm" ref="systemAuthorizingRealm" /> <property name="cacheManager" ref="shiroCacheManager" /> </bean> 这部分代码定义了securitymanager的主要属性的实体，systemAuthorizingRealm和shiroCacheManager都是自己实现的，分别用于进行验证管理和cache管理。从配置文件能看出，配置文件指定了作为安全逻辑的几个结构，除了这两部分，还包括formAuthenticationFilter。其中realm和filter在com.thinkgem.jeesite.modules.sys.security包里实现。 2.2 FormAuthenticationFilter 从可见都逻辑上讲，FormAuthenticationFilter类是用户验证时所接触的第一个类。 当用户登录任意界面时，shiro会对当前状态进行检查。如果发现需要登录，则会自动跳转到配置文件里loginUrl属性所指定的url中。 而这一url又被指定为authc权限，即需要验证。接着，authc的filter被指定为formAuthenticationFilter，因此login页面所提交的信息被改filter截获进行处理。 其中的核心逻辑是createToken函数： protected AuthenticationToken createToken(S2ervletRequest 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); } UsernamePasswordToken是security包里的第四个类，它继承自shiro的同名类，用于shiro处理中的参数传递。createtoken函数接受到login网页所接受的表单，生成一个token传给下一个类处理。 函数中的rememberme以及captcha分别表示的是记住用户功能和验证码功能，这部分也是shiro自身携带，我们并不需要修改。filter是通过aop的方式结合到系统里的，因此并没有具体的接口实现。 2.3 SystemAuthorizingRealm shiro的最终处理都将交给Real进行处理。因为在Shiro中，最终是通过Realm来获取应用程序中的用户、角色及权限信息的。通常情况下，在Realm中会直接从我们的数据源中获取Shiro需要的验证信息。可以说，Realm是专用于安全框架的DAO. Realm中有个参数是systemService，这个便是spring的具体业务逻辑，其中也包含了具体的DAO，正是在这个部分，shiro与spring的接口具体的结合了起来。 realm当中有两个函数特别重要，分别是用户认证函数和授权函数。 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; } } 以上便是用户认证函数，中间关于验证码检测的部分我们暂且不表，其最核心的语句是 User user = getSystemService().getUserByLoginName(token.getUsername()); 以及另一句语句 return new SimpleAuthenticationInfo(new Principal(user), user.getPassword().substring(16), ByteSource.Util.bytes(salt), getName()); 函数的参数AuthenticationToken便是filter所截获并生成的token实例，其内容是login表单里的内容，通常是用户名和密码。在前一句话里，getSystemService取到了systemService实例，该实例中又含有配置好的userDAO，能够直接与数据库交互。因此将用户id传入，取出数据库里所存有的user实例，接着在SimpleAuthenticationInfo生成过程中分别以user实例的用户名密码，以及token里的用户名密码做为参数，验证密码是否相同，以达到验证的目的。 doGetAuthorizationInfo函数是授权的函数，其具体的权限是在实现类中以annotation的形式指派的，它负责验证用户是否有权限访问。详细的细节容我之后添加。 2.3 LoginController LoginController是本该实现登录认证的部分。由于shiro的引入和AOP的使用，jeesite中的LoginController只处理验证之后的部分。 如果通过验证，系统中存在user实例，则返回对应的主页。否则重新定位于login页面。 2.4 annotation 常用的annotation主要如下： @RequiresAuthentication 要求当前Subject 已经在当前的session 中被验证通过才能被注解的类/实例/方法访问或调用。 验证用户是否登录，等同于方法subject.isAuthenticated() 结果为true时。 @RequiresUser 需要当前的Subject 是一个应用程序用户才能被注解的类/实例/方法访问或调用。要么是通过验证被确认，或者在之前session 中的'RememberMe'服务被记住。 验证用户是否被记忆，user有两种含义：一种是成功登录的（subject.isAuthenticated() 结果为true）；另外一种是被记忆的（subject.isRemembered()结果为true）。 @RequiresGuest 要求当前的Subject 是一个“guest”，也就是他们必须是在之前的session中没有被验证或记住才能被注解的类/实例/方法访问或调用。 验证是否是一个guest的请求，与@RequiresUser完全相反。 换言之，RequiresUser == !RequiresGuest。此时subject.getPrincipal() 结果为null. @RequiresRoles 要求当前的Subject 拥有所有指定的角色。如果他们没有，则该方法将不会被执行，而且AuthorizationException 异常将会被抛出。例如：@RequiresRoles("administrator") 或者@RequiresRoles("aRoleName"); void someMethod(); 如果subject中有aRoleName角色才可以访问方法someMethod。如果没有这个权限则会抛出异常AuthorizationException。 @RequiresPermissions 要求当前的Subject 被允许一个或多个权限，以便执行注解的方法，比如： @RequiresPermissions("account:create") 或者@RequiresPermissions({"file:read", "write:aFile.txt"} ) void someMethod(); 3.相关网页 有一个简易的整体流程实现 http://blog.csdn.net/clj198606061111/article/details/24185023 对授权部分解释较为详细 http://sishuok.com/forum/posts/list/7456.html
2.1.4 运行bin\eclipse.bat脚本： 直接在jeesite根目录运行以下命令： mvn -Declipse.workspace=. eclipse:clean eclipse:eclipse 2.3.3 根据修改参数创建对应数据库（数据库编码：UTF-8)。 在mysql中自己手动创建jeesite，使用命令行： create database jeesite; 5. 运行db\init-db.bat脚本，即可导入表结构及演示数据。 直接在jeesite根目录下运行以下命令： mvn antrun:run -Pinit-db 关于tomcat的两个问题： 由于这个项目还算比较大，导致tomcat内存很快被耗完，就会产生内存崩溃，服务器失去响应，解决方案如下：eclipse配置选项,打开选项,输入tomcat,然后点开Server下面的JDK就会出现配置区域. 直接加入: -Xms2048m -Xmx2048m 参考网址：http://www.haogongju.net/art/1910909 在eclipse中启动tomcat，在浏览器中输入localhost:8080找不到页面，解决方案如下： a).双击tomcat服务器，会弹出如下界面，如果tomcat上已经部署了项目，第一个红框是灰色的不能改变的，需要右击tomcat，选择Add and Remove，remove掉tomcat上所有的项目，如果依然不能选择，则右击tomcat，Clean，之后就可以选择。 b).修改两个地方，修改成下面这个样子： 参考网址: http://blog.csdn.net/ji_ju/article/details/8545588 关于在mac下不能mvn install成功地原因： log4j的配置路径需要修改： 在log4j.properties中修改： junit java.lang.IllegalStateException: Failed to load ApplicationContext错误：因为测试基准类的读取文件名不正确， BaseDaoTest extends SpringTransactionalContextTests，SpringTransactionalContextTests类中的读取，由 @ContextConfiguration(locations = {"/applicationContext.xml", "/applicationContext-shiro.xml”}) 修改为： @ContextConfiguration(locations = {"/spring-context.xml", "/spring-context-shiro.xml”}) 原因: 项目中重命名了这两个配置文件，但是在读取时却没有更改。 关于运行maven install时，运行测试的乱码问题： 在pom中添加以下代码： <plugin> <groupId>org.apache.maven.plugins\</groupId> <artifactId>maven-surefire-plugin\</artifactId> <version>2.7.2\</version> <configuration> <forkMode>once\</forkMode> <argLine>-Dfile.encoding=UTF-8\</argLine> </configuration> </plugin>