后端代码规范

前言

虽然oinone框架减少了很多的代码,但是低代码部分的代码质量也需要高度关注,不管是写的代码bug多,或者说被吐槽代码不行,还是说写的代码经常被重构,核心点还是没有代码规范的意识和技巧,下面摘录了一些常见的规范要求,去提高后端的代码质量,代码质量提高后,自然效率也会提升。

常见代码规范

**1、规范命名**

命名是写代码中最频繁的操作,比如类、属性、方法、参数等。好的名字应当能遵循以下几点:

**见名知意**

比如需要定义一个变量需要来计数

int i = 0;

名称 i 没有任何的实际意义,没有体现出数量的意思,所以我们应当指明数量的名称

int count = 0;
**能够读的出来**

如下代码:

private String sfzh;
private String dhhm;

这些变量的名称,根本读不出来,更别说实际意义了。

所以我们可以使用正确的可以读出来的英文来命名

private String idCardNo;
private String phone;

**2、规范代码格式**

好的代码格式能够让人感觉看起来代码更加舒适。

好的代码格式应当遵守以下几点:

  • 合适的空格
  • 代码对齐,比如大括号要对齐
  • 及时换行,一行不要写太多代码

好在现在开发工具支持一键格式化,可以帮助美化代码格式,大家统一使用idea的规范即可。

**3、写好代码注释**

在《代码整洁之道》这本书中作者提到了一个观点,注释的恰当用法是用来弥补我们在用代码表达意图时的失败。换句话说,当无法通过读代码来了解代码所表达的意思的时候,就需要用注释来说明。

书的作者之所以这么说,是因为作者觉得随着时间的推移,代码可能会变动,如果不及时更新注释,那么注释就容易产生误导,偏离代码的实际意义。而不及时更新注释的原因是,程序员不喜欢写注释。😒

但是这不意味着可以不写注释,当通过代码如果无法表达意思的时候,就需要注释,比如如下代码:

for (Integer id : ids) {
    if (id == 0) {
        continue;
    }
    //做其他事
}

为什么 id == 0 需要跳过,代码是无法看出来了,就需要注释了。

好的注释应当满足一下几点:

  • 解释代码的意图,说明为什么这么写,用来做什么
  • 对参数和返回值注释,入参代表什么,出参代表什么
  • 有警示作用,比如说入参不能为空,或者代码是不是有坑
  • 当代码还未完成时可以使用 todo 注释来标记
  • 代码review发现漏洞时 可以使用 fixme 注释来标记

**4、try catch 内部代码抽成一个方法**

try catch代码有时会干扰我们阅读核心的代码逻辑,这时就可以把try catch内部主逻辑抽离成一个单独的方法

如下图是Eureka服务端源码中服务下线的实现中的一段代码

后端代码规范

整个方法非常长,try中代码是真正的服务下线的代码实现,finally可以保证读锁最终一定可以释放。

所以这段代码其实就可以对核心的逻辑进行抽取。

protected boolean internalCancel(String appName, String id, boolean isReplication) {
    try {
        read.lock();
        doInternalCancel(appName, id, isReplication);
    } finally {
        read.unlock();
    }

    // 剩余代码
}

private boolean doInternalCancel(String appName, String id, boolean isReplication) {
    //真正处理下线的逻辑
}

**5、方法别太长**

方法别太长就是字面的意思。一旦代码太长,给人的第一眼感觉就很复杂,让人不想读下去;

同时方法太长的代码可能读起来容易让人摸不着头脑,不知道哪一些代码是同一个业务的功能。

比如代码中有那种2000+行大类,各种if else判断,光理清代码思路就需要用很久时间。🤷🏻‍♀️

所以一旦方法过长,可以尝试将相同业务功能的代码单独抽取一个方法,最后在主方法中调用即可。

**6、抽取重复代码**

当一份代码重复出现在程序的多处地方,就会造成程序又臭又长,当这份代码的结构要修改时,每一处出现这份代码的地方都得修改,导致程序的扩展性很差。

所以一般遇到这种情况,可以抽取成一个工具类,还可以抽成一个公共的父类。

**7、多用return**

在有时我们平时写代码的情况可能会出现if条件套if的情况,当if条件过多的时候可能会出现如下情况:

if (条件1) {
    if (条件2) {
        if (条件3) {
            if (条件4) {
                if (条件5) {
                    System.out.println("11111");
                }
            }
        }
    }
}

面对这种情况,可以换种思路,使用return来优化

if (!条件1) {
    return;
}
if (!条件2) {
    return;
}
if (!条件3) {
    return;
}
if (!条件4) {
    return;
}
if (!条件5) {
    return;
}
System.out.println("11111");

这样优化就感觉看起来更加直观

**8、if条件表达式不要太复杂**

比如在如下代码:

if (((StringUtils.isBlank(person.getName())
        || "11111".equals(person.getName()))
        && (person.getAge() != null && person.getAge() > 10))
        && "汉".equals(person.getNational())) {
    // 处理逻辑
}

这段逻辑,这种条件表达式乍一看不知道是什么,仔细一看还是不知道是什么,这时就可以这么优化

boolean sanyouOrBlank = StringUtils.isBlank(person.getName()) || "11111".equals(person.getName());
boolean ageGreaterThanTen = person.getAge() != null && person.getAge() > 10;
boolean isHanNational = "汉".equals(person.getNational());

if (sanyouOrBlank
    && ageGreaterThanTen
    && isHanNational) {
    // 处理逻辑
}

此时就很容易看懂if的逻辑了

**9、优雅地参数校验**

