市长信箱邮件查询服务: 使用SpringBoot搭建基础

2 篇文章 0 订阅
2 篇文章 0 订阅

市长信箱邮件查询服务: 使用SpringBoot搭建基础

一直想用SpringBoot做个微服务,练练手, 为后续部署到docker打下基础. 今天比较空闲, 就开始把部分想法落地了.
git地址:
https://github.com/ybak/mycrawler/tree/basic


概览

用来练手的demo应用是一个市长信箱的内容抓取与检索页面. 鉴于我的八卦特质,总想了解下周边的一些投诉信息. 而成都的市长信箱是一个绝好的信息来源.

信件格式:
来信情况张三
来信标题生活困扰
来信内容尊敬市长你好我们有十三户污水到我处无法排走,使我处蚊虫飞舞…..
办理结果郫县(2016-05-20 11:31:10): 来信人: 您好! 来信收悉。感谢您对政府工作的关心与支持……

这个demo应用的主要功能有:

  1. 从市长信箱抓取所有的市民投诉并保存
  2. 提供按关键字检索的web页面来检索感兴趣的投诉信息

按照循序渐进的原则, 先实现只实现基本功能, 不考虑性能, 后续再进行优化.
Mysql的提供了基本的模糊匹配功能, 且SpringBoot中,能方便的集成JPA.
使用Mysql保存抓取信息, 并提供给Web应用查询, 是很容易实现的. 所以该demo应用的第一版技术设计如下:
这里写图片描述
SpringBoot的代码使用maven的多模块组织:
父模块(声明此工程的spring-boot的版本)
pom.xml

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>1.3.5.RELEASE</version>
</parent>
crawler-downloader:抓取模块
pom.xml
crawler-persistence:存储模块(使用spring的jpa实现orm)
pom.xml
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
crawler-search-web:页面模块(使用spring的thymeleaf实现mvc和rest api)
pom.xml
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

接下来,分别介绍各个模块的细节:


crawler-persistence:存储模块

根据信件内容的格式,设计存储信件的表结构如下:
这里写图片描述

抓取邮件信息是的DB操作

这里我使用的rxjava-jdbc来进行数据的插入.相比JPA, rxjava-jdbc如果做基础的查询和插入操作使用起来很方便.

// 查询邮件详情url
Iterable<Tuple2<Integer, String>> results = db
.select("select id, url from chengdu12345 limit ?,?").parameters(i * 50, 50)
.getAs(Integer.class, String.class).toBlocking().toIterable();
//插入邮件记录
int updates = db.update("insert into chengdu12345(url, title, sender, accept_unit, status, category, views, create_date) values (?,?,?,?,?,?,?,?)")
.parameters(url, title, sender, receiveUnit, status, category, views, publishDate)
.execute();
WEB展示的DB操作

查询数据库的邮件信息时, 会涉及到分页, 模糊匹配, 这个时候rxjava-jdbc显的有些力不从心了. 而spring-data的大量的模板方法,会让查询代码简化. 所以这里我使用了spring-data-jpa的方式来进行查询.

@Table(name = "chengdu12345")
@NamedQuery(name = "Mail.search",
        query = "select m from Mail m where m.title like ?1 or m.content like ?1 or m.result like ?1")
public class Mail implements Serializable {...}
......
public interface MailRepository extends Repository<Mail, Long> {
    Page<Mail> search(String keyword, Pageable pageable);
}
......
public class MailService {
    @Autowired
    private MailRepository mailRepository;

    public Page<Mail> search(String keyword, Pageable pageable) {
        return mailRepository.search("%" + keyword + "%", pageable);
    }
}

MailService 的search方法,只需传入Pageable 实例, spring-data将自动为我们处理好分页的逻辑, 非常方便.


crawler-downloader:抓取模块

市长信箱的邮件展示列表中只有邮件标题和邮件详情链接等基础信息,没有邮件正文和处理结果详情. 我的抓取流程是:

  1. 遍历所有的邮件列表的分页信息, 将邮件基础信息保存到数据库.
  2. 遍历数据库中的所有已保存的邮件基础信息, 取出邮件详情链接, 再对该链接进行抓取, 取得内容进行分析并保存到数据库中.

我用来抓取页面的http客户端类库是okhttp,
okhttp不但提供了简洁的api, 还在内部建立了url连接池, 在快速抓取页面时, 减少了tcp链接的建立, 提高了速度, 也降低了抓取失败的几率.

public class HtmlUtil {
    static OkHttpClient client = new OkHttpClient();
    public static String getURLBody(String url) throws IOException {
        Request request = new Request.Builder()
                .url(url)
                .build();
        Response response = client.newCall(request).execute();
        if (!response.isSuccessful()) {
            response.body().close();
            throw new IllegalArgumentException(response.message());
        }
        return response.body().string();
    }
}

