日常编程中的小技巧以及注意点

<1>避免使用String s = new String(”abc”),而应该使用String s =”abc”

        该条原则主要是避免创建不必要的对象。String的字符串常量是存放在常量池中,如果常量池之前没有abc的常量存在,则首先会在常量池中创造出来该对象。然后因为使用了new,再在堆中创建一个abc的拷贝对象,这样就创建了两个对象;而后者只会在常量池中创建一个对象(如果此时常量池中没有abc常量的存在)(String的intern方法另提)。

<2>覆盖equals时总要覆盖hashCode

        一个很常见的错误根源在于没有覆盖hashCode方法。在每个覆盖了equals方法的类中,也必须覆盖hashCode方法。如果不这样做的话,就会违反Object.hashCode的通用约定,从而导致该类无法结合所有基于散列的集合一起正常运作,这样的集合包括HashMap、HashSet和Hashtable。考虑如下代码:

public class Student {

    private String name;
    private int    age;

    public Student(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (obj == null) {
            return false;
        }
        if (getClass() != obj.getClass()) {
            return false;
        }
        Student other = (Student) obj;
        if (age != other.age) {
            return false;
        }
        if (name == null) {
            if (other.name != null) {
                return false;
            }
        } else if (!name.equals(other.name)) {
            return false;
        }
        return true;
    }

    public static void main(String[] args) {
        Map<Student, Integer> map = new HashMap<>(16);
        map.put(new Student("Robert Hou", 23), 30);
        System.out.println(map.get(new Student("Robert Hou", 23)));
    }
}

        当我们用自定义引用类型作为HashMap的键时,期望通过get方法获取到该数据,输出30的时候,但是运行结果却为null。为什么呢?因为Student类没有覆写hashCode方法,从而导致两个相等的实例具有不同的散列码。因此,put方法把Student对象存放在一个散列桶(hash bucket)中,get方法却在另一个散列桶中查找这个Student。即使这两个实例正好被放到同一个散列桶中,get方法也必定会返回null,因为HashMap有一项优化,可以将与每个项相关联的散列码缓存起来,如果散列码不匹配,也不必检验对象的等同性。

        修正这个问题很简单,只需要为Student类覆写一个hashCode方法即可。如下所示:

    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + age;
        result = prime * result + ((name == null) ? 0 : name.hashCode());
        return result;
    }

        但有种情况是:Student类出于某种需求,做成了单例模式。那此时再将其放进HashMap中会有问题。例如笔者最近正好遇到了这个问题,后来发现是因为单例导致的问题。在往HashMap中放Student时,因为用的是单例,所有Student都是同一个实例,之前放过的Student的属性值会被更新成为现在要插入的Student属性值,造成插入错误。解决办法是实现Cloneable接口,覆写clone方法。这里需要注意的是clone是浅拷贝,但是因为笔者的单例类中不含有引用类型属性(String除外),所以用浅拷贝就能解决问题,如若含有引用类型的属性,则必须将引用类型的属性也都clone化,即深拷贝。或者使用序列化反序列化的方式也一样能做到深拷贝。深拷贝和浅拷贝的概念和区别可以参考笔者的另一篇文章《深拷贝&浅拷贝》

<3>使类和成员的可访问性最小化

        尽可能地使每个类或者成员不被外界访问。设计良好的模块会隐藏所有的实现细节,把它的API与它的实现清晰地隔离开来。然后,模块之间只通过它们的API进行通信,一个模块不需要知道其他模块的内部工作情况。这个概念被称为信息隐藏(information hiding)或封装(encapsulation),是软件设计的基本原则之一。

        信息隐藏可以有效地解除组成系统的各模块之间的耦合关系,使得这些模块可以独立地开发、测试、优化、使用、理解、和修改。同时,也减轻了维护的负担,因为程序员可以更快地理解这些模块,并且在调试它们的时候可以不影响其他的模块。

        信息隐藏可以通过访问控制(access control)来实现:

