把系统从Struts2 迁移到 Spring MVC六大步总结

基于struts的系统迁移到SpringMVC架构上来,共分六部曲,让系统一部一部迁移过来,本文讲的知识点以Struts2 to Spring4,但是针对其他应用场景也是可以参考的。

Step 1: 替换基本的框架库。

Firstly while migrating from struts to spring we have to replace our struts related libraries with spring libraries in lib folder.

I have mentioned basic libraries of both struts and spring for your clarification.

Struts basic libraries :
  1. struts.jar
  2. struts-legacy.jar
  3. etc.. 

Have you ever use :   Javadoc comment in Java

Spring basic libraries :

  1. standard.jar
  2. org.springframework.asm-4.0.1.RELEASE-A.jar
  3. org.springframework.beans-4.0.1.RELEASE-A.jar
  4. org.springframework.context-4.0.1.RELEASE-A.jar
  5. org.springframework.core-4.0.1.RELEASE-A.jar
  6. org.springframework.expression-4.0.1.RELEASE-A.jar
  7. org.springframework.web.servlet-4.0.1.RELEASE-A.jar
  8. org.springframework.web-4.0.1.RELEASE-A.jar
  9. etc..

Step 2: 修改web.xml配置文件

In this step we have to remove Action filter dispatcher for the web.xml and add Spring dipatcher servlet as Front controller
Work on new technology  :  Create and manage cloud applications using Java

In Strut application web.xml look like as follows
<?xml version="1.0" encoding="UTF-8"?>  
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" id="WebApp_ID" version="3.0">  
  <display-name>Struts2MyFirstApp</display-name>  
  <filter>  
        <filter-name>struts2</filter-name>  
        <filter-class>  
            org.apache.struts2.dispatcher.FilterDispatcher  
        </filter-class>  
    </filter>  

<filter-mapping>  
        <filter-name>struts2</filter-name>  
        <url-pattern>/*</url-pattern>  
    </filter-mapping>  
    <welcome-file-list>  
        <welcome-file>Login.jsp</welcome-file>  
    </welcome-file-list>  
</web-app>
In Spring application web.xml look like as follows
<?xml version="1.0" encoding="UTF-8"?>  
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" id="WebApp_ID" version="3.0">  
  <display-name>dispatcher</display-name>  
  <servlet>  
    <servlet-name>dispatcher</servlet-name>  
    <servlet-class>  
            org.springframework.web.servlet.DispatcherServlet  
        </servlet-class>  
    <load-on-startup>1</load-on-startup>  
  </servlet>  
  <servlet-mapping>  
    <servlet-name>dispatcher</servlet-name>  
    <url-pattern>/</url-pattern>  
  </servlet-mapping>  
</web-app>

Step 3: 替换Struts本身的配置文件

Now replace all struts configuration files to spring configuration file as follows

In Struts applivation struts configuration file-
<?xml version="1.0" encoding="UTF-8" ?>  
<!DOCTYPE struts PUBLIC  
    "-//Apache Software Foundation//DTD Struts Configuration 2.0//EN"  
    "http://struts.apache.org/dtds/struts-2.0.dtd">  
   
<struts>  
    <constant name="struts.enable.DynamicMethodInvocation" value="false" />  
    <constant name="struts.devMode" value="false" />  
    <constant name="struts.custom.i18n.resources" value="myapp" />  
   
    <package name="default" extends="struts-default" namespace="/">  
        <action name="login" class="com.geekonjavaonjava.struts2.login.LoginAction">  
            <result name="success">Welcome.jsp</result>  
            <result name="error">Login.jsp</result>  
        </action>  
    </package>  
</struts>  
In Spring application spring configuration file as follows
<?xml version="1.0" encoding="UTF-8"?>    
<beans xmlns="http://www.springframework.org/schema/beans"    
     xmlns:context="http://www.springframework.org/schema/context"    
     xmlns:p="http://www.springframework.org/schema/p"      
     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"    
     xsi:schemaLocation="http://www.springframework.org/schema/beans    
    http://www.springframework.org/schema/beans/spring-beans-4.0.xsd    
    http://www.springframework.org/schema/context    
    http://www.springframework.org/schema/context/spring-context-4.0.xsd">    
        
     <context:component-scan base-package="com.geekonjavaonjava.spring.login.controller" />    
        
     <bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">    
        <property name="prefix">    
          <value>/WEB-INF/views/</value>    
        </property>    
        <property name="suffix">    
          <value>.jsp</value>    
        </property>    
      </bean>    
</beans>

Here, <context:component-scan> tag is used, so that spring will load all the components from given package i.e. " com.geekonjavaonjava.spring.login.controller".

Use this in Struts2 : Get value of struts property tag into jsp variable

We can use different view resolver, here I have used InternalResourceViewResolver. In which prefix and suffix are used to resolve the view by prefixing and suffixing values to the object returned by ModelAndView in action class.

Step 4: 修改JSP文件

While migration an application from struts to spring we need to change in jsp file as following

Firstly replace all tlds-
<%@ taglib uri="http://struts.apache.org/tags-bean" prefix="bean" %>  
<%@ taglib uri="http://struts.apache.org/tags-html" prefix="html" %>  
<%@ taglib uri="http://struts.apache.org/tags-logic" prefix="logic" %>  
<%@ taglib uri="http://struts.apache.org/tags-tiles" prefix="tiles" %>
Replace these with following spring taglib's :
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form"%>  
<%@ taglib prefix="spring" uri="http://www.springframework.org/tags"%>
In Struts :
<html:form action="/addLogin" method="post">
In Spring :
<form:form method="POST" commandName="loginForm" name="loginForm" action="login.do"> 

Here commandName is going to map with corresponding formbean for that jsp. Next we will see, how action is getting called with spring 4 annotations.

Step 5: 修改Action 类文件

Now following changes need to be done in action classes for struts to spring migration using annotations-

Struts Action:
package com.geekonjavaonjava.struts2.login;  
  
import com.opensymphony.xwork2.ActionSupport;  
  
/** 
 * @author geekonjava 
 * 
 */  
@SuppressWarnings("serial")  
public class LoginAction  extends ActionSupport{  
 private String username;  
    private String password;  
      
 public String execute() {  
     
        if (this.username.equals("geekonjava")   
                && this.password.equals("sweety")) {  
            return "success";  
        } else {  
         addActionError(getText("error.login"));  
            return "error";  
        }  
    }  
  
 public String getUsername() {  
  return username;  
 }  
  
 public void setUsername(String username) {  
  this.username = username;  
 }  
  
 public String getPassword() {  
  return password;  
 }  
  
 public void setPassword(String password) {  
  this.password = password;  
 }  
   
}
Spring action
package com.geekonjavaonjava.spring.login.controller;  
  
import org.springframework.stereotype.Controller;  
import org.springframework.ui.ModelMap;  
import org.springframework.web.bind.annotation.RequestMapping;  
import org.springframework.web.bind.annotation.RequestMethod;  
  
/** 
 * @author GeekOnJava 
 * 
 */  
@Controller  
public class LoginController {  
   
 @RequestMapping(value="/login.do", method = RequestMethod.GET)  
 public String doLogin(ModelMap model, LoginForm loginForn) {  
 if (this.username.equals("geekonjava")   
                && this.password.equals("sweety")) {  
            model.addAttribute("message", "Login Success");  
        } else {  
            model.addAttribute("message", "Login Failure");  
        }  
         return "home";  
   
 }  
}

Step 6: 修改前端验证机制

In struts JSP file validation changes as follows
<% ActionErrors actionErrors = (ActionErrors)request.getAttribute("org.apache.struts.action.ERROR"); %>
In Spring JSP file as follows-
<form:errors path="*" cssClass="error" /> 

参考链接


Android: 通过Intent筛选多种类型文件

一般使用setType()方法来实现文件过滤,如:只显示PDF文件:

     int requestCode = 100;
     Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
     intent.setType("application/pdf");
     intent.addCategory(Intent.CATEGORY_OPENABLE);
     startActivityForResult(intent, requestCode); 

但如果要指定多种类型呢,同时要指定pdf,excel,word,ppt这些类型的文件,那要怎样设置呢?

指定多种类型文件

在网上查了,有些答案是

错误方式1——setType中进行拼接:

intent.setType(“video/*;image/*”);//同时选择视频和图片

错误方式2——调用多次setType:

intent.setType("video/*");
intent.setType("image/*");

这两种方式都是错误的

我们看下源码

/**
     * Set an explicit MIME data type.
     *
     * <p>This is used to create intents that only specify a type and not data,
     * for example to indicate the type of data to return.
     *
     * <p>This method automatically clears any data that was
     * previously set (for example by {@link #setData}).
     *
     * <p><em>Note: MIME type matching in the Android framework is
     * case-sensitive, unlike formal RFC MIME types.  As a result,
     * you should always write your MIME types with lower case letters,
     * or use {@link #normalizeMimeType} or {@link #setTypeAndNormalize}
     * to ensure that it is converted to lower case.</em>
     *
     * @param type The MIME type of the data being handled by this intent.
     *
     * @return Returns the same Intent object, for chaining multiple calls
     * into a single statement.
     *
     * @see #getType
     * @see #setTypeAndNormalize
     * @see #setDataAndType
     * @see #normalizeMimeType
     */
    public Intent setType(String type) {
        mData = null;
        mType = type;
        return this;
    }

我们可以看到,setType每次都是重新赋值,没有添加到list和数组中,因此这两种方式是实现不了指定多种类型文件的。
既然这种方式实现不了,那么Intent会不会提供字段以便我们传递过滤数据,我们通过官方文档及源码,发现Intent提供了EXTRA_MIME_TYPES这个字段来传递,而且是数组类型:

/**
     * Extra used to communicate a set of acceptable MIME types. The type of the
     * extra is {@code String[]}. Values may be a combination of concrete MIME
     * types (such as "image/png") and/or partial MIME types (such as
     * "audio/*").
     *
     * @see #ACTION_GET_CONTENT
     * @see #ACTION_OPEN_DOCUMENT
     */
    public static final String EXTRA_MIME_TYPES = "android.intent.extra.MIME_TYPES";

因此结果就简单了,我们要指定ppt,word,excel,pdf类型的文件,代码如下

public static void openDirChooseFile(Activity activity, int requestCode, String[] mimeTypes) {
        Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
        if (mimeTypes != null) {
            intent.putExtra(Intent.EXTRA_MIME_TYPES, mimeTypes);
        }
        intent.setType("*/*");
        intent.addCategory(Intent.CATEGORY_OPENABLE);
        // intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);//多选
        activity.startActivityForResult(intent, requestCode);
    }
 public static void chooseFile(Activity activity, int requestCode) {
        String[] mimeTypes = {MimeType.DOC, MimeType.DOCX, MimeType.PDF, MimeType.PPT, MimeType.PPTX, MimeType.XLS, MimeType.XLSX};
        FileUtil.openDirChooseFile(activity, requestCode, mimeTypes);
    }

MimeType文件

public class MimeType {
    public static final String DOC = "application/msword";
    public static final String DOCX = "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
    public static final String XLS = "application/vnd.ms-excel application/x-excel";
    public static final String XLSX = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
    public static final String PPT = "application/vnd.ms-powerpoint";
    public static final String PPTX = "application/vnd.openxmlformats-officedocument.presentationml.presentation";
    public static final String PDF = "application/pdf";
}

Intent指定多种类型的文件,正确的做法,是通过Intent.EXTRA_MIME_TYPES传递Mime类型数组实现

 intent.setType("*/*");
 intent.putExtra(Intent.EXTRA_MIME_TYPES, mimeTypes);

MimeType 文件列表参考

 1. Mozilla MDN Web Docs: https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types

扩展名 文档类型 MIME 类型
.aac AAC audio audio/aac
.abw AbiWord document application/x-abiword
.arc Archive document (multiple files embedded) application/x-freearc
.avi AVI: Audio Video Interleave video/x-msvideo
.azw Amazon Kindle eBook format application/vnd.amazon.ebook
.bin Any kind of binary data application/octet-stream
.bmp Windows OS/2 Bitmap Graphics image/bmp
.bz BZip archive application/x-bzip
.bz2 BZip2 archive application/x-bzip2
.csh C-Shell script application/x-csh
.css Cascading Style Sheets (CSS) text/css
.csv Comma-separated values (CSV) text/csv
.doc Microsoft Word application/msword
.docx Microsoft Word (OpenXML) application/vnd.openxmlformats-officedocument.wordprocessingml.document
.eot MS Embedded OpenType fonts application/vnd.ms-fontobject
.epub Electronic publication (EPUB) application/epub+zip
.gif Graphics Interchange Format (GIF) image/gif
.htm
.html
HyperText Markup Language (HTML) text/html
.ico Icon format image/vnd.microsoft.icon
.ics iCalendar format text/calendar
.jar Java Archive (JAR) application/java-archive
.jpeg
.jpg
JPEG images image/jpeg
.js JavaScript text/javascript
.json JSON format application/json
.jsonld JSON-LD format application/ld+json
.mid
.midi
Musical Instrument Digital Interface (MIDI) audio/midi audio/x-midi
.mjs JavaScript module text/javascript
.mp3 MP3 audio audio/mpeg
.mpeg MPEG Video video/mpeg
.mpkg Apple Installer Package application/vnd.apple.installer+xml
.odp OpenDocument presentation document application/vnd.oasis.opendocument.presentation
.ods OpenDocument spreadsheet document application/vnd.oasis.opendocument.spreadsheet
.odt OpenDocument text document application/vnd.oasis.opendocument.text
.oga OGG audio audio/ogg
.ogv OGG video video/ogg
.ogx OGG application/ogg
.otf OpenType font font/otf
.png Portable Network Graphics image/png
.pdf Adobe Portable Document Format (PDF) application/pdf
.ppt Microsoft PowerPoint application/vnd.ms-powerpoint
.pptx Microsoft PowerPoint (OpenXML) application/vnd.openxmlformats-officedocument.presentationml.presentation
.rar RAR archive application/x-rar-compressed
.rtf Rich Text Format (RTF) application/rtf
.sh Bourne shell script application/x-sh
.svg Scalable Vector Graphics (SVG) image/svg+xml
.swf Small web format (SWF) or Adobe Flash document application/x-shockwave-flash
.tar Tape Archive (TAR) application/x-tar
.tif
.tiff
Tagged Image File Format (TIFF) image/tiff
.ttf TrueType Font font/ttf
.txt Text, (generally ASCII or ISO 8859-n) text/plain
.vsd Microsoft Visio application/vnd.visio
.wav Waveform Audio Format audio/wav
.weba WEBM audio audio/webm
.webm WEBM video video/webm
.webp WEBP image image/webp
.woff Web Open Font Format (WOFF) font/woff
.woff2 Web Open Font Format (WOFF) font/woff2
.xhtml XHTML application/xhtml+xml
.xls Microsoft Excel application/vnd.ms-excel
.xlsx Microsoft Excel (OpenXML) application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
.xml XML application/xml 代码对普通用户来说不可读 (RFC 3023, section 3)
text/xml 代码对普通用户来说可读 (RFC 3023, section 3)
.xul XUL application/vnd.mozilla.xul+xml
.zip ZIP archive application/zip
.3gp 3GPP audio/video container video/3gpp
audio/3gpp(若不含视频)
.3g2 3GPP2 audio/video container video/3gpp2
audio/3gpp2(若不含视频)
.7z 7-zip archive application/x-7z-compressed

2. AOSP MediaFle:  

    public static String getMimeTypeForFile(String path) {
        MediaFileType mediaFileType = getFileType(path);
        return (mediaFileType == null ? null : mediaFileType.mimeType);
    }
    static {
        /// M: Add more audio file types to maps. {@
        addFileType("3GP", FILE_TYPE_3GPP3, "audio/3gpp");
        addFileType("3GA", FILE_TYPE_3GA, "audio/3gpp");
        addFileType("MOV", FILE_TYPE_QUICKTIME_AUDIO, "audio/quicktime");
        addFileType("QT", FILE_TYPE_QUICKTIME_AUDIO, "audio/quicktime");
        /// Add to support Apple Lossless Codec(audio/alac)
        addFileType("CAF", FILE_TYPE_CAF, "audio/alac");
        /// Add to support PCM(audio/wav)
        addFileType("WAV", FILE_TYPE_WAV, "audio/wav", MtpConstants.FORMAT_WAV);
        addFileType("OGG", FILE_TYPE_OGG, "audio/vorbis", MtpConstants.FORMAT_OGG);
        addFileType("OGG", FILE_TYPE_OGG, "audio/webm", MtpConstants.FORMAT_OGG);
        /// Add to support MP2, first add video/mp2p, so that use MP2 can return as audio type
        addFileType("MP2", FILE_TYPE_MP2PS, "video/mp2p");
        addFileType("MP2", FILE_TYPE_MP2, "audio/mpeg");
        /// Add to support Monkey's Audio APE(audio/ape)
        if (SystemProperties.getBoolean("ro.mtk_audio_ape_support", false)) {
            addFileType("APE", FILE_TYPE_APE, "audio/ape");
        }
        /// Add to support OMA DRM audio type DCF
        if (SystemProperties.getBoolean("ro.mtk_oma_drm_support", false)) {
            addFileType("DCF", FILE_TYPE_MP3, "audio/mpeg");
        }
        /// @}

        addFileType("MP3", FILE_TYPE_MP3, "audio/mpeg", MtpConstants.FORMAT_MP3);
        addFileType("MPGA", FILE_TYPE_MP3, "audio/mpeg", MtpConstants.FORMAT_MP3);
        addFileType("M4A", FILE_TYPE_M4A, "audio/mp4", MtpConstants.FORMAT_MPEG);
        addFileType("WAV", FILE_TYPE_WAV, "audio/x-wav", MtpConstants.FORMAT_WAV);
        addFileType("AMR", FILE_TYPE_AMR, "audio/amr");
        addFileType("AWB", FILE_TYPE_AWB, "audio/amr-wb");
        if (isWMAEnabled()) {
            addFileType("WMA", FILE_TYPE_WMA, "audio/x-ms-wma", MtpConstants.FORMAT_WMA);
        }
        addFileType("OGG", FILE_TYPE_OGG, "audio/ogg", MtpConstants.FORMAT_OGG);
        addFileType("OGG", FILE_TYPE_OGG, "application/ogg", MtpConstants.FORMAT_OGG);
        addFileType("OGA", FILE_TYPE_OGG, "application/ogg", MtpConstants.FORMAT_OGG);
        addFileType("AAC", FILE_TYPE_AAC, "audio/aac", MtpConstants.FORMAT_AAC);
        addFileType("AAC", FILE_TYPE_AAC, "audio/aac-adts", MtpConstants.FORMAT_AAC);
        addFileType("MKA", FILE_TYPE_MKA, "audio/x-matroska");

        addFileType("MID", FILE_TYPE_MID, "audio/midi");
        addFileType("MIDI", FILE_TYPE_MID, "audio/midi");
        addFileType("XMF", FILE_TYPE_MID, "audio/midi");
        addFileType("RTTTL", FILE_TYPE_MID, "audio/midi");
        addFileType("SMF", FILE_TYPE_SMF, "audio/sp-midi");
        addFileType("IMY", FILE_TYPE_IMY, "audio/imelody");
        addFileType("RTX", FILE_TYPE_MID, "audio/midi");
        addFileType("OTA", FILE_TYPE_MID, "audio/midi");
        addFileType("MXMF", FILE_TYPE_MID, "audio/midi");

        /// M: Add more video file types to maps. {@
        addFileType("MTS", FILE_TYPE_MP2TS, "video/mp2ts");
        addFileType("M2TS", FILE_TYPE_MP2TS, "video/mp2ts");
        addFileType("MOV", FILE_TYPE_QUICKTIME_VIDEO, "video/quicktime");
        addFileType("QT", FILE_TYPE_QUICKTIME_VIDEO, "video/quicktime");
        addFileType("OGV", FILE_TYPE_OGM, "video/ogm");
        addFileType("OGM", FILE_TYPE_OGM, "video/ogm");
        if (SystemProperties.getBoolean("ro.mtk_flv_playback_support", false)) {
            addFileType("FLV", FILE_TYPE_FLV, "video/x-flv");
            addFileType("F4V", FILE_TYPE_FLV, "video/x-flv");
            addFileType("PFV", FILE_TYPE_FLV, "video/x-flv");
            addFileType("FLA", FILE_TYPE_FLA, "audio/x-flv");
        }
        if (SystemProperties.getBoolean("ro.mtk_mtkps_playback_support", false)) {
            addFileType("PS", FILE_TYPE_MP2PS, "video/mp2p");
            /// Only support VOB when mtkps feature option is enabled
            addFileType("VOB", FILE_TYPE_MP2PS, "video/mp2p");
            /// DAT files should not be scanned as mpeg2 PS if PS is not supported.
            addFileType("DAT", FILE_TYPE_MP2PS, "video/mp2p");
        }
        /// @}

        addFileType("MPEG", FILE_TYPE_MP4, "video/mpeg", MtpConstants.FORMAT_MPEG);
        addFileType("MPG", FILE_TYPE_MP4, "video/mpeg", MtpConstants.FORMAT_MPEG);
        addFileType("MP4", FILE_TYPE_MP4, "video/mp4", MtpConstants.FORMAT_MPEG);
        addFileType("M4V", FILE_TYPE_M4V, "video/mp4", MtpConstants.FORMAT_MPEG);
        addFileType("3GP", FILE_TYPE_3GPP, "video/3gpp",  MtpConstants.FORMAT_3GP_CONTAINER);
        addFileType("3GPP", FILE_TYPE_3GPP, "video/3gpp", MtpConstants.FORMAT_3GP_CONTAINER);
        addFileType("3G2", FILE_TYPE_3GPP2, "video/3gpp2", MtpConstants.FORMAT_3GP_CONTAINER);
        addFileType("3GPP2", FILE_TYPE_3GPP2, "video/3gpp2", MtpConstants.FORMAT_3GP_CONTAINER);
        addFileType("MKV", FILE_TYPE_MKV, "video/x-matroska");
        addFileType("WEBM", FILE_TYPE_WEBM, "video/webm");
        addFileType("TS", FILE_TYPE_MP2TS, "video/mp2ts");
        addFileType("AVI", FILE_TYPE_AVI, "video/avi");

        if (isWMVEnabled()) {
            addFileType("WMV", FILE_TYPE_WMV, "video/x-ms-wmv", MtpConstants.FORMAT_WMV);
            addFileType("ASF", FILE_TYPE_ASF, "video/x-ms-asf");
        }

        /// M: Add more image file types to maps. {@
        if (!SystemProperties.getBoolean("ro.mtk_bsp_package", false)) {
            /// Mpo files should not be scanned as images in BSP
            addFileType("MPO", FILE_TYPE_MPO, "image/mpo");
        }
        /// @}

        addFileType("JPG", FILE_TYPE_JPEG, "image/jpeg", MtpConstants.FORMAT_EXIF_JPEG);
        addFileType("JPEG", FILE_TYPE_JPEG, "image/jpeg", MtpConstants.FORMAT_EXIF_JPEG);
        addFileType("GIF", FILE_TYPE_GIF, "image/gif", MtpConstants.FORMAT_GIF);
        addFileType("PNG", FILE_TYPE_PNG, "image/png", MtpConstants.FORMAT_PNG);
        addFileType("BMP", FILE_TYPE_BMP, "image/x-ms-bmp", MtpConstants.FORMAT_BMP);
        addFileType("WBMP", FILE_TYPE_WBMP, "image/vnd.wap.wbmp");
        addFileType("WEBP", FILE_TYPE_WEBP, "image/webp");

        addFileType("M3U", FILE_TYPE_M3U, "audio/x-mpegurl", MtpConstants.FORMAT_M3U_PLAYLIST);
        addFileType("M3U", FILE_TYPE_M3U, "application/x-mpegurl", MtpConstants.FORMAT_M3U_PLAYLIST);
        addFileType("PLS", FILE_TYPE_PLS, "audio/x-scpls", MtpConstants.FORMAT_PLS_PLAYLIST);
        addFileType("WPL", FILE_TYPE_WPL, "application/vnd.ms-wpl", MtpConstants.FORMAT_WPL_PLAYLIST);
        addFileType("M3U8", FILE_TYPE_HTTPLIVE, "application/vnd.apple.mpegurl");
        addFileType("M3U8", FILE_TYPE_HTTPLIVE, "audio/mpegurl");
        addFileType("M3U8", FILE_TYPE_HTTPLIVE, "audio/x-mpegurl");

        addFileType("FL", FILE_TYPE_FL, "application/x-android-drm-fl");

        addFileType("TXT", FILE_TYPE_TEXT, "text/plain", MtpConstants.FORMAT_TEXT);
        addFileType("HTM", FILE_TYPE_HTML, "text/html", MtpConstants.FORMAT_HTML);
        addFileType("HTML", FILE_TYPE_HTML, "text/html", MtpConstants.FORMAT_HTML);
        addFileType("PDF", FILE_TYPE_PDF, "application/pdf");
        addFileType("DOC", FILE_TYPE_MS_WORD, "application/msword", MtpConstants.FORMAT_MS_WORD_DOCUMENT);
        addFileType("XLS", FILE_TYPE_MS_EXCEL, "application/vnd.ms-excel", MtpConstants.FORMAT_MS_EXCEL_SPREADSHEET);
        addFileType("PPT", FILE_TYPE_MS_POWERPOINT, "application/vnd.ms-powerpoint", MtpConstants.FORMAT_MS_POWERPOINT_PRESENTATION);
        addFileType("FLAC", FILE_TYPE_FLAC, "audio/flac", MtpConstants.FORMAT_FLAC);
        addFileType("ZIP", FILE_TYPE_ZIP, "application/zip");
        addFileType("MPG", FILE_TYPE_MP2PS, "video/mp2p");
        addFileType("MPEG", FILE_TYPE_MP2PS, "video/mp2p");

        /// M: Add more Other popular file types to maps. {@
        addFileType("ICS", FILE_TYPE_ICS, "text/calendar");
        addFileType("ICZ", FILE_TYPE_ICZ, "text/calendar");
        addFileType("VCF", FILE_TYPE_VCF, "text/x-vcard");
        addFileType("VCS", FILE_TYPE_VCS, "text/x-vcalendar");
        addFileType("APK", FILE_TYPE_APK, "application/vnd.android.package-archive");
        addFileType("DOCX", FILE_TYPE_MS_WORD, "application/vnd.openxmlformats-officedocument.wordprocessingml.document");
        addFileType("DOTX", FILE_TYPE_MS_WORD, "application/vnd.openxmlformats-officedocument.wordprocessingml.template");
        addFileType("XLSX", FILE_TYPE_MS_EXCEL, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
        addFileType("XLTX", FILE_TYPE_MS_EXCEL, "application/vnd.openxmlformats-officedocument.spreadsheetml.template");
        addFileType("PPTX", FILE_TYPE_MS_POWERPOINT, "application/vnd.openxmlformats-officedocument.presentationml.presentation");
        addFileType("POTX", FILE_TYPE_MS_POWERPOINT, "application/vnd.openxmlformats-officedocument.presentationml.template");
        addFileType("PPSX", FILE_TYPE_MS_POWERPOINT, "application/vnd.openxmlformats-officedocument.presentationml.slideshow");
        /// @}
    }

根据不同的MIME类型,显示不同的图标:

    /**
     * This method gets the drawable id based on the mimetype
     *
     * @param mimeType the mimeType of a file/folder
     * @return the drawable icon id based on the mimetype
     */
    public static int getDrawableId(Context context, String mimeType) {
        if (TextUtils.isEmpty(mimeType)) {
            return R.drawable.fm_unknown;
        } else if (mimeType.startsWith("application/vnd.android.package-archive")) {
            return R.drawable.fm_apk;
        } else if (mimeType.startsWith("application/zip")) {
            return R.drawable.fm_zip;
        } else if (mimeType.startsWith("application/ogg")) {
            return R.drawable.fm_audio;
        } else if (mimeType.startsWith("audio/")) {
            return R.drawable.fm_audio;
        } else if (mimeType.startsWith("image/")) {
            return R.drawable.fm_picture;
        } else if (mimeType.startsWith("text/")) {
            return R.drawable.fm_txt;
        } else if (mimeType.startsWith("video/")) {
            return R.drawable.fm_video;
        }
        return getCustomDrawableId(context, mimeType);
    }

参考链接


android: 通过Intent筛选多种类型文件

Ubuntu 22.04使用Podman部署Tomcat 9的详细教程

安装必要的依赖:

# 安装 podman
$ sudo apt install podman

# 准备本地目录映射
$ mkdir /home/data/.tomcat

$ mkdir /home/data/.tomcat/webapps

$ mkdir /home/data/.tomcat/logs

官方镜像会在报错的时候暴露 Tomcat 9 版本号,错误堆栈,构成安全隐患,我们需要通过构建自定义镜像解决此问题:

$ touch Dockerfile

内容如下:

# 详细启动日志在 /usr/local/tomcat/logs 目录下

From docker.io/library/tomcat:9

# xml 编辑工具,方便我们后续修改 tomcat 的xml配置文件
# RUN apt update && apt install -y xmlstarlet

# 增加 <Valve className="org.apache.catalina.valves.ErrorReportValve" showReport="false" showServerInfo="false" /> 阻止错误日志输出

RUN sed -i "s/<\/Host>/  <Valve className='org.apache.catalina.valves.ErrorReportValve' showReport='false' showServerInfo='false' \/>\n\t<\/Host>/" /usr/local/tomcat/conf/server.xml

# 配置AJP协议
# 如果只希望通过 AJP访问,可以参考如下命令 移除原有的 AJP 协议配置
# RUN xmlstarlet ed -L -P -S -d '/Server/Service/Connector' /usr/local/tomcat/conf/server.xml

# 增加新的协议配置
RUN sed -i "s/<\/Service>/  <Connector port='8009' protocol='AJP\/1.3' address='0.0.0.0' redirectPort='8443' secretRequired=''\/>\n  <\/Service>/" /usr/local/tomcat/conf/server.xml

# 移除 xml 编辑工具
# RUN apt -y autoremove --purge xmlstarlet

构建镜像:

$ sudo podman build -t tomcat-9 .

设置容器开机自启:

$ sudo podman stop tomcat-9

$ sudo podman rm tomcat-9

# 启动一个 tomcat 容器 8080 HTTP 访问端口 8009 AJP访问端口
# sudo podman run -d --name tomcat-9 -p 8080:8080 -p 8009:8009 -v /my/local/path:/usr/local/tomcat/webapps localhost/tomcat-9

$ sudo podman run -d --name tomcat-9 -p 8080:8080 -p 8009:8009 -v /home/data/.tomcat/webapps:/usr/local/tomcat/webapps -v /home/data/.tomcat/logs:/usr/local/tomcat/logs localhost/tomcat-9

# 查看该容器
$ sudo podman ps

# 如果需要进入容器查看执行情况,参考如下命令
# sudo podman exec -it tomcat-9 bash

# 每次都启动新容器方式创建servcie //--new参数,每次启动都删除旧容器,启动一个新容器 
$ sudo podman generate systemd --restart-policy=always -n --new -f tomcat-9

查看启动文件:

$ cat container-tomcat-9.service

内容如下:

# container-tomcat-9.service
# autogenerated by Podman 3.4.4
# Sun Mar 10 12:31:31 CST 2024

[Unit]
Description=Podman container-tomcat-9.service
Documentation=man:podman-generate-systemd(1)
Wants=network-online.target
After=network-online.target
RequiresMountsFor=%t/containers

[Service]
Environment=PODMAN_SYSTEMD_UNIT=%n
Restart=always
TimeoutStopSec=70
ExecStartPre=/bin/rm -f %t/%n.ctr-id
ExecStart=/usr/bin/podman run --cidfile=%t/%n.ctr-id --cgroups=no-conmon --rm --sdnotify=conmon --replace -d --name tomcat-9 -p 8080:8080 -p 8009:8009 -v /home/data/.tomcat/webapps:/usr/local/tomcat/webapps -v /home/data/.tomcat/logs:/usr/local/tomcat/logs localhost/tomcat-9
ExecStop=/usr/bin/podman stop --ignore --cidfile=%t/%n.ctr-id
ExecStopPost=/usr/bin/podman rm -f --ignore --cidfile=%t/%n.ctr-id
Type=notify
NotifyAccess=all

[Install]
WantedBy=default.target

Systemd 配置:

# 保存到/etc/systemd/system/
$ sudo mv container-tomcat-9.service /etc/systemd/system/

# 刷新配置文件,让其生效
$ sudo systemctl daemon-reload

# 设置容器开机自启,并且现在启动
$ sudo systemctl enable --now /etc/systemd/system/container-tomcat-9.service

# 如果需要进入容器查看执行情况,参考如下命令
# sudo podman exec -it tomcat-9 bash

# 测试,重启虚拟机
$ sudo reboot

# 启动或重启服务 
# systemctl --user start container-tomcat-9.service 
# systemctl --user restart container-tomcat-9.service

# 如果启动失败,观察服务日志
# sudo journalctl -f

后续 WAR 包存储到 /home/data/.tomcat/webapps 目录下即可进行正常访问。

参考链接


macOS Sonoma 14.3.1 安装包Apple官网下载集合

macOS Sonoma Final Full Installer

macOS Sonoma Final Version Build App Avail Date
InstallAssistant.pkg 14.3.1 23D60   YES 2/08
InstallAssistant.pkg 14.3 23D56   YES 1/22
InstallAssistant.pkg 14.2.1 23C71   YES 12/19
InstallAssistant.pkg 14.2 23C64   YES 12/11
InstallAssistant.pkg 14.1.2 23B92   YES 11/30
InstallAssistant.pkg 14.1.2 23B2091 M3 only YES 11/30
^ For M3 Macs ONLY
InstallAssistant.pkg 14.1.1 23B81 19.1.02 YES 11/07
InstallAssistant.pkg 14.1.1 23B2082 M3 only YES 11/07
^ For M3 Macs ONLY
InstallAssistant.pkg 14.1 23B74 19.1.01 YES 10/25
InstallAssistant.pkg 14.1 23B2077 M3 only YES 11/01
^ For M3 Macs ONLY
InstallAssistant.pkg 14.0 22A344 19.0.02 YES 9/26

参考链接


Synology群晖NAS如何安装iperf3内网测速工具 非Docker运行模式

几经搜索,发现了群晖的Diagnosis Tool工具,里面有78个工具,其中就包括iperf3,方法很简单。

群晖NAS内网速度测试

  1. SSH登录群晖NAS,并切换到管理员权限
    $ sudo -i
  2. 安装Diagnosis Tool,
    $ sudo synogear install
  3. 运行
    $ /var/packages/DiagnosisTool/target/tool/iperf3 -c 192.168.188.1

群晖NAS外网速度测试

  • 运行:/var/packages/DiagnosisTool/target/tool/speedtest-cli.py
  • 会在结果可以找到Download: 138.89 Mbit/s;Upload: 63.01 Mbit/s(这是我千兆移动的测试结果)

如果不想使用synogear了, 一个命令就可移除:

$ sudo synogear remove

78个群晖的Diagnosis Tool工具命令行参考

/var/packages/DiagnosisTool/target/tool/addr2name
/var/packages/DiagnosisTool/target/tool/arping
/var/packages/DiagnosisTool/target/tool/ash
/var/packages/DiagnosisTool/target/tool/bash
/var/packages/DiagnosisTool/target/tool/cifsiostat
/var/packages/DiagnosisTool/target/tool/clockdiff
/var/packages/DiagnosisTool/target/tool/dig
/var/packages/DiagnosisTool/target/tool/domain_test.sh
/var/packages/DiagnosisTool/target/tool/file
/var/packages/DiagnosisTool/target/tool/fix_idmap.sh
/var/packages/DiagnosisTool/target/tool/free
/var/packages/DiagnosisTool/target/tool/gcore
/var/packages/DiagnosisTool/target/tool/gdb
/var/packages/DiagnosisTool/target/tool/gdbserver
/var/packages/DiagnosisTool/target/tool/iftop
/var/packages/DiagnosisTool/target/tool/iostat
/var/packages/DiagnosisTool/target/tool/iotop
/var/packages/DiagnosisTool/target/tool/iperf
/var/packages/DiagnosisTool/target/tool/iperf3
/var/packages/DiagnosisTool/target/tool/kill
/var/packages/DiagnosisTool/target/tool/killall
/var/packages/DiagnosisTool/target/tool/ldd
/var/packages/DiagnosisTool/target/tool/lsof
/var/packages/DiagnosisTool/target/tool/ltrace
/var/packages/DiagnosisTool/target/tool/mpstat
/var/packages/DiagnosisTool/target/tool/name2addr
/var/packages/DiagnosisTool/target/tool/ncat
/var/packages/DiagnosisTool/target/tool/ndisc6
/var/packages/DiagnosisTool/target/tool/nethogs
/var/packages/DiagnosisTool/target/tool/nfsiostat-sysstat
/var/packages/DiagnosisTool/target/tool/nmap
/var/packages/DiagnosisTool/target/tool/nping
/var/packages/DiagnosisTool/target/tool/nslookup
/var/packages/DiagnosisTool/target/tool/perf-check.py
/var/packages/DiagnosisTool/target/tool/pgrep
/var/packages/DiagnosisTool/target/tool/pidof
/var/packages/DiagnosisTool/target/tool/pidstat
/var/packages/DiagnosisTool/target/tool/ping6
/var/packages/DiagnosisTool/target/tool/ping
/var/packages/DiagnosisTool/target/tool/pkill
/var/packages/DiagnosisTool/target/tool/pmap
/var/packages/DiagnosisTool/target/tool/ps
/var/packages/DiagnosisTool/target/tool/pstree
/var/packages/DiagnosisTool/target/tool/pwdx
/var/packages/DiagnosisTool/target/tool/rarpd
/var/packages/DiagnosisTool/target/tool/rdisc6
/var/packages/DiagnosisTool/target/tool/rdisc
/var/packages/DiagnosisTool/target/tool/rltraceroute6
/var/packages/DiagnosisTool/target/tool/sa1
/var/packages/DiagnosisTool/target/tool/sa2
/var/packages/DiagnosisTool/target/tool/sadc
/var/packages/DiagnosisTool/target/tool/sadf
/var/packages/DiagnosisTool/target/tool/sar
/var/packages/DiagnosisTool/target/tool/sh
/var/packages/DiagnosisTool/target/tool/sid2ugid.sh
/var/packages/DiagnosisTool/target/tool/slabtop
/var/packages/DiagnosisTool/target/tool/sockstat
/var/packages/DiagnosisTool/target/tool/speedtest-cli.py
/var/packages/DiagnosisTool/target/tool/strace
/var/packages/DiagnosisTool/target/tool/sysctl
/var/packages/DiagnosisTool/target/tool/sysstat
/var/packages/DiagnosisTool/target/tool/tcpdump
/var/packages/DiagnosisTool/target/tool/tcpdump_wrapper
/var/packages/DiagnosisTool/target/tool/tcpspray6
/var/packages/DiagnosisTool/target/tool/tcptraceroute6
/var/packages/DiagnosisTool/target/tool/telnet
/var/packages/DiagnosisTool/target/tool/tload
/var/packages/DiagnosisTool/target/tool/top
/var/packages/DiagnosisTool/target/tool/tracepath
/var/packages/DiagnosisTool/target/tool/traceroute6
/var/packages/DiagnosisTool/target/tool/tracert6
/var/packages/DiagnosisTool/target/tool/uptime
/var/packages/DiagnosisTool/target/tool/vmstat
/var/packages/DiagnosisTool/target/tool/w
/var/packages/DiagnosisTool/target/tool/watch
/var/packages/DiagnosisTool/target/tool/zblacklist
/var/packages/DiagnosisTool/target/tool/zmap
/var/packages/DiagnosisTool/target/tool/ztee

参考链接


Java—bouncycastle支持国密SM2的公钥加密算法

java代码是依赖 BouncyCastle 类库,经修改此类库中的 SM2Engine 类的原码而来,用于支持 SM2 公钥加密算法,符合《GB/T 35276-2017: 信息安全技术 SM2密码算法使用规范》。

SM4 国标《GB/T 32907-2016 信息安全技术 SM4分组密码算法》。

可以使用 gmssl 工具进行交互测试(http://gmssl.org)

引入jar:

<dependency>
  <groupId>org.bouncycastle</groupId>
  <artifactId>bcprov-jdk15on</artifactId>
  <version>1.68</version>
</dependency>

加解密工具类:

import java.io.IOException;
import java.math.BigInteger;
import java.security.SecureRandom;

import org.bouncycastle.asn1.ASN1EncodableVector;
import org.bouncycastle.asn1.ASN1InputStream;
import org.bouncycastle.asn1.ASN1Integer;
import org.bouncycastle.asn1.ASN1OctetString;
import org.bouncycastle.asn1.ASN1Sequence;
import org.bouncycastle.asn1.DEROctetString;
import org.bouncycastle.asn1.DERSequence;
import org.bouncycastle.crypto.CipherParameters;
import org.bouncycastle.crypto.Digest;
import org.bouncycastle.crypto.InvalidCipherTextException;
import org.bouncycastle.crypto.digests.SM3Digest;
import org.bouncycastle.crypto.params.ECDomainParameters;
import org.bouncycastle.crypto.params.ECKeyParameters;
import org.bouncycastle.crypto.params.ECPrivateKeyParameters;
import org.bouncycastle.crypto.params.ECPublicKeyParameters;
import org.bouncycastle.crypto.params.ParametersWithRandom;
import org.bouncycastle.math.ec.ECFieldElement;
import org.bouncycastle.math.ec.ECMultiplier;
import org.bouncycastle.math.ec.ECPoint;
import org.bouncycastle.math.ec.FixedPointCombMultiplier;
import org.bouncycastle.util.Arrays;
import org.bouncycastle.util.BigIntegers;
import org.bouncycastle.util.Memoable;
import org.bouncycastle.util.Pack;


/**
 * 自义的 SM2 公钥加密、私钥解密引擎, 用于替换  BouncyCastle 中的 SM2Engine 的实现,
 * 可用于非 java 开发的系统之间交换数据时的公钥加密、私钥解密,完全符合 GM/T 以下两个标准:
 * <br/>
 * <li>《GM/T 0003.4-2012: SM2椭圆曲线公钥密钥算法:第4部分:公钥加密算法》</li>
 * <li>《GM/T 0009-2012: SM2密码算法使用规范》</li>
 * <br/>
 * <p/>
 * <li>1.加密密文默认为 C1||C3||C2, 输出内容为 ASN.1 编码。符合 《GM/T 0009-2012: SM2密码算法使用规范》 标准
 * </li>
 * <li>2.加密密文设置为 C1||C2||C3,则输出内容不是 ASN.1 编码。与 《GM/T 0009-2012 ...》 标准不兼容。</li>
 * <br/>
 * 建议可以使用 GmSSL 工具进行交互测试,请参考 {@code http://gmssl.org}
 * <p>
 * SM2 public key encryption engine - based on https://tools.ietf.org/html/draft-shen-sm2-ecdsa-02.
 *
 *
 * @author YangHongFeng
 * @since 2020/5/28 creation
 */
public class MySm2Engine {

    public enum Mode {
        C1C2C3, C1C3C2;
    }

    private final Digest digest;
    private final Mode mode;

    private boolean forEncryption;
    private ECKeyParameters ecKey;
    private ECDomainParameters ecParams;
    private int curveLength;
    private SecureRandom random;

    /***
     * 默认采用国标:C1||C3||C2
     */
    public MySm2Engine() {
        this(new SM3Digest(), Mode.C1C3C2);
    }

    public MySm2Engine(Mode mode) {
        this(new SM3Digest(), mode);
    }

    public MySm2Engine(Digest digest) {
        this(digest, Mode.C1C2C3);
    }

    public MySm2Engine(Digest digest, Mode mode) {
        if (mode == null) {
            throw new IllegalArgumentException("mode cannot be NULL");
        }
        this.digest = digest;
        this.mode = mode;
    }

    /**
     * 初始化
     * @param forEncryption true-公钥加密, false-私钥解密
     * @param param 密码参数,从中获取公或私钥、及椭圆曲线相关参数
     */
    public void init(boolean forEncryption, CipherParameters param) {
        this.forEncryption = forEncryption;

        if (forEncryption) {
            ParametersWithRandom rParam = (ParametersWithRandom) param;

            ecKey = (ECKeyParameters) rParam.getParameters();
            ecParams = ecKey.getParameters();

            ECPoint s = ((ECPublicKeyParameters) ecKey).getQ().multiply(ecParams.getH());
            if (s.isInfinity()) {
                throw new IllegalArgumentException("invalid key: [h]Q at infinity");
            }

            random = rParam.getRandom();
        } else {
            ecKey = (ECKeyParameters) param;
            ecParams = ecKey.getParameters();
        }

        curveLength = (ecParams.getCurve().getFieldSize() + 7) / 8;
    }

    /**
     * 进行加密、或解密
     * @param in
     * @param inOff
     * @param inLen
     * @return
     * @throws InvalidCipherTextException
     */
    public byte[] processBlock(
            byte[] in,
            int inOff,
            int inLen)
            throws IOException, InvalidCipherTextException {
        if (forEncryption) {
            return encrypt(in, inOff, inLen);
        } else {
            return decrypt(in, inOff, inLen);
        }
    }

    public int getOutputSize(int inputLen) {
        return (1 + 2 * curveLength) + inputLen + digest.getDigestSize();
    }

    protected ECMultiplier createBasePointMultiplier() {
        return new FixedPointCombMultiplier();
    }

    private byte[] encrypt(byte[] in, int inOff, int inLen)
            throws IOException {
        byte[] c2 = new byte[inLen];

        System.arraycopy(in, inOff, c2, 0, c2.length);

        ECMultiplier multiplier = createBasePointMultiplier();

        ECPoint c1P;
        ECPoint kPB;
        do {
            BigInteger k = nextK();

            c1P = multiplier.multiply(ecParams.getG(), k).normalize();
            // c1 = c1P.getEncoded(false);

            kPB = ((ECPublicKeyParameters) ecKey).getQ().multiply(k).normalize();

            kdf(digest, kPB, c2);
        }

        while (notEncrypted(c2, in, inOff));

        byte[] c3 = new byte[digest.getDigestSize()];

        addFieldElement(digest, kPB.getAffineXCoord());
        digest.update(in, inOff, inLen);
        addFieldElement(digest, kPB.getAffineYCoord());

        digest.doFinal(c3, 0);

        switch (mode) {
        case C1C3C2:
            // 2020/06/01 按国标组装为 ANS.1 编码
            final ASN1EncodableVector vector = new ASN1EncodableVector();
            vector.add(new ASN1Integer(c1P.getXCoord().toBigInteger()));
            vector.add(new ASN1Integer(c1P.getYCoord().toBigInteger()));
            vector.add(new DEROctetString(c3));
            vector.add(new DEROctetString(c2));
            return new DERSequence(vector).getEncoded();
        default:
            byte[] c1 = c1P.getEncoded(false);
            return Arrays.concatenate(c1, c2, c3);
        }
    }

    private byte[] decrypt(byte[] in, int inOff, int inLen)
            throws InvalidCipherTextException, IOException {

        ECPoint c1P; // = ecParams.getCurve().decodePoint(c1);
        byte[] inHash ;
        byte[] inCipherData;

        if (mode == Mode.C1C3C2) {
            // 2020/06/01 按国标 ANS.1 编码 进行解码
            ASN1InputStream inputStream = new ASN1InputStream(in);
            ASN1Sequence seq = (ASN1Sequence) inputStream.readObject();
            if (seq.size() != 4){
                throw new InvalidCipherTextException("invalid cipher text");
            }
            int index = 0;

            // C1 == XCoordinate 、YCoordinate
            BigInteger x = ((ASN1Integer) seq.getObjectAt(index ++)).getPositiveValue();
            // YCoordinate
            BigInteger y = ((ASN1Integer) seq.getObjectAt(index ++)).getPositiveValue();

            // XCoord 、YCoord ==> CEPoint (C1)
            c1P = ecParams.getCurve().createPoint(x, y);

            // HASH (C3)
            inHash = ((ASN1OctetString)seq.getObjectAt(index ++)).getOctets();

            // CipherText (C2)
            inCipherData = ((ASN1OctetString)seq.getObjectAt(index)).getOctets();
        } else {
            // C1
            byte[] c1 = new byte[curveLength * 2 + 1];
            System.arraycopy(in, inOff, c1, 0, c1.length);
            c1P = ecParams.getCurve().decodePoint(c1);

            // C2 == inCipherData
            int digestSize = this.digest.getDigestSize();
            inCipherData = new byte[inLen - c1.length - digestSize];
            System.arraycopy(in, inOff + c1.length, inCipherData, 0, inCipherData.length);

            // C3 == Hash
            inHash = new byte[digestSize];
            System.arraycopy(in, inOff + c1.length + inCipherData.length, inHash, 0, inHash.length);
        }

        // 解密 ==> inCipherData;
        ECPoint s = c1P.multiply(ecParams.getH());
        if (s.isInfinity())
        {
            throw new InvalidCipherTextException("[h]C1 at infinity");
        }
        c1P = c1P.multiply(((ECPrivateKeyParameters)ecKey).getD()).normalize();
        kdf(digest, c1P, inCipherData);

        // 动态计算已解密的明文的摘要并比较
        byte[] cipherDigest = new byte[digest.getDigestSize()];

        addFieldElement(digest, c1P.getAffineXCoord());
        digest.update(inCipherData, 0, inCipherData.length);
        addFieldElement(digest, c1P.getAffineYCoord());

        digest.doFinal(cipherDigest, 0);

        int check = 0;
        if (mode == Mode.C1C3C2)
        {
            for (int i = 0; i != cipherDigest.length; i++) {
                check |= cipherDigest[i] ^ inHash[i];
            }
        } else {
            for (int i = 0; i != cipherDigest.length; i++) {
                check |= cipherDigest[i] ^ inHash[i];
            }
        }

        // Arrays.fill(c1, (byte)0);
        Arrays.fill(cipherDigest, (byte)0);

        if (check != 0)
        {
            Arrays.fill(inCipherData, (byte)0);
            throw new InvalidCipherTextException("invalid cipher text");
        }

        // return c2;
        return inCipherData;
    }

    private boolean notEncrypted(byte[] encData, byte[] in, int inOff) {
        for (int i = 0; i != encData.length; i++) {
            if (encData[i] != in[inOff + i]) {
                return false;
            }
        }

        return true;
    }

    private void kdf(Digest digest, ECPoint c1, byte[] encData) {
        int digestSize = digest.getDigestSize();
        byte[] buf = new byte[Math.max(4, digestSize)];
        int off = 0;

        Memoable memo = null;
        Memoable copy = null;

        if (digest instanceof Memoable) {
            addFieldElement(digest, c1.getAffineXCoord());
            addFieldElement(digest, c1.getAffineYCoord());
            memo = (Memoable) digest;
            copy = memo.copy();
        }

        int ct = 0;

        while (off < encData.length) {
            if (memo != null) {
                memo.reset(copy);
            } else {
                addFieldElement(digest, c1.getAffineXCoord());
                addFieldElement(digest, c1.getAffineYCoord());
            }

            Pack.intToBigEndian(++ct, buf, 0);
            digest.update(buf, 0, 4);
            digest.doFinal(buf, 0);

            int xorLen = Math.min(digestSize, encData.length - off);
            xor(encData, buf, off, xorLen);
            off += xorLen;
        }
    }

    private void xor(byte[] data, byte[] kdfOut, int dOff, int dRemaining) {
        for (int i = 0; i != dRemaining; i++) {
            data[dOff + i] ^= kdfOut[i];
        }
    }

    private BigInteger nextK() {
        int qBitLength = ecParams.getN().bitLength();

        BigInteger k;
        do {
            k = BigIntegers.createRandomBigInteger(qBitLength, random);
        }
        while (k.equals(BigIntegers.ZERO) || k.compareTo(ecParams.getN()) >= 0);

        return k;
    }

    private void addFieldElement(Digest digest, ECFieldElement v) {
        byte[] p = BigIntegers.asUnsignedByteArray(curveLength, v.toBigInteger());

        digest.update(p, 0, p.length);
    }
}

工具类:

import org.bouncycastle.asn1.gm.GMNamedCurves;
import org.bouncycastle.asn1.x9.X9ECParameters;
import org.bouncycastle.crypto.InvalidCipherTextException;
import org.bouncycastle.crypto.params.ECDomainParameters;
import org.bouncycastle.crypto.params.ECPrivateKeyParameters;
import org.bouncycastle.crypto.params.ECPublicKeyParameters;
import org.bouncycastle.crypto.params.ParametersWithRandom;
import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPrivateKey;
import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.jce.spec.ECParameterSpec;
import org.bouncycastle.jce.spec.ECPrivateKeySpec;
import org.bouncycastle.jce.spec.ECPublicKeySpec;
import org.bouncycastle.util.encoders.Hex;

import java.io.IOException;
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.SecureRandom;


public class SM2Util {
    //椭圆曲线ECParameters ASN.1 结构
    private static final X9ECParameters x9ECParameters = GMNamedCurves.getByName("sm2p256v1");
    //椭圆曲线公钥或私钥的基本域参数。
    private static final ECParameterSpec ecDomainParameters = new ECParameterSpec(x9ECParameters.getCurve(), x9ECParameters.getG(), x9ECParameters.getN());

    /**
     * 公钥字符串转换为 BCECPublicKey 公钥对象
     *
     * @param pubKeyHex 64字节十六进制公钥字符串(如果公钥字符串为65字节首个字节为0x04:表示该公钥为非压缩格式,操作时需要删除)
     * @return BCECPublicKey SM2公钥对象
     */
    public static BCECPublicKey getECPublicKeyByPublicKeyHex(String pubKeyHex) {
        //截取64字节有效的SM2公钥(如果公钥首个字节为0x04)
        if (pubKeyHex.length() > 128) {
            pubKeyHex = pubKeyHex.substring(pubKeyHex.length() - 128);
        }
        //将公钥拆分为x,y分量(各32字节)
        String stringX = pubKeyHex.substring(0, 64);
        String stringY = pubKeyHex.substring(stringX.length());
        //将公钥x、y分量转换为BigInteger类型
        BigInteger x = new BigInteger(stringX, 16);
        BigInteger y = new BigInteger(stringY, 16);
        //通过公钥x、y分量创建椭圆曲线公钥规范
        ECPublicKeySpec ecPublicKeySpec = new ECPublicKeySpec(x9ECParameters.getCurve().createPoint(x, y), ecDomainParameters);
        //通过椭圆曲线公钥规范,创建出椭圆曲线公钥对象(可用于SM2加密及验签)
        return new BCECPublicKey("EC", ecPublicKeySpec, BouncyCastleProvider.CONFIGURATION);
    }


    /**
     * 私钥字符串转换为 BCECPrivateKey 私钥对象
     *
     * @param privateKeyHex: 32字节十六进制私钥字符串
     * @return BCECPrivateKey:SM2私钥对象
     */
    public static BCECPrivateKey getBCECPrivateKeyByPrivateKeyHex(String privateKeyHex) {
        //将十六进制私钥字符串转换为BigInteger对象
        BigInteger d = new BigInteger(privateKeyHex, 16);
        //通过私钥和私钥域参数集创建椭圆曲线私钥规范
        ECPrivateKeySpec ecPrivateKeySpec = new ECPrivateKeySpec(d, ecDomainParameters);
        //通过椭圆曲线私钥规范,创建出椭圆曲线私钥对象(可用于SM2解密和签名)
        return new BCECPrivateKey("EC", ecPrivateKeySpec, BouncyCastleProvider.CONFIGURATION);
    }

    /**
     * SM2加密算法
     *
     * @param parameters 公钥参数
     * @param data       明文数据
     */
    public static String encrypt(ECPublicKeyParameters parameters, String data) throws InvalidCipherTextException, IOException {
        MySm2Engine sm2Engine = new MySm2Engine();
        sm2Engine.init(true, new ParametersWithRandom(parameters, new SecureRandom()));

        byte[] in = data.getBytes(StandardCharsets.UTF_8);
        return Hex.toHexString(sm2Engine.processBlock(in, 0, in.length));
    }

    /**
     * SM2加密算法
     *
     * @param publicKey 公钥
     * @param data      明文数据
     */
    public static String encrypt(PublicKey publicKey, String data) throws InvalidCipherTextException, IOException {
        ECPublicKeyParameters ecPublicKeyParameters = null;
        if (publicKey instanceof BCECPublicKey) {
            BCECPublicKey bcecPublicKey = (BCECPublicKey) publicKey;
            ECParameterSpec ecParameterSpec = bcecPublicKey.getParameters();
            ECDomainParameters ecDomainParameters = new ECDomainParameters(ecParameterSpec.getCurve(),
                    ecParameterSpec.getG(), ecParameterSpec.getN());
            ecPublicKeyParameters = new ECPublicKeyParameters(bcecPublicKey.getQ(), ecDomainParameters);
        }

        return encrypt(ecPublicKeyParameters, data);
    }

    /**
     * SM2解密算法
     *
     * @param privateKey 私钥
     * @param cipherData 密文数据
     */
    public static String decrypt(PrivateKey privateKey, String cipherData) throws InvalidCipherTextException, IOException {
        byte[] cipherDataByte = Hex.decode(cipherData);

        BCECPrivateKey bcecPrivateKey = (BCECPrivateKey) privateKey;
        ECParameterSpec ecParameterSpec = bcecPrivateKey.getParameters();

        ECDomainParameters ecDomainParameters = new ECDomainParameters(ecParameterSpec.getCurve(),
                ecParameterSpec.getG(), ecParameterSpec.getN());

        ECPrivateKeyParameters ecPrivateKeyParameters = new ECPrivateKeyParameters(bcecPrivateKey.getD(),
                ecDomainParameters);

        MySm2Engine sm2Engine = new MySm2Engine();
        sm2Engine.init(false, ecPrivateKeyParameters);

        byte[] arrayOfBytes = sm2Engine.processBlock(cipherDataByte, 0, cipherDataByte.length);
        return new String(arrayOfBytes, StandardCharsets.UTF_8);
    }

}

测试例子:

import org.bouncycastle.crypto.InvalidCipherTextException;
import org.bouncycastle.jce.provider.BouncyCastleProvider;

import java.io.IOException;
import java.security.InvalidAlgorithmParameterException;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.spec.ECGenParameterSpec;

public class sm2_demo {

    public static void main(String[] args) throws NoSuchAlgorithmException, InvalidAlgorithmParameterException, InvalidCipherTextException, IOException {

        final ECGenParameterSpec sm2Spec = new ECGenParameterSpec("sm2p256v1");
        // 获取一个椭圆曲线类型的密钥对生成器
        final KeyPairGenerator kpg = KeyPairGenerator.getInstance("EC", new BouncyCastleProvider());
        // 使用SM2参数初始化生成器
        kpg.initialize(sm2Spec);
        // 获取密钥对
        KeyPair keyPair = kpg.generateKeyPair();
        PublicKey publicKey = keyPair.getPublic();
        PrivateKey privateKey = keyPair.getPrivate();
        String m = "lzc_SM2_demo";
        String data = SM2Util.encrypt(publicKey, m);
        System.out.println(data);
        String text = SM2Util.decrypt(privateKey, data);
        System.out.println(text);
    }
}

参考链接


为容器添加基于BoxDecoration的labelText (作为TextField)

TextField 有一个很好的方法来在它的 BoxDecoration 装饰上放置一个文本标签,如下:

通过以下方式:

TextField(
        onTap: onTap,
        controller: controller,
        decoration: InputDecoration(
          labelText: "XP",
          border: OutlineInputBorder(
            borderRadius: BorderRadius.circular(50.0),
          ),
        ));

有没有办法在 Container 的其他 BoxDecoration 上实现同样的效果?例如,我想指定标签"XP":

        Container(
          decoration: BoxDecoration(
            borderRadius: BorderRadius.all(Radius.circular(5)),
            border: Border.all(color: color, width: 2.0),
            labelText: Text("XP"),   // No such attribute
          ),
          child: child,
        ),

但是 Flutter 没有为 BoxDecoration 提供 labelText (仅存在于 InputDecoration )。

我们可以使用 InputDecorator 作为父对象来完成上述需求,如下:

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const Home(),
      debugShowCheckedModeBanner: false,
    );
  }
}