当前端传递给后端参数的时候,通常需要对参数进场检验,一般可能会这么写

public void addPerson(ModelA add) {
    if (StringUtils.isBlank(add.getName())) {
        throw new BizException("人员姓名不能为空");
    }

    if (StringUtils.isBlank(add.getIdCardNo())) {
        throw new BizException("身份证号不能为空");
    }

    // 处理新增逻辑
}

这种写虽然可以,但是当字段的多的时候,光校验就占据了很长的代码,不够优雅。

针对参数校验这个问题,oinone其实提供了一些方案:

模型校验:模型之元数据详解,可以通过check

动作校验:Action之校验,可以通过@Validation 注解进行校验

其次:可以抽通用的校验Utils,进行校验的归集

**10、统一返回值**

及时我们有GraphQL统一了一层默认返回参数,但是涉及到移动端或者特殊Action的时候,后端在设计Action的时候,最好还是统一返回值。

{  
    "code":0,
    "message":"成功",
    "data":"返回数据"
}

不仅是给前端参数,也包括提供给第三方的接口等,这样接口调用方法可以按照固定的格式解析代码,不用进行判断。如果不一样,相信我,前端半夜都一定会来找你。

oinone中定义了很多的基类模型,可以用于统一回参的规范。

**11、统一异常处理**

如果系统需要做异常码的规范和翻译,那统一异常码是必须要做的事情,比如一个跨境系统,报错中文,其实就是个灾难,所以大家还是统一使用PamisException进行异常抛出。

public ModelA addPerson(Model modelA) {
  Model a = xxxxService.queryById(modelA.getId());
  if(a == null){
    throw PamirsException.construct(ModuleExpEnumerate.XXX_ERROR).errThrow();
  }
  return modelA;
}

当异常统一后,可以使用AOP进行统一的异常处理和优化,或者业务实现。

**12、尽量不传递null值**

这个很好理解,不传null值可以避免方法不支持为null入参时产生的空指针问题。

当然为了更好的表明该方法是不是可以传null值,可以通过@NonNull和@Nullable注解来标记。@NonNull就表示不能传null值,@Nullable就是可以传null值。

//示例1
public void updatePerson(@Nullable Person person) {
    if (person == null) {
        return;
    }
    personService.updateById(person);
}

//示例2
public void updatePerson(@NonNull Person person) {
    personService.updateById(person);
}

**13、尽量不返回null值**

尽量不返回null值是为了减少调用者对返回值的为null判断,如果无法避免返回null值,可以通过返回Optional去判断下。

public List<T> getPersonById(Long personId) {
    return Optional.ofNullable(personService.selectById(personId)).orElse(new ArrayList<>());
}

如果不想这么写,也可以通过@NonNull和@Nullable表示方法会不会返回null值。

**14、日志打印规范**

好的日志打印能帮助我们快速定位问题

好的日志应该遵循以下几点:

  • 可搜索性,要有明确的关键字信息
  • 异常日志需要打印出堆栈信息
  • 合适的日志级别,比如异常使用error,正常使用info
  • 日志内容太大不打印,比如有时需要将图片转成Base64,那么这个Base64就可以不用打印,如果只是为了调试某个功能,可以尝试使用debug日志,测试环境可以指定logback打印某些package的打印debug级别日志

**15、统一类库**

在一个项目中,可能会由于引入的依赖不同导致引入了很多相似功能的类库,比如常见的json类库,又或者是一些常用的工具类,当遇到这种情况下,应当规范在项目中到底应该使用什么类库,而不是一会用JsonUtils,一会使用JsonObject,我们可以统一使用Oinone的类库作为我们的使用标准。

**16、尽量使用工具类**

比如在对集合判空的时候,可以这么写

public void updatePersons(List<Person> persons) {
    if (persons != null && persons.size() > 0) {

    }
   persons.stream().map(Person::getId).collect(Collectors.toSet());
}

但是一般不推荐这么写,可以通过一些判断的工具类来写,可以统一使用工具了去进行集合或者Map的处理,可以参考:

AriesCollectionUtils�

public void updatePersons(List<Person> persons) {
    if (CollectionUtils.isNotEmpty(persons)) {

    }
    Map<Long,Person> personMap = AriesCollectionUtils.toMap(persons,Person::getId);
}

不仅集合,比如字符串的判断等等,就使用工具类,不要手动判断。

**17、尽量不要重复造轮子**

就拿格式化日期来来说,我们一般封装成一个工具类来调用,比如如下代码

private static final SimpleDateFormat DATE_TIME_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

public static String formatDateTime(Date date) {
    return DATE_TIME_FORMAT.format(date);
}

这段代码看似没啥问题,但是却忽略了SimpleDateFormat是个线程不安全的类,所以这就会引起坑。

一般对于这种已经有开源的项目并且已经做得很好的时候,比如平台底层的DateUtils,就可以把轮子直接拿过来用了。

**18、类和方法单一职责**

单一职责原则是设计模式的七大设计原则之一,它的核心意思就是字面的意思,一个类或者一个方法只做单一的功能。

就拿Nacos来说,在Nacos1.x的版本中,有这么一个接口HttpAgent

后端代码规范

这个类只干了一件事,那就是封装http请求参数,向Nacos服务端发送请求,接收响应,这其实就是单一职责原则的体现。

当其它的地方需要向Nacos服务端发送请求时,只需要通过这个接口的实现,传入参数就可以发送请求了,而不需要关心如何携带服务端鉴权参数、http请求参数如何组装等问题。

**19、继承不要太深**

