原文地址: 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

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

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

    SpringBoot系列之自定义starter实践教程 Springboot是有提供了很多starter的,starter翻译过来可以理解为场景启动器,所谓场景启动器配置了自动配置等等对应业务模块的一 ...

  2. Springboot 系列(十五)如何编写自己的 Springboot starter

    1. 前言 Springboot 中的自动配置确实方便,减少了我们开发上的复杂性,那么自动配置原理是什么呢?之前我也写过了一篇文章进行了分析. Springboot 系列(三)Spring Boot ...

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

    前言 通过之前的博客:SpringBoot系列——jar包与war包的部署,我们已经知道了如果实现项目的简单部署,但项目部署的时候最烦的是什么?修改成发布环境对应的配置!数据库连接地址.Eureka注 ...

  4. Springboot 系列(十二)使用 Mybatis 集成 pagehelper 分页插件和 mapper 插件

    前言 在 Springboot 系列文章第十一篇里(使用 Mybatis(自动生成插件) 访问数据库),实验了 Springboot 结合 Mybatis 以及 Mybatis-generator 生 ...

  5. Springboot 系列(九)使用 Spring JDBC 和 Druid 数据源监控

    前言 作为一名 Java 开发者,相信对 JDBC(Java Data Base Connectivity)是不会陌生的,JDBC作为 Java 基础内容,它提供了一种基准,据此可以构建更高级的工具和 ...

  6. SpringBoot系列——Spring-Data-JPA(究极进化版) 自动生成单表基础增、删、改、查接口

    前言 我们在之前的实现了springboot与data-jpa的增.删.改.查简单使用(请戳:SpringBoot系列——Spring-Data-JPA),并实现了升级版(请戳:SpringBoot系 ...

  7. SpringBoot系列——Spring-Data-JPA

    前言 jpa是ORM映射框架,更多详情,请戳:apring-data-jpa官网:http://spring.io/projects/spring-data-jpa,以及一篇优秀的博客:https:/ ...

  8. SpringBoot系列——Spring-Data-JPA(升级版)

    前言 在上篇博客中:SpringBoot系列——Spring-Data-JPA:https://www.cnblogs.com/huanzi-qch/p/9970545.html,我们实现了单表的基础 ...

  9. SpringBoot系列: SpringBoot Web项目中使用Shiro

    注意点有:1. 不要启用 spring-boot-devtools, 如果启用 devtools 后, 不管是热启动还是手工重启, devtools总是试图重新恢复之前的session数据, 很有可能 ...

随机推荐

  1. Python自学之路---Day13

    目录 Python自学之路---Day13 常用的三个方法 匹配单个字符 边界匹配 数量匹配 逻辑与分组 编译正则表达式 其他方法 Python自学之路---Day13 常用的三个方法 1.re.ma ...

  2. input只允许输入数字,并且小数点后保留4位

    <input type="text" value="" name="should_send_num" id="should_ ...

  3. Redis主从复制以及主从复制原理

    Redis 是一个开源的使用 ANSI C 语言编写.支持网络.可基于内存亦可持久化的日志型.Key-Value 数据库,并提供多种语言的 API.从 2010年 3 月 15 日起,Redis 的开 ...

  4. CSS padidng-top\margin-top\fixed 的特殊性

    参考: 使用css时,可能会出错的两个地方 1.padidng-top\margin-top padidng-top\margin-top可以设置'px' 或者是'%',设置'px'略过,说一下设置‘ ...

  5. Bugku 加密(持续更新)

    1.滴答~滴 不多说,摩斯密码解密. 2.聪明的小羊 栅栏密码解密. 3.ok Ook解密 4.这不是摩斯密码 brainfuck解码 5.简单加密 凯撒有两种编码脚本,一种是字母26内循环移位,一种 ...

  6. 吴裕雄--天生自然 PHP开发学习:While 循环

    <html> <body> <?php $i=1; while($i<=5) { echo "The number is " . $i . &q ...

  7. InnoDB和MyISAM区别总结

    原来是MyISAM类型不支持事务处理等高级处理,而InnoDB类型支持. MyISAM类型的表强调的是性能,其执行数度比InnoDB类型更快,但是不提供事务支持,而InnoDB提供事务支持已经外部键等 ...

  8. upstream实现内网网站在公网访问

    背景描述:公司内网有个网站aa.com,B部门需要访问这个aa.com,但是网站部署在内网服务器(服务器是192.168.1网段),B部门网段是192.168.100 需求描述:B部门需要访问aa.c ...

  9. java.lang.AbstractMethodError: org.slf4j.impl.JDK14LoggerAdapter.log(Lorg/slf4j/Marker;Ljava/lang/String;ILjava/lang/String;[Ljava/lang/Object;Ljava/lang/Throwable;)V

    java.lang.AbstractMethodError: org.slf4j.impl.JDK14LoggerAdapter.log(Lorg/slf4j/Marker;Ljava/lang/St ...

  10. 吴裕雄--天生自然TensorFlow高层封装:解决ImportError: cannot import name 'tf_utils'

    将原来版本的keras卸载了,再安装2.1.5版本的keras就可以了.