百变的 spring boot 框架

在接触java开发和spring框架后,发现整个框架相当的灵活,相比于非常老的struts2框架,有太多强大的功能,完全可以根据自己需要和想法,去修改和添加各种功能,所以我将标题起为百变的springBoot框架。

springMvc 和 springBoot

springBoot增加了自动扫描,通过@Component以及其衍生出来的注解(如@Service@RestController等)和@Autowired自动注入注解,实现了自动实例化和自动注入的功能。这大大降低了原本springMvc中xml配置文件的冗余程度。也体现了依赖注入(Dependency Injection)和控制反转(Inversion of Control)的思想。

注:自动扫描功能说明,即指定一个包路径(Package路径),会自动扫描路径下的所有@Component以及其衍生注解,将这些被注解的类实例化,并扫描类下的@Autowired注解,自动匹配并set进去。

两框架实例化和注入的区别如下:

<!-- 声明MyService的bean -->
<bean id="myService" class="com.example.MyService" />
<!-- springBoot框架 仅需要在MyService类上添加@Service注解即可 -->

<!-- 声明MyController的bean -->
<bean id="myController" class="com.example.MyController">
    <!-- 将myService注入到myController -->
    <property name="myService" ref="myService" />
</bean>
<!-- springBoot框架 仅需要在MyController类上添加@Controller注解 并在setMyService方法上添加@Autowired注解即可 -->

在springBoot框架后,xml原本的依赖注入功能被注解完美的替代了。而另外一个配置的功能,也基本可以使用yml文件(.properties文件)替代。不过springBoot并未完全抛弃xml,还为其添加了新的功能。

springBoot框架下的xml配置文件

为何要专门介绍已经“过时”的xml配置文件?在springBoot中,xml与yml两种配置文件完全是可以共存的,各自负责各自的功能,互不干扰。而我个人更喜欢用xml作为配置文件,一是因为当配置内容较多的时候,xml可以非常方便的拆分成多个文件,二是因为xml可以避免生成多个相同bean时的代码重复问题。

在springBoot中,可以使用下面的方法引入xml文件。

@ImportResource("classpath:spring.xml")
@SpringBootApplication
public class App {
	public static void main(String[] args) {
		// ...
	}
}

自动扫描功能
功能和@ComponentScan相同。使用方式:<context:component-scan base-package="要扫描的包名"/>

beans标签和profile配置
可以通过这个来控制不同环境下有哪些xml配置文件生效。

<!-- 测试环境 -->
<beans profile="test">
    <import resource="classpath:test/constant.xml"/>
</beans>
<!-- 正式环境 -->
<beans profile="product">
    <import resource="classpath:product/constant.xml"/>
</beans>

经过研究,xml配置是不可能完全替代yml配置的,在springboot中,有些配置项只能通过yml来配置。
例如,上面beans标签下的profile属性,最终运行时,还需要通过yml指定运行哪一种模式。这一步操作不可能用xml配置文件代替。

spring:
  # 匹配maven的打包环境 @active@ 在打包时会被mave替换为test或product 属于Maven的属性替换功能
  profiles.active: @active@

而yml文件,配合@Configuration@Bean两种注解,是完全可以替代xml的,但是在某些情况下,不太理想。
例如一个项目需要连接多个redis资源的时候,就不得不通过下面的方法来实现,书写大量的重复代码,idea也会提示你代码重复。虽然可以完成对应的功能,但是非常的不完美。

@Configuration
public class PaymentRedisConfig {

    @Bean
    public RedisConnectionFactory masterPaymentFactory(@Value("${spring.redis.master.host}") String host,
                                                       @Value("${spring.redis.master.port}") int port,
                                                       @Value("${spring.redis.master.password:}") String password,
                                                      @Value("${spring.redis.jedis.pool.max-active:20}") int maxActive,
                                                      @Value("${spring.redis.jedis.pool.min-idle:0}") int minIdle
                                                      ) {

        JedisPoolConfig config = new JedisPoolConfig();
        config.setMinIdle(minIdle);
        config.setMaxIdle(maxActive);
        config.setMaxTotal(maxActive);
        JedisConnectionFactory factory = new JedisConnectionFactory();
        factory.setPoolConfig(config);
        if (StringUtils.hasLength(password)) {
            factory.setPassword(password);
            factory.setHostName(host);
            factory.setPort(port);
        }
        return factory;
    }