继承的弊端:

  • 灵活性低。java语言是单继承的,无法同时继承很多类,并且继承容易导致代码层次太深,不易于维护
  • 耦合性高。一旦父类的代码修改,可能会影响到子类的行为

所以一般推荐使用聚合/组合代替继承。

聚合/组合的意思就是通过成员变量的方式来使用类。

比如说,OrderService需要使用UserService,可以注入一个UserService而非通过继承UserService。

聚合和组合的区别就是,组合是当对象一创建的时候,就直接给属性赋值,而聚合的方式可以通过set方式来设置。

组合:

public class OrderService {

    private UserService userService = new UserService();

}

聚合:

public class OrderService {

    private UserService userService;

    public void setUserService(UserService userService) {
        this.userService = userService;
    }
}

现实写代码的时候,其实使用@Autowired�也可以。

public class OrderService {

    @Autowired
    private UserService userService;

    public void setUserService(UserService userService) {
        this.userService = userService;
    }
}

**20、使用设计模式优化代码**

在平时开发中,使用设计模式可以增加代码的扩展性。

比如说,当你需要做一个可以根据不同的平台做不同消息推送的功能时,就可以使用策略模式的方式来优化。

设计一个接口:

public interface MessageNotifier {

    /**
     * 是否支持改类型的通知的方式
     *
     * @param type 0:短信 1:app
     * @return
     */
    boolean support(int type);

    /**
     * 通知
     *
     * @param user
     * @param content
     */
    void notify(User user, String content);

}

短信通知实现:

@Component
public class SMSMessageNotifier implements MessageNotifier {
    @Override
    public boolean support(int type) {
        return type == 0;
    }

    @Override
    public void notify(User user, String content) {
        //调用短信通知的api发送短信
    }
}

app通知实现:

public class AppMessageNotifier implements MessageNotifier {
    @Override
    public boolean support(int type) {
        return type == 1;
    }

    @Override
    public void notify(User user, String content) {
       //调用通知app通知的api
    }
}

最后提供一个方法,当需要进行消息通知时,调用notifyMessage,传入相应的参数就行。

@Autowired
private List<MessageNotifier> messageNotifiers;

public void notifyMessage(User user, String content, int notifyType) {
    for (MessageNotifier messageNotifier : messageNotifiers) {
        if (messageNotifier.support(notifyType)) {
            messageNotifier.notify(user, content);
        }
    }
}

假设此时需要支持通过邮件通知,只需要有对应实现就行,这里就可以使用【工厂模式】去协助处理不同的发送者。

**21、不滥用设计模式**

用好设计模式可以增加代码的扩展性,但是滥用设计模式确是不可取的。

public void printPerson(Person person) {
    StringBuilder sb = new StringBuilder();
    if (StringUtils.isNotBlank(person.getName())) {
        sb.append("姓名:").append(person.getName());
    }
    if (StringUtils.isNotBlank(person.getIdCardNo())) {
        sb.append("身份证号:").append(person.getIdCardNo());
    }

    // 省略
    System.out.println(sb.toString());
}

比如上面打印Person信息的代码,用if判断就能够做到效果,你说我要不用责任链或者什么设计模式来优化一下吧,没必要。

**22、面向接口编程**

在一些可替换的场景中,应该引用父类或者抽象,而非实现。

举个例子,在实际项目中可能需要对一些图片进行存储,但是存储的方式很多,比如可以选择阿里云的OSS,又或者是七牛云,MINIO存储服务器等等。所以对于存储图片这个功能来说,这些具体的实现是可以相互替换的。

所以在项目中,我们不应当在代码中耦合一个具体的实现,而是可以提供一个存储接口

public interface FileStorage {

    String store(String fileName, byte[] bytes);

}

如果选择了阿里云OSS作为存储服务器,那么就可以基于OSS实现一个FileStorage,在项目中哪里需要存储的时候,只要实现注入这个接口就可以了。

@Autowired
private FileStorage fileStorage;

假设用了一段时间之后,发现阿里云的OSS比较贵,此时想换成七牛云的,那么此时只需要基于七牛云的接口实现FileStorage接口,然后注入到IOC,那么原有代码用到FileStorage根本不需要动,实现轻松的替换。

**23、经常重构旧的代码**

随着时间的推移,业务的增长,有的代码可能不再适用,或者有了更好的设计方式,那么可以及时的重构业务代码。

就拿上面的消息通知为例,在业务刚开始的时候可能只支持短信通知,于是在代码中就直接耦合了短信通知的代码。但是随着业务的增长,逐渐需要支持app、邮件之类的通知,那么此时就可以重构以前的代码,抽出一个策略接口,进行代码优化。

重构过程中,不要通过注释代码的方式去做逻辑保留,会导致代码可读性更差,建议直接删除,调整代码结构,保证代码简洁清晰。

**24、null值判断**

空指针是代码开发中的一个难题,作为程序员的基本修改,应该要防止空指针。

可能产生空指针的原因:

  • 数据返回对象为null
  • 自动拆箱导致空指针
  • 调用方法返回的对象可能为空值

所以在需要这些的时候,需要强制判断是否为null。前面也提到可以使用Optional来优雅地进行null值判断。

**25、模型类充血模式**

针对于很多场景,希望做到模型类的统一处理变化,可以写一些充血方法,因为平台底层重写了lombok插件,所以重写模型的方法时候,需要去class中找到具体的get、set方法,例如:

public class ModelA {

  private String salesOrderId;