私有的(private)——只有在声明该成员的顶层类内部才可以访问这个成员。

包级私有的(package-private/default)——声明该成员的包内部的任何类都可以访问这个成员。

受保护的(protected)——声明该成员的类的子类可以访问这个成员,并且,声明该成员的包内部的任何类也可以访问这个成员。

公有的(public)——在任何地方都可以访问该成员。

        最忌讳的就是所有的方法,不假思索全都用public来修饰,应该想想这个方法到底要不要暴露给外界,即使暴露给外界,也要想想是只能子类看到还是包级可见。但是有个特例:非public方法用了@ Transactional注解会失效的问题,详见第12条所述。

<4>复合优先于继承

        继承是实现代码重用的有力手段,但它并非永远是完成这项工作的最佳工具,使用不当会导致软件变得很脆弱。继承打破了封装性。换句话说,子类依赖于其超类中特定功能的实现细节。超类的实现有可能会随着发行版本的不同而有所变化,如果真的发生了变化,子类可能会遭到破坏,即使它的代码完全没有改变。因而,子类必须要跟着其超类的更新而演变,除非超类是专门为了扩展而设计的,并且具有很好的文档说明。

        解决办法也是有的,不用扩展现有的类,而是在新的类中增加一个私有域,它引用现有类的一个实例。这种设计被称作复合(composition)。因为现有的类变成了新类的一个组件,新类中的每个实例方法都可以调用被包含的现有类实例中对应的方法,并返回它的结果,这被称为转发(forwarding),新类中的方法被称为转发方法(forwarding method)。这样得到的类将会非常稳固,它不依赖于现有类的实现细节。即使现有的类添加了新的方法,也不会影响新的类。

/**
 * 
 * <p>Classname: BracketsStack </p>
 * <p>Description: 匹配左右括号的堆栈实现</p>
 * @author houyishuang
 * @date 2018年5月31日
 */
public class BracketsStack {

    private static final Logger    logger       = LoggerFactory.getLogger(BracketsStack.class);
    /**
     * 堆栈用List实现(Stack已过时,不建议使用)
     */
    private static List<Character> bracketsList = new LinkedList<>();

    private static class BracketsStackLazyHolder {

        private static final BracketsStack INSTANCE = new BracketsStack();
    }

    private BracketsStack() {}

    public static BracketsStack getInstance() {
        return BracketsStackLazyHolder.INSTANCE;
    }

    /**
     * 
    * <p>Title: push </p>
    * <p>Description: 在堆栈顶部插入一个新元素 </p>
    * @param bracket    参数说明
    * @author houyishuang
    * @date 2018年5月31日
     */
    public void push(char bracket) {
        bracketsList.add(bracket);
    }

    /**
     * 
    * <p>Title: pop </p>
    * <p>Description: 删除堆栈顶部元素并返回 </p>
    * @return    参数说明
    * @author houyishuang
    * @date 2018年5月31日
     */
    public Character pop() {
        if (!isEmpty()) {
            return bracketsList.remove(bracketsList.size() - 1);
        } else {
            logger.error("堆栈元素为空,删除失败");
            return null;
        }
    }

    /**
     * 
    * <p>Title: isEmpty </p>
    * <p>Description: 判断堆栈是否为空 </p>
    * @return    参数说明
    * @author houyishuang
    * @date 2018年5月31日
     */
    public boolean isEmpty() {
        return bracketsList.isEmpty();
    }
}

        上述代码是笔者在实际项目中写过的代码,用BracketsStack包装了LinkedList的部分方法实现,使代码更加灵活、健壮。所以这也被称作包装类(wrapper class),也是装饰者模式的核心体现:对扩展开放、对修改关闭(开放-关闭原则)

<5>接口只用于定义类型

        有一种接口被称为常量接口(constant interface),这种接口没有包含任何方法,它只包含静态的final域,每个域都导出一个常量。使用这些常量的类实现这个接口,以避免用类名来修饰常量名。如下:

public interface StandardGraphConstant {

    /**
    
     * 主题
    
     */
    public static final String NODE_LABEL_INFOSUBJECT    = "InfoSubject";
    /**
    
     * 信息项
    
     */
    public static final String NODE_LABEL_INFOITEM       = "InfoItem";
    /**
    
     * 外键信息项
    
     */
    public static final String NODE_LABEL_FKINFOITEM     = "FKInfoItem";
    /**
    
     * 公共代码根
    
     */
    public static final String NODE_LABEL_COMMONCODEROOT = "CommonCodeRoot";
    //do something...
}

        这是笔者在实际项目中的部分真实代码。但是很遗憾,这里想说的是:常量接口模式是对接口的不良使用。类在内部使用某些常量,这纯粹是实现细节。如果非final类实现了常量接口,它的所有子类的命名空间也会被接口中的常量所污染。更糟糕的是,它代表了一种承诺:如果在将来的发行版本中,这个类被修改了,它不再需要使用这些常量了,它依然必须实现这个接口,以保证二进制兼容性:如果一个类A实现了一个常量接口B,我现在要重写A类中的方法,但是因为原来已经有方法用了常量接口中的常量了,为了保证向下兼容性,这些方法依然会保留,即依然会实现常量接口B。

        替代方案可以选择枚举类或者不可实例化的工具类(笔者之前介绍单例模式的文章中曾介绍过不可实例化工具类,其实就是在工具类里显示覆写私有构造器)。笔者认为能使用枚举的情况下还是推荐使用枚举的。枚举类型不用将常量转成String类型,更具有意义。比如说RED常量用工具类表示可能是下面这样:

private static final String RED = "red";

        我得用个String类型来修饰,同时要赋个初始值。这样修饰RED常量,总感觉很别扭。但是用枚举就不一样了:

public enum Color {
    /**
    
     * red
    
     */
    RED,
    /**
    
     * yellow
    
     */
    YELLOW,
    /**
    
     * green
    
     */
    GREEN,
    /**
    
     * blue
    
     */
    BLUE
}

        类型就是相应的枚举类型。同时这意味着如果要传参的话,只能传Color的枚举类型,其他类型例如String会报错。这样的话从源头上就控制了参数的有效性,更加安全。但是用工具类的方式,随便传个什么String类型的值都不会报编译时异常,而在运行时会报异常。如果能把运行时异常尽量转换成编译时异常,那么这就会是一种好的实现方案。

<6>返回0长度的数组或者集合,而不是null

        考虑下面的代码:

    public static List<String> method(List<String> source) {
        if (source.isEmpty()) {
            return null;
        }
        //...
    }

        method方法传进来一个source集合,当source是一个空集合的时候,返回null。该代码首先考虑了空集合的特殊情况,是一种好的思想体现。但是这样却会要求使用者在调用该方法后必须考虑结果为null的情况,如下:

    List<String> returnList = method(source);
    if (null != returnList) {
        //do something...
    }

        如果能返回空集合,则不用考虑结果为null的情况:

    List<String> returnList = method(source);
    //do something...

        这样每次都需要处理null情况的话,很容易出错,可能会忘记要做这种处理。

        可能会有人认为:返回null比返回空集合,能减少分配数组集合的开销,返回空集合每次都会new出一个空对象。但是,下面要说的方法,每次通过Collections.emptyList方法返回的都只会是同一个实例:

    public static List<String> method(List<String> source) {
        if (source.isEmpty()) {
            return Collections.emptyList();
        }
        //do something...
        return null;
    }

        通过查看相关源码可知:emptyList方法返回的是一个静态内部类,所以保证了只有调用该方法的时候才会生成实例,同时只会有一份。

