咨询1
咨询2
交流群
电话

Jspxcms无侵入式二次开发教程(V7)

文档 > 二次开发 2017-05-10 16:51 阅读


第1章概述

作为通用的网站内容管理系统,只能尽可能的满足大部分需求,却很难满足所有的需求,这时就需要对系统进行二次开发。对于一些可以公用的二次开发程序可以做成插件,提供给其他开发者或客户使用。

互联网的应用类型非常丰富、多种多样,不同的网站有着不同的需求,所以二次开发(插件开发)是否简便、快捷是评价CMS优劣的重要指标。

Jspxcms首创无侵入式二次开发(插件开发),无需修改系统原有代码,无缝整合EntityServiceController、功能菜单、权限、标签、国际化等功能。真正做到随意拔插,将插件文件拷贝系统,即可增加模块;将插件文件从系统中删除,即可去除模块。

系统中“插件--简历管理”(以下称“本例”)就是以这种方式实现的,下面就以“简历管理”讲解Jspxcms插件和二次开发的方法。

1.1配置文件

1.1.1配置文件目录

/WEB-INF/classes/conf/plugin

二次开发的核心就是配置文件,通过配置文件将不同模块和插件整合到一起。所有的配置文件都必须在这个目录,在该目录下的配置文件会自动加载。

在该目录下新建自己的文件夹,可以是任何名字,如:abcnovel等,本例为plug。即/WEB-INF/classes/conf/plugin/plug

1.1.2配置文件介绍

  • application.properties:功能菜单、权限、国际化、Entity都与这个文件相关。

  • context-dao.xmlDao的加载。

  • context-service.xmlService的加载。

  • controller-back.xml:后台Controller的加载。

  • controller-fore.xml:前台Controller的加载。

  • context-directive.xml:标签的加载。

1.2源代码包名

源代码可以放在任何包下,只要与配置文件中设置相对应即可。

本例为:com.jspxcms.plug

1.3后台JSP文件

后台JSP文件的根目录为:/WEB-INF/views

详细的路径取决于Controller返回的地址,如返回plug/resume/resume_list,则对应的JSP文件为:/WEB-INF/views/plug/resume/resume_list.jsp

本例的后台JSP所在目录:/WEB-INF/views/plug/resume

1.4前台模版文件

前台模版使用freemarker技术,模版根目录为:/template/1/default

其中1是站点IDdefault是模版主题,后台“系统-网站设置-基本设置”可以修改本项设置(前提是/template/1目录下有多个文件夹)。

本例的前台模版是:/template/1/default/plug_resume.html。该地址取决于Controller返回地址。

1.5国际化文件

编辑和查看国际化文件,请安装EclipseProperties Editor插件。否则不能看到中文,只能看到\u5217\u8868之类的代码;编辑时直接输入中文,则会出现乱码。

国际化文件根目录为:/WEB-INF/messages

本例的国际化路径为:/WEB-INF/messages/plugin

第2章菜单和权限

菜单和权限是插件(二次)开发的最核心的部分,也是最难做到无缝整合的部分。大部分系统会将菜单信息保存到数据库中,这对升级和维护带来一定的困难。

Jspxcms的菜单和权限信息存放在配置中,方便管理、维护和升级;只需要在一个配置文件中设置好,即可以无缝整合系统的菜单、权限、赋权等问题,无需另外修改代码和页面。

开发时可以根据需要,将新功能菜单加到任意的一级菜单下的任意位置,也可以自己新增一级菜单,新增的一级菜单也可以放在任何你想要的位置上。

2.1菜单介绍

Jspxcms系统菜单分为两级。

一级菜单:

二级菜单:

2.2配置文件

配置文件为:application.properties

在本例中的完整路径:/WEB-INF/classes/conf/plugin/plug/application.properties

与菜单和权限相关的配置内容(分号为分割符):

menu.650=navigation.plug;nav.do?menuId=650;nav_plug


menu.650.100=resume.management;plug/resume/list.do;plug:resume:list;create@plug:resume:create;copy@plug:resume:copy;edit@plug:resume:edit;save@plug:resume:save;update@plug:resume:update;delete@plug:resume:delete


menu.650.200=dbBackup.management;plug/db_backup/list.do;plug:db_backup:list;dbBackup.backup@plug:db_backup:backup;dbBackup.restore@plug:db_backup:restore;delete@plug:workflow_group:delete

2.2.1一级菜单配置