  public Long getSalesOrderId() {
    Object obj = this._d.get("salesOrderId");
    Long salesOrderId = obj == null ? null : Long.valueOf(obj.toString());
    //xxxx 做自己的逻辑
    return salesOrderId;
  }
}

**26、魔法值用常量表示**

public void sayHello(String province) {
    if ("广东省".equals(province)) {
        System.out.println("靓仔~~");
    } else {
        System.out.println("帅哥~~");
    }
}

代码里,广东省就是一个魔法值,那么就可以将用一个常量来保存

private static final String GUANG_DONG_PROVINCE = "广东省";

public void sayHello(String province) {
    if (GUANG_DONG_PROVINCE.equals(province)) {
        System.out.println("靓仔~~");
    } else {
        System.out.println("帅哥~~");
    }
}

**27、资源释放写到finally**

比如在使用一个api类锁或者进行IO操作的时候,需要主动写代码需释放资源,为了能够保证资源能够被真正释放,那么就需要在finally中写代码保证资源释放。

后端代码规范

如图所示,就是CopyOnWriteArrayList的add方法的实现,最终是在finally中进行锁的释放。

或者统一使用:JDK7 新特性之try-with-resources-CSDN博客

**28、使用线程池代替手动创建线程**

使用线程池还有以下好处:

  • 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
  • 提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
  • 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统 的稳定性,使用线程池可以进行统一的分配,调优和监控。

所以为了达到更好的利用资源,提高响应速度,就可以使用线程池的方式来代替手动创建线程。

如果对线程池不清楚的同学,可以看一下这篇文章:7000字+24张图带你彻底弄懂线程池

一个项目中最好是统一维护线程池的定义,不要出现线程池散落的情况。

**29、线程设置名称**

在日志打印的时候,日志是可以把线程的名字给打印出来。

后端代码规范

如上图,日志打印出来的就是tom猫的线程。

所以,设置线程的名称可以帮助我们更好的知道代码是通过哪个线程执行的,更容易排查问题。

**30、涉及线程间可见性加volatile**

在RocketMQ源码中有这么一段代码

后端代码规范

在消费者在从服务端拉取消息的时候,会单独开一个线程,执行while循环,只要stopped状态一直为false,那么就会一直循环下去,线程就一直会运行下去,拉取消息。

当消费者客户端关闭的时候,就会将stopped状态设置为true,告诉拉取消息的线程需要停止了。但是由于并发编程中存在可见性的问题,所以虽然客户端关闭线程将stopped状态设置为true,但是拉取消息的线程可能看不见,不能及时感知到数据的修改,还是认为stopped状态设置为false,那么就还会运行下去。

针对这种可见性的问题,java提供了一个volatile关键字来保证线程间的可见性。

后端代码规范

所以,源码中就加了volatile关键字。

加了volatile关键字之后,一旦客户端的线程将stopped状态设置为true时候,拉取消息的线程就能立马知道stopped已经是false了,那么再次执行while条件判断的时候,就不成立,线程就运行结束了,然后退出。

**31、考虑线程安全问题**

大家掌握多线程增加处理效率后,其实同时也会带来线程安全问题,比如ArrayList就是一个非线程安全的集合,多线程处理该集合数据会出现数据错乱的问题,所以我们要知道一些线程安全的类库。

例如 ArrayList就可以替换为CopyOnWriteArrayListCollections.synchronizedList() 方法去定义集合。

CopyOnWriteArrayList<Integer> list = new CopyOnWriteArrayList<>();

List<Integer> list = Collections.synchronizedList(new ArrayList<>());

所以,在实际中,需要多考虑考虑业务是否有线程安全问题,有集合读写安全问题,那么就用线程安全的集合,业务有安全的问题,那么就可以通过加锁的手段来解决。

**32、慎用异步**

虽然在使用多线程可以帮助我们提高接口的响应速度,但是也会带来很多问题。

事务问题

一旦使用了异步,就会导致两个线程不是同一个事务的,导致异常之后无法正常回滚数据。

cpu负载过高

之前有个小伙伴遇到需要同时处理几万调数据的需求,每条数据都需要调用很多次接口,为了达到老板期望的时间要求,使用了多线程跑,开了很多线程,此时会发现系统的cpu会飙升

意想不到的异常

还是上面的提到的例子,在测试的时候就发现,由于并发量激增,在请求第三方接口的时候,返回了很多错误信息,导致有的数据没有处理成功。

虽然说慎用异步,但不代表不用,如果可以保证事务的问题,或是CPU负载不会高的话,那么还是可以使用的。

消息队列(MQ)或者 Oinone的触发任务也是一个异步的解决方案:函数之触发与定时

**33、减小锁的范围**

减小锁的范围就是给需要加锁的代码加锁,不需要加锁的代码不要加锁。这样就能减少加锁的时间,从而可以较少锁互斥的时间,提高效率。

后端代码规范

比如CopyOnWriteArrayList的addAll方法的实现,lock.lock(); 代码完全可以放到代码的第一行,但是作者并没有,因为前面判断的代码不会有线程安全的问题,不放到加锁代码中可以减少锁抢占和占有的时间。

**34、有类型区分时定义好枚举**

比如在项目中不同的类型的业务可能需要上传各种各样的附件,此时就可以定义好不同的一个附件的枚举,来区分不同业务的附件。

不要在代码中直接写死,不定义枚举,代码阅读起来非常困难,直接看到数字都是懵逼的。。

**35、集合使用应当指明初始化大小**

比如在写代码的时候,经常会用到List、Map来临时存储数据,其中最常用的就是ArrayList和HashMap。但是用不好可能也会导致性能的问题。

