- 一、简介
- 二、父项目
- 三、图片管理服务的数据库设计
- 四、定义module : img_pojo
- 五、定义module : img_mapper
- 六、定义module : img_service_api
- 七、module : img_service_provider
- 八、 定义module : imp_service_consumer
内容:
实现一个图片管理应用。可以上传图片,查看图片,删除图片,下载图片。
技术:
SpringBoot:开发平台
Dubbo:远程服务调用技术
MyBatis:数据库访问
MySQL:数据库
Zookeeper:Dubbo的注册中心
FastDFS:集中管理所有上传的图片
Nginx:为FastDFS中的Storage服务器,提供一个虚拟主机,就是可以在线使用浏览器查看Storage内的图片。
实现:
二、父项目实体 - 定义module : img_pojo
module中定义需要的实体类型
数据访问 - 定义module : img_mapper
module中定义需要的数据访问接口和SQL映射文件。
服务标准 - 定义module : img_service_api
module中定义dubbo远程访问是的服务标准接口。只定义标准接口。
服务提供者 - 定义module : img_service_provider
module中实现img_service_api中的服务接口,并发布服务。在zk中发布服务信息。
服务消费者 - 定义module : imp_service_consumer
module中使用img_service_api中的接口,远程调用服务提供者给予的实现。
提供客户视图(UI)
提供图片上传|下载。 图片上传|下载不适合使用dubbo处理。
pom.xml
三、图片管理服务的数据库设计org.springframework.boot spring-boot-dependencies 2.2.5.RELEASE pom import org.apache.dubbo dubbo-spring-boot-starter 2.7.5 org.apache.curator curator-recipes 4.3.0 org.apache.curator curator-framework 4.3.0 com.github.pagehelper pagehelper-spring-boot-starter 1.2.13 org.mybatis.spring.boot mybatis-spring-boot-starter 2.1.1 cn.bestwu fastdfs-client-java 1.27 org.apache.commons commons-lang3 3.9
-- 创建图片表格 create table img_manage( id bigint not null auto_increment comment '主键', url varchar(255) default '' comment '访问这个图片的HTTP地址', group_name varchar(64) default 'group1' comment 'FastDFS中的卷名', remote_file_name varchar(255) default '' comment 'FastDFS中的文件名,如:M00/00/00/abc.jpg', origin_file_name varchar(255) default '' comment '上传的图片的原始名称', upload_time date not null comment '上传图片的时间', primary key (id) ) comment '保存上传到FastDFS中的图片信息表';四、定义module : img_pojo
public class Image implements Serializable {
private Long id;
private String groupName;
private String remoteFileName;
private String originFileName;
private Date uploadTime;
public String getUrl(){
return groupName + "/" + remoteFileName;
}
public void setUrl(String url){
// url属性为推导属性。url的值,是由groupName和remoteFileName组合得到。
}
//其他正常
五、定义module : img_mapper
pom.xml
com.bjsxt img_pojo 1.0-SNAPSHOT org.mybatis.spring.boot mybatis-spring-boot-starter mysql mysql-connector-java
com.bjsxt.img.mapper
package com.bjsxt.img.mapper;
import com.bjsxt.img.pojo.Image;
import java.util.List;
public interface ImageMapper {
// 新增数据
int insert(Image image);
// 删除数据
int deleteByPK(Long id);
// 主键查询
Image selectById(Long id);
// 全数据查询
List select();
}
ImageMapper.xml
insert into img_manage(id, url, group_name, remote_file_name, origin_file_name, upload_time) values(DEFAULT, #{url}, #{groupName}, #{remoteFileName}, #{originFileName}, #{uploadTime}) delete from img_manage where id = #{id}
application.yml
mybatis: # sql映射文件
mapper-locations: classpath:mybatis/mapper/*.xml
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/test?serverTimezone=GMT%2B8
username: root
password: 1234
六、定义module : img_service_api
pom.xml
com.bjsxt img_pojo 1.0-SNAPSHOT
com.bjsxt.img.serviceapi
package com.bjsxt.img.serviceapi;
import com.bjsxt.img.pojo.Image;
import java.util.Map;
public interface ImageServiceAPI {
// 新增图片
int save(Image image);
// 删除图片
int remove(Long id);
// 查看图片详情
Image getById(Long id);
// 分页查看图片信息,使用PageHelper实现分页。
// 返回的结果是: {rows=[{图片对象}], total=总计图片数量, currentPage=当前页码, pages=总计页数, size=每页行数}
Map getImages(int page, int rows);
}
七、module : img_service_provider
pom.xml
org.springframework.boot spring-boot-starter org.apache.dubbo dubbo-spring-boot-starter org.apache.curator curator-recipes org.apache.curator curator-framework com.github.pagehelper pagehelper-spring-boot-starter com.bjsxt img_service_api 1.0-SNAPSHOT com.bjsxt img_mapper 1.0-SNAPSHOT
com.bjsxt.img.service.impl
package com.bjsxt.img.service.impl;
import com.bjsxt.img.mapper.ImageMapper;
import com.bjsxt.img.pojo.Image;
import com.bjsxt.img.serviceapi.ImageServiceAPI;
import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import org.apache.dubbo.config.annotation.Service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Service
public class ImageServiceImpl implements ImageServiceAPI {
// 注入Mapper对象
@Autowired
private ImageMapper imageMapper;
@Override
@Transactional
public int save(Image image) {
return imageMapper.insert(image);
}
@Override
@Transactional
public int remove(Long id) {
return imageMapper.deleteByPK(id);
}
@Override
public Image getById(Long id) {
return imageMapper.selectById(id);
}
@Override
public Map getImages(int page, int rows) {
// 使用分页查询
PageHelper.startPage(page, rows);
// 分页查询, 返回的结果是PageHelper封装的List的实现类型Page
List list = imageMapper.select();
// 使用PageInfo辅助工具对象,实现分页数据的获取
PageInfo info = new PageInfo<>(list);
// 创建返回结果
Map result = new HashMap<>();
result.put("total", info.getTotal()); // 总计数据行数
result.put("rows", list); // 当前页面的数据集合
result.put("currentPage", page); // 当前是第几页
result.put("pages", info.getPages()); // 总计多少页
result.put("size", rows); // 每页显示多少行。
return result;
}
}
启动类
com.bjsxt.img
package com.bjsxt.img;
import org.apache.dubbo.config.spring.context.annotation.EnableDubbo;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
@EnableDubbo
@MapperScan(basePackages = {"com.bjsxt.img.mapper"})
public class ImageProviderApp {
public static void main(String[] args) {
SpringApplication.run(ImageProviderApp.class, args);
}
}
application.yml
spring:
profiles:
active: db
dubbo:
application: # dubbo应用必须提供一个唯一的命名。同名的dubbo应用自动组成集群。
name: img_manage_provider
protocol:
name: dubbo
port: 20880
registry:
address: zookeeper://192.168.14.129:2181
config-center:
timeout: 10000
server:
port: 8081
pagehelper:
helper-dialect: mysql
启动zookeeper,启动ImageProviderApp
八、 定义module : imp_service_consumerpom.xml
org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-thymeleaf org.apache.dubbo dubbo-spring-boot-starter org.apache.curator curator-recipes org.apache.curator curator-framework cn.bestwu fastdfs-client-java org.apache.commons commons-lang3 com.bjsxt img_service_api 1.0-SNAPSHOT
application.yml
# FastDFS客户端配置
fdfs:
connect-timeout: 10
network-timeout: 30
charset: UTF-8
http:
tracker_http_port: 8080
tracker_server: 192.168.14.129:22122
dubbo:
application:
name: img_manage_consumer
registry:
address: zookeeper://192.168.89.140:2181
server:
port: 80
FastDFSUtils.java
package com.bjsxt.img.utils;
import org.csource.common.NamevaluePair;
import org.csource.fastdfs.*;
import java.util.Properties;
public class FastDFSUtils {
// 通过static初始化代码块,读取配置文件,初始化客户端连接对象。
// 客户端连接对象,用于实现文件读写操作使用。
private StorageClient storageClient;
// 默认构造
public FastDFSUtils() {
// 提供默认配置
Properties properties = new Properties();
properties.setProperty("fastdfs.connect_timeout_in_seconds", "5");
properties.setProperty("fastdfs.network_timeout_in_seconds", "30");
properties.setProperty("fastdfs.charset", "UTF-8");
properties.setProperty("fastdfs.http_tracker_http_port", "8080");
properties.setProperty("fastdfs.tracker_servers", "localhost:22122");
init(properties);
}
public FastDFSUtils(Properties properties) {
init(properties);
}
public void init(Properties properties){
try {
// 读取配置文件,借助SpringBoot实现配置
ClientGlobal.initByProperties(properties);
// 创建Tracker客户端对象
TrackerClient trackerClient = new TrackerClient();
// 创建Tracker服务器对象
TrackerServer trackerServer = trackerClient.getConnection();
// 创建Storage服务器对象
StorageServer storageServer = trackerClient.getStoreStorage(trackerServer);
// 创建Storage客户端对象
storageClient = new StorageClient(trackerServer, storageServer);
}catch(Exception e){
e.printStackTrace();
// 初始化代码块出错,一定要抛出错误,停止虚拟机。
throw new ExceptionInInitializerError(e);
}
}
public boolean deleteFile(String groupName, String fileName){
try{
// storageClient.delete_file(String groupName, String fileName);
// groupName - 要删除的文件的卷名,就是保存文件的storage服务配置中的groupName
// fileName - 要删除的文件的文件名,包含路径地址。格式:M00/目录/目录/文件名.后缀名
// M00代表保存文件的目录, store_path0 。 目录/目录 - 保存文件的具体位置。
int result = storageClient.delete_file(groupName, fileName);
// 返回结果为0,代表删除成功,其他是删除失败。
return result == 0;
}catch(Exception e){
e.printStackTrace();
return false;
}
}
public byte[] downloadFile(String groupName, String fileName, NamevaluePair[] metaDatas){
try{
byte[] datas = storageClient.download_file(groupName, fileName);
// 要下载的文件的扩展信息。
if(metaDatas != null) {
NamevaluePair[] tmp = storageClient.get_metadata(groupName, fileName);
// 把查询到的文件扩展信息。保存到传入的数组中。
for(int i = 0; i < tmp.length; i++){
metaDatas[i] = tmp[i];
}
}
// 返回下载的文件内容
return datas;
}catch(Exception e){
e.printStackTrace();
return null; // 下载失败,返回null
}
}
public String[] uploadFile(byte[] datas, String fileName, String authName){
try{
// 文件上传
// 获取文件的扩展名
String extName = fileName.substring(fileName.lastIndexOf(".") + 1);
// 创建文件扩展信息。扩展信息包括文件的原始名称,文件的大小,文件的上传者姓名
NamevaluePair[] metaDatas = new NamevaluePair[3];
metaDatas[0] = new NamevaluePair("fileName", fileName);
metaDatas[1] = new NamevaluePair("fileSize", datas.length+"");
metaDatas[2] = new NamevaluePair("auth", authName);
String[] result = storageClient.upload_file(datas, extName, metaDatas);
// 上传成功,无异常。返回字符串数组。
// 字符串数组长度为2。 0下标位置是 卷名|组名。 1下标位置是 文件名(目录/文件)
// fdfs为了解决上传的文件原始名称冲突内容不冲突而覆盖的问题,存储文件的时候,会提供一个uuid文件名称。
return result;
}catch(Exception e){
e.printStackTrace();
return null; // 异常发生,返回null。代表上传失败。
}
}
}
FastDFS客户端工具类型需要使用的自动装配对象,用来创建工具类型的对象。
spring容器管理创建的工具类型的对象
package com.bjsxt.img.config;
import com.bjsxt.img.utils.FastDFSUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.Properties;
@Configuration
public class FastDFSAutoConfigure {
@Value("${fdfs.connect-timeout}")
private String connectTimeout;
@Value("${fdfs.network-timeout}")
private String networkTimeout;
@Value("${fdfs.charset}")
private String charSet;
@Value("${fdfs.http.tracker_http_port}")
private String trackerHttpPort;
@Value("${fdfs.tracker_server}")
private String trackerServer;
@Bean
public FastDFSUtils fastDFSUtils(){
// 根据SpringBoot配置文件,创建一个Properties对象。
Properties properties = new Properties();
properties.setProperty("fastdfs.connect_timeout_in_seconds", connectTimeout);
properties.setProperty("fastdfs.network_timeout_in_seconds", networkTimeout);
properties.setProperty("fastdfs.charset", charSet);
properties.setProperty("fastdfs.http_tracker_http_port", trackerHttpPort);
properties.setProperty("fastdfs.tracker_servers", trackerServer);
// 构造工具类型对象。
FastDFSUtils fastDFSUtils = new FastDFSUtils(properties);
return fastDFSUtils;
}
}
服务接口ImageService
package com.bjsxt.img.service;
import com.bjsxt.img.pojo.Image;
import java.util.Map;
public interface ImageService {
// 保存图片, 新增
int save(Image image);
// 删除图片
int remove(Long id);
// 主键查询图片详情
Image getById(Long id);
// 分页查询图片信息
Map getImages(int page, int rows);
}
服务接口ImageService的实现类
ImageServiceImpl
package com.bjsxt.img.service.impl;
import com.bjsxt.img.pojo.Image;
import com.bjsxt.img.service.ImageService;
import com.bjsxt.img.serviceapi.ImageServiceAPI;
import org.apache.dubbo.config.annotation.Reference;
import org.springframework.stereotype.Service;
import java.util.Map;
@Service
public class ImageServiceImpl implements ImageService {
// 注入远程服务代理对象。
@Reference
private ImageServiceAPI serviceAPI;
@Override
public int save(Image image) {
return serviceAPI.save(image);
}
@Override
public int remove(Long id) {
return serviceAPI.remove(id);
}
@Override
public Image getById(Long id) {
return serviceAPI.getById(id);
}
@Override
public Map getImages(int page, int rows) {
return this.serviceAPI.getImages(page, rows);
}
}
ImageController
package com.bjsxt.img.contoller;
import com.bjsxt.img.pojo.Image;
import com.bjsxt.img.service.ImageService;
import com.bjsxt.img.utils.FastDFSUtils;
import org.csource.common.NamevaluePair;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletResponse;
import java.awt.im.InputContext;
import java.io.InputStream;
import java.net.URLEncoder;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
@Controller
public class ImageController {
@Autowired
private ImageService imageService;
@Autowired
private FastDFSUtils utils;
@GetMapping(value = {"/", "/index"})
public String toIndex(@RequestParam(value = "page", defaultValue = "1") int page,
@RequestParam(value = "rows", defaultValue = "5") int rows,
Model model){
// 调用服务逻辑,分页查询
Map result = this.imageService.getImages(page, rows);
// 查询结果使用请求作用域传递给页面。
model.addAttribute("datas", result);
return "index";
}
}
index.html
图片管理-分页查看
启动类ImageConsumerApp
package com.bjsxt.img;
import org.apache.dubbo.config.spring.context.annotation.EnableDubbo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
@EnableDubbo
public class ImageConsumerApp {
public static void main(String[] args) {
SpringApplication.run(ImageConsumerApp.class, args);
}
}
上传页面
upload.html
图片管理-上传图片
完善功能ImageController
package com.bjsxt.img.contoller;
import com.bjsxt.img.pojo.Image;
import com.bjsxt.img.service.ImageService;
import com.bjsxt.img.utils.FastDFSUtils;
import org.csource.common.NamevaluePair;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletResponse;
import java.awt.im.InputContext;
import java.io.InputStream;
import java.net.URLEncoder;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
@Controller
public class ImageController {
@Autowired
private ImageService imageService;
@Autowired
private FastDFSUtils utils;
@GetMapping("/download")
public void download(Long id, HttpServletResponse response){
try {
// 查询数据
Image image = imageService.getById(id);
// 从FastDFS中查询要下载的文件内容
NamevaluePair[] metaDatas = new NamevaluePair[3];
byte[] datas = utils.downloadFile(image.getGroupName(), image.getRemoteFileName(), metaDatas);
// 使用响应输出流,向客户端输出文件内容,并提示下载。
// 设置响应头为流输出
response.setContentType("application/octet-stream");
// 获取文件的原始名称
String fileName = "";
for (NamevaluePair nvp : metaDatas) {
if (nvp.getName().equals("fileName")) {
fileName = nvp.getValue();
}
}
// 编码处理,避免响应头设置的中文出现乱码。
fileName = URLEncoder.encode(fileName, "UTF-8");
// 设置响应头,并标记附件文件名为fileName。
response.setHeader("content-disposition", "attachment;filename=" + fileName);
// 输出文件内容到客户端
response.getOutputStream().write(datas);
// 刷新输出流缓冲。
response.getOutputStream().flush();
}catch(Exception e){
e.printStackTrace();
return ;
}
}
@GetMapping("/remove")
public String remove(Long id){
// 从数据库查询id对应的Image对象
Image image = imageService.getById(id);
int rows = imageService.remove(id); // 删除数据库中的数据
if(rows == 1){
// 删数据成功,需要从FastDFS中删除对应的图片
utils.deleteFile(image.getGroupName(), image.getRemoteFileName());
}
return "redirect:/index?page=1&rows=5";
}
@PostMapping("/saveImages")
public String saveImages(String[] url, String[] groupName, String[] remoteFileName, String[] originFileName){
if(url.length != groupName.length || url.length != remoteFileName.length || url.length != originFileName.length){
// 参数个数不对。 不做任何操作,直接返回。
return "redirect:/index?page=1&rows=5";
}
for(int i = 0; i < url.length; i++){
// 循环新增图片对象数据,到数据库
Image image = new Image();
image.setGroupName(groupName[i]);
image.setRemoteFileName(remoteFileName[i]);
image.setOriginFileName(originFileName[i]);
image.setUploadTime(new Date());
this.imageService.save(image);
}
return "redirect:/index?page=1&rows=5";
}
@PostMapping("/uploadImg")
@ResponseBody
public Object uploadImg(String filename, @RequestParam("imgFile") MultipartFile imgFile){
try {
InputStream inputStream = imgFile.getInputStream();
byte[] datas = new byte[inputStream.available()];
inputStream.read(datas);
// 上传文件
String[] result = utils.uploadFile(datas,imgFile.getOriginalFilename(), "老金");
Map map = new HashMap<>();
map.put("url", "http://192.168.89.140:8888/"+result[0]+"/"+result[1]);
map.put("error", 0);
map.put("groupName", result[0]);
map.put("remoteFileName", result[1]);
map.put("originFileName", imgFile.getOriginalFilename());
return map;
}catch(Exception e){
e.printStackTrace();
Map map = new HashMap<>();
map.put("message", "上传文件失败");
map.put("error", 1);
return map;
}
}
@GetMapping("/toUpload")
public String toUpload(){
return "upload";
}
@GetMapping(value = {"/", "/index"})
public String toIndex(@RequestParam(value = "page", defaultValue = "1") int page,
@RequestParam(value = "rows", defaultValue = "5") int rows,
Model model){
// 调用服务逻辑,分页查询
Map result = this.imageService.getImages(page, rows);
// 查询结果使用请求作用域传递给页面。
model.addAttribute("datas", result);
return "index";
}
}



