优雅的用工厂+策略+模板+建造者模式+开闭原则来生成网站的Sitemap地图

592 阅读11分钟

一、背景

1.1、Sitemap是什么

SiteMap就是一般网站的管理员/站长称之为网站地图的东西,包含并列出了网站中几乎所有的URL,以便搜索引擎可以更加方便快捷的抓取和发现网站的网页,从而提高搜索引擎的抓取效率。因为很多网站的连接层次比较深,蜘蛛很难抓取到,网站地图可以方便搜索引擎蜘蛛抓取网站页面,通过抓取网站页面,清晰了解网站的架构,网站地图一般存放在根目录下并命名为sitemap,为搜索引擎蜘蛛指路,增加网站重要内容页面的收录。

1.2、Sitemap.xml文件结构

对于sitemap.xml的标准约定来说,单个文件的URL数量不能超过5W,那么如果文件和URL的数量庞大,则需要拆分文件,同时增加sitemap index文件来指定,其中,sitemap.xml的索引文件的格式如下,假设我们有这样一个名称的网站:

www.abcjava.com.cn

那么我们可能会首先产生这样一份索引文件:

<?xml version="1.0" encoding="UTF-8"?>
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
  <sitemap>
    <loc>https://www.abcjava.com.cn/main_sitemap.xml</loc>
  </sitemap>
  <sitemap>
    <loc>https://www.abcjava.com.cn/sku_sitemap.xml</loc>
  </sitemap>
  <sitemap>
    <loc>https://www.abcjava.com.cn/activity_sitemap.xml</loc>
  </sitemap>
  <sitemap>
    <loc>https://www.abcjava.com.cn/blog_sitemap.xml</loc>
  </sitemap>
  <sitemap>
    <loc>https://www.abcjava.com.cn/topic_sitemap.xml</loc>
  </sitemap>
</sitemapindex>

该索引文件中指定了若干个子文件。然后每个sitemap.xml文件中存在类似如下地址:

<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" >
  <url>
    <loc>https://www.abcjava.com.cn/blog/10.html</loc>
    <lastmod>2023-09-02</lastmod>
    <changefreq>daily</changefreq>
    <priority>0.9</priority>
  </url>
  <url>
    <loc>https://www.abcjava.com.cn/blog/11.html</loc>
    <lastmod>2023-09-01</lastmod>
    <changefreq>daily</changefreq>
    <priority>0.9</priority>
  </url>
</urlset>

通过为网站定义这样一份文件,可以让网站的内容更多的被搜索蜘蛛爬取,从而提高公司的网站/商城内容的业务的访问量等。

1.3、Sitemap.xml如何生成

通常一个网站的内容都是存储在数据库中的,而这就需要我们通过定时任务,定期的去查询数据库中的数据,将需要进行推广的页面生成URL的sitemap.xml文件,那么如果没有一个好的设计,我们可能得代码思路就是如下:

public void test(){
    //1.查询数据库中的商品页面数据、文章页面数据、营销活动页面数据
    //2.循环遍历List对象,通过dom4j或者手动拼接xml字符串的方式
    //3.构造URL的xml标签数据,生成并写入到sitemap.xml文件中
}

而随着软件的逐步迭代和开发,可能代码会越来越难以维护,代码的行数和耦合度越来越高,作为一名对自己有要求的Java工程师,如何把这种业务做的更好也是我们需要持续思考的东西,最近接手的项目中,自己遇到了这种情况,于是采用设计模式封装了一种可以较好可扩展的代码设计,对于新业务的增加,只需要增加内容即可。

二、正文开始

2.1、sitemap生成枚举类

针对本文开始的几个文件,我们定义一个枚举,去维护:

main_sitemap.xml、sku_sitemap.xml、activity_sitemap.xml、blog_sitema.xml文件的基本信息。

代码如下:

public enum WebSiteMapBizEnums {
    
    /**
     * main_sitemap
     */
    MAIN_SITEMAP(1, "main_sitemap.xml", "main"),
    
    /**
     * sku_sitemap
     */
    SKU_SITEMAP(2, "sku_sitemap.xml", "sku"),
    
    /**
     * activity_sitemap
     */
    ACTIVITY_SITEMAP(3, "activity_sitemap.xml", "activity"),
    
    /**
     * blog_sitemap
     */
    BLOG_SITEMAP(4, "blog_sitemap.xml", "blog");
    
    /**
     * 类型标识
     */
    private final Integer key;
    
    /**
     * 文件名称
     */
    private final String fileName;
    
    /**
     * 分组
     */
    private String group;
    
    WebSiteMapBizEnums(Integer key, String fileName, String group) {
        this.key = key;
        this.fileName = fileName;
        this.group = group;
    }
    
    public Integer getKey() {
        return key;
    }
    
