构建SpringBoot基本框架(上篇)

   当单体架构到达一定规模时,修正bug和正确的添加新功能变的非常困难,并且很耗时。单体应用模块之间的强依赖很可能因为某一模块而导致整个应用宕机,很影响开发效率。所以说复杂而笨重的单体式应用就非常不适合持续性开发了。这时候Spring Boot完美解决了复杂臃肿的单体式应用出现的问题。它对开发者异常友好,很容易常见一个单模块的Spring应用、内置了常见的web服务器,模块最终可以打包成jar包启动、提供了很多可以选择的'starter'、简化了Spring大量配置等...  当前开发系统(百万行代码)就存在单体式应用的问题,花了2天时间研究了下Spring Boot,准备后期拆分成Spring Boot微服务架构
Spring Boot框架搭建
一、创建Spring Boot
      IDEA编辑器的话,只需要File-->New-->Project-->Spring Initializr 需要的依赖选择后就可以创建一个Spring Boot应用了。这个时候就可以启动应用程序了,代码块:
@Controller
@EnableAutoConfiguration
public class SampleController {

    @RequestMapping("/")
    @ResponseBody
    String home() {
        return "Hello World!";
    }

    public static void main(String[] args) throws Exception {
        SpringApplication.run(SampleController.class, args);
    }
}
运行SampleController.java后,访问http://localhost:8080,页面会返回Hello World ! 结果。这样一个基本的微服务就搭建成功了。比普通的web框架简易太多了,再也不用管理Tomcat和Spring的大量配置了!
二、Spring Boot集成log4j2
    添加POM依赖:
 <dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>
添加log4j2.xml配置文件:
<?xml version="1.0" encoding="UTF-8"?>
<configuration status="warn">
    <appenders>
        <console name="Console" target="SYSTEM_OUT">
            <PatternLayout pattern="[%d{yyyy-MM-dd HH:mm:ss:SSS}] [%p] - [Thread:%t] -%l - %m%n"/>
        </console>

        <RollingFile name="RollingFileInfo" fileName="/Users/tian_dd/logs/spring-boot/info.log"
                     filePattern="/Users/tian_dd/logs/spring-boot/$${date:yyyy-MM}/info-%d{yyyy-MM-dd}-%i.log">
            <Filters>
                <ThresholdFilter level="INFO"/>
                <ThresholdFilter level="WARN" onMatch="DENY" onMismatch="NEUTRAL"/>
            </Filters>
            <PatternLayout pattern="[%d{yyyy-MM-dd HH:mm:ss:SSS}] [%p] - [Thread:%t] -%l - %m%n"/>
            <Policies>
                <TimeBasedTriggeringPolicy/>
                <SizeBasedTriggeringPolicy size="100 MB"/>
            </Policies>
        </RollingFile>

        <RollingFile name="RollingFileWarn" fileName="/Users/tian_dd/logs/spring-boot/warn.log"
                     filePattern="/Users/tian_dd/logs/spring-boot/$${date:yyyy-MM}/warn-%d{yyyy-MM-dd}-%i.log">
            <Filters>
                <ThresholdFilter level="WARN"/>
                <ThresholdFilter level="ERROR" onMatch="DENY" onMismatch="NEUTRAL"/>
            </Filters>
            <PatternLayout pattern="[%d{yyyy-MM-dd HH:mm:ss:SSS}] [%p] - [Thread:%t] -%l - %m%n"/>
            <Policies>
                <TimeBasedTriggeringPolicy/>
                <SizeBasedTriggeringPolicy size="100 MB"/>
            </Policies>
        </RollingFile>

        <RollingFile name="RollingFileError" fileName="/Users/tian_dd/logs/spring-boot/error.log"
                     filePattern="/Users/tian_dd/logs/spring-boot/$${date:yyyy-MM}/error-%d{yyyy-MM-dd}-%i.log">
            <ThresholdFilter level="ERROR"/>
            <PatternLayout pattern="[%d{yyyy-MM-dd HH:mm:ss:SSS}] [%p] - [Thread:%t] -%l - %m%n"/>
            <Policies>
                <TimeBasedTriggeringPolicy/>
                <SizeBasedTriggeringPolicy size="100 MB"/>
            </Policies>
        </RollingFile>
    </appenders>

    <loggers>

        <logger name="org.springframework" level="INFO">
        </logger>
        <logger name="org.hibernate" level="INFO">
        </logger>

        <root level="all">
            <appender-ref ref="Console"/>
            <appender-ref ref="RollingFileInfo"/>
            <appender-ref ref="RollingFileWarn"/>
            <appender-ref ref="RollingFileError"/>
        </root>

    </loggers>