<7>如果需要精确的答案,避免使用float和double

        考虑下面的代码:

    float f = 1.01f;
    System.out.println(f - 0.31);

        我们预想的结果应该是0.7,而实际运行结果却是0.6999999904632568。double也会存在同样的问题。原因是浮点数在转换成二进制数的时候不能保证精确转换,可能存在误差。具体可以查看相关文章。

        一种解决办法是调用round四舍五入,但是四舍五入并不能保证在任何情况下都能正确执行。一种较好的解决办法是使用BigDecimal,如下所示:

    BigDecimal a = new BigDecimal(1.01);
    BigDecimal b = new BigDecimal(0.31);
    System.out.println(a.subtract(b).doubleValue());

        该方法能计算出正确结果。同时,int和long也能确保返回正确的值,所以也可以考虑对数据进行升值去掉小数点的方法解决。

<8>基本类型优先于装箱类型

        考虑下面的代码:

    public static int method(Integer a, Integer b) {
        return a < b ? -1 : (a == b ? 0 : 1);
    }

        该方法比较两个Integer值的大小,相当于是个比较器。如果a<b,返回-1;如果a=b,返回0;如果a>b,返回1。此时我传进两个参数都是new Integer("123"),本以为执行结果会是0,但实际结果却是1。为什么呢?

        首先会比较a<b。包装类型进行比较首先会自动拆箱,转换成int,然后比较。结果发现a不小于b。然后比较a==b。==比较的是地址,java中new出来的对象都会存在堆中。此时new出了两个对象,这两个对象的地址并不一样,尽管它们指向的都是常量123。所以a不等于b,结果会是1。

        我们可以通过下面的方式修正这个问题,即首先将Integer转换成int,再进行比较:

    public static int method(Integer a, Integer b) {
        int a1 = a;
        int b1 = b;
        return a1 < b1 ? 1 : (a1 == b1 ? 0 : -1);
    }

        该方法传进两个new Integer("123"),结果为0,运行正确。

        第二个例子如下:

    public static void main(String[] args) {
        Integer sum = 0;
        long start = System.currentTimeMillis();
        for (int i = 0; i < Integer.MAX_VALUE; i++) {
            sum += i;
        }
        long end = System.currentTimeMillis();
        System.out.println(end - start);
    }

        该方法实现遍历添加的操作,经过笔者在自己机器上实测,运行时间大概在4167毫秒左右。但是在第二行将sum变量类型由Integer改成int,其他代码不变,再次运行。运行时间大概在540毫秒左右,高下立判。

        那为什么会有这么大的差距呢?原因是因为程序一直在做大量的自动拆箱和自动装箱操作。字节码文件如下:


        首先sum是Integer类型,它会自动拆箱转成int类型和i进行加法运算,加出来的值再自动装箱成Integer类型赋给sum。也就是每运行一次循环都有一次拆箱和装箱的操作,导致效率低下。

        包装类型在例如集合中的元素时应该使用它,因为集合中不能使用基本类型。在必要的情况下使用包装类型。否则,应该使用基本类型。

<9>当心字符串连接的性能

        如下图所示:

public class Test {

    private static int TIME = 200000;

    public static void main(String[] args) {
        long start = System.currentTimeMillis();
        method1();
        long end = System.currentTimeMillis();
        System.out.println(end - start);
    }

    public static String method1() {
        String result = "";
        for (int i = 0; i < TIME; i++) {
            result += i;
        }
        return result;
    }
}

        method1方法实现将String对象循环遍历添加的操作。这里为了能显著看出差异,循环次数选择了二十万次。经过笔者在自己电脑上的实测,运行时间大概在41995毫秒左右,即41秒钟上下。

public class Test {

    private static int TIME = 200000;

    public static void main(String[] args) {
        long start = System.currentTimeMillis();
        method2();
        long end = System.currentTimeMillis();
        System.out.println(end - start);
    }

