SpringBoot开发案例之打造十万博文Web篇

前言

通过 Python 爬取十万博文之后,最重要的是要让互联网用户访问到,那么如何做呢?

选型

从后台框架、前端模板、数据库连接池、缓存、代理服务、限流等组件多个维度选型。

  • 后台框架 SpringBoot2+、JPA
  • 前端框架 Vue
  • 模块框架 Thymeleaf
  • 数据库连接池 HikariCP
  • 缓存 Redis
  • 限流 Guava
  • 代理服务 Nginx
  • 文章编辑 Markdown

架构

博文

我们可以通过以下方式访问:

https://blog.52itstyle.top/49.html

亦或是:

https://blog.52itstyle.top/49.shtml

当然,如果你愿意你也可以显示为:

https://blog.52itstyle.top/49.php
https://blog.52itstyle.top/49.asp
https://blog.52itstyle.top/49.jsp

只需要在后台配置对应的映射关系即可:

/**
* 博文
*/
@RequestMapping("{id}.html")
public String blog(@PathVariable("id") Long id, ModelMap model) {
   Blog blog = blogService.getById(id);
   model.addAttribute("blog",blog);
   return  "article";
}

由于数据库存储的是 markedown 格式的数据,前台我们通过 editormd 转为 html 代码显示,这里只展示部分代码:


<!--省略部分代码-->

<!--省略部分代码-->
<div id="article">
    <textarea  th:text="${blog.content}"  style="display:none;" placeholder="markdown语言">
    </textarea>
</div>
<!--省略部分代码-->

缓存

爬取的博文一般、基本、大概不会修改,所以我们完全可以缓存起来,避免跟数据库直接交互,顺便提升一下访问速速。正好手头有个 256MB 的阿里云 Redis 服务,拿来就用了。

首相引入以下组件:

<dependency>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

配置 redis:

spring.redis.database=1
spring.redis.host=r-m5e4873fd882de14.redis.rds.aliyuncs.com
spring.redis.port=6379
spring.redis.password=6347888
spring.redis.pool.max-active=8
spring.redis.pool.max-wait=-1
spring.redis.pool.max-idle=8
spring.redis.pool.min-idle=0
spring.redis.timeout=3000ms
spring.cache.type = redis

接口实现,引入 Cacheable 注解:

@Override
@Cacheable(cacheNames ="blog")
public Blog getById(Long id) {
     String nativeSql = "SELECT * FROM blog WHERE id=?";
     return dynamicQuery.nativeQuerySingleResult(Blog.class,nativeSql,new Object[]{id});
}

配置完成之后,我们打开数据库配置,多次访问博文地址,如果只是初次打印 SQL 说明配置成功:

spring.jpa.show-sql = true

限流

万一哪天流量暴涨亦或是有人恶意攻击,尔等小服务器根本扛不住,所以有时候我们需要一定的手段进行限流,比如限制IP访问的频率次数。

这里我们使用开源的第三方组件库,引入以下组件:

<dependency>
     <groupId>com.google.guava</groupId>
     <artifactId>guava</artifactId>
     <version>25.1-jre</version>
</dependency>

自定义注解:

/**
 * 自定义注解  限流
 */