比如说,在ArrayList中,底层是基于数组来存储的,数组是一旦确定大小是无法再改变容量的。但不断的往ArrayList中存储数据的时候,总有那么一刻会导致数组的容量满了,无法再存储其它元素,此时就需要对数组扩容。所谓的扩容就是新创建一个容量是原来1.5倍的数组,将原有的数据给拷贝到新的数组上,然后用新的数组替代原来的数组。

在扩容的过程中,由于涉及到数组的拷贝,就会导致性能消耗;同时HashMap也会由于扩容的问题,消耗性能。所以在使用这类集合时可以在构造的时候指定集合的容量大小。

**36、尽量不要使用BeanUtils来拷贝属性**

在开发中经常需要对JavaBean进行转换,但是又不想一个一个手动set,比较麻烦,所以一般会使用属性拷贝的一些工具,比如说Spring提供的BeanUtils来拷贝。不得不说,使用BeanUtils来拷贝属性是真的舒服,使用一行代码可以代替几行甚至十几行代码,我也喜欢用。

但是喜欢归喜欢,但是会带来性能问题,因为底层是通过反射来的拷贝属性的,所以尽量不要用BeanUtils来拷贝属性。

比如你可以装个generateallsetter插件转换的插件,帮你自动生成转换代码;

后端代码规范

或者使用ArgUtils针对模型处理,会更方便去处理;

或者可以使用性能更高的MapStruct来进行JavaBean转换,MapStruct底层是通过调用(settter/getter)来实现的,而不是反射来快速执行。

**37、使用StringBuilder进行字符串拼接**

如下代码:

String str1 = "123";
String str2 = "456";
String str3 = "789";
String str4 = str1 + str2 + str3;

使用 + 拼接字符串的时候,会创建一个StringBuilder,然后将要拼接的字符串追加到StringBuilder,再toString,这样如果多次拼接就会执行很多次的创建StringBuilder,z执行toString的操作。

所以可以手动通过StringBuilder拼接,这样只会创建一次StringBuilder,效率更高。

StringBuilder sb = new StringBuilder();
String str = sb.append("123").append("456").append("789").toString();

**38、使用编程式事务**

使用注解代码省了大家很多事情,但是代码层级太深时候,对于事务处理来说就需要看大量的代码去,很难去发现事务不一致的情况,对于性能也是一个灾难。

@Transactional一时爽,性能优化就是个灾难!

Tx.build(new TxConfig().setPropagation(Propagation.REQUIRED.value())).executeWithoutResult(status -> {

    //逻辑代码
});

**39、大事务拆小事务**

事务会导致很直接的性能问题,例如锁表、锁行等情况,代码执行就会很慢,所以我们可以通过编程式事务的写法控制事务包裹的代码内容,同时也可以通过new TxConfig().setPropagation(Propagation.REQUIRED.value()))�,去控制事务的传播行为,让事务能够快速结束。

比如查询之类准备数据的可以放在事务外处理,其次用了非强一致事务要求的,可以用Propagation.REQUIRED,去用新的事物处理,保障事物处理快。

List<T> data = xxxService.select(xx);
Tx.build(new TxConfig().setPropagation(Propagation.REQUIRED.value())).executeWithoutResult(status -> {
    data.updateById();                                                                                    
});

**40、谨慎方法内部调用动态代理的方法**

如下事务代码

@Service
public class PersonService {

    public void update(Person person) {
        // 处理
        updatePerson(person);
    }

    @Transactional(rollbackFor = Exception.class)
    public void updatePerson(Person person) {
        // 处理
    }

}

update调用了加了@Transactional注解的updatePerson方法,那么此时updatePerson的事务就是失效。

其实失效的原因不是事务的锅,是由AOP机制决定的,因为事务是基于AOP实现的。AOP是基于对象的代理,当内部方法调用时,走的不是动态代理对象的方法,而是原有对象的方法调用,如此就走不到动态代理的代码,就会失效了。

如果实在需要让动态代理生效,可以注入自己的代理对象

@Service
public class PersonService {

    @Autowired
    private PersonService personService;

    public void update(Person person) {
        // 处理
        personService.updatePerson(person);
    }

    @Transactional(rollbackFor = Exception.class)
    public void updatePerson(Person person) {
        // 处理
    }

}

**41、不循环调用数据库**

不要在循环中访问数据库,这样会严重影响数据库性能。

比如需要查询一批人员的信息,人员的其他信息存在其他表中,错误的代码如下:

public List<Person> selectPersons(List<Long> personIds) {
  List<Person> persons = new Person().queryByIds(personIds);
  for (Person person : persons) {
    PersonOther personOther = new PersonOther().queryOtherById(person.getId());
    // 组装数据
    person.setOther(personOther);
  }
  return persons;
}

正确的写法是查询所有的数据,通过map在for循环中set值:

public List<Person> selectPersons(List<Long> personIds) {
  List<Person> persons = new Person().queryByIds(personIds);
  List<PersonOther> personOthers = new PersonOther().queryByPersonIds(personIds);
  Map<Long,PersonOther> otherMap = AriesCollectionUtils.toMap(personOthers,PersonOther::getPersonId); 
  for (Person person : persons) {
    // 组装数据
    person.setOther(otherMap.get(person.getId()));
  }
  return persons;
}

**42、需要什么字段select什么字段**

查询全字段有以下几点坏处:

**增加不必要的字段的网络传输**

比如有些文本的字段,存储的数据非常长,但是本次业务使用不到,但是如果查了就会把这个数据返回给客户端,增加了网络传输的负担