页面解析的列库我使用了Jsoup, Jsoup也可以直接用来抓取页面. 但它没有提供易用的连接池机制. 默认每次抓取都会创建tcp连接. 在快速抓取页面的情况下很容易打开过多的端口,从而造成抓取失败. 但Jsoup的html解析api却是相当的强大. 尤其它的对css selector的支持, 选取dom就像使用jquery一样方便.

String html = HtmlUtil.getURLBody(pageUrl);
Document doc = Jsoup.parse(html);
Elements elements = doc.select("div.left5 ul li.f12px");
for (Element element : elements) {
    String url = urlPrefix + element.select("css").attr("href");
......
}

crawler-search-web:页面模块

页面模块是使用SpringBoot启动的模块. 该模块功能非常简单:

  1. 提供一个静态页面
  2. 提供一个搜索API

这里使用Spring MVC来提供实现:

@Controller
public class WelcomeController {
    @Autowired
    private MailService mailService;
    @RequestMapping("/")
    public String welcome() {
        return "welcome";//提供静态页面
    }
    @RequestMapping("/search")
    @ResponseBody
    public Page<Mail> search(String keyword) {
        Pageable query = new PageRequest(0, 100);//提供一个搜索API
        return mailService.search(keyword, query);
    }
}

有了Controller和页面, 剩下的工作就是利用Spring Boot来启动工程了. 使用Spring Boot启动应用非常方便, 只需几行代码:

@SpringBootApplication(scanBasePackages = {
        "org.ybak.crawler.persistence.service",
        "org.ybak.crawler.web"
})
@EnableJpaRepositories("org.ybak.crawler.persistence.repo")
@EntityScan("org.ybak.crawler.persistence.vo")
public class WebApplication {
    public static void main(String[] args) throws Exception {
        SpringApplication.run(WebApplication.class, args);
    }
}

有了Spring Boot的启动类, 只用运行main就可以启动应用了. 最后的页面是这样的:
这里写图片描述


踩过的坑

  1. 应用启动报错, 提示”Service not found.”

    因为Spring Boot默认会扫描启动类所在包(org.ybak.crawler.web)下的Spring注解. 但我的Service类在另外一个包:org.ybak.crawler.persistence.service, 所以Spring启动时没有将service初始化. 解决的方法很简单. 参照上面的WebApplication代码中scanBasePackages设置, 制定扫描的包列表即可.

  2. 应用启动报错, 提示”Repository not found.”

    和之前的问题类似, 需要通过EnableJpaRepositories指定repo的扫描路径.

  3. 应用启动报错,提示”Entity not found.”

    和之前的问题类似, 需要通过EntityScan指定Entity的扫描路径.

  4. 使用SpringBoot开发时, 页面模板文件修改后浏览器不生效, Java逻辑修改后不生效.

    引入spring-boot-devtools, 该模块可在调试时设置各种禁止模板缓存的配置, 方便开发调试.

  5. 使用了SpringBootDevtools开发时, 任何文件修改都会导致SpringBoot重启. 影响开发效率.

    devtools通过重启来加载新类,让新代码生效. 但没完没了的重启也会降低开发效率. 幸好spring提供了spring-loaded工具, 可以理解为开源的针对spring的JRebel. 使用了它以后, 就可以享受无重启热部署了.

总结