</configuration>
这样系统就可以记录操作日志了。 
三、Spring Boot集成Mybatis(数据库动态切换)&YML&单元测试
    添加POM依赖:
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.1.1</version>
<exclusions>
<exclusion>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-jdbc</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
yml文件配置:
spring:
  profiles:
    active: dev
#配置监控项
server:
  port: 8082
management:
  port: 9999
  health:
    mail:
      enabled: false
  security:
    enabled: false

info:
  app:
    name: "@project.name@"
    description: "@project.description@"
    version: "@project.version@"

mybatis:
  mapperLocations: classpath:mapper/*.xml

---
# 开发环境配置
spring:
  profiles: dev
aop:
  multiple:
    datasources:
      default:
        type: com.mysql.jdbc.jdbc2.optional.MysqlXADataSource
        driver-class-name: com.mysql.jdbc.Driver
        url: jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=UTF-8
        username: root
        password: root
      slave:
        type: com.mysql.jdbc.jdbc2.optional.MysqlXADataSource
        driver-class-name: com.mysql.jdbc.Driver
        url: jdbc:mysql://localhost:3306/test_slave?useUnicode=true&characterEncoding=UTF-8
        username: root
        password: root
---
# 测试环境配置
spring:
  profiles: test
aop:
  multiple:
    datasources:
      default:
        type: com.mysql.jdbc.jdbc2.optional.MysqlXADataSource
        driver-class-name: com.mysql.jdbc.Driver
        url: jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=UTF-8
        username: root
        password: root
      slave:
        type: com.mysql.jdbc.jdbc2.optional.MysqlXADataSource
        driver-class-name: com.mysql.jdbc.Driver
        url: jdbc:mysql://localhost:3306/test_slave?useUnicode=true&characterEncoding=UTF-8
        username: root
        password: root

---
# 生产环境配置
spring:
  profiles: prod
aop:
  multiple:
    datasources:
      default:
        type: com.mysql.jdbc.jdbc2.optional.MysqlXADataSource
        driver-class-name: com.mysql.jdbc.Driver
        url: jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=UTF-8
        username: root
        password: root
      slave:
        type: com.mysql.jdbc.jdbc2.optional.MysqlXADataSource
        driver-class-name: com.mysql.jdbc.Driver
        url: jdbc:mysql://localhost:3306/test_slave?useUnicode=true&characterEncoding=UTF-8
        username: root
        password: root
PersonDao接口:
@Repository
@Mapper
public interface PersonDao {
    @Select("SELECT id, first_name AS firstName, last_name AS lastName, birth_date AS birthDate, sex, phone_no AS phoneNo"
            + " FROM t_person WHERE id=#{0};")
    Person getPersonById(int id);
    int insertPerson(Person person);
}
PersonDaoMapper.xml文件:
<insert id="insertPerson">
INSERT INTO t_person(first_name,last_name,birth_date,sex,phone_no,update_dt)
VALUES(#{firstName},#{lastName},#{birthDate},#{sex},#{phoneNo},NOW())
</insert>

Spring框架提供了AbstractRoutingDataSource类来实现数据库动态路由,具体编码如下。
MultipleDataSource.java
public class MultipleDataSource extends AbstractRoutingDataSource {

    @Override
    protected Object determineCurrentLookupKey() {
        return MultipleDataSourceContextHolder.getDataSourceName();
    }

}
MultipleDataSourceContextHolder.java 
private static final ThreadLocal contextHolder = new ThreadLocal(); //每个请求单独一个数据库持有线程
    public static List dataSourceNames = new ArrayList<>(10);

    public static void setDataSourceName(String dataSourceName) {
        contextHolder.set(dataSourceName);
    }

    public static String getDataSourceName() {
        return contextHolder.get();
    }

    public static void resetDataSourceName() {
        contextHolder.remove();
    }

    public static boolean containsDataSource(String dataSourceName) {
        return dataSourceNames.contains(dataSourceName);
    }
MultipleDataSourceConfiguration.java
@Configuration
@ConfigurationProperties(prefix = "aop.multiple") //Spring Boot会自动从yml文件里解析配置
public class MultipleDataSourceConfiguration {
    private Logger logger = LoggerFactory.getLogger(MultipleDataSourceConfiguration.class);
    //数据源配置,从yml文件中加载
    private Map> datasources;
    @Bean
    public DataSource dataSource() {
        Object defaultDataSource = null;
        Map targetDataSources = new HashMap();
        for (String name : datasources.keySet()) {
            try {
                Map config = datasources.get(name);

                AtomikosDataSourceBean datasource = new AtomikosDataSourceBean();
                datasource.setUniqueResourceName(name);
                datasource.setXaDataSourceClassName(config.get("type"));

                Properties properties = new Properties();
                properties.put("URL", config.get("url"));
                properties.put("user", config.get("username"));
                properties.put("password", config.get("password"));
                datasource.setXaProperties(properties);
                if (name.equals("default")) {
                    defaultDataSource = datasource; //默认为主数据库
                } else {
                    targetDataSources.put(name, datasource);
                    MultipleDataSourceContextHolder.dataSourceNames.add(name);
                }
            } catch (Exception e) {
                logger.error("please check datasources config, ex={}", ExceptionUtils.getStackTrace(e));
                System.exit(1);
            }
        }

        MultipleDataSource multipleDataSource = new MultipleDataSource();
        multipleDataSource.setTargetDataSources(targetDataSources);
        multipleDataSource.setDefaultTargetDataSource(defaultDataSource);
        return multipleDataSource;

    }
}
TargetDataSource 
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface TargetDataSource {
    String name();
}
MultipleDataSourceAspect.java
@Aspect
@Component
public class MultipleDataSourceAspect {

    private Logger logger = LoggerFactory.getLogger(MultipleDataSourceAspect.class);

    @Around("@annotation(com.netease.mail.springboot.annotation.TargetDataSource) && @annotation(targetDataSource)")
    public Object processed(ProceedingJoinPoint point, TargetDataSource targetDataSource) throws Throwable {
        try {

            logger.info("method={}, choose={} database", getMethodName(point), targetDataSource.name());

            String dataSourceName = targetDataSource.name();
            if (MultipleDataSourceContextHolder.containsDataSource(dataSourceName)) {
                MultipleDataSourceContextHolder.setDataSourceName(dataSourceName);
            }
            return point.proceed();
        } finally {
            MultipleDataSourceContextHolder.resetDataSourceName();
        }

    }

    private String getMethodName(ProceedingJoinPoint point) throws NoSuchMethodException{
        Signature sig = point.getSignature();
        MethodSignature msig = (MethodSignature) sig;
        Object target = point.getTarget();
        Method currentMethod = target.getClass().getMethod(msig.getName(), msig.getParameterTypes());
        return currentMethod.getName();
    }
通过对数据库操作方法进行注解&AOP拦截实现数据库动态切换。
PersonService.java
@Service
public class PersonService {

    protected final Logger logger = LoggerFactory.getLogger(this.getClass());
    @Autowired
    private PersonDao personDao;

    @Cacheable(value="getPersonById", sync=true)
    @TargetDataSource(name = "slave") //查询请求走Slave
    public Person getPersonById(int id){
	     logger.info("getting data from database, personId={}", id);
             return personDao.getPersonById(id);
	}

    @Transactional //如果不添加注解,则不会被AOP拦截,这时候会默认走主库
    public int insertPerson(Person person){
	    return personDao.insertPerson(person);
    }

}
可以通过单元测试验证数据库结果。
本文来自网易实践者社区,经作者田躲躲授权发布。