• JIRA Plugin Development——Configurable Custom Field Plugin


    关于JIRA Plugin开发的中文资料相当少,这可能还是由于JIRA Plugin开发在国内比较小众的原因吧,下面介绍下自己的一个JIRA Plugin开发的详细过程。

    业务需求

    创建JIRA ISSUE时能提供一个字段,字段内容是类似于订单号或手机号这种样式的数据,并且显示出来是一个链接,点击后可跳转到订单详情页或手机号所对应的客户的整个订单页,供用户查看和此任务工单关联的订单数据;

    例如:

    订单号为123456;

    订单详情URL为:http://192.168.11.211?order=123456;

    则字段中显示出来的只能是123456的链接,而不是完整的URL,操作的用户是看不到整个链接地址的,不管是view还是edit界面,都不会显示URL地址,用户只需输入或修改订单号,保存后点击就可以直接跳转到订单详情页面;

    解决办法

    对于这种需求,JIRA自带的Custom Field Plugin就无法满足了,只能自己开发,开始没想到使用可配置的Custom Field,开始的解决办法是字段Value仍保存完整的URL,只是在显示和编辑时只让用户看到订单号,这样做有几个缺点,具体如下所示:

    • 必须在字段配置的Default Value中绑定URL前缀,拿上面的例子来说,就是http://192.168.11.211?order=,但是在显示和编辑时又不能让用户看到,只能在Velocity模板中去做一堆事情来完成,包括和默认URL前缀的匹配,js的处理等,限制性非常大;
    • 无法实现根据订单号的搜索,例如在Issue的Search for issues中搜索订单号为123456的issue就无法实现,因为字段值本身还是整个URL,而不是单纯的订单号;

    身为程序员,自然不允许自己做出的东西是上面那样的残次品,于是研究了下可配置的Custom Field Plugin的实现过程;

    关于Configurable Custom Field Plugin的参考资料相当少,具体实现参考了《Practical JIRA Plugins》第三章的一个例子;

    可配置的字段,就是可以为字段添加一个配置项,在配置项中保存URL前缀,Value值只存储订单号,这样可以保证可按订单号搜索相关issue;

    具体实现

    实现Plugin的前提是我们的环境已经准备好了,即Atlassian的SDK包已经安装成功,并且本机Java环境的配置也已经OK,具体可参考:

    https://developer.atlassian.com/docs/getting-started/set-up-the-atlassian-plugin-sdk-and-build-a-project

    创建Plugin Project

    切换到相应目录下,使用如下命令创建JIRA Plugin:

    $ atlas-create-jira-plugin
    

    会提示输入group-id,artifact-id,version,package,具体如下:

    group-id  
    com.mt.mcs.customfields
    artifact-id  
    configurableURL
    version   
    1.0-SNAPSHOT
    package  
    com.mt.mcs.customfields.configurableurl

     

     

     

     

     

     

    group-id和artifact-id用来生成Plugin的唯一key,在本例中此Plugin的key为:com.mt.mcs.customfields.configurableurl;

    version用在pom.xml中,并且是生成的.jar文件名种的一部分;

    package是编写源码使用的Java包名;

    之后会出现提示是否确认构建此Plugin,输入"Y"或"y"即可;

    将项目导入IDE

    我是用的是idea,操作很简单,只需Import Project—>当前Plugin的根目录(即pom.xml文件所在的目录),点击pom.xml后,点击导入,一路next即可(选择Java环境时记得选择你配置好的Java版本),具体可参考:https://developer.atlassian.com/docs/developer-tools/working-in-an-ide/configure-idea-to-use-the-sdk

    如果使用Eclipse,可参考:https://developer.atlassian.com/docs/getting-started/set-up-the-atlassian-plugin-sdk-and-build-a-project/set-up-the-eclipse-ide-for-linux

    修改pom.xml

    添加你的组织或公司名称以及你网址的URL到<organization>,具体如下所示:

    <organization>
        <name>Example Company</name>
        <url>http://www.example.com/</url>
    </organization>
    

      修改<description>元素;

    <description>This plugin is used for an URL which can config prefix.</description>
    

      添加customfield-type到atlassian-plugin.xml

    添加完成后的atlassian-plugin.xml如下所示:

     1 <atlassian-plugin key="${project.groupId}.${project.artifactId}" name="${project.name}" plugins-version="2">
     2     <plugin-info>
     3         <description>${project.description}</description>
     4         <version>${project.version}</version>
     5         <vendor name="${project.organization.name}" url="${project.organization.url}" />
     6         <param name="plugin-icon">images/pluginIcon.png</param>
     7         <param name="plugin-logo">images/pluginLogo.png</param>
     8     </plugin-info>
     9 
    10     <!-- add our i18n resource -->
    11     <resource type="i18n" name="i18n" location="configurableURL"/>
    12 
    13     <!-- import from the product container -->
    14     <component-import key="applicationProperties" interface="com.atlassian.sal.api.ApplicationProperties" />
    15 
    16     <customfield-type key="configurable-url"
    17                       name="Configurable URL"
    18                       class="com.mt.mcs.customfields.configurableurl.PrefixUrlCFType">
    19         <description>
    20             The Prefix URL Custom Field Type Plugin ...
    21         </description>
    22         <resource type="velocity"
    23                   name="view"
    24                   location="templates/com/mt/mcs/customfields/configurableurl/view.vm"></resource>
    25         <resource type="velocity"
    26                   name="edit"
    27                   location="templates/com/mt/mcs/customfields/configurableurl/edit.vm"></resource>
    28     </customfield-type>
    29 </atlassian-plugin>

      第一行key="${project.groupId}.${project.artifactId}",表示此plugin的唯一标识;

      <customfield-type key="configurable-url" ...中的key为此customfield-type的唯一标识,要求在atlassian-plugin.xml中是唯一的;

    name="Configurable URL",name为此custom field type在JIRA中显示的名字;

    class="com.meituan.mcs.customfields.configurableurl.PrefixUrlCFType">,class为实现custom field type的Java类;

    resource元素中包含了view和edit时,此字段使用的Velocity模板引擎;

    创建CustomField Type的Class

    现在我们需要创建一个Java类,实现CustomFieldType接口,并实现新的custom field type的各项功能,在类名末尾附加"CFType"是一个通用的约定,例如在我们的例子中,使用的Java类名为PrefixUrlCFType.java;

     代码如下所示:

     1 package com.mt.mcs.customfields.configurableurl;
     2 
     7 import com.atlassian.jira.issue.Issue;
     8 import com.atlassian.jira.issue.customfields.impl.FieldValidationException;
     9 import com.atlassian.jira.issue.customfields.impl.GenericTextCFType;
    10 import com.atlassian.jira.issue.customfields.manager.GenericConfigManager;
    11 import com.atlassian.jira.issue.customfields.persistence.CustomFieldValuePersister;
    12 import com.atlassian.jira.issue.fields.CustomField;
    13 import com.atlassian.jira.issue.fields.config.FieldConfig;
    14 import com.atlassian.jira.issue.fields.config.FieldConfigItemType;
    15 import com.atlassian.jira.issue.fields.layout.field.FieldLayoutItem;
    16 
    17 import java.util.List;
    18 import java.util.Map;
    19 import java.util.regex.Matcher;
    20 import java.util.regex.Pattern;
    21 
    22 public class PrefixUrlCFType extends GenericTextCFType {
    23 
    24     public PrefixUrlCFType(CustomFieldValuePersister customFieldValuePersister, GenericConfigManager genericConfigManager) {
    25         super(customFieldValuePersister, genericConfigManager);
    26     }
    27 
    28     @Override
    29     public List<FieldConfigItemType> getConfigurationItemTypes() {
    30         final List<FieldConfigItemType> configurationItemTypes = super.getConfigurationItemTypes();
    31         configurationItemTypes.add(new PrefixURLConfigItem());
    32         return configurationItemTypes;
    33     }
    34 
    35     @Override
    36     public Map<String, Object> getVelocityParameters(final Issue issue,
    37                                                      final CustomField field,
    38                                                      final FieldLayoutItem fieldLayoutItem) {
    39         final Map<String, Object> map = super.getVelocityParameters(issue, field, fieldLayoutItem);
    40 
    41         // This method is also called to get the default value, in
    42         // which case issue is null so we can't use it to add currencyLocale
    43         if (issue == null) {
    44             return map;
    45         }
    46 
    47         FieldConfig fieldConfig = field.getRelevantConfig(issue);
    48         //add what you need to the map here50 
    51         return map;
    52     }
    53 
    54     public String getSingularObjectFromString(final String string) throws FieldValidationException
    55     {
    56         // JRA-14998 - trim the value.
    57         final String value = (string == null) ? "Default" : string.trim();
    58         if (value != null && value != "Default") {
    59             Pattern p = Pattern.compile("^[0-9A-Za-z]+$");
    60             Matcher m = p.matcher(value);
    61             if (!m.matches()) {
    62                 throw new FieldValidationException("Not Valid, only support a-z, A-Z and 0-9 ...");
    63             }
    64         }
    65         return value;
    66     }
    67 }

    添加配置项到Custom Field

    对于每一个custom field,JIRA允许配置不同的内容,例如在不同的项目和任务类型中,select list字段就可以配置不同的option;

    对于字段的配置项,我们首先要做的就是决定配置项中要存储什么值,在我们的项目中,存储的是URL前缀,使用字符串形式保存即可;

    JIRA的配置项需要新定义一个类,并需要实现com.atlassian.jira.issue.fields.config.FieldConfigItemType接口,除此之外,我们还需要在JIRA中定义一个新的web页面,让我们填写并保存配置项的值;

    代码如下所示:

     1 package com.meituan.mcs.customfields.configurableurl;
     2 
     3 import com.atlassian.jira.issue.Issue;
     4 import com.atlassian.jira.issue.fields.config.FieldConfig;
     5 import com.atlassian.jira.issue.fields.config.FieldConfigItemType;
     6 import com.atlassian.jira.issue.fields.layout.field.FieldLayoutItem;
     7 
     8 import java.util.HashMap;
     9 import java.util.Map;
    10 
    14 public class PrefixURLConfigItem implements FieldConfigItemType {
    15 
    16     @Override
    17     //The name of this kind of configuration, as seen in the field configuration scheme;
    18     public String getDisplayName() {
    19         return "Config Prefix URL";
    20     }
    21 
    22     @Override
    23     // This is the text shown in the field configuration screen;
    24     public String getDisplayNameKey() {
    25         return "Prefix Of The URL";
    26     }
    27 
    28     @Override
    29     // This is the current value as shown in the field configuration screen
    30     public String getViewHtml(FieldConfig fieldConfig, FieldLayoutItem fieldLayoutItem) {
    31         String prefix_url = DAO.getCurrentPrefixURL(fieldConfig);
    32         return prefix_url;
    33     }
    34 
    35     @Override
    36     //The unique identifier for this kind of configuration,
    37     //and also the key for the $configs Map used in edit.vm
    38     public String getObjectKey() {
    39         return "PrefixUrlConfig";
    40     }
    41 
    42     @Override
    43     // Return the Object used in the Velocity edit context in $configs
    44     public Object getConfigurationObject(Issue issue, FieldConfig fieldConfig) {
    45         Map result = new HashMap();
    46         result.put("prefixurl", DAO.getCurrentPrefixURL(fieldConfig));
    47         return result;
    48     }
    49 
    50     @Override
    51     // Where the Edit link should redirect to when it's clicked on
    52     public String getBaseEditUrl() {
    53         return "EditPrefixUrlConfig.jspa";
    54     }
    55 }

    DAO(Data Access Object)类的任务就是存储配置数据到数据库,具体数据存储先不在这里详细说明了,DAO类代码如下所示:

     1 package com.mt.mcs.customfields.configurableurl;
     2 
     3 import com.atlassian.jira.issue.fields.config.FieldConfig;
     4 import com.opensymphony.module.propertyset.PropertySet;
     5 import com.opensymphony.module.propertyset.PropertySetManager;
     6 import org.apache.log4j.Logger;
     7 
     8 import java.util.HashMap;
     9 
    10 public class DAO {
    11 
    12     public static final Logger log;
    13 
    14     static {
    15         log = Logger.getLogger(DAO.class);
    16     }
    17 
    18     private static PropertySet ofbizPs = null;
    19 
    20     private static final int ENTITY_ID = 20000;
    21 
    22     private static PropertySet getPS() {
    23         if (ofbizPs == null) {
    24             HashMap ofbizArgs = new HashMap();
    25             ofbizArgs.put("delegator.name", "default");
    26             ofbizArgs.put("entityName", "prefix_url_fields");
    27             ofbizArgs.put("entityId", new Long(ENTITY_ID));
    28             ofbizPs = PropertySetManager.getInstance("ofbiz", ofbizArgs);
    29         }
    30         return ofbizPs;
    31     }
    32 
    33     private static String getEntityName(FieldConfig fieldConfig) {
    34         Long context = fieldConfig.getId();
    35         String psEntityName = fieldConfig.getCustomField().getId() + "_" + context + "_config";
    36         return psEntityName;
    37     }
    38 
    39     public static String retrieveStoredValue(FieldConfig fieldConfig) {
    40         String entityName = getEntityName(fieldConfig);
    41         return getPS().getString(entityName);
    42     }
    43 
    44     public static void updateStoredValue(FieldConfig fieldConfig, String value) {
    45         String entityName = getEntityName(fieldConfig);
    46         getPS().setString(entityName, value);
    47     }
    48 
    49     public static String getCurrentPrefixURL(FieldConfig fieldConfig) {
    50         String prefixurl = retrieveStoredValue(fieldConfig);
    51         log.info("Current stored prefix url is " + prefixurl);
    52         if (prefixurl == null || prefixurl.equals("")) {
    53             prefixurl = null;
    54         }
    55         return prefixurl;
    56     }
    57 }

    做完这些之后,还需要把PrefixURLConfigItem类和PrefixUrlCFType类关联起来,需要重写getConfigurationItemTypes方法,添加后的PrefixUrlCFType类如下所示:

     1 package com.mt.mcs.customfields.configurableurl;
     2 
     3 import com.atlassian.jira.issue.Issue;
     4 import com.atlassian.jira.issue.customfields.impl.FieldValidationException;
     5 import com.atlassian.jira.issue.customfields.impl.GenericTextCFType;
     6 import com.atlassian.jira.issue.customfields.manager.GenericConfigManager;
     7 import com.atlassian.jira.issue.customfields.persistence.CustomFieldValuePersister;
     8 import com.atlassian.jira.issue.fields.CustomField;
     9 import com.atlassian.jira.issue.fields.config.FieldConfig;
    10 import com.atlassian.jira.issue.fields.config.FieldConfigItemType;
    11 import com.atlassian.jira.issue.fields.layout.field.FieldLayoutItem;
    12 
    13 import java.util.List;
    14 import java.util.Map;
    15 import java.util.regex.Matcher;
    16 import java.util.regex.Pattern;
    17 
    18 public class PrefixUrlCFType extends GenericTextCFType {
    19 
    20     public PrefixUrlCFType(CustomFieldValuePersister customFieldValuePersister, GenericConfigManager genericConfigManager) {
    21         super(customFieldValuePersister, genericConfigManager);
    22     }
    23 
    24     @Override
    25     public List<FieldConfigItemType> getConfigurationItemTypes() {
    26         final List<FieldConfigItemType> configurationItemTypes = super.getConfigurationItemTypes();
    27         configurationItemTypes.add(new PrefixURLConfigItem());
    28         return configurationItemTypes;
    29     }
    30 
    31     @Override
    32     public Map<String, Object> getVelocityParameters(final Issue issue,
    33                                                      final CustomField field,
    34                                                      final FieldLayoutItem fieldLayoutItem) {
    35         final Map<String, Object> map = super.getVelocityParameters(issue, field, fieldLayoutItem);
    36 
    37         // This method is also called to get the default value, in
    38         // which case issue is null so we can't use it to add currencyLocale
    39         if (issue == null) {
    40             return map;
    41         }
    42 
    43         FieldConfig fieldConfig = field.getRelevantConfig(issue);
    44         //add what you need to the map here
    45         map.put("currentPrefixURL", DAO.getCurrentPrefixURL(fieldConfig));
    46 
    47         return map;
    48     }
    49 
    50     public String getSingularObjectFromString(final String string) throws FieldValidationException
    51     {
    52         // JRA-14998 - trim the value.
    53         final String value = (string == null) ? "Default" : string.trim();
    54         if (value != null && value != "Default") {
    55             Pattern p = Pattern.compile("^[0-9A-Za-z]+$");
    56             Matcher m = p.matcher(value);
    57             if (!m.matches()) {
    58                 throw new FieldValidationException("Not Valid, only support a-z, A-Z and 0-9 ...");
    59             }
    60         }
    61         return value;
    62     }
    63 }

     Velocity模板引擎

    custom field在JIRA中显示和编辑,需要使用Velocity模板,即view.vm和edit.vm,具体如下所示:

    view.vm

     1 #disable_html_escaping()
     2 #set($defaultValue = "Default")
     3 #if ($value && $value != $defaultValue)
     4     #if ($currentPrefixURL)
     5         <a class="tinylink" target="_blank" href="$currentPrefixURL$value">$!textutils.htmlEncode($value)</a>
     6     #else
     7         #set($displayValue = "没有配置URL前缀...")
     8         $!textutils.htmlEncode($displayValue)
     9     #end
    10 #elseif ($value == $defaultValue)
    11     #set($displayValue = "请输入相关信息...")
    12     $textutils.htmlEncode($displayValue)
    13 #else
    14     #set($displayValue = "出现错误了....")
    15     $textutils.htmlEncode($displayValue)
    16 #end

    edit.vm

     1 #disable_html_escaping()
     2 #customControlHeader ($action $customField.id $customField.name $fieldLayoutItem.required $displayParameters $auiparams)
     3 #set($configObj = $configs.get("PrefixUrlConfig"))
     4 #set($prefixUrl = $configObj.get("prefixurl"))
     5 #set($defaultValue = "Default")
     6 #if ($value == $defaultValue)
     7     <input class="text" id="displayText" name="displayText" type="text" value="" onchange="changeValue(${customField.id})">
     8     <input class="text" id="$customField.id" name="$customField.id" type="hidden" value="$textutils.htmlEncode($!value)">
     9 #else
    10     <input class="text" id="$customField.id" name="$customField.id" type="text" value="$textutils.htmlEncode($!value)">
    11 #end
    12 <script type="text/javascript">
    13   function changeValue(cfElmId) {
    14     var cfElmId = cfElmId.id;
    15     var element = document.getElementById("displayText");
    16     var elmVal = element.value;
    17     var cfElm = document.getElementById(cfElmId);
    18     cfElm.value = elmVal;
    19   }
    20 </script>
    21 #customControlFooter ($action $customField.id $fieldLayoutItem.fieldDescription $displayParameters $auiparams)

    WebWork Action

    到现在为止,我们定义了一个新类型的配置项,并且更新了PrefixUrlCFType类和Velocity模板引擎,我们还需要一个新的web页面,来设置配置项的值(即URL前缀信息)并保存到数据库;

    JIRA是通过WebWork web应用框架来定义web页面的,需要在atlassian-plugin.xml文件中配置webwork元素,具体如下所示:

     1 <webwork1 key="url-configurable"
     2           name="URL configuration action"
     3           class="java.lang.Object">
     4     <description>
     5         The action for editing a prefix url custom field type configuration.
     6     </description>
     7     <actions>
     8         <action name="com.mt.mcs.customfields.configurableurl.EditPrefixUrlConfig"
     9                 alias="EditPrefixUrlConfig">
    10             <view name="input">
    11                 /templates/com/mt/mcs/customfields/configurableurl/edit-config.vm
    12             </view>
    13             <view name="securitybreach">
    14                 /secure/views/securitybreach.jsp
    15             </view>
    16         </action>
    17     </actions>
    18 </webwork1>

    使用的edit-config.vm模板文件代码如下所示:

     1 <html>
     2 <head>
     3   <title>
     4       $i18n.getText('common.words.configure')
     5       $action.getCustomField().getName()
     6   </title>
     7   <meta content="admin" name="decorator">
     8   <link rel="stylesheet" type="text/css" media="print" href="/styles/combined-printtable.css">
     9   <link rel="stylesheet" type="text/css" media="all" href="/styles/combined.css">
    10   <style>
    11     table.base-table {
    12       margin: 15px auto;
    13       border-spacing: 5px 10px;
    14       line-height: 1.5;
    15       font-size: 16px;
    16     }
    17     input.prefixurl {
    18       outline: none;
    19       box-shadow: bisque;
    20       width: 350px !important;
    21     }
    22     table.base-table input#Save {
    23       margin-left: 80px;
    24     }
    25   </style>
    26 </head>
    27 <body>
    28 <h2 class="formtitle">
    29     $i18n.getText('common.words.configure') $action.getCustomField().getName()
    30 </h2>
    31 <div class="aui-message aui-message-info">
    32   <p class="title">
    33     <span class="aui-icon icon-info"></span>
    34     <strong>Notice</strong>
    35   </p>
    36   <p>
    37     Config the prefix of your URL.
    38   </p>
    39   <p>
    40     At the end of the URL, you need to add a '/', such as 'http://192.168.11.234/' !
    41   </p>
    42 </div>
    43 <form action="EditPrefixUrlConfig.jspa" method="post" class="aui">
    44   <table class="base-table">
    45     <tr>
    46       <td>
    47         Prefix Url:&nbsp;
    48       </td>
    49       <td>
    50         #set($prefix_url = $action.getPrefixurl())
    51         <input type="text" name="prefixurl" id="prefixurl" value="$!prefix_url" class="text prefixurl">
    52       </td>
    53     </tr>
    54     <tr>
    55       <td colspan="2">
    56         <input type="submit" name="Save" id="Save" value="$i18n.getText('common.words.save')" class="aui-button">
    57         <a href="ConfigureCustomField!default.jspa?customFieldId=$action.getCustomField().getIdAsLong().toString()"
    58            id="cancelButton" class="aui-button" name="ViewCustomFields.jspa">
    59           Cancel
    60         </a>
    61       </td>
    62     </tr>
    63   </table>
    64   <input type="hidden" name="fieldConfigId" value="$fieldConfigId">
    65 </form>
    66 </body>
    67 </html>

    Action Class

    配置项的web页面使用的Action类是EditPrefixUrlConfig.java,代码如下所示:

     1 package com.mt.mcs.customfields.configurableurl;
     2 
     3 import com.atlassian.jira.config.managedconfiguration.ManagedConfigurationItemService;
     4 import com.atlassian.jira.issue.customfields.impl.FieldValidationException;
     5 import com.atlassian.jira.security.Permissions;
     6 import com.atlassian.jira.web.action.admin.customfields.AbstractEditConfigurationItemAction;
     7 import com.opensymphony.util.UrlUtils;
     8 
     9 public class EditPrefixUrlConfig extends AbstractEditConfigurationItemAction {
    10 
    11     protected EditPrefixUrlConfig(ManagedConfigurationItemService managedConfigurationItemService) {
    12         super(managedConfigurationItemService);
    13     }
    14 
    15     private String prefixurl;
    16 
    17     public void setPrefixurl(String prefixurl) {
    18         this.prefixurl = prefixurl;
    19     }
    20 
    21     public String getPrefixurl() {
    22         return this.prefixurl;
    23     }
    24 
    25     protected void doValidation() {
    26         String prefix_url = getPrefixurl();
    27         prefix_url = (prefix_url == null) ? null : prefix_url.trim();
    28         if (prefix_url == null) {
    29             return;
    30         }
    31         if (!UrlUtils.verifyHierachicalURI(prefix_url)) {
    32             addErrorMessage("ERROR: " + prefix_url + " is not a valid URL...");
    33         }
    34     }
    35 
    36     protected String doExecute() throws Exception {
    37         if (!isHasPermission(Permissions.ADMINISTER)) {
    38             return "securitybreach";
    39         }
    40         if (getPrefixurl() == null) {
    41             setPrefixurl(DAO.retrieveStoredValue(getFieldConfig()));
    42         }
    43         DAO.updateStoredValue(getFieldConfig(), getPrefixurl());
    44         String save = request.getParameter("Save");
    45         if (save != null && save.equals("Save")) {
    46             setReturnUrl("/secure/admin/ConfigureCustomField!default.jspa?customFieldId=" + getFieldConfig().getCustomField().getIdAsLong().toString());
    47             return getRedirect("not used");
    48         }
    49         return INPUT;
    50     }
    51 }

    这样整个可配置的Custom Field Plugin已经正式开发完成了,只是搜索功能还没有实现,搜索只是继承已有的Searcher即可,本例继承的是TextSearcher;

    Searcher的实现可参考:https://www.safaribooksonline.com/library/view/practical-jira-plugins/9781449311322/ch04.html,讲解非常详细;

  • 相关阅读:
    WebApi接口返回值不困惑:返回值类型详解
    Autofac 依赖注入框架 使用
    ASP.NET Core:使用Dapper和SwaggerUI来丰富你的系统框架
    ASP .Net Core 使用 Dapper 轻型ORM框架
    基于AspNet Core2.0 开发框架,包含简单的个人博客Demo
    Asp.Net MVC及Web API框架配置会碰到的几个问题及解决方案 (精髓)
    精简版自定义 jquery
    vs code 前端如何以服务器模式打开 [安装服务器] server insteall
    RestSharp用法小结
    翻译:WebApi 认证--用户认证Oauth解析
  • 原文地址:https://www.cnblogs.com/pflee/p/4279645.html
Copyright © 2020-2023  润新知