class Home extends StatefulWidget {
  const Home({super.key});

  @override
  State<Home> createState() => _HomeState();
}

class _HomeState extends State<Home> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Container(
          height: 100,
          width: 400,
          child: InputDecorator(
            decoration: InputDecoration(
                labelText: 'XP',
                border: OutlineInputBorder(
                  borderRadius: BorderRadius.circular(10),
                )),
            child: Text("Container Content"),
          ),
        ),
      ),
    );
  }
}

输出

参考链接


Flutter TextField/TextFormField垂直方向完整填充父容器高度(或者全屏显示)

正常情况下,我们使用 TextField/TextFormField 的时候,都是单行的表单或者指定行数的情况。但是有时候,我们希望能自适应父容器的高度,或者高度动态自适应变化。

那么我们应该怎么操作呢?

继续阅读Flutter TextField/TextFormField垂直方向完整填充父容器高度(或者全屏显示)

基于 Verdaccio 搭建鸿蒙(HarmonyOS Next)开发的轻量级 Node.js 私有仓库

一、背景

最近在进行 HarmonyOS Next 应用开发,官方的 DevEco Studio 4.1 需要时候 Node.js,但是公司开发环境不支持外网访问,需要搭建内网的镜像服务器。下面,我们研究在内网服务器只使用 Apache(HTTPD)/Nginx 提供文件下载服务,不安装 NodeJs 搭建代理服务的方法来建立 NPM 文件下载代理。

