SpringBoot 系列 - 自己写starter

原文地址: https://www.xncoding.com/2017/07/22/spring/sb-starter.html

前言:

Spring Boot由众多Starter组成,随着版本的推移Starter家族成员也与日俱增。在传统Maven项目中通常将一些层、组件拆分为模块来管理, 以便相互依赖复用,在Spring Boot项目中我们则可以创建自定义Spring Boot Starter来达成该目的。

可以认为starter是一种服务——使得使用某个功能的开发者不需要关注各种依赖库的处理,不需要具体的配置信息, 由Spring Boot自动通过classpath路径下的类发现需要的Bean,并织入相应的Bean。举个栗子,spring-boot-starter-jdbc这个starter的存在, 使得我们只需要在BookPubApplication下用@Autowired引入DataSource的bean就可以,Spring Boot会自动创建DataSource的实例。

本篇将通过一个简单的例子来演示如何编写自己的starter。

这个例子就是自己封转的支付功能的 easy-pay-spring-boot-starter

当然了官方的名称 spring-boot-stater-{name} ,然后自己定义的stater 名称 {name}-spring-boot-stater

一、添加Maven 依赖

第一步当然是创建一个 maven 工程,添加SpringBoot 的自动依赖:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">

    <groupId>com.zuoyan.spring.boot</groupId>
    <modelVersion>4.0.0</modelVersion>
    <artifactId>easy-pay-spring-boot-starter</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>jar</packaging>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.5.9.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-autoconfigure</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>

        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>servlet-api</artifactId>
            <version>2.5</version>
            <scope>provided</scope>
        </dependency>

    </dependencies>

</project>

上面的servlet-api 是由于项目中有需要用到的HttpRequest 所以需要使用这个依赖,剩下的就是自动配置需要的依赖

注意其中 spring-boot-configuration-processor 的作用是编译时生成spring-configuration-metadata.json, 此文件主要给IDE使用,用于提示使用。如在intellij idea中,当配置此jar相关配置属性在application.yml, 你可以用ctlr+鼠标左键,IDE会跳转到你配置此属性的类中。

这里说下artifactId的命名问题,Spring 官方 Starter通常命名为spring-boot-starter-{name}spring-boot-starter-web

Spring官方建议非官方Starter命名应遵循{name}-spring-boot-starter的格式。

二、编写属性配置类

? 也就是通常我们在 SpringBoot 中配置的application.properties 或者是 application.yml 文件中配置的属性,然后通过注入到项目中给我们使用

package com.zuoyan.springboot.easypay.bean;

import org.springframework.boot.context.properties.ConfigurationProperties;

/**
 *
 * 易支付的配置文件
 *  通过application.properties  配置文件配置
 * 1.partner:商户ID
 * 2.key :分配给商户的密钥
 *
 * @author 左岩
 *
 */

@ConfigurationProperties(value = "spring.easy.pay")
public class Alipay_config {

    //商户ID
    private String partner = "your ID";
    //商户Key
    private String key = "your key";
    //签名方式不用更改
    private String sign_type = "MD5";
    //字符编码格式,目前支持GBK或 utf-8
    private String input_charset = "utf-8";
    //访问模式,根据自己的服务器是否支持ssl访问,若支持请选择https;若不支持请选择http
    private String transport = "http";
    //支付API地址
    private String apiurl = "http://pay.hackwl.cn/";
    //支付成功返回通知的url
    private String notify_url = "http://www.xxxxx.com/notifyurl";
    //支付成功后需要跳转的页面地址
    private String return_url = "http://www.xxxxxx.com/returnurl";

    public String getPartner() {
        return partner;
    }

    public void setPartner(String partner) {
        this.partner = partner;
    }

    public String getKey() {
        return key;
    }

    public void setKey(String key) {
        this.key = key;
    }

    public String getSign_type() {
        return sign_type;
    }

    public void setSign_type(String sign_type) {
        this.sign_type = sign_type;
    }

    public String getInput_charset() {
        return input_charset;
    }

    public void setInput_charset(String input_charset) {
        this.input_charset = input_charset;
    }

    public String getTransport() {
        return transport;
    }

    public void setTransport(String transport) {
        this.transport = transport;
    }