    @Bean
    public RedisTemplate<String, Object> masterPaymentRedisTemplate( @Qualifier("masterPaymentFactory") RedisConnectionFactory masterMemberFactory){
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<String,Object>();
        redisTemplate.setConnectionFactory(masterMemberFactory);
        GenericJackson2JsonRedisSerializer jackson2JsonRedisSerializer = new GenericJackson2JsonRedisSerializer();
        redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
        RedisSerializer<String> redisSerializer = new StringRedisSerializer();
        redisTemplate.setKeySerializer(redisSerializer);
        redisTemplate.setHashKeySerializer(redisSerializer);
        return redisTemplate;
    }

    @Bean
    public RedisConnectionFactory slavePaymentFactory(@Value("${spring.redis.slave.host}") String host,
                                                     @Value("${spring.redis.slave.port}") int port,
                                                     @Value("${spring.redis.slave.password:}") String password,
                                                     @Value("${spring.redis.jedis.pool.max-active:20}") int maxActive,
                                                     @Value("${spring.redis.jedis.pool.min-idle:0}") int minIdle
    ) {
        JedisPoolConfig config = new JedisPoolConfig();
        config.setMinIdle(minIdle);
        config.setMaxIdle(maxActive);
        config.setMaxTotal(maxActive);
        JedisConnectionFactory factory = new JedisConnectionFactory();
        factory.setPoolConfig(config);
        if (StringUtils.hasLength(password)) {
            factory.setPassword(password);
            factory.setHostName(host);
            factory.setPort(port);
        }
        return factory;
    }

    @Bean
    public RedisTemplate<String, Object> slavePaymentRedisTemplate( @Qualifier("slavePaymentFactory") RedisConnectionFactory slaveMemberFactory){
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<String,Object>();
        redisTemplate.setConnectionFactory(slaveMemberFactory);
        GenericJackson2JsonRedisSerializer jackson2JsonRedisSerializer = new GenericJackson2JsonRedisSerializer();
        redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
        RedisSerializer<String> redisSerializer = new StringRedisSerializer();
        redisTemplate.setKeySerializer(redisSerializer);
        redisTemplate.setHashKeySerializer(redisSerializer);
        return redisTemplate;
    }
}

若是使用xml来配置多个redis的连接,一切都会好很多:

<!-- redis 配置信息 -->
<bean id="jedisConfig" class="redis.clients.jedis.JedisPoolConfig">
    <property name="testWhileIdle" value="true"/>
    <property name="testOnBorrow" value="true"/>
    <property name="maxTotal" value="20"/>
    <property name="maxIdle" value="10"/>
    <property name="minIdle" value="5"/>
</bean>

<bean id="jedisPool1" class="redis.clients.jedis.JedisPool"
      destroy-method="destroy">
    <constructor-arg ref="jedisConfig"/>
    <constructor-arg value="ip地址"/>
    <constructor-arg value="端口号"/>
    <constructor-arg value="超时时间"/>
    <constructor-arg value="密码"/>
</bean>

<bean id="jedisPool2" class="redis.clients.jedis.JedisPool"
      destroy-method="destroy">
    <constructor-arg ref="jedisConfig"/>
    <constructor-arg value="ip地址"/>
    <constructor-arg value="端口号"/>
    <constructor-arg value="超时时间"/>
    <constructor-arg value="密码"/>
</bean>

yml配置与xml配置在本质上的区别

在springBoot中,有着一个非常重要的对象,其功能类似控制反转思想中的IOC容器,就是ApplicationContext对象。我们所有的声明的bean,不管是xml中声明的,还是通过@Component注解声明的,实例化之后都会存入ApplicationContext对象。而这个对象在我们启动springBoot时就会生成。

ApplicationContext context = SpringApplication.run(App.class,args); // 初始化springBoot框架并获取ApplicationContext

通过xml配置实例化的类,可以根据类的类型或bean的id,从ApplicationContext中获取,例如:

ApplicationContext context = SpringApplication.run(App.class,args);
Bean1 bean1 = context.getBean(Bean1.class); // 根据类的类型从spring容器中获取实例化后的Bean1对象

