Практически в каждом веб-проекте требуется собственное хранилище файлов. Назначений у него множество. Сегодня мы рассмотрим 2 простых варианта его создания: первый — с использованием типа данных blob средствами Java, Spring MVC, Hibernate, MySQL и второй — с кластеризацией (разбиением файла на кусочки) средствами groovy, grails, hibernate, PostgreSQL.
Зачем нужен этот велосипед? Зачастую нужно отдавать пользователю сформированные на стороне сервера файлы и предусмотреть возможность самому выкладывать туда что-нибудь. К тому же, мы работаем с СУБД, к которой можно подключиться по JDBC с других хостов, и если сделать реплицируемую базу с несколькими нодами, то получится хорошая балансировка нагрузки на скачивание.
Вариант 1 — Использование типа данных blob
Итак, у нас есть Spring MVC. Создаём бин с persistence слоя, в котором заложен функционал “объекта”, хранимого в базе
package ru.cpro.uchteno.domain.attach;
import java.sql.Blob;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Lob;
import javax.persistence.Table;
import org.hibernate.annotations.Type;
@Entity
@Table(name = "data_object")
public class DataObject {
@Id @GeneratedValue(strategy=GenerationType.IDENTITY)
@Column(name="id")
private Integer id;
@Column(name="data_name")
private String name;
@Column(name="data_data", columnDefinition="longblob", length=2*1024*1024*1024)
@Lob()
private Blob data;
@Column(name="contype")
private String contentType;
@Column(name="surname")
private String surname;
@Column(name="access_count")
private Integer accessCount;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Blob getData() {
return data;
}
public void setData(Blob data) {
this.data = data;
}
public String getContentType() {
return contentType;
}
public void setContentType(String contentType) {
this.contentType = contentType;
}
public String getSurname() {
return surname;
}
public void setSurname(String surname) {
this.surname = surname;
}
public Integer getAccessCount() {
return accessCount;
}
public void setAccessCount(Integer accessCount) {
this.accessCount = accessCount;
}
}
private Integer id — идентификатор, первичный ключ нашего объекта;
private String name — имя загруженного объекта;
private Blob data — данные объекта;
private String contentType — тип данных объекта;
private String surname — видимое пользователем имя объекта;
private Integer accessCount — количество скачиваний;
Теперь опишем интерфейс дао слоя для нашего объекта.
package ru.cpro.uchteno.dao.attach;
import java.util.List;
import ru.cpro.uchteno.domain.attach.DataObject;
public interface AttachmentDAO {
//Получить объект по айдишнику.
public DataObject getObjectByID(Integer id);
//Сохранить объект.
public void saveOrUpdate(DataObject dao);
//удалить объект.
public void deleteDataObject(DataObject dao);
//Получить список всех объектов.
public List<DataObject> listObjects();
//Получить объект по “фамилии”, видимому узеру имени.
public DataObject getObjectBySurname(String surname);
//Поиск объектов в базе по имени.
public List<DataObject> searchObjectsByName(String query);
}
Делаем реализацию интерфейса:
package ru.cpro.uchteno.dao.attach;
import java.util.ArrayList;
import java.util.List;
import org.hibernate.SQLQuery;
import org.hibernate.Session;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.orm.hibernate3.HibernateTemplate;
import org.springframework.stereotype.Repository;
import ru.cpro.uchteno.domain.attach.DataObject;
@Repository
public class AttachmentDAOImpl implements AttachmentDAO {
//Внедряем hibernateTemplate
@Autowired
private HibernateTemplate attachHibernateTemplate;
@Override
public List<DataObject> listObjects() {
return attachHibernateTemplate.find("from DataObject");
}
@Override
public DataObject getObjectByID(Integer id) {
List<DataObject> doList = attachHibernateTemplate.find(
"from DataObject where id = ?", id);
if (doList == null || doList.size() <= 0)
return null;
return doList.get(0);
}
@Override
public void saveOrUpdate(DataObject dao) {
attachHibernateTemplate.saveOrUpdate(dao);
}
@Override
public void deleteDataObject(DataObject dao) {
attachHibernateTemplate.delete(dao);
}
@Override
public DataObject getObjectBySurname(String surname) {
List<DataObject> doList = attachHibernateTemplate.find(
"from DataObject where surname = ?", surname);
if (doList == null || doList.size() <= 0)
return null;
return doList.get(0);
}
@Override
public List<DataObject> searchObjectsByName(String query) {
if (query == null || query.trim().equals(""))
return attachHibernateTemplate
.find("from DataObject order by id desc limit 20");
return attachHibernateTemplate.find(
"from DataObject where name like ?", "%" + query + "%");
}
}
В сервисе делегируем все методы из дао
package ru.cpro.uchteno.service.attach;
import java.util.List;
import ru.cpro.uchteno.domain.attach.DataObject;
public interface AttachmentService {
public DataObject getObjectByID(Integer id);
public void saveOrUpdate(DataObject dao);
public void deleteDataObject(DataObject dao);
public List<DataObject> listObjects();
public DataObject getObjectBySurname(String surname);
public List<DataObject> searchObjectsByName(String query);
}
package ru.cpro.uchteno.service.attach;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import ru.cpro.uchteno.dao.attach.AttachmentDAO;
import ru.cpro.uchteno.domain.attach.DataObject;
@Service
public class AttachmentServiceImpl implements AttachmentService {
@Autowired
private AttachmentDAO attachmentDAO;
@Override
public List<DataObject> listObjects() {
return attachmentDAO.listObjects();
}
@Override
public DataObject getObjectByID(Integer id) {
return attachmentDAO.getObjectByID(id);
}
@Override
public void saveOrUpdate(DataObject dao) {
attachmentDAO.saveOrUpdate(dao);
}
@Override
public void deleteDataObject(DataObject dao) {
attachmentDAO.deleteDataObject(dao);
}
@Override
public DataObject getObjectBySurname(String surname) {
return attachmentDAO.getObjectBySurname(surname);
}
@Override
public List<DataObject> searchObjectsByName(String query) {
return attachmentDAO.searchObjectsByName(query);
}
}
Всё, что нужно у нас есть. Теперь осталось только сделать контроллер:
package ru.cpro.uchteno.web.attachment;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.sql.Blob;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.List;
import javax.servlet.http.HttpServletResponse;
import org.hibernate.Hibernate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.multipart.MultipartFile;
import ru.cpro.uchteno.domain.attach.DataObject;
import ru.cpro.uchteno.service.attach.AttachmentService;
@Controller()
@RequestMapping("/")
public class AttachmentController {
//Подключаем наш сервис
@Autowired
private AttachmentService attachmentService;
//Вывод на страницу всех файлов в базе
@RequestMapping("/admin/dataObjects/private")
public String index(HashMap<String, Object> map) {
List<DataObject> doList = attachmentService.listObjects(); //список файлов
map.put("dlist", doList);// заталкать его в кодель фтраницы
return "admin/attach/attach";// вернуть путь к вьюхе
}
//Добавление файла
@RequestMapping("/admin/dataObjects/private/add")
public String add(
HashMap<String, Object> map,
@RequestParam(value = "addName", required = true) String name,
@RequestParam(value = "addFile", required = true) MultipartFile file,
@RequestParam(value = "addSurname", required = true) String surname) {
try {
DataObject dob = new DataObject();//Создать новый файл
if (name == null || name.trim().equals("")) //Если имя файла пустое
dob.setName(file.getOriginalFilename()); // Взять имя из загружаемого файла
else
dob.setName(name); // иначе дать файлу новое имя
Blob blob = Hibernate.createBlob(file.getInputStream()); //Создаем блоб по входному потоку
dob.setData(blob);
dob.setContentType(file.getContentType()); // Контент тайп
dob.setSurname(surname); // задаем файлу “фамилию”
attachmentService.saveOrUpdate(dob); // сохраняем файл
} catch (IOException e) {
System.out.println("Не удалось записать объект в базу");
return "redirect:/admin/attach/dataObjects/private/";
}
return "redirect:/admin/dataObjects/private/";
}
//Глушим файлы по айдишнику
@RequestMapping("/admin/dataObjects/private/del")
public @ResponseBody
String del(@RequestParam(value = "id", required = true) Integer id) {
try {
DataObject dao = attachmentService.getObjectByID(id);
attachmentService.deleteDataObject(dao);
} catch (Exception ex) {
return "FAIL";
}
return "SUCCESS";
}
//Скачивание файлов по фамилии. Публичная часть.
@RequestMapping("/public/dataObjects/getObject")
public void getObject(
@RequestParam(value = "s", required = false) String surname,
HttpServletResponse resp) {
if (surname == null || surname.trim().equals("")) {
try {
resp.getOutputStream().close();
return;
} catch (IOException e) {
e.printStackTrace();
}
}
DataObject dao = attachmentService.getObjectBySurname(surname); // Ищем файл по фамилии
if (dao == null) {
try {
resp.getOutputStream().close();
} catch (Exception ex) {
}
return;
}
// наращиваем счетчик скачиваний
if (dao.getAccessCount() == null)
dao.setAccessCount(1);
else
dao.setAccessCount(dao.getAccessCount() + 1);
attachmentService.saveOrUpdate(dao);
//Отдаем юзеру файл
Blob blob = dao.getData();
try {
InputStream is = blob.getBinaryStream();
OutputStream os = resp.getOutputStream();
resp.setContentType(dao.getContentType());
int n = 0;
byte buff[] = new byte[1024];
while (n >= 0) {
n = is.read(buff);
if (n > 0)
os.write(buff, 0, n);
}
is.close();
os.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
Вариант 2 — Файловое хранилище с кластеризацией
Я решил использовать grails. Но когда пишем на groovy, Blob не работает адекватно. Поэтому разобьем наш файл на кластеры и сохраним по-отдельности.
В принципе тут всё делается практически так же, как и в предыдущем примере. Приложение доступно здесь.
Спасибо всем за внимание! Надеюсь, что мои примеры будут вам полезны.
Автор: boiler5