执行缓存任务的设备是 MacBook Pro 2023 / macOS Sonoma 14.3

二、简介

1. 什么是 Verdaccio

“一个基于 Node.js 的轻量级私有仓库”。
平时使用 npm publish 进行发布时,上传的仓库默认地址是 npm,通过 Verdaccio 工具在本地新建一个仓库地址,再把本地的默认上传仓库地址切换到本地仓库地址即可。当 npm install 时没有找到本地的仓库,则 Verdaccio 默认配置中会从 npm 中央仓库下载。

注:Verdaccio 表示意大利中世纪晚期 fresco 绘画中流行的一种绿色的意思。

2. 优点
  • 私密性高,仅团队共享。
  • 安全性高,能够有效的防治恶意代码攻击。
  • 使用局域网,传输速度快。
3. 官网

三、准备环境

# 我们通过 nvm 管理 node 进行多版本切换 
$ brew install nvm

# 加载并且列出远程的 node 分支,否则执行 `nvm list` `nvm install` 等命令的
# 时候没办法列出或者安装对应的版本

$ nvm ls-remote

# 但是我们使用最新版本的 node 执行安装操作,这样才能干净的进行缓存
# 否则在我们后续安装 verdaccio 的时候,数据是已经缓存过的了
$ nvm install 20.11.0

