工程结构:
taotao-parent:仅仅用来管理整个项目的依赖版本和插件信息 并不是主工程 也不会在此工程编写任何代码 因为只是管理项目的依赖和版本 所以创建pom工程
taotao-common:在多个子系统中都需要使用的一些公共类,所以定义一个
taotao-common工程,用来编写通用的java 类 taotao-common工程作为一个存放通用工具类的jar工程,实际运行时或者发生改变后,需要安装到maven的仓库中,操作是: 选中taotoa-common工程,右键,maven maven instatll
聚合工程的父工程必须使用pom打包方式
导入依赖的原则:
所有的子工程都需要的依赖应该在聚合工程(taotao-manage)中导入
在使用依赖的最底层系统中导入
运行时所需要的依赖在web工程中导入
nginx+tomcat架构下输入url到地址栏的访问流程
流程:host->nginx->tomcat
用户访问 http://manage.taotao.com/rest/page/index
host域名解析,得到地址:127.0.0.1 www.taotao.com
因为是默认端口80,被Nginx监听到
Nginx将服务转发到:127.0.0.1:8081
tomcat监听8081,所以最终被Tomcat处理
Spring4的泛型注入
在Spring4中使用通用Mapper
Spring4增加了对泛型注入的支持 这个特性对于通用Mapper来说 非常有用 可以说有了这个特性 可以直接在Service中写 Mapper mapper 可以通过BaseService 来实现通用的Service
public abstract class BaseService_bak<T> {
public abstract Mapper getMapper();
/**根据主键查询
* @param id
* @return
*/
public T queryById(Object id){
return this.getMapper().selectByPrimaryKey(id);
}
}
public class BaseService<T extends BasePojo> {
@Autowired
public Mapper mapper;
/**根据主键查询
* @param id
* @return
*/
public T queryById(Object id){
return mapper.selectByPrimaryKey(id);
}
}
添加商品和添加商品描述的业务逻辑应该放在一个Service方法中 因为要考虑到事务
图片上传后台处理逻辑
@Controller
@RequestMapping("/pic")
public class PicUploadController {
private static final Logger LOGGER = LoggerFactory.getLogger(PicUploadController.class);
@Autowired
private ProperttiesService properttiesService;
// 允许上传的格式
private static final String[] IMAGE_TYPE = new String[] { ".bmp", ".jpg", ".jpeg", ".gif", ".png" };
/**
*
* @param uploadFile
* @param response
* @return 返回文本类型的json数据
* @throws Exception
*/
@RequestMapping(value = "/upload", method = RequestMethod.POST, produces = MediaType.TEXT_PLAIN_VALUE)//指定响应类型为text/plain
@ResponseBody
public String upload(@RequestParam("uploadFile") MultipartFile uploadFile, HttpServletResponse response)
throws Exception {
// 校验图片格式 循环校验 使用后缀校验
boolean isLegal = false;
for (String type : IMAGE_TYPE) {
if (StringUtils.endsWithIgnoreCase(uploadFile.getOriginalFilename(), type)) {
isLegal = true;
break;
}
}
// 封装Result对象,并且将文件的byte数组放置到result对象中
PicUploadResult fileUploadResult = new PicUploadResult();
// 状态
fileUploadResult.setError(isLegal ? 0 : 1);
// 文件新路径
String filePath = getFilePath(uploadFile.getOriginalFilename());
//如果开启debug模式 那么记录上传文件的日志
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Pic file upload .[{}] to [{}] .", uploadFile.getOriginalFilename(), filePath);
}
// 生成图片的绝对引用地址
String picUrl = StringUtils.replace(StringUtils.substringAfter(filePath, properttiesService.REPOSITORY_PATH),
"\\", "/");
fileUploadResult.setUrl(properttiesService.IMAGE_BASE_URL + picUrl);
File newFile = new File(filePath);
// 写文件到磁盘
uploadFile.transferTo(newFile);
// 校验图片是否合法
isLegal = false;
try {
BufferedImage image = ImageIO.read(newFile);
if (image != null) {
fileUploadResult.setWidth(image.getWidth() + "");
fileUploadResult.setHeight(image.getHeight() + "");
isLegal = true;
}
} catch (IOException e) {
}
// 状态
fileUploadResult.setError(isLegal ? 0 : 1);
if (!isLegal) {
// 不合法,将磁盘上的文件删除
newFile.delete();
}
//将java对象序列化为json字符串
return properttiesService.Mapper.writeValueAsString(fileUploadResult);
}
/**传入文件名获取生成的包含路径的文件名
* @param sourceFileName
* @return
*/
//C:\\upload\\images\\2018\\05\\0820151113111111111.jpg
private String getFilePath(String sourceFileName) {
String baseFolder = properttiesService.REPOSITORY_PATH + File.separator + "images";
Date nowDate = new Date();
// yyyy/MM/dd
String fileFolder = baseFolder + File.separator + new DateTime(nowDate).toString("yyyy")
+ File.separator + new DateTime(nowDate).toString("MM") + File.separator
+ new DateTime(nowDate).toString("dd");
File file = new File(fileFolder);
if (!file.isDirectory()) {
// 如果目录不存在,则创建目录
file.mkdirs();
}
// 生成新的文件名
String fileName = new DateTime(nowDate).toString("yyyyMMddhhmmssSSSS")
+ RandomUtils.nextInt(100, 9999) + "." + StringUtils.substringAfterLast(sourceFileName, ".");
return fileFolder + File.separator + fileName;
}
}
图片上传后解决图片回显问题
解决方案:
以前:我们将图上传后直接放到项目所在的路径中,这样用户访问图片,其实是通过Tomcat来访问。
真实开发中,不会有人这样玩,为什么?
Tomcat并发能力差
不利于项目的水平扩展
通过昨天的学习我们已经知道:Tomcat是应用服务器,主要作用处理动态请求
而Nginx等Web服务器,其作用才是处理静态资源的请求。因为WEB服务器的并发能力远远高于Tomcat这样把静态资源交给Nginx处理,减去了Tomcat压力,提高了项目的并发能力!
Spring的父子容器
Spring容器 – 父容器
SpringMVC容器 – 子容器
父子容器的关系:
1. 子容器能够访问父容器的资源(bean)a:示例:Controller可以注入Service
2. 父容器不能访问子容器的资源(bean)
跨域问题
可以看出来,以下情况都属于跨域:
* 不同域名
* 不同二级域名
* 同一域名不同端口
==> 两个应用之间的调用就是跨域
跨域问题出现的场景:浏览器对ajax请求的限制,不允许跨域请求资源。其它情况没有跨域问题。
同源策略
实现统一的Jsonp支持
如果我们有很多的跨域请求,那每个方法都要这样写,是不是很繁琐?有什么好的解决方案?Spring是否默认支持呢?
实际上,spring并不支持,所以我们得自己来.
说明:这里我们提供两种解决方案,自定义消息转化器和使用springmvc的controller增强通知 AbstractJsonpResponseBodyAdvice,各位自行选择 二选一
前台系统以什么方式来访问后台系统呢?
我们已知的接口访问的方式:
JS访问
Ajax,不能跨域
Jsonp,可以跨域,但是只支持GET请求
Java代码访问
webService,太重
HttpClient,轻量级
httpClient的特点
1. 基于标准、纯净的Java语言。实现了Http1.0和Http1.1
2. 以可扩展的面向对象的结构实现了Http全部的方法(GET, POST, PUT, DELETE, HEAD, OPTIONS, and TRACE)。
3. 支持HTTPS协议。
4. 通过Http代理建立透明的连接。
5. 自动处理Set-Cookie中的Cookie。
其主要作用就是通过Http协议,向某个URL地址发起请求,并且获取响应结果。
taotao项目构建整个流程思路整理
这个项目作为多系统分布式项目,由于是不断迭代演进,所以理清思路很重要
taotao-parent作为父工程 只起到管理项目依赖版本的目的 打包方式为pom方式
taotao-manage作为聚合工程的maven project 可以理解为后台系统的抽象 打包方式也为pom方式 后台系统启动是启动taotao-manage这个module 它的parent工程为taotao-parent
四个module的parent工程为taotao-manage 其中 依赖关系自上而下 除了taotao-manage-web打包方式war 其余都为jar方式
taotao-common 为管理公共功能的 父工程为taotao-parent 打包方式为jar 四层结构中需要相关功能直接引入common的jar包即可 同时 注意的是taotao-common如果被修改 必须 maven install 否则不能生效
重要步骤结点如下
①搭建整个框架 测试
配置文件 尤其是web.xml配置文件 spring文件夹下的配置文件都要谨慎配置 然后配置域名 nginx配置 host配置 启动nginx 测试 WEB-INF文件夹下的静态文件和webapp下的欢迎页面都能正常访问springmvc的url-pattern配置要注意 前后台配置不一样 和以前单独学习阶段也不一样applicationContext.xml中读取资源文件 在新增资源文件后不能忘记配置
写一个通用方法作为统一的访问视图页面的方法
@Controller
@RequestMapping("page")
public class PageController {
/**统一的访问视图页面的处理方法
* @param viewName
* @return
*/
@RequestMapping("{viewName}")
public String toPage(@PathVariable("viewName")String viewName){
return viewName;
}
}
②商品类目展示
商品列表按树形展示 以弹框形式展现 其实就是以一个分类id去查询子分类数据的逻辑 默认发送分类id为0
重要的是和easyUI交互 如果在实体中没有需要的字段 那么需要在pojo中添加
public String getText(){
return this.getName();
}
public String getState(){
return this.getIsParent()?"closed":"open";
}
③ 新增商品 图片上传
这一部分在新增商品上主要是注意事务问题 因为商品信息和商品描述信息是在两个表里面 要保证事务必须将两个逻辑在一个service中实现 这样才能实现出现异常回滚,通过打印日志也能看到只有一次start transaction 而如果在两个service中实现 那么其实是有两次的。
封装BaseService 因为在service层很多方法的逻辑都是重复的 所以封装了BaseService 它有两个版本 第一个版本 需要子类实现抽象方法 这样实现了向父类传递泛型T的目的 第二种方法 直接在父类中注入 Mapper 不需要子类实现抽象方法
图片上传复制现成的代码 其中一个类是处理上传逻辑的 另一个类是封装返回结果对象的。可以先采取将配置写死 实现上传功能后在优化 路径 回显等相关的配置 上传代码的思路很重要 记住 首先要导入依赖包 在springmvc中配置上传文件解析器 如果需要对异常进行处理还需要写异常处理器代码和配置处理器异常解析器
处理路径和回显问题:
上传路径需要在配置文件中灵活配置 路径回显示问需要在配置文件中配置图片相关子域名
注意配置过图片域名image.taotao.com后访问图片路径不通过tomcat处理的。利用的是nginx的处理静态资源的功能
a:写一个配置文件添加上传文件路径配置和图片域名配置
b:添加到applicationContext.xml资源文件扫描列表中
c:写一个专门的service文件读取配置文件中的相关配置 PropertiesService.java
d:将PropertiesService类注入上传文件的Controller中
e:为了使用jackson方便 在PropertiesService中配置
上面的处理过程中引入了父子容器的概念
spring和springmvc的容器是独立的容器 子容器可以读取父容器 父容器不能读取子容器
④ 商品列表编辑和搭建前台系统相关功能
商品编辑需要显示商品分类 所以需要在Item表中增加cname 方便显示使用 同时需要在添加商品逻辑中添加这个字段的内容
搭建taotao-web工程 父工程是taotao-parent 打包方式为war 配置端口为8082 配置host nginx 同时复制后台的相关配置文件过去
注意前台不需要jdbc相关和mybatis相关 修改web.xml中springmvc的url-pattern的路径为.html
测试WEB-INF和webapp下的文件都能正常访问 注意访问webapp下的静态文件也是经过tomcat的
⑤ 首页商品类目显示 跨域问题
前台系统搭建完毕需要显示前台分类
分析前台商品类目显示的json数据格式 在线分析出类目显示依赖的三个实体类 ItemCatResult ItemCatData ItemCataData2
考虑到字段名称和json中的key不一致 所以需要加注解@JsonProperty
然后在后台service层写封装逻辑 将数据结构封装成 ItemCatResult结构
public ItemCatResult queryAllItemCatToTree() {
//三层for循环
}
//为了避免出错 可以先在controller层写伪代码通过浏览器访问生成json数据看结构是否是需要的结构 如果可以再继续写service层结构
//优化service的代码 将分类数据封装到map结构 这样原本需要在三层循环中都进行N此查询数据库的操作缩减为只需要一次查询了
//获取所有的分类数据
List<ItemCat> itemCatList = super.queryAll();
//对商品类目进行整理
Map<Long,List<ItemCat>> map=new HashMap<Long,List<ItemCat>>();
for (ItemCat itemCat : itemCatList) {
//判断当前类目的父类目是否已经存在于map中
if(!map.containsKey(itemCat.getParentId())){
//如果不存在 就直接增进去
map.put(itemCat.getParentId(), new ArrayList<ItemCat>());
}
//如果map中绝对已经存在当前类目的父类目集合
List<ItemCat> list = map.get(itemCat.getParentId());
list.add(itemCat);
}
整合以上controller和service的操作 访问浏览器 http://manage.taotao.com/rest/api/item/cat/all 可以得到完整的json数据了
将返回的json数据替换前台taotao-web中的模拟json文件中的数据 发现前台系统分类数据也就可以正常访问了 但是如果将js请求的接口替换为后台却不行!出现了跨域问题
单独谈跨域问题
复制测试跨域的两个文件到后台测试 发现能正常请求 不存在跨域情况
复制请求js文件到前台访问后台文件发现不能正常弹出返回的数据 是跨域造成的
跨域是指由于浏览器的同源测试 导致 只要是域名 端口不同出现不能正常发送ajax请求的问题
但是为什么html页面的script标签可以正常加载js文件呢 这是因为 script不受到跨域限制 但是只能通过script标签请求js文件 而不能是jsp文件 但是可以把jsp文件返回的json数据转化为js字符串来解决
<script type="text/javascript" src="http://manage.taotao.com/js/jquery-easyui-1.5.1/jquery.min.js" >script>
<script type="text/javascript">
alert($); //验证jquery.min.js是否加载成功
function abc(msg){
alert(msg.abc)
}
$(function(){
//尝试异步加载json.jsp的内容
$.ajax({
url : "http://manage.taotao.com/json.jsp",
type : "get",
dataType : "jsonp",
success : function(data){
alert(data.abc);
}
});
})
script>
后台的json.jsp文件
<%
//获取回调函数名称
String callback=request.getParameter("callback");
//判断名称是否为null
if(StringUtils.isNoneEmpty(callback)){
//如果有 返回带回调函数的数据
out.print(callback+"({\"abc\":123})");
}else{
//如果没有 则返回普通数据
out.print("{\"abc\":123}");
}
%>
jquery提供的解决跨域问题的方式很简单 就是将 dataType:json 改为 dataType:jsonp 同时在后端接收key为callback的值 将callback拼接后当做字符串返回回去
@RequestMapping("all")
public ResponseEntity getItemCat(
@RequestParam(value="callback",required=false)String callback){
try {
ItemCatResult result=this.itemCatService.queryAllItemCatToTree();
//手动将对象转换成json格式
String jsonResult=properttiesService.Mapper.writeValueAsString(result);
if(StringUtils.isNotBlank(callback)){
//如果callback不为空 说明请求是跨域请求 需要将返回值组装成js格式返回
jsonResult=callback+"("+jsonResult+")";
}
return ResponseEntity.status(HttpStatus.OK).body(jsonResult);
} catch (Exception e) {
e.printStackTrace();
}
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(null);
}
这样处理的话 还会出现乱码问题 解决的思路是在springmvc的注解驱动中配置消息转换器设置编码为UTF-8
这样写太繁琐 还有两种方式实现统一的jsonp支持
第一种是 自定义消息转换器
第二种是使用springmvc的controller增强通知AbstractJsonpResponseBodyAdvice
具体实现 参见笔记中介绍
每次都要请求后端接口获取分类数据很麻烦 后面还要考虑为获取分类数据添加redis缓存
前台发送ajax请求是在lib-v1.js中
⑥ 首页广告轮播图 和 httpclient
首页轮播图显示想要完成后台的轮播图内容管理系统 主要是分类删除是有点麻烦 需要递归获取结点的子节点删除
is_parent属性标识该分类是否是父分类 如果有子分类 那么是1 如果没有 则为0 因此在处理新增分类 删除分类的时候需要处理父分类的is_parent
在设置字段属性值的时候 只需要设置true和false mybatis会自动完成布尔值到数据库数值类型的转换
在数据库tb_content_category 中 is_parent
tinyint(1) DEFAULT ‘1’ COMMENT ‘该类目是否为父类目,1为true,0为false’,
那么问题是前台系统怎么获取后台系统添加的轮播图呢
这里可以通过ajax请求后端接口 也可以通过httpclient这种java请求java的方式 类似于php中的curl
首先测试httpclient的包中的相关方法的功能 其次 需要根据httpclient包中的HttpConnectManage类编写applicationContext-httpclient.xml文件
同时编写IdleConnectionEvictor 同时在xml中增加定期关闭失效连接的配置
因为后台功能里面有内容列表 所以代码已经写好了
接下来就是前台请求后台获取数据的逻辑了 因为后台给的json数据是以easyUI形式给的 不是页面需要的json数据格式 所以需要将后台返回的json数据处理一下转换为map结构 然后将map结构转换为json格式 这是一种非常重要的处理思路 以后可能还会用到
mapList = new ArrayList
这里还可以对HttpclientService类进行优化
使用相应解析器 获取EasyUIResult对象
⑦ 缓存相关
这里谈缓存主要是redis缓存
如果是前台js请求后端接口数据 比如首页分类数据请求后端接口 需要在后端逻辑中加入缓存处理
如果是前台httpclient请求后端接口 那么需要在前台和后台都加上redis缓存
关于技术选择 如果是对持久化 数据结构和处理有复杂要求的 肯定选用redis 如果仅仅是基本的key-value结构的基本get和set需求 建议使用memcache
redis的window安装方式需要注意一下
Redis中支持的数据类型:string(字符串key-value) hash(散列类型) list(列表类型) set(集合类型) zset(有序集合)
Redis值的生存时间设置
返回值:
1)返回剩余的过期时间
2)-1:永不过期
3)-2:已过期或不存在
清除生存时间
返回值:
1)当生存时间移除成功时,返回1
2)如果key不存在或者key没有设置生存时间 返回0
通过算法CRC16算法对abc取值 得到一个数值 使用该数值对16384取余 得到余数范围:0-16383 这就是插槽
搭建redis集群 需要预先分配插槽
淘汰逻辑是LRU算法 淘汰最不常使用的缓存
其次就是 spring整合jredis
导入itcast-redis包 测试JedisDemo JedisPoolDemo ShardedJedisPoolDemo类 其中第一个为基本的set get测试
第二个为使用连接池实现的set get测试 第三个使用集群连接池实现的set get测试
根据上面的第三个文件写出 applicationContext-redis.xml配置文件 封装RedisService工具类
优化RedisService代码 封装一下后如下
public interface Function {
T callback(E e);
}
@Service
public class RedisService {
@Autowired(required=false)
public ShardedJedisPool shardedJedisPool;
private T execute(Function fun){
ShardedJedis shardedJedis = null;
try {
// 从连接池中获取到jedis分片对象
shardedJedis = shardedJedisPool.getResource();
// 从redis中获取数据
return fun.callback(shardedJedis);
}finally {
if (null != shardedJedis) {
// 关闭,检测连接是否有效,有效则放回到连接池中,无效则重置状态
shardedJedis.close();
}
}
};
public String get(final String key){
return this.execute(new Function() {
@Override
public String callback(ShardedJedis e) {
return e.get(key);
}
});
}
}
上面代码用的是什么模式还需要研究一下 暂时不知道
在这个时候需要把公用的Service移动到taotao-common中 将前台系统的httpclient相关类 移动到taotao-common中
将后台系统的redis相关的类也移动到taotao-common中 同时将前后台的applicationContext.xml中的包扫描中增加对taotao-common中service的包扫描
为什么需要移动?
因为后面在后台系统中也需要请求前台系统(比如修改商品信息 请求前台接口清空商品缓存) 需要用到httpclient相关
前台系统也需要使用redis相关功能 比如 前台系统需要保存商品详情页面缓存
移动后 将taotao-common重新安装一下
启台发现后台正常 前台报错 (Could not autowire field :private redis.clients.jedis.ShardedJedisPool)将HttpclientService RedisService中的成员变量的@Autowired添加required=false因为配置文件没有没有移动过去 所以无法自动注入这些属性 后面需要用到相关功能的时候 将配置文件复制过去 但是 这个属性也没有必要去掉 加上只是为了避免启动报错
当后台需要使用httpclient 前台需要使用Redis的时候 复制相应的配置文件 同时在applicationContext.xml增加相应的资源文件
⑧商品详情页面
商品详情页面没有什么难点 主要就是前台通过httpclient请求后台的商品数据 然后通过EL表达式赋值到页面上
需要定义好请求url的格式 同时由于页面显示的图片的属性为images 所以需要在pojo中增加一个getImages方法
这个需求的完整要求是后台也要加上相应的缓存 类似于详情的二级缓存
优化需要实现将缓存的key的形式保存到配置文件中 而不能写死
加入缓存后如果后台修改商品数据后 前台显示的数据会和数据库不一致 所以在后台修改商品的逻辑中增加一个httpclient请求前台清除缓存的一个接口
如果是订单系统 搜索系统 购物车 也有详情缓存 那么需要单独在这些系统中增加接口 在后台修改商品的逻辑中一个个请求 这样实现起来太复杂 冗余 =>引出消息队列MQ的系统
插一句 之前在红薯网后台修改小说数据的时候也是在后台修改逻辑中通过接口清除前台缓存数据的,可以对比一下看一下
⑨ 单点登录系统
解决的关键就在于如何实现多个Tomcat之间的数据共享,Session是无法完成这一点的,因此我们需要用其它东西来代替Session的功能。
要代替Session,需要具备怎样的特点?
1. 内存中保存,速度快
2. 数据具有时效性
3. 多项目数据共享
而这些特性Redis都具备,因此我们的解决方案就是使用Redis来代替Session。在一个独立系统中完成登录注册的逻辑,并完成用户登录时数据保存到Redis中。这样的一个系统就是单点登录系统(SSO)
登录和注册异步校验的时候 如果发送的链接请求是.html结尾 而返回json格式的数据 那么会出现406 Not Acceptable 的错误 解决方法有三种 采用在web.xml中增加一种拦截方案。
登录和注册时候的时候的数据校验使用 Hibernate-Validator
cookie中不能保存域名的问题
我们知道cookie的domain决定了cookie所能作用的域名,现在这个cookie是0.0.1,自然不会在www.taotao.com的访问中携带。
为什么是127.0.0.1,而不是sso.taotao.com ?
虽然我们在浏览器访问的是sso.taotao.com,但是这个请求最终是被nginx处理,然后由Nginx 请求Tomcat的,而Nginx访问Tomcat时,请求的路径正是:127.0.0.1:8083,因此,Tomcat中通过request来获取URL,得到的就是127.0.0.1:8083了
proxy_set_header Host $host;
重新加载nginx配置
在redis中保存用户信息的时候 使用 @JsonIgnore 序列化时候 忽略密码字段
需要注意的是:在从缓存中查询到用户信息后,一定要重置缓存中的数据的存活时间!代表用户一直处于活跃状态
sso单点登录系统主要要注意的登录的逻辑(校验密码的逻辑 如何生成唯一的token 如何把用户信息保存到redis中 登录成功后将token值保存到cookie中) 校验逻辑
封装TaotaoResult
//根据用户名和密码查询该用户
boolean flag = StringUtils.equals(DigestUtils.md5Hex(password), user.getPassword());
if(!flag){
return null;
}
//用户名+当前时间戳 加密作为token值
String token = DigestUtils.md5Hex(userName+System.currentTimeMillis());
//存在用户则根据规则生成token
String redisKey = "TOKEN_"+ token;
//将登录用户的信息保存到redis中
this.redisService.setnx(redisKey, MAPPER.writeValueAsString(user), 1800);
return token;
⑨ 订单系统
订单表 订单商品表 订单物流表
生成唯一订单号的需求:唯一 可读性高 长度
方案有哪些?
1. 时间戳 + 自增id 要保证自增id的不重复(线程安全) 可行 (使用Redis的INCR命令完成)
2. 用户id + 时间戳 可行,而且简单,可读
添加订单的时候 一个statement包含了多条insert语句。那么问题来了: 一个statement执行多条SQL语句,默认情况下支持吗? – 默认不支持的。
在jdbc.properties中连接URL字符串中,跟上下面的配置才可以:allowMultiQueries=true 这几条Sql是在同一个事务中吗? – 是的。
但是,我们绝对不能在Service层捕获异常,否则是不会触发事务的回滚的
taotao-order使用的四层架构 提供 创建订单 查询订单 修改订单状态接口 而且只是提供这些接口 都是异步请求
前台点击立即购买后 还是请求前台的方法 方法里面请求订单系统里面创建订单的接口 在这之前通过前台拦截器校验是否登录 如果没有登录 那么跳转到sso系统登录
在前台系统的springmvc配置文件中配置针对下下订单前必须登录的拦截器
<mvc:interceptors>
<mvc:interceptor>
<mvc:mapping path="/order/**"/>
<bean class="com.taotao.web.interceptor.LoginInterceptor">bean>
mvc:interceptor>
mvc:interceptors>
创建订单方法
public Map<String,Object> createOrder(Order order) throws Exception{
Map<String,Object> map = null;
//从本地线程中获取用户信息
User user = UserThreadLocal.get();
order.setUserId(user.getId());
order.setBuyerNick(user.getUsername());
String url = "http://order.taotao.com/order/create";
//将order对象序列化为json字符串
String orderJsonData = PropertiesService.Mapper.writeValueAsString(order);
HttpclientResult httpclientResult = this.httpclientService.doPostJson(url, orderJsonData);
if(httpclientResult.getCode().intValue()==200){ //响应状态码
//响应的数据
String JsonData = httpclientResult.getData();
if(StringUtils.isNoneBlank(JsonData)){
//将json数据字符串转化为JSON Tree
JsonNode jsonNode = PropertiesService.Mapper.readTree(JsonData);
int status = jsonNode.get("status").asInt();//业务状态
if(status==200){
map = new HashMap<String,Object>();
map.put("status", 200);
String data = jsonNode.get("data").asText();
map.put("data", data);
}
}
}
return map;
}
根据订单id查询订单
public Order queryOrderById(String orderId) throws Exception {
String url = "http://order.taotao.com/order/query/"+orderId;
String orderJsonData = this.httpclientService.doGet(url);
Order order = PropertiesService.Mapper.readValue(orderJsonData, Order.class);
return order;
}
什么时候用readValue 什么时候用readTree 什么时候需要把JsonNode转化为ArrayNode
注意:登录系统的拦截器里面注入的UserService是没有使用dubbo的版本 注入的UserQueryService是使用dubbo的版本 使用后者需要启动zookeeper以及taotao-sso-query项目
同时在前台的登录校验拦截器中使用UserThreadLocal存储当前登录用户的对象
针对订单模块操作的拦截器
逻辑:
1.获取浏览器cookie中的token
2.如果token为空,说明未登录,拦截,重定向到登录页
3.如果token不为空,调用SSO系统接口,查询用户信息
4.如果查询返回的用户为空,说明未登录,拦截,重定向到登录页
5.如果查询返回的用户不为空,说明用户已登录,放行,通过拦截 同时将对象保存到UserThreadLocal中
订单模块还有很多需要看的 比如 mapper的xml文件 延迟加载 高级映射 定时扫描
mv.addObject(“date”, new DateTime().plusDays(2).toString(“MM月dd日”));
笔记中扩展HttpClient的ResponseHandler还没有看
⑩ 商品搜索
主体步骤:安装solr 创建taotao的core 导入数据到solr
solr默认使用的是jetty服务器 可以先使用solr默认的配置跑起来 然后修改配置 抽取出solrhome 然后单独拿出一个tomcat 放到tomcat中的ROOT中 启动搜索模块之前必须先启动tomcat
在tomcat的配置文件catalina.bat中首行添加配置项 set “JAVA_OPTS=-Dsolr.solr.home=C:/Users/User/Desktop/solrhome”
solr.taotao.com;配置的端口是8982
search.taotao.com;配置的端口是8085
修改tomcat的端口号为8982
搜索模块中的实体类中加入注解 暂时不知道具体用途
//反序列化时候 忽略json中的一些未知字段
@JsonIgnoreProperties(ignoreUnknown = true)
搜索部分主要完成solr配置文件 scheme.xml solrconfig.xml 搜索逻辑的实现 applicationContext-solr.xml文件的编写
<lst name="defaults">
<str name="echoParams">explicitstr>
<int name="rows">10int>
<str name="df">textstr>
lst>
<copyField source="sellPoint" dest="text"/>
<copyField source="title" dest="text"/>
<copyField source="cname" dest="text"/>
还是不太明白使用itcast-solrj项目导入数据到solr中的代码逻辑
ItemDataImport.java
//将数据从数据库导入到索引库 solr core
@Test
public void testImport() throws Exception {
// 查询商品数据,1、从数据库查询,2、从后台系统中查询(推荐)
String url = "http://manage.taotao.com/rest/item?page={page}&rows=100";
int page = 1;
int pageSize = 0;
do {
String jsonData = doGet(StringUtils.replace(url, "{page}", "" + page));
JsonNode jsonNode = MAPPER.readTree(jsonData);
ArrayNode rows = (ArrayNode) jsonNode.get("rows");
List- items = MAPPER.readValue(rows.toString(), MAPPER.getTypeFactory()
.constructCollectionType(List.class, Item.class));
pageSize = items.size();
this.httpSolrServer.addBeans(items);
this.httpSolrServer.commit();
page++;
} while (pageSize == 100);
}
搜索系统中还有rabbitmq的部分逻辑 就是后台修改商品信息的时候通知搜索系统修改商品solr数据
itcast-solrj项目还需要看看
十一: RabbitMQ
MQ是一种应用程序对应用程序的通信方法
MQ是消费-生产者模型的一个典型代表 一端往消息队列中不断写入消息 另一端可以读取或者订阅队列中的消息
MQ和JMS类似 但不同的是JMS是SUN JAVA消息中间件服务的一个标准和API定义 而MQ则是遵循了AMQP协议的具体实现和产品
AMQP 标准高级消息队列协议
总结来说:
1. JMS是定义了统一的接口,来对消息操作进行统一;AMQP是通过规定协议来统一数据交互的格式
2. JMS限定了必须使用Java语言;AMQP只是协议,不规定实现方式,因此是跨语言的。
3. JMS规定了两种消息模型;而AMQP的消息模型更加丰富
Erlang是一种通用的面向并发的编程语言
RabbitMQ的5种消息模型简介
Hello World
Work queues 根据RabbitMQ的特点,队列中的消息一旦消费,就会消失,因此任务是不会被重复执行的。
Publish/Subscribe
Routing
Topic
RPC
ACK 消息确认机制
在RabbitMQ中,消息确认有两种模式:
1. 自动模式,我们无需任何操作,在消息被消费者领取后,就会自动确认,消息也会被从队列删除。
2. 手动模式,消息被消费后,我们需要调用RabbitMQ提供的API来实现消息确认。
示例应用
在后台修改 删除商品时候 通知前台系统删除缓存 增加商品的时候 前台系统不做处理在后台系统 修改 添加 商品的时候 搜索系统将通过后台系统查询接口 将查询到的商品添加到solr库 删除商品的时候 将商品从solr库中删除
/**时间戳 操作类型 操作对象
* @param type
* @param id
* @throws Exception
*/
public void sendMsg(String type,Long id) throws Exception{
Map map = new HashMap();
//操作时间
map.put("date", System.currentTimeMillis());
//操作类型
map.put("type", "item."+type);
//操作对象
map.put("data", id);
rabbitTemplate.convertAndSend("item."+type,this.properttiesService.Mapper.writeValueAsString(map));
}
port:5672?
十二:购物车系统
登录和未登录情况下都是可以添加到购物车的,未登录状态下存储在cookie中 允许丢失
在登录情况下需要将cookie中的购物车数据持久化到数据库
tb_cart 表中 索引 KEY userId_itemId
(user_id
,item_id
) USING BTREE
联合索引情况下 索引在特定的查询方式下才会生效
# 索引生效
select * from tb_cart where user_id=1 and item_id=1
# 索引生效
select * from tb_cart where item_id=1 and user_id=1
# 索引生效
select * from tb_cart where item_id=1
# 索引不会生效
select * from tb_cart where user_id=1
在购物车页面使用的是service/cart/… ,并不符合购物车系统的映射规则。我们默认是以.html结尾。此处可以修改这里的请求路径,但是这里我们采用一种新的方案:
在web.xml中添加多种映射方式:
<servlet-mapping>
<servlet-name>taotao-cartservlet-name>
<url-pattern>/service/*url-pattern>
servlet-mapping>
总结:在一个系统中如果有静态的页面的访问也有异步的接口的访问 那么最好加上两种映射方法 一种是.html 一种是 /service/*
为了减轻服务端的压力 可以把未登录时的购物车数据保存到客户端 也就是cookie中
以什么形式保存?
方案1:直接把购物车对象序列化后保存到cookie中
方案2:把数据保存到Redis中,然后形成key,保存到cookie中。
注意 购物车系统也涉及到cookie的操作 所以也需要在nginx中配置
proxy_set_header Host $host;
针对购物车模块操作的拦截器 不管是否登录都是可以购物的 这里如果登录的话 那么将用户对象保存到本地线程中 在taotao-cart的web.xml中实现
逻辑:
1.获取浏览器cookie中的token
2.如果token为空,说明未登录,放行
3.如果token不为空,调用SSO系统接口,查询用户信息
4.如果查询返回的用户为空,说明未登录,放行
基于购物车的下单功能主要代码
/**
* 查询购物车内的商品信息
* @return
* @throws Exception
*/
public List queryCartList(Long userId) throws Exception {
String url = "http://cart.taotao.com/service/api/cart/list/"+userId;
String cartListJson = this.httpclientService.doGet(url);
List cartList = null;
if(StringUtils.isNoneBlank(cartListJson)){
TypeFactory typeFactory = PropertiesService.Mapper.getTypeFactory();
CollectionLikeType type = typeFactory.constructCollectionType(ArrayList.class, Cart.class);
cartList = PropertiesService.Mapper.readValue(cartListJson, type);
}
return cartList;
}
立即购买和加入购物车比较
立即购买直接到订单确认页面 省去了购物车列表页面 提交订单都是走订单生成的逻辑 都是走order.js submit_Order方法
加入购物车后的购物车列表页面是前台系统的order-cart.jsp页面
mysql的读写分离未做
13:dubbo
dubbo高性能和透明化的RPC远程服务调用方案
PRC 即远程过程调用 是一种服务间的调用方式 用大白话说 就是让你的程序像调用自己项目中的方法一样去远程调用其它程序中的方法
无论是Ajax请求,还是HttpClient都是通过 Http请求来访问服务端,使用的都是Http协议,而http协议是短连接。
RPC的底层协议,大部分都是使用长连接,减少了建立和销毁连接的次数,大大提高了程序的访问效率,但是会导致连接长期被占用。
为什么要使用dubbo?
在用户量很大,请求频繁的场景下,例如用户访问我们的网站服务,请求数量很大,请求连接数有限,使用短连接比较合适。此时使用长连接就不太合适。当请求连接数不多,但是请求非常频繁的时候,例如系统间访问,此时使用长连接,在一次连接中完成尽可能多的数据交互,效率更高。而系统的调用在分布式系统中,十分的常见。这种场景下,使用RPC会非常合适。而Dubbo就是一款非常优秀的RPC服务架构
Dubbo的架构演变
阶段1:集中式架构
阶段2:功能拆分
虽然解决集中式架构存在的问题,但是多个系统间,会有一些相同的功能逻辑,重复编码。
阶段3:分布式架构服务
形成分布式架构,把 核心业务 抽取成服务,形成服务中心
存在的问题:当服务越来越多,容量的评估,小服务资源的浪费等问题逐渐显现,此时需增加一个调度中心基于访问压力实时管理集群容量,提高集群利用率。 此时,用于提高机器利用率的资源调度和治理中心
阶段4:服务治理
Dubbo就是资源调度和治理中心,用来解决这些问题的
Dubbo的架构图说明:
服务容器负责启动,加载,运行服务提供者。
服务提供者在启动时,向注册中心注册自己提供的服务。
服务消费者在启动时,向注册中心订阅自己所需的服务。
注册中心返回服务提供者地址列表给消费者,如果有变更,注册中心将基于长连接推送变更数据给消费者。
服务消费者,从提供者地址列表中,基于软负载均衡算法,选一台提供者进行调用,如果调用失败,再选另一台调用。
服务消费者和提供者,在内存中累计调用次数和调用时间,定时每分钟发送一次统计数据到监控中心。
Dubbo入门案例
dubbo-a是一个jar工程 写一个dubbo-b.xml配置文件 是服务的消费者 只需要写一个测试文件
dubbo-b是一个war工程 写一个applicationContext-dubbo.xml配置文件 和web.xml 是服务的提供者 需要写接口的实现类
dubbo-interface 是一个jar工程 写pojo和接口的定义 需要安装到maven中
运行的顺序是 启动zookeeper 安装dubbo-interface 启动dubbo-b 再运行dubbo-a中的测试代码
需要注意的是:tomcat和dubbo的端口不能被占用 注册到zk中的服务名不能重复
Dubbo入门案例中的重要角色:
需求中重要角色:
consumer :服务的消费者,本例中就是系统A
provider:服务的提供者,本例中就是系统B
registry:dubbo支持很多种注册中心。我们这里计划采用zookeeper作为注册中心,也是比较流行的用法。
monitory:默认
container:B系统的容器可以有各种各样,此处我们使用Tomcat作为启动容器
安装zookeeper
先在解压的zookeeper目录下创建新目录:data
进入conf目录,修改zoo.sample.cfg为zoo.cfg,然后打开并编辑:dataDir=C:/Users/User/Desktop/zookeeper-3.4.8/data
这个目录修改为你刚刚创建的data目录。
2181端口
解决入门案例的代码重复问题
通过刚刚的示例我们可以发现,其中User对象和UserService在A系统和B系统中都使用,那么,我们是否应该讲该代码复用呢?
答案是肯定的。
在使用dubbo时,provider需要将提供服务所需要的java代码(bean、interface等)单独打包成jar提供给consumer使用。
最佳实践方案:
以下内容需要抽取到单独的项目,并打包发布:
1. 服务接口
2. 接口中使用的模型类(JavaBean)
dubbo消费者的配置文件(可选。这个有配置文件名冲突风险,所以我们不实现这一点)
使用dubbo优化单点登录系统部分
已经完成 不过还有一些需要增加理解
优化的是前台系统中的登录拦截校验器 之前注入的是UserService 现在注入的是UserQueryService 同时导致必须更改引入的User实体优化后的导致前台系统不能单独启动 先要启动zookeeper 启动 taotao-sso-query服务
监控中心和治理中心看看笔记即可
13:springboot实现
spring1.x使用xml配置
spring2.x提供注解支持
spring3.x 4.x 支持java配置方式
java配置是spring4.x推荐的配置方式 可以完全替代xml配置
Spring的Java配置方式是通过 @Configuration 和 @Bean 这两个注解实现的:
1、@Configuration 作用于类上,相当于一个xml配置文件;
2、@Bean 作用于方法上,相当于xml配置中的;
public static void main(String[] args) {
// 通过Java配置来实例化Spring容器
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(SpringConfig.class);
// 在Spring容器中获取Bean对象
UserService userService = context.getBean(UserService.class);
// 调用对象中的方法
List list = userService.queryUserList();
for (User user : list) {
System.out.println(user.getUsername() + ", " + user.getPassword() + ", " + user.getPassword());
}
// 销毁该容器
context.destroy();
}
@Configuration //通过该注解来表明该类是一个Spring的配置,相当于一个xml文件
@ComponentScan(basePackages = "cn.itcast.springboot.config") //配置扫描包
@PropertySource(value={"jdbc.properties"},ignoreResourceNotFound=true)
public class SpringConfig {
@Value("${jdbc.url}")
private String jdbcUrl;
@Value("${jdbc.driverClassName}")
private String jdbcDriverClassName;
@Value("${jdbc.username}")
private String jdbcUsername;
@Value("${jdbc.password}")
private String jdbcPassword;
@Bean // 通过该注解来表明是一个Bean对象,相当于xml中的
public UserDAO getUserDAO(){
return new UserDAO(); // 直接new对象做演示
}
}
springboot
@SpringBootApplication(exclude={RedisAutoConfiguration.class}) //手动设置需要排除自动配置
@SpringBootConfiguration //该注解声明该类是一个配置类
//@ComponentScan(basePackages={"cn.itcast.springboot.config"}) //手动扫描配置
//@ImportResource(value={"mybatis-config.xml"}) //引入项目的xml文件
public class HelloApplication {
public static void main(String[] args) {
SpringApplication.run(HelloApplication.class, args);
}
}
该注解主要组合了以下注解:
Spring Boot的项目一般都会有*Application的入口类,入口类中会有main方法,这是一个标准的Java应用程序的入口方法。
1 @SpringBootConfiguration:这是Spring Boot项目的配置注解,这也是一个组合注解:
在Spring Boot项目中推荐使用@ SpringBootConfiguration替代@Configuration
2 @EnableAutoConfiguration:启用自动配置,该注解会使Spring Boot根据项目中依赖的jar包自动配置项目的配置项:
如:我们添加了spring-boot-starter-web的依赖,项目中也就会引入SpringMVC的依赖,Spring Boot就会自动配置tomcat和SpringMVC
3 @ComponentScan:默认扫描@SpringBootApplication所在类的同级目录以及它的子目录。
全局配置文件
server.port=8089
server.servletPath=*.html
spring.mvc.view.prefix=/WEB-INF/views/
spring.mvc.view.suffix=.jsp
spring.resources.static-locations=classpath:/META-INF/resources/,classpath:/resources/,classpath:/static/,classpath:/public/
logging.level.org.springframework=DEBUG
Starter pom
Spring Boot 为我们提供了简化企业级开发绝大多数场景的starter pom 只要使用了应用场景所需要的starter pom 相关的技术配置将会消除 就可以得到Spring Boot为我们提供的自动配置Bean
xml配置文件
spring Boot提倡零配置 即无xml配置 但是在实际项目中 可能有一些特殊要求你必须使用xml配置 这时候可以通过spring提供的@ImportResource来加载xml配置 例如:@ImportResource(value={“mybatis-config.xml”}) //引入项目的xml文件
Spring Boot的自动配置原理
Spring Boot在进行SpringApplication对象实例化时会加载META-INF/spring.factories文件,将该配置文件中的配置载入到Spring容器。
条件注解
@ConditionalOnBean:当容器中有指定的Bean的条件下
@ConditionalOnClass:当类路径下有指定的类的条件下
Spring Boot的web开发
Web开发的自动配置类:org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration
自动配置静态资源
进入规则为/
如果进入SpringMVC的规则为/ 时,Spring Boot的默认静态资源的路径为:
spring.resources.static-locations=classpath:/META-INF/resources/,classpath:/resources/,classpath:/static/,classpath:/public/
进入规则为*.xxx或者不指定静态文件路径时
将静态资源放置到webapp下的static目录中即可通过地址访问
自定义消息转化器
自定义消息转化器 只需要在@Configuration的类中添加消息转化器的@bean加入到spring容器 就会被Spring Boot自动加入到容器中
@Bean
public StringHttpMessageConverter stringHttpMessageConverter(){
StringHttpMessageConverter converter = new StringHttpMessageConverter(Charset.forName("UTF-8"));
return converter;
}
自定义springmvc的配置
有些时候我们需要自已配置SpringMVC而不是采用默认,比如说增加一个拦截器,这个时候就得通过继承WebMvcConfigurerAdapter然后重写父类中的方法进行扩展。
使用springBoot改造购物车系统 还没做
后台修改数据 前台系统和搜索系统使用mq
dubbo是抽取queryUserByToken为服务
现在启动前台系统 先要启动zookeeper 启动 taotao-sso-query服务
各个模块的分工有什么原则吗?订单系统只提供了操作订单的接口 而购物车系统 则有接口 有静态页面
很多jar包越用越随意了 都不知道为什么了
拦截器能配置具体的URL类型进行拦截
购物车去结算有问题
还需要把rabbitmq内容看一遍
完成几个重要的功能 比如后台的删除商品
前台系统的购物车 订单
收货地址管理
后台的商品参数管理
跳转
String requestURI = request.getRequestURI();
StringBuffer requestURL = request.getRequestURL();
登录后的跳转callback实现
详情页面添加购物车可以携带数量 购物车页面去结算可以选择商品
在订单确认页面 可以添加 删除 选择默认收货地址
课程到这里为止 基本上项目阶段过半了
展望一下后面的课程主要有:
单点登录系统 订单系统 搜索系统 RabbitMQ 购物车系统 Dubbo springBoot
同时还有一些拔高课程 包括redis mysql nginx
2小时学习springBoot http://www.imooc.com/learn/767,
SpringBoot进阶之web进阶 https://www.imooc.com/learn/810
SpringBoot开发常用技术整合 http://www.imooc.com/learn/956
Zookeeper分布式专题与Dubbo微服务入门视频基础学习
Spring Cloud微服务实战
待完成功能
收货地址管理 新增 删除 选择默认收货地址
购物车系统完成 购物车生成订单 通知删除购物车内物品 购物车内的cookie数据和数据库数据在登录时合并 选择数量 提交订单时选择购物车内的特定商品
订单系统部分 立即购买选择商品数量
商品规格参数管理 后台规格参数增删改查 同时在修改商品页面的参数管理以及在前台系统商品详情页面的商品规格参数的显示
个人中心管理
生成订单的时候通知问题
去购物车结算 不刷新显示数字
规格参数前台需要实现?
使用架构搜索
搜索系统为什么不能按照sellprice搜索
生成订单的时候乱码
淘淘拔高部分
JVM内存优化
Java栈是与每一个线程关联的,JVM在创建每一个线程的时候,会分配一定的栈空间给线程。它主要用来存储线程执行过程中的局部变量,方法的返回值,以及方法调用上下文。栈空间随着线程的终止而释放。
Java中堆是由所有的线程共享的一块内存区域,堆用来保存各种JAVA对象,比如数组,线程对象等。
Young 年轻区(代)
Young区被划分为三部分,Eden区和两个大小严格相同的Survivor区,其中,Survivor区间中,某一时刻只有其中一个是被使用的,另外一个留做垃圾收集时复制对象用,在Eden区间变满的时候, GC就会将存活的对象移到空闲的Survivor区间中,根据JVM的策略,在经过几次垃圾收集后,任然存活于Survivor的对象将被移动到Tenured区间。
Tenured 年老区
Tenured区主要保存生命周期长的对象,一般是一些老的对象,当一些对象在Young复制转移一定的次数以后,对象就会被转移到Tenured区,一般如果系统中用了application级别的缓存,缓存中的对象往往会被转移到这一区间。
Perm 永久区
Perm代主要保存class,method,filed对象,这部份的空间一般不会溢出,除非一次性加载了很多的类,不过在涉及到热部署的应用服务器的时候,有时候会遇到java.lang.OutOfMemoryError : PermGen space 的错误,造成这个错误的很大原因就有可能是每次都重新部署,但是重新部署后,类的class没有被卸载掉,这样就造成了大量的class对象保存在了perm中,这种情况下,一般重新启动应用服务器可以解决问题。
Redis
Redis支持两种方式的持久化,一种是RDB方式 一种是AOF方式。
RDB方式是通过快照完成的,当符合一定条件时候Redis会自动将内存中的所有数据进行快照并且存储到硬盘上。进行快照的条件在配置文件中指定。有2个参数构成:时间和改动的键的个数,当在指定时间内被更改的键的个数大于指定数值时就会进行快照
RDB是Redis的默认持久化方式
Redis的AOF持久化策略是将发送到Redis服务端的每一条命令都记录下来 并且保存到硬盘中的AOF文件
tomcat
tomcat的运行模式有3种:
1. bio
默认的模式,性能非常低下,没有经过任何优化处理和支持.
2. nio
nio(new I/O),是Java SE 1.4及后续版本提供的一种新的I/O操作方式(即java.nio包及其子包)。Java nio是一个基于缓冲区、并能提供非阻塞I/O操作的Java API,因此nio也被看成是non-blocking I/O的缩写。它拥有比传统I/O操作(bio)更好的并发运行性能。
3. apr
安装起来最困难,但是从操作系统级别来解决异步的IO问题,大幅度的提高性能.
执行器 线程池
在tomcat中每一个用户请求都是一个线程,所以可以使用线程池提高性能。
连接器
Connector是Tomcat接收请求的入口,每个Connector有自己专属的监听端口
Connector有两种:HTTP Connector和AJP Connector
禁用AJP连接器
AJP(Apache JServer Protocol)
AJPv13协议是面向包的。WEB服务器和Servlet容器通过TCP连接来交互;为了节省SOCKET创建的昂贵代价,WEB服务器会尝试维护一个永久TCP连接到servlet容器,并且在多个请求和响应周期过程会重用连接。