配置分为三部分:菜单位置及名称、菜单链接地址、权限值。

  • 菜单位置及名称

menu.650=navigation.plug

650:是后台导航一级菜单的编号,编号大小决定菜单的前后位置。其值可以根据需要调整,如330970,但不要与系统菜单或其他插件菜单重复。系统菜单通常为menu.100menu.200menu.600等,系统菜单定义文件在/WEB-INF/classes/conf/application-menu.properties

navigation.plug:菜单名称。这里使用了国际化代码,也可以直接用中文,比如“menu.650=我的插件”。(在properties文件中直接写中文,需要安装Properties Editor插件,否则会出现乱码)

  • 菜单链接地址

nav.do?menuId=650

其中650需与menu.650的值一样。

  • 权限值

nav_plug

其中plug通常与配置文件目录名称一样。如目录为abc,则应为nav_abc。也可不一样,但不能与其他一级菜单权限名称一样。

2.2.2二级菜单配置

配置分为四部分:菜单位置及名称、菜单链接地址、菜单权限值、功能名称及权限值。

  • 菜单位置及名称

menu.650.100=resume.management

650:二级菜单所属的一级菜单编号。

100:二级菜单编号。其值的意义和一级菜单编号一样,用于确定二级菜单的前后位置。

resume.management:二级菜单名称。可以直接用中文,如“小说管理”。

  • 菜单链接地址

plug/resume/list.do

需与Controller中的地址对应,否则会找不到页面。

  • 菜单权限值

plug:resume:list

需与Controllerlist方法的权限对应,否则会报没有权限。

  • 功能名称及权限值

create@plug:resume:create;copy@plug:resume:copy;edit@plug:resume:edit;save@plug:resume:save;update@plug:resume:update;delete@plug:resume:delete

create@plug:resume:create:新增按钮的权限值。其中create是国际化代码,可以用直接用中文,如“新增@plug:resume:create”。其中plug:resume:create是该按钮的权限值,需与Controller中对应的create方法权限值一致。

edit@plug:resume:edit:意义和上面一样,后面的以此类推。

2.2.3菜单配置与权限管理

按照上面步骤设置好菜单之后,权限管理部分就会读取配置文件,无需修改权限管理页面及代码。

第3章Controller

ControllerMVC中的控制部分,主要的功能是接收客户端提交的请求,然后调用Service的功能及获取数据,最后返回View(视图,即JSPfreemarker页面)。

Jspxcms中,Controller分为前台和后台。前台是普通用户浏览的页面,使用freemarker作为视图,通常不需要登录,比如网站首页、栏目页、专题页、搜索页等;后台一般为管理功能,使用JSP作为视图,需要管理员登录后台,才能访问。

3.1配置文件

3.1.1后台配置文件

/WEB-INF/classes/conf/plugin/plug/controller-back.xml

其中com.jspxcms.plug.web.back是后台Controller所在包,在这个包下的Controller都会被spring扫描到。

3.1.2前台配置文件

/WEB-INF/classes/conf/plugin/plug/controller-fore.xml

其中com.jspxcms.plug.web.fore是前台Controller所在包,在这个包下的Controller都会被spring扫描到。

3.2后台Controller

package com.jspxcms.plug.web.back;

@Controller

@RequestMapping("/plug/resume")

public class ResumeController {


@RequiresPermissions("plug:resume:list")

@RequestMapping("list.do")

public String list(

@PageableDefaults(sort = "id", sortDir = Direction.DESC) Pageable pageable,

HttpServletRequest request, org.springframework.ui.Model modelMap) {

Integer siteId = Context.getCurrentSiteId(request);

Map<String, String[]> params = Servlets.getParameterValuesMap(request,

Constants.SEARCH_PREFIX);

Page<Resume> pagedList = service.findAll(siteId, params, pageable);

modelMap.addAttribute("pagedList", pagedList);

return "plug/resume/resume_list";

}


@RequiresPermissions("plug:resume:create")

@RequestMapping("create.do")

public String create(Integer id, org.springframework.ui.Model modelMap) {

if (id != null) {

Resume bean = service.get(id);

modelMap.addAttribute("bean", bean);

}

modelMap.addAttribute(OPRT, CREATE);

return "plug/resume/resume_form";

}


@Autowired

private ResumeService service;

}


其中RequestMapping为访问地址,RequiresPermissions为权限代码,需与application.properties中的菜单权限配置相吻合。

3.3前台Controller


