Spring Boot重启后服务第一次访问时间慢的一次调优记录

背景今天和分子公司合并服务接口(降低成本),对方反应我这边有个服务慢,搞了一天,就顺便记录下服务调优1. 网络由于生产机和测试机在机房处于不同网段,网络环境质量有差异,最开始怀疑的是网络导致的。分别在几个环境中跑相同代码,发现是网络影响的调用三方服务返回时间波动。2.调优基于业务需求,更改调用三方服务方法为异步调用。嗯!应该没问题了。3.验证进行优化验证,发现调用平均时长有明显降低(废话)。但是,但可是,发现了新问题,在spring boot启动后第一次调用本服务,耗时仍旧远远高于后续调用,正常在20ms/次,第一次平均在600ms/次,于是开始google于是看到了这个提问https://segmentfault.com/q/10…修改项目在查看Dockerfile后,发现启动脚本中有加如下参数JAVA_ALL_OPTS=" -Djava.security.egd=file:/dev/./urandom “继而想修改docker基础镜像中jre的java.security文件遂在Dockerfile中增加如下shellsed -i “117csecurerandom.source=file:/dev/./urandom” /usr/lib/jvm/java-8-openjdk-amd64/jre/lib/security/java.security就是用shell 替换了文本的内容结论其实,也没有明显的效率提升,服务首次加载还是比之后慢。所以考虑,是不是文件是不是没有改全(待完成,还没验证)最后,通过验证发现一个规律,假设有A B两个服务,在Spring Boot 启动后,如果先首次访问A,那么B的首次访问时间会缩短,但是还是会高于第二次及以后的访问时间如果先首次访问B,那么A的首次访问时间会缩短,但是还是会高于第二次及以后的访问时间因此,在Spring boot启动后,第一个被访问的服务耗时一定大于第二个被访问的服务,且每个服务之后的访问时间一定小于本服务第一次被访问的时间。暂时就这么多,这是个记录。之后会对基础镜像中jdk里面的java.security进行修改,如果有效果 会再更新。刚才又找了一下,发现jdk目录里没有java.security,是我秀逗了

April 10, 2019 · 1 min · jiezi

Tomcat 上的项目参数传递问题

修改 Tomcat 中的 catalina.sh 文件部署在 Tomcat 上面的 Spring Boot 项目,在某些情况下,我们可能会修改配置文件中的参数,这样应该怎么做呢?传统的方式是直接在本地修改,然后打包部署,但是这种方式太麻烦了,要是我只是修改了很小的一个参数,都要重新打包,得不偿失。于是稍微研究了一下,找到了两种方式,来向项目传递参数。第一种方式就是修改 Tomcat 中的 catalina.sh 这个文件,例如我在 Spring Boot 项目的配置文件中自定义了一个配置,如下:#application.yml 中的配置project: args: ${info}然后修改 Tomcat 下面的 bin/catalina.sh 文件,添加一个 JAVA_OPTS 属性,指定 info 的值:需要注意的是,如果指定的是一个包含了空格的字符串,要用单引号包围,例如上面的 -Dinfo="‘I am roseduan’" 。这种方式出现的问题:但是,这种传递参数的方式是不太方便的,举个例子:在本地环境,启动项目的时候,该怎么去设置这个 info 的值呢?如果不指定肯定是要报错的。只不过我也找到了一种方式,在 IntelliJ IDEA 中,我们可以在 Run/Debug Configuration 中设置参数:这样就能够在本地启动启动项目了。但是还存在一个问题,就是使用 maven 打包的时候,也会报错,仍然是找不到 info 的值,这时候我们也可以使用mvn 打包时来传递参数,命令是:mvn package -Dinfo=“I am roseduan” 。部署到 tomcat 中后,我们就可以使用修改 catalina.sh 中的内容来指定 info 的值。2. 修改 Tomcat 中的 contxt.xml 文件上面的这种方式,其实应该少量的配置是可以的,但是如果我们需要指定大量的配置,并且每个配置的内容都很长,这样就不是非常方便了,因为每次打包,都需要写很多参数。所以第二种方式,修改 context.xml 文件,就十分的有优势了。我们不需要修改任务本地的配置,也不用配置任何参数,还是上面那个例子,假如 application.yml 中有如下配置:#application.yml 中的配置project: args: I am roseduan并且这个配置是已经打包在了 Tomcat 上了,我们可以在 Tomcat 中的 conf/context.xml 中添加一些配置来修改这个参数的值:name 是配置参数在 application.yml 文件中的路径,Type 对应的是 Java 类型,value 是具体的值。这样的话,我们可以直接在这个文件中指定很多的值了,这样修改还有一个好处便是,不用重启 Tomcat ,配置即时生效 (只不过需要多试几次,或者稍微等一会)。最后,需要注意一点:配置在 application.yml 中的自定义配置,最好是小写,或者使用 - 分隔,不然有可能 context.xml 中的配置不会生效。

April 10, 2019 · 1 min · jiezi

动态控制 Spring Boot 中的 @Scheduled 定时任务

概论Spring Boot 中的 @Scheduled 注解为定时任务提供了一种很简单的实现,只需要在注解中加上一些属性,例如 fixedRate、fixedDelay、cron(最常用)等等,并且在启动类上面加上 @EnableScheduling 注解,就可以启动一个定时任务了。但是在某些情况下,并没有这么简单,例如项目部署上线之后,我们可能会修改定时任务的执行时间,并且停止、重启定时任务等,因为定时任务是直接写死在程序中的,修改起来不是非常的方便。所以,简单记录一下自己的一些解决方案,仅供参考。2. 在配置文件中设置参数以 cron 表达式为例,一般的做法是将 @Scheduled 的属性写在程序中的,例如这样:@Componentpublic class TestTask { private static SimpleDateFormat dateFmt = new SimpleDateFormat(“yyyy-MM-dd HH:mm:ss”); @Scheduled(cron = “0/5 * * * * ?”) public void test(){ System.out.println(dateFmt.format(new Date()) + " : 执行定时任务"); }}如果需要修改的话,我们可以将 cron 表达式配在 application.yml 中:#application.yml中的配置scheduled: cron: 0/5 * * * * ?然后在 @Scheduled 中获取这个配置:@Scheduled(cron = “${scheduled.cron}")public void test(){ System.out.println(dateFmt.format(new Date()) + " : 执行定时任务”);}等到了线上的时候,再通过修改配置文件中的内容来进行控制。具体怎么动态的修改配置文件中的内容,后面我会专门写一篇文章来说明。3. 如何关闭定时任务一种方式是根据实际的需求,设置一个很久之后的时间再执行,例如明年的某个时间点,你可能会想何不设置一个已经过去的时间(例如 2012 年),但是很遗憾,@Scheduled 并不支持设置年份。另外 Spring Boot 2.1 以上的版本还提供了一种停止定时任务的方案,就是在 cron 中配置 “-” 即可,你也可以在配置文件中设置这个符号:#application.yml中的配置scheduled: cron: “-“注意这里必须加上一个双引号,因为在 application.yml 中, - 是一个特殊的字符。4. 为定时任务设置开关如果嫌上面这种方式比较死板,可以尝试另一种,给定时任务加上开关的方案,在配置文件中配置一个 boolean 属性,如果是 true 的话,就开启定时任务,否则不开启。#application.yml中的配置scheduled: cron: 0/5 * * * * ?enable: scheduled: true然后我们可以使前面说到的 @Conditional 注解来实现这个功能,如果你还不了解,可以看我这篇文章:浅谈 Spring Boot 中的 @Conditional 注解其实 @Scheduled 注解,是被一个叫做 ScheduledAnnotationBeanPostProcessor 的类所拦截的,所以我们可以根据配置,决定是否创建这个 bean,如果没有这个 bean,@Scheduled 就不会被拦截,那么定时任务肯定不会执行了,有了这个思路,实现起来就很简单了。需要注意的是:这种方式,启动类上面的 @EnableScheduling 需要去掉。然后创建一个 ScheduledCondtion 类,内容如下:public class ScheduledCondition implements Condition { @Override public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) { //读取配置中的属性 return Boolean.valueOf(context.getEnvironment().getProperty(“enable.scheduled”)); }}这个类的功能很简单,就是去读取配置,然后返回一个 boolean 值。然后创建一个配置类 ScheduledConfig ,内容如下:@Configurationpublic class ScheduledConfig { @Conditional(ScheduledCondition.class) @Bean public ScheduledAnnotationBeanPostProcessor processor() { return new ScheduledAnnotationBeanPostProcessor(); }}这个配置,就是以 ScheduledCondtion 为条件,决定是否创建 bean。然后,启动项目,定时任务就会执行,如果我们将配置修改为 false,则不会执行。这样的话,我们就能够很容易的启动或者关闭定时任务了,并且也可以实时修改 cron 表达式的值。

April 9, 2019 · 1 min · jiezi

Spring Boot 2.2 增加了一个新功能,启动飞起~

前几天栈长分享了一个好玩的框架:一个比Spring Boot快44倍的Java框架!,是不是感觉 Spring Boot 略慢?今天讲一下 Spring Boot 添加的这个新特性,可以大大提升 Spring Boot 的启动速度。最近,Spring团队宣布在 Spring Boot 2.2+ 中添加了一个重要功能:延迟加载,目前这个版本暂时还是快照版,不过我们可以先了解下怎么使用这个延迟加载功能。延迟加载是什么意思?有点经验的程序员应该都知道,在 Spring 框架中早已经支持延迟加载功能的,简单来说就是一个类的实例化,不需要 Spring 容器启动的时候就开始实例化,而是在第一次需要它的时候再实例化,这样大大提升了程序启动速度,也在一定程序上节省了系统资源。怎么开启延迟加载?在传统 Spring 项目中我们是这么做的:<bean id=“testBean” calss=“cn.javastack.TestBean” lazy-init=“true” />以上 bean 配置是不是很熟悉?没错,加了 lazy-init=“true” 表示延迟加载,默认不加为false,表示容器启动时立即加载。在 Spring 3.0+ 之后也可以这么做:@Lazypublic TestBean testBean() { return new TestBean();}@Lazy:默认值为true,表示延迟加载;Spring Boot如何开启?由上面的例子我们可以知道,在任何 Spring Boot 版本中其实是支持 Bean 的延迟加载的,但这样是需要我们手工去配置的,这样会比较麻烦。在 Spring Boot 2.2+ 中,延期加载将变得更加简单,有几下几种配置方式:参数:spring.main.lazy-initialization类:SpringApplication类:SpringApplicationBuilder通过以上几种方式设置成:true,容器中的 Bean 就将配置成延迟加载。Spring Boot 项目在 IDE 中再配合 DevTools 工具,可以使本发开发环境启动变得更快,400ms就可以启动起来了,大大提高了开发效率。延迟加载有没有缺点?延迟加载确实可以大大减少应用程序的启动时间,还能节省系统资源,那么问题来了,你可能会问,为什么不默认开启它呢?为什么还要额外提供一个配置?听栈长道来,延迟加载确实有很多好处,但也会造成一些在启动的时候就能发现而要等到延迟加载才发现的问题,如:内存不足啊、类找不到啊、又或者是配置错误引发的系列问题。还有一个问题就是,因为第一次请求的时候才去实例化,可能造成第一个请求变慢,响应延迟,体验不是很好。这样一来,对负载均衡和自动伸缩方面也会有不利影响。结束语正如我们在上面所分析到的,延迟加载确实可以显着改善启动时间,但也有一些明显的缺点,所以我们一定小心谨慎的启用它。或者我们可以对项目进行评估下,延迟加载真的对我们的项目有这么重要或者急迫么?等正式版 Spring Boot 2.2 发布,栈长给再出一个实战文章,欢迎关注栈长的微信公众号:Java技术栈,不要走开。好了,今天的分享就到这里,关注Java技术栈微信公众号,在后台回复:boot,获取栈长整理的更多的 Spring Boot 教程,都是实战干货,以下仅为部分预览。Spring Boot 读取配置的几种方式Spring Boot 如何做参数校验?Spring Boot 最核心的 25 个注解!Spring Boot 2.x 启动全过程源码分析Spring Boot 2.x 新特性总结及迁移指南……最后,你们是怎么应用延迟加载功能的,欢迎留言分享~本文原创首发于微信公众号:Java技术栈(id:javastack),关注公众号在后台回复 “boot” 可获取更多 Spring Boot 教程,转载请原样保留本信息。 ...

April 9, 2019 · 1 min · jiezi

干货|一个案例学会Spring Security 中使用 JWT

在前后端分离的项目中,登录策略也有不少,不过 JWT 算是目前比较流行的一种解决方案了,本文就和大家来分享一下如何将 Spring Security 和 JWT 结合在一起使用,进而实现前后端分离时的登录解决方案。1 无状态登录1.1 什么是有状态?有状态服务,即服务端需要记录每次会话的客户端信息,从而识别客户端身份,根据用户身份进行请求的处理,典型的设计如Tomcat中的Session。例如登录:用户登录后,我们把用户的信息保存在服务端session中,并且给用户一个cookie值,记录对应的session,然后下次请求,用户携带cookie值来(这一步有浏览器自动完成),我们就能识别到对应session,从而找到用户的信息。这种方式目前来看最方便,但是也有一些缺陷,如下:服务端保存大量数据,增加服务端压力服务端保存用户状态,不支持集群化部署1.2 什么是无状态微服务集群中的每个服务,对外提供的都使用RESTful风格的接口。而RESTful风格的一个最重要的规范就是:服务的无状态性,即:服务端不保存任何客户端请求者信息客户端的每次请求必须具备自描述信息,通过这些信息识别客户端身份那么这种无状态性有哪些好处呢?客户端请求不依赖服务端的信息,多次请求不需要必须访问到同一台服务器服务端的集群和状态对客户端透明服务端可以任意的迁移和伸缩(可以方便的进行集群化部署)减小服务端存储压力1.3.如何实现无状态无状态登录的流程:首先客户端发送账户名/密码到服务端进行认证认证通过后,服务端将用户信息加密并且编码成一个token,返回给客户端以后客户端每次发送请求,都需要携带认证的token服务端对客户端发送来的token进行解密,判断是否有效,并且获取用户登录信息1.4 JWT1.4.1 简介JWT,全称是Json Web Token, 是一种JSON风格的轻量级的授权和身份认证规范,可实现无状态、分布式的Web应用授权: JWT 作为一种规范,并没有和某一种语言绑定在一起,常用的Java 实现是GitHub 上的开源项目 jjwt,地址如下:https://github.com/jwtk/jjwt1.4.2 JWT数据格式JWT包含三部分数据:Header:头部,通常头部有两部分信息:声明类型,这里是JWT加密算法,自定义我们会对头部进行Base64Url编码(可解码),得到第一部分数据。Payload:载荷,就是有效数据,在官方文档中(RFC7519),这里给了7个示例信息:iss (issuer):表示签发人exp (expiration time):表示token过期时间sub (subject):主题aud (audience):受众nbf (Not Before):生效时间iat (Issued At):签发时间jti (JWT ID):编号这部分也会采用Base64Url编码,得到第二部分数据。Signature:签名,是整个数据的认证信息。一般根据前两步的数据,再加上服务的的密钥secret(密钥保存在服务端,不能泄露给客户端),通过Header中配置的加密算法生成。用于验证整个数据完整和可靠性。生成的数据格式如下图: 注意,这里的数据通过 . 隔开成了三部分,分别对应前面提到的三部分,另外,这里数据是不换行的,图片换行只是为了展示方便而已。1.4.3 JWT交互流程流程图:步骤翻译:应用程序或客户端向授权服务器请求授权获取到授权后,授权服务器会向应用程序返回访问令牌应用程序使用访问令牌来访问受保护资源(如API)因为JWT签发的token中已经包含了用户的身份信息,并且每次请求都会携带,这样服务的就无需保存用户信息,甚至无需去数据库查询,这样就完全符合了RESTful的无状态规范。1.5 JWT 存在的问题说了这么多,JWT 也不是天衣无缝,由客户端维护登录状态带来的一些问题在这里依然存在,举例如下:续签问题,这是被很多人诟病的问题之一,传统的cookie+session的方案天然的支持续签,但是jwt由于服务端不保存用户状态,因此很难完美解决续签问题,如果引入redis,虽然可以解决问题,但是jwt也变得不伦不类了。注销问题,由于服务端不再保存用户信息,所以一般可以通过修改secret来实现注销,服务端secret修改后,已经颁发的未过期的token就会认证失败,进而实现注销,不过毕竟没有传统的注销方便。密码重置,密码重置后,原本的token依然可以访问系统,这时候也需要强制修改secret。基于第2点和第3点,一般建议不同用户取不同secret。2 实战说了这么久,接下来我们就来看看这个东西到底要怎么用?2.1 环境搭建首先我们来创建一个Spring Boot项目,创建时需要添加Spring Security依赖,创建完成后,添加 jjwt 依赖,完整的pom.xml文件如下:<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId></dependency><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId></dependency><dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.1</version></dependency>然后在项目中创建一个简单的 User 对象实现 UserDetails 接口,如下:public class User implements UserDetails { private String username; private String password; private List<GrantedAuthority> authorities; public String getUsername() { return username; } @Override public boolean isAccountNonExpired() { return true; } @Override public boolean isAccountNonLocked() { return true; } @Override public boolean isCredentialsNonExpired() { return true; } @Override public boolean isEnabled() { return true; } //省略getter/setter}这个就是我们的用户对象,先放着备用,再创建一个HelloController,内容如下:@RestControllerpublic class HelloController { @GetMapping("/hello") public String hello() { return “hello jwt !”; } @GetMapping("/admin") public String admin() { return “hello admin !”; }}HelloController 很简单,这里有两个接口,设计是 /hello 接口可以被具有 user 角色的用户访问,而 /admin 接口则可以被具有 admin 角色的用户访问。2.2 JWT 过滤器配置接下来提供两个和 JWT 相关的过滤器配置:一个是用户登录的过滤器,在用户的登录的过滤器中校验用户是否登录成功,如果登录成功,则生成一个token返回给客户端,登录失败则给前端一个登录失败的提示。第二个过滤器则是当其他请求发送来,校验token的过滤器,如果校验成功,就让请求继续执行。这两个过滤器,我们分别来看,先看第一个:public class JwtLoginFilter extends AbstractAuthenticationProcessingFilter { protected JwtLoginFilter(String defaultFilterProcessesUrl, AuthenticationManager authenticationManager) { super(new AntPathRequestMatcher(defaultFilterProcessesUrl)); setAuthenticationManager(authenticationManager); } @Override public Authentication attemptAuthentication(HttpServletRequest req, HttpServletResponse resp) throws AuthenticationException, IOException, ServletException { User user = new ObjectMapper().readValue(req.getInputStream(), User.class); return getAuthenticationManager().authenticate(new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword())); } @Override protected void successfulAuthentication(HttpServletRequest req, HttpServletResponse resp, FilterChain chain, Authentication authResult) throws IOException, ServletException { Collection<? extends GrantedAuthority> authorities = authResult.getAuthorities(); StringBuffer as = new StringBuffer(); for (GrantedAuthority authority : authorities) { as.append(authority.getAuthority()) .append(","); } String jwt = Jwts.builder() .claim(“authorities”, as)//配置用户角色 .setSubject(authResult.getName()) .setExpiration(new Date(System.currentTimeMillis() + 10 * 60 * 1000)) .signWith(SignatureAlgorithm.HS512,“sang@123”) .compact(); resp.setContentType(“application/json;charset=utf-8”); PrintWriter out = resp.getWriter(); out.write(new ObjectMapper().writeValueAsString(jwt)); out.flush(); out.close(); } protected void unsuccessfulAuthentication(HttpServletRequest req, HttpServletResponse resp, AuthenticationException failed) throws IOException, ServletException { resp.setContentType(“application/json;charset=utf-8”); PrintWriter out = resp.getWriter(); out.write(“登录失败!”); out.flush(); out.close(); }}关于这个类,我说如下几点:自定义 JwtLoginFilter 继承自 AbstractAuthenticationProcessingFilter,并实现其中的三个默认方法。attemptAuthentication方法中,我们从登录参数中提取出用户名密码,然后调用AuthenticationManager.authenticate()方法去进行自动校验。第二步如果校验成功,就会来到successfulAuthentication回调中,在successfulAuthentication方法中,将用户角色遍历然后用一个 , 连接起来,然后再利用Jwts去生成token,按照代码的顺序,生成过程一共配置了四个参数,分别是用户角色、主题、过期时间以及加密算法和密钥,然后将生成的token写出到客户端。第二步如果校验失败就会来到unsuccessfulAuthentication方法中,在这个方法中返回一个错误提示给客户端即可。再来看第二个token校验的过滤器:public class JwtFilter extends GenericFilterBean { @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { HttpServletRequest req = (HttpServletRequest) servletRequest; String jwtToken = req.getHeader(“authorization”); System.out.println(jwtToken); Claims claims = Jwts.parser().setSigningKey(“sang@123”).parseClaimsJws(jwtToken.replace(“Bearer”,"")) .getBody(); String username = claims.getSubject();//获取当前登录用户名 List<GrantedAuthority> authorities = AuthorityUtils.commaSeparatedStringToAuthorityList((String) claims.get(“authorities”)); UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(username, null, authorities); SecurityContextHolder.getContext().setAuthentication(token); filterChain.doFilter(req,servletResponse); }}关于这个过滤器,我说如下几点:首先从请求头中提取出 authorization 字段,这个字段对应的value就是用户的token。将提取出来的token字符串转换为一个Claims对象,再从Claims对象中提取出当前用户名和用户角色,创建一个UsernamePasswordAuthenticationToken放到当前的Context中,然后执行过滤链使请求继续执行下去。如此之后,两个和JWT相关的过滤器就算配置好了。2.3 Spring Security 配置接下来我们来配置 Spring Security,如下:@Configurationpublic class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Bean PasswordEncoder passwordEncoder() { return NoOpPasswordEncoder.getInstance(); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.inMemoryAuthentication().withUser(“admin”) .password(“123”).roles(“admin”) .and() .withUser(“sang”) .password(“456”) .roles(“user”); } @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers("/hello").hasRole(“user”) .antMatchers("/admin").hasRole(“admin”) .antMatchers(HttpMethod.POST, “/login”).permitAll() .anyRequest().authenticated() .and() .addFilterBefore(new JwtLoginFilter("/login",authenticationManager()),UsernamePasswordAuthenticationFilter.class) .addFilterBefore(new JwtFilter(),UsernamePasswordAuthenticationFilter.class) .csrf().disable(); }}简单起见,这里我并未对密码进行加密,因此配置了NoOpPasswordEncoder的实例。简单起见,这里并未连接数据库,我直接在内存中配置了两个用户,两个用户具备不同的角色。配置路径规则时, /hello 接口必须要具备 user 角色才能访问, /admin 接口必须要具备 admin 角色才能访问,POST 请求并且是 /login 接口则可以直接通过,其他接口必须认证后才能访问。最后配置上两个自定义的过滤器并且关闭掉csrf保护。2.4 测试做完这些之后,我们的环境就算完全搭建起来了,接下来启动项目然后在 POSTMAN 中进行测试,如下: 登录成功后返回的字符串就是经过 base64url 转码的token,一共有三部分,通过一个 . 隔开,我们可以对第一个 . 之前的字符串进行解码,即Header,如下: 再对两个 . 之间的字符解码,即 payload: 可以看到,我们设置信息,由于base64并不是加密方案,只是一种编码方案,因此,不建议将敏感的用户信息放到token中。 接下来再去访问 /hello 接口,注意认证方式选择 Bearer Token,Token值为刚刚获取到的值,如下: 可以看到,访问成功。总结这就是 JWT 结合 Spring Security 的一个简单用法,讲真,如果实例允许,类似的需求我还是推荐使用 OAuth2 中的 password 模式。 不知道大伙有没有看懂呢?如果没看懂,松哥还有一个关于这个知识点的视频教程,如下: 如何获取这个视频教程呢?很简单,将本文转发到一个超过100人的微信群中(QQ群不算,松哥是群主的微信群也不算,群要为Java方向),或者多个微信群中,只要累计人数达到100人即可,然后加松哥微信,截图发给松哥即可获取资料。 ...

April 8, 2019 · 2 min · jiezi

浅谈 Spring Boot 中的 @Conditional 注解

概述Spring boot 中的 @Conditional 注解是一个不太常用到的注解,但确实非常的有用,我们知道 Spring Boot 是根据配置文件中的内容,决定是否创建 bean,以及如何创建 bean 到 Spring 容器中,而 Spring boot 自动化配置的核心控制,就是 @Conditional 注解。@Conditional 注解是 Spring 4.0 之后出的一个注解,与其搭配的一个接口是 Condition,@Conditional 注解会根据具体的条件决定是否创建 bean 到容器中,接下来看看 @Conditional 注解的简单使用。1. @Conditional 和 Condition 接口搭配使用这里需要实现的功能是,我们根据配置文件中的具体内容,来决定是否创建 bean,首先我们在 application.yml 中加上一个自定义配置:这里我们决定,这个配置中包含了 product 这个字符串的时候,才创建 bean。Product 是我自己随便创建的一个实体类,你可以自行创建。新建一个类 ProductCondition,内容如下:public class ProductCondition implements Condition { @Override public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata annotatedTypeMetadata) { //从配置文件中获取属性 String property = conditionContext.getEnvironment().getProperty(“create.bean”); if (property != null){ return property.contains(“product”); } else { return false; } }}这个类实现了 Condition 接口,这个接口只有一个方法,我们从配置文件中获取刚才创建的自定义配置,如果配置中包含了 product 这个字符串,就会返回 true。接下来创建一个配置类 ProductConfig,内容如下:@Configurationpublic class ProductConfig { @Conditional(ProductCondition.class) @Bean(name = “product”) public Product createProd(){ return Product.builder().id(12312).categoryId(12). productName(“Mac Book Pro”).productImg(“prod.png”) .productPrice(18000).build(); }}我们在创建的 bean 方法前面加上了 @Conditional 注解,判断的标准是刚才的 ProductCondition,如果是 true,则创建 bean,否则不创建。我们写一个测试类,来测试一下 bean 是否被创建了。测试代码如下:@Slf4j@SpringBootTest@RunWith(SpringRunner.class)public class ProductConfigTest { @Test public void createProd() { try { Product product = SpringContextUtil.getBean(“product”, Product.class); if (product != null){ System.out.println(“创建了 bean : " + product.toString()); } } catch (Exception e){ log.info(“发生异常,{}”, e.getMessage()); System.out.println(“没有创建 bean”); } }}运行测试代码,发现 bean 已经创建了:如果把 application.yml 中的配置改一下,不包含 product 这个字符串,那么返回的是 false,bean 则不会被创建,你可以试一下。2. @ConditionalOnClass 的使用这个注解的属性可以跟上一个类的完整路径或者是类的 Class 对象,如果类存在,则会创建 bean,例如下面的例子:@Configurationpublic class ProductConfig { @ConditionalOnClass(name = “com.roseduan.demo.entity.Product”) @Bean(name = “product”) public Product createProd(){ return Product.builder().id(12312).categoryId(12). productName(“Mac Book Pro”).productImg(“prod.png”) .productPrice(18000).build(); }}这个路径下面的实体类 Product 是存在的,所以会创建 bean,如果是一个不存在的类,则不会创建。3. @ConditionalOnProperty 的使用这个注解可以直接从配置文件中获取属性,然后做为是否创建 bean 的依据。例如我们在 application.yml 中添加一个自定义配置:ProductConfig 类的内容是这样的:@Configurationpublic class ProductConfig { @ConditionalOnProperty(value = “create.product.bean”) @Bean(name = “product”) public Product createProd(){ return Product.builder().id(12312).categoryId(12). productName(“Mac Book Pro”).productImg(“prod.png”) .productPrice(18000).build(); }}这里使用了 @ConditionalOnProperty 注解,从文件中读取配置,因为我们设置的是 true,所以这个 bean 会被创建,如果设置成 false,则 bean 不会被创建,你可以自己试一下。根据这个特性,我们可以给一些特定的配置加上一个开关,非常方便控制。这里我只是列举了几个常用的注解,你可以查看官方文档,里面有更详细的说明:参考文档:Spring Boot 官网文档 ...

April 6, 2019 · 1 min · jiezi

Spring boot webflux 中实现 RequestContextHolder

说明在 Spring boot web 中我们可以通过 RequestContextHolder 很方便的获取 request。ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();// 获取 requestHttpServletRequest request = requestAttributes.getRequest();不再需要通过参数传递 request。在 Spring webflux 中并没提供该功能,使得我们在 Aop 或者一些其他的场景中获取 request 变成了一个奢望???寻求解决方案首先我想到的是看看 spring-security 中是否有对于的解决方案,因为在 spring-security 中我们也是可以通过 SecurityContextHolder 很方便快捷的获取当前登录的用户信息。找到了 ReactorContextWebFilter,我们来看看 security 中他是怎么实现的。https://github.com/spring-pro…public class ReactorContextWebFilter implements WebFilter { private final ServerSecurityContextRepository repository; public ReactorContextWebFilter(ServerSecurityContextRepository repository) { Assert.notNull(repository, “repository cannot be null”); this.repository = repository; } @Override public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) { return chain.filter(exchange) .subscriberContext(c -> c.hasKey(SecurityContext.class) ? c : withSecurityContext(c, exchange) ); } private Context withSecurityContext(Context mainContext, ServerWebExchange exchange) { return mainContext.putAll(this.repository.load(exchange) .as(ReactiveSecurityContextHolder::withSecurityContext)); }}源码里面我们可以看到 他利用一个 Filter,chain.filter(exchange) 的返回值 Mono 调用了 subscriberContext 方法。那么我们就去了解一下这个 reactor.util.context.Context。找到 reactor 官方文档中的 context 章节:https://projectreactor.io/doc…大意是:从 Reactor 3.1.0 开始提供了一个高级功能,可以与 ThreadLocal 媲美,应用于 Flux 和 Mono 的上下文工具 Context。更多请大家查阅官方文档,对英文比较抵触的朋友可以使用 google 翻译。mica 中的实现mica 中的实现比较简单,首先是我们的 ReactiveRequestContextFilter:/** * ReactiveRequestContextFilter * * @author L.cm /@Configuration@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE)public class ReactiveRequestContextFilter implements WebFilter { @Override public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) { ServerHttpRequest request = exchange.getRequest(); return chain.filter(exchange) .subscriberContext(ctx -> ctx.put(ReactiveRequestContextHolder.CONTEXT_KEY, request)); }}在 Filter 中直接将 request 存储到 Context 上下文中。ReactiveRequestContextHolder 工具:/* * ReactiveRequestContextHolder * * @author L.cm /public class ReactiveRequestContextHolder { static final Class<ServerHttpRequest> CONTEXT_KEY = ServerHttpRequest.class; /* * Gets the {@code Mono<ServerHttpRequest>} from Reactor {@link Context} * @return the {@code Mono<ServerHttpRequest>} */ public static Mono<ServerHttpRequest> getRequest() { return Mono.subscriberContext() .map(ctx -> ctx.get(CONTEXT_KEY)); }}怎么使用呢?mica 中对未知异常处理,从 request 中获取请求的相关信息@ExceptionHandler(Throwable.class)@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)public Mono<?> handleError(Throwable e) { log.error(“未知异常”, e); // 发送:未知异常异常事件 return ReactiveRequestContextHolder.getRequest() .doOnSuccess(r -> publishEvent(r, e)) .flatMap(r -> Mono.just(R.fail(SystemCode.FAILURE)));}private void publishEvent(ServerHttpRequest request, Throwable error) { // 具体业务逻辑}WebClient 透传 request 中的 header此示例来源于开源中国问答中笔者的回复: 《如何在gateway 中获取 webflux的 RequestContextHolder》@GetMapping("/test")@ResponseBodypublic Mono<String> test() { WebClient webClient = testClient(); return webClient.get().uri("").retrieve().bodyToMono(String.class);}@Beanpublic WebClient testClient() { return WebClient.builder() .filter(testFilterFunction()) .baseUrl(“https://www.baidu.com”) .build();}private ExchangeFilterFunction testFilterFunction() { return (request, next) -> ReactiveRequestContextHolder.getRequest() .flatMap(r -> { ClientRequest clientRequest = ClientRequest.from(request) .headers(headers -> headers.set(HttpHeaders.USER_AGENT, r.getHeaders().getFirst(HttpHeaders.USER_AGENT))) .build(); return next.exchange(clientRequest); });}上段代码是透传 web 中的 request 中的 user_agent 请求头到 WebClient 中。开源推荐mica Spring boot 微服务核心组件集:https://gitee.com/596392912/micaAvue 一款基于vue可配置化的神奇框架:https://gitee.com/smallweigit/avuepig 宇宙最强微服务(架构师必备):https://gitee.com/log4j/pigSpringBlade 完整的线上解决方案(企业开发必备):https://gitee.com/smallc/SpringBladeIJPay 支付SDK让支付触手可及:https://gitee.com/javen205/IJPay关注我们扫描上面二维码,更多精彩内容每天推荐!转载声明如梦技术对此篇文章有最终所有权,转载请注明出处,参考也请注明,谢谢! ...

April 4, 2019 · 2 min · jiezi

Spring boot 微服务核心组件集 mica v1.0.1 发布

mica(云母)mica 云母,寓意为云服务的核心,使得云服务开发更加方便快捷。mica 的前身是 lutool,lutool在内部孵化了小两年,已经被多个朋友运用到企业。由于 lutool 对微服务不够友好,故重塑了mica。mica 中的部分大部分组件进行了持续性打磨,增强易用性和性能。mica 核心依赖mica 基于 java 8,没有历史包袱,支持传统 Servlet 和 Reactive(webflux)。采用 mica-auto 自动生成 spring.factories 和 spring-devtools.properties 配置,仅依赖 Spring boot、Spring cloud 全家桶,无第三方依赖。市面上鲜有的微服务核心组件。更新说明[1.0.1] - 2019-04-03 处理几处 P3C 代码检查问题。@冷冷 优化泛型,避免部分环境下的编译问题。 添加 lutool 中的 WebUtil.renderJson()。 优化 DateUtil 性能。 优化 RuntimeUtil,提高性能。 升级 gradle 到 5.3.1。本次版本主要是进行了一些工具的压力测试:Bean copy 测试BenchmarkScoreErrorUnitshutool1939.09226.747ops/msspring3569.03539.607ops/mscglib9112.785560.503ops/msmica17753.409393.245ops/ms结论:mica 在非编译期 Bean copy 性能强劲,功能强大。UUID 压测BenchmarkScoreErrorUnitsjdk8UUId734.59517.220ops/msjdk8ThreadLocalRandomUUId3224.75932.107ops/mshutoolFastSimpleUUID3619.74867.195ops/msmicaUUId(java9 方式)12375.405241.879ops/ms结论:mica 在使用了 java9 的算法,性能卓越。Date format 压测BenchmarkScoreErrorUnitsjava8Date2405.92444.912ops/msmicaDateUtil2541.75348.321ops/mshutoolDateUtil2775.53113.526ops/ms结论:hutool 使用的 common lang3 的 FastDateFormat 占用优势。开源推荐mica Spring boot 微服务核心组件集:https://gitee.com/596392912/micaAvue 一款基于vue可配置化的神奇框架:https://gitee.com/smallweigit/avuepig 宇宙最强微服务(架构师必备):https://gitee.com/log4j/pigSpringBlade 完整的线上解决方案(企业开发必备):https://gitee.com/smallc/SpringBladeIJPay 支付SDK让支付触手可及:https://gitee.com/javen205/IJPay关注我们扫描上面二维码,更多精彩内容每天推荐!

April 4, 2019 · 1 min · jiezi

SpringBoot 基础配置 & Hello Word

基础配置 yml跟properties 例如设置端口为:8000 application.propertiesserver.port=8000server.context-path=/shuibo application.ymlserver: port: 8000 context-path: /shuibo #使用localhost:8000/shuibo YAML yaml是JSON的一个超集,是一种结构层次清晰明了的数据格式,简单易读易用, Spring Boot对SnakeYAML库做了集成,所以可以在Spring Boot项目直接使用。 Spring Boot配置优先级顺序,从高到低:命令行参数通过System.getProperties()获取的Java系统参数操作系统环境变量从java:comp/env得到JNDI属性通过RandomValuePropertySource 生成的“random.*”属性应用Jar文件之外的属性配置文件,通过spring.config.location参数应用Jar文件内部的属性文件在应用配置 Java 类(包含“@Configuration”注解的 Java 类)中通过“@PropertySource”注解声明的属性文件通过“SpringApplication.setDefaultProperties”声明的默认属性。配置环境一般在实际项目中会有多个环境,比如: 测试环境 -> 正式环境 -> … 每个环境的配置比如:Sql链接,redis配置之类都不一样,通过配置文件决定启用的配置文件。spring: profiles: active: pro获取配置1.在application.yml配置key value 例如: 获取配置浏览器输入:localhost:8000/index2.通过ConfigBean 添加配置 创建ConfigBean@Component@ConfigurationProperties(prefix = “bobby”)//获取前缀为bobby下的配置信息public class ConfigBean { private String name;//名字与配置文件中一致 private Integer age; public String getName() { return name; } public void setName(String name) { this.name = name; } public Integer getAge() { return age; } public void setAge(Integer age) { this.age = age; }}获取配置@RestControllerpublic class IndexController { @Autowired private ConfigBean configBean; @RequestMapping("/config") public String config(){ return “姓名:” + configBean.getName() + “,年龄:” + configBean.getAge(); }}浏览器输入:localhost:8000/config小结 本文讲述了配置文件的加载顺序,properties跟yml区别,通过两种方式读取配置文件。本文GitHub地址:https://github.com/ishuibo/Sp… ...

April 3, 2019 · 1 min · jiezi

spring boot学习(7)— 配置信息的获取方式

使用 ConfigurationProperties 来使用 properties 的值。启用自定义配置: @Configuration @EnableConfigurationProperties({YourConfigClass}.class)@ConfigurationProperties(prefix) 注解自定义的 YourConfigClass通过 bean 来使用自定义的配置信息类@SpringBootApplication@EnableConfigurationProperties(TestConfigurationProperties.class)public class DemoApplication{ @Autowired TestConfigurationProperties testConfig; public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); new DemoApplication().testConfig.printProperties(); } @PostConstruct private void init(){ testConfig.printProperties(); }}@ConfigurationProperties(“testconfig”)public class TestConfigurationProperties { private String first; private String second; private String third; private String fourth; private String fifth; private String sixth; private String seventh; private String eightth; //getters and setters这样就可以通过 Bean 来使用。2. 通过 @Value 使用通过注解 @Value("${testconfig.first}") 可以给变量赋值成 配置 testconfig.first 的信息。@Componentpublic class TestValue { @Value("${testconfig.first}") private String first; @Value("${testconfig.second}") private String second; @Value("${testconfig.third}") private String third; @Value("${testconfig.fourth}") private String fourth; @Value("${testconfig.fifth}") private String fifth; @Value("${testconfig.sixth}") private String sixth; @Value("${testconfig.seventh}") private String seventh; @Value("${testconfig.eightth}") private String eightth; public String getFirst() { return first; } public void setFirst(String first) { this.first = first; } public String getSecond() { return second; } public void setSecond(String second) { this.second = second; } public String getThird() { return third; } public void setThird(String third) { this.third = third; } public String getFourth() { return fourth; } public void setFourth(String fourth) { this.fourth = fourth; } public String getFifth() { return fifth; } public void setFifth(String fifth) { this.fifth = fifth; } public String getSixth() { return sixth; } public void setSixth(String sixth) { this.sixth = sixth; } public String getSeventh() { return seventh; } public void setSeventh(String seventh) { this.seventh = seventh; } public String getEightth() { return eightth; } public void setEightth(String eightth) { this.eightth = eightth; } public void printProperties(){ System.out.println("\ntest value:"); System.out.println(“first: " + first); System.out.println(“second: " + second); System.out.println(“third: " + third); System.out.println(“fourth: " + fourth); System.out.println(“fifth: " + fifth); System.out.println(“sixth: " + sixth); System.out.println(“seventh: " + seventh); System.out.println(“eightth: " + eightth); }}输出为:test value:first: ./config/second: ./config/ymlthird: classpath/config/fourth: classpathfifth: ./config/sixth: ./config/seventh: ./config/eightth: ./config/

March 31, 2019 · 2 min · jiezi

spring boot学习(6)— 配置信息及其读取优先级

properties 信息从哪里取在不同的环境,我们需要使用不同的配置,Spring boot 已经提供了相关功能,可以是 properties 文件, yaml 文件 或是命令行参数。优先级如下Devtools global settings properties on your home directory (~/.spring-boot-devtools.properties when devtools is active).@TestPropertySource annotations on your tests.@SpringBootTest#properties annotation attribute on your tests.Command line arguments.java -jar app.jar –name=“Spring"Properties from SPRING_APPLICATION_JSON (inline JSON embedded in an environment variable or system property).environment vaiable:SPRING_APPLICATION_JSON=’{“acme”:{“name”:“test”}}’ java -jar myapp.jarcommand line:java -Dspring.application.json=’{“name”:“test”}’ -jar myapp.jarjava -jar myapp.jar –spring.application.json=’{“name”:“test”}‘ServletConfig init parameters.ServletContext init parameters.JNDI attributes from java:comp/env.Java System properties (System.getProperties()).OS environment variables.A RandomValuePropertySource that has properties only in random.*.my.secret=${random.value}my.number=${random.int}my.bignumber=${random.long}my.uuid=${random.uuid}my.number.less.than.ten=${random.int(10)}my.number.in.range=${random.int[1024,65536]}Profile-specific application properties outside of your packaged jar (application-{profile}.properties and YAML variants).Profile-specific application properties packaged inside your jar (application-{profile}.properties and YAML variants).Application properties outside of your packaged jar (application.properties and YAML variants).Application properties packaged inside your jar (application.properties and YAML variants).@PropertySource annotations on your @Configuration classes.Default properties (specified by setting SpringApplication.setDefaultProperties).2. 使用 application.properties 文件使用 properties 文件,spring boot 会根据以下目录去寻找,添加到 Spring Environment 中,优先级依次递增。classpath:/: resources 目录classpath:/config/: resources 下 config 目录file:./:工程根目录file:./config/: 工程跟目录下的 config 目录2.1 加载顺序:从优先级高的先加载。file:./config/file:./classpath:/config/classpath:/2019-03-27 22:38:24.848 DEBUG 39802 — [ main] o.s.boot.SpringApplication : Loading source class com.example.exitcode.DemoApplication2019-03-27 22:38:24.915 DEBUG 39802 — [ main] o.s.b.c.c.ConfigFileApplicationListener : Loaded config file ‘file:./config/application.properties’ (file:./config/application.properties)2019-03-27 22:38:24.915 DEBUG 39802 — [ main] o.s.b.c.c.ConfigFileApplicationListener : Loaded config file ‘file:./application.properties’ (file:./application.properties)2019-03-27 22:38:24.915 DEBUG 39802 — [ main] o.s.b.c.c.ConfigFileApplicationListener : Loaded config file ‘jar:file:xxxxx-0.0.1-SNAPSHOT.jar!/BOOT-INF/classes!/config/application.properties’ (classpath:/config/application.properties)2019-03-27 22:38:24.915 DEBUG 39802 — [ main] o.s.b.c.c.ConfigFileApplicationListener : Loaded config file ‘jar:file:xxxxx-0.0.1-SNAPSHOT.jar!/BOOT-INF/classes!/application.properties’ (classpath:/application.properties)2.2 属性值怎么取优先级高的会覆盖优先级低的。./config/application.propertiestestconfig.first=./config/#testconfig.second=./config/#testconfig.third=./config/#testconfig.fourth=./config/./application.propertiestestconfig.first=./testconfig.second=./#testconfig.third=./#testconfig.fourth=./classpath:/config/application.propertiestestconfig.first=classpath/config/testconfig.second=classpath/config/testconfig.third=classpath/config/#testconfig.fourth=classpath/config/classpath:/application.propertiestestconfig.first=classpathtestconfig.second=classpathtestconfig.third=classpathtestconfig.fourth=classpath输出如下:2019-03-27 23:29:12.434 INFO 1335 — [ main] com.example.properties.DemoApplication : No active profile set, falling back to default profiles: defaultfirst: ./config/second: ./third: classpath/config/fourth: classpath2019-03-27 23:29:13.052 INFO 1335 — [ main] com.example.properties.DemoApplication : Started DemoApplication in 16.565 seconds (JVM running for 23.467)2.3 多环境配置文件加一个文件: classpath:/application-product.propertiestestconfig.first=product-classpathtestconfig.second=product-classpath通过 spring.profiles.active 来指定环境所对应的 properties 文件:运行 java -jar build/libs/properties-0.0.1-SNAPSHOT.jar –spring.profiles.active=product, 输出如下:2019-03-28 20:34:44.726 INFO 25859 — [ main] com.example.properties.DemoApplication : The following profiles are active: productfirst: product-classpathsecond: product-classpaththird: classpath/config/fourth: classpathfifth: ./config/sixth: ./config/seventh: ./config/eightth: ./config/2.3 使用 yaml 文件来代替 properties 文件。也可以使用 yaml 格式的文件。但是在同等目录下,properties 优先级高于 yaml 文件的配置信息。新增文件 ./config/application.ymltestconfig: frist: ./config/yml second: ./config/yml命令 java -jar build/libs/properties-0.0.1-SNAPSHOT.jar 输出为:first: ./config/second: ./config/ymlthird: classpath/config/fourth: classpathfifth: ./config/sixth: ./config/seventh: ./config/eightth: ./config/2.5 属性文件中可以使用变量已经声明过的变量值:app.name=MyAppapp.description=${app.name} is a Spring Boot application

March 30, 2019 · 2 min · jiezi

【spring boot2】第10篇:spring boot 整合 cache

spring缓存org.springframework.cache.Cache和org.springframework.cache.CacheManager接口是spring中用来统一不同的缓存技术公用接口Cache接口为缓存的组件规范定义,包含缓存的各种操作集合Cache接口下spring提供了各种xxxCache的实现,如 RedisCache、ConcurrentMapCache等每次调用有缓存功能的方法时,spring 会检查指定参数的目标方法是否已经被调用过,如果有就直接从缓存中获取方法的结果;如果没有就调用过方法就先缓存结果后在将结果返回给用户,下次调用直接从缓存中获取。使用spring缓存抽象时我们需要关注以下两点:确定方法需要被缓存以及他们的缓存策略从缓存中读取之前缓存存储的数据spring 缓存中几个概念名词说明Cache缓存接口,定义缓存操作。实现有:RedisCache、EhCacheCache、 ConcurrentMapCache等CacheManager缓存管理器,管理各种缓存(Cache)组件@Cacheable主要针对方法配置,能够根据方法的请求参数对其结果进行缓存@CacheEvict清空缓存的注解@CachePut保证方法被调用,又希望结果被缓存@EnableCaching开启基于注解的缓存keyGenerator缓存数据时key生成策略serialize缓存数据时value序列化策略@Cacheable、@CacheEvict和@CachePut 注解属性属性说明示例value缓存的名称,在 spring 配置文件中定义,必须指定 至少一个@Cacheable(value=”mycache”) 或者 @Cacheable(value={”cache1”,”cache2”}key缓存的 key,可以为空,如果指定要必须按照 spel 表达式编写,如果不指定,则缺省按照方法的所有参数进行组合@Cacheable(value=”testcache”,key=”#userName”)condition缓存的条件,可以为空,使用 spel 编写,返回 true 或者 false,只有为 true 才进行缓存/清除缓存,在调用方法之前之后都能判断@Cacheable(value=”testcache”,condition=”#userNam e.length()>2”)allEntries (@CacheEvict )是否清空所有缓存内容,缺省为 false,如果指定为 true,则方法调用后将立即清空所有缓存@CachEvict(value=”testcache”,allEntries=true)beforeInvocation(@CacheEvict)是否在方法执行前就清空,缺省为 false,如果指定 为 true,则在方法还没有执行的时候就清空缓存, 缺省情况下,如果方法执行抛出异常,则不会清空 缓存@CachEvict(value=”testcache”, beforeInvocation=true)unless(@CachePut) (@Cacheable)用于否决缓存的,不像condition,该表达式只在方 法执行之后判断,此时可以拿到返回值result进行判 断。条件为true不会缓存,fasle才缓存@Cacheable(value=”testcache”,unless=”#result == null”)

March 30, 2019 · 1 min · jiezi

Spring Boot 文件上传与下载

文件的上传及下载功能是开发人员在日常应用及编程开发中经常会遇到的。正好最近开发需要用到此功能,虽然本人是 Android 开发人员,但还是业余客串了一下后台开发。在本文中,您将学习如何使用 Spring Boot 实现 Web 服务中的文件上传和下载功能。首先会构建一个 REST APIs 实现上传及下载的功能,然后使用 Postman 工具来测试这些接口,最后创建一个 Web 界面使用 JavaScript 调用接口演示完整的功能。最终界面及功能如下:项目环境- Spring Boot : 2.1.3.RELEASE- Gredle : 5.2.1- Java : 1.8- Intellij IDEA : 2018.3.3项目创建开发环境为 Intellij IDEA,项目创建很简单,按照下面的步骤创建即可:File -> New -> Project…选择 Spring Initializr,点击 Next填写 Group (项目域名) 和 Artifact (项目别名)构建类型可以选择 Maven 或 Gradle, 看个人习惯添加 Web 依赖输入项目名称及保存路径,完成创建项目创建完毕之后就可以进行开发,项目的完整结构如下图所示:参数配置项目创建完成之后,需要设置一些必要的参数,打开项目resources目录下配置文件application.properties,在其中添加以下参数:server.port=80## MULTIPART (MultipartProperties)# 开启 multipart 上传功能spring.servlet.multipart.enabled=true# 文件写入磁盘的阈值spring.servlet.multipart.file-size-threshold=2KB# 最大文件大小spring.servlet.multipart.max-file-size=200MB# 最大请求大小spring.servlet.multipart.max-request-size=215MB## 文件存储所需参数# 所有通过 REST APIs 上传的文件都将存储在此目录下file.upload-dir=./uploads其中file.upload-dir=./uploads参数为自定义的参数,创建FileProperties.javaPOJO类,使配置参数可以自动绑定到POJO类。import org.springframework.boot.context.properties.ConfigurationProperties;@ConfigurationProperties(prefix = “file”)public class FileProperties { private String uploadDir; public String getUploadDir() { return uploadDir; } public void setUploadDir(String uploadDir) { this.uploadDir = uploadDir; }}然后在@SpringBootApplication注解的类中添加@EnableConfigurationProperties注解以开启ConfigurationProperties功能。SpringBootFileApplication.java@SpringBootApplication@EnableConfigurationProperties({ FileProperties.class})public class SpringBootFileApplication { public static void main(String[] args) { SpringApplication.run(SpringBootFileApplication.class, args); }}配置完成,以后若有file前缀开头的参数需要配置,可直接在application.properties配置文件中配置并更新FileProperties.java即可。另外再创建一个上传文件成功之后的Response响应实体类UploadFileResponse.java及异常类FileException.java来处理异常信息。UploadFileResponse.javapublic class UploadFileResponse { private String fileName; private String fileDownloadUri; private String fileType; private long size; public UploadFileResponse(String fileName, String fileDownloadUri, String fileType, long size) { this.fileName = fileName; this.fileDownloadUri = fileDownloadUri; this.fileType = fileType; this.size = size; } // getter and setter …}FileException.javapublic class FileException extends RuntimeException{ public FileException(String message) { super(message); } public FileException(String message, Throwable cause) { super(message, cause); }}创建接口下面需要创建文件上传下载所需的 REST APIs 接口。创建文件FileController.java。import com.james.sample.file.dto.UploadFileResponse;import com.james.sample.file.service.FileService;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.core.io.Resource;import org.springframework.http.HttpHeaders;import org.springframework.http.MediaType;import org.springframework.http.ResponseEntity;import org.springframework.web.bind.annotation.*;import org.springframework.web.multipart.MultipartFile;import org.springframework.web.servlet.support.ServletUriComponentsBuilder;import javax.servlet.http.HttpServletRequest;import java.io.IOException;import java.util.Arrays;import java.util.List;import java.util.stream.Collectors;@RestControllerpublic class FileController { private static final Logger logger = LoggerFactory.getLogger(FileController.class); @Autowired private FileService fileService; @PostMapping("/uploadFile") public UploadFileResponse uploadFile(@RequestParam(“file”) MultipartFile file){ String fileName = fileService.storeFile(file); String fileDownloadUri = ServletUriComponentsBuilder.fromCurrentContextPath() .path("/downloadFile/") .path(fileName) .toUriString(); return new UploadFileResponse(fileName, fileDownloadUri, file.getContentType(), file.getSize()); } @PostMapping("/uploadMultipleFiles") public List<UploadFileResponse> uploadMultipleFiles(@RequestParam(“files”) MultipartFile[] files) { return Arrays.stream(files) .map(this::uploadFile) .collect(Collectors.toList()); } @GetMapping("/downloadFile/{fileName:.+}") public ResponseEntity<Resource> downloadFile(@PathVariable String fileName, HttpServletRequest request) { // Load file as Resource Resource resource = fileService.loadFileAsResource(fileName); // Try to determine file’s content type String contentType = null; try { contentType = request.getServletContext().getMimeType(resource.getFile().getAbsolutePath()); } catch (IOException ex) { logger.info(“Could not determine file type.”); } // Fallback to the default content type if type could not be determined if(contentType == null) { contentType = “application/octet-stream”; } return ResponseEntity.ok() .contentType(MediaType.parseMediaType(contentType)) .header(HttpHeaders.CONTENT_DISPOSITION, “attachment; filename="” + resource.getFilename() + “"”) .body(resource); }}FileController类在接收到用户的请求后,使用FileService类提供的storeFile()方法将文件写入到系统中进行存储,其存储目录就是之前在application.properties配置文件中的file.upload-dir参数的值./uploads。下载接口downloadFile()在接收到用户请求之后,使用FileService类提供的loadFileAsResource()方法获取存储在系统中文件并返回文件供用户下载。FileService.javaimport com.james.sample.file.exception.FileException;import com.james.sample.file.property.FileProperties;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.core.io.Resource;import org.springframework.core.io.UrlResource;import org.springframework.stereotype.Service;import org.springframework.util.StringUtils;import org.springframework.web.multipart.MultipartFile;import java.io.IOException;import java.net.MalformedURLException;import java.nio.file.Files;import java.nio.file.Path;import java.nio.file.Paths;import java.nio.file.StandardCopyOption;@Servicepublic class FileService { private final Path fileStorageLocation; // 文件在本地存储的地址 @Autowired public FileService(FileProperties fileProperties) { this.fileStorageLocation = Paths.get(fileProperties.getUploadDir()).toAbsolutePath().normalize(); try { Files.createDirectories(this.fileStorageLocation); } catch (Exception ex) { throw new FileException(“Could not create the directory where the uploaded files will be stored.”, ex); } } /** * 存储文件到系统 * * @param file 文件 * @return 文件名 / public String storeFile(MultipartFile file) { // Normalize file name String fileName = StringUtils.cleanPath(file.getOriginalFilename()); try { // Check if the file’s name contains invalid characters if(fileName.contains("..")) { throw new FileException(“Sorry! Filename contains invalid path sequence " + fileName); } // Copy file to the target location (Replacing existing file with the same name) Path targetLocation = this.fileStorageLocation.resolve(fileName); Files.copy(file.getInputStream(), targetLocation, StandardCopyOption.REPLACE_EXISTING); return fileName; } catch (IOException ex) { throw new FileException(“Could not store file " + fileName + “. Please try again!”, ex); } } /* * 加载文件 * @param fileName 文件名 * @return 文件 / public Resource loadFileAsResource(String fileName) { try { Path filePath = this.fileStorageLocation.resolve(fileName).normalize(); Resource resource = new UrlResource(filePath.toUri()); if(resource.exists()) { return resource; } else { throw new FileException(“File not found " + fileName); } } catch (MalformedURLException ex) { throw new FileException(“File not found " + fileName, ex); } }}接口测试在完成上述的代码之后,打开SpringBootFileApplication.java并运行,运行完成之后就可以使用 Postman 进行测试了。单个文件上传结果:多个文件上传结果:文件下载结果:Web 前端开发index.html<!DOCTYPE html><html lang=“zh-cn”><head> <!– Required meta tags –> <meta charset=“UTF-8”> <meta http-equiv=“X-UA-Compatible” content=“IE=edge”> <meta name=“viewport” content=“width=device-width, initial-scale=1, shrink-to-fit=no”> <title>Spring Boot File Upload / Download Rest API Example</title> <!– Bootstrap CSS –> <link href="/css/main.css” rel=“stylesheet”/></head><body><noscript> <h2>Sorry! Your browser doesn’t support Javascript</h2></noscript><div class=“upload-container”> <div class=“upload-header”> <h2>Spring Boot File Upload / Download Rest API Example</h2> </div> <div class=“upload-content”> <div class=“single-upload”> <h3>Upload Single File</h3> <form id=“singleUploadForm” name=“singleUploadForm”> <input id=“singleFileUploadInput” type=“file” name=“file” class=“file-input” required/> <button type=“submit” class=“primary submit-btn”>Submit</button> </form> <div class=“upload-response”> <div id=“singleFileUploadError”></div> <div id=“singleFileUploadSuccess”></div> </div> </div> <div class=“multiple-upload”> <h3>Upload Multiple Files</h3> <form id=“multipleUploadForm” name=“multipleUploadForm”> <input id=“multipleFileUploadInput” type=“file” name=“files” class=“file-input” multiple required/> <button type=“submit” class=“primary submit-btn”>Submit</button> </form> <div class=“upload-response”> <div id=“multipleFileUploadError”></div> <div id=“multipleFileUploadSuccess”></div> </div> </div> </div></div><!– Optional JavaScript –><script src="/js/main.js”></script></body></html>main.css { -webkit-box-sizing: border-box; -moz-box-sizing: border-box; box-sizing: border-box;}body { margin: 0; padding: 0; font-weight: 400; font-family: “Helvetica Neue”, Helvetica, Arial, sans-serif; font-size: 1rem; line-height: 1.58; color: #333; background-color: #f4f4f4;}body:before { height: 50%; width: 100%; position: absolute; top: 0; left: 0; background: #128ff2; content: “”; z-index: 0;}.clearfix:after { display: block; content: “”; clear: both;}h1, h2, h3, h4, h5, h6 { margin-top: 20px; margin-bottom: 20px;}h1 { font-size: 1.7em;}a { color: #128ff2;}button { box-shadow: none; border: 1px solid transparent; font-size: 14px; outline: none; line-height: 100%; white-space: nowrap; vertical-align: middle; padding: 0.6rem 1rem; border-radius: 2px; transition: all 0.2s ease-in-out; cursor: pointer; min-height: 38px;}button.primary { background-color: #128ff2; box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.12); color: #fff;}input { font-size: 1rem;}input[type=“file”] { border: 1px solid #128ff2; padding: 6px; max-width: 100%;}.file-input { width: 100%;}.submit-btn { display: block; margin-top: 15px; min-width: 100px;}@media screen and (min-width: 500px) { .file-input { width: calc(100% - 115px); } .submit-btn { display: inline-block; margin-top: 0; margin-left: 10px; }}.upload-container { max-width: 700px; margin-left: auto; margin-right: auto; background-color: #fff; box-shadow: 0 1px 11px rgba(0, 0, 0, 0.27); margin-top: 60px; min-height: 400px; position: relative; padding: 20px;}.upload-header { border-bottom: 1px solid #ececec;}.upload-header h2 { font-weight: 500;}.single-upload { padding-bottom: 20px; margin-bottom: 20px; border-bottom: 1px solid #e8e8e8;}.upload-response { overflow-x: hidden; word-break: break-all;}main.js’use strict’;var singleUploadForm = document.querySelector(’#singleUploadForm’);var singleFileUploadInput = document.querySelector(’#singleFileUploadInput’);var singleFileUploadError = document.querySelector(’#singleFileUploadError’);var singleFileUploadSuccess = document.querySelector(’#singleFileUploadSuccess’);var multipleUploadForm = document.querySelector(’#multipleUploadForm’);var multipleFileUploadInput = document.querySelector(’#multipleFileUploadInput’);var multipleFileUploadError = document.querySelector(’#multipleFileUploadError’);var multipleFileUploadSuccess = document.querySelector(’#multipleFileUploadSuccess’);function uploadSingleFile(file) { var formData = new FormData(); formData.append(“file”, file); var xhr = new XMLHttpRequest(); xhr.open(“POST”, “/uploadFile”); xhr.onload = function() { console.log(xhr.responseText); var response = JSON.parse(xhr.responseText); if(xhr.status == 200) { singleFileUploadError.style.display = “none”; singleFileUploadSuccess.innerHTML = “<p>File Uploaded Successfully.</p><p>DownloadUrl : <a href=’” + response.fileDownloadUri + “’ target=’_blank’>” + response.fileDownloadUri + “</a></p>”; singleFileUploadSuccess.style.display = “block”; } else { singleFileUploadSuccess.style.display = “none”; singleFileUploadError.innerHTML = (response && response.message) || “Some Error Occurred”; } } xhr.send(formData);}function uploadMultipleFiles(files) { var formData = new FormData(); for(var index = 0; index < files.length; index++) { formData.append(“files”, files[index]); } var xhr = new XMLHttpRequest(); xhr.open(“POST”, “/uploadMultipleFiles”); xhr.onload = function() { console.log(xhr.responseText); var response = JSON.parse(xhr.responseText); if(xhr.status == 200) { multipleFileUploadError.style.display = “none”; var content = “<p>All Files Uploaded Successfully</p>”; for(var i = 0; i < response.length; i++) { content += “<p>DownloadUrl : <a href=’” + response[i].fileDownloadUri + “’ target=’_blank’>” + response[i].fileDownloadUri + “</a></p>”; } multipleFileUploadSuccess.innerHTML = content; multipleFileUploadSuccess.style.display = “block”; } else { multipleFileUploadSuccess.style.display = “none”; multipleFileUploadError.innerHTML = (response && response.message) || “Some Error Occurred”; } } xhr.send(formData);}singleUploadForm.addEventListener(‘submit’, function(event){ var files = singleFileUploadInput.files; if(files.length === 0) { singleFileUploadError.innerHTML = “Please select a file”; singleFileUploadError.style.display = “block”; } uploadSingleFile(files[0]); event.preventDefault();}, true);multipleUploadForm.addEventListener(‘submit’, function(event){ var files = multipleFileUploadInput.files; if(files.length === 0) { multipleFileUploadError.innerHTML = “Please select at least one file”; multipleFileUploadError.style.display = “block”; } uploadMultipleFiles(files); event.preventDefault();}, true);总结至此,文件的上传及下载功能已完成。在正式环境中可能还需要将上传的文件存储到数据库,此处按照实际需求去处理即可。本文源代码地址:https://github.com/JemGeek/SpringBoot-Sample/tree/master/SpringBoot-File本文参考(需要FQ):https://www.callicoder.com/spring-boot-file-upload-download-rest-api-example/更多技术文章欢迎关注我的博客主页:http://JemGeek.com阅读原文 ...

March 30, 2019 · 6 min · jiezi

Spring boot 配置 SSL证书

背景现在在做的项目,一部分功能时建立在小程序上的,所以就不得不面临一个问题,就是小程序在发起请求,请求后台的时候,只能使用https,会对服务器的域名进行https证书校验,所以,我们就不得不去考虑配置证书的问题。这里需要先声明一下,由于我这里还没有一个域名证书,所以使用的是本地自签证书。自签证书只能在开发的时候使用,一旦小程序上线,证书将会失效。大家可以通过各种途径获取到证书,然后配置的过程基本一致,自己配置的时候注意替换就好了。自签证书为了开发试验,我们需要本地生成一个自签证书。我们直接使用JDK自带的keytool工具来生成证书。首先先找到jdk的bin目录:然后命令行进入文件对应的路径,输入如下命令:keytool -genkey -alias tomcat -storetype PKCS12 -keyalg RSA -keysize 2048 -keystore keystore.p12 -validity 3650然后按照提示,输入相应的信息:要注意记住秘钥库口令,这个后面会用到最后,就会在当前目录下生成一个证书:配置application.properties先将我们生成的证书移到项目目录下:然后配置application.properties文件:# SSL证书相关配置# https加密端口server.port=7443# 证书路径server.ssl.key-store=classpath:keystore.p12# 证书秘钥server.ssl.key-store-password=生成证书时候输入的密钥库口令# 证书类型server.ssl.key-store-type=PKCS12# 证书别名server.ssl.key-alias=tomcat细心的读者会发现这里的配置和我们上面创建证书时使用的命令式对应的。重定向http到https因为我们原来的请求方式都是http, 现在我们想使用https,就需要做一下重定向,不能跟之前的冲突。package com.yunzhiclub.alice;import org.apache.catalina.Context;import org.apache.catalina.connector.Connector;import org.apache.tomcat.util.descriptor.web.SecurityCollection;import org.apache.tomcat.util.descriptor.web.SecurityConstraint;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;import org.springframework.context.annotation.Bean;@SpringBootApplicationpublic class AliceApplication { public static void main(String[] args) { SpringApplication.run(AliceApplication.class, args); } /** * 配置一个 TomcatServletWebServerFactory bean * 将http 重定向到 https * @return / @Bean public TomcatServletWebServerFactory servletContainer() { TomcatServletWebServerFactory tomcat = new TomcatServletWebServerFactory () { @Override protected void postProcessContext(Context context) { SecurityConstraint securityConstraint = new SecurityConstraint(); securityConstraint.setUserConstraint(“CONFIDENTIAL”); SecurityCollection collection = new SecurityCollection(); collection.addPattern("/"); securityConstraint.addCollection(collection); context.addConstraint(securityConstraint); } }; tomcat.addAdditionalTomcatConnectors(initiateHttpConnector()); return tomcat; } /** * 让我们的应用支持HTTP是个好想法,但是需要重定向到HTTPS, * 但是不能同时在application.properties中同时配置两个connector, * 所以要以编程的方式配置HTTP connector,然后重定向到HTTPS connector * @return Connector */ private Connector initiateHttpConnector() { Connector connector = new Connector(“org.apache.coyote.http11.Http11NioProtocol”); connector.setScheme(“http”); connector.setPort(8080); // http端口 connector.setSecure(false); connector.setRedirectPort(7443); // application.properties中配置的https端口 return connector; }}测试在浏览其中请求一下后台接口正确请求。总结自己生成的证书,会被浏览器看做不安全的,所以要上线的项目,还是去申请一个正规的SSl证书吧。相关参考:https://blog.csdn.net/MasonQA…https://blog.csdn.net/m0_3812… ...

March 29, 2019 · 1 min · jiezi

spring-boot下如何满足多生产环境中个性化定制功能

在项目的开发中,我们很难做到开发一套标准的流程来解决所有客户的需求。比如,我们当前的计量项目,分别运行于赤峰市和河北省。虽然两个区域处理的业务相同,但是对细节的实现要求却不同。前面也学习过计量检定软件,其为了解决各个定制者使用的功能需求,最后采取的方案是:将基础项目复制多份,进而满足不同的客户需求。优点当然是有的,但比起缺点来,优点便不值一提。缺点很明显,总结为一句话就是:项目变得难以维护。所以,当前让我们看到的就是,几个开发人员,每天处于解决问题当中。本文将给出一种方案,来有效的规避上述问题。资源与环境示例代码:https://github.com/mengyunzhi/springBootSampleCode/tree/master/dynamic-autowire开发环境:java1.8 + spring-boot:2.1.3.RELEASE需求假设假设使用本项目的人员为:中国人、美国人,分别能接受的语言为中文和英文。项目运行后,可以根据当前的访问人员是国籍来动态显示:你好或hello有新的需求后,比如:增加德国人并显示Hallo。增加功能时,不更改核心代码。不使用if else注意:如果你看完需求假设后,毫无触动,请忽略本文以下内容解决方案解决方案中,我们涉及了两种设计模块,分别为:策略模式及工厂模式。 策略模式:一般用于将具体的算法进行抽象及剥离。此项目中,我们的具体算法是说你好。工厂模式:一般用于根据环境来动态的创建BEAN的情况下。引项目中,我们将根据不同国家的人,来返回不同的说你好这个算法。先给出UML图:SpeakServiceSpeakService即为我们供其它模块调用的说话服务,调用其中的SayHello()来完成说你好功能。package com.mengyunzhi.demo.dynamicautowire;/** * 你好 /public interface SpeakService { void sayHello();}在其实现类中,我们注入SayHelloFactory,让其来返回正确的SayHelloService,最终调用sayHello()来完成目标。package com.mengyunzhi.demo.dynamicautowire;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Service;/* * 你好 /@Servicepublic class SpeakServiceImpl implements SpeakService { private final SayHelloFactory sayHelloFactory; // 说话工厂 @Autowired public SpeakServiceImpl(SayHelloFactory sayHelloFactory) { this.sayHelloFactory = sayHelloFactory; } @Override public void sayHello() { this.sayHelloFactory.getSayHelloService().sayHello(); }}SayHelloFactorypackage com.mengyunzhi.demo.dynamicautowire;/* * 说话工厂 /public interface SayHelloFactory { void setCountryCode(CountryCode countryCode); SayHelloService getSayHelloService();}在此,我们增加一个CountryCode表示当前访问者的国家。其实在获取访问者国家时,我们也可以调用其它Bean的其它来实现。package com.mengyunzhi.demo.dynamicautowire;/* * 国家代码 /public enum CountryCode { CHINA((byte) 0, “中国”), USA((byte) 1, “美国”); private Byte code; private String name; CountryCode(Byte code, String name) { this.code = code; this.name = name; } public Byte getCode() { return code; } public String getName() { return name; }}使用enum来控制范围,避免Factory在获取Bean时发生异常。package com.mengyunzhi.demo.dynamicautowire;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Service;import java.util.HashMap;import java.util.List;import java.util.Map;/* * 说话工厂 /@Servicepublic class SayHelloFactoryImpl implements SayHelloFactory { /* * BEAN列表 / private final Map<Byte, SayHelloService> servicesByCode = new HashMap<>(); /* * 国家代码 / private CountryCode countryCode = CountryCode.CHINA; @Override public void setCountryCode(CountryCode countryCode) { this.countryCode = countryCode; } /* * 初始化 * * @param sayHelloServices spring获取到的所以实现了SpeakService的BEAN / @Autowired public void init(List<SayHelloService> sayHelloServices) { for (SayHelloService sayHelloService : sayHelloServices) { this.register(sayHelloService.getCode(), sayHelloService); } } /* * 注册Bean * * @param code 代码 * @param sayHelloService BEAN / private void register(Byte code, SayHelloService sayHelloService) { this.servicesByCode.put(code, sayHelloService); } /* * 获取BEAN * * @return 对应的SayHelloService BEAN / @Override public SayHelloService getSayHelloService() { return this.servicesByCode.get(this.countryCode.getCode()); }}增加Map<Byte, SayHelloService> servicesByCode来存储对应国家的SayHelloServiceBEAN。增加getSayHelloService()来根据当前国家代码来返回相应的Bean。SayHelloServicepackage com.mengyunzhi.demo.dynamicautowire;/* * 说话 /public interface SayHelloService { void sayHello(); Byte getCode();}将sayHello()方法抽离,getCode()以获取国家代码。中国人你好package com.mengyunzhi.demo.dynamicautowire;import org.springframework.stereotype.Component;/* * 中国话 /@Componentpublic class SayHelloServiceChineseImpl implements SayHelloService { @Override public void sayHello() { System.out.println(“您好”); } @Override public Byte getCode() { return CountryCode.CHINA.getCode(); }}美国人Hellopackage com.mengyunzhi.demo.dynamicautowire;import org.springframework.stereotype.Component;/* * 美国话 /@Componentpublic class SayHelloServiceEnglishImpl implements SayHelloService { @Override public void sayHello() { System.out.println(“hello”); } @Override public Byte getCode() { return CountryCode.USA.getCode(); }}测试package com.mengyunzhi.demo.dynamicautowire;import org.junit.Test;import org.junit.runner.RunWith;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.boot.test.context.SpringBootTest;import org.springframework.test.context.junit4.SpringRunner;@SpringBootTest@RunWith(SpringRunner.class)public class SpeakServiceImplTest { @Autowired SpeakService speakService; @Autowired SayHelloFactory sayHelloFactory; @Test public void sayHello() { // 默认说你好 speakService.sayHello(); // 将国家设置为美国,再说你好 sayHelloFactory.setCountryCode(CountryCode.USA); speakService.sayHello(); // 将国家设置为中国,再说你好 sayHelloFactory.setCountryCode(CountryCode.CHINA); speakService.sayHello(); }}您好hello您好时序图增加德国人增加德国人SayHelloServiceGermanyImpl.在CountryCode中,增加德国.package com.mengyunzhi.demo.dynamicautowire;import org.springframework.stereotype.Component;@Componentpublic class SayHelloServiceGermanyImpl implements SayHelloService { @Override public void sayHello() { System.out.println(“Hallo”); } @Override public Byte getCode() { return CountryCode.GERMANY.getCode(); }}package com.mengyunzhi.demo.dynamicautowire;/* * 国家代码 */public enum CountryCode { CHINA((byte) 0, “中国”), USA((byte) 1, “美国”), GERMANY((byte) 2, “德国”); private Byte code; private String name; CountryCode(Byte code, String name) { this.code = code; this.name = name; } public Byte getCode() { return code; } public String getName() { return name; }}单元测试 @Test public void sayHello1() { // 默认说你好 speakService.sayHello(); // 将国家设置为美国,再说你好 sayHelloFactory.setCountryCode(CountryCode.USA); speakService.sayHello(); // 将国家设置为德国,再说你好 sayHelloFactory.setCountryCode(CountryCode.GERMANY); speakService.sayHello(); // 将国家设置为中国,再说你好 sayHelloFactory.setCountryCode(CountryCode.CHINA); speakService.sayHello(); }测试结果如下:您好helloHallo您好总结在解决问题时,只所有我们看的不够远,可能是由于自己站的不够高。同样的问题,困惑我了多日,直到近期系统的学习设计模式 、angular官方教程、Spring 实战后,结合近期项目变更带来的新需求,才在使用设计模式解决此问题上有所启发。欲穷千里目,更上一层楼 唐·王之涣·《登鹳雀楼》 ...

March 27, 2019 · 2 min · jiezi

Spring 中优雅的获取泛型信息

简介Spring 源码是个大宝库,我们能遇到的大部分工具在源码里都能找到,所以笔者开源的 mica 完全基于 Spring 进行基础增强,不重复造轮子。今天我要分享的是在 Spring 中优雅的获取泛型。获取泛型自己解析我们之前的处理方式,代码来源 vjtools(江南白衣)。/** * 通过反射, 获得Class定义中声明的父类的泛型参数的类型. * * 注意泛型必须定义在父类处. 这是唯一可以通过反射从泛型获得Class实例的地方. * * 如无法找到, 返回Object.class. * * 如public UserDao extends HibernateDao<User,Long> * * @param clazz clazz The class to introspect * @param index the Index of the generic declaration, start from 0. * @return the index generic declaration, or Object.class if cannot be determined */public static Class getClassGenericType(final Class clazz, final int index) { Type genType = clazz.getGenericSuperclass(); if (!(genType instanceof ParameterizedType)) { logger.warn(clazz.getSimpleName() + “’s superclass not ParameterizedType”); return Object.class; } Type[] params = ((ParameterizedType) genType).getActualTypeArguments(); if ((index >= params.length) || (index < 0)) { logger.warn(“Index: " + index + “, Size of " + clazz.getSimpleName() + “’s Parameterized Type: " + params.length); return Object.class; } if (!(params[index] instanceof Class)) { logger.warn(clazz.getSimpleName() + " not set the actual class on superclass generic parameter”); return Object.class; } return (Class) params[index];}ResolvableType 工具从 Spring 4.0 开始 Spring 中添加了 ResolvableType 工具,这个类可以更加方便的用来回去泛型信息。首先我们来看看官方示例:private HashMap<Integer, List<String>> myMap;public void example() { ResolvableType t = ResolvableType.forField(getClass().getDeclaredField(“myMap”)); t.getSuperType(); // AbstractMap<Integer, List<String>> t.asMap(); // Map<Integer, List<String>> t.getGeneric(0).resolve(); // Integer t.getGeneric(1).resolve(); // List t.getGeneric(1); // List<String> t.resolveGeneric(1, 0); // String}详细说明构造获取 Field 的泛型信息ResolvableType.forField(Field)构造获取 Method 的泛型信息ResolvableType.forMethodParameter(Method, int)构造获取方法返回参数的泛型信息ResolvableType.forMethodReturnType(Method)构造获取构造参数的泛型信息ResolvableType.forConstructorParameter(Constructor, int)构造获取类的泛型信息ResolvableType.forClass(Class)构造获取类型的泛型信息ResolvableType.forType(Type)构造获取实例的泛型信息ResolvableType.forInstance(Object)更多使用 Api 请查看,ResolvableType java doc: https://docs.spring.io/spring…开源推荐Spring boot 微服务高效开发 mica 工具集:https://gitee.com/596392912/micaAvue 一款基于vue可配置化的神奇框架:https://gitee.com/smallweigit/avuepig 宇宙最强微服务(架构师必备):https://gitee.com/log4j/pigSpringBlade 完整的线上解决方案(企业开发必备):https://gitee.com/smallc/SpringBladeIJPay 支付SDK让支付触手可及:https://gitee.com/javen205/IJPay加入【如梦技术】Spring QQ群:479710041,了解更多。关注我们扫描上面二维码,更多精彩内容每天推荐! ...

March 27, 2019 · 1 min · jiezi

Spring Boot 2.x基础教程:快速入门未指定标题的文章

简介在您第1次接触和学习Spring框架的时候,是否因为其繁杂的配置而退却了?在你第n次使用Spring框架的时候,是否觉得一堆反复黏贴的配置有一些厌烦?那么您就不妨来试试使用Spring Boot来让你更易上手,更简单快捷地构建Spring应用!Spring Boot让我们的Spring应用变的更轻量化。我们不必像以前那样繁琐的构建项目、打包应用、部署到Tomcat等应用服务器中来运行我们的业务服务。通过Spring Boot实现的服务,只需要依靠一个Java类,把它打包成jar,并通过java -jar命令就可以运行起来。这一切相较于传统Spring应用来说,已经变得非常的轻便、简单。总结一下Spring Boot的主要优点:为所有Spring开发者更快的入门开箱即用,提供各种默认配置来简化项目配置内嵌式容器简化Web项目没有冗余代码生成和XML配置的要求快速入门本文我们将学习如何快速的创建一个Spring Boot应用,并且实现一个简单的Http请求处理。通过这个例子对Spring Boot有一个初步的了解,并体验其结构简单、开发快速的特性。创建基础项目Spring官方提供了非常方便的工具Spring Initializr来帮助我们创建Spring Boot应用。使用Spring Initializr页面创建第一步:访问Spring Initializr:https://start.spring.io/如图所示,几个选项说明:Project:使用什么构建工具,Maven还是Gradle;本教程将采用大部分Java人员都熟悉的Maven,以方便更多读者入门学习。Language:使用什么编程语言,Java、Kotlin还是Groovy;本教程将采用Java为主编写,以方便更多读者入门学习。Spring Boot:选用的Spring Boot版本;这里将使用当前最新的2.1.3版本。Project Metadata:项目的元数据;其实就是Maven项目的基本元素,点开More options可以看到更多设置,根据自己组织的情况输入相关数据,比如:Dependencies:选择要加入的Spring Boot组件;本文将实现一个Http接口,所以可以选择Web组件,只需要输入Web,页面会自动联想显示匹配的可选组件:点击”+“之后,就如下图所示:第二步:点击”Generate Project“按钮生成项目;此时浏览器会下载一个与上面Artifact名称一样的压缩包。第三步:解压项目包,并用编译器以Maven项目导入,以IntelliJ IDEA为例:菜单中选择:File –> New –> Project from Existing Sources…选择解压后的项目文件夹,点击OK点击:Import project from external model,并选择Maven,点击Next到底为止。若你的环境有多个版本的JDK,注意到选择Java SDK的时候请选择Java 8(具体根据你在第一步中选择的Java版本为准)由于我们后续会有很多样例工程,您也可以像我们样例仓库那样,用一个基础仓库,每篇文章的样例以模块的方式保存,具体形式可见文末的案例仓库。使用IntelliJ IDEA创建如果是使用IntelliJ IDEA来写Java程序的话,那么还可以直接在编译器中创建Spring Boot应用。第一步:菜单栏中选择:File => New => Project..,我们可以看到如下图所示的创建功能窗口。其中Initial Service Url指向的地址就是Spring官方提供的Spring Initializr工具地址,所以这里创建的工程实际上也是基于它的Web工具来实现的。第二步:点击Next,等待片刻后,我们可以看到如下图所示的工程信息窗口:其实内容就跟我们用Web版的Spring Initializr是一模一样的,跟之前在页面上一样填写即可。第三步:继续点击Next,进入选择Spring Boot版本和依赖管理的窗口:在这里值的我们关注的是,它不仅包含了Spring Boot Starter POMs中的各个依赖,还包含了Spring Cloud的各种依赖。第四步:点击Next,进入最后关于工程物理存储的一些细节。最后,点击Finish就能完成工程的构建了。Intellij中的Spring Initializr虽然还是基于官方Web实现,但是通过工具来进行调用并直接将结果构建到我们的本地文件系统中,让整个构建流程变得更加顺畅,还没有体验过此功能的Spring Boot/Cloud爱好者们不妨可以尝试一下这种不同的构建方式。项目结构解析通过上面步骤完成了基础项目的创建。如上图所示,Spring Boot的基础结构共三个文件(具体路径根据用户生成项目时填写的Group所有差异):src/main/java下的程序入口:Chapter11Applicationsrc/main/resources下的配置文件:application.propertiessrc/test/下的测试入口:Chapter11ApplicationTests生成的Chapter11Application和Chapter11ApplicationTests类都可以直接运行来启动当前创建的项目,由于目前该项目未配合任何数据访问或Web模块,程序会在加载完Spring之后结束运行。项目依赖解析打开pom.xml,一起来看看Spring Boot项目的依赖:<?xml version=“1.0” encoding=“UTF-8”?><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> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.1.3.RELEASE</version> <relativePath/> <!– lookup parent from repository –> </parent> <groupId>com.didispace</groupId> <artifactId>chapter1-1</artifactId> <version>0.0.1-SNAPSHOT</version> <name>chapter1-1</name> <description>Demo project for Spring Boot</description> <properties> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build></project>如上所示,主要有四个部分:项目元数据:创建时候输入的Project Metadata部分,也就是Maven项目的基本元素,包括:groupId、artifactId、version、name、description等parent:继承spring-boot-starter-parent的依赖管理,控制版本与打包等内容dependencies:项目具体依赖,这里包含了spring-boot-starter-web用于实现HTTP接口(该依赖中包含了Spring MVC);spring-boot-starter-test用于编写单元测试的依赖包。更多功能模块的使用我们将在后面的教程中逐步展开。build:构建配置部分。默认使用了spring-boot-maven-plugin,配合spring-boot-starter-parent就可以把Spring Boot应用打包成JAR来直接运行。编写一个HTTP接口创建package命名为com.didispace.web(根据实际情况修改)创建HelloController类,内容如下:@RestControllerpublic class HelloController { @RequestMapping("/hello”) public String index() { return “Hello World”; }}启动主程序,使用PostMan等工具发起请求:http://localhost:8080/hello,可以看到页面返回:Hello World编写单元测试用例打开的src/test/下的测试入口Chapter11ApplicationTests类。下面编写一个简单的单元测试来模拟http请求,具体如下:import static org.hamcrest.Matchers.equalTo;import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;@RunWith(SpringRunner.class)@SpringBootTestpublic class Chapter11ApplicationTests { private MockMvc mvc; @Before public void setUp() throws Exception { mvc = MockMvcBuilders.standaloneSetup(new HelloController()).build(); } @Test public void getHello() throws Exception { mvc.perform(MockMvcRequestBuilders.get("/hello”).accept(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) .andExpect(content().string(equalTo(“Hello World”))); }}使用MockServletContext来构建一个空的WebApplicationContext,这样我们创建的HelloController就可以在@Before函数中创建并传递到MockMvcBuilders.standaloneSetup()函数中。注意引入下面内容,让status、content、equalTo函数可用import static org.hamcrest.Matchers.equalTo;import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;至此已完成目标,通过Maven构建了一个空白Spring Boot项目,再通过引入web模块实现了一个简单的请求处理。代码示例本文的相关例子可以查看下面仓库中的chapter1-1目录:Github:https://github.com/dyc87112/SpringBoot-Learning/tree/2.xGitee:https://gitee.com/didispace/SpringBoot-Learning/tree/2.x如果您觉得本文不错,欢迎Star支持,您的关注是我坚持的动力! ...

March 26, 2019 · 1 min · jiezi

Spring Boot 2 - 初识与新工程的创建

Spring Boot的由来相信大家都听说过Spring框架。Spring从诞生到现在一直是流行的J2EE开发框架。随着Spring的发展,它的功能越来越强大,随之而来的缺点也越来越明显,以至于发展到后来变得越来越臃肿,使用起来也非常的麻烦。到后来由于过于强调配置的灵活性,有时即使只为了加入一个简单的特性,而需要相当多的XML配置,从而被人们诟病为"配置地狱"!后来许多优秀的服务端框架涌现出来,比如基于JavaScript的nodeJS,基于Python的Django,Flask,Tornado框架。都由于其使用简单的特性被越来越多的开发者采用。Sprint Boot就是为了应对这些框架的挑战而出现的,它彻底改变了Spring框架臃肿的现状。使得J2EE的框架变得简单起来,目前越来越多的公司和项目选择了它。Spring Boot最新的版本是2.x,本文我们就来介绍它的安装与配置,快速创建你的第一个Spring Boot工程,享受她的优雅与强大。Spring Boot的特性Spring Boot的主要有以下几个杀手级特性,可以大大减少学习与使用的复杂性,让我们更多地关注业务,提升开发效率:可创建独立可运行的应用程序,打包后仅一个jar包,运行即可。内置应用服务器Tomcat,Jetty等,无需部署。零XML配置,彻底摆脱"配置地狱"。自动配置各种第三方库,常用的第三方库引入即可用。内置各种服务监控系统,实时观察服务运行状态。创建Spring Boot工程我们废话不多说,现在就开始介绍创建Spring Boot 2工程的方法,这是进行Spring Boot学习与开发的第一步。方法一:通过Idea内置工具创建如果你使用IntelliJ IDEA作为你的开发IDE的话,这种方式最为方便,不过前提是使用Ultimate版(最终版),在IntelliJ的官网可以下载到(当然如果条件允许推荐购买正版)。打开Idea选择创建新工程选择导航栏中的Spring Initializr然后填入工程信息注意这里有使用Maven还是Gradle的选择。我们这里既然要零XML配置,这里选择使用Gradle工程,如图。我们使用Sprint Boot的目的也就是简化我们的开发生活,不是吗?添加第三方依赖我们这里添加需要的第三方依赖。如果你第一次接触Spring Boot,为了避免复杂性,可以选择添加以下两个依赖。其他的依赖不必担心,你可以在任何时候非常容易地添加依赖。DevTools:是一系列开发工具配置,比如热部署。Web: 对Web开发的基础支持。完成工程创建填入工程名和保存目录后,点击完成。创建完工程后,会有一个gradle配置的一个界面,这里我们选择使用默认的wrapper。这个选项会自动为我们下载对应版本的gradle进行配置和编译,无需我们自己安装配置等,非常方便。点击OK后我们就成功地创建了新工程!恭喜!方法二:通过Spring Initializr创建这种方式适用于不使用IntelliJ IDEA和使用免费版Idea的同学,通过官方创建Spring Boot工程的网站直接创建。方法一其实也是使用这个网站作为模板来集成到Idea中的。点击这里进入到这个网站(https://start.spring.io/)输入工程信息,并选择Gradle工程输入工程的信息后,如果需要更详细的信息设置,可以点击下方的"More options"按钮进行设置。添加依赖这里我们可以直接搜索需要的依赖进行添加,比如我们添加Web和Devtools库。生成工程在我们把所有信息填完后,接下来我们就可以点击页面底部的按钮(Generate Project)开始生成。生成后会自动把工程下载到本地,我们解压后,将该工程保存到开发目录(你喜欢的任何位置都可以),然后使用IDE打开即可。比如我这里使用的是IntelliJ IDEA,打开即可。运行工程!至此我们的工程已经创建完毕,下面就是运行它了。我们观察工程源码包的结构,发现有一个Hellospringboot2Application的类,这个类就是我们服务的运行入口。运行它后,我们的服务就可以正常启动了!总结通过创建Spring Boot新工程的过程,我们就会发现它的简洁之处,不会像以前使用Spring那样要花费很多时间和精力去创建和配置,我们现在甚至可以在短短的两分钟之内创建好工程!后面的文章我们会深入讨论Spring Boot的方方面面。我的博客中其他关于Spring Boot的所有文章可以点击这里找到,欢迎关注!如果有问题可以留言,或者给我发邮件lloyd@examplecode.cn,期待我们共同学习与成长!

March 19, 2019 · 1 min · jiezi

SpringBoot | @Value 和 @ConfigurationProperties 的区别

微信公众号:一个优秀的废人。如有问题,请后台留言,反正我也不会听。前言最近有跳槽的想法,所以故意复习了下 SpringBoot 的相关知识,复习得比较细。其中有些,我感觉是以前忽略掉的东西,比如 @Value 和 @ConfigurationProperties 的区别 。如何使用定义两个对象,一个学生对象,对应着一个老师对象,代码如下:@ConfigurationProperties学生类@Component@ConfigurationProperties(prefix = “student”) // 指定配置文件中的 student 属性与这个 bean绑定public class Student { private String firstName; private String lastName; private Integer age; private String gender; private String city; private Teacher teacher; private List<String> hobbys; private Map<String,Integer> scores; //注意,为了测试必须重写 toString 和 get,set 方法}老师类public class Teacher { private String name; private Integer age; private String gender; //注意,为了测试必须重写 toString 和 get,set 方法}测试类@RunWith(SpringRunner.class)@SpringBootTestpublic class SpringbootValConproDemoApplicationTests { @Autowired private Student student; @Test public void contextLoads() { // 这里为了方便,但工作中千万不能用 System.out System.out.println(student.toString()); }}输出结果Student{firstName=‘陈’, lastName=‘一个优秀的废人’, age=24, gender=‘男’, city=‘广州’, teacher=Teacher{name=‘eses’, age=24, gender=‘女’}, hobbys=[篮球, 羽毛球, 兵兵球], scores={java=100, Python=99, C=99}}@Value@Value 支持三种取值方式,分别是 字面量、${key}从环境变量、配置文件中获取值以及 #{SpEL}学生类@Component//@ConfigurationProperties(prefix = “student”) // 指定配置文件中的 student 属性与这个 bean绑定public class Student { /** * <bean class=“Student”> * <property name=“lastName” value=“字面量/${key}从环境变量、配置文件中获取值/#{SpEL}"></property> * <bean/> / @Value(“陈”) // 字面量 private String firstName; @Value("${student.lastName}”) // 从环境变量、配置文件中获取值 private String lastName; @Value("#{122}") // #{SpEL} private Integer age; private String gender; private String city; private Teacher teacher; private List<String> hobbys; private Map<String,Integer> scores; //注意,为了测试必须重写 toString 和 get,set 方法}测试结果Student{firstName=‘陈’, lastName=‘一个优秀的废人’, age=24, gender=‘null’, city=‘null’, teacher=null, hobbys=null, scores=null}区别二者区别@ConfigurationProperties@Value功能批量注入配置文件中的属性一个个指定松散绑定(松散语法)支持不支持SpEL不支持支持JSR303数据校验支持不支持复杂类型封装支持不支持从上表可以看见,@ConfigurationProperties 和 @Value 主要有 5 个不同,其中第一个功能上的不同,上面已经演示过。下面我来介绍下剩下的 4 个不同。松散语法松散语法的意思就是一个属性在配置文件中可以有多个属性名,举个栗子:学生类当中的 firstName 属性,在配置文件中可以叫 firstName、first-name、first_name 以及 FIRST_NAME。 而 @ConfigurationProperties 是支持这种命名的,@Value 不支持。下面以 firstName 为例,测试一下。如下代码:@ConfigurationProperties学生类的 firstName 属性在 yml 文件中被定义为 first_name:student: first_name: 陈 # 学生类的 firstName 属性在 yml 文件中被定义为 first_name lastName: 一个优秀的废人 age: 24 gender: 男 city: 广州 teacher: {name: eses,age: 24,gender: 女} hobbys: [篮球,羽毛球,兵兵球] scores: {java: 100,Python: 99,C++: 99}学生类:@Component@ConfigurationProperties(prefix = “student”) // 指定配置文件中的 student 属性与这个 bean绑定public class Student { /** * <bean class=“Student”> * <property name=“lastName” value=“字面量/${key}从环境变量、配置文件中获取值/#{SpEL}"></property> * <bean/> / //@Value(“陈”) // 字面量 private String firstName; //@Value("${student.lastName}”) // 从环境变量、配置文件中获取值 private String lastName; //@Value("#{122}") // #{SpEL} private Integer age; private String gender; private String city; private Teacher teacher; private List<String> hobbys; private Map<String,Integer> scores; //注意,为了测试必须重写 toString 和 get,set 方法}测试结果:Student{firstName=‘陈’, lastName=‘一个优秀的废人’, age=24, gender=‘男’, city=‘广州’, teacher=Teacher{name=‘eses’, age=24, gender=‘女’}, hobbys=[篮球, 羽毛球, 兵兵球], scores={java=100, Python=99, C=99}}@Value学生类:@Component//@ConfigurationProperties(prefix = “student”) // 指定配置文件中的 student 属性与这个 bean绑定public class Student { /** * <bean class=“Student”> * <property name=“lastName” value=“字面量/${key}从环境变量、配置文件中获取值/#{SpEL}"></property> * <bean/> / //@Value(“陈”) // 字面量 @Value("${student.firstName}”) private String firstName; //@Value("${student.lastName}") // 从环境变量、配置文件中获取值 private String lastName; //@Value("#{122}") // #{SpEL} private Integer age; private String gender; private String city; private Teacher teacher; private List<String> hobbys; private Map<String,Integer> scores; //注意,为了测试必须重写 toString 和 get,set 方法}测试结果:启动报错,找不到 bean。从上面两个测试结果可以看出,使用 @ConfigurationProperties 注解时,yml 中的属性名为 last_name 而学生类中的属性为 lastName 但依然能取到值,而使用 @value 时,使用 lastName 确报错了。证明 @ConfigurationProperties 支持松散语法,@value 不支持。SpELSpEL 使用 #{…} 作为定界符 , 所有在大括号中的字符都将被认为是 SpEL , SpEL 为 bean 的属性进行动态赋值提供了便利。@Value如上述介绍 @Value 注解使用方法时,有这样一段代码:@Value("#{122}") // #{SpEL}private Integer age;证明 @Value 是支持 SpEL 表达式的。@ConfigurationProperties由于 yml 中的 # 被当成注释看不到效果。所以我们新建一个 application.properties 文件。把 yml 文件内容注释,我们在 properties 文件中把 age 属性写成如下所示:student.age=#{122}把学生类中的 @ConfigurationProperties 注释打开,注释 @value 注解。运行报错, age 属性匹配异常。说明 @ConfigurationProperties 不支持 SpELJSR303 数据校验@Value加入 @Length 校验:@Component@Validated//@ConfigurationProperties(prefix = “student”) // 指定配置文件中的 student 属性与这个 bean绑定public class Student { /** * <bean class=“Student”> * <property name=“lastName” value=“字面量/${key}从环境变量、配置文件中获取值/#{SpEL}"></property> * <bean/> / //@Value(“陈”) // 字面量 @Value("${student.first-name}”) @Length(min=5, max=20, message=“用户名长度必须在5-20之间”) private String firstName; //@Value("${student.lastName}") // 从环境变量、配置文件中获取值 private String lastName; //@Value("#{122}") // #{SpEL} private Integer age; private String gender; private String city; private Teacher teacher; private List<String> hobbys; private Map<String,Integer> scores;}yaml:student: first_name: 陈测试结果:Student{firstName=‘陈’, lastName=‘null’, age=null, gender=‘null’, city=‘null’, teacher=null, hobbys=null, scores=null}yaml 中的 firstname 长度为 1 。而检验规则规定 5-20 依然能取到属性,说明检验不生效,@Value 不支持 JSR303 数据校验@ConfigurationProperties学生类:@Component@Validated@ConfigurationProperties(prefix = “student”) // 指定配置文件中的 student 属性与这个 bean绑定public class Student { /** * <bean class=“Student”> * <property name=“lastName” value=“字面量/${key}从环境变量、配置文件中获取值/#{SpEL}"></property> * <bean/> / //@Value(“陈”) // 字面量 //@Value("${student.first-name}”) @Length(min=5, max=20, message=“用户名长度必须在5-20之间”) private String firstName; //@Value("${student.lastName}") // 从环境变量、配置文件中获取值 private String lastName; //@Value("#{122}") // #{SpEL} private Integer age; private String gender; private String city; private Teacher teacher; private List<String> hobbys; private Map<String,Integer> scores;}测试结果:报错[firstName],20,5]; default message [用户名长度必须在5-20之间]校验生效,支持 JSR303 数据校验。复杂类型封装复杂类型封装指的是,在对象以及 map (如学生类中的老师类以及 scores map)等属性中,用 @Value 取是取不到值,比如:@Component//@Validated//@ConfigurationProperties(prefix = “student”) // 指定配置文件中的 student 属性与这个 bean绑定public class Student { /** * <bean class=“Student”> * <property name=“lastName” value=“字面量/${key}从环境变量、配置文件中获取值/#{SpEL}"></property> * <bean/> / //@Value(“陈”) // 字面量 //@Value("${student.first-name}”) //@Length(min=5, max=20, message=“用户名长度必须在5-20之间”) private String firstName; //@Value("${student.lastName}") // 从环境变量、配置文件中获取值 private String lastName; //@Value("#{122}") // #{SpEL} private Integer age; private String gender; private String city; @Value("${student.teacher}") private Teacher teacher; private List<String> hobbys; @Value("${student.scores}") private Map<String,Integer> scores;}这样取是报错的。而上文介绍 @ConfigurationProperties 和 @Value 的使用方法时已经证实 @ConfigurationProperties 是支持复杂类型封装的。也就是说 yaml 中直接定义 teacher 以及 scores 。 @ConfigurationProperties 依然能取到值。怎么选用?如果说,只是在某个业务逻辑中需要获取一下配置文件中的某项值,使用 @Value;比如,假设现在学生类加多一个属性叫 school 那这个属性对于该校所有学生来说都是一样的,但防止我这套系统到了别的学校就用不了了。那我们可以直接在 yml 中给定 school 属性,用 @Value 获取。当然上述只是举个粗暴的例子,实际开发时,school 属性应该是保存在数据库中的。如果说,专门编写了一个 javaBean 来和配置文件进行映射,我们就直接使用 @ConfigurationProperties。完整代码https://github.com/turoDog/Demo/tree/master/springboot_val_conpro_demo如果觉得对你有帮助,请给个 Star 再走呗,非常感谢。后语如果本文对你哪怕有一丁点帮助,请帮忙点好看。你的好看是我坚持写作的动力。另外,关注之后在发送 1024 可领取免费学习资料。资料详情请看这篇旧文:Python、C++、Java、Linux、Go、前端、算法资料分享 ...

March 17, 2019 · 4 min · jiezi

Spring Boot 记录 Http 请求日志

在使用Spring Boot开发 web api 的时候希望把 request,request header ,response reponse header , uri, method 等等的信息记录到我们的日志中,方便我们排查问题,也能对系统的数据做一些统计。Spring 使用了 DispatcherServlet 来拦截并分发请求,我们只要自己实现一个 DispatcherServlet 并在其中对请求和响应做处理打印到日志中即可。我们实现一个自己的分发 Servlet ,它继承于 DispatcherServlet,我们实现自己的 doDispatch(HttpServletRequest request, HttpServletResponse response) 方法。public class LoggableDispatcherServlet extends DispatcherServlet { private static final Logger logger = LoggerFactory.getLogger(“HttpLogger”); private static final ObjectMapper mapper = new ObjectMapper(); @Override protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception { ContentCachingRequestWrapper requestWrapper = new ContentCachingRequestWrapper(request); ContentCachingResponseWrapper responseWrapper = new ContentCachingResponseWrapper(response); //创建一个 json 对象,用来存放 http 日志信息 ObjectNode rootNode = mapper.createObjectNode(); rootNode.put(“uri”, requestWrapper.getRequestURI()); rootNode.put(“clientIp”, requestWrapper.getRemoteAddr()); rootNode.set(“requestHeaders”, mapper.valueToTree(getRequestHeaders(requestWrapper))); String method = requestWrapper.getMethod(); rootNode.put(“method”, method); try { super.doDispatch(requestWrapper, responseWrapper); } finally { if(method.equals(“GET”)) { rootNode.set(“request”, mapper.valueToTree(requestWrapper.getParameterMap())); } else { JsonNode newNode = mapper.readTree(requestWrapper.getContentAsByteArray()); rootNode.set(“request”, newNode); } rootNode.put(“status”, responseWrapper.getStatus()); JsonNode newNode = mapper.readTree(responseWrapper.getContentAsByteArray()); rootNode.set(“response”, newNode); responseWrapper.copyBodyToResponse(); rootNode.set(“responseHeaders”, mapper.valueToTree(getResponsetHeaders(responseWrapper))); logger.info(rootNode.toString()); } } private Map<String, Object> getRequestHeaders(HttpServletRequest request) { Map<String, Object> headers = new HashMap<>(); Enumeration<String> headerNames = request.getHeaderNames(); while (headerNames.hasMoreElements()) { String headerName = headerNames.nextElement(); headers.put(headerName, request.getHeader(headerName)); } return headers; } private Map<String, Object> getResponsetHeaders(ContentCachingResponseWrapper response) { Map<String, Object> headers = new HashMap<>(); Collection<String> headerNames = response.getHeaderNames(); for (String headerName : headerNames) { headers.put(headerName, response.getHeader(headerName)); } return headers; }在 LoggableDispatcherServlet 中,我们可以通过 HttpServletRequest 中的 InputStream 或 reader 来获取请求的数据,但如果我们直接在这里读取了流或内容,到后面的逻辑将无法进行下去,所以需要实现一个可以缓存的 HttpServletRequest。好在 Spring 提供这样的类,就是 ContentCachingRequestWrapper 和 ContentCachingResponseWrapper, 根据官方的文档这两个类正好是来干这个事情的,我们只要将 HttpServletRequest 和 HttpServletResponse 转化即可。HttpServletRequest wrapper that caches all content read from the input stream and reader, and allows this content to be retrieved via a byte array.Used e.g. by AbstractRequestLoggingFilter. Note: As of Spring Framework 5.0, this wrapper is built on the Servlet 3.1 API.HttpServletResponse wrapper that caches all content written to the output stream and writer, and allows this content to be retrieved via a byte array.Used e.g. by ShallowEtagHeaderFilter. Note: As of Spring Framework 5.0, this wrapper is built on the Servlet 3.1 API.实现好我们的 LoggableDispatcherServlet后,接下来就是要指定使用 LoggableDispatcherServlet 来分发请求。@SpringBootApplicationpublic class SbDemoApplication implements ApplicationRunner { public static void main(String[] args) { SpringApplication.run(SbDemoApplication.class, args); } @Bean public ServletRegistrationBean dispatcherRegistration() { return new ServletRegistrationBean(dispatcherServlet()); } @Bean(name = DispatcherServletAutoConfiguration.DEFAULT_DISPATCHER_SERVLET_BEAN_NAME) public DispatcherServlet dispatcherServlet() { return new LoggableDispatcherServlet(); }}增加一个简单的 Controller 来测试一下@RestController@RequestMapping("/hello")public class HelloController { @RequestMapping(value = “/word”, method = RequestMethod.POST) public Object hello(@RequestBody Object object) { return object; }}使用 curl 发送一个 Post 请求:$ curl –header “Content-Type: application/json” \ –request POST \ –data ‘{“username”:“xyz”,“password”:“xyz”}’ \ http://localhost:8080/hello/word{“username”:“xyz”,“password”:“xyz”}查看打印的日志:{ “uri”:"/hello/word", “clientIp”:“0:0:0:0:0:0:0:1”, “requestHeaders”:{ “content-length”:“35”, “host”:“localhost:8080”, “content-type”:“application/json”, “user-agent”:“curl/7.54.0”, “accept”:"/" }, “method”:“POST”, “request”:{ “username”:“xyz”, “password”:“xyz” }, “status”:200, “response”:{ “username”:“xyz”, “password”:“xyz” }, “responseHeaders”:{ “Content-Length”:“35”, “Date”:“Sun, 17 Mar 2019 08:56:50 GMT”, “Content-Type”:“application/json;charset=UTF-8” }}当然打印出来是在一行中的,我进行了一下格式化。我们还可以在日志中增加请求的时间,耗费的时间以及异常信息等。 ...

March 17, 2019 · 2 min · jiezi

spring boot学习(4): 命令行启动

在使用spring boot 构建应用启动时,我们在工作中都是通过命令行来启动应用,有时候会需要一些特定的参数以在应用启动时,做一些初始化的操作。spring boot 提供了 CommandLineRunner 和 ApplicationRunner 这两个接口供用户使用。1. CommandLineRunner1.1 声明:@FunctionalInterfacepublic interface CommandLineRunner { /** * Callback used to run the bean. * @param args incoming main method arguments * @throws Exception on error / void run(String… args) throws Exception;}1.2 使用:package com.example.consoleapplication;import org.springframework.boot.CommandLineRunner;import org.springframework.stereotype.Component;@Componentpublic class TestRunner implements CommandLineRunner { @Override public void run(String… args) { // Do something… for(String arg: args){ System.out.println(arg); } System.out.print(“test command runner”); }}1.3 运行结果运行: java -jar build/libs/consoleapplication-0.0.1-SNAPSHOT.jar -sdfsaf sdfas,结果如下:2019-03-16 17:31:56.544 INFO 18679 — [ main] c.e.consoleapplication.DemoApplication : No active profile set, falling back to default profiles: default2019-03-16 17:31:57.195 INFO 18679 — [ main] c.e.consoleapplication.DemoApplication : Started DemoApplication in 16.172 seconds (JVM running for 16.65)-sdfsafsdfastest command runner%2. ApplicationRunner2.1 声明/* * Interface used to indicate that a bean should <em>run</em> when it is contained within * a {@link SpringApplication}. Multiple {@link ApplicationRunner} beans can be defined * within the same application context and can be ordered using the {@link Ordered} * interface or {@link Order @Order} annotation. * * @author Phillip Webb * @since 1.3.0 * @see CommandLineRunner /@FunctionalInterfacepublic interface ApplicationRunner { /* * Callback used to run the bean. * @param args incoming application arguments * @throws Exception on error */ void run(ApplicationArguments args) throws Exception;}2.2 使用ApplicationRunner 和 CommandLineRunner 的使用是有差别的:CommandLineRunner 的使用,只是把参数根据空格分割。ApplicationRunner 会根据 是否匹配 –key=value 来解析参数,能匹配,则为 optional 参数, 可用getOptionValues获取参数值。不匹配则是 non optional 参数。package com.example.consoleapplication;import org.springframework.boot.ApplicationRunner;import org.springframework.stereotype.Component;import org.springframework.boot.ApplicationArguments;@Componentpublic class TestApplicationRunner implements ApplicationRunner { @Override public void run(ApplicationArguments args) throws Exception { // Do something… System.out.println(“option arg names” + args.getOptionNames()); System.out.println(“non option+” + args.getNonOptionArgs()); }}2.3 运行结果运行命令 java -jar build/libs/consoleapplication-0.0.1-SNAPSHOT.jar -non1 non2 –option=1, 结果为:2019-03-16 18:08:08.528 INFO 19778 — [ main] c.e.consoleapplication.DemoApplication : No active profile set, falling back to default profiles: default2019-03-16 18:08:09.166 INFO 19778 — [ main] c.e.consoleapplication.DemoApplication : Started DemoApplication in 16.059 seconds (JVM running for 16.56)testoption arg names[option]non option+[-non1, non2]-non1non2–option=1test%可以看到, optional 参数名有 option, non optional 参数有 -non1 和 non23. 小结CommandLineRunner 和 ApplicationRunner 都能实现命令行应用启动时根据参数获取我们需要的值,做特殊的逻辑。但两者有所不同,推荐使用 ApplicationRunner 的 optional 参数, 方便扩展。4. 参考文档https://docs.spring.io/spring… ...

March 16, 2019 · 2 min · jiezi

【西瓜皮】Spring Boot 2.x 整合 Redis(一)

Spring Boot 2 整合 Redis(一)Spring Boot 2.0.3简单整合RedisIDEA Spring Initialzr 创建工程:选上Redis依赖项Maven依赖 // Spring Boot 1.5版本的依赖下artifactId是没有data的 <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>application.yml文件的配置,其中Jedis配置有默认值,Spring Boot 2后默认的连接池是lettuce,后面会讲。server: port: 6868spring: redis: database: 0 # 0-15db host: 127.0.0.1 port: 6379 password: timeout: 1200 # Jedis的配置,可以不配置,有默认值(RedisProperties类中有指定默认值) jedis: pool: max-active: 8 max-idle: 8 min-idle: 0 max-wait: -1配置完可以直接注入使用 // 测试StringRedisTemplate @GetMapping("/testStringRedisTemplate") public String testStringRedisTemplate() { String now = LocalDateTime.now().toString(); stringRedisTemplate.opsForValue().set(“key_” + now, now); return now; }结果如下:在这里,实际上很少直接使用RedisTemplate<Object,Object> redisTemplate,一般是写Redis的配置类自定义RedisTemplate,接下来就实现自定义customRedisTemplate。customRedisTemplate@Configurationpublic class RedisConfig { /** * 自定义RedisTemplate * @param connectionFactory * @return */ @Bean public RedisTemplate<String, Student> customRedisTemplate( RedisConnectionFactory connectionFactory) { RedisTemplate<String, Student> rt = new RedisTemplate<>(); // 实例化Jackson的序列化器 Jackson2JsonRedisSerializer<Student> serializer = new Jackson2JsonRedisSerializer<Student>(Student.class); // 设置value值的序列化器为serializer rt.setValueSerializer(serializer); rt.setHashValueSerializer(serializer); // 设置key键的序列化器为serializer rt.setKeySerializer(new StringRedisSerializer()); rt.setHashKeySerializer(new StringRedisSerializer()); // 设置redis连接工厂(线程安全的) rt.setConnectionFactory(connectionFactory); return rt; }}测试自定义RedisTemplate的用例 @PostMapping("/add") public String add(@RequestBody Student student) { System.out.println(student); customRedisTemplate.opsForValue().set(“key_” + student.getId(), student); return “add success”; }启动Spring Boot并通过Restlet测试:结果如下:到此,简单的整合Redis已经成功。接下来是cache注解。 ...

March 16, 2019 · 1 min · jiezi

spring boot 应用安装(留坑中)

留坑中java 命令启动Linux 系统下以服务的方式启动init.d 服务systemd 服务windows系统下以服务方式启动官方文档

March 13, 2019 · 1 min · jiezi

全网Star最多(近20k)的Spring Boot开源教程 2019 年要继续更新了!

从2016年1月开始写博客,默默地更新《Spring Boot系列教程》,从无人问津到千万访问,作为一个独立站点(http://blog.didispace.com),相信只有那些跟我一样,坚持维护自己独立博客的童鞋才能体会这有多么不容易。由于没有CSDN、博客园这样的权重优势,各种发布于这些平台上的洗稿文章与相似内容,就算发布时间较晚,它依然可以在百度上占据很大的搜索优势,以至于一些读者在读了其他人发布于CSDN、博客园上的一些文章之后看到我的原文,再来我这里喷我抄袭,这样的现象早已经习以为常了。但是庆幸,这些内容的很大一部分读者都是科学上网的好手,我大部分的流量来源都源自谷歌,这点不得不佩服谷歌对原创与一手内容的尊重,这才让我们这些能够独立思考与写作分享的技术人可以一直坚持下去。不知道从什么时候开始,技术圈里的浮夸运营风也越来越重,各种原本非常有含金量的数据也变得越来越虚假,洗稿、盗版等内容的横行,不断侵害着所有原创作者的切身利益。也许这其中包含各种原因:运营KPI的压力,一些大v自媒体的粗暴价值观宣导,所谓的运营套路分享等等。很多原本坚持原创和自有版权的技术人,也都逐步顶不住诱惑得去制造低质量内容,甚至也去传播盗版侵权内容。这些环境问题,有时候很想去改变,但是当我想去做什么的时候,才发现自己是多么渺小,因为面对这个现实,要对抗的不是简单的内容发布者,而是那些有背景强大的机构、是那些拥有更大流量的自媒体。想要去改变这样的环境,对于我这样的个体来说几乎是不可能的。对于这样的现状,我虽然无力去改变,也无法控制别人不要去做那些盗版侵权的事,但是我还是可以继续坚持做好自己。所以,下面我想给大家推荐一下我在维护的目前全网关注(Star)最多的Spring Boot开源教程项目!因为,接下来对于该项目的内容更新,将列入2019年的主要输出内容计划之一,下周开始,我会以每周至少1-2篇的速度持续更新该系列内容,主要目标是整理最新的Spring 2.1.x的入门指南。如果您关注Spring Boot,并且认可我对该框架的解读,欢迎在文末获取项目地址,点击”Star“关注,第一时间获得更新内容!一直以来,我从来都没有这样直接的给大家推荐过自己的开源项目。对于我个人而言,一直都是一个比较纯粹的技术人,至今依然每天都有大量的时间花在了阅读和编写代码,享受每天解决问题的成就感与获取新知识的满足感。对于开源项目数据的增长没有KPI压力,也没有对数据的虚荣追求,长期以来这些数据的唯一意义是作为顺带的评价指标,在没有主动索要和刷量的情况下,这些指标对于任何一个开源项目质量的评价有着重要意义(当然放在今日,很多国内项目的数据虚胖问题,相信大家也有所了解,前文也提到了一些背景原因,这里就不做过多导向性的评判)。下面列一下主要维护的两个渠道信息,截止到现在,我维护的Spring Boot系列教程的两个代码库,累计接近2万Star。GithubGithub是我所有内容的第一更新渠道,所以如果您对后续更新感兴趣,那就Star关注吧!地址:https://github.com/dyc87112/SpringBoot-LearningGiteeGitee的仓库是Github的镜像仓库,由于网络优势,所以一直都会第一时间同步。这个项目的数据是最另我意外的,在整站所有项目的Star排名中居然位列第二,如果是Gitee的忠实用户也可以直接关注这里,一样会得到最快的更新信息。地址:https://gitee.com/didispace/SpringBoot-Learning如果您觉得内容不错,”Star“、”转发“ 支持一下吧~

March 13, 2019 · 1 min · jiezi

spring boot学习(2): SpringApplication和自定义banner

SpringApplication一般,我们用 SpringApplication 来启动spring boot应用。如@SpringBootApplicationpublic class DemoApplication { public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); }}还有其他两种方式:自定义SpringApplication:SpringApplication app = new SpringApplication(MySpringConfiguration.class);app.setBannerMode(Banner.Mode.OFF);app.run(args);使用Builder:new SpringApplicationBuilder() .sources(Parent.class) .child(Application.class) .bannerMode(Banner.Mode.OFF) .run(args);自定义banner自定义文本在 resources 目录下添加 banner.txt 文件: Test ${AnsiColor.YELLOW} Test Banner TextApplication Version: ${application.version}${application.formatted-version}Spring Boot Version: ${spring-boot.version}${spring-boot.formatted-version}启动应用时,显示如下: Test Test Banner TextApplication Version:Spring Boot Version: 2.1.3.RELEASE (v2.1.3.RELEASE)自定义banner图:在 resources 目录下添加 banner.png 文件启动应用时显示: @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ @@@@@@&@@@@&@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ @:@@#@@@@@@@#@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ @@@@@@@@@@@@@@@@@@@@@@#@@&@@@@*@:@o@@@@:@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ Test Test Banner TextApplication Version:Spring Boot Version: 2.1.3.RELEASE (v2.1.3.RELEASE)以上的 @ 行,其实是图片 banner.png 的字符信息。接下来看一下实现逻辑, 搜索 banner.txt:class SpringApplicationBannerPrinter { static final String BANNER_LOCATION_PROPERTY = “spring.banner.location”; static final String BANNER_IMAGE_LOCATION_PROPERTY = “spring.banner.image.location”; static final String DEFAULT_BANNER_LOCATION = “banner.txt”; static final String[] IMAGE_EXTENSION = { “gif”, “jpg”, “png” }; private Banner getBanner(Environment environment) { Banners banners = new Banners(); // 先添加图片 banner banners.addIfNotNull(getImageBanner(environment)); // 再添加文本信息的 banner banners.addIfNotNull(getTextBanner(environment)); if (banners.hasAtLeastOneBanner()) { return banners; } // 没有在运行环境中配置 banner 信息时, A if (this.fallbackBanner != null) { return this.fallbackBanner; } // 没有任何的 banner 信息,使用默认 return DEFAULT_BANNER; } private Banner getTextBanner(Environment environment) { String location = environment.getProperty(BANNER_LOCATION_PROPERTY, DEFAULT_BANNER_LOCATION); Resource resource = this.resourceLoader.getResource(location); if (resource.exists()) { return new ResourceBanner(resource); } return null; } private Banner getImageBanner(Environment environment) { String location = environment.getProperty(BANNER_IMAGE_LOCATION_PROPERTY); if (StringUtils.hasLength(location)) { Resource resource = this.resourceLoader.getResource(location); return resource.exists() ? new ImageBanner(resource) : null; } for (String ext : IMAGE_EXTENSION) { Resource resource = this.resourceLoader.getResource(“banner.” + ext); if (resource.exists()) { return new ImageBanner(resource); } } return null; }}代码中可以看出:默认图片的优先级由高到底为:gif, jpg, png.A 处的 fallbackBanner 是个啥:搜索赋值的地方:SpringApplicationBannerPrinter(ResourceLoader resourceLoader, Banner fallbackBanner) { this.resourceLoader = resourceLoader; this.fallbackBanner = fallbackBanner; }SpringApplicationBannerPrinter 的使用的位置如下: // SpringApplication private Banner printBanner(ConfigurableEnvironment environment) { …… SpringApplicationBannerPrinter bannerPrinter = new SpringApplicationBannerPrinter( resourceLoader, this.banner); …… } public void setBanner(Banner banner) { this.banner = banner; }因此我们可以对 SpringApplication 实例进行 banner 属性的设置,使用方式:SpringApplicationBuilder.banner()SpringApplication.setBanner()小结:优先 spring.banner.location, spring.banner.image.location 配置的 banner如果没有配置,则使用 classpath 里面的 banner.txt 或图片内容, 图片命名为 banner.[ext], 其中 ext 的格式按照优先级高低依次是 gif, jpg, png,文本和图片可以共存,先展示图片,后展示文本。图片内部展示时不能共存。如果没有在环境中配置,展示我们自己设置的自定义bannenr。如果都没有,则展示默认的banner。 ...

March 12, 2019 · 2 min · jiezi

想在Java中实现Excel和Csv的导出吗?看这就对了

title: 想在Java中实现Excel和Csv的导出吗?看这就对了date: 2019-03-01 20:07:07tags: Javakeywords: Java导出Excel和Csvdescription:前言最近在项目中遇到一个需求,需要后端提供一个下载Csv和Excel表格的接口。这个接口接收前端的查询参数,针对这些参数对数据库做查询操作。将查询到的结果生成Excel和Csv文件,再以字节流的形式返回给前端。前端拿到这个流文件之后,最开始用ajax来接收,但是前端发送的请求却被浏览器cancel掉了。后来发现,发展了如此之久的Ajax居然不支持流文件下载。后来前端换成了最原始的XMLHttpRequest,才修复了这个问题。首先给出项目源码的地址。这是源码,欢迎大家star或者提MR。Csv新建controller先来一个简单的例子。首先在controller中新建这样一个接口。@GetMapping(“csv”)public void csv( HttpServletRequest request, HttpServletResponse response) throws IOException { String fileName = this.getFileName(request, “测试数据.csv”); response.setContentType(MediaType.APPLICATION_OCTET_STREAM.toString()); response.setHeader(“Content-Disposition”, “attachment; filename="” + fileName + “";”); LinkedHashMap<String, Object> header = new LinkedHashMap<>(); LinkedHashMap<String, Object> body = new LinkedHashMap<>(); header.put(“1”, “姓名”); header.put(“2”, “年龄”); List<LinkedHashMap<String, Object>> data = new ArrayList<>(); body.put(“1”, “小明”); body.put(“2”, “小王”); data.add(header); data.add(body); data.add(body); data.add(body); FileCopyUtils.copy(ExportUtil.exportCSV(data), response.getOutputStream());}其中this.getFileName(request, “测试数据.csv”)函数是用来获取导出文件名的函数。单独提出来是因为不同浏览器使用的默认的编码不同。例如,如果使用默认的UTF-8编码。在chrome浏览器中下载会出现中文乱码。代码如下。private String getFileName(HttpServletRequest request, String name) throws UnsupportedEncodingException { String userAgent = request.getHeader(“USER-AGENT”); return userAgent.contains(“Mozilla”) ? new String(name.getBytes(), “ISO8859-1”) : name;}response.getOutputStream()则是用于创建字节输出流,在导出csv文件的controller代码结尾,通过工具类中的复制文件函数将字节流写入到输出流中,从而将csv文件以字节流的形式返回给客户端。当前端通过http请求访问服务器接口的时候,http中的所有的请求信息都会封装在HttpServletRequest对象中。例如,你可以通过这个对象获取到请求的URL地址,请求的方式,请求的客户端IP和完整主机名,Web服务器的IP和完整主机名,请求行中的参数,获取请求头的参数等等。针对每一次的HTTP请求,服务器会自动创建一个HttpServletResponse对象和请求对象相对应。响应对象可以对当前的请求进行重定向,自定义响应体的头部,设置返回流等等。新建导出工具类我们新建一个导出工具类,来专门负责导出各种格式的文件。代码如下。public class ExportUtil { public static byte[] exportCSV(List<LinkedHashMap<String, Object>> exportData) { ByteArrayOutputStream out = new ByteArrayOutputStream(); BufferedWriter buffCvsWriter = null; try { buffCvsWriter = new BufferedWriter(new OutputStreamWriter(out, StandardCharsets.UTF_8)); // 将body数据写入表格 for (Iterator<LinkedHashMap<String, Object>> iterator = exportData.iterator(); iterator.hasNext(); ) { fillDataToCsv(buffCvsWriter, iterator.next()); if (iterator.hasNext()) { buffCvsWriter.newLine(); } } // 刷新缓冲 buffCvsWriter.flush(); } catch (IOException e) { e.printStackTrace(); } finally { // 释放资源 if (buffCvsWriter != null) { try { buffCvsWriter.close(); } catch (IOException e) { e.printStackTrace(); } } } return out.toByteArray(); } private static void fillDataToCsv(BufferedWriter buffCvsWriter, LinkedHashMap row) throws IOException { Map.Entry propertyEntry; for (Iterator<Map.Entry> propertyIterator = row.entrySet().iterator(); propertyIterator.hasNext(); ) { propertyEntry = propertyIterator.next(); buffCvsWriter.write(""" + propertyEntry.getValue().toString() + “"”); if (propertyIterator.hasNext()) { buffCvsWriter.write(","); } } }}fillDataToCsv主要是抽离出来为csv填充一行一行的数据的。运行然后运行项目,调用http://localhost:8080/csv,就可以下载示例的csv文件。示例如下。Excel新建controller新建下载xlsx文件的接口。@GetMapping(“xlsx”)public void xlsx( HttpServletRequest request, HttpServletResponse response) throws IOException { String fileName = this.getFileName(request, “测试数据.xlsx”); response.setContentType(MediaType.APPLICATION_OCTET_STREAM.toString()); response.setHeader(“Content-Disposition”, “attachment; filename="” + fileName + “";”); List<LinkedHashMap<String, Object>> datas = new ArrayList<>(); LinkedHashMap<String, Object> data = new LinkedHashMap<>(); data.put(“1”, “姓名”); data.put(“2”, “年龄”); datas.add(data); for (int i = 0; i < 5; i++) { data = new LinkedHashMap<>(); data.put(“1”, “小青”); data.put(“2”, “小白”); datas.add(data); } Map<String, List<LinkedHashMap<String, Object>>> tableData = new HashMap<>(); tableData.put(“日报表”, datas); tableData.put(“周报表”, datas); tableData.put(“月报表”, datas); FileCopyUtils.copy(ExportUtil.exportXlsx(tableData), response.getOutputStream());}补充工具类上面新建的导出工具类中,只有导出csv的函数,接下来我们要添加导出xlsx的函数。public static byte[] exportXlsx(Map<String, List<LinkedHashMap<String, Object>>> tableData) { ByteArrayOutputStream out = new ByteArrayOutputStream(); try { HSSFWorkbook workbook = new HSSFWorkbook(); // 创建多个sheet for (Map.Entry<String, List<LinkedHashMap<String, Object>>> entry : tableData.entrySet()) { fillDataToXlsx(workbook.createSheet(entry.getKey()), entry.getValue()); } workbook.write(out); } catch (IOException e) { e.printStackTrace(); } return out.toByteArray();}/** * 将linkedHashMap中的数据,写入xlsx表格中 * * @param sheet * @param data */private static void fillDataToXlsx(HSSFSheet sheet, List<LinkedHashMap<String, Object>> data) { HSSFRow currRow; HSSFCell cell; LinkedHashMap row; Map.Entry propertyEntry; int rowIndex = 0; int cellIndex = 0; for (Iterator<LinkedHashMap<String, Object>> iterator = data.iterator(); iterator.hasNext(); ) { row = iterator.next(); currRow = sheet.createRow(rowIndex++); for (Iterator<Map.Entry> propertyIterator = row.entrySet().iterator(); propertyIterator.hasNext(); ) { propertyEntry = propertyIterator.next(); if (propertyIterator.hasNext()) { String value = String.valueOf(propertyEntry.getValue()); cell = currRow.createCell(cellIndex++); cell.setCellValue(value); } else { String value = String.valueOf(propertyEntry.getValue()); cell = currRow.createCell(cellIndex++); cell.setCellValue(value); break; } } if (iterator.hasNext()) { cellIndex = 0; } }}fillDataToXlsx的用途与csv一样,为xlsx文件的每一行刷上数据。运行然后运行项目,调用http://localhost:8080/xlsx,就可以下载示例的csv文件。示例如下。项目地址最后再次给出项目地址,大家如果没有理解到其中的一些地方,不妨把项目clone下来,自己亲自操作一波。参考这是在解决请求被浏览器cancel掉的过程中,很重要的一个参考,分享给大家。https://www.cnblogs.com/cdemo… ...

March 10, 2019 · 2 min · jiezi

Spring Boot 学习 (1): 初始化工程

spring boot 项目初始化,介绍三种方式:IntelliJ 创建、Spring CLI 创建以及手动创建,工程使用 gradle 构建工具。IntelliJ创建选择 spring initializr填写自己想要的配置信息选择依赖包:配置工程名和工程所在目录:进入到工程,如下图所示:创建完成。Spring CLI创建示例:spring init -dweb,data-jpa,h2,thymeleaf –build gradle initbycli运行命令后会显示:Using service at https://start.spring.ioProject extracted to ‘<current_path>/initbycli’执行完成会看到当前目录下会多出 initbycli 目录..└── initbycli ├── HELP.md ├── build.gradle ├── gradle │ └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── src ├── main │ ├── java │ │ └── com │ │ └── example │ │ └── initbycli │ │ └── DemoApplication.java │ └── resources │ ├── application.properties │ ├── static │ └── templates └── test └── java └── com └── example └── initbycli └── DemoApplicationTests.java可以看到基本工程目录文件已经创建好了。再看一下 Spring CLI 的说明:$ spring help initspring init - Initialize a new project using Spring Initializr (start.spring.io)usage: spring init [options] [location]Option Description—— ————a, –artifactId <String> Project coordinates; infer archive name (for example ’test’)-b, –boot-version <String> Spring Boot version (for example ‘1.2.0.RELEASE’)–build <String> Build system to use (for example ‘maven’ or ‘gradle’) (default: maven)-d, –dependencies <String> Comma-separated list of dependency identifiers to include in the generated project–description <String> Project description-f, –force Force overwrite of existing files–format <String> Format of the generated content (for example ‘build’ for a build file, ‘project’ for a project archive) (default: project)-g, –groupId <String> Project coordinates (for example ‘org.test’)-j, –java-version <String> Language level (for example ‘1.8’)-l, –language <String> Programming language (for example ‘java’)–list List the capabilities of the service. Use it to discover the dependencies and the types that are available-n, –name <String> Project name; infer application name-p, –packaging <String> Project packaging (for example ‘jar’)–package-name <String> Package name-t, –type <String> Project type. Not normally needed if you use – build and/or –format. Check the capabilities of the service (–list) for more details–target <String> URL of the service to use (default: https://start. spring.io)-v, –version <String> Project version (for example ‘0.0.1-SNAPSHOT’)-x, –extract Extract the project archive. Inferred if a location is specified without an extension说明:依赖: 使用 -d, –dependencies <String>, , 号分割.项目构建类型: –build <String>, 默认为 maven, 另一选项是 gradle.初始化类型: –format <String>, 默认为 project, 会初始化整个工程的目录结构。可选项是 build, 只会生成工程所需要的 build.gradle 文件.查看有哪些可以配置的:–list, 命令输出以后,内容包括可选的依赖包,工程类型和构建属性( java 版本等).手动创建建立工程目录: mkdir initbyself在工程目录下建立 build.gradle 文件,cd initbyselfvim build.gradlebuild.gradle 内容为: plugins { id ‘org.springframework.boot’ version ‘2.1.3.RELEASE’ id ‘java’ } apply plugin: ‘io.spring.dependency-management’ group = ‘com.example’ version = ‘0.0.1-SNAPSHOT’ sourceCompatibility = ‘1.8’ repositories { mavenCentral() } dependencies { implementation ‘org.springframework.boot:spring-boot-starter-data-jpa’ implementation ‘org.springframework.boot:spring-boot-starter-thymeleaf’ implementation ‘org.springframework.boot:spring-boot-starter-web’ runtimeOnly ‘com.h2database:h2’ testImplementation ‘org.springframework.boot:spring-boot-starter-test’ }创建 setting.gradle 文件 pluginManagement { repositories { gradlePluginPortal() } } rootProject.name = ‘initbyself’创建java代码目录mkdir -p src/main/java/com/example/initbyselfmkdir -p src/main/java/resources创建 application切换到代码目录cd src/main/java/com/example/initbyself编写源码文件 DemoApplication.javapackage com.example.initbyself;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;@SpringBootApplicationpublic class DemoApplication { public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); }}运行 gradle build, 结果为:> Task :buildSkipping task ‘:build’ as it has no actions.:build (Thread[Daemon worker Thread 2,5,main]) completed. Took 0.0 secs.BUILD SUCCESSFUL in 3s2 actionable tasks: 2 executed运行项目:java -jar build/libs/initbyself-0.0.1-SNAPSHOT.jar . ____ _ __ _ _ /\ / ’ __ _ () __ __ _ \ \ \ ( ( )__ | ‘_ | ‘| | ‘ / ` | \ \ \ \ \/ )| |)| | | | | || (| | ) ) ) ) ’ || .__|| ||| |_, | / / / / =========||==============|/=//// :: Spring Boot :: (v2.1.3.RELEASE)…..2019-03-07 00:17:36.996 INFO 11848 — [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path ‘‘2019-03-07 00:17:36.999 INFO 11848 — [ main] com.example.initbyself.DemoApplication : Started DemoApplication in 19.497 seconds (JVM running for 19.992)此时,已说明工程初始化成功了。小结以上是 Spring Boot 项目的初始化的三种方式,不一定全。第一种和第二种都依赖 https://start.spring.io 这个地址,这是官方提供的快速初始化方式,第三种是我们全手动一步一步初始化,需要对构建工具十分熟悉。 ...

March 7, 2019 · 3 min · jiezi

SpringBoot 实战 (十七) | 整合 WebSocket 实现聊天室

微信公众号:一个优秀的废人。如有问题,请后台留言,反正我也不会听。前言昨天那篇介绍了 WebSocket 实现广播,也即服务器端有消息时,将消息发送给所有连接了当前 endpoint 的浏览器。但这无法解决消息由谁发送,又由谁接收的问题。所以,今天写一篇实现一对一的聊天室。今天这一篇建立在昨天那一篇的基础之上,为便于更好理解今天这一篇,推荐先阅读:「SpringBoot 整合WebSocket 实现广播消息 」准备工作Spring Boot 2.1.3 RELEASESpring Security 2.1.3 RELEASEIDEAJDK8pom 依赖因聊天室涉及到用户相关,所以在上一篇基础上引入 Spring Security 2.1.3 RELEASE 依赖<!– Spring Security 依赖 –><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId></dependency>Spring Security 的配置虽说涉及到 Spring Security ,但鉴于篇幅有限,这里只对这个项目相关的部分进行介绍,具体的 Spring Security 教程,后面会出。这里的 Spring Security 配置很简单,具体就是设置登录路径、设置安全资源以及在内存中创建用户和密码,密码需要注意加密,这里使用 BCrypt 加密算法在用户登录时对密码进行加密。 代码注释很详细,不多说。package com.nasus.websocket.config;import org.springframework.context.annotation.Configuration;import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;import org.springframework.security.config.annotation.web.builders.HttpSecurity;import org.springframework.security.config.annotation.web.builders.WebSecurity;import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;@Configuration// 开启Spring Security的功能@EnableWebSecuritypublic class WebSecurityConfig extends WebSecurityConfigurerAdapter{ @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() // 设置 SpringSecurity 对 / 和 “/login” 路径不拦截 .mvcMatchers("/","/login").permitAll() .anyRequest().authenticated() .and() .formLogin() // 设置 Spring Security 的登录页面访问路径为/login .loginPage("/login") // 登录成功后转向 /chat 路径 .defaultSuccessUrl("/chat") .permitAll() .and() .logout() .permitAll(); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.inMemoryAuthentication() // 在内存中分配两个用户 nasus 和 chenzy ,用户名和密码一致 // BCryptPasswordEncoder() 是 Spring security 5.0 中新增的加密方式 // 登陆时用 BCrypt 加密方式对用户密码进行处理。 .passwordEncoder(new BCryptPasswordEncoder()) .withUser(“nasus”) // 保证用户登录时使用 bcrypt 对密码进行处理再与内存中的密码比对 .password(new BCryptPasswordEncoder().encode(“nasus”)).roles(“USER”) .and() // 登陆时用 BCrypt 加密方式对用户密码进行处理。 .passwordEncoder(new BCryptPasswordEncoder()) .withUser(“chenzy”) // 保证用户登录时使用 bcrypt 对密码进行处理再与内存中的密码比对 .password(new BCryptPasswordEncoder().encode(“chenzy”)).roles(“USER”); } @Override public void configure(WebSecurity web) throws Exception { // /resource/static 目录下的静态资源,Spring Security 不拦截 web.ignoring().antMatchers("/resource/static**"); }}WebSocket 的配置在上一篇的基础上另外注册一个名为 “/endpointChat” 的节点,以供用户订阅,只有订阅了该节点的用户才能接收到消息;然后,再增加一个名为 “/queue” 消息代理。@Configuration// @EnableWebSocketMessageBroker 注解用于开启使用 STOMP 协议来传输基于代理(MessageBroker)的消息,这时候控制器(controller)// 开始支持@MessageMapping,就像是使用 @requestMapping 一样。@EnableWebSocketMessageBrokerpublic class WebSocketConfig implements WebSocketMessageBrokerConfigurer { @Override public void registerStompEndpoints(StompEndpointRegistry registry) { //注册一个名为 /endpointNasus 的 Stomp 节点(endpoint),并指定使用 SockJS 协议。 registry.addEndpoint("/endpointNasus").withSockJS(); //注册一个名为 /endpointChat 的 Stomp 节点(endpoint),并指定使用 SockJS 协议。 registry.addEndpoint("/endpointChat").withSockJS(); } @Override public void configureMessageBroker(MessageBrokerRegistry registry) { // 广播式配置名为 /nasus 消息代理 , 这个消息代理必须和 controller 中的 @SendTo 配置的地址前缀一样或者全匹配 // 点对点增加一个 /queue 消息代理 registry.enableSimpleBroker("/queue","/nasus/getResponse"); }}控制器 controller指定发送消息的格式以及模板。详情见,代码注释。@Autowired//使用 SimpMessagingTemplate 向浏览器发送信息private SimpMessagingTemplate messagingTemplate;@MessageMapping("/chat")public void handleChat(Principal principal,String msg){ // 在 SpringMVC 中,可以直接在参数中获得 principal,principal 中包含当前用户信息 if (principal.getName().equals(“nasus”)){ // 硬编码,如果发送人是 nasus 则接收人是 chenzy 反之也成立。 // 通过 messageingTemplate.convertAndSendToUser 方法向用户发送信息,参数一是接收消息用户,参数二是浏览器订阅地址,参数三是消息本身 messagingTemplate.convertAndSendToUser(“chenzy”, “/queue/notifications”,principal.getName()+"-send:" + msg); } else { messagingTemplate.convertAndSendToUser(“nasus”, “/queue/notifications”,principal.getName()+"-send:" + msg); }}登录页面<!DOCTYPE html><html xmlns=“http://www.w3.org/1999/xhtml" xmlns:th=“http://www.thymeleaf.org” xmlns:sec=“http://www.thymeleaf.org/thymeleaf-extras-springsecurity3"><meta charset=“UTF-8” /><head> <title>登陆页面</title></head><body><div th:if="${param.error}"> 无效的账号和密码</div><div th:if="${param.logout}"> 你已注销</div><form th:action=”@{/login}” method=“post”> <div><label> 账号 : <input type=“text” name=“username”/> </label></div> <div><label> 密码: <input type=“password” name=“password”/> </label></div> <div><input type=“submit” value=“登陆”/></div></form></body></html>聊天页面<!DOCTYPE html><html xmlns:th=“http://www.thymeleaf.org”><meta charset=“UTF-8” /><head> <title>Home</title> <script th:src="@{sockjs.min.js}"></script> <script th:src="@{stomp.min.js}"></script> <script th:src="@{jquery.js}"></script></head><body><p> 聊天室</p><form id=“nasusForm”> <textarea rows=“4” cols=“60” name=“text”></textarea> <input type=“submit”/></form><script th:inline=“javascript”> $(’#nasusForm’).submit(function(e){ e.preventDefault(); var text = $(’#nasusForm’).find(’textarea[name=“text”]’).val(); sendSpittle(text); }); // 连接 SockJs 的 endpoint 名称为 “/endpointChat” var sock = new SockJS("/endpointChat"); var stomp = Stomp.over(sock); stomp.connect(‘guest’, ‘guest’, function(frame) { // 订阅 /user/queue/notifications 发送的消息,这里与在控制器的 // messagingTemplate.convertAndSendToUser 中订阅的地址保持一致 // 这里多了 /user 前缀,是必须的,使用了 /user 才会把消息发送到指定用户 stomp.subscribe("/user/queue/notifications", handleNotification); }); function handleNotification(message) { $(’#output’).append("<b>Received: " + message.body + “</b><br/>”) } function sendSpittle(text) { stomp.send("/chat", {}, text); } $(’#stop’).click(function() {sock.close()});</script><div id=“output”></div></body></html>页面控制器 controller@Controllerpublic class ViewController { @GetMapping("/nasus") public String getView(){ return “nasus”; } @GetMapping("/login") public String getLoginView(){ return “login”; } @GetMapping("/chat") public String getChatView(){ return “chat”; }}测试预期结果应该是:两个用户登录系统,可以互相发送消息。但是同一个浏览器的用户会话的 session 是共享的,这里需要在 Chrome 浏览器再添加一个用户。具体操作在 Chrome 的 设置–>管理用户–>添加用户:两个用户分别访问 http://localhost:8080/login 登录系统,跳转至聊天界面:相互发送消息:完整代码https://github.com/turoDog/De…如果觉得对你有帮助,请给个 Star 再走呗,非常感谢。后语如果本文对你哪怕有一丁点帮助,请帮忙点好看。你的好看是我坚持写作的动力。另外,关注之后在发送 1024 可领取免费学习资料。资料详情请看这篇旧文:Python、C++、Java、Linux、Go、前端、算法资料分享 ...

March 6, 2019 · 2 min · jiezi

已有项目改造——Spring boot集成flyway

背景目前项目是spring boot+mysql+maven,因为需要数据库版本管理,因而集成flyway目标集成flyway对于已存在的数据库结构不影响步骤1.在pom.xml中加入依赖<dependency> <groupId>org.flywaydb</groupId> <artifactId>flyway-core</artifactId> <version>5.2.4</version></dependency>2.在项目中,创建放置sql的文件夹cd src/main/resourcesmkdir db/migration3.导出当前数据库的DDL和数据文件清空数据库导出数据库 mysqldump -uroot -p Mydb >Mydb.sql清空数据库drop database Mydb;create schema Mydb default character set utf8 collate utf8_general_ci;4.将导出文件放置到db/migration中mv Mydb.sql db/migration/V1__init.sql5.编译&&运行项目#编译打包mvn package#运行项目java -jar target/myproject-0.0.1-SNAPSHOT.jar 6.运行结果数据库和未集成前一致,且多一个schema_version表,该表是flyway进行版本管理的主表注意这个方法简单粗暴,用于比数据量不大的情况一些可以配置的flyway参数方法一:在application.properties 或者 application.yml 中配置# FLYWAY (FlywayProperties)spring.flyway.baseline-description=<< Flyway Baseline >> # Description to tag an existing schema with when applying a baseline.spring.flyway.baseline-on-migrate=false # Whether to automatically call baseline when migrating a non-empty schema.spring.flyway.baseline-version=1 # Version to tag an existing schema with when executing baseline.spring.flyway.check-location=true # Whether to check that migration scripts location exists.spring.flyway.clean-disabled=false # Whether to disable cleaning of the database.spring.flyway.clean-on-validation-error=false # Whether to automatically call clean when a validation error occurs.spring.flyway.connect-retries=0 # Maximum number of retries when attempting to connect to the database.spring.flyway.enabled=true # Whether to enable flyway.spring.flyway.encoding=UTF-8 # Encoding of SQL migrations.spring.flyway.group=false # Whether to group all pending migrations together in the same transaction when applying them.spring.flyway.ignore-future-migrations=true # Whether to ignore future migrations when reading the schema history table.spring.flyway.ignore-ignored-migrations=false # Whether to ignore ignored migrations when reading the schema history table.spring.flyway.ignore-missing-migrations=false # Whether to ignore missing migrations when reading the schema history table.spring.flyway.ignore-pending-migrations=false # Whether to ignore pending migrations when reading the schema history table.spring.flyway.init-sqls= # SQL statements to execute to initialize a connection immediately after obtaining it.spring.flyway.installed-by= # Username recorded in the schema history table as having applied the migration.spring.flyway.locations=classpath:db/migration # Locations of migrations scripts. Can contain the special “{vendor}” placeholder to use vendor-specific locations.spring.flyway.mixed=false # Whether to allow mixing transactional and non-transactional statements within the same migration.spring.flyway.out-of-order=false # Whether to allow migrations to be run out of order.spring.flyway.password= # Login password of the database to migrate.spring.flyway.placeholder-prefix=${ # Prefix of placeholders in migration scripts.spring.flyway.placeholder-replacement=true # Perform placeholder replacement in migration scripts.spring.flyway.placeholder-suffix=} # Suffix of placeholders in migration scripts.spring.flyway.placeholders= # Placeholders and their replacements to apply to sql migration scripts.spring.flyway.repeatable-sql-migration-prefix=R # File name prefix for repeatable SQL migrations.spring.flyway.schemas= # Scheme names managed by Flyway (case-sensitive).spring.flyway.skip-default-callbacks=false # Whether to skip default callbacks. If true, only custom callbacks are used.spring.flyway.skip-default-resolvers=false # Whether to skip default resolvers. If true, only custom resolvers are used.spring.flyway.sql-migration-prefix=V # File name prefix for SQL migrations.spring.flyway.sql-migration-separator=__ # File name separator for SQL migrations.spring.flyway.sql-migration-suffixes=.sql # File name suffix for SQL migrations.spring.flyway.table=flyway_schema_history # Name of the schema schema history table that will be used by Flyway.spring.flyway.target= # Target version up to which migrations should be considered.spring.flyway.url= # JDBC url of the database to migrate. If not set, the primary configured data source is used.spring.flyway.user= # Login user of the database to migrate.spring.flyway.validate-on-migrate=true # Whether to automatically call validate when performing a migration.方法二:用环境变量配置略参考资料Execute Flyway Database Migrations on Startupspring boot 开箱集成flywayExisting database setup ...

March 5, 2019 · 2 min · jiezi

Spring Cloud Alibaba与Spring Boot、Spring Cloud之间不得不说的版本关系

这篇博文是临时增加出来的内容,主要是由于最近连载《Spring Cloud Alibaba基础教程》系列的时候,碰到读者咨询的大量问题中存在一个比较普遍的问题:版本的选择。其实这类问题,在之前写Spring Cloud基础教程的时候,就已经发过一篇《聊聊Spring Cloud版本的那些事儿》,来说明Spring Boot和Spring Cloud版本之间的关系。Spring Cloud Alibaba现阶段版本的特殊性现在的Spring Cloud Alibaba由于没有纳入到Spring Cloud的主版本管理中,所以我们需要自己去引入其版本信息,比如之前教程中的例子:<dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>Finchley.SR1</version> <type>pom</type> <scope>import</scope> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-alibaba-dependencies</artifactId> <version>0.2.1.RELEASE</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies></dependencyManagement>而不是像以往使用Spring Cloud的时候,直接引入Spring Cloud的主版本(Dalston、Edgware、Finchley、Greenwich这些)就可以的。我们需要像上面的例子那样,单独的引入spring-cloud-alibaba-dependencies来管理Spring Cloud Alibaba下的组件版本。由于Spring Cloud基于Spring Boot构建,而Spring Cloud Alibaba又基于Spring Cloud Common的规范实现,所以当我们使用Spring Cloud Alibaba来构建微服务应用的时候,需要知道这三者之间的版本关系。下表整理了目前Spring Cloud Alibaba的版本与Spring Boot、Spring Cloud版本的兼容关系:Spring BootSpring CloudSpring Cloud Alibaba2.1.xGreenwich0.2.2(还未RELEASE)2.0.xFinchley0.2.11.5.xEdgware0.1.11.5.xDalston0.1.1所以,不论您是在读我的《Spring Boot基础教程》、《Spring Cloud基础教程》还是正在连载的《Spring Cloud Alibaba系列教程》。当您照着博子的顺序,一步步做下来,但是没有调试成功的时候,强烈建议检查一下,您使用的版本是否符合上表的关系。推荐:Spring Cloud Alibaba基础教程《Spring Cloud Alibaba基础教程:使用Nacos实现服务注册与发现》《Spring Cloud Alibaba基础教程:支持的几种服务消费方式》《Spring Cloud Alibaba基础教程:使用Nacos作为配置中心》《Spring Cloud Alibaba基础教程:Nacos配置的加载规则详解》《Spring Cloud Alibaba基础教程:Nacos配置的多环境管理》《Spring Cloud Alibaba基础教程:Nacos配置的多文件加载与共享配置》《Spring Cloud Alibaba基础教程:Nacos的数据持久化》《Spring Cloud Alibaba基础教程:Nacos的集群部署》该系列教程的代码示例:Github:https://github.com/dyc87112/SpringCloud-Learning/Gitee:https://gitee.com/didispace/SpringCloud-Learning/如果您对这些感兴趣,欢迎star、follow、收藏、转发给予支持!以下专题教程也许您会有兴趣Spring Boot基础教程【新版】Spring Cloud从入门到精通 ...

March 4, 2019 · 1 min · jiezi

【spring boot2】第9篇:spring boot 整合 mybatis

引入starter修改pom.xml文件,整合mybatis需要如下的包<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId></dependency><!–mybatis的starter–><dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.0.0</version></dependency><!–mysql驱动–><dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope></dependency><!–druid连接池–><dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.1.12</version></dependency><dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.1.10</version></dependency>durid连接池配置修改yml文件spring: datasource: username: root password: root url: jdbc:mysql://127.0.0.1:3306/sff_test driver-class-name: com.mysql.jdbc.Driver # 指定自己使用的数据源 type: com.alibaba.druid.pool.DruidDataSource # DruidDataSource 其他属性配置 druid: initialSize: 5 minIdle: 5 maxActive: 20 maxWait: 60000 timeBetweenEvictionRunsMillis: 60000 minEvictableIdleTimeMillis: 300000 validationQuery: SELECT 1 FROM DUAL testWhileIdle: true testOnBorrow: false testOnReturn: false poolPreparedStatements: true maxPoolPreparedStatementPerConnectionSize: 20 useGlobalDataSourceStat: true filter: stat: enabled: true log-slow-sql: true wall: enabled: true注入duird配置@Configurablepublic class DruidConfig { @Bean @ConfigurationProperties(“spring.datasource.druid”) public DataSource dataSourceTwo() { return DruidDataSourceBuilder.create().build(); } /** * 配置一个管理后台的Servlet * * @return / @Bean public ServletRegistrationBean statViewServlet() { ServletRegistrationBean bean = new ServletRegistrationBean(new StatViewServlet(), “/druid/”); Map<String, String> initParams = new HashMap<>(); initParams.put(“loginUsername”, “admin”); initParams.put(“loginPassword”, “admin”); initParams.put(“allow”, “”);//默认就是允许所有访问 bean.setInitParameters(initParams); return bean; } /** * 配置一个web监控的filter * * @return / @Bean public FilterRegistrationBean webStatFilter() { FilterRegistrationBean bean = new FilterRegistrationBean(); bean.setFilter(new WebStatFilter()); Map<String, String> initParams = new HashMap<>(); //通过 localhost:8080/druid/ 可以访问到监控后台 initParams.put(“exclusions”, “.js,.css,/druid/”); bean.setInitParameters(initParams); bean.setUrlPatterns(Arrays.asList("/*")); return bean; }} ...

March 1, 2019 · 1 min · jiezi

Spring 执行 sql 脚本(文件)

本篇解决 Spring 执行SQL脚本(文件)的问题。场景描述可以不看。场景描述:我在运行单测的时候,也就是 Spring 工程启动的时候,Spring 会去执行 classpath:schema.sql(后面会解释),我想利用这一点,解决一个问题:一次运行多个测试文件,每个文件先后独立运行,而上一个文件创建的数据,会对下一个文件运行时造成影响,所以我要在每个文件执行完成之后,重置数据库,不单单是把数据删掉,而 schema.sql 里面有 drop table 和create table。解决方法://Schema 处理器@Componentpublic class SchemaHandler { private final String SCHEMA_SQL = “classpath:schema.sql”; @Autowired private DataSource datasource; @Autowired private SpringContextGetter springContextGetter; public void execute() throws Exception { Resource resource = springContextGetter.getApplicationContext().getResource(SCHEMA_SQL); ScriptUtils.executeSqlScript(datasource.getConnection(), resource); }}// 获取 ApplicationContext@Componentpublic class SpringContextGetter implements ApplicationContextAware { private ApplicationContext applicationContext; public ApplicationContext getApplicationContext() { return applicationContext; } @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { this.applicationContext = applicationContext; }}备注:关于为何 Spring 会去执行 classpath:schema.sql,可以参考源码org.springframework.boot.autoconfigure.jdbc.DataSourceInitializer#runSchemaScriptsprivate void runSchemaScripts() { List<Resource> scripts = getScripts(“spring.datasource.schema”, this.properties.getSchema(), “schema”); if (!scripts.isEmpty()) { String username = this.properties.getSchemaUsername(); String password = this.properties.getSchemaPassword(); runScripts(scripts, username, password); try { this.applicationContext .publishEvent(new DataSourceInitializedEvent(this.dataSource)); // The listener might not be registered yet, so don’t rely on it. if (!this.initialized) { runDataScripts(); this.initialized = true; } } catch (IllegalStateException ex) { logger.warn(“Could not send event to complete DataSource initialization (” + ex.getMessage() + “)”); } } }/** * 默认拿 classpath*:schema-all.sql 和 classpath*:schema.sql /private List<Resource> getScripts(String propertyName, List<String> resources, String fallback) { if (resources != null) { return getResources(propertyName, resources, true); } String platform = this.properties.getPlatform(); List<String> fallbackResources = new ArrayList<String>(); fallbackResources.add(“classpath:” + fallback + “-” + platform + “.sql”); fallbackResources.add(“classpath*:” + fallback + “.sql”); return getResources(propertyName, fallbackResources, false); }参考:https://github.com/spring-pro…原文链接:http://zhige.me/2019/02/28/20… ...

February 28, 2019 · 1 min · jiezi

spring-boot List转Page

需求:班级与教师是多对多关系,在后台班级管理需要添加一个接口,传入教师的id和pageable,返回带分页数据的班级信息。Page<Klass> pageByTeacher(Long teacherId, Pageable pageable);一开始打算是在KlassRepository(继承自PagingAndSortingRepository)中添加一个类似findByElementId的接口,然后直接返回带分页的数据。但是试了几次并不成功,无论是把teacher还是将带teacher的List传入方法中都失败。换了一种思路,直接调TeacherRepository的FindById()方法找到teacher,然后返回teacher的成员klassList就行了。 Teacher teacher = teacherRepository.findById(teacherId).get(); List<Klass> klassList = teacher.getKlassList();但是光返回klassList还不行,需要将它包装成Page才行,去官网上查到了一种使用List构造Page的方法PageImplpublic PageImpl(List<T> content, Pageable pageable, long total)Constructor of PageImpl.Parameters:content - the content of this page, must not be null.pageable - the paging information, must not be null.total - the total amount of items available. The total might be adapted considering the length of the content given, if it is going to be the content of the last page. This is in place to mitigate inconsistencies.参数:content: 要传的List,不为空pageable: 分页信息,不为空total: 可用项的总数。如果是最后一页,考虑到给定内容的长度,total可以被调整。这是为了缓解不一致性。(这句没懂什么意思),可选一开始还以为它会自己按照传入的参数分割ListPage<Klass> klassPage = new PageImpl<Klass>(klassList, pageable, klassList.size());结果debug发现不行,得手动分割,就去网上参考了别人的写法 // 当前页第一条数据在List中的位置 int start = (int)pageable.getOffset(); // 当前页最后一条数据在List中的位置 int end = (start + pageable.getPageSize()) > klassList.size() ? klassList.size() : ( start + pageable.getPageSize()); // 配置分页数据 Page<Klass> klassPage = new PageImpl<Klass>(klassList.subList(start, end), pageable, klassList.size());debug查看结果最后为了增加复用性,改成范型方法: public <T> Page<T> listConvertToPage(List<T> list, Pageable pageable) { int start = (int)pageable.getOffset(); int end = (start + pageable.getPageSize()) > list.size() ? list.size() : ( start + pageable.getPageSize()); return new PageImpl<T>(list.subList(start, end), pageable, list.size());}总结:这样装填出来的Page还缺少一些信息,只能满足基本的分页要求。有待改进。 ...

February 28, 2019 · 1 min · jiezi

在使用spring-boot-maven-plugin的下生成普通的jar包

当使用springboot的maven插件的时候,默认是生成的可执行jar包,如果我们想让其生成普通的jar包该怎么做呢?一、解决办法直接上方法mvn clean package -D spring-boot.repackage.skip=true 加上-Dspring-boot.repackage.skip=true参数即可,此时只会生成一个普通的jar包二、理解当使用SpringBoot开发项目的时候,会使用到spring-boot-maven-plugin插件官方文档:https://docs.spring.io/spring…Spring Boot Maven plugin有5个Goals:命令说明spring-boot:repackage默认goal。在mvn package之后,再次打包可执行的jar/war,<br/>并将mvn package生成的软件包重命名为*.originalspring-boot:run运行Spring Boot应用spring-boot:start在mvn integration-test阶段,进行Spring Boot应用生命周期的管理spring-boot:stop在mvn integration-test阶段,进行Spring Boot应用生命周期的管理spring-boot:build-info生成Actuator使用的构建信息文件build-info.properties当时用spring-boot-maven-plugin插件时,下面的mvn命令会生成两个文件:mvn package执行后会看到生成的两个jar文件:.jar.jar.original这是由于在执行上述命令的过程中,Maven首先在package阶段打包生成*.jar文件;然后执行spring-boot:repackage重新打包,将之前的*.jar包重命名为*.jar.original,然后生成springboot的可执行jar包文件*.jar所以,我们只需要跳过spring-boot:repackage阶段即可。

February 26, 2019 · 1 min · jiezi

Spring Security and Angular 实现用户认证

引言度过了前端框架的技术选型之后,新系统起步。ng-alain,一款功能强大的前端框架,设计得很好,两大缺点,文档不详尽,框架代码不规范。写前台拦截器的时候是在花了大约半小时的时间对代码进行全面规范化之后才开始进行的。又回到了最原始的问题,认证授权,也就是Security。认证授权认证,也就是判断用户是否可登录系统。授权,用户登录系统后可以干什么,哪些操作被允许。本文,我们使用Spring Security与Angular进行用户认证。开发环境Java 1.8Spring Boot 2.0.5.RELEASE学习这里给大家介绍一下我学习用户认证的经历。官方文档第一步,肯定是想去看官方文档,Spring Security and Angular - Spring.io。感叹一句这个文档,实在是太长了!!!记得当时看这个文档看了一晚上,看完还不敢睡觉,一鼓作气写完,就怕第二天起来把学得都忘了。我看完这个文档,其实我们需要的并不是文档的全部。总结一下文档的结构:引言讲解前后台不分离项目怎么使用basic方式登录前后台不分离项目怎么使用form方式登录,并自定义登录表单讲解CSRF保护(这块没看懂,好像就是防止伪造然后多存一个X-XSRF-TOKEN)修改架构,启用API网关进行转发(计量项目原实现方式)使用Spring Session自定义token实现Oauth2登录文档写的很好,讲解了许多why?,我们为什么要这么设计。我猜想这篇文章应该默认学者已经掌握Spring Security,反正我零基础看着挺费劲的。初学建议结合IBM开发者社区上的博客进行学习(最近才发现的,上面写的都特别好,有的作者怕文字说不明白的还特意录了个视频放在上面)。学习 - IBM中国这是我结合学习的文章:Spring Security 的 Web 应用和指纹登录实践实现引入Security依赖<!– Security –><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId></dependency>基础配置继承配置适配器WebSecurityConfigurerAdapter,就实现了Spring Security的配置。重写configure,自定义认证规则。注意,configure里的代码不要当成代码看,否则会死得很惨。就把他当成普通的句子看!!!@Componentpublic class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http // 使用Basic认证方式进行验证进行验证 .httpBasic() // 要求SpringSecurity对后台的任何请求进行认证保护 .and().authorizeRequests().anyRequest().authenticated(); }}如此,我们后台的接口就被Spring Security保护起来了,当访问接口时,浏览器会弹出登录提示框。用户名是user,密码已打印在控制台:自定义认证这不行呀,不可能项目一上线,用的还是随机生成的用户名和密码,应该去数据库里查。实现UserDetailsService接口并交给Spring托管,在用户认证时,Spring Security即自动调用我们实现的loadUserByUsername方法,传入username,然后再用我们返回的对象进行其他认证操作。该方法要求我们根据我们自己的User来构造Spring Security内置的org.springframework.security.core.userdetails.User,如果抛出UsernameNotFoundException,则Spring Security代替我们返回401。@Componentpublic class YunzhiAuthService implements UserDetailsService { @Autowired private UserRepository userRepository; private static final Logger logger = LoggerFactory.getLogger(YunzhiAuthService.class); @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { logger.debug(“根据用户名查询用户”); User user = userRepository.findUserByUsername(username); logger.debug(“用户为空,则抛出异常”); if (user == null) { throw new UsernameNotFoundException(“用户名不存在”); } // TODO: 学习Spring Security中的role授权,看是否对项目有所帮助 return new org.springframework.security.core.userdetails.User(username, user.getPassword(), AuthorityUtils.createAuthorityList(“admin”)); }}基础的代码大家都能看懂,这里讲解一下最后一句。return new org.springframework.security.core.userdetails.User(username, user.getPassword(), AuthorityUtils.createAuthorityList(“admin”));构建一个用户,用户名密码都是我们查出来set进去的,对该用户授权admin角色(暂且这么写,这个对用户授予什么角色关系到授权,我们日后讨论)。然后Spring Security就调用我们返回的User对象进行密码判断与用户授权。用户冻结Spring Security只有用户名和密码认证吗?那用户冻结了怎么办呢?这个无须担心,点开org.springframework.security.core.userdetails.User,一个三个参数的构造函数,一个七个参数的构造函数,去看看源码中的注释,一切都不是问题。Spring Security设计得相当完善。public User(String username, String password, Collection<? extends GrantedAuthority> authorities) { this(username, password, true, true, true, true, authorities);}public User(String username, String password, boolean enabled, boolean accountNonExpired, boolean credentialsNonExpired, boolean accountNonLocked, Collection<? extends GrantedAuthority> authorities) { if (((username == null) || “".equals(username)) || (password == null)) { throw new IllegalArgumentException( “Cannot pass null or empty values to constructor”); } this.username = username; this.password = password; this.enabled = enabled; this.accountNonExpired = accountNonExpired; this.credentialsNonExpired = credentialsNonExpired; this.accountNonLocked = accountNonLocked; this.authorities = Collections.unmodifiableSet(sortAuthorities(authorities));}启用密码加密忘了当时是什么场景了,好像是写完YunzhiAuthService之后再启动项目,控制台中就有提示:具体内容记不清了,大体意思就是推荐我采用密码加密。特意查了一下数据库中的密码需不需要加密,然后就查到了CSDN的密码泄露事件,很多开发者都批判CSDN的程序员,说明文存储密码是一种非常不服责任的行为。然后又搜到了腾讯有关的一些文章,反正密码加密了,数据泄露了也不用承担过多的法律责任。腾讯还是走在法律的前列啊,话说是不是腾讯打官司还没输过?既然这么多人都推荐加密,那我们也用一用吧。去Google了一下查了,好像BCryptPasswordEncoder挺常用的,就添加到上下文里了,然后Spring Security再进行密码判断的话,就会把传来的密码经过BCryptPasswordEncoder加密,判断和我们传给它的加密密码是否一致。@Configurationpublic class BeanConfig { /** * 密码加密 / @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); }}然后一些User的细节就参考李宜衡的文章:Hibernate实体监听器。Help, How is My Application Going to Scale?其实,如果对技术要求不严谨的人来说,上面已经足够了。如果你也有一颗崇尚技术的心,我们一起往下看。嘿!我的应用程序怎么扩大规模?这是Spring官方文档中引出的话题,官方文档中对这一块的描述过于学术,什么TCP,什么stateless。说实话,这段我看了好几遍也没看懂,但是我非常同意这个结论:我们不能用Spring Security帮我们管理Session。以下是我个人的观点:因为这是存在本地的,当我们的后台有好多台服务器,怎么办?用户这次请求的是Server1,Server1上存了一个seesion,然后下次请求的是Server2,Server2没有session,完了,401。所以我们要禁用Spring Security的Session,但是手动管理Session又太复杂,所以引入了新项目:Spring Session。Spring Session的一大优点也是支持集群Session。引入Spring Session<!– Redis –><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId></dependency><!– Session –><dependency> <groupId>org.springframework.session</groupId> <artifactId>spring-session-data-redis</artifactId></dependency>这里引入的是Spring Session中的Session-Redis项目,使用Redis服务器存储Session,实现集群共享。禁用Spring Security的Session管理@Componentpublic class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http // 使用Basic认证方式进行验证进行验证 .httpBasic() // 要求SpringSecurity对后台的任何请求进行认证保护 .and().authorizeRequests().anyRequest().authenticated() // 关闭Security的Session,使用Spring Session .and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.NEVER); }}关闭Spring Security的Session管理,设置Session创建策略为NEVER。Spring Security will never create an HttpSession, but will use the HttpSession if it already existsSpring Security不会创建HttpSession,但是如果存在,会使用这个HttpSession。启用Redis管理SessionMac下使用Homebrew安装redis十分简单,Mac下安装配置Redis。@EnableRedisHttpSession@Configurationpublic class BeanConfig { /* * 设置Session的token策略 / @Bean public HeaderHttpSessionIdResolver httpSessionIdResolver() { return new HeaderHttpSessionIdResolver(“token”); }}@EnableRedisHttpSession启用Redis的Session管理,上下文中加入对象HeaderHttpSessionIdResolver,设置从Http请求中找header里的token最为认证字段。梳理逻辑很乱是吗?让我们重新梳理一下逻辑。使用HttpBasic方式登录,用户名和密码传给后台,Spring Security进行用户认证,然后根据我们的配置,Spring Security使用的是Spring Session创建的Session,最后存入Redis。以后呢?登录之后,就是用token的方式进行用户认证,将token添加到header中,然后请求的时候后台识别header里的token进行用户认证。所以,我们需要在用户登录的时候返回token作为以后用户认证的条件。登录方案登录方案,参考官方文档学来的,很巧妙。以Spring的话来说:这个叫trick,小骗术。我们的login方法长成这样:@GetMapping(“login”)public Map<String, String> login(@AuthenticationPrincipal Principal user, HttpSession session) { logger.info(“用户: " + user.getName() + “登录系统”); return Collections.singletonMap(“token”, session.getId());}简简单单的四行,就实现了后台的用户认证。原理因为我们的后台是受Spring Security保护的,所以当访问login方法时,就需要进行用户认证,认证成功才能执行到login方法。换句话说,只要我们的login方法执行到了,那就说明用户认证成功,所以login方法完全不需要业务逻辑,直接返回token,供之后认证使用。怎么样,是不是很巧妙?注销方案注销相当简单,直接清空当前的用户认证信息即可。@GetMapping(“logout”)public void logout(HttpServletRequest request, HttpServletResponse response) { logger.info(“用户注销”); // 获取用户认证信息 Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); // 存在认证信息,注销 if (authentication != null) { new SecurityContextLogoutHandler().logout(request, response, authentication); }}单元测试如果对整个流程不是很明白的话,看下面的单元测试会有所帮助,代码很详尽,请理解整个认证的流程。@RunWith(SpringRunner.class)@SpringBootTest@AutoConfigureMockMvc@Transactionalpublic class AuthControllerTest { private static final Logger logger = LoggerFactory.getLogger(AuthControllerTest.class); private static final String LOGIN_URL = “/auth/login”; private static final String LOGOUT_URL = “/auth/logout”; private static final String TOKEN_KEY = “token”; @Autowired private MockMvc mockMvc; @Test public void securityTest() throws Exception { logger.debug(“初始化基础变量”); String username; String password; byte[] encodedBytes; MvcResult mvcResult; logger.debug(“1. 测试用户名不存在”); username = CommonService.getRandomStringByLength(10); password = “admin”; encodedBytes = Base64.encodeBase64((username + “:” + password).getBytes()); logger.debug(“断言401”); this.mockMvc.perform(MockMvcRequestBuilders.get(LOGIN_URL) .header(“Authorization”, “Basic " + new String(encodedBytes))) .andExpect(status().isUnauthorized()); logger.debug(“2. 用户名存在,但密码错误”); username = “admin”; password = CommonService.getRandomStringByLength(10); encodedBytes = Base64.encodeBase64((username + “:” + password).getBytes()); logger.debug(“断言401”); this.mockMvc.perform(MockMvcRequestBuilders.get(LOGIN_URL) .header(“Authorization”, “Basic " + new String(encodedBytes))) .andExpect(status().isUnauthorized()); logger.debug(“3. 用户名密码正确”); username = “admin”; password = “admin”; encodedBytes = Base64.encodeBase64((username + “:” + password).getBytes()); logger.debug(“断言200”); mvcResult = this.mockMvc.perform(MockMvcRequestBuilders.get(LOGIN_URL) .header(“Authorization”, “Basic " + new String(encodedBytes))) .andExpect(status().isOk()) .andReturn(); logger.debug(“从返回体中获取token”); String json = mvcResult.getResponse().getContentAsString(); JSONObject jsonObject = JSON.parseObject(json); String token = jsonObject.getString(“token”); logger.debug(“空的token请求后台,断言401”); this.mockMvc.perform(MockMvcRequestBuilders.get(LOGIN_URL) .header(TOKEN_KEY, “”)) .andExpect(status().isUnauthorized()); logger.debug(“加上token请求后台,断言200”); this.mockMvc.perform(MockMvcRequestBuilders.get(LOGIN_URL) .header(TOKEN_KEY, token)) .andExpect(status().isOk()); logger.debug(“用户注销”); this.mockMvc.perform(MockMvcRequestBuilders.get(LOGOUT_URL) .header(TOKEN_KEY, token)) .andExpect(status().isOk()); logger.debug(“注销后,断言该token失效”); this.mockMvc.perform(MockMvcRequestBuilders.get(LOGIN_URL) .header(TOKEN_KEY, token)) .andExpect(status().isUnauthorized()); }}前台方法和这么复杂的后台设计相比较,前台没有啥技术含量,把代码粘贴出来大家参考参考即可,没什么要说的。前台Service:@Injectable({ providedIn: ‘root’,})export class AuthService { constructor(private http: _HttpClient) { } /* * 登录 * @param username 用户名 * @param password 密码 / public login(username: string, password: string): Observable<ITokenModel> { // 新建Headers,并添加认证信息 let headers = new HttpHeaders(); headers = headers.append(‘Content-Type’, ‘application/x-www-form-urlencoded’); headers = headers.append(‘Authorization’, ‘Basic ’ + btoa(username + ‘:’ + password)); // 发起get请求并返回 return this.http .get(’/auth/login?_allow_anonymous=true’, {}, { headers: headers }); } /* * 注销 / public logout(): Observable<any> { return this.http.get(’/auth/logout’); }}登录组件核心代码:this.authService.login(this.userName.value, this.password.value) .subscribe((response: ITokenModel) => { // 清空路由复用信息 this.reuseTabService.clear(); // 设置用户Token信息 this.tokenService.set(response); // 重新获取 StartupService 内容,我们始终认为应用信息一般都会受当前用户授权范围而影响 this.startupSrv.load().then(() => { // noinspection JSIgnoredPromiseFromCall this.router.navigateByUrl(‘main/index’); }); }, () => { // 显示错误信息提示 this.showLoginErrorInfo = true; });注销组件核心代码:// 调用Service进行注销this.authService.logout().subscribe(() => { }, () => { }, () => { // 清空token信息 this.tokenService.clear(); // 跳转到登录页面,因为无论是否注销成功都要跳转,写在complete中 // noinspection JSIgnoredPromiseFromCall this.router.navigateByUrl(this.tokenService.login_url); });前台拦截器有一点,headers.append(‘X-Requested-With’, ‘XMLHttpRequest’),如果不设置这个,在用户名密码错误的时候会弹出Spring Security原生的登录提示框。还有就是,为什么这里没有处理token,因为Ng-Alain的默认的拦截器已经对token进行添加处理。// noinspection SpellCheckingInspection/* * Yunzhi拦截器,用于实现添加url,添加header,全局异常处理 /@Injectable()export class YunzhiInterceptor implements HttpInterceptor { constructor(private router: Router) { } intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> { /* * 为request加上服务端前缀 / let url = req.url; if (!url.startsWith(‘https://’) && !url.startsWith(‘http://’)) { url = environment.SERVER_URL + url; } let request = req.clone({ url }); /* * 设置headers,防止弹出对话框 * https://stackoverflow.com/questions/37763186/spring-boot-security-shows-http-basic-auth-popup-after-failed-login / let headers = request.headers; headers = headers.append(‘X-Requested-With’, ‘XMLHttpRequest’); request = request.clone({ headers: headers }); /* * 数据过滤 */ return next.handle(request).pipe( // mergeMap = merge + map mergeMap((event: any) => { return of(event); }), // Observable对象发生错误时,执行catchError catchError((error: HttpErrorResponse) => { return this.handleHttpException(error); }), ); } private handleHttpException(error: HttpErrorResponse): Observable<HttpErrorResponse> { switch (error.status) { case 401: if (this.router.url !== ‘/passport/login’) { // noinspection JSIgnoredPromiseFromCall this.router.navigateByUrl(’/passport/login’); } break; case 403: case 404: case 500: // noinspection JSIgnoredPromiseFromCall this.router.navigateByUrl(/${error.status}); break; } // 最终将异常抛出来,便于组件个性化处理 throw new Error(error.error); }}解决H2控制台看不见问题Spring Security直接把H2数据库的控制台也拦截了,且禁止查看,启用以下配置恢复控制台查看。@Componentpublic class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http // 使用Basic认证方式进行验证进行验证 .httpBasic() // 要求SpringSecurity对后台的任何请求进行认证保护 .and().authorizeRequests().anyRequest().authenticated() // 关闭Security的Session,使用Spring Session .and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.NEVER) // 设置frameOptions为sameOrigin,否则看不见h2控制台 .and().headers().frameOptions().sameOrigin() // 禁用csrf,否则403. 这个在上线的时候判断是否需要开启 .and().csrf().disable(); }}总结一款又一款框架,是前辈们智慧的结晶。永远,文档比书籍更珍贵! ...

February 22, 2019 · 4 min · jiezi

SpringBoot 实战 (九) | 整合 Mybatis

微信公众号:一个优秀的废人如有问题或建议,请后台留言,我会尽力解决你的问题。前言如题,今天介绍 SpringBoot 与 Mybatis 的整合以及 Mybatis 的使用,本文通过注解的形式实现。什么是 MybatisMyBatis 是支持定制化 SQL、存储过程以及高级映射的优秀的持久层框架。MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集。MyBatis 可以对配置和原生 Map 使用简单的 XML 或注解,将接口和 Java 的 POJOs(Plain Old Java Objects,普通的 Java 对象)映射成数据库中的记录。优点:简单易学:本身就很小且简单。没有任何第三方依赖,最简单安装只要两个 jar 文件+配置几个 sql 映射文件易于学习,易于使用,通过文档和源代码,可以比较完全的掌握它的设计思路和实现。灵活:mybatis 不会对应用程序或者数据库的现有设计强加任何影响。 sql 写在 xml 里,便于统一管理和优化。通过 sql 基本上可以实现我们不使用数据访问框架可以实现的所有功能,或许更多。解除 sql 与程序代码的耦合:通过提供 DAL 层,将业务逻辑和数据访问逻辑分离,使系统的设计更清晰,更易维护,更易单元测试。sql 和代码的分离,提高了可维护性。提供映射标签,支持对象与数据库的 orm 字段关系映射提供对象关系映射标签,支持对象关系组建维护提供xml标签,支持编写动态 sql。缺点:编写 SQL 语句时工作量很大,尤其是字段多、关联表多时,更是如此。SQL 语句依赖于数据库,导致数据库移植性差,不能更换数据库。框架还是比较简陋,功能尚有缺失,虽然简化了数据绑定代码,但是整个底层数据库查询实际还是要自己写的,工作量也比较大,而且不太容易适应快速数据库修改。二级缓存机制不佳准备工作IDEAJDK1.8SpringBoot 2.1.3sql 语句,创建表,插入数据:CREATE TABLE student ( id int(11) NOT NULL AUTO_INCREMENT, name varchar(20) NOT NULL, age double DEFAULT NULL, PRIMARY KEY (id)) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;INSERT INTO student VALUES (‘1’, ‘aaa’, ‘21’);INSERT INTO student VALUES (‘2’, ‘bbb’, ‘22’);INSERT INTO student VALUES (‘3’, ‘ccc’, ‘23’);pom.xml 文件配置依赖<?xml version=“1.0” encoding=“UTF-8”?><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> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.1.3.RELEASE</version> <relativePath/> <!– lookup parent from repository –> </parent> <groupId>com.nasus</groupId> <artifactId>mybatis</artifactId> <version>0.0.1-SNAPSHOT</version> <name>mybatis</name> <description>mybatis Demo project for Spring Boot</description> <properties> <java.version>1.8</java.version> </properties> <dependencies> <!– 启动 web –> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!– mybatis 依赖 –> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.0.0</version> </dependency> <!– mysql 连接类 –> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <!– druid 数据库连接池–> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.1.9</version> </dependency> <!– lombok 插件 用于简化实体代码 –> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <!– 单元测试 –> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.hibernate.javax.persistence</groupId> <artifactId>hibernate-jpa-2.1-api</artifactId> <version>1.0.0.Final</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build></project>application.yaml 配置文件spring: datasource: url: jdbc:mysql://localhost:3306/test username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver实体类import javax.persistence.GeneratedValue;import javax.persistence.Id;import lombok.AllArgsConstructor;import lombok.Data;import lombok.NoArgsConstructor;@Data@AllArgsConstructor@NoArgsConstructorpublic class Student { @Id @GeneratedValue private Integer id; private String name; private Integer age;}使用了 lombok 简化了代码。dao 层import com.nasus.mybatis.domain.Student;import java.util.List;import org.apache.ibatis.annotations.Delete;import org.apache.ibatis.annotations.Insert;import org.apache.ibatis.annotations.Mapper;import org.apache.ibatis.annotations.Param;import org.apache.ibatis.annotations.Select;import org.apache.ibatis.annotations.Update;@Mapperpublic interface StudentMapper { @Insert(“insert into student(name, age) values(#{name}, #{age})”) int add(Student student); @Update(“update student set name = #{name}, age = #{age} where id = #{id}”) int update(@Param(“name”) String name, @Param(“age”) Integer age, @Param(“id”) Integer id); @Delete(“delete from student where id = #{id}”) int delete(int id); @Select(“select id, name as name, age as age from student where id = #{id}”) Student findStudentById(@Param(“id”) Integer id); @Select(“select id, name as name, age as age from student”) List<Student> findStudentList();}这里有必要解释一下,@Insert 、@Update、@Delete、@Select 这些注解中的每一个代表了执行的真实 SQL。 它们每一个都使用字符串数组 (或单独的字符串)。如果传递的是字符串数组,它们由每个分隔它们的单独空间串联起来。这就当用 Java 代码构建 SQL 时避免了“丢失空间”的问题。 然而,如果你喜欢,也欢迎你串联单独 的字符串。属性:value,这是字符串 数组用来组成单独的 SQL 语句。@Param 如果你的映射方法的形参有多个,这个注解使用在映射方法的参数上就能为它们取自定义名字。若不给出自定义名字,多参数(不包括 RowBounds 参数)则先以 “param” 作前缀,再加上它们的参数位置作为参数别名。例如 #{param1},#{param2},这个是默认值。如果注解是 @Param(“id”),那么参数就会被命名为 #{id}。service 层import com.nasus.mybatis.domain.Student;import java.util.List;public interface StudentService { int add(Student student); int update(String name, Integer age, Integer id); int delete(Integer id); Student findStudentById(Integer id); List<Student> findStudentList();}实现类:import com.nasus.mybatis.dao.StudentMapper;import com.nasus.mybatis.domain.Student;import com.nasus.mybatis.service.StudentService;import java.util.List;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Service;@Servicepublic class StudentServiceImpl implements StudentService { @Autowired private StudentMapper studentMapper; /** * 添加 Student * @param name * @param age * @return / @Override public int add(Student student) { return studentMapper.add(student); } /* * 更新 Student * @param name * @param age * @param id * @return / @Override public int update(String name, Integer age, Integer id) { return studentMapper.update(name,age,id); } /* * 删除 Student * @param id * @return / @Override public int delete(Integer id) { return studentMapper.delete(id); } /* * 根据 id 查询 Student * @param id * @return / @Override public Student findStudentById(Integer id) { return studentMapper.findStudentById(id); } /* * 查询所有的 Student * @return */ @Override public List<Student> findStudentList() { return studentMapper.findStudentList(); }}controller 层构建 restful APIimport com.nasus.mybatis.domain.Student;import com.nasus.mybatis.service.StudentService;import java.util.List;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.web.bind.annotation.DeleteMapping;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.PathVariable;import org.springframework.web.bind.annotation.PostMapping;import org.springframework.web.bind.annotation.PutMapping;import org.springframework.web.bind.annotation.RequestBody;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RequestParam;import org.springframework.web.bind.annotation.RestController;@RestController@RequestMapping("/Student”)public class StudentController { @Autowired private StudentService studentService; @PostMapping(””) public int add(@RequestBody Student student){ return studentService.add(student); } @PutMapping("/{id}") public int updateStudent(@PathVariable(“id”) Integer id, @RequestParam(value = “name”, required = true) String name, @RequestParam(value = “age”, required = true) Integer age){ return studentService.update(name,age,id); } @DeleteMapping("/{id}") public void deleteStudent(@PathVariable(“id”) Integer id){ studentService.delete(id); } @GetMapping("/{id}") public Student findStudentById(@PathVariable(“id”) Integer id){ return studentService.findStudentById(id); } @GetMapping("/list") public List<Student> findStudentList(){ return studentService.findStudentList(); }}测试结果其他接口已通过 postman 测试,无问题。源码下载:github 地址后语以上为 SpringBoot 实战 (九) | 整合 Mybatis 的教程,除了注解方式实现以外,Mybatis 还提供了 XML 方式实现。想了解更多用法请移步官方文档。最后,对 Python 、Java 感兴趣请长按二维码关注一波,我会努力带给你们价值,如果觉得本文对你哪怕有一丁点帮助,请帮忙点好看,让更多人知道。另外,关注之后在发送 1024 可领取免费学习资料。资料内容详情请看这篇旧文:Python、C++、Java、Linux、Go、前端、算法资料分享 ...

February 20, 2019 · 3 min · jiezi

Spring Boot MyBatis配置多种数据库

mybatis-config.xml是支持配置多种数据库的,本文将介绍在Spring Boot中使用配置类来配置。1. 配置application.yml# mybatis配置mybatis: check-config-location: false type-aliases-package: ${base.package}.model configuration: map-underscore-to-camel-case: true # 二级缓存的总开关 cache-enabled: false mapper-locations: classpath:mapping/*.xml2. 新增数据源配置类/** * 数据源配置 * @author simon * @date 2019-02-18 /@Configurationpublic class DataSourceConfig { @Value("${mybatis.mapper-locations}") private String mapperLocations; @Primary @Bean @ConfigurationProperties(“spring.datasource.druid”) public DataSource dataSource(){ return DruidDataSourceBuilder.create().build(); } @Bean public JdbcTemplate jdbcTemplate(){ return new JdbcTemplate(dataSource()); } @Bean public DatabaseIdProvider databaseIdProvider(){ DatabaseIdProvider databaseIdProvider = new VendorDatabaseIdProvider(); Properties p = new Properties(); p.setProperty(“Oracle”, “oracle”); p.setProperty(“MySQL”, “mysql”); p.setProperty(“PostgreSQL”, “postgresql”); p.setProperty(“DB2”, “db2”); p.setProperty(“SQL Server”, “sqlserver”); databaseIdProvider.setProperties(p); return databaseIdProvider; } @Bean public SqlSessionFactoryBean sqlSessionFactoryBean() throws Exception { SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean(); factoryBean.setDataSource(dataSource()); factoryBean.setDatabaseIdProvider(databaseIdProvider()); factoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(mapperLocations)); return factoryBean; }}3. 在mapper.xml中使用方法1 <select id=“findAuthorityByUrl” resultType=“java.lang.String” databaseId=“mysql”> SELECT group_concat( tsma.authority ) as authority FROM t_side_menu tsm LEFT JOIN t_side_menu_authority tsma ON tsm.id = tsma.side_menu_id </select> <select id=“findAuthorityByUrl” resultType=“java.lang.String” databaseId=“postgresql”> SELECT string_agg( tsma.authority, ‘,’) as authority FROM t_side_menu tsm LEFT JOIN t_side_menu_authority tsma ON tsm.id = tsma.side_menu_id </select>方法2 <select id=“selectByPids” parameterType=“String” resultMap=“SuperResultMap”> SELECT tsm., <if test="_databaseId == ‘mysql’"> group_concat( tsma.authority ) as authority </if> <if test="_databaseId == ‘postgresql’"> string_agg( tsma.authority, ‘,’) as authority </if> FROM t_side_menu tsm LEFT JOIN t_side_menu_authority tsma ON tsm.id = tsma.side_menu_id WHERE pid IN (#{pids}) GROUP BY tsm.id </select>题外话如果有兴趣,请给oauthserer项目一个star。oauthserver是一个基于Spring Boot Oauth2的完整的独立的Oauth2 Server微服务。项目的目的是,仅仅需要创建相关数据表,修改数据库的连接信息,你就可以得到一个Oauth2 Server微服务。 ...

February 18, 2019 · 1 min · jiezi

SpringBoot入门系列HelloWorld

根据咱们程序员学习的惯例,学习一门新技术都是从HelloWorld开始的。感觉编程是一件非常富有意义的事情,程序员也是一群可爱的人,渴望被关怀和关注,因为我们总在和世界say Hi.好了进入正题创建项目首先创建一个项目,可看我上一篇文章写得IntelliJ IDEA创建第一个Spring boot项目接下来运行这个项目,你将会看到如下页面提示我们当前没有准确的映射,所以找不到对应的页面也就是404。莫慌,接下来咱们处理一下创建HelloController控制器在项目名/src/main/java/包名下,新建一个config包,包下面创建HelloController@Controllerpublic class HelloController { @RequestMapping(value = “/hello”,method = RequestMethod.GET) @ResponseBody public String hello(){ return “Hello World”; }}注解说明:@Controller: 可让项目扫描自动检测到这个类,处理http请求@ RequestMapping 请求的路由映射,访问的路径就是:http://localhost:8080/hellovalue: 路由名method: 请求方式,GET,POST,PUT,DELETE等重新启动项目访问:http://localhost:8080/hello, 就看到Hello World了看到如上图所示,就表示我们的hello world成功了。目录结构:src/main/java: Java代码的目录src/main/resources: 资源目录src/test/java: 测试代码的目录src/test/resources: 测试资源目录文件说明pom.xml文件父项目<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.1.3.RELEASE</version> <relativePath/> <!– lookup parent from repository –></parent>管理Spring Boot应用里面所依赖的版本管理依赖<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies>Spring Boot将所有的功能场景都抽取出来,做成一个个的starters(启动器),只需要在项目里面引入这些starter相关场景的所有依赖都会导入进来,要用什么功能就导入什么场景的启动器主程序类,入口类@SpringBootApplication : Spring Boot应用标注在某个类上说明这个类是SpringBoot的主配置类,SpringBoot就应该运行这个类的main方法来启动SpringBoot应用;我的网站:https://wayne214.github.io

February 16, 2019 · 1 min · jiezi

手把手教你如何优雅的使用Aop记录带参数的复杂Web接口日志

前言不久前,因为需求的原因,需要实现一个操作日志。几乎每一个接口被调用后,都要记录一条跟这个参数挂钩的特定的日志到数据库。举个例子,就比如禁言操作,日志中需要记录因为什么禁言,被禁言的人的id和各种信息。方便后期查询。这样的接口有很多个,而且大部分接口的参数都不一样。可能大家很容易想到的一个思路就是,实现一个日志记录的工具类,然后在需要记录日志的接口中,添加一行代码。由这个日志工具类去判断此时应该处理哪些参数。但是这样有很大的问题。如果需要记日志的接口数量非常多,先不讨论这个工具类中需要做多少的类型判断,仅仅是给所有接口添加这样一行代码在我个人看来都是不能接受的行为。首先,这样对代码的侵入性太大。其次,后期万一有改动,维护的人将会十分难受。想象一下,全局搜索相同的代码,再一一进行修改。所以我放弃了这个略显原始的方法。我最终采用了Aop的方式,采取拦截的请求的方式,来记录日志。但是即使采用这个方法,仍然面临一个问题,那就是如何处理大量的参数。以及如何对应到每一个接口上。我最终没有拦截所有的controller,而是自定义了一个日志注解。所有打上了这个注解的方法,将会记录日志。同时,注解中会带有类型,来为当前的接口指定特定的日志内容以及参数。<!–more–>那么如何从众多可能的参数中,为当前的日志指定对应的参数呢。我的解决方案是维护一个参数类,里面列举了所有需要记录在日志中的参数名。然后在拦截请求时,通过反射,获取到该请求的request和response中的所有参数和值,如果该参数存在于我维护的param类中,则将对应的值赋值进去。然后在请求结束后,将模板中的所有预留的参数全部用赋了值的参数替换掉。这样一来,在不大量的侵入业务的前提下,满足了需求,同时也保证了代码的可维护性。下面我将会把详细的实现过程列举出来。开始操作前文章结尾我会给出这个demo项目的所有源码。所以不想看过程的兄台可移步到末尾,直接看源码。(听说和源码搭配,看文章更美味…)开始操作新建项目大家可以参考我之前写的另一篇文章,手把手教你从零开始搭建SpringBoot后端项目框架。只要能请求简单的接口就可以了。本项目的依赖如下。<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <version>2.1.1.RELEASE</version></dependency><!– https://mvnrepository.com/artifact/org.aspectj/aspectjrt –><dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjrt</artifactId> <version>1.9.2</version></dependency><!– https://mvnrepository.com/artifact/org.aspectj/aspectjweaver –><dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> <version>1.9.2</version></dependency><dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.2</version></dependency><dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>4.1.14</version></dependency>新建Aop类新建LogAspect类。代码如下。package spring.aop.log.demo.api.util;import org.aspectj.lang.annotation.Aspect;import org.aspectj.lang.annotation.Pointcut;import org.springframework.stereotype.Component;/** * LogAspect * * @author Lunhao Hu * @date 2019-01-30 16:21 /@Aspect@Componentpublic class LogAspect { / * 定义切入点 / @Pointcut("@annotation(spring.aop.log.demo.api.util.Log)") public void operationLog() { } /* * 新增结果返回后触发 * * @param point * @param returnValue / @AfterReturning(returning = “returnValue”, pointcut = “operationLog() && @annotation(log)”) public void doAfterReturning(JoinPoint point, Object returnValue, Log log) { System.out.println(“test”); }}Pointcut中传入了一个注解,表示凡是打上了这个注解的方法,都会触发由Pointcut修饰的operationLog函数。而AfterReturning则是在请求返回之后触发。自定义注解上一步提到了自定义注解,这个自定义注解将打在controller的每个方法上。新建一个annotation的类。代码如下。package spring.aop.log.demo.api.util;import java.lang.annotation.ElementType;import java.lang.annotation.Retention;import java.lang.annotation.RetentionPolicy;import java.lang.annotation.Target;/* * Log * * @author Lunhao Hu * @date 2019-01-30 16:19 /@Target(ElementType.METHOD)@Retention(RetentionPolicy.RUNTIME)public @interface Log { String type() default “”;}Target和Retention都属于元注解。共有4种,分别是@Retention、@Target、@Document、@Inherited。Target注解说明了该Annotation所修饰的范围。可以传入很多类型,参数为ElementType。例如TYPE,用于描述类、接口或者枚举类;FIELD用于描述属性;METHOD用于描述方法;PARAMETER用于描述参数;CONSTRUCTOR用于描述构造函数;LOCAL_VARIABLE用于描述局部变量;ANNOTATION_TYPE用于描述注解;PACKAGE用于描述包等。Retention注解定义了该Annotation被保留的时间长短。参数为RetentionPolicy。例如SOURCE表示只在源码中存在,不会在编译后的class文件存在;CLASS是该注解的默认选项。 即存在于源码,也存在于编译后的class文件,但不会被加载到虚拟机中去;RUNTIME存在于源码、class文件以及虚拟机中,通俗一点讲就是可以在运行的时候通过反射获取到。加上普通注解给需要记录日志的接口加上Log注解。package spring.aop.log.demo.api.controller;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.PathVariable;import org.springframework.web.bind.annotation.RestController;import spring.aop.log.demo.api.util.Log;/ * HelloController * * @author Lunhao Hu * @date 2019-01-30 15:52 /@RestControllerpublic class HelloController { @Log @GetMapping(“test/{id}”) public String test(@PathVariable(name = “id”) Integer id) { return “Hello” + id; }}加上之后,每一次调用test/{id}这个接口,都会触发拦截器中的doAfterReturning方法中的代码。加上带类型注解上面介绍了记录普通日志的方法,接下来要介绍记录特定日志的方法。什么特定日志呢,就是每个接口要记录的信息不同。为了实现这个,我们需要实现一个操作类型的枚举类。代码如下。操作类型模板枚举新建一个枚举类Type。代码如下。package spring.aop.log.demo.api.util;/ * Type * * @author Lunhao Hu * @date 2019-01-30 17:12 /public enum Type { / * 操作类型 / WARNING(“警告”, “因被其他玩家举报,警告玩家”); /* * 类型 / private String type; /* * 执行操作 / private String operation; Type(String type, String operation) { this.type = type; this.operation = operation; } public String getType() { return type; } public String getOperation() { return operation; }}给注解加上类型给上面的controller中的注解加上type。代码如下。package spring.aop.log.demo.api.controller;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.PathVariable;import org.springframework.web.bind.annotation.RestController;import spring.aop.log.demo.api.util.Log;/* * HelloController * * @author Lunhao Hu * @date 2019-01-30 15:52 /@RestControllerpublic class HelloController { @Log(type = “WARNING”) @GetMapping(“test/{id}”) public String test(@PathVariable(name = “id”) Integer id) { return “Hello” + id; }}修改aop类将aop类中的doAfterReturning为如下。@AfterReturning(returning = “returnValue”, pointcut = “operationLog() && @annotation(log)")public void doAfterReturning(JoinPoint point, Object returnValue, Log log) { // 注解中的类型 String enumKey = log.type(); System.out.println(Type.valueOf(enumKey).getOperation());}加上之后,每一次调用加了@Log(type = “WARNING”)这个注解的接口,都会打印这个接口所指定的日志。例如上述代码就会打印出如下代码。因被其他玩家举报,警告玩家获取aop拦截的请求参数为每个接口指定一个日志并不困难,只需要为每个接口指定一个类型即可。但是大家应该也注意到了,一个接口日志,只记录因被其他玩家举报,警告玩家这样的信息没有任何意义。记录日志的人倒不觉得,而最后去查看日志的人就要吾日三省吾身了,被谁举报了?因为什么举报了?我警告的谁?这样的日志做了太多的无用功,根本没有办法在出现问题之后溯源。所以我们下一步的操作就是给每个接口加上特定的参数。那么大家可能会有问题,如果每个接口的参数几乎都不一样,那这个工具类岂不是要传入很多参数,要怎么实现呢,甚至还要组织参数,这样会大量的侵入业务代码,并且会大量的增加冗余代码。大家可能会想到,实现一个记录日志的方法,在要记日志的接口中调用,把参数传进去。如果类型很多的话,参数也会随之增多,每个接口的参数都不一样。处理起来十分麻烦,而且对业务的侵入性太高。几乎每个地方都要嵌入日志相关代码。一旦涉及到修改,将会变得十分难维护。所以我直接利用反射获取aop拦截到的请求中的所有参数,如果我的参数类(所有要记录的参数)里面有请求中的参数,那么我就将参数的值写入参数类中。最后将日志模版中参数预留字段替换成请求中的参数。流程图如下所示。新建参数类新建一个类Param,其中包含所有在操作日志中,可能会出现的参数。为什么要这么做?因为每个接口需要的参数都有可能完全不一样,与其去维护大量的判断逻辑,还不如贪心一点,直接传入所有的可能参数。当然后期如果有新的参数需要记录,则需要修改代码。package spring.aop.log.demo.api.util;import lombok.Data;/ * Param * * @author Lunhao Hu * @date 2019-01-30 17:14 /@Datapublic class Param { / * 所有可能参数 / private String id; private String workOrderNumber; private String userId;}修改模板将模板枚举类中的WARNING修改为如下。WARNING(“警告”, “因 工单号 [(%workOrderNumber)] /举报 ID [(%id)] 警告玩家 [(%userId)]”);其中的参数,就是要在aop拦截阶段获取并且替换掉的参数。修改controller我们给之前的controller加上上述模板中国呢的参数。部分代码如下。@Log(type = “WARNING”)@GetMapping(“test/{id}")public String test( @PathVariable(name = “id”) Integer id, @RequestParam(name = “workOrderNumber”) String workOrderNumber, @RequestParam(name = “userId”) String userId, @RequestParam(name = “name”) String name) { return “Hello” + id;}通过反射获取请求的参数在此处分两种情况,一种是简单参数类型,另外一种是复杂参数类型,也就是参数中带了请求DTO的情况。获取简单参数类型给aop类添加几个私有变量。/* * 请求中的所有参数 /private Object[] args;/* * 请求中的所有参数名 /private String[] paramNames;/* * 参数类 /private Param params;然后将doAfterReturning中的代码改成如下。try { // 获取请求详情 ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); HttpServletRequest request = attributes.getRequest(); HttpServletResponse response = attributes.getResponse(); // 获取所有请求参数 Signature signature = point.getSignature(); MethodSignature methodSignature = (MethodSignature) signature; this.paramNames = methodSignature.getParameterNames(); this.args = point.getArgs(); // 实例化参数类 this.params = new Param(); // 注解中的类型 String enumKey = log.type(); String logDetail = Type.valueOf(enumKey).getOperation(); // 从请求传入参数中获取数据 this.getRequestParam();} catch (Exception e) { System.out.println(e.getMessage());}首先要做的就是拦截打上了自定义注解的请求。我们可以获取到请求的详情,以及请求中的所有的参数名,以及参数。下面我们就来实现上述代码中的getRequestParam方法。getRequestParam/* * 获取拦截的请求中的参数 * @param point /private void getRequestParam() { // 获取简单参数类型 this.getSimpleParam();}getSimpleParam/* * 获取简单参数类型的值 /private void getSimpleParam() { // 遍历请求中的参数名 for (String reqParam : this.paramNames) { // 判断该参数在参数类中是否存在 if (this.isExist(reqParam)) { this.setRequestParamValueIntoParam(reqParam); } }}上述代码中,遍历请求所传入的参数名,然后我们实现isExist方法, 来判断这个参数在我们的Param类中是否存在,如果存在我们就再调用setRequestParamValueIntoParam方法,将这个参数名所对应的参数值写入到Param类的实例中。isExistisExist的代码如下。/* * 判断该参数在参数类中是否存在(是否是需要记录的参数) * @param targetClass * @param name * @param <T> * @return /private <T> Boolean isExist(String name) { boolean exist = true; try { String key = this.setFirstLetterUpperCase(name); Method targetClassGetMethod = this.params.getClass().getMethod(“get” + key); } catch (NoSuchMethodException e) { exist = false; } return exist;}在上面我们也提到过,在编译的时候会加上getter和setter,所以参数名的首字母都会变成大写,所以我们需要自己实现一个setFirstLetterUpperCase方法,来将我们传入的参数名的首字母变成大写。setFirstLetterUpperCase代码如下。/* * 将字符串的首字母大写 * * @param str * @return /private String setFirstLetterUpperCase(String str) { if (str == null) { return null; } return str.substring(0, 1).toUpperCase() + str.substring(1);}setRequestParamValueIntoParam代码如下。/* * 从参数中获取 * @param paramName * @return /private void setRequestParamValueIntoParam(String paramName) { int index = ArrayUtil.indexOf(this.paramNames, paramName); if (index != -1) { String value = String.valueOf(this.args[index]); this.setParam(this.params, paramName, value); }}ArrayUtil是hutool中的一个工具函数。用来判断在一个元素在数组中的下标。setParam代码如下。/* * 将数据写入参数类的实例中 * @param targetClass * @param key * @param value * @param <T> /private <T> void setParam(T targetClass, String key, String value) { try { Method targetClassParamSetMethod = targetClass.getClass().getMethod(“set” + this.setFirstLetterUpperCase(key), String.class); targetClassParamSetMethod.invoke(targetClass, value); } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { e.printStackTrace(); }}该函数使用反射的方法,获取该参数的set方法,将Param类中对应的参数设置成传入的值。运行启动项目,并且请求controller中的方法。并且传入定义好的参数。http://localhost:8080/test/8?workOrderNumber=3231732&userId=748327843&name=testName该GET请求总共传入了4个参数,分别是id,workOrderNumber,userId, name。大家可以看到,在Param类中并没有定义name这个字段。这是特意加了一个不需要记录的参数,来验证我们接口的健壮性的。运行之后,可以看到控制台打印的信息如下。Param(id=8, workOrderNumber=3231732, userId=748327843)我们想让aop记录的参数全部记录到Param类中的实例中,而传入了意料之外的参数也没有让程序崩溃。接下里我们只需要将这些参数,将之前定义好的模板的参数预留字段替换掉即可。替换参数在doAfterReturning中的getRequestParam函数后,加入以下代码。if (!logDetail.isEmpty()) { // 将模板中的参数全部替换掉 logDetail = this.replaceParam(logDetail);}System.out.println(logDetail);下面我们实现replaceParam方法。replaceParam代码如下。/* * 将模板中的预留字段全部替换为拦截到的参数 * @param template * @return /private String replaceParam(String template) { // 将模板中的需要替换的参数转化成map Map<String, String> paramsMap = this.convertToMap(template); for (String key : paramsMap.keySet()) { template = template.replace(”%” + key, paramsMap.get(key)).replace("(", “”).replace(")", “”); } return template;}convertToMap方法将模板中的所有预留字段全部提取出来,当作一个Map的Key。convertToMap代码如下。/* * 将模板中的参数转换成map的key-value形式 * @param template * @return /private Map<String, String> convertToMap(String template) { Map<String, String> map = new HashMap<>(); String[] arr = template.split("\("); for (String s : arr) { if (s.contains("%")) { String key = s.substring(s.indexOf("%"), s.indexOf(")")).replace("%", “”).replace(")", “”).replace("-", “”).replace("]", “”); String value = this.getParam(this.params, key); map.put(key, “null”.equals(value) ? “(空)” : value); } } return map;}其中的getParam方法,类似于setParam,也是利用反射的方法,通过传入的Class和Key,获取对应的值。getParam代码如下。/* * 通过反射获取传入的类中对应key的值 * @param targetClass * @param key * @param <T> /private <T> String getParam(T targetClass, String key) { String value = “”; try { Method targetClassParamGetMethod = targetClass.getClass().getMethod(“get” + this.setFirstLetterUpperCase(key)); value = String.valueOf(targetClassParamGetMethod.invoke(targetClass)); } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { e.printStackTrace(); } return value;}再次运行再次请求上述的url,则可以看到控制台的输出如下。因 工单号 [3231732] /举报 ID [8] 警告玩家 [748327843]可以看到,我们需要记录的所有的参数,都被正确的替换了。而不需要记录的参数,同样也没有对程序造成影响。让我们试试传入不传入非必选参数,会是什么样。修改controller如下,把workOrderNumber改成非必须按参数。@Log(type = “WARNING”)@GetMapping(“test/{id}")public String test( @PathVariable(name = “id”) Integer id, @RequestParam(name = “workOrderNumber”, required = false) String workOrderNumber, @RequestParam(name = “userId”) String userId, @RequestParam(name = “name”) String name) { return “Hello” + id;}请求如下url。http://localhost:8080/test/8?userId=748327843&name=testName然后可以看到,控制台的输出如下。因 工单号 [空] /举报 ID [8] 警告玩家 [748327843]并不会影响程序的正常运行。获取复杂参数类型接下来要介绍的是如何记录复杂参数类型的日志。其实,大致的思路是不变的。我们看传入的类中的参数,有没有需要记录的。有的话就按照上面记录简单参数的方法来替换记录参数。定义测试复杂类型新建TestDTO。代码如下。package spring.aop.log.demo.api.util;import lombok.Data;/* * TestDto * * @author Lunhao Hu * @date 2019-02-01 15:02 /@Datapublic class TestDTO { private String name; private Integer age; private String email;}修改Param将上面的所有的参数全部添加到Param类中,全部定义成字符串类型。package spring.aop.log.demo.api.util;import lombok.Data;/ * Param * * @author Lunhao Hu * @date 2019-01-30 17:14 /@Datapublic class Param { / * 所有可能参数 / private String id; private String age; private String workOrderNumber; private String userId; private String name; private String email;}修改模板将WARNING模板修改如下。/* * 操作类型 /WARNING(“警告”, “因 工单号 [(%workOrderNumber)] /举报 ID [(%id)] 警告玩家 [(%userId)], 游戏名 [(%name)], 年龄 [(%age)]”);修改controller@Log(type = “WARNING”)@PostMapping(“test/{id}")public String test( @PathVariable(name = “id”) Integer id, @RequestParam(name = “workOrderNumber”, required = false) String workOrderNumber, @RequestParam(name = “userId”) String userId, @RequestBody TestDTO testDTO) { return “Hello” + id;}修改getRequestParam/* * 获取拦截的请求中的参数 * @param point /private void getRequestParam() { // 获取简单参数类型 this.getSimpleParam(); // 获取复杂参数类型 this.getComplexParam();}接下来实现getComplexParam方法。getComplexParam/* * 获取复杂参数类型的值 /private void getComplexParam() { for (Object arg : this.args) { // 跳过简单类型的值 if (arg != null && !this.isBasicType(arg)) { this.getFieldsParam(arg); } }}getFieldsParam/* * 遍历一个复杂类型,获取值并赋值给param * @param target * @param <T> /private <T> void getFieldsParam(T target) { Field[] fields = target.getClass().getDeclaredFields(); for (Field field : fields) { String paramName = field.getName(); if (this.isExist(paramName)) { String value = this.getParam(target, paramName); this.setParam(this.params, paramName, value); } }}运行启动项目。使用postman对上面的url发起POST请求。请求body中带上TestDTO中的参数。请求成功返回后就会看到控制台输出如下。因 工单号 [空] /举报 ID [8] 警告玩家 [748327843], 游戏名 [tom], 年龄 [12]然后就可以根据需求,将上面的日志记录到相应的地方。到这可能有些哥们就觉得行了,万事具备,只欠东风。但其实这样的实现方式,还存在几个问题。比如,如果请求失败了怎么办?请求失败,在需求上将,是根本不需要记录操作日志的,但是即使请求失败也会有返回值,就代表日志也会成功的记录。这就给后期查看日志带来了很大的困扰。再比如,如果我需要的参数在返回值中怎么办?如果你没有用统一的生成唯一id的服务,就会遇到这个问题。就比如我需要往数据库中插入一条新的数据,我需要得到数据库自增id,而我们的日志拦截只拦截了请求中的参数。所以这就是我们接下来要解决的问题。判断请求是否成功实现success函数,代码如下。/* * 根据http状态码判断请求是否成功 * * @param response * @return /private Boolean success(HttpServletResponse response) { return response.getStatus() == 200;}然后将getRequestParam之后的所有操作,包括getRequestParam本身,用success包裹起来。如下。if (this.success(response)) { // 从请求传入参数中获取数据 this.getRequestParam(); if (!logDetail.isEmpty()) { // 将模板中的参数全部替换掉 logDetail = this.replaceParam(logDetail); }}这样一来,就可以保证只有在请求成功的前提下,才会记录日志。通过反射获取返回的参数新建Result类在一个项目中,我们用一个类来统一返回值。package spring.aop.log.demo.api.util;import lombok.Data;/* * Result * * @author Lunhao Hu * @date 2019-02-01 16:47 /@Datapublic class Result { private Integer id; private String name; private Integer age; private String email;}修改controller@Log(type = “WARNING”)@PostMapping(“test”)public Result test( @RequestParam(name = “workOrderNumber”, required = false) String workOrderNumber, @RequestParam(name = “userId”) String userId, @RequestBody TestDTO testDTO) { Result result = new Result(); result.setId(1); result.setAge(testDTO.getAge()); result.setName(testDTO.getName()); result.setEmail(testDTO.getEmail()); return result;}运行启动项目,发起POST请求会发现,返回值如下。{ “id”: 1, “name”: “tom”, “age”: 12, “email”: “test@test.com”}而控制台的输出如下。因 工单号 [39424] /举报 ID [空] 警告玩家 [748327843], 游戏名 [tom], 年龄 [12]可以看到,id没有被获取到。所以我们还需要添加一个函数,从返回值中获取id的数据。getResponseParam在getRequestParam后,添加方法getResponseParam,直接调用之前写好的函数。代码如下。/ * 从返回值从获取数据 */private void getResponseParam(Object value) { this.getFieldsParam(value);}运行再次发起POST请求,可以发现控制台的输出如下。因 工单号 [39424] /举报 ID [1] 警告玩家 [748327843], 游戏名 [tom], 年龄 [12]一旦得到了这条信息,我们就可以把它记录到任何我们想记录的地方。项目源码地址想要参考源码的大佬请戳 ->这里<- ...

February 11, 2019 · 6 min · jiezi

Spring Boot 发起 HTTP 请求

起步新年目标Spring Cloud开始实施,打开慕课网。刚学了一章,大体就是调用中国天气网的api,使用Spring Boot构建自己的天气预报系统,然后使用Spring Cloud,一步一步使用微服务的思想来演进架构。小目标昨天去百度抢了新年红包,感叹百度的高并发做的也是如此优秀。阿里的双十一,百度的新年。(发现了二者的共同点,可能也是解决并发的一种思路,并发的时候只允许增加数据。)感叹归感叹,期望着学习完Spring Cloud也能设计出优秀的架构,解决并发的一些问题。遇到的问题学习时也跟着课程进行编码,讲师讲的非常好,但是本课程的重点是后面的微服务架构,所以前面的功能有一些瑕疵,特此提出自己的实现,供大家学习交流。功能描述最初的功能很简单,因为后台是没有任何数据的,所以前台有请求,就直接去天气网要数据,然后再返回去。数据序列化问题这是天气网api返回来的数据格式,乍一看没啥毛病。{ “data”: { “yesterday”: { “date”: “4日星期一”, “high”: “高温 26℃”, “fx”: “无持续风向”, “low”: “低温 18℃”, “fl”: “<![CDATA[<3级]]>”, “type”: “多云” }, “city”: “深圳”, “forecast”: [ { “date”: “5日星期二”, “high”: “高温 25℃”, “fengli”: “<![CDATA[<3级]]>”, “low”: “低温 18℃”, “fengxiang”: “无持续风向”, “type”: “多云” }, { “date”: “6日星期三”, “high”: “高温 26℃”, “fengli”: “<![CDATA[<3级]]>”, “low”: “低温 17℃”, “fengxiang”: “无持续风向”, “type”: “多云” }, { “date”: “7日星期四”, “high”: “高温 27℃”, “fengli”: “<![CDATA[<3级]]>”, “low”: “低温 18℃”, “fengxiang”: “无持续风向”, “type”: “多云” }, { “date”: “8日星期五”, “high”: “高温 26℃”, “fengli”: “<![CDATA[<3级]]>”, “low”: “低温 17℃”, “fengxiang”: “无持续风向”, “type”: “多云” }, { “date”: “9日星期六”, “high”: “高温 24℃”, “fengli”: “<![CDATA[<3级]]>”, “low”: “低温 14℃”, “fengxiang”: “无持续风向”, “type”: “小雨” } ], “ganmao”: “相对今天出现了较大幅度降温,较易发生感冒,体质较弱的朋友请注意适当防护。”, “wendu”: “23” }, “status”: 1000, “desc”: “OK”}缺点1:有拼音;ganmao、wendu。缺点2:名称不一致;理论上来说yesterday与forecast应该是同一个实体,都表示一天的天气情况,只是名称不同。但是在yesterday中,风向和风力是fx和fl,在forecast中,名称却是fengli、fengxiang。解决此问题,想到的思路就是使用jackson进行序列化与反序列化时进行配置的一些注解。最初使用此种方法实现:@JsonProperty(“wendu”)private Float temperature;一个对象中的名字,一个json数据中的名字。可以实现,但是不好。举个例子,天气api返回给我wendu,添加了@JsonProperty,然后wendu就绑定到了temperature上,但是如果我前台再返回该对象,序列化后生成的名称还是wendu。不好!目标是实现,反序列化时:从wendu能绑定到我的temperature,序列化时直接使用我的字段名。get、set尝试猜测是不是和get、set方法有关。就把@JsonProperty(“wendu”)添加到set方法上,发现并没有用。JsonAlias后来经过查询,原来是注解用错了,此种情况应使用别名。关于JsonProperty和JsonAlias的详细讲解,请参考Jackson @JsonProperty and @JsonAlias Example。@JsonAlias(“wendu”)private Float temperature;同时,可以应用多个别名:@JsonAlias({“fengli”, “fl”})private String windForce;发起请求发起请求的示例代码,供以后参考。@Autowiredprivate RestTemplate restTemplate;@Overridepublic Weather getWeatherByCityName(String cityName) { return this.getWeatherByUrl(BASE_URL + “?” + CITY_NAME + “=” + cityName) .getData();}private Response getWeatherByUrl(String url) { // 发起Get请求 ResponseEntity<String> response = restTemplate.getForEntity(url, String.class); // 如果状态码非200, 抛异常 if (response.getStatusCodeValue() != 200) { throw new YunzhiNetworkException(“数据请求失败”); } // 实例化对象映射对象 ObjectMapper mapper = new ObjectMapper(); // 初始化响应数据 Response data; // 从字符串转换为Response对象 try { data = mapper.readValue(response.getBody(), Response.class); } catch (IOException e) { throw new YunzhiIOException(“json数据转换失败”); } // 返回 return data;}RestTemplate配置这里与正常的RestTemplate构建有些不同,通常的RestTemplate是使用Spring工具类构造的,此处使用Apache的Http组件构造,以支持更多的数据格式。implementation ‘org.apache.httpcomponents:httpclient’同时去除了默认的对String的Http消息转换器,默认的转换器使用的不是UTF-8编码。讲师原文章:Spring RestTemplate 调用天气预报接口乱码的解决@Configurationpublic class BeanConfiguration { @Bean public RestTemplate restTemplate() { // 使用Apache HttpClient构建RestTemplate, 支持的比Spring自带的更多 RestTemplate restTemplate = new RestTemplate(new HttpComponentsClientHttpRequestFactory()); // 去除默认的String转换器 restTemplate.getMessageConverters().removeIf(converter -> converter instanceof StringHttpMessageConverter); // 添加自定义的String转换器, 支持UTF-8 restTemplate.getMessageConverters() .add(new StringHttpMessageConverter(StandardCharsets.UTF_8)); return restTemplate; }}更完善的单元测试同时,在编写单元测试的时候,看了一篇关于AssertJ的文章。Testing with AssertJ assertions - Tutorial之前学Junit5的时候,觉得这个东西挺好使的啊?为什么被开源社区抛弃而使用AssertJ呢?原来之前用的断言都太简单,其实AssertJ远比我们使用的更强大。@Testpublic void getWeatherByCityName() throws Exception { final String cityName = “深圳”; MvcResult mvcResult = this.mockMvc .perform(MockMvcRequestBuilders.get(BASE_URL + “/cityName/” + cityName)) .andExpect(MockMvcResultMatchers.status().isOk()) .andReturn(); String json = mvcResult.getResponse().getContentAsString(); Assertions.assertThat(json) .contains(“cityName”, “cold”, “temperature”, “windDirection”, “windForce”) .doesNotContain(“ganmao”, “wendu”, “fx”, “fl”, “fengxiang”, “fengli”);}总结多看英文文章,Tutorial写得都特别好。 ...

February 5, 2019 · 2 min · jiezi

【spring boot2】第8篇:spring boot 中的 servlet 容器

嵌入式 servlet 容器在 spring boot 之前的web开发,我们都是把我们的应用部署到 Tomcat 等servelt容器,这些容器一般都会在我们的应用服务器上安装好环境,但是 spring boot 中并不需要外部应用服务器安装这些servlet容器,spring boot自带了嵌入式的servlet容器。如何修改和定制嵌入式servlet容器在application.yaml文件中配置修改#修改服务端口号server.port=8081#配置统一请求路径server.context‐path=/crud#配置tomcat相关server.tomcat.uri‐encoding=UTF‐8这些配置相关的属性都定义在org.springframework.boot.autoconfigure.web.ServerProperties类中编写一个嵌入式的servlet容器的定制器来修改相关的配置@Componentpublic class CustomizationBean implements WebServerFactoryCustomizer<ConfigurableServletWebServerFactory> { @Override public void customize(ConfigurableServletWebServerFactory server) { server.setPort(9000); }}直接定制具体的servlet容器配置,比如 tomcat 容器@Configurationpublic class ApplicationConfig implements WebMvcConfigurer { @Bean public ConfigurableServletWebServerFactory configurableServletWebServerFactory() { TomcatServletWebServerFactory factory = new TomcatServletWebServerFactory(); factory.setPort(8585); return factory; }}注册 Servlet, Filter, 和 Listenerspring boot默认是以 jar 包的方式启动嵌入式的 servlet 容器来启动web应用,应用中并没有 web.xml 文件,我们注册 Servlet, Filter, 和 Listener 三大组件可以使用以下方式

February 5, 2019 · 1 min · jiezi

Spring Boot 2.1.2 & Spring Cloud Greenwich 升级记录

节前没有新业务代码,正好Greenwich刚发布,于是开始为期四天的框架代码升级。之前的版本是 spring boot 1.5.10 , spring cloud Edgware.SR3依赖升级增加依赖管理插件 apply plugin: ‘io.spring.dependency-management’spring-cloud-starter-eureka → spring-cloud-starter-netflix-eureka-clientspring-cloud-starter-feign → spring-cloud-starter-openfeigngradle版本要求4.4boot : spring-boot-starter-data-jpadelete → deleteByIdfindone → findById这个改动确实大,返回值变成了Optional,合理是合理的,只改的真多。。boot : spring-boot-starter-data-redisJedis → Lettuce还好并没有使用它的autoconfiguration,配置上有一个小坑,Jedis的redis.timeout是表示connection timeout, 而Lettuce是表示command timeout,之前配置成0的,如果set到Lettuce的commandtimeout里面那就要抛异常了。配置:可以在build.gradle中加入,启动时会检查配置是否兼容compile “org.springframework.boot:spring-boot-properties-migrator” 注意:完成迁移后需要删除警告如上图会告知最新的配置格式boot: spring-boot-starter-actuatorendpoint的暴露方式变化,management.endpoints.web.exposure.include = “*” 表示暴露所有endpoints,如果配置了security那么也需要在security的配置中开放访问/actuator路径boot: spring-boot-starter-security自动注入的AuthenticationManager可能会找不到If you want to expose Spring Security’s AuthenticationManager as a bean, override the authenticationManagerBean method on your WebSecurityConfigurerAdapter and annotate it with @Bean.cloud : eureka各个项目在注册中心里面的客户端实例IP显示不正确,需要修改每个项目的bootstarp.yml${spring.cloud.client.ipAddress} → ${spring.cloud.client.ip-address}boot: spring-boot-starter-test:org.mockito.Matchers → org.mockito.ArgumentMatchers 注意build时的warningMock方法时请使用Mocikto.doReturn(…).when(…),不使用when(…).thenReturn(…),否则@spybean的会调用实际方法其他问题版本升级后会有deprecated的类或方法,所以要注意看console中build的warning信息由于spring cloud依赖管理插件强制cuator升级到4.0.1,导致我们使用的elestic-job不能正常工作,只能强行控制版本。dependencyManagement { imports { mavenBom “org.springframework.cloud:spring-cloud-dependencies:${SPRING_CLOUD_VERSION}” } dependencies { dependency ‘org.apache.curator:curator-framework:2.10.0’ dependency ‘org.apache.curator:curator-recipes:2.10.0’ dependency ‘org.apache.curator:curator-client:2.10.0’ }}如果启用出现error,报bean重复,首先确认是不是故意覆盖,如重写spring-boot自带的bean,如是,可以在bootstrap.yml加入spring.main.allow-bean-definition-overriding=trueFeignClient注解增加了contextId属性@FeignClient(value = “foo”, contextId = “fooFeign”)此contextId即表示bean id,所有注入使用时需要@AutowriedFooFeign fooFeign如果不写contextId,当多个class都是@FeignClient(“foo”),即会认为是同一个bean而排除上一条所说的warning ...

February 2, 2019 · 1 min · jiezi

Spring MVC打印@RequestBody、@Response日志

问题描述:使用JSON接收前端参数时, SpringMVC默认输出日志如下:o.s.web.servlet.DispatcherServlet : POST “/example_project/app/login”, parameters={}parameters={}无法打印出JSON消息内容。如果自己实现参数打印, 则需要从reqeust.getInputStream中获取JSON内容, 但是由于流只能读取一次, 所以会导致后续SpringMVC解析参数异常。网上找到一种比较解决方法: 用HttpRequestWrapper重新封装Reqeust, 使打印日志后SpringMVC能正常解析HttpReqeust。这种方法比较麻烦, 这里不去研究这里主要说说Spring提供的较好的解决方案:可以通过自定义RequestBodyAdvisor、ResponseBodyAdvisor来实现日志输出。RequestBodyAdvisor可以获取到解析后的Controller方法参数对象。ResponseBodyAdvisor可以获取到Controller方法返回值对象。然后将他们注册到requestMappingHandlerAdapter:// 继承WebMvcConfigurationSupport, 重写该方法@Override@Beanpublic RequestMappingHandlerAdapter requestMappingHandlerAdapter() { RequestMappingHandlerAdapter adapter = super.requestMappingHandlerAdapter(); adapter.setRequestBodyAdvice(Lists.newArrayList(new CustomerRequestBodyAdvisor())); adapter.setResponseBodyAdvice(Lists.newArrayList(new CustomerResponseBodyAdvisor())); return adapter;}另附CustomerRequestBodyAdvisor、CustomerResponseBodyAdvisor日志输出实现参考:RequestBodyAdvisor实现参考:// CustomerRequestBodyAdvisor.java/*** 打印请求参数日志*/public class CustomerRequestBodyAdvisor extends RequestBodyAdviceAdapter { private static final Logger logger = LoggerFactory.getLogger(CustomerRequestBodyAdvisor.class); @Override public boolean supports(MethodParameter methodParameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) { // 只处理@RequestBody注解了的参数 return methodParameter.getParameterAnnotation(RequestBody.class) != null; } @Override public Object afterBodyRead(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) { Method method = parameter.getMethod(); // 参数对象转JSON字符串 String jsonBody; if (StringHttpMessageConverter.class.isAssignableFrom(converterType)) { jsonBody = body.toString(); } else { jsonBody = JSON.toJSONString(body, SerializerFeature.UseSingleQuotes); } // 自定义日志输出 if (logger.isInfoEnabled()) { logger.info("{}#{}: {}", parameter.getContainingClass().getSimpleName(), method.getName(), jsonBody); // logger.info(“json request<=========method:{}#{}”, parameter.getContainingClass().getSimpleName(), method.getName()); } return super.afterBodyRead(body, inputMessage, parameter, targetType, converterType); }}ResponseBodyAdvisor实现参考:// CustomerResponseBodyAdvisor.java/*** 打印响应值日志*/public class CustomerResponseBodyAdvisor implements ResponseBodyAdvice<Object> { private static final Logger logger = LoggerFactory.getLogger(CustomerResponseBodyAdvisor.class); @Override public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) { return AbstractJackson2HttpMessageConverter.class.isAssignableFrom(converterType) || returnType.getMethod().isAnnotationPresent(ResponseBody.class); } @Override public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) { // 响应值转JSON串输出到日志系统 if (logger.isInfoEnabled()) { logger.info("{}: {}", request.getURI(), JSON.toJSONString(body, SerializerFeature.UseSingleQuotes)); } return body; }} ...

February 1, 2019 · 1 min · jiezi

Spring Boot系列实战文章合集(附源码)

概 述文章开始之前先感叹一番吧。个人从之前的 C语言项目开发转到 Java项目开发来之后开始学着用 Spring Boot做一些后端服务,不得不说 Spring Boot脚手架式的开发真的是十分便利,最近连掉头发现象也好了很多,于是从内心感叹 Java阵营程序员真的比 C阵营程序员工作起来舒服多了,原因就在于Java领域繁荣的生态圈催生了一大批诸如 Spring Boot这样优秀的框架的出现。这段时间也陆陆续续记录了一些有关 Spring Boot应用层开发的点点滴滴,特在此汇聚成文章合集,并 放在了Github上,项目名为 Spring-Boot-In-Action,后续仍然会持续更新。注: 本文首发于 My Personal Blog:CodeSheep·程序羊,欢迎光临 小站数据库/缓存相关Guava Cache本地缓存在 Spring Boot应用中的实践EVCache缓存在 Spring Boot中的实战Spring Boot应用缓存实践之:Ehcache加持Spring Boot集成 MyBatis和 SQL Server实践Elasticsearch搜索引擎在Spring Boot中的实践日志相关Spring Boot日志框架实践应用监控相关利用神器 BTrace 追踪线上 Spring Boot应用运行时信息Spring Boot应用监控实战Spring Boot Admin 2.0开箱体验内部机制相关SpringBoot 中 @SpringBootApplication注解背后的三体结构探秘SpringBoot 应用程序启动过程探秘如何自制一个Spring Boot Starter并推送到远端公服实战经验相关Spring Boot工程集成全局唯一ID生成器 UidGeneratorSpring Boot 工程集成全局唯一ID生成器 VestaSpring Boot优雅编码之:Lombok加持Spring Boot应用 Docker化Spring Boot热部署加持基于Spring Boot实现图片上传/加水印一把梭操作从Spring Boot到 SpringMVC自然语言处理工具包 HanLP在 Spring Boot中的应用Spring Boot应用部署于外置Tomcat容器初探 Kotlin + Spring Boot联合编程【持续更新中……】后 记由于能力有限,若有错误或者不当之处,还请大家批评指正,一起学习交流!My Personal Blog:CodeSheep 程序羊 ...

January 31, 2019 · 1 min · jiezi

【spring boot】第6篇:spring boot 对 spring mvc 的支持

spring mvc 自动配置spring boot 启动时会对 spring mvc 做自动配置,默认的配置功能在 org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration 中进行了说明。有以下功能:包含 ContentNegotiatingViewResolver 和 BeanNameViewResolverContentNegotiatingViewResolverBeanNameViewResolver支持对静态资源自动注册Converter,GenericConverter和FormatterConverterGenericConverterFormatterSupport for HttpMessageConverters (covered later in this document).自动注册MessageCodesResolver支持静态欢迎首页index.html支持自定义 favicon 图标自动使用ConfigurableWebBindingInitializer

January 30, 2019 · 1 min · jiezi

【spring boot】第6篇:spring boot 整合 jdbc 数据源

简述以jdbc的形式访问mysql数据库是比较基础的知识,理解spring boot中如何使用jdbc对我们理解spring boot对mybatis等数据框架是很有意义的。spring boot 整合 jdbc 的步骤引入 starts 启动器<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency></dependencies>配置 application.yaml 文件配置数据库相关信息spring: datasource: username: root password: root url: jdbc:mysql://127.0.0.1:3306/sff_test driver-class-name: com.mysql.jdbc.Driver这些配置信息都封装在org.springframework.boot.autoconfigure.jdbc.DataSourceProperties类中运行测试@RunWith(SpringRunner.class)@SpringBootTestpublic class SpringBootJdbcApplicationTests { @Autowired private DataSource dataSource; @Test public void testDataSource() throws SQLException { System.out.println(“dataSource类型:” + dataSource.getClass()); Connection connection = dataSource.getConnection(); System.out.println(“connection连接:” + connection); connection.close(); }}运行结果:从结果中可以看到 spring boot 的 2.1.2.RELEASE 版本默认使用com.zaxxer.hikari.HikariDataSource 作为数据源。spring boot 数据源自动配置原理spring boot 对 jdbc 的自动配置类都封装在org.springframework.boot.autoconfigure.jdbc包下。DataSourceConfiguration该配置类中定义了spring boot支持的默认数据源种类org.apache.tomcat.jdbc.pool.DataSourcecom.zaxxer.hikari.HikariDataSourceorg.apache.commons.dbcp2.BasicDataSource通过spring.datasource.type属性指定自定义数据源类型,比如druid、 c3p0等

January 30, 2019 · 1 min · jiezi

【spring boot】第4篇:spring boot对模板引擎的支持

spring boot中支持哪些模板引擎freemarkerthymeleafspring boot 整合 freemarker添加场景启动器<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-freemarker</artifactId></dependency>spring boot 如何配置 freemarkerFreeMarkerAutoConfiguration :自动配置,给容器中添加 freemarker 相关组件FreeMarkerProperties :配置 freemarker 的相关属性@ConfigurationProperties(prefix = “spring.freemarker”)public class FreeMarkerProperties extends AbstractTemplateViewResolverProperties { //模板文件存放的路径,存放在该默认路径的文件 freemarker会自动渲染 public static final String DEFAULT_TEMPLATE_LOADER_PATH = “classpath:/templates/”; public static final String DEFAULT_PREFIX = “”; //模板文件默认后缀,可以在属性文件中配置覆盖 public static final String DEFAULT_SUFFIX = “.ftl”;}freemarker 语法介绍spring boot 整合 thymeleafpom.xml中添加依赖 <!–配置启动器–> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring‐boot‐starter‐thymeleaf</artifactId></dependency><!–修改thymeleaf的版本号–>properties> <thymeleaf.version>3.0.9.RELEASE</thymeleaf.version> <!‐‐ 布局功能的支持程序 thymeleaf3 则需要 layout2 以上版本 ‐‐> <thymeleaf‐layout‐dialect.version>2.2.2</thymeleaf‐layout‐ dialect.version></properties>thymeleaf的属性配置类@ConfigurationProperties(prefix = “spring.thymeleaf”)public class ThymeleafProperties { private static final Charset DEFAULT_ENCODING = StandardCharsets.UTF_8; public static final String DEFAULT_PREFIX = “classpath:/templates/”; public static final String DEFAULT_SUFFIX = “.html”;}utf-8 编码文件使用html文件把html页面放在classpath:/templates/中就能自动渲染thymeleaf 语法官方文档 ...

January 18, 2019 · 1 min · jiezi

【spring boot】第4篇:spring boot对静态资源的管理

spring boot 对 web 静态资源的配置管理是通过配置类 WebMvcAutoConfiguration 来实现的。WebMvcAutoConfiguration 的理解顾名思义,WebMvcAutoConfiguration 是web开发的相关配置都放在该类中的。那我们看看静态资源是如何配置的呢?addResourceHandlers 方法中对静态资源路径做了说明public void addResourceHandlers(ResourceHandlerRegistry registry) { if (!this.resourceProperties.isAddMappings()) { logger.debug(“Default resource handling disabled”); return; } Duration cachePeriod = this.resourceProperties.getCache().getPeriod(); CacheControl cacheControl = this.resourceProperties.getCache() .getCachecontrol().toHttpCacheControl(); if (!registry.hasMappingForPattern("/webjars/")) { customizeResourceHandlerRegistration(registry .addResourceHandler("/webjars/") .addResourceLocations(“classpath:/META-INF/resources/webjars/”) .setCachePeriod(getSeconds(cachePeriod)) .setCacheControl(cacheControl)); } //staticPathPattern的值是 /** String staticPathPattern = this.mvcProperties.getStaticPathPattern(); if (!registry.hasMappingForPattern(staticPathPattern)) { customizeResourceHandlerRegistration( registry.addResourceHandler(staticPathPattern) .addResourceLocations(getResourceLocations( this.resourceProperties.getStaticLocations())) .setCachePeriod(getSeconds(cachePeriod)) .setCacheControl(cacheControl)); } }从上面的代码中可以解读出两点关键信息:所有的"/webjars/**都去classpath:/META-INF/resources/webjars/路径下找静态资源什么是webjars :以jar包的形式引入静态资源文件webjars官方网站比如我们现在要使用 jquery 框架,在webjars官网找到他的依赖放到你的项目当中即可<dependency> <groupId>org.webjars.bower</groupId> <artifactId>jquery</artifactId> <version>3.3.1</version></dependency>如果路径是/**时,就去以下classpath:/META-INF/resources/classpath:/resources/classpath:/static/classpath:/public/类路径查找资源文件,idea 中的项目路径如下图所示:3.欢迎页面的映射private Resource getIndexHtml(String location) { return this.resourceLoader.getResource(location + “index.html”); }意思是只要在我们的静态资源文件夹中放有 index.html文件,就能自动访问到,比如:http://localhost:8080 ...

January 18, 2019 · 1 min · jiezi

利用神器BTrace 追踪线上 Spring Boot应用运行时信息

概述生产环境中的服务可能会出现各种问题,但总不能让服务下线来专门排查错误,这时候最好有一些手段来获取程序运行时信息,比如 接口方法参数/返回值、外部调用情况 以及 函数执行时间等信息以便定位问题。传统的日志记录方式的确可以,但有时非常麻烦,甚至可能需要重启服务,因此代价太大,这时可以借助一个牛批的工具:BTrace !BTrace 可用于动态跟踪正在运行的 Java程序,其原理是通过动态地检测目标应用程序的类并注入跟踪代码 ( “字节码跟踪” ),因此可以直接用于监控和追踪线上问题而无需修改业务代码并重启应用程序。BTrace 的使用方式是用户自己编写符合 BTrace使用语法的脚本,并结合btrace命令,来获取应用的一切调用信息,就像下面这样:<btrace>/bin/btrace <PID> <trace_script>其中 <PID>为被监控 Java应用的 进程ID<trace_script> 为 根据需要监控的信息 而自行编写的 Java脚本本文就来实操一波 BTrace工具的使用,实验环境如下:OS:CentOS 7.4 64bitBTrace版本:1.3.11.3被追踪的 Java应用:Spring Boot 2.1.1 应用,这里使用我的文章《Spring Boot应用缓存实践之:Ehcache加持》一文中的 Spring Boot工程BTrace 安装部署下载 二进制文件并解压这里我解压到目录:/home/btrace配置系统环境变量vim /etc/profileBTRACE_HOME=/home/btraceexport BTRACE_HOMEexport PATH=$PATH:$BTRACE_HOME/bin验证 BTrace安装情况btrace –version编译 BTrace源码克隆源码git clone git@github.com:btraceio/btrace.git编译源码./gradlew build构建完成的生成物路径位于:build/libs目录下我们取出构建生成的 jar包供下文使用。利用btrace追踪 Spring Boot应用例析首先我们得构造一个 Spring Boot的模拟业务 用于下文被追踪和分析,这里我就使用文章 《Spring Boot应用缓存实践之:Ehcache加持》中的实验工程。我们在此工程里再添加一个 scripts包,用于放置 btrace 脚本文件:由于 btrace脚本中需要用到 btrace相关的组件和函数库,因此我们还需要在工程的 pom.xml中引入 btrace的依赖,所使用的 jar包就是上文编译生成的 btrace-1.3.11.3.jar <dependency> <groupId>com.sun.btrace</groupId> <artifactId>btrace</artifactId> <version>1.3.11.3</version> </dependency>Talk is cheap ,Show you the code !接下来就用四五个实验来说明一切吧:0x01 监控方法耗时情况btrace 脚本:@BTracepublic class BtraceTest2 { @OnMethod(clazz = “cn.codesheep.springbt_brace.controller.UserController”, method = “getUsersByName”, location = @Location(Kind.RETURN)) public static void getFuncRunTime( @ProbeMethodName String pmn, @Duration long duration) { println( “接口 " + pmn + strcat(“的执行时间(ms)为: “, str(duration / 1000000)) ); //单位是纳秒,要转为毫秒 }}接下来开始运行 btrace脚本来拦截方法的参数,首先我们用 jps命令取到需要被监控的 Spring Boot应用的进程 Id为 27887,然后执行:/home/btrace/bin/btrace 27887 BtraceTest2.java这里我总共对 /getusersbyname接口发出了 12次 POST请求,情况如下:接下来我们再看看利用btrace脚本监控到的 /getuserbyname接口的执行时间:这样一对比很明显,从数据库取数据还是需要 花费十几毫秒的,但从缓存读取数据 几乎没有耗时,这就是为什么要让缓存加持于应用的原因!!!0x02 拦截方法的 参数/返回值btrace 脚本: @OnMethod( clazz = “cn.codesheep.springbt_brace.controller.UserController”, method = “getUsersByName”, location = @Location(Kind.ENTRY) ) public static void getFuncEntry(@ProbeClassName String pcn, @ProbeMethodName String pmn, User user ) { println(“类名: " + pcn); println(“方法名: " + pmn); // 先打印入参实体整体信息 BTraceUtils.print(“入参实体为: “); BTraceUtils.printFields(user); // 再打印入参实体每个属性的信息 Field oneFiled = BTraceUtils.field(“cn.codesheep.springbt_brace.entity.User”, “userName”); println(“userName字段为: " + BTraceUtils.get(oneFiled, user)); oneFiled = BTraceUtils.field(“cn.codesheep.springbt_brace.entity.User”, “userAge”); println(“userAge字段为: " + BTraceUtils.get(oneFiled, user)); }接下来开始运行 btrace脚本来拦截方法的参数,首先我们用 jps命令取到需要被监控的java应用的进程 Id为 27887,然后执行:/home/btrace/bin/btrace -cp springbt_brace/target/classes 27887 BtraceTest4.java此时正常带参数 {“userName”:“codesheep.cn”} 去请求业务接口:POST /getusersbyname,会得到如下输出:很明显请求参数已经被 btrace给拦截到了同理,如果想拦截方法的返回值,可以使用如下 btrace脚本: @OnMethod( clazz = “cn.codesheep.springbt_brace.controller.UserController”, method = “getUsersByName”, location = @Location(Kind.RETURN) //函数返回的时候执行,如果不填,则在函数开始的时候执行 ) public static void getFuncReturn( @Return List<User> users ) { println(“返回值为: “); println(str(users)); }运行 btrace命令后,继续请求想要被监控的业务接口,则可以得到类似如下的输出:0x03 监控代码是否到达了某类的某一行btrace 脚本如下:@BTracepublic class BtraceTest3 { @OnMethod( clazz=“cn.codesheep.springbt_brace.service.UserService”, method=“getUsersByName”, location=@Location(value= Kind.LINE, line=28) // 比如拦截第28行, 28行是从数据库取数据操作 ) public static void lineTest( @ProbeClassName String pcn, @ProbeMethodName String pmn, int line ) { BTraceUtils.println(“ClassName: " + pcn); BTraceUtils.println(“MethodName: " + pmn); BTraceUtils.println(“执行到的line行数: " + line); }}执行 btrace追踪命令/home/btrace/bin/btrace 28927 BtraceTest3.java接着用 POSTMAN工具连续发出了对 /getuserbyname接口的 十几次POST请求,由于只有第一次请求没有缓存时才会从数据库读,因此也才会执行到 UserService类的第 28行 !0x04 监控指定函数中所有外部调用的耗时情况btrace脚本如下:@BTracepublic class BtraceTest5 { @OnMethod (clazz = “cn.codesheep.springbt_brace.service.UserService”,method = “getUsersByName”, location=@Location(value= Kind.CALL, clazz=”/./”, method=”/./”, where = Where.AFTER) ) public static void printMethodRunTime(@Self Object self,@TargetInstance Object instance,@TargetMethodOrField String method, @Duration long duration) { if( duration > 5000000 ){ //如果外部调用耗时大于 5ms 则打印出来 println( “self: " + self ); println( “instance: " + instance ); println( method + “,cost:” + duration/1000000 + " ms” ); } }}执行监控命令:/home/btrace/bin/btrace 28927 BtraceTest5.java然后再对接口 /getuserbyname发出POST请求,观察监控结果如下:我们发现最耗时的外部调用来源于 MyBatis调用。0x05 其他追踪与监控除了上面四种典型的追踪场景之外,其他的 btrace追踪与监控场景还比如 查看谁调用了System.gc(),调用栈如何,则可以使用如下 btrace脚本进行监控@BTracepublic class BtraceTest { @OnMethod(clazz = “java.lang.System”, method = “gc”) public static void onSystemGC() { println(“entered System.gc()”); jstack(); }}很明显,因为btrace 内置了一系列诸如 jstack等十分有用的监控命令。当然最后需要说明的是 btrace内置了很多语法和命令,可以应对很多线上 Java应用监控场景,大家可以去研究一下官方文档后记由于能力有限,若有错误或者不当之处,还请大家批评指正,一起学习交流!My Personal Blog:CodeSheep 程序羊程序羊的 2018年终总(gen)结(feng) ...

January 17, 2019 · 2 min · jiezi

【spring boot】第3篇:spring boot中的日志框架

常用的日志框架logbacklog4jlog4j2commons loggingslf4j在spring boot中选用的是 slf4j + logback 进行日志输出。slf4j的使用我们知道 slf4j 是一个日志门面,具体的日志实现是由具体的日志框架实现的,比如 log4j、logback等日子框架。如下图所示,slf4j集成日志框架需要导入的 jar 包:当我们使用某个日志框架时,只需要在我们的系统类路径下配置对应的日志框架配置文件即可,比如使用logback日志框架,配置其配置文件logback.xml即可。系统如何统一面向 slf4j 进行日志输出我们在系统开发中可能想使用 slf4j + logback 进行日志输出,但是我们系统依赖的 spring、mybatis 框架可能使用的是其他日志框架,那我们如何统一面向 slf4j 进行日志输出呢?通过这张图我们就知道如何实现:系统中其他框架使用的日志框架jar排除,比如图中统一使用的slf4j+logback输出日志,但是系统依赖的框架是commons logging,此时就是排除commons logging的jar包导入适配包,因为系统依赖的框架还需要日志输出,比如图中导入了 jcl-over-slf4j.jar使用 slf4j + logback 进行统一日志记录

January 16, 2019 · 1 min · jiezi

spring-boot下使用LogBack,使用HTTP协议将日志推送到日志服务器

当项目上线发生错误或是异常后,我们总是期望能够在第一时间内收到用户的详细反馈。当然,这也无疑会是一个非常好的提升软件质量的方法。但如果用户不愿意反馈呢?此时,我们便可以借助日志系统,比如:每隔一小时,服务器自动向我们报告一下当前的服务情况。当有错误或是警告或是异常信息时,及时向我们的报告等。在基于上述的需求上,我们结合spring-boot内置的LogBack,来给出将warn,error信息发送到远程服务器的示例。项目地址https://github.com/mengyunzhi/sample/tree/master/spring-boot/log-back开发环境: java1.8 + spring-boot:2.1.2实现步骤引入相关的依赖pom.xml<?xml version=“1.0” encoding=“UTF-8”?><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> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.1.2.RELEASE</version> <relativePath/> <!– lookup parent from repository –> </parent> <groupId>com.mengyunzhi.sample</groupId> <artifactId>log-back</artifactId> <version>0.0.1-SNAPSHOT</version> <name>log-back</name> <description>Demo project for Spring Boot</description> <properties> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build></project>启动项目控制台打印信息如下: . ____ _ __ _ _ /\ / ’ __ _ () __ __ _ \ \ \ ( ( )__ | ‘_ | ‘| | ‘ / | \ \ \ \ \\/ ___)| |_)| | | | | || (_| | ) ) ) ) ' |____| .__|_| |_|_| |_\__, | / / / / =========|_|==============|___/=/_/_/_/ :: Spring Boot :: (v2.1.2.RELEASE)2019-01-16 10:35:04.999 INFO 1571 --- [ main] c.m.sample.logback.LogBackApplication : Starting LogBackApplication on panjiedeMac-Pro.local with PID 1571 (/Users/panjie/github/mengyunzhi/sample/spring-boot/log-back/target/classes started by panjie in /Users/panjie/github/mengyunzhi/sample/spring-boot/log-back)2019-01-16 10:35:05.002 INFO 1571 --- [ main] c.m.sample.logback.LogBackApplication : No active profile set, falling back to default profiles: default2019-01-16 10:35:05.913 INFO 1571 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port(s): 8080 (http)2019-01-16 10:35:05.934 INFO 1571 --- [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat]2019-01-16 10:35:05.935 INFO 1571 --- [ main] org.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/9.0.14]2019-01-16 10:35:05.940 INFO 1571 --- [ main] o.a.catalina.core.AprLifecycleListener : The APR based Apache Tomcat Native library which allows optimal performance in production environments was not found on the java.library.path: [/Users/panjie/Library/Java/Extensions:/Library/Java/Extensions:/Network/Library/Java/Extensions:/System/Library/Java/Extensions:/usr/lib/java:.]2019-01-16 10:35:06.008 INFO 1571 --- [ main] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext2019-01-16 10:35:06.008 INFO 1571 --- [ main] o.s.web.context.ContextLoader : Root WebApplicationContext: initialization completed in 968 ms2019-01-16 10:35:06.183 INFO 1571 --- [ main] o.s.s.concurrent.ThreadPoolTaskExecutor : Initializing ExecutorService 'applicationTaskExecutor'2019-01-16 10:35:06.335 INFO 1571 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path ''2019-01-16 10:35:06.338 INFO 1571 --- [ main] c.m.sample.logback.LogBackApplication : Started LogBackApplication in 1.616 seconds (JVM running for 2.093)配置logback新建resources/logback-spring.xml,初始化以下信息:&lt;?xml version="1.0" encoding="UTF-8"?&gt;&lt;!--开启debug模式--&gt;&lt;configuration debug="true"&gt;&lt;/configuration&gt;启动项目,控制台打印信息如下:10:33:41,053 |-INFO in ch.qos.logback.classic.joran.action.ConfigurationAction - End of configuration.10:33:41,054 |-INFO in org.springframework.boot.logging.logback.SpringBootJoranConfigurator@55a1c291 - Registering current configuration as safe fallback point10:33:41,067 |-WARN in Logger[org.springframework.boot.context.logging.ClasspathLoggingApplicationListener] - No appenders present in context [default] for logger [org.springframework.boot.context.logging.ClasspathLoggingApplicationListener]. . ____ _ __ _ _ /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \( ( )\___ | '_ | '_| | '_ \/ _ | \ \ \ \ \/ )| |)| | | | | || (| | ) ) ) ) ’ || .__|| ||| |_, | / / / / =========||==============|/=//// :: Spring Boot :: (v2.1.2.RELEASE)如何判断配置成功了?我们比较上面两个日志,第一个是没有配置logback-spring.xml,第二个是配置logback-spring.xml了。是的,如果我们发现spring 大LOG打印前,在控制台中打印了ch.qos…输出的日志信息,则说明logback-spring.xml。同时,如果logback-spring.xml起作用的话,我们还发现spring 大LOG下面,一行日志也没有了。是的,由于logback-spring.xml对日志输出进行了控制,而配置信息中,我们又没有写任何的信息,为空。所以spring 大LOG后面当然就不显示任何日志了信息了。查看spring-boot的默认配置我们使用IDEA的打开文件快捷键commod+shift+o,输入base.xml,然后再使用查看文件位置快捷键option+F1来查看文件位置。更来到了spring-boot的默认配置。上述文件,即为spring-boot的默认配置。下面,我们将以上配置引入到我们的logback-spring.xml中,来实现spring-boot的默认日志效果。实现默认效果复制相应的代码至logback-spring.xml中:<?xml version=“1.0” encoding=“UTF-8”?><!–启用debug模式后,将在spring-boot 大LOG上方打印中logBack的配置信息–><configuration debug=“true”> <!–包含配置文件 org/springframework/boot/logging/logback/defaults.xml–> <include resource=“org/springframework/boot/logging/logback/defaults.xml” /> <!–定义变量LOG_FILE,值为${LO…}–> <property name=“LOG_FILE” value="${LOG_FILE:-${LOG_PATH:-${LOG_TEMP:-${java.io.tmpdir:-/tmp}}}/spring.log}”/> <!–包含配置文件,该配置文件中,定义了 控制台日志是按什么规则,什么形式输出的–> <include resource=“org/springframework/boot/logging/logback/console-appender.xml” /> <!–包含配置文件,该配置文件中,定义了 文件日志是按什么规则,什么形式输出的–> <include resource=“org/springframework/boot/logging/logback/file-appender.xml” /> <!–定义日志等级–> <root level=“INFO”> <!–启用第一个appender为CONSOLE, 该名称定义于org/springframework/boot/logging/logback/console-appender.xml中–> <appender-ref ref=“CONSOLE” /> <!–启用第二个appender为FILE, 该名称定义于org/springframework/boot/logging/logback/file-appender.xml中–> <appender-ref ref=“FILE” /> </root></configuration>然后我们再次启动项目,会发现与原spring-boot相比较,在spring 大LOGO前多一些日志相关的配置信息输出,其它的信息是一致的。实现http日志appenderappender通过上面的注释,我们猜测:appender这个东西,能够把日志处理成我们想要的样子。在进行官方文档的学习中,我们发现了很多已经存在的appender。与我们的需求比较相近的是SyslogAppender。liunx有标准的syslog服务,用于接收syslog日志。通过查询相关资料,我们获悉,此syslog服务,一身作用于514端口上。直接使用UDP或TCP协议发送MESSAGE。而我们此时想用更熟悉的http协议。所以暂时放弃。小于1024的均为已知端口,可以通过端口号来查询对应的协议或服务名称。第三方http appender除了按官方的教程来写自己的http appender,还有一些比较好的第三方appender可以使用,比如:LogglyAppender。找到官方文档,并引入:pom.xml <dependency> <groupId>org.logback-extensions</groupId> <artifactId>logback-ext-loggly</artifactId> <version>0.1.5</version> </dependency>logback-spring.xml<?xml version=“1.0” encoding=“UTF-8”?><!–启用debug模式后,将在spring-boot 大LOG上方打印中logBack的配置信息–><configuration debug=“true”> <!–包含配置文件 org/springframework/boot/logging/logback/defaults.xml–> <include resource=“org/springframework/boot/logging/logback/defaults.xml” /> <!–定义变量LOG_FILE,值为${LO…}–> <property name=“LOG_FILE” value="${LOG_FILE:-${LOG_PATH:-${LOG_TEMP:-${java.io.tmpdir:-/tmp}}}/spring.log}”/> <!–包含配置文件,该配置文件中,定义了 控制台日志是按什么规则,什么形式输出的–> <include resource=“org/springframework/boot/logging/logback/console-appender.xml” /> <!–包含配置文件,该配置文件中,定义了 文件日志是按什么规则,什么形式输出的–> <include resource=“org/springframework/boot/logging/logback/file-appender.xml” /> <!–引入第三方appender, 起名为http–> <appender name=“HTTP” class=“ch.qos.logback.ext.loggly.LogglyAppender”> <!–请求的地址–> <endpointUrl>http://localhost:8081/log</endpointUrl> </appender> <!–定义日志等级–> <root level=“INFO”> <!–启用第一个appender为CONSOLE, 该名称定义于org/springframework/boot/logging/logback/console-appender.xml中–> <appender-ref ref=“CONSOLE” /> <!–启用第二个appender为FILE, 该名称定义于org/springframework/boot/logging/logback/file-appender.xml中–> <appender-ref ref=“FILE” /> <!–启用第三个appender为HTTP–> <appender-ref ref=“HTTP” /> </root></configuration>测试测试方法如图:使用浏览器来访问当前项目的’/send’地址send中我们加入logger。再新建一个新项目,用来接收http appender发送过来的日志。建立测试方法LogBackApplication.javapackage com.mengyunzhi.sample.logback;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;import javax.servlet.http.HttpServletRequest;@SpringBootApplication@RestControllerpublic class LogBackApplication { private static final Logger logger = LoggerFactory.getLogger(LogBackApplication.class); public static void main(String[] args) { SpringApplication.run(LogBackApplication.class, args); } @RequestMapping(“send”) public void send() { logger.info(“info”); logger.warn(“warn”); logger.error(“error”); }}接收模块新建一个spring boot项目,然后设置端口为8081。application.propertiesserver.port=8081ServiceApplication.javapackage com.mengyunzhi.sample.logback.service;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;import org.springframework.http.HttpRequest;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;import javax.servlet.http.HttpServletRequest;@SpringBootApplication@RestControllerpublic class ServiceApplication { private final static Logger logger = LoggerFactory.getLogger(ServiceApplication.class); public static void main(String[] args) { SpringApplication.run(ServiceApplication.class, args); } @RequestMapping(“log”) public void log(HttpServletRequest httpServletRequest) { logger.info(httpServletRequest.toString()); }}启动测试使用debug模式来启动两个项目,项目启动后,打开浏览器,输入:http://localhost:8080/send,并在8081端口上的接收位置打断点。查看断点信息:此时我们发现两项信息,也证明数据的确是发送和接收成功了:请求方法: POST请求的协议:http查看发送过来的MESSAGE @RequestMapping(“log”) public void log(HttpServletRequest httpServletRequest) throws IOException { logger.info(httpServletRequest.toString()); BufferedReader bufferedReader = httpServletRequest.getReader(); String str, wholeStr = “”; while((str = bufferedReader.readLine()) != null) { wholeStr += str; } logger.info(wholeStr); }如下:2019-01-16T06:06:49.707Z INFO [http-nio-8080-exec-1] org.apache.catalina.core.ContainerBase.[Tomcat].[localhost].[/]: Initializing Spring DispatcherServlet ‘dispatcherServlet’是的,正如你发现在的一样,一些本来打印在8081项目上面的info信息,被发送过来了。格式化数据传过来的字段串,并不友好,我们接下来将其进行格式化。格式化的方法有两种:1. 发送端格式化。2. 接收端格式化。接收端的格式化的思想是按空格将日志拆分,然后要传入到格式实体的不同的字段。这里不阐述,不实现。我们重点放在第1种,发送端使用第三方库进行格式化。pom.xml <!–log to json–> <dependency> <groupId>ch.qos.logback.contrib</groupId> <artifactId>logback-jackson</artifactId> <version>0.1.5</version> </dependency> <!–log to json–> <dependency> <groupId>ch.qos.logback.contrib</groupId> <artifactId>logback-json-classic</artifactId> <version>0.1.5</version> </dependency>logback.xml <!–引入第三方appender, 起名为http–> <appender name=“HTTP” class=“ch.qos.logback.ext.loggly.LogglyAppender”> <!–请求的地址–> <endpointUrl>http://localhost:8081/log</endpointUrl> <!–定义输出格式JSON–> <layout class=“ch.qos.logback.contrib.json.classic.JsonLayout”> <jsonFormatter class=“ch.qos.logback.contrib.jackson.JacksonJsonFormatter”> <prettyPrint>true</prettyPrint> </jsonFormatter> <timestampFormat>yyyy-MM-dd’ ‘HH:mm:ss.SSS</timestampFormat> </layout> </appender>再次启动项目,访问:http://localhost:8080/send查看断点。{ “timestamp” : “2019-01-16 14:17:54.783”, “level” : “ERROR”, “thread” : “http-nio-8080-exec-1”, “logger” : “com.mengyunzhi.sample.logback.LogBackApplication”, “message” : “error”, “context” : “default”}我们发现,以前的字段串,变成的json字符串,此时我们便可以在接收端建立对应的实体,来轻易的接收了。过滤掉INFO信息当前虽然实现了将日志写入到第三方HTTP日志服务器,但是一些我们不想写入的,比如说INFO信息,也被写入了。下面,我们写一个过滤器,来实现只输出warn有error的信息。新建过滤器Filter.javapackage com.mengyunzhi.sample.logback.service;import ch.qos.logback.classic.Level;import ch.qos.logback.classic.spi.LoggingEvent;import ch.qos.logback.core.filter.AbstractMatcherFilter;import ch.qos.logback.core.spi.FilterReply;import java.util.Arrays;import java.util.List;/** * @author panjie */public class Filter extends AbstractMatcherFilter { @Override public FilterReply decide(Object event) { if (!isStarted()) { return FilterReply.NEUTRAL; } LoggingEvent loggingEvent = (LoggingEvent) event; // 当级别为warn或error,时触发日志。 List<Level> eventsToKeep = Arrays.asList(Level.WARN, Level.ERROR); if (eventsToKeep.contains(loggingEvent.getLevel())) { return FilterReply.NEUTRAL; } else { return FilterReply.DENY; } }}设置过滤器:logback-spring.xml <!–引入第三方appender, 起名为http–> <appender name=“HTTP” class=“ch.qos.logback.ext.loggly.LogglyAppender”> <!–请求的地址–> <endpointUrl>http://localhost:8081/log</endpointUrl> <!–定义过滤器–> <filter class=“com.mengyunzhi.sample.logback.Filter”/> <!–定义输出格式JSON–> <layout class=“ch.qos.logback.contrib.json.classic.JsonLayout”> <jsonFormatter class=“ch.qos.logback.contrib.jackson.JacksonJsonFormatter”> <prettyPrint>true</prettyPrint> </jsonFormatter> <timestampFormat>yyyy-MM-dd’ ‘HH:mm:ss.SSS</timestampFormat> </layout> </appender>测试:只接收了warn与error的数据。统一配置我们在logback-spring.xml定义了<endpointUrl>http://localhost:8081/log</endpointUrl>,如何可以将此项配置搬迁到application.properties中呢?定义变量application.propertiesyunzhi.log.url=http://localhost:8081/log引用变量logback-spring.xml <!–引入application配置信息–> <springProperty scope=“context” name=“logUrl” source=“yunzhi.log.url” defaultValue=“localhost”/> <!–引入第三方appender, 起名为http–> <appender name=“HTTP” class=“ch.qos.logback.ext.loggly.LogglyAppender”> <!–请求的地址–> <endpointUrl>${logUrl}</endpointUrl>此时,我们便可以对logUrl在application.properties中进行统一管理了,当然了,不止如此,我们还可以在启动项目的时候,使用–yunzhi.log.url=xxx来轻松的改变日志接收地址。总结在整体实现的过程中,我们的解决思路仍然是:看官方文档,学官方文档,照抄官方文档。欲速则不达,有学习一门新的知识时,优先学习官方sample code,其次是官方文档。在学习的过程中,还要特别的注意版本号的问题;如何正确的快速的高效率测试的问题。TODO:每次有日志就进行一次请求,对网络资源是种浪费。将APPENDER修改为:每1分钟发送一次、每100条日志发送1次。 ...

January 16, 2019 · 4 min · jiezi

解读:spring-boot logging。记一次Logback在spring-boot中的使用方法

有个任务停留在任务列表中很久了:使用Appenders 完成 loger4j 的日志推送,始终没有成功实现。追其原因,仍然是官方的文档没有认真看。在spring-boot的项目中看到log4j,就想当然的认为Spring-boot使用的是log4j,然后不假思索的去google。最终导致的就是:功能没有实现,而且还浪费了很多不必要的时间,最后:还是老老实实的回来阅读spring-boot的官方文档。本文主要对官方文档Logging部分进行解读。原文地址:https://docs.spring.io/spring-boot/docs/current/reference/html/howto-logging.html.如果你使用的是不是最新版本,那么应该使用https://docs.spring.io/spring-boot/docs/版本号/reference/htmlsingle/#boot-features-logging 如:https://docs.spring.io/spring-boot/docs/1.5.3.RELEASE/reference/htmlsingle/#boot-features-logging76 日志在web开中,我们仅需要依赖于spring-boot-starter-web便自动启用了日志系统Logback。如果仅仅是想改变日志的等级,则可以直接使用logging.level前缀在application.properties中进行设置,比如:logging.level.org.springframework.web=DEBUGlogging.level.org.hibernate=ERROR除了控制日志的等级外,还可以使用logging.file来定义日志输入到的文件位置。如果我们还想配置更多选项,则可以在classpath(resourse)中定义logback.xml或logback-spring.xml。76.1 配置Logback找到logback.xml或logback-spring.xml,复制以下基本内容:<?xml version=“1.0” encoding=“UTF-8”?><configuration> <include resource=“org/springframework/boot/logging/logback/base.xml”/> <logger name=“org.springframework.web” level=“DEBUG”/></configuration>使用idea的ctrl+o来打开spring-boot jar中的base.xml,我们会看到配置信息包含一些特殊的字符,解读如下:${PID}当前的进程ID${LOG_FILE} 如果设置了logging.file,则使用logging.file做为日志输入文件。${LOG_PATH} 同上.指定日志输出路径。${LOG_EXCEPTION_CONVERSION_WORD} ..我们自己定义日志输入的方式和字符串时,当然也可以使用它们了。76.1.1 配置:将日志仅写入文件如果我们想禁用控制台的日志输出(生产环境中,我们的确是要这么做的),然后把日志写入某个日志文件的话。那么需要新建logback-spring.xml,并引入file-appender.xml,比如:<?xml version=“1.0” encoding=“UTF-8”?><configuration> <include resource=“org/springframework/boot/logging/logback/defaults.xml” /> <property name=“LOG_FILE” value="${LOG_FILE:-${LOG_PATH:-${LOG_TEMP:-${java.io.tmpdir:-/tmp}}/}spring.log}"/> <include resource=“org/springframework/boot/logging/logback/file-appender.xml” /> <root level=“INFO”> <appender-ref ref=“FILE” /> </root></configuration>然后:在application.properties定义logging.file来指定日志文件位置.例:logging.file=myapplication.log再看看上面是怎么回事:打开org/springframework/boot/logging/logback/file-appender.xml内容如下:<?xml version=“1.0” encoding=“UTF-8”?><!–File appender logback configuration provided for import, equivalent to the programmaticinitialization performed by Boot–><included> <appender name=“FILE” class=“ch.qos.logback.core.rolling.RollingFileAppender”> <encoder> <pattern>${FILE_LOG_PATTERN}</pattern> </encoder> <file>${LOG_FILE}</file> <rollingPolicy class=“ch.qos.logback.core.rolling.FixedWindowRollingPolicy”> <fileNamePattern>${LOG_FILE}.%i</fileNamePattern> </rollingPolicy> <triggeringPolicy class=“ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy”> <MaxFileSize>10MB</MaxFileSize> </triggeringPolicy> </appender></included>注意:这里面有个<appender name=“FILE”,指定了appender名称为FILE,对应logback-spring.xml的以下语句: <root level=“INFO”> <!–日志等级–> <appender-ref ref=“FILE” /> <!–指定appender为FILE,则前面我们刚刚找到的name值–> </root>总结有了以上内容,我们知道了如下知识点:spring-boot默认使用的是Logback而非log4j。我们可以单独建立logback-spring.xml来细化Logback的配置。在Logback中,是可以指定使用不同的appender来定义日志的输出的。是否可以自定义appender来达到将日志输出到我们的日志服务器,从而达到系统监控的目的呢? ...

January 15, 2019 · 1 min · jiezi

软件设计与编程实践总结

问题描述大三了,一年一度的软件设计与编程实践到来了。继今年的软件工程实验之后第二个大实验;要求类似,多用户登录的复杂系统,软件工程实验要求五个下午,本实验要求八个上午。感谢Spring Data JPA,此框架真的是实验利器,大大提高了开发效率。感谢团队,感谢潘老师。要不我可能也要和我的同学一样一起学Tomcat,写Servlet,生连JDBC,手写SELECT,最后把实验写黄。需求描述三个角色:货主、司机、管理员。管理员负责维护基础信息,就是基础模块的增删改查,就是实体有点多。主要的就是业务流程:货主在平台发起订单,司机能综合查询相关订单。司机进行抢单,一个单可以被多个司机抢,然后货主选择我这批货由哪个司机进行承运,并进行付款。司机能更改运输状态,当货物到站时,货主确认后,平台将经过抽成的钱转给司机。问题Spring Data JPA@RepositoryRestResource(path = “Tax”)public interface TaxRepository extends JpaRepository<Tax, Long> { Tax findByMinPriceLessThanAndMaxPriceGreaterThanEqual(BigDecimal priceMin, BigDecimal priceMax);}直接在仓库接口上加注解,然后就在当前的path上生成了增删改查、分页、排序等接口。这个框架用是很好用,就是有一个问题,查询数据时后台不序列化id。id: 1name: zhangsanage: 18假如数据表中由这样一个实体,如果调用getAll接口的话,返回的却是这样的数据。[{ name: zhangsan, age: 18}]没有id在首页显示是没问题的,但是如果编辑的时候怎么办呢?查找官方文档,实现RepositoryRestConfigurer接口,在configureRepositoryRestConfiguration中配置为哪个实体暴露id。@Configurationpublic class CustomRestConfiguration implements RepositoryRestConfigurer { @Override public void configureRepositoryRestConfiguration(RepositoryRestConfiguration config) { config.exposeIdsFor(GoodCategory.class) .exposeIdsFor(OrderDetail.class) .exposeIdsFor(Orders.class) .exposeIdsFor(Payment.class) .exposeIdsFor(Price.class) .exposeIdsFor(Tax.class) .exposeIdsFor(User.class) .exposeIdsFor(Vehicle.class); }}地图我们设计的是货主发起订单时的出发地与目的地都是在百度地图上选的。所以,需要解决三个问题:定位当前位置,点开地图时默认是这个位置。用户选择了位置,我怎么知道选的是哪个地方?计算出发地与目的地之间的距离,用于计算总价。设计和之前写定时任务不知道本初子午线一样,解决地图问题的时候,又暴露了我地理没学好的缺陷。一直想怎么存储位置呢?后来学习了一下百度地图的开发文档发现,经纬度是最好的解决方法。经纬度唯一地标识一个位置,我们可以根据经纬度再去百度查这个经纬度是一个什么地点。最后就是这么设计的,先选出经纬度,然后前台去根据当前选中的经纬度去百度要数据,这个经纬度对应的是哪个市哪个区那条街?同时根据起点与终点调百度的接口查询距离是多少。private String startPlace; // 起点private String endPlace; // 终点private Float startLongitude; // 起点经度private Float startLatitude; // 起点纬度private Float endLongitude; // 终点经度private Float endLatitude; // 终点纬度private Float distance; // 运输距离定位百度地图开放平台上的DEMO特别详细,有四种定位方式确定当前用户在哪,我们采用的是根据ip的定位方式,虽然距离不太精确,但对于实验来说足够了。算距离发现百度地图的api和我想象中的有差距,点开获取两点间的距离,没想到竟然是勾股定理。综合查询司机查订单的时候,用到了团队的核心库。想把这个YunzhiService的实例放到我的应用上下文中。@Configurationpublic class BeanConfig { @Bean public YunzhiService yunzhiService() { return new YunzhiServiceImpl(); }}总结弃用虽然不太愿意承认,但是当我帮同学装环境的时候,npm提示grunt不推荐使用。新技术将至。README文档对于一个项目的参考价值来说十分重要,我之前在Github上看到过不少开源的项目,我看过以后觉得毫无参考价值,一个文档都不写,这个项目开源对他人来说有何意义吗?物流运输平台 - Github深觉文档的价值,以后开源的每个项目,不管代码如何,在至少保证文档齐全。至少保证我这个项目开源,对他人有意义! ...

January 13, 2019 · 1 min · jiezi

@PropertySource 分环境读取配置

工作的时候,一般来说代码都是分环境的,比如dev,test,prd什么的,在用到@PropertySource 注解的时候,发现好像不能根据环境读取自定义的.properties文件,比如我有个systemProperties-dev.properties文件,一开始只是systemProperties-${spring.profiles.active}.properties这样的方式勉强能用,但是后来当我的环境变量变成多环境的时候,也就是spring.profiles.active = dev,test这样的是,这个方法就不奏效了,(多傻啊,其实早就想到了,他会直接在“-”后面拼了一个“dev,test”)然后在网上看了看资料,参考了以下的一篇文章,然后参照了下源码,用了一个比较简单,但是很难看的方法实现了:P(感觉也是暂时解决问题。)。参照文章:Springboot中PropertySource注解多环境支持以及原理主要思想,重写PropertySourceFactory,在PropertySourceFactory中,重新取得resource,SystemProperties.java@Component@PropertySource(name=“systemConfig”, value = {“classpath:/systemConfig-${spring.profiles.active}.properties”}, factory = SystemPropertySourceFactory.class)public class SystemProperties { // 自己的内容…. }这里指定了 factory = SystemPropertySourceFactory.class,接下来SystemPropertySourceFactory.java@Configurationpublic class SystemPropertySourceFactory implements PropertySourceFactory { @Override public PropertySource<?> createPropertySource(String name, EncodedResource encodedResource) throws IOException { FileSystemResourceLoader resourceLoader = new FileSystemResourceLoader(); //取得当前活动的环境名称(因为直接获取spring.profiles.active 失败,所以才把环境名称拼在文件名后面来拿) //其实感觉应该有可以直接取的方法比如从环境里取 String[] actives = encodedResource.getResource().getFilename().split("\.")[0].replace(name + “-”, “”).split(","); //如果只有一个,就直接返回 if (actives.length <= 1) { return (name != null ? new ResourcePropertySource(name, encodedResource) : new ResourcePropertySource(encodedResource)); } //如果是多个 List<URL> resourceUrls = new ArrayList<>(); //遍历后把所有环境的url全部抓取到list中 Arrays.stream(actives).forEach(active -> { //在resource目录下读取配置文件 URL url = this.getClass().getResource("/" + name.concat("-" + active).concat(".properties")); if (url != null) { resourceUrls.add(url); } }); if (resourceUrls != null && resourceUrls.size() > 0) { List<InputStream> inputStreamList = new ArrayList<>(); //取得所有资源的inputStream for (URL url : resourceUrls) { Resource resource0 = resourceLoader.getResource(url.getPath()); InputStream in = resource0.getInputStream(); inputStreamList.add(in); } //串行流,将多个文件流合并车一个流 SequenceInputStream inputStream = new SequenceInputStream(Collections.enumeration(inputStreamList)); //转成resource InputStreamResource resource = new InputStreamResource(inputStream); return (name != null ? new ResourcePropertySource(name, new EncodedResource(resource)) : new ResourcePropertySource(new EncodedResource(resource))); } else { return (name != null ? new ResourcePropertySource(name, encodedResource) : new ResourcePropertySource(encodedResource)); } }}这样实现后,就能将多个环境的Property文件加载进去了。然后是关于spring.profiles.active 为什么要这么取,我试过@value,和用Environment 对象,都取不到,可能跟bean创建的先后顺序有关。没有继续调查,希望知道原因的朋友能帮忙解答~ ...

January 9, 2019 · 1 min · jiezi

SpringBoot究竟是如何跑起来的?

SpringBoot究竟是如何跑起来的?摘要: 神奇的SpringBoot。原文:SpringBoot 究竟是如何跑起来的?作者:老钱Fundebug经授权转载,版权归原作者所有。不得不说 SpringBoot 太复杂了,我本来只想研究一下 SpringBoot 最简单的 HelloWorld 程序是如何从 main 方法一步一步跑起来的,但是这却是一个相当深的坑。你可以试着沿着调用栈代码一层一层的深入进去,如果你不打断点,你根本不知道接下来程序会往哪里流动。这个不同于我研究过去的 Go 语言、Python 语言框架,它们通常都非常直接了当,设计上清晰易懂,代码写起来简单,里面的实现同样也很简单。但是 SpringBoot 不是,它的外表轻巧简单,但是它的里面就像一只巨大的怪兽,这只怪兽有千百只脚把自己缠绕在一起,把爱研究源码的读者绕的晕头转向。但是这 Java 编程的世界 SpringBoot 就是老大哥,你却不得不服。即使你的心中有千万头草泥马在奔跑,但是它就是天下第一。如果你是一个学院派的程序员,看到这种现象你会怀疑人生,你不得不接受一个规则 —— 受市场最欢迎的未必就是设计的最好的,里面夹杂着太多其它的非理性因素。经过了一番痛苦的折磨,我还是把 SpringBoot 的运行原理摸清楚了,这里分享给大家。Hello World首先我们看看 SpringBoot 简单的 Hello World 代码,就两个文件 HelloControll.java 和 Application.java,运行 Application.java 就可以跑起来一个简单的 RESTFul Web 服务器了。// HelloController.javapackage hello;import org.springframework.web.bind.annotation.RestController;import org.springframework.web.bind.annotation.RequestMapping;@RestControllerpublic class HelloController { @RequestMapping("/") public String index() { return “Greetings from Spring Boot!”; }}// Application.javapackage hello;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;@SpringBootApplicationpublic class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); }}当我打开浏览器看到服务器正常地将输出呈现在浏览器的时候,我不禁大呼 —— SpringBoot 真他妈太简单了。但是问题来了,在 Application 的 main 方法里我压根没有任何地方引用 HelloController 类,那么它的代码又是如何被服务器调用起来的呢?这就需要深入到 SpringApplication.run() 方法中看个究竟了。不过即使不看代码,我们也很容易有这样的猜想,SpringBoot 肯定是在某个地方扫描了当前的 package,将带有 RestController 注解的类作为 MVC 层的 Controller 自动注册进了 Tomcat Server。还有一个让人不爽的地方是 SpringBoot 启动太慢了,一个简单的 Hello World 启动居然还需要长达 5 秒,要是再复杂一些的项目这样龟漫的启动速度那真是不好想象了。再抱怨一下,这个简单的 HelloWorld 虽然 pom 里只配置了一个 maven 依赖,但是传递下去,它一共依赖了 36 个 jar 包,其中以 spring 开头的 jar 包有 15 个。说这是依赖地狱真一点不为过。批评到这里就差不多了,下面就要正是进入主题了,看看 SpringBoot 的 main 方法到底是如何跑起来的。SpringBoot 的堆栈了解 SpringBoot 运行的最简单的方法就是看它的调用堆栈,下面这个启动调用堆栈还不是太深,我没什么可抱怨的。public class TomcatServer { @Override public void start() throws WebServerException { … }}接下来再看看运行时堆栈,看看一个 HTTP 请求的调用栈有多深。不看不知道一看吓了一大跳!我通过将 IDE 窗口全屏化,并将其它的控制台窗口源码窗口统统最小化,总算勉强一个屏幕装下了整个调用堆栈。不过转念一想,这也不怪 SpringBoot,绝大多数都是 Tomcat 的调用堆栈,跟 SpringBoot 相关的只有不到 10 层。探索 ClassLoaderSpringBoot 还有一个特色的地方在于打包时它使用了 FatJar 技术将所有的依赖 jar 包一起放进了最终的 jar 包中的 BOOT-INF/lib 目录中,当前项目的 class 被统一放到了 BOOT-INF/classes 目录中。<build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins></build>这不同于我们平时经常使用的 maven shade 插件,将所有的依赖 jar 包中的 class 文件解包出来后再密密麻麻的塞进统一的 jar 包中。下面我们将 springboot 打包的 jar 包解压出来看看它的目录结构。├── BOOT-INF│ ├── classes│ │ └── hello│ └── lib│ ├── classmate-1.3.4.jar│ ├── hibernate-validator-6.0.12.Final.jar│ ├── jackson-annotations-2.9.0.jar│ ├── jackson-core-2.9.6.jar│ ├── jackson-databind-2.9.6.jar│ ├── jackson-datatype-jdk8-2.9.6.jar│ ├── jackson-datatype-jsr310-2.9.6.jar│ ├── jackson-module-parameter-names-2.9.6.jar│ ├── javax.annotation-api-1.3.2.jar│ ├── jboss-logging-3.3.2.Final.jar│ ├── jul-to-slf4j-1.7.25.jar│ ├── log4j-api-2.10.0.jar│ ├── log4j-to-slf4j-2.10.0.jar│ ├── logback-classic-1.2.3.jar│ ├── logback-core-1.2.3.jar│ ├── slf4j-api-1.7.25.jar│ ├── snakeyaml-1.19.jar│ ├── spring-aop-5.0.9.RELEASE.jar│ ├── spring-beans-5.0.9.RELEASE.jar│ ├── spring-boot-2.0.5.RELEASE.jar│ ├── spring-boot-autoconfigure-2.0.5.RELEASE.jar│ ├── spring-boot-starter-2.0.5.RELEASE.jar│ ├── spring-boot-starter-json-2.0.5.RELEASE.jar│ ├── spring-boot-starter-logging-2.0.5.RELEASE.jar│ ├── spring-boot-starter-tomcat-2.0.5.RELEASE.jar│ ├── spring-boot-starter-web-2.0.5.RELEASE.jar│ ├── spring-context-5.0.9.RELEASE.jar│ ├── spring-core-5.0.9.RELEASE.jar│ ├── spring-expression-5.0.9.RELEASE.jar│ ├── spring-jcl-5.0.9.RELEASE.jar│ ├── spring-web-5.0.9.RELEASE.jar│ ├── spring-webmvc-5.0.9.RELEASE.jar│ ├── tomcat-embed-core-8.5.34.jar│ ├── tomcat-embed-el-8.5.34.jar│ ├── tomcat-embed-websocket-8.5.34.jar│ └── validation-api-2.0.1.Final.jar├── META-INF│ ├── MANIFEST.MF│ └── maven│ └── org.springframework└── org └── springframework └── boot这种打包方式的优势在于最终的 jar 包结构很清晰,所有的依赖一目了然。如果使用 maven shade 会将所有的 class 文件混乱堆积在一起,是无法看清其中的依赖。而最终生成的 jar 包在体积上两也者几乎是相等的。在运行机制上,使用 FatJar 技术运行程序是需要对 jar 包进行改造的,它还需要自定义自己的 ClassLoader 来加载 jar 包里面 lib 目录中嵌套的 jar 包中的类。我们可以对比一下两者的 MANIFEST 文件就可以看出明显差异// Generated by Maven Shade PluginManifest-Version: 1.0Implementation-Title: gs-spring-bootImplementation-Version: 0.1.0Built-By: qianwpImplementation-Vendor-Id: org.springframeworkCreated-By: Apache Maven 3.5.4Build-Jdk: 1.8.0_191Implementation-URL: https://projects.spring.io/spring-boot/#/spring-bo ot-starter-parent/gs-spring-bootMain-Class: hello.Application// Generated by SpringBootLoader PluginManifest-Version: 1.0Implementation-Title: gs-spring-bootImplementation-Version: 0.1.0Built-By: qianwpImplementation-Vendor-Id: org.springframeworkSpring-Boot-Version: 2.0.5.RELEASEMain-Class: org.springframework.boot.loader.JarLauncherStart-Class: hello.ApplicationSpring-Boot-Classes: BOOT-INF/classes/Spring-Boot-Lib: BOOT-INF/lib/Created-By: Apache Maven 3.5.4Build-Jdk: 1.8.0_191Implementation-URL: https://projects.spring.io/spring-boot/#/spring-bo ot-starter-parent/gs-spring-bootSpringBoot 将 jar 包中的 Main-Class 进行了替换,换成了 JarLauncher。还增加了一个 Start-Class 参数,这个参数对应的类才是真正的业务 main 方法入口。我们再看看这个 JarLaucher 具体干了什么public class JarLauncher{ … static void main(String[] args) { new JarLauncher().launch(args); } protected void launch(String[] args) { try { JarFile.registerUrlProtocolHandler(); ClassLoader cl = createClassLoader(getClassPathArchives()); launch(args, getMainClass(), cl); } catch (Exception ex) { ex.printStackTrace(); System.exit(1); } } protected void launch(String[] args, String mcls, ClassLoader cl) { Runnable runner = createMainMethodRunner(mcls, args, cl); Thread runnerThread = new Thread(runner); runnerThread.setContextClassLoader(classLoader); runnerThread.setName(Thread.currentThread().getName()); runnerThread.start(); }}class MainMethodRunner { @Override public void run() { try { Thread th = Thread.currentThread(); ClassLoader cl = th.getContextClassLoader(); Class<?> mc = cl.loadClass(this.mainClassName); Method mm = mc.getDeclaredMethod(“main”, String[].class); if (mm == null) { throw new IllegalStateException(this.mainClassName + " does not have a main method"); } mm.invoke(null, new Object[] { this.args }); } catch (Exception ex) { ex.printStackTrace(); System.exit(1); } }}从源码中可以看出 JarLaucher 创建了一个特殊的 ClassLoader,然后由这个 ClassLoader 来另启一个单独的线程来加载 MainClass 并运行。又一个问题来了,当 JVM 遇到一个不认识的类,BOOT-INF/lib 目录里又有那么多 jar 包,它是如何知道去哪个 jar 包里加载呢?我们继续看这个特别的 ClassLoader 的源码class LaunchedURLClassLoader extends URLClassLoader { … private Class<?> doLoadClass(String name) { if (this.rootClassLoader != null) { return this.rootClassLoader.loadClass(name); } findPackage(name); Class<?> cls = findClass(name); return cls; }}这里的 rootClassLoader 就是双亲委派模型里的 ExtensionClassLoader ,JVM 内置的类会优先使用它来加载。如果不是内置的就去查找这个类对应的 Package。private void findPackage(final String name) { int lastDot = name.lastIndexOf(’.’); if (lastDot != -1) { String packageName = name.substring(0, lastDot); if (getPackage(packageName) == null) { try { definePackage(name, packageName); } catch (Exception ex) { // Swallow and continue } } }}private final HashMap<String, Package> packages = new HashMap<>();protected Package getPackage(String name) { Package pkg; synchronized (packages) { pkg = packages.get(name); } if (pkg == null) { if (parent != null) { pkg = parent.getPackage(name); } else { pkg = Package.getSystemPackage(name); } if (pkg != null) { synchronized (packages) { Package pkg2 = packages.get(name); if (pkg2 == null) { packages.put(name, pkg); } else { pkg = pkg2; } } } } return pkg;}private void definePackage(String name, String packageName) { String path = name.replace(’.’, ‘/’).concat(".class"); for (URL url : getURLs()) { try { if (url.getContent() instanceof JarFile) { JarFile jf= (JarFile) url.getContent(); if (jf.getJarEntryData(path) != null && jf.getManifest() != null) { definePackage(packageName, jf.getManifest(), url); return null; } } } catch (IOException ex) { // Ignore } } return null;}ClassLoader 会在本地缓存包名和 jar包路径的映射关系,如果缓存中找不到对应的包名,就必须去 jar 包中挨个遍历搜寻,这个就比较缓慢了。不过同一个包名只会搜寻一次,下一次就可以直接从缓存中得到对应的内嵌 jar 包路径。深层 jar 包的内嵌 class 的 URL 路径长下面这样,使用感叹号 ! 分割jar:file:/workspace/springboot-demo/target/application.jar!/BOOT-INF/lib/snakeyaml-1.19.jar!/org/yaml/snakeyaml/Yaml.class不过这个定制的 ClassLoader 只会用于打包运行时,在 IDE 开发环境中 main 方法还是直接使用系统类加载器加载运行的。不得不说,SpringbootLoader 的设计还是很有意思的,它本身很轻量级,代码逻辑很独立没有其它依赖,它也是 SpringBoot 值得欣赏的点之一。HelloController 自动注册还剩下最后一个问题,那就是 HelloController 没有被代码引用,它是如何注册到 Tomcat 服务中去的?它靠的是注解传递机制。SpringBoot 深度依赖注解来完成配置的自动装配工作,它自己发明了几十个注解,确实严重增加了开发者的心智负担,你需要仔细阅读文档才能知道它是用来干嘛的。Java 注解的形式和功能是分离的,它不同于 Python 的装饰器是功能性的,Java 的注解就好比代码注释,本身只有属性,没有逻辑,注解相应的功能由散落在其它地方的代码来完成,需要分析被注解的类结构才可以得到相应注解的属性。那注解是又是如何传递的呢?@SpringBootApplicationpublic class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); }}@ComponentScanpublic @interface SpringBootApplication {…}public @interface ComponentScan { String[] basePackages() default {};}首先 main 方法可以看到的注解是 SpringBootApplication,这个注解又是由ComponentScan 注解来定义的,ComponentScan 注解会定义一个被扫描的包名称,如果没有显示定义那就是当前的包路径。SpringBoot 在遇到 ComponentScan 注解时会扫描对应包路径下面的所有 Class,根据这些 Class 上标注的其它注解继续进行后续处理。当它扫到 HelloController 类时发现它标注了 RestController 注解。@RestControllerpublic class HelloController {…}@Controllerpublic @interface RestController {}而 RestController 注解又标注了 Controller 注解。SpringBoot 对 Controller 注解进行了特殊处理,它会将 Controller 注解的类当成 URL 处理器注册到 Servlet 的请求处理器中,在创建 Tomcat Server 时,会将请求处理器传递进去。HelloController 就是如此被自动装配进 Tomcat 的。扫描处理注解是一个非常繁琐肮脏的活计,特别是这种用注解来注解注解(绕口)的高级使用方法,这种方法要少用慎用。SpringBoot 中有大量的注解相关代码,企图理解这些代码是乏味无趣的没有必要的,它只会把你的本来清醒的脑袋搞晕。SpringBoot 对于习惯使用的同学来说它是非常方便的,但是其内部实现代码不要轻易模仿,那绝对算不上模范 Java 代码。最后老钱表示自己真的很讨厌 SpringBoot 这只怪兽,但是很无奈,这个世界人人都在使用它。这就好比老人们常常告诫年轻人的那句话:如果你改变不了世界,那就先适应这个世界吧! ...

January 9, 2019 · 4 min · jiezi

Guava Cache本地缓存在 Spring Boot应用中的实践

概述在如今高并发的互联网应用中,缓存的地位举足轻重,对提升程序性能帮助不小。而 3.x开始的 Spring也引入了对 Cache的支持,那对于如今发展得如火如荼的 Spring Boot来说自然也是支持缓存特性的。当然 Spring Boot默认使用的是 SimpleCacheConfiguration,即使用 ConcurrentMapCacheManager 来实现的缓存。但本文将讲述如何将 Guava Cache缓存应用到 Spring Boot应用中。Guava Cache是一个全内存的本地缓存实现,而且提供了线程安全机制,所以特别适合于代码中已经预料到某些值会被多次调用的场景下文就上手来摸一摸它,结合对数据库的操作,我们让 Guava Cache作为本地缓存来看一下效果!准备工作准备好数据库和数据表并插入相应实验数据(MySQL)比如我这里准备了一张用户表,包含几条记录:我们将通过模拟数据库的存取操作来看看 Guava Cache缓存加入后的效果。搭建工程:Springboot + MyBatis + MySQL + Guava Cachepom.xml 中添加如下依赖: <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <!–for mybatis–> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>1.3.2</version> </dependency> <!–for Mysql–> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <!– Spring boot Cache–> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-cache</artifactId> </dependency> <!–for guava cache–> <dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>27.0.1-jre</version> </dependency> </dependencies>建立 Guava Cache配置类引入 Guava Cache的配置文件 GuavaCacheConfig@Configuration@EnableCachingpublic class GuavaCacheConfig { @Bean public CacheManager cacheManager() { GuavaCacheManager cacheManager = new GuavaCacheManager(); cacheManager.setCacheBuilder( CacheBuilder.newBuilder(). expireAfterWrite(10, TimeUnit.SECONDS). maximumSize(1000)); return cacheManager; }}Guava Cache配置十分简洁,比如上面的代码配置缓存存活时间为 10 秒,缓存最大数目为 1000 个配置 application.propertiesserver.port=82# Mysql 数据源配置spring.datasource.url=jdbc:mysql://121.116.23.145:3306/demo?useUnicode=true&characterEncoding=utf-8&useSSL=falsespring.datasource.username=rootspring.datasource.password=xxxxxxspring.datasource.driver-class-name=com.mysql.jdbc.Driver# mybatis配置mybatis.type-aliases-package=cn.codesheep.springbt_guava_cache.entitymybatis.mapper-locations=classpath:mapper/*.xmlmybatis.configuration.map-underscore-to-camel-case=true编写数据库操作和 Guava Cache缓存的业务代码编写 entitypublic class User { private Long userId; private String userName; private Integer userAge; public Long getUserId() { return userId; } public void setUserId(Long userId) { this.userId = userId; } public String getUserName() { return userName; } public void setUserName(String userName) { this.userName = userName; } public Integer getUserAge() { return userAge; } public void setUserAge(Integer userAge) { this.userAge = userAge; }}编写 mapperpublic interface UserMapper { List<User> getUsers(); int addUser(User user); List<User> getUsersByName( String userName );}编写 service@Servicepublic class UserService { @Autowired private UserMapper userMapper; public List<User> getUsers() { return userMapper.getUsers(); } public int addUser( User user ) { return userMapper.addUser(user); } @Cacheable(value = “user”, key = “#userName”) public List<User> getUsersByName( String userName ) { List<User> users = userMapper.getUsersByName( userName ); System.out.println( “从数据库读取,而非读取缓存!” ); return users; }}看得很明白了,我们在 getUsersByName接口上添加了注解:@Cacheable。这是 缓存的使用注解之一,除此之外常用的还有 @CachePut和 @CacheEvit,分别简单介绍一下:@Cacheable:配置在 getUsersByName方法上表示其返回值将被加入缓存。同时在查询时,会先从缓存中获取,若不存在才再发起对数据库的访问@CachePut:配置于方法上时,能够根据参数定义条件来进行缓存,其与 @Cacheable不同的是使用 @CachePut标注的方法在执行前不会去检查缓存中是否存在之前执行过的结果,而是每次都会执行该方法,并将执行结果以键值对的形式存入指定的缓存中,所以主要用于数据新增和修改操作上@CacheEvict:配置于方法上时,表示从缓存中移除相应数据。编写 controller@RestControllerpublic class UserController { @Autowired private UserService userService; @Autowired CacheManager cacheManager; @RequestMapping( value = “/getusersbyname”, method = RequestMethod.POST) public List<User> geUsersByName( @RequestBody User user ) { System.out.println( “——————————————-” ); System.out.println(“call /getusersbyname”); System.out.println(cacheManager.toString()); List<User> users = userService.getUsersByName( user.getUserName() ); return users; }}改造 Spring Boot应用主类主要是在启动类上通过 @EnableCaching注解来显式地开启缓存功能@SpringBootApplication@MapperScan(“cn.codesheep.springbt_guava_cache”)@EnableCachingpublic class SpringbtGuavaCacheApplication { public static void main(String[] args) { SpringApplication.run(SpringbtGuavaCacheApplication.class, args); }}最终完工的整个工程的结构如下:实际实验通过多次向接口 localhost:82/getusersbyname POST数据来观察效果:可以看到缓存的启用和失效时的效果如下所示(上文 Guava Cache的配置文件中设置了缓存 user的实效时间为 10s):怎么样,缓存的作用还是很明显的吧!后 记由于能力有限,若有错误或者不当之处,还请大家批评指正,一起学习交流!My Personal Blog:CodeSheep 程序羊程序羊的 2018年终总(gen)结(feng) ...

January 8, 2019 · 2 min · jiezi

spring boot 拦截器(interceptor)与切面(aop)的使用场景

在使用spring-boot的过程中,我们在处理一些before、after操作时,往往有两种技术选择:interceptor 拦截器和aop 向对切面编程。那么:什么时候该使用interceptor 拦截器,什么时候又该使用aop 向对切面编程呢?比如:我们在进行用户是否登录验证时。可以使用interceptor 拦截器结合注解来实现,也可以使用aop 向对切面编程结合注解来实现。个人经验如下:如果注解仅应用到controller 控制器或是controller 控制器对应的function 方法上,那么应该使用interceptor 拦截器。如果注解的应用范围不仅仅是controller 控制器或是controller 控制器对应的function 方法上,比如注解应用到服务 service中,那么应该使用AOP 向对切面编程。

January 7, 2019 · 1 min · jiezi

spring boot 开发soap webservice

介绍spring boot web模块提供了RestController实现restful,第一次看到这个名字的时候以为还有SoapController,很可惜没有,对于soap webservice提供了另外一个模块spring-boot-starter-web-services支持。本文介绍如何在spring boot中开发soap webservice接口,以及接口如何同时支持soap和restful两种协议。soap webserviceWeb service是一个平台独立的,低耦合的,自包含的、基于可编程的web的应用程序,既可以是soap webservice也可以是rest webservice,在rest还没出来之前,我们说webservice一般是指基于soap协议进行通信的web应用程序。在开始之前,我觉得有必要了解下soap webservice,具体的概念网上可以找到很多资料,但网上资料概念性较强,而且soap协议使用的是xml进行通信,相信xml里面一个namespace就能吓跑一大堆人,所以这里不讨论具体的soap协议细节,我想通过一个例子来说明什么是soap webservice,通过该例子,你能了解soap webservice其运作原理,当然如果你觉得你对这个已经很了解了,大可跳过本章节,本章节跟后面的内容没有任何关系。假设我们开发了一个web接口,想给别人用,我们要怎么办部署接口到服务器编写接口文档,写清楚接口是通过什么方法调的,输入参数是什么,输出参数是什么,错误时返回什么。那问题来了,我们能不能只把接口部署到服务器上,然后接口不单能提供具体的服务,而且还能自动生成一份标准的接口文档,把接口信息都记录在该文档里,如果能做到,是不是能做到"接口即文档"的目的。那么一个接口的信息包括哪些呢?接口地址接口调用方法接口输入参数接口输出参数接口出错返回信息….soap webservice里wsdl文件就是接口描述信息。核心的信息就是以上几个。第二个问题,由于Web service是一个平台独立,也就是说,使用接口的人不知道这个service是用什么技术开发的,可能是php可能是java等,但接口的参数和返回的数据都是一样的,要达到这种目的,就需要两个东西,一个是跟平台无关的数据格式,soap使用的是xml,一个是通信协议,也就是soap协议。下面就介绍如何不使用任何框架,仅通过servlet实现一个webservice。该webservice功能很简单,就是通过一个人的姓名查询这个人的详细信息。ps:servlet是java web的基础,理解servlet对理解整个java web非常重要,没写过servlet就开始用各种框架写接口就是在胡闹。1. wsdl文件准备以下wsdl文件,不要管这个文件是怎么来的,是怎么生成的,我们这次只讲原理,不谈细节,总之,你根据需求写出了这个wsdl文件。<?xml version=“1.0” encoding=“UTF-8” standalone=“no”?><wsdl:definitions xmlns:wsdl=“http://schemas.xmlsoap.org/wsdl/" xmlns:sch=“http://www.definesys.com/xml/employee" xmlns:soap=“http://schemas.xmlsoap.org/wsdl/soap/" xmlns:tns=“http://www.definesys.com/xml/employee" targetNamespace=“http://www.definesys.com/xml/employee"> <wsdl:types> <xs:schema xmlns:xs=“http://www.w3.org/2001/XMLSchema" elementFormDefault=“qualified” targetNamespace=“http://www.definesys.com/xml/employee"> <xs:element name=“EmployeeDetailRequest”> <xs:complexType> <xs:sequence> <xs:element name=“name” type=“xs:string”/> </xs:sequence> </xs:complexType> </xs:element> <xs:element name=“EmployeeDetailResponse”> <xs:complexType> <xs:sequence> <xs:element name=“Employee” type=“tns:Employee”/> </xs:sequence> </xs:complexType> </xs:element> <xs:complexType name=“Employee”> <xs:sequence> <xs:element name=“name” type=“xs:string”/> <xs:element name=“email” type=“xs:string”/> </xs:sequence> </xs:complexType></xs:schema> </wsdl:types> <wsdl:message name=“EmployeeDetailRequest”> <wsdl:part element=“tns:EmployeeDetailRequest” name=“EmployeeDetailRequest”> </wsdl:part> </wsdl:message> <wsdl:message name=“EmployeeDetailResponse”> <wsdl:part element=“tns:EmployeeDetailResponse” name=“EmployeeDetailResponse”> </wsdl:part> </wsdl:message> <wsdl:portType name=“Employee”> <wsdl:operation name=“EmployeeDetail”> <wsdl:input message=“tns:EmployeeDetailRequest” name=“EmployeeDetailRequest”> </wsdl:input> <wsdl:output message=“tns:EmployeeDetailResponse” name=“EmployeeDetailResponse”> </wsdl:output> </wsdl:operation> </wsdl:portType> <wsdl:binding name=“EmployeeSoap11” type=“tns:Employee”> <soap:binding style=“document” transport=“http://schemas.xmlsoap.org/soap/http"/> <wsdl:operation name=“EmployeeDetail”> <soap:operation soapAction=””/> <wsdl:input name=“EmployeeDetailRequest”> <soap:body use=“literal”/> </wsdl:input> <wsdl:output name=“EmployeeDetailResponse”> <soap:body use=“literal”/> </wsdl:output> </wsdl:operation> </wsdl:binding> <wsdl:service name=“EmployeeService”> <wsdl:port binding=“tns:EmployeeSoap11” name=“EmployeeSoap11”> <soap:address location=“http://localhost:8081/ws-servlet/ws/employee-detail”/> </wsdl:port> </wsdl:service></wsdl:definitions>soap:address location里面端口号需要修改为servlet运行的端口号。从以下xml片段可以看出…<wsdl:binding name=“EmployeeSoap11” type=“tns:Employee”> <soap:binding style=“document” transport=“http://schemas.xmlsoap.org/soap/http"/> <wsdl:operation name=“EmployeeDetail”> <soap:operation soapAction=””/> <wsdl:input name=“EmployeeDetailRequest”> <soap:body use=“literal”/> </wsdl:input> <wsdl:output name=“EmployeeDetailResponse”> <soap:body use=“literal”/> </wsdl:output> </wsdl:operation> </wsdl:binding> <wsdl:service name=“EmployeeService”> <wsdl:port binding=“tns:EmployeeSoap11” name=“EmployeeSoap11”> <soap:address location=“http://localhost:8081/ws-servlet/ws/employee-detail”/> </wsdl:port> </wsdl:service>接口名称是EmployeeDetail(wsdl:operation)接口输入参数是EmployeeDetailRequest(wsdl:input)接口输出参数是EmployeeDetailResponse(wsdl:output)接口地址是http://localhost:8081/ws-servlet/ws/employee-detail(soap:address)2. 获取wsdl文件servletpackage com.definesys.demo.servlet;import javax.servlet.ServletException;import javax.servlet.http.HttpServlet;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import java.io.IOException;/** * @Copyright: Shanghai Definesys Company.All rights reserved. * @Description: * @author: jianfeng.zheng * @since: 2019/1/5 下午1:45 * @history: 1.2019/1/5 created by jianfeng.zheng /public class WsdlServlet extends HttpServlet { public static final String WSDL_XML = “<?xml version="1.0" encoding="UTF-8" standalone="no"?><wsdl:definitions xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/" xmlns:sch="http://www.definesys.com/xml/employee" xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/" xmlns:tns="http://www.definesys.com/xml/employee" targetNamespace="http://www.definesys.com/xml/employee">\n” + " <wsdl:types>\n” + " <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" elementFormDefault="qualified" targetNamespace="http://www.definesys.com/xml/employee">\n” + “\n” + " <xs:element name="EmployeeDetailRequest">\n” + " <xs:complexType>\n” + " <xs:sequence>\n” + " <xs:element name="name" type="xs:string"/>\n" + " </xs:sequence>\n" + " </xs:complexType>\n" + " </xs:element>\n" + “\n” + " <xs:element name="EmployeeDetailResponse">\n" + " <xs:complexType>\n" + " <xs:sequence>\n" + " <xs:element name="Employee" type="tns:Employee"/>\n" + " </xs:sequence>\n" + " </xs:complexType>\n" + " </xs:element>\n" + “\n” + " <xs:complexType name="Employee">\n" + " <xs:sequence>\n" + " <xs:element name="name" type="xs:string"/>\n" + " <xs:element name="email" type="xs:string"/>\n" + " </xs:sequence>\n" + " </xs:complexType>\n" + “\n” + “</xs:schema>\n” + " </wsdl:types>\n" + " <wsdl:message name="EmployeeDetailRequest">\n" + " <wsdl:part element="tns:EmployeeDetailRequest" name="EmployeeDetailRequest">\n" + " </wsdl:part>\n" + " </wsdl:message>\n" + " <wsdl:message name="EmployeeDetailResponse">\n" + " <wsdl:part element="tns:EmployeeDetailResponse" name="EmployeeDetailResponse">\n" + " </wsdl:part>\n" + " </wsdl:message>\n" + " <wsdl:portType name="Employee">\n" + " <wsdl:operation name="EmployeeDetail">\n" + " <wsdl:input message="tns:EmployeeDetailRequest" name="EmployeeDetailRequest">\n" + " </wsdl:input>\n" + " <wsdl:output message="tns:EmployeeDetailResponse" name="EmployeeDetailResponse">\n" + " </wsdl:output>\n" + " </wsdl:operation>\n" + " </wsdl:portType>\n" + " <wsdl:binding name="EmployeeSoap11" type="tns:Employee">\n" + " <soap:binding style="document" transport="http://schemas.xmlsoap.org/soap/http"/>\n" + " <wsdl:operation name="EmployeeDetail">\n" + " <soap:operation soapAction=""/>\n" + " <wsdl:input name="EmployeeDetailRequest">\n" + " <soap:body use="literal"/>\n" + " </wsdl:input>\n" + " <wsdl:output name="EmployeeDetailResponse">\n" + " <soap:body use="literal"/>\n" + " </wsdl:output>\n" + " </wsdl:operation>\n" + " </wsdl:binding>\n" + " <wsdl:service name="EmployeeService">\n" + " <wsdl:port binding="tns:EmployeeSoap11" name="EmployeeSoap11">\n" + " <soap:address location="http://localhost:8081/ws-servlet/ws/employee-detail"/>\n" + " </wsdl:port>\n" + " </wsdl:service>\n" + “</wsdl:definitions>”; @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.setContentType(“text/xml”); resp.getOutputStream().write(WSDL_XML.getBytes()); }}是不是很简单,是的,为了简单,我直接将wsdl文件用变量存储,我们还需要配置下web.xmlweb.xml<?xml version=“1.0” encoding=“UTF-8”?><web-app xmlns=“http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi=“http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation=“http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd" version=“3.1”> <servlet> <servlet-name>wsdl</servlet-name> <servlet-class>com.definesys.demo.servlet.WsdlServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>wsdl</servlet-name> <url-pattern>/ws/employee</url-pattern> </servlet-mapping></web-app>这样我们访问http://localhost:8080/ws/employee就能返回一个wsdl文件,也就是接口描述文件。在wsdl文件里,我们定义接口地址为http://localhost:8080/ws/employee-detail,接下来我们就要实现这个接口。3. 业务servletimport javax.servlet.ServletException;import javax.servlet.http.HttpServlet;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import java.io.IOException;/* * @Copyright: Shanghai Definesys Company.All rights reserved. * @Description: * @author: jianfeng.zheng * @since: 2019/1/5 下午2:56 * @history: 1.2019/1/5 created by jianfeng.zheng /public class EmployeeServlet extends HttpServlet { @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String response = “<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">\n” + " <SOAP-ENV:Header/>\n” + " <SOAP-ENV:Body>\n” + " <ns2:EmployeeDetailResponse xmlns:ns2="http://www.definesys.com/xml/employee">\n” + " <ns2:Employee>\n" + " <ns2:name>jianfeng</ns2:name>\n" + " <ns2:email>jianfeng.zheng@definesys.com</ns2:email>\n" + " </ns2:Employee>\n" + " </ns2:EmployeeDetailResponse>\n" + " </SOAP-ENV:Body>\n" + “</SOAP-ENV:Envelope>”; resp.getOutputStream().write(response.getBytes()); }}这里不做任何业务处理,不做xml转bean,不做bean转xml,就是这么暴力,直接返回xml,但他仍是一个soap服务,支持所有soap工具调用。将servlet配置到web.xml里web.xml<?xml version=“1.0” encoding=“UTF-8”?><web-app xmlns=“http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi=“http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation=“http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd" version=“3.1”> <servlet> <servlet-name>wsdl</servlet-name> <servlet-class>com.definesys.demo.servlet.WsdlServlet</servlet-class> </servlet> <servlet> <servlet-name>employee</servlet-name> <servlet-class>com.definesys.demo.servlet.EmployeeServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>wsdl</servlet-name> <url-pattern>/ws/employee</url-pattern> </servlet-mapping> <servlet-mapping> <servlet-name>employee</servlet-name> <url-pattern>/ws/employee-detail</url-pattern> </servlet-mapping></web-app>/ws/employee-detail这个地址必须和wsdl文件里定义的保持一致,不然服务无法被找到。4. 测试使用soapui测试我们的webservice,通过地址http://localhost:8081/ws-servlet/ws/employee导入wsdl文件,测试接口,返回我们在业务servlet里面写死的内容。恭喜你,你已经不依赖任何第三方包完成了一个soap webservice。当然这个只是一个玩具,但框架就是在上面的基础上进行扩展,增加wsdl文件自动生成,xml转java,java转xml,xml校验,错误处理等功能,如果你有时间,你也可以写一个soap webservice框架。代码已经上传至github,欢迎star,开始进入正题,偏的有点远。spring boot开发soap webservice1. 创建spring boot工程你可以通过spring initializr初始化spring boot工程,也可以通过inte idea的spring initializr插件进行初始化,个人推荐后面这种。2. 添加依赖添加soap webservice相关依赖包和插件,pom.xml<!–依赖–><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web-services</artifactId></dependency><dependency> <groupId>wsdl4j</groupId> <artifactId>wsdl4j</artifactId></dependency>…<!–插件–><plugin> <groupId>org.codehaus.mojo</groupId> <artifactId>jaxb2-maven-plugin</artifactId> <version>1.6</version> <executions> <execution> <id>xjc</id> <goals> <goal>xjc</goal> </goals> </execution> </executions> <configuration> <schemaDirectory>${project.basedir}/src/main/resources/</schemaDirectory> <!–<schemaFiles>employee.xsd</schemaFiles>–> <outputDirectory>${project.basedir}/src/main/java</outputDirectory> <packageName>com.definesys.tutorial.ws.type</packageName> <clearOutputDir>false</clearOutputDir> </configuration></plugin>插件jaxb2能够实现java和xml之间互转,下面是几个参数的说明schemaDirectory:xsd文件目录schemaFiles:指定schemaDirectory下的xsd文件,多个用逗号隔开,必须指定schemaDirectoryoutputDirectory:生成java文件保存目录packageName:生成java文件包路径clearOutputDir:重新生成前是否需要清空目录3. 编写xsd文件假设我们的需求是通过员工工号查询员工详细信息,根据需求编写以下xsd文件,并保存在/src/main/resources/目录下。employee.xsd<xs:schema xmlns:xs=“http://www.w3.org/2001/XMLSchema" xmlns:tns=“http://www.definesys.com/xml/employee" targetNamespace=“http://www.definesys.com/xml/employee" elementFormDefault=“qualified”> <xs:element name=“EmployeeDetailRequest”> <xs:complexType> <xs:sequence> <xs:element name=“code” type=“xs:string”/> </xs:sequence> </xs:complexType> </xs:element> <xs:element name=“EmployeeDetailResponse”> <xs:complexType> <xs:sequence> <xs:element name=“Employee” type=“tns:Employee”/> </xs:sequence> </xs:complexType> </xs:element> <xs:complexType name=“Employee”> <xs:sequence> <xs:element name=“code” type=“xs:string”/> <xs:element name=“name” type=“xs:string”/> <xs:element name=“email” type=“xs:string”/> </xs:sequence> </xs:complexType></xs:schema>4. 生成java类型文件我们需要根据xsd文件生成java类型文件,这就要借助maven插件jaxb2,打开终端运行命令mvn jaxb2:xjc,如果运行正常,就会在目录com.definesys.tutorial.ws.type下生成一堆java文件,此时文件结构如下:.├── java│ └── com│ └── definesys│ └── tutorial│ └── ws│ ├── SpringbootWsApplication.java│ └── type│ ├── Employee.java│ ├── EmployeeDetailRequest.java│ ├── EmployeeDetailResponse.java│ ├── ObjectFactory.java│ └── package-info.java└── resources ├── application.properties ├── employee.xsd ├── static └── templates5. 创建配置文件WebserviceConfig.javapackage com.definesys.tutorial.ws;import org.springframework.boot.web.servlet.ServletRegistrationBean;import org.springframework.context.ApplicationContext;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.core.io.ClassPathResource;import org.springframework.ws.config.annotation.EnableWs;import org.springframework.ws.config.annotation.WsConfigurerAdapter;import org.springframework.ws.transport.http.MessageDispatcherServlet;import org.springframework.ws.wsdl.wsdl11.DefaultWsdl11Definition;import org.springframework.ws.wsdl.wsdl11.Wsdl11Definition;import org.springframework.xml.xsd.SimpleXsdSchema;import org.springframework.xml.xsd.XsdSchema;/* * @Copyright: Shanghai Definesys Company.All rights reserved. * @Description: * @author: jianfeng.zheng * @since: 2019/1/5 下午4:46 * @history: 1.2019/1/5 created by jianfeng.zheng /@EnableWs@Configurationpublic class WebserviceConfig extends WsConfigurerAdapter { @Bean public ServletRegistrationBean messageDispatcherServlet(ApplicationContext applicationContext) { MessageDispatcherServlet servlet = new MessageDispatcherServlet(); servlet.setApplicationContext(applicationContext); servlet.setTransformWsdlLocations(true); return new ServletRegistrationBean(servlet, “/ws/”); } @Bean(name = “employee”) public Wsdl11Definition defaultWsdl11Definition(XsdSchema schema) { DefaultWsdl11Definition wsdl = new DefaultWsdl11Definition(); wsdl.setPortTypeName(“EmployeePort”); wsdl.setLocationUri("/ws/employee-detail”); wsdl.setTargetNamespace(“http://www.definesys.com/xml/employee"); wsdl.setSchema(schema); return wsdl; } @Bean public XsdSchema employeeSchema() { return new SimpleXsdSchema(new ClassPathResource(“employee.xsd”)); }}6. 创建业务服务EmployeeSoapController.javapackage com.definesys.tutorial.ws;import com.definesys.tutorial.ws.type.Employee;import com.definesys.tutorial.ws.type.EmployeeDetailRequest;import com.definesys.tutorial.ws.type.EmployeeDetailResponse;import org.springframework.ws.server.endpoint.annotation.PayloadRoot;import org.springframework.ws.server.endpoint.annotation.RequestPayload;import org.springframework.ws.server.endpoint.annotation.ResponsePayload;/** * @Copyright: Shanghai Definesys Company.All rights reserved. * @Description: * @author: jianfeng.zheng * @since: 2019/1/5 下午4:49 * @history: 1.2019/1/5 created by jianfeng.zheng /@Endpointpublic class EmployeeSoapController { private static final String NAMESPACE_URI = “http://www.definesys.com/xml/employee"; @PayloadRoot(namespace = NAMESPACE_URI, localPart = “EmployeeDetailRequest”) @ResponsePayload public EmployeeDetailResponse getEmployee(@RequestPayload EmployeeDetailRequest request) { EmployeeDetailResponse response = new EmployeeDetailResponse(); //这里只作为演示,真正开发中需要编写业务逻辑代码 Employee employee = new Employee(); employee.setName(“jianfeng”); employee.setEmail(“jianfeng.zheng@definesys.com”); employee.setCode(request.getCode()); response.setEmployee(employee); return response; }}与RestController不一样的是,spring boot soap是根据请求报文来指定调用的函数,RestController是根据请求路径来确定。@PayloadRoot就是关键,如本次请求报文如下:<soapenv:Envelope xmlns:soapenv=“http://schemas.xmlsoap.org/soap/envelope/" xmlns:emp=“http://www.definesys.com/xml/employee"> <soapenv:Header/> <soapenv:Body> <emp:EmployeeDetailRequest> <emp:code>?</emp:code> </emp:EmployeeDetailRequest> </soapenv:Body></soapenv:Envelope>xmlns:emp=“http://www.definesys.com/xml/employee"就是@PayloadRoot.namespace,emp:EmployeeDetailRequest对应@PayloadRoot.localPart。理解了这个其他都很好理解。7. 测试使用soapui进行测试,通过地址http://localhost:8080/ws/employee.wsdl导入wsdl文件进行测试。输入报文<soapenv:Envelope xmlns:soapenv=“http://schemas.xmlsoap.org/soap/envelope/" xmlns:emp=“http://www.definesys.com/xml/employee"> <soapenv:Header/> <soapenv:Body> <emp:EmployeeDetailRequest> <emp:code>004</emp:code> </emp:EmployeeDetailRequest> </soapenv:Body></soapenv:Envelope>输出报文<SOAP-ENV:Envelope xmlns:SOAP-ENV=“http://schemas.xmlsoap.org/soap/envelope/"> <SOAP-ENV:Header/> <SOAP-ENV:Body> <ns2:EmployeeDetailResponse xmlns:ns2=“http://www.definesys.com/xml/employee"> <ns2:Employee> <ns2:code>004</ns2:code> <ns2:name>jianfeng</ns2:name> <ns2:email>jianfeng.zheng@definesys.com</ns2:email> </ns2:Employee> </ns2:EmployeeDetailResponse> </SOAP-ENV:Body></SOAP-ENV:Envelope>同时提供soap和restful两种服务soap一般在企业内部用的比较多,做系统间的集成,restful一般用于移动应用和h5应用,如果在企业应用开发里能够同时提供两种协议的支持,将极大提高接口的复用。其实也没有想象中的那么复杂,在本例中,只需把业务逻辑部分用service实现再创建一个RestController即可,通过设计模式即可解决,不需要引入新的技术。EmployeeService.javapackage com.definesys.tutorial.ws;import com.definesys.tutorial.ws.type.Employee;import com.definesys.tutorial.ws.type.EmployeeDetailRequest;import com.definesys.tutorial.ws.type.EmployeeDetailResponse;import org.springframework.stereotype.Service;/* * @Copyright: Shanghai Definesys Company.All rights reserved. * @Description: * @author: jianfeng.zheng * @since: 2019/1/5 下午5:42 * @history: 1.2019/1/5 created by jianfeng.zheng /@Servicepublic class EmployeeService { public EmployeeDetailResponse getEmployee(EmployeeDetailRequest request) { EmployeeDetailResponse response = new EmployeeDetailResponse(); //这里只作为演示,真正开发中需要编写业务逻辑代码 Employee employee = new Employee(); employee.setName(“jianfeng”); employee.setEmail(“jianfeng.zheng@definesys.com”); employee.setCode(request.getCode()); response.setEmployee(employee); return response; }}EmployeeSoapController.javapackage com.definesys.tutorial.ws;import com.definesys.tutorial.ws.type.Employee;import com.definesys.tutorial.ws.type.EmployeeDetailRequest;import com.definesys.tutorial.ws.type.EmployeeDetailResponse;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.ws.server.endpoint.annotation.Endpoint;import org.springframework.ws.server.endpoint.annotation.PayloadRoot;import org.springframework.ws.server.endpoint.annotation.RequestPayload;import org.springframework.ws.server.endpoint.annotation.ResponsePayload;/* * @Copyright: Shanghai Definesys Company.All rights reserved. * @Description: * @author: jianfeng.zheng * @since: 2019/1/5 下午4:49 * @history: 1.2019/1/5 created by jianfeng.zheng /@Endpointpublic class EmployeeSoapController { @Autowired private EmployeeService service; private static final String NAMESPACE_URI = “http://www.definesys.com/xml/employee"; @PayloadRoot(namespace = NAMESPACE_URI, localPart = “EmployeeDetailRequest”) @ResponsePayload public EmployeeDetailResponse getEmployee(@RequestPayload EmployeeDetailRequest request) { return service.getEmployee(request); }}EmployeeRestController.javapackage com.definesys.tutorial.ws;import com.definesys.tutorial.ws.type.EmployeeDetailRequest;import com.definesys.tutorial.ws.type.EmployeeDetailResponse;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.web.bind.annotation.RequestBody;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RequestMethod;import org.springframework.web.bind.annotation.RestController;/* * @Copyright: Shanghai Definesys Company.All rights reserved. * @Description: * @author: jianfeng.zheng * @since: 2019/1/5 下午5:43 * @history: 1.2019/1/5 created by jianfeng.zheng */@RestController@RequestMapping(value = “/rest”)public class EmployeeRestController { @Autowired private EmployeeService service; @RequestMapping(value = “/employee-detail”, method = RequestMethod.POST) public EmployeeDetailResponse getEmployeeDetail(@RequestBody EmployeeDetailRequest request) { return service.getEmployee(request); }}测试$ curl http://localhost:8080/rest/employee-detail -X POST -d ‘{“code”:“004”}’ -H “Content-Type: application/json”{ “employee”: { “code”: “004”, “name”: “jianfeng”, “email”: “jianfeng.zheng@definesys.com” }}这样就实现了soap和rest同时提供的目的。本文代码已提交至gitlab欢迎star相关参考文档https://spring.io/guides/gs/producing-web-service/https://github.com/wls1036/tutorial-springboot-soaphttps://github.com/wls1036/pure-ws-servlet ...

January 5, 2019 · 5 min · jiezi

Spring Boot引起的“堆外内存泄漏”排查及经验总结

背景为了更好地实现对项目的管理,我们将组内一个项目迁移到MDP框架(基于Spring Boot),随后我们就发现系统会频繁报出Swap区域使用量过高的异常。笔者被叫去帮忙查看原因,发现配置了4G堆内内存,但是实际使用的物理内存竟然高达7G,确实不正常。JVM参数配置是“-XX:MetaspaceSize=256M -XX:MaxMetaspaceSize=256M -XX:+AlwaysPreTouch -XX:ReservedCodeCacheSize=128m -XX:InitialCodeCacheSize=128m, -Xss512k -Xmx4g -Xms4g,-XX:+UseG1GC -XX:G1HeapRegionSize=4M”,实际使用的物理内存如下图所示:排查过程1. 使用Java层面的工具定位内存区域(堆内内存、Code区域或者使用unsafe.allocateMemory和DirectByteBuffer申请的堆外内存)笔者在项目中添加-XX:NativeMemoryTracking=detailJVM参数重启项目,使用命令jcmd pid VM.native_memory detail查看到的内存分布如下:发现命令显示的committed的内存小于物理内存,因为jcmd命令显示的内存包含堆内内存、Code区域、通过unsafe.allocateMemory和DirectByteBuffer申请的内存,但是不包含其他Native Code(C代码)申请的堆外内存。所以猜测是使用Native Code申请内存所导致的问题。为了防止误判,笔者使用了pmap查看内存分布,发现大量的64M的地址;而这些地址空间不在jcmd命令所给出的地址空间里面,基本上就断定就是这些64M的内存所导致。2. 使用系统层面的工具定位堆外内存因为笔者已经基本上确定是Native Code所引起,而Java层面的工具不便于排查此类问题,只能使用系统层面的工具去定位问题。首先,使用了gperftools去定位问题gperftools的使用方法可以参考gperftools,gperftools的监控如下:从上图可以看出:使用malloc申请的的内存最高到3G之后就释放了,之后始终维持在700M-800M。笔者第一反应是:难道Native Code中没有使用malloc申请,直接使用mmap/brk申请的?(gperftools原理就使用动态链接的方式替换了操作系统默认的内存分配器(glibc)。)然后,使用strace去追踪系统调用因为使用gperftools没有追踪到这些内存,于是直接使用命令“strace -f -e"brk,mmap,munmap" -p pid”追踪向OS申请内存请求,但是并没有发现有可疑内存申请。strace监控如下图所示:接着,使用GDB去dump可疑内存因为使用strace没有追踪到可疑内存申请;于是想着看看内存中的情况。就是直接使用命令gdp -pid pid进入GDB之后,然后使用命令dump memory mem.bin startAddress endAddressdump内存,其中startAddress和endAddress可以从/proc/pid/smaps中查找。然后使用strings mem.bin查看dump的内容,如下:从内容上来看,像是解压后的JAR包信息。读取JAR包信息应该是在项目启动的时候,那么在项目启动之后使用strace作用就不是很大了。所以应该在项目启动的时候使用strace,而不是启动完成之后。再次,项目启动时使用strace去追踪系统调用项目启动使用strace追踪系统调用,发现确实申请了很多64M的内存空间,截图如下:使用该mmap申请的地址空间在pmap对应如下:最后,使用jstack去查看对应的线程因为strace命令中已经显示申请内存的线程ID。直接使用命令jstack pid去查看线程栈,找到对应的线程栈(注意10进制和16进制转换)如下:这里基本上就可以看出问题来了:MCC(美团统一配置中心)使用了Reflections进行扫包,底层使用了Spring Boot去加载JAR。因为解压JAR使用Inflater类,需要用到堆外内存,然后使用Btrace去追踪这个类,栈如下:然后查看使用MCC的地方,发现没有配置扫包路径,默认是扫描所有的包。于是修改代码,配置扫包路径,发布上线后内存问题解决。3. 为什么堆外内存没有释放掉呢?虽然问题已经解决了,但是有几个疑问:为什么使用旧的框架没有问题?为什么堆外内存没有释放?为什么内存大小都是64M,JAR大小不可能这么大,而且都是一样大?为什么gperftools最终显示使用的的内存大小是700M左右,解压包真的没有使用malloc申请内存吗?带着疑问,笔者直接看了一下Spring Boot Loader那一块的源码。发现Spring Boot对Java JDK的InflaterInputStream进行了包装并且使用了Inflater,而Inflater本身用于解压JAR包的需要用到堆外内存。而包装之后的类ZipInflaterInputStream没有释放Inflater持有的堆外内存。于是笔者以为找到了原因,立马向Spring Boot社区反馈了这个bug。但是反馈之后,笔者就发现Inflater这个对象本身实现了finalize方法,在这个方法中有调用释放堆外内存的逻辑。也就是说Spring Boot依赖于GC释放堆外内存。笔者使用jmap查看堆内对象时,发现已经基本上没有Inflater这个对象了。于是就怀疑GC的时候,没有调用finalize。带着这样的怀疑,笔者把Inflater进行包装在Spring Boot Loader里面替换成自己包装的Inflater,在finalize进行打点监控,结果finalize方法确实被调用了。于是笔者又去看了Inflater对应的C代码,发现初始化的使用了malloc申请内存,end的时候也调用了free去释放内存。此刻,笔者只能怀疑free的时候没有真正释放内存,便把Spring Boot包装的InflaterInputStream替换成Java JDK自带的,发现替换之后,内存问题也得以解决了。这时,再返过来看gperftools的内存分布情况,发现使用Spring Boot时,内存使用一直在增加,突然某个点内存使用下降了好多(使用量直接由3G降为700M左右)。这个点应该就是GC引起的,内存应该释放了,但是在操作系统层面并没有看到内存变化,那是不是没有释放到操作系统,被内存分配器持有了呢?继续探究,发现系统默认的内存分配器(glibc 2.12版本)和使用gperftools内存地址分布差别很明显,2.5G地址使用smaps发现它是属于Native Stack。内存地址分布如下:到此,基本上可以确定是内存分配器在捣鬼;搜索了一下glibc 64M,发现glibc从2.11开始对每个线程引入内存池(64位机器大小就是64M内存),原文如下:按照文中所说去修改MALLOC_ARENA_MAX环境变量,发现没什么效果。查看tcmalloc(gperftools使用的内存分配器)也使用了内存池方式。为了验证是内存池搞的鬼,笔者就简单写个不带内存池的内存分配器。使用命令gcc zjbmalloc.c -fPIC -shared -o zjbmalloc.so生成动态库,然后使用export LD_PRELOAD=zjbmalloc.so替换掉glibc的内存分配器。其中代码Demo如下:#include<sys/mman.h>#include<stdlib.h>#include<string.h>#include<stdio.h>//作者使用的64位机器,sizeof(size_t)也就是sizeof(long) void* malloc ( size_t size ){ long* ptr = mmap( 0, size + sizeof(long), PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, 0, 0 ); if (ptr == MAP_FAILED) { return NULL; } ptr = size; // First 8 bytes contain length. return (void)(&ptr[1]); // Memory that is after length variable}void calloc(size_t n, size_t size) { void ptr = malloc(n * size); if (ptr == NULL) { return NULL; } memset(ptr, 0, n * size); return ptr;}void *realloc(void ptr, size_t size){ if (size == 0) { free(ptr); return NULL; } if (ptr == NULL) { return malloc(size); } long plen = (long)ptr; plen–; // Reach top of memory long len = plen; if (size <= len) { return ptr; } void rptr = malloc(size); if (rptr == NULL) { free(ptr); return NULL; } rptr = memcpy(rptr, ptr, len); free(ptr); return rptr;}void free (void ptr ){ if (ptr == NULL) { return; } long plen = (long)ptr; plen–; // Reach top of memory long len = plen; // Read length munmap((void)plen, len + sizeof(long));}通过在自定义分配器当中埋点可以发现其实程序启动之后应用实际申请的堆外内存始终在700M-800M之间,gperftools监控显示内存使用量也是在700M-800M左右。但是从操作系统角度来看进程占用的内存差别很大(这里只是监控堆外内存)。笔者做了一下测试,使用不同分配器进行不同程度的扫包,占用的内存如下:为什么自定义的malloc申请800M,最终占用的物理内存在1.7G呢?因为自定义内存分配器采用的是mmap分配内存,mmap分配内存按需向上取整到整数个页,所以存在着巨大的空间浪费。通过监控发现最终申请的页面数目在536k个左右,那实际上向系统申请的内存等于512k * 4k(pagesize) = 2G。为什么这个数据大于1.7G呢?因为操作系统采取的是延迟分配的方式,通过mmap向系统申请内存的时候,系统仅仅返回内存地址并没有分配真实的物理内存。只有在真正使用的时候,系统产生一个缺页中断,然后再分配实际的物理Page。总结整个内存分配的流程如上图所示。MCC扫包的默认配置是扫描所有的JAR包。在扫描包的时候,Spring Boot不会主动去释放堆外内存,导致在扫描阶段,堆外内存占用量一直持续飙升。当发生GC的时候,Spring Boot依赖于finalize机制去释放了堆外内存;但是glibc为了性能考虑,并没有真正把内存归返到操作系统,而是留下来放入内存池了,导致应用层以为发生了“内存泄漏”。所以修改MCC的配置路径为特定的JAR包,问题解决。笔者在发表这篇文章时,发现Spring Boot的最新版本(2.0.5.RELEASE)已经做了修改,在ZipInflaterInputStream主动释放了堆外内存不再依赖GC;所以Spring Boot升级到最新版本,这个问题也可以得到解决。参考资料GNU C Library (glibc)Native Memory TrackingSpring BootgperftoolsBtrace作者简介纪兵,2015年加入美团,目前主要从事酒店C端相关的工作。 ...

January 4, 2019 · 2 min · jiezi

Spring Cloud Stream 使用延迟消息实现定时任务(RabbitMQ)

应用场景我们在使用一些开源调度系统(比如:elastic-job等)的时候,对于任务的执行时间通常都是有规律性的,可能是每隔半小时执行一次,或者每天凌晨一点执行一次。然而实际业务中还存在另外一种定时任务,它可能需要一些触发条件才开始定时,比如:编写博文时候,设置2小时之后发送。对于这些开始时间不确定的定时任务,我们也可以通过Spring Cloud Stream来很好的处理。为了实现开始时间不确定的定时任务触发,我们将引入延迟消息的使用。RabbitMQ中提供了关于延迟消息的插件,所以本文就来具体介绍以下如何利用Spring Cloud Stream以及RabbitMQ轻松的处理上述问题。动手试试插件安装关于RabbitMQ延迟消息的插件介绍可以查看官方网站:https://www.rabbitmq.com/blog…安装方式很简单,只需要在这个页面:http://www.rabbitmq.com/commu… 中找到rabbitmq_delayed_message_exchange插件,根据您使用的RabbitMQ版本选择对应的插件版本下载即可。注意:只有RabbitMQ 3.6.x以上才支持在下载好之后,解压得到.ez结尾的插件包,将其复制到RabbitMQ安装目录下的plugins文件夹。然后通过命令行启用该插件:rabbitmq-plugins enable rabbitmq_delayed_message_exchange该插件在通过上述命令启用后就可以直接使用,不需要重启。另外,如果您没有启用该插件,您可能为遇到类似这样的错误:ERROR 156 — [ 127.0.0.1:5672] o.s.a.r.c.CachingConnectionFactory : Channel shutdown: connection error; protocol method: #method(reply-code=503, reply-text=COMMAND_INVALID - unknown exchange type ‘x-delayed-message’, class-id=40, method-id=10)应用编码下面通过编写一个简单的例子来具体体会一下这个属性的用法:@EnableBinding(TestApplication.TestTopic.class)@SpringBootApplicationpublic class TestApplication { public static void main(String[] args) { SpringApplication.run(TestApplication.class, args); } @Slf4j @RestController static class TestController { @Autowired private TestTopic testTopic; /** * 消息生产接口 * * @param message * @return / @GetMapping("/sendMessage") public String messageWithMQ(@RequestParam String message) { log.info(“Send: " + message); testTopic.output().send(MessageBuilder.withPayload(message).setHeader(“x-delay”, 5000).build()); return “ok”; } } /* * 消息消费逻辑 */ @Slf4j @Component static class TestListener { @StreamListener(TestTopic.INPUT) public void receive(String payload) { log.info(“Received: " + payload); } } interface TestTopic { String OUTPUT = “example-topic-output”; String INPUT = “example-topic-input”; @Output(OUTPUT) MessageChannel output(); @Input(INPUT) SubscribableChannel input(); }}内容很简单,既包含了消息的生产,也包含了消息消费。在/sendMessage接口的定义中,发送了一条消息,一条消息的头信息中包含了x-delay字段,该字段用来指定消息延迟的时间,单位为毫秒。所以上述代码发送的消息会在5秒之后被消费。在消息监听类TestListener中,对TestTopic.INPUT通道定义了@StreamListener,这里会对延迟消息做具体的逻辑。由于消息的消费是延迟的,从而变相实现了从消息发送那一刻起开始的定时任务。在启动应用之前,还要需要做一些必要的配置,下面分消息生产端和消费端做说明:消息生产端spring.cloud.stream.bindings.example-topic-output.destination=delay-topicspring.cloud.stream.rabbit.bindings.example-topic-output.producer.delayed-exchange=true注意这里的一个新参数spring.cloud.stream.rabbit.bindings.example-topic-output.producer.delayed-exchange,用来开启延迟消息的功能,这样在创建exchange的时候,会将其设置为具有延迟特性的exchange,也就是用到上面我们安装的延迟消息插件的功能。消息消费端spring.cloud.stream.bindings.example-topic-input.destination=delay-topicspring.cloud.stream.bindings.example-topic-input.group=testspring.cloud.stream.rabbit.bindings.example-topic-input.consumer.delayed-exchange=true在消费端也一样,需要设置spring.cloud.stream.rabbit.bindings.example-topic-output.producer.delayed-exchange=true。如果该参数不设置,将会出现类似下面的错误:ERROR 9340 — [ 127.0.0.1:5672] o.s.a.r.c.CachingConnectionFactory : Channel shutdown: channel error; protocol method: #method<channel.close>(reply-code=406, reply-text=PRECONDITION_FAILED - inequivalent arg ’type’ for exchange ‘delay-topic’ in vhost ‘/’: received ’topic’ but current is ‘‘x-delayed-message’’, class-id=40, method-id=10)完成了上面配置之后,就可以启动应用,并尝试访问localhost:8080/sendMessage?message=hello接口来发送一个消息到MQ中了。此时可以看到类似下面的日志:2019-01-02 23:28:45.318 INFO 96164 — [ctor-http-nio-3] c.d.s.TestApplication$TestController : Send: hello2019-01-02 23:28:45.328 INFO 96164 — [ctor-http-nio-3] o.s.a.r.c.CachingConnectionFactory : Attempting to connect to: [localhost:5672]2019-01-02 23:28:45.333 INFO 96164 — [ctor-http-nio-3] o.s.a.r.c.CachingConnectionFactory : Created new connection: rabbitConnectionFactory.publisher#5c5f9a03:0/SimpleConnection@3278a728 [delegate=amqp://guest@127.0.0.1:5672/, localPort= 53536]2019-01-02 23:28:50.349 INFO 96164 — [ay-topic.test-1] c.d.stream.TestApplication$TestListener : Received: hello从日志中可以看到,Send: hello和Received: hello两条输出之间间隔了5秒,符合我们上面编码设置的延迟时间。深入思考在代码层面已经完成了定时任务,那么我们如何查看延迟的消息数等信息呢?此时,我们可以打开RabbitMQ的Web控制台,首先可以进入Exchanges页面,看看这个特殊exchange,具体如下:可以看到,这个exchange的Type类型是x-delayed-message。点击该exchange的名称,进入详细页面,就可以看到更多具体信息了:代码示例本文示例读者可以通过查看下面仓库的中的stream-delayed-message项目:GithubGitee如果您对这些感兴趣,欢迎star、follow、收藏、转发给予支持!以下专题教程也许您会有兴趣Spring Boot基础教程Spring Cloud基础教程本文首发于我的独立博客:http://blog.didispace.com/spr… ...

January 4, 2019 · 1 min · jiezi

Thymeleaf 的基本用法

Thymeleaf 的基本用法属于个人整理的文档,大部分内容来源自网络在这里我们没有打算使用SpringMVC进行整合使用或者说跟Spring Boot 一起使用我们在这里单独使用Servelet版本-算是为了给一些初学者提供部分代码Thymeleaf是一款用于渲染XML/XHTML/HTML5内容的模板引擎,类似JSP,Velocity,FreeMaker等,它也可以轻易的与Spring MVC等Web框架进行集成作为Web应用的模板引擎。Thymeleaf最大的特点是能够直接在浏览器中打开并正确显示模板页面,而不需要启动整个Web应用,但是总是看到说其效率有点低Thymeleaf 在有网络和无网络的环境下皆可运行,即它可以让美工在浏览器查看页面的静态效果,也可以让程序员在服务器查看带数据的动态页面效果。这是由于它支持 html 原型,然后在 html 标签里增加额外的属性来达到模板+数据的展示方式。浏览器解释 html 时会忽略未定义的标签属性,所以 thymeleaf 的模板可以静态地运行;当有数据返回到页面时,Thymeleaf 标签会动态地替换掉静态内容,使页面动态显示。Thymeleaf 开箱即用的特性。它提供标准和spring标准两种方言,可以直接套用模板实现JSTL、 OGNL表达式效果,避免每天套模板、改jstl、改标签的困扰。同时开发人员也可以扩展和创建自定义的方言。1.引入提示在html页面中引入thymeleaf命名空间,即,此时在html模板文件中动态的属性使用th:命名空间修饰 。<html lang=“en” xmlns:th=“http://www.thymeleaf.org”>这样才可以在其他标签里面使用th:这样的语法.这是下面语法的前提.2.变量表达式(获取变量值)<div th:text="‘你是否读过,’+${session.book}+’!!’"> 同EL表达式有些相似的效果,如果有数据,被替换 完成前后端分离效果(美工代码)</div>代码分析:1.可以看出获取变量值用$符号,对于javaBean的话使用变量名.属性名方式获取,这点和EL表达式一样2.它通过标签中的th:text属性来填充该标签的一段内容,意思是$表达式只能写在th标签内部,不然不会生效,上面例子就是使用th:text标签的值替换div标签里面的值,至于div里面的原有的值只是为了给前端开发时做展示用的.这样的话很好的做到了前后端分离.意味着div标签中的内容会被表达式${session.book}的值所替代,无论模板中它的内容是什么,之所以在模板中“多此一举“地填充它的内容,完全是为了它能够作为原型在浏览器中直接显示出来。3.访问spring-mvc中model的属性,语法格式为“${}”,如${user.id}可以获取model里的user对象的id属性 4.牛叉的循环<li th:each=“book : ${books}” >3.URL表达式(引入URL)重点!重点!重点!引用静态资源文件(CSS使用th:href,js使用使用th:src)href链接URL(使用th:href)代码分析1.最终解析的href为: /seconddemo/ /seconddemo/usethymeleaf?name=Dear 相对路径,带一个参数 /seconddemo/usethymeleaf?name=Dear&alis=Dear 相对路径,带多个参数 /seconddemo/usethymeleaf?name=Dear&alis=Dear 相对路径,带多个参数 /seconddemo/usethymeleaf/Dear 相对路径,替换URL一个变量 /seconddemo/usethymeleaf/Dear/Dear 相对路径,替换URL多个变量2.URL最后的(name=${name})表示将括号内的内容作为URL参数处理,该语法避免使用字符串拼接,大大提高了可读性3.@{/usethymeleaf}是Context相关的相对路径,在渲染时会自动添加上当前Web应用的Context名字,假设context名字为seconddemo,那么结果应该是/seconddemo/usethymeleaf,即URL中以”/“开头的路径(比如/usethymeleaf将会加上服务器地址和域名和应用cotextpath,形成完整的URL。4.th:href属性修饰符:它将计算并替换使用href链接URL 值,并放入的href属性中。5.th:href中可以直接使用静态地址4.选择或星号表达式表达式很像变量表达式,不过它们用一个预先选择的对象来代替上下文变量容器(map)来执行{customer.name}<div th:object="${session.user}"> <p>Name: <span th:text="{firstName}">Sebastian</span>.</p> <p>Surname: <span th:text="{lastName}">Pepper</span>.</p> <p>Nationality: <span th:text="{nationality}">Saturn</span>.</p> </div>//等价于<div> <p>Name: <span th:text="${session.user.firstName}">Sebastian</span>.</p> <p>Surname: <span th:text="${session.user.lastName}">Pepper</span>.</p> <p>Nationality: <span th:text="${session.user.nationality}">Saturn</span>.</p></div>1.如果不考虑上下文的情况下,两者没有区别;星号语法评估在选定对象上表达,而不是整个上下文,什么是选定对象?就是父标签的值。上面的{title}表达式可以理解为${book.title}。(父对象) 2.当然,美元符号和星号语法可以混合使用小插曲:三和四的变量表达式、URL表达式所对应的属性都可以使用统一的方式:th.attr=“属性名=属性值”的方式来设置,参考第“七.设置属性值”部分5.文字国际化表达式j简单看一下就可以,文字国际化表达式允许我们从一个外部文件获取区域文字信息(.properties),用Key索引Value,还可以提供一组参数(可选).#{main.title} #{message.entrycreated(${entryId})} 可以在模板文件中找到这样的表达式代码: <table> <th th:text="#{header.address.city}"> <th th:text="#{header.address.country}"></table>6. 表达式支持的语法字面量(Literals)文本文字(Text literals): ‘one text’, ‘Another one!’,…数字文本(Number literals): 0, 34, 3.0, 12.3,…布尔文本(Boolean literals): true, false空(Null literal): null文字标记(Literal tokens): one , sometext文本操作(Text operations)字符串连接(String concatenation): +文本替换(Literal substitutions): |The name is ${name}|<div th:class="‘content’">…</div><span th:text="|Welcome to our application, ${user.name}!|">//Which is equivalent to:<span th:text="‘Welcome to our application, ’ + ${user.name} + ‘!’"><span th:text="${onevar} + ’ ’ + |${twovar}, ${threevar}|">算术运算(Arithmetic operations)二元运算符(Binary operators): + , - , * , / , %减号(Minus sign (unary operator)): -布尔操作(Boolean operations)Binary operators: and , orBoolean negation (unary operator): ! , not比较和等价(Comparisons and equality)Comparators: > , < , >= , <= ( gt , lt , ge , le )Equality operators: == , != ( eq , ne )条件运算符(Conditional operators)三元运算符If-then: (if) ? (then)If-then-else: (if) ? (then) : (else)Default: (value) ?: (defaultvalue)示例一: <h2 th:text="${expression} ? ‘Hello’ : ‘Something else’"></h2>示例二: <!– IF CUSTOMER IS ANONYMOUS –> <div th:if="${customer.anonymous}"> <div>Welcome, Gues!</div> </div> <!– ELSE –> <div th:unless="${customer.anonymous}"> <div th:text=" ‘Hi,’ + ${customer.name}">Hi, User</div> </div>Special tokens:No-Operation: _switch循环渲染列表数据是一种非常常见的场景,例如现在有n条记录需要渲染成一个表格或li列表标签该数据集合必须是可以遍历的,使用th:each标签代码分析:循环,在html的标签中,加入th:each=“value:${list}”形式的属性,如可以迭代prods的数据 又如带状态变量的循环: 利用状态变量判断:7.设置属性值1. th:attr 任何属性值,语法格式:th:attr=“属性名=属性值,[属性名=属性值]" 属性值如果是使用表达式的话:通常有URL表达式@{}和变量表达式${} 但此标签语法不太优雅 示例: th:attr=“action=@{/subscribe}” //当然也可以直接使用th:action th:attr=“src=@{/images/gtvglogo.png},title=#{logo},alt=#{logo}" //可直接使用th:src th:attr=“value=#{subscribe.submit}”//可直接使用th:value <input type=“checkbox” name=“active” th:attr=“checked=${user.active}”/> 设置多个属性在同一时间,有两个特殊的属性可以这样设置: th:alt-title 和 th:lang-xmllang th:src=”@{/images/gtvglogo.png}” th:alt-title="#{logo}" 2.前置和后置添加属性值 th:attrappend 和 th:attrprepend 主要对class和style两个属性 class=“btn” th:attrappend=“class=${’ ’ + cssStyle}" 转换后:class=“btn warning” 3.还有两个特定的添加属性 th:classappend 和 th:styleappend 与上面的attrappend功能一样 class=“row” th:classappend="${prodStat.odd}? ‘odd’" 转换后:奇数行class=“row odd”,偶数行class=“row"8.内嵌变量Utilities为了模板更加易用,Thymeleaf还提供了一系列Utility对象(内置于Context中),可以通过#直接访问。dates : java.util.Date的功能方法类 calendars : 类似#dates,面向java.util.Calendar numbers : 格式化数字的功能方法类 strings : 字符串对象的功能类,contains,startWiths,prepending/appending等等 objects: 对objects的功能类操作 bools: 对布尔值求值的功能方法 arrays:对数组的功能类方法 lists: 对lists功能类方法 sets maps代码示例: ${#dates.format(dateVar, ‘dd/MMM/yyyy HH:mm’)} ${#dates.arrayFormat(datesArray, ‘dd/MMM/yyyy HH:mm’)} ${#dates.listFormat(datesList, ‘dd/MMM/yyyy HH:mm’)} ${#dates.setFormat(datesSet, ‘dd/MMM/yyyy HH:mm’)} ${#dates.createNow()} ${#dates.createToday()} ${#strings.isEmpty(name)} ${#strings.arrayIsEmpty(nameArr)} ${#strings.listIsEmpty(nameList)} ${#strings.setIsEmpty(nameSet)} ${#strings.startsWith(name,‘Don’)} // also array*, list* and set* ${#strings.endsWith(name,endingFragment)} // also array*, list* and set* ${#strings.length(str)} ${#strings.equals(str)} ${#strings.equalsIgnoreCase(str)} ${#strings.concat(str)} ${#strings.concatReplaceNulls(str)} ${#strings.randomAlphanumeric(count)}//产生随机字符串9.thymeleaf布局10.附录thymeleaf_3.0.5_中文参考手册 提取码:emk0 ...

December 29, 2018 · 2 min · jiezi

Spring Boot 静态资源文件配置 A卷

Spring Boot 静态资源文件配置说在前面的话:创建SpringBoot应用,选中我们需要的模块SpringBoot已经默认将这些场景配置好了,只需要在配置文件中指定少量配置就可以运行起来自己编写业务代码由于 Spring Boot 采用了”约定优于配置”这种规范,所以在使用静态资源的时候也很简单。SpringBoot本质上是为微服务而生的,以JAR的形式启动运行,但是有时候静态资源的访问是必不可少的,比如:image、js、css 等资源的访问1.webjars配置静态路径简单了解即可,感觉实用性不大,public class WebMvcAutoConfiguration { public void addResourceHandlers(ResourceHandlerRegistry registry) { if (!this.resourceProperties.isAddMappings()) { logger.debug(“Default resource handling disabled”); } else { Duration cachePeriod = this.resourceProperties.getCache().getPeriod(); CacheControl cacheControl = this.resourceProperties.getCache().getCachecontrol().toHttpCacheControl(); if (!registry.hasMappingForPattern("/webjars/")) { this.customizeResourceHandlerRegistration(registry.addResourceHandler(new String[]{"/webjars/"}).addResourceLocations(new String[]{“classpath:/META-INF/resources/webjars/”}).setCachePeriod(this.getSeconds(cachePeriod)).setCacheControl(cacheControl)); } String staticPathPattern = this.mvcProperties.getStaticPathPattern(); if (!registry.hasMappingForPattern(staticPathPattern)) { this.customizeResourceHandlerRegistration(registry.addResourceHandler(new String[]{staticPathPattern}).addResourceLocations(getResourceLocations(this.resourceProperties.getStaticLocations())).setCachePeriod(this.getSeconds(cachePeriod)).setCacheControl(cacheControl)); } } }}代码分析: 所有 /webjars/** ,都去 classpath:/META-INF/resources/webjars/ 找资源;webjars:以jar包的方式引入静态资源; webjars提供的依赖官网<dependency> <groupId>org.webjars</groupId> <artifactId>jquery</artifactId> <version>1.12.4</version></dependency>启动服务,测试访问静态地址http://127.0.0.1:8001/hp/webjars/jquery/1.12.4/jquery.js2.默认静态资源路径@ConfigurationProperties( prefix = “spring.resources”, ignoreUnknownFields = false)public class ResourceProperties { private static final String[] CLASSPATH_RESOURCE_LOCATIONS = new String[]{“classpath:/META-INF/resources/”, “classpath:/resources/”, “classpath:/static/”, “classpath:/public/”}; private String[] staticLocations; private boolean addMappings; private final ResourceProperties.Chain chain; private final ResourceProperties.Cache cache; public ResourceProperties() { this.staticLocations = CLASSPATH_RESOURCE_LOCATIONS; this.addMappings = true; this.chain = new ResourceProperties.Chain(); this.cache = new ResourceProperties.Cache(); } public String[] getStaticLocations() { return this.staticLocations; }}摘抄了部分源码,算是为了增加篇幅,从上述代码中我们可以看到,提供了几种默认的配置方式classpath:/staticclasspath:/publicclasspath:/resourcesclasspath:/META-INF/resources备注说明: “/"=>当前项目的根路径我们在src/main/resources目录下新建 public、resources、static 、META-INF等目录目录,并分别放入 1.jpg 2.jpg 3.jpg 4.jpg 5.jpg 五张图片。注意:需要排除webjars的形式,将pom.xml中的代码去掉,在进行测试结果结果如下3.新增静态资源路径我们在spring.resources.static-locations后面追加一个配置classpath:/os/:# 静态文件请求匹配方式spring.mvc.static-path-pattern=/# 修改默认的静态寻址资源目录 多个使用逗号分隔spring.resources.static-locations = classpath:/META-INF/resources/,classpath:/resources/,classpath:/static/,classpath:/public/,classpath:/os/4.自定义静态资源映射在实际开发中,我们可能需要自定义静态资源访问以及上传路径,特别是文件上传,不可能上传的运行的JAR服务中,那么可以通过继承WebMvcConfigurerAdapter来实现自定义路径映射。application.properties 文件配置:# 图片音频上传路径配置(win系统自行变更本地路径)web.upload.path=D:/upload/attr/Demo05BootApplication.java 启动配置:package com.hanpang;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.beans.factory.annotation.Value;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;@SpringBootApplicationpublic class Demo05BootApplication implements WebMvcConfigurer { private final static Logger LOGGER = LoggerFactory.getLogger(Demo05BootApplication.class); @Value("${web.upload.path}”) private String uploadPath; @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/uploads/").addResourceLocations( “file:” + uploadPath); LOGGER.info(“自定义静态资源目录、此处功能用于文件映射”); } public static void main(String[] args) { SpringApplication.run(Demo05BootApplication.class, args); }}5.设置欢迎界面依然从源码出手来解决这个问题public class WebMvcAutoConfiguration { private Optional<Resource> getWelcomePage() { String[] locations = getResourceLocations(this.resourceProperties.getStaticLocations()); return Arrays.stream(locations).map(this::getIndexHtml).filter(this::isReadable).findFirst(); } private Resource getIndexHtml(String location) { return this.resourceLoader.getResource(location + “index.html”); }}5.1 直接设置静态默认页面欢迎页; 静态资源文件夹下的所有index.html页面,被"/“映射;5.2 增加控制器的方式新增模版引擎的支持<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId></dependency>配置核心文件application.propertiesserver.port=8001server.servlet.context-path=/hpspring.mvc.view.prefix=classpath:/templates/没有去设置后缀名设置增加路由@Controllerpublic class IndexController { @GetMapping({”/","/index"}) public String index(){ return “default”; }}访问http://127.0.0.1:8001/hp/5.3 设置默认的View跳转页面新增模版引擎的支持<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId></dependency>配置核心文件application.propertiesserver.port=8001server.servlet.context-path=/hpspring.mvc.view.prefix=classpath:/templates/没有去设置后缀名启动文件的修改如下@SpringBootApplicationpublic class Demo05BootApplication implements WebMvcConfigurer { @Override public void addViewControllers(ViewControllerRegistry registry) { registry.addViewController("/").setViewName(“default”); registry.addViewController("/index1").setViewName(“default”); registry.setOrder(Ordered.HIGHEST_PRECEDENCE); } public static void main(String[] args) { SpringApplication.run(Demo05BootApplication.class, args); }}6. Favicon设置 @Configuration @ConditionalOnProperty( value = {“spring.mvc.favicon.enabled”}, matchIfMissing = true ) public static class FaviconConfiguration implements ResourceLoaderAware { private final ResourceProperties resourceProperties; private ResourceLoader resourceLoader; public FaviconConfiguration(ResourceProperties resourceProperties) { this.resourceProperties = resourceProperties; } public void setResourceLoader(ResourceLoader resourceLoader) { this.resourceLoader = resourceLoader; } @Bean public SimpleUrlHandlerMapping faviconHandlerMapping() { SimpleUrlHandlerMapping mapping = new SimpleUrlHandlerMapping(); mapping.setOrder(-2147483647); mapping.setUrlMap(Collections.singletonMap("/favicon.ico", this.faviconRequestHandler())); return mapping; } @Bean public ResourceHttpRequestHandler faviconRequestHandler() { ResourceHttpRequestHandler requestHandler = new ResourceHttpRequestHandler(); requestHandler.setLocations(this.resolveFaviconLocations()); return requestHandler; } private List<Resource> resolveFaviconLocations() { String[] staticLocations = WebMvcAutoConfiguration.WebMvcAutoConfigurationAdapter.getResourceLocations(this.resourceProperties.getStaticLocations()); List<Resource> locations = new ArrayList(staticLocations.length + 1); Stream var10000 = Arrays.stream(staticLocations); ResourceLoader var10001 = this.resourceLoader; var10001.getClass(); var10000.map(var10001::getResource).forEach(locations::add); locations.add(new ClassPathResource("/")); return Collections.unmodifiableList(locations); } }SpringBoot 默认是开启Favicon,并且提供了一个默认的Favicon,如果想关闭Favicon,只需要在application.properties中添加spring.mvc.favicon.enabled=false如果想更改Favicon,只需要将自己的Favicon.ico(文件名不能改动),放置到类路径根目录、类路径META_INF/resources/下、类路径resources/下、类路径static/下或者类路径public/下。5.附录A.WebMvcConfigurerAdapter过时在Springboot中配置WebMvcConfigurerAdapter的时候发现这个类过时了。所以看了下源码,发现官方在spring5弃用了WebMvcConfigurerAdapter,因为springboot2.0使用的spring5,所以会出现过时。WebMvcConfigurerAdapter已经过时,在新版本中被废弃,以下是比较常用的重写接口:/** 解决跨域问题 /public void addCorsMappings(CorsRegistry registry) ;/ 添加拦截器 /void addInterceptors(InterceptorRegistry registry);/ 这里配置视图解析器 /void configureViewResolvers(ViewResolverRegistry registry);/ 配置内容裁决的一些选项 /void configureContentNegotiation(ContentNegotiationConfigurer configurer);/ 视图跳转控制器 /void addViewControllers(ViewControllerRegistry registry);/ 静态资源处理 /void addResourceHandlers(ResourceHandlerRegistry registry);/ 默认静态资源处理器 /void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer);方案1:直接实现WebMvcConfigurer@Configurationpublic class WebMvcConfg implements WebMvcConfigurer { @Override public void addViewControllers(ViewControllerRegistry registry) { registry.addViewController("/index").setViewName(“index”); }}方案2:直接继承WebMvcConfigurationSupport@Configurationpublic class WebMvcConfg extends WebMvcConfigurationSupport { @Override public void addViewControllers(ViewControllerRegistry registry) { registry.addViewController("/index").setViewName(“index”); }}其实,源码下WebMvcConfigurerAdapter是实现WebMvcConfigurer接口,所以直接实现WebMvcConfigurer接口也可以;WebMvcConfigurationSupport与WebMvcConfigurerAdapter、接口WebMvcConfigurer处于同一个目录下WebMvcConfigurationSupport包含WebMvcConfigurer里面的方法,由此看来版本中应该是推荐使用WebMvcConfigurationSupport类的,WebMvcConfigurationSupport应该是新版本中对WebMvcConfigurerAdapter的替换和扩展B.关于加载目录问题// 可以直接使用addResourceLocations 指定磁盘绝对路径,同样可以配置多个位置,注意路径写法需要加上file:registry.addResourceHandler("/myimgs/").addResourceLocations(“file:H:/myimgs/”);// 访问myres根目录下的fengjing.jpg 的URL为 http://localhost:8080/fengjing.jpg (/** 会覆盖系统默认的配置)registry.addResourceHandler("/**").addResourceLocations(“classpath:/myres/”).addResourceLocations(“classpath:/static/”); ...

December 28, 2018 · 2 min · jiezi

Spring、Spring Boot和TestNG测试指南 - 集成测试中用Docker创建数据库

原文地址在测试关系型数据库一篇里我们使用的是H2数据库,这是为了让你免去你去安装/配置一个数据库的工作,能够尽快的了解到集成测试的过程。在文章里也说了:在真实的开发环境中,集成测试用数据库应该和最终的生产数据库保持一致那么很容易就能想到两种解决方案:开发团队使用共用同一个数据库。这样做的问题在于:当有多个集成测试同时在跑时,会产生错误的测试结果。每个人使用自己的数据库。这样做的问题在于让开发人员维护MySQL数据库挺麻烦的。那么做到能否这样呢?测试启动前,创建一个MySQL数据库测试过程中连接到这个数据库测试结束后,删除这个MySQL数据库So, Docker comes to the rescue。我们还是会以测试关系型数据库里的FooRepositoryImpl来做集成测试(代码在这里)。下面来讲解具体步骤:安装Docker请查阅官方文档。并且掌握Docker的基本概念。配置fabric8 docker-maven-pluginfarbic8 docker-maven-plugin顾名思义就是一个能够使用docker的maven plugin。它主要功能有二:创建Docker image启动Docker container我们这里使用启动Docker container的功能。大致配置如下 <plugin> <groupId>io.fabric8</groupId> <artifactId>docker-maven-plugin</artifactId> <version>0.28.0</version> <configuration> <images> <image> <!– 使用mysql:8 docker image –> <name>mysql:8</name> <!– 定义docker run mysql:8 时的参数 –> <run> <ports> <!– host port到container port的映射 这里随机选择一个host port,并将值存到property docker-mysql.port里 –> <port>docker-mysql.port:3306</port> </ports> <!– 启动时给的环境变量,参阅文档:https://hub.docker.com/_/mysql –> <env> <MYSQL_ROOT_PASSWORD>123456</MYSQL_ROOT_PASSWORD> <MYSQL_DATABASE>test</MYSQL_DATABASE> <MYSQL_USER>foo</MYSQL_USER> <MYSQL_PASSWORD>bar</MYSQL_PASSWORD> </env> <!– 设置判定container启动成功的的条件及timeout –> <wait> <!– 如果container打出了这行日志,则说明容器启动成功 –> <log>MySQL init process done. Ready for start up.</log> <time>120000</time> </wait> </run> </image> </images> </configuration> <executions> <execution> <!– 在集成测试开始前启动容器 –> <id>start</id> <phase>pre-integration-test</phase> <goals> <goal>start</goal> </goals> </execution> <execution> <!– 在集成测试结束后停止并删除容器 –> <id>stop</id> <phase>post-integration-test</phase> <goals> <goal>stop</goal> </goals> </execution> </executions> </plugin>配置maven-failsafe-plugin<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-failsafe-plugin</artifactId> <executions> <execution> <id>integration-test</id> <goals> <goal>integration-test</goal> </goals> </execution> <execution> <id>verify</id> <goals> <goal>verify</goal> </goals> </execution> </executions> <configuration> <!– 我们被测的是一个Spring Boot项目,因此可以通过System Properties把MySQL container的相关信息传递给程序 详见文档:https://docs.spring.io/spring-boot/docs/1.5.4.RELEASE/reference/html/boot-features-external-config.html –> <systemPropertyVariables> <spring.datasource.url>jdbc:mysql://localhost:${docker-mysql.port}/test</spring.datasource.url> <spring.datasource.username>foo</spring.datasource.username> <spring.datasource.password>bar</spring.datasource.password> </systemPropertyVariables> </configuration></plugin>执行三种常见用法:mvn clean integration-test,会启动docker container、运行集成测试。这个很有用,如果集成测试失败,那么你还可以连接到MySQL数据库查看情况。mvn clean verify,会执行mvn integration-test、删除docker container。mvn clean install,会执mvn verify,并将包安装到本地maven 仓库。下面是mvn clean verify的日志:…[INFO] — docker-maven-plugin:0.28.0:start (start) @ spring-test-examples-rdbs-docker —[INFO] DOCKER> [mysql:8]: Start container f683aadfe8ba[INFO] DOCKER> Pattern ‘MySQL init process done. Ready for start up.’ matched for container f683aadfe8ba[INFO] DOCKER> [mysql:8]: Waited on log out ‘MySQL init process done. Ready for start up.’ 13717 ms[INFO][INFO] — maven-failsafe-plugin:2.22.1:integration-test (integration-test) @ spring-test-examples-rdbs-docker —[INFO][INFO] ——————————————————-[INFO] T E S T S[INFO] ——————————————————-…[INFO][INFO] Results:[INFO][INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0[INFO][INFO][INFO] — docker-maven-plugin:0.28.0:stop (stop) @ spring-test-examples-rdbs-docker —[INFO] DOCKER> [mysql:8]: Stop and removed container f683aadfe8ba after 0 ms[INFO][INFO] — maven-failsafe-plugin:2.22.1:verify (verify) @ spring-test-examples-rdbs-docker —[INFO] ————————————————————————[INFO] BUILD SUCCESS[INFO] ————————————————————————…可以看到fabric8 dmp在集成测试前后start和stop容器的相关日志,且测试成功。如何找到MySQL的端口开在哪一个呢?运行docker ps查看端口(注意下面的0.0.0.0:32798->3306/tcp):CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMESa1f4b51d7c75 mysql:8 … … Up 19… 33060/tcp, 0.0.0.0:32798->3306/tcp mysql-1参考文档Fabric8 dmpSpring boot - Externalized Configuration ...

December 28, 2018 · 2 min · jiezi

spring cloud 访问controller报(type=Not Found, status=404)错误

最近在学习微服务架构过程中,项目启动成功,但在浏览器访问controller对应的requestMapping的时候一直报There was an unexpected error (type=Not Found, status=404).错误。解决过程如下:1. 检查了访问地址路径http://localhost:8089/client1/hello?name=w,对应的application.yml里面配置的端口号和上下文路径没有问题。 controller里面的mapping也没有问题。 2. 百度一下,网上说springboot+thymeleaf的项目需要添加thymeleaf依赖,而此项目只是 springboot。 3. 检查包路径。springboot扫描文件@ComponentScan(basePackages = {"com.spring.*"}), 由此springboot启动类的包必须是项目下的父路径,其他类的包路径必须是其子路径。这里ApplicationClient.java是项目启动类,路径正确,而controller的路径不在ApplicationClient.java的包路径下,扫描不到。问题就在这里啦,修改controller的路径为com.xxx.helloworld.eureka.client.controllers,最后访问成功啦。

December 19, 2018 · 1 min · jiezi

spring cloud gateway 之限流篇

转载请标明出处: https://www.fangzhipeng.com本文出自方志朋的博客在高并发的系统中,往往需要在系统中做限流,一方面是为了防止大量的请求使服务器过载,导致服务不可用,另一方面是为了防止网络攻击。常见的限流方式,比如Hystrix适用线程池隔离,超过线程池的负载,走熔断的逻辑。在一般应用服务器中,比如tomcat容器也是通过限制它的线程数来控制并发的;也有通过时间窗口的平均速度来控制流量。常见的限流纬度有比如通过Ip来限流、通过uri来限流、通过用户访问频次来限流。一般限流都是在网关这一层做,比如Nginx、Openresty、kong、zuul、Spring Cloud Gateway等;也可以在应用层通过Aop这种方式去做限流。本文详细探讨在 Spring Cloud Gateway 中如何实现限流。常见的限流算法计数器算法计数器算法采用计数器实现限流有点简单粗暴,一般我们会限制一秒钟的能够通过的请求数,比如限流qps为100,算法的实现思路就是从第一个请求进来开始计时,在接下去的1s内,每来一个请求,就把计数加1,如果累加的数字达到了100,那么后续的请求就会被全部拒绝。等到1s结束后,把计数恢复成0,重新开始计数。具体的实现可以是这样的:对于每次服务调用,可以通过AtomicLong#incrementAndGet()方法来给计数器加1并返回最新值,通过这个最新值和阈值进行比较。这种实现方式,相信大家都知道有一个弊端:如果我在单位时间1s内的前10ms,已经通过了100个请求,那后面的990ms,只能眼巴巴的把请求拒绝,我们把这种现象称为“突刺现象”漏桶算法漏桶算法为了消除"突刺现象",可以采用漏桶算法实现限流,漏桶算法这个名字就很形象,算法内部有一个容器,类似生活用到的漏斗,当请求进来时,相当于水倒入漏斗,然后从下端小口慢慢匀速的流出。不管上面流量多大,下面流出的速度始终保持不变。不管服务调用方多么不稳定,通过漏桶算法进行限流,每10毫秒处理一次请求。因为处理的速度是固定的,请求进来的速度是未知的,可能突然进来很多请求,没来得及处理的请求就先放在桶里,既然是个桶,肯定是有容量上限,如果桶满了,那么新进来的请求就丢弃。在算法实现方面,可以准备一个队列,用来保存请求,另外通过一个线程池(ScheduledExecutorService)来定期从队列中获取请求并执行,可以一次性获取多个并发执行。这种算法,在使用过后也存在弊端:无法应对短时间的突发流量。令牌桶算法从某种意义上讲,令牌桶算法是对漏桶算法的一种改进,桶算法能够限制请求调用的速率,而令牌桶算法能够在限制调用的平均速率的同时还允许一定程度的突发调用。在令牌桶算法中,存在一个桶,用来存放固定数量的令牌。算法中存在一种机制,以一定的速率往桶中放令牌。每次请求调用需要先获取令牌,只有拿到令牌,才有机会继续执行,否则选择选择等待可用的令牌、或者直接拒绝。放令牌这个动作是持续不断的进行,如果桶中令牌数达到上限,就丢弃令牌,所以就存在这种情况,桶中一直有大量的可用令牌,这时进来的请求就可以直接拿到令牌执行,比如设置qps为100,那么限流器初始化完成一秒后,桶中就已经有100个令牌了,这时服务还没完全启动好,等启动完成对外提供服务时,该限流器可以抵挡瞬时的100个请求。所以,只有桶中没有令牌时,请求才会进行等待,最后相当于以一定的速率执行。实现思路:可以准备一个队列,用来保存令牌,另外通过一个线程池定期生成令牌放到队列中,每来一个请求,就从队列中获取一个令牌,并继续执行。Spring Cloud Gateway限流在Spring Cloud Gateway中,有Filter过滤器,因此可以在“pre”类型的Filter中自行实现上述三种过滤器。但是限流作为网关最基本的功能,Spring Cloud Gateway官方就提供了RequestRateLimiterGatewayFilterFactory这个类,适用Redis和lua脚本实现了令牌桶的方式。具体实现逻辑在RequestRateLimiterGatewayFilterFactory类中,lua脚本在如下图所示的文件夹中:具体源码不打算在这里讲述,读者可以自行查看,代码量较少,先以案例的形式来讲解如何在Spring Cloud Gateway中使用内置的限流过滤器工厂来实现限流。首先在工程的pom文件中引入gateway的起步依赖和redis的reactive依赖,代码如下: <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-gateway</artifactId></dependency><dependency> <groupId>org.springframework.boot</groupId> <artifatId>spring-boot-starter-data-redis-reactive</artifactId></dependency>在配置文件中做以下的配置:server: port: 8081spring: cloud: gateway: routes: - id: limit_route uri: http://httpbin.org:80/get predicates: - After=2017-01-20T17:42:47.789-07:00[America/Denver] filters: - name: RequestRateLimiter args: key-resolver: ‘#{@hostAddrKeyResolver}’ redis-rate-limiter.replenishRate: 1 redis-rate-limiter.burstCapacity: 3 application: name: gateway-limiter redis: host: localhost port: 6379 database: 0在上面的配置文件,指定程序的端口为8081,配置了 redis的信息,并配置了RequestRateLimiter的限流过滤器,该过滤器需要配置三个参数:burstCapacity,令牌桶总容量。replenishRate,令牌桶每秒填充平均速率。key-resolver,用于限流的键的解析器的 Bean 对象的名字。它使用 SpEL 表达式根据#{@beanName}从 Spring 容器中获取 Bean 对象。KeyResolver需要实现resolve方法,比如根据Hostname进行限流,则需要用hostAddress去判断。实现完KeyResolver之后,需要将这个类的Bean注册到Ioc容器中。public class HostAddrKeyResolver implements KeyResolver { @Override public Mono<String> resolve(ServerWebExchange exchange) { return Mono.just(exchange.getRequest().getRemoteAddress().getAddress().getHostAddress()); }} @Bean public HostAddrKeyResolver hostAddrKeyResolver() { return new HostAddrKeyResolver(); }可以根据uri去限流,这时KeyResolver代码如下:public class UriKeyResolver implements KeyResolver { @Override public Mono<String> resolve(ServerWebExchange exchange) { return Mono.just(exchange.getRequest().getURI().getPath()); }} @Bean public UriKeyResolver uriKeyResolver() { return new UriKeyResolver(); } 也可以以用户的维度去限流: @Bean KeyResolver userKeyResolver() { return exchange -> Mono.just(exchange.getRequest().getQueryParams().getFirst(“user”)); }用jmeter进行压测,配置10thread去循环请求lcoalhost:8081,循环间隔1s。从压测的结果上看到有部分请求通过,由部分请求失败。通过redis客户端去查看redis中存在的key。如下:可见,RequestRateLimiter是使用Redis来进行限流的,并在redis中存储了2个key。关注这两个key含义可以看lua源代码。源码下载https://github.com/forezp/Spr…参考资料http://cloud.spring.io/spring…https://windmt.com/2018/05/09...http://www.spring4all.com/art…扫一扫,支持下作者吧: ...

December 18, 2018 · 1 min · jiezi

Spring Boot+SQL/JPA实战悲观锁和乐观锁

最近在公司的业务上遇到了并发的问题,并且还是很常见的并发问题,算是低级的失误了。由于公司业务相对比较复杂且不适合公开,在此用一个很常见的业务来还原一下场景,同时介绍悲观锁和乐观锁是如何解决这类并发问题的。公司业务就是最常见的“订单+账户”问题,在解决完公司问题后,转头一想,我的博客项目Fame中也有同样的问题(虽然访问量根本完全不需要考虑并发问题…),那我就拿这个来举例好了。业务还原首先环境是:Spring Boot 2.1.0 + data-jpa + mysql + lombok数据库设计对于一个有评论功能的博客系统来说,通常会有两个表:1.文章表 2.评论表。其中文章表除了保存一些文章信息等,还有个字段保存评论数量。我们设计一个最精简的表结构来还原该业务场景。article 文章表字段类型备注idINT自增主键idtitleVARCHAR文章标题comment_countINT文章的评论数量comment 评论表字段类型备注idINT自增主键idarticle_idINT评论的文章idcontentVARCHAR评论内容当一个用户评论的时候,1. 根据文章id获取到文章 2. 插入一条评论记录 3. 该文章的评论数增加并保存代码实现首先在maven中引入对应的依赖<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.1.0.RELEASE</version> <relativePath/> <!– lookup parent from repository –></parent><dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency></dependencies>然后编写对应数据库的实体类@Data@Entitypublic class Article { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String title; private Long commentCount;}@Data@Entitypublic class Comment { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private Long articleId; private String content;}接着创建这两个实体类对应的Repository,由于spring-jpa-data的CrudRepository已经帮我们实现了最常见的CRUD操作,所以我们的Repository只需要继承CrudRepository接口其他啥都不用做。public interface ArticleRepository extends CrudRepository<Article, Long> {}public interface CommentRepository extends CrudRepository<Comment, Long> {}接着我们就简单的实现一下Controller接口和Service实现类。@Slf4j@RestControllerpublic class CommentController { @Autowired private CommentService commentService; @PostMapping(“comment”) public String comment(Long articleId, String content) { try { commentService.postComment(articleId, content); } catch (Exception e) { log.error("{}", e); return “error: " + e.getMessage(); } return “success”; }}@Slf4j@Servicepublic class CommentService { @Autowired private ArticleRepository articleRepository; @Autowired private CommentRepository commentRepository; public void postComment(Long articleId, String content) { Optional<Article> articleOptional = articleRepository.findById(articleId); if (!articleOptional.isPresent()) { throw new RuntimeException(“没有对应的文章”); } Article article = articleOptional.get(); Comment comment = new Comment(); comment.setArticleId(articleId); comment.setContent(content); commentRepository.save(comment); article.setCommentCount(article.getCommentCount() + 1); articleRepository.save(article); }}并发问题分析从刚才的代码实现里可以看出这个简单的评论功能的流程,当用户发起评论的请求时,从数据库找出对应的文章的实体类Article,然后根据文章信息生成对应的评论实体类Comment,并且插入到数据库中,接着增加该文章的评论数量,再把修改后的文章更新到数据库中,整个流程如下流程图。在这个流程中有个问题,当有多个用户同时并发评论时,他们同时进入步骤1中拿到Article,然后插入对应的Comment,最后在步骤3中更新评论数量保存到数据库。只是由于他们是同时在步骤1拿到的Article,所以他们的Article.commentCount的值相同,那么在步骤3中保存的Article.commentCount+1也相同,那么原来应该+3的评论数量,只加了1。我们用测试用例代码试一下@RunWith(SpringRunner.class)@SpringBootTest(classes = LockAndTransactionApplication.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)public class CommentControllerTests { @Autowired private TestRestTemplate testRestTemplate; @Test public void concurrentComment() { String url = “http://localhost:9090/comment”; for (int i = 0; i < 100; i++) { int finalI = i; new Thread(() -> { MultiValueMap<String, String> params = new LinkedMultiValueMap<>(); params.add(“articleId”, “1”); params.add(“content”, “测试内容” + finalI); String result = testRestTemplate.postForObject(url, params, String.class); }).start(); } }}这里我们开了100个线程,同时发送评论请求,对应的文章id为1。在发送请求前,数据库数据为select * from articleselect count() comment_count from comment发送请求后,数据库数据为select * from articleselect count() comment_count from comment明显的看到在article表里的comment_count的值不是100,这个值不一定是我图里的14,但是必然是不大于100的,而comment表的数量肯定等于100。这就展示了在文章开头里提到的并发问题,这种问题其实十分的常见,只要有类似上面这样评论功能的流程的系统,都要小心避免出现这种问题。下面就用实例展示展示如何通过悲观锁和乐观锁防止出现并发数据问题,同时给出SQL方案和JPA自带方案,SQL方案可以通用“任何系统”,甚至不限语言,而JPA方案十分快捷,如果你恰好用的也是JPA,那就可以简单的使用上乐观锁或悲观锁。最后也会根据业务比较一下乐观锁和悲观锁的一些区别悲观锁解决并发问题悲观锁顾名思义就是悲观的认为自己操作的数据都会被其他线程操作,所以就必须自己独占这个数据,可以理解为”独占锁“。在java中synchronized和ReentrantLock等锁就是悲观锁,数据库中表锁、行锁、读写锁等也是悲观锁。利用SQL解决并发问题行锁就是操作数据的时候把这一行数据锁住,其他线程想要读写必须等待,但同一个表的其他数据还是能被其他线程操作的。只要在需要查询的sql后面加上for update,就能锁住查询的行,特别要注意查询条件必须要是索引列,如果不是索引就会变成表锁,把整个表都锁住。现在在原有的代码的基础上修改一下,先在ArticleRepository增加一个手动写sql查询方法。public interface ArticleRepository extends CrudRepository<Article, Long> { @Query(value = “select * from article a where a.id = :id for update”, nativeQuery = true) Optional<Article> findArticleForUpdate(Long id);}然后把CommentService中使用的查询方法由原来的findById改为我们自定义的方法public class CommentService { … public void postComment(Long articleId, String content) { // Optional<Article> articleOptional = articleRepository.findById(articleId); Optional<Article> articleOptional = articleRepository.findArticleForUpdate(articleId); … }}这样我们查出来的Article,在我们没有将其提交事务之前,其他线程是不能获取修改的,保证了同时只有一个线程能操作对应数据。现在再用测试用例测一下,article.comment_count的值必定是100。利用JPA自带行锁解决并发问题对于刚才提到的在sql后面增加for update,JPA有提供一个更优雅的方式,就是@Lock注解,这个注解的参数可以传入想要的锁级别。现在在ArticleRepository中增加JPA的锁方法,其中LockModeType.PESSIMISTIC_WRITE参数就是行锁。public interface ArticleRepository extends CrudRepository<Article, Long> { … @Lock(value = LockModeType.PESSIMISTIC_WRITE) @Query(“select a from Article a where a.id = :id”) Optional<Article> findArticleWithPessimisticLock(Long id);}同样的只要在CommentService里把查询方法改为findArticleWithPessimisticLock(),再测试用例测一下,肯定不会有并发问题。而且这时看一下控制台打印信息,发现实际上查询的sql还是加了for update,只不过是JPA帮我们加了而已。乐观锁解决并发问题乐观锁顾名思义就是特别乐观,认为自己拿到的资源不会被其他线程操作所以不上锁,只是在插入数据库的时候再判断一下数据有没有被修改。所以悲观锁是限制其他线程,而乐观锁是限制自己,虽然他的名字有锁,但是实际上不算上锁,只是在最后操作的时候再判断具体怎么操作。乐观锁通常为版本号机制或者CAS算法利用SQL实现版本号解决并发问题版本号机制就是在数据库中加一个字段当作版本号,比如我们加个字段version。那么这时候拿到Article的时候就会带一个版本号,比如拿到的版本是1,然后你对这个Article一通操作,操作完之后要插入到数据库了。发现哎呀,怎么数据库里的Article版本是2,和我手里的版本不一样啊,说明我手里的Article不是最新的了,那么就不能放到数据库了。这样就避免了并发时数据冲突的问题。所以我们现在给article表加一个字段versionarticle 文章表字段类型备注versionINT DEFAULT 0版本号然后对应的实体类也增加version字段@Data@Entitypublic class Article { … private Long version;}接着在ArticleRepository增加更新的方法,注意这里是更新方法,和悲观锁时增加查询方法不同。public interface ArticleRepository extends CrudRepository<Article, Long> { @Modifying @Query(value = “update article set comment_count = :commentCount, version = version + 1 where id = :id and version = :version”, nativeQuery = true) int updateArticleWithVersion(Long id, Long commentCount, Long version);}可以看到update的where有一个判断version的条件,并且会set version = version + 1。这就保证了只有当数据库里的版本号和要更新的实体类的版本号相同的时候才会更新数据。接着在CommentService里稍微修改一下代码。// CommentServicepublic void postComment(Long articleId, String content) { Optional<Article> articleOptional = articleRepository.findById(articleId); … int count = articleRepository.updateArticleWithVersion(article.getId(), article.getCommentCount() + 1, article.getVersion()); if (count == 0) { throw new RuntimeException(“服务器繁忙,更新数据失败”); } // articleRepository.save(article);}首先对于Article的查询方法只需要普通的findById()方法就行不用上任何锁。然后更新Article的时候改用新加的updateArticleWithVersion()方法。可以看到这个方法有个返回值,这个返回值代表更新了的数据库行数,如果值为0的时候表示没有符合条件可以更新的行。这之后就可以由我们自己决定怎么处理了,这里是直接回滚,spring就会帮我们回滚之前的数据操作,把这次的所有操作都取消以保证数据的一致性。现在再用测试用例测一下select * from articleselect count() comment_count from comment现在看到Article里的comment_count和Comment的数量都不是100了,但是这两个的值必定是一样的了。因为刚才我们处理的时候假如Article表的数据发生了冲突,那么就不会更新到数据库里,这时抛出异常使其事务回滚,这样就能保证没有更新Article的时候Comment也不会插入,就解决了数据不统一的问题。这种直接回滚的处理方式用户体验比较差,通常来说如果判断Article更新条数为0时,会尝试重新从数据库里查询信息并重新修改,再次尝试更新数据,如果不行就再查询,直到能够更新为止。当然也不会是无线的循环这样的操作,会设置一个上线,比如循环3次查询修改更新都不行,这时候才会抛出异常。利用JPA实现版本现解决并发问题JPA对悲观锁有实现方式,乐观锁自然也是有的,现在就用JPA自带的方法实现乐观锁。首先在Article实体类的version字段上加上@Version注解,我们进注解看一下源码的注释,可以看到有部分写到:The following types are supported for version properties: int, Integer, short, Short, long, Long, java.sql.Timestamp.注释里面说版本号的类型支持int, short, long三种基本数据类型和他们的包装类以及Timestamp,我们现在用的是Long类型。@Data@Entitypublic class Article { … @Version private Long version;}接着只需要在CommentService里的评论流程修改回我们最开头的“会触发并发问题”的业务代码就行了。说明JPA的这种乐观锁实现方式是非侵入式的。// CommentServicepublic void postComment(Long articleId, String content) { Optional<Article> articleOptional = articleRepository.findById(articleId); … article.setCommentCount(article.getCommentCount() + 1); articleRepository.save(article);}和前面同样的,用测试用例测试一下能否防止并发问题的出现。select * from articleselect count() comment_count from comment同样的Article里的comment_count和Comment的数量也不是100,但是这两个数值肯定是一样的。看一下IDEA的控制台会发现系统抛出了ObjectOptimisticLockingFailureException的异常。这和刚才我们自己实现乐观锁类似,如果没有成功更新数据则抛出异常回滚保证数据的一致性。如果想要实现重试流程可以捕获ObjectOptimisticLockingFailureException这个异常,通常会利用AOP+自定义注解来实现一个全局通用的重试机制,这里就是要根据具体的业务情况来拓展了,想要了解的可以自行搜索一下方案。悲观锁和乐观锁比较悲观锁适合写多读少的场景。因为在使用的时候该线程会独占这个资源,在本文的例子来说就是某个id的文章,如果有大量的评论操作的时候,就适合用悲观锁,否则用户只是浏览文章而没什么评论的话,用悲观锁就会经常加锁,增加了加锁解锁的资源消耗。乐观锁适合写少读多的场景。由于乐观锁在发生冲突的时候会回滚或者重试,如果写的请求量很大的话,就经常发生冲突,经常的回滚和重试,这样对系统资源消耗也是非常大。所以悲观锁和乐观锁没有绝对的好坏,必须结合具体的业务情况来决定使用哪一种方式。另外在阿里巴巴开发手册里也有提到:如果每次访问冲突概率小于 20%,推荐使用乐观锁,否则使用悲观锁。乐观锁的重试次数不得小于 3 次。阿里巴巴建议以冲突概率20%这个数值作为分界线来决定使用乐观锁和悲观锁,虽然说这个数值不是绝对的,但是作为阿里巴巴各个大佬总结出来的也是一个很好的参考。 ...

December 18, 2018 · 3 min · jiezi

InChat一版,仅仅两个接口实现自己的IM系统(可兼容)

InChat 一个IM通讯框架一个轻量级、高效率的支持多端(应用与硬件Iot)的异步网络应用通讯框架。(核心底层Netty)Github:InChat版本目标:完成基本的消息通讯(仅支持文本消息),离线消息存储,历史消息查询,一对一聊天、自我聊天、群聊等。你可以使用InChat,快速搭建一个基于SpringBoot的IM项目,而且没有任何硬性要求,你完全可以兼容自己原有的项目。v1.0.0版本使用说明关于InChat的Maven依赖fastjson 》 1.2.53gson 》 2.8.5netty 》 4.1.32.Finalcommons-lang 》 3.5aspectj 》 1.9.2lombok 》 1.18.4spring-boot 》 2.0.2.RELEASEspring-boot-starter-websocket关于一版依旧使用SpringBoot的环境,同时为应用注入了web环境,引入InChat依赖包后,对于SpringBoot相关的web可以无需引入,同时请注意相关版本的兼容性。引入InChat默认可以自动运行web环境。创建项目创建一个空的Maven项目,并引入InChatMaven包,(注意,请不要使用与本项目相同的包目录)。可能你只需要这样的Maven依赖即可<dependencies> <dependency> <groupId>com.github.UncleCatMySelf</groupId> <artifactId>InChat</artifactId> <version>1.0-alpha</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency></dependencies>注入InChat的项目到自身项目中你可能需要在你的项目上进行报扫描@SpringBootApplication@ComponentScan({“com.inchat”}) //你的demo包目录@ComponentScan({“com.github.unclecatmyself”}) //InChat的包目录 –请将InChat的放到最下面public class DemoApplication { public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); }}对接InChat的接口与实现这次你仅需写两个实现接口即可啦!!!@Servicepublic class ToDataBaseServiceImpl implements InChatToDataBaseService{ @Override public Boolean writeMapToDB(Map<String, Object> maps) { //异步写入数据库 System.out.println(maps.toString()); return true; }}这个接口是每个人通讯的信息,InChat自带实现了异步的数据外抛得接口InChatToDataBaseService,目前一版只有一个方法,就是上面得writeMapToDB,你仅需要map的内容转为对应的对象(一版还没提供对应的转换类,下一版对提供),并将数据存入自己喜欢的数据库中。如果数据并发大,也可以先放到MQ中,再写入数据库。@Servicepublic class verifyServiceImpl implements InChatVerifyService { @Override public boolean verifyToken(String token) { //登录校验 return true; } @Override public JSONArray getArrayByGroupId(String groupId) { //根据群聊id获取对应的群聊人员ID JSONArray jsonArray = JSONArray.parseArray("["1111","2222","3333"]"); return jsonArray; }}这个接口是InChat的校验层实现,对于Token的校验就是,verifyToken,websocket链接的时候,你将在初次做登录校验,你可以将从InChat拿到的websocket传过来的Token,你可以与自己的用户登录的token做校验,返回true,则用户成功链接InChat。关于getArrayByGroupId,目前是否应该放在这个接口中还有待确定,不过目前一版暂时这样,你可以去数据库中查询对应的群聊id所对应的人员ID(或Token),并返回对应的JSONArray即可啦。自定义配置InChat参数这个你可以直接在application中按照自己的意思配置,不过你最好先了解netty启动项目接着启动项目即可啦当你看到这个日志就标志着Inchat搭建成功了!!!2018-12-14 10:29:09.269 INFO 4920 — [ BOSS_1] c.g.u.bootstrap.NettyBootstrapServer : 服务端启动成功【192.168.1.121:8090】关于前端这里你可以来到InChat的Front-End-Testing文档夹中的chat.html。你可以直接使用,你进需要修改对应的对接IP即可。关于前端的js暂时还是模板关于登录你会看到chat.html中的登录按钮对应的jsfunction send(value) { if (!window.WebSocket) { return; } if (socket.readyState == WebSocket.OPEN) { var message = { type: “login”, //与InChat对应的 不可修改 token: “1111” } socket.send(JSON.stringify(message)); } else { alert(“连接没有开启.”); }}本demo,默认登录的Token是“1111”,关于用户校验则直接返回true即可。登录成功,返回以下内容。(不需要显示给用户看){“success”:“true”,“type”:“login”}InChat不会有登录记录发送给自己你会看到chat.html中的登录按钮对应的jsfunction sendToMe(value) { if (!window.WebSocket) { return; } if (socket.readyState == WebSocket.OPEN) { var message = { type: “sendMe”, //与InChat对应的 不可修改 value: value, //发送的内容 token: “1111” //发送用户的token } socket.send(JSON.stringify(message)); } else { alert(“连接没有开启.”); }}发送成功,InChat返回内容.(你仅需将value显示到前端即可){“type”:“sendMe”,“value”:“发送给自己的内容”}InChat消息记录,你将在异步消息中接受到InChat传递给你的用户通讯消息,你可以进行对应的入库操作{“time”:“2018-12-14 10:56:24”,“type”:“sendMe”,“value”:“发送给自己的内容”,“token”:“1111”}发送给某人你会看到chat.html中的登录按钮对应的jsfunction sendToOne(value) { if (!window.WebSocket) { return; } if (socket.readyState == WebSocket.OPEN) { var message = { type : “sendTo”, //与InChat对应的 不可修改 token : “1111”, //发送用户Token value: value, //发送内容 one: “2222”, //接受用户Token(唯一标识) } socket.send(JSON.stringify(message)); } else { alert(“连接没有开启.”); }}发送成功,接受的用户是否登录,你都能接受到返回信息。(value应用于自己界面展示){“one”:“2222”,“type”:“sendTo”,“value”:“发送给朋友的内容”}但是用户那边就不一样了。登录正常在线。{“from”:“1111”,“type”:“sendTo”,“value”:“发送给朋友的内容”}离线接受不到信息InChat异步消息推送,你可以看到两种在线: {“one”:“2222”,“time”:“2018-12-14 11:01:36”,“type”:“sendTo”,“value”:“发送给朋友的内容”,“token”:“1111”}离线: {“one”:“2222”,“time”:“2018-12-14 10:59:04”,“on_online”:“2222”,“type”:“sendTo”,“value”:“发送给朋友的内容”,“token”:“1111”}如果出现用户发送给用户的状态是离线的,则会在消息多出on_online的字段,该字段的内容就是离线用户的Token,你可以针对性的数据入库,并在用户上线的时候,读写信息的时候,有一个未读消息的状态。发送群聊你会看到chat.html中的登录按钮对应的jsfunction sendGroup(value) { if (!window.WebSocket) { return; } if (socket.readyState == WebSocket.OPEN) { var message = { type: “sendGroup”, //与InChat对应的 不可修改 groupId: “2”, //群聊ID token: “1111”, //发送用户的Token value: value //发送的消息 } socket.send(JSON.stringify(message)); } else { alert(“连接没有开启.”); }}发送成功,本人将接受到消息{“groupId”:“2”,“from”:“1111”,“type”:“sendGroup”,“value”:“大家明天一起去唱K吧”}群组中有些人在线接受、离线不接受在线:{“groupId”:“2”,“from”:“1111”,“type”:“sendGroup”,“value”:“大家明天一起去唱K吧”}InChat异步消息入库,群组只会异步给你一个消息,你可以看到on_online中,3333用户是没有接受到信息的,所以你可以在他上线发送未读消息。{“groupId”:“2”,“time”:“2018-12-14 11:09:17”,“on_online”:[“3333”],“type”:“sendGroup”,“value”:“大家明天一起去唱K吧”,“token”:“1111”}关于数据库设计当前一版不会固定大家的数据库设计,大家可以自己自由设计,同时搭上自己的项目,构建一个附带IM的自项目。前端效果发送人接收人 ...

December 14, 2018 · 2 min · jiezi

超详细,新手都能看懂 !使用SpringBoot+Dubbo 搭建一个简单的分布式服务

Github 地址:https://github.com/Snailclimb/springboot-integration-examples ,欢迎各位 Star。目录:使用 SpringBoot+Dubbo 搭建一个简单分布式服务实战之前,先来看几个重要的概念什么是分布式?什么是 Duboo?Dubbo 架构什么是 RPC?为什么要用 Dubbo?开始实战 1 :zookeeper 环境安装搭建1. 下载2. 解压3. 进入zookeeper目录,创建data文件夹。4. 进入/zookeeper/conf目录下,复制zoo_sample.cfg,命名为zoo.cfg5. 修改配置文件6. 启动测试开始实战 2 :实现服务接口 dubbo-interface1. dubbo-interface 项目创建2. 创建接口类3. 将项目打成 jar 包供其他项目使用开始实战 3 :实现服务提供者 dubbo-provider1. dubbo-provider 项目创建2. pom 文件引入相关依赖3. 在 application.properties 配置文件中配置 dubbo 相关信息4. 实现接口5. 服务提供者启动类编写开始实战 4 :实现服务消费者 dubbo-consumer4. 编写一个简单 Controller 调用远程服务5. 服务消费者启动类编写6. 测试效果使用 SpringBoot+Dubbo 搭建一个简单分布式服务实战之前,先来看几个重要的概念开始实战之前,我们先来简单的了解一下这样几个概念:Dubbo、RPC、分布式、由于本文的目的是带大家使用SpringBoot+Dubbo 搭建一个简单的分布式服务,所以这些概念我只会简单给大家普及一下,不会做深入探究。什么是分布式?分布式或者说 SOA 分布式重要的就是面向服务,说简单的分布式就是我们把整个系统拆分成不同的服务然后将这些服务放在不同的服务器上减轻单体服务的压力提高并发量和性能。比如电商系统可以简单地拆分成订单系统、商品系统、登录系统等等。我们可以使用 Dubbo作为分布式系统的桥梁,那么什么是 Dubbo 呢?什么是 Duboo?Apache Dubbo (incubating) |db| 是一款高性能、轻量级的开源Java RPC 框架,它提供了三大核心能力:面向接口的远程方法调用,智能容错和负载均衡,以及服务自动注册和发现。简单来说 Dubbo 是一个分布式服务框架,致力于提供高性能和透明化的RPC远程服务调用方案,以及SOA服务治理方案。Dubbo 目前已经有接近 23k 的 Star ,Dubbo的Github 地址:https://github.com/apache/inc…。另外,在开源中国举行的2018年度最受欢迎中国开源软件这个活动的评选中,Dubbo 更是凭借其超高人气仅次于 vue.js 和 ECharts 获得第三名的好成绩。Dubbo 是由阿里开源,后来加入了 Apache 。正式由于 Dubbo 的出现,才使得越来越多的公司开始使用以及接受分布式架构。下面我们简单地来看一下 Dubbo 的架构,加深对 Dubbo 的理解。Dubbo 架构下面我们再来看看 Dubbo 的架构,我们后面会使用 zookeeper 作为注册中心,这也是 Dubbo 官方推荐的一种方式。上述节点简单说明:Provider 暴露服务的服务提供方Consumer 调用远程服务的服务消费方Registry 服务注册与发现的注册中心Monitor 统计服务的调用次数和调用时间的监控中心Container 服务运行容器调用关系说明:服务容器负责启动,加载,运行服务提供者。服务提供者在启动时,向注册中心注册自己提供的服务。服务消费者在启动时,向注册中心订阅自己所需的服务。注册中心返回服务提供者地址列表给消费者,如果有变更,注册中心将基于长连接推送变更数据给消费者。服务消费者,从提供者地址列表中,基于软负载均衡算法,选一台提供者进行调用,如果调用失败,再选另一台调用。服务消费者和提供者,在内存中累计调用次数和调用时间,定时每分钟发送一次统计数据到监控中心。我们在讲 Dubbo 的时候提到了 Dubbo 实际上是一款 RPC 框架,那么RPC 究竟是什么呢?相信看了下面我对 RPC 的介绍你就明白了!什么是 RPC?RPC(Remote Procedure Call)—远程过程调用,它是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议。比如两个不同的服务A,B部署在两台不同的机器上,那么服务 A 如果想要调用服务 B 中的某个方法该怎么办呢?使用 HTTP请求 当然可以,但是可能会比较慢而且一些优化做的并不好。 RPC 的出现就是为了解决这个问题。为什么要用 Dubbo?如果你要开发分布式程序,你也可以直接基于 HTTP 接口进行通信,但是为什么要用 Dubbo呢?我觉得主要可以从 Dubbo 提供的下面四点特性来说为什么要用 Dubbo:负载均衡——同一个服务部署在不同的机器时该调用那一台机器上的服务服务调用链路生成——服务之间互相是如何调用的服务访问压力以及时长统计——当前系统的压力主要在哪里,如何来扩容和优化服务降级——某个服务挂掉之后调用备用服务开始实战 1 :zookeeper 环境安装搭建我使用的是 CentOS 7.4 阿里云服务器,注意:如果你也同样阿里云服务器必须配置一个安全组,不然你的应用程序会无法访问你的 zookeeper 服务器,这一点我在后面也提到了。1. 下载通过 http://mirror.bit.edu.cn/apache/zookeeper/ 这个链接下载,然后上传到Linux上。(可以说那个 Xhell 附带的文件传输功能)或者直接在Linux中使用 wget https://archive.apache.org/dist/zookeeper/zookeeper-3.4.12/zookeeper-3.4.12.tar.gz 命令下载(版本号 3.4.12 是我写这篇文章的时候最新的稳定版本,各位可以根据实际情况修改)2. 解压tar -zxvf zookeeper-3.4.12-alpha.tar.gz解压完毕之后修改一下解压之后所得的文件夹名mv zookeeper-3.4.12 zookeeper删除 zookeeper 安装包rm -rf zookeeper-3.4.12.tar.gz3. 进入zookeeper目录,创建data文件夹。mkdir data进入 data 文件夹 然后执行pwd命令,复制所得的当前目录位置(就是我用红色圈出来的文字)4. 进入/zookeeper/conf目录下,复制zoo_sample.cfg,命名为zoo.cfgcp zoo_sample.cfg zoo.cfg5. 修改配置文件使用 vim zoo.cfg 命令修改配置文件vim 文件——>进入文件—–>命令模式——>按i进入编辑模式—–>编辑文件 ——->按Esc进入底行模式—–>输入:wq/q! (输入wq代表写入内容并退出,即保存;输入q!代表强制退出不保存。)修改配置文件中的 data 属性:dataDir=/usr/local/zookeeper/data6. 启动测试进入 /zookeeper/bin 目录然后执行下面的命令./zkServer.sh start执行 ./zkServer.sh status 查看当前 zookeeper 状态。或者运行 netstat -lntup 命令查看网络状态,可以看到 zookeeper 的端口号 2181 已经被占用注意没有关闭防火墙可能出现的问题!!!如果你使用的阿里云服务器注意配置相关安全组:进入本实例安全组页面选择配置规则选择添加安全组规则,然后按照下图配置在开始实战之前提个建议:尽量新建一个文件夹,然后后面将接口项目、服务提供者以及服务消费者都放在这个文件夹。开始实战 2 :实现服务接口 dubbo-interface主要分为下面几步:创建 Maven 项目;创建接口类将项目打成 jar 包供其他项目使用项目结构:dubbo-interface 后面被打成 jar 包,它的作用只是提供接口。1. dubbo-interface 项目创建File->New->Module… ,然后选择 Maven类型的项目,其他的按照提示一步一步走就好。2. 创建接口类package top.snailclimb.service;public interface HelloService { public String sayHello(String name);}3. 将项目打成 jar 包供其他项目使用点击右边的 Maven Projects 然后选择 install ,这样 jar 宝就打好了。开始实战 3 :实现服务提供者 dubbo-provider主要分为下面几步:创建 springboot 项目;加入 dubbo 、zookeeper以及接口的相关依赖 jar 包;在 application.properties 配置文件中配置 dubbo 相关信息;实现接口类;服务提供者启动类编写项目结构:1. dubbo-provider 项目创建创建一个 SpringBoot 项目,注意勾选上 web 模块。不会创建的话,可以查看下面这篇文章:,可以说很详细了。https://blog.csdn.net/qq_34337272/article/details/795636062. pom 文件引入相关依赖需要引入 dubbo 、zookeeper以及接口的相关依赖 jar 包。注意将本项目和 dubbo-interface 项目的 dependency 依赖的 groupId 和 artifactId 改成自己的。dubbo 整合spring boot 的 jar 包在这里找dubbo-spring-boot-starter。zookeeper 的 jar包在 Maven 仓库 搜索 zkclient 即可找到。 <dependency> <groupId>top.snailclimb</groupId> <artifactId>dubbo-interface</artifactId> <version>1.0-SNAPSHOT</version> </dependency> <!–引入dubbo的依赖–> <dependency> <groupId>com.alibaba.spring.boot</groupId> <artifactId>dubbo-spring-boot-starter</artifactId> <version>2.0.0</version> </dependency> <!– 引入zookeeper的依赖 –> <dependency> <groupId>com.101tec</groupId> <artifactId>zkclient</artifactId> <version>0.10</version> </dependency>3. 在 application.properties 配置文件中配置 dubbo 相关信息配置很简单,这主要得益于 springboot 整合 dubbo 专属的@EnableDubboConfiguration 注解提供的 Dubbo 自动配置。# 配置端口server.port=8333spring.dubbo.application.name=dubbo-providerspring.dubbo.application.registry=zookeeper://ip地址:21814. 实现接口注意: @Service 注解使用的时 Dubbo 提供的而不是 Spring 提供的。另外,加了Dubbo 提供的 @Service 注解之后还需要加入package top.snailclimb.service.impl;import com.alibaba.dubbo.config.annotation.Service;import org.springframework.stereotype.Component;import top.snailclimb.service.HelloService;@Component@Servicepublic class HelloServiceImpl implements HelloService { @Override public String sayHello(String name) { return “Hello " + name; }}5. 服务提供者启动类编写注意:不要忘记加上 @EnableDubboConfiguration 注解开启Dubbo 的自动配置。package top.snailclimb;import com.alibaba.dubbo.spring.boot.annotation.EnableDubboConfiguration;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;@SpringBootApplication// 开启dubbo的自动配置@EnableDubboConfigurationpublic class DubboProviderApplication { public static void main(String[] args) { SpringApplication.run(DubboProviderApplication.class, args); }}开始实战 4 :实现服务消费者 dubbo-consumer主要分为下面几步:创建 springboot 项目;加入 dubbo 、zookeeper以及接口的相关依赖 jar 包;在 application.properties 配置文件中配置 dubbo 相关信息;编写测试类;服务消费者启动类编写测试效果项目结构:第1,2,3 步和服务提供者的一样,这里直接从第 4 步开始。4. 编写一个简单 Controller 调用远程服务package top.snailclimb.dubboconsumer;import com.alibaba.dubbo.config.annotation.Reference;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;import top.snailclimb.service.HelloService;@RestControllerpublic class HelloController { @Reference private HelloService helloService; @RequestMapping("/hello”) public String hello() { String hello = helloService.sayHello(“world”); System.out.println(helloService.sayHello(“SnailClimb”)); return hello; }}5. 服务消费者启动类编写package top.snailclimb.dubboconsumer;import com.alibaba.dubbo.spring.boot.annotation.EnableDubboConfiguration;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;@SpringBootApplication@EnableDubboConfigurationpublic class DubboConsumerApplication { public static void main(String[] args) { SpringApplication.run(DubboConsumerApplication.class, args); }}6. 测试效果浏览器访问 http://localhost:8330/hello 页面返回 Hello world,控制台输出 Hello SnailClimb,和预期一直,使用SpringBoot+Dubbo 搭建第一个简单的分布式服务实验成功! ...

November 28, 2018 · 2 min · jiezi

spring boot 结合Redis 实现工具类

自己整理了 spring boot 结合 Redis 的工具类引入依赖<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId></dependency>加入配置# Redis数据库索引(默认为0)spring.redis.database=0# Redis服务器地址spring.redis.host=localhost# Redis服务器连接端口spring.redis.port=6379实现代码这里用到了 静态类工具类中 如何使用 @Autowiredpackage com.lmxdawn.api.common.utils;import java.util.Collection;import java.util.Set;import java.util.concurrent.TimeUnit;import javax.annotation.PostConstruct;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.data.redis.core.RedisTemplate;import org.springframework.stereotype.Component;/** * 缓存操作类 /@Componentpublic class CacheUtils { @Autowired private RedisTemplate<String, String> redisTemplate; // 维护一个本类的静态变量 private static CacheUtils cacheUtils; @PostConstruct public void init() { cacheUtils = this; cacheUtils.redisTemplate = this.redisTemplate; } /* * 将参数中的字符串值设置为键的值,不设置过期时间 * @param key * @param value 必须要实现 Serializable 接口 / public static void set(String key, String value) { cacheUtils.redisTemplate.opsForValue().set(key, value); } /* * 将参数中的字符串值设置为键的值,设置过期时间 * @param key * @param value 必须要实现 Serializable 接口 * @param timeout / public static void set(String key, String value, Long timeout) { cacheUtils.redisTemplate.opsForValue().set(key, value, timeout, TimeUnit.SECONDS); } /* * 获取与指定键相关的值 * @param key * @return / public static Object get(String key) { return cacheUtils.redisTemplate.opsForValue().get(key); } /* * 设置某个键的过期时间 * @param key 键值 * @param ttl 过期秒数 / public static boolean expire(String key, Long ttl) { return cacheUtils.redisTemplate.expire(key, ttl, TimeUnit.SECONDS); } /* * 判断某个键是否存在 * @param key 键值 / public static boolean hasKey(String key) { return cacheUtils.redisTemplate.hasKey(key); } /* * 向集合添加元素 * @param key * @param value * @return 返回值为设置成功的value数 / public static Long sAdd(String key, String… value) { return cacheUtils.redisTemplate.opsForSet().add(key, value); } /* * 获取集合中的某个元素 * @param key * @return 返回值为redis中键值为key的value的Set集合 / public static Set<String> sGetMembers(String key) { return cacheUtils.redisTemplate.opsForSet().members(key); } /* * 将给定分数的指定成员添加到键中存储的排序集合中 * @param key * @param value * @param score * @return / public static Boolean zAdd(String key, String value, double score) { return cacheUtils.redisTemplate.opsForZSet().add(key, value, score); } /* * 返回指定排序集中给定成员的分数 * @param key * @param value * @return / public static Double zScore(String key, String value) { return cacheUtils.redisTemplate.opsForZSet().score(key, value); } /* * 删除指定的键 * @param key * @return / public static Boolean delete(String key) { return cacheUtils.redisTemplate.delete(key); } /* * 删除多个键 * @param keys * @return */ public static Long delete(Collection<String> keys) { return cacheUtils.redisTemplate.delete(keys); }}相关地址GitHub 地址: https://github.com/lmxdawn/vu… ...

November 24, 2018 · 2 min · jiezi

spring boot 利用注解实现权限验证

这里使用 aop 来实现权限验证引入依赖<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId></dependency>定义注解package com.lmxdawn.api.admin.annotation;import java.lang.annotation.ElementType;import java.lang.annotation.Retention;import java.lang.annotation.RetentionPolicy;import java.lang.annotation.Target;/** * 后台登录授权/权限验证的注解 ///此注解只能修饰方法@Target(ElementType.METHOD)//当前注解如何去保持@Retention(RetentionPolicy.RUNTIME)public @interface AuthRuleAnnotation { String value();}拦截实现登录和权限验证package com.lmxdawn.api.admin.aspect;import com.lmxdawn.api.admin.annotation.AuthRuleAnnotation;import com.lmxdawn.api.admin.enums.ResultEnum;import com.lmxdawn.api.admin.exception.JsonException;import com.lmxdawn.api.admin.service.auth.AuthLoginService;import com.lmxdawn.api.common.utils.JwtUtils;import io.jsonwebtoken.Claims;import lombok.extern.slf4j.Slf4j;import org.aspectj.lang.JoinPoint;import org.aspectj.lang.annotation.Aspect;import org.aspectj.lang.annotation.Before;import org.aspectj.lang.annotation.Pointcut;import org.aspectj.lang.reflect.MethodSignature;import org.springframework.stereotype.Component;import org.springframework.web.context.request.RequestContextHolder;import org.springframework.web.context.request.ServletRequestAttributes;import javax.annotation.Resource;import javax.servlet.http.HttpServletRequest;import java.lang.reflect.Method;import java.util.List;/* * 登录验证 AOP /@Aspect@Component@Slf4jpublic class AuthorizeAspect { @Resource private AuthLoginService authLoginService; @Pointcut("@annotation(com.lmxdawn.api.admin.annotation.AuthRuleAnnotation)") public void adminLoginVerify() { } /* * 登录验证 * * @param joinPoint / @Before(“adminLoginVerify()”) public void doAdminAuthVerify(JoinPoint joinPoint) { ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); if (attributes == null) { throw new JsonException(ResultEnum.NOT_NETWORK); } HttpServletRequest request = attributes.getRequest(); String id = request.getHeader(“X-Adminid”); Long adminId = Long.valueOf(id); String token = request.getHeader(“X-Token”); if (token == null) { throw new JsonException(ResultEnum.LOGIN_VERIFY_FALL); } // 验证 token Claims claims = JwtUtils.parse(token); if (claims == null) { throw new JsonException(ResultEnum.LOGIN_VERIFY_FALL); } Long jwtAdminId = Long.valueOf(claims.get(“admin_id”).toString()); if (adminId.compareTo(jwtAdminId) != 0) { throw new JsonException(ResultEnum.LOGIN_VERIFY_FALL); } // 判断是否进行权限验证 MethodSignature signature = (MethodSignature) joinPoint.getSignature(); //从切面中获取当前方法 Method method = signature.getMethod(); //得到了方,提取出他的注解 AuthRuleAnnotation action = method.getAnnotation(AuthRuleAnnotation.class); // 进行权限验证 authRuleVerify(action.value(), adminId); } /* * 权限验证 * * @param authRule / private void authRuleVerify(String authRule, Long adminId) { if (authRule != null && authRule.length() > 0) { List<String> authRules = authLoginService.listRuleByAdminId(adminId); // admin 为最高权限 for (String item : authRules) { if (item.equals(“admin”) || item.equals(authRule)) { return; } } throw new JsonException(ResultEnum.AUTH_FAILED); } }}Controller 中使用使用 AuthRuleAnnotation 注解, value 值就是在数据库里面定义的 权限规则名称/* * 获取管理员列表 */@AuthRuleAnnotation(“admin/auth/admin/index”)@GetMapping("/admin/auth/admin/index")public ResultVO index(@Valid AuthAdminQueryForm authAdminQueryForm, BindingResult bindingResult) { if (bindingResult.hasErrors()) { return ResultVOUtils.error(ResultEnum.PARAM_VERIFY_FALL, bindingResult.getFieldError().getDefaultMessage()); } if (authAdminQueryForm.getRoleId() != null) { List<AuthRoleAdmin> authRoleAdmins = authRoleAdminService.listByRoleId(authAdminQueryForm.getRoleId()); List<Long> ids = new ArrayList<>(); if (authRoleAdmins != null && !authRoleAdmins.isEmpty()) { ids = authRoleAdmins.stream().map(AuthRoleAdmin::getAdminId).collect(Collectors.toList()); } authAdminQueryForm.setIds(ids); } List<AuthAdmin> authAdminList = authAdminService.listAdminPage(authAdminQueryForm); // 查询所有的权限 List<Long> adminIds = authAdminList.stream().map(AuthAdmin::getId).collect(Collectors.toList()); List<AuthRoleAdmin> authRoleAdminList = authRoleAdminService.listByAdminIdIn(adminIds); // 视图列表 List<AuthAdminVo> authAdminVoList = authAdminList.stream().map(item -> { AuthAdminVo authAdminVo = new AuthAdminVo(); BeanUtils.copyProperties(item, authAdminVo); List<Long> roles = authRoleAdminList.stream() .filter(authRoleAdmin -> authAdminVo.getId().equals(authRoleAdmin.getAdminId())) .map(AuthRoleAdmin::getRoleId) .collect(Collectors.toList()); authAdminVo.setRoles(roles); return authAdminVo; }).collect(Collectors.toList()); PageInfo<AuthAdmin> authAdminPageInfo = new PageInfo<>(authAdminList); PageSimpleVO<AuthAdminVo> authAdminPageSimpleVO = new PageSimpleVO<>(); authAdminPageSimpleVO.setTotal(authAdminPageInfo.getTotal()); authAdminPageSimpleVO.setList(authAdminVoList); return ResultVOUtils.success(authAdminPageSimpleVO);}相关地址GitHub 地址: https://github.com/lmxdawn/vu… ...

November 24, 2018 · 2 min · jiezi

使用Docker部署Spring-Boot+Vue博客系统

在今年年初的时候,完成了自己的个Fame博客系统的实现,当时也做了一篇博文Spring-boot+Vue = Fame 写blog的一次小结作为记录和介绍。从完成实现到现在,也断断续续的根据实际的使用情况进行更新。只不过每次上线部署的时候都觉得有些麻烦,因为我的服务器内存太小,每次即使只更新了前台部分(fame-front)的代码,在执行npm build的时候都还必须把我的后端服务(fame-server)的进程关掉,不然会造成服务器卡死(惨啊)。而且这个项目是前后端分离的,博客前台页面还为了SEO用了Nuxt框架,假如是第一次部署或者要服务器迁移的话,麻烦的要死啊,部署一次的话要以下步骤安装mysql,修改相关配置文件,设置编码时区等,然后重启下载安装java,配置java环境下载安装maven,配置maven环境下载安装nginx,修改配置文件,设计反向代理等启动spring-boot项目打包vue项目,npm install,npm run build等启动nuxt项目,npm install,npm run start等如果能够顺利的完成这七个步骤算是幸运儿了,假如中间哪个步骤报错出了问题,可能还要回头查找哪个步骤出了问题,然后又重新部署。在这些需求面前,Docker就是解决这些问题的大杀器。无论是其虚拟化技术隔离各个容器使其资源互不影响,还是一致的运行环境,以及docker-compose的一键部署,都完美的解决了上述问题。项目地址:FameDocker和Docker-compose安装Docker和Docker-compose的功能和使用可以看线上的一个中文文档Docker — 从入门到实践下面是Centos7安装和配置Docker以及Docker-compose的shell脚本,其他操作系统可以参考修改来安装。其中Docker版本为docker-ce,Docker-compose版本为1.22.0#!/bin/sh### 更新 ###yum -y update### 安装docker #### 安装一些必要的系统工具sudo yum install -y yum-utils device-mapper-persistent-data lvm2# 添加软件源信息sudo yum-config-manager –add-repo http://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo# 更新 yum 缓存sudo yum makecache fast# 安装 Docker-cesudo yum -y install docker-ce# 启动docker并设置为开机启动(centos7)systemctl start docker.servicesystemctl enable docker.service# 替换docker为国内源echo ‘{“registry-mirrors”: [“https://registry.docker-cn.com”],“live-restore”: true}’ > /etc/docker/daemon.jsonsystemctl restart docker# 安装dokcer-composesudo curl -L https://github.com/docker/compose/releases/download/1.22.0/docker-compose-`uname -s-uname -m` -o /usr/local/bin/docker-composechmod +x /usr/local/bin/docker-compose# 安装命令补全工具yum -y install bash-completioncurl -L https://raw.githubusercontent.com/docker/compose/$(docker-compose version –short)/contrib/completion/bash/docker-compose > /etc/bash_completion.d/docker-compose### 安装docker结束 ###Docker化改造改造后目录结构先看一下改造后的项目的结构├─Fame│ │ .env // docker-compose环境参数配置文件│ │ docker-compose.yml // docker-compose文件│ ├─fame-docker│ │ │ fame-front-Dockerfile // fame-front的Dockerfile文件│ │ │ fame-server-Dockerfile // fame-server的Dockerfile文件│ │ │ │ │ ├─fame-admin│ │ │ fame-admin-Dockerfile // fame-admin的Dockerfile文件│ │ │ nginx.conf // fame-admin的nginx服务器配置文件│ │ │ │ │ ├─fame-mysql│ │ │ fame-mysql-Dockerfile // mysql的Dockerfile文件│ │ │ mysqld.cnf // mysql的配置文件mysqld.cnf│ │ │ │ │ └─fame-nginx│ │ nginx-Dockerfile // 整个项目的nginx服务器的Dockerfile文件│ │ nginx.conf // 整个项目的nginx的配置文件│ │ │ ├─fame-admin // 博客管理后台,基于Vue+elementui│ ├─fame-front // 博客前端,基于Nuxt│ └─fame-server // 博客服务端,基于spring-boot为了不破坏原有项目的结构,无论前端还是后端的docker的相关配置文件全部提取出来,单独放在了fame-docker文件夹中。docker-compose.yml放在项目根目录下,直接在根目录运行命令:docker-compose up -d[root@localhost Fame]# docker-compose up -dStarting fame-front … Starting fame-admin … Starting fame-front … doneStarting fame-admin … doneStarting fame-nginx … done就启动项目了,再也不用重复繁琐的步骤!改造后的docker项目结构改造后的docker-compose.yaml文件version: ‘3’services: fame-nginx: container_name: fame-nginx build: context: ./ dockerfile: ./fame-docker/fame-nginx/nginx-Dockerfile ports: - “80:80” volumes: - ./logs/nginx:/var/log/nginx depends_on: - fame-server - fame-admin - fame-front fame-mysql: container_name: fame-mysql build: context: ./ dockerfile: ./fame-docker/fame-mysql/fame-mysql-Dockerfile environment: MYSQL_DATABASE: fame MYSQL_ROOT_PASSWORD: root MYSQL_ROOT_HOST: ‘%’ TZ: Asia/Shanghai expose: - “3306” volumes: - ./mysql/mysql_data:/var/lib/mysql restart: always fame-server: container_name: fame-server restart: always build: context: ./ dockerfile: ./fame-docker/fame-server-Dockerfile working_dir: /app volumes: - ./fame-server:/app - ~/.m2:/root/.m2 - ./logs/fame:/app/log expose: - “9090” command: mvn clean spring-boot:run -Dspring-boot.run.profiles=docker -Dmaven.test.skip=true depends_on: - fame-mysql fame-admin: container_name: fame-admin build: context: ./ dockerfile: ./fame-docker/fame-admin/fame-admin-Dockerfile args: BASE_URL: ${BASE_URL} expose: - “3001” fame-front: container_name: fame-front build: context: ./ dockerfile: ./fame-docker/fame-front-Dockerfile environment: BASE_URL: ${BASE_URL} PROXY_HOST: ${PROXY_HOST} PROXY_PORT: ${PROXY_PORT} expose: - “3000"docker-compose.yml的结构和刚才目录结构大体类似,也是分以下几个部分fame-nginxfame-mysqlfame-serverfame-adminfame-front这个docker-compose.yml中有几个要点fame-mysql和fame-server的restart要设置为always,因为目前Docker-compose是没有一个方案可以解决容器启动的先后的问题的。即使设置了depends_on,那也只是控制容器开始启动的时间,不能控制容器启动完成的时间,所以让fame-mysql和fame-server这两个容器设置restart,防止spring-boot在mysql启动完成之前启动而报错启动失败fame-server,fame-mysql,fame-nginx这三个容器都设置了volumes,把容器里的logs日志文件挂载到宿主机的项目目录里,方便随时看日志文件fame-mysql容器的mysql存储文件也设置了volumes挂载在项目目录里(./mysql/mysql_data:/var/lib/mysql),这个建议大家可以根据实际的情况设置到宿主机的其他目录里,不然不小心删除项目的话那么容器里的数据库数据也都没了几个镜像的Dockerfile大部分都比较简单,这部分就不全部详细介绍了,可以直接去我项目中了解。Docker化过程的困难和解决方法spring-boot双配置切换为了能够让spring-boot能够在开发环境和Docker环境下快速切换,需要将spring-boot的配置文件进行修改└─fame-server … │ └─resources │ │ application-dev.properties │ │ application-docker.properties │ │ application.properties在原有的application.properties基础上增加application-dev.properties和application-docker.properties配置文件,把application.properties里的数据库日志等信息分别放到application-dev.properties和application-docker.properties这两个文件中,实现开发环境和Docker环境的快速切换。# application.properties文件#端口号server.port=9090#mybatismybatis.type-aliases-package=com.zbw.fame.Model#mappermapper.mappers=com.zbw.fame.util.MyMappermapper.not-empty=falsemapper.identity=MYSQL#mailspring.mail.properties.mail.smtp.auth=truespring.mail.properties.mail.smtp.starttls.enable=truespring.mail.properties.mail.smtp.starttls.required=true#默认propertiesspring.profiles.active=dev# application-docker.properties文件#datasourcespring.datasource.driverClassName=com.mysql.jdbc.Driverspring.datasource.url=jdbc:mysql://fame-mysql:3306/fame?useUnicode=true&characterEncoding=utf-8&useSSL=falsespring.datasource.username=rootspring.datasource.password=root#loglogging.level.root=INFOlogging.level.org.springframework.web=INFOlogging.file=log/fame.logapplication-dev.properties的内容和application-docker.properties文件类似,只是根据自己开发环境的情况修改mysql和log配置。动态配置axios的baseUrl地址在fame-admin和fame-front中用了axios插件,用于发起和获取fame-server服务器的请求。在axios要配置服务器url地址baseUrl,那么通常开发环境和Docker环境以及生产环境的url可能都不一样,每次都去修改有点麻烦。(虽然只需要配置两处,但是代码洁癖不允许我硬编码这个配置)。先修改fame-admin(Vue)使其兼容手动部署模式和Docker模式fame-admin是基于Vue CLI 3搭建的,相对于cli 2.0官方把webpack的一些配置文件都封装起来了,所以没有config和build文件夹。不过对应的官网也给了一些设置更加方便的配置参数。在官方文档中提到:只有以 VUE_APP_ 开头的变量会被 webpack.DefinePlugin 静态嵌入到客户端侧的包中。你可以在应用的代码中这样访问它们:console.log(process.env.VUE_APP_SECRET)在构建过程中,process.env.VUE_APP_SECRET 将会被相应的值所取代。在 VUE_APP_SECRET=secret 的情况下,它会被替换为 “sercet”。利用这个特性来设置环境变量来动态的设置Docker模式和手动部署模式的baseUrl的值在fame-admin目录下创建文件server-config.js,编写以下内容const isProd = process.env.NODE_ENV === ‘production’const localhost = ‘http://127.0.0.1:9090/‘const baseUrl = process.env.VUE_APP_API_URL || localhostconst api = isProd ? baseUrl : localhostexport default { isProd, api}那么只要在环境变量中有VUE_APP_API_URL的值,且NODE_ENV === ‘production’,baseUrl就等于VUE_APP_API_URL的值,否则就是localhost的值。接着在axios配置文件中引用该文件设置// fame-admin/src/plugins/http.js…import serverConfig from ‘../../server-config’const Axios = axios.create({ baseURL: serverConfig.api + ‘api/’, …}) …现在只要将docker的环境变量设置一个VUE_APP_API_URL的值就行了,只要在对应的Dockerfile中增加一个步骤就可以了。ENV VUE_APP_API_URL http://xx.xxx.xxx.xxx再修改fame-front(Nuxt)使其兼容手动部署模式和Docker模式同样的,对于用Nuxt搭建fame-front博客前台修改也是类似的思路。在Nuxt的官方文档中写到:Nuxt.js 让你可以配置在客户端和服务端共享的环境变量。例如 (nuxt.config.js):module.exports = { env: { baseUrl: process.env.BASE_URL || ‘http://localhost:3000’ }}以上配置我们创建了一个 baseUrl 环境变量,如果应用设定了 BASE_URL 环境变量,那么 baseUrl 的值等于 BASE_URL 的值,否则其值为 http://localhost:3000。所以我们只要和官方文档说的一样,在nuxt.config.js文件中增加代码就可以了module.exports = { env: { baseUrl: process.env.BASE_URL || ‘http://localhost:3000’ }}接着在server-config.js文件和axios的配置文件fame-front/plugins/http.js以及对应的Dockerfile文件中编写和上面fame-admin部分一样的代码就可以了现在已经把baseUrl的设置从代码的硬编码中解放出来了,但事实上我们只是把这个参数的编码从代码从转移到Dockerfile文件里了,要是想要修改的话也要去这两个文件里查找然后修改,这样也不方便。后面会解决这个问题把所有环境配置统一起来。Nuxt在Docker中无法访问到宿主机ip问题先要说明一点,为什么博客前端要单独去使用的Nuxt而不是和博客后台一样用Vue呢,因为博客前端有SEO的需求的,像Vue这样的对搜索引擎很不友好。所以Nuxt的页面是服务器端渲染(SSR)的这样就产生了问题fame-front的页面在渲染之前必须获取到fame-server服务器中的数据,但是每个docker容器都是互相独立的,其内部想要互相访问只能通过容器名访问。例如容器fame-front想要访问容器fame-server,就设置baseURL = fame-server (fame-server是服务器的容器的container_name)。这样设置之后打开浏览器输入网址:http://xx.xxx.xxx.xx可以成功…,但是随便点击一个链接,就会看到浏览器提示错误无法访问到地址http://fame-server/…vendor.e2feb665ef91f298be86.js:2 GET http://fame-server/api/article/1 net::ERR_CONNECTION_REFUSED这是必然的结果,在容器里http://fame-server/就是服务器…,但是你本地的浏览器当然是不知道http://fame-server/是个什么鬼…,所以就浏览器就报出无法访问的错误。什么?可是刚才不是说Nuxt是服务器渲染的页面吗,怎么又让本地浏览器报这个错误了。原来是因为当通过浏览器链接直接访问的时候,Nuxt的确是从后端渲染了页面再传过来,但是在页面中点击链接的时候是通过Vue-Router跳转的,这时候不在Nuxt的控制范围,而是和Vue一样在浏览器渲染的,这时候就要从浏览器里向服务端获取数据来渲染,浏览器就会报错。如何解决呢这个问题开始的时候一直想要尝试配置Docker容器的网络模式来解决,可是都没有解决。直到后面我看axios文档的时候才注意到axios的代理功能,其本质是解决跨域的问题的,因为只要在axios设置了代理,在服务端渲染的时候就会使用代理的地址,同时在浏览器访问的时候会用baseUrl 的地址,这个特点完美解决我的问题啊。在server-config.js文件里增加以下代码(在nuxt.config.js里获取环境变量里的proxyHost和proxyPort)…const localProxy = { host: ‘127.0.0.1’, port: 9090}const baseProxy = { host: process.env.proxyHost || localProxy.host, port: process.env.proxyPort || localProxy.port}exports.baseProxy = isProd ? baseProxy : localProxy…然后在axios配置文件里增加代码// fame-front/plugins/http.jsconst Axios = axios.create({ proxy: serverConfig.baseProxy …}) …就可以完美的解决问题了。Dockerfile的环境参数统一设置在上文解决动态配置axios地址的部分把baseUrl的设置放在了Dockerfile中,现在就再把Dockerfile中的硬编码提取出来,放到统一的配置文件中。首先在docker-compose.yml文件目录下(即项目跟目录)创建环境文件.env并编写一下内容BASE_URL=http://xx.xxx.xxx.xxxPROXY_HOST=fame-nginxPROXY_PORT=80这个是docker-compose的env_file参数,从文件中获取环境变量,可以为单独的文件路径或列表,如果同目录下有.env文件则会默认读取,也可以自己在docker-compose里设置路径。已经在.env设置了环境变量BASE_URL的值,就能在docker-compose.yml里直接使用了。修改docker-compose.yml的fame-front部分:fame-front: … environment: BASE_URL: ${BASE_URL} PROXY_HOST: ${PROXY_HOST} PROXY_PORT: ${PROXY_PORT} …这样在fame-front的容器里就有对应的BASE_URL,PROXY_HOST,PROXY_PORT环境变量,Nuxt也能够成功获取并设置。不过对于fame-admin容器来说就要稍微复杂一点点了。先来看一下fame-admin容器的Dockerfile文件fame-admin-Dockerfile# build stageFROM node:10.10.0-alpine as build-stage#中间一些操作省略…RUN npm run build# production stageFROM nginx:1.15.3-alpine as production-stageCOPY ./fame-docker/fame-admin/nginx.conf /etc/nginx/conf.d/default.confCOPY –from=build-stage /app/dist /usr/share/nginx/htmlEXPOSE 80CMD [“nginx”, “-g”, “daemon off;"]这里用了多阶段构建容器,如果直接通过docker-compose设置环境变量只会在后面一个阶段生效,但是npm run build是在第一个阶段执行的,所以环境变量不能应用到Vue当中。为了让环境变量在第一阶段就应用,必须要在构建的时候就把变量从docker-compose传到fame-admin-Dockerfile中,然后在Dockerfile中的第一阶段把这个环境变量应用到容器里。下面修改docker-compose.yml的fame-admin部分: fame-admin: … build: context: ./ dockerfile: ./fame-docker/fame-admin/fame-admin-Dockerfile args: BASE_URL: ${BASE_URL} # 这里把环境变量当做ARG传给Dockerfile …然后在fame-admin-Dockerfile的第一阶段增加步骤# build stageFROM node:10.10.0-alpine as build-stageARG BASE_URL # 必须申明这个ARG才能从docker-compose里获取ENV VUE_APP_API_URL $BASE_URL# 以下省略…这样就可以在构建阶段一镜像的时候就把环境变量传入到阶段一的镜像里,让Vue里的变量生效了。总结现在网上很多复杂一点的项目即使用了docker-compose部署,也多少依赖shell脚本来操作,比如复制文件设置环境等,我觉得这样会降低docker-compose的意义。如果都使用了shell脚本,那不如直接不用docker-compose而全用shell来构建和启动镜像。所以在Docker化的过程中虽然遇到一些坎坷,但坚持实现了只用docker-compose部署,以后上线和下线就及其方便了。也希望我的Docker化思路可以给其他项目做一些参考。对比以前恐怖的步骤,现在Fame博客的上线和下线只需要两行命令,真的十分的便捷。docker-compose updocker-compose down源码地址:doodle原文地址:使用Docker部署Spring-Boot+Vue博客系统 ...

September 29, 2018 · 3 min · jiezi