# 没有特殊情况下,建议把刚刚安装的版本设置为默认版本
$ nvm alias default 20.11.0

# 安装 verdaccio
$ npm install --location=global verdaccio

# 全局安裝 npm 源管理工具(可以快速切换仓库源)

$ npm install --location=global nrm

# 添加一个私有 npm 源,'verdaccio' 为自定义的源地址名称 
$ nrm add verdaccio http://localhost:4873/

# 如果需要还原到默认设置,只需要删除当前用户下的 .nrmrc 文件即可

$ rm -rf .nrmrc

配置 verdaccio 从华为镜像服务器地址下载,默认配置服务器地址国内访问可能存在问题。另外注意禁用 npm-audit ,安全审计会非常非常慢,而且经常失败。

修改后的完整配置如下:

#
# This is the default configuration file. It allows all users to do anything,
# please read carefully the documentation and best practices to
# improve security.
#
# Look here for more config file examples:
# https://github.com/verdaccio/verdaccio/tree/5.x/conf
#
# Read about the best practices
# https://verdaccio.org/docs/best

# path to a directory with all packages
storage: /Users/xxxx/.local/share/verdaccio/storage
# path to a directory with plugins to include
plugins: ./plugins

# https://verdaccio.org/docs/webui
web:
  title: Verdaccio
  # comment out to disable gravatar support
  # gravatar: false
  # by default packages are ordercer ascendant (asc|desc)
  # sort_packages: asc
  # convert your UI to the dark side
  # darkMode: true
  # html_cache: true
  # by default all features are displayed
  # login: true
  # showInfo: true
  # showSettings: true
  # In combination with darkMode you can force specific theme
  # showThemeSwitch: true
  # showFooter: true
  # showSearch: true
  # showRaw: true
  # showDownloadTarball: true
  #  HTML tags injected after manifest <scripts/>
  # scriptsBodyAfter:
  #    - '<script type="text/javascript" src="https://my.company.com/customJS.min.js"></script>'
  #  HTML tags injected before ends </head>
  #  metaScripts:
  #    - '<script type="text/javascript" src="https://code.jquery.com/jquery-3.5.1.slim.min.js"></script>'
  #    - '<script type="text/javascript" src="https://browser.sentry-cdn.com/5.15.5/bundle.min.js"></script>'
  #    - '<meta name="robots" content="noindex" />'
  #  HTML tags injected first child at <body/>
  #  bodyBefore:
  #    - '<div id="myId">html before webpack scripts</div>'
  #  Public path for template manifest scripts (only manifest)
  #  publicPath: http://somedomain.org/