@Target({ElementType.PARAMETER, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public  @interface ServiceLimit {
    /**
     * 描述
     */
    String description()  default "";

    /**
     * key
     */
    String key() default "";

    /**
     * 类型
     */
    LimitType limitType() default LimitType.CUSTOMER;

    enum LimitType {
        /**
         * 自定义key
         */
        CUSTOMER,
        /**
         * 根据请求者IP
         */
        IP
    }
}

限流逻辑:

/**
 * 限流 AOP
 */
@Aspect
@Configuration
public class LimitAspect {

    //根据IP分不同的令牌桶, 每天自动清理缓存
    private static LoadingCache<String, RateLimiter> caches = CacheBuilder.newBuilder()
            .maximumSize(1000)
            .expireAfterWrite(1, TimeUnit.DAYS)
            .build(new CacheLoader<String, RateLimiter>() {
                @Override
                public RateLimiter load(String key){
                    // 新的IP初始化 每秒只发出5个令牌
                    return RateLimiter.create(5);
                }
            });

    //Service层切点  限流
    @Pointcut("@annotation(com.itstyle.blog.common.limit.ServiceLimit)")
    public void ServiceAspect() {

    }

    @Around("ServiceAspect()")
    public  Object around(ProceedingJoinPoint joinPoint) {
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        ServiceLimit limitAnnotation = method.getAnnotation(ServiceLimit.class);
        ServiceLimit.LimitType limitType = limitAnnotation.limitType();
        String key = limitAnnotation.key();
        Object obj;
        try {
            if(limitType.equals(ServiceLimit.LimitType.IP)){
                key = IPUtils.getIpAddr();
            }
            RateLimiter rateLimiter = caches.get(key);
            Boolean flag = rateLimiter.tryAcquire();
            if(flag){
                obj = joinPoint.proceed();
            }else{
                throw new RrException("小同志,你访问的太频繁了");
            }
        } catch (Throwable e) {
            throw new RrException("小同志,你访问的太频繁了");
        }
        return obj;
    }
}

收录

完事具备,就差被搜索引擎收录了,我们可以通过手动生成网站地图,提交给百度。

/**
 * 生成地图
 * 参见:https://blog.52itstyle.top/sitemap.xml
 */
@Component
public class SitemapTask {

    @Autowired
    private DynamicQuery dynamicQuery;

    protected Logger logger = LoggerFactory.getLogger(getClass());

    @Value("${blog.url}")
    private  String blogUrl;

    //每天23点执行一次
    @Scheduled(cron = "0 0 23 * * ?")
    public void createSitemap() {
        logger.info("定时提交百度收录开始");
        StringBuffer xml = new  StringBuffer();
        xml.append("<?xml version='1.0' encoding='utf-8'?>\n");
        xml.append("<urlset>\n");
        String nativeSql = "SELECT id,create_time FROM blog";
        List<Object[]> list = dynamicQuery.query(nativeSql,new Object[]{});
        list.forEach(blog -> {
            String url = blogUrl+blog[0]+".html";
            xml.append("   <url>\n");
            xml.append("       <loc>"+url+"</loc>\n");
            xml.append("       <lastmod>"+blog[1]+"</lastmod>\n");
            xml.append("   </url>\n");
        });
        xml.append("</urlset>\n");
        saveAsFileWriter(xml.toString());
        logger.info("定时提交百度收录结束");
    }

    private static void saveAsFileWriter(String content) {
        String path = ClassUtils.getDefaultClassLoader().getResource("").getPath();
        String filePath = path + "static"+ SystemConstant.SF_FILE_SEPARATOR+"sitemap.xml";
        FileWriter fwriter = null;
        try {
            fwriter = new FileWriter(filePath, false);
            fwriter.write(content);
        } catch (IOException ex) {
            ex.printStackTrace();
        } finally {
            try {
                fwriter.flush();
                fwriter.close();
            } catch (IOException ex) {
                ex.printStackTrace();
            }
        }
    }
}

打包

尽量不要以Jar包形式部署,为了以后方便部署,最好放置到 外置Tomcat 下。

pom.xml 中移除内置 Tomcat:

<dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-tomcat</artifactId>
      <scope>provided</scope>
</dependency>

修改启动类:

/**
 * 启动类
 * 创建者 科帮网
 * 创建时间 2019年7月21日
 */
@SpringBootApplication
@EnableCaching
@EnableScheduling
public class Application extends SpringBootServletInitializer {
    private static final Logger logger = LoggerFactory.getLogger(Application.class);

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
        logger.info("项目启动");
    }

    @Override
    protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
        return application.sources(Application.class);
    }
}

代理

项目部署后,最好加一层代理服务,这里我们使用Nginx:

server {
    listen 80;
    server_name blog.52itstyle.top;
    return 301 https://$server_name$request_uri;
}
server{
    listen 443 ssl;
    server_name blog.52itstyle.top;
    #证书路径
    ssl_certificate    /usr/local/openresty/nginx/cert/2543486_blog.52itstyle.top.pem;
    #私钥路径
    ssl_certificate_key   /usr/local/openresty/nginx/cert/2543486_blog.52itstyle.top.key;
    #缓存有效期
    ssl_session_timeout 5m;
    #可选的加密算法,顺序很重要,越靠前的优先级越高.
    ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!ADH:!RC4;
    #安全链接可选的加密协议
    ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
    ssl_prefer_server_ciphers on;
    location = /500.html {
        root   /usr/local/openresty/nginx/html;
    }
    error_page 500 502 503 504 = /503/503.html;
    location / {
        proxy_pass  http://127.0.0.1:8080;
    }
    location ~ /\.ht {
        deny  all;
    }
}

动静分离,将静态文件交由Nginx处理,加速博客访问:

#静态文件交给nginx处理
location ~ .*\.(js|css|gif|jpg|jpeg|png|bmp)?$
{
   root /home/tomcat8/webapps/ROOT/WEB-INF/classes/static;
   expires 2h;
}

源码:https://gitee.com/52itstyle/Python

演示:https://blog.52itstyle.top

列表:https://blog.52itstyle.top/index

详情:https://blog.52itstyle.top/49.shtml

小结

撸完整个项目,基本能接触的都用上了,前后端框架、连接池、限流、缓存、动静分离,HTTPS安全认证、百度收录等等,特别适合有一定开发基础的小伙伴!

原文地址:https://www.cnblogs.com/smallSevens/p/11301025.html

时间: 2024-10-01 18:51:31

SpringBoot开发案例之打造十万博文Web篇的相关文章

SpringBoot开发案例从0到1构建分布式秒杀系统

前言 最近,被推送了不少秒杀架构的文章,忙里偷闲自己也总结了一下互联网平台秒杀架构设计,当然也借鉴了不少同学的思路.俗话说,脱离案例讲架构都是耍流氓,最终使用SpringBoot模拟实现了部分秒杀场景,同时跟大家分享交流一下. 秒杀场景 秒杀场景无非就是多个用户在同时抢购一件或者多件商品,专用词汇就是所谓的高并发.现实中经常被大家喜闻乐见的场景,一群大妈抢购打折鸡蛋的画面一定不会陌生,如此场面让服务员大姐很无奈,赶上不要钱了. 业务特点 瞬间高并发.电脑旁边的小哥哥.小姐姐们如超市哄抢的大妈一般

SpringBoot开发案例之多任务并行+线程池处理

前言 前几篇文章着重介绍了后端服务数据库和多线程并行处理优化,并示例了改造前后的伪代码逻辑.当然了,优化是无止境的,前人栽树后人乘凉.作为我们开发者来说,既然站在了巨人的肩膀上,就要写出更加优化的程序. SpringBoot开发案例之JdbcTemplate批量操作 SpringBoot开发案例之CountDownLatch多任务并行处理 改造 理论上讲,线程越多程序可能更快,但是在实际使用中我们需要考虑到线程本身的创建以及销毁的资源消耗,以及保护操作系统本身的目的.我们通常需要将线程限制在一定

SpringBoot开发案例之整合Dubbo分布式服务

前言 在 SpringBoot 很火热的时候,阿里巴巴的分布式框架 Dubbo 不知是处于什么考虑,在停更N年之后终于进行维护了.在之前的微服务中,使用的是当当维护的版本 Dubbox,整合方式也是使用的 xml 配置方式. 改造前 之前在 SpringBoot 中使用 Dubbox是这样的.先简单记录下版本,Dubbox-2.8.4.zkclient-0.6.zookeeper-3.4.6. 项目中引入 spring-context-dubbo.xml 配置文件如下: <?xml versio