    public String getFileName() {
        return fileName;
    }
    
    public String getGroup() {
        return group;
    }
}

2.2、定义一个配置类

该属性配置类帮助我们从Nacos等配置中获取生成文件的存储目录与域名信息,代码如下:

@ConfigurationProperties("sitemap")
@Data
public class WebSitemapProperties {
    
    /**
     * 生成文件的目录
     */
    private String fileDir;
    
    /**
     * 生成文件的域名
     */
    private String domain;

}

@Configuration
@EnableConfigurationProperties({WebSitemapProperties.class})
public class WebSitemapConfiguration {

}

2.3、引入策略设计模式

下面这句话,大家应该很熟悉:

策略设计模式(Strategy Pattern)是一种定义一系列算法的方法,从概念上来看,所有这些算法完成的都是相同的工作,只是实现不同,它可以让算法的变化独立于使用算法的客户端。

我们看一下针对这个需求的类设计图:

我们定义了一个WebSiteMapLocationStrategy的策略接口,并定义了3个具体策略的实现类,然后定义一个类型的方法(通常在策略模式设计中,会有一个方法标识自己是谁)、核心执行策略的方法。

接口类的代码如下所示:

public interface WebSiteMapLocationStrategy {
    
    /**
     * 获得业务类型
     */
    public WebSiteMapBizEnums getEnumType();
    
    /**
     * 执行构造方法
     */
    public void executeBuild(WebSitemapProperties sitemapProperties);
    
}

其中某一个类的实现如下所示:

public class MainLocationStrategy implements WebSiteMapLocationStrategy {
    
    @Override
    public WebSiteMapBizEnums getEnumType() {
        return WebSiteMapBizEnums.MAIN_SITEMAP;
    }
    
    @Override
    public void executeBuild(WebSitemapProperties sitemapProperties) {
        // 1.查询数据库中的数据
        
        // 2.循环遍历数据库中的每一个数据进行封装构造URL数据
        
        // 3.执行写入操作,封装到对应的sitemap.xml文件中
    }
}

这样简单的一个类我们就写完了,然后我们接下来用一种方法去驱动策略的执行,去驱动策略的选择,最笨的方法伪代码如下:

new 策略1

new 策略2

new 策略3

然后将策略进行IF/ELSE判断或者放到某个集合容器中。

那么在真实的工作中,都有哪些办法去驱动呢,这里我总结了几种方法:

(1)、手动注册将策略对象到Map容器中或Function对象中。

(2)、通过Java SPI或Spring Boot的spring.factories这种技术将策略对象定义声明在文件里面,然后通过ServiceLoader注册到Map容器中。

(3)、从Spring IOC容器中获取具体的策略对象,注册到Map容器中。

(4)、通过反射机制,维护策略的Class对象,需要的时候通过反射创建对象的方式去处理。

这里分享的第2种设计也是值得大家学习的,第2种的驱动方式,可以让我们自己的系统提供一种插件化的能力与服务,知名的开源软件Nacos中。大量的用到了这种思路设计,如果以后别人问你,如何设计一种插件化的方案,请你告诉它这种设计思路与方案。

这里我用了第3种方式,默认在每个具体的策略实现对象上,增加@Service注解,让其加入到Spring IOC的容器中,然后我们定义一个工厂类,去加载获取、维护这些对象。

2.4、引入工厂设计模式+开闭原则

开闭原则的定义:

对扩展开放,对修改关闭。当应用的需求改变时,可以不通过修改源代码的前提下,通过扩展方式来实现。

工厂模式的定义:

这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。工厂模式提供了一种将对象的实例化过程封装在工厂类中的方式。通过使用工厂模式,可以将对象的创建与使用代码分离,提供一种统一的接口来创建不同类型的对象。

工厂模式的代码,如下所示:

public class WebSitemapFactory {
    
    private static Map<Integer,WebSiteMapLocationStrategy> registerMap = new HashMap<Integer, WebSiteMapLocationStrategy>();
    
    /**
     * 加载数据
     */
    public static void initLoad(){
        Map<String, WebSiteMapLocationStrategy> beanMap = SpringContextUtil.getContext().getBeansOfType(
                WebSiteMapLocationStrategy.class);
        Set<String> keySet = beanMap.keySet();
        for (String key : keySet) {
            WebSiteMapLocationStrategy siteMapBuilder = beanMap.get(key);
            registerMap.put(siteMapBuilder.getEnumType().getKey(), siteMapBuilder);
        }
    }
    
    /**
     * 根据类型取出来执行的策略
     * @param key 标识区分
     * @return 具体策略对象
     */
    public static WebSiteMapLocationStrategy getSiteMapLocationStrategy(Integer key) {
        return registerMap.get(key);
    }
    
}

