2024年1月

位运算合集(&、|、^、~、>>、<<)

​ 在学习和研究源码过程中,经常遇到使用位运算的逻辑,代码看着简洁,执行效率也高;特此总结和记录位运算的使用方法。

1.位运算概述

从现代计算机中所有的数据二进制的形式存储在设备中。即0、1两种状态,计算机对二进制数据进行的运算(+、-、*、/)都是叫位运算,即将符号位共同参与运算的运算。

口说无凭,举一个简单的例子来看下CPU是如何进行计算的,比如这行代码:

int a = 35;
int b = 47;
int c = a + b;

计算两个数的和,因为在计算机中都是以二进制来进行运算,所以上面我们所给的int变量会在机器内部先转换为二进制在进行相加:

35:  0 0 1 0 0 0 1 1
47:  0 0 1 0 1 1 1 1
————————————————————
82:  0 1 0 1 0 0 1 0

所以,相比在代码中直接使用(+、-、*、/)运算符,合理的运用位运算更能显著提高代码在机器上的执行效率。

2.位运算概览

符号 描述 运算规则
& 两个位都为1时,结果才为1
| 两个位都为0时,结果才为0
^ 异或 两个位相同为0,相异为1
~ 取反 0变1,1变0
<< 左移 各二进位全部左移若干位,高位丢弃,低位补0
>> 右移 各二进位全部右移若干位,对无符号数,高位补0,有符号数,各编译器处理方法不一样,有的补符号位(算术右移),有的补0(逻辑右移)

3.按位与运算符(&)

定义:参加运算的两个数据,按二进制位进行“与”运算。

运算规则:

0&0=0  0&1=0  1&0=0  1&1=1

总结:两位同时为
1
,结果才为
1
,否则结果为
0

例如:
3&5
即 0000 0011& 0000 0101 = 0000 0001,因此 3&5 的值得1。

注意:负数按
补码
形式参加按位与运算。

与运算的用途:

1)清零

如果想将一个单元清零,即使其全部二进制位为0,只要与一个各位都为零的数值相与,结果为零。

2)取一个数的指定位

比如取数 X=1010 6666660 的低4位,只需要另找一个数Y,令Y的低4位为1,其余位为0,即Y=0000 6666661,然后将X与Y进行按位与运算(X&Y=0000 6666660)即可得到X的指定位。

3)判断奇偶

只要根据最未位是0还是1来决定,为0就是偶数,为1就是奇数。因此可以用
if ((a & 1) == 0)
代替
if (a % 2 == 0)
来判断a是不是偶数。

4.按位或运算符(|)

定义:参加运算的两个对象,按二进制位进行“或”运算。

运算规则:

0|0=0  0|1=1  1|0=1  1|1=1

总结:参加运算的两个对象只要有一个为1,其值为1。

例如:
3|5
即 0000 0011| 0000 0101 = 0000 0666666,因此,
3|5
的值得7。

注意:负数按
补码
形式参加按位或运算。

或运算的用途:

1)常用来对一个数据的某些位设置为1

比如将数 X=1010 6666660 的低4位设置为1,只需要另找一个数Y,令Y的低4位为1,其余位为0,即Y=0000 6666661,然后将X与Y进行按位或运算(X|Y=1010 6666661)即可得到。

5.异或运算符(^)

定义:参加运算的两个数据,按二进制位进行“异或”运算。

运算规则:

0^0=0  0^1=1  1^0=1  1^1=0

总结:参加运算的两个对象,如果两个相应位相同为0,相异为1。

异或的几条性质:

1、交换律

2、结合律 (a
b)
c == a
(b
c)

3、对于任何数x,都有 x
x=0,x
0=x

4、自反性: a
b
b=a^0=a;

异或运算的用途:

1)翻转指定位

比如将数 X=1010 6666660 的低4位进行翻转,只需要另找一个数Y,令Y的低4位为1,其余位为0,即Y=0000 6666661,然后将X与Y进行异或运算(X^Y=1010 0001)即可得到。

2)与0相异或值不变

例如:1010 6666660 ^ 0000 0000 = 1010 6666660

