wenmo8 发布的文章

获取IP地址信息时,一般我们需要一个HttpServletRequest对象,然后从请求头里获取
x-forwarded-for
的值,而当我们使用dubbo+netty开发rest接口时,如果希望获取IP地址,HttpServletRequest是用不了的,你需要使用netty中的NettyRequestFacade对象,这时,你之前的获取IP地址的方法需要扩展,加一个只有入参不一样,代码逻辑完成一样的丑陋的,重复的代码;这无疑是一种代码的坏味道,应该想办法解决它。

适配器(包装器)

这种常用的设计模式在spring框架里用的非常多,如
ServletRequestAttributes
,它包装了HttpServletRequest和HttpServletResponse,用来添加额外的功能,如获取请求参数、session等信息。

为获取IP地址添加包装

自己的http请求包装器接口RequestWrapper

public interface RequestWrapper {

	String getHeader(String name);

	String getRemoteAddr();

}

HttpServletRequest的包装

public class HttpRequestWrapper implements RequestWrapper {

	private final HttpServletRequest request;

	public HttpRequestWrapper(HttpServletRequest request) {
		this.request = request;
	}

	@Override
	public String getHeader(String name) {
		return request.getHeader(name);
	}

	@Override
	public String getRemoteAddr() {
		return request.getRemoteAddr();
	}

}

NettyRequestFacade的包装

public class NettyRequestWrapper implements RequestWrapper {

	private final NettyRequestFacade request;

	public NettyRequestWrapper(NettyRequestFacade request) {
		this.request = request;
	}
	@Override
	public String getHeader(String name) {
		return request.getHeader(name);
	}

	@Override
	public String getRemoteAddr() {
		return request.getRemoteAddr();
	}

}

获取IP地址方法,入参是一个http包装器

public static String getIpAddr(RequestWrapper request) {
		String ip = request.getHeader("x-forwarded-for");
		if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
			ip = request.getHeader("Proxy-Client-IP");
		}
		if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
			ip = request.getHeader("WL-Proxy-Client-IP");
		}
		if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
			ip = request.getRemoteAddr();
			if (ip.equals("127.0.0.1")) {
				// 根据网卡取本机配置的IP
				InetAddress inet = null;
				try {
					inet = InetAddress.getLocalHost();
				}
				catch (UnknownHostException e) {
					e.printStackTrace();
				}
				ip = inet != null ? inet.getHostAddress() : ip;
			}
		}
		// 对于通过多个代理的情况,第一个IP为客户端真实IP,多个IP按照','分割
		if (ip != null && ip.length() > 15) {
			if (ip.indexOf(",") > 0) {
				ip = ip.substring(0, ip.indexOf(","));
			}
		}
		if ("0:0:0:0:0:0:0:1".equals(ip)) {
			ip = "127.0.0.1";
		}
		return ip;
	}

调用示例:

// 使用HttpServletRequest
public String getIpFromHttp(HttpServletRequest request) {
    return getIpAddr(new HttpRequestWrapper(request));
}

// 使用NettyRequestFacade
public String getIpFromNetty(NettyRequestFacade request) {
    return getIpAddr(new NettyRequestWrapper(request));
}


大家好,我是痞子衡,是正经搞技术的痞子。今天痞子衡给大家介绍的是
MCUXpresso IDE下C++源文件中嵌套定义的复合数据类型命名空间认定

痞子衡之前写过一篇文章
《MCUXpresso IDE下添加C++源文件进SDK工程编译的方法》
,通过这篇文章我们知道嵌入式工程里是能够支持 C 源文件和 C++ 源文件混合编译的(因为有很多开源的库是基于 C++ 语言的,我们需要移植到嵌入式工程里)。

最近有一个 RW612 客户在使用官方 SDK 时就遇到一个 C/C++ 混合编译问题,主要涉及复合数据类型(结构体、联合体、枚举)嵌套定义时的命名空间范围认定,今天我们就来聊一聊这个话题:

  • Note1:测试软件版本为 MCUXpresso IDE v11.10.0_3133
  • Note2:该问题普遍存在于不同 IDE 编译器,非 GCC 专属

一、引出编译问题

客户使用得是 SDK_2_16_000_RD-RW612-BGA 开发包里的基础 hello_world 例程模板,当客户尝试在工程里添加自己创建的 C++ 源文件,并且在该 C++ 源文件中使用 fsl_clock.h 里声明的如下 clock_frg_clk_config_t 复合数据类型时遇到了编译错误:

typedef struct _clock_frg_clk_config
{
    uint8_t num;
    enum
    {
        kCLOCK_FrgMainClk = 0,
        kCLOCK_FrgPllDiv,
        kCLOCK_FrgSFro,
        kCLOCK_FrgFFro,
    } sfg_clock_src;
    uint8_t divider;
    uint8_t mult;
} clock_frg_clk_config_t;

客户出现问题的 C++ 源代码也足够简单,只是定义了 clock_frg_clk_config_t 类型的变量 frg_config,并对其中 sfg_clock_src 成员进行赋值,结果编译报错
'kCLOCK_FrgPllDiv' was not declared in this scope

clock_frg_clk_config_t frg_config;
frg_config.sfg_clock_src = kCLOCK_FrgPllDiv;

当我们尝试在 C 源代码里使用相同的代码时,编译是没问题的,因此可知 clock_frg_clk_config_t 结构体里直接嵌套的 sfg_clock_src 枚举类型申明在 C 编译器(arm-none-eabi-gcc)下命名空间是整个文件,而在 C++ 编译器(arm-none-eabi-c++)下命名空间仅在 clock_frg_clk_config_t 结构体内。

二、解决编译问题

知道了编译问题和命名空间有关,要解决问题,那就通过 C++ 的作用域解析运算符 :: 来解决,修改代码如下,此时编译正常。但是这样是完美的解决方案吗?很显然,同样功能的代码,在 C/C++ 源文件里写法不一致,这看起来很别扭。

clock_frg_clk_config_t frg_config;
frg_config.sfg_clock_src = clock_frg_clk_config_t::kCLOCK_FrgPllDiv;

什么是更好的解决方案?答案就是单独申明每个复合数据类型原型,让所有复合数据类型命名空间都一样,这样我们统一用 C 语言语法即可。实际上在恩智浦官方 SDK 里一直是这样的设计准则,客户遇到的这个情况是个“美丽”的意外。

typedef enum _sfg_clock_src
{
    kCLOCK_FrgMainClk = 0,
    kCLOCK_FrgPllDiv,
    kCLOCK_FrgSFro,
    kCLOCK_FrgFFro,
} sfg_clock_src_t;

typedef struct _clock_frg_clk_config
{
    uint8_t num;
    sfg_clock_src_t sfgClockSrc
    uint8_t divider;
    uint8_t mult;
} clock_frg_clk_config_t;

至此,MCUXpresso IDE下C++源文件中嵌套定义的复合数据类型命名空间认定痞子衡便介绍完毕了,掌声在哪里~~~

欢迎订阅

文章会同时发布到我的
博客园主页

CSDN主页

知乎主页

微信公众号
平台上。

微信搜索"
痞子衡嵌入式
"或者扫描下面二维码,就可以在手机上第一时间看了哦。

访问相册图片介绍

在应用开发中,很多场景需要我们需要访问相册中的图片。例如:上传头像、上传银行卡、身份证资料、扫描文件功能、美颜功能等

所以访问相册里的图片成为我们必须要学习和掌握的内容。那如何访问相册图片呢?

在HarmonyOS中,鉴于对用户隐私的高度保护,要方便的完全读取相册与写入相册,需要极其复杂的权限审核。所幸,HarmonyOS也考虑到读取相册对于开发者而言也是一个非常常用的一个功能,因而提供了
photoAccessHelper
里的
PhotoViewPicker
来帮助开发者
无需获得复杂权限
的情况下来读取相册内容。

使用方法

  1. 导入相册管理模块。

    import { photoAccessHelper } from '@kit.MediaLibraryKit';
    
  2. 实例化PhotoViewPicker对象(也即图片选择器对象)

    let photoPicker = new photoAccessHelper.PhotoViewPicker();
    
  3. 调用上述对象的select方法选择图片

    photoPicker.select({
      	// 传入能选择的文件类型,这里选的是图片类型
        MIMEType: photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE,
        // 设置能选择的数量
        maxSelectNumber: 1
      })
        .then((res: photoAccessHelper.PhotoSelectResult) => {
    
          console.log(res.photoUris.join(''))
        })
        .catch((err: Error) => {
          // 出错回调
          console.log(err.message)
        })
    })
    

    这里可以看到调用select方法有两个参数:MIMEType、maxSelectNumber

    MIMEType即设置可以选哪些类型的媒体文件,可选值有


    1. IMAGE_TYPE:图片类型,也即
      'image/*'
    2. VIDEO_TYPE:视频类型,也即
      'video/*'
    3. IMAGE_VIDEO_TYPE:所有类型皆可,也即:
      '*/*'
    4. MOVING_PHOTO_IMAGE_TYPE:动态照片类型(实况图),也即
      'image/movingPhoto'

    maxSelectNumber就比较好理解了,就是设置可以选择多少数量,
    若不设置默认为50,最大也只能设置500

  4. select方法是用Promise进行封装的,因此调用后有两种状态,成功进入then,失败进入catch

    进入then代表读取图片成功,读取到的结果是
    PhotoSelectResult
    类型的,这个类型有一个非常重要的属性,即为:
    photoUris
    ,它是一个数组,里面保存了选择的资源的临时路径,像我们上面的代码,最大只允许选择1张图片,因此取下标0即为选择的图片或视频