会导致无法使用到覆盖索引

比如说,现在有身份证号和姓名做了联合索引,现在只需要根据身份证号查询姓名,如果直接select name 的话,那么在遍历索引的时候,发现要查询的字段在索引中已经存在,那么此时就会直接从索引中将name字段的数据查出来,返回,而不会继续去查找聚簇索引,减少回表的操作。

所以建议是需要使用什么字段查询什么字段。oinone中的Wrapper支持这样的写法,如下:

LambdaQueryWrapper<AriesCompany> wrapper = Pops.<AriesCompany>lambdaQuery()
  .from(AriesCompany.MODEL_MODEL)
  .select(AriesCompany::getId)
  .eq(AriesCompany::getId, data.getId());

**43、用业务代码代替多表join**

如上面代码所示,原本也可以将两张表根据人员的id进行关联查询。但是不推荐这么,阿里也禁止多表join的操作

后端代码规范

而之所以会禁用,是因为join的效率比较低。

MySQL是使用了嵌套循环的方式来实现关联查询的,也就是for循环会套for循环的意思。用第一张表做外循环,第二张表做内循环,外循环的每一条记录跟内循环中的记录作比较,符合条件的就输出,这种效率肯定低。

其次,join对分表也是种伤害,慎用!

**44、装上阿里代码检查插件**

我们平时写代码由于各种原因,比如项目紧张、功能重要,一直催进度,导致写代码都来不及思考,怎么快怎么来,cv大法上线,虽然有心想写好代码,但是手确不听使唤。所以建议装一个阿里的代码规范插件,如果有代码不规范,会有提醒,这样就可以知道哪些是可以优化的了。

后端代码规范

如果你有强迫症,相信我,装了这款插件,你的代码会写的很漂亮。

后端代码规范

**45、及时跟团队沟通**

写代码的时候不能闭门造车,及时跟团队的同事们沟通,比如Oinone的一些特性你不知道,一些技术方案不了解,一些特殊逻辑不清晰,如果上来就直接写代码,很有可能就会踩坑。

:: 代码写的漂亮,也一定会变得更受欢迎,希望大家都成为受人喜爱和尊敬的程序猿**👨🏻‍💻**::

Oinone社区 作者:冯, 天宇原创文章,如若转载,请注明出处:https://doc.oinone.top/backend/19837.html

访问Oinone官网:https://www.oinone.top获取数式Oinone低代码应用平台体验

(3)
冯, 天宇的头像冯, 天宇数式员工
上一篇 2024年12月11日 am10:42
下一篇 2024年12月11日 pm2:57

