一、在业务层spring对数据库的事务管理
基本配置
1.spring-mybatis.xml(spring整合mybatis的配置文件)
<!-- 开启组件扫描器,类型为注解,除了控制层-->
<context:component-scan base-package="com">
<context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
</context:component-scan>
...
...
<!-- 配置事务管理器 -->
<bean name="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<!-- 加载数据源 注入 -->
<property name="dataSource" ref="dataSource"></property>
</bean>
<!-- 方式一:注解的方式
事务注解驱动
<tx:annotation-driven transaction-manager="transactionManager"></tx:annotation-driven>
-->
<!-- 方式二:非注解的方式
配置事务管理的具体情况
配置通知
propagation: 事务的传播特性
isolation: 事务的隔离级别-->
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<!-- RQUIRED是当我们执行一个操作,如果有事务就把这个操作加入事务中,如果没有事务就另外新建一个事务。如:增删改操作。 SUPPORTS是如果有事务就把操作加入事务,如果没有事务也能脱离事务独立运行。如查询操作。 -->
<tx:attributes>
<tx:method name="save*" propagation="REQUIRED" isolation="DEFAULT"/>
<tx:method name="delete*" propagation="REQUIRED" isolation="DEFAULT"/>
<tx:method name="update*" propagation="REQUIRED" isolation="DEFAULT"/>
<tx:method name="add*" propagation="REQUIRED" isolation="DEFAULT"/>
<tx:method name="find*" propagation="SUPPORTS" isolation="DEFAULT"
read-only="true"/>
<tx:method name="get*" propagation="SUPPORTS" isolation="DEFAULT"
read-only="true"/>
<tx:method name="select*" propagation="SUPPORTS" isolation="DEFAULT"
read-only="true"/>
</tx:attributes>
</tx:advice>
<!-- 哪些类和方法要受到上面的事务管理 -->
<aop:config>
<aop:pointcut id="myPointCut" expression="execution(* com.serviceImpl.*.*(..))"/>
<aop:advisor advice-ref="txAdvice" pointcut-ref="myPointCut"></aop:advisor>
</aop:config>
2.spring-mvc.xml(springmvc的配置文件)
<!-- 开启组件扫描器 扫描控制层-->
<context:component-scan base-package="com.controller"/>
<!-- 开启注解驱动 一个配置解决映射器和适配器的配置注解配置 -->
<mvc:annotation-driven/>
...
...
3.业务层以注解方式加事务的核心代码
...
//为方法加事务,propagation传播方式,isolation隔离级别
@Override
//@Transactional(propagation = Propagation.REQUIRED,isolation = Isolation.DEFAULT)
public void addArticle(Article article) {
int addArticle = articleMapper.addArticle(article);
//int i = 1/0;//事务回滚了
for(Category c : article.getCategorys()) {
articleMapper.addArticleCategory(article.getArticleId(), c.getCategoryId());
}
//int i = 1/0;//事务回滚了
for(Tag t : article.getTags()) {
articleMapper.addArticleTag(article.getArticleId(), t.getTagId());
}
int i = 1/0;//事务回滚了
}
...
...
【注意】
(1)在mysql中,只有当表的类型是INNODB的时候,才支持事务。
1)查看数据库表的类型:show table status from 数据库;
2)修改表的类型:alter table 表名 ENGINE = INNODB;
(2)事务失效场景
1)底层数据库引擎不支持事务
如果数据库引擎不支持事务,则Spring自然无法支持事务。
2)在非public修饰的方法使用
@Transactional注解使用的是AOP,在使用动态代理的时候只能针对
public方法进行代理,源码依据AbstractFallbackTransactionAttributeSource
类中的computeTransactionAttribute方法中,如下:
protected TransactionAttribute computeTransactionAttribute(Method method,Class<?> targetClass) {
// Don't allow no-public methods as required.
if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {
return null;
}
在非public修饰的方法上使用并不会抛出异常,但是会导致事务失效。
3)异常被处理了
在整个事务的方法中使用try-catch,导致异常无法抛出,自然会导致事务失效。伪代码如下:
@Transactional
public void method(){
try{
//插入一条数据
//更改一条数据
}catch(Exception ex){
return;
}
}
4)方法中调用同类的方法
伪代码如下:
public class Test{
public void A(){
//插入一条数据
//调用B方法
B();
}
@Transactional(声明式事务)
public void B(){
//插入数据
}
}
(3)脏读、不可重复读、幻读的概念
*1) 脏读 :*脏读就是指当一个事务正在访问数据,并且对数据进行了修改,而这种修改还没有提交到数据库中,这时,另外一个事务也访问这个数据,然后使用了这个数据。
*2) 不可重复读 :*是指在一个事务内,多次读同一数据。在这个事务还没有结束时(未提交),另外一个事务也访问该同一数据。那么,在第一个事务中的两次读数据之间,由于第二个事务的修改,那么第一个事务两次读到的数据可能是不一样的。这样就发生了在一个事务内两次读到的数据是不一样的,因此称为是不可重复读。
3) 幻读 : 是指当事务不是独立执行时发生的一种现象,例如第一个事务对一个表中的数据进行了修改,这种修改涉及到表中的全部数据行。 同时,第二个事务也修改这个表中的数据,这种修改是向表中插入一行新数据。那么,以后就会发生操作第一个事务的用户发现表中还有没有修改的数据行,就好象发生了幻觉一样。
(4)五种事务隔离级别(isolation)
1)Isolation.DEFAULT 这是一个PlatfromTransactionManager默认的隔离级别,使用数据库默认的事务隔离级别;另外四个与JDBC的隔离级别相对应 。
2)Isolation.READ_UNCOMMITTED 这是事务最低的隔离级别,它充许另外一个事务可以看到这个事务未提交的数据。这种隔离级别会产生脏读,不可重复读和幻读。
3)Isolation.READ_COMMITTED 保证一个事务修改的数据提交后才能被另外一个事务读取。另外一个事务不能读取该事务未提交的数据。这种事务隔离级别可以避免脏读出现,但是可能会出现不可重复读和幻读。
4)Isolation.REPEATABLE_READ它保证了一个事务可重复读。这种事务隔离级别可以防止脏读、不可重复读,但是可能出现幻读。
5)Isolation.SERIALIZABLE 这是花费最高代价但最可靠的事务隔离级别。事务被处理为顺序执行。除了防止脏读,不可重复读外,还避免了幻读。
(5)七种事务传播行为(propagation)
1)Propagation.REQUIRED ,默认的spring事务传播行为,该行为的特点是,如果存在父事务,那么就加入到父事务中执行;如果不存在父事务,则新建事务执行。所以这个级别通常能满足处理大多数的业务场景。
2)Propagation.SUPPORTS ,该事务传播行为的特点是,如果存在父事务,那么就加入到事务中执行;如果没有父事务,则使用非事务的方式执行。
3)Propagation.MANDATORY , 该事务传播行为要求方法上必须要存在父事务,否则就会抛出异常(IllegalTransationStateException)。(父事务不能是Propagation.MANDATORY,但可以加Propagation.REQUIRED)
4)Propagation.REQUIRES_NEW ,该事务传播行为的特点是,每次都会新建一个事务,并且同时将父事务挂起,执行完新建事务后,父事务恢复再执行。案例代码如下
@Service("userServiceImpl")
public class UserServiceImpl implements UserService{
@Resource
private UserMapper userMapper;
@Override
public void addUser(User user) {
userMapper.addUser(user);
}
}
========================================================================
@Service("scoreServiceImpl")
public class ScoreServiceImpl implements ScoreService{
@Resource
private ScoreMapper scoreMapper;
@Override
@Transactional(propagation = Propagation.REQUIRES_NEW,isolation = Isolation.DEFAULT)
public void addScore(Score score) {
scoreMapper.addScore(score);
}
}
========================================================================
@Service("infoServiceImpl")
public class InfoServiceImpl implements InfoService {
@Resource
private UserService userService;
@Resource
private ScoreService scoreService;
@Override
@Transactional(propagation = Propagation.REQUIRED,isolation = Isolation.DEFAULT)
public void addInfo(User user, Score score) {
userService.addUser(user);//初始化操作,没有异常 最终回滚了
try {
scoreService.addScore(score);//发红包没有出现异常,REQUIRES_NEW挂起父事务,最终不回滚
} catch (Exception e) {
System.out.println("子事务回滚了");//父事务回滚到 回滚点
}
throw new RuntimeException();//发送日志出现异常
}
}
========================================================================
public class Demo {
@Test
public void addInfo() {
ClassPathXmlApplicationContext cxt = new ClassPathXmlApplicationContext("classpath:spring-mybatis.xml");
InfoService infoService = cxt.getBean("infoServiceImpl", InfoService.class);
User user = new User();
user.setUname("HH");
user.setAge(25);
Score score = new Score();
score.setGrade(79);
score.setRank(56);
infoService.addInfo(user, score);
System.out.println("成功添加了"+user.getUid()+"号"+"的人");
System.out.println("成功添加了"+score.getSid()+"号"+"的成绩");
}
}
====================================================================
**结论:在子事务执行前父事务没有接收到异常的情况下,只要发红包没有出现异常,一定会更新其对应数据库表的数据**
5)Propagation.NOT_SUPPORTED ,该事务传播行为的特点就是如果存在父事务,则挂起父事务,执行当前逻辑,结束后恢复父事务。(可以将事务极可能的缩小)案例代码如下
@Service("userServiceImpl")
public class UserServiceImpl implements UserService{
@Resource
private UserMapper userMapper;
@Override
public void addUser(User user) {
userMapper.addUser(user);
}
}
========================================================================
@Service("scoreServiceImpl")
public class ScoreServiceImpl implements ScoreService{
@Resource
private ScoreMapper scoreMapper;
@Override
@Transactional(propagation = Propagation.NOT_SUPPORTED,isolation = Isolation.DEFAULT)
public void addScore(Score score) {
long a = 2;
for (int i = 0; i < 1000; i++) {
a = a +2;
System.out.println(a);
if(i == 998) {
scoreMapper.addScore(score);
}
if(i == 999) {
throw new RuntimeException();
}
}
}
}
========================================================================
@Service("infoServiceImpl")
public class InfoServiceImpl implements InfoService {
@Resource
private UserService userService;
@Resource
private ScoreService scoreService;
@Override
@Transactional(propagation = Propagation.REQUIRED,isolation = Isolation.DEFAULT)
public void addInfo(User user, Score score) {
userService.addUser(user);//初始化操作,没有异常 最终回滚了
scoreService.addScore(score);//不管发红包有没有出现异常(非核心业务),NOT_SUPPORTED挂起父事务,最终不回滚
throw new RuntimeException();//发送日志出现异常
}
}
========================================================================
public class Demo {
@Test
public void addInfo() {
ClassPathXmlApplicationContext cxt = new ClassPathXmlApplicationContext("classpath:spring-mybatis.xml");
InfoService infoService = cxt.getBean("infoServiceImpl", InfoService.class);
User user = new User();
user.setUname("HH");
user.setAge(25);
Score score = new Score();
score.setGrade(79);
score.setRank(56);
infoService.addInfo(user, score);
System.out.println("成功添加了"+user.getUid()+"号"+"的人");
System.out.println("成功添加了"+score.getSid()+"号"+"的成绩");
}
}
========================================================================
**结论:不管发红包有没有出现异常,一定会更新其对应数据库表的数据**
6)Propagation.NEVER ,该事务传播行为要求不能存在父事务,一旦有父事务,就抛出异常(IllegalTransationStateException),强制停止执行。案例代码如下
@Service("userServiceImpl")
public class UserServiceImpl implements UserService{
@Resource
private UserMapper userMapper;
@Override
public void addUser(User user) {
userMapper.addUser(user);
throw new RuntimeException();
}
}
========================================================================
@Service("scoreServiceImpl")
public class ScoreServiceImpl implements ScoreService{
@Resource
private ScoreMapper scoreMapper;
@Override
@Transactional(propagation = Propagation.NEVER,isolation = Isolation.DEFAULT)
public void addScore(Score score) {
long a = 2;
for (int i = 0; i < 1000; i++) {
a = a +2;
System.out.println(a);
if(i == 998) {
scoreMapper.addScore(score);
}
if(i == 999) {
throw new RuntimeException();
}
}
}
}
========================================================================
@Service("infoServiceImpl")
public class InfoServiceImpl implements InfoService {
@Resource
private UserService userService;
@Resource
private ScoreService scoreService;
@Override
@Transactional(propagation = Propagation.REQUIRED,isolation = Isolation.DEFAULT)
public void addInfo(User user, Score score) {
userService.addUser(user);//初始化操作,不管有没有出现异常
scoreService.addScore(score);//不管发红包有没有出现异常,Propagation.NEVER引起addInfo被强制停止执行,两个表的数据没有发生更新。
}
}
结论:Propagation.NEVER引起addInfo被强制停止执行,两个表的数据没有发生更新
7)Propagation.NESTED ,嵌套传播行为的事务。该事务传播行为的特点是,如果存在父事务,则嵌套事务执行。
分析
嵌套是子事务套在父事务中执行,子事务是父事务的一部分,在进入子事务之前,父事务建立一个回滚点,叫save point,然后执行子事务,这个子事务的执行也算是父事务的一部分,然后子事务执行结束,父事务继续执行。
如果子事务回滚,会发生什么?
父事务会回滚到进入子事务前建立的save point,然后尝试其他的事务或者其他的业务逻辑,父事务之前的操作不会受到影响,更不会回滚。
如果父事务回滚,会发生什么?
父事务回滚,子事务也会跟着回滚。
事务的提交,是什么情况?
子事务是父事务的一部分,由父事务统一提交。
代码案例如下
@Service("userServiceImpl")
public class UserServiceImpl implements UserService{
@Resource
private UserMapper userMapper;
@Override
public void addUser(User user) {
userMapper.addUser(user);
}
}
========================================================================
@Service("scoreServiceImpl")
public class ScoreServiceImpl implements ScoreService{
@Resource
private ScoreMapper scoreMapper;
@Override
@Transactional(propagation = Propagation.NESTED,isolation = Isolation.DEFAULT)
public void addScore(Score score) {
long a = 2;
for (int i = 0; i < 10; i++) {
a = a +2;
System.out.println(a);
if(i == 8) {
scoreMapper.addScore(score);
}
if(i == 9) {
throw new RuntimeException();//子事务会发生回滚
}
}
}
}
========================================================================
@Service("cardServiceImpl")
public class CardServiceImpl implements CardService {
@Resource
private CardMapper cardMapper;
@Override
public void addCard(Card card) {
cardMapper.addCard(card);
}
}
========================================================================
@Service("infoServiceImpl")
public class InfoServiceImpl implements InfoService {
@Resource
private UserService userService;
@Resource
private ScoreService scoreService;
@Resource
private CardService cardService;
@Override
@Transactional(propagation = Propagation.REQUIRED,isolation = Isolation.DEFAULT)
public void addInfo(User user, Score score,Card card) {
userService.addUser(user);//初始化操作,没有出现异常
//父事务建立一个回滚点
try {
scoreService.addScore(score);//发红包,Propagation.NESTED作为子事务执行(不能让父事务感到异常)
} catch (Exception e) {
System.out.println("子事务回滚了");//父事务回滚到 回滚点
}
cardService.addCard(card);//发送日志信息,没有出现异常
//throw new RuntimeException();//父事务回滚,子事务也跟着回滚
}
}
========================================================================
public class Demo {
@Test
public void addInfo() {
ClassPathXmlApplicationContext cxt = new ClassPathXmlApplicationContext("classpath:spring-mybatis.xml");
InfoService infoService = cxt.getBean("infoServiceImpl", InfoService.class);
User user = new User();
user.setUname("HH");
user.setAge(25);
Score score = new Score();
score.setGrade(79);
score.setRank(56);
Card card = new Card();
card.setCode("07789028357786");
card.setAdress("北京");
infoService.addInfo(user, score,card);
System.out.println("成功添加了"+user.getUid()+"号"+"的人");
System.out.println("成功添加了"+score.getSid()+"号"+"的成绩");
System.out.println("成功添加了"+card.getCid()+"号"+"的卡");
}
}
以上是事务的7个传播级别,在日常应用中,通常可以满足各种业务需求,但是除了传播级别,在读取数据库的过程中,如果两个事务并发执行,那么彼此之间的数据是如何影响的呢?
这就需要了解一下事务的另一个特性:数据隔离级别
(6)四种数据隔离级别
1)Serializable :最严格的级别,事务串行执行,资源消耗最大。
2)REPEATABLE READ :保证了一个事务不会修改已经由另一个事务读取但未提交的数据。避免了“脏读”和“不可重复读”的情况,但是带来了更多的性能损失。
3)READ COMMITTED :大多数主流数据库的默认事务隔离级别,保证了一个事务不会读到另一个并行事务已修改但未提交的数据,避免了“脏读”。该级别适用于大多数系统。
4)Read Uncommitted :保证了读取过程中不会读取到非法数据。
(7)其它的属性
timeout
事务的超时时间,单位为秒。
readOnly
该属性用于设置当前事务是否为只读事务,设置为true表示只读,false则表示可读写,默认值为false。如果一个事务只涉及到只读,可以设置为true。
*rollbackFor *
用于指定能够触发事务回滚的异常类型,可以指定多个异常类型。
默认是在RuntimeException和Error上回滚。
noRollbackFor
抛出指定的异常类型,不回滚事务,也可以指定多个异常类型。
二、文件的上传(文件保存在服务器硬盘上)
1.引入kindEditor编辑器
<script charset="utf-8"
src="resources/kindeditor/kindeditor-all-min.js"></script>
<script charset="utf-8" src="resources/kindeditor/lang/zh-CN.js"></script>
<script charset="utf-8"
src="resources/kindeditor/plugins/code/prettify.js"></script>
<!-- 重点 kindEditor编辑器-->
<script>
KindEditor.ready(function(K) {
var editor1 = K.create('textarea[id="content"]', {
cssPath : '../resources/kindeditor/plugins/code/prettify.css',
uploadJson : 'article/uploadimg', //指明上传图片用的服务端程序
allowFileManager : true,
width:'1000px',
height:'400px'
});
prettyPrint();
});
</script>
<link rel="stylesheet"
href="resources/kindeditor/themes/default/default.css" />
<link rel="stylesheet"
href="resources/kindeditor/plugins/code/prettify.css" />
2.在spring-mvc.xml配置文件解析器
<!--配置文件解析器 -->
<bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
<!--最大尺寸为50MB-->
<property name="maxUploadSize" value="52428800" />
<property name="defaultEncoding" value="UTF-8" />
</bean>
3.在控制层的java代码
@RequestMapping("/uploadimg") @ResponseBody
public String uploadFile(MultipartHttpServletRequest req) throws IllegalStateException, IOException {
//imgFile这个名称就是这么写的
MultipartFile file = req.getFile("imgFile");
//生成一个随机的文件名
String newName = UUID.randomUUID().toString();
File destFile =new File("H:/imguploads/"+newName);
file.transferTo(destFile);
String path="http://localhost:8080/uploads/"+newName;
return "{\"error\":0,\"url\":\""+path+"\"}";
}
4.在对应tomcat家目录/config文件夹/server.xml里配置虚拟地址
<Context docBase="H:/imguploads" path="/uploads" debug="0" reloadable="true"/>
另外,eclipse里的话还需要在Modules里配置虚拟地址;idea的话还需要√上Deploy…Tomcat instance
5.导入相关的依赖
<!-- 引入文件上传的依赖 -->
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.3.3</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.6</version>
</dependency>
三、下拉列表的二级联动
1.前端代码实现
<div class="layui-form-item">
<label class="layui-form-label">分类 <span
style="color: #FF5722;">*</span>
</label>
<div class="layui-input-inline">
<select name="articleParentCategoryId" id="articleParentCategoryId2"
lay-filter="articleParentCategoryId3" required>
<option value="" selected>一级分类</option>
<c:forEach items="${categorys }" var="category">
<c:if test="${category.categoryPid == 0 }">
<option value="${category.categoryId }">${category.categoryName }</option>
</c:if>
</c:forEach>
</select>
</div>
<div class="layui-input-inline">
<select name="articleChildCategoryId" id="articleChildCategoryId">
<option value="" selected>二级分类</option>
</select>
</div>
</div>
2.ajax实现二级联动
1)js实现
<script>
layui.use(
[ 'form', 'laydate' ],
function() {
var form = layui.form, laydate = layui.laydate;
//二级联动 重点 ajax实现
form.on("select(articleParentCategoryId3)",function () {
var optionstring = "";
var articleParentCategoryId = $("#articleParentCategoryId2").val();
$.ajax({
type: "GET",
url: "category/chirdname",
async:true,
contentType:"application/json",
data:{articleParentCategoryId:articleParentCategoryId},
success:function (data) {
var categorylistByCategoryPid = eval("("+data+")");
for(var i=0;i<categorylistByCategoryPid.length;i++){
optionstring += "<option value="+categorylistByCategoryPid[i].categoryId+">"+categorylistByCategoryPid[i].categoryName+"</option>";
}
$("#articleChildCategoryId").html("<option value='' selected>二级分类</option>"+optionstring);
form.render('select'); //这个很重要
}
})
});
});
</script>
2)控制层java代码实现
public void returnChirdname(Integer articleParentCategoryId,HttpServletResponse rep) {
List<Category> categorylistByCategoryPid = categoryService.getCategorylistByCategoryPid(articleParentCategoryId);
try {
Gson gson = new Gson();
String json = gson.toJson(categorylistByCategoryPid);
rep.getWriter().write(json);
} catch (IOException e) {
e.printStackTrace();
}
}
四、带参数的url实现删除
1)前端代码实现
<a href="category/delete/${category2.categoryId }"
class="layui-btn layui-btn-danger layui-btn-mini"
onclick="return confirmDelete()">删除</a>
2)控制层代码实现
@RequestMapping("/delete/{categoryId}")
public String deleteCategory(@PathVariable("categoryId") Integer categoryId) {
Category c = categoryService.getCategoryBycategoryId(categoryId);
categoryService.deletCategoryBycategoryId(categoryId);
if(c.getCategoryPid()==0) {
categoryService.deletCategoryBycategoryPid(categoryId);
}
return "redirect:/category/categoryPage";
}
五、基于layui框架弹框式的修改页面设计
1.编写更新页面(将作为子页面被父页面引用)
<%@ page language="java" import="java.util.*"
contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>
<%
String path = request.getContextPath();
String basePath = request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort() + path + "/";
%>
<!DOCTYPE HTML>
<html>
<head>
<base href="<%=basePath%>">
<title></title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<meta http-equiv="pragma" content="no-cache">
<meta http-equiv="cache-control" content="no-cache">
<meta http-equiv="expires" content="0">
<link rel="shortcut icon" href="resources/img/logo.png">
<link rel="stylesheet" href="resources/plugin/layui/css/layui.css">
<link rel="stylesheet" href="resources/css/back.css">
<link rel="stylesheet"
href="resources/plugin/font-awesome/css/font-awesome.min.css">
<script src="resources/js/jquery-3.4.1.min.js"></script>
<script type="text/javascript">
$(function() {
$("#submitId").click(function() {
$.ajax({
type : "GET",
url : "category/updateSubmit",
async : true,
contentType : "application/json",
data : {categoryId : $("#updatecategoryId").val(),categoryName:$("#updatecategoryName").val(),categoryPid:$("#updatecategoryPid").val(),categoryDescription:$("#updatecategoryDescription").val(),categoryIcon:$("#updatecategoryIcon").val()},
success : function(data) {
if(data == "success"){
parent.window.location.href="category/categoryPage";
}
}
});
});
$("#closeId").click(function() {
var index = parent.layer.getFrameIndex(window.name); //先得到当前iframe层的索引
parent.layer.close(index); //再执行关闭
});
});
</script>
<script type="text/javascript">
$(function() {
var category = parent.category;//从父层继承而来
$("#updatecategoryId").val(category.categoryId);
$("#updatecategoryName").val(category.categoryName);
$("#updatecategoryPid").val(category.categoryPid);
$("#updatecategoryDescription").val(category.categoryDescription);
$("#updatecategoryIcon").val(category.categoryIcon);
});
</script>
</head>
<body>
<div class="layui-col-md4">
<form class="layui-form" method="post" id="myForm">
<div class="layui-form-item">
<div class="layui-input-block">
<strong>修改分类</strong>
</div>
<div class="layui-input-block">
<input type="hidden" name="categoryId" id="updatecategoryId">
名称 <span style="color: #FF5722; ">*</span>
<input type="text" name="categoryName" placeholder="请输入分类名称" autocomplete="off" class="layui-input" required id="updatecategoryName">
</div>
<br>
<input type="hidden" name="categoryPid" id="updatecategoryPid">
<br>
<div class="layui-input-block">
分类描述
<input type="text" name="categoryDescription" placeholder="请输入分类描述" autocomplete="off" class="layui-input" id="updatecategoryDescription">
</div>
<br>
<div class="layui-input-block">
图标样式
<input type="text" name="categoryIcon" placeholder="请输入图标样式,如 fa fa-coffee" autocomplete="off" class="layui-input" id="updatecategoryIcon">
</div>
<br>
<div class="layui-input-block">
<button class="layui-btn" type="button" id="submitId">确认</button>
<button class="layui-btn" type="button" id="closeId">取消</button>
</div>
</div>
</form>
</div>
</body>
</html>
2.父页面的核心代码
1)巧妙构造带参数的方法
<td><a class="layui-btn layui-btn-mini"
onclick="editfun(${category2.categoryId })">编辑</a>
2)添加一个隐藏div
<style type="text/css">
#updateCategory {
width: 100px;
height: 30px;
line-height: 30px;
background: #009F95;
color: #FFFFFF;
text-align: center;
}
</style>
<div id="updateCategory" style="display: none;"></div>
3)通过editfun(a)的触发查值和弹框
<script type="text/javascript">
function editfun(categoryId) {
$.ajax({
type : "GET",
url : "category/updateAjax",
async : true,
contentType : "application/json",
data : {categoryId : categoryId},
success : function(data) {
window.category = eval("("+data+")");
$('#updateCategory').click();
}
});
}
console.log(category);
</script>
<script type="text/javascript">
$("#updateCategory").on("click", function() {
layer.open({
type : 2,
title : '编辑',
area : [ '500px', '500px' ],
fix : false, //
content : 'jsp/Category/category-update.jsp',
end : function() {
dataTable.reloadTable();
}
});
});
</script>
六、上传图片并保存在数据库中(字段的类型:longblob,属性的类型:byte[])
1.上传图片并保存在数据库中
1)前端html的编写
<div class="layui-form-item">
<input type="hidden" id="userId" value="0"> <label
class="layui-form-label">头像</label>
<div class="layui-input-inline">
<div class="layui-upload">
<div class="layui-upload-list" id="localImag">
<img class="layui-upload-img" id="demo1" width="100" height="100">
</div>
<!-- 重点 -->
<input type="file" name="photo" id="photo" style="display:none" onchange="preview(this,localImag,demo1,'100px','100px')">
<button type="button" onclick="$('#photo').click()" class="layui-btn" id="test1">上传图片</button>
</div>
</div>
</div>
2)前端js实现图片预览
<script>
//预览图片
function preview(docObj, localImagId, imgObjPreview, width, height) {
if (docObj.files && docObj.files[0]) { //火狐下,直接设img属性
imgObjPreview.style.display = 'block';
imgObjPreview.style.width = width;
imgObjPreview.style.height = height;
//火狐7以上版本不能用上面的getAsDataURL()方式获取,需要以下方式
imgObjPreview.src = window.URL.createObjectURL(docObj.files[0]);
} else { //IE下,使用滤镜
docObj.select();
var imgSrc = document.selection.createRange().text;
//必须设置初始大小
localImagId.style.width = width;
localImagId.style.height = height;
//图片异常的捕捉,防止用户修改后缀来伪造图片
try {
localImagId.style.filter = "progid:DXImageTransform.Microsoft.AlphaImageLoader(sizingMethod=scale)";
localImagId.filters
.item("DXImageTransform.Microsoft.AlphaImageLoader").src = imgSrc;
} catch (e) {
alert("您上传的图片格式不正确,请重新选择!");
return false;
}
imgObjPreview.style.display = 'none';
document.selection.empty();
}
}
</script>
3)控制层插入数据到数据库
@RequestMapping(value = "/add",method = RequestMethod.POST)
public String addUser(User user,MultipartFile photo,HttpServletRequest req) throws IOException {
user.setUserLastLoginIp(req.getRemoteAddr());
user.setUserRegisterTime(new Date());
user.setUserLastLoginTime(new Date());
user.setUserStatus(1);
user.setUserPhoto(photo.getBytes());
userService.addUser(user);
return "forward:/admin/userlist";
}
2.从数据库中读取图片
1)前端html编写
<c:forEach items="${users }" var="user">
<tr>
<td>
<!-- 重点 -->
<img src="admin/photo/${user.userId }" width="48" height="48">
2)控制层从数据库中取出数据,并回传至前端
@RequestMapping("/photo/{userId}")
public void showPhoto(@PathVariable("userId") Integer userId,HttpServletResponse response) throws IOException {
User u = userService.getUserById(userId);
response.setContentType("image/jpg");
ServletOutputStream sos = response.getOutputStream();//网络字节流
sos.write(u.getUserPhoto());
sos.flush();
}
七、防止用户非法登录
1.拦截器的配置
1)配置spring-mvc.xml文件
<!--mvc映射路径拦截器 防止非法登录-->
<mvc:interceptors>
<mvc:interceptor>
<!-- 拦截所有的请求 这里拦截的是mvc映射路径 间接拦截.jsp文件;
为了防止非法直接访问除了login.jsp页面外的其它页面 需要
额外配置过滤器对*.jsp文件进行处理-->
<mvc:mapping path="/**"/>
<!-- 不包括对登录处理的映射路径-->
<mvc:exclude-mapping path="/admin/login"/>
<bean class="com.interceptor.LoginInterceptor"></bean>
</mvc:interceptor>
</mvc:interceptors>
2)编写拦截器
/**
* 登录拦截器
*/
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
HttpSession session = request.getSession();
if(session.getAttribute("user_session") == null){
response.sendRedirect(request.getContextPath()+"/jsp/login.jsp");
return false;
}
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
}
}
3)补充知识点
Springmvc拦截器三个方法的执行时机
(1)preHandle
预处理回调方法,实现处理器的预处理(如登录检查),第三个参数为响应的处理器(如具体的Controller实现);
返回值:true表示继续流程(如调用下一个拦截器或处理器);false表示流程中断(如登录检查失败),不会继续调用其他的拦截器或处理器,此时我们需要通过response来产生响应;
(2)postHandle
后处理回调方法,实现处理器的后处理(但在渲染视图之前),此时我们可以通过modelAndView(模型和视图对象)对模型数据进行处理或对视图进行处理,modelAndView也可能为null。
(3)afterCompletion
整个请求处理完毕回调方法,即在视图渲染完毕时回调,如性能监控中我们可以在此记录结束时间并输出消耗时间,还可以进行一些资源清理,类似于try-catch-finally中的finally,但仅调用处理器执行链中preHandle返回true的拦截器的afterCompletion。
首先用户请求到达前端控制器 DispatcherServlet,前端控制器找到处理器映射器,根据请求的方法找到对应的处理器handler,生成拦截器和handler执行顺序的执行链,交给DispatcherServlet,dispatcherServlet找到对应的处理器适配器进行处理。
prehandler在请求处理之前执行.该方法的返回值是布尔值 Boolean 类型的,当它返回为 false 时,表示请求结束,后续的 Interceptor 和 Controller 都不会再执行;当返回值为 true 时,就会继续调用下一个 Interceptor 的 preHandle 方法,如果已经是最后一个 Interceptor 的时候,就会是调用当前请求的 Controller 中的方法。
postHandler 方法在当前请求进行处理之后,也就是在 Controller 中的方法调用之后执行,但是它会在 DispatcherServlet 进行视图返回渲染之前被调用,所以我们可以在这个方法中对 Controller 处理之后的 ModelAndView 对象进行操作。
afterCompletion该方法将在整个请求结束之后,也就是在 DispatcherServlet 渲染了对应的视图之后执行,这个方法的主要作用是用于进行资源清理的工作。像异常处理资源释放会放在这一步。
多个拦截器的执行顺序是: 拦截器A的preHandler–>拦截器B的preHandler–>B的postHandler–>A的postHandler–>B的afterCompletion–>A的afterCompletion。
2.过滤器的配置
1)配置web.xml文件
<!-- jsp过滤器 防止非法直接访问.jsp页面-->
<filter>
<filter-name>loginFilter</filter-name>
<filter-class>com.filter.LoginFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>loginFilter</filter-name>
<!-- 对所有的.jsp文件进行过滤-->
<url-pattern>*.jsp</url-pattern>
</filter-mapping>
2)编写过滤器
/**
* 登录过滤器
*/
public class LoginFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse resp = (HttpServletResponse) response;
String requestURI = req.getRequestURI();// 如/idea-ssm/jsp/Article/article-add.jsp
String contextPath = req.getContextPath();// 如/idea-ssm
HttpSession session = req.getSession();// 获取一个session对象
//第一个参数和子串的起点一样,第二个参数比子串的终点大1
String targURI = requestURI.substring(contextPath.length(), requestURI.length());
if("/jsp/login.jsp".equals(targURI)){
chain.doFilter(req,resp);
return;
}else {
if("/".equals(targURI)){
return;
}else{
if(session.getAttribute("user_session") == null){
resp.sendRedirect(contextPath+"/jsp/login.jsp");
return;
}else {
chain.doFilter(req,resp);
return;
}
}
}
}
@Override
public void destroy() {
}
}
八、基于插件的分页技术
1.引入Mybatis - 通用分页拦截器的依赖
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper</artifactId>
<version>4.2.1</version>
</dependency>
2.配置mybatis-config.xml全局配置文件
<!-- 引入分页拦截器-->
<plugins>
<plugin interceptor="com.github.pagehelper.PageHelper">
<property name="dialect" value="mysql"/>
<property name="offsetAsPageNum" value="false"/>
<property name="rowBoundsWithCount" value="false"/>
<property name="pageSizeZero" value="true"/>
<property name="reasonable" value="true"/>
<property name="supportMethodsArguments" value="false"/>
<property name="returnPageInfo" value="none"/>
</plugin>
</plugins>
</configuration>
3.编写java代码
1)在控制层代码如下
@RequestMapping("/article-list")
public String getarticle_list(@RequestParam(required = false, defaultValue = "1") Integer pageNo,
@RequestParam(required = false, defaultValue = "3") Integer pageSize, ModelMap m) {
PageInfo<Article> pageInfo = articleService.pagelistArticle(pageNo, pageSize);//只需前端将页码传过来
m.put("pageInfo", pageInfo);
m.put("pageUrlPrefix", "article/article-list?pageNo");
return "Article/article-list";
2)在业务层代码如下
public PageInfo<Article> pagelistArticle(Integer pageNo, Integer pageSize) {
PageHelper.startPage(pageNo, pageSize);
List<Article> findAll = articleMapper.findAll();
PageInfo<Article> pageInfo = new PageInfo<Article>(findAll);
return pageInfo;
}
3)Article/article-list.jsp代码如下
<table class="layui-table">
<colgroup>
<col width="300">
<col width="150">
<col width="100">
<col width="150">
<col width="100">
<col width="50">
</colgroup>
<thead>
<tr>
<th>标题</th>
<th>所属分类</th>
<th>状态</th>
<th>发布时间</th>
<th>操作</th>
<th>id</th>
</tr>
</thead>
<tbody>
<c:forEach items="${pageInfo.list }" var="article">
<tr>
<td><a href="/article/${article.articleId }" target="_blank"> ${article.articleTitle }</a></td>
<td>
<c:forEach items="${article.categorys }" var="category">
<a href="/category/${category.categoryId }" target="_blank">${category.categoryName }</a>
</c:forEach>
</td>
<td>
<a href="/admin/article?status=${article.articleStatus}">
<c:if test="${article.articleStatus == 1}">
<span style="color: #5FB878;">已发布</span>
</c:if>
<c:if test="${article.articleStatus == 0}">
<span style="color: red;">草稿</span>
</c:if>
</a>
</td>
<td>
<fmt:formatDate value="${article.articleCreateTime }" pattern="yyyy-MM-dd hh:mm:ss"/>
</td>
<td><a href="/admin/article/edit/${article.articleId }"
class="layui-btn layui-btn-mini">编辑</a> <a
href="javascript:void(0)" onclick="deleteArticle(${article.articleId })"
class="layui-btn layui-btn-danger layui-btn-mini">删除</a></td>
<td>${article.articleId }</td>
</tr>
</c:forEach>
</tbody>
</table>
<%@ include file="../page.jsp" %>
4)引入分页page.jsp
<%@ page language="java" import="java.util.*"
contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>
<c:if test="${pageInfo.pages > 1}">
<nav class="navigation pagination" role="navigation">
<div class="nav-links">
<c:choose>
<c:when test="${pageInfo.pages <= 3 }">
<c:set var="begin" value="1" />
<c:set var="end" value="${pageInfo.pages }" />
</c:when>
<c:otherwise>
<c:set var="begin" value="${pageInfo.pageNum-1 }" />
<c:set var="end" value="${pageInfo.pageNum + 2}" />
<c:if test="${begin < 2 }">
<c:set var="begin" value="1" />
<c:set var="end" value="3" />
</c:if>
<c:if test="${end > pageInfo.pages }">
<c:set var="begin" value="${pageInfo.pages-2 }" />
<c:set var="end" value="${pageInfo.pages }" />
</c:if>
</c:otherwise>
</c:choose>
<%--上一页 --%>
<c:choose>
<c:when test="${pageInfo.pageNum eq 1 }">
<%--当前页为第一页,隐藏上一页按钮--%>
</c:when>
<c:otherwise>
<a class="page-numbers"
href="${pageUrlPrefix}=${pageInfo.pageNum-1}"> <i
class="layui-icon"></i>
</a>
</c:otherwise>
</c:choose>
<%--显示第一页的页码--%>
<c:if test="${begin >= 2 }">
<a class="page-numbers" href="${pageUrlPrefix}=1">1</a>
</c:if>
<%--显示点点点--%>
<c:if test="${begin > 2 }">
<span class="page-numbers dots">…</span>
</c:if>
<%--打印 页码--%>
<c:forEach begin="${begin }" end="${end }" var="i">
<c:choose>
<c:when test="${i eq pageInfo.pageNum }">
<a class="page-numbers current">${i}</a>
</c:when>
<c:otherwise>
<a class="page-numbers" href="${pageUrlPrefix}=${i}">${i}</a>
</c:otherwise>
</c:choose>
</c:forEach>
<%-- 显示点点点 --%>
<c:if test="${end < pageInfo.pages-1 }">
<span class="page-numbers dots">…</span>
</c:if>
<%-- 显示最后一页的数字 --%>
<c:if test="${end < pageInfo.pages }">
<a href="${pageUrlPrefix}=${pageInfo.pages}"> ${pageInfo.pages}</a>
</c:if>
<%--下一页 --%>
<c:choose>
<c:when test="${pageInfo.pageNum eq pageInfo.pages }">
<%--到了尾页隐藏,下一页按钮--%>
</c:when>
<c:otherwise>
<a class="page-numbers"
href="${pageUrlPrefix}=${pageInfo.pageNum+1}"> <i
class="layui-icon"></i>
</a>
</c:otherwise>
</c:choose>
</div>
</nav>
</c:if>
5)其实page.jsp代码不需完全读懂
只需要看懂如下代码即可
<a class="page-numbers"
href="${pageUrlPrefix}=${pageInfo.pageNum+1}">
<i class="layui-icon"></i>
</a>