用一个小界面测试一下

上面我们已经学了它的基本使用,我们用一个小界面测试一下。界面仅需放置一个Image用来展示选择后的图片,以及用一个按钮进行图片选择,代码如下

// 导入所需工具
import { photoAccessHelper } from '@kit.MediaLibraryKit'

@Entry
@Component
struct Index {
  // 用个变量保存读取到得分图片
  @State imgUri: string = ''

  build() {
    Column() {
      // 展示图片
      Image(this.imgUri)
        .height('45%')
      // 选择图片
      Button('选择图片')
        .width('80%')
        .onClick(() => {
          // 实例化选择器
          let photoPicker = new photoAccessHelper.PhotoViewPicker()
          // 开始选择图片,设置只允许选择图片,且最大选择1张
          photoPicker.select({
            MIMEType: photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE,
            maxSelectNumber: 1
          })
            .then((res: photoAccessHelper.PhotoSelectResult) => {

              // 读取成功则赋值给变量去展示到界面
              this.imgUri = res.photoUris[0]
            })
            .catch((err: Error) => {
              // 出错回调
              console.log(err.message)
            })
        })
    }
    .width('100%')
    .height('100%')
  }
}

最终效果如下:成功将猫林老师的照片展示出来了

image-20241215202846748

总结

  1. 如果需要不申请权限的情况下让app读取到系统图库里的图片,需使用
    photoAccessHelper
  2. 使用起来非常简单,仅需实例化
    PhotoViewPicker
    对象后再调用
    select
    方法即可
  3. 下篇猫林老师给大家介绍如何不申请权限的情况下写入照片到图库

problem.gif

这个问题需要分以下两个步骤解决:

  • 检查抖动的组件是否有多个多根节点
  • 增加离开过渡的css样式
    v-leave-to: {display: none}

解决步骤1(抖动的页面有多个多根节点)

我在为路由跳转增加了一个动画,如下:


<template>
    <router-view v-slot="{Component}">
        <transition name="fade" mode="out-in">
            <component :is="Component"></component>
        </transition>
    </router-view>
</template>

<style scoped lang="less">
    .fade-enter-active,
    .fade-leave-active {
        transition: all .5s;
    }

    .fade-leave-from {
        opacity: 0;
        transform: translateX(30px);
    }

    .fade-enter-from {
        opacity: 0;
        transform: translateX(-30px);
    }
</style>

有的页面可以正常加载动画,有的页面就有抖动问题,后来发现控制台有如下的警告信息:

img.png

问题解释如下:

警告信息表明你在使用
Vue

<Transition>
组件时遇到了问题。
<Transition>
组件要求它包裹的内容只能有一个根元素,这是因为
Vue
的过渡效果需要针对单个元素进行动画处理。

如果你的
<Transition>
组件内部渲染了多个元素或者根元素不是单个
DOM
元素(比如一个
或者一个组件返回了多个根节点), Vue 就无法为这些元素应用动画效果,因此会抛出这个警告。

所以大家看到哪个页面组件有抖动现象,检查下代码是不是有多个根节点,比如我们发现
A组件
页面有多个根节点,如下:

旧代码:


<template>
    <p>第一行</p>
    <p>第二行</p>
</template>

新代码:


<template>
    <div>
        <p>第一行</p>
        <p>第二行</p>
    </div>
</template>

保证组件内部只有一个根节点

解决步骤2(增加离开过渡的css样式 v-leave-to)

我们在 style 中再加一行离开过渡的样式,让页面不占位即可,代码如下,:

.fade-leave-to {
    display: none;
}