针对这个工厂类,其实也可以给他设计成单例的模式,这是暂时没有处理。知名的开源软件Nacos中大量的使用XxxxManager这种单例类,从SPI中取出来策略对象,然后完成插件化的设计和业务处理。

类中的initLoad方法是一种使用了Spring的代码开发的,一种典型的满足开闭原则的设计,对于未来新增的业务类型,整体业务流程功能的代码无需改动,只需要新开发一个实现类即可。业务会自动加载并读取到。当日也可以采用SPI进行处理。

2.5、真实策略类的sitemap业务处理+第三方建造者模式

接下来我们简单写一下真实的业务sitemap生成的逻辑,这里使用了开源的sitemap生成组件包,非常的小巧,其内部已经完成了sitemap的基本功能,非常符合面向对象思想的设计,组件包的名字是:sitemapgen4j。

我们可以直接使用它,项目的pom中引入如下依赖:

  <dependency>
            <groupId>com.github.dfabulich</groupId>
            <artifactId>sitemapgen4j</artifactId>
            <version>1.1.2</version>
        </dependency>

然后我们针对系统中的文章数据,进行sitemap.xml的内容的生成与构建,样例代码如下所示:

public class ArticleLocationStrategy implements WebSiteMapLocationStrategy {
    
    @Override
    public WebSiteMapBizEnums getEnumType() {
        return WebSiteMapBizEnums.ARTICLE_SITEMAP;
    }
    
    @Override
    public void executeBuild(WebSitemapProperties sitemapProperties) {
        try{
            // 1.查询数据库中的数据
            List<Object> dataList = this.doQueryDatabaseDataList();
    
            // 2.循环遍历数据库中的每一个数据进行封装构造URL数据
            WebSitemapGenerator generator = this.buildSitemap(sitemapProperties, dataList);
    
            // 3.执行写入操作,封装到对应的sitemap.xml文件中
            this.writeData(generator);
        }catch (Exception e){
            e.printStackTrace();
        }
    }
    
    /**
     * 步骤1:查询数据库中的数据,这里为了演示Demo定义假数据
     */
    private List<Object> doQueryDatabaseDataList() {
        return new ArrayList<Object>();
    }
    
    /**
     * 构造URL信息
     */
    private WebSitemapGenerator buildSitemap(WebSitemapProperties sitemapProperties, List<Object> dataList)
            throws Exception {
        //定义一个WebSitemapGenerator对象 
        W3CDateFormat dateFormat = new W3CDateFormat(W3CDateFormat.Pattern.DAY);
        // 使用建造者设计模式进行构造对象,读取配置中的数据目录和域名信息
        // 未来可以放到模板中
        WebSitemapGenerator websiteMapGen = WebSitemapGenerator.builder(sitemapProperties.getDomain(), 
                        new File(sitemapProperties.getFileDir()))
                .dateFormat(dateFormat).fileNamePrefix(getEnumType().getFileName()).build();
        
        //循环业务数据
        String prefix = sitemapProperties.getDomain() + "article/";
        for (Object article : dataList) {
            //生成sitemapUrl
            String sitemapUrl = prefix + "文章的业务ID.html";
            WebSitemapUrl.Options options = new WebSitemapUrl.Options(sitemapUrl);
            options.lastMod(new Date());
            WebSitemapUrl url = options.changeFreq(ChangeFreq.DAILY).priority(0.9).build();
            websiteMapGen.addUrl(url);
        }
        return websiteMapGen;
    }
    
    /**
     * 写数据到文件中
     * @param generator
     */
    private void writeData(WebSitemapGenerator generator) {
        List<File> fileList = generator.write();
    }
    
}

好,到了这里,有没有发现如果当前业务存在多种,每种的sitemap生成的业务的处理流程都非常相似,都存在部分重复的代码设计,这个时候怎么办的,针对每个新增的业务场景,都需要开发一套类似如下的流程设计:

2.6、引入模板方法设计模式统一步骤流程

模板模式(Template Pattern)中,一个抽象类公开定义了执行它的方法的方式/模板。它的子类可以按需要重写方法实现,但调用将以抽象类中定义的方式进行。这种类型的设计模式属于行为型模式。

**意图:**定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。

**主要解决:**一些方法通用,却在每一个子类都重新写了这一方法。

**何时使用:**有一些通用的方法。

**如何解决:**将这些通用算法抽象出来。

**关键代码:**在抽象类实现,其他步骤在子类实现。

经过上面的分析可以看到,如果当前存在多个sitemap的生成处理任务,为了提高扩展性,减少代码的重复,我们接下来可以通过对步骤流程的抽象,使用模板方法,抽取业务共性,让变化的数据处理延迟到子类中进行实现。