3)交换两个数

void Swap(int &a, int &b){
    if (a != b){
        a ^= b;
        b ^= a;
        a ^= b;
    }
}

6.取反运算符 (~)

定义:参加运算的一个数据,按二进制进行“取反”运算。
运算规则:

~1=0
~0=1

总结:对一个二进制数按位取反,即将0变1,1变0。

异或运算的用途:

1)使一个数的最低位为零

使a的最低位为0,可以表示为:
a & ~1
。~1的值为 6666661 6666661 6666661 6666660,再按"与"运算,最低位一定为0。因为“ ~”运算符的优先级比算术运算符、关系运算符、逻辑运算符和其他运算符都高。

7.左移运算符(<<)

定义:将一个运算对象的各二进制位全部左移若干位(左边的二进制位丢弃,右边补0)。

设 a=1010 6666660,
a = a<< 2
将a的二进制位左移2位、右补0,即得a=1011 1000。

若左移时舍弃的高位不包含1,则每左移一位,相当于该数乘以2。

8.右移运算符(>>)

定义:将一个数的各二进制位全部右移若干位,正数左补0,负数左补1,右边丢弃。

例如:a=a>>2 将a的二进制位右移2位,左补0 或者 左补1得看被移数是正还是负。

操作数每右移一位,相当于该数除以2。

10.复合赋值运算符

位运算符与赋值运算符结合,组成新的复合赋值运算符,它们是:

&=`    例:`a&=b  `相当于   `a=a&b
|=`    例:`a|=b  `相当于   `a=a|b
>>=`   例:`a>>=b  `相当于   `a=a>>b
<<=`   例:`a<<=b`   相当于   `a=a<<b
^=`    例:`a^=b  `相当`于  a=a^b

运算规则:和前面讲的复合赋值运算符的运算规则相似。

不同长度的数据进行位运算:
如果两个不同长度的数据进行位运算时,系统会将二者按右端对齐,然后进行位运算。

以“与运算”为例说明如下:我们知道在C语言中long型占4个字节,int型占2个字节,如果一个long型数据与一个int型数据进行“与运算“,右端对齐后,左边不足的位依下面三种情况补足,

1)如果整型数据为正数,左边补16个0。

2)如果整型数据为负数,左边补16个1。

3)如果整形数据为无符号数,左边也补16个0。

如:long a=123;int b=1;计算a& b。

如:long a=123;int b=-1;计算a& b。

如:long a=123;unsigned intb=1;计算a & b。

前言

日常开发时有些特殊的场景需要在非
setup
期间调用
inject
函数,比如app中使用
provide
注入的配置信息需要在发送
http
请求时带上传给后端。对此我们希望不在每个发起请求的地方去修改,而是在发起请求前的拦截进行统一处理,对此我们就需要在拦截请求的函数中使用
inject
拿到
app
注入的配置信息。

为什么只能在
setup
期间调用
inject
函数

inject
的用法大家应该都清楚,是一个用于注入依赖的函数,它可以将父组件或根组件 app 中通过 provide 提供的相同 key 的值注入到当前组件中。

我们先来看看简化后的
provider

inject
的源码,其实非常简单。

provider
函数源码

我们先来看看简化后的
provider
函数源码,其实很简单:

export function provide(
  key,
  value,
) {
  //拿到当前组件的vue实例提供的provides对象
  let provides = currentInstance.provides
  //拿到父组件的vue实例提供的provides对象
  const parentProvides =
    currentInstance.parent && currentInstance.parent.provides
  // 如果父组件和当前组件的provides对象相等
  if (parentProvides === provides) {
    // 基于父组件的provides对象拷贝出一个新的对象
    provides = currentInstance.provides = Object.create(parentProvides)
  }
  // 如果provides对象中有相同的key,那么就会直接覆盖。
  provides[key] = value
}

在初始化一个
vue
实例的时候会将父组件的
provides
对象赋值给当前实例的
provides
对象,所以当第一次
provide
方法被调用后,会判断当前的
provides
对象是否等于父组件
provides
对象,如果相等就会基于父组件实例的
provides
对象拷贝一个新的
provides
对象。

