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