In those years, we stepped on the Java pit

Keywords: Java Spring Attribute JDK

Preface

There is an old saying in China that "nothing is more than three times". It means that a person who has made the same mistake can be forgiven twice and three times at a time, and can't be forgiven more than three times. It has been pointed out that this "three" is an imaginary number, which is used to refer to many times in general, so "no more than three" does not include "three". As for "no more than three" package, it does not include "three". It may have something to do with everyone's bottom line. It belongs to philosophy category and is not in the scope of this article.

It's the same with code writing. For the first time, it's called "experience", for the second time, it's called "deep impression", for the third time, it's called "not long heart and eyes", and for more than three times, it's called "hopeless". In this paper, the author summarizes some code pits, describes the problem phenomenon, analyzes the problem, and gives the method of avoiding the pit. I hope you can avoid this kind of code pit in your daily coding in advance.

1. Object comparison method

The Objects.equals method provided by JDK 1.7 is very convenient to realize the object comparison and effectively avoid the tedious null pointer check.

1.1. Problem phenomenon

Before JDK 1.7, when judging whether the data type of a short, integer and long integer package is equal to the constant, we usually write as follows:

Short shortValue = (short)12345;
System.out.println(shortValue == 12345); // true
System.out.println(12345 == shortValue); // true
Integer intValue = 12345;
System.out.println(intValue == 12345); // true
System.out.println(12345 == intValue); // true
Long longValue = 12345L;
System.out.println(longValue == 12345); // true
System.out.println(12345 == longValue); // true

After JDK1.7, the Objects.equals method is provided, and functional programming is recommended. The change code is as follows:

Short shortValue = (short)12345;
System.out.println(Objects.equals(shortValue, 12345)); // false
System.out.println(Objects.equals(12345, shortValue)); // false
Integer intValue = 12345;
System.out.println(Objects.equals(intValue, 12345)); // true
System.out.println(Objects.equals(12345, intValue)); // true
Long longValue = 12345L;
System.out.println(Objects.equals(longValue, 12345)); // false
System.out.println(Objects.equals(12345, longValue)); // false

Why does replacing = = with Objects.equals directly result in different output results?

1.2. Problem analysis

By decompiling the first section of code, we get the bytecode instructions of the statement "System.out.println(shortValue == 12345);"

7   getstatic java.lang.System.out : java.io.PrintStream [22]
10  aload_1 [shortValue]
11  invokevirtual java.lang.Short.shortValue() : short [28]
14  sipush 12345
17  if_icmpne 24
20  iconst_1
21  goto 25
24  iconst_0
25  invokevirtual java.io.PrintStream.println(boolean) : void [32]

Originally, the compiler will judge the basic data type corresponding to the packing data type, and compare the instructions with this basic data type (such as sipush and if "icmpne" in the bytecode instructions above), which is equivalent to the compiler's automatic forced conversion of the data type to the constant.

Why does the compiler not automatically convert the data types of constants after using the Objects.equals method? By decompiling the second code, we get the bytecode instructions of the statement "System.out.println(Objects.equals(shortValue, 12345));"

7   getstatic java.lang.System.out : java.io.PrintStream [22]
10  aload_1 [shortValue]
11  sipush 12345
14  invokestatic java.lang.Integer.valueOf(int) : java.lang.Integer [28]
17  invokestatic java.util.Objects.equals(java.lang.Object, java.lang.Object) : boolean [33]
20  invokevirtual java.io.PrintStream.println(boolean) : void [39]

Originally, according to the literal meaning of the compiler, the default basic data type of constant 12345 is int, so it will be automatically converted to the wrapper data type Integer.

In Java language, the default data type of integer is int, and the default data type of decimal is double.

Let's analyze the code implementation of the Objects.equals method:

The code implementation of Objects.equals method is as follows:

public static boolean equals(Object a, Object b) {
    return (a == b) || (a != null && a.equals(b));
}

Where the statement "a.equals(b)" will use the Short.equals method.

The code implementation of the Short.equals method is as follows:

public boolean equals(Object obj) {
    if (obj instanceof Short) {
        return value == ((Short)obj).shortValue();
    }
    return false;
}

Through code implementation analysis: the corresponding statement "System.out.println(Objects.equals(shortValue, 12345));", because the two parameter object types of Objects.equals are inconsistent, one is the packing data type Short, the other is the packing data type Integer, so the final comparison result must be false. Similarly, the statement "System.out.println(Objects.equals(intValue, 12345));", because the two parameter object types of Objects.equals are the same, both are the wrapper data type Integer and the values are the same, the final comparison result must be true.

1.3. Pit avoidance method

(1) Keep good coding habits and avoid automatic conversion of data types

In order to avoid automatic conversion of data types, a more scientific way is to directly declare constants as the corresponding basic data types.

The first code can be written as follows:

Short shortValue = (short)12345;
System.out.println(shortValue == (short)12345); // true
System.out.println((short)12345 == shortValue); // true
Integer intValue = 12345;
System.out.println(intValue == 12345); // true
System.out.println(12345 == intValue); // true
Long longValue = 12345L;
System.out.println(longValue == 12345L); // true
System.out.println(12345L == longValue); // true

The second code can be written as follows:

Short shortValue = (short)12345;
System.out.println(Objects.equals(shortValue, (short)12345)); // true
System.out.println(Objects.equals((short)12345, shortValue)); // true
Integer intValue = 12345;
System.out.println(Objects.equals(intValue, 12345)); // true
System.out.println(Objects.equals(12345, intValue)); // true
Long longValue = 12345L;
System.out.println(Objects.equals(longValue, 12345L)); // true
System.out.println(Objects.equals(12345L, longValue)); // true

(2) Early detection of data type mismatch with development tools or plug-ins

In the question window of Eclipse, we will see the following prompt:

Unlikely argument type for equals(): int seems to be unrelated to Short
Unlikely argument type for equals(): Short seems to be unrelated to int
Unlikely argument type for equals(): int seems to be unrelated to Long
Unlikely argument type for equals(): Long seems to be unrelated to int

Through the FindBugs plug-in scanning, we will see the following warning:

Call to Short.equals(Integer) in xxx.Xxx.main(String[]) [Scariest(1), High confidence]
Call to Integer.equals(Short) in xxx.Xxx.main(String[]) [Scariest(1), High confidence]
Call to Long.equals(Integer) in xxx.Xxx.main(String[]) [Scariest(1), High confidence]
Call to Integer.equals(Long) in xxx.Xxx.main(String[]) [Scariest(1), High confidence]

(3) Conduct routine unit test, and try to find out the problem in the R & D stage

"Don't be good at small things instead of doing things". Don't avoid unit testing because the changes are small. Bug s often appear in your overconfident code. As long as a unit test is carried out, problems like this can be found completely.

2. Unpacking of ternary expression

Ternary expression is a fixed syntax format in Java coding: "conditional expression? Expression 1: expression 2 ". The logic of a ternary expression is: if the conditional expression is true, expression 1 is executed; otherwise, expression 2 is executed.

2.1. Problem phenomenon

boolean condition = false;
Double value1 = 1.0D;
Double value2 = 2.0D;
Double value3 = null;
Double result = condition ? value1 * value2 : value3; // Throw null pointer exception

When the condition expression condition is equal to false, assign the Double object value3 directly to the Double object result. There is no problem according to reason. Why throw the null pointer exception?

2.2. Problem analysis

By decompiling the code, we get the bytecode instructions of the statement "double result = condition? Value1 * Value2: value3;" as follows:

17  iload_1 [condition]
18  ifeq 33
21  aload_2 [value1]
22  invokevirtual java.lang.Double.doubleValue() : double [24]
25  aload_3 [value2]
26  invokevirtual java.lang.Double.doubleValue() : double [24]
29  dmul
30  goto 38
33  aload 4 [value3]
35  invokevirtual java.lang.Double.doubleValue() : double [24]
38  invokestatic java.lang.Double.valueOf(double) : java.lang.Double [16]
41  astore 5 [result]
43  getstatic java.lang.System.out : java.io.PrintStream [28]
46  aload 5 [result]