相关推荐

  • 【PostgreSQL】后端部署使用PostgreSQL数据库

    PostgreSQL数据库配置 驱动配置 Maven配置(14.3版本可用) <postgresql.version>42.6.0</postgresql.version> <dependency> <groupId>org.postgresql</groupId> <artifactId>postgresql</artifactId> <version>${postgresql.version}</version> </dependency> 离线驱动下载 postgresql-42.2.18.jarpostgresql-42.6.0.jarpostgresql-42.7.3.jar JDBC连接配置 pamirs: datasource: base: type: com.alibaba.druid.pool.DruidDataSource driverClassName: org.postgresql.Driver url: jdbc:postgresql://127.0.0.1:5432/pamirs?currentSchema=base username: xxxxxx password: xxxxxx 连接url配置 暂无官方资料 url格式 jdbc:postgresql://${host}:${port}/${database}?currentSchema=${schema} 在jdbc连接配置时,${database}和${schema}必须完整配置,不可缺省。 其他连接参数如需配置,可自行查阅相关资料进行调优。 方言配置 pamirs方言配置 pamirs: dialect: ds: base: type: PostgreSQL version: 14 major-version: 14.3 pamirs: type: PostgreSQL version: 14 major-version: 14.3 数据库版本 type version majorVersion 14.x PostgreSQL 14 14.3 PS:由于方言开发环境为14.3版本,其他类似版本(14.x)原则上不会出现太大差异,如出现其他版本无法正常支持的,可在文档下方留言。 schedule方言配置 pamirs: event: enabled: true schedule: enabled: true dialect: type: PostgreSQL version: 14 major-version: 14.3 type version majorVersion PostgreSQL 14 14.3 PS:由于schedule的方言在多个版本中并无明显差异,目前仅提供一种方言配置。 其他配置 逻辑删除的值配置 pamirs: mapper: global: table-info: logic-delete-value: (EXTRACT(epoch FROM CURRENT_TIMESTAMP) * 1000000 + EXTRACT(MICROSECONDS FROM CURRENT_TIMESTAMP))::bigint PostgreSQL数据库用户初始化及授权 — init root user (user name can be modified by oneself) CREATE USER root WITH PASSWORD 'password'; — if using automatic database and schema creation, this is very important. ALTER USER root CREATEDB; SELECT * FROM pg_roles; — if using postgres database, this authorization is required. GRANT CREATE ON DATABASE postgres TO root;

    2023年11月1日
    86700
  • 【OpenGauss】后端部署使用OpenGauss高斯数据库

    Gauss数据库配置 适配版本 4.7.8.3之后的版本 配置步骤 Maven配置 去华为官网下周驱动包:gsjdbc4.jar;https://support.huaweicloud.com/mgtg-dws/dws_01_0032.html <dependency> <groupId>org.postgresql</groupId> <artifactId>gsjdbc</artifactId> <version>4</version> <scope>system</scope> <!– 下面两种方式都可以–> <systemPath>${pom.basedir}/libs/gsjdbc4.jar</systemPath> <!–<systemPath>/Users/wangxian/java-tools/guassdb/gsjdbc4.jar</systemPath>–> </dependency> 导入 scope 为 system 的包,spring 编译插件需要增加 includeSystemScope: true 配置。 <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <includeSystemScope>true</includeSystemScope> </configuration> <executions> <execution> <goals> <goal>repackage</goal> </goals> </execution> </executions> </plugin> JDBC连接配置 pamirs: datasource: pamirs: type: com.alibaba.druid.pool.DruidDataSource driverClassName: org.postgresql.Driver url: jdbc:postgresql://127.0.0.1:5432/pamirs?currentSchema=demo username: XXXXXX password: XXXXXX initialSize: 5 maxActive: 200 minIdle: 5 maxWait: 60000 timeBetweenEvictionRunsMillis: 60000 testWhileIdle: true testOnBorrow: false testOnReturn: false poolPreparedStatements: true asyncInit: true base: type: com.alibaba.druid.pool.DruidDataSource driverClassName: org.postgresql.Driver url: jdbc:postgresql://127.0.0.1:5432/pamirs?currentSchema=demo_base username: XXXXXX password: XXXXXX initialSize: 5 maxActive: 200 minIdle: 5 maxWait: 60000 timeBetweenEvictionRunsMillis: 60000 testWhileIdle: true testOnBorrow: false testOnReturn: false poolPreparedStatements: true asyncInit: true 连接url配置 点击查看官方文档:官方文档 url格式 jdbc:postgresql://${host}:${port}/${database}?currentSchema=${schema} 在pamirs连接配置时,${database}和${schema}必须完整配置,不可缺省。 其他连接参数如需配置,可自行查阅相关资料进行调优。 方言配置 pamirs方言配置 pamirs: dialect: ds: base: type: GaussDB version: 5 majorVersion: 5.0.1 pamirs: type: GaussDB version: 5 majorVersion: 5.0.1 数据库版本 type version majorVersion 5.x GaussDB 5 5.0.1 PS:由于方言开发环境为5.0.1版本,其他类似版本(5.x)原则上不会出现太大差异,如出现其他版本无法正常支持的,可在文档下方留言。 schedule方言配置 pamirs: event: enabled: true schedule: enabled: true dialect: type: GaussDB version: 5 major-version: 5.0.1 type version majorVersion GaussDB 5 5.0.1 PS:由于schedule的方言在多个版本中并无明显差异,目前仅提供一种方言配置。 其他配置 逻辑删除的值配置 pamirs: mapper: global: table-info: logic-delete-value: (EXTRACT(epoch FROM CURRENT_TIMESTAMP) * 1000000 + EXTRACT(MICROSECONDS FROM CURRENT_TIMESTAMP))::bigint Gauss数据库用户初始化及授权 — init root…

    2024年3月27日
    2.0K00
  • 如何通过传输模型完成页面能力

    介绍 在业务中我们经常能遇到这种场景,我们的数据是通过调用第三方接口获取的,在业务系统中没有对应的存储模型,但是我们又需要展示这些数据,这时候可以利用传输模型不建表的特性完成这个功能。 定义传输模型 package pro.shushi.pamirs.demo.api.tmodel; import pro.shushi.pamirs.meta.annotation.Field; import pro.shushi.pamirs.meta.annotation.Model; import pro.shushi.pamirs.meta.base.TransientModel; @Model.model(DemoCreateOrder.MODEL_MODEL) @Model(displayName = "下单页面模型") public class DemoCreateOrder extends TransientModel { public static final String MODEL_MODEL = "demo.DemoCreateOrder"; @Field.Integer @Field(displayName ="下单人uid") private Long userId; } 定义action,由于传输模型用于表现层和应用层之间的数据交互,本身不会存储,没有默认的数据管理器,只有数据构造器,所以需要手动添加所需的queryOne、create、update等方法 注意:传输模型没有数据管理器能力,所以不提供类似queryPage的方法,后续版本考虑支持中 package pro.shushi.pamirs.demo.core.action; import org.springframework.stereotype.Component; import pro.shushi.pamirs.demo.api.tmodel.DemoCreateOrder; import pro.shushi.pamirs.meta.annotation.Action; import pro.shushi.pamirs.meta.annotation.Function; import pro.shushi.pamirs.meta.annotation.Model; import pro.shushi.pamirs.meta.api.dto.condition.Pagination; import pro.shushi.pamirs.meta.api.dto.wrapper.IWrapper; import pro.shushi.pamirs.meta.constant.FunctionConstants; import pro.shushi.pamirs.meta.enmu.FunctionOpenEnum; import pro.shushi.pamirs.meta.enmu.FunctionTypeEnum; import pro.shushi.pamirs.meta.enmu.ViewTypeEnum; import static pro.shushi.pamirs.meta.enmu.FunctionOpenEnum.*; @Component @Model.model(DemoCreateOrder.MODEL_MODEL) public class DemoCreateOrderAction { @Function.Advanced(type = FunctionTypeEnum.QUERY) @Function.fun(FunctionConstants.queryByEntity) @Function(openLevel = {LOCAL, REMOTE, API}) public DemoCreateOrder queryOne(DemoCreateOrder query) { return query; } @Action.Advanced(name = FunctionConstants.create, managed = true) @Action(displayName = "创建", label = "确定", summary = "添加", bindingType = ViewTypeEnum.FORM) @Function(name = FunctionConstants.create) @Function.fun(FunctionConstants.create) public DemoCreateOrder create(DemoCreateOrder data) { return data; } @Action.Advanced(name = FunctionConstants.update, managed = true) @Action(displayName = "确定", summary = "修改", bindingType = ViewTypeEnum.FORM) @Function(name = FunctionConstants.update) @Function.fun(FunctionConstants.update) public DemoCreateOrder update(DemoCreateOrder data) { return data; } }

    2024年5月24日
    89100
  • 工作流引入流程概览与流程监控

    流程概览依赖说明 使用 流程概览 功能前,需要在项目中引入 pamirs-workflow-datavi-core 依赖: <dependency> <groupId>pro.shushi.pamirs.workflow</groupId> <artifactId>pamirs-workflow-datavi-core</artifactId> </dependency> 警告: 在 oinone 平台启用「流程概览」能力时,应用启动模块一旦引入 pamirs-workflow-api/core,必须同时引入 pamirs-workflow-datavi-api/core。在多启动模块架构下,严禁出现仅部分启动模块引入 pamirs-workflow-core 而未引入 pamirs-workflow-datavi-core 的情况,否则将导致流程概览相关元数据计算异常,出现删表等情况。 流程概览配置项 流程概览页面内置缓存机制,可通过配置项调整缓存刷新周期及图表展示的数据条数: pamirs: workflow: dashboard: cache-time: 10 # 流程概览缓存刷新时间(单位:分钟),默认 10 分钟 page-size: 10 # 流程运行分析中 4 个图表的展示数量,默认查询前 10 条数据 统计指标说明 引入 pamirs-workflow-datavi-core 依赖后,系统会按照以下规则进行数据同步: 当日数据同步:每小时同步一次当日数据; 昨日数据同步:次日凌晨同步前一日数据。 由于在引入依赖后才会开始执行数据同步,统计指标页提供了「同步」按钮,可用于对历史数据进行补采。即使不执行历史同步,也不会影响核心业务流程,仅会影响统计数据和图表的展示效果。 统计指标数据主要用于 支撑 流程概览 和 流程监控 中的统计图表展示; 为数据分析与可视化提供基础数据。 上述统计数据对工作流的审批、流转等核心业务无任何影响。如有需要,也可以基于流程监控的数据,配合数据可视化设计器,自定义构建符合业务需求的展示页面。

    1天前
    1400
  • OSS(CDN)配置和文件系统的一些操作

    目前Oinone支持的OSS类型 类型 服务 OSS 阿里云OSS UPYUN 又拍云 MINIO MinIO HUAWEI_OBS 华为云OBS LOCAL 本地NGINX文件存储 TENCENT_COS 腾讯云COS OSS通用yaml配置 cdn: oss: name: # 名称 type: # 类型 bucket: uploadUrl: # 上传URL downloadUrl: # 下载URL accessKeyId: accessKeySecret: mainDir: # 主目录 validTime: 3600000 timeout: 600000 active: true referer: localFolderUrl: others: [key]: name: # 名称 type: # 类型 bucket: uploadUrl: # 上传URL downloadUrl: # 下载URL accessKeyId: accessKeySecret: mainDir: # 主目录 validTime: 3600000 timeout: 600000 active: true referer: localFolderUrl: PS:others中使用自定义key来指定OSS服务进行文件上传/下载功能。上传/下载必须匹配,否则无法正常使用。 OSS配置示例 阿里云OSS cdn: oss: name: 阿里云 type: OSS bucket: pamirs(根据实际情况修改) uploadUrl: oss-cn-hangzhou.aliyuncs.com downloadUrl: oss-cn-hangzhou.aliyuncs.com accessKeyId: 你的accessKeyId accessKeySecret: 你的accessKeySecret # 根据实际情况修改 mainDir: upload/ validTime: 3600000 timeout: 600000 active: true imageResizeParameter: referer: 华为云OBS cdn: oss: name: 华为云 type: HUAWEI_OBS bucket: pamirs(根据实际情况修改) uploadUrl: obs.cn-east-2.myhuaweicloud.com downloadUrl: obs.cn-east-2.myhuaweicloud.com accessKeyId: 你的accessKeyId accessKeySecret: 你的accessKeySecret # 根据实际情况修改 mainDir: upload/ validTime: 3600000 timeout: 600000 active: true allowedOrigin: http://192.168.95.31:8888,https://xxxx.xxxxx.com referer: 华为云OBS需要在启动工程增加以下依赖 <okhttp3.version>4.9.3</okhttp3.version> <dependency> <groupId>com.squareup.okhttp3</groupId> <artifactId>okhttp</artifactId> <version>${okhttp3.version}</version> </dependency> 注意事项华为云OBS的防盗链配置,仅允许携带特定referer的才可以,而excel导入后端处理的逻辑匿名读的时候是不带referer的,所以会被拒绝 MINIO 文件系统,mino的配置: cdn: oss: name: minio type: MINIO bucket: pamirs(根据实际情况修改) uploadUrl: http://192.168.243.6:32190(根据实际情况修改) downloadUrl: http://192.168.243.6:9000(根据实际情况修改) accessKeyId: 你的accessKeyId accessKeySecret: 你的accessKeySecret # 根据实际情况修改 mainDir: upload/ validTime: 3600000 timeout: 600000 active: true referer: localFolderUrl: MINIO无公网访问地址下OSS的配置方式: https://doc.oinone.top/yun-wei-shi-jian/7112.html 又拍云 cdn: oss: name: 又拍云 type: UPYUN…

    后端 2023年11月1日
    1.4K00

Leave a Reply

登录后才能评论