# https://verdaccio.org/docs/configuration#authentication
auth:
  htpasswd:
    file: ./htpasswd
    # Maximum amount of users allowed to register, defaults to "+inf".
    # You can set this to -1 to disable registration.
    # max_users: 1000
    # Hash algorithm, possible options are: "bcrypt", "md5", "sha1", "crypt".
    # algorithm: bcrypt # by default is crypt, but is recommended use bcrypt for new installations
    # Rounds number for "bcrypt", will be ignored for other algorithms.
    # rounds: 10

# https://verdaccio.org/docs/configuration#uplinks
# a list of other known repositories we can talk to
uplinks:
  # npmjs:
  #   url: https://registry.npmjs.org/
  # 注意依赖顺序,一定要把 ohpm 放在最前面,先去 ohpm 更新,有些依赖,两个镜像上都存在,后面会出现冲突的情况
  ohpm:
    url: https://ohpm.openharmony.cn/ohpm/  
  ohpm2:
    url: https://repo.harmonyos.com/ohpm/
  oh_npm:
    url: https://repo.harmonyos.com/npm/
  npm_mirror:
    url: https://mirrors.huaweicloud.com/repository/npm/

# Learn how to protect your packages
# https://verdaccio.org/docs/protect-your-dependencies/
# https://verdaccio.org/docs/configuration#packages
packages:
  '@*/*':
    # scoped packages
    access: $all
    publish: $authenticated
    unpublish: $authenticated
    proxy: ohpm oh_npm npm_mirror

  '**':
    # allow all users (including non-authenticated users) to read and
    # publish all packages
    #
    # you can specify usernames/groupnames (depending on your auth plugin)
    # and three keywords: "$all", "$anonymous", "$authenticated"
    access: $all

    # allow all known users to publish/publish packages
    # (anyone can register by default, remember?)
    publish: $authenticated
    unpublish: $authenticated

    # if package is not available locally, proxy requests to 'npmjs' registry
    proxy: ohpm oh_npm npm_mirror