通过使用Spring Boot来快速实现一个web应用, 确实感受到它的方便. 大量约定的默认配置能让代码简介不少, 但当需要自定义配置是, 面对spring-boot凌乱的文档, 有着实让人头大. 必须经常google才能解决不断冒出的问题.
另一方面, 使用Spring Boot开发一个简单的Web应用,并不能展示Spring Boot作为微服务开发框架的威力. 后续我调整这个web应用的架构, 配以docker+ elasticsearch, 来实现这个应用的微服务化.
这里写图片描述

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
好的,以下是一个更完整的示例代码,可以实现您所需的所有功能: ```cpp #include <iostream> #include <fstream> #include <vector> #include <string> using namespace std; // 地区类 class Area { protected: string name; // 地区名称 public: Area(string name) : name(name) {} virtual void showInfo() { cout << "地区名称:" << name << endl; } string getName() { return name; } }; // 省份类,继承自地区类 class Province : public Area { private: int population; // 省份人口 public: Province(string name, int population) : Area(name), population(population) {} void showInfo() { cout << "省份名称:" << name << endl; cout << "省份人口:" << population << endl; } }; // 市区类,继承自地区类 class City : public Area { private: string mayor; // 市长姓名 public: City(string name, string mayor) : Area(name), mayor(mayor) {} void showInfo() { cout << "市区名称:" << name << endl; cout << "市长姓名:" << mayor << endl; } }; // 客户类 class Customer { protected: string name; // 客户姓名 int age; // 客户年龄 string area; // 客户所属地区 public: Customer(string name, int age, string area) : name(name), age(age), area(area) {} virtual void showInfo() { cout << "客户姓名:" << name << endl; cout << "客户年龄:" << age << endl; cout << "客户所属地区:" << area << endl; } string getName() { return name; } string getArea() { return area; } }; // VIP客户类,继承自客户类 class VIPCustomer : public Customer { private: double money; // VIP客户消费金额 public: VIPCustomer(string name, int age, string area, double money) : Customer(name, age, area), money(money) {} void showInfo() { cout << "VIP客户姓名:" << name << endl; cout << "VIP客户年龄:" << age << endl; cout << "VIP客户所属地区:" << area << endl; cout << "VIP客户消费金额:" << money << endl; } }; // 客户管理系统类 class CustomerManagementSystem { private: vector<Area*> areas; // 地区列表 vector<Customer*> customers; // 客户列表 public: // 构造函数,读取文件中的数据 CustomerManagementSystem() { // 读取地区信息 ifstream areaFile("areas.txt"); if (areaFile.is_open()) { string line; while (getline(areaFile, line)) { string name = line; areas.push_back(new Area(name)); } areaFile.close(); } // 读取客户信息 ifstream customerFile("customers.txt"); if (customerFile.is_open()) { string line; while (getline(customerFile, line)) { string name = line; int age; string area; double money; getline(customerFile, line); age = stoi(line); getline(customerFile, line); area = line; getline(customerFile, line); if (line != "") { money = stod(line); customers.push_back(new VIPCustomer(name, age, area, money)); } else { customers.push_back(new Customer(name, age, area)); } } customerFile.close(); } } // 析构函数,保存数据到文件中 ~CustomerManagementSystem() { // 保存地区信息 ofstream areaFile("areas.txt"); if (areaFile.is_open()) { for (Area* area : areas) { areaFile << area->getName() << endl; } areaFile.close(); } // 保存客户信息 ofstream customerFile("customers.txt"); if (customerFile.is_open()) { for (Customer* customer : customers) { customerFile << customer->getName() << endl; customerFile << customer->age << endl; customerFile << customer->getArea() << endl; VIPCustomer* vip = dynamic_cast<VIPCustomer*>(customer); if (vip) { customerFile << vip->money << endl; } else { customerFile << endl; } } customerFile.close(); } } // 添加地区 void addArea(string name) { areas.push_back(new Area(name)); } // 添加省份 void addProvince(string name, int population) { areas.push_back(new Province(name, population)); } // 添加市区 void addCity(string name, string mayor) { areas.push_back(new City(name, mayor)); } // 添加客户 void addCustomer(string name, int age, string area) { customers.push_back(new Customer(name, age, area)); } // 添加VIP客户 void addVIPCustomer(string name, int age, string area, double money) { customers.push_back(new VIPCustomer(name, age, area, money)); } // 修改客户信息 void modifyCustomer(string name, int age, string area, double money = -1) { for (Customer* customer : customers) { if (customer->getName() == name) { customer->age = age; customer->area = area; VIPCustomer* vip = dynamic_cast<VIPCustomer*>(customer); if (vip) { vip->money = money; } break; } } } // 删除客户 void deleteCustomer(string name) { for (int i = 0; i < customers.size(); i++) { if (customers[i]->getName() == name) { customers.erase(customers.begin() + i); break; } } } // 查找客户 void searchCustomer(string name) { bool found = false; for (Customer* customer : customers) { if (customer->getName() == name) { customer->showInfo(); found = true; break; } } if (!found) { cout << "未找到客户:" << name << endl; } } // 输出所有地区信息 void showAllAreas() { for (Area* area : areas) { area->showInfo(); } } // 输出所有客户信息 void showAllCustomers() { for (Customer* customer : customers) { customer->showInfo(); } } }; int main() { CustomerManagementSystem cms; // 添加地区信息 cms.addArea("中国"); cms.addProvince("广东省", 120000000); cms.addCity("深圳市", "张三"); // 添加客户信息 cms.addCustomer("李四", 30, "广东省"); cms.addVIPCustomer("王五", 40, "深圳市", 100000); // 修改客户信息 cms.modifyCustomer("李四", 35, "广东省"); // 删除客户信息 cms.deleteCustomer("王五"); // 查找客户信息 cms.searchCustomer("李四"); // 输出所有地区信息 cms.showAllAreas(); // 输出所有客户信息 cms.showAllCustomers(); return 0; } ``` 在这个示例代码中,我们定义了一个CustomerManagementSystem类,用于管理地区和客户信息,并实现了各种操作,如添加、修改、删除、查找和输出等。在程序启动时,我们从文件中读取数据,并在程序结束时将数据保存到文件中。在客户类和VIP客户类中,我们使用了虚函数和多态性的知识,并使用dynamic_cast运算符来判断对象的类型,以实现不同的操作。在主函数中,我们创建了一个CustomerManagementSystem对象,并进行各种操作,以演示该程序的功能。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值