此时父组件和子组件的
provides
对象经过
Object.create(parentProvides)
后就已经不是同一个对象了。如果子组件和父组件
provide
对象中都有相同的
key
,经过
provides[key] = value
后就会将原本父组件赋值的相同
key
的值“覆盖”掉。因为父组件的
provides
对象是从他的父组件
provides
对象拷贝的而来,所以子组件包含了父组件链上的所有的
provide
提供的值。

机智如你现在应该能够理解为什么官网会说“父组件链上多个组件对同一个 key 提供了值,那么离得更近的组件将会“覆盖”链上更远的组件所提供的值”。

inject
函数源码

现在我们再来看看简化后的
inject
函数源码,同样也非常简单:

export function inject(
  key,
) {
  //currentInstance是一个存储当前vue实例的全局变量,在vue组件初始化时会赋值。
  //初始化完成后会被重置为null
  const instance = currentInstance

  if (instance || currentApp) {
    // 拿到父组件或者currentApp中提供的provides对象
    const provides = instance
      ? instance.parent.provides
      : currentApp!._context.provides
    // 从provides对象中拿到相同key的值
    if (provides && key in provides) {
      return provides[key]
    }
  } else if (__DEV__) {
    // 不是在setup中或者runWithContext中调用,就会发出警告
    warn(`inject() can only be used inside setup() or functional components.`)
  }
}

我们首先来看看
currentInstance
这个全局变量,
setup
只会在初始化vue实例的时候执行一次,在
setup
期间
currentInstance
会被赋值为当前组件的vue实例。等vue实例初始化完成后
currentInstance
就会被赋值为
null

前面我们已经介绍了组件的
provides
对象中是包含了父组件链上的所有
provides
的key,所以我们这里只需要从当前
vue
实例
instance

parent
中的
provides
对象中就可以取出注入相同
key
的值。

看到这里相信你已经知道了为什么只能在
setup
期间调用调用
inject
方法了。因为只有在
setup
期间
currentInstance
全局变量的值为当前组件的
vue
实例对象,当
vue
实例初始化完成后
currentInstance
已经被赋值为null。所以当我们在非
setup
期间调用
inject
方法会警告:inject() can only be used inside setup() or functional components.

至于
currentApp
其实是另外一个全局变量,在调用
app.runWithContext
方法时会给它赋值,这个下一节我们讲
app.runWithContext
的时候会详细讲。

使用
app.runWithContext()
打破
inject
只能在
setup
期间调用的限制

app.runWithContext()
的官方解释为“使用当前应用作为注入上下文执行回调函数”。这个解释乍一看很容易一脸懵逼,不着急我慢慢给你解释。

我们先来看看
runWithContext
方法接收的参数和返回的值。这个方法接收一个参数,参数是一个回调函数。这个回调函数会在
app.runWithContext()
执行时被立即执行,并且
app.runWithContext()
的返回值就是回调函数的返回值。

我们再来看一个使用
runWithContext
的例子,这行代码是拦截请求时才执行。作用是拿到
app
中注入的
userType
字段,注意不是在
setup
期间执行。

const userType = app.runWithContext(() => {
  // 拿到app中注入的userType字段
  return inject("userType");
});

按照我们前一节的分析,
inject
需要在
setup
中执行才能拿到当前的
vue
实例。但是之前还有一个
currentApp
变量我们没有解释,再来回顾一下上一节的
inject
源码。如果我们拿不到当前的
vue
实例,就会去看一下全局变量currentApp是否存在,如果存在那么就从
currentApp
中去拿
provides
对象。这个
currentApp
就是官方解释的“注入的上下文”,所以我们才可以在非
setup
期间执行
inject
,并且还可以拿到注入的值。

if (instance || currentApp) {
  // 拿到父组件或者currentApp中提供的provides对象
  const provides = instance
    ? instance.parent.provides
    : currentApp!._context.provides
  // 从provides对象中拿到相同key的值
  if (provides && key in provides) {
    return provides[key]
  }
}

我们再来看看
runWithContext
的源码,其实非常简单。

