首先,分析项目可能用到哪些类型的数据。本项目涉及的数据包括:日志、菜单、角色、用户。
然后,分析这些数据相关模块的开发顺序,优先开发基础数据和简单数据对应的模块。
基础数据:不以数据为前提,或者是其他数据的前提
简单数据:字段少,逻辑清晰,与其他表关联少
上述数据模块的开发顺序:日志 -> 菜单 -> 角色 -> 用户
接下来,针对一类数据,考虑都需要实现哪些功能,可以从CRUD的方向去考虑。
例如日志数据:添加日志,日志列表,删除日志
然后,决定一类数据相关功能的开发顺序,一般按照从简单到复杂顺序,常见的顺序是“增 -> 查 -> 删 -> 改”
日志数据相关功能开发顺序:日志列表 -> 删除日志 -> 添加日志
针对每一个功能,开发顺序是:创建数据表-> 创建实体类 -> 持久层 -> 业务层 -> 控制器层 -> 前端页面
POJO是各种实体类的统称,常用的实体类包括DO/DTO/BO/VO。
分页就是将全部数据分成多页进行展示,每次仅展示1页的内容
当SpringMVC将JsonResult转换成JSON字符串时,值为null的属性默认也会参与转换。可以在application.propertis中声明一行属性spring.jackson.default-property-inclusion=non-null,指定null值属性不再参与JSON字符串的转换。
将starter.html设为项目首页,就是在用户访问http://localhost:8080/
时,为用户返回pages/starter.html
的内容。在Controller
中开发一个方法
@Controller
public class PageController{
@RequestMapping("/")
public String findStarterPage(){
return "starter";
}
}
在src/main/resources/application.properties
中添加如下配置:
spring.mvc.view.prefix=/pages/
spring.mvc.view.suffix=.html
启动项目,访问http://localhost:8080/
,查看是否能够正确显示项目首页。
页面内部引用了一些过时的css文件,所以在开发者工具视图下,可能出现css文件的404错误 ,只要页面可以正常显示,这些404可以忽略。
url=localhost:8080/log/findSysLog/1/tom
url=localhost:8080/log/find/1/tom
url=localhost:8080/log/del/1/tom
对于REST风格的API,后台就需要从请求URL中截取目标位置的内容,作为请求参数。基于SpringMVC的@PathVariable
可以非常简单的解决这一问题。
在Controller
中开发一个方法,响应用户对所有系统管理子页面的请求,返回对应的子页面内容:
@RequestMapping("/sys/{subPage}")
public String getSubPage(@PathVariable("subPage")String subPage){
return "sys/"+subPage;
}
面试题:SpringMVC能否从请求url中截取参数?
答案:可以利用@PathVariable实现。在Controller方法上添加的@RequestMapping
注解中,使用{变量名}
的方式,将url中指定位置的值作为变量。在对应方法的参数中,声明一个接收该变量值的参数,前面使用@PathVariable("变量名")
标注。
AOP,指面向切面编程(Aspect Oriented Programming),是一种编程思想,在实际应用中是对OOP的有效补充。
在OOP中,模块化的核心单元是类,在AOP中,模块化的核心单元是切面(Aspect),切面中封装了具体的代码。
应用程序中的处理逻辑可以分为两类:核心关注点 和 横切关注点。
核心关注点指某项业务的核心处理逻辑。
横切关注点指那些会被多个业务重复调用,但是和具体业务关系不大的模块,例如日志模块,性能统计模块,事务管理模块,安全验证模块等。
AOP可以将横切关注点的内容封装在Aspect内部,并注入到所需的地方,有效实现核心关注点和横切关注点的解耦,提高了程序的可扩展性和可维护性,提高了开发效率。
Spring的IOC为AOP提供了强大的支持,利用Spring,可以非常便捷的实现AOP编程:
需要在项目中添加aspectj-tools
和aspectjweaver
的依赖:
需要开发一个切面类cn.tedu.db.common.aop.TimerAspect
,在类上添加2个必要的注解@Aspect
和@Component
:
@Aspect
@Component
public class TimerAspect {
}
在类中添加切面方法:
方法的参数列表中必须添加参数ProceedingJoinPoint
,它代表了目标方法的句柄:
@Around("execution(* cn.sd.db.sys.service.impl.*.*(..))")
public Object a(ProceedingJoinPoint pjp) throws Throwable {
// 记录开始时间
long st=System.currentTimeMillis();
Object result=pjp.proceed();
// 记录结束时间
long et=System.currentTimeMillis();
// 输出耗时
System.err.println(pjp.getSignature().getName()+"-> 耗时:"+(et-st)+"ms.");
return result;
}
pjp.proceed()
代表调用了目标方法,该目标方法可能是有返回值的方法,对于这类方法,应该接收方法的返回值,并在切面结束时返回该返回值。
pjp.proceed()
调用目标方法时,可能抛出异常Throwable
,如果在切面方法中不需要对异常进行处理,可直接在签名中声明抛出。
需要在切面方法前添加@Around
注解,指明该切面方法是在目标方法调用前和调用后都有逻辑执行,对应的也可以添加@Before
或@After
,但是一般没有必要。
在@Around
注解后需要指明当前切面方法注入的目标位置,@Around("execution(* cn.sd.db.sys.service.impl.*.*(..))")
代表业务层所有的方法都被注入。
* `execution()`为表达式的主体
* 第一个"*"号表示返回值的类型任意
* `cn.sd.db.sys.service.impl`表示AOP所切的服务的包名
* 第二个"*"表示类名,*即所有类
* `.*(..)`表示任何方法名,括号表示参数,两个点表示任何参数类型
Aspect(切面): Aspect声明类似于Java中的类声明,在Aspect中会包含着一些Pointcut以及相应的Advice。
Joint point(连接点):表示在程序中明确定义的点,典型的包括方法调用,对类成员的访问以及异常处理程序块的执行等等,它自身还可以嵌套其它 joint point。
Pointcut(切点):表示一组 joint point,这些 joint point 或是通过逻辑关系组合起来,或是通过通配、正则表达式等方式集中起来,它定义了相应的 Advice 将要发生的地方。
Advice(通知):Advice 定义了在 Pointcut 里面定义的程序点具体要做的操作,它通过 before、after和around来区别是在每个joint point之前、之后还是代替执行的代码。
Target(目标对象):织入 Advice 的目标对象。
Weaving(织入):将 Aspect 和其他对象连接起来, 并创建 Adviced object 的过程
AOP proxy: 由AOP框架创建的对象,用于实现方面协定(建议方法执行等)。 在Spring框架中,AOP代理将是JDK动态代理或CGLIB代理。
根据正常的三层架构思维,不应该在业务层操作任何的控制器层的特有对象,如request,response,session等等,这样会造成强耦合。
但是总有一些业务无法完全做到上述描述。
例如,事务管理在JDBC的API中是通过Connection对象的API实现的,但是事务管理的控制是在业务层完成的,基于原生的API,业务层必须操作持久层的特殊对象-Connection,这也是一种强耦合。
上述场景,一般通过第三方工具提供的特殊类,实现一定程度上的解耦。例如事务管理是通过Spring提供的TransactionManager实现解耦。
在业务层获取控制器层的request对象,也可以通过Spring提供的一个工具RequestContextHolder
来实现,API如下:
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
aspectj-tools
和aspectjweaver
的依赖@Aspect
和@Component
@Around("...")
,配置通知注入的方式和位置https://www.echartsjs.com/zh/index.html
需要获取echarts.min.js文件
在页面中使用引入echarts.min.js文件
在页面的 基于ECharts的API,实现图表的显示 使用插件的标准步骤: 菜单节点列表是通过zTree这个前端插件实现的。使用插件的步骤: 对于任何一个新的第三方插件,开发者最初都是不会的,开发者应该具备读API文档,并找到解决方案的能力。zTree的API文档url为 具体代码如下:标签中声明一个
第一步:初始化一个图表对象
var myChart = echarts.init(document.getElementById('main'));
// echarts是ECharts API的内置对象,封装了常用的方法
// init方法用于初始化图表对象,并且绑定图表显示的div
第二步:声明一个图表的配置对象
var option={
title: // 用于设定图表的标题
tooltip: // 用于设定点击图表数据时显示的提示信息
legend: // 设定图例
xAxis: // 设定图表X轴显示的数据
yAxis: // 设定图表y轴显示的数据,由于y轴可能同时显示多组数据,因此yAxis的值一般使用{},在后面的series属性中对y轴数据进行详细的设置,但是yAxis属性不能删除
series: [{ // 对y轴显示的数据进行配置,每个js对象是一组数据
name: // 数据的名称,
type: // 数据显示的类型,
data: // y轴实际显示的数据值
}]
}
// 第三步:调用图表对象的setOption()方法,绑定图表的配置
myChart.setOption(option); // 应用配置显示图表
zTree前端插件
http://www.treejs.cn/v3/api.php
$(function(){
doLoadZtreeNodes();
});
var zTree; //zTree是第三方扩展的一个Jquery插件
//初始化zTree时会用到
var setting = {
check:{
enable:true,
chkboxType:{"Y":"s","N":"s"}
}
,data : {
simpleData : {
enable : true,
idKey : "id", //节点数据中保存唯一标识的属性名称
pIdKey : "parentId", //节点数据中保存其父节点唯一标识的属性名称
rootPId : null //根节点id
}
}
}
//加载zTree菜单
function doLoadZtreeNodes(){
//1.url
var url="menu/findMenuNode";
//2.request
$.getJSON(url,function(result){
if(result.state==20){
zTree=$.fn.zTree.init(
$("#menuTree"),
setting,
result.data);
}else{
alert(result.message);
}
});
}
Mybatis
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="cn.sd.db.sys.mapper.SysLogMapper">
<sql id="queryWhereId">
from sys_logs
<where> <!-- 下面的if判断不通过的时候,where不会写入 -->
<if test="username!=null and username!=''">
username like concat("%",#{username},"%")
</if>
</where>
</sql>
<!-- 插入日志记录 -->
<!-- int insertSysLog(SysLogDO sysLogDO) -->
<insert id="insertSysLog">
insert into sys_logs
(username, operation,
method, params,
time, ip,
createdTime
) values(
#{username}, #{operation},
#{method}, #{params},
#{time}, #{ip},
#{createdTime}
)
</insert>
<!-- 基于条件删除日志记录 -->
<!-- int deleteSysLog(@Param("ids")Integer[] ids) -->
<delete id="deleteSysLog">
delete from
sys_logs
where
id
in
<foreach collection="ids"
open="(" close=")"
separator="," item="id"
>
#{id}
</foreach>
</delete>
<!-- 基于条件查询日志记录条数 -->
<!-- int getRowCount(@Param("username")String username) -->
<select id="getRowCount" resultType="int">
select count(*)
<include refid="queryWhereId"></include>
</select>
<!-- 基于条件查询一页的日志数据 -->
<!-- List<SysLogDO> listSysLog(@Param("username")String username,
@Param("recordIndex")int recordIndex,
@Param("pageSize")int pageSize); -->
<select id="listSysLog" resultType="cn.sd.db.sys.pojo.SysLogDO">
select *
<include refid="queryWhereId"></include>
order by createdTime desc
limit #{recordIndex},#{pageSize}
</select>
<!-- 基于菜单id查询菜单信息 -->
<!-- SysMenuDO getSysMenu(Integer id) -->
<select id="getSysMenu" resultType="cn.sd.db.sys.pojo.SysMenuDO">
select
t1.*, t2.name as parentName
from
sys_menus t1 left join sys_menus t2
on
t1.parentId = t2.id
where
t1.id=#{id}
</select>
</mapper>