我们定义一个抽象类,提供一个子类必须要实现的抽象方法,然后增加了前置业务和后置业务的扩展设计,代码如下:

public abstract class AbstractLocationStrategy implements WebSiteMapLocationStrategy {
    
    @Override
    public void executeBuild(WebSitemapProperties sitemapProperties){
        try{
            //1.前置业务
            this.processBefore();
            //2.生成数据
            WebSitemapGenerator generator = this.doWebSitemapGeneratorBuild(sitemapProperties);
            //3.写数据
            this.writeData(generator);
            //4.后置业务
            this.processAfter();
        }catch (Exception e){
            e.printStackTrace();
        }
    }
    
    private void processAfter() {
    }
    
    /**
     * 子类自己处理各自的业务
     */
    protected abstract WebSitemapGenerator doWebSitemapGeneratorBuild(WebSitemapProperties sitemapProperties)
            throws Exception;
    
    /**
     * 写数据到文件中
     */
    private void writeData(WebSitemapGenerator generator) {
        List<File> fileList = generator.write();
    }
    
    protected void processBefore() {}
    
}

我们定义了一个doWebSitemapGeneratorBuild方法来让不同的业务子类进行实现,然后子类只需要关注自己的责任:查询属于自己的业务数据,构造sitemap的url对象交给父类去执行,父类去管理整个流程的执行步骤。

现在的子类变为如下代码:

public class ArticleLocationStrategy extends AbstractLocationStrategy {
    
    @Override
    public WebSiteMapBizEnums getEnumType() {
        return WebSiteMapBizEnums.Article_SITEMAP;
    }
    
    @Override
    protected WebSitemapGenerator doWebSitemapGeneratorBuild(WebSitemapProperties sitemapProperties) throws Exception {
        
        // 1.查询数据库中的数据
        List<Object> dataList = this.doQueryDatabaseDataList();
        
        // 2.循环遍历数据库中的每一个数据进行封装构造URL数据
        WebSitemapGenerator generator = this.buildSitemap(sitemapProperties, dataList);
        
        return generator;
        
    }
    
    /**
     * 步骤1:查询数据库中的数据
     */
    private List<Object> doQueryDatabaseDataList() {
        return new ArrayList<Object>();
    }
    
    /**
     * 构造URL信息
     */
    private WebSitemapGenerator buildSitemap(WebSitemapProperties sitemapProperties, List<Object> dataList)
            throws Exception {
        //定义一个WebSitemapGenerator对象
        W3CDateFormat dateFormat = new W3CDateFormat(W3CDateFormat.Pattern.DAY);
        // 使用建造者设计模式进行构造对象,未来可以放到模板中
        WebSitemapGenerator websiteMapGen = WebSitemapGenerator.builder(sitemapProperties.getDomain(),
                        new File(sitemapProperties.getFileDir())).dateFormat(dateFormat)
                .fileNamePrefix(getEnumType().getFileName()).build();
        
        //循环业务数据
        String prefix = sitemapProperties.getDomain() + "article/";
        for (Object article : dataList) {
            //生成sitemapUrl
            String sitemapUrl = prefix + "文章的业务ID.html";
            WebSitemapUrl.Options options = new WebSitemapUrl.Options(sitemapUrl);
            options.lastMod(new Date());
            WebSitemapUrl url = options.changeFreq(ChangeFreq.DAILY).priority(0.9).build();
            websiteMapGen.addUrl(url);
        }
        return websiteMapGen;
    }
    
    
}

2.7、客户端使用者调用

public class TestBiz {
    
    public static void main(String[] args) {
        //1.加载数据,真实情况下可以在项目启动成功后执行加载的方法
        WebSitemapFactory.initLoad();
        //2.为了演示 真实的需要注入
        WebSitemapProperties webSitemapProperties = new WebSitemapProperties();
        //3.循环处理业务
        for (WebSiteMapBizEnums biz : WebSiteMapBizEnums.values()) {
            //根据当前的sitemap业务枚举类 获得具体执行的构造的策略
            WebSiteMapLocationStrategy strategy = WebSitemapFactory.getSiteMapLocationStrategy(biz.getKey());
            if (strategy != null) {
                strategy.executeBuild(webSitemapProperties);
            }
        }
    }
    
}

三、总结

本文通过对日常的一个需求,通过多种设计模式实现了一种可扩展的代码设计,对于这种代码设计,真实环境中是非常常见的,也是每个Java工程师必须要会的技能。这里的建造者设计模式是间接的使用,日常的工作中,也可以仿照其设计,完成日常一些复杂对象的构建。当出去面试的时候,谈到项目中是否使用到了设计模式,不仅可说直接用了哪些,还可以说一下间接用了哪些。

如果本篇文章,对你有帮助,欢迎收藏、关注、分享、评论。