package com.jspxcms.plug.web.fore;

@Controller

public class ResumeController {

public static final String TEMPLATE = "plug_resume.html";

@RequestMapping(value = "/resume.jspx")

public String resumeForm(Integer page, HttpServletRequest request,

org.springframework.ui.Model modelMap) {

Site site = Context.getCurrentSite(request);

Map<String, Object> data = modelMap.asMap();

ForeContext.setData(data, request);

ForeContext.setPage(data, page);

return site.getTemplate(TEMPLATE);

}

@RequestMapping(value = "/resume.jspx", method = RequestMethod.POST)

public String resumeSubmit(@Valid Resume bean, HttpServletRequest request,

HttpServletResponse response, org.springframework.ui.Model modelMap) {

Response resp = new Response(request, response, modelMap);

Site site = Context.getCurrentSite(request);

service.save(bean, site.getId());

return resp.post();

}

@Autowired

private ResumeService service;

}


第4章Entity

4.1配置文件

/WEB-INF/classes/conf/plugin/plug/application.properties

entityManagerFactory.packagesToScan.plug=com.jspxcms.plug.domain

plug:通常与配置文件所在目录一样,也可不一样,但不能与其他相关配置同名。

com.jspxcms.plug.domainEntity所在包名。

4.2数据库表

不使用主键自增策略,将主键放到数据库中的一个表里。


create table plug_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     datetime not null comment '投递日期',

f_gender             char(1) not null default 'M' comment '性别',

f_birth_date         datetime comment '出生日期',

f_mobile             varchar(100) comment '手机',

f_email             varchar(100) comment '邮箱',

f_expected_salary    int comment '期望薪水',

f_education_experience longtext comment '教育经历',

f_work_experience   longtext comment '工作经历',

f_remark             longtext comment '备注',

primary key (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;


4.3实体类

需注意以下三个值,其中plug_resume为表名:

name = "tg_plug_resume", pkColumnValue = "plug_resume"

generator = "tg_plug_resume"


@Entity

@Table(name = "plug_resume")

public class Resume implements java.io.Serializable {

private Integer id;

&hellip;&hellip;


@Id

@Column(name = "f_resume_id", unique = true, nullable = false)

@TableGenerator(name = "tg_plug_resume", pkColumnValue = "plug_resume", table = "t_id_table", pkColumnName = "f_table", valueColumnName = "f_id_value", initialValue = 1, allocationSize = 1)

@GeneratedValue(strategy = GenerationType.TABLE, generator = "tg_plug_resume")

public Integer getId() {

return this.id;

}


public void setId(Integer id) {

this.id = id;

}

&hellip;&hellip;

}


第5章DAO

5.1配置文件

/WEB-INF/classes/conf/plugin/plug/context-dao.xml


<jpa:repositories base-package="com.jspxcms.plug.repository"

transaction-manager-ref="transactionManager"

entity-manager-factory-ref="entityManagerFactory"

factory-class="com.jspxcms.common.orm.MyJpaRepositoryFactoryBean"

repository-impl-postfix="Impl">

</jpa:repositories>


其中com.jspxcms.plug.repositorydao接口所在包。

5.2DAO

Dao使用了SpringDataJPA技术。

SpringDataJPA官网:http://projects.spring.io/spring-data-jpa/


package com.jspxcms.plug.repository;

public interface ResumeDao extends Repository<Resume, Integer>, ResumeDaoPlus {

public Page<Resume> findAll(Specification<Resume> spec, Pageable pageable);

public List<Resume> findAll(Specification<Resume> spec, Limitable limitable);

public Resume findOne(Integer id);

public Resume save(Resume bean);

public void delete(Resume bean);

}

ResumeDao接口中的方法不用实现。以下接口中的方法均可放到ResumeDao,且无需实现:

org.springframework.data.repository. CrudRepository

org.springframework.data.repository. PagingAndSortingRepository

org.springframework.data.jpa.repository. JpaRepository

com.jspxcms.common.orm. MyJpaRepository


需要实现的dao方法,放到ResumeDaoPlus接口中。


package com.jspxcms.plug.repository;

public interface ResumeDaoPlus {

public List<Resume> getList(Integer[] siteId, Limitable limitable);

}


package com.jspxcms.plug.repository.impl;

public class ResumeDaoImpl implements ResumeDaoPlus {

@SuppressWarnings("unchecked")

public List<Resume> getList(Integer[] siteId, Limitable limitable) {

JpqlBuilder jpql = new JpqlBuilder();

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));

}

