代码调优Java开发中总结的代码质量优化技巧

Posted chenry777

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了代码调优Java开发中总结的代码质量优化技巧相关的知识,希望对你有一定的参考价值。

1、MyBatis导致的SQL注入

SQL注入是一种数据库攻击手段。攻击者通过向应用程序提交恶意代码来改变原SQL语句的含义,进而执行任意SQL命令,达到入侵数据库乃至操作系统的目的。在Mybatis Mapper Xml中,#变量名称创建参数化查询SQL语句,即占位符’ ? ',不会导致SQL注入。而$变量名称直接使用SQL指令,将会存在一定风险,当SQL指令所需的数据来源于不可信赖的数据源时,可能会导致SQL注入。

这里可以简单地理解为#{a},a为变量,会在SQL语句中被替换为’ a ';${a},a为变量,则会在SQL语句中被替换为a本身。

例如:以下代码片段采用${变量名称}动态地构造并执行了SQL查询。

<!--select user information by name-->

<select id="queryByUserName" resultMap="userResultMap" parameterType="String">

select * from db_user where user_name=${username}

</select>

如果攻击者能够替代username中的任意字符串,它们可以使用下面的关于username的字符串进行SQL注入。

validuser' OR '1'='1

当其注入到命令时,命令就会变成:

select * from db_user where user_name ='validuser' OR '1'='1'。即使所输入字符串不是来源于不可信赖的数据源,程序仍然存在着一定风险。

解决思路:

在能够使用#的地方,直接使用#代替$。如果在不能直接使用#的场景,例如需要加入字段本身的地方:

  • 模糊查询like

    • 修改前:select * from db_user where user_name like '%${likeColumn}%'
    • 修改后:select * from db_user where user_name like concat('%',#{likeColumn},'%')

    从 ${} 改为 #{},从sql拼接到 concat方式

  • 范围查询in

    • 修改前: select * from db_user where id in (${id})
    • 修改后:select * from aaa where id in <foreach collection="ids" item="item" open="("separator="," close=")">#{item} </foreach>

    用mybatis的for-each标签替代

  • 排序order by

    • order by 后面的字段写死在应用程序内,不开放给外部调用
    • order by 后面的字段增加一个范围限制,即设置白名单,每次传参进来时判断该字段是可信的

2、SQL注入

直接的SQL语句拼接可能会导致SQL注入,原因和1一样。

public void doPrivilegedAction(String username, char[] password) throws SQLException {
	
    Connection connection = getConnection();
		if (connection == null) {
		// handle error
	}
    
	try {
		String pwd = hashPassword(password);
		String sqlString = "SELECT * FROM db_user WHERE username = '" + username + "' AND password = '" + pwd + "'";
		Statement stmt = connection.createStatement();
		ResultSet rs = stmt.executeQuery(sqlString);
		if (!rs.next()) {
			throw new SecurityException( "User name or password incorrect");
		}
		// Authenticated; proceed
	} finally {
		try {
			connection.close();
		} catch (SQLException x) {
		// forward to handler
		}
	}
}

如果攻击者能够替代usernamepassword中的任意字符串,它们可以使用下面的关于username的字符串进行SQL注入。

validuser' OR '1'='1

当其注入到命令时,命令就会变成:

SELECT * FROM db_user WHERE username='validuser' OR '1'='1' AND password=''

同样,攻击者可以为password提供如下字符串。

' OR '1'='1

当其注入到命令时,命令就会变成:

SELECT * FROM db_user WHERE username='' AND password='' OR '1'='1'

解决思路:

使用java.sql.PreparedStatement代替java.sql.Statement,做一个预编译,例如,select * from db_user where username=? and password=?,然后向PreparedStatement对象中添加对应的属性

public void doPrivilegedAction(String username, char[] password) throws SQLException {
    
    Connection connection = getConnection();
    if (connection == null) {
    // Handle error
    }
    try {
        String pwd = hashPassword(password);
        // Ensure that the length of user name is legitimate
        if ((username.length() > 8) {
        // Handle error
    }

	String sqlString = "select * from db_user where username=? and password=?";
	PreparedStatement stmt = connection.prepareStatement(sqlString);
    stmt.setString(1, username);
	stmt.setString(2, pwd);
	ResultSet rs = stmt.executeQuery();
	if (!rs.next()) {
		throw new SecurityException("User name or password incorrect");
	}
	try {
        connection.close();
	} catch (SQLException x) {
        // forward to handler
    } finally {
        // forward to handler          
	}
}

3、不安全的随机数

Java API中提供了java.util.Random类实现PRNG(),该PRNG是可移植和可重复的,如果两个java.util.Random类的实例使用相同的种子,会在所有Java实现中生成相同的数值序列。

例如:下面代码片段中,使用了java.util.Random类,该类对每一个指定的种子值生成同一个序列。

import java.util.Random;

public static void main (String args[]) {
	
    for (int i = 0; i < 10; i++) {
	Random random = new Random(123456);
	int number = random.nextInt(21);
	...
	}
}

解决思路:

在安全性要求较高的应用中,应使用更安全的随机数生成器,如java.security.SecureRandom类。

例如:下面代码片段中,使用java.security.SecureRandom来生成更安全的随机数。

import java.security.SecureRandom;
import java.security.NoSuchAlgorithmException;

public static void main (String args[]) {

	try {
		SecureRandom random = SecureRandom.getInstance("SHA1PRNG");
		for (int i = 0; i < 10; i++) {
		int number = random.nextInt(21);
	...
	} catch (NoSuchAlgorithmException nsae) {
		...
	} 
}

使用java.security.SecureRandom可以确保是真*伪随机数,详见真伪随机数SecureRandom

4、硬编码的密码

程序中采用硬编码方式处理密码,一方面会降低系统安全性,另一方面不易于程序维护。

例如:下列代码中采用硬编码方式处理密码。

public class ConnectionConfig{
	
    String url = "localhost";
	String name = "admin";
	String password = "123456";
	...
}

解决思路:

程序中不应对密码进行硬编码,可以使用配置文件或数据库存储的方式来存储系统所需的数据;并且录入数据时,还可以在对敏感数据做加密处理之后再进行数据的录入,对于双向的加密算法可以解密后判断;对于单向的加密算法,可以通过对输入值的再次加密来判断。

可以构造一个属性文件工具类PropertiesUtil和加解密工具类EncryptUtil,把敏感数据加密后存在属性文件中,需要的时候再解码出来调用。比如可以用jasypt加密密码。

例如:下列代码中从配置文件中获取经过加密的密码值并解密使用。

public class ConnectionConfig{
	
    String url = EncryptUtil.decrypt(PropertiesUtil.get("connection.url"));
	String name = EncryptUtil.decrypt(PropertiesUtil.get("connection.username"));
	String password = EncryptUtil.decrypt(PropertiesUtil.get("connection.password"));
	...
}

jasypt详细使用可查:springboot使用jasypt加密敏感数据

5、SimpleDateFormat的线程不安全

public class DateUtil{
	
    //全局属性 new SimpleDateFormat
	private static SimpleDateFormat dateFormatter = new SimpleDateFormat();
    
    /**
     * 格式化时间到毫秒级
     */
    public static String longDateFormat(Long time, String dateFormat) {

        if (time == null) {
            return null;
        }
        dateFormatter.applyPattern(dateFormat);
        return dateFormatter.format(new Date(time));
    }    
}

这样一个时间工具类,在并发场景下,可能会产生线程不安全的情况,即某些调用者线程会读取到意料之外的日期(非自己输入),出现了幻读。

下图来自网络:

img

写个单元测试看一下效果:

public class test{
    
	@Test
    public void test1() {
        //创建自定义线程对象
        MyThread mt = new MyThread("新的线程!");
        //开启新线程
        mt.start();
        //在主方法中执行for循环
        for (int i = 0; i < 100; i++) {
            logger.info("main线程!" + i + DateUtil.longDateFormat(1692517360211l,"yyyy-MM-dd HH-mm-ss"));
        }
    }

    class MyThread extends Thread {
        //定义指定线程名称的构造方法
        public MyThread(String name) {
            //调用父类的String参数的构造方法,指定线程的名称
            super(name);
        }
        /**
         * 重写run方法,完成该线程执行的逻辑
         */
        @Override
        public void run() {
            for (int i = 0; i < 100; i++) {
                logger.info(getName()+":正在执行!" + i + DateUtil.longDateFormat(1622517360211l,"yyyy-MM-dd HH-mm-ss"));
            }
        }
    }
} 

解决思路:

  1. 可以把SimpleDateFormat对象的创建放到方法内部,当成局部变量来使用,这样的好处是逻辑很简单直接,坏处是每次调用方法都要创建一个SimpleDateFormat对象,浪费堆内存。

    SimpleDateFormat dateFormatter = new SimpleDateFormat();

  2. 使用ThreadLocal本地线程类,创建一个只属于每个副本的本地变量,不同线程互不可见从而保证线程安全。(推荐)

        //使用ThreadLocal代替原来的new SimpleDateFormat
        private static final ThreadLocal<SimpleDateFormat> dateFormatter = new ThreadLocal<SimpleDateFormat>(){
            @Override
            protected SimpleDateFormat initialValue() {
                return  new SimpleDateFormat("yyyy-MM-dd");
            }
        };
    

    以上代码可以用Lambda表达式简化为:private static final ThreadLocal<SimpleDateFormat> dateFormatter = ThreadLocal.withInitial(SimpleDateFormat::new);

    对应的方法修改为:

        public static String longDateFormat(Long time, String dateFormat) {
    
            if (time == null) {
                return null;
            }
    
            dateFormatter.get().applyPattern(dateFormat);
            return dateFormatter.get().format(new Date(time));
    
        }
    
    1. 对代码块加同步锁synchronized,这个在并发大的情况下会显著影响性能

    2. 用Java8提供的DateTimeFormatter替代SimpleDateFormat。这两者最大的区别就是前者是线程安全的,后者线程不安全。(推荐)

      • 使用DateTimeFormatter解析日期

        String dateStr= "2021年6月2日"; 
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy年MM月dd日");   
        LocalDate date= LocalDate.parse(dateStr, formatter);
        
      • 日期转换为字符串

        LocalDateTime now = LocalDateTime.now();  
        DateTimeFormatter format = DateTimeFormatter.ofPattern("yyyy年MM月dd日 hh:mm a"); 
        String nowStr = now .format(format);
        

        由DateTimeFormatter的静态方法ofPattern()构建日期格式,LocalDateTime和LocalDate等一些表示日期或时间的类使用parse和format方法把日期和字符串做转换。

        使用新的API,整个转换过程都不需要考虑线程安全的问题。

6、null引用

程序间接引用了可能为null的变量,从而引发空指针异常。这种情况需要即使的加非空判断,如何做好非空判断可查链接:Java中对各种类型判空的最优解

也要注意减少无意义的判空语句,比如在对某一对象判空之前已经调用了这个对象的某个属性或方法,如果为null,会提前产生空指针异常,那么就应该提前判空或者try - catch捕捉异常。

7、可以通过try - with - resources优化资源释放

在此不赘述,有兴趣的可看这篇博客【如何高效的使用IO流?】字节流、字符流、缓冲流、序列化对象流、打印流全整理

8、合理使用线程安全类

典型的就是在没有线程安全需求的代码中用了StringBuffer(),这个你可以通过下载一些代码质量检测的插件,会智能提醒你修改成StringBuilder(),当然自己也要有这个意识,根据业务场景和需求来区分。在使用StringBuilder(int capacity) 时候,根据业务情况手动指定初始容量(默认长度为16)。这是因为当StringBuilder内部是一个数组,每次容量不足时,他会产生一个新的大小的数组并将原数组的数据复制,旧数组依然留在堆内存中等待GC回收,在一定时间内会造成内存的浪费。

9、不安全的哈希算法

某些哈希算法是"不太安全的",即有被暴力破解的风险,比如MD5。在安全性要求较高的系统中,应采用比如:散列值>=224比特的SHA系列算法(如SHA-224、SHA-256、SHA-384和SHA-512)来保证敏感数据的完整性。(也可以是其他)

10、对常量CONST的声明

对CONST,如果仅在本类中使用就声明private,如果需要在其他类中调用,就声明public static final (切忌public static)会导致其他类有权限更改常量

11、使用了Number构造函数

Java默认会对整数-128到127之间进行缓存,程序中采用new Integer(int expression)等基本数据类型包装类构造函数来创建对象,而参数的值在-128到127之间,该方法产生的额外对象将占用更多空间、降低性能。

例如:下面代码示例中:使用了new Integer(int expression)来创建对象。

Integer integer = new Integer(100);

解决思路:

使用valueOf方法来代替构造方法。

//与Integer integer = 100相同
Integer integer = Integer.valueOf(100); 

12、判""

看Java官方和各大厂提供的判空工具类可以发现,对于字符串的判空(String str),都使用str.length() > 0 来替换!"".equals(str)来判断字符串是否为空,据说是有性能差异,这个养成好习惯吧。

觉得有帮助的同学,不要吝啬你们的一键三连┗|`O′|┛ 嗷~~。

以上是关于代码调优Java开发中总结的代码质量优化技巧的主要内容,如果未能解决你的问题,请参考以下文章

Java性能优化

编写高效的Java代码:常用的优化技巧

PHP代码优化技巧总结

北大青鸟java培训:Java性能调优都有哪些技巧?

编写高效的Java代码:常用的优化技巧之并发编程技巧

编写高效的Java代码:常用的优化技巧之并发编程技巧