此次跟随教程完成项目,主要目的是学习一些新的技术,掌握一些开发工具的使用。因此,并不会对事务逻辑进行细致记录。此篇博客仅用于博主本人日后搭建项目时的参考,并非教程
在接下来的学习过程中,我们需要使用vue及相关框架 。在此之前我们需要做一些准备工作
NPM是Node提供的模块管理工具,可以非常方便的下载安装很多前端框架,包括Jquery、AngularJS、VueJs都有。为了后面学习方便,我们先安装node及NPM工具。 node.js的下载安装比较简单,直接去官网下载相应版本即可。下载后开始安装,按照提示逐步进行,安装完成后,Node应该已经自带了npm。打开控制台输入命令 node -v查询node版本,再输入npm -v查询npm版本。由于npm默认的仓库是在国外网站,速度较慢所以设置到淘宝镜像。切换镜像的工具我们使用nrm,首先安装nrm。在控制台输入如下命令:npm install nrm -g,-g是代表全局安装。然后通过命令:nrm ls查看npm仓库列表,带有*的为当前选中的镜像仓库。如下图:
我们可以使用命令:nrm use taobao来切换至淘宝镜像源,然后通过nrm test npm(taobao)来测试npm(淘宝)速度。切记安装完成后一定要重启电脑!
使用idea创建一个空项目,添加一个static web的module。要想使用vue,我们首先要进行安装,推荐使用npm安装,点击Terminal打开控制台,进入刚刚创建好的module目录,输入命令:npm init -y进行初始化,初始化后我们会发现工程目录中多了package.json文件(可大致理解为maven中的pom.xml文件)。接下来安装vue,输入命令:npm install vue --save,其中--save表示只针对当前项目使用。安装后我们会发现目录下多了一个名为node_modules的文件夹。它是通过npm安装的所有模块的默认位置,我们可以看到里面有一个名为vue的文件夹,这样我们就已经安装好了vue。此时我们仅需在页面中引入vue就可以使用<script src="node_modules/vue/dist/vue.js"></script>
在构建前端页面时,我们需要使用webpack打包工具,但是由于其配置较麻烦,所以我们使用vue-cli脚手架来迅速搭建项目。首先在windows控制台下输入命令:npm install -g vue-cli进行安装,进入idea中的终端,输入命令:vue init webpack来快速搭建一个webpack的项目,如图所示进行选择:
输入命令:npm run dev进行启动,Ctrl+V关闭项目
项目搭建完成后,前端界面的端口号为9001,网关端口号为10010,注册中心端口号为10086。之前我们访问网站都是在地址栏输入http://localhost:端口号进行访问,但是这次我们需要使用域名进行访问,例如:www.leyou.com。因为我们未购买域名,所以无法使用域名服务器解析只能使用本地域名解析。这里我们通过SwitchHosts这个小工具来修改host文件。但是只是这样还是不够的,因为我们默认使用http协议,在http协议中, 如果不写端口号则默认为80。所以此处我们需要使用nginx作为反向代理,这里我们在虚拟机中安装nginx。虚拟机的安装过程在另一篇博客中已经有详细记录,在此就不再赘述了。另外,在之后实现图片上传功能时,我们要用到FastDFS,所以在此一并安装。
首先我们添加一个新用户名为leyou。添加完成后我们可以在home文件夹下看到名为leyou的文件夹,点击进入并创建一个名为fdfs的文件夹。将如下文件通过xftp传输到fdfs文件夹中。
在安装FastDFS和nginx之前,我们需要安装一些依赖。安装方法如下:
1.安装GCC依赖
GCC用来对C语言代码进行编译运行,使用yum命令安装:sudo yum -y install gcc
2.安装unzip工具
unzip工具可以帮我们对.zip的压缩包进行解压 命令:sudo yum install -y unzip zip
3.安装libevent
sudo yum -y install libevent
4.安装nginx所需依赖
sudo yum -y install pcre pcre-devel zlib zlib-devel openssl openssl-devel
5.安装libfastcommon-master
这个没有yum包,只能通过编译安装
解压刚刚上传的libfastcommon-master.zip(记得进入/home/leyou/fdfs/)
unzip libfastcommon-master.zip
进入解压完成的目录:
cd libfastcommon-master
编译并且安装:
sudo ./make.sh
sudo ./make.sh install
到这里为止,所有依赖都已经安装完毕,接下来我们安装FastDFS(同样使用编译安装):
解压:tar -xvf FastDFS_v5.08.tar.gz
进入目录:cd FastDFS
编译并安装:sudo ./make.sh
sudo ./make.sh install
安装完成,我们应该能在/etc/init.d/
目录,通过命令ll /etc/init.d/ | grep fdfs
看到FastDFS提供的启动脚本:
fdfs_trackerd
是tracker启动脚本 ,fdfs_storaged
是storage启动脚本
我们可以在 /etc/fdfs
目录,通过命令查看到以下配置文件模板:
tarcker.conf.sample
是tracker的配置文件模板
storage.conf.sample
是storage的配置文件模板
client.conf.sample
是客户端的配置文件模板
关闭防火墙并重启:chkconfig iptables off
启动trackercd:
FastDFS的tracker和storage在刚刚的安装过程中,都已经被安装了,因此我们安装这两种角色的方式是一样的。不同的是,两种需要不同的配置文件。
我们要启动tracker,就修改刚刚看到的tarcker.conf
,并且启动fdfs_trackerd
脚本即可。
- 编辑tracker配置
首先我们将模板文件进行赋值和重命名:
sudo cp tracker.conf.sample tracker.conf
sudo vim tracker.conf
打开tracker.conf
,修改base_path
配置:
base_path=/leyou/fdfs/tracker # tracker的数据和日志存放目录
-
创建目录
刚刚配置的目录可能不存在,我们创建出来
sudo mkdir -p /leyou/fdfs/tracker
-
启动tracker
我们可以使用
sh /etc/init.d/fdfs_trackerd
启动,不过安装过程中,fdfs已经被设置为系统服务,我们可以采用熟悉的服务启动方式:
sudo service fdfs_trackerd start # 启动fdfs_trackerd服务,停止用stop
另外,我们可以通过以下命令,设置tracker开机启动:
sudo chkconfig fdfs_trackerd on
启动storage:
我们要启动storage,就修改刚刚看到的storage.conf
,并且启动fdfs_storaged脚本即可。
-
编辑storage配置
首先我们将模板文件进行赋值和重命名:
sudo cp storage.conf.sample storage.conf
sudo vim storage.conf
打开storage.conf
,修改base_path
配置:
base_path=/leyou/fdfs/storage # storage的数据和日志存放目录
store_path0=/leyou/fdfs/storage # storage的上传文件存放路径
tracker_server=tracker所在主机的ip:22122 # tracker的地址
-
创建目录
刚刚配置的目录可能不存在,我们创建出来
sudo mkdir -p /leyou/fdfs/storage
-
启动storage
我们可以使用
sh /etc/init.d/fdfs_storaged
启动,同样我们可以用服务启动方式:
sudo service fdfs_storaged start # 启动fdfs_storaged服务,停止用stop
另外,我们可以通过以下命令,设置tracker开机启动:
sudo chkconfig fdfs_storaged on
安装FastDFS的Nginx模块 :
-
解压
tar -xvf fastdfs-nginx-module_v1.16.tar.gz
-
配置config文件
# 进入配置目录 cd /home/leyou/fdfs/fastdfs-nginx-module/src/ # 修改配置 vim config # 执行下面命令(将配置中的/usr/local改为/usr): :%s+/usr/local/+/usr/+g
-
配置mod_fastdfs.conf
# 将src目录下的mod_fastdfs.conf复制到 /etc/fdfs目录: sudo cp mod_fastdfs.conf /etc/fdfs/ # 编辑该文件 sudo vim /etc/fdfs/mod_fastdfs.conf
-
修改一下配置:
connect_timeout=10 # 客户端访问文件连接超时时长(单位:秒) tracker_server=192.168.56.101:22122 # tracker服务IP和端口 url_have_group_name=true # 访问链接前缀加上组名 store_path0=/leyou/fdfs/storage # 文件存储路径
- 复制 FastDFS的部分配置文件到/etc/fdfs目录
cd /home/leyou/fdfs/FastDFS/conf/
cp http.conf mime.types /etc/fdfs/
安装nginx:
-
解压
tar -xvf nginx-1.10.0.tar.gz
-
配置
sudo ./configure --prefix=/opt/nginx --sbin-path=/usr/bin/nginx --add-module=/home/leyou/fdfs/fastdfs-nginx-module/src
-
编译安装
sudo make && sudo make install
-
配置nginx整合fastdfs-module模块
我们需要修改nginx配置文件,在/opt/nginx/config/nginx.conf文件中:
sudo vim /opt/nginx/conf/nginx.conf
#user nobody;
worker_processes 1;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65;
gzip on;
server {
listen 80;
server_name manage.leyou.com;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Server $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
#nginx监听80端口,根据域名进行相应的处理
location / {
proxy_pass http://127.0.0.1:9001;
proxy_connect_timeout 600;
proxy_read_timeout 600;
}
}
server {
listen 80;
server_name api.leyou.com;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Server $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
location / {
proxy_pass http://127.0.0.1:10010;
proxy_connect_timeout 600;
proxy_read_timeout 600;
}
}
}
修改后记得重启nginx,使用switchhosts修改配置文件使各域名均指向nginx所在虚拟机的ip地址。这样我们就可以通过域名来访问网站了
使用cors解决跨域问题:
跨域原因说明 | 实例 |
---|---|
域名不同 | www.jd.com 与 www.taobao.com |
域名相同,端口不同 | www.jd.com:8080 与 www.jd.com:8081 |
二级域名不同 | item.jd.com 与 miaosha.jd.com |
我们刚才是从manage.leyou.com通过ajax向api.leyou.com发起请求,属于二级域名不同会出现跨域问题,解决方法较为简单,在网关工程中添加一个配置类如下,不要漏写相应注解!
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
@Configuration
public class GlobalCorsConfig {
@Bean
public CorsFilter corsFilter() {
//1.添加CORS配置信息
CorsConfiguration config = new CorsConfiguration();
//1) 允许的域,不要写*,否则cookie就无法使用了
config.addAllowedOrigin("http://manage.leyou.com");
//2) 是否发送Cookie信息
config.setAllowCredentials(true);
//3) 允许的请求方式
config.addAllowedMethod("OPTIONS");
config.addAllowedMethod("HEAD");
config.addAllowedMethod("GET");
config.addAllowedMethod("PUT");
config.addAllowedMethod("POST");
config.addAllowedMethod("DELETE");
config.addAllowedMethod("PATCH");
// 4)允许的头信息
config.addAllowedHeader("*");
//2.添加映射路径,我们拦截一切请求
UrlBasedCorsConfigurationSource configSource = new UrlBasedCorsConfigurationSource();
configSource.registerCorsConfiguration("/**", config);
//3.返回新的CorsFilter.
return new CorsFilter(configSource);
}
}
这样跨域问题就得到了解决。
支持restful风格:
为了支持restful风格,我们的web层返回类型均为ResponseEntity如下(有返回值和无返回值的写法):
@GetMapping("page")
public ResponseEntity<PageResult<Brand>> queryBrandByPage(
@RequestParam(value = "page",defaultValue="1")Integer page,
@RequestParam(value = "rows",defaultValue="5")Integer rows,
@RequestParam(value = "sortBy",required = false)String sortBy,
@RequestParam(value = "desc",defaultValue="falese")Boolean desc,
@RequestParam(value = "key",required = false)String key
) {
return ResponseEntity.ok(brandService.queryBrandByPage(page,rows,sortBy,desc,key));
}
@PostMapping
public ResponseEntity<Void> saveBrand(Brand brand, @RequestParam("cids")List<Long> cids) {
brandService.saveBrand(brand,cids);
return ResponseEntity.status(HttpStatus.CREATED).build();
}
服务端使用分页助手和通用mapper实现分页和字段查询的方法:
public PageResult<Brand> queryBrandByPage(Integer page, Integer rows, String sortBy, Boolean desc, String key) {
//这里的分页查询我们使用分页助手
//分页
PageHelper.startPage(page,rows);
//过滤,创建example对象时需传入对应类的字节码
Example example = new Example(Brand.class);
if(StringUtils.isNotBlank(key)) {
example.createCriteria().orLike("name","%" + key + "%")
.orEqualTo("letter",key.toUpperCase());
}
//排序
if(StringUtils.isNotBlank(sortBy)) {
String orderClause = sortBy + (desc ? " DESC" : " ASC");
example.setOrderByClause(orderClause);
}
//查询
List<Brand> list = brandMapper.selectByExample(example);
if(CollectionUtils.isEmpty(list)) {
throw new LyException(ExceptionEnum.BRAND_NOT_FOUND);
}
PageInfo<Brand> info = new PageInfo<>(list);
return new PageResult<>(info.getTotal(),list);
}
通用mapper的拓展使用方法:
通常情况下,我们仅需写出mapper接口,然后继承Mapper<>即可,如下:
package com.leyou.item.mapper;
import com.leyou.item.pojo.Sku;
import tk.mybatis.mapper.common.Mapper;
public interface SkuMapper extends Mapper<Sku> {
}
但在特殊情况下,我们需要对较多的数据进行批量增加或删除,这时如果我们的接口仅仅继承了Mapper<>,那便只能使用循环来实现此功能。如果想要避免循环,我们可以在接口上继承其他几个由通用mapper提供的接口如下所示:
package com.leyou.item.mapper;
import com.leyou.item.pojo.Stock;
import tk.mybatis.mapper.additional.idlist.DeleteByIdListMapper;
import tk.mybatis.mapper.additional.insert.InsertListMapper;
import tk.mybatis.mapper.common.Mapper;
public interface StockMapper extends Mapper<Stock>,InsertListMapper<Stock>,DeleteByIdListMapper<Stock,Long> {
}
如果有多个接口都需要继承多个接口,那么我们可以向上抽取出自己的接口供其他接口继承如下:
package com.leyou.common.mapper;
import tk.mybatis.mapper.additional.idlist.IdListMapper;
import tk.mybatis.mapper.additional.insert.InsertListMapper;
import tk.mybatis.mapper.annotation.RegisterMapper;
import tk.mybatis.mapper.common.Mapper;
@RegisterMapper
public interface BaseMapper<T> extends Mapper<T>, IdListMapper<T,Long> ,InsertListMapper<T> {
}
另外,通用mapper仅能解决单表操作,当我们需要进行多表操作时,就需要自己写sql语句,我们可以在原有的mapper接口中结合注解来编写sql语句如下:
package com.leyou.item.mapper;
import com.leyou.item.pojo.Brand;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import tk.mybatis.mapper.common.Mapper;
import java.util.List;
public interface BrandMapper extends Mapper<Brand> {
@Insert("INSERT INTO tb_category_brand (category_id,brand_id) VALUES (#{cid},#{bid})")
int insertCategoryBrand(@Param("cid")Long cid,@Param("bid")Long bid);
@Select("SELECT b.* FROM tb_category_brand cb INNER JOIN tb_brand b ON cb.brand_id = b.id WHERE cb.category_id = #{cid}")
List<Brand> queryByCategoryId(@Param("cid") Long cid);
}
使用java8的新特性处理集合(避免for循环):
/*
skuList是一个装有自定义类Sku的集合,Sku为类名,使用此条语句可获得一个由sku的成员变量
id组成的集合
*/
List<Long> ids = skuList.stream().map(Sku::getId).collect(Collectors.toList());
实现图片上传:
因图片上传功能可能被广泛使用,所以我们单独创建一个工程将图片上传作为一个微服务。
pom.xml:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>leyou</artifactId>
<groupId>com.leyou.parent</groupId>
<version>1.0.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<groupId>com.leyou.service</groupId>
<artifactId>ly-upload</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
<dependency>
<groupId>com.github.tobato</groupId>
<artifactId>fastdfs-client</artifactId>
</dependency>
<dependency>
<groupId>com.leyou.common</groupId>
<artifactId>ly-common</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>
</dependencies>
</project>
启动类:
package com.leyou;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
@SpringBootApplication
@EnableDiscoveryClient
public class LyUploadApplication {
public static void main(String[] args) {
SpringApplication.run(LyUploadApplication.class);
}
}
默认情况下,在请求经过zuul网关的代理,会通过springmvc对请求进行处理缓存,普通请求不会产生什么影响,但是文件上传会造成不必要的网络负担。在高并发时可能导致网络阻塞,因此我们的文件上传请求应绕过请求缓存直接通过路由到达目标微服务。为了绕过缓存,我们需要在请求路径前加上/zuul,例:原路径为api.leyou.com/api/upload/image,现应改为api.leyou.com/zuul/aip/upload/image。由于前后端分离开发,所以我们不应该去对前端请求路径进行改写。这里我们使用nginx实现路径改写,需在nginx配置文件中进行如下改动:
server {
listen 80;
server_name api.leyou.com;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Server $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# 上传路径的映射
# 下面是刚刚增添的
location /api/upload {
rewrite "^/(.*)$" /zuul/$1;
}
# 上面是刚刚增添的
location / {
proxy_pass http://127.0.0.1:10010;
proxy_connect_timeout 600;
proxy_read_timeout 600;
}
}
保存重启nginx。
现在我们解决了网关缓存问题,但是我们不希望把用户上传的文件保存在本地硬盘或者tomcat下。因为随着用户上传文件的增多,tomcat的文件会越来越多,启动加载速度也会逐渐变慢;并且tomcat解析静态资源效率十分低,解析静态资源的工作应该交由nginx来做;最后,大型项目中,一台机器很难存储下所有文件并且容易出现单点故障。所以我们应使用分布式文件系统,这里我们选择FastDFS。
架构图:
在前面我们已经完成了FastDFS的安装和配置,现在我们需要修改nginx的配置,使nginx将相关任务交由FastDFS模块处理。
sudo vim /opt/nginx/conf/nginx.conf
在nginx的配置文件中添加如下内容:
server {
listen 80;
server_name image.leyou.com;
# 监听域名中带有group的,交给FastDFS模块处理
location ~/group([0-9])/ {
ngx_fastdfs_module;
}
location / {
root html;
index index.html index.htm;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
}
如果需要更改nginx对上传文件大小的限制,需要在http{}内server外添加如下配置:
client_max_body_size 10m;
重启nginx
现在我们需要利用java代码将图片上传至FastDFS,首先我们引入支持springboot2.0的FastDFS客户端依赖,注意就是下面这个,一定不能缺少!版本已经通过父工程进行管理
<dependency>
<groupId>com.github.tobato</groupId>
<artifactId>fastdfs-client</artifactId>
</dependency>
添加FastDFS客户端的配置类如下:
package com.leyou.upload.config;
import com.github.tobato.fastdfs.FdfsClientConfig;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableMBeanExport;
import org.springframework.context.annotation.Import;
import org.springframework.jmx.support.RegistrationPolicy;
@Configuration
@Import(FdfsClientConfig.class)
// 解决jmx重复注册bean的问题
@EnableMBeanExport(registration = RegistrationPolicy.IGNORE_EXISTING)
public class FastClientImporter {
}
改写配置文件:
server:
port: 8082
spring:
application:
name: upload-service
servlet:
multipart:
max-file-size: 5MB # 限制文件上传的大小
# Eureka
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:10086/eureka
fdfs:
so-timeout: 2500
connect-timeout: 600
thumb-image: # 缩略图
width: 60
height: 60
tracker-list: # tracker地址
- 192.168.114.129:22122
web层:
package com.leyou.upload.web;
import com.leyou.upload.service.UploadService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
@RestController
@RequestMapping("upload")
public class UploadController {
@Autowired
private UploadService uploadService;
@PostMapping("image")
public ResponseEntity<String> uploadImage(@RequestParam("file")MultipartFile file) {
return ResponseEntity.ok(uploadService.uploadImage(file));
}
}
service层:
package com.leyou.upload.service;
import com.github.tobato.fastdfs.domain.StorePath;
import com.github.tobato.fastdfs.service.FastFileStorageClient;
import com.leyou.common.enums.ExceptionEnum;
import com.leyou.common.exception.LyException;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
@Slf4j
@Service
public class UploadService {
@Autowired
private FastFileStorageClient fastFileStorageClient;
private static final List<String> ALLOW_TYPES = Arrays.asList("image/jpeg","image/png","image/bmp");
public String uploadImage(MultipartFile file) {
try {
//首先我们通过后缀名校验文件的类型
String contentType = file.getContentType();
if(!ALLOW_TYPES.contains(contentType)) {
throw new LyException(ExceptionEnum.INVALID_FILE_TYPE);
}
//然后我们通过校验文件内容判断文件类型
BufferedImage image = ImageIO.read(file.getInputStream());
if (image == null) {
throw new LyException(ExceptionEnum.INVALID_FILE_TYPE);
}
//上传到FastDFS
String extension = StringUtils.substringAfterLast(file.getOriginalFilename(), ".");
StorePath storePath = fastFileStorageClient.uploadFile(file.getInputStream(),file.getSize(),extension,null);
return "http://image.leyou.com/" + storePath.getFullPath();
} catch (IOException e) {
log.error("上传文件失败",e);
throw new LyException(ExceptionEnum.UPLOAD_FILE_ERROR);
}
}
}
我们的图片上传功能到这里就实现了
第二部分到此为止