JSPXCMS架构介绍
JSPXCMS开发架构介绍
V1 – 架构概述
基本概述
配置文件目录
/src/main/resources/conf/plugin/plug
有以下文件:
backend.xml:后台Controller配置文件
conf.properties:前台模板Freemarker标签、国际化文件目录等配置文件
context.xml:Spring配置文件
menu.yml:后台菜单及权限配置文件
源代码目录
/src/main/java/com/jspxcms/plug
有以下包:
domain:实体类Entity
repository:数据库操作类Dao
service:服务类Service
web.back:后台Controller
web.fore:前台Controller
web.directive:FreeMarker标签类
后台JSP文件
/src/main/webapp/WEB-INF/views/plug
前台模板文件
/src/main/webapp/template/1/default/
国际化文件
/src/main/resources/messages/plugin/plug 后台国际化文件
/src/main/resources/messages/plugin/plugfore 前台国际化文件
编辑和查看国际化文件,请安装Eclipse的Properties Editor插件。否则不能看到中文,只能看到\u5217\u8868之类的代码;并且在编辑时直接输入中文,页面会显示为乱码。
源代码目录结构详解
webapp目录
jsp jsp文件。如果需要可以直接访问的jsp页面,可以放在这个文件夹下,放到其他文件夹下的jsp是无法直接访问的。访问路径不需要加上jsp路径,例如/jsp/abc.jsp文件的访问路径为abc.jsp。
static 静态资源文件。
css
img
js
vendor 第三方组件库。如jquery、bootstrop、ueditor、ztree、My97DatePicker等。
template 前台FreeMarker模板。
uploads 文件上传目录。
WEB-INF
fulltext Lucene全文检索文件目录。
tags 后台jsp标签。
tlds JSTL functions。
views 后台jsp页面。
commons 部分公用jsp页面。
core 核心模块jsp页面。
error 发生异常时显示的jsp页面。
ext 扩展模块jsp页面。
plug 插件模块jsp页面。
index.jsp 后台首页框架页。
login.jsp 后台登录页面。
weblogic.xml 用于部署在weblogic。
crossdomain.xml 跨域策略文件。主要针对flash。
favicon.ico 浏览器头部图标。
Java源码
com.jspxcms.common 公用组件。
captcha 验证码。
file 文件相关。
freemarker
fulltext 全文索引。
image 图片相关。
ip 通过IP地址查询实际地址。
office word转html。
orm 对象关系映射。JPA及SpringDataJPA相关辅助类。
security 安全相关。如密码加密等。
upload 上传相关。
util 工具类。
web SpringMVC等web相关类。
com.jspxcms.core 核心模块。
constant 静态变量。
domain 实体类。
fulltext 全文索引。
holder
html 生成静态页。
listener 监听器。
quartz 定时器。
repository 数据库持久化层。
security 安全相关。
service 服务层。
support 支持类。
web Controller层。
back 后台Controller。
directive FreeMarker标签。
fore 前台Controller。
method FreeMarker方法。
com.jspxcms.ext 扩展模块。
com.jspxcms.com 插件模块。
Resources目录
源码包的resources目录在/src/main/resources,安装包里的resources目录在/WEB-INF/classes。
conf 配置文件目录。
core 核心模块配置文件。
plugin 插件模块配置文件。
conf.properties 系统properties配置文件。
context.xml spring context配置文件。
context-quartz.xml 定时任务配置文件。
menu.yml 后台菜单配置文件。
spring.jpa.propertis spring jpa配置文件。
ehcache ehcache缓存配置文件。
messages 国际化文件
application.properties spring-boot配置文件。
config.properties 微博第三方登录配置。
custom.xml 验证码、全文索引配置文件。
IKAnalyzer.cfg.xml IK Analyzer配置文件。
qqconnectconfig.properties QQ第三方登录配置文件。
qqwry.dat IP地址数据库。
quartz.properties 定时任务配置文件。
stopword.dic IK Analyzer停止词文件。
stopword_ext.dic IK Analyzer停止词扩展文件。
weixin.properties 微信配置文件。
V2 – 配置文件介绍
二次开发的核心就是配置文件,通过配置文件将不同模块和插件整合到一起。可以在这个目录下新建自己的文件夹,如:abc、novel等,本例为plug,即/src/main/resources/conf/plugin/plug
Spring配置自动加载
配置文件目录及其子目录下文件名为context*.xml的配置文件会自动加载为Spring的WebApplicationContext。
触发自动加载的类是com.jspxcms.core.Application,相关代码为:@ImportResource({"classpath:conf/**/context*.xml",
"classpath:custom.xml" })。
本例的配置文件/src/main/resources/conf/plugin/plug/context.xml,符合自动加载规则,文件中
class="com.jspxcms.plug.ContextConfig" />会加载该类中的配置。在这个类里分别加载了:
Entity:@EntityScan({
"com.jspxcms.plug.domain" })
Dao:@EnableJpaRepositories(basePackages
= { "com.jspxcms.plug.repository" }, repositoryFactoryBeanClass
= MyJpaRepositoryFactoryBean.class)
Service和前台Controller:@ComponentScan({
"com.jspxcms.plug.service.impl",
"com.jspxcms.plug.web.fore" })
后台Controller配置自动加载
配置文件目录及其子目录下文件名为backend*.xml的配置文件会自动加载为后台Controller
配置文件目录及其子目录下文件名为backend*.xml的配置文件会自动加载为后台Controller。
触发自动加载的类是com.jspxcms.core.BackendWebConfig,相关代码为:@ImportResource({ "classpath:conf/**/backend*.xml"
})。
本例的配置文件/src/main/resources/conf/plugin/plug/backend.xml符合自动加载规则。
<context:component-scanbase-package="com.jspxcms.plug.web.back"use-default-filters="false">
<context:include-filtertype="annotation"expression="org.springframework.stereotype.Controller"/>
<context:include-filtertype="annotation"expression="org.springframework.web.bind.annotation.ControllerAdvice"/>
context:component-scan>
这个配置会自动加载com.jspxcms.plug.web.back包下所有带有Controller或ControllerAdvice注解的类。
conf.properties自动加载
前台模板Freemarker标签、国际化文件目录等配置在conf.properties中配置。
配置文件目录及其子目录下文件名为conf*.properties的文件会自动加载。
触发自动加载的类是com.jspxcms.core.ShiroConfig,相关代码为:
loader.setValue("classpath:conf/plugin/**/conf*.properties","classpath:conf/conf.properties");。
本例的配置文件/src/main/resources/conf/plugin/plug/conf.properties,符合自动加载规则。
menu.yml自动加载
后台功能菜单及权限由该文件配置。
配置文件目录及其子目录下文件名为menu*.yml的文件会自动加载。
触发自动加载的类是com.jspxcms.core.MenuConfig,相关代码为:appContext.getResources("classpath:conf/**/menu*.yml");
本例的配置文件/src/main/resources/conf/plugin/plug/menu.properties,符合自动加载规则。
V3 - 菜单与权限
Jspxcms的菜单和权限信息存放在配置中,方便管理、维护和升级;只需要在一个配置文件中设置好,即可以无缝整合系统的菜单、权限、赋权等问题,无需另外修改代码和页面。
开发时可以根据需要,将新功能菜单加到任意的一级菜单下的任意位置,也可以自己新增一级菜单,新增的一级菜单也可以放在任何你想要的位置上。
菜单介绍
后台菜单分为两级,如内容管理、文件管理、用户权限、系统管理为一级菜单,文档管理、栏目管理、用户管理、角色管理为二级菜单。
后台菜单目前不支持三级。
配置文件
/src/main/resources/conf/plugin/plug/menu.yml
菜单配置文件支持通配加载,符合classpath:conf/**/menu*.yml这个规则的配置文件都会加载,系统中其它菜单的配置位于/src/main/resources/conf/menu.yml。
加载菜单配置文件由com.jspxcms.core.MenuConfig的appContext.getResources("classpath:conf/**/menu*.yml");代码加载
一级菜单配置
在/src/main/resources/conf/menu.yml文件中有一级菜单的配置。
600:
name: navigation.plug
icon: fa fa-plug
perms: nav_plug
600: 菜单序号,决定一级菜单排列的位置。比如序号500的菜单会排在600的前面,而序号550则会排在500与600之间。注意:这个序号在所有的菜单配置文件中必须是唯一的,如果已经有600序号的菜单,则不能再建一个同样序号的菜单,可以为610或者558。
name: navigation.plug 菜单名称。其中navigation.plug是国际化代码,在/src/main/resources/messages目录下的文件里有这个代码对应的中文。也可以直接写中文,如name: 我的菜单。
icon: fa fa-plug 菜单图标。其中fa fa-plug是图标样式,请参考:https://getbootstrap.com/docs/3.3/components/。
perms: nav_plug 权限值。一级菜单通常都是虚拟菜单,点击后只是展开二级菜单,不需要访问后台,所以权限值可以随意定义,只要不和原有权限值重复即可。
二级菜单配置
在/src/main/resources/conf/plugin/plug/menu.yml文件中有二级菜单的配置。
600-1000:
name:resume.management
url: plug/resume/list.do
perms: plug:resume:list
ops:
- create@plug:resume:create
- copy@plug:resume:copy
- edit@plug:resume:edit
- save@plug:resume:save
- update@plug:resume:update
- delete@plug:resume:delete
600-1100:
name:weixinMenu.management
url: plug/weixin_menu/list.do
perms: plug:weixin_menu:list
ops:
- save@plug:weixin_menu:save
600-1000 菜单序号。此处为两级,所以是二级菜单,其中600是一级菜单的序号,代表着二级菜单是属于序号为600的一级菜单之下。1000是二级菜单的序号,决定二级菜单排列的位置,这个序号在所属的一级菜单里必须是唯一的。注意:对应的一级菜单必须存在。
name: resume.management 菜单名称。与一级菜单相同。
url: plug/resume/list.do 菜单URL地址。点击这个菜单所访问的url。这是一个相对路径,以/cmscp/为基础路径,所以这个地址实际上是/cmscp/plug/resume/list.do。这个url地址必须要有相应Controller,否则点击这个菜单会找不到页面。如@RequestMapping("/plug/resume")和@RequestMapping("list.do")。
perms: plug:resume:list 菜单权限值。这个权限值必须要对应url的Controller方法的@RequiresPermissions("plug:resume:list")一致。
ops: 按钮权限列表。一个模块除了点击菜单的权限,还有其他权限,比如点击简历管理这个菜单链接后,里面还有新增简历、修改简历、删除简历等功能按钮。
create@plug:resume:create 按钮权限值。create是国际化代码。plug:resume:create是权限值,必须与Controller方法中的@RequiresPermissions("plug:resume:create")一致。也可以直接用中文,如新增@plug:resume:create。
对应的Java代码
packagecom.jspxcms.plug.web.back;
@Controller
@RequestMapping("/plug/resume")
public class ResumeController {
@RequiresPermissions("plug:resume:list")
@RequestMapping("list.do")
public String list(...) {
...
return "plug/resume/resume_list";
}
@RequiresPermissions("plug:resume:create")
@RequestMapping("create.do")
public String create(...) {
...
return "plug/resume/resume_form";
}
...
}
菜单配置与权限管理
按照上面步骤配置菜单之后,角色管理的功能权限树会读取配置文件,无需修改角色管理页面及代码。
V4-Controller
在Jspxcms中,Controller分为前台和后台。前台是普通用户浏览的页面,使用freemarker作为视图,通常不需要登录,比如网站首页、栏目页、专题页、搜索页等;后台一般为管理功能,使用JSP作为视图,需要管理员登录后台并且有相应权限,才能访问。
后台Controller配置
/src/main/resources/conf/plugin/plug/backend.xml
符合classpath:conf/**/backend*.xml这个规则的文件会加载为后台Controller的配置文件。
<context:component-scan base-package="com.jspxcms.plug.web.back" use-default-filters="false">
<context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
<context:include-filter type="annotation" expression="org.springframework.web.bind.annotation.ControllerAdvice"/>
context:component-scan>
这个配置会自动加载com.jspxcms.plug.web.back包下所有带有Controller或ControllerAdvice注解的类。
后台Controller返回JSP页面,JSP路径的前后缀分别为/WEB-INF/views/、.jsp,相应的配置文件是/src/main/resources/application.properties。
# 后台JSP地址前缀
spring.mvc.view.prefix=/WEB-INF/views/
# 后台JSP地址后缀
spring.mvc.view.suffix=.jsp
后台Controller类
后台访问地址以/cmscp为根路径,@RequestMapping("/plug/resume")和@RequestMapping("list.do")配置的最终访问地址为/cmscp/plug/resume/list.do。
/cmscp路径由com.jspxcms.core.Application中的new
ServletRegistrationBean(backendDispatcherServlet(), "/cmscp/*");代码设定。
后台Controller返回JSP页面,如plug/resume/resume_list,加上前后缀,实际文件地址为/WEB-INF/views/plug/resume/resume_list.jsp
packagecom.jspxcms.plug.web.back;
@Controller
@RequestMapping("/plug/resume")
public class ResumeController {
@RequiresPermissions("plug:resume:list")
@RequestMapping("list.do")
public String list(...) {
...
return "plug/resume/resume_list";
}
@RequiresPermissions("plug:resume:create")
@RequestMapping("create.do")
public String create(...) {
...
return "plug/resume/resume_form";
}
...
}
前台Controller配置
com.jspxcms.plug.ContextConfig中的@ComponentScan({“com.jspxcms.plug.web.fore”})会加载com.jspxcms.plug.web.fore包中的@Controller类.
前台Controller返回的视图是FreeMarker,有关FreeMarker的配置在src/main/resources/context.xml。
其中templateLoaderPath是模板存储路径,也就是模板前缀,默认为/template。
<bean id="freeMarkerViewResolver" class="com.jspxcms.common.freemarker.MyFreeMarkerViewResolver">
<property name="contentType" value="text/html; charset=UTF-8"/>
<property name="cacheUnresolved" value="false"/>
<property name="redirectHttp10Compatible" value="false"/>
bean>
<bean id="freemarkerConfig" class="com.jspxcms.common.freemarker.AdapterFreeMarkerConfigurer">
<property name="freemarkerVariables" value="#{propertiesHelper.getBeanMap('freemarkerVariables.')}"/>
<property name="templateLoaderPath" value="${templateStorePath}"/>
<property name="freemarkerSettings">
<props>
<prop key="tag_syntax">square_bracketprop>
<prop key="template_update_delay">${freemarkerConfig.template_update_delay}prop>
<prop key="defaultEncoding">UTF-8prop>
<prop key="url_escaping_charset">UTF-8prop>
<prop key="localized_lookup">falseprop>
<prop key="locale">zh_CNprop>
<prop key="boolean_format">true,falseprop>
<prop key="datetime_format">yyyy-MM-dd'T'HH:mm:ssprop>
<prop key="date_format">yyyy-MM-ddprop>
<prop key="time_format">HH:mm:ssprop>
<prop key="number_format">0.###prop>
<prop key="whitespace_stripping">trueprop>
<prop key="auto_import">/spring.ftl as sprop>
props>
property>
bean>
前台Controller类
前台访问地址直接以网站根路径为相对路径,@RequestMapping(value =
"/resume")配置的访问地址就为/resume。
前台返回的FreeMarker模板路径,一般返回当前站点的模板路径,如/1/default/plug_resume.html,加上模板前缀,实际地址是/template/1/default/plug_resume.html;
packagecom.jspxcms.plug.web.fore;
@Controller
public class ResumeController {
public static final String TEMPLATE = "plug_resume.html";
@RequestMapping(value = "/resume")
public String form(HttpServletRequest request, org.springframework.ui.Model modelMap) {
...
// 将通用对象放到modelMap里,如ctx dy user site global等
Map data = modelMap.asMap();
ForeContext.setData(data, request);
// 获得当前站点对象
Site site = Context.getCurrentSite();
// 返回当前站点模板路径。如:/1/default/plug_resume.html。加上模板前缀,实际地址是 /template/1/default/plug_resume.html
returnsite.getTemplate(TEMPLATE);
}
@RequestMapping(value = "/resume", method = RequestMethod.POST)
public String submit(...) {
...
}
}
V5-Entity
配置文件
com.jspxcms.plug.ContextConfig的@EntityScan({
"com.jspxcms.plug.domain" })会自动扫描该包下含有@Entity注解的类。
数据库表
不使用主键自增策略,而是使用JPA的TABLE主键生成策略,将主键放到数据库中的一个表里,这个表在Hibernate里默认为Hibernate_sequences。所以在建表的时候不要使用主键自增。
create tableplug_resume
(
f_resume_id int not null,
f_site_id int not null,
f_name varchar(100) not null comment '姓名',
f_post varchar(100) not null comment '应聘职位',
f_creation_date datetimenot null comment '投递日期',
f_gender char(1) not null default 'M' comment '性别',
f_birth_date datetimecomment '出生日期',
f_mobile varchar(100) comment '手机',
f_email varchar(100) comment '邮箱',
f_expected_salary int comment '期望薪水',
f_education_experience longtextcomment '教育经历',
f_work_experience longtextcomment '工作经历',
f_remark longtextcomment '备注',
primarykey(f_resume_id)
)
engine = innodb;
alter table plug_resume comment '简历表';
alter table plug_resume add constraint fk_plug_resume_site foreign key(f_site_id)
references cms_site (f_site_id) on delete restrict on update restrict;
实体类
使用JPA的TABLE主键生成策略。
需注意以下三个值:name = "tg_plug_resume",
pkColumnValue = "plug_resume" generator = "tg_plug_resume",其中plug_resume为表名,如果表名为abc,则这三个值分别为name =
"tg_abc", pkColumnValue = "abc" generator =
"tg_abc"。
initialValue = 1代表主键从1开始。allocationSize
= 10代表hibernate一次获取10个主键值,如果没有用完系统就重启了,那么在数据库中会出现主键不连续的情况。但由于获取主键值要查询并修改数据库,对于频繁插入数据的表来说,是一个很大的开销,所以可以根据情况适当调整这个值。
如果使用MySQL的主键自增,除了在表主键里增加主键自增属性,在Entity里的ID注解也要改为@GeneratedValue(
generation = IDENTITY )或@GeneratedValue(
generation = AUTO )。
packagecom.jspxcms.plug.domain;
@Entity
@Table(name = "plug_resume")
public class Resume implements java.io.Serializable {
privateInteger id;
……
@Id
@Column(name = "f_resume_id", unique = true, nullable = false)
@TableGenerator(name = "tg_plug_resume", pkColumnValue = "plug_resume", initialValue = 1, allocationSize = 10)
@GeneratedValue(strategy = GenerationType.TABLE, generator = "tg_plug_resume")
public Integer getId() {
return this.id;
}
public void setId(Integer id) {
this.id = id;
}
……
}
V6-DAO
配置文件
系统的DAO在com.jspxcms.core.ContextConfig的@EnableJpaRepositories(basePackages
= {"com.jspxcms.core.repository",
"com.jspxcms.ext.repository"}, repositoryFactoryBeanClass =
MyJpaRepositoryFactoryBean.class)配置。
本例的DAO在com.jspxcms.plug.ContextConfig的@EnableJpaRepositories(basePackages
= { "com.jspxcms.plug.repository" }, repositoryFactoryBeanClass =
MyJpaRepositoryFactoryBean.class)配置
DAO类
packagecom.jspxcms.plug.repository;
public interface ResumeDao extends Repository<Resume, Integer>, ResumeDaoPlus {
public Page findAll(Specification spec, Pageable pageable);
public List findAll(Specification spec, Limitable limitable);
public Resume findOne(Integer id);
public Resume save(Resume bean);
public void delete(Resume bean);
@Modifying
@Query("delete from Resume bean where bean.site.id in (?1)")
public int deleteBySiteId(Collection siteIds);
}
ResumeDao接口中的方法不用实现。以下接口中的方法均可放到ResumeDao,且无需实现:
org.springframework.data.repository.CrudRepository
org.springframework.data.repository.PagingAndSortingRepository
org.springframework.data.jpa.repository.JpaRepository
com.jspxcms.common.orm.MyJpaRepository
其中public
int deleteBySiteId(Collection siteIds);方法并不在这些接口中,则需要使用@Query("delete
from Resume bean where bean.site.id in (?1)")指定SQL语句,由于该SQL会修改数据库的数据,所以要加上@Modifying。
需要实现的dao方法,放到ResumeDaoPlus接口中。规则是在DAO类名后加上Plus。如DAO类名是AbcDao,则Plus的类名为AbcDaoPlus。
packagecom.jspxcms.plug.repository;
public interface ResumeDaoPlus {
public List getList(Integer[] siteId, Limitable limitable);
}
ResumeDaoPlus接口的实现类要放到对应的impl包中
packagecom.jspxcms.plug.repository.impl;
public class ResumeDaoImpl implements ResumeDaoPlus {
@SuppressWarnings("unchecked")
public List getList(Integer[] siteId, Limitable limitable) {
JpqlBuilder jpql =newJpqlBuilder();
jpql.append("from Resume bean where 1=1");
if(ArrayUtils.isNotEmpty(siteId)) {
jpql.append(" and bean.site.id in (:siteId)");
jpql.setParameter("siteId", Arrays.asList(siteId));
}
returnjpql.list(em, limitable);
}
privateEntityManager em;
@PersistenceContext
public void setEm(EntityManager em) {
this.em = em;
}
}
V7-Service
Service用于处理业务逻辑和调用DAO操作数据库。
配置文件
系统的Service在com.jspxcms.core.ContextConfig的@ComponentScan({"com.jspxcms.core.service.impl",
"com.jspxcms.ext.service.impl"})配置。
本例的Service在com.jspxcms.plug.ContextConfig的@ComponentScan({
"com.jspxcms.plug.service.impl"})配置。
Service类
package com.jspxcms.plug.service.impl;
@Service
@Transactional(readOnly = true)
public class ResumeServiceImpl implements ResumeService{
public Page findAll(Integer siteId, Map params,
Pageable pageable) {
return dao.findAll(spec(siteId, params), pageable);
}
public RowSide findSide(Integer siteId,Map params,
Resume bean, Integer position, Sort sort) {
if (position == null) {
return newRowSide();
}
Limitable limit = RowSide.limitable(position, sort);
List list = dao.findAll(spec(siteId,params), limit);
returnRowSide.create(list, bean);
}
private Specification spec(final Integer siteId,
Mapparams) {
Collection filters = SearchFilter.parse(params).values();
final Specification fsp = SearchFilter.spec(filters, Resume.class);
Specification sp =newSpecification() {
public Predicate toPredicate(Root root,
CriteriaQuery query, CriteriaBuilder cb) {
Predicate pred = fsp.toPredicate(root, query, cb);
if (siteId != null) {
pred = cb.and(pred, cb.equal(root.get("site")
.get("id"), siteId));
}
returnpred;
}
};
returnsp;
}
privateResumeDao dao;
@Autowired
public void setDao(ResumeDao dao) {
this.dao = dao;
}
}
该类使用到JPA的Specification查询方式。可实现后台列表点击表头,按任意列排序;列表页按任意字段查询;编辑页面上一条、下一条功能。
V8- FreeMarker标签
在前台模板使用FreeMarker标签获取数据。
标签类package com.jspxcms.plug.web.directive;
/**
* FreeMarker标签类需实现TemplateDirectiveModel接口
*/
public class ResumeListDirective implements TemplateDirectiveModel{
public static final String SITE_ID = "siteId";
public void execute(Environment env, Map params, TemplateModel[] loopVars,
TemplateDirectiveBody body) throws TemplateException, IOException {
// 使用标签时,返回变量必须存在,如[@ResumeList; result]...[/@ResumeList]中分号后的result。
if (loopVars.length < 1) {
throw new TemplateModelException("Loop variable is required.");
}
// 标签体必须存在,即[@ResumeList; list]...[/@ResumeList]中间的部分。
if (body == null) {
throw new RuntimeException("missing body");
}
// 获取标签参数,如[@ResumeList siteId='123'; list]...[/@ResumeList]中的123。
Integer[] siteId = Freemarkers.getIntegers(params, SITE_ID);
if (siteId == null && params.get(SITE_ID) == null) {
// 如果没有传入siteId这个参数,则获取当前站点的ID。
siteId =newInteger[]{ForeContext.getSiteId(env)};
}
Sort defSort =new Sort(Direction.DESC, "creationDate", "id");
Limitable limitable = Freemarkers.getLimitable(params, defSort);
List list = service.findList(siteId, limitable);
// 将获取的数据放到返回变量里。
loopVars[0] = env.getObjectWrapper().wrap(list);
// 执行标签体。
body.render(env.getOut());
}
@Autowired
privateResumeService service;
}
配置文件
FreeMarker标签类需要在/src/main/resources/conf/plugin/plug/context.xml文件中声明
id="PlugResumeList" class="com.jspxcms.plug.web.directive.ResumeListDirective"
/>
然后在/src/main/resources/conf/plugin/plug/conf.properties文件中加入freemarkerVariables.ResumeList=PlugResumeList。在模板中调用标签的名称是ResumeList,而非PlugResumeList。
标签的使用
定义了标签后,在任意的前台模板中都可以使用这个标签,如:
[@ResumeList; result]
[#list result as bean]
${bean.name}, ${bean.mobile}
[/#list]
[/@ResumeList]
V9-国际化
在/src/main/resources/conf/plugin/plug/conf.properties中指定国际化文件位置:
messageSource.basenames.plug=classpath:messages/plugin/plug/plug
messageSource.basenames.plugfore=classpath:messages/plugin/plugfore/plugfore
系统中其他的国际化文件在/src/main/resources/conf/conf.properties文件中指定:
messageSource.basenames.common=classpath:messages/common/common
messageSource.basenames.core=classpath:messages/core/core
messageSource.basenames.corefore=classpath:messages/corefore/corefore
messageSource.basenames.ext=classpath:messages/ext/ext
messageSource.basenames.extfore=classpath:messages/extfore/extfore
这些配置会在/src/main/resources/conf/context.xml文件中加载:
class="org.springframework.context.support.ReloadableResourceBundleMessageSource">
value="${messageSource.cacheSeconds}"/>
value="#{propertiesHelper.getList('messageSource.basenames.')}"/>
value="false"/>
value="false"/>
value="UTF-8"/>
V10-定时任务开发
系统中有定时任务功能,里面有一些系统已经定义好的任务类型。如果系统自带的任务类型里没有自己需要的,可以开发一个任务类型。
本着无侵入的二次开发设计思想,开发一个自己的任务类型也可以做到不修改系统原有代码和文件。
定时任务配置文件
/src/main/resources/conf/conf.properties
相关配置内容:
scheduleJob.100=com.jspxcms.core.quartz.InfoPublishJob
scheduleJobPath.com.jspxcms.core.quartz.InfoPublishJob=
scheduleJob.200=com.jspxcms.core.quartz.HtmlHomeJob
scheduleJobPath.com.jspxcms.core.quartz.HtmlHomeJob=
scheduleJob.300=com.jspxcms.ext.quartz.CollectJob
scheduleJobPath.com.jspxcms.ext.quartz.CollectJob=/cmscp/ext/collect/schedule_job.do
自定义的定时任务类型的配置也可以写在其它的conf.properties文件中,如/src/main/resources/conf/plugin/plug/conf.properties。
定时任务类型序号
scheduleJob.300:序号300决定这个类型的排序,即在选择任务类型时的前后顺序。序号不能重复。
定时任务名称
com.jspxcms.ext.quartz.CollectJob既是定时任务实现类,又是定时任务类型名称。需要在国际化文件中定义相应的国际化名称。/src/main/resources/messages/ext/ext.properties
scheduleJob.code.com.jspxcms.ext.quartz.CollectJob=采集
需以scheduleJob.code.开头。国际化名称也可以写在其他文件中,如/src/main/resources/messages/plugin/plug/plug.properties。
定时任务实现类
com.jspxcms.ext.quartz.CollectJob是定时任务实现类。
publicclassCollectJobimplementsJob{
privatestaticfinalLogger logger = LoggerFactory
.getLogger(CollectJob.class);
publicstaticfinalString COLLECT_ID ="collectId";
publicvoidexecute(JobExecutionContextcontext)
throwsJobExecutionException{
try{
ApplicationContext appContext =(ApplicationContext) context
.getScheduler().getContext().get(Constants.APP_CONTEXT);
Collector collector =appContext.getBean(Collector.class);
JobDataMap map =context.getJobDetail().getJobDataMap();
Integer collectId =map.getIntegerFromString(COLLECT_ID);
collector.start(collectId);
System.out.println("collect
ok");
logger.info("run
collect job: "+ collectId);
}catch(SchedulerException e) {
thrownewJobExecutionException("Cannot get ApplicationContext", e);
}
}
}
需要实现org.quartz.Job接口,在public
void execute(JobExecutionContext context)方法中编写任务需要执行的代码。
ApplicationContext appContext = (ApplicationContext)
context.getScheduler().getContext().get(Constants.APP_CONTEXT);可以获取Spring的ApplicationContext,通过ApplicationContext可以获取到Spring管理的对象,如Collector
collector = appContext.getBean(Collector.class);。注意:这个类中必须使用这种方法获取Spring管理的对象,不能使用@Autowired等其他方式。
Integer collectId =
map.getIntegerFromString(COLLECT_ID);可以获取额外的参数。
额外的参数
scheduleJobPath.com.jspxcms.core.quartz.InfoPublishJob=如定时任务无需额外参数,则等号后面留空。
定时任务有时需要传递外的参数,比如采集定时任务需要选择执行哪个采集数据源。此时需要在定时任务新增/修改界面增加相应的录入项。
scheduleJobPath.com.jspxcms.ext.quartz.CollectJob=/cmscp/ext/collect/schedule_job.do
录入界面的Controller
编写一个获取录入界面的地址:/cmscp/ext/collect/schedule_job.do,这里使用相对路径,相对于定时任务新增界面的地址。
此例中,这个地址的实现类是com.jspxcms.ext.web.back.CollectController。
@Controller
@RequestMapping("/ext/collect")
public class CollectController {
...
@RequestMapping("schedule_job.do")
public String scheduleJob(HttpServletRequest request, org.springframework.ui.Model modelMap) {
Integer siteId = Context.getCurrentSiteId();
List collectList = service.findList(siteId);
modelMap.addAttribute("collectList", collectList);
modelMap.addAttribute("includePage", "../../ext/collect/collect_job.jsp");
return "core/schedule_job/schedule_job_form";
}
...
}
modelMap.addAttribute("collectList",
collectList);传递数据。
modelMap.addAttribute("includePage","../../ext/collect/collect_job.jsp");传递录入界面。
录入界面的JSP
根据Controller中传递的includePage的值,对应JSP页面为:/WEB-INF/views/ext/collect/collect_job.jsp。
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%@ taglib prefix="s" uri="http://www.springframework.org/tags"%>
<%@ taglib prefix="f" uri="http://www.jspxcms.com/tags/form"%>
<tr>
<td class="in-lab" width="15%"><s:message code="scheduleJob.collectSource"/>:td>
<td class="in-ctt" width="85%" colspan="3">
<select name="data_collectId">
<c:forEach var="collect" items="${collectList}">
<f:option value="${collect.id}" selected="${dataMap['collectId']}">${collect.name}f:option>
c:forEach>
select>
td>
tr>
name="data_collectId">:此处的data_collectId对应定时任务实现类CollectJob中获取参数的方法Integer collectId =
map.getIntegerFromString("collectId");
items="${collectList}">:此处的${collectList}对应CollectController的modelMap.addAttribute("collectList",
collectList);。
V11-安全框架Shiro
原理简述
安全框架需要处理的事情:
访问受保护页面必须有权限。如果没有登录,则自动重定向至登录页面;如果登录了,没有权限则显示没有权限。
登录的时候需要校验密码和用户状态(被锁定的用户不能登录),并获取用户的权限信息,以判断用户是否有权访问该页面。
在页面里要能判断用户是否具有访问某一页面的权限,以便控制是否显示该功能。
Shiro使用Servlet Filter过滤器保护受访的页面,通过下面介绍的shiroFilterChainDefinitionMap配置需要保护的页面路径。
使用AuthorizingRealm获取用户密码及权限信息,即下面介绍的com.jspxcms.core.security.ShiroDbRealm。
在JSP页面中使用标签
name="my:perm:code">判断是否有访问Controller中@RequiresPermissions("my:perm:code")标识的方法。
配置及源代码
配置类com.jspxcms.core.ShiroConfig(7.0及之前版本/src/main/resources/conf/context-shiro.xml)
权限相关的类包:com.jspxcms.core.security
加密相关的公用类包:com.jspxcms.common.security
核心类:
com.jspxcms.core.security.CmsAuthenticationFilter 登录逻辑处理类。包括加入验证码判断、记录登录日志的逻辑。
com.jspxcms.core.security.ShiroDbRealm 登录时查询用户名、密码及获取用户权限信息。
过滤器映射配置
ShiroConfig会读取过滤器映射配置。
@Bean("shiroFilter")
@DependsOn("propertiesHelper")
public ShiroFilterFactoryBean shiroFilterFactoryBean(BeanFactory beanFactory) throws IOException {
ShiroFilterFactoryBean factoryBean =newShiroFilterFactoryBean();
...
Map filterChainDefinitionMap = propertiesHelper()
.getSortedMap("shiroFilterChainDefinitionMap.");
factoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
...
}
过滤器映射配置:/src/main/resources/conf/conf.properties
shiroFilterChainDefinitionMap[100]/login=authc
shiroFilterChainDefinitionMap[200]/logout=logout
shiroFilterChainDefinitionMap[300]/cmscp=backSite,anon
shiroFilterChainDefinitionMap[400]/cmscp/=backSite,anon
shiroFilterChainDefinitionMap[500]/cmscp/index.do=backSite,anon
shiroFilterChainDefinitionMap[600]/cmscp/login.do=backSite,authc
shiroFilterChainDefinitionMap[700]/cmscp/logout.do=backSite,logout
shiroFilterChainDefinitionMap[800]/cmscp/**=backSite,user
shiroFilterChainDefinitionMap[900]/my/**=user
shiroFilterChainDefinitionMap[1000]/**=anon
大致描述如下:
/my/** /cmscp/** 路径需要登录后才能访问,如未登录则会重定向至登录页面。前者为是前台会员中心路径,后者为后台管理路径。
/login /cmscp/login.do 是登录请求。前者为前台登录请求,后者为后台登录请求。
/logout /cmscp/logout.do 是退出登录请求。
/** 其他路径可以随便访问。
密码加密
将用户密码直接使用明文保存在数据库中是极其不安全的。要对密码进行加密后,再保存到数据库,通常的加密方式有md5 sha1 sha256等,md5使用的最为广泛,但由于安全性较差,已经不建议使用。系统中使用sha1作为加密方式。
ShiroConfig中的配置如下:
@Bean("credentialsDigest")
public SHA1CredentialsDigest credentialsDigest() {
return newSHA1CredentialsDigest();
}
这个加密对象会在com.jspxcms.core.security.ShiroDbRealm中注入:
@Autowired
public void setCredentialsDigest(CredentialsDigest credentialsDigest) {
this.credentialsDigest = credentialsDigest;
}
V12-页面动态查询
系统后台的列表页通常都有查询功能,如果通过写sql语句进行查询非常费时,特别是在查询条件较多的情况下。如果查询条件发生变化,则需要修改sql语句,非常麻烦。
jspxcms使用页面动态查询来解决这个问题,直接解析查询表单的名称来生成sql查询条件,只要修改页面就可以实现查询,而不需要修改java代码和写sql语句。
/WEB-INF/views/plug/resume/resume_list.jsp(已做简化):
名称:
职位:
开始日期:
结束日期:
搜索
com.jspxcms.plug.domain.Resume;
public class Resume {
privateInteger id;
privateSite site;
privateString name;
privateString post;
privateDate creationDate;
privateString gender;
privateDate birthDate;
privateString mobile;
privateString email;
...
}
其中
search_CONTAIN_name search_CONTAIN_post search_GTE_creationDate_Date search_LTE_creationDate_Date 是关键内容。
search_ 是前缀,代表这个输入框用于构建搜索条件。
CONTAIN GTE LTE 是查询关键字。相当于 like >= <=。
name post creationDate 是查询的字段。是com.jspxcms.plug.domain.Resume实体类中的属性名。
_Date 是类型后缀,默认是字符串,所以字符串不需要类型后缀;creationDate是日期,所以要加上_Date后缀。
最后的效果类似 where name like '%abc%' and post like '%def%' andcreationDate >= 'xxxx-xx-xx' and creationDate <= 'xxxx-xx-xx'
保留页面查询条件值
点击查询按钮后,希望把之前的查询条件值依然保留在页面上。
value="${search_CONTAIN_name[0]}"的作用是将查询条件反填至页面,也就是点击查询按钮得到查询结果后,查询条件值依然显示在页面上,而不会变成空白
如果name里面的值带有.号,则必须使用这种格式:
name="search_CONTAIN_detail.title"value="${requestScope['search_CONTAIN_detail.title'][0]}"
表连接
上面的例子是只查询当前实体类的字段。
查询many-to-one或one-to-one的关联实体类的字段用.分隔。比如search_CONTAIN_site.name。
查询many-to-many或one-to-many的关联实体类的字段需加上J前缀。比如Info查询中的search_CONTAIN_JinfoSpecials.Jspecial.title search_CONTAIN_JinfoTags.Jtag.name
查询条件
EQ :代表sql中的 =。
LIKE : 代表sql中的 like。
CONTAIN : 代表sql中的 like,且会在查询内容前后加上通配符,如 like
'%abc%'。
STARTWITH : 代表sql中的 like,且会在查询内容后加上通配符,如 like 'abc%'。
ENDWITH : 代表sql中的 like,且会在查询内容后前上通配符,如 like '%abc'。
GT : 代表sql中的 >。
LT : 代表sql中的 <。
GTE : 代表sql中的 >=。
LTE : 代表sql中的 <=。
IN : 代表sql中的 in
字段类型后缀
String:默认类型,不指定类型则默认为该类型。
Integer
Long
Float
Double
BigDecimal
BigInteger
Boolean
Date
Timestamp
Controller
packagecom.jspxcms.plug.web.back;
public class ResumeController {
@RequiresPermissions("plug:resume:list")
@RequestMapping("list.do")
public String list(@PageableDefault(sort = "id", direction = Direction.DESC)Pageable pageable,
HttpServletRequest request, org.springframework.ui.Model modelMap){
Integer siteId = Context.getCurrentSiteId();
// 获取`search_`开头的查询字段
Map params = Servlets.getParamValuesMap(request, Constants.SEARCH_PREFIX);
Page pagedList = service.findAll(siteId, params, pageable);
modelMap.addAttribute("pagedList", pagedList);
return "plug/resume/resume_list";
}
在Controller中需要获取search_开头的查询参数:Map params = Servlets.getParamValuesMap(request,Constants.SEARCH_PREFIX);
Service
packagecom.jspxcms.plug.service.impl;
public class ResumeServiceImpl implements ResumeService, SiteDeleteListener {
public Page findAll(Integer siteId, Map params, Pageable pageable) {
returndao.findAll(spec(siteId, params), pageable);
}
private Specification spec(final Integer siteId, Map params) {
// 解析页面传递过来的params参数
Collection filters = SearchFilter.parse(params).values();
finalSpecification fsp = SearchFilter.spec(filters, Resume.class);
Specification sp =newSpecification() {
public Predicate toPredicate(Root root, CriteriaQuery query, CriteriaBuilder cb) {
// 此处可以增加额外查询条件。属于JPA的用法,具体请查看JPA文档。
Predicate pred = fsp.toPredicate(root, query, cb);
if (siteId != null) {
pred = cb.and(pred, cb.equal(root.get("site").get("id"), siteId));
}
returnpred;
}
};
returnsp;
}
}
查询解析类: com.jspxcms.common.orm.SearchFilter。
V13-开发环境下部署的方法和原理
Tomcat部署目录
Tomcat部署非常简单,直接把程序复制到{Tomcat安装目录}/webapps目录下即可。该目录下会有很多默认的文件夹,如docs examples host-manager manager ROOT,这些都是Tomcat的文档、演示以及管理平台,实际使用中都不需要,如果配置不当甚至还会有安全隐患。所以在部署前,要先删除这些文件夹。
webapps下有一个特殊的文件夹ROOT,放在该目录下会作为Tomcat访问的根目录,比如/webapps/ROOT/index.html的访问地址为http://localhost:8080/index.html。而/webapps/abc/index.html的访问地址则为http://localhost:8080/abc/index.html。因此大部分情况下,都是部署在ROOT目录。
通过修改tomcat的server.xml文件可以改变tomcat的部署位置,但如果不是非常精通Tomcat,我们不建议你这么做。特别是在部署碰到错误时,更应该放弃这种做法。我们碰到过很多因为修改tomcat配置导致错误的案例。
maven目录结构与部署的关系
/src/main/java/ 源码目录。编译后会保存在/target/classes/目录。该目录不用直接参与部署。
/src/main/resources/ 资源目录。编译后会也会保存在/target/classes/目录。该目录不用直接参与部署。
/src/main/wabapp/ web程序的页面、图片、css、js等文件。该目录的文件可以需要部署在{tomcat}/webapps/ROOT/目录下。如/src/main/webapp/index.html则部署到{tomcat}/webapps/ROOT/index.html。
/target/classes/ 编译后会自动生成该目录,包含/src/main/java/和/src/main/resources/中的内容。该目录下的文件需要部署在{tomcat}/webapps/ROOT/WEB-INF/classes/目录下。
综上所述,只要将/src/main/wabapp/和/target/classes/复制到tomcat相应目录即可,但还缺少依赖包,即{tomcat}/webapps/ROOT/WEB-INF/lib/目录下的jar包。
maven打包
使用mvn package或者mav install可以得到完整的war包。直接在命令行使用maven命令,或者使用eclipse、idea等开发工具中相应的功能,都可以实现打包。
打包后在/target目录下会出现类似jspxcms-9.0.0.war的文件。war文件和zip文件格式是一样的,可以使用解压软件打开。解压后得到的文件就是上述的/src/main/wabapp/、/target/classes/和依赖包/WEB-INF/lib/组成的。
war部署
war文件可以直接部署到{Tomcat安装目录}/webapps/运行tomcat后会自动解压,解压的目录和war文件名是一样的,比如{Tomcat安装目录}/webapps/jspxcms-9.0.0.war会解压成{Tomcat安装目录}/webapps/jspxcms-9.0.0。通过上面的知识可以知道,要访问到这个程序,访问地址应该是http://localhost:8080/jspxcms-9.0.0/,而这个访问地址http://localhost:8080/是无法访问的。这显然不是我们想要的。这时需要将jspxcms-9.0.0.war重命名为ROOT.war,解压后成{Tomcat安装目录}/webapps/ROOT/,这样就可以通过http://localhost:8080/访问了。
有一点需要注意,如果一个目录已经存在{Tomcat安装目录}/webapps/ROOT/,只替换ROOT.war包,这样不会重新解压。需要将ROOT目录删除再替换ROOT.war文件才会重新解压。
该文章内容对官网内容进行整合,如果有不理解的请移步官网:http://www.jspxcms.com/documentation/