    public String getApiurl() {
        return apiurl;
    }

    public void setApiurl(String apiurl) {
        this.apiurl = apiurl;
    }

    public String getNotify_url() {
        return notify_url;
    }

    public void setNotify_url(String notify_url) {
        this.notify_url = notify_url;
    }

    public String getReturn_url() {
        return return_url;
    }

    public void setReturn_url(String return_url) {
        this.return_url = return_url;
    }

}

大家就不要吐槽我的命名了,主要是当初写这个功能类的时候 ,是根据PHP改的,还有就是当时刚入门Java 还没有命名规范的良好习惯,然后有太多引用了,也懒得替换了,就这样用吧。 这个里面主要配置的就是一些支付需要用到的参数 比如: 商户的key 、商户的security key 、 支付成功跳转的url 、支付成功通知的url

编写业务类(个人理解通俗来说,就是哪里需要这个属性配置类的地方)

package com.zuoyan.springboot.easypay.bean;

import com.zuoyan.springboot.easypay.function.EpayCoreFunction;
import com.zuoyan.springboot.easypay.function.EpayMD5Function;

import java.security.NoSuchAlgorithmException;
import java.util.HashMap;
import java.util.TreeMap;

/**
 * 类名:EpaySubmit
 * 功能:易支付请求接口提交类
 * 详细:构造易支付接口表单HTML文本,获取远程HTTP数据
 *
 * @author 左岩
 *
 */
public class EpaySubmit {

    private Alipay_config alipay_config;
    private String alipay_gateway_new;

    //设置EpaySubmit 配置
    public void setAlipay_config(Alipay_config alipay_config) {
        this.alipay_config = alipay_config;
    }

    public EpaySubmit(Alipay_config alipay_config) {
        this.alipay_config = alipay_config;
        this.alipay_gateway_new = alipay_config.getApiurl()+"submit.php?";
    }

    public TreeMap<String,String> buildRequestPara(HashMap<String, String> parameter) throws Exception
    {
        //除去待签名参数数组中的空值和签名参数
        HashMap<String, String> para_filter = EpayCoreFunction.paraFilter(parameter);
        //对签名参数数组排序
        TreeMap<String, String> para_sort = EpayCoreFunction.argSort(para_filter);
        //生成签名结果
        String mysign = this.buildRequestMysign(para_sort);
        //签名结果与签名方式加入请求提交参数数组中
        para_sort.put("sign", mysign);
        para_sort.put("sign_type",alipay_config.getSign_type().toUpperCase());
        return para_sort;

    }

    public String buildRequestMysign(TreeMap<String, String> para_sort) throws Exception
    {
        //把数组所有元素,按照“参数=参数值”的模式用“&”字符拼接成字符串
        String prestr = EpayCoreFunction.createLinkstring(para_sort);
        String mysign = EpayMD5Function.md5Sign(prestr.trim(),alipay_config.getKey());
        return mysign;

    }

    /**
     * 建立请求,以表单Html的形式构造
     * @param parameter  请求参数数组
     * @return
     * @throws NoSuchAlgorithmException
     */
    public String buildRequestForm(HashMap<String, String> parameter) throws Exception
    {
        TreeMap<String,String> para = this.buildRequestPara(parameter);
        String method = "";
        String sHtml = "<form id='alipaysubmit' name='alipaysubmit' action='"+this.alipay_gateway_new+"_input_charset="+this.alipay_config.getInput_charset().toLowerCase().trim()+"' method='"+method+"'>";
        //遍历处理过后的 Parameter,拼接字符串
        for(String key : para.keySet())
        {
             sHtml+= "<input type='hidden' name='"+key+"' value='"+para.get(key)+"'/>";
        }
        String button_name="页面正在跳转,请稍后!";
        //submit按钮请不要含有name属性
        sHtml+="<input type='submit' value='"+button_name+"'></form>";
        //设置js事件然后自动提交
        sHtml+="<script>document.forms['alipaysubmit'].submit();</script>";
        //将拼装好的js返回
        return sHtml;
    }

}

支付成功通知返回类:

package com.zuoyan.springboot.easypay.function;