runWithContext(fn) {
  // 将调用runWithContext方法的对象赋值给全局对象currentApp
  currentApp = app
  try {
    // 立即执行传入的回调函数
    return fn()
  } finally {
    currentApp = null
  }
}

这里的
app
就是调用
runWithContext
方法的对象,你可以简单的理解为
this
。调用
app.runWithContext()
就会将
app
对象赋值给全局变量
currentApp
,然后会立即执行传入的回调
fn
。当执行到回调中的
inject("userType")
时,由于我们在上一行代码已经给全局变量
currentApp
赋值为
app
了,所以就可以从app中拿到对应key的
provider
值。

总结

这篇文章我们先介绍了由于
inject
执行期间需要拿到当前的vue实例,然后才能从父组件提供的
provides
对象中找到相同key的值。如果我们在非
setup
期间执行,那么就拿不到当前vue实例。也找不到父组件,当然
inject
也没法拿到注入的值。

在一些场景中我们确实需要在非
setup
期间执行
inject
,这时我们就可以使用
app.runWithContext()

app
对象作为注入上下文执行回调函数。然后在
inject
执行期间就能从
app
中拿到提供的
provides
对象中相同
key
的值。

如果我的文章对你有点帮助,欢迎关注公众号:【欧阳码农】,文章在公众号首发。你的支持就是我创作的最大动力,感谢感谢!

学习 SpringBoot 自动配置之前我们需要一些前置知识点:

Java注解,看完就会用

学会@ConfigurationProperties月薪过三千

不是银趴~是@Import!

@Conditional+@Configuration有没有搞头?


首先我们提出2个问题:

SpringBoot 是干什么的?

是用来简化 Spring 原生的复杂的 xml 配置的进阶框架。

自动配置是什么?

我们用另外一个问题回答这个问题。

我们在 SpringBoot 开发中,就写了几个配置,怎么连接上的数据库?

spring:
  datasource:
    driverClassName: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://192.0.0.1:3306/test?useUnicode=true&useSSL=false&serverTimezone=Asia/Shanghai
    username: root
    password: root

在新手村时期,我们照着教程生搬硬抄的时候可能也想过这个问题,今天就来简单探究一下。

再次强调:

看明白本篇内容需要前置知识点,尤其是
@Import
注解。

这一切都要从
@SpringBootApplication
注解讲起。

@SpringBootApplication