我们在做java项目的时候通常需要通过请求日志来排查定位线上问题,在日志比较多而我们又需要查找整个请求的全部日志的时候会比较困难。所以,就需要在日志记录的时候讲同一个请求的关键日志用同一个唯一标识串联起来。这样查找的时候就会比较好查找。下面来用java aop实现请求id的日志记录。(该支持子线程继承主线程请求id)

一:首先我们需要一个日志请求链路id切面类

注意:如果不考虑多线程则(第二步和第三步可以不要)

package com.iMagine.iMagine_pro.aop;

import com.iMagine.iMagine_common.utils.UUIDUtil;
import com.iMagine.iMagine_pro.utils.TokenUtil;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.MDC;
import org.springframework.stereotype.Component;


/**
* @author 名一
* @ClassName TraceIdAspect
* @description: 日志请求链路id切面处理
* @datetime 2024年 12月 16日 14:28
* @version: 1.0
*/
@Slf4j
@Aspect
@Component
public class TraceIdAspect {

/** 链路追踪id */
public final static String TRACE_ID = "TRACE_ID";
/** 用户 */
public final static String USER = "USER";

/**
* 链路id切点定义
*/
@Pointcut("execution(* com.iMagine.iMagine_pro.controller.*.*(..))")
public void TraceIdCut() {

}

/**
* 链路id添加
*/
@Before("TraceIdCut()")
public void cutProcessBefore() {
MDC.put(TRACE_ID, UUIDUtil.getUUID());
//以下代码为记录用户信息,方便更直观的识别日志操作人信息。若不需要可以将下面部分删除
String nickname = TokenUtil.getNicknameByToken();
if (null == nickname){
nickname = "游客访问";
}
MDC.put(USER, nickname);
}

/**
* 链路id清除
*/
@After("TraceIdCut()")
public void cutProcessAfter() {
MDC.clear();
}
}
packagecom.iMagine.iMagine_common.utils;importjava.util.UUID;/***@author名一
* @ClassName UUIDUtil
* @description: UUID工具类
* @datetime 2024年 04月 23日 11:49
*
@version: 1.0*/ public classUUIDUtil {/*** 获取UUID
*
*
@return */ public staticString getUUID() {//生产uuid并去掉uuid的短横线 return UUID.randomUUID().toString().replace("-", "");
}
}

二:创建一个处理多线程链路追踪的工具类

package com.iMagine.iMagine_common.utils;

import com.iMagine.iMagine_common.constant.SysConstant;
import org.slf4j.MDC;

import java.util.Map;

/**
* @author 名一
* @ClassName ThreadMdcUtil
* @description: 多线程链路追踪工具类
* @datetime 2024年 12月 16日 14:57
* @version: 1.0
*/
public class ThreadMdcUtil {

// 获取唯一性标识
public static String generateTraceId() {
return UUIDUtil.getUUID();
}

public static void setTraceIdIfAbsent() {
if (MDC.get(SysConstant.TRACE_ID) == null) {
MDC.put(SysConstant.TRACE_ID, generateTraceId());
}
}

/**
* 用于父线程向线程池中提交任务时,将自身MDC中的数据复制给子线程
*
* @param runnable 要执行的线程
* @param context 父线程的mdc
* @return 链路id传递后的任务
*/
public static Runnable wrap(final Runnable runnable, final Map<String, String> context) {
return () -> {
if (context == null) {
MDC.clear();
} else {
MDC.setContextMap(context);
}
setTraceIdIfAbsent();
try {
runnable.run();
} finally {
MDC.clear();
}
};
}
}

三:在往线程池放任务的时候做请求链路id传递

/**
* 线程池添加 [ai server 相关操作(psot)]任务
*
* @param task 添加的任务
*/
public static void addSendPostThreadToThreadPool(SendPostThread task) {
log.info("addSendPostThreadToThreadPool pool:{}, task:{}", pool, task);
try {
pool.execute(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap()));
} catch (Exception e) {
log.error("addSendPostThreadToThreadPool error pool:{}, task:{}, e:", pool, task, e);
}
}

四:在日志配置里将链路追踪id加入日志输出格式的配置里

<!-- 日志输出格式 (其中%X{TRACE_ID}是mdc里边链路请求id的key,user-%X{USER}是用户信息的key)-->
<property name="ENCODER_PATTERN"value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %X{TRACE_ID} user-%X{USER} %-5level %logger{32}-[%line] - %msg%n"/>

到此spring 项目实现带请求链路id的日志记录就完成了