由于上一篇太长了,所以有了这篇《CGBIV-JT项目-lession4-jt项目正式开启-下》
1 商品的删除 DELETE
这个模块的难点在于,要支持同时删除多个商品Item。
有两个方法:
方法1.通过MP中的deleteBatchIds()方法。
需注意的是这个方法的参数规定是要List,而客户端传过来的是数组[ ],所以要先将数组—>List,而JDK中的Arrays.asList( )方法就能实现这个目的。
方法2.不用MP,自己手写sql,并且需要写的是动态sql。这是难点!
需要手写sql,并且需要写动态sql!!!!
1.1 需求
在商品列表页面,用户可以同时选择1个或多个商品信息。点击工具栏上的“删除”后,即可删除对应的Item。
1.2 商品删除—前端JS—item_list
有关商品删除的js在item_list.jsp中,第132行左右开始。这段代码是toolbar这个数组型json对象中的一个。
客户端也是通过发ajax请求到服务端的。
{
text:'删除',
iconCls:'icon-cancel',
handler:function(){
var ids = getSelectionsIds();
if(ids.length == 0){
$.messager.alert('提示','未选中商品!');
return ;
}
$.messager.confirm('确认','确定删除ID为 '+ids+' 的商品吗?',function(r){
if (r){
var params = {"ids":ids};
$.post("/item/delete",params, function(data){
if(data.status == 200){
$.messager.alert('提示','删除商品成功!',undefined,function(){
$("#itemList").datagrid("reload");
});
}else{
$.messager.alert("提示",data.msg);
}
});
}
});
}
},
1.3 商品删除—服务端Controller—ItemController
根据客户端传来的ajax请求,ItemController中要新写一个方法去接收请求,并于过后返回结果。
/**
* 业务需求:完成商品信息的删除,返回系统vo对象SysResult
* 客户端发送的请求url: http://localhost:8091/item/delete
* 参数: {"ids","100,101,102,103....."}
* SpringMVC知识点:可以根据指定的类型,动态地实现参数类型的转化
* 返回值:SysResult对象
*/
@RequestMapping("/delete")
public SysResult deleteItems(Long[] ids) {
itemService.deleteItems(ids);
return SysResult.success();
}
注意:客户端传过来的参数是String类型的"100,101,102…"
而我Controller方法中要接收的是一个数组类型。
这可怎么转变呢?
早期,在没有MVC时,是这样的:
客户端 通过 http协议传递 参数 params
服务器端的servlet创建的HttpServletRequest这个对象抓取到这个params.
如果HttpServletRequest没去取params,那么params就会一直存在于http协议中。
String abc = request.getParameter("ids") //这个方法中的参数必须与客户端发送的ajax参数中的key一致,要不然取不到值。
String[] abcArray = abc.split(","); //将abc这个字符串用","分隔开,形成一个数组,这个数组中的每个元素都是字符串
for(id id : abcArray) //再遍历这个数组,就得到了每个元素
//以上就是底层源码干的活
而有了MVC后,底层源码帮我干了这个活,不用我自己干了。它告诉我只要往方法参数中把客户端param的key写进来,并规定为数组类型即可。
1.4 商品删除—服务端Service—ItemService
void deleteItems(Long[] ids);
1.5 商品删除—服务端ServiceImpl—ItemServiceImpl
有两种方式:
1.用MP
2.手写动态sql。(如果有的公司不用MP,那只能手写SQL了)
//===删除:选中要删除的若干个Item后,点击“删除”,将对应的Item们删除
//===还要同时删除itemDesc信息
@Transactional //控制事务
@Override
public void deleteItems(Long[] ids) {
//1.方式一,利用MP,将数组转化为List集合
//List<Long> idList = Arrays.asList(ids);//jdk中的方法
//itemMapper.deleteBatchIds(idList);
//2.方式二:不用MP,手写sql
//sql大致这样:delete from tb_item where id in (id1,id2.....)
itemMapper.deleteItems(ids);
}
1.6 商品删除—服务端Mapper—ItemMapper
如果是手写sql,那就需要在ItemMapper中添加方法
void deleteItems(Long[] ids);
1.7 商品删除—服务端XML—ItemMapper.xml
初步考虑,sql语句大概应该是这样
delete from tb_item where id in (id1,id2,id3…)
由于我现在有的是Long[] ids,我需要遍历这个ids,才能获取到一个个的id1,id2,id3…
要想遍历,我就得用到遍历的标签<foreach>,所以就得写动态sql,所以这么复杂的sql我还是写到ItemMapper.xml中吧。
<!-- 删除商品item -->
<delete id="deleteItems">
delete from tb_item where id in (
<foreach collection="array" item="id" separator=",">
#{id}
</foreach>
)
</delete>
**这个collection后面写什么,有大学问。
我的Mapper中的方法是这么写的:void deleteItems(Long[] ids);
参数只有1个ids,(数组本身看做是一个整体,属于单值传递)。
当方法中传递的是“单值”(即方法参数只有一个)时:
而本例中ids是一个“数组”,所以collection后面要写“array”
如果参数是一个“集合”,那么collection后面要写“list”
(如果我方法中传的是数组ids,而我<foreach>中的collection后面非要写“ids”,可以吗?
可以。需要在参数前加@Param("ids")。这样,数组ids就被封装成了一个Map集合,Map集合的key就是@Param("ids")中的ids。
这样,collection后面就可以写ids了)
Mybatis中有规定:默认条件下,Mapper中的方法可以通过“单值”的形式,将变量传递到sql语句中。
而实际工作中,我Mapper中的方法可能有多个参数,即“多值”传递。就得把“多值”封装为一个Map集合。
因为Map集合整体可以看做是一个“单值”,之前那些多个参数都是这个Map集合内部的数据。
所以封装为Map集合后,又变成了Mybatis能接受的“单值”传递的形式。
怎么将“多值”封装为Map集合呢?
用@Param()这个注解:
如果方法是这么写的:void deleteItems(Long[] ids,Integer age,String name);
显然方法中的参数有3个,属于“多值传递”。
而我在sql语句中如果用<foreach>标签,肯定只能遍历其中的一个参数,确切的说,只能遍历数组。
所以我就得在Long[] ids前面加上@Param("ids")注解,表示的是:我sql语句中遍历的是方法参数中,ids这个数组。
void deleteItems(@Param("ids") Long[] ids,Integer age,String name);**
2 商品 上架/下架 操作
2.1 本模块特点
要展现的效果:
需求分析:
通常情况下,我点“下架”后,客户端会发送一个ajax请求给服务器端,
而我点“上架”后,客户端还会发送另一个ajax请求给服务器端
并且当我选中的一堆Item中既有“上架”又有“下架”的商品时,
当我点击“下架”,原来“上架”的商品会变成“下架”,
而原来是“下架”状态的商品,状态不变,依然是“下架”。
这样的话,我服务器端中的ItemController中需要写2个方法去接收这两个长得差不多的ajax请求。
问题来了,我客户端要怎么改一改,我controller中才能通过restful风格,只写一个方法就能解决客户端的“下架”和“上架”两个请求?
2.2 商品 上架/下架—前端JS—item_list
原来传统的写法:
下架:
url: /item/instock
{
text:'下架',
iconCls:'icon-remove',
handler:function(){
//获取选中的ID串中间使用","号分割
var ids = getSelectionsIds();
if(ids.length == 0){
$.messager.alert('提示','未选中商品!');
return ;
}
$.messager.confirm('确认','确定下架ID为 '+ids+' 的商品吗?',function(r){
if (r){
var params = {"ids":ids};
$.post("/item/instock",params, function(data){
if(data.status == 200){
$.messager.alert('提示','下架商品成功!',undefined,function(){
$("#itemList").datagrid("reload");
});
}
});
}
});
}
}
上架:
url:/item/reshelf
{
text:'上架',
iconCls:'icon-remove',
handler:function(){
var ids = getSelectionsIds();
if(ids.length == 0){
$.messager.alert('提示','未选中商品!');
return ;
}
$.messager.confirm('确认','确定上架ID为 '+ids+' 的商品吗?',function(r){
if (r){
var params = {"ids":ids};
$.post("/item/reshelf",params, function(data){
if(data.status == 200){
$.messager.alert('提示','上架商品成功!',undefined,function(){
$("#itemList").datagrid("reload");
});
}
});
}
});
}
}
而Item的pojo中规定了,status为1时表示上架 ,status为2时表示下架
那我就想着把url改成差不多的样子:
下架url:/item/updateStatus/2 (即把这个Item的status改成2)
上架url:/item/updateStatus/1 (即把这个Item的status改成1)
这样ItemController中就可以通过restful风格,只写一个方法就能解决客户端的“下架”和“上架”两个请求。
2.3 商品 上架/下架—服务端controller—ItemController
客户端传来的参数有2个:
一个是选中的Item们原来的状态:
另一个是选中的Item们的ids
/**
* 业务需求:利用restFul风格实现Item的state的修改 (可同时修改多个Item的status)
* 客户端发送的请求url已改分别成下面的模样:
* url1:/item/updateStatus/2 status=2 下架
* url2:/item/updateStatus/1 status=1 上架
* 参数: 传过来的Item原来的status值,以及要修改状态的那些item的ids
* 返回值:SysResult对象,告知成功/失败
*/
@RequestMapping("/updateStatus/{status}")
public SysResult updateStatus(@PathVariable Integer status, Long[] ids){
itemService.updateStatus(status,ids);
return SysResult.success();
}
根据restful风格的规定:
@RequestMapping("/updateStatus/{status}")中{status}接收到的就是客户端传来的状态值,1或者2
而@PathVariable这个注解可以把{status}的值获取到,赋给方法中的参数Integer status
2.4 商品 上架/下架—服务端service—ItemService
void updateStatus(Integer status, Long[] ids);
2.5 商品 上架/下架—服务端serviceImpl—ItemServiceImpl
实现 上架/下架 业务也有2种方法:
1.利用MP中的API
2.手写动态sql
方法1:
//===修改:选中1个或几个Item们,修改其status状态,原来是“上架”,就改为“下架”。原来是“下架”,就改为“上架”。
//sql语句差不多为:update tb_item set status = #{status},update=now() where id in (id1,id2...)
@Transactional //控制事务
@Override
public void updateStatus(Integer status, Long[] ids) {
//1.方式一,利用MP
//由方法倒推,需要两个参数item和updateWrapper
//先new一个item
Item item = new Item(); //第2步
item.setStatus(status); //第3步
//再new一个updateWrapper
UpdateWrapper<Item> updateWrapper = new UpdateWrapper<Item>(); //第4步
//将Long[] ids转换为集合,更方便操作。
List<Long> idsList = Arrays.asList(ids);
//由于原sql语句中筛选条件是where id in (id1,id2,id3.....)
//而MP中的条件构造器updateWrapper就要用.in()方法
updateWrapper.in("id",idsList); //第一个参数代表(id1,id2,id3....)是Item这个对象的哪个字段,那肯定就是"id"
//第二个参数就是 客户端传过来的数组ids们,经过转化后形成的集合idsList
//执行MP中的update方法
itemMapper.update(item, updateWrapper); //第1步
}
方法2:手写动态sql
//===商品的 上架/下架
@Transactional
@Override
public void updateStatus(Integer status, Long[] ids) {
//方法2:手写动态sql
itemMapper.updateStatus(status,ids);
}
2.6 商品 上架/下架—服务端Mapper—ItemMapper
void updateStatus(Integer status, Long[] ids);
2.7 商品 上架/下架—服务端Xml—ItemMapper.xml
注意,此次就是多值传递(Mapper的方法中传了2个参数:Integer status 和 Long[] ids)
而sql语句中要遍历的就是Long[] ids,所以
所以Long[] ids前面要加@Param(“ids”),
如果用的旧版本的mybatis一定要加,
如果用的新版本的mybatis,那么@Param(“ids”)也可以不写。(但建议还是写上)
<!-- 修改商品的 上架/下架 -->
<update id="updateStatus">
update tb_item set status=#{status},updated=now() where id in (
<foreach collection="ids" item="id" separator=",">
#{id}
</foreach>
)
</update>
3 商品详情描述 的实现
目前为止 还有两块的业务没完成:
3.1 富文本编辑器介绍
商品描述的大框框就是一个富文本编辑器。
官方的解释:
KindEditor是一套开源的HTML可视化编辑器,主要用于让用户在网站上获得所见即所得编辑效果,兼容IE、Firefox、Chrome、Safari、Opera等主流浏览器。
3.1.1 快速上手
就像在京东中的商品,都会有大篇幅的商品详情及展示图片,具体的介绍商品。
而我的jt项目的介绍商品的内容就要写在商品描述的富文本编辑器的大框框中。
比如我盗个别人的商品的介绍:
当我在京东上 把别人的商品的介绍CV到我的富文本编辑器中时,效果如下:
效果很是逼真。
说明:
我其实并没有把人家的真格的图片拿过来,我CV过来的只是人家图片链接的html的代码片段。
怎么知道的呢?
点击富文本编辑器上的这个按钮,我CV过来的内容就显出原形了:
也就是说,我CV过来的只是人家html页面上的一些元素~~~~
而KindEditor这个富文本编辑器就可以把这些元素编译成 成品的样子
3.1.2 入门案例
这个就是一个富文本编辑器一个最简易的样子。
在浏览器中打开这个KindEditor.jsp
打开KindEditor.jsp的代码
<%@ page language="java" contentType="text/html; charset=utf-8"
pageEncoding="utf-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<!-- 引入kindeditor的css样式 -->
<link href="/js/kindeditor-4.1.10/themes/default/default.css" type="text/css" rel="stylesheet">
<!-- 引入kindeditor的js方法类库,里面包含着kindeditor中定义的各种js方法 -->
<script type="text/javascript" charset="utf-8" src="/js/kindeditor-4.1.10/kindeditor-all-min.js"></script>
<script type="text/javascript" charset="utf-8" src="/js/kindeditor-4.1.10/lang/zh_CN.js"></script>
<script type="text/javascript" charset="utf-8" src="/js/jquery-easyui-1.4.1/jquery.min.js"></script>
<script type="text/javascript">
$(function(){
KindEditor.ready(function(){
//在指定的位置创建富文本编辑器
KindEditor.create("#editor")
})
})
</script>
</head>
<body>
<h1>富文本编辑器</h1>
<textarea style="width:700px;height:350px" id="editor"></textarea>
</body>
</html>
3.1.3 为什么会有商品描述这个表?
在jt这个项目中,把商品信息表tb_item 和 商品信息描述表tb_item_desc 分成了两个表。
为什么要这么做呢?
因为我们平时在网上买东西时,经常是先大致浏览一下 商品的大概的介绍,这些信息商品信息表tb_item就可以提供。
而只有当我想进一步了解这个商品时,才会去看商品的详细介绍,这时候才会用到商品信息描述表tb_item_desc 。
商品信息描述表tb_item_desc 中存储的都是一些大字段的内容,数据库查询起来挺费劲的,效率低。
所以把这两个表拆开,在用户点进去看详情时才去查商品信息描述表tb_item_desc,就会减少数据库很大的压力。
3.2 商品详情的pojo对象–ItemDesc
通过商品信息描述表tb_item_desc 表中的字段可以看出
ItemDesc要包含
itemId和itemDesc
而created和updated已经在BasePojo中定义了,所以ItemDesc直接继承BasePojo就行了
并且item_id没有被设置为主键自增,是因为这个item_id与tb_item表中的item_id是一模一样的,同一个。所以不能设置为主键自增。
itemDesc是text类型,是大字段。
这个pojo要写在jt-common中
@Data
@Accessors(chain = true)
@TableName("tb_item_desc")
public class ItemDesc extends BasePojo{
private static final long serialVersionUID = -3532471496881435303L;
@TableId //这里只是用这个注解将itemId标为主键
private Long itemId; //没有被设置为主键自增,因为它与tb_item中的itemId要保持一致
private String itemDesc;
}
3.3 商品详情–服务端Mapper–ItemDescMapper
还要准备个ItemDescMapper接口,在jt-manage中
//注意点:
//1.ItemDescMapper要继承BaseMapper
//2.BaseMapper的泛型要写啥呢?操作哪张表就写表对应的POJO对象名
public interface ItemDescMapper extends BaseMapper<ItemDesc>{
}
3.4 商品详情的新增入库----INSERT
需求:
点击“提交”后,将商品信息Item与商品详情信息ItemDesc同时存入数据库中。
3.4.1 商品详情入库—服务端Controller—ItemController
要想完成这个需求,只需在ItemController中的saveItem方法中再加一个参数ItemDesc
/**============================================================================================
* 业务需求:在新增商品页面,点击“提交”后,完成商品入库操作,返回系统vo对象SysResult
* 客户端发送的请求url:http://localhost:8091/item/save
* 参数:整个form表单中的数据,都在item对象中,直接用item对象接就行了,就不用一个一个单独接了。
* 返回值:SysResult对象
*/
@RequestMapping("/save")
public SysResult saveItem(Item item,ItemDesc itemDesc) {
//方法参数也加上ItemDesc,实现Item和ItemDesc一起入库
itemService.saveItem(item,itemDesc);
return SysResult.success(); //这里为啥敢直接写success()呢?万一fail呢?没关系,可以try{}catch(){}。
//但这方法有点儿out了,现在都是写一个全局异常处理类了。把fail()的情况写在全局异常处理类中。
}
3.4.2 商品详情入库—服务端Service—ItemService
既然Controller的方法中加ItemDesc这个参数了,相应的ItemService中的方法肯定也要加ItemDesc这个参数
void saveItem(Item item, ItemDesc itemDesc);
3.4.3 商品详情入库—服务端ServiceImpl—ItemServiceImpl
既然Service接口中的方法加ItemDesc这个参数了,那么实现类中的方法肯定也要加ItemDesc这个参数了。
并且要记得注入ItemDescMapper这个对象,通过这个对象调用.insert()可以实现将ItemDesc信息入库。
看程序中的注解,有注意事项!!!!
//===商品信息新增
@Transactional
@Override
public void saveItem(Item item,ItemDesc itemDesc) {
//1.保存商品时,设置商品默认的状态为“上架”状态。
item.setStatus(1);//在Item的pojo中 规定着1代表上架,2代表下架
//2.借用MP中的insert方法 完成商品信息的入库。
itemMapper.insert(item);
//3.由于Item中没有created和updated属性,要想保存Item,这俩属性就得有值。
//但每次都给这俩属性赋值又很麻烦。
//而这两个属性是在BasePojo中定义的。所以只要在BasePojo中动一些手脚就能达成,自动为created和updated赋值。
//4.通过itemDescMapper调用方法,将ItemDesc信息存入数据库中
/***
* 在保存Item时,要将“Item对应的ItemDesc”一同存入数据库中,怎么才能做到Item与ItemDesc的对应?
* 如果我直接写成itemDescMapper.insert(itemDesc);
* 方法中的参数只有一个itemDesc
* 而itemDesc只是ItemDesc这个POJO中的参数之一,POJO中的itemId却没有传过来,也就是为null了
* 那不行啊,ItemDesc这个对象在入库时也得有itemId,人家表中规定着呢。
* 那ItemDesc中的itemId上哪找去呢?
* 思路:itemDesc中的itemId 与 item中的itemId是 相同的!!!
* 那太好了了,只要把item中的itemId传过来就好了呀!
* itemDesc.setItemId(item.getId());
* 可以问题又来了,item中的itemId是主键自增的,它是什么时候才有值的呢?
* 它是在item信息入库之后,数据库中的itemId就有值了!
* 可是数据库并没有把itemId再回传给item对象。也就是说item对象中的itemId属性依然为null。
* 可是人家itemDesc也急等着itemId的值入库呢,并且ItemDesc要和Item一起入库的,可咋整。
* 方法:在item商品信息入库之后,库里的itemId就有值了,并且,我要求数据库将itemId动态地返回给item对象。怎么实现呢?
* 实现:在ItemMapper.xml中,在\<insert\>标签中,通过keyProperty ,keyColumn,useGeneratedKeys这几个属性进行标识,就可以将数据库中的itemId回传给Item对象中的itemId属性了。
* 记住:这个功能一般都是在insert操作中需要用的,所以一般都会写在insert标签中
* 但,其实只有我用手写sql的时候,才需要在ItemMapper.xml中这么做。
* 如果我用MP之后,MP深知我的这个需求,我在通过itemDescMapper,调用它的insert()方法时,MP会自动帮我把itemId从数据库中传回来。
* 所以即使我直接itemDesc.setItemId(item.getId()); 时,item.getId()是可以获取到itemId的。
*
*/
//MP帮我从数据库中将itemId回传给item了
itemDesc.setItemId(item.getId());
itemDescMapper.insert(itemDesc);
}
没用MP,手写sql时,需要在xml文件中的insert标签中这么配置一下:
3.5 商品详情的修改时的回显----SELECT
需求:
当我选中一个商品准备进行修改时,在修改页面不仅要显示Item的信息,还要显示ItemDesc的信息。
3.5.1 商品详情的修改时的回显—前端JS
其中
itemEditEditor.html(_data.data.itemDesc);就是在动态地获取ItemDesc的信息
3.5.2 商品详情的修改时的回显—服务端Controller—ItemController
/**
* 业务需求:在商品编辑页面,根据客户端传的ajax请求,回显这个商品的ItemDesc信息
* url:/item/query/item/desc/1474391989 1474391989这个就是这个商品在数据库中主键自增后的id
* 参数:data.id 就是itemId 获取到的就是主键id restful风格
* 返回值:SysResult对象中还携带着ItemDesc信息,返回给客户端
*/
@RequestMapping("/query/item/desc/{itemId}")
public SysResult findItemDescById(@PathVariable Long itemId) {
ItemDesc itemDesc = itemService.findItemDescById(itemId);
//由于需要将itemDesc回显回去,所以itemDesc要加在方法的参数中
return SysResult.success(itemDesc);
}
3.5.3 商品详情的修改时的回显—服务端Service—ItemService
ItemDesc findItemDescById(Long itemId);
3.5.4 商品详情的修改时的回显—服务端ServiceImpl—ItemServiceImpl
//====修改商品信息时,回显商品的详情ItemDesc
@Override
public ItemDesc findItemDescById(Long itemId) {
return itemDescMapper.selectById(itemId);
}
3.6 商品详情的修改–UPDATE
与ItemDesc的新增入库类似,在修改商品时,也是将修改后的商品信息和商品详情一同修改入库。
即在原update的方法参数中加一个ItemDesc参数
3.6.1 商品详情的修改—服务端Controller—ItemController
在ItemController中的updateItem方法中添加一个参数ItemDesc
/**
* 业务需求:完成商品信息的修改,返回系统vo对象SysResult
* 客户端发送的请求url: http://localhost:8091/item/update
* 参数:整个form表单中的数据,都在item对象中,所以将item当做参数,由客户端传递给服务器端就可以了
* 返回值:SysResult对象
*
* 由于又要同时修改itemDesc的信息,所以要把itemDesc传进来
*
*/
@RequestMapping("/update")
public SysResult updateItem(Item item,ItemDesc itemDesc) {
itemService.updateItem(item,itemDesc);
return SysResult.success();
}
3.6.2 商品详情的修改—服务端Service—ItemService
controller中的方法加参数了,Service中的方法肯定也得加参数ItemDesc
void updateItem(Item item, ItemDesc itemDesc);
3.6.3 商品详情的修改—服务端ServiceImpl—ItemServiceImpl
service中的方法加参数了,serviceImpl中的方法肯定也得加参数ItemDesc
与ItemDesc的新增入库类似,
updateItem()方法中的参数ItemDesc,只是ItemDesc对象属性中的一个属性的值,而另一个属性itemId还没有值。
要想进行update,就得获取到itemId的值。
//====商品信息修改
@Transactional
@Override
public void updateItem(Item item,ItemDesc itemDesc) {
//利用MP中的updateById()方法实现商品信息的修改。
itemMapper.updateById(item);
//要想保存itemDesc,方法参数中传过来的itemDesc只是ItemDesc对象中的一个属性的值。还得获取到itemDesc对象中itemId的值
//这个id值还得是通过item对象传过来
itemDesc.setItemId(item.getId());
//这时itemDesc对象中的信息才是完整的,就可以通过MP中的update方法去修改itemDesc对象了
itemDescMapper.updateById(itemDesc);
}
3.7 商品详情的删除–DELETE
需求:删除商品时将商品信息和商品详情信息一同删除。
由于都是根据商品的id进行删除,
并且item对象中的itemId与 itemDesc对象中的itemId是同一个。
所以只需修改业务层itemServiceImpl中的deleteItems方法即可。
即只要在deleteItems方法中加入一行删除itemDesc的业务代码即可。
//====商品信息删除
@Transactional
@Override
public void deleteItems(Long[] ids) {
//方法1,通过MP中的deleteBatchIds()方法
//由于这个方法要求参数是集合,所以我要先把ids这个数组转化为集合。而jdk中就有这样的一个方法
List<Long> idList = Arrays.asList(ids);
itemMapper.deleteBatchIds(idList);
itemDescMapper.deleteBatchIds(idList);
}
4 文件命名的一个小知识点:文件名重名
在同一个文件夹中,我曾试图把这个txt命名为aaa,但系统就提示我“已包含同名文件”。显然它觉得AAA.txt和aaa.txt文件名是一样的!!
而后缀名,大写小写都一样。
5 文件上传
在新增商品,修改商品界面都有“上传图片”的功能。
目前,点击“开始上传”会显示 上传失败,因为在项目代码中我还没写这部分功能,并且也还没配置nginx。
5.1 文件上传知识点回顾
文件传输,就是将文件变成0和1之后,通过IO流的形式,将内存中的文件保存到磁盘中,或者将磁盘中的文件倒回内存。
常用的IO流的对象有:
inputStream
outputStream
fileInputStream
fileOutputStream
BufferedInputStream
BufferedOutputSream
reader
writer
5.2 文件上传入门案例
5.2.1 入门案例—前端JS
老师为我们准备了一个file.jsp,用以展示文件上传的案例
其中的标签讲解:
<form> :一般上传文件,上传用户输入的内容等,都会用到form表单的形式。
action: 就是客户端发的ajax请求的url地址
method :就是客户端发的ajax请求的请求类型。这里之所以用post请求,而不用get请求,是因为get请求是将参数直接拼在url地址后面。而上传文件时的请求参数是0和1…这样就参数就会很长很长,地址栏放不下。所以传这种大型的数据时要用post请求。
enctype:上传多媒体类型的文件时,传递的是0,1这种字节信息,要加这个属性。否则服务端接收不到。
之前上传用户写的信息我都是用input = text…这种文本框的文本形式(传递的是字符信息)。
下面的第1行的<input> 中,type=“file”,说明我上传的东西可以是任意类型。
下面的第2行的<input> 中,type=“submit”,表示我一点击“提交”,客户端就开始发送ajax请求。
5.2.2 入门案例—服务器端Controller—FileController
既然客户端发送了ajax请求,那服务器端这边就要有对应的方法去接收。
我新建一个FileController,再在里面新建一个file方法,去接收客户端的请求。
@RestController //返回Json字符串
public class FileController {
/**
* demo :客户端访问file.jsp页面,点击上传,完成文件的上传。(由于还没进行配置,所以上传的文件大小不要超过1M)。
*
* @return
*/
@RequestMapping("/file")
//由于客户是想把文件上传到服务器,所以服务器中controller中要以输入流InputStream的格式接收fileImage信息
//但如果用InputStream,就又要写那一大堆共性代码。
//所以此处我用一个工具API----MultipartFile
public String file(MultipartFile fileImage){
//1.文件上传的目录(文件上传到哪里)
String fileDir = "D:/CGB2005IV/imgdemo";
//2.判断这个路径是否存在
File file = new File(fileDir);
if(!file.exists()){//如果文件不存在,就创建这个文件路径
file.mkdirs();//一次性创建多级目录
}
//3.要上传的文件的信息filename = 文件名 + 后缀名
String fileName = fileImage.getOriginalFilename(); //注意这个方法
//4.将这个文件要上传的路径+这个文件的信息 拼接成 这个文件的整体信息
//整体信息 = 要长传的路径 + 文件的信息
File imageFile = new File(fileDir+"/"+fileName);
//5.实现文件的上传,将文件的字节数组传输到imgdemo这个文件夹中
try {
fileImage.transferTo(imageFile); //注意这个方法
} catch (IOException e) {
e.printStackTrace();
}
return "文件上传成功!";
}
}
5.2.3 结果验证
在浏览器地址栏中输入localhost:8081/file
页面显示:
并且确实将图片传到了D:/CGB2005IV/imgdemo这个路径下。
表名我FileController中的代码是没有问题的。
5.3 实现京淘项目的图片上传功能
5.3.1 封装VO对象
图片上传这个功能中,需要一个VO对象。
这个对象身上的有4个属性,到时候服务器端需要给客户端返回的JSON数据 就长这样:
{“error”:0,“url”:“图片的保存路径”,“width”:图片的宽度,“height”:图片的高度}
说明:
error:
0 代表客户上传的确实是一张图片。如果是0,客户端才能成功解析并正确显示。即文件上传成功。
1 代表客户上传的不是图片。即文件上传失败
url地址: (这个要写网络地址)
我想查看图片,如果这个图片在我电脑的本机中,我可以通过物理地址访问。
如果这个图片存在于别人的电脑中,我只能通过网络地址才有可能能访问到。
访问图片的网络地址… 用户通过url地址获取图片信息
访问图片的物理地址… 真实存储的地址 D:/a/a.jpg
width和height 指的是图片的宽度和高度。
如果不给它们赋值,就表示默认用图片原来的大小去让客户端展示。
这两个属性是图片特有的属性。
所以我想判断客户传的文件是不是图片文件时,可以根据查客户传的这个文件的“宽”和“高”来判断。
如果这个文件的“宽”和“高”有值,说明它是一个图片,否则它就不是一个图片文件。
@Data
@Accessors(chain = true)
@NoArgsConstructor
@AllArgsConstructor
public class ImageVO implements Serializable {
// {"error":0,"url":"图片的保存路径","width":图片的宽度,"height":图片的高度}
private Integer error;
private String url;
private Integer width;
private Integer height;
//success fail
//如果图片上传失败,就调用这个fail方法,告诉客户端
public static ImageVO fail(){
return new ImageVO(1, null, null, null);
}
//如果图片上传成功了,就调用这个success方法,告诉客户端
public static ImageVO success(String url,Integer width,Integer height){
return new ImageVO(0,url,width,height);
}
}
5.3.2 文件上传页面发送的ajax请求中的url分析
参数说明:
通过在程序中按CTRL+H搜索 pic/upload
可以发现,这个是在common.js中的第26行定义的
富文本编辑器KindEditor框架和EasyUI框架有点儿不一样。
EasyUI框架中我要是发ajax请求,url处,我就把想访问的url写上就行了。
但KindEditor框架框架中我得单独写一个kingEditorParams这么一个JSON对象。
5.3.3 编辑pro配置文件
说明: 为了将来实现项目的扩展性,将核心的配置写入image.properties文件中
#properties的作用就是封装key=value 业务数据
image.dirPath=D:/JT-SOFT/images
image.urlPath=http://image.jt.com
5.3.4 编辑FileController
package com.jt.controller;
import com.jt.service.FileService;
import com.jt.vo.ImageVO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
@RestController
public class FileController {
/**
* MultipartFile 接口作用 主要就是优化了文件上传 API集合
* 1. 文件上传位置??? D:\JT-SOFT\images
* 2. 判断一下文件目录是否存在
* 3. 利用API实现文件上传.
*/
@RequestMapping("/file")
public String file(MultipartFile fileImage){
String fileDir = "D:/JT-SOFT/images";
File file = new File(fileDir);
if(!file.exists()){ //文件不存在则创建文件
file.mkdirs(); //一次性创建多级目录
}
//文件信息 = 文件名+文件后缀
String fileName = fileImage.getOriginalFilename();
//将文件的整体封装为对象 文件路径/文件名称
File imageFile = new File(fileDir+"/"+fileName);
//实现文件上传,将文件字节数组传输到指定的位置.
try {
fileImage.transferTo(imageFile);
} catch (IOException e) {
e.printStackTrace();
}
return "文件上传成功!!!!";
}
/**
* 业务:实现商品的文件上传操作
* url地址: http://localhost:8091/pic/upload?dir=image
* 参数: uploadFile 注意字母的大小写
* 返回值结果: ImageVO对象.
*/
@Autowired
private FileService fileService;
@RequestMapping("/pic/upload")
public ImageVO upload(MultipartFile uploadFile){
//将所有的业务操作,放到Service层中完成!!!
return fileService.upload(uploadFile);
}
}
5.3.5 编辑FileService
package com.jt.service;
import com.jt.vo.ImageVO;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.PropertySource;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;
@Service
@PropertySource("classpath:/properties/image.properties")
public class FileServiceImpl implements FileService{
@Value("${image.dirPath}")
private String dirPath;
@Value("${image.urlPath}")
private String urlPath;
//为了防止Set集合每次都要创建,则通过static代码块的形式负责封装数据
private static Set<String> imageSet = new HashSet<>();
static {
imageSet.add(".jpg");
imageSet.add(".png");
imageSet.add(".gif");
//....
}
/**
* 文件上传具体步骤:
* 1.如何校验用户上传的是图片? jpg|png
* 2.如何访问用户上传恶意程序 木马.exe.jpg 宽度*高度
* 3.应该采用分目录存储的方式 保存数据
* 4.上传的文件名称应该尽量避免重名 自定义文件名称... UUID.后缀...
* @param uploadFile
* @return
*/
@Override
public ImageVO upload(MultipartFile uploadFile) {
//1.校验图片类型是否正确 jpg|png|gifxxxx 1.正则表达式判断 2.准备集合之后进行校验Set<去重>
//1.1 获取上传的图片类型 ABC.JPG
String fileName = uploadFile.getOriginalFilename(); //文件的全名 abc.jpg
fileName = fileName.toLowerCase(); //将所有的字符转化为小写
int index = fileName.lastIndexOf(".");
String fileType = fileName.substring(index); //含头不含尾
//1.2判断是否为图片类型 bug
if(!imageSet.contains(fileType)){
//用户上传的不是图片
return ImageVO.fail();
}
//2.上传的数据是否为恶意程序. 高度和宽度是否为null. 利用图片API
//BufferedImage对象 专门负责封装图片
try {
BufferedImage bufferedImage = ImageIO.read(uploadFile.getInputStream());
int width = bufferedImage.getWidth();
int height = bufferedImage.getHeight();
if(width==0 || height ==0){
return ImageVO.fail();
}
//3.采用分目录存储的方式 a.jpg
//String dirPath = "D:/JT-SOFT/images"; //动态获取
//3.1 分目录存储方式1 hash方式 ACBBCDD
//3.1 分目录存储方式2 时间方式存储 yyyy/MM/dd
String dateDir = new SimpleDateFormat("/yyyy/MM/dd/").format(new Date());
//3.2 准备文件存储的目录
String imageDir = dirPath + dateDir;
File imageFileDir = new File(imageDir);
if(!imageFileDir.exists()){
imageFileDir.mkdirs();
}
//4 实现文件上传
//4.1 动态拼接文件名称 uuid.后缀 f3aa1378-ece6-11ea-98c9-00d861eaf238
String uuid =
UUID.randomUUID().toString().replace("-", "");
String realFileName = uuid + fileType;
//4.2 准备文件上传的全路径 磁盘路径地址+文件名称
File imageFile = new File(imageDir+realFileName);
//4.3 实现文件上传
uploadFile.transferTo(imageFile);
//5.动态生成URL地址
//请求协议: http:// https:// 带证书的网址 安全性更高 公钥私钥进行加密解密.
//向服务器运行商购买域名 com cn org hosts文件
//图片存储的虚拟地址的路径 动态变化的路径
//http://image.jt.com/2020/09/02/uuid.jpg
String url = urlPath+dateDir+realFileName;
return ImageVO.success(url,width,height);
} catch (IOException e) {
e.printStackTrace();
return ImageVO.fail();
}
}
}
5.3.6 上传效果
6 Nginx服务器
6.1 业务需求分析
1.本地磁盘路径
D:/JT-SOFT/images/2020/09/04/21e815df0e1642a0adf417515b8c39b3.png;
2.网络虚拟路径
http://image.jt.com/2020/09/04/0714c3d41ac9409a934dff98f4a2db3a.png;
别人可以通过在浏览器总输入网络虚拟路径 就能 访问到本地磁盘路径中的 资源。
问题: 如何通过虚拟地址来找到真实的磁盘地址,之后为用户响应数据.
通过某种机制可以实现域名与磁盘地址的映射.
6.2 反向代理说明
反向代理服务器位于用户与目标服务器之间,但是对于用户而言,反向代理服务器就相当于目标服务器,即用户直接访问反向代理服务器就可以获得目标服务器的资源。
同时,用户不需要知道目标服务器的地址,也无须在用户端作任何设定。反向代理服务器通常可用来作为Web加速,即使用反向代理作为Web服务器的前置机来降低网络和服务器的负载,提高访问效率。
总结:
1.反向代理服务器介于用户和真实服务器之间
2.用户以为反向代理服务器就是真实的服务器
3.用户不需要了解真实的服务器到底是谁.
4反向代理服务器保护了真实服务器信息.
5.反向代理服务器是服务器端代理.
6.3 正向代理说明
正向代理,意思是一个位于客户端和原始服务器(origin server)之间的服务器,为了从原始服务器取得内容,客户端向代理发送一个请求并指定目标(原始服务器),然后代理向原始服务器转交请求并将获得的内容返回给客户端。客户端才能使用正向代理。
特点:
1.代理服务器介于目标和客户端之间
2.客户端非常清楚自己访问的服务器到底是谁
3.正向代理是客户端代理.保护了真实的客户信息.
一般条件下网络通讯时会使用正向代理.
6.4 Nginx介绍
Nginx (engine x) 是一个高性能的HTTP和反向代理web服务器,同时也提供了IMAP/POP3/SMTP服务。Nginx是由伊戈尔·赛索耶夫为俄罗斯访问量第二的Rambler.ru站点(俄文:Рамблер)开发的,第一个公开版本0.1.0发布于2004年10月4日。
其将源代码以类BSD许可证的形式发布,因它的稳定性、丰富的功能集、示例配置文件和低系统资源的消耗而闻名。2011年6月1日,nginx 1.0.4发布。
Nginx是一款轻量级的Web 服务器/反向代理服务器及电子邮件(IMAP/POP3)代理服务器,在BSD-like 协议下发行。其特点是占有内存少,并发能力强,事实上nginx的并发能力在同类型的网页服务器中表现较好,中国大陆使用nginx网站用户有:百度、京东、新浪、网易、腾讯、淘宝等。
特点:
1.内存少 不超过2M tomcat服务器启动 300-500M
2.并发能力强 并发3万-5万次 tomcat并发 150-220之间
3.Nginx使用C语言开发.
6.5 Nginx安装和下载
6.5.1 nginx下载
6.5.2 nginx安装的注意事项
1.Nginx服务器启动时会占用80端口.
2.Nginx服务安装时不要出现中文+空格问题 C盘慎用.(程序员操守)
3.Nginx底层开发是用C语言写的,.所以注释 使用#号 独占一行完成注释
6.5.3 nginx命令
命令:
启动命令: start nginx
重启命令: nginx -s reload
关闭命令: nginx -s stop
6.5.4 nginx服务项说明
1.nginx主进程 主要提供反向代理服务.
2.nginx守护进程 防止主进程意外关闭的.
如果将来想要关闭nginx服务器,则应该先关闭守护再关闭主进程.
6.6 反向代理入门案例
# nginx 需要使用http/https协议的
http {
#反向代理服务 一个服务就是一个server
server {
# nginx监听的端口号 默认监听80端口
listen 80;
# server名称 业务逻辑名称
server_name localhost;
# 反向代理实现 / 代表拦截所有请求
location / {
# root 转向到目录中 html index 默认访问页面
root html;
index index.html index.htm;
}
}
}
7 京淘商品图片回显
7.1 需求说明
使用url:http://image.jt.com:80/2020/09/04/0714c3d41ac9409a934dff98f4a2db3a.png 找到位于
D:\JT-SOFT\images\2020/09/04/0714c3d41ac9409a934dff98f4a2db3a.png目录下的文件.最终实现图片回显.
分析:
拦截的域名: image.jt.com.
拦截的端口:80
转向的目录: D:\JT-SOFT\images
7.2 nginx反向代理配置
说明:当修改完成nginx之后.则重启nginx
#配置图片服务器
server{
listen 80;
server_name image.jt.com;
location / {
#由于windows操作系统问题 所以需要替换/
root D:/JT-SOFT/images;
}
}
7.3 关于HOSTS文件说明
说明:操作系统为了开发人员测试方便,可以通过hosts执行文件的域名与IP的映射关系.如果配置了hosts文件,则先走hosts之后执行全球DNS域名解析服务.
7.4 编辑hosts文件
说明: 操作系统为开发者提供了一个hosts文件**.该文件可以实现域名与IP地址的映射关系**.但是由于只是测试时使用.所以该配置只对本机有效.
hosts文件位置:
具体配置(可以借助工具SwitchHosts)
#@SwitchHosts! {"url": null, "icon_idx": 0, "title": "\u5f53\u524d\u7cfb\u7edf hosts"}
# 京淘配置
#左侧写IP地址 右侧写域名 中间使用空格分隔
#为了实现Linux发布修改如下
#192.168.126.129 image.jt.com
#192.168.126.129 manager.jt.com
127.0.0.1 image.jt.com
127.0.0.1 manage.jt.com
127.0.0.1 www.jt.com
127.0.0.1 sso.jt.com
7.5 关于Nginx实现域名代理
7.5.1 需求说明
用户通过域名 http://manage.jt.com:80的域名 要求访问http://localhost:8091的服务器.
7.5.2 编辑host文件
# 京淘配置
#左侧写IP地址 右侧写域名 中间使用空格分隔
127.0.0.1 image.jt.com
127.0.0.1 manage.jt.com
127.0.0.1 www.jt.com
#Bug 有时在使用该软件时可能会出现丢失字母的现象.
127.0.0.1 sso.jt.com
7.5.3 编辑Nginx服务器
#配置后台管理系统
server {
listen 80;
server_name manage.jt.com;
location / {
#root 代表文件目录
#index 代表默认的访问页面
#proxy_pass 代表发起url请求
proxy_pass http://localhost:8091;
}
}
8 搭建服务器集群
8.1 集群分析
需求:根据用户的反向代理的调用.用户不清楚自己到底访问的是哪台服务器.那么应该如何测试负载均衡呢???
解决方案: 通过1个url请求获取访问服务器端口号即可.
8.2 动态获取端口
8.2.1 动态获取端口
8.2.2 编辑PortController
package com.jt.controller;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class PortController {
/**
* 通过Spring容器动态获取YML配置文件中的端口即可
*/
@Value("${server.port}")
private int port;
@RequestMapping("/getPort")
public String getPort(){
return "当前访问的端口号为:"+port;
}
}
8.3 搭建tomcat服务器集群
8.3.1 需求
将京淘后台管理系统打成3个war包程序. 端口号分别为8081/8082/8083,之后通过java命令启动3台服务器.
java -jar 808X.war ;
8.3.2 项目打包操作
说明:先修改端口号之后,将maven进行打包操作.
8.3.3 项目运行测试
一次性将8081-8083打包之后测试即可.
效果展现:
9 实现Nginx集群配置
9.1 业务说明
当用户通过manage.jt.com的方式访问服务器时,要求通过反向代理的方式实现.要求配置nginx中集群.
9.2 配置nginx
#配置后台管理系统
server {
listen 80;
server_name manage.jt.com;
location / {
#root 代表文件目录
#index 代表默认的访问页面
#proxy_pass 代表发起url请求
#proxy_pass http://localhost:8091;
proxy_pass http://jtW;
}
}
#配置集群的关键字 通过集群配置tomcat服务器即可
#默认: 1.轮询的机制
upstream jtW {
server 127.0.0.1:8081;
server 127.0.0.1:8082;
server 127.0.0.1:8083;
}
10 关于nginx负载均衡策略说明
10.1 轮询策略
说明:根据配置文件的顺序,之后依次访问服务器. 该策略也是默认的机制.
#默认: 1.轮询的机制
upstream jtW {
server 127.0.0.1:8081;
server 127.0.0.1:8082;
server 127.0.0.1:8083;
}
10.2 权重策略
场景说明: 公司采购服务器都是有时间间隔的. 但是由于服务器新旧不同,硬件版本不同,导致服务器处理能力不同!!!
如果上述的问题不做处理,依然采用轮询的机制,则会出现严重的负载不均衡的现象.
所以需要通过权重的方式平衡压力.
#配置集群的关键字 通过集群配置tomcat服务器即可
#默认: 1.轮询的机制 2.权重策略
upstream jtW {
server 127.0.0.1:8081 weight=6;
server 127.0.0.1:8082 weight=3;
server 127.0.0.1:8083 weight=1;
}
10.3 IPHASH策略
需求:当某些业务需要用户特定的访问固定的服务器时,就要选用iphash机制.
配置:
#默认: 1.轮询的机制 2.权重策略 3.IPHASH
upstream jtW {
ip_hash;
server 127.0.0.1:8081 weight=6;
server 127.0.0.1:8082 weight=3;
server 127.0.0.1:8083 weight=1;
}
11 关于Nginx负载均衡补充
11.1 down属性
说明:如果tomcat服务器发生了宕机的现象,则通过配置文件标识down的属性,则nginx将不会再次访问故障机.
#默认: 1.轮询的机制 2.权重策略 3.IPHASH
upstream jtW {
#ip_hash;
server 127.0.0.1:8081 down;
server 127.0.0.1:8082 ;
server 127.0.0.1:8083 ;
}
11.2 backup属性
说明:通常情况下 都会部署一些备用机防止由于主机宕机,剩余的机器不能实现高负责从而导致整个服务宕机的问题.
如果设置了备用机,则正常情况下用户不会访问.但是当主机宕机或者主机遇忙时才会访问.
#配置集群的关键字 通过集群配置tomcat服务器即可
#默认: 1.轮询的机制 2.权重策略 3.IPHASH
upstream jtW {
#ip_hash;
server 127.0.0.1:8081 down;
server 127.0.0.1:8082 ;
server 127.0.0.1:8083 backup;
}
11.3 tomcat高可用配置
属性配置:
1.max_fails=1 配置nginx访问服务器的最大的失败次数.
2.fail_timeout=60s; 理解为一个时间周期. 如果发现服务器宕机,则在60秒内不会再次访问故障机.
#配置集群的关键字 通过集群配置tomcat服务器即可
#默认: 1.轮询的机制 2.权重策略 3.IPHASH
upstream jtW {
#ip_hash;
server 127.0.0.1:8081 max_fails=1 fail_timeout=60s;
server 127.0.0.1:8082 max_fails=1 fail_timeout=60s;
server 127.0.0.1:8083 max_fails=1 fail_timeout=60s;
}
11.4 关于IP地址获取命令
service NetworkManager stop
chkconfig NetworkManager off 永久关闭 Manager网卡
service network restart 重启network网卡