import com.zuoyan.springboot.easypay.bean.Alipay_config;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.HashMap;
import java.util.Map;
import java.util.TreeMap;
import java.util.regex.Pattern;

/**
 *
 * 类名:EpayNotify 功能:彩虹易支付通知处理类 详细:处理易支付接口通知返回
 *
 * @author 左岩
 *
 */
public class AlipayNotify {

    private Alipay_config alipay_config;
    private String http_verify_url;

    public void setAlipay_config(Alipay_config alipay_config) {
        this.alipay_config = alipay_config;
    }

    public AlipayNotify(Alipay_config alipay_config) {
        this.alipay_config = alipay_config;
        this.http_verify_url = alipay_config.getApiurl() + "api.php?";
    }

    public AlipayNotify(){}

    /**
     * 针对notify_url验证消息是否是支付宝发出的合法消息
     *
     * @param request
     * @param response
     * @return 验证结果
     * @throws Exception
     */
    public boolean verifyNotify(HttpServletRequest request, HttpServletResponse response) throws Exception {
        return veryfy(request, response);

    }

    /**
     * 针对return_url 验证消息是否是支付宝发出的和合法消息
     *
     * @param request
     * @param response
     * @return 验证结果
     * @throws Exception
     */
    public boolean verifyReturn(HttpServletRequest request, HttpServletResponse response) throws Exception {

        return veryfy(request, response);
    }

    /**
     * 从Request请求参数中 获取请求参数的 map
     *
     * @param request
     * @return
     * @throws Exception
     */
    public static HashMap<String, String> getGetMap(HttpServletRequest request) throws Exception {

        Map<String, String[]> requestMap = request.getParameterMap();
        HashMap<String, String> returnMap = new HashMap<String, String>();

        for (String key : requestMap.keySet()) {
            returnMap.put(key, new String(request.getParameter(key).getBytes("ISO8859-1"), "UTF-8"));
            //测试获取的字符
        }

        return returnMap;

    }

    /**
     * 功能:获取返回时的签名验证结果
     *
     * @param para_temp 通知返回来的参数数组
     * @param sign      返回的签名结果
     * @return 签名验证结果
     * @throws Exception
     */
    public boolean getSignVeryfy(HashMap<String, String> para_temp, String sign) throws Exception {
        // 出去签名数组中参数数组中的空值和签名参数
        HashMap<String, String> paraFilter = EpayCoreFunction.paraFilter(para_temp);
        // 对待签名参数数组排序
        TreeMap<String, String> para_sort = EpayCoreFunction.argSort(paraFilter);
        // 把数组所有元素,按照 “参数=参数值”的模式用"&"字符拼接成字符串
        String prestr = EpayCoreFunction.createLinkstring(para_sort);

        System.out.println("测试创建的字符串为:" + prestr);

        boolean isSgin = false;

        isSgin = EpayMD5Function.md5Verify(prestr, sign, this.alipay_config.getKey());

        return isSgin;

    }