return jpql.list(em, limitable);

}

private EntityManager em;

@PersistenceContext

public void setEm(EntityManager em) {

this.em = em;

}

}


其中JpqlBuilder用于拼装jqpl语句、设置参数,并可处理分页问题。

com.jspxcms.common.orm.JpqlBuilder

第6章Service

6.1配置文件

/WEB-INF/classes/conf/plugin/plug/context-service.xml


<context:component-scan base-package="com.jspxcms.plug.service.impl">

<context:exclude-filter type="annotation"

expression="org.springframework.stereotype.Controller"/>

</context:component-scan>


其中com.jspxcms.plug.service.implService的实现类所在包。


package com.jspxcms.plug.service.impl;

@Service

@Transactional(readOnly = true)

public class ResumeServiceImpl implements ResumeService {

public Page<Resume> findAll(Integer siteId, Map<String, String[]> params,

Pageable pageable) {

return dao.findAll(spec(siteId, params), pageable);

}

public RowSide<Resume> findSide(Integer siteId,

Map<String, String[]> params, Resume bean, Integer position,

Sort sort) {

if (position == null) {

return new RowSide<Resume>();

}

Limitable limit = RowSide.limitable(position, sort);

List<Resume> list = dao.findAll(spec(siteId, params), limit);

return RowSide.create(list, bean);

}

private Specification<Resume> spec(final Integer siteId,

Map<String, String[]> params) {

Collection<SearchFilter> filters = SearchFilter.parse(params).values();

final Specification<Resume> fsp = SearchFilter.spec(filters,

Resume.class);

Specification<Resume> sp = new Specification<Resume>() {

public Predicate toPredicate(Root<Resume> root,

CriteriaQuery<?> query, CriteriaBuilder cb) {

Predicate pred = fsp.toPredicate(root, query, cb);

if (siteId != null) {

pred = cb.and(pred, cb.equal(root.get("site")

.<Integer> get("id"), siteId));

}

return pred;

}

};

return sp;

}

private ResumeDao dao;

@Autowired

public void setDao(ResumeDao dao) {

this.dao = dao;

}

}


该类使用到JPASpecification查询方式。

可实现后台列表点击表头,按任意列排序;列表页按任意字段查询;编辑页面上一条、下一条功能。

第7章标签

7.1配置文件

/WEB-INF/classes/conf/plugin/plug/controller-back.xml


<bean id="PlugResumeList"

class="com.jspxcms.plug.web.directive.ResumeListDirective" />


/WEB-INF/classes/conf/plugin/plug/application.properties中加上配置:

freemarkerVariables.ResumeList=PlugResumeList

其中ResumeList为标签名。

7.2标签类

package com.jspxcms.plug.web.directive;

public class ResumeListDirective implements TemplateDirectiveModel {

public static final String SITE_ID = "siteId";


@SuppressWarnings({ "rawtypes", "unchecked" })

public void execute(Environment env, Map params, TemplateModel[] loopVars,

TemplateDirectiveBody body) throws TemplateException, IOException {

if (loopVars.length < 1) {

throw new TemplateModelException("Loop variable is required.");

}

if (body == null) {

throw new RuntimeException("missing body");

}

Integer[] siteId = Freemarkers.getIntegers(params, SITE_ID);

if (siteId == null && params.get(SITE_ID) == null) {

siteId = new Integer[] { ForeContext.getSiteId(env) };

}

Sort defSort = new Sort(Direction.DESC, "creationDate", "id");

Limitable limitable = Freemarkers.getLimitable(params, defSort);

List<Resume> list = service.findList(siteId, limitable);

loopVars[0] = env.getObjectWrapper().wrap(list);

body.render(env.getOut());

}


@Autowired

private ResumeService service;

}


第8章国际化

8.1配置

/WEB-INF/classes/conf/plugin/plug/application.properties中加上如下配置:

  • 后台国际化文件

messageSource.basenames.plug=/WEB-INF/messages/plugin/plug/plug

  • 前台国际化文件

messageSource.basenames.plugfore=/WEB-INF/messages/plugin/plugfore/plugfore


其中messageSource.basenames.plug中的plug通常为配置文件所在文件夹的名称,可以改为其他名字,但不能与其他相关配置重名。

8.2国际化文件

/WEB-INF/messages/plugin/plug/plug为国际化文件所在文件夹和文件名。