# To improve your security configuration and  avoid dependency confusion
# consider removing the proxy property for private packages
# https://verdaccio.org/docs/best#remove-proxy-to-increase-security-at-private-packages

# https://verdaccio.org/docs/configuration#server
# You can specify HTTP/1.1 server keep alive timeout in seconds for incoming connections.
# A value of 0 makes the http server behave similarly to Node.js versions prior to 8.0.0, which did not have a keep-alive timeout.
# WORKAROUND: Through given configuration you can workaround following issue https://github.com/verdaccio/verdaccio/issues/301. Set to 0 in case 60 is not enough.
server:
  keepAliveTimeout: 60
  # Allow `req.ip` to resolve properly when Verdaccio is behind a proxy or load-balancer
  # See: https://expressjs.com/en/guide/behind-proxies.html
  # trustProxy: '127.0.0.1'

# https://verdaccio.org/docs/configuration#offline-publish
# publish:
#   allow_offline: false

# https://verdaccio.org/docs/configuration#url-prefix
# url_prefix: /verdaccio/
# VERDACCIO_PUBLIC_URL='https://somedomain.org';
# url_prefix: '/my_prefix'
# // url -> https://somedomain.org/my_prefix/
# VERDACCIO_PUBLIC_URL='https://somedomain.org';
# url_prefix: '/'
# // url -> https://somedomain.org/
# VERDACCIO_PUBLIC_URL='https://somedomain.org/first_prefix';
# url_prefix: '/second_prefix'
# // url -> https://somedomain.org/second_prefix/'

