zoukankan      html  css  js  c++  java
  • (02)验证Spring的事务及其7种传播机制真实案例

      原文:

      https://blog.csdn.net/soonfly/article/details/70305683

      https://www.cnblogs.com/dennyzhangdd/p/9602670.html

      https://blog.csdn.net/fly910905/article/details/80000242

      说起Spring的事务,仿佛是很熟悉的老朋友了,然鹅。。。现实却很残酷。。。起初我以 Spring、Mybatis、druid、Mysql尝试,发现其中一个问题,无论数据源的defaultAutoCommit设置为true或者false,事务总会自动提交。确定配置无误后,发现网上有一种说法是把数据库的autocommit设置为OFF,即关闭数据库的自动提交,且不说这样是否可以,这种方法本身就有问题。因为在代码中获取到一个Connection,执行commit即可提交,不执行commit就不会提交,完全可以由代码控制,去设置数据库本身,这很不合理。经过一番周折还是没有搞定这个问题。

      无奈之下我把Mybatis换成JdbcTemplate,终于正常了。下面基于Spring+JdbcTemplate+druid+Mysql说说事务。

      1、事务、事务传播机制的简单说明

      事务是一个单体行为,只有提交了事务,数据才会保存到数据库,否则不会保存到数据库中。事务传播行要求至少有两个东西,才可以发生传播。指的是当一个事务方法被另一个事务方法调用时,这个被调用方法的事务方法应该如何进行。例如:methodA事务方法调用methodB事务方法时,methodB是继续在调用者methodA的事务中运行呢,还是为自己开启一个新事务运行,这就是由methodB的事务传播行为决定的。

      2、defaultAutoCommit与Transactional的关系

      配置数据源时参数defaultAutoCommit设置为ture、false代表自动、不自动提交。Transactional注解也控制事务,他们有什么关系?下面用例子说明。

      (1)defaultAutoCommit为false,不使用Transactional注解。结论:不会提交

    <property name="defaultAutoCommit" value="false" />
    public void save() {
      testDao.save();
    }

       (2)defaultAutoCommit为false,使用Transactional注解。结论:会提交

    <property name="defaultAutoCommit" value="false" />
    @Transactional
    public void save() {   testDao.save(); }

      (3)defaultAutoCommit为true,不使用Transactional注解。结论:会提交

    <property name="defaultAutoCommit" value="true" />
    public void save() {
      testDao.save();
    }

      (4)defaultAutoCommit为true,使用Transactional注解。结论:会提交

    <property name="defaultAutoCommit" value="true" />
    @Transactional
    public void save() {
      testDao.save();
    }

      总结:只要defaultAutoCommit或者Transactional有一项设置为可提交即可。

      3、Transactional与异常自动回滚的关系

      在项目中希望当方法产生异常时自动回滚事务,下面我们在defaultAutoCommit设置为false的情况下进行验证

    <property name="defaultAutoCommit" value="false" />

      (1)使用Transactional的默认配置,抛出检查型异常。事务不会回滚

    @Transactional
    public void save () throws Exception {
      testDao.save();
      throw new ClassNotFoundException();
    }

      (2)使用Transactional的默认配置,抛出运行时异常。事务会回滚

    @Transactional
    public void save (){
      testDao.save();
      throw new RuntimeException();
    }

      (3)使用Transactional注解,指定rollbackFor为抛出的异常或其父类时,检查型异常会回滚

    @Transactional(rollbackFor=Exception.class)
    public void save () throws Exception {
      testDao.save();   
    throw new ClassNotFoundException(); }

      (4)使用Transactional注解,指定rollbackFor不是抛出的异常或其父类时,运行时异常会回滚(运行时异常与rollbackFor无关,肯定回滚)

    @Transactional(rollbackFor=FileNotFoundException.class)
    public void save () throws Exception {
      testDao.save();
      throw new RuntimeException();
    }

      (5)使用Transactional注解,捕获异常,事务不会回滚

    @Transactional
    public void save () throws Exception {
      try {
        testDao.save();
        throw new RuntimeException();
      } catch (Exception e) {
      // TODO: handle exception
      }
    }

    @Transactional
    public void save () throws Exception {
      try {
        testDao.save();
        throw new ClassNotFoundException();
      } catch (Exception e) {
        // TODO: handle exception
      }
    }

      (6)捕获的异常需要手动回滚,手动回滚时检查型异常可以不指定rollbackFor

    @Transactional
    public void save () {
      try {
        testDao.save();
        throw new ClassNotFoundException();
      } catch (Exception e) {
        TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(); 
      }
    }

    @Transactional(rollbackFor=FileNotFoundException.class)
    public void save () {
      try {
        testDao.save();
        throw new ClassNotFoundException();
      } catch (Exception e) {
      TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(); 
      }
    }

    @Transactional
    public void save () {
      try {
        testDao.save();
        throw new RuntimeException();
      } catch (Exception e) {
        TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(); 
      }
    }

      (7) Transactional要加在主动(直接)调用的方法上面,以下事务不会提交,没有开启事务(spring容器管理的类直接调用test)

    public void test(){
        save();
    }
    
    @Transactional
    public void save () {
        try {
            testDao.save();
            throw new RuntimeException();
        } catch (Exception e) {
        }
    }    

      4、spring中的事务传播行为

      spring中共有7种事务传播行为,分别介绍如下:

      (1)PROPAGATION_REQUIRED:如果存在一个事务,则支持当前事务。如果没有事务则开启一个新的事务。

      方法A加注解,方法B也加注解,当方法A运行时会开启事务A,调用方法B时,方法B也加入到事务A中

     @Transactional(propagation = Propagation.REQUIRED)
     public void methodA() {
       methodB();
       testDao.methodA();
     }
    
     @Transactional(propagation = Propagation.REQUIRED)
     public void methodB() {
       testDao.methodB();
     }

       如上图,总共开启了一个事务。

      (2)PROPAGATION_SUPPORTS:如果存在一个事务,支持当前事务,如果没有事务,则不会开启事务。

    @Transactional(propagation = Propagation.REQUIRED)
    public void methodA() {
      methodB();
      testDao.methodA();
    }
    
    @Transactional(propagation = Propagation.SUPPORTS)
    public void methodB() {
      testDao.methodB();
    }

      如果直接调用methodA,methodA会开启一个事务,methodA调用methodB,则methodB支持当前methodA开启的事务,如下图:

      如果直接调用methodB,不会开启事务,如下图:

      如果直接调用methodA,由于methodA是SUPPORTS,不会开始事务,methodB不是直接调用,也不会开启事务

    @Transactional(propagation = Propagation.SUPPORTS)
    public void methodA() {
      methodB();
      testDao.methodA();
    } @Transactional(propagation
    = Propagation.REQUIRED)   public void methodB() {   testDao.methodB(); }

      (3)PROPAGATION_MANDATORY:必须在一个事务中运行,否则报异常

    @Transactional(propagation = Propagation.REQUIRED)
    public void methodA() {
    methodB();
    testDao.methodA();
    }

    @Transactional(propagation
    = Propagation.MANDATORY)   public void methodB() {   testDao.methodB(); }

      直接调用methodA,开启一个事务,methodB也在该事务中运行

      直接调用methodB,报异常 No existing transaction found for transaction marked with propagation 'mandatory'

     @Transactional(propagation = Propagation.MANDATORY)
     public void methodB() {
       testDao.methodB();
     }

      (4)PROPAGATION_REQUIRES_NEW:开启一个新事务。如果一个事务已经存在,则先将存在的事务挂起,执行完这个新事务,再执行挂起的事务,两个事务的成功或失败没有联系。

    @Transactional(propagation = Propagation.REQUIRED)
    public void methodA() {
      methodB();
      testDao.methodA();
    }
    @Transactional(propagation
    = Propagation.REQUIRES_NEW) public void methodB() {   testDao.methodB(); }

      从上图中看到,并没有挂起旧事务,先执行新事务,因为只有使用JtaTransactionManager作为事务管理器时才生效。后面再研究。。。

      (5)PROPAGATION_NOT_SUPPORTED:在非事务中运行。如果有事务正在运行,他将在运行期被挂起,直到这个事务提交或者回滚才恢复执行。

     @Transactional(propagation = Propagation.REQUIRED)
     public void methodA() {
       methodB();
        testDao.methodA();
     }
    
     @Transactional(propagation = Propagation.NOT_SUPPORTED)
     public void methodB() {
       testDao.methodB();
     }

      直接调用methodA,运行到methodB,事务应该挂起,即methodB对应的数据不会保存到数据库。

      但上图与预期的不一致,因为也需要JtaTransactionManager作为事务管理器 。

      直接调用methodB不会开启事务,可以自己尝试一下。

      (6)PROPAGATION_NEVER:总是非事务地执行,如果存在一个活动事务,则抛出异常。

    @Transactional(propagation = Propagation.NEVER)
    public void methodB() {
      testDao.methodB();
    }

      直接调用methodB,不会开启事务

     @Transactional(propagation = Propagation.REQUIRED)
     public void methodA() {
       methodB();
       testDao.methodA();
     }
    
     @Transactional(propagation = Propagation.NEVER)
     public void methodB() {
       testDao.methodB();
     }

      直接调用methodA,报异常,发现下面日志没有报异常,,,是版本问题还是我的理解有误???先留个疑问吧

      (7) PROPAGATION_NESTED:如果一个活动的事务存在,则运行在一个嵌套的事务中。 如果没有活动事务, 则按PROPAGATION_REQUIRED属性执行。

      附 相关配置文件和代码

      pom.xml

    <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
      <modelVersion>4.0.0</modelVersion>
      <groupId>com.sl</groupId>
      <artifactId>spring-web-transaction</artifactId>
      <version>0.0.1-SNAPSHOT</version>
      <packaging>war</packaging>
      
      <!-- 项目属性 -->
      <properties>
          <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
        <tomcat.version>2.2</tomcat.version>
        
          <spring.version>5.1.0.RELEASE</spring.version>
          <!-- 声明式事务 -->
          <aspectjweaver.version>1.7.4</aspectjweaver.version>
          <druid.version>1.1.11</druid.version>
          <mysql.driver.version>5.1.30</mysql.driver.version>
          
        <jackson.version>2.5.4</jackson.version>
          <slf4j.version>1.7.7</slf4j.version>
      </properties>
      
      <!-- 依赖 -->
      <dependencies>
        
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-webmvc</artifactId>
            <version>${spring.version}</version>
        </dependency>
        
        <dependency>
                <groupId>org.aspectj</groupId>
                <artifactId>aspectjweaver</artifactId>
                <version>${aspectjweaver.version}</version>
            </dependency>
        
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-jdbc</artifactId>
            <version>${spring.version}</version>
        </dependency>
          
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>${druid.version}</version>
        </dependency>
        
        <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
                <version>${mysql.driver.version}</version>
                <scope>runtime</scope>
            </dependency>
        
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>${slf4j.version}</version>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-log4j12</artifactId>
            <version>${slf4j.version}</version>
        </dependency>
        
      </dependencies>
      
      <build>
            <plugins>
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-war-plugin</artifactId>
                    <version>2.4</version>
                    <configuration>
                        <webappDirectory>${project.build.directory}/${project.artifactId}</webappDirectory>
                        <warName>${project.artifactId}</warName>
                    </configuration>
                </plugin>    
                <!-- tomcat7插件 -->
                <plugin>
                    <groupId>org.apache.tomcat.maven</groupId>
                    <artifactId>tomcat7-maven-plugin</artifactId>
                    <version>${tomcat.version}</version> 
                    <configuration>
                        <port>8080</port>
                        <path>/${project.artifactId}</path>
                        <uriEncoding>${project.build.sourceEncoding}</uriEncoding>
                    </configuration>
                </plugin>    
                
            </plugins>
        </build>
      
    </project>
    View Code

      spring-context.xml

    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xmlns:aop="http://www.springframework.org/schema/aop"
           xmlns:context="http://www.springframework.org/schema/context"
           xmlns:tx="http://www.springframework.org/schema/tx"
           xmlns:task="http://www.springframework.org/schema/task"
           xsi:schemaLocation="http://www.springframework.org/schema/beans
                      http://www.springframework.org/schema/beans/spring-beans.xsd
                      http://www.springframework.org/schema/aop
                      http://www.springframework.org/schema/aop/spring-aop.xsd
                      http://www.springframework.org/schema/context
                      http://www.springframework.org/schema/context/spring-context.xsd
                      http://www.springframework.org/schema/tx
                      http://www.springframework.org/schema/tx/spring-tx.xsd
                      http://www.springframework.org/schema/task  http://www.springframework.org/schema/task/spring-task-3.1.xsd
                      ">
                     
        <!-- 启用注解 -->
            <context:component-scan base-package="com.sl.*">
                <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller" />
            </context:component-scan>
             <tx:annotation-driven transaction-manager="transactionManager"/>  
    
        <!--读取配置文件;可以读取多个-->
        <bean id="propertyConfigurer" class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
            <property name="locations">
                <list>
                    <value>classpath:db.properties</value>
                </list>
            </property>
        </bean>
        
        <!-- 阿里 druid数据库连接池 -->
        <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" destroy-method="close">
            <!-- 数据库基本信息配置 -->
            <property name="url" value="${url}" />
            <property name="username" value="${username}" />
            <property name="password" value="${password}" />
            <property name="driverClassName" value="${driverClassName}" />
            <property name="defaultAutoCommit" value="false" />
            <property name="filters" value="${filters}" />
            <!-- 最大并发连接数 -->
            <property name="maxActive" value="${maxActive}" />
            <!-- 初始化连接数量 -->
            <property name="initialSize" value="${initialSize}" />
            <!-- 配置获取连接等待超时的时间 -->
            <property name="maxWait" value="${maxWait}" />
            <!-- 最小空闲连接数 -->
            <property name="minIdle" value="${minIdle}" />
            <!-- 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 -->
            <property name="timeBetweenEvictionRunsMillis" value="${timeBetweenEvictionRunsMillis}" />
            <!-- 配置一个连接在池中最小生存的时间,单位是毫秒 -->
            <property name="minEvictableIdleTimeMillis" value="${minEvictableIdleTimeMillis}" />
            <property name="validationQuery" value="${validationQuery}" />
            <property name="testWhileIdle" value="${testWhileIdle}" />
            <property name="testOnBorrow" value="${testOnBorrow}" />
            <property name="testOnReturn" value="${testOnReturn}" />
            <property name="maxOpenPreparedStatements" value="${maxOpenPreparedStatements}" />
            <!-- 打开removeAbandoned功能 -->
            <property name="removeAbandoned" value="${removeAbandoned}" />
            <!-- 1800秒,也就是30分钟 -->
            <property name="removeAbandonedTimeout" value="${removeAbandonedTimeout}" />
            <!-- 关闭abanded连接时输出错误日志 -->
            <property name="logAbandoned" value="${logAbandoned}" />
        </bean>
        
            <!--配置数据库连接-->
        <bean name="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
            <property name="dataSource" ref="dataSource"></property>
        </bean>
    
    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate"> 
     <property name="dataSource" ref="dataSource"></property>
    </bean>
       
    
    </beans>
    View Code

      spring-mvc.xml

    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xmlns:mvc="http://www.springframework.org/schema/mvc"
           xmlns:context="http://www.springframework.org/schema/context"
           xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
            http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd
            http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
    
    
        <context:component-scan base-package="com.sl.controller" />
         <mvc:default-servlet-handler/>
    
        <!-- 对静态资源文件的访问  restful-->
    
        <!--  -->
        <mvc:annotation-driven>
        </mvc:annotation-driven>
        
        <!-- 配置SpringMVC的视图解析器 -->
        <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
            <property name="prefix" value="/WEB-INF/"/>
            <property name="suffix" value=".jsp"/>
        </bean>
    
    
    </beans>
    View Code

      log4j.properties

    # DEBUG,INFO,WARN,ERROR,FATAL
    log4j.rootLogger=DEBUG,CONSOLE,FILE
    
    log4j.appender.CONSOLE=org.apache.log4j.ConsoleAppender
    log4j.appender.CONSOLE.layout=org.apache.log4j.PatternLayout
    log4j.appender.CONSOLE.layout.ConversionPattern=[%-5p] %d{yyyy-MM-dd HH:mm:ss} %C{1}@(%F:%L):%m%n
    
    log4j.appender.FILE=org.apache.log4j.DailyRollingFileAppender
    log4j.appender.FILE.File=${catalina.base}/logs/spring-web.log
    log4j.appender.FILE.Encoding=utf-8
    log4j.appender.FILE.DatePattern='.'yyyy-MM-dd
    log4j.appender.FILE.layout=org.apache.log4j.PatternLayout
    log4j.appender.FILE.layout.ConversionPattern=[%-5p] %d{yyyy-MM-dd HH:mm:ss} %C{1}@(%F:%L):%m%n
    
    log4j.logger.com.mybatis=DEBUG
    log4j.logger.com.mybatis.common.jdbc.SimpleDataSource=DEBUG 
    log4j.logger.com.mybatis.common.jdbc.ScriptRunner=DEBUG 
    log4j.logger.com.mybatis.sqlmap.engine.impl.SqlMapClientDelegate=DEBUG 
    log4j.logger.java.sql.Connection=DEBUG
    log4j.logger.java.sql.Statement=DEBUG
    log4j.logger.java.sql.PreparedStatement=DEBUG
    log4j.logger.java.sql.ResultSet=DEBUG
    View Code

      db.properties

    url:jdbc:mysql://localhost:3309/mytest?useUnicode=true&characterEncoding=utf8&characterSetResults=utf8 
    driverClassName:com.mysql.jdbc.Driver
    username:root
    password:123456
           
    filters:stat
       
    maxActive:20
    initialSize:1
    maxWait:60000
    minIdle:10
    maxIdle:15
       
    timeBetweenEvictionRunsMillis:60000
    minEvictableIdleTimeMillis:300000
       
    validationQuery:SELECT 'x'
    testWhileIdle:true
    testOnBorrow:false
    testOnReturn:false
    
    maxOpenPreparedStatements:20
    removeAbandoned:true
    removeAbandonedTimeout:1800
    logAbandoned:true
    View Code

      TestController.java

    package com.sl.controller;
    
    import java.util.List;
    import java.util.Map;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Controller;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.ResponseBody;
    
    import com.sl.service.TestService;
    
    @Controller
    @RequestMapping("/test")
    public class TestController {
    
        @Autowired
        private TestService testService;
        
        @RequestMapping("/save")
        @ResponseBody
        public void save(){
            //testService.save();
            testService.methodA();
        }
        
        @RequestMapping("/del")
        @ResponseBody
        public void del() throws Exception {
            testService.del();
        }
        
        @RequestMapping("/get")
        @ResponseBody
        public String get(){
            String str= "...";
            List<Map<String, Object>> list = testService.get();
            for(Map<String, Object> map : list) {
                str = map.get("name").toString();
            }
            return str;
        }
        
        @RequestMapping("/update")
        @ResponseBody
        public void update() throws Exception {
            testService.update();
        }
    }
    View Code  

      TestService.java

    package com.sl.service;
    
    import java.util.List;
    import java.util.Map;
    
    public interface TestService {
        public void save() throws Exception;
        public void del();
        public void test();
        public List<Map<String, Object>> get();
        public void update();
        public void methodA();
        public void methodB();
    }
    View Code

      TestServiceImpl.java

    package com.sl.service.impl;
    
    import java.util.List;
    import java.util.Map;
    
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    import org.springframework.transaction.annotation.Propagation;
    import org.springframework.transaction.annotation.Transactional;
    
    import com.sl.dao.TestDao;
    import com.sl.service.TestService;
    
    @Service
    public class TestServiceImpl implements TestService{
    
    private static Logger logger=LoggerFactory.getLogger(TestServiceImpl.class);
    
        @Autowired
        private TestDao testDao;
        
    
        public void test(){
            save();
        }
    
        @Transactional
        public void save () {
            try {
                testDao.save();
                throw new RuntimeException();
            } catch (Exception e) {
            }
        }
        
        @Transactional(propagation = Propagation.REQUIRED)
        public void methodA() {
         methodB();
         testDao.methodA();
        }
    
        @Transactional(propagation = Propagation.NEVER)
        public void methodB() {
            testDao.methodB();
        }
    
        @Override
        @Transactional(propagation=Propagation.NEVER)
        public void del() {
            // TODO Auto-generated method stub
            testDao.del();
        }
    
        @Override
        public List<Map<String, Object>> get() {
            // TODO Auto-generated method stub
            return testDao.get();
        }
    
        @Override
        @Transactional(propagation=Propagation.REQUIRED)
        public void update() {
            // TODO Auto-generated method stub
            testDao.update();
        }
    
    }
    View Code

      TestDao.java

    package com.sl.dao;
    
    import java.sql.SQLException;
    import java.util.List;
    import java.util.Map;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.jdbc.core.JdbcTemplate;
    import org.springframework.stereotype.Repository;
    
    @Repository
    public class TestDao {
    
        @Autowired
        private JdbcTemplate jdbcTemplate;
        
        public void save() {
            String sql="insert into t_testa(name) values('11111')";
            jdbcTemplate.execute(sql);
        }
        
        public void methodA() {
            String sql="insert into t_testa(name) values('11111')";
            jdbcTemplate.execute(sql);
        }
        
        public void methodB() {
            String sql="insert into t_testb(name) values('11111')";
            jdbcTemplate.execute(sql);
        }
        
        public void del() {
            String sql="delete from t_testa";
            jdbcTemplate.execute(sql);
        }
        
        public List<Map<String, Object>> get() {
            String sql="select * from t_testa";
            List<Map<String, Object>> list = jdbcTemplate.queryForList(sql);
            return list;
        }
        
        public void update() {
            String sql="update t_testa set name = 'asdfg'";
            jdbcTemplate.execute(sql);
        }
    }
    View Code
  • 相关阅读:
    JavaScript脚本语言特色时钟
    这个周末安排,
    市场营销书籍推荐
    比较好的管理类书籍推荐
    如何培养自己的领导力?或许你该看看这些书
    十本最畅销的市场营销书籍,你看过几本?
    如何提高情商?答案可能在《情商必读12篇》这本书里
    如何管理好员工?
    做销售要看哪些书?《销售管理必读12篇》了解下
    管理书籍推荐,你看过哪些呢?
  • 原文地址:https://www.cnblogs.com/javasl/p/12334583.html
Copyright © 2011-2022 走看看