    /**
     * 功能描述: <br>
     * 〈抽取公用方法〉
     * @Param: No such property: code for class: Script1
     * @Return: boolean
     * @Author: Administrator
     * @Date: 2019/10/24 15:34
     *
     */
    public boolean veryfy(HttpServletRequest request, HttpServletResponse response) {

        try {
            // 判断GET来的数组是否为空
            if (request.getParameterMap().isEmpty()) {
                return false;
            } else {
                // 获取Request请求中所带的参数,并将这些个参数封装成一个map
                HashMap<String, String> para_temp = getGetMap(request);
                // 这个获取Map,也就是Request返回的签名参数
                String sign = para_temp.get("sign");
                // 生成签名结果
                boolean isSign = getSignVeryfy(para_temp, sign);
                // 获取支付宝远程服务器ATN结果 (验证是否是支付宝发来的消息)
                String responseTxt = "true";
                // 验证
                // responseTxt的结果不是true,与服务器的设置问题、合作身份者ID、notify_id 一分钟失效有关
                // isSign 的结果不是true,与安全校验码、请求时的参数格式(如:带自定义参数等)、编码格式有关

                // Java中的正则匹配
                String regex = ".*(?i)true$";
                if (Pattern.matches(regex, responseTxt) && isSign) {
                    return true;
                } else {
                    return false;
                }

            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        //后来添加的 可能会成为问题代码
        return  false;
    }

}

还有上面的 System.out.println() ,也希望大家轻点吐槽,这个也是当时不习惯使用log ,现在也是不太习惯,但是还好,当时感觉把值打印出来 调试多方便,后来就没改

三、重中之重来了: 自动配置类

package com.zuoyan.springboot.easypay;

import com.zuoyan.springboot.easypay.bean.Alipay_config;
import com.zuoyan.springboot.easypay.bean.EpaySubmit;
import com.zuoyan.springboot.easypay.function.AlipayNotify;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * @ProjectName: EasyPayBootStarter
 * @Package: PACKAGE_NAME
 * @ClassName: EasyPayAutoConfiguration
 * @Author: ZuoYanCoder
 * @Description: 声明Starter自动配置类
 * @Date: 2019/10/24 15:00
 * @Version: 1.0
 */

@Configuration
@ConditionalOnClass({AlipayNotify.class,EpaySubmit.class})
@EnableConfigurationProperties(Alipay_config.class)
public class EasyPayAutoConfiguration {

    private final Alipay_config alipay_config;

    @Autowired
    public EasyPayAutoConfiguration(Alipay_config alipay_config)
    {
        this.alipay_config = alipay_config;
    }

    @Bean
    //当容器中没有这个Bean的时候才创建这个Bean
    @ConditionalOnMissingBean(AlipayNotify.class)
    public AlipayNotify alipayNotify(){
        AlipayNotify alipayNotify = new AlipayNotify(alipay_config);
        return alipayNotify;
    }

    @Bean
    @ConditionalOnMissingBean(EpaySubmit.class)
    public EpaySubmit epaySubmit(){
        EpaySubmit epaySubmit = new EpaySubmit(alipay_config);
        return epaySubmit;
    }

}

这个就是将application.properties 配置文件中配置的属性 获取出来,然后配置另外两个配置类 EpaySubmit、 AlipayNotify

解释下用到的几个和Starter相关的注解:

1. @ConditionalOnClass,当classpath下发现该类的情况下进行自动配置。
2. @ConditionalOnMissingBean,当Spring Context中不存在该Bean时。
3. @ConditionalOnProperty(prefix = "example.service",value = "enabled",havingValue = "true"),当配置文件中example.service.enabled=true时。

四、添加spring.factories

最后一步,在resources/META-INF/下创建spring.factories文件,内容供参考下面:

# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.zuoyan.springboot.easypay.EasyPayAutoConfiguration

如果有多个自动配置类,用逗号分隔换行即可。

OK,完事,运行 mvn:install 打包安装,一个Spring Boot Starter便开发完成了。

五、总结

总结下Starter的工作原理:

  1. Spring Boot在启动时扫描项目所依赖的JAR包,寻找包含spring.factories文件的JAR包
  2. 根据spring.factories配置加载AutoConfigure类
  3. 根据 @Conditional 注解的条件,进行自动配置并将Bean注入Spring Context

原文地址:https://www.cnblogs.com/kangxinxin/p/11881191.html

时间: 2024-11-08 14:55:54

SpringBoot 系列 - 自己写starter的相关文章

SpringBoot系列之自定义starter实践教程

Springboot是有提供了很多starter的,starter翻译过来可以理解为场景启动器,所谓场景启动器配置了自动配置等等对应业务模块的一个工程,有需要时候直接引入项目就可以,比如需要使用rabbitMQ,直接引入spring-boot-starter-activemq既可,详细介绍可以参考Springboot官方文档关于starters的介绍 查看官方文档,可以找到如下的命名规范: 其意思是SpringBoot官方的starter命名要定义为spring-boot-starter-*,自

SpringBoot系列——利用系统环境变量与配置文件的分支选择实现“智能部署”

前言 通过之前的博客:SpringBoot系列——jar包与war包的部署,我们已经知道了如果实现项目的简单部署,但项目部署的时候最烦的是什么?修改成发布环境对应的配置!数据库连接地址.Eureka注册中心地址.Redis服务地址等,部署环境不一样,打包的时候就要改成对应的配置:常用的环境有本地开发环境dev,本地测试环境dev-test,生产测试环境prod-test,生产环境prod: 开发的时候我们用dev,项目直接运行,不用改配置:发布本地测试环境的时候,打包之前我们要先改成对应配置:上

SpringBoot系列教程web篇之404、500异常页面配置

接着前面几篇web处理请求的博文,本文将说明,当出现异常的场景下,如404请求url不存在,,403无权,500服务器异常时,我们可以如何处理 原文友链: SpringBoot系列教程web篇之404.500异常页面配置 I. 环境搭建 首先得搭建一个web应用才有可能继续后续的测试,借助SpringBoot搭建一个web应用属于比较简单的活; 创建一个maven项目,pom文件如下 <parent> <groupId>org.springframework.boot</gr

SpringBoot系列教程web篇之重定向

原文地址: SpringBoot系列教程web篇之重定向 前面介绍了spring web篇数据返回的几种常用姿势,当我们在相应一个http请求时,除了直接返回数据之外,还有另一种常见的case -> 重定向: 比如我们在逛淘宝,没有登录就点击购买时,会跳转到登录界面,这其实就是一个重定向.本文主要介绍对于后端而言,可以怎样支持302重定向 I. 环境搭建 首先得搭建一个web应用才有可能继续后续的测试,借助SpringBoot搭建一个web应用属于比较简单的活; 创建一个maven项目,pom文

利用SpringBoot+Logback手写一个简单的链路追踪

目录 一.实现原理 二.代码实战 三.测试 最近线上排查问题时候,发现请求太多导致日志错综复杂,没办法把用户在一次或多次请求的日志关联在一起,所以就利用SpringBoot+Logback手写了一个简单的链路追踪,下面详细介绍下. 一.实现原理 Spring Boot默认使用LogBack日志系统,并且已经引入了相关的jar包,所以我们无需任何配置便可以使用LogBack打印日志. MDC(Mapped Diagnostic Context,映射调试上下文)是log4j和logback提供的一种

SpringBoot系列之@Value和@ConfigurationProperties

继上一篇博客SpringBoot系列之YAML配置用法之后,再写一篇@Value.@ConfigurationProperties的对比博客 这两个主键都是可以获取配置文件属性的,不过是有比较大的区别的,所以本博客做一下对比,ok,继续拿上一篇博客的例子来实验 ## 测试ConfigurationProperties user: userName: root isAdmin: true regTime: 2019/11/01 isOnline: 1 maps: {k1 : v1,k2: v2}

SpringBoot系列之集成Thymeleaf用法手册

目录 1.模板引擎 2.Thymeleaf简介 2.1).Thymeleaf定义 2.2).适用模板 3.重要知识点 3.1).th:text和th:utext 3.2).标准表达式 3.3).Thymeleaf遍历 3.4).公共模块抽取 3.5).行内写法介绍 3.6).Thymeleaf语法规则 4.SpringBoot集成 4.1).Springboot集成Thymeleaf简介 4.2).Thymeleaf自动配置源码简单分析 SpringBoot系列之Thymeleaf语法简单介绍

SpringBoot系列之集成Druid配置数据源监控

继上一篇博客SpringBoot系列之JDBC数据访问之后,本博客再介绍数据库连接池框架Druid的使用 实验环境准备: Maven IntelliJ IDEA 先新建一个Springboot Initializer项目,详情参考SpringBoot系列之快速创建Initializer项目,注意引入必须的JDBC,web依赖等等,因为Druid默认没提供,所以去https://mvnrepository.com/artifact/com.alibaba/druid获取配置信息,项目创建之后,po

SpringBoot系列之集成Dubbo的方式

本博客介绍Springboot框架集成Dubbo实现微服务的3种常用方式,对于Dubbo知识不是很熟悉的,请先学习我上一篇博客:SpringBoot系列之集成Dubbo实现微服务教程,本博客只是对上篇博客的补充,上篇博客已经介绍过的就不重复介绍 还是使用上篇博客的例子,业务场景: 某个电商系统,订单服务需要调用用户服务获取某个用户的所有地址: 我们现在 需要创建两个服务模块进行测试 模块 功能 订单服务模块 创建订单等 用户服务模块 查询用户地址等 测试预期结果: 订单服务web模块在A服务器,