On line 33, load the double object value3 into the operand stack; on line 35, call the double value method of the double object value3. At this time, because value3 is null, calling the doubleValue method must throw a null pointer exception. However, why convert null object value3 to basic data type double?

Refer to relevant data to get the type conversion rules of ternary expression:

  1. If the two expression types are the same, the return value type is this type;
  2. If two expression types are different, but the type is not convertible, the return value type is Object type;
  3. If the two expression types are different, but the types can be converted, first convert the wrapper data type to the basic data type, and then follow the conversion rules of the basic data type (byte

According to rule analysis, expression 1 (value1 * value2) returns the basic data type double after calculation, and expression 2 (value3) returns the packing data type double. According to the type conversion rule of ternary expression, the final return type is the basic data type double. Therefore, when the condition expression is equal to false, you need to convert the null object value3 to the basic data type double, so you call the doubleValue method of value3 and throw a null pointer exception.

You can use the following cases to verify the type conversion rules of a ternary expression:

boolean condition = false;
Double value1 = 1.0D;
Double value2 = 2.0D;
Double value3 = null;
Integer value4 = null;
// Return type is Double, no null pointer exception is thrown
Double result1 = condition ? value1 : value3;
// If the return type is double, a null pointer exception will be thrown
Double result2 = condition ? value1 : value4;
// Return type is double, no null pointer exception is thrown
Double result3 = !condition ? value1 * value2 : value3;
// If the return type is double, a null pointer exception will be thrown
Double result4 = condition ? value1 * value2 : value3;

2.3. Pit avoidance method

(1) Try to avoid using ternary expressions, instead of if else statements

If there are arithmetic calculation and packing data types in ternary expressions, you can consider using if else statements instead. The rewrite code is as follows:

boolean condition = false;
Double value1 = 1.0D;
Double value2 = 2.0D;
Double value3 = null;
Double result;
if (condition) {
    result = value1 * value2;
} else {
    result = value3;
}

(2) Try to use basic data types to avoid automatic conversion of data types

If there are arithmetic calculation and packing data types in ternary expressions, you can consider using if else statements instead. The rewrite code is as follows:

boolean condition = false;
double value1 = 1.0D;
double value2 = 2.0D;
double value3 = 3.0D;
double result = condition ? value1 * value2 : value3;

(3) Conduct coverage unit test and try to find out the problems in the R & D stage

For this kind of problem, as long as some unit test cases are written and some coverage tests are carried out, it can be found in advance.

3. Generic object assignment

Java generics is a new feature introduced in JDK 1.5. Its essence is parameterized type, that is, data type is used as a parameter.

3.1. Problems

In the paging query of user data, the following code is written due to clerical errors:

(1)PageDataVO.java:

/** Paging data VO class */
@Getter
@Setter
@ToString
@NoArgsConstructor
@AllArgsConstructor
public class PageDataVO<T> {
    /** Total quantity */
    private Long totalCount;
    /** Data list */
    private List<T> dataList;
}

(2)UserDAO.java:

/** User DAO interface */
@Mapper
public interface UserDAO {
    /** Count the number of users */
    public Long countUser(@Param("query") UserQueryVO query);
    /** Query user information */
    public List<UserDO> queryUser(@Param("query") UserQueryVO query);
}

(3)UserService.java:

/** User service class */
@Service
public class UserService {
    /** User DAO */
    @Autowired
    private UserDAO userDAO;

    /** Query user information */
    public PageDataVO<UserVO> queryUser(UserQueryVO query) {
        List<UserDO> dataList = null;
        Long totalCount = userDAO.countUser(query);
        if (Objects.nonNull(totalCount) && totalCount.compareTo(0L) > 0) {
            dataList = userDAO.queryUser(query);
        }
        return new PageDataVO(totalCount, dataList);
    }
}

(4)UserController.java:

/** User controller class */
@Controller
@RequestMapping("/user")
public class UserController {
    /** User service */
    @Autowired
    private UserService userService;

    /** Query users */
    @ResponseBody
    @RequestMapping(value = "/query", method = RequestMethod.POST)
    public Result<PageDataVO<UserVO>> queryUser(@RequestBody UserQueryVO query) {
        PageDataVO<UserVO> pageData = userService.queryUser(query);
        return ResultBuilder.success(pageData);
    }
}

There is no compilation problem in the above code, but some secret fields in UserDO are returned to the front end. Careful readers may have found that in the statement "return new PageDataVO(totalCount, dataList);" of the queryUser method of the UserService class, we assigned the list < UserDO > object dataList to the list < uservo > field dataList of pagedatavo < uservo >.

The question is: why don't development tools report compilation errors?

3.2. Problem analysis

Parametric types and primitive types need to be compatible for historical reasons. Let's take the example of ArrayList to see how it's compatible.

Previous writing:

ArrayList list = new ArrayList();

The current way of writing:

ArrayList<String> list = new ArrayList<String>();

Considering the compatibility with the previous code, the following situations are inevitable when passing values between various object references:

// The first situation
ArrayList list1 = new ArrayList<String>();
// The second situation
ArrayList<String> list2 = new ArrayList();

Therefore, the Java compiler is compatible with the above two types, and there will be no compilation errors, but compilation warnings. However, my development tools did not show any warning at compile time.

Then we analyze the problems we encountered, which actually hit two situations at the same time:

  1. Assign the List < userdo > object to List, and hit the first case;
  2. The second case is hit by assigning the PageDataVO object to PageDataVO < uservo >.

The final effect is that we magically assign the list < userdo > object to list < uservo >.

The root cause of the problem is that when we initialize the PageDataVO object, we do not require a mandatory type check.

3.3. Pit avoidance method

(1) diamond syntax is recommended when initializing generic objects

In the Alibaba Java development manual, there is a recommended rule:

[Recommendation]When a collection generic is defined, the JDK7 And above, use diamond Grammar or omission.
Explain:Rhombic generics, i.e diamond,Direct use<>To refer to the type already specified by the preceding edge.
Example:

// < > diamond mode
HashMap<String, String> userCache = new HashMap<>(16);
// Total ellipsis
ArrayList<User> users = new ArrayList(10); 

In fact, when initializing generic objects, it is not recommended to omit all. This avoids type checking, which can cause the above problems.

When initializing generic objects, diamond syntax is recommended. The code is as follows:

return new PageDataVO<>(totalCount, dataList); 

Now, in Eclipse's problem window, we see the following error:

Cannot infer type arguments for PageDataVO<>

As a result, we forget to convert the list < userdo > object to the list < uservo > object.

(2) In unit testing, data content needs to be compared

In unit test, normal operation is an indicator, but correct data is a more important indicator.

4. Generic attribute copy

Spring's BeanUtils.copyProperties method is a very useful property copying tool method.

4.1. Problems

According to the database development specification, the database table must contain three fields, i.e. id, gmt_create and gmt_modified. Among them, the field id may adopt the type of int or long according to the different data quantity (Note: Alibaba specification requires that it must be of long type, here for example, it is allowed to be of int or long type).

Therefore, the three fields are extracted and a BaseDO base class is defined:

/** Basic DO class */
@Getter
@Setter
@ToString
public class BaseDO<T> {
    private T id;
    private Date gmtCreate;
    private Date gmtModified;
}

For the user table, a UserDO class is defined:

/** User class DO */
@Getter
@Setter
@ToString
public class UserDO extends BaseDO<Long>{
    private String name;
    private String description;
}

For the query interface, a UserVO class is defined:

/** User class VO */
@Getter
@Setter
@ToString
public static class UserVO {
    private Long id;
    private String name;
    private String description;
}

The implementation code of the query user service interface is as follows:

/** User service class */
@Service
public class UserService {
    /** User DAO */
    @Autowired
    private UserDAO userDAO;

    /** Query users */
    public List<UserVO> queryUser(UserQueryVO query) {
        // Query user information
        List<UserDO> userDOList = userDAO.queryUser(query);
        if (CollectionUtils.isEmpty()) {
            return Collections.emptyList();
        }

        // Convert user list
        List<UserVO> userVOList = new ArrayList<>(userDOList.size());
        for (UserDO userDO : userDOList) {
            UserVO userVO = new UserVO();
            BeanUtils.copyProperties(userDO, userVO);
            userVOList.add(userVO);
        }

        // Return to user list
        return userVOList;
    }
}

Through the test, we will find a problem - calling to query the user service interface, and the value of the user ID is not returned.

[{"description":"This is a tester.","name":"tester"},...]

4.2. Problem analysis

According to the principle, the id fields of UserDO class and UserVO class are of Long type, and there is no type that cannot be converted, so they should be able to be assigned normally. Try to assign manually, the code is as follows:

for (UserDO userDO : userDOList) {
    UserVO userVO = new UserVO();
    userVO.setId(userDO.getId());
    userVO.setName(userDO.getName());
    userVO.setDescription(userDO.getDescription());
    userVOList.add(userVO);
}

After testing, the above code returns normal results and the value of user ID is returned successfully.

So, it's the BeanUtils.copyProperties tool method. Run in Debug mode and enter the BeanUtils.copyProperties tool method to get the following data:

It turns out that the return type of getId method of UserDO class is not of Long type, but is restored to Object type by generics. The following ClassUtils.isAssignable tool method determines whether the Object type can be assigned to the Long type. Of course, it will return false, so that the property copy cannot be performed.

Why doesn't the author consider "getting the attribute value first, and then judging whether it can be assigned"? The suggested codes are as follows:

Object value = readMethod.invoke(source);
if (Objects.nonNull(value) && ClassUtils.isAssignable(writeMethod.getParameterTypes()[0], value.getClass())) {
   ... // Assignment related code
}

4.3. Pit avoidance method

(1) Don't blindly believe in third-party toolkits. Any toolkit may have problems

In Java, there are many third-party toolkits, such as: Apache's Commons lang3, commons collections, Google's guava They are all very useful third-party toolkits. However, do not blindly believe in third-party toolkits, any toolkits may have problems.

(2) If fewer attributes need to be copied, you can manually code them for attribute copying

The main advantage of using BeanUtils.copyProperties to reflect the copy property is to save the code amount, and the main disadvantage is to reduce the program performance. Therefore, if fewer attributes need to be copied, you can manually code them for attribute copying.

(3) Unit test must be carried out and data content must be compared

After writing the code, we must carry out unit test and compare the data content. Don't take it for granted that the toolkit is mature, the code is simple, and there is no problem.

5.Set object weight removal

In Java language, Set data structure can be used for object weight removal. The common Set classes are HashSet, LinkedHashSet, etc.

5.1. Problems

Write a city auxiliary class to read the city data from the CSV file:

/** Urban auxiliary */
@Slf4j
public class CityHelper {
    /** Test main method */
    public static void main(String[] args) {
        Collection<City> cityCollection = readCities2("cities.csv");
        log.info(JSON.toJSONString(cityCollection));
    }

    /** Reading City */
    public static Collection<City> readCities(String fileName) {
        try (FileInputStream stream = new FileInputStream(fileName);
            InputStreamReader reader = new InputStreamReader(stream, "GBK");
            CSVParser parser = new CSVParser(reader, CSVFormat.DEFAULT.withHeader())) {
            Set<City> citySet = new HashSet<>(1024);
            Iterator<CSVRecord> iterator = parser.iterator();
            while (iterator.hasNext()) {
                citySet.add(parseCity(iterator.next()));
            }
            return citySet;
        } catch (IOException e) {
            log.warn("Read all city exceptions", e);
        }
        return Collections.emptyList();
    }

    /** Analytic city */
    private static City parseCity(CSVRecord record) {
        City city = new City();
        city.setCode(record.get(0));
        city.setName(record.get(1));
        return city;
    }

    /** City class */
    @Getter
    @Setter
    @ToString
    private static class City {
        /** City encoding */
        private String code;
        /** City name */
        private String name;
    }
}

The purpose of using HashSet data structure in the code is to avoid the duplication of city data and force the city data to be read.

When the content of the input file is as follows:

Code, name
 010, Beijing
 020, Guangzhou
 010, Beijing

The parsed JSON results are as follows:

[{"code":"010","name":"Beijing"},{"code":"020","name":"Guangzhou"},{"code":"010","name":"Beijing"}]

However, the city "Beijing" has not been ranked.

5.2. Problem analysis

When adding objects to a Set, the Set first calculates the hashCode of the object to be added, and then gets a location to store the current object according to the value. If there is no object in this location, the collection Set considers that the object does not exist in the collection and directly adds it. If there is an object in the location, then compare the object to be added to the collection with the object in the location by using the equals method: if the equals method returns false, then the collection thinks that the object does not exist in the collection, put the object after the object; if the equals method returns true, then the object already exists in the collection, The object will no longer be added to the collection. Therefore, it is necessary to use the hashCode method and the equals method to determine whether the two elements are repeated in the hash table. The hashCode method determines where the data is stored in the table, while the equals method determines whether the same data exists in the table.

Analyze the above problems. Since the hashCode method and equals method of the City class are not overridden, the hashCode method and equals method of the Object class will be used. It is as follows:

public native int hashCode();
public boolean equals(Object obj) {
    return (this == obj);
}

It can be seen that the hashCode method of the Object class is a local method, which returns the Object address; the equals method of the Object class only compares whether the objects are equal. Therefore, for two identical Beijing data, different City objects are initialized during parsing, resulting in different values of hashCode method and equals method, which must be considered as different objects by Set, so there is no duplication.

Then, we rewrite the hashCode method and equals method of the City class. The code is as follows:

/** City class */
@Getter
@Setter
@ToString
private static class City {
    /** City encoding */
    private String code;
    /** City name */
    private String name;

    /** Equal judgement */
    @Override
    public boolean equals(Object obj) {
        if (obj == this) {
            return true;
        }
        if (Objects.isNull(obj)) {
            return false;
        }
        if (obj.getClass() != this.getClass()) {
            return false;
        }
        return Objects.equals(this.code, ((City)obj).code);
    }

    /** hash coding */
    @Override
    public int hashCode() {
        return Objects.hashCode(this.code);
    }
}

Support the test program again. The parsed JSON results are as follows:

[{"code":"010","name":"Beijing"},{"code":"020","name":"Guangzhou"}]

The result is correct. The city "Beijing" has been ranked.

5.3. Pit avoidance method

(1) When it is determined that the data is unique, you can use List instead of Set

When it is determined that the parsed city data is unique, there is no need to perform the operation of de duplication. You can directly use List to store it.

List<City> citySet = new ArrayList<>(1024);
Iterator<CSVRecord> iterator = parser.iterator();
while (iterator.hasNext()) {
    citySet.add(parseCity(iterator.next()));
}
return citySet;

(2) When it is determined that the data is not unique, Map can be used instead of Set

When it is determined that the parsed City data is not unique, the City name needs to be installed for re arrangement, and the Map can be directly used for storage. Why is it not recommended to implement the hashCode method of City class, and then use HashSet to realize weight removal? First, we don't want to put the business logic in the model DO class; second, we want to put the field of sorting in the code to facilitate the reading, understanding and maintenance of the code.

Map<String, City> cityMap = new HashMap<>(1024);
Iterator<CSVRecord> iterator = parser.iterator();
while (iterator.hasNext()) {
    City city = parseCity(iterator.next());
    cityMap.put(city.getCode(), city);
}
return cityMap.values();

(3) Rewrite hashCode method and equals method according to Java language specification

Custom classes that do not override the hashCode and equals methods should not be used in sets.

6. Public method agent

The proxy class generated by spring cglib proxy inherits the proxy class and implements the proxy by overriding the non final methods in the proxy class. Therefore, the class of spring cglib proxy cannot be final, and the method of proxy cannot be final, which is limited by inheritance mechanism.

6.1. Problem phenomenon

Here is a simple example. Only the super user has the right to delete the company, and all service functions are intercepted by AOP to handle exceptions. The example code is as follows:

(1)UserService.java:

/** User service class */
@Service
public class UserService {
    /** Super user */
    private User superUser;

    /** Set superuser */
    public void setSuperUser(User superUser) {
        this.superUser = superUser;
    }

    /** Get superuser */
    public final User getSuperUser() {
        return this.superUser;
    }
}

(2)CompanyService.java:

/** Company services */
@Service
public class CompanyService {
    /** Company DAO */
    @Autowired
    private CompanyDAO companyDAO;
    /** User service */
    @Autowired
    private UserService userService;

    /** Delete company */
    public void deleteCompany(Long companyId, Long operatorId) {
        // Set superuser
        userService.setSuperUser(new User(0L, "admin", "Super user"));

        // Verify superuser
        if (!Objects.equals(operatorId, userService.getSuperUser().getId())) {
            throw new ExampleException("Only super users can delete companies");
        }

        // Delete company information
        companyDAO.delete(companyId, operatorId);
    }
}

(3)AopConfiguration.java:

/** AOP Configuration class */
@Slf4j
@Aspect
@Configuration
public class AopConfiguration {
    /** Surround method */
    @Around("execution(* org.changyi.springboot.service..*.*(..))")
    public Object around(ProceedingJoinPoint joinPoint) {
        try {
            log.info("Start calling service method...");
            return joinPoint.proceed();
        } catch (Throwable e) {
            log.error(e.getMessage(), e);
            throw new ExampleException(e.getMessage(), e);
        }
    }
}

When we call the deleteCompany method of CompanyService, we also throw a null pointerexception, because the superuser obtained by calling the getSuperUser method of UserService class is null. However, in the deleteCompany method of the CompanyService class, the superuser is forced to be specified by the setSuperUser method of the UserService class every time. The superuser obtained by the getSuperUser method of the UserService class should not be null. In fact, this problem is also caused by AOP agent.

6.2. Problem analysis

When using the Spring cglib proxy class, Spring will create a proxy class named UserService$$EnhancerBySpringCGLIB $. Decompile the proxy class to get the following main code:

public class UserService$$EnhancerBySpringCGLIB$$a2c3b345 extends UserService implements SpringProxy, Advised, Factory {
    ......
    public final void setSuperUser(User var1) {
        MethodInterceptor var10000 = this.CGLIB$CALLBACK_0;
        if (var10000 == null) {
            CGLIB$BIND_CALLBACKS(this);
            var10000 = this.CGLIB$CALLBACK_0;
        }

        if (var10000 != null) {
            var10000.intercept(this, CGLIB$setSuperUser$0$Method, new Object[]{var1}, CGLIB$setSuperUser$0$Proxy);
        } else {
            super.setSuperUser(var1);
        }
    }
    ......
}

As you can see, this proxy class inherits the UserService class and proxy the setSuperUser method, but there is no proxy getSuperUser method. Therefore, when we call the setSuperUser method, we set the superUser field value of the original object instance; when we call the getSuperUser method, we get the superUser field value of the proxy object instance. If the final modifiers of these two methods are interchanged, there is also the problem of getting superUser null.

6.3. Pit avoidance method

(1) Strictly follow the CGLIB proxy specification, and do not add the final modifier to the classes and methods being proxied

Strictly follow the CGLIB proxy specification, and do not add the final modifier to the classes and methods being proxied, so as to avoid the data inconsistency or null pointer problem caused by the different dynamic proxy operation object instances (original object instance and proxy object instance).

(2) Narrow the scope of CGLIB proxy classes, and do not proxy classes that can not be proxied

Reducing the scope of CGLIB proxy class can save memory cost and improve function call efficiency.

7. Public field agent

When fastjson was forced to upgrade to 1.2.60, the author stepped on a pit. In order to develop quickly, the author defined in ParseConfig:

public class ParseConfig {
    public final SymbolTable symbolTable = new SymbolTable(4096);
    ......
}

We inherited this class in our project, and it was dynamically represented by AOP, so a line of code caused a "murder".

7.1. Problem phenomenon

The example in the previous chapter is still used, but the methods of getting and setting are deleted, and a public field is defined. The example code is as follows:

(1)UserService.java:

/** User service class */
@Service
public class UserService {
    /** Super user */
    public final User superUser = new User(0L, "admin", "Super user");
    ......
}

(2)CompanyService.java:

/** Company services */
@Service
public class CompanyService {
    /** Company DAO */
    @Autowired
    private CompanyDAO companyDAO;
    /** User service */
    @Autowired
    private UserService userService;

    /** Delete company */
    public void deleteCompany(Long companyId, Long operatorId) {
        // Verify superuser
        if (!Objects.equals(operatorId, userService.superUser.getId())) {
            throw new ExampleException("Only super users can delete companies");
        }

        // Delete company information
        companyDAO.delete(companyId, operatorId);
    }
}

(3)AopConfiguration.java:

The same chapter AopConfiguration.java.

When we call the deleteCompany method of CompanyService, a null pointerexception is thrown. After debugging and printing, it is found that the superUser variable of UserService is null. If AopConfiguration is deleted, there will be no null pointer exception, indicating that this problem is caused by AOP agent.

7.2. Problem analysis

When using the Spring cglib proxy class, Spring will create a proxy class named UserService$$EnhancerBySpringCGLIB $. This proxy class inherits the userservice class and covers all non final public methods in the userservice class. However, this proxy class does not call the method of the super base class; instead, it creates a member userservice and points to the original userservice class object instance. Now, there are two object instances in memory: the original userservice object instance and the proxy object instance pointing to userservice. This proxy class is just a virtual proxy. It inherits the userservice class and has the same fields as userservice, but it never initializes and uses them. Therefore, once the public member variable is obtained through the proxy class object instance, a default value of null will be returned.

7.3. Pit avoidance method

(1) When it is determined that the field is immutable, it can be defined as a public static constant

When it is determined that the field is immutable, it can be defined as a public static constant and accessed by class name + field name. Class name + field name access public static constants, regardless of the dynamic proxy of the class instance.

/** User service class */
@Service
public class UserService {
    /** Super user */
    public static final User SUPER_USER = new User(0L, "admin", "Super user");
    ......
}

/** Use code */
if (!Objects.equals(operatorId, UserService.SUPER_USER.getId())) {
    throw new ExampleException("Only super users can delete companies");
}

(2) When it is determined that the field is immutable, it can be defined as a private member variable

When it is determined that the field is immutable, it can be defined as a private member variable, and a public method is provided to obtain the value of the variable. When the class instance is dynamically proxied, the proxy method will call the proxied method to return the member variable value of the proxied class.

/** User service class */
@Service
public class UserService {
    /** Super user */
    private User superUser = new User(0L, "admin", "Super user");
    /** Get superuser */
    public User getSuperUser() {
        return this.superUser;
    }
    ......
}

/** Use code */
if (!Objects.equals(operatorId, userService.getSuperUser().getId())) {
    throw new ExampleException("Only super users can delete companies");
}

(3) Follow the JavaBean coding specification and do not define public member variables

Follow the JavaBean coding specification and do not define public member variables. The JavaBean specification is as follows:

(1)JavaBean class must be a public class and its access property is set to public, such as public class User {...}
(2) A JavaBean class must have an empty constructor: there must be a public constructor without parameters in the class
(3) A JavaBean class should not have a public instance variable, which is private, such as private Integer id;
(4) Property should be accessed through a set of getter/setter methods.

Epilogue

Human beings benefit from the "analogy" thinking, which is the wisdom of human beings. When encountering new things, people often use similar known things as reference, which can accelerate the cognition of new things. However, human beings are subject to the "stereotype" thinking, because the known things can not represent the new things, and people are easy to form the preconceived concept, which eventually leads to miscalculation of the new things.

Author information: Chen Changyi, Hua Mingchang Yi, a map technology expert of golde, joined Alibaba in 2018 and has been engaged in map data collection.

Posted by Grunt on Thu, 09 Apr 2020 23:44:14 -0700