而yml配置文件,本身不会实例化任何的类。它本身仅表示了配置属性和对应的值。我们可以从下面的代码查看其与xml的区别:

ApplicationContext context = SpringApplication.run(App.class,args);
Environment environment = context.getBean(Environment.class);
String msg = environment..getProperty("abc.ef"); // 获取yml配置文件中属性abc.ef的值。

可以看出,在初始化springBoot框架时,会读取yml中的所有配置,放入Environment对象,然后将Environment放入ApplicationContext中。

百变的springBoot 自定义数据库的连接配置

springBoot框架自带了很多的功能,包括连接Mysql,mongoDb,Redis等,在仅连接一个资源的时候使用着非常方便。但是也有着各种各样的问题。就比如springBoot自带的RedisTemplate用起来非常的不方便,想使用Jedis对象来操作redis,或者是公司有专门的数据库配置中心,想每次启动时请求配置中心获取对应Redis机器的节点地址,然后再进行连接。面对这些复杂情况,springBoot自带的连接配置功能显然不够用,这时我们就需要自定义连接逻辑。
不管你要自定义哪个数据库连接,第一步就是关闭Redis自己的针对每个数据库的自动配置功能,因为可能引起一些干扰。关闭方式如下:

spring:
  # 根据需要禁用部分自动配置
  autoconfigure.exclude:
    - org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration # Mysql的
    - org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration # mongo的
    - org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration # Redis的

接下来就可以自定义自己的数据库配置了。

public class RedisClusterBuilder {

    private String clusterUrl; // 数据库配置中心url、
    private int appId; // Redis集群appId
    private int connectTimeout;
    private int soTimeout;
    private int maxRedirections;
    private JedisPoolConfig jedisPoolConfig;
    private JedisCluster jedisCluster;

    public void init() {
        Set<HostAndPort> nodes = requestShardInfo();
        jedisCluster = new JedisCluster(nodes, connectTimeout, soTimeout, maxRedirections, jedisPoolConfig);
    }

    public JedisCluster get() {
        return jedisCluster;
    }

    private Set<HostAndPort> requestShardInfo() {
        // 请求数据库配置中心 略
        return null;
    }

    public void setClusterUrl(String clusterUrl) {
        this.clusterUrl = clusterUrl;
    }

    public void setAppId(int appId) {
        this.appId = appId;
    }

    public void setConnectTimeout(int connectTimeout) {
        this.connectTimeout = connectTimeout;
    }

    public void setSoTimeout(int soTimeout) {
        this.soTimeout = soTimeout;
    }

    public void setMaxRedirections(int maxRedirections) {
        this.maxRedirections = maxRedirections;
    }

    public void setJedisPoolConfig(JedisPoolConfig jedisPoolConfig) {
        this.jedisPoolConfig = jedisPoolConfig;
    }
}

然后就可以在xml中实例化该类(也可以通过yml配置文件配合@Configuration@Bean两种注解实例化该类),在使用的时候就可以通过自动注入获取该类,案例如下:

@Service
public class AnService {

    private JedisCluster redis;

    public void clearKey(String key) {
        redis.del(key);
    }

    @Autowired
    public void setRedis(RedisClusterBuilder redisBuilder) {
        this.redis = redisBuilder.get();
    }
}

百变的springBoot 不带有http请求和响应功能的精简框架

springBoot框架是用来做网站后台的,着简直就是一种深入人心的常识,但是它还有很多其他的功能。
某天需要开发一个脚本,定时执行一些操作。直接用java从头开发实在是太麻烦。而springBoot带有很多非常方便的功能,尤其是他的自动实例化和注入功能。如何去除其http功能的情况下启动springBoot?一开始我的方案是这样的:

<!-- maven中仅引入spring的IOC容器 也就是ApplicationContext对象中带有的自动扫描注入功能 -->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>5.3.20</version>
</dependency>
public class App {

    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("classpath:application.xml");
        Work work = context.getBean(Work.class);
        Work.run(); // 一次性脚本 运行结束后自动退出程序
    }
}

不过这种方式有个缺点。就是无法读取yml配置文件,也就意味着某些只能依靠yml配置的功能均无法使用。
经过研究,发现springBoot有专门提供不带有http的框架。其与一般使用的web框架依赖引入的方式区别如下:

<!-- 一般web项目使用的springBoot依赖 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 不带有tomcat容器和http请求响应功能的springBoot依赖 -->
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter</artifactId>
</dependency>

初始化框架的代码:

@ImportResource("classpath:spring.xml")
@SpringBootApplication
public class App {

    public static ApplicationContext context;

    public static void main(String[] args) throws InterruptedException {
        CountDownLatch countDown = new CountDownLatch(1);
        Runtime.getRuntime().addShutdownHook(new Thread(countDown::countDown));
        LOG.info("[init applications]");
        context = SpringApplication.run(App.class, args);
        LOG.info("[start run]");
        countDown.await(); // 为定时任务脚本 这里卡死主线程 防止程序自动退出
        LOG.info("[stop run]");
    }
}

在此基础上,就可以使用@Scheduled注解配置各种定时任务。

百变的springBoot 请求响应日志

有时,我们需要打印服务所有接口的请求响应日志,目前总共有两种实现方式,个有优缺点。

通过过滤器实现

由于http请求流和响应流只能读取一次,所以必须进行特殊处理。这里使用ContentCachingRequestWrapperContentCachingResponseWrapper对原有的请求和响应对象进行包裹,就可以实现在读取内容的同时,不影响数据流的二次读取。不过也是有缺点的,其本质是在本地缓存了请求和响应的数据,若数据量特别大,尤其是响应内容是一个文件的这种情况,会造成大量的内存开销。

@Override
public void doFilter(ServletRequest servletRequest,
                     ServletResponse servletResponse,
                     FilterChain filterChain)
        throws IOException, ServletException {
    HttpServletRequest request = (HttpServletRequest) servletRequest;
    HttpServletResponse response = (HttpServletResponse) servletResponse;
    String uri = request.getRequestURI();
    // 自动缓存请求和响应的数据
    ContentCachingRequestWrapper requestWrapper = new ContentCachingRequestWrapper(request);
    ContentCachingResponseWrapper responseWrapper = new ContentCachingResponseWrapper(response);
    long time = System.currentTimeMillis();
    filterChain.doFilter(requestWrapper, responseWrapper);
    // 获取缓存的请求数据和响应数据
    LOG.info("uri: {}, request: {}, response: {}, cost time: {}",
            uri,
            new String(requestWrapper.getContentAsByteArray(), StandardCharsets.UTF_8),
            new String(responseWrapper.getContentAsByteArray(), StandardCharsets.UTF_8),
            System.currentTimeMillis() - time);
    // 将响应缓存写入响应数据流,写入后无法再获取响应的缓存内容
    responseWrapper.copyBodyToResponse();
}

通过切面注解实现

相比于过滤器,使用切面注解的静态日志打印位置是在调用Controller方法的前后。其优点就是不需要处理请求和响应流,可以更方便的控制哪些接口需要打印日志,哪些不需要(若需要打印日志,只需要加一个注解即可)。缺点就是无法获取到请求body数据的全貌,因为到达Controller时,请求流已经被springBoot框架读取并解析过了。其实现方式如下:
切面注解类,若哪个方法需要打印日志,在方法前添加该注解即可。