# https://verdaccio.org/docs/configuration#security
# security:
#   api:
#     legacy: true
#     jwt:
#       sign:
#         expiresIn: 29d
#       verify:
#         someProp: [value]
#    web:
#      sign:
#        expiresIn: 1h # 1 hour by default
#      verify:
#         someProp: [value]

# https://verdaccio.org/docs/configuration#user-rate-limit
# userRateLimit:
#   windowMs: 50000
#   max: 1000

# https://verdaccio.org/docs/configuration#max-body-size
# max_body_size: 10mb

# https://verdaccio.org/docs/configuration#listen-port
# listen:
# - localhost:4873            # default value
# - http://localhost:4873     # same thing
# - 0.0.0.0:4873              # listen on all addresses (INADDR_ANY)
# - https://example.org:4873  # if you want to use https
# - "[::1]:4873"                # ipv6
# - unix:/tmp/verdaccio.sock    # unix socket

# The HTTPS configuration is useful if you do not consider use a HTTP Proxy
# https://verdaccio.org/docs/configuration#https
# https:
#   key: ./path/verdaccio-key.pem
#   cert: ./path/verdaccio-cert.pem
#   ca: ./path/verdaccio-csr.pem

# https://verdaccio.org/docs/configuration#proxy
# http_proxy: http://something.local/
# https_proxy: https://something.local/

# https://verdaccio.org/docs/configuration#notifications
# notify:
#   method: POST
#   headers: [{ "Content-Type": "application/json" }]
#   endpoint: https://usagge.hipchat.com/v2/room/3729485/notification?auth_token=mySecretToken
#   content: '{"color":"green","message":"New package published: * {{ name }}*","notify":true,"message_format":"text"}'

middlewares:
  audit:
    enabled: false

# https://verdaccio.org/docs/logger
# log settings
log: { type: stdout, format: pretty, level: http }
#experiments:
#  # support for npm token command
#  token: false
#  # disable writing body size to logs, read more on ticket 1912
#  bytesin_off: false
#  # enable tarball URL redirect for hosting tarball with a different server, the tarball_url_redirect can be a template string
#  tarball_url_redirect: 'https://mycdn.com/verdaccio/${packageName}/${filename}'
#  # the tarball_url_redirect can be a function, takes packageName and filename and returns the url, when working with a js configuration file
#  tarball_url_redirect(packageName, filename) {
#    const signedUrl = // generate a signed url
#    return signedUrl;
#  }

# translate your registry, api i18n not available yet
# i18n:
# list of the available translations https://github.com/verdaccio/verdaccio/blob/master/packages/plugins/ui-theme/src/i18n/ABOUT_TRANSLATIONS.md
#   web: en-US

启动一个独立的Shell 运行 verdaccio 

$ nvm use 20.11.0

$ verdaccio                                
 info --- config file  - /Users/xxxx/.config/verdaccio/config.yaml
 info --- the "crypt" algorithm is deprecated consider switch to "bcrypt" in the configuration file. Read the documentation for additional details
 info --- using htpasswd file: /Users/xxxx/.config/verdaccio/htpasswd
 info --- plugin successfully loaded: verdaccio-htpasswd
 info --- plugin successfully loaded: verdaccio-audit
 warn --- http address - http://localhost:4873/ - verdaccio/5.29.0

清理缓存,并且要求通过 verdaccio 代理下载:

$ nvm use 20.11.0

# 'verdaccio' 为添加源时定义的源地址名称
$ nrm use verdaccio

# 列出缓存目录路径
$ npm config ls -l | grep cache

# 清理缓存
$ npm cache clean --force

当前(2024/02/01)申请并通过了华为开发计划的才可以下载到 HarmonyOS NEXT 开发需要的 HUAWEI DevEco Studio 4.x 版本(API 11)HUAWEI DevEco Studio 5.x (API 11、API 12) HarmonyOS Developer管理中心套件货架 目前只有这个版本的包含离线鸿蒙开发依赖 ohpm-repo,官方文档以及报错信息还是稀烂,基本找不到有用信息,需要自己研究。

注意: 目前测试发现 DevEco Studio 4.1.3.500 版本无法真机调试 C++ 代码。 DevEco Studio 4.1.3.501 版本可以正常调试。

截止 2024/04/01 最新Release版本是 DevEco Studio 4.1.3.700,最新测试版本 DevEco Studio 5.0.3.100(SP1),可惜编译不通过,报错如下:

ERR_PNPM_NO_MATCHING_VERSION  No matching version found for @ohos/hvigor-ohos-plugin@4.2.0

DevEco Studio 4.1.3.501 使用的官方SDK下载地址:

下载完成后,解压缩到 SDK 目录下的 HarmonyOS-NEXT-DP1 目录即可,如下图:

继续阅读基于 Verdaccio 搭建鸿蒙(HarmonyOS Next)开发的轻量级 Node.js 私有仓库

Flutter中AspectRatio、Card 卡片组件

1. AspectRatio 组件

AspectRatio 的作用是根据设置调整子元素 child 的宽高比。

AspectRatio 首先会在布局限制条件允许的范围内尽可能的扩展,widget 的高度是由宽度和比率决定的,类似于 BoxFit 中的 contain,按照固定比率去尽量占满区域。

如果在满足所有限制条件过后无法找到一个可行的尺寸,AspectRatio 最终将会去优先适应布局限制条件,而忽略所设置的比率。

常见属性:

1. aspectRatio 宽高比。页面最终也许不会按这个值去布局, 具体则要看综合因素,这只是一个参考值;

2. child 子组件。值的类型为Widget;

代码示例:

import 'package:flutter/material.dart';

void main(){
    runApp(MyApp());
}

// 抽离成一个单独的组件
class MyApp extends StatelessWidget{
    @override
    Widget build(BuildContext context) {
        return MaterialApp(
            home: Scaffold(
                // 导航条
                appBar:AppBar(title:Text('Flutter App')),
                // 主体
                body:HomeContent(),
            ),
            // 主题
            theme: ThemeData(primarySwatch:Colors.yellow),
        );
    }
}

// AspectRatio组件的用法
class HomeContent extends StatelessWidget{
    @override
    Widget build(BuildContext context) {
        return AspectRatio(
            // 高度与宽度的比值
            aspectRatio: 5.0/1.0 ,
            child:Container(
                color:Colors.red
            )
        );
    }
}

继续阅读Flutter中AspectRatio、Card 卡片组件