    public static String method2() {
        StringBuilder result = new StringBuilder(TIME);
        for (int i = 0; i < TIME; i++) {
            result.append(i);
        }
        return result.toString();
    }
}

        如上图所示,改用StringBuilder字符串变量循环遍历二十万次,运行时间大概在10毫秒左右,高下立判。

        那这回为什么又会有这么大的区别呢?众所周知,String常量的值是放在常量池中的,即如果你修改了String的值,是会在常量池中重新生成新的你修改过的值,而不是会把之前的值进行修改(如果修改后的值在常量池中已存在,则不会生成新值,确保常量池中同样的值只会有一份)。所以我循环了二十万次,会在常量池中生成二十万个常量(如果常量池中这些数值之前不存在的话),我其实只是要最后一次循环生成的值而已。

        但是,编译器会优化这段代码,会将字符串拼接优化成StringBuilder的append操作。如下图所示:


        通过反编译可知,编译器是将StringBuilder对象的new操作放在了循环体里面,即一共生成了二十万个StringBuilder对象。new出来的对象都是放在堆里,回收是靠垃圾回收器gc回收的。回收时间不确定,所以执行速度会变得这么慢。再来看一下method2的class文件,如下:


        由上可见StringBuilder对象的new操作是在循环体之外。这也提示我们当进行循环操作的时候,尽量要将new操作放在循环体外面,提高执行效率。这样的话,只会生成一个StringBuilder对象,拿它去进行遍历操作。

<10>尽量使用StringBuilder而不是StringBuffer

        先说结论:StringBuffer在大多数时候增加了大量不必要的成本,并且未必能达到目的。StringBuffer和StringBuilder的区别就是StringBuffer的很多方法上加了同步锁,如下面的append操作所示:

    @Override
    public StringBuilder append(String str) {
        super.append(str);
        return this;
    }
    @Override
    public synchronized StringBuffer append(Object obj) {
        toStringCache = null;
        super.append(String.valueOf(obj));
        return this;
    }

        考虑下面的代码:

    StringBuffer result = new StringBuffer();
    for (int i = 0; i < 500000; i++) {
        result.append(i);
    }

        StringBuffer的每一步操作都先获取同步锁,然后释放同步锁,在大数据量高并发的情况下,这会造成高昂的成本。同时考虑一种情况:在一个线程刚跑完一次循环,释放完锁,准备下次循环上锁的时候,有另一个线程做了result.append(”abc”)的操作,是能插入成功的,那么数据此时就已经被污染了。也就是说,StringBuffer在释放完锁而再次上锁的短暂间隙时间内不能保证线程安全,StringBuffer并不能保证在任何情况下都能线程安全地执行出结果。

        要想线程安全,可以采用如下方案:

    StringBuffer buffer = new StringBuffer();
    synchronized (buffer) {
        for (int i = 0; i < 500000; i++) {
            buffer.append(i);
        }
    }

        即使用者在集合外面手动加上同步锁。但是这样的话,使用StringBuffer就失去了它的同步意义,用StringBuilder+同步锁的方式一样能保证线程安全,且性能比StringBuffer更好。所以这也就是为什么StringBuffer这个类有天生缺陷的原因。在API设计中有一条规则是这样说的:一个类不应该自己实现同步,而应该把同步的工作留给用户。也就是说,使用者更加清楚业务需求,更加清楚什么时候要加同步锁,什么时候不加,所以应该要将同步的工作交给使用者来处理,而不是通过工具类里的方法来实现。

        同样的道理,Vector、Stack也有着天生的缺陷不建议使用,所以后来出现了ArrayList、LinkedList等等。

<11>针对接口编程,而不针对实现编程

        该条建议指的是应该使用接口而不是用类作为参数的类型,更一般地讲,应该优先使用接口而不是类来引用对象,如下:

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

        而不是像下面这么写:

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

        如果你养成了用接口作为类型的习惯,你的程序将会更加灵活。当你决定更换实现时,所要做的就只是改变构造器中类的名称(或者使用一个不同的静态工厂)。例如,第一个声明可以被改变为:

List<String> list = new LinkedList<>();

        周围的所有代码都可以继续工作。周围的代码并不知道原来的实现类型,所以它们对于这种变化并不在意。

        工厂模式也是该原则的一个体现。方法的参数为了能保证通用性,也可以做成接口参数类型。

<12>@Transactional的自调用失效问题

        有时候配置了注解@Transactional,但是它会失效,这里要注意一些细节问题,以避免落入陷阱。注解@Transactional的底层实现是Spring AOP技术,而Spring AOP技术使用的是动态代理。这就意味着对于静态(static)方法和非public方法,注解@Transactional是失效的。

        还有一个更为隐秘的,而且在使用过程中极其容易犯错误的——自调用。先解释一下什么是自调用的问题。

        所谓自调用,就是一个类的一个方法去调用自身另外一个方法的过程。代码如下:

public class RoleServiceImpl {

    @Autowired
    private RoleMapper roleMapper = null;

    @Override
    @Transactional(propagation = Propagation.REQUIRES_NEW, isolation = Isolation.READ_COMMITTED)
    public int insertRole(Role role) {
        return roleMapper.insertRole(role);
    }

    @Override
    @Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.READ_COMMITTED)
    public int insertRoleList(List<Role> roleList) {
        int count = 0;
        for (Role role : roleList) {
            try {
                //调用自身类的方法,产生自调用问题
                insertRole(role);
                count++;
            } catch (Exception ex) {
                ex.printStackTrace();
            }
        }
        return count;
    }
}

        在insertRoleList方法的实现中,它调用了自身类实现insertRole的方法,而insertRole声明是REQUIRES_NEW的传播行为,也就是每次调用就会产生新的事务运行。但是通过运行该段程序可知,角色插入两次都使用了同一事务,也就是说,在insertRole上标注的@Transactional失效了,这是一个很容易掉进去的陷阱。

        出现这个问题的根本原因在于AOP的实现原理。由于@Transactional的实现原理是AOP,而AOP的实现原理是动态代理,而在上述代码中使用的是自己调用自己的过程。换句话说,在自调用的情况下,并不会生成代理对象,从而产生代理对象的调用。这样就不会产生AOP去为我们设置@Transactional配置的参数,这样就出现了自调用注解失效的问题。

        为了克服这个问题,一种解决办法是使用两个服务类,Spring IoC容器中为你生成了RoleService的代理对象,这样就可以使用AOP,且不会出现自调用问题;另一种解决办法是可以直接从容器中获取RoleService的代理对象,如下所示:

    @Override
    @Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.READ_COMMITTED)
    public int insertRoleList(List<Role> roleList) {
        int count = 0;
        //从容器中获取RoleService对象,实际是一个代理对象
        RoleService service = ctx.getBean(RoleService.class);
        for (Role role : roleList) {
            try {
                service.insertRole(role);
                count++;
            } catch (Exception ex) {
                ex.printStackTrace();
            }
        }
        return count;
    }

        但是该方法有一个弊端,就是从容器获取代理对象的方法有侵入之嫌,你的类需要依赖于Spring IoC容器,推荐使用第一种使用两个服务类的方法解决。

<13>错误使用Service

        如下代码所示:

public class RoleController {

    @Autowired
    private RoleService     roleService     = null;
    @Autowired
    private RoleListService roleListService = null;

    public void errorUseServices() {
        Role role1 = new Role();
        role1.setRoleName("role_name_1");
        role1.setNote("role_note_1");
        roleService.insertRole(role1);
        Role role2 = new Role();
        role2.setRoleName("role_name_2");
        role2.setNote("role_note_2");
        roleService.insertRole(role2);
    }
}

        这里存在的问题是两个insertRole方法根本不在同一个事务里。

        当一个Controller使用Service方法时,如果这个Service标注有@Tranctional,那么它就会启用一个事务,而一个Service方法完成后,它就会释放该事务,所以前后两个insertRole的方法是在两个不同的事务中完成的。

        这样如果第一个插入成功了,而第二个插入失败了,就会使数据库数据不完全同时成功或者失败,可能产生严重的数据不一致的问题,给生产带来严重的损失。