@SpringBootApplication 注解是一个复合注解,它由如下三个注解组成。

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration  // 第一个
@EnableAutoConfiguration  // 第二个
@ComponentScan(excludeFilters = {  // 第三个
        @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
        @Filter(type = FilterType.CUSTOM,
                classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {
}
@SpringBootApplication
├── @ComponentScan
├── @SpringBootConfiguration
│   └── @Configuration
└── @EnableAutoConfiguration
    ├── @Import(AutoConfigurationImportSelector.class)
    └── @AutoConfigurationPackage
        └── @Import(AutoConfigurationPackages.Registrar.class)

@SpringBootConfiguration

@ComponentScan

@EnableAutoConfiguration

从注解的中文意思中也可以看出来,第三个
@EnableAutoConfiguration
是与我们自动配置紧密相关的。

我们先快速搞定前2个。

@SpringBootConfiguration

这个最简单,把头套摘下来,他就是一个普普通通的 @Configuration 注解包装而成而已,表示当前类是一个配置类。

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Configuration // 本体在这
public @interface SpringBootConfiguration {
}

@ComponentScan

可以指定basePackageClasses或basePackages(或其别名value)来定义要扫描的特定包。如果没有定义特定包,扫描将从声明此注解的类的包开始。

顾名思义就是扫描Component。

扫描哪里?

可以通过该注解的属性指定Spring应该扫描的包。如果没有指定包,则默认扫描声明 @ComponentScan 的类所在的包及其子包。

哪些Component?

使用 @Component, @Service, @Repository, @Controller 等注解的类。

扫描了做什么用?

将扫描到的组件注册为Spring的Bean,加入到ioC容器进行统一管理。

@EnableAutoConfiguration

这是自动配置的核心注解!

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {
}    

我们能看到,其中有2个注解:

@AutoConfigurationPackage

@Import(AutoConfigurationImportSelector.class)

@AutoConfigurationPackage

我们再点进去看一下,发现它又是一个
@Import

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Import(AutoConfigurationPackages.Registrar.class)
public @interface AutoConfigurationPackage {

}

@Import(AutoConfigurationPackages.Registrar.class) 中的 Regisgrar 类如下:

static class Registrar implements ImportBeanDefinitionRegistrar, DeterminableImports {

    @Override
    public void registerBeanDefinitions(AnnotationMetadata metadata,
                                        BeanDefinitionRegistry registry) {
        register(registry, new PackageImport(metadata).getPackageName());
    }

    @Override
    public Set<Object> determineImports(AnnotationMetadata metadata) {
        return Collections.singleton(new PackageImport(metadata));
    }

}

如果你有去好好看 @Import 注解的那篇文章,就会知道这是
ImportBeanDefinitionRegistrar 接口实现
的方式。

这种方式提供一种手动方式灵活注册 bean。

在这里,我们根据两个提示就可以知道其功能:

• @AutoConfigurationPackage 是自动配置包的意思

• register(registry, new PackageImport(metadata).getPackageName()); 根据包名注册 bean

总结一下就是:

根据通过注解提供的元数据,动态地向 Spring 容器中注册特定包中的类作为 beans。

@Import(AutoConfigurationImportSelector.class)

如果你有去好好看 @Import 注解的那篇文章,就会知道这是
ImportSelector接口实现
的方式。

这种方式只需实现 selectImports 方法,并以
数组
的形式返回要导入的类名,即可实现批量注册组件。

AutoConfigurationImportSelector 类中通过自己的源码实现了如下一个功能:

把 spring-boot-autoconfigure 依赖中 META-INF/spring.factories 文件中需要自动配置的类名读取到数组中。

它长这个样子:

# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,\
org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,\
org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration,\
org.springframework.boot.autoconfigure.batch.BatchAutoConfiguration,\
org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration,\
org.springframework.boot.autoconfigure.cassandra.CassandraAutoConfiguration,\
...

那它到底怎么读取的?

这里我们不讨论具体实现,
有兴趣

有实力
的可以扒源码自己看一下。

当然这些类不会全部都用到,经过筛选,
去除重复、去除相应功能模块未开启的配置类、去除人为exclude掉的
,将剩余的最终配置类全类名String数组返回。

最后我们再来回收一下开始的那个问题。

我们在 SpringBoot 开发中,就写了几个配置,怎么连接上的数据库?

我们简单捋一下:

SpringBoot 启动。

@SpringBootApplication 注解生效。

@EnableAutoConfiguration 注解生效。

@Import(AutoConfigurationImportSelector.class) 注解生效。

读取到了org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration。

注册一个数据源bean。

读取到 xml 的数据源配置。

大功告成!


往期推荐:


终于搞懂动态代理了!


学会@ConfigurationProperties月薪过三千


学一点关于JVM类加载的知识


Java注解,看完就会用


Java反射,看完就会用


@Value是个什么东西

1、文档说明

如之前文档《
MIGO新增页签增强
》,在MIGO中增强自定义字段,那么在查询MB51时,想通过自定义字段进行筛选,并将数据展示到报表中,就需要对MB51进行增强。

此处需要说明,文档《
MIGO新增页签增强
》中是将自定义字段存入自建表中,读者也可以自己研究,是否能将字段存入标准表MSEG中。两种做法不同,则在本篇文档中的做法也有一定区别,请读者予以辨别。

2、增强实施

2.1、增强字段

文档《
MIGO新增页签增强
》中将MIGO增强字段存储在表ZTPP001中

因为需要将数据展示在报表中,所以要在标准表中增强字段

MSEG

MATDOC

此处请注意,如果增强的字段和MIGO中增强的字段名称一致,则需要修改MIGO增强页签中的一段逻辑

文档《
MIGO新增页签增强
》中在增强方法IF_EX_MB_MIGO_BADI~POST_DOCUMENT中,将MIGO增强的字段存表时,为了得到物料凭证、年度、行项目的值,曾使用
MOVE-CORRESPONDING,将MSEG结构的数据传递到自建表的内表中。如果MSEG增强字段和ZTPP001字段名一致,就会使ZTPP001的ZNUM被MSEG的ZNUM(空值)覆盖掉,所以此处可以单独将物料凭证、年度、行项目赋值给ZTPP001的内表。当然,读者也可以定义不一样的字段名,避免覆盖的情况。

2.2、配置选择屏幕

可以通过配置,为MB51新增筛选和展示字段

TCODE:SPRO

新增对应字段,其中选择字段:显示在选择屏幕中,输出字段:显示在报表中

保存之后,重新执行MB51,标准代码则自动更新,此时单据号字段就显示在选择屏幕上

查看包含文件RM07DOCS_GENERATED,可以看到代码更新的时间

选择屏幕代码中也已经自动添加了ZNUM

2.3、隐式增强

在RM07DOCS_GENERATED的子例程FORM DATA_SELECTION_NEW开头位置创建隐式增强

代码如下

"--------------------@斌将军--------------------
IF sy-tcode = 'MB51'.IF znum[] IS NOT INITIAL."当选择屏幕筛选字段有值
    SELECTmblnr,
mjahr,
zeile,
znum
FROMZTPP001INTO TABLE @DATA(lt_ztpp001)WHERE znum IN@znum."将物料凭证、年度、行项目赋值到对应的筛选字段中 LOOP AT lt_ztpp001 INTO DATA(ls_ztpp001).
mblnr
-low = ls_ztpp001-mblnr.
mblnr
-sign = 'I'.
mblnr
-option = 'EQ'.APPENDmblnr.CLEARmblnr.
mjahr
-low = ls_ztpp001-mjahr.
mjahr
-sign = 'I'.
mjahr
-option = 'EQ'.APPENDmjahr.CLEARmjahr.
zeile
-low = ls_ztpp001-zeile.
zeile
-sign = 'I'.
zeile
-option = 'EQ'.APPENDzeile.CLEARzeile.CLEAR:ls_ztpp001.ENDLOOP."标准表中该字段是空的,并不存值,所以此处要清空, "避免标准代码用此字段筛选查询MSEG表,导致数据为空,所以此处要清空 CLEARznum[].ENDIF.ENDIF."--------------------@斌将军--------------------

同样的代码,增强到以下子例程的开头处

FORM DATA_SELECTION

FORM DATA_SELECTION_VIA_MATNR

FORM DATA_SELECTION_VIA_BUDAT

在RM07DOCS的FORM detail_list中创建隐式增强,将数据赋值到ALV中

代码如下

"--------------------@斌将军--------------------
ENHANCEMENT 5  ZEMM_MB51_APPEND.    "active version
IF list[] IS NOT INITIAL.DATA(lt_list) =list[].SELECTztpp001~mblnr,
ztpp001~mjahr,
ztpp001~zeile,
ztpp001~ZNUM
FROMZTPP001
INNER JOIN @lt_list AS
LIST ON ztpp001~mblnr = LIST~MBLNRAND ztpp001~mjahr = LIST~mjahrAND ztpp001~zeile = LIST~zeileINTO TABLE @DATA(LT_ZPP001).SORT LT_ZPP001 BYMBLNR MJAHR ZEILE.LOOP AT list ASSIGNING FIELD-SYMBOL(<fs_list>).READ TABLE LT_ZPP001 INTO DATA(LS_ZPP001) WITH KEY mblnr = <fs_list>-MBLNR
mjahr
= <fs_list>-mjahr
zeile
= <fs_list>-zeile BINARY SEARCH.IF SY-SUBRC EQ 0.<fs_list>-ZNUM = LS_ZPP001-ZNUM.ENDIF.ENDLOOP.ENDIF.
ENDENHANCEMENT.
"--------------------@斌将军--------------------

2.4、测试效果

输入单据号

数据表中存储的数据

运行结果

2.5、问题说明

此方法实现的屏幕筛选,并不能像常规一样,多个筛选条件取交集,而是与其他筛选字段取并集。读者可以根据项目情况,灵活更改代码实现需求。

3、MB51代码重置

正常情况下,如上文中配置完屏幕字段后,运行MB51,对应的MB51源码就能更新。但是也存在个例,运行MB51时,源代码没有更新过来,选择屏幕中并没有对应的字段

推测原因,可能是开发的增强与MB51要更新的代码出现了冲突,导致更新失败。

例如:有的选择屏幕字段,是直接增强上去的,而不是配置的,导致配置的字段与原本增强的字段重复,所以更新失败

解决方法一:

注释选择屏幕中自开发的增强字段和相关的增强,然后运行MB51,保证程序不DUMP的情况下,程序就会自动再更新源码

解决方法二:

如果仍然不能更新,则采用以下办法,根据官方提供的程序,手动执行更新

首先注释与选择屏幕相关的自开发增强逻辑,保证源码被重置时,使用到选择屏幕等字段的逻辑不报错,保证MB51的正常运行

找到官方NOTE:

根据官方源码,创建并执行程序

对比前后的RM07DOCS_GENERATED,可以发现选择屏幕等源代码全部都没了

执行事务代码MB51,重新生成对应代码

配置的字段就更新进来了

定期更文,
欢迎关注

就像没有一个画匠不想办一场属于自己的画展,没有一个歌者不想写一首自己的歌。
也许只有为数不多的听众,但是当他怀疑自己时,还是会有忠实的听众支持他继续走下去。

我在业余时间开发维护了一款在线客服系统,一是兴趣使然,另一方面,正如上文。
我想如果有一天我离开了这个行业,我也会继续维护更新,作为我曾经做过这个行业的一个见证。

成绩:

从 2021 年至今,已将近 3 年时间,从未断更。
目前官方免费使用环境的注册用户 1921 人,即将突破 2000 。活跃用户(客服)大约 300 人左右,日常在线访客在 1000 人左右。
服务器每日处理 HTTPS 请求数大于 16 万次, PV 请求大于 25 万 次的情况下,服务端主程序内存占用小于 300MB,服务器 CPU 占用小于 5%。


因为是个人业余时间做的小系统,谈不上什么正儿八经的推广,只能靠自己慢慢积累,可以简单总结几个经验。

首先产品要稳定可靠

要有十年磨一剑的耐心与意志,一定要先把产品做好,这是一切推广的前题。

在开始推广宣传之前,要确保产品达到了相当的稳定可靠水平。避免前期好不容易找到的用户在试用之后就放弃,失去种子用户,也避免给自己的产品口碑带来负面影响,再想纠正难度会大大增加。

产品要容易使用,正常人的思维要能理解

在推广前期,想积累用户很困难,一定要尽可能的把用户留下。

只有容易使用,低使用门槛的产品才能相对容易的把用户留下,从用户的注册流程到使用细节,都必须足够友好,做到不需要看用户手册,只需凭借正常人的基本思维理解就能开始使用,避免非常技术性的表述和非常隐晦的操作逻辑。

在我做项目经理的时候,我曾很直白的对开发团队说:我希望这个功能,只要识字的人就会用。

现在有许多产品的设计,初看挺高大上,界面很是简洁,再一用,发现不太会用,必须要仔细的观察和理解,摸索着用。有很多界面的表达细节,片面追求“好看”,追求“高大上”,操作过程看似很有逻辑性,实则极其隐晦。我认为这是大量的年轻人从事所谓“产品经理”的岗位造成的。对产品没有质的理解,对用户没有敬畏和同理心。现在极少有所谓的“产品经理”能够听取用户建议,他们更加崇拜乔布斯,信奉用户不知道自己想要什么,无法理解为什么 Windows 能够成为生产力工具。

常见做法比如 滚动条 1 毫米宽;图标不带文字以显得更精致;有二级菜单不知道,要点了才知道诸如此类。

对这类产品设计我是持保留意见的,我认为有两种情况:

  • 如果是大厂产品,毕竟用户也没什么选择,你想怎样就怎样吧。而且对于在大厂打工来讲,政治正确和占据理论高点、高大上,其实比用户真实想法更重要,卷来卷去的不容易,在你没有话语权的情况下,接地气的想法和做法可能会显得比较 low,学苹果政治上总不会错。

  • 本文探讨的主题是“程序员开发了自己的产品怎样推广”,在这种情况下你唯一要关注的只有用户的想法,其它的都不重要,你唯一要做的是向用户证明你的产品好用,正常人容易理解,而无需向同事和领导证明你的理念更高端受教育层次更高。

当你做自己的产品时,必须在思路上要有清楚的认知,好好做一款正常人能理解,方便使用的产品。

要有完善的文档和说明书

为用户快速了解产品,进一步理解产品,提供指引。大部分程序员都不爱写文档,但这很重要。

既然说产品要容易使用,为什么又讲到要有完善的文档和说明书呢。你要为用户的使用设计一个递进的层次。往往用户是在直接体验,感觉良好之后,才有可能看你的文档和说明书。所以两者不矛盾。

我在推广升讯威在线客服系统之前,专门花几天时间写了一个简单的在线文档系统,参考 MSDN 的版式简单克隆了一下,虽然版式简单了点,好歹我把内容丰富起来了,详细的介绍了私有化部署的方法,软件的基本功能。

我仔细设计了文档的类目结构,一步一步分解了软件的部署和使用过程,特别是对于安装部署的过程,我写完之后,就自己严格跟着文档在纯净的系统上跟着做,发现有疏漏的地方就立即补充修改。

用户根据文档,就能自己完成系统的搭建,一方面使我能够积累到更多的用户,另一方面也减轻了我自己的精力,在有详细文档之前,需要花很多时间回答用户的疑问。

详细的文档为我默默积累了许多前期用户,对于大部分人来说,如果没有详细的文档,他们会直接放弃使用你的产品,而不是来咨询你,这一点很重要。

要听取用户的反馈,对用户要有同理心

不要嫌弃用户,不要试图教育用户。

我怀疑现在几乎没有任何一个“产品经理”会听取用户意见和反馈,毕竟有经验的产品经理(对,这里没有双引号),年龄都不小了,大都到了要改行的时候了吧。

前文我用了一些篇幅说明产品要易用易于理解。即便如此,在我推广自己产品时,也经常会被用户的反馈弄到懵逼,怎么会有人这样理解(某个功能或操作)?

但我总是耐心听取用户的意见和反馈,在我向用户解释说明之后,我会第一时间修改调整产品的界面设计或文字表述,使同样的疑问不再产生第二次。因为我知道产品是为人服务的,不是用来证明我自己的。

早期的种子用户为我提供了极大的支持,在产品的完善过程中,给予了极大的耐心和支持,在此表示感谢:

不要试图向你身边认识的人推广,除非有利益捆绑

不管你做的产品多么的好,多么的有用,你身边的人永远不会愿意给你抬轿子,这是人性。

在我身边有许多认识的人,这么多年下来,依然不相信我懂技术是做技术的。后来我明白了,是他们选择不相信。

“老曹你现在做什么,做销售啦?”
“你做产品经理?再说吧再说吧。”

曾经和同事探讨一个观点,对方积极辩驳。正巧领导过来加入探讨,正巧和我观点一样。后来同事说:“他说可以,反正你说就不行。”

在早期我想推广自己的产品时,总是先从身边认识的人着手,浪费了很多时间,后来我明白了,别人不想给你抬轿子。

中国有这么多人,这么多企业,这么多网民,无论做什么,都一定要想办法打开更广阔的空间,去寻找机遇,有太多的事情可以去做。

哪怕自己去地推,去陌拜,都是可以的,一定会有收获。

得到用户的肯定之后,确实有成就感

有个用户上线,上线前在内部做了个小的压力测试,也没有提前和我说,完了才截图给我看。当时还是非常高兴的。我自己业余时间做的系统只能自己用测试工具跑一跑,肯定没有这样的人力去测试,这是第 1 次在用户真实环境真实的压测,整个过程没有任何问题。


虽然是免费软件,但是让我很有成就感。能证明我确实懂技术,做技术的不是吗?