@Target({ElementType.PARAMETER, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface SysLog {
    String title()  default "";
}

注解功能实现

@Aspect
@Component
public class SysLogAspect {
    /**
     * @return java.lang.Object
     * @description 空
     * @author weizhichao
     * @date 2018/3/27 18:46
     * @params [pjp, cLog]
     */
    @Around(value = "@annotation(loginfo)")
    public Object saveLog(final ProceedingJoinPoint pjp, SysLog loginfo) throws Throwable {
        long start = System.currentTimeMillis();
        Object result = pjp.proceed();
        try {
            String time = (System.currentTimeMillis() - start)+"";
            LOG.info(("title: " + loginfo.title() + "req: " + JSONObject.toJSONString(pjp.getArgs())
            		+ "rsp: " + JSONObject.toJSONString(result) + "time: " + time));
            return result;
        } catch (Exception e) {
        	LOG.error("SysLogAspect error", e);
        }
        return result;
    }
}

百变的springBoot 自定义请求参数处理方式

某些情况下,我们想用自己的逻辑解析处理请求中的数据。
例如,自定义新的请求参数格式,其格式如下,多个参数通过逗号分隔,并非json数据,这意味着springBoot无法自动解析并将其自动转换对应的java对象。

a=12,b=13

而我,想在Controller中像这样,方便快捷的接收数据。

@RestController
public class OneController {
	@PostMapping("/info")
    public Objeck info(Ab request) {
    	// ...
    }
	// AB对象
	public static class Ab { public int a,b; }
}

这时,就可以使用过滤器加@RequestAttribute注解的组合来实现这一功能。在springBoot处理请求参数前,先读取请求参数,解析并处理,最后透传到Controller下的每一个方法中。
考虑到每个方法可能接收到的对象不同,而一个个的标明接收对象又过于麻烦。不过好在,在spring框架初始化的时候,会扫描Controller下的每一个@RequestMapping及其衍生的注解,并将这些请求方法放入IOC容器中。所以,就有了获取spring服务中,所有http请求的方法:

// 拿到Handler适配器中的全部方法 App.context为spring的ioc容器 ApplicationContext对象
RequestMappingHandlerMapping mapping = App.context.getBean(RequestMappingHandlerMapping.class);
Map<RequestMappingInfo, HandlerMethod> methodMap = mapping.getHandlerMethods();
for (Map.Entry<RequestMappingInfo, HandlerMethod> entry : methodMap.entrySet()) {
	Set<PathPattern> pathSet = entry.getKey().getPathPatternsCondition().getPatterns();
	// 获取请求对应的uri
	Set<String> uriSet = new HashSet<>();
    for (PathPattern path : pathSet) {
	    uriSet.add(path.getPatternString());
	}
	// 获取请求方法要求的请求参数类
	MethodParameter[] parameters = entry.getValue().getMethodParameters();
	Class<?> clazz = parameters.length == 0 ? null : parameters[0].getParameter().getType();
}

之后,我们就可以在过滤器中创建逻辑,自动解析请求数据。过滤器极不建议通过注解@WebFilter来实现,因为这样无法调整请求经过过滤器的顺序,虽然网上有说可以通过@Order注解来调整,但是通过实测,该注解完全无用。
一种办法是可以通过@Bean注解手动实例化FilterRegistrationBean,并制定执行顺序,但是还是老问题,在有多个过滤器的情况下,这会造成大量的代码重复,所以我建议通过xml配置文件来实例化过滤器。

<!-- 过滤器 日志 -->
<bean class="org.springframework.boot.web.servlet.FilterRegistrationBean">
    <property name="order" value="1"/>
    <property name="filter" ref="logFilter"/>
    <property name="urlPatterns" value="/*"/>
</bean>
<!-- 过滤器 校验 -->
<bean class="org.springframework.boot.web.servlet.FilterRegistrationBean">
    <property name="order" value="2"/>
    <property name="filter" ref="signFilter"/>
    <property name="urlPatterns" value="/*"/>
</bean>

对应的java类,这里以日志过滤器(logFilter)举例

@Component
public class LogFilter implements Filter {
    @Override
    public void doFilter(ServletRequest servletRequest,
                         ServletResponse servletResponse,
                         FilterChain filterChain) throws IOException, ServletException {
		// 过滤器相关逻辑
	}
}

然后,我们就可以在过滤器中添加下面的代码,从而实现自定义的请求参数处理

HttpServletRequest request = (HttpServletRequest) servletRequest;
// 获取uri
String uri = request.getRequestURI();
// 获取请求数据
String body;
int size = request.getContentLength();
if (size == -1 || size == 0) {
    body = null;
} else {
    byte[] bytes = new byte[request.getContentLength()];
    int ignore = request.getInputStream().read(bytes);
    body = new String(bytes, StandardCharsets.UTF_8);
}
// 转换为uri下对应的java对象
Class<?> clazz = getJavaTypeByUri(uri) // 方法实现方式上面有讲
Object obj = parseBody(body, clazz); // 自定义的请求参数解析逻辑
// 通过Attribute透传到Controller
request.setAttribute("body", obj);

最后,我们就可以在Controller中很方便的拿到请求参数

@RestController
public class OneController {
	@PostMapping("/info")
    public Objeck info(@RequestAttribute("body") Ab request) {
    	// ...
    }
	// AB对象
	public static class Ab { public int a,b; }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值