<14>错误捕捉异常

        如下代码所示:

    @Autowired
    private ProductService     productService;
    @Autowired
    private TransactionService transactionService;

    @Override
    @Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.READ_COMMITTED)
    public int doTransaction(TransactionBean trans) {
        int result = 0;
        try {
            //减少库存
            result = productService.decreaseStock(trans.getProductId, trans.getQuantity());
            //如果减少库存成功则保存记录
            if (result > 0) {
                transactionService.save(trans);
            }
        } catch (Exception ex) {
            //自行处理异常代码
            //记录异常日志
            log.info(ex);
        }
        return result;
    }

        这里的问题是方法已经存在异常了,由于不了解Spring的事务约定,在两个操作的方法里面加入了自己的try-catch语句,就可能发生这样的结果。当减少库存成功了,但是保存交易信息时失败而发生了异常,此时由于加入了try-catch语句,所以Spring在数据库事务所约定的流程中再也得不到任何异常信息了,不会认为这是一个出现错误的方法。此时Spring就会提交事务,这样就出现了库存减少,而交易记录却没有的糟糕情况。下面是改进方法:

    @Autowired
    private ProductService     productService;
    @Autowired
    private TransactionService transactionService;

    @Override
    @Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.READ_COMMITTED)
    public int doTransaction(TransactionBean trans) {
        int result = 0;
        try {
            //减少库存
            result = productService.decreaseStock(trans.getProductId, trans.getQuantity());
            //如果减少库存成功则保存记录
            if (result > 0) {
                transactionService.save(trans);
            }
        } catch (Exception ex) {
            //自行处理异常代码
            //记录异常日志
            log.info(ex);
            //自行抛出异常,让Spring事务管理流程获取异常,进行事务管理
            throw new RuntimeException(ex);
        }
        return result;
    }

        在catch子句中抛出一个运行时异常,这样在Spring的事务流程中,就会捕捉到抛出的这个异常,进行事务回滚,从而保证了产品减库存和交易记录保存的一致性,这才是正确的用法。

        默认配置下,spring只有在抛出的异常为运行时unchecked异常时才回滚该事务,也就是抛出的异常为RuntimeException的子类(Error也会导致事务回滚),而抛出checked受检异常则不会导致事务回滚。

<15>避免在service层循环调用dao层的方法

        如下代码所示:

    @Override
    public void updateStatCdForPub(StandardVO standardVO, String statCd) {
        //do something...
        for (StandardVO standardVO2 : codeItemList) {
            standardPublishAbolishDao.updateStatCd(standardVO2, statCd);
        }
    }

        现在service层需要完成一个更新数据集合中属性的操作。如上面所写就是一种不好的实现:将循环写在service层,意味着要循环去和数据库进行交互,如果数据量大的话,这个效率会很慢。应该将循环写在mapper文件中(mybaties),如下所示:

    <update id="updateStatCd " parameterType="java.util.List">
	UPDATE
	table1
	SET
	stat_cd= #{statCd}
	WHERE
	id
	IN
	<foreach collection="codeItemList" index="index"
		item="codeItemList" open="(" separator="," close=")">
		#{codeItemList.id}
	</foreach>
    </update>

        mybaties支持foreach功能,我们应该尽量用起来。其他的持久层框架也都是类似的思路,将循环放在里面,减少java和数据库的交互次数。

<16>将代码中变化的部分抽取出来

        此条原则是一个很抽象的概念。具体来说,工厂模式是其一种体现:将对象实例化工作的代码拆出来,如果以后有新的实例化代码要添加,直接在拆出来的方法中修改就行了,不需要再动别的代码。在我们的日常写代码过程中,如果碰到经常需要扩展改变的代码,也可以考虑抽取出来,方便维护。

猜你喜欢

转载自blog.csdn.net/weixin_30342639/article/details/80754501
今日推荐