SpringBoot开发案例Nacos配置管理中心

前言 在开发过程中,通常我们会配置一些参数来实现某些功能,比如是否开启某项服务,告警邮件配置等等.一般会通过硬编码.配置文件或者数据库的形式实现. 那么问题来了,如何更加优雅的实现?欢迎来到 Nacos 的世界! Nacos 配置管理 Nacos 是阿里巴巴的开源的项目,全称 Naming Configuration Service ,专注于服务发现和配置管理领域. Nacos 致力于帮助您发现.配置和管理微服务.Nacos 提供了一组简单易用的特性集,帮助您快速实现动态服务发现.服务配置.服务

SpringBoot 开发案例之参数传递的正确姿势

前言 开发这么多年,肯定还有不少小伙伴搞不清各种类型的参数是如何传递的,很多同学都是拿来即用,复制粘贴一把撸,遇到问题还是一脸懵逼. 姿势 学习参数传递的正确姿势,先说怎么做,再说为什么,本质上还是复制粘贴一把撸,问题是你想问不想问为什么! 传递 用户登录 前端代码: var param = { "username": "admin", "password": "admin" } $.ajax({ url: "/sy

从SpringBoot构建十万博文聊聊限流特技

前言 在开发十万博客系统的的过程中,前面主要分享了爬虫.缓存穿透以及文章阅读量计数等等.爬虫的目的就是解决十万+问题:缓存穿透是为了保护后端数据库查询服务:计数服务解决了接近真实阅读数以及数据库服务的压力. 架构图 限流 就拿十万博客来说,如果存在热点文章,可能会有数十万级别的并发用户参与阅读.如果想让这些用户正常访问,无非就是加机器横向扩展各种服务,但凡事都有一个利益平衡点,有时候只需要少量的机器保证大部分用户在大部分时间可以正常访问即可. 亦或是,如果存在大量爬虫或者恶意攻击,我们必须采取一

Azure Stack技术深入浅出系列5:在Azure Stack上使用Web App PaaS服务及其背后原理窥探(开发案例)

App Service 是微软Azure的PaaS产品. 为任何平台或设备创建Web App PaaS服务和mobile App PaaS服务. 将应用与SaaS解决方案集成.与本地应用程序进行连接,以实现业务流程的自动化.在我们日常开发中,经常会使用Web App PaaS服务来承载企业的业务. 本文试图通过一个案例来分别详细说明Azure Web App业务的下列几大特点: 应用服务计划 多种语言和框架 持续集成和部署 连接数据库服务 可用性全局缩放 就在本文撰写过程中,Azure Stac

移动web开发案例实战/webApp网站开发/手机网站模板/web前端切页

网站目录.代码结构清晰,语义化.主张100%纯手打代码.拒绝冗余框架慵懒建站.拒绝散漫,执着于把细节做到完美无瑕.我们的作品,可以逐级放大欣赏. 运用当下热门HTML5+CSS3(rem)+jQuery+zepto+Iscroll+swiper+Vue等技术开发手机webApp应用 - 移动端web网站(移动端web开发/APP开发).前端开发.移动端App模版.手机网站模版.HTML5+CSS3响应式网站模版~~~ /** 我们建站作品特色 **/主张原创设计,拒绝数量.追求质量!独特的设计风

使用Jquery+EasyUI进行框架项目开发案例解说之中的一个---员工管理源代码分享

使用Jquery+EasyUI 进行框架项目开发案例解说之中的一个 员工管理源代码分享 在開始解说之前,我们先来看一下什么是Jquery EasyUI?jQuery EasyUI是一组基于jQuery的UI插件集合,而jQuery EasyUI的目标就是帮助web开发人员更轻松的打造出功能丰富而且美观的UI界面.开发人员不须要编写复杂的javascript,也不须要对css样式有深入的了解,开发人员须要了解的仅仅有一些简单的html标签.jQuery EasyUI为我们提供了大多数UI控件的使用